4 Commits

Author SHA1 Message Date
STF e1b0434774 补充遥调业务代码
CI / L0 静态门禁 (pre-commit) (push) Failing after 45s
CI / L0.5 主机侧单测 (MSVC) (push) Failing after 43s
2026-11-01 00:38:09 -07:00
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
STF 7596f3371a 补充总召唤的广播地址,电度召唤+时钟同步业务的实现
CI / L0 静态门禁 (pre-commit) (push) Failing after 44s
CI / L0.5 主机侧单测 (MSVC) (push) Failing after 42s
2026-10-29 03:20:19 -07:00
STF 4c55544f05 通信协议代码
CI / L0 静态门禁 (pre-commit) (push) Failing after 45s
CI / L0.5 主机侧单测 (MSVC) (push) Failing after 42s
2026-10-26 08:31:48 -04:00
649 changed files with 533275 additions and 0 deletions
@@ -0,0 +1,454 @@
# IEC104 总召业务模块
`IEC104_Interrogation.c/h`在`IEC104_Slave`之上实现Type100总召业务。通信服务负责连接、启停、收发和会话身份;总召模块负责请求准入、业务阶段、数据批次检查、应答调度及终结通知。应用通过`BEGIN / NEED_DATA / FINISHED`事件提供点表和数据,没有业务回调。
当前采用**每个实例至多一个未终结总召,忙时否定所有新的总召激活**。空闲时支持全站和全部16个分组,忙时不区分相同或不同QOI。任务池仍保留4个槽,用于一个总召及待发送短应答;没有后续总召等待队列,也没有独立排队超时。
本文描述库本身的设计,不依赖应用模拟点表或可执行程序。完整接入位置见[完整调用流程](IEC104_完整调用流程.md#flow-interrogation),函数声明及返回值汇总见[API接口汇总](IEC104_API接口汇总.md#file-21)。
## 1. 职责与调用关系
所有接口由所属事件循环串行调用。模块不创建socket、线程或系统定时器,不动态分配内存,也不会在`Process`中跳转执行应用代码。异步采样可以由应用自己的线程或设备驱动完成,但结果须回到所属事件循环后提交。
```text
应用事件循环
├─ IEC104_Slave:Poll / Prepare / Process / NextEvent
│ └─ TcpServer → Connection → Link / Receiver / SendManager → Net
└─ IEC104_Interrogation
Handle_Event ← 应用逐条转交Slave通知
Process → 沿保存的原连接身份提交确认、终止及短应答
Submit_Batch → 沿保存的原连接身份尝试直接提交数据,不入队等待
NextEvent → BEGIN / NEED_DATA / FINISHED
Reply_Begin / Submit_Batch / Complete_Data / Fail_Request ← 应用显式应答
```
| 模块 | 自己维护的规则 | 与总召的关系 |
|---|---|---|
| Slave | 服务生命周期、事件来源、连接代次、启停及提交结果 | 总召通过公共接口取得原身份、校验会话和提交应答 |
| TcpServer及内部通信模块 | TCP字节流、APCI序号、k/w、t0..t3、HIGH容量和发送完成 | 总召不直接操作socket、链路状态或发送窗口 |
| Interrogation | 一个总召的准入与阶段、短应答、业务事件及业务期限 | 不持有应用点表,不主动读取设备 |
| 应用 | IOA、分组成员、数据源、遍历状态、资源清理 | 根据业务事件决定接受、逐批取值交付或终结数据 |
`Slave_Get_Event_Identity`校验事件来源;`Slave_Check_Request`判断是否有未读通知、原会话是否仍可提交。总召不自行拼凑TCP状态快照判断连接是否有效。应用应使用Slave和总召公开接口,不访问不透明对象内部字段。
## 2. 对象、身份与固定存储
`IEC104_Interrogation_t`是不透明的静态池句柄,通过`Create`取得,`Destroy`归还。一个Slave只能绑定一个总召实例;须在开始消费Slave通知前创建。配置按值复制,调用方配置返回后可复用;Slave须存活到总召实例销毁之后。
设置config.use_station_registry=true后,总召按请求CA选取本Slave的已注册站点,65535逐站遍历;config.station须NULL,common_address忽略。当前任务持有节点引用,删除使该站立即退出新请求路由,已有任务继续使用原节点。BEGIN携带station_id,应用跨循环保留context时应立即按编号查节点并Acquire,FINISHED清理后Release,防止同CA替换串站。默认false兼容原单CA调用,可选config.station仍固定绑定一个站点并持有到Destroy。注册、引用、延迟回收及快照契约见[共用站点表](IEC104_API接口汇总.md#station-registry)。
| 存储 | 默认容量 | 保存内容与约束 |
|---|---|---|
| 总召实例池 | 随`IEC104_TCP_SERVER_CAPACITY` | 各实例的会话、配置、任务及业务事件互不混用 |
| 每实例协议任务池 | 4槽 | 至多一个未终结总召;其他槽仅保存单帧应答,也允许4槽全是应答 |
| 每实例业务事件FIFO | 4槽,最小2槽 | 单目标预留FINISHED位置;多目标终结记录保存在调用方状态区,按需交付 |
| 多站广播状态区 | 调用方提供,每目标站一项 | 保存站引用、身份、计时及进度,不保存点值或数据ASDU |
| 总召数据存储 | 不设置私有批次缓冲 | WAIT不接手数据;OK时发送层已复制到当前APDU |
编译实现文件时可覆盖`IEC104_INTERROGATION_CAPACITY`、`IEC104_INTERROGATION_TASK_CAPACITY`、`IEC104_INTERROGATION_EVENT_CAPACITY`。增加任务槽只增加短应答积压容量,不允许第二个总召。任务池设为1仍可工作,但当前总召占槽且短应答不能立即提交时,新请求只能丢弃并计数。
协议任务只保存`InterrogationResponse`:由`Slave_Get_Event_Identity`取得的原服务/实例/连接代次,加10字节Type100 ASDU。合法请求复制原文并处理应答CA;可可靠应答的畸形单对象请求只保存规范为10字节、VSQ=1的否定应答副本,不执行激活或撤销。错误IOA、QOI、OA、TEST等字段仍完整保留。每次发送临时建立ASDU视图,调用`Slave_Send_Response`或`Slave_Try_Send_Response`;任务不保存完整SlaveEvent或自引用接口,源事件出队后可立即复用。固定任务槽及`order[]`调度规则保持不变,非队首短应答完成后仍立即释放对应槽。
2026-10-27按默认4任务槽、4业务事件槽测量,Windows x64 GCC的`sizeof`如下;这是结构存储尺寸,不是STM32的链接占用,Keil结果须另行实测。
| 项目 | 精简前 | 精简后 |
|---|---:|---:|
| 每任务请求存储 | 400字节(完整SlaveEvent) | 40字节(原身份及10字节ASDU,含对齐) |
| 每任务 | 480字节 | 120字节 |
| 每实例4个任务合计 | 1920字节 | 480字节 |
| 每个总召实例 | 2400字节 | 960字节 |
每实例减少1440字节(60%)。任务槽数、业务事件FIFO、TCP通知环、HIGH/NORMAL容量、k窗口及事件覆盖策略均保持原样,节省来自请求表示,不减少背压容量。立即应答也不再在栈上复制完整SlaveEvent;GCC `-O2 -fstack-usage`中该函数从464降至96字节,但这不是ARMCC或完整调用链的实测栈水位。
上表保留请求存储精简阶段的比较。当前Windows x64默认配置实测:总召1240字节、每任务168字节、紧凑请求40字节、业务事件80字节、配置40字节。相对逐站完整执行版本的1192字节,管理对象增加48字节;多目标广播另需调用方提供每站48字节的状态区,13站为624字节。单目标不要求状态区。广播共用一个任务及一套应用取数游标,HIGH/NORMAL和任务/事件容量不变。数值不是目标板RAM实测;站点及Slave开销见[共用站点表](IEC104_API接口汇总.md#station-registry)。
业务事件是纯值结构,可以复制和跨循环保存。`instance_id`区分静态池槽的历次使用,`request_id`在实例内递增,`batch_index`区分本次取数交互;编号耗尽时拒绝新建,不回绕复用。TCP的`generation`属于另一层身份,防止旧请求向新连接发送。
## 3. 请求校验与单总召准入
### 3.1 接收范围及校验顺序
总召使用`C_IC_NA_1`,Type=100、SQ=0、一个对象、IOA=0。支持激活COT=6和撤销COT=8,请求P/N须为0,CA可为已注册站地址或广播65535(0xFFFF);兼容模式只接受其配置站。实际站CA限于1..65534。QOI=20为全站,21..36为第1..16组;分组内容由应用维护。
先校验事件身份,再保护长度并读取类型字节。非100返回`IGNORED`,总召不代替其他业务发送UNKNOWN_TYPE;公共ASDU视图不可用时也返回`IGNORED`,由公共入口处理。正常网络路径的公共头及最低载荷已由Receiver检查,真实截短/帧错误仍沿连接层原策略处理。Type100先判断是否存在唯一完整对象,可靠时按`COT → CA → IOA → 布局/QOI/P/N → 事务状态`处理,避免忙状态掩盖地址或传送原因错误。
| 条件 | 结果 |
|---|---|
| Type不是100,或公共视图不可用 | `IGNORED`;公共入口应先过滤不可用视图,再分派其他业务 |
| Type100对象数为0或大于1,无法确定唯一完整IOA/QOI | 计入rejected并返回`HANDLED`,丢弃新请求,不生成应答 |
| Type100单对象完整,但SQ=1或有多余尾字节 | 仅回否定COT=7/9,COT/CA/IOA错误仍优先45/46/47;不执行激活或撤销 |
| COT不是6或8 | 回COT=45,P/N=1 |
| CA既不是本站地址也不是65535 | 回COT=46,P/N=1,保留请求中的错误CA |
| IOA不是0 | 回COT=47,P/N=1 |
| 激活QOI超范围或P/N非法 | 回否定ACT_CON,COT=7、P/N=1 |
| 撤销QOI超范围、P/N非法或没有匹配对象 | 回否定DEACT_CON,COT=9、P/N=1 |
| 合法激活,已有未终结总召 | 回否定ACT_CON,不替换原请求,不建立第二个总召 |
| 合法激活,空闲且有任务槽 | 保存请求、分配request_id,进入START |
| 合法激活,空闲但任务槽被短应答占满 | 尝试否定确认;不能提交或保存时丢弃新请求并计数 |
确认和错误应答调整COT及P/N;请求CA为65535时,应答CA改为本站地址,其他CA原样保留。OA、TEST、IOA和QOI保持原文,包括错误请求中的原值。畸形单对象的否定副本规范为10字节、VSQ=1,原输入事件不修改;该容错应答是本项目策略,不是将畸形命令视作有效命令。忙拒绝同样使用否定ACT_CON,没有另设“忙”的专用传送原因。
上述请求错误不会关闭TCP、取消已有总召、重置原期限或生成其FINISHED(INVALID_REQUEST),其他CA任务继续。否定应答复用原HIGH/FIFO背压与满槽丢弃策略,不扩大任务或缓冲容量。无唯一对象的丢弃只计入rejected;dropped_requests仍专指无法安全提交/保存的容量丢弃。内部服务故障、会话失效及发送失败继续走原失败路径。
### 3.2 忙状态的起止点
忙状态从合法激活被保存为START开始,早于生成BEGIN和提交肯定ACT_CON。因此,同一批网络通知连续包含多个激活时,只有第一个可建立总召,后续请求立即进入否定应答路径。不能等到应用接受后才设置忙,否则等待应用期间会接入多个总召。
忙状态覆盖BEGIN尚未生成、等待应用接受、确认等待发送、等待数据、有效批次未获发送机会及终止等待发送。相同QOI、不同QOI、不同OA或TEST的新激活都不能覆盖原请求。
| 终结方式 | 何时释放总召资格 |
|---|---|
| 正常完成 | ACT_TERM被发送层接手,释放任务;不等待TCP写完或主站ACK |
| 应用拒绝BEGIN | 生成FINISHED(REJECTED),原任务转为否定REPLY |
| 肯定确认提交前超时或BEGIN数据源失败 | 终结原因保留为对应TIMEOUT或SOURCE_ERROR,原槽转为否定REPLY,保留TCP及其他任务 |
| 主站匹配撤销 | 结束原业务,原任务转为肯定撤销REPLY |
| 本地Cancel、STOP、断线或确认后失败 | 取消总召及本模块待发短应答,保留应有的FINISHED |
单站终结并转为REPLY后不再占用总召资格;多目标广播中某站拒绝、超时或定向撤销时,仍为其他站保留总召资格。广播网络流程结束后进入FINISHING,gi_pending为false,但仍占用状态区并忙拒绝新激活,直到全部FINISHED出队。此前被拒绝的请求不会自动启动,主站需要重新发起。
### 3.3 广播地址与本站地址
广播CA表示请求面向所有站。`IEC104_Types.h`以`IEC104_CA_BROADCAST`定义0xFFFF(65535),它不是实际站地址。开启共用站点表后,多目标广播采用c104的三阶段顺序:先按注册顺序完成各站BEGIN及ACT_CON,再逐站处理全部数据,最后逐站ACT_TERM及FINISHED。例如两站线上顺序为CON1、CON2、DATA1、DATA2、TERM1、TERM2;每站数据可有多批或为空。各站有独立request_id、进度和期限,但共享一个任务和一个取数位置。兼容单站及仅含一个目标的广播保持单目标流程。
接收合法多目标广播时,把目标节点引用保存到`config.broadcast_states`;`broadcast_capacity`须覆盖当时的注册站数。数组由调用方持有至Destroy,地址稳定,不得读写或与其他实例重叠;无需逐项初始化。容量不足在任何肯定确认前整轮拒绝,各站收到否定ACT_CON,并累计overloads,不截取部分站执行。单目标不需要此数组,注册表本身仍无固定站数上限。
目标集合不冻结IOA或点值。后来新增或同CA重建的节点下轮广播才参加;尚未选入BEGIN便被删除的站跳过。已选中站持原引用完成,尤其已确认但尚未取数的站不能因删除而静默消失;应用通过station_id取得原context。无法继续取数时显式Fail_Request。没有注册站时广播回复COT=46并保留65535。忙或错误广播仍使用共用站点快照遍历实际CA,无槽保存遍历时整体丢弃新请求并计数。
每次Process最多推进2倍任务容量步,默认8步;剩余可提交应答通过need_process继续调度。多目标每站状态区保留终结原因,NextEvent按FIFO空位逐个交付,即使13站也只需2槽事件FIFO。发送WAIT不推进当前发送项,仍须重试。确认前拒绝或超时只结束该站并继续确认其他站;确认后取数失败或超时关闭原连接,整轮取消,各已交付BEGIN都有FINISHED。CA选站、QOI选组,两者独立,广播支持20..36。
以仅注册CA=7为例(广播多站时逐站应用同一规则):
| 请求或交互 | CA的处理 |
|---|---|
| 激活或撤销CA=7 | 接受本站寻址,后续应答CA=7 |
| 激活或撤销CA=65535 | 映射到本站7,后续应答CA=7 |
| BEGIN / NEED_DATA / FINISHED | request.common_address均为7,应用无需判断原请求是否广播 |
| Submit_Batch | 必须提供CA=7;CA=65535或其他站地址返回ERROR且不消费当前交互 |
| 肯定/否定ACT_CON、ACT_TERM、肯定/否定DEACT_CON、广播请求的错误应答 | CA均为7,发送等待与确认前失败不改变这条规则 |
| 其他CA且前置校验已通过 | COT=46、P/N=1,保留原错误CA,避免伪装成本站请求 |
输入的`const SlaveEvent`始终保持原文。Request完整解析Type100后选择实际目标站,广播从首站开始;未知CA保留原值。错误原因仍按COT、CA、IOA、QOI/P/N优先检查。撤销匹配本轮活动站集合,不依赖注册表首站或当前取数位置。
内部Copy_Response_Request保存原身份及10字节ASDU,再用临时视图调用Asdu_Set_CA。广播每次切换站点才更新副本CA,同一站的WAIT重试只重建视图、设置COT/P/N并沿用原CA。副本不借用调用方事件,也不重新绑定当前连接。
任务仅保留info这一份业务元数据,数据COT、OA、TEST及CA均按当前任务info检查;CA必须等于当前目标站。错误短应答不生成BEGIN。数据批次不自动改写地址,非法批次返回ERROR供应用修正;同一IOA在不同CA下属于不同点。
`IEC60870_Asdu_Set_CA`是共用ASDU层的字段接口,按COT宽度定位CA并按CA宽度写入小端字节,支持构造容器和借用可写原文。1字节CA只允许0..255,2字节允许0..65535;参数或容器无效时返回ERROR且不修改原文。它不负责站地址的业务合法性,总召配置仍拒绝0和65535。
该规则仅属于Type100处理器。即使CA=65535,Type101等其他有效类型仍返回IGNORED,后续业务模块自行决定其寻址及应答规则。
## 4. 协议阶段与完成含义
```text
空闲收到合法激活
→ START:保存请求,已占用总召资格
→ 业务事件有空位,生成BEGIN,启动业务计时
├─ Reply_Begin(false) → FINISHED(REJECTED) → REPLY(否定ACT_CON)
└─ Reply_Begin(true) → CONFIRM
→ 提交肯定ACT_CON成功 → DATA
→ NEED_DATA → Submit_Batch → OK → 下一个NEED_DATA
→ WAIT → 保留事件和游标,重读当前值后重试
→ NEED_DATA → Complete_Data → TERMINATE
→ 提交ACT_TERM成功 → FINISHED(COMPLETED) → 释放任务
任何未终结阶段:新激活 → 否定REPLY;原总召阶段与进度不变
匹配撤销:停止当前业务 → FINISHED(已取出BEGIN时)→ REPLY(肯定DEACT_CON)
START/CONFIRM超时或BEGIN失败:终结当前业务 → REPLY(否定ACT_CON),保留连接
DATA/TERMINATE超时或NEED_DATA失败:关闭原连接,取消本模块任务,不发成功ACT_TERM
```
| 阶段 | 等待或动作 | 推进条件 |
|---|---|---|
| START | 等事件空位生成BEGIN,随后等应用决定 | 单目标需要BEGIN及终结预留;多目标终结位置由状态区保证 |
| CONFIRM | 提交肯定ACT_CON | 多目标继续下一站确认,全部确认完成后开始数据 |
| DATA | 生成NEED_DATA,应用读取当前值并尝试直接提交 | Submit_Batch返回OK才消费交互、增加批次和对象计数 |
| TERMINATE | 提交ACT_TERM | 发送层接手后终结;发送WAIT时仍然忙 |
| REPLY | 一条否定确认、错误应答或撤销确认 | 提交成功即释放,不产生新的取数交互 |
| FINISHING | 多目标网络流程结束,等待应用读完FINISHED | 最后一个终结事件出队后释放任务及节点引用,才接受新总召 |
| IDLE | 状态查询时表示没有协议任务 | 不表示业务FIFO一定为空,仍可能有FINISHED待取 |
`Reply_Begin / Complete_Data`成功表示应用决定已被模块接手,后续`Process`提交确认或终止。`Submit_Batch`返回OK则表示数据已被发送层接手并确定帧顺序;不再等待Process提交本批。`FINISHED(COMPLETED)`表示ACT_TERM已被发送层接手;这些成功状态均不保证报文写完或被主站确认。发送层可能随后因STOP或连接故障终止传输。
## 5. 应用事件与资源生命周期
### 5.1 三类业务事件
| 事件 | 应用需要做什么 | 应答接口 |
|---|---|---|
| BEGIN | 根据QOI检查数据源并决定是否接受;可延至首个NEED_DATA建立取数游标,不采样整轮数据 | `Reply_Begin(true/false)`;数据源失败可`Fail_Request` |
| NEED_DATA | 应用选择本批IOA与类型,读取当前值和品质后构造非空批次;暂未就绪则保存本事件,继续驱动通信 | `Submit_Batch`、`Complete_Data`、`Fail_Request`三者择一 |
| FINISHED | 停止对应采样工作,按实例/请求编号释放资源 | 无需应答 |
`NextEvent`主动取出事件;事件入队不会执行应用代码。BEGIN或NEED_DATA只有被取出之后才允许应答。工作事件只发出一次,应用暂未就绪时保存事件,不能反复等待模块发出同一事件。
模块核对实例、请求、批次、事件类型、当前阶段和已取出标志。应答时须原样传回事件;实际元数据和发送身份始终使用内部保存的原请求,不依赖应用回传字段构造发送目标。
| 应答结果 | 调用方处理 |
|---|---|
| OK=1 | 交互已消费,不要重复应答;输入ASDU可复用 |
| WAIT=0 | 有未读Slave通知,或Submit_Batch暂不能直接发送;保留事件及游标,推进通信、读空通知后重试;数据批次须重新读取当前值组包 |
| STALE=-2 | 事件已失效或不匹配,停止重试;按FINISHED清理资源 |
| ERROR=-1 | 参数或批次不合法等;通常不消费交互,可修正后重试。NEED_DATA阶段Fail_Request关闭失败是例外,任务仍已终结 |
### 5.2 FINISHED预留
单目标生成BEGIN需要两个业务位置:一个用于BEGIN,一个预留给未来FINISHED。NEED_DATA不能占用终结预留,始终保持`实际事件数 + 终结预留数 ≤ 事件容量`。多目标广播改由每站状态区保存终结记录,因此BEGIN只需一个FIFO空位,所有站可以先确认而不扩大FIFO。pending_events包含尚在状态区中等待入队的FINISHED,可能大于FIFO容量。
应用取出BEGIN后,无论正常完成、拒绝、撤销、STOP、断线、取消或超时,都恰好产生一次FINISHED。BEGIN尚未生成或尚未取出便取消时,移除工作事件并释放预留,不产生FINISHED,因为应用尚未取得请求。取消同时移除未读NEED_DATA;已经取出的旧事件再应答返回STALE。
历史FINISHED保留到应用读取。业务FIFO容量不足时,新的唯一总召可能停留在START等待空位;它已经占用总召资格,但尚未生成BEGIN,不启动业务期限。应用必须及时读完终结事件,不能只等待新的网络通知。默认容量4时可以同时保留旧FINISHED及下一次BEGIN;测试使用最小容量2验证这个边界。
多目标广播进入FINISHING后,连续NextEvent即可读完全部终结记录,无需靠Process搬运。应用不能假定BEGIN与FINISHED严格交替。示例在首个NEED_DATA建立唯一游标,Complete_Data返回OK后立即归还该站取数资源;失败/取消则按FINISHED清理仍在取数的站。若应用在BEGIN另行建立每站资源,须按request_id管理并等对应FINISHED清理。
## 6. 数据批次与发送路径
应用以`Asdu_Create`及信息对象接口构造批次,再调用`Submit_Batch`。模块立即尝试不排队提交,不保存待发批次、应用点表、对象指针或数据源回调。WAIT时输入存储也可复用,但同一业务事件和遍历位置须保留到成功或终结。
IOA、数据类型、分组成员和遍历方式均由调用者决定。BEGIN用于检查数据源和判断是否接受,遍历状态可延至首个NEED_DATA建立,不向库登记或冻结完整IOA列表。应用自行保证总召或分组点的遍历完整性,包括点表变化时如何避免漏点、重复点;库不强制点表在整轮期间保持不变。
推荐接入方式是在每次NEED_DATA组包时读取本批点的当前值和品质,不默认在BEGIN冻结整轮数值。值与品质的读取一致性由数据源保证;逐批取值不承诺整轮所有点属于同一采样时刻。需要固定时刻快照的应用可以自行实现,但必须另行处理快照与突发上报的顺序,不能把快照当成库提供的一致性保证。
总召数据通过`Slave_Try_Send_Response → TcpServer_Try_Send_Response → Connection_Try_Submit_Response → SendManager_Try_Submit_Response`提交,Slave按保存的原身份校验服务实例及连接代次。只有当前发送缓冲空闲、HIGH和NORMAL均无未提交条目、链路和发送窗口允许时,才直接准备I帧并确定N(S)。否则返回WAIT,不把旧值放入HIGH等待。NORMAL中仅保留已提交、待ACK的原文不单独阻挡本批,仍须满足k窗口;不要求等待全部ACK或整轮总召结束。
应用须把“更新当前点值并发布突发”与“读取本批当前值并调用Submit_Batch”放在同一串行执行上下文,或用等效同步保证二者不交错。WAIT之后只保留事件和游标,每次重试重新取值:已积压的突发先发送,随后总召读取最新值;总召返回OK后,本帧顺序已固定,后来发布的突发不能插入该帧之前。这同时避免旧NORMAL被新总召越过,以及新突发之后重发WAIT期间缓存的旧总召值。
这个保证依赖应用发布顺序:库不比较测点版本,无法识别应用在读取之后才迟到发布的旧采样,也无法修正调用者仍使用BEGIN数值快照的做法。突发发送不因总召被暂停;若HIGH/NORMAL持续有待发条目,总召数据可能持续WAIT,本路径没有强制抢占或最长完成时间。应用应按实际负载安排上报频率和批次,不能通过重试旧数值消除等待。
| 检查项 | 要求 |
|---|---|
| 应用层参数 | COT/CA/IOA长度分别为2/2/3 |
| 数据Type | 1、3、5、7、9、11、13、20、21,均为本模块支持的无时标监视信息 |
| 数据COT | 等于原请求QOI:全站20,第1..16组为21..36 |
| CA | 等于配置的本站地址,与业务事件request.common_address一致;不能是65535 |
| OA/TEST | 与原请求一致 |
| P/N | 0 |
| 数量与长度 | 至少1个对象,不超过配置上限及249字节;完整布局有效 |
| SQ及类型切换 | 每批允许SQ=0/1;一个ASDU同一Type,不同批次可切换Type |
返回OK时发送层已复制本批原文,应用可立即复用输入缓冲并推进自己的游标,`batch_index / objects_submitted`也已增加。返回WAIT时不消费NEED_DATA、不推进计数、不保存原文,也不重复生成NEED_DATA;应用先处理通知,交互仍有效时在通信推进后重读同一遍历位置并再次调用Submit_Batch。发送ERROR若确认为正常STOP导致的拒绝,返回WAIT等待通知,随后终结为STOPPED;其他发送ERROR/FATAL以SEND_ERROR终结并返回STALE,不能重试原事件。参数或批次校验ERROR则不消费交互,允许修正。
空数据集或已经遍历完毕使用`Complete_Data`,不能用空ASDU代替结束。未配置分组可以在BEGIN时拒绝;接受的空组仍按ACT_CON → ACT_TERM正常结束。累计量数据不在当前总召批次范围,留给总电度业务。模块不检查IOA是否属于请求的分组,分组筛选和取值由应用负责。
确认、终止和短应答沿原请求connection走普通HIGH应答接口;数据沿同一connection走不排队提交接口,不使用NORMAL,也不进入HIGH等待。只有上一批返回OK后才能完成数据阶段,因此终止不会越过尚未接手的数据。已接手帧按通信层规则发送,不能被新应答越过,也不能通过Cancel撤回。
## 7. 短应答调度与满槽处理
### 7.1 Handle_Event中的提前提交
忙拒绝、地址或COT错误、非法QOI、撤销确认等只需要单帧应答,先尝试提交现有HIGH接口。提前提交必须同时满足:没有未读Slave通知、原会话仍可发送、没有更早形成的REPLY。
```text
形成短应答
→ 有旧REPLY或未读Slave通知:暂不提交
→ 可安全提交:Copy_Response_Request复制请求并写入已确定的应答CA,调整COT/P/N,调用原HIGH接口
├─ 已接手:累计submitted,否定时累计negative_submitted,不占新任务槽
├─ WAIT:未接手,保留/保存为REPLY
├─ ERROR且确认为正常STOP拒绝:保留/保存为REPLY,先交付停用通知
└─ 实际提交错误:按SEND_ERROR等失败路径处理
→ 需要保存但池已满:只丢弃新请求,增加overloads和dropped_requests
```
立即提交不修改调用方事件,不增加绕过通信队列的公开模式。发送成功后本地副本即可释放;WAIT后如需保存任务,以已确定的应答CA建立稳定请求副本,后续只按阶段设置COT/P/N并重试提交。实际连接或发送错误仍按失败路径处理,不能当作普通满槽丢弃。
### 7.2 Process中的选择规则
单帧应答按形成顺序FIFO提交。调度器分别查找唯一总召和最早REPLY,不要求总召位于任务索引队首。总召正在等应用或业务事件空位时,直接处理REPLY;双方都可推进时轮转,避免总召一直挡住短应答,也避免短应答持续占据所有推进机会。
Send_Response内部会推进保留的接收帧并交付通知。Process每步检查FIFO;发送返回值还须区分以下情况,Process、立即应答和Submit_Batch共用同一STOP判定:
| 发送返回值 | 后续处理 |
|---|---|
| OK | 按已接手事实记账/推进,下一次提交前检查通知;不撤回或重发本帧 |
| WAIT | 保留任务并让出;先处理通知,再按原背压规则恢复 |
| ERROR且确认为正常STOP拒绝 | 设置events_pending并让出,保留任务/业务事件,由DEACTIVATED终结为STOPPED |
| 其他ERROR或FATAL | 保留SEND_ERROR失败路径;FATAL可能已写出部分或整帧,不能转成可重试WAIT |
只有ERROR分支额外查询状态:请求须仍属于原服务/实例/代次,TCP仍连接,会话ACTIVE且无故障、队列状态有效,传输已停用并且通知未读,才按正常STOP拒绝让出。不能仅凭FIFO非空忽略错误。成功路径不增加查询;正常STOP拒绝不累计waits、不推进游标、不停止或刷新取数期限。应用先读取Slave通知,旧NEED_DATA随后返回STALE;立即应答保存的短任务也由STOP清理。
Process提交确认、终止或短应答遇到WAIT就结束本轮,保存请求和阶段,下次优先检查最早REPLY。每一步都重新检查Slave通知,发送过程中产生的新通知也必须先处理。不会跨过未读STOP/START或撤销继续发送旧事务。数据WAIT由持有NEED_DATA的应用在通信推进后重读并重试。
`Process(gi)`没有预算参数,也不内部等待:当前站REPLY或终止成功后继续广播或释放任务;DATA只生成NEED_DATA。没有就绪任务、发送WAIT、有未读Slave通知或已推进2倍任务容量步时返回;剩余本地工作由need_process提示续跑。每步发送前重查通知,不执行应用代码。
### 7.3 4个槽的含义
任务池的容量与“同时允许几个总召”是两个独立约束。总召资格上限恒为1,任务池还为无法立即发送的短应答提供存储。忙应答占槽并不代表原激活已被保存,发完否定确认后即释放,不会再生成该请求的BEGIN。
满槽时保留已有任务、应用事件及进度,不主动关闭TCP,不挤掉最早应答。只有新的、无法安全提交或保存的请求被丢弃。`EVENT_HANDLED`包括这种丢弃;应用不能把它当成“主站已经收到应答”,也不能再次投递同一Slave通知。`dropped_requests`用于观察这个情况,主站可能等待超时或自行重试。
## 8. 业务期限与驱动
| 配置 | 默认值 | 起止点 |
|---|---|---|
| `begin_timeout_ms` | 15000ms | BEGIN入业务FIFO至应用接受、拒绝或报告失败,包含未取事件的时间 |
| `data_timeout_ms` | 15000ms | 每次NEED_DATA入队至首次有效批次尝试、声明结束或报告失败,每批重新计时 |
| `total_timeout_ms` | 0(默认禁用) | 启用后从BEGIN生成至ACT_TERM被发送层接手,覆盖取数、事件容量等待和发送等待,不等待主站ACK |
三项默认值是本项目的可配置业务策略,不是IEC104规定的总召时限,也不代替链路t0..t3。每项0表示禁用。执行总期限默认关闭,避免慢链路的累计发送等待触发固定整体截止;BEGIN和每批取数期限仍各为15000ms,底层单帧发送、确认和链路探测期限继续有效。需要限制整次总召耗时时,可显式将`total_timeout_ms`设为非零。没有`queue_timeout_ms`;START尚未生成BEGIN时不计业务期限,但此时仍拒绝后续总召。及时消费业务事件是应用的驱动义务,模块不会通过后台线程保证进度。
所有期限使用Net提供的64位单调毫秒时钟,以经过时间比较,避免校时和绝对截止相加溢出。0毫秒有效。业务应答停止对应阶段期限,下一次NEED_DATA重新计时;显式总期限在同一站内不因新批次、忙应答或重试而延长。每站从自己的BEGIN独立计时,包含等待其他站确认、取数及终止的时间;已确认但不在当前取数位置的站也会被检查,到期关闭原连接并取消整轮。没有额外跨全部CA的整体定时器,同一时刻两项到点优先报告TOTAL_TIMEOUT。
有效批次因发送背压返回WAIT时置`data_send_waiting=true`,说明应用已响应,本批取数计时停止,之后不再恢复,下一批NEED_DATA才重新计时。无效批次或因未读Slave通知而提前返回WAIT不停止取数计时。发送等待仍受底层链路期限及显式启用的总期限约束;模块不替应用重读或重试数据,调用方必须持续驱动保留的事件。
Process和有效应用应答都会检查期限。先消费Slave通知,再判断超时,使已经到达的STOP、撤销或断线优先终结事务。到期后旧交互失效,迟到应答返回STALE;具体处理按肯定ACT_CON是否已被发送层接手划分:
| 失败发生的阶段 | 处理方式 | 连接及其他任务 |
|---|---|---|
| START:BEGIN超时、总超时或Fail_Request | 终结当前业务,复用原槽保存否定ACT_CON;保留BEGIN_TIMEOUT、TOTAL_TIMEOUT或SOURCE_ERROR | 保留TCP及已有短应答 |
| CONFIRM:肯定ACT_CON尚未提交成功,总期限到点 | 转为否定ACT_CON,终结原因仍为TOTAL_TIMEOUT | 保留TCP及已有短应答 |
| DATA / TERMINATE:取数超时、总超时或NEED_DATA阶段Fail_Request | 沿原请求关闭连接,取消本模块任务,不补发成功ACT_TERM | 原连接其他业务也会受影响,不关闭后来复用服务槽的新连接 |
分界是发送层接手肯定ACT_CON,不是应用调用Reply_Begin(true),也不是TCP写完或主站ACK。CONFIRM提交返回WAIT时仍可以拒绝激活;肯定确认即使仅进入HIGH或当前发送缓冲,也已经进入DATA,不能再改发否定确认。确认后的断链是本项目当前执行失败策略,不增加按超时类型区分的配置开关。
内部Reject_Request统一处理应用主动拒绝、BEGIN失败和确认前超时,Fail_Interrogation按阶段选择拒绝请求或关闭连接。不能直接从超时处理调用公开Reply_Begin(false):BEGIN可能尚未取出,该接口的校验也会再次检查期限。内部转换先Finish,再将原槽改为REPLY并移到应答顺序末尾;无需空闲任务槽,保留原QOI/OA/TEST,应答CA使用本站地址,旧工作事件立即失效。BEGIN已取出才产生FINISHED,具体失败原因不会统一改成REJECTED。
否定确认继续按现有调度发送,WAIT保留重试,不再计算已终结请求的业务期限。Process检查到确认前超时后会重新检查Slave通知,在本轮继续推进短应答;应用应答接口检查到超时则返回STALE,调用方仍须驱动Process。失败转换完成即释放总召资格,但原槽要到否定确认提交或取消后才归还。主站可以重新发起请求,模块不自动重放旧请求。
等待前查询`Get_Status`。若`timer_active=true`,以`timeout_remaining_ms`限制下一次Poll或应用等待的上限,0表示立即推进;false表示当前没有启用的业务期限,不代表业务已经结束。状态查询只读,不触发发送、终结或超时。
`need_process=true`表示应立即调用总召Process:例如新请求有空位生成BEGIN、业务应答接手后可提交下一帧、读取旧FINISHED后解除事件容量等待,或业务期限已到期。该值按Process同一选择规则计算,既考虑总召,也考虑排队的短应答;它不是`!waiting`,也不保证发送层一定接手。当前帧提交WAIT后暂停本地续跑,避免空转;每次Slave_Poll/Process推进网络并读空Slave通知后,仍须调用总召Process重试,不能以此标志为false跳过网络进展。
应用持有的数据WAIT也须在上述网络推进后重试,不能只等need_process置位或新的NEED_DATA。`data_send_waiting`表示已交付有效数据但等待发送机会;`application_waiting`只表示仍等待应用首次有效应答,两者都不是发送就绪查询。
Get_Status实时检查Slave FIFO;`events_pending=true`时`need_process=false`,先处理可能存在的STOP、撤销或断线。`pending_events`表示另一条业务FIFO,应及时读取;每次读取事件或调用应答接口后重新查询状态。外部事件循环还须合并Slave_Prepare的等待条件,总召的标志不替代TCP的need_process及链路期限。
## 9. 撤销、取消与连接变化
主站撤销为Type100、COT=8,在忙拒绝判断之前按原连接代次、目标CA、OA、TEST和QOI匹配,包括已选中但BEGIN尚未生成的START。定向撤销匹配站,其他站继续;已确认但尚未取数、数据已交完等终止、或已删除的原站均可匹配。尚未选入BEGIN的未来站没有活动事务。广播撤销停止整轮,对每个仍活动的站发送肯定COT=9,不向已结束或尚未启动的未来站补发确认。
撤销确认组复用原任务槽,排在已有短应答之后;即使任务池满也能撤销。当前站待发的否定ACT_CON不会被其他站的撤销确认覆盖。一组撤销确认尚未提交完时,新的重叠撤销按未匹配拒绝;不能安全保存否定回复时按既有满槽策略计数丢弃。已被发送层接手的帧不回滚,不再为被撤销站发送ACT_TERM。
匹配成功后,先通过Copy_Response_Request保存撤销请求并写入应答CA,使用原激活的info和进度生成FINISHED(DEACTIVATED),随后把原槽转换为肯定DEACT_CON。转换不需要额外任务槽,池满仍可撤销;形成的确认排在已有REPLY之后,条件允许时立即提交。已被发送层接手的帧继续在确认前按原顺序发送,本地未提交批次丢弃,不再提交原总召ACT_TERM。
| 操作或通知 | 本模块行为 | 网络确认 |
|---|---|---|
| 匹配COT=8 | 停止当前总召,旧工作事件失效,必要时FINISHED(DEACTIVATED) | 肯定COT=9 |
| 不匹配或重复撤销 | 当前总召保持原状 | 否定COT=9 |
| `Cancel` | 取消总召及待发短应答,保留必要的FINISHED(CANCELLED) | 不自动发送撤销确认或ACT_TERM |
| STOPDT对应DEACTIVATED通知 | 停止本模块任务,必要时FINISHED(STOPPED) | 链路STOP确认由通信层负责 |
| DISCONNECTED | 取消任务、清除已处理代次,必要时FINISHED(DISCONNECTED) | 无业务成功确认 |
| 新CONNECTED或会话已失效 | 旧任务终结为CONNECTION_CHANGED,不跨代次重放 | 后续新激活重新建立事务 |
COT=8的总召撤销与Slave的DEACTIVATED通知不是同一事件:前者结束一个业务,后者表示STOPDT停用。FINISHED表示应用可以清理资源,不承诺撤销确认已发送完成。
非法批次返回ERROR并保留交互,应用可以修正;数据源确实不能继续则调用`Fail_Request`。BEGIN阶段回否定确认并保留连接,NEED_DATA阶段关闭原连接,两者都终结为SOURCE_ERROR。确认后失败仍可能影响同连接其他业务。接受BEGIN前应检查数据源和必要业务资源,减少确认后才发现无法执行的情况;无需预先采样或编码全部数据,仍可逐批交付。
## 10. 应用接入与退出顺序
创建前使用`Config_Default`初始化配置,再按站地址、单批大小和实际业务耗时调整。公开API共12个,调用功能如下:
| 接口(省略IEC104_Interrogation_前缀) | 功能 |
|---|---|
| Config_Default | 填写CA=1、单批127对象及三项默认业务期限 |
| Create | 复制配置、借用Slave,取得静态池实例 |
| Destroy | 任务及业务事件全部为空时归还实例,不关闭Slave |
| Cancel | 本地取消任务并保留应有终结事件 |
| Handle_Event | 校验并接收应用转交的Slave通知,处理准入、短应答及会话变化 |
| Process | 自动推进就绪任务,遇等待条件返回 |
| NextEvent | 主动读取业务事件,只有READY写出结果 |
| Reply_Begin | 应答已取出的BEGIN,明确接受或拒绝 |
| Submit_Batch | 校验当前NEED_DATA对应的非空ASDU,立即尝试不排队提交;WAIT须重读数据后重试 |
| Complete_Data | 声明数据耗尽,后续Process提交ACT_TERM |
| Fail_Request | 报告数据源失败;BEGIN转否定确认并保留连接,NEED_DATA关闭原连接,均保留SOURCE_ERROR |
| Get_Status | 查询进度、累计计数及最近业务期限,不产生副作用 |
```text
Slave_Create / Config_Default / Init
→ Interrogation_Config_Default / Create
→ 循环
1. 驱动Slave,逐条读取并转交连接通知及请求
2. Slave通知未读空时继续处理通知
3. Interrogation_Process
4. Interrogation_NextEvent,处理BEGIN / NEED_DATA / FINISHED
5. 应用数据就绪时显式应答,继续推进
6. 查询Slave等待条件及Interrogation的need_process、事件和期限,再安排等待
→ Interrogation_Cancel
→ 读完业务事件并清理FINISHED对应资源
→ Slave_Close,读空收尾通知
→ Interrogation_Destroy
→ Slave_Destroy
```
网络驱动可以使用Slave的Poll,也可以接入应用自己的Prepare/Process事件循环。数据暂未就绪不能停止驱动网络;应用也不能在忙等待循环中反复调用Process而不处理事件。
`Destroy`不会隐式取消或丢弃事件。即使没有活动总召,只要还有REPLY或未读FINISHED就返回false;调用方完成清理后再销毁。实例创建失败要清理已创建的Slave,不能继续使用NULL句柄。
## 11. 状态与统计
| 字段 | 解释 |
|---|---|
| generation / active | 最近已处理的连接代次和启用状态,不能代替实时会话查询 |
| gi_pending | 总召网络流程未终结;FINISHING虽为false,状态区清理前仍忙拒绝新激活 |
| phase | 当前站阶段,收尾为FINISHING;仅有应答为REPLY,无任务为IDLE |
| pending_requests | 总召、FINISHING及短应答占用的任务槽数,不是等待执行的总召数 |
| pending_events | FIFO事件及状态区中待交付的FINISHED,不包含预留位置 |
| waiting / application_waiting | 存在发送、应用或事件容量等待;后者表示仍等待应用首次有效应答 |
| data_send_waiting | 当前批次已有效尝试但发送WAIT;不保存原文,应用须保留事件、重读数据并重试 |
| events_pending | 当前Slave通知尚未读空,应先处理网络通知 |
| need_process | 当前有需立即推进的本地工作、到期期限或失效会话;发送WAIT后等待网络推进再重试 |
| timer_active / timeout_remaining_ms | 是否启用业务期限及距最近期限的剩余毫秒 |
| batch_index / objects_submitted | 当前总召已提交的批次和对象数;仅有REPLY时保留最早应答的历史进度 |
| submitted / gi_submitted | 被发送层接手的ASDU数 / ACT_TERM提交成功的总召次数 |
| waits | 实际提交返回发送WAIT的次数,不统计应用等待循环次数 |
| rejected | 校验、忙、应用决定、确认前失败和容量原因拒绝的请求数,含无法可靠应答而丢弃的畸形请求 |
| negative_submitted | 发送层已接手的否定应答数,不等于主站已收到 |
| overloads / dropped_requests | 任务池满或广播状态区不足的次数 / 无法安全提交或保存的新请求丢弃数 |
| cancelled | 取消的协议任务数,包含待发REPLY;不等于FINISHED数量 |
| deactivated / timeouts | 成功匹配主站撤销次数 / 执行业务超时次数 |
| last_finish_reason | 最近合法总召的终结原因,BEGIN尚未交付时取消也会更新 |
状态为独立副本,累计计数从Create开始,Cancel和重连不清零。`waiting=true`不代表所有任务都无法推进:总召等数据时仍可提交短应答。`gi_pending=false`也不表示可以直接Destroy,仍须检查待发应答及业务事件。
确认前超时增加timeouts及rejected,BEGIN阶段Fail_Request增加rejected;两者都不增加cancelled或gi_submitted。只有否定确认被发送层接手才增加negative_submitted。REPLY等待期间反复Process不会重复累计超时或交付FINISHED,last_finish_reason保留原终结原因。
## 12. 总电度扩展与设计边界
忙检查放在Type100激活处理内部,Type101总电度请求不会因总召忙被本模块否定。后续总电度模块应维护自己的状态、请求身份、QCC及数据流程,不能复用总召的gi_pending作为全局准入开关。
接入多个业务处理器时,连接、启停和断线通知应广播给每个相关模块,不能因某个模块返回HANDLED就停止广播;I帧按业务类型分派,未接手的交给后续处理器,最后再由应用兜底。总召模块不实现总电度冻结、复位或累计量点表。
多个业务可以分别持有任务,但仍共享连接、HIGH容量和发送窗口,应用需要持续驱动各模块。业务忙相互独立不代表发送时延相互独立;STOP、断线和任一业务触发的连接关闭会影响同连接所有业务。
本设计参考了[lib60870-C多客户端示例](https://github.com/mz-automation/lib60870/blob/master/lib60870-C/examples/multi_client_server/multi_client_server.c)按连接限制活动总召、忙时及会话槽满时否定确认的做法。该示例仅处理QOI=20,本项目保留20..36分组支持,使用应用主动读取事件、固定请求副本和明确的WAIT重试。其会话槽保存不同连接的活动总召,不能理解为同一连接的后续总召队列。
业务期限、静态容量和忙拒绝是本项目的实现策略。当前不提供多QOI待执行队列,不自动替主站重试,也不通过应用回调获得下一批数据。若现场要求保存连续分组请求,需要单独定义准入、主站确认等待和取消语义,不能仅扩大任务槽数量。
## 13. 验证与调用方迁移
可控网络回归共97组,使用真实Slave/TCP提交路径及可控单调时钟;HIGH缩到251字节、业务FIFO缩到2槽。覆盖原有突发排序、部分写入、取数期限、k=1窗口及到期ACK;多站覆盖13站三阶段顺序、非空混合批次、状态区不足与跨实例重叠、已确认未来站删除/同CA重建、超出FIFO容量的无损FINISHED、各阶段定向及满槽广播撤销、拒绝与撤销同时等待、非当前站总期限。畸形请求覆盖否定/丢弃边界、输入不变、基础截短仍被Receiver拒绝、原任务/期限保持、背压满槽和广播撤销隔离。提交期STOP覆盖三个入口、真实通知容量背压、FATAL与STOP并存、非STOP的ERROR、OK/WAIT伴随新回执及START后重新总召。真实TCP回环验证命令行单站,使用Windows后端和k=1逐帧核对业务字节。
| 验证范围 | 应满足的结果 |
|---|---|
| 全站及全部16组 | QOI、数据COT、OA、TEST及点对象验证正确 |
| 广播全站及全部16组 | 业务事件、确认、数据、终止均用本站CA;输入Slave事件保持原文 |
| 广播错误、忙拒绝及确认前失败 | 副本建立时使用本站CA,立即提交和等待重试均保持该地址;其他错误CA仍按原值回显 |
| 广播数据地址 | Submit_Batch拒绝65535及其他站地址,应用修正为本站CA后可重试 |
| BEGIN生成前后、确认/数据/终止等待 | 相同和不同QOI的新激活均否定,原身份和进度不变 |
| 完成后重新发起 | 不自动启动被拒绝请求;主站重发后产生新request_id并获得完整期限 |
| 总电度类型隔离 | 总召空闲和忙时,本站或广播CA的Type101均返回IGNORED,不产生总召否定应答 |
| 短应答及满槽 | 安全时提前提交,积压保持FIFO;满槽只丢新请求,旧工作事件仍有效 |
| 远方撤销 | 本站/广播激活与本站/广播撤销四种组合均匹配实际站CA,其他身份仍严格检查;满槽仍可复用原槽 |
| 业务FIFO满 | FINISHED保留;BEGIN前不计业务期限,读出旧FINISHED后才能启动当前请求 |
| 本地续跑 | 事件空位恢复及业务应答后need_process置位;发送WAIT时不空转,网络推进后继续发送;未读Slave通知优先 |
| 三项执行期限 | 迟到应答不能绕过期限,忙应答不刷新总期限,先处理已到达的STOP/撤销 |
| 确认前失败 | 未读BEGIN超时不交付FINISHED;已交付时恰好终结一次,否定确认保留QOI/OA/TEST并使用本站CA |
| 满槽及发送等待期间失败 | 原槽转REPLY,不挤掉旧应答;按形成顺序发送,保留TCP,同连接重试可以完成 |
| 肯定确认提交边界 | CONFIRM发送WAIT时超时回否定确认;确认已接手但尚未写出时,取数失败及超时仍关闭原连接 |
| STOP、断线及实例复用 | 旧请求不能跨会话发送或接受迟到应用应答 |
| 真实TCP连续分组请求 | 第一个完成,后续得到否定确认;结束后重发的请求分别正常完成 |
| 真实TCP广播流程 | 全站、分组、忙拒绝、非法QOI及撤销均返回本站CA;连接继续工作 |
从先前多QOI排队版本迁移时,删除`queue_timeout_ms`、`queued_interrogations`、`queue_timeouts`字段访问,以及`QUEUED / FINISH_QUEUE_TIMEOUT`分支。配置和状态布局已改变,所有调用方须重新编译并以Config_Default初始化配置。`Process`继续只接收实例,不增加预算参数;公开函数仍为12个。`FINISH_OVERLOAD`仅保留旧枚举值,当前满槽不以此终结已有总召。
状态新增`need_process`和`data_send_waiting`,所有调用方须与库一起重新编译。原来用`pending_requests != 0 && !waiting`决定是否立即续跑的代码应改为读取need_process,并保留网络推进后调用总召Process的步骤。Submit_Batch现立即尝试提交,OK时进度已推进;WAIT时只保留业务事件和游标,每次重试须重新读取当前值,不能继续复用旧构造结果。Reply_Begin和Complete_Data仍由Process发送后续确认或终止。
请求存储精简只改变内部表示,总召公开函数、业务事件及配置字段不变。Slave新增按身份发送、尝试发送和关闭原连接的接口;总召内部使用,调用方继续按原方式驱动总召即可。完整SlaveEvent及Copy_Event保留原语义,其他业务无需迁移。
确认前失败修复不新增接口、配置字段或枚举值。调用方收到BEGIN阶段Fail_Request的OK或超时后的STALE时,不应再假定TCP已经断开,须继续处理FINISHED并驱动Process发送否定确认。
早期单站广播支持新增`IEC60870_Asdu_Set_CA`,当时未改变总召配置及事件布局。当前多CA版本新增use_station_registry与request.station_id,所有调用方须重新编译。新应用启用站点表,按事件实际CA构造数据,不把65535填入数据ASDU;BEGIN获取引用,FINISHED归还。Remove的OK已改为立即退出路由,释放内存须另查Is_Reusable,原先“Remove成功即可释放”的调用代码必须迁移。
广播发送顺序与[c104 Server.cpp](https://github.com/Fraunhofer-FIT-DIEN/iec104-python/blob/main/src/Server.cpp)一致:全部站确认、逐站数据、全部站终止。c104使用shared_ptr;这里用调用方节点、显式引用及轻量状态区,并保留可重试WAIT、业务事件和各站明确收尾。仍只允许一轮总召,不引入每站独立发送任务或动态发送队列。新增broadcast_states/broadcast_capacity及FINISHING,调用方须重新编译;启用多目标广播前配置状态区,并适配多个BEGIN先于首个FINISHED的事件顺序。
地址处理重构新增语义常量`IEC104_CA_BROADCAST`,将地址准备集中到内部副本创建,并移除任务中重复的header。公开函数签名和事件字段保持原样,应用无需增加调用。59组可控网络及真实TCP回归继续检查确认/数据/终止WAIT、错误地址、原事件不变、广播/本站交叉撤销和确认前失败,避免内部重构改变协议行为。
测试验证本模块的功能和边界,不代表已通过正式协议一致性认证。现场仍须确定主站对否定确认的处理、各分组点表、采样一致性及业务期限取值。
@@ -0,0 +1,136 @@
# 104子站示例
[iec104_slave_example.c](../../examples/iec104_slave_example.c) 包含模拟点表、业务事件处理、其他命令兜底、事件循环及main。总召状态机使用独立库模块[IEC104_Interrogation.c/h](../../104/IEC104_Interrogation.h),不在示例内重复实现。
当前实现站总召(`C_IC_NA_1`,QOI=20),返回8个单点遥信和8个标度化遥测值;分组1(QOI=21)只返回遥信,分组2(QOI=22)只返回遥测。库支持全部16组,示例未配置的其余组由应用拒绝。通信通过 `IEC104_Slave`,总召通过 `IEC104_Interrogation` 接入,所有任务使用固定存储、在主循环线程串行推进。模拟点表是只读的;变化上报、实际设备取值、校时及遥控可在这个入口继续接入。
ExampleApp持有一个零初始化的Station节点。Example_Init按`--ca`注册CA及应用context,再以`config.use_station_registry=true`创建总召。命令行仍只注册一个站,状态区参数传NULL/0;扩展多CA时须提供broadcast_states/broadcast_capacity,广播先确认所有站,再逐站数据,最后终止所有站。BEGIN只判断接受;首个NEED_DATA才按station_id查原节点并Acquire,建立唯一取数游标。Complete_Data返回OK后释放游标及引用,异常时按FINISHED清理。处理中删除/同CA重建不改变旧数据源。节点须等Is_Reusable为true后释放,见[共用站点表](../104/IEC104_API接口汇总.md#station-registry)。
例如最多一轮覆盖13站时,由应用提供以下存储并在Create前写入配置(或传给示例内部Example_Init的后两个参数):
```c
static IEC104_InterrogationStationState_t gi_stations[13];
/* config已经通过IEC104_Interrogation_Config_Default初始化。 */
config.use_station_registry = true;
config.broadcast_states = gi_stations;
config.broadcast_capacity = sizeof(gi_stations) / sizeof(gi_stations[0]);
```
数组借用至Destroy,期间不可读写或重叠使用;它保存身份、进度和终结状态,不保存点值。注册表不限制为13站,但若本轮目标数超过状态区容量,会在任何肯定确认前整轮否定。单目标不需要该状态区。
## 构建与运行
在项目根目录的 PowerShell 中执行,需要 MinGW GCC:
示例通过 `IEC104_Interrogation.h` 引入库声明;构建脚本同时添加 `104/` 和 `common/` 头文件搜索路径,编辑器也应配置这两个目录。构建可执行程序仍使用下面的脚本,它会链接所需库源文件及 Windows 网络库;只编译当前 C 文件会缺少库函数实现。编辑器的 C 语言标准应与构建保持一致,使用 C11,并选择实际的 MinGW GCC 编译器。
```powershell
./scripts/build.ps1
./.build/slave-example/iec104_slave_example.exe
```
默认监听 `127.0.0.1:2404`,公共地址 CA=1;按 Ctrl+C 退出并关闭服务。也可以指定监听地址、端口和公共地址:
```powershell
./.build/slave-example/iec104_slave_example.exe --bind 0.0.0.0 --port 2404 --ca 1
```
| 参数 | 含义 |
|---|---|
| `--bind IPv4` | 数字IPv4监听地址,默认127.0.0.1 |
| `--port 0..65535` | 默认2404;0由系统分配,启动时输出实际端口 |
| `--ca 1..65534` | 本站公共地址,默认1;总召请求也接受65535,广播请求的应答使用本站地址 |
| `--window 1..32767` | 发送窗口k,默认12;设为1可观察逐帧ACK后续发 |
| `--run-ms N` | 运行约N毫秒后正常关闭;默认0表示持续运行 |
| `--help` | 打印用法后退出 |
启动输出 `LISTEN 地址:端口 CA=站地址`。退出输出已提交ASDU数、已提交完毕的总召数、WAIT次数、拒绝数、取消任务数、积压超限次数、主站撤销数及业务超时数。`submitted` 和 `gi_submitted` 表示报文已交给发送模块,不表示主站已经ACK。
## 主站操作和模拟点表
主站先建立TCP并发送 `STARTDT_ACT`,随后发送总召:Type=100、SQ=0、对象数=1、COT=6、P/N=0、CA匹配、IOA=0,QOI=20选择全站,21/22选择本例的两个分组。TEST请求同样只读取模拟点表,应答保留TEST和OA。
| IOA | 类型 | 模拟值(按地址顺序) |
|---|---|---|
| 1001..1008 | 单点信息 `M_SP_NA_1`,Type=1 | 0、1、0、1、1、0、1、0 |
| 2001..2008 | 标度化测量值 `M_ME_NB_1`,Type=11 | 2300、2310、2290、5000、-125、0、1234、42 |
所有品质为0。遥测值为原始有符号16位标度值,本示例没有定义物理单位和倍率。
一次成功站总召依次提交6个ASDU:
1. Type=100,COT=7,肯定激活确认。
2. 两帧Type=1、COT=20的单点数据,每帧4个对象。
3. 两帧Type=11、COT=20的标度化遥测,每帧4个对象。
4. Type=100,COT=10,激活终止。
分组总召各提交4个ASDU:ACT_CON、两批对应类型数据、ACT_TERM。分组1数据为Type=1/COT=21,分组2数据为Type=11/COT=22;确认和终止保留请求QOI。BEGIN校验分组,首个NEED_DATA按该组初始化应用游标,不把模拟点表写进库。
全部应答使用原请求的 `connection`。确认、终止及短应答走普通HIGH路径,总召数据由Submit_Batch尝试不排队提交;有未提交HIGH/NORMAL、当前帧或窗口背压时返回WAIT。数据不使用 `Enqueue_Event`,只有本批接手后才推进游标及后续终止。
主站撤销使用相同的实际站CA/OA/TEST/QOI,COT改为8、P/N=0。激活和撤销均接受本站地址或65535,匹配时65535映射为本站地址,允许广播激活配本站撤销及本站激活配广播撤销。尚有匹配总召时模块停止任务、回复肯定COT=9,并通过FINISHED(DEACTIVATED)通知应用;不匹配或已完成则否定COT=9。已提交发送层的帧可能仍在确认前发出,撤销后不再提交旧批次和ACT_TERM。示例点数很少,总召可能在主站发撤销前已全部提交;观察中途撤销和各阶段拥塞可使用下面的可控网络测试。
广播总召的BEGIN/NEED_DATA/FINISHED携带本站CA,示例直接用该字段构造数据。确认、数据、终止、否定及错误应答的CA都为本站地址,OA/TEST/QOI保留原请求值;原始Slave事件不被修改。该支持属于Type100总召处理器,其他类型仍按各自业务规则分派。
## 请求校验及任务处理
| 情况 | 本示例的处理 |
|---|---|
| 不支持的Type ID | 回显原文,COT=44、P/N=1 |
| 总召COT不是6或8 | COT=45、P/N=1 |
| 总召CA既不是本站地址也不是65535 | COT=46、P/N=1,保留原错误CA |
| 总召IOA不是0 | COT=47、P/N=1 |
| 激活QOI超出20..36、请求P/N非法或当前已有未终结总召 | 否定ACT_CON,COT=7、P/N=1 |
| 空闲时收到合法激活请求 | 有空槽则建立唯一总召;BEGIN后应用接受才发肯定确认,忙时不同QOI也拒绝 |
| 激活QOI为23..36 | 合法请求交给应用,示例因未配置点表而Reply_Begin(false),回否定ACT_CON |
| 撤销不匹配、QOI超范围或P/N非法 | 否定DEACT_CON,COT=9、P/N=1 |
| 匹配的主站撤销 | 取消对应总召,肯定DEACT_CON,COT=9、P/N=0 |
| 总召唯一对象完整,但SQ=1或有多余尾字节 | 回否定7/9,COT/CA/IOA错误优先45/46/47;不执行激活或撤销 |
| 总召零对象/多对象等无法可靠应答的布局错误 | 计入rejected并丢弃;保留连接和已有总召 |
| 需要新增总召且4个任务槽已占满 | 尝试否定ACT_CON;无法安全提交/保存则丢新请求并计数,保留连接与原任务;匹配撤销复用原槽 |
| 接受/取数/整体业务期限到点 | 肯定ACT_CON提交前回否定确认并保留连接,提交后关闭原连接;已取出BEGIN才交付对应TIMEOUT终结事件 |
Example_Handle_Event先过滤asdu_result失败的I帧视图,避免进入业务分派或未知类型兜底;正常线上基础头/载荷截短已由Receiver和连接层处理。非总召命令先留给其他业务处理器;本示例最终回UNKNOWN_TYPE,单独保存一个待发送请求,若该槽占用时再收到未知命令则关闭原连接。
校验从类型开始;已知总召先检查是否有唯一完整对象,再按COT、CA、IOA、布局/QOI/P/N及事务状态决定。畸形单对象的否定副本规范为10字节、VSQ=1,保留OA/TEST/IOA/QOI,不修改原输入。广播应答使用实际站CA。错误请求不取消原总召或刷新期限;否定应答沿现有队列重试,满槽只丢新请求。这里只实现上述命令集合,不代表所有类型的业务规则。
`Example_Drain_Business`主动读取总召业务事件:BEGIN按QOI选点表,Reply_Begin接受或拒绝请求;NEED_DATA调用普通辅助函数Example_Build_Batch构造数据,再Submit_Batch,数据耗尽则Complete_Data;FINISHED按实例/请求编号清理对应资源,撤销和超时也走该分支。没有向库注册业务函数指针。数据源暂未就绪时可保存事件,后续就绪再应答;发送WAIT也保留事件,下一次调用重新组包,模块不缓存本批原文或重复生成NEED_DATA。
本例BEGIN只判断接受,首个NEED_DATA初始化游标并读取这一批;接真实数据源时,在Example_Build_Batch的逐点组包处读取当前值和品质。IOA、类型、分组成员及遍历策略均由调用者决定,协议库不登记或冻结完整IOA列表。示例常量表只用于展示业务接入,不是库规定的地址范围。逐批采样不保证整轮同一时刻,也不自动解决与突发上报的新旧排序;完整边界见[总召模块设计](../104/IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。
Example_Build_Batch使用局部游标遍历,仅在Submit_Batch返回OK后推进应用游标,此时发送层已复制原文并确定帧顺序,输入存储可复用。返回WAIT时保留同一业务事件及游标,先推进网络、读空Slave通知,再从同一位置重新取当前值和品质,不能重发旧构造结果。STALE表示原交互已失效(包括取消、撤销、超时及提交故障),丢弃旧应答并处理FINISHED。接实际数据源时,须将点值更新及突发发布与总召读值及提交串行化;排序保证及持续突发可能推迟总召的边界见[数据批次与发送路径](../104/IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。
`IEC104_Interrogation_Process(app->interrogation)`自动推进当前就绪任务,无需预算参数。Handle_Event在原会话有效、Slave通知已读空且没有更早REPLY时立即尝试提交短应答;未接手再保存,无法保存则计入dropped_requests。Process继续处理保存的应答及串行总召;任务为空、全部等待应用/事件空位、发送WAIT或Slave有未读通知时返回。Example_Step随后消费业务事件,下一轮继续推进;不在Process内部执行应用代码。
Example_Wait_Time直接读取总召状态的`need_process`,不再用`pending_requests && !waiting`推断。业务应答后或旧FINISHED出队释放空位时,下轮Poll使用0等待;发送WAIT后允许等待网络。每次Poll并读空Slave通知后,Example_Step仍调用总召Process重试,不能用need_process为false跳过;未读Slave通知始终优先处理。
Example_Init使用默认业务期限:BEGIN应答15秒、每批NEED_DATA至首次有效批次尝试15秒、执行整体期限0(禁用);对应begin_timeout_ms/data_timeout_ms/total_timeout_ms,各项0表示禁用。有效批次因发送背压WAIT后,本批取数计时停止,下一批NEED_DATA才重新计时;应用仍须重读并重试。BEGIN生成前不计业务期限;总期限显式设为非零时,从BEGIN生成起计算完整执行耗时,发送等待计入且不因新批次而延长。START/CONFIRM超时或BEGIN阶段Fail_Request转为否定ACT_CON并保留TCP及已有短应答;肯定ACT_CON提交后发生取数超时、总超时或NEED_DATA失败,仍关闭原连接。失败通知保留具体原因,须继续读取FINISHED并驱动Process,不能根据BEGIN失败假定已断链。业务FIFO被旧FINISHED占用时要及时读取,才能启动当前请求,期间仍拒绝新的总召。Example_Wait_Time在每次Poll前重新查询最近期限,将50ms等待上限进一步缩短。期限使用单调时钟,不代替链路t0..t3及单帧发送期限,也不等待主站ACK;默认关闭整体期限不影响这些底层保护。退出统计包含negative_submitted和dropped_requests;丢弃请求没有响应,主站可能超时或重试。
STOPDT或断线通知取消未提交任务。发送内部受理STOP而返回ERROR时,总召确认原连接正常停用后先让出,待通知终结为STOPPED;其他ERROR/FATAL仍失败。Submit_Batch为等待STOP通知而返回WAIT时不停止取数计时,应用继续按FIFO交付通知,旧交互随后STALE。事件消费预算耗尽时先续读通知,再推进业务。关闭时执行`Interrogation_Cancel → 读空业务事件并清理FINISHED/Release → Slave_Close → 读空Slave通知 → Interrogation_Destroy → Slave_Remove_Station → Slave_Destroy`。总召Destroy遇到活动任务或未读业务事件会拒绝释放;应用引用须持有到FINISHED清理,不能用Remove成功代替资源清理。
## 验证
```powershell
./scripts/build.ps1 -Test
```
`-Test` 另外需要 Node.js,只使用其内置模块,无需安装依赖。它依次运行23组Slave/站点测试(单服务及双服务配置)、97组总召可控网络测试和真实本机TCP测试:
- `iec104_slave_test.c`:站点注册/移除、延迟回收、重复CA、13站共享服务、CA/context隔离、注册编号防替换、快照增删及嵌套集合锁、关闭重启保留,以及原有应答、事件身份和背压回归。
- `iec104_slave_example_test.c`:完整报文、部分写入、各发送阶段HIGH满与WAIT、请求校验、STOP/重连、非法请求、传输通知预算及API生命周期;业务事件覆盖延迟应答、批次校验、混合类型、容量边界、拒绝/失败/取消、终结预留和实例复用。总召覆盖全部16组、各阶段撤销、三项期限、迟到应答及通知优先。调度覆盖短应答立即提交及顺序保护、非队首槽复用、满槽否定或计数丢弃且当前任务有效、各阶段相同/不同QOI均忙拒绝、结束后重发成功、撤销对象匹配、停止清理、BEGIN前等事件空位及BEGIN后完整执行期限,并验证忙状态不拦截Type101总电度请求。测试设置HIGH=251字节、业务事件FIFO=2槽,使用真实容量背压限制发送。
- `slave_example_loopback_test.cjs`:启动实际可执行程序,使用Windows网络后端和127.0.0.1随机端口,以独立主站字节验证分片请求、k=1时逐帧ACK、站总召、多个不同QOI连续请求的忙拒绝及结束后重发成功、主站撤销与重复撤销、错误应答、STOP/START、重连和正常退出。服务约4秒后自行关闭。
畸形请求回归覆盖SQ/尾字节否定、零/多对象丢弃、基础头及载荷截短仍由Receiver拒绝、原事件不变、错误原因优先级、畸形激活/撤销不影响原BEGIN/NEED_DATA及期限、背压满槽后的FIFO恢复和多CA广播撤销隔离。
确认前失败另覆盖BEGIN尚未取出的超时、满任务池及HIGH等待时复用原槽、QOI/OA/TEST保持且应答使用本站CA、旧应答FIFO顺序、同连接重新总召、STOP/撤销/断线优先。确认已被发送层接手但尚未写出时,取数失败及超时仍断链,避免将已接受请求改发否定确认。
need_process回归覆盖完整业务交互、事件空位释放、总召等待应用时的短应答、CONFIRM/数据/终止/短应答的发送WAIT及恢复、到期立即续跑和未读STOP/START优先,并验证只读查询不提交帧或重复累计waits。
提交期通知回归覆盖真实启停容量背压留下STOP、三个发送入口的STOPPED归因、FATAL与STOP并存时仍为SEND_ERROR、非STOP的ERROR、OK/WAIT顺带交付RECORD时暂停后续提交,以及START后重新总召。
不排队提交回归覆盖旧NORMAL先于新总召、WAIT期间新突发先发后重新取值、HIGH积压、已接手总召部分写入后突发不能越过、背压不计取数超时而下一批重新计时、k=1窗口释放、到期ACK优先处理及发送故障终结。WAIT时验证队列和批次进度不增加,OK后覆盖输入存储验证发送层持有独立副本。
广播回归覆盖QOI=20..36、13个CA全部确认/数据/终止顺序、非空混合批次、状态区不足及跨实例重叠、已确认站删除/同CA替换、三阶段WAIT、超出FIFO容量的FINISHED、各阶段定向撤销及满槽广播撤销、拒绝与撤销同时等待、非当前站总期限和STOP/断线清理。Submit_Batch拒绝广播CA及非当前站CA;Type101继续交后续处理器。真实TCP用命令行注册的单站验证广播全站/分组、忙拒绝、非法QOI和撤销组合,共核对62个ASDU及11次正常完成的总召;多CA由可控Net回归覆盖。
这些检查覆盖本示例的模拟点表流程;实际主站设备、现场点表和设备执行接口仍需联调。
@@ -0,0 +1,249 @@
# IEC104 测试说明
> **目录与命令同步日期**:2026-10-27。测试代码位于根目录的 `test/`,本文仅列出当前保留的测试入口及可运行命令。
> **配套文档**:[TCP服务设计](../104/IEC104_TcpServer_设计架构.md)、[链路设计](../104/IEC104_Link_设计架构.md)、[发送模块设计](../104/IEC104_发送模块_设计架构.md)。
## 1. 当前测试入口
`iec60870_asdu_test.c`包含10组信息对象编解码测试,使用独立预期报文字节覆盖12种监视类型(各SQ=0/1)、18种命令编解码和12种COT/CA/IOA宽度组合。它不创建socket、不读取或设置系统时钟。覆盖可配置容量、249/254字节边界、127对象限制、失败原子性、地址连续性与溢出、品质和CP56校验、应答原文保留、重叠复制及公共地址修改。
`iec60870_asdu_extended_test.c`新增7组测试,以独立预期字节覆盖其余37种类型的696组布局。两套ASDU测试合计覆盖67种类型、1200组COT/CA/IOA及SQ布局。新增测试检查CP16完整数值范围、CP24边界/保留位、步位置负数、QDP、QPM、变长文件段0/255字节及配置容量、249/254总长、对象复制独立性、截短/多余字节、SQ/数量约束和失败原子性。
`iec104_tcp_event_ring_test.c`包含13个TCP事件字节环场景。它使用可控Net后端,不创建真实socket、不执行真实等待;通过直接包含`IEC104_TcpServer.c`检查内部容量、跨环尾布局和损坏数据的出队行为。
`iec104_tcp_scheduler_test.c`复用上述可控Net后端,包含25个调度场景;其中2个明确验证原有严格容量背压策略仍会超时,不能把测试通过解释为消除了队头阻塞。`iec104_net_pending_input_test.c`使用当前Windows后端进行真实本机回环检查。
`iec104_slave_test.c`复用可控Net后端,共23组统一入口测试,覆盖ASDU视图、共用发送分派、确认/终止字节、HIGH背压、非法参数、原连接身份、延迟复制、服务复用、生命周期、外部调度及离线上报。站点测试覆盖13节点、CA/context隔离、删除即时退出路由与引用延迟回收、同CA替代节点的注册编号、快照期间增删/嵌套/结束回收、显式集合锁及跨服务误用。共用层非104参数测试不代表已提供101串口传输。
因接口过期而无法编译的旧协议、发送管理器、TCP模拟后端和Windows回环测试及其专用辅助文件已删除。这里新增的测试均使用当前接口,历史测试未恢复。
`iec104_slave_example_test.c`共97组,覆盖总召报文、部分写入、各阶段WAIT、忙拒绝、撤销、STOP/断线、need_process、突发排序、不排队提交及业务/链路期限。提交期通知覆盖三个入口的STOPPED归因、FATAL与STOP并存、非STOP的ERROR、OK/WAIT伴随新回执及START后恢复。畸形请求覆盖可靠否定/丢弃边界、基础截短仍被Receiver拒绝、输入原文保持、活动总召/期限不变、背压满槽及广播撤销隔离。多站覆盖13站三阶段顺序、非空混合批次、状态区不足/重叠、已确认未来站删除与同CA重建、超出2槽FIFO的FINISHED交付、各阶段定向及满槽广播撤销、拒绝与撤销同时等待、非当前站总期限。`test/slave_example_loopback_test.cjs`通过Windows后端与真实TCP验证命令行单站,多CA由可控Net回归覆盖。构建及点表见[示例说明](../examples/IEC104子站示例.md)。
## 2. 编译与运行
在项目根目录的PowerShell中执行,要求GCC可用。下述TCP事件环和调度测试已包含TcpServer实现且自行实现Net接口,构建时不要再次链接`IEC104_TcpServer.c`或`IEC104_Net_Windows.c`。
```powershell
Get-Command gcc -ErrorAction Stop | Out-Null
New-Item -ItemType Directory -Path .build -Force | Out-Null
$testSources = @(
'test/iec104_tcp_event_ring_test.c',
'104/IEC104_Connection.c',
'104/IEC104_Receiver.c',
'104/IEC104_SendManager.c',
'104/IEC104_SendBuffer.c',
'104/IEC104_AsduQueue.c',
'104/IEC104_Codec.c',
'common/IEC60870_Codec.c',
'104/IEC104_Link.c',
'104/IEC104_Link_Slave_StartStop.c'
)
gcc -std=c11 -O2 -I104 -Icommon -Wall -Wextra -Werror @testSources -o .build/iec104_tcp_event_ring_test.exe
if ($LASTEXITCODE -ne 0) { throw 'TCP event ring build failed' }
./.build/iec104_tcp_event_ring_test.exe
if ($LASTEXITCODE -ne 0) { throw 'TCP event ring tests failed' }
```
成功时末行输出`TCP event ring: 13 cases passed`,进程退出码为0。任何断言失败都会输出函数名、行号和条件并以非零状态退出。编译失败后立即停止,不能继续运行旧二进制。
沿用上述`$testSources`编译调度测试;它已经包含字节环测试源文件,不能重复链接:
```powershell
$schedulerSources = @('test/iec104_tcp_scheduler_test.c') + $testSources[1..($testSources.Length - 1)]
gcc -std=c11 -O2 -I104 -Icommon -Wall -Wextra -Werror @schedulerSources -o .build/iec104_tcp_scheduler_test.exe
if ($LASTEXITCODE -ne 0) { throw 'TCP scheduler build failed' }
./.build/iec104_tcp_scheduler_test.exe
if ($LASTEXITCODE -ne 0) { throw 'TCP scheduler tests failed' }
gcc -std=c11 -O2 -I104 -Icommon -Wall -Wextra -Werror test/iec104_net_pending_input_test.c 104/IEC104_Net_Windows.c -lws2_32 -o .build/iec104_net_pending_input_test.exe
if ($LASTEXITCODE -ne 0) { throw 'Net loopback build failed' }
./.build/iec104_net_pending_input_test.exe
if ($LASTEXITCODE -ne 0) { throw 'Net loopback test failed' }
```
分别输出`TCP scheduler: 25 cases passed (including strict backpressure policy checks)`和`Windows Net pending input: loopback passed`。
两套公共ASDU测试仅链接 `IEC60870_*.c`,不包含104头文件,不依赖104 Codec、TCP、Net或Clock。以下分别使用默认Windows时间实现和纯整数实现:
```powershell
New-Item -ItemType Directory -Path .build -Force | Out-Null
$asduSources = @('test/iec60870_asdu_test.c', 'common/IEC60870_Asdu.c', 'common/IEC60870_InformationObjects.c', 'common/IEC60870_Codec.c', 'common/IEC60870_Time.c')
gcc -std=c11 -O2 -Icommon -Wall -Wextra -Werror @asduSources -o .build/iec60870_asdu_test.exe
if ($LASTEXITCODE -ne 0) { throw 'ASDU build failed' }
./.build/iec60870_asdu_test.exe
if ($LASTEXITCODE -ne 0) { throw 'ASDU tests failed' }
gcc -std=c11 -O2 -Icommon -Wall -Wextra -Werror -DIEC60870_TIME_USE_PLATFORM=0 @asduSources -o .build/iec60870_asdu_integer_time_test.exe
if ($LASTEXITCODE -ne 0) { throw 'ASDU integer-time build failed' }
./.build/iec60870_asdu_integer_time_test.exe
if ($LASTEXITCODE -ne 0) { throw 'ASDU integer-time tests failed' }
```
两次均应输出`ASDU: 10 groups passed (30 concrete types, 504 wire layouts, 12 parameter configurations)`。源码中的完整字节向量不通过待测编码器生成;另外检查未对齐输入、浮点位模式(含NaN/Inf)及有符号累计量边界。对每种监视/命令向量检查截短和多余尾字节;命令另检查非法SQ/数量、SCO保留位、DCO/RCO无效状态和时标错误。测试允许业务自行判断未知COT、地址及限定词,并验证原始字段不会被解析器丢失。
新增类型测试使用相同生产依赖,可分别验证默认时间后端和纯整数后端:
```powershell
$extendedSources = @('test/iec60870_asdu_extended_test.c', 'common/IEC60870_Asdu.c', 'common/IEC60870_InformationObjects.c', 'common/IEC60870_Codec.c', 'common/IEC60870_Time.c')
gcc -std=c11 -O2 -Icommon -Wall -Wextra -Werror @extendedSources -o .build/iec60870_asdu_extended_test.exe
if ($LASTEXITCODE -ne 0) { throw 'Extended ASDU build failed' }
./.build/iec60870_asdu_extended_test.exe
if ($LASTEXITCODE -ne 0) { throw 'Extended ASDU tests failed' }
gcc -std=c11 -O2 -Icommon -Wall -Wextra -Werror -DIEC60870_TIME_USE_PLATFORM=0 @extendedSources -o .build/iec60870_asdu_extended_integer_test.exe
if ($LASTEXITCODE -ne 0) { throw 'Extended integer-time build failed' }
./.build/iec60870_asdu_extended_integer_test.exe
if ($LASTEXITCODE -ne 0) { throw 'Extended integer-time tests failed' }
```
输出`Extended ASDU: 696 independent wire layouts verified`及`Extended ASDU: 7 groups passed (37 additional types, short times, variable file segments)`。这里验证无状态编解码,未模拟文件传输业务或真实设备互操作。
104封装检查独立放在 `iec104_asdu_transport_test.c`:复用公共套件的独立载荷向量,验证30种类型的42种类型/SQ组合、I帧序号与完整字节、249字节可发送及250/254字节拒绝。公共套件的测试入口仅用于复用夹具,不会在此重复运行。
```powershell
$transportSources = @('test/iec104_asdu_transport_test.c', 'common/IEC60870_Asdu.c', 'common/IEC60870_InformationObjects.c', 'common/IEC60870_Codec.c', 'common/IEC60870_Time.c', '104/IEC104_Codec.c')
gcc -std=c11 -O2 -I104 -Icommon -Wall -Wextra -Werror @transportSources -o .build/iec104_asdu_transport_test.exe
if ($LASTEXITCODE -ne 0) { throw '104 ASDU transport build failed' }
./.build/iec104_asdu_transport_test.exe
if ($LASTEXITCODE -ne 0) { throw '104 ASDU transport tests failed' }
```
输出 `104 ASDU transport: 2 groups passed (42 layouts, 249/254-byte boundary)`。
共用应答接口测试同时覆盖ASDU对象与104真实提交路径,不链接真实网络后端:
```powershell
$slaveSources = @(
'test/iec104_slave_test.c', 'common/IEC60870_Slave.c', '104/IEC104_Slave.c',
'common/IEC60870_Asdu.c', 'common/IEC60870_InformationObjects.c', 'common/IEC60870_Time.c',
'104/IEC104_Connection.c', '104/IEC104_Receiver.c', '104/IEC104_SendManager.c',
'104/IEC104_SendBuffer.c', '104/IEC104_AsduQueue.c', '104/IEC104_Codec.c', 'common/IEC60870_Codec.c',
'104/IEC104_Link.c', '104/IEC104_Link_Slave_StartStop.c'
)
gcc -std=c11 -O2 -I104 -Icommon -Wall -Wextra -Werror @slaveSources -o .build/iec104_slave_test.exe
if ($LASTEXITCODE -ne 0) { throw 'Slave replies build failed' }
./.build/iec104_slave_test.exe
if ($LASTEXITCODE -ne 0) { throw 'Slave replies tests failed' }
```
输出`Slave service: 23 groups passed (replies, identities, stations, lifecycle, backpressure)`。默认及`-DIEC104_TCP_SERVER_CAPACITY=2`均运行同一套测试。确认及终止字节由独立请求向量和明确COT比较,不用待测函数生成期望。失败且未接手时不增加队列条目,旧代次不能写入新连接;FATAL注入验证结果传递,不代表穷尽所有发送故障组合。
参数矩阵遍历COT=1/2、CA=1/2、IOA=1/2/3:288组双对象监视报文布局、216组命令编解码报文;应答单独遍历全部30类型、12种配置、7种COT及两种P/N。所有布局检查截短输入及额外尾字节。边界测试检查省略OA、1字节CA溢出、三种IOA连续地址溢出、非法/NULL参数、容量与配置上限分别生效、4字节未知类型应答的重叠复制及5字节读命令。通用ASDU可编码/解析254字节报文,而104 I帧封装仍拒绝该长度。
另逐类验证具体Create/Get_From_Buffer、编码函数表身份、输出存储尺寸、首对象失败不设置类型、命令数量上限、非法SQ及修改对象后重新编码的校验。具体解析结果按值保存,覆盖输入载荷被清零后对象仍有效。
以上编解码和共用接口用例验证信息对象格式和接口契约,具体业务流程由下面的示例测试验证。两种时间配置通过同一CP56用例,不等同于全部日历转换函数的一致性覆盖。
单文件示例及其业务测试通过一个脚本构建;需要MinGW GCC,真实回环测试另需Node.js(仅使用内置模块):
```powershell
./scripts/build.ps1 -Test
```
脚本先输出两次`Slave service: 22 groups passed ...`(单/双服务),再输出`slave example + event interrogation tests: 70 groups passed`和`Slave example loopback: passed ...`。可控后端使用251字节HIGH和2槽业务事件FIFO,验证数据WAIT不接手原文、不推进进度,重读后提交;确认、终止及短应答仍保留请求重试。真实回环使用默认存储、k=1、随机本机端口,验证分片请求、逐帧ACK、错误应答、STOP/START、重连及4秒后的正常退出。这里只实现模拟点表总召,不包含实际校时、遥控业务,也不构成完整互操作认证。
## 3. 覆盖范围
| 场景 | 验证内容 |
|---|---|
| 跨环尾往返 | 每个环起点、短长I帧、RECORD和全部连接通知;头部及载荷跨界、短帧尾部清零、历史代次交付 |
| 满容量预留 | 普通区、启停区、全部收尾项同时占满;失败入队保持原数据,FIFO次序不变 |
| 容量边界 | CONNECT按实际大小判断;旧启停通知及收尾占用对新接入和启停的限制 |
| 损坏项拒绝 | 非法类型、代次、长度、游标和计数;ERROR保持输出和队列不变 |
| 短帧背压恢复 | 默认普通区缓存185条16字节APDU,下一帧暂留Receiver;消费后按序恢复 |
| I帧先于回执 | 等待空间的长I帧阻止较短RECORD提前转移 |
| 空间不足时回执延后 | 回执暂留内部,启停通知和CON仍推进 |
| 预算用尽时回执延后 | 回执可晚于随后受理的启停通知交付 |
| 收尾与重连 | 剩余I帧、全部记录、DISCONNECTED的次序,以及新连接下读取旧代次事件 |
| 启停配额背压 | 精确核对CON字节数和帧内容;未受理ACT不回CON,消费一条通知后受理并确认,剩余通知及状态快照按序保留 |
| 同状态重复ACT | 已启动时START、已停止时STOP均回CON而不通知;无未读通知及配额满时均覆盖两个方向 |
| 跨代次启停顺序 | 旧启停通知读完前不接入;读完后可保留旧断开通知接入,严格交付DISCONNECTED(旧)→CONNECTED(新)→ACTIVATED(新)及各自快照 |
| 分包与每轮预算 | 默认一次recv容纳8个满长APDU;分段输入完整交付且不重复 |
这些场景不等同于完整协议测试,也不覆盖真实网络、全部异常超时、多线程并发或ASDU业务语义。性能模拟的测量方法和局限见[TCP服务设计中的验证记录](../104/IEC104_TcpServer_设计架构.md)。
调度专项覆盖:
| 场景 | 验证内容 |
|---|---|
| ACK边界(8组) | t1边界前置7/8/9/33个I帧;1/16/17字节recv分段;满长APDU突发;按序交付且每轮I事件不超过8条 |
| 有限快照(3组) | 用户态尾部、内核分段输入及跨Feed预算的S流;持续新输入不能扩大旧快照或无限推迟缺ACK超时 |
| 真正缺ACK | 没有输入时按原t1触发I_ACK_TIMEOUT |
| 持续接收发送(2组) | 每轮8个I帧且业务读空FIFO,完整/部分send均持续推进 |
| 业务发送 | 事件预算用尽后NORMAL业务仍能实际写出 |
| 不可写(2组) | WOULD_BLOCK不忙重试;恢复可写后发送,持续不可写仍触发LOCAL_SEND_TIMEOUT |
| TESTFR(2组) | CON在事件预算后仍受理;没有CON时仍触发TESTFR_TIMEOUT |
| 真实容量背压(2组) | I帧普通区满、启停配额满仍按原策略暂停接收并检查严格期限 |
| 续收时提交应答 | 业务在轮次之间可正常Send_Response,不提前误判旧ACK超时 |
| 非法ACK | 续收不屏蔽越界ACK,仍以PROTOCOL收尾 |
| 快照查询失败 | 保留输入与事件收尾,以TRANSPORT报告网络失败 |
| EOF与应答 | 先交付I帧让业务提交应答,再读取EOF排空 |
Windows回环专项验证查询不会消费字节、部分recv后计数准确、输入耗尽与EOF均可返回零字节、非法参数不修改输出。它不代表真实网络下的协议性能或吞吐测试。
## 4. 配置覆盖
沿用上一节的`$testSources`,分别编译和运行:
```powershell
gcc -std=c11 -O2 -I104 -Icommon -Wall -Wextra -Werror -DIEC104_TCP_RECORD_CAPACITY=3 -DIEC104_TCP_EVENT_CAPACITY=12 -DIEC104_TCP_EVENT_STATE_RESERVE=2 @testSources -o .build/iec104_tcp_event_ring_small.exe
if ($LASTEXITCODE -ne 0) { throw 'Small configuration build failed' }
./.build/iec104_tcp_event_ring_small.exe
if ($LASTEXITCODE -ne 0) { throw 'Small configuration tests failed' }
gcc -std=c11 -O2 -I104 -Icommon -Wall -Wextra -Werror -DIEC104_TCP_RECORD_CAPACITY=40 -DIEC104_TCP_EVENT_CAPACITY=100 -DIEC104_TCP_EVENT_STATE_RESERVE=6 -DIEC104_TCP_RX_CAPACITY=4096 @testSources -o .build/iec104_tcp_event_ring_large.exe
if ($LASTEXITCODE -ne 0) { throw 'Large configuration build failed' }
./.build/iec104_tcp_event_ring_large.exe
if ($LASTEXITCODE -ne 0) { throw 'Large configuration tests failed' }
```
调度专项使用同样的配置参数,将命令中的`@testSources`换为`@schedulerSources`并使用独立输出文件名即可。
## 5. 验证记录
2026-10-27 提交期STOP归因:总召三个发送入口仅在ERROR时复核原连接正常停用状态,先交付DEACTIVATED,保留STOPPED原因;其他ERROR/FATAL不降级,成功路径不增加查询。目录内严格编译、97组总召、单/双服务各23组Slave及真实Windows TCP回环通过。新增真实通知容量背压复现,以及其他入口的容量/故障注入;验证等待不推进批次、不停止取数计时、不重复提交,STOP伴随FATAL仍为SEND_ERROR,OK/WAIT交付新回执后暂停后续提交。原临时复现程序已迁入正式测试,公开接口和固定存储容量不变。
2026-10-27 总召畸形请求隔离:单对象完整但SQ/尾字节错误时规范化否定应答,零/多对象丢弃;公共视图失败交公共入口,基础截短仍由Receiver处理。目录内生产示例通过严格警告编译,92组总召回归及单/双服务各23组Slave回归通过,真实Windows TCP回环通过(沙箱子进程EPERM后按原命令重跑)。覆盖畸形激活/撤销不取消已有总召、不刷新期限、不修改输入,以及FIFO背压、满槽丢弃和多CA活动广播继续完成。未增加任务槽或发送缓冲,修改及验证均限IEC60870-Slave目录。
2026-10-27 c104广播发送顺序:合法多目标广播改为全部ACT_CON、逐站数据、全部ACT_TERM,调用方提供每站状态区,仍共用一个任务。87组总召及23组Slave回归在单/双服务配置下通过C11严格警告编译及运行;生产示例构建和真实Windows TCP回环通过。新增覆盖状态区不足/重叠、非空多站报文、已确认未来站删除、13条FINISHED通过2槽FIFO无损交付、满槽及跨阶段撤销、拒绝与撤销并存、非当前站总期限。回环因沙箱子进程EPERM按原授权命令重跑。Windows x64默认配置实测管理对象1240B、每站状态48B,通信队列容量和覆盖策略不变。文档197个本地链接及锚点、UTF-8编码和diff空白检查通过;修改及验证均限本目录。
2026-10-27 多CA总召及站点删除:Remove即时退出路由,引用/快照结束后回收;总召按请求CA选站,广播以一个任务逐站推进。23组Slave及78组总召测试均在单/双服务配置下通过`-std=c11 -O2 -Wall -Wextra -Werror`编译和运行,包含13站广播、同CA替换、三阶段WAIT及撤销/超时/STOP。生产示例严格编译和真实TCP回环通过;回环子进程EPERM后以已授权原命令运行。Windows x64默认配置测量:站点40B、快照32B、Slave40B、总召1192B、任务168B;通信队列容量及事件覆盖策略未变。本次修改与验证仅在IEC60870-Slave目录内进行。
2026-10-27 共用站点基础:Slave提供调用方节点注册、按CA查找、context、引用及集合锁,总召可绑定已注册节点,示例改为注册一次CA后复用。22组Slave测试及70组总召测试均在单/双服务配置下通过`-std=c11 -O2 -Wall -Wextra -Werror`构建与运行;生产示例严格编译及真实Windows TCP回环通过。回环因沙箱子进程EPERM使用已授权原命令重跑。当前总召仍只处理一个绑定站点,未验证或宣称多CA总召调度。x64测量每节点32字节,Slave32字节,总召968字节,通信队列及业务槽容量不变。仓库`.venv`和可执行Python缺失,`@build`及RAM/栈报告入口未能启动;未执行ARMCC目标验证或硬件联调。
2026-10-27 总召请求存储精简:任务改为保存原事件身份及10字节Type100原文,发送时临时建立ASDU视图;Slave新增按身份发送/尝试发送/关闭接口,保留原服务实例与连接代次校验。任务、事件及传输队列容量和覆盖策略未调整,内存测量见[固定存储](../104/IEC104_Interrogation_设计架构.md#2-对象身份与固定存储)。68组总召回归、18组Slave回归(默认及双服务配置)和真实Windows TCP回环通过严格警告构建及运行。新增验证源事件复用、输入存储释放、HIGH接手条数不变、Try等待不入队/覆盖及旧身份隔离;原总召回归继续覆盖忙拒绝、广播错误字段、满槽撤销、超时、STOP和新旧数据排序。回环子进程经已授权的原命令运行;未执行Keil目标构建或硬件联调。
2026-10-27 总召与突发发送排序:总召数据改为不排队提交,有未提交HIGH/NORMAL、当前帧或窗口背压时返回WAIT;应用保留事件及游标,重读当前值后重试。有效批次遇发送WAIT后停止本批取数期限,下一批重新计时;示例仅在OK后推进游标。68组总召测试、13个TCP事件环场景、25个调度场景及15组Slave测试通过C11严格警告编译与运行;真实Windows TCP回环通过。回环子进程在沙箱内遇EPERM,经授权使用原命令重跑通过。新增场景覆盖新旧突发及总召顺序、部分写入、窗口和期限边界、提交故障;产物位于`.build/`,未连接实际设备。
2026-10-26 总召本地续跑:状态新增need_process,示例直接据此决定立即续跑;实时检查未读Slave通知,发送WAIT后让出,网络推进后继续重试。62组可控网络测试通过C11严格警告编译与运行,覆盖业务应答、旧FINISHED出队释放空位、短应答、四类发送背压、到期及STOP/START优先;真实Windows TCP回环通过。回环在沙箱内启动子进程遇到EPERM,经授权使用原测试命令重跑通过。产物位于`.build/slave-example/`,未连接实际设备。
2026-10-26 目录整理:104协议、公共模块、示例、测试、文档和构建脚本分别归入 `104/`、`common/`、`examples/`、`test/`、`docs/` 和 `scripts/`,同步头文件搜索路径、示例引用及文档命令。49个生产源码/头文件的SHA256与迁移前一致,21个生产C文件通过C11严格警告检查。按本文命令运行的默认/纯整数时间ASDU测试(各10组)、扩展ASDU测试(各7组)、104封装2组、子站15组、TCP事件环默认/小容量/大容量各13组、调度25组及Windows输入查询回环均通过;从非项目根目录调用构建脚本,示例编译及59组总召测试通过。真实TCP回环核对62个ASDU和11次正常完成的总召通过。当前执行环境拒绝Node默认子进程管道(EPERM);本次回环以临时文件接收真实子进程输出,原测试代码、网络交互和断言未修改。迁移清单、路径校验及临时运行适配保存在 `.build/layout-migration/`,未连接实际设备。
2026-10-26 广播地址处理重构:新增固定协议常量IEC104_CA_BROADCAST,Type100入口一次确定应答CA,统一在内部请求副本建立时写入;Process及发送重试直接沿用该地址。移除重复任务header,批次COT/OA/TEST从info校验,CA继续按本站配置校验。原59组可控网络测试通过C11严格警告编译与运行,覆盖广播及定向总召、错误CA回显、发送WAIT、忙拒绝、撤销及确认前失败。真实Windows TCP回环继续通过,核对62个ASDU和11次正常完成的总召。修改前备份与可控测试产物位于`.build/interrogation-address-copy-20261026-031956/`;公开函数签名、事件及配置字段不变。
2026-10-26 总召广播公共地址:Type100激活和撤销接受65535,业务事件、确认、数据、终止及广播错误应答使用本站CA;其他错误CA仍原值回显,输入Slave事件不变。新增共用Asdu_Set_CA,1/2字节CA及COT组合、空/非空构造容器、借用原文、越界及无效容器不写入均通过测试。ASDU共10组,总召可控网络共59组,均通过C11严格警告编译与运行;总召覆盖QOI=20..36、发送WAIT、满槽忙拒绝、确认前失败、四种激活/撤销地址组合、非法数据CA和广播Type101隔离。真实Windows TCP回环逐帧核对62个ASDU和11次正常完成的总召,通过广播全站/分组、忙拒绝与重试、撤销、错误应答、STOP/START及重连。API文档359个声明与源码一致,声明块通过C11/C++17严格检查,API及流程链接和总召设计格式检查通过。备份与可控测试产物位于`.build/interrogation-broadcast-20261026-000144/`,实际服务产物位于`.build/slave-example/`。未执行真实设备联调。
2026-10-23 单文件子站示例:新增`examples/iec104_slave_example.c`,模拟点表、请求分派、总召状态机和main集中在一个文件;使用现有Slave接口,不修改协议库实现。示例及9组业务测试通过C11严格警告编译,可控后端验证逐阶段WAIT、部分发送、拒绝及任务取消;独立Node主站通过真实Windows本机TCP验证分片输入、k=1、3次完整总召、错误应答、STOP/START、重连及正常关闭。未连接实际主站设备,未设置系统时间。产物位于`.build/slave-example/`。
2026-10-23 子站统一入口:IEC104_Slave内部持有静态池TCP服务,NextEvent自动交付ASDU及原连接发送接口,Copy_Event复制延迟任务并重建自引用。删除手动SlaveConnection_Init接口,生命周期、主动上报、查询和关闭统一由Slave提供;TCP共享值类型迁至IEC104_TcpTypes.h,通信状态机及发送调度代码保持不变。默认及双服务容量各通过13组Slave回归,TCP事件环13个场景、调度25个场景通过;20个生产源文件通过C11严格警告检查,公开入口示例通过C11/C++11检查,依赖清单确认Slave业务头不引入TcpServer操作接口或内部对象头。Slave.c通过实际`-fanalyzer -c`分析。源码及文档修改前备份、测试产物位于`.build/slave-service-20261023/`。未执行真实设备联调。
2026-10-22 公共应用层拆分:信息对象、ASDU、参数和公共头解析统一为IEC60870命名,移除原信息对象/ASDU文件与符号,更新104接入及文档构建清单。两套公共ASDU测试仅链接IEC60870源文件,在默认及纯整数时间配置下各通过9组/504布局和7组/696布局;104封装独立通过2组/42布局及249/254字节边界。共用应答8组、TCP事件环13个场景、调度25个场景通过;20个生产源文件通过严格警告语法检查,公共及104接入头通过C++11严格检查。编译器依赖清单确认公共层不包含104头文件;源码对照确认对象与容器算法仅迁移名称和包含路径,接口汇总保持323个模块接口及7个内部协作函数。未实现101串口链路,未进行真实设备联调。
2026-10-22 常规类型补齐:新增37种对象及CP16/CP24工具,完整支持67种类型;ASDU接入变长文件段和独立SQ/数量约束。原ASDU套件9组、504组布局通过;新增套件在默认及纯整数时间配置下各通过7组、696组布局。共用应答8组、TCP事件环13个场景、调度25个场景通过。19个生产源文件通过严格警告语法检查;新公共头通过C++11检查;Asdu、InformationObjects和Time通过实际`-fanalyzer -c`分析。代码及文档沿用原有中文注释格式,未执行真实设备联调。
2026-10-21 对象分层:新增30种具体对象,统一ASDU创建/添加/解析/取对象,移除旧联合体与分类接口。默认及纯整数时间配置各通过9组测试(504种完整报文布局);17个生产源文件通过严格警告语法检查,Asdu与InformationObjects通过 `-O2 -fanalyzer -c -Wall -Wextra -Werror`。TCP事件环13个场景、调度25个场景重新编译运行通过。未执行真实设备联调。
2026-10-21 ASDU参数化:所有相关入口直接改为显式传入应用层参数,不保留旧签名。默认及`IEC60870_TIME_USE_PLATFORM=0`各通过12组测试,编译选项为`-std=c11 -O2 -Wall -Wextra -Werror`;`IEC60870_Asdu.c`和`IEC104_Codec.c`通过`-fanalyzer -c`严格警告分析。现有默认配置TCP事件环13个场景及调度25个场景重新编译运行通过。未执行真实设备联调或系统设时。
2026-10-20启停通知测试补强:默认、小容量及大容量配置各通过13个字节环场景,默认配置通过25个调度场景,均使用`-std=c11 -O2 -Wall -Wextra -Werror`构建。可执行文件及修改前字节环测试源码保存在`.build/state-events-coverage-20261020/`。
补充单槽边界(`IEC104_TCP_EVENT_STATE_RESERVE=1`):本次加强的启停背压及新增的重复ACT、跨代次顺序共3个场景单独运行通过;完整字节环套件在原有`test_delayed_receipt`的回执计数断言处失败,修改前源码也在同一位置失败。单槽配置未计入整套通过结果。
2026-10-20调度修复:默认、小容量及大容量配置各通过11个字节环场景和25个调度场景;Windows输入快照真实回环及生产库严格警告构建通过。默认服务对象为21472字节,比上一版增加8字节;事件环及RX容量不变。验证输出及修改前备份在`.build/scheduler-fix-20261020/`。此前每帧增加0.02–0.03 μs的结果仅对应存储改造,不涵盖本次新增查询与调度行为。
使用`-fanalyzer -c`实际编译时,TcpServer和Connection通过。Windows后端严格分析仍报告Listen/Accept处的3条文件描述符诊断(第142、151、190行);修改前备份产生相同诊断,均不在新增输入查询函数中。未修改既有socket代码或屏蔽告警;完整对照保存在`net_analysis_before.log`和`net_analysis_after.log`。因此不将Windows后端描述为静态分析全通过。
2026-10-19字节环改造验证:GCC 16.1.0(MinGW-W64 x64),默认、小容量及大容量配置各通过11个场景。Windows生产库严格警告构建及GCC静态分析通过。源码哈希、完整命令和测量输出位于`.build/fifo-byte-ring-20261019/`。
历史测试及构建记录仍保存在`.build`中,仅描述当时的源码版本,不属于当前可运行测试。本文不再提供已删除测试的构建命令,也不将其历史通过数计入当前结果。
@@ -0,0 +1,58 @@
/*
* 104计数量种类规则:解析C_CI_NA_1的QCC,限定RQT和读取数据类型,选择完成方式。
* 规则函数不保存状态、不访问网络;计数器存储及冻结/复位由应用提供。
* 创建和动作入口只委托共用引擎,避免另建一套任务、广播、期限及取消流程。
*/
#include "IEC104_CounterInterrogation.h"
#include "IEC104_Interrogation_Internal.h"
/* 从首对象取IOA和QCC;对象层已将低6位RQT与高2位FRZ拆开。
* 这里只解析,不校验是否支持该分组;失败时不写调用方的输出。 */
static bool Counter_Decode(const IEC60870_AsduInfo_t *info, uint32_t *ioa, uint8_t *qualifier, uint8_t *freeze)
{
IEC60870_CounterInterrogationCommand_t command;
if (IEC60870_Asdu_Get_Element(info, 0U, &command, sizeof(command)) != IEC60870_ASDU_OK) { return false; }
*ioa = command.object.ioa;
*qualifier = command.qualifier;
*freeze = command.freeze;
return true;
}
/* RQT=1..4为分组,5为总请求;0未用、6..63保留。具体分组是否配置由应用在BEGIN决定。 */
static bool Counter_Qualifier_Valid(uint8_t qualifier) { return qualifier >= 1U && qualifier <= 5U; }
/* 引擎准入后使用:总请求的数据COT为37,第1..4组依次为38..41。 */
static uint8_t Counter_Data_Cause(uint8_t qualifier)
{
return (uint8_t)(qualifier == 5U ? IEC60870_ASDU_COT_COUNTER_INTERROGATION :
IEC60870_ASDU_COT_COUNTER_INTERROGATION + qualifier);
}
/* 只有读取存在数据阶段;冻结/复位在肯定确认提交后直接完成。 */
static bool Counter_Has_Data(uint8_t freeze) { return freeze == 0U; }
/* 读取接受无时标Type15和CP56时标Type37;Type37仅SQ=0,Type16不用于104。
* 报文完整布局、对象数和头部一致性仍由共用引擎及ASDU层检查。 */
static bool Counter_Data_Allowed(const IEC60870_AsduInfo_t *info, uint8_t freeze)
{
return freeze == 0U && (info->type_id == IEC60870_ASDU_M_IT_NA_1 ||
(info->type_id == IEC60870_ASDU_M_IT_TB_1 && info->is_sequence == 0U));
}
/* 只读规则存活至进程结束;has_deactivation=false使COT=8按未知传送原因处理。 */
const IEC104_InterrogationRules_t IEC104_COUNTER_INTERROGATION_RULES = {
IEC60870_ASDU_C_CI_NA_1, false, Counter_Decode, Counter_Qualifier_Valid,
Counter_Data_Cause, Counter_Data_Allowed, Counter_Has_Data
};
/* 用计数量规则创建独立实例;配置、站点引用、重复实例和状态区重叠统一由引擎校验。 */
IEC104_Interrogation_t *IEC104_CounterInterrogation_Create(IEC104_Slave_t *slave,
const IEC104_InterrogationConfig_t *config)
{
return IEC104_Interrogation_Create_With_Rules(slave, config, &IEC104_COUNTER_INTERROGATION_RULES);
}
/* 传入本种类规则作为身份约束,防止误把总召句柄交给动作入口。 */
IEC104_InterrogationReplyResult_t IEC104_CounterInterrogation_Execute_Begin(
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event,
IEC104_CounterInterrogationAction_t action, void *context)
{
return IEC104_Interrogation_Execute_Action(self, event, &IEC104_COUNTER_INTERROGATION_RULES, action, context);
}
@@ -0,0 +1,48 @@
/*
* 104计数量召唤(C_CI_NA_1):读取冻结值,或由应用执行冻结/复位。
* 本模块提供种类规则和创建/动作入口;事件、调度、广播、期限与清理由共用引擎维护。
* 返回的句柄沿用Interrogation的驱动接口,所有调用在所属事件循环中串行执行。
*/
#ifndef IEC104_COUNTER_INTERROGATION_H
#define IEC104_COUNTER_INTERROGATION_H
#include "IEC104_Interrogation.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 创建计数量实例:复制配置、借用Slave;参数或容量不满足要求时返回NULL,不操作网络。
* 同一Slave可同时拥有总召和计数量各一个实例,须分别提供广播状态区并接收全部连接/启停通知。
* RQT=1..4选择分组,5选择全部;FRZ=0读取冻结值,接受Type15(SQ=0/1)或Type37(SQ=0),
* 数据COT使用request.data_cause;104计数量不接受带CP24时标的Type16。
* FRZ=1冻结,2冻结并复位,3仅复位;肯定ACT_CON提交后即完成,不生成NEED_DATA或ACT_TERM。
* gi_submitted在此实例统计完成响应的提交次数:读取为ACT_TERM,动作命令为ACT_CON。
* 不支持COT=8撤销,回COT=45;TEST冻结/复位请求否定且不执行动作。
* 销毁前先Cancel并读完FINISHED;连接关闭会同时影响共享该连接的其他业务。 */
IEC104_Interrogation_t *IEC104_CounterInterrogation_Create(IEC104_Slave_t *slave,
const IEC104_InterrogationConfig_t *config);
/* 同步动作回调:context为应用上下文,request为引擎保存的请求信息,仅在回调期间借用。
* true表示所选组的动作已完成;false必须保证未改变任何计数器,不能部分清零后报告失败。
* 应先检查整个组的资源和条件,再提交修改;动作须短时完成,不访问网络或执行阻塞I/O。
* 不得重入引擎或推进协议事件循环;真实计数器值、冻结值、序号和品质均由应用维护。 */
typedef bool (*IEC104_CounterInterrogationAction_t)(void *context,
const IEC104_InterrogationRequest_t *request);
/* 应答已取出的FRZ=1..3 BEGIN;event须原样传回,action非NULL,context原样传给回调。
* 先校验实例、事件、会话和期限;WAIT/STALE/ERROR均不调用action,也不会修改计数器。
* 校验通过后先消费BEGIN,再调用一次action;返回OK表示交互已消费,不表示动作一定成功。
* action返回true排队肯定确认,返回false排队否定确认;TEST请求直接否定,不调用action。
* 同一事件再次提交返回STALE;只保证本次BEGIN不重复执行,主站重发的新请求仍可能再次执行。
* 动作成功后等待肯定确认期间停止业务期限,避免计数器已改变却因排队超时改发否定确认。
* 传输超时、STOP、断线及本地Cancel仍可终结协议任务并交付FINISHED,但不回滚已执行动作。
* 本接口不管理异步外部电表事务;这类事务须另定义执行状态、迟到结果和重复请求策略。
* FRZ=0使用Reply_Begin及数据接口,整轮读取(含WAIT重试)应保持同一冻结快照。
* 动作命令没有NEED_DATA,拿其BEGIN调用Submit_Batch会返回STALE,不发送数据。 */
IEC104_InterrogationReplyResult_t IEC104_CounterInterrogation_Execute_Begin(
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event,
IEC104_CounterInterrogationAction_t action, void *context);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_COUNTER_INTERROGATION_H */
@@ -0,0 +1,54 @@
/*
* 104总召种类规则:C_IC_NA_1、QOI=20..36、无时标监视数据及主站撤销。
* 原IEC104_Interrogation_Create入口在此保留;调度、生命周期及发送由共用引擎维护。
*/
#include "IEC104_GeneralInterrogation.h"
#include "IEC104_Interrogation_Internal.h"
/* 解析首对象的IOA和QOI;总召没有FRZ,统一输出0供共用请求结构使用。 */
static bool General_Decode(const IEC60870_AsduInfo_t *info, uint32_t *ioa, uint8_t *qualifier, uint8_t *freeze)
{
IEC60870_InterrogationCommand_t command;
if (IEC60870_Asdu_Get_Element(info, 0U, &command, sizeof(command)) != IEC60870_ASDU_OK) { return false; }
*ioa = command.object.ioa;
*qualifier = command.qualifier;
*freeze = 0U;
return true;
}
/* QOI=20为站总召,21..36为16个分组;数据COT与合法QOI相同。 */
static bool General_Qualifier_Valid(uint8_t qualifier) { return qualifier >= 20U && qualifier <= 36U; }
static uint8_t General_Data_Cause(uint8_t qualifier) { return qualifier; }
/* 总召总是进入数据阶段;空点表也由应用Complete_Data结束,再发送ACT_TERM。 */
static bool General_Has_Data(uint8_t freeze) { (void)freeze; return true; }
/* 保持原总召的9种无时标监视类型,SQ=0/1由ASDU层校验;累计量由计数量实例处理。 */
static bool General_Data_Allowed(const IEC60870_AsduInfo_t *info, uint8_t freeze)
{
(void)freeze;
switch (info->type_id) {
case IEC60870_ASDU_M_SP_NA_1:
case IEC60870_ASDU_M_DP_NA_1:
case IEC60870_ASDU_M_ST_NA_1:
case IEC60870_ASDU_M_BO_NA_1:
case IEC60870_ASDU_M_ME_NA_1:
case IEC60870_ASDU_M_ME_NB_1:
case IEC60870_ASDU_M_ME_NC_1:
case IEC60870_ASDU_M_PS_NA_1:
case IEC60870_ASDU_M_ME_ND_1: return true;
default: return false;
}
}
/* 总召支持COT=8撤销;规则只负责声明能力,匹配请求及撤销清理由引擎执行。 */
const IEC104_InterrogationRules_t IEC104_GENERAL_INTERROGATION_RULES = {
IEC60870_ASDU_C_IC_NA_1, true, General_Decode, General_Qualifier_Valid,
General_Data_Cause, General_Data_Allowed, General_Has_Data
};
/* 保留旧公开函数名和签名;工程需同时编译本文件与共用引擎。 */
IEC104_Interrogation_t *IEC104_Interrogation_Create(IEC104_Slave_t *slave,
const IEC104_InterrogationConfig_t *config)
{
return IEC104_Interrogation_Create_With_Rules(slave, config, &IEC104_GENERAL_INTERROGATION_RULES);
}
@@ -0,0 +1,9 @@
/*
* 104总召(C_IC_NA_1)的种类入口。
* 创建函数仍名为IEC104_Interrogation_Create,声明和共用驱动接口由下列头文件提供。
* 只使用总召的工程无需链接CounterInterrogation.c。
*/
#ifndef IEC104_GENERAL_INTERROGATION_H
#define IEC104_GENERAL_INTERROGATION_H
#include "IEC104_Interrogation.h"
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,326 @@
/*
* 104召唤共用引擎:具体种类规则由GeneralInterrogation/CounterInterrogation创建入口提供。
* 总召旧入口Create保持兼容;计数量召唤入口见IEC104_CounterInterrogation.h。
* 借用一个Slave,不创建socket/线程,不保存业务回调;动作入口仅同步借用回调执行一次。
* 所有接口在同一事件循环串行调用,动作回调不得重入引擎。
* 先把Slave通知按FIFO交给Handle_Event,读空后Process,再NextEvent并显式应答。
* 事件来源和会话状态由Slave公共接口校验;本模块维护召唤阶段、业务期限和应用事件。
* 退出顺序:Cancel -> 读完业务FINISHED并清理应用资源 -> Destroy -> Destroy Slave。
*/
#ifndef IEC104_INTERROGATION_H
#define IEC104_INTERROGATION_H
#include "IEC104_Slave.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 不透明静态池句柄,只能由Create取得、Destroy释放,不能自行分配、复制对象或free。
* 销毁成功后停止使用旧句柄;同一实例的全部接口须在所属事件循环中串行调用。 */
typedef struct IEC104_Interrogation IEC104_Interrogation_t;
/* 请求信息随事件按值复制,不暴露可发送的connection。
* 应用据此选择数据源、构造数据头;IOA、分组成员及遍历状态均由应用维护。
* 引擎不登记或冻结IOA列表;总召由应用逐批取当前值,计数量读取由应用固定本轮冻结快照。
* 冻结/复位命令在BEGIN执行应用动作,不进入NEED_DATA。 */
typedef struct {
uint64_t generation, receive_serial; /* 原TCP会话代次、接收I帧标识,供应用关联和诊断。 */
uint64_t station_id; /* 本次选中的注册编号,兼容裸CA模式为0;按编号查找避免同CA替换串站。 */
uint16_t common_address; /* 实际本站地址;广播65535解析为此地址,应用数据批次须使用同一CA。 */
uint8_t originator_address; /* 请求的源发地址OA,应答数据须原样保留。 */
uint8_t qualifier; /* 总召QOI=20..36;计数量RQT=1..5,应用维护分组点表。 */
bool is_test; /* 请求的TEST标志,数据批次须保持一致。 */
uint8_t max_objects_per_asdu; /* 1..127,实际另受249字节容量限制。 */
uint8_t freeze; /* 计数量FRZ:0读取,1冻结,2冻结并复位,3仅复位;总召恒为0。 */
uint8_t data_cause; /* 数据COT:总召等于QOI,计数量总请求37、分组38..41。 */
} IEC104_InterrogationRequest_t;
/* FINISHED通知应用清理本次游标及业务资源;终结原因不等于网络发送回执。
* COMPLETED表示结束帧被发送层接手,不表示写完或收到ACK;读取为ACT_TERM,动作命令为ACT_CON。 */
typedef enum {
IEC104_INTERROGATION_FINISH_NONE = 0, /* 未终结;BEGIN/NEED_DATA事件也使用此值。 */
IEC104_INTERROGATION_FINISH_COMPLETED, /* 读取的全部批次和ACT_TERM,或动作命令的ACT_CON已提交。 */
IEC104_INTERROGATION_FINISH_REJECTED, /* 应用拒绝;否定ACT_CON可能仍在排队。 */
IEC104_INTERROGATION_FINISH_CANCELLED, /* 应用调用本地Cancel放弃任务。 */
IEC104_INTERROGATION_FINISH_STOPPED, /* 收到Slave的停用通知,结束本次数据传输。 */
IEC104_INTERROGATION_FINISH_DISCONNECTED, /* 原TCP会话断开。 */
IEC104_INTERROGATION_FINISH_CONNECTION_CHANGED, /* 连接代次或当前传输状态已不再适用于任务。 */
IEC104_INTERROGATION_FINISH_SOURCE_ERROR, /* 应用通过Fail_Request报告数据源准备/取数失败。 */
IEC104_INTERROGATION_FINISH_SEND_ERROR, /* 提交应答失败且不是可等待的发送容量不足。 */
IEC104_INTERROGATION_FINISH_OVERLOAD, /* 兼容保留;满槽现在拒绝/丢弃新请求,不终结已有任务。 */
IEC104_INTERROGATION_FINISH_INVALID_REQUEST, /* 保留枚举值兼容;畸形新请求不再终结已有总召。 */
IEC104_INTERROGATION_FINISH_SERVICE_ERROR, /* 服务查询、内部状态或构造应答失败。 */
IEC104_INTERROGATION_FINISH_DEACTIVATED, /* 主站COT=8撤销;COT=9可能仍待发送。 */
IEC104_INTERROGATION_FINISH_BEGIN_TIMEOUT, /* BEGIN入队后未在期限内得到应用决定。 */
IEC104_INTERROGATION_FINISH_DATA_TIMEOUT, /* NEED_DATA入队后未在期限内得到应用应答。 */
IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT /* BEGIN生成至ACT_TERM提交超过执行整体期限。 */
} IEC104_InterrogationFinishReason_t;
typedef enum {
IEC104_INTERROGATION_BEGIN = 1, /* 检查数据源并接受/拒绝;取数游标可延至首个NEED_DATA建立。 */
IEC104_INTERROGATION_NEED_DATA, /* 应用选择本批IOA并读取当前值/品质;暂未就绪时保存事件。 */
IEC104_INTERROGATION_FINISHED /* 清理游标及业务资源,无需应答。 */
} IEC104_InterrogationEventType_t;
/* 纯值事件,无借用指针和自引用,可赋值、排入应用队列、保存到后续循环。
* 字段只读,应答时原样传回;实例/请求/批次标识用于拒绝迟到和重复应答。
* 每个已取出的BEGIN恰好配对一次FINISHED,取消也不丢失终结通知。
* BEGIN尚未取出便取消时移除BEGIN,不产生FINISHED(应用尚未建立资源)。
* 取消移除队列中的NEED_DATA;已取出的旧事件仍可读取,应答返回STALE。 */
typedef struct {
IEC104_InterrogationEventType_t type; /* 决定调用Reply_Begin、提交数据或只清理资源。 */
uint64_t instance_id, request_id; /* 实例及请求身份;必须与事件其他字段一起原样传回。 */
IEC104_InterrogationRequest_t request; /* 原激活请求的元数据,FINISHED仍保留此副本。 */
size_t batch_index, objects_submitted; /* 已被发送层接手的批次/对象数;首个NEED_DATA的批次号为0。 */
IEC104_InterrogationFinishReason_t reason; /* 仅FINISHED有意义。 */
} IEC104_InterrogationEvent_t;
/* 多站广播的调用方存储:每个目标站一项,不保存点值、ASDU或独立发送任务。
* config借用整个数组至Destroy,期间地址稳定、不得读写或与其他实例重叠。
* 模块初始化/清理内部字段,不要求调用方逐项初始化。 */
typedef struct {
IEC104_Station_t *_station; /* 本轮持有的原节点引用,直到整轮终结事件读完才释放。 */
uint64_t _request_id, _started_ms; /* 每站独立的请求身份和BEGIN时刻,切换当前站不丢失。 */
size_t _batch_index, _objects_submitted; /* 已提交进度,不是应用的IOA遍历游标。 */
uint8_t _state, _flags, _reason; /* 协议阶段、事件交付/撤销标志、终结原因,仅库内部使用。 */
} IEC104_InterrogationStationState_t;
/* Create复制此配置;创建后修改调用方对象不会改变已有实例,指向的广播数组仍为借用。
* 接受/取数期限从事件入队开始,包含应用尚未NextEvent读取它的时间。 */
typedef struct {
uint16_t common_address; /* station为NULL时的兼容站地址1..65534;绑定站点时从节点取得CA并忽略此值。 */
uint8_t max_objects_per_asdu; /* 每批1..127个对象,配置无业务函数指针。 */
bool use_station_registry; /* true按请求CA查共用站点表,广播分三阶段;station须NULL,common_address忽略。 */
uint32_t begin_timeout_ms; /* BEGIN入队至Reply_Begin/Fail_Request,默认15000ms。 */
uint32_t data_timeout_ms; /* NEED_DATA至首次有效批次/Complete/Fail,默认15000ms;发送WAIT后的重试不计。 */
uint32_t total_timeout_ms; /* 读取:每站BEGIN至ACT_TERM提交;动作:BEGIN至应用接受。默认0禁用。 */
/* 上述期限是业务策略,独立于104链路t0..t3;每项0表示禁用该项。
* BEGIN尚未生成时不计业务期限;START/CONFIRM超时转为否定ACT_CON并保留连接。
* 动作命令接受后的CONFIRM不再受业务期限约束,避免执行成功后被改发否定;连接期限仍生效。
* 肯定ACT_CON被发送层接手后,DATA/TERMINATE超时关闭原连接,不发送成功ACT_TERM。
* 同一执行任务的总期限与阶段期限同时到点时优先报告总超时。 */
IEC104_Station_t *station; /* 兼容固定站绑定:Create持有至Destroy;删除后不接新请求,已接受任务可完成。 */
IEC104_InterrogationStationState_t *broadcast_states; /* 调用方独占提供的状态数组,寿命覆盖实例。 */
size_t broadcast_capacity; /* 数组项数,即单轮多站广播能接纳的目标数,不是注册站数上限。 */
/* 默认NULL/0。广播涉及多个站时,容量须覆盖请求时的全部注册站,否则整轮否定确认。
* 单站/定向总召不需要数组。容量由应用决定,不限制注册站数;数组含已确认目标及终结记录。 */
} IEC104_InterrogationConfig_t;
/* 输入Slave通知的处理结果;其他类型I帧返回IGNORED,交后续业务处理。 */
typedef enum {
IEC104_INTERROGATION_EVENT_ERROR = -1, /* 参数、复制、关闭连接等处理失败。 */
IEC104_INTERROGATION_EVENT_IGNORED = 0, /* 本模块未接手,可转交其他业务处理器。 */
IEC104_INTERROGATION_EVENT_HANDLED = 1 /* 已处理,含畸形/满槽丢弃;不保证应用接受或已发送响应。 */
} IEC104_InterrogationEventResult_t;
typedef enum {
IEC104_INTERROGATION_NEXT_ERROR = -1, /* 非法句柄或NULL输出。 */
IEC104_INTERROGATION_NEXT_EMPTY = 0, /* 当前无业务事件,不代表总召已经结束。 */
IEC104_INTERROGATION_NEXT_READY = 1 /* 已复制并移除最早事件,输出可独立保存。 */
} IEC104_InterrogationNextResult_t;
/* 显式比较,不能按真假判断。
* OK:交互已消费;Submit_Batch已交发送层并固定顺序,其余应答由Process推进;不表示写完/ACK。
* WAIT:先处理Slave通知并推进网络;Submit_Batch还可能因发送背压WAIT,不缓存本次原文。
* 保留同一事件,批次游标不动,下次重读当前值组包。首次有效批次的发送WAIT结束本批取数计时。
* STALE:实例/请求/批次/类型不匹配、未取出、已应答、已取消或已超时;不要重试旧事件。
* ERROR:参数/批次/服务查询错误,不消费交互,可修正再提交。
* NEED_DATA阶段Fail_Request例外:即使关闭连接失败,仍取消并交付FINISHED,不能重试。 */
typedef enum {
IEC104_INTERROGATION_REPLY_STALE = -2,
IEC104_INTERROGATION_REPLY_ERROR = -1,
IEC104_INTERROGATION_REPLY_WAIT = 0,
IEC104_INTERROGATION_REPLY_OK = 1
} IEC104_InterrogationReplyResult_t;
/* 优先描述唯一未终结总召的诊断阶段;仅有应答时REPLY,无任务时IDLE。
* 阶段由模块推进,应用通过事件交互,不直接修改。 */
typedef enum {
IEC104_INTERROGATION_IDLE, /* 没有协议任务。 */
IEC104_INTERROGATION_REPLY, /* 等待提交单帧确认(含撤销确认)或错误应答。 */
IEC104_INTERROGATION_START, /* 等事件空位生成BEGIN,或等待应用接受/拒绝;两者均占用总召资格。 */
IEC104_INTERROGATION_CONFIRM, /* 应用已接受,等待提交肯定ACT_CON。 */
IEC104_INTERROGATION_DATA, /* 生成NEED_DATA,等待应用取值并尝试直接提交。 */
IEC104_INTERROGATION_TERMINATE, /* 应用声明数据结束,等待提交ACT_TERM。 */
IEC104_INTERROGATION_FINISHING /* 多站广播网络任务已结束,等NextEvent读完各站FINISHED后归还状态区。 */
} IEC104_InterrogationPhase_t;
/* Get_Status返回的独立副本;计数自Create起累计,Cancel和重连不清零。
* active/waiting反映最近已处理的通知/推进结果;events_pending及need_process在查询时计算。 */
typedef struct {
uint64_t generation; /* 已消费的连接代次,断线后0。 */
bool active; /* 已消费ACTIVATED且未停用,不是socket实时状态。 */
bool gi_pending; /* 总召网络流程尚未终结;FINISHING虽为false,读完终结事件前仍拒绝新激活。 */
bool waiting; /* 存在发送、应用或事件空位等待;总召等应用时仍可推进单帧应答。 */
bool application_waiting; /* 等待应用首次有效应答,包含事件尚未取出;发送背压另见data_send_waiting。 */
bool data_send_waiting; /* 有效批次提交曾遇发送WAIT;应用仍持有NEED_DATA,网络推进后重取再试。 */
bool events_pending; /* 当前Slave FIFO未空,须先处理网络通知。 */
bool need_process; /* 有本地工作/到期/会话失效待处理,应立即Process;未读Slave通知优先。 */
bool timer_active; /* 存在启用的业务期限;为false时timeout_remaining_ms无意义。 */
uint32_t timeout_remaining_ms; /* 最近期限距当前单调时钟的毫秒数;0表示应立即Process。 */
unsigned pending_requests, pending_events; /* 任务数含FINISHING;事件数含状态区中待交付的FINISHED,不含预留。 */
IEC104_InterrogationPhase_t phase; /* 当前站阶段;收尾为FINISHING,仅有应答为REPLY,无任务为IDLE。 */
size_t batch_index, objects_submitted; /* 活动总召已提交的批次/对象数;仅有应答时取最早应答保留的进度。 */
unsigned long submitted, gi_submitted; /* 接手ASDU数、完成协议提交的召唤数;动作命令以ACT_CON计。 */
unsigned long waits, cancelled, rejected, overloads;
/* waits统计发送层返回WAIT;cancelled统计取消的协议任务(含单帧应答);
* rejected统计校验拒绝(含无法应答的畸形请求)/总召忙/应用/确认前失败/容量拒绝;
* overloads含任务槽满或广播状态区不足。 */
unsigned long negative_submitted, dropped_requests; /* 已提交否定应答数、无法保存或提交而丢弃的新请求数。 */
unsigned long deactivated, timeouts; /* 成功匹配的主站撤销、执行阶段业务超时次数。 */
IEC104_InterrogationFinishReason_t last_finish_reason; /* 最近合法总召的终结原因,含BEGIN尚未交付的任务。 */
} IEC104_InterrogationStatus_t;
/* 初始化调用方的可写配置:station=NULL、兼容CA=1,broadcast_states=NULL、broadcast_capacity=0,每批最多127个对象,
* 接受/取数期限各15000ms,执行整体期限为0(禁用);NULL无操作。 */
void IEC104_Interrogation_Config_Default(IEC104_InterrogationConfig_t *config);
/* 复制配置,借用Slave;同一Slave的同一种类只能绑定一个实例,在消费Slave通知前创建。
* slave须保持有效直到本实例Destroy;config在返回后可释放或复用。
* station非NULL时校验其属于本Slave并占用节点,本站CA只取节点值;创建失败不泄漏引用。
* use_station_registry开启时按CA选站;广播共用一个任务,依次完成全部确认、各站数据、全部终止。
* 各站独立BEGIN/FINISHED和request_id;多目标广播须有足够broadcast_states,容量不足整轮否定。
* 接收时持有目标节点引用;尚未选入BEGIN便删除的站跳过,已经选中的站持原引用完成。
* 新增/替代节点不加入本轮;已确认站即使尚未取数或被删除,也必须完成或明确终结。
* BEGIN后需跨循环保留context的应用须按station_id查节点并Acquire,FINISHED清理后Release。
* 兼容模式use_station_registry=false保留单CA或固定station行为。
* NULL:参数非法、重复绑定、状态区重叠、池满或实例编号耗尽;无网络操作,无malloc。
* 编译.c可覆盖IEC104_INTERROGATION_CAPACITY(默认为TCP实例容量的两倍)、
* IEC104_INTERROGATION_TASK_CAPACITY(默认4)、IEC104_INTERROGATION_EVENT_CAPACITY
* (默认4,最小2)。任务池容纳至多一个总召及短应答,不是多个总召的等待队列。
* 单目标流程在FIFO预留FINISHED;多目标用每站状态区保留终结记录,NextEvent按空位交付。 */
IEC104_Interrogation_t *IEC104_Interrogation_Create(IEC104_Slave_t *slave,
const IEC104_InterrogationConfig_t *config);
/* 协议任务和业务事件均为空才释放槽,否则false并保持原状态。
* 先Cancel,再NextEvent读完FINISHED并清理应用资源,最后Destroy;成功后句柄失效。 */
bool IEC104_Interrogation_Destroy(IEC104_Interrogation_t *self);
/* 放弃未提交任务;保留已交付BEGIN的FINISHED(CANCELLED),移除未交付工作事件。
* 不撤回已接手帧、不补发ACT_TERM,也不回滚已执行的冻结/复位;非法句柄无操作。 */
void IEC104_Interrogation_Cancel(IEC104_Interrogation_t *self);
/* 按FIFO接收本Slave的每个通知,不消费Slave FIFO,不保存调用方指针。
* 下列QOI和撤销约定针对总召;计数量的RQT/FRZ规则见CounterInterrogation.h。
* 同时使用两个实例时,连接/启停通知交给两边;I帧仅在当前实例IGNORED后转交下一个。
* 总召须SQ=0、一个对象、COT=6或8、P/N=0、CA为本站或65535、IOA=0、QOI=20..36。
* 广播常量为IEC104_CA_BROADCAST;应用事件、确认、数据、否定应答及终止均使用实际本站CA。
* 原输入事件不修改;每次选站设置应答CA,同一站发送及重试沿用该地址。
* 空闲时保存一个COT=6激活并进入START;从保存起至终结,拒绝所有后续激活,不区分QOI。
* 忙或QOI/P/N非法时回否定ACT_CON;保留已有请求的QOI/OA/TEST、工作事件及数据进度。
* 单帧应答在无未读Slave通知、原会话有效且无更早REPLY时尝试立即提交,未接手再保存。
* 多站广播应答先保存一个遍历任务,再由Process逐站提交;无槽时整体丢新请求并计数。
* 满槽时尝试否定确认;无法安全提交/保存则仅丢新请求并累计dropped_requests,不断链。
* 撤销COT=8不受忙拒绝限制,按连接代次、实际站CA、OA、TEST、QOI匹配当前总召;
* 定向撤销停止匹配站,包括已确认但尚未取数/等待终止的站;其他站继续。
* 尚未选入BEGIN的站没有可撤销的活动事务。广播撤销停止整轮,按各活动站CA确认。
* 已完成/已拒绝站不补发撤销确认;无注册站且无匹配事务时,广播请求回复未知CA。
* 为已交付BEGIN保留FINISHED(DEACTIVATED),按单帧应答规则提交肯定COT=9;
* 不匹配/非法QOI或P/N回否定COT=9。
* 撤销无需应用再次批准;应用收到FINISHED后停止采样并释放本次资源。
* 匹配撤销复用原任务槽,并将确认排到单帧应答队尾;满池也不需要额外槽。
* 一组撤销确认尚未提交完时,重叠的新撤销按未匹配拒绝,不覆盖已有确认。
* COT/CA/IOA错误回45/46/47;撤销不撤回发送层已接手的帧,不补发ACT_TERM。
* 尚未生成/交付BEGIN的撤销不产生FINISHED;已有BEGIN交付才通知应用清理。
* 先安全读取Type,非100返回IGNORED;公共视图不可用也返回IGNORED,由公共入口处理。
* Type100仅一个对象且IOA/QOI完整时,SQ=1或多余尾字节回否定7/9(COT/CA/IOA错误优先45/46/47)。
* 此应答规范为10字节、VSQ=1,保留OA/TEST/IOA/QOI;绝不据规范化副本执行激活或撤销。
* 零对象/多对象等无法可靠对应请求的布局错误计入rejected并返回HANDLED,不生成应答。
* 上述请求错误不取消已有总召、不刷新其期限;基础帧错误仍由Receiver/连接层处理。
* STOPDT/断线取消总召及待发短应答并保留终结事件;不替其他业务回答UNKNOWN_TYPE。 */
IEC104_InterrogationEventResult_t IEC104_Interrogation_Handle_Event(
IEC104_Interrogation_t *self, const IEC104_SlaveEvent_t *event);
/* 自动推进当前就绪任务,不等待应用或网络;调用方不需要指定处理步数。
* Slave FIFO未空仅设置events_pending。顺序:START -> BEGIN -> 应用接受 -> ACT_CON ->
* NEED_DATA/提交批次(可重复)-> 应用Complete_Data -> ACT_TERM -> FINISHED。
* 动作命令例外:ACT_CON被发送层接手后直接FINISHED,不生成NEED_DATA或ACT_TERM。
* 多目标广播先完成所有站的BEGIN/ACT_CON,再逐站NEED_DATA,全部数据结束后才逐站ACT_TERM。
* 工作事件只交付一次;应用暂未就绪可保存事件,后续Process不会重复请求。
* 单帧应答保持FIFO,可在总召等待应用/业务事件空位时提交;双方可推进时先应答再总召轮转,
* 继续处理当前就绪任务,直至空闲、所有任务等待应用/事件空位,或发送返回WAIT。
* 每个站的REPLY成功后继续广播或释放任务;不保存后续主站新发来的总召激活。
* 每轮最多推进2倍任务容量步,广播余下站点由need_process提示继续,避免大量站点独占事件循环。
* 应答可穿插总召各批次,本次总召自身ACT_CON/数据/ACT_TERM顺序不变。
* 确认、终止和短应答沿原connection走HIGH,WAIT保留请求;DATA只生成NEED_DATA,
* 由应用Submit_Batch直接提交,OK才增加批次/对象计数,WAIT须重新取值后重试。
* WAIT让出本轮,下轮优先检查单帧应答;已被发送层接手的帧不被后到应答越过。
* 发送ERROR时复核原连接;正常STOP拒绝且通知未读时仅置events_pending并让出,由通知终结为STOPPED。
* 不因此增加waits、重置期限或撤销已接手帧;其他ERROR及FATAL仍按SEND_ERROR处理。
* 使用平台单调时钟检查期限,先处理Slave通知;START/CONFIRM超时转为否定REPLY并继续调度,
* 肯定ACT_CON提交后的DATA/TERMINATE超时关闭原连接;FINISHED保留具体超时原因。
* 执行总期限默认禁用,启用后按每站BEGIN独立开始,含等待其他站完成的时间。
* 已确认站总期限到点即关闭原连接;此前等待事件空位不计期限,应及时读FINISHED。
* 网络推进并读空Slave通知后须Process重试,即使need_process为false;发送WAIT不自行触发空转。
* 数据WAIT还须重试应用持有的NEED_DATA;本函数不会代为重读或重发数据。
* 本地应答/事件消费后重查Get_Status,need_process为true不得休眠;并用剩余期限限制等待时长。
* true本轮正常(可等待/取消/超时);false参数、组包、关闭连接或服务查询失败。 */
bool IEC104_Interrogation_Process(IEC104_Interrogation_t *self);
/* 取最早业务事件;event须为非NULL输出,只有READY写入,EMPTY/ERROR保持输出不变。
* READY后工作事件才允许应答;FINISHED仅用于应用清理,不调用应答接口。
* 多站终结记录超过FIFO容量时,本函数按需补入,连续NextEvent即可读完,无需Process。
* 最后一条FINISHED出队后归还广播状态区和节点引用;应用持有的节点引用仍须自行Release。
* 事件是可保存的纯值副本,本函数无网络操作,也不自动执行应用代码。 */
IEC104_InterrogationNextResult_t IEC104_Interrogation_NextEvent(
IEC104_Interrogation_t *self, IEC104_InterrogationEvent_t *event);
/* 应答已取出的BEGIN。true允许提交ACT_CON;false立即FINISHED(REJECTED),
* 并排队否定ACT_CON;不支持的分组可在这里拒绝。应用按instance_id/request_id管理遍历状态。
* 接受前检查数据源及业务资源;无需登记完整IOA列表或预先采样全部数据。
* event须原样传回;OK表示接受/拒绝决定已消费,实际ACT_CON由后续Process提交。
* 所有应答接口也检查期限,不能通过迟到应答绕过超时;Slave通知未读空时仍优先返回WAIT。
* 计数量FRZ!=0建议使用CounterInterrogation_Execute_Begin,在校验通过后才执行副作用。
* 不要在本函数之前无条件执行冻结/复位:WAIT会重试,STALE也不能撤销已经发生的动作。
* 动作命令接受后停止业务期限,保留确认等待;TEST动作始终拒绝。 */
IEC104_InterrogationReplyResult_t IEC104_Interrogation_Reply_Begin(
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, bool accept);
/* 应答已取出的NEED_DATA,验证非空ASDU后当场尝试不排队提交。
* OK表示已复制进发送缓冲并固定发送顺序,当场累计批次/对象数;输入缓冲及参数可立即复用。
* WAIT不接手、不推进游标;应用保存同一事件,网络推进后重新组包,不保留旧编码等待重发。
* 总召可重取当前值;计数量必须保持本轮冻结版本,值、序号、品质及可选时标一起固定。
* 首次有效批次发生发送WAIT后停止本批取数计时;下一NEED_DATA重新计时,显式整体期限仍有效。
* 参数须2/2/3,COT=request.data_cause、P/N=0、CA=本事件目标站地址、OA/TEST与事件相同。
* 广播请求的数据CA也不能使用65535;对象数不超配置。
* 总召接受Type 1/3/5/7/9/11/13/20/21,SQ=0/1均可;计数量读取接受Type 15(SQ=0/1)、37(SQ=0)。
* IOA及分组归属由调用方决定;库校验编码布局,不规定点表或强制固定IOA集合。
* 总召建议每次NEED_DATA读取当前值/品质,整轮不保证同一采样时刻;计数量由应用保证快照一致。
* 点值更新及突发发布、总召取值及本调用须由同一发布流程串行协调,不能迟交旧事件。
* HIGH/NORMAL有待发数据时总召退让;持续事件拥塞可能延后总召。OK后不再重新取值或重复提交。
* 发送ERROR若确认为原连接正常STOP拒绝且有未读通知,返回WAIT并保留事件,先处理DEACTIVATED。
* 此等待不设置data_send_waiting、不停止取数计时;通知处理后旧事件STALE,FINISHED为STOPPED。
* 其他发送ERROR/FATAL取消请求并返回STALE;FATAL可能已发出字节,不允许重试旧批次。
* 非法批次ERROR,保留NEED_DATA供修正,不断线;无法继续则Fail_Request。
* 应用使用Asdu_Create/InformationObject_Create/Add自行构造业务ASDU。 */
IEC104_InterrogationReplyResult_t IEC104_Interrogation_Submit_Batch(
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event,
const IEC60870_Asdu_t *asdu);
/* 应答已取出的NEED_DATA,声明数据耗尽;不以空ASDU作为结束标记。
* OK后本请求不再取数,可归还其取数游标;多目标广播继续下一站数据,全部结束后才发终止。
* Process提交本站ACT_TERM成功后才交付FINISHED(COMPLETED)。 */
IEC104_InterrogationReplyResult_t IEC104_Interrogation_Complete_Data(
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event);
/* 应答已取出的BEGIN/NEED_DATA,报告数据源失败,两者均交付FINISHED(SOURCE_ERROR)。
* BEGIN阶段复用原槽保存否定ACT_CON,保留连接和其他任务;OK不表示否定确认已发送,须继续Process。
* NEED_DATA阶段肯定ACT_CON已经提交:关闭原连接并取消任务,不发送成功终止,不影响后来新代次。
* 检查通过后即终结交互;即使NEED_DATA阶段关闭失败返回ERROR,任务仍已取消,不能重试。 */
IEC104_InterrogationReplyResult_t IEC104_Interrogation_Fail_Request(
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event);
/* 取得独立状态副本,实时查询Slave通知、可推进任务和最近期限;不消费事件或触发I/O/超时。
* need_process表示无需等新网络事件就应尝试推进,不保证发送接手,也不是waiting的反值。
* 生成工作事件有空位、应答接手后可提交下一帧、业务期限到期时置位;Slave通知未读空时不置位。
* 当前帧提交WAIT后停止本地续跑,网络推进后仍须调用Process重试;不能仅凭本标志决定是否处理网络进展。
* data_send_waiting时应用仍须在网络推进后重取并重试保留的NEED_DATA,不能等新事件或need_process置位。
* pending_events非零先消费业务事件;释放空位或完成应答后须重查,不能沿用旧状态休眠。
* timer_active时,调用方等待时长不得超过timeout_remaining_ms;false时不限制等待。
* 返回false为非法句柄/NULL,输出保持不变。 */
bool IEC104_Interrogation_Get_Status(const IEC104_Interrogation_t *self,
IEC104_InterrogationStatus_t *status);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_INTERROGATION_H */
@@ -0,0 +1,39 @@
/*
* 104共用召唤引擎与种类文件之间的私有接口,不供业务直接调用。
* 规则函数必须无状态:不访问网络、不保存业务进度、不回调引擎。
* 规则表在实例整个寿命内保持有效且只读;现有两种请求共用6字节公共头、3字节IOA和1字节限定词。
*/
#ifndef IEC104_INTERROGATION_INTERNAL_H
#define IEC104_INTERROGATION_INTERNAL_H
#include "IEC104_Interrogation.h"
typedef struct {
uint8_t type_id; /* 请求类型,用于入口分派和应答副本校验。 */
bool has_deactivation; /* 是否受理COT=8撤销;不支持时按未知传送原因应答。 */
/* 解析首对象,输出IOA、种类限定词及FRZ;总召FRZ恒为0,失败不改输出。 */
bool (*decode)(const IEC60870_AsduInfo_t *info, uint32_t *ioa, uint8_t *qualifier, uint8_t *freeze);
/* 校验协议允许的限定词范围;应用是否配置该组留到BEGIN判断。 */
bool (*qualifier_valid)(uint8_t qualifier);
/* 返回准入请求应使用的数据COT;只读元数据,不改变原请求。 */
uint8_t (*data_cause)(uint8_t qualifier);
/* 校验数据类型、SQ及动作是否允许携带数据;公共头和容量由引擎另行检查。 */
bool (*data_allowed)(const IEC60870_AsduInfo_t *info, uint8_t freeze);
bool (*has_data)(uint8_t freeze); /* false表示肯定ACT_CON提交后直接完成,不产生数据事件或ACT_TERM。 */
} IEC104_InterrogationRules_t;
/* 种类文件各自定义,只读规则也供不经过网络的规则单测使用;引擎不引用这两个具体符号。 */
extern const IEC104_InterrogationRules_t IEC104_GENERAL_INTERROGATION_RULES;
extern const IEC104_InterrogationRules_t IEC104_COUNTER_INTERROGATION_RULES;
/* 复制配置并借用rules;校验全部规则回调,同一Slave与同一规则表只能创建一个实例。
* 其他创建/存储契约与公开Create一致,失败返回NULL,不触发网络操作。 */
IEC104_Interrogation_t *IEC104_Interrogation_Create_With_Rules(IEC104_Slave_t *slave,
const IEC104_InterrogationConfig_t *config, const IEC104_InterrogationRules_t *rules);
/* 动作入口的内部实现:额外匹配rules身份,校验并消费BEGIN后才调用一次同步action。
* WAIT/STALE/ERROR不调用action;TEST不执行动作而生成否定确认。成功与失败均不保留回调指针。 */
IEC104_InterrogationReplyResult_t IEC104_Interrogation_Execute_Action(
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event,
const IEC104_InterrogationRules_t *rules,
bool (*action)(void *context, const IEC104_InterrogationRequest_t *request), void *context);
#endif
@@ -0,0 +1,65 @@
# IEC60870-Slave
IEC 60870 子站协议库,当前提供 IEC104 子站、Windows TCP 后端、总召和计数量召唤。101/104 可复用的 ASDU、信息对象、时间工具及子站应答接口集中在公共目录。
## 目录结构
按职责归档,同一模块的 `.c` 和 `.h` 放在一起:
```text
IEC60870-Slave/
├── 104/ # IEC104 专用协议代码
│ ├── IEC104_Slave.c/h # 子站统一入口
│ ├── IEC104_Interrogation.c/h # 共用召唤引擎
│ ├── IEC104_GeneralInterrogation.c/h # 站总召、分组总召规则
│ ├── IEC104_CounterInterrogation.c/h # 计数量读取、冻结与复位
│ ├── IEC104_TcpServer.c/h # TCP 服务与事件调度
│ ├── IEC104_Connection.c/h # 连接协调
│ ├── IEC104_Link*.c/h # 链路、主站/子站启停
│ ├── IEC104_Send*.c/h # 发送管理与缓冲
│ ├── IEC104_AsduQueue.c/h # ASDU 队列
│ ├── IEC104_Receiver.c/h # 流式接收
│ ├── IEC104_Codec.c/h # APCI 编解码
│ ├── IEC104_Net.h / IEC104_Net_Windows.c # 网络接口与 Windows 后端
│ └── IEC104*.c/h # 类型、容量配置、日志及旧接收入口
├── common/ # 101/104 公共代码
│ ├── IEC60870_Types.h # 公共类型及应用层参数
│ ├── IEC60870_Codec.c/h # ASDU 公共头编解码
│ ├── IEC60870_Asdu.c/h # ASDU 容器
│ ├── IEC60870_InformationObjects*.c/h # 信息对象
│ ├── IEC60870_Slave.c/h # 公共子站应答接口
│ └── IEC60870_Time.c/h / IEC60870_Clock.c/h # 时间工具与时钟接口
├── examples/ # 应用示例
│ └── iec104_slave_example.c # 模拟点表、业务分派与程序入口
├── test/ # 测试源码与主机脚本
│ ├── iec104_*_test.c # 104 传输、调度、子站与总召测试
│ ├── iec60870_*_test.c # 公共 ASDU 与信息对象测试
│ └── slave_example_loopback_test.cjs # 真实 TCP 回环测试
├── docs/ # 设计、接口、示例及测试说明
│ ├── README.md # 文档索引与推荐阅读顺序
│ ├── 104/ # 104 设计及接口说明
│ ├── common/ # 公共模块说明
│ ├── examples/ # 示例使用说明
│ └── test/ # 测试说明与验证记录
├── scripts/ # 构建脚本
│ └── build.ps1 # 构建示例,-Test 同时运行示例回归
├── .build/ # 本地构建及验证产物(运行脚本后生成)
└── README.md # 项目总览
```
## 构建与测试
在项目根目录的 PowerShell 中执行,需要 MinGW GCC;`-Test` 还需要 Node.js:
```powershell
./scripts/build.ps1
./.build/slave-example/iec104_slave_example.exe
# 运行示例业务回归和真实 TCP 回环测试
./scripts/build.ps1 -Test
```
脚本按自身位置定位项目,可从其他工作目录调用。构建产物统一写入项目根目录的 `.build/`。
接入自己的工程时,将 `104/` 和 `common/` 加入头文件搜索路径,按所用模块选择源文件;仅使用公共模块时只需 `common/`。Windows 网络后端需链接 `Ws2_32`。业务入口为 `IEC104_Slave.h`,总召使用 `IEC104_Interrogation.h`(同时编译GeneralInterrogation.c),计数量使用 `IEC104_CounterInterrogation.h`。接入约定见[计数量设计](docs/104/IEC104_CounterInterrogation_设计架构.md)。
详细资料见[文档索引](docs/README.md)、[模块说明与接入示例](docs/104/README.md)、[子站示例说明](docs/examples/IEC104子站示例.md)和[完整测试命令](docs/test/IEC104测试文档.md#2-编译与运行)。
@@ -0,0 +1,218 @@
# IEC104 子站连接协调层
`IEC104_Connection.c/h`是TCP内部的连接协调模块,把接收器、链路状态和发送管理接成一条子站连接。TcpServer提供TCP字节、实际写入结果和单调时间;Connection安排模块调用顺序,不创建socket、线程或系统定时器。
业务的连接与收发统一通过`IEC104_Slave.h`接入,Slave内部持有并调用TcpServer。本文供维护TCP内部实现时阅读,下文的调用方指TcpServer;业务不包含Connection或SendManager头文件,不创建这些对象,也不调用其接口。业务接入示例见[TCP服务设计](IEC104_TcpServer_设计架构.md#4-业务接入)。
## 1. 对象与存储
每个`IEC104_Connection_t`内含独立的Receiver、CONTROLLED角色Link、SendBuffer及SendManager。Manager引用该对象内的Link和SendBuffer,因此**Init成功后不能复制或移动Connection**。所有操作在同一个执行上下文串行进行,内含对象的字段和修改接口不再由业务直接操作。
TcpServer持有Connection,并提供一个ASDU队列、一个控制槽数组及一个发送记录数组。ASDU队列随服务初始化,位于Connection之外,首次连接前和断线期间均可缓存NORMAL原文;它仍属于TcpServer内部存储。各对象、数组、输入和输出存储不得重叠;所有借用存储在Close前持续有效。无动态分配、无业务回调。
各模块的协作边界如下:
| 模块 | 自己维护的规则 | Connection使用方式 |
|---|---|---|
| Link | 启停门控、停止排空、S确认条件和探测进度 | `Can_Prepare_*`查询控制帧准备资格;`Get_Status`提供观察状态和未确认接收数量 |
| Receiver | 半帧/完整帧进度、原文保留及绑定 | `Get_Status`区分新成帧与旧帧;`Peek/Release`交付原文;`Abort`负责清理 |
| SendManager | Link/Buffer初始化及运行期修改、帧与记录关联、ACK交接 | `Submit_Response`准备应答或入HIGH;`Handle_*`统一接收和完成;`Get_Status`提供记录概况;`Get_Deadlines`提供发送期限、队首身份及控制容量 |
| Connection | 调用顺序、过程时间、故障及连接代次 | 先推进STOP确认,再推进普通确认及t3;状态变化立即交给内部通知适配器 |
Link查询与对应Prepare共用同一份准入判断;查询不构帧、不预留控制槽、不消耗帧编号。返回可准备后,Manager仍须检查实际容量并完成准备,WAIT留待下一次驱动。同一连接全程串行调用。
运行期间Connection对Link仅作只读访问,Link和SendBuffer的修改统一由SendManager提交;TESTFR_CON经Manager处理,发送状态也由Manager查询,不直接操作SendBuffer。Connection和SendManager的完整结构体定义保留在内部头文件中,供上层内部对象静态嵌入;TcpServer的公开头文件隐藏这些定义。协议配置统一定义为`IEC104_Types.h`中的`IEC104_ProtocolConfig_t`,本层保留`IEC104_ConnectionConfig_t`内部别名。初始化通过Manager在最终Link/Buffer地址建立绑定,Connection不再修补其内部指针;成功后的对象地址仍须保持不变。
内部初始化关系示例(展示对象绑定顺序;实际存储位于TcpServer内部,业务使用TcpServer的Create/Init):
```c
#include "IEC104_Connection.h"
static IEC104_Connection_t connection;
static IEC104_AsduQueue_t queue;
static uint8_t high_bytes[1024], normal_bytes[4096];
static IEC104_SendBufferControlFrame_t controls[4];
static IEC104_IFrameSendRecordStorage_t records[12];
static int Init_Service_Queue(void)
{
/* 只在服务启动时初始化一次;无连接期间也可由TcpServer缓存NORMAL。 */
return IEC104_AsduQueue_Init(&queue, high_bytes, sizeof high_bytes,
normal_bytes, sizeof normal_bytes, 0U) == IEC104_ASDU_QUEUE_OK;
}
static int Open_New_Connection(uint64_t generation, uint64_t now_ms)
{
IEC104_ConnectionConfig_t config;
IEC104_Connection_Config_Default(&config);
/* 首次连接与重连都绑定已有服务队列;旧连接先Close/Detach,不能重置离线原文。 */
if (IEC104_AsduQueue_Rebind(&queue, generation) != IEC104_ASDU_QUEUE_OK) {
return 0;
}
if (IEC104_Connection_Init(&connection, &queue, controls, 4, records, 12,
&config, generation, now_ms) != IEC104_CONNECTION_OK) {
/* 恢复离线绑定;失败仍须停止接入并由服务层收尾,不能继续使用异常队列。 */
(void)IEC104_AsduQueue_Detach(&queue, generation);
return 0;
}
return 1;
}
```
服务启动先调用一次Init_Service_Queue,再在每次接入时调用Open_New_Connection。连接的`generation`非零且跨连接不复用;队列的代次0仅表示离线缓存,不能用于Connection或发送提交。配置默认k=12、w=8、t1=15000ms、t2=10000ms、t3=20000ms;本地发送及STOP总期限各15000ms,HIGH严格优先。k/w范围1..32767,所有时间期限必须非零,且`t2_ms < t1_ms`;相等或大于均由Init返回ERROR,Connection和队列保持原值。配置在Init时复制,之后通过新连接Init更换。
队列须已绑定同代次,没有保留或已覆盖的待ACK条目;可以已有待发NORMAL。成功初始化清空HIGH、保留NORMAL,链路为STOPPED。调用失败不覆盖Connection或队列。对象首次使用不必预先清零,但运行中不得重新Init;应先按第6节完成Close。
初始化按预检后提交组织:先在局部Connection中校验Receiver及配置;`SendManager_Init_Connection`在局部Link/Buffer/Manager上完成校验,最后清理HIGH,之后将Link和Buffer提交到最终地址并建立绑定。Connection随后复制已初始化的子对象及其余连接字段,没有可失败步骤。构造接口不读取旧目标对象内容,不提供运行会话迁移或任意重绑功能。
## 2. 已连接的完整流程
```text
收到STARTDT_ACT -> Feed受理、立即STARTED并排队STARTDT_CON -> ACTIVATED
Prepare_Next_Send -> 实际写TCP -> Handle_Send_Progress
CON完整写完 -> 仅释放发送任务,不再改变启停状态
TcpServer_Enqueue_Event -> 服务ASDU队列(无连接时也可调用)
TcpServer_Send_Response -> Connection_Submit_Response(校验当前连接代次)
-> Manager.Submit_Response直接准备I帧,或暂存HIGH
服务事件及HIGH积压 -> Prepare_Next_Send生成I帧
两条路径共用发送缓冲和记录
实际写TCP -> Handle_Send_Progress提交整帧完成
Feed收到有效I/S累计ACK -> 更新记录并释放对应NORMAL原文
TcpServer经Connection.Peek_Confirmed_Record读取 -> 复制RECORD事件 -> Remove_Confirmed_Record
业务调用TcpServer_NextEvent -> 处理事件副本
```
准备APDU、TCP整帧写完、对端ACK、转入事件队列、业务处理事件是不同阶段。队列HIGH原文在交接时出队,直接应答不进入ASDU队列;两者的HIGH I帧记录仍等待本地完成及ACK。NORMAL原文等待ACK或被队列覆盖;收到ACK后释放原文,记录保留到TcpServer成功复制为RECORD事件并调用Remove。尚未转移的确认记录仍占记录容量,满时新I帧等待,S/U仍可发送。业务稍后读取事件副本,不负责释放Manager记录。
## 3. 接收与业务读取
`Connection_Feed(connection, generation, data, length, now_ms, &consumed)`最多消费到一帧末尾。TCP输入有多帧时,调用方按consumed推进,继续喂入剩余部分;半帧留在Receiver等待下次输入。`consumed`必填,任何返回都以它为本次实际消费量;参数/旧代次错误消费0字节。
| 返回值 | 当前状态及调用方动作 |
|---|---|
| OK | 输入尚未成帧,或本次S/U已受理并释放;还有未消费尾部时继续Feed |
| FRAME_READY | I帧已通过链路检查、累计ACK已处理;TcpServer经Peek复制I_FRAME事件,成功入队后Release并继续Feed尾部 |
| WAIT | 当前控制请求尚未受理,通常是控制槽已满;保留尾部,驱动发送或Tick后重试 |
| ERROR | 接口参数、身份、阶段或时钟非法;修正调用,不跳过未消费字节 |
| FATAL | 冻结连接,停止正常收发并进入收尾 |
保留完整帧时再次Feed消费0字节,不能把未消费尾部丢弃或忙循环反复调用。一次调用在重试中释放旧帧时也不顺带消费新输入,调用方仍按consumed推进。
Receiver在APDU边界遇到非0x68字节时立即返回FRAME_ERROR,不跳过垃圾重新同步。完整I帧先通过Parse_APCI检查APCI和6字节ASDU公共头空间,再经Parse_ASDU_Header按已知固定长度Type ID、VSQ数量和SQ位检查最低载荷长度。任何格式或最低长度失败均由Connection登记FRAME故障并返回FATAL,随后TcpServer收尾;该帧不会推进V(A)/V(R),其携带的ACK不生效,也不会生成I_FRAME事件。未知/变长类型及零对象的业务合法性仍由应用检查。
I/S统一交给Manager检查序号及累计ACK。I帧受理后设置`rx_link_applied`,后续Tick、Prepare或重复Feed只报告当前帧,不能重复推进V(R)或释放发送原文。S帧没有待业务读取的ASDU,受理后自动释放。
子站受理STOP后立即处于STOPPED/STOP_DRAINING,禁止新I帧;后到START成功受理才重新开放。非法I帧不推进V(A)/V(R),不设置rx_link_applied,不交付业务,Connection冻结并由TcpServer断链。停止等待期间仍处理合法S确认。
`Peek_Received_Frame`向TcpServer返回`IEC104_ConnectionReceivedFrame_t`,包含完整APDU、APCI及ASDU借用地址。TcpServer把完整APDU复制到I_FRAME事件后,用本次`receive_serial`和连接代次调用`Release_Received_Frame`,然后内部视图失效。Release只归还接收空间,不再次处理ACK。相同代次的旧Release也会被编号检查拒绝,不能误释放下一帧。业务通过NextEvent取得独立副本,再用Codec或自己的解析器读取ASDU;链路受理不代表ASDU格式或业务执行已经成功。
普通通知满时仍可处理排在前面的S/U;I帧无处交付时保留原帧并暂停续收。启停使用独立配额:TcpServer通过Set_Transfer_Observer安装内部容量查询和通知复制函数,Connection在改变状态的ACT受理前查容量,满时WAIT保留原帧;状态改变后立即复制通知,再继续检查期限。该适配不调用业务、不重入修改接口,仅只读取得状态快照。重复ACT不产生状态变化,无须新槽。已有发送及协议期限继续推进;NextEvent释放容量后安排Process重试。
U帧路由:
| U帧 | Connection动作 |
|---|---|
| STARTDT_ACT | 先预留控制槽,成功后立即STARTED/IDLE、排CON并取消旧STOP总计时;WAIT保留原请求和状态;实际变化立即通知 |
| STOPDT_ACT | 受理即STOPPED/STOP_DRAINING,清空HIGH并关闭新I帧;停止确认满足后排CON;不等待首个START_CON完成 |
| TESTFR_ACT | 排队TESTFR_CON;控制槽满时保留当前请求 |
| TESTFR_CON | 经Manager.Handle_Testfr_Con提交;OK时结束本端探测t1,IGNORED时保持计时并释放接收帧;不占用控制槽 |
| STARTDT_CON / STOPDT_CON | 格式合法时消费并忽略;不应答、不改变启停或确认期限、不刷新t3,也不产生业务通知 |
反向启停CON在查询启停通知容量及准备控制应答之前释放,不占通知或控制发送槽,不因自身等待资源而阻挡后续输入。该例外只适用于通过Codec完整格式校验的两种CON,其他格式错误及I/S序号、ACK错误仍按原规则冻结连接。忽略后照常执行期限检查和已有控制义务,已到期的真实故障仍可结束连接。
子站START受理后立即开放I/S处理,新的本端报文仍须排在已有CON之后。任何阶段后到START成功受理都立即激活,不再暂存;先前已经排队的STOP_CON仍按FIFO写出,完成不能再次停用链路。
首个START_CON尚未完成时收到STOP,立即停用并推进确认;原CON及部分写入位置保留,不以STOP_CON替换它。Advance_Control每轮最多推进两项,使已覆盖接收数据的S与STOP_CON可依次排队,不必等S完成才生成CON。
START成功受理取消旧STOP计时;新的STOP以原received_at_ms开始总期限。STOP_CON入队时保存其frame_id,只有匹配的最后一帧完成才能结束该停止期限;旧CON完成不影响后续请求。重复STOP不延长有效期限,WAIT尚未受理也不改变状态。取消停止不恢复已丢弃HIGH,不重置I帧t1、S快照和本地发送期限。
## 4. 实际发送
`Submit_Response(connection, generation, asdu, length, now_ms, &head_info)`供TcpServer提交当前请求的应答。它拒绝旧代次、无效原文、已关闭会话和未结束的接收批次;FAILED返回FATAL。先只读查询协议等待时间:期限已到时调用Manager.Enqueue(HIGH),通过启停准入后仅复制到HIGH,交回Process先处理socket中的ACK,再检查是否超时,避免应答入口抢先判定确认丢失。
未到期路径先Drive,再由Manager判断实际准入;期限已到路径直接Enqueue(HIGH),也使用同一准入函数。两条路径均只在STARTED/IDLE接手,其他状态在容量检查前ERROR且不入HIGH;拒绝不冻结或断开连接。已有控制帧时应答暂存HIGH,不越过FIFO发送。
返回SEND_READY时head_info为新应答帧,TcpServer只尝试一次非阻塞写入;OK表示已缓存HIGH;WAIT表示尚未接手;ERROR表示输入/状态/容量不符;FATAL进入收尾。成功后原文已复制,返回后不再借用业务输入。旧HIGH、当前帧、窗口、记录容量及公平配额都由Manager检查,不在Connection重做调度。
STOP受理清空尚未组帧的HIGH,即使它们此前已返回OK;没有逐条作废事件。当前I帧和记录继续完成及等待ACK,NORMAL保持原生命周期。从停用状态重新START仍清理HIGH并重置配额,已启动时重复START保留HIGH。
`Prepare_Next_Send`先重试保留接收帧、检查本地发送及STOP期限并推进控制义务,再向Manager取得待发送数据。**只有SEND_READY时head_info有效**;EMPTY表示没有当前连接可发送的项(可能仍有EOF后留给下一连接的未编号NORMAL),WAIT表示门控、窗口、容量阻塞或旧确认t1已到。TcpServer在EOF时通过AsduQueue停止选取全部未编号NORMAL,已有当前帧及HIGH/控制发送仍走本接口,不在Connection另设EOF状态。接收WAIT不阻止已有发送帧继续写出。发送阶段不直接报告I帧ACK或TESTFR_CON超时故障,调用方须按Get_Wait_Time及时接收并Tick,不能只调用发送接口。
TcpServer在WAIT之后转移已确认记录,若实际释放了记录槽,须重新调用Prepare_Next_Send,不能沿用释放前的WAIT判断EOF收尾。若仍WAIT,内部只读接口`Get_Record_Wait`区分本地可恢复的记录容量等待:记录槽已满、最早记录已完整写出且已ACK,同时Link的`Can_Prepare_I_Frame`允许新I帧,且上次准备时刻没有到期的确认t1,才报告`record_wait=1`。Link查询与实际构帧共用Plan_I_Frame,不在连接层另写启停、k窗口或序号边界规则。查询不推进协议、不移除记录、不分配N(S),仅OK写输出;只用于紧随WAIT的EOF收尾判断,不能替代实际发送准备或期限调度。
准备新I帧前,Connection使用`Collect_Deadlines`取得Manager报告的当前帧存在状态和确认期限。已有I/S/U继续按原顺序发送,包括已编号但尚未写出字节的当前I帧,不能撤回或交错。没有已有帧时,`Get_Confirmation_Timeout`从同一视图选择I帧ACK或TESTFR确认超时:任一等待达到t1就返回WAIT,不调用Manager选择HIGH/NORMAL,不消耗N(S)、记录槽或待发条目。此限制在接收批次内、预算续收期间及常规发送阶段都有效。有效确认解除全部已到期等待后自动恢复;部分ACK若留下另一项已到期等待,仍保持限制。不新增持久状态、恢复计时器或固定宽限时间。
TcpServer把`head_info.pending_data`至`remaining_bytes`交给Net写操作,再把本次成功写入的增量及原`frame_id`传给`Handle_Send_Progress`。同一连接最多一个未回报的写操作。部分发送必须继续当前APDU,不能交错发送另一帧;若后续替换为异步传输,也必须在结束I/O借用后才能调用Connection_Close。
| 写入结果 | 提交方式 |
|---|---|
| 成功写入部分字节 | 回报本次增量,返回OK,保留剩余字节和原期限 |
| EAGAIN / 暂不可写 | 可回报0或等待下次可写事件;继续调用Tick,不延长期限 |
| 写完剩余字节 | 回报实际增量;FRAME_WRITTEN表示对应Link完成、记录更新及缓冲释放均完成 |
| 最终发送错误 | Handle_Send_Failure,匹配当前帧时提交失败并冻结连接 |
完成回报会自动重试受阻的接收请求和控制过程。因此极少数情况下,写入已记账后,后续接收/控制处理失败使本次返回FATAL;此时不能重报写入增量,应冻结后读取记录判断事实。completion仅在FRAME_WRITTEN时写入。
`Handle_Send_Progress`使用实际写入后的单调时刻登记进度、整帧完成及TESTFR起点,不借用send前的旧时刻。它与Submit_Response、Prepare_Next_Send的内部Drive只检查本地发送和STOP期限,继续推进保留帧及控制义务;I帧ACK和TESTFR_CON的t1交给接收阶段判定。这样,即使提交应答时尚未到期、send返回后已经到期,也不会在处理socket中待解析的确认前直接冻结连接。TcpServer安排立即续跑,按原始期限处理确认或报告超时,不增加恢复期限或宽限时间。
本地已知整帧完成必须先回报,再处理覆盖该I帧的ACK,或处理依赖本次发送完成的U帧。异步事件循环须保证这一提交顺序;不能用收到ACK来推定本地发送成功,也不能在Connection之外再次调用底层Complete。
## 5. 时间与驱动
所有`now_ms`使用同一单调毫秒时钟,允许0,按本连接调用顺序不倒退。时钟倒退返回ERROR;旧代次事件在改变时刻前被拒绝。系统定时器只负责唤醒,`Tick`按连接保存的当前过程起点重新计算,不接收旧回调保存的时间差。Tick不准备新的业务I帧。
| 期限/门限 | 起点与结束条件 |
|---|---|
| 本地发送期限 | 当前帧原queued_at_ms;0字节、部分发送和重试不续期;到期提交发送失败 |
| I帧t1 | 最早已整帧完成但未ACK记录的written_at_ms;ACK推进后采用下一记录的原始起点 |
| TESTFR t1 | 本次TESTFR_ACT整帧完成时启动;匹配CON受理后结束;其他收发活动不续期 |
| w/t2确认 | 已受理I帧数量达到w,或连续未确认区间达到t2时安排S;S完成或I捎带完成后更新确认位置 |
| t3空闲 | 除反向STARTDT_CON/STOPDT_CON外,完整合法APDU按成帧时刻更新活动;本地整帧完成也更新;半包、0字节、Peek和WAIT重试不刷新 |
| STOP总期限 | 本轮STOP受理时启用,以原成帧时刻received_at_ms为起点,包含受理前WAIT,至本轮最后一帧STOP_CON完整写出;成功受理START也结束旧期限;重复STOP及旧CON完成均不续期或误清新期限 |
未受理的STOP不会提前设置`stop_timer_active`;受理时才用保留的原成帧时刻建立期限。因此受理前的WAIT已经用尽总期限时,受理后的同轮超时检查即可报告STOP_TIMEOUT。
`Connection_Get_Wait_Time(connection, generation, now_ms, max_wait_ms, &wait_ms)`供TcpServer_Prepare查询协议等待上限,Poll也复用相同查询。它只读汇总上述期限,把等待时长限制在0..max_wait_ms;已到期取0,没有更早期限则保留调用方上限。仅OK修改输出;无效参数/代次、倒退时钟、内部状态异常或尚未结束的接收批次返回ERROR,FAILED返回FATAL。查询不修改last_now_ms或任何计时起点,不准备报文、不登记故障,也不替代Tick;业务不直接调用此内部接口。
`Collect_Deadlines`是本层唯一的期限收集入口,超时检查、发送准入、EOF记录等待判定、控制推进及Get_Wait_Time共用它。Manager.Get_Deadlines提供队首身份、本地发送剩余时间、最早已完成未ACK记录的t1剩余时间以及控制容量;底层各帧的原始时间戳继续由原模块保存。Connection汇入自身TESTFR、STOP、t2、t3的剩余时间,只在栈上形成临时视图,不保存同步副本。活动标志与剩余时间分开,起点0合法,到期剩余时间为0。
控制推进后会重新收集期限,避免继续使用排入S/STOP_CON前的容量或准入状态。t2/t3同时经过Link的Can_Prepare查询和Manager报告的control_has_room判断;已有S在途、探测正在进行或控制槽满时,不因不可推进的旧t2/t3不断唤醒。门控或容量恢复后按原起点重试。时间计算先校验时钟再作64位差值,不用起点加期限。
Check_Timeouts保持本地发送、I帧ACK、TESTFR、STOP的故障优先顺序。本地发送到期时使用同一次查询取得的frame_id向Manager报告失败。接收批次仅决定何时执行确认超时故障;期限视图仍报告实际到期,因此发送准入和等待时间不会把暂缓误当作续期。
t2采用固定存储:保存当前连续未确认接收区间的最早到达时刻,只有Link快照报告`unconfirmed_rx_count == 0`时清除,含义是全部已收I帧都已实际发出确认。S或I仅确认区间前缀时,剩余部分沿用原起点,可能提前发S,不会因部分确认把等待时间向后推。START_CON是否已完成不再限制S准备,实际S发送仍排在已有CON之后。t2到期时Connection请求立即确认,是否允许构帧仍由Link判断。
Feed在接收批次之外先提交已到达的接收事实,再检查全部期限;发送准备和写入回报仅因本地发送及STOP期限直接报告超时,对端确认t1故障由接收阶段判定。发送准备仍查询确认是否到期,用于暂停新I帧准入,不提前冻结连接。同一时刻若先调用Tick触发FATAL,后续事件不能恢复该连接。TcpServer在Process的接收阶段结束时驱动Tick并按实际事件顺序串行提交,Poll复用该推进核心。发送后若查询到期限已到,则登记process_pending,下一轮立即接收;Prepare也始终把到期的协议等待时间报告为0。业务根据TcpServer_Prepare返回的socket/定时/本地条件调度Process,或直接使用Poll。Tick的OK不表示发送缓冲为空,TcpServer后续仍调用内部Prepare_Next_Send检查待发送内容;公开TcpServer_Prepare只查询调度条件,不推进协议。
TcpServer在Process的接收阶段调用`Begin_Receive_Batch(connection, generation)`,然后在预算内Feed、复制I_FRAME事件并释放内部帧,按需完成非阻塞发送。应用将同一轮就绪和定时通知合批交付;批次内只暂缓I帧ACK和TESTFR_CON的t1判定,避免前面的其他报文先触发超时,使后面的有效确认来不及处理。本地发送期限、STOP总期限及格式/协议错误仍立即处理。没有调用Begin时,Feed仍执行即时的完整超时检查。
同一轮接收必须以`Tick`或`Yield_Receive_Batch`结束;重复Begin返回ERROR,Close清除全部批次状态。2026-10-20起,仅因调度预算让出且还有已到达输入时,TcpServer先固定用户态尾部和内核待收字节数,再调用Yield结束当前轮次。`receive_batch_pending`只暂缓对端确认期限;Get_Wait_Time返回0,调用方必须立即续收,不能跨阻塞等待,也不得把后来到达的输入加入旧快照。本地发送和STOP总期限、控制推进及协议错误不受该标志影响。
快照耗尽、接收无进展或真正容量背压时必须调用Tick,清除续收标志并检查全部期限,不能借此等待业务消费或推迟无ACK连接的失败。接口不修改任何期限起点,也不判断内核实际收到字节的时刻。未调用Begin/Yield的Feed保持原行为,发送接口不替代接收及Tick的确认期限检查。异步接入仍须先提交已经完成的本地写入,再喂入依赖该完成的ACK。
## 6. 故障、关闭与恢复
Connection状态为`ACTIVE -> FAILED -> CLOSED`,正常收尾也可从ACTIVE直接Close。ACTIVE只表示可以处理当前连接事件,不表示已经STARTED。
`Connection_Get_Status`组合各模块的数值快照,允许在FAILED/CLOSED时查询;参数、代次或模块状态非法返回ERROR且不改输出。CLOSED不再读取已解除绑定的Receiver/Manager,接收与记录数量为0,链路字段仅为历史值。记录回收接口只要求Manager存储合法,不因Link运行状态异常而阻断读取。
格式/链路错误、发送失败和期限到达会记录首次故障并进入FAILED,后续普通收发入口返回FATAL。此时不自动清空发送记录或NORMAL原文:`Get_Record_By_Offset`可读取全部记录,`Peek_Confirmed_Record`及Remove仍可消费已确认回执,已受理I帧也仍可Peek/Release。`Get_Status`返回阶段、首次原因、启停状态和保留帧概况。这些接口均由TcpServer在内部使用,向业务交付公开的记录事件和状态快照。传输异常由TcpServer调用`Handle_Transport_Closed`冻结;有序EOF采用正常收尾路径,复制剩余记录后调用Connection_Close,不新增故障。未ACK业务仍须根据事件中的记录字段处理。
故障分类按失败来源保留:Receiver的起始字节、长度/APCI或ASDU最低长度错误为FRAME;Manager明确返回PROTOCOL_ERROR的I/S接收拒绝为PROTOCOL。格式合法的反向STARTDT_CON/STOPDT_CON消费并忽略,不登记故障。本地会话或记录预检失败、有效ACK受理后的原文释放失败、合法U帧的控制交接失败均为INTERNAL。START/STOP/TESTFR的合法重复或无匹配确认继续按既有规则受理或忽略。实际发送失败登记TRANSPORT,各类超时保留独立分类;后续清理异常不覆盖已经登记的首次故障。
TcpServer内部的收尾与恢复顺序:
1. 停止实际收发,关闭/取消并等待旧I/O结束,确保没有传输仍借用缓冲。
2. 在Connection_Close之前复制剩余I帧、发送记录及断开通知到事件队列;记录区分本地完成和peer_acknowledged,未ACK不能证明对端未收到。
3. 调用`Connection_Close(旧代次)`,解除内部借用。重复Close同代次返回OK,外部ASDU队列保留。
4. 调用`AsduQueue_Detach(旧代次)`清空HIGH、恢复现存NORMAL待发状态并解除绑定;代次0期间继续接受NORMAL,已覆盖原文无法恢复。转换失败须停止服务,不得重新Init覆盖缓存。
5. 新连接建立时调用`AsduQueue_Rebind(新代次)`,再调用`Connection_Init(新代次)`。新连接重新进行STARTDT,不沿用旧序号;首次连接同样绑定服务启动时建立的队列。
同一TCP上的STOP/START不调用Rebind。旧代次的Feed、发送完成、Tick、Release和Close均不能影响新连接。代次检查解决身份隔离,不能代替结束旧I/O或同步线程;调用方仍须保证被旧I/O引用的内存不会提前复用。
## 7. 构建与边界
Connection部分的生产构建依赖包含:`IEC104_Connection.c`、`IEC104_Receiver.c`、`IEC104_SendManager.c`、`IEC104_SendBuffer.c`、`IEC104_AsduQueue.c`、`IEC104_Codec.c`、`IEC60870_Codec.c`、`IEC104_Link.c`和`IEC104_Link_Slave_StartStop.c`。不依赖主站启停实现、旧全局接收器或日志模块;完整TCP构建还需TcpServer及所选Net后端。
本层负责单条子站连接的内部模块协作。`IEC104_TcpServer.c/h`由Slave持有,负责监听调度、连接生命周期、收发预算和业务事件队列;网络操作及单调计时通过`IEC104_Net.h`执行,当前提供`IEC104_Net_Windows.c`后端。应用使用Slave_Prepare/Process接入自己的事件循环,或使用Slave_Poll组合等待和推进,通过Slave_NextEvent取得ASDU及应答接口;TcpServer在复制事件成功后统一释放协议帧和记录。初始化及收尾见[TCP服务设计](IEC104_TcpServer_设计架构.md)。Connection不依赖Net或操作系统接口;借出的socket仅供应用注册,真实I/O仍在TCP内部,业务不直接驱动Connection。ASDU信息对象处理、业务去重及冗余组切换仍由业务层负责。旧`IEC104.c/h`保留作为历史接口,不参与当前TcpServer接入路径,同一会话不能重复喂给旧全局接收器。
@@ -0,0 +1,48 @@
# IEC104 计数量召唤
`IEC104_CounterInterrogation.c/h`实现 Type 101(C_CI_NA_1)的规则与创建入口,状态机、事件、广播、期限及发送背压由`IEC104_Interrogation`共用引擎维护。总召和计数量召唤各有实例,忙状态独立;共享连接、HIGH队列和发送窗口,任一业务关闭连接都会影响另一业务。
## 1. 请求与数据
请求为104的2/2/3地址布局、单对象、SQ=0、IOA=0、COT=6。QCC低6位RQT为1..4分组或5总请求,高2位FRZ为0读取、1冻结、2冻结并复位、3仅复位。COT=8不支持,返回未知传送原因45。未知CA和非零IOA分别返回46、47;限定词、P/N、忙状态及可可靠回显的畸形单对象请求回否定ACT_CON。无法对应单对象的请求丢弃并统计。输入原文不修改。
读取数据限Type15(SQ=0/1)和Type37(SQ=0)。数据COT使用`request.data_cause`:RQT=5为37,分组1..4为38..41。CA必须为事件中的实际站地址,OA、TEST保持原请求,P/N=0。249字节上限分别容纳30、48、16个对象,并受配置对象数限制。IOA分组、冻结值、BCR序号和品质由应用负责。
## 2. 两种完成方式
| 请求 | 应用处理 | 线路顺序 |
|---|---|---|
| FRZ=0 | BEGIN接受;NEED_DATA提交冻结值;Complete_Data结束 | ACT_CON → 数据批次 → ACT_TERM |
| FRZ=1..3 | BEGIN中通过Execute_Begin执行动作 | 动作完成 → ACT_CON;随后FINISHED |
冻结及复位不产生NEED_DATA,也不发送ACT_TERM。流程依据IEC60870-5-101:2003 §7.4.8模式C的冻结/冻结并复位流程;读取另按模式B进行。目标主站若要求额外终止帧,应作为单独兼容需求评估。Type16及撤销不在104计数量模块的支持范围内。
因此拿动作的BEGIN去调用Submit_Batch会因事件类型不匹配返回STALE,不会发送数据;ERROR仍用于有效NEED_DATA的非法批次。这与最初“无数据也产生NEED_DATA”的草案不同。
`gi_submitted`在计数量实例中统计成功提交完成响应的事务:读取的ACT_TERM或动作的ACT_CON;不是主站ACK计数。总召实例原含义不变。多站读取仍为全部确认、逐站数据、全部终止;多站动作逐站执行和确认,每站独立FINISHED,不保证同时冻结或失败回滚。
## 3. 动作执行与失败边界
应用用`IEC104_CounterInterrogation_Execute_Begin(self, event, action, context)`处理FRZ=1..3。回调签名为`bool action(void *context, const IEC104_InterrogationRequest_t *request)`。接口先检查实例、事件、会话及期限;WAIT/STALE不会调用动作,事件消费后再次调用不会重复执行。TEST动作请求被否定,不调用动作。
回调必须同步、短时完成,不得重入协议接口或推进网络;返回false表示未改变计数器,库生成否定确认。应用须先完成分组、资源等检查,再整体提交修改,不能部分清零后返回false。成功后确认等待不再因业务期限变成否定确认;传输层的发送期限、STOP和断线仍有效。已执行动作不会因连接断开或本地Cancel而撤销。
这个接口适合内存计数器或已准备完成的本地原子提交,不提供异步外部电表事务。异步冻结需要另行管理设备命令、迟到完成和不可撤销结果,不能简单“先发设备命令,稍后Reply_Begin”。主站重新发送一个新请求仍可能再次冻结;本模块不提供跨请求去重或断电持久化事务。
读取的BEGIN继续使用`Reply_Begin`;拒绝未配置分组也使用该接口。不得在可重试的BEGIN处理分支直接清零,再调用Reply_Begin。
## 4. 数据一致性与接入
一次读取必须固定同一冻结版本,包含值、序号、品质及可选时标。应用可复制快照、持有不可变版本,或阻止读取期间改写相关冻结区。WAIT后重组包仍取同一版本。多个Slave共享数据源时同样需要协调,不能只避让本地定时器。Type37时标由应用给出,本模块不读取墙钟。
创建顺序为Slave、站点、总召实例、计数量实例、Slave_Init。计数量调用`IEC104_CounterInterrogation_Create`,配置沿用Config_Default;使用多站广播时,两个实例必须各有独占状态数组。所有连接/启停通知交给两个实例;I帧依次交总召、计数量,均IGNORED才兜底。
每轮先推进Slave并读空通知,再推进两种业务并读取事件。合并两个实例的need_process、events_pending及最小剩余期限;发送WAIT后网络有进展时必须重试。示例轮换两实例的先后次序,避免固定优先级长期占据小窗口。退出时两实例分别Cancel、读完FINISHED、Destroy,然后释放站点与Slave。
## 5. 构建与边界
引擎依赖只读规则,不引用具体种类符号。总召工程加入`IEC104_Interrogation.c`和`IEC104_GeneralInterrogation.c`;计数量再加入`IEC104_CounterInterrogation.c`。只用计数量时不需要总召规则文件。默认实例池为TCP服务容量的两倍;只用一种业务可显式定义`IEC104_INTERROGATION_CAPACITY=1`。
规则表及Create_With_Rules属于内部接口;业务使用两个公开创建入口及共用驱动接口。现有总召Create签名保留,所有调用方重新编译。该引擎仍绑定104的Slave、传输及地址布局,尚不是101串口业务适配层。
可运行验证入口及当前记录见[测试说明](../test/IEC104测试文档.md),模拟计数器操作见[示例说明](../examples/IEC104子站示例.md)。
@@ -0,0 +1,458 @@
# IEC104 总召业务模块
`IEC104_Interrogation.c/h`在`IEC104_Slave`之上实现共用召唤引擎,Type100规则和原Create入口位于`IEC104_GeneralInterrogation.c/h`。通信服务负责连接、启停、收发和会话身份;引擎负责请求准入、业务阶段、数据批次检查、应答调度及终结通知。本文描述总召行为,应用通过`BEGIN / NEED_DATA / FINISHED`事件提供点表和数据;计数量的差异见第12节。
当前采用**每个实例至多一个未终结总召,忙时否定所有新的总召激活**。空闲时支持全站和全部16个分组,忙时不区分相同或不同QOI。任务池仍保留4个槽,用于一个总召及待发送短应答;没有后续总召等待队列,也没有独立排队超时。
本文描述库本身的设计,不依赖应用模拟点表或可执行程序。完整接入位置见[完整调用流程](IEC104_完整调用流程.md#flow-interrogation),函数声明及返回值汇总见[API接口汇总](IEC104_API接口汇总.md#file-21)。
## 1. 职责与调用关系
所有接口由所属事件循环串行调用。模块不创建socket、线程或系统定时器,不动态分配内存,也不会在`Process`中跳转执行应用代码。异步采样可以由应用自己的线程或设备驱动完成,但结果须回到所属事件循环后提交。
```text
应用事件循环
├─ IEC104_Slave:Poll / Prepare / Process / NextEvent
│ └─ TcpServer → Connection → Link / Receiver / SendManager → Net
└─ IEC104_Interrogation
Handle_Event ← 应用逐条转交Slave通知
Process → 沿保存的原连接身份提交确认、终止及短应答
Submit_Batch → 沿保存的原连接身份尝试直接提交数据,不入队等待
NextEvent → BEGIN / NEED_DATA / FINISHED
Reply_Begin / Submit_Batch / Complete_Data / Fail_Request ← 应用显式应答
```
| 模块 | 自己维护的规则 | 与总召的关系 |
|---|---|---|
| Slave | 服务生命周期、事件来源、连接代次、启停及提交结果 | 总召通过公共接口取得原身份、校验会话和提交应答 |
| TcpServer及内部通信模块 | TCP字节流、APCI序号、k/w、t0..t3、HIGH容量和发送完成 | 总召不直接操作socket、链路状态或发送窗口 |
| Interrogation | 一个总召的准入与阶段、短应答、业务事件及业务期限 | 不持有应用点表,不主动读取设备 |
| 应用 | IOA、分组成员、数据源、遍历状态、资源清理 | 根据业务事件决定接受、逐批取值交付或终结数据 |
`Slave_Get_Event_Identity`校验事件来源;`Slave_Check_Request`判断是否有未读通知、原会话是否仍可提交。总召不自行拼凑TCP状态快照判断连接是否有效。应用应使用Slave和总召公开接口,不访问不透明对象内部字段。
## 2. 对象、身份与固定存储
`IEC104_Interrogation_t`是不透明的静态池句柄,通过`Create`取得,`Destroy`归还。一个Slave只能绑定一个总召实例;须在开始消费Slave通知前创建。配置按值复制,调用方配置返回后可复用;Slave须存活到总召实例销毁之后。
设置config.use_station_registry=true后,总召按请求CA选取本Slave的已注册站点,65535逐站遍历;config.station须NULL,common_address忽略。当前任务持有节点引用,删除使该站立即退出新请求路由,已有任务继续使用原节点。BEGIN携带station_id,应用跨循环保留context时应立即按编号查节点并Acquire,FINISHED清理后Release,防止同CA替换串站。默认false兼容原单CA调用,可选config.station仍固定绑定一个站点并持有到Destroy。注册、引用、延迟回收及快照契约见[共用站点表](IEC104_API接口汇总.md#station-registry)。
| 存储 | 默认容量 | 保存内容与约束 |
|---|---|---|
| 召唤实例池 | `2 * IEC104_TCP_SERVER_CAPACITY` | 同一Slave的总召及计数量各一个;各实例会话、配置、任务及事件独立 |
| 每实例协议任务池 | 4槽 | 至多一个未终结总召;其他槽仅保存单帧应答,也允许4槽全是应答 |
| 每实例业务事件FIFO | 4槽,最小2槽 | 单目标预留FINISHED位置;多目标终结记录保存在调用方状态区,按需交付 |
| 多站广播状态区 | 调用方提供,每目标站一项 | 保存站引用、身份、计时及进度,不保存点值或数据ASDU |
| 总召数据存储 | 不设置私有批次缓冲 | WAIT不接手数据;OK时发送层已复制到当前APDU |
编译实现文件时可覆盖`IEC104_INTERROGATION_CAPACITY`、`IEC104_INTERROGATION_TASK_CAPACITY`、`IEC104_INTERROGATION_EVENT_CAPACITY`。增加任务槽只增加短应答积压容量,不允许第二个总召。任务池设为1仍可工作,但当前总召占槽且短应答不能立即提交时,新请求只能丢弃并计数。
协议任务只保存`InterrogationResponse`:由`Slave_Get_Event_Identity`取得的原服务/实例/连接代次,加10字节Type100 ASDU。合法请求复制原文并处理应答CA;可可靠应答的畸形单对象请求只保存规范为10字节、VSQ=1的否定应答副本,不执行激活或撤销。错误IOA、QOI、OA、TEST等字段仍完整保留。每次发送临时建立ASDU视图,调用`Slave_Send_Response`或`Slave_Try_Send_Response`;任务不保存完整SlaveEvent或自引用接口,源事件出队后可立即复用。固定任务槽及`order[]`调度规则保持不变,非队首短应答完成后仍立即释放对应槽。
2026-10-27按默认4任务槽、4业务事件槽测量,Windows x64 GCC的`sizeof`如下;这是结构存储尺寸,不是STM32的链接占用,Keil结果须另行实测。
| 项目 | 精简前 | 精简后 |
|---|---:|---:|
| 每任务请求存储 | 400字节(完整SlaveEvent) | 40字节(原身份及10字节ASDU,含对齐) |
| 每任务 | 480字节 | 120字节 |
| 每实例4个任务合计 | 1920字节 | 480字节 |
| 每个总召实例 | 2400字节 | 960字节 |
每实例减少1440字节(60%)。任务槽数、业务事件FIFO、TCP通知环、HIGH/NORMAL容量、k窗口及事件覆盖策略均保持原样,节省来自请求表示,不减少背压容量。立即应答也不再在栈上复制完整SlaveEvent;GCC `-O2 -fstack-usage`中该函数从464降至96字节,但这不是ARMCC或完整调用链的实测栈水位。
上表保留请求存储精简阶段的比较。当前Windows x64默认配置实测:总召1240字节、每任务168字节、紧凑请求40字节、业务事件80字节、配置40字节。相对逐站完整执行版本的1192字节,管理对象增加48字节;多目标广播另需调用方提供每站48字节的状态区,13站为624字节。单目标不要求状态区。广播共用一个任务及一套应用取数游标,HIGH/NORMAL和任务/事件容量不变。数值不是目标板RAM实测;站点及Slave开销见[共用站点表](IEC104_API接口汇总.md#station-registry)。
业务事件是纯值结构,可以复制和跨循环保存。`instance_id`区分静态池槽的历次使用,`request_id`在实例内递增,`batch_index`区分本次取数交互;编号耗尽时拒绝新建,不回绕复用。TCP的`generation`属于另一层身份,防止旧请求向新连接发送。
## 3. 请求校验与单总召准入
### 3.1 接收范围及校验顺序
总召使用`C_IC_NA_1`,Type=100、SQ=0、一个对象、IOA=0。支持激活COT=6和撤销COT=8,请求P/N须为0,CA可为已注册站地址或广播65535(0xFFFF);兼容模式只接受其配置站。实际站CA限于1..65534。QOI=20为全站,21..36为第1..16组;分组内容由应用维护。
先校验事件身份,再保护长度并读取类型字节。非100返回`IGNORED`,总召不代替其他业务发送UNKNOWN_TYPE;公共ASDU视图不可用时也返回`IGNORED`,由公共入口处理。正常网络路径的公共头及最低载荷已由Receiver检查,真实截短/帧错误仍沿连接层原策略处理。Type100先判断是否存在唯一完整对象,可靠时按`COT → CA → IOA → 布局/QOI/P/N → 事务状态`处理,避免忙状态掩盖地址或传送原因错误。
| 条件 | 结果 |
|---|---|
| Type不是100,或公共视图不可用 | `IGNORED`;公共入口应先过滤不可用视图,再分派其他业务 |
| Type100对象数为0或大于1,无法确定唯一完整IOA/QOI | 计入rejected并返回`HANDLED`,丢弃新请求,不生成应答 |
| Type100单对象完整,但SQ=1或有多余尾字节 | 仅回否定COT=7/9,COT/CA/IOA错误仍优先45/46/47;不执行激活或撤销 |
| COT不是6或8 | 回COT=45,P/N=1 |
| CA既不是本站地址也不是65535 | 回COT=46,P/N=1,保留请求中的错误CA |
| IOA不是0 | 回COT=47,P/N=1 |
| 激活QOI超范围或P/N非法 | 回否定ACT_CON,COT=7、P/N=1 |
| 撤销QOI超范围、P/N非法或没有匹配对象 | 回否定DEACT_CON,COT=9、P/N=1 |
| 合法激活,已有未终结总召 | 回否定ACT_CON,不替换原请求,不建立第二个总召 |
| 合法激活,空闲且有任务槽 | 保存请求、分配request_id,进入START |
| 合法激活,空闲但任务槽被短应答占满 | 尝试否定确认;不能提交或保存时丢弃新请求并计数 |
确认和错误应答调整COT及P/N;请求CA为65535时,应答CA改为本站地址,其他CA原样保留。OA、TEST、IOA和QOI保持原文,包括错误请求中的原值。畸形单对象的否定副本规范为10字节、VSQ=1,原输入事件不修改;该容错应答是本项目策略,不是将畸形命令视作有效命令。忙拒绝同样使用否定ACT_CON,没有另设“忙”的专用传送原因。
上述请求错误不会关闭TCP、取消已有总召、重置原期限或生成其FINISHED(INVALID_REQUEST),其他CA任务继续。否定应答复用原HIGH/FIFO背压与满槽丢弃策略,不扩大任务或缓冲容量。无唯一对象的丢弃只计入rejected;dropped_requests仍专指无法安全提交/保存的容量丢弃。内部服务故障、会话失效及发送失败继续走原失败路径。
### 3.2 忙状态的起止点
忙状态从合法激活被保存为START开始,早于生成BEGIN和提交肯定ACT_CON。因此,同一批网络通知连续包含多个激活时,只有第一个可建立总召,后续请求立即进入否定应答路径。不能等到应用接受后才设置忙,否则等待应用期间会接入多个总召。
忙状态覆盖BEGIN尚未生成、等待应用接受、确认等待发送、等待数据、有效批次未获发送机会及终止等待发送。相同QOI、不同QOI、不同OA或TEST的新激活都不能覆盖原请求。
| 终结方式 | 何时释放总召资格 |
|---|---|
| 正常完成 | ACT_TERM被发送层接手,释放任务;不等待TCP写完或主站ACK |
| 应用拒绝BEGIN | 生成FINISHED(REJECTED),原任务转为否定REPLY |
| 肯定确认提交前超时或BEGIN数据源失败 | 终结原因保留为对应TIMEOUT或SOURCE_ERROR,原槽转为否定REPLY,保留TCP及其他任务 |
| 主站匹配撤销 | 结束原业务,原任务转为肯定撤销REPLY |
| 本地Cancel、STOP、断线或确认后失败 | 取消总召及本模块待发短应答,保留应有的FINISHED |
单站终结并转为REPLY后不再占用总召资格;多目标广播中某站拒绝、超时或定向撤销时,仍为其他站保留总召资格。广播网络流程结束后进入FINISHING,gi_pending为false,但仍占用状态区并忙拒绝新激活,直到全部FINISHED出队。此前被拒绝的请求不会自动启动,主站需要重新发起。
### 3.3 广播地址与本站地址
广播CA表示请求面向所有站。`IEC104_Types.h`以`IEC104_CA_BROADCAST`定义0xFFFF(65535),它不是实际站地址。开启共用站点表后,多目标广播采用c104的三阶段顺序:先按注册顺序完成各站BEGIN及ACT_CON,再逐站处理全部数据,最后逐站ACT_TERM及FINISHED。例如两站线上顺序为CON1、CON2、DATA1、DATA2、TERM1、TERM2;每站数据可有多批或为空。各站有独立request_id、进度和期限,但共享一个任务和一个取数位置。兼容单站及仅含一个目标的广播保持单目标流程。
接收合法多目标广播时,把目标节点引用保存到`config.broadcast_states`;`broadcast_capacity`须覆盖当时的注册站数。数组由调用方持有至Destroy,地址稳定,不得读写或与其他实例重叠;无需逐项初始化。容量不足在任何肯定确认前整轮拒绝,各站收到否定ACT_CON,并累计overloads,不截取部分站执行。单目标不需要此数组,注册表本身仍无固定站数上限。
目标集合不冻结IOA或点值。后来新增或同CA重建的节点下轮广播才参加;尚未选入BEGIN便被删除的站跳过。已选中站持原引用完成,尤其已确认但尚未取数的站不能因删除而静默消失;应用通过station_id取得原context。无法继续取数时显式Fail_Request。没有注册站时广播回复COT=46并保留65535。忙或错误广播仍使用共用站点快照遍历实际CA,无槽保存遍历时整体丢弃新请求并计数。
每次Process最多推进2倍任务容量步,默认8步;剩余可提交应答通过need_process继续调度。多目标每站状态区保留终结原因,NextEvent按FIFO空位逐个交付,即使13站也只需2槽事件FIFO。发送WAIT不推进当前发送项,仍须重试。确认前拒绝或超时只结束该站并继续确认其他站;确认后取数失败或超时关闭原连接,整轮取消,各已交付BEGIN都有FINISHED。CA选站、QOI选组,两者独立,广播支持20..36。
以仅注册CA=7为例(广播多站时逐站应用同一规则):
| 请求或交互 | CA的处理 |
|---|---|
| 激活或撤销CA=7 | 接受本站寻址,后续应答CA=7 |
| 激活或撤销CA=65535 | 映射到本站7,后续应答CA=7 |
| BEGIN / NEED_DATA / FINISHED | request.common_address均为7,应用无需判断原请求是否广播 |
| Submit_Batch | 必须提供CA=7;CA=65535或其他站地址返回ERROR且不消费当前交互 |
| 肯定/否定ACT_CON、ACT_TERM、肯定/否定DEACT_CON、广播请求的错误应答 | CA均为7,发送等待与确认前失败不改变这条规则 |
| 其他CA且前置校验已通过 | COT=46、P/N=1,保留原错误CA,避免伪装成本站请求 |
输入的`const SlaveEvent`始终保持原文。Request完整解析Type100后选择实际目标站,广播从首站开始;未知CA保留原值。错误原因仍按COT、CA、IOA、QOI/P/N优先检查。撤销匹配本轮活动站集合,不依赖注册表首站或当前取数位置。
内部Copy_Response_Request保存原身份及10字节ASDU,再用临时视图调用Asdu_Set_CA。广播每次切换站点才更新副本CA,同一站的WAIT重试只重建视图、设置COT/P/N并沿用原CA。副本不借用调用方事件,也不重新绑定当前连接。
任务仅保留info这一份业务元数据,数据COT、OA、TEST及CA均按当前任务info检查;CA必须等于当前目标站。错误短应答不生成BEGIN。数据批次不自动改写地址,非法批次返回ERROR供应用修正;同一IOA在不同CA下属于不同点。
`IEC60870_Asdu_Set_CA`是共用ASDU层的字段接口,按COT宽度定位CA并按CA宽度写入小端字节,支持构造容器和借用可写原文。1字节CA只允许0..255,2字节允许0..65535;参数或容器无效时返回ERROR且不修改原文。它不负责站地址的业务合法性,总召配置仍拒绝0和65535。
该规则仅属于Type100处理器。即使CA=65535,Type101等其他有效类型仍返回IGNORED,后续业务模块自行决定其寻址及应答规则。
## 4. 协议阶段与完成含义
```text
空闲收到合法激活
→ START:保存请求,已占用总召资格
→ 业务事件有空位,生成BEGIN,启动业务计时
├─ Reply_Begin(false) → FINISHED(REJECTED) → REPLY(否定ACT_CON)
└─ Reply_Begin(true) → CONFIRM
→ 提交肯定ACT_CON成功 → DATA
→ NEED_DATA → Submit_Batch → OK → 下一个NEED_DATA
→ WAIT → 保留事件和游标,重读当前值后重试
→ NEED_DATA → Complete_Data → TERMINATE
→ 提交ACT_TERM成功 → FINISHED(COMPLETED) → 释放任务
任何未终结阶段:新激活 → 否定REPLY;原总召阶段与进度不变
匹配撤销:停止当前业务 → FINISHED(已取出BEGIN时)→ REPLY(肯定DEACT_CON)
START/CONFIRM超时或BEGIN失败:终结当前业务 → REPLY(否定ACT_CON),保留连接
DATA/TERMINATE超时或NEED_DATA失败:关闭原连接,取消本模块任务,不发成功ACT_TERM
```
| 阶段 | 等待或动作 | 推进条件 |
|---|---|---|
| START | 等事件空位生成BEGIN,随后等应用决定 | 单目标需要BEGIN及终结预留;多目标终结位置由状态区保证 |
| CONFIRM | 提交肯定ACT_CON | 多目标继续下一站确认,全部确认完成后开始数据 |
| DATA | 生成NEED_DATA,应用读取当前值并尝试直接提交 | Submit_Batch返回OK才消费交互、增加批次和对象计数 |
| TERMINATE | 提交ACT_TERM | 发送层接手后终结;发送WAIT时仍然忙 |
| REPLY | 一条否定确认、错误应答或撤销确认 | 提交成功即释放,不产生新的取数交互 |
| FINISHING | 多目标网络流程结束,等待应用读完FINISHED | 最后一个终结事件出队后释放任务及节点引用,才接受新总召 |
| IDLE | 状态查询时表示没有协议任务 | 不表示业务FIFO一定为空,仍可能有FINISHED待取 |
`Reply_Begin / Complete_Data`成功表示应用决定已被模块接手,后续`Process`提交确认或终止。`Submit_Batch`返回OK则表示数据已被发送层接手并确定帧顺序;不再等待Process提交本批。`FINISHED(COMPLETED)`表示ACT_TERM已被发送层接手;这些成功状态均不保证报文写完或被主站确认。发送层可能随后因STOP或连接故障终止传输。
## 5. 应用事件与资源生命周期
### 5.1 三类业务事件
| 事件 | 应用需要做什么 | 应答接口 |
|---|---|---|
| BEGIN | 根据QOI检查数据源并决定是否接受;可延至首个NEED_DATA建立取数游标,不采样整轮数据 | `Reply_Begin(true/false)`;数据源失败可`Fail_Request` |
| NEED_DATA | 应用选择本批IOA与类型,读取当前值和品质后构造非空批次;暂未就绪则保存本事件,继续驱动通信 | `Submit_Batch`、`Complete_Data`、`Fail_Request`三者择一 |
| FINISHED | 停止对应采样工作,按实例/请求编号释放资源 | 无需应答 |
`NextEvent`主动取出事件;事件入队不会执行应用代码。BEGIN或NEED_DATA只有被取出之后才允许应答。工作事件只发出一次,应用暂未就绪时保存事件,不能反复等待模块发出同一事件。
模块核对实例、请求、批次、事件类型、当前阶段和已取出标志。应答时须原样传回事件;实际元数据和发送身份始终使用内部保存的原请求,不依赖应用回传字段构造发送目标。
| 应答结果 | 调用方处理 |
|---|---|
| OK=1 | 交互已消费,不要重复应答;输入ASDU可复用 |
| WAIT=0 | 有未读Slave通知,或Submit_Batch暂不能直接发送;保留事件及游标,推进通信、读空通知后重试;数据批次须重新读取当前值组包 |
| STALE=-2 | 事件已失效或不匹配,停止重试;按FINISHED清理资源 |
| ERROR=-1 | 参数或批次不合法等;通常不消费交互,可修正后重试。NEED_DATA阶段Fail_Request关闭失败是例外,任务仍已终结 |
### 5.2 FINISHED预留
单目标生成BEGIN需要两个业务位置:一个用于BEGIN,一个预留给未来FINISHED。NEED_DATA不能占用终结预留,始终保持`实际事件数 + 终结预留数 ≤ 事件容量`。多目标广播改由每站状态区保存终结记录,因此BEGIN只需一个FIFO空位,所有站可以先确认而不扩大FIFO。pending_events包含尚在状态区中等待入队的FINISHED,可能大于FIFO容量。
应用取出BEGIN后,无论正常完成、拒绝、撤销、STOP、断线、取消或超时,都恰好产生一次FINISHED。BEGIN尚未生成或尚未取出便取消时,移除工作事件并释放预留,不产生FINISHED,因为应用尚未取得请求。取消同时移除未读NEED_DATA;已经取出的旧事件再应答返回STALE。
历史FINISHED保留到应用读取。业务FIFO容量不足时,新的唯一总召可能停留在START等待空位;它已经占用总召资格,但尚未生成BEGIN,不启动业务期限。应用必须及时读完终结事件,不能只等待新的网络通知。默认容量4时可以同时保留旧FINISHED及下一次BEGIN;测试使用最小容量2验证这个边界。
多目标广播进入FINISHING后,连续NextEvent即可读完全部终结记录,无需靠Process搬运。应用不能假定BEGIN与FINISHED严格交替。示例在首个NEED_DATA建立唯一游标,Complete_Data返回OK后立即归还该站取数资源;失败/取消则按FINISHED清理仍在取数的站。若应用在BEGIN另行建立每站资源,须按request_id管理并等对应FINISHED清理。
## 6. 数据批次与发送路径
应用以`Asdu_Create`及信息对象接口构造批次,再调用`Submit_Batch`。模块立即尝试不排队提交,不保存待发批次、应用点表、对象指针或数据源回调。WAIT时输入存储也可复用,但同一业务事件和遍历位置须保留到成功或终结。
IOA、数据类型、分组成员和遍历方式均由调用者决定。BEGIN用于检查数据源和判断是否接受,遍历状态可延至首个NEED_DATA建立,不向库登记或冻结完整IOA列表。应用自行保证总召或分组点的遍历完整性,包括点表变化时如何避免漏点、重复点;库不强制点表在整轮期间保持不变。
推荐接入方式是在每次NEED_DATA组包时读取本批点的当前值和品质,不默认在BEGIN冻结整轮数值。值与品质的读取一致性由数据源保证;逐批取值不承诺整轮所有点属于同一采样时刻。需要固定时刻快照的应用可以自行实现,但必须另行处理快照与突发上报的顺序,不能把快照当成库提供的一致性保证。
总召数据通过`Slave_Try_Send_Response → TcpServer_Try_Send_Response → Connection_Try_Submit_Response → SendManager_Try_Submit_Response`提交,Slave按保存的原身份校验服务实例及连接代次。只有当前发送缓冲空闲、HIGH和NORMAL均无未提交条目、链路和发送窗口允许时,才直接准备I帧并确定N(S)。否则返回WAIT,不把旧值放入HIGH等待。NORMAL中仅保留已提交、待ACK的原文不单独阻挡本批,仍须满足k窗口;不要求等待全部ACK或整轮总召结束。
应用须把“更新当前点值并发布突发”与“读取本批当前值并调用Submit_Batch”放在同一串行执行上下文,或用等效同步保证二者不交错。WAIT之后只保留事件和游标,每次重试重新取值:已积压的突发先发送,随后总召读取最新值;总召返回OK后,本帧顺序已固定,后来发布的突发不能插入该帧之前。这同时避免旧NORMAL被新总召越过,以及新突发之后重发WAIT期间缓存的旧总召值。
这个保证依赖应用发布顺序:库不比较测点版本,无法识别应用在读取之后才迟到发布的旧采样,也无法修正调用者仍使用BEGIN数值快照的做法。突发发送不因总召被暂停;若HIGH/NORMAL持续有待发条目,总召数据可能持续WAIT,本路径没有强制抢占或最长完成时间。应用应按实际负载安排上报频率和批次,不能通过重试旧数值消除等待。
| 检查项 | 要求 |
|---|---|
| 应用层参数 | COT/CA/IOA长度分别为2/2/3 |
| 数据Type | 1、3、5、7、9、11、13、20、21,均为本模块支持的无时标监视信息 |
| 数据COT | 等于原请求QOI:全站20,第1..16组为21..36 |
| CA | 等于配置的本站地址,与业务事件request.common_address一致;不能是65535 |
| OA/TEST | 与原请求一致 |
| P/N | 0 |
| 数量与长度 | 至少1个对象,不超过配置上限及249字节;完整布局有效 |
| SQ及类型切换 | 每批允许SQ=0/1;一个ASDU同一Type,不同批次可切换Type |
返回OK时发送层已复制本批原文,应用可立即复用输入缓冲并推进自己的游标,`batch_index / objects_submitted`也已增加。返回WAIT时不消费NEED_DATA、不推进计数、不保存原文,也不重复生成NEED_DATA;应用先处理通知,交互仍有效时在通信推进后重读同一遍历位置并再次调用Submit_Batch。发送ERROR若确认为正常STOP导致的拒绝,返回WAIT等待通知,随后终结为STOPPED;其他发送ERROR/FATAL以SEND_ERROR终结并返回STALE,不能重试原事件。参数或批次校验ERROR则不消费交互,允许修正。
空数据集或已经遍历完毕使用`Complete_Data`,不能用空ASDU代替结束。未配置分组可以在BEGIN时拒绝;接受的空组仍按ACT_CON → ACT_TERM正常结束。累计量数据不在当前总召批次范围,留给总电度业务。模块不检查IOA是否属于请求的分组,分组筛选和取值由应用负责。
确认、终止和短应答沿原请求connection走普通HIGH应答接口;数据沿同一connection走不排队提交接口,不使用NORMAL,也不进入HIGH等待。只有上一批返回OK后才能完成数据阶段,因此终止不会越过尚未接手的数据。已接手帧按通信层规则发送,不能被新应答越过,也不能通过Cancel撤回。
## 7. 短应答调度与满槽处理
### 7.1 Handle_Event中的提前提交
忙拒绝、地址或COT错误、非法QOI、撤销确认等只需要单帧应答,先尝试提交现有HIGH接口。提前提交必须同时满足:没有未读Slave通知、原会话仍可发送、没有更早形成的REPLY。
```text
形成短应答
→ 有旧REPLY或未读Slave通知:暂不提交
→ 可安全提交:Copy_Response_Request复制请求并写入已确定的应答CA,调整COT/P/N,调用原HIGH接口
├─ 已接手:累计submitted,否定时累计negative_submitted,不占新任务槽
├─ WAIT:未接手,保留/保存为REPLY
├─ ERROR且确认为正常STOP拒绝:保留/保存为REPLY,先交付停用通知
└─ 实际提交错误:按SEND_ERROR等失败路径处理
→ 需要保存但池已满:只丢弃新请求,增加overloads和dropped_requests
```
立即提交不修改调用方事件,不增加绕过通信队列的公开模式。发送成功后本地副本即可释放;WAIT后如需保存任务,以已确定的应答CA建立稳定请求副本,后续只按阶段设置COT/P/N并重试提交。实际连接或发送错误仍按失败路径处理,不能当作普通满槽丢弃。
### 7.2 Process中的选择规则
单帧应答按形成顺序FIFO提交。调度器分别查找唯一总召和最早REPLY,不要求总召位于任务索引队首。总召正在等应用或业务事件空位时,直接处理REPLY;双方都可推进时轮转,避免总召一直挡住短应答,也避免短应答持续占据所有推进机会。
Send_Response内部会推进保留的接收帧并交付通知。Process每步检查FIFO;发送返回值还须区分以下情况,Process、立即应答和Submit_Batch共用同一STOP判定:
| 发送返回值 | 后续处理 |
|---|---|
| OK | 按已接手事实记账/推进,下一次提交前检查通知;不撤回或重发本帧 |
| WAIT | 保留任务并让出;先处理通知,再按原背压规则恢复 |
| ERROR且确认为正常STOP拒绝 | 设置events_pending并让出,保留任务/业务事件,由DEACTIVATED终结为STOPPED |
| 其他ERROR或FATAL | 保留SEND_ERROR失败路径;FATAL可能已写出部分或整帧,不能转成可重试WAIT |
只有ERROR分支额外查询状态:请求须仍属于原服务/实例/代次,TCP仍连接,会话ACTIVE且无故障、队列状态有效,传输已停用并且通知未读,才按正常STOP拒绝让出。不能仅凭FIFO非空忽略错误。成功路径不增加查询;正常STOP拒绝不累计waits、不推进游标、不停止或刷新取数期限。应用先读取Slave通知,旧NEED_DATA随后返回STALE;立即应答保存的短任务也由STOP清理。
Process提交确认、终止或短应答遇到WAIT就结束本轮,保存请求和阶段,下次优先检查最早REPLY。每一步都重新检查Slave通知,发送过程中产生的新通知也必须先处理。不会跨过未读STOP/START或撤销继续发送旧事务。数据WAIT由持有NEED_DATA的应用在通信推进后重读并重试。
`Process(gi)`没有预算参数,也不内部等待:当前站REPLY或终止成功后继续广播或释放任务;DATA只生成NEED_DATA。没有就绪任务、发送WAIT、有未读Slave通知或已推进2倍任务容量步时返回;剩余本地工作由need_process提示续跑。每步发送前重查通知,不执行应用代码。
### 7.3 4个槽的含义
任务池的容量与“同时允许几个总召”是两个独立约束。总召资格上限恒为1,任务池还为无法立即发送的短应答提供存储。忙应答占槽并不代表原激活已被保存,发完否定确认后即释放,不会再生成该请求的BEGIN。
满槽时保留已有任务、应用事件及进度,不主动关闭TCP,不挤掉最早应答。只有新的、无法安全提交或保存的请求被丢弃。`EVENT_HANDLED`包括这种丢弃;应用不能把它当成“主站已经收到应答”,也不能再次投递同一Slave通知。`dropped_requests`用于观察这个情况,主站可能等待超时或自行重试。
## 8. 业务期限与驱动
内部`Task_Timeout`按同一时刻汇总最近剩余时间、超时原因和已确认广播站的总期限到期标志。`Get_Status`读取该结果安排唤醒,`Expire`据此选择拒绝当前站或关闭原连接,不再二次遍历广播站或重新取时判定。结果只在本次调用中使用,不在任务内保存第二份期限状态。
| 配置 | 默认值 | 起止点 |
|---|---|---|
| `begin_timeout_ms` | 15000ms | BEGIN入业务FIFO至应用接受、拒绝或报告失败,包含未取事件的时间 |
| `data_timeout_ms` | 15000ms | 每次NEED_DATA入队至首次有效批次尝试、声明结束或报告失败,每批重新计时 |
| `total_timeout_ms` | 0(默认禁用) | 启用后从BEGIN生成至ACT_TERM被发送层接手,覆盖取数、事件容量等待和发送等待,不等待主站ACK |
三项默认值是本项目的可配置业务策略,不是IEC104规定的总召时限,也不代替链路t0..t3。每项0表示禁用。执行总期限默认关闭,避免慢链路的累计发送等待触发固定整体截止;BEGIN和每批取数期限仍各为15000ms,底层单帧发送、确认和链路探测期限继续有效。需要限制整次总召耗时时,可显式将`total_timeout_ms`设为非零。没有`queue_timeout_ms`;START尚未生成BEGIN时不计业务期限,但此时仍拒绝后续总召。及时消费业务事件是应用的驱动义务,模块不会通过后台线程保证进度。
所有期限使用Net提供的64位单调毫秒时钟,以经过时间比较,避免校时和绝对截止相加溢出。0毫秒有效。业务应答停止对应阶段期限,下一次NEED_DATA重新计时;显式总期限在同一站内不因新批次、忙应答或重试而延长。每站从自己的BEGIN独立计时,包含等待其他站确认、取数及终止的时间;已确认但不在当前取数位置的站也会被检查,到期关闭原连接并取消整轮。没有额外跨全部CA的整体定时器,同一时刻两项到点优先报告TOTAL_TIMEOUT。
有效批次因发送背压返回WAIT时置`data_send_waiting=true`,说明应用已响应,本批取数计时停止,之后不再恢复,下一批NEED_DATA才重新计时。无效批次或因未读Slave通知而提前返回WAIT不停止取数计时。发送等待仍受底层链路期限及显式启用的总期限约束;模块不替应用重读或重试数据,调用方必须持续驱动保留的事件。
Process和有效应用应答都会检查期限。先消费Slave通知,再判断超时,使已经到达的STOP、撤销或断线优先终结事务。到期后旧交互失效,迟到应答返回STALE;具体处理按肯定ACT_CON是否已被发送层接手划分:
| 失败发生的阶段 | 处理方式 | 连接及其他任务 |
|---|---|---|
| START:BEGIN超时、总超时或Fail_Request | 终结当前业务,复用原槽保存否定ACT_CON;保留BEGIN_TIMEOUT、TOTAL_TIMEOUT或SOURCE_ERROR | 保留TCP及已有短应答 |
| CONFIRM:肯定ACT_CON尚未提交成功,总期限到点 | 转为否定ACT_CON,终结原因仍为TOTAL_TIMEOUT | 保留TCP及已有短应答 |
| DATA / TERMINATE:取数超时、总超时或NEED_DATA阶段Fail_Request | 沿原请求关闭连接,取消本模块任务,不补发成功ACT_TERM | 原连接其他业务也会受影响,不关闭后来复用服务槽的新连接 |
分界是发送层接手肯定ACT_CON,不是应用调用Reply_Begin(true),也不是TCP写完或主站ACK。CONFIRM提交返回WAIT时仍可以拒绝激活;肯定确认即使仅进入HIGH或当前发送缓冲,也已经进入DATA,不能再改发否定确认。确认后的断链是本项目当前执行失败策略,不增加按超时类型区分的配置开关。
内部Reject_Request统一处理应用主动拒绝、BEGIN失败和确认前超时,Fail_Interrogation按阶段选择拒绝请求或关闭连接。不能直接从超时处理调用公开Reply_Begin(false):BEGIN可能尚未取出,该接口的校验也会再次检查期限。内部转换先Finish,再将原槽改为REPLY并移到应答顺序末尾;无需空闲任务槽,保留原QOI/OA/TEST,应答CA使用本站地址,旧工作事件立即失效。BEGIN已取出才产生FINISHED,具体失败原因不会统一改成REJECTED。
否定确认继续按现有调度发送,WAIT保留重试,不再计算已终结请求的业务期限。Process检查到确认前超时后会重新检查Slave通知,在本轮继续推进短应答;应用应答接口检查到超时则返回STALE,调用方仍须驱动Process。失败转换完成即释放总召资格,但原槽要到否定确认提交或取消后才归还。主站可以重新发起请求,模块不自动重放旧请求。
等待前查询`Get_Status`。若`timer_active=true`,以`timeout_remaining_ms`限制下一次Poll或应用等待的上限,0表示立即推进;false表示当前没有启用的业务期限,不代表业务已经结束。状态查询只读,不触发发送、终结或超时。
`need_process=true`表示应立即调用总召Process:例如新请求有空位生成BEGIN、业务应答接手后可提交下一帧、读取旧FINISHED后解除事件容量等待,或业务期限已到期。该值按Process同一选择规则计算,既考虑总召,也考虑排队的短应答;它不是`!waiting`,也不保证发送层一定接手。当前帧提交WAIT后暂停本地续跑,避免空转;每次Slave_Poll/Process推进网络并读空Slave通知后,仍须调用总召Process重试,不能以此标志为false跳过网络进展。
应用持有的数据WAIT也须在上述网络推进后重试,不能只等need_process置位或新的NEED_DATA。`data_send_waiting`表示已交付有效数据但等待发送机会;`application_waiting`只表示仍等待应用首次有效应答,两者都不是发送就绪查询。
Get_Status实时检查Slave FIFO;`events_pending=true`时`need_process=false`,先处理可能存在的STOP、撤销或断线。`pending_events`表示另一条业务FIFO,应及时读取;每次读取事件或调用应答接口后重新查询状态。外部事件循环还须合并Slave_Prepare的等待条件,总召的标志不替代TCP的need_process及链路期限。
## 9. 撤销、取消与连接变化
主站撤销为Type100、COT=8,在忙拒绝判断之前按原连接代次、目标CA、OA、TEST和QOI匹配,包括已选中但BEGIN尚未生成的START。定向撤销匹配站,其他站继续;已确认但尚未取数、数据已交完等终止、或已删除的原站均可匹配。尚未选入BEGIN的未来站没有活动事务。广播撤销停止整轮,对每个仍活动的站发送肯定COT=9,不向已结束或尚未启动的未来站补发确认。
撤销确认组复用原任务槽,排在已有短应答之后;即使任务池满也能撤销。当前站待发的否定ACT_CON不会被其他站的撤销确认覆盖。一组撤销确认尚未提交完时,新的重叠撤销按未匹配拒绝;不能安全保存否定回复时按既有满槽策略计数丢弃。已被发送层接手的帧不回滚,不再为被撤销站发送ACT_TERM。
匹配成功后,先通过Copy_Response_Request保存撤销请求并写入应答CA,使用原激活的info和进度生成FINISHED(DEACTIVATED),随后把原槽转换为肯定DEACT_CON。转换不需要额外任务槽,池满仍可撤销;形成的确认排在已有REPLY之后,条件允许时立即提交。已被发送层接手的帧继续在确认前按原顺序发送,本地未提交批次丢弃,不再提交原总召ACT_TERM。
| 操作或通知 | 本模块行为 | 网络确认 |
|---|---|---|
| 匹配COT=8 | 停止当前总召,旧工作事件失效,必要时FINISHED(DEACTIVATED) | 肯定COT=9 |
| 不匹配或重复撤销 | 当前总召保持原状 | 否定COT=9 |
| `Cancel` | 取消总召及待发短应答,保留必要的FINISHED(CANCELLED) | 不自动发送撤销确认或ACT_TERM |
| STOPDT对应DEACTIVATED通知 | 停止本模块任务,必要时FINISHED(STOPPED) | 链路STOP确认由通信层负责 |
| DISCONNECTED | 取消任务、清除已处理代次,必要时FINISHED(DISCONNECTED) | 无业务成功确认 |
| 新CONNECTED或会话已失效 | 旧任务终结为CONNECTION_CHANGED,不跨代次重放 | 后续新激活重新建立事务 |
COT=8的总召撤销与Slave的DEACTIVATED通知不是同一事件:前者结束一个业务,后者表示STOPDT停用。FINISHED表示应用可以清理资源,不承诺撤销确认已发送完成。
非法批次返回ERROR并保留交互,应用可以修正;数据源确实不能继续则调用`Fail_Request`。BEGIN阶段回否定确认并保留连接,NEED_DATA阶段关闭原连接,两者都终结为SOURCE_ERROR。确认后失败仍可能影响同连接其他业务。接受BEGIN前应检查数据源和必要业务资源,减少确认后才发现无法执行的情况;无需预先采样或编码全部数据,仍可逐批交付。
## 10. 应用接入与退出顺序
创建前使用`Config_Default`初始化配置,再按站地址、单批大小和实际业务耗时调整。公开API共12个,调用功能如下:
| 接口(省略IEC104_Interrogation_前缀) | 功能 |
|---|---|
| Config_Default | 填写CA=1、单批127对象及三项默认业务期限 |
| Create | 复制配置、借用Slave,取得静态池实例 |
| Destroy | 任务及业务事件全部为空时归还实例,不关闭Slave |
| Cancel | 本地取消任务并保留应有终结事件 |
| Handle_Event | 校验并接收应用转交的Slave通知,处理准入、短应答及会话变化 |
| Process | 自动推进就绪任务,遇等待条件返回 |
| NextEvent | 主动读取业务事件,只有READY写出结果 |
| Reply_Begin | 应答已取出的BEGIN,明确接受或拒绝 |
| Submit_Batch | 校验当前NEED_DATA对应的非空ASDU,立即尝试不排队提交;WAIT须重读数据后重试 |
| Complete_Data | 声明数据耗尽,后续Process提交ACT_TERM |
| Fail_Request | 报告数据源失败;BEGIN转否定确认并保留连接,NEED_DATA关闭原连接,均保留SOURCE_ERROR |
| Get_Status | 查询进度、累计计数及最近业务期限,不产生副作用 |
```text
Slave_Create / Config_Default / Init
→ Interrogation_Config_Default / Create
→ 循环
1. 驱动Slave,逐条读取并转交连接通知及请求
2. Slave通知未读空时继续处理通知
3. Interrogation_Process
4. Interrogation_NextEvent,处理BEGIN / NEED_DATA / FINISHED
5. 应用数据就绪时显式应答,继续推进
6. 查询Slave等待条件及Interrogation的need_process、事件和期限,再安排等待
→ Interrogation_Cancel
→ 读完业务事件并清理FINISHED对应资源
→ Slave_Close,读空收尾通知
→ Interrogation_Destroy
→ Slave_Destroy
```
网络驱动可以使用Slave的Poll,也可以接入应用自己的Prepare/Process事件循环。数据暂未就绪不能停止驱动网络;应用也不能在忙等待循环中反复调用Process而不处理事件。
`Destroy`不会隐式取消或丢弃事件。即使没有活动总召,只要还有REPLY或未读FINISHED就返回false;调用方完成清理后再销毁。实例创建失败要清理已创建的Slave,不能继续使用NULL句柄。
## 11. 状态与统计
| 字段 | 解释 |
|---|---|
| generation / active | 最近已处理的连接代次和启用状态,不能代替实时会话查询 |
| gi_pending | 总召网络流程未终结;FINISHING虽为false,状态区清理前仍忙拒绝新激活 |
| phase | 当前站阶段,收尾为FINISHING;仅有应答为REPLY,无任务为IDLE |
| pending_requests | 总召、FINISHING及短应答占用的任务槽数,不是等待执行的总召数 |
| pending_events | FIFO事件及状态区中待交付的FINISHED,不包含预留位置 |
| waiting / application_waiting | 存在发送、应用或事件容量等待;后者表示仍等待应用首次有效应答 |
| data_send_waiting | 当前批次已有效尝试但发送WAIT;不保存原文,应用须保留事件、重读数据并重试 |
| events_pending | 当前Slave通知尚未读空,应先处理网络通知 |
| need_process | 当前有需立即推进的本地工作、到期期限或失效会话;发送WAIT后等待网络推进再重试 |
| timer_active / timeout_remaining_ms | 是否启用业务期限及距最近期限的剩余毫秒 |
| batch_index / objects_submitted | 当前总召已提交的批次和对象数;仅有REPLY时保留最早应答的历史进度 |
| submitted / gi_submitted | 被发送层接手的ASDU数 / ACT_TERM提交成功的总召次数 |
| waits | 实际提交返回发送WAIT的次数,不统计应用等待循环次数 |
| rejected | 校验、忙、应用决定、确认前失败和容量原因拒绝的请求数,含无法可靠应答而丢弃的畸形请求 |
| negative_submitted | 发送层已接手的否定应答数,不等于主站已收到 |
| overloads / dropped_requests | 任务池满或广播状态区不足的次数 / 无法安全提交或保存的新请求丢弃数 |
| cancelled | 取消的协议任务数,包含待发REPLY;不等于FINISHED数量 |
| deactivated / timeouts | 成功匹配主站撤销次数 / 执行业务超时次数 |
| last_finish_reason | 最近合法总召的终结原因,BEGIN尚未交付时取消也会更新 |
状态为独立副本,累计计数从Create开始,Cancel和重连不清零。`waiting=true`不代表所有任务都无法推进:总召等数据时仍可提交短应答。`gi_pending=false`也不表示可以直接Destroy,仍须检查待发应答及业务事件。
确认前超时增加timeouts及rejected,BEGIN阶段Fail_Request增加rejected;两者都不增加cancelled或gi_submitted。只有否定确认被发送层接手才增加negative_submitted。REPLY等待期间反复Process不会重复累计超时或交付FINISHED,last_finish_reason保留原终结原因。
## 12. 总电度扩展与设计边界
Type100与Type101分别使用GeneralInterrogation和CounterInterrogation只读规则,共用引擎通过创建时传入的规则执行解析、限定词和数据检查以及完成方式选择,不引用具体种类符号。总召旧Create名称保留;计数量使用CounterInterrogation_Create。同一Slave每种规则至多一个实例;不能复用总召的gi_pending作为全局准入开关。
计数量RQT=1..5、FRZ=0..3;读取接受Type15/37,COT为37..41。冻结/复位通过Execute_Begin先检查请求再执行一次同步动作,肯定ACT_CON提交后完成,不产生NEED_DATA或ACT_TERM。TEST动作拒绝;已执行动作不会因超时转为否定,也不因断线回滚。请求结构追加freeze/data_cause,总召分别为0/QOI。完整约定见[计数量设计](IEC104_CounterInterrogation_设计架构.md)。
接入多个业务处理器时,连接、启停和断线通知应广播给每个相关模块,不能因某个模块返回HANDLED就停止广播;I帧按业务类型分派,未接手的交给后续处理器,最后再由应用兜底。总召模块不实现总电度冻结、复位或累计量点表。
多个业务可以分别持有任务,但仍共享连接、HIGH容量和发送窗口,应用需要持续驱动各模块。业务忙相互独立不代表发送时延相互独立;STOP、断线和任一业务触发的连接关闭会影响同连接所有业务。
本设计参考了[lib60870-C多客户端示例](https://github.com/mz-automation/lib60870/blob/master/lib60870-C/examples/multi_client_server/multi_client_server.c)按连接限制活动总召、忙时及会话槽满时否定确认的做法。该示例仅处理QOI=20,本项目保留20..36分组支持,使用应用主动读取事件、固定请求副本和明确的WAIT重试。其会话槽保存不同连接的活动总召,不能理解为同一连接的后续总召队列。
业务期限、静态容量和忙拒绝是本项目的实现策略。当前不提供多QOI待执行队列,不自动替主站重试,也不通过应用回调获得下一批数据。若现场要求保存连续分组请求,需要单独定义准入、主站确认等待和取消语义,不能仅扩大任务槽数量。
## 13. 验证与调用方迁移
总召基线回归98组,计数量另有12组,合计110组,使用真实Slave/TCP提交路径及可控单调时钟;HIGH缩到251字节、业务FIFO缩到2槽。覆盖原有突发排序、部分写入、取数期限、k=1窗口及到期ACK;多站覆盖13站三阶段顺序、非空混合批次、状态区不足与跨实例重叠、已确认未来站删除/同CA重建、超出FIFO容量的无损FINISHED、各阶段定向及满槽广播撤销、拒绝与撤销同时等待、非当前站总期限,以及总期限禁用、BEGIN先到期和两项同时到期的处理优先级。畸形请求覆盖否定/丢弃边界、输入不变、基础截短仍被Receiver拒绝、原任务/期限保持、背压满槽和广播撤销隔离。提交期STOP覆盖三个入口、真实通知容量背压、FATAL与STOP并存、非STOP的ERROR、OK/WAIT伴随新回执及START后重新总召。真实TCP回环验证命令行单站,使用Windows后端和k=1逐帧核对业务字节。
| 验证范围 | 应满足的结果 |
|---|---|
| 全站及全部16组 | QOI、数据COT、OA、TEST及点对象验证正确 |
| 广播全站及全部16组 | 业务事件、确认、数据、终止均用本站CA;输入Slave事件保持原文 |
| 广播错误、忙拒绝及确认前失败 | 副本建立时使用本站CA,立即提交和等待重试均保持该地址;其他错误CA仍按原值回显 |
| 广播数据地址 | Submit_Batch拒绝65535及其他站地址,应用修正为本站CA后可重试 |
| BEGIN生成前后、确认/数据/终止等待 | 相同和不同QOI的新激活均否定,原身份和进度不变 |
| 完成后重新发起 | 不自动启动被拒绝请求;主站重发后产生新request_id并获得完整期限 |
| 总电度类型隔离 | 总召空闲和忙时,本站或广播CA的Type101均返回IGNORED,不产生总召否定应答 |
| 短应答及满槽 | 安全时提前提交,积压保持FIFO;满槽只丢新请求,旧工作事件仍有效 |
| 远方撤销 | 本站/广播激活与本站/广播撤销四种组合均匹配实际站CA,其他身份仍严格检查;满槽仍可复用原槽 |
| 业务FIFO满 | FINISHED保留;BEGIN前不计业务期限,读出旧FINISHED后才能启动当前请求 |
| 本地续跑 | 事件空位恢复及业务应答后need_process置位;发送WAIT时不空转,网络推进后继续发送;未读Slave通知优先 |
| 三项执行期限 | 迟到应答不能绕过期限,忙应答不刷新总期限,先处理已到达的STOP/撤销 |
| 确认前失败 | 未读BEGIN超时不交付FINISHED;已交付时恰好终结一次,否定确认保留QOI/OA/TEST并使用本站CA |
| 满槽及发送等待期间失败 | 原槽转REPLY,不挤掉旧应答;按形成顺序发送,保留TCP,同连接重试可以完成 |
| 肯定确认提交边界 | CONFIRM发送WAIT时超时回否定确认;确认已接手但尚未写出时,取数失败及超时仍关闭原连接 |
| STOP、断线及实例复用 | 旧请求不能跨会话发送或接受迟到应用应答 |
| 真实TCP连续分组请求 | 第一个完成,后续得到否定确认;结束后重发的请求分别正常完成 |
| 真实TCP广播流程 | 全站、分组、忙拒绝、非法QOI及撤销均返回本站CA;连接继续工作 |
从先前多QOI排队版本迁移时,删除`queue_timeout_ms`、`queued_interrogations`、`queue_timeouts`字段访问,以及`QUEUED / FINISH_QUEUE_TIMEOUT`分支。所有调用方须重新编译并以Config_Default初始化配置。`Process`继续只接收实例,不增加预算参数;原12个总召接口保留,其中Create实现迁至GeneralInterrogation.c,工程须加入此源文件。计数量另增2个入口。`FINISH_OVERLOAD`仅保留旧枚举值,当前满槽不以此终结已有总召。
状态新增`need_process`和`data_send_waiting`,所有调用方须与库一起重新编译。原来用`pending_requests != 0 && !waiting`决定是否立即续跑的代码应改为读取need_process,并保留网络推进后调用总召Process的步骤。Submit_Batch现立即尝试提交,OK时进度已推进;WAIT时只保留业务事件和游标,每次重试须重新读取当前值,不能继续复用旧构造结果。Reply_Begin和Complete_Data仍由Process发送后续确认或终止。
请求存储精简只改变内部表示,总召公开函数、业务事件及配置字段不变。Slave新增按身份发送、尝试发送和关闭原连接的接口;总召内部使用,调用方继续按原方式驱动总召即可。完整SlaveEvent及Copy_Event保留原语义,其他业务无需迁移。
确认前失败修复不新增接口、配置字段或枚举值。调用方收到BEGIN阶段Fail_Request的OK或超时后的STALE时,不应再假定TCP已经断开,须继续处理FINISHED并驱动Process发送否定确认。
早期单站广播支持新增`IEC60870_Asdu_Set_CA`,当时未改变总召配置及事件布局。当前多CA版本新增use_station_registry与request.station_id,所有调用方须重新编译。新应用启用站点表,按事件实际CA构造数据,不把65535填入数据ASDU;BEGIN获取引用,FINISHED归还。Remove的OK已改为立即退出路由,释放内存须另查Is_Reusable,原先“Remove成功即可释放”的调用代码必须迁移。
广播发送顺序与[c104 Server.cpp](https://github.com/Fraunhofer-FIT-DIEN/iec104-python/blob/main/src/Server.cpp)一致:全部站确认、逐站数据、全部站终止。c104使用shared_ptr;这里用调用方节点、显式引用及轻量状态区,并保留可重试WAIT、业务事件和各站明确收尾。仍只允许一轮总召,不引入每站独立发送任务或动态发送队列。新增broadcast_states/broadcast_capacity及FINISHING,调用方须重新编译;启用多目标广播前配置状态区,并适配多个BEGIN先于首个FINISHED的事件顺序。
地址处理重构新增语义常量`IEC104_CA_BROADCAST`,将地址准备集中到内部副本创建,并移除任务中重复的header。公开函数签名和事件字段保持原样,应用无需增加调用。59组可控网络及真实TCP回归继续检查确认/数据/终止WAIT、错误地址、原事件不变、广播/本站交叉撤销和确认前失败,避免内部重构改变协议行为。
测试验证本模块的功能和边界,不代表已通过正式协议一致性认证。现场仍须确定主站对否定确认的处理、各分组点表、采样一致性及业务期限取值。
@@ -0,0 +1,550 @@
# IEC104 TCP子站服务与网络适配
业务默认使用`IEC104_Slave.h`统一入口;Slave拥有并驱动本模块,自动为I帧提供ASDU及原连接应答接口。本文保留独立TCP接入与内部维护的接口说明,下文的TCP调用方通常为Slave。业务示例见[README](README.md#子站统一入口)。
`IEC104_TcpServer.c/h`负责IPv4监听调度、连接生命周期、收发预算和业务事件队列,通过现有`IEC104_Connection`驱动子站协议。应用可用Prepare查询监听和定时条件,在自己的事件循环中等待,再调用Process推进;独立TCP循环也可用Poll组合这三个步骤。业务事件通过NextEvent主动取出,TCP模块不保存或调用业务函数。真实网络操作和单调计时通过`IEC104_Net.h`调用;当前提供的实现为`IEC104_Net_Windows.c`,尚未提供Linux或其他平台后端。当前仅提供模块,暂不提供独立运行入口。同时维护一条活动主站连接;额外连接被接受后关闭,原连接继续运行。
## 1. 构建与接入
应用工程需编译以下源文件:
```text
104/IEC104_TcpServer.c
104/IEC104_Net_Windows.c
104/IEC104_Connection.c
104/IEC104_Receiver.c
104/IEC104_SendManager.c
104/IEC104_SendBuffer.c
104/IEC104_AsduQueue.c
104/IEC104_Codec.c
common/IEC60870_Codec.c
104/IEC104_Link.c
104/IEC104_Link_Slave_StartStop.c
```
应用先调用Create取得静态池中的不透明句柄,再调用`IEC104_TcpServer_Config_Default`取得配置并Init;成功后在同一线程使用Prepare/Process或Poll驱动,并通过NextEvent处理事件,退出时Close、读完剩余事件,最后Destroy归还静态槽。具体接口和接入示例见下文。独立程序的入口、事件循环、退出控制和日志由应用负责。
配置中的`bind_address`默认`0.0.0.0`,表示监听本机全部IPv4接口;主站填写子站电脑实际IP和相同端口。同机运行可配置`127.0.0.1`。`port`默认2404;设为0时由系统分配,Init成功后调用Get_Status,从输出快照的`bound_port`读取实际端口。
业务I帧复制为`IEC104_TCP_EVENT_I_FRAME`事件,链路确认由Connection处理;总召、校时、遥控等ASDU业务应答需要接入对应业务处理。应用可使用`IEC60870_Asdu.h`进行命令解析、监视对象构造和原文应答构造,依赖及示例见[README](README.md#asdu信息对象模块);TCP不自动调用这些接口或执行命令。U帧启停和TESTFR应答由现有协议模块自动生成,TCP模块负责实际发送。TCP建立/断开与数据传输激活/停用分别通过CONNECTED、DISCONNECTED、ACTIVATED、DEACTIVATED交付,业务不必通过查询快照推断启停变化。
统一子站入口在以上依赖之外编译`IEC104_Slave.c`、`IEC60870_Slave.c`、`IEC60870_Asdu.c`、`IEC60870_InformationObjects.c`和`IEC60870_Time.c`。Slave内部保存服务句柄,由NextEvent创建ASDU视图并绑定原服务实例及连接代次;业务不再创建手动SlaveConnection。TcpServer本身继续独立于共用发送接口和完整对象编解码;发送/通知/启停策略不变。
以上路径均相对于项目根目录;工程需将 `104/` 和 `common/` 加入头文件搜索路径,并按所用模块选择源文件。应用工程需增加`IEC104_Net_Windows.c`并链接Windows库`Ws2_32`,MinGW参数为`-lws2_32`。平台实现按编译链接选择,同一工程只链接一个Net实现。
`IEC104_TcpServer.h`只包含`IEC104_TcpTypes.h`(其依赖`IEC104_Types.h`),不包含Connection、Net或其他运行对象的定义,也不要求`_WIN32`。业务只看到公开配置、事件、状态和调度值类型;内部对象完整定义在`IEC104_TcpServer.c`。库内Windows依赖集中在Net后端;应用自己的事件循环适配代码负责把借出的标准整数socket转换为本平台注册类型。业务解析ASDU公共头时显式包含 `IEC60870_Codec.h`,完整对象解析包含 `IEC60870_Asdu.h`。接口迁移见下一节;平台解耦不代表已经提供其他操作系统后端。
## 2. 对外接口与存储
| 接口 | 用途 |
|---|---|
| `IEC104_TcpServer_Create` | 从静态池取得不透明句柄;无可用槽返回NULL,不申请网络资源 |
| `IEC104_TcpServer_Destroy` | 归还已关闭且事件读空的静态槽;运行中或尚有事件时拒绝 |
| `IEC104_TcpServer_Config_Default` | 设置默认监听地址、端口及公开协议参数 |
| `IEC104_TcpServer_Init` | 初始化Net后端、非阻塞监听和服务队列;成功后即可缓存NORMAL |
| `IEC104_TcpServer_Prepare` | 只读查询完整socket关注集合、定时条件、待取事件数量和本地续跑标志 |
| `IEC104_TcpServer_Process` | 输入一批socket/定时/本地通知,有界推进接受、收发和协议,不执行等待 |
| `IEC104_TcpServer_Poll` | 组合Prepare、至多一次Net_Wait和Process;max_wait_ms限制网络等待 |
| `IEC104_TcpServer_NextEvent` | 按FIFO复制并取出一个事件;READY=1、EMPTY=0、ERROR=-1 |
| `IEC104_TcpServer_Enqueue_Event` | 复制服务级主动上报ASDU;无连接也可缓存,不需要generation |
| `IEC104_TcpServer_Send_Response` | 提交当前连接应答,先检查启停准入,通过后尝试一次非阻塞发送或暂存HIGH;须携带原事件generation |
| `IEC104_TcpServer_Get_Status` | 复制端口、代次、事件数、诊断、会话状态及HIGH/NORMAL积压与容量 |
| `IEC104_TcpServer_Get_Remote_Address` | 只读复制当前主站的IPv4、源端口和连接代次;无连接返回NOT_CONNECTED |
| `IEC104_TcpServer_Close_Connection` | 仅关闭匹配代次的当前主站,保留监听和现存NORMAL,继续接受新连接 |
| `IEC104_TcpServer_Close` | 结束当前连接、保存收尾事件、清空ASDU队列并关闭网络资源;保留未读事件 |
Init/Prepare/Process/Poll/Close_Connection/Close/Get_Remote_Address/Get_Status/Destroy统一返回`IEC104_TcpResult_t`,只有`IEC104_TCP_OK`(值为0)表示成功,其他值均为明确的失败分类。调用方必须显式比较枚举,不使用`if (!Init(...))`或将结果直接当作循环条件。这些接口的最后一个参数统一为可选的`IEC104_TcpError_t *error`;不需要详情时传NULL。不提供旧签名、布尔返回值或Ex兼容包装。
| 结果(均带`IEC104_TCP_`前缀) | 含义与处理 |
|---|---|
| `OK` | 本次操作成功;Poll空闲、暂不能接入或单条连接已收尾时也可成功 |
| `INVALID_ARGUMENT` | 必填指针为NULL,或就绪批次数量、事件位、重复项等不合法 |
| `INVALID_HANDLE` | 句柄为NULL或不属于当前已占用的静态槽,修正对象生命周期 |
| `INVALID_CONFIG` | 配置范围或地址字符串结束符检查失败,修正配置后重试Init |
| `INVALID_STATE` | Init/Destroy时服务仍在运行,或Prepare/Process/Poll/Get_Remote_Address/Close_Connection时尚未Init |
| `BUSY` | Process/Poll/Send_Response/Close_Connection/Close期间发生重入;所有调用须回到所属线程串行执行 |
| `EVENTS_PENDING` | Close后尚有未读事件,先通过NextEvent读空,再Init/Destroy |
| `NETWORK_ERROR` | 网络初始化、监听、等待或不可恢复的接入失败,按error的阶段和原生码诊断;接入资源暂缺由内部暂停重试 |
| `INTERNAL_ERROR` | 队列/协议会话初始化、等待期限查询或内部一致性异常,停止服务并检查实现 |
| `GENERATION_EXHAUSTED` | 代次用尽,Close并读完事件后Destroy;该槽不再重新分配 |
| `REGISTRATION_EXHAUSTED` | socket注册编号用尽,同样关闭并归还;不回绕分配旧身份 |
| `NOT_CONNECTED` | Get_Remote_Address/Close_Connection时服务已Init,但没有当前主站;不返回旧地址、不产生新的断开事件 |
| `STALE_GENERATION` | Close_Connection指定的非零代次不匹配当前主站,拒绝操作,不关闭新连接 |
`error`包含与返回值一致的`result`、失败`stage`和本次网络错误的`socket_error`。每次调用均完整覆盖,成功为OK/NONE/0,非网络错误的原生码为0;即使句柄无效或Destroy成功也能取得诊断。诊断存储由业务持有,不得与config/status/info/ready等输入输出重叠。需要保存首个失败时,清理或查询调用可传NULL,避免覆盖自己保存的error。阶段包括VALIDATE、LIFECYCLE、NET_INIT、LISTEN、WAIT、ACCEPT、GENERATION、QUEUE_INIT、QUEUE_STATE、CONNECTION_INIT、SCHEDULE和REGISTRATION;LISTEN包含后端的地址内容解析、socket创建、绑定及监听,QUEUE_STATE表示服务队列异常或无法解除旧绑定,SCHEDULE表示无法取得可靠的协议等待上限,REGISTRATION表示无法继续分配socket注册身份。
在服务队列正常的情况下,客户端EOF、收发错误、协议错误和超时只结束该客户端,Process/Poll仍返回OK并保留监听;连接原因经DISCONNECTED事件中的`IEC104_TcpFault_t`交付。服务级推进或期限查询失败后保存本次error,再Close并读完事件。应用事件循环自身的等待/注册失败由应用诊断和处理,不伪装成TCP协议故障。Get_Status中的`last_socket_error`是历史后端诊断,可能来自更早的连接或可恢复错误,不能用它判断本次接口是否失败。该字段、error及连接事件中的原生错误码仅用于日志,当前Windows后端使用Winsock错误;跨平台业务按公开结果及连接故障分类处理。
正常TCP有序关闭在没有既有故障时交付`fault=IEC104_TCP_FAULT_NONE`和`socket_error=0`。例如TRANSPORT表示传输异常,I_ACK_TIMEOUT表示I帧确认超时,不能把所有非零fault都当作协议错误。公开的connection快照只包含会话阶段、故障、尚未转成RECORD事件的记录数量及已提交的STARTDT状态;已经进入事件FIFO的记录不计入该数量。Connection内部启停过程、接收借用和队列字段不再透传。启动状态也不表示窗口和发送容量一定可用,业务仍以提交结果为准。DISCONNECTED保留关闭协议前的阶段和故障,phase可能仍为ACTIVE;其中`pending_send_records`扣除本次收尾已成功转为RECORD事件的记录,全部转移成功时为0,不表示这些帧全部已ACK。当前TCP是否连通应查询Get_Status的connected。
内部Connection调用返回ERROR、EOF时设置NORMAL发送限制失败,以及其他内部异常均按INTERNAL收尾,不冒充网络传输错误。已冻结连接返回FATAL时保留其首次原因;Send_Response遇到未约定的内部返回值也归INTERNAL,正常的业务参数或启停准入拒绝仍只返回SUBMIT_ERROR。实际网络失败通过Connection登记TRANSPORT;若发送失败或传输关闭回报未能返回预期FATAL,则由TcpServer补记INTERNAL,同时保留本次原生socket_error。收尾读取或保存记录失败也补记INTERNAL;已有协议、传输或超时故障优先保留。状态无法可靠查询时status_valid仍为0,不能读取无效快照推断故障类别。
子站收到格式合法的STARTDT_CON或STOPDT_CON时消费并忽略,不因该帧断连,不回复、不切换启停状态、不产生业务通知,也不解除确认等待或刷新t3。该处理不占通知和控制发送槽;后续输入及原有期限检查照常推进。起始字节、长度/APCI、ASDU最低长度、I/S序号或ACK错误仍沿用原故障规则。
每个APDU的首字节必须为0x68,接收器不跳过前导垃圾;遇到错误起始字节立即登记FRAME故障并断开。报文体仍按声明长度收集,其中的0x68是普通数据,不参与重新同步。
`IEC104_TcpServer_t`是不完整类型,应用只能声明其指针,不能直接定义对象、清零、复制或访问成员。内部静态池保存真实结构体,地址与对齐由编译器保证,不使用malloc,也不需要业务准备字节数组并强转。`IEC104_TcpServer_Config.h`中的`IEC104_TCP_SERVER_CAPACITY`默认1,表示同时持有的服务器句柄数;需要多个监听服务时,调整该值或为TcpServer.c增加编译参数`-DIEC104_TCP_SERVER_CAPACITY=数量`。每个服务仍只接受一条活动主站连接,容量增加也会增加静态内存。
默认每个槽持有Connection、ASDU队列、2048字节TCP接收暂存、1024/4096字节HIGH/NORMAL队列、8个控制槽、32条发送记录和一个变长事件字节环。2048字节能容纳每轮8个满长APDU(2040字节),保留原有每轮交付预算;接收预算让出后的确认超时检查采用下文的固定输入快照规则。容量统一在配置头文件中设置,编译期检查普通、启停及收尾容量。内部容量变化只需要重新构建模块,业务头文件不暴露这些容量或内部结构大小。静态槽在Close后仍占用,直到Destroy成功才归还;首次Init失败也必须重试或Destroy,不能丢弃句柄。
公开提交接口接受6..249字节ASDU,因此`IEC104_TCP_HIGH_CAPACITY`至少为`IEC104_ASDU_HIGH_ENTRY_HEADER_LEN + IEC104_MAX_ASDU_LEN`,`IEC104_TCP_NORMAL_CAPACITY`至少为`IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN + IEC104_MAX_ASDU_LEN`;当前x64 ABI下分别为251和265字节。每级总容量还须不超过UINT32_MAX,头长由sizeof推导,过小的配置在编译期拒绝。`Send_Response`直接准备时不经过HIGH,回退排队时使用HIGH;`Enqueue_Event`始终使用NORMAL。该约束保证合法长度不会仅因直接发送转为排队而被判SUBMIT_ERROR;HIGH空闲不足仍返回WAIT,NORMAL仍按原规则覆盖旧原文或因编号窗口受阻返回WAIT。
同一服务的全部调用及业务入队必须串行执行;Create/Destroy还须在各服务之间串行。句柄在Destroy后失效,应立即清空业务保存的指针。Destroy不隐式关闭服务或丢弃事件;业务先Close、读空NextEvent,再归还槽。
业务通知FIFO按实际长度存放,头部`TcpEventHeader_t`含generation、payload_length、type;载荷直接复制公开data成员的有效前缀。I_FRAME只保存`offsetof(IEC104_TcpIFrameEvent_t, apdu) + apdu_length`字节;RECORD和连接/启停通知分别保存对应公开结构的sizeof。头部及载荷均用memcpy读写,跨环尾时分两段复制,不留下空隙,也不强转环内结构体指针。
配置宏名和条数推导保持不变:`EVENT_CLOSE_RESERVE = RECORD_CAPACITY + 2`,`EVENT_NORMAL_CAPACITY = EVENT_CAPACITY - EVENT_STATE_RESERVE - EVENT_CLOSE_RESERVE`。普通配额默认30,表示按最长I帧通知计算的保证条数,供CONNECTED、I_FRAME及正常RECORD共享;启停配额默认4,仍限制ACTIVATED/DEACTIVATED的未读条数;收尾保证32条记录、1个最长I帧及1个DISCONNECTED。全部尺寸由sizeof/offsetof推导,编译器`-D`覆盖仍有效;编译期同时检查字节索引和最短通知可能达到的公开16位计数范围。
当前x64 ABI下头部16字节,I帧通知为`30 + APDU长度`字节,RECORD为64字节,连接及启停通知为72字节:
| 区域 | 字节容量计算 | 默认值 |
|---|---|---:|
| 普通 | 普通保证条数 × 最长I帧通知 | 30 × 285 = 8550 |
| 启停 | 启停保证条数 × 连接通知 | 4 × 72 = 288 |
| 收尾 | 最长I帧 + 全部记录 + DISCONNECTED | 285 + 32 × 64 + 72 = 2405 |
| 字节环合计 | 普通 + 启停 + 收尾 | 11243 |
事件环与接收暂存合计从23680降至13291字节,减少43.9%。2026-10-19字节环改造后,MinGW x64服务对象从31848降至21464字节;2026-10-20调度修复增加8字节,当前为21472字节。完整对象大小还取决于其他内部结构及目标ABI。16字节APDU每项46字节;普通区空闲时可存185条,仍为已受理但等待交付的下一帧保留收尾空间。这里的普通通知区与NORMAL发送队列独立,NORMAL覆盖旧原文不会释放通知FIFO空间。
普通入口同时检查非启停占用加本项不超普通区、总占用加本项不侵占收尾区;CONNECT可按自身实际大小接入,但旧启停通知必须先读完。启停入口统一检查条数配额及完整收尾预留。Append先检查空间,再写头部和载荷,最后提交字节占用及通知计数。全部已入队通知按顺序逐条保存,不覆盖、不合并。
普通通知满仍可按TCP顺序处理前面的S/U,ACK记账不等待业务取出回执。已确认回执在普通空间不足或本轮交付预算用尽时暂留内部,可能晚于随后发生的启停通知交付;保持不因回执积压而暂缓启停受理及CON发送的行为。启停变化由Connection在ACT受理时通过内部适配器立即交付:先查询启停容量,状态改变后复制事件。重复ACT不改变状态时不占新配额;CON完整写出仅确认发送结果。
启停专用槽满时保留完整ACT并返回WAIT,尚未受理、不改变状态;TCP输入按原顺序等待,不越过该请求。已有CON继续写出,发送不再等待状态通知槽。NextEvent消费事件后安排Process重试;原有协议期限继续生效,长期不消费仍可能超时。
I帧按本帧实际长度检查普通空间;不足时将已受理帧保留在Receiver,设置`receive_paused=1`并暂停后续Feed/recv,同时停止转移回执,避免较短的RECORD插到该I帧前面。不覆盖请求、不自动回复业务拒绝,也不因通知积压主动断开连接。NextEvent腾出普通空间后安排本地Process,先复制该帧、Release,再恢复原来的输入尾部和接收关注;不重复处理该帧序号或ACK。等待期间已有发送及协议期限继续推进,不能延长计时来掩盖积压。TCP有序,保留I帧会阻挡其后的S/U;应用必须及时消费事件,持续积压仍可能导致原有协议超时。若此时关闭连接,收尾顺序为剩余I帧、全部记录、DISCONNECTED。旧启停通知须先读完,其他旧通知也须释放出新连接建连通知及完整收尾预留所需的容量,才允许接入新会话。
每轮最多交付8条通知;预算或本类配额耗尽时先返回应用,下一轮继续。普通区放不下最长通知才视为配额用尽,下一轮仍按每项实际长度判断。receive_paused只表示已受理I帧等待普通通知空间,不代表STOP停用或启停ACT等待通知容量。应用仍通过NextEvent消费事件;适配器仅连接TcpServer和Connection,不是业务回调。
NextEvent先检查游标、计数、类型、载荷长度及I帧APDU长度,在清零的临时公开结构中展开;全部校验通过后才写输出并出队。EMPTY/ERROR不改输出,损坏项也不部分出队;短APDU后的数组尾部清零,历史代次照常交付。副本无任何借用指针,可独立保存、按值复制,无需Release。Close保留未消费事件;Close后尚未读空时再次Init或Destroy均返回EVENTS_PENDING。读空后可以Init重开,或Destroy归还槽;同一静态槽重新Create仍保留generation计数。旧事件可以作为历史记录保留,但不能配上另一个服务器句柄重新提交。
`iec104_tcp_event_ring_test.c`以可控Net后端覆盖跨环尾、容量预留、损坏项原子拒绝、短帧背压恢复、回执次序、断线重连及分包。该测试直接包含TcpServer实现,构建时不要再次链接TcpServer.c或Net_Windows:
```powershell
New-Item -ItemType Directory -Path .build -Force | Out-Null
gcc -std=c11 -O2 -I104 -Icommon -Wall -Wextra -Werror test/iec104_tcp_event_ring_test.c 104/IEC104_Connection.c 104/IEC104_Receiver.c 104/IEC104_SendManager.c 104/IEC104_SendBuffer.c 104/IEC104_AsduQueue.c 104/IEC104_Codec.c common/IEC60870_Codec.c 104/IEC104_Link.c 104/IEC104_Link_Slave_StartStop.c -o .build/iec104_tcp_event_ring_test.exe
if ($LASTEXITCODE -ne 0) { throw 'TCP event ring build failed' }
./.build/iec104_tcp_event_ring_test.exe
if ($LASTEXITCODE -ne 0) { throw 'TCP event ring tests failed' }
```
2026-10-19验证:默认配置、`RECORD_CAPACITY=3/EVENT_CAPACITY=12/STATE_RESERVE=2`、`RECORD_CAPACITY=40/EVENT_CAPACITY=100/STATE_RESERVE=6/RX_CAPACITY=4096`均通过11个专项场景,Windows生产库以`-Wall -Wextra -Werror`构建通过,GCC静态分析通过。公开头文件去注释后保持一致,Connection/Net文件哈希以及Process_Input、Read_Client、Receive_Input函数均未改变。接口过期的旧回归测试及专用辅助文件已删除,当前入口和构建命令见[测试说明](../test/IEC104测试文档.md)。
以下是2026-10-19存储改造的历史性能测量,不包含2026-10-20新增的内核输入快照查询及独立发送调度,不能作为当前版本的性能结论。不能保证recv次数及每帧耗时在所有输入下完全相同。模拟Net、每批8帧并安排一次空输入推进发送的对照中,16字节和255字节APDU分别处理160万帧,修改前后均调用recv 40万次(包含空输入检查);5次串行测量的每帧耗时中位数如下,包含测试报文生成、Poll及NextEvent,不代表真实socket的端到端延迟:
| APDU长度 | 固定槽 | 字节环 |
|---|---:|---:|
| 16字节 | 0.125微秒 | 0.148微秒 |
| 255字节 | 0.137微秒 | 0.166微秒 |
一次积压16个满长APDU(4080字节)时,recv调用由1次变为2次,这是暂存从4096缩至2048字节的直接结果;每轮最多交付8条的调度预算保持不变。测量输出与验证记录保存在`.build/fifo-byte-ring-20261019/benchmark-results.json`及`verification.json`。
绑定地址目前只支持数字IPv4字符串,不解析主机名。Init复制配置,`protocol`使用`IEC104_Types.h`中的`IEC104_ProtocolConfig_t`;Connection也使用此公共配置,保留原类型名作为兼容别名。配置必须满足`0 < t2_ms < t1_ms`;`t2_ms >= t1_ms`在申请网络资源及初始化服务队列前返回`INVALID_CONFIG`,error阶段为`VALIDATE`。默认t2=10000ms、t1=15000ms满足该约束。Get_Status返回独立副本,失败不改输出,Create后、Init失败后以及Close后都可查询;关闭后端口为0,最近代次保留。Windows后端保持`SO_EXCLUSIVEADDRUSE`策略,避免同一地址被重复监听。
原TCP调用方需要迁移以下用法并重新编译:
| 原用法 | 现在的用法 |
|---|---|
| 直接定义或清零`IEC104_TcpServer_t server` | 持有`IEC104_TcpServer_t *server = IEC104_TcpServer_Create()`,完成后Destroy |
| Init/Poll/Close/Get_Status/Destroy返回值按0/1判断 | 为调用增加error或NULL末参数,并显式比较`IEC104_TCP_OK` |
| `config.poll_interval_ms`及`Poll(server, error)` | 移除该配置项,直接使用`Poll(server, max_wait_ms, error)`指定本轮等待上限 |
| 接口失败后只查询`last_socket_error` | 直接读取本次`IEC104_TcpError_t`的result/stage/socket_error |
| 读取`server.bound_port/last_socket_error/generation` | Get_Status取得对应快照字段 |
| `config.connection` | `config.protocol`,各协议配置字段含义不变 |
| 入队结果使用`IEC104_ConnectionResult_t`及`IEC104_CONNECTION_*` | 使用`IEC104_TcpSubmitResult_t`及`IEC104_TCP_SUBMIT_*` |
| `Enqueue_Normal` / `Enqueue_High`及`IEC104_TcpEnqueueResult_t` | 分别迁移为`Enqueue_Event` / `Send_Response`及`IEC104_TcpSubmitResult_t`;结果常量改为`IEC104_TCP_SUBMIT_*`,无旧入口包装 |
| 通用业务入队入口及重连保留开关 | 主动上报调用Enqueue_Event;连接应答调用Send_Response;NORMAL随服务保留,Close清空,无兼容包装 |
| 直接使用Connection状态枚举或内部`dt_state` | 使用`IEC104_TCP_SESSION_*`、`IEC104_TCP_FAULT_*`及`data_transfer_started` |
| 发送记录中的`frame_id`、`i_frame_snapshot`、`asdu_priority` | 使用事件generation及`value.frame_serial/send_number/receive_number/apdu_length/priority` |
发送事件不再公开NORMAL队列的内部条目编号。业务身份应由ASDU业务层维护,发送记录只表达传输与链路确认事实。
### 2.1 ASDU队列积压查询
`IEC104_TcpServer_Get_Status`同时提供`high_queue`和`normal_queue`,两者均为`IEC104_TcpAsduQueueStatus_t`数值副本。业务通过Slave_Get_Status取得同一快照,不取得队列指针,不读取Connection、SendManager或字节环成员。状态结构新增字段,库与调用方须一起重新编译;不新增兼容入口。
| 字段 | 含义 |
|---|---|
| `pending_asdu_count` | 队列中尚未交给发送模块的ASDU条目数;不含已组帧、部分写出或等待ACK的帧 |
| `stored_asdu_count` | 队列中仍保留ASDU原文的条目总数;不是累计入队次数 |
| `capacity_bytes` | 本级队列的实际总容量,单位字节 |
| `used_bytes` | 全部保留条目占用的字节数,包含内部条目头及ASDU原文 |
| `free_bytes` | 当前未占用的字节数;与used_bytes相加等于capacity_bytes |
先检查Get_Status返回`IEC104_TCP_OK`,再检查`status.asdu_queue_status_valid`。标志为1时两级统计均有效;与`connection_status_valid`相互独立。Init成功后,即使尚未接入主站或已经断线,也能查询NORMAL缓存。尚未成功Init、Close后或内部队列不可用时,标志为0且两份统计清零;此时表示统计不可用,不能按“没有积压”处理。重复Init被拒绝不会清除仍在运行的服务或使其队列统计失效。
HIGH原文在交给发送模块后立即出队,因此其pending与stored相等;直接进入发送缓冲的应答不经过HIGH队列。NORMAL原文通常保留到ACK后才释放,因此stored可能大于pending。例如10条NORMAL中已有4条交给发送模块且尚未确认,原文均未被覆盖时,pending为6、stored为10。字节占用统计这10条原文及条目头,不统计独立发送缓冲、APCI或发送记录;已覆盖原文对应的待ACK帧不再计入队列统计。
受理新STOPDT_ACT时立即清空HIGH待发应答,不产生逐条作废事件;队列数量归零不代表这些应答已发送。断线收尾也会清空HIGH,并将仍保留的NORMAL恢复为待发;断线期间新增NORMAL继续计入pending。收到EOF后暂留给下一连接的NORMAL也计入pending,因此待发数量不等于“本轮允许立即发送的数量”。队列为空也不表示主站已经确认或业务已经完成,传输完成事实仍从RECORD事件获取;`connection.pending_send_records`另含尚未交付为事件的记录,不能作为队列积压相加计算。
统计用于观察积压趋势及容量占用,不预留空间。ASDU长度不同,free_bytes不能直接换算为可接收条数;NORMAL空间不足时仍可能覆盖旧原文,HIGH直接发送时也可能不占队列。业务提交仍以Enqueue_Event/Send_Response返回值为准。Get_Status只做常数时间查询,不执行I/O、不消费ASDU或推进协议;与其他接口一样,应在服务所属线程串行调用。
### 2.2 接入暂停与错误诊断
`IEC104_TcpServerStatus_t`另提供以下只读字段。可恢复接入失败由模块安排重试,Process/Poll返回OK,不新增业务事件;应用通过这些字段观察持续接入异常。状态结构新增字段后,库与调用方须一起重新编译。
| 字段 | 含义 |
|---|---|
| `accept_paused` | 查询时刻仍在接入暂停期为1;到期立即为0,与是否有接入容量无关 |
| `accept_retry_remaining_ms` | 本次暂停剩余毫秒数;未暂停为0;查询不会刷新期限 |
| `last_accept_error` | 最近一次Net_Accept错误的原生码;初始为0,后续错误更新,成功、WOULD_BLOCK及客户端断开不清除 |
| `consecutive_accept_failures` | Net_Accept连续返回RETRY、RETRY_LATER或ERROR的次数;到UINT32_MAX饱和,OK或WOULD_BLOCK清零 |
连续失败计数只描述接入调用结果,WOULD_BLOCK表示当前没有待接入连接,计数清零不证明资源已经恢复。`last_accept_error`独立于可能被客户端收尾或接入成功覆盖的`last_socket_error`,便于保留接入失败线索;二者均不代表当前API调用失败。成功接入额外客户端并随即关闭也属于Net_Accept的OK,会清零连续失败次数。
Close_Connection和客户端自然断开不提前结束接入暂停,也不清除接入诊断;整个服务Close时撤销重试期限,保留最近错误及连续失败计数供查询。下一次Init重置这些诊断;Destroy结束对象生命周期后不再保留。固定100毫秒暂停不开放配置,持续失败继续按此间隔重试,不自动升级为服务级错误。
## 3. 收发循环
Net后端交付的监听socket与客户端socket均为非阻塞。调度分为两个公开步骤:`Prepare(server, &info, error)`只查询等待条件;`Process(server, &ready, error)`只做有界非阻塞推进。等待由应用的事件循环统一安排,库不会在Prepare或Process中再次调用Net_Wait。
`Poll(server, max_wait_ms, error)`是独立TCP循环的组合入口,内部使用相同的准备和推进核心,并在中间至多调用一次Net_Wait。0表示只查询就绪,正数限制本轮网络等待,UINT32_MAX仍表示有限毫秒数;实际等待还受协议期限、接入/发送重试期限、本地工作和未读事件约束。该参数只约束等待,不保证整个函数的执行耗时。两个入口没有两套收发实现;同一服务由一个串行调度者管理,不应同时给Poll和另一个事件循环安排等待。
Prepare通过内部`Connection_Get_Wait_Time`查询当前帧本地发送、最早未ACK的I帧t1、TESTFR t1、STOP总期限以及可准备S/TESTFR的t2/t3。它不推进协议、不准备发送帧、不更新计时起点、不关闭连接。查询异常返回INTERNAL_ERROR/SCHEDULE且不修改输出;已失败会话则标记立即推进,由Process执行收尾。TcpServer不读取Connection、Link或SendManager的计时字段。
S确认已在途、TESTFR已发送或等待CON、控制槽已满时,当前不能新增控制帧的t2/t3不反复触发零等待;已有帧仍受本地发送期限约束,等待CON仍受t1约束。容量或协议门控恢复后,按原始期限重新推进。
每轮Process共用最多32次Feed、32次发送尝试、4次accept和8条通知交付的预算;断线收尾必须一次完整交付,不受通知预算限制。可读/接入通知一直保留到实际操作返回WOULD_BLOCK;预算耗尽或交付用尽本类配额时,可立即推进的工作由下次Prepare要求续跑,不依赖新的边沿;接入/发送暂停期间则等待各自的重试期限。发送预算耗尽、有界无进展重试、活动连接的HIGH入队、未EOF时的NORMAL入队及NextEvent释放通知槽也会安排本地推进;无连接或EOF后的NORMAL只缓存。普通区满但尚无受阻I帧时仍关注可读;出现`receive_paused`后撤销客户端READ关注,并停止为该受阻输入反复安排本地续跑,保留接收边沿供恢复使用。协议WAIT仍等待控制发送或期限解除。已准备好待发字节、未处于发送重试暂停且不因启停通知槽不足而暂缓时才关注可写;消费通知后通过本地唤醒恢复,不能把“业务发送队列非空但等待STARTDT/ACK”作为持续关注可写的理由。
Net_Accept返回RETRY_LATER时,立即结束本轮接入并保存`accept_retry_at_ms = Now_Ms() + 100`,保留`accept_pending`及当前连接。普通RETRY仍可在本轮4次预算内继续;若整轮4次全部为RETRY,也进入相同暂停,避免每轮有预算却跨轮不断立即重试。曾有OK接入进度的预算耗尽仍按原规则续跑,额外客户端被接入后关闭同样算进度。WOULD_BLOCK清除接入待处理标志,真正的监听失败仍返回NETWORK_ERROR。
暂停状态统一定义为`now_ms < accept_retry_at_ms`,不另存需要解除的标志;等于期限即已到期。Prepare只读取一次时钟,用于监听关注、本地续跑、协议期限和接入期限的合并。暂停期间移除监听READ关注,`accept_pending`不单独触发need_process;Process末尾真正调用Accept_Clients之前再次按当前时钟检查期限,客户端收发、NextEvent或迟到监听通知均不能提前触发accept,也不能刷新暂停期限。暂停只限制接入,当前客户端的收发和协议期限继续推进。
到期后保留的accept_pending配合原有容量规则安排本地重试,不依赖新的监听边沿。无客户端且通知容量仍不足时,不恢复监听关注、不因接入请求续跑,也不把已过期的接入期限保留为零等待定时器;业务消费NextEvent后再恢复接入。客户端在暂停期间断开不会提前结束暂停,新主站仍须等待剩余时间。
`Send_Response`仅在Connection返回`OK`(已入HIGH)或`SEND_READY`(已准备APDU)时,为本次提交设置`process_pending`。`WAIT`/`ERROR`不因提交失败新增本地唤醒,也不清除已有标志;后续就绪I帧和确认记录仍照常交付。接收恢复、发送重试、预算续跑、事件消费及协议期限继续按各自条件安排推进。HIGH满且发送受k窗口限制时,失败提交不会反复把Poll等待强制归零;业务保留应答,在后续调度间隙重试。
当前接收轮次产生业务事件后,不再继续recv;已有TCP尾部在Feed及通知预算内处理,然后以Tick或Yield_Receive_Batch结束当前轮次。发送始终使用独立的32次预算,不能因8条通知预算耗尽而跳过;部分发送及不可写仍按原规则处理。旧回执转移安排在输入处理后,避免先交付旧回执而延误已到达的ACK。应用先取出并处理事件,再执行下一轮Process/Poll,使应答有机会在下一次读取EOF之前发送或入HIGH。EOF后停止接收,继续有界排空当前可发送内容,不因接收尾部保留而反复立即调度。
同一轮收到的socket就绪与定时事件应合成一个ready批次。Process先处理已有输入,再准备下一次发送;本地/定时唤醒也允许非阻塞查看已到达的输入。每轮接收以Connection的`Begin_Receive_Batch`开始。仅因Feed/通知预算或交付让出而停止时,TcpServer保留用户态尾部,并通过`Net_Pending_Input`只查询一次当时内核中的剩余字节数,形成有限输入快照。还有快照输入时调用`Yield_Receive_Batch`结束这一轮,暂缓I帧ACK及TESTFR_CON判定,Prepare返回`need_process=1/timeout_ms=0`要求立即续收;不能阻塞等待。后续recv长度不超过快照的剩余字节数,不把新到达的流量加入旧快照。
快照处理完后立即用`Tick`按原起点检查确认期限,持续的新I/S流量不能无限延长旧t1。真正的通知容量背压、无法解除的WAIT、recv无进展或EOF仍用Tick结束批次;该机制不承诺隔离业务长期积压。本地发送和STOP总期限、格式及序号错误始终有效。它不读取内核到达时间戳,也不重置任何计时起点。2026-10-20的调度测试覆盖7/8/9/33个前置I帧、任意recv分段、满长突发、固定快照外的新流量、TESTFR确认、连续接收下发送、部分发送及真实缺失确认。
### 3.1 接入应用自己的事件循环
`IEC104_TcpWaitInfo_t`中的字段按以下规则使用:
| 字段 | 应用处理 |
|---|---|
| `sockets[0..socket_count)` | 当前完整关注集合,最多两项;新增/更新注册,移除旧集合中已不存在的项及已撤销的READ/WRITE位 |
| 每项`socket/token/events` | 保存数值句柄及注册身份,按READ/WRITE注册;返回就绪时回传原身份,只填写实际就绪位 |
| `service_token` | 本次Init身份,保存在定时器、本地唤醒和就绪批次中 |
| `timer_active/timeout_ms` | 协议检查与接入/发送重试共用的最近等待上限;活动时替换一次性定时器,0毫秒立即调度;非活动时取消;UINT32_MAX不是无限等待 |
| `need_process` | 安排一次本地推进,不能阻塞等待新socket事件 |
| `pending_event_count` | 先通过NextEvent处理业务事件,再重新Prepare |
| `receive_paused` | 已受理I帧因通知容量不足暂留内部;消费普通通知后安排Process交付并恢复接收 |
存在客户端时,查询的协议等待上限最多为UINT32_MAX毫秒;没有更早期限仍可在该上限唤醒检查。接入或发送暂停尚未到期时,把各自剩余时间与协议等待上限取较小值。接入暂停即使没有客户端,也返回timer_active=1及接入重试剩余时间。没有客户端且没有尚未到期的接入暂停时,timer_active=0;到期后能否接入由容量和need_process表达。timeout_ms从本次查询时刻开始计算,处理其他任务较久后必须重新Prepare,不能用旧相对时间推迟期限。每次Process、Enqueue_Event、Send_Response、Close_Connection或事件消费改变状态后,下一次等待前都要重新查询。
接入暂停时,监听socket会暂时从Prepare的完整集合中消失,应用必须撤销旧注册;否则一直可读的监听socket仍可能使水平触发的外部等待空转。暂停结束后按最新快照恢复注册,监听socket和service_token的生命周期没有改变。发送暂停时同样须撤销客户端WRITE位,并保留仍需处理的READ位;客户端不再有任何关注位时将其移出集合。`ready.timer_expired`只表示定时唤醒,实际协议及接入/发送期限仍按当前单调时钟判断,提前或迟到的通知不会提前解除暂停。
应用循环的调用关系如下,其中“统一等待”由应用实现,可同时等待TCP、其他模块和业务定时器:
```text
NextEvent处理业务事件(可调用Send_Response或Enqueue_Event)
-> Prepare取得最新info
-> 更新完整socket注册集合和一次性定时器
-> need_process时安排本地唤醒,否则统一等待全部模块的事件
-> 合并本轮TCP socket就绪与定时通知,构造ready
-> Process(server, &ready, &error)
-> 处理其他模块任务,回到循环开头
```
本地唤醒时把ready清零、复制最近info.service_token即可;定时器触发再置timer_expired=1。socket通知每项复制注册时的socket/token,把events替换成本轮实际就绪位。同一socket只放一项,可读和可写合并;ERROR可表示平台报告的错误/挂断提示,Process通过实际I/O判定结果,不能把它直接当作某种协议故障。不要只把info原样复制成ready并把关注位当成实际就绪。即使需要立即推进,也应收集已经可用的socket通知,与定时提示一起交付。
socket只是借给事件循环注册,所有权仍属于TcpServer;应用不能对它recv/send/close或修改非阻塞属性。Windows事件循环适配代码可把`IEC104_TcpSocket_t`转换成SOCKET用于select等就绪机制,业务ASDU处理无需看到系统类型。TCP内部的Net后端仍负责所有真实I/O。此接口面向就绪驱动,不能直接把重叠I/O完成通知当作就绪位,或让异步I/O继续借用库内缓冲。
注册身份与业务generation分开:监听和客户端每次创建都获得新的非零token;计数跨Close/Init及Destroy/Create保留,不回绕。Process先检查service_token,旧服务整批忽略;当前服务中的旧socket/token逐项忽略,全部是旧socket通知且没有定时事件时不推进当前连接。不能为了处理旧通知而替换成最新token。旧定时器若属于同一次Init,最多触发一次按当前状态计算的提前检查,不能沿用旧时间差判定新连接超时。
Process/Send_Response/Close_Connection/Close可能关闭socket,应用须撤销旧注册;准备销毁服务时还须取消定时器和本地任务,避免继续使用已Destroy的句柄。边沿触发应保留need_process续跑;单次通知模式须在每轮按新快照重新布防。工作线程的结果由应用自己的唤醒机制送回事件循环线程,再调用Send_Response或Enqueue_Event并刷新等待条件。库不创建跨线程唤醒或注册回调。
接收路径:
```text
Net_Recv -> 固定接收缓冲 -> Connection_Feed(consumed_length)
-> I帧复制为事件,成功入队后Release;业务通过NextEvent取副本
-> S/U由Connection受理并释放
```
TCP层不解析APCI,也不假定一次recv对应一帧。Feed按实际消费量推进位置,剩余尾部跨Process/Poll保存;半帧由Receiver保留。WAIT不丢弃原文,发送和Tick继续驱动。即使旧S/U本轮刚释放、Feed消费0字节,也在预算内继续尝试尾部,不无限重试。
发送路径:
```text
Connection_Prepare_Next_Send == SEND_READY
-> Net_Send(pending_data, remaining_bytes)
-> Connection_Handle_Send_Progress(frame_id, 本次成功写入量)
```
每次Net_Send返回OK后立即回报实际增量,再处理后续接收事件;可读和可写同时就绪时,先处理已有接收字节,因为之前的同步send已全部记账,无须先发送新的帧才能处理旧帧的ACK。部分写入后重新Prepare取得同一APDU的剩余字节,不交错发送。`IEC104_NET_WOULD_BLOCK`保留当前帧,清零无进展重试计数并等待可写通知;`IEC104_NET_RETRY`及OK但写入0字节按下述有界策略重试,不提前Complete、不重置期限。最终发送失败调用Handle_Send_Failure并关闭客户端。发送完成回报即使因后续协议推进返回FATAL,也不重报已登记的增量。
发送准备及进度回报仅因本地发送和STOP期限直接报告超时,I帧ACK与TESTFR_CON的t1故障在接收阶段判定。Write_Output仍在Net_Send返回后读取真实时刻,供写入记录、活动时间和新TESTFR计时使用。若发送期间跨过旧确认的t1,快速应答已有process_pending;常规发送结束也只读查询协议期限,到期时登记立即续跑。下一轮Process先按既有预算和固定输入快照处理ACK/CON,再由Tick按原始起点判断;Prepare报告零等待,不依赖新的可读边沿。没有有效确认时仍超时,真正事件容量背压仍执行原期限,不新增1秒或其他固定宽限时间。Submit_Response入口发现期限已到时仍只入HIGH,不能靠连续提交应答推迟检查。
暂缓确认超时判定期间,旧I帧ACK或TESTFR_CON的t1已经到期时,Connection_Prepare_Next_Send暂停为HIGH/NORMAL创建、编号新I帧。已准备的当前I帧(含尚未写出字节的帧)及S/U仍按原顺序发送,以释放控制槽并继续受理接收报文;已有帧排空后返回WAIT,使接收解阻用的Flush_Output及时退出,不能顺带发出新的业务帧。有效确认解除所有已到期等待后自动恢复准入,未解除则由接收阶段按原期限断开。准入限制同时覆盖接收批次内、预算续收期间和常规发送,不改变未确认NORMAL在断线后的保留规则。
发送无进展采用跨轮预算:每次实际Net_Send返回OK+0或RETRY,累计一次`send_retry_count`,两种结果混合返回也共同计数。前3次安排本地续跑;第4次清零计数并保存`send_retry_at_ms = Now_Ms() + 10`,暂停发送。计数由Process普通发送、接收WAIT解阻发送和Send_Response快速入口共用,不能靠进入下一轮或换入口重置。写入正数字节、返回WOULD_BLOCK或已无待发帧时清零;断开及新连接初始化同时清除重试期限和计数。10毫秒为内部固定重试间隔,不新增业务配置,不改变正常有进展的发送路径。
发送暂停统一按`now_ms < send_retry_at_ms`判断。Prepare在同一次时钟快照中撤销客户端WRITE关注,并将剩余暂停时间与协议及接入期限取最小值;READ仍按接收背压规则保留。所有实际send都经过Write_Output的期限检查,因此接收就绪、NextEvent、业务提交、迟到的WRITE或提前的定时提示均不能提前写入,也不能延后原重试时刻。暂停不新增发送续跑,但保留其他工作已经登记的process_pending;这些工作可以推进一次,实际发送仍受期限限制。
等于或超过重试期限时,Prepare通过need_process主动安排发送,不依赖新的可写边沿。Write_Output在检查启停通知槽之前消费已到期的重试期限;若此时通知槽不足,转为等待NextEvent,不能保留已过期的期限反复零等待。已无待发帧也清除重试状态。重试后再次连续4次无进展,重新暂停10毫秒;Process和快速入口本身均不睡眠,等待由Poll或应用事件循环完成。
暂停期间接收及Tick继续运行,本地发送超时仍从原queued_at_ms计算,部分发送、零字节、RETRY及调度暂停都不续期。更早的发送或协议期限优先唤醒并按原规则收尾。因此修复消除持续无进展造成的忙等,不取消长时间无法完成发送时的超时断开;EOF后的待发内容也遵循相同规则。
Connection的Feed、Prepare_Next_Send、发送回报及Tick均可能推进保留帧;模块统一检查就绪I帧并复制事件,已确认记录也在复制事件后才释放。TcpServer_Prepare只查询等待条件,与内部Prepare_Next_Send不同。所有`now_ms`来自`Net_Now_Ms`,Windows后端内部使用`GetTickCount64`;日历时间/校时不影响协议期限。调用方长时间不调用Process/Poll或业务主循环阻塞,都会延迟收发和定时处理。
## 4. 业务接入
应用每轮Process/Poll后主动取事件,TCP不调用业务函数:
| 事件类型(均带`IEC104_TCP_EVENT_`前缀) | 有效载荷与契约 |
|---|---|
| `CONNECTED` | `data.connection`包含TCP建连状态及`peer`地址副本,不表示104已STARTED |
| `ACTIVATED` | `data.connection`保存STARTDT_ACT成功受理并激活后的快照;CON可能尚未写出,data_transfer_started=1 |
| `DEACTIVATED` | `data.connection`保存STOPDT_ACT成功受理并停用后的快照;ACK或CON可能仍待完成,data_transfer_started=0,TCP保持 |
| `I_FRAME` | `data.i_frame`保存完整APDU数组、长度、N(S)/N(R)及receive_serial |
| `RECORD` | `data.record.value`为发送记录;closing=0表示正常ACK回执,closing=1表示断线时剩余记录 |
| `DISCONNECTED` | `data.connection`为收尾前状态及原主站`peer`;status_valid仅控制status有效性,socket_error为后端诊断码 |
所有事件都有`generation`,同一代次以CONNECTED开始,以DISCONNECTED结束。连接建立、激活、停用和断开均使用`data.connection`,携带原主站的地址副本;启停事件的socket_error固定为0,不沿用此前网络操作的历史诊断。closing=1不等于发送失败或成功,仍须检查`local_send_completed`和`peer_acknowledged`。TCP有序关闭不新增协议fault,收到链路ACK也不表示对端业务已执行。
ACTIVATED/DEACTIVATED反映ACT受理时的实际状态变化;初始STOPPED、同状态重复ACT、CON发送完成及单纯断开不额外产生启停事件。每次变化立即入FIFO,保留激活、停用、再激活的顺序。后续发送失败不会撤销此前事件,DISCONNECTED最后交付。通知容量不足时延后ACT受理,不会先改变状态再丢弃通知。
STOP受理即产生停用变化并关闭新I帧接收,非法I帧不交付业务。后到START成功受理重新激活并开放I帧;已有STOP_CON继续按FIFO发送,旧完成不改变当前状态。取消前按序收到的非法I帧仍断链。
首次START_CON未完成时也能受理STOP:状态事件在受理时依次为ACTIVATED、DEACTIVATED,不等CON写完。随后再受理START产生新的ACTIVATED;已经生成的CON不撤回,尚未生成的旧STOP_CON可因重新激活而不再生成。默认4个启停通知槽,满时延后受理改变状态的ACT。
业务事件是历史事实副本,不保证当前仍连接或处于同一状态。首次ACTIVATED在START受理时生成,先于该次激活后合法I_FRAME的交付;应答是否接手仍由提交时的实际状态判断。下面函数由应用在读取NextEvent后自行调用,不向TcpServer注册:
```c
#include "IEC104_TcpServer.h"
#include <inttypes.h>
#include <stdio.h>
void App_Log_Connection_Event(const IEC104_TcpEvent_t *event)
{
const char *name;
switch (event->type) {
case IEC104_TCP_EVENT_CONNECTED: name = "connected"; break;
case IEC104_TCP_EVENT_ACTIVATED: name = "activated"; break;
case IEC104_TCP_EVENT_DEACTIVATED: name = "deactivated"; break;
case IEC104_TCP_EVENT_DISCONNECTED: name = "disconnected"; break;
default: return; /* I_FRAME和RECORD由原有业务分派处理。 */
}
printf("%s: generation=%" PRIu64 ", %s:%u\n", name, event->generation,
event->data.connection.peer.address, (unsigned)event->data.connection.peer.port);
}
```
处理I_FRAME时,ASDU首地址为`event.data.i_frame.apdu + IEC104_APCI_LEN`,长度为`event.data.i_frame.apdu_length - IEC104_APCI_LEN`。接收入口已保证6字节公共头完整,并对已知固定长度类型按VSQ数量和SQ位检查最低载荷长度;不足时按FRAME故障断连,不处理该帧的序号或ACK,也不上报I_FRAME事件。该检查不要求长度恰好相等,未知/变长类型、零对象和具体字段是否合法仍由业务检查,范围见[基础设计的ASDU长度检查](IEC104_设计架构.md#43-已知类型的最低载荷长度)。需要取得公共头字段时,显式包含 `IEC60870_Codec.h` 并调用 `IEC60870_Parse_ASDU_Header`。提交业务应答时保留事件中的generation:
```c
IEC104_TcpSubmitResult_t result = IEC104_TcpServer_Send_Response(
server, event.generation,
response_asdu, response_length);
```
返回类型为`IEC104_TcpSubmitResult_t`,结果须显式判断:
| 结果(均带`IEC104_TCP_SUBMIT_`前缀) | 含义 |
|---|---|
| `OK` | 已复制到发送缓冲或HIGH,可以复用输入原文;可能已写完、仅写出部分、暂不可写或仍在排队,不承诺对端已ACK;尚在HIGH中的应答会在受理新STOP时作废 |
| `WAIT` | HIGH当前容量不足,本次未接手,业务保留原文后重试 |
| `ERROR` | 参数、阶段、连接身份或总容量不符;无连接、旧代次、EOF后、未启动且无启动CON待发、停止排空及停止CON待发均拒绝应答 |
| `FATAL` | 服务队列异常,或会话失败并收尾;可能已经写出部分或整帧,不能直接重放,须处理RECORD/DISCONNECTED |
应答仅在STARTED/IDLE接手,其余状态在记录、窗口和HIGH容量判断前返回SUBMIT_ERROR,不入HIGH也不因此断链。普通路径先Drive再判断实际状态;期限已到时直接Enqueue(HIGH)仍使用同一准入函数,不能用此前Get_Status快照代替。
准入后,发送缓冲为空、窗口和记录有余量、HIGH无旧应答且公平配额允许时,直接准备I帧并尝试一次非阻塞send。已有CON或其他帧时暂存HIGH,不能越过FIFO;发送暂停及原期限保持有效。START受理即可接手,不再等待CON完整写出才激活。
一旦APDU已准备,部分写入或暂不可写都保留该帧,后续从剩余字节继续;不会把原文再次放入HIGH。快速路径和队列路径使用同一N(S)、完成回报和ACK处理逻辑,均不把旧连接应答交给新连接。OK后不可重复提交同一应答。调用后重新Prepare刷新socket、定时及本地续跑条件;发送失败可能已关闭当前socket。
SUBMIT_OK表示已接手,不保证HIGH积压最终发出;STOP受理时清理待发HIGH并产生停用事件,没有逐条作废回执。已有当前帧和记录继续完成,NORMAL保持原生命周期。从停用状态重新START会清理HIGH和配额;已启用时重复START保留HIGH,这是此前确定的项目队列策略。
NextEvent返回后副本归业务所有,业务处理失败时自行保存副本,事件FIFO不会自动重投。主站请求通过I_FRAME事件交给业务解析;HIGH缓存的是业务生成的应答ASDU。
主动上报使用服务级NORMAL,不需要先取得连接代次:
```c
IEC104_TcpSubmitResult_t result = IEC104_TcpServer_Enqueue_Event(
server, event_asdu, event_length);
```
Enqueue_Event只复制到服务级低优先级事件队列(内部NORMAL),不立即发送;OK表示已入队,WAIT表示本次未接手,ERROR表示输入/阶段非法,FATAL表示队列异常。Init成功后、首次连接前和断线期间均可缓存;尚未STARTDT也可入队。重连后按现存原文顺序重新构帧,STARTDT完成后才发送。NORMAL满时覆盖最旧原文,包括未ACK原文;未ACK数据重发可能重复,不提供掉电持久化。EOF后全部尚未编号的NORMAL及后来新增的NORMAL均留给下一连接,已经编号的当前帧继续完成。离线入队不设置need_process,避免无连接空转;有连接且未EOF时成功入队会要求本地推进。事件提交和应答发送都须在所属事件循环线程串行调用。
接收和发送接入同一个服务对象。以下示例使用Poll;应用事件循环使用第3.1节的Prepare/Process流程,初始化、事件读取、入队和收尾相同。下例中的`App_Handle_Event`由业务实现,按上述事件类型分派;它由应用主循环直接调用,可以调用Send_Response提交应答。传入的event在下一次NextEvent覆盖前有效,需稍后处理时按值复制整个事件:
```c
#include "IEC104_TcpServer.h"
#include <stddef.h>
static IEC104_TcpServer_t *server; /* 只保存句柄,实际对象由模块静态持有。 */
static IEC104_TcpServerStatus_t last_status; /* 应用留存端口和会话快照。 */
static IEC104_TcpError_t last_error; /* 本次服务操作的结果、失败阶段及网络诊断。 */
/* 应用自己的函数,不向TcpServer注册。 */
void App_Handle_Event(IEC104_TcpServer_t *server, const IEC104_TcpEvent_t *event);
static int App_Read_Events(void)
{
IEC104_TcpEvent_t event;
IEC104_TcpEventResult_t result;
while ((result = IEC104_TcpServer_NextEvent(server, &event)) == IEC104_TCP_EVENT_READY) {
App_Handle_Event(server, &event);
}
return result == IEC104_TCP_EVENT_EMPTY;
}
int App_Tcp_Start(void)
{
IEC104_TcpServerConfig_t config;
if (server != NULL) {
return 0; /* 先结束上一轮,不能覆盖仍持有的句柄。 */
}
server = IEC104_TcpServer_Create();
if (server == NULL) {
return 0;
}
IEC104_TcpServer_Config_Default(&config);
if (IEC104_TcpServer_Init(server, &config, &last_error) != IEC104_TCP_OK) {
/* 后续查询和清理传NULL,保留last_error中的初始化失败原因。 */
(void)IEC104_TcpServer_Get_Status(server, &last_status, NULL);
(void)IEC104_TcpServer_Destroy(server, NULL); /* 新建句柄初始化失败,没有资源或事件。 */
server = NULL;
return 0;
}
(void)IEC104_TcpServer_Get_Status(server, &last_status, NULL);
return 1;
}
/* 初始化成功后持续调用;共享业务主循环传0,专门的TCP循环可传50等正数。 */
int App_Tcp_Step(uint32_t max_wait_ms)
{
IEC104_TcpResult_t result = IEC104_TcpServer_Poll(server, max_wait_ms, &last_error);
if (result != IEC104_TCP_OK) {
(void)IEC104_TcpServer_Get_Status(server, &last_status, NULL);
(void)IEC104_TcpServer_Close(server, NULL);
}
if (!App_Read_Events()) {
return 0;
}
return result == IEC104_TCP_OK; /* 应用自己的0/1约定,不把TCP枚举直接当作布尔值。 */
}
int App_Tcp_Stop(void)
{
if (server == NULL) {
return 1;
}
if (IEC104_TcpServer_Close(server, &last_error) != IEC104_TCP_OK || !App_Read_Events()) {
return 0;
}
/* Close后先交付全部收尾事件,随后归还静态槽。 */
if (IEC104_TcpServer_Destroy(server, &last_error) != IEC104_TCP_OK) {
return 0;
}
server = NULL;
return 1;
}
```
App_Tcp_Start返回0时处理初始化失败;新建句柄上的Init失败会在示例中保存last_error并归还槽。生命周期错误返回明确分类,error.socket_error保持0;Create仍以NULL表示无可用槽,不产生网络诊断。示例中的App函数沿用应用自己的0/1约定,底层统一结果接口则必须比较IEC104_TCP_OK。成功后持续调用App_Tcp_Step(0)或App_Tcp_Step(50),按本轮业务调度决定是否等待;0模式下仍须由主循环持续调度Poll,协议期限不会由后台线程自动推进。Step失败时先记录last_error,随后调用App_Tcp_Stop,因为Stop自身的诊断会覆盖该变量。正常退出也调用Stop,保证归还句柄。实际端口和会话状态使用Get_Status输出,本次失败使用error,不能访问对象成员。事件与应答入口只接受ASDU原文(6..249字节),不包含APCI;不用手动构造I帧或调用socket的send。请求应答通过Send_Response在处理I_FRAME事件时提交,条件允许就立即尝试发送;主动上报通过Enqueue_Event进入服务队列。连接建立时尚未STARTED,实际业务发送受启停和k窗口约束。START尚未受理时,Send_Response返回ERROR,不能在CONNECTED事件中预存应答等待启动。START成功受理即开放准入,已有CON时应答入HIGH;已启用时重复START保留HIGH。
业务异步处理时可以复制整个事件交给工作线程,完成后由应用唤醒所属事件循环线程,再用保存的generation调用Send_Response并刷新Prepare结果;主动上报调用Enqueue_Event。不能从工作线程并发调用NextEvent、Enqueue_Event或Send_Response,也不能改用新连接的generation提交旧请求的应答。处理WAIT时自行保存应答并在后续调度间隙重试;对ERROR或FATAL应停止该次提交并按业务策略处理,不要无限重试。
### 4.1 对端查询与单连接关闭
对端地址查询与关闭连接是独立操作,Slave门面调用本节的TCP接口,不向业务交付Connection对象。查询使用接入时缓存的数值副本;关闭使用当前连接代次及统一结果分类。
`IEC104_TcpServer_Get_Remote_Address(server, &peer, error)`仅在OK时写入`IEC104_TcpPeerInfo_t`:
| 字段 | 含义 |
|---|---|
| `address[16]` | NUL结尾数字IPv4字符串,不含端口;当前未提供IPv6后端 |
| `port` | 主站TCP源端口,主机字节序,与子站监听端口bound_port不同 |
| `generation` | 当前主站非零代次,后续主动断开须携带原值 |
查询不执行I/O、不推进协议、不改服务状态;未Init或已Close返回INVALID_STATE,已Init但无客户端返回NOT_CONNECTED,其余参数/句柄/重入按统一结果处理,失败保持peer原值。EOF发送收尾期间仍持有客户端,查询可成功。CONNECTED、ACTIVATED、DEACTIVATED和DISCONNECTED均在`event.data.connection.peer`保留同一结构的副本,peer.generation等于外层event.generation;该地址不受协议status_valid影响。处理历史事件时直接使用副本,不查询当前新主站地址来标记旧连接。
主动断开当前主站的调用示例:
```c
#include "IEC104_TcpServer.h"
IEC104_TcpResult_t App_Close_Current_Connection(
IEC104_TcpServer_t *server, IEC104_TcpError_t *error)
{
IEC104_TcpPeerInfo_t peer;
IEC104_TcpResult_t result = IEC104_TcpServer_Get_Remote_Address(server, &peer, error);
if (result != IEC104_TCP_OK) {
return result;
}
/* 可先记录peer.address、peer.port及peer.generation,再结束此次查询对应的会话。 */
return IEC104_TcpServer_Close_Connection(server, peer.generation, error);
}
```
若是针对某个历史请求、事件或异步任务执行断开,直接传该任务保存的generation,不能重新查询最新代次来替换目标。generation为0返回INVALID_ARGUMENT;服务未Init返回INVALID_STATE;无客户端返回NOT_CONNECTED;当前存在其他代次返回STALE_GENERATION,阶段为GENERATION。这些拒绝分支都不关闭任何连接或追加收尾事件。两接口均须回到所属事件循环线程串行调用。
Close_Connection直接复用Drop_Client:关闭客户端,将剩余已受理I帧、发送记录及DISCONNECTED复制到事件FIFO,清理HIGH,恢复现存NORMAL的离线待发状态。它不发送STOPDT、不等待应答排空或ACK,不新增协议故障,不补造发送成功。重复断开已结束的连接只返回NOT_CONNECTED,不重复发事件;如果新主站已经接入,则旧代次返回STALE_GENERATION。若队列解除绑定失败,返回INTERNAL_ERROR/QUEUE_STATE,客户端已结束、监听仍在,应用须Close服务处理异常。
成功后监听地址、端口和service_token不变,后续Process按接入暂停与通知容量规则继续接入新主站;先消费事件并重新Prepare,移除旧客户端注册、更新定时和本地续跑条件。Close_Connection不限制主站再次连接。现存NORMAL可继续离线新增,重连并完成STARTDT后发送;未ACK原文恢复可能重复。Close仍负责关闭整个服务并清空所有ASDU缓存。
## 5. 启停、断线和重连
TCP连接后初始STOPPED,仅交付CONNECTED。START受理即激活并交付ACTIVATED,STOP受理即停用并交付DEACTIVATED;CON随后按FIFO完整写出,完成不再提交启停状态。后到START可结束尚未生成CON的停止等待;已生成的STOP_CON仍发送但不能覆盖新激活状态。停用不关闭TCP,可继续控制通信和重新启动。
EOF表示对端关闭发送方向:先调用AsduQueue_Limit_Normal_Send停止选取全部尚未编号的NORMAL,再记录`rx_eof`并结束当前接收批次。EOF前已入队及EOF后新增的未编号NORMAL都留给下一连接,避免在已无法取得新ACK的旧连接上继续新增上报帧。后续Process/Poll不再recv或关注该客户端的可读事件,继续通过发送调度处理当前已编号帧、此前已受理的HIGH及控制帧。已有当前I帧即使尚未写出字节也继续按原身份完成,不撤回N(S)。部分写入和`WSAEWOULDBLOCK`保留当前帧并关注可写;连续OK+0/RETRY仍执行发送暂停与定时恢复,发送预算耗尽安排本地续跑,均沿用原本的发送期限;不会仅因收到EOF而丢弃待发应答。Tick和其他协议期限仍持续推进,发送失败或超时按原故障路径收尾。
内部发送准备返回WAIT后,先按原有顺序和通知预算转移已确认记录;只要实际回收了记录槽,就重新准备发送,不用回收前的WAIT结果决定关闭。重试以实际回收为前提,受本轮通知预算约束。若仍WAIT,通过Connection.Get_Record_Wait确认是否仅受本地可回收记录容量限制:记录槽已满且队首已完整写出、已ACK,同时启停/k窗口允许新I帧、确认t1未到期。满足时保留连接及已接手的HIGH;只是本轮交付预算不足则安排下一轮Process,通知空间不足则由NextEvent腾出空间后唤醒。等待记录空间时不关注可写,也不持续为满FIFO安排本地续跑,原有协议及发送期限照常检查。
EMPTY表示当前帧、HIGH及控制发送均已排空,即使仍有未编号NORMAL也结束连接;其他WAIT表示仍需新的ACK/STARTDT等对端输入或已经受确认期限限制,也结束连接。不能仅因存在旧的已确认记录,就掩盖同时存在的窗口已满或停用状态。EOF后的等待只用于回收已经获得确认的记录,不等待新的对端报文,也不等待异步业务追加应答。业务应在取得I_FRAME事件后、下一次Process/Poll前入队,或在正常双向连接期间完成异步应答。协议错误、发送失败或期限到达仍可立即收尾,历史I帧事件不保证连接此刻仍可发送。
有序关闭本身不新增连接故障;接收错误调用Handle_Transport_Closed,发送错误先调用Handle_Send_Failure。协议FATAL保持原故障原因。收尾时结束实际I/O、交付剩余记录及关闭通知,最后Connection_Close并清空TCP接收尾部、EOF标记及发送重试期限/计数。监听socket继续等待新连接,新连接正常接收。正常TCP关闭不等于业务全部成功,未完成或未ACK的I帧仍通过`closing=1`的记录交付,不补造发送成功或对端确认;现存NORMAL在解除连接绑定后继续保留,等待新连接启动。
收尾按偏移只读复制内部发送记录,仅在RECORD成功入队后累计`records_saved`。生成DISCONNECTED时,查询关闭协议前的状态,并从内部记录总数中扣除`records_saved`,得到尚未转为事件的记录数;实际记录仍由最后的Connection_Close统一清理。读取记录返回EMPTY表示已读完,ERROR或事件入队失败则走现有收尾异常处理,快照仅扣除成功转移的部分,不能无条件清零。状态查询失败或成功转移数大于内部总数时,断开快照标记`status_valid=0`;计数不一致也按收尾异常处理。RECORD和DISCONNECTED的先后顺序及各记录中的发送、ACK事实保持不变。
每次接受主站都分配递增的非零generation,新连接重新进行STARTDT。代次到UINT64_MAX后拒绝继续分配,Poll返回GENERATION_EXHAUSTED且阶段为GENERATION,不回绕复用;该静态槽归还后也不再Create分配。同一槽Close/Init或Destroy/Create均沿用代次计数。句柄仅在一次Create到Destroy之间有效;业务在Destroy前须结束引用它的旧任务,也不能把旧事件交给另一个服务器句柄。
队列生命周期固定属于服务:Init先以内部代次0建立离线队列;客户端关闭时,在Connection_Close之后调用AsduQueue_Detach,立即清空HIGH、恢复现存NORMAL待发状态并回到代次0;断线期间新增NORMAL继续追加到同一字节环。新连接使用AsduQueue_Rebind绑定新的非零代次,再初始化Connection。绑定转换失败报告服务级INTERNAL_ERROR,不能重新Init队列而静默丢失缓存。主动Close整个服务才清空队列,下一次Init从空队列开始。
不再提供重连保留开关或旧通用入队接口。NORMAL用于面向同一业务接收方/冗余域的主动事件,业务须允许未ACK原文重发;模块不认证主站身份、不管理冗余组,也不实现应用去重。
## 6. 平台边界与接入约定
`Net.h`只包含标准C类型,不依赖Connection、Link或业务结构。后端不维护104状态,也不处理业务事件队列、记录消费、接收预算或NORMAL重连保留;这些职责留在TcpServer。后端使用以下统一结果:
| 结果 | TcpServer处理 |
|---|---|
| `OK` | 收发按实际字节数推进;Wait的就绪位为0表示未报告socket就绪;发送0字节不当作EOF,与send的RETRY合计连续4次后暂停发送10毫秒 |
| `WOULD_BLOCK` | 本次暂不可接受连接/读写,保留状态后续重试 |
| `RETRY` | accept在4次预算内重试,整轮均为RETRY时暂停100毫秒;send与OK+0合计连续4次后暂停发送10毫秒;recv结束本次尝试并交还调度;等待中断后仍推进接收和Tick |
| `RETRY_LATER` | 仅Accept使用:资源暂缺,立即暂停接入100毫秒后重试,保留监听和活动客户端 |
| `EOF` | 仅接收方向返回,停止继续接收,按现有规则排空发送后关闭客户端 |
| `ERROR` | 接收/发送错误收尾当前客户端;监听或等待错误返回服务级失败 |
Windows后端在内部把`WSAEWOULDBLOCK`转换为WOULD_BLOCK,把`WSAEINTR`以及accept的`WSAECONNRESET`/`WSAECONNABORTED`转换为RETRY。接入路径的`WSAENOBUFS`/`WSAEMFILE`转换为RETRY_LATER;原生错误码另外保留用于诊断。该分类不改变recv/send或监听初始化的错误处理,新的枚举值追加在EOF之后,已有值不变。Net_Accept(listener, &client, &peer, &native_error)成功时同时交付非阻塞且启用TCP_NODELAY的客户端和IEC104_NetPeer_t。Windows后端由accept取得sockaddr_in,先显式设置非阻塞及`TCP_NODELAY=1`,再用InetNtopA及ntohs转换IPv4和源端口;任一步骤失败先保存错误码并回收新句柄,再返回RETRY,若错误属于上述资源暂缺则返回RETRY_LATER。失败时client保持INVALID_SOCKET,peer保持原值。后端本身不循环重试或等待;监听句柄仍由调用方管理。Listen失败由后端回收其申请的句柄;TcpServer负责配对调用Net_Init/Net_Cleanup。
TCP_NODELAY设置在每个新接入的客户端socket上,关闭Nagle以减少S/U帧和短I帧的合并等待;重连时同样执行。该选项不改变104确认规则、发送顺序或非阻塞收发语义,send成功仍只表示字节交给本地TCP,不保证对端已经收到。
Net_Wait只接受通用事件位,系统的`fd_set`、`timeval`和等待参数差异留在平台文件中;只有Poll使用这个等待入口。应用自己的事件循环通过TcpServer_Prepare借出socket并传回就绪批次,Process仍经Net完成真实收发。所有I/O接口同步返回、无未完成I/O借用;后端须满足此契约,不能在返回后继续借用同一发送缓冲,也不能让业务绕过TcpServer驱动内部Connection。
2026-10-20增加内部接口`Net_Pending_Input(socket, &available, &error)`:只读取得当前内核接收队列字节数,不等待、不消费数据,失败返回ERROR。Windows使用`ioctlsocket(FIONREAD)`,未来其他平台或自定义Net后端必须补齐此接口。每份预算续收快照最多查询一次;快照期间不重新查询来扩大范围。该接口不属于业务公开API,零字节也不等于EOF,EOF仍由recv确认。真实Windows回环验证见`iec104_net_pending_input_test.c`。
Net_Wait允许interest为0,用于没有socket关注时的纯定时等待。Windows后端不把空集合传给select:wait_ms为0直接返回,正数调用一次有限Sleep,UINT32_MAX截为UINT32_MAX-1,避免被解释成INFINITE。Poll在进入该路径之前仍先合并max_wait_ms、协议/接入/发送重试期限、本地工作及未读事件;有事件背压时等待为0,立即交还业务。空集合等待返回后也照常计算timer_expired;Sleep可能提前结束,Process的实际接入及发送门控仍以当前单调时钟为准。Prepare/Process不执行睡眠,其他平台后端也须支持空集合的有限等待。
Slave是业务的统一子站入口,TcpServer是其内部的TCP服务。以下操作由门面代理,也可用于独立TCP接入。应用通过Create/Destroy管理句柄,使用Prepare/Process或Poll推进,并调用NextEvent/Enqueue_Event/Send_Response/Get_Remote_Address/Get_Status;Close_Connection只断开指定主站,Init/Close管理服务生命周期。Connection、SendManager及发送队列由TCP内部创建、驱动和回收。业务包含`IEC104_TcpServer.h`,其依赖的`IEC104_TcpTypes.h`和`IEC104_Types.h`提供公共值类型;Connection和SendManager头文件中的完整结构体定义用于内部静态存储,不属于业务接口。连接状态、发送记录和返回值在TCP实现中显式转换,业务不依赖内部结果或记录布局。
应用已有事件循环时通过Prepare/Process/NextEvent接入,socket注册的平台差异由应用的事件循环适配处理,库内真实I/O的平台差异由Net后端处理。Connection不反向调用Net,业务不直接驱动Connection或SendManager。ASDU内容可继续由业务使用Codec和公共时间工具解析、构造,这些无连接状态的工具不改变TCP接入边界。
新增后端时,实现`IEC104_Net.h`中的全部接口并替换链接文件;同时验证句柄无效值、非阻塞状态、部分写入、EOF、单调计时及失败资源回收。无需修改TcpServer或104协议模块。业务事件接口与平台选择无关。
## 7. Windows接口参考
- [Winsock服务端基本流程](https://learn.microsoft.com/en-us/windows/win32/winsock/complete-server-code)
- [非阻塞socket设置](https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-ioctlsocket)
- [select就绪等待](https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-select)
- [accept返回值与错误码](https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-accept)
- [Sleep与无限等待哨兵](https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleep)
- [send的部分写入与返回值](https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-send)
- [recv的EOF及暂不可读](https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-recv)
- [单调毫秒计时](https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-gettickcount64)
@@ -0,0 +1,728 @@
# IEC104 子站发送模块设计
同步日期:2026-10-20。本文对应 AsduQueue、SendBuffer、SendManager 当前实现,接口声明分别见 [AsduQueue.h](../../104/IEC104_AsduQueue.h)、[SendBuffer.h](../../104/IEC104_SendBuffer.h)、[SendManager.h](../../104/IEC104_SendManager.h)。
TCP接入边界:业务统一使用`IEC104_Slave.h`,Slave内部调用TcpServer;事件经`TcpServer_Enqueue_Event → AsduQueue_Enqueue`进入服务级NORMAL队列,应答经`TcpServer_Send_Response → Connection_Submit_Response → SendManager_Submit_Response`校验会话及启停准入后直接准备或缓存HIGH;协议期限已到时改走Manager.Enqueue(HIGH),使用同一准入检查。本文说明TCP内部发送模块的存储和调用契约,下文的模块调用方指Connection等内部实现。业务不直接包含或操作Manager、Buffer、Queue;应用主循环和事件用法见[TCP服务设计](IEC104_TcpServer_设计架构.md#4-业务接入)。
发送模块采用变长业务字节环、发送前编号、单当前I报文和独立确认记录。满时策略采用HIGH拒绝、NORMAL覆盖最旧原文,后者包含已提交未ACK条目且不提供逐条丢弃通知。保留静态内存、部分写入、本地发送期限、逐帧业务回执、代次隔离、公平配额及严格启停/TESTFR语义;不加入动态分配、线程或socket封装。
条目头按高低优先级职责拆分:HIGH仅有2字节长度,NORMAL保存编号、状态及长度,采用 `sizeof(IEC104_AsduNormalEntryHeader_t)` 作为头长,当前Windows 64位GCC下为16字节。两级队列及发送记录均移除user_tag;只有NORMAL分配条目编号,HIGH只消费当前队首。现有分段字节环、独立帧身份和帧回执继续使用,接口迁移见第6.2节。
- [整体数据生命周期](#overview)
- [SendBuffer:当前I帧与控制队列](#sendbuffer)
- [AsduQueue:变长业务队列](#asduqueue)
- [SendManager:准备、完成、ACK及回执](#sendmanager)
- [内部调度和恢复](#integration)
- [构建与接口迁移](#build)
<a id="overview"></a>
## 1. 整体数据生命周期
业务事件通过Enqueue_Event缓存NORMAL,入队时没有N(S),离线也可缓存。应答通过Send_Response提交:Connection核对会话及期限,Manager仅在STARTED+IDLE时接手,其他启停阶段返回ERROR且不入HIGH。准入后,在顺序、发送门控、窗口、记录和公平配额允许时直接准备I帧并分配N(S),暂不能准备时只复制到HIGH。TcpServer对直接应答最多尝试一次非阻塞send。
后续由Connection调用Manager.Prepare_Next_Send:续传已有当前帧;否则先取S/U控制队列;两者均空时,才在HIGH/NORMAL待发业务之间选择。队列发送和直接应答共用Prepare_I_Asdu完成预留、Link构帧和记录交接;实际socket写入统一由TcpServer经Net执行。
~~~mermaid
flowchart LR
B[业务] -->|Enqueue_Event / Send_Response / Poll / NextEvent| T[TcpServer]
T -->|内部连接调度| C[Connection]
T -->|实际网络收发| N[Net后端]
C -->|入队 / 准备 / 进度 / ACK| M[SendManager]
M -->|入队 / 选取 / 释放原文| Q[AsduQueue 两级字节环]
T -->|NORMAL服务缓存 / 绑定生命周期| Q
M -->|准备一帧并分配序号| L[Link]
M -->|报文存储与写入进度| S[SendBuffer 当前I帧 / 控制FIFO]
M -->|内部记录查询结果| C
C -->|内部帧和记录查询结果| T
T -->|NextEvent返回事件副本| B
~~~
| 阶段 | AsduQueue | SendBuffer | Manager记录 |
|---|---|---|---|
| 事件入队或应答暂存 | 保留未编号ASDU | 无变化 | 无记录 |
| 准备当前I帧 | HIGH原文出队;NORMAL标记已提交并保留原文 | 保存唯一当前I APDU | 建立帧身份、快照及NORMAL来源编号 |
| 直接准备应答 | 不入ASDU队列,不调用Mark_Submitted | 保存唯一当前I APDU | 建立HIGH记录,来源编号为0,纳入high_unacked_count |
| 部分写入/EAGAIN | HIGH没有队列原文,NORMAL可被后续入队覆盖 | 同帧原字节、原身份,续写剩余部分 | 未本地完成 |
| 整帧写完并提交 | HIGH没有队列原文,NORMAL可被后续入队覆盖 | I槽立即可复用 | 记录本地完成及实际写完时刻 |
| NORMAL空间不足时入队 | 覆盖最旧原文,记录已提交条目的待确认数量 | 已准备APDU保持原字节和进度 | 已编号记录保留,未编号条目无帧回执 |
| 对端有效累计ACK | HIGH不访问队列;NORMAL释放原文或核减覆盖待确认数 | 不访问旧I APDU | 标记已确认,等待TcpServer转为事件 |
| TcpServer复制RECORD事件并经Connection移除记录 | 无变化 | 无变化 | 释放记录槽 |
| 业务通过NextEvent读取事件副本 | 无变化 | 无变化 | 无变化;对应内部记录已移除 |
| 断线后Manager.Abort | 保留环内剩余原文;已覆盖原文不能恢复 | 清除发送状态 | 终止前须读取待回收记录 |
链路ACK表示对端协议接收确认,不表示遥控等应用操作已经执行成功。本地写完也不等于已ACK。
这些对象、字节数组、控制槽和记录数组均包含在TcpServer的私有存储中,Connection负责绑定和驱动发送模块;存储生命周期覆盖整个使用期。本模块不malloc、不释放借用内存、不内置锁。同一连接的入队、发送准备、写入结果、接收、查询和回收必须串行。Connection把事件交给Manager后,不得再调用底层Link、Buffer或Queue重复提交。业务只持有TcpServer句柄和公开值类型。
### 1.1 身份与时间
| 字段 | 用途 |
|---|---|
| connection_generation | 连接使用非零且不复用的代次;AsduQueue的0表示离线服务缓存,仅接受NORMAL,不能提交发送 |
| entry_serial | 仅NORMAL成功入队分配,1..UINT64_MAX循环,Rebind不重置;HIGH恒为0,无条目身份 |
| frame_serial | SendBuffer成功预留分配,取消后也不复用 |
| N(S)/N(R) | Link管理的15位协议序号,按模32768运算 |
| queued_at_ms | I帧准备或控制帧入队时刻,本地发送期限起点 |
| written_at_ms | 整帧实际写完时刻,I帧t1和主动TESTFR等待CON的起点 |
NORMAL的64位entry_serial在UINT64_MAX之后回到1,0对NORMAL是无效值;回绕不改变connection_generation。HIGH不分配条目编号,查询和发送记录的来源编号均为0,不能用0识别某条HIGH业务。活跃NORMAL编号按循环距离比较,不能按整数大小排序。SendBuffer的frame_serial仍采用耗尽时报错、不回绕的策略,两种编号独立。跨连接代次的全局不复用由调用方保障。is_initialized只表示对象初始化成功,不表示TCP存活。单调时刻允许0;部分写入、重复准备、重复ACK及WAIT均不续期。
<a id="sendbuffer"></a>
## 2. SendBuffer:当前I帧与控制队列
源码:[IEC104_SendBuffer.c](../../104/IEC104_SendBuffer.c)。
### 2.1 存储结构
SendBuffer结构内置一份255字节i_apdu和一份I帧快照;只允许一帧已准备但尚未提交本地完成的I报文。调用方另提供固定的IEC104_SendBufferControlFrame_t数组,容量1..65535,每槽仅保存6字节S/U APDU及元数据。
没有APDU节点池、空闲链、保留链、control_reserve_count,也不复制Link的三个序号边界。控制队列容量与k、ASDU字节容量、回执容量分别配置。S/U不占I帧k窗口或业务回执槽,控制队列自身仍可能满。
| 结构/字段组 | 含义 |
|---|---|
| FrameState | frame_serial、入队/写完时刻、长度、已写字节、encoded_nr、帧类型 |
| ControlFrame | FrameState + 6字节APDU |
| control_frames/capacity/head_index/count | 独立S/U环形FIFO;队尾由头和数量推导 |
| i_frame_state/i_frame_snapshot/i_apdu | 当前I帧元数据、不可变序号快照和字节 |
| has_i_frame | 当前I帧已提交,即使已写0字节也占用 |
| has_reservation/reservation_is_i_frame | 唯一预留是否存在及其类型 |
| generation/next_frame_serial | 本地身份及不复用编号 |
| Status | 代次、控制数量、当前I帧是否存在及其N(S)、预留标志、是否空闲 |
| HeadInfo | 当前身份、类型、快照、剩余字节借用指针、进度及时间 |
HeadInfo中的pending_data只借用当前存储,完整提交、Abort或Init后不能继续使用。发送异步进行时,同帧最多允许一个尚未回报的写操作。
<a id="sb-functions"></a>
### 2.2 初始化与验证
Init(buffer, control_frames, control_capacity, generation)只初始化控制结构,绑定调用方数组。Get_Status验证边界及要访问的端点并返回数值副本,不扫描整个控制队列。
Buffer_State_Valid检查索引、容量、0/1标志、身份及当前/控制端点元数据;Frame_State_Valid检查长度、快照、进度和完整写完时刻。Frame_Type_Matches结合Codec.Parse_APCI验证提交类型,子站仅接受I、S_ACK、STARTDT_CON、STOPDT_CON、TESTFR_ACT、TESTFR_CON。不能提交主站STARTDT_ACT或STOPDT_ACT。
<a id="sb-reserve"></a>
### 2.3 预留和提交
| 操作 | 成功及阻塞含义 |
|---|---|
| Reserve_I_Frame | 只有当前I帧和控制队列均空时成功;否则BUSY |
| Reserve_S_U_Frame | 预留控制队尾;控制队列满为FULL |
| 两种Reserve | 任一已有预留时BUSY;成功分配不复用frame_id |
| Commit_Frame | 校验身份、APCI、长度和I快照;I成为当前帧,S/U追加FIFO |
| Cancel_Reservation | 只归还尚未构帧成功的预留,已使用身份不复用 |
| Peek_Send_Head | 有当前I帧就返回它,否则返回控制队首;不选择业务 |
先预留存储,再调用会产生发送义务的Link构帧接口。Link.Prepare成功后N(S)或控制过程已提交,不能取消预留并继续旧连接;此后的提交异常必须结束连接。
控制帧优先于**尚未准备的业务**。已经准备的当前I帧,即使第一次写socket得到EAGAIN且尚未写出字节,也必须保持顺序先完成。此规则避免撤回已编号帧及已经冻结的N(R)快照。部分写出的任意APDU都不能被另一个APDU插入字节。
<a id="sb-completion"></a>
### 2.4 写入进度与释放
Record_Written_Bytes仅接受当前身份和实际写入增量。增量0不推进;未写完返回OK;恰好写完返回FRAME_WRITTEN并记录时刻;超出剩余长度、旧身份、重复完整提交返回ERROR。
Manager在整帧写完时连续执行:
1. 按保存的类型调用Link.Complete,I帧传回原始快照。
2. 登记I帧本地完成和实际写完时刻。
3. 调用Buffer.Commit_Send_Completion,释放当前I槽或控制队首。
只有第三步结束才可以复用报文字节。中间不得分派ACK事件,不得重入发送操作。该顺序由Manager内部维护,Connection按序提交发送结果和ACK;业务不调用SendBuffer完成接口。
<a id="sb-ack"></a>
### 2.5 ACK与超时的归属
SendBuffer不处理ACK,没有Apply_Peer_Ack。协议确认边界由Link独占;Manager记录ACK,核减HIGH未确认计数,并清理NORMAL仍保留的原文或已覆盖待确认计数。Buffer不再需要保留已发送APDU等ACK。
Head_Send_Expired从当前帧原queued_at_ms判断本地发送期限,返回1到期、0无帧或未到期、-1非法。控制帧等待前一帧期间也计入其原期限;进入队首后不会重新计时。I帧ACK的t1查询位于Manager。
Abort清除Buffer控制结构,不关闭TCP、不取消异步I/O;调用方先结束旧I/O再复用存储。
<a id="asduqueue"></a>
## 3. AsduQueue:变长业务队列
源码:[IEC104_AsduQueue.c](../../104/IEC104_AsduQueue.c)。
### 3.1 两级可跨尾部的字节环
HIGH和NORMAL各使用调用方提供的uint8_t数组,容量单位为**字节**。两级分别使用 `IEC104_ASDU_HIGH_ENTRY_HEADER_LEN` 和 `IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN` 计算容量:
~~~text
HIGH:ASDU长度 uint16(2) | ASDU(6..249)
NORMAL:entry_serial uint64 | entry_state:2位 | asdu_length:8位 | ABI填充 | ASDU(6..249)
~~~
HIGH头固定2字节;NORMAL头是 `IEC104_AsduNormalEntryHeader_t` 的当前ABI布局,Windows 64位GCC下为16字节,其他平台以sizeof为准。NORMAL的entry_state为1表示未提交、2表示已提交待ACK,0/3无效;头内状态必须与已提交前缀/未提交后缀一致。两个头都通过memcpy读写,不要求环内地址对齐,不能将字节环强转成结构体指针。头不是协议字段或跨版本持久化格式;ASDU不含APCI、不含N(S)。Enqueue验证长度和容器条件,具体业务对象合法性由生产者负责。两级均不保存user_tag。
HIGH最小容量为2+6=8字节,NORMAL最小容量为NORMAL头长+6(当前ABI下22字节),只保证容纳最短ASDU。Enqueue先检查 `本级头长 + asdu_length <= 目标桶总容量`;不满足时返回ERROR,空队列也不能接纳该条目。100字节环最多容纳98字节HIGH ASDU,或当前ABI下84字节NORMAL ASDU;最大249字节ASDU分别需要251字节及NORMAL头长+249(当前ABI下265字节)。这种错误须调整容量或业务组帧,等待ACK释放空间无法解决。
条目能放入目标桶总容量后,才检查当前总空闲空间。条目头和ASDU都可以跨数组尾部:先使用尾部剩余字节,再从下标0继续,不插入额外回绕填充、不搬动在途原文。`used_bytes` 恰好等于所有保留条目的 `本级头长 + 实际长度` 之和,NORMAL结构体本身的ABI填充已计入头长。参数、状态合法时,只要总空闲不少于本条大小就有足够字节容量;HIGH总空闲不足时返回FULL,NORMAL总空闲不足时按队首顺序覆盖旧条目直到足够。NORMAL还检查编号窗口,HIGH不受该窗口限制。队列清空时索引归零。
例如当前ABI下100字节NORMAL环中,`head=40、tail=80、used=40`,尾部有20字节、开头有40字节可用。新的50字节条目(16字节头+34字节ASDU)直接使用尾部20字节及开头30字节,成功后 `tail=30、used=90`。其中ASDU原文分为尾部4字节和开头30字节,队列通过两段只读视图交给调用方。
NORMAL内部保持已登记交接、保留原文待ACK的前缀;HIGH只保存队列待发条目:
~~~text
NORMAL:队首 → 保留原文待ACK的前缀 → unsubmitted_index指向的待发后缀 → 队尾
HIGH:队首(也是unsubmitted_index)→ 全部待发条目 → 队尾
~~~
unsubmitted_index直接指向队列中下一条尚未登记发送交接的条目,发送选择不扫描保留待ACK前缀。NORMAL在入队时已经分配entry_serial,HIGH没有条目编号;刚入队时两级都没有I帧N(S),编号由后续发送流程分配,不能仅凭队列待发状态判断是否已编号。unsubmitted_count为0时,unsubmitted_index等于tail_index。HIGH在对应I帧已交给SendBuffer且Manager已登记记录后,通过Mark_Submitted消费队列原文;NORMAL的Mark_Submitted将头内状态设为保留原文待ACK,并推进待发游标。NORMAL空间不足时删除最旧条目,既可删除待发条目,也可删除保留待ACK原文,一次入队可能删除多条。新条目成功入队仍返回OK,不增加丢弃通知或回调。
原文删除不会撤销已编号的I帧:当前APDU在SendBuffer中有独立副本,Manager记录继续保留,k窗口、本地发送期限、t1和逐帧回执仍按真实发送及ACK推进。已覆盖但未ACK的已提交数量单独记录,不再占字节环容量。未编号条目被覆盖时尚未生成I帧,因此没有对应帧回执。
Enqueue先检查新条目本身能否放入总容量,再在控制副本中检查需要删除的全部旧条目;NORMAL另检查下一编号窗口,最后才写新字节并发布状态。条目过大或覆盖途中发现非法头时返回ERROR,原队列、字节和编号不变。NORMAL的UINT64_MAX仍可分配,下一编号回到1。正常覆盖复杂度为O(本次删除条目数);当前ABI下NORMAL条目为22..265字节,一次最多删除13条。其他ABI的上界为向上取整 `(NORMAL头长+249)/(NORMAL头长+6)`。
<a id="aq-fields"></a>
### 3.2 控制字段
| 字段 | 含义 |
|---|---|
| bytes/byte_capacity | 借用的字节数组及容量 |
| head_index/tail_index | 最旧保留条目和下一写入位置 |
| used_bytes | 所有保留条目的本级头长+ASDU实际长度之和,无额外回绕填充 |
| entry_count | 原文仍保留的ASDU总条目数;HIGH只保留待发区,NORMAL包含保留待ACK前缀和待发后缀 |
| unsubmitted_index/unsubmitted_count | 队列中下一条尚未登记发送交接的ASDU位置及条目数,不是SendBuffer中的帧数 |
| next_entry_serial | 下一个NORMAL成功入队身份;UINT64_MAX之后回到1;HIGH不消耗编号 |
| connection_generation | 队列当前绑定;0表示离线服务缓存,非零值绑定当前连接 |
| normal_send_limited | EOF收尾开关;为1时停止选取全部未提交NORMAL,原文留给下一连接,Detach/Rebind清零并解除限制 |
| overwritten_unacked_serial_begin | NORMAL原文已覆盖、仍待ACK的循环区间起始边界,含端点;ACK后推进,计数为0时为0 |
| overwritten_unacked_serial_end | 同一循环区间的结束边界,含端点;对应最后一个已覆盖待确认身份,计数为0时为0 |
| overwritten_unacked_count | NORMAL原文已覆盖、已提交且待确认的ASDU条目数,Queue和Status均提供 |
| Status.entry_count/retained_unacked_count | 各优先级原文仍保留的ASDU总条目数/其中已提交且待确认的ASDU条目数 |
| Status.is_storage_empty | 两级原文环均为空;HIGH或NORMAL仍可能有对应I帧等待ACK |
| HeadInfo.entry_id/priority/is_submitted | 条目身份、优先级及队列记账状态;is_submitted为0表示尚未登记交接,为1表示NORMAL保留原文待ACK;HIGH的entry_serial及is_submitted恒为0 |
| HeadInfo.asdu_first/asdu_first_length | 原文第一段及长度,指针非NULL |
| HeadInfo.asdu_second/asdu_length | 第二段或NULL,以及两段总长度;第二段长度=总长度-第一段长度 |
这些计数的单位都是ASDU条目。每一级的 `retained_unacked_count = entry_count - unsubmitted_count` 只统计原文仍保留、已提交且待确认的ASDU条目。HIGH提交后已经出队,其entry_count等于unsubmitted_count,retained_unacked_count恒为0。队列状态只能给出NORMAL保留及覆盖待确认数;全部已编号未ACK的I帧数还须加上Manager.high_unacked_count。
“未提交/已提交”在AsduQueue中描述队列记账状态。`Mark_Submitted` 的含义是登记已经完成的发送交接,调用顺序为:
```text
AsduQueue保存待发ASDU
→ SendBuffer接手组装好的完整I帧
→ Manager建立发送记录
→ Mark_Submitted完成队列记账
HIGH:消费队列原文,SendBuffer报文及Manager记录继续有效
NORMAL:登记为保留原文待ACK,将待发游标移到下一条
```
在最后一步执行前,队列仍保留待发旧状态,即使SendBuffer已经接手报文。因此HIGH在Mark_Submitted中核对待发旧状态后出队,出队依据是前面的交接已成功;不能把状态值0理解为“尚未交给发送模块就丢弃”。主动取消待发业务使用Discard_Unsubmitted_Head,且必须发生在发送交接之前。上述交接操作必须串行完成,中途不得插入队列修改。
公开视图 `is_submitted` 的0/1表示尚未登记交接/NORMAL保留待ACK;NORMAL条目头 `entry_state` 的1/2是对应的内部编码,两者不是同一组数值。HIGH条目头没有状态字段,完成交接后直接出队,不会留下is_submitted为1的条目。
待确认表示对应I帧尚未收到有效ACK,包含尚未写出、部分写出及整帧写完后等待ACK的阶段。当前实现中,一个已提交ASDU条目对应一个I帧,因此Manager的HIGH未确认数加NORMAL保留及覆盖待确认数,在数值上等于已编号未ACK的I帧数。AsduQueue保存条目头和ASDU原文;完整I帧的存储归SendBuffer管理,整帧写完并提交本地完成后即可复用,Manager的发送记录继续等待ACK。
`is_storage_empty` 只表示两级ASDU原文环为空,已经出队的HIGH或原文已覆盖的NORMAL仍可能有对应I帧待确认。这里的retained仅描述原文仍在缓冲区。只有NORMAL有条目编号;同一级内部提交及ACK处理始终FIFO。
overwritten_unacked统一表示“原文已覆盖、仍待有效ACK”。begin/end是包含两端的循环区间边界,实际数量以count为准。例如NORMAL已覆盖待确认编号为10、12,而11此前在未提交时被主动丢弃,则begin=10、end=12、count=2;确认10后begin推进到11、count减为1,实际下一待确认身份仍是12。区间起始边界不一定是某条现存记录的编号,也不能用普通整数大小比较跨UINT64_MAX回绕的边界。
<a id="aq-functions"></a>
### 3.3 公开操作
| 接口 | 行为 |
|---|---|
| Init | 服务启动时绑定两个字节数组;代次0可离线缓存NORMAL,非零值绑定连接,不分配内存 |
| Enqueue | 固定HIGH用于当前连接应答、NORMAL用于可跨连接恢复的事件;超过桶总容量为ERROR;HIGH当前空间不足为FULL,NORMAL覆盖最旧原文;NORMAL编号窗口受阻也返回FULL;OK复制ASDU,仅NORMAL分配身份;不接收user_tag,失败不改队列、字节或编号 |
| Get_Status | 常数时间查询代次、各优先级ASDU原文保留条目数、其中已提交待确认条目数、NORMAL原文已覆盖待确认条目数及原文是否为空 |
| Peek_Head | 查看最旧保留条目,可能已提交 |
| Peek_Unsubmitted | 常数时间查看当前允许交接的待发条目;EOF限制期间NORMAL返回EMPTY,即使仍有EOF前后入队的原文 |
| Copy_Asdu | 把仍有效的最多两段原文复制到调用方连续缓冲;容量不足或描述非法时ERROR且不改输出 |
| Mark_Submitted | 仅非零绑定代次可调用;SendBuffer接手I帧且Manager建立记录后核对状态及身份,HIGH消费原文,NORMAL保留待ACK;EOF后NORMAL禁止新交接,不影响此前已提交的帧;不执行I/O |
| Confirm_Next_Normal_Entry | 仅接受NORMAL:有原文时核对已提交队首并释放,已覆盖时核减待确认数;HIGH返回ERROR,其ACK由Manager记录处理 |
| Discard_Unsubmitted_Head | 仅允许主动移除未提交队首,拒绝撤回已编号业务 |
| Get_Entry_By_Offset | O(offset)查看保留条目,供故障检查及业务恢复 |
| Get_Free_Bytes | 返回未占用字节数;空闲不少于本级头长+ASDU长度即有足够容量;NORMAL另检查编号窗口 |
| Reset_High | 同代次清空HIGH待发条目;保留NORMAL及next_entry_serial,不影响已提交帧和Manager记录 |
| Limit_Normal_Send | EOF时停止选取全部未提交NORMAL,原文留给下一连接;不改已提交帧或HIGH,重复调用不解除限制 |
| Detach | 旧I/O及Manager结束后核对旧代次,清空HIGH并恢复现存NORMAL待发状态,切换到代次0;清除旧覆盖记账及发送限制 |
| Rebind | 完整检查两级,绑定新的非零代次,可从离线代次0接入;恢复现存NORMAL、清空HIGH及旧连接记账;ERROR不修改控制字段或字节 |
| Abort | 明确丢弃全部引用,调用方先完成业务回收 |
Peek操作成功才写输出;原文按 `asdu_first`、`asdu_second` 的顺序读取,后者为NULL表示全部位于第一段。本条目被覆盖、确认释放或Remove后两段借用失效;HIGH的Mark_Submitted释放当前队首,Reset_High使所有HIGH借用失效;Detach、Rebind、Abort或Init使所有借用失效。NORMAL的后续Enqueue可能覆盖旧原文,调用方不能跨可能覆盖它的操作继续使用借用。NORMAL的Remove/Mark及仍有原文的Confirm核对完整generation和entry_serial,不能用可回绕的N(S)代替身份。HIGH只核对generation及entry_serial==0,从Peek到Mark/Discard之间不得移除、清空或替换队首;不提供同代次的逐条身份保护,0编号不得长期保存为业务身份。
原文已覆盖时不另存一份完整身份列表;准确条目身份由Manager记录保存,Confirm_Next_Normal_Entry核对当前代次、计数及从overwritten_unacked_serial_begin到overwritten_unacked_serial_end的循环区间,要求按本优先级真实发送顺序调用。ACK后将区间下界推进到该编号的后继,拒绝重复/逆序确认;最后一个覆盖身份必须匹配区间终点,再允许释放仍保留的物理队首。覆盖计数归零时清除两个边界,避免无在途义务的历史确认长期占住编号窗口。普通业务不得构造身份调用Confirm_Next_Normal_Entry伪造ACK。
NORMAL编号环包含1..UINT64_MAX,前向距离为`to >= from ? to - from : UINT64_MAX - from + to`,因此UINT64_MAX到1的距离是1,而不是普通uint64溢出减法所得的2。非零距离不超过UINT64_MAX/2时才认为旧编号在新编号之前。NORMAL入队提交前检查其保留队首及覆盖待ACK区间下界到新next的距离,始终限制在半环内;只读至多一个队首,保持常数检查开销。若长期未处理的旧记录阻止窗口前进,NORMAL返回FULL,原队列及字节完全不变;处理旧条目或ACK后可重试。HIGH不分配编号,不受此窗口阻塞。NORMAL已经在控制副本中淘汰的未提交条目不再占用窗口。
该约束以仍保留的NORMAL条目和覆盖待ACK身份为准。HIGH的asdu_entry_serial恒为0,帧完成及ACK依据独立frame_id和N(S)匹配。释放后的NORMAL entry_id已经失效,调用方不得无限期保存并跨完整编号周期重新提交。回绕不会触发Rebind、清空业务或增加连接代次,也不增加内部条目头字段。
需要连续原文的业务恢复代码可使用以下接口。Copy_Asdu只检查描述和输出容量,不重新查询队列或验证身份;必须在借用有效期内串行调用,输出不得与原文或描述重叠。
~~~c
IEC104_AsduHeadInfo_t info;
uint8_t raw[IEC104_MAX_ASDU_LEN];
if (IEC104_AsduQueue_Peek_Head(&queue, IEC104_ASDU_PRIORITY_NORMAL,
&info) == IEC104_ASDU_QUEUE_OK &&
IEC104_AsduQueue_Copy_Asdu(&info, raw, sizeof raw) == IEC104_ASDU_QUEUE_OK) {
/* raw[0..info.asdu_length-1]是供内部处理使用的连续副本。 */
}
~~~
Bucket_Valid检查容器边界、数量和占用区与队尾/待发游标的关系,不遍历条目。Ring_Advance处理下标回绕并避免加法溢出,Ring_Read/Ring_Write最多分两次复制。Read_Entry_With_Expected_State先验证条目头位于已占用区,再读取可能跨尾部的长度和身份,核对预期队列状态,检查整条占用后返回原文视图。Mark/Remove会先验证需要访问的后继,再提交控制字段,避免错误返回时部分移动游标。
内部读取及删除函数的 `expected_queue_state` 参数只核对操作前的旧状态,使用 `QUEUE_ENTRY_PENDING`(尚未登记交接)或 `QUEUE_ENTRY_RETAINED_UNACKED`(NORMAL保留原文待ACK)表达。HIGH完成交接时调用 `Remove_Head_With_Expected_State(..., QUEUE_ENTRY_PENDING)`;该参数不是删除原因或要写入的新状态。NORMAL通过 `Mark_Normal_Entry_Retained_Unacked` 明确更新条目头,将内部编码写为2,不再用含义不清的0/1参数控制状态更新。
内部队列返回FULL时,NORMAL由TcpServer直接转换为WAIT,HIGH经Manager和Connection转换后返回给业务;业务保留原文和生产游标。HIGH需要等待字节空间,NORMAL可能需要等待占住编号窗口的旧条目/ACK处理完成。NORMAL成功可能伴随旧原文覆盖。ERROR须修正输入、容量配置或状态,不能进入等待空间的重试分支;库不会自动丢弃这条业务或推进游标。只有TcpServer提交接口返回OK才推进业务游标。静态数组的“释放”表示空间可复用,不是归还操作系统内存。
<a id="aq-reconnect-policy"></a>
### 3.4 固定的队列生命周期
优先级同时决定发送顺序和存储生命周期;不再提供逐条重连策略,也不根据TypeID或COT自动推断。同为遥测ASDU,自发事件与总召响应应按业务来源放入不同队列。
| 业务 | 队列 | 生命周期 |
|---|---|---|
| 总召激活确认、总召数据及结束确认 | HIGH | 属于发起总召的连接,提交后出队;连接初始化、启动应答及重连清空剩余待发业务 |
| 命令确认、读响应、时钟同步确认等请求应答 | HIGH | 同上,不向新连接重放旧请求结果 |
| 允许跨连接恢复的自发事件 | NORMAL | 随服务初始化,无连接也可入队;有效ACK释放原文,满时覆盖最旧原文;重连绑定现存原文,Close服务才清空 |
内部队列统一使用AsduQueue_Enqueue;NORMAL由TcpServer直接调用,需要缓存的HIGH经Connection/SendManager调用,直接应答不入队。业务分别使用Enqueue_Event和Send_Response,不接触队列指针。下面仅展示内部NORMAL入队接口,generation须匹配队列绑定,无连接时为0:
~~~c
/* 可跨连接恢复的事件使用NORMAL。
* 返回值仍须检查;仅OK推进生产者游标,FULL等待,ERROR明确处理。
*/
IEC104_AsduQueueResult_t result = IEC104_AsduQueue_Enqueue(
&queue, generation, IEC104_ASDU_PRIORITY_NORMAL,
event_asdu, event_length);
~~~
原先显式保留的HIGH事件应迁移到NORMAL;仅属于当前连接的NORMAL请求应答应迁移到HIGH。没有同一优先级内混合保留/丢弃策略的接口。NORMAL的保留不保证数据不丢失:空间不足时仍可能被覆盖;未ACK也不证明对端未收到。恢复只适用于业务认可的同一接收方或冗余域,本模块不识别远端主站身份,也不提供应用去重。
服务Init先以代次0初始化队列;离线状态下HIGH为空,NORMAL全部待发,覆盖待ACK记账及发送限制均为空。接入时Rebind到非零代次,再初始化Connection/Manager。EOF时Limit_Normal_Send将normal_send_limited置1,已有及后来新增的未提交NORMAL均不再交给旧连接。Peek_Unsubmitted对NORMAL返回EMPTY,原文及待发计数保持可查询;入队、覆盖、取消条目和重复调用均不解除限制,Detach/Rebind清零开关后全部现存NORMAL重新可选取。已提交的I帧由SendBuffer及Manager独立收尾:Manager先续写当前帧,再处理控制FIFO及已受理HIGH,不从NORMAL创建新帧,也不会因HIGH公平配额耗尽而选取被限制的NORMAL。EOF限制只用normal_send_limited表示,不维护剩余额度;它不改变接口签名及NORMAL容量覆盖策略。
高优先级队列用于连接应答,低优先级队列用于可跨连接保留的事件;高优先级原文出队后,由独立发送记录继续等待ACK。
Manager.Init清空HIGH;STOP受理立即作废待发HIGH,无逐条回执。从停用状态重新START仍清理HIGH及公平配额;已激活时重复START保留。WAIT尚未受理、不清理;排空中的重复STOP不重复清理。清理不撤回已提交I帧、不清除记录或ACK义务、不改变NORMAL;同一TCP启停不调用Rebind。
<a id="sendmanager"></a>
## 4. SendManager:发送准备、ACK及回执
源码:[IEC104_SendManager.c](../../104/IEC104_SendManager.c)。只绑定CONTROLLED子站;主站启停算法仍由独立Link角色模块提供,不在这里发送ACT。
### 4.1 记录与返回值
每个I帧占用一条 `IEC104_IFrameSendRecordStorage_t`:frame_serial、asdu_entry_serial、asdu_priority、原N(S)/N(R)/长度、准备和写完时刻、本地完成及对端确认标志;不保存user_tag。所有记录共用Manager保存的connection_generation,每条不再重复保存代次。查询接口仍返回 `IEC104_IFrameSendRecord_t`,将共享代次与该条frame_serial组合成完整frame_id,其他字段逐项复制;返回值是独立数值副本,Manager重建不会改变已经取出的快照。
NORMAL原文身份使用Manager的连接代次和该条非零asdu_entry_serial;HIGH原文提交时已出队,其asdu_entry_serial恒为0,没有入队业务身份。两种记录均以完整frame_id和N(S)处理完成及ACK,不借用原文指针,也不保存完整APDU。发送进度、失败、ACK和回执消费仍校验外部事件的连接代次;旧连接事件不能因新连接复用frame_serial而被接受。确认回执只报告帧级事实,不再返回调用方的业务标识。
Manager持有三个模块的指针及内部提供的记录环形数组,维护head/count、high_unacked_count、next_record_ns、k和高优先配额。high_unacked_count统计已登记且未ACK的HIGH记录,包括尚未完整写出的帧;登记HIGH记录时增加,有效ACK时减少,移除已ACK回执不再减少。next_record_ns表示下一条新登记I帧记录的N(S),在发布记录时推进,用于校验记录的连续编号;发送完成位置和ACK位置由Link另外维护。记录容量1..32767,k也为1..32767;它们可独立配置,记录容量包含已经ACK但尚未由TcpServer转为事件并移除的回执。已经复制到事件队列的回执由事件容量单独计数。
| 返回值 | 含义 |
|---|---|
| OK | 操作成功;Prepare_Next_Send得到可写字节,Handle_Send_Progress尚未整帧完成 |
| QUEUED | 控制帧已排队,不代表已写socket |
| EMPTY | 无待选业务/无记录/无对应待推进过程 |
| WAIT | 门控、窗口、容量或待完成义务阻塞;接收入口表示当前请求尚未受理 |
| FRAME_WRITTEN | 已连续提交Link完成、记录和Buffer释放,输出完成副本 |
| IGNORED | 排空中的重复STOP,或提前/无匹配等待的TESTFR_CON;不重启或撤销原有计时 |
| ERROR | 参数、身份或接入状态非法,需要修正;不能按容量不足盲目重试 |
| PROTOCOL_ERROR | I/S报文因接收阶段、序号或ACK违反协议而被Link拒绝,当前连接必须结束 |
| FATAL | 当前连接必须结束,先保留事实用于业务回收 |
所有枚举精确比较,不能用“非零为成功”。
<a id="manager-consistency"></a>
### 4.2 常数时间一致性检查
Manager_Storage_Valid校验自身容量、指针、代次、配额及HIGH未确认数不大于记录数,不依赖故障后的Link,供回执读取和恢复使用。运行会话的Manager_Session_Valid另外要求Link保持CONTROLLED角色;绑定后的角色变化属于本地状态非法。
`IEC104_SendManager_Get_Status`按同样的存储边界检查返回代次、记录容量、当前记录数及HIGH未ACK数;不暴露绑定指针,不消费记录,OK才写输出。该查询可在连接冻结后使用,Abort之后返回ERROR。
`IEC104_SendManager_Enqueue`先验证当前会话及代次,再调用绑定队列的Enqueue;返回OK、WAIT(队列FULL)或ERROR。它沿用HIGH/NORMAL的保留、覆盖和编号规则,不生成I帧或分配N(S),用于应答暂不能直接准备或Connection因期限已到而延后处理的路径。TcpServer把直接准备或暂存成功统一转换为公开的IEC104_TCP_SUBMIT_OK。服务级NORMAL由TcpServer直接写入所持队列,不要求Manager存在;实际发送仍统一由Manager处理,业务不依赖内部指针。
Manager_Session_Valid调用各模块公开查询,检查:
- 三模块绑定代次一致,无外部遗留预留。
- manager.next_record_ns等于Link下一分配序号。
- distance(next_completion_ns, next_frame_ns)为0或1,等于Buffer当前I帧数量;有当前帧时N(S)等于下一完成位置。
- HIGH原文环中已提交待确认数恒为0。
- 已编号未ACK的I帧数不超过k、不大于记录数,且等于Manager.high_unacked_count加NORMAL原文保留及覆盖待确认条目数。
所有距离按模32768计算,不以普通整数大小比较。Manager通过Link接口修改协议状态,角色也通过Get_Status读取。运行检查在完整事务的公开入口执行,不能插入Prepare/Commit、Complete/释放、ACK/原文释放之间;参数、身份、序号及模块计数校验不依赖构建模式。
记录通过N(S)与记录环偏移常数时间定位,再核对不复用frame_id及快照。ACK只访问本次新确认区间,不扫描全部历史回执。
故障回收允许Remove_Confirmed_Record在FAILED阶段调用,通过既有队首记录查询校验frame_id、本地完成及ACK标志,不要求Link或队列恢复正常会话状态。完整事务仍由Manager按顺序提交,不能在中间步骤插入其他队列操作或ACK处理。
<a id="manager-i-transfer"></a>
### 4.3 Prepare_Next_Send
签名:
~~~c
IEC104_SendManagerResult_t IEC104_SendManager_Prepare_Next_Send(
IEC104_SendManager_t *manager, uint64_t generation, uint64_t now_ms,
IEC104_SendBufferHeadInfo_t *head_info);
~~~
本接口由Connection在准备实际发送时调用,业务生产者只调用TcpServer_Enqueue_Event或TcpServer_Send_Response。内部也不能提前反复调用它来预建一批I帧。
1. 校验会话;已有当前I帧或控制队首时,直接返回该帧剩余字节。
2. 查看两级下一未编号条目,均空返回EMPTY。
3. 选择优先级;原文连续时直接借用,跨尾部时用Copy_Asdu拼成连续副本。调用共用Prepare_I_Asdu,记录满返回WAIT,否则保障当前I帧存储。
4. Link.Prepare检查k和链路门控,生成APDU并分配N(S)。未生成可取消预留,返回WAIT/ERROR。
5. 让SendBuffer接手完整I帧,登记Manager业务记录并在HIGH时增加high_unacked_count;再调用Mark_Submitted完成队列记账,HIGH消费队列原文,NORMAL登记为保留原文待ACK;最后更新配额。
6. 返回当前HeadInfo,结果为OK。
Prepare_Next_Send使用一个249字节的局部数组存放跨尾部ASDU;这是调用期间的自动存储,嵌入式接入需计入栈预算。只有原文确实分两段时才执行额外复制,连续原文仍直接交给Link。HIGH原文提交时已出队;NORMAL原文可被后续入队覆盖。SendBuffer对连接层始终提供独立、连续的APDU,原文覆盖不影响当前帧续写,实际I/O接口不变。
high_priority_burst_limit=0为严格高优先;非0时,NORMAL有待发条目才累积连续HIGH次数,达到上限让出一次NORMAL。普通桶仅有已提交未ACK条目不算待发,不积累配额欠账。每个新I帧重新选择,重复准备/WAIT不改变配额。
Link.Prepare成功后序号不可撤回,后续异常返回FATAL;保留尚存原文及已登记事实供恢复,已经出队的HIGH不恢复。最多一帧已编号但未完成,不能认为连这一帧也完全没有提前分配:编号发生在实际写调用前,EAGAIN时该帧仍持有序号。
### 4.3.1 Submit_Response
```c
IEC104_SendManagerResult_t IEC104_SendManager_Submit_Response(
IEC104_SendManager_t *manager, uint64_t generation,
const uint8_t *asdu, uint16_t asdu_length, uint64_t now_ms,
IEC104_SendBufferHeadInfo_t *head_info);
```
Connection在未到期路径先Drive处理保留接收和当前控制义务,再调用此接口。Manager的Submit_Response和Enqueue(HIGH)共用Response_Submit_Allowed,在记录容量、k窗口和HIGH容量判断之前按实际接手时的状态准入:
| 启停状态 | 应答准入 |
|---|---|
| STARTED + IDLE | 允许;满足发送条件时直接准备,否则暂存HIGH |
| STOPPED + IDLE | ERROR,不入HIGH |
| STOPPED + STOP_DRAINING | ERROR,不入HIGH |
子站START/STOP受理立即更新dt_state;应答准入仍集中检查STARTED/IDLE。协议期限已到路径直接Enqueue(HIGH),也经过同一检查。新的I帧须等待发送缓冲为空;已有CON时应答暂存HIGH,由FIFO保证先写控制帧,再发业务。
直接准备不虚构ASDU队列条目,也不调用Mark_Submitted;记录仍为HIGH、asdu_entry_serial为0,并增加high_unacked_count。之后与队列应答使用同一Handle_Send_Progress、Handle_I_S_Frame和回执逻辑。只有实际准备成功才消耗N(S)和配额,单纯入HIGH不会分配协议序号。
OK表示已准备APDU,只有此时写head_info;QUEUED表示已复制HIGH;WAIT表示HIGH暂满、本次未接手。ERROR表示参数/状态或HIGH总容量不符,包含启停准入拒绝,Connection及TcpServer原样映射为ERROR且不因此断连;编号后交接失败为FATAL,必须收尾。QUEUED及对外SUBMIT_OK不保证待发HIGH最终发出,受理新STOP时按4.7节作废。Manager不调用socket;TcpServer直接发送时最多一次非阻塞写入,部分写入或暂不可写由Process/Poll继续同一帧,不能重复入HIGH。
<a id="manager-send"></a>
### 4.4 Handle_Send_Progress / Failure
TcpServer经Connection取得HeadInfo后,通过Net实际写入,再由Connection向Manager提交该次成功写入增量。Manager按frame_id匹配当前帧;I帧还核对关联记录。整帧完成时按原类型分派:
| 原类型 | Link完成接口 |
|---|---|
| I | Complete_I_Send(原快照) |
| S_ACK | Complete_S_Ack_Send |
| STARTDT_CON | Complete_Startdt_Con_Send |
| STOPDT_CON | Complete_Stopdt_Con_Send |
| TESTFR_ACT | Complete_Testfr_Act_Send |
| TESTFR_CON | 无独立Link完成接口;仍提交Buffer进度与释放 |
FRAME_WRITTEN输出IEC104_LocalSendCompletion_t,只有数值副本,无已经释放的报文指针。里面的Link结果保留OK/IGNORED,主动TESTFR成功时可据written_at_ms建立等待CON的t1。
最终写入失败或本地发送超时用Handle_Send_Failure,匹配身份后分派失败完成并返回FATAL。EAGAIN不是最终失败,不调用该接口。不能重复报告写入增量或重复调用底层Complete。
<a id="manager-ack"></a>
### 4.5 Handle_I_S_Frame及记录消费
apci必须来自完整APDU的成功解析,只接受I/S:
1. 读取Link原位置,验证本次新ACK区间对应的记录已本地完成;越界网络N(R)交给Link拒绝,不用于记录寻址。
2. Link.Handle验证ACK、接收阶段及I帧N(S),成功后推进协议位置。
3. 先标记整个新确认区间的peer_acknowledged,并按本次新确认的HIGH记录数核减high_unacked_count。
4. HIGH不再访问ASDU队列;仅对NORMAL按真实记录的generation、entry_serial调用Queue.Confirm_Next_Normal_Entry:仍有原文就严格匹配队首并释放,原文已覆盖就核减待确认数,不访问或删除新队首。
Link只接受不越过本地完成位置的ACK。例如0、1还仅在ASDU队列时,ACK=2非法;当前I帧只写了一部分时,也不能由ACK代替本地完成。异步接入先提交真实发送结果,再按接收顺序处理相关ACK。
本地会话或记录预检失败返回ERROR;在本地状态合法时,Link拒绝I/S报文返回独立的`IEC104_SEND_MANAGER_PROTOCOL_ERROR`。Link已经受理后出现状态同步或原文释放失败仍返回FATAL。Connection将前者的协议拒绝归为PROTOCOL,将本地预检及后续交接失败归为INTERNAL,不再把所有非OK结果都归责于对端报文。原有返回值数值不变,新增结果仅用于I/S接收接口。
重复ACK不释放第二次,也不重复核减HIGH或覆盖待确认计数,不重置t1。原文覆盖本身不推进V(A)、不释放k窗口或业务回执槽;已有帧仍须真实完成和取得有效ACK。ACK处理复杂度为O(本次新确认条目数)。Link接受后释放原文失败为FATAL;所有已接受的ACK事实仍保留在记录中,恢复时不能把这些数据当成未确认重报。
| 记录接口 | 用途 |
|---|---|
| Peek_Oldest_Sent_Unacked_Record | 常数时间获取最早已本地完成未ACK记录,只有部分发送时EMPTY |
| T1_I_Ack_Expired | 使用该记录原written_at_ms,1到期/0未到期或无等待/-1非法 |
| Get_Deadlines | 当前队首身份、本地发送及I帧t1剩余时间、活动标志和控制容量;只读且不选择新业务 |
| Peek_Confirmed_Record | 查看已完成且已ACK的记录队首;未就绪WAIT,不自动释放 |
| Remove_Confirmed_Record | TcpServer成功复制RECORD事件后,经Connection按frame_id移除队首 |
| Get_Record_By_Offset | 不依赖运行中的Link读取记录,供故障恢复;ACK事实不丢失 |
回执未转为事件并移除时仍占records,容量满会阻止新I帧;已经ACK的原ASDU空间可复用,S/U仍可发送。业务处理RECORD副本失败时应自行保存副本,不调用内部Remove,也不会恢复已移除的记录槽。
<a id="manager-control"></a>
### 4.6 S、t2、TESTFR与t3
Queue_S_Ack按w门限、immediate或停止排空规则生成S。Queue_T2_Ack在原待确认接收数据达到t2时按相同门控生成S。Queue_Testfr_Act主动探测;Queue_T3_Testfr额外检查原空闲时长。Handle_Testfr_Act受理对端ACT并排队CON,不结束本端独立探测。
上述构帧入口先预留控制槽,再调用Link。无容量返回WAIT,不改变链路;未生成报文则取消预留,成功生成返回QUEUED。构帧成功后的提交异常、取消异常均为FATAL。时钟和定时器由连接层管理。
接收确认使用`Handle_Testfr_Con(manager, generation, apci)`。它校验会话后调用Link,不预留发送槽、不生成应答;CON_PENDING下接受确认返回OK,Connection此时才撤销探测t1。提前收到或没有匹配等待时返回IGNORED,不缓存、不撤销计时,控制槽满也不阻止确认受理。Connection不再直接修改Link的探测状态。
Connection分派的STARTDT_ACT、STOPDT_ACT和TESTFR_ACT已经通过格式与方向检查;合法重复请求沿用原受理规则,处理中的ERROR/FATAL归为INTERNAL,包括HIGH清理、控制槽提交或取消失败。TESTFR_CON没有匹配等待时仍忽略,内部状态非法归INTERNAL;子站收到格式合法的反向STARTDT_CON/STOPDT_CON由Connection消费并忽略,不交给Manager,不改变启停、确认期限或t3活动时刻。
Handle_Testfr_Act或Slave_Handle_Startdt_Act返回WAIT时请求尚未受理,调用方必须有界保留完整请求并维持接收顺序,不能静默丢弃或越过它分派后续帧。子站启停不再暂存相反ACT。
<a id="manager-slave"></a>
### 4.7 子站启停
| 入口 | 行为 |
|---|---|
| Slave_Handle_Startdt_Act | 先预留控制槽,成功受理即STARTED/IDLE并排CON;结束旧停止等待,不暂存START |
| Slave_Handle_Stopdt_Act | 无需控制槽,立即STOPPED/STOP_DRAINING并清理HIGH;不等待启动CON,不暂存STOP |
| Slave_Queue_Stopdt_Response | 先排S确认,再等已预留I帧获ACK;满足条件后排STOP_CON并回STOPPED/IDLE |
已在FIFO中的S完整覆盖接收数据时,STOP_CON可紧随排队,不必等S网络完成。队列容量不足返回WAIT,调用方保留义务;START的WAIT表示请求尚未受理,不能取消STOP。Link只控制协议状态,发送缓冲持有部分写入和帧身份。
从停用状态重新START时清理HIGH并重置公平配额;已启用时重复START保留HIGH。STOP清理不撤回当前I帧或记录,不影响NORMAL。
两个CON完成接口只检查发送结果,均不回写dt_state/dt_procedure。后到START可以在旧STOP_CON仍待发时激活,旧完成不能覆盖新状态。停止排空被START结束时尚未生成的STOP_CON不再生成;已经入队的CON必须按FIFO发送。
Slave_Queue_Stopdt_Response新增可空输出stop_con_id,仅STOP_CON成功入队时写入。Connection用它记录本轮最后一帧停止应答,匹配完整发送后才结束STOP期限;成功受理START也取消旧计时。重复STOP不刷新有效起点,旧CON完成不能误清新STOP的期限。
### 4.8 Init与Abort
Init要求Link为CONTROLLED初始状态、Buffer空闲、Queue与Buffer同代次且Queue没有已提交条目。允许Queue含预先排队或Rebind恢复的未编号业务;Init成功会清空HIGH、保留NORMAL,检查失败时不清理。HIGH出队后队列本身无法体现旧连接的在途帧,接入方仍须先结束旧I/O并Abort旧Manager,不能用Init接管运行中的连接。不分配存储。
Connection使用构造专用`Init_Connection`:在局部Link/Buffer/Manager完成上述校验,最后一次可失败操作为清理HIGH;随后只向指定的最终地址提交已校验对象,并在Manager内部建立绑定。ERROR时Manager、Link、Buffer及队列都不变,也不改控制槽和记录数组。Connection随后提交其余连接字段,无需访问或修补Manager内部指针。此入口不支持运行中重绑;对象成功初始化后仍不能复制或移动。原Init继续支持绑定已单独初始化的Link/Buffer。
Abort前,TcpServer先结束旧I/O并把剩余记录复制为事件;Connection_Close内部调用Manager.Abort清除Manager及Buffer,**保留AsduQueue**。TcpServer随后调用Queue.Detach,立即清空HIGH并恢复NORMAL离线缓存;仅关闭整个服务时调用Queue.Abort。新连接代次由TcpServer管理,Link重新初始化和发送模块绑定由Connection_Init完成。
<a id="integration"></a>
## 5. 内部调度、容量与恢复
### 5.1 静态存储与初始化
以下示例仅说明发送模块的内部绑定关系。实际对象由TcpServer及其内含Connection持有,业务通过TcpServer_Create/Init启动服务,无需复制这些声明或调用底层Init。
~~~c
#include "IEC104_SendManager.h"
static uint8_t high_bytes[1024];
static uint8_t normal_bytes[4096];
static IEC104_SendBufferControlFrame_t controls[4];
static IEC104_IFrameSendRecordStorage_t records[12];
static IEC104_AsduQueue_t queue;
static IEC104_SendBuffer_t buffer;
static IEC104_LinkState_t link;
static IEC104_SendManager_t manager;
/* 仅用于全新或明确丢弃旧业务后的队列,不用于保留业务的重连。 */
static int Init_Fresh(uint64_t generation)
{
if (!IEC104_Link_State_Init(&link, IEC104_ROLE_CONTROLLED) ||
IEC104_AsduQueue_Init(&queue, high_bytes, sizeof high_bytes,
normal_bytes, sizeof normal_bytes, generation) != IEC104_ASDU_QUEUE_OK ||
IEC104_SendBuffer_Init(&buffer, controls, 4, generation) != IEC104_SEND_BUFFER_OK) {
return 0;
}
return IEC104_SendManager_Init(&manager, &queue, &buffer, &link,
records, 12, 12, 4) == IEC104_SEND_MANAGER_OK;
}
~~~
直接使用AsduQueue时,HIGH至少 `IEC104_ASDU_HIGH_QUEUE_MIN_CAPACITY`(8)字节;NORMAL至少 `IEC104_ASDU_NORMAL_QUEUE_MIN_CAPACITY`(当前ABI下22)字节。保证同时保留N条、ASDU最长L字节(N≥1、6≤L≤249)时,HIGH配置 `N × (2 + L)`,NORMAL配置 `N × (IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN + L)`,无需额外回绕空间。已知各条实际长度时,占用为 `Σ(本级头长 + Li)`。保留10条最大249字节ASDU,HIGH需2510字节,NORMAL在当前ABI下需2650字节。
HIGH的N只计算尚未提交的待发条目;NORMAL的N还包括原文仍保留的当前发送和已发未ACK条目,两级分别规划。需要支持任意合法ASDU长度时,对应桶至少本级头长+249字节。回执尚未转为事件的延迟计入records容量,业务读取事件的延迟计入TcpServer事件容量;事件区满也会阻止继续转移记录。仅NORMAL原文环采用覆盖;HIGH、控制FIFO、确认记录和k窗口各自保持拒绝或等待语义。已覆盖原文不再占字节容量,但对应的已编号帧仍占k窗口和records。
TcpServer公开接口支持全部合法ASDU长度,因此编译期要求HIGH和NORMAL各自至少容纳一条最大ASDU及本级条目头,即对应`ENTRY_HEADER_LEN + IEC104_MAX_ASDU_LEN`;当前ABI下分别为251和265字节。`Send_Response`的回退排队使用HIGH,`Enqueue_Event`使用NORMAL。底层AsduQueue仍允许上述较小容量;TcpServer不接受这种无法容纳最大ASDU的配置,避免应答在直接准备时成功、回退排队时却因总容量不足返回SUBMIT_ERROR。
Windows 64位GCC 16.1.0默认ABI下:AsduQueue控制结构120字节、SendBuffer结构336字节(含唯一255字节I APDU)、控制槽40字节、发送记录存储类型48字节、查询快照类型56字节、Manager结构64字节。共享连接代次曾将存储记录从64字节降为56字节;此次移除user_tag后从56字节降为48字节,12条记录数组由672字节降为576字节,再节省96字节。以上不是跨平台尺寸承诺,目标平台应以sizeof为准。
ASDU每条恰好占本级头长+实际长度,回绕不额外占容量。Manager有249字节的局部拼接数组;Init_Connection另有临时Link、Buffer和Manager用于预检,两条调用路径均须按目标编译结果计入栈预算;Rebind更新NORMAL头内状态和控制字段,不搬移ASDU,不使用整理数组。
相比之前每条固定272字节的业务槽,小ASDU可以共用更多容量;最大249字节ASDU在HIGH中占251字节,在NORMAL中占头长+249(当前ABI下265)字节。原文环容量和发送记录容量分别计算,实际RAM需求还取决于两者的配置。
### 5.2 业务入口与内部收发循环
~~~text
业务提交ASDU:
NORMAL:TcpServer_Enqueue_Event → AsduQueue_Enqueue(服务缓存)
应答:TcpServer_Send_Response → Connection_Submit_Response → Manager.Submit_Response
直接准备 → 共用Write_Output最多一次非阻塞写入
暂不能准备或协议期限已到 → Manager.Enqueue暂存HIGH
仅TCP_SUBMIT_OK后推进生产游标;NORMAL的OK可能已经覆盖旧原文
TCP_SUBMIT_WAIT保留原文,后续Poll间隙重试
TCP_SUBMIT_ERROR/FATAL → 按参数、连接身份或故障处理,不当作容量不足盲目重试
TCP内部准备发送:
先安排应生成的S/U
Connection_Prepare_Next_Send → Manager.Prepare_Next_Send
EMPTY/WAIT → 等待业务、ACK、记录转移或状态变化
Connection返回SEND_READY → 共用Write_Output经Net写socket(head.pending_data, head.remaining_bytes)
正字节数 → Connection_Handle_Send_Progress → Manager.Handle_Send_Progress
EAGAIN/EWOULDBLOCK → 等待再次可写,保持当前帧
EINTR → 按平台规则重试,不伪造进度
最终失败 → Connection_Handle_Send_Failure → 结束连接
FRAME_WRITTEN后才处理依赖这次完成的接收事件,再准备下一帧
TCP内部接收:
TcpServer经Net收到字节 → Connection.Feed → Receiver.Feed
TcpServer按consumed保留剩余输入
成帧后Connection经Receiver.Peek_Frame取得APDU及已解析APCI
Connection把I/S及需要处理的U帧交给Manager,对Link只作只读查询
WAIT → 保留当前接收帧,容量恢复后重试,不越过它处理后续帧
S/U已受理 → Connection释放内部接收帧
I帧已受理 → TcpServer复制I_FRAME事件 → 经Connection释放内部接收帧
确认回执就绪 → TcpServer复制RECORD事件 → 经Connection移除内部记录
业务处理事件:
TcpServer_NextEvent取出副本 → 解析ASDU或处理发送回执
需要应答时调用TcpServer_Send_Response,不调用底层Release或Remove
~~~
不要把Prepare成功当作已经发送;也不要把socket实际成功但尚未提交的写入长期滞留在异步完成队列。异步情况下同一帧最多一个未回报写操作;隔离代次不能代替结束旧I/O对数组的访问。
仅检查dt_state==STARTED不能代替Link完整门控。初始STOPPED时可以缓存NORMAL;HIGH应答须等STARTED/IDLE后再提交。被控站受理START立即开放Link的新I/S准备;Manager继续遵守SendBuffer顺序,已有当前I帧先完成、控制FIFO随后排空,再选择新I帧。HIGH在Manager初始化、受理新STOP以及从STOPPED受理START时清空;已启用时重复START保留HIGH。
<a id="timeouts"></a>
### 5.3 原始期限
| 期限 | 起点及接口 |
|---|---|
| 当前帧本地发送期限 | 原queued_at_ms;Connection经Manager.Get_Deadlines取得剩余时间和同一队首身份;Buffer.Head_Send_Expired保留为底层接口 |
| I帧等待ACK t1 | 最早已完成未ACK记录的原written_at_ms;Connection经Manager.Get_Deadlines查询,单项T1_I_Ack_Expired保留 |
| TESTFR等待CON t1 | ACT完整发送且Link完成成功的written_at_ms;Connection记录并通过Collect_Deadlines计算 |
| 接收确认t2 | 连续未确认接收区间的原起点;Collect_Deadlines判定到期后调用Queue_S_Ack(immediate=1),Queue_T2_Ack保留为底层接口 |
| 空闲t3 | 有效链路活动起点;Collect_Deadlines判定到期后调用Queue_Testfr_Act,Queue_T3_Testfr保留为底层接口 |
| 停止过程总期限 | 新STOP的received_at_ms;重复STOP不续期,成功受理START或本轮最后一帧STOP_CON匹配完成时结束 |
Connection的Check_Timeouts、Get_Wait_Time、发送准入及控制推进共用临时Collect_Deadlines视图。Manager的Get_Deadlines仅观察已存在的发送头和记录,OK才写输出;对应has_*为1且remaining_ms为0才表示到期。期限计算使用64位时间差,起点0合法,不计算可能溢出的绝对到期时刻。接收批次可以暂缓确认故障处理,实际到期事实及原始起点不改变。
定时器只负责唤醒,回调再次核对代次和当前有效起点。仅业务排队尚未编号不启动I帧t1。ACK推进后选下一最早未确认帧的原时间,不从当前时间重新开始。
<a id="failure"></a>
### 5.4 断线恢复
以下操作由TcpServer和Connection完成。NORMAL固定随服务保留,HIGH固定属于连接;业务使用事件提交与应答发送入口,并通过事件读取收尾结果。直接应答同样不跨连接恢复。
1. 停止分派旧连接事件,关闭TCP并结束/隔离旧I/O对存储的访问。
2. TcpServer经Connection_Get_Record_By_Offset读取剩余记录并复制为closing事件,区分已ACK、未ACK和未本地完成。NORMAL的记录可能仍在而原文已覆盖。
3. 核对记录中的ACK事实。只有NORMAL现存原文参与恢复,且业务须允许向同一接收方或冗余域重报;未ACK不证明对端未收到。HIGH请求应答不跨连接保留。
4. Connection_Close调用Manager.Abort,保留Queue。TcpServer随即调用Queue.Detach(old_generation),清空HIGH、恢复现存NORMAL待发状态并切换到代次0,继续接受离线NORMAL。新连接建立时调用Queue.Rebind(new_generation);任一步ERROR均停止本次恢复,不能重新Init而静默丢弃缓存。
5. Connection_Init经Manager.Init_Connection校验并提交初始CONTROLLED Link、空Buffer及Manager绑定,再提交其余连接字段;队列可含未编号业务,失败不覆盖原对象或队列。
6. 新连接STARTDT完成后,以新N(S)生成新的APDU和frame_id。
例如HIGH中有总召应答A、命令确认C,NORMAL中仍有事件B、D;断线Detach清空A、C,保留B、D原顺序并恢复未提交;离线新增E追加在后,下次Rebind绑定B、D、E。已提交的HIGH原文此前已出队,也不会重发。
Detach与Rebind共用绑定转换,先完整验证两级所有条目的长度、占用范围及待发分界,并验证NORMAL的编号顺序和头内状态。均通过后,将NORMAL已提交前缀的头内状态恢复为未提交;后续没有可失败分支,因此ERROR仍保证控制字段及字节不变。HIGH元数据清零,NORMAL将unsubmitted_index设为head_index、unsubmitted_count设为entry_count;只更新头,不搬移或清除ASDU原文。复杂度O(条目数),额外存储固定,不使用malloc。NORMAL保留条目的entry_serial和原文不变,next_entry_serial不重置;更换generation使所有旧entry_id和借用失效。
Detach/Rebind清除旧连接的覆盖待确认计数、确认边界及EOF发送限制,只能恢复NORMAL环中仍存在的原文。已覆盖的原文无法恢复。重连丢弃不产生回调或伪造ACK;未编号条目没有帧回执,已编号帧的旧回执应在Manager.Abort前读取。Rebind本身不发送,不提供冗余组管理;不要在同一TCP的普通STARTDT恢复中调用它。
如果FATAL发生在有效ACK后的原文释放阶段,部分已ACK条目可能仍保留在Queue。记录先登记了整个ACK区间,内部恢复逻辑不能把所有保留原文盲目当作未确认数据重报;业务根据公开记录判断发送事实,不直接修改内部队列。常规网络断线中,正常完成的ACK已释放对应原文。
### 5.5 满时背压
| 满的位置 | 行为及恢复条件 |
|---|---|
| HIGH字节环 | 当前空间不足返回FULL,释放后重试;不覆盖旧原文 |
| NORMAL字节环 | 从队首覆盖最旧原文直至足够,包含已提交未ACK条目;成功返回OK,无逐条丢弃通知 |
| 两级条目超过桶总容量 | ERROR,不覆盖、不修改队列或编号;须修正容量或组帧,不按背压重试 |
| 控制队列 | 控制接口WAIT,未受理接收请求须有界保存;控制发送完成后恢复 |
| 记录数组 | 准备新I返回WAIT;TcpServer复制RECORD事件并移除内部记录后恢复;控制帧仍可发 |
| k窗口 | Link不生成新I,Manager返回WAIT;有效ACK后恢复 |
| 当前帧部分写入 | 返回同帧剩余字节;保持FIFO字节顺序,不为后续业务编号 |
<a id="build"></a>
## 6. 构建和迁移
### 6.1 生产构建依赖
子站发送模块需要以下六个源文件:
~~~text
IEC104_Codec.c
IEC104_Link.c
IEC104_Link_Slave_StartStop.c
IEC104_AsduQueue.c
IEC104_SendBuffer.c
IEC104_SendManager.c
~~~
发送管理本身不需要主站启停实现或日志模块。独立流式接收另加入 `IEC104_Receiver.c` 和 `IEC60870_Codec.c`,分别完成104 APCI与公共ASDU头检查;旧 `IEC104.c/h`已原样恢复,但旧入口使用私有Link且不向Manager交付报文。Receiver完成APCI检查,连接层通过Peek取得视图并分派,受理后显式释放;Link公开协议接口签名不变。
2026-09-30共享连接代次历史验证:当时的5个专项场景覆盖完整身份及业务字段往返、混合优先级、部分写入、记录环复用、重连后旧代次拒绝、故障后读取ACK事实、NORMAL原文覆盖及查询失败不修改输出。GCC 16.1.0以 `-std=c11 -O2 -Wall -Wextra -Werror` 编译,专项 `5 passed, 0 failed`,原有全量 `119 passed, 0 failed`。该测试后因接口过期已于2026-10-19删除,不计入当前回归;历史命令和运行记录保存在`.build/shared-generation-check`。使用模拟I/O,未运行真实TCP。
2026-09-24编号回绕历史验证:当时新增独立的iec104_asdu_queue_test.c,以GCC 16.1.0、`-std=c11 -O2 -Wall -Wextra -Werror`编译,13个专项场景通过,其中包含3万次与独立FIFO模型逐步对照的随机操作。专项覆盖编号/字节环同时回绕、跨界覆盖ACK、重复及旧代次拒绝、重连保留顺序、失败不修改字节、半环窗口阻塞及恢复,以及Manager部分发送、EAGAIN和累计ACK。原有全部119个用例重新编译通过。64位边界测试在夹具中设置计数起点,窗口极限测试显式快进计数,其余队列及发送操作使用公开接口;未模拟真实TCP。当前目录未保留该专项源文件,这13个场景未计入2026-09-30的运行结果。
以下为此前字节环和重连策略改造的历史验证:严格警告编译、GCC静态分析和当时全部119个用例通过。临时校验从标准输入编译运行,覆盖1182组条目起点与长度组合、60万次队列随机操作、391组跨界位置构帧及7万帧协议序号回绕;重连策略另验证29760组满环起点、保留标记及提交分界组合。这些历史结果不代替本次编号回绕专项,真实TCP及嵌入式目标平台仍需接入验证。
<a id="migration"></a>
<a id="64-本次环形队列改造与接口迁移"></a>
### 6.2 接口迁移
此次高低优先级头拆分不提供旧条目格式兼容。所有调用方须按新头文件重新编译并初始化队列及Manager,不能把旧18字节头的缓冲内容交给新代码。NORMAL头采用本次构建的位域及对齐布局,不是跨ABI持久化格式。
| 原接口/约定 | 当前接口/约定 |
|---|---|
| `Enqueue(queue, generation, priority, asdu, length, user_tag)` | 删除最后一个参数,改为 `Enqueue(queue, generation, priority, asdu, length)` |
| `AsduHeadInfo.user_tag`、两种发送记录的 `user_tag` | 删除,不再保存或返回业务标识 |
| `IEC104_ASDU_ENTRY_HEADER_LEN` | 删除,按优先级改用 `IEC104_ASDU_HIGH_ENTRY_HEADER_LEN` / `IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN` |
| `IEC104_ASDU_QUEUE_MIN_CAPACITY` | 删除,按优先级改用 `IEC104_ASDU_HIGH_QUEUE_MIN_CAPACITY` / `IEC104_ASDU_NORMAL_QUEUE_MIN_CAPACITY` |
| HIGH的非零entry_serial及按身份消费 | 查询的entry_serial和记录的asdu_entry_serial均为0;Mark/Discard消费当前队首,Peek至消费之间不得移除、清空或替换队首 |
| HIGH/NORMAL共用条目编号 | 仅NORMAL消耗next_entry_serial,HIGH不受NORMAL编号窗口阻塞 |
| NORMAL只用控制游标推导提交状态 | 条目头另存状态并与游标交叉校验;Mark写已提交,Rebind将已提交前缀恢复未提交 |
传给 `IEC104_SendManager_Init` 的记录数组继续使用 `IEC104_IFrameSendRecordStorage_t[]`,容量仍以条数计;查询输出继续使用 `IEC104_IFrameSendRecord_t`,两个类型均已删除user_tag,不能继续使用旧布局。发送结果和回执消费仍传完整frame_id,HIGH与NORMAL均有独立帧身份。结束旧I/O并回收记录后再Abort,随后重新绑定新代次。
发送状态命名按“未提交”“原文保留且未确认”“原文覆盖且未确认”区分。本次还改变HIGH存储生命周期、重连策略和结构布局;调用方需同步迁移,重新编译并初始化对象,不提供旧内存布局兼容。
| 旧名称 | 当前名称及含义 |
|---|---|
| AsduQueue.overwritten_serial_begin | overwritten_unacked_serial_begin,覆盖待ACK循环区间起点,含端点 |
| AsduQueue.last_overwritten_submitted_serial | overwritten_unacked_serial_end,同一区间终点,含端点 |
| AsduQueue/Status.overwritten_submitted_count | overwritten_unacked_count,原文已覆盖但仍待ACK的实际数量 |
| AsduPriorityQueue.waiting_index/waiting_count | unsubmitted_index/unsubmitted_count,队列中尚未登记发送交接的条目位置/数量 |
| AsduQueue.Peek_Waiting | Peek_Unsubmitted,直接查询队列中下一条尚未登记发送交接的条目 |
| AsduQueueStatus.submitted_count | retained_unacked_count,只统计原文仍保留的已提交未ACK条目 |
| AsduQueueStatus.is_empty | is_storage_empty,只说明原文存储为空 |
| SendBufferStatus.is_initial_state | is_idle,当前无I帧、控制帧及未结束的预留;发送结束后也可再次为1 |
| SendManager.next_commit_ns | next_record_ns,下一条新登记I帧记录的N(S) |
以下为此前存储和接口改造的迁移说明。
本次改变三个模块的结构布局和部分接口,需要更新外部调用方并重新编译,不提供旧布局兼容。
| 旧接口/结构 | 当前替换 |
|---|---|
| IEC104_AsduEntry_t固定条目数组 | 两个uint8_t字节数组,Init容量单位改为字节 |
| AsduQueue.Get_Free_Count | Get_Free_Bytes;容量计每条本级头长和ASDU实际长度 |
| AsduHeadInfo.asdu单一连续指针 | asdu_first/asdu_first_length及可选asdu_second;需要连续原文时调用Copy_Asdu |
| AsduPriorityQueue.padding_bytes | 已删除;头和原文可跨尾部,不再产生填充占用 |
| 两级Enqueue空间不足都返回FULL | 字节容量不足时HIGH返回FULL、NORMAL覆盖最旧原文;只有NORMAL可能被编号窗口阻塞;条目超过总容量仍为ERROR |
| AsduQueue.last_confirmed_normal_serial | 替换为overwritten_unacked_serial_begin,作为覆盖待ACK区间下界;该控制成员语义改变,版本迁移须重新初始化 |
| AsduQueue及Status只记录原文条目 | 增加overwritten_unacked_count;Queue另保存覆盖和确认身份边界,结构布局改变 |
| AsduHeadInfo借用可跨任意入队保留 | NORMAL后续入队可能覆盖该原文,覆盖后借用立即失效 |
| Peek_Head后构帧并立即移除队首 | Manager内部Peek_Unsubmitted、Mark_Submitted;HIGH提交时出队,NORMAL有效ACK才Confirm_Next_Normal_Entry |
| IEC104_SendBufferNode_t数组 | IEC104_SendBufferControlFrame_t数组;I APDU已内置Buffer |
| Buffer.Init(buffer,nodes,capacity,reserve,generation) | Init(buffer,controls,control_capacity,generation),删除reserve参数 |
| BufferStatus三个序号位置及未确认数 | Link.Get_Send_Progress;BufferStatus仅当前I、控制数和预留状态 |
| Buffer.Apply_Peer_Ack | 删除;统一Manager.Handle_I_S_Frame处理 |
| Manager.Queue_Next_I_Frame(...,record) | Prepare_Next_Send(...,head_info),成功返回OK,紧邻实际发送调用 |
| 发送前只Buffer.Peek_Send_Head | Manager.Prepare_Next_Send;Buffer.Peek仍仅查询已准备帧 |
| Manager.Init只允许空业务队列 | 允许非空未编号队列,成功绑定会清空HIGH、保留NORMAL;拒绝仍有NORMAL待确认条目的队列 |
| Manager.Abort同时丢弃业务队列 | 保留业务队列;显式Rebind清空HIGH、恢复NORMAL,或Queue.Abort全部丢弃 |
| Enqueue_Ex及逐条重连策略 | 删除Enqueue_Ex、IEC104_AsduReconnectPolicy_t和HeadInfo.reconnect_policy;HIGH固定清空,NORMAL固定保留,统一调用Enqueue |
| Rebind逐条筛选和原地整理 | 完整预检后清空HIGH、将NORMAL现存条目恢复未提交;不搬移原文,失败不改字节或控制状态 |
| HIGH原文留到ACK | Mark_Submitted成功时立即出队;HIGH未确认数量改由Manager.high_unacked_count维护,ACK不调用Queue.Confirm_Next_Normal_Entry |
| STARTDT只改变链路状态 | 从停用状态START时清理HIGH及配额;已启用时重复START保留,WAIT未受理不清理;启停立即生效、CON完成不回写状态 |
| 应答在任意启停阶段都可缓存HIGH | Submit_Response及Enqueue(HIGH)共用准入白名单;新STOP受理时清空待发HIGH,已提交I帧和记录继续排空 |
| 仅靠user_tag找恢复数据 | 已删除user_tag;NORMAL用generation和asdu_entry_serial关联保留原文,HIGH只提供frame_id标识的帧回执 |
Enqueue参数顺序为queue、generation、priority、asdu、length;旧Enqueue_Ex调用须删除策略和user_tag参数,并按业务生命周期选择HIGH或NORMAL。HIGH头为uint16长度,NORMAL头为编号、状态和长度结构,须分别按对应头长宏计算容量。跨尾部存储不改变Manager.Prepare_Next_Send及其返回的连续APDU视图,直接读取AsduHeadInfo原文的外部代码须按两段接口使用。Handle_Send_Progress、Handle_I_S_Frame、控制帧封装及记录消费的函数签名保持;结构布局和来源身份语义按本文更新。
### 6.3 当前边界
已经实现发送数据结构、编号调度、部分写入记账、ACK释放、回执、超时判定和显式恢复基础。Receiver提供每连接独立分帧、代次检查和完整APDU交付;旧全局接收入口作为历史代码保留,不参与TcpServer路径。Connection统一收发分派、U帧路由及期限推进;实际网络操作经Net后端执行,TCP连接管理和周期驱动由[TcpServer](IEC104_TcpServer_设计架构.md)负责。应用持续调用TcpServer_Poll/NextEvent,通过TcpServer_Enqueue_Event或TcpServer_Send_Response提交ASDU;底层发送模块由TCP内部驱动,没有后台自动同步。
@@ -0,0 +1,529 @@
# IEC104 编解码与独立接收器设计架构
> **用途**:说明 `IEC104_Codec.c` 的APCI解析与I/S/U帧构造、`IEC60870_Codec.c` 的101/104公共ASDU头解析,以及 `IEC104_Receiver.c` 的每连接流式成帧、完整报文交付和错误处理。
> **范围**:第一至九章描述基础模块现有实现;第十章说明后续集成设计,其中单当前I帧、独立S/U队列、两级ASDU字节环及发送调度接口已实现,其余能力以各项说明为准。`IEC104_Link.c` 的状态和算法见配套文档 [IEC104_Link 设计架构](IEC104_Link_设计架构.md)。本文不是 IEC 60870-5-104 标准全文,也不定义具体 Type ID 的信息对象业务。
> **实现依据**:`IEC104_Codec.c/h`、`IEC60870_Codec.c/h`、`IEC60870_Types.h`、`IEC104_Receiver.c/h`及旧 `IEC104.c/h`;历史验证记录按当时版本保留。
> **同步日期**:2026-09-23。发送模块已改为变长ASDU队列、发送前编号及单当前I帧。严格警告编译、原有119个用例及临时专项校验通过;测试源码、配置和测试文档保持原样。
> **当前接口同步**:2026-10-08。已新增 `IEC104_Receiver.c/h`,并从删除前备份原样恢复 `IEC104.c/h`及旧全局接口,两套接收实现共存。上面的2026-09-23同步说明为历史记录;本文主要描述Receiver,旧入口边界另行注明。
> **测试清理**:2026-10-19。接口过期的旧回归测试及专用辅助文件已删除,当前测试入口及覆盖范围见[测试说明](../test/IEC104测试文档.md)。
> **信息对象同步**:2026-10-21。`IEC60870_InformationObjects.c/h`提供12种监视、18种命令的独立对象编解码;`IEC60870_Asdu.c/h`统一维护公共头、VSQ/SQ、容量、对象分派及原文应答。编码经对象函数表,解码按Type ID分派;带时标类型复用基础值处理及公共CP56工具。该模块由业务显式调用,不改变Receiver准入或链路序号处理;实际总召、校时和遥控流程仍待业务接入。接口及示例见[README](README.md#asdu信息对象模块)。
> **TCP接入同步**:2026-10-15。`IEC104_Connection.c/h`负责子站连接调度;`IEC104_TcpServer.c/h`提供IPv4监听及非阻塞收发,Windows依赖在Net后端。业务通过Create取得静态池不透明句柄,使用Init、Prepare/Process或Poll、NextEvent、Enqueue_Event/Send_Response、Get_Status/Get_Remote_Address;Close_Connection只断开指定代次主站,Close整个服务后读空事件再Destroy。公开头文件只依赖`IEC104_Types.h`及标准类型。构建和业务接入见 [TCP服务设计](IEC104_TcpServer_设计架构.md)。
> **子站入口同步**:2026-10-23。`IEC104_Slave`统一管理服务、调度和应答,NextEvent自动提供ASDU视图及原连接接口;业务无需手动绑定generation。延迟任务通过Copy_Event复制原文并重建自引用,旧请求在重连、重新Init和服务槽复用后仍被隔离。共享TCP值类型已迁入`IEC104_TcpTypes.h`。
> **业务边界**:子站业务从Slave接入,Slave内部调用TcpServer。本文的Receiver、Connection、SendManager及链路调用说明面向内部实现;业务不创建或驱动这些对象,不持有内部借用视图。业务可使用Codec和公共时间工具处理NextEvent返回的ASDU副本。
> **连接事件**:NextEvent分别交付CONNECTED、ACTIVATED、DEACTIVATED和DISCONNECTED。子站激活/停用在START/STOP受理时产生,使用data.connection载荷并按序保存;CON完整写出不再改变状态。同状态重复ACT及单纯断开不补造启停事件。
> **通知容量**:按实际载荷长度存放于字节环;默认保证普通30条最长通知、启停4条、收尾32条记录加1个最长I帧和1个断开通知,均不覆盖。启停配额满时延后受理改变状态的ACT,已有CON继续发送,NextEvent消费后安排重试。普通I帧无处交付时保留原帧并暂停续收;回执因空间或预算不足可晚于启停通知交付,各类背压不延长协议期限。
> **队列监控同步**:2026-10-16。Get_Status新增high_queue/normal_queue数值快照,分别查询待发条目数、保留原文条目数及总容量/已用/空闲字节数。通过asdu_queue_status_valid判断统计有效性;Init成功后离线仍有效,Close后无效。待发数不含已交给发送模块的ASDU,NORMAL等待ACK的保留原文仍占用存储,字节数包含条目头。业务继续只使用TcpServer,公开状态结构扩展后须重新编译调用方;详见[ASDU队列积压查询](IEC104_TcpServer_设计架构.md#21-asdu队列积压查询)。
---
## 一、模块速览
### 1.1 总体职责
`IEC104_Codec.c` 负责104完整APDU的APCI检查及I/S/U编码;`IEC60870_Codec.c` 负责101/104共用的参数校验、ASDU公共头解析及最低载荷长度检查。两者均不保存连接状态,公共Codec不依赖104实现。
`IEC104_Receiver.c` 为每条连接提供独立的固定接收缓冲,接受任意长度的有序字节片段。每次Feed最多收齐一帧,通过consumed报告实际消费的输入,APCI检查成功后保留完整帧,供调用方读取和显式释放:
~~~text
IEC104_TcpServer.recv提供有序字节,经Connection.Feed递入接收器
→ Receiver.Feed:同步、长度检查、收集APDU、调用104 Parse_APCI和公共Parse_ASDU_Header
→ FRAME_READY:停止消费后续输入
→ Receiver.Peek_Frame:取得APDU及APCI只读视图
→ 连接层按类型分派:子站I/S交给SendManager,U交给对应控制入口
→ S/U受理后释放;I帧由TcpServer复制为事件后经Connection释放
→ 再递入剩余字节;业务通过TcpServer.NextEvent读取事件副本
~~~
Receiver已经提供多实例、半包/粘包处理、完整帧交付、代次检查及待处理帧保留。它不检查链路序号,不修改Link,不预设STARTED,仅检查ASDU公共头及最低载荷长度,不执行对象或业务校验,也不执行socket、系统定时器或业务回调。
同连接的接收、发送完成及ACK处理由`IEC104_Connection`串行组织。`IEC104_TcpServer`负责TCP监听调度、收发预算、断线收尾和事件队列,真实网络操作通过Net后端执行;Connection负责I/S/U统一分派及协议期限。业务通过`IEC104_Slave_NextEvent`取得ASDU及应答接口并校验具体Type ID;Slave内部经`IEC104_TcpServer_Send_Response`提交应答。
### 1.2 文件地图
| 文件 | 职责 |
|---|---|
| `IEC104_Codec.c/h` | 104专用APCI解析、I/S/U帧构造和帧类型 |
| `IEC60870_Codec.c/h` | 101/104共用ASDU公共头解析、参数及最低载荷长度检查 |
| `IEC60870_Types.h` | 公共应用层参数和ASDU存储上限,无传输状态 |
| `IEC60870_Asdu.c/h` | 通用ASDU容器、精确布局、VSQ/SQ、地址和容量检查、取对象及原文应答 |
| `IEC60870_InformationObjects.c/h` | 每类独立结构、Create与Get_From_Buffer;函数表编码、Type ID解码分派,复用基础类型和CP56 |
| `IEC104_Receiver.c/h` | 每连接独立流式成帧、完整APDU借用视图、消费数和显式释放;依赖104 Codec和公共Codec |
| `IEC104.c/h` | 已恢复的旧单路逐字节接收入口,持有全局接收器和私有Link,仅返回接收结果 |
| `IEC104_Link.c`、`IEC104_Link_Common.h` | 连接层持有的公共I/S、窗口、确认及TESTFR状态;角色启停分别在Master_StartStop、Slave_StartStop模块 |
| `IEC104_Link.h` | 兼容总头文件;角色接口和构建迁移见 `README.md` |
| `IEC104_SendBuffer.c/h` | 子站唯一当前I帧、紧凑S/U FIFO、部分写入与本地发送期限;协议序号由Link独占 |
| `IEC104_AsduQueue.c/h` | HIGH/NORMAL变长字节环,条目可跨尾部、无填充碎片;HIGH满时拒绝、提交后出队,NORMAL覆盖最旧原文;Rebind清空HIGH、恢复NORMAL现存原文 |
| `IEC104_SendManager.c/h` | 子站发送前选择及编号、I/S接收、完成分派、ACK释放原文、独立回执和I帧t1;由Connection统一驱动 |
| `IEC104_Connection.c/h` | 每连接持有Receiver、共享Link和发送模块,统一分派、超时推进及故障收尾 |
| `IEC104_Types.h` | 协议常量、ASDU优先级及公开协议配置,不包含运行对象 |
| `IEC104_TcpServer.c/h` | IPv4单主站监听调度、静态池不透明句柄、收发预算、事件/状态/对端地址查询及单连接关闭;通过Net适配平台 |
| `IEC104_TcpServer_Config.h` | 对象池及内部缓冲的编译期容量,不由业务头文件转发 |
| `IEC104_Log.c/h` | 日志级别、格式化、回调或标准流输出 |
| `iec104_tcp_event_ring_test.c` | 当前TCP事件字节环专项,覆盖容量、次序、跨环尾及背压恢复 |
| `iec104_tcp_scheduler_test.c` | ACK/TESTFR确认边界、固定输入快照、独立发送预算及真实超时 |
| `iec104_net_pending_input_test.c` | Windows回环验证内核输入字节数快照、部分读取及EOF |
| `IEC104测试文档.md` | 当前测试的构建方法、覆盖范围和验证记录 |
依赖方向为:
~~~text
IEC104_Receiver.c ──→ IEC104_Codec.c(APCI)/ IEC60870_Codec.c(ASDU公共头)
IEC104.c ──→ IEC104_Codec.c / IEC104_Link.c / IEC104_Log.c(旧单路入口)
IEC104_Link.c / Master_StartStop.c / Slave_StartStop.c
└──────→ IEC104_Codec.c(调用IEC104_Build_*编码函数)
IEC104_SendBuffer.c ──→ IEC104_Codec.c(提交报文时校验APCI)
IEC104_TcpServer.c ──→ IEC104_Connection.c ──→ Receiver / SendManager / Link
应用层 ──→ IEC104_Slave.h(不透明子站句柄、自动ASDU/应答接口)
IEC104_Slave.c ──→ IEC104_TcpServer.h(不透明TCP句柄、公开配置/事件/状态)
IEC104_TcpServer.h ──→ IEC104_Types.h(仅值类型,不包含Connection或Net)
Receiver / Link / AsduQueue的内部模块头文件 ──→ IEC104_Codec.h
IEC104_Codec.c / IEC60870_Codec.c ──→ C标准库
IEC60870_Asdu.c / IEC60870_InformationObjects.c ──→ IEC60870_Codec.c / IEC60870_Time.c
IEC60870_Codec.h / IEC60870_InformationObjects.h ──→ IEC60870_Types.h
IEC104_Log.c ──→ 平台时间与标准I/O,不依赖协议模块
~~~
独立成帧需编译 `IEC104_Receiver.c`、`IEC104_Codec.c` 和 `IEC60870_Codec.c`。仅检查104 APCI或构帧使用104 Codec,仅解析ASDU头使用公共Codec;Link及发送模块继续按README选择源文件。
旧 `IEC104.c/h`已原样恢复,原有调用方可继续包含 `IEC104.h`;该头文件仍包含Codec声明。旧入口保留全局接收器、私有Link和STARTED预设。独立流式接收使用 `IEC104_Receiver.h`及显式对象接口,由连接层管理共享Link;两套入口不互相转发报文或共享接收状态。
### 1.3 协议与缓存边界
```text
字节下标 0 1 2 3 4 5 6 ...
┌──────┬────────┬──────────────────────────────┬──────────┐
字段 │ 0x68 │ 长度 L │ 4字节控制域 │ ASDU │
└──────┴────────┴──────────────────────────────┴──────────┘
```
- 完整 APDU 长度的计算公式为 `2 + L`。
- I 帧 ASDU 长度的计算公式为 `L - 4`,其中4是控制域长度。
| 项 | 当前值 | 含义 |
|---|---:|---|
| 起始字节 | `0x68` | APDU 同步标志 |
| 长度域 L | 1 字节,数值为 `4..253` | 仅计算控制域和 ASDU,不包含起始字节及长度域自身 |
| 控制域 C0..C3 | 共 4 字节 | I/S 帧编码序号,U 帧编码功能码 |
| ASDU | 最大 249 字节 | I 帧携带;S/U 帧不携带 |
| ASDU 公共头 | 104为6字节 | Type ID、VSQ、COT、OA和CA;公共ASDU层按配置计算为4..6字节 |
| APDU 最大长度 | 255字节 | 2字节头部加253字节长度域内容 |
### 1.4 使用硬约束
1. 每条连接使用独立Receiver,先用非零代次Init;仅清零对象不等于完成初始化。同一对象的所有操作串行,保持输入顺序。
2. 输入字节、输出对象及Receiver存储不得重叠。Feed的consumed指针必须有效,length非0时data必须有效。
3. Feed每次最多交付一帧;未消费的输入尾部由调用方保留。FRAME_READY后未释放时再次Feed消费0字节。
4. Peek返回的apdu及apci.asdu借用Receiver内部缓冲。成功Release、Reset或Init后视图失效;持有视图期间不得搬移对象或修改原文。
5. 所有接口按枚举值判断。FRAME_READY说明APCI及ASDU最低长度检查通过,不说明具体对象、业务字段、N(S)/N(R)或连接启停状态合法。
6. 同一帧的I/S序号及ACK只交给同连接SendManager一次,不再另调底层Link重复提交。U帧由连接层按角色和功能码路由。
7. 返回WAIT时保留当前帧及接收顺序;已受理的QUEUED/IGNORED等结果按接口契约消费,不能重放。
8. 新连接复用对象前须结束旧I/O,再用未复用的非零代次Init;旧代次的输入、查询、释放和复位均被拒绝。
### 1.5 状态归属与数据生命周期
| 对象 | 所有者 | 内容及有效期 |
|---|---|---|
| `IEC104_Receiver_t` | 调用方,每连接一个实例 | 255字节缓冲、已收/预期长度、待帧标志及连接代次;跨调用保留半帧或完整帧 |
| `IEC104_LinkState_t` | 连接层,供SendManager和U帧处理共享 | 序号、窗口及启停状态;Receiver不持有副本、不修改它 |
| `IEC104_ReceivedFrame_t` | 调用方的输出对象 | 保存代次、APDU指针/长度及APCI结果;指针借用Receiver缓冲 |
| `IEC60870_AsduInfo_t` | 调用方 | 公共头字段及信息对象区视图,有效期不超过原ASDU |
| 构帧输出缓冲 | 调用方或SendBuffer | 保存已构造报文,由发送管理及传输层推进 |
Receiver不缓存指向自身缓冲的APCI指针;Feed完帧检查APCI,Peek重新解析到局部结果后一次性输出。因此移动对象不会留下内部悬空指针,但已经交付的借用视图仍要求原对象地址保持不变。
Init清零整个对象并绑定代次;Release和Reset只清除组帧进度,不擦除缓冲。即使物理字节仍在,释放后的旧视图也不可继续使用。
---
## 二、报文模型与 APCI(应用协议控制信息) 校验
### 2.1 APDU(应用协议数据单元) 布局
```text
偏移: 0 1 2 3 4 5 6 ...
+------+------+------+------+------+------+----------------+
| 0x68 | L | C0 | C1 | C2 | C3 | ASDU(可选) |
+------+------+------+------+------+------+----------------+
| APCI | ASDU |
| APDU |
完整 APDU 长度 = 2 + L
L = 4 字节控制域 + ASDU 长度
```
`IEC104_Parse_APCI()` 首先统一检查:
- `frame` 和输出参数 `apci` 非空;
- 总长度不少于 6 字节;
- 起始字节等于 `0x68`;
- `L` 位于 `4..253`;
- 调用方给出的 `frame_len` 严格等于 `2 + L`。
### 2.2 帧类型识别
`IEC104_Get_Frame_Type(C0)` 只按控制域首字节的低位分类:
| 分类条件 | 结果 |
|---|---|
| `(C0 & 0x01) == 0` | I 帧 |
| `(C0 & 0x03) == 0x01` | S 帧 |
| 其他 | U 帧候选 |
分类只是第一步。候选类型随后还必须通过对应的完整控制域校验,不能把任意“其他值”当作合法 U 帧。
### 2.3 三类控制域规则
| 类型 | 长度约束 | 控制域约束 | 解析结果 |
|---|---|---|---|
| I 帧 | `L >= IEC104_MIN_LENGTH_FIELD + IEC104_ASDU_HEADER_LEN`(当前为10) | `C0.bit0=0`、`C2.bit0=0` | `N(S)=(C1:C0)>>1`,`N(R)=(C3:C2)>>1`,ASDU 长度=`L-4` |
| S 帧 | `L == 4` | `C0=0x01`、`C1=0x00`、`C2.bit0=0` | `N(R)=(C3:C2)>>1`,无 ASDU |
| U 帧 | `L == 4` | `C0` 为六种允许功能码之一,`C1..C3=0` | 输出 U 功能码,无 ASDU |
允许的 U 帧功能码:
| 功能 | ACT | CON |
|---|---:|---:|
| STARTDT | `0x07` | `0x0B` |
| STOPDT | `0x13` | `0x23` |
| TESTFR | `0x43` | `0x83` |
> 当前模块只校验并报告 U 功能码,不改变数据传输启停状态,也不会自动生成对应 CON 帧。
### 2.4 I/S/U帧构造
`IEC104_Build_Iframe()`执行APCI解析的反向操作:调用方传入`N(S)`、`N(R)`和完整ASDU,函数将起始字节、长度域、四字节控制域及ASDU写入输出缓冲区,并返回完整APDU长度。
该函数是无状态编码接口:不读取或修改`IEC104_LinkState_t`,不检查发送窗口,也不调用网络发送函数。调用方必须提供至少`IEC104_APCI_LEN + asdu_len`字节的输出空间。实际连接通过下述链路 Prepare/Complete 配对管理序号,不能单独构帧后伪造一个完成快照。
`IEC104_Build_Sframe()`根据调用方提供的`N(R)`构造固定6字节S帧:
```text
encoded_receive = (uint16_t)(recv_seq << 1)
68 04 01 00 encoded_receive低字节 encoded_receive高字节
```
写入控制域前,`N(R)`先左移1位。`buf`为空或`N(R)>32767`时返回`-1`,成功时返回`IEC104_APCI_LEN`。
`IEC104_Build_Uframe()`接收`int function`,使用`IEC104_UFrameFunction_t`定义的六种功能码构造固定6字节U帧:
```text
68 04 功能码 00 00 00
```
功能码仅允许`STARTDT_ACT/CON`、`STOPDT_ACT/CON`和`TESTFR_ACT/CON`。`buf`为空或功能码非法时返回`-1`且不写入缓冲区,成功时返回`IEC104_APCI_LEN`。
链路模块内部已检查非空缓冲及至少 6 字节容量,并传入固定合法功能码,因此这些调用使用 `(void)IEC104_Build_Uframe(...)` 显式忽略固定返回值。编码仍执行,公开函数仍返回 `int`;构造成功只代表缓冲已写入,不代表网络发送成功。对外部传入的未验证参数仍须按返回值处理。
基础构造函数只生成字节数组,不决定何时发送,也不调用网络接口。连接发送 I 帧应使用 `IEC104_Link_Prepare_I_Frame()`,由它在构帧成功时预留 `N(S)`;完整发送后把原 `IEC104_IFrameTx_t` 快照和最终发送长度传给 `IEC104_Link_Complete_I_Send()`。普通 S 确认则使用 `IEC104_Link_Prepare_S_Ack()` 与 `IEC104_Link_Complete_S_Ack_Send()` 配对。S/U帧调用方必须提供至少6字节输出空间。
构帧参数与返回值汇总:
| 接口 | 输入边界 | 成功返回 | 失败返回 |
|---|---|---|---|
| `IEC104_Build_Iframe()` | `buf/asdu` 非空,`N(S)/N(R)` 为 `0..32767`,`asdu_len` 为 `6..249` | APDU 长度 `6 + asdu_len`,范围 `12..255` | `-1` |
| `IEC104_Build_Sframe()` | `buf` 非空,`N(R)` 为 `0..32767` | `6` | `-1` |
| `IEC104_Build_Uframe()` | `buf` 非空,`int function` 的原值等于六种U帧功能码之一 | `6` | `-1` |
I 帧的序号先左移一位,再按低字节、高字节顺序写入控制域;ASDU 使用 `memcpy()` 原样复制,不验证其公共头或信息对象语义。输入 ASDU 与输出报文应使用不重叠的存储区域。所有构造接口都没有输出容量参数,无法检查调用方实际分配的空间。
例如,`N(S)=1`、`N(R)=2`、ASDU 为 `01 01 03 00 01 00` 时,I 帧构造结果为:
```text
68 0A 02 00 04 00 01 01 03 00 01 00
```
其中 `02 00` 编码 `N(S)=1`,`04 00` 编码 `N(R)=2`。该例仅用于验证字节编码,六字节 ASDU 只有公共头,不代表包含完整业务对象。APCI解析与I帧构造的ASDU长度范围一致,均为`IEC104_ASDU_HEADER_LEN..IEC104_MAX_ASDU_LEN`,当前为`6..249`字节。
U帧编码入口及内部功能码校验函数均使用`int`参数,先按完整整数值检查六种合法功能码,再转换为`uint8_t`写入控制域。启用短枚举时也会拒绝`0x107`等非法值,避免误生成`STARTDT_ACT`。调用方应直接传入待校验整数或合法枚举常量;若提前转换为短枚举或`uint8_t`,已丢失的高位无法由编码函数恢复。公开参数类型已调整,调用方及依赖该签名的函数指针须使用新声明并重新编译。
---
## 三、流式接收状态机
### 3.1 状态定义
阶段由received_length及is_frame_ready推导,不另存一份stage枚举。
| 状态 | 不变量 | 输入处理 |
|---|---|---|
| 等待帧头 | received=0、expected=0、ready=0 | 首字节必须为0x68;否则消费该字节并返回FRAME_ERROR |
| 等待长度 | received=1、expected=0、ready=0 | 长度必须4..253;合法后expected=2+L、received=2 |
| 收集报文体 | 2≤received<expected≤255、ready=0 | 批量复制当前帧所需字节,不消费下一帧 |
| 完整帧待处理 | received=expected、ready=1 | Feed返回FRAME_READY且consumed=0,等待显式释放 |
### 3.2 完帧处理
Feed收齐APDU后调用Parse_APCI,I帧再调用Parse_ASDU_Header检查公共头及已知固定长度对象的最低载荷长度。任一步失败都清空当前进度并返回FRAME_ERROR;全部通过后才设置待帧标志并返回FRAME_READY,保持APDU原文。
Peek只在待帧状态下输出 `IEC104_ReceivedFrame_t`。它复用相同的格式及最低长度检查并重建APCI视图,失败或EMPTY时不修改调用方输出。接收器不调用Link、SendManager或业务代码,不提交任何序号或ACK。
### 3.3 错误恢复边界
- 每个APDU边界的首字节必须为0x68;遇到其他值立即消费该字节、清除进度并返回FRAME_ERROR,不扫描后续字节重新同步。Connection登记FRAME故障,TcpServer随后断开连接。
- 长度非法时消费该长度字节,丢弃当前候选帧,停止本次Feed;剩余输入不被消费。
- 完整APCI非法、ASDU公共头不足或已知固定长度对象被截短时,消费到声明的候选帧末尾,丢弃候选帧,停止本次Feed;不处理该帧的序号或累计ACK。
- 报文体按已声明长度收集,内部0x68属于数据;不会在半帧中搜索新帧头。半帧超时由连接层处理。
- 参数、代次或内部进度非法返回ERROR,不修改接收器;有效的consumed输出被置0。
- Receiver_Reset只核对初始化及代次,允许显式丢弃异常组帧进度。是否继续格式错误后的连接由接入方决定;链路FATAL不能靠复位接收器继续旧会话。
### 3.4 接收结果与调用方责任
| 结果 | 值 | 含义及处理 |
|---|---:|---|
| ERROR | -2 | 参数、代次或状态错误;consumed=0,修正调用或结束故障连接 |
| FRAME_ERROR | -1 | 起始字节、长度/APCI或ASDU最低长度错误;候选帧已清除,consumed标明停止位置 |
| IN_PROGRESS | 0 | 全部输入已消费,尚无完整帧;继续输入后续字节 |
| OK | 1 | Init、Reset、Release_Frame或Get_Status成功 |
| FRAME_READY | 2 | 有完整帧,通过APCI及ASDU最低长度检查;Peek、分派、受理后Release |
| EMPTY | 3 | Peek/Release时没有完整帧;半帧保留,查询输出不变 |
表中省略 `IEC104_RECEIVER_` 前缀,不能按布尔值判断。data=NULL、length=0是合法的Feed查询;其他情况下data必须指向length字节有效输入。
### 3.5 状态校验与复位
Receiver_Valid在读写前核对初始化、代次、待帧标志,以及帧头、长度域、expected_length和received_length的一致性。报文体复制长度取“本帧剩余长度”和“本次剩余输入”的较小值,因此不会将粘包中的后续帧写入当前缓冲。
| 操作 | 当前半帧/完整帧 | 连接代次 | Link状态 |
|---|---|---|---|
| Init | 全部清空并使旧视图失效 | 绑定非零参数 | 不操作 |
| Release_Frame | 有完整帧才清空进度;半帧返回EMPTY | 保留 | 不操作 |
| Reset | 丢弃当前进度和待处理帧,保留缓冲字节 | 必须匹配并保留 | 不操作 |
| Get_Status | 只读返回已收长度、期望长度及完整帧标志 | 必须匹配并保留 | 不操作 |
| Abort | 清空全部状态及缓冲,使旧视图失效 | 解除绑定,须重新Init | 不操作 |
| 旧代次调用 | 不修改 | 不修改 | 不操作 |
`Get_Status`复用完整进度校验,OK时才写数值快照,不重新解析或分派报文。Connection据此区分本次新成帧和此前保留的帧。`Abort`用于已结束旧I/O及所有原文借用后的收尾,允许NULL;它不核对代次,Connection在调用前负责检查身份,不能拿旧事件直接清理新接收器。
### 3.6 分段输入与待帧保留
调用方为当前网络输入保存offset;每次Feed后只增加consumed,不丢弃尾部。FRAME_READY时先Peek并分派,即使offset已经到输入末尾,也必须处理这帧。
如果分派暂时返回WAIT,保留当前Receiver帧及尚未消费的输入尾部,并暂停该连接的后续分派。条件满足后重试当前帧;一旦受理,Release后从原offset继续递入。禁止在待帧状态下仅重复Feed形成空转。
Receiver只提供一个待处理帧槽。未消费输入的容量和网络读取背压由连接层负责;无额外接收FIFO、动态分配或后台任务。
---
## 四、ASDU 公共头解析
### 4.1 当前实际调用关系
Receiver.Feed调用Parse_APCI校验帧格式,I帧还调用Parse_ASDU_Header检查最低载荷长度;Peek_Frame返回完整APDU、已解析APCI和ASDU视图。两次检查都在Link/Manager受理之前完成。ASDU公共头解析结果不缓存在接收器中,业务仍按需解析事件副本。
~~~text
Receiver.Feed → Parse_APCI → I帧调用Parse_ASDU_Header → FRAME_READY → Peek_Frame
→ 连接层:I/S交给SendManager,U交给对应控制接口
→ I帧受理后:TcpServer复制I_FRAME事件 → 经Connection调用Release_Frame
业务:TcpServer_NextEvent取得副本 → Parse_ASDU_Header → Type ID业务处理
~~~
Parse_APCI要求I帧ASDU长度至少为IEC104_ASDU_HEADER_LEN,当前为6字节;接收器随后通过Parse_ASDU_Header检查已知固定长度类型的载荷。任一步失败都报告FRAME_ERROR,不进入链路序号及累计ACK处理,也不交付I_FRAME。apci.asdu指向APDU偏移6,asdu_len为长度域减4。业务仍须检查具体字段和对象内容;业务检查失败不能回滚已经由链路接受的传输序号。Parse_APCI也用于发送缓冲,仍只承担APCI及公共头最小空间检查;发送构帧不调用ASDU载荷检查。
### 4.2 可配置公共头与104默认布局
公共头解析现在必须显式传入`IEC60870_AppLayerParameters_t`,不保留原三参数签名。COT和CA分别支持1/2字节,IOA支持1/2/3字节,头长为`2+sizeOfCOT+sizeOfCA`。以下图表描述本工程Receiver显式使用的104标准配置(COT/CA/IOA=2/2/3、最大249);通用ASDU解析也可使用其他合法配置。
```text
偏移: 0 1 2 3 4 5 6 ... 末尾
+------+------+------+------+------+------+----------------+
| TI | VSQ | COT | ORG | CA | 信息对象区 |
+------+------+------+------+------+------+----------------+
| ASDU |
```
| ASDU 偏移 | 字段 | 当前解析规则 |
|---:|---|---|
| 0 | Type ID | 原值输出;已知固定长度类型参与最低载荷检查,未知类型不因此被拒绝 |
| 1 | VSQ | bit7 → `is_sequence`;bit0..6 → `object_count` |
| 2 | COT | bit7 → `is_test`;bit6 → `is_negative_confirm`;bit0..5 →传送原因 |
| 3 | ORG | 源发地址原值输出 |
| 4..5 | CA | 小端组合为 16 位公共地址 |
| 6..末尾 | 信息对象区 | 输出首地址视图和剩余长度 |
函数接受所选头长至`maxSizeOfASDU`字节的ASDU(通用上限254),并在写输出前检查已知固定长度对象所需的最低载荷长度。COT为1字节时没有OA,结果OA为0;CA从偏移`2+sizeOfCOT`开始,信息对象区从所选头长开始。参数或长度失败返回0,输出保持不变。它不判断是否支持该Type ID,也不验证传送原因、公共地址或信息对象内容是否合法。参数与原报文均为借用,视图使用期间必须有效且不变。104传输层仍只接受标准配置及6..249字节ASDU。
`is_sequence`、`is_negative_confirm`、`is_test` 输出为 `0/1`,不是保留原掩码值。SQ参与最低长度计算,但不会自动展开连续信息对象地址。信息对象区长度为零时,其指针位于公共头之后,不得解引用。
### 4.3 已知类型的最低载荷长度
设IOA宽度为L=`parameters->sizeOfIOA`(104接收器传入3)、VSQ对象数量为N、单个对象除IOA以外的数据长度为D,公共头后的最低载荷为:
- SQ=0:`N × (L + D)`;
- SQ=1:`L + N × D`。
固定长度表覆盖Type ID `1..21`、`30..41`、`45..51`、`58..64`、`70`、`100..107`、`110..113`、`120..124`和`126..127`。例如104配置下单点命令45、N=1时,6字节公共头后至少还须有3字节IOA和1字节命令;只有公共头的声明长度在链路受理前被拒绝。读命令102的数据部分为零字节,但仍按配置检查IOA是否完整。
这是最低长度检查:未知/变长类型(包括文件段125)及N=0不通过该表限制;额外尾字节、某类型是否允许SQ或多个对象、字段取值和业务支持范围继续由业务检查。长度字段仍决定APDU边界,不根据后续字节猜测或补齐被截短的命令。
ASDU长度检查接入Receiver,保证格式错误在序号和累计ACK受理之前被发现。
---
## 五、与链路模块的接口边界
Receiver与Link独立,连接层持有一份LinkState供发送和接收共同使用,不存在接收器私有序号状态。链路必须显式初始化角色并通过既有启停流程推进;接收器不会将其预设为STARTED。
| 帧类型 | 子站连接层的处理位置 | 释放接收帧的时机 |
|---|---|---|
| I | `SendManager_Handle_I_S_Frame` | 成功受理且TcpServer已复制I_FRAME事件后,由Connection释放 |
| S | `SendManager_Handle_I_S_Frame` | 成功受理后,由Connection释放 |
| STARTDT_ACT / STOPDT_ACT / TESTFR_ACT | 对应Manager入口 | 根据各接口返回值确认已受理后 |
| TESTFR_CON | `Link_Handle_Testfr_Con` | 当前事件已处理或按契约忽略后 |
| 角色不接受的U功能 | 连接层按角色策略处理 | 处理完成或连接终止 |
控制入口WAIT表示尚未受理,必须保留当前帧;成功受理后释放控制帧。对端ACK必须晚于对应本地发送完成的提交;Receiver仅保留接收顺序,不代替发送完成记账。
旧 `IEC104_Legacy_Link_Reset`及其私有Link已恢复,该接口只复位旧入口的I/S状态并恢复STARTED预设,不改变旧接收进度。独立Receiver的接收复位调用Receiver_Reset;指定连接的Link初始化/复位继续使用Link公共接口。链路状态、超时及错误收尾约定见 [链路设计](IEC104_Link_设计架构.md)。
---
## 六、公开接口契约
104帧接口位于 `IEC104_Codec.h/.c`,公共ASDU头与参数检查位于 `IEC60870_Codec.h/.c`,独立接收接口位于 `IEC104_Receiver.h/.c`。旧 `IEC104.h/.c` 提供原有逐字节接口;下表合并列出两个Codec和Receiver。
| 接口 | 状态性 | 成功结果 | 主要注意事项 |
|---|---|---|---|
| `IEC104_Get_Frame_Type(control)` | 无状态 | 返回 I/S/U 候选类型 | 只分类,不代表控制域合法 |
| `IEC104_Parse_APCI(frame,frame_len,apci)` | 无状态 | 返回 `1`,填充类型、序号、U 功能或 ASDU 视图 | 失败返回 `0`;基础参数检查失败不写输出,分类后的控制域检查失败可能已改写部分输出;不校验链路序号和 ASDU 语义 |
| `IEC104_Build_Iframe(buf,send_seq,recv_seq,asdu,asdu_len)` | 无状态 | 写入完整 I 帧,返回 `12..255` | 参数非法返回 `-1`;调用方保证输出空间;不修改链路状态 |
| `IEC104_Build_Sframe(buf,recv_seq)` | 无状态 | 写入固定 6 字节 S 帧,返回 `6` | 参数非法返回 `-1`;`N(R)` 范围为 `0..32767`;不标记接收已确认 |
| `IEC104_Build_Uframe(buf,function)` | 无状态 | 写入固定 6 字节 U 帧,返回 `6` | 按`int`原值校验功能码,失败返回`-1`且不写入缓冲区;不执行状态转换或自动应答 |
| `IEC60870_AppLayerParameters_Valid(parameters)` | 无状态 | 合法参数返回 `1` | NULL、非法字段宽度/OA/最大长度返回 `0` |
| `IEC60870_Parse_ASDU_Header(parameters,asdu,asdu_len,asdu_info)` | 无状态 | 返回 `1`,填充公共头、信息对象区及参数借用视图 | 参数或最低载荷长度检查失败返回 `0`,不修改输出;长度限制由配置决定 |
| `IEC104_Receiver_Init(receiver,generation)` | 初始化指定实例 | OK | 非零代次;新连接复用前先结束旧I/O |
| `IEC104_Receiver_Feed(receiver,generation,data,length,consumed)` | 更新指定实例 | FRAME_READY或IN_PROGRESS | 每次最多一帧;所有返回都按consumed推进输入;ERROR消费0 |
| `IEC104_Receiver_Peek_Frame(receiver,generation,frame)` | 只读 | FRAME_READY | 输出借用视图;EMPTY/ERROR不改输出 |
| `IEC104_Receiver_Release_Frame(receiver,generation)` | 释放待处理帧 | OK | 无完整帧返回EMPTY,保留半帧 |
| `IEC104_Receiver_Reset(receiver,generation)` | 丢弃接收进度 | OK | 保留代次,不操作Link;所有旧视图失效 |
| `IEC104_Receiver_Get_Status(receiver,generation,status)` | 只读 | OK | 数值快照不借用原文;参数、旧代次或进度非法返回ERROR且不改输出 |
| `IEC104_Receiver_Abort(receiver)` | 清除状态及绑定 | 无返回值 | 先结束I/O和借用;允许NULL,再使用须Init |
`IEC104_Link.c` 自身的公开接口不属于本文接口定义,详见 `IEC104_Link_设计架构.md`。
---
## 七、设计决策与考量
### 7.1 分段输入与显式释放
Feed兼容单字节和批量输入,报文体批量复制,每次停在一帧边界。显式Release将报文有效期与下一次输入分开,便于连接层处理控制队列满等暂时无法受理的情况,并保持同连接的严格分派顺序。
### 7.2 固定缓冲、零动态分配
每个Receiver内置255字节APDU缓冲,另有少量长度、代次和标志字段。无堆分配、接收FIFO或独立ASDU副本。Peek输出只读借用视图;需要长期保存的调用方自行复制数据。
### 7.3 先检查、后更新
参数、代次和内部进度在处理输入前检查,ERROR不修改接收器。FRAME_ERROR属于已经消费的输入格式错误,会丢弃候选帧并报告消费位置。Peek在局部结果完成APCI解析后才写调用方输出。Receiver不提前或重复提交Link状态。
### 7.4 接收复位与链路复位分离
Reset只丢弃指定Receiver的接收进度并保留连接代次;Link序号、启停和发送记录由各模块独立管理。重建连接用新代次Init,且必须先结束旧I/O。实例初始化不使链路进入STARTED。
### 7.5 日志独立
Receiver和Codec不依赖日志模块。连接层根据返回值决定日志级别和断连策略;现有 `IEC104_Log.c/h`可按应用需要继续使用,无需随接收器一并链接。
### 7.6 旧接口迁移
`IEC104.c/h`、`IEC104_Rx_Feed_Byte`、`IEC104_Rx_Reset`、`IEC104_Legacy_Link_Reset`及旧接收枚举/结构已从删除前备份原样恢复。旧入口仍编译IEC104.c、Codec.c、Link.c和Log.c;独立Receiver需要Receiver.c、IEC104_Codec.c和IEC60870_Codec.c。恢复旧文件不改变新接收器或发送模块。
内部接收实现迁移到独立Receiver时,由Connection持有Receiver并处理consumed、Peek及Release,TcpServer负责保留输入尾部和复制业务事件。应用的子站接入统一使用IEC104_Slave.h,门面内部使用TcpServer.h;只处理公共ASDU头的业务包含IEC60870_Codec.h,104帧工具包含IEC104_Codec.h。旧入口在成帧成功前包含I/S链路检查;新路径由Connection交给SendManager执行,不能仅重命名函数后继续假设序号已被校验。
接口过期的旧测试入口已删除;当前测试直接使用现有TcpServer实现,不提供测试兼容层。
## 八、变更记录
| 日期 | 变更 |
|---|---|
| 2026-09-03 | 初始版本:依据当前源码建立成帧、APCI/ASDU 解析、链路序号、接口契约、测试基线和已知边界设计说明 |
| 2026-09-03 | 拆分 `IEC104_Link.c` 设计:本文只保留调用边界和全局状态集成限制,链路状态、算法及测试迁移至独立文档 |
| 2026-09-03 | 更正 ASDU 调用关系:明确当前接收链路只调用 APCI 解析,`IEC60870_Parse_ASDU_Header()` 尚未接入且仅由测试直接调用 |
| 2026-09-04 | 增加I、S、U帧无状态构造接口及其参数、输出格式和职责边界说明 |
| 2026-09-07 | 链路模块增加普通S帧的 `w` 门限、立即确认、N(R)快照和发送完成接口;全局接收器与发送调度仍未接入该流程 |
| 2026-09-08 | 同步固定连接角色模型:全局旧入口默认被控站;连接故障只由外部 TCP 上下文管理,不在链路状态重复锁存;详细状态与接口契约见链路设计文档 |
| 2026-09-10 | 同步IEC104_Legacy_Link_Reset命名及两类复位边界,纠正I/S链路结果类型,补充U帧编码返回约定与当前测试状态;区分已有算法和待实现的连接调度 |
| 2026-09-17 | 将无状态编解码、协议常量和报文类型提取到IEC104_Codec.c/h;旧单路接收入口及其行为保留,IEC104.h继续提供原有声明,生产构建新增Codec源文件 |
## 九、当前验证与使用边界
本文描述当前源码行为,不等于已经完成端到端协议验证。当前保留的字节环专项、编译命令和覆盖范围集中维护在[测试说明](../test/IEC104测试文档.md);旧回归套件已删除,其历史通过数不计入当前验证。
链路层已提供 `sent_vs`、不可变 I 帧快照、固定角色启停、按受理顺序立即启停和独立 TESTFR。独立 `IEC104_SendBuffer` 已提供 唯一当前I帧、独立紧凑S/U FIFO、部分写入及本地发送期限;`IEC104_AsduQueue` 和 `IEC104_SendManager` 已提供变长业务排队、发送前编号、未ACK原文保留、I/S接收、完成分派、独立回执、I帧t1及显式重连恢复。Receiver已提供独立接收上下文、完整APDU交付和代次隔离;Connection已统一收发事件分派、U帧路由及协议期限,Windows TcpServer已提供实际I/O,并用Poll周期调用Tick,无须业务另建协议定时器。基础编码器只校验可见参数,U枚举截断边界见2.4节,不能宣称已经全面校验所有整数输入。
## 十、集成设计与接入边界
### 10.1 当前能力与职责分配
Receiver已实现每连接成帧和完整APDU交付;独立发送队列、业务队列及发送调度接口见 [发送模块统一设计](IEC104_发送模块_设计架构.md)。当前Windows子站接入由Connection、TcpServer和Net_Windows组成,业务持续Poll、通过NextEvent主动读取事件并按需排入ASDU;具体业务对象仍由上层实现。
| 能力 | 当前负责层 | 与现有实现的衔接 |
|---|---|---|
| TCP建连、读写、断连和重连 | TcpServer(已实现) | 将recv字节交给Connection;回报send实际写入量;断线后保留监听并接受新主站 |
| 每连接独立状态、统一错误收尾 | Connection(已实现) | 持有接收器、链路状态、发送模块及配置,统一串行调度 |
| 完整帧的有序分派 | Connection(已实现) | 使用Receiver.Feed/Peek/Release;I帧由TcpServer复制为事件后释放,S/U受理后释放,WAIT保留 |
| STARTDT/STOPDT/TESTFR事件分派 | Connection(已实现) | 按子站角色路由U功能码,由Manager统一提交Link修改;Connection仅作只读Link查询 |
| `t1/t2/t3`、`k`和`w`调用时机 | Connection + TcpServer(已实现) | Poll使用单调时间推进Tick,执行确认、探测和超时收尾;被动监听不发起TCP连接,未实现主站`t0`建连流程 |
| I/S/U实际发送 | TcpServer + Connection(已实现) | Prepare取得待发字节,send后立即回报增量;部分发送下轮续传,业务不再直接调用底层完成接口 |
| 遥信、遥测、遥控、总召、对时等 | ASDU业务层(待接入) | 从NextEvent取出I_FRAME副本后解析公共头及业务对象,携带事件generation调用TcpServer_Send_Response提交应答 |
### 10.2 统一连接上下文
独立Receiver由Connection持有,接收和发送使用同一份LinkState;已恢复的旧入口仍保留原有接收/Link私有全局对象,不参与TcpServer路径。当前归属如下:
```mermaid
flowchart TD
App[业务层] -->|Poll / NextEvent / Enqueue| Transport
Transport["IEC104_TcpServer:监听 / 收发 / 断连事件"] --> Context["IEC104_Connection:统一串行调度"]
Transport --> Net[Net网络后端]
Transport -->|NextEvent返回事件副本| App
App --> Asdu[ASDU公共头与业务对象处理]
Context --> Rx["接收器:IEC104_Receiver_t"]
Context --> Link["共享序号:IEC104_LinkState_t"]
Context --> Manager[SendManager发送管理]
Manager --> Link
Manager --> Tx[SendBuffer报文与写入进度]
Manager --> Queue[AsduQueue原文队列]
```
Receiver各入口已经显式接收实例和连接代次;Connection按序调用SendManager及控制接口,统一处理错误收尾。TcpServer为每次接受的主站分配新代次,旧单路入口及其私有Link不参与此路径。
Receiver的内部Peek视图持续有效到Release、Reset或重新Init。TcpServer先复制完整APDU到事件FIFO,再经Connection释放内部帧;业务通过NextEvent取得无借用指针的独立副本,可以跨下一轮Poll或断线保留,无需调用底层Release。普通通知满仍可处理排在前面的S/U;遇到无处交付的I帧时保留原帧、暂停续收,NextEvent腾出空间后由Process恢复。启停和收尾分别预留容量;启停变化逐条保留,启停槽满时延后受理改变状态的ACT,消费对应通知后重试。通知积压不主动断连,但原有协议期限继续生效;持续积压的I帧会挡住后续S/U,等待启停槽会延迟ACT受理及其应答,应用必须及时消费事件。Close后仍能取出全部请求、回执和连接收尾事件。同连接业务需要应答时,携带事件generation通过TcpServer_Send_Response提交ASDU;通过启停准入后,条件允许时立即尝试一次非阻塞发送,暂不能发送时缓存HIGH。主动上报通过TcpServer_Enqueue_Event缓存,不要求存在连接,帧头、序号及实际socket写入由协议和TCP层完成。
### 10.3 收发与状态提交顺序
以下说明底层顺序。SendManager.Prepare_Next_Send在实际发送前选择并准备当前帧,Handle_Send_Progress执行完整发送后的Complete/Commit_Send_Completion,Handle_I_S_Frame执行I/S校验、记录ACK并释放对应ASDU原文;调用方不再重复执行其中的底层步骤。S/TESTFR及子站启停构帧入队使用Manager封装,TESTFR_CON也经Manager.Handle_Testfr_Con提交,只有受理成功才由Connection撤销探测t1。Connection安排调用时机及U帧路由,TcpServer执行I/O。Manager仅绑定CONTROLLED链路,不参与主站启停流程。
Connection的Link/Buffer运行期修改入口统一为Manager;初始化经Manager.Init_Connection预检后直接绑定最终地址,失败保留原对象和队列。发送期限及队首身份由Manager.Get_Deadlines提供,Connection以临时Collect_Deadlines视图统一超时检查、发送准入、控制推进和等待计算,原始帧时间戳仍保存在所属模块。运行期参数、身份、序号及模块计数校验继续保留。
接收流程为:严格检查帧头并按长度成帧 → APCI校验 → I帧公共头及已知类型最低载荷检查 → 连接控制状态与I/S序号校验 → 提交链路接收状态 → 按帧类型分派。成帧及最低长度失败统一登记FRAME;I帧通过后仍由业务检查对象内容,S帧进入发送确认处理,U帧进入启停或心跳控制。后续ASDU业务校验失败应单独报告,不能自行回滚已经接受的传输序号。
发送侧必须区分“已构造并预留”“已排队”“完整写入传输层”“对端已确认”四个阶段。`IEC104_Link_Prepare_I_Frame()` 成功构帧时立即分配 `N(S)`、保存 `N(R)` 快照并推进 `V(S)`,使预编码和排队中的帧也占用 k 窗口;`V(A)` 仍只能由对端合法 `N(R)` 推进。
子站连接层在准备实际写socket时调用 `IEC104_SendManager_Prepare_Next_Send()`。已有当前I帧时续传,否则先发控制FIFO;两者均空才从ASDU字节环选择未编号业务。ASDU头和原文均可跨数组尾部,队列提供最多两段视图;Manager在原文跨界时先用249字节局部数组拼接,再交给Link,传输层仍取得连续APDU。Manager保障记录和当前APDU存储后调用Link.Prepare,让SendBuffer接手完整I帧并建立Manager记录,再通过Mark_Submitted完成队列交接记账:HIGH消费原文,NORMAL登记为保留原文待ACK。队列记账前仍保留待发旧状态,该状态不能用来判断发送缓冲是否已经接手。最多一帧已编号I帧等待本地完成,其余业务仍可按优先级选择。S/U使用独立6字节报文槽;当前帧不可被截断交错。Prepare成功后的序号不可撤回,后续提交或发送失败须结束连接。
应答入口经Manager的共享准入函数,仅在STARTED+IDLE时接手;其他启停阶段在记录容量、k窗口及HIGH容量检查前返回ERROR且不入HIGH、不因该拒绝断连。普通路径先由Connection.Drive处理保留接收和控制义务,再调用SendManager.Submit_Response检查实际接手时的状态;期限已到时直接走SendManager.Enqueue(HIGH),同样受准入限制。准入后,没有当前帧和旧HIGH且发送门控、窗口、记录容量、公平配额允许时直接准备一帧,再由TcpServer尝试一次非阻塞写入;暂不能准备或协议期限已到时先缓存HIGH。直接路径与队列路径共用Prepare_I_Asdu、SendBuffer及HIGH发送记录,仅跳过ASDU入队和Mark_Submitted;不会重复实现序号、完成或ACK逻辑。部分写入或暂不可写保留当前帧,由Process/Poll续发。公开OK只表示已接手原文,FATAL可能发生在实际写入之后。
I帧完成接口要求快照的 `N(S)==sent_vs`,只推进本地完成边界和实际捎带确认位置。Manager提交完成记录后立即释放当前I帧APDU;HIGH原文在I帧提交并建立发送记录时已出队,NORMAL原文保留到ACK或被后续入队覆盖,发送记录独立保留。对端ACK限制在 `va..sent_vs` 内;Link接受后Manager先登记所有新增ACK事实,核减high_unacked_count,再仅对NORMAL按记录身份释放原文或核减覆盖待确认计数,不能删除后来入队的新条目。原文出队或覆盖不推进ACK、不释放k窗口或回执槽。SendBuffer不保存确认边界,不再提供Apply_Peer_Ack。TcpServer把就绪回执复制为RECORD事件后,经Connection移除内部记录,业务通过NextEvent读取副本。Manager按最早已完成未确认记录的原始写完时刻判断t1;异步传输适配必须先提交对应发送结果,再分派相关接收事件。
发送准备和写入回报保留真实单调时间,仅因本地发送及STOP期限直接报告超时;对端ACK/TESTFR确认超时故障由接收阶段判定。旧确认t1已到时,Connection暂停为HIGH/NORMAL创建、编号新I帧;已有帧及控制发送仍可推进,以解除接收阻塞。有效确认解除所有已到期等待后自动恢复准入,不改变任何期限起点。快速应答或常规发送跨过确认期限后安排立即Process,先按有界输入快照处理确认,再按原始t1检查;Prepare报告零等待。不增加恢复期限或固定宽限时间,事件容量背压下仍按既有期限处理。
断线时TcpServer先复制剩余记录和断开通知,再由Connection_Close调用Manager.Abort;服务队列继续存在。TcpServer在关闭旧Connection后调用Detach:清空HIGH,将NORMAL现存原文恢复未提交,清除旧覆盖记账并切换到代次0;离线期间继续缓存NORMAL。新连接建立时Rebind到新代次,再Init连接。业务只使用公开提交及事件接口,不直接操作队列。HIGH固定用于当前连接的请求应答,NORMAL用于业务允许跨连接恢复的事件,业务分别调用Send_Response和Enqueue_Event;不提供通用入队兼容入口或重连保留开关,Close服务才清空NORMAL。已覆盖原文无法恢复,新连接从新序号开始,不能复用旧APDU。同一TCP上的启停不执行Rebind。Manager初始化成功、受理新STOPDT_ACT及每轮首次生成STARTDT_CON时清空待发HIGH;已启用时重复START保留HIGH及公平配额,WAIT尚未受理不清理;STOP成功受理立即清理HIGH。STOP受理时仍在HIGH的应答即使已返回OK也按约定作废,不产生逐条回执;已提交的当前帧和发送记录不受清理影响,继续完成及等待ACK。
需要纳入链路状态管理的 I/S 帧不能只调用基础 `IEC104_Build_*` 构造函数:I 帧必须通过 `IEC104_Link_Prepare_I_Frame()` 与 `IEC104_Link_Complete_I_Send()` 配对,普通 S 帧必须通过 `IEC104_Link_Prepare_S_Ack()` 与 `IEC104_Link_Complete_S_Ack_Send()` 配对。两个完成接口内部只把 `acked_nr` 推进到对应报文实际携带的位置,因此排队后新收到的 I 帧仍保留在 `acked_nr..V(R)`。同一连接仍须按上线顺序串行提交;较新的确认已经发送后,旧 S 帧不得再上线。
被控站受理START时立即激活,受理STOP时立即停用并等待确认;相反ACT不暂存。后到START结束尚未生成CON的停止等待,已有CON按FIFO写出,完成不回写状态。S确认覆盖接收数据且I帧全部获ACK时可排STOP_CON;Connection最多依次推进S与CON两项。STOP总期限从received_at_ms起算,按本轮最后一帧STOP_CON身份结束,成功受理START也取消旧计时。状态事件由Connection内部适配器在ACT受理时交给TcpServer,通知容量不足则WAIT保留原请求。发送完成仍准确记录字节、序号及ACK事实,见[发送缓冲设计](IEC104_发送模块_设计架构.md#sendbuffer)。
### 10.4 建议实施顺序与验收点
| 顺序 | 实施内容 | 可检查的完成条件 |
|---:|---|---|
| 1(已实现接收基础) | Receiver独立实例、完整帧视图、消费数和显式释放;Link由连接层共享 | 两个接收实例可交错输入;待处理帧不被后续Feed覆盖,旧代次调用被拒绝 |
| 2 | 接入发送前准备、部分写入续传及独立确认记录,补齐实际I/O和收发串行调度 | 发出 `N(S)=0` 后收到 `N(R)=1` 能正确确认并释放原ASDU;发送失败不重复推进状态,旧确认不清除新增待确认数据 |
| 3 | 连接生命周期、U 控制、定时器及窗口策略 | 受控传输与时间事件下可验证启动、停止、心跳、超时和窗口边界;序号错误停止当前批次并关闭连接 |
| 4(对象编解码已实现) | IEC60870_Asdu与InformationObjects提供监视/命令对象编解码和应答构造;继续接入业务分派、点表与执行流程 | 已有精确长度、品质/时标、容量与原文应答测试;地址匹配、总召进度、校时及遥控业务验收仍由后续实现补齐 |
---
@@ -0,0 +1,420 @@
# 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/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不重复通知,直接断开不补造停用事件。业务可按事件类型处理启停,无需通过查询状态比较变化。
发送队列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站点表 |
| `IEC104_Interrogation.c/h` | 共用召唤引擎:任务、广播、期限、事件、取消与发送等待;接入见[总召流程](IEC104_完整调用流程.md#flow-interrogation) |
| `IEC104_GeneralInterrogation.c/h` | Type100规则及兼容的Interrogation_Create入口 |
| `IEC104_CounterInterrogation.c/h` | Type101规则、创建及一次动作执行入口;见[计数量设计](IEC104_CounterInterrogation_设计架构.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及其依赖之外加入`IEC104_Interrogation.c`和`IEC104_GeneralInterrogation.c`,共用Net单调时钟;应用按QOI选点表,响应NEED_DATA提交数据 |
| 104计数量召唤 | 引擎加`IEC104_CounterInterrogation.c`;与总召分别创建,状态通知交付两边,动作通过Execute_Begin执行 |
| 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处理带时标信息对象和命令,实际事件采集及校时业务流程仍由应用接入。**业务发送入口负责使用本地标准时间日历,并显式设置SU=0。** 公共编解码继续支持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、分组及遍历状态;其他命令的选择/执行、校时权限及业务任务由应用接入。
| 层次 | 职责 |
|---|---|
| `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/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)。
## 启停主流程
| 角色与过程 | 调用顺序 |
|---|---|
| 主站启动 | `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)。
@@ -0,0 +1,142 @@
# 104子站示例
[iec104_slave_example.c](../../examples/iec104_slave_example.c) 包含模拟点表、业务事件处理、其他命令兜底、事件循环及main。总召状态机使用独立库模块[IEC104_Interrogation.c/h](../../104/IEC104_Interrogation.h),不在示例内重复实现。
当前实现站总召(`C_IC_NA_1`,QOI=20),返回8个单点遥信和8个标度化遥测值;分组1(QOI=21)只返回遥信,分组2(QOI=22)只返回遥测。库支持全部16组,示例未配置的其余组由应用拒绝。通信通过 `IEC104_Slave`,总召通过 `IEC104_Interrogation` 接入,所有任务使用固定存储、在主循环线程串行推进。模拟点表是只读的;变化上报、实际设备取值、校时及遥控可在这个入口继续接入。
ExampleApp持有一个零初始化的Station节点。Example_Init按`--ca`注册CA及应用context,再以`config.use_station_registry=true`创建总召。命令行仍只注册一个站,状态区参数传NULL/0;扩展多CA时须提供broadcast_states/broadcast_capacity,广播先确认所有站,再逐站数据,最后终止所有站。BEGIN只判断接受;首个NEED_DATA才按station_id查原节点并Acquire,建立唯一取数游标。Complete_Data返回OK后释放游标及引用,异常时按FINISHED清理。处理中删除/同CA重建不改变旧数据源。节点须等Is_Reusable为true后释放,见[共用站点表](../104/IEC104_API接口汇总.md#station-registry)。
例如最多一轮覆盖13站时,由应用提供以下存储并在Create前写入配置(或传给示例内部Example_Init的后两个参数):
```c
static IEC104_InterrogationStationState_t gi_stations[13];
/* config已经通过IEC104_Interrogation_Config_Default初始化。 */
config.use_station_registry = true;
config.broadcast_states = gi_stations;
config.broadcast_capacity = sizeof(gi_stations) / sizeof(gi_stations[0]);
```
数组借用至Destroy,期间不可读写或重叠使用;它保存身份、进度和终结状态,不保存点值。注册表不限制为13站,但若本轮目标数超过状态区容量,会在任何肯定确认前整轮否定。单目标不需要该状态区。
## 构建与运行
命令行示例还创建独立的Type101计数量实例。IOA3001..3004为第1组,初值120000、345600、7890、0;IOA3005..3008为第2组,初值1、65535、2147483647、42。RQT=5读取全部,1/2读取对应组,3/4拒绝。每批4个Type15对象、SQ=0;读取开始固定冻结快照,WAIT不混入新版本。
FRZ=1冻结并将序号模32加一;FRZ=2另将当前值清零;FRZ=3仅清当前值,冻结值和序号不变。动作通过Execute_Begin执行一次;TEST动作拒绝。FRZ=0读冻结值并发送ACT_TERM;FRZ=1..3只回ACT_CON后结束。CY/CA/IV在模拟点表中为0,不模拟真实计量品质。STOP统计原字段保持总召含义,末尾增加counter_submitted和counter_completed。
主循环向两实例广播生命周期通知,交替优先推进业务,其他类型在两者均未接手后兜底。原Example_Init仍可用于仅总召测试,Example_Init_Counters补充计数量实例,两个实例各需独立广播状态区。协议及应用动作限制见[计数量设计](../104/IEC104_CounterInterrogation_设计架构.md)。
在项目根目录的 PowerShell 中执行,需要 MinGW GCC:
示例通过 `IEC104_Interrogation.h` 引入库声明;构建脚本同时添加 `104/` 和 `common/` 头文件搜索路径,编辑器也应配置这两个目录。构建可执行程序仍使用下面的脚本,它会链接所需库源文件及 Windows 网络库;只编译当前 C 文件会缺少库函数实现。编辑器的 C 语言标准应与构建保持一致,使用 C11,并选择实际的 MinGW GCC 编译器。
```powershell
./scripts/build.ps1
./.build/slave-example/iec104_slave_example.exe
```
默认监听 `127.0.0.1:2404`,公共地址 CA=1;按 Ctrl+C 退出并关闭服务。也可以指定监听地址、端口和公共地址:
```powershell
./.build/slave-example/iec104_slave_example.exe --bind 0.0.0.0 --port 2404 --ca 1
```
| 参数 | 含义 |
|---|---|
| `--bind IPv4` | 数字IPv4监听地址,默认127.0.0.1 |
| `--port 0..65535` | 默认2404;0由系统分配,启动时输出实际端口 |
| `--ca 1..65534` | 本站公共地址,默认1;总召请求也接受65535,广播请求的应答使用本站地址 |
| `--window 1..32767` | 发送窗口k,默认12;设为1可观察逐帧ACK后续发 |
| `--run-ms N` | 运行约N毫秒后正常关闭;默认0表示持续运行 |
| `--help` | 打印用法后退出 |
启动输出 `LISTEN 地址:端口 CA=站地址`。退出输出已提交ASDU数、已提交完毕的总召数、WAIT次数、拒绝数、取消任务数、积压超限次数、主站撤销数及业务超时数。`submitted` 和 `gi_submitted` 表示报文已交给发送模块,不表示主站已经ACK。
## 主站操作和模拟点表
主站先建立TCP并发送 `STARTDT_ACT`,随后发送总召:Type=100、SQ=0、对象数=1、COT=6、P/N=0、CA匹配、IOA=0,QOI=20选择全站,21/22选择本例的两个分组。TEST请求同样只读取模拟点表,应答保留TEST和OA。
| IOA | 类型 | 模拟值(按地址顺序) |
|---|---|---|
| 1001..1008 | 单点信息 `M_SP_NA_1`,Type=1 | 0、1、0、1、1、0、1、0 |
| 2001..2008 | 标度化测量值 `M_ME_NB_1`,Type=11 | 2300、2310、2290、5000、-125、0、1234、42 |
所有品质为0。遥测值为原始有符号16位标度值,本示例没有定义物理单位和倍率。
一次成功站总召依次提交6个ASDU:
1. Type=100,COT=7,肯定激活确认。
2. 两帧Type=1、COT=20的单点数据,每帧4个对象。
3. 两帧Type=11、COT=20的标度化遥测,每帧4个对象。
4. Type=100,COT=10,激活终止。
分组总召各提交4个ASDU:ACT_CON、两批对应类型数据、ACT_TERM。分组1数据为Type=1/COT=21,分组2数据为Type=11/COT=22;确认和终止保留请求QOI。BEGIN校验分组,首个NEED_DATA按该组初始化应用游标,不把模拟点表写进库。
全部应答使用原请求的 `connection`。确认、终止及短应答走普通HIGH路径,总召数据由Submit_Batch尝试不排队提交;有未提交HIGH/NORMAL、当前帧或窗口背压时返回WAIT。数据不使用 `Enqueue_Event`,只有本批接手后才推进游标及后续终止。
主站撤销使用相同的实际站CA/OA/TEST/QOI,COT改为8、P/N=0。激活和撤销均接受本站地址或65535,匹配时65535映射为本站地址,允许广播激活配本站撤销及本站激活配广播撤销。尚有匹配总召时模块停止任务、回复肯定COT=9,并通过FINISHED(DEACTIVATED)通知应用;不匹配或已完成则否定COT=9。已提交发送层的帧可能仍在确认前发出,撤销后不再提交旧批次和ACT_TERM。示例点数很少,总召可能在主站发撤销前已全部提交;观察中途撤销和各阶段拥塞可使用下面的可控网络测试。
广播总召的BEGIN/NEED_DATA/FINISHED携带本站CA,示例直接用该字段构造数据。确认、数据、终止、否定及错误应答的CA都为本站地址,OA/TEST/QOI保留原请求值;原始Slave事件不被修改。该支持属于Type100总召处理器,其他类型仍按各自业务规则分派。
## 请求校验及任务处理
| 情况 | 本示例的处理 |
|---|---|
| 不支持的Type ID | 回显原文,COT=44、P/N=1 |
| 总召COT不是6或8 | COT=45、P/N=1 |
| 总召CA既不是本站地址也不是65535 | COT=46、P/N=1,保留原错误CA |
| 总召IOA不是0 | COT=47、P/N=1 |
| 激活QOI超出20..36、请求P/N非法或当前已有未终结总召 | 否定ACT_CON,COT=7、P/N=1 |
| 空闲时收到合法激活请求 | 有空槽则建立唯一总召;BEGIN后应用接受才发肯定确认,忙时不同QOI也拒绝 |
| 激活QOI为23..36 | 合法请求交给应用,示例因未配置点表而Reply_Begin(false),回否定ACT_CON |
| 撤销不匹配、QOI超范围或P/N非法 | 否定DEACT_CON,COT=9、P/N=1 |
| 匹配的主站撤销 | 取消对应总召,肯定DEACT_CON,COT=9、P/N=0 |
| 总召唯一对象完整,但SQ=1或有多余尾字节 | 回否定7/9,COT/CA/IOA错误优先45/46/47;不执行激活或撤销 |
| 总召零对象/多对象等无法可靠应答的布局错误 | 计入rejected并丢弃;保留连接和已有总召 |
| 需要新增总召且4个任务槽已占满 | 尝试否定ACT_CON;无法安全提交/保存则丢新请求并计数,保留连接与原任务;匹配撤销复用原槽 |
| 接受/取数/整体业务期限到点 | 肯定ACT_CON提交前回否定确认并保留连接,提交后关闭原连接;已取出BEGIN才交付对应TIMEOUT终结事件 |
Example_Handle_Event先过滤asdu_result失败的I帧视图,避免进入业务分派或未知类型兜底;正常线上基础头/载荷截短已由Receiver和连接层处理。非总召命令先留给其他业务处理器;本示例最终回UNKNOWN_TYPE,单独保存一个待发送请求,若该槽占用时再收到未知命令则关闭原连接。
校验从类型开始;已知总召先检查是否有唯一完整对象,再按COT、CA、IOA、布局/QOI/P/N及事务状态决定。畸形单对象的否定副本规范为10字节、VSQ=1,保留OA/TEST/IOA/QOI,不修改原输入。广播应答使用实际站CA。错误请求不取消原总召或刷新期限;否定应答沿现有队列重试,满槽只丢新请求。这里只实现上述命令集合,不代表所有类型的业务规则。
`Example_Drain_Business`主动读取总召业务事件:BEGIN按QOI选点表,Reply_Begin接受或拒绝请求;NEED_DATA调用普通辅助函数Example_Build_Batch构造数据,再Submit_Batch,数据耗尽则Complete_Data;FINISHED按实例/请求编号清理对应资源,撤销和超时也走该分支。没有向库注册业务函数指针。数据源暂未就绪时可保存事件,后续就绪再应答;发送WAIT也保留事件,下一次调用重新组包,模块不缓存本批原文或重复生成NEED_DATA。
本例BEGIN只判断接受,首个NEED_DATA初始化游标并读取这一批;接真实数据源时,在Example_Build_Batch的逐点组包处读取当前值和品质。IOA、类型、分组成员及遍历策略均由调用者决定,协议库不登记或冻结完整IOA列表。示例常量表只用于展示业务接入,不是库规定的地址范围。逐批采样不保证整轮同一时刻,也不自动解决与突发上报的新旧排序;完整边界见[总召模块设计](../104/IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。
Example_Build_Batch使用局部游标遍历,仅在Submit_Batch返回OK后推进应用游标,此时发送层已复制原文并确定帧顺序,输入存储可复用。返回WAIT时保留同一业务事件及游标,先推进网络、读空Slave通知,再从同一位置重新取当前值和品质,不能重发旧构造结果。STALE表示原交互已失效(包括取消、撤销、超时及提交故障),丢弃旧应答并处理FINISHED。接实际数据源时,须将点值更新及突发发布与总召读值及提交串行化;排序保证及持续突发可能推迟总召的边界见[数据批次与发送路径](../104/IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。
`IEC104_Interrogation_Process(app->interrogation)`自动推进当前就绪任务,无需预算参数。Handle_Event在原会话有效、Slave通知已读空且没有更早REPLY时立即尝试提交短应答;未接手再保存,无法保存则计入dropped_requests。Process继续处理保存的应答及串行总召;任务为空、全部等待应用/事件空位、发送WAIT或Slave有未读通知时返回。Example_Step随后消费业务事件,下一轮继续推进;不在Process内部执行应用代码。
Example_Wait_Time直接读取总召状态的`need_process`,不再用`pending_requests && !waiting`推断。业务应答后或旧FINISHED出队释放空位时,下轮Poll使用0等待;发送WAIT后允许等待网络。每次Poll并读空Slave通知后,Example_Step仍调用总召Process重试,不能用need_process为false跳过;未读Slave通知始终优先处理。
Example_Init使用默认业务期限:BEGIN应答15秒、每批NEED_DATA至首次有效批次尝试15秒、执行整体期限0(禁用);对应begin_timeout_ms/data_timeout_ms/total_timeout_ms,各项0表示禁用。有效批次因发送背压WAIT后,本批取数计时停止,下一批NEED_DATA才重新计时;应用仍须重读并重试。BEGIN生成前不计业务期限;总期限显式设为非零时,从BEGIN生成起计算完整执行耗时,发送等待计入且不因新批次而延长。START/CONFIRM超时或BEGIN阶段Fail_Request转为否定ACT_CON并保留TCP及已有短应答;肯定ACT_CON提交后发生取数超时、总超时或NEED_DATA失败,仍关闭原连接。失败通知保留具体原因,须继续读取FINISHED并驱动Process,不能根据BEGIN失败假定已断链。业务FIFO被旧FINISHED占用时要及时读取,才能启动当前请求,期间仍拒绝新的总召。Example_Wait_Time在每次Poll前重新查询最近期限,将50ms等待上限进一步缩短。期限使用单调时钟,不代替链路t0..t3及单帧发送期限,也不等待主站ACK;默认关闭整体期限不影响这些底层保护。退出统计包含negative_submitted和dropped_requests;丢弃请求没有响应,主站可能超时或重试。
STOPDT或断线通知取消未提交任务。发送内部受理STOP而返回ERROR时,总召确认原连接正常停用后先让出,待通知终结为STOPPED;其他ERROR/FATAL仍失败。Submit_Batch为等待STOP通知而返回WAIT时不停止取数计时,应用继续按FIFO交付通知,旧交互随后STALE。事件消费预算耗尽时先续读通知,再推进业务。关闭时执行`Interrogation_Cancel → 读空业务事件并清理FINISHED/Release → Slave_Close → 读空Slave通知 → Interrogation_Destroy → Slave_Remove_Station → Slave_Destroy`。总召Destroy遇到活动任务或未读业务事件会拒绝释放;应用引用须持有到FINISHED清理,不能用Remove成功代替资源清理。
## 验证
```powershell
./scripts/build.ps1 -Test
```
`-Test` 需要预先安装 Node.js。脚本依次运行23组Slave/站点测试(单服务及双服务配置)、110组总召/计数量可控网络测试(单服务及双服务)、3组规则单测和真实本机TCP测试:
- `iec104_slave_test.c`:站点注册/移除、延迟回收、重复CA、13站共享服务、CA/context隔离、注册编号防替换、快照增删及嵌套集合锁、关闭重启保留,以及原有应答、事件身份和背压回归。
- `iec104_slave_example_test.c`:完整报文、部分写入、各发送阶段HIGH满与WAIT、请求校验、STOP/重连、非法请求、传输通知预算及API生命周期;业务事件覆盖延迟应答、批次校验、混合类型、容量边界、拒绝/失败/取消、终结预留和实例复用。总召覆盖全部16组、各阶段撤销、三项期限、迟到应答及通知优先。调度覆盖短应答立即提交及顺序保护、非队首槽复用、满槽否定或计数丢弃且当前任务有效、各阶段相同/不同QOI均忙拒绝、结束后重发成功、撤销对象匹配、停止清理、BEGIN前等事件空位及BEGIN后完整执行期限,并验证忙状态不拦截Type101总电度请求。测试设置HIGH=251字节、业务事件FIFO=2槽,使用真实容量背压限制发送。
- `slave_example_loopback_test.cjs`:启动实际可执行程序,使用Windows网络后端和127.0.0.1随机端口,以独立主站字节验证分片请求、k=1时逐帧ACK、站总召、多个不同QOI连续请求的忙拒绝及结束后重发成功、主站撤销与重复撤销、错误应答、STOP/START、重连和正常退出。服务约4秒后自行关闭。
畸形请求回归覆盖SQ/尾字节否定、零/多对象丢弃、基础头及载荷截短仍由Receiver拒绝、原事件不变、错误原因优先级、畸形激活/撤销不影响原BEGIN/NEED_DATA及期限、背压满槽后的FIFO恢复和多CA广播撤销隔离。
确认前失败另覆盖BEGIN尚未取出的超时、满任务池及HIGH等待时复用原槽、QOI/OA/TEST保持且应答使用本站CA、旧应答FIFO顺序、同连接重新总召、STOP/撤销/断线优先。确认已被发送层接手但尚未写出时,取数失败及超时仍断链,避免将已接受请求改发否定确认。
need_process回归覆盖完整业务交互、事件空位释放、总召等待应用时的短应答、CONFIRM/数据/终止/短应答的发送WAIT及恢复、到期立即续跑和未读STOP/START优先,并验证只读查询不提交帧或重复累计waits。
提交期通知回归覆盖真实启停容量背压留下STOP、三个发送入口的STOPPED归因、FATAL与STOP并存时仍为SEND_ERROR、非STOP的ERROR、OK/WAIT顺带交付RECORD时暂停后续提交,以及START后重新总召。
不排队提交回归覆盖旧NORMAL先于新总召、WAIT期间新突发先发后重新取值、HIGH积压、已接手总召部分写入后突发不能越过、背压不计取数超时而下一批重新计时、k=1窗口释放、到期ACK优先处理及发送故障终结。WAIT时验证队列和批次进度不增加,OK后覆盖输入存储验证发送层持有独立副本。
广播回归覆盖QOI=20..36、13个CA全部确认/数据/终止顺序、非空混合批次、状态区不足及跨实例重叠、已确认站删除/同CA替换、三阶段WAIT、超出FIFO容量的FINISHED、各阶段定向撤销及满槽广播撤销、拒绝与撤销同时等待、非当前站总期限和STOP/断线清理。Submit_Batch拒绝广播CA及非当前站CA;Type101继续交后续处理器。真实TCP用命令行注册的单站验证广播全站/分组、忙拒绝、非法QOI和撤销组合,共核对62个ASDU及11次正常完成的总召;多CA由可控Net回归覆盖。
这些检查覆盖本示例的模拟点表流程;实际主站设备、现场点表和设备执行接口仍需联调。
@@ -0,0 +1,257 @@
# IEC104 测试说明
> **目录与命令同步日期**:2026-10-28。测试代码位于根目录的 `test/`,本文仅列出当前保留的测试入口及可运行命令。
> **配套文档**:[TCP服务设计](../104/IEC104_TcpServer_设计架构.md)、[链路设计](../104/IEC104_Link_设计架构.md)、[发送模块设计](../104/IEC104_发送模块_设计架构.md)。
## 1. 当前测试入口
`iec60870_asdu_test.c`包含10组信息对象编解码测试,使用独立预期报文字节覆盖12种监视类型(各SQ=0/1)、18种命令编解码和12种COT/CA/IOA宽度组合。它不创建socket、不读取或设置系统时钟。覆盖可配置容量、249/254字节边界、127对象限制、失败原子性、地址连续性与溢出、品质和CP56校验、应答原文保留、重叠复制及公共地址修改。
`iec60870_asdu_extended_test.c`新增7组测试,以独立预期字节覆盖其余37种类型的696组布局。两套ASDU测试合计覆盖67种类型、1200组COT/CA/IOA及SQ布局。新增测试检查CP16完整数值范围、CP24边界/保留位、步位置负数、QDP、QPM、变长文件段0/255字节及配置容量、249/254总长、对象复制独立性、截短/多余字节、SQ/数量约束和失败原子性。
`iec104_tcp_event_ring_test.c`包含13个TCP事件字节环场景。它使用可控Net后端,不创建真实socket、不执行真实等待;通过直接包含`IEC104_TcpServer.c`检查内部容量、跨环尾布局和损坏数据的出队行为。
`iec104_tcp_scheduler_test.c`复用上述可控Net后端,包含25个调度场景;其中2个明确验证原有严格容量背压策略仍会超时,不能把测试通过解释为消除了队头阻塞。`iec104_net_pending_input_test.c`使用当前Windows后端进行真实本机回环检查。
`iec104_slave_test.c`复用可控Net后端,共23组统一入口测试,覆盖ASDU视图、共用发送分派、确认/终止字节、HIGH背压、非法参数、原连接身份、延迟复制、服务复用、生命周期、外部调度及离线上报。站点测试覆盖13节点、CA/context隔离、删除即时退出路由与引用延迟回收、同CA替代节点的注册编号、快照期间增删/嵌套/结束回收、显式集合锁及跨服务误用。共用层非104参数测试不代表已提供101串口传输。
因接口过期而无法编译的旧协议、发送管理器、TCP模拟后端和Windows回环测试及其专用辅助文件已删除。这里新增的测试均使用当前接口,历史测试未恢复。
`iec104_slave_example_test.c`包含原98组总召基线,覆盖总召报文、部分写入、各阶段WAIT、忙拒绝、撤销、STOP/断线、need_process、突发排序、不排队提交及业务/链路期限。提交期通知覆盖三个入口的STOPPED归因、FATAL与STOP并存、非STOP的ERROR、OK/WAIT伴随新回执及START后恢复。畸形请求覆盖可靠否定/丢弃边界、基础截短仍被Receiver拒绝、输入原文保持、活动总召/期限不变、背压满槽及广播撤销隔离。多站覆盖13站三阶段顺序、非空混合批次、状态区不足/重叠、已确认未来站删除与同CA重建、超出2槽FIFO的FINISHED交付、各阶段定向及满槽广播撤销、拒绝与撤销同时等待、非当前站总期限,以及总期限禁用、BEGIN先到期和两项同时到期的处理优先级。`test/slave_example_loopback_test.cjs`通过Windows后端与真实TCP验证命令行单站,多CA由可控Net回归覆盖。构建及点表见[示例说明](../examples/IEC104子站示例.md)。
## 2. 编译与运行
计数量扩展在上述98组总召基线上新增12组,共110组,单/双服务配置各跑一次。覆盖独立实例、请求校验、Type15/37批次边界、忙隔离、冻结/复位、动作重复调用和WAIT/STALE/TEST、业务期限、STOP/断线、共享连接失败、发送背压及多站广播。`iec104_interrogation_rules_test.c`另有3组规则测试,遍历限定词、数据COT和类型/SQ/FRZ矩阵并检查解析;没有网络交互。
在项目根目录的PowerShell中执行,要求GCC可用。下述TCP事件环和调度测试已包含TcpServer实现且自行实现Net接口,构建时不要再次链接`IEC104_TcpServer.c`或`IEC104_Net_Windows.c`。
```powershell
Get-Command gcc -ErrorAction Stop | Out-Null
New-Item -ItemType Directory -Path .build -Force | Out-Null
$testSources = @(
'test/iec104_tcp_event_ring_test.c',
'104/IEC104_Connection.c',
'104/IEC104_Receiver.c',
'104/IEC104_SendManager.c',
'104/IEC104_SendBuffer.c',
'104/IEC104_AsduQueue.c',
'104/IEC104_Codec.c',
'common/IEC60870_Codec.c',
'104/IEC104_Link.c',
'104/IEC104_Link_Slave_StartStop.c'
)
gcc -std=c11 -O2 -I104 -Icommon -Wall -Wextra -Werror @testSources -o .build/iec104_tcp_event_ring_test.exe
if ($LASTEXITCODE -ne 0) { throw 'TCP event ring build failed' }
./.build/iec104_tcp_event_ring_test.exe
if ($LASTEXITCODE -ne 0) { throw 'TCP event ring tests failed' }
```
成功时末行输出`TCP event ring: 13 cases passed`,进程退出码为0。任何断言失败都会输出函数名、行号和条件并以非零状态退出。编译失败后立即停止,不能继续运行旧二进制。
沿用上述`$testSources`编译调度测试;它已经包含字节环测试源文件,不能重复链接:
```powershell
$schedulerSources = @('test/iec104_tcp_scheduler_test.c') + $testSources[1..($testSources.Length - 1)]
gcc -std=c11 -O2 -I104 -Icommon -Wall -Wextra -Werror @schedulerSources -o .build/iec104_tcp_scheduler_test.exe
if ($LASTEXITCODE -ne 0) { throw 'TCP scheduler build failed' }
./.build/iec104_tcp_scheduler_test.exe
if ($LASTEXITCODE -ne 0) { throw 'TCP scheduler tests failed' }
gcc -std=c11 -O2 -I104 -Icommon -Wall -Wextra -Werror test/iec104_net_pending_input_test.c 104/IEC104_Net_Windows.c -lws2_32 -o .build/iec104_net_pending_input_test.exe
if ($LASTEXITCODE -ne 0) { throw 'Net loopback build failed' }
./.build/iec104_net_pending_input_test.exe
if ($LASTEXITCODE -ne 0) { throw 'Net loopback test failed' }
```
分别输出`TCP scheduler: 25 cases passed (including strict backpressure policy checks)`和`Windows Net pending input: loopback passed`。
两套公共ASDU测试仅链接 `IEC60870_*.c`,不包含104头文件,不依赖104 Codec、TCP、Net或Clock。以下分别使用默认Windows时间实现和纯整数实现:
```powershell
New-Item -ItemType Directory -Path .build -Force | Out-Null
$asduSources = @('test/iec60870_asdu_test.c', 'common/IEC60870_Asdu.c', 'common/IEC60870_InformationObjects.c', 'common/IEC60870_Codec.c', 'common/IEC60870_Time.c')
gcc -std=c11 -O2 -Icommon -Wall -Wextra -Werror @asduSources -o .build/iec60870_asdu_test.exe
if ($LASTEXITCODE -ne 0) { throw 'ASDU build failed' }
./.build/iec60870_asdu_test.exe
if ($LASTEXITCODE -ne 0) { throw 'ASDU tests failed' }
gcc -std=c11 -O2 -Icommon -Wall -Wextra -Werror -DIEC60870_TIME_USE_PLATFORM=0 @asduSources -o .build/iec60870_asdu_integer_time_test.exe
if ($LASTEXITCODE -ne 0) { throw 'ASDU integer-time build failed' }
./.build/iec60870_asdu_integer_time_test.exe
if ($LASTEXITCODE -ne 0) { throw 'ASDU integer-time tests failed' }
```
两次均应输出`ASDU: 10 groups passed (30 concrete types, 504 wire layouts, 12 parameter configurations)`。源码中的完整字节向量不通过待测编码器生成;另外检查未对齐输入、浮点位模式(含NaN/Inf)及有符号累计量边界。对每种监视/命令向量检查截短和多余尾字节;命令另检查非法SQ/数量、SCO保留位、DCO/RCO无效状态和时标错误。测试允许业务自行判断未知COT、地址及限定词,并验证原始字段不会被解析器丢失。
新增类型测试使用相同生产依赖,可分别验证默认时间后端和纯整数后端:
```powershell
$extendedSources = @('test/iec60870_asdu_extended_test.c', 'common/IEC60870_Asdu.c', 'common/IEC60870_InformationObjects.c', 'common/IEC60870_Codec.c', 'common/IEC60870_Time.c')
gcc -std=c11 -O2 -Icommon -Wall -Wextra -Werror @extendedSources -o .build/iec60870_asdu_extended_test.exe
if ($LASTEXITCODE -ne 0) { throw 'Extended ASDU build failed' }
./.build/iec60870_asdu_extended_test.exe
if ($LASTEXITCODE -ne 0) { throw 'Extended ASDU tests failed' }
gcc -std=c11 -O2 -Icommon -Wall -Wextra -Werror -DIEC60870_TIME_USE_PLATFORM=0 @extendedSources -o .build/iec60870_asdu_extended_integer_test.exe
if ($LASTEXITCODE -ne 0) { throw 'Extended integer-time build failed' }
./.build/iec60870_asdu_extended_integer_test.exe
if ($LASTEXITCODE -ne 0) { throw 'Extended integer-time tests failed' }
```
输出`Extended ASDU: 696 independent wire layouts verified`及`Extended ASDU: 7 groups passed (37 additional types, short times, variable file segments)`。这里验证无状态编解码,未模拟文件传输业务或真实设备互操作。
104封装检查独立放在 `iec104_asdu_transport_test.c`:复用公共套件的独立载荷向量,验证30种类型的42种类型/SQ组合、I帧序号与完整字节、249字节可发送及250/254字节拒绝。公共套件的测试入口仅用于复用夹具,不会在此重复运行。
```powershell
$transportSources = @('test/iec104_asdu_transport_test.c', 'common/IEC60870_Asdu.c', 'common/IEC60870_InformationObjects.c', 'common/IEC60870_Codec.c', 'common/IEC60870_Time.c', '104/IEC104_Codec.c')
gcc -std=c11 -O2 -I104 -Icommon -Wall -Wextra -Werror @transportSources -o .build/iec104_asdu_transport_test.exe
if ($LASTEXITCODE -ne 0) { throw '104 ASDU transport build failed' }
./.build/iec104_asdu_transport_test.exe
if ($LASTEXITCODE -ne 0) { throw '104 ASDU transport tests failed' }
```
输出 `104 ASDU transport: 2 groups passed (42 layouts, 249/254-byte boundary)`。
共用应答接口测试同时覆盖ASDU对象与104真实提交路径,不链接真实网络后端:
```powershell
$slaveSources = @(
'test/iec104_slave_test.c', 'common/IEC60870_Slave.c', '104/IEC104_Slave.c',
'common/IEC60870_Asdu.c', 'common/IEC60870_InformationObjects.c', 'common/IEC60870_Time.c',
'104/IEC104_Connection.c', '104/IEC104_Receiver.c', '104/IEC104_SendManager.c',
'104/IEC104_SendBuffer.c', '104/IEC104_AsduQueue.c', '104/IEC104_Codec.c', 'common/IEC60870_Codec.c',
'104/IEC104_Link.c', '104/IEC104_Link_Slave_StartStop.c'
)
gcc -std=c11 -O2 -I104 -Icommon -Wall -Wextra -Werror @slaveSources -o .build/iec104_slave_test.exe
if ($LASTEXITCODE -ne 0) { throw 'Slave replies build failed' }
./.build/iec104_slave_test.exe
if ($LASTEXITCODE -ne 0) { throw 'Slave replies tests failed' }
```
输出`Slave service: 23 groups passed (replies, identities, stations, lifecycle, backpressure)`。默认及`-DIEC104_TCP_SERVER_CAPACITY=2`均运行同一套测试。确认及终止字节由独立请求向量和明确COT比较,不用待测函数生成期望。失败且未接手时不增加队列条目,旧代次不能写入新连接;FATAL注入验证结果传递,不代表穷尽所有发送故障组合。
参数矩阵遍历COT=1/2、CA=1/2、IOA=1/2/3:288组双对象监视报文布局、216组命令编解码报文;应答单独遍历全部30类型、12种配置、7种COT及两种P/N。所有布局检查截短输入及额外尾字节。边界测试检查省略OA、1字节CA溢出、三种IOA连续地址溢出、非法/NULL参数、容量与配置上限分别生效、4字节未知类型应答的重叠复制及5字节读命令。通用ASDU可编码/解析254字节报文,而104 I帧封装仍拒绝该长度。
另逐类验证具体Create/Get_From_Buffer、编码函数表身份、输出存储尺寸、首对象失败不设置类型、命令数量上限、非法SQ及修改对象后重新编码的校验。具体解析结果按值保存,覆盖输入载荷被清零后对象仍有效。
以上编解码和共用接口用例验证信息对象格式和接口契约,具体业务流程由下面的示例测试验证。两种时间配置通过同一CP56用例,不等同于全部日历转换函数的一致性覆盖。
单文件示例及其业务测试通过一个脚本构建;需要MinGW GCC,真实回环测试另需Node.js(仅使用内置模块):
```powershell
./scripts/build.ps1 -Test
```
脚本输出两次`Slave service: 23 groups passed ...`(单/双服务)、两次`slave example + event interrogation tests: 110 groups passed ...`、`interrogation rules: 3 groups passed ...`和`Slave example loopback: passed ...`。可控后端使用251字节HIGH和2槽业务事件FIFO。真实回环使用默认存储、k=1、随机本机端口,验证总召与计数量交错、冻结/复位及后续读取、分组/广播、TEST拒绝、COT=8返回45、STOP/START、重连及8秒后的正常退出。这里使用模拟点表,不包含真实计量设备、实际校时、遥控,也不构成完整互操作认证。
## 3. 覆盖范围
| 场景 | 验证内容 |
|---|---|
| 跨环尾往返 | 每个环起点、短长I帧、RECORD和全部连接通知;头部及载荷跨界、短帧尾部清零、历史代次交付 |
| 满容量预留 | 普通区、启停区、全部收尾项同时占满;失败入队保持原数据,FIFO次序不变 |
| 容量边界 | CONNECT按实际大小判断;旧启停通知及收尾占用对新接入和启停的限制 |
| 损坏项拒绝 | 非法类型、代次、长度、游标和计数;ERROR保持输出和队列不变 |
| 短帧背压恢复 | 默认普通区缓存185条16字节APDU,下一帧暂留Receiver;消费后按序恢复 |
| I帧先于回执 | 等待空间的长I帧阻止较短RECORD提前转移 |
| 空间不足时回执延后 | 回执暂留内部,启停通知和CON仍推进 |
| 预算用尽时回执延后 | 回执可晚于随后受理的启停通知交付 |
| 收尾与重连 | 剩余I帧、全部记录、DISCONNECTED的次序,以及新连接下读取旧代次事件 |
| 启停配额背压 | 精确核对CON字节数和帧内容;未受理ACT不回CON,消费一条通知后受理并确认,剩余通知及状态快照按序保留 |
| 同状态重复ACT | 已启动时START、已停止时STOP均回CON而不通知;无未读通知及配额满时均覆盖两个方向 |
| 跨代次启停顺序 | 旧启停通知读完前不接入;读完后可保留旧断开通知接入,严格交付DISCONNECTED(旧)→CONNECTED(新)→ACTIVATED(新)及各自快照 |
| 分包与每轮预算 | 默认一次recv容纳8个满长APDU;分段输入完整交付且不重复 |
这些场景不等同于完整协议测试,也不覆盖真实网络、全部异常超时、多线程并发或ASDU业务语义。性能模拟的测量方法和局限见[TCP服务设计中的验证记录](../104/IEC104_TcpServer_设计架构.md)。
调度专项覆盖:
| 场景 | 验证内容 |
|---|---|
| ACK边界(8组) | t1边界前置7/8/9/33个I帧;1/16/17字节recv分段;满长APDU突发;按序交付且每轮I事件不超过8条 |
| 有限快照(3组) | 用户态尾部、内核分段输入及跨Feed预算的S流;持续新输入不能扩大旧快照或无限推迟缺ACK超时 |
| 真正缺ACK | 没有输入时按原t1触发I_ACK_TIMEOUT |
| 持续接收发送(2组) | 每轮8个I帧且业务读空FIFO,完整/部分send均持续推进 |
| 业务发送 | 事件预算用尽后NORMAL业务仍能实际写出 |
| 不可写(2组) | WOULD_BLOCK不忙重试;恢复可写后发送,持续不可写仍触发LOCAL_SEND_TIMEOUT |
| TESTFR(2组) | CON在事件预算后仍受理;没有CON时仍触发TESTFR_TIMEOUT |
| 真实容量背压(2组) | I帧普通区满、启停配额满仍按原策略暂停接收并检查严格期限 |
| 续收时提交应答 | 业务在轮次之间可正常Send_Response,不提前误判旧ACK超时 |
| 非法ACK | 续收不屏蔽越界ACK,仍以PROTOCOL收尾 |
| 快照查询失败 | 保留输入与事件收尾,以TRANSPORT报告网络失败 |
| EOF与应答 | 先交付I帧让业务提交应答,再读取EOF排空 |
Windows回环专项验证查询不会消费字节、部分recv后计数准确、输入耗尽与EOF均可返回零字节、非法参数不修改输出。它不代表真实网络下的协议性能或吞吐测试。
## 4. 配置覆盖
沿用上一节的`$testSources`,分别编译和运行:
```powershell
gcc -std=c11 -O2 -I104 -Icommon -Wall -Wextra -Werror -DIEC104_TCP_RECORD_CAPACITY=3 -DIEC104_TCP_EVENT_CAPACITY=12 -DIEC104_TCP_EVENT_STATE_RESERVE=2 @testSources -o .build/iec104_tcp_event_ring_small.exe
if ($LASTEXITCODE -ne 0) { throw 'Small configuration build failed' }
./.build/iec104_tcp_event_ring_small.exe
if ($LASTEXITCODE -ne 0) { throw 'Small configuration tests failed' }
gcc -std=c11 -O2 -I104 -Icommon -Wall -Wextra -Werror -DIEC104_TCP_RECORD_CAPACITY=40 -DIEC104_TCP_EVENT_CAPACITY=100 -DIEC104_TCP_EVENT_STATE_RESERVE=6 -DIEC104_TCP_RX_CAPACITY=4096 @testSources -o .build/iec104_tcp_event_ring_large.exe
if ($LASTEXITCODE -ne 0) { throw 'Large configuration build failed' }
./.build/iec104_tcp_event_ring_large.exe
if ($LASTEXITCODE -ne 0) { throw 'Large configuration tests failed' }
```
调度专项使用同样的配置参数,将命令中的`@testSources`换为`@schedulerSources`并使用独立输出文件名即可。
## 5. 验证记录
2026-10-28 计数量召唤:先验证原98组总召、单/双服务各23组Slave及真实TCP基线;拆分规则后原98组不修改通过。新实现用独立规则和实例复用引擎,动作接口校验后执行一次,冻结/复位只确认,不制造取数阶段;总召接口名称和行为保留。严格GCC构建通过,110组业务测试在单/双服务配置均通过,3组规则测试、10组ASDU和7组扩展ASDU测试通过。真实Windows TCP回环通过;沙箱限制Node子进程启动(EPERM)时,原回环命令在获准的环境中运行通过。只链接总召或只链接计数量(实例容量1)的创建/销毁检查通过。修改前字节备份和验证产物位于`.build/counter-interrogation-20261028/`。未执行Keil目标构建或硬件联调;本次仅修改IEC60870-Slave目录。
同日Windows x64 GCC实测:实例1240→1248字节,默认单服务池1240→2496字节,请求32字节、事件80字节、广播每站48字节不变。新增请求字段使用原尾部填充,旧字段偏移保留;此测量不代替ARMCC目标的RAM/栈预算。只用一种业务可显式缩小实例池。
2026-10-27 广播总期限判定收敛:Task_Timeout以同一次取时返回最近期限、原因及已确认站到期标志,Expire不再重复遍历。公开API、固定存储容量及超时处理策略不变。新增总期限禁用、BEGIN先到期和同时到期用例,重构前后均通过。GCC C11严格警告构建、98组总召、单/双服务各23组Slave及真实Windows TCP回环通过;回环在沙箱内因子进程EPERM失败,按原命令在沙箱外重跑通过。修改及验证限本目录,未执行Keil构建或硬件联调。
2026-10-27 提交期STOP归因:总召三个发送入口仅在ERROR时复核原连接正常停用状态,先交付DEACTIVATED,保留STOPPED原因;其他ERROR/FATAL不降级,成功路径不增加查询。目录内严格编译、97组总召、单/双服务各23组Slave及真实Windows TCP回环通过。新增真实通知容量背压复现,以及其他入口的容量/故障注入;验证等待不推进批次、不停止取数计时、不重复提交,STOP伴随FATAL仍为SEND_ERROR,OK/WAIT交付新回执后暂停后续提交。原临时复现程序已迁入正式测试,公开接口和固定存储容量不变。
2026-10-27 总召畸形请求隔离:单对象完整但SQ/尾字节错误时规范化否定应答,零/多对象丢弃;公共视图失败交公共入口,基础截短仍由Receiver处理。目录内生产示例通过严格警告编译,92组总召回归及单/双服务各23组Slave回归通过,真实Windows TCP回环通过(沙箱子进程EPERM后按原命令重跑)。覆盖畸形激活/撤销不取消已有总召、不刷新期限、不修改输入,以及FIFO背压、满槽丢弃和多CA活动广播继续完成。未增加任务槽或发送缓冲,修改及验证均限IEC60870-Slave目录。
2026-10-27 c104广播发送顺序:合法多目标广播改为全部ACT_CON、逐站数据、全部ACT_TERM,调用方提供每站状态区,仍共用一个任务。87组总召及23组Slave回归在单/双服务配置下通过C11严格警告编译及运行;生产示例构建和真实Windows TCP回环通过。新增覆盖状态区不足/重叠、非空多站报文、已确认未来站删除、13条FINISHED通过2槽FIFO无损交付、满槽及跨阶段撤销、拒绝与撤销并存、非当前站总期限。回环因沙箱子进程EPERM按原授权命令重跑。Windows x64默认配置实测管理对象1240B、每站状态48B,通信队列容量和覆盖策略不变。文档197个本地链接及锚点、UTF-8编码和diff空白检查通过;修改及验证均限本目录。
2026-10-27 多CA总召及站点删除:Remove即时退出路由,引用/快照结束后回收;总召按请求CA选站,广播以一个任务逐站推进。23组Slave及78组总召测试均在单/双服务配置下通过`-std=c11 -O2 -Wall -Wextra -Werror`编译和运行,包含13站广播、同CA替换、三阶段WAIT及撤销/超时/STOP。生产示例严格编译和真实TCP回环通过;回环子进程EPERM后以已授权原命令运行。Windows x64默认配置测量:站点40B、快照32B、Slave40B、总召1192B、任务168B;通信队列容量及事件覆盖策略未变。本次修改与验证仅在IEC60870-Slave目录内进行。
2026-10-27 共用站点基础:Slave提供调用方节点注册、按CA查找、context、引用及集合锁,总召可绑定已注册节点,示例改为注册一次CA后复用。22组Slave测试及70组总召测试均在单/双服务配置下通过`-std=c11 -O2 -Wall -Wextra -Werror`构建与运行;生产示例严格编译及真实Windows TCP回环通过。回环因沙箱子进程EPERM使用已授权原命令重跑。当前总召仍只处理一个绑定站点,未验证或宣称多CA总召调度。x64测量每节点32字节,Slave32字节,总召968字节,通信队列及业务槽容量不变。仓库`.venv`和可执行Python缺失,`@build`及RAM/栈报告入口未能启动;未执行ARMCC目标验证或硬件联调。
2026-10-27 总召请求存储精简:任务改为保存原事件身份及10字节Type100原文,发送时临时建立ASDU视图;Slave新增按身份发送/尝试发送/关闭接口,保留原服务实例与连接代次校验。任务、事件及传输队列容量和覆盖策略未调整,内存测量见[固定存储](../104/IEC104_Interrogation_设计架构.md#2-对象身份与固定存储)。68组总召回归、18组Slave回归(默认及双服务配置)和真实Windows TCP回环通过严格警告构建及运行。新增验证源事件复用、输入存储释放、HIGH接手条数不变、Try等待不入队/覆盖及旧身份隔离;原总召回归继续覆盖忙拒绝、广播错误字段、满槽撤销、超时、STOP和新旧数据排序。回环子进程经已授权的原命令运行;未执行Keil目标构建或硬件联调。
2026-10-27 总召与突发发送排序:总召数据改为不排队提交,有未提交HIGH/NORMAL、当前帧或窗口背压时返回WAIT;应用保留事件及游标,重读当前值后重试。有效批次遇发送WAIT后停止本批取数期限,下一批重新计时;示例仅在OK后推进游标。68组总召测试、13个TCP事件环场景、25个调度场景及15组Slave测试通过C11严格警告编译与运行;真实Windows TCP回环通过。回环子进程在沙箱内遇EPERM,经授权使用原命令重跑通过。新增场景覆盖新旧突发及总召顺序、部分写入、窗口和期限边界、提交故障;产物位于`.build/`,未连接实际设备。
2026-10-26 总召本地续跑:状态新增need_process,示例直接据此决定立即续跑;实时检查未读Slave通知,发送WAIT后让出,网络推进后继续重试。62组可控网络测试通过C11严格警告编译与运行,覆盖业务应答、旧FINISHED出队释放空位、短应答、四类发送背压、到期及STOP/START优先;真实Windows TCP回环通过。回环在沙箱内启动子进程遇到EPERM,经授权使用原测试命令重跑通过。产物位于`.build/slave-example/`,未连接实际设备。
2026-10-26 目录整理:104协议、公共模块、示例、测试、文档和构建脚本分别归入 `104/`、`common/`、`examples/`、`test/`、`docs/` 和 `scripts/`,同步头文件搜索路径、示例引用及文档命令。49个生产源码/头文件的SHA256与迁移前一致,21个生产C文件通过C11严格警告检查。按本文命令运行的默认/纯整数时间ASDU测试(各10组)、扩展ASDU测试(各7组)、104封装2组、子站15组、TCP事件环默认/小容量/大容量各13组、调度25组及Windows输入查询回环均通过;从非项目根目录调用构建脚本,示例编译及59组总召测试通过。真实TCP回环核对62个ASDU和11次正常完成的总召通过。当前执行环境拒绝Node默认子进程管道(EPERM);本次回环以临时文件接收真实子进程输出,原测试代码、网络交互和断言未修改。迁移清单、路径校验及临时运行适配保存在 `.build/layout-migration/`,未连接实际设备。
2026-10-26 广播地址处理重构:新增固定协议常量IEC104_CA_BROADCAST,Type100入口一次确定应答CA,统一在内部请求副本建立时写入;Process及发送重试直接沿用该地址。移除重复任务header,批次COT/OA/TEST从info校验,CA继续按本站配置校验。原59组可控网络测试通过C11严格警告编译与运行,覆盖广播及定向总召、错误CA回显、发送WAIT、忙拒绝、撤销及确认前失败。真实Windows TCP回环继续通过,核对62个ASDU和11次正常完成的总召。修改前备份与可控测试产物位于`.build/interrogation-address-copy-20261026-031956/`;公开函数签名、事件及配置字段不变。
2026-10-26 总召广播公共地址:Type100激活和撤销接受65535,业务事件、确认、数据、终止及广播错误应答使用本站CA;其他错误CA仍原值回显,输入Slave事件不变。新增共用Asdu_Set_CA,1/2字节CA及COT组合、空/非空构造容器、借用原文、越界及无效容器不写入均通过测试。ASDU共10组,总召可控网络共59组,均通过C11严格警告编译与运行;总召覆盖QOI=20..36、发送WAIT、满槽忙拒绝、确认前失败、四种激活/撤销地址组合、非法数据CA和广播Type101隔离。真实Windows TCP回环逐帧核对62个ASDU和11次正常完成的总召,通过广播全站/分组、忙拒绝与重试、撤销、错误应答、STOP/START及重连。API文档359个声明与源码一致,声明块通过C11/C++17严格检查,API及流程链接和总召设计格式检查通过。备份与可控测试产物位于`.build/interrogation-broadcast-20261026-000144/`,实际服务产物位于`.build/slave-example/`。未执行真实设备联调。
2026-10-23 单文件子站示例:新增`examples/iec104_slave_example.c`,模拟点表、请求分派、总召状态机和main集中在一个文件;使用现有Slave接口,不修改协议库实现。示例及9组业务测试通过C11严格警告编译,可控后端验证逐阶段WAIT、部分发送、拒绝及任务取消;独立Node主站通过真实Windows本机TCP验证分片输入、k=1、3次完整总召、错误应答、STOP/START、重连及正常关闭。未连接实际主站设备,未设置系统时间。产物位于`.build/slave-example/`。
2026-10-23 子站统一入口:IEC104_Slave内部持有静态池TCP服务,NextEvent自动交付ASDU及原连接发送接口,Copy_Event复制延迟任务并重建自引用。删除手动SlaveConnection_Init接口,生命周期、主动上报、查询和关闭统一由Slave提供;TCP共享值类型迁至IEC104_TcpTypes.h,通信状态机及发送调度代码保持不变。默认及双服务容量各通过13组Slave回归,TCP事件环13个场景、调度25个场景通过;20个生产源文件通过C11严格警告检查,公开入口示例通过C11/C++11检查,依赖清单确认Slave业务头不引入TcpServer操作接口或内部对象头。Slave.c通过实际`-fanalyzer -c`分析。源码及文档修改前备份、测试产物位于`.build/slave-service-20261023/`。未执行真实设备联调。
2026-10-22 公共应用层拆分:信息对象、ASDU、参数和公共头解析统一为IEC60870命名,移除原信息对象/ASDU文件与符号,更新104接入及文档构建清单。两套公共ASDU测试仅链接IEC60870源文件,在默认及纯整数时间配置下各通过9组/504布局和7组/696布局;104封装独立通过2组/42布局及249/254字节边界。共用应答8组、TCP事件环13个场景、调度25个场景通过;20个生产源文件通过严格警告语法检查,公共及104接入头通过C++11严格检查。编译器依赖清单确认公共层不包含104头文件;源码对照确认对象与容器算法仅迁移名称和包含路径,接口汇总保持323个模块接口及7个内部协作函数。未实现101串口链路,未进行真实设备联调。
2026-10-22 常规类型补齐:新增37种对象及CP16/CP24工具,完整支持67种类型;ASDU接入变长文件段和独立SQ/数量约束。原ASDU套件9组、504组布局通过;新增套件在默认及纯整数时间配置下各通过7组、696组布局。共用应答8组、TCP事件环13个场景、调度25个场景通过。19个生产源文件通过严格警告语法检查;新公共头通过C++11检查;Asdu、InformationObjects和Time通过实际`-fanalyzer -c`分析。代码及文档沿用原有中文注释格式,未执行真实设备联调。
2026-10-21 对象分层:新增30种具体对象,统一ASDU创建/添加/解析/取对象,移除旧联合体与分类接口。默认及纯整数时间配置各通过9组测试(504种完整报文布局);17个生产源文件通过严格警告语法检查,Asdu与InformationObjects通过 `-O2 -fanalyzer -c -Wall -Wextra -Werror`。TCP事件环13个场景、调度25个场景重新编译运行通过。未执行真实设备联调。
2026-10-21 ASDU参数化:所有相关入口直接改为显式传入应用层参数,不保留旧签名。默认及`IEC60870_TIME_USE_PLATFORM=0`各通过12组测试,编译选项为`-std=c11 -O2 -Wall -Wextra -Werror`;`IEC60870_Asdu.c`和`IEC104_Codec.c`通过`-fanalyzer -c`严格警告分析。现有默认配置TCP事件环13个场景及调度25个场景重新编译运行通过。未执行真实设备联调或系统设时。
2026-10-20启停通知测试补强:默认、小容量及大容量配置各通过13个字节环场景,默认配置通过25个调度场景,均使用`-std=c11 -O2 -Wall -Wextra -Werror`构建。可执行文件及修改前字节环测试源码保存在`.build/state-events-coverage-20261020/`。
补充单槽边界(`IEC104_TCP_EVENT_STATE_RESERVE=1`):本次加强的启停背压及新增的重复ACT、跨代次顺序共3个场景单独运行通过;完整字节环套件在原有`test_delayed_receipt`的回执计数断言处失败,修改前源码也在同一位置失败。单槽配置未计入整套通过结果。
2026-10-20调度修复:默认、小容量及大容量配置各通过11个字节环场景和25个调度场景;Windows输入快照真实回环及生产库严格警告构建通过。默认服务对象为21472字节,比上一版增加8字节;事件环及RX容量不变。验证输出及修改前备份在`.build/scheduler-fix-20261020/`。此前每帧增加0.02–0.03 μs的结果仅对应存储改造,不涵盖本次新增查询与调度行为。
使用`-fanalyzer -c`实际编译时,TcpServer和Connection通过。Windows后端严格分析仍报告Listen/Accept处的3条文件描述符诊断(第142、151、190行);修改前备份产生相同诊断,均不在新增输入查询函数中。未修改既有socket代码或屏蔽告警;完整对照保存在`net_analysis_before.log`和`net_analysis_after.log`。因此不将Windows后端描述为静态分析全通过。
2026-10-19字节环改造验证:GCC 16.1.0(MinGW-W64 x64),默认、小容量及大容量配置各通过11个场景。Windows生产库严格警告构建及GCC静态分析通过。源码哈希、完整命令和测量输出位于`.build/fifo-byte-ring-20261019/`。
历史测试及构建记录仍保存在`.build`中,仅描述当时的源码版本,不属于当前可运行测试。本文不再提供已删除测试的构建命令,也不将其历史通过数计入当前结果。
@@ -0,0 +1,67 @@
param(
[switch]$Test,
[string]$Compiler = 'gcc'
)
$ErrorActionPreference = 'Stop'
$projectRoot = Split-Path -Parent $PSScriptRoot
$protocolInclude = Join-Path $projectRoot '104'
$commonInclude = Join-Path $projectRoot 'common'
$exampleBuild = Join-Path $projectRoot '.build/slave-example'
New-Item -ItemType Directory -Path $exampleBuild -Force | Out-Null
Get-Command $Compiler -ErrorAction Stop | Out-Null
# Shared dependencies; the example adds the actual TCP and Windows Net implementations.
$libraryNames = @(
'104/IEC104_Interrogation.c', '104/IEC104_GeneralInterrogation.c',
'104/IEC104_CounterInterrogation.c', '104/IEC104_Slave.c',
'common/IEC60870_Slave.c', 'common/IEC60870_Asdu.c',
'common/IEC60870_InformationObjects.c', 'common/IEC60870_Time.c', 'common/IEC60870_Codec.c',
'104/IEC104_Connection.c', '104/IEC104_Receiver.c', '104/IEC104_SendManager.c',
'104/IEC104_SendBuffer.c', '104/IEC104_AsduQueue.c', '104/IEC104_Codec.c',
'104/IEC104_Link.c', '104/IEC104_Link_Slave_StartStop.c'
)
$librarySources = @($libraryNames | ForEach-Object { Join-Path $projectRoot $_ })
$compileFlags = @('-std=c11', '-O2', '-Wall', '-Wextra', '-Werror', "-I$protocolInclude", "-I$commonInclude")
$exampleOutput = Join-Path $exampleBuild 'iec104_slave_example.exe'
$exampleSources = @(
(Join-Path $projectRoot 'examples/iec104_slave_example.c'),
(Join-Path $projectRoot '104/IEC104_TcpServer.c'),
(Join-Path $projectRoot '104/IEC104_Net_Windows.c')
) + $librarySources
& $Compiler @compileFlags @exampleSources -o $exampleOutput -lws2_32
if ($LASTEXITCODE -ne 0) { throw 'Slave example build failed' }
Write-Output "Built: $exampleOutput"
if ($Test) {
# Shared Station/Slave APIs, including isolation between two independent services.
foreach ($capacity in @(1, 2)) {
$slaveTestOutput = Join-Path $exampleBuild "iec104_slave_test_$capacity.exe"
$slaveTestSources = @((Join-Path $projectRoot 'test/iec104_slave_test.c')) + $librarySources
& $Compiler @compileFlags "-DIEC104_TCP_SERVER_CAPACITY=$capacity" @slaveTestSources -o $slaveTestOutput
if ($LASTEXITCODE -ne 0) { throw 'Slave/Station test build failed' }
& $slaveTestOutput
if ($LASTEXITCODE -ne 0) { throw 'Slave/Station tests failed' }
}
# The controlled-backend test includes TcpServer.c itself. Do not link a second TCP/Net implementation.
$testSources = @((Join-Path $projectRoot 'test/iec104_slave_example_test.c')) + $librarySources
foreach ($capacity in @(1, 2)) {
$testOutput = Join-Path $exampleBuild "iec104_slave_example_test_$capacity.exe"
& $Compiler @compileFlags "-DIEC104_TCP_SERVER_CAPACITY=$capacity" -DIEC104_TCP_HIGH_CAPACITY=251 -DIEC104_INTERROGATION_EVENT_CAPACITY=2 @testSources -o $testOutput
if ($LASTEXITCODE -ne 0) { throw 'Slave example test build failed' }
& $testOutput
if ($LASTEXITCODE -ne 0) { throw 'Slave example tests failed' }
}
$rulesOutput = Join-Path $exampleBuild 'iec104_interrogation_rules_test.exe'
$rulesSources = @(
(Join-Path $projectRoot 'test/iec104_interrogation_rules_test.c'),
(Join-Path $projectRoot '104/IEC104_TcpServer.c'),
(Join-Path $projectRoot '104/IEC104_Net_Windows.c')
) + $librarySources
& $Compiler @compileFlags @rulesSources -o $rulesOutput -lws2_32
if ($LASTEXITCODE -ne 0) { throw 'Interrogation rules test build failed' }
& $rulesOutput
if ($LASTEXITCODE -ne 0) { throw 'Interrogation rules tests failed' }
Get-Command node -ErrorAction Stop | Out-Null
& node (Join-Path $projectRoot 'test/slave_example_loopback_test.cjs') $exampleOutput
if ($LASTEXITCODE -ne 0) { throw 'Slave example TCP loopback test failed' }
}
@@ -0,0 +1,87 @@
/* 召唤种类规则单测:直接调用无状态规则,验证限定词解析、数据COT和允许的数据类型。
* 使用独立请求字节,不经过网络,也不执行应用的冻结或复位动作。 */
#include "../104/IEC104_Interrogation_Internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define CHECK(x) do { if (!(x)) { fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #x); exit(1); } } while (0)
/* QCC同时包含RQT和FRZ;用非零IOA确认解析保留原值,业务合法性由后续校验决定。 */
static void test_decode(void)
{
static const uint8_t qcc[] = {0x05, 0x45, 0x85, 0xc5, 0x01, 0x3f};
IEC60870_AppLayerParameters_t parameters = {1, 1, 2, 0, 2, 3, 249};
unsigned i;
for (i = 0; i < sizeof(qcc); ++i) {
uint8_t bytes[] = {101, 1, 6, 0x5a, 1, 0, 0x34, 0x12, 0, 0};
IEC60870_AsduInfo_t info;
uint32_t ioa = 0;
uint8_t qualifier = 0, freeze = 0;
bytes[9] = qcc[i];
CHECK(IEC60870_Asdu_Parse(&parameters, bytes, sizeof(bytes), &info) == IEC60870_ASDU_OK);
CHECK(IEC104_COUNTER_INTERROGATION_RULES.decode(&info, &ioa, &qualifier, &freeze));
CHECK(ioa == 0x1234 && qualifier == (qcc[i] & 0x3f) && freeze == (qcc[i] >> 6));
}
{
uint8_t bytes[] = {100, 1, 6, 0, 1, 0, 0, 0, 0, 36};
IEC60870_AsduInfo_t info;
uint32_t ioa = 1;
uint8_t qualifier = 0, freeze = 3;
CHECK(IEC60870_Asdu_Parse(&parameters, bytes, sizeof(bytes), &info) == IEC60870_ASDU_OK);
CHECK(IEC104_GENERAL_INTERROGATION_RULES.decode(&info, &ioa, &qualifier, &freeze));
CHECK(ioa == 0 && qualifier == 36 && freeze == 0);
}
}
/* 遍历完整字节范围验证限定词边界,并检查总请求及四个分组的数据COT映射。 */
static void test_qualifiers(void)
{
unsigned q;
CHECK(IEC104_GENERAL_INTERROGATION_RULES.type_id == 100);
CHECK(IEC104_GENERAL_INTERROGATION_RULES.has_deactivation);
CHECK(IEC104_COUNTER_INTERROGATION_RULES.type_id == 101);
CHECK(!IEC104_COUNTER_INTERROGATION_RULES.has_deactivation);
for (q = 0; q < 256; ++q) {
CHECK(IEC104_GENERAL_INTERROGATION_RULES.qualifier_valid((uint8_t)q) == (q >= 20 && q <= 36));
CHECK(IEC104_COUNTER_INTERROGATION_RULES.qualifier_valid((uint8_t)q) == (q >= 1 && q <= 5));
}
for (q = 20; q <= 36; ++q) { CHECK(IEC104_GENERAL_INTERROGATION_RULES.data_cause((uint8_t)q) == q); }
CHECK(IEC104_COUNTER_INTERROGATION_RULES.data_cause(1) == 38);
CHECK(IEC104_COUNTER_INTERROGATION_RULES.data_cause(2) == 39);
CHECK(IEC104_COUNTER_INTERROGATION_RULES.data_cause(3) == 40);
CHECK(IEC104_COUNTER_INTERROGATION_RULES.data_cause(4) == 41);
CHECK(IEC104_COUNTER_INTERROGATION_RULES.data_cause(5) == 37);
}
/* 交叉验证Type/SQ/FRZ:计数量只在读取时允许Type15或非连续寻址的Type37。
* 总召保持原九种监视类型,不能因共用引擎而接受累计量或命令类型。 */
static void test_types(void)
{
static const uint8_t types[] = {1, 3, 5, 7, 9, 11, 13, 15, 16, 20, 21, 37, 100, 101};
unsigned i, sq, freeze;
for (i = 0; i < sizeof(types); ++i) {
for (sq = 0; sq < 2; ++sq) {
for (freeze = 0; freeze < 4; ++freeze) {
IEC60870_AsduInfo_t info;
bool expected = freeze == 0 && (types[i] == 15 || (types[i] == 37 && sq == 0));
memset(&info, 0, sizeof(info)); info.type_id = types[i]; info.is_sequence = (uint8_t)sq;
CHECK(IEC104_COUNTER_INTERROGATION_RULES.data_allowed(&info, (uint8_t)freeze) == expected);
if (freeze == 0) {
bool general = types[i] == 1 || types[i] == 3 || types[i] == 5 || types[i] == 7 ||
types[i] == 9 || types[i] == 11 || types[i] == 13 || types[i] == 20 || types[i] == 21;
CHECK(IEC104_GENERAL_INTERROGATION_RULES.data_allowed(&info, 0) == general);
}
}
}
}
}
int main(void)
{
test_decode(); test_qualifiers(); test_types();
printf("interrogation rules: 3 groups passed; request=%zu event=%zu station=%zu\n",
sizeof(IEC104_InterrogationRequest_t), sizeof(IEC104_InterrogationEvent_t),
sizeof(IEC104_InterrogationStationState_t));
return 0;
}
@@ -0,0 +1,37 @@
[
{
"old": "104/IEC104_Interrogation.c",
"next": "104/business/IEC104_Interrogation.c",
"sha256": "d4a4ca0530780a45f83e4c4989289f36d2a63f61f3625b1b550fe74ea9caddca"
},
{
"old": "104/IEC104_Interrogation.h",
"next": "104/business/IEC104_Interrogation.h",
"sha256": "466897aa79a5607d65c88c2cc7493eef72995209c61186365f0361584d7ff3fb"
},
{
"old": "104/IEC104_Interrogation_Internal.h",
"next": "104/business/IEC104_Interrogation_Internal.h",
"sha256": "95ae4b2c54ca213375647bb28eb54339198ed2cb04d4da55c9e9cb6fa7fa6bd5"
},
{
"old": "104/IEC104_GeneralInterrogation.c",
"next": "104/business/IEC104_GeneralInterrogation.c",
"sha256": "0c2f31ee24f2757136edd87fd631ebd83cef0ae3cfa333e8d9a651622767c023"
},
{
"old": "104/IEC104_GeneralInterrogation.h",
"next": "104/business/IEC104_GeneralInterrogation.h",
"sha256": "785d298fa21493592b61d72aede75f5e48f8909472eae9a897dc5d1bb8a1df79"
},
{
"old": "104/IEC104_CounterInterrogation.c",
"next": "104/business/IEC104_CounterInterrogation.c",
"sha256": "f92a064e95a7add319be9197b485d0ebdd549b97c0de24ae46e380a48344be40"
},
{
"old": "104/IEC104_CounterInterrogation.h",
"next": "104/business/IEC104_CounterInterrogation.h",
"sha256": "f633522212bfcd00bb72d40bc1f81890a114338cdab5c1352341cf685085eba5"
}
]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,494 @@
/* 104时钟同步:固定任务槽、显式同步动作及按请求顺序的应答。 */
#include "IEC104_ClockSynchronization.h"
#include "IEC104_TcpServer_Config.h"
#include "IEC104_Net.h"
#include "IEC60870_Codec.h"
#include <string.h>
#ifndef IEC104_CLOCK_SYNCHRONIZATION_CAPACITY
#define IEC104_CLOCK_SYNCHRONIZATION_CAPACITY IEC104_TCP_SERVER_CAPACITY
#endif
#ifndef IEC104_CLOCK_SYNCHRONIZATION_TASK_CAPACITY
#define IEC104_CLOCK_SYNCHRONIZATION_TASK_CAPACITY 4U
#endif
_Static_assert(IEC104_CLOCK_SYNCHRONIZATION_CAPACITY > 0U, "Clock needs instance storage");
_Static_assert(IEC104_CLOCK_SYNCHRONIZATION_TASK_CAPACITY > 0U &&
IEC104_CLOCK_SYNCHRONIZATION_TASK_CAPACITY <= UINT32_MAX / 2U, "Clock needs bounded task storage");
enum { CLOCK_EXECUTE = 1, CLOCK_REPLY = 2 };
typedef struct {
IEC104_SlaveEventIdentity_t identity;
uint8_t bytes[16];
IEC104_StationSnapshot_t stations;
IEC104_Station_t *station; /* 定向Acquire;广播由活动快照保护当前节点。 */
uint64_t receive_serial, accepted_ms;
uint16_t response_ca;
uint8_t phase, cause;
bool negative, executed, send_waiting, occupied;
} ClockTask;
struct IEC104_ClockSynchronization {
IEC104_Slave_t *slave;
IEC104_SlaveEventIdentity_t session;
IEC104_ClockSynchronizationConfig_t config;
IEC104_ClockSynchronizationStatus_t status;
ClockTask tasks[IEC104_CLOCK_SYNCHRONIZATION_TASK_CAPACITY];
unsigned order[IEC104_CLOCK_SYNCHRONIZATION_TASK_CAPACITY], count;
uint64_t serial;
bool in_use, in_action;
};
static IEC104_ClockSynchronization_t clock_pool[IEC104_CLOCK_SYNCHRONIZATION_CAPACITY];
static uint64_t next_serial;
static bool Owned(const IEC104_ClockSynchronization_t *self)
{
unsigned i;
for (i = 0U; i < IEC104_CLOCK_SYNCHRONIZATION_CAPACITY; ++i) {
if (self == &clock_pool[i]) { return self->in_use; }
}
return false;
}
/* 活动快照含自引用,任务永不搬移;仅压缩顺序索引。 */
static void Remove_Task(IEC104_ClockSynchronization_t *self, ClockTask *task)
{
unsigned index, slot = (unsigned)(task - self->tasks);
if (task->stations._owner != NULL) {
(void)IEC104_Slave_StationSnapshot_End(&task->stations);
} else if (task->station != NULL) {
(void)IEC104_Slave_Release_Station(self->slave, task->station);
}
for (index = 0U; index < self->count; ++index) {
if (self->order[index] == slot) {
memmove(self->order + index, self->order + index + 1U,
(self->count - index - 1U) * sizeof(self->order[0]));
--self->count;
break;
}
}
memset(task, 0, sizeof(*task));
}
static void Cancel(IEC104_ClockSynchronization_t *self)
{
while (self->count != 0U) {
++self->status.cancelled;
Remove_Task(self, &self->tasks[self->order[0]]);
}
}
static ClockTask *Allocate_Task(IEC104_ClockSynchronization_t *self)
{
unsigned slot;
for (slot = 0U; slot < IEC104_CLOCK_SYNCHRONIZATION_TASK_CAPACITY; ++slot) {
if (!self->tasks[slot].occupied) {
ClockTask *task = &self->tasks[slot];
memset(task, 0, sizeof(*task));
task->occupied = true;
self->order[self->count++] = slot;
return task;
}
}
return NULL;
}
static uint64_t Limit_Ms(void)
{
return ((uint64_t)IEC60870_TIME_MAX_SECONDS -
(uint64_t)IEC60870_LOCAL_UTC_OFFSET_MINUTES * 60U) * 1000U + 999U;
}
/* 只对完整时标进行对象解码;错误回复仍使用原文字节,不要求非法时标重新编码。 */
static bool Decode_Time(const IEC104_ClockSynchronization_t *self, const uint8_t *bytes,
IEC60870_CP56Time2a_t *time, uint64_t *utc_ms)
{
IEC60870_ClockSynchronizationCommand_t command;
IEC60870_WallTime_t local;
uint32_t seconds;
uint64_t converted;
if (IEC60870_ClockSynchronizationCommand_Get_From_Buffer(&command, 0U, bytes + 9U, 7U) !=
IEC60870_ASDU_OK ||
!IEC60870_Time_CP56_To_Seconds(&command.time, self->config.master_utc_offset_minutes, &seconds)) {
return false;
}
converted = (uint64_t)seconds * 1000U + command.time.time.ms;
if (converted > Limit_Ms() ||
!IEC60870_Time_Seconds_To_Local(seconds, command.time.time.ms, &local)) { return false; }
*time = command.time;
*utc_ms = converted;
return true;
}
/* 返回值与Slave保持一致;通知优先于超时和旧会话清理。 */
static IEC104_SlaveRequestState_t Check_Session(IEC104_ClockSynchronization_t *self)
{
IEC104_SlaveRequestState_t state = IEC104_Slave_Check_Request(self->slave, &self->session);
self->status.events_pending = state == IEC104_SLAVE_REQUEST_WAIT_EVENTS;
if (state == IEC104_SLAVE_REQUEST_ERROR ||
(state != IEC104_SLAVE_REQUEST_WAIT_EVENTS && (!self->status.active || state != IEC104_SLAVE_REQUEST_READY))) {
Cancel(self);
self->status.active = false;
return state == IEC104_SLAVE_REQUEST_ERROR ? state : IEC104_SLAVE_REQUEST_STALE;
}
return state;
}
static void Negative_Reply(ClockTask *task)
{
task->phase = CLOCK_REPLY;
task->cause = IEC60870_ASDU_COT_ACTIVATION_CON;
task->negative = true;
task->send_waiting = false;
}
/* 同一采样供到期判断与执行补偿使用;倒退显式归因,不依赖无符号下溢。 */
static uint32_t Remaining(const IEC104_ClockSynchronization_t *self, const ClockTask *task, uint64_t now)
{
if (now < task->accepted_ms || now - task->accepted_ms >= self->config.execute_timeout_ms) { return 0U; }
return self->config.execute_timeout_ms - (uint32_t)(now - task->accepted_ms);
}
static void Expire(IEC104_ClockSynchronization_t *self, uint64_t now)
{
unsigned index;
if (self->config.execute_timeout_ms == 0U) { return; }
for (index = 0U; index < self->count; ++index) {
ClockTask *task = &self->tasks[self->order[index]];
if (task->phase == CLOCK_EXECUTE && !task->executed && Remaining(self, task, now) == 0U) {
Negative_Reply(task);
++self->status.timeouts;
}
}
}
/* ERROR只有原会话正常STOP待通知时才让出;FATAL始终不可重试。 */
static bool Defer_For_Stop(IEC104_ClockSynchronization_t *self,
const IEC104_SlaveEventIdentity_t *identity, IEC104_TcpSubmitResult_t submitted)
{
IEC104_TcpServerStatus_t status;
if (submitted != IEC104_TCP_SUBMIT_ERROR || identity->slave != self->slave ||
identity->instance_id != self->session.instance_id || identity->generation != self->session.generation ||
IEC104_Slave_Get_Status(self->slave, &status, NULL) != IEC104_TCP_OK ||
status.generation != identity->generation || !status.connected || !status.connection_status_valid ||
!status.asdu_queue_status_valid || status.connection.phase != IEC104_TCP_SESSION_ACTIVE ||
status.connection.fault != IEC104_TCP_FAULT_NONE || status.connection.data_transfer_started ||
status.pending_event_count == 0U) { return false; }
self->status.events_pending = true;
return true;
}
/* 一步最多提交一个目标,0表示让出,1表示有进展,-1表示查询/组包失败。
* 每次先查会话,不能跨过发送入口顺带交付的新通知;任务头之外的应答不能抢先。 */
static int Submit_Head(IEC104_ClockSynchronization_t *self)
{
ClockTask *task;
IEC60870_Asdu_t reply;
IEC104_TcpSubmitResult_t submitted;
IEC104_SlaveRequestState_t state = Check_Session(self);
if (state == IEC104_SLAVE_REQUEST_ERROR) { return -1; }
if (state != IEC104_SLAVE_REQUEST_READY || self->count == 0U) { return 0; }
task = &self->tasks[self->order[0]];
if (task->phase != CLOCK_REPLY) { return 0; }
if (task->stations._owner != NULL) {
/* WAIT期间删除当前目标也须跳过;快照保护节点存储,不重新按CA绑定替代站。 */
if (task->station != NULL && !IEC104_Station_Is_Registered(task->station)) { task->station = NULL; }
if (task->station == NULL) { task->station = IEC104_Slave_StationSnapshot_Next(&task->stations); }
if (task->station == NULL) { Remove_Task(self, task); return 1; }
task->response_ca = IEC104_Station_Get_Common_Address(task->station);
}
if (IEC60870_Asdu_Create_From_Buffer(&reply, IEC104_Slave_Get_Application_Layer_Parameters(self->slave),
task->bytes, sizeof(task->bytes)) != IEC60870_ASDU_OK ||
IEC60870_Asdu_Set_COT(&reply, task->cause) != IEC60870_ASDU_OK ||
IEC60870_Asdu_Set_Negative(&reply, task->negative) != IEC60870_ASDU_OK ||
IEC60870_Asdu_Set_CA(&reply, task->response_ca) != IEC60870_ASDU_OK) { return -1; }
submitted = task->executed ? IEC104_Slave_Try_Send_Response(self->slave, &task->identity, &reply) :
IEC104_Slave_Send_Response(self->slave, &task->identity, &reply);
if (submitted == IEC104_TCP_SUBMIT_OK) {
++self->status.submitted;
if (task->negative) { ++self->status.negative_submitted; }
task->send_waiting = false;
if (task->stations._owner != NULL) { task->station = NULL; }
else { Remove_Task(self, task); }
return 1;
}
if (submitted == IEC104_TCP_SUBMIT_WAIT) {
++self->status.waits;
task->send_waiting = true;
return 0;
}
if (Defer_For_Stop(self, &task->identity, submitted)) { return 0; }
Cancel(self);
self->status.active = false;
return 0;
}
static IEC104_ClockSynchronizationEventResult_t Request(IEC104_ClockSynchronization_t *self,
const IEC104_SlaveEvent_t *event, const IEC104_SlaveEventIdentity_t *identity)
{
IEC60870_AsduInfo_t info;
IEC60870_CP56Time2a_t time;
IEC104_Station_t *station;
ClockTask *task;
const uint8_t *data;
uint16_t length;
uint32_t ioa;
uint64_t utc_ms;
uint8_t cause = 0U;
bool broadcast, valid_time;
if (!self->status.active || event->asdu_result != IEC60870_ASDU_OK ||
IEC60870_Asdu_Get_Data(&event->asdu, &data, &length) != IEC60870_ASDU_OK ||
length == 0U || data[0] != 103U ||
!IEC60870_Parse_ASDU_Header(event->asdu.parameters, data, length, &info)) {
return IEC104_CLOCK_SYNCHRONIZATION_EVENT_IGNORED;
}
if (info.object_count != 1U || info.information_objects_len < 10U) {
++self->status.rejected;
return IEC104_CLOCK_SYNCHRONIZATION_EVENT_HANDLED;
}
ioa = (uint32_t)data[6] | ((uint32_t)data[7] << 8) | ((uint32_t)data[8] << 16);
station = IEC104_Slave_Get_Station(self->slave, info.common_address);
broadcast = info.common_address == IEC104_CA_BROADCAST && IEC104_Slave_Get_Station_Count(self->slave) != 0U;
valid_time = Decode_Time(self, data, &time, &utc_ms);
if (info.cause_of_transmission != IEC60870_ASDU_COT_ACTIVATION) { cause = IEC60870_ASDU_COT_UNKNOWN_CAUSE; }
else if (!broadcast && station == NULL) { cause = IEC60870_ASDU_COT_UNKNOWN_COMMON_ADDRESS; }
else if (ioa != 0U) { cause = IEC60870_ASDU_COT_UNKNOWN_IOA; }
else if (info.is_sequence || length != 16U || info.is_negative_confirm || info.is_test || !valid_time) {
cause = IEC60870_ASDU_COT_ACTIVATION_CON;
}
if (cause != 0U) { ++self->status.rejected; }
if (self->count == IEC104_CLOCK_SYNCHRONIZATION_TASK_CAPACITY) {
++self->status.overloads;
++self->status.dropped_requests;
return IEC104_CLOCK_SYNCHRONIZATION_EVENT_HANDLED;
}
task = Allocate_Task(self);
if (task == NULL) { return IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR; }
task->identity = *identity;
memcpy(task->bytes, data, sizeof(task->bytes));
task->bytes[1] = 1U;
task->response_ca = info.common_address;
task->receive_serial = event->transport.data.i_frame.receive_serial;
task->accepted_ms = IEC104_Net_Now_Ms();
task->phase = cause == 0U ? CLOCK_EXECUTE : CLOCK_REPLY;
task->cause = cause;
task->negative = cause != 0U;
/* 无站广播没有快照,按COT优先级用原CA单帧回45或46。 */
if (broadcast) {
if (IEC104_Slave_StationSnapshot_Begin(self->slave, &task->stations) != IEC104_STATION_OK) {
Remove_Task(self, task);
return IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR;
}
} else if (cause == 0U) {
if (IEC104_Slave_Acquire_Station(self->slave, station) != IEC104_STATION_OK) {
Remove_Task(self, task);
return IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR;
}
task->station = station;
}
if (cause == 0U) { ++self->status.accepted; }
if (cause != 0U && !broadcast && self->count == 1U && Submit_Head(self) < 0) {
return IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR;
}
return IEC104_CLOCK_SYNCHRONIZATION_EVENT_HANDLED;
}
void IEC104_ClockSynchronization_Config_Default(IEC104_ClockSynchronizationConfig_t *config)
{
if (config != NULL) {
memset(config, 0, sizeof(*config));
config->master_utc_offset_minutes = IEC60870_LOCAL_UTC_OFFSET_MINUTES;
config->execute_timeout_ms = 15000U;
}
}
IEC104_ClockSynchronization_t *IEC104_ClockSynchronization_Create(IEC104_Slave_t *slave,
const IEC104_ClockSynchronizationConfig_t *config)
{
unsigned i;
IEC104_ClockSynchronization_t *free_slot = NULL;
if (config == NULL || config->master_utc_offset_minutes < -1439 ||
config->master_utc_offset_minutes > 1439 || next_serial == UINT64_MAX ||
IEC104_Slave_Get_Application_Layer_Parameters(slave) == NULL) { return NULL; }
for (i = 0U; i < IEC104_CLOCK_SYNCHRONIZATION_CAPACITY; ++i) {
if (clock_pool[i].in_use && clock_pool[i].slave == slave) { return NULL; }
if (!clock_pool[i].in_use && free_slot == NULL) { free_slot = &clock_pool[i]; }
}
if (free_slot == NULL) { return NULL; }
memset(free_slot, 0, sizeof(*free_slot));
free_slot->slave = slave;
free_slot->config = *config;
free_slot->serial = ++next_serial;
free_slot->in_use = true;
return free_slot;
}
bool IEC104_ClockSynchronization_Destroy(IEC104_ClockSynchronization_t *self)
{
if (!Owned(self) || self->in_action || self->count != 0U) { return false; }
memset(self, 0, sizeof(*self));
return true;
}
void IEC104_ClockSynchronization_Cancel(IEC104_ClockSynchronization_t *self)
{
if (Owned(self) && !self->in_action) { Cancel(self); }
}
IEC104_ClockSynchronizationEventResult_t IEC104_ClockSynchronization_Handle_Event(
IEC104_ClockSynchronization_t *self, const IEC104_SlaveEvent_t *event)
{
IEC104_SlaveEventIdentity_t identity;
if (!Owned(self) || self->in_action ||
!IEC104_Slave_Get_Event_Identity(self->slave, event, &identity) ||
(self->session.instance_id != 0U && self->session.instance_id != identity.instance_id)) {
return IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR;
}
self->session.slave = identity.slave;
self->session.instance_id = identity.instance_id;
if (event->transport.type == IEC104_TCP_EVENT_CONNECTED) {
Cancel(self);
self->session.generation = identity.generation;
self->status.active = false;
return IEC104_CLOCK_SYNCHRONIZATION_EVENT_HANDLED;
}
if (identity.generation != self->session.generation) { return IEC104_CLOCK_SYNCHRONIZATION_EVENT_IGNORED; }
switch (event->transport.type) {
case IEC104_TCP_EVENT_ACTIVATED:
self->status.active = true;
return IEC104_CLOCK_SYNCHRONIZATION_EVENT_HANDLED;
case IEC104_TCP_EVENT_DEACTIVATED:
case IEC104_TCP_EVENT_DISCONNECTED:
Cancel(self);
self->status.active = false;
if (event->transport.type == IEC104_TCP_EVENT_DISCONNECTED) { self->session.generation = 0U; }
return IEC104_CLOCK_SYNCHRONIZATION_EVENT_HANDLED;
case IEC104_TCP_EVENT_I_FRAME: return Request(self, event, &identity);
default: return IEC104_CLOCK_SYNCHRONIZATION_EVENT_IGNORED;
}
}
bool IEC104_ClockSynchronization_Process(IEC104_ClockSynchronization_t *self)
{
unsigned step;
if (!Owned(self) || self->in_action) { return false; }
for (step = 0U; step < 2U * IEC104_CLOCK_SYNCHRONIZATION_TASK_CAPACITY; ++step) {
IEC104_SlaveRequestState_t state = Check_Session(self);
int submitted;
if (state == IEC104_SLAVE_REQUEST_ERROR) { return false; }
if (state != IEC104_SLAVE_REQUEST_READY || self->count == 0U) { return true; }
Expire(self, IEC104_Net_Now_Ms());
if (self->tasks[self->order[0]].phase == CLOCK_EXECUTE) { return true; }
submitted = Submit_Head(self);
if (submitted <= 0) { return submitted == 0; }
}
return true;
}
IEC104_ClockSynchronizationExecuteResult_t IEC104_ClockSynchronization_Execute_Next(
IEC104_ClockSynchronization_t *self, IEC104_ClockSynchronizationAction_t action, void *context)
{
unsigned index;
if (!Owned(self) || self->in_action || action == NULL) { return IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_ERROR; }
for (;;) {
IEC104_ClockSynchronizationRequest_t request;
IEC104_SlaveRequestState_t state;
ClockTask *task = NULL;
uint64_t now, utc_ms;
bool synchronized;
for (index = 0U; index < self->count; ++index) {
if (self->tasks[self->order[index]].phase == CLOCK_EXECUTE) {
task = &self->tasks[self->order[index]];
break;
}
}
if (task == NULL) { return IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY; }
state = Check_Session(self);
if (state == IEC104_SLAVE_REQUEST_ERROR) { return IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_ERROR; }
if (state == IEC104_SLAVE_REQUEST_WAIT_EVENTS) { return IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_WAIT; }
if (state != IEC104_SLAVE_REQUEST_READY) { return IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY; }
/* 期限与设时基准使用同一采样;到期转换保持原槽及顺序,不调用应用。 */
now = IEC104_Net_Now_Ms();
Expire(self, now);
if (task->phase != CLOCK_EXECUTE) { continue; }
memset(&request, 0, sizeof(request));
if (now < task->accepted_ms || !Decode_Time(self, task->bytes, &request.master_time, &utc_ms) ||
now - task->accepted_ms > Limit_Ms() - utc_ms) {
Negative_Reply(task);
++self->status.time_errors;
continue;
}
utc_ms += now - task->accepted_ms;
if (!IEC60870_Time_Seconds_To_Local((uint32_t)(utc_ms / 1000U),
(uint16_t)(utc_ms % 1000U), &request.local_time)) {
Negative_Reply(task);
++self->status.time_errors;
continue;
}
request.generation = task->identity.generation;
request.receive_serial = task->receive_serial;
request.station_id = task->stations._owner != NULL ? 0U : IEC104_Station_Get_Id(task->station);
request.common_address = (uint16_t)((uint16_t)task->bytes[4] | ((uint16_t)task->bytes[5] << 8));
request.originator_address = task->bytes[3];
request.utc_ms = utc_ms;
request.accepted_ms = task->accepted_ms;
request.base_ms = now;
/* 先消费,再开放只读查询:回调内已不计执行期限,且confirm_pending为true。
* in_action阻止递归执行下一槽或释放当前槽,回调返回后不重新判定业务超时。 */
task->executed = true;
task->phase = CLOCK_REPLY;
task->cause = IEC60870_ASDU_COT_ACTIVATION_CON;
task->send_waiting = false;
self->in_action = true;
synchronized = action(context, &request);
self->in_action = false;
task->negative = !synchronized;
++self->status.executed;
if (synchronized) { ++self->status.synchronized; }
/* 动作结果与传输结果分开:此后无论等待、STOP或故障均不会重复动作。 */
if (&self->tasks[self->order[0]] == task) { (void)Submit_Head(self); }
return IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_OK;
}
}
bool IEC104_ClockSynchronization_Get_Status(const IEC104_ClockSynchronization_t *self,
IEC104_ClockSynchronizationStatus_t *status)
{
IEC104_SlaveRequestState_t state;
uint64_t now;
unsigned index;
if (!Owned(self) || status == NULL) { return false; }
state = IEC104_Slave_Check_Request(self->slave, &self->session);
if (state == IEC104_SLAVE_REQUEST_ERROR) { return false; }
*status = self->status;
status->generation = self->session.generation;
status->pending_requests = self->count;
status->events_pending = state == IEC104_SLAVE_REQUEST_WAIT_EVENTS;
status->execute_pending = false;
status->confirm_pending = false;
status->timer_active = false;
status->timeout_remaining_ms = 0U;
status->need_process = false;
now = IEC104_Net_Now_Ms();
for (index = 0U; index < self->count; ++index) {
const ClockTask *task = &self->tasks[self->order[index]];
if (task->executed) { status->confirm_pending = true; }
else if (task->phase == CLOCK_EXECUTE) {
status->execute_pending = true;
if (self->config.execute_timeout_ms != 0U) {
uint32_t remaining = Remaining(self, task, now);
if (!status->timer_active || remaining < status->timeout_remaining_ms) {
status->timer_active = true;
status->timeout_remaining_ms = remaining;
}
}
}
}
if (self->count != 0U && !self->in_action && !status->events_pending) {
const ClockTask *head = &self->tasks[self->order[0]];
status->need_process = state != IEC104_SLAVE_REQUEST_READY || !status->active ||
(status->timer_active && status->timeout_remaining_ms == 0U) ||
(head->phase == CLOCK_REPLY && !head->send_waiting);
}
return true;
}
@@ -0,0 +1,102 @@
/*
* 104时钟同步(C_CS_NA_1):校验、换算、站内滞留补偿和逐站确认。
* Handle_Event/Process不调用应用代码;动作仅由Execute_Next同步执行一次,不保存回调。
* 所有接口与Slave在同一线程串行调用;模块内应答严格按请求顺序。
* 执行后的ACT_CON使用Try路径,不越过HIGH/NORMAL待发原文;确认只回显请求时标。
* 退出顺序:Cancel -> Slave_Close -> 读空并分派通知 -> Destroy -> Remove_Station -> Slave_Destroy。
*/
#ifndef IEC104_CLOCK_SYNCHRONIZATION_H
#define IEC104_CLOCK_SYNCHRONIZATION_H
#include "IEC104_Slave.h"
#include "IEC60870_Time.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct IEC104_ClockSynchronization IEC104_ClockSynchronization_t;
typedef struct {
int16_t master_utc_offset_minutes; /* 主站标准时区,-1439..1439,不含SU;默认480。 */
uint32_t execute_timeout_ms; /* 受理至执行检查,默认15000;0不关闭时间范围检查。 */
} IEC104_ClockSynchronizationConfig_t;
/* 纯值请求,仅在动作回调期间借用。base_ms为执行检查基准,不是硬件生效时刻。
* local_time已换算为固定UTC+8并补偿受理至base_ms的等待;不补偿网络、FIFO及硬件写入耗时。 */
typedef struct {
uint64_t generation, receive_serial;
uint64_t station_id; /* 定向请求的原注册编号;广播为0。 */
uint16_t common_address;
uint8_t originator_address;
IEC60870_CP56Time2a_t master_time; /* 原报文解码值,IV=0,保留SU。 */
IEC60870_WallTime_t local_time;
uint64_t utc_ms;
uint64_t accepted_ms, base_ms;
} IEC104_ClockSynchronizationRequest_t;
/* true报告成功,false未报告成功;false不保证时钟未变,也不提供回滚。
* 同步短时完成,权限及回拨限制由应用判断。不得重入本模块或推进Slave事件循环;
* 本模块仅Get_Status允许在回调内调用。每条有效新请求都会执行,不按内容去重。 */
typedef bool (*IEC104_ClockSynchronizationAction_t)(void *context,
const IEC104_ClockSynchronizationRequest_t *request);
typedef enum {
IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR = -1,
IEC104_CLOCK_SYNCHRONIZATION_EVENT_IGNORED = 0,
IEC104_CLOCK_SYNCHRONIZATION_EVENT_HANDLED = 1 /* 含拒绝和丢新,不表示已应答。 */
} IEC104_ClockSynchronizationEventResult_t;
typedef enum {
IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_ERROR = -1,
IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY = 0, /* 无可执行请求;仍可能有待提交应答。 */
IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_WAIT = 1, /* 先处理未读Slave通知。 */
IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_OK = 2 /* 已调用一次动作;确认可能因后续STOP/故障取消。 */
} IEC104_ClockSynchronizationExecuteResult_t;
typedef struct {
uint64_t generation; /* 已消费的会话代次;断线后为0。 */
bool active;
bool events_pending;
bool execute_pending;
bool confirm_pending; /* 含正在执行的动作;发布方据此协调带时标事件的发布。 */
bool need_process;
bool timer_active;
uint32_t timeout_remaining_ms;
unsigned pending_requests;
unsigned long accepted, executed, synchronized;
unsigned long submitted, negative_submitted;
unsigned long rejected, waits, cancelled, overloads, dropped_requests, timeouts, time_errors;
} IEC104_ClockSynchronizationStatus_t;
void IEC104_ClockSynchronization_Config_Default(IEC104_ClockSynchronizationConfig_t *config);
/* 复制配置、借用Slave;每服务一个实例,在消费通知前创建,只按共用站点表路由。
* 静态池宏IEC104_CLOCK_SYNCHRONIZATION_CAPACITY默认等于TCP服务容量,TASK_CAPACITY默认4。 */
IEC104_ClockSynchronization_t *IEC104_ClockSynchronization_Create(IEC104_Slave_t *slave,
const IEC104_ClockSynchronizationConfig_t *config);
/* 有任务或处于动作回调中返回false;不隐式Cancel。 */
bool IEC104_ClockSynchronization_Destroy(IEC104_ClockSynchronization_t *self);
/* 仅取消模块尚未交接的任务,不撤回已编号帧、不回滚时钟;回调中无操作。 */
void IEC104_ClockSynchronization_Cancel(IEC104_ClockSynchronization_t *self);
/* 按Slave FIFO分派所有连接/启停通知。非103返回IGNORED;原事件不修改。
* COT/CA/IOA优先45/46/47,其余可对应的非法请求否定7;满槽丢新且不断链。
* 广播动作一次、按快照逐站确认;被删除的未提交目标跳过,新注册站不参与。 */
IEC104_ClockSynchronizationEventResult_t IEC104_ClockSynchronization_Handle_Event(
IEC104_ClockSynchronization_t *self, const IEC104_SlaveEvent_t *event);
/* 有界推进回复和执行期限;不执行动作。每轮网络推进、通知读空后仍须调用,
* 不能因need_process为false跳过WAIT重试;参数、重入、查询或组包错误返回false。
* 发送ERROR/FATAL取消任务后返回true;发送故障仍须由应用继续消费Slave断线通知。 */
bool IEC104_ClockSynchronization_Process(IEC104_ClockSynchronization_t *self);
/* 检查会话、期限及补偿范围后消费最早EXECUTE;可越过较早REPLY执行,但回复不插队。
* WAIT/EMPTY不调用动作,OK不表示动作成功或确认已收到;动作之后不再判业务超时。 */
IEC104_ClockSynchronizationExecuteResult_t IEC104_ClockSynchronization_Execute_Next(
IEC104_ClockSynchronization_t *self, IEC104_ClockSynchronizationAction_t action, void *context);
/* 只读,包括回调期间;执行中任务不计执行期限,confirm_pending为true。
* Try等待当前两级队列排空,持续发布可能推迟确认,需发布方串行协调。 */
bool IEC104_ClockSynchronization_Get_Status(const IEC104_ClockSynchronization_t *self,
IEC104_ClockSynchronizationStatus_t *status);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_CLOCK_SYNCHRONIZATION_H */
@@ -0,0 +1,54 @@
Clock synchronization implementation acceptance, 2026-10-29
Scope: IEC60870-Slave only. Original interrogation engine, Slave, transport,
common sources, and test/iec104_slave_test.c are byte-identical to before/.
No commit, MCU/Keil build, hardware test, or system-clock change was performed.
Toolchain: GCC 16.1.0, x86_64-w64-mingw32.
Flags: -std=c11 -O2 -Wall -Wextra -Werror.
Final command: ./scripts/build.ps1 -Test
Example executable: built without compiler diagnostics.
Slave tests: 32 groups PASS, each of capacities 1 and 2.
Example/business tests: 130 groups PASS, each of capacities 1 and 2.
98 general + 12 counter + 4 fallback + 16 clock groups.
Interrogation rules: 3 groups PASS.
Script exit was 1 because the sandbox denied Node spawnSync (EPERM).
Final authorized rerun outside the sandbox:
node test/slave_example_loopback_test.cjs .build/slave-example/iec104_slave_example.exe
Exit 0, real TCP loopback PASS.
Covers 106+102 fallback, directed clock echo, TEST, COT=5, broadcast clock,
103+102 batching, existing interrogation/counter flows, STOP/START,
reconnection and cleanup. STOP statistics checked:
submitted=65, clock_executed=3, clock_synchronized=3, clock_submitted=5.
Clock regression coverage includes explicit once-only execution, callback
reentry guards, timezone/SU conversion, dwell compensation, 1999/2099 bounds,
monotonic rollback, deadline checks in both execution and processing,
malformed requests, FIFO/full-slot drops, Type 30 NORMAL ordering, HIGH-full
retry, window/send backpressure, STOP during submission, already-numbered
confirmation completion after STOP, broadcast snapshots and cancellation,
station deletion/replacement, service isolation, and independent retransmits.
Storage probe (size_probe.c/exe), bytes, default one-service configuration:
ClockTask: 104
ClockSynchronization instance with four task slots: 568
Request: 80
Status: 72
Clock instance pool: 568
GCC -O2 -fstack-usage, individual x64 stack frames, bytes:
Handle_Event: 256
Execute_Next: 240
Submit_Head: 224
Process: 64
Get_Status: 80
Example_Clock_Action: 48
Example main: 768 before, 800 after
Evidence: clock.su, example-before.su, example-after.su.
These are individual host ABI frames, not worst-case call chains or MCU sizes.
Final checks: modified/new text is valid UTF-8 with no U+FFFD; existing BOM
presence preserved; git diff --check passed (only existing CRLF notices).
Documentation local links/anchors were checked. Baseline is kept in before/.
@@ -0,0 +1,169 @@
/**
* IEC 60870-5-104旧单路接收兼容层。
*
* 逐字节成帧后调用Codec解析APCI,再使用私有链路状态检查I/S序号。
* 编解码独立实现在IEC104_Codec.c;本入口仍保留单路全局状态。
*/
#include "IEC104.h"
#include "IEC104_Link_Common.h"
#include "IEC104_Log.h"
/* 当前单路接收解析器;静态存储期保证启动时处于RX_SYNC且计数为零。 */
static IEC104_RxParser_t g_rx_parser;
/*
* 旧单路收帧入口只用于成帧和I/S序号检查,不分派U帧启停流程;
* 因此兼容路径预设数据传输已启动。完整连接应显式持有自己的状态。
*/
static IEC104_LinkState_t g_link_state = {
.role = IEC104_ROLE_CONTROLLED,
.dt_state = IEC104_DT_STARTED
};
/**
* 功能:内部复位当前APDU接收进度,重新进入等待起始字节状态。
* 输入参数:无。
* 输出参数:无。
* 返回值:无。
*/
static void Rx_Reset_State(void)
{
g_rx_parser.stage = RX_SYNC;
g_rx_parser.len = 0;
g_rx_parser.expected_len = 0;
}
/**
* 功能:公开复位接收状态机,丢弃当前未完成APDU。
* 输入参数:无。
* 输出参数:无;内部接收阶段恢复同步搜索,已收长度和预期长度清零。
* 返回值:无。
* 说明:不清零接收缓冲区,不修改旧单路入口的链路序号状态。
*/
void IEC104_Rx_Reset(void)
{
Rx_Reset_State();
}
/**
* 功能:复位旧单路成帧入口的I/S序号状态,保留当前APDU接收进度。
* 输入参数:无。
* 输出参数:无;内部g_link_state重新初始化为被控站,并恢复STARTED预设。
* 返回值:无。
* 说明:仅影响IEC104_Rx_Feed_Byte使用的私有全局状态。外部连接的独立状态对象
* 使用IEC104_Link_State_Reset复位,该接口恢复STOPPED;旧会话结束后再复位。
*/
void IEC104_Legacy_Link_Reset(void)
{
/* 兼容入口不处理STARTDT U帧,复位后仍预设已进入数据传输阶段。 */
(void)IEC104_Link_State_Init(
&g_link_state, IEC104_ROLE_CONTROLLED);
g_link_state.dt_state = IEC104_DT_STARTED;
}
/**
* 功能:检查已收齐APDU的APCI及适用的I/S序号,再复位接收进度;不解析ASDU内容。
* 输入参数:无。
* 输出参数:无。
* 返回值:返回完整帧、帧格式错误或链路序号错误对应的接收结果。
*/
static IEC104_RxResult_t Rx_Finish_Frame(void)
{
IEC104_ApciInfo_t apci;
uint16_t frame_len = g_rx_parser.len;
if (!IEC104_Parse_APCI(g_rx_parser.buf, frame_len, &apci)) {
IEC104_LOG_WARN("[IEC104] invalid APCI frame; receiver reset");
Rx_Reset_State();
return IEC104_RX_FRAME_ERROR;
}
/* I帧检查N(S)连续性和N(R),S帧只检查N(R);U帧不使用序号。 */
if ((apci.frame_type == IEC104_FRAME_TYPE_I &&
IEC104_Link_Handle_I_Frame(&g_link_state, &apci) ==
IEC104_LINK_RESULT_FATAL) ||
(apci.frame_type == IEC104_FRAME_TYPE_S &&
IEC104_Link_Handle_S_Frame(&g_link_state, &apci) ==
IEC104_LINK_RESULT_FATAL)) {
IEC104_LOG_WARN("[IEC104] invalid I/S frame sequence; receiver reset");
Rx_Reset_State();
return IEC104_RX_SEQUENCE_ERROR;
}
/* 仅复位成帧进度,已通过检查的链路序号必须保留给下一帧。 */
IEC104_LOG_DEBUG("[IEC104] RX frame ready: type=%d, len=%u",
(int)apci.frame_type, (unsigned)frame_len);
Rx_Reset_State();
return IEC104_RX_FRAME_READY;
}
/**
* 功能:把一个字节送入IEC-104接收状态机。
* 输入参数:byte——当前收到的一个字节。
* 输出参数:无。
* 返回值:返回IEC104_RxResult_t,用于区分未收齐、完整帧和各类错误。
*/
IEC104_RxResult_t IEC104_Rx_Feed_Byte(uint8_t byte)
{
switch (g_rx_parser.stage) {
case RX_SYNC:
/* 同步阶段:丢弃所有非0x68字节,直到找到APDU起始字节。 */
if (byte == IEC104_START_BYTE) {
g_rx_parser.buf[0] = byte;
g_rx_parser.len = 1;
g_rx_parser.stage = RX_LEN;
}
return IEC104_RX_IN_PROGRESS;
case RX_LEN:
/* RX_LEN 必须由 RX_SYNC 收到 0x68 后进入。 */
if (g_rx_parser.len != 1U ||
g_rx_parser.buf[0] != IEC104_START_BYTE) {
IEC104_LOG_ERR("[IEC104] inconsistent RX_LEN state; receiver reset");
Rx_Reset_State();
return IEC104_RX_INTERNAL_ERROR;
}
g_rx_parser.buf[1] = byte;
g_rx_parser.len = 2;
/* 长度域包含 4 字节控制域和 ASDU,协议允许范围为 4..253。 */
if (byte < IEC104_MIN_LENGTH_FIELD ||
byte > IEC104_MAX_LENGTH_FIELD) {
IEC104_LOG_WARN("[IEC104] invalid length %u; valid range is %u..%u; receiver reset",
(unsigned)byte,
(unsigned)IEC104_MIN_LENGTH_FIELD,
(unsigned)IEC104_MAX_LENGTH_FIELD);
Rx_Reset_State();
return IEC104_RX_FRAME_ERROR;
}
g_rx_parser.expected_len = (uint16_t)(2 + byte);
g_rx_parser.stage = RX_BODY;
return IEC104_RX_IN_PROGRESS;
case RX_BODY:
/* 写缓冲区前检查内部状态,防止状态损坏造成越界写入。 */
if (g_rx_parser.len >= (uint16_t)sizeof(g_rx_parser.buf)) {
IEC104_LOG_ERR("[IEC104] invalid RX_BODY bounds: len=%u, expected_len=%u, buffer_capacity=%u; receiver reset",
(unsigned)g_rx_parser.len,
(unsigned)g_rx_parser.expected_len,
(unsigned)sizeof(g_rx_parser.buf));
Rx_Reset_State();
return IEC104_RX_INTERNAL_ERROR;
}
/* 报文体阶段:按长度域给出的目标长度逐字节收集APDU。 */
g_rx_parser.buf[g_rx_parser.len++] = byte;
if (g_rx_parser.len == g_rx_parser.expected_len)
return Rx_Finish_Frame();
return IEC104_RX_IN_PROGRESS;
default:
/* 理论上不会进入此分支;若状态被破坏,则记录错误并自恢复。 */
IEC104_LOG_ERR("[IEC104] invalid parser state; receiver reset");
Rx_Reset_State();
return IEC104_RX_INTERNAL_ERROR;
}
}
@@ -0,0 +1,72 @@
/* IEC 60870-5-104旧单路接收入口;包含Codec以兼容原有编解码调用方。 */
#ifndef IEC104_H
#define IEC104_H
#include "IEC104_Codec.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 接收状态机的三个阶段。 */
typedef enum {
RX_SYNC = 0, /* 等待起始字节 0x68 */
RX_LEN, /* 等待长度域 */
RX_BODY /* 按目标长度接收控制域和 ASDU */
} IEC104_RxStage_t;
/* 逐字节接收结果;负值表示已经完成错误分类。 */
typedef enum {
IEC104_RX_INTERNAL_ERROR = -3, /* 接收器内部状态异常 */
IEC104_RX_SEQUENCE_ERROR = -2, /* I/S帧序号非法,应断开当前连接 */
IEC104_RX_FRAME_ERROR = -1, /* 长度或APCI格式非法 */
IEC104_RX_IN_PROGRESS = 0, /* 当前APDU尚未收齐 */
IEC104_RX_FRAME_READY = 1 /* 完整APDU通过APCI检查及适用的I/S序号检查;未校验ASDU内容 */
} IEC104_RxResult_t;
/* 单路字节流接收器的全部解析状态。 */
typedef struct {
uint8_t buf[IEC104_MAX_APDU_LEN]; /* 当前APDU的接收缓冲区 */
uint16_t len; /* 已经收到的字节数 */
uint16_t expected_len; /* 根据长度域计算出的完整APDU长度 */
IEC104_RxStage_t stage; /* 当前状态机阶段 */
} IEC104_RxParser_t;
/**
* 功能:向IEC-104接收状态机递入一个字节。
* 输入参数:byte——当前收到的一个字节。
* 输出参数:无。
* 返回值:返回IEC104_RxResult_t;序号错误要求调用方停止处理并断开当前连接。
* 说明:该旧单路兼容入口只返回接收结果,不向调用方输出完整报文或ASDU内容。
* 不分派STARTDT/STOPDT U帧,因此预设已启动
* 数据传输,只对I/S帧执行序号检查。完整连接应使用独立的
* IEC104_LinkState_t并显式处理启停流程。
*/
extern IEC104_RxResult_t IEC104_Rx_Feed_Byte(uint8_t byte);
/**
* 功能:丢弃当前未完成APDU并复位接收进度。
* 输入参数:无。
* 输出参数:无。
* 返回值:无。
* 说明:只重置内部接收阶段和长度,不清零接收缓冲区,不修改链路序号状态。
*/
extern void IEC104_Rx_Reset(void);
/**
* 功能:复位旧单路兼容入口的I/S序号状态。
* 输入参数:无。
* 输出参数:无。
* 返回值:无。
* 说明:清零I/S序号并恢复兼容路径的STARTED预设,不改变当前
* APDU接收进度;仅应在旧会话已经结束时调用。
* 仅影响IEC104_Rx_Feed_Byte的私有全局状态;外部独立的IEC104_LinkState_t
* 应使用IEC104_Link_State_Reset复位,后者恢复STOPPED。
*/
extern void IEC104_Legacy_Link_Reset(void);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_H */
@@ -0,0 +1,233 @@
/* 子站两级变长ASDU字节环:借用调用方存储,不动态分配;HIGH满时拒绝,NORMAL覆盖最旧原文。 */
#ifndef IEC104_ASDUQUEUE_H
#define IEC104_ASDUQUEUE_H
#include "IEC104_Codec.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 区分条目头:HIGH只有uint16长度;NORMAL保存编号、状态及长度。
* NORMAL通过memcpy存取,可跨尾部且不要求环内地址对齐;布局仅供本次构建使用,不能持久化。
* 此类型公开用于容量计算,调用方不得把字节环强转为结构体指针。 */
typedef struct {
uint64_t entry_serial;
unsigned int entry_state : 2; /* 头内编码:1队列待发,2已登记发送交接且保留待ACK;0/3无效 */
unsigned int asdu_length : 8; /* 6..249 */
} IEC104_AsduNormalEntryHeader_t;
#define IEC104_ASDU_HIGH_ENTRY_HEADER_LEN ((uint32_t)sizeof(uint16_t))
#define IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN ((uint32_t)sizeof(IEC104_AsduNormalEntryHeader_t))
#define IEC104_ASDU_HIGH_QUEUE_MIN_CAPACITY (IEC104_ASDU_HIGH_ENTRY_HEADER_LEN + IEC104_ASDU_HEADER_LEN)
#define IEC104_ASDU_NORMAL_QUEUE_MIN_CAPACITY (IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN + IEC104_ASDU_HEADER_LEN)
typedef enum {
IEC104_ASDU_QUEUE_ERROR = -1, /* 条目超过桶总容量,或参数、代次、状态非法 */
IEC104_ASDU_QUEUE_FULL = 0, /* HIGH空闲字节不足,或NORMAL被旧条目占住编号窗口 */
IEC104_ASDU_QUEUE_OK = 1, /* 本次操作成功,查询输出有效 */
IEC104_ASDU_QUEUE_EMPTY = 2 /* 无匹配条目,不代表操作成功 */
} IEC104_AsduQueueResult_t;
typedef struct {
uint64_t connection_generation; /* 队列绑定代次;0表示离线NORMAL,Detach/Rebind后旧身份失效 */
uint64_t entry_serial; /* NORMAL为1..UINT64_MAX循环编号;HIGH固定0,无条目身份 */
} IEC104_AsduEntryId_t;
/* 条目逻辑连续,头和原文均可跨数组尾部。
* 队列待发区是尚未完成Mark_Submitted记账的条目;unsubmitted_*沿用此接口命名。
* HIGH在发送模块接手后直接出队,NORMAL另有已登记交接、保留原文待ACK的前缀。 */
typedef struct {
uint8_t *bytes; /* 调用方持有的字节数组,模块只借用 */
uint32_t byte_capacity; /* 数组字节容量,至少对应优先级的MIN_CAPACITY */
uint32_t head_index; /* 最旧保留条目头的字节下标;是否为空由entry_count决定 */
uint32_t tail_index; /* 下一次追加位置,直接从这里跨尾部写入 */
uint32_t used_bytes; /* 所有保留条目的本级条目头+ASDU实际长度之和 */
uint32_t entry_count; /* 原文仍保留的ASDU条目数;HIGH只有待发区,NORMAL另含待ACK前缀 */
uint32_t unsubmitted_index; /* 队列待发区首条头的字节下标;待发区为空时等于tail_index */
uint32_t unsubmitted_count; /* 尚未完成队列交接记账的条目数,不表示SendBuffer中的待发帧数 */
} IEC104_AsduPriorityQueue_t;
/*
* overwritten_unacked表示NORMAL原文已覆盖、对应I帧仍未被有效ACK确认;具体发送身份由Manager记录。
* 区间从overwritten_unacked_serial_begin沿编号递增方向到overwritten_unacked_serial_end,包含两端。
* 仅NORMAL使用条目编号,丢弃未提交条目等操作可造成编号间隔;ACK推进后的下界不一定对应实际条目。
* 待ACK计数为0时两端均为0;非空区间和保留条目共同受半环编号窗口约束。
*/
typedef struct {
IEC104_AsduPriorityQueue_t priority_queues[IEC104_ASDU_PRIORITY_COUNT]; /* HIGH/NORMAL两个独立字节环 */
uint64_t connection_generation; /* 当前绑定代次;0为服务离线缓存,只允许NORMAL待发原文 */
uint64_t next_entry_serial; /* 仅NORMAL分配;UINT64_MAX之后回到1,Rebind不重置 */
uint64_t overwritten_unacked_serial_end; /* 待确认循环区间的结束边界,含端点;计数为0时为0 */
uint64_t overwritten_unacked_serial_begin; /* 待确认循环区间的起始边界,含端点;ACK后推进,计数为0时为0 */
uint32_t overwritten_unacked_count; /* NORMAL原文已覆盖、已提交且待确认的ASDU条目数 */
uint8_t normal_send_limited; /* 0/1:EOF后停止选取全部未提交NORMAL;Detach/Rebind解除 */
uint8_t is_initialized; /* 1表示已初始化,不表示TCP存活或可发送 */
} IEC104_AsduQueue_t;
/*
* 只读ASDU条目状态快照,各count均以ASDU条目为单位;本队列保存原文,SendBuffer仅保存当前待完成的I帧APDU。
* 队列“已提交”表示发送模块接手后又完成了Mark_Submitted记账,不表示整帧已写完。
* 顺序是:SendBuffer持有完整I帧 -> Manager建立记录 -> Mark_Submitted更新队列。
* 最后一步执行前,队列仍保留旧的待发状态;所有步骤串行,不得插入其他队列修改。
* 待确认表示对应I帧尚未收到有效ACK,涵盖尚未写出、部分写出及整帧写完后的等待阶段。
* HIGH在交接记账时消费原文,其retained_unacked_count恒为0;未确认HIGH数量由Manager发送记录维护。
* NORMAL的retained_unacked_count加overwritten_unacked_count等于NORMAL已提交待确认条目数。
* is_storage_empty只说明原文存储为空,HIGH或NORMAL仍可能有对应I帧等待ACK。
*/
typedef struct {
uint64_t connection_generation; /* 查询时的绑定代次;0为未绑定连接的服务队列 */
uint32_t entry_count[IEC104_ASDU_PRIORITY_COUNT]; /* 各优先级仍保留原文的ASDU总条目数,包含未提交条目 */
uint32_t retained_unacked_count[IEC104_ASDU_PRIORITY_COUNT]; /* 原文仍保留的已提交待确认ASDU条目数;HIGH恒为0 */
uint32_t overwritten_unacked_count; /* NORMAL原文已覆盖、已提交且待确认的ASDU条目数 */
uint8_t is_storage_empty; /* 两级ASDU原文环均为空,不表示所有对应I帧均已确认 */
} IEC104_AsduQueueStatus_t;
/*
* ASDU原文最多分两段借用,先first后second;总长度为asdu_length。
* asdu_second==NULL时全部位于第一段;否则第二段长度=asdu_length-asdu_first_length。
* 本条目被覆盖、确认释放、Remove或HIGH提交后借用失效;Reset_High使HIGH借用失效。
* Detach/Rebind/Abort/Init使所有借用失效。
* NORMAL再次Enqueue可能覆盖旧借用;需要连续副本时,在借用仍有效期间使用Copy_Asdu。
*/
typedef struct {
IEC104_AsduEntryId_t entry_id; /* NORMAL核对完整身份;HIGH仅有当前代次,entry_serial为0 */
IEC104_AsduPriority_t priority; /* 条目所属字节环 */
const uint8_t *asdu_first; /* 原文第一段,非NULL,不包含APCI */
const uint8_t *asdu_second; /* 跨尾部时指向数组起点,否则NULL */
uint16_t asdu_first_length; /* 第一段长度,1..asdu_length */
uint16_t asdu_length; /* ASDU实际长度,6..249字节 */
uint8_t is_submitted; /* 0队列尚未登记交接;1 NORMAL已登记交接、保留待ACK;HIGH出队前恒0 */
} IEC104_AsduHeadInfo_t;
/*
* 两个缓冲区容量单位均为字节,至少各自MIN_CAPACITY;不分配内存,不清空字节数组。
* 条目占本级HEADER_LEN+asdu_length,支持跨尾部写入;最大ASDU需要本级HEADER_LEN+249字节。
* Enqueue中条目超过桶总容量返回ERROR;空间不足时HIGH返回FULL,NORMAL覆盖最旧原文。
* NORMAL覆盖包括已提交未ACK条目,不产生丢弃通知;发送记录及ACK义务由Manager独立保留。
* 结构、数组、输入、输出不得重叠;所有操作串行。generation为0时只缓存NORMAL,不能提交发送。
* 绑定连接须使用非零且跨连接不复用的generation;服务可先Init(0),接入后再Rebind。
* NORMAL编号在1..UINT64_MAX循环,回绕不改变generation;所有活跃编号距next不超过UINT64_MAX/2。
* NORMAL的entry_id只在原文条目或覆盖待ACK记录存活期间有效;释放后不得跨编号复用再次提交。
* HIGH没有条目身份;从Peek到Mark/Discard之间不得移除、清空或替换队首,不能异步保存0编号当作身份。
* 两级后续发送完成和ACK均由Manager的frame_id及协议序号处理,不保存业务user_tag。
*/
extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Init(
IEC104_AsduQueue_t *queue,
uint8_t *high_bytes, uint32_t high_capacity,
uint8_t *normal_bytes, uint32_t normal_capacity,
uint64_t connection_generation);
/* 只读数值快照;仅OK写status,查询不预留容量,也不证明TCP或链路状态。 */
extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Get_Status(
const IEC104_AsduQueue_t *queue, IEC104_AsduQueueStatus_t *status);
/*
* HIGH用于连接请求应答,提交后出队;Manager初始化成功、受理新STOP或从STOPPED受理START时清空待发条目。
* 重复START的CON不清理HIGH;HIGH重连不保留,启停准入由Manager控制,本队列不读取链路状态。
* NORMAL用于允许向同一业务接收方恢复的事件,离线时以generation=0入队;HIGH必须绑定非零代次。
* Detach/Rebind保留仍存在的NORMAL原文并恢复未提交;EOF后全部未提交NORMAL均留给下一连接。
* 长度6..249;条目头加ASDU超过目标桶总容量时ERROR,不为装不下的条目清空队列。
* HIGH空间不足时FULL,等待释放后重试;NORMAL按队首顺序计划移除旧条目,全部检查通过后才覆盖并返回OK。
* NORMAL旧条目/覆盖待ACK区间阻止编号窗口前进时也返回FULL;HIGH不分配编号,不受该窗口阻塞。
* NORMAL的UINT64_MAX仍可分配,成功后下一编号为1,不重连、不清空已有条目。
* 覆盖包括已提交原文,但不撤回已经编号的帧、不伪造ACK、不产生逐条丢弃通知。
* 仅OK后生产者推进游标;FULL/ERROR不修改队列、字节或编号。ERROR不能按背压盲目重试。
*/
extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Enqueue(
IEC104_AsduQueue_t *queue, uint64_t connection_generation,
IEC104_AsduPriority_t priority, const uint8_t *asdu,
uint16_t asdu_length);
/*
* Peek_Head查看最旧保留条目,Peek_Unsubmitted查看队列待发区首条;均不修改状态。
* Peek_Unsubmitted返回EMPTY表示当前没有可交接的条目;EOF后全部待发NORMAL留待下次绑定,不代表整个队列为空。
* 离线时仍可查看NORMAL待发原文,但必须Rebind到非零代次后才能Mark_Submitted。
* 查询只描述队列记账状态,不检查SendBuffer;is_submitted为0不证明发送模块尚未接手。
*/
extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Peek_Head(
const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority,
IEC104_AsduHeadInfo_t *head_info);
extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Peek_Unsubmitted(
const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority,
IEC104_AsduHeadInfo_t *head_info);
/*
* 将仍有效的两段原文复制到destination;capacity至少asdu_length,且不得与原文或描述重叠。
* OK写完整ASDU,ERROR不改输出;不重新验证条目身份,调用方须保证借用仍有效。
*/
extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Copy_Asdu(
const IEC104_AsduHeadInfo_t *head_info, uint8_t *destination, uint16_t capacity);
/* 登记发送交接的队列结果;调用前完整I帧须已交给SendBuffer且Manager已建立记录。
* 必须绑定非零代次;本函数入口处,队列仍保留待发旧状态。
* HIGH直接消费原文,NORMAL改为保留待ACK并推进待发游标;EOF后NORMAL不允许新的发送交接。
* 出队或修改状态均在发送模块接手之后,不表示报文已写完或已收到ACK。
* 本队列无法验证发送缓冲是否接手,调用方须保证上述顺序,不能提前调用本函数取消待发业务。
* HIGH要求当前代次及0编号,不提供同代次逐条身份保护;NORMAL另核对条目编号。
* 从Peek到本次记账之间不得移除、清空或替换所选条目;成功前的原文借用须持续有效。 */
extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Mark_Submitted(
IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduEntryId_t entry_id);
/*
* 仅接受NORMAL的有效ACK路径,按发送顺序提交Manager记录中的真实身份;HIGH的ACK只由Manager处理。
* 仍有原文时严格匹配已提交队首并释放;NORMAL原文已覆盖时核减待确认计数,不删除新队首。
* 已覆盖身份仅以计数和循环编号区间检查,准确身份及顺序由Manager的记录和Link有效ACK保证。
*/
extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Confirm_Next_Normal_Entry(
IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduEntryId_t entry_id);
/* 主动丢弃只允许尚未登记交接的物理队首,不能越过NORMAL待ACK前缀删除后面的待发条目。
* HIGH只核对代次及0编号,NORMAL另核对条目编号;成功使该条目的原文借用失效。
* 本函数无法检查SendBuffer是否已接手,调用方须在发送交接开始前使用,不得插入Manager交接过程。
* 不能用它撤回已编号I帧;ERROR不修改队列。 */
extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Discard_Unsubmitted_Head(
IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduEntryId_t entry_id);
/* 故障检查/业务恢复使用,offset为从保留队首起的条目序位,0表示队首,单位不是字节;O(offset+1)。 */
extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Get_Entry_By_Offset(
const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority,
uint32_t offset, IEC104_AsduHeadInfo_t *head_info);
/* 返回未占用字节数;空闲>=本级HEADER_LEN+ASDU长度即有足够容量;NORMAL入队另检查编号窗口。
* NORMAL空闲不足时仍可能覆盖旧原文入队;查询不预留空间,最终以Enqueue返回值为准。 */
extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Get_Free_Bytes(
const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, uint32_t *free_bytes);
/* 清空HIGH未提交条目,保留缓冲区、代次及编号进度;不影响NORMAL、已提交I帧和Manager发送记录。 */
extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Reset_High(
IEC104_AsduQueue_t *queue, uint64_t connection_generation);
/* EOF收尾前停止选取全部未提交NORMAL;已有及后来新增的未提交原文均留给下一连接,仍按正常容量策略缓存。
* 必须匹配当前非零代次;重复调用不解除限制,已提交I帧继续由SendBuffer/Manager收尾。
* 不影响HIGH,不修改ASDU原文或已有交接记账;Detach/Rebind解除限制。 */
extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Limit_Normal_Send(
IEC104_AsduQueue_t *queue, uint64_t connection_generation);
/* 旧I/O结束、旧Manager已Abort且已知ACK事实已处理后,解除与当前非零代次的绑定。
* 清空HIGH,保留NORMAL全部现存原文并恢复待发,清除旧ACK记账和发送限制;编号分配进度不变。
* 成功后generation为0,可继续缓存离线NORMAL;所有旧身份及借用失效,ERROR不改变队列。 */
extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Detach(
IEC104_AsduQueue_t *queue, uint64_t connection_generation);
/*
* 旧连接及所有旧I/O结束、Manager已Abort后显式调用;要求新的非零代次,也可从离线代次0绑定。
* 清空HIGH,保留NORMAL全部现存原文并恢复未提交;清除旧代次的覆盖待确认计数、边界及发送限制。
* 先验证两个环的全部条目,再把NORMAL头内状态恢复未提交并更新控制字段,不搬移原文。
* ERROR不改控制字段或字节,成功使所有旧借用失效。
* 已覆盖原文无法恢复;不自动发报,允许新Manager绑定恢复后的非空队列。
* 调用前先处理已知ACK事实;NORMAL只能恢复到业务认可的接收方或冗余域,未确认不等于对端未收到。
* Rebind后旧entry_id失效,保留NORMAL条目的serial和原文不变;新连接使用新N(S)。
* next_entry_serial继续沿用;本接口只检查新代次非零且不同于当前值,历史代次不复用由调用方保证。
*/
extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Rebind(
IEC104_AsduQueue_t *queue, uint64_t connection_generation);
/* 明确丢弃全部引用;先完成业务回收。Manager.Abort不会隐式调用本接口。
* 不清零或释放调用方数组;队列随后须Init才能重新使用。需要保留NORMAL恢复时应使用Rebind。 */
extern void IEC104_AsduQueue_Abort(IEC104_AsduQueue_t *queue);
#ifdef __cplusplus
}
#endif
#endif
@@ -0,0 +1,300 @@
/**
* IEC 60870-5-104无状态编解码:完整APDU的APCI检查及I/S/U报文编码。
* ASDU公共头解析由IEC60870_Codec提供,本模块只处理104传输帧。
*
* 仅使用调用方提供的报文、序号和输出缓冲,不保存连接状态,
* 不依赖接收状态机、链路状态机、发送队列或日志模块。
*/
#include "IEC104_Codec.h"
#include <stddef.h>
#include <string.h>
/**
* 功能:根据 IEC-104 控制域首字节识别 I、S 或 U 帧。
* 输入参数:control——控制域首字节。
* 输出参数:无。
* 返回值:对应的 IEC104_FrameType_t 帧类型。
* 说明:仅按控制域低位分类,不验证保留位、U功能码或帧长度。
*/
IEC104_FrameType_t IEC104_Get_Frame_Type(uint8_t control)
{
if ((control & 0x01U) == 0U) {
return IEC104_FRAME_TYPE_I;
}
if ((control & 0x03U) == 0x01U) {
return IEC104_FRAME_TYPE_S;
}
return IEC104_FRAME_TYPE_U;
}
/**
* 功能:解析 I 帧控制域中的发送序号 N(S)。
* 输入参数:frame——至少包含6字节APCI且控制域合法的I帧。
* 输出参数:无。
* 返回值:返回0至32767范围内的N(S);frame为NULL时返回0。
*/
static uint16_t Get_Send_Number(const uint8_t *frame)
{
uint16_t encoded_number;
if (frame == NULL) {
return 0;
}
encoded_number = (uint16_t)(((uint16_t)frame[3] << 8) |
(uint16_t)frame[2]);
return (uint16_t)(encoded_number >> 1);
}
/**
* 功能:解析 I 帧或 S 帧控制域中的接收序号 N(R)。
* 输入参数:frame——至少包含6字节APCI且控制域合法的I帧或S帧。
* 输出参数:无。
* 返回值:返回0至32767范围内的N(R);frame为NULL时返回0。
*/
static uint16_t Get_Receive_Number(const uint8_t *frame)
{
uint16_t encoded_number;
if (frame == NULL) {
return 0;
}
encoded_number = (uint16_t)(((uint16_t)frame[5] << 8) |
(uint16_t)frame[4]);
return (uint16_t)(encoded_number >> 1);
}
/**
* 功能:检查 U 帧功能码是否为标准定义的六种合法值之一。
* 输入参数:function——报文中的控制字节,或编码入口传入的原始功能码。
* 输出参数:无。
* 返回值:功能码合法返回1,否则返回0。
* 说明:与编码入口一样使用int接收原值,避免短枚举在校验前将0x107等非法值截成0x07。
*/
static int U_Frame_Function_Valid(int function)
{
switch (function) {
case IEC104_U_STARTDT_ACT:
case IEC104_U_STARTDT_CON:
case IEC104_U_STOPDT_ACT:
case IEC104_U_STOPDT_CON:
case IEC104_U_TESTFR_ACT:
case IEC104_U_TESTFR_CON:
return 1;
default:
return 0;
}
}
/**
* 功能:检查 I 帧控制域和对应的长度约束。
* 输入参数:frame——至少包含6字节APCI的完整帧。
* 输出参数:无。
* 返回值:控制域合法返回1,否则返回0。
*/
static int I_Frame_Control_Field_Valid(const uint8_t *frame)
{
/* I 帧必须容纳完整ASDU公共头,N(S) 和 N(R) 的格式位必须为0。 */
return frame[1] >= IEC104_MIN_LENGTH_FIELD + IEC104_ASDU_HEADER_LEN &&
(frame[2] & 0x01U) == 0U &&
(frame[4] & 0x01U) == 0U;
}
/**
* 功能:检查 S 帧控制域和对应的长度约束。
* 输入参数:frame——至少包含6字节APCI的完整帧。
* 输出参数:无。
* 返回值:控制域合法返回1,否则返回0。
*/
static int S_Frame_Control_Field_Valid(const uint8_t *frame)
{
/* S 帧无 ASDU,前两个控制字节固定,N(R) 格式位必须为0。 */
return frame[1] == IEC104_MIN_LENGTH_FIELD &&
frame[2] == 0x01U &&
frame[3] == 0x00U &&
(frame[4] & 0x01U) == 0U;
}
/**
* 功能:检查 U 帧控制域和对应的长度约束。
* 输入参数:frame——至少包含6字节APCI的完整帧。
* 输出参数:无。
* 返回值:控制域合法返回1,否则返回0。
*/
static int U_Frame_Control_Field_Valid(const uint8_t *frame)
{
/* U 帧无 ASDU,只允许六种功能码,后三个控制字节固定为0。 */
return frame[1] == IEC104_MIN_LENGTH_FIELD &&
U_Frame_Function_Valid(frame[2]) &&
frame[3] == 0x00U &&
frame[4] == 0x00U &&
frame[5] == 0x00U;
}
/**
* 功能:检查完整APDU的外层长度和APCI控制域,解析控制信息及ASDU位置。
* 输入参数:frame——完整APDU首地址;frame_len——完整APDU长度。
* 输出参数:apci——解析成功时写入帧类型、序号、U功能码和ASDU位置。
* 返回值:解析成功返回1,否则返回0且不保证apci中的内容。
* 说明:I帧要求ASDU至少含IEC104_ASDU_HEADER_LEN字节;公共头字段和信息对象由后续解析处理。
*/
int IEC104_Parse_APCI(const uint8_t *frame,
uint16_t frame_len,
IEC104_ApciInfo_t *apci)
{
IEC104_FrameType_t frame_type;
if (frame == NULL || apci == NULL ||
frame_len < IEC104_APCI_LEN ||
frame[0] != IEC104_START_BYTE ||
frame[1] < IEC104_MIN_LENGTH_FIELD ||
frame[1] > IEC104_MAX_LENGTH_FIELD ||
frame_len != (uint16_t)(2U + frame[1])) {
return 0;
}
frame_type = IEC104_Get_Frame_Type(frame[2]);
apci->frame_type = frame_type;
apci->send_number = 0;
apci->receive_number = 0;
apci->u_function = 0;
apci->asdu = NULL;
apci->asdu_len = 0;
switch (frame_type) {
case IEC104_FRAME_TYPE_I:
if (!I_Frame_Control_Field_Valid(frame)) {
return 0;
}
apci->send_number = Get_Send_Number(frame);
apci->receive_number = Get_Receive_Number(frame);
apci->asdu = &frame[IEC104_APCI_LEN];
apci->asdu_len =
(uint16_t)(frame[1] - IEC104_MIN_LENGTH_FIELD);
break;
case IEC104_FRAME_TYPE_S:
if (!S_Frame_Control_Field_Valid(frame)) {
return 0;
}
apci->receive_number = Get_Receive_Number(frame);
break;
case IEC104_FRAME_TYPE_U:
if (!U_Frame_Control_Field_Valid(frame)) {
return 0;
}
apci->u_function = frame[2];
break;
default:
return 0;
}
return 1;
}
/**
* 功能:根据发送序号、接收序号和ASDU构造一个完整的I帧APDU。
* 输入参数:buf——输出缓冲区;send_seq——N(S);recv_seq——N(R);
* asdu——完整ASDU首地址;asdu_len——ASDU长度。
* 输出参数:buf——成功时写入起始字节、长度域、控制域和ASDU。
* 返回值:成功返回生成的APDU总长度,参数非法返回-1。
* 说明:调用方必须保证buf至少具有IEC104_APCI_LEN+asdu_len字节空间;
* 本函数只编码报文,不读取或修改链路序号状态;调用方须保存recv_seq供发送完成提交。
*/
int IEC104_Build_Iframe(uint8_t *buf,
uint16_t send_seq,
uint16_t recv_seq,
const uint8_t *asdu,
int asdu_len)
{
uint16_t encoded_send;
uint16_t encoded_receive;
if (buf == NULL || asdu == NULL ||
asdu_len < (int)IEC104_ASDU_HEADER_LEN ||
asdu_len > (int)IEC104_MAX_ASDU_LEN ||
send_seq > IEC104_SEQUENCE_MASK ||
recv_seq > IEC104_SEQUENCE_MASK ||
IEC104_MIN_LENGTH_FIELD + asdu_len >
IEC104_MAX_LENGTH_FIELD) {
return -1;
}
encoded_send = (uint16_t)(send_seq << 1);
encoded_receive = (uint16_t)(recv_seq << 1);
buf[0] = IEC104_START_BYTE;
buf[1] = (uint8_t)(IEC104_MIN_LENGTH_FIELD + asdu_len);
buf[2] = (uint8_t)(encoded_send & 0xFFU);
buf[3] = (uint8_t)(encoded_send >> 8);
buf[4] = (uint8_t)(encoded_receive & 0xFFU);
buf[5] = (uint8_t)(encoded_receive >> 8);
memcpy(&buf[IEC104_APCI_LEN], asdu, (size_t)asdu_len);
return IEC104_APCI_LEN + asdu_len;
}
/**
* 功能:根据接收序号构造一个完整的S帧APDU。
* 输入参数:buf——输出缓冲区;recv_seq——待编码的N(R)。
* 输出参数:buf——成功时写入6字节S帧。
* 返回值:成功返回IEC104_APCI_LEN,参数非法返回-1。
* 说明:调用方必须保证buf至少具有IEC104_APCI_LEN字节空间;
* 本函数只编码报文,不读取或修改链路序号状态;调用方须保存recv_seq供发送完成提交。
*/
int IEC104_Build_Sframe(uint8_t *buf, uint16_t recv_seq)
{
uint16_t encoded_receive;
if (buf == NULL || recv_seq > IEC104_SEQUENCE_MASK) {
return -1;
}
encoded_receive = (uint16_t)(recv_seq << 1);
buf[0] = IEC104_START_BYTE;
buf[1] = IEC104_MIN_LENGTH_FIELD;
buf[2] = 0x01U;
buf[3] = 0x00U;
buf[4] = (uint8_t)(encoded_receive & 0xFFU);
buf[5] = (uint8_t)(encoded_receive >> 8);
return IEC104_APCI_LEN;
}
/**
* 功能:根据U帧功能码构造一个完整的U帧APDU。
* 输入参数:buf——输出缓冲区;function——待校验的整数功能码,只允许六种U帧功能值。
* 输出参数:buf——成功时写入6字节U帧,参数检查失败时不写入。
* 返回值:成功返回IEC104_APCI_LEN,参数或功能码非法返回-1。
* 说明:调用方必须保证buf至少具有IEC104_APCI_LEN字节空间;
* buf非NULL且功能码合法时固定返回6,容量由调用方保证,本函数无法检查。
* function保持int原值直到校验通过,再转为控制字节;调用方不要预先收窄未校验值。
* 本函数只编码报文,不处理U帧状态转换或网络发送。
*/
int IEC104_Build_Uframe(uint8_t *buf, int function)
{
if (buf == NULL || !U_Frame_Function_Valid(function)) {
return -1;
}
buf[0] = IEC104_START_BYTE;
buf[1] = IEC104_MIN_LENGTH_FIELD;
buf[2] = (uint8_t)function;
buf[3] = 0x00U;
buf[4] = 0x00U;
buf[5] = 0x00U;
return IEC104_APCI_LEN;
}
@@ -0,0 +1,103 @@
/* 104专用无状态编解码:APCI检查及I/S/U帧构造;ASDU公共头见IEC60870_Codec。 */
#ifndef IEC104_CODEC_H
#define IEC104_CODEC_H
#include "IEC104_Types.h"
#ifdef __cplusplus
extern "C" {
#endif
/* IEC-104 帧类型。 */
typedef enum {
IEC104_FRAME_TYPE_I = 0,
IEC104_FRAME_TYPE_S,
IEC104_FRAME_TYPE_U
} IEC104_FrameType_t;
/* U 帧控制域首字节;ACT 为请求,CON 为对应确认。
* 编码入口用int接收待校验值,不依赖本枚举的存储宽度。 */
typedef enum {
IEC104_U_STARTDT_ACT = 0x07,
IEC104_U_STARTDT_CON = 0x0B,
IEC104_U_STOPDT_ACT = 0x13,
IEC104_U_STOPDT_CON = 0x23,
IEC104_U_TESTFR_ACT = 0x43,
IEC104_U_TESTFR_CON = 0x83
} IEC104_UFrameFunction_t;
/* 完整APDU中APCI控制信息及ASDU位置的解析结果;各字段是否有效由frame_type决定。 */
typedef struct {
IEC104_FrameType_t frame_type; /* I、S或U帧 */
uint16_t send_number; /* I帧的N(S) */
uint16_t receive_number; /* I帧或S帧的N(R) */
uint8_t u_function; /* U帧功能码 */
const uint8_t *asdu; /* I帧中的ASDU首地址 */
uint16_t asdu_len; /* I帧中的ASDU长度 */
} IEC104_ApciInfo_t;
/**
* 功能:根据 IEC-104 控制域首字节识别 I、S 或 U 帧。
* 输入参数:control——控制域首字节。
* 输出参数:无。
* 返回值:对应的 IEC104_FrameType_t 帧类型。
* 说明:仅按控制域低位分类,不验证保留位、U功能码或帧长度;完整APCI检查由Parse_APCI执行。
*/
extern IEC104_FrameType_t IEC104_Get_Frame_Type(uint8_t control);
/**
* 功能:检查完整APDU的外层长度和APCI控制域,解析控制信息及ASDU位置。
* 输入参数:frame——完整APDU首地址;frame_len——完整APDU长度。
* 输出参数:apci——解析成功时写入帧类型、序号、U功能码和ASDU位置。
* 返回值:解析成功返回1,否则返回0且不保证apci中的内容。
* 说明:I帧要求ASDU长度至少为IEC104_ASDU_HEADER_LEN,不足时返回0;不解析公共头字段或信息对象。
* apci->asdu直接指向frame内部,其有效期不超过frame缓冲区有效期。
*/
extern int IEC104_Parse_APCI(const uint8_t *frame,
uint16_t frame_len,
IEC104_ApciInfo_t *apci);
/**
* 功能:根据发送序号、接收序号和ASDU构造一个完整的I帧APDU。
* 输入参数:buf——输出缓冲区;send_seq——N(S);recv_seq——N(R);
* asdu——完整ASDU首地址;asdu_len——ASDU长度,范围为6..249。
* 输出参数:buf——成功时写入完整I帧。
* 返回值:成功返回APDU总长度,参数非法返回-1。
* 说明:调用方必须保证buf至少具有IEC104_APCI_LEN+asdu_len字节空间;
* 本函数不读取或修改IEC104_LinkState_t。调用方须保存recv_seq,并在
* 完整发送后把它作为实际N(R)提交给链路状态。
*/
extern int IEC104_Build_Iframe(uint8_t *buf,
uint16_t send_seq,
uint16_t recv_seq,
const uint8_t *asdu,
int asdu_len);
/**
* 功能:根据接收序号构造一个完整的S帧APDU。
* 输入参数:buf——输出缓冲区;recv_seq——待编码的N(R),范围为0..32767。
* 输出参数:buf——成功时写入6字节S帧。
* 返回值:成功返回IEC104_APCI_LEN,参数非法返回-1。
* 说明:调用方必须保证buf至少具有IEC104_APCI_LEN字节空间;
* 本函数不读取或修改IEC104_LinkState_t。调用方须保存recv_seq,并在
* 完整发送后把它作为实际N(R)提交给链路状态。
*/
extern int IEC104_Build_Sframe(uint8_t *buf, uint16_t recv_seq);
/**
* 功能:根据U帧功能码构造一个完整的U帧APDU。
* 输入参数:buf——输出缓冲区;function——待校验的整数功能码,只允许六种U帧功能值。
* 输出参数:buf——成功时写入6字节U帧,参数检查失败时不写入。
* 返回值:成功返回IEC104_APCI_LEN,参数或功能码非法返回-1。
* 说明:调用方必须保证buf至少具有IEC104_APCI_LEN字节空间;
* buf非NULL且功能码合法时固定返回6,容量由调用方保证,本函数无法检查。
* function使用int,短枚举配置下也能拒绝0x107等非法值;调用方不要预先收窄未校验值。
* 本函数不处理U帧状态转换或网络发送。
*/
extern int IEC104_Build_Uframe(uint8_t *buf, int function);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_CODEC_H */
@@ -0,0 +1,294 @@
/*
* TCP内部的子站连接协调:串行衔接Receiver、Link和SendManager,不执行socket I/O。
* 本头文件供TcpServer实现使用;业务通过IEC104_TcpServer.h接入。
* 下文的调用方指TcpServer;对象及借用存储由其管理,内部帧和记录由其转为业务事件。
*/
#ifndef IEC104_CONNECTION_H
#define IEC104_CONNECTION_H
#include "IEC104_Receiver.h"
#include "IEC104_SendManager.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 按具体值分支;只有SEND_READY提供发送字节,FRAME_WRITTEN提供完成结果。 */
typedef enum {
IEC104_CONNECTION_FATAL = -2, /* 已冻结连接;停止I/O,读取记录后Close */
IEC104_CONNECTION_ERROR = -1, /* 参数、身份、调用阶段或时钟非法 */
IEC104_CONNECTION_EMPTY = 0, /* 无可查询项或无待发送业务 */
IEC104_CONNECTION_OK = 1, /* 本次操作成功;发送进度接口中表示尚未整帧写完 */
IEC104_CONNECTION_WAIT = 2, /* 接收请求未受理,或发送门控/容量阻塞 */
IEC104_CONNECTION_FRAME_READY = 3, /* I帧已完成链路处理,等待TcpServer复制为事件并释放 */
IEC104_CONNECTION_SEND_READY = 4, /* 当前帧的剩余字节可交给实际传输 */
IEC104_CONNECTION_FRAME_WRITTEN = 5 /* 整帧写完及对应完成处理均已提交 */
} IEC104_ConnectionResult_t;
typedef enum {
IEC104_CONNECTION_UNINITIALIZED = 0, /* 零值标记;仍须成功Init后才能调用运行接口 */
IEC104_CONNECTION_ACTIVE, /* TCP由TcpServer管理;不表示已经STARTED */
IEC104_CONNECTION_FAILED, /* 保留接收原文、发送记录及ASDU队列供回收 */
IEC104_CONNECTION_CLOSED /* 借用已解除;ASDU队列仍由TcpServer持有 */
} IEC104_ConnectionPhase_t;
typedef enum {
IEC104_CONNECTION_FAULT_NONE = 0, /* 尚未登记故障 */
IEC104_CONNECTION_FAULT_FRAME, /* 起始字节、APCI或ASDU最低长度非法 */
IEC104_CONNECTION_FAULT_PROTOCOL, /* 序号、ACK或接收阶段不符合协议 */
IEC104_CONNECTION_FAULT_INTERNAL, /* 模块状态或交接失败 */
IEC104_CONNECTION_FAULT_TRANSPORT, /* 发送失败或外部传输故障;正常有序EOF由TcpServer收尾 */
IEC104_CONNECTION_FAULT_LOCAL_SEND_TIMEOUT, /* 当前APDU排队及写入总期限到达 */
IEC104_CONNECTION_FAULT_I_ACK_TIMEOUT, /* 已完整发出的I帧等待对端ACK达到t1 */
IEC104_CONNECTION_FAULT_TESTFR_TIMEOUT, /* 本端TESTFR_ACT已发完,等待CON达到t1 */
IEC104_CONNECTION_FAULT_STOP_TIMEOUT /* STOP已受理,确认及对应CON发送未在总期限内完成 */
} IEC104_ConnectionFault_t;
/* 内部连接配置的类型别名;配置值由公共Types定义,与TcpServer使用同一布局。 */
typedef IEC104_ProtocolConfig_t IEC104_ConnectionConfig_t;
/* TcpServer借用原文至匹配的Release或Close;代次与receive_serial共同标识这次内部读取。 */
typedef struct {
IEC104_ReceivedFrame_t frame; /* 完整APDU/APCI;仅交付已受理的I帧 */
uint64_t receive_serial; /* 本连接内不复用,拒绝迟到的重复Release */
} IEC104_ConnectionReceivedFrame_t;
/* 无指针的观察快照;CLOSED后的链路字段是收尾时的历史值,不用这些字段自行组合收发门控。 */
typedef struct {
uint64_t connection_generation; /* 本快照所属连接代次 */
IEC104_ConnectionPhase_t phase; /* Connection运行、故障冻结或已关闭阶段 */
IEC104_ConnectionFault_t fault; /* 首次故障原因;正常关闭仍可为NONE */
IEC104_DataTransferState_t dt_state; /* 受理START/STOP后立即更新的数据传输状态 */
IEC104_DtProcedure_t dt_procedure; /* 子站仅IDLE或STOP_DRAINING,CON进度由发送缓冲维护 */
IEC104_TestfrState_t testfr_state; /* 本端主动探测的发送/等待CON进度 */
uint16_t send_record_count; /* 全部未消费I帧记录数,包括已经ACK的回执 */
uint8_t has_received_i_frame; /* TcpServer可Peek,复制为事件后显式Release */
uint8_t has_unhandled_frame; /* WAIT或故障时保留的尚未受理帧 */
} IEC104_ConnectionStatus_t;
/*
* 仅供TcpServer内部的启停通知适配;不是业务回调,不得重入修改接口,可只读查询状态。
* can_accept只读查询启停通知容量;notify在实际状态改变后、请求处理返回前复制通知。
* 两个函数必须同时设置或同时为空;为空时独立Connection不产生通知背压。
*/
typedef struct {
void *context;
int (*can_accept)(void *context);
int (*notify)(void *context, IEC104_DataTransferState_t state);
} IEC104_ConnectionTransferObserver_t;
/*
* TcpServer内部持有,完整布局用于静态存储;全部字段由接口维护,无malloc、无锁、无业务回调。
* Init后地址固定,不能复制/移动:Manager引用本结构内唯一的Link和SendBuffer。
* 同一连接的所有调用必须串行;不要再直接调用内含对象的修改接口。
* Link和SendBuffer的运行期修改统一经Manager;Connection只读Link,发送期限也通过Manager查询。
* ASDU队列独立持有以支持重连恢复;对象、数组及输入/输出参数存储不得重叠。
*/
typedef struct {
IEC104_Receiver_t receiver; /* 组装半帧并保留完整APDU;I帧由TcpServer复制为事件后释放 */
IEC104_LinkState_t link_state; /* 本连接唯一协议状态,收发共用序号和启停进度 */
IEC104_SendBuffer_t send_buffer; /* 当前I帧及控制FIFO,保存实际写入进度 */
IEC104_SendManager_t send_manager; /* 关联ASDU、发送帧和ACK记录,引用本对象内的Link/Buffer */
IEC104_ConnectionConfig_t config; /* Init时复制的配置,运行期间保持不变 */
IEC104_ConnectionTransferObserver_t transfer_observer; /* 内部通知容量及变化交接 */
IEC104_SendFrameId_t stop_con_frame_id; /* 当前STOP对应的最后一帧CON,拒绝旧完成结束新期限 */
uint64_t connection_generation; /* 非零且跨连接不复用,所有外部事件均须匹配 */
uint64_t last_now_ms; /* 串行事件的单调时刻,不接受倒退 */
uint64_t last_activity_ms; /* 完整合法APDU到达或本地整帧完成;反向启停CON除外 */
uint64_t received_at_ms; /* 当前完整接收帧到达时刻,WAIT不续期 */
uint64_t receive_serial; /* 每次成帧递增;耗尽时结束连接 */
uint64_t rx_ack_started_ms; /* 当前连续未确认接收区间的最早起点 */
uint64_t testfr_started_ms; /* 本次TESTFR_ACT完整写出的时刻 */
uint64_t stop_started_ms; /* 本轮STOP原成帧时刻;受理时启用计时,受理前WAIT也计入总期限 */
IEC104_ConnectionPhase_t phase; /* Connection生命周期,独立于Link的STARTED/STOPPED */
IEC104_ConnectionFault_t fault; /* 只登记首次故障;Close保留该值供查询 */
uint8_t rx_link_applied; /* I帧已交给Manager,不得因重复Peek/驱动再交一次 */
uint8_t rx_ack_timer_active; /* 0/1:存在尚未全部确认的接收区间,rx_ack_started_ms有效 */
uint8_t testfr_timer_active; /* 0/1:本次ACT已完整发送,testfr_started_ms有效 */
uint8_t stop_timer_active; /* 0/1:STOP已受理、未取消且CON尚未完成,stop_started_ms有效 */
uint8_t receive_batch_active; /* 有界接收批次内暂缓对端确认超时,下一次Tick结束批次 */
uint8_t receive_batch_pending; /* 固定输入快照尚未处理完,下一轮必须立即续收 */
} IEC104_Connection_t;
/* 写入Link默认值;本地发送和停止期限默认等于默认t1,HIGH严格优先。NULL无操作。 */
extern void IEC104_Connection_Config_Default(IEC104_ConnectionConfig_t *config);
/*
* 初始化新子站连接,初始STOPPED;generation非零且跨连接不复用,now_ms允许0。
* queue已Init/Rebind到该代次且无待ACK条目;可有待发NORMAL,成功时清空HIGH。
* control_capacity>=1,record_capacity为1..32767;config不可为NULL。
* 配置须满足0<t2_ms<t1_ms;不满足时返回ERROR,不改connection或队列。
* OK后对象地址及借用存储保持有效;失败不改connection或队列。
* Manager负责校验并在最终地址初始化Link/Buffer;成功后仅提交其余连接字段,不修补Manager内部指针。
* 仅用于首次使用或Close后的对象;重新Init前必须结束旧I/O和所有借用。
*/
extern IEC104_ConnectionResult_t IEC104_Connection_Init(
IEC104_Connection_t *connection, IEC104_AsduQueue_t *queue,
IEC104_SendBufferControlFrame_t *control_frames, uint16_t control_capacity,
IEC104_IFrameSendRecordStorage_t *records, uint16_t record_capacity,
const IEC104_ConnectionConfig_t *config, uint64_t connection_generation, uint64_t now_ms);
/*
* Init后、首次Feed前安装内部启停通知适配;observer复制入连接,context须持续有效。
* 参数非法或已经接收过APDU返回ERROR,不改变适配器;不执行通知或推进协议。
*/
extern IEC104_ConnectionResult_t IEC104_Connection_Set_Transfer_Observer(
IEC104_Connection_t *connection, uint64_t connection_generation,
const IEC104_ConnectionTransferObserver_t *observer);
/*
* 可选:开始一轮有界的非阻塞接收,暂缓I帧ACK和TESTFR_CON的t1判定。
* 用于先处理socket中已经到达的多帧输入,再检查对端是否仍欠确认;不重置期限。
* 同一轮必须调用Tick或Yield_Receive_Batch结束批次;重复Begin返回ERROR。
* 期间仍检查本地发送和STOP总期限,格式/协议错误仍立即失败;Close也会结束批次。
* 未调用本接口时,Feed仍即时检查全部期限;发送准备/回报仅检查本地发送和STOP期限。
*/
extern IEC104_ConnectionResult_t IEC104_Connection_Begin_Receive_Batch(
IEC104_Connection_t *connection, uint64_t connection_generation);
/* 仅供调度预算让出:结束当前轮次,但已到达的有限输入快照尚未全部处理。
* 不改任何期限起点,暂缓I_ACK/TESTFR判定;本地发送、STOP及控制推进仍有效。
* Get_Wait_Time返回0,调用方必须立即续收,且不得把后来到达的输入加入旧快照。
* 快照耗尽、实际容量背压或接收无进展时必须调用Tick恢复完整期限检查。
* 不能用于等待业务消费或socket可读;仅ACTIVE且已Begin时可调用。 */
extern IEC104_ConnectionResult_t IEC104_Connection_Yield_Receive_Batch(
IEC104_Connection_t *connection, uint64_t connection_generation, uint64_t now_ms);
/*
* 最多消费至一帧末尾,consumed_length必填;非NULL时始终写入实际消费量。
* 半帧或已受理S/U返回OK;I帧返回FRAME_READY;控制槽或启停通知容量不足返回WAIT并保留当前帧。
* 后两者重入时消费0字节。保留未消费尾部;Release/驱动解除阻塞后再Feed尾部。
* 长度为0时data可为NULL,用于重试当前帧。格式/协议错误冻结连接并返回FATAL。
* 格式合法的STARTDT_CON/STOPDT_CON消费后忽略,不应答、不改变启停或确认期限、不刷新t3;
* 不占业务通知或控制发送槽,处理后仍照常检查期限并推进已有控制义务。
* 链路受理不代表ASDU业务执行成功;ASDU内容由业务解析。无效参数不消费输入。
*/
extern IEC104_ConnectionResult_t IEC104_Connection_Feed(
IEC104_Connection_t *connection, uint64_t connection_generation,
const uint8_t *data, uint32_t length, uint64_t now_ms, uint32_t *consumed_length);
/* FRAME_READY时写只读视图;无业务帧EMPTY,尚未受理WAIT。FAILED仍可读取已受理I帧。 */
extern IEC104_ConnectionResult_t IEC104_Connection_Peek_Received_Frame(
const IEC104_Connection_t *connection, uint64_t connection_generation,
IEC104_ConnectionReceivedFrame_t *frame);
/* TcpServer成功复制I_FRAME事件后释放匹配I帧,OK/ERROR;允许FAILED回收,不再次提交链路事件。 */
extern IEC104_ConnectionResult_t IEC104_Connection_Release_Received_Frame(
IEC104_Connection_t *connection, uint64_t connection_generation, uint64_t receive_serial);
/*
* 提交当前连接的应答;仅接收批次之外、同代次ACTIVE连接可用,不执行socket I/O。
* 两条路径均由Manager按实际状态准入:仅STARTED+IDLE时接手。
* 其他阶段在容量检查前返回ERROR且不入HIGH;普通路径先Drive,再检查准入。
* SEND_READY已生成I帧并写head_info;OK已复制到HIGH;两者都不再借用输入原文。
* WAIT尚未接手,ERROR参数/阶段/容量非法,FATAL连接失败;准备后的故障不能直接重放原文。
* 到期且准入允许时仅入HIGH,由Process先读取待处理ACK再检查期限;未到期时统一推进控制流程。
* 受理新STOPDT_ACT时,HIGH待发应答作废且无逐条回执;重复STARTDT_ACT不清理HIGH。
* 服务级NORMAL直接进入TcpServer持有的ASDU队列;应答的HIGH顺序、窗口及公平配额由Manager保持。
*/
extern IEC104_ConnectionResult_t IEC104_Connection_Submit_Response(
IEC104_Connection_t *connection, uint64_t connection_generation,
const uint8_t *asdu, uint16_t asdu_length, uint64_t now_ms,
IEC104_SendBufferHeadInfo_t *head_info);
/* 不排队变体:仅SEND_READY接手;两级待发数据、窗口/缓冲背压均返回WAIT。
* 到期协议工作也先返回WAIT,由正常接收/Process处理后重试,不提前判远端超时。
* WAIT不保存原文、不编号;其余参数、会话及输出约定沿用Submit_Response。 */
extern IEC104_ConnectionResult_t IEC104_Connection_Try_Submit_Response(
IEC104_Connection_t *connection, uint64_t connection_generation,
const uint8_t *asdu, uint16_t asdu_length, uint64_t now_ms,
IEC104_SendBufferHeadInfo_t *head_info);
/*
* 推进接收重试、控制应答及本地发送/STOP期限,再取得剩余发送字节;仅SEND_READY写head_info。
* 不在此处报告I帧ACK或TESTFR_CON超时故障;调用方须按Get_Wait_Time及时接收并Tick。
* 任一确认t1已到时只取得已有帧;无已有帧返回WAIT,不选择HIGH/NORMAL、不分配N(S)。
* 有效确认解除所有已到期等待后重新准入,不重置原期限;接收批次内外均适用。
* 无当前帧时才选ASDU并编号。RX的WAIT不阻止已有帧发送,避免控制槽满时互等。
* pending_data借用至该帧完成或Close;一次只能有一个未回报的写操作,禁止APDU交错。
*/
extern IEC104_ConnectionResult_t IEC104_Connection_Prepare_Next_Send(
IEC104_Connection_t *connection, uint64_t connection_generation, uint64_t now_ms,
IEC104_SendBufferHeadInfo_t *head_info);
/* 内部EOF收尾查询:仅在Prepare_Next_Send返回WAIT、转移就绪事件后使用。
* 记录容量已满、队首可回收,且启停/k窗口和上次准备时刻的确认t1均允许新I帧时,record_wait=1。
* 表示当前等待可通过转移RECORD解除;其他限制为0,不把还需新ACK/STARTDT的等待误判为可恢复。
* 只读,不推进协议或预留序号;仅OK写输出,ERROR/FATAL沿用连接查询约定。 */
extern IEC104_ConnectionResult_t IEC104_Connection_Get_Record_Wait(
const IEC104_Connection_t *connection, uint64_t connection_generation, uint8_t *record_wait);
/*
* 回报实际写入增量,0表示暂不可写;OK尚未写完,FRAME_WRITTEN才写completion。
* now_ms使用实际写入后的时刻;记录及计时照常更新,对端确认t1由后续接收/Tick判定。
* 本地发送和STOP期限仍检查;不得通过回报发送进度替代接收及Tick调度。
* 完成后自动重试保留接收帧/控制流程。FATAL可能发生在已登记写入之后,不能重放增量。
* 先回报本地已知完成,再Feed覆盖该帧的ACK;不允许用对端ACK推定写入结果。
*/
extern IEC104_ConnectionResult_t IEC104_Connection_Handle_Send_Progress(
IEC104_Connection_t *connection, IEC104_SendFrameId_t frame_id,
uint16_t bytes_written_now, uint64_t now_ms, IEC104_LocalSendCompletion_t *completion);
/* 最终写失败:匹配当前帧时提交失败并冻结,返回FATAL;错身份ERROR,不影响当前帧。 */
extern IEC104_ConnectionResult_t IEC104_Connection_Handle_Send_Failure(
IEC104_Connection_t *connection, IEC104_SendFrameId_t frame_id);
/*
* TcpServer在Poll中调用,结束可选接收批次并检查全部期限;会排队到期的S/TESTFR。
* 不创建系统定时器、不准备新I帧。固定输入快照耗尽或实际背压时必须调用Tick。
* 返回OK、接收FRAME_READY/WAIT或FATAL;参数、阶段、旧代次或时钟非法返回ERROR。
* 所有now_ms须为同一单调时钟且按调用顺序不倒退,时间戳0有效,是否计时由active标志区分。
* t2保留连续未确认区间的最早起点;只确认区间前缀时不续期,剩余部分可能提前确认。
*/
extern IEC104_ConnectionResult_t IEC104_Connection_Tick(
IEC104_Connection_t *connection, uint64_t connection_generation, uint64_t now_ms);
/*
* 供TcpServer在接收批次结束且已推进协议后,查询下一次驱动前可等待的毫秒数。
* max_wait_ms是调用方本轮允许的上限,0表示不等待,UINT32_MAX仍是有限毫秒数。
* OK才写wait_ms,范围0..max_wait_ms;到期返回0,没有更早期限则返回max_wait_ms。
* 覆盖本地发送、I帧/TESTFR的t1、STOP总期限及当前可准备控制帧的t2/t3。
* 与超时检查、发送准入和控制推进共用即时期限视图,不另存或改写各帧的原始起点。
* S/TESTFR已在途或控制槽满时不反复被旧t2/t3唤醒,仍受发送及其他活动期限约束。
* 只读查询,不更新时刻、不续期、不排队或冻结连接;实际处理仍由Tick/收发驱动。
* 参数、代次、时钟、内部状态非法或接收批次未结束返回ERROR;FAILED返回FATAL。
*/
extern IEC104_ConnectionResult_t IEC104_Connection_Get_Wait_Time(
const IEC104_Connection_t *connection, uint64_t connection_generation, uint64_t now_ms,
uint32_t max_wait_ms, uint32_t *wait_ms);
/* 供TcpServer查询内部回执并转换为RECORD事件;允许ACTIVE或FAILED,OK才写record。 */
extern IEC104_ConnectionResult_t IEC104_Connection_Peek_Confirmed_Record(
const IEC104_Connection_t *connection, uint64_t connection_generation,
IEC104_IFrameSendRecord_t *record);
/* TcpServer成功复制RECORD事件后移除匹配队首回执;允许FAILED回收,不再次释放ASDU原文。 */
extern IEC104_ConnectionResult_t IEC104_Connection_Remove_Confirmed_Record(
IEC104_Connection_t *connection, IEC104_SendFrameId_t frame_id);
/* 按交接顺序读取记录,供故障回收;OK写副本,越界EMPTY。Close后不能再读取。 */
extern IEC104_ConnectionResult_t IEC104_Connection_Get_Record_By_Offset(
const IEC104_Connection_t *connection, uint64_t connection_generation,
uint16_t offset, IEC104_IFrameSendRecord_t *record);
/* 返回数值状态,允许FAILED/CLOSED;OK写status,参数、旧代次或模块状态非法时ERROR不改输出。 */
extern IEC104_ConnectionResult_t IEC104_Connection_Get_Status(
const IEC104_Connection_t *connection, uint64_t connection_generation,
IEC104_ConnectionStatus_t *status);
/* EOF或外部传输失败时冻结;ACTIVE/FAILED下同代次返回FATAL并保留首次原因。
* 旧代次、未初始化或CLOSED返回ERROR;不关闭socket、提交当前帧Complete或清空记录。 */
extern IEC104_ConnectionResult_t IEC104_Connection_Handle_Transport_Closed(
IEC104_Connection_t *connection, uint64_t connection_generation);
/*
* 先结束旧I/O、结束字节借用、读取所需记录,再Close;同代次重复Close返回OK。
* 清理Receiver/Manager/SendBuffer,保留外部ASDU队列;不执行socket关闭或隐式Rebind。
* 重连顺序:Close -> Queue.Rebind(新代次) -> Connection.Init -> 新STARTDT。
*/
extern IEC104_ConnectionResult_t IEC104_Connection_Close(
IEC104_Connection_t *connection, uint64_t connection_generation);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_CONNECTION_H */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,15 @@
/**
* 兼容总头文件:汇总公共接口和两种角色的启停接口,保留既有include用法。
* 新代码可直接包含Common.h、Master_StartStop.h或Slave_StartStop.h;角色头文件已包含Common.h。
* 头文件的选择不设置本端角色,仍须由State_Init为每条连接固定角色;包含两端
* 声明也不允许混用角色专用接口。构建时包含公共链路实现,并按使用的启停接口加入角色实现。
* Internal.h面向实现内部的跨文件协作,故不由本公开入口转发。
*/
#ifndef IEC104_LINK_H
#define IEC104_LINK_H
#include "IEC104_Link_Common.h"
#include "IEC104_Link_Master_StartStop.h"
#include "IEC104_Link_Slave_StartStop.h"
#endif /* IEC104_LINK_H */
@@ -0,0 +1,570 @@
/**
* 公共链路类型与接口:初始化、I/S序号、k/w窗口、S确认及TESTFR,两种角色共用。
* 业务调用方按本端角色包含Master_StartStop.h或Slave_StartStop.h即可同时获得本头文件的声明。
* 启停接口分别由角色头文件公开;Internal.h只声明实现之间的协作函数。
*
* 每条连接持有一份State,所有事件串行处理。Prepare/应答Handle成功只代表
* 报文已生成,须由调用方发送并按具体接口提交Complete;构帧不能当作发送完成。
* 完整状态校验集中在IEC104_Link.c;使用启停接口时再链接对应角色实现。
*/
#ifndef IEC104_LINK_COMMON_H
#define IEC104_LINK_COMMON_H
#include "IEC104_Codec.h"
#ifdef __cplusplus
extern "C" {
#endif
/** 每条TCP连接的本端角色;初始化后在该连接存续期间不得切换。 */
typedef enum {
IEC104_ROLE_INVALID = 0, /* 未初始化或损坏标记;正常链路接口拒绝此值 */
IEC104_ROLE_CONTROLLING, /* 控制站:主动发起STARTDT/STOPDT并等待CON */
IEC104_ROLE_CONTROLLED /* 被控站:接收STARTDT/STOPDT_ACT并发送CON */
} IEC104_LinkRole_t;
/**
* 数据传输是否启用。被控站按START/STOP受理结果立即更新,与CON写入完成解耦。
* 控制站仍按原ACT/CON流程提交状态,并允许在启动过程中提前发送命令。
* 本字段不表达停止等待ACK;被控站STOPPED/STOP_DRAINING仍接受S确认。
*/
typedef enum {
IEC104_DT_STOPPED = 0, /* 数据传输未启用;具体接收/确认权限由角色及过程决定 */
IEC104_DT_STARTED /* 已提交启动;I帧收发还须满足当前dt_procedure的门控条件 */
} IEC104_DataTransferState_t;
/**
* STARTDT/STOPDT过程,控制站与被控站使用各自的合法值;TESTFR独立。
* 控制站:STOPPED/IDLE -> START_ACT_SEND_PENDING -> START_CON_WAITING -> STARTED/IDLE;
* STARTED/IDLE -> STOP_REQUESTED -> STOP_ACT_SEND_PENDING -> STOP_CON_WAITING -> STOPPED/IDLE。
* 被控站使用以下三个逻辑状态:
* - STOPPED/IDLE:停用;
* - STARTED/IDLE:启用,受理START立即进入;
* - STOPPED/STOP_DRAINING:停用并等待确认,受理STOP立即进入。
* 停止确认满足后生成CON并回STOPPED/IDLE;任何阶段受理START都进入STARTED/IDLE。
* 被控站不保存CON发送半态或相反ACT暂存标志,已有CON只由发送FIFO维护。
*/
typedef enum {
IEC104_DT_PROC_IDLE = 0, /* 当前没有STARTDT或STOPDT过程 */
/* 控制站专用过程。 */
IEC104_DT_PROC_START_ACT_SEND_PENDING, /* STARTDT_ACT已生成,等待发送完成 */
IEC104_DT_PROC_START_CON_WAITING, /* STARTDT_ACT已发送,等待STARTDT_CON */
IEC104_DT_PROC_STOP_REQUESTED, /* 已受理主动停止,正在准备S确认/ACT */
IEC104_DT_PROC_STOP_ACT_SEND_PENDING, /* STOPDT_ACT已生成,等待发送完成 */
IEC104_DT_PROC_STOP_CON_WAITING, /* STOPDT_ACT已发送,等待STOPDT_CON */
/* 被控站专用过程。 */
IEC104_DT_PROC_STOP_DRAINING = 7 /* 被控站已停用,等待确认后生成STOPDT_CON */
} IEC104_DtProcedure_t;
/**
* 普通S确认帧的发送进度。
*
* 普通w/t2确认与两端STOPDT流程共用此状态;同一时刻只允许一个S确认在途。
* 成功路径为IDLE -> 生成S帧 -> SEND_PENDING -> Complete_S_Ack_Send -> IDLE。
* 普通确认和STOPDT准备接口都使用同一构帧步骤,冻结报文携带的N(R);
* Complete成功后才推进acked_nr。发送失败同样清除在途状态,但保持
* acked_nr不变并返回FATAL。
*/
typedef enum {
IEC104_S_ACK_IDLE = 0, /* 无S帧等待完成;此时s_ack_nr必须为0 */
IEC104_S_ACK_SEND_PENDING /* S帧已生成;s_ack_nr冻结其实际N(R) */
} IEC104_SAckState_t;
/**
* 主动TESTFR探测状态;与dt_state和dt_procedure相互独立。
* 顺序为IDLE -> ACT_SEND_PENDING -> CON_PENDING -> IDLE。
* 调用方须先提交ACT完整发送结果,再处理TESTFR_CON;未等待时忽略CON,不缓存。
* Prepare_T3_Testfr按t3触发共用的内部准备路径,不创建另一套探测状态。
* TESTFR_ACT发送失败时Complete接口回到IDLE并返回FATAL。
*/
typedef enum {
IEC104_TESTFR_IDLE = 0, /* 没有正在进行的主动探测 */
IEC104_TESTFR_ACT_SEND_PENDING, /* TESTFR_ACT已生成,等待发送完成 */
IEC104_TESTFR_CON_PENDING /* TESTFR_ACT已完整发送,等待TESTFR_CON */
} IEC104_TestfrState_t;
/**
* 链路事件结果,必须与枚举值比较。
* 被控站START受理立即启用并生成CON,STOP受理立即停用并登记停止义务。
* 排空中的重复STOP返回IGNORED;停用IDLE下的STOP重新应答。
* CON完成仅检查发送结果;完成旧CON不改变当前启停状态。
* 控制站的多余CON仍按相应接口返回IGNORED;非法序号、角色、状态或发送失败为FATAL。
* FATAL由上层断链;失败前已受理的状态或发送事实不能回滚重放。
*/
typedef enum {
IEC104_LINK_RESULT_FATAL = -1, /* 当前连接必须由上层立即关闭 */
IEC104_LINK_RESULT_IGNORED = 0, /* 合法重复或没有匹配的在途过程 */
IEC104_LINK_RESULT_OK = 1, /* 本次处理成功,具体输出与状态变化见接口约定 */
} IEC104_LinkResult_t;
/** 主动停止请求的受理结果;除ACCEPTED外均不修改状态,必须按枚举值分支。 */
typedef enum {
IEC104_STOPDT_REQUEST_ERROR = -1, /* 参数、角色或状态非法 */
IEC104_STOPDT_REQUEST_ACCEPTED = 0, /* 已进入STOP_REQUESTED */
IEC104_STOPDT_REQUEST_ALREADY_STOPPED, /* 已STOPPED且没有启停过程 */
IEC104_STOPDT_REQUEST_IN_PROGRESS, /* 此前的主动停止仍在进行 */
IEC104_STOPDT_REQUEST_BUSY /* 另一个启停过程占用过程槽 */
} IEC104_StopdtRequestResult_t;
/** STOPDT报文准备结果;不是字节数,必须按枚举值分支。 */
typedef enum {
IEC104_STOPDT_PREPARE_ERROR = -1, /* 参数、角色、状态或编码结果非法 */
IEC104_STOPDT_NOT_ACTIVE = 0, /* 当前没有对应的停止流程 */
IEC104_STOPDT_FRAME_READY, /* 新报文及发送描述已生成 */
IEC104_STOPDT_WAIT_SEND, /* 等待已有S/ACT/CON的发送完成 */
IEC104_STOPDT_WAIT_PEER_ACK, /* 被控站等待对端确认本端I帧 */
IEC104_STOPDT_WAIT_PEER_CON /* 控制站等待STOPDT_CON */
} IEC104_StopdtPrepareResult_t;
/** STOPDT准备接口实际生成的报文类型,用于选择对应的发送完成接口。 */
typedef enum {
IEC104_STOPDT_FRAME_S_ACK, /* S确认:提交给Complete_S_Ack_Send */
IEC104_STOPDT_FRAME_ACT, /* 主动停止:提交给Complete_Stopdt_Act_Send */
IEC104_STOPDT_FRAME_CON /* 被动停止应答:提交给Complete_Stopdt_Con_Send */
} IEC104_StopdtFrameType_t;
/**
* STOPDT报文的发送描述,仅在Prepare返回FRAME_READY时写入。
* 调用方须与报文一起保存在同一FIFO节点中,最终发送结束后按frame_type分派
* 完成事件;frame_length为预期长度,Complete仍须传入实际最终发送字节数。
*/
typedef struct {
IEC104_StopdtFrameType_t frame_type; /* 本次生成的S确认、ACT或CON */
uint16_t frame_length; /* 完整APDU长度;当前三种报文均为6字节 */
} IEC104_StopdtTx_t;
/**
* I帧在Prepare成功时固定的发送快照。
*
* 调用方必须把该对象与当前发送报文关联保存,并在最终发送结束时原样
* 传给Complete_I_Send;不能用完成时刻的V(S)或V(R)重新构造快照。
*/
typedef struct {
uint16_t send_number; /* 本帧已预留且写入控制域的N(S),范围0..32767 */
uint16_t receive_number; /* 构帧时写入控制域的N(R),范围0..32767 */
uint16_t frame_length; /* 本帧完整APDU长度,用于判断是否最终完整发送 */
} IEC104_IFrameTx_t;
/**
* Link独占的I帧发送进度快照,SendBuffer不再复制这三个协议位置。
* 序号范围均为0..32767;ACK不能越过已提交本地完成的next_completion_ns。
* 顺序按循环距离检查,不能用普通整数大小比较回绕前后的序号。
* next_frame_ns在Prepare成功后推进;Manager完成整个跨模块事务后再核对
* 当前I帧、已提交ASDU数量和业务记录,不能在中间状态检查一致性。
*/
typedef struct {
uint16_t next_frame_ns; /* Link下一帧可分配的N(S) */
uint16_t next_completion_ns; /* 下一个尚未提交本地发送完成的I帧N(S) */
uint16_t peer_ack_nr; /* 已接受的对端累计确认位置,不代表本地完成 */
} IEC104_IFrameSendProgress_t;
/**
* 单条IEC-104连接的全部链路运行状态。
*
* 主要不变量:
* - vs、sent_vs、va、vr、acked_nr和s_ack_nr均为15位序号,按模32768运算;
* - distance(va, vs)是本端已经预留但尚未被对端确认的I帧数;
* - distance(sent_vs, vs)是尚未提交发送完成事件的I帧数,包含排队帧;
* - 上述两个区间均必须小于32768;Prepare同时保护两条边界,避免回绕歧义;
* - 先提交本地Complete,再处理覆盖该帧的ACK;distance(sent_vs,vs)<=distance(va,vs);
* - ACK仅允许推进到sent_vs,已完成未确认帧数为distance(va,sent_vs);
* - distance(acked_nr, vr)是已经接收但尚未由成功上线I/S帧确认的I帧数;
* - s_ack_state为IDLE时s_ack_nr必须为0;
* - role决定允许出现的dt_procedure,稳定状态由dt_state单独保存;
* - testfr_state独立于启停过程,可以与STARTDT/STOPDT过程并存。
* - 被控站仅使用IDLE或STOP_DRAINING;STOP_DRAINING必须同时为STOPPED。
*
* 本结构不包含锁、发送队列和时钟。一个连接上的所有Prepare、Handle、Complete
* 和Expired调用必须由上层串行化;TCP重连必须重新初始化状态并丢弃旧完成事件。
* 本结构不保存故障标志;Handle/Complete通过FATAL返回值报告致命故障,由连接层断连。
* 本结构保存单条连接的状态,连接组的选择/切换由上层负责。本实现允许控制站
* 在ACT之后、CON之前发送命令,适用条件见IEC104_Link_设计架构.md第3.2节;
* dt_state不能作为双向I帧的统一开关。
* 这些不变量依赖合法的事件序列维持;内部状态校验不保存发送队列历史,不能
* 证明完成事件身份或重建序号绕环次数。调用方不应直接改字段来跳过启停流程。
*/
typedef struct {
IEC104_LinkRole_t role; /* 本端固定角色;正常运行只能是两个有效角色之一 */
uint16_t vs; /* V(S):下一个尚未分配给发送队列I帧的N(S) */
uint16_t sent_vs; /* 下一个尚未提交发送完成事件的I帧N(S),按FIFO推进 */
uint16_t va; /* V(A):最早尚未被对端确认的本端发送序号 */
uint16_t vr; /* V(R):期望从对端收到的下一个I帧N(S) */
uint16_t acked_nr; /* 最近一次成功上线I/S帧实际携带的N(R) */
IEC104_SAckState_t s_ack_state; /* 普通确认及两端STOPDT共用的S帧发送进度 */
uint16_t s_ack_nr; /* 在途S帧携带的N(R)快照;IDLE时必须为0 */
IEC104_DataTransferState_t dt_state; /* 已提交的数据传输稳定状态 */
IEC104_DtProcedure_t dt_procedure; /* 当前唯一的STARTDT/STOPDT过程半态 */
IEC104_TestfrState_t testfr_state; /* 独立的主动TESTFR探测进度 */
} IEC104_LinkState_t;
/* 只读观察快照;连接层据unconfirmed_rx_count管理接收计时,不自行计算序号环距离。
* 启停/探测字段供诊断展示,是否应构帧须使用Can_Prepare查询,不能在外层组合状态条件。 */
typedef struct {
IEC104_LinkRole_t role; /* 本端固定角色 */
IEC104_DataTransferState_t dt_state; /* 已提交的启停稳定状态 */
IEC104_DtProcedure_t dt_procedure; /* 当前启停过程 */
IEC104_TestfrState_t testfr_state; /* 独立的主动探测进度 */
uint16_t unconfirmed_rx_count; /* 已接收且尚未由完整发送的I/S确认的I帧数 */
} IEC104_LinkStatus_t;
/* 完整校验后返回数值快照;1成功,0参数/状态非法,失败不改输出,不推进任何过程。 */
extern int IEC104_Link_Get_Status(const IEC104_LinkState_t *state, IEC104_LinkStatus_t *status);
/*
* 只读检查普通S是否应准备,与Prepare_S_Ack共用同一规划规则;w范围1..32767。
* immediate非0跳过w门限(例如t2已到期),仍检查启停门控、在途S及待确认数量。
* 1应准备,0当前无需/不能准备,-1参数或状态非法;不预留缓冲、不生成报文、不提交确认。
* 同一连接的查询和准备须串行执行;返回1后仍须处理实际Prepare/Manager排队结果。
*/
extern int IEC104_Link_Can_Prepare_S_Ack(const IEC104_LinkState_t *state, uint16_t w, int immediate);
/* 只读检查是否可开始一轮TESTFR;不检查t3时间。1可准备,0已有探测,-1状态非法。
* 与主动/t3构帧入口共用准入规则;查询不改变探测状态,调用方仍须处理实际准备结果。 */
extern int IEC104_Link_Can_Prepare_Testfr_Act(const IEC104_LinkState_t *state);
/**
* 适用端:控制站与被控站。
* 功能:把IEC 60870-5-104推荐默认参数写入一条连接的链路配置。
* 输入参数:config——待初始化的配置对象;允许为NULL。
* 输出参数:k=12、w=8、t1=15000ms、t2=10000ms、t3=20000ms。
* 返回值:无。
* 说明:本函数只复制默认值,不创建定时器;连接调度层可在初始化后按需改写。
*/
extern void IEC104_Link_Config_Default(IEC104_LinkConfig_t *config);
/**
* 适用端:控制站与被控站,由role参数固定当前连接的本端角色。
* 功能:为新连接初始化链路状态。
* 输入参数:state——待初始化的状态对象;role——CONTROLLING或CONTROLLED。
* 输出参数:成功时写入role,清零序号和确认快照,数据传输置STOPPED,
* 启停过程、S确认和TESTFR置IDLE。
* 返回值:1表示初始化成功;0表示state为NULL或role非法,此时不修改状态。
* 说明:仅初始化链路算法状态,连接配置由Config_Default单独初始化。
* 新连接处理任何链路事件前须初始化成功,不要求在TCP建连成功后才调用。
* 本模块未实现连接生命周期管理;旧事件隔离和状态内存安全复用由接入方负责。
*/
extern int IEC104_Link_State_Init(IEC104_LinkState_t *state,
IEC104_LinkRole_t role);
/**
* 功能:校验完整链路状态并读取I帧发送进度,不修改链路。
* 输入参数:state——本连接的链路状态。
* 输出参数:progress——成功时写入数值快照;失败时保持原值,不得与state重叠。
* 返回值:1表示读取成功;0表示指针或链路状态非法,不是链路事件结果。
* 说明:快照只表示调用时的进度,不预留序号,不证明TCP可用或事件属于本连接。
* 同一连接须串行调用;跨模块核对前须完成对应的Prepare/Commit、
* Complete/Commit_Send_Completion或Handle/原ASDU确认释放整个步骤。
*/
extern int IEC104_Link_Get_Send_Progress(
const IEC104_LinkState_t *state, IEC104_IFrameSendProgress_t *progress);
/**
* 功能:判断链路是否满足新连接绑定所需的初始状态条件,不修改状态。
* 输入参数:state——待检查链路,允许NULL。
* 输出参数:无。
* 返回值:1表示状态合法、序号与确认位置为0、STOPPED且各过程空闲;否则0。
* 说明:由Link维护初始状态规则;返回1不证明TCP为新连接,也不能用于故障恢复。
*/
extern int IEC104_Link_Is_Initial_State(const IEC104_LinkState_t *state);
/**
* 适用端:控制站与被控站。
* 功能:复位链路序号、S确认、启停和探测状态,清除待完成过程的算法记录。
* 输入参数:state——链路状态对象;允许为NULL。
* 输出参数:state——保留有效角色,序号及S快照清零,回到STOPPED/IDLE;
* S确认和TESTFR置IDLE。
* 返回值:无。
* 说明:本接口只复位链路算法字段,不能用于致命故障后在原连接上继续通信。
* 外部接入要求:调用方负责关闭故障连接,并防止旧事件提交到重新初始化后的状态;
* 对应连接管理、任务调度和定时器管理当前未实现,不属于本接口行为。
* 新连接处理链路事件前须用State_Init成功初始化完整状态并指定角色。
* 原角色非法时规范化为INVALID,不会自动选择角色;须用State_Init重新初始化。
* Reset会读取原role,不能对未初始化的自动变量调用本接口。
*/
extern void IEC104_Link_State_Reset(IEC104_LinkState_t *state);
/**
* 适用端:控制站与被控站。
* 功能:检查接收到的I帧序号,并在合法时更新接收和发送确认状态。
* 输入参数:state——链路状态;apci——已经通过基础格式检查的I帧APCI。
* 输出参数:成功时更新state->vr和state->va;未确认数量由acked_nr到vr的距离推导。
* 返回值:OK表示接收方向已经开放且序号合法并完成更新;FATAL表示接收方向未开放,
* 或参数、帧类型、状态、序号非法。
* 说明:控制站收到STARTDT_CON前禁止接收I帧;被控站受理START后立即允许接收。
* 被控站STOP受理后拒绝新I帧并返回FATAL;后到START成功受理后重新开放。
* 本端新I帧和S可在START_CON之后排队,实际发送不得越过已有控制帧。
* 控制站停止期间接收合法I帧后,上层须立即安排S帧。
* 本函数成功时只推进V(A)/V(R)。
* 捎带N(R)不得越过sent_vs;对应I帧的Complete须先于本次Handle提交。
*/
extern IEC104_LinkResult_t IEC104_Link_Handle_I_Frame(
IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci);
/**
* 适用端:控制站与被控站。
* 功能:检查接收到的S帧确认号,并在合法时更新V(A)。
* 输入参数:state——链路状态;apci——已经通过基础格式检查的S帧APCI。
* 输出参数:成功时将state->va更新为apci->receive_number。
* 返回值:OK表示接收阶段和确认号合法并完成更新;其他情况为FATAL且状态不变。
* 说明:STARTED时允许S;被控站STOPPED/STOP_DRAINING时仍接收ACK。
* 被控站STOPPED/IDLE及控制站等待STARTDT_CON时拒绝S帧。
* S帧接收只更新va,不提交发送完成边界sent_vs,也不推进接收确认位置acked_nr。
* N(R)不得越过sent_vs;异步接入须先提交对应发送结果,再分派相关ACK。
*/
extern IEC104_LinkResult_t IEC104_Link_Handle_S_Frame(
IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci);
/* 只读检查新I帧的启停、k窗口及序号边界,与Prepare_I_Frame共用规划逻辑。
* 返回1可准备,0当前门控/窗口阻塞,-1参数或状态非法;不编码、不预留N(S)。 */
extern int IEC104_Link_Can_Prepare_I_Frame(const IEC104_LinkState_t *state, uint16_t k);
/**
* 适用端:控制站与被控站;仅在本端数据传输方向允许时生成。
* 功能:检查数据传输状态和k窗口,构造I帧并在编码成功后预留其N(S);同连接调用须串行。
* 输入参数:state——链路状态;k——最大发送未确认I帧数,范围1..32767;
* asdu/asdu_len——完整ASDU及长度;frame/capacity——输出缓冲及容量;
* tx——由调用方随发送队列节点保存的序号和长度快照。
* 输出参数:成功时写入完整I帧和tx,并将V(S)循环加1。
* 返回值:成功返回完整APDU长度;0表示当前方向/阶段不允许、k窗口已满,
* 或新增预留将使未完成区间占满序号环;
* -1表示参数或链路状态非法。返回0/-1时不修改state、frame和tx。
* 说明:成功返回表示N(S)已经不可撤销地提交,报文必须进入该连接的严格FIFO队列;
* 两端STARTED/IDLE可预留;控制站还可在START_ACT_SEND_PENDING
* 和START_CON_WAITING预留,新I帧必须排在本次STARTDT_ACT之后。
* 调用方须先取得队列容量,将成功生成的ACT入队后再Prepare I帧;所有
* I/S/U帧在同一FIFO发送,先提交ACT完成再提交其后I帧完成。ACT失败须
* 丢弃整条连接及其队列,不能继续发送排在它后面的I帧。
* 后续发送失败必须断连,不得回滚V(S)。最终发送结果必须连同原tx传给
* IEC104_Link_Complete_I_Send(),完成阶段不能再次推进V(S)。
* 预留/排队时不启动等待确认的t1;发送层须自行限制排队和发送等待时间,
* 每次成功Prepare后恰好报告一次Complete,发送失败/超时也须报告失败长度。
*/
extern int IEC104_Link_Prepare_I_Frame(IEC104_LinkState_t *state,
uint16_t k,
const uint8_t *asdu,
uint16_t asdu_len,
uint8_t *frame,
uint16_t capacity,
IEC104_IFrameTx_t *tx);
/**
* 适用端:控制站与被控站。
* 功能:提交一帧由Prepare_I_Frame()生成的I帧最终本地发送结果。
* 输入参数:state——链路状态;tx——构帧成功时由调用方随队列节点保存的快照;
* sent_len——最终累计成功发送的字节数,等于tx->frame_length才算成功;发送失败也可传-1。
* 输出参数:完整发送、FIFO顺序及N(R)快照合法时更新acked_nr和sent_vs;
* 失败时均不推进,不回滚Prepare阶段的V(S)。
* 返回值:OK表示完整发送并成功提交;FATAL表示参数/状态非法、长度不符或
* 无待完成帧、完成顺序错误、N(R)快照失效。本接口没有IGNORED结果。
* 说明:Send限定为本地发送提交;返回OK不表示对端已经确认本帧。
* 部分写入尚在继续时不要调用;发送失败后上层连接管理器须断连。
* 本连接I/S完成事件须按发送FIFO恰好提交一次;本接口验证I帧N(S)==sent_vs。
* 发送层在整帧成功交给传输层时记录单调时间sent_at_ms并随完成事件传递;
* 不得用延迟执行回调时的当前时间代替实际完成时间。本接口不读取时钟。
* 成功后该帧进入等待ACK的区间;接收处理不得先于本次完成提交确认该帧。
* 完整APDU在传输层不再借用且本地完成已提交后可释放;等待ACK期间仍须保留
* 帧身份、序号快照、原始发送时间和业务关联,供确认、t1和故障回收使用。
* 业务原文的保留和重连恢复策略由发送管理层决定,链路完成不处理原文队列。
* 新连接不复用旧APDU、协议序号或完成事件。
*/
extern IEC104_LinkResult_t IEC104_Link_Complete_I_Send(
IEC104_LinkState_t *state, const IEC104_IFrameTx_t *tx, int sent_len);
/**
* 适用端:控制站与被控站。
* 功能:判断最早已完成发送但尚未被对端确认的I帧是否达到t1超时。
* 输入参数:state——链路状态;elapsed_ms——当前最早已完成未确认帧自整帧成功
* 交给传输层后经过的毫秒数,由调用方以该帧原始sent_at_ms计算;
* t1_ms——t1配置值,必须大于0。
* 输出参数:无,不修改链路状态。
* 返回值:1表示存在已完成未确认I帧且已经超时;0表示无此类帧或尚未超时;
* -1表示参数或链路序号状态非法。
* 说明:Complete_I_Send成功后,以及Handle_I/S_Frame成功推进V(A)后,调用方须
* 在同一串行调度区间重新选取最早已完成未确认帧;不存在则取消t1。
* 存在时以该帧原始发送完成时间作为起点;部分ACK后不可从当前时刻重计,
* 后续I帧Complete也不能延长前面未确认帧的截止时间。重复ACK不重置起点;
* 仅排队的帧不受此t1保护,超前于本地完成位置的ACK应被拒绝。
* 定时器只负责唤醒;回调必须校验连接/计时对象、重算当前帧elapsed_ms,
* 再调用本函数;只有返回1才按I帧确认超时断连。返回-1按非法状态处理。
* 本函数不保存时钟;使用宽位单调时间作差,超过uint32_t时饱和而非截断。
*/
extern int IEC104_Link_T1_I_Ack_Expired(
const IEC104_LinkState_t *state,
uint32_t elapsed_ms, uint32_t t1_ms);
/**
* 适用端:控制站与被控站。
* 功能:按w门限、显式立即确认或STOPDT排空要求生成一份普通S帧,并保存其N(R)快照。
* 输入参数:state——链路状态;w——接收确认门限,范围1..32767;
* immediate——非0跳过w门限,仍受启停状态和已有S在途限制;response——输出缓冲;
* capacity——缓冲实际容量,至少6字节。
* 输出参数:成功时response写入S帧,state转SEND_PENDING并保存构帧时V(R)。
* 返回值:6表示生成新S帧;0表示当前启停阶段不允许、未达到门限、
* 没有待确认数据或已有S帧在途;
* -1表示参数或状态非法。
* 说明:处于STOP_REQUESTED、STOP_ACT_SEND_PENDING、STOP_CON_WAITING或STOP_DRAINING时,
* 有待确认数据且无S在途时,即使immediate为0且未达到w,也会生成确认。生成不等于发送成功;
* 同一连接的I/S报文须按上线顺序串行发送,不能让较旧N(R)晚于较新N(R)上线。
* 被控站START受理后可准备S并排在CON之后;停用IDLE下不再准备普通S。
* 原接收时间和在途S快照不因启停改变。
*/
extern int IEC104_Link_Prepare_S_Ack(IEC104_LinkState_t *state,
uint16_t w, int immediate,
uint8_t *response, uint16_t capacity);
/**
* 适用端:控制站与被控站。
* 功能:提交公共S确认帧的最终发送结果,普通确认与两端STOPDT流程共用。
* 输入参数:state——链路状态;sent_len——最终累计发送字节数,恰好6表示成功。
* 输出参数:成功时用保存的N(R)推进acked_nr并回IDLE;发送失败或快照失效时
* 清除在途S帧且不推进确认位置,由上层断开连接。
* 返回值:OK表示完整发送并成功提交;IGNORED表示没有S帧等待完成;
* FATAL表示参数/状态非法、发送失败或快照失效。
* 说明:发送期间新收到的I帧仍保留在新acked_nr到V(R)之间;新连接初始化后必须
* 丢弃旧连接的完成事件。调用方必须与具体枚举值比较。
* 状态合法且无S在途时先返回IGNORED,不检查sent_len;状态非法则直接FATAL。
* S完成保留当前启停状态。这里的回IDLE仅指s_ack_state,不是dt_procedure。
* 普通确认及两端STOPDT中的S帧均由本接口完成。本接口不接收报文身份,
* 须按FIFO节点恰好分派一次。
*/
extern IEC104_LinkResult_t IEC104_Link_Complete_S_Ack_Send(
IEC104_LinkState_t *state, int sent_len);
/**
* 适用端:控制站与被控站。
* 功能:在最早已接收但本端尚未确认的I帧达到t2等待时间后生成S确认帧。
* 输入参数:state——链路状态;elapsed_ms——该接收帧等待本端确认的毫秒数;
* t2_ms——t2配置值,必须大于0;response——输出缓冲;
* capacity——缓冲实际容量,至少6字节。
* 输出参数:到期且可生成时写入S帧,并由普通S确认流程保存N(R)快照。
* 返回值:6表示生成S帧;0表示尚未到期、当前启停阶段不允许、
* 没有待确认I帧或已有S帧在途;
* -1表示参数或状态非法。
* 说明:本函数不读取系统时钟,也不维护计时起点;调用方必须使用单调时钟计算
* elapsed_ms。生成后仍由IEC104_Link_Complete_S_Ack_Send()提交最终发送结果。
* 即使尚未到期,也会完整校验参数和状态,非法输入返回-1。
* 启停及其他控制帧不重置t2,S由FIFO保证发送顺序。
*/
extern int IEC104_Link_Prepare_T2_Ack(IEC104_LinkState_t *state,
uint32_t elapsed_ms, uint32_t t2_ms,
uint8_t *response, uint16_t capacity);
/**
* 适用端:控制站与被控站;收到对端TESTFR_ACT时使用。
* 功能:独立处理已通过APCI解析的TESTFR_ACT,生成TESTFR_CON应答。
* 输入参数:apci——U帧且功能码为TESTFR_ACT;response——应答输出缓冲;
* capacity——缓冲实际容量,至少6字节。
* 输出参数:response——应答为68 04 83 00 00 00。
* 返回值:OK表示生成固定6字节应答;FATAL表示参数或报文非法。
* 说明:无状态接口,不修改启停状态、链路序号或I帧确认计数;
* 只生成应答,不执行发送,也不处理主动心跳及超时。
* TESTFR_CON发送没有对应链路Complete接口,发送失败由上层断连;不能
* 调用Complete_Testfr_Act_Send提交该应答,也不能据此结束本端主动探测。
*/
extern IEC104_LinkResult_t IEC104_Link_Handle_Testfr_Act(
const IEC104_ApciInfo_t *apci, uint8_t *response, uint16_t capacity);
/**
* 适用端:控制站与被控站。
* 功能:独立生成主动探测请求TESTFR_ACT,并标记等待最终发送结果。
* 输入参数:state——链路状态;request——请求输出缓冲;capacity——实际容量,至少6字节。
* 输出参数:request——写入68 04 43 00 00 00;state——探测转为ACT_SEND_PENDING。
* 返回值:6表示生成新请求;0表示已有探测进行中;-1表示参数或链路状态非法。
* 返回0或-1时不修改状态和输出缓冲。
* 说明:不执行网络发送;待发送完成和等待CON期间均不重复生成请求。
* 不受启停状态限制,不修改启停状态、序号及接收确认位置。
* 每次成功返回6都会建立一次完成义务:发送层必须保证恰好调用一次
* Complete_Testfr_Act_Send();发送超时或失败也必须用非6的最终长度报告。
* ACT完整发送后先提交Complete,再分派收到的TESTFR_CON,与主动启停顺序一致。
*/
extern int IEC104_Link_Prepare_Testfr_Act(IEC104_LinkState_t *state,
uint8_t *request,
uint16_t capacity);
/**
* 适用端:控制站与被控站。
* 功能:在链路空闲达到t3后生成主动探测请求TESTFR_ACT。
* 输入参数:state——链路状态;idle_elapsed_ms——距最近链路活动的毫秒数;
* t3_ms——t3配置值,必须大于0;request——请求输出缓冲;
* capacity——实际容量,至少6字节。
* 输出参数:到期且当前没有主动探测时生成TESTFR_ACT,并进入ACT_SEND_PENDING。
* 返回值:6表示生成新请求;0表示尚未到期或已有主动探测;
* -1表示参数或链路状态非法。返回0或-1时不修改状态和输出缓冲。
* 说明:本函数不读取时钟,也不维护最近活动时间;连接层统一定义有效链路活动,
* 据此更新t3的单调计时起点并计算idle_elapsed_ms,不得重置正在等待CON的t1起点。
* 返回6只表示生成报文,尚未发送;发送结果仍由Complete_Testfr_Act_Send()提交。
* 成功生成后同样要求发送层恰好报告一次最终完成结果。
* 即使尚未到期,也会完整校验参数和状态,非法输入返回-1。
*/
extern int IEC104_Link_Prepare_T3_Testfr(IEC104_LinkState_t *state,
uint32_t idle_elapsed_ms,
uint32_t t3_ms,
uint8_t *request,
uint16_t capacity);
/**
* 适用端:控制站与被控站。
* 功能:记录主动TESTFR_ACT的最终发送结果,成功后开始等待TESTFR_CON。
* 输入参数:state——链路状态;sent_len——最终成功发送的累计字节数;负值也表示失败。
* 输出参数:有匹配ACT时,恰好6字节转CON_PENDING;其他长度结束本次探测并回IDLE。
* 返回值:OK表示ACT完整发送并开始等待CON;
* IGNORED表示没有待完成的ACT;
* FATAL表示状态非法或发送失败。
* 说明:部分写入仍在继续时不要调用;发送失败后上层须断连,不得在原连接重试。
* 同一连接串行调用;须先提交ACT完整发送结果,再分派收到的TESTFR_CON。
* 状态合法但无匹配ACT时先返回IGNORED,不检查sent_len,也不修改状态。
* 新连接初始化后丢弃旧连接的发送完成和接收事件;调用方必须与具体枚举值比较;
* 本接口不修改启停及序号状态,也不操作外部定时器。返回OK后,由连接层以
* 本次ACT实际完整发送时刻为等待CON的t1起点,不能用延迟回调处理时刻代替。
*/
extern IEC104_LinkResult_t IEC104_Link_Complete_Testfr_Act_Send(
IEC104_LinkState_t *state, int sent_len);
/**
* 适用端:控制站与被控站;收到对端TESTFR_CON时使用。
* 功能:处理已通过APCI解析且当前正在等待的TESTFR_CON。
* 输入参数:state——链路状态;apci——U帧且功能码为TESTFR_CON。
* 输出参数:仅CON_PENDING时回IDLE;非OK时不修改状态。
* 返回值:OK表示接受当前探测的确认;IGNORED表示当前未等待CON;
* FATAL表示参数、报文或链路状态非法。
* 说明:不生成应答,不修改启停状态、序号和确认位置;输入校验先于等待状态检查。
* ACT_SEND_PENDING时返回IGNORED且不缓存CON;调用方须先完成本ACT的Complete。
* 收到对端TESTFR_ACT时须另行调用Handle_Testfr_Act生成应答,不结束本端的确认等待。
* 返回OK后由连接层取消本轮等待计时;本接口不操作外部定时器。
*/
extern IEC104_LinkResult_t IEC104_Link_Handle_Testfr_Con(
IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci);
/**
* 适用端:控制站与被控站。
* 功能:判断主动TESTFR_ACT发送成功后等待TESTFR_CON是否达到t1超时。
* 输入参数:state——链路状态;elapsed_ms——距当前探测ACT实际完整发送的毫秒数;
* t1_ms——t1配置值,必须大于0。
* 输出参数:无,不修改链路状态。
* 返回值:1表示正在等待TESTFR_CON且已经超时;0表示未等待或尚未超时;
* -1表示参数或链路状态非法。
* 说明:Complete_Testfr_Act_Send()返回OK后,由连接层以本次ACT实际完整发送时刻
* 为单调计时起点,Handle_Testfr_Con()返回OK后由连接层取消本轮等待计时;
* 重复请求、无匹配事件或t3活动更新均不得重置t1起点。
* 连接层定时器只用作唤醒;回调须校验连接代次,并在连接串行调度中按当前
* 有效探测的起点重新计算elapsed_ms,再调用本函数,只有返回1才按t1超时断连。
* 同一连接的旧探测回调也可能晚到,不能沿用它携带的旧起点或时间差判断新探测。
* 本函数不识别探测轮次,不读时钟,也不操作外部定时器或关闭连接。
* 状态或t1_ms非法时先返回-1;合法时ACT_SEND_PENDING返回0,该状态表示本地
* 发送结果尚未提交,须先Complete再处理CON。发送层须限定排队和发送期限,
* 成功Prepare后恰好一次Complete,发送超时也以失败长度报告,不能静默丢弃完成事件。
*/
extern int IEC104_Link_T1_Testfr_Con_Expired(
const IEC104_LinkState_t *state,
uint32_t elapsed_ms, uint32_t t1_ms);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_LINK_COMMON_H */
@@ -0,0 +1,80 @@
/**
* 链路模块内部的跨文件协作声明,不属于供业务调用的稳定公开API。
*
* “内部”指使用范围限于链路实现,不是C语言的static内部链接。
* 本文件声明的函数均定义在IEC104_Link.c,供公共链路及角色实现调用,
* 因此采用外部链接。角色状态组合校验是Link.c中的static函数,不在此声明。
* 若改为static,必须同时消除跨编译单元调用;仅移动或删除头文件声明无效。
* 只在一个.c内使用的辅助函数直接定义为static,不应加入本头文件。
*
* 校验调用顺序:Role_Valid -> State_Valid -> Link.c内对应角色的static State_Consistent。
* 最后一级不能反调前两级,否则形成递归。其余辅助函数依赖各自列出的前置条件。
* 同一连接的检查、编码、状态提交和完成事件由调用方串行化;这里不提供锁或调度。
* 业务调用方使用Common、Master_StartStop、Slave_StartStop公开头文件,或兼容总头文件IEC104_Link.h。
* SendManager通过公开的Get_Send_Progress和Is_Initial_State查询,不包含本头文件。
*/
#ifndef IEC104_LINK_INTERNAL_H
#define IEC104_LINK_INTERNAL_H
#include "IEC104_Link_Common.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* 功能:检查公共字段定义域、S状态/快照组合,以及当前角色的启停状态约束。
* 定义/调用:定义于IEC104_Link.c;公共接口及主站超时查询使用,角色入口经Role_Valid使用。
* 输入参数:state——待检查对象;允许NULL,返回0。
* 输出参数:无,不修改state。
* 返回值:通过检查为1,否则为0;不是IEC104_LinkResult_t事件结果。
* 说明:不判断TCP连接、FIFO事件身份或本次报文序号;这些由对应处理步骤负责。
*/
int IEC104_Link_State_Valid_Internal(const IEC104_LinkState_t *state);
/**
* 功能:完成上述状态检查,并要求本端角色与指定接口相符。
* 定义/调用:定义于IEC104_Link.c;主站和子站的角色专用入口调用。
* 输入参数:state——待检查对象;role——入口要求的CONTROLLING或CONTROLLED。
* 输出参数:无,不修改state。
* 返回值:状态合法且角色匹配为1;NULL、状态非法或角色不匹配为0。
* 说明:检查的是本端固定角色,不会设置角色,也不会推进启停过程。
*/
int IEC104_Link_Role_Valid_Internal(const IEC104_LinkState_t *state, IEC104_LinkRole_t role);
/**
* 功能:计算已收但尚未由成功发送的I/S帧确认的I帧数。
* 定义/调用:定义于IEC104_Link.c;公共确认逻辑和子站停止排空使用。
* 前置条件:state非NULL,acked_nr和vr为有效15位序号,未确认区间未占满序号环。
* 输出参数:无,不修改state;不重复执行完整状态校验。
* 返回值:从acked_nr到vr的循环距离,范围0..32767;0表示无待确认数据。
*/
uint16_t IEC104_Link_Unconfirmed_Rx_Internal(const IEC104_LinkState_t *state);
/**
* 功能:判断当前接收位置已确认,或已由在途S帧的N(R)完整覆盖。
* 定义/调用:定义于IEC104_Link.c;主站准备STOPDT_ACT及接收STOPDT_CON使用。
* 前置条件:state已通过完整状态校验;调用方保证同连接FIFO与事件串行顺序。
* 输出参数:无,不修改state。
* 返回值:acked_nr==vr,或在途S帧s_ack_nr==vr时为1,否则为0。
* 说明:返回1不等于S已发送完成,不能据此推进acked_nr;旧V(R)快照不算完整覆盖。
*/
int IEC104_Link_Rx_Ack_Covered_Internal(const IEC104_LinkState_t *state);
/**
* 功能:编码一帧S确认并保存它的实际N(R)快照,供普通确认及两端STOPDT复用。
* 定义/调用:定义于IEC104_Link.c;公共S准备、主站/子站停止准备调用。
* 前置条件:state已校验且无S帧在途;response非NULL且至少有IEC104_APCI_LEN字节;
* receive_number是本次选定的有效15位确认号。容量和是否应确认由调用方判断。
* 输出参数:成功写入response,设置s_ack_nr及S_ACK_SEND_PENDING;不更新acked_nr。
* 返回值:6表示构帧成功,-1表示编码失败且不提交状态;不执行网络发送。
* 说明:完整发送后才能通过Complete_S_Ack_Send提交确认;本函数不重新读取vr或规划报文。
*/
int IEC104_Link_Prepare_S_Ack_Internal(IEC104_LinkState_t *state,
uint8_t *response, uint16_t receive_number);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_LINK_INTERNAL_H */
@@ -0,0 +1,305 @@
/**
* 主站启停流程:发STARTDT_ACT -> 等STARTDT_CON;请求停止 -> 发STOPDT_ACT -> 等STOPDT_CON。
* 本端不应答对端STARTDT/STOPDT_ACT。共用序号、确认和状态校验由公共模块提供。
* Prepare仅生成报文,Complete提交最终发送结果;FIFO、实际发送和时钟由连接调度层负责。
*
* 主流程:START的Prepare -> 发送ACT -> Complete -> Handle CON;
* STOP的Request -> Prepare(必要时先S再ACT)-> 对应Complete -> Handle CON。
* 两个主动过程串行执行;重复或忙结果不产生新过程,也不授权连接层重置已有超时。
* 主站仅在*_CON_WAITING接受对应CON,不保存早于ACT完成的CON;调度层必须先
* 提交本地已知的ACT完整发送结果,再处理对端CON;TESTFR也采用相同顺序。
* 角色入口经公共Role_Valid完成检查;完整状态校验及角色组合规则集中在Link.c,
* 本文件只实现主站启停流程,不向公共模块提供反向校验入口。
*/
#include "IEC104_Link_Master_StartStop.h"
#include "IEC104_Link_Internal.h"
#include <stddef.h>
/**
* 功能:生成主动STARTDT_ACT请求并记录等待发送完成的状态。
* 输入参数:state——链路状态;request——输出缓冲;capacity——至少6字节。
* 输出参数:request——写入68 04 07 00 00 00;state——转ACT_SEND_PENDING。
* 返回值:6表示生成请求;0表示已有启停过程或稳定状态已为STARTED;
* -1表示参数或状态非法。返回0/-1时不修改状态和输出缓冲。
*/
/* 适用端:仅控制站。 */
int IEC104_Link_Prepare_Startdt_Act(IEC104_LinkState_t *state,
uint8_t *request, uint16_t capacity)
{
if (request == NULL || capacity < IEC104_APCI_LEN ||
!IEC104_Link_Role_Valid_Internal(state, IEC104_ROLE_CONTROLLING)) {
return -1;
}
if (state->dt_procedure != IEC104_DT_PROC_IDLE ||
state->dt_state != IEC104_DT_STOPPED) {
return 0;
}
/* 缓冲已验证且功能码固定,编码必定生成6字节;(void)表示有意忽略返回值。 */
(void)IEC104_Build_Uframe(request, IEC104_U_STARTDT_ACT);
state->dt_procedure = IEC104_DT_PROC_START_ACT_SEND_PENDING;
return IEC104_APCI_LEN;
}
/**
* 功能:根据主动STARTDT_ACT最终发送结果决定是否等待STARTDT_CON。
* 输入参数:state——链路状态;sent_len——最终累计发送字节数,恰好6表示成功。
* 输出参数:成功转START_CON_WAITING;失败结束本次主动请求并回IDLE。
* 返回值:OK表示完整发送并开始等待CON;IGNORED表示没有ACT等待完成;
* FATAL表示状态/角色非法或发送失败。
* 说明:角色和状态合法后先匹配START_ACT_SEND_PENDING,再检查sent_len。
* 无匹配过程时IGNORED不改状态;发送失败只清过程,不提交STARTED,须断连。
* Complete成功后才能分派STARTDT_CON,并以ACT实际完整发送时刻记录t1起点。
*/
/* 适用端:仅控制站。 */
IEC104_LinkResult_t IEC104_Link_Complete_Startdt_Act_Send(
IEC104_LinkState_t *state, int sent_len)
{
if (!IEC104_Link_Role_Valid_Internal(state, IEC104_ROLE_CONTROLLING)) {
return IEC104_LINK_RESULT_FATAL;
}
if (state->dt_procedure != IEC104_DT_PROC_START_ACT_SEND_PENDING) {
return IEC104_LINK_RESULT_IGNORED;
}
if (sent_len != IEC104_APCI_LEN) {
state->dt_procedure = IEC104_DT_PROC_IDLE;
return IEC104_LINK_RESULT_FATAL;
}
state->dt_procedure = IEC104_DT_PROC_START_CON_WAITING;
return IEC104_LINK_RESULT_OK;
}
/**
* 功能:接受主动启动正在等待的STARTDT_CON并提交数据传输启动。
* 输入参数:state——链路状态;apci——已解析的STARTDT_CON。
* 输出参数:主动请求回IDLE,数据传输状态进入或保持STARTED。
* 返回值:OK表示接受并提交;IGNORED表示当前没有等待STARTDT_CON;
* FATAL表示参数、角色、报文或状态非法。
* 说明:仅START_CON_WAITING可提交;ACT_SEND_PENDING时返回IGNORED且不暂存CON。
* 输入校验在过程匹配之前,即使没有等待过程,非法报文仍返回FATAL。
* 非OK时状态不变;成功也不重置序号、S确认或TESTFR。
*/
/* 适用端:仅控制站。 */
IEC104_LinkResult_t IEC104_Link_Handle_Startdt_Con(
IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci)
{
if (!IEC104_Link_Role_Valid_Internal(state, IEC104_ROLE_CONTROLLING) ||
apci == NULL ||
apci->frame_type != IEC104_FRAME_TYPE_U ||
apci->u_function != IEC104_U_STARTDT_CON) {
return IEC104_LINK_RESULT_FATAL;
}
if (state->dt_procedure != IEC104_DT_PROC_START_CON_WAITING) {
return IEC104_LINK_RESULT_IGNORED;
}
state->dt_procedure = IEC104_DT_PROC_IDLE;
state->dt_state = IEC104_DT_STARTED;
return IEC104_LINK_RESULT_OK;
}
/**
* 功能:判断主动STARTDT流程等待STARTDT_CON是否达到t1超时。
* 输入参数:state——链路状态;elapsed_ms——已经等待的毫秒数;
* t1_ms——t1配置值。
* 输出参数:无,不修改state。
* 返回值:1表示已经超时;0表示未等待CON或尚未超时;-1表示参数或状态非法。
* 说明:两种合法角色均可查询;仅控制站可能处于对应等待过程,被控站返回0。
* 本函数只比较调用方提供的elapsed_ms,不创建定时器,也不覆盖ACT排队/发送时间。
* 调用方持续使用本次ACT实际完整发送时刻计算elapsed_ms,不能因重复请求改用当前时刻。
*/
/* 适用端:控制站与被控站;实际超时只适用于控制站等待CON的过程。 */
int IEC104_Link_T1_Startdt_Con_Expired(const IEC104_LinkState_t *state,
uint32_t elapsed_ms,
uint32_t t1_ms)
{
if (!IEC104_Link_State_Valid_Internal(state) || t1_ms == 0U) {
return -1;
}
if (state->dt_procedure != IEC104_DT_PROC_START_CON_WAITING) {
return 0;
}
return elapsed_ms >= t1_ms ? 1 : 0;
}
/**
* 功能:控制站受理主动停止意图,不构帧;受理后立即关闭新业务I帧发送入口。
* 输入参数:state——控制站链路状态。
* 输出参数:ACCEPTED时dt_procedure转为STOP_REQUESTED,保留稳定状态和序号。
* 返回值:ACCEPTED表示新受理;ALREADY_STOPPED表示已经停止;IN_PROGRESS表示
* 停止已在进行;BUSY表示存在其他启停过程;REQUEST_ERROR表示参数、角色
* 或状态非法。除ACCEPTED外均不修改状态。
* 说明:待确认接收数据不阻止受理;Prepare_Stopdt_Request负责后续S确认/ACT。
* 只有ACCEPTED代表新过程;重复请求不重置连接层的准备/排队或CON等待计时。
*/
/* 适用端:仅控制站。 */
IEC104_StopdtRequestResult_t IEC104_Link_Request_Stopdt(
IEC104_LinkState_t *state)
{
if (!IEC104_Link_Role_Valid_Internal(state, IEC104_ROLE_CONTROLLING)) {
return IEC104_STOPDT_REQUEST_ERROR;
}
switch (state->dt_procedure) {
case IEC104_DT_PROC_STOP_REQUESTED:
case IEC104_DT_PROC_STOP_ACT_SEND_PENDING:
case IEC104_DT_PROC_STOP_CON_WAITING:
return IEC104_STOPDT_REQUEST_IN_PROGRESS;
case IEC104_DT_PROC_IDLE:
break;
default:
return IEC104_STOPDT_REQUEST_BUSY;
}
if (state->dt_state == IEC104_DT_STOPPED) {
return IEC104_STOPDT_REQUEST_ALREADY_STOPPED;
}
state->dt_procedure = IEC104_DT_PROC_STOP_REQUESTED;
return IEC104_STOPDT_REQUEST_ACCEPTED;
}
/**
* 功能:推进已受理的主动停止,按需生成S确认或STOPDT_ACT。
* 输入参数:state——控制站链路状态;request——输出缓冲;capacity——至少6字节;
* tx——报文类型和长度的输出地址,须随报文保存在FIFO节点中。
* 输出参数:FRAME_READY时写入request和tx,并提交S确认或STOPDT_ACT待发送状态。
* 返回值:FRAME_READY表示生成报文;NOT_ACTIVE表示没有对应停止过程;
* WAIT_SEND表示等待已有S/ACT发送完成;WAIT_PEER_CON表示等待STOPDT_CON;
* PREPARE_ERROR表示参数、角色、状态或编码结果非法。
* 非FRAME_READY时不修改状态和输出,也不丢失已受理的停止请求。
* 说明:调用前先用Request_Stopdt受理;在途S帧覆盖当前V(R)时允许ACT紧随排队。
* 按tx.frame_type选择Complete_S_Ack_Send或Complete_Stopdt_Act_Send,按FIFO提交完成事件。
* 构造S时STOP_REQUESTED保持不变,可再次Prepare尝试把ACT排在该S之后;
* 构造ACT不要求va==vs,对端I帧确认是否排空留到Handle_Stopdt_Con检查。
*/
/* 适用端:仅控制站。 */
IEC104_StopdtPrepareResult_t IEC104_Link_Prepare_Stopdt_Request(
IEC104_LinkState_t *state, uint8_t *request, uint16_t capacity,
IEC104_StopdtTx_t *tx)
{
if (!IEC104_Link_Role_Valid_Internal(state, IEC104_ROLE_CONTROLLING) || request == NULL ||
capacity < IEC104_APCI_LEN || tx == NULL) {
return IEC104_STOPDT_PREPARE_ERROR;
}
if (state->dt_procedure == IEC104_DT_PROC_STOP_ACT_SEND_PENDING) {
return IEC104_STOPDT_WAIT_SEND;
}
if (state->dt_procedure == IEC104_DT_PROC_STOP_CON_WAITING) {
return IEC104_STOPDT_WAIT_PEER_CON;
}
if (state->dt_procedure != IEC104_DT_PROC_STOP_REQUESTED) {
return IEC104_STOPDT_NOT_ACTIVE;
}
if (!IEC104_Link_Rx_Ack_Covered_Internal(state)) {
/* 在途S只确认旧V(R)时,先等它完成,再生成最新确认。 */
if (state->s_ack_state == IEC104_S_ACK_SEND_PENDING) {
return IEC104_STOPDT_WAIT_SEND;
}
if (IEC104_Link_Prepare_S_Ack_Internal(state, request, state->vr) != IEC104_APCI_LEN) {
return IEC104_STOPDT_PREPARE_ERROR;
}
tx->frame_type = IEC104_STOPDT_FRAME_S_ACK;
} else {
/* 在途S已覆盖当前V(R)时,ACT可以紧随其后排队。 */
(void)IEC104_Build_Uframe(request, IEC104_U_STOPDT_ACT);
state->dt_procedure = IEC104_DT_PROC_STOP_ACT_SEND_PENDING;
tx->frame_type = IEC104_STOPDT_FRAME_ACT;
}
tx->frame_length = IEC104_APCI_LEN;
return IEC104_STOPDT_FRAME_READY;
}
/**
* 功能:根据主动STOPDT_ACT最终发送结果决定是否等待STOPDT_CON。
* 输入参数:state——链路状态;sent_len——最终累计发送字节数,恰好6表示成功。
* 输出参数:成功转STOP_CON_WAITING;失败结束主动请求并要求上层断连。
* 返回值:OK表示完整发送并开始等待CON;IGNORED表示没有ACT等待完成;
* FATAL表示状态/角色非法或发送失败。
* 说明:角色和状态合法后先匹配STOP_ACT_SEND_PENDING,再检查sent_len。
* 发送失败将过程清为IDLE,但dt_state仍为STARTED;FATAL后必须断连,
* 不能因稳定状态仍为STARTED而继续发送业务I帧。
* 本ACT的Complete必须先于Handle_Stopdt_Con提交;它不代替前序S帧的Complete。
*/
/* 适用端:仅控制站。 */
IEC104_LinkResult_t IEC104_Link_Complete_Stopdt_Act_Send(
IEC104_LinkState_t *state, int sent_len)
{
if (!IEC104_Link_Role_Valid_Internal(state, IEC104_ROLE_CONTROLLING)) {
return IEC104_LINK_RESULT_FATAL;
}
if (state->dt_procedure != IEC104_DT_PROC_STOP_ACT_SEND_PENDING) {
return IEC104_LINK_RESULT_IGNORED;
}
if (sent_len != IEC104_APCI_LEN) {
state->dt_procedure = IEC104_DT_PROC_IDLE;
return IEC104_LINK_RESULT_FATAL;
}
state->dt_procedure = IEC104_DT_PROC_STOP_CON_WAITING;
return IEC104_LINK_RESULT_OK;
}
/**
* 功能:接受主动停止正在等待的STOPDT_CON并提交数据传输停止。
* 输入参数:state——链路状态;apci——已解析的STOPDT_CON。
* 输出参数:主动请求回IDLE,数据传输状态进入或保持STOPPED。
* 返回值:OK表示接受并提交;IGNORED表示当前没有等待STOPDT_CON;
* FATAL表示参数、角色、报文、状态非法,或发送方向未排空/接收确认未覆盖。
* 说明:在途S帧完整覆盖V(R)时允许CON先于S完成回调;保留S待完成状态,
* 后续Complete_S_Ack_Send成功才推进acked_nr,发送失败仍返回FATAL要求断连。
* 此处允许晚到的是S完成事件,不是本ACT完成事件;STOP_ACT_SEND_PENDING
* 时CON返回IGNORED且不暂存。排空检查va==vs表示对端已确认全部预留I帧,
* 公共序号约束保证此时sent_vs也已到达vs,I帧完成须已由调度层按序提交。
*/
/* 适用端:仅控制站。 */
IEC104_LinkResult_t IEC104_Link_Handle_Stopdt_Con(
IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci)
{
if (!IEC104_Link_Role_Valid_Internal(state, IEC104_ROLE_CONTROLLING) ||
apci == NULL ||
apci->frame_type != IEC104_FRAME_TYPE_U ||
apci->u_function != IEC104_U_STOPDT_CON) {
return IEC104_LINK_RESULT_FATAL;
}
if (state->dt_procedure != IEC104_DT_PROC_STOP_CON_WAITING) {
return IEC104_LINK_RESULT_IGNORED;
}
if (state->va != state->vs || !IEC104_Link_Rx_Ack_Covered_Internal(state)) {
return IEC104_LINK_RESULT_FATAL;
}
state->dt_procedure = IEC104_DT_PROC_IDLE;
state->dt_state = IEC104_DT_STOPPED;
return IEC104_LINK_RESULT_OK;
}
/**
* 功能:判断主动STOPDT流程等待STOPDT_CON是否达到t1超时。
* 输入参数:state——链路状态;elapsed_ms——已经等待的毫秒数;
* t1_ms——t1配置值。
* 输出参数:无,不修改state。
* 返回值:1表示已经超时;0表示未等待CON或尚未超时;-1表示参数或状态非法。
* 说明:两种合法角色均可查询;仅控制站可能处于对应等待过程,被控站返回0。
* elapsed_ms始终从本次ACT实际完整发送时刻计算;重复请求或I/S收发不重置起点。
* 本函数不保存时钟,也不能用作被控站STOP_DRAINING的排空计时器。
*/
/* 适用端:控制站与被控站;实际超时只适用于控制站等待CON的过程。 */
int IEC104_Link_T1_Stopdt_Con_Expired(const IEC104_LinkState_t *state,
uint32_t elapsed_ms,
uint32_t t1_ms)
{
if (!IEC104_Link_State_Valid_Internal(state) || t1_ms == 0U) {
return -1;
}
if (state->dt_procedure != IEC104_DT_PROC_STOP_CON_WAITING) {
return 0;
}
return elapsed_ms >= t1_ms ? 1 : 0;
}
@@ -0,0 +1,174 @@
/**
* 主站启停公开接口:本端为CONTROLLING,主动发送STARTDT/STOPDT_ACT并接收CON。
* START:Prepare -> 实际发送 -> Complete -> Handle CON;
* STOP:Request -> Prepare(S/ACT)-> 按tx类型发送并Complete -> Handle CON。
* 主站的两个CON处理接口都只接受CON_WAITING过程,不缓存ACT完成前的CON;
* 调用方必须先提交已知的ACT完整发送结果。I/S与TESTFR使用Common.h的公共接口。
* 主动启停须等本次CON成功处理后再发起下一过程;忙或重复请求不得重发ACT或重启计时。
* 实现见Master_StartStop.c;Internal.h仅供实现协作,不供业务调用。
*/
#ifndef IEC104_LINK_MASTER_STARTSTOP_H
#define IEC104_LINK_MASTER_STARTSTOP_H
#include "IEC104_Link_Common.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* 适用端:仅控制站。
* 功能:生成主动启动请求STARTDT_ACT,并记录等待最终发送结果的状态。
* 输入参数:state——链路状态;request——输出缓冲;capacity——实际容量,至少6字节。
* 输出参数:request——写入68 04 07 00 00 00;state——主动请求转ACT_SEND_PENDING。
* 返回值:6表示生成新请求;0表示已有启停过程或链路已启动;
* -1表示参数或状态非法。返回0或-1时不修改状态和输出缓冲。
* 说明:仅控制站可调用,只允许从STOPPED且过程IDLE时开始;不执行网络发送,不修改序号。
* 返回6后必须提交最终发送结果;同一连接的三个主动启动接口须串行调用。
* ACT成功入队后,控制站可预留命令I帧,不必等待CON;ACT必须在
* 同一FIFO中先于这些I帧发送和提交Complete。未受理STARTDT前仍禁止I帧。
*/
extern int IEC104_Link_Prepare_Startdt_Act(IEC104_LinkState_t *state,
uint8_t *request,
uint16_t capacity);
/**
* 适用端:仅控制站。
* 功能:记录主动STARTDT_ACT的最终发送结果。
* 输入参数:state——链路状态;sent_len——最终累计成功发送字节数,恰好6表示成功。
* 输出参数:有匹配ACT时,完整成功转START_CON_WAITING,发送失败回IDLE;
* 无匹配过程或状态/角色非法时不修改状态。所有分支均不改变稳定状态。
* 返回值:OK表示完整发送并开始等待CON;IGNORED表示没有ACT等待完成;
* FATAL表示状态/角色非法或发送失败。
* 说明:失败后上层须断连。部分写入未结束时不要调用;调用方必须与具体枚举值比较。
* 状态合法但无匹配ACT时先返回IGNORED,不检查sent_len;成功提交后才处理CON。
* ACT失败后不得继续发送其后已预留的I帧,也不得回滚vs后复用当前连接。
*/
extern IEC104_LinkResult_t IEC104_Link_Complete_Startdt_Act_Send(
IEC104_LinkState_t *state, int sent_len);
/**
* 适用端:仅控制站。
* 功能:接受正在等待的STARTDT_CON,完成本端主动启动。
* 输入参数:state——链路状态;apci——已通过APCI解析的STARTDT_CON。
* 输出参数:过程回IDLE,dt_state进入STARTED。
* 返回值:OK表示接受并提交;IGNORED表示当前未等待CON;
* FATAL表示参数、角色、报文或状态非法。
* 说明:仅控制站可调用;不修改序号、确认位置及TESTFR状态。
* START_ACT_SEND_PENDING时返回IGNORED,不保存提前CON;须先完成本ACT的Complete。
* 报文/角色/状态检查先于等待过程检查,非法输入不会因当前未等待而被忽略。
*/
extern IEC104_LinkResult_t IEC104_Link_Handle_Startdt_Con(
IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci);
/**
* 适用端:控制站与被控站;实际超时只适用于控制站等待CON的过程。
* 功能:判断主动STARTDT_ACT发送成功后等待STARTDT_CON是否达到t1超时。
* 输入参数:state——链路状态;elapsed_ms——等待STARTDT_CON的毫秒数;
* t1_ms——t1配置值,必须大于0。
* 输出参数:无,不修改链路状态。
* 返回值:1表示正在等待STARTDT_CON且已经超时;0表示未等待或尚未超时;
* -1表示参数或链路状态非法。
* 说明:调用方在Complete_Startdt_Act_Send()成功后,以ACT实际完整发送时刻为单调计时起点,
* Handle_Startdt_Con()成功时取消计时;返回1后的断连由调用方执行。
* 同一次ACT的计时起点只记录一次;重复调用、业务收发或无匹配的完成事件不重置起点。
* 合法被控站没有对应等待过程,返回0;角色与过程组合非法仍返回-1。
*/
extern int IEC104_Link_T1_Startdt_Con_Expired(
const IEC104_LinkState_t *state,
uint32_t elapsed_ms, uint32_t t1_ms);
/**
* 适用端:仅控制站。
* 功能:受理主动停止意图,进入STOP_REQUESTED,立即禁止新的业务I帧预留。
* 输入参数:state——控制站链路状态。
* 输出参数:ACCEPTED时dt_procedure转为STOP_REQUESTED,保留稳定状态和序号。
* 返回值:ACCEPTED表示新受理;ALREADY_STOPPED表示已经停止;IN_PROGRESS表示
* 停止已在进行;BUSY表示存在其他启停过程;REQUEST_ERROR表示参数、角色
* 或状态非法。除ACCEPTED外均不修改状态。
* 说明:不构帧;重复请求不重置进度。受理后调用Prepare_Stopdt_Request推进,
* 接收数据和发送完成事件到达后继续推进;等待期间停止意图始终保留。
* 只有ACCEPTED代表新过程;其余结果不得重启已有的准备/排队或CON等待计时。
* 等待CON的t1须等ACT完整发送成功后开始,不能从本接口受理时刻开始计算。
*/
extern IEC104_StopdtRequestResult_t IEC104_Link_Request_Stopdt(
IEC104_LinkState_t *state);
/**
* 适用端:仅控制站。
* 功能:推进已受理的主动停止,按需生成S确认或STOPDT_ACT。
* 输入参数:state——控制站链路状态;request——输出缓冲;capacity——至少6字节;
* tx——本次报文类型和长度的输出地址,须随报文保存在FIFO节点中。
* 输出参数:FRAME_READY时写入request和tx,并提交S确认或STOPDT_ACT待发送状态。
* 返回值:FRAME_READY表示生成报文;NOT_ACTIVE表示没有对应停止过程;
* WAIT_SEND表示等待已有S/ACT发送完成;WAIT_PEER_CON表示等待STOPDT_CON;
* PREPARE_ERROR表示参数、角色、状态或编码结果非法。
* 非FRAME_READY时不修改状态和输出;NOT_ACTIVE不会隐式受理停止请求。
* 说明:在途S帧覆盖当前V(R)时可生成ACT,否则等待S完成后补确认。
* 本端未获确认的I帧不阻止生成ACT;收到CON时检查V(A)==V(S)及接收确认覆盖。
* 调用方须先取得有界FIFO节点,再构帧并按生成顺序排队;无法入队须断连。
* 将tx与报文共同保存:S_ACK完成调用Complete_S_Ack_Send,ACT完成调用
* Complete_Stopdt_Act_Send。
* 同一连接按FIFO提交完成事件,不能因当前状态改变而重新猜测报文类型。
* 等待CON的t1仍从ACT完整发送成功开始;准备/排队超时由连接层管理。
*/
extern IEC104_StopdtPrepareResult_t IEC104_Link_Prepare_Stopdt_Request(
IEC104_LinkState_t *state, uint8_t *request, uint16_t capacity,
IEC104_StopdtTx_t *tx);
/**
* 适用端:仅控制站。
* 功能:记录主动STOPDT_ACT的最终发送结果。
* 输入参数:state——链路状态;sent_len——最终累计成功发送字节数,恰好6表示成功。
* 输出参数:有匹配ACT时,完整成功转STOP_CON_WAITING,发送失败回IDLE;
* 无匹配过程或状态/角色非法时不修改状态。
* 返回值:OK表示完整发送并开始等待CON;IGNORED表示没有ACT等待完成;
* FATAL表示状态/角色非法或发送失败。
* 说明:失败后上层须断连。部分写入未结束时不要调用;调用方必须与具体枚举值比较。
* 状态合法但无匹配ACT时先返回IGNORED,不检查sent_len;匹配ACT发送失败时
* dt_state仍为STARTED,不能因过程回IDLE而继续业务发送。
* 成功提交后才处理STOPDT_CON;本接口不能代替前序S帧的Complete_S_Ack_Send。
*/
extern IEC104_LinkResult_t IEC104_Link_Complete_Stopdt_Act_Send(
IEC104_LinkState_t *state, int sent_len);
/**
* 适用端:仅控制站。
* 功能:接受正在等待的STOPDT_CON,完成本端主动停止。
* 输入参数:state——链路状态;apci——已通过APCI解析的STOPDT_CON。
* 输出参数:过程回IDLE,dt_state进入STOPPED。
* 返回值:OK表示接受并提交;IGNORED表示当前未等待CON;FATAL表示参数、角色、
* 报文或状态非法、发送方向未排空,或接收数据未确认且无完整覆盖的在途S帧。
* 说明:要求V(A)==V(S),并且acked_nr==V(R)或在途S帧s_ack_nr==V(R)。
* 覆盖完整时允许CON早于本地S完成回调;保留S待完成状态和确认快照,
* 后续Complete_S_Ack_Send成功才推进确认位置,发送失败仍要求上层断连。
* 非OK结果不修改状态;始终不修改序号、确认位置及TESTFR状态。
* STOP_ACT_SEND_PENDING时返回IGNORED且不缓存CON;允许S完成回调晚到,
* 不代表允许本ACT完成回调晚到。V(A)==V(S)时,公共序号约束保证
* sent_vs也已经到达vs;这些I帧的本地发送完成必须先于ACK提交。
*/
extern IEC104_LinkResult_t IEC104_Link_Handle_Stopdt_Con(
IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci);
/**
* 适用端:控制站与被控站;实际超时只适用于控制站等待CON的过程。
* 功能:判断主动STOPDT_ACT发送成功后等待STOPDT_CON是否达到t1超时。
* 输入参数:state——链路状态;elapsed_ms——等待STOPDT_CON的毫秒数;
* t1_ms——t1配置值,必须大于0。
* 输出参数:无,不修改链路状态。
* 返回值:1表示正在等待STOPDT_CON且已经超时;0表示未等待或尚未超时;
* -1表示参数或链路状态非法。
* 说明:调用方在Complete_Stopdt_Act_Send()成功后,以ACT实际完整发送时刻为单调计时起点,
* Handle_Stopdt_Con()成功时取消计时;返回1后的断连由调用方执行。
* 同一次ACT的计时起点只记录一次;重复请求、I/S收发及WAIT_*结果均不重置起点。
* 合法被控站没有对应等待过程,返回0;角色与过程组合非法仍返回-1。
*/
extern int IEC104_Link_T1_Stopdt_Con_Expired(
const IEC104_LinkState_t *state,
uint32_t elapsed_ms, uint32_t t1_ms);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_LINK_MASTER_STARTSTOP_H */
@@ -0,0 +1,111 @@
/**
* 子站启停的受理语义:START立即激活,STOP立即停用并等待确认。
* CON是发送任务,完成时不回写启停状态;后到START可结束尚未生成CON的停止等待。
* 已生成的CON始终按FIFO完成;不暂存相反方向ACT,不重置序号,所有调用串行。
*/
#include "IEC104_Link_Slave_StartStop.h"
#include "IEC104_Link_Internal.h"
#include <stddef.h>
/* 调用方先预留发送空间;OK受理START并生成一帧CON,非法输入不修改状态。 */
IEC104_LinkResult_t IEC104_Link_Handle_Startdt_Act(
IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci,
uint8_t *response, uint16_t capacity)
{
if (response == NULL || capacity < IEC104_APCI_LEN ||
!IEC104_Link_Role_Valid_Internal(state, IEC104_ROLE_CONTROLLED) ||
apci == NULL || apci->frame_type != IEC104_FRAME_TYPE_U ||
apci->u_function != IEC104_U_STARTDT_ACT) {
return IEC104_LINK_RESULT_FATAL;
}
(void)IEC104_Build_Uframe(response, IEC104_U_STARTDT_CON);
state->dt_state = IEC104_DT_STARTED;
state->dt_procedure = IEC104_DT_PROC_IDLE;
return IEC104_LINK_RESULT_OK;
}
/* CON可能属于后续STOP之前的请求;完成不能重新激活链路。 */
IEC104_LinkResult_t IEC104_Link_Complete_Startdt_Con_Send(
IEC104_LinkState_t *state, int sent_len)
{
return IEC104_Link_Role_Valid_Internal(state, IEC104_ROLE_CONTROLLED) &&
sent_len == IEC104_APCI_LEN ? IEC104_LINK_RESULT_OK : IEC104_LINK_RESULT_FATAL;
}
/* 首个START_CON是否已写出不影响STOP受理;排空中的重复STOP不重启期限。 */
IEC104_LinkResult_t IEC104_Link_Handle_Stopdt_Act(
IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci)
{
if (!IEC104_Link_Role_Valid_Internal(state, IEC104_ROLE_CONTROLLED) ||
apci == NULL || apci->frame_type != IEC104_FRAME_TYPE_U ||
apci->u_function != IEC104_U_STOPDT_ACT) {
return IEC104_LINK_RESULT_FATAL;
}
if (state->dt_procedure == IEC104_DT_PROC_STOP_DRAINING) {
return IEC104_LINK_RESULT_IGNORED;
}
state->dt_state = IEC104_DT_STOPPED;
state->dt_procedure = IEC104_DT_PROC_STOP_DRAINING;
return IEC104_LINK_RESULT_OK;
}
/* 覆盖全部接收数据的在途S已在FIFO中,可以先于CON排队,无须等实际写完。 */
static IEC104_StopdtPrepareResult_t Stopdt_Response_Status(const IEC104_LinkState_t *state)
{
if (state->dt_procedure != IEC104_DT_PROC_STOP_DRAINING) {
return IEC104_STOPDT_NOT_ACTIVE;
}
if (state->s_ack_state == IEC104_S_ACK_SEND_PENDING) {
if (state->s_ack_nr != state->vr) {
return IEC104_STOPDT_WAIT_SEND;
}
} else if (IEC104_Link_Unconfirmed_Rx_Internal(state) > 0U) {
return IEC104_STOPDT_FRAME_READY;
}
return state->va != state->vs ? IEC104_STOPDT_WAIT_PEER_ACK : IEC104_STOPDT_FRAME_READY;
}
int IEC104_Link_Can_Prepare_Stopdt_Response(const IEC104_LinkState_t *state)
{
if (!IEC104_Link_Role_Valid_Internal(state, IEC104_ROLE_CONTROLLED)) {
return -1;
}
return Stopdt_Response_Status(state) == IEC104_STOPDT_FRAME_READY;
}
/* 先排S确认,全部已预留I帧获ACK后排STOP_CON;生成CON即结束协议排空状态。 */
IEC104_StopdtPrepareResult_t IEC104_Link_Prepare_Stopdt_Response(
IEC104_LinkState_t *state, uint8_t *response, uint16_t capacity,
IEC104_StopdtTx_t *tx)
{
IEC104_StopdtPrepareResult_t readiness;
if (!IEC104_Link_Role_Valid_Internal(state, IEC104_ROLE_CONTROLLED) || response == NULL ||
capacity < IEC104_APCI_LEN || tx == NULL) {
return IEC104_STOPDT_PREPARE_ERROR;
}
readiness = Stopdt_Response_Status(state);
if (readiness != IEC104_STOPDT_FRAME_READY) {
return readiness;
}
if (state->s_ack_state == IEC104_S_ACK_IDLE && IEC104_Link_Unconfirmed_Rx_Internal(state) > 0U) {
if (IEC104_Link_Prepare_S_Ack_Internal(state, response, state->vr) != IEC104_APCI_LEN) {
return IEC104_STOPDT_PREPARE_ERROR;
}
tx->frame_type = IEC104_STOPDT_FRAME_S_ACK;
} else {
(void)IEC104_Build_Uframe(response, IEC104_U_STOPDT_CON);
state->dt_procedure = IEC104_DT_PROC_IDLE;
tx->frame_type = IEC104_STOPDT_FRAME_CON;
}
tx->frame_length = IEC104_APCI_LEN;
return IEC104_STOPDT_FRAME_READY;
}
/* 后到START可以已经激活链路,旧STOP_CON完成不得覆盖它。 */
IEC104_LinkResult_t IEC104_Link_Complete_Stopdt_Con_Send(
IEC104_LinkState_t *state, int sent_len)
{
return IEC104_Link_Role_Valid_Internal(state, IEC104_ROLE_CONTROLLED) &&
sent_len == IEC104_APCI_LEN ? IEC104_LINK_RESULT_OK : IEC104_LINK_RESULT_FATAL;
}
@@ -0,0 +1,56 @@
/*
* 被控站STARTDT/STOPDT:受理时改变传输状态,发送完成只校验CON结果。
* 本层不执行socket I/O、不维护发送队列、不计时;所有调用必须串行。
*/
#ifndef IEC104_LINK_SLAVE_STARTSTOP_H
#define IEC104_LINK_SLAVE_STARTSTOP_H
#include "IEC104_Link_Common.h"
#ifdef __cplusplus
extern "C" {
#endif
/*
* 每个合法STARTDT_ACT生成6字节CON,立即置STARTED/IDLE,可结束停止等待。
* 先预留发送空间再调用,OK后按FIFO提交;提交失败须断链,不能回滚后重放。
* 不暂存相反请求、不撤回已排队CON、不重置序号;非法输入FATAL且不修改输出。
*/
extern IEC104_LinkResult_t IEC104_Link_Handle_Startdt_Act(
IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci,
uint8_t *response, uint16_t capacity);
/* 恰好6字节为OK,否则FATAL;完成不修改当前启停状态。 */
extern IEC104_LinkResult_t IEC104_Link_Complete_Startdt_Con_Send(
IEC104_LinkState_t *state, int sent_len);
/*
* 合法STOP立即置STOPPED/STOP_DRAINING,返回OK,不生成报文。
* 既有START_CON仍按FIFO完成;不等待它、不暂存STOP、不因启停交错断链。
* 排空中的重复STOP返回IGNORED且不延长期限;STOPPED/IDLE再次STOP会重新应答。
* 非法输入返回FATAL且不修改状态。
*/
extern IEC104_LinkResult_t IEC104_Link_Handle_Stopdt_Act(
IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci);
/* 只读查询:1可生成S或STOP_CON,0无义务或等待,-1非法。 */
extern int IEC104_Link_Can_Prepare_Stopdt_Response(const IEC104_LinkState_t *state);
/*
* 先确认已收到的数据,再等待本端已预留I帧获ACK,不发送新的业务I帧。
* 已排队且覆盖V(R)的S允许STOP_CON紧随排队,发送及完成顺序由FIFO保证。
* FRAME_READY写response和tx;生成STOP_CON即结束排空,保持STOPPED。
* 后到START可结束未生成CON的排空;已生成的CON仍须按序完成。
* WAIT_SEND等既有S,WAIT_PEER_ACK等I帧确认,NOT_ACTIVE表示无停止义务。
*/
extern IEC104_StopdtPrepareResult_t IEC104_Link_Prepare_Stopdt_Response(
IEC104_LinkState_t *state, uint8_t *response, uint16_t capacity,
IEC104_StopdtTx_t *tx);
/* 恰好6字节为OK,否则FATAL;旧STOP_CON完成不覆盖后到START的状态。 */
extern IEC104_LinkResult_t IEC104_Link_Complete_Stopdt_Con_Send(
IEC104_LinkState_t *state, int sent_len);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_LINK_SLAVE_STARTSTOP_H */
@@ -0,0 +1,221 @@
/**
* IEC104_Log.c —— IEC104 通用日志模块实现。
*
* 日志处理流程:
* 级别过滤 -> 时间戳/级别前缀 -> 正文格式化 -> 回调或标准流输出
*
* 模块不依赖 IEC104 报文结构,不使用动态内存。输出缓冲区位于调用栈,
* 用户回调不得在返回后继续引用收到的字符串地址。
*/
/* localtime_r 和 clock_gettime 是 POSIX 接口。特性宏必须先于所有头文件。 */
#if !defined(_WIN32) && !defined(_POSIX_C_SOURCE)
# define _POSIX_C_SOURCE 200809L
#endif
#include "IEC104_Log.h"
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#ifdef _WIN32
/* 减少 <windows.h> 引入的非必要声明,并避免旧版 Winsock 头文件干扰。 */
# ifndef WIN32_LEAN_AND_MEAN
# define WIN32_LEAN_AND_MEAN
# endif
# include <windows.h>
#else
/* 非 Windows 平台使用 POSIX 实时时钟和线程安全的本地时间转换接口。 */
# include <time.h>
#endif
/* 包含结尾 '\0';时间戳、级别与正文最多合计511字节,不等同于511个多字节字符。 */
#define IEC104_LOG_BUFFER_SIZE 512U
/*
* 日志配置状态。
*
* 默认级别为 INFO,默认回调为空。Set_* 应在开始并发输出之前调用;
* 本模块不为配置读写增加锁。
*/
static IEC104_LogCb_t g_log_callback = NULL;
static int g_min_log_level = IEC104_LOG_LEVEL_INFO;
/**
* 功能:把格式化函数报告的长度限制在缓冲区实际可存储范围内。
* 输入参数:length——snprintf/vsnprintf 的返回值;capacity——缓冲区容量。
* 输出参数:无。
* 返回值:截断后占用的字节数,不包含结尾 '\0';格式化失败或容量为0时返回0。
*/
static size_t Log_Clamp_Length(int length, size_t capacity)
{
if (length <= 0 || capacity == 0U) {
return 0U;
}
if ((size_t)length >= capacity) {
return capacity - 1U;
}
return (size_t)length;
}
/**
* 功能:将日志级别转换为时间戳后显示的级别名称。
* 输入参数:level——日志级别。
* 输出参数:无。
* 返回值:指向静态只读字符串;未知级别返回 "???"。
*/
static const char *Log_Level_Name(int level)
{
switch (level) {
case IEC104_LOG_LEVEL_DEBUG:
return "DBG ";
case IEC104_LOG_LEVEL_INFO:
return "INFO";
case IEC104_LOG_LEVEL_WARN:
return "WARN";
case IEC104_LOG_LEVEL_ERR:
return "ERR ";
default:
return "???";
}
}
/**
* 功能:格式化本地时间戳和日志级别前缀。
* 输入参数:buffer——目标缓冲区;capacity——缓冲区容量;level——日志级别。
* 输出参数:buffer——形如 "[yyyy-mm-dd hh:mm:ss.mmm][INFO] " 的前缀。
* 返回值:缓冲区内实际占用的字节数,不包含结尾 '\0'。
* 说明:POSIX 时间接口失败时输出全零时间戳,避免使用未初始化数据。
*/
static size_t Log_Format_Prefix(char *buffer, size_t capacity, int level)
{
int length;
#ifdef _WIN32
SYSTEMTIME local_time;
/* GetLocalTime 直接返回含毫秒字段的本地时间。 */
GetLocalTime(&local_time);
length = snprintf(
buffer, capacity,
"[%04d-%02d-%02d %02d:%02d:%02d.%03d][%s] ",
local_time.wYear, local_time.wMonth, local_time.wDay,
local_time.wHour, local_time.wMinute, local_time.wSecond,
local_time.wMilliseconds, Log_Level_Name(level));
#else
struct timespec timestamp;
struct tm local_time;
/* clock_gettime 提供毫秒来源,localtime_r 负责线程安全的时区转换。 */
if (clock_gettime(CLOCK_REALTIME, &timestamp) != 0 ||
localtime_r(&timestamp.tv_sec, &local_time) == NULL) {
length = snprintf(buffer, capacity,
"[0000-00-00 00:00:00.000][%s] ",
Log_Level_Name(level));
} else {
length = snprintf(
buffer, capacity,
"[%04d-%02d-%02d %02d:%02d:%02d.%03d][%s] ",
local_time.tm_year + 1900, local_time.tm_mon + 1,
local_time.tm_mday, local_time.tm_hour, local_time.tm_min,
local_time.tm_sec, (int)(timestamp.tv_nsec / 1000000L),
Log_Level_Name(level));
}
#endif
return Log_Clamp_Length(length, capacity);
}
/**
* 功能:使用默认通道输出一条日志。
* 输入参数:level——日志级别;message——不含行末换行的完整日志字符串。
* 输出参数:无。
* 返回值:无。
* 说明:WARN/ERR 写 stderr,DEBUG/INFO 写 stdout;每条日志立即刷新。
*/
static void Log_Write_Default(int level, const char *message)
{
FILE *output =
(level >= IEC104_LOG_LEVEL_WARN) ? stderr : stdout;
fputs(message, output);
fputc('\n', output);
fflush(output);
}
/**
* 功能:设置日志输出回调。
* 输入参数:cb——新的日志回调;传入空指针时恢复默认标准流输出。
* 输出参数:无。
* 返回值:无。
* 说明:该配置没有内部同步,应在开始并发输出日志之前完成。
*/
void IEC104_Set_Log_Callback(IEC104_LogCb_t cb)
{
/* 空回调表示恢复 Log_Write_Default()。 */
g_log_callback = cb;
}
/**
* 功能:设置允许输出的最低日志级别。
* 输入参数:min_level——IEC104_LOG_LEVEL_* 之一。
* 输出参数:无。
* 返回值:无。
* 说明:低于该级别的消息会在格式化之前被丢弃。
*/
void IEC104_Set_Log_Level(int min_level)
{
/* 保留 int 接口,调用方通常传入 IEC104_LOG_LEVEL_*。 */
g_min_log_level = min_level;
}
/**
* 功能:格式化并输出一条带时间戳和级别前缀的日志。
* 输入参数:level——日志级别;fmt——非空且不含行末换行的 printf 格式串;
* 其后为格式化参数。
* 输出参数:无;优先交给用户回调,否则写入默认标准流。
* 返回值:无。
* 说明:低级别消息直接忽略;超出固定缓冲区的内容按字节截断,可能截断多字节字符。
*/
void IEC104_Log_Emit(int level, const char *fmt, ...)
{
char buffer[IEC104_LOG_BUFFER_SIZE];
size_t prefix_length;
size_t remaining_capacity;
size_t total_length;
int body_length;
va_list args;
/* 被过滤的消息不执行时间和正文格式化。 */
if (fmt == NULL || level < g_min_log_level) {
return;
}
/* 先生成前缀,再把正文写入同一个固定长度缓冲区。 */
prefix_length = Log_Format_Prefix(buffer, sizeof(buffer), level);
remaining_capacity = sizeof(buffer) - prefix_length;
va_start(args, fmt);
body_length = vsnprintf(buffer + prefix_length, remaining_capacity,
fmt, args);
va_end(args);
/* 格式化失败时仍可输出已经生成的前缀。 */
if (body_length < 0) {
total_length = prefix_length;
} else {
total_length = prefix_length +
Log_Clamp_Length(body_length, remaining_capacity);
}
buffer[total_length] = '\0';
/* 回调优先;未配置回调时使用模块默认输出通道。 */
if (g_log_callback != NULL) {
g_log_callback(level, buffer);
} else {
Log_Write_Default(level, buffer);
}
}
@@ -0,0 +1,95 @@
/**
* IEC104_Log.h —— IEC 60870-5-104 通用日志接口。
*
* 本模块独立于报文解析和链路状态,只负责:
* 1. 按最低日志级别过滤消息;
* 2. 为消息添加本地时间和级别前缀;
* 3. 将完整消息交给用户回调或默认标准流。
*
* 公共名称全部使用 IEC104_ 前缀,可与 POSIX <syslog.h> 同时使用。
* 构建与测试命令见 IEC104测试文档.md。
*/
#ifndef IEC104_LOG_H
#define IEC104_LOG_H
#ifdef __cplusplus
extern "C" {
#endif
/**
* 日志级别。
*
* 数值必须按严重程度递增,因为过滤条件为:
* message_level >= minimum_level
*/
typedef enum {
IEC104_LOG_LEVEL_DEBUG = 0,
IEC104_LOG_LEVEL_INFO = 1,
IEC104_LOG_LEVEL_WARN = 2,
IEC104_LOG_LEVEL_ERR = 3
} IEC104_LogLevel_t;
/**
* 日志回调。
*
* level:本条消息的 IEC104_LOG_LEVEL_* 级别。
* msg 指向已经拼接时间戳和级别、但不含行末换行的临时字符串;
* 字符串只在本次回调期间有效,需要保留时应由回调自行复制。
* 回调可能从不同调用线程执行,如需共享数据应自行同步。
*/
typedef void (*IEC104_LogCb_t)(int level, const char *msg);
/**
* 功能:设置日志输出回调。
* 输入参数:cb——回调函数;传入空指针时恢复默认标准流输出。
* 输出参数:无。
* 返回值:无。
* 说明:应在开始并发输出日志之前完成配置。
*/
extern void IEC104_Set_Log_Callback(IEC104_LogCb_t cb);
/**
* 功能:设置允许输出的最低日志级别。
* 输入参数:min_level——IEC104_LOG_LEVEL_* 之一。
* 输出参数:无。
* 返回值:无。
* 说明:应在开始并发输出日志之前完成配置。
*/
extern void IEC104_Set_Log_Level(int min_level);
/**
* 功能:格式化并输出一条日志。
* 输入参数:level——日志级别;fmt——非空且不含行末换行的 printf 风格格式串;
* 其后为格式化参数。
* 输出参数:无;日志交给回调,或默认写入 stdout/stderr。
* 返回值:无。
* 说明:时间戳、级别与正文合计最多保留511字节,不含结尾空字符及默认输出追加的换行;
* 超出部分按字节截断,不保证UTF-8等多字节字符的边界完整。
*/
#if defined(__GNUC__) || defined(__clang__)
extern void IEC104_Log_Emit(int level, const char *fmt, ...)
__attribute__((format(printf, 2, 3)));
#else
extern void IEC104_Log_Emit(int level, const char *fmt, ...);
#endif
/*
* 便捷输出宏。
*
* 使用模块前缀,避免与 POSIX <syslog.h> 的 LOG_DEBUG、LOG_INFO、
* LOG_ERR 等常量冲突。宏至少需要传入一个格式串参数。
*/
#define IEC104_LOG_DEBUG(...) \
IEC104_Log_Emit(IEC104_LOG_LEVEL_DEBUG, __VA_ARGS__)
#define IEC104_LOG_INFO(...) \
IEC104_Log_Emit(IEC104_LOG_LEVEL_INFO, __VA_ARGS__)
#define IEC104_LOG_WARN(...) \
IEC104_Log_Emit(IEC104_LOG_LEVEL_WARN, __VA_ARGS__)
#define IEC104_LOG_ERR(...) \
IEC104_Log_Emit(IEC104_LOG_LEVEL_ERR, __VA_ARGS__)
#ifdef __cplusplus
}
#endif
#endif /* IEC104_LOG_H */
@@ -0,0 +1,202 @@
/*
* TCP平台接口:只负责同步非阻塞I/O、就绪等待和单调计时,不依赖104协议模块。
* 工程须链接且只链接一个实现;当前提供IEC104_Net_Windows.c。
* 不创建线程、不保存业务回调,也不决定协议故障、EOF排空或重连保留策略。
*
* 职责边界:TcpServer决定何时接入、读写和关闭,Net负责执行一次平台操作。
* 典型生命周期:Init -> Listen -> Wait/Accept/Recv/Send -> Close各句柄 -> Cleanup。
* 调用方持有监听和客户端句柄;本层不会为其保存连接对象或业务状态。
*
* 通用约定:native_error输出均允许NULL;成功时写0,失败时保留平台诊断码。
* 上层应按result调度,native_error只用于诊断,不应依赖某个平台的错误常量。
* 所有缓冲区均只在调用期间使用,函数返回后本层不再持有调用方的内存。
*/
#ifndef IEC104_NET_H
#define IEC104_NET_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* TCP内部拥有的数值句柄;0可以是有效句柄。
* TcpServer可按其公开契约借给应用事件循环注册,应用不得接管I/O、关闭或修改句柄属性。
* 使用整数类型承载平台句柄,使公共头文件无需包含SOCKET等系统类型。
* 是否无效只能与IEC104_NET_INVALID_SOCKET比较,不能用if (!handle)判断。 */
typedef uintptr_t IEC104_NetSocket_t;
#define IEC104_NET_INVALID_SOCKET ((IEC104_NetSocket_t)UINTPTR_MAX)
/* 必须按枚举值判断:OK为0,正数不统一表示成功或有数据。
* WOULD_BLOCK表示当前没有进度,通常等待下一次就绪;RETRY表示本次操作可重试。
* RETRY_LATER仅用于接入资源暂缺,须由调用方暂停接入并安排定时重试。
* 重试次数、收发预算及超时期限由调用方限制,不能在后端无限循环。 */
typedef enum {
IEC104_NET_ERROR = -1, /* 最终失败,native_error保存平台错误码。 */
IEC104_NET_OK = 0, /* 操作完成;Wait的就绪标志可为0,表示未报告socket就绪。 */
IEC104_NET_WOULD_BLOCK, /* 暂不可接受连接/读写,保留原有状态后续再试。 */
IEC104_NET_RETRY, /* 本次被中断或单个待接入客户端失效,可有界重试。 */
IEC104_NET_EOF, /* 仅Recv使用:对端发送方向有序关闭。 */
IEC104_NET_RETRY_LATER /* 仅Accept使用:资源暂缺,不能立即循环重试。 */
} IEC104_NetResult_t;
/* 一次收发的完整结果;把状态、字节数和平台错误分开,避免把EOF当成暂不可读。 */
typedef struct {
IEC104_NetResult_t result; /* 调度依据;读取transferred前先判断此字段。 */
size_t transferred; /* OK时为本次实际读写字节数,其余结果恒为0。 */
int native_error; /* 平台诊断码,OK/EOF为0;上层按result决定调度。 */
} IEC104_NetIoResult_t;
/* Accept交付的对端地址副本,平台sockaddr和网络字节序不越过Net边界。
* 当前监听仅支持IPv4;address不包含端口,port使用主机字节序。 */
typedef struct {
char address[16]; /* NUL结尾数字IPv4,最长15字符。 */
uint16_t port; /* 客户端源端口,并非本地监听端口。 */
} IEC104_NetPeer_t;
/* Wait的关注位和就绪位使用同一组掩码,可以按位或组合。
* 就绪只表示可以尝试操作,最终状态仍由Accept/Recv/Send的返回值确定。 */
#define IEC104_NET_EVENT_ACCEPT 1U /* 监听句柄可尝试接入客户端。 */
#define IEC104_NET_EVENT_READ 2U /* 客户端可尝试接收,也可能读到EOF或错误。 */
#define IEC104_NET_EVENT_WRITE 4U /* 客户端可尝试发送,不保证整段数据一次写完。 */
/**
* 功能:取得本次网络使用所需的平台初始化资源。
* 输出参数:native_error可为NULL;成功写0,失败写平台错误码。
* 返回值:OK表示初始化成功;ERROR表示失败,此时无需为本次调用Cleanup。
* 说明:在创建socket前调用;每次成功Init均须配对一次Cleanup。
* 重复调用并非幂等操作,不能把多次成功Init只配对一次Cleanup。
*/
extern IEC104_NetResult_t IEC104_Net_Init(int *native_error);
/**
* 功能:释放一次成功Init取得的平台初始化资源。
* 说明:调用前先关闭本次持有的全部socket;本函数不逐一管理调用方的句柄。
* 无返回值,也不负责清空TcpServer中的连接状态。
*/
extern void IEC104_Net_Cleanup(void);
/**
* 功能:取得平台对应的“参数无效”诊断码,不执行网络操作。
* 返回值:当前后端的参数错误码,用于上层自身参数校验失败时填写诊断信息。
* 说明:避免TcpServer为记录错误而包含Windows等平台的系统错误常量。
*/
extern int IEC104_Net_Invalid_Argument_Error(void);
/**
* 功能:创建、绑定并启动一个非阻塞IPv4 TCP监听socket。
* 输入参数:bind_address为有效的NUL结尾数字IPv4字符串,不进行域名解析。
* port使用主机字节序;为0时由系统分配可用端口。
* 输出参数:listener、bound_port必填,分别接收新监听句柄和实际端口。
* bound_port同样使用主机字节序;native_error可为NULL。
* 各输出存储区不得重叠,也不得覆盖输入字符串。
* 返回值:OK时将新句柄的所有权交给调用方;ERROR时回收本次创建的socket,
* 并将有效输出指针指向的listener/bound_port置为INVALID_SOCKET/0。
* 说明:只用于创建新句柄,不会关闭listener原来保存的句柄;先处理旧资源再调用。
* 失败不撤销此前成功的Init,调用方仍须按生命周期执行Cleanup。
*/
extern IEC104_NetResult_t IEC104_Net_Listen(
const char *bind_address, uint16_t port, IEC104_NetSocket_t *listener,
uint16_t *bound_port, int *native_error);
/**
* 功能:从非阻塞监听socket接入至多一个客户端,不在内部循环accept。
* 输入参数:listener为Listen成功交付的监听句柄。
* 输出参数:client、peer必填且不得重叠,OK时交付非阻塞且启用TCP_NODELAY的客户端及其IPv4地址/源端口;
* native_error可为NULL。
* 返回值:OK表示接入成功,调用方负责关闭客户端;WOULD_BLOCK表示暂无待接入连接;
* RETRY表示本次被中断、待接入客户端失效、非阻塞/TCP_NODELAY设置或地址转换失败;
* RETRY_LATER表示接入资源暂缺,调用方应暂停关注监听就绪并定时重试;
* ERROR表示参数无效或其他失败。非OK时有效client输出为INVALID_SOCKET,peer保持原值。
* 说明:新客户端设置或地址转换失败时,先关闭它再返回RETRY(资源暂缺为RETRY_LATER),
* 监听句柄仍由调用方持有,后端本身不等待。
* client只接收新句柄,不会自动关闭其中的旧句柄;不得覆盖仍在使用的连接。
* 是否接受额外客户端及重试多少次,由TcpServer的连接策略和预算决定。
*/
extern IEC104_NetResult_t IEC104_Net_Accept(
IEC104_NetSocket_t listener, IEC104_NetSocket_t *client, IEC104_NetPeer_t *peer, int *native_error);
/**
* 功能:对已接入的非阻塞客户端执行至多一次同步recv。
* 输入参数:socket_handle为有效客户端句柄;data非NULL,指向可写缓冲区;
* capacity为缓冲区容量,必须大于0,当前Windows后端还要求不大于INT_MAX。
* 输出参数:OK时将transferred个字节写入data,不追加字符串结束符。
* 返回值:OK表示收到数据;EOF表示对端发送方向有序关闭;WOULD_BLOCK表示暂不可读;
* RETRY表示本次操作被中断;ERROR表示参数或socket错误。
* 非OK时transferred为0;OK/EOF时native_error为0,其余状态保留诊断码。
* 说明:TCP是字节流,一次接收不对应一个完整104报文;拆帧由上层负责。
* EOF不会自动关闭句柄,调用方仍可排空待发送数据;函数返回后不再借用data。
* WOULD_BLOCK/RETRY不代表数据进度,不能据此重新计算或延长原超时期限。
*/
extern IEC104_NetIoResult_t IEC104_Net_Recv(
IEC104_NetSocket_t socket_handle, void *data, size_t capacity);
/* 只读取得当前内核接收队列的字节数,不等待、不消费输入、不包含后来到达的字节。
* 用于给跨调度轮次的续收设定有限边界;不是完整APDU数,也不表示连接仍存活。
* available必填,仅OK时写入;失败返回ERROR并保留平台诊断,由上层结束连接。 */
extern IEC104_NetResult_t IEC104_Net_Pending_Input(
IEC104_NetSocket_t socket_handle, uint32_t *available, int *native_error);
/**
* 功能:对已接入的非阻塞客户端执行至多一次同步send。
* 输入参数:socket_handle为有效客户端句柄;data非NULL,指向待发送数据;
* length为待发送长度,必须大于0,当前Windows后端还要求不大于INT_MAX。
* 返回值:OK时transferred表示本次被本机网络栈接受的字节数,可能小于length或为0;
* WOULD_BLOCK表示暂不可写;RETRY表示本次被中断;ERROR表示参数或socket错误。
* 非OK时transferred为0;此接口不会返回EOF,写入0字节仍属于OK。
* 说明:只按transferred推进发送位置,未写入的后缀由调用方保留并择机继续发送。
* 函数返回后不再借用data;OK不代表对端应用收到数据或完成104协议确认。
* WOULD_BLOCK/RETRY以及OK但写入0字节都不应被视为发送进度。
* 调用方须限制连续无进展的立即重试,并安排延时恢复;不能仅依赖新的可写边沿。
*/
extern IEC104_NetIoResult_t IEC104_Net_Send(
IEC104_NetSocket_t socket_handle, const void *data, size_t length);
/**
* 功能:等待本轮关注的监听或客户端事件;空集合只作有限定时等待,不执行I/O。
* 输入参数:interest为上述事件位的组合,允许为0,不允许包含未知位。
* 关注ACCEPT时listener须有效;关注READ/WRITE时client须有效;
* 未被关注的句柄可以为INVALID_SOCKET。
* timeout_ms为本次等待时长,单位毫秒;为0时只查询,空集合直接返回。
* UINT32_MAX仍为有限时长;后端须避开系统无限等待哨兵,必要时提前返回。
* 输出参数:ready_events必填,每次先清零;native_error可为NULL。
* 返回值:OK时ready_events给出就绪事件的位组合,0表示未报告就绪(包括纯定时等待);
* RETRY表示等待被中断,ERROR表示参数或等待失败,两者就绪位均为0。
* 说明:最多调用一次平台等待;RETRY交由调用方按剩余期限重新调度,后端不无限重试。
* 等待可能提前结束,是否到期由调用方按单调时钟重新判断。
* 就绪后实际I/O仍可能暂不可读写、部分发送或遇到EOF,必须检查后续操作结果。
*/
extern IEC104_NetResult_t IEC104_Net_Wait(
IEC104_NetSocket_t listener, IEC104_NetSocket_t client, uint32_t interest,
uint32_t timeout_ms, uint32_t *ready_events, int *native_error);
/**
* 功能:关闭一个监听或客户端句柄,不调用Cleanup或业务回调。
* 输入参数:socket_handle为要关闭的句柄;INVALID_SOCKET视为已关闭。
* 输出参数:native_error可为NULL,失败时保留关闭操作的诊断码。
* 返回值:OK表示关闭成功或传入无效哨兵值;ERROR表示关闭操作失败。
* 说明:句柄按值传入,本函数无法修改调用方字段;调用方需维护自己的句柄状态。
* 成功关闭后应将保存的句柄置为INVALID_SOCKET,避免重复关闭已被系统复用的值。
*/
extern IEC104_NetResult_t IEC104_Net_Close(
IEC104_NetSocket_t socket_handle, int *native_error);
/**
* 功能:读取平台单调时钟,供连接超时和收发期限计算使用。
* 返回值:64位单调毫秒计数,不是UTC时间戳,也不用于显示日期;0及连续读取相等值有效。
* 说明:与日历时间及校时无关;同一会话的时间差和期限必须使用同一时钟来源。
* 有效运行期间不得倒退、复位或回绕。
* 若底层仅提供32位节拍,须通过回绕计数或等效机制扩展为64位,
* 再按节拍频率换算为毫秒;不得仅作uint64_t强制转换。
* 扩展机制必须保证不漏记回绕,不能依赖本函数被频繁调用。
* 扩展计数的更新与读取须正确同步,取得高低位一致的快照,
* 防止节拍中断或其他并发更新造成读值错误;这不构成从中断上下文调用本接口的保证。
* 本函数只读取时钟,不创建定时器,也不触发协议超时处理。
*/
extern uint64_t IEC104_Net_Now_Ms(void);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_NET_H */
@@ -0,0 +1,377 @@
/*
* Windows网络后端:所有Winsock类型、错误码和系统计时依赖集中在本文件。
* 实现IEC104_Net.h约定的同步非阻塞操作;上层无需包含Windows系统头文件。
* 不保存连接、不执行协议回调、不实现收发循环:这些调度职责由TcpServer承担。
* 本文件只在Windows目标上编译,最终链接时需加入Winsock库ws2_32。
*/
#ifndef _WIN32
#error "Select a network backend for this platform; IEC104_Net_Windows requires Windows"
#endif
#ifndef _WIN32_WINNT
/* 为InetPtonA/InetNtopA和GetTickCount64启用至少Windows Vista级别的系统接口声明。 */
#define _WIN32_WINNT 0x0600
#endif
#ifndef WIN32_LEAN_AND_MEAN
/* 缩减windows.h引入的内容,配合先包含winsock2.h,避免旧版Winsock头文件冲突。 */
#define WIN32_LEAN_AND_MEAN
#endif
#include "IEC104_Net.h"
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <limits.h>
#include <string.h>
/* Windows SOCKET是指针宽度的数值句柄;编译时检查跨层转换不会丢失句柄位。 */
_Static_assert(sizeof(IEC104_NetSocket_t) >= sizeof(SOCKET), "Network handle must hold a SOCKET");
/**
* 功能:返回通用状态,并按需写出已经取得的平台错误码。
* 参数:result为调度状态,error为诊断码,native_error可为NULL。
* 说明:本函数不调用WSAGetLastError;调用方应先保存原始错误,再执行资源清理。
*/
static IEC104_NetResult_t Result_With_Error(
IEC104_NetResult_t result, int error, int *native_error)
{
if (native_error != NULL) {
*native_error = error;
}
return result;
}
/**
* 功能:将常见Winsock失败原因转换为通用操作状态。
* 返回值:暂不可读写映射为WOULD_BLOCK,操作被中断映射为RETRY,其余映射为ERROR。
* 说明:只转换状态,不丢弃原始错误码;Accept特有的客户端失效及资源暂缺另行处理。
*/
static IEC104_NetResult_t Socket_Error_Result(int error)
{
if (error == WSAEWOULDBLOCK) {
return IEC104_NET_WOULD_BLOCK;
}
return error == WSAEINTR ? IEC104_NET_RETRY : IEC104_NET_ERROR;
}
/* 只用于接入路径;收发及监听初始化仍保留各自原有的错误分类。 */
static int Accept_Resource_Error(int error)
{
return error == WSAENOBUFS || error == WSAEMFILE;
}
/* 构造一次收发结果;这里只打包参数,字节数与状态的一致性由调用位置保证。 */
static IEC104_NetIoResult_t Io_Result(IEC104_NetResult_t result, size_t transferred, int error)
{
IEC104_NetIoResult_t output;
output.result = result;
output.transferred = transferred;
output.native_error = error;
return output;
}
/**
* 功能:把已经创建的Windows socket切换到非阻塞模式。
* 返回值:非0表示成功,0表示失败;失败后调用方须立即取得WSAGetLastError。
* 说明:FIONBIO取1只改变I/O模式,本函数不会关闭句柄,也不会创建后台任务。
*/
static int Set_Nonblocking(SOCKET socket_handle)
{
u_long enabled = 1UL;
return ioctlsocket(socket_handle, FIONBIO, &enabled) == 0;
}
/**
* 功能:请求Winsock 2.2初始化,成功后取得一次需配对释放的引用。
* 返回值:将WSAStartup直接返回的错误码转为OK/ERROR,并按需写入native_error。
* 说明:WSAStartup的错误不能改用WSAGetLastError读取;本层不缓存“已初始化”标志。
*/
IEC104_NetResult_t IEC104_Net_Init(int *native_error)
{
WSADATA startup;
int error = WSAStartup(MAKEWORD(2, 2), &startup);
return Result_With_Error(error == 0 ? IEC104_NET_OK : IEC104_NET_ERROR, error, native_error);
}
/* 释放一次成功Init取得的Winsock引用;调用方应先关闭自己持有的全部socket。
* 与Init一一配对,本函数不检查重复释放,接口也不对外返回WSACleanup的结果。 */
void IEC104_Net_Cleanup(void)
{
(void)WSACleanup();
}
/* 向上层提供Windows参数错误码,使WSAEINVAL不出现在通用调度代码中。 */
int IEC104_Net_Invalid_Argument_Error(void)
{
return WSAEINVAL;
}
/**
* 功能:完成数字IPv4地址解析、socket创建、非阻塞设置、绑定及监听。
* 输出:成功才交付新句柄和实际端口;失败关闭本次创建的socket,保留原始错误码。
* 说明:本函数只负责监听socket;失败不释放调用方由Net_Init取得的平台引用。
* 参数及资源所有权的完整约定见IEC104_Net.h。
*/
IEC104_NetResult_t IEC104_Net_Listen(
const char *bind_address, uint16_t port, IEC104_NetSocket_t *listener,
uint16_t *bound_port, int *native_error)
{
struct sockaddr_in address;
int address_length = (int)sizeof(address);
int enabled = 1;
int error;
SOCKET socket_handle;
/* 先统一失败输出;只清空输出值,不会替调用方关闭原来保存在其中的句柄。 */
if (listener != NULL) {
*listener = IEC104_NET_INVALID_SOCKET;
}
if (bound_port != NULL) {
*bound_port = 0U;
}
if (bind_address == NULL || listener == NULL || bound_port == NULL) {
return Result_With_Error(IEC104_NET_ERROR, WSAEINVAL, native_error);
}
/* 只解析数字IPv4,不做可能阻塞的域名查询;系统sockaddr中的端口使用网络字节序。 */
memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(port);
if (InetPtonA(AF_INET, bind_address, &address.sin_addr) != 1) {
return Result_With_Error(IEC104_NET_ERROR, WSAEINVAL, native_error);
}
socket_handle = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (socket_handle == INVALID_SOCKET) {
return Result_With_Error(IEC104_NET_ERROR, WSAGetLastError(), native_error);
}
/* 独占地址选项须在bind前设置,保留原有监听地址占用策略。
* 各步骤按短路顺序执行,任一步骤失败即停止;getsockname读取实际绑定端口,
* 因而port=0时也能将系统分配的端口交回上层。 */
if (setsockopt(socket_handle, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,
(const char *)&enabled, sizeof(enabled)) == SOCKET_ERROR ||
!Set_Nonblocking(socket_handle) ||
bind(socket_handle, (const struct sockaddr *)&address, sizeof(address)) == SOCKET_ERROR ||
listen(socket_handle, SOMAXCONN) == SOCKET_ERROR ||
getsockname(socket_handle, (struct sockaddr *)&address, &address_length) == SOCKET_ERROR) {
/* closesocket可能改变线程错误状态,必须先保存最初失败的错误码。 */
error = WSAGetLastError();
(void)closesocket(socket_handle);
return Result_With_Error(IEC104_NET_ERROR, error, native_error);
}
/* 到此才转移句柄所有权;公共接口交付的端口恢复为主机字节序。 */
*listener = (IEC104_NetSocket_t)socket_handle;
*bound_port = ntohs(address.sin_port);
return Result_With_Error(IEC104_NET_OK, 0, native_error);
}
/**
* 功能:尝试接入一个客户端,设置非阻塞和TCP_NODELAY并转换对端地址后一起交付。
* 返回值:没有待接入客户端时返回WOULD_BLOCK;本次可恢复失败返回RETRY,资源暂缺返回RETRY_LATER。
* 说明:不在后端循环accept,额外连接及失败重试均受TcpServer本轮接入预算约束。
* 任何失败都不会在本函数内关闭listener或修改上层连接状态。
*/
IEC104_NetResult_t IEC104_Net_Accept(
IEC104_NetSocket_t listener, IEC104_NetSocket_t *client, IEC104_NetPeer_t *peer, int *native_error)
{
SOCKET accepted;
struct sockaddr_in address;
int address_length = (int)sizeof(address);
int enabled = 1;
IEC104_NetPeer_t prepared;
int error;
IEC104_NetResult_t result;
if (client != NULL) {
*client = IEC104_NET_INVALID_SOCKET;
}
if (client == NULL || peer == NULL || listener == IEC104_NET_INVALID_SOCKET) {
return Result_With_Error(IEC104_NET_ERROR, WSAEINVAL, native_error);
}
/* 接入时取得不可变的TCP对端信息;上层后续只读缓存,不再查询可能已关闭或复用的socket。 */
memset(&address, 0, sizeof(address));
accepted = accept((SOCKET)listener, (struct sockaddr *)&address, &address_length);
if (accepted == INVALID_SOCKET) {
error = WSAGetLastError();
/* 资源暂缺交给上层定时重试;单个客户端复位或中止仍可在本轮预算内重试。 */
if (Accept_Resource_Error(error)) {
result = IEC104_NET_RETRY_LATER;
} else {
result = error == WSAECONNRESET || error == WSAECONNABORTED ?
IEC104_NET_RETRY : Socket_Error_Result(error);
}
return Result_With_Error(result, error, native_error);
}
/* 显式设置非阻塞并关闭Nagle,减少104短报文的合并等待;任一步骤失败均回收新客户端。 */
if (!Set_Nonblocking(accepted) ||
setsockopt(accepted, IPPROTO_TCP, TCP_NODELAY,
(const char *)&enabled, sizeof(enabled)) == SOCKET_ERROR) {
error = WSAGetLastError();
(void)closesocket(accepted);
return Result_With_Error(Accept_Resource_Error(error) ? IEC104_NET_RETRY_LATER : IEC104_NET_RETRY,
error, native_error);
}
if (address_length < (int)sizeof(address) || address.sin_family != AF_INET) {
(void)closesocket(accepted);
return Result_With_Error(IEC104_NET_RETRY, WSAEAFNOSUPPORT, native_error);
}
memset(&prepared, 0, sizeof(prepared));
if (InetNtopA(AF_INET, &address.sin_addr, prepared.address, sizeof(prepared.address)) == NULL) {
error = WSAGetLastError();
(void)closesocket(accepted);
return Result_With_Error(Accept_Resource_Error(error) ? IEC104_NET_RETRY_LATER : IEC104_NET_RETRY,
error, native_error);
}
prepared.port = ntohs(address.sin_port);
/* 所有准备均成功才发布地址及句柄;失败不泄漏新socket,也不覆盖调用方原地址。 */
*peer = prepared;
*client = (IEC104_NetSocket_t)accepted;
return Result_With_Error(IEC104_NET_OK, 0, native_error);
}
/**
* 功能:执行一次同步非阻塞接收,将字节数、EOF和错误分别交付上层。
* 说明:不循环读取、不识别104帧边界,也不因EOF主动关闭客户端。
* recv返回0表示对端发送方向关闭,上层仍可决定排空本地待发送数据。
*/
IEC104_NetIoResult_t IEC104_Net_Recv(
IEC104_NetSocket_t socket_handle, void *data, size_t capacity)
{
int received;
int error;
/* Winsock长度参数为int,先校验再转换,避免大size_t被截断成负数。
* 排除零容量请求,使recv返回0可以明确表示EOF。 */
if (socket_handle == IEC104_NET_INVALID_SOCKET || data == NULL || capacity == 0U || capacity > INT_MAX) {
return Io_Result(IEC104_NET_ERROR, 0U, WSAEINVAL);
}
received = recv((SOCKET)socket_handle, (char *)data, (int)capacity, 0);
if (received > 0) {
return Io_Result(IEC104_NET_OK, (size_t)received, 0);
}
if (received == 0) {
return Io_Result(IEC104_NET_EOF, 0U, 0);
}
/* 只在recv失败后读取错误;WOULD_BLOCK和RETRY都没有新增接收字节。 */
error = WSAGetLastError();
return Io_Result(Socket_Error_Result(error), 0U, error);
}
IEC104_NetResult_t IEC104_Net_Pending_Input(
IEC104_NetSocket_t socket_handle, uint32_t *available, int *native_error)
{
u_long count = 0UL;
if (socket_handle == IEC104_NET_INVALID_SOCKET || available == NULL) {
return Result_With_Error(IEC104_NET_ERROR, WSAEINVAL, native_error);
}
if (ioctlsocket((SOCKET)socket_handle, FIONREAD, &count) == SOCKET_ERROR) {
return Result_With_Error(IEC104_NET_ERROR, WSAGetLastError(), native_error);
}
*available = (uint32_t)count;
return Result_With_Error(IEC104_NET_OK, 0, native_error);
}
/**
* 功能:执行一次同步非阻塞发送,准确报告本次交给本机网络栈的字节数。
* 说明:不代替上层循环补发;调用方须保留未发送的后缀,并按实际进度推进发送计划。
* 使用普通send,不发起需在返回后继续借用data的异步重叠I/O。
*/
IEC104_NetIoResult_t IEC104_Net_Send(
IEC104_NetSocket_t socket_handle, const void *data, size_t length)
{
int written;
int error;
/* 与Recv一致,长度须能无损转换为Winsock要求的正int。 */
if (socket_handle == IEC104_NET_INVALID_SOCKET || data == NULL || length == 0U || length > INT_MAX) {
return Io_Result(IEC104_NET_ERROR, 0U, WSAEINVAL);
}
written = send((SOCKET)socket_handle, (const char *)data, (int)length, 0);
/* 允许部分写入及零进度;send返回0不同于recv返回0,不转成EOF。 */
if (written >= 0) {
return Io_Result(IEC104_NET_OK, (size_t)written, 0);
}
error = WSAGetLastError();
return Io_Result(Socket_Error_Result(error), 0U, error);
}
/**
* 功能:通过一次select等待socket就绪并转换事件位;空集合改作有限定时等待。
* 返回值:正常就绪或超时均为OK;中断为RETRY,其他等待失败为ERROR。
* 说明:最多关注一个监听和一个客户端;不执行实际收发,也不在内部重试select。
* 等待期限及中断后的剩余时长由TcpServer重新计算,避免反复等待延长期限。
*/
IEC104_NetResult_t IEC104_Net_Wait(
IEC104_NetSocket_t listener, IEC104_NetSocket_t client, uint32_t interest,
uint32_t timeout_ms, uint32_t *ready_events, int *native_error)
{
fd_set read_set, write_set;
struct timeval timeout;
int ready;
/* 清除上一轮结果;参数错误、等待失败及正常超时均不会残留旧就绪位。 */
if (ready_events != NULL) {
*ready_events = 0U;
}
/* 只校验本轮实际关注的句柄;空集合用于接入暂停等纯定时等待。 */
if (ready_events == NULL ||
(interest & ~(IEC104_NET_EVENT_ACCEPT | IEC104_NET_EVENT_READ | IEC104_NET_EVENT_WRITE)) != 0U ||
((interest & IEC104_NET_EVENT_ACCEPT) != 0U && listener == IEC104_NET_INVALID_SOCKET) ||
((interest & (IEC104_NET_EVENT_READ | IEC104_NET_EVENT_WRITE)) != 0U && client == IEC104_NET_INVALID_SOCKET)) {
return Result_With_Error(IEC104_NET_ERROR, WSAEINVAL, native_error);
}
if (interest == 0U) {
/* Winsock的select不接受空集合;0直接返回,UINT32_MAX不能传成Sleep(INFINITE)。 */
if (timeout_ms != 0U) {
Sleep((DWORD)(timeout_ms == UINT32_MAX ? UINT32_MAX - 1U : timeout_ms));
}
return Result_With_Error(IEC104_NET_OK, 0, native_error);
}
/* select会改写集合,每次都重新构建;监听接入和客户端接收都使用读集合。 */
FD_ZERO(&read_set);
FD_ZERO(&write_set);
if ((interest & IEC104_NET_EVENT_ACCEPT) != 0U) {
FD_SET((SOCKET)listener, &read_set);
}
if ((interest & IEC104_NET_EVENT_READ) != 0U) {
FD_SET((SOCKET)client, &read_set);
}
if ((interest & IEC104_NET_EVENT_WRITE) != 0U) {
FD_SET((SOCKET)client, &write_set);
}
/* 将毫秒拆成秒和微秒;二者均为0时select只查询,不等待后续事件。 */
timeout.tv_sec = (long)(timeout_ms / 1000U);
timeout.tv_usec = (long)((timeout_ms % 1000U) * 1000U);
/* Winsock忽略nfds,固定传0;fd_set和timeval等平台细节不进入公共头文件。 */
ready = select(0, &read_set, &write_set, NULL, &timeout);
if (ready == SOCKET_ERROR) {
int error = WSAGetLastError();
return Result_With_Error(error == WSAEINTR ? IEC104_NET_RETRY : IEC104_NET_ERROR, error, native_error);
}
/* select返回就绪数量,不能直接作为事件掩码;逐项还原本轮关注的事件位。 */
if (ready > 0) {
if ((interest & IEC104_NET_EVENT_ACCEPT) != 0U && FD_ISSET((SOCKET)listener, &read_set)) {
*ready_events |= IEC104_NET_EVENT_ACCEPT;
}
if ((interest & IEC104_NET_EVENT_READ) != 0U && FD_ISSET((SOCKET)client, &read_set)) {
*ready_events |= IEC104_NET_EVENT_READ;
}
if ((interest & IEC104_NET_EVENT_WRITE) != 0U && FD_ISSET((SOCKET)client, &write_set)) {
*ready_events |= IEC104_NET_EVENT_WRITE;
}
}
return Result_With_Error(IEC104_NET_OK, 0, native_error);
}
/**
* 功能:关闭单个socket,将closesocket失败转换为ERROR及原始诊断码。
* 说明:INVALID_SOCKET直接返回OK,便于调用方清理尚未完整初始化的资源。
* 参数按值传入,不会清空调用方字段;也不会释放Init取得的Winsock引用。
*/
IEC104_NetResult_t IEC104_Net_Close(IEC104_NetSocket_t socket_handle, int *native_error)
{
if (socket_handle != IEC104_NET_INVALID_SOCKET && closesocket((SOCKET)socket_handle) == SOCKET_ERROR) {
return Result_With_Error(IEC104_NET_ERROR, WSAGetLastError(), native_error);
}
return Result_With_Error(IEC104_NET_OK, 0, native_error);
}
/* 读取系统启动以来的64位毫秒计数;供相对期限计算,不受用户修改日历时间影响。
* 不在此处换算为UTC或本地时间,避免将协议超时与日历时钟混用。 */
uint64_t IEC104_Net_Now_Ms(void)
{
return (uint64_t)GetTickCount64();
}
@@ -0,0 +1,299 @@
/**
* 每连接独立的IEC-104流式接收器。
* 每个实例只保留一帧;完整帧经APCI及ASDU最低长度检查后暂停输入,显式释放后再处理剩余字节。
* Codec负责报文格式,Link/SendManager负责序号、ACK和启停;本模块不改变链路状态。
*/
#include "IEC104_Receiver.h"
#include "IEC60870_Codec.h"
#include <stddef.h>
#include <string.h>
/**
* 功能:丢弃当前候选帧,将接收器恢复到等待帧头的阶段。
* 输入参数:receiver——非NULL接收器,调用方已核对初始化状态和连接代次。
* 输出参数:receiver——接收长度、期望长度及完整帧标志清零。
* 返回值:无。
* 说明:保留连接代次、初始化标志和缓冲字节;旧字节不再属于有效报文。
* 如此前已交付完整帧视图,该视图随本次清除失效。
*/
static void Clear_Progress(IEC104_Receiver_t *receiver)
{
receiver->received_length = 0U;
receiver->expected_length = 0U;
receiver->is_frame_ready = 0U;
}
/**
* 功能:检查接收器是否已初始化,以及本次操作是否属于当前连接代次。
* 输入参数:receiver——待检查对象,允许NULL;connection_generation——本次操作所属代次。
* 输出参数:无。
* 返回值:对象已初始化且非零代次相同返回1,否则0。
* 说明:不检查组帧进度,因此Reset可以在进度字段异常时仍清除当前报文。
* 代次由连接层分配;本模块只比较代次,不负责生成或递增。
*/
static int Session_Valid(const IEC104_Receiver_t *receiver, uint64_t connection_generation)
{
return receiver != NULL && receiver->is_initialized == 1U &&
connection_generation != 0U && receiver->connection_generation == connection_generation;
}
/**
* 功能:检查连接身份、接收阶段、帧头和长度字段是否自洽。
* 输入参数:receiver——待检查对象,允许NULL;connection_generation——本次操作所属代次。
* 输出参数:无。
* 返回值:状态及已知边界合法返回1,否则0。
* 说明:未收到长度域时expected_length必须为0;收到后必须与长度域一致。
* 只有收齐报文时才能置完整帧标志。本函数不解析APCI,也不检查链路序号。
*/
static int Receiver_Valid(const IEC104_Receiver_t *receiver, uint64_t connection_generation)
{
if (!Session_Valid(receiver, connection_generation) || receiver->is_frame_ready > 1U) {
return 0;
}
/* 尚未收到帧头:没有候选帧,也不能存在期望长度或完整帧标志。 */
if (receiver->received_length == 0U) {
return receiver->expected_length == 0U && receiver->is_frame_ready == 0U;
}
if (receiver->buffer[0] != IEC104_START_BYTE) {
return 0;
}
/* 仅收到0x68:长度域尚未知,不能用缓冲中遗留的第二字节判断报文长度。 */
if (receiver->received_length == 1U) {
return receiver->expected_length == 0U && receiver->is_frame_ready == 0U;
}
/* 长度域不包含起始字节和长度字节,因此APDU总长为长度域加2。 */
if (receiver->buffer[1] < IEC104_MIN_LENGTH_FIELD || receiver->buffer[1] > IEC104_MAX_LENGTH_FIELD ||
receiver->expected_length != (uint16_t)(2U + receiver->buffer[1]) ||
receiver->expected_length > sizeof(receiver->buffer) ||
receiver->received_length > receiver->expected_length) {
return 0;
}
/* 完整帧等待释放;未就绪的候选帧则必须仍缺少至少一个字节。 */
return receiver->is_frame_ready != 0U ?
receiver->received_length == receiver->expected_length :
receiver->received_length < receiver->expected_length;
}
/* 成帧及借用共用格式检查;I帧在交给Link/Manager前检查已知对象的最低载荷长度。
* APCI解析也供发送缓冲使用,ASDU接收检查在此单独组合,不改变发送构帧路径。 */
static int Parse_Received_Frame(const IEC104_Receiver_t *receiver, IEC104_ApciInfo_t *apci)
{
/* 本接收器属于104传输,显式使用104标准应用层布局。 */
static const IEC60870_AppLayerParameters_t parameters = IEC104_APP_LAYER_PARAMETERS_DEFAULT;
IEC60870_AsduInfo_t asdu_info;
if (!IEC104_Parse_APCI(receiver->buffer, receiver->received_length, apci)) {
return 0;
}
return apci->frame_type != IEC104_FRAME_TYPE_I ||
IEC60870_Parse_ASDU_Header(&parameters, apci->asdu, apci->asdu_len, &asdu_info);
}
/**
* 功能:初始化独立接收器,并绑定一个非零连接代次。
* 输入参数:receiver——调用方提供的存储;connection_generation——连接层分配的代次。
* 输出参数:receiver——成功时清零全部字段和缓冲,再记录代次及初始化标志。
* 返回值:OK表示成功;ERROR表示指针为空或代次为0,对象保持不变。
* 说明:首次使用及新连接换代都调用本函数;重新初始化会使已有帧视图失效。
* 调用前须结束旧连接及其I/O,不得与Feed或视图读取并发执行。
*/
IEC104_ReceiverResult_t IEC104_Receiver_Init(
IEC104_Receiver_t *receiver, uint64_t connection_generation)
{
if (receiver == NULL || connection_generation == 0U) {
return IEC104_RECEIVER_ERROR;
}
memset(receiver, 0, sizeof(*receiver));
receiver->connection_generation = connection_generation;
receiver->is_initialized = 1U;
return IEC104_RECEIVER_OK;
}
/**
* 功能:读取接收进度,避免上层直接依赖接收器内部的成帧标志和长度字段。
* 输入参数:receiver/connection_generation——当前对象和代次;status——不重叠的输出存储。
* 返回值:OK写数值快照;ERROR参数、身份或进度非法且不改输出。查询不消费字节或释放原文。
*/
IEC104_ReceiverResult_t IEC104_Receiver_Get_Status(
const IEC104_Receiver_t *receiver, uint64_t connection_generation, IEC104_ReceiverStatus_t *status)
{
IEC104_ReceiverStatus_t prepared;
if (status == NULL || !Receiver_Valid(receiver, connection_generation)) {
return IEC104_RECEIVER_ERROR;
}
prepared.received_length = receiver->received_length;
prepared.expected_length = receiver->expected_length;
prepared.has_complete_frame = receiver->is_frame_ready;
*status = prepared;
return IEC104_RECEIVER_OK;
}
/**
* 功能:在连接收尾时清除接收缓冲及绑定,所有旧视图随即失效。
* 输入参数:receiver——待清理对象,允许NULL;调用方必须已经结束旧I/O及字节借用。
* 返回值:无。不调用Link或socket;之后重新使用须Init,不能直接Feed或Reset。
*/
void IEC104_Receiver_Abort(IEC104_Receiver_t *receiver)
{
if (receiver != NULL) {
memset(receiver, 0, sizeof(*receiver));
}
}
/**
* 功能:将按序到达的字节片段拼成一帧完整APDU,检查APCI及ASDU最低长度。
* 输入参数:receiver——已初始化接收器;connection_generation——本次输入所属代次;
* data——输入片段首地址,length为0时允许NULL;length——输入字节数。
* 输出参数:consumed——非NULL,写入本次实际消费数,格式错误时包括触发错误的字节;
* receiver——保存半帧,或保留一帧已通过格式及最低长度检查的完整报文。
* 返回值:FRAME_READY表示已有完整帧;IN_PROGRESS表示输入用完但尚无完整帧;
* FRAME_ERROR表示候选帧格式非法且已丢弃;ERROR表示参数、代次或内部状态非法。
* 说明:每次最多完成一帧;未消费的尾部由调用方保留,当前帧释放后再按原顺序递入。
* ERROR时接收器不变,有效consumed输出为0;输入、输出及接收器存储不得重叠。
* 这里只检查报文格式,不判断ASDU业务合法性、链路序号及启停状态。
*/
IEC104_ReceiverResult_t IEC104_Receiver_Feed(
IEC104_Receiver_t *receiver, uint64_t connection_generation,
const uint8_t *data, uint32_t length, uint32_t *consumed)
{
IEC104_ApciInfo_t apci;
if (consumed == NULL) {
return IEC104_RECEIVER_ERROR;
}
*consumed = 0U;
if (!Receiver_Valid(receiver, connection_generation) || (length != 0U && data == NULL)) {
return IEC104_RECEIVER_ERROR;
}
/* 上一帧尚未释放时暂停消费,保证调用方正在读取的帧内容不会被覆盖。 */
if (receiver->is_frame_ready != 0U) {
return IEC104_RECEIVER_FRAME_READY;
}
while (*consumed < length) {
uint32_t copy_length;
/* 阶段一:帧边界的首字节必须为0x68;不跳过垃圾重新同步。 */
if (receiver->received_length == 0U) {
uint8_t byte = data[(*consumed)++];
if (byte != IEC104_START_BYTE) {
Clear_Progress(receiver);
return IEC104_RECEIVER_FRAME_ERROR;
}
receiver->buffer[0] = byte;
receiver->received_length = 1U;
continue;
}
/* 阶段二:读取长度域。允许4..253,对应包含起始字节和长度字节的总长6..255。 */
if (receiver->received_length == 1U) {
uint8_t byte = data[(*consumed)++];
if (byte < IEC104_MIN_LENGTH_FIELD || byte > IEC104_MAX_LENGTH_FIELD) {
/* 非法长度字节已消费;立即返回,让调用方决定如何处理尚未消费的尾部。 */
Clear_Progress(receiver);
return IEC104_RECEIVER_FRAME_ERROR;
}
receiver->buffer[1] = byte;
receiver->received_length = 2U;
receiver->expected_length = (uint16_t)(2U + byte);
continue;
}
/* 阶段三:按长度收齐控制域及可选ASDU;报文体中的0x68不再当作帧头。
* 复制量取“当前帧缺少的字节数”和“本次剩余输入”中的较小值,避免吞掉下一帧。 */
copy_length = (uint32_t)(receiver->expected_length - receiver->received_length);
if (copy_length > length - *consumed) {
copy_length = length - *consumed;
}
memcpy(receiver->buffer + receiver->received_length, data + *consumed, copy_length);
receiver->received_length = (uint16_t)(receiver->received_length + copy_length);
*consumed += copy_length;
if (receiver->received_length == receiver->expected_length) {
/* 长度收齐后仍须检查控制域、ASDU公共头和已知对象的最低载荷长度。 */
if (!Parse_Received_Frame(receiver, &apci)) {
/* 整个非法候选帧已消费,只清除组帧进度,不继续搜索其内部字节。 */
Clear_Progress(receiver);
return IEC104_RECEIVER_FRAME_ERROR;
}
/* 完整帧留在内部缓冲,直到调用方Peek、处理并显式Release。 */
receiver->is_frame_ready = 1U;
return IEC104_RECEIVER_FRAME_READY;
}
}
/* 包括length为0且无完整帧的情况;保留当前进度,等待后续片段。 */
return IEC104_RECEIVER_IN_PROGRESS;
}
/**
* 功能:取得当前完整帧的只读借用视图,不消费或释放报文。
* 输入参数:receiver——已初始化接收器;connection_generation——本次读取所属代次。
* 输出参数:frame——非NULL且不与receiver重叠,仅成功时写入原文地址、长度和APCI结果。
* 返回值:FRAME_READY表示视图有效;EMPTY表示尚无完整帧;ERROR表示参数或状态非法。
* 说明:允许重复读取,但同一帧被业务受理后不能重复分派;本函数不修改接收器。
* 视图引用内部缓冲,成功Release、Reset或重新Init后失效;需要长期保存时须复制。
*/
IEC104_ReceiverResult_t IEC104_Receiver_Peek_Frame(
const IEC104_Receiver_t *receiver, uint64_t connection_generation,
IEC104_ReceivedFrame_t *frame)
{
IEC104_ReceivedFrame_t prepared;
if (frame == NULL || !Receiver_Valid(receiver, connection_generation)) {
return IEC104_RECEIVER_ERROR;
}
if (receiver->is_frame_ready == 0U) {
return IEC104_RECEIVER_EMPTY;
}
memset(&prepared, 0, sizeof(prepared));
/* 重新解析到调用方视图,避免在Receiver对象中缓存指向自身缓冲的指针。 */
if (!Parse_Received_Frame(receiver, &prepared.apci)) {
return IEC104_RECEIVER_ERROR;
}
prepared.connection_generation = receiver->connection_generation;
prepared.apdu = receiver->buffer;
prepared.apdu_length = receiver->received_length;
/* 临时结果全部准备成功后再写输出,避免失败时向调用方交付不完整的视图。 */
*frame = prepared;
return IEC104_RECEIVER_FRAME_READY;
}
/**
* 功能:释放已处理的完整帧,允许接收器继续消费后续输入。
* 输入参数:receiver——已初始化接收器;connection_generation——本次释放所属代次。
* 输出参数:receiver——成功时清除组帧进度;缓冲字节、连接代次及初始化标志保留。
* 返回值:OK表示已释放;EMPTY表示没有完整帧,已有半帧继续保留;ERROR表示状态非法。
* 说明:释放后旧视图立即失效;此处只复用接收空间,不发送ACK、不推进链路状态。
* 业务暂未受理当前帧时不能调用本函数,应保留当前帧等待后续处理。
*/
IEC104_ReceiverResult_t IEC104_Receiver_Release_Frame(
IEC104_Receiver_t *receiver, uint64_t connection_generation)
{
if (!Receiver_Valid(receiver, connection_generation)) {
return IEC104_RECEIVER_ERROR;
}
if (receiver->is_frame_ready == 0U) {
return IEC104_RECEIVER_EMPTY;
}
Clear_Progress(receiver);
return IEC104_RECEIVER_OK;
}
/**
* 功能:显式丢弃当前半帧或完整帧,重新等待帧头。
* 输入参数:receiver——已初始化接收器;connection_generation——本次复位所属代次。
* 输出参数:receiver——成功时清除组帧进度,保留代次、初始化标志和缓冲字节。
* 返回值:OK表示复位完成;ERROR表示对象未初始化或代次非法,对象保持不变。
* 说明:与Release不同,无需已经收齐一帧,也允许清除异常进度;已有视图随之失效。
* 只复位接收器,不复位Link;新连接换代须调用Init,不能用Reset替代。
*/
IEC104_ReceiverResult_t IEC104_Receiver_Reset(
IEC104_Receiver_t *receiver, uint64_t connection_generation)
{
/* 此处仅核对连接身份,避免进度字段异常时反而无法执行清除。 */
if (!Session_Valid(receiver, connection_generation)) {
return IEC104_RECEIVER_ERROR;
}
Clear_Progress(receiver);
return IEC104_RECEIVER_OK;
}
@@ -0,0 +1,128 @@
/* 每连接独立的IEC-104流式接收器:固定缓冲、完整APDU交付,不处理链路序号或ACK。 */
#ifndef IEC104_RECEIVER_H
#define IEC104_RECEIVER_H
#include "IEC104_Codec.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 接收器操作结果;FRAME_ERROR表示收到非法报文,ERROR表示调用条件或内部状态非法。 */
typedef enum {
IEC104_RECEIVER_ERROR = -2, /* 参数、初始化状态、连接代次或内部进度非法 */
IEC104_RECEIVER_FRAME_ERROR = -1, /* 起始字节、APCI或ASDU最低长度非法,已清除候选帧 */
IEC104_RECEIVER_IN_PROGRESS = 0, /* 本次输入已消费完,尚无完整帧 */
IEC104_RECEIVER_OK = 1, /* Init、Reset、Release_Frame或Get_Status成功 */
IEC104_RECEIVER_FRAME_READY = 2, /* 已有一帧可读取,Feed不会继续消费后续字节 */
IEC104_RECEIVER_EMPTY = 3 /* Peek/Release时没有完整帧,半帧仍保留 */
} IEC104_ReceiverResult_t;
/* 调用方持有,每条连接使用独立实例;先Init,后续同连接串行操作,字段只由接口维护。
* is_frame_ready为0时:received_length为0表示等待帧头,1表示等待长度域,>=2表示收集报文体。
* is_frame_ready为1时:已收齐并通过APCI及ASDU最低长度检查,暂停输入,等待读取及释放。
* 完整帧保留至成功Release_Frame、Reset、Abort或重新Init,不保存自引用指针或私有Link状态。 */
typedef struct {
uint64_t connection_generation; /* 非零;新连接使用未复用的代次,拒绝旧I/O输入 */
uint16_t received_length; /* 已收到的候选帧字节数,0..255 */
uint16_t expected_length; /* APDU总长=长度域+2,范围6..255;长度域尚未知时为0 */
uint8_t is_initialized; /* 1表示Init成功;其他值不能作为已初始化实例使用 */
uint8_t is_frame_ready; /* 0表示尚无完整帧;1表示完整帧已就绪,等待显式释放 */
uint8_t buffer[IEC104_MAX_APDU_LEN]; /* 固定255字节,保存起始字节、长度域、控制域及可选ASDU */
} IEC104_Receiver_t;
/* 接收进度快照,不借用原文;供连接层判断新成帧、保留帧及诊断半帧进度。 */
typedef struct {
uint16_t received_length; /* 当前候选帧已收字节数;0表示尚无帧头 */
uint16_t expected_length; /* APDU总长;尚未取得长度域时为0 */
uint8_t has_complete_frame; /* 0/1:完整帧已通过格式及最低长度校验且尚未释放 */
} IEC104_ReceiverStatus_t;
/* 匹配代次并校验接收进度后写status;OK成功,ERROR不改输出,不重新分派报文。 */
extern IEC104_ReceiverResult_t IEC104_Receiver_Get_Status(
const IEC104_Receiver_t *receiver, uint64_t connection_generation, IEC104_ReceiverStatus_t *status);
/* 结束旧I/O及所有借用后解除接收器绑定;允许NULL,不操作外部TCP或Link。 */
extern void IEC104_Receiver_Abort(IEC104_Receiver_t *receiver);
/* 只读借用视图;apdu及I帧的apci.asdu指向Receiver缓冲,不拥有或复制原文;S/U帧的asdu为NULL。
* 成功Release_Frame/Reset/Init或Abort后失效;完整帧等待释放期间Feed消费0字节,Peek可重复读取。
* 调用方不得修改原文;跨释放保存数据时,先自行复制并在新缓冲上重新解析APCI。 */
typedef struct {
uint64_t connection_generation; /* 当前完整帧所属连接代次;来自接收器绑定的代次 */
const uint8_t *apdu; /* 完整APDU首地址,指向receiver->buffer */
uint16_t apdu_length; /* 完整APDU字节数,包含起始字节和长度域 */
IEC104_ApciInfo_t apci; /* APCI解析结果;I帧的asdu指向apdu+IEC104_APCI_LEN */
} IEC104_ReceivedFrame_t;
/**
* 功能:清空接收器并绑定非零连接代次,用于首次使用或新连接换代。
* 输入参数:receiver——调用方提供的接收器存储;connection_generation——连接层分配的非零代次。
* 输出参数:receiver——成功时全部清零,再设置代次及初始化标志。
* 返回值:OK表示成功;ERROR表示参数非法,不修改receiver。
* 说明:旧连接及其I/O须先结束,再为新连接重新Init;已有帧视图在成功初始化后失效。
* 只初始化接收器,不初始化Link或发送模块。
*/
extern IEC104_ReceiverResult_t IEC104_Receiver_Init(
IEC104_Receiver_t *receiver, uint64_t connection_generation);
/**
* 功能:按序递入任意长度字节片段,支持半包、粘包;每次最多收齐一帧。
* 输入参数:receiver——已初始化接收器;connection_generation——本次输入所属代次;
* data——输入首地址,仅在length非0时必须非NULL;length——输入字节数。
* 输出参数:consumed——非NULL,写入实际消费数,包括触发格式错误的字节;ERROR时为0;
* receiver——保存接收进度或一帧完整APDU;输入、consumed及receiver不得重叠。
* 返回值:FRAME_READY——已有完整帧,先Peek并处理,再Release;未释放时再次Feed消费0字节;
* IN_PROGRESS——全部输入已消费,尚无完整帧,保留已有半帧;
* FRAME_ERROR——起始字节、APCI或ASDU最低长度非法,清除候选帧,停止在错误位置;
* ERROR——参数、初始化状态、连接代次或内部进度非法,不修改接收器。
* 说明:未消费的尾部起点为data + *consumed,长度为length - *consumed,由调用方保留后再按序递入;
* 不能跳过剩余字节,也不能重复递入已消费字节。length为0可查询当前是否已有完整帧。
* FRAME_ERROR时已消费错误起始/长度字节或整个非法候选帧,是否断开由连接层决定。
* 帧边界遇到非0x68立即报错;报文体内不搜索帧头。无动态分配、锁、socket I/O或半帧超时。
* FRAME_READY保证APCI合法、ASDU公共头完整,且已知固定长度对象满足VSQ/SQ所需最低长度;
* 未知/变长类型、零对象及具体字段是否合法仍由业务检查,详见Parse_ASDU_Header。
* N(S)/N(R)与链路序号的关系及启停状态由链路层处理;旧连接代次的输入返回ERROR。
*/
extern IEC104_ReceiverResult_t IEC104_Receiver_Feed(
IEC104_Receiver_t *receiver, uint64_t connection_generation,
const uint8_t *data, uint32_t length, uint32_t *consumed);
/**
* 功能:读取当前完整帧的只读借用视图,允许重复读取,不释放报文。
* 输入参数:receiver——已初始化接收器;connection_generation——本次读取所属代次。
* 输出参数:frame——非NULL且不与receiver重叠,仅返回FRAME_READY时写入,其余返回不改输出。
* 返回值:FRAME_READY表示取得视图;EMPTY表示尚无完整帧;ERROR表示参数、代次或状态非法。
* 说明:视图有效期见IEC104_ReceivedFrame_t;将frame.apci的I/S交给同连接的SendManager受理一次,
* U帧由连接层按功能码分派。业务未受理(如Manager返回WAIT)时保持当前帧,不释放,
* 也不越过它分派后续帧;Peek到Release之间不得复位或替换当前帧。
*/
extern IEC104_ReceiverResult_t IEC104_Receiver_Peek_Frame(
const IEC104_Receiver_t *receiver, uint64_t connection_generation,
IEC104_ReceivedFrame_t *frame);
/**
* 功能:在完整帧已被受理后释放接收空间,允许继续接收下一帧。
* 输入参数:receiver——已初始化接收器;connection_generation——本次释放所属代次。
* 输出参数:receiver——成功时清除组帧进度,使已有视图失效;不清零缓冲、不修改链路。
* 返回值:OK表示释放成功;EMPTY表示没有完整帧,保留已有半帧;ERROR表示状态非法,不改对象。
* 说明:释放不代表已发送协议ACK;业务暂未受理当前帧时应保留它,稍后重试处理。
*/
extern IEC104_ReceiverResult_t IEC104_Receiver_Release_Frame(
IEC104_Receiver_t *receiver, uint64_t connection_generation);
/**
* 功能:显式丢弃当前半帧或完整帧,恢复到等待帧头的阶段。
* 输入参数:receiver——已初始化接收器;connection_generation——本次复位所属代次。
* 输出参数:receiver——成功时清除组帧进度,保留代次及缓冲字节,使所有已有视图失效。
* 返回值:OK表示复位完成;ERROR表示对象未初始化或代次非法,不修改receiver。
* 说明:仅核对初始化标志和代次,允许清除异常组帧进度;新连接换代必须用Init。
* 不复位Link,也不能作为链路序号错误的恢复手段。
*/
extern IEC104_ReceiverResult_t IEC104_Receiver_Reset(
IEC104_Receiver_t *receiver, uint64_t connection_generation);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_RECEIVER_H */
@@ -0,0 +1,526 @@
/**
* 子站发送字节存储:一份当前I APDU,以及独立的固定6字节S/U控制FIFO。
* 已准备的当前帧连续发送;本地完成提交后复用存储,等待ACK的记录由Manager保存。
* HIGH原文已在提交时出队;NORMAL原文由Queue保留至ACK或被后续入队覆盖。
* 本文件不进行I/O,不分配协议序号,不推进Link状态;所有操作由同连接上下文串行执行。
*/
#include "IEC104_SendBuffer.h"
#include "IEC104_Codec.h"
#include <stddef.h>
#include <string.h>
/**
* 功能:将控制队列的逻辑偏移换算为数组下标。
* 输入参数:buffer——容量非零且边界已验证的缓冲;offset——相对控制队首的槽数偏移,不是字节数。
* 输出参数:无。
* 返回值:0..control_capacity-1范围内的物理下标。
* 说明:队尾位置也用此公式计算;使用32位加法,避免两个16位索引相加溢出。
*/
static uint16_t Control_Index(const IEC104_SendBuffer_t *buffer, uint16_t offset)
{
return (uint16_t)(((uint32_t)buffer->control_head_index + offset) % buffer->control_capacity);
}
/**
* 功能:检查一帧已提交报文的身份、类型、长度和写入进度。
* 输入参数:buffer——已检查公共字段的缓冲;state——非NULL帧元数据;is_i——非0检查当前I帧。
* 输出参数:无。
* 返回值:合法返回1,否则0。
* 说明:I帧另核对构帧快照;控制帧固定6字节。这里只检查元数据,不重新解析APDU字节。
*/
static int Frame_State_Valid(const IEC104_SendBuffer_t *buffer,
const IEC104_SendBufferFrameState_t *state, int is_i)
{
if (state->frame_serial == 0U || state->frame_serial >= buffer->next_frame_serial ||
state->written_bytes > state->apdu_length || state->encoded_nr > IEC104_SEQUENCE_MASK ||
(state->written_bytes == state->apdu_length && state->written_at_ms < state->queued_at_ms)) {
return 0;
}
if (is_i) {
return state->frame_type == IEC104_SEND_BUFFER_FRAME_I &&
state->apdu_length >= IEC104_APCI_LEN + IEC104_ASDU_HEADER_LEN &&
state->apdu_length <= IEC104_MAX_APDU_LEN &&
buffer->i_frame_snapshot.send_number <= IEC104_SEQUENCE_MASK &&
buffer->i_frame_snapshot.receive_number == state->encoded_nr &&
buffer->i_frame_snapshot.frame_length == state->apdu_length;
}
return state->frame_type >= IEC104_SEND_BUFFER_FRAME_S_ACK &&
state->frame_type <= IEC104_SEND_BUFFER_FRAME_TESTFR_CON &&
state->apdu_length == IEC104_APCI_LEN &&
(state->frame_type == IEC104_SEND_BUFFER_FRAME_S_ACK || state->encoded_nr == 0U);
}
/**
* 功能:检查缓冲控制结构、当前I帧、控制FIFO端点和唯一预留。
* 输入参数:buffer——待检查对象,允许NULL。
* 输出参数:无。
* 返回值:访问到的边界及元数据自洽返回1,否则0。
* 说明:不遍历控制FIFO中间槽,不维护空闲链、保留链或另一套协议序号。
* 当前I帧存在时,后续控制队首必须尚未写出字节,避免两个APDU交错发送。
*/
static int Buffer_State_Valid(const IEC104_SendBuffer_t *buffer)
{
const IEC104_SendBufferFrameState_t *state;
if (buffer == NULL || buffer->is_initialized != 1U || buffer->control_frames == NULL ||
buffer->control_capacity == 0U || buffer->control_head_index >= buffer->control_capacity ||
buffer->control_count > buffer->control_capacity || buffer->connection_generation == 0U ||
buffer->next_frame_serial == 0U || buffer->has_i_frame > 1U || buffer->has_reservation > 1U ||
buffer->reservation_is_i_frame > 1U ||
(buffer->has_reservation == 0U && buffer->reservation_is_i_frame != 0U)) {
return 0;
}
if (buffer->has_i_frame != 0U && !Frame_State_Valid(buffer, &buffer->i_frame_state, 1)) {
return 0;
}
if (buffer->control_count != 0U) {
state = &buffer->control_frames[buffer->control_head_index].state;
if (!Frame_State_Valid(buffer, state, 0) ||
(buffer->has_i_frame != 0U && state->written_bytes != 0U) ||
!Frame_State_Valid(buffer, &buffer->control_frames[
Control_Index(buffer, (uint16_t)(buffer->control_count - 1U))].state, 0)) {
return 0;
}
}
/* 预留槽还未进入发送集合,类型与长度保持初始值;身份必须是刚分配的最后一个。 */
if (buffer->has_reservation != 0U) {
if (buffer->reservation_is_i_frame != 0U) {
if (buffer->has_i_frame != 0U || buffer->control_count != 0U) {
return 0;
}
state = &buffer->i_frame_state;
} else {
if (buffer->control_count == buffer->control_capacity) {
return 0;
}
state = &buffer->control_frames[Control_Index(buffer, buffer->control_count)].state;
}
if (state->frame_serial == 0U || state->frame_serial != buffer->next_frame_serial - 1U ||
state->frame_type != IEC104_SEND_BUFFER_FRAME_INVALID ||
state->apdu_length != 0U || state->written_bytes != 0U) {
return 0;
}
}
return 1;
}
/**
* 功能:核对事件是否属于指定帧的当前连接代次和预留身份。
* 输入参数:buffer/state——已验证对象;id——调用方提交的帧身份。
* 输出参数:无。
* 返回值:代次及非零frame_serial都匹配返回1,否则0。
* 说明:N(S)会回绕,不能用协议序号代替本地发送身份。
*/
static int Frame_Id_Matches(const IEC104_SendBuffer_t *buffer,
const IEC104_SendBufferFrameState_t *state, IEC104_SendFrameId_t id)
{
return id.connection_generation == buffer->connection_generation &&
id.frame_serial != 0U && id.frame_serial == state->frame_serial;
}
/**
* 功能:核对本地完成分派类型与已解析APCI是否一致。
* 输入参数:type——请求保存的帧类型;apci——成功解析的非NULL描述。
* 输出参数:无。
* 返回值:匹配返回1,否则0。
* 说明:只允许子站发送I、S、STARTDT_CON、STOPDT_CON及TESTFR ACT/CON。
*/
static int Frame_Type_Matches(IEC104_SendBufferFrameType_t type, const IEC104_ApciInfo_t *apci)
{
switch (type) {
case IEC104_SEND_BUFFER_FRAME_I:
return apci->frame_type == IEC104_FRAME_TYPE_I;
case IEC104_SEND_BUFFER_FRAME_S_ACK:
return apci->frame_type == IEC104_FRAME_TYPE_S;
case IEC104_SEND_BUFFER_FRAME_STARTDT_CON:
return apci->frame_type == IEC104_FRAME_TYPE_U && apci->u_function == IEC104_U_STARTDT_CON;
case IEC104_SEND_BUFFER_FRAME_STOPDT_CON:
return apci->frame_type == IEC104_FRAME_TYPE_U && apci->u_function == IEC104_U_STOPDT_CON;
case IEC104_SEND_BUFFER_FRAME_TESTFR_ACT:
return apci->frame_type == IEC104_FRAME_TYPE_U && apci->u_function == IEC104_U_TESTFR_ACT;
case IEC104_SEND_BUFFER_FRAME_TESTFR_CON:
return apci->frame_type == IEC104_FRAME_TYPE_U && apci->u_function == IEC104_U_TESTFR_CON;
default:
return 0;
}
}
/**
* 功能:初始化新连接的当前I帧存储,并绑定独立的控制槽数组。
* 输入参数:control_frames——持续有效的调用方数组;control_capacity——1..65535个槽;
* connection_generation——非零且跨连接不复用的代次。
* 输出参数:buffer——非NULL,仅成功时写入空闲状态。
* 返回值:OK或ERROR,失败不改buffer。
* 说明:唯一I APDU内置于buffer;不malloc、不清空控制数组、不创建socket或定时器。
*/
IEC104_SendBufferResult_t IEC104_SendBuffer_Init(
IEC104_SendBuffer_t *buffer, IEC104_SendBufferControlFrame_t *control_frames,
uint16_t control_capacity, uint64_t connection_generation)
{
IEC104_SendBuffer_t prepared;
if (buffer == NULL || control_frames == NULL || control_capacity == 0U || connection_generation == 0U) {
return IEC104_SEND_BUFFER_ERROR;
}
memset(&prepared, 0, sizeof(prepared));
prepared.control_frames = control_frames;
prepared.control_capacity = control_capacity;
prepared.connection_generation = connection_generation;
prepared.next_frame_serial = 1U;
prepared.is_initialized = 1U;
*buffer = prepared;
return IEC104_SEND_BUFFER_OK;
}
/**
* 功能:查询控制帧数量、当前I帧及预留状态。
* 输入参数:buffer——有效发送缓冲。
* 输出参数:status——非NULL,仅OK时写入数值副本。
* 返回值:OK或ERROR,失败不改输出。
* 说明:i_frame_ns仅在has_i_frame=1时有效;is_idle表示当前为空闲,
* 不证明TCP为新连接,也不报告Link的完成或ACK边界。
*/
IEC104_SendBufferResult_t IEC104_SendBuffer_Get_Status(
const IEC104_SendBuffer_t *buffer, IEC104_SendBufferStatus_t *status)
{
IEC104_SendBufferStatus_t prepared;
if (!Buffer_State_Valid(buffer) || status == NULL) {
return IEC104_SEND_BUFFER_ERROR;
}
prepared.connection_generation = buffer->connection_generation;
prepared.control_count = buffer->control_count;
prepared.has_i_frame = buffer->has_i_frame;
prepared.i_frame_ns = buffer->has_i_frame != 0U ? buffer->i_frame_snapshot.send_number : 0U;
prepared.has_reservation = buffer->has_reservation;
prepared.is_idle = buffer->control_count == 0U &&
buffer->has_i_frame == 0U && buffer->has_reservation == 0U;
/* Connection据此判断到期的t2/t3是否能准备新控制帧,避免容量阻塞时反复零等待。 */
prepared.control_has_room = buffer->has_reservation == 0U &&
buffer->control_count < buffer->control_capacity;
*status = prepared;
return IEC104_SEND_BUFFER_OK;
}
/**
* 功能:取得唯一预留,为随后Link构帧提供可写存储。
* 输入参数:buffer——有效缓冲;is_i——1预留当前I槽,0预留控制队尾。
* 输出参数:reservation——非NULL,仅OK时写入身份、APDU地址和容量;buffer记录预留。
* 返回值:OK成功;BUSY表示已有预留或I帧不能取得当前发送位置;
* FULL表示控制槽满;ERROR表示参数、状态或身份编号耗尽。
* 说明:I槽只能在当前I及控制队列均空时预留;S/U可以排在已准备的I帧之后。
* 编号在成功预留时消耗,取消后也不复用;非OK不修改对象或输出。
*/
static IEC104_SendBufferResult_t Reserve_Frame(
IEC104_SendBuffer_t *buffer, IEC104_SendBufferReservation_t *reservation, uint8_t is_i)
{
IEC104_SendBufferFrameState_t *state;
IEC104_SendBufferReservation_t prepared;
if (!Buffer_State_Valid(buffer) || reservation == NULL) {
return IEC104_SEND_BUFFER_ERROR;
}
if (buffer->has_reservation != 0U ||
(is_i != 0U && (buffer->has_i_frame != 0U || buffer->control_count != 0U))) {
return IEC104_SEND_BUFFER_BUSY;
}
if (is_i == 0U && buffer->control_count == buffer->control_capacity) {
return IEC104_SEND_BUFFER_FULL;
}
if (buffer->next_frame_serial == UINT64_MAX) {
return IEC104_SEND_BUFFER_ERROR;
}
if (is_i != 0U) {
state = &buffer->i_frame_state;
prepared.apdu = buffer->i_apdu;
prepared.apdu_capacity = IEC104_MAX_APDU_LEN;
memset(&buffer->i_frame_snapshot, 0, sizeof(buffer->i_frame_snapshot));
} else {
IEC104_SendBufferControlFrame_t *control = &buffer->control_frames[
Control_Index(buffer, buffer->control_count)];
state = &control->state;
prepared.apdu = control->apdu;
prepared.apdu_capacity = IEC104_APCI_LEN;
}
memset(state, 0, sizeof(*state));
memset(prepared.apdu, 0, prepared.apdu_capacity);
state->frame_serial = buffer->next_frame_serial++;
buffer->has_reservation = 1U;
buffer->reservation_is_i_frame = is_i;
prepared.frame_id.connection_generation = buffer->connection_generation;
prepared.frame_id.frame_serial = state->frame_serial;
*reservation = prepared;
return IEC104_SEND_BUFFER_OK;
}
/**
* 功能:预留控制FIFO的下一个空槽。
* 输入参数:buffer——有效发送缓冲。
* 输出参数:reservation——仅OK时返回frame_id和6字节可写APDU存储。
* 返回值:OK、FULL、BUSY或ERROR,含义见Reserve_Frame。
* 说明:不占I帧窗口或业务记录;成功后须构帧提交,或在未构帧成功时取消预留。
*/
IEC104_SendBufferResult_t IEC104_SendBuffer_Reserve_S_U_Frame(
IEC104_SendBuffer_t *buffer, IEC104_SendBufferReservation_t *reservation)
{
return Reserve_Frame(buffer, reservation, 0U);
}
/**
* 功能:预留唯一当前I帧的APDU存储。
* 输入参数:buffer——有效发送缓冲。
* 输出参数:reservation——仅OK时返回frame_id和最大255字节可写存储。
* 返回值:OK、BUSY或ERROR,含义见Reserve_Frame。
* 说明:此处尚未分配协议N(S);后续由Link.Prepare_I_Frame在实际发送准备时分配。
*/
IEC104_SendBufferResult_t IEC104_SendBuffer_Reserve_I_Frame(
IEC104_SendBuffer_t *buffer, IEC104_SendBufferReservation_t *reservation)
{
return Reserve_Frame(buffer, reservation, 1U);
}
/**
* 功能:校验预留报文并发布为当前I帧或控制FIFO的新队尾。
* 输入参数:buffer/frame_id——本次预留及身份;frame_type/apdu_length——实际报文描述;
* i_frame_snapshot——I帧必须传原Prepare快照,S/U必须为NULL;
* queued_at_ms——原始本地发送期限起点,使用单调毫秒,允许0。
* 输出参数:buffer及对应槽元数据——成功时保存描述、时间、快照并解除预留。
* 返回值:OK或ERROR;ERROR不修改缓冲状态,调用前存在的合法预留仍保留。
* 说明:调用前APDU已写入reservation.apdu。Link构帧成功后本接口出错须结束连接,
* 不能通过Cancel撤回已经分配的N(S)或已建立的控制发送义务。
*/
IEC104_SendBufferResult_t IEC104_SendBuffer_Commit_Frame(
IEC104_SendBuffer_t *buffer, IEC104_SendFrameId_t frame_id,
IEC104_SendBufferFrameType_t frame_type, uint16_t apdu_length,
const IEC104_IFrameTx_t *i_frame_snapshot, uint64_t queued_at_ms)
{
IEC104_SendBufferFrameState_t *state;
const uint8_t *apdu;
IEC104_ApciInfo_t parsed;
if (!Buffer_State_Valid(buffer) || buffer->has_reservation == 0U) {
return IEC104_SEND_BUFFER_ERROR;
}
if (buffer->reservation_is_i_frame != 0U) {
if (frame_type != IEC104_SEND_BUFFER_FRAME_I || i_frame_snapshot == NULL ||
apdu_length < IEC104_APCI_LEN + IEC104_ASDU_HEADER_LEN || apdu_length > IEC104_MAX_APDU_LEN ||
i_frame_snapshot->frame_length != apdu_length) {
return IEC104_SEND_BUFFER_ERROR;
}
state = &buffer->i_frame_state;
apdu = buffer->i_apdu;
} else {
const IEC104_SendBufferControlFrame_t *control = &buffer->control_frames[
Control_Index(buffer, buffer->control_count)];
if (frame_type == IEC104_SEND_BUFFER_FRAME_I || i_frame_snapshot != NULL || apdu_length != IEC104_APCI_LEN) {
return IEC104_SEND_BUFFER_ERROR;
}
state = &buffer->control_frames[Control_Index(buffer, buffer->control_count)].state;
apdu = control->apdu;
}
/* 完成全部匹配检查后才发布元数据;实际字节和保存的完成分派类型不能分离。 */
if (!Frame_Id_Matches(buffer, state, frame_id) || !IEC104_Parse_APCI(apdu, apdu_length, &parsed) ||
!Frame_Type_Matches(frame_type, &parsed) ||
(i_frame_snapshot != NULL &&
(i_frame_snapshot->send_number != parsed.send_number ||
i_frame_snapshot->receive_number != parsed.receive_number))) {
return IEC104_SEND_BUFFER_ERROR;
}
state->apdu_length = apdu_length;
state->frame_type = (uint8_t)frame_type;
state->encoded_nr = parsed.receive_number;
state->queued_at_ms = queued_at_ms;
if (i_frame_snapshot != NULL) {
buffer->i_frame_snapshot = *i_frame_snapshot;
buffer->has_i_frame = 1U;
} else {
buffer->control_count++;
}
buffer->has_reservation = 0U;
buffer->reservation_is_i_frame = 0U;
return IEC104_SEND_BUFFER_OK;
}
/**
* 功能:取消身份匹配的唯一预留。
* 输入参数:buffer——有效缓冲;frame_id——Reserve返回的完整身份。
* 输出参数:buffer——成功时清除预留标志,不推进控制队列或发布当前I帧。
* 返回值:OK或ERROR,身份不符或没有预留时不修改状态。
* 说明:调用方保证Link尚未成功构帧;这里只归还存储,不回滚Link,不复用frame_serial。
*/
IEC104_SendBufferResult_t IEC104_SendBuffer_Cancel_Reservation(
IEC104_SendBuffer_t *buffer, IEC104_SendFrameId_t frame_id)
{
const IEC104_SendBufferFrameState_t *state;
if (!Buffer_State_Valid(buffer) || buffer->has_reservation == 0U) {
return IEC104_SEND_BUFFER_ERROR;
}
state = buffer->reservation_is_i_frame != 0U ? &buffer->i_frame_state :
&buffer->control_frames[Control_Index(buffer, buffer->control_count)].state;
if (!Frame_Id_Matches(buffer, state, frame_id)) {
return IEC104_SEND_BUFFER_ERROR;
}
buffer->has_reservation = 0U;
buffer->reservation_is_i_frame = 0U;
return IEC104_SEND_BUFFER_OK;
}
/**
* 功能:取得已经准备好的当前发送帧及剩余字节。
* 输入参数:buffer——有效发送缓冲。
* 输出参数:head_info——非NULL,仅OK时写入身份、元数据及剩余字节借用指针。
* 返回值:OK有当前帧,EMPTY没有已提交报文,ERROR表示参数或状态非法。
* 说明:优先续传当前I帧,否则取控制队首;不选择ASDU、不重新编号。
* 已写完但尚未Commit_Send_Completion的帧仍返回OK,此时remaining_bytes为0。
* 原始帧即使因EAGAIN尚未写出字节也保持当前位置,借用在完成释放前结束。
*/
IEC104_SendBufferResult_t IEC104_SendBuffer_Peek_Send_Head(
const IEC104_SendBuffer_t *buffer, IEC104_SendBufferHeadInfo_t *head_info)
{
IEC104_SendBufferHeadInfo_t prepared;
const IEC104_SendBufferFrameState_t *state;
const uint8_t *apdu;
if (!Buffer_State_Valid(buffer) || head_info == NULL) {
return IEC104_SEND_BUFFER_ERROR;
}
memset(&prepared, 0, sizeof(prepared));
if (buffer->has_i_frame != 0U) {
state = &buffer->i_frame_state;
apdu = buffer->i_apdu;
prepared.i_frame_snapshot = buffer->i_frame_snapshot;
} else if (buffer->control_count != 0U) {
state = &buffer->control_frames[buffer->control_head_index].state;
apdu = buffer->control_frames[buffer->control_head_index].apdu;
} else {
return IEC104_SEND_BUFFER_EMPTY;
}
prepared.frame_id.connection_generation = buffer->connection_generation;
prepared.frame_id.frame_serial = state->frame_serial;
prepared.pending_data = apdu + state->written_bytes;
prepared.remaining_bytes = (uint16_t)(state->apdu_length - state->written_bytes);
prepared.apdu_length = state->apdu_length;
prepared.written_bytes = state->written_bytes;
prepared.frame_type = (IEC104_SendBufferFrameType_t)state->frame_type;
prepared.encoded_nr = state->encoded_nr;
prepared.queued_at_ms = state->queued_at_ms;
prepared.written_at_ms = state->written_at_ms;
*head_info = prepared;
return IEC104_SEND_BUFFER_OK;
}
/**
* 功能:记录当前帧本次实际成功写入的字节增量。
* 输入参数:buffer/frame_id——当前缓冲及帧身份;bytes_written_now——不超过剩余长度的增量;
* written_at_ms——本次实际写入时刻;仅整帧写完时校验不早于queued_at_ms并保存。
* 输出参数:buffer——推进written_bytes;整帧写完时登记written_at_ms。
* 返回值:OK尚未完整;FRAME_WRITTEN恰好整帧写完;ERROR表示身份、长度、时间或状态非法。
* 说明:增量0不推进;部分写入不续期。函数只记账,不写socket、不调用Link.Complete,
* 同一成功增量不能重报;最终完成后再重复记账返回ERROR。
*/
IEC104_SendBufferResult_t IEC104_SendBuffer_Record_Written_Bytes(
IEC104_SendBuffer_t *buffer, IEC104_SendFrameId_t frame_id,
uint16_t bytes_written_now, uint64_t written_at_ms)
{
IEC104_SendBufferFrameState_t *state;
if (!Buffer_State_Valid(buffer) || (buffer->has_i_frame == 0U && buffer->control_count == 0U)) {
return IEC104_SEND_BUFFER_ERROR;
}
state = buffer->has_i_frame != 0U ? &buffer->i_frame_state :
&buffer->control_frames[buffer->control_head_index].state;
if (!Frame_Id_Matches(buffer, state, frame_id) || state->written_bytes == state->apdu_length ||
bytes_written_now > (uint16_t)(state->apdu_length - state->written_bytes) ||
(bytes_written_now == (uint16_t)(state->apdu_length - state->written_bytes) &&
written_at_ms < state->queued_at_ms)) {
return IEC104_SEND_BUFFER_ERROR;
}
/* 只累计成功字节;queued_at_ms始终保留,最后一段才建立实际写完时刻。 */
state->written_bytes = (uint16_t)(state->written_bytes + bytes_written_now);
if (state->written_bytes == state->apdu_length) {
state->written_at_ms = written_at_ms;
return IEC104_SEND_BUFFER_FRAME_WRITTEN;
}
return IEC104_SEND_BUFFER_OK;
}
/**
* 功能:释放已整帧写完且身份匹配的当前I槽或控制队首。
* 输入参数:buffer——有效缓冲;frame_id——本次已经完成的当前帧身份。
* 输出参数:buffer——清除has_i_frame,或推进控制队首并减少数量。
* 返回值:OK或ERROR;未写完、身份不符或新队首非法时不释放。
* 说明:调用方须先提交该帧适用的Link完成结果;I帧还须登记本地完成事实。
* I槽不等待ACK;Manager继续保留发送记录,NORMAL原文可能仍在Queue或已被覆盖。
* TESTFR_CON没有Link完成接口;本接口本身不分派Link完成或解释ACK。
*/
IEC104_SendBufferResult_t IEC104_SendBuffer_Commit_Send_Completion(
IEC104_SendBuffer_t *buffer, IEC104_SendFrameId_t frame_id)
{
IEC104_SendBufferFrameState_t *state;
if (!Buffer_State_Valid(buffer) || (buffer->has_i_frame == 0U && buffer->control_count == 0U)) {
return IEC104_SEND_BUFFER_ERROR;
}
state = buffer->has_i_frame != 0U ? &buffer->i_frame_state :
&buffer->control_frames[buffer->control_head_index].state;
if (!Frame_Id_Matches(buffer, state, frame_id) || state->written_bytes != state->apdu_length) {
return IEC104_SEND_BUFFER_ERROR;
}
if (buffer->has_i_frame != 0U) {
buffer->has_i_frame = 0U;
} else {
/* 先验证即将成为队首的槽,再整体推进,避免错误返回时丢失当前帧引用。 */
if (buffer->control_count > 1U &&
!Frame_State_Valid(buffer, &buffer->control_frames[Control_Index(buffer, 1U)].state, 0)) {
return IEC104_SEND_BUFFER_ERROR;
}
buffer->control_head_index = Control_Index(buffer, 1U);
buffer->control_count--;
}
return IEC104_SEND_BUFFER_OK;
}
/**
* 功能:判断当前发送帧是否达到本地排队及写入总期限。
* 输入参数:buffer/connection_generation——有效缓冲及匹配代次;
* now_ms——当前单调毫秒;timeout_ms——非零期限。
* 输出参数:无,不修改进度或关闭连接。
* 返回值:1到期,0无当前帧或未到期,-1参数/状态非法或当前时刻早于起点。
* 说明:从原queued_at_ms作差,等于期限即到期;部分写入和重复查询不续期。
* 尚未提交到本缓冲区的ASDU排队时间不在此计算;I帧等待ACK的t1由Manager检查。
* 已整帧写完但尚未提交完成释放的帧仍在检查范围内。
*/
int IEC104_SendBuffer_Head_Send_Expired(
const IEC104_SendBuffer_t *buffer, uint64_t connection_generation, uint64_t now_ms, uint32_t timeout_ms)
{
IEC104_SendBufferHeadInfo_t head;
IEC104_SendBufferResult_t result;
if (buffer == NULL || connection_generation != buffer->connection_generation || timeout_ms == 0U) {
return -1;
}
result = IEC104_SendBuffer_Peek_Send_Head(buffer, &head);
if (result == IEC104_SEND_BUFFER_EMPTY) {
return 0;
}
if (result != IEC104_SEND_BUFFER_OK || now_ms < head.queued_at_ms) {
return -1;
}
return now_ms - head.queued_at_ms >= timeout_ms;
}
/**
* 功能:清除发送缓冲控制结构和内置当前I帧,解除对控制数组的引用。
* 输入参数:buffer——待终止对象,允许NULL。
* 输出参数:buffer——非NULL时清零;调用方控制数组不清零、不释放。
* 返回值:无。
* 说明:不关闭socket、不取消异步I/O,调用方必须先结束旧I/O及报文字节借用。
*/
void IEC104_SendBuffer_Abort(IEC104_SendBuffer_t *buffer)
{
if (buffer != NULL) {
memset(buffer, 0, sizeof(*buffer));
}
}
@@ -0,0 +1,168 @@
/* 子站发送字节存储:一帧当前I报文 + 独立紧凑S/U队列,不执行TCP I/O。 */
#ifndef IEC104_SENDBUFFER_H
#define IEC104_SENDBUFFER_H
#include "IEC104_Link_Common.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 本地发送描述;子站启停只发送CON,TESTFR支持ACT和CON。 */
typedef enum {
IEC104_SEND_BUFFER_FRAME_INVALID = 0,
IEC104_SEND_BUFFER_FRAME_I,
IEC104_SEND_BUFFER_FRAME_S_ACK,
IEC104_SEND_BUFFER_FRAME_STARTDT_CON,
IEC104_SEND_BUFFER_FRAME_STOPDT_CON,
IEC104_SEND_BUFFER_FRAME_TESTFR_ACT,
IEC104_SEND_BUFFER_FRAME_TESTFR_CON
} IEC104_SendBufferFrameType_t;
/* 必须精确比较枚举值;正数不统一表示存在报文或发送完成。 */
typedef enum {
IEC104_SEND_BUFFER_ERROR = -1, /* 参数、身份、状态或报文描述非法 */
IEC104_SEND_BUFFER_FULL = 0, /* S/U控制队列无空槽 */
IEC104_SEND_BUFFER_OK = 1, /* 操作成功;记账时表示尚未整帧写完 */
IEC104_SEND_BUFFER_EMPTY = 2, /* 没有已提交帧;未Commit的预留不算已提交帧 */
IEC104_SEND_BUFFER_BUSY = 3, /* 已有预留,或I帧不能取得当前发送位置 */
IEC104_SEND_BUFFER_FRAME_WRITTEN = 4 /* 字节已全部写完,尚需适用的链路完成处理及缓冲释放 */
} IEC104_SendBufferResult_t;
typedef struct {
uint64_t connection_generation; /* 非零且跨连接不复用 */
uint64_t frame_serial; /* 每次预留分配;取消后也不复用 */
} IEC104_SendFrameId_t;
/* apdu只在本次预留有效时可写,Commit/Cancel后不可继续修改。 */
typedef struct {
IEC104_SendFrameId_t frame_id; /* 预留身份,Commit/Cancel必须原样传回 */
uint8_t *apdu; /* Link直接构帧的可写地址 */
uint16_t apdu_capacity; /* I槽255字节,S/U槽6字节 */
} IEC104_SendBufferReservation_t;
/* 数值可以复制;pending_data借用队首存储,Commit_Send_Completion/Abort/Init后失效。 */
typedef struct {
IEC104_SendFrameId_t frame_id; /* 当前发送身份,用于每次增量及完成匹配 */
const uint8_t *pending_data; /* APDU起点+written_bytes,供实际I/O借用 */
uint16_t remaining_bytes; /* 本次最多可写的剩余字节数 */
uint16_t apdu_length; /* 完整报文长度,不随部分写入改变 */
uint16_t written_bytes; /* 累计成功写入字节数 */
IEC104_SendBufferFrameType_t frame_type; /* 完成分派类型,不按当前Link状态推断 */
IEC104_IFrameTx_t i_frame_snapshot; /* I帧原始快照;S/U查询时为0 */
uint16_t encoded_nr; /* I/S中实际编码的N(R),U帧为0 */
uint64_t queued_at_ms; /* 本地发送期限的原始起点 */
uint64_t written_at_ms; /* 仅整帧写完时有效,允许时刻0 */
} IEC104_SendBufferHeadInfo_t;
/* 共用写入元数据;frame_type保存上面的枚举值。 */
typedef struct {
uint64_t frame_serial; /* 本次预留分配的编号,不随槽位复用而复用 */
uint64_t queued_at_ms; /* Commit时保存的原始单调时刻 */
uint64_t written_at_ms; /* 最后一段成功写入的实际时刻,允许0 */
uint16_t apdu_length; /* 完整长度;尚未提交的预留为0 */
uint16_t written_bytes; /* 累计已写长度,不可超过apdu_length */
uint16_t encoded_nr; /* Commit从APCI读取的确认快照 */
uint8_t frame_type; /* 已提交枚举类型;尚未提交为INVALID */
} IEC104_SendBufferFrameState_t;
/* 调用方仅为控制帧分配数组,每槽只有6字节APDU,不再为S/U预留255字节。 */
typedef struct {
IEC104_SendBufferFrameState_t state; /* 本槽身份、类型、写入进度及时间 */
uint8_t apdu[IEC104_APCI_LEN]; /* S/U固定6字节报文 */
} IEC104_SendBufferControlFrame_t;
typedef struct {
IEC104_SendBufferControlFrame_t *control_frames; /* 调用方提供的S/U槽数组 */
uint16_t control_capacity; /* S/U槽总数,1..65535 */
uint16_t control_head_index; /* 控制队首下标;空队列由control_count=0表示 */
uint16_t control_count; /* 已提交S/U数量,不含正在预留的槽 */
IEC104_SendBufferFrameState_t i_frame_state; /* 唯一当前I槽的元数据 */
IEC104_IFrameTx_t i_frame_snapshot; /* 原Prepare快照,交回Link.Complete使用 */
uint8_t i_apdu[IEC104_MAX_APDU_LEN]; /* 唯一当前I帧,Commit_Send_Completion成功后可复用 */
uint8_t has_i_frame; /* 0/1:I槽已提交,包括尚未写出字节的当前帧 */
uint8_t has_reservation; /* 0/1:当前是否存在尚未Commit/Cancel的预留 */
uint8_t reservation_is_i_frame; /* 0/1:预留是否为I槽;无预留时必须为0 */
uint8_t is_initialized; /* 1表示Init成功,不表示TCP存活 */
uint64_t connection_generation; /* 本次连接的非零、不复用代次 */
uint64_t next_frame_serial; /* 下一个预留身份,耗尽时报错而不回绕 */
} IEC104_SendBuffer_t;
/* 不复制协议序号边界;Link管序号,AsduQueue保留原文,Manager保存确认记录。 */
typedef struct {
uint64_t connection_generation; /* 查询时的绑定代次 */
uint16_t control_count; /* 已提交控制帧数 */
uint16_t i_frame_ns; /* 仅has_i_frame=1时有效 */
uint8_t has_i_frame; /* 当前是否有已提交I帧 */
uint8_t has_reservation; /* 当前是否还有未结束的预留 */
uint8_t is_idle; /* 当前无I帧、S/U或预留;完成发送后也可重新空闲 */
uint8_t control_has_room; /* 无预留且控制槽未满;只表示容量,不保证编号可分配 */
} IEC104_SendBufferStatus_t;
/*
* 初始化新连接;control_capacity>=1,generation非零且跨连接不复用。
* control_frames由调用方持有;无malloc、无锁,全部操作串行,存储/参数不得重叠。
* 控制帧有独立容量,不再设置control_reserve_count或APDU节点池。
*/
extern IEC104_SendBufferResult_t IEC104_SendBuffer_Init(
IEC104_SendBuffer_t *buffer, IEC104_SendBufferControlFrame_t *control_frames,
uint16_t control_capacity, uint64_t connection_generation);
/* 查询只返回数值副本,OK写status;失败不改输出,不提供已完成/ACK协议边界。 */
extern IEC104_SendBufferResult_t IEC104_SendBuffer_Get_Status(
const IEC104_SendBuffer_t *buffer, IEC104_SendBufferStatus_t *status);
/*
* S/U预留队尾,容量不足FULL;I只能在无当前帧且控制队列为空时预留,否则BUSY。
* 任一时刻只有一个预留;OK写输出,非OK不改对象或输出。编号耗尽ERROR。
* 先取得存储,再调用会产生发送义务的Link构帧接口。
*/
extern IEC104_SendBufferResult_t IEC104_SendBuffer_Reserve_S_U_Frame(
IEC104_SendBuffer_t *buffer, IEC104_SendBufferReservation_t *reservation);
extern IEC104_SendBufferResult_t IEC104_SendBuffer_Reserve_I_Frame(
IEC104_SendBuffer_t *buffer, IEC104_SendBufferReservation_t *reservation);
/*
* I提交为唯一当前帧,S/U追加到独立控制FIFO;校验APCI、身份和原始快照。
* 已准备的当前I帧(包括EAGAIN后尚未写出字节)先完成,再发后来的控制帧。
* 禁止交错写两个APDU,禁止撤回已编号的帧;Prepare后Commit失败须结束连接。
* Commit成功后借用的构帧指针不可再写;queued_at_ms是原始本地发送期限起点。
*/
extern IEC104_SendBufferResult_t IEC104_SendBuffer_Commit_Frame(
IEC104_SendBuffer_t *buffer, IEC104_SendFrameId_t frame_id,
IEC104_SendBufferFrameType_t frame_type, uint16_t apdu_length,
const IEC104_IFrameTx_t *i_frame_snapshot, uint64_t queued_at_ms);
/* 核对唯一预留身份后归还存储;仅限Link尚未构帧成功时使用,不回滚协议状态。 */
extern IEC104_SendBufferResult_t IEC104_SendBuffer_Cancel_Reservation(
IEC104_SendBuffer_t *buffer, IEC104_SendFrameId_t frame_id);
/* 查看已提交帧,不挑选业务;已写完但未提交完成时仍返回OK且remaining_bytes为0。上层入口为Manager.Prepare_Next_Send。 */
extern IEC104_SendBufferResult_t IEC104_SendBuffer_Peek_Send_Head(
const IEC104_SendBuffer_t *buffer, IEC104_SendBufferHeadInfo_t *head_info);
/*
* 匹配当前帧,记录实际成功写入的增量;0不推进,部分写入OK,整帧FRAME_WRITTEN。
* 最后一次时间不得早于入队时刻;已写完不能重报,输出缓冲借用须在完成前结束。
* 同帧最多一个未回报写操作;不执行I/O、不调用Link.Complete。
*/
extern IEC104_SendBufferResult_t IEC104_SendBuffer_Record_Written_Bytes(
IEC104_SendBuffer_t *buffer, IEC104_SendFrameId_t frame_id,
uint16_t bytes_written_now, uint64_t written_at_ms);
/* 对应Link.Complete及I帧本地完成记录提交后调用;TESTFR_CON无Link.Complete。成功释放当前槽,不等待ACK。 */
extern IEC104_SendBufferResult_t IEC104_SendBuffer_Commit_Send_Completion(
IEC104_SendBuffer_t *buffer, IEC104_SendFrameId_t frame_id);
/* 1到期、0无帧/未到期、-1非法;部分写入不续期,已写完未释放的帧也参与检查。I帧ACK的t1由Manager查询。 */
extern int IEC104_SendBuffer_Head_Send_Expired(
const IEC104_SendBuffer_t *buffer, uint64_t connection_generation,
uint64_t now_ms, uint32_t timeout_ms);
/* 旧I/O结束后清除引用;不关闭TCP,不释放调用方数组。 */
extern void IEC104_SendBuffer_Abort(IEC104_SendBuffer_t *buffer);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_SENDBUFFER_H */
@@ -0,0 +1,402 @@
/*
* TCP内部的子站串行发送管理:ASDU调度、I帧交接、完成分派和确认记录;不执行socket I/O。
*
* 本头文件供Connection实现使用;下文的调用方指内部协调层,业务只通过TcpServer接入:
* - TcpServer_Send_Response经Connection_Submit_Response校验会话,本模块Submit_Response直接准备或入HIGH。
* - NORMAL由TcpServer直接缓存到服务队列;本模块仅在有效会话中准备实际发送时分配协议N(S)。
* - Prepare_Next_Send或Submit_Response取得字节,TcpServer经Net写socket,再经Connection回报写入增量或失败。
* 暂不可写时保留当前帧等待;本模块的Handle接口不代替实际I/O。
* - Connection从Receiver取得完整APDU及APCI,将I/S帧交给Handle_I_S_Frame处理累计ACK。
* - TcpServer经Connection查询回执,成功复制为RECORD事件后移除内部记录;业务用NextEvent取事件副本。
*
* Manager是本连接Link和SendBuffer的运行期修改入口,在一次调用内完成成组更新;没有后台自动同步。
* Link维护协议序号,SendBuffer保存当前I帧及控制FIFO,AsduQueue保留原文,Manager保存帧记录和NORMAL来源编号。
* 同连接的这些操作由Connection串行组织;已经交给Manager的事件不得再手动调用底层接口重复提交。
*/
#ifndef IEC104_SENDMANAGER_H
#define IEC104_SENDMANAGER_H
#include "IEC104_AsduQueue.h"
#include "IEC104_SendBuffer.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 按具体结果分支:Prepare_Next_Send及Submit_Response返回OK才提供可写字节;仅入队返回QUEUED。
* WAIT等正数不表示本次有报文可发送,FRAME_WRITTEN表示完整完成流程已经提交。 */
typedef enum {
IEC104_SEND_MANAGER_PROTOCOL_ERROR = -3, /* I/S报文被Link拒绝,须结束连接;与内部交接失败区分 */
IEC104_SEND_MANAGER_FATAL = -2, /* 当前连接必须结束,先回收业务再Abort */
IEC104_SEND_MANAGER_ERROR = -1, /* 参数、代次、frame_id或接入状态非法 */
IEC104_SEND_MANAGER_EMPTY = 0, /* 没有待处理项,具体含义见各接口说明 */
IEC104_SEND_MANAGER_OK = 1, /* 当前操作成功;Prepare可写,Handle_Send_Progress尚未整帧写完 */
IEC104_SEND_MANAGER_QUEUED = 2, /* 控制帧进入FIFO,或Submit_Response已缓存HIGH;尚未发送 */
IEC104_SEND_MANAGER_WAIT = 3, /* 门控/窗口/容量/待完成义务阻塞,不重置期限 */
IEC104_SEND_MANAGER_FRAME_WRITTEN = 4, /* 本地完成处理成功且已释放当前帧缓冲,不能再手动完成同一帧 */
IEC104_SEND_MANAGER_IGNORED = 5, /* 重复STOP或当前没有匹配等待过程的TESTFR_CON,不重启过程 */
} IEC104_SendManagerResult_t;
/*
* 每个I帧一条存储记录,只保存关联、序号快照及完成事实,不保存完整APDU。
* records数组使用Storage类型,帧身份只存frame_serial;连接代次由所属Manager统一保存。
* local_send_completed由Handle_Send_Progress登记,peer_acknowledged由Handle_I_S_Frame登记;
* 必须先提交本地发送完成,再处理覆盖该帧的ACK;两项都成立时才返回就绪回执。
* SendBuffer在本地完成时就释放APDU槽;就绪记录由TcpServer复制为事件后,经Connection调用Remove移除。
* 收尾时剩余记录先复制为closing事件,再由Connection_Close统一清理;不等待业务处理事件。
* 队列HIGH原文在I帧提交及记录建立后即出队;直接应答不入ASDU队列,使用相同HIGH记录。
* NORMAL原文保留到有效ACK或被后续入队覆盖。
* 两级发送记录均独立等待本地完成和有效ACK;HIGH队列清空不撤回已提交I帧。
* 原文存储与回执消费分开;asdu_entry_serial只用于NORMAL确认匹配,HIGH为0;不保存业务user_tag。
* 覆盖不撤回帧、不提前释放k窗口;两级发送完成及回执仍使用独立frame_id。
*/
typedef struct {
uint64_t frame_serial; /* 本连接内不复用的发送编号;代次取所属Manager */
uint64_t asdu_entry_serial; /* NORMAL条目编号;HIGH固定0,不提供ASDU业务身份 */
uint64_t queued_at_ms; /* 实际准备I帧的时刻,本地发送期限起点 */
uint64_t written_at_ms; /* local_send_completed==1时有效,允许0 */
IEC104_IFrameTx_t i_frame_snapshot; /* 原始N(S)/N(R)/长度快照 */
IEC104_AsduPriority_t asdu_priority; /* 原始优先级 */
uint8_t local_send_completed; /* 0/1:完整写出且Link.Complete成功 */
uint8_t peer_acknowledged; /* 0/1:有效累计ACK覆盖 */
} IEC104_IFrameSendRecordStorage_t;
/* 内部查询的独立数值副本:用Manager代次补齐frame_id;TcpServer另转换为公开记录类型。 */
typedef struct {
IEC104_SendFrameId_t frame_id; /* 连接代次 + 不复用的发送身份 */
uint64_t asdu_entry_serial; /* NORMAL用于ACK释放;HIGH固定0,按frame_id读取帧回执 */
uint64_t queued_at_ms; /* 实际准备I帧的时刻,本地发送期限起点 */
uint64_t written_at_ms; /* local_send_completed==1时有效,允许0 */
IEC104_IFrameTx_t i_frame_snapshot; /* 原始N(S)/N(R)/长度快照 */
IEC104_AsduPriority_t asdu_priority; /* 原始优先级;HIGH只确认记录,NORMAL另确认原文队列 */
uint8_t local_send_completed; /* 0/1:完整写出且Link.Complete成功 */
uint8_t peer_acknowledged; /* 0/1:有效累计ACK覆盖,非业务执行结果 */
} IEC104_IFrameSendRecord_t;
/* 完成结果不含已移出发送FIFO槽的裸指针;TESTFR_ACT完成时可记录等待CON的原始起点。 */
typedef struct {
IEC104_SendFrameId_t frame_id; /* 已完成帧的连接代次与帧身份,供接入方关联事件 */
IEC104_SendBufferFrameType_t frame_type; /* 原帧保存的帧类型,不根据完成后的链路状态推断 */
IEC104_LinkResult_t link_completion_result; /* 保留OK/IGNORED,供调用方决定是否启动过程计时 */
IEC104_IFrameTx_t i_frame_snapshot; /* 仅I帧有效 */
uint64_t queued_at_ms; /* 原始入队时刻,部分写入或重试不续期 */
uint64_t written_at_ms; /* 实际整帧写完时刻;主动U帧成功后用于启动等待CON计时 */
} IEC104_LocalSendCompletion_t;
/*
* 发送管理器由Connection内含,完整布局用于内部静态存储;仅借用下列对象和数组。
* link_state/send_buffer指向本连接实际对象,不复制整份状态。
* 对Link/SendBuffer/AsduQueue的更新通过模块接口完成,Manager直接维护自身控制字段及records数组。
* 字段由接口维护,不提供锁或TCP连接存活判定。
*/
typedef struct {
IEC104_AsduQueue_t *asdu_queue; /* 仍保留的原文及NORMAL覆盖待确认计数 */
IEC104_SendBuffer_t *send_buffer; /* 唯一当前I帧及独立紧凑S/U FIFO */
IEC104_LinkState_t *link_state; /* 本连接唯一的CONTROLLED链路状态 */
IEC104_IFrameSendRecordStorage_t *records; /* 调用方提供的存储数组,各条不重复保存连接代次 */
uint16_t record_capacity; /* 包括未完成记录和未消费帧回执的总容量 */
uint16_t record_head_index; /* 最早尚未由TcpServer转为事件并移除的记录下标 */
uint16_t record_count; /* ACK已到也不自动减计数,消费回执后才释放 */
uint16_t high_unacked_count; /* 已登记且未ACK的HIGH I帧数,含直接应答及已出队应答 */
uint16_t next_record_ns; /* 下一条新登记I帧记录的N(S),与Link下一分配位置一致 */
uint16_t send_window_k; /* 交给Link检查的未确认I帧窗口上限 */
uint16_t high_priority_burst_limit; /* 0严格高优先;非0限制NORMAL有未提交条目时连续选择HIGH的次数 */
uint16_t high_priority_burst_count; /* 成功交接后更新;Init及从停用状态启动时清零 */
uint64_t connection_generation; /* 本Manager所有记录共用的代次;旧事件不得提交到新连接 */
uint8_t is_initialized; /* 1表示发送管理器已初始化,不表示TCP可用 */
} IEC104_SendManager_t;
/* 存储概况,不包含可修改的队列/Link指针;FAILED回收可用,不要求运行中的Link仍合法。 */
typedef struct {
uint64_t connection_generation; /* 当前绑定代次,Abort后不再可查询 */
uint16_t record_capacity; /* 调用方提供的记录槽总数 */
uint16_t record_count; /* 尚未消费的全部记录,包含已ACK回执 */
uint16_t high_unacked_count; /* HIGH已交接但尚未被对端ACK的记录数 */
} IEC104_SendManagerStatus_t;
/* 读取存储快照,OK写status,ERROR不改输出;不消费记录、不检查跨模块运行一致性。 */
extern IEC104_SendManagerResult_t IEC104_SendManager_Get_Status(
const IEC104_SendManager_t *manager, IEC104_SendManagerStatus_t *status);
/* 当前发送期限的只读计算结果,不保存第二份计时起点。
* 对应has_*为1时remaining_ms有效,0表示已经到期;起点为0也是合法时刻。
* 队首身份与本地期限来自同一次查询,供超时后提交该帧失败;控制容量供连接安排t2/t3唤醒。 */
typedef struct {
IEC104_SendFrameId_t head_frame_id;
uint32_t local_send_remaining_ms;
uint32_t i_ack_remaining_ms;
uint8_t has_send_head;
uint8_t has_i_ack_deadline;
uint8_t control_has_room;
} IEC104_SendManagerDeadlines_t;
/* OK写完整副本,ERROR不改输出;两项期限均须非零,代次匹配,now_ms不得早于活动起点。
* 只观察已准备队首和最早已写完未ACK记录,不选择ASDU、不编号、不推进发送或关闭连接。
* 部分写入、重复ACK及队首更换不续期;部分ACK后使用下一条记录原有的written_at_ms。 */
extern IEC104_SendManagerResult_t IEC104_SendManager_Get_Deadlines(
const IEC104_SendManager_t *manager, uint64_t connection_generation,
uint64_t now_ms, uint32_t local_send_timeout_ms, uint32_t t1_ms,
IEC104_SendManagerDeadlines_t *deadlines);
/*
* 通过绑定的队列复制ASDU入队,避免上层取出manager内部指针再修改队列。
* HIGH仅在STARTED+IDLE时接手;否则先ERROR,不检查容量、不入队。
* OK成功,WAIT队列空间/条目编号窗口不足,ERROR参数/会话/阶段非法;不生成I帧或分配N(S)。
* NORMAL覆盖和内部条目编号规则同AsduQueue,入队不表示已完成本地发送或收到ACK。
* 受理新STOPDT_ACT时,尚在HIGH中的应答作废;已提交I帧及NORMAL不受该清理影响。
*/
extern IEC104_SendManagerResult_t IEC104_SendManager_Enqueue(
IEC104_SendManager_t *manager, uint64_t connection_generation,
IEC104_AsduPriority_t priority, const uint8_t *asdu, uint16_t asdu_length);
/* 仅STARTED+IDLE时接手;否则在容量检查前ERROR,不入HIGH。
* 准入后优先尝试直接准备I帧,不执行socket I/O;已有帧/旧HIGH/公平配额/窗口或记录容量阻塞时进入HIGH。
* START受理后即可接手;已有CON时暂存HIGH,不能越过控制FIFO发送。已启用时重复START保留HIGH。
* OK表示SendBuffer及发送记录已接手,仅此时写head_info;QUEUED表示HIGH已复制,WAIT表示尚未接手。
* QUEUED不保证最终发送;受理新STOPDT_ACT时,HIGH待发条目作废且不产生逐条回执。
* 直接应答不经过ASDU队列,也不调用Mark_Submitted;仍按HIGH计入未确认记录,使用相同的完成及ACK路径。
* ERROR表示输入/状态非法或HIGH总容量装不下,FATAL可能已编号,必须收尾;输出和输入不得重叠。
* 调用前Connection须先处理控制义务及期限;OK后应紧接一次有界的非阻塞写入尝试。 */
extern IEC104_SendManagerResult_t IEC104_SendManager_Submit_Response(
IEC104_SendManager_t *manager, uint64_t connection_generation,
const uint8_t *asdu, uint16_t asdu_length, uint64_t now_ms,
IEC104_SendBufferHeadInfo_t *head_info);
/* 不排队应答:仅在无当前帧、HIGH/NORMAL无待发条目且窗口/记录有容量时准备I帧。
* OK已固定N(S)并复制进SendBuffer;WAIT不接手,不缓存原文,不产生QUEUED。
* 已提交待ACK的NORMAL不阻挡本次尝试,仍受k窗口限制;其余校验同Submit_Response。 */
extern IEC104_SendManagerResult_t IEC104_SendManager_Try_Submit_Response(
IEC104_SendManager_t *manager, uint64_t connection_generation,
const uint8_t *asdu, uint16_t asdu_length, uint64_t now_ms,
IEC104_SendBufferHeadInfo_t *head_info);
/*
* 紧邻实际发送前调用;OK返回当前帧剩余字节,没有当前帧时才选择ASDU并分配N(S)。
* 输入:已Init的manager、匹配代次及当前单调时刻;输出head_info只在OK时有效。
* 当前I帧先续传,否则先发控制FIFO,再按优先级及配额选择未提交ASDU;最多一帧I待本地完成。
* 重复调用只返回同一当前帧,不重复编号;EAGAIN保持身份、内容、进度和原期限。
* EMPTY表示既无已准备帧,也无未提交ASDU;WAIT为门控/k窗口/记录容量阻塞。非OK不写head_info。
* Link构帧成功后的状态提交异常返回FATAL,须结束连接,不能撤回序号后重试。
* APDU提交及记录建立后,HIGH原文出队,NORMAL原文标记已提交;当前APDU与记录独立保留。
* 仅准备成功才推进调度配额;未提交ASDU被覆盖时没有对应帧回执。
* 跨字节环尾部的ASDU通过249字节局部缓冲拼接;连续ASDU直接借用,不malloc。
* now_ms为单调时刻。NORMAL没有未提交条目时,选择HIGH会清零配额计数。
* 先安排当前应生成的S/U,再调用本接口;不执行I/O,不因调用本接口即认定实际已发送。
*/
extern IEC104_SendManagerResult_t IEC104_SendManager_Prepare_Next_Send(
IEC104_SendManager_t *manager, uint64_t connection_generation, uint64_t now_ms,
IEC104_SendBufferHeadInfo_t *head_info);
/*
* S/U入队接口:同连接串行执行会话校验、控制槽预留、Link构帧和FIFO提交。
* 公共参数:manager须已Init且绑定对象持续有效;connection_generation须与当前连接一致;
* now_ms为单调毫秒读数,允许0;apci参数须来自Parse_APCI成功的完整报文。
* now_ms是本次入队的单调时刻,不是发送完成时刻;不执行I/O或创建定时器。
* QUEUED表示已提交一帧,后续统一用Handle_Send_Progress/Failure处理发送结果。
* Queue_*返回WAIT表示控制队列满/尚未到期/当前不能生成,无新报文;可稍后重试。
* 外部遗留的未结束预留属于接入状态错误,会在会话校验中返回ERROR。
* 未生成报文时取消本次预留;已使用的frame_serial不复用。
* ERROR表示参数、会话、预留或准备结果非法;构帧成功后提交异常、取消预留异常均为FATAL,须断连。
* FATAL前可能已经改变Link状态,不能假定整次调用未发生,也不能取消预留后重试原会话。
* Handle_*接收接口的WAIT表示本次请求尚未受理:调用方必须有界保留该请求,
* 在其被处理前不得越过它分派后续接收帧;不能保留时按过载断连,不得静默丢弃。
* START成功受理即结束此前停止的总计时;新STOP从收到该请求时起计总期限。
* apci不得与本模块存储重叠。NULL、错误帧类型/功能码在入口返回ERROR;已解析I/S报文被Link
* 按接收阶段、序号或ACK规则拒绝时返回PROTOCOL_ERROR,内部状态或交接异常返回ERROR/FATAL。
* 所有构帧接口先预留再调用Link,无容量时优先WAIT;EMPTY/无需报文等结果不是独立状态查询。
* 采用这些接口后,不再为同一操作手动Reserve、Link构帧或Commit_Frame。
*/
/**
* 功能:按w门限、立即确认或子站停止排空规则生成S确认并入队。
* 输入参数:公共参数见上;w范围1..32767;immediate非0跳过数量门限,但不跳过启停/在途S门控。
* 输出参数:无独立输出对象;Link冻结N(R),SendBuffer保存S帧,发送完成后才提交确认位置。
* 返回值:QUEUED表示S已入队;WAIT表示暂不能生成或无需确认;ERROR/FATAL按公共约定处理。
* 调用时机:I帧接收、启动CON发送完成或其他确认条件变化后,由连接调度代码调用。
*/
extern IEC104_SendManagerResult_t IEC104_SendManager_Queue_S_Ack(
IEC104_SendManager_t *manager, uint64_t connection_generation,
uint16_t w, int immediate, uint64_t now_ms);
/**
* 功能:t2到期后按公共S规则准备确认,仍受启停和已有S在途约束。
* 输入参数:公共参数见上;elapsed_ms为原待确认数据起点至今的毫秒数,t2_ms须非0。
* 输出参数:无独立输出对象;成功生成的S进入发送FIFO,不直接执行发送或读取时钟。
* 返回值:QUEUED表示S已入队;WAIT包括未到期、没有可发S或容量不足;ERROR/FATAL按公共约定。
* 说明:elapsed_ms等于t2_ms即到期;等待或重试不续期,接入方负责保存正确的原计时起点。
*/
extern IEC104_SendManagerResult_t IEC104_SendManager_Queue_T2_Ack(
IEC104_SendManager_t *manager, uint64_t connection_generation,
uint32_t elapsed_ms, uint32_t t2_ms, uint64_t now_ms);
/**
* 功能:子站主动生成并排队TESTFR_ACT,不要求当前已经STARTED。
* 输入参数:公共参数见上;本入口不检查空闲时长,按t3触发时使用Queue_T3_Testfr。
* 输出参数:无独立输出对象;Link进入ACT_SEND_PENDING,ACT进入SendBuffer。
* 返回值:QUEUED表示ACT已入队;WAIT表示控制容量不足或已有探测;ERROR/FATAL按公共约定。
* 说明:完整发送后经Handle_Send_Progress才进入CON_PENDING,以实际written_at_ms作为t1起点。
*/
extern IEC104_SendManagerResult_t IEC104_SendManager_Queue_Testfr_Act(
IEC104_SendManager_t *manager, uint64_t connection_generation, uint64_t now_ms);
/**
* 功能:空闲达到t3后准备TESTFR_ACT,复用同一主动探测状态。
* 输入参数:公共参数见上;idle_elapsed_ms为距有效链路活动的毫秒数,t3_ms须非0。
* 输出参数:无独立输出对象;成功时ACT入队,Link等待本地完整发送。
* 返回值:QUEUED表示ACT已入队;WAIT包括未到期、已有探测或容量不足;ERROR/FATAL按公共约定。
* 说明:等于t3_ms即满足空闲门限;不更新活动时间,不重置已有探测的t1,不创建系统定时器。
*/
extern IEC104_SendManagerResult_t IEC104_SendManager_Queue_T3_Testfr(
IEC104_SendManager_t *manager, uint64_t connection_generation,
uint32_t idle_elapsed_ms, uint32_t t3_ms, uint64_t now_ms);
/**
* 功能:子站接收TESTFR_ACT并排队TESTFR_CON,不结束本端正在进行的主动探测。
* 输入参数:公共参数见上;apci必须是TESTFR_ACT,now_ms为应答入队时刻。
* 输出参数:无独立输出对象;每次QUEUED均生成一帧CON,按同一FIFO完成发送。
* 返回值:QUEUED表示已安排应答;WAIT表示请求尚未受理;ERROR/FATAL按公共约定。
* 说明:WAIT须有界保留请求并保持接收顺序;TESTFR_CON没有Link完成接口,仍用Manager提交发送结果。
*/
extern IEC104_SendManagerResult_t IEC104_SendManager_Handle_Testfr_Act(
IEC104_SendManager_t *manager, uint64_t connection_generation,
const IEC104_ApciInfo_t *apci, uint64_t now_ms);
/* 按当前主动探测阶段受理TESTFR_CON;不需要发送槽,不生成应答。
* OK表示已结束CON_PENDING,Connection此时才撤销探测t1;提前/无匹配CON返回IGNORED且不缓存。
* ERROR表示参数或会话非法,FATAL表示Link处理异常;调用方不得再直接向Link重复提交。 */
extern IEC104_SendManagerResult_t IEC104_SendManager_Handle_Testfr_Con(
IEC104_SendManager_t *manager, uint64_t connection_generation, const IEC104_ApciInfo_t *apci);
/*
* TcpServer取得待发字节并经Net写socket后,由Connection提交本次成功写入增量bytes_written_now。
* OK表示未写完;FRAME_WRITTEN表示已提交适用的链路完成结果并释放缓冲槽,同时写completion。
* TESTFR_CON没有Link.Complete;其完整写入只提交缓冲完成,不推进链路过程。
* bytes_written_now=0不续期;最后一次now_ms为整帧实际发送时刻。错误不写completion。
* 链路Complete或SendBuffer.Commit_Send_Completion异常时返回FATAL。
* 同一写入增量不能重复提交,也不能再手动调用缓冲进度、链路Complete或缓冲完成接口。
* 该函数完成后再分派依赖本地完成的接收事件;不自动处理TESTFR_CON或创建t1定时器。
*/
extern IEC104_SendManagerResult_t IEC104_SendManager_Handle_Send_Progress(
IEC104_SendManager_t *manager, IEC104_SendFrameId_t frame_id,
uint16_t bytes_written_now, uint64_t now_ms, IEC104_LocalSendCompletion_t *completion);
/* 匹配队首发送失败/发送超时,分派对应Complete(-1),返回FATAL;错frame_id返回ERROR。 */
extern IEC104_SendManagerResult_t IEC104_SendManager_Handle_Send_Failure(
IEC104_SendManager_t *manager, IEC104_SendFrameId_t frame_id);
/*
* apci须已通过Parse_APCI;仅接受I/S,Link.Handle验证后登记ACK,HIGH核减未确认记录数,NORMAL清理原文或覆盖计数。
* 内部记录保留到转为事件后显式移除;ACK不代替本地Complete。Link拒绝报文返回PROTOCOL_ERROR;
* 接受后的同步或原文释放异常返回FATAL,参数、会话或记录预检非法返回ERROR。
* N(R)只能覆盖已由Handle_Send_Progress提交整帧完成的I帧;ACK越界时不推进确认和接收位置。
* 异步接入须先提交对应发送结果,再按接收顺序分派相关I/S帧,不能用ACK推定写入成功。
* 调用方不能先手动Handle再调用本接口,也不能直接用网络N(R)释放业务记录。
*/
extern IEC104_SendManagerResult_t IEC104_SendManager_Handle_I_S_Frame(
IEC104_SendManager_t *manager, uint64_t connection_generation, const IEC104_ApciInfo_t *apci);
/*
* 返回最早已本地完成、尚未ACK的I帧记录;OK复制记录,EMPTY/ERROR不改输出。
* 报文槽可能已复用,结果只包含帧身份、序号、时间及NORMAL来源编号,不提供完整APDU或user_tag。
* 只读当前有效会话,不消费回执;仅有排队/部分发送帧时返回EMPTY。
*/
extern IEC104_SendManagerResult_t IEC104_SendManager_Peek_Oldest_Sent_Unacked_Record(
const IEC104_SendManager_t *manager, IEC104_IFrameSendRecord_t *record);
/*
* 按最早已完成未确认I帧的原始written_at_ms检查t1;无定时器或断连副作用。
* 1到期、0无等待/未到期、-1非法;t1_ms非零,代次须匹配,时钟不得早于起点。
* ACK越过旧记录后选取下一条的原始时刻;重复ACK和后续发送均不续期。
*/
extern int IEC104_SendManager_T1_I_Ack_Expired(
const IEC104_SendManager_t *manager, uint64_t connection_generation,
uint64_t now_ms, uint32_t t1_ms);
/*
* 返回最早同时满足本地完成和对端ACK的业务记录;无记录EMPTY,尚未就绪WAIT。
* OK只复制记录,不释放;TcpServer成功复制到事件队列后,经Connection调用Remove_Confirmed_Record。
* 确认记录未被消费也占record_capacity,满时Prepare_Next_Send等待;S/U仍可继续发送。
*/
extern IEC104_SendManagerResult_t IEC104_SendManager_Peek_Confirmed_Record(
const IEC104_SendManager_t *manager, IEC104_IFrameSendRecord_t *record);
/* TcpServer已成功保存事件副本后,经Connection调用;核对队首frame_id及两个完成标志,返回OK或ERROR。 */
extern IEC104_SendManagerResult_t IEC104_SendManager_Remove_Confirmed_Record(
IEC104_SendManager_t *manager, IEC104_SendFrameId_t frame_id);
/*
* 按交接顺序复制第offset条业务记录,供故障回收;不依赖故障后的链路状态。
* 0<=offset<record_count时返回OK;越过已有记录返回EMPTY;非法参数返回ERROR。
* 输出frame_id由Manager的connection_generation和存储记录的frame_serial组合,返回独立数值副本。
* 保留peer_acknowledged事实:已确认事件无需重发;未确认不等于对端未收到。
*/
extern IEC104_SendManagerResult_t IEC104_SendManager_Get_Record_By_Offset(
const IEC104_SendManager_t *manager, uint16_t offset, IEC104_IFrameSendRecord_t *record);
/* 先结束旧I/O并读取回执,再终止SendBuffer和本对象;Queue.Rebind清空HIGH并恢复NORMAL,Queue.Abort全部丢弃。 */
extern void IEC104_SendManager_Abort(IEC104_SendManager_t *manager);
/* ==================== 子站:发送CON ==================== */
/*
* START先取得控制槽,再立即激活链路并排队CON;停止等待可被结束,已排队CON不撤回。
* QUEUED表示已受理,WAIT表示容量不足且未受理,非法为ERROR/FATAL。
* 从停用状态重新激活时清理HIGH;已激活时重复START保留HIGH和公平配额。
*/
extern IEC104_SendManagerResult_t IEC104_SendManager_Slave_Handle_Startdt_Act(
IEC104_SendManager_t *manager, uint64_t connection_generation,
const IEC104_ApciInfo_t *apci, uint64_t now_ms);
/*
* STOP立即停用并清理待发HIGH,无需控制槽;当前APDU、记录及NORMAL仍按原规则处理。
* OK为新停止义务,IGNORED为排空中重复STOP;ERROR/FATAL为非法或故障。
* 停止后不接手新HIGH;后到START不恢复已经清理的HIGH。
*/
extern IEC104_SendManagerResult_t IEC104_SendManager_Slave_Handle_Stopdt_Act(
IEC104_SendManager_t *manager, uint64_t connection_generation,
const IEC104_ApciInfo_t *apci);
/*
* 推进停止排空并排队S/CON。QUEUED为已入队,EMPTY为无义务,WAIT等容量/确认。
* 已排队S覆盖全部接收数据时,CON可紧随其后;生成CON即结束Link的排空状态。
* stop_con_id可为NULL;非NULL时仅在STOP_CON成功入队后写入其身份,其他结果不改输出。
* 调用方用帧身份结束对应STOP期限,不能由任意旧CON完成清掉新停止的计时。
*/
extern IEC104_SendManagerResult_t IEC104_SendManager_Slave_Queue_Stopdt_Response(
IEC104_SendManager_t *manager, uint64_t connection_generation, uint64_t now_ms,
IEC104_SendFrameId_t *stop_con_id);
/* ==================== 子站初始化 ==================== */
/*
* 绑定子站新连接:链路已按CONTROLLED角色State_Init,SendBuffer为空,AsduQueue同代次,且没有保留或已覆盖的待确认条目。
* AsduQueue允许有未提交业务;成功绑定时清空HIGH,保留NORMAL。records为调用方分配的Storage数组,容量1..32767;send_window_k范围1..32767,数组和参数对象不得重叠。
* 数组中的帧身份只保存frame_serial,连接代次统一取Manager;查询仍输出带完整frame_id的IEC104_IFrameSendRecord_t。
* 所有对象持续有效且同连接串行操作;无锁、无动态分配、无业务回调。
* 模块自身合法性和初始条件通过公开查询检查;额外核对Link的固定角色必须为CONTROLLED。
* 绑定后I帧由Prepare_Next_Send或Submit_Response统一准备,I/S接收经Handle_I_S_Frame,所有发送进度经Handle_Send_Progress。
* S/TESTFR及子站启停使用本模块的Queue/Handle封装,完成时按原帧类型直接提交Link。
* 不拥有socket,不调度U帧接收和系统定时器;接收成帧由Receiver提供,调用方负责按序分派。
* ERROR时不修改manager;成功后不得改写Link角色或绑定对象,Abort后新连接重新初始化。
*/
extern IEC104_SendManagerResult_t IEC104_SendManager_Init(
IEC104_SendManager_t *manager, IEC104_AsduQueue_t *asdu_queue,
IEC104_SendBuffer_t *send_buffer, IEC104_LinkState_t *link_state,
IEC104_IFrameSendRecordStorage_t *records, uint16_t record_capacity,
uint16_t send_window_k, uint16_t high_priority_burst_limit);
/* Connection构造专用:先在局部对象中校验新会话,再一次提交初始CONTROLLED Link、空Buffer和Manager。
* 成功时Manager直接绑定参数给出的最终Link/Buffer地址,清空HIGH、保留NORMAL;ERROR时所有输出及队列不变。
* 数组、队列和输出对象不得重叠;仅用于首次构造或旧I/O已结束并Abort后的新连接,不能迁移运行中的会话。
* 最后一次可能失败的操作是队列HIGH清理,其后仅提交已校验对象,没有需要回滚的失败步骤。 */
extern IEC104_SendManagerResult_t IEC104_SendManager_Init_Connection(
IEC104_SendManager_t *manager, IEC104_AsduQueue_t *asdu_queue,
IEC104_SendBuffer_t *send_buffer, IEC104_LinkState_t *link_state,
IEC104_SendBufferControlFrame_t *control_frames, uint16_t control_capacity,
IEC104_IFrameSendRecordStorage_t *records, uint16_t record_capacity,
uint16_t send_window_k, uint16_t high_priority_burst_limit, uint64_t connection_generation);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_SENDMANAGER_H */
@@ -0,0 +1,802 @@
/*
* 104子站业务入口:拥有一个TcpServer,并把底层I帧通知转换为ASDU视图和应答接口。
* 职责分工:Slave管理业务入口和事件绑定;TcpServer管理socket、调度、队列及协议会话。
* 本层只保存不透明的TcpServer指针,通过公开函数操作它,不访问其成员或复制底层对象。
*
* 生命周期:Create -> Config_Default/Init -> Poll或Prepare/Process -> Close -> 读空事件 -> Destroy。
* 接收路径:TcpServer_NextEvent -> 复制原文及来源身份 -> Bind_Event -> 应用处理。
* 应答路径:IEC60870_MasterConnection_Send_* -> 本文件的发送适配 -> TcpServer_Send_Response。
* 主动上报:Slave_Enqueue_Event -> 服务级NORMAL队列,不借用某个请求的连接身份。
* 本层不自动执行业务命令或发送确认;ASDU检查及业务许可仍由应用负责。
*
* 两种身份分别解决不同的复用问题:serial标识本次Slave_Create,generation标识原TCP会话。
* 事件保存两者,防止延迟应答误发给重连的主站或静态槽复用后的另一服务。
* 共用站点表只管理CA路由和节点寿命;总召等业务按注册编号保留原站,点表仍由应用管理。
* 节点的引用寿命与TCP会话寿命分开,持有节点并不表示原连接仍可应答。
* 所有调用在所属事件循环线程串行执行;静态池没有锁,Create/Destroy也须跨服务串行。
*/
#include "IEC104_Slave.h"
#include "IEC104_TcpServer.h"
#include "IEC104_TcpServer_Config.h"
#include <string.h>
/* 104专用默认布局来自IEC104_Types.h;共用ASDU模块通过参数工作,不硬编码104布局。
* 静态只读存储供接收视图和主动上报共用,返回的参数指针不依赖某个服务或事件的寿命。 */
static const IEC60870_AppLayerParameters_t parameters104 = IEC104_APP_LAYER_PARAMETERS_DEFAULT;
/* 完整类型仅在本文件可见;业务只保存Create返回的指针。 */
struct IEC104_Slave {
IEC104_TcpServer_t *server; /* 私有TCP服务;NULL表示该Slave槽空闲,Close不会清除此成员。 */
uint64_t serial; /* 每次成功Create递增,Destroy保留;到UINT64_MAX后不再复用该槽。 */
IEC104_Station_t *stations; /* 调用方节点按注册顺序排列,含待回收的退役节点;不另分配站点池。 */
uint32_t station_locks; /* 固定站点集合的嵌套借用计数,不是线程锁。 */
uint32_t station_snapshots; /* 活动遍历期间不回收退役节点,保护所有游标。 */
uint64_t station_serial; /* 注册编号不随Destroy清零,避免同CA替换/服务槽复用混淆。 */
};
_Static_assert(IEC104_TCP_SERVER_CAPACITY > 0U, "Slave needs at least one service slot");
/* Slave和TcpServer各有对象池;每个Slave占用一个底层名额,直接创建TCP服务也占该名额。 */
static IEC104_Slave_t slave_pool[IEC104_TCP_SERVER_CAPACITY];
static bool Send_ASDU(IEC60870_MasterConnection_t *connection, IEC60870_Asdu_t *asdu);
/**
* 功能:判断句柄是否指向当前占用的Slave静态槽,NULL和池外地址返回false。
* 说明:先按地址查池再解引用;server非空只表示拥有底层对象,不代表已Init或已连接。
* 槽复用后,单独的旧句柄副本无法与新句柄区分;Destroy成功后业务必须停止使用旧句柄。
*/
static bool Slave_Owned(const IEC104_Slave_t *self)
{
size_t index;
for (index = 0U; index < IEC104_TCP_SERVER_CAPACITY; ++index) {
if (self == &slave_pool[index]) { return self->server != NULL; }
}
return false;
}
/* 先在本服务的链表中按地址查找,再读取目标节点;外来节点不借用其_owner字段找服务。
* 同时接受尚未回收的退役节点,以便原业务继续Acquire/Release;这里不判断CA路由资格。 */
static IEC104_Station_t *find_owned_station(const IEC104_Slave_t *self,
const IEC104_Station_t *station)
{
IEC104_Station_t *current;
if (!Slave_Owned(self) || station == NULL) { return NULL; }
for (current = self->stations; current != NULL; current = current->_next) {
if (current == station) { return current; }
}
return NULL;
}
/* 活动快照可能停在任意节点,因此全部快照End前都不摘除退役节点。
* 无快照且已删除、引用归零时才摘链并清零;不释放调用方的节点或context存储。
* Remove/Release/快照End均尝试回收,调用方据Is_Reusable决定何时释放自己的存储。 */
static void collect_retired_stations(IEC104_Slave_t *self)
{
IEC104_Station_t **entry = &self->stations;
if (self->station_snapshots != 0U) { return; }
while (*entry != NULL) {
IEC104_Station_t *station = *entry;
if (!station->_registered && station->_users == 0U) {
*entry = station->_next;
memset(station, 0, sizeof(*station));
} else { entry = &station->_next; }
}
}
/* 借用调用方节点,成功前不改链表或节点;不创建通信资源,不分配点表。 */
IEC104_StationResult_t IEC104_Slave_Add_Station(IEC104_Slave_t *self,
IEC104_Station_t *station, uint16_t common_address, void *context)
{
IEC104_Station_t **tail;
if (!Slave_Owned(self) || station == NULL || common_address == 0U ||
common_address == IEC104_CA_BROADCAST) { return IEC104_STATION_INVALID_ARGUMENT; }
if (station->_owner != NULL) { return IEC104_STATION_ALREADY_REGISTERED; }
if (self->station_locks != 0U) { return IEC104_STATION_BUSY; }
if (self->station_serial == UINT64_MAX) { return IEC104_STATION_LIMIT; }
for (tail = &self->stations; *tail != NULL; tail = &(*tail)->_next) {
if ((*tail)->_registered && (*tail)->_common_address == common_address) {
return IEC104_STATION_DUPLICATE_CA;
}
}
station->_next = NULL;
station->_owner = self;
station->_context = context;
station->_users = 0U;
station->_common_address = common_address;
station->_id = ++self->station_serial;
station->_registered = true;
*tail = station;
return IEC104_STATION_OK;
}
/* 删除先退出路由,引用和快照决定实际回收时机;OK不代表节点已可释放。
* 保留中的旧节点不会挡住相同CA的新注册,原业务仍通过注册编号找到旧节点。 */
IEC104_StationResult_t IEC104_Slave_Remove_Station(IEC104_Slave_t *self, IEC104_Station_t *station)
{
if (!Slave_Owned(self) || station == NULL) { return IEC104_STATION_INVALID_ARGUMENT; }
if (find_owned_station(self, station) == NULL || !station->_registered) { return IEC104_STATION_NOT_FOUND; }
if (self->station_locks != 0U) { return IEC104_STATION_BUSY; }
station->_registered = false;
collect_retired_stations(self);
return IEC104_STATION_OK;
}
/* 新请求只匹配仍注册的节点;返回借用指针,不自动增加引用或决定广播范围。 */
IEC104_Station_t *IEC104_Slave_Get_Station(const IEC104_Slave_t *self, uint16_t common_address)
{
IEC104_Station_t *station;
if (!Slave_Owned(self) || common_address == 0U || common_address == IEC104_CA_BROADCAST) { return NULL; }
for (station = self->stations; station != NULL; station = station->_next) {
if (station->_registered && station->_common_address == common_address) { return station; }
}
return NULL;
}
/* 不另存可失配的数量;上限仅由可注册CA和调用方存储决定。 */
size_t IEC104_Slave_Get_Station_Count(const IEC104_Slave_t *self)
{
const IEC104_Station_t *station;
size_t count = 0U;
if (!Slave_Owned(self)) { return 0U; }
for (station = self->stations; station != NULL; station = station->_next) {
if (station->_registered) { ++count; }
}
return count;
}
/* 普通遍历读取当前注册集合;跨循环固定集合须Lock,允许增删的遍历须用Snapshot。 */
IEC104_Station_t *IEC104_Slave_Next_Station(const IEC104_Slave_t *self, const IEC104_Station_t *current)
{
IEC104_Station_t *station;
if (!Slave_Owned(self)) { return NULL; }
station = current == NULL ? self->stations : find_owned_station(self, current);
if (current != NULL && station != NULL) { station = station->_next; }
while (station != NULL && !station->_registered) { station = station->_next; }
return station;
}
/* 固定遍历集合,不锁点值;串行事件循环中的嵌套借用必须显式配对。 */
IEC104_StationResult_t IEC104_Slave_Lock_Stations(IEC104_Slave_t *self)
{
if (!Slave_Owned(self)) { return IEC104_STATION_INVALID_ARGUMENT; }
if (self->station_locks == UINT32_MAX) { return IEC104_STATION_LIMIT; }
++self->station_locks;
return IEC104_STATION_OK;
}
IEC104_StationResult_t IEC104_Slave_Unlock_Stations(IEC104_Slave_t *self)
{
if (!Slave_Owned(self) || self->station_locks == 0U) { return IEC104_STATION_INVALID_ARGUMENT; }
--self->station_locks;
return IEC104_STATION_OK;
}
/* 持有具体节点,不重新按CA路由;尚被保留的退役节点也能取得引用,供原业务继续处理。 */
IEC104_StationResult_t IEC104_Slave_Acquire_Station(IEC104_Slave_t *self, IEC104_Station_t *station)
{
if (!Slave_Owned(self) || station == NULL) { return IEC104_STATION_INVALID_ARGUMENT; }
if (find_owned_station(self, station) == NULL) { return IEC104_STATION_NOT_FOUND; }
if (station->_users == UINT32_MAX) { return IEC104_STATION_LIMIT; }
++station->_users;
return IEC104_STATION_OK;
}
/* 归还自己的引用后可能立即摘链清零;没有其他寿命保护时,不再使用该节点或context。 */
IEC104_StationResult_t IEC104_Slave_Release_Station(IEC104_Slave_t *self, IEC104_Station_t *station)
{
if (!Slave_Owned(self) || station == NULL) { return IEC104_STATION_INVALID_ARGUMENT; }
if (find_owned_station(self, station) == NULL) { return IEC104_STATION_NOT_FOUND; }
if (station->_users == 0U) { return IEC104_STATION_INVALID_ARGUMENT; }
--station->_users;
collect_retired_stations(self);
return IEC104_STATION_OK;
}
/* 延迟事件按原注册编号找回节点,包括被引用/快照保留的退役节点,避免同CA替换串站。
* 与按CA查找一样只返回借用指针;需要独立持有时仍须Acquire。 */
IEC104_Station_t *IEC104_Slave_Get_Station_By_Id(const IEC104_Slave_t *self, uint64_t station_id)
{
IEC104_Station_t *station;
if (!Slave_Owned(self) || station_id == 0U) { return NULL; }
for (station = self->stations; station != NULL; station = station->_next) {
if (station->_id == station_id) { return station; }
}
return NULL;
}
uint64_t IEC104_Station_Get_Id(const IEC104_Station_t *station)
{
return station != NULL && station->_owner != NULL ? station->_id : 0U;
}
bool IEC104_Station_Is_Registered(const IEC104_Station_t *station)
{
return station != NULL && station->_owner != NULL && station->_registered;
}
bool IEC104_Station_Is_Reusable(const IEC104_Station_t *station)
{
return station != NULL && station->_owner == NULL;
}
/* 只保存注册编号上界和遍历位置,不冻结IOA或点值,也不阻止站点增删。
* 注册编号单调递增,后来加入的同CA替代节点也会落在本次上界之外。 */
IEC104_StationResult_t IEC104_Slave_StationSnapshot_Begin(IEC104_Slave_t *self,
IEC104_StationSnapshot_t *snapshot)
{
if (!Slave_Owned(self) || snapshot == NULL || snapshot->_owner != NULL) {
return IEC104_STATION_INVALID_ARGUMENT;
}
if (self->station_snapshots == UINT32_MAX) { return IEC104_STATION_LIMIT; }
snapshot->_owner = self;
snapshot->_next = self->stations;
snapshot->_last_id = self->station_serial;
snapshot->_self = snapshot;
++self->station_snapshots;
return IEC104_STATION_OK;
}
/* 先推进游标再返回当前仍注册的节点;删除的未来节点跳过,新增节点不纳入。
* 快照保护节点存储但不自动Acquire;返回NULL只表示遍历完毕,仍须End归还保护。 */
IEC104_Station_t *IEC104_Slave_StationSnapshot_Next(IEC104_StationSnapshot_t *snapshot)
{
if (snapshot == NULL || snapshot->_self != snapshot || !Slave_Owned(snapshot->_owner)) { return NULL; }
while (snapshot->_next != NULL) {
IEC104_Station_t *station = snapshot->_next;
snapshot->_next = station->_next;
if (station->_id > snapshot->_last_id) { snapshot->_next = NULL; return NULL; }
if (station->_registered) { return station; }
}
return NULL;
}
/* 与Begin配对一次;最后一个快照结束会尝试回收所有无引用的退役节点。
* 业务若要跨End继续使用Next返回的节点,必须提前Acquire。 */
IEC104_StationResult_t IEC104_Slave_StationSnapshot_End(IEC104_StationSnapshot_t *snapshot)
{
IEC104_Slave_t *self;
if (snapshot == NULL || snapshot->_self != snapshot || !Slave_Owned(snapshot->_owner)) {
return IEC104_STATION_INVALID_ARGUMENT;
}
self = snapshot->_owner;
--self->station_snapshots;
memset(snapshot, 0, sizeof(*snapshot));
collect_retired_stations(self);
return IEC104_STATION_OK;
}
uint16_t IEC104_Station_Get_Common_Address(const IEC104_Station_t *station)
{
return station != NULL && station->_owner != NULL ? station->_common_address : 0U;
}
void *IEC104_Station_Get_Context(const IEC104_Station_t *station)
{
return station != NULL && station->_owner != NULL ? station->_context : NULL;
}
/**
* 功能:返回本层校验失败的结果,同时完整填写可选的error。
* 输入参数:result——本层检测出的错误;error——调用方诊断存储,允许NULL。
* 说明:阶段统一为VALIDATE,socket_error为0;底层操作的诊断直接透传,不在这里改写。
*/
static IEC104_TcpResult_t Slave_Error(IEC104_TcpResult_t result, IEC104_TcpError_t *error)
{
if (error != NULL) {
error->result = result;
error->stage = IEC104_TCP_STAGE_VALIDATE;
error->socket_error = 0;
}
return result;
}
/**
* 功能:从共用连接接口取回它所属的Slave事件;接口不匹配时返回NULL。
* 说明:同时核对发送函数和事件内接口的地址,拒绝赋值/memcpy产生的未修复副本。
* 此处只检查接口绑定,服务所有权、serial和连接代次由发送路径继续检查。
* connection及其object必须仍指向有效存储,不能用此检查代替业务的对象寿命管理。
*/
static IEC104_SlaveEvent_t *Get_Context(const IEC60870_MasterConnection_t *connection)
{
IEC104_SlaveEvent_t *event;
if (connection == NULL || connection->send_asdu != Send_ASDU || connection->object == NULL) {
return NULL;
}
event = (IEC104_SlaveEvent_t *)connection->object;
return &event->connection == connection ? event : NULL;
}
/**
* 功能:确认待发送ASDU使用可由本接入层处理的104应用层布局。
* 返回值:基础参数合法、COT/CA/IOA为2/2/3字节且ASDU上限不超过249时为true。
* 说明:允许业务采用更小的最大长度或不同默认OA;这里不检查具体信息对象和业务权限。
*/
static bool Parameters_Compatible(const IEC60870_AppLayerParameters_t *parameters)
{
return IEC60870_AppLayerParameters_Valid(parameters) &&
parameters->sizeOfCOT == 2 && parameters->sizeOfCA == 2 && parameters->sizeOfIOA == 3 &&
parameters->maxSizeOfASDU <= IEC104_MAX_ASDU_LEN;
}
/* 完整事件和紧凑身份共用发送校验,不把旧身份替换成当前连接。
* self只在确认仍被静态池占用后解引用;队列策略、发送期限和FATAL语义保持由TcpServer处理。 */
static IEC104_TcpSubmitResult_t Submit_Response(IEC104_Slave_t *self,
const IEC104_SlaveEventIdentity_t *identity, const IEC60870_Asdu_t *asdu, int bypass_queue)
{
const uint8_t *data;
uint16_t length;
if (!Slave_Owned(self) || identity == NULL || identity->slave != self ||
identity->instance_id != self->serial || identity->generation == 0U ||
asdu == NULL || !Parameters_Compatible(asdu->parameters) ||
IEC60870_Asdu_Get_Data(asdu, &data, &length) != IEC60870_ASDU_OK) {
return IEC104_TCP_SUBMIT_ERROR;
}
return bypass_queue ? IEC104_TcpServer_Try_Send_Response(self->server, identity->generation, data, length) :
IEC104_TcpServer_Send_Response(self->server, identity->generation, data, length);
}
IEC104_TcpSubmitResult_t IEC104_Slave_Send_Response(IEC104_Slave_t *self,
const IEC104_SlaveEventIdentity_t *identity, const IEC60870_Asdu_t *asdu)
{
return Submit_Response(self, identity, asdu, 0);
}
IEC104_TcpSubmitResult_t IEC104_Slave_Try_Send_Response(IEC104_Slave_t *self,
const IEC104_SlaveEventIdentity_t *identity, const IEC60870_Asdu_t *asdu)
{
return Submit_Response(self, identity, asdu, 1);
}
IEC104_TcpSubmitResult_t IEC104_Slave_Reject_Request(IEC104_Slave_t *self,
const IEC104_SlaveEvent_t *event, int cause)
{
uint8_t bytes[IEC104_MAX_ASDU_LEN];
const uint8_t *data;
uint16_t length, reply_length;
IEC60870_Asdu_t reply;
IEC104_SlaveEventIdentity_t identity;
IEC104_TcpServerStatus_t status;
if (cause < IEC60870_ASDU_COT_UNKNOWN_TYPE || cause > IEC60870_ASDU_COT_UNKNOWN_IOA ||
!IEC104_Slave_Get_Event_Identity(self, event, &identity) ||
event->transport.type != IEC104_TCP_EVENT_I_FRAME || event->asdu_result != IEC60870_ASDU_OK ||
IEC104_TcpServer_Get_Status(self->server, &status, NULL) != IEC104_TCP_OK) {
return IEC104_TCP_SUBMIT_ERROR;
}
/* 本次事件之后尚未消费的启停通知表明链路已停用或曾停用,不能跨启停提交应答。 */
if (status.pending_state_event_count != 0U ||
IEC60870_Asdu_Get_Data(&event->asdu, &data, &length) != IEC60870_ASDU_OK ||
IEC60870_Asdu_Build_Response(&parameters104, data, length, cause, 1,
bytes, sizeof(bytes), &reply_length) != IEC60870_ASDU_OK ||
IEC60870_Asdu_Create_From_Buffer(&reply, &parameters104, bytes, reply_length) != IEC60870_ASDU_OK) {
return IEC104_TCP_SUBMIT_ERROR;
}
return Submit_Response(self, &identity, &reply, 0);
}
/**
* 功能:把普通/不排队应答交给产生该事件的原服务、原会话。
* 输入参数:connection——事件内已绑定的接口;asdu——布局兼容的完整ASDU,可独立于接收视图。
* 返回值:透传OK/WAIT/ERROR/FATAL并记录在原事件中;普通send_asdu包装为bool。
* 说明:先检查Slave所有权和serial,再由TcpServer检查generation、启停状态及发送容量。
* OK表示已复制接手,不代表写完或收到ACK;WAIT未接手,业务保存原事件/ASDU后择机重试。
* 不查找当前新连接来替换旧身份;FATAL可能已经写出字节,不能作为未发送直接重放。
*/
static IEC104_TcpSubmitResult_t Submit_ASDU(IEC60870_MasterConnection_t *connection,
const IEC60870_Asdu_t *asdu, int bypass_queue)
{
IEC104_SlaveEvent_t *event = Get_Context(connection);
IEC104_SlaveEventIdentity_t identity;
if (event == NULL) { return IEC104_TCP_SUBMIT_ERROR; }
identity.slave = event->_owner;
identity.instance_id = event->_owner_serial;
identity.generation = event->_generation;
event->_last_submit_result = Submit_Response(event->_owner, &identity, asdu, bypass_queue);
return event->_last_submit_result;
}
static bool Send_ASDU(IEC60870_MasterConnection_t *connection, IEC60870_Asdu_t *asdu)
{
return Submit_ASDU(connection, asdu, 0) == IEC104_TCP_SUBMIT_OK;
}
/* 104专用不排队入口;不改变共用连接接口的函数指针或普通应答排队约定。 */
IEC104_TcpSubmitResult_t IEC104_Slave_Try_Send_ASDU(
IEC60870_MasterConnection_t *connection, const IEC60870_Asdu_t *asdu)
{
return Submit_ASDU(connection, asdu, 1);
}
/**
* 功能:修改ASDU的传送原因和否定标志,再沿原连接提交确认或终止。
* 输入参数:cause——目标COT;negative——目标P/N;asdu——允许原地修改的ASDU。
* 返回值:true已接手;false表示字段修改或提交失败,详细提交结果保存在事件中。
* 说明:按COT、P/N顺序修改,后续失败不回滚已经成功的修改;不执行业务,也不自动重试。
*/
static bool Send_Confirmation(IEC60870_MasterConnection_t *connection,
IEC60870_Asdu_t *asdu, int cause, bool negative)
{
IEC104_SlaveEvent_t *event = Get_Context(connection);
if (event == NULL) { return false; }
event->_last_submit_result = IEC104_TCP_SUBMIT_ERROR;
if (IEC60870_Asdu_Set_COT(asdu, cause) != IEC60870_ASDU_OK ||
IEC60870_Asdu_Set_Negative(asdu, negative ? 1 : 0) != IEC60870_ASDU_OK) {
return false;
}
return Send_ASDU(connection, asdu);
}
/* 共用激活确认入口:COT=7,P/N由业务指定;沿用Send_Confirmation的修改及提交契约。 */
static bool Send_ACT_CON(IEC60870_MasterConnection_t *connection, IEC60870_Asdu_t *asdu, bool negative)
{
return Send_Confirmation(connection, asdu, IEC60870_ASDU_COT_ACTIVATION_CON, negative);
}
/* 共用激活终止入口:COT=10、P/N=0;由业务处理完成后调用,不自动跟随ACT_CON发送。 */
static bool Send_ACT_TERM(IEC60870_MasterConnection_t *connection, IEC60870_Asdu_t *asdu)
{
return Send_Confirmation(connection, asdu, IEC60870_ASDU_COT_ACTIVATION_TERMINATION, false);
}
/* 共用参数查询入口:有效事件接口返回静态只读参数;不要求原连接仍在线,也不表示可以发送。 */
static const IEC60870_AppLayerParameters_t *Get_Parameters(IEC60870_MasterConnection_t *connection)
{
return Get_Context(connection) != NULL ? &parameters104 : NULL;
}
/**
* 功能:让目标事件的ASDU视图和共用发送接口指向目标自身的存储。
* 前提:transport原文已复制完毕,事件地址在后续使用期间保持稳定。
* 输出参数:event->asdu/connection/asdu_result;不改来源身份、原文和历史提交结果。
* 说明:非I帧不提供ASDU接口;I帧视图构造失败时保留原通知供诊断,connection保持清零。
* 视图借用本事件中APCI之后的ASDU字节,不再借用TcpServer或Receiver的内部缓冲。
*/
static void Bind_Event(IEC104_SlaveEvent_t *event)
{
uint16_t length;
memset(&event->asdu, 0, sizeof(event->asdu));
memset(&event->connection, 0, sizeof(event->connection));
event->asdu_result = IEC60870_ASDU_UNSUPPORTED;
if (event->transport.type != IEC104_TCP_EVENT_I_FRAME) { return; }
event->asdu_result = IEC60870_ASDU_ERROR;
length = event->transport.data.i_frame.apdu_length;
if (length < IEC104_APCI_LEN || length > IEC104_MAX_APDU_LEN) { return; }
event->asdu_result = IEC60870_Asdu_Create_From_Buffer(&event->asdu, &parameters104,
event->transport.data.i_frame.apdu + IEC104_APCI_LEN, (uint16_t)(length - IEC104_APCI_LEN));
if (event->asdu_result != IEC60870_ASDU_OK) { return; }
event->connection.object = event;
event->connection.send_asdu = Send_ASDU;
event->connection.send_act_con = Send_ACT_CON;
event->connection.send_act_term = Send_ACT_TERM;
event->connection.get_application_layer_parameters = Get_Parameters;
}
/**
* 功能:从静态池取得一个Slave,并为它取得一个私有TcpServer。
* 返回值:成功返回不透明句柄;任一池无可用槽或身份编号耗尽时返回NULL。
* 说明:不创建socket、不分配堆内存;只有取得底层服务后才占用Slave槽并递增serial。
* 成功后由业务配对Destroy;不能自行分配、复制、清零或free句柄指向的对象。
*/
IEC104_Slave_t *IEC104_Slave_Create(void)
{
size_t index;
for (index = 0U; index < IEC104_TCP_SERVER_CAPACITY; ++index) {
IEC104_Slave_t *self = &slave_pool[index];
if (self->server == NULL && self->serial != UINT64_MAX) {
IEC104_TcpServer_t *server = IEC104_TcpServer_Create();
if (server == NULL) { return NULL; }
self->server = server;
++self->serial;
return self;
}
}
return NULL;
}
/**
* 功能:归还已关闭且通知已读空的Slave及其底层服务。
* 输入参数:self——Create取得且未Destroy的句柄;error——可选的本次调用诊断。
* 返回值:OK时句柄失效;运行中为INVALID_STATE,通知未读空为EVENTS_PENDING,失败保留对象。
* 说明:先由TcpServer检查并归还底层槽,再释放Slave槽;保留serial隔离旧事件。
* 常规顺序为Close -> NextEvent读空 -> Destroy;尚未Init或Init失败后也可直接归还。
*/
IEC104_TcpResult_t IEC104_Slave_Destroy(IEC104_Slave_t *self, IEC104_TcpError_t *error)
{
IEC104_TcpResult_t result;
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
if (self->stations != NULL || self->station_locks != 0U || self->station_snapshots != 0U) {
return Slave_Error(IEC104_TCP_INVALID_STATE, error);
}
result = IEC104_TcpServer_Destroy(self->server, error);
if (result == IEC104_TCP_OK) { self->server = NULL; }
return result;
}
/**
* 功能:填写默认监听地址0.0.0.0、端口2404及协议参数。
* 输出参数:config——调用方可继续调整的配置;NULL时无操作。
* 说明:复用TcpServer默认值,不占用服务槽;ASDU布局另由parameters104提供。
*/
void IEC104_Slave_Config_Default(IEC104_SlaveConfig_t *config)
{
IEC104_TcpServer_Config_Default(config);
}
/**
* 功能:初始化所拥有的TCP服务,开始监听并建立服务级发送队列。
* 输入参数:self——有效Slave句柄;config——本次监听和协议配置,底层复制后使用。
* 输出参数:error——本次诊断,允许NULL;底层返回的阶段和网络错误保持原样。
* 返回值:OK已监听,其他结果区分参数、配置、生命周期或网络失败。
* 说明:成功不表示已有主站,之后必须驱动Poll或Prepare/Process;失败不销毁句柄。
* 再次Init前须Close并读空旧通知,旧事件的连接身份不会因重新初始化而更新。
*/
IEC104_TcpResult_t IEC104_Slave_Init(IEC104_Slave_t *self,
const IEC104_SlaveConfig_t *config, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
return IEC104_TcpServer_Init(self->server, config, error);
}
/**
* 功能:为外部事件循环查询socket关注集合、等待期限及本地待处理工作。
* 输入参数:self——已Init的服务;info——非NULL快照存储;error——可选诊断。
* 返回值:OK时写出info;失败保持info不变,不进行收发或协议推进。
* 说明:有待消费事件或need_process时不得阻塞等待;状态改变后须重新Prepare。
* 快照中的socket仅供注册,所有权仍属于内部TcpServer,业务不能直接读写或关闭。
*/
IEC104_TcpResult_t IEC104_Slave_Prepare(const IEC104_Slave_t *self,
IEC104_TcpWaitInfo_t *info, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
return IEC104_TcpServer_Prepare(self->server, info, error);
}
/**
* 功能:把外部事件循环的一批就绪、定时或本地通知交给底层有界推进。
* 输入参数:self——已Init的服务;ready——必填就绪批次,身份来自对应Prepare/注册快照。
* 输出参数:error——可选诊断;返回值与TcpServer_Process一致,OK也包括没有进展。
* 说明:不等待;返回后消费NextEvent,再Prepare更新关注集合。旧通知不得替换为新token。
* 客户端故障通过收尾事件交付,服务级失败则由调用方保存诊断后Close。
*/
IEC104_TcpResult_t IEC104_Slave_Process(IEC104_Slave_t *self,
const IEC104_TcpReady_t *ready, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
return IEC104_TcpServer_Process(self->server, ready, error);
}
/**
* 功能:独立主循环使用的准备、至多一次网络等待和有界推进组合入口。
* 输入参数:self——已Init的服务;max_wait_ms——本轮网络等待上限,0不等待;error可为NULL。
* 返回值:OK表示服务可继续;客户端断线不一定使本函数失败,须读取NextEvent。
* 说明:等待还受协议期限和本地工作约束;max_wait_ms不保证整个函数的执行耗时。
* 不创建后台线程、不调用业务处理函数;每轮返回后由应用消费通知。
*/
IEC104_TcpResult_t IEC104_Slave_Poll(IEC104_Slave_t *self,
uint32_t max_wait_ms, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
return IEC104_TcpServer_Poll(self->server, max_wait_ms, error);
}
/**
* 功能:取出最早的TCP通知,并为有效I帧建立ASDU视图和原连接的发送接口。
* 输入参数:self——有效句柄,Close后仍可读取;event——地址稳定的非NULL输出存储。
* 返回值:READY替换整个event;EMPTY/ERROR保持上次事件及其视图不变。
* 说明:先读入临时量,只有取得通知后才覆盖输出;保存原服务serial和连接generation。
* READY仅表示取得事件,I帧还须检查asdu_result;视图失败仍保留原文,不自动确认。
* 不进行I/O;下一次READY会覆盖同一event,延迟处理须另存并调用Copy_Event修复自引用。
*/
IEC104_TcpEventResult_t IEC104_Slave_NextEvent(IEC104_Slave_t *self, IEC104_SlaveEvent_t *event)
{
IEC104_TcpEvent_t received;
IEC104_TcpEventResult_t result;
if (!Slave_Owned(self) || event == NULL) { return IEC104_TCP_EVENT_ERROR; }
result = IEC104_TcpServer_NextEvent(self->server, &received);
if (result != IEC104_TCP_EVENT_READY) { return result; }
memset(event, 0, sizeof(*event));
event->transport = received;
event->_owner = self;
event->_owner_serial = self->serial;
event->_generation = received.generation;
event->_last_submit_result = IEC104_TCP_SUBMIT_ERROR;
Bind_Event(event);
return IEC104_TCP_EVENT_READY;
}
/* 只校验事件自身布局和自引用,不要求原服务或TCP会话仍存活。
* Copy_Event允许保存历史原文;Get_Event_Identity在此基础上额外校验当前服务实例。
* 解析失败的I帧仍是有效通知,由业务处理其asdu_result,不在此丢弃诊断信息。 */
static bool Event_Valid(const IEC104_SlaveEvent_t *event)
{
const uint8_t *data;
uint16_t length;
if (event == NULL || event->_owner == NULL || event->_owner_serial == 0U ||
event->_generation == 0U || event->_generation != event->transport.generation ||
event->transport.type < IEC104_TCP_EVENT_CONNECTED ||
event->transport.type > IEC104_TCP_EVENT_DEACTIVATED) {
return false;
}
if (event->transport.type == IEC104_TCP_EVENT_I_FRAME && event->asdu_result == IEC60870_ASDU_OK) {
if (event->connection.object != event || Get_Context(&event->connection) != event ||
event->asdu.parameters != &parameters104 ||
IEC60870_Asdu_Get_Data(&event->asdu, &data, &length) != IEC60870_ASDU_OK ||
data != event->transport.data.i_frame.apdu + IEC104_APCI_LEN ||
(uint32_t)length + IEC104_APCI_LEN != event->transport.data.i_frame.apdu_length) {
return false;
}
}
return true;
}
/**
* 功能:把事件复制到稳定的目标存储,同时重建目标ASDU视图及发送接口的自引用。
* 输入参数:source——NextEvent或本函数产生的有效事件;destination——目标事件存储。
* 返回值:true已复制或源/目标为同一合法对象;false校验失败且不修改目标。
* 说明:先核对源视图仍借用源原文,避免传播普通赋值形成的悬空绑定。
* 保留原服务、代次和最近提交结果,不查询当前主站、不发送;历史事件也可复制保存。
* 复制只延长原文和接口存储的寿命,不能使已关闭或已被替换的原连接重新可用。
*/
bool IEC104_Slave_Copy_Event(IEC104_SlaveEvent_t *destination, const IEC104_SlaveEvent_t *source)
{
if (destination == NULL || !Event_Valid(source)) { return false; }
if (destination != source) { *destination = *source; Bind_Event(destination); }
return true;
}
/**
* 功能:验证业务收到的Slave事件,交付可保存的服务实例和原连接身份。
* 参数:self为所属服务;event为原事件或Copy_Event副本;identity为非NULL输出。
* 返回:true写出身份;false为非法来源/布局/视图或参数,保持输出不变。
* 说明:只匹配服务实例,不用当前TCP代次拒绝历史通知;是否仍可推进另由Check_Request判断。
*/
bool IEC104_Slave_Get_Event_Identity(const IEC104_Slave_t *self,
const IEC104_SlaveEvent_t *event, IEC104_SlaveEventIdentity_t *identity)
{
IEC104_SlaveEventIdentity_t copy;
if (!Slave_Owned(self) || identity == NULL || !Event_Valid(event) ||
event->_owner != self || event->_owner_serial != self->serial) {
return false;
}
memset(&copy, 0, sizeof(copy));
copy.slave = self;
copy.instance_id = event->_owner_serial;
copy.generation = event->_generation;
*identity = copy;
return true;
}
/**
* 功能:统一判断业务是否应先处理通知、放弃失效请求,或继续当前会话的工作。
* 参数:identity为Get_Event_Identity的只读结果,允许零初始化表示尚未绑定会话。
* 返回:ERROR为参数/查询失败;WAIT_EVENTS优先于会话判断;STALE为原身份不再可用;READY可继续。
* 说明:检查通知FIFO、服务实例、TCP代次和传输激活状态,不执行I/O或保留发送窗口。
* 先读空通知可避免同一代次STOP后又START时,仅凭最终激活状态误续旧业务。
*/
IEC104_SlaveRequestState_t IEC104_Slave_Check_Request(const IEC104_Slave_t *self,
const IEC104_SlaveEventIdentity_t *identity)
{
IEC104_TcpServerStatus_t status;
if (!Slave_Owned(self) || identity == NULL ||
IEC104_TcpServer_Get_Status(self->server, &status, NULL) != IEC104_TCP_OK) {
return IEC104_SLAVE_REQUEST_ERROR;
}
if (status.pending_event_count != 0U) { return IEC104_SLAVE_REQUEST_WAIT_EVENTS; }
if (identity->slave != self || identity->instance_id != self->serial ||
identity->generation == 0U || identity->generation != status.generation ||
!status.connected || !status.connection_status_valid || !status.connection.data_transfer_started) {
return IEC104_SLAVE_REQUEST_STALE;
}
return IEC104_SLAVE_REQUEST_READY;
}
/**
* 功能:补充共用发送接口bool结果,查询这个事件连接最近一次提交的具体状态。
* 输入参数:connection——event.connection的地址;不是Slave句柄,也不是底层socket。
* 返回值:OK/WAIT/ERROR/FATAL;尚未发送或接口绑定无效时返回ERROR。
* 说明:记录只在提交时更新,不随后续Poll或主站ACK更新;发送完成事实应查看RECORD事件。
*/
IEC104_TcpSubmitResult_t IEC104_Slave_Get_Last_Submit_Result(
const IEC60870_MasterConnection_t *connection)
{
const IEC104_SlaveEvent_t *event = Get_Context(connection);
return event != NULL ? event->_last_submit_result : IEC104_TCP_SUBMIT_ERROR;
}
/**
* 功能:取得本接入使用的104应用层参数,供业务构造主动上报或独立应答ASDU。
* 输入参数:self——有效Slave句柄,不要求已Init或当前在线。
* 返回值:静态只读参数地址;无效句柄返回NULL,调用方不能修改或释放该参数。
*/
const IEC60870_AppLayerParameters_t *IEC104_Slave_Get_Application_Layer_Parameters(
const IEC104_Slave_t *self)
{
return Slave_Owned(self) ? &parameters104 : NULL;
}
/**
* 功能:校验ASDU布局后复制到服务级NORMAL队列,用于主动上报。
* 输入参数:self——已Init的服务;asdu——合法且与104布局兼容的ASDU,不包含APCI。
* 返回值:OK已复制,WAIT未接手可重试,ERROR参数/状态非法,FATAL底层队列异常。
* 说明:允许首次接入前及断线期间缓存,本次不直接写socket;空间不足可覆盖最旧NORMAL。
* Close清空缓存;重连恢复可能重复,业务应按自身语义处理。请求应答使用事件connection。
*/
IEC104_TcpSubmitResult_t IEC104_Slave_Enqueue_Event(IEC104_Slave_t *self, IEC60870_Asdu_t *asdu)
{
const uint8_t *data;
uint16_t length;
if (!Slave_Owned(self) || asdu == NULL || !Parameters_Compatible(asdu->parameters) ||
IEC60870_Asdu_Get_Data(asdu, &data, &length) != IEC60870_ASDU_OK) {
return IEC104_TCP_SUBMIT_ERROR;
}
return IEC104_TcpServer_Enqueue_Event(self->server, data, length);
}
/**
* 功能:读取当前主站的IPv4、源端口和连接代次副本,不执行网络查询。
* 输入参数:self——已Init的服务;peer——非NULL输出存储;error——可选诊断。
* 返回值:仅OK写peer;无当前连接为NOT_CONNECTED,其他失败也保持peer原值。
* 说明:这是当前状态;历史事件的地址应从事件自身读取,不能用新主站地址替代旧来源。
*/
IEC104_TcpResult_t IEC104_Slave_Get_Remote_Address(const IEC104_Slave_t *self,
IEC104_TcpPeerInfo_t *peer, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
return IEC104_TcpServer_Get_Remote_Address(self->server, peer, error);
}
/**
* 功能:按事件捕获的原连接身份主动断开主站,保留监听和服务级NORMAL。
* 输入参数:self——所属Slave;event——本服务事件或Copy_Event副本;error——可选诊断。
* 返回值:OK已断开;其他服务/已复用Slave的事件为INVALID_ARGUMENT;当前无连接为NOT_CONNECTED;
* 仍有连接但代次不同为STALE_GENERATION;底层服务异常通过原结果和error返回。
* 说明:来源serial先由本层检查,原generation再由TcpServer核对;不用当前代次代替旧身份。
* 不等待STOPDT或ACK,成功后继续读取收尾事件,再Prepare更新监听条件。
*/
IEC104_TcpResult_t IEC104_Slave_Close_Connection(IEC104_Slave_t *self,
const IEC104_SlaveEvent_t *event, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
if (event == NULL || event->_owner != self || event->_owner_serial != self->serial ||
event->_generation == 0U) {
return Slave_Error(IEC104_TCP_INVALID_ARGUMENT, error);
}
return IEC104_TcpServer_Close_Connection(self->server, event->_generation, error);
}
/* 紧凑业务上下文只需保留来源身份即可关闭原会话;校验与完整事件关闭入口一致。 */
IEC104_TcpResult_t IEC104_Slave_Close_Connection_By_Identity(IEC104_Slave_t *self,
const IEC104_SlaveEventIdentity_t *identity, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
if (identity == NULL || identity->slave != self || identity->instance_id != self->serial ||
identity->generation == 0U) {
return Slave_Error(IEC104_TCP_INVALID_ARGUMENT, error);
}
return IEC104_TcpServer_Close_Connection(self->server, identity->generation, error);
}
/**
* 功能:取得内部TCP服务、队列和最近会话的状态副本,不推进协议或消费队列。
* 输入参数:self——有效句柄;status——非NULL输出存储;error——可选诊断。
* 返回值:OK写入status,失败保持输出;Create后、Init失败后和Close后均可查询。
* 说明:会话和队列各自有有效标志,connected与data_transfer_started含义不同。
*/
IEC104_TcpResult_t IEC104_Slave_Get_Status(const IEC104_Slave_t *self,
IEC104_TcpServerStatus_t *status, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
return IEC104_TcpServer_Get_Status(self->server, status, error);
}
/**
* 功能:关闭监听和当前连接,清空待发ASDU,但保留已生成的业务及收尾通知。
* 输入参数:self——尚未Destroy的句柄;error——可选的本次调用诊断。
* 返回值:OK已收尾或本来未初始化;无效句柄、重入按底层约定返回错误。
* 说明:不等待业务发送/ACK,不归还对象池名额;读空事件后可重新Init或Destroy。
* 自建事件循环还须撤销旧socket注册和定时器,不能将其通知用于下一次Init。
*/
IEC104_TcpResult_t IEC104_Slave_Close(IEC104_Slave_t *self, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
return IEC104_TcpServer_Close(self->server, error);
}
@@ -0,0 +1,286 @@
/*
* 104子站业务入口:内部持有TcpServer,NextEvent自动提供ASDU及原连接的发送接口。
* 服务来自静态池;业务只驱动Slave的事件循环,无需创建TCP对象或手动绑定generation。
* 仅包含本头文件即可使用服务接口、共用ASDU及IEC60870_MasterConnection_Send_*应答接口。
*
* 常规顺序:Create -> Config_Default/Init -> 循环Poll、NextEvent -> Close -> NextEvent读空 -> Destroy。
* 自有事件循环可用Prepare/应用等待/Process替代Poll;两种方式使用同一套底层调度。
* 处理I_FRAME前检查asdu_result==IEC60870_ASDU_OK,再通过&event.connection提交业务应答。
* 主动上报由Enqueue_Event提交;NextEvent不自动执行请求、确认或终止业务。
* 应用未接手的I帧可在当前事件处理时交给Reject_Request,沿普通应答路径回否定回显。
*
* 带error参数的接口允许error为NULL;非NULL时完整填写本次结果、阶段和网络错误。
* 服务结果须比较IEC104_TCP_OK;事件结果须比较IEC104_TCP_EVENT_READY,不能混用真假判断。
* 所有操作在同一事件循环线程串行执行,Create/Destroy也须跨服务串行。
*/
#ifndef IEC104_SLAVE_H
#define IEC104_SLAVE_H
#include "IEC60870_Slave.h"
#include "IEC104_TcpTypes.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 不透明的子站句柄:完整结构只在IEC104_Slave.c定义,业务不能访问或分配内部成员。
* Slave与TcpServer是两个分别管理的对象;这里是持有关系,不可把两种句柄互相强转。 */
typedef struct IEC104_Slave IEC104_Slave_t;
/* 复用TCP监听和协议配置的值类型,不暴露TcpServer的对象布局或操作接口。 */
typedef IEC104_TcpServerConfig_t IEC104_SlaveConfig_t;
/* 共用逻辑站点:调用方提供零初始化、固定地址的存储,Add后直到Is_Reusable为true均须存活。
* 字段仅供库维护,禁止直接改写、复制、清零或搬移已注册节点;不拥有context指向的业务数据。
* 同一Slave按CA唯一,多个Slave可分别注册相同CA;节点本身只能属于一个Slave。
* 站点不分配socket、发送队列或点表,IOA及点值仍由context对应的应用管理。 */
typedef struct IEC104_Station {
struct IEC104_Station *_next; /* 注册顺序链,也保留尚不能回收的已删除节点。 */
IEC104_Slave_t *_owner; /* 回收后才清NULL;Remove成功不一定立即解除所有权。 */
void *_context; /* 调用方业务上下文,须与节点一起保留至可复用。 */
uint64_t _id; /* 本次注册的唯一编号;同CA重新注册得到新编号。 */
uint32_t _users; /* 显式Acquire引用数;活动快照另由Slave统一保护。 */
uint16_t _common_address; /* ASDU公共地址,不是点的IOA或连接编号。 */
bool _registered; /* 是否参与新请求的CA路由,独立于节点是否仍被引用。 */
} IEC104_Station_t;
/* 跨循环遍历的轻量快照,仅固定注册编号上界,不复制点表/点值、不阻止增删。
* 零初始化并保持地址不变,Begin/End配对;内部字段只读,活动期间禁止复制或搬移。 */
typedef struct IEC104_StationSnapshot {
IEC104_Slave_t *_owner; /* Begin至End期间保护所属服务的退役节点。 */
IEC104_Station_t *_next; /* 下次扫描位置;即使该节点被删除也不能提前回收。 */
uint64_t _last_id; /* Begin时的注册编号上界,排除后来新增或同CA替换的节点。 */
const struct IEC104_StationSnapshot *_self; /* 校验地址未变,拒绝活动快照的副本。 */
} IEC104_StationSnapshot_t;
/* 站点操作结果,与TCP发送/网络错误分开;须显式比较OK。 */
typedef enum {
IEC104_STATION_OK = 0,
IEC104_STATION_INVALID_ARGUMENT,
IEC104_STATION_DUPLICATE_CA,
IEC104_STATION_ALREADY_REGISTERED,
IEC104_STATION_NOT_FOUND,
IEC104_STATION_BUSY,
IEC104_STATION_LIMIT
} IEC104_StationResult_t;
/* 注册到服务末尾;CA须1..65534,0和广播65535不能成为站点。context允许NULL。
* 无固定站点池上限、无堆分配;容量取决于应用提供的节点与实际RAM。
* 重复CA/已注册节点/站点表锁定时失败且不改变任何节点。所有操作与Slave事件循环串行。 */
IEC104_StationResult_t IEC104_Slave_Add_Station(IEC104_Slave_t *self,
IEC104_Station_t *station, uint16_t common_address, void *context);
/* 立即从CA查找/普通遍历中移除;已有引用和快照延迟回收节点,不中断已接手业务。
* OK仅表示已移除,Is_Reusable为true后才可释放/复用节点和context;不撤回已接手ASDU。
* 显式Lock仍返回BUSY;重复删除返回NOT_FOUND。已移除CA可由另一个节点重新注册。 */
IEC104_StationResult_t IEC104_Slave_Remove_Station(IEC104_Slave_t *self, IEC104_Station_t *station);
/* 按CA查找借用节点;无效服务、未知CA、0及65535返回NULL。不会自动接受或应答任何请求。
* 跨事件循环保存节点前须Acquire;短时查找后直接使用可不占用,期间不得删除节点。
* 广播是否允许、涉及哪些站、如何应答由各业务决定,不能把65535当作某个默认站。 */
IEC104_Station_t *IEC104_Slave_Get_Station(const IEC104_Slave_t *self, uint16_t common_address);
size_t IEC104_Slave_Get_Station_Count(const IEC104_Slave_t *self);
/* 注册编号在同一Slave槽内不复用;可找到仍被引用保留的已删除节点,避免按CA误取替代站点。 */
IEC104_Station_t *IEC104_Slave_Get_Station_By_Id(const IEC104_Slave_t *self, uint64_t station_id);
uint64_t IEC104_Station_Get_Id(const IEC104_Station_t *station);
bool IEC104_Station_Is_Registered(const IEC104_Station_t *station);
bool IEC104_Station_Is_Reusable(const IEC104_Station_t *station);
/* Begin记录当前注册上界;Next按注册顺序借用节点,跳过已删除节点,不包含后来新增节点。
* 即使Next返回NULL也须End。快照期间退役节点的存储仍须保留;不锁点值,不做堆分配。
* Next返回值跨End使用前须Acquire;这些接口与所有站点操作在同一事件循环串行调用。 */
IEC104_StationResult_t IEC104_Slave_StationSnapshot_Begin(IEC104_Slave_t *self,
IEC104_StationSnapshot_t *snapshot);
IEC104_Station_t *IEC104_Slave_StationSnapshot_Next(IEC104_StationSnapshot_t *snapshot);
IEC104_StationResult_t IEC104_Slave_StationSnapshot_End(IEC104_StationSnapshot_t *snapshot);
/* 按注册顺序遍历,current为NULL时取首站;非法/外来节点或末尾返回NULL。
* 短时串行遍历不修改站点表;跨循环保留游标或要求固定站点集合时须配对Lock/Unlock。
* Lock可嵌套,阻止Add/Remove及Slave_Destroy,允许查找和Acquire/Release;不锁点值或网络。
* 最后一次Unlock后所有未Acquire的借用节点/游标均不可继续依赖旧集合。 */
IEC104_Station_t *IEC104_Slave_Next_Station(const IEC104_Slave_t *self, const IEC104_Station_t *current);
IEC104_StationResult_t IEC104_Slave_Lock_Stations(IEC104_Slave_t *self);
IEC104_StationResult_t IEC104_Slave_Unlock_Stations(IEC104_Slave_t *self);
/* 业务持有引用:每次成功Acquire必须配对一次Release,计数溢出返回LIMIT、零计数释放报错。
* 只占用指定站点,其他站仍可增删;不能用一个业务的Release替另一个业务归还引用。
* 异步业务须一直持有到延迟事件和清理完成;引用不代表原连接仍有效,仍须Check_Request。 */
IEC104_StationResult_t IEC104_Slave_Acquire_Station(IEC104_Slave_t *self, IEC104_Station_t *station);
IEC104_StationResult_t IEC104_Slave_Release_Station(IEC104_Slave_t *self, IEC104_Station_t *station);
/* station须为NULL或仍有效的调用方节点;已回收/NULL时返回0或NULL,退役但被引用时仍可读取。
* context只读出指针,库不读取其内容,也不规定点表、权限或各业务状态的布局。 */
uint16_t IEC104_Station_Get_Common_Address(const IEC104_Station_t *station);
void *IEC104_Station_Get_Context(const IEC104_Station_t *station);
/* transport保存原通知;I_FRAME且asdu_result为OK时,asdu和connection可直接使用。
* asdu借用本对象transport中的字节;确认/终止会原地修改这份副本的COT/P/N。
* 具体对象、地址及业务权限仍须由应用校验,ASDU_OK不代表允许执行请求。
* NextEvent为EMPTY/ERROR时整个输出保持不变;READY会替换上次事件。
* 使用期间地址必须稳定。延迟处理可直接读入任务存储,或使用Copy_Event复制;
* 禁止用赋值/memcpy搬移含自引用的事件。一个事件的多个应答沿用同一connection。
* transport元数据、connection及以下内部字段只读;用ASDU接口修改报文。
* 内部字段保存原服务实例和原代次,重连、重新Init及静态槽复用均不重新绑定旧请求。
*/
typedef struct {
IEC104_TcpEvent_t transport; /* 原通知的独立副本;只有type对应的data成员有效。 */
IEC60870_Asdu_t asdu; /* 借用本事件I帧中APCI之后的字节;仅asdu_result为OK时可用。 */
IEC60870_AsduResult_t asdu_result; /* 非I_FRAME为UNSUPPORTED;异常原文为ERROR。 */
IEC60870_MasterConnection_t connection; /* 仅ASDU_OK的I_FRAME提供发送函数。 */
IEC104_Slave_t *_owner; /* 以下字段由模块维护:产生本事件的Slave地址。 */
uint64_t _owner_serial; /* 原Slave_Create编号,防止静态槽销毁重用后旧应答串入新服务。 */
uint64_t _generation; /* 原TCP会话代次,防止断线重连后把旧应答发给新主站。 */
IEC104_TcpSubmitResult_t _last_submit_result; /* 此接口最近的提交结果,不表示实时发送/ACK状态。 */
} IEC104_SlaveEvent_t;
/* Get_Event_Identity校验后给出的来源身份,可按值保存,不借用事件原文或自引用视图。
* 三个字段只读,调用Check_Request时原样传回;slave只用于身份比较,不延长服务寿命。
* 不同Slave实例的局部编号可能相同,不能丢弃slave而只比较两个编号。 */
typedef struct {
const IEC104_Slave_t *slave; /* 产生事件的服务句柄。 */
uint64_t instance_id; /* 区分该服务静态槽的历次使用。 */
uint64_t generation; /* 原TCP会话代次;断线重连后旧身份不会重新绑定。 */
} IEC104_SlaveEventIdentity_t;
/* 业务请求推进检查,与发送容量/ACK状态无关;须按枚举值显式分支。 */
typedef enum {
IEC104_SLAVE_REQUEST_ERROR = -1, /* 非法服务/NULL身份,或服务状态查询失败。 */
IEC104_SLAVE_REQUEST_STALE = 0, /* 身份未绑定、服务/代次不符,或原连接不再处于激活状态。 */
IEC104_SLAVE_REQUEST_WAIT_EVENTS = 1, /* 先按FIFO处理Slave通知,再检查请求,防止漏过STOP/撤销。 */
IEC104_SLAVE_REQUEST_READY = 2 /* 原请求会话仍可处理;实际发送是否接手以发送接口为准。 */
} IEC104_SlaveRequestState_t;
/* 从静态池取得子站及其私有TCP服务,无malloc、无网络操作;容量不足返回NULL。
* 容量由IEC104_TCP_SERVER_CAPACITY决定,与直接使用TcpServer共享底层名额。
* 成功后配对Close、读空事件、Destroy;业务不得自行分配、复制或free服务对象。 */
IEC104_Slave_t *IEC104_Slave_Create(void);
/* 只销毁已关闭且事件读空的服务;失败保留句柄。成功后所有旧句柄失效。
* 未Init或Init失败后也可直接销毁;运行中为INVALID_STATE,未读事件为EVENTS_PENDING。
* 站点须Remove并归还引用,快照须End,站点表锁须Unlock,否则INVALID_STATE;Close/Init保留站点。
* 旧事件仍可读其原文字节,但发送接口不能用于新的服务实例。error允许NULL。 */
IEC104_TcpResult_t IEC104_Slave_Destroy(IEC104_Slave_t *self, IEC104_TcpError_t *error);
/* 默认监听0.0.0.0:2404及推荐协议参数;NULL无操作。 */
void IEC104_Slave_Config_Default(IEC104_SlaveConfig_t *config);
/* 初始化网络和监听,复制config;成功后用Prepare/Process或Poll驱动。
* 初始化成功只表示可以接入主站;TCP连接及104激活状态分别由事件/状态查询反映。
* 失败不会销毁句柄;再次Init要求服务已Close且旧事件读空。 */
IEC104_TcpResult_t IEC104_Slave_Init(IEC104_Slave_t *self,
const IEC104_SlaveConfig_t *config, IEC104_TcpError_t *error);
/* 只读查询调度条件。need_process或pending_event_count非零时不得阻塞等待。
* socket只供事件循环注册;业务不得直接recv/send/close。 */
IEC104_TcpResult_t IEC104_Slave_Prepare(const IEC104_Slave_t *self,
IEC104_TcpWaitInfo_t *info, IEC104_TcpError_t *error);
/* 有界处理一批socket/定时就绪,不等待;ready的service_token及socket token来自原注册。
* 返回后读取NextEvent并重新Prepare;旧通知的身份不可替换成当前token。 */
IEC104_TcpResult_t IEC104_Slave_Process(IEC104_Slave_t *self,
const IEC104_TcpReady_t *ready, IEC104_TcpError_t *error);
/* 独立循环入口:组合准备、最多等待max_wait_ms和有界推进;0不等待。
* 有待消费通知时不阻塞等待,调用方仍须读取NextEvent;等待上限不等于整个调用的耗时上限。 */
IEC104_TcpResult_t IEC104_Slave_Poll(IEC104_Slave_t *self,
uint32_t max_wait_ms, IEC104_TcpError_t *error);
/* 按原FIFO取一个事件,自动绑定原服务/代次并创建I帧的可写ASDU视图。
* READY交付事件;EMPTY/ERROR保持输出。不执行业务、不自动确认、不进行I/O。
* I_FRAME须检查asdu_result;解析失败仍交付原事件,connection为空,不丢弃诊断原文。 */
IEC104_TcpEventResult_t IEC104_Slave_NextEvent(IEC104_Slave_t *self, IEC104_SlaveEvent_t *event);
/* 把事件及原文字节复制到稳定的任务存储,重建目标ASDU与接口的自引用。
* 不改变原服务实例/代次,不提交或重发任何报文;源和目标相同时只校验。
* true成功;false为NULL、非法事件/视图或未经本函数修复的按值副本,目标保持原值。 */
bool IEC104_Slave_Copy_Event(IEC104_SlaveEvent_t *destination, const IEC104_SlaveEvent_t *source);
/* 校验event的服务归属、身份字段和有效I帧视图,再写出来源身份;失败不改identity。
* self须为当前有效服务,event须由NextEvent/Copy_Event取得;identity为非NULL输出。
* 允许同一服务的历史连接事件,不要求原连接仍在线,便于按FIFO处理断开/重连通知。
* 其他服务、已销毁复用实例及未经Copy_Event修复的I帧副本返回false;不进行I/O或消费事件。 */
bool IEC104_Slave_Get_Event_Identity(const IEC104_Slave_t *self,
const IEC104_SlaveEvent_t *event, IEC104_SlaveEventIdentity_t *identity);
/* 检查由Get_Event_Identity取得的请求身份是否仍允许业务推进,不发送、不消费通知。
* 服务查询成功后,未读通知优先返回WAIT_EVENTS;读空后再判断来源、代次和激活状态。
* 零初始化身份在通知读空后返回STALE,可用于业务尚未绑定会话时的检查。
* READY不预留发送资源,不保证窗口/队列有空间;也不检查总召等业务自己的阶段或期限。 */
IEC104_SlaveRequestState_t IEC104_Slave_Check_Request(const IEC104_Slave_t *self,
const IEC104_SlaveEventIdentity_t *identity);
/* 查询该事件连接最近一次发送的OK/WAIT/ERROR/FATAL;空或无效接口返回ERROR。
* connection传&event.connection;结果仅在该接口提交时更新,不随Poll或ACK自动改变。
* true/OK为已接手,不代表已写完或已ACK;仅WAIT可保存原事件/ASDU等待重试。
* 确认/终止提交失败也不恢复已修改的字段;FATAL可能已写出字节。 */
IEC104_TcpSubmitResult_t IEC104_Slave_Get_Last_Submit_Result(
const IEC60870_MasterConnection_t *connection);
/* 使用原事件连接尝试直接提交,HIGH/NORMAL有待发数据或无法构帧时WAIT,不排队。
* OK已复制原文并固定发送顺序,不能重交;WAIT未接手,允许重取当前值后再试。
* ERROR为参数/身份/阶段非法,FATAL可能已发出字节;均不可当作背压盲目重试。
* 先读空Slave通知;取值、突发发布及本调用须在应用发布流程中串行协调。
* 同时更新Get_Last_Submit_Result;不改变普通MasterConnection_Send_*的排队行为。 */
IEC104_TcpSubmitResult_t IEC104_Slave_Try_Send_ASDU(
IEC60870_MasterConnection_t *connection, const IEC60870_Asdu_t *asdu);
/* 按Get_Event_Identity保存的原身份提交独立ASDU,无需长期保存完整SlaveEvent。
* identity可按值保存;三个字段保持原值,self必须是原服务,旧实例/旧代次不能发送。
* 普通路径可直接发送或排入HIGH;Try路径只有当前缓冲空闲且HIGH/NORMAL无待发条目时尝试构帧,
* 不能构帧则WAIT且不缓存。两者均直接返回OK/WAIT/ERROR/FATAL,不借用输入、不保存最近结果。
* 调用前须处理Slave通知并检查请求是否仍有效;此入口不管理业务撤销或业务超时。
* OK后不可重交;Try的WAIT后允许重读当前值重试,FATAL可能已发出字节。 */
IEC104_TcpSubmitResult_t IEC104_Slave_Send_Response(IEC104_Slave_t *self,
const IEC104_SlaveEventIdentity_t *identity, const IEC60870_Asdu_t *asdu);
IEC104_TcpSubmitResult_t IEC104_Slave_Try_Send_Response(IEC104_Slave_t *self,
const IEC104_SlaveEventIdentity_t *identity, const IEC60870_Asdu_t *asdu);
/* 对应用未接手的I帧当场回否定回显:复制请求,只改COT=cause(44..47)和P/N=1,
* 保留TEST、OA、CA、VSQ及对象原文;不修改输入事件或其最近提交结果。
* 沿原连接走普通应答路径,直接构帧或复制进HIGH,按提交顺序发送,不另建暂存队列。
* 仅在串行事件循环中、本次NextEvent之后且读取后续事件之前调用,无需先读空通知。
* 不得保存事件后延迟调用;延迟应答仍用Copy_Event/Check_Request并在启停、断线时取消。
* 返回:OK已接手,不表示已写完或已ACK;
* WAIT为HIGH剩余空间不足、未接手,兜底应答可丢弃并计数,不要求重试;
* ERROR为参数/事件非法、cause越界、原连接停用/断开/换代,或有未读启停通知;
* FATAL为发送故障,可能已写出字节,不得重试,继续读取断线收尾通知。
* 未读启停通知使本请求不跨启停提交;该检查不能识别已经被应用读完的历史启停。 */
IEC104_TcpSubmitResult_t IEC104_Slave_Reject_Request(IEC104_Slave_t *self,
const IEC104_SlaveEvent_t *event, int cause);
/* 返回标准104只读参数2/2/3、最大249,供主动上报构造ASDU;无效句柄返回NULL。
* 参数为静态常量,已取得的指针不借用服务存储。 */
const IEC60870_AppLayerParameters_t *IEC104_Slave_Get_Application_Layer_Parameters(
const IEC104_Slave_t *self);
/* 校验104布局后把ASDU复制到NORMAL,支持离线缓存;本次不直接写socket。
* 空间不足沿用NORMAL覆盖最旧原文的策略;返回OK表示已接手。 */
IEC104_TcpSubmitResult_t IEC104_Slave_Enqueue_Event(IEC104_Slave_t *self, IEC60870_Asdu_t *asdu);
/* 取得当前主站地址副本;尚未连接返回NOT_CONNECTED,不使用历史请求代替当前状态。 */
IEC104_TcpResult_t IEC104_Slave_Get_Remote_Address(const IEC104_Slave_t *self,
IEC104_TcpPeerInfo_t *peer, IEC104_TcpError_t *error);
/* 关闭指定事件的原连接,保留监听/NORMAL;无需业务提取或传入generation。
* event须来自本服务实例;其他服务或已销毁复用实例的事件为INVALID_ARGUMENT。
* 当前无连接为NOT_CONNECTED;仍有连接但原代次已过期为STALE_GENERATION,不影响新连接。
* 成功后继续读取收尾事件;本操作不等待STOPDT或ACK。 */
IEC104_TcpResult_t IEC104_Slave_Close_Connection(IEC104_Slave_t *self,
const IEC104_SlaveEvent_t *event, IEC104_TcpError_t *error);
/* 使用已保存的事件身份关闭原连接,适用于不保留完整事件的延迟业务。
* 身份来自Get_Event_Identity;原服务实例不符为INVALID_ARGUMENT,旧连接代次为STALE_GENERATION。
* 无连接为NOT_CONNECTED;OK保留监听/NORMAL并交付收尾通知,不影响后来复用槽位的新连接。 */
IEC104_TcpResult_t IEC104_Slave_Close_Connection_By_Identity(IEC104_Slave_t *self,
const IEC104_SlaveEventIdentity_t *identity, IEC104_TcpError_t *error);
/* 只读取得服务、队列、连接状态副本;Create后、Init失败后、Close后也可调用。 */
IEC104_TcpResult_t IEC104_Slave_Get_Status(const IEC104_Slave_t *self,
IEC104_TcpServerStatus_t *status, IEC104_TcpError_t *error);
/* 关闭监听和当前连接,保留未读通知;随后NextEvent读空,再Destroy归还静态槽。 */
IEC104_TcpResult_t IEC104_Slave_Close(IEC104_Slave_t *self, IEC104_TcpError_t *error);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_SLAVE_H */
@@ -0,0 +1,198 @@
/*
* IPv4子站TCP业务接口:隐藏Connection、队列及存储布局;socket只借给事件循环注册。
* 子站业务默认通过IEC104_Slave.h接入;本接口供Slave及独立TCP接入使用。
* Connection、SendManager等由TCP内部调度,服务对象仍保持不透明。
* 一个Slave拥有一个本模块句柄;本模块每个实例同时只服务一条主站TCP连接。
* 本层交付完整APDU及发送/连接事件,Slave再建立ASDU视图和原连接应答接口。
* 本层不解析具体业务命令、不自动应答ASDU;104序号、启停和超时由内部Connection处理。
*
* 生命周期:Create -> Config_Default/Init -> Prepare/Process/NextEvent -> Close/NextEvent -> Destroy。
* 独立TCP循环也可使用Poll,由它组合准备、平台等待和推进;两种入口使用同一推进实现。
* Close释放网络并保留通知,Destroy在通知读空后归还静态槽,两者不能互相替代。
* 服务结果、提交结果和取事件结果是三种枚举;须分别比较OK、SUBMIT_OK、EVENT_READY。
* error可为NULL;非NULL时完整填写本次调用诊断,历史网络错误另见状态快照。
*
* 同一服务的全部接口在同一线程串行调用;Create/Destroy也须跨服务串行调用。
* 工程只链接一个Net后端;当前Windows实现需链接Ws2_32。
*/
#ifndef IEC104_TCPSERVER_H
#define IEC104_TCPSERVER_H
#include "IEC104_TcpTypes.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 不透明句柄:这里仅声明结构体名称,完整成员在IEC104_TcpServer.c中定义。
* 业务只持有Create返回的指针,不能定义对象、计算其sizeof、复制对象或访问内部成员。
* Slave可以保存并传递此指针,无需看见完整结构;这不表示把底层对象公开给业务。
* Connection、SendManager和缓冲区都由实现持有,业务无需为它们分配存储。 */
typedef struct IEC104_TcpServer IEC104_TcpServer_t;
/* 从模块内部静态池取得对象,无malloc、无网络操作;无可用槽时返回NULL。
* 默认容量1,可在IEC104_TcpServer_Config.h或编译参数中配置;成功后必须配对Destroy。
* 代次或注册编号耗尽的槽不再分配;句柄至Destroy成功前有效,不得free、清零或自行分配对象。 */
extern IEC104_TcpServer_t *IEC104_TcpServer_Create(void);
/* 只释放已Close(或尚未Init)且事件已读空的对象,成功返回OK。
* 无效句柄、重入、仍在运行、未读事件分别返回INVALID_HANDLE/BUSY/INVALID_STATE/EVENTS_PENDING。
* 不隐式丢弃事件或关闭运行中的服务;成功后所有旧句柄副本失效。
* 同一静态槽再分配仍保留代次及注册计数;调用方须结束旧业务任务,不能继续使用旧句柄。
* 成功后应将业务保存的指针置NULL;对象来自静态池,不得调用free。 */
extern IEC104_TcpResult_t IEC104_TcpServer_Destroy(
IEC104_TcpServer_t *server, IEC104_TcpError_t *error);
/* 填写监听和协议默认值;NULL无操作,不占用对象池。
* 监听默认0.0.0.0:2404;协议默认值沿用Connection配置,静态存储容量由Config.h控制。 */
extern void IEC104_TcpServer_Config_Default(IEC104_TcpServerConfig_t *config);
/* 成功返回OK;server来自Create,或已Close且事件已读空。
* 参数/基础配置失败为INVALID_ARGUMENT/INVALID_CONFIG,运行中为INVALID_STATE,旧事件未读为EVENTS_PENDING。
* 协议配置须满足0<t2_ms<t1_ms;不满足时在申请网络资源前返回INVALID_CONFIG,error阶段为VALIDATE。
* 注册编号耗尽为REGISTRATION_EXHAUSTED,error阶段为REGISTRATION;须Destroy归还句柄,不再重试Init。
* NETWORK_ERROR通过error区分NET_INIT/LISTEN阶段;地址内容解析失败也属于LISTEN阶段。
* 成功后服务队列已经可用,尚未接入客户端也可Enqueue_Event;内部队列初始化异常为INTERNAL_ERROR。
* 失败回收本次申请的网络资源,句柄仍归业务持有,可查询状态、重试或Destroy。
* 重复Init不关闭旧服务;成功后通过Prepare/Process或Poll持续驱动。 */
extern IEC104_TcpResult_t IEC104_TcpServer_Init(
IEC104_TcpServer_t *server, const IEC104_TcpServerConfig_t *config, IEC104_TcpError_t *error);
/* 查询下一轮事件循环需要的socket、协议/接入/发送等待上限和本地工作标志;仅OK修改info。
* server须已Init;失败原因通过统一结果和error交付,不读取或修改Connection内部布局。
* service_token标识本次Init,socket token标识某次socket注册;二者与业务连接generation用途不同。
* Enqueue_Event、Send_Response、NextEvent、Process、Close_Connection、Init/Close改变调度条件,等待前须重新查询。
* 借出的socket只用于注册;Process/Send_Response/Close_Connection/Close可能关闭它们,之后须移除过期注册。
* 接入边沿触发时仍须遵守need_process;预算耗尽或背压恢复后不能只等下一个边沿。
* 接入资源暂缺或整轮4次均为RETRY时暂停100毫秒;期间撤销监听关注,由timer安排到期检查。
* 到期有接入容量即要求本地重试;无容量时等待NextEvent释放通知空间,不保留已到期定时器。
* 连续4次send返回OK+0或RETRY后暂停发送10毫秒;期间撤销客户端WRITE关注,由timer恢复。
* 暂停同时约束Process及快速应答;到期主动重试,接收和原协议期限继续推进。
* 单次通知模式须由应用按新快照重新布防;业务事件继续通过NextEvent取出。 */
extern IEC104_TcpResult_t IEC104_TcpServer_Prepare(
const IEC104_TcpServer_t *server, IEC104_TcpWaitInfo_t *info, IEC104_TcpError_t *error);
/* 根据一批就绪/定时/本地通知,有界执行接受、非阻塞收发和协议推进,绝不调用Net_Wait。
* ready必填;本地工作也使用最近Prepare的service_token构造空批次,不能用旧通知冒充新唤醒。
* 每轮最多32次Feed、32次send、4次accept和8条业务通知交付;断线收尾须一次完整交付。
* 交付预算或本类配额刚耗尽时先返回业务一次,下一轮重新获得预算;无处交付的I帧等待普通空间。
* 改变状态的ACT等待启停专用槽,NextEvent消费后安排重试;保留接收顺序,不影响已有CON续写。
* 可立即推进的工作由need_process提示,接入/发送暂停由timer提示,原有协议期限不重置。
* 就绪只是提示,实际I/O仍可能暂不可用;本地/定时唤醒也允许一次有界的非阻塞接收检查。
* 本批先接收并提交已到达事实,再检查确认期限;输入事件产生后交还应用处理,再续收。
* 仅因预算让出时,固定已到达输入的字节范围并要求立即续收,处理完后再检查ACK/TESTFR期限。
* 后来到达的输入不扩大旧批次;真正容量背压或接收无进展仍检查原期限。发送使用独立预算。
* OK包括无进度、过期通知被忽略及客户端已断线;服务级错误应保存error后Close。
* 所有调用必须在同一线程;不创建线程、系统定时器、跨线程唤醒或业务回调。 */
extern IEC104_TcpResult_t IEC104_TcpServer_Process(
IEC104_TcpServer_t *server, const IEC104_TcpReady_t *ready, IEC104_TcpError_t *error);
/* 独立循环的组合入口:Prepare -> 至多一次Net_Wait -> Process,使用同一套推进逻辑。
* 每轮返回后NextEvent处理事件;使用应用自己的事件循环时直接调用Prepare/Process。
* max_wait_ms是本轮网络等待上限:0仅查询就绪,正数允许等待;UINT32_MAX仍为有限毫秒数。
* 实际等待还受最近协议/接入/发送重试期限约束;已有事件或可立即推进的工作时不等待。
* 无socket关注时也可有限等待;0毫秒立即返回,UINT32_MAX不会转成系统无限睡眠。
* 上限只约束网络等待,不保证整个函数的执行耗时;0模式下由业务主循环安排其他任务和休眠。
* OK表示服务可继续(包括客户端已断线);尚未Init为INVALID_STATE,重入为BUSY。
* NETWORK_ERROR/INTERNAL_ERROR及身份编号耗尽为服务级失败,保存error后应Close。
* 空闲、接入容量/资源暂缺、可恢复的网络中断及客户端故障均不作为服务级失败。
* 普通通知满仍可处理排在前面的S/U,不以普通通知容量阻挡CON;已确认回执暂留内部记录。
* 遇到无处交付的I帧时暂停续收并置receive_paused;启停专用槽满时暂缓受理后续改变状态的ACT。
* 消费对应通知后由Process恢复,不丢通知、不因通知积压直接断连;等待启停槽不设置receive_paused。
* 保留的I帧会挡住同一TCP流中后续S/U;应用须及时消费事件,原有协议期限不延长,超时仍可断连。
* EOF后停止为未编号NORMAL创建新I帧,原文留给下一连接;当前已编号帧、已受理HIGH及控制帧继续收尾。
* 部分写入跨Poll续发,不再等待新的ACK/STARTDT。
* 若仅因已确认记录尚未转为事件而缺少记录槽,则保留已受理应答,预算恢复或NextEvent腾出空间后续发;
* 已排空或仍需对端输入才能解除限制时关闭,原有协议及发送期限仍生效。 */
extern IEC104_TcpResult_t IEC104_TcpServer_Poll(
IEC104_TcpServer_t *server, uint32_t max_wait_ms, IEC104_TcpError_t *error);
/* 复制并取出最早事件,不执行I/O或业务函数;仅READY修改输出。
* EMPTY/ERROR保持输出不变,副本无需Release;Close后可读完全部收尾事件。
* 本层事件只含值和原文字节,可按值保存;Slave事件另外含自引用,须通过Slave_Copy_Event复制。
* 建立/激活/停用/断开均通过本入口逐条交付,不合并中间变化。
* 消费后安排保留I帧交付、启停CON续发、回执转移或接入重试;这里只取副本,不执行I/O或业务函数。
* 应用须及时消费通知并持续调用Process/Poll;等待通知空间不延长协议期限,长期不消费仍可能超时。
* READY后事件已从内部FIFO移除;需要稍后处理时由业务另存副本,不能依赖自动重投。 */
extern IEC104_TcpEventResult_t IEC104_TcpServer_NextEvent(
IEC104_TcpServer_t *server, IEC104_TcpEvent_t *event);
/* 复制主动上报到服务级低优先级事件队列(内部NORMAL);本入口只缓存,不立即写socket。
* Init成功后即可调用,无需连接代次,首次连接前和断线期间也可缓存。
* EOF后全部尚未编号的数据及新增数据留给下一连接;重连后现存NORMAL按原顺序重新组帧,STARTDT完成后才发送。
* 容量不足沿用覆盖最旧原文的策略,包括未ACK原文;不持久化,Close清空队列。
* 未ACK不代表对端未收到,恢复可能重复;同一服务应面向相同业务接收方或冗余域。
* 有活动连接且未EOF时,成功入队使Prepare报告need_process;离线缓存不会触发空转。
* 本入口与Send_Response均接收6..249字节ASDU,不含APCI;OK已接手,WAIT需业务保存重试。
* 所有调用须在所属事件循环线程串行执行;异步生产者先唤醒该线程,入队后刷新等待条件。 */
extern IEC104_TcpSubmitResult_t IEC104_TcpServer_Enqueue_Event(
IEC104_TcpServer_t *server, const uint8_t *asdu, uint16_t asdu_length);
/* 提交当前请求的应答ASDU,须携带原I_FRAME事件的generation;旧代次、无连接及EOF后均拒绝。
* 仅STARTED+IDLE时接手;否则在容量检查前ERROR,不入HIGH、不因拒绝断连。
* 已受理START、窗口及记录有余量、没有当前帧或旧HIGH且公平配额允许时,直接准备I帧并尝试一次send。
* 准入后,已到协议期限时先入HIGH,交由Process接收待处理ACK后再检查期限;其他暂不能准备的情况也入HIGH。
* send后按真实时刻登记进度;若期间跨过ACK/TESTFR的t1,安排立即Process先接收再按原期限判定。
* 确认t1已到且尚未解除时,Process也暂停为HIGH/NORMAL创建新I帧;已有帧及控制发送仍可推进。
* 不增加宽限时间;本地发送和STOP总期限仍在发送推进时检查。
* START受理即激活,已有CON待发时应答入HIGH,实际I帧发送仍排在控制FIFO之后。
* STOP受理即停用并清空待发HIGH,不等待START_CON完成;新应答返回ERROR。
* 后到START成功受理即重新激活并取消旧STOP总计时,已有CON继续按FIFO完成。
* 本项目保留已启用状态下重复START不清理HIGH的约定;重新激活不恢复已经丢弃的HIGH。
* 不等待、不循环发送,并遵守发送重试暂停;无进展时保留当前APDU,由Process/Poll续发,不重复放入HIGH。
* OK已复制到发送缓冲或HIGH,可以复用原文;不承诺已写完或已ACK。HIGH满返回WAIT,本次未接手。
* 受理新STOPDT_ACT时,尚在HIGH中的应答立即作废,无逐条事件;已有I帧及ACK记录继续排空。
* FATAL可能已写出字节并关闭当前连接,须处理收尾事件;HIGH及直接应答均不转给新连接。
* 所有调用在事件循环线程串行执行;返回后刷新Prepare,及时处理本地续跑、socket及定时变化。 */
extern IEC104_TcpSubmitResult_t IEC104_TcpServer_Send_Response(
IEC104_TcpServer_t *server, uint64_t generation,
const uint8_t *asdu, uint16_t asdu_length);
/* 不排队应答:HIGH/NORMAL有待发数据、当前帧未完成、窗口/记录不足或协议工作到期时WAIT。
* WAIT未接手,不保存ASDU;继续Process/Poll后可用新取值重试。
* OK已复制到SendBuffer并固定发送顺序,部分写入时也不能重交,不要求已ACK。
* 调用串行、会话身份、FATAL和等待条件刷新约定同Send_Response。 */
extern IEC104_TcpSubmitResult_t IEC104_TcpServer_Try_Send_Response(
IEC104_TcpServer_t *server, uint64_t generation,
const uint8_t *asdu, uint16_t asdu_length);
/* 查询当前主站的IPv4、源端口及连接代次;仅OK写peer,不查询socket、不推进协议。
* server须已Init且当前持有客户端(包括EOF收尾期间);未Init为INVALID_STATE,无客户端为NOT_CONNECTED。
* NULL输出为INVALID_ARGUMENT;无效句柄/重入为INVALID_HANDLE/BUSY;失败不改peer,error可为NULL。
* 结果是可独立保存的副本,不保证以后仍有同一连接;Close_Connection应携带该副本的generation。
* 处理历史事件时使用事件内peer,不可将此处查到的新主站地址归到旧代次。 */
extern IEC104_TcpResult_t IEC104_TcpServer_Get_Remote_Address(
const IEC104_TcpServer_t *server, IEC104_TcpPeerInfo_t *peer, IEC104_TcpError_t *error);
/* 立即结束指定代次的当前主站,保留监听、服务队列和已有业务事件;不执行STOPDT或等待发送/ACK。
* generation来自原连接事件或Get_Remote_Address,0为INVALID_ARGUMENT;非零但与当前连接不符为STALE_GENERATION。
* 未Init为INVALID_STATE,无客户端为NOT_CONNECTED;拒绝结果均不关闭连接、不产生新的收尾事件。
* 成功复用断线收尾流程:交付剩余I帧/记录及DISCONNECTED,清空HIGH,保留现存NORMAL并恢复离线缓存。
* 主动断开不新增协议故障,不补造发送完成或ACK;部分/未确认NORMAL恢复后可能重复,由业务处理。
* 队列解除绑定异常返回INTERNAL_ERROR/QUEUE_STATE:客户端已结束,监听保留,须Close服务处理异常。
* 同一事件循环线程串行调用;返回后重新Prepare并移除旧客户端注册,后续Process继续接入新主站。
* 此操作不是封禁;对端可再次连接。Close才关闭整个服务并清空缓存。 */
extern IEC104_TcpResult_t IEC104_TcpServer_Close_Connection(
IEC104_TcpServer_t *server, uint64_t generation, IEC104_TcpError_t *error);
/* 取得端口、诊断、会话及HIGH/NORMAL队列快照,不执行I/O;Create后、Init失败后、Close后均可用。
* OK时写入status;INVALID_HANDLE/INVALID_ARGUMENT/BUSY保持status不变,但仍填写error。
* 会话/队列分别检查connection_status_valid/asdu_queue_status_valid,无效快照不妨碍读取其他服务状态。
* 队列统计在Init成功后即可读取,断线期间继续有效;Close后无效且清零,不返回历史队列占用。
* accept_paused及剩余时间按查询时刻计算;last_accept_error和连续失败计数用于观察接入异常。
* 所有调用须在同一线程串行执行;查询为常数时间,不消费ASDU或记录,也不改变发送调度。
* 接口失败诊断直接使用该次调用的error,统计数值不能代替提交返回值。 */
extern IEC104_TcpResult_t IEC104_TcpServer_Get_Status(
const IEC104_TcpServer_t *server, IEC104_TcpServerStatus_t *status, IEC104_TcpError_t *error);
/* OK表示完成收尾或本就未初始化;无效句柄/重入为INVALID_HANDLE/BUSY。
* 关闭网络、清空ASDU队列并撤销接入/发送重试期限,保留未读事件及接入历史诊断。
* 不等待业务发送或ACK;重复Close有效。事件读空后可以Init重开,或Destroy归还静态槽。
* 应用负责取消注册和定时器;关闭前已取得的socket及通知不可作为下一次Init的身份使用。 */
extern IEC104_TcpResult_t IEC104_TcpServer_Close(
IEC104_TcpServer_t *server, IEC104_TcpError_t *error);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_TCPSERVER_H */
@@ -0,0 +1,57 @@
/*
* TcpServer实现的编译期容量配置,不属于业务接口。
* 修改后重新编译IEC104_TcpServer.c;SERVER_CAPACITY也用于IEC104_Slave.c,须一起重编译。
* 业务无需了解各内部对象的尺寸或对齐。
* 可通过编译器-D覆盖;所有存储来自模块内静态对象池,不使用malloc。
* 除SERVER_CAPACITY外,其余容量均按每个服务对象计算;增加服务数量会同比增加这些静态存储。
*/
#ifndef IEC104_TCPSERVER_CONFIG_H
#define IEC104_TCPSERVER_CONFIG_H
#ifndef IEC104_TCP_SERVER_CAPACITY
#define IEC104_TCP_SERVER_CAPACITY 1U /* 同时持有的服务器句柄数;每个支持一条主站连接。 */
#endif
#ifndef IEC104_TCP_RX_CAPACITY
#define IEC104_TCP_RX_CAPACITY 2048U /* 字节数:recv暂存及未消费尾部,默认可容纳每轮8个满长APDU。 */
#endif
/* 两级ASDU队列均须容纳一条最大ASDU:
* HIGH至少IEC104_ASDU_HIGH_ENTRY_HEADER_LEN+IEC104_MAX_ASDU_LEN;
* NORMAL至少IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN+IEC104_MAX_ASDU_LEN。
* TcpServer.c按实际sizeof静态校验,过小的-D覆盖会导致编译失败。
* Send_Response的排队路径使用HIGH;Enqueue_Event始终使用NORMAL。 */
#ifndef IEC104_TCP_HIGH_CAPACITY
#define IEC104_TCP_HIGH_CAPACITY 1024U /* 字节数:包含HIGH条目头和ASDU原文;空间不足时拒绝入队。 */
#endif
#ifndef IEC104_TCP_NORMAL_CAPACITY
#define IEC104_TCP_NORMAL_CAPACITY 4096U /* 字节数:包含NORMAL条目头和原文;空间不足可覆盖最旧原文。 */
#endif
#ifndef IEC104_TCP_CONTROL_CAPACITY
#define IEC104_TCP_CONTROL_CAPACITY 8U /* 槽数:待发及正在发送的S/U帧,不占用业务I帧记录槽。 */
#endif
#ifndef IEC104_TCP_RECORD_CAPACITY
#define IEC104_TCP_RECORD_CAPACITY 32U /* 槽数:未转为事件的I帧记录,含待ACK及已ACK待转移记录。 */
#endif
#ifndef IEC104_TCP_EVENT_CAPACITY
#define IEC104_TCP_EVENT_CAPACITY 68U /* 保证条数:包含按最长通知计的普通配额、启停及断线收尾预留。 */
#endif
#ifndef IEC104_TCP_EVENT_STATE_RESERVE
#define IEC104_TCP_EVENT_STATE_RESERVE 4U /* 保证条数:启停专用;逐条保存变化,不借给普通通知。 */
#endif
/* 最多全部发送记录、一个已受理I帧和一个断开事件;这部分不能挪作普通事件。
* 普通通知容量=EVENT_CAPACITY-EVENT_STATE_RESERVE-EVENT_CLOSE_RESERVE,必须大于0;默认30。
* 宏名及条数推导不变,实际FIFO按有效载荷长度存放于字节环;短通知可多于保证条数。
* 普通字节数=普通配额×最长I帧通知;启停字节数=启停配额×连接通知。
* 收尾字节数=最长I帧通知+RECORD_CAPACITY×记录通知+断开通知;均包含内部头部。
* 三类容量属于同一个FIFO;已入队通知按顺序保存,不覆盖、不合并。
* 普通通知满仍可处理排在前面的S/U,不以普通容量阻挡CON;无处交付的I帧保留原帧并暂停续收。
* NextEvent腾出普通空间后恢复;已确认回执暂留内部记录,可能晚于后续启停通知交付。
* 启停专用槽满时保留旧通知,延后受理改变状态的ACT;NextEvent释放槽后由Process重试。
* CON写入和同状态重复ACT不产生新启停通知;已有CON不受启停通知容量阻挡。
* 通知积压不直接断连,但不延长协议期限;收尾预留保证关闭时已受理数据仍可交付。
* 这里的普通通知区不是NORMAL发送队列;NORMAL可覆盖旧ASDU原文,两者容量和用途独立。 */
#define IEC104_TCP_EVENT_CLOSE_RESERVE (IEC104_TCP_RECORD_CAPACITY + 2U)
#define IEC104_TCP_EVENT_NORMAL_CAPACITY \
(IEC104_TCP_EVENT_CAPACITY - IEC104_TCP_EVENT_STATE_RESERVE - IEC104_TCP_EVENT_CLOSE_RESERVE)
#endif /* IEC104_TCPSERVER_CONFIG_H */
@@ -0,0 +1,282 @@
/* TCP服务与子站门面共用的配置、事件、调度和结果值类型;不声明服务句柄或操作接口。 */
#ifndef IEC104_TCP_TYPES_H
#define IEC104_TCP_TYPES_H
#include "IEC104_Types.h"
/* Init/Prepare/Process/Poll/Close_Connection/Close/Get_Remote_Address/Get_Status/Destroy统一使用此结果;显式比较IEC104_TCP_OK。
* OK为0,不能沿用if (result)或if (!result)判断成功;连接故障另由事件中的fault表达。
* 返回错误不会隐式Destroy句柄;按错误分类修正调用、读完事件或执行Close。 */
typedef enum {
IEC104_TCP_OK = 0, /* 本次接口操作完成;Poll无进度也可成功。 */
IEC104_TCP_INVALID_ARGUMENT = -1, /* 必填指针为NULL,或就绪批次的数量/事件位非法。 */
IEC104_TCP_INVALID_HANDLE = -2, /* server不是当前占用的静态池句柄,包括NULL。 */
IEC104_TCP_INVALID_CONFIG = -3, /* 配置基础校验不通过,未申请网络资源。 */
IEC104_TCP_INVALID_STATE = -4, /* Init/Destroy时仍在运行,或准备/推进时尚未Init。 */
IEC104_TCP_BUSY = -5, /* 正在收发推进、Send_Response、Close_Connection或Close中,拒绝重入;非线程锁。 */
IEC104_TCP_EVENTS_PENDING = -6, /* Init/Destroy前须先通过NextEvent读完旧事件。 */
IEC104_TCP_NETWORK_ERROR = -7, /* 网络初始化、监听或就绪/接入操作失败。 */
IEC104_TCP_INTERNAL_ERROR = -8, /* 内部队列或协议会话无法初始化等实现异常。 */
IEC104_TCP_GENERATION_EXHAUSTED = -9, /* 会话代次已用尽,关闭并归还该句柄,不回绕重用。 */
IEC104_TCP_REGISTRATION_EXHAUSTED = -10, /* socket注册编号已用尽,关闭并归还,不回绕重用。 */
IEC104_TCP_NOT_CONNECTED = -11, /* 服务已Init但当前无主站;Get_Remote_Address/Close_Connection不使用历史会话。 */
IEC104_TCP_STALE_GENERATION = -12 /* Close_Connection的非零代次不匹配当前主站,未关闭任何连接。 */
} IEC104_TcpResult_t;
/* 失败发生的位置,不公开Connection/SendManager的内部返回值或结构布局。
* LISTEN包括地址解析、创建socket、绑定及监听,具体平台原因见socket_error。 */
typedef enum {
IEC104_TCP_STAGE_NONE = 0, /* 成功时无失败阶段。 */
IEC104_TCP_STAGE_VALIDATE, /* 句柄、指针或配置检查。 */
IEC104_TCP_STAGE_LIFECYCLE, /* 重入、运行状态、未读事件或静态槽回收检查。 */
IEC104_TCP_STAGE_NET_INIT, /* 取得平台网络初始化资源。 */
IEC104_TCP_STAGE_LISTEN, /* 创建非阻塞IPv4监听。 */
IEC104_TCP_STAGE_WAIT, /* 等待本轮网络就绪。 */
IEC104_TCP_STAGE_ACCEPT, /* 从监听句柄接入客户端。 */
IEC104_TCP_STAGE_GENERATION, /* 为新会话分配代次,或核对Close_Connection目标代次。 */
IEC104_TCP_STAGE_QUEUE_INIT, /* 初始化或重绑本服务的ASDU队列。 */
IEC104_TCP_STAGE_QUEUE_STATE, /* 服务队列状态异常,或旧会话无法解除绑定。 */
IEC104_TCP_STAGE_CONNECTION_INIT,/* 初始化新协议会话。 */
IEC104_TCP_STAGE_SCHEDULE, /* 查询协议期限并计算本轮等待时间。 */
IEC104_TCP_STAGE_REGISTRATION /* 为监听或客户端分配不复用的注册编号。 */
} IEC104_TcpErrorStage_t;
/* 可选的本次调用诊断,error传NULL时仍返回同样的结果枚举。
* 每次调用均完整覆盖:成功为OK/NONE/0,非网络错误的socket_error为0,不沿用历史错误。
* 即使句柄无效或Destroy成功仍可取得诊断;输出存储不得与配置、状态等输入/输出重叠。
* 本结构由调用方持有,可按值保存;后续Get_Status、Close等调用不会修改已保存的副本。 */
typedef struct {
IEC104_TcpResult_t result; /* 与本次函数返回值一致。 */
IEC104_TcpErrorStage_t stage; /* 失败阶段,成功时为NONE。 */
int socket_error; /* 本次网络失败的原生诊断码;仅供日志,不作跨平台分支。 */
} IEC104_TcpError_t;
/* 只供应用事件循环注册/取消注册,所有权仍属于TcpServer。
* Windows适配代码可转换为SOCKET;不能recv/send/close、修改非阻塞模式或接管I/O。
* socket数值可能被系统复用,必须连同token一起保存、回传,不能只按数值匹配通知。
* 这里只使用标准整数类型,不要求业务包含Net或操作系统头文件。 */
typedef uintptr_t IEC104_TcpSocket_t;
#define IEC104_TCP_MAX_SOCKETS 2U /* 一个监听socket和一个活动客户端。 */
#define IEC104_TCP_IO_READ 0x01U /* 监听socket可接入,或客户端可读/收到EOF。 */
#define IEC104_TCP_IO_WRITE 0x02U /* 客户端可写;只在确有待发字节时注册。 */
#define IEC104_TCP_IO_ERROR 0x04U /* 就绪输入专用:错误/挂断提示,由实际非阻塞I/O判定原因。 */
/* Prepare中events是关注位(READ/WRITE),Process中events是本批实际就绪位。
* 同一socket同一批只占一项,可读和可写合并;计数以外的数组项不使用。 */
typedef struct {
IEC104_TcpSocket_t socket;
uint64_t token; /* 本次socket生命周期的非零编号,关闭、重连、重新Init后不复用。 */
uint32_t events;
} IEC104_TcpSocketEvent_t;
/* 等待条件的独立快照;Prepare只查询,不执行收发、accept、协议推进或等待。
* 应用更新完整关注集合,移除旧项及已撤销的READ/WRITE位,并替换/取消一次性定时器。
* 接入暂停时监听socket暂时不在集合中,应用须撤销注册;暂停结束后按新快照恢复。
* timeout_ms从Prepare查询时刻计起;若应用处理其他任务耗时较长,应在等待前重新Prepare。
* need_process或pending_event_count非0时不能阻塞等待:先处理业务事件,再安排一轮Process。
* 无本地工作时才等待socket或timer;timer_active=0表示取消定时器,不是立即超时。 */
typedef struct {
uint64_t service_token; /* 本次Init的身份,同时用于隔离旧服务的定时/本地调度通知。 */
IEC104_TcpSocketEvent_t sockets[IEC104_TCP_MAX_SOCKETS];
uint32_t socket_count;
uint32_t timeout_ms; /* timer_active=1时有效;0须立即调度,UINT32_MAX仍为有限毫秒数。 */
uint16_t pending_event_count; /* 待NextEvent处理的业务事件数,与socket就绪分开。 */
uint8_t receive_paused; /* 1为已受理I帧因通知容量不足暂留内部;消费NextEvent后安排Process恢复。 */
uint8_t timer_active; /* 1为协议检查或接入/发送重试安排定时;无客户端时也可能为1。 */
uint8_t need_process; /* 本地入队、预算续跑、接收恢复或已到期,无需等待新的socket通知。 */
} IEC104_TcpWaitInfo_t;
/* 应用收集同一轮socket就绪及定时事件后一次传入,保证已有输入先于超时检查。
* service_token来自Prepare;每项socket/token来自实际注册时保存的副本,不得换成新token。
* 只有定时器到期时置timer_expired=1;本地唤醒用socket_count=0、timer_expired=0。
* 旧service_token整批忽略;当前服务中已关闭/替换socket的旧项逐项忽略,均返回OK。
* timer_expired只是唤醒提示,协议及接入/发送重试期限仍按模块当前单调时钟重新判定。 */
typedef struct {
uint64_t service_token;
IEC104_TcpSocketEvent_t sockets[IEC104_TCP_MAX_SOCKETS];
uint32_t socket_count;
uint8_t timer_expired;
} IEC104_TcpReady_t;
typedef enum {
IEC104_TCP_EVENT_CONNECTED = 1, /* TCP建立,不表示已完成STARTDT。 */
IEC104_TCP_EVENT_I_FRAME, /* 链路已受理的I帧,ASDU业务由应用处理。 */
IEC104_TCP_EVENT_RECORD, /* 正常ACK回执或断线时剩余发送记录。 */
IEC104_TCP_EVENT_DISCONNECTED, /* 同一generation的最后一个事件;不补发停用事件。 */
IEC104_TCP_EVENT_ACTIVATED, /* STARTDT_ACT成功受理并激活数据传输,CON可能仍待发。 */
IEC104_TCP_EVENT_DEACTIVATED /* STOPDT_ACT成功受理并停用数据传输,确认可能仍待完成;TCP保持。 */
} IEC104_TcpEventType_t;
/* 必须明确比较READY,ERROR不表示有事件。 */
typedef enum {
IEC104_TCP_EVENT_ERROR = -1,
IEC104_TCP_EVENT_EMPTY = 0,
IEC104_TCP_EVENT_READY = 1
} IEC104_TcpEventResult_t;
/* 表达业务ASDU是否已接手,不泄露Connection的内部调度结果。
* OK后不得因尚未ACK而重复提交;WAIT未接手,可以保留原文后重试。
* 应答FATAL可能发生在发送之后,须结合RECORD/DISCONNECTED判断,不能当作完全未发送。 */
typedef enum {
IEC104_TCP_SUBMIT_FATAL = -2, /* 服务队列异常,或应答所属会话已失败并收尾;可能已写出字节。 */
IEC104_TCP_SUBMIT_ERROR = -1, /* 参数、代次或调用阶段非法;应答要求活动且未EOF,并满足启停准入。 */
IEC104_TCP_SUBMIT_OK = 1, /* ASDU已复制到队列或发送缓冲;不承诺已写完或已ACK。 */
IEC104_TCP_SUBMIT_WAIT = 2 /* 容量或编号窗口暂不足,业务保留原文稍后重试。 */
} IEC104_TcpSubmitResult_t;
typedef enum {
IEC104_TCP_SESSION_NONE = 0, /* 尚未建立协议会话;仍须先检查状态快照的有效标志。 */
IEC104_TCP_SESSION_ACTIVE, /* 协议会话可运行,数据传输是否启动另见快照。 */
IEC104_TCP_SESSION_FAILED, /* 已登记故障,等待收尾。 */
IEC104_TCP_SESSION_CLOSED /* 历史会话已关闭。 */
} IEC104_TcpSessionPhase_t;
/* 跨平台的连接故障分类;包含协议及TcpServer调度故障,数值不依赖内部模块枚举。 */
/* 已登记的会话故障优先保留;内部调用或失败回报异常归INTERNAL。
* 格式合法的反向STARTDT_CON/STOPDT_CON直接忽略,不导致故障或启停通知。
* socket_error独立保留原生网络错误码,不能用其是否为0代替fault分类。 */
typedef enum {
IEC104_TCP_FAULT_NONE = 0, /* 未登记协议会话故障,有序断开可保持此值。 */
IEC104_TCP_FAULT_FRAME, /* 接收起始字节、APCI或ASDU最低长度非法。 */
IEC104_TCP_FAULT_PROTOCOL, /* 序号、ACK或接收阶段不符合协议。 */
IEC104_TCP_FAULT_INTERNAL, /* 内部状态、调用接入或模块交接异常。 */
IEC104_TCP_FAULT_TRANSPORT, /* 网络传输异常,原生错误码另见socket_error。 */
IEC104_TCP_FAULT_LOCAL_SEND_TIMEOUT,/* 当前APDU未在本地发送总期限内写完。 */
IEC104_TCP_FAULT_I_ACK_TIMEOUT, /* 已写完I帧等待对端累计ACK超过t1。 */
IEC104_TCP_FAULT_TESTFR_TIMEOUT, /* 本端探测ACT已写完,等待TESTFR_CON超过t1。 */
IEC104_TCP_FAULT_STOP_TIMEOUT /* STOP受理后未在停止总期限内完成应答。 */
} IEC104_TcpFault_t;
/* 业务只读快照;启停中间步骤、接收器借用及队列下标均留在实现内部。
* data_transfer_started表示已提交STARTDT,不代表此刻发送窗口或容量一定可用。
* 实际启停变化通过ACTIVATED/DEACTIVATED按发生顺序逐条交付,不合并、不覆盖。 */
typedef struct {
IEC104_TcpSessionPhase_t phase; /* 协议会话阶段,不代替TcpServer状态中的connected。 */
IEC104_TcpFault_t fault; /* 该会话保留的首次故障分类。 */
uint16_t pending_send_records; /* 尚未转为RECORD事件的记录,包含待ACK及已ACK待转移的项。 */
uint8_t data_transfer_started; /* 1表示Link已提交STARTED;停止排空的中间过程不对外展开。 */
} IEC104_TcpConnectionStatus_t;
/* 对端身份的独立数值副本,可跨断线保存;不是socket句柄或可直接操作的连接对象。
* Get_Remote_Address返回当前主站;连接建立/激活/停用/断开事件保存原身份,后续重连不会改写它。
* 当前仅支持IPv4,地址不包含端口;generation只能与取得它的同一服务句柄一起使用。 */
typedef struct {
uint64_t generation; /* 该主站的非零连接代次;事件中与外层generation一致。 */
char address[16]; /* NUL结尾的数字IPv4,如127.0.0.1。 */
uint16_t port; /* 主站源端口,主机字节序;与子站bound_port不同。 */
} IEC104_TcpPeerInfo_t;
/* 完整APDU独立副本;ASDU为apdu + IEC104_APCI_LEN,长度为apdu_length减该常量。
* 接收入口已保证ASDU公共头完整,并检查已知固定长度类型在VSQ/SQ下的最低载荷长度;
* 未知/变长类型、零对象及具体字段是否合法仍由业务检查,详见Parse_ASDU_Header。
* receive_serial与外层generation共同标识本次接收,不需要Release。 */
typedef struct {
uint64_t receive_serial; /* 本会话的接收帧编号,与对端协议N(S)不同。 */
uint16_t apdu_length; /* apdu中有效字节数,包含6字节APCI。 */
uint16_t send_number; /* 对端I帧携带的N(S)。 */
uint16_t receive_number; /* 对端I帧携带的累计确认N(R)。 */
uint8_t apdu[IEC104_MAX_APDU_LEN]; /* 接收原文副本,仅前apdu_length字节有效。 */
} IEC104_TcpIFrameEvent_t;
/* 发送事实的业务副本;frame_serial与事件generation共同标识一帧。
* 不公开ASDU字节环条目编号或Link发送描述,不能据此直接操作内部队列。 */
typedef struct {
uint64_t frame_serial; /* 本会话内的发送帧编号,与会回绕的协议N(S)不同。 */
uint64_t queued_at_ms; /* 准备I帧时的单调毫秒值,并非业务入队时刻。 */
uint64_t written_at_ms; /* local_send_completed为1时有效,允许0。 */
uint16_t send_number; /* 原I帧发送时使用的N(S)。 */
uint16_t receive_number; /* 原I帧捎带的N(R),不是查询时刻的最新接收位置。 */
uint16_t apdu_length; /* 原I帧总长度,包含APCI。 */
IEC104_AsduPriority_t priority; /* 原ASDU入队时的优先级。 */
uint8_t local_send_completed; /* 整帧已写出且链路完成提交成功;不代表对端已确认。 */
uint8_t peer_acknowledged; /* 链路确认,不表示对端业务已执行。 */
} IEC104_TcpSendRecord_t;
typedef struct {
IEC104_TcpSendRecord_t value; /* 原发送事实的独立副本,不附带ASDU原文或内部队列地址。 */
uint8_t closing; /* 0为正常ACK回执;1为收尾剩余记录,完成事实仍以value为准。 */
} IEC104_TcpRecordEvent_t;
/* 连接建立、激活、停用和断开共用此载荷,均是产生时的独立副本。
* ACTIVATED/DEACTIVATED的status_valid为1,data_transfer_started分别为1/0,socket_error为0。
* 启停完成后的同次协议推进若失败,快照可带fault,随后交付DISCONNECTED;不抹去已完成的变化。
* DISCONNECTED保留关闭协议前的阶段和故障,主动关闭时phase可能仍为ACTIVE。
* 其pending_send_records扣除本次收尾已成功入队的RECORD,全部转移成功时为0;不表示全部已ACK。
* 记录转移失败时保留未转移数量;状态无法可靠取得时status_valid为0。
* 任何历史事件均不代表当前仍连通,旧代次故障也不会带入后续新连接。 */
typedef struct {
IEC104_TcpConnectionStatus_t status; /* 连接事件产生时的协议状态;启停事件取自对应ACT受理后。 */
IEC104_TcpPeerInfo_t peer; /* 建连时保存的地址及代次;不受status_valid影响,断开后仍可用于日志。 */
int socket_error; /* 平台原生诊断码,只用于诊断,不用于跨平台分支。 */
uint8_t status_valid; /* 为1才能读取status;错误码0也可能伴随协议故障。 */
} IEC104_TcpConnectionEvent_t;
/* FIFO按通知的入队顺序逐条交付;全部载荷可按值复制,跨Process/Poll/Close保留。
* CONNECTED先于同代次其他事件,DISCONNECTED最后;启停变化不覆盖、不合并。
* 已确认回执在普通通知空间不足或本轮交付预算用完时暂留内部,可能晚于随后发生的启停通知交付。
* 回执积压不暂缓启停受理或CON发送;等待普通空间的已受理I帧仍先于回执交付。
* 启停专用槽满时暂缓受理后续改变状态的ACT;NextEvent释放槽后由Process重试,已有CON仍可发送。
* 初始STOPPED、同状态重复ACT和单纯断开均不额外产生启停事件;CON发送完成不产生启停通知。
* 全部通知均不覆盖,与NORMAL发送队列独立;普通通知不能占用启停及收尾预留。
* 同代次首次ACTIVATED先于后续合法I_FRAME交付;所有事件仍为历史事实,不代表当前状态。
* 新I帧无普通空间时保留原帧并暂停续收,消费通知后恢复;关闭时由收尾预留保存已受理数据。
* 使用事件generation提交应答,不可替换成新连接代次。 */
typedef struct {
IEC104_TcpEventType_t type; /* 决定data中哪个成员有效。 */
uint64_t generation; /* 来源服务内的会话代次;应答须使用原服务句柄和此值。 */
union {
IEC104_TcpIFrameEvent_t i_frame;
IEC104_TcpRecordEvent_t record;
IEC104_TcpConnectionEvent_t connection;
} data; /* I_FRAME读i_frame;RECORD读record;建立/激活/停用/断开读connection。 */
} IEC104_TcpEvent_t;
typedef struct {
char bind_address[16]; /* NUL结尾的数字IPv4,默认0.0.0.0。 */
uint16_t port; /* 默认2404;0由系统选择,Get_Status查询实际值。 */
IEC104_ProtocolConfig_t protocol; /* Init复制,业务不能修改运行中的配置。 */
} IEC104_TcpServerConfig_t;
/* 单个优先级ASDU队列的只读数值快照,不包含内部指针、条目头或队列下标。
* pending_asdu_count只统计尚未交给发送模块的队列条目,不包含已组帧、部分写出或等待ACK的帧。
* stored_asdu_count统计仍保留原文的条目;NORMAL含待发及已提交待ACK原文,HIGH交接后即出队。
* 因而HIGH的pending与stored相等;NORMAL的pending不大于stored,原文已覆盖的待ACK帧不计入二者。
* 字节数计入内部条目头及ASDU原文,不含APCI、独立发送缓冲或发送记录;used_bytes + free_bytes = capacity_bytes。
* 队列为空不表示主站已确认或业务已完成;查询不预留容量,提交是否接手仍以入队/应答返回值为准。 */
typedef struct {
uint32_t pending_asdu_count; /* 队列待发条目数;NORMAL包括离线缓存及EOF后留给下一连接的数据。 */
uint32_t stored_asdu_count; /* 当前仍保留原文的ASDU条目总数,不是累计入队数量。 */
uint32_t capacity_bytes; /* 本级字节环的总容量,单位字节。 */
uint32_t used_bytes; /* 当前全部保留条目占用的字节数,包含内部条目头。 */
uint32_t free_bytes; /* 当前未占用的字节数;NORMAL不足时仍可能通过覆盖旧原文入队。 */
} IEC104_TcpAsduQueueStatus_t;
/* 服务状态独立副本;关闭后bound_port为0,generation保留最近会话代次。
* connection_status_valid为1时,connection可以描述已关闭的最近会话。
* asdu_queue_status_valid为1时,high_queue/normal_queue是当前服务队列快照;与是否存在TCP连接无关。
* 尚未成功Init、Close后或队列状态异常时,队列有效标志为0且两份统计清零,不能据此判定队列为空。
* 接入诊断与客户端诊断独立;Close解除暂停但保留last_accept_error及连续失败计数,Init重置。
* 连续失败只统计Net_Accept的错误返回,OK/WOULD_BLOCK清零,不表示资源故障已经恢复。
* 当前TCP是否存在只看connected,不能从历史快照或generation自行推断。 */
typedef struct {
uint64_t generation; /* 最近分配的会话代次;同一静态槽的历史值可跨Create保留。 */
uint16_t bound_port; /* 实际监听端口;服务关闭后为0。 */
uint16_t pending_event_count; /* 可通过NextEvent继续取出的事件数量。 */
uint16_t pending_state_event_count; /* 其中未读的启停通知(ACTIVATED/DEACTIVATED)数。 */
uint8_t receive_paused; /* 1表示已受理I帧等待通知空间;不会因此主动断连,不代表STOPDT。 */
int last_socket_error; /* 历史后端诊断,不等同于本次API错误;0不能排除协议故障。 */
uint8_t listening; /* 1表示服务已初始化并保留监听资源。 */
uint8_t connected; /* 1表示当前仍持有客户端TCP连接,可能正在EOF发送收尾。 */
uint8_t connection_status_valid; /* 为1时才能读取下面的connection快照。 */
IEC104_TcpConnectionStatus_t connection; /* 当前或最近已关闭会话的状态。 */
uint8_t asdu_queue_status_valid; /* 为1时下列两级队列统计均有效;无连接但服务已Init时仍可为1。 */
IEC104_TcpAsduQueueStatus_t high_queue; /* 当前会话的应答队列;直接进入发送缓冲的应答不占此队列。 */
IEC104_TcpAsduQueueStatus_t normal_queue; /* 服务级主动上报队列;断线后仍可查询缓存积压。 */
uint8_t accept_paused; /* 1为当前仍在100毫秒接入暂停期;到期即为0,不依赖是否已有容量。 */
uint32_t accept_retry_remaining_ms; /* 接入暂停剩余毫秒数;未暂停为0,查询不刷新期限。 */
int last_accept_error; /* 最近接入错误;成功、暂无待接入连接或客户端断开均不清除。 */
uint32_t consecutive_accept_failures; /* 连续接入错误返回次数;UINT32_MAX饱和,OK/WOULD_BLOCK清零。 */
} IEC104_TcpServerStatus_t;
#endif /* IEC104_TCP_TYPES_H */
@@ -0,0 +1,58 @@
/*
* 104传输使用的值类型:APCI尺寸、标准应用层配置、优先级和链路配置。
* 本头文件依赖公共应用层值类型,不包含对象布局、socket或模块操作接口。
* 这些定义属于公开契约;内部队列、连接和发送状态不应放入本文件。
*/
#ifndef IEC104_TYPES_H
#define IEC104_TYPES_H
#include "IEC60870_Types.h"
#define IEC104_START_BYTE 0x68 /* APDU起始字节。 */
#define IEC104_APCI_LEN 6 /* 起始、长度及四字节控制域。 */
#define IEC104_MIN_LENGTH_FIELD 4 /* 长度域至少包含控制域。 */
#define IEC104_MAX_LENGTH_FIELD 253 /* 协议允许的长度域上限。 */
#define IEC104_MAX_APDU_LEN 255 /* 两字节头部加长度域。 */
#define IEC104_MAX_ASDU_LEN 249 /* 最大长度域减控制域。 */
#define IEC104_ASDU_HEADER_LEN 6 /* 标准104配置的头长;通用ASDU按参数计算。 */
#define IEC104_CA_BROADCAST 0xFFFFU /* 104两字节CA的广播地址;本站配置使用1..65534。 */
#define IEC104_SEQUENCE_MASK 0x7FFFU /* 序号按模32768回绕。 */
/* 标准104的应用层参数初始化值;类型使用IEC60870_AppLayerParameters_t。
* 此配置属于104接入,公共应用层不选择默认传输布局;调用方仍须显式传参。 */
#define IEC104_APP_LAYER_PARAMETERS_DEFAULT {1, 1, 2, 0, 2, 3, IEC104_MAX_ASDU_LEN}
/* HIGH标识当前连接的请求应答,包含直接准备及排队两种路径,不表示收到的主站请求。
* NORMAL是服务级低优先级事件,跨断线保留现存原文;空间不足时可能覆盖最旧原文。
* 提交成功不保证最终发送或业务执行;链路ACK与业务执行结果也不同。 */
typedef enum {
IEC104_ASDU_PRIORITY_HIGH = 0,
IEC104_ASDU_PRIORITY_NORMAL = 1,
IEC104_ASDU_PRIORITY_COUNT = 2 /* 数量哨兵,不可作为入队优先级。 */
} IEC104_AsduPriority_t;
#define IEC104_DEFAULT_K 12U
#define IEC104_DEFAULT_W 8U
#define IEC104_DEFAULT_T1_MS 15000U
#define IEC104_DEFAULT_T2_MS 10000U
#define IEC104_DEFAULT_T3_MS 20000U
/* 链路配置只保存数值;不创建定时器,不包含链路运行状态。 */
typedef struct {
uint16_t k; /* 未确认发送窗口,1..32767。 */
uint16_t w; /* 接收确认门限,1..32767。 */
uint32_t t1_ms; /* I帧确认及主动U帧CON的等待期限,非零。 */
uint32_t t2_ms; /* 接收未达w时延迟确认的期限,非零且必须小于t1_ms。 */
uint32_t t3_ms; /* 空闲后主动探测的期限,非零。 */
} IEC104_LinkConfig_t;
/* 业务通过TcpServer提交协议配置,内部Connection复用同一份值类型。
* 配置独立于运行对象,业务无需包含Connection的内部头文件。 */
typedef struct {
IEC104_LinkConfig_t link;
uint32_t local_send_timeout_ms; /* 一帧从准备到完整写出的总期限,非零。 */
uint32_t stop_timeout_ms; /* STOP受理到CON写完的总期限,非零。 */
uint16_t high_priority_burst_limit; /* 0严格HIGH优先;非零限制连续HIGH帧数。 */
} IEC104_ProtocolConfig_t;
#endif /* IEC104_TYPES_H */
@@ -0,0 +1,58 @@
/*
* 104计数量种类规则:解析C_CI_NA_1的QCC,限定RQT和读取数据类型,选择完成方式。
* 规则函数不保存状态、不访问网络;计数器存储及冻结/复位由应用提供。
* 创建和动作入口只委托共用引擎,避免另建一套任务、广播、期限及取消流程。
*/
#include "IEC104_CounterInterrogation.h"
#include "IEC104_Interrogation_Internal.h"
/* 从首对象取IOA和QCC;对象层已将低6位RQT与高2位FRZ拆开。
* 这里只解析,不校验是否支持该分组;失败时不写调用方的输出。 */
static bool Counter_Decode(const IEC60870_AsduInfo_t *info, uint32_t *ioa, uint8_t *qualifier, uint8_t *freeze)
{
IEC60870_CounterInterrogationCommand_t command;
if (IEC60870_Asdu_Get_Element(info, 0U, &command, sizeof(command)) != IEC60870_ASDU_OK) { return false; }
*ioa = command.object.ioa;
*qualifier = command.qualifier;
*freeze = command.freeze;
return true;
}
/* RQT=1..4为分组,5为总请求;0未用、6..63保留。具体分组是否配置由应用在BEGIN决定。 */
static bool Counter_Qualifier_Valid(uint8_t qualifier) { return qualifier >= 1U && qualifier <= 5U; }
/* 引擎准入后使用:总请求的数据COT为37,第1..4组依次为38..41。 */
static uint8_t Counter_Data_Cause(uint8_t qualifier)
{
return (uint8_t)(qualifier == 5U ? IEC60870_ASDU_COT_COUNTER_INTERROGATION :
IEC60870_ASDU_COT_COUNTER_INTERROGATION + qualifier);
}
/* 只有读取存在数据阶段;冻结/复位在肯定确认提交后直接完成。 */
static bool Counter_Has_Data(uint8_t freeze) { return freeze == 0U; }
/* 读取接受无时标Type15和CP56时标Type37;Type37仅SQ=0,Type16不用于104。
* 报文完整布局、对象数和头部一致性仍由共用引擎及ASDU层检查。 */
static bool Counter_Data_Allowed(const IEC60870_AsduInfo_t *info, uint8_t freeze)
{
return freeze == 0U && (info->type_id == IEC60870_ASDU_M_IT_NA_1 ||
(info->type_id == IEC60870_ASDU_M_IT_TB_1 && info->is_sequence == 0U));
}
/* 只读规则存活至进程结束;has_deactivation=false使COT=8按未知传送原因处理。 */
const IEC104_InterrogationRules_t IEC104_COUNTER_INTERROGATION_RULES = {
IEC60870_ASDU_C_CI_NA_1, false, Counter_Decode, Counter_Qualifier_Valid,
Counter_Data_Cause, Counter_Data_Allowed, Counter_Has_Data
};
/* 用计数量规则创建独立实例;配置、站点引用、重复实例和状态区重叠统一由引擎校验。 */
IEC104_Interrogation_t *IEC104_CounterInterrogation_Create(IEC104_Slave_t *slave,
const IEC104_InterrogationConfig_t *config)
{
return IEC104_Interrogation_Create_With_Rules(slave, config, &IEC104_COUNTER_INTERROGATION_RULES);
}
/* 传入本种类规则作为身份约束,防止误把总召句柄交给动作入口。 */
IEC104_InterrogationReplyResult_t IEC104_CounterInterrogation_Execute_Begin(
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event,
IEC104_CounterInterrogationAction_t action, void *context)
{
return IEC104_Interrogation_Execute_Action(self, event, &IEC104_COUNTER_INTERROGATION_RULES, action, context);
}
@@ -0,0 +1,48 @@
/*
* 104计数量召唤(C_CI_NA_1):读取冻结值,或由应用执行冻结/复位。
* 本模块提供种类规则和创建/动作入口;事件、调度、广播、期限与清理由共用引擎维护。
* 返回的句柄沿用Interrogation的驱动接口,所有调用在所属事件循环中串行执行。
*/
#ifndef IEC104_COUNTER_INTERROGATION_H
#define IEC104_COUNTER_INTERROGATION_H
#include "IEC104_Interrogation.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 创建计数量实例:复制配置、借用Slave;参数或容量不满足要求时返回NULL,不操作网络。
* 同一Slave可同时拥有总召和计数量各一个实例,须分别提供广播状态区并接收全部连接/启停通知。
* RQT=1..4选择分组,5选择全部;FRZ=0读取冻结值,接受Type15(SQ=0/1)或Type37(SQ=0),
* 数据COT使用request.data_cause;104计数量不接受带CP24时标的Type16。
* FRZ=1冻结,2冻结并复位,3仅复位;肯定ACT_CON提交后即完成,不生成NEED_DATA或ACT_TERM。
* gi_submitted在此实例统计完成响应的提交次数:读取为ACT_TERM,动作命令为ACT_CON。
* 不支持COT=8撤销,回COT=45;TEST冻结/复位请求否定且不执行动作。
* 销毁前先Cancel并读完FINISHED;连接关闭会同时影响共享该连接的其他业务。 */
IEC104_Interrogation_t *IEC104_CounterInterrogation_Create(IEC104_Slave_t *slave,
const IEC104_InterrogationConfig_t *config);
/* 同步动作回调:context为应用上下文,request为引擎保存的请求信息,仅在回调期间借用。
* true表示所选组的动作已完成;false必须保证未改变任何计数器,不能部分清零后报告失败。
* 应先检查整个组的资源和条件,再提交修改;动作须短时完成,不访问网络或执行阻塞I/O。
* 不得重入引擎或推进协议事件循环;真实计数器值、冻结值、序号和品质均由应用维护。 */
typedef bool (*IEC104_CounterInterrogationAction_t)(void *context,
const IEC104_InterrogationRequest_t *request);
/* 应答已取出的FRZ=1..3 BEGIN;event须原样传回,action非NULL,context原样传给回调。
* 先校验实例、事件、会话和期限;WAIT/STALE/ERROR均不调用action,也不会修改计数器。
* 校验通过后先消费BEGIN,再调用一次action;返回OK表示交互已消费,不表示动作一定成功。
* action返回true排队肯定确认,返回false排队否定确认;TEST请求直接否定,不调用action。
* 同一事件再次提交返回STALE;只保证本次BEGIN不重复执行,主站重发的新请求仍可能再次执行。
* 动作成功后等待肯定确认期间停止业务期限,避免计数器已改变却因排队超时改发否定确认。
* 传输超时、STOP、断线及本地Cancel仍可终结协议任务并交付FINISHED,但不回滚已执行动作。
* 本接口不管理异步外部电表事务;这类事务须另定义执行状态、迟到结果和重复请求策略。
* FRZ=0使用Reply_Begin及数据接口,整轮读取(含WAIT重试)应保持同一冻结快照。
* 动作命令没有NEED_DATA,拿其BEGIN调用Submit_Batch会返回STALE,不发送数据。 */
IEC104_InterrogationReplyResult_t IEC104_CounterInterrogation_Execute_Begin(
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event,
IEC104_CounterInterrogationAction_t action, void *context);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_COUNTER_INTERROGATION_H */
@@ -0,0 +1,54 @@
/*
* 104总召种类规则:C_IC_NA_1、QOI=20..36、无时标监视数据及主站撤销。
* 原IEC104_Interrogation_Create入口在此保留;调度、生命周期及发送由共用引擎维护。
*/
#include "IEC104_GeneralInterrogation.h"
#include "IEC104_Interrogation_Internal.h"
/* 解析首对象的IOA和QOI;总召没有FRZ,统一输出0供共用请求结构使用。 */
static bool General_Decode(const IEC60870_AsduInfo_t *info, uint32_t *ioa, uint8_t *qualifier, uint8_t *freeze)
{
IEC60870_InterrogationCommand_t command;
if (IEC60870_Asdu_Get_Element(info, 0U, &command, sizeof(command)) != IEC60870_ASDU_OK) { return false; }
*ioa = command.object.ioa;
*qualifier = command.qualifier;
*freeze = 0U;
return true;
}
/* QOI=20为站总召,21..36为16个分组;数据COT与合法QOI相同。 */
static bool General_Qualifier_Valid(uint8_t qualifier) { return qualifier >= 20U && qualifier <= 36U; }
static uint8_t General_Data_Cause(uint8_t qualifier) { return qualifier; }
/* 总召总是进入数据阶段;空点表也由应用Complete_Data结束,再发送ACT_TERM。 */
static bool General_Has_Data(uint8_t freeze) { (void)freeze; return true; }
/* 保持原总召的9种无时标监视类型,SQ=0/1由ASDU层校验;累计量由计数量实例处理。 */
static bool General_Data_Allowed(const IEC60870_AsduInfo_t *info, uint8_t freeze)
{
(void)freeze;
switch (info->type_id) {
case IEC60870_ASDU_M_SP_NA_1:
case IEC60870_ASDU_M_DP_NA_1:
case IEC60870_ASDU_M_ST_NA_1:
case IEC60870_ASDU_M_BO_NA_1:
case IEC60870_ASDU_M_ME_NA_1:
case IEC60870_ASDU_M_ME_NB_1:
case IEC60870_ASDU_M_ME_NC_1:
case IEC60870_ASDU_M_PS_NA_1:
case IEC60870_ASDU_M_ME_ND_1: return true;
default: return false;
}
}
/* 总召支持COT=8撤销;规则只负责声明能力,匹配请求及撤销清理由引擎执行。 */
const IEC104_InterrogationRules_t IEC104_GENERAL_INTERROGATION_RULES = {
IEC60870_ASDU_C_IC_NA_1, true, General_Decode, General_Qualifier_Valid,
General_Data_Cause, General_Data_Allowed, General_Has_Data
};
/* 保留旧公开函数名和签名;工程需同时编译本文件与共用引擎。 */
IEC104_Interrogation_t *IEC104_Interrogation_Create(IEC104_Slave_t *slave,
const IEC104_InterrogationConfig_t *config)
{
return IEC104_Interrogation_Create_With_Rules(slave, config, &IEC104_GENERAL_INTERROGATION_RULES);
}
@@ -0,0 +1,9 @@
/*
* 104总召(C_IC_NA_1)的种类入口。
* 创建函数仍名为IEC104_Interrogation_Create,声明和共用驱动接口由下列头文件提供。
* 只使用总召的工程无需链接CounterInterrogation.c。
*/
#ifndef IEC104_GENERAL_INTERROGATION_H
#define IEC104_GENERAL_INTERROGATION_H
#include "IEC104_Interrogation.h"
#endif
@@ -0,0 +1,326 @@
/*
* 104召唤共用引擎:具体种类规则由GeneralInterrogation/CounterInterrogation创建入口提供。
* 总召旧入口Create保持兼容;计数量召唤入口见IEC104_CounterInterrogation.h。
* 借用一个Slave,不创建socket/线程,不保存业务回调;动作入口仅同步借用回调执行一次。
* 所有接口在同一事件循环串行调用,动作回调不得重入引擎。
* 先把Slave通知按FIFO交给Handle_Event,读空后Process,再NextEvent并显式应答。
* 事件来源和会话状态由Slave公共接口校验;本模块维护召唤阶段、业务期限和应用事件。
* 退出顺序:Cancel -> 读完业务FINISHED并清理应用资源 -> Destroy -> Destroy Slave。
*/
#ifndef IEC104_INTERROGATION_H
#define IEC104_INTERROGATION_H
#include "IEC104_Slave.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 不透明静态池句柄,只能由Create取得、Destroy释放,不能自行分配、复制对象或free。
* 销毁成功后停止使用旧句柄;同一实例的全部接口须在所属事件循环中串行调用。 */
typedef struct IEC104_Interrogation IEC104_Interrogation_t;
/* 请求信息随事件按值复制,不暴露可发送的connection。
* 应用据此选择数据源、构造数据头;IOA、分组成员及遍历状态均由应用维护。
* 引擎不登记或冻结IOA列表;总召由应用逐批取当前值,计数量读取由应用固定本轮冻结快照。
* 冻结/复位命令在BEGIN执行应用动作,不进入NEED_DATA。 */
typedef struct {
uint64_t generation, receive_serial; /* 原TCP会话代次、接收I帧标识,供应用关联和诊断。 */
uint64_t station_id; /* 本次选中的注册编号,兼容裸CA模式为0;按编号查找避免同CA替换串站。 */
uint16_t common_address; /* 实际本站地址;广播65535解析为此地址,应用数据批次须使用同一CA。 */
uint8_t originator_address; /* 请求的源发地址OA,应答数据须原样保留。 */
uint8_t qualifier; /* 总召QOI=20..36;计数量RQT=1..5,应用维护分组点表。 */
bool is_test; /* 请求的TEST标志,数据批次须保持一致。 */
uint8_t max_objects_per_asdu; /* 1..127,实际另受249字节容量限制。 */
uint8_t freeze; /* 计数量FRZ:0读取,1冻结,2冻结并复位,3仅复位;总召恒为0。 */
uint8_t data_cause; /* 数据COT:总召等于QOI,计数量总请求37、分组38..41。 */
} IEC104_InterrogationRequest_t;
/* FINISHED通知应用清理本次游标及业务资源;终结原因不等于网络发送回执。
* COMPLETED表示结束帧被发送层接手,不表示写完或收到ACK;读取为ACT_TERM,动作命令为ACT_CON。 */
typedef enum {
IEC104_INTERROGATION_FINISH_NONE = 0, /* 未终结;BEGIN/NEED_DATA事件也使用此值。 */
IEC104_INTERROGATION_FINISH_COMPLETED, /* 读取的全部批次和ACT_TERM,或动作命令的ACT_CON已提交。 */
IEC104_INTERROGATION_FINISH_REJECTED, /* 应用拒绝;否定ACT_CON可能仍在排队。 */
IEC104_INTERROGATION_FINISH_CANCELLED, /* 应用调用本地Cancel放弃任务。 */
IEC104_INTERROGATION_FINISH_STOPPED, /* 收到Slave的停用通知,结束本次数据传输。 */
IEC104_INTERROGATION_FINISH_DISCONNECTED, /* 原TCP会话断开。 */
IEC104_INTERROGATION_FINISH_CONNECTION_CHANGED, /* 连接代次或当前传输状态已不再适用于任务。 */
IEC104_INTERROGATION_FINISH_SOURCE_ERROR, /* 应用通过Fail_Request报告数据源准备/取数失败。 */
IEC104_INTERROGATION_FINISH_SEND_ERROR, /* 提交应答失败且不是可等待的发送容量不足。 */
IEC104_INTERROGATION_FINISH_OVERLOAD, /* 兼容保留;满槽现在拒绝/丢弃新请求,不终结已有任务。 */
IEC104_INTERROGATION_FINISH_INVALID_REQUEST, /* 保留枚举值兼容;畸形新请求不再终结已有总召。 */
IEC104_INTERROGATION_FINISH_SERVICE_ERROR, /* 服务查询、内部状态或构造应答失败。 */
IEC104_INTERROGATION_FINISH_DEACTIVATED, /* 主站COT=8撤销;COT=9可能仍待发送。 */
IEC104_INTERROGATION_FINISH_BEGIN_TIMEOUT, /* BEGIN入队后未在期限内得到应用决定。 */
IEC104_INTERROGATION_FINISH_DATA_TIMEOUT, /* NEED_DATA入队后未在期限内得到应用应答。 */
IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT /* BEGIN生成至ACT_TERM提交超过执行整体期限。 */
} IEC104_InterrogationFinishReason_t;
typedef enum {
IEC104_INTERROGATION_BEGIN = 1, /* 检查数据源并接受/拒绝;取数游标可延至首个NEED_DATA建立。 */
IEC104_INTERROGATION_NEED_DATA, /* 应用选择本批IOA并读取当前值/品质;暂未就绪时保存事件。 */
IEC104_INTERROGATION_FINISHED /* 清理游标及业务资源,无需应答。 */
} IEC104_InterrogationEventType_t;
/* 纯值事件,无借用指针和自引用,可赋值、排入应用队列、保存到后续循环。
* 字段只读,应答时原样传回;实例/请求/批次标识用于拒绝迟到和重复应答。
* 每个已取出的BEGIN恰好配对一次FINISHED,取消也不丢失终结通知。
* BEGIN尚未取出便取消时移除BEGIN,不产生FINISHED(应用尚未建立资源)。
* 取消移除队列中的NEED_DATA;已取出的旧事件仍可读取,应答返回STALE。 */
typedef struct {
IEC104_InterrogationEventType_t type; /* 决定调用Reply_Begin、提交数据或只清理资源。 */
uint64_t instance_id, request_id; /* 实例及请求身份;必须与事件其他字段一起原样传回。 */
IEC104_InterrogationRequest_t request; /* 原激活请求的元数据,FINISHED仍保留此副本。 */
size_t batch_index, objects_submitted; /* 已被发送层接手的批次/对象数;首个NEED_DATA的批次号为0。 */
IEC104_InterrogationFinishReason_t reason; /* 仅FINISHED有意义。 */
} IEC104_InterrogationEvent_t;
/* 多站广播的调用方存储:每个目标站一项,不保存点值、ASDU或独立发送任务。
* config借用整个数组至Destroy,期间地址稳定、不得读写或与其他实例重叠。
* 模块初始化/清理内部字段,不要求调用方逐项初始化。 */
typedef struct {
IEC104_Station_t *_station; /* 本轮持有的原节点引用,直到整轮终结事件读完才释放。 */
uint64_t _request_id, _started_ms; /* 每站独立的请求身份和BEGIN时刻,切换当前站不丢失。 */
size_t _batch_index, _objects_submitted; /* 已提交进度,不是应用的IOA遍历游标。 */
uint8_t _state, _flags, _reason; /* 协议阶段、事件交付/撤销标志、终结原因,仅库内部使用。 */
} IEC104_InterrogationStationState_t;
/* Create复制此配置;创建后修改调用方对象不会改变已有实例,指向的广播数组仍为借用。
* 接受/取数期限从事件入队开始,包含应用尚未NextEvent读取它的时间。 */
typedef struct {
uint16_t common_address; /* station为NULL时的兼容站地址1..65534;绑定站点时从节点取得CA并忽略此值。 */
uint8_t max_objects_per_asdu; /* 每批1..127个对象,配置无业务函数指针。 */
bool use_station_registry; /* true按请求CA查共用站点表,广播分三阶段;station须NULL,common_address忽略。 */
uint32_t begin_timeout_ms; /* BEGIN入队至Reply_Begin/Fail_Request,默认15000ms。 */
uint32_t data_timeout_ms; /* NEED_DATA至首次有效批次/Complete/Fail,默认15000ms;发送WAIT后的重试不计。 */
uint32_t total_timeout_ms; /* 读取:每站BEGIN至ACT_TERM提交;动作:BEGIN至应用接受。默认0禁用。 */
/* 上述期限是业务策略,独立于104链路t0..t3;每项0表示禁用该项。
* BEGIN尚未生成时不计业务期限;START/CONFIRM超时转为否定ACT_CON并保留连接。
* 动作命令接受后的CONFIRM不再受业务期限约束,避免执行成功后被改发否定;连接期限仍生效。
* 肯定ACT_CON被发送层接手后,DATA/TERMINATE超时关闭原连接,不发送成功ACT_TERM。
* 同一执行任务的总期限与阶段期限同时到点时优先报告总超时。 */
IEC104_Station_t *station; /* 兼容固定站绑定:Create持有至Destroy;删除后不接新请求,已接受任务可完成。 */
IEC104_InterrogationStationState_t *broadcast_states; /* 调用方独占提供的状态数组,寿命覆盖实例。 */
size_t broadcast_capacity; /* 数组项数,即单轮多站广播能接纳的目标数,不是注册站数上限。 */
/* 默认NULL/0。广播涉及多个站时,容量须覆盖请求时的全部注册站,否则整轮否定确认。
* 单站/定向总召不需要数组。容量由应用决定,不限制注册站数;数组含已确认目标及终结记录。 */
} IEC104_InterrogationConfig_t;
/* 输入Slave通知的处理结果;其他类型I帧返回IGNORED,交后续业务处理。 */
typedef enum {
IEC104_INTERROGATION_EVENT_ERROR = -1, /* 参数、复制、关闭连接等处理失败。 */
IEC104_INTERROGATION_EVENT_IGNORED = 0, /* 本模块未接手,可转交其他业务处理器。 */
IEC104_INTERROGATION_EVENT_HANDLED = 1 /* 已处理,含畸形/满槽丢弃;不保证应用接受或已发送响应。 */
} IEC104_InterrogationEventResult_t;
typedef enum {
IEC104_INTERROGATION_NEXT_ERROR = -1, /* 非法句柄或NULL输出。 */
IEC104_INTERROGATION_NEXT_EMPTY = 0, /* 当前无业务事件,不代表总召已经结束。 */
IEC104_INTERROGATION_NEXT_READY = 1 /* 已复制并移除最早事件,输出可独立保存。 */
} IEC104_InterrogationNextResult_t;
/* 显式比较,不能按真假判断。
* OK:交互已消费;Submit_Batch已交发送层并固定顺序,其余应答由Process推进;不表示写完/ACK。
* WAIT:先处理Slave通知并推进网络;Submit_Batch还可能因发送背压WAIT,不缓存本次原文。
* 保留同一事件,批次游标不动,下次重读当前值组包。首次有效批次的发送WAIT结束本批取数计时。
* STALE:实例/请求/批次/类型不匹配、未取出、已应答、已取消或已超时;不要重试旧事件。
* ERROR:参数/批次/服务查询错误,不消费交互,可修正再提交。
* NEED_DATA阶段Fail_Request例外:即使关闭连接失败,仍取消并交付FINISHED,不能重试。 */
typedef enum {
IEC104_INTERROGATION_REPLY_STALE = -2,
IEC104_INTERROGATION_REPLY_ERROR = -1,
IEC104_INTERROGATION_REPLY_WAIT = 0,
IEC104_INTERROGATION_REPLY_OK = 1
} IEC104_InterrogationReplyResult_t;
/* 优先描述唯一未终结总召的诊断阶段;仅有应答时REPLY,无任务时IDLE。
* 阶段由模块推进,应用通过事件交互,不直接修改。 */
typedef enum {
IEC104_INTERROGATION_IDLE, /* 没有协议任务。 */
IEC104_INTERROGATION_REPLY, /* 等待提交单帧确认(含撤销确认)或错误应答。 */
IEC104_INTERROGATION_START, /* 等事件空位生成BEGIN,或等待应用接受/拒绝;两者均占用总召资格。 */
IEC104_INTERROGATION_CONFIRM, /* 应用已接受,等待提交肯定ACT_CON。 */
IEC104_INTERROGATION_DATA, /* 生成NEED_DATA,等待应用取值并尝试直接提交。 */
IEC104_INTERROGATION_TERMINATE, /* 应用声明数据结束,等待提交ACT_TERM。 */
IEC104_INTERROGATION_FINISHING /* 多站广播网络任务已结束,等NextEvent读完各站FINISHED后归还状态区。 */
} IEC104_InterrogationPhase_t;
/* Get_Status返回的独立副本;计数自Create起累计,Cancel和重连不清零。
* active/waiting反映最近已处理的通知/推进结果;events_pending及need_process在查询时计算。 */
typedef struct {
uint64_t generation; /* 已消费的连接代次,断线后0。 */
bool active; /* 已消费ACTIVATED且未停用,不是socket实时状态。 */
bool gi_pending; /* 总召网络流程尚未终结;FINISHING虽为false,读完终结事件前仍拒绝新激活。 */
bool waiting; /* 存在发送、应用或事件空位等待;总召等应用时仍可推进单帧应答。 */
bool application_waiting; /* 等待应用首次有效应答,包含事件尚未取出;发送背压另见data_send_waiting。 */
bool data_send_waiting; /* 有效批次提交曾遇发送WAIT;应用仍持有NEED_DATA,网络推进后重取再试。 */
bool events_pending; /* 当前Slave FIFO未空,须先处理网络通知。 */
bool need_process; /* 有本地工作/到期/会话失效待处理,应立即Process;未读Slave通知优先。 */
bool timer_active; /* 存在启用的业务期限;为false时timeout_remaining_ms无意义。 */
uint32_t timeout_remaining_ms; /* 最近期限距当前单调时钟的毫秒数;0表示应立即Process。 */
unsigned pending_requests, pending_events; /* 任务数含FINISHING;事件数含状态区中待交付的FINISHED,不含预留。 */
IEC104_InterrogationPhase_t phase; /* 当前站阶段;收尾为FINISHING,仅有应答为REPLY,无任务为IDLE。 */
size_t batch_index, objects_submitted; /* 活动总召已提交的批次/对象数;仅有应答时取最早应答保留的进度。 */
unsigned long submitted, gi_submitted; /* 接手ASDU数、完成协议提交的召唤数;动作命令以ACT_CON计。 */
unsigned long waits, cancelled, rejected, overloads;
/* waits统计发送层返回WAIT;cancelled统计取消的协议任务(含单帧应答);
* rejected统计校验拒绝(含无法应答的畸形请求)/总召忙/应用/确认前失败/容量拒绝;
* overloads含任务槽满或广播状态区不足。 */
unsigned long negative_submitted, dropped_requests; /* 已提交否定应答数、无法保存或提交而丢弃的新请求数。 */
unsigned long deactivated, timeouts; /* 成功匹配的主站撤销、执行阶段业务超时次数。 */
IEC104_InterrogationFinishReason_t last_finish_reason; /* 最近合法总召的终结原因,含BEGIN尚未交付的任务。 */
} IEC104_InterrogationStatus_t;
/* 初始化调用方的可写配置:station=NULL、兼容CA=1,broadcast_states=NULL、broadcast_capacity=0,每批最多127个对象,
* 接受/取数期限各15000ms,执行整体期限为0(禁用);NULL无操作。 */
void IEC104_Interrogation_Config_Default(IEC104_InterrogationConfig_t *config);
/* 复制配置,借用Slave;同一Slave的同一种类只能绑定一个实例,在消费Slave通知前创建。
* slave须保持有效直到本实例Destroy;config在返回后可释放或复用。
* station非NULL时校验其属于本Slave并占用节点,本站CA只取节点值;创建失败不泄漏引用。
* use_station_registry开启时按CA选站;广播共用一个任务,依次完成全部确认、各站数据、全部终止。
* 各站独立BEGIN/FINISHED和request_id;多目标广播须有足够broadcast_states,容量不足整轮否定。
* 接收时持有目标节点引用;尚未选入BEGIN便删除的站跳过,已经选中的站持原引用完成。
* 新增/替代节点不加入本轮;已确认站即使尚未取数或被删除,也必须完成或明确终结。
* BEGIN后需跨循环保留context的应用须按station_id查节点并Acquire,FINISHED清理后Release。
* 兼容模式use_station_registry=false保留单CA或固定station行为。
* NULL:参数非法、重复绑定、状态区重叠、池满或实例编号耗尽;无网络操作,无malloc。
* 编译.c可覆盖IEC104_INTERROGATION_CAPACITY(默认为TCP实例容量的两倍)、
* IEC104_INTERROGATION_TASK_CAPACITY(默认4)、IEC104_INTERROGATION_EVENT_CAPACITY
* (默认4,最小2)。任务池容纳至多一个总召及短应答,不是多个总召的等待队列。
* 单目标流程在FIFO预留FINISHED;多目标用每站状态区保留终结记录,NextEvent按空位交付。 */
IEC104_Interrogation_t *IEC104_Interrogation_Create(IEC104_Slave_t *slave,
const IEC104_InterrogationConfig_t *config);
/* 协议任务和业务事件均为空才释放槽,否则false并保持原状态。
* 先Cancel,再NextEvent读完FINISHED并清理应用资源,最后Destroy;成功后句柄失效。 */
bool IEC104_Interrogation_Destroy(IEC104_Interrogation_t *self);
/* 放弃未提交任务;保留已交付BEGIN的FINISHED(CANCELLED),移除未交付工作事件。
* 不撤回已接手帧、不补发ACT_TERM,也不回滚已执行的冻结/复位;非法句柄无操作。 */
void IEC104_Interrogation_Cancel(IEC104_Interrogation_t *self);
/* 按FIFO接收本Slave的每个通知,不消费Slave FIFO,不保存调用方指针。
* 下列QOI和撤销约定针对总召;计数量的RQT/FRZ规则见CounterInterrogation.h。
* 同时使用两个实例时,连接/启停通知交给两边;I帧仅在当前实例IGNORED后转交下一个。
* 总召须SQ=0、一个对象、COT=6或8、P/N=0、CA为本站或65535、IOA=0、QOI=20..36。
* 广播常量为IEC104_CA_BROADCAST;应用事件、确认、数据、否定应答及终止均使用实际本站CA。
* 原输入事件不修改;每次选站设置应答CA,同一站发送及重试沿用该地址。
* 空闲时保存一个COT=6激活并进入START;从保存起至终结,拒绝所有后续激活,不区分QOI。
* 忙或QOI/P/N非法时回否定ACT_CON;保留已有请求的QOI/OA/TEST、工作事件及数据进度。
* 单帧应答在无未读Slave通知、原会话有效且无更早REPLY时尝试立即提交,未接手再保存。
* 多站广播应答先保存一个遍历任务,再由Process逐站提交;无槽时整体丢新请求并计数。
* 满槽时尝试否定确认;无法安全提交/保存则仅丢新请求并累计dropped_requests,不断链。
* 撤销COT=8不受忙拒绝限制,按连接代次、实际站CA、OA、TEST、QOI匹配当前总召;
* 定向撤销停止匹配站,包括已确认但尚未取数/等待终止的站;其他站继续。
* 尚未选入BEGIN的站没有可撤销的活动事务。广播撤销停止整轮,按各活动站CA确认。
* 已完成/已拒绝站不补发撤销确认;无注册站且无匹配事务时,广播请求回复未知CA。
* 为已交付BEGIN保留FINISHED(DEACTIVATED),按单帧应答规则提交肯定COT=9;
* 不匹配/非法QOI或P/N回否定COT=9。
* 撤销无需应用再次批准;应用收到FINISHED后停止采样并释放本次资源。
* 匹配撤销复用原任务槽,并将确认排到单帧应答队尾;满池也不需要额外槽。
* 一组撤销确认尚未提交完时,重叠的新撤销按未匹配拒绝,不覆盖已有确认。
* COT/CA/IOA错误回45/46/47;撤销不撤回发送层已接手的帧,不补发ACT_TERM。
* 尚未生成/交付BEGIN的撤销不产生FINISHED;已有BEGIN交付才通知应用清理。
* 先安全读取Type,非100返回IGNORED;公共视图不可用也返回IGNORED,由公共入口处理。
* Type100仅一个对象且IOA/QOI完整时,SQ=1或多余尾字节回否定7/9(COT/CA/IOA错误优先45/46/47)。
* 此应答规范为10字节、VSQ=1,保留OA/TEST/IOA/QOI;绝不据规范化副本执行激活或撤销。
* 零对象/多对象等无法可靠对应请求的布局错误计入rejected并返回HANDLED,不生成应答。
* 上述请求错误不取消已有总召、不刷新其期限;基础帧错误仍由Receiver/连接层处理。
* STOPDT/断线取消总召及待发短应答并保留终结事件;不替其他业务回答UNKNOWN_TYPE。 */
IEC104_InterrogationEventResult_t IEC104_Interrogation_Handle_Event(
IEC104_Interrogation_t *self, const IEC104_SlaveEvent_t *event);
/* 自动推进当前就绪任务,不等待应用或网络;调用方不需要指定处理步数。
* Slave FIFO未空仅设置events_pending。顺序:START -> BEGIN -> 应用接受 -> ACT_CON ->
* NEED_DATA/提交批次(可重复)-> 应用Complete_Data -> ACT_TERM -> FINISHED。
* 动作命令例外:ACT_CON被发送层接手后直接FINISHED,不生成NEED_DATA或ACT_TERM。
* 多目标广播先完成所有站的BEGIN/ACT_CON,再逐站NEED_DATA,全部数据结束后才逐站ACT_TERM。
* 工作事件只交付一次;应用暂未就绪可保存事件,后续Process不会重复请求。
* 单帧应答保持FIFO,可在总召等待应用/业务事件空位时提交;双方可推进时先应答再总召轮转,
* 继续处理当前就绪任务,直至空闲、所有任务等待应用/事件空位,或发送返回WAIT。
* 每个站的REPLY成功后继续广播或释放任务;不保存后续主站新发来的总召激活。
* 每轮最多推进2倍任务容量步,广播余下站点由need_process提示继续,避免大量站点独占事件循环。
* 应答可穿插总召各批次,本次总召自身ACT_CON/数据/ACT_TERM顺序不变。
* 确认、终止和短应答沿原connection走HIGH,WAIT保留请求;DATA只生成NEED_DATA,
* 由应用Submit_Batch直接提交,OK才增加批次/对象计数,WAIT须重新取值后重试。
* WAIT让出本轮,下轮优先检查单帧应答;已被发送层接手的帧不被后到应答越过。
* 发送ERROR时复核原连接;正常STOP拒绝且通知未读时仅置events_pending并让出,由通知终结为STOPPED。
* 不因此增加waits、重置期限或撤销已接手帧;其他ERROR及FATAL仍按SEND_ERROR处理。
* 使用平台单调时钟检查期限,先处理Slave通知;START/CONFIRM超时转为否定REPLY并继续调度,
* 肯定ACT_CON提交后的DATA/TERMINATE超时关闭原连接;FINISHED保留具体超时原因。
* 执行总期限默认禁用,启用后按每站BEGIN独立开始,含等待其他站完成的时间。
* 已确认站总期限到点即关闭原连接;此前等待事件空位不计期限,应及时读FINISHED。
* 网络推进并读空Slave通知后须Process重试,即使need_process为false;发送WAIT不自行触发空转。
* 数据WAIT还须重试应用持有的NEED_DATA;本函数不会代为重读或重发数据。
* 本地应答/事件消费后重查Get_Status,need_process为true不得休眠;并用剩余期限限制等待时长。
* true本轮正常(可等待/取消/超时);false参数、组包、关闭连接或服务查询失败。 */
bool IEC104_Interrogation_Process(IEC104_Interrogation_t *self);
/* 取最早业务事件;event须为非NULL输出,只有READY写入,EMPTY/ERROR保持输出不变。
* READY后工作事件才允许应答;FINISHED仅用于应用清理,不调用应答接口。
* 多站终结记录超过FIFO容量时,本函数按需补入,连续NextEvent即可读完,无需Process。
* 最后一条FINISHED出队后归还广播状态区和节点引用;应用持有的节点引用仍须自行Release。
* 事件是可保存的纯值副本,本函数无网络操作,也不自动执行应用代码。 */
IEC104_InterrogationNextResult_t IEC104_Interrogation_NextEvent(
IEC104_Interrogation_t *self, IEC104_InterrogationEvent_t *event);
/* 应答已取出的BEGIN。true允许提交ACT_CON;false立即FINISHED(REJECTED),
* 并排队否定ACT_CON;不支持的分组可在这里拒绝。应用按instance_id/request_id管理遍历状态。
* 接受前检查数据源及业务资源;无需登记完整IOA列表或预先采样全部数据。
* event须原样传回;OK表示接受/拒绝决定已消费,实际ACT_CON由后续Process提交。
* 所有应答接口也检查期限,不能通过迟到应答绕过超时;Slave通知未读空时仍优先返回WAIT。
* 计数量FRZ!=0建议使用CounterInterrogation_Execute_Begin,在校验通过后才执行副作用。
* 不要在本函数之前无条件执行冻结/复位:WAIT会重试,STALE也不能撤销已经发生的动作。
* 动作命令接受后停止业务期限,保留确认等待;TEST动作始终拒绝。 */
IEC104_InterrogationReplyResult_t IEC104_Interrogation_Reply_Begin(
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, bool accept);
/* 应答已取出的NEED_DATA,验证非空ASDU后当场尝试不排队提交。
* OK表示已复制进发送缓冲并固定发送顺序,当场累计批次/对象数;输入缓冲及参数可立即复用。
* WAIT不接手、不推进游标;应用保存同一事件,网络推进后重新组包,不保留旧编码等待重发。
* 总召可重取当前值;计数量必须保持本轮冻结版本,值、序号、品质及可选时标一起固定。
* 首次有效批次发生发送WAIT后停止本批取数计时;下一NEED_DATA重新计时,显式整体期限仍有效。
* 参数须2/2/3,COT=request.data_cause、P/N=0、CA=本事件目标站地址、OA/TEST与事件相同。
* 广播请求的数据CA也不能使用65535;对象数不超配置。
* 总召接受Type 1/3/5/7/9/11/13/20/21,SQ=0/1均可;计数量读取接受Type 15(SQ=0/1)、37(SQ=0)。
* IOA及分组归属由调用方决定;库校验编码布局,不规定点表或强制固定IOA集合。
* 总召建议每次NEED_DATA读取当前值/品质,整轮不保证同一采样时刻;计数量由应用保证快照一致。
* 点值更新及突发发布、总召取值及本调用须由同一发布流程串行协调,不能迟交旧事件。
* HIGH/NORMAL有待发数据时总召退让;持续事件拥塞可能延后总召。OK后不再重新取值或重复提交。
* 发送ERROR若确认为原连接正常STOP拒绝且有未读通知,返回WAIT并保留事件,先处理DEACTIVATED。
* 此等待不设置data_send_waiting、不停止取数计时;通知处理后旧事件STALE,FINISHED为STOPPED。
* 其他发送ERROR/FATAL取消请求并返回STALE;FATAL可能已发出字节,不允许重试旧批次。
* 非法批次ERROR,保留NEED_DATA供修正,不断线;无法继续则Fail_Request。
* 应用使用Asdu_Create/InformationObject_Create/Add自行构造业务ASDU。 */
IEC104_InterrogationReplyResult_t IEC104_Interrogation_Submit_Batch(
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event,
const IEC60870_Asdu_t *asdu);
/* 应答已取出的NEED_DATA,声明数据耗尽;不以空ASDU作为结束标记。
* OK后本请求不再取数,可归还其取数游标;多目标广播继续下一站数据,全部结束后才发终止。
* Process提交本站ACT_TERM成功后才交付FINISHED(COMPLETED)。 */
IEC104_InterrogationReplyResult_t IEC104_Interrogation_Complete_Data(
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event);
/* 应答已取出的BEGIN/NEED_DATA,报告数据源失败,两者均交付FINISHED(SOURCE_ERROR)。
* BEGIN阶段复用原槽保存否定ACT_CON,保留连接和其他任务;OK不表示否定确认已发送,须继续Process。
* NEED_DATA阶段肯定ACT_CON已经提交:关闭原连接并取消任务,不发送成功终止,不影响后来新代次。
* 检查通过后即终结交互;即使NEED_DATA阶段关闭失败返回ERROR,任务仍已取消,不能重试。 */
IEC104_InterrogationReplyResult_t IEC104_Interrogation_Fail_Request(
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event);
/* 取得独立状态副本,实时查询Slave通知、可推进任务和最近期限;不消费事件或触发I/O/超时。
* need_process表示无需等新网络事件就应尝试推进,不保证发送接手,也不是waiting的反值。
* 生成工作事件有空位、应答接手后可提交下一帧、业务期限到期时置位;Slave通知未读空时不置位。
* 当前帧提交WAIT后停止本地续跑,网络推进后仍须调用Process重试;不能仅凭本标志决定是否处理网络进展。
* data_send_waiting时应用仍须在网络推进后重取并重试保留的NEED_DATA,不能等新事件或need_process置位。
* pending_events非零先消费业务事件;释放空位或完成应答后须重查,不能沿用旧状态休眠。
* timer_active时,调用方等待时长不得超过timeout_remaining_ms;false时不限制等待。
* 返回false为非法句柄/NULL,输出保持不变。 */
bool IEC104_Interrogation_Get_Status(const IEC104_Interrogation_t *self,
IEC104_InterrogationStatus_t *status);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_INTERROGATION_H */
@@ -0,0 +1,39 @@
/*
* 104共用召唤引擎与种类文件之间的私有接口,不供业务直接调用。
* 规则函数必须无状态:不访问网络、不保存业务进度、不回调引擎。
* 规则表在实例整个寿命内保持有效且只读;现有两种请求共用6字节公共头、3字节IOA和1字节限定词。
*/
#ifndef IEC104_INTERROGATION_INTERNAL_H
#define IEC104_INTERROGATION_INTERNAL_H
#include "IEC104_Interrogation.h"
typedef struct {
uint8_t type_id; /* 请求类型,用于入口分派和应答副本校验。 */
bool has_deactivation; /* 是否受理COT=8撤销;不支持时按未知传送原因应答。 */
/* 解析首对象,输出IOA、种类限定词及FRZ;总召FRZ恒为0,失败不改输出。 */
bool (*decode)(const IEC60870_AsduInfo_t *info, uint32_t *ioa, uint8_t *qualifier, uint8_t *freeze);
/* 校验协议允许的限定词范围;应用是否配置该组留到BEGIN判断。 */
bool (*qualifier_valid)(uint8_t qualifier);
/* 返回准入请求应使用的数据COT;只读元数据,不改变原请求。 */
uint8_t (*data_cause)(uint8_t qualifier);
/* 校验数据类型、SQ及动作是否允许携带数据;公共头和容量由引擎另行检查。 */
bool (*data_allowed)(const IEC60870_AsduInfo_t *info, uint8_t freeze);
bool (*has_data)(uint8_t freeze); /* false表示肯定ACT_CON提交后直接完成,不产生数据事件或ACT_TERM。 */
} IEC104_InterrogationRules_t;
/* 种类文件各自定义,只读规则也供不经过网络的规则单测使用;引擎不引用这两个具体符号。 */
extern const IEC104_InterrogationRules_t IEC104_GENERAL_INTERROGATION_RULES;
extern const IEC104_InterrogationRules_t IEC104_COUNTER_INTERROGATION_RULES;
/* 复制配置并借用rules;校验全部规则回调,同一Slave与同一规则表只能创建一个实例。
* 其他创建/存储契约与公开Create一致,失败返回NULL,不触发网络操作。 */
IEC104_Interrogation_t *IEC104_Interrogation_Create_With_Rules(IEC104_Slave_t *slave,
const IEC104_InterrogationConfig_t *config, const IEC104_InterrogationRules_t *rules);
/* 动作入口的内部实现:额外匹配rules身份,校验并消费BEGIN后才调用一次同步action。
* WAIT/STALE/ERROR不调用action;TEST不执行动作而生成否定确认。成功与失败均不保留回调指针。 */
IEC104_InterrogationReplyResult_t IEC104_Interrogation_Execute_Action(
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event,
const IEC104_InterrogationRules_t *rules,
bool (*action)(void *context, const IEC104_InterrogationRequest_t *request), void *context);
#endif
@@ -0,0 +1,67 @@
# IEC60870-Slave
IEC 60870 子站协议库,当前提供 IEC104 子站、Windows TCP 后端、总召和计数量召唤。101/104 可复用的 ASDU、信息对象、时间工具及子站应答接口集中在公共目录。
## 目录结构
按职责归档,同一模块的 `.c` 和 `.h` 放在一起:
```text
IEC60870-Slave/
├── 104/ # IEC104 专用协议代码
│ ├── IEC104_Slave.c/h # 子站统一入口
│ ├── business/ # 104业务模块
│ │ ├── IEC104_Interrogation.c/h # 共用召唤引擎
│ │ ├── IEC104_Interrogation_Internal.h # 引擎与种类规则的私有接口
│ │ ├── IEC104_GeneralInterrogation.c/h # 站总召、分组总召规则
│ │ └── IEC104_CounterInterrogation.c/h # 计数量读取、冻结与复位
│ ├── IEC104_TcpServer.c/h # TCP 服务与事件调度
│ ├── IEC104_Connection.c/h # 连接协调
│ ├── IEC104_Link*.c/h # 链路、主站/子站启停
│ ├── IEC104_Send*.c/h # 发送管理与缓冲
│ ├── IEC104_AsduQueue.c/h # ASDU 队列
│ ├── IEC104_Receiver.c/h # 流式接收
│ ├── IEC104_Codec.c/h # APCI 编解码
│ ├── IEC104_Net.h / IEC104_Net_Windows.c # 网络接口与 Windows 后端
│ └── IEC104*.c/h # 类型、容量配置、日志及旧接收入口
├── common/ # 101/104 公共代码
│ ├── IEC60870_Types.h # 公共类型及应用层参数
│ ├── IEC60870_Codec.c/h # ASDU 公共头编解码
│ ├── IEC60870_Asdu.c/h # ASDU 容器
│ ├── IEC60870_InformationObjects*.c/h # 信息对象
│ ├── IEC60870_Slave.c/h # 公共子站应答接口
│ └── IEC60870_Time.c/h / IEC60870_Clock.c/h # 时间工具与时钟接口
├── examples/ # 应用示例
│ └── iec104_slave_example.c # 模拟点表、业务分派与程序入口
├── test/ # 测试源码与主机脚本
│ ├── iec104_*_test.c # 104 传输、调度、子站与总召测试
│ ├── iec60870_*_test.c # 公共 ASDU 与信息对象测试
│ └── slave_example_loopback_test.cjs # 真实 TCP 回环测试
├── docs/ # 设计、接口、示例及测试说明
│ ├── README.md # 文档索引与推荐阅读顺序
│ ├── 104/ # 104 设计及接口说明
│ ├── common/ # 公共模块说明
│ ├── examples/ # 示例使用说明
│ └── test/ # 测试说明与验证记录
├── scripts/ # 构建脚本
│ └── build.ps1 # 构建示例,-Test 同时运行示例回归
├── .build/ # 本地构建及验证产物(运行脚本后生成)
└── README.md # 项目总览
```
## 构建与测试
在项目根目录的 PowerShell 中执行,需要 MinGW GCC;`-Test` 还需要 Node.js:
```powershell
./scripts/build.ps1
./.build/slave-example/iec104_slave_example.exe
# 运行示例业务回归和真实 TCP 回环测试
./scripts/build.ps1 -Test
```
脚本按自身位置定位项目,可从其他工作目录调用。构建产物统一写入项目根目录的 `.build/`。
接入自己的工程时,将 `104/` 和 `common/` 加入头文件搜索路径,使用总召或计数量业务时再加入 `104/business/`;按所用模块选择源文件,仅使用公共模块时只需 `common/`。Windows 网络后端需链接 `Ws2_32`。统一入口为 `IEC104_Slave.h`,总召使用 `IEC104_Interrogation.h`(编译 `104/business/` 下的引擎和GeneralInterrogation.c),计数量使用 `IEC104_CounterInterrogation.h`。接入约定见[计数量设计](docs/104/IEC104_CounterInterrogation_设计架构.md)。
详细资料见[文档索引](docs/README.md)、[模块说明与接入示例](docs/104/README.md)、[子站示例说明](docs/examples/IEC104子站示例.md)和[完整测试命令](docs/test/IEC104测试文档.md#2-编译与运行)。
@@ -0,0 +1,461 @@
/*
* ASDU层仅处理公共头、对象布局、容量和分派。
* 对象编码调用基类函数表;对象解码按Type ID调用独立类型解析器。
* 所有外部可见写入都在字段、布局和容量验证完成后提交,失败不留下半条报文。
*/
#include "IEC60870_Asdu.h"
#include "IEC60870_InformationObjects_Internal.h"
#include <string.h>
/**
* 功能:计算当前配置的ASDU公共头字节数。
* 输入参数:parameters——已通过IEC60870_AppLayerParameters_Valid检查的非空参数。
* 输出参数:无。
* 返回值:2+sizeOfCOT+sizeOfCA,即4、5或6字节。
* 说明:固定的2字节是Type ID和VSQ;OA已包含在2字节COT中,不能再次累加。
*/
static uint16_t Header_Size(const IEC60870_AppLayerParameters_t *parameters)
{
return (uint16_t)(2 + parameters->sizeOfCOT + parameters->sizeOfCA);
}
/**
* 功能:检查信息对象区的精确长度、数量、SQ约束及连续地址上限。
* 输入参数:info——非空公共头视图,参数和对象原文在使用期间必须有效且不变。
* type——与info->type_id匹配的非空只读类型描述。
* 输出参数:无。
* 返回值:1表示对象区布局合法;0表示参数、数量、长度或隐含地址范围非法。
* 说明:先检查长度,再读取首IOA;这里只验证布局,不检查值、品质或时标。
*/
static int Layout_Valid(const IEC60870_AsduInfo_t *info,
const IEC60870_InformationObjectVTable *type)
{
uint16_t expected;
if (!IEC60870_AppLayerParameters_Valid(info->parameters) ||
info->information_objects == NULL || info->object_count == 0U ||
info->object_count > IEC60870_ASDU_MAX_OBJECTS || info->is_sequence > 1U ||
(info->is_sequence && !type->supports_sequence) ||
(type->single_object && info->object_count != 1U)) {
return 0;
}
/* 设N为数量、D为值载荷长度、L为IOA宽度:SQ=0占N*(L+D),SQ=1占L+N*D。
* 精确相等同时拒绝截短对象和额外尾字节;这里不计ASDU公共头。 */
expected = (uint16_t)(info->object_count * type->payload_size +
(info->is_sequence ? info->parameters->sizeOfIOA :
info->object_count * info->parameters->sizeOfIOA));
if (type->get_payload_size_from_buffer != NULL) {
uint16_t payload_size;
uint16_t address_size = (uint16_t)info->parameters->sizeOfIOA;
/* 当前变长类型是单对象文件段;先保护段头读取,再依据LOS计算实际长度。 */
if (info->object_count != 1U || info->is_sequence ||
info->information_objects_len < address_size) { return 0; }
payload_size = type->get_payload_size_from_buffer(info->information_objects + address_size,
(uint16_t)(info->information_objects_len - address_size));
if (payload_size == 0U) { return 0; }
expected = (uint16_t)(address_size + payload_size);
}
if (expected != info->information_objects_len ||
expected > info->parameters->maxSizeOfASDU - Header_Size(info->parameters)) {
return 0;
}
/* SQ首地址本身可表示,但末地址首IOA+N-1也必须能由同样的IOA宽度表示。 */
return !info->is_sequence ||
IEC60870_Object_Read_LE(info->information_objects, (unsigned int)info->parameters->sizeOfIOA) <=
IEC60870_Object_Max_IOA(info->parameters) - (info->object_count - 1U);
}
/**
* 功能:定位指定对象的值载荷,同时恢复其信息对象地址。
* 输入参数:info——已通过Layout_Valid校验的非空ASDU视图。
* type——与info->type_id匹配的非空只读类型描述。
* index——从0开始的索引,调用方必须保证小于object_count。
* 输出参数:ioa——非空地址输出;SQ=1时使用首地址加索引还原。
* payload_length——非空输出,返回固定或变长载荷的实际长度。
* 返回值:输入原文中的载荷借用指针,不含IOA,不复制字节。
* 说明:本内部函数不再验证长度/索引;返回指针的有效期与原ASDU一致。
*/
static const uint8_t *Element_Position(const IEC60870_AsduInfo_t *info,
const IEC60870_InformationObjectVTable *type, uint8_t index, uint32_t *ioa,
uint16_t *payload_length)
{
unsigned int address_size = (unsigned int)info->parameters->sizeOfIOA;
const uint8_t *data = info->information_objects;
*payload_length = type->payload_size;
if (type->get_payload_size_from_buffer != NULL) {
/* Layout_Valid已确认变长对象为SQ=0、数量1且载荷完整。 */
*payload_length = type->get_payload_size_from_buffer(data + address_size,
(uint16_t)(info->information_objects_len - address_size));
}
/* SQ=1的对象区为:首IOA、载荷0、载荷1……;后续地址不占报文字节。 */
if (info->is_sequence) {
*ioa = IEC60870_Object_Read_LE(data, address_size) + index;
return data + address_size + index * type->payload_size;
}
/* SQ=0每个对象都有独立IOA,故步长包含地址;取值前先跳过该地址。 */
data += index * (address_size + type->payload_size);
*ioa = IEC60870_Object_Read_LE(data, address_size);
return data + address_size;
}
/**
* 功能:核对容器控制字段与已构造ASDU布局是否一致。
* 输入参数:self——待检查容器或借用原文对象,可为NULL。
* 输出参数:无。
* 返回值:1表示结构状态一致;0表示参数、容量、类型、数量、长度或首地址非法。
* 说明:构造容器按类型重算精确长度;借用原文对象只检查公共头及最低长度。
* 不重新解码值,参数、控制字段及对象区须由调用方保持有效且不变。
*/
static int Container_Valid(const IEC60870_Asdu_t *self)
{
uint16_t expected;
const IEC60870_InformationObjectVTable *type;
if (self == NULL || self->buffer == NULL ||
!IEC60870_AppLayerParameters_Valid(self->parameters) ||
self->capacity < Header_Size(self->parameters) ||
self->capacity > self->parameters->maxSizeOfASDU ||
self->length < Header_Size(self->parameters) ||
self->length > self->capacity || self->is_sequence > 1U ||
self->object_count > IEC60870_ASDU_MAX_OBJECTS || self->is_from_buffer > 1U) {
return 0;
}
if (self->is_from_buffer) {
IEC60870_AsduInfo_t info;
/* 已有原文的容量固定为其长度,不将未知对象当作可追加的构造容器。
* 不读取未知类型的IOA;first_ioa仅供构造时的连续地址检查使用。 */
return self->capacity == self->length && self->first_ioa == 0U &&
IEC60870_Parse_ASDU_Header(self->parameters, self->buffer, self->length, &info) &&
self->type_id == info.type_id && self->object_count == info.object_count &&
self->is_sequence == info.is_sequence;
}
expected = Header_Size(self->parameters);
if (self->object_count == 0U) {
if (self->type_id != 0U) { return 0; }
} else {
type = IEC60870_InformationObject_Get_Descriptor(self->type_id);
if (type == NULL || (self->is_sequence && !type->supports_sequence) ||
(type->single_object && self->object_count != 1U)) {
return 0;
}
expected = (uint16_t)(expected + self->object_count * type->payload_size +
(self->is_sequence ? self->parameters->sizeOfIOA :
self->object_count * self->parameters->sizeOfIOA));
if (type->get_payload_size_from_buffer != NULL) {
IEC60870_AsduInfo_t info;
if (!IEC60870_Parse_ASDU_Header(self->parameters, self->buffer, self->length, &info) ||
!Layout_Valid(&info, type)) { return 0; }
expected = self->length;
}
}
if (self->length != expected || self->buffer[0] != self->type_id ||
self->buffer[1] != (uint8_t)(self->object_count | (self->is_sequence << 7))) {
return 0;
}
if (self->object_count != 0U &&
(self->first_ioa != IEC60870_Object_Read_LE(self->buffer + Header_Size(self->parameters),
(unsigned int)self->parameters->sizeOfIOA) ||
(self->is_sequence && self->first_ioa >
IEC60870_Object_Max_IOA(self->parameters) - (self->object_count - 1U)))) {
return 0;
}
return 1;
}
/**
* 功能:在调用方缓冲区中创建一个尚未加入对象的ASDU。
* 输入参数:parameters——非空合法参数;容器借用指针,使用期间必须有效且不变。
* capacity——buffer实际可用字节数;有效容量取此值与maxSizeOfASDU的较小值。
* header——非空公共头配置;COT为0..63,P/N和TEST为0/1,CA须适合所选宽度。
* is_sequence——0为逐对象IOA,1为连续地址;一条ASDU内不自动切换。
* 输出参数:self——非空容器存储,成功后记录缓冲区、参数及初始状态。
* buffer——非空可写ASDU缓冲区,初始只写公共头,不含APCI。
* 返回值:OK为成功,FULL为实际缓冲区不足头长,ERROR为参数或字段非法。
* 说明:全部检查通过后才写回;首对象尚未确定,Type ID和对象数均为0。
* 本函数不分配内存,也不发送;SQ适用性按具体类型检查,空容器不能Get_Data。
*/
IEC60870_AsduResult_t IEC60870_Asdu_Create(IEC60870_Asdu_t *self,
const IEC60870_AppLayerParameters_t *parameters,
uint8_t *buffer, uint16_t capacity, const IEC60870_AsduHeader_t *header, int is_sequence)
{
IEC60870_Asdu_t result;
uint16_t header_length;
if (self == NULL || buffer == NULL || header == NULL ||
!IEC60870_AppLayerParameters_Valid(parameters) ||
(is_sequence != 0 && is_sequence != 1) ||
header->cause_of_transmission > 63U || header->is_negative_confirm > 1U ||
header->is_test > 1U || (parameters->sizeOfCA == 1 && header->common_address > 255U)) {
return IEC60870_ASDU_ERROR;
}
header_length = Header_Size(parameters);
if (capacity < header_length) { return IEC60870_ASDU_FULL; }
memset(&result, 0, sizeof(result));
result.buffer = buffer;
/* 缓冲区较大也不能突破协议配置上限;仅保存参数指针,不复制配置。 */
result.capacity = capacity > parameters->maxSizeOfASDU ?
(uint16_t)parameters->maxSizeOfASDU : capacity;
result.length = header_length;
result.is_sequence = (uint8_t)is_sequence;
result.parameters = parameters;
/* 此后没有可能失败的校验;按所选宽度恰好写入header_length字节。 */
/* Type ID暂置0;VSQ bit7为SQ、低7位为数量,初始数量为0。 */
buffer[0] = 0;
buffer[1] = (uint8_t)(is_sequence << 7);
/* COT首字节:bit0..5为原因、bit6为P/N、bit7为TEST;2字节COT才写OA。 */
buffer[2] = (uint8_t)(header->cause_of_transmission |
(header->is_negative_confirm << 6) | (header->is_test << 7));
if (parameters->sizeOfCOT == 2) { buffer[3] = header->originator_address; }
IEC60870_Object_Write_LE(buffer + 2 + parameters->sizeOfCOT,
header->common_address, (unsigned int)parameters->sizeOfCA);
*self = result;
return IEC60870_ASDU_OK;
}
/**
* 功能:将已有可写ASDU原文包装成调用方持有的对象。
* 输入参数:parameters——非空应用层参数;buffer——可写原文;length——原文长度。
* 输出参数:self——全部检查成功后写入;buffer不变。
* 返回值:OK为成功,ERROR为参数或最低长度非法。
* 说明:借用参数和原文,不分配、不复制;本入口不替代业务使用的完整Parse。
* 容量固定为原文长度,后续只允许修改公共头,不能追加信息对象。
*/
IEC60870_AsduResult_t IEC60870_Asdu_Create_From_Buffer(IEC60870_Asdu_t *self,
const IEC60870_AppLayerParameters_t *parameters, uint8_t *buffer, uint16_t length)
{
IEC60870_AsduInfo_t info;
IEC60870_Asdu_t result;
if (self == NULL || !IEC60870_Parse_ASDU_Header(parameters, buffer, length, &info)) {
return IEC60870_ASDU_ERROR;
}
memset(&result, 0, sizeof(result));
result.buffer = buffer;
result.capacity = length;
result.length = length;
result.type_id = info.type_id;
result.object_count = info.object_count;
result.is_sequence = info.is_sequence;
result.is_from_buffer = 1U;
result.parameters = parameters;
*self = result;
return IEC60870_ASDU_OK;
}
/**
* 功能:修改ASDU传送原因,保留COT字节中的两个独立标志。
* 输入参数:self——合法ASDU对象;cause——0..63。
* 输出参数:self->buffer[2]——校验成功后更新低6位。
* 返回值:OK为成功,ERROR为参数或对象状态非法。
* 说明:所有字段宽度配置中COT首字节均位于偏移2;不改变OA或载荷。
*/
IEC60870_AsduResult_t IEC60870_Asdu_Set_COT(IEC60870_Asdu_t *self, int cause)
{
if (cause < 0 || cause > 63 || !Container_Valid(self)) { return IEC60870_ASDU_ERROR; }
self->buffer[2] = (uint8_t)((self->buffer[2] & 0xC0U) | (uint8_t)cause);
return IEC60870_ASDU_OK;
}
/**
* 功能:修改ASDU的P/N标志,保留传送原因及TEST。
* 输入参数:self——合法ASDU对象;is_negative——0或1。
* 输出参数:self->buffer[2]——校验成功后仅更新bit6。
* 返回值:OK为成功,ERROR为参数或对象状态非法。
* 说明:不执行命令,不提交发送;失败保持字节原值。
*/
IEC60870_AsduResult_t IEC60870_Asdu_Set_Negative(IEC60870_Asdu_t *self, int is_negative)
{
if ((is_negative != 0 && is_negative != 1) || !Container_Valid(self)) {
return IEC60870_ASDU_ERROR;
}
self->buffer[2] = (uint8_t)((self->buffer[2] & 0xBFU) | (uint8_t)(is_negative << 6));
return IEC60870_ASDU_OK;
}
/**
* 功能:修改ASDU公共地址,保留其他公共头字段与对象区。
* 输入参数:self——合法ASDU对象;common_address——须适合sizeOfCA的无符号地址。
* 输出参数:self->buffer——在2+sizeOfCOT偏移按小端写入sizeOfCA字节。
* 返回值:OK为成功,ERROR为非法容器或1字节CA无法表示的地址;失败不写入。
* 说明:沿用Container_Valid检查构造及借用对象,不截断地址,不决定是否允许广播。
*/
IEC60870_AsduResult_t IEC60870_Asdu_Set_CA(IEC60870_Asdu_t *self, uint16_t common_address)
{
if (!Container_Valid(self) || (self->parameters->sizeOfCA == 1 && common_address > 255U)) {
return IEC60870_ASDU_ERROR;
}
IEC60870_Object_Write_LE(self->buffer + 2 + self->parameters->sizeOfCOT,
common_address, (unsigned int)self->parameters->sizeOfCA);
return IEC60870_ASDU_OK;
}
/**
* 功能:编码并追加一个同类型对象,成功后更新ASDU长度及VSQ。
* 输入参数:self——已成功Create且控制字段/参数/原文未被修改的容器。
* object——已由具体类型Create或解析器初始化的公共基类首成员。
* 输出参数:self——成功后更新容器及其字节缓冲区,失败保持原值。
* 返回值:OK为追加成功;FULL为字节空间或对象数已满;UNSUPPORTED为未知类型;
* ERROR为容器、身份、字段、地址或SQ约束非法。
* 说明:在有界临时区完成IOA和值编码(含变长文件段),确认容量后才写入。
* 首个成功对象固定Type ID;SQ=1首对象仍带IOA,其后只写载荷且地址必须递增1。
* FULL时由调用方提交已有非空ASDU,另起一条重试同一对象;不会自动拆包。
* 成功后仅保存对象的报文字节,不借用object,调用方可以立即复用其存储。
*/
IEC60870_AsduResult_t IEC60870_Asdu_Add_InformationObject(IEC60870_Asdu_t *self,
const IEC60870_InformationObject_t *object)
{
const IEC60870_InformationObjectVTable *type;
uint8_t encoded[IEC60870_OBJECT_MAX_ENCODED];
uint16_t added;
IEC60870_AsduResult_t status;
if (!Container_Valid(self) || self->is_from_buffer || object == NULL) { return IEC60870_ASDU_ERROR; }
type = IEC60870_InformationObject_Get_Descriptor(object->type_id);
if (type == NULL) { return IEC60870_ASDU_UNSUPPORTED; }
if (object->virtual_function_table != type ||
(self->object_count != 0U && self->type_id != type->type_id) ||
(self->is_sequence && !type->supports_sequence) ||
object->ioa > IEC60870_Object_Max_IOA(self->parameters)) {
return IEC60870_ASDU_ERROR;
}
/* 用减法先判断地址范围,再比较首IOA+已有数量,避免允许隐含地址越界。 */
if (self->is_sequence && self->object_count != 0U &&
(self->first_ioa > IEC60870_Object_Max_IOA(self->parameters) - self->object_count ||
object->ioa != self->first_ioa + self->object_count)) {
return IEC60870_ASDU_ERROR;
}
/* 字段错误优先于容量已满,以免把非法对象误当作下一条ASDU的可重试输入。 */
status = IEC60870_InformationObject_Encode(object, self->parameters,
self->is_sequence && self->object_count != 0U, encoded, sizeof(encoded), &added);
if (status != IEC60870_ASDU_OK) { return status; }
if (self->object_count == IEC60870_ASDU_MAX_OBJECTS ||
(type->single_object && self->object_count != 0U) ||
added > self->capacity - self->length) {
return IEC60870_ASDU_FULL;
}
/* 从此处开始统一提交,没有后续失败分支,容量不足不会留下半个对象。 */
memcpy(self->buffer + self->length, encoded, added);
if (self->object_count == 0U) {
self->first_ioa = object->ioa;
self->type_id = type->type_id;
self->buffer[0] = type->type_id;
}
self->length = (uint16_t)(self->length + added);
++self->object_count;
self->buffer[1] = (uint8_t)(self->object_count | (self->is_sequence << 7));
return IEC60870_ASDU_OK;
}
/**
* 功能:导出构造完成或直接借用的ASDU原文。
* 输入参数:self——非空合法对象;Create创建的容器必须至少加入一个对象。
* 输出参数:asdu——非空输出指针,成功时指向调用方原有缓冲区。
* length——非空输出指针,成功时得到当前ASDU字节数,不含APCI。
* 返回值:OK为成功;ERROR为容器、输出参数非法或容器为空;失败不修改输出。
* 说明:返回借用视图,不复制或发送。借用原文对象不额外进行完整对象校验。
* 后续Add/Create/Set会影响原文,length只是本次快照。
*/
IEC60870_AsduResult_t IEC60870_Asdu_Get_Data(const IEC60870_Asdu_t *self,
const uint8_t **asdu, uint16_t *length)
{
if (!Container_Valid(self) || (!self->is_from_buffer && self->object_count == 0U) ||
asdu == NULL || length == NULL) {
return IEC60870_ASDU_ERROR;
}
*asdu = self->buffer;
*length = self->length;
return IEC60870_ASDU_OK;
}
/**
* 功能:校验完整ASDU(含参数和文件类型),生成可用于逐对象提取的只读视图。
* 输入参数:parameters——显式非空应用层参数,字段宽度必须对应收到的报文。
* asdu——非空ASDU原文,不含APCI。
* length——完整ASDU字节数,不能超过配置上限,不能包含多余尾字节。
* 输出参数:info——非空视图输出,借用parameters和asdu,二者须在取对象期间有效且不变。
* 返回值:OK为成功;UNSUPPORTED为未支持类型;ERROR为公共头、布局或任一对象非法。
* 说明:先检查公共头和布局,再验证所有对象,全部成功后才发布info,失败保留旧输出。
* 不执行命令,也不判断COT/CA/IOA的业务适用性;具体对象通过Get_Element获取。
*/
IEC60870_AsduResult_t IEC60870_Asdu_Parse(const IEC60870_AppLayerParameters_t *parameters,
const uint8_t *asdu, uint16_t length, IEC60870_AsduInfo_t *info)
{
IEC60870_AsduInfo_t result;
const IEC60870_InformationObjectVTable *type;
uint8_t i;
if (info == NULL || !IEC60870_Parse_ASDU_Header(parameters, asdu, length, &result)) {
return IEC60870_ASDU_ERROR;
}
type = IEC60870_InformationObject_Get_Descriptor(result.type_id);
if (type == NULL) { return IEC60870_ASDU_UNSUPPORTED; }
if (!Layout_Valid(&result, type)) { return IEC60870_ASDU_ERROR; }
/* 输出传NULL使类型解析器仅校验,不分配对象数组;任一对象失败就丢弃整个临时视图。 */
for (i = 0; i < result.object_count; ++i) {
uint32_t ioa;
uint16_t payload_length;
const uint8_t *payload = Element_Position(&result, type, i, &ioa, &payload_length);
IEC60870_AsduResult_t status = IEC60870_InformationObject_Decode(result.type_id, ioa,
payload, payload_length, NULL, 0);
if (status != IEC60870_ASDU_OK) { return status; }
}
*info = result;
return IEC60870_ASDU_OK;
}
/**
* 功能:从已解析视图中按索引取得一个独立类型的对象副本。
* 输入参数:info——由Asdu_Parse生成且未修改的视图,其原文和参数仍须有效。
* index——从0开始的对象索引,必须小于object_count。
* object_size——object实际可用字节数,至少为对应具体结构的sizeof。
* 输出参数:object——非空具体对象存储,结构类型必须由info->type_id正确选择。
* 返回值:OK为成功;UNSUPPORTED为未知类型;ERROR为布局、索引、载荷或输出尺寸非法。
* 说明:输出无需预先Create,成功时按值保存且不借用原文,失败保持原输出不变。
* 只重新检查布局和选中对象;尺寸校验不能替代C语言类型匹配,公共头仍从info读取。
*/
IEC60870_AsduResult_t IEC60870_Asdu_Get_Element(const IEC60870_AsduInfo_t *info,
uint8_t index, void *object, size_t object_size)
{
const IEC60870_InformationObjectVTable *type;
const uint8_t *payload;
uint32_t ioa;
uint16_t payload_length;
if (info == NULL || object == NULL) { return IEC60870_ASDU_ERROR; }
type = IEC60870_InformationObject_Get_Descriptor(info->type_id);
if (type == NULL) { return IEC60870_ASDU_UNSUPPORTED; }
if (!Layout_Valid(info, type) || index >= info->object_count) { return IEC60870_ASDU_ERROR; }
payload = Element_Position(info, type, index, &ioa, &payload_length);
return IEC60870_InformationObject_Decode(info->type_id, ioa, payload,
payload_length, object, object_size);
}
/**
* 功能:复制请求原文并改写COT/P/N,构造确认、终止或未知字段错误应答。
* 输入参数:parameters——与请求布局匹配的非空合法应用层参数。
* request——非空请求ASDU原文,不含APCI。
* length——请求ASDU的完整字节数。
* cause——接受7、9、10及44..47;分别用于确认、终止及未知类型/原因/地址。
* is_negative——P/N标志,0或1;否定激活确认使用cause=7、is_negative=1。
* capacity——out实际可写字节数,至少为length。
* 输出参数:out——非空应答缓冲区,可与request原地或部分重叠。
* out_length——非空长度输出,成功时等于length。
* 返回值:OK为成功,FULL为输出容量不足,ERROR为参数或请求基础格式非法。
* 说明:只依赖公共头和最低长度检查,不要求具体对象解析成功,因此可回显未知类型。
* 保留TEST、OA、CA、VSQ及所有对象字节;失败不修改out/out_length,不自动发送。
*/
IEC60870_AsduResult_t IEC60870_Asdu_Build_Response(const IEC60870_AppLayerParameters_t *parameters,
const uint8_t *request, uint16_t length,
int cause, int is_negative, uint8_t *out, uint16_t capacity, uint16_t *out_length)
{
IEC60870_AsduInfo_t info;
uint8_t cot;
if (out == NULL || out_length == NULL || (is_negative != 0 && is_negative != 1) ||
(cause != IEC60870_ASDU_COT_ACTIVATION_CON && cause != IEC60870_ASDU_COT_DEACTIVATION_CON &&
cause != IEC60870_ASDU_COT_ACTIVATION_TERMINATION &&
(cause < IEC60870_ASDU_COT_UNKNOWN_TYPE || cause > IEC60870_ASDU_COT_UNKNOWN_IOA)) ||
!IEC60870_Parse_ASDU_Header(parameters, request, length, &info)) {
return IEC60870_ASDU_ERROR;
}
if (capacity < length) { return IEC60870_ASDU_FULL; }
/* 先保存新COT,再memmove原文;即使输入输出重叠,也不会丢失请求的TEST。 */
cot = (uint8_t)(cause | (is_negative << 6) | (info.is_test << 7));
memmove(out, request, length);
out[2] = cot;
*out_length = length;
return IEC60870_ASDU_OK;
}
@@ -0,0 +1,167 @@
/*
* 101/104共用ASDU容器。公共头、VSQ/SQ、容量、IOA布局由本层维护,
* 数值、品质、命令限定词和时标的编解码由具体信息对象完成。
* 参数与字节缓冲区均由调用方提供;无动态内存或可变全局状态。
*
* 发送:具体类型Create -> Asdu_Create -> Add_InformationObject(逐个)-> Get_Data。
* 接收:Asdu_Parse -> 按info.type_id选择具体结构 -> Get_Element。
* Type ID决定载荷格式,COT表示传送原因,CA是站公共地址,IOA是点/对象地址;
* VSQ低7位是对象数,bit7是SQ。SQ=1只保存首IOA,随后各对象地址依次加1。
*/
#ifndef IEC60870_ASDU_H
#define IEC60870_ASDU_H
#include "IEC60870_Codec.h"
#include "IEC60870_InformationObjects.h"
#ifdef __cplusplus
extern "C" {
#endif
/* COT首字节的低6位,不含独立的P/N和TEST标志;不是完整的COT适用规则表。 */
typedef enum {
IEC60870_ASDU_COT_PERIODIC = 1, /* 周期/循环上报。 */
IEC60870_ASDU_COT_BACKGROUND = 2, /* 背景扫描。 */
IEC60870_ASDU_COT_SPONTANEOUS = 3, /* 突发上报。 */
IEC60870_ASDU_COT_INITIALIZED = 4, /* 初始化结束。 */
IEC60870_ASDU_COT_REQUEST = 5, /* 请求或被请求的数据。 */
IEC60870_ASDU_COT_ACTIVATION = 6, /* 激活请求。 */
IEC60870_ASDU_COT_ACTIVATION_CON = 7, /* 激活确认,P/N另行指定。 */
IEC60870_ASDU_COT_DEACTIVATION = 8, /* 停止激活请求。 */
IEC60870_ASDU_COT_DEACTIVATION_CON = 9, /* 停止激活确认。 */
IEC60870_ASDU_COT_ACTIVATION_TERMINATION = 10, /* 激活终止。 */
IEC60870_ASDU_COT_FILE_TRANSFER = 13, /* 文件传输。 */
IEC60870_ASDU_COT_INTERROGATED_BY_STATION = 20, /* 响应站总召唤的数据。 */
IEC60870_ASDU_COT_COUNTER_INTERROGATION = 37, /* 响应电能脉冲总召唤的数据。 */
IEC60870_ASDU_COT_COUNTER_GROUP_1 = 38,
IEC60870_ASDU_COT_COUNTER_GROUP_2 = 39,
IEC60870_ASDU_COT_COUNTER_GROUP_3 = 40,
IEC60870_ASDU_COT_COUNTER_GROUP_4 = 41,
IEC60870_ASDU_COT_UNKNOWN_TYPE = 44, /* 未知类型标识。 */
IEC60870_ASDU_COT_UNKNOWN_CAUSE = 45, /* 未知传送原因。 */
IEC60870_ASDU_COT_UNKNOWN_COMMON_ADDRESS = 46, /* 未知公共地址。 */
IEC60870_ASDU_COT_UNKNOWN_IOA = 47 /* 未知信息对象地址。 */
} IEC60870_AsduCause_t;
/* 创建ASDU时仅提供公共头;Type ID由首个成功添加的对象决定,VSQ由容器维护。
* 字段赋值前不得截窄越界整数。COT/CA/IOA的业务适用性由应用层检查。 */
typedef struct {
uint8_t cause_of_transmission; /* COT低6位,0..63,不含P/N及TEST。 */
uint8_t is_negative_confirm; /* P/N:0肯定、1否定。 */
uint8_t is_test; /* TEST:0/1。 */
uint8_t originator_address; /* 显式OA;COT为1字节时忽略,解析输出0。 */
uint16_t common_address; /* 1字节CA拒绝超过255的值,不截断。 */
} IEC60870_AsduHeader_t;
/* 调用方保存的容器;控制字段由Create/Create_From_Buffer初始化,不得手工修改。
* 字节区通过Add或字段设置接口修改;parameters在使用期间必须有效且不变。
* Create_From_Buffer借用可写原文,只允许设置公共头,不允许继续追加对象。
* 同一实例由调用方串行使用;对象不分配或释放其借用的缓冲区。 */
typedef struct {
uint8_t *buffer; /* ASDU原文,不含APCI。 */
uint16_t capacity; /* min(调用方容量, maxSizeOfASDU)。 */
uint16_t length; /* 当前字节数,空容器仅有公共头。 */
uint8_t type_id; /* 空容器为0,首个对象成功加入后固定。 */
uint8_t object_count; /* 按类型限制为0..127或0..1;借用时记录原VSQ数量。 */
uint8_t is_sequence; /* SQ:0逐对象地址,1连续地址。 */
uint8_t is_from_buffer;/* 1为借用收到的原文;0为Create创建的构造容器。 */
uint32_t first_ioa; /* 构造时供SQ连续性校验;借用原文时为0且不使用。 */
const IEC60870_AppLayerParameters_t *parameters; /* 借用参数,不复制。 */
} IEC60870_Asdu_t;
/* 结果须显式比较IEC60870_ASDU_OK;FULL=0,ERROR/UNSUPPORTED为负值。
* 所有失败保留调用方输出。结构体、输出参数和字节区不得互相重叠;
* 唯一例外是Build_Response允许输入/输出字节区重叠。 */
/* 创建空容器并固定参数、SQ和公共头;参数必须非空,头长=2+COT宽度+CA宽度。
* 有效容量不足头长返回FULL;SQ=1只适用于监视信息和文件目录。
* 初始Type ID为0,首个对象失败不会改变类型;空容器不能Get_Data。 */
IEC60870_AsduResult_t IEC60870_Asdu_Create(IEC60870_Asdu_t *self,
const IEC60870_AppLayerParameters_t *parameters,
uint8_t *buffer, uint16_t capacity, const IEC60870_AsduHeader_t *header, int is_sequence);
/**
* 功能:在调用方存储中建立可修改的ASDU对象,直接借用已有报文字节。
* 输入参数:parameters——对应原文布局的非空合法参数,使用期间有效且不变。
* buffer——非空可写ASDU原文,不含APCI;length——原文完整长度。
* 输出参数:self——成功时写入容器,失败保持原值;buffer不变。
* 返回值:OK为成功,ERROR为参数或最低长度非法。
* 说明:只检查公共头和已知类型最低长度,允许未知类型回显错误应答。
* 业务执行前仍应调用Parse进行完整校验;不复制、不分配、不追加对象。
* 字段设置会修改buffer;必须保证buffer和parameters在使用期间有效。
*/
IEC60870_AsduResult_t IEC60870_Asdu_Create_From_Buffer(IEC60870_Asdu_t *self,
const IEC60870_AppLayerParameters_t *parameters, uint8_t *buffer, uint16_t length);
/**
* 功能:原地设置ASDU的传送原因。
* 输入参数:self——合法构造容器或借用原文的对象;cause——0..63。
* 输出参数:self->buffer——仅更新COT低6位,保留P/N、TEST及其余字节。
* 返回值:OK为成功,ERROR为参数非法;失败不修改原文。
* 说明:不判断传送原因的业务适用性,也不发送报文。
*/
IEC60870_AsduResult_t IEC60870_Asdu_Set_COT(IEC60870_Asdu_t *self, int cause);
/**
* 功能:原地设置ASDU的肯定或否定确认标志。
* 输入参数:self——合法ASDU对象;is_negative——0肯定、1否定。
* 输出参数:self->buffer——仅更新P/N位,保留COT、TEST及其余字节。
* 返回值:OK为成功,ERROR为参数非法;失败不修改原文。
* 说明:设置字段不代表业务已接受命令,也不提交发送。
*/
IEC60870_AsduResult_t IEC60870_Asdu_Set_Negative(IEC60870_Asdu_t *self, int is_negative);
/**
* 功能:原地设置ASDU公共地址,适用于合法构造容器或借用可写原文的对象。
* 输入参数:self——合法ASDU对象;common_address——0..65535,须适合配置的CA宽度。
* 输出参数:self->buffer——仅更新CA字段,按小端序写入1或2字节。
* 返回值:OK为成功,ERROR为对象状态或字段宽度非法;失败不修改任何原文字节。
* 说明:偏移由COT宽度确定,保留OA、TEST、P/N及对象原文;不限制业务上的站地址范围。
* 不分配、不改变容器长度、不发送;原文修改后,旧解析视图须重新解析。
*/
IEC60870_AsduResult_t IEC60870_Asdu_Set_CA(IEC60870_Asdu_t *self, uint16_t common_address);
/* 通过对象函数表编码并复制到ASDU,成功后不再借用对象本身。
* 单类型约束、SQ连续地址和IOA宽度统一检查。字节容量不足、对象达到127个,
* 或单对象类型已加入对象时返回FULL;类型不匹配、SQ或字段非法返回ERROR。
* FULL时由调用方提交当前非空ASDU,另起一条重试同一对象,不自动发送或拆包。 */
IEC60870_AsduResult_t IEC60870_Asdu_Add_InformationObject(IEC60870_Asdu_t *self,
const IEC60870_InformationObject_t *object);
/* 获取构造容器或Create_From_Buffer对象的借用原文及当前长度。
* Create创建的空容器不可导出;借用原文对象沿用公共头最低长度检查。
* 不分配、不复制、不发送;需完整检查对象值时先调用Parse。
* 缓冲区修改后原视图失效;提交104传输时应使用2/2/3配置及<=249的总长。 */
IEC60870_AsduResult_t IEC60870_Asdu_Get_Data(const IEC60870_Asdu_t *self,
const uint8_t **asdu, uint16_t *length);
/* 统一解析67种监视、命令、初始化结束、参数和文件类型。
* 验证精确长度和所有对象,文件段按LOS检查变长载荷。
* 监视与文件目录支持SQ=0/1;参数允许多个对象但要求SQ=0。
* 其余类型要求SQ=0、数量=1;未知类型返回UNSUPPORTED。
* info借用参数和输入原文,二者在取对象期间均须有效且不变。 */
IEC60870_AsduResult_t IEC60870_Asdu_Parse(const IEC60870_AppLayerParameters_t *parameters,
const uint8_t *asdu, uint16_t length, IEC60870_AsduInfo_t *info);
/* 按Type ID调用具体解析器,向对应类型的存储写入一个按值对象。
* 例如Type 1传入&point、sizeof(point),其中point为IEC60870_SinglePointInformation_t。
* 调用方必须依据info->type_id选择正确的结构类型;object_size至少为该结构尺寸,
* 尺寸检查不能替代C语言中的类型匹配。index从0开始,输出无需预先Create。
* 对象不借用原文;公共头继续从info读取,选中对象之外不再次重复解码。 */
IEC60870_AsduResult_t IEC60870_Asdu_Get_Element(const IEC60870_AsduInfo_t *info,
uint8_t index, void *object, size_t object_size);
/* 复制请求,只更新COT及P/N,保留TEST、OA、CA、VSQ和对象原文。
* cause接受7/9/10/44..47,is_negative为0/1;否定激活确认传7、1。
* 仅检查公共头及最低长度,允许未知类型回显,不要求对象解析成功。
* 参数须对应请求布局。容量不足返回FULL,所有失败保留out及out_length。
* 输入/输出字节区支持原地与部分重叠;不执行命令或自动发送。 */
IEC60870_AsduResult_t IEC60870_Asdu_Build_Response(const IEC60870_AppLayerParameters_t *parameters,
const uint8_t *request, uint16_t length,
int cause, int is_negative, uint8_t *out, uint16_t capacity, uint16_t *out_length);
#ifdef __cplusplus
}
#endif
#endif /* IEC60870_ASDU_H */
@@ -0,0 +1,204 @@
/*
* IEC 60870-5-101/104 共用时间源实现:读取当前日历、设置实际时钟。
* IEC60870_CLOCK_USE_RTC=0 使用本文件内的 Windows 读写,=1 调用 RTC 预留接口。
* 公共入口统一使用固定 UTC+8 标准时间,检查日历、读取质量并重算星期。
* Windows 分支负责 UTC 与 UTC+8 的转换,不使用计算机配置的本地时区或夏令时。
* RTC 分支直接收发约定的 UTC+8 日历;芯片初始化、寄存器读写由板级驱动负责。
*/
#include "IEC60870_Clock.h"
/* 编译时选择时间源;RTC 模式不编译下面的 Windows 实现,也不包含 windows.h。 */
#if !IEC60870_CLOCK_USE_RTC
#ifndef _WIN32
#error "Windows clock requires _WIN32; define IEC60870_CLOCK_USE_RTC=1 for RTC"
#else
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
/*
* 函数功能:在日历数值上加减固定八小时,完成 UTC 与 UTC+8 标准时间的转换。
* 入口参数:source——源日历;to_local——非 0 表示 UTC 加八小时,0 表示 UTC+8 减八小时。
* 出口参数:result——转换后的日历,仅返回成功时可使用;两个指针均由内部调用方保证非空。
* 返回值:1 转换成功;0 Windows 日期转换失败,或加减偏移会使中间计数溢出。
* 说明:FILETIME 以 100 纳秒为单位,从 1601-01-01 起计数;此处只用它进行日期运算。
* 本地日历也按字段数值展开,平移后的中间计数不作为 UTC 时间戳对外返回。
* 加减后交给 Windows 还原年月日,自然处理跨日、跨月、跨年及闰日。
* 1970..2099 的业务日历范围由读写入口检查,本函数不额外限制年份。
*/
static int Shift_Calendar(const SYSTEMTIME *source, int to_local, SYSTEMTIME *result)
{
/* 分钟数 × 每分钟秒数 × 每秒 100 纳秒单位数;先转为 64 位再乘,避免中间结果溢出。 */
const uint64_t offset_ticks = (uint64_t)IEC60870_LOCAL_UTC_OFFSET_MINUTES *
UINT64_C(60) * UINT64_C(10000000);
FILETIME encoded;
uint64_t ticks;
/* 只展开日历数值,不查询系统本地时区;转换失败时不使用 encoded。 */
if (!SystemTimeToFileTime(source, &encoded)) {
return 0;
}
/* FILETIME 分为高、低两个 32 位字段,按位组合可避免指针强转带来的对齐和别名问题。 */
ticks = ((uint64_t)encoded.dwHighDateTime << 32) | encoded.dwLowDateTime;
if (to_local != 0) {
/* 先检查再加,防止无符号整数回绕为较小的时间值。 */
if (ticks > UINT64_MAX - offset_ticks) {
return 0;
}
ticks += offset_ticks;
} else {
/* 先检查再减,防止越过计数原点后回绕为很大的时间值。 */
if (ticks < offset_ticks) {
return 0;
}
ticks -= offset_ticks;
}
/* 拆回 FILETIME,由系统重新计算全部日历字段,毫秒随计数一起保留。 */
encoded.dwLowDateTime = (DWORD)(ticks & UINT64_C(0xFFFFFFFF));
encoded.dwHighDateTime = (DWORD)(ticks >> 32);
return FileTimeToSystemTime(&encoded, result) != 0;
}
/*
* 函数功能:读取 Windows 系统时间,输出固定 UTC+8 标准时间。
* 入口参数:value——接收结果的指针,不得为空。
* 出口参数:value——合法日历及 is_invalid=0;失败时不修改调用方结果。
* 返回值:1 取得合法结果;0 空指针、偏移转换失败或日历超出模块支持范围。
* 说明:一次 GetSystemTime 取得完整 UTC 日历及毫秒,避免分次取字段时遇到秒或日期进位。
* 固定加八小时,与 Windows 设置的本地时区无关;最终结果仍交由公共入口校验。
*/
static int Windows_Read(IEC60870_ClockValue_t *value)
{
IEC60870_ClockValue_t result = {0};
SYSTEMTIME utc, local;
if (value == NULL) {
return 0;
}
GetSystemTime(&utc);
if (!Shift_Calendar(&utc, 1, &local)) {
return 0;
}
result.time.year = local.wYear;
result.time.month = (uint8_t)local.wMonth;
result.time.day = (uint8_t)local.wDay;
result.time.hour = (uint8_t)local.wHour;
result.time.minute = (uint8_t)local.wMinute;
result.time.second = (uint8_t)local.wSecond;
result.time.ms = local.wMilliseconds;
result.time.weekday = (uint8_t)local.wDayOfWeek;
/* 加八小时可能跨到下一年,必须检查转换后的日历是否仍在 1970..2099 内。 */
if (!IEC60870_Time_Is_Valid(&result.time)) {
return 0;
}
/* Windows 读时接口不提供同步质量;0 仅表示未标记无效,不代表已校准或已同步。 */
result.is_invalid = 0U;
*value = result;
return 1;
}
/*
* 函数功能:将固定 UTC+8 标准时间转换为 UTC,并实际修改 Windows 系统时钟。
* 入口参数:local_time——1970..2099 的合法 UTC+8 日历,原始输入不会被修改。
* 返回值:1 SetSystemTime 报告成功;0 参数非法、转换失败或系统调用失败。
* 说明:SetSystemTime 接收 UTC,因此必须先减八小时;它不使用 wDayOfWeek 字段。
* 调用进程需要系统设时权限,权限不足时返回 0;成功与否以系统调用结果为准。
*/
static int Windows_Set(const IEC60870_WallTime_t *local_time)
{
SYSTEMTIME local = {0}, utc;
if (!IEC60870_Time_Is_Valid(local_time)) {
return 0;
}
local.wYear = local_time->year;
local.wMonth = local_time->month;
local.wDay = local_time->day;
local.wHour = local_time->hour;
local.wMinute = local_time->minute;
local.wSecond = local_time->second;
local.wMilliseconds = local_time->ms;
if (!Shift_Calendar(&local, 0, &utc)) {
return 0;
}
return SetSystemTime(&utc) != 0;
}
#endif /* _WIN32 */
#endif /* !IEC60870_CLOCK_USE_RTC */
/*
* 函数功能:检查完整日历,再根据年月日重算星期,保证日期与星期一致。
* 入口参数:time——公共读写入口持有的局部日历副本。
* 出口参数:time->weekday——0=周日,1=周一,...,6=周六;其他字段保持原值。
* 返回值:1 日历合法且星期计算成功;0 空指针、日历非法或星期计算失败。
* 说明:传入的 weekday 不参与合法性判断;本函数不校准时钟,也不转换时区。
*/
static int Normalize_Calendar(IEC60870_WallTime_t *time)
{
if (!IEC60870_Time_Is_Valid(time)) {
return 0;
}
return IEC60870_Time_Get_Weekday(time->year, time->month, time->day, &time->weekday);
}
/*
* 函数功能:从选定的 Windows 或 RTC 时间源读取日历,检查完成后再提交输出。
* 入口参数:value——接收结果的指针,不得为空。
* 出口参数:value——固定 UTC+8 日历、重算的星期及时间源提供的 is_invalid。
* 返回值:1 得到合法日历和 0/1 质量标志;0 参数非法、底层读取失败或结果非法。
* 说明:返回 1 只表示本次读取完成,is_invalid=1 的结果仍需业务按无效时间处理。
* 底层仅操作局部结果,失败前即使写入了部分字段,也不会修改调用方输出。
*/
int IEC60870_Clock_Read(IEC60870_ClockValue_t *value)
{
IEC60870_ClockValue_t result = {0};
int status;
if (value == NULL) {
return 0;
}
/* 驱动未主动报告有效状态时,保留无效标志,不因零初始化而假定时钟有效。 */
result.is_invalid = 1U;
#if IEC60870_CLOCK_USE_RTC
/* RTC 直接提供 UTC+8 日历,公共层无需将其转成时间戳再还原。 */
status = IEC60870_Clock_RTC_Read(&result);
#else
status = Windows_Read(&result);
#endif
/* 底层必须明确返回 1;质量值只能为 0/1,日历通过检查后才覆盖输出。 */
if (status != 1 || result.is_invalid > 1U || !Normalize_Calendar(&result.time)) {
return 0;
}
*value = result;
return 1;
}
/*
* 函数功能:检查固定 UTC+8 日历,将重算星期的副本交给选定的时间源设置。
* 入口参数:local_time——待设置的完整日历,不得为空;传入的 weekday 会被重新计算。
* 返回值:1 底层明确报告设置成功;0 参数非法或底层未报告成功。
* 说明:先复制并校验,检查失败时不调用底层,也不修改原始输入。
* 校时报文的 IV/SU、源时区及业务校时条件应由上层处理,本入口只接收日历。
* 返回 0 不保证硬件未发生部分写入,公共层不提供设备写入回滚。
*/
int IEC60870_Clock_Set(const IEC60870_WallTime_t *local_time)
{
IEC60870_WallTime_t prepared;
if (local_time == NULL) {
return 0;
}
prepared = *local_time;
if (!Normalize_Calendar(&prepared)) {
return 0;
}
#if IEC60870_CLOCK_USE_RTC
return IEC60870_Clock_RTC_Set(&prepared) == 1;
#else
return Windows_Set(&prepared) == 1;
#endif
}
@@ -0,0 +1,106 @@
/*
* IEC 60870-5-101/104 共用时间源接口:读取当前日历、设置实际时钟。
* 读写日历统一采用固定UTC+8标准时间,年份1970..2099;CP56编码另限2000..2099。
* 此处的本地时间指固定UTC+8,不随Windows本地时区设置或夏令时变化。
* 时间源仅考虑Windows系统时钟和RTC,构建时二选一,业务直接调用Clock_Read/Clock_Set。
* Windows实现已提供;RTC目前只保留接入接口,具体芯片驱动暂不实现。
* 与IEC60870_Time的日期转换、CP56编解码分工;不自动接入ASDU或链路单调计时。
*/
#ifndef IEC60870_CLOCK_H
#define IEC60870_CLOCK_H
#include "IEC60870_Time.h"
/*
* 时间源编译选项:0=Windows系统时钟(默认,须在Windows构建),1=RTC预留接口。
* 这是编译时选择,不在运行期间切换;RTC模式不编译Windows读写代码。
* 选项须作用于IEC60870_Clock.c的编译,不能只在业务源文件中定义。
* RTC模式须由板级驱动实现下方的Clock_RTC_Read/Set;本模块不提供芯片驱动。
* 此选项与IEC60870_TIME_USE_PLATFORM的日期转换算法选择相互独立:
* 前者决定从哪里读取/设置当前时间,后者决定如何将UTC秒数转换为日历。
* RTC已提供约定的日历时,Clock读写入口不需要时间戳与日历之间的往返转换。
*/
#ifndef IEC60870_CLOCK_USE_RTC
#define IEC60870_CLOCK_USE_RTC 0
#endif
#if IEC60870_CLOCK_USE_RTC != 0 && IEC60870_CLOCK_USE_RTC != 1
#error "IEC60870_CLOCK_USE_RTC must be 0 (Windows) or 1 (RTC)"
#endif
#ifdef __cplusplus
extern "C" {
#endif
/*
* 时间源的读取结果。日历数值合法与时间质量分开表示。
* is_invalid=1仍须提供合法日历;没有可用日历时,平台读时函数应返回0。
* is_invalid=0仅表示未标记无效,不证明时钟已经校准或与外部时间源同步。
*/
typedef struct {
IEC60870_WallTime_t time; /* 固定UTC+8标准时间,ms为0..999,星期由公共入口重算 */
uint8_t is_invalid; /* 0=未标记无效,1=时间无效;可按业务约定传递给CP56的IV */
} IEC60870_ClockValue_t;
/*
* 构建:IEC60870_Clock.c + IEC60870_Time.c;Windows读写实现在Clock.c内部。
* 以后接RTC时,定义IEC60870_CLOCK_USE_RTC=1并加入实现下方RTC接口的板级驱动。
* 时间源由构建选择,公共接口不需要时钟对象、函数指针绑定或初始化。
* RTC等硬件仍须由应用按驱动要求初始化;同一时钟的读写由调用方串行执行。
* 本模块不提供设备锁,不进行动态内存分配。
*/
/**
* 功能:从构建时选定的时间源读取固定UTC+8标准时间,并校验日历和质量标志。
* 输入:value——接收结果的指针,不得为空。
* 输出:value——合法日历、星期(0=周日..6=周六)及源提供的is_invalid。
* 返回:1 取得格式合法的结果,is_invalid可能为1;0 参数非法、底层失败或结果非法。
* 说明:先读取局部变量,检查完成后才提交输出,失败不修改value。
* 局部结果的is_invalid初始为1,驱动应按实际状态明确填写0或1。
* 调用方须同时检查返回值和is_invalid,读取成功不等于时钟准确或已同步。
*/
int IEC60870_Clock_Read(IEC60870_ClockValue_t *value);
/**
* 功能:将固定UTC+8标准时间交给构建时选定的时间源设置实际时钟。
* 输入:local_time——非空,1970..2099的合法UTC+8标准时间;输入的weekday可忽略。
* 返回:1 底层报告设置成功;0 参数非法、平台不支持设时或底层未报告成功。
* 说明:先检查日历并重算星期,再将副本交给时间源实现,原始输入保持不变。
* Windows分支转换为UTC后调用系统设时接口;RTC分支直接将日历交给驱动。
* 本接口不接收IV/SU;校时报文须先通过CP56转换及业务校时条件检查。
* 不提供硬件写入回滚;失败时设备是否已有部分修改由具体驱动决定。
*/
int IEC60870_Clock_Set(const IEC60870_WallTime_t *local_time);
/*
* RTC预留接入接口:仅RTC模式需要由板级驱动实现,Windows模式不引用这两个符号。
* 业务调用上方公共入口,下面的函数由公共层同步调用,无需注册回调或绑定时钟对象。
* 不得保留输入/输出指针供返回后使用,也不能反向调用公共读写入口。
* 本模块仅声明,不提供空实现或默认成功结果;RTC硬件由应用按驱动要求初始化。
*/
/**
* 功能:由板级驱动读取RTC日历,供IEC60870_Clock_Read调用。
* 输入:value——公共层提供的非空局部结果,其is_invalid初始为1。
* 输出:value——一组一致的固定UTC+8日历和is_invalid=0/1;weekday由公共层重算。
* 返回:1 取得可供检查的日历;0 读取失败或没有可用日历。
* 说明:驱动负责寄存器格式转换及读取一致性,避免读到跨秒、跨日拼接的字段。
* RTC日历的时间基准由设备约定,驱动须保证输出为UTC+8;无毫秒能力时可填写ms=0。
* 即使日历数值合法,停振、掉电等状态仍可能使时间无效,须按实际状态填写质量。
*/
int IEC60870_Clock_RTC_Read(IEC60870_ClockValue_t *value);
/**
* 功能:由板级驱动设置RTC实际时钟,供IEC60870_Clock_Set调用。
* 输入:local_time——非空,公共层已校验并重算星期的UTC+8日历副本,只在调用期间有效。
* 返回:1 驱动报告设置成功;0 设置失败或设备不支持设置。
* 说明:驱动负责将日历转换为芯片要求的寄存器格式,按芯片能力处理毫秒。
* 只读驱动直接返回0;写入失败时可能已有部分寄存器改变,公共层不提供回滚。
*/
int IEC60870_Clock_RTC_Set(const IEC60870_WallTime_t *local_time);
#ifdef __cplusplus
}
#endif
#endif /* IEC60870_CLOCK_H */
@@ -0,0 +1,190 @@
/*
* 101/104共用的无状态ASDU基础解析:参数校验、公共头及最低载荷长度检查。
* 只处理调用方提供的字节和参数,不依赖APCI、TCP、串口或连接状态。
* 具体信息对象的完整校验由IEC60870_Asdu和InformationObjects完成。
*/
#include "IEC60870_Codec.h"
#include <stddef.h>
#include <string.h>
/* ASDU公共头中各组合字段的位掩码。 */
#define IEC60870_ASDU_OBJECT_COUNT_MASK 0x7FU
#define IEC60870_ASDU_SEQUENCE_MASK 0x80U
#define IEC60870_ASDU_CAUSE_MASK 0x3FU
#define IEC60870_ASDU_NEGATIVE_MASK 0x40U
#define IEC60870_ASDU_TEST_MASK 0x80U
#define IEC60870_ASDU_IOA_LEN 3U
/* 已知固定长度类型的单个信息对象总长,包含3字节IOA;0表示未在本表检查。
* 保留IOA使读命令C_RD_NA_1的零数据长度与未知类型可区分。
* Type ID是线上的协议值;变长文件段F_SG_NA_1及其他未列类型由业务解析。 */
static const uint8_t asdu_object_lengths[] = {
[1] = IEC60870_ASDU_IOA_LEN + 1U, /* M_SP_NA_1 */
[2] = IEC60870_ASDU_IOA_LEN + 4U, /* M_SP_TA_1 */
[3] = IEC60870_ASDU_IOA_LEN + 1U, /* M_DP_NA_1 */
[4] = IEC60870_ASDU_IOA_LEN + 4U, /* M_DP_TA_1 */
[5] = IEC60870_ASDU_IOA_LEN + 2U, /* M_ST_NA_1 */
[6] = IEC60870_ASDU_IOA_LEN + 5U, /* M_ST_TA_1 */
[7] = IEC60870_ASDU_IOA_LEN + 5U, /* M_BO_NA_1 */
[8] = IEC60870_ASDU_IOA_LEN + 8U, /* M_BO_TA_1 */
[9] = IEC60870_ASDU_IOA_LEN + 3U, /* M_ME_NA_1 */
[10] = IEC60870_ASDU_IOA_LEN + 6U, /* M_ME_TA_1 */
[11] = IEC60870_ASDU_IOA_LEN + 3U, /* M_ME_NB_1 */
[12] = IEC60870_ASDU_IOA_LEN + 6U, /* M_ME_TB_1 */
[13] = IEC60870_ASDU_IOA_LEN + 5U, /* M_ME_NC_1 */
[14] = IEC60870_ASDU_IOA_LEN + 8U, /* M_ME_TC_1 */
[15] = IEC60870_ASDU_IOA_LEN + 5U, /* M_IT_NA_1 */
[16] = IEC60870_ASDU_IOA_LEN + 8U, /* M_IT_TA_1 */
[17] = IEC60870_ASDU_IOA_LEN + 6U, /* M_EP_TA_1 */
[18] = IEC60870_ASDU_IOA_LEN + 7U, /* M_EP_TB_1 */
[19] = IEC60870_ASDU_IOA_LEN + 7U, /* M_EP_TC_1 */
[20] = IEC60870_ASDU_IOA_LEN + 5U, /* M_PS_NA_1 */
[21] = IEC60870_ASDU_IOA_LEN + 2U, /* M_ME_ND_1 */
[30] = IEC60870_ASDU_IOA_LEN + 8U, /* M_SP_TB_1 */
[31] = IEC60870_ASDU_IOA_LEN + 8U, /* M_DP_TB_1 */
[32] = IEC60870_ASDU_IOA_LEN + 9U, /* M_ST_TB_1 */
[33] = IEC60870_ASDU_IOA_LEN + 12U, /* M_BO_TB_1 */
[34] = IEC60870_ASDU_IOA_LEN + 10U, /* M_ME_TD_1 */
[35] = IEC60870_ASDU_IOA_LEN + 10U, /* M_ME_TE_1 */
[36] = IEC60870_ASDU_IOA_LEN + 12U, /* M_ME_TF_1 */
[37] = IEC60870_ASDU_IOA_LEN + 12U, /* M_IT_TB_1 */
[38] = IEC60870_ASDU_IOA_LEN + 10U, /* M_EP_TD_1 */
[39] = IEC60870_ASDU_IOA_LEN + 11U, /* M_EP_TE_1 */
[40] = IEC60870_ASDU_IOA_LEN + 11U, /* M_EP_TF_1 */
[41] = IEC60870_ASDU_IOA_LEN + 14U, /* S_IT_TC_1 */
[45] = IEC60870_ASDU_IOA_LEN + 1U, /* C_SC_NA_1 */
[46] = IEC60870_ASDU_IOA_LEN + 1U, /* C_DC_NA_1 */
[47] = IEC60870_ASDU_IOA_LEN + 1U, /* C_RC_NA_1 */
[48] = IEC60870_ASDU_IOA_LEN + 3U, /* C_SE_NA_1 */
[49] = IEC60870_ASDU_IOA_LEN + 3U, /* C_SE_NB_1 */
[50] = IEC60870_ASDU_IOA_LEN + 5U, /* C_SE_NC_1 */
[51] = IEC60870_ASDU_IOA_LEN + 4U, /* C_BO_NA_1 */
[58] = IEC60870_ASDU_IOA_LEN + 8U, /* C_SC_TA_1 */
[59] = IEC60870_ASDU_IOA_LEN + 8U, /* C_DC_TA_1 */
[60] = IEC60870_ASDU_IOA_LEN + 8U, /* C_RC_TA_1 */
[61] = IEC60870_ASDU_IOA_LEN + 10U, /* C_SE_TA_1 */
[62] = IEC60870_ASDU_IOA_LEN + 10U, /* C_SE_TB_1 */
[63] = IEC60870_ASDU_IOA_LEN + 12U, /* C_SE_TC_1 */
[64] = IEC60870_ASDU_IOA_LEN + 11U, /* C_BO_TA_1 */
[70] = IEC60870_ASDU_IOA_LEN + 1U, /* M_EI_NA_1 */
[100] = IEC60870_ASDU_IOA_LEN + 1U, /* C_IC_NA_1 */
[101] = IEC60870_ASDU_IOA_LEN + 1U, /* C_CI_NA_1 */
[102] = IEC60870_ASDU_IOA_LEN, /* C_RD_NA_1 */
[103] = IEC60870_ASDU_IOA_LEN + 7U, /* C_CS_NA_1 */
[104] = IEC60870_ASDU_IOA_LEN + 2U, /* C_TS_NA_1 */
[105] = IEC60870_ASDU_IOA_LEN + 1U, /* C_RP_NA_1 */
[106] = IEC60870_ASDU_IOA_LEN + 2U, /* C_CD_NA_1 */
[107] = IEC60870_ASDU_IOA_LEN + 9U, /* C_TS_TA_1 */
[110] = IEC60870_ASDU_IOA_LEN + 3U, /* P_ME_NA_1 */
[111] = IEC60870_ASDU_IOA_LEN + 3U, /* P_ME_NB_1 */
[112] = IEC60870_ASDU_IOA_LEN + 5U, /* P_ME_NC_1 */
[113] = IEC60870_ASDU_IOA_LEN + 1U, /* P_AC_NA_1 */
[120] = IEC60870_ASDU_IOA_LEN + 6U, /* F_FR_NA_1 */
[121] = IEC60870_ASDU_IOA_LEN + 7U, /* F_SR_NA_1 */
[122] = IEC60870_ASDU_IOA_LEN + 4U, /* F_SC_NA_1 */
[123] = IEC60870_ASDU_IOA_LEN + 5U, /* F_LS_NA_1 */
[124] = IEC60870_ASDU_IOA_LEN + 4U, /* F_AF_NA_1 */
[126] = IEC60870_ASDU_IOA_LEN + 13U, /* F_DR_TA_1 */
[127] = IEC60870_ASDU_IOA_LEN + 16U /* F_SC_NB_1 */
};
/* 调用前已检查参数、可变公共头及ASDU总长。表中仍以3字节IOA为基准,
* 计算时先减去基准IOA再加上配置宽度。仅检查已知固定长度对象是否被截短;
* 不要求长度恰好相等,不解释对象内容,也不在这里判定零对象或SQ的业务合法性。 */
static int ASDU_Payload_Length_Valid(const IEC60870_AppLayerParameters_t *parameters,
const uint8_t *asdu, uint16_t asdu_len, uint16_t header_length)
{
uint32_t object_count = asdu[1] & IEC60870_ASDU_OBJECT_COUNT_MASK;
uint32_t object_length;
uint32_t required_length;
if (object_count == 0U || asdu[0] >= sizeof(asdu_object_lengths)) {
return 1;
}
object_length = asdu_object_lengths[asdu[0]];
if (object_length == 0U) {
return 1;
}
if ((asdu[1] & IEC60870_ASDU_SEQUENCE_MASK) != 0U) {
/* 连续地址只传首个IOA;非连续地址则每个对象均携带IOA。 */
required_length = (uint32_t)parameters->sizeOfIOA +
object_count * (object_length - IEC60870_ASDU_IOA_LEN);
} else {
required_length = object_count *
(object_length - IEC60870_ASDU_IOA_LEN + (uint32_t)parameters->sizeOfIOA);
}
return (uint32_t)(asdu_len - header_length) >= required_length;
}
/**
* 功能:检查101/104共用应用层参数的字段宽度、源发地址和总长上限。
* 输入参数:parameters——待检查参数,允许NULL。
* 输出参数:无。
* 返回值:合法返回1,NULL或非法字段返回0。
* 说明:先校验小范围字段再计算头长,不选择默认配置或修改参数。
*/
int IEC60870_AppLayerParameters_Valid(const IEC60870_AppLayerParameters_t *parameters)
{
return parameters != NULL && parameters->sizeOfTypeId == 1 &&
parameters->sizeOfVSQ == 1 &&
(parameters->sizeOfCOT == 1 || parameters->sizeOfCOT == 2) &&
(parameters->sizeOfCA == 1 || parameters->sizeOfCA == 2) &&
parameters->sizeOfIOA >= 1 && parameters->sizeOfIOA <= 3 &&
parameters->originatorAddress >= 0 && parameters->originatorAddress <= 255 &&
parameters->maxSizeOfASDU >= 2 + parameters->sizeOfCOT + parameters->sizeOfCA &&
parameters->maxSizeOfASDU <= IEC60870_MAX_ASDU_LEN;
}
/**
* 功能:检查ASDU公共头及已知固定长度对象的最低载荷长度,解析公共头。
* 输入参数:parameters——双方约定的非空应用层参数;asdu/asdu_len——完整ASDU。
* 输出参数:asdu_info——解析成功时写入公共头字段和信息对象区位置。
* 返回值:解析成功返回1,参数或长度非法返回0且不修改asdu_info。
* 说明:按Type ID、VSQ数量及SQ位检查最低载荷长度;未知/变长类型及零对象不做此检查。
* 不检查多余尾字节、SQ/数量的类型约束、传送原因、地址或信息对象内容是否合法。
*/
int IEC60870_Parse_ASDU_Header(const IEC60870_AppLayerParameters_t *parameters,
const uint8_t *asdu,
uint16_t asdu_len,
IEC60870_AsduInfo_t *asdu_info)
{
IEC60870_AsduInfo_t result;
uint16_t header_length;
unsigned int ca_index;
if (asdu == NULL || asdu_info == NULL || !IEC60870_AppLayerParameters_Valid(parameters)) {
return 0;
}
header_length = (uint16_t)(2 + parameters->sizeOfCOT + parameters->sizeOfCA);
if (asdu_len < header_length || asdu_len > parameters->maxSizeOfASDU ||
!ASDU_Payload_Length_Valid(parameters, asdu, asdu_len, header_length)) {
return 0;
}
/* COT为1字节时CA紧跟COT,不存在OA;先组装局部结果,再一次性发布。 */
memset(&result, 0, sizeof(result));
ca_index = (unsigned int)(2 + parameters->sizeOfCOT);
result.common_address = asdu[ca_index];
if (parameters->sizeOfCA == 2) {
result.common_address |= (uint16_t)((uint16_t)asdu[ca_index + 1U] << 8);
}
result.type_id = asdu[0];
result.object_count =
(uint8_t)(asdu[1] & IEC60870_ASDU_OBJECT_COUNT_MASK);
result.is_sequence =
(uint8_t)((asdu[1] & IEC60870_ASDU_SEQUENCE_MASK) != 0U);
result.cause_of_transmission =
(uint8_t)(asdu[2] & IEC60870_ASDU_CAUSE_MASK);
result.is_negative_confirm =
(uint8_t)((asdu[2] & IEC60870_ASDU_NEGATIVE_MASK) != 0U);
result.is_test =
(uint8_t)((asdu[2] & IEC60870_ASDU_TEST_MASK) != 0U);
result.originator_address = parameters->sizeOfCOT == 2 ? asdu[3] : 0;
result.information_objects = &asdu[header_length];
result.information_objects_len = (uint16_t)(asdu_len - header_length);
result.parameters = parameters;
*asdu_info = result;
return 1;
}
@@ -0,0 +1,54 @@
/* 101/104共用的ASDU公共头解析与参数检查,不包含传输帧接口。 */
#ifndef IEC60870_CODEC_H
#define IEC60870_CODEC_H
#include "IEC60870_Types.h"
#ifdef __cplusplus
extern "C" {
#endif
/* ASDU公共头的解析结果;本模块不解析具体信息对象,调用方按type_id另行处理。 */
typedef struct {
uint8_t type_id; /* 类型标识 */
uint8_t object_count; /* VSQ中的信息对象数量 */
uint8_t is_sequence; /* VSQ中的SQ标志 */
uint8_t cause_of_transmission; /* 传送原因,不含标志位 */
uint8_t is_negative_confirm; /* 传送原因中的P/N标志 */
uint8_t is_test; /* 传送原因中的TEST标志 */
uint8_t originator_address; /* 源发地址 */
uint16_t common_address; /* ASDU公共地址 */
const uint8_t *information_objects; /* 信息对象区首地址 */
uint16_t information_objects_len; /* 信息对象区长度 */
const IEC60870_AppLayerParameters_t *parameters; /* 借用本次解析参数,不可修改。 */
} IEC60870_AsduInfo_t;
/**
* 功能:校验应用层字段宽度、默认源发地址及ASDU总长上限。
* 输入参数:parameters——待检查参数,允许NULL。
* 输出参数:无。
* 返回值:合法返回1,NULL或非法值返回0。
* 说明:不修改参数,不替调用方选择101/104的传输配置。
*/
extern int IEC60870_AppLayerParameters_Valid(const IEC60870_AppLayerParameters_t *parameters);
/**
* 功能:检查ASDU公共头及已知固定长度对象的最低载荷长度,解析公共头。
* 输入参数:parameters——非空应用层参数;asdu——ASDU首地址;asdu_len——总长度。
* 输出参数:asdu_info——解析成功时写入公共头字段和信息对象区位置。
* 返回值:解析成功返回1,参数或长度非法返回0且不修改asdu_info。
* 说明:头长为2+sizeOfCOT+sizeOfCA,IOA及最低载荷长度按sizeOfIOA计算。
* 未知/变长类型及零对象不做载荷检查;不检查多余尾字节、SQ/数量的类型约束、
* 传送原因、地址或信息对象内容是否合法。接收器在链路受理前调用本函数。
* information_objects及parameters均为借用,取对象期间报文和参数必须有效且不变。
*/
extern int IEC60870_Parse_ASDU_Header(const IEC60870_AppLayerParameters_t *parameters,
const uint8_t *asdu,
uint16_t asdu_len,
IEC60870_AsduInfo_t *asdu_info);
#ifdef __cplusplus
}
#endif
#endif /* IEC60870_CODEC_H */
@@ -0,0 +1,83 @@
/* 信息对象与ASDU的内部协作,不作为业务API。
* ASDU使用类型描述计算布局;具体对象通过只读函数表编码;解码按Type ID另行分派。
* 字节辅助函数不读写连接状态,也不依赖结构体对齐或主机字节序。 */
#ifndef IEC60870_INFORMATION_OBJECTS_INTERNAL_H
#define IEC60870_INFORMATION_OBJECTS_INTERNAL_H
#include "IEC60870_InformationObjects.h"
/* 单个文件段LOS最多255字节,另有4字节文件段头和最多3字节IOA。
* 即使对象可表示更长载荷,ASDU容器仍按配置的总长上限返回FULL。 */
#define IEC60870_OBJECT_MAX_PAYLOAD 259U
#define IEC60870_OBJECT_MAX_ENCODED (IEC60870_OBJECT_MAX_PAYLOAD + 3U)
/* 每个类型独立的只读编码函数表;解码仍由Type ID选择具体类型的解析函数。
* encode仅写临时载荷,公共入口验证后原子提交;变长类型提供两个长度回调。 */
typedef struct IEC60870_InformationObjectVTable {
uint8_t type_id; /* 线上类型标识;同时用于核对对象基类身份。 */
uint16_t payload_size; /* 固定类型的精确长度;变长类型的最低长度,不含IOA。 */
uint8_t single_object; /* 1表示仅允许一个对象;SQ约束由supports_sequence独立决定。 */
size_t object_size; /* 具体C结构的sizeof,用于输出存储检查,与线上长度无关。 */
int (*encode)(const IEC60870_InformationObject_t *object, uint8_t *payload); /* 写临时载荷,1成功/0字段非法。 */
uint8_t supports_sequence; /* 1允许SQ=1;参数类型允许多个对象但仅支持SQ=0。 */
uint16_t (*get_payload_size)(const IEC60870_InformationObject_t *object); /* NULL为固定长度。 */
uint16_t (*get_payload_size_from_buffer)(const uint8_t *payload, uint16_t length); /* 变长载荷精确长度;0非法。 */
} IEC60870_InformationObjectVTable;
/* 未支持的Type ID返回NULL;地址和长度校验由使用方在读报文前完成。 */
const IEC60870_InformationObjectVTable *IEC60870_InformationObject_Get_Descriptor(uint8_t type_id);
/* 按Type ID调用具体Get_From_Buffer;object==NULL时仅验证,不输出。
* object_size是调用方实际提供的容量,必须至少为所选具体类型的sizeof;
* 非空object必须对应该具体结构,尺寸足够不能替代类型匹配。失败不修改输出。 */
IEC60870_AsduResult_t IEC60870_InformationObject_Decode(uint8_t type_id, uint32_t ioa,
const uint8_t *payload, uint16_t length, void *object, size_t object_size);
/**
* 功能:从低字节在前的字节序列读取无符号值。
* 输入参数:data——至少length字节的有效输入,调用方先完成边界检查。
* length——1..4,用于1/2/3字节地址或2/4字节数值。
* 输出参数:无。
* 返回值:按位拼接的uint32_t,未使用高位为0。
* 说明:逐字节读,允许未对齐输入;不检查指针或范围。
*/
static inline uint32_t IEC60870_Object_Read_LE(const uint8_t *data, unsigned int length)
{
uint32_t value = 0;
unsigned int i;
for (i = 0; i < length; ++i) {
value |= (uint32_t)data[i] << (8U * i);
}
return value;
}
/**
* 功能:将数值低length字节按低字节在前写入报文。
* 输入参数:value——待写数值;需要保留的高位是否越界由调用方预先检查。
* length——1..4;只写指定字节数,不写尾部填充。
* 输出参数:data——至少length字节的可写区域。
* 返回值:无。
* 说明:逐字节写,不要求对齐;IOA必须先按所选宽度校验,防止被静默截断。
*/
static inline void IEC60870_Object_Write_LE(uint8_t *data, uint32_t value, unsigned int length)
{
unsigned int i;
for (i = 0; i < length; ++i) {
data[i] = (uint8_t)(value >> (8U * i));
}
}
/**
* 功能:计算所选IOA字段宽度能够表示的最大地址。
* 输入参数:parameters——非空且已通过AppLayerParameters_Valid校验,sizeOfIOA为1..3。
* 输出参数:无。
* 返回值:分别为0xFF、0xFFFF或0xFFFFFF。
* 说明:位移最多24位;本函数不检查参数,非法宽度不得传入。
*/
static inline uint32_t IEC60870_Object_Max_IOA(const IEC60870_AppLayerParameters_t *parameters)
{
return (UINT32_C(1) << (8U * (unsigned int)parameters->sizeOfIOA)) - 1U;
}
#endif /* IEC60870_INFORMATION_OBJECTS_INTERNAL_H */
@@ -0,0 +1,53 @@
/* 共用发送入口只做接口分派;101/104分别提供其实际处理函数。 */
#include "IEC60870_Slave.h"
/**
* 功能:将普通ASDU发送请求分派给对应协议。
* 输入参数:self——连接接口;asdu——待提交ASDU。
* 输出参数:由接入实现记录提交结果。
* 返回值:接入函数的布尔结果;连接或函数指针为空时返回false。
* 说明:ASDU合法性由接入实现检查,便于同一次调用记录准确的失败原因。
*/
bool IEC60870_MasterConnection_Send_ASDU(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu)
{
return self != NULL && self->send_asdu != NULL && self->send_asdu(self, asdu);
}
/**
* 功能:将激活确认请求分派给对应协议。
* 输入参数:self——连接接口;asdu——可修改ASDU;negative——否定标志。
* 输出参数:由接入实现修改ASDU并记录提交结果。
* 返回值:接入函数的布尔结果;连接或函数指针为空时返回false。
* 说明:共用入口不依赖TCP或串口,不保存请求或执行重试。
*/
bool IEC60870_MasterConnection_Send_ACT_CON(IEC60870_MasterConnection_t *self,
IEC60870_Asdu_t *asdu, bool negative)
{
return self != NULL && self->send_act_con != NULL && self->send_act_con(self, asdu, negative);
}
/**
* 功能:将激活终止请求分派给对应协议。
* 输入参数:self——连接接口;asdu——可修改ASDU。
* 输出参数:由接入实现修改ASDU并记录提交结果。
* 返回值:接入函数的布尔结果;连接或函数指针为空时返回false。
* 说明:本入口不判断业务是否完成,调用时机由业务决定。
*/
bool IEC60870_MasterConnection_Send_ACT_TERM(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu)
{
return self != NULL && self->send_act_term != NULL && self->send_act_term(self, asdu);
}
/**
* 功能:通过协议接入获取应用层参数。
* 输入参数:self——连接接口。
* 输出参数:无。
* 返回值:接入提供的只读参数指针;连接或函数指针为空时返回NULL。
* 说明:不创建默认参数,不改变连接状态,也不延长参数的生命周期。
*/
const IEC60870_AppLayerParameters_t *IEC60870_MasterConnection_Get_Application_Layer_Parameters(
IEC60870_MasterConnection_t *self)
{
if (self == NULL || self->get_application_layer_parameters == NULL) { return NULL; }
return self->get_application_layer_parameters(self);
}
@@ -0,0 +1,73 @@
/*
* 101/104共用的子站发送接口,表示一条面向主站的连接。
* 共用入口通过函数指针分派;具体发送、确认和终止由对应协议接入实现。
* 本层不包含TCP、串口、队列或连接状态机,不分配内存。
*/
#ifndef IEC60870_SLAVE_H
#define IEC60870_SLAVE_H
#include "IEC60870_Asdu.h"
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct IEC60870_MasterConnection IEC60870_MasterConnection_t;
/* 接入实现初始化此接口并保证object及函数指针有效;业务通过下面的函数调用。
* 参数及ASDU的实际布局必须与所接协议一致。所有调用由所属事件循环串行执行。
* send返回true前必须复制或消耗ASDU原文,不能在返回后继续借用调用方字节。
* false不统一表示可重试,具体接入实现负责提供失败原因和处理约定。 */
struct IEC60870_MasterConnection {
void *object; /* 协议接入上下文;共用入口不解释此指针。 */
bool (*send_asdu)(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu);
bool (*send_act_con)(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu, bool negative);
bool (*send_act_term)(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu);
const IEC60870_AppLayerParameters_t *(*get_application_layer_parameters)(
IEC60870_MasterConnection_t *self);
};
/**
* 功能:通过当前连接提交一条已构造好的ASDU。
* 输入参数:self——已由协议接入初始化的连接;asdu——有效ASDU对象。
* 输出参数:具体接入实现记录本次提交结果;本入口不修改ASDU字段。
* 返回值:true表示已发送或已被发送模块接手;false表示失败。
* 说明:true不等于主站已确认;false后按具体协议的结果处理,不自动重试。
*/
bool IEC60870_MasterConnection_Send_ASDU(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu);
/**
* 功能:通过当前连接发送激活确认。
* 输入参数:self——已初始化连接;asdu——可修改ASDU;negative——是否否定确认。
* 输出参数:asdu原地设置COT=7及指定P/N;具体接入实现记录提交结果。
* 返回值:true表示发送模块已接手;false表示失败。
* 说明:字段在提交前修改,提交失败也不会恢复;不执行请求中的业务命令。
*/
bool IEC60870_MasterConnection_Send_ACT_CON(IEC60870_MasterConnection_t *self,
IEC60870_Asdu_t *asdu, bool negative);
/**
* 功能:通过当前连接发送激活终止。
* 输入参数:self——已初始化连接;asdu——对应请求的可修改ASDU对象。
* 输出参数:asdu原地设置COT=10、P/N=0;具体接入实现记录提交结果。
* 返回值:true表示发送模块已接手;false表示失败。
* 说明:业务在相应处理完成后调用;提交失败也不恢复已修改的字段。
*/
bool IEC60870_MasterConnection_Send_ACT_TERM(IEC60870_MasterConnection_t *self,
IEC60870_Asdu_t *asdu);
/**
* 功能:获取当前连接使用的应用层参数。
* 输入参数:self——已初始化连接。
* 输出参数:无。
* 返回值:借用的只读参数指针;接口为空或未实现时返回NULL。
* 说明:返回指针的有效期由协议接入定义;调用方不能修改参数。
*/
const IEC60870_AppLayerParameters_t *IEC60870_MasterConnection_Get_Application_Layer_Parameters(
IEC60870_MasterConnection_t *self);
#ifdef __cplusplus
}
#endif
#endif /* IEC60870_SLAVE_H */
@@ -0,0 +1,502 @@
/*
* IEC 60870-5-101/104 公共时间工具:日历计算、CP56Time2a 编解码及本地标准时间转换。
* 供 101 和 104 的业务层按需调用,编解码不读取协议类型或链路状态。
* CP56 编解码保留 IV 和 SU;转本地标准时间时检查 IV,处理 SU 和源时区,返回完整日历。
* 公共编码允许 SU=0/1;后续业务发送入口负责提供本地标准时间日历并设置 SU=0。
* 本文件只提供公共工具,不自动接入 ASDU 业务处理或执行校时。
* 整数公历换算按 400 年周期展开,将三月作为年内起点以统一处理二月和闰日。
* 仅在校验后的 1970..2099 年范围内运算,天数及秒数均可用 uint32_t 表示。
* 时间戳转日历可选平台库或纯整数算法;接入层提供约定的标准时区偏移,负责系统时钟、RTC 和单调计时。
*/
#include "IEC60870_Time.h"
#include <string.h>
/**
* 功能:编码两字节经过时间。
* 输入参数:value——毫秒值;capacity——out的实际容量。
* 输出参数:out——成功写入2字节,失败不变。
* 返回值:1成功;0空指针或容量不足。
* 说明:先保存输入,允许输入输出重叠;不将经过时间解释为日历时间。
*/
int IEC60870_Time_To_CP16(const IEC60870_CP16Time2a_t *value, uint8_t *out, size_t capacity)
{
uint16_t milliseconds;
if (value == NULL || out == NULL || capacity < IEC60870_CP16_LEN) { return 0; }
milliseconds = value->milliseconds;
out[0] = (uint8_t)milliseconds;
out[1] = (uint8_t)(milliseconds >> 8);
return 1;
}
/**
* 功能:解码两字节经过时间。
* 输入参数:in——低字节在前的输入;length——至少2字节。
* 输出参数:value——经过时间,失败不变。
* 返回值:1成功;0空指针或长度不足。
* 说明:CP16使用完整16位数值范围,不含质量标志。
*/
int IEC60870_Time_From_CP16(const uint8_t *in, size_t length, IEC60870_CP16Time2a_t *value)
{
IEC60870_CP16Time2a_t result;
if (in == NULL || value == NULL || length < IEC60870_CP16_LEN) { return 0; }
result.milliseconds = (uint16_t)((uint16_t)in[0] | ((uint16_t)in[1] << 8));
*value = result;
return 1;
}
/**
* 功能:编码三字节短时标。
* 输入参数:value——分钟、分钟内毫秒和IV;capacity——out容量。
* 输出参数:out——成功写入3字节,失败不变。
* 返回值:1成功;0参数或字段非法。
* 说明:分钟0..59,毫秒0..59999,IV为0/1;第三字节bit6始终为0。
*/
int IEC60870_Time_To_CP24(const IEC60870_CP24Time2a_t *value, uint8_t *out, size_t capacity)
{
uint8_t encoded[IEC60870_CP24_LEN];
if (value == NULL || out == NULL || capacity < sizeof(encoded) ||
value->milliseconds > 59999U || value->minute > 59U || value->is_invalid > 1U) { return 0; }
encoded[0] = (uint8_t)value->milliseconds;
encoded[1] = (uint8_t)(value->milliseconds >> 8);
encoded[2] = (uint8_t)(value->minute | (value->is_invalid << 7));
memcpy(out, encoded, sizeof(encoded));
return 1;
}
/**
* 功能:校验并解码三字节短时标。
* 输入参数:in——至少3字节;length——实际可读长度。
* 输出参数:value——成功保存解析值;失败保持原值。
* 返回值:1成功;0长度、保留位、分钟或毫秒非法。
* 说明:IV保留;调用方结合已有时间基准恢复小时及日期。
*/
int IEC60870_Time_From_CP24(const uint8_t *in, size_t length, IEC60870_CP24Time2a_t *value)
{
IEC60870_CP24Time2a_t result = {0};
if (in == NULL || value == NULL || length < IEC60870_CP24_LEN || (in[2] & 0x40U)) { return 0; }
result.milliseconds = (uint16_t)((uint16_t)in[0] | ((uint16_t)in[1] << 8));
result.minute = in[2] & 0x3FU;
result.is_invalid = in[2] >> 7;
if (result.milliseconds > 59999U || result.minute > 59U) { return 0; }
*value = result;
return 1;
}
#if IEC60870_TIME_USE_PLATFORM != 0 && IEC60870_TIME_USE_PLATFORM != 1
#error "IEC60870_TIME_USE_PLATFORM must be 0 or 1"
#endif
#if IEC60870_TIME_USE_PLATFORM
#ifndef _WIN32
#error "Platform time conversion currently requires Windows; use IEC60870_TIME_USE_PLATFORM=0"
#endif
#include <time.h>
#endif
/*
* 函数功能:判断公历闰年。
* 入口参数:year——公历年份;本函数只执行闰年规则,不检查模块支持的年份范围。
* 返回值:1 为闰年,0 为平年。
* 说明:能被 4 整除且不能被 100 整除,或能被 400 整除的年份为闰年。
*/
static int Is_Leap(uint32_t year)
{
return ((year % 4U) == 0U && (year % 100U) != 0U) || (year % 400U) == 0U;
}
/*
* 函数功能:取得指定公历月份的实际天数。
* 入口参数:year——公历年份;month——月份,合法范围为 1..12。
* 返回值:合法月份返回 28..31,非法月份返回 0。
* 说明:先检查月份再访问表,闰年二月单独返回 29;年份范围由上层日期检查负责。
*/
static uint32_t Days_In_Month(uint32_t year, uint32_t month)
{
static const uint8_t days[12] = {31U, 28U, 31U, 30U, 31U, 30U,
31U, 31U, 30U, 31U, 30U, 31U};
if (month < 1U || month > 12U) {
return 0U;
}
if (month == 2U && Is_Leap(year)) {
return 29U;
}
return days[month - 1U];
}
/*
* 函数功能:统一检查年份范围和日期是否真实存在。
* 入口参数:year——1970..2099;month——1..12;day——待检查的日号。
* 返回值:1 为合法日期,0 为年份、月份或日号非法。
* 说明:按当月实际天数检查 day,不接受平年二月二十九;非法月份的天数为 0,也会被拒绝。
*/
static int Date_Is_Valid(uint32_t year, uint32_t month, uint32_t day)
{
uint32_t limit;
if (year < IEC60870_TIME_MIN_YEAR || year > IEC60870_TIME_MAX_YEAR) {
return 0;
}
limit = Days_In_Month(year, month);
return day >= 1U && day <= limit;
}
/*
* 函数功能:检查完整日历值的日期、时分秒和毫秒范围。
* 入口参数:time——待检查的日历指针,可为空。
* 返回值:1 为数值合法,0 为空指针、无效日期或时间字段越界。
* 说明:小时 0..23,分/秒 0..59,毫秒 0..999;weekday 为派生字段,不参与检查。
* 这里只检查数值,不能据此判断时间基准、时钟是否准确或该值是否允许用于校时。
*/
static int WallTime_Is_Valid(const IEC60870_WallTime_t *time)
{
return time != NULL && Date_Is_Valid(time->year, time->month, time->day) &&
time->hour <= 23U && time->minute <= 59U && time->second <= 59U && time->ms <= 999U;
}
/*
* 函数功能:将公历日期转换为自 1970-01-01 起的完整天数。
* 入口参数:year、month、day——已校验的合法日期,年份须在 1970..2099 内。
* 返回值:完整天数,0 对应 1970-01-01;本内部函数不重复校验参数。
* 说明:将三月作为年首,一、二月归入上一年,使闰日统一落在年末。
* era 为 400 年周期编号,yoe 为周期内年份,mp 为从三月起算的月份编号 0..11。
* doy 为从三月起算的年内天数,doe 为周期内天数;每 400 年有 146097 天。
* 最后减去 719468,将算法的天数原点平移到 1970-01-01。
*/
static uint32_t Days_From_Civil(uint32_t year, uint32_t month, uint32_t day)
{
uint32_t y = (month <= 2U) ? (year - 1U) : year;
uint32_t era = y / 400U;
uint32_t yoe = y - era * 400U;
uint32_t mp = (month > 2U) ? (month - 3U) : (month + 9U);
uint32_t doy = (153U * mp + 2U) / 5U + day - 1U;
uint32_t doe = yoe * 365U + yoe / 4U - yoe / 100U + doy;
return era * 146097U + doe - 719468U;
}
/*
* 函数功能:将已校验的日历字段展开为相对 1970-01-01 00:00:00 的线性秒数。
* 入口参数:time——非空且已通过 WallTime_Is_Valid 检查的日历值。
* 返回值:0..IEC60870_TIME_MAX_SECONDS 的完整秒数,不包含毫秒。
* 说明:此处只计算天数和日内秒数,忽略 ms、weekday,不处理时区或 SU。
* 只有输入日历本身采用 UTC 时,结果才可直接作为 UTC 纪元秒数使用。
*/
static uint32_t Seconds_From_Civil(const IEC60870_WallTime_t *time)
{
return Days_From_Civil(time->year, time->month, time->day) * 86400U +
(uint32_t)time->hour * 3600U + (uint32_t)time->minute * 60U + (uint32_t)time->second;
}
/*
* 函数功能:根据自 1970-01-01 起的完整天数计算星期。
* 入口参数:days——模块支持年份内的天数,0 对应 1970-01-01。
* 返回值:0=周日,1=周一,...,6=周六。
* 说明:纪元日是周四,因此先加 4 再对 7 取余;本内部函数不检查日期范围。
*/
static uint8_t Weekday_From_Days(uint32_t days)
{
return (uint8_t)((days + 4U) % 7U);
}
/*
* 函数功能:供时间源适配等调用方复用日历合法性检查,不把本地日历当作UTC转秒数。
* 入口参数:time——待检查的日历指针,可以为空;weekday不参与检查。
* 返回值:1 年月日、时分秒和毫秒在模块范围内;0 参数非法。
*/
int IEC60870_Time_Is_Valid(const IEC60870_WallTime_t *time)
{
return WallTime_Is_Valid(time);
}
/*
* 函数功能:校验公历日期并计算星期。
* 入口参数:year——1970..2099;month——1..12;day——当月日号。
* 出口参数:weekday——接收 0..6 的星期值,0 表示周日。
* 返回值:1 成功;0 日期非法或输出指针为空,失败时输出不变。
*/
int IEC60870_Time_Get_Weekday(uint16_t year, uint8_t month, uint8_t day, uint8_t *weekday)
{
if (weekday == NULL || !Date_Is_Valid(year, month, day)) {
return 0;
}
*weekday = Weekday_From_Days(Days_From_Civil(year, month, day));
return 1;
}
/*
* 函数功能:将 UTC 日历转换为自 1970-01-01 00:00:00 UTC 起的完整秒数。
* 入口参数:time——1970..2099 年的合法 UTC 日历;ms 须在 0..999 内。
* 出口参数:seconds——接收 0..IEC60870_TIME_MAX_SECONDS;0 是有效时间值。
* 返回值:1 成功;0 参数非法,失败时输出不变。
* 说明:日历类型没有时区或 SU,调用方须保证输入采用 UTC;ms 只检查范围,不计入完整秒数。
* 含 IV/SU 的 CP56 解析值应使用 CP56_To_Seconds,不能直接丢弃标志后调用本函数。
*/
int IEC60870_Time_To_Seconds(const IEC60870_WallTime_t *time, uint32_t *seconds)
{
uint32_t result;
if (seconds == NULL || !WallTime_Is_Valid(time)) {
return 0;
}
result = Seconds_From_Civil(time);
*seconds = result;
return 1;
}
/*
* 函数功能:将 UTC 纪元秒数和毫秒余数还原为完整公历时间。
* 入口参数:seconds——0..IEC60870_TIME_MAX_SECONDS;ms——0..999。
* 出口参数:time——接收 UTC 年月日、时分秒、毫秒及重新计算的 weekday。
* 返回值:1 成功;0 参数非法或平台转换失败,失败时输出不变,不对越界毫秒进行截断或进位。
* 说明:编译时选择平台库或整数算法;使用局部结果,全部完成后才写回调用方输出。
*/
int IEC60870_Time_From_Seconds(uint32_t seconds, uint16_t ms, IEC60870_WallTime_t *time)
{
IEC60870_WallTime_t result = {0};
#if IEC60870_TIME_USE_PLATFORM
__time64_t stamp;
struct tm utc;
#else
uint32_t days, rem, z, era, doe, yoe, doy, mp, month, year;
#endif
if (time == NULL || seconds > IEC60870_TIME_MAX_SECONDS || ms > 999U) {
return 0;
}
#if IEC60870_TIME_USE_PLATFORM
/* 显式使用 64 位接口,避免 32 位 time_t 的 2038 年上限;转换结果始终为 UTC。 */
stamp = (__time64_t)seconds;
if (_gmtime64_s(&utc, &stamp) != 0) {
return 0;
}
result.year = (uint16_t)(utc.tm_year + 1900);
result.month = (uint8_t)(utc.tm_mon + 1);
result.day = (uint8_t)utc.tm_mday;
result.hour = (uint8_t)utc.tm_hour;
result.minute = (uint8_t)utc.tm_min;
result.second = (uint8_t)utc.tm_sec;
result.weekday = (uint8_t)utc.tm_wday;
#else
/* 先分成完整天数和日内秒数,日期换算与时分秒拆分分别进行。 */
days = seconds / 86400U;
rem = seconds % 86400U;
/*
* 与 Days_From_Civil 反向对应:先恢复以三月为年首的天数,再求 400 年周期及周期内年份。
* yoe 中的修正项处理闰日、世纪和周期末日,避免将周期最后一天归入下一周期。
* mp 先得到从三月起算的月份编号,再还原为 1..12;一、二月归回下一公历年。
*/
z = days + 719468U;
era = z / 146097U;
doe = z - era * 146097U;
yoe = (doe - doe / 1460U + doe / 36524U - doe / 146096U) / 365U;
doy = doe - (365U * yoe + yoe / 4U - yoe / 100U);
mp = (5U * doy + 2U) / 153U;
month = (mp < 10U) ? (mp + 3U) : (mp - 9U);
year = yoe + era * 400U + ((month <= 2U) ? 1U : 0U);
result.year = (uint16_t)year;
result.month = (uint8_t)month;
result.day = (uint8_t)(doy - (153U * mp + 2U) / 5U + 1U);
result.hour = (uint8_t)(rem / 3600U);
result.minute = (uint8_t)((rem % 3600U) / 60U);
result.second = (uint8_t)(rem % 60U);
result.weekday = Weekday_From_Days(days);
#endif
result.ms = ms;
*time = result;
return 1;
}
/*
* 函数功能:将 UTC 纪元秒数和毫秒直接展开为固定 UTC+8 的本地标准时间。
* 入口参数:utc_seconds——未加时区偏移的 UTC 纪元秒数;ms——0..999。
* 出口参数:local_time——本地年月日、时分秒、毫秒及重新计算的星期。
* 返回值:1 成功;0 参数非法、本地日期超出 1970..2099 或平台转换失败,失败时输出不变。
* 说明:先检查再平移秒数,只展开一次日历;调用方无需手动调整小时、日期或星期。
*/
int IEC60870_Time_Seconds_To_Local(uint32_t utc_seconds, uint16_t ms,
IEC60870_WallTime_t *local_time)
{
const uint32_t local_offset_seconds = (uint32_t)IEC60870_LOCAL_UTC_OFFSET_MINUTES * 60U;
if (local_time == NULL || ms > 999U ||
utc_seconds > IEC60870_TIME_MAX_SECONDS - local_offset_seconds) {
return 0;
}
/* 平移值只用于展开本地日历,不作为新的 UTC 时间戳保存或返回。 */
return IEC60870_Time_From_Seconds(utc_seconds + local_offset_seconds, ms, local_time);
}
/*
* 函数功能:将源日历、SU 和源标准时区一起换算为有效的 UTC 纪元秒数。
* 入口参数:value——合法的 2000..2099 年 CP56 解析值,IV=0,SU/has_weekday 为 0/1;
* standard_utc_offset_minutes——源标准当地时间减 UTC 的分钟数,范围 -1439..1439,
* 不包含夏令时;源标准时区 UTC+8 填 480,即使 SU=1 也不改变该偏移。
* 出口参数:seconds——接收 UTC 完整秒数,毫秒仍由输入日历保存。
* 返回值:1 成功;0 参数非法、IV=1 或结果越界,失败时输出不变。
* 说明:原始解码尚未修正 SU;在本函数中扣除一小时后,不应再由业务重复扣减。
*/
int IEC60870_Time_CP56_To_Seconds(const IEC60870_CP56Time2a_t *value,
int16_t standard_utc_offset_minutes, uint32_t *seconds)
{
int64_t utc_seconds;
/*
* 标准时区偏移不包含夏令时,绝对值须小于一天;这是本接口的参数范围。
* IV=1 的时间可被原始编解码保留,但不能在这里作为有效时间转换;SU=1 则正常参与换算。
*/
if (value == NULL || seconds == NULL || !WallTime_Is_Valid(&value->time) ||
value->time.year < IEC60870_CP56_MIN_YEAR || value->time.year > IEC60870_CP56_MAX_YEAR ||
value->is_invalid != 0U || value->is_summer_time > 1U || value->has_weekday > 1U ||
standard_utc_offset_minutes < -1439 || standard_utc_offset_minutes > 1439) {
return 0;
}
/*
* 标准时间 = 报文日历 - SU * 3600;UTC = 标准时间 - 标准时区偏移。
* 在秒数上调整可跨日/月/年;有符号 64 位中间值避免 2038 年问题及加减回绕。
* 原日历和 SU 保持配套,不能先改小时再保留 SU,否则再次换算会重复扣减。
*/
utc_seconds = (int64_t)Seconds_From_Civil(&value->time) -
(int64_t)value->is_summer_time * INT64_C(3600) -
(int64_t)standard_utc_offset_minutes * INT64_C(60);
/* 在有符号域中检查最终值,再转 uint32_t,避免负数或超范围结果被当成有效纪元秒数。 */
if (utc_seconds < 0 || utc_seconds > (int64_t)IEC60870_TIME_MAX_SECONDS) {
return 0;
}
/* 读取输入和检查结果范围后才提交输出;毫秒余数仍取 value->time.ms。 */
*seconds = (uint32_t)utc_seconds;
return 1;
}
/*
* 函数功能:将 CP56 解析值转换为固定 UTC+8 的本地标准时间日历。
* 入口参数:value——合法 CP56 解析值,须保留配套的源日历及 IV/SU;
* source_standard_utc_offset_minutes——源标准时区偏移,单位为分钟,范围 -1439..1439,
* 不包含夏令时;源标准时区与本地相同时传 IEC60870_LOCAL_UTC_OFFSET_MINUTES。
* 出口参数:local_time——接收本地标准时间年月日、时分秒、毫秒和重新计算的星期。
* 返回值:1 成功;0 参数非法、IV=1、转换结果越界或平台转换失败,失败时输出不变。
* 说明:通常在 From_CP56 成功后调用;输出已处理 SU 和时区,业务不再加八小时或减一小时。
*/
int IEC60870_Time_CP56_To_Local(const IEC60870_CP56Time2a_t *value,
int16_t source_standard_utc_offset_minutes,
IEC60870_WallTime_t *local_time)
{
uint32_t utc_seconds;
/* 下层已统一检查 IV、SU、源偏移和日期,并完成夏令时修正;本层不再重复扣除一小时。 */
if (local_time == NULL ||
!IEC60870_Time_CP56_To_Seconds(value, source_standard_utc_offset_minutes, &utc_seconds)) {
return 0;
}
/* 与直接从时间戳取本地时间共用入口,统一处理上限、跨日/月/年和星期。 */
return IEC60870_Time_Seconds_To_Local(utc_seconds, value->time.ms, local_time);
}
/*
* 函数功能:将日历及 IV/SU/星期标志编码为七字节 CP56Time2a。
* 入口参数:value——合法的 2000..2099 年日历,三个标志均为 0/1;
* capacity——out 的实际可写字节数,至少 IEC60870_CP56_LEN。
* 出口参数:out——只写前七字节,超出部分保持原样。
* 返回值:1 成功;0 参数非法,失败时整个输出缓冲区不变。
* 说明:公共编码只校验并写入日历和标志;业务发送统一设置 SU=0 的约束由后续业务入口落实。
*/
int IEC60870_Time_To_CP56(const IEC60870_CP56Time2a_t *value, uint8_t *out, size_t capacity)
{
uint8_t encoded[IEC60870_CP56_LEN];
uint8_t weekday = 0U;
uint16_t msec;
if (value == NULL || out == NULL || capacity < IEC60870_CP56_LEN ||
!WallTime_Is_Valid(&value->time) ||
value->time.year < IEC60870_CP56_MIN_YEAR || value->time.year > IEC60870_CP56_MAX_YEAR ||
value->is_invalid > 1U || value->is_summer_time > 1U || value->has_weekday > 1U) {
return 0;
}
/* 报文星期 0 表示未使用;携带星期时由日期重算,将内存中的周日 0 转成报文值 7。 */
if (value->has_weekday != 0U) {
weekday = Weekday_From_Days(Days_From_Civil(value->time.year, value->time.month, value->time.day));
weekday = (uint8_t)((weekday == 0U) ? 7U : weekday);
}
/* CP56 前两字节保存分钟内毫秒,范围 0..59999,由秒和毫秒余数组合而成。 */
msec = (uint16_t)((uint32_t)value->time.second * 1000U + value->time.ms);
/* 逐字节布局,不把结构体转成报文,也不要求输出缓冲区对齐。 */
encoded[0] = (uint8_t)(msec & 0xFFU);
encoded[1] = (uint8_t)(msec >> 8);
encoded[2] = (uint8_t)(value->time.minute | (uint8_t)(value->is_invalid << 7));
/*
* SU 按输入写入 bit7,不改变小时值;小时字段及 SU 已校验,保留位保持为 0。
* 这里不能直接清除 SU=1:若小时仍是夏令时值,单独清标志会改变报文所表达的时刻。
*/
encoded[3] = (uint8_t)(value->time.hour | (uint8_t)(value->is_summer_time << 7));
/* 日号占低五位、星期占高三位;月份和两位年份单独存储,未使用的高位保持为 0。 */
encoded[4] = (uint8_t)(value->time.day | (uint8_t)(weekday << 5));
encoded[5] = value->time.month;
encoded[6] = (uint8_t)(value->time.year - IEC60870_CP56_MIN_YEAR);
/* 校验和读取输入完成后才统一提交输出;也允许 out 与 value 的存储重叠。 */
memcpy(out, encoded, IEC60870_CP56_LEN);
return 1;
}
/*
* 函数功能:校验并解码前七字节 CP56Time2a,保留源日历及 IV/SU。
* 入口参数:in——报文起点;length——实际可读字节数,至少 IEC60870_CP56_LEN。
* 出口参数:value——接收日历、IV、SU 和是否携带星期;weekday 始终按日期重新计算。
* 返回值:1 成功;0 指针、长度、保留位、日期时间或非零星期不合法,失败时输出不变。
* 说明:后续字节不处理;SU=1 或 IV=1 本身不导致解码失败,仍要求日期及其余字段合法。
* 成功只表示满足格式约束,尚未进行时间转换,也不表示该时间可以用于校时。
*/
int IEC60870_Time_From_CP56(const uint8_t *in, size_t length, IEC60870_CP56Time2a_t *value)
{
IEC60870_CP56Time2a_t result = {0};
uint16_t msec;
uint8_t received_weekday, expected_weekday;
if (in == NULL || value == NULL || length < IEC60870_CP56_LEN) {
return 0;
}
/*
* 本模块要求分钟 bit6、小时 bit5..6、月份 bit4..7、年份 bit7 为 0。
* 非零保留位直接拒绝,不能用掩码静默丢弃;IV 和 SU 为有效标志,另外提取。
*/
if ((in[2] & 0x40U) != 0U || (in[3] & 0x60U) != 0U ||
(in[5] & 0xF0U) != 0U || (in[6] & 0x80U) != 0U) {
return 0;
}
/* 按低字节在前还原分钟内毫秒;超过 59999 不能表示合法的秒与毫秒余数。 */
msec = (uint16_t)((uint16_t)in[0] | ((uint16_t)in[1] << 8));
if (msec > 59999U) {
return 0;
}
result.time.year = (uint16_t)(IEC60870_CP56_MIN_YEAR + in[6]);
result.time.month = (uint8_t)(in[5] & 0x0FU);
result.time.day = (uint8_t)(in[4] & 0x1FU);
result.time.hour = (uint8_t)(in[3] & 0x1FU);
result.time.minute = (uint8_t)(in[2] & 0x3FU);
result.time.second = (uint8_t)(msec / 1000U);
result.time.ms = (uint16_t)(msec % 1000U);
result.is_invalid = (uint8_t)((in[2] >> 7) & 1U);
/* 与原始小时配套保留 SU,交给后续转换接口处理,避免丢失信息或重复扣除一小时。 */
result.is_summer_time = (uint8_t)((in[3] >> 7) & 1U);
/* 报文星期 0 表示未提供星期;内存中的 weekday=0 表示周日,需由 has_weekday 区分。 */
received_weekday = (uint8_t)(in[4] >> 5);
result.has_weekday = (uint8_t)(received_weekday != 0U);
/* 年份 100..127 会展开为 2100..2127,在此随其他无效日期一起拒绝,不自动映射世纪。 */
if (!WallTime_Is_Valid(&result.time)) {
return 0;
}
/* 无论报文是否携带星期,都从日期计算输出;非零报文星期还须与计算结果一致。 */
result.time.weekday = Weekday_From_Days(Days_From_Civil(result.time.year, result.time.month,
result.time.day));
expected_weekday = (uint8_t)((result.time.weekday == 0U) ? 7U : result.time.weekday);
if (received_weekday != 0U && received_weekday != expected_weekday) {
return 0;
}
/*
* IV=1 仍可供业务记录;取有效的本地标准时间时,应将完整结果交给 CP56_To_Local 检查和转换。
* 是否允许校时、远端时间是否合理仍由业务判断,此处不设置 RTC。
*/
*value = result;
return 1;
}
@@ -0,0 +1,244 @@
/*
* 模块名称:IEC 60870-5-101/104 公共时间工具
* 模块功能:CP16/CP24/CP56编解码、公历日期/星期、本地标准时间及纪元秒数工具。
* 适用协议:101 和 104 的时间格式共用同一实现,独立于各自的帧格式和链路层。
* 职责边界:公共编解码保留 IV/SU,允许合法的 SU=0/1,不调整输入日历。
* 业务接收时通过 CP56_To_Local 检查 IV、处理 SU 和源时区,取得本地标准时间日历。
* 后续业务发送入口须保证日历已是本地标准时间,并统一设置 SU=0。
* 接入状态:IEC60870_Asdu 已复用 CP56 编解码;事件采集、时间源读取和实际校时仍由业务接入。
* 适用平台:可选平台库或纯整数日期转换;不读取系统时钟、不设置 RTC、不推断时区规则或处理链路定时器。
*/
#ifndef IEC60870_TIME_H
#define IEC60870_TIME_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* CP16为经过时间;CP24仅含分钟及分钟内毫秒,不能单独恢复完整日期。 */
#define IEC60870_CP16_LEN 2U
#define IEC60870_CP24_LEN 3U
#define IEC60870_CP56_LEN 7U
#define IEC60870_TIME_MIN_YEAR 1970U
#define IEC60870_TIME_MAX_YEAR 2099U
/* 两位年份按 2000 年起解释是本实现的世纪约定,须与通信对端一致。 */
#define IEC60870_CP56_MIN_YEAR 2000U
#define IEC60870_CP56_MAX_YEAR 2099U
/* 2099-12-31 23:59:59 UTC;本模块不接受 uint32_t 可表示但超出约定年份的秒数。 */
#define IEC60870_TIME_MAX_SECONDS UINT32_C(4102444799)
/*
* 本地标准时区固定为 UTC+8,不读取系统时区。
* 源标准时区也为 UTC+8 时,转换接口传入此值;即使报文 SU=1,源标准偏移仍为 480。
* 其他源时区由调用方按双方约定提供标准偏移,不能一律传入此值。
*/
#define IEC60870_LOCAL_UTC_OFFSET_MINUTES 480
/*
* 时间戳转日历的编译选项:1 使用平台库,0 使用本模块保留的纯整数算法。
* 当前平台库后端为 Windows CRT 的 _gmtime64_s;Windows 默认 1,其他平台默认 0。
* 无合适时间库时,在编译 IEC60870_Time.c 时定义 IEC60870_TIME_USE_PLATFORM=0。
* 两种实现均输出 UTC,支持相同的 1970..2099 年范围,不读取当前时间或本地时区。
*/
#ifndef IEC60870_TIME_USE_PLATFORM
#ifdef _WIN32
#define IEC60870_TIME_USE_PLATFORM 1
#else
#define IEC60870_TIME_USE_PLATFORM 0
#endif
#endif
typedef struct {
uint16_t milliseconds; /* CP16经过时间,0..65535毫秒,无IV或SU。 */
} IEC60870_CP16Time2a_t;
typedef struct {
uint16_t milliseconds; /* CP24分钟内毫秒,0..59999,包含秒。 */
uint8_t minute; /* 0..59;不含小时、日期或时区。 */
uint8_t is_invalid; /* IV,0/1;编码和解码保留。 */
} IEC60870_CP24Time2a_t;
/**
* 功能:编码CP16经过时间或CP24短时标。
* 输入参数:value——非空解析值;capacity——输出容量,分别至少2/3字节。
* 输出参数:out——低字节在前的协议字节,不改变输入时间基准。
* 返回值:1成功;0参数或字段非法,输出不变。
* 说明:CP24保留IV但检查分钟和毫秒范围,不推断小时或日期,不携带SU。
*/
int IEC60870_Time_To_CP16(const IEC60870_CP16Time2a_t *value, uint8_t *out, size_t capacity);
int IEC60870_Time_To_CP24(const IEC60870_CP24Time2a_t *value, uint8_t *out, size_t capacity);
/**
* 功能:解码CP16经过时间或CP24短时标。
* 输入参数:in——非空字节串;length——可读长度,分别至少2/3字节。
* 输出参数:value——成功后保存独立解析值,失败不变。
* 返回值:1成功;0参数、长度、CP24保留位或字段非法。
* 说明:只读取前2/3字节;CP24的IV=1仍可解析,但不表示时间有效。
*/
int IEC60870_Time_From_CP16(const uint8_t *in, size_t length, IEC60870_CP16Time2a_t *value);
int IEC60870_Time_From_CP24(const uint8_t *in, size_t length, IEC60870_CP24Time2a_t *value);
/*
* 公历时间值,不携带时区或 SU;Seconds_To_Local、CP56_To_Local 输出本地标准时间,
* From_Seconds 输出 UTC。
* 调用 To_Seconds 时须提供 UTC 日历,不能把本地标准时间日历直接当成 UTC 转秒数。
* weekday 为派生字段:0=周日,1=周一,...,6=周六。
* 编码及转秒数时忽略传入的 weekday;解码及从秒数转换时按日期重新计算。
* 此类型仅供内存中传值,不能直接作为 CP56Time2a 的报文字节布局。
*/
typedef struct {
uint16_t year; /* 普通日历为 1970..2099;CP56 编解码限于 2000..2099 */
uint8_t month; /* 1..12 */
uint8_t day; /* 1..当月实际天数 */
uint8_t hour; /* 0..23 */
uint8_t minute; /* 0..59 */
uint8_t second; /* 0..59,不表示闰秒 */
uint16_t ms; /* 0..999 */
uint8_t weekday; /* 0..6,输出时由日期推导 */
} IEC60870_WallTime_t;
/*
* CP56Time2a 的解析值;标志与普通日历值分开,避免丢失报文中的时间质量信息。
* is_invalid、is_summer_time 和 has_weekday 只能为 0 或 1。
* 编码由 has_weekday 决定是否携带星期,IV 和 SU 按输入保留。
* IV=1 仍可编解码,但日期字段仍须合法;成功不表示该时间可用于校时。
* 标准允许选用 SU 但不推荐使用;本项目后续业务发送入口负责统一设置 SU=0。
* 公共编解码支持 SU=0/1,不强制清零;发送前须确保日历与标志一致,不能只清 SU 而保留夏令时日历。
* SU 不携带时区信息;取本地标准时间时将完整解析值及源标准时区偏移交给 CP56_To_Local。
*/
typedef struct {
IEC60870_WallTime_t time; /* 2000..2099;按双方约定的时间基准及 SU 解释 */
uint8_t is_invalid; /* IV:1=时间无效,0=未置无效标志 */
uint8_t is_summer_time; /* SU:0=标准时间,1=夏令时;原始编解码保留,CP56 转换接口在为 1 时扣除一小时 */
uint8_t has_weekday; /* 1=报文带星期,0=报文星期域为 0(未使用) */
} IEC60870_CP56Time2a_t;
/*
* 所有接口:成功返回 1;参数/格式非法、结果越界、平台转换失败或将 IV=1 转为有效时间时返回 0。
* 返回 0 时不修改任何输出;调用方仍须保证指针有效且实际空间不少于所报长度。
* 模块不维护共享可变状态或动态内存。输入和输出允许重叠。
* 成功写回输出时,会覆盖与输出重叠的输入存储;需要保留原始输入时应使用独立输出存储。
*/
/**
* 功能:检查日历的年月日、时分秒及毫秒是否合法,不进行时间转换。
* 输入:time——待检查的日历值;年份1970..2099,毫秒0..999。
* 返回:1 数值合法;0 空指针、无效日期或字段越界。
* 说明:忽略派生字段weekday;不判断时区、时钟是否准确或是否允许校时。
*/
int IEC60870_Time_Is_Valid(const IEC60870_WallTime_t *time);
/**
* 功能:检查公历日期并计算星期。
* 输入:year——1970..2099;month、day——合法月份和日期。
* 输出:weekday——0=周日,1=周一,...,6=周六。
* 返回:1 成功;0 日期非法或输出为空,输出不变。
*/
int IEC60870_Time_Get_Weekday(uint16_t year, uint8_t month, uint8_t day, uint8_t *weekday);
/**
* 功能:将 UTC 日期和时间转换为自 1970-01-01 00:00:00 UTC 起的完整秒数。
* 输入:time——年份 1970..2099,毫秒 0..999,其余字段须组成合法时间。
* 输出:seconds——0..IEC60870_TIME_MAX_SECONDS;0 是有效纪元时间,不表示失败。
* 返回:1 成功;0 参数非法,输出不变。
* 说明:ms 参与范围校验,但不计入完整秒数;需保留亚秒精度时另行保存 time->ms。
* 忽略 weekday,不进行本地时区换算,也不计闰秒。
* time 没有时区或 SU,函数无法检查其是否真的采用 UTC,须由调用方保证。
* 对 CP56 解析值,取本地标准时间调用 CP56_To_Local;取纪元秒数调用 CP56_To_Seconds。
*/
int IEC60870_Time_To_Seconds(const IEC60870_WallTime_t *time, uint32_t *seconds);
/**
* 功能:将纪元秒数和毫秒余数转换为 UTC 日期和时间。
* 输入:seconds——0..IEC60870_TIME_MAX_SECONDS;ms——0..999。
* 输出:time——完整公历时间及按日期计算的 weekday。
* 返回:1 成功;0 参数非法或平台转换失败,输出不变;不会把越界毫秒截断成 999。
* 说明:由 IEC60870_TIME_USE_PLATFORM 选择平台库或纯整数算法;均不做本地时区换算。
*/
int IEC60870_Time_From_Seconds(uint32_t seconds, uint16_t ms, IEC60870_WallTime_t *time);
/**
* 功能:将 UTC 纪元秒数和毫秒余数直接转换为本地标准时间日历(固定 UTC+8)。
* 输入:utc_seconds——未加时区偏移的 UTC 纪元秒数;ms——0..999。
* 本地日期须在 1970..2099 年内,因此 utc_seconds 上限为
* IEC60870_TIME_MAX_SECONDS - IEC60870_LOCAL_UTC_OFFSET_MINUTES * 60。
* 输出:local_time——本地年月日、时分秒、毫秒及重新计算的星期。
* 返回:1 成功;0 参数非法、本地日期越界或平台转换失败,输出不变。
* 说明:内部统一处理时区偏移及跨日/月/年,调用方不再加八小时。
* 不读取系统时区或开启夏令时;已是固定 UTC+8 标准时间的 RTC 日历可直接用于 CP56 编码。
*/
int IEC60870_Time_Seconds_To_Local(uint32_t utc_seconds, uint16_t ms,
IEC60870_WallTime_t *local_time);
/**
* 功能:将 CP56Time2a 解析值转换为 UTC 纪元完整秒数,SU=1 时扣除一小时。
* 输入:value——合法的 2000..2099 年日期时间,IV=0,SU/has_weekday 为 0/1;
* standard_utc_offset_minutes——源报文标准当地时间减 UTC 的分钟数,范围 -1439..1439,
* 向东为正、向西为负;UTC 填 0,UTC+8 填 480,UTC-5 填 -300。
* 此偏移必须不含夏令时的一小时;SU 会单独扣除,不能重复计算。
* 输出:seconds——0..IEC60870_TIME_MAX_SECONDS 的 UTC 秒数。
* 返回:1 成功;0 参数非法、IV=1 或换算结果越界,输出不变。
* 说明:UTC = 报文日历对应秒数 - SU * 3600 - 标准时区偏移 * 60。
* 在秒数上运算,支持跨日/月/年;年份 2000 的输入可换算为 1999 年的 UTC。
* ms 校验后不计入完整秒数;未与输出重叠时,输入日期、SU 和毫秒均保持不变。
* 忽略传入的 weekday;不根据日期猜测夏令时,不访问系统时区或设置 RTC。
*/
int IEC60870_Time_CP56_To_Seconds(const IEC60870_CP56Time2a_t *value,
int16_t standard_utc_offset_minutes, uint32_t *seconds);
/**
* 功能:将 CP56Time2a 解析值转换为本地标准时间日历(固定 UTC+8),并处理 SU。
* 输入:value——合法的 2000..2099 年日期时间,IV=0,SU/has_weekday 为 0/1;
* source_standard_utc_offset_minutes——源报文标准当地时间减 UTC 的分钟数,
* 范围 -1439..1439,不含夏令时;双方约定本地标准时间时传
* IEC60870_LOCAL_UTC_OFFSET_MINUTES(480),源报文为 UTC 时传 0。
* 输出:local_time——1970..2099 年的本地标准时间年月日、时分秒、毫秒及重新计算的星期。
* 返回:1 成功;0 参数非法、IV=1、本地标准时间越界或平台转换失败,输出不变。
* 说明:本地标准时间 = 报文日历 - SU * 一小时 - 源标准时区偏移 + 八小时。
* 源标准时区为 UTC+8 时,SU=0 保持原时间,SU=1 减一小时,自动处理跨日/月/年。
* 毫秒原样保留;日历结果通过 local_time 输出,函数返回值仅表示成功或失败。
* 输入与输出未重叠时,不改写输入日历或 SU。
* 结果已是本地标准时间,调用方无需再次加八小时或减夏令时的一小时。
* 业务再次发送此日历时应构造新的 CP56 值并置 SU=0,不能沿用原报文的 SU=1。
*/
int IEC60870_Time_CP56_To_Local(const IEC60870_CP56Time2a_t *value,
int16_t source_standard_utc_offset_minutes,
IEC60870_WallTime_t *local_time);
/**
* 功能:将 CP56Time2a 解析值编码为 7 字节报文,毫秒域按低字节在前写入。
* 输入:value——合法的 2000..2099 年日期时间,三个标志字段均为 0/1;
* capacity——out 可写字节数,至少 IEC60870_CP56_LEN。
* 输出:out——仅写前 7 字节,保留位清零,IV 和 SU 按输入保留。
* 返回:1 成功;0 参数非法,整个输出缓冲区不变。
* 说明:has_weekday=1 时由日期计算报文星期(1=周一..7=周日),否则填 0。
* 不校时、不转换时区,也不因 SU=1 而调整小时值。
* SU=0/1 均可编码,本接口不会自动清零或因 SU=1 而拒绝合法输入。
* 后续业务发送入口负责传入已是本地标准时间的日历并设置 SU=0;此处只执行公共编码。
* IV=1 不放宽日期、时间及标志取值的检查。
*/
int IEC60870_Time_To_CP56(const IEC60870_CP56Time2a_t *value, uint8_t *out, size_t capacity);
/**
* 功能:检查并解析前 7 字节 CP56Time2a,后续字节留给调用方处理。
* 输入:in——报文起点;length——可读字节数,至少 IEC60870_CP56_LEN。
* 输出:value——源报文的日历时间、IV、SU 及报文是否携带星期,尚未转换为本地标准时间;
* weekday 始终由日期计算。
* 返回:1 成功;0 参数、保留位、日期时间非法,输出不变。
* 说明:年份域只接受 0..99,解释为 2000..2099;星期域 0 表示未使用,
* 非零星期必须与日期一致。分钟 bit6 等保留位必须为 0,不接收扩展含义。
* SU=1 本身不导致失败,IV/SU 均原样返回;不转换时区或夏令时。
* IV=1 且其余字段合法时仍可成功解析,供业务记录;两个 CP56 转换接口会拒绝该无效时间。
* 解码后取本地标准时间调用 CP56_To_Local;需要纪元秒数时调用 CP56_To_Seconds。
* 转换前须保留完整的日历及 SU,不可先清 SU,也不可先减一小时再携带 SU=1 转换。
* 业务仍须检查时间约定及校时权限;不自动设置 RTC。
*/
int IEC60870_Time_From_CP56(const uint8_t *in, size_t length, IEC60870_CP56Time2a_t *value);
#ifdef __cplusplus
}
#endif
#endif /* IEC60870_TIME_H */
@@ -0,0 +1,29 @@
/*
* 101/104共用的应用层值类型:ASDU字段宽度和总长上限。
* 只依赖标准整数类型,不包含传输帧、连接、队列或平台接口。
* 调用方按接入协议显式选择参数;104的默认配置由104接入层提供。
*/
#ifndef IEC60870_TYPES_H
#define IEC60870_TYPES_H
#include <stdint.h>
#define IEC60870_MAX_ASDU_LEN 254 /* 通用101/104 ASDU存储上限;104传输仍限249。 */
/* 应用层参数描述ASDU字段宽度及长度上限,字段使用int以便校验原始范围。
* Type ID/VSQ固定为1字节;COT/CA可为1或2字节,IOA可为1、2或3字节。
* 通用ASDU上限为254;接入本工程104 TCP时使用2/2/3及不超过249的上限。
* 参数由调用方持有,构造器及解析视图借用它;使用期间必须有效且不可修改。
* originatorAddress供业务作为发送OA的默认值,报文构造仍以header中显式OA为准;
* COT为1字节时不编码OA,解析得到的OA为0。配置需要双方一致,不自动协商。 */
typedef struct {
int sizeOfTypeId; /* 固定1,不支持修改。 */
int sizeOfVSQ; /* 固定1,不支持修改。 */
int sizeOfCOT; /* 1或2;2字节时包含OA。 */
int originatorAddress; /* 业务默认源发地址,0..255。 */
int sizeOfCA; /* 公共地址长度,1或2。 */
int sizeOfIOA; /* 信息对象地址长度,1、2或3。 */
int maxSizeOfASDU; /* 包含公共头;至少容纳头,最多254。 */
} IEC60870_AppLayerParameters_t;
#endif /* IEC60870_TYPES_H */
@@ -0,0 +1,218 @@
# IEC104 子站连接协调层
`IEC104_Connection.c/h`是TCP内部的连接协调模块,把接收器、链路状态和发送管理接成一条子站连接。TcpServer提供TCP字节、实际写入结果和单调时间;Connection安排模块调用顺序,不创建socket、线程或系统定时器。
业务的连接与收发统一通过`IEC104_Slave.h`接入,Slave内部持有并调用TcpServer。本文供维护TCP内部实现时阅读,下文的调用方指TcpServer;业务不包含Connection或SendManager头文件,不创建这些对象,也不调用其接口。业务接入示例见[TCP服务设计](IEC104_TcpServer_设计架构.md#4-业务接入)。
## 1. 对象与存储
每个`IEC104_Connection_t`内含独立的Receiver、CONTROLLED角色Link、SendBuffer及SendManager。Manager引用该对象内的Link和SendBuffer,因此**Init成功后不能复制或移动Connection**。所有操作在同一个执行上下文串行进行,内含对象的字段和修改接口不再由业务直接操作。
TcpServer持有Connection,并提供一个ASDU队列、一个控制槽数组及一个发送记录数组。ASDU队列随服务初始化,位于Connection之外,首次连接前和断线期间均可缓存NORMAL原文;它仍属于TcpServer内部存储。各对象、数组、输入和输出存储不得重叠;所有借用存储在Close前持续有效。无动态分配、无业务回调。
各模块的协作边界如下:
| 模块 | 自己维护的规则 | Connection使用方式 |
|---|---|---|
| Link | 启停门控、停止排空、S确认条件和探测进度 | `Can_Prepare_*`查询控制帧准备资格;`Get_Status`提供观察状态和未确认接收数量 |
| Receiver | 半帧/完整帧进度、原文保留及绑定 | `Get_Status`区分新成帧与旧帧;`Peek/Release`交付原文;`Abort`负责清理 |
| SendManager | Link/Buffer初始化及运行期修改、帧与记录关联、ACK交接 | `Submit_Response`准备应答或入HIGH;`Handle_*`统一接收和完成;`Get_Status`提供记录概况;`Get_Deadlines`提供发送期限、队首身份及控制容量 |
| Connection | 调用顺序、过程时间、故障及连接代次 | 先推进STOP确认,再推进普通确认及t3;状态变化立即交给内部通知适配器 |
Link查询与对应Prepare共用同一份准入判断;查询不构帧、不预留控制槽、不消耗帧编号。返回可准备后,Manager仍须检查实际容量并完成准备,WAIT留待下一次驱动。同一连接全程串行调用。
运行期间Connection对Link仅作只读访问,Link和SendBuffer的修改统一由SendManager提交;TESTFR_CON经Manager处理,发送状态也由Manager查询,不直接操作SendBuffer。Connection和SendManager的完整结构体定义保留在内部头文件中,供上层内部对象静态嵌入;TcpServer的公开头文件隐藏这些定义。协议配置统一定义为`IEC104_Types.h`中的`IEC104_ProtocolConfig_t`,本层保留`IEC104_ConnectionConfig_t`内部别名。初始化通过Manager在最终Link/Buffer地址建立绑定,Connection不再修补其内部指针;成功后的对象地址仍须保持不变。
内部初始化关系示例(展示对象绑定顺序;实际存储位于TcpServer内部,业务使用TcpServer的Create/Init):
```c
#include "IEC104_Connection.h"
static IEC104_Connection_t connection;
static IEC104_AsduQueue_t queue;
static uint8_t high_bytes[1024], normal_bytes[4096];
static IEC104_SendBufferControlFrame_t controls[4];
static IEC104_IFrameSendRecordStorage_t records[12];
static int Init_Service_Queue(void)
{
/* 只在服务启动时初始化一次;无连接期间也可由TcpServer缓存NORMAL。 */
return IEC104_AsduQueue_Init(&queue, high_bytes, sizeof high_bytes,
normal_bytes, sizeof normal_bytes, 0U) == IEC104_ASDU_QUEUE_OK;
}
static int Open_New_Connection(uint64_t generation, uint64_t now_ms)
{
IEC104_ConnectionConfig_t config;
IEC104_Connection_Config_Default(&config);
/* 首次连接与重连都绑定已有服务队列;旧连接先Close/Detach,不能重置离线原文。 */
if (IEC104_AsduQueue_Rebind(&queue, generation) != IEC104_ASDU_QUEUE_OK) {
return 0;
}
if (IEC104_Connection_Init(&connection, &queue, controls, 4, records, 12,
&config, generation, now_ms) != IEC104_CONNECTION_OK) {
/* 恢复离线绑定;失败仍须停止接入并由服务层收尾,不能继续使用异常队列。 */
(void)IEC104_AsduQueue_Detach(&queue, generation);
return 0;
}
return 1;
}
```
服务启动先调用一次Init_Service_Queue,再在每次接入时调用Open_New_Connection。连接的`generation`非零且跨连接不复用;队列的代次0仅表示离线缓存,不能用于Connection或发送提交。配置默认k=12、w=8、t1=15000ms、t2=10000ms、t3=20000ms;本地发送及STOP总期限各15000ms,HIGH严格优先。k/w范围1..32767,所有时间期限必须非零,且`t2_ms < t1_ms`;相等或大于均由Init返回ERROR,Connection和队列保持原值。配置在Init时复制,之后通过新连接Init更换。
队列须已绑定同代次,没有保留或已覆盖的待ACK条目;可以已有待发NORMAL。成功初始化清空HIGH、保留NORMAL,链路为STOPPED。调用失败不覆盖Connection或队列。对象首次使用不必预先清零,但运行中不得重新Init;应先按第6节完成Close。
初始化按预检后提交组织:先在局部Connection中校验Receiver及配置;`SendManager_Init_Connection`在局部Link/Buffer/Manager上完成校验,最后清理HIGH,之后将Link和Buffer提交到最终地址并建立绑定。Connection随后复制已初始化的子对象及其余连接字段,没有可失败步骤。构造接口不读取旧目标对象内容,不提供运行会话迁移或任意重绑功能。
## 2. 已连接的完整流程
```text
收到STARTDT_ACT -> Feed受理、立即STARTED并排队STARTDT_CON -> ACTIVATED
Prepare_Next_Send -> 实际写TCP -> Handle_Send_Progress
CON完整写完 -> 仅释放发送任务,不再改变启停状态
TcpServer_Enqueue_Event -> 服务ASDU队列(无连接时也可调用)
TcpServer_Send_Response -> Connection_Submit_Response(校验当前连接代次)
-> Manager.Submit_Response直接准备I帧,或暂存HIGH
服务事件及HIGH积压 -> Prepare_Next_Send生成I帧
两条路径共用发送缓冲和记录
实际写TCP -> Handle_Send_Progress提交整帧完成
Feed收到有效I/S累计ACK -> 更新记录并释放对应NORMAL原文
TcpServer经Connection.Peek_Confirmed_Record读取 -> 复制RECORD事件 -> Remove_Confirmed_Record
业务调用TcpServer_NextEvent -> 处理事件副本
```
准备APDU、TCP整帧写完、对端ACK、转入事件队列、业务处理事件是不同阶段。队列HIGH原文在交接时出队,直接应答不进入ASDU队列;两者的HIGH I帧记录仍等待本地完成及ACK。NORMAL原文等待ACK或被队列覆盖;收到ACK后释放原文,记录保留到TcpServer成功复制为RECORD事件并调用Remove。尚未转移的确认记录仍占记录容量,满时新I帧等待,S/U仍可发送。业务稍后读取事件副本,不负责释放Manager记录。
## 3. 接收与业务读取
`Connection_Feed(connection, generation, data, length, now_ms, &consumed)`最多消费到一帧末尾。TCP输入有多帧时,调用方按consumed推进,继续喂入剩余部分;半帧留在Receiver等待下次输入。`consumed`必填,任何返回都以它为本次实际消费量;参数/旧代次错误消费0字节。
| 返回值 | 当前状态及调用方动作 |
|---|---|
| OK | 输入尚未成帧,或本次S/U已受理并释放;还有未消费尾部时继续Feed |
| FRAME_READY | I帧已通过链路检查、累计ACK已处理;TcpServer经Peek复制I_FRAME事件,成功入队后Release并继续Feed尾部 |
| WAIT | 当前控制请求尚未受理,通常是控制槽已满;保留尾部,驱动发送或Tick后重试 |
| ERROR | 接口参数、身份、阶段或时钟非法;修正调用,不跳过未消费字节 |
| FATAL | 冻结连接,停止正常收发并进入收尾 |
保留完整帧时再次Feed消费0字节,不能把未消费尾部丢弃或忙循环反复调用。一次调用在重试中释放旧帧时也不顺带消费新输入,调用方仍按consumed推进。
Receiver在APDU边界遇到非0x68字节时立即返回FRAME_ERROR,不跳过垃圾重新同步。完整I帧先通过Parse_APCI检查APCI和6字节ASDU公共头空间,再经Parse_ASDU_Header按已知固定长度Type ID、VSQ数量和SQ位检查最低载荷长度。任何格式或最低长度失败均由Connection登记FRAME故障并返回FATAL,随后TcpServer收尾;该帧不会推进V(A)/V(R),其携带的ACK不生效,也不会生成I_FRAME事件。未知/变长类型及零对象的业务合法性仍由应用检查。
I/S统一交给Manager检查序号及累计ACK。I帧受理后设置`rx_link_applied`,后续Tick、Prepare或重复Feed只报告当前帧,不能重复推进V(R)或释放发送原文。S帧没有待业务读取的ASDU,受理后自动释放。
子站受理STOP后立即处于STOPPED/STOP_DRAINING,禁止新I帧;后到START成功受理才重新开放。非法I帧不推进V(A)/V(R),不设置rx_link_applied,不交付业务,Connection冻结并由TcpServer断链。停止等待期间仍处理合法S确认。
`Peek_Received_Frame`向TcpServer返回`IEC104_ConnectionReceivedFrame_t`,包含完整APDU、APCI及ASDU借用地址。TcpServer把完整APDU复制到I_FRAME事件后,用本次`receive_serial`和连接代次调用`Release_Received_Frame`,然后内部视图失效。Release只归还接收空间,不再次处理ACK。相同代次的旧Release也会被编号检查拒绝,不能误释放下一帧。业务通过NextEvent取得独立副本,再用Codec或自己的解析器读取ASDU;链路受理不代表ASDU格式或业务执行已经成功。
普通通知满时仍可处理排在前面的S/U;I帧无处交付时保留原帧并暂停续收。启停使用独立配额:TcpServer通过Set_Transfer_Observer安装内部容量查询和通知复制函数,Connection在改变状态的ACT受理前查容量,满时WAIT保留原帧;状态改变后立即复制通知,再继续检查期限。该适配不调用业务、不重入修改接口,仅只读取得状态快照。重复ACT不产生状态变化,无须新槽。已有发送及协议期限继续推进;NextEvent释放容量后安排Process重试。
U帧路由:
| U帧 | Connection动作 |
|---|---|
| STARTDT_ACT | 先预留控制槽,成功后立即STARTED/IDLE、排CON并取消旧STOP总计时;WAIT保留原请求和状态;实际变化立即通知 |
| STOPDT_ACT | 受理即STOPPED/STOP_DRAINING,清空HIGH并关闭新I帧;停止确认满足后排CON;不等待首个START_CON完成 |
| TESTFR_ACT | 排队TESTFR_CON;控制槽满时保留当前请求 |
| TESTFR_CON | 经Manager.Handle_Testfr_Con提交;OK时结束本端探测t1,IGNORED时保持计时并释放接收帧;不占用控制槽 |
| STARTDT_CON / STOPDT_CON | 格式合法时消费并忽略;不应答、不改变启停或确认期限、不刷新t3,也不产生业务通知 |
反向启停CON在查询启停通知容量及准备控制应答之前释放,不占通知或控制发送槽,不因自身等待资源而阻挡后续输入。该例外只适用于通过Codec完整格式校验的两种CON,其他格式错误及I/S序号、ACK错误仍按原规则冻结连接。忽略后照常执行期限检查和已有控制义务,已到期的真实故障仍可结束连接。
子站START受理后立即开放I/S处理,新的本端报文仍须排在已有CON之后。任何阶段后到START成功受理都立即激活,不再暂存;先前已经排队的STOP_CON仍按FIFO写出,完成不能再次停用链路。
首个START_CON尚未完成时收到STOP,立即停用并推进确认;原CON及部分写入位置保留,不以STOP_CON替换它。Advance_Control每轮最多推进两项,使已覆盖接收数据的S与STOP_CON可依次排队,不必等S完成才生成CON。
START成功受理取消旧STOP计时;新的STOP以原received_at_ms开始总期限。STOP_CON入队时保存其frame_id,只有匹配的最后一帧完成才能结束该停止期限;旧CON完成不影响后续请求。重复STOP不延长有效期限,WAIT尚未受理也不改变状态。取消停止不恢复已丢弃HIGH,不重置I帧t1、S快照和本地发送期限。
## 4. 实际发送
`Submit_Response(connection, generation, asdu, length, now_ms, &head_info)`供TcpServer提交当前请求的应答。它拒绝旧代次、无效原文、已关闭会话和未结束的接收批次;FAILED返回FATAL。先只读查询协议等待时间:期限已到时调用Manager.Enqueue(HIGH),通过启停准入后仅复制到HIGH,交回Process先处理socket中的ACK,再检查是否超时,避免应答入口抢先判定确认丢失。
未到期路径先Drive,再由Manager判断实际准入;期限已到路径直接Enqueue(HIGH),也使用同一准入函数。两条路径均只在STARTED/IDLE接手,其他状态在容量检查前ERROR且不入HIGH;拒绝不冻结或断开连接。已有控制帧时应答暂存HIGH,不越过FIFO发送。
返回SEND_READY时head_info为新应答帧,TcpServer只尝试一次非阻塞写入;OK表示已缓存HIGH;WAIT表示尚未接手;ERROR表示输入/状态/容量不符;FATAL进入收尾。成功后原文已复制,返回后不再借用业务输入。旧HIGH、当前帧、窗口、记录容量及公平配额都由Manager检查,不在Connection重做调度。
STOP受理清空尚未组帧的HIGH,即使它们此前已返回OK;没有逐条作废事件。当前I帧和记录继续完成及等待ACK,NORMAL保持原生命周期。从停用状态重新START仍清理HIGH并重置配额,已启动时重复START保留HIGH。
`Prepare_Next_Send`先重试保留接收帧、检查本地发送及STOP期限并推进控制义务,再向Manager取得待发送数据。**只有SEND_READY时head_info有效**;EMPTY表示没有当前连接可发送的项(可能仍有EOF后留给下一连接的未编号NORMAL),WAIT表示门控、窗口、容量阻塞或旧确认t1已到。TcpServer在EOF时通过AsduQueue停止选取全部未编号NORMAL,已有当前帧及HIGH/控制发送仍走本接口,不在Connection另设EOF状态。接收WAIT不阻止已有发送帧继续写出。发送阶段不直接报告I帧ACK或TESTFR_CON超时故障,调用方须按Get_Wait_Time及时接收并Tick,不能只调用发送接口。
TcpServer在WAIT之后转移已确认记录,若实际释放了记录槽,须重新调用Prepare_Next_Send,不能沿用释放前的WAIT判断EOF收尾。若仍WAIT,内部只读接口`Get_Record_Wait`区分本地可恢复的记录容量等待:记录槽已满、最早记录已完整写出且已ACK,同时Link的`Can_Prepare_I_Frame`允许新I帧,且上次准备时刻没有到期的确认t1,才报告`record_wait=1`。Link查询与实际构帧共用Plan_I_Frame,不在连接层另写启停、k窗口或序号边界规则。查询不推进协议、不移除记录、不分配N(S),仅OK写输出;只用于紧随WAIT的EOF收尾判断,不能替代实际发送准备或期限调度。
准备新I帧前,Connection使用`Collect_Deadlines`取得Manager报告的当前帧存在状态和确认期限。已有I/S/U继续按原顺序发送,包括已编号但尚未写出字节的当前I帧,不能撤回或交错。没有已有帧时,`Get_Confirmation_Timeout`从同一视图选择I帧ACK或TESTFR确认超时:任一等待达到t1就返回WAIT,不调用Manager选择HIGH/NORMAL,不消耗N(S)、记录槽或待发条目。此限制在接收批次内、预算续收期间及常规发送阶段都有效。有效确认解除全部已到期等待后自动恢复;部分ACK若留下另一项已到期等待,仍保持限制。不新增持久状态、恢复计时器或固定宽限时间。
TcpServer把`head_info.pending_data`至`remaining_bytes`交给Net写操作,再把本次成功写入的增量及原`frame_id`传给`Handle_Send_Progress`。同一连接最多一个未回报的写操作。部分发送必须继续当前APDU,不能交错发送另一帧;若后续替换为异步传输,也必须在结束I/O借用后才能调用Connection_Close。
| 写入结果 | 提交方式 |
|---|---|
| 成功写入部分字节 | 回报本次增量,返回OK,保留剩余字节和原期限 |
| EAGAIN / 暂不可写 | 可回报0或等待下次可写事件;继续调用Tick,不延长期限 |
| 写完剩余字节 | 回报实际增量;FRAME_WRITTEN表示对应Link完成、记录更新及缓冲释放均完成 |
| 最终发送错误 | Handle_Send_Failure,匹配当前帧时提交失败并冻结连接 |
完成回报会自动重试受阻的接收请求和控制过程。因此极少数情况下,写入已记账后,后续接收/控制处理失败使本次返回FATAL;此时不能重报写入增量,应冻结后读取记录判断事实。completion仅在FRAME_WRITTEN时写入。
`Handle_Send_Progress`使用实际写入后的单调时刻登记进度、整帧完成及TESTFR起点,不借用send前的旧时刻。它与Submit_Response、Prepare_Next_Send的内部Drive只检查本地发送和STOP期限,继续推进保留帧及控制义务;I帧ACK和TESTFR_CON的t1交给接收阶段判定。这样,即使提交应答时尚未到期、send返回后已经到期,也不会在处理socket中待解析的确认前直接冻结连接。TcpServer安排立即续跑,按原始期限处理确认或报告超时,不增加恢复期限或宽限时间。
本地已知整帧完成必须先回报,再处理覆盖该I帧的ACK,或处理依赖本次发送完成的U帧。异步事件循环须保证这一提交顺序;不能用收到ACK来推定本地发送成功,也不能在Connection之外再次调用底层Complete。
## 5. 时间与驱动
所有`now_ms`使用同一单调毫秒时钟,允许0,按本连接调用顺序不倒退。时钟倒退返回ERROR;旧代次事件在改变时刻前被拒绝。系统定时器只负责唤醒,`Tick`按连接保存的当前过程起点重新计算,不接收旧回调保存的时间差。Tick不准备新的业务I帧。
| 期限/门限 | 起点与结束条件 |
|---|---|
| 本地发送期限 | 当前帧原queued_at_ms;0字节、部分发送和重试不续期;到期提交发送失败 |
| I帧t1 | 最早已整帧完成但未ACK记录的written_at_ms;ACK推进后采用下一记录的原始起点 |
| TESTFR t1 | 本次TESTFR_ACT整帧完成时启动;匹配CON受理后结束;其他收发活动不续期 |
| w/t2确认 | 已受理I帧数量达到w,或连续未确认区间达到t2时安排S;S完成或I捎带完成后更新确认位置 |
| t3空闲 | 除反向STARTDT_CON/STOPDT_CON外,完整合法APDU按成帧时刻更新活动;本地整帧完成也更新;半包、0字节、Peek和WAIT重试不刷新 |
| STOP总期限 | 本轮STOP受理时启用,以原成帧时刻received_at_ms为起点,包含受理前WAIT,至本轮最后一帧STOP_CON完整写出;成功受理START也结束旧期限;重复STOP及旧CON完成均不续期或误清新期限 |
未受理的STOP不会提前设置`stop_timer_active`;受理时才用保留的原成帧时刻建立期限。因此受理前的WAIT已经用尽总期限时,受理后的同轮超时检查即可报告STOP_TIMEOUT。
`Connection_Get_Wait_Time(connection, generation, now_ms, max_wait_ms, &wait_ms)`供TcpServer_Prepare查询协议等待上限,Poll也复用相同查询。它只读汇总上述期限,把等待时长限制在0..max_wait_ms;已到期取0,没有更早期限则保留调用方上限。仅OK修改输出;无效参数/代次、倒退时钟、内部状态异常或尚未结束的接收批次返回ERROR,FAILED返回FATAL。查询不修改last_now_ms或任何计时起点,不准备报文、不登记故障,也不替代Tick;业务不直接调用此内部接口。
`Collect_Deadlines`是本层唯一的期限收集入口,超时检查、发送准入、EOF记录等待判定、控制推进及Get_Wait_Time共用它。Manager.Get_Deadlines提供队首身份、本地发送剩余时间、最早已完成未ACK记录的t1剩余时间以及控制容量;底层各帧的原始时间戳继续由原模块保存。Connection汇入自身TESTFR、STOP、t2、t3的剩余时间,只在栈上形成临时视图,不保存同步副本。活动标志与剩余时间分开,起点0合法,到期剩余时间为0。
控制推进后会重新收集期限,避免继续使用排入S/STOP_CON前的容量或准入状态。t2/t3同时经过Link的Can_Prepare查询和Manager报告的control_has_room判断;已有S在途、探测正在进行或控制槽满时,不因不可推进的旧t2/t3不断唤醒。门控或容量恢复后按原起点重试。时间计算先校验时钟再作64位差值,不用起点加期限。
Check_Timeouts保持本地发送、I帧ACK、TESTFR、STOP的故障优先顺序。本地发送到期时使用同一次查询取得的frame_id向Manager报告失败。接收批次仅决定何时执行确认超时故障;期限视图仍报告实际到期,因此发送准入和等待时间不会把暂缓误当作续期。
t2采用固定存储:保存当前连续未确认接收区间的最早到达时刻,只有Link快照报告`unconfirmed_rx_count == 0`时清除,含义是全部已收I帧都已实际发出确认。S或I仅确认区间前缀时,剩余部分沿用原起点,可能提前发S,不会因部分确认把等待时间向后推。START_CON是否已完成不再限制S准备,实际S发送仍排在已有CON之后。t2到期时Connection请求立即确认,是否允许构帧仍由Link判断。
Feed在接收批次之外先提交已到达的接收事实,再检查全部期限;发送准备和写入回报仅因本地发送及STOP期限直接报告超时,对端确认t1故障由接收阶段判定。发送准备仍查询确认是否到期,用于暂停新I帧准入,不提前冻结连接。同一时刻若先调用Tick触发FATAL,后续事件不能恢复该连接。TcpServer在Process的接收阶段结束时驱动Tick并按实际事件顺序串行提交,Poll复用该推进核心。发送后若查询到期限已到,则登记process_pending,下一轮立即接收;Prepare也始终把到期的协议等待时间报告为0。业务根据TcpServer_Prepare返回的socket/定时/本地条件调度Process,或直接使用Poll。Tick的OK不表示发送缓冲为空,TcpServer后续仍调用内部Prepare_Next_Send检查待发送内容;公开TcpServer_Prepare只查询调度条件,不推进协议。
TcpServer在Process的接收阶段调用`Begin_Receive_Batch(connection, generation)`,然后在预算内Feed、复制I_FRAME事件并释放内部帧,按需完成非阻塞发送。应用将同一轮就绪和定时通知合批交付;批次内只暂缓I帧ACK和TESTFR_CON的t1判定,避免前面的其他报文先触发超时,使后面的有效确认来不及处理。本地发送期限、STOP总期限及格式/协议错误仍立即处理。没有调用Begin时,Feed仍执行即时的完整超时检查。
同一轮接收必须以`Tick`或`Yield_Receive_Batch`结束;重复Begin返回ERROR,Close清除全部批次状态。2026-10-20起,仅因调度预算让出且还有已到达输入时,TcpServer先固定用户态尾部和内核待收字节数,再调用Yield结束当前轮次。`receive_batch_pending`只暂缓对端确认期限;Get_Wait_Time返回0,调用方必须立即续收,不能跨阻塞等待,也不得把后来到达的输入加入旧快照。本地发送和STOP总期限、控制推进及协议错误不受该标志影响。
快照耗尽、接收无进展或真正容量背压时必须调用Tick,清除续收标志并检查全部期限,不能借此等待业务消费或推迟无ACK连接的失败。接口不修改任何期限起点,也不判断内核实际收到字节的时刻。未调用Begin/Yield的Feed保持原行为,发送接口不替代接收及Tick的确认期限检查。异步接入仍须先提交已经完成的本地写入,再喂入依赖该完成的ACK。
## 6. 故障、关闭与恢复
Connection状态为`ACTIVE -> FAILED -> CLOSED`,正常收尾也可从ACTIVE直接Close。ACTIVE只表示可以处理当前连接事件,不表示已经STARTED。
`Connection_Get_Status`组合各模块的数值快照,允许在FAILED/CLOSED时查询;参数、代次或模块状态非法返回ERROR且不改输出。CLOSED不再读取已解除绑定的Receiver/Manager,接收与记录数量为0,链路字段仅为历史值。记录回收接口只要求Manager存储合法,不因Link运行状态异常而阻断读取。
格式/链路错误、发送失败和期限到达会记录首次故障并进入FAILED,后续普通收发入口返回FATAL。此时不自动清空发送记录或NORMAL原文:`Get_Record_By_Offset`可读取全部记录,`Peek_Confirmed_Record`及Remove仍可消费已确认回执,已受理I帧也仍可Peek/Release。`Get_Status`返回阶段、首次原因、启停状态和保留帧概况。这些接口均由TcpServer在内部使用,向业务交付公开的记录事件和状态快照。传输异常由TcpServer调用`Handle_Transport_Closed`冻结;有序EOF采用正常收尾路径,复制剩余记录后调用Connection_Close,不新增故障。未ACK业务仍须根据事件中的记录字段处理。
故障分类按失败来源保留:Receiver的起始字节、长度/APCI或ASDU最低长度错误为FRAME;Manager明确返回PROTOCOL_ERROR的I/S接收拒绝为PROTOCOL。格式合法的反向STARTDT_CON/STOPDT_CON消费并忽略,不登记故障。本地会话或记录预检失败、有效ACK受理后的原文释放失败、合法U帧的控制交接失败均为INTERNAL。START/STOP/TESTFR的合法重复或无匹配确认继续按既有规则受理或忽略。实际发送失败登记TRANSPORT,各类超时保留独立分类;后续清理异常不覆盖已经登记的首次故障。
TcpServer内部的收尾与恢复顺序:
1. 停止实际收发,关闭/取消并等待旧I/O结束,确保没有传输仍借用缓冲。
2. 在Connection_Close之前复制剩余I帧、发送记录及断开通知到事件队列;记录区分本地完成和peer_acknowledged,未ACK不能证明对端未收到。
3. 调用`Connection_Close(旧代次)`,解除内部借用。重复Close同代次返回OK,外部ASDU队列保留。
4. 调用`AsduQueue_Detach(旧代次)`清空HIGH、恢复现存NORMAL待发状态并解除绑定;代次0期间继续接受NORMAL,已覆盖原文无法恢复。转换失败须停止服务,不得重新Init覆盖缓存。
5. 新连接建立时调用`AsduQueue_Rebind(新代次)`,再调用`Connection_Init(新代次)`。新连接重新进行STARTDT,不沿用旧序号;首次连接同样绑定服务启动时建立的队列。
同一TCP上的STOP/START不调用Rebind。旧代次的Feed、发送完成、Tick、Release和Close均不能影响新连接。代次检查解决身份隔离,不能代替结束旧I/O或同步线程;调用方仍须保证被旧I/O引用的内存不会提前复用。
## 7. 构建与边界
Connection部分的生产构建依赖包含:`IEC104_Connection.c`、`IEC104_Receiver.c`、`IEC104_SendManager.c`、`IEC104_SendBuffer.c`、`IEC104_AsduQueue.c`、`IEC104_Codec.c`、`IEC60870_Codec.c`、`IEC104_Link.c`和`IEC104_Link_Slave_StartStop.c`。不依赖主站启停实现、旧全局接收器或日志模块;完整TCP构建还需TcpServer及所选Net后端。
本层负责单条子站连接的内部模块协作。`IEC104_TcpServer.c/h`由Slave持有,负责监听调度、连接生命周期、收发预算和业务事件队列;网络操作及单调计时通过`IEC104_Net.h`执行,当前提供`IEC104_Net_Windows.c`后端。应用使用Slave_Prepare/Process接入自己的事件循环,或使用Slave_Poll组合等待和推进,通过Slave_NextEvent取得ASDU及应答接口;TcpServer在复制事件成功后统一释放协议帧和记录。初始化及收尾见[TCP服务设计](IEC104_TcpServer_设计架构.md)。Connection不依赖Net或操作系统接口;借出的socket仅供应用注册,真实I/O仍在TCP内部,业务不直接驱动Connection。ASDU信息对象处理、业务去重及冗余组切换仍由业务层负责。旧`IEC104.c/h`保留作为历史接口,不参与当前TcpServer接入路径,同一会话不能重复喂给旧全局接收器。
@@ -0,0 +1,48 @@
# IEC104 计数量召唤
`IEC104_CounterInterrogation.c/h`实现 Type 101(C_CI_NA_1)的规则与创建入口,状态机、事件、广播、期限及发送背压由`IEC104_Interrogation`共用引擎维护。总召和计数量召唤各有实例,忙状态独立;共享连接、HIGH队列和发送窗口,任一业务关闭连接都会影响另一业务。
## 1. 请求与数据
请求为104的2/2/3地址布局、单对象、SQ=0、IOA=0、COT=6。QCC低6位RQT为1..4分组或5总请求,高2位FRZ为0读取、1冻结、2冻结并复位、3仅复位。COT=8不支持,返回未知传送原因45。未知CA和非零IOA分别返回46、47;限定词、P/N、忙状态及可可靠回显的畸形单对象请求回否定ACT_CON。无法对应单对象的请求丢弃并统计。输入原文不修改。
读取数据限Type15(SQ=0/1)和Type37(SQ=0)。数据COT使用`request.data_cause`:RQT=5为37,分组1..4为38..41。CA必须为事件中的实际站地址,OA、TEST保持原请求,P/N=0。249字节上限分别容纳30、48、16个对象,并受配置对象数限制。IOA分组、冻结值、BCR序号和品质由应用负责。
## 2. 两种完成方式
| 请求 | 应用处理 | 线路顺序 |
|---|---|---|
| FRZ=0 | BEGIN接受;NEED_DATA提交冻结值;Complete_Data结束 | ACT_CON → 数据批次 → ACT_TERM |
| FRZ=1..3 | BEGIN中通过Execute_Begin执行动作 | 动作完成 → ACT_CON;随后FINISHED |
冻结及复位不产生NEED_DATA,也不发送ACT_TERM。流程依据IEC60870-5-101:2003 §7.4.8模式C的冻结/冻结并复位流程;读取另按模式B进行。目标主站若要求额外终止帧,应作为单独兼容需求评估。Type16及撤销不在104计数量模块的支持范围内。
因此拿动作的BEGIN去调用Submit_Batch会因事件类型不匹配返回STALE,不会发送数据;ERROR仍用于有效NEED_DATA的非法批次。这与最初“无数据也产生NEED_DATA”的草案不同。
`gi_submitted`在计数量实例中统计成功提交完成响应的事务:读取的ACT_TERM或动作的ACT_CON;不是主站ACK计数。总召实例原含义不变。多站读取仍为全部确认、逐站数据、全部终止;多站动作逐站执行和确认,每站独立FINISHED,不保证同时冻结或失败回滚。
## 3. 动作执行与失败边界
应用用`IEC104_CounterInterrogation_Execute_Begin(self, event, action, context)`处理FRZ=1..3。回调签名为`bool action(void *context, const IEC104_InterrogationRequest_t *request)`。接口先检查实例、事件、会话及期限;WAIT/STALE不会调用动作,事件消费后再次调用不会重复执行。TEST动作请求被否定,不调用动作。
回调必须同步、短时完成,不得重入协议接口或推进网络;返回false表示未改变计数器,库生成否定确认。应用须先完成分组、资源等检查,再整体提交修改,不能部分清零后返回false。成功后确认等待不再因业务期限变成否定确认;传输层的发送期限、STOP和断线仍有效。已执行动作不会因连接断开或本地Cancel而撤销。
这个接口适合内存计数器或已准备完成的本地原子提交,不提供异步外部电表事务。异步冻结需要另行管理设备命令、迟到完成和不可撤销结果,不能简单“先发设备命令,稍后Reply_Begin”。主站重新发送一个新请求仍可能再次冻结;本模块不提供跨请求去重或断电持久化事务。
读取的BEGIN继续使用`Reply_Begin`;拒绝未配置分组也使用该接口。不得在可重试的BEGIN处理分支直接清零,再调用Reply_Begin。
## 4. 数据一致性与接入
一次读取必须固定同一冻结版本,包含值、序号、品质及可选时标。应用可复制快照、持有不可变版本,或阻止读取期间改写相关冻结区。WAIT后重组包仍取同一版本。多个Slave共享数据源时同样需要协调,不能只避让本地定时器。Type37时标由应用给出,本模块不读取墙钟。
创建顺序为Slave、站点、总召实例、计数量实例、Slave_Init。计数量调用`IEC104_CounterInterrogation_Create`,配置沿用Config_Default;使用多站广播时,两个实例必须各有独占状态数组。所有连接/启停通知交给两个实例;I帧依次交总召、计数量,均IGNORED才兜底。
每轮先推进Slave并读空通知,再推进两种业务并读取事件。合并两个实例的need_process、events_pending及最小剩余期限;发送WAIT后网络有进展时必须重试。示例轮换两实例的先后次序,避免固定优先级长期占据小窗口。退出时两实例分别Cancel、读完FINISHED、Destroy,然后释放站点与Slave。
## 5. 构建与边界
引擎及种类模块集中在`104/business/`,头文件搜索路径为`104/`、`104/business/`和`common/`。引擎依赖只读规则,不引用具体种类符号。总召工程加入`104/business/IEC104_Interrogation.c`和`104/business/IEC104_GeneralInterrogation.c`;计数量再加入`104/business/IEC104_CounterInterrogation.c`。只用计数量时不需要总召规则文件。默认实例池为TCP服务容量的两倍;只用一种业务可显式定义`IEC104_INTERROGATION_CAPACITY=1`。
规则表及Create_With_Rules属于内部接口;业务使用两个公开创建入口及共用驱动接口。现有总召Create签名保留,所有调用方重新编译。该引擎仍绑定104的Slave、传输及地址布局,尚不是101串口业务适配层。
可运行验证入口及当前记录见[测试说明](../test/IEC104测试文档.md),模拟计数器操作见[示例说明](../examples/IEC104子站示例.md)。
@@ -0,0 +1,458 @@
# IEC104 总召业务模块
`104/business/IEC104_Interrogation.c/h`在`IEC104_Slave`之上实现共用召唤引擎,Type100规则和原Create入口位于同目录的`IEC104_GeneralInterrogation.c/h`。通信服务负责连接、启停、收发和会话身份;引擎负责请求准入、业务阶段、数据批次检查、应答调度及终结通知。本文描述总召行为,应用通过`BEGIN / NEED_DATA / FINISHED`事件提供点表和数据;计数量的差异见第12节。
当前采用**每个实例至多一个未终结总召,忙时否定所有新的总召激活**。空闲时支持全站和全部16个分组,忙时不区分相同或不同QOI。任务池仍保留4个槽,用于一个总召及待发送短应答;没有后续总召等待队列,也没有独立排队超时。
本文描述库本身的设计,不依赖应用模拟点表或可执行程序。完整接入位置见[完整调用流程](IEC104_完整调用流程.md#flow-interrogation),函数声明及返回值汇总见[API接口汇总](IEC104_API接口汇总.md#file-21)。
## 1. 职责与调用关系
所有接口由所属事件循环串行调用。模块不创建socket、线程或系统定时器,不动态分配内存,也不会在`Process`中跳转执行应用代码。异步采样可以由应用自己的线程或设备驱动完成,但结果须回到所属事件循环后提交。
```text
应用事件循环
├─ IEC104_Slave:Poll / Prepare / Process / NextEvent
│ └─ TcpServer → Connection → Link / Receiver / SendManager → Net
└─ IEC104_Interrogation
Handle_Event ← 应用逐条转交Slave通知
Process → 沿保存的原连接身份提交确认、终止及短应答
Submit_Batch → 沿保存的原连接身份尝试直接提交数据,不入队等待
NextEvent → BEGIN / NEED_DATA / FINISHED
Reply_Begin / Submit_Batch / Complete_Data / Fail_Request ← 应用显式应答
```
| 模块 | 自己维护的规则 | 与总召的关系 |
|---|---|---|
| Slave | 服务生命周期、事件来源、连接代次、启停及提交结果 | 总召通过公共接口取得原身份、校验会话和提交应答 |
| TcpServer及内部通信模块 | TCP字节流、APCI序号、k/w、t0..t3、HIGH容量和发送完成 | 总召不直接操作socket、链路状态或发送窗口 |
| Interrogation | 一个总召的准入与阶段、短应答、业务事件及业务期限 | 不持有应用点表,不主动读取设备 |
| 应用 | IOA、分组成员、数据源、遍历状态、资源清理 | 根据业务事件决定接受、逐批取值交付或终结数据 |
`Slave_Get_Event_Identity`校验事件来源;`Slave_Check_Request`判断是否有未读通知、原会话是否仍可提交。总召不自行拼凑TCP状态快照判断连接是否有效。应用应使用Slave和总召公开接口,不访问不透明对象内部字段。
## 2. 对象、身份与固定存储
`IEC104_Interrogation_t`是不透明的静态池句柄,通过`Create`取得,`Destroy`归还。一个Slave只能绑定一个总召实例;须在开始消费Slave通知前创建。配置按值复制,调用方配置返回后可复用;Slave须存活到总召实例销毁之后。
设置config.use_station_registry=true后,总召按请求CA选取本Slave的已注册站点,65535逐站遍历;config.station须NULL,common_address忽略。当前任务持有节点引用,删除使该站立即退出新请求路由,已有任务继续使用原节点。BEGIN携带station_id,应用跨循环保留context时应立即按编号查节点并Acquire,FINISHED清理后Release,防止同CA替换串站。默认false兼容原单CA调用,可选config.station仍固定绑定一个站点并持有到Destroy。注册、引用、延迟回收及快照契约见[共用站点表](IEC104_API接口汇总.md#station-registry)。
| 存储 | 默认容量 | 保存内容与约束 |
|---|---|---|
| 召唤实例池 | `2 * IEC104_TCP_SERVER_CAPACITY` | 同一Slave的总召及计数量各一个;各实例会话、配置、任务及事件独立 |
| 每实例协议任务池 | 4槽 | 至多一个未终结总召;其他槽仅保存单帧应答,也允许4槽全是应答 |
| 每实例业务事件FIFO | 4槽,最小2槽 | 单目标预留FINISHED位置;多目标终结记录保存在调用方状态区,按需交付 |
| 多站广播状态区 | 调用方提供,每目标站一项 | 保存站引用、身份、计时及进度,不保存点值或数据ASDU |
| 总召数据存储 | 不设置私有批次缓冲 | WAIT不接手数据;OK时发送层已复制到当前APDU |
编译实现文件时可覆盖`IEC104_INTERROGATION_CAPACITY`、`IEC104_INTERROGATION_TASK_CAPACITY`、`IEC104_INTERROGATION_EVENT_CAPACITY`。增加任务槽只增加短应答积压容量,不允许第二个总召。任务池设为1仍可工作,但当前总召占槽且短应答不能立即提交时,新请求只能丢弃并计数。
协议任务只保存`InterrogationResponse`:由`Slave_Get_Event_Identity`取得的原服务/实例/连接代次,加10字节Type100 ASDU。合法请求复制原文并处理应答CA;可可靠应答的畸形单对象请求只保存规范为10字节、VSQ=1的否定应答副本,不执行激活或撤销。错误IOA、QOI、OA、TEST等字段仍完整保留。每次发送临时建立ASDU视图,调用`Slave_Send_Response`或`Slave_Try_Send_Response`;任务不保存完整SlaveEvent或自引用接口,源事件出队后可立即复用。固定任务槽及`order[]`调度规则保持不变,非队首短应答完成后仍立即释放对应槽。
2026-10-27按默认4任务槽、4业务事件槽测量,Windows x64 GCC的`sizeof`如下;这是结构存储尺寸,不是STM32的链接占用,Keil结果须另行实测。
| 项目 | 精简前 | 精简后 |
|---|---:|---:|
| 每任务请求存储 | 400字节(完整SlaveEvent) | 40字节(原身份及10字节ASDU,含对齐) |
| 每任务 | 480字节 | 120字节 |
| 每实例4个任务合计 | 1920字节 | 480字节 |
| 每个总召实例 | 2400字节 | 960字节 |
每实例减少1440字节(60%)。任务槽数、业务事件FIFO、TCP通知环、HIGH/NORMAL容量、k窗口及事件覆盖策略均保持原样,节省来自请求表示,不减少背压容量。立即应答也不再在栈上复制完整SlaveEvent;GCC `-O2 -fstack-usage`中该函数从464降至96字节,但这不是ARMCC或完整调用链的实测栈水位。
上表保留请求存储精简阶段的比较。当前Windows x64默认配置实测:总召1240字节、每任务168字节、紧凑请求40字节、业务事件80字节、配置40字节。相对逐站完整执行版本的1192字节,管理对象增加48字节;多目标广播另需调用方提供每站48字节的状态区,13站为624字节。单目标不要求状态区。广播共用一个任务及一套应用取数游标,HIGH/NORMAL和任务/事件容量不变。数值不是目标板RAM实测;站点及Slave开销见[共用站点表](IEC104_API接口汇总.md#station-registry)。
业务事件是纯值结构,可以复制和跨循环保存。`instance_id`区分静态池槽的历次使用,`request_id`在实例内递增,`batch_index`区分本次取数交互;编号耗尽时拒绝新建,不回绕复用。TCP的`generation`属于另一层身份,防止旧请求向新连接发送。
## 3. 请求校验与单总召准入
### 3.1 接收范围及校验顺序
总召使用`C_IC_NA_1`,Type=100、SQ=0、一个对象、IOA=0。支持激活COT=6和撤销COT=8,请求P/N须为0,CA可为已注册站地址或广播65535(0xFFFF);兼容模式只接受其配置站。实际站CA限于1..65534。QOI=20为全站,21..36为第1..16组;分组内容由应用维护。
先校验事件身份,再保护长度并读取类型字节。非100返回`IGNORED`,总召不代替其他业务发送UNKNOWN_TYPE;公共ASDU视图不可用时也返回`IGNORED`,由公共入口处理。正常网络路径的公共头及最低载荷已由Receiver检查,真实截短/帧错误仍沿连接层原策略处理。Type100先判断是否存在唯一完整对象,可靠时按`COT → CA → IOA → 布局/QOI/P/N → 事务状态`处理,避免忙状态掩盖地址或传送原因错误。
| 条件 | 结果 |
|---|---|
| Type不是100,或公共视图不可用 | `IGNORED`;公共入口应先过滤不可用视图,再分派其他业务 |
| Type100对象数为0或大于1,无法确定唯一完整IOA/QOI | 计入rejected并返回`HANDLED`,丢弃新请求,不生成应答 |
| Type100单对象完整,但SQ=1或有多余尾字节 | 仅回否定COT=7/9,COT/CA/IOA错误仍优先45/46/47;不执行激活或撤销 |
| COT不是6或8 | 回COT=45,P/N=1 |
| CA既不是本站地址也不是65535 | 回COT=46,P/N=1,保留请求中的错误CA |
| IOA不是0 | 回COT=47,P/N=1 |
| 激活QOI超范围或P/N非法 | 回否定ACT_CON,COT=7、P/N=1 |
| 撤销QOI超范围、P/N非法或没有匹配对象 | 回否定DEACT_CON,COT=9、P/N=1 |
| 合法激活,已有未终结总召 | 回否定ACT_CON,不替换原请求,不建立第二个总召 |
| 合法激活,空闲且有任务槽 | 保存请求、分配request_id,进入START |
| 合法激活,空闲但任务槽被短应答占满 | 尝试否定确认;不能提交或保存时丢弃新请求并计数 |
确认和错误应答调整COT及P/N;请求CA为65535时,应答CA改为本站地址,其他CA原样保留。OA、TEST、IOA和QOI保持原文,包括错误请求中的原值。畸形单对象的否定副本规范为10字节、VSQ=1,原输入事件不修改;该容错应答是本项目策略,不是将畸形命令视作有效命令。忙拒绝同样使用否定ACT_CON,没有另设“忙”的专用传送原因。
上述请求错误不会关闭TCP、取消已有总召、重置原期限或生成其FINISHED(INVALID_REQUEST),其他CA任务继续。否定应答复用原HIGH/FIFO背压与满槽丢弃策略,不扩大任务或缓冲容量。无唯一对象的丢弃只计入rejected;dropped_requests仍专指无法安全提交/保存的容量丢弃。内部服务故障、会话失效及发送失败继续走原失败路径。
### 3.2 忙状态的起止点
忙状态从合法激活被保存为START开始,早于生成BEGIN和提交肯定ACT_CON。因此,同一批网络通知连续包含多个激活时,只有第一个可建立总召,后续请求立即进入否定应答路径。不能等到应用接受后才设置忙,否则等待应用期间会接入多个总召。
忙状态覆盖BEGIN尚未生成、等待应用接受、确认等待发送、等待数据、有效批次未获发送机会及终止等待发送。相同QOI、不同QOI、不同OA或TEST的新激活都不能覆盖原请求。
| 终结方式 | 何时释放总召资格 |
|---|---|
| 正常完成 | ACT_TERM被发送层接手,释放任务;不等待TCP写完或主站ACK |
| 应用拒绝BEGIN | 生成FINISHED(REJECTED),原任务转为否定REPLY |
| 肯定确认提交前超时或BEGIN数据源失败 | 终结原因保留为对应TIMEOUT或SOURCE_ERROR,原槽转为否定REPLY,保留TCP及其他任务 |
| 主站匹配撤销 | 结束原业务,原任务转为肯定撤销REPLY |
| 本地Cancel、STOP、断线或确认后失败 | 取消总召及本模块待发短应答,保留应有的FINISHED |
单站终结并转为REPLY后不再占用总召资格;多目标广播中某站拒绝、超时或定向撤销时,仍为其他站保留总召资格。广播网络流程结束后进入FINISHING,gi_pending为false,但仍占用状态区并忙拒绝新激活,直到全部FINISHED出队。此前被拒绝的请求不会自动启动,主站需要重新发起。
### 3.3 广播地址与本站地址
广播CA表示请求面向所有站。`IEC104_Types.h`以`IEC104_CA_BROADCAST`定义0xFFFF(65535),它不是实际站地址。开启共用站点表后,多目标广播采用c104的三阶段顺序:先按注册顺序完成各站BEGIN及ACT_CON,再逐站处理全部数据,最后逐站ACT_TERM及FINISHED。例如两站线上顺序为CON1、CON2、DATA1、DATA2、TERM1、TERM2;每站数据可有多批或为空。各站有独立request_id、进度和期限,但共享一个任务和一个取数位置。兼容单站及仅含一个目标的广播保持单目标流程。
接收合法多目标广播时,把目标节点引用保存到`config.broadcast_states`;`broadcast_capacity`须覆盖当时的注册站数。数组由调用方持有至Destroy,地址稳定,不得读写或与其他实例重叠;无需逐项初始化。容量不足在任何肯定确认前整轮拒绝,各站收到否定ACT_CON,并累计overloads,不截取部分站执行。单目标不需要此数组,注册表本身仍无固定站数上限。
目标集合不冻结IOA或点值。后来新增或同CA重建的节点下轮广播才参加;尚未选入BEGIN便被删除的站跳过。已选中站持原引用完成,尤其已确认但尚未取数的站不能因删除而静默消失;应用通过station_id取得原context。无法继续取数时显式Fail_Request。没有注册站时广播回复COT=46并保留65535。忙或错误广播仍使用共用站点快照遍历实际CA,无槽保存遍历时整体丢弃新请求并计数。
每次Process最多推进2倍任务容量步,默认8步;剩余可提交应答通过need_process继续调度。多目标每站状态区保留终结原因,NextEvent按FIFO空位逐个交付,即使13站也只需2槽事件FIFO。发送WAIT不推进当前发送项,仍须重试。确认前拒绝或超时只结束该站并继续确认其他站;确认后取数失败或超时关闭原连接,整轮取消,各已交付BEGIN都有FINISHED。CA选站、QOI选组,两者独立,广播支持20..36。
以仅注册CA=7为例(广播多站时逐站应用同一规则):
| 请求或交互 | CA的处理 |
|---|---|
| 激活或撤销CA=7 | 接受本站寻址,后续应答CA=7 |
| 激活或撤销CA=65535 | 映射到本站7,后续应答CA=7 |
| BEGIN / NEED_DATA / FINISHED | request.common_address均为7,应用无需判断原请求是否广播 |
| Submit_Batch | 必须提供CA=7;CA=65535或其他站地址返回ERROR且不消费当前交互 |
| 肯定/否定ACT_CON、ACT_TERM、肯定/否定DEACT_CON、广播请求的错误应答 | CA均为7,发送等待与确认前失败不改变这条规则 |
| 其他CA且前置校验已通过 | COT=46、P/N=1,保留原错误CA,避免伪装成本站请求 |
输入的`const SlaveEvent`始终保持原文。Request完整解析Type100后选择实际目标站,广播从首站开始;未知CA保留原值。错误原因仍按COT、CA、IOA、QOI/P/N优先检查。撤销匹配本轮活动站集合,不依赖注册表首站或当前取数位置。
内部Copy_Response_Request保存原身份及10字节ASDU,再用临时视图调用Asdu_Set_CA。广播每次切换站点才更新副本CA,同一站的WAIT重试只重建视图、设置COT/P/N并沿用原CA。副本不借用调用方事件,也不重新绑定当前连接。
任务仅保留info这一份业务元数据,数据COT、OA、TEST及CA均按当前任务info检查;CA必须等于当前目标站。错误短应答不生成BEGIN。数据批次不自动改写地址,非法批次返回ERROR供应用修正;同一IOA在不同CA下属于不同点。
`IEC60870_Asdu_Set_CA`是共用ASDU层的字段接口,按COT宽度定位CA并按CA宽度写入小端字节,支持构造容器和借用可写原文。1字节CA只允许0..255,2字节允许0..65535;参数或容器无效时返回ERROR且不修改原文。它不负责站地址的业务合法性,总召配置仍拒绝0和65535。
该规则仅属于Type100处理器。即使CA=65535,Type101等其他有效类型仍返回IGNORED,后续业务模块自行决定其寻址及应答规则。
## 4. 协议阶段与完成含义
```text
空闲收到合法激活
→ START:保存请求,已占用总召资格
→ 业务事件有空位,生成BEGIN,启动业务计时
├─ Reply_Begin(false) → FINISHED(REJECTED) → REPLY(否定ACT_CON)
└─ Reply_Begin(true) → CONFIRM
→ 提交肯定ACT_CON成功 → DATA
→ NEED_DATA → Submit_Batch → OK → 下一个NEED_DATA
→ WAIT → 保留事件和游标,重读当前值后重试
→ NEED_DATA → Complete_Data → TERMINATE
→ 提交ACT_TERM成功 → FINISHED(COMPLETED) → 释放任务
任何未终结阶段:新激活 → 否定REPLY;原总召阶段与进度不变
匹配撤销:停止当前业务 → FINISHED(已取出BEGIN时)→ REPLY(肯定DEACT_CON)
START/CONFIRM超时或BEGIN失败:终结当前业务 → REPLY(否定ACT_CON),保留连接
DATA/TERMINATE超时或NEED_DATA失败:关闭原连接,取消本模块任务,不发成功ACT_TERM
```
| 阶段 | 等待或动作 | 推进条件 |
|---|---|---|
| START | 等事件空位生成BEGIN,随后等应用决定 | 单目标需要BEGIN及终结预留;多目标终结位置由状态区保证 |
| CONFIRM | 提交肯定ACT_CON | 多目标继续下一站确认,全部确认完成后开始数据 |
| DATA | 生成NEED_DATA,应用读取当前值并尝试直接提交 | Submit_Batch返回OK才消费交互、增加批次和对象计数 |
| TERMINATE | 提交ACT_TERM | 发送层接手后终结;发送WAIT时仍然忙 |
| REPLY | 一条否定确认、错误应答或撤销确认 | 提交成功即释放,不产生新的取数交互 |
| FINISHING | 多目标网络流程结束,等待应用读完FINISHED | 最后一个终结事件出队后释放任务及节点引用,才接受新总召 |
| IDLE | 状态查询时表示没有协议任务 | 不表示业务FIFO一定为空,仍可能有FINISHED待取 |
`Reply_Begin / Complete_Data`成功表示应用决定已被模块接手,后续`Process`提交确认或终止。`Submit_Batch`返回OK则表示数据已被发送层接手并确定帧顺序;不再等待Process提交本批。`FINISHED(COMPLETED)`表示ACT_TERM已被发送层接手;这些成功状态均不保证报文写完或被主站确认。发送层可能随后因STOP或连接故障终止传输。
## 5. 应用事件与资源生命周期
### 5.1 三类业务事件
| 事件 | 应用需要做什么 | 应答接口 |
|---|---|---|
| BEGIN | 根据QOI检查数据源并决定是否接受;可延至首个NEED_DATA建立取数游标,不采样整轮数据 | `Reply_Begin(true/false)`;数据源失败可`Fail_Request` |
| NEED_DATA | 应用选择本批IOA与类型,读取当前值和品质后构造非空批次;暂未就绪则保存本事件,继续驱动通信 | `Submit_Batch`、`Complete_Data`、`Fail_Request`三者择一 |
| FINISHED | 停止对应采样工作,按实例/请求编号释放资源 | 无需应答 |
`NextEvent`主动取出事件;事件入队不会执行应用代码。BEGIN或NEED_DATA只有被取出之后才允许应答。工作事件只发出一次,应用暂未就绪时保存事件,不能反复等待模块发出同一事件。
模块核对实例、请求、批次、事件类型、当前阶段和已取出标志。应答时须原样传回事件;实际元数据和发送身份始终使用内部保存的原请求,不依赖应用回传字段构造发送目标。
| 应答结果 | 调用方处理 |
|---|---|
| OK=1 | 交互已消费,不要重复应答;输入ASDU可复用 |
| WAIT=0 | 有未读Slave通知,或Submit_Batch暂不能直接发送;保留事件及游标,推进通信、读空通知后重试;数据批次须重新读取当前值组包 |
| STALE=-2 | 事件已失效或不匹配,停止重试;按FINISHED清理资源 |
| ERROR=-1 | 参数或批次不合法等;通常不消费交互,可修正后重试。NEED_DATA阶段Fail_Request关闭失败是例外,任务仍已终结 |
### 5.2 FINISHED预留
单目标生成BEGIN需要两个业务位置:一个用于BEGIN,一个预留给未来FINISHED。NEED_DATA不能占用终结预留,始终保持`实际事件数 + 终结预留数 ≤ 事件容量`。多目标广播改由每站状态区保存终结记录,因此BEGIN只需一个FIFO空位,所有站可以先确认而不扩大FIFO。pending_events包含尚在状态区中等待入队的FINISHED,可能大于FIFO容量。
应用取出BEGIN后,无论正常完成、拒绝、撤销、STOP、断线、取消或超时,都恰好产生一次FINISHED。BEGIN尚未生成或尚未取出便取消时,移除工作事件并释放预留,不产生FINISHED,因为应用尚未取得请求。取消同时移除未读NEED_DATA;已经取出的旧事件再应答返回STALE。
历史FINISHED保留到应用读取。业务FIFO容量不足时,新的唯一总召可能停留在START等待空位;它已经占用总召资格,但尚未生成BEGIN,不启动业务期限。应用必须及时读完终结事件,不能只等待新的网络通知。默认容量4时可以同时保留旧FINISHED及下一次BEGIN;测试使用最小容量2验证这个边界。
多目标广播进入FINISHING后,连续NextEvent即可读完全部终结记录,无需靠Process搬运。应用不能假定BEGIN与FINISHED严格交替。示例在首个NEED_DATA建立唯一游标,Complete_Data返回OK后立即归还该站取数资源;失败/取消则按FINISHED清理仍在取数的站。若应用在BEGIN另行建立每站资源,须按request_id管理并等对应FINISHED清理。
## 6. 数据批次与发送路径
应用以`Asdu_Create`及信息对象接口构造批次,再调用`Submit_Batch`。模块立即尝试不排队提交,不保存待发批次、应用点表、对象指针或数据源回调。WAIT时输入存储也可复用,但同一业务事件和遍历位置须保留到成功或终结。
IOA、数据类型、分组成员和遍历方式均由调用者决定。BEGIN用于检查数据源和判断是否接受,遍历状态可延至首个NEED_DATA建立,不向库登记或冻结完整IOA列表。应用自行保证总召或分组点的遍历完整性,包括点表变化时如何避免漏点、重复点;库不强制点表在整轮期间保持不变。
推荐接入方式是在每次NEED_DATA组包时读取本批点的当前值和品质,不默认在BEGIN冻结整轮数值。值与品质的读取一致性由数据源保证;逐批取值不承诺整轮所有点属于同一采样时刻。需要固定时刻快照的应用可以自行实现,但必须另行处理快照与突发上报的顺序,不能把快照当成库提供的一致性保证。
总召数据通过`Slave_Try_Send_Response → TcpServer_Try_Send_Response → Connection_Try_Submit_Response → SendManager_Try_Submit_Response`提交,Slave按保存的原身份校验服务实例及连接代次。只有当前发送缓冲空闲、HIGH和NORMAL均无未提交条目、链路和发送窗口允许时,才直接准备I帧并确定N(S)。否则返回WAIT,不把旧值放入HIGH等待。NORMAL中仅保留已提交、待ACK的原文不单独阻挡本批,仍须满足k窗口;不要求等待全部ACK或整轮总召结束。
应用须把“更新当前点值并发布突发”与“读取本批当前值并调用Submit_Batch”放在同一串行执行上下文,或用等效同步保证二者不交错。WAIT之后只保留事件和游标,每次重试重新取值:已积压的突发先发送,随后总召读取最新值;总召返回OK后,本帧顺序已固定,后来发布的突发不能插入该帧之前。这同时避免旧NORMAL被新总召越过,以及新突发之后重发WAIT期间缓存的旧总召值。
这个保证依赖应用发布顺序:库不比较测点版本,无法识别应用在读取之后才迟到发布的旧采样,也无法修正调用者仍使用BEGIN数值快照的做法。突发发送不因总召被暂停;若HIGH/NORMAL持续有待发条目,总召数据可能持续WAIT,本路径没有强制抢占或最长完成时间。应用应按实际负载安排上报频率和批次,不能通过重试旧数值消除等待。
| 检查项 | 要求 |
|---|---|
| 应用层参数 | COT/CA/IOA长度分别为2/2/3 |
| 数据Type | 1、3、5、7、9、11、13、20、21,均为本模块支持的无时标监视信息 |
| 数据COT | 等于原请求QOI:全站20,第1..16组为21..36 |
| CA | 等于配置的本站地址,与业务事件request.common_address一致;不能是65535 |
| OA/TEST | 与原请求一致 |
| P/N | 0 |
| 数量与长度 | 至少1个对象,不超过配置上限及249字节;完整布局有效 |
| SQ及类型切换 | 每批允许SQ=0/1;一个ASDU同一Type,不同批次可切换Type |
返回OK时发送层已复制本批原文,应用可立即复用输入缓冲并推进自己的游标,`batch_index / objects_submitted`也已增加。返回WAIT时不消费NEED_DATA、不推进计数、不保存原文,也不重复生成NEED_DATA;应用先处理通知,交互仍有效时在通信推进后重读同一遍历位置并再次调用Submit_Batch。发送ERROR若确认为正常STOP导致的拒绝,返回WAIT等待通知,随后终结为STOPPED;其他发送ERROR/FATAL以SEND_ERROR终结并返回STALE,不能重试原事件。参数或批次校验ERROR则不消费交互,允许修正。
空数据集或已经遍历完毕使用`Complete_Data`,不能用空ASDU代替结束。未配置分组可以在BEGIN时拒绝;接受的空组仍按ACT_CON → ACT_TERM正常结束。累计量数据不在当前总召批次范围,留给总电度业务。模块不检查IOA是否属于请求的分组,分组筛选和取值由应用负责。
确认、终止和短应答沿原请求connection走普通HIGH应答接口;数据沿同一connection走不排队提交接口,不使用NORMAL,也不进入HIGH等待。只有上一批返回OK后才能完成数据阶段,因此终止不会越过尚未接手的数据。已接手帧按通信层规则发送,不能被新应答越过,也不能通过Cancel撤回。
## 7. 短应答调度与满槽处理
### 7.1 Handle_Event中的提前提交
忙拒绝、地址或COT错误、非法QOI、撤销确认等只需要单帧应答,先尝试提交现有HIGH接口。提前提交必须同时满足:没有未读Slave通知、原会话仍可发送、没有更早形成的REPLY。
```text
形成短应答
→ 有旧REPLY或未读Slave通知:暂不提交
→ 可安全提交:Copy_Response_Request复制请求并写入已确定的应答CA,调整COT/P/N,调用原HIGH接口
├─ 已接手:累计submitted,否定时累计negative_submitted,不占新任务槽
├─ WAIT:未接手,保留/保存为REPLY
├─ ERROR且确认为正常STOP拒绝:保留/保存为REPLY,先交付停用通知
└─ 实际提交错误:按SEND_ERROR等失败路径处理
→ 需要保存但池已满:只丢弃新请求,增加overloads和dropped_requests
```
立即提交不修改调用方事件,不增加绕过通信队列的公开模式。发送成功后本地副本即可释放;WAIT后如需保存任务,以已确定的应答CA建立稳定请求副本,后续只按阶段设置COT/P/N并重试提交。实际连接或发送错误仍按失败路径处理,不能当作普通满槽丢弃。
### 7.2 Process中的选择规则
单帧应答按形成顺序FIFO提交。调度器分别查找唯一总召和最早REPLY,不要求总召位于任务索引队首。总召正在等应用或业务事件空位时,直接处理REPLY;双方都可推进时轮转,避免总召一直挡住短应答,也避免短应答持续占据所有推进机会。
Send_Response内部会推进保留的接收帧并交付通知。Process每步检查FIFO;发送返回值还须区分以下情况,Process、立即应答和Submit_Batch共用同一STOP判定:
| 发送返回值 | 后续处理 |
|---|---|
| OK | 按已接手事实记账/推进,下一次提交前检查通知;不撤回或重发本帧 |
| WAIT | 保留任务并让出;先处理通知,再按原背压规则恢复 |
| ERROR且确认为正常STOP拒绝 | 设置events_pending并让出,保留任务/业务事件,由DEACTIVATED终结为STOPPED |
| 其他ERROR或FATAL | 保留SEND_ERROR失败路径;FATAL可能已写出部分或整帧,不能转成可重试WAIT |
只有ERROR分支额外查询状态:请求须仍属于原服务/实例/代次,TCP仍连接,会话ACTIVE且无故障、队列状态有效,传输已停用并且通知未读,才按正常STOP拒绝让出。不能仅凭FIFO非空忽略错误。成功路径不增加查询;正常STOP拒绝不累计waits、不推进游标、不停止或刷新取数期限。应用先读取Slave通知,旧NEED_DATA随后返回STALE;立即应答保存的短任务也由STOP清理。
Process提交确认、终止或短应答遇到WAIT就结束本轮,保存请求和阶段,下次优先检查最早REPLY。每一步都重新检查Slave通知,发送过程中产生的新通知也必须先处理。不会跨过未读STOP/START或撤销继续发送旧事务。数据WAIT由持有NEED_DATA的应用在通信推进后重读并重试。
`Process(gi)`没有预算参数,也不内部等待:当前站REPLY或终止成功后继续广播或释放任务;DATA只生成NEED_DATA。没有就绪任务、发送WAIT、有未读Slave通知或已推进2倍任务容量步时返回;剩余本地工作由need_process提示续跑。每步发送前重查通知,不执行应用代码。
### 7.3 4个槽的含义
任务池的容量与“同时允许几个总召”是两个独立约束。总召资格上限恒为1,任务池还为无法立即发送的短应答提供存储。忙应答占槽并不代表原激活已被保存,发完否定确认后即释放,不会再生成该请求的BEGIN。
满槽时保留已有任务、应用事件及进度,不主动关闭TCP,不挤掉最早应答。只有新的、无法安全提交或保存的请求被丢弃。`EVENT_HANDLED`包括这种丢弃;应用不能把它当成“主站已经收到应答”,也不能再次投递同一Slave通知。`dropped_requests`用于观察这个情况,主站可能等待超时或自行重试。
## 8. 业务期限与驱动
内部`Task_Timeout`按同一时刻汇总最近剩余时间、超时原因和已确认广播站的总期限到期标志。`Get_Status`读取该结果安排唤醒,`Expire`据此选择拒绝当前站或关闭原连接,不再二次遍历广播站或重新取时判定。结果只在本次调用中使用,不在任务内保存第二份期限状态。
| 配置 | 默认值 | 起止点 |
|---|---|---|
| `begin_timeout_ms` | 15000ms | BEGIN入业务FIFO至应用接受、拒绝或报告失败,包含未取事件的时间 |
| `data_timeout_ms` | 15000ms | 每次NEED_DATA入队至首次有效批次尝试、声明结束或报告失败,每批重新计时 |
| `total_timeout_ms` | 0(默认禁用) | 启用后从BEGIN生成至ACT_TERM被发送层接手,覆盖取数、事件容量等待和发送等待,不等待主站ACK |
三项默认值是本项目的可配置业务策略,不是IEC104规定的总召时限,也不代替链路t0..t3。每项0表示禁用。执行总期限默认关闭,避免慢链路的累计发送等待触发固定整体截止;BEGIN和每批取数期限仍各为15000ms,底层单帧发送、确认和链路探测期限继续有效。需要限制整次总召耗时时,可显式将`total_timeout_ms`设为非零。没有`queue_timeout_ms`;START尚未生成BEGIN时不计业务期限,但此时仍拒绝后续总召。及时消费业务事件是应用的驱动义务,模块不会通过后台线程保证进度。
所有期限使用Net提供的64位单调毫秒时钟,以经过时间比较,避免校时和绝对截止相加溢出。0毫秒有效。业务应答停止对应阶段期限,下一次NEED_DATA重新计时;显式总期限在同一站内不因新批次、忙应答或重试而延长。每站从自己的BEGIN独立计时,包含等待其他站确认、取数及终止的时间;已确认但不在当前取数位置的站也会被检查,到期关闭原连接并取消整轮。没有额外跨全部CA的整体定时器,同一时刻两项到点优先报告TOTAL_TIMEOUT。
有效批次因发送背压返回WAIT时置`data_send_waiting=true`,说明应用已响应,本批取数计时停止,之后不再恢复,下一批NEED_DATA才重新计时。无效批次或因未读Slave通知而提前返回WAIT不停止取数计时。发送等待仍受底层链路期限及显式启用的总期限约束;模块不替应用重读或重试数据,调用方必须持续驱动保留的事件。
Process和有效应用应答都会检查期限。先消费Slave通知,再判断超时,使已经到达的STOP、撤销或断线优先终结事务。到期后旧交互失效,迟到应答返回STALE;具体处理按肯定ACT_CON是否已被发送层接手划分:
| 失败发生的阶段 | 处理方式 | 连接及其他任务 |
|---|---|---|
| START:BEGIN超时、总超时或Fail_Request | 终结当前业务,复用原槽保存否定ACT_CON;保留BEGIN_TIMEOUT、TOTAL_TIMEOUT或SOURCE_ERROR | 保留TCP及已有短应答 |
| CONFIRM:肯定ACT_CON尚未提交成功,总期限到点 | 转为否定ACT_CON,终结原因仍为TOTAL_TIMEOUT | 保留TCP及已有短应答 |
| DATA / TERMINATE:取数超时、总超时或NEED_DATA阶段Fail_Request | 沿原请求关闭连接,取消本模块任务,不补发成功ACT_TERM | 原连接其他业务也会受影响,不关闭后来复用服务槽的新连接 |
分界是发送层接手肯定ACT_CON,不是应用调用Reply_Begin(true),也不是TCP写完或主站ACK。CONFIRM提交返回WAIT时仍可以拒绝激活;肯定确认即使仅进入HIGH或当前发送缓冲,也已经进入DATA,不能再改发否定确认。确认后的断链是本项目当前执行失败策略,不增加按超时类型区分的配置开关。
内部Reject_Request统一处理应用主动拒绝、BEGIN失败和确认前超时,Fail_Interrogation按阶段选择拒绝请求或关闭连接。不能直接从超时处理调用公开Reply_Begin(false):BEGIN可能尚未取出,该接口的校验也会再次检查期限。内部转换先Finish,再将原槽改为REPLY并移到应答顺序末尾;无需空闲任务槽,保留原QOI/OA/TEST,应答CA使用本站地址,旧工作事件立即失效。BEGIN已取出才产生FINISHED,具体失败原因不会统一改成REJECTED。
否定确认继续按现有调度发送,WAIT保留重试,不再计算已终结请求的业务期限。Process检查到确认前超时后会重新检查Slave通知,在本轮继续推进短应答;应用应答接口检查到超时则返回STALE,调用方仍须驱动Process。失败转换完成即释放总召资格,但原槽要到否定确认提交或取消后才归还。主站可以重新发起请求,模块不自动重放旧请求。
等待前查询`Get_Status`。若`timer_active=true`,以`timeout_remaining_ms`限制下一次Poll或应用等待的上限,0表示立即推进;false表示当前没有启用的业务期限,不代表业务已经结束。状态查询只读,不触发发送、终结或超时。
`need_process=true`表示应立即调用总召Process:例如新请求有空位生成BEGIN、业务应答接手后可提交下一帧、读取旧FINISHED后解除事件容量等待,或业务期限已到期。该值按Process同一选择规则计算,既考虑总召,也考虑排队的短应答;它不是`!waiting`,也不保证发送层一定接手。当前帧提交WAIT后暂停本地续跑,避免空转;每次Slave_Poll/Process推进网络并读空Slave通知后,仍须调用总召Process重试,不能以此标志为false跳过网络进展。
应用持有的数据WAIT也须在上述网络推进后重试,不能只等need_process置位或新的NEED_DATA。`data_send_waiting`表示已交付有效数据但等待发送机会;`application_waiting`只表示仍等待应用首次有效应答,两者都不是发送就绪查询。
Get_Status实时检查Slave FIFO;`events_pending=true`时`need_process=false`,先处理可能存在的STOP、撤销或断线。`pending_events`表示另一条业务FIFO,应及时读取;每次读取事件或调用应答接口后重新查询状态。外部事件循环还须合并Slave_Prepare的等待条件,总召的标志不替代TCP的need_process及链路期限。
## 9. 撤销、取消与连接变化
主站撤销为Type100、COT=8,在忙拒绝判断之前按原连接代次、目标CA、OA、TEST和QOI匹配,包括已选中但BEGIN尚未生成的START。定向撤销匹配站,其他站继续;已确认但尚未取数、数据已交完等终止、或已删除的原站均可匹配。尚未选入BEGIN的未来站没有活动事务。广播撤销停止整轮,对每个仍活动的站发送肯定COT=9,不向已结束或尚未启动的未来站补发确认。
撤销确认组复用原任务槽,排在已有短应答之后;即使任务池满也能撤销。当前站待发的否定ACT_CON不会被其他站的撤销确认覆盖。一组撤销确认尚未提交完时,新的重叠撤销按未匹配拒绝;不能安全保存否定回复时按既有满槽策略计数丢弃。已被发送层接手的帧不回滚,不再为被撤销站发送ACT_TERM。
匹配成功后,先通过Copy_Response_Request保存撤销请求并写入应答CA,使用原激活的info和进度生成FINISHED(DEACTIVATED),随后把原槽转换为肯定DEACT_CON。转换不需要额外任务槽,池满仍可撤销;形成的确认排在已有REPLY之后,条件允许时立即提交。已被发送层接手的帧继续在确认前按原顺序发送,本地未提交批次丢弃,不再提交原总召ACT_TERM。
| 操作或通知 | 本模块行为 | 网络确认 |
|---|---|---|
| 匹配COT=8 | 停止当前总召,旧工作事件失效,必要时FINISHED(DEACTIVATED) | 肯定COT=9 |
| 不匹配或重复撤销 | 当前总召保持原状 | 否定COT=9 |
| `Cancel` | 取消总召及待发短应答,保留必要的FINISHED(CANCELLED) | 不自动发送撤销确认或ACT_TERM |
| STOPDT对应DEACTIVATED通知 | 停止本模块任务,必要时FINISHED(STOPPED) | 链路STOP确认由通信层负责 |
| DISCONNECTED | 取消任务、清除已处理代次,必要时FINISHED(DISCONNECTED) | 无业务成功确认 |
| 新CONNECTED或会话已失效 | 旧任务终结为CONNECTION_CHANGED,不跨代次重放 | 后续新激活重新建立事务 |
COT=8的总召撤销与Slave的DEACTIVATED通知不是同一事件:前者结束一个业务,后者表示STOPDT停用。FINISHED表示应用可以清理资源,不承诺撤销确认已发送完成。
非法批次返回ERROR并保留交互,应用可以修正;数据源确实不能继续则调用`Fail_Request`。BEGIN阶段回否定确认并保留连接,NEED_DATA阶段关闭原连接,两者都终结为SOURCE_ERROR。确认后失败仍可能影响同连接其他业务。接受BEGIN前应检查数据源和必要业务资源,减少确认后才发现无法执行的情况;无需预先采样或编码全部数据,仍可逐批交付。
## 10. 应用接入与退出顺序
创建前使用`Config_Default`初始化配置,再按站地址、单批大小和实际业务耗时调整。公开API共12个,调用功能如下:
| 接口(省略IEC104_Interrogation_前缀) | 功能 |
|---|---|
| Config_Default | 填写CA=1、单批127对象及三项默认业务期限 |
| Create | 复制配置、借用Slave,取得静态池实例 |
| Destroy | 任务及业务事件全部为空时归还实例,不关闭Slave |
| Cancel | 本地取消任务并保留应有终结事件 |
| Handle_Event | 校验并接收应用转交的Slave通知,处理准入、短应答及会话变化 |
| Process | 自动推进就绪任务,遇等待条件返回 |
| NextEvent | 主动读取业务事件,只有READY写出结果 |
| Reply_Begin | 应答已取出的BEGIN,明确接受或拒绝 |
| Submit_Batch | 校验当前NEED_DATA对应的非空ASDU,立即尝试不排队提交;WAIT须重读数据后重试 |
| Complete_Data | 声明数据耗尽,后续Process提交ACT_TERM |
| Fail_Request | 报告数据源失败;BEGIN转否定确认并保留连接,NEED_DATA关闭原连接,均保留SOURCE_ERROR |
| Get_Status | 查询进度、累计计数及最近业务期限,不产生副作用 |
```text
Slave_Create / Config_Default / Init
→ Interrogation_Config_Default / Create
→ 循环
1. 驱动Slave,逐条读取并转交连接通知及请求
2. Slave通知未读空时继续处理通知
3. Interrogation_Process
4. Interrogation_NextEvent,处理BEGIN / NEED_DATA / FINISHED
5. 应用数据就绪时显式应答,继续推进
6. 查询Slave等待条件及Interrogation的need_process、事件和期限,再安排等待
→ Interrogation_Cancel
→ 读完业务事件并清理FINISHED对应资源
→ Slave_Close,读空收尾通知
→ Interrogation_Destroy
→ Slave_Destroy
```
网络驱动可以使用Slave的Poll,也可以接入应用自己的Prepare/Process事件循环。数据暂未就绪不能停止驱动网络;应用也不能在忙等待循环中反复调用Process而不处理事件。
`Destroy`不会隐式取消或丢弃事件。即使没有活动总召,只要还有REPLY或未读FINISHED就返回false;调用方完成清理后再销毁。实例创建失败要清理已创建的Slave,不能继续使用NULL句柄。
## 11. 状态与统计
| 字段 | 解释 |
|---|---|
| generation / active | 最近已处理的连接代次和启用状态,不能代替实时会话查询 |
| gi_pending | 总召网络流程未终结;FINISHING虽为false,状态区清理前仍忙拒绝新激活 |
| phase | 当前站阶段,收尾为FINISHING;仅有应答为REPLY,无任务为IDLE |
| pending_requests | 总召、FINISHING及短应答占用的任务槽数,不是等待执行的总召数 |
| pending_events | FIFO事件及状态区中待交付的FINISHED,不包含预留位置 |
| waiting / application_waiting | 存在发送、应用或事件容量等待;后者表示仍等待应用首次有效应答 |
| data_send_waiting | 当前批次已有效尝试但发送WAIT;不保存原文,应用须保留事件、重读数据并重试 |
| events_pending | 当前Slave通知尚未读空,应先处理网络通知 |
| need_process | 当前有需立即推进的本地工作、到期期限或失效会话;发送WAIT后等待网络推进再重试 |
| timer_active / timeout_remaining_ms | 是否启用业务期限及距最近期限的剩余毫秒 |
| batch_index / objects_submitted | 当前总召已提交的批次和对象数;仅有REPLY时保留最早应答的历史进度 |
| submitted / gi_submitted | 被发送层接手的ASDU数 / ACT_TERM提交成功的总召次数 |
| waits | 实际提交返回发送WAIT的次数,不统计应用等待循环次数 |
| rejected | 校验、忙、应用决定、确认前失败和容量原因拒绝的请求数,含无法可靠应答而丢弃的畸形请求 |
| negative_submitted | 发送层已接手的否定应答数,不等于主站已收到 |
| overloads / dropped_requests | 任务池满或广播状态区不足的次数 / 无法安全提交或保存的新请求丢弃数 |
| cancelled | 取消的协议任务数,包含待发REPLY;不等于FINISHED数量 |
| deactivated / timeouts | 成功匹配主站撤销次数 / 执行业务超时次数 |
| last_finish_reason | 最近合法总召的终结原因,BEGIN尚未交付时取消也会更新 |
状态为独立副本,累计计数从Create开始,Cancel和重连不清零。`waiting=true`不代表所有任务都无法推进:总召等数据时仍可提交短应答。`gi_pending=false`也不表示可以直接Destroy,仍须检查待发应答及业务事件。
确认前超时增加timeouts及rejected,BEGIN阶段Fail_Request增加rejected;两者都不增加cancelled或gi_submitted。只有否定确认被发送层接手才增加negative_submitted。REPLY等待期间反复Process不会重复累计超时或交付FINISHED,last_finish_reason保留原终结原因。
## 12. 总电度扩展与设计边界
Type100与Type101分别使用GeneralInterrogation和CounterInterrogation只读规则,共用引擎通过创建时传入的规则执行解析、限定词和数据检查以及完成方式选择,不引用具体种类符号。总召旧Create名称保留;计数量使用CounterInterrogation_Create。同一Slave每种规则至多一个实例;不能复用总召的gi_pending作为全局准入开关。
计数量RQT=1..5、FRZ=0..3;读取接受Type15/37,COT为37..41。冻结/复位通过Execute_Begin先检查请求再执行一次同步动作,肯定ACT_CON提交后完成,不产生NEED_DATA或ACT_TERM。TEST动作拒绝;已执行动作不会因超时转为否定,也不因断线回滚。请求结构追加freeze/data_cause,总召分别为0/QOI。完整约定见[计数量设计](IEC104_CounterInterrogation_设计架构.md)。
接入多个业务处理器时,连接、启停和断线通知应广播给每个相关模块,不能因某个模块返回HANDLED就停止广播;I帧按业务类型分派,未接手的交给后续处理器,最后再由应用兜底。总召模块不实现总电度冻结、复位或累计量点表。
多个业务可以分别持有任务,但仍共享连接、HIGH容量和发送窗口,应用需要持续驱动各模块。业务忙相互独立不代表发送时延相互独立;STOP、断线和任一业务触发的连接关闭会影响同连接所有业务。
本设计参考了[lib60870-C多客户端示例](https://github.com/mz-automation/lib60870/blob/master/lib60870-C/examples/multi_client_server/multi_client_server.c)按连接限制活动总召、忙时及会话槽满时否定确认的做法。该示例仅处理QOI=20,本项目保留20..36分组支持,使用应用主动读取事件、固定请求副本和明确的WAIT重试。其会话槽保存不同连接的活动总召,不能理解为同一连接的后续总召队列。
业务期限、静态容量和忙拒绝是本项目的实现策略。当前不提供多QOI待执行队列,不自动替主站重试,也不通过应用回调获得下一批数据。若现场要求保存连续分组请求,需要单独定义准入、主站确认等待和取消语义,不能仅扩大任务槽数量。
## 13. 验证与调用方迁移
总召基线回归98组,计数量另有12组,合计110组,使用真实Slave/TCP提交路径及可控单调时钟;HIGH缩到251字节、业务FIFO缩到2槽。覆盖原有突发排序、部分写入、取数期限、k=1窗口及到期ACK;多站覆盖13站三阶段顺序、非空混合批次、状态区不足与跨实例重叠、已确认未来站删除/同CA重建、超出FIFO容量的无损FINISHED、各阶段定向及满槽广播撤销、拒绝与撤销同时等待、非当前站总期限,以及总期限禁用、BEGIN先到期和两项同时到期的处理优先级。畸形请求覆盖否定/丢弃边界、输入不变、基础截短仍被Receiver拒绝、原任务/期限保持、背压满槽和广播撤销隔离。提交期STOP覆盖三个入口、真实通知容量背压、FATAL与STOP并存、非STOP的ERROR、OK/WAIT伴随新回执及START后重新总召。真实TCP回环验证命令行单站,使用Windows后端和k=1逐帧核对业务字节。
| 验证范围 | 应满足的结果 |
|---|---|
| 全站及全部16组 | QOI、数据COT、OA、TEST及点对象验证正确 |
| 广播全站及全部16组 | 业务事件、确认、数据、终止均用本站CA;输入Slave事件保持原文 |
| 广播错误、忙拒绝及确认前失败 | 副本建立时使用本站CA,立即提交和等待重试均保持该地址;其他错误CA仍按原值回显 |
| 广播数据地址 | Submit_Batch拒绝65535及其他站地址,应用修正为本站CA后可重试 |
| BEGIN生成前后、确认/数据/终止等待 | 相同和不同QOI的新激活均否定,原身份和进度不变 |
| 完成后重新发起 | 不自动启动被拒绝请求;主站重发后产生新request_id并获得完整期限 |
| 总电度类型隔离 | 总召空闲和忙时,本站或广播CA的Type101均返回IGNORED,不产生总召否定应答 |
| 短应答及满槽 | 安全时提前提交,积压保持FIFO;满槽只丢新请求,旧工作事件仍有效 |
| 远方撤销 | 本站/广播激活与本站/广播撤销四种组合均匹配实际站CA,其他身份仍严格检查;满槽仍可复用原槽 |
| 业务FIFO满 | FINISHED保留;BEGIN前不计业务期限,读出旧FINISHED后才能启动当前请求 |
| 本地续跑 | 事件空位恢复及业务应答后need_process置位;发送WAIT时不空转,网络推进后继续发送;未读Slave通知优先 |
| 三项执行期限 | 迟到应答不能绕过期限,忙应答不刷新总期限,先处理已到达的STOP/撤销 |
| 确认前失败 | 未读BEGIN超时不交付FINISHED;已交付时恰好终结一次,否定确认保留QOI/OA/TEST并使用本站CA |
| 满槽及发送等待期间失败 | 原槽转REPLY,不挤掉旧应答;按形成顺序发送,保留TCP,同连接重试可以完成 |
| 肯定确认提交边界 | CONFIRM发送WAIT时超时回否定确认;确认已接手但尚未写出时,取数失败及超时仍关闭原连接 |
| STOP、断线及实例复用 | 旧请求不能跨会话发送或接受迟到应用应答 |
| 真实TCP连续分组请求 | 第一个完成,后续得到否定确认;结束后重发的请求分别正常完成 |
| 真实TCP广播流程 | 全站、分组、忙拒绝、非法QOI及撤销均返回本站CA;连接继续工作 |
从先前多QOI排队版本迁移时,删除`queue_timeout_ms`、`queued_interrogations`、`queue_timeouts`字段访问,以及`QUEUED / FINISH_QUEUE_TIMEOUT`分支。所有调用方须重新编译并以Config_Default初始化配置。`Process`继续只接收实例,不增加预算参数;原12个总召接口保留,其中Create实现迁至GeneralInterrogation.c,工程须加入此源文件。计数量另增2个入口。`FINISH_OVERLOAD`仅保留旧枚举值,当前满槽不以此终结已有总召。
状态新增`need_process`和`data_send_waiting`,所有调用方须与库一起重新编译。原来用`pending_requests != 0 && !waiting`决定是否立即续跑的代码应改为读取need_process,并保留网络推进后调用总召Process的步骤。Submit_Batch现立即尝试提交,OK时进度已推进;WAIT时只保留业务事件和游标,每次重试须重新读取当前值,不能继续复用旧构造结果。Reply_Begin和Complete_Data仍由Process发送后续确认或终止。
请求存储精简只改变内部表示,总召公开函数、业务事件及配置字段不变。Slave新增按身份发送、尝试发送和关闭原连接的接口;总召内部使用,调用方继续按原方式驱动总召即可。完整SlaveEvent及Copy_Event保留原语义,其他业务无需迁移。
确认前失败修复不新增接口、配置字段或枚举值。调用方收到BEGIN阶段Fail_Request的OK或超时后的STALE时,不应再假定TCP已经断开,须继续处理FINISHED并驱动Process发送否定确认。
早期单站广播支持新增`IEC60870_Asdu_Set_CA`,当时未改变总召配置及事件布局。当前多CA版本新增use_station_registry与request.station_id,所有调用方须重新编译。新应用启用站点表,按事件实际CA构造数据,不把65535填入数据ASDU;BEGIN获取引用,FINISHED归还。Remove的OK已改为立即退出路由,释放内存须另查Is_Reusable,原先“Remove成功即可释放”的调用代码必须迁移。
广播发送顺序与[c104 Server.cpp](https://github.com/Fraunhofer-FIT-DIEN/iec104-python/blob/main/src/Server.cpp)一致:全部站确认、逐站数据、全部站终止。c104使用shared_ptr;这里用调用方节点、显式引用及轻量状态区,并保留可重试WAIT、业务事件和各站明确收尾。仍只允许一轮总召,不引入每站独立发送任务或动态发送队列。新增broadcast_states/broadcast_capacity及FINISHING,调用方须重新编译;启用多目标广播前配置状态区,并适配多个BEGIN先于首个FINISHED的事件顺序。
地址处理重构新增语义常量`IEC104_CA_BROADCAST`,将地址准备集中到内部副本创建,并移除任务中重复的header。公开函数签名和事件字段保持原样,应用无需增加调用。59组可控网络及真实TCP回归继续检查确认/数据/终止WAIT、错误地址、原事件不变、广播/本站交叉撤销和确认前失败,避免内部重构改变协议行为。
测试验证本模块的功能和边界,不代表已通过正式协议一致性认证。现场仍须确定主站对否定确认的处理、各分组点表、采样一致性及业务期限取值。
@@ -0,0 +1,550 @@
# IEC104 TCP子站服务与网络适配
业务默认使用`IEC104_Slave.h`统一入口;Slave拥有并驱动本模块,自动为I帧提供ASDU及原连接应答接口。本文保留独立TCP接入与内部维护的接口说明,下文的TCP调用方通常为Slave。业务示例见[README](README.md#子站统一入口)。
`IEC104_TcpServer.c/h`负责IPv4监听调度、连接生命周期、收发预算和业务事件队列,通过现有`IEC104_Connection`驱动子站协议。应用可用Prepare查询监听和定时条件,在自己的事件循环中等待,再调用Process推进;独立TCP循环也可用Poll组合这三个步骤。业务事件通过NextEvent主动取出,TCP模块不保存或调用业务函数。真实网络操作和单调计时通过`IEC104_Net.h`调用;当前提供的实现为`IEC104_Net_Windows.c`,尚未提供Linux或其他平台后端。当前仅提供模块,暂不提供独立运行入口。同时维护一条活动主站连接;额外连接被接受后关闭,原连接继续运行。
## 1. 构建与接入
应用工程需编译以下源文件:
```text
104/IEC104_TcpServer.c
104/IEC104_Net_Windows.c
104/IEC104_Connection.c
104/IEC104_Receiver.c
104/IEC104_SendManager.c
104/IEC104_SendBuffer.c
104/IEC104_AsduQueue.c
104/IEC104_Codec.c
common/IEC60870_Codec.c
104/IEC104_Link.c
104/IEC104_Link_Slave_StartStop.c
```
应用先调用Create取得静态池中的不透明句柄,再调用`IEC104_TcpServer_Config_Default`取得配置并Init;成功后在同一线程使用Prepare/Process或Poll驱动,并通过NextEvent处理事件,退出时Close、读完剩余事件,最后Destroy归还静态槽。具体接口和接入示例见下文。独立程序的入口、事件循环、退出控制和日志由应用负责。
配置中的`bind_address`默认`0.0.0.0`,表示监听本机全部IPv4接口;主站填写子站电脑实际IP和相同端口。同机运行可配置`127.0.0.1`。`port`默认2404;设为0时由系统分配,Init成功后调用Get_Status,从输出快照的`bound_port`读取实际端口。
业务I帧复制为`IEC104_TCP_EVENT_I_FRAME`事件,链路确认由Connection处理;总召、校时、遥控等ASDU业务应答需要接入对应业务处理。应用可使用`IEC60870_Asdu.h`进行命令解析、监视对象构造和原文应答构造,依赖及示例见[README](README.md#asdu信息对象模块);TCP不自动调用这些接口或执行命令。U帧启停和TESTFR应答由现有协议模块自动生成,TCP模块负责实际发送。TCP建立/断开与数据传输激活/停用分别通过CONNECTED、DISCONNECTED、ACTIVATED、DEACTIVATED交付,业务不必通过查询快照推断启停变化。
统一子站入口在以上依赖之外编译`IEC104_Slave.c`、`IEC60870_Slave.c`、`IEC60870_Asdu.c`、`IEC60870_InformationObjects.c`和`IEC60870_Time.c`。Slave内部保存服务句柄,由NextEvent创建ASDU视图并绑定原服务实例及连接代次;业务不再创建手动SlaveConnection。TcpServer本身继续独立于共用发送接口和完整对象编解码;发送/通知/启停策略不变。
以上路径均相对于项目根目录;工程需将 `104/` 和 `common/` 加入头文件搜索路径,并按所用模块选择源文件。应用工程需增加`IEC104_Net_Windows.c`并链接Windows库`Ws2_32`,MinGW参数为`-lws2_32`。平台实现按编译链接选择,同一工程只链接一个Net实现。
`IEC104_TcpServer.h`只包含`IEC104_TcpTypes.h`(其依赖`IEC104_Types.h`),不包含Connection、Net或其他运行对象的定义,也不要求`_WIN32`。业务只看到公开配置、事件、状态和调度值类型;内部对象完整定义在`IEC104_TcpServer.c`。库内Windows依赖集中在Net后端;应用自己的事件循环适配代码负责把借出的标准整数socket转换为本平台注册类型。业务解析ASDU公共头时显式包含 `IEC60870_Codec.h`,完整对象解析包含 `IEC60870_Asdu.h`。接口迁移见下一节;平台解耦不代表已经提供其他操作系统后端。
## 2. 对外接口与存储
| 接口 | 用途 |
|---|---|
| `IEC104_TcpServer_Create` | 从静态池取得不透明句柄;无可用槽返回NULL,不申请网络资源 |
| `IEC104_TcpServer_Destroy` | 归还已关闭且事件读空的静态槽;运行中或尚有事件时拒绝 |
| `IEC104_TcpServer_Config_Default` | 设置默认监听地址、端口及公开协议参数 |
| `IEC104_TcpServer_Init` | 初始化Net后端、非阻塞监听和服务队列;成功后即可缓存NORMAL |
| `IEC104_TcpServer_Prepare` | 只读查询完整socket关注集合、定时条件、待取事件数量和本地续跑标志 |
| `IEC104_TcpServer_Process` | 输入一批socket/定时/本地通知,有界推进接受、收发和协议,不执行等待 |
| `IEC104_TcpServer_Poll` | 组合Prepare、至多一次Net_Wait和Process;max_wait_ms限制网络等待 |
| `IEC104_TcpServer_NextEvent` | 按FIFO复制并取出一个事件;READY=1、EMPTY=0、ERROR=-1 |
| `IEC104_TcpServer_Enqueue_Event` | 复制服务级主动上报ASDU;无连接也可缓存,不需要generation |
| `IEC104_TcpServer_Send_Response` | 提交当前连接应答,先检查启停准入,通过后尝试一次非阻塞发送或暂存HIGH;须携带原事件generation |
| `IEC104_TcpServer_Get_Status` | 复制端口、代次、事件数、诊断、会话状态及HIGH/NORMAL积压与容量 |
| `IEC104_TcpServer_Get_Remote_Address` | 只读复制当前主站的IPv4、源端口和连接代次;无连接返回NOT_CONNECTED |
| `IEC104_TcpServer_Close_Connection` | 仅关闭匹配代次的当前主站,保留监听和现存NORMAL,继续接受新连接 |
| `IEC104_TcpServer_Close` | 结束当前连接、保存收尾事件、清空ASDU队列并关闭网络资源;保留未读事件 |
Init/Prepare/Process/Poll/Close_Connection/Close/Get_Remote_Address/Get_Status/Destroy统一返回`IEC104_TcpResult_t`,只有`IEC104_TCP_OK`(值为0)表示成功,其他值均为明确的失败分类。调用方必须显式比较枚举,不使用`if (!Init(...))`或将结果直接当作循环条件。这些接口的最后一个参数统一为可选的`IEC104_TcpError_t *error`;不需要详情时传NULL。不提供旧签名、布尔返回值或Ex兼容包装。
| 结果(均带`IEC104_TCP_`前缀) | 含义与处理 |
|---|---|
| `OK` | 本次操作成功;Poll空闲、暂不能接入或单条连接已收尾时也可成功 |
| `INVALID_ARGUMENT` | 必填指针为NULL,或就绪批次数量、事件位、重复项等不合法 |
| `INVALID_HANDLE` | 句柄为NULL或不属于当前已占用的静态槽,修正对象生命周期 |
| `INVALID_CONFIG` | 配置范围或地址字符串结束符检查失败,修正配置后重试Init |
| `INVALID_STATE` | Init/Destroy时服务仍在运行,或Prepare/Process/Poll/Get_Remote_Address/Close_Connection时尚未Init |
| `BUSY` | Process/Poll/Send_Response/Close_Connection/Close期间发生重入;所有调用须回到所属线程串行执行 |
| `EVENTS_PENDING` | Close后尚有未读事件,先通过NextEvent读空,再Init/Destroy |
| `NETWORK_ERROR` | 网络初始化、监听、等待或不可恢复的接入失败,按error的阶段和原生码诊断;接入资源暂缺由内部暂停重试 |
| `INTERNAL_ERROR` | 队列/协议会话初始化、等待期限查询或内部一致性异常,停止服务并检查实现 |
| `GENERATION_EXHAUSTED` | 代次用尽,Close并读完事件后Destroy;该槽不再重新分配 |
| `REGISTRATION_EXHAUSTED` | socket注册编号用尽,同样关闭并归还;不回绕分配旧身份 |
| `NOT_CONNECTED` | Get_Remote_Address/Close_Connection时服务已Init,但没有当前主站;不返回旧地址、不产生新的断开事件 |
| `STALE_GENERATION` | Close_Connection指定的非零代次不匹配当前主站,拒绝操作,不关闭新连接 |
`error`包含与返回值一致的`result`、失败`stage`和本次网络错误的`socket_error`。每次调用均完整覆盖,成功为OK/NONE/0,非网络错误的原生码为0;即使句柄无效或Destroy成功也能取得诊断。诊断存储由业务持有,不得与config/status/info/ready等输入输出重叠。需要保存首个失败时,清理或查询调用可传NULL,避免覆盖自己保存的error。阶段包括VALIDATE、LIFECYCLE、NET_INIT、LISTEN、WAIT、ACCEPT、GENERATION、QUEUE_INIT、QUEUE_STATE、CONNECTION_INIT、SCHEDULE和REGISTRATION;LISTEN包含后端的地址内容解析、socket创建、绑定及监听,QUEUE_STATE表示服务队列异常或无法解除旧绑定,SCHEDULE表示无法取得可靠的协议等待上限,REGISTRATION表示无法继续分配socket注册身份。
在服务队列正常的情况下,客户端EOF、收发错误、协议错误和超时只结束该客户端,Process/Poll仍返回OK并保留监听;连接原因经DISCONNECTED事件中的`IEC104_TcpFault_t`交付。服务级推进或期限查询失败后保存本次error,再Close并读完事件。应用事件循环自身的等待/注册失败由应用诊断和处理,不伪装成TCP协议故障。Get_Status中的`last_socket_error`是历史后端诊断,可能来自更早的连接或可恢复错误,不能用它判断本次接口是否失败。该字段、error及连接事件中的原生错误码仅用于日志,当前Windows后端使用Winsock错误;跨平台业务按公开结果及连接故障分类处理。
正常TCP有序关闭在没有既有故障时交付`fault=IEC104_TCP_FAULT_NONE`和`socket_error=0`。例如TRANSPORT表示传输异常,I_ACK_TIMEOUT表示I帧确认超时,不能把所有非零fault都当作协议错误。公开的connection快照只包含会话阶段、故障、尚未转成RECORD事件的记录数量及已提交的STARTDT状态;已经进入事件FIFO的记录不计入该数量。Connection内部启停过程、接收借用和队列字段不再透传。启动状态也不表示窗口和发送容量一定可用,业务仍以提交结果为准。DISCONNECTED保留关闭协议前的阶段和故障,phase可能仍为ACTIVE;其中`pending_send_records`扣除本次收尾已成功转为RECORD事件的记录,全部转移成功时为0,不表示这些帧全部已ACK。当前TCP是否连通应查询Get_Status的connected。
内部Connection调用返回ERROR、EOF时设置NORMAL发送限制失败,以及其他内部异常均按INTERNAL收尾,不冒充网络传输错误。已冻结连接返回FATAL时保留其首次原因;Send_Response遇到未约定的内部返回值也归INTERNAL,正常的业务参数或启停准入拒绝仍只返回SUBMIT_ERROR。实际网络失败通过Connection登记TRANSPORT;若发送失败或传输关闭回报未能返回预期FATAL,则由TcpServer补记INTERNAL,同时保留本次原生socket_error。收尾读取或保存记录失败也补记INTERNAL;已有协议、传输或超时故障优先保留。状态无法可靠查询时status_valid仍为0,不能读取无效快照推断故障类别。
子站收到格式合法的STARTDT_CON或STOPDT_CON时消费并忽略,不因该帧断连,不回复、不切换启停状态、不产生业务通知,也不解除确认等待或刷新t3。该处理不占通知和控制发送槽;后续输入及原有期限检查照常推进。起始字节、长度/APCI、ASDU最低长度、I/S序号或ACK错误仍沿用原故障规则。
每个APDU的首字节必须为0x68,接收器不跳过前导垃圾;遇到错误起始字节立即登记FRAME故障并断开。报文体仍按声明长度收集,其中的0x68是普通数据,不参与重新同步。
`IEC104_TcpServer_t`是不完整类型,应用只能声明其指针,不能直接定义对象、清零、复制或访问成员。内部静态池保存真实结构体,地址与对齐由编译器保证,不使用malloc,也不需要业务准备字节数组并强转。`IEC104_TcpServer_Config.h`中的`IEC104_TCP_SERVER_CAPACITY`默认1,表示同时持有的服务器句柄数;需要多个监听服务时,调整该值或为TcpServer.c增加编译参数`-DIEC104_TCP_SERVER_CAPACITY=数量`。每个服务仍只接受一条活动主站连接,容量增加也会增加静态内存。
默认每个槽持有Connection、ASDU队列、2048字节TCP接收暂存、1024/4096字节HIGH/NORMAL队列、8个控制槽、32条发送记录和一个变长事件字节环。2048字节能容纳每轮8个满长APDU(2040字节),保留原有每轮交付预算;接收预算让出后的确认超时检查采用下文的固定输入快照规则。容量统一在配置头文件中设置,编译期检查普通、启停及收尾容量。内部容量变化只需要重新构建模块,业务头文件不暴露这些容量或内部结构大小。静态槽在Close后仍占用,直到Destroy成功才归还;首次Init失败也必须重试或Destroy,不能丢弃句柄。
公开提交接口接受6..249字节ASDU,因此`IEC104_TCP_HIGH_CAPACITY`至少为`IEC104_ASDU_HIGH_ENTRY_HEADER_LEN + IEC104_MAX_ASDU_LEN`,`IEC104_TCP_NORMAL_CAPACITY`至少为`IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN + IEC104_MAX_ASDU_LEN`;当前x64 ABI下分别为251和265字节。每级总容量还须不超过UINT32_MAX,头长由sizeof推导,过小的配置在编译期拒绝。`Send_Response`直接准备时不经过HIGH,回退排队时使用HIGH;`Enqueue_Event`始终使用NORMAL。该约束保证合法长度不会仅因直接发送转为排队而被判SUBMIT_ERROR;HIGH空闲不足仍返回WAIT,NORMAL仍按原规则覆盖旧原文或因编号窗口受阻返回WAIT。
同一服务的全部调用及业务入队必须串行执行;Create/Destroy还须在各服务之间串行。句柄在Destroy后失效,应立即清空业务保存的指针。Destroy不隐式关闭服务或丢弃事件;业务先Close、读空NextEvent,再归还槽。
业务通知FIFO按实际长度存放,头部`TcpEventHeader_t`含generation、payload_length、type;载荷直接复制公开data成员的有效前缀。I_FRAME只保存`offsetof(IEC104_TcpIFrameEvent_t, apdu) + apdu_length`字节;RECORD和连接/启停通知分别保存对应公开结构的sizeof。头部及载荷均用memcpy读写,跨环尾时分两段复制,不留下空隙,也不强转环内结构体指针。
配置宏名和条数推导保持不变:`EVENT_CLOSE_RESERVE = RECORD_CAPACITY + 2`,`EVENT_NORMAL_CAPACITY = EVENT_CAPACITY - EVENT_STATE_RESERVE - EVENT_CLOSE_RESERVE`。普通配额默认30,表示按最长I帧通知计算的保证条数,供CONNECTED、I_FRAME及正常RECORD共享;启停配额默认4,仍限制ACTIVATED/DEACTIVATED的未读条数;收尾保证32条记录、1个最长I帧及1个DISCONNECTED。全部尺寸由sizeof/offsetof推导,编译器`-D`覆盖仍有效;编译期同时检查字节索引和最短通知可能达到的公开16位计数范围。
当前x64 ABI下头部16字节,I帧通知为`30 + APDU长度`字节,RECORD为64字节,连接及启停通知为72字节:
| 区域 | 字节容量计算 | 默认值 |
|---|---|---:|
| 普通 | 普通保证条数 × 最长I帧通知 | 30 × 285 = 8550 |
| 启停 | 启停保证条数 × 连接通知 | 4 × 72 = 288 |
| 收尾 | 最长I帧 + 全部记录 + DISCONNECTED | 285 + 32 × 64 + 72 = 2405 |
| 字节环合计 | 普通 + 启停 + 收尾 | 11243 |
事件环与接收暂存合计从23680降至13291字节,减少43.9%。2026-10-19字节环改造后,MinGW x64服务对象从31848降至21464字节;2026-10-20调度修复增加8字节,当前为21472字节。完整对象大小还取决于其他内部结构及目标ABI。16字节APDU每项46字节;普通区空闲时可存185条,仍为已受理但等待交付的下一帧保留收尾空间。这里的普通通知区与NORMAL发送队列独立,NORMAL覆盖旧原文不会释放通知FIFO空间。
普通入口同时检查非启停占用加本项不超普通区、总占用加本项不侵占收尾区;CONNECT可按自身实际大小接入,但旧启停通知必须先读完。启停入口统一检查条数配额及完整收尾预留。Append先检查空间,再写头部和载荷,最后提交字节占用及通知计数。全部已入队通知按顺序逐条保存,不覆盖、不合并。
普通通知满仍可按TCP顺序处理前面的S/U,ACK记账不等待业务取出回执。已确认回执在普通空间不足或本轮交付预算用尽时暂留内部,可能晚于随后发生的启停通知交付;保持不因回执积压而暂缓启停受理及CON发送的行为。启停变化由Connection在ACT受理时通过内部适配器立即交付:先查询启停容量,状态改变后复制事件。重复ACT不改变状态时不占新配额;CON完整写出仅确认发送结果。
启停专用槽满时保留完整ACT并返回WAIT,尚未受理、不改变状态;TCP输入按原顺序等待,不越过该请求。已有CON继续写出,发送不再等待状态通知槽。NextEvent消费事件后安排Process重试;原有协议期限继续生效,长期不消费仍可能超时。
I帧按本帧实际长度检查普通空间;不足时将已受理帧保留在Receiver,设置`receive_paused=1`并暂停后续Feed/recv,同时停止转移回执,避免较短的RECORD插到该I帧前面。不覆盖请求、不自动回复业务拒绝,也不因通知积压主动断开连接。NextEvent腾出普通空间后安排本地Process,先复制该帧、Release,再恢复原来的输入尾部和接收关注;不重复处理该帧序号或ACK。等待期间已有发送及协议期限继续推进,不能延长计时来掩盖积压。TCP有序,保留I帧会阻挡其后的S/U;应用必须及时消费事件,持续积压仍可能导致原有协议超时。若此时关闭连接,收尾顺序为剩余I帧、全部记录、DISCONNECTED。旧启停通知须先读完,其他旧通知也须释放出新连接建连通知及完整收尾预留所需的容量,才允许接入新会话。
每轮最多交付8条通知;预算或本类配额耗尽时先返回应用,下一轮继续。普通区放不下最长通知才视为配额用尽,下一轮仍按每项实际长度判断。receive_paused只表示已受理I帧等待普通通知空间,不代表STOP停用或启停ACT等待通知容量。应用仍通过NextEvent消费事件;适配器仅连接TcpServer和Connection,不是业务回调。
NextEvent先检查游标、计数、类型、载荷长度及I帧APDU长度,在清零的临时公开结构中展开;全部校验通过后才写输出并出队。EMPTY/ERROR不改输出,损坏项也不部分出队;短APDU后的数组尾部清零,历史代次照常交付。副本无任何借用指针,可独立保存、按值复制,无需Release。Close保留未消费事件;Close后尚未读空时再次Init或Destroy均返回EVENTS_PENDING。读空后可以Init重开,或Destroy归还槽;同一静态槽重新Create仍保留generation计数。旧事件可以作为历史记录保留,但不能配上另一个服务器句柄重新提交。
`iec104_tcp_event_ring_test.c`以可控Net后端覆盖跨环尾、容量预留、损坏项原子拒绝、短帧背压恢复、回执次序、断线重连及分包。该测试直接包含TcpServer实现,构建时不要再次链接TcpServer.c或Net_Windows:
```powershell
New-Item -ItemType Directory -Path .build -Force | Out-Null
gcc -std=c11 -O2 -I104 -Icommon -Wall -Wextra -Werror test/iec104_tcp_event_ring_test.c 104/IEC104_Connection.c 104/IEC104_Receiver.c 104/IEC104_SendManager.c 104/IEC104_SendBuffer.c 104/IEC104_AsduQueue.c 104/IEC104_Codec.c common/IEC60870_Codec.c 104/IEC104_Link.c 104/IEC104_Link_Slave_StartStop.c -o .build/iec104_tcp_event_ring_test.exe
if ($LASTEXITCODE -ne 0) { throw 'TCP event ring build failed' }
./.build/iec104_tcp_event_ring_test.exe
if ($LASTEXITCODE -ne 0) { throw 'TCP event ring tests failed' }
```
2026-10-19验证:默认配置、`RECORD_CAPACITY=3/EVENT_CAPACITY=12/STATE_RESERVE=2`、`RECORD_CAPACITY=40/EVENT_CAPACITY=100/STATE_RESERVE=6/RX_CAPACITY=4096`均通过11个专项场景,Windows生产库以`-Wall -Wextra -Werror`构建通过,GCC静态分析通过。公开头文件去注释后保持一致,Connection/Net文件哈希以及Process_Input、Read_Client、Receive_Input函数均未改变。接口过期的旧回归测试及专用辅助文件已删除,当前入口和构建命令见[测试说明](../test/IEC104测试文档.md)。
以下是2026-10-19存储改造的历史性能测量,不包含2026-10-20新增的内核输入快照查询及独立发送调度,不能作为当前版本的性能结论。不能保证recv次数及每帧耗时在所有输入下完全相同。模拟Net、每批8帧并安排一次空输入推进发送的对照中,16字节和255字节APDU分别处理160万帧,修改前后均调用recv 40万次(包含空输入检查);5次串行测量的每帧耗时中位数如下,包含测试报文生成、Poll及NextEvent,不代表真实socket的端到端延迟:
| APDU长度 | 固定槽 | 字节环 |
|---|---:|---:|
| 16字节 | 0.125微秒 | 0.148微秒 |
| 255字节 | 0.137微秒 | 0.166微秒 |
一次积压16个满长APDU(4080字节)时,recv调用由1次变为2次,这是暂存从4096缩至2048字节的直接结果;每轮最多交付8条的调度预算保持不变。测量输出与验证记录保存在`.build/fifo-byte-ring-20261019/benchmark-results.json`及`verification.json`。
绑定地址目前只支持数字IPv4字符串,不解析主机名。Init复制配置,`protocol`使用`IEC104_Types.h`中的`IEC104_ProtocolConfig_t`;Connection也使用此公共配置,保留原类型名作为兼容别名。配置必须满足`0 < t2_ms < t1_ms`;`t2_ms >= t1_ms`在申请网络资源及初始化服务队列前返回`INVALID_CONFIG`,error阶段为`VALIDATE`。默认t2=10000ms、t1=15000ms满足该约束。Get_Status返回独立副本,失败不改输出,Create后、Init失败后以及Close后都可查询;关闭后端口为0,最近代次保留。Windows后端保持`SO_EXCLUSIVEADDRUSE`策略,避免同一地址被重复监听。
原TCP调用方需要迁移以下用法并重新编译:
| 原用法 | 现在的用法 |
|---|---|
| 直接定义或清零`IEC104_TcpServer_t server` | 持有`IEC104_TcpServer_t *server = IEC104_TcpServer_Create()`,完成后Destroy |
| Init/Poll/Close/Get_Status/Destroy返回值按0/1判断 | 为调用增加error或NULL末参数,并显式比较`IEC104_TCP_OK` |
| `config.poll_interval_ms`及`Poll(server, error)` | 移除该配置项,直接使用`Poll(server, max_wait_ms, error)`指定本轮等待上限 |
| 接口失败后只查询`last_socket_error` | 直接读取本次`IEC104_TcpError_t`的result/stage/socket_error |
| 读取`server.bound_port/last_socket_error/generation` | Get_Status取得对应快照字段 |
| `config.connection` | `config.protocol`,各协议配置字段含义不变 |
| 入队结果使用`IEC104_ConnectionResult_t`及`IEC104_CONNECTION_*` | 使用`IEC104_TcpSubmitResult_t`及`IEC104_TCP_SUBMIT_*` |
| `Enqueue_Normal` / `Enqueue_High`及`IEC104_TcpEnqueueResult_t` | 分别迁移为`Enqueue_Event` / `Send_Response`及`IEC104_TcpSubmitResult_t`;结果常量改为`IEC104_TCP_SUBMIT_*`,无旧入口包装 |
| 通用业务入队入口及重连保留开关 | 主动上报调用Enqueue_Event;连接应答调用Send_Response;NORMAL随服务保留,Close清空,无兼容包装 |
| 直接使用Connection状态枚举或内部`dt_state` | 使用`IEC104_TCP_SESSION_*`、`IEC104_TCP_FAULT_*`及`data_transfer_started` |
| 发送记录中的`frame_id`、`i_frame_snapshot`、`asdu_priority` | 使用事件generation及`value.frame_serial/send_number/receive_number/apdu_length/priority` |
发送事件不再公开NORMAL队列的内部条目编号。业务身份应由ASDU业务层维护,发送记录只表达传输与链路确认事实。
### 2.1 ASDU队列积压查询
`IEC104_TcpServer_Get_Status`同时提供`high_queue`和`normal_queue`,两者均为`IEC104_TcpAsduQueueStatus_t`数值副本。业务通过Slave_Get_Status取得同一快照,不取得队列指针,不读取Connection、SendManager或字节环成员。状态结构新增字段,库与调用方须一起重新编译;不新增兼容入口。
| 字段 | 含义 |
|---|---|
| `pending_asdu_count` | 队列中尚未交给发送模块的ASDU条目数;不含已组帧、部分写出或等待ACK的帧 |
| `stored_asdu_count` | 队列中仍保留ASDU原文的条目总数;不是累计入队次数 |
| `capacity_bytes` | 本级队列的实际总容量,单位字节 |
| `used_bytes` | 全部保留条目占用的字节数,包含内部条目头及ASDU原文 |
| `free_bytes` | 当前未占用的字节数;与used_bytes相加等于capacity_bytes |
先检查Get_Status返回`IEC104_TCP_OK`,再检查`status.asdu_queue_status_valid`。标志为1时两级统计均有效;与`connection_status_valid`相互独立。Init成功后,即使尚未接入主站或已经断线,也能查询NORMAL缓存。尚未成功Init、Close后或内部队列不可用时,标志为0且两份统计清零;此时表示统计不可用,不能按“没有积压”处理。重复Init被拒绝不会清除仍在运行的服务或使其队列统计失效。
HIGH原文在交给发送模块后立即出队,因此其pending与stored相等;直接进入发送缓冲的应答不经过HIGH队列。NORMAL原文通常保留到ACK后才释放,因此stored可能大于pending。例如10条NORMAL中已有4条交给发送模块且尚未确认,原文均未被覆盖时,pending为6、stored为10。字节占用统计这10条原文及条目头,不统计独立发送缓冲、APCI或发送记录;已覆盖原文对应的待ACK帧不再计入队列统计。
受理新STOPDT_ACT时立即清空HIGH待发应答,不产生逐条作废事件;队列数量归零不代表这些应答已发送。断线收尾也会清空HIGH,并将仍保留的NORMAL恢复为待发;断线期间新增NORMAL继续计入pending。收到EOF后暂留给下一连接的NORMAL也计入pending,因此待发数量不等于“本轮允许立即发送的数量”。队列为空也不表示主站已经确认或业务已经完成,传输完成事实仍从RECORD事件获取;`connection.pending_send_records`另含尚未交付为事件的记录,不能作为队列积压相加计算。
统计用于观察积压趋势及容量占用,不预留空间。ASDU长度不同,free_bytes不能直接换算为可接收条数;NORMAL空间不足时仍可能覆盖旧原文,HIGH直接发送时也可能不占队列。业务提交仍以Enqueue_Event/Send_Response返回值为准。Get_Status只做常数时间查询,不执行I/O、不消费ASDU或推进协议;与其他接口一样,应在服务所属线程串行调用。
### 2.2 接入暂停与错误诊断
`IEC104_TcpServerStatus_t`另提供以下只读字段。可恢复接入失败由模块安排重试,Process/Poll返回OK,不新增业务事件;应用通过这些字段观察持续接入异常。状态结构新增字段后,库与调用方须一起重新编译。
| 字段 | 含义 |
|---|---|
| `accept_paused` | 查询时刻仍在接入暂停期为1;到期立即为0,与是否有接入容量无关 |
| `accept_retry_remaining_ms` | 本次暂停剩余毫秒数;未暂停为0;查询不会刷新期限 |
| `last_accept_error` | 最近一次Net_Accept错误的原生码;初始为0,后续错误更新,成功、WOULD_BLOCK及客户端断开不清除 |
| `consecutive_accept_failures` | Net_Accept连续返回RETRY、RETRY_LATER或ERROR的次数;到UINT32_MAX饱和,OK或WOULD_BLOCK清零 |
连续失败计数只描述接入调用结果,WOULD_BLOCK表示当前没有待接入连接,计数清零不证明资源已经恢复。`last_accept_error`独立于可能被客户端收尾或接入成功覆盖的`last_socket_error`,便于保留接入失败线索;二者均不代表当前API调用失败。成功接入额外客户端并随即关闭也属于Net_Accept的OK,会清零连续失败次数。
Close_Connection和客户端自然断开不提前结束接入暂停,也不清除接入诊断;整个服务Close时撤销重试期限,保留最近错误及连续失败计数供查询。下一次Init重置这些诊断;Destroy结束对象生命周期后不再保留。固定100毫秒暂停不开放配置,持续失败继续按此间隔重试,不自动升级为服务级错误。
## 3. 收发循环
Net后端交付的监听socket与客户端socket均为非阻塞。调度分为两个公开步骤:`Prepare(server, &info, error)`只查询等待条件;`Process(server, &ready, error)`只做有界非阻塞推进。等待由应用的事件循环统一安排,库不会在Prepare或Process中再次调用Net_Wait。
`Poll(server, max_wait_ms, error)`是独立TCP循环的组合入口,内部使用相同的准备和推进核心,并在中间至多调用一次Net_Wait。0表示只查询就绪,正数限制本轮网络等待,UINT32_MAX仍表示有限毫秒数;实际等待还受协议期限、接入/发送重试期限、本地工作和未读事件约束。该参数只约束等待,不保证整个函数的执行耗时。两个入口没有两套收发实现;同一服务由一个串行调度者管理,不应同时给Poll和另一个事件循环安排等待。
Prepare通过内部`Connection_Get_Wait_Time`查询当前帧本地发送、最早未ACK的I帧t1、TESTFR t1、STOP总期限以及可准备S/TESTFR的t2/t3。它不推进协议、不准备发送帧、不更新计时起点、不关闭连接。查询异常返回INTERNAL_ERROR/SCHEDULE且不修改输出;已失败会话则标记立即推进,由Process执行收尾。TcpServer不读取Connection、Link或SendManager的计时字段。
S确认已在途、TESTFR已发送或等待CON、控制槽已满时,当前不能新增控制帧的t2/t3不反复触发零等待;已有帧仍受本地发送期限约束,等待CON仍受t1约束。容量或协议门控恢复后,按原始期限重新推进。
每轮Process共用最多32次Feed、32次发送尝试、4次accept和8条通知交付的预算;断线收尾必须一次完整交付,不受通知预算限制。可读/接入通知一直保留到实际操作返回WOULD_BLOCK;预算耗尽或交付用尽本类配额时,可立即推进的工作由下次Prepare要求续跑,不依赖新的边沿;接入/发送暂停期间则等待各自的重试期限。发送预算耗尽、有界无进展重试、活动连接的HIGH入队、未EOF时的NORMAL入队及NextEvent释放通知槽也会安排本地推进;无连接或EOF后的NORMAL只缓存。普通区满但尚无受阻I帧时仍关注可读;出现`receive_paused`后撤销客户端READ关注,并停止为该受阻输入反复安排本地续跑,保留接收边沿供恢复使用。协议WAIT仍等待控制发送或期限解除。已准备好待发字节、未处于发送重试暂停且不因启停通知槽不足而暂缓时才关注可写;消费通知后通过本地唤醒恢复,不能把“业务发送队列非空但等待STARTDT/ACK”作为持续关注可写的理由。
Net_Accept返回RETRY_LATER时,立即结束本轮接入并保存`accept_retry_at_ms = Now_Ms() + 100`,保留`accept_pending`及当前连接。普通RETRY仍可在本轮4次预算内继续;若整轮4次全部为RETRY,也进入相同暂停,避免每轮有预算却跨轮不断立即重试。曾有OK接入进度的预算耗尽仍按原规则续跑,额外客户端被接入后关闭同样算进度。WOULD_BLOCK清除接入待处理标志,真正的监听失败仍返回NETWORK_ERROR。
暂停状态统一定义为`now_ms < accept_retry_at_ms`,不另存需要解除的标志;等于期限即已到期。Prepare只读取一次时钟,用于监听关注、本地续跑、协议期限和接入期限的合并。暂停期间移除监听READ关注,`accept_pending`不单独触发need_process;Process末尾真正调用Accept_Clients之前再次按当前时钟检查期限,客户端收发、NextEvent或迟到监听通知均不能提前触发accept,也不能刷新暂停期限。暂停只限制接入,当前客户端的收发和协议期限继续推进。
到期后保留的accept_pending配合原有容量规则安排本地重试,不依赖新的监听边沿。无客户端且通知容量仍不足时,不恢复监听关注、不因接入请求续跑,也不把已过期的接入期限保留为零等待定时器;业务消费NextEvent后再恢复接入。客户端在暂停期间断开不会提前结束暂停,新主站仍须等待剩余时间。
`Send_Response`仅在Connection返回`OK`(已入HIGH)或`SEND_READY`(已准备APDU)时,为本次提交设置`process_pending`。`WAIT`/`ERROR`不因提交失败新增本地唤醒,也不清除已有标志;后续就绪I帧和确认记录仍照常交付。接收恢复、发送重试、预算续跑、事件消费及协议期限继续按各自条件安排推进。HIGH满且发送受k窗口限制时,失败提交不会反复把Poll等待强制归零;业务保留应答,在后续调度间隙重试。
当前接收轮次产生业务事件后,不再继续recv;已有TCP尾部在Feed及通知预算内处理,然后以Tick或Yield_Receive_Batch结束当前轮次。发送始终使用独立的32次预算,不能因8条通知预算耗尽而跳过;部分发送及不可写仍按原规则处理。旧回执转移安排在输入处理后,避免先交付旧回执而延误已到达的ACK。应用先取出并处理事件,再执行下一轮Process/Poll,使应答有机会在下一次读取EOF之前发送或入HIGH。EOF后停止接收,继续有界排空当前可发送内容,不因接收尾部保留而反复立即调度。
同一轮收到的socket就绪与定时事件应合成一个ready批次。Process先处理已有输入,再准备下一次发送;本地/定时唤醒也允许非阻塞查看已到达的输入。每轮接收以Connection的`Begin_Receive_Batch`开始。仅因Feed/通知预算或交付让出而停止时,TcpServer保留用户态尾部,并通过`Net_Pending_Input`只查询一次当时内核中的剩余字节数,形成有限输入快照。还有快照输入时调用`Yield_Receive_Batch`结束这一轮,暂缓I帧ACK及TESTFR_CON判定,Prepare返回`need_process=1/timeout_ms=0`要求立即续收;不能阻塞等待。后续recv长度不超过快照的剩余字节数,不把新到达的流量加入旧快照。
快照处理完后立即用`Tick`按原起点检查确认期限,持续的新I/S流量不能无限延长旧t1。真正的通知容量背压、无法解除的WAIT、recv无进展或EOF仍用Tick结束批次;该机制不承诺隔离业务长期积压。本地发送和STOP总期限、格式及序号错误始终有效。它不读取内核到达时间戳,也不重置任何计时起点。2026-10-20的调度测试覆盖7/8/9/33个前置I帧、任意recv分段、满长突发、固定快照外的新流量、TESTFR确认、连续接收下发送、部分发送及真实缺失确认。
### 3.1 接入应用自己的事件循环
`IEC104_TcpWaitInfo_t`中的字段按以下规则使用:
| 字段 | 应用处理 |
|---|---|
| `sockets[0..socket_count)` | 当前完整关注集合,最多两项;新增/更新注册,移除旧集合中已不存在的项及已撤销的READ/WRITE位 |
| 每项`socket/token/events` | 保存数值句柄及注册身份,按READ/WRITE注册;返回就绪时回传原身份,只填写实际就绪位 |
| `service_token` | 本次Init身份,保存在定时器、本地唤醒和就绪批次中 |
| `timer_active/timeout_ms` | 协议检查与接入/发送重试共用的最近等待上限;活动时替换一次性定时器,0毫秒立即调度;非活动时取消;UINT32_MAX不是无限等待 |
| `need_process` | 安排一次本地推进,不能阻塞等待新socket事件 |
| `pending_event_count` | 先通过NextEvent处理业务事件,再重新Prepare |
| `receive_paused` | 已受理I帧因通知容量不足暂留内部;消费普通通知后安排Process交付并恢复接收 |
存在客户端时,查询的协议等待上限最多为UINT32_MAX毫秒;没有更早期限仍可在该上限唤醒检查。接入或发送暂停尚未到期时,把各自剩余时间与协议等待上限取较小值。接入暂停即使没有客户端,也返回timer_active=1及接入重试剩余时间。没有客户端且没有尚未到期的接入暂停时,timer_active=0;到期后能否接入由容量和need_process表达。timeout_ms从本次查询时刻开始计算,处理其他任务较久后必须重新Prepare,不能用旧相对时间推迟期限。每次Process、Enqueue_Event、Send_Response、Close_Connection或事件消费改变状态后,下一次等待前都要重新查询。
接入暂停时,监听socket会暂时从Prepare的完整集合中消失,应用必须撤销旧注册;否则一直可读的监听socket仍可能使水平触发的外部等待空转。暂停结束后按最新快照恢复注册,监听socket和service_token的生命周期没有改变。发送暂停时同样须撤销客户端WRITE位,并保留仍需处理的READ位;客户端不再有任何关注位时将其移出集合。`ready.timer_expired`只表示定时唤醒,实际协议及接入/发送期限仍按当前单调时钟判断,提前或迟到的通知不会提前解除暂停。
应用循环的调用关系如下,其中“统一等待”由应用实现,可同时等待TCP、其他模块和业务定时器:
```text
NextEvent处理业务事件(可调用Send_Response或Enqueue_Event)
-> Prepare取得最新info
-> 更新完整socket注册集合和一次性定时器
-> need_process时安排本地唤醒,否则统一等待全部模块的事件
-> 合并本轮TCP socket就绪与定时通知,构造ready
-> Process(server, &ready, &error)
-> 处理其他模块任务,回到循环开头
```
本地唤醒时把ready清零、复制最近info.service_token即可;定时器触发再置timer_expired=1。socket通知每项复制注册时的socket/token,把events替换成本轮实际就绪位。同一socket只放一项,可读和可写合并;ERROR可表示平台报告的错误/挂断提示,Process通过实际I/O判定结果,不能把它直接当作某种协议故障。不要只把info原样复制成ready并把关注位当成实际就绪。即使需要立即推进,也应收集已经可用的socket通知,与定时提示一起交付。
socket只是借给事件循环注册,所有权仍属于TcpServer;应用不能对它recv/send/close或修改非阻塞属性。Windows事件循环适配代码可把`IEC104_TcpSocket_t`转换成SOCKET用于select等就绪机制,业务ASDU处理无需看到系统类型。TCP内部的Net后端仍负责所有真实I/O。此接口面向就绪驱动,不能直接把重叠I/O完成通知当作就绪位,或让异步I/O继续借用库内缓冲。
注册身份与业务generation分开:监听和客户端每次创建都获得新的非零token;计数跨Close/Init及Destroy/Create保留,不回绕。Process先检查service_token,旧服务整批忽略;当前服务中的旧socket/token逐项忽略,全部是旧socket通知且没有定时事件时不推进当前连接。不能为了处理旧通知而替换成最新token。旧定时器若属于同一次Init,最多触发一次按当前状态计算的提前检查,不能沿用旧时间差判定新连接超时。
Process/Send_Response/Close_Connection/Close可能关闭socket,应用须撤销旧注册;准备销毁服务时还须取消定时器和本地任务,避免继续使用已Destroy的句柄。边沿触发应保留need_process续跑;单次通知模式须在每轮按新快照重新布防。工作线程的结果由应用自己的唤醒机制送回事件循环线程,再调用Send_Response或Enqueue_Event并刷新等待条件。库不创建跨线程唤醒或注册回调。
接收路径:
```text
Net_Recv -> 固定接收缓冲 -> Connection_Feed(consumed_length)
-> I帧复制为事件,成功入队后Release;业务通过NextEvent取副本
-> S/U由Connection受理并释放
```
TCP层不解析APCI,也不假定一次recv对应一帧。Feed按实际消费量推进位置,剩余尾部跨Process/Poll保存;半帧由Receiver保留。WAIT不丢弃原文,发送和Tick继续驱动。即使旧S/U本轮刚释放、Feed消费0字节,也在预算内继续尝试尾部,不无限重试。
发送路径:
```text
Connection_Prepare_Next_Send == SEND_READY
-> Net_Send(pending_data, remaining_bytes)
-> Connection_Handle_Send_Progress(frame_id, 本次成功写入量)
```
每次Net_Send返回OK后立即回报实际增量,再处理后续接收事件;可读和可写同时就绪时,先处理已有接收字节,因为之前的同步send已全部记账,无须先发送新的帧才能处理旧帧的ACK。部分写入后重新Prepare取得同一APDU的剩余字节,不交错发送。`IEC104_NET_WOULD_BLOCK`保留当前帧,清零无进展重试计数并等待可写通知;`IEC104_NET_RETRY`及OK但写入0字节按下述有界策略重试,不提前Complete、不重置期限。最终发送失败调用Handle_Send_Failure并关闭客户端。发送完成回报即使因后续协议推进返回FATAL,也不重报已登记的增量。
发送准备及进度回报仅因本地发送和STOP期限直接报告超时,I帧ACK与TESTFR_CON的t1故障在接收阶段判定。Write_Output仍在Net_Send返回后读取真实时刻,供写入记录、活动时间和新TESTFR计时使用。若发送期间跨过旧确认的t1,快速应答已有process_pending;常规发送结束也只读查询协议期限,到期时登记立即续跑。下一轮Process先按既有预算和固定输入快照处理ACK/CON,再由Tick按原始起点判断;Prepare报告零等待,不依赖新的可读边沿。没有有效确认时仍超时,真正事件容量背压仍执行原期限,不新增1秒或其他固定宽限时间。Submit_Response入口发现期限已到时仍只入HIGH,不能靠连续提交应答推迟检查。
暂缓确认超时判定期间,旧I帧ACK或TESTFR_CON的t1已经到期时,Connection_Prepare_Next_Send暂停为HIGH/NORMAL创建、编号新I帧。已准备的当前I帧(含尚未写出字节的帧)及S/U仍按原顺序发送,以释放控制槽并继续受理接收报文;已有帧排空后返回WAIT,使接收解阻用的Flush_Output及时退出,不能顺带发出新的业务帧。有效确认解除所有已到期等待后自动恢复准入,未解除则由接收阶段按原期限断开。准入限制同时覆盖接收批次内、预算续收期间和常规发送,不改变未确认NORMAL在断线后的保留规则。
发送无进展采用跨轮预算:每次实际Net_Send返回OK+0或RETRY,累计一次`send_retry_count`,两种结果混合返回也共同计数。前3次安排本地续跑;第4次清零计数并保存`send_retry_at_ms = Now_Ms() + 10`,暂停发送。计数由Process普通发送、接收WAIT解阻发送和Send_Response快速入口共用,不能靠进入下一轮或换入口重置。写入正数字节、返回WOULD_BLOCK或已无待发帧时清零;断开及新连接初始化同时清除重试期限和计数。10毫秒为内部固定重试间隔,不新增业务配置,不改变正常有进展的发送路径。
发送暂停统一按`now_ms < send_retry_at_ms`判断。Prepare在同一次时钟快照中撤销客户端WRITE关注,并将剩余暂停时间与协议及接入期限取最小值;READ仍按接收背压规则保留。所有实际send都经过Write_Output的期限检查,因此接收就绪、NextEvent、业务提交、迟到的WRITE或提前的定时提示均不能提前写入,也不能延后原重试时刻。暂停不新增发送续跑,但保留其他工作已经登记的process_pending;这些工作可以推进一次,实际发送仍受期限限制。
等于或超过重试期限时,Prepare通过need_process主动安排发送,不依赖新的可写边沿。Write_Output在检查启停通知槽之前消费已到期的重试期限;若此时通知槽不足,转为等待NextEvent,不能保留已过期的期限反复零等待。已无待发帧也清除重试状态。重试后再次连续4次无进展,重新暂停10毫秒;Process和快速入口本身均不睡眠,等待由Poll或应用事件循环完成。
暂停期间接收及Tick继续运行,本地发送超时仍从原queued_at_ms计算,部分发送、零字节、RETRY及调度暂停都不续期。更早的发送或协议期限优先唤醒并按原规则收尾。因此修复消除持续无进展造成的忙等,不取消长时间无法完成发送时的超时断开;EOF后的待发内容也遵循相同规则。
Connection的Feed、Prepare_Next_Send、发送回报及Tick均可能推进保留帧;模块统一检查就绪I帧并复制事件,已确认记录也在复制事件后才释放。TcpServer_Prepare只查询等待条件,与内部Prepare_Next_Send不同。所有`now_ms`来自`Net_Now_Ms`,Windows后端内部使用`GetTickCount64`;日历时间/校时不影响协议期限。调用方长时间不调用Process/Poll或业务主循环阻塞,都会延迟收发和定时处理。
## 4. 业务接入
应用每轮Process/Poll后主动取事件,TCP不调用业务函数:
| 事件类型(均带`IEC104_TCP_EVENT_`前缀) | 有效载荷与契约 |
|---|---|
| `CONNECTED` | `data.connection`包含TCP建连状态及`peer`地址副本,不表示104已STARTED |
| `ACTIVATED` | `data.connection`保存STARTDT_ACT成功受理并激活后的快照;CON可能尚未写出,data_transfer_started=1 |
| `DEACTIVATED` | `data.connection`保存STOPDT_ACT成功受理并停用后的快照;ACK或CON可能仍待完成,data_transfer_started=0,TCP保持 |
| `I_FRAME` | `data.i_frame`保存完整APDU数组、长度、N(S)/N(R)及receive_serial |
| `RECORD` | `data.record.value`为发送记录;closing=0表示正常ACK回执,closing=1表示断线时剩余记录 |
| `DISCONNECTED` | `data.connection`为收尾前状态及原主站`peer`;status_valid仅控制status有效性,socket_error为后端诊断码 |
所有事件都有`generation`,同一代次以CONNECTED开始,以DISCONNECTED结束。连接建立、激活、停用和断开均使用`data.connection`,携带原主站的地址副本;启停事件的socket_error固定为0,不沿用此前网络操作的历史诊断。closing=1不等于发送失败或成功,仍须检查`local_send_completed`和`peer_acknowledged`。TCP有序关闭不新增协议fault,收到链路ACK也不表示对端业务已执行。
ACTIVATED/DEACTIVATED反映ACT受理时的实际状态变化;初始STOPPED、同状态重复ACT、CON发送完成及单纯断开不额外产生启停事件。每次变化立即入FIFO,保留激活、停用、再激活的顺序。后续发送失败不会撤销此前事件,DISCONNECTED最后交付。通知容量不足时延后ACT受理,不会先改变状态再丢弃通知。
STOP受理即产生停用变化并关闭新I帧接收,非法I帧不交付业务。后到START成功受理重新激活并开放I帧;已有STOP_CON继续按FIFO发送,旧完成不改变当前状态。取消前按序收到的非法I帧仍断链。
首次START_CON未完成时也能受理STOP:状态事件在受理时依次为ACTIVATED、DEACTIVATED,不等CON写完。随后再受理START产生新的ACTIVATED;已经生成的CON不撤回,尚未生成的旧STOP_CON可因重新激活而不再生成。默认4个启停通知槽,满时延后受理改变状态的ACT。
业务事件是历史事实副本,不保证当前仍连接或处于同一状态。首次ACTIVATED在START受理时生成,先于该次激活后合法I_FRAME的交付;应答是否接手仍由提交时的实际状态判断。下面函数由应用在读取NextEvent后自行调用,不向TcpServer注册:
```c
#include "IEC104_TcpServer.h"
#include <inttypes.h>
#include <stdio.h>
void App_Log_Connection_Event(const IEC104_TcpEvent_t *event)
{
const char *name;
switch (event->type) {
case IEC104_TCP_EVENT_CONNECTED: name = "connected"; break;
case IEC104_TCP_EVENT_ACTIVATED: name = "activated"; break;
case IEC104_TCP_EVENT_DEACTIVATED: name = "deactivated"; break;
case IEC104_TCP_EVENT_DISCONNECTED: name = "disconnected"; break;
default: return; /* I_FRAME和RECORD由原有业务分派处理。 */
}
printf("%s: generation=%" PRIu64 ", %s:%u\n", name, event->generation,
event->data.connection.peer.address, (unsigned)event->data.connection.peer.port);
}
```
处理I_FRAME时,ASDU首地址为`event.data.i_frame.apdu + IEC104_APCI_LEN`,长度为`event.data.i_frame.apdu_length - IEC104_APCI_LEN`。接收入口已保证6字节公共头完整,并对已知固定长度类型按VSQ数量和SQ位检查最低载荷长度;不足时按FRAME故障断连,不处理该帧的序号或ACK,也不上报I_FRAME事件。该检查不要求长度恰好相等,未知/变长类型、零对象和具体字段是否合法仍由业务检查,范围见[基础设计的ASDU长度检查](IEC104_设计架构.md#43-已知类型的最低载荷长度)。需要取得公共头字段时,显式包含 `IEC60870_Codec.h` 并调用 `IEC60870_Parse_ASDU_Header`。提交业务应答时保留事件中的generation:
```c
IEC104_TcpSubmitResult_t result = IEC104_TcpServer_Send_Response(
server, event.generation,
response_asdu, response_length);
```
返回类型为`IEC104_TcpSubmitResult_t`,结果须显式判断:
| 结果(均带`IEC104_TCP_SUBMIT_`前缀) | 含义 |
|---|---|
| `OK` | 已复制到发送缓冲或HIGH,可以复用输入原文;可能已写完、仅写出部分、暂不可写或仍在排队,不承诺对端已ACK;尚在HIGH中的应答会在受理新STOP时作废 |
| `WAIT` | HIGH当前容量不足,本次未接手,业务保留原文后重试 |
| `ERROR` | 参数、阶段、连接身份或总容量不符;无连接、旧代次、EOF后、未启动且无启动CON待发、停止排空及停止CON待发均拒绝应答 |
| `FATAL` | 服务队列异常,或会话失败并收尾;可能已经写出部分或整帧,不能直接重放,须处理RECORD/DISCONNECTED |
应答仅在STARTED/IDLE接手,其余状态在记录、窗口和HIGH容量判断前返回SUBMIT_ERROR,不入HIGH也不因此断链。普通路径先Drive再判断实际状态;期限已到时直接Enqueue(HIGH)仍使用同一准入函数,不能用此前Get_Status快照代替。
准入后,发送缓冲为空、窗口和记录有余量、HIGH无旧应答且公平配额允许时,直接准备I帧并尝试一次非阻塞send。已有CON或其他帧时暂存HIGH,不能越过FIFO;发送暂停及原期限保持有效。START受理即可接手,不再等待CON完整写出才激活。
一旦APDU已准备,部分写入或暂不可写都保留该帧,后续从剩余字节继续;不会把原文再次放入HIGH。快速路径和队列路径使用同一N(S)、完成回报和ACK处理逻辑,均不把旧连接应答交给新连接。OK后不可重复提交同一应答。调用后重新Prepare刷新socket、定时及本地续跑条件;发送失败可能已关闭当前socket。
SUBMIT_OK表示已接手,不保证HIGH积压最终发出;STOP受理时清理待发HIGH并产生停用事件,没有逐条作废回执。已有当前帧和记录继续完成,NORMAL保持原生命周期。从停用状态重新START会清理HIGH和配额;已启用时重复START保留HIGH,这是此前确定的项目队列策略。
NextEvent返回后副本归业务所有,业务处理失败时自行保存副本,事件FIFO不会自动重投。主站请求通过I_FRAME事件交给业务解析;HIGH缓存的是业务生成的应答ASDU。
主动上报使用服务级NORMAL,不需要先取得连接代次:
```c
IEC104_TcpSubmitResult_t result = IEC104_TcpServer_Enqueue_Event(
server, event_asdu, event_length);
```
Enqueue_Event只复制到服务级低优先级事件队列(内部NORMAL),不立即发送;OK表示已入队,WAIT表示本次未接手,ERROR表示输入/阶段非法,FATAL表示队列异常。Init成功后、首次连接前和断线期间均可缓存;尚未STARTDT也可入队。重连后按现存原文顺序重新构帧,STARTDT完成后才发送。NORMAL满时覆盖最旧原文,包括未ACK原文;未ACK数据重发可能重复,不提供掉电持久化。EOF后全部尚未编号的NORMAL及后来新增的NORMAL均留给下一连接,已经编号的当前帧继续完成。离线入队不设置need_process,避免无连接空转;有连接且未EOF时成功入队会要求本地推进。事件提交和应答发送都须在所属事件循环线程串行调用。
接收和发送接入同一个服务对象。以下示例使用Poll;应用事件循环使用第3.1节的Prepare/Process流程,初始化、事件读取、入队和收尾相同。下例中的`App_Handle_Event`由业务实现,按上述事件类型分派;它由应用主循环直接调用,可以调用Send_Response提交应答。传入的event在下一次NextEvent覆盖前有效,需稍后处理时按值复制整个事件:
```c
#include "IEC104_TcpServer.h"
#include <stddef.h>
static IEC104_TcpServer_t *server; /* 只保存句柄,实际对象由模块静态持有。 */
static IEC104_TcpServerStatus_t last_status; /* 应用留存端口和会话快照。 */
static IEC104_TcpError_t last_error; /* 本次服务操作的结果、失败阶段及网络诊断。 */
/* 应用自己的函数,不向TcpServer注册。 */
void App_Handle_Event(IEC104_TcpServer_t *server, const IEC104_TcpEvent_t *event);
static int App_Read_Events(void)
{
IEC104_TcpEvent_t event;
IEC104_TcpEventResult_t result;
while ((result = IEC104_TcpServer_NextEvent(server, &event)) == IEC104_TCP_EVENT_READY) {
App_Handle_Event(server, &event);
}
return result == IEC104_TCP_EVENT_EMPTY;
}
int App_Tcp_Start(void)
{
IEC104_TcpServerConfig_t config;
if (server != NULL) {
return 0; /* 先结束上一轮,不能覆盖仍持有的句柄。 */
}
server = IEC104_TcpServer_Create();
if (server == NULL) {
return 0;
}
IEC104_TcpServer_Config_Default(&config);
if (IEC104_TcpServer_Init(server, &config, &last_error) != IEC104_TCP_OK) {
/* 后续查询和清理传NULL,保留last_error中的初始化失败原因。 */
(void)IEC104_TcpServer_Get_Status(server, &last_status, NULL);
(void)IEC104_TcpServer_Destroy(server, NULL); /* 新建句柄初始化失败,没有资源或事件。 */
server = NULL;
return 0;
}
(void)IEC104_TcpServer_Get_Status(server, &last_status, NULL);
return 1;
}
/* 初始化成功后持续调用;共享业务主循环传0,专门的TCP循环可传50等正数。 */
int App_Tcp_Step(uint32_t max_wait_ms)
{
IEC104_TcpResult_t result = IEC104_TcpServer_Poll(server, max_wait_ms, &last_error);
if (result != IEC104_TCP_OK) {
(void)IEC104_TcpServer_Get_Status(server, &last_status, NULL);
(void)IEC104_TcpServer_Close(server, NULL);
}
if (!App_Read_Events()) {
return 0;
}
return result == IEC104_TCP_OK; /* 应用自己的0/1约定,不把TCP枚举直接当作布尔值。 */
}
int App_Tcp_Stop(void)
{
if (server == NULL) {
return 1;
}
if (IEC104_TcpServer_Close(server, &last_error) != IEC104_TCP_OK || !App_Read_Events()) {
return 0;
}
/* Close后先交付全部收尾事件,随后归还静态槽。 */
if (IEC104_TcpServer_Destroy(server, &last_error) != IEC104_TCP_OK) {
return 0;
}
server = NULL;
return 1;
}
```
App_Tcp_Start返回0时处理初始化失败;新建句柄上的Init失败会在示例中保存last_error并归还槽。生命周期错误返回明确分类,error.socket_error保持0;Create仍以NULL表示无可用槽,不产生网络诊断。示例中的App函数沿用应用自己的0/1约定,底层统一结果接口则必须比较IEC104_TCP_OK。成功后持续调用App_Tcp_Step(0)或App_Tcp_Step(50),按本轮业务调度决定是否等待;0模式下仍须由主循环持续调度Poll,协议期限不会由后台线程自动推进。Step失败时先记录last_error,随后调用App_Tcp_Stop,因为Stop自身的诊断会覆盖该变量。正常退出也调用Stop,保证归还句柄。实际端口和会话状态使用Get_Status输出,本次失败使用error,不能访问对象成员。事件与应答入口只接受ASDU原文(6..249字节),不包含APCI;不用手动构造I帧或调用socket的send。请求应答通过Send_Response在处理I_FRAME事件时提交,条件允许就立即尝试发送;主动上报通过Enqueue_Event进入服务队列。连接建立时尚未STARTED,实际业务发送受启停和k窗口约束。START尚未受理时,Send_Response返回ERROR,不能在CONNECTED事件中预存应答等待启动。START成功受理即开放准入,已有CON时应答入HIGH;已启用时重复START保留HIGH。
业务异步处理时可以复制整个事件交给工作线程,完成后由应用唤醒所属事件循环线程,再用保存的generation调用Send_Response并刷新Prepare结果;主动上报调用Enqueue_Event。不能从工作线程并发调用NextEvent、Enqueue_Event或Send_Response,也不能改用新连接的generation提交旧请求的应答。处理WAIT时自行保存应答并在后续调度间隙重试;对ERROR或FATAL应停止该次提交并按业务策略处理,不要无限重试。
### 4.1 对端查询与单连接关闭
对端地址查询与关闭连接是独立操作,Slave门面调用本节的TCP接口,不向业务交付Connection对象。查询使用接入时缓存的数值副本;关闭使用当前连接代次及统一结果分类。
`IEC104_TcpServer_Get_Remote_Address(server, &peer, error)`仅在OK时写入`IEC104_TcpPeerInfo_t`:
| 字段 | 含义 |
|---|---|
| `address[16]` | NUL结尾数字IPv4字符串,不含端口;当前未提供IPv6后端 |
| `port` | 主站TCP源端口,主机字节序,与子站监听端口bound_port不同 |
| `generation` | 当前主站非零代次,后续主动断开须携带原值 |
查询不执行I/O、不推进协议、不改服务状态;未Init或已Close返回INVALID_STATE,已Init但无客户端返回NOT_CONNECTED,其余参数/句柄/重入按统一结果处理,失败保持peer原值。EOF发送收尾期间仍持有客户端,查询可成功。CONNECTED、ACTIVATED、DEACTIVATED和DISCONNECTED均在`event.data.connection.peer`保留同一结构的副本,peer.generation等于外层event.generation;该地址不受协议status_valid影响。处理历史事件时直接使用副本,不查询当前新主站地址来标记旧连接。
主动断开当前主站的调用示例:
```c
#include "IEC104_TcpServer.h"
IEC104_TcpResult_t App_Close_Current_Connection(
IEC104_TcpServer_t *server, IEC104_TcpError_t *error)
{
IEC104_TcpPeerInfo_t peer;
IEC104_TcpResult_t result = IEC104_TcpServer_Get_Remote_Address(server, &peer, error);
if (result != IEC104_TCP_OK) {
return result;
}
/* 可先记录peer.address、peer.port及peer.generation,再结束此次查询对应的会话。 */
return IEC104_TcpServer_Close_Connection(server, peer.generation, error);
}
```
若是针对某个历史请求、事件或异步任务执行断开,直接传该任务保存的generation,不能重新查询最新代次来替换目标。generation为0返回INVALID_ARGUMENT;服务未Init返回INVALID_STATE;无客户端返回NOT_CONNECTED;当前存在其他代次返回STALE_GENERATION,阶段为GENERATION。这些拒绝分支都不关闭任何连接或追加收尾事件。两接口均须回到所属事件循环线程串行调用。
Close_Connection直接复用Drop_Client:关闭客户端,将剩余已受理I帧、发送记录及DISCONNECTED复制到事件FIFO,清理HIGH,恢复现存NORMAL的离线待发状态。它不发送STOPDT、不等待应答排空或ACK,不新增协议故障,不补造发送成功。重复断开已结束的连接只返回NOT_CONNECTED,不重复发事件;如果新主站已经接入,则旧代次返回STALE_GENERATION。若队列解除绑定失败,返回INTERNAL_ERROR/QUEUE_STATE,客户端已结束、监听仍在,应用须Close服务处理异常。
成功后监听地址、端口和service_token不变,后续Process按接入暂停与通知容量规则继续接入新主站;先消费事件并重新Prepare,移除旧客户端注册、更新定时和本地续跑条件。Close_Connection不限制主站再次连接。现存NORMAL可继续离线新增,重连并完成STARTDT后发送;未ACK原文恢复可能重复。Close仍负责关闭整个服务并清空所有ASDU缓存。
## 5. 启停、断线和重连
TCP连接后初始STOPPED,仅交付CONNECTED。START受理即激活并交付ACTIVATED,STOP受理即停用并交付DEACTIVATED;CON随后按FIFO完整写出,完成不再提交启停状态。后到START可结束尚未生成CON的停止等待;已生成的STOP_CON仍发送但不能覆盖新激活状态。停用不关闭TCP,可继续控制通信和重新启动。
EOF表示对端关闭发送方向:先调用AsduQueue_Limit_Normal_Send停止选取全部尚未编号的NORMAL,再记录`rx_eof`并结束当前接收批次。EOF前已入队及EOF后新增的未编号NORMAL都留给下一连接,避免在已无法取得新ACK的旧连接上继续新增上报帧。后续Process/Poll不再recv或关注该客户端的可读事件,继续通过发送调度处理当前已编号帧、此前已受理的HIGH及控制帧。已有当前I帧即使尚未写出字节也继续按原身份完成,不撤回N(S)。部分写入和`WSAEWOULDBLOCK`保留当前帧并关注可写;连续OK+0/RETRY仍执行发送暂停与定时恢复,发送预算耗尽安排本地续跑,均沿用原本的发送期限;不会仅因收到EOF而丢弃待发应答。Tick和其他协议期限仍持续推进,发送失败或超时按原故障路径收尾。
内部发送准备返回WAIT后,先按原有顺序和通知预算转移已确认记录;只要实际回收了记录槽,就重新准备发送,不用回收前的WAIT结果决定关闭。重试以实际回收为前提,受本轮通知预算约束。若仍WAIT,通过Connection.Get_Record_Wait确认是否仅受本地可回收记录容量限制:记录槽已满且队首已完整写出、已ACK,同时启停/k窗口允许新I帧、确认t1未到期。满足时保留连接及已接手的HIGH;只是本轮交付预算不足则安排下一轮Process,通知空间不足则由NextEvent腾出空间后唤醒。等待记录空间时不关注可写,也不持续为满FIFO安排本地续跑,原有协议及发送期限照常检查。
EMPTY表示当前帧、HIGH及控制发送均已排空,即使仍有未编号NORMAL也结束连接;其他WAIT表示仍需新的ACK/STARTDT等对端输入或已经受确认期限限制,也结束连接。不能仅因存在旧的已确认记录,就掩盖同时存在的窗口已满或停用状态。EOF后的等待只用于回收已经获得确认的记录,不等待新的对端报文,也不等待异步业务追加应答。业务应在取得I_FRAME事件后、下一次Process/Poll前入队,或在正常双向连接期间完成异步应答。协议错误、发送失败或期限到达仍可立即收尾,历史I帧事件不保证连接此刻仍可发送。
有序关闭本身不新增连接故障;接收错误调用Handle_Transport_Closed,发送错误先调用Handle_Send_Failure。协议FATAL保持原故障原因。收尾时结束实际I/O、交付剩余记录及关闭通知,最后Connection_Close并清空TCP接收尾部、EOF标记及发送重试期限/计数。监听socket继续等待新连接,新连接正常接收。正常TCP关闭不等于业务全部成功,未完成或未ACK的I帧仍通过`closing=1`的记录交付,不补造发送成功或对端确认;现存NORMAL在解除连接绑定后继续保留,等待新连接启动。
收尾按偏移只读复制内部发送记录,仅在RECORD成功入队后累计`records_saved`。生成DISCONNECTED时,查询关闭协议前的状态,并从内部记录总数中扣除`records_saved`,得到尚未转为事件的记录数;实际记录仍由最后的Connection_Close统一清理。读取记录返回EMPTY表示已读完,ERROR或事件入队失败则走现有收尾异常处理,快照仅扣除成功转移的部分,不能无条件清零。状态查询失败或成功转移数大于内部总数时,断开快照标记`status_valid=0`;计数不一致也按收尾异常处理。RECORD和DISCONNECTED的先后顺序及各记录中的发送、ACK事实保持不变。
每次接受主站都分配递增的非零generation,新连接重新进行STARTDT。代次到UINT64_MAX后拒绝继续分配,Poll返回GENERATION_EXHAUSTED且阶段为GENERATION,不回绕复用;该静态槽归还后也不再Create分配。同一槽Close/Init或Destroy/Create均沿用代次计数。句柄仅在一次Create到Destroy之间有效;业务在Destroy前须结束引用它的旧任务,也不能把旧事件交给另一个服务器句柄。
队列生命周期固定属于服务:Init先以内部代次0建立离线队列;客户端关闭时,在Connection_Close之后调用AsduQueue_Detach,立即清空HIGH、恢复现存NORMAL待发状态并回到代次0;断线期间新增NORMAL继续追加到同一字节环。新连接使用AsduQueue_Rebind绑定新的非零代次,再初始化Connection。绑定转换失败报告服务级INTERNAL_ERROR,不能重新Init队列而静默丢失缓存。主动Close整个服务才清空队列,下一次Init从空队列开始。
不再提供重连保留开关或旧通用入队接口。NORMAL用于面向同一业务接收方/冗余域的主动事件,业务须允许未ACK原文重发;模块不认证主站身份、不管理冗余组,也不实现应用去重。
## 6. 平台边界与接入约定
`Net.h`只包含标准C类型,不依赖Connection、Link或业务结构。后端不维护104状态,也不处理业务事件队列、记录消费、接收预算或NORMAL重连保留;这些职责留在TcpServer。后端使用以下统一结果:
| 结果 | TcpServer处理 |
|---|---|
| `OK` | 收发按实际字节数推进;Wait的就绪位为0表示未报告socket就绪;发送0字节不当作EOF,与send的RETRY合计连续4次后暂停发送10毫秒 |
| `WOULD_BLOCK` | 本次暂不可接受连接/读写,保留状态后续重试 |
| `RETRY` | accept在4次预算内重试,整轮均为RETRY时暂停100毫秒;send与OK+0合计连续4次后暂停发送10毫秒;recv结束本次尝试并交还调度;等待中断后仍推进接收和Tick |
| `RETRY_LATER` | 仅Accept使用:资源暂缺,立即暂停接入100毫秒后重试,保留监听和活动客户端 |
| `EOF` | 仅接收方向返回,停止继续接收,按现有规则排空发送后关闭客户端 |
| `ERROR` | 接收/发送错误收尾当前客户端;监听或等待错误返回服务级失败 |
Windows后端在内部把`WSAEWOULDBLOCK`转换为WOULD_BLOCK,把`WSAEINTR`以及accept的`WSAECONNRESET`/`WSAECONNABORTED`转换为RETRY。接入路径的`WSAENOBUFS`/`WSAEMFILE`转换为RETRY_LATER;原生错误码另外保留用于诊断。该分类不改变recv/send或监听初始化的错误处理,新的枚举值追加在EOF之后,已有值不变。Net_Accept(listener, &client, &peer, &native_error)成功时同时交付非阻塞且启用TCP_NODELAY的客户端和IEC104_NetPeer_t。Windows后端由accept取得sockaddr_in,先显式设置非阻塞及`TCP_NODELAY=1`,再用InetNtopA及ntohs转换IPv4和源端口;任一步骤失败先保存错误码并回收新句柄,再返回RETRY,若错误属于上述资源暂缺则返回RETRY_LATER。失败时client保持INVALID_SOCKET,peer保持原值。后端本身不循环重试或等待;监听句柄仍由调用方管理。Listen失败由后端回收其申请的句柄;TcpServer负责配对调用Net_Init/Net_Cleanup。
TCP_NODELAY设置在每个新接入的客户端socket上,关闭Nagle以减少S/U帧和短I帧的合并等待;重连时同样执行。该选项不改变104确认规则、发送顺序或非阻塞收发语义,send成功仍只表示字节交给本地TCP,不保证对端已经收到。
Net_Wait只接受通用事件位,系统的`fd_set`、`timeval`和等待参数差异留在平台文件中;只有Poll使用这个等待入口。应用自己的事件循环通过TcpServer_Prepare借出socket并传回就绪批次,Process仍经Net完成真实收发。所有I/O接口同步返回、无未完成I/O借用;后端须满足此契约,不能在返回后继续借用同一发送缓冲,也不能让业务绕过TcpServer驱动内部Connection。
2026-10-20增加内部接口`Net_Pending_Input(socket, &available, &error)`:只读取得当前内核接收队列字节数,不等待、不消费数据,失败返回ERROR。Windows使用`ioctlsocket(FIONREAD)`,未来其他平台或自定义Net后端必须补齐此接口。每份预算续收快照最多查询一次;快照期间不重新查询来扩大范围。该接口不属于业务公开API,零字节也不等于EOF,EOF仍由recv确认。真实Windows回环验证见`iec104_net_pending_input_test.c`。
Net_Wait允许interest为0,用于没有socket关注时的纯定时等待。Windows后端不把空集合传给select:wait_ms为0直接返回,正数调用一次有限Sleep,UINT32_MAX截为UINT32_MAX-1,避免被解释成INFINITE。Poll在进入该路径之前仍先合并max_wait_ms、协议/接入/发送重试期限、本地工作及未读事件;有事件背压时等待为0,立即交还业务。空集合等待返回后也照常计算timer_expired;Sleep可能提前结束,Process的实际接入及发送门控仍以当前单调时钟为准。Prepare/Process不执行睡眠,其他平台后端也须支持空集合的有限等待。
Slave是业务的统一子站入口,TcpServer是其内部的TCP服务。以下操作由门面代理,也可用于独立TCP接入。应用通过Create/Destroy管理句柄,使用Prepare/Process或Poll推进,并调用NextEvent/Enqueue_Event/Send_Response/Get_Remote_Address/Get_Status;Close_Connection只断开指定主站,Init/Close管理服务生命周期。Connection、SendManager及发送队列由TCP内部创建、驱动和回收。业务包含`IEC104_TcpServer.h`,其依赖的`IEC104_TcpTypes.h`和`IEC104_Types.h`提供公共值类型;Connection和SendManager头文件中的完整结构体定义用于内部静态存储,不属于业务接口。连接状态、发送记录和返回值在TCP实现中显式转换,业务不依赖内部结果或记录布局。
应用已有事件循环时通过Prepare/Process/NextEvent接入,socket注册的平台差异由应用的事件循环适配处理,库内真实I/O的平台差异由Net后端处理。Connection不反向调用Net,业务不直接驱动Connection或SendManager。ASDU内容可继续由业务使用Codec和公共时间工具解析、构造,这些无连接状态的工具不改变TCP接入边界。
新增后端时,实现`IEC104_Net.h`中的全部接口并替换链接文件;同时验证句柄无效值、非阻塞状态、部分写入、EOF、单调计时及失败资源回收。无需修改TcpServer或104协议模块。业务事件接口与平台选择无关。
## 7. Windows接口参考
- [Winsock服务端基本流程](https://learn.microsoft.com/en-us/windows/win32/winsock/complete-server-code)
- [非阻塞socket设置](https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-ioctlsocket)
- [select就绪等待](https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-select)
- [accept返回值与错误码](https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-accept)
- [Sleep与无限等待哨兵](https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleep)
- [send的部分写入与返回值](https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-send)
- [recv的EOF及暂不可读](https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-recv)
- [单调毫秒计时](https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-gettickcount64)
@@ -0,0 +1,728 @@
# IEC104 子站发送模块设计
同步日期:2026-10-20。本文对应 AsduQueue、SendBuffer、SendManager 当前实现,接口声明分别见 [AsduQueue.h](../../104/IEC104_AsduQueue.h)、[SendBuffer.h](../../104/IEC104_SendBuffer.h)、[SendManager.h](../../104/IEC104_SendManager.h)。
TCP接入边界:业务统一使用`IEC104_Slave.h`,Slave内部调用TcpServer;事件经`TcpServer_Enqueue_Event → AsduQueue_Enqueue`进入服务级NORMAL队列,应答经`TcpServer_Send_Response → Connection_Submit_Response → SendManager_Submit_Response`校验会话及启停准入后直接准备或缓存HIGH;协议期限已到时改走Manager.Enqueue(HIGH),使用同一准入检查。本文说明TCP内部发送模块的存储和调用契约,下文的模块调用方指Connection等内部实现。业务不直接包含或操作Manager、Buffer、Queue;应用主循环和事件用法见[TCP服务设计](IEC104_TcpServer_设计架构.md#4-业务接入)。
发送模块采用变长业务字节环、发送前编号、单当前I报文和独立确认记录。满时策略采用HIGH拒绝、NORMAL覆盖最旧原文,后者包含已提交未ACK条目且不提供逐条丢弃通知。保留静态内存、部分写入、本地发送期限、逐帧业务回执、代次隔离、公平配额及严格启停/TESTFR语义;不加入动态分配、线程或socket封装。
条目头按高低优先级职责拆分:HIGH仅有2字节长度,NORMAL保存编号、状态及长度,采用 `sizeof(IEC104_AsduNormalEntryHeader_t)` 作为头长,当前Windows 64位GCC下为16字节。两级队列及发送记录均移除user_tag;只有NORMAL分配条目编号,HIGH只消费当前队首。现有分段字节环、独立帧身份和帧回执继续使用,接口迁移见第6.2节。
- [整体数据生命周期](#overview)
- [SendBuffer:当前I帧与控制队列](#sendbuffer)
- [AsduQueue:变长业务队列](#asduqueue)
- [SendManager:准备、完成、ACK及回执](#sendmanager)
- [内部调度和恢复](#integration)
- [构建与接口迁移](#build)
<a id="overview"></a>
## 1. 整体数据生命周期
业务事件通过Enqueue_Event缓存NORMAL,入队时没有N(S),离线也可缓存。应答通过Send_Response提交:Connection核对会话及期限,Manager仅在STARTED+IDLE时接手,其他启停阶段返回ERROR且不入HIGH。准入后,在顺序、发送门控、窗口、记录和公平配额允许时直接准备I帧并分配N(S),暂不能准备时只复制到HIGH。TcpServer对直接应答最多尝试一次非阻塞send。
后续由Connection调用Manager.Prepare_Next_Send:续传已有当前帧;否则先取S/U控制队列;两者均空时,才在HIGH/NORMAL待发业务之间选择。队列发送和直接应答共用Prepare_I_Asdu完成预留、Link构帧和记录交接;实际socket写入统一由TcpServer经Net执行。
~~~mermaid
flowchart LR
B[业务] -->|Enqueue_Event / Send_Response / Poll / NextEvent| T[TcpServer]
T -->|内部连接调度| C[Connection]
T -->|实际网络收发| N[Net后端]
C -->|入队 / 准备 / 进度 / ACK| M[SendManager]
M -->|入队 / 选取 / 释放原文| Q[AsduQueue 两级字节环]
T -->|NORMAL服务缓存 / 绑定生命周期| Q
M -->|准备一帧并分配序号| L[Link]
M -->|报文存储与写入进度| S[SendBuffer 当前I帧 / 控制FIFO]
M -->|内部记录查询结果| C
C -->|内部帧和记录查询结果| T
T -->|NextEvent返回事件副本| B
~~~
| 阶段 | AsduQueue | SendBuffer | Manager记录 |
|---|---|---|---|
| 事件入队或应答暂存 | 保留未编号ASDU | 无变化 | 无记录 |
| 准备当前I帧 | HIGH原文出队;NORMAL标记已提交并保留原文 | 保存唯一当前I APDU | 建立帧身份、快照及NORMAL来源编号 |
| 直接准备应答 | 不入ASDU队列,不调用Mark_Submitted | 保存唯一当前I APDU | 建立HIGH记录,来源编号为0,纳入high_unacked_count |
| 部分写入/EAGAIN | HIGH没有队列原文,NORMAL可被后续入队覆盖 | 同帧原字节、原身份,续写剩余部分 | 未本地完成 |
| 整帧写完并提交 | HIGH没有队列原文,NORMAL可被后续入队覆盖 | I槽立即可复用 | 记录本地完成及实际写完时刻 |
| NORMAL空间不足时入队 | 覆盖最旧原文,记录已提交条目的待确认数量 | 已准备APDU保持原字节和进度 | 已编号记录保留,未编号条目无帧回执 |
| 对端有效累计ACK | HIGH不访问队列;NORMAL释放原文或核减覆盖待确认数 | 不访问旧I APDU | 标记已确认,等待TcpServer转为事件 |
| TcpServer复制RECORD事件并经Connection移除记录 | 无变化 | 无变化 | 释放记录槽 |
| 业务通过NextEvent读取事件副本 | 无变化 | 无变化 | 无变化;对应内部记录已移除 |
| 断线后Manager.Abort | 保留环内剩余原文;已覆盖原文不能恢复 | 清除发送状态 | 终止前须读取待回收记录 |
链路ACK表示对端协议接收确认,不表示遥控等应用操作已经执行成功。本地写完也不等于已ACK。
这些对象、字节数组、控制槽和记录数组均包含在TcpServer的私有存储中,Connection负责绑定和驱动发送模块;存储生命周期覆盖整个使用期。本模块不malloc、不释放借用内存、不内置锁。同一连接的入队、发送准备、写入结果、接收、查询和回收必须串行。Connection把事件交给Manager后,不得再调用底层Link、Buffer或Queue重复提交。业务只持有TcpServer句柄和公开值类型。
### 1.1 身份与时间
| 字段 | 用途 |
|---|---|
| connection_generation | 连接使用非零且不复用的代次;AsduQueue的0表示离线服务缓存,仅接受NORMAL,不能提交发送 |
| entry_serial | 仅NORMAL成功入队分配,1..UINT64_MAX循环,Rebind不重置;HIGH恒为0,无条目身份 |
| frame_serial | SendBuffer成功预留分配,取消后也不复用 |
| N(S)/N(R) | Link管理的15位协议序号,按模32768运算 |
| queued_at_ms | I帧准备或控制帧入队时刻,本地发送期限起点 |
| written_at_ms | 整帧实际写完时刻,I帧t1和主动TESTFR等待CON的起点 |
NORMAL的64位entry_serial在UINT64_MAX之后回到1,0对NORMAL是无效值;回绕不改变connection_generation。HIGH不分配条目编号,查询和发送记录的来源编号均为0,不能用0识别某条HIGH业务。活跃NORMAL编号按循环距离比较,不能按整数大小排序。SendBuffer的frame_serial仍采用耗尽时报错、不回绕的策略,两种编号独立。跨连接代次的全局不复用由调用方保障。is_initialized只表示对象初始化成功,不表示TCP存活。单调时刻允许0;部分写入、重复准备、重复ACK及WAIT均不续期。
<a id="sendbuffer"></a>
## 2. SendBuffer:当前I帧与控制队列
源码:[IEC104_SendBuffer.c](../../104/IEC104_SendBuffer.c)。
### 2.1 存储结构
SendBuffer结构内置一份255字节i_apdu和一份I帧快照;只允许一帧已准备但尚未提交本地完成的I报文。调用方另提供固定的IEC104_SendBufferControlFrame_t数组,容量1..65535,每槽仅保存6字节S/U APDU及元数据。
没有APDU节点池、空闲链、保留链、control_reserve_count,也不复制Link的三个序号边界。控制队列容量与k、ASDU字节容量、回执容量分别配置。S/U不占I帧k窗口或业务回执槽,控制队列自身仍可能满。
| 结构/字段组 | 含义 |
|---|---|
| FrameState | frame_serial、入队/写完时刻、长度、已写字节、encoded_nr、帧类型 |
| ControlFrame | FrameState + 6字节APDU |
| control_frames/capacity/head_index/count | 独立S/U环形FIFO;队尾由头和数量推导 |
| i_frame_state/i_frame_snapshot/i_apdu | 当前I帧元数据、不可变序号快照和字节 |
| has_i_frame | 当前I帧已提交,即使已写0字节也占用 |
| has_reservation/reservation_is_i_frame | 唯一预留是否存在及其类型 |
| generation/next_frame_serial | 本地身份及不复用编号 |
| Status | 代次、控制数量、当前I帧是否存在及其N(S)、预留标志、是否空闲 |
| HeadInfo | 当前身份、类型、快照、剩余字节借用指针、进度及时间 |
HeadInfo中的pending_data只借用当前存储,完整提交、Abort或Init后不能继续使用。发送异步进行时,同帧最多允许一个尚未回报的写操作。
<a id="sb-functions"></a>
### 2.2 初始化与验证
Init(buffer, control_frames, control_capacity, generation)只初始化控制结构,绑定调用方数组。Get_Status验证边界及要访问的端点并返回数值副本,不扫描整个控制队列。
Buffer_State_Valid检查索引、容量、0/1标志、身份及当前/控制端点元数据;Frame_State_Valid检查长度、快照、进度和完整写完时刻。Frame_Type_Matches结合Codec.Parse_APCI验证提交类型,子站仅接受I、S_ACK、STARTDT_CON、STOPDT_CON、TESTFR_ACT、TESTFR_CON。不能提交主站STARTDT_ACT或STOPDT_ACT。
<a id="sb-reserve"></a>
### 2.3 预留和提交
| 操作 | 成功及阻塞含义 |
|---|---|
| Reserve_I_Frame | 只有当前I帧和控制队列均空时成功;否则BUSY |
| Reserve_S_U_Frame | 预留控制队尾;控制队列满为FULL |
| 两种Reserve | 任一已有预留时BUSY;成功分配不复用frame_id |
| Commit_Frame | 校验身份、APCI、长度和I快照;I成为当前帧,S/U追加FIFO |
| Cancel_Reservation | 只归还尚未构帧成功的预留,已使用身份不复用 |
| Peek_Send_Head | 有当前I帧就返回它,否则返回控制队首;不选择业务 |
先预留存储,再调用会产生发送义务的Link构帧接口。Link.Prepare成功后N(S)或控制过程已提交,不能取消预留并继续旧连接;此后的提交异常必须结束连接。
控制帧优先于**尚未准备的业务**。已经准备的当前I帧,即使第一次写socket得到EAGAIN且尚未写出字节,也必须保持顺序先完成。此规则避免撤回已编号帧及已经冻结的N(R)快照。部分写出的任意APDU都不能被另一个APDU插入字节。
<a id="sb-completion"></a>
### 2.4 写入进度与释放
Record_Written_Bytes仅接受当前身份和实际写入增量。增量0不推进;未写完返回OK;恰好写完返回FRAME_WRITTEN并记录时刻;超出剩余长度、旧身份、重复完整提交返回ERROR。
Manager在整帧写完时连续执行:
1. 按保存的类型调用Link.Complete,I帧传回原始快照。
2. 登记I帧本地完成和实际写完时刻。
3. 调用Buffer.Commit_Send_Completion,释放当前I槽或控制队首。
只有第三步结束才可以复用报文字节。中间不得分派ACK事件,不得重入发送操作。该顺序由Manager内部维护,Connection按序提交发送结果和ACK;业务不调用SendBuffer完成接口。
<a id="sb-ack"></a>
### 2.5 ACK与超时的归属
SendBuffer不处理ACK,没有Apply_Peer_Ack。协议确认边界由Link独占;Manager记录ACK,核减HIGH未确认计数,并清理NORMAL仍保留的原文或已覆盖待确认计数。Buffer不再需要保留已发送APDU等ACK。
Head_Send_Expired从当前帧原queued_at_ms判断本地发送期限,返回1到期、0无帧或未到期、-1非法。控制帧等待前一帧期间也计入其原期限;进入队首后不会重新计时。I帧ACK的t1查询位于Manager。
Abort清除Buffer控制结构,不关闭TCP、不取消异步I/O;调用方先结束旧I/O再复用存储。
<a id="asduqueue"></a>
## 3. AsduQueue:变长业务队列
源码:[IEC104_AsduQueue.c](../../104/IEC104_AsduQueue.c)。
### 3.1 两级可跨尾部的字节环
HIGH和NORMAL各使用调用方提供的uint8_t数组,容量单位为**字节**。两级分别使用 `IEC104_ASDU_HIGH_ENTRY_HEADER_LEN` 和 `IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN` 计算容量:
~~~text
HIGH:ASDU长度 uint16(2) | ASDU(6..249)
NORMAL:entry_serial uint64 | entry_state:2位 | asdu_length:8位 | ABI填充 | ASDU(6..249)
~~~
HIGH头固定2字节;NORMAL头是 `IEC104_AsduNormalEntryHeader_t` 的当前ABI布局,Windows 64位GCC下为16字节,其他平台以sizeof为准。NORMAL的entry_state为1表示未提交、2表示已提交待ACK,0/3无效;头内状态必须与已提交前缀/未提交后缀一致。两个头都通过memcpy读写,不要求环内地址对齐,不能将字节环强转成结构体指针。头不是协议字段或跨版本持久化格式;ASDU不含APCI、不含N(S)。Enqueue验证长度和容器条件,具体业务对象合法性由生产者负责。两级均不保存user_tag。
HIGH最小容量为2+6=8字节,NORMAL最小容量为NORMAL头长+6(当前ABI下22字节),只保证容纳最短ASDU。Enqueue先检查 `本级头长 + asdu_length <= 目标桶总容量`;不满足时返回ERROR,空队列也不能接纳该条目。100字节环最多容纳98字节HIGH ASDU,或当前ABI下84字节NORMAL ASDU;最大249字节ASDU分别需要251字节及NORMAL头长+249(当前ABI下265字节)。这种错误须调整容量或业务组帧,等待ACK释放空间无法解决。
条目能放入目标桶总容量后,才检查当前总空闲空间。条目头和ASDU都可以跨数组尾部:先使用尾部剩余字节,再从下标0继续,不插入额外回绕填充、不搬动在途原文。`used_bytes` 恰好等于所有保留条目的 `本级头长 + 实际长度` 之和,NORMAL结构体本身的ABI填充已计入头长。参数、状态合法时,只要总空闲不少于本条大小就有足够字节容量;HIGH总空闲不足时返回FULL,NORMAL总空闲不足时按队首顺序覆盖旧条目直到足够。NORMAL还检查编号窗口,HIGH不受该窗口限制。队列清空时索引归零。
例如当前ABI下100字节NORMAL环中,`head=40、tail=80、used=40`,尾部有20字节、开头有40字节可用。新的50字节条目(16字节头+34字节ASDU)直接使用尾部20字节及开头30字节,成功后 `tail=30、used=90`。其中ASDU原文分为尾部4字节和开头30字节,队列通过两段只读视图交给调用方。
NORMAL内部保持已登记交接、保留原文待ACK的前缀;HIGH只保存队列待发条目:
~~~text
NORMAL:队首 → 保留原文待ACK的前缀 → unsubmitted_index指向的待发后缀 → 队尾
HIGH:队首(也是unsubmitted_index)→ 全部待发条目 → 队尾
~~~
unsubmitted_index直接指向队列中下一条尚未登记发送交接的条目,发送选择不扫描保留待ACK前缀。NORMAL在入队时已经分配entry_serial,HIGH没有条目编号;刚入队时两级都没有I帧N(S),编号由后续发送流程分配,不能仅凭队列待发状态判断是否已编号。unsubmitted_count为0时,unsubmitted_index等于tail_index。HIGH在对应I帧已交给SendBuffer且Manager已登记记录后,通过Mark_Submitted消费队列原文;NORMAL的Mark_Submitted将头内状态设为保留原文待ACK,并推进待发游标。NORMAL空间不足时删除最旧条目,既可删除待发条目,也可删除保留待ACK原文,一次入队可能删除多条。新条目成功入队仍返回OK,不增加丢弃通知或回调。
原文删除不会撤销已编号的I帧:当前APDU在SendBuffer中有独立副本,Manager记录继续保留,k窗口、本地发送期限、t1和逐帧回执仍按真实发送及ACK推进。已覆盖但未ACK的已提交数量单独记录,不再占字节环容量。未编号条目被覆盖时尚未生成I帧,因此没有对应帧回执。
Enqueue先检查新条目本身能否放入总容量,再在控制副本中检查需要删除的全部旧条目;NORMAL另检查下一编号窗口,最后才写新字节并发布状态。条目过大或覆盖途中发现非法头时返回ERROR,原队列、字节和编号不变。NORMAL的UINT64_MAX仍可分配,下一编号回到1。正常覆盖复杂度为O(本次删除条目数);当前ABI下NORMAL条目为22..265字节,一次最多删除13条。其他ABI的上界为向上取整 `(NORMAL头长+249)/(NORMAL头长+6)`。
<a id="aq-fields"></a>
### 3.2 控制字段
| 字段 | 含义 |
|---|---|
| bytes/byte_capacity | 借用的字节数组及容量 |
| head_index/tail_index | 最旧保留条目和下一写入位置 |
| used_bytes | 所有保留条目的本级头长+ASDU实际长度之和,无额外回绕填充 |
| entry_count | 原文仍保留的ASDU总条目数;HIGH只保留待发区,NORMAL包含保留待ACK前缀和待发后缀 |
| unsubmitted_index/unsubmitted_count | 队列中下一条尚未登记发送交接的ASDU位置及条目数,不是SendBuffer中的帧数 |
| next_entry_serial | 下一个NORMAL成功入队身份;UINT64_MAX之后回到1;HIGH不消耗编号 |
| connection_generation | 队列当前绑定;0表示离线服务缓存,非零值绑定当前连接 |
| normal_send_limited | EOF收尾开关;为1时停止选取全部未提交NORMAL,原文留给下一连接,Detach/Rebind清零并解除限制 |
| overwritten_unacked_serial_begin | NORMAL原文已覆盖、仍待ACK的循环区间起始边界,含端点;ACK后推进,计数为0时为0 |
| overwritten_unacked_serial_end | 同一循环区间的结束边界,含端点;对应最后一个已覆盖待确认身份,计数为0时为0 |
| overwritten_unacked_count | NORMAL原文已覆盖、已提交且待确认的ASDU条目数,Queue和Status均提供 |
| Status.entry_count/retained_unacked_count | 各优先级原文仍保留的ASDU总条目数/其中已提交且待确认的ASDU条目数 |
| Status.is_storage_empty | 两级原文环均为空;HIGH或NORMAL仍可能有对应I帧等待ACK |
| HeadInfo.entry_id/priority/is_submitted | 条目身份、优先级及队列记账状态;is_submitted为0表示尚未登记交接,为1表示NORMAL保留原文待ACK;HIGH的entry_serial及is_submitted恒为0 |
| HeadInfo.asdu_first/asdu_first_length | 原文第一段及长度,指针非NULL |
| HeadInfo.asdu_second/asdu_length | 第二段或NULL,以及两段总长度;第二段长度=总长度-第一段长度 |
这些计数的单位都是ASDU条目。每一级的 `retained_unacked_count = entry_count - unsubmitted_count` 只统计原文仍保留、已提交且待确认的ASDU条目。HIGH提交后已经出队,其entry_count等于unsubmitted_count,retained_unacked_count恒为0。队列状态只能给出NORMAL保留及覆盖待确认数;全部已编号未ACK的I帧数还须加上Manager.high_unacked_count。
“未提交/已提交”在AsduQueue中描述队列记账状态。`Mark_Submitted` 的含义是登记已经完成的发送交接,调用顺序为:
```text
AsduQueue保存待发ASDU
→ SendBuffer接手组装好的完整I帧
→ Manager建立发送记录
→ Mark_Submitted完成队列记账
HIGH:消费队列原文,SendBuffer报文及Manager记录继续有效
NORMAL:登记为保留原文待ACK,将待发游标移到下一条
```
在最后一步执行前,队列仍保留待发旧状态,即使SendBuffer已经接手报文。因此HIGH在Mark_Submitted中核对待发旧状态后出队,出队依据是前面的交接已成功;不能把状态值0理解为“尚未交给发送模块就丢弃”。主动取消待发业务使用Discard_Unsubmitted_Head,且必须发生在发送交接之前。上述交接操作必须串行完成,中途不得插入队列修改。
公开视图 `is_submitted` 的0/1表示尚未登记交接/NORMAL保留待ACK;NORMAL条目头 `entry_state` 的1/2是对应的内部编码,两者不是同一组数值。HIGH条目头没有状态字段,完成交接后直接出队,不会留下is_submitted为1的条目。
待确认表示对应I帧尚未收到有效ACK,包含尚未写出、部分写出及整帧写完后等待ACK的阶段。当前实现中,一个已提交ASDU条目对应一个I帧,因此Manager的HIGH未确认数加NORMAL保留及覆盖待确认数,在数值上等于已编号未ACK的I帧数。AsduQueue保存条目头和ASDU原文;完整I帧的存储归SendBuffer管理,整帧写完并提交本地完成后即可复用,Manager的发送记录继续等待ACK。
`is_storage_empty` 只表示两级ASDU原文环为空,已经出队的HIGH或原文已覆盖的NORMAL仍可能有对应I帧待确认。这里的retained仅描述原文仍在缓冲区。只有NORMAL有条目编号;同一级内部提交及ACK处理始终FIFO。
overwritten_unacked统一表示“原文已覆盖、仍待有效ACK”。begin/end是包含两端的循环区间边界,实际数量以count为准。例如NORMAL已覆盖待确认编号为10、12,而11此前在未提交时被主动丢弃,则begin=10、end=12、count=2;确认10后begin推进到11、count减为1,实际下一待确认身份仍是12。区间起始边界不一定是某条现存记录的编号,也不能用普通整数大小比较跨UINT64_MAX回绕的边界。
<a id="aq-functions"></a>
### 3.3 公开操作
| 接口 | 行为 |
|---|---|
| Init | 服务启动时绑定两个字节数组;代次0可离线缓存NORMAL,非零值绑定连接,不分配内存 |
| Enqueue | 固定HIGH用于当前连接应答、NORMAL用于可跨连接恢复的事件;超过桶总容量为ERROR;HIGH当前空间不足为FULL,NORMAL覆盖最旧原文;NORMAL编号窗口受阻也返回FULL;OK复制ASDU,仅NORMAL分配身份;不接收user_tag,失败不改队列、字节或编号 |
| Get_Status | 常数时间查询代次、各优先级ASDU原文保留条目数、其中已提交待确认条目数、NORMAL原文已覆盖待确认条目数及原文是否为空 |
| Peek_Head | 查看最旧保留条目,可能已提交 |
| Peek_Unsubmitted | 常数时间查看当前允许交接的待发条目;EOF限制期间NORMAL返回EMPTY,即使仍有EOF前后入队的原文 |
| Copy_Asdu | 把仍有效的最多两段原文复制到调用方连续缓冲;容量不足或描述非法时ERROR且不改输出 |
| Mark_Submitted | 仅非零绑定代次可调用;SendBuffer接手I帧且Manager建立记录后核对状态及身份,HIGH消费原文,NORMAL保留待ACK;EOF后NORMAL禁止新交接,不影响此前已提交的帧;不执行I/O |
| Confirm_Next_Normal_Entry | 仅接受NORMAL:有原文时核对已提交队首并释放,已覆盖时核减待确认数;HIGH返回ERROR,其ACK由Manager记录处理 |
| Discard_Unsubmitted_Head | 仅允许主动移除未提交队首,拒绝撤回已编号业务 |
| Get_Entry_By_Offset | O(offset)查看保留条目,供故障检查及业务恢复 |
| Get_Free_Bytes | 返回未占用字节数;空闲不少于本级头长+ASDU长度即有足够容量;NORMAL另检查编号窗口 |
| Reset_High | 同代次清空HIGH待发条目;保留NORMAL及next_entry_serial,不影响已提交帧和Manager记录 |
| Limit_Normal_Send | EOF时停止选取全部未提交NORMAL,原文留给下一连接;不改已提交帧或HIGH,重复调用不解除限制 |
| Detach | 旧I/O及Manager结束后核对旧代次,清空HIGH并恢复现存NORMAL待发状态,切换到代次0;清除旧覆盖记账及发送限制 |
| Rebind | 完整检查两级,绑定新的非零代次,可从离线代次0接入;恢复现存NORMAL、清空HIGH及旧连接记账;ERROR不修改控制字段或字节 |
| Abort | 明确丢弃全部引用,调用方先完成业务回收 |
Peek操作成功才写输出;原文按 `asdu_first`、`asdu_second` 的顺序读取,后者为NULL表示全部位于第一段。本条目被覆盖、确认释放或Remove后两段借用失效;HIGH的Mark_Submitted释放当前队首,Reset_High使所有HIGH借用失效;Detach、Rebind、Abort或Init使所有借用失效。NORMAL的后续Enqueue可能覆盖旧原文,调用方不能跨可能覆盖它的操作继续使用借用。NORMAL的Remove/Mark及仍有原文的Confirm核对完整generation和entry_serial,不能用可回绕的N(S)代替身份。HIGH只核对generation及entry_serial==0,从Peek到Mark/Discard之间不得移除、清空或替换队首;不提供同代次的逐条身份保护,0编号不得长期保存为业务身份。
原文已覆盖时不另存一份完整身份列表;准确条目身份由Manager记录保存,Confirm_Next_Normal_Entry核对当前代次、计数及从overwritten_unacked_serial_begin到overwritten_unacked_serial_end的循环区间,要求按本优先级真实发送顺序调用。ACK后将区间下界推进到该编号的后继,拒绝重复/逆序确认;最后一个覆盖身份必须匹配区间终点,再允许释放仍保留的物理队首。覆盖计数归零时清除两个边界,避免无在途义务的历史确认长期占住编号窗口。普通业务不得构造身份调用Confirm_Next_Normal_Entry伪造ACK。
NORMAL编号环包含1..UINT64_MAX,前向距离为`to >= from ? to - from : UINT64_MAX - from + to`,因此UINT64_MAX到1的距离是1,而不是普通uint64溢出减法所得的2。非零距离不超过UINT64_MAX/2时才认为旧编号在新编号之前。NORMAL入队提交前检查其保留队首及覆盖待ACK区间下界到新next的距离,始终限制在半环内;只读至多一个队首,保持常数检查开销。若长期未处理的旧记录阻止窗口前进,NORMAL返回FULL,原队列及字节完全不变;处理旧条目或ACK后可重试。HIGH不分配编号,不受此窗口阻塞。NORMAL已经在控制副本中淘汰的未提交条目不再占用窗口。
该约束以仍保留的NORMAL条目和覆盖待ACK身份为准。HIGH的asdu_entry_serial恒为0,帧完成及ACK依据独立frame_id和N(S)匹配。释放后的NORMAL entry_id已经失效,调用方不得无限期保存并跨完整编号周期重新提交。回绕不会触发Rebind、清空业务或增加连接代次,也不增加内部条目头字段。
需要连续原文的业务恢复代码可使用以下接口。Copy_Asdu只检查描述和输出容量,不重新查询队列或验证身份;必须在借用有效期内串行调用,输出不得与原文或描述重叠。
~~~c
IEC104_AsduHeadInfo_t info;
uint8_t raw[IEC104_MAX_ASDU_LEN];
if (IEC104_AsduQueue_Peek_Head(&queue, IEC104_ASDU_PRIORITY_NORMAL,
&info) == IEC104_ASDU_QUEUE_OK &&
IEC104_AsduQueue_Copy_Asdu(&info, raw, sizeof raw) == IEC104_ASDU_QUEUE_OK) {
/* raw[0..info.asdu_length-1]是供内部处理使用的连续副本。 */
}
~~~
Bucket_Valid检查容器边界、数量和占用区与队尾/待发游标的关系,不遍历条目。Ring_Advance处理下标回绕并避免加法溢出,Ring_Read/Ring_Write最多分两次复制。Read_Entry_With_Expected_State先验证条目头位于已占用区,再读取可能跨尾部的长度和身份,核对预期队列状态,检查整条占用后返回原文视图。Mark/Remove会先验证需要访问的后继,再提交控制字段,避免错误返回时部分移动游标。
内部读取及删除函数的 `expected_queue_state` 参数只核对操作前的旧状态,使用 `QUEUE_ENTRY_PENDING`(尚未登记交接)或 `QUEUE_ENTRY_RETAINED_UNACKED`(NORMAL保留原文待ACK)表达。HIGH完成交接时调用 `Remove_Head_With_Expected_State(..., QUEUE_ENTRY_PENDING)`;该参数不是删除原因或要写入的新状态。NORMAL通过 `Mark_Normal_Entry_Retained_Unacked` 明确更新条目头,将内部编码写为2,不再用含义不清的0/1参数控制状态更新。
内部队列返回FULL时,NORMAL由TcpServer直接转换为WAIT,HIGH经Manager和Connection转换后返回给业务;业务保留原文和生产游标。HIGH需要等待字节空间,NORMAL可能需要等待占住编号窗口的旧条目/ACK处理完成。NORMAL成功可能伴随旧原文覆盖。ERROR须修正输入、容量配置或状态,不能进入等待空间的重试分支;库不会自动丢弃这条业务或推进游标。只有TcpServer提交接口返回OK才推进业务游标。静态数组的“释放”表示空间可复用,不是归还操作系统内存。
<a id="aq-reconnect-policy"></a>
### 3.4 固定的队列生命周期
优先级同时决定发送顺序和存储生命周期;不再提供逐条重连策略,也不根据TypeID或COT自动推断。同为遥测ASDU,自发事件与总召响应应按业务来源放入不同队列。
| 业务 | 队列 | 生命周期 |
|---|---|---|
| 总召激活确认、总召数据及结束确认 | HIGH | 属于发起总召的连接,提交后出队;连接初始化、启动应答及重连清空剩余待发业务 |
| 命令确认、读响应、时钟同步确认等请求应答 | HIGH | 同上,不向新连接重放旧请求结果 |
| 允许跨连接恢复的自发事件 | NORMAL | 随服务初始化,无连接也可入队;有效ACK释放原文,满时覆盖最旧原文;重连绑定现存原文,Close服务才清空 |
内部队列统一使用AsduQueue_Enqueue;NORMAL由TcpServer直接调用,需要缓存的HIGH经Connection/SendManager调用,直接应答不入队。业务分别使用Enqueue_Event和Send_Response,不接触队列指针。下面仅展示内部NORMAL入队接口,generation须匹配队列绑定,无连接时为0:
~~~c
/* 可跨连接恢复的事件使用NORMAL。
* 返回值仍须检查;仅OK推进生产者游标,FULL等待,ERROR明确处理。
*/
IEC104_AsduQueueResult_t result = IEC104_AsduQueue_Enqueue(
&queue, generation, IEC104_ASDU_PRIORITY_NORMAL,
event_asdu, event_length);
~~~
原先显式保留的HIGH事件应迁移到NORMAL;仅属于当前连接的NORMAL请求应答应迁移到HIGH。没有同一优先级内混合保留/丢弃策略的接口。NORMAL的保留不保证数据不丢失:空间不足时仍可能被覆盖;未ACK也不证明对端未收到。恢复只适用于业务认可的同一接收方或冗余域,本模块不识别远端主站身份,也不提供应用去重。
服务Init先以代次0初始化队列;离线状态下HIGH为空,NORMAL全部待发,覆盖待ACK记账及发送限制均为空。接入时Rebind到非零代次,再初始化Connection/Manager。EOF时Limit_Normal_Send将normal_send_limited置1,已有及后来新增的未提交NORMAL均不再交给旧连接。Peek_Unsubmitted对NORMAL返回EMPTY,原文及待发计数保持可查询;入队、覆盖、取消条目和重复调用均不解除限制,Detach/Rebind清零开关后全部现存NORMAL重新可选取。已提交的I帧由SendBuffer及Manager独立收尾:Manager先续写当前帧,再处理控制FIFO及已受理HIGH,不从NORMAL创建新帧,也不会因HIGH公平配额耗尽而选取被限制的NORMAL。EOF限制只用normal_send_limited表示,不维护剩余额度;它不改变接口签名及NORMAL容量覆盖策略。
高优先级队列用于连接应答,低优先级队列用于可跨连接保留的事件;高优先级原文出队后,由独立发送记录继续等待ACK。
Manager.Init清空HIGH;STOP受理立即作废待发HIGH,无逐条回执。从停用状态重新START仍清理HIGH及公平配额;已激活时重复START保留。WAIT尚未受理、不清理;排空中的重复STOP不重复清理。清理不撤回已提交I帧、不清除记录或ACK义务、不改变NORMAL;同一TCP启停不调用Rebind。
<a id="sendmanager"></a>
## 4. SendManager:发送准备、ACK及回执
源码:[IEC104_SendManager.c](../../104/IEC104_SendManager.c)。只绑定CONTROLLED子站;主站启停算法仍由独立Link角色模块提供,不在这里发送ACT。
### 4.1 记录与返回值
每个I帧占用一条 `IEC104_IFrameSendRecordStorage_t`:frame_serial、asdu_entry_serial、asdu_priority、原N(S)/N(R)/长度、准备和写完时刻、本地完成及对端确认标志;不保存user_tag。所有记录共用Manager保存的connection_generation,每条不再重复保存代次。查询接口仍返回 `IEC104_IFrameSendRecord_t`,将共享代次与该条frame_serial组合成完整frame_id,其他字段逐项复制;返回值是独立数值副本,Manager重建不会改变已经取出的快照。
NORMAL原文身份使用Manager的连接代次和该条非零asdu_entry_serial;HIGH原文提交时已出队,其asdu_entry_serial恒为0,没有入队业务身份。两种记录均以完整frame_id和N(S)处理完成及ACK,不借用原文指针,也不保存完整APDU。发送进度、失败、ACK和回执消费仍校验外部事件的连接代次;旧连接事件不能因新连接复用frame_serial而被接受。确认回执只报告帧级事实,不再返回调用方的业务标识。
Manager持有三个模块的指针及内部提供的记录环形数组,维护head/count、high_unacked_count、next_record_ns、k和高优先配额。high_unacked_count统计已登记且未ACK的HIGH记录,包括尚未完整写出的帧;登记HIGH记录时增加,有效ACK时减少,移除已ACK回执不再减少。next_record_ns表示下一条新登记I帧记录的N(S),在发布记录时推进,用于校验记录的连续编号;发送完成位置和ACK位置由Link另外维护。记录容量1..32767,k也为1..32767;它们可独立配置,记录容量包含已经ACK但尚未由TcpServer转为事件并移除的回执。已经复制到事件队列的回执由事件容量单独计数。
| 返回值 | 含义 |
|---|---|
| OK | 操作成功;Prepare_Next_Send得到可写字节,Handle_Send_Progress尚未整帧完成 |
| QUEUED | 控制帧已排队,不代表已写socket |
| EMPTY | 无待选业务/无记录/无对应待推进过程 |
| WAIT | 门控、窗口、容量或待完成义务阻塞;接收入口表示当前请求尚未受理 |
| FRAME_WRITTEN | 已连续提交Link完成、记录和Buffer释放,输出完成副本 |
| IGNORED | 排空中的重复STOP,或提前/无匹配等待的TESTFR_CON;不重启或撤销原有计时 |
| ERROR | 参数、身份或接入状态非法,需要修正;不能按容量不足盲目重试 |
| PROTOCOL_ERROR | I/S报文因接收阶段、序号或ACK违反协议而被Link拒绝,当前连接必须结束 |
| FATAL | 当前连接必须结束,先保留事实用于业务回收 |
所有枚举精确比较,不能用“非零为成功”。
<a id="manager-consistency"></a>
### 4.2 常数时间一致性检查
Manager_Storage_Valid校验自身容量、指针、代次、配额及HIGH未确认数不大于记录数,不依赖故障后的Link,供回执读取和恢复使用。运行会话的Manager_Session_Valid另外要求Link保持CONTROLLED角色;绑定后的角色变化属于本地状态非法。
`IEC104_SendManager_Get_Status`按同样的存储边界检查返回代次、记录容量、当前记录数及HIGH未ACK数;不暴露绑定指针,不消费记录,OK才写输出。该查询可在连接冻结后使用,Abort之后返回ERROR。
`IEC104_SendManager_Enqueue`先验证当前会话及代次,再调用绑定队列的Enqueue;返回OK、WAIT(队列FULL)或ERROR。它沿用HIGH/NORMAL的保留、覆盖和编号规则,不生成I帧或分配N(S),用于应答暂不能直接准备或Connection因期限已到而延后处理的路径。TcpServer把直接准备或暂存成功统一转换为公开的IEC104_TCP_SUBMIT_OK。服务级NORMAL由TcpServer直接写入所持队列,不要求Manager存在;实际发送仍统一由Manager处理,业务不依赖内部指针。
Manager_Session_Valid调用各模块公开查询,检查:
- 三模块绑定代次一致,无外部遗留预留。
- manager.next_record_ns等于Link下一分配序号。
- distance(next_completion_ns, next_frame_ns)为0或1,等于Buffer当前I帧数量;有当前帧时N(S)等于下一完成位置。
- HIGH原文环中已提交待确认数恒为0。
- 已编号未ACK的I帧数不超过k、不大于记录数,且等于Manager.high_unacked_count加NORMAL原文保留及覆盖待确认条目数。
所有距离按模32768计算,不以普通整数大小比较。Manager通过Link接口修改协议状态,角色也通过Get_Status读取。运行检查在完整事务的公开入口执行,不能插入Prepare/Commit、Complete/释放、ACK/原文释放之间;参数、身份、序号及模块计数校验不依赖构建模式。
记录通过N(S)与记录环偏移常数时间定位,再核对不复用frame_id及快照。ACK只访问本次新确认区间,不扫描全部历史回执。
故障回收允许Remove_Confirmed_Record在FAILED阶段调用,通过既有队首记录查询校验frame_id、本地完成及ACK标志,不要求Link或队列恢复正常会话状态。完整事务仍由Manager按顺序提交,不能在中间步骤插入其他队列操作或ACK处理。
<a id="manager-i-transfer"></a>
### 4.3 Prepare_Next_Send
签名:
~~~c
IEC104_SendManagerResult_t IEC104_SendManager_Prepare_Next_Send(
IEC104_SendManager_t *manager, uint64_t generation, uint64_t now_ms,
IEC104_SendBufferHeadInfo_t *head_info);
~~~
本接口由Connection在准备实际发送时调用,业务生产者只调用TcpServer_Enqueue_Event或TcpServer_Send_Response。内部也不能提前反复调用它来预建一批I帧。
1. 校验会话;已有当前I帧或控制队首时,直接返回该帧剩余字节。
2. 查看两级下一未编号条目,均空返回EMPTY。
3. 选择优先级;原文连续时直接借用,跨尾部时用Copy_Asdu拼成连续副本。调用共用Prepare_I_Asdu,记录满返回WAIT,否则保障当前I帧存储。
4. Link.Prepare检查k和链路门控,生成APDU并分配N(S)。未生成可取消预留,返回WAIT/ERROR。
5. 让SendBuffer接手完整I帧,登记Manager业务记录并在HIGH时增加high_unacked_count;再调用Mark_Submitted完成队列记账,HIGH消费队列原文,NORMAL登记为保留原文待ACK;最后更新配额。
6. 返回当前HeadInfo,结果为OK。
Prepare_Next_Send使用一个249字节的局部数组存放跨尾部ASDU;这是调用期间的自动存储,嵌入式接入需计入栈预算。只有原文确实分两段时才执行额外复制,连续原文仍直接交给Link。HIGH原文提交时已出队;NORMAL原文可被后续入队覆盖。SendBuffer对连接层始终提供独立、连续的APDU,原文覆盖不影响当前帧续写,实际I/O接口不变。
high_priority_burst_limit=0为严格高优先;非0时,NORMAL有待发条目才累积连续HIGH次数,达到上限让出一次NORMAL。普通桶仅有已提交未ACK条目不算待发,不积累配额欠账。每个新I帧重新选择,重复准备/WAIT不改变配额。
Link.Prepare成功后序号不可撤回,后续异常返回FATAL;保留尚存原文及已登记事实供恢复,已经出队的HIGH不恢复。最多一帧已编号但未完成,不能认为连这一帧也完全没有提前分配:编号发生在实际写调用前,EAGAIN时该帧仍持有序号。
### 4.3.1 Submit_Response
```c
IEC104_SendManagerResult_t IEC104_SendManager_Submit_Response(
IEC104_SendManager_t *manager, uint64_t generation,
const uint8_t *asdu, uint16_t asdu_length, uint64_t now_ms,
IEC104_SendBufferHeadInfo_t *head_info);
```
Connection在未到期路径先Drive处理保留接收和当前控制义务,再调用此接口。Manager的Submit_Response和Enqueue(HIGH)共用Response_Submit_Allowed,在记录容量、k窗口和HIGH容量判断之前按实际接手时的状态准入:
| 启停状态 | 应答准入 |
|---|---|
| STARTED + IDLE | 允许;满足发送条件时直接准备,否则暂存HIGH |
| STOPPED + IDLE | ERROR,不入HIGH |
| STOPPED + STOP_DRAINING | ERROR,不入HIGH |
子站START/STOP受理立即更新dt_state;应答准入仍集中检查STARTED/IDLE。协议期限已到路径直接Enqueue(HIGH),也经过同一检查。新的I帧须等待发送缓冲为空;已有CON时应答暂存HIGH,由FIFO保证先写控制帧,再发业务。
直接准备不虚构ASDU队列条目,也不调用Mark_Submitted;记录仍为HIGH、asdu_entry_serial为0,并增加high_unacked_count。之后与队列应答使用同一Handle_Send_Progress、Handle_I_S_Frame和回执逻辑。只有实际准备成功才消耗N(S)和配额,单纯入HIGH不会分配协议序号。
OK表示已准备APDU,只有此时写head_info;QUEUED表示已复制HIGH;WAIT表示HIGH暂满、本次未接手。ERROR表示参数/状态或HIGH总容量不符,包含启停准入拒绝,Connection及TcpServer原样映射为ERROR且不因此断连;编号后交接失败为FATAL,必须收尾。QUEUED及对外SUBMIT_OK不保证待发HIGH最终发出,受理新STOP时按4.7节作废。Manager不调用socket;TcpServer直接发送时最多一次非阻塞写入,部分写入或暂不可写由Process/Poll继续同一帧,不能重复入HIGH。
<a id="manager-send"></a>
### 4.4 Handle_Send_Progress / Failure
TcpServer经Connection取得HeadInfo后,通过Net实际写入,再由Connection向Manager提交该次成功写入增量。Manager按frame_id匹配当前帧;I帧还核对关联记录。整帧完成时按原类型分派:
| 原类型 | Link完成接口 |
|---|---|
| I | Complete_I_Send(原快照) |
| S_ACK | Complete_S_Ack_Send |
| STARTDT_CON | Complete_Startdt_Con_Send |
| STOPDT_CON | Complete_Stopdt_Con_Send |
| TESTFR_ACT | Complete_Testfr_Act_Send |
| TESTFR_CON | 无独立Link完成接口;仍提交Buffer进度与释放 |
FRAME_WRITTEN输出IEC104_LocalSendCompletion_t,只有数值副本,无已经释放的报文指针。里面的Link结果保留OK/IGNORED,主动TESTFR成功时可据written_at_ms建立等待CON的t1。
最终写入失败或本地发送超时用Handle_Send_Failure,匹配身份后分派失败完成并返回FATAL。EAGAIN不是最终失败,不调用该接口。不能重复报告写入增量或重复调用底层Complete。
<a id="manager-ack"></a>
### 4.5 Handle_I_S_Frame及记录消费
apci必须来自完整APDU的成功解析,只接受I/S:
1. 读取Link原位置,验证本次新ACK区间对应的记录已本地完成;越界网络N(R)交给Link拒绝,不用于记录寻址。
2. Link.Handle验证ACK、接收阶段及I帧N(S),成功后推进协议位置。
3. 先标记整个新确认区间的peer_acknowledged,并按本次新确认的HIGH记录数核减high_unacked_count。
4. HIGH不再访问ASDU队列;仅对NORMAL按真实记录的generation、entry_serial调用Queue.Confirm_Next_Normal_Entry:仍有原文就严格匹配队首并释放,原文已覆盖就核减待确认数,不访问或删除新队首。
Link只接受不越过本地完成位置的ACK。例如0、1还仅在ASDU队列时,ACK=2非法;当前I帧只写了一部分时,也不能由ACK代替本地完成。异步接入先提交真实发送结果,再按接收顺序处理相关ACK。
本地会话或记录预检失败返回ERROR;在本地状态合法时,Link拒绝I/S报文返回独立的`IEC104_SEND_MANAGER_PROTOCOL_ERROR`。Link已经受理后出现状态同步或原文释放失败仍返回FATAL。Connection将前者的协议拒绝归为PROTOCOL,将本地预检及后续交接失败归为INTERNAL,不再把所有非OK结果都归责于对端报文。原有返回值数值不变,新增结果仅用于I/S接收接口。
重复ACK不释放第二次,也不重复核减HIGH或覆盖待确认计数,不重置t1。原文覆盖本身不推进V(A)、不释放k窗口或业务回执槽;已有帧仍须真实完成和取得有效ACK。ACK处理复杂度为O(本次新确认条目数)。Link接受后释放原文失败为FATAL;所有已接受的ACK事实仍保留在记录中,恢复时不能把这些数据当成未确认重报。
| 记录接口 | 用途 |
|---|---|
| Peek_Oldest_Sent_Unacked_Record | 常数时间获取最早已本地完成未ACK记录,只有部分发送时EMPTY |
| T1_I_Ack_Expired | 使用该记录原written_at_ms,1到期/0未到期或无等待/-1非法 |
| Get_Deadlines | 当前队首身份、本地发送及I帧t1剩余时间、活动标志和控制容量;只读且不选择新业务 |
| Peek_Confirmed_Record | 查看已完成且已ACK的记录队首;未就绪WAIT,不自动释放 |
| Remove_Confirmed_Record | TcpServer成功复制RECORD事件后,经Connection按frame_id移除队首 |
| Get_Record_By_Offset | 不依赖运行中的Link读取记录,供故障恢复;ACK事实不丢失 |
回执未转为事件并移除时仍占records,容量满会阻止新I帧;已经ACK的原ASDU空间可复用,S/U仍可发送。业务处理RECORD副本失败时应自行保存副本,不调用内部Remove,也不会恢复已移除的记录槽。
<a id="manager-control"></a>
### 4.6 S、t2、TESTFR与t3
Queue_S_Ack按w门限、immediate或停止排空规则生成S。Queue_T2_Ack在原待确认接收数据达到t2时按相同门控生成S。Queue_Testfr_Act主动探测;Queue_T3_Testfr额外检查原空闲时长。Handle_Testfr_Act受理对端ACT并排队CON,不结束本端独立探测。
上述构帧入口先预留控制槽,再调用Link。无容量返回WAIT,不改变链路;未生成报文则取消预留,成功生成返回QUEUED。构帧成功后的提交异常、取消异常均为FATAL。时钟和定时器由连接层管理。
接收确认使用`Handle_Testfr_Con(manager, generation, apci)`。它校验会话后调用Link,不预留发送槽、不生成应答;CON_PENDING下接受确认返回OK,Connection此时才撤销探测t1。提前收到或没有匹配等待时返回IGNORED,不缓存、不撤销计时,控制槽满也不阻止确认受理。Connection不再直接修改Link的探测状态。
Connection分派的STARTDT_ACT、STOPDT_ACT和TESTFR_ACT已经通过格式与方向检查;合法重复请求沿用原受理规则,处理中的ERROR/FATAL归为INTERNAL,包括HIGH清理、控制槽提交或取消失败。TESTFR_CON没有匹配等待时仍忽略,内部状态非法归INTERNAL;子站收到格式合法的反向STARTDT_CON/STOPDT_CON由Connection消费并忽略,不交给Manager,不改变启停、确认期限或t3活动时刻。
Handle_Testfr_Act或Slave_Handle_Startdt_Act返回WAIT时请求尚未受理,调用方必须有界保留完整请求并维持接收顺序,不能静默丢弃或越过它分派后续帧。子站启停不再暂存相反ACT。
<a id="manager-slave"></a>
### 4.7 子站启停
| 入口 | 行为 |
|---|---|
| Slave_Handle_Startdt_Act | 先预留控制槽,成功受理即STARTED/IDLE并排CON;结束旧停止等待,不暂存START |
| Slave_Handle_Stopdt_Act | 无需控制槽,立即STOPPED/STOP_DRAINING并清理HIGH;不等待启动CON,不暂存STOP |
| Slave_Queue_Stopdt_Response | 先排S确认,再等已预留I帧获ACK;满足条件后排STOP_CON并回STOPPED/IDLE |
已在FIFO中的S完整覆盖接收数据时,STOP_CON可紧随排队,不必等S网络完成。队列容量不足返回WAIT,调用方保留义务;START的WAIT表示请求尚未受理,不能取消STOP。Link只控制协议状态,发送缓冲持有部分写入和帧身份。
从停用状态重新START时清理HIGH并重置公平配额;已启用时重复START保留HIGH。STOP清理不撤回当前I帧或记录,不影响NORMAL。
两个CON完成接口只检查发送结果,均不回写dt_state/dt_procedure。后到START可以在旧STOP_CON仍待发时激活,旧完成不能覆盖新状态。停止排空被START结束时尚未生成的STOP_CON不再生成;已经入队的CON必须按FIFO发送。
Slave_Queue_Stopdt_Response新增可空输出stop_con_id,仅STOP_CON成功入队时写入。Connection用它记录本轮最后一帧停止应答,匹配完整发送后才结束STOP期限;成功受理START也取消旧计时。重复STOP不刷新有效起点,旧CON完成不能误清新STOP的期限。
### 4.8 Init与Abort
Init要求Link为CONTROLLED初始状态、Buffer空闲、Queue与Buffer同代次且Queue没有已提交条目。允许Queue含预先排队或Rebind恢复的未编号业务;Init成功会清空HIGH、保留NORMAL,检查失败时不清理。HIGH出队后队列本身无法体现旧连接的在途帧,接入方仍须先结束旧I/O并Abort旧Manager,不能用Init接管运行中的连接。不分配存储。
Connection使用构造专用`Init_Connection`:在局部Link/Buffer/Manager完成上述校验,最后一次可失败操作为清理HIGH;随后只向指定的最终地址提交已校验对象,并在Manager内部建立绑定。ERROR时Manager、Link、Buffer及队列都不变,也不改控制槽和记录数组。Connection随后提交其余连接字段,无需访问或修补Manager内部指针。此入口不支持运行中重绑;对象成功初始化后仍不能复制或移动。原Init继续支持绑定已单独初始化的Link/Buffer。
Abort前,TcpServer先结束旧I/O并把剩余记录复制为事件;Connection_Close内部调用Manager.Abort清除Manager及Buffer,**保留AsduQueue**。TcpServer随后调用Queue.Detach,立即清空HIGH并恢复NORMAL离线缓存;仅关闭整个服务时调用Queue.Abort。新连接代次由TcpServer管理,Link重新初始化和发送模块绑定由Connection_Init完成。
<a id="integration"></a>
## 5. 内部调度、容量与恢复
### 5.1 静态存储与初始化
以下示例仅说明发送模块的内部绑定关系。实际对象由TcpServer及其内含Connection持有,业务通过TcpServer_Create/Init启动服务,无需复制这些声明或调用底层Init。
~~~c
#include "IEC104_SendManager.h"
static uint8_t high_bytes[1024];
static uint8_t normal_bytes[4096];
static IEC104_SendBufferControlFrame_t controls[4];
static IEC104_IFrameSendRecordStorage_t records[12];
static IEC104_AsduQueue_t queue;
static IEC104_SendBuffer_t buffer;
static IEC104_LinkState_t link;
static IEC104_SendManager_t manager;
/* 仅用于全新或明确丢弃旧业务后的队列,不用于保留业务的重连。 */
static int Init_Fresh(uint64_t generation)
{
if (!IEC104_Link_State_Init(&link, IEC104_ROLE_CONTROLLED) ||
IEC104_AsduQueue_Init(&queue, high_bytes, sizeof high_bytes,
normal_bytes, sizeof normal_bytes, generation) != IEC104_ASDU_QUEUE_OK ||
IEC104_SendBuffer_Init(&buffer, controls, 4, generation) != IEC104_SEND_BUFFER_OK) {
return 0;
}
return IEC104_SendManager_Init(&manager, &queue, &buffer, &link,
records, 12, 12, 4) == IEC104_SEND_MANAGER_OK;
}
~~~
直接使用AsduQueue时,HIGH至少 `IEC104_ASDU_HIGH_QUEUE_MIN_CAPACITY`(8)字节;NORMAL至少 `IEC104_ASDU_NORMAL_QUEUE_MIN_CAPACITY`(当前ABI下22)字节。保证同时保留N条、ASDU最长L字节(N≥1、6≤L≤249)时,HIGH配置 `N × (2 + L)`,NORMAL配置 `N × (IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN + L)`,无需额外回绕空间。已知各条实际长度时,占用为 `Σ(本级头长 + Li)`。保留10条最大249字节ASDU,HIGH需2510字节,NORMAL在当前ABI下需2650字节。
HIGH的N只计算尚未提交的待发条目;NORMAL的N还包括原文仍保留的当前发送和已发未ACK条目,两级分别规划。需要支持任意合法ASDU长度时,对应桶至少本级头长+249字节。回执尚未转为事件的延迟计入records容量,业务读取事件的延迟计入TcpServer事件容量;事件区满也会阻止继续转移记录。仅NORMAL原文环采用覆盖;HIGH、控制FIFO、确认记录和k窗口各自保持拒绝或等待语义。已覆盖原文不再占字节容量,但对应的已编号帧仍占k窗口和records。
TcpServer公开接口支持全部合法ASDU长度,因此编译期要求HIGH和NORMAL各自至少容纳一条最大ASDU及本级条目头,即对应`ENTRY_HEADER_LEN + IEC104_MAX_ASDU_LEN`;当前ABI下分别为251和265字节。`Send_Response`的回退排队使用HIGH,`Enqueue_Event`使用NORMAL。底层AsduQueue仍允许上述较小容量;TcpServer不接受这种无法容纳最大ASDU的配置,避免应答在直接准备时成功、回退排队时却因总容量不足返回SUBMIT_ERROR。
Windows 64位GCC 16.1.0默认ABI下:AsduQueue控制结构120字节、SendBuffer结构336字节(含唯一255字节I APDU)、控制槽40字节、发送记录存储类型48字节、查询快照类型56字节、Manager结构64字节。共享连接代次曾将存储记录从64字节降为56字节;此次移除user_tag后从56字节降为48字节,12条记录数组由672字节降为576字节,再节省96字节。以上不是跨平台尺寸承诺,目标平台应以sizeof为准。
ASDU每条恰好占本级头长+实际长度,回绕不额外占容量。Manager有249字节的局部拼接数组;Init_Connection另有临时Link、Buffer和Manager用于预检,两条调用路径均须按目标编译结果计入栈预算;Rebind更新NORMAL头内状态和控制字段,不搬移ASDU,不使用整理数组。
相比之前每条固定272字节的业务槽,小ASDU可以共用更多容量;最大249字节ASDU在HIGH中占251字节,在NORMAL中占头长+249(当前ABI下265)字节。原文环容量和发送记录容量分别计算,实际RAM需求还取决于两者的配置。
### 5.2 业务入口与内部收发循环
~~~text
业务提交ASDU:
NORMAL:TcpServer_Enqueue_Event → AsduQueue_Enqueue(服务缓存)
应答:TcpServer_Send_Response → Connection_Submit_Response → Manager.Submit_Response
直接准备 → 共用Write_Output最多一次非阻塞写入
暂不能准备或协议期限已到 → Manager.Enqueue暂存HIGH
仅TCP_SUBMIT_OK后推进生产游标;NORMAL的OK可能已经覆盖旧原文
TCP_SUBMIT_WAIT保留原文,后续Poll间隙重试
TCP_SUBMIT_ERROR/FATAL → 按参数、连接身份或故障处理,不当作容量不足盲目重试
TCP内部准备发送:
先安排应生成的S/U
Connection_Prepare_Next_Send → Manager.Prepare_Next_Send
EMPTY/WAIT → 等待业务、ACK、记录转移或状态变化
Connection返回SEND_READY → 共用Write_Output经Net写socket(head.pending_data, head.remaining_bytes)
正字节数 → Connection_Handle_Send_Progress → Manager.Handle_Send_Progress
EAGAIN/EWOULDBLOCK → 等待再次可写,保持当前帧
EINTR → 按平台规则重试,不伪造进度
最终失败 → Connection_Handle_Send_Failure → 结束连接
FRAME_WRITTEN后才处理依赖这次完成的接收事件,再准备下一帧
TCP内部接收:
TcpServer经Net收到字节 → Connection.Feed → Receiver.Feed
TcpServer按consumed保留剩余输入
成帧后Connection经Receiver.Peek_Frame取得APDU及已解析APCI
Connection把I/S及需要处理的U帧交给Manager,对Link只作只读查询
WAIT → 保留当前接收帧,容量恢复后重试,不越过它处理后续帧
S/U已受理 → Connection释放内部接收帧
I帧已受理 → TcpServer复制I_FRAME事件 → 经Connection释放内部接收帧
确认回执就绪 → TcpServer复制RECORD事件 → 经Connection移除内部记录
业务处理事件:
TcpServer_NextEvent取出副本 → 解析ASDU或处理发送回执
需要应答时调用TcpServer_Send_Response,不调用底层Release或Remove
~~~
不要把Prepare成功当作已经发送;也不要把socket实际成功但尚未提交的写入长期滞留在异步完成队列。异步情况下同一帧最多一个未回报写操作;隔离代次不能代替结束旧I/O对数组的访问。
仅检查dt_state==STARTED不能代替Link完整门控。初始STOPPED时可以缓存NORMAL;HIGH应答须等STARTED/IDLE后再提交。被控站受理START立即开放Link的新I/S准备;Manager继续遵守SendBuffer顺序,已有当前I帧先完成、控制FIFO随后排空,再选择新I帧。HIGH在Manager初始化、受理新STOP以及从STOPPED受理START时清空;已启用时重复START保留HIGH。
<a id="timeouts"></a>
### 5.3 原始期限
| 期限 | 起点及接口 |
|---|---|
| 当前帧本地发送期限 | 原queued_at_ms;Connection经Manager.Get_Deadlines取得剩余时间和同一队首身份;Buffer.Head_Send_Expired保留为底层接口 |
| I帧等待ACK t1 | 最早已完成未ACK记录的原written_at_ms;Connection经Manager.Get_Deadlines查询,单项T1_I_Ack_Expired保留 |
| TESTFR等待CON t1 | ACT完整发送且Link完成成功的written_at_ms;Connection记录并通过Collect_Deadlines计算 |
| 接收确认t2 | 连续未确认接收区间的原起点;Collect_Deadlines判定到期后调用Queue_S_Ack(immediate=1),Queue_T2_Ack保留为底层接口 |
| 空闲t3 | 有效链路活动起点;Collect_Deadlines判定到期后调用Queue_Testfr_Act,Queue_T3_Testfr保留为底层接口 |
| 停止过程总期限 | 新STOP的received_at_ms;重复STOP不续期,成功受理START或本轮最后一帧STOP_CON匹配完成时结束 |
Connection的Check_Timeouts、Get_Wait_Time、发送准入及控制推进共用临时Collect_Deadlines视图。Manager的Get_Deadlines仅观察已存在的发送头和记录,OK才写输出;对应has_*为1且remaining_ms为0才表示到期。期限计算使用64位时间差,起点0合法,不计算可能溢出的绝对到期时刻。接收批次可以暂缓确认故障处理,实际到期事实及原始起点不改变。
定时器只负责唤醒,回调再次核对代次和当前有效起点。仅业务排队尚未编号不启动I帧t1。ACK推进后选下一最早未确认帧的原时间,不从当前时间重新开始。
<a id="failure"></a>
### 5.4 断线恢复
以下操作由TcpServer和Connection完成。NORMAL固定随服务保留,HIGH固定属于连接;业务使用事件提交与应答发送入口,并通过事件读取收尾结果。直接应答同样不跨连接恢复。
1. 停止分派旧连接事件,关闭TCP并结束/隔离旧I/O对存储的访问。
2. TcpServer经Connection_Get_Record_By_Offset读取剩余记录并复制为closing事件,区分已ACK、未ACK和未本地完成。NORMAL的记录可能仍在而原文已覆盖。
3. 核对记录中的ACK事实。只有NORMAL现存原文参与恢复,且业务须允许向同一接收方或冗余域重报;未ACK不证明对端未收到。HIGH请求应答不跨连接保留。
4. Connection_Close调用Manager.Abort,保留Queue。TcpServer随即调用Queue.Detach(old_generation),清空HIGH、恢复现存NORMAL待发状态并切换到代次0,继续接受离线NORMAL。新连接建立时调用Queue.Rebind(new_generation);任一步ERROR均停止本次恢复,不能重新Init而静默丢弃缓存。
5. Connection_Init经Manager.Init_Connection校验并提交初始CONTROLLED Link、空Buffer及Manager绑定,再提交其余连接字段;队列可含未编号业务,失败不覆盖原对象或队列。
6. 新连接STARTDT完成后,以新N(S)生成新的APDU和frame_id。
例如HIGH中有总召应答A、命令确认C,NORMAL中仍有事件B、D;断线Detach清空A、C,保留B、D原顺序并恢复未提交;离线新增E追加在后,下次Rebind绑定B、D、E。已提交的HIGH原文此前已出队,也不会重发。
Detach与Rebind共用绑定转换,先完整验证两级所有条目的长度、占用范围及待发分界,并验证NORMAL的编号顺序和头内状态。均通过后,将NORMAL已提交前缀的头内状态恢复为未提交;后续没有可失败分支,因此ERROR仍保证控制字段及字节不变。HIGH元数据清零,NORMAL将unsubmitted_index设为head_index、unsubmitted_count设为entry_count;只更新头,不搬移或清除ASDU原文。复杂度O(条目数),额外存储固定,不使用malloc。NORMAL保留条目的entry_serial和原文不变,next_entry_serial不重置;更换generation使所有旧entry_id和借用失效。
Detach/Rebind清除旧连接的覆盖待确认计数、确认边界及EOF发送限制,只能恢复NORMAL环中仍存在的原文。已覆盖的原文无法恢复。重连丢弃不产生回调或伪造ACK;未编号条目没有帧回执,已编号帧的旧回执应在Manager.Abort前读取。Rebind本身不发送,不提供冗余组管理;不要在同一TCP的普通STARTDT恢复中调用它。
如果FATAL发生在有效ACK后的原文释放阶段,部分已ACK条目可能仍保留在Queue。记录先登记了整个ACK区间,内部恢复逻辑不能把所有保留原文盲目当作未确认数据重报;业务根据公开记录判断发送事实,不直接修改内部队列。常规网络断线中,正常完成的ACK已释放对应原文。
### 5.5 满时背压
| 满的位置 | 行为及恢复条件 |
|---|---|
| HIGH字节环 | 当前空间不足返回FULL,释放后重试;不覆盖旧原文 |
| NORMAL字节环 | 从队首覆盖最旧原文直至足够,包含已提交未ACK条目;成功返回OK,无逐条丢弃通知 |
| 两级条目超过桶总容量 | ERROR,不覆盖、不修改队列或编号;须修正容量或组帧,不按背压重试 |
| 控制队列 | 控制接口WAIT,未受理接收请求须有界保存;控制发送完成后恢复 |
| 记录数组 | 准备新I返回WAIT;TcpServer复制RECORD事件并移除内部记录后恢复;控制帧仍可发 |
| k窗口 | Link不生成新I,Manager返回WAIT;有效ACK后恢复 |
| 当前帧部分写入 | 返回同帧剩余字节;保持FIFO字节顺序,不为后续业务编号 |
<a id="build"></a>
## 6. 构建和迁移
### 6.1 生产构建依赖
子站发送模块需要以下六个源文件:
~~~text
IEC104_Codec.c
IEC104_Link.c
IEC104_Link_Slave_StartStop.c
IEC104_AsduQueue.c
IEC104_SendBuffer.c
IEC104_SendManager.c
~~~
发送管理本身不需要主站启停实现或日志模块。独立流式接收另加入 `IEC104_Receiver.c` 和 `IEC60870_Codec.c`,分别完成104 APCI与公共ASDU头检查;旧 `IEC104.c/h`已原样恢复,但旧入口使用私有Link且不向Manager交付报文。Receiver完成APCI检查,连接层通过Peek取得视图并分派,受理后显式释放;Link公开协议接口签名不变。
2026-09-30共享连接代次历史验证:当时的5个专项场景覆盖完整身份及业务字段往返、混合优先级、部分写入、记录环复用、重连后旧代次拒绝、故障后读取ACK事实、NORMAL原文覆盖及查询失败不修改输出。GCC 16.1.0以 `-std=c11 -O2 -Wall -Wextra -Werror` 编译,专项 `5 passed, 0 failed`,原有全量 `119 passed, 0 failed`。该测试后因接口过期已于2026-10-19删除,不计入当前回归;历史命令和运行记录保存在`.build/shared-generation-check`。使用模拟I/O,未运行真实TCP。
2026-09-24编号回绕历史验证:当时新增独立的iec104_asdu_queue_test.c,以GCC 16.1.0、`-std=c11 -O2 -Wall -Wextra -Werror`编译,13个专项场景通过,其中包含3万次与独立FIFO模型逐步对照的随机操作。专项覆盖编号/字节环同时回绕、跨界覆盖ACK、重复及旧代次拒绝、重连保留顺序、失败不修改字节、半环窗口阻塞及恢复,以及Manager部分发送、EAGAIN和累计ACK。原有全部119个用例重新编译通过。64位边界测试在夹具中设置计数起点,窗口极限测试显式快进计数,其余队列及发送操作使用公开接口;未模拟真实TCP。当前目录未保留该专项源文件,这13个场景未计入2026-09-30的运行结果。
以下为此前字节环和重连策略改造的历史验证:严格警告编译、GCC静态分析和当时全部119个用例通过。临时校验从标准输入编译运行,覆盖1182组条目起点与长度组合、60万次队列随机操作、391组跨界位置构帧及7万帧协议序号回绕;重连策略另验证29760组满环起点、保留标记及提交分界组合。这些历史结果不代替本次编号回绕专项,真实TCP及嵌入式目标平台仍需接入验证。
<a id="migration"></a>
<a id="64-本次环形队列改造与接口迁移"></a>
### 6.2 接口迁移
此次高低优先级头拆分不提供旧条目格式兼容。所有调用方须按新头文件重新编译并初始化队列及Manager,不能把旧18字节头的缓冲内容交给新代码。NORMAL头采用本次构建的位域及对齐布局,不是跨ABI持久化格式。
| 原接口/约定 | 当前接口/约定 |
|---|---|
| `Enqueue(queue, generation, priority, asdu, length, user_tag)` | 删除最后一个参数,改为 `Enqueue(queue, generation, priority, asdu, length)` |
| `AsduHeadInfo.user_tag`、两种发送记录的 `user_tag` | 删除,不再保存或返回业务标识 |
| `IEC104_ASDU_ENTRY_HEADER_LEN` | 删除,按优先级改用 `IEC104_ASDU_HIGH_ENTRY_HEADER_LEN` / `IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN` |
| `IEC104_ASDU_QUEUE_MIN_CAPACITY` | 删除,按优先级改用 `IEC104_ASDU_HIGH_QUEUE_MIN_CAPACITY` / `IEC104_ASDU_NORMAL_QUEUE_MIN_CAPACITY` |
| HIGH的非零entry_serial及按身份消费 | 查询的entry_serial和记录的asdu_entry_serial均为0;Mark/Discard消费当前队首,Peek至消费之间不得移除、清空或替换队首 |
| HIGH/NORMAL共用条目编号 | 仅NORMAL消耗next_entry_serial,HIGH不受NORMAL编号窗口阻塞 |
| NORMAL只用控制游标推导提交状态 | 条目头另存状态并与游标交叉校验;Mark写已提交,Rebind将已提交前缀恢复未提交 |
传给 `IEC104_SendManager_Init` 的记录数组继续使用 `IEC104_IFrameSendRecordStorage_t[]`,容量仍以条数计;查询输出继续使用 `IEC104_IFrameSendRecord_t`,两个类型均已删除user_tag,不能继续使用旧布局。发送结果和回执消费仍传完整frame_id,HIGH与NORMAL均有独立帧身份。结束旧I/O并回收记录后再Abort,随后重新绑定新代次。
发送状态命名按“未提交”“原文保留且未确认”“原文覆盖且未确认”区分。本次还改变HIGH存储生命周期、重连策略和结构布局;调用方需同步迁移,重新编译并初始化对象,不提供旧内存布局兼容。
| 旧名称 | 当前名称及含义 |
|---|---|
| AsduQueue.overwritten_serial_begin | overwritten_unacked_serial_begin,覆盖待ACK循环区间起点,含端点 |
| AsduQueue.last_overwritten_submitted_serial | overwritten_unacked_serial_end,同一区间终点,含端点 |
| AsduQueue/Status.overwritten_submitted_count | overwritten_unacked_count,原文已覆盖但仍待ACK的实际数量 |
| AsduPriorityQueue.waiting_index/waiting_count | unsubmitted_index/unsubmitted_count,队列中尚未登记发送交接的条目位置/数量 |
| AsduQueue.Peek_Waiting | Peek_Unsubmitted,直接查询队列中下一条尚未登记发送交接的条目 |
| AsduQueueStatus.submitted_count | retained_unacked_count,只统计原文仍保留的已提交未ACK条目 |
| AsduQueueStatus.is_empty | is_storage_empty,只说明原文存储为空 |
| SendBufferStatus.is_initial_state | is_idle,当前无I帧、控制帧及未结束的预留;发送结束后也可再次为1 |
| SendManager.next_commit_ns | next_record_ns,下一条新登记I帧记录的N(S) |
以下为此前存储和接口改造的迁移说明。
本次改变三个模块的结构布局和部分接口,需要更新外部调用方并重新编译,不提供旧布局兼容。
| 旧接口/结构 | 当前替换 |
|---|---|
| IEC104_AsduEntry_t固定条目数组 | 两个uint8_t字节数组,Init容量单位改为字节 |
| AsduQueue.Get_Free_Count | Get_Free_Bytes;容量计每条本级头长和ASDU实际长度 |
| AsduHeadInfo.asdu单一连续指针 | asdu_first/asdu_first_length及可选asdu_second;需要连续原文时调用Copy_Asdu |
| AsduPriorityQueue.padding_bytes | 已删除;头和原文可跨尾部,不再产生填充占用 |
| 两级Enqueue空间不足都返回FULL | 字节容量不足时HIGH返回FULL、NORMAL覆盖最旧原文;只有NORMAL可能被编号窗口阻塞;条目超过总容量仍为ERROR |
| AsduQueue.last_confirmed_normal_serial | 替换为overwritten_unacked_serial_begin,作为覆盖待ACK区间下界;该控制成员语义改变,版本迁移须重新初始化 |
| AsduQueue及Status只记录原文条目 | 增加overwritten_unacked_count;Queue另保存覆盖和确认身份边界,结构布局改变 |
| AsduHeadInfo借用可跨任意入队保留 | NORMAL后续入队可能覆盖该原文,覆盖后借用立即失效 |
| Peek_Head后构帧并立即移除队首 | Manager内部Peek_Unsubmitted、Mark_Submitted;HIGH提交时出队,NORMAL有效ACK才Confirm_Next_Normal_Entry |
| IEC104_SendBufferNode_t数组 | IEC104_SendBufferControlFrame_t数组;I APDU已内置Buffer |
| Buffer.Init(buffer,nodes,capacity,reserve,generation) | Init(buffer,controls,control_capacity,generation),删除reserve参数 |
| BufferStatus三个序号位置及未确认数 | Link.Get_Send_Progress;BufferStatus仅当前I、控制数和预留状态 |
| Buffer.Apply_Peer_Ack | 删除;统一Manager.Handle_I_S_Frame处理 |
| Manager.Queue_Next_I_Frame(...,record) | Prepare_Next_Send(...,head_info),成功返回OK,紧邻实际发送调用 |
| 发送前只Buffer.Peek_Send_Head | Manager.Prepare_Next_Send;Buffer.Peek仍仅查询已准备帧 |
| Manager.Init只允许空业务队列 | 允许非空未编号队列,成功绑定会清空HIGH、保留NORMAL;拒绝仍有NORMAL待确认条目的队列 |
| Manager.Abort同时丢弃业务队列 | 保留业务队列;显式Rebind清空HIGH、恢复NORMAL,或Queue.Abort全部丢弃 |
| Enqueue_Ex及逐条重连策略 | 删除Enqueue_Ex、IEC104_AsduReconnectPolicy_t和HeadInfo.reconnect_policy;HIGH固定清空,NORMAL固定保留,统一调用Enqueue |
| Rebind逐条筛选和原地整理 | 完整预检后清空HIGH、将NORMAL现存条目恢复未提交;不搬移原文,失败不改字节或控制状态 |
| HIGH原文留到ACK | Mark_Submitted成功时立即出队;HIGH未确认数量改由Manager.high_unacked_count维护,ACK不调用Queue.Confirm_Next_Normal_Entry |
| STARTDT只改变链路状态 | 从停用状态START时清理HIGH及配额;已启用时重复START保留,WAIT未受理不清理;启停立即生效、CON完成不回写状态 |
| 应答在任意启停阶段都可缓存HIGH | Submit_Response及Enqueue(HIGH)共用准入白名单;新STOP受理时清空待发HIGH,已提交I帧和记录继续排空 |
| 仅靠user_tag找恢复数据 | 已删除user_tag;NORMAL用generation和asdu_entry_serial关联保留原文,HIGH只提供frame_id标识的帧回执 |
Enqueue参数顺序为queue、generation、priority、asdu、length;旧Enqueue_Ex调用须删除策略和user_tag参数,并按业务生命周期选择HIGH或NORMAL。HIGH头为uint16长度,NORMAL头为编号、状态和长度结构,须分别按对应头长宏计算容量。跨尾部存储不改变Manager.Prepare_Next_Send及其返回的连续APDU视图,直接读取AsduHeadInfo原文的外部代码须按两段接口使用。Handle_Send_Progress、Handle_I_S_Frame、控制帧封装及记录消费的函数签名保持;结构布局和来源身份语义按本文更新。
### 6.3 当前边界
已经实现发送数据结构、编号调度、部分写入记账、ACK释放、回执、超时判定和显式恢复基础。Receiver提供每连接独立分帧、代次检查和完整APDU交付;旧全局接收入口作为历史代码保留,不参与TcpServer路径。Connection统一收发分派、U帧路由及期限推进;实际网络操作经Net后端执行,TCP连接管理和周期驱动由[TcpServer](IEC104_TcpServer_设计架构.md)负责。应用持续调用TcpServer_Poll/NextEvent,通过TcpServer_Enqueue_Event或TcpServer_Send_Response提交ASDU;底层发送模块由TCP内部驱动,没有后台自动同步。
@@ -0,0 +1,529 @@
# IEC104 编解码与独立接收器设计架构
> **用途**:说明 `IEC104_Codec.c` 的APCI解析与I/S/U帧构造、`IEC60870_Codec.c` 的101/104公共ASDU头解析,以及 `IEC104_Receiver.c` 的每连接流式成帧、完整报文交付和错误处理。
> **范围**:第一至九章描述基础模块现有实现;第十章说明后续集成设计,其中单当前I帧、独立S/U队列、两级ASDU字节环及发送调度接口已实现,其余能力以各项说明为准。`IEC104_Link.c` 的状态和算法见配套文档 [IEC104_Link 设计架构](IEC104_Link_设计架构.md)。本文不是 IEC 60870-5-104 标准全文,也不定义具体 Type ID 的信息对象业务。
> **实现依据**:`IEC104_Codec.c/h`、`IEC60870_Codec.c/h`、`IEC60870_Types.h`、`IEC104_Receiver.c/h`及旧 `IEC104.c/h`;历史验证记录按当时版本保留。
> **同步日期**:2026-09-23。发送模块已改为变长ASDU队列、发送前编号及单当前I帧。严格警告编译、原有119个用例及临时专项校验通过;测试源码、配置和测试文档保持原样。
> **当前接口同步**:2026-10-08。已新增 `IEC104_Receiver.c/h`,并从删除前备份原样恢复 `IEC104.c/h`及旧全局接口,两套接收实现共存。上面的2026-09-23同步说明为历史记录;本文主要描述Receiver,旧入口边界另行注明。
> **测试清理**:2026-10-19。接口过期的旧回归测试及专用辅助文件已删除,当前测试入口及覆盖范围见[测试说明](../test/IEC104测试文档.md)。
> **信息对象同步**:2026-10-21。`IEC60870_InformationObjects.c/h`提供12种监视、18种命令的独立对象编解码;`IEC60870_Asdu.c/h`统一维护公共头、VSQ/SQ、容量、对象分派及原文应答。编码经对象函数表,解码按Type ID分派;带时标类型复用基础值处理及公共CP56工具。该模块由业务显式调用,不改变Receiver准入或链路序号处理;实际总召、校时和遥控流程仍待业务接入。接口及示例见[README](README.md#asdu信息对象模块)。
> **TCP接入同步**:2026-10-15。`IEC104_Connection.c/h`负责子站连接调度;`IEC104_TcpServer.c/h`提供IPv4监听及非阻塞收发,Windows依赖在Net后端。业务通过Create取得静态池不透明句柄,使用Init、Prepare/Process或Poll、NextEvent、Enqueue_Event/Send_Response、Get_Status/Get_Remote_Address;Close_Connection只断开指定代次主站,Close整个服务后读空事件再Destroy。公开头文件只依赖`IEC104_Types.h`及标准类型。构建和业务接入见 [TCP服务设计](IEC104_TcpServer_设计架构.md)。
> **子站入口同步**:2026-10-23。`IEC104_Slave`统一管理服务、调度和应答,NextEvent自动提供ASDU视图及原连接接口;业务无需手动绑定generation。延迟任务通过Copy_Event复制原文并重建自引用,旧请求在重连、重新Init和服务槽复用后仍被隔离。共享TCP值类型已迁入`IEC104_TcpTypes.h`。
> **业务边界**:子站业务从Slave接入,Slave内部调用TcpServer。本文的Receiver、Connection、SendManager及链路调用说明面向内部实现;业务不创建或驱动这些对象,不持有内部借用视图。业务可使用Codec和公共时间工具处理NextEvent返回的ASDU副本。
> **连接事件**:NextEvent分别交付CONNECTED、ACTIVATED、DEACTIVATED和DISCONNECTED。子站激活/停用在START/STOP受理时产生,使用data.connection载荷并按序保存;CON完整写出不再改变状态。同状态重复ACT及单纯断开不补造启停事件。
> **通知容量**:按实际载荷长度存放于字节环;默认保证普通30条最长通知、启停4条、收尾32条记录加1个最长I帧和1个断开通知,均不覆盖。启停配额满时延后受理改变状态的ACT,已有CON继续发送,NextEvent消费后安排重试。普通I帧无处交付时保留原帧并暂停续收;回执因空间或预算不足可晚于启停通知交付,各类背压不延长协议期限。
> **队列监控同步**:2026-10-16。Get_Status新增high_queue/normal_queue数值快照,分别查询待发条目数、保留原文条目数及总容量/已用/空闲字节数。通过asdu_queue_status_valid判断统计有效性;Init成功后离线仍有效,Close后无效。待发数不含已交给发送模块的ASDU,NORMAL等待ACK的保留原文仍占用存储,字节数包含条目头。业务继续只使用TcpServer,公开状态结构扩展后须重新编译调用方;详见[ASDU队列积压查询](IEC104_TcpServer_设计架构.md#21-asdu队列积压查询)。
---
## 一、模块速览
### 1.1 总体职责
`IEC104_Codec.c` 负责104完整APDU的APCI检查及I/S/U编码;`IEC60870_Codec.c` 负责101/104共用的参数校验、ASDU公共头解析及最低载荷长度检查。两者均不保存连接状态,公共Codec不依赖104实现。
`IEC104_Receiver.c` 为每条连接提供独立的固定接收缓冲,接受任意长度的有序字节片段。每次Feed最多收齐一帧,通过consumed报告实际消费的输入,APCI检查成功后保留完整帧,供调用方读取和显式释放:
~~~text
IEC104_TcpServer.recv提供有序字节,经Connection.Feed递入接收器
→ Receiver.Feed:同步、长度检查、收集APDU、调用104 Parse_APCI和公共Parse_ASDU_Header
→ FRAME_READY:停止消费后续输入
→ Receiver.Peek_Frame:取得APDU及APCI只读视图
→ 连接层按类型分派:子站I/S交给SendManager,U交给对应控制入口
→ S/U受理后释放;I帧由TcpServer复制为事件后经Connection释放
→ 再递入剩余字节;业务通过TcpServer.NextEvent读取事件副本
~~~
Receiver已经提供多实例、半包/粘包处理、完整帧交付、代次检查及待处理帧保留。它不检查链路序号,不修改Link,不预设STARTED,仅检查ASDU公共头及最低载荷长度,不执行对象或业务校验,也不执行socket、系统定时器或业务回调。
同连接的接收、发送完成及ACK处理由`IEC104_Connection`串行组织。`IEC104_TcpServer`负责TCP监听调度、收发预算、断线收尾和事件队列,真实网络操作通过Net后端执行;Connection负责I/S/U统一分派及协议期限。业务通过`IEC104_Slave_NextEvent`取得ASDU及应答接口并校验具体Type ID;Slave内部经`IEC104_TcpServer_Send_Response`提交应答。
### 1.2 文件地图
| 文件 | 职责 |
|---|---|
| `IEC104_Codec.c/h` | 104专用APCI解析、I/S/U帧构造和帧类型 |
| `IEC60870_Codec.c/h` | 101/104共用ASDU公共头解析、参数及最低载荷长度检查 |
| `IEC60870_Types.h` | 公共应用层参数和ASDU存储上限,无传输状态 |
| `IEC60870_Asdu.c/h` | 通用ASDU容器、精确布局、VSQ/SQ、地址和容量检查、取对象及原文应答 |
| `IEC60870_InformationObjects.c/h` | 每类独立结构、Create与Get_From_Buffer;函数表编码、Type ID解码分派,复用基础类型和CP56 |
| `IEC104_Receiver.c/h` | 每连接独立流式成帧、完整APDU借用视图、消费数和显式释放;依赖104 Codec和公共Codec |
| `IEC104.c/h` | 已恢复的旧单路逐字节接收入口,持有全局接收器和私有Link,仅返回接收结果 |
| `IEC104_Link.c`、`IEC104_Link_Common.h` | 连接层持有的公共I/S、窗口、确认及TESTFR状态;角色启停分别在Master_StartStop、Slave_StartStop模块 |
| `IEC104_Link.h` | 兼容总头文件;角色接口和构建迁移见 `README.md` |
| `IEC104_SendBuffer.c/h` | 子站唯一当前I帧、紧凑S/U FIFO、部分写入与本地发送期限;协议序号由Link独占 |
| `IEC104_AsduQueue.c/h` | HIGH/NORMAL变长字节环,条目可跨尾部、无填充碎片;HIGH满时拒绝、提交后出队,NORMAL覆盖最旧原文;Rebind清空HIGH、恢复NORMAL现存原文 |
| `IEC104_SendManager.c/h` | 子站发送前选择及编号、I/S接收、完成分派、ACK释放原文、独立回执和I帧t1;由Connection统一驱动 |
| `IEC104_Connection.c/h` | 每连接持有Receiver、共享Link和发送模块,统一分派、超时推进及故障收尾 |
| `IEC104_Types.h` | 协议常量、ASDU优先级及公开协议配置,不包含运行对象 |
| `IEC104_TcpServer.c/h` | IPv4单主站监听调度、静态池不透明句柄、收发预算、事件/状态/对端地址查询及单连接关闭;通过Net适配平台 |
| `IEC104_TcpServer_Config.h` | 对象池及内部缓冲的编译期容量,不由业务头文件转发 |
| `IEC104_Log.c/h` | 日志级别、格式化、回调或标准流输出 |
| `iec104_tcp_event_ring_test.c` | 当前TCP事件字节环专项,覆盖容量、次序、跨环尾及背压恢复 |
| `iec104_tcp_scheduler_test.c` | ACK/TESTFR确认边界、固定输入快照、独立发送预算及真实超时 |
| `iec104_net_pending_input_test.c` | Windows回环验证内核输入字节数快照、部分读取及EOF |
| `IEC104测试文档.md` | 当前测试的构建方法、覆盖范围和验证记录 |
依赖方向为:
~~~text
IEC104_Receiver.c ──→ IEC104_Codec.c(APCI)/ IEC60870_Codec.c(ASDU公共头)
IEC104.c ──→ IEC104_Codec.c / IEC104_Link.c / IEC104_Log.c(旧单路入口)
IEC104_Link.c / Master_StartStop.c / Slave_StartStop.c
└──────→ IEC104_Codec.c(调用IEC104_Build_*编码函数)
IEC104_SendBuffer.c ──→ IEC104_Codec.c(提交报文时校验APCI)
IEC104_TcpServer.c ──→ IEC104_Connection.c ──→ Receiver / SendManager / Link
应用层 ──→ IEC104_Slave.h(不透明子站句柄、自动ASDU/应答接口)
IEC104_Slave.c ──→ IEC104_TcpServer.h(不透明TCP句柄、公开配置/事件/状态)
IEC104_TcpServer.h ──→ IEC104_Types.h(仅值类型,不包含Connection或Net)
Receiver / Link / AsduQueue的内部模块头文件 ──→ IEC104_Codec.h
IEC104_Codec.c / IEC60870_Codec.c ──→ C标准库
IEC60870_Asdu.c / IEC60870_InformationObjects.c ──→ IEC60870_Codec.c / IEC60870_Time.c
IEC60870_Codec.h / IEC60870_InformationObjects.h ──→ IEC60870_Types.h
IEC104_Log.c ──→ 平台时间与标准I/O,不依赖协议模块
~~~
独立成帧需编译 `IEC104_Receiver.c`、`IEC104_Codec.c` 和 `IEC60870_Codec.c`。仅检查104 APCI或构帧使用104 Codec,仅解析ASDU头使用公共Codec;Link及发送模块继续按README选择源文件。
旧 `IEC104.c/h`已原样恢复,原有调用方可继续包含 `IEC104.h`;该头文件仍包含Codec声明。旧入口保留全局接收器、私有Link和STARTED预设。独立流式接收使用 `IEC104_Receiver.h`及显式对象接口,由连接层管理共享Link;两套入口不互相转发报文或共享接收状态。
### 1.3 协议与缓存边界
```text
字节下标 0 1 2 3 4 5 6 ...
┌──────┬────────┬──────────────────────────────┬──────────┐
字段 │ 0x68 │ 长度 L │ 4字节控制域 │ ASDU │
└──────┴────────┴──────────────────────────────┴──────────┘
```
- 完整 APDU 长度的计算公式为 `2 + L`。
- I 帧 ASDU 长度的计算公式为 `L - 4`,其中4是控制域长度。
| 项 | 当前值 | 含义 |
|---|---:|---|
| 起始字节 | `0x68` | APDU 同步标志 |
| 长度域 L | 1 字节,数值为 `4..253` | 仅计算控制域和 ASDU,不包含起始字节及长度域自身 |
| 控制域 C0..C3 | 共 4 字节 | I/S 帧编码序号,U 帧编码功能码 |
| ASDU | 最大 249 字节 | I 帧携带;S/U 帧不携带 |
| ASDU 公共头 | 104为6字节 | Type ID、VSQ、COT、OA和CA;公共ASDU层按配置计算为4..6字节 |
| APDU 最大长度 | 255字节 | 2字节头部加253字节长度域内容 |
### 1.4 使用硬约束
1. 每条连接使用独立Receiver,先用非零代次Init;仅清零对象不等于完成初始化。同一对象的所有操作串行,保持输入顺序。
2. 输入字节、输出对象及Receiver存储不得重叠。Feed的consumed指针必须有效,length非0时data必须有效。
3. Feed每次最多交付一帧;未消费的输入尾部由调用方保留。FRAME_READY后未释放时再次Feed消费0字节。
4. Peek返回的apdu及apci.asdu借用Receiver内部缓冲。成功Release、Reset或Init后视图失效;持有视图期间不得搬移对象或修改原文。
5. 所有接口按枚举值判断。FRAME_READY说明APCI及ASDU最低长度检查通过,不说明具体对象、业务字段、N(S)/N(R)或连接启停状态合法。
6. 同一帧的I/S序号及ACK只交给同连接SendManager一次,不再另调底层Link重复提交。U帧由连接层按角色和功能码路由。
7. 返回WAIT时保留当前帧及接收顺序;已受理的QUEUED/IGNORED等结果按接口契约消费,不能重放。
8. 新连接复用对象前须结束旧I/O,再用未复用的非零代次Init;旧代次的输入、查询、释放和复位均被拒绝。
### 1.5 状态归属与数据生命周期
| 对象 | 所有者 | 内容及有效期 |
|---|---|---|
| `IEC104_Receiver_t` | 调用方,每连接一个实例 | 255字节缓冲、已收/预期长度、待帧标志及连接代次;跨调用保留半帧或完整帧 |
| `IEC104_LinkState_t` | 连接层,供SendManager和U帧处理共享 | 序号、窗口及启停状态;Receiver不持有副本、不修改它 |
| `IEC104_ReceivedFrame_t` | 调用方的输出对象 | 保存代次、APDU指针/长度及APCI结果;指针借用Receiver缓冲 |
| `IEC60870_AsduInfo_t` | 调用方 | 公共头字段及信息对象区视图,有效期不超过原ASDU |
| 构帧输出缓冲 | 调用方或SendBuffer | 保存已构造报文,由发送管理及传输层推进 |
Receiver不缓存指向自身缓冲的APCI指针;Feed完帧检查APCI,Peek重新解析到局部结果后一次性输出。因此移动对象不会留下内部悬空指针,但已经交付的借用视图仍要求原对象地址保持不变。
Init清零整个对象并绑定代次;Release和Reset只清除组帧进度,不擦除缓冲。即使物理字节仍在,释放后的旧视图也不可继续使用。
---
## 二、报文模型与 APCI(应用协议控制信息) 校验
### 2.1 APDU(应用协议数据单元) 布局
```text
偏移: 0 1 2 3 4 5 6 ...
+------+------+------+------+------+------+----------------+
| 0x68 | L | C0 | C1 | C2 | C3 | ASDU(可选) |
+------+------+------+------+------+------+----------------+
| APCI | ASDU |
| APDU |
完整 APDU 长度 = 2 + L
L = 4 字节控制域 + ASDU 长度
```
`IEC104_Parse_APCI()` 首先统一检查:
- `frame` 和输出参数 `apci` 非空;
- 总长度不少于 6 字节;
- 起始字节等于 `0x68`;
- `L` 位于 `4..253`;
- 调用方给出的 `frame_len` 严格等于 `2 + L`。
### 2.2 帧类型识别
`IEC104_Get_Frame_Type(C0)` 只按控制域首字节的低位分类:
| 分类条件 | 结果 |
|---|---|
| `(C0 & 0x01) == 0` | I 帧 |
| `(C0 & 0x03) == 0x01` | S 帧 |
| 其他 | U 帧候选 |
分类只是第一步。候选类型随后还必须通过对应的完整控制域校验,不能把任意“其他值”当作合法 U 帧。
### 2.3 三类控制域规则
| 类型 | 长度约束 | 控制域约束 | 解析结果 |
|---|---|---|---|
| I 帧 | `L >= IEC104_MIN_LENGTH_FIELD + IEC104_ASDU_HEADER_LEN`(当前为10) | `C0.bit0=0`、`C2.bit0=0` | `N(S)=(C1:C0)>>1`,`N(R)=(C3:C2)>>1`,ASDU 长度=`L-4` |
| S 帧 | `L == 4` | `C0=0x01`、`C1=0x00`、`C2.bit0=0` | `N(R)=(C3:C2)>>1`,无 ASDU |
| U 帧 | `L == 4` | `C0` 为六种允许功能码之一,`C1..C3=0` | 输出 U 功能码,无 ASDU |
允许的 U 帧功能码:
| 功能 | ACT | CON |
|---|---:|---:|
| STARTDT | `0x07` | `0x0B` |
| STOPDT | `0x13` | `0x23` |
| TESTFR | `0x43` | `0x83` |
> 当前模块只校验并报告 U 功能码,不改变数据传输启停状态,也不会自动生成对应 CON 帧。
### 2.4 I/S/U帧构造
`IEC104_Build_Iframe()`执行APCI解析的反向操作:调用方传入`N(S)`、`N(R)`和完整ASDU,函数将起始字节、长度域、四字节控制域及ASDU写入输出缓冲区,并返回完整APDU长度。
该函数是无状态编码接口:不读取或修改`IEC104_LinkState_t`,不检查发送窗口,也不调用网络发送函数。调用方必须提供至少`IEC104_APCI_LEN + asdu_len`字节的输出空间。实际连接通过下述链路 Prepare/Complete 配对管理序号,不能单独构帧后伪造一个完成快照。
`IEC104_Build_Sframe()`根据调用方提供的`N(R)`构造固定6字节S帧:
```text
encoded_receive = (uint16_t)(recv_seq << 1)
68 04 01 00 encoded_receive低字节 encoded_receive高字节
```
写入控制域前,`N(R)`先左移1位。`buf`为空或`N(R)>32767`时返回`-1`,成功时返回`IEC104_APCI_LEN`。
`IEC104_Build_Uframe()`接收`int function`,使用`IEC104_UFrameFunction_t`定义的六种功能码构造固定6字节U帧:
```text
68 04 功能码 00 00 00
```
功能码仅允许`STARTDT_ACT/CON`、`STOPDT_ACT/CON`和`TESTFR_ACT/CON`。`buf`为空或功能码非法时返回`-1`且不写入缓冲区,成功时返回`IEC104_APCI_LEN`。
链路模块内部已检查非空缓冲及至少 6 字节容量,并传入固定合法功能码,因此这些调用使用 `(void)IEC104_Build_Uframe(...)` 显式忽略固定返回值。编码仍执行,公开函数仍返回 `int`;构造成功只代表缓冲已写入,不代表网络发送成功。对外部传入的未验证参数仍须按返回值处理。
基础构造函数只生成字节数组,不决定何时发送,也不调用网络接口。连接发送 I 帧应使用 `IEC104_Link_Prepare_I_Frame()`,由它在构帧成功时预留 `N(S)`;完整发送后把原 `IEC104_IFrameTx_t` 快照和最终发送长度传给 `IEC104_Link_Complete_I_Send()`。普通 S 确认则使用 `IEC104_Link_Prepare_S_Ack()` 与 `IEC104_Link_Complete_S_Ack_Send()` 配对。S/U帧调用方必须提供至少6字节输出空间。
构帧参数与返回值汇总:
| 接口 | 输入边界 | 成功返回 | 失败返回 |
|---|---|---|---|
| `IEC104_Build_Iframe()` | `buf/asdu` 非空,`N(S)/N(R)` 为 `0..32767`,`asdu_len` 为 `6..249` | APDU 长度 `6 + asdu_len`,范围 `12..255` | `-1` |
| `IEC104_Build_Sframe()` | `buf` 非空,`N(R)` 为 `0..32767` | `6` | `-1` |
| `IEC104_Build_Uframe()` | `buf` 非空,`int function` 的原值等于六种U帧功能码之一 | `6` | `-1` |
I 帧的序号先左移一位,再按低字节、高字节顺序写入控制域;ASDU 使用 `memcpy()` 原样复制,不验证其公共头或信息对象语义。输入 ASDU 与输出报文应使用不重叠的存储区域。所有构造接口都没有输出容量参数,无法检查调用方实际分配的空间。
例如,`N(S)=1`、`N(R)=2`、ASDU 为 `01 01 03 00 01 00` 时,I 帧构造结果为:
```text
68 0A 02 00 04 00 01 01 03 00 01 00
```
其中 `02 00` 编码 `N(S)=1`,`04 00` 编码 `N(R)=2`。该例仅用于验证字节编码,六字节 ASDU 只有公共头,不代表包含完整业务对象。APCI解析与I帧构造的ASDU长度范围一致,均为`IEC104_ASDU_HEADER_LEN..IEC104_MAX_ASDU_LEN`,当前为`6..249`字节。
U帧编码入口及内部功能码校验函数均使用`int`参数,先按完整整数值检查六种合法功能码,再转换为`uint8_t`写入控制域。启用短枚举时也会拒绝`0x107`等非法值,避免误生成`STARTDT_ACT`。调用方应直接传入待校验整数或合法枚举常量;若提前转换为短枚举或`uint8_t`,已丢失的高位无法由编码函数恢复。公开参数类型已调整,调用方及依赖该签名的函数指针须使用新声明并重新编译。
---
## 三、流式接收状态机
### 3.1 状态定义
阶段由received_length及is_frame_ready推导,不另存一份stage枚举。
| 状态 | 不变量 | 输入处理 |
|---|---|---|
| 等待帧头 | received=0、expected=0、ready=0 | 首字节必须为0x68;否则消费该字节并返回FRAME_ERROR |
| 等待长度 | received=1、expected=0、ready=0 | 长度必须4..253;合法后expected=2+L、received=2 |
| 收集报文体 | 2≤received<expected≤255、ready=0 | 批量复制当前帧所需字节,不消费下一帧 |
| 完整帧待处理 | received=expected、ready=1 | Feed返回FRAME_READY且consumed=0,等待显式释放 |
### 3.2 完帧处理
Feed收齐APDU后调用Parse_APCI,I帧再调用Parse_ASDU_Header检查公共头及已知固定长度对象的最低载荷长度。任一步失败都清空当前进度并返回FRAME_ERROR;全部通过后才设置待帧标志并返回FRAME_READY,保持APDU原文。
Peek只在待帧状态下输出 `IEC104_ReceivedFrame_t`。它复用相同的格式及最低长度检查并重建APCI视图,失败或EMPTY时不修改调用方输出。接收器不调用Link、SendManager或业务代码,不提交任何序号或ACK。
### 3.3 错误恢复边界
- 每个APDU边界的首字节必须为0x68;遇到其他值立即消费该字节、清除进度并返回FRAME_ERROR,不扫描后续字节重新同步。Connection登记FRAME故障,TcpServer随后断开连接。
- 长度非法时消费该长度字节,丢弃当前候选帧,停止本次Feed;剩余输入不被消费。
- 完整APCI非法、ASDU公共头不足或已知固定长度对象被截短时,消费到声明的候选帧末尾,丢弃候选帧,停止本次Feed;不处理该帧的序号或累计ACK。
- 报文体按已声明长度收集,内部0x68属于数据;不会在半帧中搜索新帧头。半帧超时由连接层处理。
- 参数、代次或内部进度非法返回ERROR,不修改接收器;有效的consumed输出被置0。
- Receiver_Reset只核对初始化及代次,允许显式丢弃异常组帧进度。是否继续格式错误后的连接由接入方决定;链路FATAL不能靠复位接收器继续旧会话。
### 3.4 接收结果与调用方责任
| 结果 | 值 | 含义及处理 |
|---|---:|---|
| ERROR | -2 | 参数、代次或状态错误;consumed=0,修正调用或结束故障连接 |
| FRAME_ERROR | -1 | 起始字节、长度/APCI或ASDU最低长度错误;候选帧已清除,consumed标明停止位置 |
| IN_PROGRESS | 0 | 全部输入已消费,尚无完整帧;继续输入后续字节 |
| OK | 1 | Init、Reset、Release_Frame或Get_Status成功 |
| FRAME_READY | 2 | 有完整帧,通过APCI及ASDU最低长度检查;Peek、分派、受理后Release |
| EMPTY | 3 | Peek/Release时没有完整帧;半帧保留,查询输出不变 |
表中省略 `IEC104_RECEIVER_` 前缀,不能按布尔值判断。data=NULL、length=0是合法的Feed查询;其他情况下data必须指向length字节有效输入。
### 3.5 状态校验与复位
Receiver_Valid在读写前核对初始化、代次、待帧标志,以及帧头、长度域、expected_length和received_length的一致性。报文体复制长度取“本帧剩余长度”和“本次剩余输入”的较小值,因此不会将粘包中的后续帧写入当前缓冲。
| 操作 | 当前半帧/完整帧 | 连接代次 | Link状态 |
|---|---|---|---|
| Init | 全部清空并使旧视图失效 | 绑定非零参数 | 不操作 |
| Release_Frame | 有完整帧才清空进度;半帧返回EMPTY | 保留 | 不操作 |
| Reset | 丢弃当前进度和待处理帧,保留缓冲字节 | 必须匹配并保留 | 不操作 |
| Get_Status | 只读返回已收长度、期望长度及完整帧标志 | 必须匹配并保留 | 不操作 |
| Abort | 清空全部状态及缓冲,使旧视图失效 | 解除绑定,须重新Init | 不操作 |
| 旧代次调用 | 不修改 | 不修改 | 不操作 |
`Get_Status`复用完整进度校验,OK时才写数值快照,不重新解析或分派报文。Connection据此区分本次新成帧和此前保留的帧。`Abort`用于已结束旧I/O及所有原文借用后的收尾,允许NULL;它不核对代次,Connection在调用前负责检查身份,不能拿旧事件直接清理新接收器。
### 3.6 分段输入与待帧保留
调用方为当前网络输入保存offset;每次Feed后只增加consumed,不丢弃尾部。FRAME_READY时先Peek并分派,即使offset已经到输入末尾,也必须处理这帧。
如果分派暂时返回WAIT,保留当前Receiver帧及尚未消费的输入尾部,并暂停该连接的后续分派。条件满足后重试当前帧;一旦受理,Release后从原offset继续递入。禁止在待帧状态下仅重复Feed形成空转。
Receiver只提供一个待处理帧槽。未消费输入的容量和网络读取背压由连接层负责;无额外接收FIFO、动态分配或后台任务。
---
## 四、ASDU 公共头解析
### 4.1 当前实际调用关系
Receiver.Feed调用Parse_APCI校验帧格式,I帧还调用Parse_ASDU_Header检查最低载荷长度;Peek_Frame返回完整APDU、已解析APCI和ASDU视图。两次检查都在Link/Manager受理之前完成。ASDU公共头解析结果不缓存在接收器中,业务仍按需解析事件副本。
~~~text
Receiver.Feed → Parse_APCI → I帧调用Parse_ASDU_Header → FRAME_READY → Peek_Frame
→ 连接层:I/S交给SendManager,U交给对应控制接口
→ I帧受理后:TcpServer复制I_FRAME事件 → 经Connection调用Release_Frame
业务:TcpServer_NextEvent取得副本 → Parse_ASDU_Header → Type ID业务处理
~~~
Parse_APCI要求I帧ASDU长度至少为IEC104_ASDU_HEADER_LEN,当前为6字节;接收器随后通过Parse_ASDU_Header检查已知固定长度类型的载荷。任一步失败都报告FRAME_ERROR,不进入链路序号及累计ACK处理,也不交付I_FRAME。apci.asdu指向APDU偏移6,asdu_len为长度域减4。业务仍须检查具体字段和对象内容;业务检查失败不能回滚已经由链路接受的传输序号。Parse_APCI也用于发送缓冲,仍只承担APCI及公共头最小空间检查;发送构帧不调用ASDU载荷检查。
### 4.2 可配置公共头与104默认布局
公共头解析现在必须显式传入`IEC60870_AppLayerParameters_t`,不保留原三参数签名。COT和CA分别支持1/2字节,IOA支持1/2/3字节,头长为`2+sizeOfCOT+sizeOfCA`。以下图表描述本工程Receiver显式使用的104标准配置(COT/CA/IOA=2/2/3、最大249);通用ASDU解析也可使用其他合法配置。
```text
偏移: 0 1 2 3 4 5 6 ... 末尾
+------+------+------+------+------+------+----------------+
| TI | VSQ | COT | ORG | CA | 信息对象区 |
+------+------+------+------+------+------+----------------+
| ASDU |
```
| ASDU 偏移 | 字段 | 当前解析规则 |
|---:|---|---|
| 0 | Type ID | 原值输出;已知固定长度类型参与最低载荷检查,未知类型不因此被拒绝 |
| 1 | VSQ | bit7 → `is_sequence`;bit0..6 → `object_count` |
| 2 | COT | bit7 → `is_test`;bit6 → `is_negative_confirm`;bit0..5 →传送原因 |
| 3 | ORG | 源发地址原值输出 |
| 4..5 | CA | 小端组合为 16 位公共地址 |
| 6..末尾 | 信息对象区 | 输出首地址视图和剩余长度 |
函数接受所选头长至`maxSizeOfASDU`字节的ASDU(通用上限254),并在写输出前检查已知固定长度对象所需的最低载荷长度。COT为1字节时没有OA,结果OA为0;CA从偏移`2+sizeOfCOT`开始,信息对象区从所选头长开始。参数或长度失败返回0,输出保持不变。它不判断是否支持该Type ID,也不验证传送原因、公共地址或信息对象内容是否合法。参数与原报文均为借用,视图使用期间必须有效且不变。104传输层仍只接受标准配置及6..249字节ASDU。
`is_sequence`、`is_negative_confirm`、`is_test` 输出为 `0/1`,不是保留原掩码值。SQ参与最低长度计算,但不会自动展开连续信息对象地址。信息对象区长度为零时,其指针位于公共头之后,不得解引用。
### 4.3 已知类型的最低载荷长度
设IOA宽度为L=`parameters->sizeOfIOA`(104接收器传入3)、VSQ对象数量为N、单个对象除IOA以外的数据长度为D,公共头后的最低载荷为:
- SQ=0:`N × (L + D)`;
- SQ=1:`L + N × D`。
固定长度表覆盖Type ID `1..21`、`30..41`、`45..51`、`58..64`、`70`、`100..107`、`110..113`、`120..124`和`126..127`。例如104配置下单点命令45、N=1时,6字节公共头后至少还须有3字节IOA和1字节命令;只有公共头的声明长度在链路受理前被拒绝。读命令102的数据部分为零字节,但仍按配置检查IOA是否完整。
这是最低长度检查:未知/变长类型(包括文件段125)及N=0不通过该表限制;额外尾字节、某类型是否允许SQ或多个对象、字段取值和业务支持范围继续由业务检查。长度字段仍决定APDU边界,不根据后续字节猜测或补齐被截短的命令。
ASDU长度检查接入Receiver,保证格式错误在序号和累计ACK受理之前被发现。
---
## 五、与链路模块的接口边界
Receiver与Link独立,连接层持有一份LinkState供发送和接收共同使用,不存在接收器私有序号状态。链路必须显式初始化角色并通过既有启停流程推进;接收器不会将其预设为STARTED。
| 帧类型 | 子站连接层的处理位置 | 释放接收帧的时机 |
|---|---|---|
| I | `SendManager_Handle_I_S_Frame` | 成功受理且TcpServer已复制I_FRAME事件后,由Connection释放 |
| S | `SendManager_Handle_I_S_Frame` | 成功受理后,由Connection释放 |
| STARTDT_ACT / STOPDT_ACT / TESTFR_ACT | 对应Manager入口 | 根据各接口返回值确认已受理后 |
| TESTFR_CON | `Link_Handle_Testfr_Con` | 当前事件已处理或按契约忽略后 |
| 角色不接受的U功能 | 连接层按角色策略处理 | 处理完成或连接终止 |
控制入口WAIT表示尚未受理,必须保留当前帧;成功受理后释放控制帧。对端ACK必须晚于对应本地发送完成的提交;Receiver仅保留接收顺序,不代替发送完成记账。
旧 `IEC104_Legacy_Link_Reset`及其私有Link已恢复,该接口只复位旧入口的I/S状态并恢复STARTED预设,不改变旧接收进度。独立Receiver的接收复位调用Receiver_Reset;指定连接的Link初始化/复位继续使用Link公共接口。链路状态、超时及错误收尾约定见 [链路设计](IEC104_Link_设计架构.md)。
---
## 六、公开接口契约
104帧接口位于 `IEC104_Codec.h/.c`,公共ASDU头与参数检查位于 `IEC60870_Codec.h/.c`,独立接收接口位于 `IEC104_Receiver.h/.c`。旧 `IEC104.h/.c` 提供原有逐字节接口;下表合并列出两个Codec和Receiver。
| 接口 | 状态性 | 成功结果 | 主要注意事项 |
|---|---|---|---|
| `IEC104_Get_Frame_Type(control)` | 无状态 | 返回 I/S/U 候选类型 | 只分类,不代表控制域合法 |
| `IEC104_Parse_APCI(frame,frame_len,apci)` | 无状态 | 返回 `1`,填充类型、序号、U 功能或 ASDU 视图 | 失败返回 `0`;基础参数检查失败不写输出,分类后的控制域检查失败可能已改写部分输出;不校验链路序号和 ASDU 语义 |
| `IEC104_Build_Iframe(buf,send_seq,recv_seq,asdu,asdu_len)` | 无状态 | 写入完整 I 帧,返回 `12..255` | 参数非法返回 `-1`;调用方保证输出空间;不修改链路状态 |
| `IEC104_Build_Sframe(buf,recv_seq)` | 无状态 | 写入固定 6 字节 S 帧,返回 `6` | 参数非法返回 `-1`;`N(R)` 范围为 `0..32767`;不标记接收已确认 |
| `IEC104_Build_Uframe(buf,function)` | 无状态 | 写入固定 6 字节 U 帧,返回 `6` | 按`int`原值校验功能码,失败返回`-1`且不写入缓冲区;不执行状态转换或自动应答 |
| `IEC60870_AppLayerParameters_Valid(parameters)` | 无状态 | 合法参数返回 `1` | NULL、非法字段宽度/OA/最大长度返回 `0` |
| `IEC60870_Parse_ASDU_Header(parameters,asdu,asdu_len,asdu_info)` | 无状态 | 返回 `1`,填充公共头、信息对象区及参数借用视图 | 参数或最低载荷长度检查失败返回 `0`,不修改输出;长度限制由配置决定 |
| `IEC104_Receiver_Init(receiver,generation)` | 初始化指定实例 | OK | 非零代次;新连接复用前先结束旧I/O |
| `IEC104_Receiver_Feed(receiver,generation,data,length,consumed)` | 更新指定实例 | FRAME_READY或IN_PROGRESS | 每次最多一帧;所有返回都按consumed推进输入;ERROR消费0 |
| `IEC104_Receiver_Peek_Frame(receiver,generation,frame)` | 只读 | FRAME_READY | 输出借用视图;EMPTY/ERROR不改输出 |
| `IEC104_Receiver_Release_Frame(receiver,generation)` | 释放待处理帧 | OK | 无完整帧返回EMPTY,保留半帧 |
| `IEC104_Receiver_Reset(receiver,generation)` | 丢弃接收进度 | OK | 保留代次,不操作Link;所有旧视图失效 |
| `IEC104_Receiver_Get_Status(receiver,generation,status)` | 只读 | OK | 数值快照不借用原文;参数、旧代次或进度非法返回ERROR且不改输出 |
| `IEC104_Receiver_Abort(receiver)` | 清除状态及绑定 | 无返回值 | 先结束I/O和借用;允许NULL,再使用须Init |
`IEC104_Link.c` 自身的公开接口不属于本文接口定义,详见 `IEC104_Link_设计架构.md`。
---
## 七、设计决策与考量
### 7.1 分段输入与显式释放
Feed兼容单字节和批量输入,报文体批量复制,每次停在一帧边界。显式Release将报文有效期与下一次输入分开,便于连接层处理控制队列满等暂时无法受理的情况,并保持同连接的严格分派顺序。
### 7.2 固定缓冲、零动态分配
每个Receiver内置255字节APDU缓冲,另有少量长度、代次和标志字段。无堆分配、接收FIFO或独立ASDU副本。Peek输出只读借用视图;需要长期保存的调用方自行复制数据。
### 7.3 先检查、后更新
参数、代次和内部进度在处理输入前检查,ERROR不修改接收器。FRAME_ERROR属于已经消费的输入格式错误,会丢弃候选帧并报告消费位置。Peek在局部结果完成APCI解析后才写调用方输出。Receiver不提前或重复提交Link状态。
### 7.4 接收复位与链路复位分离
Reset只丢弃指定Receiver的接收进度并保留连接代次;Link序号、启停和发送记录由各模块独立管理。重建连接用新代次Init,且必须先结束旧I/O。实例初始化不使链路进入STARTED。
### 7.5 日志独立
Receiver和Codec不依赖日志模块。连接层根据返回值决定日志级别和断连策略;现有 `IEC104_Log.c/h`可按应用需要继续使用,无需随接收器一并链接。
### 7.6 旧接口迁移
`IEC104.c/h`、`IEC104_Rx_Feed_Byte`、`IEC104_Rx_Reset`、`IEC104_Legacy_Link_Reset`及旧接收枚举/结构已从删除前备份原样恢复。旧入口仍编译IEC104.c、Codec.c、Link.c和Log.c;独立Receiver需要Receiver.c、IEC104_Codec.c和IEC60870_Codec.c。恢复旧文件不改变新接收器或发送模块。
内部接收实现迁移到独立Receiver时,由Connection持有Receiver并处理consumed、Peek及Release,TcpServer负责保留输入尾部和复制业务事件。应用的子站接入统一使用IEC104_Slave.h,门面内部使用TcpServer.h;只处理公共ASDU头的业务包含IEC60870_Codec.h,104帧工具包含IEC104_Codec.h。旧入口在成帧成功前包含I/S链路检查;新路径由Connection交给SendManager执行,不能仅重命名函数后继续假设序号已被校验。
接口过期的旧测试入口已删除;当前测试直接使用现有TcpServer实现,不提供测试兼容层。
## 八、变更记录
| 日期 | 变更 |
|---|---|
| 2026-09-03 | 初始版本:依据当前源码建立成帧、APCI/ASDU 解析、链路序号、接口契约、测试基线和已知边界设计说明 |
| 2026-09-03 | 拆分 `IEC104_Link.c` 设计:本文只保留调用边界和全局状态集成限制,链路状态、算法及测试迁移至独立文档 |
| 2026-09-03 | 更正 ASDU 调用关系:明确当前接收链路只调用 APCI 解析,`IEC60870_Parse_ASDU_Header()` 尚未接入且仅由测试直接调用 |
| 2026-09-04 | 增加I、S、U帧无状态构造接口及其参数、输出格式和职责边界说明 |
| 2026-09-07 | 链路模块增加普通S帧的 `w` 门限、立即确认、N(R)快照和发送完成接口;全局接收器与发送调度仍未接入该流程 |
| 2026-09-08 | 同步固定连接角色模型:全局旧入口默认被控站;连接故障只由外部 TCP 上下文管理,不在链路状态重复锁存;详细状态与接口契约见链路设计文档 |
| 2026-09-10 | 同步IEC104_Legacy_Link_Reset命名及两类复位边界,纠正I/S链路结果类型,补充U帧编码返回约定与当前测试状态;区分已有算法和待实现的连接调度 |
| 2026-09-17 | 将无状态编解码、协议常量和报文类型提取到IEC104_Codec.c/h;旧单路接收入口及其行为保留,IEC104.h继续提供原有声明,生产构建新增Codec源文件 |
## 九、当前验证与使用边界
本文描述当前源码行为,不等于已经完成端到端协议验证。当前保留的字节环专项、编译命令和覆盖范围集中维护在[测试说明](../test/IEC104测试文档.md);旧回归套件已删除,其历史通过数不计入当前验证。
链路层已提供 `sent_vs`、不可变 I 帧快照、固定角色启停、按受理顺序立即启停和独立 TESTFR。独立 `IEC104_SendBuffer` 已提供 唯一当前I帧、独立紧凑S/U FIFO、部分写入及本地发送期限;`IEC104_AsduQueue` 和 `IEC104_SendManager` 已提供变长业务排队、发送前编号、未ACK原文保留、I/S接收、完成分派、独立回执、I帧t1及显式重连恢复。Receiver已提供独立接收上下文、完整APDU交付和代次隔离;Connection已统一收发事件分派、U帧路由及协议期限,Windows TcpServer已提供实际I/O,并用Poll周期调用Tick,无须业务另建协议定时器。基础编码器只校验可见参数,U枚举截断边界见2.4节,不能宣称已经全面校验所有整数输入。
## 十、集成设计与接入边界
### 10.1 当前能力与职责分配
Receiver已实现每连接成帧和完整APDU交付;独立发送队列、业务队列及发送调度接口见 [发送模块统一设计](IEC104_发送模块_设计架构.md)。当前Windows子站接入由Connection、TcpServer和Net_Windows组成,业务持续Poll、通过NextEvent主动读取事件并按需排入ASDU;具体业务对象仍由上层实现。
| 能力 | 当前负责层 | 与现有实现的衔接 |
|---|---|---|
| TCP建连、读写、断连和重连 | TcpServer(已实现) | 将recv字节交给Connection;回报send实际写入量;断线后保留监听并接受新主站 |
| 每连接独立状态、统一错误收尾 | Connection(已实现) | 持有接收器、链路状态、发送模块及配置,统一串行调度 |
| 完整帧的有序分派 | Connection(已实现) | 使用Receiver.Feed/Peek/Release;I帧由TcpServer复制为事件后释放,S/U受理后释放,WAIT保留 |
| STARTDT/STOPDT/TESTFR事件分派 | Connection(已实现) | 按子站角色路由U功能码,由Manager统一提交Link修改;Connection仅作只读Link查询 |
| `t1/t2/t3`、`k`和`w`调用时机 | Connection + TcpServer(已实现) | Poll使用单调时间推进Tick,执行确认、探测和超时收尾;被动监听不发起TCP连接,未实现主站`t0`建连流程 |
| I/S/U实际发送 | TcpServer + Connection(已实现) | Prepare取得待发字节,send后立即回报增量;部分发送下轮续传,业务不再直接调用底层完成接口 |
| 遥信、遥测、遥控、总召、对时等 | ASDU业务层(待接入) | 从NextEvent取出I_FRAME副本后解析公共头及业务对象,携带事件generation调用TcpServer_Send_Response提交应答 |
### 10.2 统一连接上下文
独立Receiver由Connection持有,接收和发送使用同一份LinkState;已恢复的旧入口仍保留原有接收/Link私有全局对象,不参与TcpServer路径。当前归属如下:
```mermaid
flowchart TD
App[业务层] -->|Poll / NextEvent / Enqueue| Transport
Transport["IEC104_TcpServer:监听 / 收发 / 断连事件"] --> Context["IEC104_Connection:统一串行调度"]
Transport --> Net[Net网络后端]
Transport -->|NextEvent返回事件副本| App
App --> Asdu[ASDU公共头与业务对象处理]
Context --> Rx["接收器:IEC104_Receiver_t"]
Context --> Link["共享序号:IEC104_LinkState_t"]
Context --> Manager[SendManager发送管理]
Manager --> Link
Manager --> Tx[SendBuffer报文与写入进度]
Manager --> Queue[AsduQueue原文队列]
```
Receiver各入口已经显式接收实例和连接代次;Connection按序调用SendManager及控制接口,统一处理错误收尾。TcpServer为每次接受的主站分配新代次,旧单路入口及其私有Link不参与此路径。
Receiver的内部Peek视图持续有效到Release、Reset或重新Init。TcpServer先复制完整APDU到事件FIFO,再经Connection释放内部帧;业务通过NextEvent取得无借用指针的独立副本,可以跨下一轮Poll或断线保留,无需调用底层Release。普通通知满仍可处理排在前面的S/U;遇到无处交付的I帧时保留原帧、暂停续收,NextEvent腾出空间后由Process恢复。启停和收尾分别预留容量;启停变化逐条保留,启停槽满时延后受理改变状态的ACT,消费对应通知后重试。通知积压不主动断连,但原有协议期限继续生效;持续积压的I帧会挡住后续S/U,等待启停槽会延迟ACT受理及其应答,应用必须及时消费事件。Close后仍能取出全部请求、回执和连接收尾事件。同连接业务需要应答时,携带事件generation通过TcpServer_Send_Response提交ASDU;通过启停准入后,条件允许时立即尝试一次非阻塞发送,暂不能发送时缓存HIGH。主动上报通过TcpServer_Enqueue_Event缓存,不要求存在连接,帧头、序号及实际socket写入由协议和TCP层完成。
### 10.3 收发与状态提交顺序
以下说明底层顺序。SendManager.Prepare_Next_Send在实际发送前选择并准备当前帧,Handle_Send_Progress执行完整发送后的Complete/Commit_Send_Completion,Handle_I_S_Frame执行I/S校验、记录ACK并释放对应ASDU原文;调用方不再重复执行其中的底层步骤。S/TESTFR及子站启停构帧入队使用Manager封装,TESTFR_CON也经Manager.Handle_Testfr_Con提交,只有受理成功才由Connection撤销探测t1。Connection安排调用时机及U帧路由,TcpServer执行I/O。Manager仅绑定CONTROLLED链路,不参与主站启停流程。
Connection的Link/Buffer运行期修改入口统一为Manager;初始化经Manager.Init_Connection预检后直接绑定最终地址,失败保留原对象和队列。发送期限及队首身份由Manager.Get_Deadlines提供,Connection以临时Collect_Deadlines视图统一超时检查、发送准入、控制推进和等待计算,原始帧时间戳仍保存在所属模块。运行期参数、身份、序号及模块计数校验继续保留。
接收流程为:严格检查帧头并按长度成帧 → APCI校验 → I帧公共头及已知类型最低载荷检查 → 连接控制状态与I/S序号校验 → 提交链路接收状态 → 按帧类型分派。成帧及最低长度失败统一登记FRAME;I帧通过后仍由业务检查对象内容,S帧进入发送确认处理,U帧进入启停或心跳控制。后续ASDU业务校验失败应单独报告,不能自行回滚已经接受的传输序号。
发送侧必须区分“已构造并预留”“已排队”“完整写入传输层”“对端已确认”四个阶段。`IEC104_Link_Prepare_I_Frame()` 成功构帧时立即分配 `N(S)`、保存 `N(R)` 快照并推进 `V(S)`,使预编码和排队中的帧也占用 k 窗口;`V(A)` 仍只能由对端合法 `N(R)` 推进。
子站连接层在准备实际写socket时调用 `IEC104_SendManager_Prepare_Next_Send()`。已有当前I帧时续传,否则先发控制FIFO;两者均空才从ASDU字节环选择未编号业务。ASDU头和原文均可跨数组尾部,队列提供最多两段视图;Manager在原文跨界时先用249字节局部数组拼接,再交给Link,传输层仍取得连续APDU。Manager保障记录和当前APDU存储后调用Link.Prepare,让SendBuffer接手完整I帧并建立Manager记录,再通过Mark_Submitted完成队列交接记账:HIGH消费原文,NORMAL登记为保留原文待ACK。队列记账前仍保留待发旧状态,该状态不能用来判断发送缓冲是否已经接手。最多一帧已编号I帧等待本地完成,其余业务仍可按优先级选择。S/U使用独立6字节报文槽;当前帧不可被截断交错。Prepare成功后的序号不可撤回,后续提交或发送失败须结束连接。
应答入口经Manager的共享准入函数,仅在STARTED+IDLE时接手;其他启停阶段在记录容量、k窗口及HIGH容量检查前返回ERROR且不入HIGH、不因该拒绝断连。普通路径先由Connection.Drive处理保留接收和控制义务,再调用SendManager.Submit_Response检查实际接手时的状态;期限已到时直接走SendManager.Enqueue(HIGH),同样受准入限制。准入后,没有当前帧和旧HIGH且发送门控、窗口、记录容量、公平配额允许时直接准备一帧,再由TcpServer尝试一次非阻塞写入;暂不能准备或协议期限已到时先缓存HIGH。直接路径与队列路径共用Prepare_I_Asdu、SendBuffer及HIGH发送记录,仅跳过ASDU入队和Mark_Submitted;不会重复实现序号、完成或ACK逻辑。部分写入或暂不可写保留当前帧,由Process/Poll续发。公开OK只表示已接手原文,FATAL可能发生在实际写入之后。
I帧完成接口要求快照的 `N(S)==sent_vs`,只推进本地完成边界和实际捎带确认位置。Manager提交完成记录后立即释放当前I帧APDU;HIGH原文在I帧提交并建立发送记录时已出队,NORMAL原文保留到ACK或被后续入队覆盖,发送记录独立保留。对端ACK限制在 `va..sent_vs` 内;Link接受后Manager先登记所有新增ACK事实,核减high_unacked_count,再仅对NORMAL按记录身份释放原文或核减覆盖待确认计数,不能删除后来入队的新条目。原文出队或覆盖不推进ACK、不释放k窗口或回执槽。SendBuffer不保存确认边界,不再提供Apply_Peer_Ack。TcpServer把就绪回执复制为RECORD事件后,经Connection移除内部记录,业务通过NextEvent读取副本。Manager按最早已完成未确认记录的原始写完时刻判断t1;异步传输适配必须先提交对应发送结果,再分派相关接收事件。
发送准备和写入回报保留真实单调时间,仅因本地发送及STOP期限直接报告超时;对端ACK/TESTFR确认超时故障由接收阶段判定。旧确认t1已到时,Connection暂停为HIGH/NORMAL创建、编号新I帧;已有帧及控制发送仍可推进,以解除接收阻塞。有效确认解除所有已到期等待后自动恢复准入,不改变任何期限起点。快速应答或常规发送跨过确认期限后安排立即Process,先按有界输入快照处理确认,再按原始t1检查;Prepare报告零等待。不增加恢复期限或固定宽限时间,事件容量背压下仍按既有期限处理。
断线时TcpServer先复制剩余记录和断开通知,再由Connection_Close调用Manager.Abort;服务队列继续存在。TcpServer在关闭旧Connection后调用Detach:清空HIGH,将NORMAL现存原文恢复未提交,清除旧覆盖记账并切换到代次0;离线期间继续缓存NORMAL。新连接建立时Rebind到新代次,再Init连接。业务只使用公开提交及事件接口,不直接操作队列。HIGH固定用于当前连接的请求应答,NORMAL用于业务允许跨连接恢复的事件,业务分别调用Send_Response和Enqueue_Event;不提供通用入队兼容入口或重连保留开关,Close服务才清空NORMAL。已覆盖原文无法恢复,新连接从新序号开始,不能复用旧APDU。同一TCP上的启停不执行Rebind。Manager初始化成功、受理新STOPDT_ACT及每轮首次生成STARTDT_CON时清空待发HIGH;已启用时重复START保留HIGH及公平配额,WAIT尚未受理不清理;STOP成功受理立即清理HIGH。STOP受理时仍在HIGH的应答即使已返回OK也按约定作废,不产生逐条回执;已提交的当前帧和发送记录不受清理影响,继续完成及等待ACK。
需要纳入链路状态管理的 I/S 帧不能只调用基础 `IEC104_Build_*` 构造函数:I 帧必须通过 `IEC104_Link_Prepare_I_Frame()` 与 `IEC104_Link_Complete_I_Send()` 配对,普通 S 帧必须通过 `IEC104_Link_Prepare_S_Ack()` 与 `IEC104_Link_Complete_S_Ack_Send()` 配对。两个完成接口内部只把 `acked_nr` 推进到对应报文实际携带的位置,因此排队后新收到的 I 帧仍保留在 `acked_nr..V(R)`。同一连接仍须按上线顺序串行提交;较新的确认已经发送后,旧 S 帧不得再上线。
被控站受理START时立即激活,受理STOP时立即停用并等待确认;相反ACT不暂存。后到START结束尚未生成CON的停止等待,已有CON按FIFO写出,完成不回写状态。S确认覆盖接收数据且I帧全部获ACK时可排STOP_CON;Connection最多依次推进S与CON两项。STOP总期限从received_at_ms起算,按本轮最后一帧STOP_CON身份结束,成功受理START也取消旧计时。状态事件由Connection内部适配器在ACT受理时交给TcpServer,通知容量不足则WAIT保留原请求。发送完成仍准确记录字节、序号及ACK事实,见[发送缓冲设计](IEC104_发送模块_设计架构.md#sendbuffer)。
### 10.4 建议实施顺序与验收点
| 顺序 | 实施内容 | 可检查的完成条件 |
|---:|---|---|
| 1(已实现接收基础) | Receiver独立实例、完整帧视图、消费数和显式释放;Link由连接层共享 | 两个接收实例可交错输入;待处理帧不被后续Feed覆盖,旧代次调用被拒绝 |
| 2 | 接入发送前准备、部分写入续传及独立确认记录,补齐实际I/O和收发串行调度 | 发出 `N(S)=0` 后收到 `N(R)=1` 能正确确认并释放原ASDU;发送失败不重复推进状态,旧确认不清除新增待确认数据 |
| 3 | 连接生命周期、U 控制、定时器及窗口策略 | 受控传输与时间事件下可验证启动、停止、心跳、超时和窗口边界;序号错误停止当前批次并关闭连接 |
| 4(对象编解码已实现) | IEC60870_Asdu与InformationObjects提供监视/命令对象编解码和应答构造;继续接入业务分派、点表与执行流程 | 已有精确长度、品质/时标、容量与原文应答测试;地址匹配、总召进度、校时及遥控业务验收仍由后续实现补齐 |
---
@@ -0,0 +1,422 @@
# 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) |
| `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执行 |
| 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处理带时标信息对象和命令,实际事件采集及校时业务流程仍由应用接入。**业务发送入口负责使用本地标准时间日历,并显式设置SU=0。** 公共编解码继续支持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、分组及遍历状态;其他命令的选择/执行、校时权限及业务任务由应用接入。
| 层次 | 职责 |
|---|---|
| `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)。
## 启停主流程
| 角色与过程 | 调用顺序 |
|---|---|
| 主站启动 | `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)。
@@ -0,0 +1,29 @@
# 文档索引
说明文档统一放在 `docs/`,源码分别位于根目录的 `104/`、`common/`、`examples/` 和 `test/`。目录总览及快速构建入口见[项目 README](../README.md)。本文中的文档链接相对于当前文件;文档内的构建命令均从项目根目录执行。
## 推荐阅读顺序
1. [模块说明与接入示例](104/README.md):公共 ASDU、子站统一入口及模块依赖。
2. [子站示例说明](examples/IEC104子站示例.md):构建运行、模拟点表与总召处理。
3. [完整调用流程](104/IEC104_完整调用流程.md):从应用入口到协议收发的调用关系。
4. [API 接口汇总](104/IEC104_API接口汇总.md):按文件查阅接口。
5. [测试说明](test/IEC104测试文档.md):测试入口、构建命令、覆盖范围与验证记录。
## 全量清单
| 分类 | 文档 | 内容 |
|---|---|---|
| 104 | [模块说明与接入示例](104/README.md) | 模块职责、依赖与使用示例 |
| 104 | [基础设计](104/IEC104_设计架构.md) | APCI 编解码、流式接收及整体结构 |
| 104 | [链路设计](104/IEC104_Link_设计架构.md) | 序号、窗口、超时、启停与 TESTFR |
| 104 | [发送模块设计](104/IEC104_发送模块_设计架构.md) | ASDU 队列、发送缓冲与调度 |
| 104 | [连接层设计](104/IEC104_Connection_设计架构.md) | 单连接收发协调与生命周期 |
| 104 | [TCP 服务设计](104/IEC104_TcpServer_设计架构.md) | 网络适配、事件循环及容量配置 |
| 104 | [总召设计](104/IEC104_Interrogation_设计架构.md) | 站总召、分组、撤销、超时与业务事件 |
| 104 | [计数量设计](104/IEC104_CounterInterrogation_设计架构.md) | 读取、冻结、复位、动作执行及双实例接入 |
| 104 | [完整调用流程](104/IEC104_完整调用流程.md) | 各层调用顺序与业务流程 |
| 104 | [API 接口汇总](104/IEC104_API接口汇总.md) | 104 及其依赖的公共接口 |
| 公共 | [时间工具设计](common/IEC60870_Time_设计架构.md) | CP16/CP24/CP56、日历转换与时钟接口 |
| 示例 | [子站示例说明](examples/IEC104子站示例.md) | 编译运行、命令参数、点表与总召接入 |
| 测试 | [测试说明与验证记录](test/IEC104测试文档.md) | 公共模块、104 及真实 TCP 回环测试 |
@@ -0,0 +1,301 @@
# IEC 60870-5-101/104 公共时间工具
`IEC60870_Time.c/h` 为 IEC 60870-5-101 和 IEC 60870-5-104 提供共用的公历计算、CP16/CP24/CP56Time2a 编解码、本地标准时间转换及纪元秒数工具。时间源模块集中在`IEC60870_Clock.c/h`:Clock.c包含公共校验和Windows读写,Clock.h包含业务接口及RTC预留声明。时间源限定为Windows系统时钟或RTC,由`IEC60870_CLOCK_USE_RTC`在编译时二选一;RTC具体芯片驱动暂不实现,也不规划其他操作系统适配。时间相关的 ASDU 业务入口尚未接入;下文约定后续业务使用本地标准时间,并给出调用示例。原始解码保留源日历和标志,业务再调用转换接口取得本地标准时间日历。公共模块不维护连接状态、共享可变状态或动态内存,可独立于协议链路编译和链接;时间戳转日期可选择平台库或保留的纯整数算法。
## 101/104 的共用边界
101 和 104 使用 CP56Time2a 时,采用相同的七字节时间字段。串口帧和 TCP/APCI 的差别由各自协议层处理,不影响这里的时间编解码。
两个协议的业务层接入时均可包含 `IEC60870_Time.h` 并调用同一组 `IEC60870_Time_*` 接口,不需要分别维护时间实现。当前模块覆盖日历工具及 CP16/CP24/CP56Time2a。CP16为2字节经过时间,范围0..65535毫秒;CP24为3字节短时标,保存分钟、分钟内毫秒和IV,不含SU、小时或日期。两者分别使用`To_CP16/From_CP16`和`To_CP24/From_CP24`,成功返回1,失败返回0且保留输出。CP24解析检查bit6保留位、分钟0..59及分钟内毫秒0..59999,不自动恢复完整日期。不同时间类型具有不同字段,不能把CP24交给CP56解码。
日历支持 1970..2099、将 CP56 两位年份解释为 2000..2099、非零星期一致性及保留位严格检查,属于本模块明确采用的范围和接入策略。通信双方须就世纪和时区约定保持一致,不能把这些实现约定理解为 101 或 104 独有的规则。
## 本地标准时间与 SU 处理
IEC 60870-5-101:2003/AMD1:2015 第7.2.6.18条将SU规定为可选且不推荐使用的功能;如果选用,SU=1与时间值早一小时且SU=0的标签表示同一时刻。第7.4.6条要求不使用夏令时时将SU置0。参见[标准修订单第4至6页](https://preview.sist.si/sist-preview/22451/3a4cae004abe434883eba6133f897d87/IEC-60870-5-101-2003-AMD1-2015.pdf)。
**后续业务发送入口负责保证:使用本地标准时间(固定UTC+8),并显式设置`is_summer_time=0U`。** 公共编解码继续支持SU=0和1,按输入保留合法的SU值;公共编码接口不强制SU=0,也不会自动清除SU=1。该发送约束由后续业务入口落实,目前文档中的发送函数仅为调用示例。
业务发送时,输入日历须已经是本地标准时间。若来源是夏令时日历,应先完成时间转换,再以SU=0编码,不能只把SU清零而保留夏令时的小时值。接收时保留SU=1的兼容处理:原始解码后调用`IEC60870_Time_CP56_To_Local`,取得已处理SU的本地标准时间日历;接收兼容不改变本地发送SU=0的约定。
| 操作 | 行为 |
|---|---|
| 公共编码:`is_summer_time=0`或`1` | 校验其余字段后编码,SU按输入写入,不调整小时值 |
| 公共编码:`is_summer_time>1` | 返回0,输出缓冲区不变;标志只能取0或1 |
| 公共解码:报文SU=0或1 | 校验其余字段后解码,在`is_summer_time`中保留SU,不调整小时值 |
| 转本地标准时间:源标准时区为UTC+8、SU=0 | 保留原年月日和时间,毫秒保留,星期按日期重算 |
| 转本地标准时间:源标准时区为UTC+8、SU=1 | 减一小时,自动处理跨日/月/年,输出本地标准时间 |
| 转本地标准时间:源标准时区不同 | 根据源偏移和SU换算,最终输出固定UTC+8的日历 |
| 转本地标准时间:IV=1或结果越界 | 返回0,输出不变,不能把无效时间用于校时 |
| 业务发送示例 | 调用方提供已是本地标准时间的日历,示例显式设置SU=0,IV由零初始化保持为0 |
| 业务接收示例 | `From_CP56 → CP56_To_Local`,直接取得本地标准时间日历 |
`From_CP56`只保留源日历及SU;`CP56_To_Local`读取完整解析值,输出已经修正为本地标准时间的`IEC60870_WallTime_t`。**调用方不需要再加八小时或再减夏令时的一小时。** 使用独立输出存储时,原始输入不改写;若输入与输出重叠,成功写回会覆盖重叠的存储,调用方须按转换后的时间基准使用结果,再次编码时构造新的CP56值并置SU=0。目前仓库尚未提供实际的校时业务处理器,下文给出本地标准时间收发的调用方式。
转换接口的`source_standard_utc_offset_minutes`表示“源报文标准当地时间减UTC”的分钟数,向东为正、向西为负。双方约定本地标准时间时传`IEC60870_LOCAL_UTC_OFFSET_MINUTES`(480);源报文为UTC时传0,源标准时区为UTC-5时传-300。-1439..1439是本接口约定的参数范围;输出时区始终是本地标准时间。
源偏移参数**不能包含夏令时的一小时**,SU会单独处理。例如源标准时区为UTC-5、报文SU=1时仍传-300,不传夏令时的-240。计算关系为:
```text
本地标准时间 = 报文日历 - SU × 一小时 - 源标准时区偏移 + 八小时
源标准时区为UTC+8时:本地标准时间 = 报文日历 - SU × 一小时
```
| 报文日历 | SU | 源标准时区偏移 | 输出的本地标准时间 |
|---|---|---|---|
| 2026-07-01 12:00 | 0 | 480(本地标准时间基准) | 2026-07-01 12:00 |
| 2026-07-01 12:00 | 1 | 480(本地标准时间基准) | 2026-07-01 11:00 |
| 2026-01-01 00:30 | 1 | 480(本地标准时间基准) | 2025-12-31 23:30 |
| 2026-07-01 04:00 | 0 | 0(UTC基准) | 2026-07-01 12:00 |
转换内部先用有符号64位中间值处理SU和源偏移,再展开本地标准时间日历,检查输出范围,不对无符号小时直接减一。输出包含毫秒和本地标准时间日期对应的星期。内部平移八小时用于日历展开的数值不作为Unix时间戳暴露;公共`To_Seconds`、`From_Seconds`和`CP56_To_Seconds`仍是明确的UTC纪元秒数工具,不改变它们的既有含义,也不能把本地标准时间日历直接传给只接受UTC的`To_Seconds`。本地标准时间业务直接使用`CP56_To_Local`的日历结果。
SU本身不携带时区信息;源偏移须来自双方约定。这里的“本地”指本模块约定的固定UTC+8,不读取操作系统时区,也不根据历史年份自动开启夏令时;接收SU=1时按报文标志归一化为本地标准时间。
时间源提供UTC纪元秒数时,业务直接调用`IEC60870_Time_Seconds_To_Local(utc_seconds, ms, &local_time)`,一次取得本地年月日、时分秒、毫秒和星期。内部先检查范围、加上固定时区偏移,再展开一次日历;调用方不先生成UTC日历,也不手动加八小时。`CP56_To_Local`在解析源时间基准及SU后复用同一个入口。RTC若直接提供已是本地标准时间的日历,发送时可直接构造CP56解析值并编码。
## 职责与调用关系
| 内容 | 负责模块 |
|---|---|
| 日期合法性、闰年、星期、UTC 日期与纪元秒数互换 | `IEC60870_Time` |
| UTC纪元秒数直接转本地标准时间,统一处理时区及日期进位 | `IEC60870_Time_Seconds_To_Local` |
| CP56Time2a 七字节布局、日期和星期检查、IV/SU标志保留 | `IEC60870_Time` |
| CP56转本地标准时间,检查IV、处理SU和源时区,输出完整日历 | `IEC60870_Time_CP56_To_Local` |
| 统一读时、设时入口,校验日历、重算星期并检查时间质量标志 | `IEC60870_Clock` |
| Windows系统时钟读写,UTC与固定UTC+8日历互换 | `IEC60870_Clock.c`中的Windows分支 |
| 发送前保证日历已是本地标准时间,并统一设置SU=0 | ASDU 业务层,后续业务发送入口落实 |
| 事件时间戳、校时 ASDU 的业务处理,确定时间约定并调用转换接口 | ASDU 业务层 |
| 实现`IEC60870_Clock_RTC_Read/Set`,处理RTC寄存器、BCD、时间基准、掉电标志及读写一致性 | 板级接入层和具体RTC驱动,暂不实现 |
| t1/t2/t3 及发送期限使用的单调计时 | 平台提供 `now_ms`,Link/Connection 使用 |
使用已有时间值进行转换或编解码时,业务层包含`IEC60870_Time.h`,构建时加入`IEC60870_Time.c`。读取或设置实际时钟时,包含`IEC60870_Clock.h`,再加入`IEC60870_Clock.c`。默认Windows模式链接Kernel32(通常由Windows工具链默认链接);以后使用RTC时,编译Clock.c定义`IEC60870_CLOCK_USE_RTC=1`,加入实现RTC预留接口的板级驱动。仅调用时间转换工具时,无需构建Clock。
模块不依赖 Codec、Receiver、Link 或 Connection,也不会自动注册 ASDU 处理或设置系统时间。墙上时间用于业务时间戳;链路超时使用单调时间,校时不能改变链路期限。
## 当前接入状态与待补齐项
当前已实现日期转换、星期计算、CP56Time2a编解码、统一时间源接口及Windows系统时钟适配。完整的协议时间业务还需补齐以下接入:
| 项目 | 当前状态与待补齐内容 |
|---|---|
| 协议时间源读取及设置 | 已提供直接调用的`Clock_Read/Clock_Set`及Windows读写实现;时间源限定为Windows或RTC,RTC目前仅预留接口。日志读取仍使用原有入口 |
| RTC数据适配 | 接入函数声明已提供,具体芯片驱动暂不实现;后续接入时再明确RTC采用UTC还是本地标准时间、返回整数还是BCD、年份表示和毫秒来源,并按芯片要求取得一致的日期、时分秒及亚秒值,整理为固定UTC+8的公共日历 |
| 时间有效性及失败处理 | `Clock_Read`分别返回操作状态和`is_invalid`,校验日期及0/1标志,失败保留输出;具体驱动负责识别未初始化、掉电或失去可信时间。下文示例传递质量到CP56的IV;实际业务仍须决定是否发送无效时间。IV=1也不能用全零日期占位 |
| ASDU业务收发与校时 | `IEC60870_InformationObjects.c`的带时标类型复用基础对象值处理,并调用CP24/CP56编解码;保护事件和延时命令使用CP16。含103校时命令,保留IV/SU。`IEC60870_Asdu.c`维护容器与原文应答。事件采集、时间源读取、校时权限、平台设时及成功/失败应答的业务决策仍需应用接入 |
| 日期转换实现 | 保留Windows CRT和纯整数算法两种实现。RTC直接提供日历时无需时间戳展开;需要转换时间戳且没有Windows CRT时使用整数算法,不规划增加其他操作系统时间库 |
| 项目内的时间模块回归 | `iec60870_asdu_test.c`已覆盖CP56已知字节、IV/SU保留、非法日期和保留位,并分别使用两种时间实现编译运行;这不等于完整日期转换一致性测试。独立转换边界及平台读写失败、时间质量用例仍待补齐 |
RTC已有年月日时分秒时,可按约定直接整理后编码;只有需要从时间戳取得日期时,才涉及平台库或整数算法的选择。RTC未提供亚秒值时,可约定毫秒填0,但这不表示时间源具有毫秒精度。已提供的Windows适配从UTC统一转换为固定UTC+8,避免依赖电脑设置的本地时区。
## 统一时间源读写
业务直接调用`IEC60870_Clock_Read(&value)`或`IEC60870_Clock_Set(&local_time)`。Clock.c通过编译选项选择本文件内的Windows读写函数或外部的`IEC60870_Clock_RTC_Read/Set`,并负责读取后的结果检查和写入前的日历检查。一个程序选其中一个时间源,无需时钟对象、函数指针或注册初始化。RTC目前仅预留接口,以后接入时,其硬件及总线仍须由应用按驱动要求初始化。不同连接共用选定的时间源,调用方负责设备读写串行化。
| 接口 | 行为 |
|---|---|
| `IEC60870_Clock_Read(value)` | 平台实现读取完整日历和`is_invalid`,公共层校验1970..2099年的日期、时分秒、毫秒和质量标志,重算星期,成功后才提交输出 |
| `IEC60870_Clock_Set(local_time)` | 校验固定UTC+8日历,重算副本中的星期,再交给平台实现设置设备;不改写调用方输入 |
Windows读写已经包含在Clock.c内。RTC模式需要板级驱动实现Clock.h声明的`IEC60870_Clock_RTC_Read`和`IEC60870_Clock_RTC_Set`;本模块只声明接口,不提供芯片驱动或空实现,缺少实现时链接会报未定义符号。Windows模式不引用这两个RTC符号。RTC函数同步执行,成功返回1、失败返回0,不得保留公共层传入的局部变量指针,也不能反向调用公共读写入口。只读RTC也须实现Set函数,直接返回0。
参数非法或底层返回失败时,公共接口返回0。`Clock_Read`返回1表示取得格式合法的结果,仍须查看`value.is_invalid`:1表示源报告时间无效,0只表示未置无效标志,不证明已经同步或校准。读取前局部结果的标志初始化为1,驱动须按实际状态明确填写;无可用日历时应返回0。读取失败不会修改调用方输出,设置失败则不承诺硬件回滚,底层可能已经完成部分寄存器写入。
公共接口始终使用**固定UTC+8标准时间日历**。RTC本来就保存这种日历时,平台读时函数直接填年月日、时分秒和毫秒,设时函数直接写入这些字段,不需要先转时间戳再还原。BCD解码、两位年份展开、24小时制转换、读跨秒一致性和掉电标志由芯片驱动处理;星期由公共层重算。若RTC保存UTC或其他时间基准,适配层先完成约定的转换,不能把原日历直接声明为UTC+8。
Windows适配调用`GetSystemTime`一次取得UTC日期、时间及毫秒,通过`SystemTimeToFileTime`/`FileTimeToSystemTime`的64位计数加八小时,统一处理跨日、跨月和跨年;设时反向减八小时后调用`SetSystemTime`。计数只用于内部日历运算,不依赖`IEC60870_TIME_USE_PLATFORM`,也不读取Windows的本地时区或夏令时配置。该适配读取时报告`is_invalid=0`,不检测NTP等同步状态;设置系统时间需要相应权限,系统调用失败时返回0。API含义见[GetSystemTime](https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsystemtime)、[SetSystemTime](https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-setsystemtime)及[FILETIME转换](https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/nf-timezoneapi-systemtimetofiletime)。
两种模式都编译`IEC60870_Time.c`和`IEC60870_Clock.c`,通过`IEC60870_CLOCK_USE_RTC`选择时间源:
| 编译选项 | 时间源及当前状态 | 额外依赖 |
|---|---|---|
| `0`(默认) | Windows系统时钟,已实现 | Windows的Kernel32接口 |
| `1` | RTC,仅预留接口,暂不实现芯片驱动 | 板级驱动提供`IEC60870_Clock_RTC_Read/Set` |
该选项只接受0或1,必须作用于`IEC60870_Clock.c`的编译;仅在调用方源文件中定义不会改变已编译的Clock。GCC使用`-DIEC60870_CLOCK_USE_RTC=1`,MSVC使用`/DIEC60870_CLOCK_USE_RTC=1`选择RTC。非Windows目标须显式选择RTC;选0会在编译Clock.c时报告不支持Windows时间源。RTC分支不包含Windows头文件,也不调用Windows时钟API。
`IEC60870_CLOCK_USE_RTC`决定从哪里读取或设置当前时间;`IEC60870_TIME_USE_PLATFORM`决定如何转换已有时间戳,两者分别生效。RTC已有日历时直接填入,不需要先转换成时间戳。
Windows按默认配置构建后,业务即可直接读取:
```c
#include "IEC60870_Clock.h"
/* 业务函数内的调用片段。 */
IEC60870_ClockValue_t now;
if (!IEC60870_Clock_Read(&now)) {
return 0;
}
/* 读取成功后,业务根据now.is_invalid决定是否使用该时间。 */
```
以后接RTC时,由板级驱动实现以下两个预留函数,业务调用方式相同。下例中的板级函数名仅示意,项目尚未实现某一型号芯片的驱动;板级函数须提供固定UTC+8日历、实际质量状态和1/0返回值:
```c
#include "IEC60870_Clock.h"
int Board_RTC_Read(IEC60870_ClockValue_t *value);
int Board_RTC_Set(const IEC60870_WallTime_t *local_time);
int IEC60870_Clock_RTC_Read(IEC60870_ClockValue_t *value)
{
return Board_RTC_Read(value);
}
int IEC60870_Clock_RTC_Set(const IEC60870_WallTime_t *local_time)
{
return Board_RTC_Set(local_time);
}
```
读取当前时间并编码CP56时可使用以下业务示例。这里选择把源质量传给IV;实际业务也可在`is_invalid=1`时拒绝发送。日历仍须满足CP56的2000..2099年范围,SU固定为0:
```c
int Encode_Current_Timestamp(uint8_t *out, size_t capacity)
{
IEC60870_ClockValue_t now;
IEC60870_CP56Time2a_t timestamp = {0};
if (!IEC60870_Clock_Read(&now)) {
return 0;
}
timestamp.time = now.time;
timestamp.is_invalid = now.is_invalid;
timestamp.is_summer_time = 0U;
timestamp.has_weekday = 1U;
return IEC60870_Time_To_CP56(&timestamp, out, capacity);
}
```
校时报文的调用顺序为`From_CP56 → CP56_To_Local → Clock_Set`。转换先检查IV并处理SU和源标准时区,业务再检查校时权限及时间合理性,最后才调用`Clock_Set`。例如已有解析值`timestamp`,且业务已经确认允许校时:
```c
IEC60870_WallTime_t local_time;
if (!IEC60870_Time_CP56_To_Local(&timestamp,
IEC60870_LOCAL_UTC_OFFSET_MINUTES,
&local_time)) {
return 0;
}
/* local_time已是固定UTC+8标准时间,不再加八小时或减一小时。 */
return IEC60870_Clock_Set(&local_time);
```
该例假定源标准时区也是UTC+8;其他源时区应传双方约定的偏移。`Clock_Set`只接收日历,不能自行判断报文IV、SU或校时权限;不能跳过转换直接把解码的源日历交给设时接口。以上函数和片段说明接入顺序,尚未注册为ASDU业务处理器,实际成功/失败的业务应答仍由ASDU层负责。
## 时间戳转日期的实现选择
业务层统一调用`IEC60870_Time_From_Seconds`,编译`IEC60870_Time.c`时通过`IEC60870_TIME_USE_PLATFORM`选择实现:
| 配置 | 实现与依赖 |
|---|---|
| `1`,Windows默认 | 使用Windows CRT的`_gmtime64_s`,将时间戳转换为UTC日历 |
| `0`,其他平台默认,也可在Windows使用 | 使用原有整数算法计算完整天数、年月日及日内时分秒,不包含`<time.h>`,不调用平台时间函数 |
这是一项**编译配置**,不会在运行时检测或加载时间库。当前平台库后端只接入Windows;嵌入式等没有合适时间库的环境使用`0`。GCC可添加`-DIEC60870_TIME_USE_PLATFORM=0`,MSVC可添加`/DIEC60870_TIME_USE_PLATFORM=0`。选项必须作用于`IEC60870_Time.c`的编译,仅在调用方源文件中定义不会改变已编译的模块。
Windows显式使用64位版本,避免32位`time_t`限制原有的1970..2099年范围;该接口的UTC语义、参数和范围见[Microsoft文档](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/gmtime-s-gmtime32-s-gmtime64-s)。两条路径共用参数检查、毫秒赋值和最终输出提交,失败不改输出;平台函数失败时返回0。整数算法保留在源码中,选择平台库时不编译该分支。
两种实现都将**传入的时间戳**转换为UTC日历,不获取当前时间。`Seconds_To_Local`、`CP56_To_Local`继续复用同一转换入口,统一输出固定UTC+8的本地标准时间,不受Windows系统时区配置影响。日期转秒数、星期计算和CP56编解码仍使用模块内的整数运算。
`IEC60870_TIME_USE_PLATFORM`只选择时间戳转日期的实现,与是否使用RTC作为时间源无关。RTC驱动若提供UTC纪元秒数,按业务需要调用`From_Seconds`或`Seconds_To_Local`;若已提供符合业务时间基准的年月日时分秒,可整理为`IEC60870_WallTime_t`并构造CP56解析值进行编码。读取RTC、确认其时间基准及将驱动返回值整理为上述格式,由平台接入层和业务层完成。
## 类型、范围与返回值
`IEC60870_WallTime_t` 是公开的日历值类型,包含年月日、时分秒、毫秒和派生的星期,不包含时区字段。`Seconds_To_Local`和`CP56_To_Local`输出本地标准时间;`From_Seconds`输出UTC。CP56Time2a中的源日历值须结合SU及双方约定的时间基准解释,不能仅凭结构体类型猜测时区。
`IEC60870_CP56Time2a_t` 另存 `is_invalid`(IV)、`is_summer_time`(SU)及 `has_weekday`,三个标志都只能是0或1。编解码保留IV/SU,由业务决定是否使用该时间。结构体只用于内存传值,不能按 `sizeof` 复制为报文,也不能把七字节报文强转成结构体指针。
| 项目 | 约定 |
|---|---|
| 普通日历范围 | 1970-01-01 至 2099-12-31,按公历检查每月实际天数 |
| 纪元秒数范围 | 0..4102444799,对应 UTC 1970-01-01 00:00:00 至 2099-12-31 23:59:59 |
| CP56 年份 | 报文只接受 0..99,解释为 2000..2099;不自动猜测世纪 |
| 时、分、秒、毫秒 | 0..23、0..59、0..59、0..999;不表示闰秒 |
| 内存中的星期 | 0=周日,1=周一,…,6=周六;输入的 `weekday` 不作为编码或秒数转换的依据 |
| 返回值 | 1=成功,0=参数/格式非法、结果越界、平台转换失败或将IV=1转为有效时间;失败不改输出 |
| 缓冲区 | 编解码检查非空及长度至少为7,仅处理前7字节;实际空间必须与所报长度一致 |
本地标准时间接口保留毫秒并重算星期。CP56日历输入须在2000..2099年,SU或源时区修正后得到1999年的本地标准时间可以成功;本地标准时间超过2099-12-31 23:59:59则失败。例如源本地标准时间的2099年最后一秒仍可完整输出,源UTC的2099-12-31 16:00则会因转换到本地标准时间2100年而失败。
`Seconds_To_Local`接受的UTC秒数为0..4102415999:0输出1970-01-01 08:00:00,4102415999输出2099-12-31 23:59:59;再增加一秒会超出本地日历上限,返回0且输出不变。参数必须是原始UTC纪元秒数,不能预先加八小时。
纪元秒数0是正常结果,不用来表示错误。基础`To_Seconds`和`CP56_To_Seconds`只输出完整秒数;`From_Seconds`和`Seconds_To_Local`同时接收秒数和毫秒余数。所有入口均检查毫秒范围,越界直接报错,不截断、不进位。
## CP56Time2a 字段策略
七字节布局采用[Beckhoff 的 CP56Time2a 字段说明](https://infosys.beckhoff.com/content/1033/tf6500_tc3_iec60870_5_10x/983926923.html):
| 字节(从0开始) | 内容 |
|---|---|
| 0、1 | 分钟内毫秒,低字节在前,范围0..59999,拆成秒和毫秒余数 |
| 2 | bit7为IV,bit6保留,bit0..5为分钟 |
| 3 | bit7为SU,bit5..6保留,bit0..4为小时 |
| 4 | bit5..7为星期,bit0..4为日 |
| 5 | bit4..7保留,bit0..3为月 |
| 6 | bit7保留,bit0..6为两位年份;数值100..127不接受 |
- **星期**:`has_weekday=0` 编码为报文星期0;为1时从日期计算1=周一至7=周日。解码允许星期0,此时仍计算内存中的 `time.weekday`;非零星期必须与日期一致,否则失败。这样不依赖调用方传入的过期星期,也不会默默接受相互矛盾的日期和星期。
- **IV**:编解码保留该标志。IV=1表示业务时间无效,不等于报文格式无法解析;调用方必须检查该标志后再决定是否记录或使用时间。即使IV=1,本模块也要求日期和时间字段合法,不接受全零日期等占位数据。
- **SU**:公共编解码支持0和1,复制标志而不调整日历值;本地标准时间转换接口在SU=1时扣除一小时,并处理源标准时区与本地标准时间的差值。编码或转换时SU大于1均失败。后续业务发送入口负责设置SU=0;接收时将完整解析值交给`CP56_To_Local`,不丢弃SU后直接使用原时间。
- **保留位**:编码统一清零,解码严格拒绝非零值。分钟bit6按本模块采用的布局视为保留位,不接受将它另作扩展标志的报文。这是明确的接入约束,不能把此接口当作任意七字节数据的无损容器。
数值校验成功只说明日期真实存在、各字段符合上述约定。它不保证远端时钟准确,也不决定校时权限或业务接受策略。
## 接口与示例
| 接口 | 功能 |
|---|---|
| `IEC60870_Time_Is_Valid` | 检查1970..2099年的年月日、时分秒及毫秒;忽略派生星期,不转换时间或判断时钟质量 |
| `IEC60870_Time_Get_Weekday` | 校验日期并返回星期 |
| `IEC60870_Time_To_Seconds` | UTC日历转完整纪元秒数,成功状态和数值分开返回 |
| `IEC60870_Time_From_Seconds` | 纪元秒数加毫秒余数转UTC日历,重新计算星期 |
| `IEC60870_Time_Seconds_To_Local` | UTC纪元秒数加毫秒余数直接转固定UTC+8的本地日历,自动处理日期和星期 |
| `IEC60870_Time_CP56_To_Local` | CP56解析值转本地标准时间日历,检查IV、处理SU及源时区,保留毫秒并重算星期 |
| `IEC60870_Time_CP56_To_Seconds` | CP56解析值转UTC秒数,检查IV、扣除SU的一小时和标准时区偏移 |
| `IEC60870_Time_To_CP56` | 日历与标志编码为七字节,校验输出容量 |
| `IEC60870_Time_From_CP56` | 从至少七字节的输入解析日历与标志,校验格式和日期 |
下面示例供后续业务层接入时使用,示例函数尚未作为生产接口实现。示例适用于**通信双方已约定CP56使用本地标准时间基准**的接入:发送使用本地标准时间日历、SU=0;接收支持SU=0和1,输出已修正的本地标准时间日历。输入日历由调用方提供,工具本身不读取系统时钟或设置RTC。
```c
#include "IEC60870_Time.h"
int Encode_Local_Timestamp(const IEC60870_WallTime_t *local_time,
uint8_t *out, size_t capacity)
{
IEC60870_CP56Time2a_t timestamp = {0};
if (local_time == NULL) {
return 0;
}
/* 输入须已是本地标准时间;此示例不转换调用方传入的日历。 */
timestamp.time = *local_time;
timestamp.is_summer_time = 0U; /* 业务发送策略在此落实;公共编码仍允许SU=1。 */
timestamp.has_weekday = 1U;
/* 直接编码本地标准时间,IV=0、SU=0;编码接口检查日期、毫秒和缓冲区。 */
return IEC60870_Time_To_CP56(&timestamp, out, capacity);
}
int Decode_Local_Timestamp(const uint8_t *in, size_t length,
IEC60870_WallTime_t *local_time)
{
IEC60870_CP56Time2a_t timestamp;
if (!IEC60870_Time_From_CP56(in, length, &timestamp)) {
return 0;
}
/* 源标准时区与本地均为UTC+8,只需按SU归一化,输出即为本地标准时间。 */
return IEC60870_Time_CP56_To_Local(&timestamp,
IEC60870_LOCAL_UTC_OFFSET_MINUTES,
local_time);
}
```
例如收到本地标准时间基准的12:00、SU=0时,输出小时为12;SU=1时输出小时为11。收到00:30、SU=1时,输出前一天23:30,日期和星期同步调整。IV=1、字段非法或转换结果越界时,整个输出日历保持不变。业务完成权限和时间合理性检查后,可以将结果交给按本地标准时间运行的RTC或显示模块,无需再次加八小时。若对端使用其他时区,应将示例中固定的480替换为对端约定的源标准时区偏移。
## 与原代码草案的差异
文件、类型、宏和API统一使用 `IEC60870` 公共前缀,供101和104共用。原草案中的单值秒数返回改为“状态返回 + 输出参数”,编解码增加缓冲区长度,并引入独立的CP56标志值类型。所有入口先检查指针、范围和格式再提交输出,修复非法日期、年份混淆、毫秒静默截断和纪元值与错误混用的问题。
原草案解码时丢弃报文星期和SU、遇IV直接失败;现在保留IV/SU及星期是否出现,检查非零星期的一致性,并通过独立转换接口落实SU=1的一小时修正。原始解码仍可返回IV=1以供业务记录,但转成有效时间戳时会拒绝。已有接收、链路和发送模块的接口及行为不变,时间工具由需要处理时间戳的业务按需接入。

Some files were not shown because too many files have changed in this diff Show More