补充总召唤的广播地址,电度召唤+时钟同步业务的实现
CI / L0 静态门禁 (pre-commit) (push) Failing after 44s
CI / L0.5 主机侧单测 (MSVC) (push) Failing after 42s

This commit is contained in:
STF
2026-10-29 03:20:19 -07:00
parent 4c55544f05
commit 7596f3371a
267 changed files with 130894 additions and 1436 deletions
+414 -90
View File
@@ -1,24 +1,30 @@
/*
* 104子站示例:模拟点表、其他命令的兜底应答及main由应用提供。
* 总召状态机位于库模块IEC104_Interrogation.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;该槽满则关闭原连接。
* 计数量使用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}
@@ -34,32 +40,58 @@ static const struct { uint32_t ioa; int16_t value; uint8_t quality; } example_me
#define EXAMPLE_MEASUREMENT_COUNT (sizeof(example_measurements) / sizeof(example_measurements[0]))
/* 一次总召的应用游标;真实设备也可在BEGIN事件后保存本次快照。 */
/* 当前取数站的应用游标;首个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或回调参数。 */
uint64_t session_instance, session_request; /* 只清理对应请求的快照。 */
IEC104_Station_t *gi_station; /* 取数期间持有原站,Complete_Data或FINISHED时释放。 */
uint64_t session_instance, session_request; /* 只清理对应请求的遍历状态及业务资源。 */
IEC104_InterrogationEvent_t business_event; /* 纯值,可保存到后续循环应答。 */
bool business_pending, batch_prepared;
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];
IEC104_SlaveEvent_t other_request; /* 非总召命令的固定地址待发送副本。 */
bool other_pending, other_waiting, events_pending;
unsigned long other_submitted, other_waits, other_rejected, other_cancelled, other_overloads;
bool events_pending;
unsigned long other_submitted, other_rejected, other_cancelled, other_overloads;
} ExampleApp;
/* 普通应用函数,只由下面的事件分支直接调用,不注册到库中。
* 为NEED_DATA构造一种类型的一批;FULL不跳过当前点。应答WAIT期间保留构造结果和事件。 */
* 应用自行选择本批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;
@@ -68,8 +100,8 @@ static int Example_Build_Batch(ExampleApp *app, const IEC104_InterrogationReques
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;
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,
@@ -79,11 +111,11 @@ static int Example_Build_Batch(ExampleApp *app, const IEC104_InterrogationReques
added = IEC60870_Asdu_Add_InformationObject(asdu, &point.object);
if (added == IEC60870_ASDU_FULL) { break; }
if (added != IEC60870_ASDU_OK) { return -1; }
++data->single_index;
++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;
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,
@@ -93,15 +125,27 @@ static int Example_Build_Batch(ExampleApp *app, const IEC104_InterrogationReques
added = IEC60870_Asdu_Add_InformationObject(asdu, &point.object);
if (added == IEC60870_ASDU_FULL) { break; }
if (added != IEC60870_ASDU_OK) { return -1; }
++data->scaled_index;
++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。
* Reply的WAIT表示先处理Slave通知,STALE表示该请求已经取消,不重试过期数据。 */
* WAIT先处理Slave通知并推进网络,下次重取数据;STALE表示交互已失效,不重试过期事件。 */
static bool Example_Drain_Business(ExampleApp *app)
{
unsigned count;
@@ -118,136 +162,380 @@ static bool Example_Drain_Business(ExampleApp *app)
case IEC104_INTERROGATION_BEGIN: {
bool accept = event->request.qualifier <= 22U;
/* 示例仅定义组1=遥信、组2=遥测;拒绝未配置的组。协议库支持全部16组。
* 确认接受后建立游标,下一轮Process才提交ACT_CON。真实快照可在应答前准备。 */
* 广播会先交付所有站的BEGIN;这里只作准入判断,不占用唯一取数游标。 */
reply = IEC104_Interrogation_Reply_Begin(app->interrogation, event, accept);
if (reply == IEC104_INTERROGATION_REPLY_OK && 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;
}
break;
}
case IEC104_INTERROGATION_NEED_DATA:
if (!app->batch_prepared) {
app->batch_result = Example_Build_Batch(app, &event->request);
app->batch_prepared = true;
}
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) {
memset(&app->gi_data, 0, sizeof(app->gi_data));
app->session_instance = app->session_request = 0U;
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 = app->batch_prepared = false;
app->business_pending = false;
}
return true;
}
/* 必须在开始消费Slave事件之前创建业务模块;配置按值复制,不借用本地变量。 */
static bool Example_Init(ExampleApp *app, IEC104_Slave_t *slave, uint16_t common_address)
/* 同步模拟计数器动作:执行入口先验证事件,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.common_address = common_address;
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;
}
/* 仅清应用自己的兜底应答;总召模块独立处理其任务。 */
static void Example_Cancel_Other(ExampleApp *app)
/* 计数量实例可选,独立提供广播状态区;不共享总召的数组或取数游标。
* CLI仅注册一站。测试可给多个CA注册同一app,它们会指向同一组模拟计数器;
* 真实多站应用应让各站context分别拥有计数器数据,尤其不要逐站复位同一物理源。 */
static bool Example_Init_Counters(ExampleApp *app,
IEC104_InterrogationStationState_t *broadcast_states, size_t broadcast_capacity)
{
if (app->other_pending) { ++app->other_cancelled; }
app->other_pending = app->other_waiting = false;
memset(&app->other_request, 0, sizeof(app->other_request));
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);
Example_Cancel_Other(app);
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;业务期限更近则提前返回,由下一轮Process实际处理超时。 */
* 本地可推进时不休眠;否则本例网络等待最多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.pending_requests != 0U && !app->gi.waiting) ||
(app->other_pending && !app->other_waiting && app->gi.pending_requests == 0U)) { return 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帧进入其他业务/未知类型兜底。
* 后续总电度可在IGNORED分支接入专用处理器,再决定是否回复UNKNOWN_TYPE。 */
/* 状态事件交给所有实例;I帧依次由总召、计数量、时钟同步及未知类型兜底接手。 */
static bool Example_Handle_Event(ExampleApp *app, const IEC104_SlaveEvent_t *event)
{
IEC104_InterrogationEventResult_t result =
IEC104_Interrogation_Handle_Event(app->interrogation, 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->gi.active || (app->other_pending &&
app->other_request.transport.generation != app->gi.generation)) {
Example_Cancel_Other(app);
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; }
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;
/* 当前事件当场提交,必须在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;
}
/* 先推进总召;其他命令的兜底应答等当前总召队列为空后再提交,WAIT保留原文。 */
/* 两种业务各自推进协议、消费业务事件并刷新状态,供外层轮换调用次序。 */
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)
{
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;
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; }
}
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;
return Example_Step_Clock(app);
}
/* 有界消费通知。预算用完先续读,不能跨过排在后面的STOP/START直接续发旧总召。 */
@@ -323,10 +611,15 @@ static bool Example_Number(const char *text, unsigned long max, unsigned long *v
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"
" [--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", program);
"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);
}
/**
@@ -340,7 +633,7 @@ static void Example_Usage(const char *program)
*/
int main(int argc, char **argv)
{
static ExampleApp app; /* 其他命令副本含自引用,固定地址存放到退出。 */
static ExampleApp app; /* 注册站点及业务借用的存储在退出前保持地址不变。 */
IEC104_SlaveConfig_t config; /* 本地配置,Slave_Init会复制,不在运行中借用它。 */
IEC104_TcpError_t error; /* 最近一次服务接口调用的诊断,与历史网络错误分开。 */
IEC104_TcpServerStatus_t status; /* 启动后查询实际端口,尤其用于--port=0。 */
@@ -348,6 +641,7 @@ int main(int argc, char **argv)
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步:采用库的协议默认值,只把示例默认监听地址收窄到本机回环地址。 */
@@ -372,6 +666,10 @@ int main(int argc, char **argv)
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;
@@ -380,15 +678,32 @@ int main(int argc, char **argv)
/* 第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)) {
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槽。 */
/* 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;
}
@@ -424,25 +739,34 @@ int main(int argc, char **argv)
cleanup:
/* 第4步:停止应用任务,再结束网络;取消只丢弃尚未提交完毕的任务,不重发已接手数据。 */
Example_Cancel(&app);
/* Cancel只将终结通知排队;释放总召实例前必须读完,并释放应用自己的快照。 */
if (!Example_Drain_Business(&app)) { exit_code = EXIT_FAILURE; }
/* 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保留收尾通知。退出阶段只读空,不再交给业务分派或创建新的应答任务。 */
while ((next = IEC104_Slave_NextEvent(slave, &event)) == IEC104_TCP_EVENT_READY) { /* 读空收尾通知。 */ }
/* 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\n",
app.gi.submitted + app.other_submitted, app.gi.gi_submitted, app.gi.waits + app.other_waits,
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.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