Files
STM32F4-Base/IEC60870-Slave/examples/iec104_slave_example.c
T
STF 7596f3371a
CI / L0 静态门禁 (pre-commit) (push) Failing after 44s
CI / L0.5 主机侧单测 (MSVC) (push) Failing after 42s
补充总召唤的广播地址,电度召唤+时钟同步业务的实现
2026-10-29 03:20:19 -07:00

773 lines
45 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。
* 计数量使用8个模拟计数器;读取快照,冻结/复位由经过事件校验的同步动作入口执行。
* 对时默认只更新模拟UTC基准;--clock system显式选择实际时钟,协议仍使用单调时钟。
* 不支持的其他命令当场提交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 "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。
* 示例点表只读;接真实数据源时,在每次NEED_DATA组包处读取所选点的当前值和品质。 */
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, 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; /* 对时执行、确认及期限的只读快照。 */
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,
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; }
++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->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 { 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;
memset(app, 0, sizeof(*app));
app->slave = slave;
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;
}
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);
}
(void)IEC104_Interrogation_Get_Status(app->interrogation, &app->gi);
}
/* 每次等待前重新查询剩余期限,避免采样/业务处理耗时后继续使用旧的毫秒数。
* 本地可推进时不休眠;否则本例网络等待最多50ms,并受最近业务期限限制。 */
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->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帧依次由总召、计数量、时钟同步及未知类型兜底接手。 */
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) { 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重试。 */
static bool Example_Step(ExampleApp *app)
{
if (app->events_pending) { return true; }
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直接续发旧总召。 */
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 (signals), 22 (scaled); IOA=0.\n"
"Signals 1001..1008, scaled values 2001..2008; 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", 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;
}
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_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只将终结通知排队;释放总召实例前必须读完,并清理应用自己的遍历状态及资源。 */
if (!Example_Drain_Business(&app) || !Example_Drain_Counter(&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帧状态,不再受理请求或执行动作。 */
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 (next != IEC104_TCP_EVENT_EMPTY) { exit_code = EXIT_FAILURE; }
}
if (!IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs)) { exit_code = EXIT_FAILURE; }
/* 只有关闭且FIFO读空后才能归还槽;清理失败也必须体现在程序退出码中。 */
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_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\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);
return exit_code;
}
#endif