/* * 104子站示例:总召、计数量召唤、时钟同步、45/46遥控、模拟点表及main。 * 两种召唤共用IEC104_Interrogation.c状态机,各自有独立实例和业务事件。 * 每轮Slave_Poll -> NextEvent逐条交付 -> Command_Process及应用交互 -> 召唤/校时;所有调用串行。 * 总召每批4点,模块自动推进至需要等待;8个单点遥信、8个双点遥信和8个遥测组成8个响应ASDU。 * QOI=21读取单点及双点遥信,22只读遥测,其余分组由应用拒绝;分组点表策略不写进协议库。 * 同一连接至多一个未终结总召,忙时不区分QOI均否定;任务满时拒绝/计数丢弃新请求,保留当前连接。 * 总召模块处理WAIT、STOPDT及断线;应用读取业务事件,选择类型并显式交付ASDU。 * 计数量使用8个模拟计数器;读取快照,冻结/复位由经过事件校验的同步动作入口执行。 * 对时默认只更新模拟UTC基准;--clock system显式选择实际时钟,协议仍使用单调时钟。 * 遥控默认先选择后执行,同一实例只占一个事务;设备结果、返回信息和Complete分步推进,WAIT保留当前步骤。 * 模拟输出同步更新点值;FINISHED只清理返校/输出标志,不恢复已执行的点值,也不代表真实设备已经空闲。 * 不支持的其他命令当场提交COT=44否定应答,复用发送层HIGH;容量不足只丢本条并计数。 * 默认127.0.0.1:2404、CA=1;构建命令见同目录README.md。 */ #include "IEC104_Interrogation.h" #include "IEC104_CounterInterrogation.h" #include "IEC104_ClockSynchronization.h" #include "IEC104_Command.h" #include "IEC60870_Clock.h" #include #include #include #define EXAMPLE_EVENT_BUDGET 32U #define EXAMPLE_COUNTER_COUNT 8U /* 模拟遥信点表:ioa为24位信息对象地址,value为0/1,quality为SIQ品质位(这里均为0)。 * 表项不是线上报文,后续由SinglePointInformation_Create及ASDU容器编码成Type=1。 * 此表提供地址、初值和品质;运行值保存在各ExampleApp中,遥控与总召共用同一数据源。 */ static const struct { uint32_t ioa; uint8_t value, quality; } example_signals[] = { {1001, 0, 0}, {1002, 1, 0}, {1003, 0, 0}, {1004, 1, 0}, {1005, 1, 0}, {1006, 0, 0}, {1007, 1, 0}, {1008, 0, 0} }; /* 模拟遥测点表:ioa为信息对象地址,value为原始有符号16位标度值,quality为QDS品质位。 * 编码为不带时标的Type=11;本示例不定义物理单位和倍率,负数也按有符号值传输。 */ static const struct { uint32_t ioa; int16_t value; uint8_t quality; } example_measurements[] = { {2001, 2300, 0}, {2002, 2310, 0}, {2003, 2290, 0}, {2004, 5000, 0}, {2005, -125, 0}, {2006, 0, 0}, {2007, 1234, 0}, {2008, 42, 0} }; /* 点数直接取数组长度;分批游标使用size_t,不另维护可能与点表不一致的手写数量。 */ #define EXAMPLE_SIGNAL_COUNT (sizeof(example_signals) / sizeof(example_signals[0])) #define EXAMPLE_MEASUREMENT_COUNT (sizeof(example_measurements) / sizeof(example_measurements[0])) /* 当前取数站的应用游标;首个NEED_DATA才初始化,协议库不登记或冻结业务IOA列表。 */ typedef struct { size_t single_index, double_index, scaled_index; } ExampleGiData; typedef struct { IEC104_Slave_t *slave; /* main拥有;本上下文仅借用。 */ IEC104_Station_t station; /* 应用拥有的固定地址站点节点,供各业务共用CA和context。 */ IEC104_Interrogation_t *interrogation; /* 库内不透明总召任务对象。 */ IEC104_InterrogationStatus_t gi; /* 只读快照,用于调度等待及统计。 */ IEC104_Interrogation_t *counter; /* 独立计数量实例,与总召共用Slave,不共用任务和广播状态区。 */ IEC104_InterrogationStatus_t ci; /* 计数量调度及统计快照,每次推进后重新查询。 */ IEC104_ClockSynchronization_t *clock; /* 独立时钟同步模块,共用Slave及站点表。 */ IEC104_ClockSynchronizationStatus_t cs; /* 对时执行、确认及期限的只读快照。 */ IEC104_Command_t *command; /* CLI启用;仅总召的旧测试可以不创建遥控实例。 */ IEC104_CommandStatus_t command_status; /* 推进后/等待前刷新;计时、通知和应答状态只供调度及统计。 */ IEC104_CommandEvent_t command_event; /* 工作事件可跨WAIT保留,执行收尾继续使用原EXECUTE身份。 */ IEC104_Station_t *command_station; /* 应用额外引用:首个工作事件先Acquire,再保存context;FINISHED后Release。 */ uint64_t command_instance, command_transaction; /* 保留事务归属;SELECT/EXECUTE共用,FINISHED须先核对再清理。 */ /* pending仅表示工作事件尚待Accept/Reject;waiting表示上次接口WAIT,避免把待重试状态当作可立即推进。 * selected/output是模拟返校及输出的副作用标志,不代替协议阶段;真实设备忙状态应独立维护。 */ 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保持原文,不借用总召缓冲。 */ /* 单点及双点运行值由遥控更新,总召与遥信分组均读取同一数组的当前值。 * 单点命令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毫秒及其单调基准,不影响协议计时。 */ IEC104_InterrogationEvent_t counter_event; /* WAIT时保留的原业务事件,重试不能换成新身份。 */ bool counter_pending, counter_first; /* 是否持有待应答事件、本轮是否先推进计数量。 */ int32_t counter_current[EXAMPLE_COUNTER_COUNT]; /* 模拟当前累计值,冻结及复位动作的数据源。 */ IEC60870_BinaryCounterReading_t counter_frozen[EXAMPLE_COUNTER_COUNT]; /* 最近冻结值及BCR序号、品质。 */ IEC60870_BinaryCounterReading_t counter_snapshot[EXAMPLE_COUNTER_COUNT]; /* 本轮读取固定的冻结副本。 */ size_t counter_cursor, counter_end; /* 当前读取区间,只有Submit_Batch成功才推进游标。 */ uint64_t counter_instance, counter_request; /* 快照归属;旧FINISHED不得清理新请求。 */ IEC104_Station_t *counter_station; /* 读取期间额外持有的原站节点,结束后归还引用。 */ unsigned long counter_completed; /* 已消费的COMPLETED通知数,包含读取及冻结/复位。 */ ExampleGiData gi_data; /* 应用拥有的游标,库内没有session或回调参数。 */ IEC104_Station_t *gi_station; /* 取数期间持有原站,Complete_Data或FINISHED时释放。 */ uint64_t session_instance, session_request; /* 只清理对应请求的遍历状态及业务资源。 */ IEC104_InterrogationEvent_t business_event; /* 纯值,可保存到后续循环应答。 */ bool business_pending; int batch_result; /* 本例构造结果:-1失败、0数据结束、1有数据。 */ IEC60870_Asdu_t batch; /* 借用本应用bytes;Submit_Batch(OK)后可复用。 */ uint8_t batch_bytes[IEC104_MAX_ASDU_LEN]; bool events_pending; unsigned long other_submitted, other_rejected, other_cancelled, other_overloads; } ExampleApp; /* 普通应用函数,只由下面的事件分支直接调用,不注册到库中。 * 应用自行选择本批IOA、类型及分组成员;为NEED_DATA构造一种类型的一批。 * 真实取值应放在下列逐点组包处,值与品质的读取一致性由数据源保证;不预读整轮数据。 * FULL不跳过当前点;本函数不推进持久游标,Submit_Batch(OK)后才推进。 * WAIT仅保留事件,每次重试重读当前值。点值更新/突发发布与本次组包/提交须串行协调。 * 逐批取值不保证整轮同一采样时刻。 */ static int Example_Build_Batch(ExampleApp *app, const IEC104_InterrogationRequest_t *request) { ExampleGiData *data = &app->gi_data; IEC60870_Asdu_t *asdu = &app->batch; IEC60870_AsduHeader_t header; IEC104_Station_t *station = request->station_id != 0U ? IEC104_Slave_Get_Station_By_Id(app->slave, request->station_id) : IEC104_Slave_Get_Station(app->slave, request->common_address); if (IEC104_Station_Get_Context(station) != app) { return -1; } memset(&header, 0, sizeof(header)); header.cause_of_transmission = request->qualifier; /* 站总召20,分组总召21..36。 */ header.common_address = request->common_address; header.originator_address = request->originator_address; header.is_test = request->is_test; if (IEC60870_Asdu_Create(asdu, IEC104_Slave_Get_Application_Layer_Parameters(app->slave), app->batch_bytes, sizeof(app->batch_bytes), &header, 0) != IEC60870_ASDU_OK) { return -1; } if (data->single_index < EXAMPLE_SIGNAL_COUNT) { size_t index = data->single_index; while (index < EXAMPLE_SIGNAL_COUNT && asdu->object_count < request->max_objects_per_asdu) { IEC60870_SinglePointInformation_t point; IEC60870_AsduResult_t added; if (IEC60870_SinglePointInformation_Create(&point, example_signals[index].ioa, app->signal_values[index], example_signals[index].quality) != IEC60870_ASDU_OK) { return -1; } added = IEC60870_Asdu_Add_InformationObject(asdu, &point.object); if (added == IEC60870_ASDU_FULL) { break; } if (added != IEC60870_ASDU_OK) { return -1; } ++index; } } else if (data->double_index < EXAMPLE_SIGNAL_COUNT) { size_t index = data->double_index; while (index < EXAMPLE_SIGNAL_COUNT && asdu->object_count < request->max_objects_per_asdu) { IEC60870_DoublePointInformation_t point; IEC60870_AsduResult_t added; if (IEC60870_DoublePointInformation_Create(&point, (uint32_t)(1101U + index), app->double_values[index], 0U) != IEC60870_ASDU_OK) { return -1; } added = IEC60870_Asdu_Add_InformationObject(asdu, &point.object); if (added == IEC60870_ASDU_FULL) { break; } if (added != IEC60870_ASDU_OK) { return -1; } ++index; } } else if (data->scaled_index < EXAMPLE_MEASUREMENT_COUNT) { size_t index = data->scaled_index; while (index < EXAMPLE_MEASUREMENT_COUNT && asdu->object_count < request->max_objects_per_asdu) { 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) { return -1; } added = IEC60870_Asdu_Add_InformationObject(asdu, &point.object); if (added == IEC60870_ASDU_FULL) { break; } if (added != IEC60870_ASDU_OK) { return -1; } ++index; } } else { return 0; } return asdu->object_count != 0U ? 1 : -1; } /* 释放应用为当前取数持有的节点引用并清空游标,库的广播状态区仍独立保存协议进度。 * Complete_Data成功即可调用,让下一站复用游标;不需要等本轮所有站的FINISHED。 */ static bool Example_End_Data(ExampleApp *app) { if (app->gi_station != NULL && IEC104_Slave_Release_Station(app->slave, app->gi_station) != IEC104_STATION_OK) { return false; } app->gi_station = NULL; memset(&app->gi_data, 0, sizeof(app->gi_data)); app->session_instance = app->session_request = 0U; return true; } /* 第二条事件流:应用主动取业务事件,再显式应答;没有库调用应用函数的路径。 * 常量点表立即就绪;异步数据源可在NEED_DATA保留business_event,完成采样后再Submit。 * WAIT先处理Slave通知并推进网络,下次重取数据;STALE表示交互已失效,不重试过期事件。 */ static bool Example_Drain_Business(ExampleApp *app) { unsigned count; for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) { IEC104_InterrogationEvent_t *event = &app->business_event; IEC104_InterrogationReplyResult_t reply; if (!app->business_pending) { IEC104_InterrogationNextResult_t next = IEC104_Interrogation_NextEvent(app->interrogation, event); if (next == IEC104_INTERROGATION_NEXT_EMPTY) { return true; } if (next != IEC104_INTERROGATION_NEXT_READY) { return false; } app->business_pending = true; } switch (event->type) { case IEC104_INTERROGATION_BEGIN: { bool accept = event->request.qualifier <= 22U; /* 示例仅定义组1=单点及双点遥信、组2=遥测;拒绝未配置的组。协议库支持全部16组。 * 广播会先交付所有站的BEGIN;这里只作准入判断,不占用唯一取数游标。 */ reply = IEC104_Interrogation_Reply_Begin(app->interrogation, event, accept); break; } case IEC104_INTERROGATION_NEED_DATA: /* 广播逐站取数,因此首个NEED_DATA才建立游标;用注册编号取得原站,避免同CA替换串站。 * 应用单独Acquire,确保库交付FINISHED并归还自己的引用时,应用清理仍有有效节点。 */ if (event->request_id != app->session_request || event->instance_id != app->session_instance) { if (app->session_request != 0U) { return false; } if (event->request.station_id != 0U) { app->gi_station = IEC104_Slave_Get_Station_By_Id(app->slave, event->request.station_id); if (app->gi_station == NULL || IEC104_Slave_Acquire_Station(app->slave, app->gi_station) != IEC104_STATION_OK) { return false; } } memset(&app->gi_data, 0, sizeof(app->gi_data)); if (event->request.qualifier == 21U) { app->gi_data.scaled_index = EXAMPLE_MEASUREMENT_COUNT; } if (event->request.qualifier == 22U) { app->gi_data.single_index = EXAMPLE_SIGNAL_COUNT; app->gi_data.double_index = EXAMPLE_SIGNAL_COUNT; } app->session_instance = event->instance_id; app->session_request = event->request_id; } app->batch_result = Example_Build_Batch(app, &event->request); if (app->batch_result < 0) { reply = IEC104_Interrogation_Fail_Request(app->interrogation, event); } else if (app->batch_result == 0) { reply = IEC104_Interrogation_Complete_Data(app->interrogation, event); } else { reply = IEC104_Interrogation_Submit_Batch(app->interrogation, event, &app->batch); } if (reply == IEC104_INTERROGATION_REPLY_OK && app->batch_result > 0) { if (app->gi_data.single_index < EXAMPLE_SIGNAL_COUNT) { app->gi_data.single_index += app->batch.object_count; } else if (app->gi_data.double_index < EXAMPLE_SIGNAL_COUNT) { app->gi_data.double_index += app->batch.object_count; } else { app->gi_data.scaled_index += app->batch.object_count; } } else if (reply == IEC104_INTERROGATION_REPLY_OK && app->batch_result == 0) { /* 其他站的数据随后才开始;无需等到所有站的ACT_TERM才复用游标。 */ if (!Example_End_Data(app)) { return false; } } break; case IEC104_INTERROGATION_FINISHED: /* 完成、本地取消、主站撤销、超时、断线和失败统一清理。 * 真实异步采样还需在这里停止对应任务;迟到的旧FINISHED不清理新请求资源。 */ if (event->instance_id == app->session_instance && event->request_id == app->session_request) { if (!Example_End_Data(app)) { return false; } } reply = IEC104_INTERROGATION_REPLY_OK; /* 终结通知无需调用任何应答接口。 */ break; default: return false; } /* WAIT保留business_event且游标不动;下轮重新Build_Batch,不重发缓存的旧点值。 */ if (reply == IEC104_INTERROGATION_REPLY_WAIT) { return true; } if (reply != IEC104_INTERROGATION_REPLY_OK && reply != IEC104_INTERROGATION_REPLY_STALE) { return false; } app->business_pending = false; } return true; } /* 同步模拟计数器动作:执行入口先验证事件,WAIT/STALE不调用本函数。 * 只在内存中一次完成选中组的操作;不调用网络,不再次进入召唤模块。 * 实际外部电表命令需要独立的执行结果和重复请求策略,不能套用此同步函数。 */ static bool Example_Counter_Action(void *context, const IEC104_InterrogationRequest_t *request) { ExampleApp *app = (ExampleApp *)context; size_t first, end, index; if (request->is_test || (request->qualifier != 1U && request->qualifier != 2U && request->qualifier != 5U) || request->freeze == 0U || request->freeze > 3U) { return false; } first = request->qualifier == 2U ? 4U : 0U; end = request->qualifier == 1U ? 4U : EXAMPLE_COUNTER_COUNT; for (index = first; index < end; ++index) { if (request->freeze != 3U) { app->counter_frozen[index].value = app->counter_current[index]; app->counter_frozen[index].sequence_number = (uint8_t)((app->counter_frozen[index].sequence_number + 1U) & 31U); } if (request->freeze >= 2U) { app->counter_current[index] = 0; } } return true; } /* 释放当前读取的节点引用和快照归属;快照字节无需清零,下次读取会整体覆盖。 * Complete_Data成功或匹配的FINISHED都可收尾,冻结动作本身不占用读取游标。 */ static bool Example_End_Counter(ExampleApp *app) { if (app->counter_station != NULL && IEC104_Slave_Release_Station(app->slave, app->counter_station) != IEC104_STATION_OK) { return false; } app->counter_station = NULL; app->counter_instance = app->counter_request = 0U; app->counter_cursor = app->counter_end = 0U; return true; } /* 从本轮固定快照构造一个Type15、SQ=0批次,共用ASDU缓冲但不推进游标。 * 返回1表示非空批次,0表示所选组已读完,-1表示构造失败;WAIT后仍取同一快照重组。 */ static int Example_Build_Counter_Batch(ExampleApp *app, const IEC104_InterrogationRequest_t *request) { IEC60870_AsduHeader_t header; size_t index; if (app->counter_cursor == app->counter_end) { return 0; } memset(&header, 0, sizeof(header)); header.cause_of_transmission = request->data_cause; header.common_address = request->common_address; header.originator_address = request->originator_address; header.is_test = request->is_test; if (IEC60870_Asdu_Create(&app->batch, IEC104_Slave_Get_Application_Layer_Parameters(app->slave), app->batch_bytes, sizeof(app->batch_bytes), &header, 0) != IEC60870_ASDU_OK) { return -1; } for (index = app->counter_cursor; index < app->counter_end && app->batch.object_count < request->max_objects_per_asdu; ++index) { IEC60870_IntegratedTotals_t point; IEC60870_AsduResult_t result; if (IEC60870_IntegratedTotals_Create(&point, (uint32_t)(3001U + index), &app->counter_snapshot[index]) != IEC60870_ASDU_OK) { return -1; } result = IEC60870_Asdu_Add_InformationObject(&app->batch, &point.object); if (result == IEC60870_ASDU_FULL) { break; } if (result != IEC60870_ASDU_OK) { return -1; } } return app->batch.object_count != 0U ? 1 : -1; } /* 冻结值在首个NEED_DATA固定为本轮快照;WAIT重建报文也使用同一份值、序号和品质。 * 模拟点值属于本应用;真实共享数据源应在其锁内取得快照后再进行网络提交。 */ static bool Example_Drain_Counter(ExampleApp *app) { unsigned count; if (app->counter == NULL) { return true; } for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) { IEC104_InterrogationEvent_t *event = &app->counter_event; IEC104_InterrogationReplyResult_t reply; if (!app->counter_pending) { IEC104_InterrogationNextResult_t next = IEC104_Interrogation_NextEvent(app->counter, event); if (next == IEC104_INTERROGATION_NEXT_EMPTY) { return true; } if (next != IEC104_INTERROGATION_NEXT_READY) { return false; } app->counter_pending = true; } switch (event->type) { case IEC104_INTERROGATION_BEGIN: { IEC104_Station_t *station = IEC104_Slave_Get_Station_By_Id(app->slave, event->request.station_id); bool accept = IEC104_Station_Get_Context(station) == app && (event->request.qualifier == 1U || event->request.qualifier == 2U || event->request.qualifier == 5U); if (accept && event->request.freeze != 0U && !event->request.is_test) { reply = IEC104_CounterInterrogation_Execute_Begin(app->counter, event, Example_Counter_Action, app); } else { reply = IEC104_Interrogation_Reply_Begin(app->counter, event, accept && event->request.freeze == 0U); } break; } case IEC104_INTERROGATION_NEED_DATA: { int built; if (event->instance_id != app->counter_instance || event->request_id != app->counter_request) { IEC104_Station_t *station; if (app->counter_request != 0U) { return false; } station = IEC104_Slave_Get_Station_By_Id(app->slave, event->request.station_id); if (station == NULL || IEC104_Station_Get_Context(station) != app || IEC104_Slave_Acquire_Station(app->slave, station) != IEC104_STATION_OK) { return false; } app->counter_station = station; memcpy(app->counter_snapshot, app->counter_frozen, sizeof(app->counter_snapshot)); app->counter_cursor = event->request.qualifier == 2U ? 4U : 0U; app->counter_end = event->request.qualifier == 1U ? 4U : EXAMPLE_COUNTER_COUNT; app->counter_instance = event->instance_id; app->counter_request = event->request_id; } built = Example_Build_Counter_Batch(app, &event->request); if (built < 0) { reply = IEC104_Interrogation_Fail_Request(app->counter, event); } else if (built == 0) { reply = IEC104_Interrogation_Complete_Data(app->counter, event); } else { reply = IEC104_Interrogation_Submit_Batch(app->counter, event, &app->batch); } if (reply == IEC104_INTERROGATION_REPLY_OK) { if (built > 0) { app->counter_cursor += app->batch.object_count; } else if (built == 0 && !Example_End_Counter(app)) { return false; } } break; } case IEC104_INTERROGATION_FINISHED: if (event->reason == IEC104_INTERROGATION_FINISH_COMPLETED) { ++app->counter_completed; } if (event->instance_id == app->counter_instance && event->request_id == app->counter_request && !Example_End_Counter(app)) { return false; } reply = IEC104_INTERROGATION_REPLY_OK; break; default: return false; } if (reply == IEC104_INTERROGATION_REPLY_WAIT) { return true; } if (reply != IEC104_INTERROGATION_REPLY_OK && reply != IEC104_INTERROGATION_REPLY_STALE) { return false; } app->counter_pending = false; } return true; } /* 必须在开始消费Slave事件之前创建业务模块;config本地对象按值复制,广播数组仍为借用。 * 单站可传NULL/0;多站广播须提供覆盖全部目标的数组,保持独占和地址稳定直到模块Destroy。 */ static bool Example_Init(ExampleApp *app, IEC104_Slave_t *slave, uint16_t common_address, IEC104_InterrogationStationState_t *broadcast_states, size_t broadcast_capacity) { IEC104_InterrogationConfig_t config; size_t index; memset(app, 0, sizeof(*app)); app->slave = slave; /* 每个应用实例独立保存受控运行值:单点沿用原总召初值,双点1表示分位。 * 这里只在应用初始化时赋值,选择结束、撤销和FINISHED清理均不得恢复这些初值。 */ for (index = 0U; index < EXAMPLE_SIGNAL_COUNT; ++index) { app->signal_values[index] = example_signals[index].value; app->double_values[index] = 1U; } 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只在注册时指定;多站广播需提供每站状态区。 */ config.broadcast_states = broadcast_states; config.broadcast_capacity = broadcast_capacity; config.max_objects_per_asdu = 4U; app->interrogation = IEC104_Interrogation_Create(slave, &config); if (app->interrogation == NULL) { (void)IEC104_Slave_Remove_Station(slave, &app->station); } return app->interrogation != NULL; } /* 计数量实例可选,独立提供广播状态区;不共享总召的数组或取数游标。 * CLI仅注册一站。测试可给多个CA注册同一app,它们会指向同一组模拟计数器; * 真实多站应用应让各站context分别拥有计数器数据,尤其不要逐站复位同一物理源。 */ static bool Example_Init_Counters(ExampleApp *app, IEC104_InterrogationStationState_t *broadcast_states, size_t broadcast_capacity) { static const int32_t initial[EXAMPLE_COUNTER_COUNT] = {120000, 345600, 7890, 0, 1, 65535, INT32_MAX, 42}; IEC104_InterrogationConfig_t config; size_t index; if (app->counter != NULL) { return false; } IEC104_Interrogation_Config_Default(&config); config.use_station_registry = true; config.broadcast_states = broadcast_states; config.broadcast_capacity = broadcast_capacity; config.max_objects_per_asdu = 4U; app->counter = IEC104_CounterInterrogation_Create(app->slave, &config); if (app->counter == NULL) { return false; } for (index = 0U; index < EXAMPLE_COUNTER_COUNT; ++index) { app->counter_current[index] = initial[index]; app->counter_frozen[index].value = initial[index]; } return true; } /* 执行检查已完成时区换算和滞留补偿;动作不重复补偿,也不推进网络事件循环。 */ 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; } app->clock_utc_ms = request->utc_ms; app->clock_base_ms = request->base_ms; app->clock_valid = true; return true; } /* config可为NULL采用默认值;创建须早于消费Slave通知,实例独占自身请求和确认状态。 */ static bool Example_Init_Clock(ExampleApp *app, const IEC104_ClockSynchronizationConfig_t *config) { IEC104_ClockSynchronizationConfig_t defaults; if (app->clock != NULL) { return false; } if (config == NULL) { IEC104_ClockSynchronization_Config_Default(&defaults); config = &defaults; } app->clock = IEC104_ClockSynchronization_Create(app->slave, config); return app->clock != NULL; } /* 每轮最多执行事件预算条请求;WAIT先交回Slave消费通知,不重放已经执行的动作。 */ static bool Example_Drain_Clock(ExampleApp *app) { unsigned count; if (app->clock == NULL) { return true; } for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) { IEC104_ClockSynchronizationExecuteResult_t result = IEC104_ClockSynchronization_Execute_Next(app->clock, Example_Clock_Action, app); if (result == IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY || result == IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_WAIT) { return true; } if (result != IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_OK) { return false; } } return true; } /** * 功能:把示例支持的遥控命令地址映射为对应运行值数组的下标。 * 输入参数: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。 */ 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; } /** * 功能:执行已通过Accept校验并消费的选择/执行动作,仅修改模拟设备状态。 * 输入参数:context——当前ExampleApp;request——库仅在本次回调期间借出的工作请求,不得保存指针。 * 输出参数:选择置返校标志;执行置输出标志并更新对应单点/双点运行值,供后续返回信息及总召读取。 * 返回值:点表不匹配返回NOT_STARTED且没有副作用;模拟动作完成返回STARTED。 * 说明:STARTED表示选择生效或输出启动,不代替Report_Result。真实异步驱动应在设备完成后报告结果。 * 不在动作中发布任何上报、推进Slave或重入修改Command;库不保存本函数或context。 */ static IEC104_CommandActionResult_t Example_Command_Action(void *context, const IEC104_CommandRequest_t *request) { ExampleApp *app = (ExampleApp *)context; size_t index; if (!Example_Command_Point(request, &index)) { 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; } } return IEC104_COMMAND_ACTION_STARTED; } /** * 功能:为已启动的当前EXECUTE构造一帧COT=11的模拟位置返回信息,此处只组包,不提交。 * 输入参数:app——非NULL,command_event保留当前EXECUTE身份,运行值已由动作更新。 * 输出参数:app->command_return及其固定10字节缓冲,CA/OA沿用工作请求,IOA映射到对应遥信。 * 返回值:true为Type1或Type3单对象ASDU构造成功;false为点表或编解码失败,此时不可提交该ASDU。 * 说明:命令模块已拒绝TEST请求,因此清零公共头得到TEST=0;对象品质为示例约定的0。 * 局部信息对象由ASDU编码复制,离开函数不借用局部变量;ASDU仍借用app内固定字节缓冲。 * 成功后冻结这一帧,Submit_Return_Info的WAIT不重新组包;只有OK才可推进,避免同一变位重复上报。 */ static bool Example_Build_Command_Return(ExampleApp *app) { const IEC104_CommandRequest_t *request = &app->command_event.request; IEC60870_AsduHeader_t header; size_t index; if (!Example_Command_Point(request, &index)) { return false; } memset(&header, 0, sizeof(header)); header.cause_of_transmission = IEC60870_ASDU_COT_RETURN_INFO_REMOTE; header.common_address = request->common_address; header.originator_address = request->originator_address; 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) { 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 { 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; } } /** * 功能:消费遥控业务事件、处理应用决定,并推进结果报告/返回信息/Complete的持久化步骤。 * 输入参数:app——非NULL,所属Slave通知须先按FIFO分派;command可为NULL以兼容仅总召测试。 * 输出参数:更新应用持有事件、站点引用、模拟标志和重试步骤;不替代Command_Process提交协议应答。 * 返回值:true为本轮处理完成或遇到正常WAIT/STALE;false为事件身份、引用或接口/组包错误。 * 说明:先读取业务事件再重试旧交互,使STOP、断线、撤销产生的FINISHED能先清理过期步骤。 * 一个事务可能交付SELECT后再交付EXECUTE,但只持有一个应用站点引用,最后恰好按FINISHED清理。 * FINISHED不回滚设备值;本例动作同步完成,可直接清除模拟标志并Release。真实输出未结束时, * 应用须另存设备操作及站点引用,保持设备忙状态,不能照搬本例的立即释放。 */ static bool Example_Drain_Command(ExampleApp *app) { unsigned count; if (app->command == NULL) { return true; } for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) { IEC104_CommandEvent_t event; IEC104_CommandNextResult_t next = IEC104_Command_NextEvent(app->command, &event); if (next == IEC104_COMMAND_NEXT_EMPTY) { break; } if (next != IEC104_COMMAND_NEXT_READY) { return false; } if (event.type == IEC104_COMMAND_FINISHED) { /* 只清理当前应用持有的事务,不能让旧实例/旧事务的终结通知释放另一次输出资源。 * 模块槽可能仍因最后应答WAIT而占用;应用读到FINISHED也不能据此强行发起新动作。 */ if (event.instance_id != app->command_instance || event.transaction_id != app->command_transaction) { return false; } if (event.select_effect) { app->command_selected = false; } if (event.execute_effect) { app->command_output = false; } if (app->command_station != NULL && IEC104_Slave_Release_Station(app->slave, app->command_station) != IEC104_STATION_OK) { return false; } app->command_station = NULL; app->command_instance = app->command_transaction = 0U; app->command_pending = app->command_waiting = false; app->command_step = 0U; } else { if (app->command_transaction == 0U) { /* 先按注册编号找到原站并Acquire,再保存引用及事务身份;同CA重新注册不能替代原站。 * 模块自身的引用可能在FINISHED出队时释放,不能借它维持应用context的生命周期。 */ IEC104_Station_t *station = IEC104_Slave_Get_Station_By_Id(app->slave, event.request.station_id); if (station == NULL || IEC104_Slave_Acquire_Station(app->slave, station) != IEC104_STATION_OK) { return false; } app->command_station = station; app->command_instance = event.instance_id; app->command_transaction = event.transaction_id; } else if (app->command_instance != event.instance_id || app->command_transaction != event.transaction_id || app->command_pending) { return false; } /* 按值保存,不持有NextEvent局部变量的地址;匹配执行覆盖工作事件,继续保留同一站点引用。 */ app->command_event = event; app->command_pending = true; app->command_waiting = false; } } if (app->command_pending) { IEC104_CommandReplyResult_t reply; size_t index; /* context和点表属于应用策略;格式已合法但不属于本应用的点仍明确回UNKNOWN_IOA。 */ 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 { reply = IEC104_Command_Accept(app->command, &app->command_event, Example_Command_Action, app); } app->command_waiting = reply == IEC104_COMMAND_REPLY_WAIT; /* WAIT未消费工作事件、未调用动作;保留身份,下轮先处理Slave通知再重试。 * STALE不再重交动作,只保留事务归属等待FINISHED;OK也仅表示事件被消费。 */ if (app->command_waiting) { return true; } if (reply != IEC104_COMMAND_REPLY_OK && reply != IEC104_COMMAND_REPLY_STALE) { return false; } app->command_pending = false; if (reply == IEC104_COMMAND_REPLY_OK && app->command_event.type == IEC104_COMMAND_EXECUTE) { IEC104_CommandTransactionInfo_t info; /* Reject和动作NOT_STARTED/FAILED也可能返回OK,必须确认仍为EXECUTING才能报告设备结果。 * 返回信息此时可先构造,但Submit_Return_Info会等待肯定执行确认已经交接。 */ if (IEC104_Command_Get_Transaction(app->command, &app->command_event, &info) && info.active && info.phase == IEC104_COMMAND_PHASE_EXECUTING) { if (!Example_Build_Command_Return(app)) { return false; } app->command_step = 1U; } } } /* 模拟动作已经完成,先报告结果停止执行计时;确认是否已交接不影响本次报告。 * 每轮至多推进三个步骤。每步仅OK后递增;返回信息OK已经固定帧序,不能因Complete等待而重发。 */ for (count = 0U; count < 3U && app->command_step != 0U; ++count) { IEC104_CommandReplyResult_t reply; if (app->command_step == 1U) { reply = IEC104_Command_Report_Result(app->command, &app->command_event, IEC104_COMMAND_RESULT_SUCCEEDED); } else if (app->command_step == 2U) { reply = IEC104_Command_Submit_Return_Info(app->command, &app->command_event, &app->command_return); } else { reply = IEC104_Command_Complete(app->command, &app->command_event); } app->command_waiting = reply == IEC104_COMMAND_REPLY_WAIT; /* WAIT保持当前步骤及完整ASDU;STALE停止后续提交,引用与模拟标志交给FINISHED统一清理。 * Complete的OK只关闭返回信息阶段,ACT_TERM交接及FINISHED仍由后续Process/NextEvent推进。 */ if (app->command_waiting) { return true; } if (reply == IEC104_COMMAND_REPLY_STALE) { app->command_step = 0U; return true; } if (reply != IEC104_COMMAND_REPLY_OK) { return false; } app->command_step = app->command_step == 3U ? 0U : app->command_step + 1U; } return true; } #ifndef IEC104_SLAVE_EXAMPLE_NO_MAIN /** * 功能:为命令行示例创建默认遥控实例,与仅总召的初始化入口分开。 * 输入参数:app——非NULL,Slave句柄和站点已创建,尚未开始消费Slave通知。 * 输出参数:成功后保存app->command;失败不创建可用实例。 * 返回值:true为创建成功;false为重复初始化、配置/归属错误或静态池没有空位。 * 说明:默认45/46均须先选择,决定15秒、选择30秒、执行60秒;本函数不执行设备动作。 * 每Slave只能绑定一个Command。main在Slave_Init前创建亦可,但必须早于首条生命周期通知。 */ static bool Example_Init_Command(ExampleApp *app) { IEC104_CommandConfig_t config; if (app->command != NULL) { return false; } IEC104_Command_Config_Default(&config); app->command = IEC104_Command_Create(app->slave, &config); return app->command != NULL; } #endif /* 统一取消应用业务;app为有效应用上下文,未创建的可选模块直接跳过,无返回值。 * 遥控Cancel丢弃模块持有应答但保留应交付FINISHED,不回滚已经执行的输出。 * 本函数仅刷新状态,调用方还须Drain_Command读取终结通知、归还应用引用,再关闭Slave或Destroy。 */ static void Example_Cancel(ExampleApp *app) { IEC104_Interrogation_Cancel(app->interrogation); if (app->counter != NULL) { IEC104_Interrogation_Cancel(app->counter); (void)IEC104_Interrogation_Get_Status(app->counter, &app->ci); } if (app->clock != NULL) { IEC104_ClockSynchronization_Cancel(app->clock); (void)IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs); } if (app->command != NULL) { IEC104_Command_Cancel(app->command); (void)IEC104_Command_Get_Status(app->command, &app->command_status); } (void)IEC104_Interrogation_Get_Status(app->interrogation, &app->gi); } /* 每次等待前重新查询剩余期限,避免采样/业务处理耗时后继续使用旧的毫秒数。 * 输入app为有效上下文,maximum_ms为调用方等待上限;返回0表示本地立即续跑,否则为缩短后的上限。 * 查询失败也返回0,让随后推进路径处理错误;本函数只查状态,不消费事件或触发业务到期转换。 * 本地可推进时不休眠;否则本例网络等待最多50ms,并受最近业务期限限制。 */ static uint32_t Example_Wait_Time(ExampleApp *app, uint32_t maximum_ms) { if (app->command != NULL) { if (!IEC104_Command_Get_Status(app->command, &app->command_status)) { return 0U; } /* 未读Slave通知、FINISHED等业务事件及模块本地进度都须立即处理。 * 应用持有事件/收尾步骤不代表能立即发送:上次WAIT时允许Poll等待,避免确认拥塞造成空转。 */ if (app->command_status.events_pending || app->command_status.pending_events != 0U || app->command_status.need_process || ((app->command_pending || app->command_step != 0U) && !app->command_waiting)) { return 0U; } if (app->command_status.timer_active && app->command_status.timeout_remaining_ms < maximum_ms) { maximum_ms = app->command_status.timeout_remaining_ms; } } if (!IEC104_Interrogation_Get_Status(app->interrogation, &app->gi)) { return 0U; } if (app->counter != NULL) { if (!IEC104_Interrogation_Get_Status(app->counter, &app->ci)) { return 0U; } if (app->ci.events_pending || app->ci.pending_events != 0U || app->ci.need_process) { return 0U; } if (app->ci.timer_active && app->ci.timeout_remaining_ms < maximum_ms) { maximum_ms = app->ci.timeout_remaining_ms; } } if (app->clock != NULL) { if (!IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs)) { return 0U; } if (app->cs.events_pending || app->cs.execute_pending || app->cs.need_process) { return 0U; } if (app->cs.timer_active && app->cs.timeout_remaining_ms < maximum_ms) { maximum_ms = app->cs.timeout_remaining_ms; } } if (app->events_pending || app->gi.events_pending || app->gi.pending_events != 0U || app->gi.need_process) { return 0U; } if (app->gi.timer_active && app->gi.timeout_remaining_ms < maximum_ms) { return app->gi.timeout_remaining_ms; } return maximum_ms; } /* 状态事件交给所有实例;I帧依次由总召、计数量、时钟同步、遥控及兜底接手。 * 输入app为有效上下文,event是刚由Slave_NextEvent交付的借用事件,本函数不保存它的指针。 * 返回true表示分派完成(包括模块拒绝/丢弃),false表示业务入口或状态查询失败,应统一收尾。 * 生命周期通知不能因前一个模块HANDLED而短路,否则遥控可能漏读STOP/换代后继续执行旧请求。 * Handle_Event负责消费这些通知,不能在它之前用Check_Request等待通知读空而阻断分派。 */ static bool Example_Handle_Event(ExampleApp *app, const IEC104_SlaveEvent_t *event) { IEC104_InterrogationEventResult_t result; /* Receiver已过滤线上基础格式错误;仍防御性过滤不可用视图,避免误交给业务分派或未知类型兜底。 */ if (event->transport.type == IEC104_TCP_EVENT_I_FRAME && event->asdu_result != IEC60870_ASDU_OK) { return true; } result = IEC104_Interrogation_Handle_Event(app->interrogation, event); if (app->counter != NULL && (event->transport.type != IEC104_TCP_EVENT_I_FRAME || result == IEC104_INTERROGATION_EVENT_IGNORED)) { IEC104_InterrogationEventResult_t counter_result = IEC104_Interrogation_Handle_Event(app->counter, event); if (counter_result == IEC104_INTERROGATION_EVENT_ERROR || !IEC104_Interrogation_Get_Status(app->counter, &app->ci)) { return false; } if (result == IEC104_INTERROGATION_EVENT_IGNORED) { result = counter_result; } } if (result == IEC104_INTERROGATION_EVENT_ERROR || !IEC104_Interrogation_Get_Status(app->interrogation, &app->gi)) { return false; } if (app->clock != NULL && (event->transport.type != IEC104_TCP_EVENT_I_FRAME || result == IEC104_INTERROGATION_EVENT_IGNORED)) { IEC104_ClockSynchronizationEventResult_t clock_result = IEC104_ClockSynchronization_Handle_Event(app->clock, event); if (clock_result == IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR || !IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs)) { return false; } if (clock_result != IEC104_CLOCK_SYNCHRONIZATION_EVENT_IGNORED && event->transport.type == IEC104_TCP_EVENT_I_FRAME) { return true; } } if (app->command != NULL && (event->transport.type != IEC104_TCP_EVENT_I_FRAME || result == IEC104_INTERROGATION_EVENT_IGNORED)) { /* HANDLED也包括格式否定、实例忙和过载丢弃,不能再走44兜底;未启用类型仍可继续分派。 */ IEC104_CommandEventResult_t command_result = IEC104_Command_Handle_Event(app->command, event); if (command_result == IEC104_COMMAND_EVENT_ERROR || !IEC104_Command_Get_Status(app->command, &app->command_status)) { return false; } if (command_result != IEC104_COMMAND_EVENT_IGNORED && event->transport.type == IEC104_TCP_EVENT_I_FRAME) { return true; } } if (result != IEC104_INTERROGATION_EVENT_IGNORED || event->transport.type != IEC104_TCP_EVENT_I_FRAME || !app->gi.active || event->transport.generation != app->gi.generation) { return true; } /* 当前事件当场提交,必须在NextEvent读取后续通知之前调用;应用不保存兜底请求。 * HIGH满只丢本条并计数;原连接停用/断开则取消,不跨启停续发。 */ ++app->other_rejected; switch (IEC104_Slave_Reject_Request(app->slave, event, IEC60870_ASDU_COT_UNKNOWN_TYPE)) { case IEC104_TCP_SUBMIT_OK: ++app->other_submitted; break; case IEC104_TCP_SUBMIT_WAIT: ++app->other_overloads; break; case IEC104_TCP_SUBMIT_FATAL: /* 可能已写出字节;cancelled仅表示不再重试,继续读取断线通知。 */ ++app->other_cancelled; break; default: ++app->other_cancelled; break; } return true; } /* 两种业务各自推进协议、消费业务事件并刷新状态,供外层轮换调用次序。 */ static bool Example_Step_General(ExampleApp *app) { return IEC104_Interrogation_Process(app->interrogation) && Example_Drain_Business(app) && IEC104_Interrogation_Get_Status(app->interrogation, &app->gi); } /* 仅总召测试不创建计数量实例,此时直接完成本轮;已创建时采用相同推进顺序。 */ static bool Example_Step_Counter(ExampleApp *app) { return app->counter == NULL || (IEC104_Interrogation_Process(app->counter) && Example_Drain_Counter(app) && IEC104_Interrogation_Get_Status(app->counter, &app->ci)); } static bool Example_Step_Clock(ExampleApp *app) { return app->clock == NULL || (IEC104_ClockSynchronization_Process(app->clock) && Example_Drain_Clock(app) && IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs)); } /* 先推进遥控,随后两类召唤轮换、对时;发送WAIT后下一轮继续,不只靠need_process重试。 * app为有效上下文;返回true表示本轮成功或因未读通知暂缓,false交给主循环停止并清理。 * Command_Process只推进协议应答,Drain_Command才执行应用决定及结果/返回信息步骤,最后刷新调度快照。 * 通知预算尚未读空时不能调用Accept;否则可能在尚未分派的STOP或撤销之后错误启动设备。 */ static bool Example_Step(ExampleApp *app) { if (app->events_pending) { return true; } if (app->command != NULL && (!IEC104_Command_Process(app->command) || !Example_Drain_Command(app) || !IEC104_Command_Get_Status(app->command, &app->command_status))) { return false; } app->counter_first = !app->counter_first; if (app->counter_first) { if (!Example_Step_Counter(app) || !Example_Step_General(app)) { return false; } } else { if (!Example_Step_General(app) || !Example_Step_Counter(app)) { return false; } } return Example_Step_Clock(app); } /* 有界消费通知。预算用完先续读,不能跨过排在后面的STOP/START直接续发旧总召或执行旧遥控。 * 只有读到EMPTY才清events_pending并允许Example_Step推进动作,避免用最终激活状态掩盖中间STOP。 */ static bool Example_Drain(ExampleApp *app) { unsigned count; IEC104_SlaveEvent_t event; 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; } if (result != IEC104_TCP_EVENT_READY || !Example_Handle_Event(app, &event)) { return false; } } app->events_pending = true; return true; } #ifndef IEC104_SLAVE_EXAMPLE_NO_MAIN #ifndef _WIN32 #error "This executable uses the Windows network backend" #endif #ifndef _WIN32_WINNT /* GetTickCount64所需的最低Windows API声明;这里只用于示例运行时长,不处理业务时标。 */ #define _WIN32_WINNT 0x0600 #endif #ifndef WIN32_LEAN_AND_MEAN /* 只引入本入口需要的Windows基础声明,socket操作仍全部留在协议库的Net后端。 */ #define WIN32_LEAN_AND_MEAN #endif #include #include #include /* 信号处理器和主循环之间只交换停止标志;静态初始化为0,不在信号处理器中访问任务或网络。 */ static volatile sig_atomic_t example_stop; /** * 功能:在已注册的停止信号到来时请求主循环退出。 * 输入参数:signal_number——运行库传入的信号编号,这里不区分SIGINT与SIGTERM。 * 输出参数:example_stop置1。 * 返回值:无。 * 说明:信号上下文只写sig_atomic_t,不打印、不分配、不调用Slave;资源由main正常收尾。 */ static void Example_Stop(int signal_number) { (void)signal_number; example_stop = 1; } /** * 功能:把纯十进制命令行文本解析为不超过max的unsigned long。 * 输入参数:text——非NULL且以NUL结束的字符串;max——此选项允许的最大值。 * 输出参数:value——非NULL输出存储,仅成功时写入;失败保持原值。 * 返回值:true为整个字符串解析成功且未越界;false为空串、符号/空白前缀、尾部字符或溢出。 * 说明:先检查再截窄为端口、CA或窗口类型,避免越界数值被静默截断;允许前导零及数值0。 * 某些选项禁止0,由main另查;端口0表示自动分配,run-ms=0表示持续运行。 */ static bool Example_Number(const char *text, unsigned long max, unsigned long *value) { char *end; unsigned long parsed; if (text[0] < '0' || text[0] > '9') { return false; } /* strtoul通过errno报告数值溢出;先清零,避免沿用上一次库调用的错误。 */ errno = 0; parsed = strtoul(text, &end, 10); if (errno != 0 || *end != '\0' || parsed > max) { return false; } *value = parsed; return true; } /** * 功能:向标准输出打印本示例支持的选项、默认监听配置及模拟点地址范围。 * 输入参数:program——非NULL程序名称,main传入argv[0]。 * 输出参数:无,文本写入stdout。 * 返回值:无。 * 说明:本函数不创建服务或退出进程;main根据--help或缺少参数分别选择成功/失败退出。 */ static void Example_Usage(const char *program) { printf("Usage: %s [--bind IPv4] [--port 0..65535] [--ca 1..65534]\n" " [--window 1..32767] [--run-ms milliseconds] [--clock simulated|system]\n" "Defaults: 127.0.0.1:2404, CA=1, k=12, run until Ctrl+C.\n" "GI: QOI=20 (all), 21 (single/double signals), 22 (scaled); IOA=0.\n" "Single signals 1001..1008, double signals 1101..1108, scaled values 2001..2008.\n" "COT=8 cancels matching GI.\n" "Counters 3001..3008: RQT=5 (all), 1/2 (groups); Type 15, four per batch.\n" "FRZ=0 reads frozen values; 1 freezes; 2 freezes/resets; 3 resets.\n" "Freeze/reset completes with ACT_CON only; TEST actions and COT=8 are rejected.\n" "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); } /** * 功能:配置并运行子站示例,退出时关闭服务、读空通知并归还静态槽。 * 输入参数:argc/argv——命令行;除--help外,每个选项后都须有一个值。 * 输出参数:stdout输出帮助、监听端点和累计统计;stderr输出参数或运行失败信息。 * 返回值:EXIT_SUCCESS为帮助输出或正常停止;参数、初始化、运行或清理失败为EXIT_FAILURE。 * 说明:服务生命周期为Create -> Init -> 循环Poll/Drain/Step -> Close -> 读空事件 -> Destroy。 * main拥有Slave句柄,app只借用;静态app和库内任务在整个运行期间保持地址不变。 * run-ms使用单调毫秒时钟,在循环边界检查,表示约定运行时长而非精确的退出截止时刻。 */ int main(int argc, char **argv) { static ExampleApp app; /* 注册站点及业务借用的存储在退出前保持地址不变。 */ IEC104_SlaveConfig_t config; /* 本地配置,Slave_Init会复制,不在运行中借用它。 */ IEC104_TcpError_t error; /* 最近一次服务接口调用的诊断,与历史网络错误分开。 */ IEC104_TcpServerStatus_t status; /* 启动后查询实际端口,尤其用于--port=0。 */ IEC104_Slave_t *slave; /* main持有的唯一服务句柄,清理结束后不再使用。 */ IEC104_TcpResult_t result; /* 服务结果须显式比较IEC104_TCP_OK,不能作布尔判断。 */ uint16_t common_address = 1U; /* 应用地址校验条件,不是TCP端口或单点IOA。 */ unsigned long run_ms = 0U; /* 0为持续运行,非零为命令行指定的运行毫秒数。 */ bool clock_system = false; /* 默认模拟;解析后再写入Example_Init清零后的app。 */ uint64_t started; /* 开始主循环前的单调时刻,与CP56日历时间无关。 */ int arg, exit_code = EXIT_SUCCESS; /* arg遍历选项和值;任一阶段失败后保留失败退出码。 */ /* 第1步:采用库的协议默认值,只把示例默认监听地址收窄到本机回环地址。 */ IEC104_Slave_Config_Default(&config); memcpy(config.bind_address, "127.0.0.1", sizeof("127.0.0.1")); for (arg = 1; arg < argc; ++arg) { unsigned long value; /* 先在宽整数中验证范围,再赋给配置中的较窄字段。 */ const char *option = argv[arg]; if (strcmp(option, "--help") == 0) { Example_Usage(argv[0]); return EXIT_SUCCESS; } /* 非帮助选项必须有值;此处前进一次取值,for尾部再前进一次到下一个选项。 */ if (++arg == argc) { Example_Usage(argv[0]); return EXIT_FAILURE; } if (strcmp(option, "--bind") == 0) { /* 这里只保证固定数组能容纳含NUL的文本,IPv4语法由Slave_Init中的Net后端检查。 */ if (strlen(argv[arg]) >= sizeof(config.bind_address)) { return EXIT_FAILURE; } strcpy(config.bind_address, argv[arg]); } else if (strcmp(option, "--port") == 0 && Example_Number(argv[arg], UINT16_MAX, &value)) { config.port = (uint16_t)value; } else if (strcmp(option, "--ca") == 0 && Example_Number(argv[arg], 65534U, &value) && value != 0U) { common_address = (uint16_t)value; } else if (strcmp(option, "--window") == 0 && Example_Number(argv[arg], IEC104_SEQUENCE_MASK, &value) && value != 0U) { config.protocol.link.k = (uint16_t)value; } else if (strcmp(option, "--run-ms") == 0 && Example_Number(argv[arg], UINT32_MAX, &value)) { run_ms = value; } else if (strcmp(option, "--clock") == 0 && strcmp(argv[arg], "simulated") == 0) { clock_system = false; } else if (strcmp(option, "--clock") == 0 && strcmp(argv[arg], "system") == 0) { clock_system = true; } else { fprintf(stderr, "Invalid option/value: %s %s\n", option, argv[arg]); return EXIT_FAILURE; } } /* 第2步:取得静态池句柄后绑定应用上下文,再初始化实际网络监听。 */ slave = IEC104_Slave_Create(); if (slave == NULL) { fputs("No free Slave service slot\n", stderr); return EXIT_FAILURE; } if (!Example_Init(&app, slave, common_address, NULL, 0U)) { (void)IEC104_Slave_Destroy(slave, NULL); return EXIT_FAILURE; } if (!Example_Init_Counters(&app, NULL, 0U)) { (void)IEC104_Interrogation_Destroy(app.interrogation); (void)IEC104_Slave_Remove_Station(slave, &app.station); (void)IEC104_Slave_Destroy(slave, NULL); return EXIT_FAILURE; } if (!Example_Init_Clock(&app, NULL)) { (void)IEC104_Interrogation_Destroy(app.interrogation); (void)IEC104_Interrogation_Destroy(app.counter); (void)IEC104_Slave_Remove_Station(slave, &app.station); (void)IEC104_Slave_Destroy(slave, NULL); return EXIT_FAILURE; } /* 在网络监听及首条Slave通知之前绑定遥控实例;失败时尚无事务,按创建的逆序归还业务对象。 */ if (!Example_Init_Command(&app)) { (void)IEC104_ClockSynchronization_Destroy(app.clock); (void)IEC104_Interrogation_Destroy(app.counter); (void)IEC104_Interrogation_Destroy(app.interrogation); (void)IEC104_Slave_Remove_Station(slave, &app.station); (void)IEC104_Slave_Destroy(slave, NULL); return EXIT_FAILURE; } app.clock_system = clock_system; 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); /* Init失败仍须释放已创建的业务模块和Slave槽。 */ (void)IEC104_Interrogation_Destroy(app.interrogation); (void)IEC104_Interrogation_Destroy(app.counter); (void)IEC104_ClockSynchronization_Destroy(app.clock); (void)IEC104_Command_Destroy(app.command); (void)IEC104_Slave_Remove_Station(slave, &app.station); (void)IEC104_Slave_Destroy(slave, NULL); return EXIT_FAILURE; } /* 监听成功后取得系统实际绑定的端口,不能把配置中的0当作可连接端口输出。 */ if (IEC104_Slave_Get_Status(slave, &status, &error) != IEC104_TCP_OK) { exit_code = EXIT_FAILURE; goto cleanup; } /* 第3步:安装仅置标志的信号处理器;真正的资源关闭仍在主线程执行。 */ (void)signal(SIGINT, Example_Stop); (void)signal(SIGTERM, Example_Stop); started = GetTickCount64(); printf("LISTEN %s:%u CA=%u\n", config.bind_address, (unsigned)status.bound_port, (unsigned)common_address); fflush(stdout); /* 让终端或通过管道启动本程序的测试立即取得实际监听端点。 */ while (!example_stop && (run_ms == 0U || GetTickCount64() - started < run_ms)) { /* 有待续读通知或可立即提交的任务时安排0等待;WAIT则交回网络循环,等待上限50ms。 * Poll还会按库内本地工作和协议期限缩短等待;50ms只约束等待,不保证整轮执行耗时。 */ uint32_t wait_ms = Example_Wait_Time(&app, 50U); result = IEC104_Slave_Poll(slave, wait_ms, &error); if (result != IEC104_TCP_OK) { /* 客户端普通断线由事件表达;这里收到的是服务接口失败,结束循环统一清理。 */ fprintf(stderr, "Poll failed: result=%d stage=%d socket=%d\n", result, error.stage, error.socket_error); exit_code = EXIT_FAILURE; break; } /* 先消费状态变化再推进业务;Drain失败会短路,不继续对不可信状态提交应答。 */ if (!Example_Drain(&app) || !Example_Step(&app)) { fputs("Application event/build failure\n", stderr); exit_code = EXIT_FAILURE; break; } } cleanup: /* 第4步:停止应用任务,再结束网络;取消只丢弃尚未提交完毕的任务,不重发已接手数据。 */ Example_Cancel(&app); /* Cancel只将终结通知排队;释放业务实例前必须读完,并清理应用自己的遍历状态及资源。 * 遥控FINISHED在离线时仍可读;这里只释放已同步完成的模拟输出,真实设备须自行等实际结束。 */ if (!Example_Drain_Business(&app) || !Example_Drain_Counter(&app) || !Example_Drain_Command(&app)) { exit_code = EXIT_FAILURE; } if (IEC104_Slave_Close(slave, &error) != IEC104_TCP_OK) { exit_code = EXIT_FAILURE; } { IEC104_SlaveEvent_t event; IEC104_TcpEventResult_t next; /* Close保留收尾通知。对时和遥控只消费非I帧状态,不再受理请求或执行动作。 * 即使Cancel已经终结事务,也须交付断线通知让模块清除会话;首次终结原因不会被改写。 */ while ((next = IEC104_Slave_NextEvent(slave, &event)) == IEC104_TCP_EVENT_READY) { if (app.clock != NULL && event.transport.type != IEC104_TCP_EVENT_I_FRAME && IEC104_ClockSynchronization_Handle_Event(app.clock, &event) == IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR) { exit_code = EXIT_FAILURE; } if (app.command != NULL && event.transport.type != IEC104_TCP_EVENT_I_FRAME && IEC104_Command_Handle_Event(app.command, &event) == IEC104_COMMAND_EVENT_ERROR) { exit_code = EXIT_FAILURE; } } if (next != IEC104_TCP_EVENT_EMPTY) { exit_code = EXIT_FAILURE; } } if (!IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs)) { exit_code = EXIT_FAILURE; } /* Close期间新产生的终结仍需读完;刷新统计后才能Destroy,不能用Destroy代替FINISHED清理。 */ if (!Example_Drain_Command(&app) || !IEC104_Command_Get_Status(app.command, &app.command_status)) { exit_code = EXIT_FAILURE; } /* 只有关闭且FIFO读空后才能归还槽;清理失败也必须体现在程序退出码中。 * Command还要求事务/独立应答全部清空;应用站点引用已在FINISHED分支释放,再Remove_Station。 */ if (!IEC104_Interrogation_Destroy(app.interrogation)) { exit_code = EXIT_FAILURE; } if (!IEC104_Interrogation_Destroy(app.counter)) { exit_code = EXIT_FAILURE; } if (!IEC104_ClockSynchronization_Destroy(app.clock)) { exit_code = EXIT_FAILURE; } if (!IEC104_Command_Destroy(app.command)) { exit_code = EXIT_FAILURE; } 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", 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); return exit_code; } #endif