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

64 KiB
Raw Blame History

IEC104 链路模块

当前子站业务统一包含IEC104_Slave.h:Slave内部持有不透明TcpServer,提供生命周期、事件循环、主动上报和状态查询,并在NextEvent交付I帧时自动建立ASDU视图及原连接的共用应答接口。业务不再手动创建TcpServer、绑定generation或安装函数指针。入口和延迟任务示例见子站统一入口。

Slave提供共用站点表:应用提供固定地址节点,动态注册多个CA和业务context,重复CA拒绝。删除立即退出路由,已有引用和遍历结束后才回收;Is_Reusable为true后应用才能释放存储。总召设置config.use_station_registry=true后按请求CA选站;多目标广播配置每站状态区,以一个任务执行“全部确认、逐站数据、全部终止”。其他业务可复用相同的查找、引用及快照遍历接口。

总召由独立库模块IEC104_Interrogation.c/h实现,支持QOI=20站总召、21..36分组总召及多类型无时标监视数据,负责批次校验、发送等待、主站撤销、业务超时及停用/断线取消。应用通过BEGIN/NEED_DATA/FINISHED事件和显式应答管理IOA、分组、遍历、逐批取值及终结;子站示例提供模拟点表、事件分派和main,构建运行见示例说明。

内部调用关系为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队列积压查询。

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不可重试约定见同步拒绝当前请求。

发送队列NORMAL与NextEvent业务通知FIFO是两份独立存储:NORMAL满时可覆盖旧ASDU原文;已入队业务通知逐条保留,不覆盖、不合并启停变化。通知按有效载荷长度存放于字节环,配置宏仍表示保证条数:默认普通30条最长通知、启停4条及收尾32条记录加1个最长I帧和1个断开通知。当前x64 ABI下环占11243字节,接收暂存默认2048字节,两块合计13291字节,比原来减少43.9%;16字节APDU可在普通区缓存185条。普通通知不能占用启停及收尾预留;启停配额满时暂缓受理后续改变状态的ACT,已有CON继续发送。详细公式见TCP服务设计。

新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_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节。

阅读入口 负责内容
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 共用召唤引擎:任务、广播、期限、事件、取消与发送等待;接入见总召流程
business/IEC104_GeneralInterrogation.c/h Type100规则及兼容的Interrogation_Create入口
business/IEC104_CounterInterrogation.c/h Type101规则、创建及一次动作执行入口;见计数量设计
business/IEC104_ClockSynchronization.c/h Type103校时校验、显式动作及原文确认;见时钟同步设计
business/IEC104_Command.c/h Type45/46遥控、选择执行、撤销、三类期限及返回信息;见遥控设计
IEC104_TcpTypes.h Slave与TcpServer共用的配置、调度、事件和状态值类型,无服务操作接口
IEC60870_InformationObjects.c/h 67种独立信息对象,覆盖监视、命令、初始化、参数和文件;编码通过只读函数表分派,支持变长文件段
IEC60870_Time.c/h 101/104共用时间工具:CP16/CP24/CP56Time2a编解码、本地标准时间转换、SU修正、日历/星期和纪元秒数工具;使用约定见时间工具设计
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_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.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_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格式、时间基准、毫秒来源及掉电状态由具体驱动处理。接入示例见统一时间源读写,业务待接入项见当前接入状态。

时间戳转日期保留两种实现:Windows默认调用CRT的64位_gmtime64_s;编译IEC60870_Time.c时定义IEC60870_TIME_USE_PLATFORM=0即可使用原有整数算法,其他平台默认采用此配置。配置在编译时生效,业务接口、UTC含义及1970..2099年的范围一致。此选项只决定如何转换传入的时间戳,与是否接入RTC无关;应用使用RTC作为时间源时,仍需通过相应驱动读取时间,再按返回值的格式和时间基准调用转换或编码接口。详见实现选择。

业务接收按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,不能单独恢复小时和日期,需由业务结合已有时间基准解释。完整范围、星期和保留位策略见时间工具设计。

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状态。内部接口和迁移说明见 编解码与接收器设计。

接收器校验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_。它们报告最终发送结果,包括失败;返回 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与启停的内部边界。

