补充遥调业务代码
CI / L0 静态门禁 (pre-commit) (push) Failing after 45s
CI / L0.5 主机侧单测 (MSVC) (push) Failing after 43s

This commit is contained in:
STF
2026-11-01 00:38:09 -07:00
parent cd42b4d61d
commit e1b0434774
233 changed files with 292584 additions and 268 deletions
+175 -33
View File
@@ -1,5 +1,5 @@
/*
* 104子站示例:总召、计数量召唤、时钟同步、45/46遥控、模拟点表及main。
* 104子站示例:总召、计数量召唤、时钟同步、45/46/49及58/59/62命令、模拟点表及main。
* 两种召唤共用IEC104_Interrogation.c状态机,各自有独立实例和业务事件。
* 每轮Slave_Poll -> NextEvent逐条交付 -> Command_Process及应用交互 -> 召唤/校时;所有调用串行。
* 总召每批4点,模块自动推进至需要等待;8个单点遥信、8个双点遥信和8个遥测组成8个响应ASDU。
@@ -8,7 +8,7 @@
* 总召模块处理WAIT、STOPDT及断线;应用读取业务事件,选择类型并显式交付ASDU。
* 计数量使用8个模拟计数器;读取快照,冻结/复位由经过事件校验的同步动作入口执行。
* 对时默认只更新模拟UTC基准;--clock system显式选择实际时钟,协议仍使用单调时钟。
* 遥控默认先选择后执行,同一实例只占一个事务;设备结果、返回信息和Complete分步推进,WAIT保留当前步骤。
* 每组命令前7点先选择后执行,末点直接执行;同一实例只占一个事务,结果/返回信息/Complete分步推进。
* 模拟输出同步更新点值;FINISHED只清理返校/输出标志,不恢复已执行的点值,也不代表真实设备已经空闲。
* 不支持的其他命令当场提交COT=44否定应答,复用发送层HIGH;容量不足只丢本条并计数。
* 默认127.0.0.1:2404、CA=1;构建命令见同目录README.md。
@@ -17,6 +17,7 @@
#include "IEC104_CounterInterrogation.h"
#include "IEC104_ClockSynchronization.h"
#include "IEC104_Command.h"
#include "IEC104_Net.h"
#include "IEC60870_Clock.h"
#include <stdio.h>
#include <stdlib.h>
@@ -24,6 +25,9 @@
#define EXAMPLE_EVENT_BUDGET 32U
#define EXAMPLE_COUNTER_COUNT 8U
#define EXAMPLE_SCALED_MIN (-10000)
#define EXAMPLE_SCALED_MAX 10000
#define EXAMPLE_CLOCK_REFRESH_MS 60000U
/* 模拟遥信点表:ioa为24位信息对象地址,value为0/1,quality为SIQ品质位(这里均为0)。
* 表项不是线上报文,后续由SinglePointInformation_Create及ASDU容器编码成Type=1。
@@ -41,7 +45,15 @@ static const struct { uint32_t ioa; int16_t value; uint8_t quality; } example_me
/* 点数直接取数组长度;分批游标使用size_t,不另维护可能与点表不一致的手写数量。 */
#define EXAMPLE_SIGNAL_COUNT (sizeof(example_signals) / sizeof(example_signals[0]))
#define EXAMPLE_MEASUREMENT_COUNT (sizeof(example_measurements) / sizeof(example_measurements[0]))
#define EXAMPLE_COMMAND_COUNT (2U * EXAMPLE_SIGNAL_COUNT + EXAMPLE_MEASUREMENT_COUNT)
/* 应用运行值的地址映射;命令许可和执行方式在Example_Init_Command中逐点明确配置。
* 基础与带时标类型共用同一物理点;此表仅用于查找运行值,不生成命令点策略。 */
static const struct { uint32_t first_ioa; size_t count; uint16_t type_mask; } example_command_groups[] = {
{6001U, EXAMPLE_SIGNAL_COUNT, IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(58U)},
{6101U, EXAMPLE_SIGNAL_COUNT, IEC104_COMMAND_TYPE_BIT(46U) | IEC104_COMMAND_TYPE_BIT(59U)},
{6301U, EXAMPLE_MEASUREMENT_COUNT, IEC104_COMMAND_TYPE_BIT(49U) | IEC104_COMMAND_TYPE_BIT(62U)}
};
/* 当前取数站的应用游标;首个NEED_DATA才初始化,协议库不登记或冻结业务IOA列表。 */
typedef struct { size_t single_index, double_index, scaled_index; } ExampleGiData;
@@ -56,6 +68,7 @@ typedef struct {
IEC104_ClockSynchronization_t *clock; /* 独立时钟同步模块,共用Slave及站点表。 */
IEC104_ClockSynchronizationStatus_t cs; /* 对时执行、确认及期限的只读快照。 */
IEC104_Command_t *command; /* CLI启用;仅总召的旧测试可以不创建遥控实例。 */
IEC104_CommandPoint_t command_points[EXAMPLE_COMMAND_COUNT]; /* Create前填完,直到Destroy均只读且地址稳定。 */
IEC104_CommandStatus_t command_status; /* 推进后/等待前刷新;计时、通知和应答状态只供调度及统计。 */
IEC104_CommandEvent_t command_event; /* 工作事件可跨WAIT保留,执行收尾继续使用原EXECUTE身份。 */
IEC104_Station_t *command_station; /* 应用额外引用:首个工作事件先Acquire,再保存context;FINISHED后Release。 */
@@ -65,12 +78,14 @@ typedef struct {
bool command_pending, command_waiting, command_selected, command_output;
unsigned command_step; /* 0等待事件/终结,1报告设备结果,2返回信息,3关闭返回阶段。 */
IEC60870_Asdu_t command_return; /* 借用下面的固定缓冲;WAIT未接手,必须连同事件保留到OK或STALE。 */
uint8_t command_return_bytes[10]; /* 单个Type1/3对象,WAIT保持原文,不借用总召缓冲。 */
uint8_t command_return_bytes[12]; /* 单个Type1/3/11对象,WAIT保持原文,不借用总召缓冲。 */
/* 单点及双点运行值由遥控更新,总召与遥信分组均读取同一数组的当前值。
* 单点命令6001..6008对应遥信1001..1008,双点命令6101..6108对应遥信1101..1108;FINISHED不重置运行值。 */
uint8_t signal_values[EXAMPLE_SIGNAL_COUNT], double_values[EXAMPLE_SIGNAL_COUNT];
bool clock_system, clock_valid; /* 实际设时为显式选项;clock_valid只描述模拟基准。 */
uint64_t clock_utc_ms, clock_base_ms; /* 模拟UTC毫秒及其单调基准,不影响协议计时。 */
int16_t scaled_values[EXAMPLE_MEASUREMENT_COUNT]; /* 49/62设定值与总召共用的运行值,初值来自遥测表。 */
bool clock_system, clock_valid, clock_update_pending; /* 仅成功校时置pending,回调外才更新Command基准。 */
uint64_t clock_utc_ms, clock_base_ms; /* UTC毫秒及同一时刻的Net单调基准,不影响协议计时。 */
uint64_t clock_refresh_ms; /* system模式最近一次读时尝试;失败也计时,避免每轮忙刷。 */
IEC104_InterrogationEvent_t counter_event; /* WAIT时保留的原业务事件,重试不能换成新身份。 */
bool counter_pending, counter_first; /* 是否持有待应答事件、本轮是否先推进计数量。 */
int32_t counter_current[EXAMPLE_COUNTER_COUNT]; /* 模拟当前累计值,冻结及复位动作的数据源。 */
@@ -148,7 +163,7 @@ static int Example_Build_Batch(ExampleApp *app, const IEC104_InterrogationReques
IEC60870_MeasuredValueScaled_t point;
IEC60870_AsduResult_t added;
if (IEC60870_MeasuredValueScaled_Create(&point, example_measurements[index].ioa,
example_measurements[index].value, example_measurements[index].quality) != IEC60870_ASDU_OK) {
app->scaled_values[index], example_measurements[index].quality) != IEC60870_ASDU_OK) {
return -1;
}
added = IEC60870_Asdu_Add_InformationObject(asdu, &point.object);
@@ -391,6 +406,9 @@ static bool Example_Init(ExampleApp *app, IEC104_Slave_t *slave, uint16_t common
app->signal_values[index] = example_signals[index].value;
app->double_values[index] = 1U;
}
for (index = 0U; index < EXAMPLE_MEASUREMENT_COUNT; ++index) {
app->scaled_values[index] = example_measurements[index].value;
}
if (IEC104_Slave_Add_Station(slave, &app->station, common_address, app) != IEC104_STATION_OK) { return false; }
IEC104_Interrogation_Config_Default(&config);
config.use_station_registry = true; /* CA只在注册时指定;多站广播需提供每站状态区。 */
@@ -426,14 +444,50 @@ static bool Example_Init_Counters(ExampleApp *app,
return true;
}
/* 执行检查已完成时区换算和滞留补偿;动作不重复补偿,也不推进网络事件循环。 */
/* system模式启动时及每60秒刷新。Clock_Read给出固定UTC+8标准日历,不能使用主站时区或SU。
* 读时失败、IV或换算失败立即清除基准;此处只读系统时钟,不触发设时。
* 在分派新命令前调用,已有事务仍按Command的受理时效契约推进。 */
static void Example_Refresh_System_Clock(ExampleApp *app, bool force)
{
IEC60870_ClockValue_t value;
IEC60870_CP56Time2a_t time;
uint32_t seconds;
uint64_t now, base_ms, utc_ms;
if (!app->clock_system) { return; }
now = IEC104_Net_Now_Ms();
if (!force && now >= app->clock_refresh_ms &&
now - app->clock_refresh_ms < EXAMPLE_CLOCK_REFRESH_MS) { return; }
app->clock_refresh_ms = now;
app->clock_update_pending = false;
if (IEC60870_Clock_Read(&value) != 0 && value.is_invalid == 0U) {
base_ms = IEC104_Net_Now_Ms();
memset(&time, 0, sizeof(time));
time.time = value.time;
if (IEC60870_Time_CP56_To_Seconds(&time, IEC60870_LOCAL_UTC_OFFSET_MINUTES, &seconds)) {
utc_ms = (uint64_t)seconds * 1000U + value.time.ms;
if (app->command == NULL || IEC104_Command_Set_Time_Base(app->command, utc_ms, base_ms)) {
app->clock_utc_ms = utc_ms;
app->clock_base_ms = base_ms;
app->clock_valid = true;
return;
}
}
}
app->clock_valid = false;
if (app->command != NULL) { IEC104_Command_Clear_Time_Base(app->command); }
}
/* 执行检查已完成时区换算和滞留补偿;动作不重复补偿,也不推进网络事件循环。
* system设时失败时不置pending;Execute_Next的OK本身不证明action报告成功。 */
static bool Example_Clock_Action(void *context, const IEC104_ClockSynchronizationRequest_t *request)
{
ExampleApp *app = (ExampleApp *)context;
if (app->clock_system) { return IEC60870_Clock_Set(&request->local_time) != 0; }
if (app->clock_system && IEC60870_Clock_Set(&request->local_time) == 0) { return false; }
app->clock_utc_ms = request->utc_ms;
app->clock_base_ms = request->base_ms;
app->clock_valid = true;
app->clock_update_pending = true;
app->clock_refresh_ms = request->base_ms;
return true;
}
@@ -458,6 +512,12 @@ static bool Example_Drain_Clock(ExampleApp *app)
for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) {
IEC104_ClockSynchronizationExecuteResult_t result =
IEC104_ClockSynchronization_Execute_Next(app->clock, Example_Clock_Action, app);
/* 不在校时动作回调内跨模块调用;仅发布本次成功action产生的更新,旧valid不代表新成功。 */
if (app->clock_update_pending) {
if (app->command != NULL &&
!IEC104_Command_Set_Time_Base(app->command, app->clock_utc_ms, app->clock_base_ms)) { return false; }
app->clock_update_pending = false;
}
if (result == IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY ||
result == IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_WAIT) { return true; }
if (result != IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_OK) { return false; }
@@ -465,27 +525,45 @@ static bool Example_Drain_Clock(ExampleApp *app)
return true;
}
/* 示例的带时标命令沿用对应基础类型的地址、应用策略和返回信息;时标由Command负责。 */
static uint8_t Example_Command_Base_Type(uint8_t type)
{
return type >= 58U && type <= 64U ? (uint8_t)(type - 13U) : type;
}
/**
* 功能:把示例支持的遥控命令地址映射为对应运行值数组的下标。
* 输入参数:request——非NULL的已解码45/46工作请求,仅借用;本函数不检查会话或选择匹配。
* 输出参数:index——非NULL,仅成功时写入0..EXAMPLE_SIGNAL_COUNT-1,失败保持原值。
* 返回值:true为Type45的6001..6008或Type46的6101..6108;false为示例点表之外的类型/地址。
* 说明:库负责协议格式和事务身份,应用负责IOA是否存在;范围外由调用者Reject(UNKNOWN_IOA)。
* 单点命令6001..6008映射到遥信1001..1008;双点命令6101..6108映射到位置1101..1108。
* 输入参数:request——非NULL的已解码45/46/49或58/59/62工作请求,仅借用。
* 输出参数:index——非NULL,仅成功时写入对应运行值数组的下标,失败保持原值。
* 返回值:true为Type45的6001..6008、Type46的6101..6108或Type49的6301..6308。
* 说明:库已按应用提供的点表校验CA/IOA/类型/模式,此处仍核对应用映射,避免动作写错运行值。
* 单点命令映射到1001..1008,双点命令映射到1101..1108,设定值映射到2001..2008。
*/
static bool Example_Command_Point(const IEC104_CommandRequest_t *request, size_t *index)
{
uint32_t first = request->type_id == 45U ? 6001U : 6101U;
if ((request->type_id != 45U && request->type_id != 46U) || request->ioa < first ||
request->ioa - first >= EXAMPLE_SIGNAL_COUNT) { return false; }
*index = (size_t)(request->ioa - first);
return true;
size_t group;
for (group = 0U; group < sizeof(example_command_groups) / sizeof(example_command_groups[0]); ++group) {
uint32_t first = example_command_groups[group].first_ioa;
if ((example_command_groups[group].type_mask & IEC104_COMMAND_TYPE_BIT(request->type_id)) != 0U &&
request->ioa >= first && request->ioa - first < example_command_groups[group].count) {
*index = (size_t)(request->ioa - first);
return true;
}
}
return false;
}
/* 示例运行范围属于应用策略;协议库只检查设定值的编码,选择和执行均须检查限值。 */
static bool Example_Command_Value_Allowed(const IEC104_CommandRequest_t *request)
{
return Example_Command_Base_Type(request->type_id) != 49U ||
(request->value.scaled >= EXAMPLE_SCALED_MIN && request->value.scaled <= EXAMPLE_SCALED_MAX);
}
/**
* 功能:执行已通过Accept校验并消费的选择/执行动作,仅修改模拟设备状态。
* 输入参数:context——当前ExampleApp;request——库仅在本次回调期间借出的工作请求,不得保存指针。
* 输出参数:选择置返校标志;执行置输出标志并更新对应单点/双点运行值,供后续返回信息及总召读取。
* 输出参数:选择置返校标志;执行置输出标志并更新对应运行值,供后续返回信息及总召读取。
* 返回值:点表不匹配返回NOT_STARTED且没有副作用;模拟动作完成返回STARTED。
* 说明:STARTED表示选择生效或输出启动,不代替Report_Result。真实异步驱动应在设备完成后报告结果。
* 不在动作中发布任何上报、推进Slave或重入修改Command;库不保存本函数或context。
@@ -495,12 +573,16 @@ static IEC104_CommandActionResult_t Example_Command_Action(void *context,
{
ExampleApp *app = (ExampleApp *)context;
size_t index;
if (!Example_Command_Point(request, &index)) { return IEC104_COMMAND_ACTION_NOT_STARTED; }
uint8_t type = Example_Command_Base_Type(request->type_id);
if (!Example_Command_Point(request, &index) || !Example_Command_Value_Allowed(request)) {
return IEC104_COMMAND_ACTION_NOT_STARTED;
}
if (request->is_select) { app->command_selected = true; }
else {
app->command_output = true;
if (request->type_id == 45U) { app->signal_values[index] = request->state; }
else { app->double_values[index] = request->state; }
if (type == 45U) { app->signal_values[index] = request->state; }
else if (type == 46U) { app->double_values[index] = request->state; }
else { app->scaled_values[index] = request->value.scaled; }
}
return IEC104_COMMAND_ACTION_STARTED;
}
@@ -508,8 +590,8 @@ static IEC104_CommandActionResult_t Example_Command_Action(void *context,
/**
* 功能:为已启动的当前EXECUTE构造一帧COT=11的模拟位置返回信息,此处只组包,不提交。
* 输入参数:app——非NULL,command_event保留当前EXECUTE身份,运行值已由动作更新。
* 输出参数:app->command_return及其固定10字节缓冲,CA/OA沿用工作请求,IOA映射到对应遥信。
* 返回值:true为Type1或Type3单对象ASDU构造成功;false为点表或编解码失败,此时不可提交该ASDU。
* 输出参数:app->command_return及其固定12字节缓冲,CA/OA沿用工作请求,IOA映射到对应监视量。
* 返回值:true为Type1/3/11单对象ASDU构造成功;false为点表或编解码失败,此时不可提交该ASDU。
* 说明:命令模块已拒绝TEST请求,因此清零公共头得到TEST=0;对象品质为示例约定的0。
* 局部信息对象由ASDU编码复制,离开函数不借用局部变量;ASDU仍借用app内固定字节缓冲。
* 成功后冻结这一帧,Submit_Return_Info的WAIT不重新组包;只有OK才可推进,避免同一变位重复上报。
@@ -519,6 +601,7 @@ static bool Example_Build_Command_Return(ExampleApp *app)
const IEC104_CommandRequest_t *request = &app->command_event.request;
IEC60870_AsduHeader_t header;
size_t index;
uint8_t type = Example_Command_Base_Type(request->type_id);
if (!Example_Command_Point(request, &index)) { return false; }
memset(&header, 0, sizeof(header));
header.cause_of_transmission = IEC60870_ASDU_COT_RETURN_INFO_REMOTE;
@@ -527,16 +610,21 @@ static bool Example_Build_Command_Return(ExampleApp *app)
if (IEC60870_Asdu_Create(&app->command_return,
IEC104_Slave_Get_Application_Layer_Parameters(app->slave), app->command_return_bytes,
sizeof(app->command_return_bytes), &header, 0) != IEC60870_ASDU_OK) { return false; }
if (request->type_id == 45U) {
if (type == 45U) {
IEC60870_SinglePointInformation_t point;
return IEC60870_SinglePointInformation_Create(&point, (uint32_t)(1001U + index),
app->signal_values[index], 0U) == IEC60870_ASDU_OK &&
IEC60870_Asdu_Add_InformationObject(&app->command_return, &point.object) == IEC60870_ASDU_OK;
} else {
} else if (type == 46U) {
IEC60870_DoublePointInformation_t point;
return IEC60870_DoublePointInformation_Create(&point, (uint32_t)(1101U + index),
app->double_values[index], 0U) == IEC60870_ASDU_OK &&
IEC60870_Asdu_Add_InformationObject(&app->command_return, &point.object) == IEC60870_ASDU_OK;
} else {
IEC60870_MeasuredValueScaled_t point;
return IEC60870_MeasuredValueScaled_Create(&point, example_measurements[index].ioa,
app->scaled_values[index], example_measurements[index].quality) == IEC60870_ASDU_OK &&
IEC60870_Asdu_Add_InformationObject(&app->command_return, &point.object) == IEC60870_ASDU_OK;
}
}
@@ -594,10 +682,12 @@ static bool Example_Drain_Command(ExampleApp *app)
if (app->command_pending) {
IEC104_CommandReplyResult_t reply;
size_t index;
/* context和点表属于应用策略;格式已合法但不属于本应用的点仍明确回UNKNOWN_IOA。 */
/* 库已查命令点表;应用仍核对原站context和运行值映射,不借用另一个站点的设备状态。 */
if (IEC104_Station_Get_Context(app->command_station) != app ||
!Example_Command_Point(&app->command_event.request, &index)) {
reply = IEC104_Command_Reject(app->command, &app->command_event, IEC104_COMMAND_REJECT_UNKNOWN_IOA);
} else if (!Example_Command_Value_Allowed(&app->command_event.request)) {
reply = IEC104_Command_Reject(app->command, &app->command_event, IEC104_COMMAND_REJECT_NEGATIVE);
} else { reply = IEC104_Command_Accept(app->command, &app->command_event, Example_Command_Action, app); }
app->command_waiting = reply == IEC104_COMMAND_REPLY_WAIT;
/* WAIT未消费工作事件、未调用动作;保留身份,下轮先处理Slave通知再重试。
@@ -636,24 +726,67 @@ static bool Example_Drain_Command(ExampleApp *app)
return true;
}
#ifndef IEC104_SLAVE_EXAMPLE_NO_MAIN
/**
* 功能:为命令行示例创建默认遥控实例,与仅总召的初始化入口分开。
* 输入参数:app——非NULL,Slave句柄和站点已创建,尚未开始消费Slave通知。
* 输出参数:成功后保存app->command;失败不创建可用实例。
* 返回值:true为创建成功;false为重复初始化、配置/归属错误或静态池没有空位。
* 说明:默认45/46均须先选择,决定15秒、选择30秒、执行60秒;本函数不执行设备动作。
* 说明:显式启用45/46/49/58/59/62,24点逐行配置CA、IOA、允许类型和执行方式。
* 点表归app所有,只在Create前填充,Destroy之前禁止移动app、修改或释放点表。
* 每Slave只能绑定一个Command。main在Slave_Init前创建亦可,但必须早于首条生命周期通知。
*/
static bool Example_Init_Command(ExampleApp *app)
{
IEC104_CommandConfig_t config;
uint16_t common_address = IEC104_Station_Get_Common_Address(&app->station);
/* 每行独立指定策略;修改某个点的执行方式,只改该行的mode。
* 使用实际站点CA以保留--ca;局部表只用于初始化,Create借用app中的持久副本。 */
const IEC104_CommandPoint_t points[] = {
/* CA IOA 允许类型 执行方式 */
{common_address, 6001U, IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(58U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6002U, IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(58U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6003U, IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(58U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6004U, IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(58U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6005U, IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(58U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6006U, IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(58U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6007U, IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(58U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6008U, IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(58U), IEC104_COMMAND_MODE_DIRECT},
{common_address, 6101U, IEC104_COMMAND_TYPE_BIT(46U) | IEC104_COMMAND_TYPE_BIT(59U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6102U, IEC104_COMMAND_TYPE_BIT(46U) | IEC104_COMMAND_TYPE_BIT(59U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6103U, IEC104_COMMAND_TYPE_BIT(46U) | IEC104_COMMAND_TYPE_BIT(59U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6104U, IEC104_COMMAND_TYPE_BIT(46U) | IEC104_COMMAND_TYPE_BIT(59U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6105U, IEC104_COMMAND_TYPE_BIT(46U) | IEC104_COMMAND_TYPE_BIT(59U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6106U, IEC104_COMMAND_TYPE_BIT(46U) | IEC104_COMMAND_TYPE_BIT(59U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6107U, IEC104_COMMAND_TYPE_BIT(46U) | IEC104_COMMAND_TYPE_BIT(59U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6108U, IEC104_COMMAND_TYPE_BIT(46U) | IEC104_COMMAND_TYPE_BIT(59U), IEC104_COMMAND_MODE_DIRECT},
{common_address, 6301U, IEC104_COMMAND_TYPE_BIT(49U) | IEC104_COMMAND_TYPE_BIT(62U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6302U, IEC104_COMMAND_TYPE_BIT(49U) | IEC104_COMMAND_TYPE_BIT(62U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6303U, IEC104_COMMAND_TYPE_BIT(49U) | IEC104_COMMAND_TYPE_BIT(62U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6304U, IEC104_COMMAND_TYPE_BIT(49U) | IEC104_COMMAND_TYPE_BIT(62U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6305U, IEC104_COMMAND_TYPE_BIT(49U) | IEC104_COMMAND_TYPE_BIT(62U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6306U, IEC104_COMMAND_TYPE_BIT(49U) | IEC104_COMMAND_TYPE_BIT(62U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6307U, IEC104_COMMAND_TYPE_BIT(49U) | IEC104_COMMAND_TYPE_BIT(62U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{common_address, 6308U, IEC104_COMMAND_TYPE_BIT(49U) | IEC104_COMMAND_TYPE_BIT(62U), IEC104_COMMAND_MODE_DIRECT}
};
_Static_assert(sizeof(points) == sizeof(app->command_points), "Command point table size mismatch");
if (app->command != NULL) { return false; }
IEC104_Command_Config_Default(&config);
/* 本例保留选择有效期30000ms:适合作为人工确认流程的联调起点。
* 主站自动选择后紧接执行时,可在此设置config.select_timeout_ms = 10000U,
* 最终值须覆盖主站操作间隔与确认发送/通信/调度余量,并与主站约定。
* 此期限由所有选择执行点共用,匹配执行到达即可进入下一阶段,不必等满;
* 等待期间占唯一事务槽。它与time_tag_max_delay_ms的时标校验窗口独立。 */
config.type_mask = IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(46U) |
IEC104_COMMAND_TYPE_BIT(49U) | IEC104_COMMAND_TYPE_BIT(58U) |
IEC104_COMMAND_TYPE_BIT(59U) | IEC104_COMMAND_TYPE_BIT(62U);
memcpy(app->command_points, points, sizeof(points));
config.points = app->command_points;
config.point_count = sizeof(points) / sizeof(points[0]);
app->command = IEC104_Command_Create(app->slave, &config);
return app->command != NULL;
}
#endif
/* 统一取消应用业务;app为有效应用上下文,未创建的可选模块直接跳过,无返回值。
* 遥控Cancel丢弃模块持有应答但保留应交付FINISHED,不回滚已经执行的输出。
@@ -814,6 +947,7 @@ static bool Example_Drain(ExampleApp *app)
{
unsigned count;
IEC104_SlaveEvent_t event;
Example_Refresh_System_Clock(app, false);
for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) {
IEC104_TcpEventResult_t result = IEC104_Slave_NextEvent(app->slave, &event);
if (result == IEC104_TCP_EVENT_EMPTY) { app->events_pending = false; return true; }
@@ -893,7 +1027,13 @@ static void Example_Usage(const char *program)
"Clock: simulated by default; system sets the actual clock and requires permission.\n"
"Clock requests use UTC+8 standard time; TEST is rejected without setting time.\n"
"Commands: Type45 IOA6001..6008 -> signals1001..1008; Type46 IOA6101..6108 -> double1101..1108.\n"
"Commands require select then execute; COT8 cancels selection; TEST is rejected.\n", program);
"Setpoints: Type49 IOA6301..6308 -> scaled2001..2008; allowed range -10000..10000.\n"
"Timed commands: Type58/59/62 use the same points and returns as Type45/46/49.\n"
"Timed commands require a valid clock; max age/future difference is 10000ms; expired requests are silent.\n"
"System clock is read at startup and every 60s; invalid reads clear the command time base.\n"
"Select/execute: IOA6001..6007, 6101..6107, 6301..6307.\n"
"Direct execute: IOA6008, 6108, 6308; SELECT is rejected on these points.\n"
"COT8 cancels selection; TEST is rejected.\n", program);
}
/**
@@ -979,6 +1119,7 @@ int main(int argc, char **argv)
return EXIT_FAILURE;
}
app.clock_system = clock_system;
Example_Refresh_System_Clock(&app, true);
result = IEC104_Slave_Init(slave, &config, &error);
if (result != IEC104_TCP_OK) {
fprintf(stderr, "Init failed: result=%d stage=%d socket=%d\n", result, error.stage, error.socket_error);
@@ -1059,14 +1200,15 @@ cleanup:
if (IEC104_Slave_Remove_Station(slave, &app.station) != IEC104_STATION_OK) { exit_code = EXIT_FAILURE; }
if (IEC104_Slave_Destroy(slave, &error) != IEC104_TCP_OK) { exit_code = EXIT_FAILURE; }
/* 打印应用提交统计;这些值不用于推断未确认帧是否已到达主站,也不作为自动重试依据。 */
printf("STOP submitted=%lu gi_submitted=%lu waits=%lu rejected=%lu cancelled=%lu overloads=%lu deactivated=%lu timeouts=%lu negative_submitted=%lu dropped_requests=%lu counter_submitted=%lu counter_completed=%lu clock_executed=%lu clock_synchronized=%lu clock_submitted=%lu command_selected=%lu command_executed=%lu command_completed=%lu command_submitted=%lu command_returns=%lu\n",
printf("STOP submitted=%lu gi_submitted=%lu waits=%lu rejected=%lu cancelled=%lu overloads=%lu deactivated=%lu timeouts=%lu negative_submitted=%lu dropped_requests=%lu counter_submitted=%lu counter_completed=%lu clock_executed=%lu clock_synchronized=%lu clock_submitted=%lu command_selected=%lu command_executed=%lu command_completed=%lu command_submitted=%lu command_returns=%lu command_time_expired=%lu command_time_rejected=%lu\n",
app.gi.submitted + app.other_submitted, app.gi.gi_submitted, app.gi.waits,
app.gi.rejected + app.other_rejected, app.gi.cancelled + app.other_cancelled,
app.gi.overloads + app.other_overloads, app.gi.deactivated, app.gi.timeouts,
app.gi.negative_submitted, app.gi.dropped_requests + app.other_overloads,
app.ci.submitted, app.counter_completed, app.cs.executed, app.cs.synchronized, app.cs.submitted,
app.command_status.selected, app.command_status.executed, app.command_status.completed,
app.command_status.submitted, app.command_status.return_info_submitted);
app.command_status.submitted, app.command_status.return_info_submitted,
app.command_status.time_expired, app.command_status.time_rejected);
return exit_code;
}
#endif