Files
STM32F4-Base/IEC60870-Slave/docs/104/README.md
T
STF cd42b4d61d
CI / L0 静态门禁 (pre-commit) (push) Failing after 2m51s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 18s
添加遥控业务代码
2026-10-30 20:03:19 -07:00

443 lines
64 KiB
Markdown
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.
# IEC104 链路模块
当前子站业务统一包含`IEC104_Slave.h`:Slave内部持有不透明TcpServer,提供生命周期、事件循环、主动上报和状态查询,并在NextEvent交付I帧时自动建立ASDU视图及原连接的共用应答接口。业务不再手动创建TcpServer、绑定generation或安装函数指针。入口和延迟任务示例见[子站统一入口](#子站统一入口)。
Slave提供[共用站点表](IEC104_API接口汇总.md#station-registry):应用提供固定地址节点,动态注册多个CA和业务context,重复CA拒绝。删除立即退出路由,已有引用和遍历结束后才回收;Is_Reusable为true后应用才能释放存储。总召设置config.use_station_registry=true后按请求CA选站;多目标广播配置每站状态区,以一个任务执行“全部确认、逐站数据、全部终止”。其他业务可复用相同的查找、引用及快照遍历接口。
总召由独立库模块[IEC104_Interrogation.c/h](../../104/business/IEC104_Interrogation.h)实现,支持QOI=20站总召、21..36分组总召及多类型无时标监视数据,负责批次校验、发送等待、主站撤销、业务超时及停用/断线取消。应用通过BEGIN/NEED_DATA/FINISHED事件和显式应答管理IOA、分组、遍历、逐批取值及终结;[子站示例](../../examples/iec104_slave_example.c)提供模拟点表、事件分派和main,构建运行见[示例说明](../examples/IEC104子站示例.md)。
内部调用关系为`Slave → TcpServer → Connection → SendManager/Receiver/Link`,网络I/O仍经Net执行,当前提供Windows后端。现有单主站、静态内存、HIGH/NORMAL队列、事件FIFO、Prepare/Process或Poll调度策略保持不变。共用ASDU及Slave发送接口继续使用IEC60870前缀;公共层不依赖104传输实现。下文的底层接口说明供维护和独立TCP接入使用,业务默认通过Slave操作服务。
`Get_Status`同时提供`high_queue`和`normal_queue`的待发条目数、保留原文条目数及总容量/已用/空闲字节数,先检查`asdu_queue_status_valid`。待发数不包含已交给发送模块的ASDU;NORMAL保留到ACK前的原文仍计入存储量,字节统计包含条目头。Init成功后离线也可查询,Close后统计无效;队列为空不代表发送或确认全部完成。公开状态结构已扩展,库与调用方须一起重新编译,字段含义见[ASDU队列积压查询](IEC104_TcpServer_设计架构.md#21-asdu队列积压查询)。
`NextEvent`区分连接建立(CONNECTED)、数据传输激活(ACTIVATED)、停用(DEACTIVATED)和连接断开(DISCONNECTED),四者共用`event.transport.data.connection`快照及对端地址。启停事件在对应ACT成功受理并提交状态变化后产生,CON可能仍待发送;同状态重复ACT不重复通知,直接断开不补造停用事件。业务可按事件类型处理启停,无需通过查询状态比较变化。
`Get_Status`的`pending_state_event_count`报告尚未消费的ACTIVATED/DEACTIVATED数量,不能追溯已消费的启停历史。当前请求的错误应答可在同步分派中调用`IEC104_Slave_Reject_Request`,支持COT=44..47,复用HIGH且不改输入事件;不新增示例应答队列。调用时机、WAIT丢新策略与FATAL不可重试约定见[同步拒绝当前请求](IEC104_API接口汇总.md#reject-request)。
发送队列NORMAL与`NextEvent`业务通知FIFO是两份独立存储:NORMAL满时可覆盖旧ASDU原文;已入队业务通知逐条保留,不覆盖、不合并启停变化。通知按有效载荷长度存放于字节环,配置宏仍表示保证条数:默认普通30条最长通知、启停4条及收尾32条记录加1个最长I帧和1个断开通知。当前x64 ABI下环占11243字节,接收暂存默认2048字节,两块合计13291字节,比原来减少43.9%;16字节APDU可在普通区缓存185条。普通通知不能占用启停及收尾预留;启停配额满时暂缓受理后续改变状态的ACT,已有CON继续发送。详细公式见[TCP服务设计](IEC104_TcpServer_设计架构.md)。
新I帧无普通空间可交付时保留原帧并暂停续收,`Prepare`及`Get_Status`的`receive_paused`为1;NextEvent腾出空间后安排Process恢复,等待中的I帧先于回执交付。已确认回执因普通空间或本轮预算不足暂留内部时,可能晚于随后发生的启停通知交付,保持不因回执积压而暂缓启停CON的行为。通知积压不直接断连,但保留I帧会阻挡后续S/U;长期不消费仍可能触发原有协议超时。应用应在每轮Process/Poll后及时取出事件。此次存储调整保持公开API及事件结构不变。
`IEC104_TcpServer_Prepare(server, &info, error)`只读导出socket关注集合、最近协议等待上限及本地续跑标志;应用统一等待其他模块和TCP事件后,用`IEC104_TcpServer_Process(server, &ready, error)`有界推进,Process不执行等待。socket只借给事件循环注册,关闭/重连后的旧通知按注册token过滤;真实I/O与Connection调度仍由TCP内部处理。活动连接未EOF时入队成功、背压解除和预算耗尽后按need_process安排续跑,不能只等待新的socket边沿;离线或EOF后新增NORMAL只缓存,不因此触发空转。EOF后也停止为此前入队但尚未编号的NORMAL创建新I帧,将这些原文留给下一连接;当前已编号帧、已受理HIGH及控制帧继续按原规则收尾。接入约定见[TCP事件循环说明](IEC104_TcpServer_设计架构.md#31-接入应用自己的事件循环)。
`IEC104_TcpServer_Poll(server, max_wait_ms, error)`组合相同的准备和推进核心,中间至多等待一次:0只查询就绪,正数允许等待并按最近协议期限缩短。原poll_interval_ms配置项已移除,业务无需分别驱动内部收发和Tick。
当前实现支持控制方向提前发送:控制站可在ACT成功入队后预留命令I帧,并在ACT之后发送,不必等待CON;被控站成功受理STARTDT_ACT即进入STARTED/IDLE,开放I/S处理与发送准备,已生成的CON按FIFO先于随后准备的I帧和S确认发送。初始STOPPED/IDLE仍禁止I/S接收及新I帧发送。标准适用条件与连接组管理边界集中说明于 [链路设计第3.2节](IEC104_Link_设计架构.md#32-接收-i-帧)。
| 阅读入口 | 负责内容 |
|---|---|
| `IEC60870_Types.h` | 101/104共用的应用层参数、ASDU存储上限;不包含传输状态 |
| `IEC104_Types.h` | 104尺寸常量、标准104应用层初始化值、ASDU优先级及链路配置 |
| `IEC60870_Codec.c/h` | 101/104共用的参数校验、ASDU公共头解析及已知类型最低载荷长度检查 |
| `IEC104_Codec.c/h` | 104专用APCI检查、I/S/U帧编码和帧类型 |
| `IEC60870_Asdu.c/h` | 通用ASDU容器、布局校验、对象分派与原文应答;使用说明见下文“ASDU信息对象模块” |
| `IEC60870_Slave.c/h` | 101/104共用的主站连接接口,分派ASDU发送、激活确认和激活终止 |
| `IEC104_Slave.c/h` | 子站统一入口:TCP服务、ASDU/应答接口、原连接隔离及各业务共用的CA站点表 |
| `business/IEC104_Interrogation.c/h` | 共用召唤引擎:任务、广播、期限、事件、取消与发送等待;接入见[总召流程](IEC104_完整调用流程.md#flow-interrogation) |
| `business/IEC104_GeneralInterrogation.c/h` | Type100规则及兼容的Interrogation_Create入口 |
| `business/IEC104_CounterInterrogation.c/h` | Type101规则、创建及一次动作执行入口;见[计数量设计](IEC104_CounterInterrogation_设计架构.md) |
| `business/IEC104_ClockSynchronization.c/h` | Type103校时校验、显式动作及原文确认;见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md) |
| `business/IEC104_Command.c/h` | Type45/46遥控、选择执行、撤销、三类期限及返回信息;见[遥控设计](IEC104_Command_设计架构.md) |
| `IEC104_TcpTypes.h` | Slave与TcpServer共用的配置、调度、事件和状态值类型,无服务操作接口 |
| `IEC60870_InformationObjects.c/h` | 67种独立信息对象,覆盖监视、命令、初始化、参数和文件;编码通过只读函数表分派,支持变长文件段 |
| `IEC60870_Time.c/h` | 101/104共用时间工具:CP16/CP24/CP56Time2a编解码、本地标准时间转换、SU修正、日历/星期和纪元秒数工具;使用约定见[时间工具设计](../common/IEC60870_Time_设计架构.md) |
| `IEC60870_Clock.c/h` | 101/104共用时间源接口:直接读取和设置固定UTC+8日历;Clock.c内置Windows读写,Clock.h预留RTC接口,由编译选项二选一 |
| `IEC104_Receiver.c/h` | 每连接严格帧头检查、成帧、APCI及ASDU最低长度检查、完整APDU借用视图和显式释放;不处理链路序号 |
| `IEC104_Connection.c/h` | TCP内部连接协调:I/S/U分派、接收保留与释放、部分发送、超时推进及旧代次隔离;内部设计见[连接层设计](IEC104_Connection_设计架构.md) |
| `IEC104_TcpServer.c/h` | 底层TCP服务:IPv4单主站、静态池句柄、事件FIFO;Prepare/Process或Poll推进,Get_Remote_Address查询对端,Close_Connection按代次断开单连接 |
| `IEC104_TcpServer_Config.h` | 实现的编译期容量配置;默认一个服务器静态槽,可调整池及缓冲容量 |
| `IEC104_Net.h` / `IEC104_Net_Windows.c` | 通用网络接口及当前Windows实现:非阻塞监听/接入/收发、就绪等待、关闭及单调计时 |
| `IEC104.c/h` | 已恢复的旧单路逐字节接收入口,保留原有全局接收器及私有Link状态 |
| `IEC104_SendBuffer.c/h` | 唯一当前I帧、独立紧凑S/U队列、部分写入续传及本地发送期限 |
| `IEC104_AsduQueue.c/h` | 调用方字节缓冲的两级变长队列,HIGH满时拒绝、NORMAL覆盖最旧原文,支持显式重连恢复 |
| `IEC104_SendManager.c/h` | Connection内部的Link/SendBuffer修改入口:会话初始化、发送调度、控制收发、完成分派、ACK和回执,以及发送期限查询 |
| `IEC104_Link.c`、`IEC104_Link_Common.h` | 初始化、完整状态校验、I/S序号、k/w窗口、S确认、公共超时判定和TESTFR |
| `IEC104_Link_Master_StartStop.c/h` | 主站发STARTDT/STOPDT_ACT,收CON,处理等待超时 |
| `IEC104_Link_Slave_StartStop.c/h` | 子站收STARTDT/STOPDT_ACT,受理时更新启停状态,生成CON并处理停止排空及交错启停 |
| `IEC104_Link_Internal.h` | 三个实现文件共用的私有声明,不供业务调用 |
| `IEC104_Link.h` | 兼容总头文件,汇总公共、主站、子站接口 |
维护内部链路实现时,可按本端角色包含 `IEC104_Link_Master_StartStop.h` 或 `IEC104_Link_Slave_StartStop.h`,两者都包含公共接口。只处理I/S或TESTFR时包含 `IEC104_Link_Common.h`。旧内部代码可继续包含 `IEC104_Link.h`;模块发送完成接口统一以 `_Send` 结尾,该命名调整本身不改变参数和处理行为。改名不保留旧名兼容入口,内部调用方须同步更新名称并重新编译。
公共ASDU基础解析由 `IEC60870_Codec.c/h` 提供,业务需要解析公共头时包含 `IEC60870_Codec.h`;104的APCI检查和I/S/U编码由 `IEC104_Codec.c/h` 提供。Receiver分别调用两层:先检查APCI,再显式传入104参数调用 `IEC60870_Parse_ASDU_Header`。遇到非法帧头或截短的已知固定长度对象时返回FRAME错误,不推进序号或ACK,也不生成I_FRAME。未知/变长类型及具体字段继续由完整ASDU解析器和应用检查,边界见[基础设计](IEC104_设计架构.md#43-已知类型的最低载荷长度)。旧 `IEC104.c/h` 的全局接收入口保留原行为,不参与TcpServer路径。两个Codec均只依赖标准库及值类型,公共Codec不依赖104实现。
| 模块及构建依赖范围 | 生产源文件 |
|---|---|
| 101/104公共ASDU头解析 | `IEC60870_Codec.c` |
| 104 APCI与I/S/U编解码 | `IEC104_Codec.c` |
| ASDU信息对象编解码 | `IEC60870_Asdu.c`、`IEC60870_InformationObjects.c`、`IEC60870_Codec.c`、`IEC60870_Time.c`;C11及IEEE 754 binary32,不依赖TcpServer或Clock |
| 子站统一入口 | 在ASDU及TCP依赖之外增加`IEC60870_Slave.c`、`IEC104_Slave.c`;共用接口本身不依赖TCP或串口实现 |
| 104站/分组总召 | 在Slave及其依赖之外加入`104/business/IEC104_Interrogation.c`和`104/business/IEC104_GeneralInterrogation.c`,头文件搜索路径增加`104/business/`;应用按QOI选点表,响应NEED_DATA提交数据 |
| 104计数量召唤 | 引擎加`104/business/IEC104_CounterInterrogation.c`,头文件搜索路径增加`104/business/`;与总召分别创建,状态通知交付两边,动作通过Execute_Begin执行 |
| 104时钟同步 | Slave依赖加`104/business/IEC104_ClockSynchronization.c`,独立实例及静态池;只由Execute_Next执行同步action,模块不依赖Clock设备适配 |
| 104遥控 | Slave依赖加`104/business/IEC104_Command.c`,头文件搜索路径增加`104/business/`;每Slave一个实例、一个事务,Accept显式执行动作,应用报告结果和提交返回信息 |
| 101/104公共时间工具 | `IEC60870_Time.c`,对已有时间值进行日历校验、时间转换及编解码 |
| 101/104时间源:Windows系统时钟 | `IEC60870_Clock.c`、`IEC60870_Time.c`;`IEC60870_CLOCK_USE_RTC=0`(默认),Clock.c直接使用Windows的Kernel32接口 |
| 101/104时间源:RTC预留接口 | 上述两个源文件,定义`IEC60870_CLOCK_USE_RTC=1`,另需板级驱动实现`IEC60870_Clock_RTC_Read/Set`;具体芯片驱动暂不提供 |
| 独立子站发送缓冲 | `IEC104_Codec.c`、`IEC104_SendBuffer.c`;公共发送类型仍由Link公共头文件提供,无需链接Link实现 |
| 链路处理 | `IEC104_Codec.c`、`IEC104_Link.c`,再按所需启停接口加入主站/子站实现 |
| 子站发送管理 | `IEC104_SendManager.c`、`IEC104_AsduQueue.c`、`IEC104_SendBuffer.c`、`IEC104_Codec.c`、`IEC104_Link.c`、`IEC104_Link_Slave_StartStop.c` |
| 子站连接协调 | 上述子站发送管理源文件,加 `IEC104_Receiver.c`、`IEC104_Connection.c` 和 `IEC60870_Codec.c`;无需主站启停或日志实现 |
| Windows TCP子站模块 | 上述子站连接协调源文件,加 `IEC104_TcpServer.c`和`IEC104_Net_Windows.c`,应用工程链接`Ws2_32`;构建与接入见[TCP服务设计](IEC104_TcpServer_设计架构.md) |
| 独立流式接收 | `IEC104_Receiver.c`、`IEC104_Codec.c`、`IEC60870_Codec.c`;不依赖Link、SendManager或日志 |
| 旧单路逐字节接收 | `IEC104.c`、`IEC104_Codec.c`、`IEC104_Link.c`、`IEC104_Log.c` |
时间工具以`IEC60870_Time`作为公共入口,供101和104的ASDU业务层及平台接入层按需调用,不在Connection中自动执行校时。`IEC60870_InformationObjects.c`复用CP56Time2a处理带时标信息对象和命令;Type 103校验及确认使用独立ClockSynchronization模块,实际设时由应用显式执行。**新生成业务时标使用本地标准时间日历并设置SU=0;Type 103确认回显原CP56及SU。** 公共编解码继续支持SU=0和1,按输入保留标志,不强制或自动清零。生成新时标前须保证日历本身已是本地标准时间,不能只清除夏令时标志而保留原小时值。
时间源提供UTC纪元秒数时,直接调用`IEC60870_Time_Seconds_To_Local(utc_seconds, ms, &local_time)`即可取得固定UTC+8的本地年月日、时分秒、毫秒和星期,调用前后都无需手动加八小时。接口内部统一处理跨日/月/年及范围检查;原始UTC秒数上限为4102415999,超过时返回0并保留输出。RTC已直接提供本地标准时间日历时,可直接构造CP56解析值并编码。
当前时间直接通过`IEC60870_Clock_Read(&value)`取得,设置实际时钟调用`IEC60870_Clock_Set(&local_time)`,均使用固定UTC+8日历。**时间源范围仅限Windows系统时钟和RTC;`IEC60870_CLOCK_USE_RTC=0`默认使用Clock.c内置的Windows实现,`=1`调用Clock.h声明的`IEC60870_Clock_RTC_Read/Set`接口。RTC具体芯片驱动暂不实现。** 编译选项须作用于`IEC60870_Clock.c`,不规划其他操作系统适配。公共入口无需时钟对象或绑定初始化;以后接RTC时,硬件仍按驱动要求初始化,已有本地标准时间日历可直接填入,无需转换成时间戳再还原。读取结果分别表示日历和`is_invalid`,读取成功不等于时间有效;失败不修改输出。RTC的BCD格式、时间基准、毫秒来源及掉电状态由具体驱动处理。接入示例见[统一时间源读写](../common/IEC60870_Time_设计架构.md#统一时间源读写),业务待接入项见[当前接入状态](../common/IEC60870_Time_设计架构.md#当前接入状态与待补齐项)。
时间戳转日期保留两种实现:Windows默认调用CRT的64位`_gmtime64_s`;编译`IEC60870_Time.c`时定义`IEC60870_TIME_USE_PLATFORM=0`即可使用原有整数算法,其他平台默认采用此配置。配置在编译时生效,业务接口、UTC含义及1970..2099年的范围一致。此选项只决定如何转换传入的时间戳,与是否接入RTC无关;应用使用RTC作为时间源时,仍需通过相应驱动读取时间,再按返回值的格式和时间基准调用转换或编码接口。详见[实现选择](../common/IEC60870_Time_设计架构.md#时间戳转日期的实现选择)。
业务接收按`From_CP56 → CP56_To_Local`调用:原始解码保留源日历及IV/SU,转换接口返回本地标准时间年月日、时分秒、毫秒和星期。双方使用本地标准时间基准时,源标准时区参数传`IEC60870_LOCAL_UTC_OFFSET_MINUTES`(480):SU=0保持原时间,SU=1减一小时并处理跨日/月/年,IV=1则转换失败。其他源时区须传入其标准偏移,输出仍为本地标准时间;调用方无需再加八小时或重复扣除夏令时。接收SU=1的兼容处理与本地发送SU=0的约定分别生效。
日历范围为1970..2099年,CP56Time2a采用2000..2099年的世纪约定;接口以1/0表示成功/失败,失败保留输出。通用纪元秒数接口保留UTC语义,不能将本地标准时间日历直接当UTC使用。RTC读写仍由平台实现,Link和Connection的`now_ms`继续使用单调计时,不受校时影响。当前提供CP16经过时间、CP24短时标、CP56及日历工具。CP24仅含分钟、分钟内毫秒及IV,不能单独恢复小时和日期,需由业务结合已有时间基准解释。完整范围、星期和保留位策略见[时间工具设计](../common/IEC60870_Time_设计架构.md)。
Connection内部持有独立 `IEC104_Receiver_t`,先用非零连接代次调用 `IEC104_Receiver_Init`,再调用 `Feed(receiver, generation, data, length, &consumed)`。Feed最多收齐一帧,TcpServer保留未消费的输入尾部;`FRAME_READY`后由Connection通过 `Peek_Frame`取得APDU和APCI并分派。S/U受理后释放,I帧在TcpServer成功复制为事件后经Connection释放。帧未释放时Feed消费0字节,不覆盖原文;`Reset`丢弃当前半帧或完整帧,但不改变连接代次或Link状态。内部接口和迁移说明见 [编解码与接收器设计](IEC104_设计架构.md)。
接收器校验APCI格式和ASDU最低长度。Connection将I/S帧交给同连接的SendManager处理序号及ACK,并按功能分派U帧;WAIT时保留接收帧。Slave内部通过TcpServer_NextEvent读取I_FRAME副本,无需释放底层帧。连接重建由TcpServer先结束旧I/O并Close,随即Detach队列回到离线状态;新连接以新代次Rebind队列并Init连接;旧代次事件返回ERROR。
I 帧本地发送结果通过 `IEC104_Link_Complete_I_Send()` 提交,`Send` 明确限定为本地发送,成功不表示对端已经确认。同一次发送结果只提交一次。具体契约见 [链路设计第3.4.4节](IEC104_Link_设计架构.md#344-complete_i_send-的本地发送完成提交)。
发送完成接口如下,表中省略统一前缀 `IEC104_Link_`。它们报告最终发送结果,包括失败;返回 OK 表示本地发送结果已成功提交,是否仍需等待对端确认由对应状态决定。
| 接口 | 对应的本地发送结果 |
|---|---|
| `Complete_I_Send` | I 帧 |
| `Complete_S_Ack_Send` | S 确认帧 |
| `Complete_Startdt_Act_Send` | 本端主动发送的 STARTDT_ACT |
| `Complete_Startdt_Con_Send` | 本端应答的 STARTDT_CON;完成不回写启停状态 |
| `Complete_Stopdt_Act_Send` | 本端主动发送的 STOPDT_ACT |
| `Complete_Stopdt_Con_Send` | 本端应答的 STOPDT_CON;完成不回写启停状态 |
| `Complete_Testfr_Act_Send` | 本端主动发送的 TESTFR_ACT |
`Prepare_*` 负责准备报文或推进准备流程,`Handle_*` 负责处理接收事件;二者名称保持原有职责划分。收到 TESTFR_CON 使用 `Handle_Testfr_Con()`,不能用本地发送完成替代对端确认。
主动 TESTFR 使用 `IDLE → ACT_SEND_PENDING → CON_PENDING → IDLE` 三态,与控制站主动发起的 STARTDT/STOPDT 一致:ACT 完整发送后先调用 Complete,再处理对应 CON。ACT 尚未 Complete 时处理到的合法 TESTFR_CON 返回 IGNORED,不缓存;只有 CON_PENDING 接受确认。被动 TESTFR_ACT 仍独立生成 CON,应答对端请求不结束本端探测。
完整状态校验集中在 `IEC104_Link.c`,主站、子站的状态组合规则由该文件内的 `static` 函数检查。角色实现通过 `IEC104_Link_Internal.h` 调用公共辅助函数,公共链路模块不再反向调用角色实现。构建时包含公共链路及基础编解码实现,再按实际使用的启停接口加入对应角色 `.c`;同时使用两端接口时加入两个角色实现。
I/S的启停准入和停止阶段立即确认规则集中在 `IEC104_Link.c` 的私有判断函数中;I/S处理与规划通过这些判断继续执行序号、窗口及确认检查。I帧接收、I帧预留、S帧接收和普通S确认分别判断,保留各自的方向与阶段差异。子站在ACT受理时提交启停状态,S确认与CON发送完成分别维护各自的完成事实。具体规则见 [I/S与启停的内部边界](IEC104_Link_设计架构.md#16-is与启停的内部边界)。
启停和TESTFR在对应入口中直接检查状态、编码并提交;I/S帧保留序号规划。内部确认提交复用公开入口的状态校验,STOPDT直接返回各等待原因,不再经过私有计划对象和结果转换。
## ASDU信息对象模块
信息对象、ASDU容器、公共头解析及参数类型统一使用 `IEC60870_` 前缀,供101/104共用。旧信息对象/ASDU文件和符号直接迁移,不提供兼容头、宏或类型别名;调用方须更新包含路径、函数/类型名称和构建源文件并重新编译。104的APCI、链路、TCP及发送队列仍使用 `IEC104_` 前缀。
公共层仅需 `IEC60870_Asdu.c`、`IEC60870_InformationObjects.c`、`IEC60870_Codec.c`、`IEC60870_Time.c`;头文件依赖也不包含任何104模块。101接入提供约定的字段宽度和容量,104接入提供标准2/2/3及249字节上限。两种接入共用同一套对象构造和解析接口。
业务包含`IEC104_Slave.h`,取得`Slave_NextEvent`交付的I帧后可直接使用ASDU视图并解析具体对象,或先构造ASDU再交给发送接口。ASDU编解码层不分配内存、不操作连接、不读取或设置系统时钟;共用连接接口可构造并提交确认/终止应答。站/分组总召可交给IEC104_Interrogation管理校验、分批、撤销、期限和重试,应用响应业务事件提交数据,自行维护IOA、分组及遍历状态;Type45/46的选择/执行由IEC104_Command管理,Type103由独立校时模块管理,权限策略及设备动作仍由应用接入。
| 层次 | 职责 |
|---|---|
| `IEC60870_Codec.c/h` | 101/104共用的参数校验、ASDU公共头解析及已知类型最低载荷长度检查 |
| `IEC104_Codec.c/h` | 104专用APCI检查、I/S/U帧编码和帧类型 |
| `IEC60870_Asdu.c/h` | ASDU创建/添加/解析/取对象,维护Type ID、VSQ/SQ、IOA布局和容量,构造原文应答 |
| `IEC60870_InformationObjects.c/h` | 每种类型独立结构、Create和Get_From_Buffer;通过对象函数表编码,按Type ID选择具体解析器 |
| `IEC60870_Time.c/h` | CP16经过时间、CP24短时标及CP56日历的编解码、范围和保留位校验 |
| 能力 | 当前范围 |
|---|---|
| 监视对象编解码 | 1–21、30–40,共32种;含步位置、比特串、成组单点、CP24及保护事件;支持SQ=0/1 |
| 命令对象编解码 | 45–51、58–64、100–107,共22种;SQ=0、一个对象、精确长度 |
| 初始化结束 | 70;COI保留原因与本地参数改变标志;SQ=0、一个对象 |
| 参数对象编解码 | 110–113;QPM/QPA按独立限定词处理;允许多个对象但SQ必须为0 |
| 文件对象编解码 | 120–127;含变长文件段125与归档查询127;126文件目录支持多个对象及SQ,其余只允许一个非SQ对象 |
| 应答构造 | COT=7激活确认、9停止激活确认、10激活终止和44–47错误应答;P/N显式传入,否定激活确认使用7和1 |
上述共67种类型都有独立结构、构造和解析接口。安全扩展、厂家私有类型不在当前范围内。本层提供无状态报文编解码;站总召状态机由IEC104_Interrogation提供,参数生效、文件传输、文件读写及归档检索仍由业务接入。类型编号可在公共对象头文件的`IEC60870_AsduType_t`中查到。
应用层参数`IEC60870_AppLayerParameters_t`定义在`IEC60870_Types.h`:
| 字段 | 合法范围及含义 |
|---|---|
| `sizeOfTypeId`、`sizeOfVSQ` | 固定为1 |
| `sizeOfCOT` | 1或2;1字节时省略OA,解析OA为0 |
| `originatorAddress` | 0..255,供业务作为默认OA;构造以header中的显式OA为准 |
| `sizeOfCA` | 1或2;1字节配置拒绝大于255的公共地址 |
| `sizeOfIOA` | 1、2或3;IOA及SQ连续地址上限随宽度变化 |
| `maxSizeOfASDU` | 至少容纳公共头、最多254字节;104传输不得超过249字节 |
标准104包含 `IEC104_Types.h` 后使用`IEC104_APP_LAYER_PARAMETERS_DEFAULT`,即`{1,1,2,0,2,3,249}`。所有ASDU创建/解析入口必须显式传入非空参数。容器和解析视图借用参数,使用期间参数必须有效且不变。通用ASDU可复用到采用其他宽度的101接入,但本工程尚未提供101串口链路;Receiver、APCI、TcpServer和发送队列继续使用104布局及249字节上限。
每类对象通过自己的`Create`写入调用方提供的结构体,并绑定类型及只读编码函数表。例如单点用`IEC60870_SinglePointInformation_t`,双命令用`IEC60870_DoubleCommand_t`。普通监视和控制的带CP24/CP56对象用`base`保存基础对象、`time`保存时标,并复用基础值处理;保护事件则显式保存事件、QDP、CP16经过时间和时标。对象可按值复制,不需要销毁;业务可修改值或IOA,重新编码时仍会校验,类型和函数表不得手工修改。
容器调用顺序为`IEC60870_Asdu_Create → IEC60870_Asdu_Add_InformationObject → IEC60870_Asdu_Get_Data`。Create固定参数、SQ和公共头,首个成功添加的对象确定Type ID;后续对象必须同类型。SQ=1要求IOA连续且不越过所选宽度上限,SQ=0逐对象携带地址。Add成功后已经复制对象字节,可以立即复用对象存储。
容量取实际缓冲区与`maxSizeOfASDU`的较小值。空间不足、对象达到127个或单对象类型已加入一个对象时返回`IEC60870_ASDU_FULL`;业务提交当前非空ASDU后另起一条重试。空容器放不下首对象时须调整容量;文件数据须按可用长度分成更小的段。类型不匹配、SQ地址不连续或字段非法返回ERROR。所有失败保留容器、原文和输出参数,首对象失败不会提前设置Type ID。结果必须显式比较`IEC60870_ASDU_OK`,不可按布尔值判断。
业务包含 `IEC60870_Asdu.h`;本例使用104默认配置,还需包含 `IEC104_Types.h`。以下代码放入业务函数,`data`在原缓冲区未修改期间有效:
```c
uint8_t buffer[IEC104_MAX_ASDU_LEN];
const IEC60870_AppLayerParameters_t parameters = IEC104_APP_LAYER_PARAMETERS_DEFAULT;
IEC60870_Asdu_t asdu;
IEC60870_AsduHeader_t header = {
.cause_of_transmission = IEC60870_ASDU_COT_SPONTANEOUS,
.common_address = 1
};
IEC60870_SinglePointInformation_t point;
const uint8_t *data = NULL;
uint16_t length = 0;
IEC60870_AsduResult_t result =
IEC60870_SinglePointInformation_Create(&point, 100, 1, 0);
if (result == IEC60870_ASDU_OK) {
result = IEC60870_Asdu_Create(
&asdu, &parameters, buffer, sizeof(buffer), &header, 0);
}
if (result == IEC60870_ASDU_OK) {
result = IEC60870_Asdu_Add_InformationObject(&asdu, &point.object);
}
if (result == IEC60870_ASDU_OK) {
result = IEC60870_Asdu_Get_Data(&asdu, &data, &length);
}
/* OK时可将data/length交给Enqueue_Event;提交返回WAIT时保留原文重试。 */
```
监视和命令统一使用`IEC60870_Asdu_Parse`校验整条报文,再通过`IEC60870_Asdu_Get_Element`取得独立类型的对象。例如处理单命令:
```c
/* parameters、data、length对应收到的ASDU;处理期间原文保持有效。 */
IEC60870_AsduInfo_t info;
IEC60870_AsduResult_t status =
IEC60870_Asdu_Parse(&parameters, data, length, &info);
if (status == IEC60870_ASDU_OK && info.type_id == IEC60870_ASDU_C_SC_NA_1) {
IEC60870_SingleCommand_t command;
status = IEC60870_Asdu_Get_Element(&info, 0, &command, sizeof(command));
if (status == IEC60870_ASDU_OK) {
/* info保存COT/CA/OA;command.object.ioa、state、is_select、
* qualifier供业务判断地址、限定词、选择/执行及权限。 */
}
}
```
Get_Element输出无需预先Create,必须按`info.type_id`选择匹配的具体结构体并传入其尺寸;尺寸检查不能替代C类型匹配。解析视图借用原文和参数,取出的具体对象按值保存。只处理单个载荷时,可直接调用对应类型的`Get_From_Buffer`,传入已解析IOA及不含IOA的精确载荷;读命令载荷为0字节,允许NULL。
归一化值使用原始`int16_t`(实际值为原值/32768),标度值使用`int16_t`,短浮点保留IEEE 754 binary32位模式。累计量用`IEC60870_BinaryCounterReading_t`保存32位计数、5位序号及CY/CA/IV。对象层校验品质保留位、SCO、DCO/RCO状态、VTI有符号范围、保护事件QDP和CP24/CP56;COT、CA、IOA及限定词的业务适用性由应用判断。
`Build_Response(&parameters, request, length, cause, is_negative, out, capacity, &out_length)`仍以`IEC60870_Asdu_`为前缀。它复制请求,只替换COT/P/N,保留TEST、存在时的OA、CA、VSQ及全部对象原文,支持原地和部分重叠。它仅作公共头及最低长度检查,可回显未知类型。生成后携带原请求generation调用Send_Response,并处理WAIT/ERROR/FATAL。
`IEC60870_Asdu_Create_From_Buffer(&asdu, &parameters, buffer, length)`直接借用可写原文,建立可修改的ASDU对象。它只做公共头及最低长度检查,未知类型也能建立对象,用于COT=44等错误应答;执行业务前仍须使用完整Parse和业务规则校验。借用对象不能Add,`Set_COT`、`Set_Negative`、`Set_CA`原地修改相应字段;Get_Data可以导出原文。Create创建的构造容器也可使用这三个设置接口,空构造容器仍不能导出。Set_CA按COT宽度定位、CA宽度写入,1字节仅允许0..255,2字节允许0..65535;非法容器或地址溢出返回ERROR且不修改原文,业务地址限制仍由业务层判断。ASDU结构新增来源标志,调用方须一起重新编译。
### 文件段和新增时间格式
`IEC60870_FileSegment_t`含255字节的数据数组。Create和Get_From_Buffer把有效数据复制进对象,调用结束后即可复用输入;对象无需销毁。它的线上载荷为`NOF(2)+NOS(1)+LOS(1)+数据(LOS)`,解析精确检查LOS与报文长度,不接受截短和多余尾字节。
先用`IEC60870_FileSegment_Get_Max_Data_Size(parameters)`计算当前配置允许的数据长度。默认104配置返回236;最短101头、1字节IOA、254字节上限时返回245。返回0表示只能容纳空段,-1表示配置非法或连空段都装不下。Create允许0..255字节,实际加入ASDU时仍检查总长;超过容量返回FULL,调用方负责分段。
`IEC60870_Time_To_CP16/From_CP16`处理0..65535毫秒的经过时间。`IEC60870_Time_To_CP24/From_CP24`处理分钟0..59、分钟内毫秒0..59999和IV;bit6是保留位,CP24没有SU、小时或日期。CP24编解码保留IV,不自动推断完整日历。所有接口均沿用成功1、失败0且保留输出的约定。
### 子站统一入口
业务包含`IEC104_Slave.h`,持有一个不透明的`IEC104_Slave_t *`。Slave内部创建、驱动和销毁TcpServer;配置、调度、状态和结果仍复用`IEC104_TcpTypes.h`中的值类型,业务头文件不引入TcpServer操作接口或内部布局。
生命周期为`IEC104_Slave_Create → Config_Default/Init → Prepare/Process或Poll → Close → NextEvent读空 → Destroy`。所有操作在同一事件循环线程串行执行。静态池容量沿用`IEC104_TCP_SERVER_CAPACITY`,与直接使用TcpServer共享底层名额,无动态分配。创建失败返回NULL;其他生命周期接口显式比较`IEC104_TCP_OK`。
`IEC104_Slave_NextEvent(slave, &event)`按原FIFO交付`IEC104_SlaveEvent_t`:`event.transport`是TCP事件副本;I_FRAME且`event.asdu_result == IEC60870_ASDU_OK`时,`event.asdu`已经借用事件自身的ASDU字节,`event.connection`已经绑定原服务实例及原连接代次。业务不再传入server/generation,也不再手工Create_From_Buffer。字段/地址/命令权限仍由业务校验,未知类型可以原文交付,视图创建成功不等于允许执行命令。
普通循环使用以下顺序;Application_Handle_Event为应用自己的同步处理函数,其中可以提交应答或复制事件到延迟任务:
```c
IEC104_TcpResult_t result = IEC104_Slave_Poll(slave, 10, NULL);
if (result == IEC104_TCP_OK) {
IEC104_SlaveEvent_t event;
IEC104_TcpEventResult_t next;
while ((next = IEC104_Slave_NextEvent(slave, &event)) == IEC104_TCP_EVENT_READY) {
Application_Handle_Event(slave, &event);
}
/* next为ERROR时处理内部读取错误;EMPTY表示本轮事件已读空。 */
}
```
外部事件循环使用Slave_Prepare/Process,继续保留原service_token和socket token;pending_event_count或need_process非零时先处理本地工作。主动上报通过`IEC104_Slave_Get_Application_Layer_Parameters`取得参数构造ASDU,再调用`IEC104_Slave_Enqueue_Event(slave, &asdu)`进入NORMAL。关闭单连接传原事件给`IEC104_Slave_Close_Connection(slave, &event, error)`,库内部使用该事件的原代次。
### 共用连接上的激活确认和激活终止
共用接口`IEC60870_MasterConnection_t`继续供101/104业务复用;当前104的接口由Slave_NextEvent自动安装,101串口后端仍未提供。下例放在业务已验证请求并决定肯定确认的分支,event为地址稳定的Slave事件:
```c
if (event.transport.type == IEC104_TCP_EVENT_I_FRAME &&
event.asdu_result == IEC60870_ASDU_OK) {
bool accepted = IEC60870_MasterConnection_Send_ACT_CON(
&event.connection, &event.asdu, false);
if (!accepted) {
IEC104_TcpSubmitResult_t submit =
IEC104_Slave_Get_Last_Submit_Result(&event.connection);
/* 按下表处理submit;WAIT需要保留事件及原文,不能忽略或盲目重发。 */
(void)submit;
}
}
```
| 共用操作 | 行为 |
|---|---|
| Send_ASDU | 提交给定ASDU对象的当前原文,也可提交业务另外构造的数据ASDU |
| Send_ACT_CON | 原地设置COT=7及指定P/N,然后提交 |
| Send_ACT_TERM | 原地设置COT=10、P/N=0,然后提交 |
| Get_Application_Layer_Parameters | 返回该连接的标准104只读参数,最大249字节 |
确认及终止先修改事件副本的字节再发送,失败也不回滚;TEST/OA/CA/VSQ和对象原文保留。需要原请求内容时,在修改前保存副本。
| 详细结果 | 布尔返回 | 业务处理 |
|---|---|---|
| OK | true | 已复制到发送缓冲或HIGH,可复用发送原文;不代表已写完或已ACK,不重复提交 |
| WAIT | false | 本次未接手;保留已修改的ASDU及原事件,待容量恢复后重试 |
| ERROR | false | 参数、布局、旧代次或调用阶段非法,不能按容量等待盲目重试 |
| FATAL | false | 发送路径故障,可能已写出字节;结合RECORD/DISCONNECTED处理 |
事件含有指向自身的ASDU视图和连接上下文,不能用赋值或memcpy搬到另一地址。延迟任务可让NextEvent直接写入自身稳定存储,或调用`IEC104_Slave_Copy_Event(&task.event, &event)`;复制时同时保存字节、重建自引用,并保留原服务实例和原连接代次。源事件随后可被下一次NextEvent覆盖,任务副本仍独立可用;复制不代表需要重发已经OK的报文。多个任务的事件存储由应用提供,库不额外引入待办任务池。
总召内部使用紧凑存储:只保留`Get_Event_Identity`取得的原身份及10字节请求ASDU,按需建立视图,通过`Slave_Send_Response / Slave_Try_Send_Response`应答,关闭原连接用`Slave_Close_Connection_By_Identity`。任务槽、事件队列和发送容量保持不变,内存测量见[固定存储](IEC104_Interrogation_设计架构.md#2-对象身份与固定存储)。其他需要完整事件的业务仍可用Copy_Event。
NextEvent返回EMPTY/ERROR时保持输出不变。Close、重连、重新Init或Destroy后复用服务槽,都不会把旧任务改绑到新连接;发送继续检查原身份。Destroy成功后旧服务句柄失效,但旧事件的字节仍由任务自身保存。所有延迟任务也在所属事件循环串行提交。
多帧总召的数据和终止应经同一事件的connection依次提交,逐帧处理WAIT;把数据放NORMAL而终止放HIGH会造成优先级越序。门面不实现点表、总召状态机、遥控选择/执行或系统校时。
[子站示例](../../examples/iec104_slave_example.c)通过BEGIN/NEED_DATA/FINISHED事件与显式应答接入独立的[总召模块](../../104/business/IEC104_Interrogation.h)推进业务;Slave和TcpServer继续负责服务、连接及传输。总召接入顺序见[完整调用流程](IEC104_完整调用流程.md#flow-interrogation)。
调用`IEC104_Interrogation_Process(gi)`自动推进当前就绪任务,无需传入预算。每个实例至多一个未终结总召,包含等待生成BEGIN的请求;忙时相同和不同QOI的新激活均否定,不保存后续总召。4个任务槽共用于这个总召和待发短应答。Handle_Event在原会话有效、Slave通知已读空且无更早REPLY时尝试立即提交短应答,未接手再保存。满槽时尝试否定确认;不能安全提交/保存则仅丢新请求并累计dropped_requests,保留当前任务及TCP。Process在空闲、等待应用/事件空位、发送WAIT或Slave通知未读空时返回。对象、状态、调度及接入约定见[总召模块设计](IEC104_Interrogation_设计架构.md)。
总召Get_Status的`need_process`用于决定是否立即续跑,不能从`waiting`取反推断。应答或读取业务事件后重新查询;发送WAIT后等待网络推进再重试。每次Slave_Poll/Process及Slave通知处理结束仍须推进总召,即使need_process为false。外部事件循环同时合并Slave_Prepare的调度条件;新增状态字段后调用方须重新编译。
BEGIN由调用者判断是否接受,遍历状态可延至首个NEED_DATA建立;NEED_DATA由调用者选择本批IOA并读取当前值和品质,协议库不登记或冻结点表。Submit_Batch立即尝试不排队提交,WAIT不保存数据、不推进游标,应用在网络推进后重新取当前值重试;OK才固定帧顺序并推进批次。点值更新及突发发布须与总召读值及提交串行化,持续突发可能推迟总召,详见[数据批次与发送路径](IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。
BEGIN携带实际CA、station_id和QOI=20..36;应用检查数据源并决定接受,首个NEED_DATA才需建立取数游标。多目标广播先完成全部BEGIN/ACT_CON,再逐站取数,最后全部ACT_TERM;每站状态区保证FINISHED无损交付。Submit_Batch只接受事件目标CA。新增/替代站下轮参加,尚未选入BEGIN便删除的站跳过;已经选中尤其已确认的原站必须完成或明确终结。各站独立计时,总期限默认关闭;FINISHING的终结事件读完前仍忙拒绝新激活。详见[总召设计](IEC104_Interrogation_设计架构.md#33-广播地址与本站地址)。
主站COT=8按原连接、目标CA/OA/TEST/QOI匹配总召,包括已确认但尚未取数或等终止的站。定向撤销匹配站,其他站继续;广播撤销停止整轮,对各仍活动的站回肯定COT=9。FINISHED(DEACTIVATED)通知应用清理,匹配站旧批次失效。已被发送层接手的帧仍按顺序发送,撤销不补发成功ACT_TERM。
业务接受和每批取数期限默认各15秒,执行整体期限默认0(禁用),各项均可单独配置;这些是业务默认策略,不是标准规定的总召时限。关闭整体期限允许慢链路累计传输超过原60秒上限,底层单帧发送、确认和链路探测期限仍有效。BEGIN生成时启动已启用的业务期限,此前等待业务事件空位没有独立超时,但已占用总召资格,应及时读取FINISHED。总期限显式设为非零后仍按固定整体耗时计算,不因新批次或发送重试而延长。START/CONFIRM超时及BEGIN阶段Fail_Request复用原槽发送否定ACT_CON,保留TCP及其他短应答;肯定ACT_CON被发送层接手后,取数超时、总超时和数据源失败仍关闭原连接。终结事件保留具体原因,BEGIN未取出则只移除工作事件。接受BEGIN前检查数据源及必要业务资源,仍可逐批取值并编码。等待前查询timer_active/timeout_remaining_ms并持续驱动Process。此次失败处理修复不新增接口或配置开关;先前已移除queue_timeout_ms及多总召排队状态/统计,保留negative_submitted和dropped_requests,使用旧字段的调用方须调整并重新编译。
原`IEC104_SlaveConnection_Init`和手动接入对象已移除,调用方改用Slave服务及其事件接口并重新编译。TCP底层接口仍供独立通信接入和维护测试使用,业务的默认入口统一为Slave。
本次接口直接替换为独立对象与统一ASDU入口,删除旧通用监视/命令联合体及分类解析、Builder接口,不提供兼容包装;调用方须按上述示例迁移并重新编译。构建须新增`IEC60870_InformationObjects.c`。
时标保留IV/SU;IV=1可解码但不代表可校时。业务新上报应提供本地标准时间且SU=0,应答回显保留请求时标。验证命令和覆盖范围见[测试说明](../test/IEC104测试文档.md)。
### 时钟同步业务
[ClockSynchronization](../../104/business/IEC104_ClockSynchronization.h)处理Type 103、COT=6、IOA=0,按共用站点表路由;COT=5读时和COT=8撤销回45。独立静态池默认与TCP服务容量相同,每实例4个任务槽,与总召/计数量分别创建、分别取消。Handle_Event校验保存,Process处理期限及FIFO应答,只有应用显式Execute_Next会调用本次传入的同步action,不保存函数指针。
默认源标准时区480分钟、执行期限15000ms,0禁用期限;IV/SU及时区由公共时间工具处理,64位毫秒计算只补偿受理后的站内滞留。action收到的local_time已经是固定UTC+8,base_ms为执行检查时刻。肯定/否定确认保留原CP56字节,不回读时钟或提供自定义确认时标。
调用过action的确认走Try,错误校验应答走普通HIGH;WAIT只重试确认,STOP取消未交接任务但不回滚时钟。广播动作一次,快照目标分别确认;满槽丢新并计数,不断链。同内容的新请求仍执行,可能再次回拨。持续NORMAL发布可能推迟Try确认,应用通过包含回调执行期的confirm_pending串行协调新的带时标上报。默认示例只设模拟时钟,`--clock system`才调用Windows时间源。完整契约见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md)和[调用流程](IEC104_完整调用流程.md#flow-clock-synchronization)。
### 单点、双点遥控业务
[Command](../../104/business/IEC104_Command.h)处理Type45/46、COT=6激活与COT=8撤销;默认两种类型都须先选择。每个Slave只能绑定一个实例,同一实例从创建事务到读取FINISHED并交接完应答期间一直忙,所有站点的新遥控互斥。Type47..51及58..64未接手,继续交给后续处理器或44兜底。
Handle_Event校验并生成SELECT/EXECUTE事件,Accept先消费事件再至多调用一次本次传入的动作;不保存回调。EXECUTE启动后先Report_Result记录设备结果,再逐帧Submit_Return_Info,全部OK后Complete。确认未交接时仍可报告结果以停止执行期限;返回信息WAIT时由应用保存ASDU重试。执行确认、返回信息与ACT_TERM走Try,模块内应答按全局FIFO提交。
已取走工作事件的事务恰好产生一次FINISHED;FINISHED只结束协议事务,应用自行维护尚未结束的物理输出和站点引用。退出时Cancel、读完FINISHED,再关闭Slave并分派余下通知,最后Destroy。完整规则见[遥控设计](IEC104_Command_设计架构.md)、[API第25节](IEC104_API接口汇总.md#file-25)和[遥控调用流程](IEC104_完整调用流程.md#flow-command)。
## 启停主流程
| 角色与过程 | 调用顺序 |
|---|---|
| 主站启动 | `Prepare_Startdt_Act` → 实际发送 → `Complete_Startdt_Act_Send` → 收CON → `Handle_Startdt_Con` |
| 主站停止 | `Request_Stopdt` → `Prepare_Stopdt_Request` → 依次发送S/ACT并提交完成 → 收CON → `Handle_Stopdt_Con` |
| 子站启动 | 收ACT → `Handle_Startdt_Act`(立即STARTED/IDLE)→ 发送CON → `Complete_Startdt_Con_Send` |
| 子站停止 | 收ACT → `Handle_Stopdt_Act`(立即STOPPED/STOP_DRAINING)→ `Prepare_Stopdt_Response` → 按需发送S、等待I帧确认 → 发送CON → `Complete_Stopdt_Con_Send` |
上表省略统一前缀 `IEC104_Link_`。只有具体接口返回值表明已生成报文时,才将本次输出交给发送层;生成不等于已发送。部分 Handle 只受理请求或提交对端确认,例如 `Handle_Stopdt_Act` 不生成应答,后续由 `Prepare_Stopdt_Response` 推进。S帧完成交给 `Complete_S_Ack_Send`。被控站不暂存相反方向ACT;停止等待期间成功受理START即恢复STARTED/IDLE,结束尚未生成STOPDT_CON的停止等待。
正常控制流程应串行执行启停,例如 `STOPDT_ACT → STOPDT_CON → STARTDT_ACT → STARTDT_CON`。主站的忙或重复请求结果不表示可以再次发送ACT。子站交错请求按实际受理顺序改变状态:START激活,STOP停用。已经生成的CON始终按FIFO发送,后到请求不撤回旧CON,旧CON完成也不覆盖后到ACT已经提交的状态。具体语义见 [链路设计第4.2节](IEC104_Link_设计架构.md#42-被控站受理启动)。
被控站成功受理STARTDT_ACT时立即进入STARTED/IDLE,并生成STARTDT_CON。首个CON尚未Complete时,后续每个成功受理的重复START也生成一帧CON;同状态重复请求不再次产生激活通知。所有CON按FIFO发送,Complete只确认对应发送结果,不重新提交启停状态。首个STARTDT_CON尚未完成也能受理STOP,立即停用并进入STOP_DRAINING。具体分支见 [链路设计第4.2节](IEC104_Link_设计架构.md#42-被控站受理启动)。
## 子站发送模块(已实现)
三个模块采用变长业务队列、发送前编号和独立确认记录组织,HIGH满时拒绝、NORMAL覆盖最旧原文,同时支持无malloc、部分写入续传、本地发送期限、严格启停/TESTFR、逐帧回执及可配置防饿死配额。
ASDU队列的状态以业务条目为单位:`unsubmitted_*` 表示队列尚未登记发送交接的待发条目。操作顺序是 **SendBuffer接手完整I帧 → Manager建立发送记录 → Mark_Submitted完成队列记账**。最后一步执行前,队列仍保留待发旧状态,不能据此判断SendBuffer是否已经接手。这里的“已提交”不表示整帧已写完或对端已ACK。
HIGH在完成交接记账时消费队列原文,SendBuffer中的完整I帧及Manager记录继续有效;NORMAL则保留原文等待ACK。`retained_unacked_count` 统计原文仍保留、已完成交接记账且待确认的ASDU条目,HIGH恒为0;`overwritten_unacked_count` 统计NORMAL原文已覆盖、仍待确认的条目。全部已编号未ACK的I帧数等于Manager的 `high_unacked_count` 加NORMAL保留及覆盖待确认数。`is_storage_empty` 只表示ASDU原文环为空,HIGH和NORMAL仍可能有对应I帧待确认;SendBuffer的 `is_idle` 表示当前无帧和预留。接口和行为变化见 [发送模块接口迁移](IEC104_发送模块_设计架构.md#migration),调用方须同步迁移、重新编译并初始化。
- **AsduQueue**:HIGH/NORMAL各用一个调用方提供的字节数组。两级使用不同条目头:HIGH仅存2字节长度;NORMAL存条目编号、状态及长度,头大小为 `sizeof(IEC104_AsduNormalEntryHeader_t)`,当前Windows 64位GCC下为16字节。条目恰好占本级头长加ASDU实际长度,头和原文均可跨尾部回绕,无额外回绕填充。两级都不保存user_tag,只有NORMAL分配条目编号。未提交条目通过待发游标直接定位。HIGH空间不足返回FULL,提交后立即释放原文;NORMAL保留原文到有效ACK,空间不足时覆盖最旧原文,包括已提交未ACK条目,成功返回OK且无逐条丢弃通知。条目超过目标桶总容量返回ERROR,须修正容量或组帧,不清空旧数据。
- **SendBuffer**:结构内只存一份255字节当前I帧,另用调用方提供的 `IEC104_SendBufferControlFrame_t[]` 保存S/U,每槽APDU仅6字节。当前帧连续写完;控制队列排在下一未编号业务之前。取消了APDU节点池、控制预留量和重复的协议序号边界。
- **SendManager**:Connection在准备实际写socket时调用 `Prepare_Next_Send`,无当前帧才重新选业务并分配N(S)。重复调用只返回当前帧剩余部分,最多一帧I尚未本地完成。实际写入后用 `Handle_Send_Progress`;解析后的I/S用 `Handle_I_S_Frame`。有效ACK更新所有对应发送记录;HIGH只核减Manager的未确认计数,NORMAL释放仍保留的原文或核减覆盖待确认数。确认记录由TcpServer复制为RECORD事件后经Connection移除,业务稍后处理事件副本。原文出队或覆盖不撤回当前帧、不释放k窗口,也不重置期限。
业务入口为Slave;其下的TCP及发送内部调用主线为:
```text
主动上报:TcpServer_Enqueue_Event → 服务ASDU队列(首次连接前、断线期间均可缓存)
连接应答:TcpServer_Send_Response → Connection_Submit_Response → Manager.Submit_Response(直接准备或缓存HIGH)
总召数据:TcpServer_Try_Send_Response → Connection_Try_Submit_Response → Manager.Try_Submit_Response(只能直接准备,不能排队则WAIT)
内部发送:Connection经Manager准备字节 → TcpServer经Net写socket → Connection回报Manager进度
内部接收:TcpServer经Net收字节 → Connection经Receiver成帧、由Manager统一提交链路事件
事件交付:TcpServer复制I帧/回执到事件队列并释放内部资源 → 业务通过NextEvent取副本
```
内部模块读取AsduQueue时,`AsduHeadInfo` 通过 `asdu_first/asdu_first_length` 和可选的 `asdu_second` 返回原文,`asdu_length` 是总长度;需要连续副本可用 `Copy_Asdu`。HIGH的 `entry_id.entry_serial` 恒为0,仅核对当前代次;从Peek到Mark/Discard之间不得移除、清空或替换队首,0编号不能当作可长期保存的条目身份。HIGH提交或清空后、NORMAL原文被覆盖或ACK释放后,对应借用立即失效。Manager已处理跨界原文,传输层取得的APDU仍连续。每级需同时保留N条、ASDU最长L字节时,HIGH配置 `N × (2 + L)`,NORMAL配置 `N × (IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN + L)`,无需另留回绕空隙;HIGH的N只计待发条目,NORMAL的N还包括原文仍保留的未ACK条目。Manager的构帧函数另使用249字节局部拼接数组,需计入栈预算。
仅NORMAL使用64位非零循环条目编号:`UINT64_MAX → 1`,回绕不改变连接代次,也不清空队列。先后顺序按编号环距离判断;NORMAL保留条目及覆盖待ACK区间不能跨越半个编号环,旧记录阻止窗口前进时NORMAL入队返回FULL,处理旧条目或ACK后可重试,HIGH不受该编号窗口阻塞。NORMAL条目释放后身份失效,调用方不得保存旧身份跨完整编号周期再次使用。NORMAL头内状态在Mark时改为已提交,Rebind时恢复未提交;接口说明见[发送模块设计](IEC104_发送模块_设计架构.md#aq-functions)。
`Prepare_Next_Send` 的成功值是 **OK**;`Submit_Response`以OK表示直接准备、QUEUED表示已缓存HIGH;控制帧入队成功仍为QUEUED,不能按布尔值判断。两条I帧路径共用组帧和发送记录,直接应答不经过ASDU队列,也不绕过旧HIGH、当前帧、窗口或公平配额。TcpServer的`Send_Response`最多尝试一次非阻塞写入;EAGAIN保留当前帧和期限,部分写入不能交错其他APDU。当前I帧已经准备后,即使暂时写出0字节,也要先完成,再发后来排队的控制帧。
`Try_Submit_Response`还要求NORMAL没有未提交条目;无发送机会时只返回WAIT,不降级为HIGH排队。Slave提供事件连接入口`Try_Send_ASDU`和纯值身份入口`Try_Send_Response`;总召使用后者,WAIT后重读数据。它们仍通过原SendBuffer、发送记录、k窗口和链路期限,不要求等待所有已提交帧的ACK。已有普通应答和主动上报接口的排队语义不变。
业务提交统一返回`IEC104_TcpSubmitResult_t`:`IEC104_TCP_SUBMIT_OK`表示已接手ASDU,不保证已写完或已ACK;WAIT表示未接手,业务保留重试;FATAL可能发生在已写出字节后,须处理收尾事件。`Enqueue_Event`只缓存服务事件,`Send_Response`尝试直接发送或缓存连接应答;调用后重新Prepare刷新调度条件。旧Enqueue_Normal/Enqueue_High入口与TCP_ENQUEUE结果名已移除,不保留兼容包装。
子站受理STARTDT_ACT即进入STARTED,STARTDT_CON作为发送任务按FIFO完成。STOPDT受理即停用并停止选取新业务,最多完成一帧此前已编号的当前I帧,等待已发帧确认后返回CON;未提交的NORMAL保留未编号,HIGH按下述规则清理。公共S、t2、TESTFR、t3及子站启停继续由Manager封装。接收接口的WAIT表示请求尚未受理,接入方须有界保留原帧并按接收顺序重试。
Manager初始化成功会清空HIGH待发原文并重置高优先配额;新STOP受理时清空HIGH。从停用状态受理START时,生成本轮首个STARTDT_CON并清空HIGH、重置高优先配额;已激活状态下的重复START仅追加CON,保留HIGH和配额。WAIT时请求尚未受理,不清理HIGH。以上清理均不影响NORMAL,也不撤销已提交的当前I帧及其发送记录。
初始化使用字节容量和控制槽数组。此次移除 `Enqueue` 的最后一个user_tag参数,以及条目查询、发送存储记录和记录查询中的user_tag字段;旧调用方须迁移、重新编译并初始化。HIGH发送记录的 `asdu_entry_serial` 恒为0,NORMAL继续保存条目编号,两级都用独立frame_id处理发送完成和帧回执。发送记录数组使用 `IEC104_IFrameSendRecordStorage_t`,连接代次由Manager统一保存;查询输出用 `IEC104_IFrameSendRecord_t`,返回完整 `frame_id`。当前Windows 64位GCC下,移除user_tag后每条存储记录由56字节降为48字节,查询快照由64字节降为56字节:
```c
static IEC104_IFrameSendRecordStorage_t records[12];
IEC104_AsduQueue_Init(&queue, high_bytes, sizeof high_bytes,
normal_bytes, sizeof normal_bytes, generation);
IEC104_SendBuffer_Init(&buffer, control_frames, control_count, generation);
IEC104_SendManager_Init(&manager, &queue, &buffer, &link,
records, 12, k, high_burst_limit);
```
以上省略返回值检查,完整示例、接口替换及容量单位见 [发送模块设计](IEC104_发送模块_设计架构.md#integration) 和 [接口迁移](IEC104_发送模块_设计架构.md#migration)。Link须先按CONTROLLED初始化。Manager允许绑定有未编号业务的队列,绑定成功会清空HIGH、保留NORMAL,不接管运行中的旧连接。
`Manager.Abort` 保留AsduQueue;结束旧I/O并核对记录中的ACK事实后,TcpServer调用 `AsduQueue.Detach` 解除绑定,清空HIGH并恢复NORMAL离线缓存;新连接再 `AsduQueue.Rebind` 到新代次并初始化。**HIGH固定用于当前连接的请求应答,重连清空;NORMAL固定用于允许跨连接恢复的事件,保留现存原文并恢复未提交。** 内部队列不提供逐条重连策略,使用 `Enqueue(queue, generation, priority, asdu, length)`;业务只调用TcpServer的Enqueue_Event/Send_Response。Rebind先完整检查两级条目,再将NORMAL头内已提交状态恢复为未提交并更新控制字段,不搬移ASDU原文,不使用整理数组。NORMAL头含编译器位域及对齐,必须按对应头长宏配置容量,不能沿用旧18字节头的缓冲内容或跨ABI解释。使用约定见 [队列生命周期](IEC104_发送模块_设计架构.md#aq-reconnect-policy)。
Rebind清除旧覆盖待确认计数,所有旧身份和借用失效;已覆盖原文不能恢复。它本身不发送,新连接仍须完成STARTDT;同一TCP上的STOPDT/STARTDT恢复不调用Rebind。未ACK不证明对端未收到,NORMAL只用于业务允许向同一接收方或冗余域恢复的事件;旧请求应答应放HIGH。关闭整个服务时由TcpServer调用Queue.Abort清空缓存,不提供掉电持久化。未编号条目被覆盖或重连丢弃时没有帧回执,旧帧回执须在Manager.Abort前读取;尚不提供冗余组切换或应用业务去重。
Connection通过`SendManager_Get_Deadlines`一次取得当前队首身份、本地发送及I帧t1剩余时间、控制槽容量;对应活动标志为1且剩余时间为0表示到期。原始queued_at_ms仍保存在SendBuffer,written_at_ms仍保存在Manager记录,部分ACK后选下一条的原始时刻。Connection的`Collect_Deadlines`汇合TESTFR、STOP、t2及t3,供超时检查、发送准入、控制推进和等待查询共用;不创建系统定时器,也不缓存第二份计时起点。原有底层单项超时接口保留。TcpServer在Process中执行TCP读写并驱动Connection,Poll复用该推进核心。
发送准备和写入回报仅因本地发送及STOP期限直接报告超时;ACK/TESTFR的t1故障交给接收阶段判定。旧确认t1已到时,统一发送准备入口暂停为HIGH/NORMAL创建、编号新I帧;已有帧及控制发送仍可推进,避免控制槽解阻后继续新增业务帧。有效确认解除所有已到期等待后自动恢复准入。Send_Response和常规发送仍按send后的真实时刻登记完成,发送期间跨过确认期限时安排立即Process,先有界处理已到达的确认,再按原始t1检查。Prepare对到期协议期限返回零等待;不增加恢复期限或固定宽限时间,也不改变真实容量背压下的超时策略。
## 子站连接协调层(已实现)
TcpServer内部的`IEC104_Connection_t`内含一个Receiver、一个Link、一个SendBuffer和一个SendManager,借用TcpServer提供的ASDU队列、控制槽数组及发送记录数组。初始化后保持对象地址不变,同一连接的所有调用串行执行。内部接口、时间规则及重连顺序见[连接层设计](IEC104_Connection_设计架构.md)。
Connection_Init先准备Receiver及配置,再由`SendManager_Init_Connection`校验并向最终地址提交Link、SendBuffer和绑定关系,最后提交其余连接字段;失败保留原Connection和队列。
控制帧是否可准备由Link的`Can_Prepare_*`只读接口判断,并与实际构帧共用规则。Connection对Link保留只读访问,运行期修改统一经SendManager,TESTFR_CON也经`SendManager_Handle_Testfr_Con`提交;只有返回OK才撤销本端探测t1,IGNORED不取消计时。Connection不直接查询SendBuffer,也不操作Manager的内部指针。Connection及SendManager的完整结构体供内部静态嵌入;TcpServer公开头文件隐藏这些布局,业务只使用TCP句柄、公开值类型及事件接口。
以下步骤均在TcpServer及Connection内部执行:
1. `Connection_Init`以CONTROLLED角色初始化STOPPED链路。成功受理STARTDT_ACT即进入STARTED/IDLE并开放I帧准入,发送队列保证已有CON先于随后准备的本端I帧写出。
2. 收到TCP字节后调用`Connection_Feed`,按`consumed_length`推进输入。一次最多处理一帧;未消费尾部由TcpServer保留。半帧留在Receiver中;S/U受理后自动释放;I帧返回FRAME_READY后由TcpServer经Peek复制为事件,再按`receive_serial`显式Release。
3. WAIT表示当前接收请求尚未受理,Connection保留原文并在后续驱动中重试。成功受理的START/STOP按控制帧路径自动释放原帧;重复驱动不会再次提交同一I帧的序号或ACK。
4. `Connection_Prepare_Next_Send == SEND_READY`时取剩余字节实际写入,再用`Handle_Send_Progress`回报增量;0字节和部分写入保留当前帧及原期限。整帧完成自动提交对应Link完成和Manager记录,不能另调底层Complete。本地已知发送完成必须先回报,再Feed覆盖该帧的ACK。
5. 定期调用`Connection_Tick`推进本地发送期限、I帧/TESTFR的t1、w/t2确认、t3探测和STOP总期限。停止过程中的重复STOP和WAIT不延长原期限;START成功受理取消旧STOP总计时。TESTFR等待CON的t1不因其他链路活动续期。t2保留连续未确认接收区间的最早起点,前缀被确认时不续期。
6. FATAL后连接冻结,TcpServer停止真实I/O,把剩余记录和断开通知复制为事件,再调用Connection_Close。Connection_Close保留内部ASDU队列;TcpServer随后Queue.Detach清空HIGH并保留NORMAL,离线期间继续接受NORMAL;重连时Queue.Rebind到新代次,再Connection.Init。仅Close服务清空全部ASDU。所有旧代次事件返回ERROR,不修改新连接。业务只处理TcpServer交付的事件并执行ASDU业务逻辑。
旧 `IEC104_Rx_Feed_Byte`、`IEC104_Rx_Reset`、`IEC104_Legacy_Link_Reset`及相关类型保持原样;旧入口继续使用全局单路状态、预设STARTED,并在成帧后检查I/S序号,只返回接收结果。Connection使用新Receiver并共享自身Link与SendManager,不调用旧全局接收入口。同一条连接不能同时交给这两条接收路径。
因接口过期而无法编译的旧回归测试及专用辅助文件已删除。当前有`iec104_tcp_event_ring_test.c`的13个字节环场景、`iec104_tcp_scheduler_test.c`的25个调度场景,以及`iec104_net_pending_input_test.c`的Windows回环检查;构建和运行见[测试说明](../test/IEC104测试文档.md#2-编译与运行)。
2026-10-20调度修复:预算让出时固定已到达输入范围并立即续跑,处理完后再判定ACK/TESTFR超时;后续新输入不能扩大旧批次。发送使用独立预算,持续接收不再跳过发送阶段。业务API及配置宏不变,内部Connection和Net新增续收/输入快照支持,自定义Net后端需实现`IEC104_Net_Pending_Input`。真正容量背压仍遵守原有严格期限;回执与启停通知的顺序继续按B方案。
以下为历史变更与验证记录,不代表当前保留的测试范围:
2026-09-23:NORMAL空间不足时改为覆盖最旧原文(含已提交未ACK条目),HIGH继续满时拒绝;当前APDU和发送记录独立保留,ACK兼容原文已覆盖。条目支持跨尾部存放,无填充碎片。仅更新代码和设计文档,测试文件保持原样。
2026-09-23:三发送模块改为变长ASDU字节环、单当前I帧和紧凑控制队列,发送前编号,有效ACK释放原文,业务回执独立消费,支持显式Rebind。保留ACK不得越过本地完成位置的修复。原有全部119个用例通过;本次补充校验从标准输入编译执行,未修改或新增测试源码、测试配置和测试文档。
2026-09-18:LINK测试改为报文回放用例,当时默认LINK套件68个、全部套件119个场景通过。该套件已随过期测试清理移除,历史记录保存在`.build`中。
2026-09-16:TESTFR改为三态,取消提前CON的缓存;当时旧用例因状态枚举变化而无法编译,未计为该次变更的验证结果。
2026-09-14:完善STARTDT期间的收发方向门控,当时新增LINK 36至40。算法契约见[链路设计](IEC104_Link_设计架构.md)。