443 lines
64 KiB
Markdown
443 lines
64 KiB
Markdown
# 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, ¶meters, 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(¶meters, 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(¶meters, 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, ¶meters, 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)。
|