添加遥控业务代码
This commit is contained in:
@@ -1,19 +1,22 @@
|
||||
/*
|
||||
* 104子站示例:总召、计数量召唤、时钟同步、模拟点表、其他命令兜底及main。
|
||||
* 104子站示例:总召、计数量召唤、时钟同步、45/46遥控、模拟点表及main。
|
||||
* 两种召唤共用IEC104_Interrogation.c状态机,各自有独立实例和业务事件。
|
||||
* 每轮Slave_Poll -> NextEvent逐条交付 -> Interrogation_Process;所有调用串行。
|
||||
* 总召每批4点,模块自动推进至需要等待;8个遥信和8个遥测组成6个响应ASDU。
|
||||
* QOI=21只读遥信,22只读遥测,其余分组由应用拒绝;分组点表策略不写进协议库。
|
||||
* 每轮Slave_Poll -> NextEvent逐条交付 -> Command_Process及应用交互 -> 召唤/校时;所有调用串行。
|
||||
* 总召每批4点,模块自动推进至需要等待;8个单点遥信、8个双点遥信和8个遥测组成8个响应ASDU。
|
||||
* QOI=21读取单点及双点遥信,22只读遥测,其余分组由应用拒绝;分组点表策略不写进协议库。
|
||||
* 同一连接至多一个未终结总召,忙时不区分QOI均否定;任务满时拒绝/计数丢弃新请求,保留当前连接。
|
||||
* 总召模块处理WAIT、STOPDT及断线;应用读取业务事件,选择类型并显式交付ASDU。
|
||||
* 计数量使用8个模拟计数器;读取快照,冻结/复位由经过事件校验的同步动作入口执行。
|
||||
* 对时默认只更新模拟UTC基准;--clock system显式选择实际时钟,协议仍使用单调时钟。
|
||||
* 遥控默认先选择后执行,同一实例只占一个事务;设备结果、返回信息和Complete分步推进,WAIT保留当前步骤。
|
||||
* 模拟输出同步更新点值;FINISHED只清理返校/输出标志,不恢复已执行的点值,也不代表真实设备已经空闲。
|
||||
* 不支持的其他命令当场提交COT=44否定应答,复用发送层HIGH;容量不足只丢本条并计数。
|
||||
* 默认127.0.0.1:2404、CA=1;构建命令见同目录README.md。
|
||||
*/
|
||||
#include "IEC104_Interrogation.h"
|
||||
#include "IEC104_CounterInterrogation.h"
|
||||
#include "IEC104_ClockSynchronization.h"
|
||||
#include "IEC104_Command.h"
|
||||
#include "IEC60870_Clock.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -24,7 +27,7 @@
|
||||
|
||||
/* 模拟遥信点表:ioa为24位信息对象地址,value为0/1,quality为SIQ品质位(这里均为0)。
|
||||
* 表项不是线上报文,后续由SinglePointInformation_Create及ASDU容器编码成Type=1。
|
||||
* 示例点表只读;接真实数据源时,在每次NEED_DATA组包处读取所选点的当前值和品质。 */
|
||||
* 此表提供地址、初值和品质;运行值保存在各ExampleApp中,遥控与总召共用同一数据源。 */
|
||||
static const struct { uint32_t ioa; uint8_t value, quality; } example_signals[] = {
|
||||
{1001, 0, 0}, {1002, 1, 0}, {1003, 0, 0}, {1004, 1, 0},
|
||||
{1005, 1, 0}, {1006, 0, 0}, {1007, 1, 0}, {1008, 0, 0}
|
||||
@@ -41,7 +44,7 @@ static const struct { uint32_t ioa; int16_t value; uint8_t quality; } example_me
|
||||
|
||||
|
||||
/* 当前取数站的应用游标;首个NEED_DATA才初始化,协议库不登记或冻结业务IOA列表。 */
|
||||
typedef struct { size_t single_index, scaled_index; } ExampleGiData;
|
||||
typedef struct { size_t single_index, double_index, scaled_index; } ExampleGiData;
|
||||
|
||||
typedef struct {
|
||||
IEC104_Slave_t *slave; /* main拥有;本上下文仅借用。 */
|
||||
@@ -52,6 +55,20 @@ typedef struct {
|
||||
IEC104_InterrogationStatus_t ci; /* 计数量调度及统计快照,每次推进后重新查询。 */
|
||||
IEC104_ClockSynchronization_t *clock; /* 独立时钟同步模块,共用Slave及站点表。 */
|
||||
IEC104_ClockSynchronizationStatus_t cs; /* 对时执行、确认及期限的只读快照。 */
|
||||
IEC104_Command_t *command; /* CLI启用;仅总召的旧测试可以不创建遥控实例。 */
|
||||
IEC104_CommandStatus_t command_status; /* 推进后/等待前刷新;计时、通知和应答状态只供调度及统计。 */
|
||||
IEC104_CommandEvent_t command_event; /* 工作事件可跨WAIT保留,执行收尾继续使用原EXECUTE身份。 */
|
||||
IEC104_Station_t *command_station; /* 应用额外引用:首个工作事件先Acquire,再保存context;FINISHED后Release。 */
|
||||
uint64_t command_instance, command_transaction; /* 保留事务归属;SELECT/EXECUTE共用,FINISHED须先核对再清理。 */
|
||||
/* pending仅表示工作事件尚待Accept/Reject;waiting表示上次接口WAIT,避免把待重试状态当作可立即推进。
|
||||
* selected/output是模拟返校及输出的副作用标志,不代替协议阶段;真实设备忙状态应独立维护。 */
|
||||
bool command_pending, command_waiting, command_selected, command_output;
|
||||
unsigned command_step; /* 0等待事件/终结,1报告设备结果,2返回信息,3关闭返回阶段。 */
|
||||
IEC60870_Asdu_t command_return; /* 借用下面的固定缓冲;WAIT未接手,必须连同事件保留到OK或STALE。 */
|
||||
uint8_t command_return_bytes[10]; /* 单个Type1/3对象,WAIT保持原文,不借用总召缓冲。 */
|
||||
/* 单点及双点运行值由遥控更新,总召与遥信分组均读取同一数组的当前值。
|
||||
* 单点命令6001..6008对应遥信1001..1008,双点命令6101..6108对应遥信1101..1108;FINISHED不重置运行值。 */
|
||||
uint8_t signal_values[EXAMPLE_SIGNAL_COUNT], double_values[EXAMPLE_SIGNAL_COUNT];
|
||||
bool clock_system, clock_valid; /* 实际设时为显式选项;clock_valid只描述模拟基准。 */
|
||||
uint64_t clock_utc_ms, clock_base_ms; /* 模拟UTC毫秒及其单调基准,不影响协议计时。 */
|
||||
IEC104_InterrogationEvent_t counter_event; /* WAIT时保留的原业务事件,重试不能换成新身份。 */
|
||||
@@ -105,7 +122,7 @@ static int Example_Build_Batch(ExampleApp *app, const IEC104_InterrogationReques
|
||||
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) {
|
||||
app->signal_values[index], example_signals[index].quality) != IEC60870_ASDU_OK) {
|
||||
return -1;
|
||||
}
|
||||
added = IEC60870_Asdu_Add_InformationObject(asdu, &point.object);
|
||||
@@ -113,6 +130,18 @@ static int Example_Build_Batch(ExampleApp *app, const IEC104_InterrogationReques
|
||||
if (added != IEC60870_ASDU_OK) { return -1; }
|
||||
++index;
|
||||
}
|
||||
} else if (data->double_index < EXAMPLE_SIGNAL_COUNT) {
|
||||
size_t index = data->double_index;
|
||||
while (index < EXAMPLE_SIGNAL_COUNT && asdu->object_count < request->max_objects_per_asdu) {
|
||||
IEC60870_DoublePointInformation_t point;
|
||||
IEC60870_AsduResult_t added;
|
||||
if (IEC60870_DoublePointInformation_Create(&point, (uint32_t)(1101U + index),
|
||||
app->double_values[index], 0U) != IEC60870_ASDU_OK) { return -1; }
|
||||
added = IEC60870_Asdu_Add_InformationObject(asdu, &point.object);
|
||||
if (added == IEC60870_ASDU_FULL) { break; }
|
||||
if (added != IEC60870_ASDU_OK) { return -1; }
|
||||
++index;
|
||||
}
|
||||
} else if (data->scaled_index < EXAMPLE_MEASUREMENT_COUNT) {
|
||||
size_t index = data->scaled_index;
|
||||
while (index < EXAMPLE_MEASUREMENT_COUNT && asdu->object_count < request->max_objects_per_asdu) {
|
||||
@@ -161,7 +190,7 @@ static bool Example_Drain_Business(ExampleApp *app)
|
||||
switch (event->type) {
|
||||
case IEC104_INTERROGATION_BEGIN: {
|
||||
bool accept = event->request.qualifier <= 22U;
|
||||
/* 示例仅定义组1=遥信、组2=遥测;拒绝未配置的组。协议库支持全部16组。
|
||||
/* 示例仅定义组1=单点及双点遥信、组2=遥测;拒绝未配置的组。协议库支持全部16组。
|
||||
* 广播会先交付所有站的BEGIN;这里只作准入判断,不占用唯一取数游标。 */
|
||||
reply = IEC104_Interrogation_Reply_Begin(app->interrogation, event, accept);
|
||||
break;
|
||||
@@ -178,7 +207,10 @@ static bool Example_Drain_Business(ExampleApp *app)
|
||||
}
|
||||
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; }
|
||||
if (event->request.qualifier == 22U) {
|
||||
app->gi_data.single_index = EXAMPLE_SIGNAL_COUNT;
|
||||
app->gi_data.double_index = EXAMPLE_SIGNAL_COUNT;
|
||||
}
|
||||
app->session_instance = event->instance_id;
|
||||
app->session_request = event->request_id;
|
||||
}
|
||||
@@ -189,6 +221,8 @@ static bool Example_Drain_Business(ExampleApp *app)
|
||||
if (reply == IEC104_INTERROGATION_REPLY_OK && app->batch_result > 0) {
|
||||
if (app->gi_data.single_index < EXAMPLE_SIGNAL_COUNT) {
|
||||
app->gi_data.single_index += app->batch.object_count;
|
||||
} else if (app->gi_data.double_index < EXAMPLE_SIGNAL_COUNT) {
|
||||
app->gi_data.double_index += app->batch.object_count;
|
||||
} else { app->gi_data.scaled_index += app->batch.object_count; }
|
||||
} else if (reply == IEC104_INTERROGATION_REPLY_OK && app->batch_result == 0) {
|
||||
/* 其他站的数据随后才开始;无需等到所有站的ACT_TERM才复用游标。 */
|
||||
@@ -348,8 +382,15 @@ static bool Example_Init(ExampleApp *app, IEC104_Slave_t *slave, uint16_t common
|
||||
IEC104_InterrogationStationState_t *broadcast_states, size_t broadcast_capacity)
|
||||
{
|
||||
IEC104_InterrogationConfig_t config;
|
||||
size_t index;
|
||||
memset(app, 0, sizeof(*app));
|
||||
app->slave = slave;
|
||||
/* 每个应用实例独立保存受控运行值:单点沿用原总召初值,双点1表示分位。
|
||||
* 这里只在应用初始化时赋值,选择结束、撤销和FINISHED清理均不得恢复这些初值。 */
|
||||
for (index = 0U; index < EXAMPLE_SIGNAL_COUNT; ++index) {
|
||||
app->signal_values[index] = example_signals[index].value;
|
||||
app->double_values[index] = 1U;
|
||||
}
|
||||
if (IEC104_Slave_Add_Station(slave, &app->station, common_address, app) != IEC104_STATION_OK) { return false; }
|
||||
IEC104_Interrogation_Config_Default(&config);
|
||||
config.use_station_registry = true; /* CA只在注册时指定;多站广播需提供每站状态区。 */
|
||||
@@ -424,6 +465,199 @@ static bool Example_Drain_Clock(ExampleApp *app)
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 功能:把示例支持的遥控命令地址映射为对应运行值数组的下标。
|
||||
* 输入参数:request——非NULL的已解码45/46工作请求,仅借用;本函数不检查会话或选择匹配。
|
||||
* 输出参数:index——非NULL,仅成功时写入0..EXAMPLE_SIGNAL_COUNT-1,失败保持原值。
|
||||
* 返回值:true为Type45的6001..6008或Type46的6101..6108;false为示例点表之外的类型/地址。
|
||||
* 说明:库负责协议格式和事务身份,应用负责IOA是否存在;范围外由调用者Reject(UNKNOWN_IOA)。
|
||||
* 单点命令6001..6008映射到遥信1001..1008;双点命令6101..6108映射到位置1101..1108。
|
||||
*/
|
||||
static bool Example_Command_Point(const IEC104_CommandRequest_t *request, size_t *index)
|
||||
{
|
||||
uint32_t first = request->type_id == 45U ? 6001U : 6101U;
|
||||
if ((request->type_id != 45U && request->type_id != 46U) || request->ioa < first ||
|
||||
request->ioa - first >= EXAMPLE_SIGNAL_COUNT) { return false; }
|
||||
*index = (size_t)(request->ioa - first);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 功能:执行已通过Accept校验并消费的选择/执行动作,仅修改模拟设备状态。
|
||||
* 输入参数:context——当前ExampleApp;request——库仅在本次回调期间借出的工作请求,不得保存指针。
|
||||
* 输出参数:选择置返校标志;执行置输出标志并更新对应单点/双点运行值,供后续返回信息及总召读取。
|
||||
* 返回值:点表不匹配返回NOT_STARTED且没有副作用;模拟动作完成返回STARTED。
|
||||
* 说明:STARTED表示选择生效或输出启动,不代替Report_Result。真实异步驱动应在设备完成后报告结果。
|
||||
* 不在动作中发布任何上报、推进Slave或重入修改Command;库不保存本函数或context。
|
||||
*/
|
||||
static IEC104_CommandActionResult_t Example_Command_Action(void *context,
|
||||
const IEC104_CommandRequest_t *request)
|
||||
{
|
||||
ExampleApp *app = (ExampleApp *)context;
|
||||
size_t index;
|
||||
if (!Example_Command_Point(request, &index)) { return IEC104_COMMAND_ACTION_NOT_STARTED; }
|
||||
if (request->is_select) { app->command_selected = true; }
|
||||
else {
|
||||
app->command_output = true;
|
||||
if (request->type_id == 45U) { app->signal_values[index] = request->state; }
|
||||
else { app->double_values[index] = request->state; }
|
||||
}
|
||||
return IEC104_COMMAND_ACTION_STARTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 功能:为已启动的当前EXECUTE构造一帧COT=11的模拟位置返回信息,此处只组包,不提交。
|
||||
* 输入参数:app——非NULL,command_event保留当前EXECUTE身份,运行值已由动作更新。
|
||||
* 输出参数:app->command_return及其固定10字节缓冲,CA/OA沿用工作请求,IOA映射到对应遥信。
|
||||
* 返回值:true为Type1或Type3单对象ASDU构造成功;false为点表或编解码失败,此时不可提交该ASDU。
|
||||
* 说明:命令模块已拒绝TEST请求,因此清零公共头得到TEST=0;对象品质为示例约定的0。
|
||||
* 局部信息对象由ASDU编码复制,离开函数不借用局部变量;ASDU仍借用app内固定字节缓冲。
|
||||
* 成功后冻结这一帧,Submit_Return_Info的WAIT不重新组包;只有OK才可推进,避免同一变位重复上报。
|
||||
*/
|
||||
static bool Example_Build_Command_Return(ExampleApp *app)
|
||||
{
|
||||
const IEC104_CommandRequest_t *request = &app->command_event.request;
|
||||
IEC60870_AsduHeader_t header;
|
||||
size_t index;
|
||||
if (!Example_Command_Point(request, &index)) { return false; }
|
||||
memset(&header, 0, sizeof(header));
|
||||
header.cause_of_transmission = IEC60870_ASDU_COT_RETURN_INFO_REMOTE;
|
||||
header.common_address = request->common_address;
|
||||
header.originator_address = request->originator_address;
|
||||
if (IEC60870_Asdu_Create(&app->command_return,
|
||||
IEC104_Slave_Get_Application_Layer_Parameters(app->slave), app->command_return_bytes,
|
||||
sizeof(app->command_return_bytes), &header, 0) != IEC60870_ASDU_OK) { return false; }
|
||||
if (request->type_id == 45U) {
|
||||
IEC60870_SinglePointInformation_t point;
|
||||
return IEC60870_SinglePointInformation_Create(&point, (uint32_t)(1001U + index),
|
||||
app->signal_values[index], 0U) == IEC60870_ASDU_OK &&
|
||||
IEC60870_Asdu_Add_InformationObject(&app->command_return, &point.object) == IEC60870_ASDU_OK;
|
||||
} else {
|
||||
IEC60870_DoublePointInformation_t point;
|
||||
return IEC60870_DoublePointInformation_Create(&point, (uint32_t)(1101U + index),
|
||||
app->double_values[index], 0U) == IEC60870_ASDU_OK &&
|
||||
IEC60870_Asdu_Add_InformationObject(&app->command_return, &point.object) == IEC60870_ASDU_OK;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 功能:消费遥控业务事件、处理应用决定,并推进结果报告/返回信息/Complete的持久化步骤。
|
||||
* 输入参数:app——非NULL,所属Slave通知须先按FIFO分派;command可为NULL以兼容仅总召测试。
|
||||
* 输出参数:更新应用持有事件、站点引用、模拟标志和重试步骤;不替代Command_Process提交协议应答。
|
||||
* 返回值:true为本轮处理完成或遇到正常WAIT/STALE;false为事件身份、引用或接口/组包错误。
|
||||
* 说明:先读取业务事件再重试旧交互,使STOP、断线、撤销产生的FINISHED能先清理过期步骤。
|
||||
* 一个事务可能交付SELECT后再交付EXECUTE,但只持有一个应用站点引用,最后恰好按FINISHED清理。
|
||||
* FINISHED不回滚设备值;本例动作同步完成,可直接清除模拟标志并Release。真实输出未结束时,
|
||||
* 应用须另存设备操作及站点引用,保持设备忙状态,不能照搬本例的立即释放。
|
||||
*/
|
||||
static bool Example_Drain_Command(ExampleApp *app)
|
||||
{
|
||||
unsigned count;
|
||||
if (app->command == NULL) { return true; }
|
||||
for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) {
|
||||
IEC104_CommandEvent_t event;
|
||||
IEC104_CommandNextResult_t next = IEC104_Command_NextEvent(app->command, &event);
|
||||
if (next == IEC104_COMMAND_NEXT_EMPTY) { break; }
|
||||
if (next != IEC104_COMMAND_NEXT_READY) { return false; }
|
||||
if (event.type == IEC104_COMMAND_FINISHED) {
|
||||
/* 只清理当前应用持有的事务,不能让旧实例/旧事务的终结通知释放另一次输出资源。
|
||||
* 模块槽可能仍因最后应答WAIT而占用;应用读到FINISHED也不能据此强行发起新动作。 */
|
||||
if (event.instance_id != app->command_instance ||
|
||||
event.transaction_id != app->command_transaction) { return false; }
|
||||
if (event.select_effect) { app->command_selected = false; }
|
||||
if (event.execute_effect) { app->command_output = false; }
|
||||
if (app->command_station != NULL &&
|
||||
IEC104_Slave_Release_Station(app->slave, app->command_station) != IEC104_STATION_OK) { return false; }
|
||||
app->command_station = NULL;
|
||||
app->command_instance = app->command_transaction = 0U;
|
||||
app->command_pending = app->command_waiting = false;
|
||||
app->command_step = 0U;
|
||||
} else {
|
||||
if (app->command_transaction == 0U) {
|
||||
/* 先按注册编号找到原站并Acquire,再保存引用及事务身份;同CA重新注册不能替代原站。
|
||||
* 模块自身的引用可能在FINISHED出队时释放,不能借它维持应用context的生命周期。 */
|
||||
IEC104_Station_t *station = IEC104_Slave_Get_Station_By_Id(app->slave, event.request.station_id);
|
||||
if (station == NULL || IEC104_Slave_Acquire_Station(app->slave, station) != IEC104_STATION_OK) {
|
||||
return false;
|
||||
}
|
||||
app->command_station = station;
|
||||
app->command_instance = event.instance_id;
|
||||
app->command_transaction = event.transaction_id;
|
||||
} else if (app->command_instance != event.instance_id ||
|
||||
app->command_transaction != event.transaction_id || app->command_pending) { return false; }
|
||||
/* 按值保存,不持有NextEvent局部变量的地址;匹配执行覆盖工作事件,继续保留同一站点引用。 */
|
||||
app->command_event = event;
|
||||
app->command_pending = true;
|
||||
app->command_waiting = false;
|
||||
}
|
||||
}
|
||||
if (app->command_pending) {
|
||||
IEC104_CommandReplyResult_t reply;
|
||||
size_t index;
|
||||
/* context和点表属于应用策略;格式已合法但不属于本应用的点仍明确回UNKNOWN_IOA。 */
|
||||
if (IEC104_Station_Get_Context(app->command_station) != app ||
|
||||
!Example_Command_Point(&app->command_event.request, &index)) {
|
||||
reply = IEC104_Command_Reject(app->command, &app->command_event, IEC104_COMMAND_REJECT_UNKNOWN_IOA);
|
||||
} else { reply = IEC104_Command_Accept(app->command, &app->command_event, Example_Command_Action, app); }
|
||||
app->command_waiting = reply == IEC104_COMMAND_REPLY_WAIT;
|
||||
/* WAIT未消费工作事件、未调用动作;保留身份,下轮先处理Slave通知再重试。
|
||||
* STALE不再重交动作,只保留事务归属等待FINISHED;OK也仅表示事件被消费。 */
|
||||
if (app->command_waiting) { return true; }
|
||||
if (reply != IEC104_COMMAND_REPLY_OK && reply != IEC104_COMMAND_REPLY_STALE) { return false; }
|
||||
app->command_pending = false;
|
||||
if (reply == IEC104_COMMAND_REPLY_OK && app->command_event.type == IEC104_COMMAND_EXECUTE) {
|
||||
IEC104_CommandTransactionInfo_t info;
|
||||
/* Reject和动作NOT_STARTED/FAILED也可能返回OK,必须确认仍为EXECUTING才能报告设备结果。
|
||||
* 返回信息此时可先构造,但Submit_Return_Info会等待肯定执行确认已经交接。 */
|
||||
if (IEC104_Command_Get_Transaction(app->command, &app->command_event, &info) && info.active &&
|
||||
info.phase == IEC104_COMMAND_PHASE_EXECUTING) {
|
||||
if (!Example_Build_Command_Return(app)) { return false; }
|
||||
app->command_step = 1U;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 模拟动作已经完成,先报告结果停止执行计时;确认是否已交接不影响本次报告。
|
||||
* 每轮至多推进三个步骤。每步仅OK后递增;返回信息OK已经固定帧序,不能因Complete等待而重发。 */
|
||||
for (count = 0U; count < 3U && app->command_step != 0U; ++count) {
|
||||
IEC104_CommandReplyResult_t reply;
|
||||
if (app->command_step == 1U) {
|
||||
reply = IEC104_Command_Report_Result(app->command, &app->command_event, IEC104_COMMAND_RESULT_SUCCEEDED);
|
||||
} else if (app->command_step == 2U) {
|
||||
reply = IEC104_Command_Submit_Return_Info(app->command, &app->command_event, &app->command_return);
|
||||
} else { reply = IEC104_Command_Complete(app->command, &app->command_event); }
|
||||
app->command_waiting = reply == IEC104_COMMAND_REPLY_WAIT;
|
||||
/* WAIT保持当前步骤及完整ASDU;STALE停止后续提交,引用与模拟标志交给FINISHED统一清理。
|
||||
* Complete的OK只关闭返回信息阶段,ACT_TERM交接及FINISHED仍由后续Process/NextEvent推进。 */
|
||||
if (app->command_waiting) { return true; }
|
||||
if (reply == IEC104_COMMAND_REPLY_STALE) { app->command_step = 0U; return true; }
|
||||
if (reply != IEC104_COMMAND_REPLY_OK) { return false; }
|
||||
app->command_step = app->command_step == 3U ? 0U : app->command_step + 1U;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifndef IEC104_SLAVE_EXAMPLE_NO_MAIN
|
||||
/**
|
||||
* 功能:为命令行示例创建默认遥控实例,与仅总召的初始化入口分开。
|
||||
* 输入参数:app——非NULL,Slave句柄和站点已创建,尚未开始消费Slave通知。
|
||||
* 输出参数:成功后保存app->command;失败不创建可用实例。
|
||||
* 返回值:true为创建成功;false为重复初始化、配置/归属错误或静态池没有空位。
|
||||
* 说明:默认45/46均须先选择,决定15秒、选择30秒、执行60秒;本函数不执行设备动作。
|
||||
* 每Slave只能绑定一个Command。main在Slave_Init前创建亦可,但必须早于首条生命周期通知。
|
||||
*/
|
||||
static bool Example_Init_Command(ExampleApp *app)
|
||||
{
|
||||
IEC104_CommandConfig_t config;
|
||||
if (app->command != NULL) { return false; }
|
||||
IEC104_Command_Config_Default(&config);
|
||||
app->command = IEC104_Command_Create(app->slave, &config);
|
||||
return app->command != NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* 统一取消应用业务;app为有效应用上下文,未创建的可选模块直接跳过,无返回值。
|
||||
* 遥控Cancel丢弃模块持有应答但保留应交付FINISHED,不回滚已经执行的输出。
|
||||
* 本函数仅刷新状态,调用方还须Drain_Command读取终结通知、归还应用引用,再关闭Slave或Destroy。 */
|
||||
static void Example_Cancel(ExampleApp *app)
|
||||
{
|
||||
IEC104_Interrogation_Cancel(app->interrogation);
|
||||
@@ -435,13 +669,30 @@ static void Example_Cancel(ExampleApp *app)
|
||||
IEC104_ClockSynchronization_Cancel(app->clock);
|
||||
(void)IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs);
|
||||
}
|
||||
if (app->command != NULL) {
|
||||
IEC104_Command_Cancel(app->command);
|
||||
(void)IEC104_Command_Get_Status(app->command, &app->command_status);
|
||||
}
|
||||
(void)IEC104_Interrogation_Get_Status(app->interrogation, &app->gi);
|
||||
}
|
||||
|
||||
/* 每次等待前重新查询剩余期限,避免采样/业务处理耗时后继续使用旧的毫秒数。
|
||||
* 输入app为有效上下文,maximum_ms为调用方等待上限;返回0表示本地立即续跑,否则为缩短后的上限。
|
||||
* 查询失败也返回0,让随后推进路径处理错误;本函数只查状态,不消费事件或触发业务到期转换。
|
||||
* 本地可推进时不休眠;否则本例网络等待最多50ms,并受最近业务期限限制。 */
|
||||
static uint32_t Example_Wait_Time(ExampleApp *app, uint32_t maximum_ms)
|
||||
{
|
||||
if (app->command != NULL) {
|
||||
if (!IEC104_Command_Get_Status(app->command, &app->command_status)) { return 0U; }
|
||||
/* 未读Slave通知、FINISHED等业务事件及模块本地进度都须立即处理。
|
||||
* 应用持有事件/收尾步骤不代表能立即发送:上次WAIT时允许Poll等待,避免确认拥塞造成空转。 */
|
||||
if (app->command_status.events_pending || app->command_status.pending_events != 0U ||
|
||||
app->command_status.need_process ||
|
||||
((app->command_pending || app->command_step != 0U) && !app->command_waiting)) { return 0U; }
|
||||
if (app->command_status.timer_active && app->command_status.timeout_remaining_ms < maximum_ms) {
|
||||
maximum_ms = app->command_status.timeout_remaining_ms;
|
||||
}
|
||||
}
|
||||
if (!IEC104_Interrogation_Get_Status(app->interrogation, &app->gi)) { return 0U; }
|
||||
if (app->counter != NULL) {
|
||||
if (!IEC104_Interrogation_Get_Status(app->counter, &app->ci)) { return 0U; }
|
||||
@@ -463,7 +714,11 @@ static uint32_t Example_Wait_Time(ExampleApp *app, uint32_t maximum_ms)
|
||||
return maximum_ms;
|
||||
}
|
||||
|
||||
/* 状态事件交给所有实例;I帧依次由总召、计数量、时钟同步及未知类型兜底接手。 */
|
||||
/* 状态事件交给所有实例;I帧依次由总召、计数量、时钟同步、遥控及兜底接手。
|
||||
* 输入app为有效上下文,event是刚由Slave_NextEvent交付的借用事件,本函数不保存它的指针。
|
||||
* 返回true表示分派完成(包括模块拒绝/丢弃),false表示业务入口或状态查询失败,应统一收尾。
|
||||
* 生命周期通知不能因前一个模块HANDLED而短路,否则遥控可能漏读STOP/换代后继续执行旧请求。
|
||||
* Handle_Event负责消费这些通知,不能在它之前用Check_Request等待通知读空而阻断分派。 */
|
||||
static bool Example_Handle_Event(ExampleApp *app, const IEC104_SlaveEvent_t *event)
|
||||
{
|
||||
IEC104_InterrogationEventResult_t result;
|
||||
@@ -485,7 +740,17 @@ static bool Example_Handle_Event(ExampleApp *app, const IEC104_SlaveEvent_t *eve
|
||||
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 (clock_result != IEC104_CLOCK_SYNCHRONIZATION_EVENT_IGNORED &&
|
||||
event->transport.type == IEC104_TCP_EVENT_I_FRAME) { return true; }
|
||||
}
|
||||
if (app->command != NULL && (event->transport.type != IEC104_TCP_EVENT_I_FRAME ||
|
||||
result == IEC104_INTERROGATION_EVENT_IGNORED)) {
|
||||
/* HANDLED也包括格式否定、实例忙和过载丢弃,不能再走44兜底;未启用类型仍可继续分派。 */
|
||||
IEC104_CommandEventResult_t command_result = IEC104_Command_Handle_Event(app->command, event);
|
||||
if (command_result == IEC104_COMMAND_EVENT_ERROR ||
|
||||
!IEC104_Command_Get_Status(app->command, &app->command_status)) { return false; }
|
||||
if (command_result != IEC104_COMMAND_EVENT_IGNORED &&
|
||||
event->transport.type == IEC104_TCP_EVENT_I_FRAME) { return true; }
|
||||
}
|
||||
if (result != IEC104_INTERROGATION_EVENT_IGNORED ||
|
||||
event->transport.type != IEC104_TCP_EVENT_I_FRAME || !app->gi.active ||
|
||||
@@ -525,10 +790,15 @@ static bool Example_Step_Clock(ExampleApp *app)
|
||||
Example_Drain_Clock(app) && IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs));
|
||||
}
|
||||
|
||||
/* 两类召唤轮流先执行,再推进对时;发送WAIT后下一轮继续,不只靠need_process重试。 */
|
||||
/* 先推进遥控,随后两类召唤轮换、对时;发送WAIT后下一轮继续,不只靠need_process重试。
|
||||
* app为有效上下文;返回true表示本轮成功或因未读通知暂缓,false交给主循环停止并清理。
|
||||
* Command_Process只推进协议应答,Drain_Command才执行应用决定及结果/返回信息步骤,最后刷新调度快照。
|
||||
* 通知预算尚未读空时不能调用Accept;否则可能在尚未分派的STOP或撤销之后错误启动设备。 */
|
||||
static bool Example_Step(ExampleApp *app)
|
||||
{
|
||||
if (app->events_pending) { return true; }
|
||||
if (app->command != NULL && (!IEC104_Command_Process(app->command) || !Example_Drain_Command(app) ||
|
||||
!IEC104_Command_Get_Status(app->command, &app->command_status))) { return false; }
|
||||
app->counter_first = !app->counter_first;
|
||||
if (app->counter_first) {
|
||||
if (!Example_Step_Counter(app) || !Example_Step_General(app)) { return false; }
|
||||
@@ -538,7 +808,8 @@ static bool Example_Step(ExampleApp *app)
|
||||
return Example_Step_Clock(app);
|
||||
}
|
||||
|
||||
/* 有界消费通知。预算用完先续读,不能跨过排在后面的STOP/START直接续发旧总召。 */
|
||||
/* 有界消费通知。预算用完先续读,不能跨过排在后面的STOP/START直接续发旧总召或执行旧遥控。
|
||||
* 只有读到EMPTY才清events_pending并允许Example_Step推进动作,避免用最终激活状态掩盖中间STOP。 */
|
||||
static bool Example_Drain(ExampleApp *app)
|
||||
{
|
||||
unsigned count;
|
||||
@@ -613,13 +884,16 @@ 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"
|
||||
"GI: QOI=20 (all), 21 (single/double signals), 22 (scaled); IOA=0.\n"
|
||||
"Single signals 1001..1008, double signals 1101..1108, scaled values 2001..2008.\n"
|
||||
"COT=8 cancels matching GI.\n"
|
||||
"Counters 3001..3008: RQT=5 (all), 1/2 (groups); Type 15, four per batch.\n"
|
||||
"FRZ=0 reads frozen values; 1 freezes; 2 freezes/resets; 3 resets.\n"
|
||||
"Freeze/reset completes with ACT_CON only; TEST actions and COT=8 are rejected.\n"
|
||||
"Clock: simulated by default; system sets the actual clock and requires permission.\n"
|
||||
"Clock requests use UTC+8 standard time; TEST is rejected without setting time.\n", program);
|
||||
"Clock requests use UTC+8 standard time; TEST is rejected without setting time.\n"
|
||||
"Commands: Type45 IOA6001..6008 -> signals1001..1008; Type46 IOA6101..6108 -> double1101..1108.\n"
|
||||
"Commands require select then execute; COT8 cancels selection; TEST is rejected.\n", program);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -695,6 +969,15 @@ int main(int argc, char **argv)
|
||||
(void)IEC104_Slave_Destroy(slave, NULL);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
/* 在网络监听及首条Slave通知之前绑定遥控实例;失败时尚无事务,按创建的逆序归还业务对象。 */
|
||||
if (!Example_Init_Command(&app)) {
|
||||
(void)IEC104_ClockSynchronization_Destroy(app.clock);
|
||||
(void)IEC104_Interrogation_Destroy(app.counter);
|
||||
(void)IEC104_Interrogation_Destroy(app.interrogation);
|
||||
(void)IEC104_Slave_Remove_Station(slave, &app.station);
|
||||
(void)IEC104_Slave_Destroy(slave, NULL);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
app.clock_system = clock_system;
|
||||
result = IEC104_Slave_Init(slave, &config, &error);
|
||||
if (result != IEC104_TCP_OK) {
|
||||
@@ -703,6 +986,7 @@ int main(int argc, char **argv)
|
||||
(void)IEC104_Interrogation_Destroy(app.interrogation);
|
||||
(void)IEC104_Interrogation_Destroy(app.counter);
|
||||
(void)IEC104_ClockSynchronization_Destroy(app.clock);
|
||||
(void)IEC104_Command_Destroy(app.command);
|
||||
(void)IEC104_Slave_Remove_Station(slave, &app.station);
|
||||
(void)IEC104_Slave_Destroy(slave, NULL);
|
||||
return EXIT_FAILURE;
|
||||
@@ -739,34 +1023,50 @@ int main(int argc, char **argv)
|
||||
cleanup:
|
||||
/* 第4步:停止应用任务,再结束网络;取消只丢弃尚未提交完毕的任务,不重发已接手数据。 */
|
||||
Example_Cancel(&app);
|
||||
/* Cancel只将终结通知排队;释放总召实例前必须读完,并清理应用自己的遍历状态及资源。 */
|
||||
if (!Example_Drain_Business(&app) || !Example_Drain_Counter(&app)) { exit_code = EXIT_FAILURE; }
|
||||
/* Cancel只将终结通知排队;释放业务实例前必须读完,并清理应用自己的遍历状态及资源。
|
||||
* 遥控FINISHED在离线时仍可读;这里只释放已同步完成的模拟输出,真实设备须自行等实际结束。 */
|
||||
if (!Example_Drain_Business(&app) || !Example_Drain_Counter(&app) || !Example_Drain_Command(&app)) {
|
||||
exit_code = EXIT_FAILURE;
|
||||
}
|
||||
if (IEC104_Slave_Close(slave, &error) != IEC104_TCP_OK) { exit_code = EXIT_FAILURE; }
|
||||
{
|
||||
IEC104_SlaveEvent_t event;
|
||||
IEC104_TcpEventResult_t next;
|
||||
/* Close保留收尾通知。对时只消费非I帧状态,不再受理请求或执行动作。 */
|
||||
/* Close保留收尾通知。对时和遥控只消费非I帧状态,不再受理请求或执行动作。
|
||||
* 即使Cancel已经终结事务,也须交付断线通知让模块清除会话;首次终结原因不会被改写。 */
|
||||
while ((next = IEC104_Slave_NextEvent(slave, &event)) == IEC104_TCP_EVENT_READY) {
|
||||
if (app.clock != NULL && event.transport.type != IEC104_TCP_EVENT_I_FRAME &&
|
||||
IEC104_ClockSynchronization_Handle_Event(app.clock, &event) ==
|
||||
IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR) { exit_code = EXIT_FAILURE; }
|
||||
if (app.command != NULL && event.transport.type != IEC104_TCP_EVENT_I_FRAME &&
|
||||
IEC104_Command_Handle_Event(app.command, &event) == IEC104_COMMAND_EVENT_ERROR) {
|
||||
exit_code = EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
if (next != IEC104_TCP_EVENT_EMPTY) { exit_code = EXIT_FAILURE; }
|
||||
}
|
||||
if (!IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs)) { exit_code = EXIT_FAILURE; }
|
||||
/* 只有关闭且FIFO读空后才能归还槽;清理失败也必须体现在程序退出码中。 */
|
||||
/* Close期间新产生的终结仍需读完;刷新统计后才能Destroy,不能用Destroy代替FINISHED清理。 */
|
||||
if (!Example_Drain_Command(&app) || !IEC104_Command_Get_Status(app.command, &app.command_status)) {
|
||||
exit_code = EXIT_FAILURE;
|
||||
}
|
||||
/* 只有关闭且FIFO读空后才能归还槽;清理失败也必须体现在程序退出码中。
|
||||
* Command还要求事务/独立应答全部清空;应用站点引用已在FINISHED分支释放,再Remove_Station。 */
|
||||
if (!IEC104_Interrogation_Destroy(app.interrogation)) { exit_code = EXIT_FAILURE; }
|
||||
if (!IEC104_Interrogation_Destroy(app.counter)) { exit_code = EXIT_FAILURE; }
|
||||
if (!IEC104_ClockSynchronization_Destroy(app.clock)) { exit_code = EXIT_FAILURE; }
|
||||
if (!IEC104_Command_Destroy(app.command)) { exit_code = EXIT_FAILURE; }
|
||||
if (IEC104_Slave_Remove_Station(slave, &app.station) != IEC104_STATION_OK) { exit_code = EXIT_FAILURE; }
|
||||
if (IEC104_Slave_Destroy(slave, &error) != IEC104_TCP_OK) { exit_code = EXIT_FAILURE; }
|
||||
/* 打印应用提交统计;这些值不用于推断未确认帧是否已到达主站,也不作为自动重试依据。 */
|
||||
printf("STOP submitted=%lu gi_submitted=%lu waits=%lu rejected=%lu cancelled=%lu overloads=%lu deactivated=%lu timeouts=%lu negative_submitted=%lu dropped_requests=%lu counter_submitted=%lu counter_completed=%lu clock_executed=%lu clock_synchronized=%lu clock_submitted=%lu\n",
|
||||
printf("STOP submitted=%lu gi_submitted=%lu waits=%lu rejected=%lu cancelled=%lu overloads=%lu deactivated=%lu timeouts=%lu negative_submitted=%lu dropped_requests=%lu counter_submitted=%lu counter_completed=%lu clock_executed=%lu clock_synchronized=%lu clock_submitted=%lu command_selected=%lu command_executed=%lu command_completed=%lu command_submitted=%lu command_returns=%lu\n",
|
||||
app.gi.submitted + app.other_submitted, app.gi.gi_submitted, app.gi.waits,
|
||||
app.gi.rejected + app.other_rejected, app.gi.cancelled + app.other_cancelled,
|
||||
app.gi.overloads + app.other_overloads, app.gi.deactivated, app.gi.timeouts,
|
||||
app.gi.negative_submitted, app.gi.dropped_requests + app.other_overloads,
|
||||
app.ci.submitted, app.counter_completed, app.cs.executed, app.cs.synchronized, app.cs.submitted);
|
||||
app.ci.submitted, app.counter_completed, app.cs.executed, app.cs.synchronized, app.cs.submitted,
|
||||
app.command_status.selected, app.command_status.executed, app.command_status.completed,
|
||||
app.command_status.submitted, app.command_status.return_info_submitted);
|
||||
return exit_code;
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user