Files
STM32F4-Base/IEC60870-Slave/examples/iec104_slave_example.c
T
STF 4c55544f05
CI / L0 静态门禁 (pre-commit) (push) Failing after 45s
CI / L0.5 主机侧单测 (MSVC) (push) Failing after 42s
通信协议代码
2026-10-26 08:31:48 -04:00

449 lines
25 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子站示例:模拟点表、其他命令的兜底应答及main由应用提供。
* 总召状态机位于库模块IEC104_Interrogation.c,不在本文件重复实现。
* 每轮Slave_Poll -> NextEvent逐条交付 -> Interrogation_Process;所有调用串行。
* 总召每批4点,模块自动推进至需要等待;8个遥信和8个遥测组成6个响应ASDU。
* QOI=21只读遥信,22只读遥测,其余分组由应用拒绝;分组点表策略不写进协议库。
* 同一连接至多一个未终结总召,忙时不区分QOI均否定;任务满时拒绝/计数丢弃新请求,保留当前连接。
* 总召模块处理WAIT、STOPDT及断线;应用读取业务事件,选择类型并显式交付ASDU。
* 不支持的其他命令由本例保存一份请求,沿原connection回COT=44;该槽满则关闭原连接。
* 默认127.0.0.1:2404、CA=1;构建命令见同目录README.md。
*/
#include "IEC104_Interrogation.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define EXAMPLE_EVENT_BUDGET 32U
/* 模拟遥信点表:ioa为24位信息对象地址,value为0/1,quality为SIQ品质位(这里均为0)。
* 表项不是线上报文,后续由SinglePointInformation_Create及ASDU容器编码成Type=1。
* 点表只读,因此总召期间数值保持一致;接真实数据源时应另行确定快照或逐点采样策略。 */
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]))
/* 一次总召的应用游标;真实设备也可在BEGIN事件后保存本次快照。 */
typedef struct { size_t single_index, scaled_index; } ExampleGiData;
typedef struct {
IEC104_Slave_t *slave; /* main拥有;本上下文仅借用。 */
IEC104_Interrogation_t *interrogation; /* 库内不透明总召任务对象。 */
IEC104_InterrogationStatus_t gi; /* 只读快照,用于调度等待及统计。 */
ExampleGiData gi_data; /* 应用拥有的游标,库内没有session或回调参数。 */
uint64_t session_instance, session_request; /* 只清理对应请求的快照。 */
IEC104_InterrogationEvent_t business_event; /* 纯值,可保存到后续循环应答。 */
bool business_pending, batch_prepared;
int batch_result; /* 本例构造结果:-1失败、0数据结束、1有数据。 */
IEC60870_Asdu_t batch; /* 借用本应用bytes;Submit_Batch(OK)后可复用。 */
uint8_t batch_bytes[IEC104_MAX_ASDU_LEN];
IEC104_SlaveEvent_t other_request; /* 非总召命令的固定地址待发送副本。 */
bool other_pending, other_waiting, events_pending;
unsigned long other_submitted, other_waits, other_rejected, other_cancelled, other_overloads;
} ExampleApp;
/* 普通应用函数,只由下面的事件分支直接调用,不注册到库中。
* 为NEED_DATA构造一种类型的一批;FULL不跳过当前点。应答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;
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) {
while (data->single_index < EXAMPLE_SIGNAL_COUNT && asdu->object_count < request->max_objects_per_asdu) {
size_t index = data->single_index;
IEC60870_SinglePointInformation_t point;
IEC60870_AsduResult_t added;
if (IEC60870_SinglePointInformation_Create(&point, example_signals[index].ioa,
example_signals[index].value, 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; }
++data->single_index;
}
} else if (data->scaled_index < EXAMPLE_MEASUREMENT_COUNT) {
while (data->scaled_index < EXAMPLE_MEASUREMENT_COUNT && asdu->object_count < request->max_objects_per_asdu) {
size_t index = data->scaled_index;
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; }
++data->scaled_index;
}
} else { return 0; }
return asdu->object_count != 0U ? 1 : -1;
}
/* 第二条事件流:应用主动取业务事件,再显式应答;没有库调用应用函数的路径。
* 常量点表立即就绪;异步数据源可在NEED_DATA保留business_event,完成采样后再Submit。
* Reply的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组。
* 确认接受后建立游标,下一轮Process才提交ACT_CON。真实快照可在应答前准备。 */
reply = IEC104_Interrogation_Reply_Begin(app->interrogation, event, accept);
if (reply == IEC104_INTERROGATION_REPLY_OK && accept) {
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->session_instance = event->instance_id;
app->session_request = event->request_id;
}
break;
}
case IEC104_INTERROGATION_NEED_DATA:
if (!app->batch_prepared) {
app->batch_result = Example_Build_Batch(app, &event->request);
app->batch_prepared = true;
}
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); }
break;
case IEC104_INTERROGATION_FINISHED:
/* 完成、本地取消、主站撤销、超时、断线和失败统一清理。
* 真实异步采样还需在这里停止对应任务;迟到的旧FINISHED不清理新请求资源。 */
if (event->instance_id == app->session_instance && event->request_id == app->session_request) {
memset(&app->gi_data, 0, sizeof(app->gi_data));
app->session_instance = app->session_request = 0U;
}
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->business_pending = app->batch_prepared = false;
}
return true;
}
/* 必须在开始消费Slave事件之前创建业务模块;配置按值复制,不借用本地变量。 */
static bool Example_Init(ExampleApp *app, IEC104_Slave_t *slave, uint16_t common_address)
{
IEC104_InterrogationConfig_t config;
memset(app, 0, sizeof(*app));
app->slave = slave;
IEC104_Interrogation_Config_Default(&config);
config.common_address = common_address;
config.max_objects_per_asdu = 4U;
app->interrogation = IEC104_Interrogation_Create(slave, &config);
return app->interrogation != NULL;
}
/* 仅清应用自己的兜底应答;总召模块独立处理其任务。 */
static void Example_Cancel_Other(ExampleApp *app)
{
if (app->other_pending) { ++app->other_cancelled; }
app->other_pending = app->other_waiting = false;
memset(&app->other_request, 0, sizeof(app->other_request));
}
static void Example_Cancel(ExampleApp *app)
{
IEC104_Interrogation_Cancel(app->interrogation);
Example_Cancel_Other(app);
(void)IEC104_Interrogation_Get_Status(app->interrogation, &app->gi);
}
/* 每次等待前重新查询剩余期限,避免采样/业务处理耗时后继续使用旧的毫秒数。
* 本例网络等待最多50ms;业务期限更近则提前返回,由下一轮Process实际处理超时。 */
static uint32_t Example_Wait_Time(ExampleApp *app, uint32_t maximum_ms)
{
if (!IEC104_Interrogation_Get_Status(app->interrogation, &app->gi)) { return 0U; }
if (app->events_pending || app->gi.events_pending || app->gi.pending_events != 0U ||
(app->gi.pending_requests != 0U && !app->gi.waiting) ||
(app->other_pending && !app->other_waiting && app->gi.pending_requests == 0U)) { return 0U; }
if (app->gi.timer_active && app->gi.timeout_remaining_ms < maximum_ms) { return app->gi.timeout_remaining_ms; }
return maximum_ms;
}
/* 所有状态事件广播给总召模块;只有未接手的I帧进入其他业务/未知类型兜底。
* 后续总电度可在IGNORED分支接入专用处理器,再决定是否回复UNKNOWN_TYPE。 */
static bool Example_Handle_Event(ExampleApp *app, const IEC104_SlaveEvent_t *event)
{
IEC104_InterrogationEventResult_t result =
IEC104_Interrogation_Handle_Event(app->interrogation, event);
if (result == IEC104_INTERROGATION_EVENT_ERROR ||
!IEC104_Interrogation_Get_Status(app->interrogation, &app->gi)) { return false; }
if (!app->gi.active || (app->other_pending &&
app->other_request.transport.generation != app->gi.generation)) {
Example_Cancel_Other(app);
}
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; }
if (app->other_pending) {
IEC104_TcpResult_t closed = IEC104_Slave_Close_Connection(app->slave, event, NULL);
++app->other_overloads;
Example_Cancel(app);
return closed == IEC104_TCP_OK || closed == IEC104_TCP_NOT_CONNECTED ||
closed == IEC104_TCP_STALE_GENERATION;
}
if (!IEC104_Slave_Copy_Event(&app->other_request, event)) { return false; }
app->other_pending = true;
++app->other_rejected;
return true;
}
/* 先推进总召;其他命令的兜底应答等当前总召队列为空后再提交,WAIT保留原文。 */
static bool Example_Step(ExampleApp *app)
{
IEC104_SlaveEvent_t *request = &app->other_request;
if (app->events_pending) { return true; }
if (!IEC104_Interrogation_Process(app->interrogation) ||
!Example_Drain_Business(app) ||
!IEC104_Interrogation_Get_Status(app->interrogation, &app->gi)) { return false; }
if (app->gi.events_pending || app->gi.pending_requests != 0U || !app->other_pending) { return true; }
app->other_waiting = false;
if (!app->gi.active || request->transport.generation != app->gi.generation) {
Example_Cancel_Other(app);
return true;
}
if (IEC60870_Asdu_Set_COT(&request->asdu, IEC60870_ASDU_COT_UNKNOWN_TYPE) != IEC60870_ASDU_OK ||
IEC60870_Asdu_Set_Negative(&request->asdu, 1) != IEC60870_ASDU_OK) { return false; }
if (IEC60870_MasterConnection_Send_ASDU(&request->connection, &request->asdu)) {
++app->other_submitted;
app->other_pending = false;
} else if (IEC104_Slave_Get_Last_Submit_Result(&request->connection) == IEC104_TCP_SUBMIT_WAIT) {
++app->other_waits;
app->other_waiting = true;
} else { Example_Cancel_Other(app); }
return true;
}
/* 有界消费通知。预算用完先续读,不能跨过排在后面的STOP/START直接续发旧总召。 */
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]\n"
"Defaults: 127.0.0.1:2404, CA=1, k=12, run until Ctrl+C.\n"
"GI: QOI=20 (all), 21 (signals), 22 (scaled); IOA=0.\n"
"Signals 1001..1008, scaled values 2001..2008; COT=8 cancels matching GI.\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为持续运行,非零为命令行指定的运行毫秒数。 */
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 {
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)) {
(void)IEC104_Slave_Destroy(slave, NULL);
return EXIT_FAILURE;
}
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_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只将终结通知排队;释放总召实例前必须读完,并释放应用自己的快照。 */
if (!Example_Drain_Business(&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保留收尾通知。退出阶段只读空,不再交给业务分派或创建新的应答任务。 */
while ((next = IEC104_Slave_NextEvent(slave, &event)) == IEC104_TCP_EVENT_READY) { /* 读空收尾通知。 */ }
if (next != IEC104_TCP_EVENT_EMPTY) { exit_code = EXIT_FAILURE; }
}
/* 只有关闭且FIFO读空后才能归还槽;清理失败也必须体现在程序退出码中。 */
if (!IEC104_Interrogation_Destroy(app.interrogation)) { 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\n",
app.gi.submitted + app.other_submitted, app.gi.gi_submitted, app.gi.waits + app.other_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);
return exit_code;
}
#endif