Files
STM32F4-Base/IEC60870-Slave/examples/iec104_slave_example.c
T
STF cd42b4d61d
CI / L0 静态门禁 (pre-commit) (push) Failing after 2m51s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 18s
添加遥控业务代码
2026-10-30 20:03:19 -07:00

1073 lines
66 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 <windows.h>
#include <errno.h>
#include <signal.h>
/* 信号处理器和主循环之间只交换停止标志;静态初始化为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