启停和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在原缓冲区未修改期间有效:

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取得独立类型的对象。例如处理单命令:

/* 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为应用自己的同步处理函数,其中可以提交应答或复制事件到延迟任务:

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事件:

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。任务槽、事件队列和发送容量保持不变,内存测量见固定存储。其他需要完整事件的业务仍可用Copy_Event。

NextEvent返回EMPTY/ERROR时保持输出不变。Close、重连、重新Init或Destroy后复用服务槽,都不会把旧任务改绑到新连接;发送继续检查原身份。Destroy成功后旧服务句柄失效,但旧事件的字节仍由任务自身保存。所有延迟任务也在所属事件循环串行提交。

多帧总召的数据和终止应经同一事件的connection依次提交,逐帧处理WAIT;把数据放NORMAL而终止放HIGH会造成优先级越序。门面不实现点表、总召状态机、遥控选择/执行或系统校时。

子站示例通过BEGIN/NEED_DATA/FINISHED事件与显式应答接入独立的总召模块推进业务;Slave和TcpServer继续负责服务、连接及传输。总召接入顺序见完整调用流程。

调用IEC104_Interrogation_Process(gi)自动推进当前就绪任务,无需传入预算。每个实例至多一个未终结总召,包含等待生成BEGIN的请求;忙时相同和不同QOI的新激活均否定,不保存后续总召。4个任务槽共用于这个总召和待发短应答。Handle_Event在原会话有效、Slave通知已读空且无更早REPLY时尝试立即提交短应答,未接手再保存。满槽时尝试否定确认;不能安全提交/保存则仅丢新请求并累计dropped_requests,保留当前任务及TCP。Process在空闲、等待应用/事件空位、发送WAIT或Slave通知未读空时返回。对象、状态、调度及接入约定见总召模块设计。

总召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才固定帧顺序并推进批次。点值更新及突发发布须与总召读值及提交串行化,持续突发可能推迟总召,详见数据批次与发送路径。

BEGIN携带实际CA、station_id和QOI=20..36;应用检查数据源并决定接受,首个NEED_DATA才需建立取数游标。多目标广播先完成全部BEGIN/ACT_CON,再逐站取数,最后全部ACT_TERM;每站状态区保证FINISHED无损交付。Submit_Batch只接受事件目标CA。新增/替代站下轮参加,尚未选入BEGIN便删除的站跳过;已经选中尤其已确认的原站必须完成或明确终结。各站独立计时,总期限默认关闭;FINISHING的终结事件读完前仍忙拒绝新激活。详见总召设计。

主站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,应答回显保留请求时标。验证命令和覆盖范围见测试说明。

时钟同步业务

ClockSynchronization处理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时间源。完整契约见时钟同步设计和调用流程。

单点、双点遥控业务

Command处理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。完整规则见遥控设计、API第25节和遥控调用流程。

启停主流程

角色与过程 调用顺序
主站启动 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节。

被控站成功受理STARTDT_ACT时立即进入STARTED/IDLE,并生成STARTDT_CON。首个CON尚未Complete时,后续每个成功受理的重复START也生成一帧CON;同状态重复请求不再次产生激活通知。所有CON按FIFO发送,Complete只确认对应发送结果,不重新提交启停状态。首个STARTDT_CON尚未完成也能受理STOP,立即停用并进入STOP_DRAINING。具体分支见 链路设计第4.2节。

子站发送模块(已实现)

三个模块采用变长业务队列、发送前编号和独立确认记录组织,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 表示当前无帧和预留。接口和行为变化见 发送模块接口迁移,调用方须同步迁移、重新编译并初始化。

  • 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及发送内部调用主线为:

主动上报: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时恢复未提交;接口说明见发送模块设计。

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字节:

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);

以上省略返回值检查,完整示例、接口替换及容量单位见 发送模块设计 和 接口迁移。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解释。使用约定见 队列生命周期。

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队列、控制槽数组及发送记录数组。初始化后保持对象地址不变,同一连接的所有调用串行执行。内部接口、时间规则及重连顺序见连接层设计。

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回环检查;构建和运行见测试说明。

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。算法契约见链路设计。