添加遥控业务代码
CI / L0 静态门禁 (pre-commit) (push) Failing after 2m51s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 18s

This commit is contained in:
STF
2026-10-30 20:03:19 -07:00
parent 7596f3371a
commit cd42b4d61d
126 changed files with 70586 additions and 149 deletions
@@ -0,0 +1,157 @@
# 104子站示例
[iec104_slave_example.c](../../examples/iec104_slave_example.c) 包含模拟点表、业务事件处理、其他命令兜底、事件循环及main。总召状态机使用独立库模块[IEC104_Interrogation.c/h](../../104/business/IEC104_Interrogation.h),不在示例内重复实现。
当前实现站总召(`C_IC_NA_1`,QOI=20),返回8个单点遥信和8个标度化遥测值;分组1(QOI=21)只返回遥信,分组2(QOI=22)只返回遥测。库支持全部16组,示例未配置的其余组由应用拒绝。通信通过 `IEC104_Slave`,总召通过 `IEC104_Interrogation` 接入,所有任务使用固定存储、在主循环线程串行推进。示例另含计数量召唤和Type 103校时;默认时钟为模拟值,实际设备数据源及遥控仍由应用接入。
ExampleApp持有一个零初始化的Station节点。Example_Init按`--ca`注册CA及应用context,再以`config.use_station_registry=true`创建总召。命令行仍只注册一个站,状态区参数传NULL/0;扩展多CA时须提供broadcast_states/broadcast_capacity,广播先确认所有站,再逐站数据,最后终止所有站。BEGIN只判断接受;首个NEED_DATA才按station_id查原节点并Acquire,建立唯一取数游标。Complete_Data返回OK后释放游标及引用,异常时按FINISHED清理。处理中删除/同CA重建不改变旧数据源。节点须等Is_Reusable为true后释放,见[共用站点表](../104/IEC104_API接口汇总.md#station-registry)。
例如最多一轮覆盖13站时,由应用提供以下存储并在Create前写入配置(或传给示例内部Example_Init的后两个参数):
```c
static IEC104_InterrogationStationState_t gi_stations[13];
/* config已经通过IEC104_Interrogation_Config_Default初始化。 */
config.use_station_registry = true;
config.broadcast_states = gi_stations;
config.broadcast_capacity = sizeof(gi_stations) / sizeof(gi_stations[0]);
```
数组借用至Destroy,期间不可读写或重叠使用;它保存身份、进度和终结状态,不保存点值。注册表不限制为13站,但若本轮目标数超过状态区容量,会在任何肯定确认前整轮否定。单目标不需要该状态区。
## 构建与运行
命令行示例还创建独立的Type101计数量实例。IOA3001..3004为第1组,初值120000、345600、7890、0;IOA3005..3008为第2组,初值1、65535、2147483647、42。RQT=5读取全部,1/2读取对应组,3/4拒绝。每批4个Type15对象、SQ=0;读取开始固定冻结快照,WAIT不混入新版本。
FRZ=1冻结并将序号模32加一;FRZ=2另将当前值清零;FRZ=3仅清当前值,冻结值和序号不变。动作通过Execute_Begin执行一次;TEST动作拒绝。FRZ=0读冻结值并发送ACT_TERM;FRZ=1..3只回ACT_CON后结束。CY/CA/IV在模拟点表中为0,不模拟真实计量品质。STOP统计原字段保持总召含义,末尾增加counter_submitted和counter_completed。
主循环向总召、计数量、校时实例广播生命周期通知,交替推进召唤业务并显式执行校时;其他类型在三个模块均未接手后兜底。原Example_Init仍可用于仅总召测试,计数量及校时通过各自初始化辅助函数补充,两个召唤实例各需独立广播状态区,校时使用共用站点快照。协议及应用动作限制见[计数量设计](../104/IEC104_CounterInterrogation_设计架构.md)和[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)。
在项目根目录的 PowerShell 中执行,需要 MinGW GCC:
示例通过 `IEC104_Interrogation.h`、`IEC104_CounterInterrogation.h` 和 `IEC104_ClockSynchronization.h` 引入业务声明;构建脚本添加 `104/`、`104/business/` 和 `common/` 头文件搜索路径,编辑器也应配置这三个目录。构建可执行程序仍使用下面的脚本,它会链接所需库源文件、Clock适配及Windows网络库;只编译当前C文件会缺少库函数实现。编辑器的C语言标准应与构建保持一致,使用C11,并选择实际的MinGW GCC编译器。
```powershell
./scripts/build.ps1
./.build/slave-example/iec104_slave_example.exe
```
默认监听 `127.0.0.1:2404`,公共地址 CA=1;按 Ctrl+C 退出并关闭服务。也可以指定监听地址、端口和公共地址:
```powershell
./.build/slave-example/iec104_slave_example.exe --bind 0.0.0.0 --port 2404 --ca 1
```
| 参数 | 含义 |
|---|---|
| `--bind IPv4` | 数字IPv4监听地址,默认127.0.0.1 |
| `--port 0..65535` | 默认2404;0由系统分配,启动时输出实际端口 |
| `--ca 1..65534` | 本站公共地址,默认1;总召请求也接受65535,广播请求的应答使用本站地址 |
| `--window 1..32767` | 发送窗口k,默认12;设为1可观察逐帧ACK后续发 |
| `--clock simulated\|system` | 默认simulated,只更新示例模拟时钟;system显式调用Windows系统设时接口 |
| `--run-ms N` | 运行约N毫秒后正常关闭;默认0表示持续运行 |
| `--help` | 打印用法后退出 |
启动输出 `LISTEN 地址:端口 CA=站地址`。退出保留原有STOP统计字段:`submitted`累计总召与兜底已接手ASDU,`gi_submitted`为已提交完毕的总召数,二者均不表示主站已经ACK。`waits`只统计总召发送WAIT;兜底HIGH满时丢新,计入`overloads`和`dropped_requests`,不记录待重试次数。兜底ERROR/FATAL计入`cancelled`,其中FATAL可能已写出字节,该计数不表示确定未发送。拒绝、主站撤销及业务超时统计继续保留,计数量另见`counter_submitted`和`counter_completed`。末尾校时字段`clock_executed`为实际action调用数、`clock_synchronized`为action返回true次数、`clock_submitted`为已交接校时ASDU数,包含错误及否定应答,广播按各提交目标分别计数;不混入原字段。
### Type 103校时
主站发送Type=103、SQ=0、对象数=1、COT=6、P/N=0、TEST=0、CA匹配或65535、IOA=0及有效CP56。默认按主站UTC+8标准时间解释,SU由时间工具处理;模块补偿受理至执行检查的等待。应用显式Execute_Next后回COT=7,原CP56字节原样回显。TEST否定7且不执行;COT=5读时否定45,不返回本站当前时间。
默认`--clock simulated`把请求的utc_ms/base_ms保存为模拟时钟基准,不改变系统时间。`--clock system`通过`IEC60870_Clock_Set(&request->local_time)`设置Windows时间;local_time已是固定UTC+8且已补偿,示例不再手工换时区或加等待。Clock内部转换为UTC后调用系统接口,权限不足时动作报告失败并回否定ACT_CON。自动回归只运行模拟模式。
确认发送WAIT时只重试原确认,不再次设时;STOP取消未交接确认而不撤销已完成的动作。同内容的新请求会再执行,旧时标重发可能回拨。真实数据发布方应在confirm_pending期间暂停新的带时标上报并继续驱动网络,避免持续新增NORMAL推迟确认。完整范围及期限见[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)。
## 主站操作和模拟点表
主站先建立TCP并发送 `STARTDT_ACT`,随后发送总召:Type=100、SQ=0、对象数=1、COT=6、P/N=0、CA匹配、IOA=0,QOI=20选择全站,21/22选择本例的两个分组。TEST请求同样只读取模拟点表,应答保留TEST和OA。
| IOA | 类型 | 模拟值(按地址顺序) |
|---|---|---|
| 1001..1008 | 单点信息 `M_SP_NA_1`,Type=1 | 0、1、0、1、1、0、1、0 |
| 2001..2008 | 标度化测量值 `M_ME_NB_1`,Type=11 | 2300、2310、2290、5000、-125、0、1234、42 |
所有品质为0。遥测值为原始有符号16位标度值,本示例没有定义物理单位和倍率。
一次成功站总召依次提交6个ASDU:
1. Type=100,COT=7,肯定激活确认。
2. 两帧Type=1、COT=20的单点数据,每帧4个对象。
3. 两帧Type=11、COT=20的标度化遥测,每帧4个对象。
4. Type=100,COT=10,激活终止。
分组总召各提交4个ASDU:ACT_CON、两批对应类型数据、ACT_TERM。分组1数据为Type=1/COT=21,分组2数据为Type=11/COT=22;确认和终止保留请求QOI。BEGIN校验分组,首个NEED_DATA按该组初始化应用游标,不把模拟点表写进库。
全部应答使用原请求的连接身份。总召确认、终止及短应答走普通HIGH路径,总召数据由Submit_Batch尝试不排队提交;校时调用过action的确认也使用Try。未提交HIGH/NORMAL、当前帧或窗口背压时Try返回WAIT;校验错误仍走普通HIGH。总召数据不使用Enqueue_Event,只有本批接手后才推进游标及后续终止。
主站撤销使用相同的实际站CA/OA/TEST/QOI,COT改为8、P/N=0。激活和撤销均接受本站地址或65535,匹配时65535映射为本站地址,允许广播激活配本站撤销及本站激活配广播撤销。尚有匹配总召时模块停止任务、回复肯定COT=9,并通过FINISHED(DEACTIVATED)通知应用;不匹配或已完成则否定COT=9。已提交发送层的帧可能仍在确认前发出,撤销后不再提交旧批次和ACT_TERM。示例点数很少,总召可能在主站发撤销前已全部提交;观察中途撤销和各阶段拥塞可使用下面的可控网络测试。
广播总召的BEGIN/NEED_DATA/FINISHED携带本站CA,示例直接用该字段构造数据。确认、数据、终止、否定及错误应答的CA都为本站地址,OA/TEST/QOI保留原请求值;原始Slave事件不被修改。该支持属于Type100总召处理器,其他类型仍按各自业务规则分派。
## 请求校验及任务处理
| 情况 | 本示例的处理 |
|---|---|
| 不支持的Type ID | 同步调用Reject_Request回显原文,COT=44、P/N=1;HIGH满时丢新并计数,不因连续请求断链 |
| 总召COT不是6或8 | COT=45、P/N=1 |
| 总召CA既不是本站地址也不是65535 | COT=46、P/N=1,保留原错误CA |
| 总召IOA不是0 | COT=47、P/N=1 |
| 激活QOI超出20..36、请求P/N非法或当前已有未终结总召 | 否定ACT_CON,COT=7、P/N=1 |
| 空闲时收到合法激活请求 | 有空槽则建立唯一总召;BEGIN后应用接受才发肯定确认,忙时不同QOI也拒绝 |
| 激活QOI为23..36 | 合法请求交给应用,示例因未配置点表而Reply_Begin(false),回否定ACT_CON |
| 撤销不匹配、QOI超范围或P/N非法 | 否定DEACT_CON,COT=9、P/N=1 |
| 匹配的主站撤销 | 取消对应总召,肯定DEACT_CON,COT=9、P/N=0 |
| 总召唯一对象完整,但SQ=1或有多余尾字节 | 回否定7/9,COT/CA/IOA错误优先45/46/47;不执行激活或撤销 |
| 总召零对象/多对象等无法可靠应答的布局错误 | 计入rejected并丢弃;保留连接和已有总召 |
| 需要新增总召且4个任务槽已占满 | 尝试否定ACT_CON;无法安全提交/保存则丢新请求并计数,保留连接与原任务;匹配撤销复用原槽 |
| 接受/取数/整体业务期限到点 | 肯定ACT_CON提交前回否定确认并保留连接,提交后关闭原连接;已取出BEGIN才交付对应TIMEOUT终结事件 |
Example_Handle_Event先过滤asdu_result失败的I帧视图,避免进入业务分派或未知类型兜底;正常线上基础头/载荷截短已由Receiver和连接层处理。当前I帧依次交给总召、计数量、校时及后续业务;都未接手时,在读取下一事件前同步调用`IEC104_Slave_Reject_Request`回UNKNOWN_TYPE。该接口借用当前事件生成原文错误应答,复用库的普通HIGH路径;示例不保存other_request,也没有other_pending、other_waiting或other_waits。未支持类型的连续及相同内容有效新请求逐条尝试应答,允许穿插正在进行的总召。103现在由校时模块接手,102仍回44;模块之间不承诺跨业务应答按接收顺序排列。
OK表示已接手;WAIT只因HIGH容量不足,示例丢弃本条并增加overloads/dropped_requests,保持连接、已有HIGH内容及当前业务。ERROR按失效请求处理并继续读通知;FATAL可能已写出字节,不得重试,继续排空收尾通知。接口不修改输入事件或其最近提交结果,不能通过Get_Last_Submit_Result读取本次返回值。未读启停通知会阻止本次应答;状态计数不能追溯已消费的STOP/START,禁止先保存事件再读完后续通知后调用。需要异步业务应答时另用Copy_Event、Check_Request并自行处理STOP取消,完整契约见[同步拒绝当前请求](../104/IEC104_API接口汇总.md#reject-request)。
校验从类型开始;已知总召先检查是否有唯一完整对象,再按COT、CA、IOA、布局/QOI/P/N及事务状态决定。畸形单对象的否定副本规范为10字节、VSQ=1,保留OA/TEST/IOA/QOI,不修改原输入。广播应答使用实际站CA。错误请求不取消原总召或刷新期限;否定应答沿现有队列重试,满槽只丢新请求。这里只实现上述命令集合,不代表所有类型的业务规则。
`Example_Drain_Business`主动读取总召业务事件:BEGIN按QOI选点表,Reply_Begin接受或拒绝请求;NEED_DATA调用普通辅助函数Example_Build_Batch构造数据,再Submit_Batch,数据耗尽则Complete_Data;FINISHED按实例/请求编号清理对应资源,撤销和超时也走该分支。没有向库注册业务函数指针。数据源暂未就绪时可保存事件,后续就绪再应答;发送WAIT也保留事件,下一次调用重新组包,模块不缓存本批原文或重复生成NEED_DATA。
本例BEGIN只判断接受,首个NEED_DATA初始化游标并读取这一批;接真实数据源时,在Example_Build_Batch的逐点组包处读取当前值和品质。IOA、类型、分组成员及遍历策略均由调用者决定,协议库不登记或冻结完整IOA列表。示例常量表只用于展示业务接入,不是库规定的地址范围。逐批采样不保证整轮同一时刻,也不自动解决与突发上报的新旧排序;完整边界见[总召模块设计](../104/IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。
Example_Build_Batch使用局部游标遍历,仅在Submit_Batch返回OK后推进应用游标,此时发送层已复制原文并确定帧顺序,输入存储可复用。返回WAIT时保留同一业务事件及游标,先推进网络、读空Slave通知,再从同一位置重新取当前值和品质,不能重发旧构造结果。STALE表示原交互已失效(包括取消、撤销、超时及提交故障),丢弃旧应答并处理FINISHED。接实际数据源时,须将点值更新及突发发布与总召读值及提交串行化;排序保证及持续突发可能推迟总召的边界见[数据批次与发送路径](../104/IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。
`IEC104_Interrogation_Process(app->interrogation)`自动推进当前就绪任务,无需预算参数。Handle_Event在原会话有效、Slave通知已读空且没有更早REPLY时立即尝试提交短应答;未接手再保存,无法保存则计入dropped_requests。Process继续处理保存的应答及串行总召;任务为空、全部等待应用/事件空位、发送WAIT或Slave有未读通知时返回。Example_Step随后消费业务事件,下一轮继续推进;不在Process内部执行应用代码。
Example_Wait_Time直接读取总召状态的`need_process`,不再用`pending_requests && !waiting`推断。业务应答后或旧FINISHED出队释放空位时,下轮Poll使用0等待;发送WAIT后允许等待网络。每次Poll并读空Slave通知后,Example_Step仍调用总召Process重试,不能用need_process为false跳过;未读Slave通知始终优先处理。
Example_Init使用默认业务期限:BEGIN应答15秒、每批NEED_DATA至首次有效批次尝试15秒、执行整体期限0(禁用);对应begin_timeout_ms/data_timeout_ms/total_timeout_ms,各项0表示禁用。有效批次因发送背压WAIT后,本批取数计时停止,下一批NEED_DATA才重新计时;应用仍须重读并重试。BEGIN生成前不计业务期限;总期限显式设为非零时,从BEGIN生成起计算完整执行耗时,发送等待计入且不因新批次而延长。START/CONFIRM超时或BEGIN阶段Fail_Request转为否定ACT_CON并保留TCP及已有短应答;肯定ACT_CON提交后发生取数超时、总超时或NEED_DATA失败,仍关闭原连接。失败通知保留具体原因,须继续读取FINISHED并驱动Process,不能根据BEGIN失败假定已断链。业务FIFO被旧FINISHED占用时要及时读取,才能启动当前请求,期间仍拒绝新的总召。Example_Wait_Time在每次Poll前重新查询最近期限,将50ms等待上限进一步缩短。期限使用单调时钟,不代替链路t0..t3及单帧发送期限,也不等待主站ACK;默认关闭整体期限不影响这些底层保护。退出统计包含negative_submitted和dropped_requests;丢弃请求没有响应,主站可能超时或重试。
STOPDT或断线通知取消未提交任务。发送内部受理STOP而返回ERROR时,总召确认原连接正常停用后先让出,待通知终结为STOPPED;其他ERROR/FATAL仍失败。Submit_Batch为等待STOP通知而返回WAIT时不停止取数计时,应用继续按FIFO交付通知,旧交互随后STALE。事件消费预算耗尽时先续读通知,再推进业务。关闭时执行`Interrogation_Cancel → 读空业务事件并清理FINISHED/Release → Slave_Close → 读空Slave通知 → Interrogation_Destroy → Slave_Remove_Station → Slave_Destroy`。总召Destroy遇到活动任务或未读业务事件会拒绝释放;应用引用须持有到FINISHED清理,不能用Remove成功代替资源清理。
## 验证
```powershell
./scripts/build.ps1 -Test
```
`-Test` 需要预先安装 Node.js。脚本依次运行32组Slave/站点测试(单服务及双服务配置)、130组总召/计数量/兜底/校时可控网络测试(单服务及双服务)、3组规则单测和真实本机TCP测试:
- `iec104_slave_test.c`:站点注册/移除、延迟回收、重复CA、13站共享服务、CA/context隔离、注册编号防替换、快照增删及嵌套集合锁、关闭重启保留,以及原有应答、事件身份和背压回归。Reject_Request另覆盖原文回显、非法参数、连续请求、窗口背压、HIGH满、启停门禁、旧会话及FATAL。
- `iec104_slave_example_test.c`:完整报文、部分写入、各发送阶段HIGH满与WAIT、请求校验、STOP/重连、非法请求、传输通知预算及API生命周期;业务事件覆盖延迟应答、批次校验、混合类型、容量边界、拒绝/失败/取消、终结预留和实例复用。总召覆盖全部16组、各阶段撤销、三项期限、迟到应答及通知优先。调度覆盖短应答立即提交及顺序保护、非队首槽复用、满槽否定或计数丢弃且当前任务有效、各阶段相同/不同QOI均忙拒绝、结束后重发成功、撤销对象匹配、停止清理、BEGIN前等事件空位及BEGIN后完整执行期限,并验证忙状态不拦截Type101总电度请求。测试设置HIGH=251字节、业务事件FIFO=2槽,使用真实容量背压限制发送。
- `slave_example_loopback_test.cjs`:启动实际可执行程序,使用Windows网络后端和127.0.0.1随机端口,以独立主站字节验证分片请求、k=1时逐帧ACK、站总召、忙拒绝及结束后重发、撤销、错误应答、106与102同批分别回44、STOP/START、重连和正常退出。校时核对正常ACT_CON、TEST否定7、读时COT=5否定45、广播实际CA,以及103与102同批时先102回44、再103回7;连接保留。服务约8秒后自行关闭,使用默认模拟时钟。
原114组基线由98组总召、12组计数量及4组兜底回归组成。兜底改用未支持的106/102,覆盖同批及反序、相同内容的有效新请求、k=1与写阻塞排队、HIGH满后丢新且不补发、STOP/STOPSTART取消,以及总召未结束时立即提交错误应答。启用校时的独立回归另外核对103业务。
新增16组校时回归覆盖无站广播45/46、同批及重复103逐条执行、FIFO中间否定、满槽丢新、Try等待不重执行、STOP/重连、Defer/FATAL、同一次采样检查期限及补偿、action内Get_Status与防重入、广播WAIT期间删除当前站及同CA替代不加入。所有action均为模拟,不设置系统时间。
畸形请求回归覆盖SQ/尾字节否定、零/多对象丢弃、基础头及载荷截短仍由Receiver拒绝、原事件不变、错误原因优先级、畸形激活/撤销不影响原BEGIN/NEED_DATA及期限、背压满槽后的FIFO恢复和多CA广播撤销隔离。
确认前失败另覆盖BEGIN尚未取出的超时、满任务池及HIGH等待时复用原槽、QOI/OA/TEST保持且应答使用本站CA、旧应答FIFO顺序、同连接重新总召、STOP/撤销/断线优先。确认已被发送层接手但尚未写出时,取数失败及超时仍断链,避免将已接受请求改发否定确认。
need_process回归覆盖完整业务交互、事件空位释放、总召等待应用时的短应答、CONFIRM/数据/终止/短应答的发送WAIT及恢复、到期立即续跑和未读STOP/START优先,并验证只读查询不提交帧或重复累计waits。
提交期通知回归覆盖真实启停容量背压留下STOP、三个发送入口的STOPPED归因、FATAL与STOP并存时仍为SEND_ERROR、非STOP的ERROR、OK/WAIT顺带交付RECORD时暂停后续提交,以及START后重新总召。
不排队提交回归覆盖旧NORMAL先于新总召、WAIT期间新突发先发后重新取值、HIGH积压、已接手总召部分写入后突发不能越过、背压不计取数超时而下一批重新计时、k=1窗口释放、到期ACK优先处理及发送故障终结。WAIT时验证队列和批次进度不增加,OK后覆盖输入存储验证发送层持有独立副本。
广播回归覆盖QOI=20..36、13个CA全部确认/数据/终止顺序、非空混合批次、状态区不足及跨实例重叠、已确认站删除/同CA替换、三阶段WAIT、超出FIFO容量的FINISHED、各阶段定向撤销及满槽广播撤销、拒绝与撤销同时等待、非当前站总期限和STOP/断线清理。Submit_Batch拒绝广播CA及非当前站CA;Type101继续交后续处理器。真实TCP用命令行注册的单站验证广播全站/分组、忙拒绝、非法QOI、撤销组合及连续兜底,共核对65个总召/兜底ASDU及11次正常完成的总召,计数量另行核对;5个校时应答独立计数,其中3次执行并成功,另2次为TEST及COT拒绝。多CA由可控Net回归覆盖。
这些检查覆盖本示例的模拟点表流程;实际主站设备、现场点表和设备执行接口仍需联调。