449 lines
25 KiB
C
449 lines
25 KiB
C
/*
|
||
* 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
|