补充遥调业务代码
CI / L0 静态门禁 (pre-commit) (push) Failing after 45s
CI / L0.5 主机侧单测 (MSVC) (push) Failing after 43s

This commit is contained in:
STF
2026-11-01 00:38:09 -07:00
parent cd42b4d61d
commit e1b0434774
233 changed files with 292584 additions and 268 deletions
@@ -0,0 +1,22 @@
Command default configuration, 2026-10-31.
Scope: IEC60870-Slave only. Existing extension changes preserved.
Default type_mask enables all 14 types (45..51, 58..64): 0x3fff.
Default select_required_mask excludes direct-only 51/64: 0x1fbf.
Timestamp validation, time base requirement and transaction policy retained.
Example explicitly configures the six types wired to its simulated point table.
No public fields or storage sizes changed in this follow-up.
Before snapshots are under before/.
Validation: scripts/build.ps1 -Test PASS (exit 0).
GCC C11 -O2 -Wall -Wextra -Werror: no diagnostics.
Command: 46 groups x4 (TCP capacity 1/2, reply capacity 1/4).
Slave: 32 groups x2; example: 132 groups x2; rules: 3 groups.
Real TCP loopback PASS: 45/46/49/58/59/62 flows, changed GI values,
silent expired commands, STOP/START/reconnect and cleanup.
The new group uses unchanged Config_Default for all 14 command types,
checks direct-only 51/64, selection for the other 12, and time-base gating.
Explicitly disabled 47/58 and out-of-scope 105 remain IGNORED.
UTF-8/LF/BOM checks for changed files and git diff --check: PASS.
Documentation: 17 Markdown files, 356 local links/anchors, UTF-8/LF: PASS.
No MCU/Keil build or hardware test was performed.
@@ -0,0 +1,342 @@
/*
* 模块:IEC104控制命令业务层,45..51及58..64,COT=6激活、COT=8撤销。
* 职责:校验请求、选择/执行匹配、撤销、三类期限、原文确认和事务收尾。
* 点表、闭锁、远方/就地策略及实际输出由应用负责;本模块不判定设备操作是否安全。
* 每个Slave至多一个实例,每实例至多一个未释放事务;其他业务仍独立推进。
* 静态存储、无动态分配、不保存回调;所有入口须与Slave在同一事件循环串行调用。
* 只有Accept调用应用动作,先消费事件、再至多调用一次;Handle_Event/Process不调用应用。
* 返回信息由Submit_Return_Info沿原连接提交,确认/返回信息/终止保持顺序。
*
* 接入顺序:先Create,再按Slave通知FIFO分派Handle_Event;每轮Process后读空NextEvent。
* SELECT/EXECUTE事件由Accept或Reject消费;EXECUTE启动成功后,先Report_Result,
* 再逐帧Submit_Return_Info,全部OK后Complete;最终读取FINISHED清理应用资源。
* Report_Result也可晚于返回信息,但必须早于Complete,且设备结果产生后应及时报告。
* WAIT表示本次应用操作尚未接手,保留输入并在读取通知/推进网络后重试;OK不得重复提交。
* 动作/确认/设备结果/FINISHED是不同节点:FINISHED只结束协议事务,不撤销物理输出。
* STOP、断线或Cancel不回滚动作,也不把原连接的返回信息转交到新连接。
*
* 退出顺序:Cancel、读完FINISHED、Slave_Close、读空并分派通知、Destroy,
* 然后由应用移除站点并销毁Slave。模块不负责Slave或应用持有引用的释放。
*/
#ifndef IEC104_COMMAND_H
#define IEC104_COMMAND_H
#include "IEC104_Slave.h"
#include "IEC60870_Time.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 不透明静态池句柄;Destroy成功后失效,不得按值复制内部对象或继续使用旧指针。 */
typedef struct IEC104_Command IEC104_Command_t;
/* 供无副作用的类型常量使用;45..51映射bit0..6,58..64映射bit7..13。
* 范围外为0,不作非法移位。Create接受全部14位,默认全部启用。 */
#define IEC104_COMMAND_TYPE_BIT(type) ((uint16_t)( \
((type) >= 45U && (type) <= 51U) ? (1U << ((type) - 45U)) : \
(((type) >= 58U && (type) <= 64U) ? (1U << ((type) - 51U)) : 0U)))
/* 创建时复制的配置。期限以Net单调时钟计算,等于边界即到期;
* 0仅禁用时长限制,等待阶段遇到时钟倒退仍终结。选择期限包含确认的发送等待;
* Report_Result之后没有业务期限,应用漏调Complete会一直占用事务槽。 */
typedef struct {
uint16_t type_mask; /* 默认启用45..51、58..64全部14种;应用可裁剪,不能为空。 */
uint16_t select_required_mask; /* 默认除51/64外均须先选择;须是type_mask子集,51/64禁止要求选择。 */
uint32_t select_timeout_ms; /* 选择动作返回至匹配执行受理,默认30000,不得为0。 */
uint32_t decision_timeout_ms; /* 工作事件生成至Accept/Reject,默认15000;0禁用。 */
uint32_t execute_timeout_ms; /* 动作STARTED至Report_Result,默认60000;0禁用。 */
bool send_act_term; /* 成功后发ACT_TERM,默认true;false时失败也不发。 */
bool act_term_on_failure; /* 默认false;true且send_act_term时,启动后报告失败也发COT=10、P/N=0。 */
uint32_t time_tag_max_delay_ms; /* 默认10000;0只关闭时差检查,仍拒绝IV=1/UTC换算失败。 */
int16_t master_utc_offset_minutes; /* 主站标准时区,默认480,范围-1439..1439;SU另由公共层处理。 */
bool negative_on_expired; /* 默认false,过期激活静默丢弃;true时回否定ACT_CON。 */
} IEC104_CommandConfig_t;
/* 可按值保存的请求快照;不含站点/ASDU等借用指针。
* state为单命令0/1、双命令1/2、步调节1降/2升;具体操作含义由应用点表定义。
* accepted_ms为Handle_Event受理时刻,包含Slave通知排队时间,不是网络接收时间。 */
typedef struct {
uint64_t generation, receive_serial; /* 原连接代次、当前工作请求的I帧接收编号。 */
uint64_t station_id; /* 站点注册身份;同CA重新注册后此值不同。 */
uint16_t common_address; /* 请求公共地址CA,不接受广播65535。 */
uint8_t originator_address, type_id; /* 请求源发地址OA、类型45..51或58..64。 */
uint32_t ioa; /* 非零24位信息对象地址;存在性由应用点表确认。 */
bool is_select, selected; /* S/E及本事务匹配选择标志;51/64两者恒为false。 */
uint8_t state, qualifier; /* 基础45..47:状态/QU;48..50:0/QL(0..127);51:0/0。 */
union {
int16_t normalized; /* 48/61:原始定点编码,实际值为normalized/32768.0。 */
int16_t scaled; /* 49/62:有符号标度值。 */
float short_float; /* 50/63:有限IEEE 754 binary32。 */
uint32_t bitstring; /* 51/64:32位数据,不包含选择位。 */
} value; /* 按type_id读取;状态命令清零。 */
bool has_time; /* 58..64为true,字段含义与对应基础类型相同。 */
IEC60870_CP56Time2a_t time; /* 解码后的原始时标,保留IV/SU,不是可直接发送的字节布局。 */
uint64_t time_utc_ms; /* UTC纪元毫秒,含毫秒余数;无时标为0。受理后不再判时差。 */
uint64_t accepted_ms; /* 当前工作请求受理时的单调毫秒。 */
} IEC104_CommandRequest_t;
/* SELECT和EXECUTE为待决定工作事件;FINISHED仅通知清理,无需应答。 */
typedef enum {
IEC104_COMMAND_SELECT = 1, /* 应用检查策略并Accept/Reject选择,可执行返校动作。 */
IEC104_COMMAND_EXECUTE, /* 应用Accept/Reject执行;selected区分选择后执行和直接执行。 */
IEC104_COMMAND_FINISHED /* 该事务唯一的结束通知,原因及副作用标志在事件中给出。 */
} IEC104_CommandEventType_t;
/* 首次结束时冻结,后续STOP/Cancel不会覆盖;枚举不表示设备输出已停止。 */
typedef enum {
IEC104_COMMAND_FINISH_NONE = 0, /* 工作事件的占位值,不是FINISHED的正常原因。 */
IEC104_COMMAND_FINISH_COMPLETED, /* 应用报告成功,Complete后最后应答已交接。 */
IEC104_COMMAND_FINISH_EXECUTION_FAILED, /* 输出已启动,但应用报告失败,协议收尾已交接。 */
IEC104_COMMAND_FINISH_REJECTED, /* 应用Reject,或动作返回NOT_STARTED。 */
IEC104_COMMAND_FINISH_ACTION_FAILED, /* 动作返回FAILED/非法值,可能存在副作用。 */
IEC104_COMMAND_FINISH_MISMATCH, /* 选择确认交接后,同一点执行的类型/OA/值/限定词不符。 */
IEC104_COMMAND_FINISH_DEACTIVATED, /* 主站COT=8成功撤销尚未启动执行的事务。 */
IEC104_COMMAND_FINISH_SELECT_TIMEOUT, /* 选择保持到期,释放选择,已交接的确认无法撤回。 */
IEC104_COMMAND_FINISH_DECISION_TIMEOUT, /* 未及时Accept/Reject,模块安排否定确认。 */
IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT, /* 未及时Report_Result;保留肯定确认,不发ACT_TERM。 */
IEC104_COMMAND_FINISH_CANCELLED, /* 本地Cancel结束事务。 */
IEC104_COMMAND_FINISH_STOPPED, /* 原连接STOPDT停用。 */
IEC104_COMMAND_FINISH_DISCONNECTED, /* 原连接断开。 */
IEC104_COMMAND_FINISH_CONNECTION_CHANGED, /* 新连接代次或会话身份已失效。 */
IEC104_COMMAND_FINISH_SEND_ERROR, /* 发送ERROR/FATAL,且不是等待分派正常STOP通知。 */
IEC104_COMMAND_FINISH_SERVICE_ERROR /* Slave服务错误或内部身份/应答资源无法继续使用。 */
} IEC104_CommandFinishReason_t;
/* 事件是可保存的纯值,应答须原样传回;调用方不能用修改request改变实际执行内容。
* 任何工作事件曾被取走的事务,恰好交付一次FINISHED;后者无需应答。
* 应用跨循环持有站点/context或异步动作时,应在首个工作事件时自行Acquire,
* FINISHED清理且物理输出确已结束后Release,不能等读到FINISHED后才申请引用。 */
typedef struct {
IEC104_CommandEventType_t type; /* 决定事件的处理方式,参与身份校验。 */
uint64_t instance_id, transaction_id, event_serial; /* 实例、事务、事件三层身份,原样回传。 */
IEC104_CommandRequest_t request; /* 最后接纳的工作请求,不被撤销/不匹配请求覆盖。 */
IEC104_CommandFinishReason_t reason; /* 仅FINISHED有效;工作事件为NONE。 */
bool select_effect, execute_effect; /* 仅FINISHED有效:对应动作曾STARTED/FAILED,可能需清理。 */
} IEC104_CommandEvent_t;
typedef enum {
IEC104_COMMAND_ACTION_NOT_STARTED = 0, /* 必须保证没有任何副作用。 */
IEC104_COMMAND_ACTION_STARTED, /* 选择生效或输出启动;输出结果另行报告。 */
IEC104_COMMAND_ACTION_FAILED /* 未报告启动成功,但可能已留下副作用。 */
} IEC104_CommandActionResult_t;
/* Accept同步调用的动作,不保存函数/context指针。
* context为应用传入的上下文,可为NULL;request由模块内部快照构造,仅本次调用有效。
* 须短时返回,不得保存request指针、推进Slave或发布上报;设备的异步完成用Report_Result报告。
* 回调期间仅可Get_Status/Get_Transaction;其他修改入口拒绝重入。
* 返回值须准确说明是否有副作用,非法值按FAILED处理,模块不会重试动作。 */
typedef IEC104_CommandActionResult_t (*IEC104_CommandAction_t)(void *context,
const IEC104_CommandRequest_t *request);
typedef enum {
IEC104_COMMAND_RESULT_SUCCEEDED = 1, /* 启动后设备实际执行成功。 */
IEC104_COMMAND_RESULT_FAILED /* 启动后设备实际执行失败,不等同于拒绝启动。 */
} IEC104_CommandResult_t;
typedef enum {
IEC104_COMMAND_REJECT_NEGATIVE = 1, /* 通用策略拒绝,回COT=7、P/N=1。 */
IEC104_COMMAND_REJECT_UNKNOWN_IOA /* 点表不存在此IOA,回COT=47、P/N=1。 */
} IEC104_CommandRejectCause_t;
typedef enum {
IEC104_COMMAND_EVENT_ERROR = -1, /* 参数/来源/服务错误;不能据此重放已产生的动作。 */
IEC104_COMMAND_EVENT_IGNORED = 0, /* 不接手,I帧可继续交后续处理器。 */
IEC104_COMMAND_EVENT_HANDLED = 1 /* 已接手,包括校验拒绝/计数丢弃,不应再走44兜底。 */
} IEC104_CommandEventResult_t;
typedef enum {
IEC104_COMMAND_NEXT_ERROR = -1, /* 句柄/参数/重入或服务错误,输出未写入。 */
IEC104_COMMAND_NEXT_EMPTY = 0, /* 暂无可交付事件,可能须先读Slave通知;输出未写入。 */
IEC104_COMMAND_NEXT_READY = 1 /* 已交付纯值事件,同一事件不会再次出队。 */
} IEC104_CommandNextResult_t;
/* 以下为共同分类;OK究竟消费事件、记录结果还是固定帧序,以各接口说明为准。
* WAIT均未接手本次应用输入;只有Submit_Return_Info的OK直接代表该ASDU帧序已固定。 */
typedef enum {
IEC104_COMMAND_REPLY_STALE = -2, /* 身份/阶段已失效、事件已消费或事务结束,不得重试旧操作。 */
IEC104_COMMAND_REPLY_ERROR = -1, /* 参数/用法/重入或服务错误,检查接口具体前置条件。 */
IEC104_COMMAND_REPLY_WAIT = 0, /* 等待通知/确认/FIFO/网络,保留输入,推进事件循环后重试。 */
IEC104_COMMAND_REPLY_OK = 1 /* 本次操作已接手;不保证设备成功、网络ACK或FINISHED已就绪。 */
} IEC104_CommandReplyResult_t;
typedef enum {
IEC104_COMMAND_PHASE_SELECT_WAIT = 1, /* 等应用决定选择;决定期限从事件生成开始。 */
IEC104_COMMAND_PHASE_SELECTED, /* 已选择,等待匹配执行;选择确认可能仍未交接。 */
IEC104_COMMAND_PHASE_EXECUTE_WAIT, /* 等应用决定执行;已停止选择期限。 */
IEC104_COMMAND_PHASE_EXECUTING, /* 已启动,等待结果/返回信息/Complete/最后应答交接。 */
IEC104_COMMAND_PHASE_ENDED /* 协议已结束,可能仍保留FINISHED或未交接应答。 */
} IEC104_CommandPhase_t;
typedef struct {
bool active; /* 尚未ENDED;不是当前连接是否可操作的保证。 */
IEC104_CommandPhase_t phase; /* 当前协议阶段;结果报告和Complete不另建阶段。 */
bool confirm_submitted; /* 当前工作请求的确认已交给发送层,不表示写完或ACK。 */
/* result_reported:结果已冻结、执行期限停止;completed:已接手Complete,返回阶段关闭;
* term_submitted:ACT_TERM已交接。不配置ACT_TERM时此值一直false,不能据此等事务结束。 */
bool result_reported, completed, term_submitted;
} IEC104_CommandTransactionInfo_t;
/* 累计计数从Create起维护;查询不改变阶段、不做超时转换。
* busy包括待消费FINISHED和未交接的事务应答,不包含独立短应答。
* need_process不因发送WAIT持续置true;每轮网络推进后仍须Process重试。
* 计数用途是诊断,不是可靠账单;各计数可重叠,unsigned long到达上限后自然回绕。 */
typedef struct {
uint64_t generation; /* 当前已分派的连接代次,断线后为0。 */
/* active:模块已处理ACTIVATED且尚未停用,不代替操作前的实时会话校验;
* events_pending:须优先读取Slave通知;need_process:有可立即推进的内部工作;
* timer_active:当前启用了非零时长期限,时钟倒退检查不受此标志限制。 */
bool active, events_pending, need_process, timer_active;
bool busy; /* 唯一事务槽仍占用,此时其他站点/其他IOA也不能新建事务。 */
bool time_base_valid; /* 持有时间基准;不表示推算时尚未越界,STOP/断线不清除。 */
uint32_t timeout_remaining_ms; /* 当前期限剩余毫秒;无期限/已到期/时钟倒退时为0。 */
/* pending_replies为模块仍持有的应答数,不含发送层已接手的帧和应用保留的返回信息;
* pending_events为尚未取走的工作事件或FINISHED数,本版最多1。 */
unsigned pending_replies, pending_events;
/* accepted:生成的工作事件数(选择后执行计两次);selected/executed:对应STARTED次数;
* completed/execution_failed:成功/失败收尾数;action_failed:动作FAILED结束数。 */
unsigned long accepted, selected, executed, completed, execution_failed, action_failed;
/* submitted:所有已交接帧,含返回信息;negative_submitted:其中P/N=1的应答;
* return_info_submitted:已交接返回信息;waits:发送接口明确返回WAIT的次数。 */
unsigned long submitted, negative_submitted, return_info_submitted, waits;
/* rejected:接收校验/匹配/忙/无选择等拒绝数;app_rejected:Reject或NOT_STARTED数;
* busy_rejected:因事务槽占用拒绝数;overloads:独立应答池满或事务/事件序号耗尽次数;
* dropped_requests:过载丢新数及决定期间同点合法激活的静默丢弃数,后者不计rejected/busy_rejected/overloads;
* cancelled:本地取消、停用、断线、换代、发送/服务错误导致的事务结束总数。 */
unsigned long rejected, app_rejected, busy_rejected, overloads, dropped_requests, cancelled;
/* deactivated:成功撤销数;mismatches:选择失配数;其余三个为各期限导致的结束数。 */
unsigned long deactivated, mismatches, select_timeouts, decision_timeouts, execute_timeouts;
/* 事务期限或时间基准检查发现单调时钟倒退的次数;两个检查的计数可能重叠。 */
unsigned long clock_errors;
unsigned long time_expired; /* 过期激活,静默时不计rejected/dropped_requests;否定时另计rejected。 */
unsigned long time_rejected; /* IV/换算/基准/倒退/推算/超前错误;同时计rejected,不含过期。 */
} IEC104_CommandStatus_t;
/* 填写本版默认配置:启用45..51、58..64;51/64直接执行,其余均要求先选择。
* 决定15s、选择30s、执行60s,成功发ACT_TERM;时标窗口10s,主站时区UTC+8,过期激活静默丢弃。
* 默认启用带时标类型;受理前须设置可信时间基准,或显式关闭时差检查。
* 入参:config为调用方可写结构;NULL时无操作。返回:无;不创建实例或启动计时。 */
void IEC104_Command_Config_Default(IEC104_CommandConfig_t *config);
/* 从静态池创建并绑定一个遥控实例;不自动开启Slave或消费其通知。
* 入参:slave为有效Slave,寿命须覆盖实例;config非NULL,按值复制,调用后可修改原结构。
* 必须在开始消费Slave通知之前创建,以完整接收CONNECTED/ACTIVATED。
* 池容量IEC104_COMMAND_CAPACITY默认等于TCP服务容量;独立应答容量
* IEC104_COMMAND_REPLY_CAPACITY默认4,均在编译模块时通过宏配置。
* 启用51/64时要求选择的掩码应为type_mask & ~(TYPE_BIT(51) | TYPE_BIT(64)),
* 其中TYPE_BIT表示IEC104_COMMAND_TYPE_BIT;不能直接令select_required_mask等于type_mask。
* 裁剪type_mask后须同步裁剪select_required_mask,例如select_required_mask &= type_mask。
* 返回:成功为实例句柄;配置/Slave非法、重复绑定、池或实例身份序号耗尽时为NULL。 */
IEC104_Command_t *IEC104_Command_Create(IEC104_Slave_t *slave, const IEC104_CommandConfig_t *config);
/* 设置可信站内时间基准;utc_ms为UTC纪元毫秒,base_ms为同一时刻的Net单调毫秒。
* 不读Clock、不保存回调;只影响之后受理的激活,STOP/断线保留基准。
* 句柄无效、动作回调内、utc_ms超出TIME_MAX_SECONDS*1000+999或base_ms晚于当前Net时间时
* 返回false且保留原基准;成功返回true。允许尚未激活或离线时设置。
* 时效只在Handle_Event受理时判断;默认配置下再等待决定,动作可在时标后接近25s启动。 */
bool IEC104_Command_Set_Time_Base(IEC104_Command_t *self, uint64_t utc_ms, uint64_t base_ms);
/* 本站时间不可信时清除基准;不取消已受理事务。无效句柄或动作回调内无操作。 */
void IEC104_Command_Clear_Time_Base(IEC104_Command_t *self);
/* 归还静态实例槽;不隐式Cancel,不关闭Slave,也不代替应用释放站点引用。
* 入参:self为待销毁实例;事务槽、未读事件及全部独立应答槽必须均空,且不在动作回调内。
* 返回:成功true,句柄随即失效;条件不满足或句柄无效为false,实例保留。 */
bool IEC104_Command_Destroy(IEC104_Command_t *self);
/* 本地结束事务,原因CANCELLED;保留欠应用的FINISHED,清空模块尚持有的全部应答。
* 已交发送层的帧无法撤回;保持会话激活,不回滚物理动作。
* 入参:self为实例;回调中/无效句柄无操作。返回:无;应用仍须读完FINISHED。
* ENDED事务保持首次终结原因,不重复产生FINISHED。 */
void IEC104_Command_Cancel(IEC104_Command_t *self);
/* 接收Slave通知并推进会话/请求状态;不调用应用动作,不保存event或其ASDU借用指针。
* 入参:self为实例;event是所属Slave按FIFO取出的通知,须在通知内容仍有效时分派。
* 全部连接/启停通知必须送达;通知消费不受队列里后续WAIT_EVENTS阻挡。
* 返回:激活且视图可用时接手配置内的控制类型,当前会话状态通知也为HANDLED;
* 未激活的I帧、视图不可用、未接手类型或旧代次通知为IGNORED;
* 参数、来源、重入或服务错误为ERROR。非法请求可否定或丢新计数,HANDLED不表示已应答。
* SELECT_WAIT/EXECUTE_WAIT期间,同会话、同station_id、同IOA的合法激活静默丢弃,计dropped_requests,
* 返回HANDLED;不比较其他命令字段,不覆盖原请求/事件、不刷新期限。格式校验、撤销及其他点忙拒绝不变。
* 对IGNORED的I帧继续分派后续业务,最后由应用兜底;不要对HANDLED再发44。 */
IEC104_CommandEventResult_t IEC104_Command_Handle_Event(IEC104_Command_t *self,
const IEC104_SlaveEvent_t *event);
/* 推进会话检查、到期转换、发送队首及事务回收;不调用应用代码。
* 入参:self为实例;每轮网络/通知处理后调用,最多推进2*(1+独立应答槽数)步。
* 队首WAIT立即让出,不能用紧循环等待发送;需处理Slave通知/网络后再次调用。
* 返回:true包括无事、背压等待或事务已终结;false为参数/重入或服务错误。
* 发送故障也可能已经转成FINISHED,此函数返回true不代表链路或设备执行成功。 */
bool IEC104_Command_Process(IEC104_Command_t *self);
/* 取出一条应用事件,同一工作事件最多交付一次;取出不等于Accept/Reject消费。
* 入参:self为实例;event非NULL,接收可按值保存的事件副本,仅READY时写入。
* 返回:READY/EMPTY/ERROR,见NextResult;EMPTY也可能表示须先读Slave通知。
* 工作事件交付前检查会话/期限,已有FINISHED可在离线或尚有Slave通知时读取。
* 读取FINISHED可能立即释放模块的站点引用,应用跨循环引用必须此前已自行Acquire。 */
IEC104_CommandNextResult_t IEC104_Command_NextEvent(IEC104_Command_t *self,
IEC104_CommandEvent_t *event);
/* 接受一个已由NextEvent交付的工作事件;先消费身份,再同步至多调用一次action。
* 入参:self为实例;event须原样传回SELECT/EXECUTE事件;context原样转交action,不保存。
* SELECT的action可为NULL,选择生效但select_effect=false;EXECUTE必须提供action。
* 返回:OK只表示工作事件已消费,不表示动作STARTED或确认已交接;动作失败也返回OK。
* WAIT为先读Slave通知,事件未消费;STALE为身份/阶段失效,不得执行输出;
* ERROR为参数/重入/服务或确认资源错误,动作未调用。
* 即使action返回STARTED,提交确认时仍可能结束事务;应用应查询事务并继续接收FINISHED。 */
IEC104_CommandReplyResult_t IEC104_Command_Accept(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandAction_t action, void *context);
/* 不调用动作,消费工作事件,安排否定ACT_CON或未知IOA应答,以REJECTED结束事务。
* 入参:self为实例;event为尚未消费的SELECT/EXECUTE;cause为两个RejectCause之一。
* 返回:OK表示事件已消费且应答已安排,可能已提交或因随后故障被取消;
* WAIT未消费、须先处理通知;STALE身份/阶段已失效;ERROR参数/原因/重入或服务错误。
* FINISHED可先于否定应答交接,读取FINISHED不代表槽已释放。 */
IEC104_CommandReplyResult_t IEC104_Command_Reject(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandRejectCause_t cause);
/* 报告设备结果,结果只记录一次并冻结;不发返回信息,也不隐式Complete。
* 本函数及下列Submit_Return_Info/Complete只接受Accept动作返回STARTED的原EXECUTE事件。
* 入参:self为实例,event为上述事件,result为SUCCEEDED或FAILED。
* 可早于ACT_CON交接;设备完成后应及时报告,OK即停止执行期限,此后发送等待无业务超时。
* 返回:OK已记录;WAIT因未读通知尚未记录;STALE事务结束/超时或事件不符;
* ERROR参数/结果非法、已报告/已Complete、重入或服务错误。 */
IEC104_CommandReplyResult_t IEC104_Command_Report_Result(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result);
/* 通过原连接Try路径提交一帧返回信息,不经过可覆盖的NORMAL队列。
* 入参:self为实例;event为启动成功的原EXECUTE;asdu为有效、完整的监视方向ASDU。
* 类型1..21/30..40,至少一对象,COT=11或3,P/N=0,TEST/CA与命令一致;完整校验对象。
* IOA可与命令IOA不同;本版命令TEST恒为0。调用可以早于或晚于Report_Result,但须早于Complete。
* 模块有更早待交应答、确认未交接、网络背压或未读通知时WAIT,不缓存ASDU、也不占排序位置。
* 返回:OK为发送层已接手且帧序固定,可释放/复用ASDU缓冲,不能重交;WAIT须保留ASDU重试;
* ERROR为ASDU/参数/重入/服务错误或Complete后仍待收尾,尚未接手;STALE为事务/事件失效。
* 正常STOP且通知待分派时先WAIT;其他发送ERROR/FATAL终结事务并返回STALE,不可重试旧ASDU。
* 必要的后续变位由应用另按COT=3上报。
* 同一变化勿再经Enqueue_Event重复发布;成功提交不表示对端ACK,断线不会迁移到新连接。 */
IEC104_CommandReplyResult_t IEC104_Command_Submit_Return_Info(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, const IEC60870_Asdu_t *asdu);
/* 声明本事务不再产生返回信息;按配置安排ACT_TERM并由Process完成后续交接。
* 入参:self为实例,event为启动成功的原EXECUTE;结果须已报告,全部返回信息须已提交OK。
* 允许零帧返回信息;可在确认未交接时调用,模块保持确认先于ACT_TERM的顺序。
* 返回:OK立即关闭返回阶段,不必重调;WAIT为未读通知,尚未接手;
* ERROR为尚未报告结果、仍待收尾时重复调用、参数/重入/服务错误;已结束/事件不符为STALE。
* 不发ACT_TERM时也须等待执行确认交接,之后才产生FINISHED;无发送等待时可能立即结束。
* 失败配置发TERM时P/N=0表示流程结束,不能据此认定设备成功。 */
IEC104_CommandReplyResult_t IEC104_Command_Complete(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event);
/* 只读查询仍占槽的事务,可在动作回调内使用,不做会话推进或期限转换。
* 入参:self为实例;event可为该事务任一已交付的SELECT/EXECUTE/FINISHED;info非NULL。
* 返回:true写入快照,ENDED的active=false;无效参数/身份、槽已回收时false且不修改info。
* 不保留历史事务;刚读出的FINISHED若已触发槽回收,使用它查询也会false。 */
bool IEC104_Command_Get_Transaction(const IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandTransactionInfo_t *info);
/* 只读查询会话、调度提示及累计计数,可在动作回调内使用。
* 入参:self为实例,status非NULL接收快照;计算剩余时间但不转换已到期事务。
* 返回:true写入快照;句柄/参数/Slave服务无效时false且不修改status。
* events_pending优先读Slave通知;need_process用于立即调度,timer_active配合剩余时间等待。
* need_process=false不免除每轮网络推进后的Process,也不代表应用没有待重试的ASDU/Complete。 */
bool IEC104_Command_Get_Status(const IEC104_Command_t *self, IEC104_CommandStatus_t *status);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_COMMAND_H */
@@ -0,0 +1,77 @@
# IEC60870-Slave
IEC 60870 子站协议库,当前提供 IEC104 子站、Windows TCP 后端、总召、计数量召唤、Type 103时钟同步,以及Type 45–51遥控/遥调和Type 58–64带时标命令。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_ClockSynchronization.c/h # 校时校验、显式动作与确认
│ │ └── IEC104_Command.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/`。
`-Test` 包含独立的 `test/iec104_command_test.c`:TCP服务容量1/2与遥控独立应答槽容量1/4组合成四组构建,HIGH容量为251字节。真实TCP回环入口为 `test/slave_example_loopback_test.cjs`,验证选择、执行、返回信息、撤销及遥控后总召读值。
接入自己的工程时,将 `104/` 和 `common/` 加入头文件搜索路径,使用总召、计数量、校时或遥控业务时再加入 `104/business/`;按所用模块选择源文件,仅使用公共模块时只需 `common/`。Windows 网络后端需链接 `Ws2_32`。统一入口为 `IEC104_Slave.h`,总召使用 `IEC104_Interrogation.h`(编译 `104/business/` 下的引擎和GeneralInterrogation.c),计数量使用 `IEC104_CounterInterrogation.h`,校时使用独立的 `IEC104_ClockSynchronization.c/h`,遥控使用独立的 `IEC104_Command.c/h`。接入约定见[计数量设计](docs/104/IEC104_CounterInterrogation_设计架构.md)、[时钟同步设计](docs/104/IEC104_ClockSynchronization_设计架构.md)和[遥控设计](docs/104/IEC104_Command_设计架构.md)。
子站示例默认使用模拟时钟;收到校时命令只更新模拟值。需要修改Windows系统时钟时显式选择`--clock system`,默认模式及自动测试均不修改系统时间。
命令模块默认启用Type 45–51及58–64全部14种类型(type_mask=0x3FFF);除只能直接执行的51/64外,其余12种默认要求先选择(select_required_mask=0x1FBF)。应用可裁剪type_mask,并须同步裁剪select_required_mask。示例显式只启用已接入的45/46/49/58/59/62,全部要求先选择后执行:45/58的6001..6008控制Type 1遥信1001..1008;46/59的6101..6108控制Type 3双点位置1101..1108,双点初值均为1;49/62的6301..6308写入Type 11遥测2001..2008。站总召及对应分组读取更新后的运行值,帧序与初值不变。
带时标激活默认按10秒时差窗口检查,主站时区为UTC+8;校时成功后应用向Command提供UTC/单调时间基准,过期激活默认静默丢弃。示例只更新内存点值,设备输出由应用动作接入。Type 51/64没有选择位,只能直接执行;完整接入与时效边界见[子站示例](docs/examples/IEC104子站示例.md)和[遥控设计](docs/104/IEC104_Command_设计架构.md)。公开结构扩展后,库与调用方须一起重新编译。
详细资料见[文档索引](docs/README.md)、[模块说明与接入示例](docs/104/README.md)、[子站示例说明](docs/examples/IEC104子站示例.md)和[完整测试命令](docs/test/IEC104测试文档.md#2-编译与运行)。
@@ -0,0 +1,139 @@
# IEC104 遥控、遥调与带时标命令设计
`104/business/IEC104_Command.c/h`统一提供Type 45–51及58–64这14种控制命令。业务入口为Slave,实例使用静态池,不动态分配内存、不保存应用回调;所有入口与所属Slave在同一事件循环中串行调用。一个Slave至多绑定一个Command实例,一个实例至多保留一个命令事务。默认启用全部14种类型,应用可通过type_mask裁剪;未启用或范围外的类型返回IGNORED,交给后续业务或未知类型兜底。
## 1. 职责与默认配置
模块管理请求校验、选择与执行匹配、撤销、三类期限、应答排序、事务身份及 FINISHED。应用管理点表、权限、闭锁、返校和设备输出,显式报告设备结果并构造监视方向返回信息。协议结束不代表物理输出已经停止。
| 配置 | 默认值 | 含义 |
|---|---|---|
| type_mask | 0x3FFF,全部14种类型 | 可裁剪45–51及58–64;非零,bit14/15保留且不能置位 |
| select_required_mask | 0x1FBF,除51/64外全部类型 | 必须是type_mask子集,且不能包含无S/E位的51/64;裁剪type_mask时须同步裁剪,其他类型清除此位允许直接执行 |
| decision_timeout_ms | 15000 | 工作事件生成至应用决定;0 禁用 |
| select_timeout_ms | 30000 | 选择生效至匹配执行被模块受理;不得为0 |
| execute_timeout_ms | 60000 | 执行动作返回 STARTED 至 Report_Result;0 禁用 |
| send_act_term | true | 成功执行后发送 ACT_TERM |
| act_term_on_failure | false | 显式启用时也发送失败执行的终止;send_act_term=false 时忽略 |
| time_tag_max_delay_ms | 10000 | 带时标激活的时差上限;0只关闭时差检查,仍拒绝IV及UTC换算失败 |
| master_utc_offset_minutes | 480 | 主站时标标准时区,范围−1439..1439分钟;SU由公共时间工具处理 |
| negative_on_expired | false | 过期激活默认静默丢弃;true时回否定ACT_CON |
TEST=1 不执行动作,返回否定确认。广播 CA=65535 不接受。失败终止可选策略发送原执行请求的 COT=10、P/N=0,表示执行过程终止,不表示设备执行成功;结果由应用监视信息和 FINISHED(EXECUTION_FAILED) 表达。该策略为本模块接入约定,默认不开启。
## 2. 请求校验和选择匹配
标准104采用2/2/3参数,6字节公共头后接3字节IOA;命令元素从`IEC104_ASDU_HEADER_LEN + 3U`开始。规范ASDU长度由类型决定:
| 类型(无时标/带时标) | 命令元素 | ASDU长度 | 选择与校验 |
|---|---|---|---|
| 45 / 58 | SCO | 10 / 17 | 有S/E;保留位必须为0 |
| 46 / 59 | DCO | 10 / 17 | 有S/E;DCS只能为1、2 |
| 47 / 60 | RCO | 10 / 17 | 有S/E;RCS为1降一步、2升一步 |
| 48 / 61 | NVA + QOS | 12 / 19 | 有S/E;原始有符号16位定点值 |
| 49 / 62 | SVA + QOS | 12 / 19 | 有S/E;有符号16位标度值 |
| 50 / 63 | R32 + QOS | 14 / 21 | 有S/E;拒绝NaN及正负Inf |
| 51 / 64 | BSI | 13 / 20 | 无S/E,只能直接执行 |
配置内请求先检查当前事务到期,再检查唯一完整对象。COT非6/8回45,CA未注册或广播回46,IOA=0回47;之后检查SQ、规范长度、P/N、TEST、命令值及CP56格式,错误回否定7/9。公共解析器负责SCO/DCO/RCO及CP56校验;QU/QL的业务含义交应用解释。对象数量不符、或到达模块的原文不足规范长度时计rejected并丢弃,不能补零伪造对象。线上已知类型的截短通常更早由Receiver拒绝,不会到达Command,不能据此期待Command计数增加或连接继续。
过长请求只回显规范长度前缀,VSQ规范为1;除VSQ、COT和P/N外保留原文字节,包括非法值。回显不重新构造合法对象。已识别的拒绝和丢弃均返回HANDLED。
只有SELECT_WAIT、EXECUTE_WAIT两个决定阶段,对同会话generation、同station_id、同IOA且通过全部现有校验的COT=6激活静默丢弃,返回HANDLED。是否已被应用取走工作事件不影响此规则;不比较类型、OA、S/E、状态或QU,但新请求仍须通过type_mask、COT、寻址、格式和值校验。只增加dropped_requests,不增加rejected、busy_rejected或overloads;不覆盖原请求、不刷新决定期限、不生成新事件或应答。先检查到期,已到期事务不再适用此规则;其他点仍忙拒绝,COT=8仍按撤销处理。
决定期间同点激活的丢弃在时标判定之前;其他带时标激活在SELECTED匹配和忙判断之前执行第8节的时效检查。这样匹配的过期执行不会绕过检查,忙时其他点的过期请求也不会产生迟到的忙否定。
SELECTED阶段的选择执行要求同会话、原站点注册编号、IOA、类型、OA,且值字节及屏蔽S/E后的命令字节相同。状态、QU/QL均参与比较,CP56不比较,允许选择和执行使用不同时标;45与58等跨类型组合仍为MISMATCH。浮点按原始字节比较,+0与−0不同。此阶段选择确认尚未交接时,通过时效检查的执行按忙拒绝并保留选择;确认交接后同点不匹配执行终结为MISMATCH,否定回显本次执行。其他点的新激活只忙拒绝。匹配前检查选择期限,到期选择不能复活。
撤销按会话、站点编号、IOA、类型、OA匹配,不比较值、状态或限定词,不判时差;带时标撤销仍须通过CP56格式检查。SELECT_WAIT、SELECTED、EXECUTE_WAIT中的匹配撤销复用事务存储,肯定DEACT_CON回显撤销请求。执行已经启动、事务已结束或没有匹配时否定撤销。发送接口已经返回OK的选择确认不能撤回,随后DEACT_CON按顺序交接。
## 3. 应用交互与设备结果
事件为纯值副本,身份包含模块实例、事务、工作事件序号和类型。Accept/Reject只接受已经取出且未消费的工作事件。模块每次从已保存的规范原文解码请求;调用者修改事件副本不能改变执行参数。
状态类45–47及对应时标类型使用state和QU;48–50使用`value.normalized/scaled/short_float`及QL,state为0;51/64使用`value.bitstring`,state、qualifier及is_select均为0。normalized保留16位定点原始值,实际值为normalized/32768.0。带时标请求另含原始CP56(保留IV/SU)及换算后的UTC毫秒,非时标请求相关字段清零。毫秒值包含CP56的毫秒余数,不能只把整秒乘1000。
保存的原文再次解码失败属于内部错误。Accept在安排肯定确认和消费事件之前解码,失败按SERVICE_ERROR收尾且不调用动作;NextEvent失败不把未交付工作事件标为已读,已交付事务仍保留FINISHED义务。若终结时原文仍不可解码,只交付事务身份及SERVICE_ERROR,不把值字段当作动作参数;应用仍可读取终结事件并释放资源,避免坏原文永久占槽。
| 入口 | 成功后的含义 |
|---|---|
| Accept(SELECT) | 消费选择事件;可无动作,仅建立协议选择 |
| Accept(EXECUTE) | 消费执行事件并至多调用一次非NULL动作 |
| Report_Result | 冻结成功/失败结果并停止执行期限,重复报告报错 |
| Submit_Return_Info | 一帧已被发送层接手并固定顺序;WAIT未接手 |
| Complete | 关闭返回信息阶段,模块按配置继续交接终止;重复调用报错 |
动作返回NOT_STARTED必须保证没有副作用;STARTED表示选择生效或执行启动;FAILED表示尝试失败且可能留下副作用。SELECT的NULL动作不置物理副作用标志。执行动作返回STARTED后,应用可以在确认尚未发送时调用Report_Result,不把发送等待算成设备执行耗时。动作返回FAILED的否定确认同样使用Try,不能越过已有过程信息。
Accept的OK只表示事件被消费,不表示动作成功。应用应检查自己的动作结果或Get_Transaction中的执行阶段。报告结果后可以提交零帧或多帧返回信息,全部交接后Complete。每个WAIT都须保存当前交互,先处理网络和通知再重试;不能越过失败的Report_Result直接Complete。
Get_Transaction仅查询当前尚占槽的事务:ENDED仍可查询active=false,槽释放后旧事件查询返回false且不修改输出。FINISHED、旧SELECT及旧EXECUTE不能用于启动新动作;已消费EXECUTE仅供其对应的结果、返回信息和完成接口使用。
## 4. 状态与期限
状态依次为SELECT_WAIT、SELECTED、EXECUTE_WAIT、EXECUTING、ENDED;直接执行跳过选择阶段。EXECUTING内部另记录结果是否已报告、返回阶段是否关闭和确认/终止交接状态,避免重复动作、覆盖结果及重复终止。
期限采用Net单调毫秒;已用时间大于或等于期限即到期,时钟倒退按到期并计clock_errors。期限配置为0只禁用时长限制,正在决定、选择或等待设备结果时仍检查单调时钟倒退。决定期限从工作事件生成开始,包含事件尚未被应用取走的时间;选择和执行期限从动作返回后重新采样开始。这里的请求到达指Handle_Event受理时刻,包含Slave通知排队时间。
Accept/Reject消费交互后停止决定期限;Report_Result后停止执行期限。选择期限有意覆盖选择确认的发送等待,作为返校保持上限。执行超时不发ACT_TERM,已经执行动作所对应的肯定确认仍可继续提交;随后STOP或Cancel可以清除尚未提交的应答。结果报告后没有额外业务收尾期限,应用漏调Complete会持续占槽,直到明确取消或会话结束。
## 5. 通知、FIFO与返回信息
Handle_Event先校验来源并按Slave的FIFO消费连接/启停通知,不因后续未读通知而拒绝当前通知。有效I帧在匹配前检查期限;动作及发送入口先Check_Request,再处理到期。WAIT_EVENTS时不消费应用交互。NextEvent在离线时仍能交付已有FINISHED。
事务必要应答保存在事务槽,独立校验/忙/撤销否定保存在固定回复池,各自取得全局顺序号。Process只提交最早应答;队首WAIT时停止本轮。返回信息直接Try前也检查模块内是否存在更早应答,避免绕过本地FIFO。WAIT的应用ASDU未被接纳、不占持久排序位置。ACT_TERM在Complete被接受后安排,保证确认和已经接手的返回信息在先。
返回信息必须能完整解析,为监视方向Type 1–21或30–40,COT=11或3、P/N=0、TEST与命令一致、CA等于命令CA;监视IOA可以不同。它绑定原连接并走Try,不进入允许覆盖及跨连接保留的NORMAL队列。断线时尚未提交部分由应用放弃;如仍需报告设备变化,可独立用COT=3发布过程事件,不继续旧命令事务。
提交返回ERROR但原连接健康、仅因STOP通知待消费时让出;其他ERROR/FATAL终结为SEND_ERROR并清理待交应答,FATAL不可重试。Try必须等已有HIGH/NORMAL及当前帧让出,持续积压时不承诺有界完成。全部等待时调度不能空转。
## 6. FINISHED、引用和退出
事务级“曾交付工作事件”与当前事件已读状态分开保存。曾取走SELECT或EXECUTE的事务恰好产生一次FINISHED;从未取走任何工作事件就结束的事务不产生FINISHED。此规则也适用于直接执行尚未读就撤销,以及已读SELECT但后续EXECUTE未读时撤销/超时。
成功和执行失败在最终应答交接后交付FINISHED;其他终结原因立即交付,不等待否定或DEACT_CON。终结原因一旦确定不被后续STOP/Cancel改写,清理不重复通知。FINISHED保存最后一个接纳的工作请求和副作用标志,不被撤销或不匹配请求的回显覆盖。
事务只有FINISHED已读(或无需交付)且所有所属应答已交接/取消后才释放。释放前实例仍忙。独立应答池也属于实例生命周期:Cancel/STOP/发送失败清理它,Destroy要求事务和整个回复池均空且不在动作回调中。
事务创建时Acquire原站点,释放时Release。应用首次工作事件需要跨循环保留context或启动异步输出时,另行Acquire;在FINISHED清理且设备实际空闲后Release,不能等模块释放引用后再申请。站点删除阻止新请求,已有事务持原注册编号完成;同CA新节点不接续旧选择。跨协议结束仍在运行的输出由应用继续维护忙状态。
退出顺序:Command_Cancel,读空FINISHED并清理应用引用,Slave_Close,读空并分派Slave通知,Command_Destroy,移除站点,Slave_Destroy。动作回调只允许Get_Status/Get_Transaction,不得重入修改入口、推进网络或发布上报。
## 7. 布局、固定存储与验证
私有`COMMAND_LAYOUTS(X)`是唯一类型布局清单,记录type_id、value_length、has_se、has_time。X-macro分别展开只读布局表和字节数组union,规范长度按“9 + 值长度 + S/E字节数 + CP56长度”生成;`COMMAND_MAX_LENGTH`取该union的sizeof,目前为21字节,并静态检查不超过104的249字节ASDU上限。这是本模块14种控制命令的容量上界,不代表所有ASDU或其他业务的存储模型。
事务request_bytes、reply_bytes、独立回复槽及提交局部缓冲统一使用该容量,持久记录另存有效长度。复制、解析、包装和发送只使用规范有效长度,不能把sizeof(buffer)当作报文长度。原工作请求与撤销/失配回显分别保存,长短槽复用不会发送旧尾部。
S/E偏移只由`Se_Offset`计算,读取只能在已确认has_se的`Is_Select`或`Same_Command`中发生。51没有该字节,64相同偏移是CP56首字节,不能先读再判断。`Decode_Element`调用公共层公开的类型解码接口,不依赖内部信息对象布局表。布局、阶段支持掩码和实例type_mask分别表达“认识”“允许启用”和“本实例启用”,互不替代。
`IEC104_COMMAND_CAPACITY`默认跟随TCP服务容量;每实例一个事务槽,`IEC104_COMMAND_REPLY_CAPACITY`默认4。事务必要应答不依赖回复池剩余容量。池满只拒绝/丢新,不覆盖活动事务,不主动断开连接。Process每轮推进次数有界。实际sizeof及配置矩阵结果统一记录在[测试说明](../test/IEC104测试文档.md),不把宿主机结果当作其他目标的预算。
示例地址、动作与返回信息见[子站示例](../examples/IEC104子站示例.md)。45/58命令6001..6008对应Type1遥信1001..1008;46/59命令6101..6108对应Type3位置1101..1108,双点初值均为1;49/62命令6301..6308写入Type11遥测2001..2008。总召及对应分组逐批读取应用当前值,反映执行后的状态;点表、限值及分组属于应用,不改变Command模块职责。
验证覆盖TCP容量1/2与回复容量1/4、独立手写的14种类型长度、值与时标校验、匹配、变长FIFO、期限边界、动作只调用一次、通知优先、部分写入、结果先于确认、撤销及站点引用,并使用真实本机TCP核对遥控、设定值、带时标执行和过期丢弃。测试期望不共用实现布局清单,避免表错时输入和期望一起错。决定窗口内的同点激活丢弃不依赖内容相同,也不提供窗口外去重;事务释放后,相同ASDU按新请求处理,允许直接执行时仍可能再次输出。
扩展仍须保持事件身份、引用与交接契约;公开Request、Event、Config、Status新增字段后,库及调用方须一起重新编译,不承诺二进制布局兼容。新增类型需同步清单、解码分支和独立测试;新值类型或掩码范围外类型还涉及公开接口。容量会随清单最大长度增长,须重新测量目标内存;这里不提供跨业务统一存储管理,也不实现并发事务、Type70/102/105/107或突发上送。
## 8. 带时标激活与时间基准
应用调用`IEC104_Command_Set_Time_Base(self, utc_ms, base_ms)`提供同一时刻的UTC纪元毫秒和Net单调毫秒。句柄无效、动作回调内调用、UTC超出公共时间工具范围或base_ms晚于当前单调时间时返回false;失败不替换原基准。`Clear_Time_Base`在本站时间不可信时清除基准,动作回调内调用不生效。模块不读取Clock,不保存时间回调;STOP和断线保留基准,更新只影响随后受理的请求。
判定仅针对58–64的COT=6激活,在Handle_Event受理时做一次。公共CP56格式错误已经按请求校验回否定;格式合法后先检查IV及UTC换算,随后才应用max=`time_tag_max_delay_ms`:
| 条件 | 处理与计数 |
|---|---|
| IV=1或UTC换算失败 | 否定7,time_rejected与rejected加一;max=0仍拒绝 |
| max=0 | 不比较时差,不要求时间基准 |
| max非零且无基准、now早于base_ms、推算溢出或超出时间范围 | 否定7,time_rejected与rejected加一;单调时钟倒退另计clock_errors |
| 站内UTC − 命令UTC ≥ max | time_expired加一;默认静默丢弃,negative_on_expired=true时回否定7并计rejected |
| 命令UTC − 站内UTC ≥ max | 否定7,time_rejected与rejected加一 |
| 其他 | 继续匹配选择、忙检查或创建事务 |
站内UTC由`utc_ms + (now - base_ms)`推算,先检查回退和加法范围。CP56按master_utc_offset_minutes换算,SU由公共工具处理,并加回毫秒余数。静默过期不计rejected或dropped_requests,不占事务槽、不生成工作事件或FINISHED;time_base_valid仅表示持有基准,并非每次查询都重新验证其时效。
clock_errors按发现异常的检查次数累计,事务期限检查和时间基准检查可能在同一请求中分别增加,不能把它当作异常请求数量。
执行请求过期时不改变已有选择,它继续保留至select_timeout_ms。撤销不做时差、IV或UTC换算判定,但仍检查CP56编码格式。决定期间同点激活先按原规则丢弃,不增加时间计数。默认静默丢弃避免迟到否定被主站误认为重试结果;max、IV策略、时区、过期响应和ACT_TERM应与主站互操作约定一致。
时效只在受理时检查,Accept前不再检查命令时标;受理后的等待由决定期限控制。默认10秒时差窗口加15秒决定期限,动作启动时命令可能已接近25秒旧。需要更严格限制时,应用可在动作中检查request.time_utc_ms并返回NOT_STARTED;这不是模块承诺的动作时效上限。
@@ -0,0 +1,534 @@
# 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信息对象模块)。
> **业务同步**:2026-10-31。总召、计数量、Type103校时及Command业务已实现;Command统一处理Type45–51及58–64的遥控、遥调和带时标命令,默认启用全部14种,51/64直接执行,其余默认先选择。采用单事务、显式动作及返回信息提交,设备策略和物理输出由应用提供,见[遥控设计](IEC104_Command_设计架构.md)。
> **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后立即回报增量;部分发送下轮续传,业务不再直接调用底层完成接口 |
| 总召、计数量召唤与Type 103校时 | 独立业务模块(已实现) | 通过Slave事件及共用站点表分派,总召/计数量使用业务事件;校时由Execute_Next显式执行,确认回显原时标,见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md) |
| 单点、双点遥控(Type45/46) | IEC104_Command(已实现) | 独立静态池、单事务互斥;管理选择执行、撤销、三类期限、原文确认及按事务提交返回信息,见[遥控设计](IEC104_Command_设计架构.md) |
| 遥调及带时标命令(47..51、58..64) | IEC104_Command(已实现) | 与45/46共用事务和FIFO;按类型解析值与限定词,51/64直接执行;应用提供时间基准,受理带时标激活时检查时效 |
| 实际遥信/遥测数据源、输出及设备设时 | 应用接入层 | 按业务模块请求读取点表或执行设备动作;示例提供模拟点表、遥控输出和模拟时钟,协议库不自动操作硬件 |
### 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(对象及四类业务已实现) | 公共对象编解码、总召/计数量、校时及Type45/46遥控;继续接入真实数据源和设备输出 | 验证地址路由、召唤进度、校时原文确认、遥控选择执行/撤销/期限及返回信息顺序;模拟模式不设系统时间,硬件设时、实际输出和主站联调须另行验收 |
| 5(已实现) | 遥调47..51及带时标命令58..64 | 私有布局统一推导长度与容量,公共解析器校验值和CP56,按值及QU/QL匹配;时间基准与过期策略由配置和应用接入,实际主站互操作仍须联调 |
---
@@ -0,0 +1,444 @@
# IEC104 链路模块
当前子站业务统一包含`IEC104_Slave.h`:Slave内部持有不透明TcpServer,提供生命周期、事件循环、主动上报和状态查询,并在NextEvent交付I帧时自动建立ASDU视图及原连接的共用应答接口。业务不再手动创建TcpServer、绑定generation或安装函数指针。入口和延迟任务示例见[子站统一入口](#子站统一入口)。
Slave提供[共用站点表](IEC104_API接口汇总.md#station-registry):应用提供固定地址节点,动态注册多个CA和业务context,重复CA拒绝。删除立即退出路由,已有引用和遍历结束后才回收;Is_Reusable为true后应用才能释放存储。总召设置config.use_station_registry=true后按请求CA选站;多目标广播配置每站状态区,以一个任务执行“全部确认、逐站数据、全部终止”。其他业务可复用相同的查找、引用及快照遍历接口。
总召由独立库模块[IEC104_Interrogation.c/h](../../104/business/IEC104_Interrogation.h)实现,支持QOI=20站总召、21..36分组总召及多类型无时标监视数据,负责批次校验、发送等待、主站撤销、业务超时及停用/断线取消。应用通过BEGIN/NEED_DATA/FINISHED事件和显式应答管理IOA、分组、遍历、逐批取值及终结;[子站示例](../../examples/iec104_slave_example.c)提供模拟点表、事件分派和main,构建运行见[示例说明](../examples/IEC104子站示例.md)。
内部调用关系为`Slave → TcpServer → Connection → SendManager/Receiver/Link`,网络I/O仍经Net执行,当前提供Windows后端。现有单主站、静态内存、HIGH/NORMAL队列、事件FIFO、Prepare/Process或Poll调度策略保持不变。共用ASDU及Slave发送接口继续使用IEC60870前缀;公共层不依赖104传输实现。下文的底层接口说明供维护和独立TCP接入使用,业务默认通过Slave操作服务。
`Get_Status`同时提供`high_queue`和`normal_queue`的待发条目数、保留原文条目数及总容量/已用/空闲字节数,先检查`asdu_queue_status_valid`。待发数不包含已交给发送模块的ASDU;NORMAL保留到ACK前的原文仍计入存储量,字节统计包含条目头。Init成功后离线也可查询,Close后统计无效;队列为空不代表发送或确认全部完成。公开状态结构已扩展,库与调用方须一起重新编译,字段含义见[ASDU队列积压查询](IEC104_TcpServer_设计架构.md#21-asdu队列积压查询)。
`NextEvent`区分连接建立(CONNECTED)、数据传输激活(ACTIVATED)、停用(DEACTIVATED)和连接断开(DISCONNECTED),四者共用`event.transport.data.connection`快照及对端地址。启停事件在对应ACT成功受理并提交状态变化后产生,CON可能仍待发送;同状态重复ACT不重复通知,直接断开不补造停用事件。业务可按事件类型处理启停,无需通过查询状态比较变化。
`Get_Status`的`pending_state_event_count`报告尚未消费的ACTIVATED/DEACTIVATED数量,不能追溯已消费的启停历史。当前请求的错误应答可在同步分派中调用`IEC104_Slave_Reject_Request`,支持COT=44..47,复用HIGH且不改输入事件;不新增示例应答队列。调用时机、WAIT丢新策略与FATAL不可重试约定见[同步拒绝当前请求](IEC104_API接口汇总.md#reject-request)。
发送队列NORMAL与`NextEvent`业务通知FIFO是两份独立存储:NORMAL满时可覆盖旧ASDU原文;已入队业务通知逐条保留,不覆盖、不合并启停变化。通知按有效载荷长度存放于字节环,配置宏仍表示保证条数:默认普通30条最长通知、启停4条及收尾32条记录加1个最长I帧和1个断开通知。当前x64 ABI下环占11243字节,接收暂存默认2048字节,两块合计13291字节,比原来减少43.9%;16字节APDU可在普通区缓存185条。普通通知不能占用启停及收尾预留;启停配额满时暂缓受理后续改变状态的ACT,已有CON继续发送。详细公式见[TCP服务设计](IEC104_TcpServer_设计架构.md)。
新I帧无普通空间可交付时保留原帧并暂停续收,`Prepare`及`Get_Status`的`receive_paused`为1;NextEvent腾出空间后安排Process恢复,等待中的I帧先于回执交付。已确认回执因普通空间或本轮预算不足暂留内部时,可能晚于随后发生的启停通知交付,保持不因回执积压而暂缓启停CON的行为。通知积压不直接断连,但保留I帧会阻挡后续S/U;长期不消费仍可能触发原有协议超时。应用应在每轮Process/Poll后及时取出事件。此次存储调整保持公开API及事件结构不变。
`IEC104_TcpServer_Prepare(server, &info, error)`只读导出socket关注集合、最近协议等待上限及本地续跑标志;应用统一等待其他模块和TCP事件后,用`IEC104_TcpServer_Process(server, &ready, error)`有界推进,Process不执行等待。socket只借给事件循环注册,关闭/重连后的旧通知按注册token过滤;真实I/O与Connection调度仍由TCP内部处理。活动连接未EOF时入队成功、背压解除和预算耗尽后按need_process安排续跑,不能只等待新的socket边沿;离线或EOF后新增NORMAL只缓存,不因此触发空转。EOF后也停止为此前入队但尚未编号的NORMAL创建新I帧,将这些原文留给下一连接;当前已编号帧、已受理HIGH及控制帧继续按原规则收尾。接入约定见[TCP事件循环说明](IEC104_TcpServer_设计架构.md#31-接入应用自己的事件循环)。
`IEC104_TcpServer_Poll(server, max_wait_ms, error)`组合相同的准备和推进核心,中间至多等待一次:0只查询就绪,正数允许等待并按最近协议期限缩短。原poll_interval_ms配置项已移除,业务无需分别驱动内部收发和Tick。
当前实现支持控制方向提前发送:控制站可在ACT成功入队后预留命令I帧,并在ACT之后发送,不必等待CON;被控站成功受理STARTDT_ACT即进入STARTED/IDLE,开放I/S处理与发送准备,已生成的CON按FIFO先于随后准备的I帧和S确认发送。初始STOPPED/IDLE仍禁止I/S接收及新I帧发送。标准适用条件与连接组管理边界集中说明于 [链路设计第3.2节](IEC104_Link_设计架构.md#32-接收-i-帧)。
| 阅读入口 | 负责内容 |
|---|---|
| `IEC60870_Types.h` | 101/104共用的应用层参数、ASDU存储上限;不包含传输状态 |
| `IEC104_Types.h` | 104尺寸常量、标准104应用层初始化值、ASDU优先级及链路配置 |
| `IEC60870_Codec.c/h` | 101/104共用的参数校验、ASDU公共头解析及已知类型最低载荷长度检查 |
| `IEC104_Codec.c/h` | 104专用APCI检查、I/S/U帧编码和帧类型 |
| `IEC60870_Asdu.c/h` | 通用ASDU容器、布局校验、对象分派与原文应答;使用说明见下文“ASDU信息对象模块” |
| `IEC60870_Slave.c/h` | 101/104共用的主站连接接口,分派ASDU发送、激活确认和激活终止 |
| `IEC104_Slave.c/h` | 子站统一入口:TCP服务、ASDU/应答接口、原连接隔离及各业务共用的CA站点表 |
| `business/IEC104_Interrogation.c/h` | 共用召唤引擎:任务、广播、期限、事件、取消与发送等待;接入见[总召流程](IEC104_完整调用流程.md#flow-interrogation) |
| `business/IEC104_GeneralInterrogation.c/h` | Type100规则及兼容的Interrogation_Create入口 |
| `business/IEC104_CounterInterrogation.c/h` | Type101规则、创建及一次动作执行入口;见[计数量设计](IEC104_CounterInterrogation_设计架构.md) |
| `business/IEC104_ClockSynchronization.c/h` | Type103校时校验、显式动作及原文确认;见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md) |
| `business/IEC104_Command.c/h` | Type45–51/58–64遥控、遥调、选择执行、撤销、时标判定及返回信息;见[遥控设计](IEC104_Command_设计架构.md) |
| `IEC104_TcpTypes.h` | Slave与TcpServer共用的配置、调度、事件和状态值类型,无服务操作接口 |
| `IEC60870_InformationObjects.c/h` | 67种独立信息对象,覆盖监视、命令、初始化、参数和文件;编码通过只读函数表分派,支持变长文件段 |
| `IEC60870_Time.c/h` | 101/104共用时间工具:CP16/CP24/CP56Time2a编解码、本地标准时间转换、SU修正、日历/星期和纪元秒数工具;使用约定见[时间工具设计](../common/IEC60870_Time_设计架构.md) |
| `IEC60870_Clock.c/h` | 101/104共用时间源接口:直接读取和设置固定UTC+8日历;Clock.c内置Windows读写,Clock.h预留RTC接口,由编译选项二选一 |
| `IEC104_Receiver.c/h` | 每连接严格帧头检查、成帧、APCI及ASDU最低长度检查、完整APDU借用视图和显式释放;不处理链路序号 |
| `IEC104_Connection.c/h` | TCP内部连接协调:I/S/U分派、接收保留与释放、部分发送、超时推进及旧代次隔离;内部设计见[连接层设计](IEC104_Connection_设计架构.md) |
| `IEC104_TcpServer.c/h` | 底层TCP服务:IPv4单主站、静态池句柄、事件FIFO;Prepare/Process或Poll推进,Get_Remote_Address查询对端,Close_Connection按代次断开单连接 |
| `IEC104_TcpServer_Config.h` | 实现的编译期容量配置;默认一个服务器静态槽,可调整池及缓冲容量 |
| `IEC104_Net.h` / `IEC104_Net_Windows.c` | 通用网络接口及当前Windows实现:非阻塞监听/接入/收发、就绪等待、关闭及单调计时 |
| `IEC104.c/h` | 已恢复的旧单路逐字节接收入口,保留原有全局接收器及私有Link状态 |
| `IEC104_SendBuffer.c/h` | 唯一当前I帧、独立紧凑S/U队列、部分写入续传及本地发送期限 |
| `IEC104_AsduQueue.c/h` | 调用方字节缓冲的两级变长队列,HIGH满时拒绝、NORMAL覆盖最旧原文,支持显式重连恢复 |
| `IEC104_SendManager.c/h` | Connection内部的Link/SendBuffer修改入口:会话初始化、发送调度、控制收发、完成分派、ACK和回执,以及发送期限查询 |
| `IEC104_Link.c`、`IEC104_Link_Common.h` | 初始化、完整状态校验、I/S序号、k/w窗口、S确认、公共超时判定和TESTFR |
| `IEC104_Link_Master_StartStop.c/h` | 主站发STARTDT/STOPDT_ACT,收CON,处理等待超时 |
| `IEC104_Link_Slave_StartStop.c/h` | 子站收STARTDT/STOPDT_ACT,受理时更新启停状态,生成CON并处理停止排空及交错启停 |
| `IEC104_Link_Internal.h` | 三个实现文件共用的私有声明,不供业务调用 |
| `IEC104_Link.h` | 兼容总头文件,汇总公共、主站、子站接口 |
维护内部链路实现时,可按本端角色包含 `IEC104_Link_Master_StartStop.h` 或 `IEC104_Link_Slave_StartStop.h`,两者都包含公共接口。只处理I/S或TESTFR时包含 `IEC104_Link_Common.h`。旧内部代码可继续包含 `IEC104_Link.h`;模块发送完成接口统一以 `_Send` 结尾,该命名调整本身不改变参数和处理行为。改名不保留旧名兼容入口,内部调用方须同步更新名称并重新编译。
公共ASDU基础解析由 `IEC60870_Codec.c/h` 提供,业务需要解析公共头时包含 `IEC60870_Codec.h`;104的APCI检查和I/S/U编码由 `IEC104_Codec.c/h` 提供。Receiver分别调用两层:先检查APCI,再显式传入104参数调用 `IEC60870_Parse_ASDU_Header`。遇到非法帧头或截短的已知固定长度对象时返回FRAME错误,不推进序号或ACK,也不生成I_FRAME。未知/变长类型及具体字段继续由完整ASDU解析器和应用检查,边界见[基础设计](IEC104_设计架构.md#43-已知类型的最低载荷长度)。旧 `IEC104.c/h` 的全局接收入口保留原行为,不参与TcpServer路径。两个Codec均只依赖标准库及值类型,公共Codec不依赖104实现。
| 模块及构建依赖范围 | 生产源文件 |
|---|---|
| 101/104公共ASDU头解析 | `IEC60870_Codec.c` |
| 104 APCI与I/S/U编解码 | `IEC104_Codec.c` |
| ASDU信息对象编解码 | `IEC60870_Asdu.c`、`IEC60870_InformationObjects.c`、`IEC60870_Codec.c`、`IEC60870_Time.c`;C11及IEEE 754 binary32,不依赖TcpServer或Clock |
| 子站统一入口 | 在ASDU及TCP依赖之外增加`IEC60870_Slave.c`、`IEC104_Slave.c`;共用接口本身不依赖TCP或串口实现 |
| 104站/分组总召 | 在Slave及其依赖之外加入`104/business/IEC104_Interrogation.c`和`104/business/IEC104_GeneralInterrogation.c`,头文件搜索路径增加`104/business/`;应用按QOI选点表,响应NEED_DATA提交数据 |
| 104计数量召唤 | 引擎加`104/business/IEC104_CounterInterrogation.c`,头文件搜索路径增加`104/business/`;与总召分别创建,状态通知交付两边,动作通过Execute_Begin执行 |
| 104时钟同步 | Slave依赖加`104/business/IEC104_ClockSynchronization.c`,独立实例及静态池;只由Execute_Next执行同步action,模块不依赖Clock设备适配 |
| 104遥控/遥调 | Slave依赖加`104/business/IEC104_Command.c`,使用公共信息对象和Time解析;每Slave一个实例、一个事务,Accept显式执行动作,应用报告结果、提交返回信息并提供带时标命令的时间基准;模块不读取Clock |
| 101/104公共时间工具 | `IEC60870_Time.c`,对已有时间值进行日历校验、时间转换及编解码 |
| 101/104时间源:Windows系统时钟 | `IEC60870_Clock.c`、`IEC60870_Time.c`;`IEC60870_CLOCK_USE_RTC=0`(默认),Clock.c直接使用Windows的Kernel32接口 |
| 101/104时间源:RTC预留接口 | 上述两个源文件,定义`IEC60870_CLOCK_USE_RTC=1`,另需板级驱动实现`IEC60870_Clock_RTC_Read/Set`;具体芯片驱动暂不提供 |
| 独立子站发送缓冲 | `IEC104_Codec.c`、`IEC104_SendBuffer.c`;公共发送类型仍由Link公共头文件提供,无需链接Link实现 |
| 链路处理 | `IEC104_Codec.c`、`IEC104_Link.c`,再按所需启停接口加入主站/子站实现 |
| 子站发送管理 | `IEC104_SendManager.c`、`IEC104_AsduQueue.c`、`IEC104_SendBuffer.c`、`IEC104_Codec.c`、`IEC104_Link.c`、`IEC104_Link_Slave_StartStop.c` |
| 子站连接协调 | 上述子站发送管理源文件,加 `IEC104_Receiver.c`、`IEC104_Connection.c` 和 `IEC60870_Codec.c`;无需主站启停或日志实现 |
| Windows TCP子站模块 | 上述子站连接协调源文件,加 `IEC104_TcpServer.c`和`IEC104_Net_Windows.c`,应用工程链接`Ws2_32`;构建与接入见[TCP服务设计](IEC104_TcpServer_设计架构.md) |
| 独立流式接收 | `IEC104_Receiver.c`、`IEC104_Codec.c`、`IEC60870_Codec.c`;不依赖Link、SendManager或日志 |
| 旧单路逐字节接收 | `IEC104.c`、`IEC104_Codec.c`、`IEC104_Link.c`、`IEC104_Log.c` |
时间工具以`IEC60870_Time`作为公共入口,供101和104的ASDU业务层及平台接入层按需调用,不在Connection中自动执行校时。`IEC60870_InformationObjects.c`复用CP56Time2a处理带时标信息对象和命令;Type 103校验及确认使用独立ClockSynchronization模块,实际设时由应用显式执行。**新生成业务时标使用本地标准时间日历并设置SU=0;Type 103确认回显原CP56及SU。** 公共编解码继续支持SU=0和1,按输入保留标志,不强制或自动清零。生成新时标前须保证日历本身已是本地标准时间,不能只清除夏令时标志而保留原小时值。
时间源提供UTC纪元秒数时,直接调用`IEC60870_Time_Seconds_To_Local(utc_seconds, ms, &local_time)`即可取得固定UTC+8的本地年月日、时分秒、毫秒和星期,调用前后都无需手动加八小时。接口内部统一处理跨日/月/年及范围检查;原始UTC秒数上限为4102415999,超过时返回0并保留输出。RTC已直接提供本地标准时间日历时,可直接构造CP56解析值并编码。
当前时间直接通过`IEC60870_Clock_Read(&value)`取得,设置实际时钟调用`IEC60870_Clock_Set(&local_time)`,均使用固定UTC+8日历。**时间源范围仅限Windows系统时钟和RTC;`IEC60870_CLOCK_USE_RTC=0`默认使用Clock.c内置的Windows实现,`=1`调用Clock.h声明的`IEC60870_Clock_RTC_Read/Set`接口。RTC具体芯片驱动暂不实现。** 编译选项须作用于`IEC60870_Clock.c`,不规划其他操作系统适配。公共入口无需时钟对象或绑定初始化;以后接RTC时,硬件仍按驱动要求初始化,已有本地标准时间日历可直接填入,无需转换成时间戳再还原。读取结果分别表示日历和`is_invalid`,读取成功不等于时间有效;失败不修改输出。RTC的BCD格式、时间基准、毫秒来源及掉电状态由具体驱动处理。接入示例见[统一时间源读写](../common/IEC60870_Time_设计架构.md#统一时间源读写),业务待接入项见[当前接入状态](../common/IEC60870_Time_设计架构.md#当前接入状态与待补齐项)。
时间戳转日期保留两种实现:Windows默认调用CRT的64位`_gmtime64_s`;编译`IEC60870_Time.c`时定义`IEC60870_TIME_USE_PLATFORM=0`即可使用原有整数算法,其他平台默认采用此配置。配置在编译时生效,业务接口、UTC含义及1970..2099年的范围一致。此选项只决定如何转换传入的时间戳,与是否接入RTC无关;应用使用RTC作为时间源时,仍需通过相应驱动读取时间,再按返回值的格式和时间基准调用转换或编码接口。详见[实现选择](../common/IEC60870_Time_设计架构.md#时间戳转日期的实现选择)。
业务接收按`From_CP56 → CP56_To_Local`调用:原始解码保留源日历及IV/SU,转换接口返回本地标准时间年月日、时分秒、毫秒和星期。双方使用本地标准时间基准时,源标准时区参数传`IEC60870_LOCAL_UTC_OFFSET_MINUTES`(480):SU=0保持原时间,SU=1减一小时并处理跨日/月/年,IV=1则转换失败。其他源时区须传入其标准偏移,输出仍为本地标准时间;调用方无需再加八小时或重复扣除夏令时。接收SU=1的兼容处理与本地发送SU=0的约定分别生效。
日历范围为1970..2099年,CP56Time2a采用2000..2099年的世纪约定;接口以1/0表示成功/失败,失败保留输出。通用纪元秒数接口保留UTC语义,不能将本地标准时间日历直接当UTC使用。RTC读写仍由平台实现,Link和Connection的`now_ms`继续使用单调计时,不受校时影响。当前提供CP16经过时间、CP24短时标、CP56及日历工具。CP24仅含分钟、分钟内毫秒及IV,不能单独恢复小时和日期,需由业务结合已有时间基准解释。完整范围、星期和保留位策略见[时间工具设计](../common/IEC60870_Time_设计架构.md)。
Connection内部持有独立 `IEC104_Receiver_t`,先用非零连接代次调用 `IEC104_Receiver_Init`,再调用 `Feed(receiver, generation, data, length, &consumed)`。Feed最多收齐一帧,TcpServer保留未消费的输入尾部;`FRAME_READY`后由Connection通过 `Peek_Frame`取得APDU和APCI并分派。S/U受理后释放,I帧在TcpServer成功复制为事件后经Connection释放。帧未释放时Feed消费0字节,不覆盖原文;`Reset`丢弃当前半帧或完整帧,但不改变连接代次或Link状态。内部接口和迁移说明见 [编解码与接收器设计](IEC104_设计架构.md)。
接收器校验APCI格式和ASDU最低长度。Connection将I/S帧交给同连接的SendManager处理序号及ACK,并按功能分派U帧;WAIT时保留接收帧。Slave内部通过TcpServer_NextEvent读取I_FRAME副本,无需释放底层帧。连接重建由TcpServer先结束旧I/O并Close,随即Detach队列回到离线状态;新连接以新代次Rebind队列并Init连接;旧代次事件返回ERROR。
I 帧本地发送结果通过 `IEC104_Link_Complete_I_Send()` 提交,`Send` 明确限定为本地发送,成功不表示对端已经确认。同一次发送结果只提交一次。具体契约见 [链路设计第3.4.4节](IEC104_Link_设计架构.md#344-complete_i_send-的本地发送完成提交)。
发送完成接口如下,表中省略统一前缀 `IEC104_Link_`。它们报告最终发送结果,包括失败;返回 OK 表示本地发送结果已成功提交,是否仍需等待对端确认由对应状态决定。
| 接口 | 对应的本地发送结果 |
|---|---|
| `Complete_I_Send` | I 帧 |
| `Complete_S_Ack_Send` | S 确认帧 |
| `Complete_Startdt_Act_Send` | 本端主动发送的 STARTDT_ACT |
| `Complete_Startdt_Con_Send` | 本端应答的 STARTDT_CON;完成不回写启停状态 |
| `Complete_Stopdt_Act_Send` | 本端主动发送的 STOPDT_ACT |
| `Complete_Stopdt_Con_Send` | 本端应答的 STOPDT_CON;完成不回写启停状态 |
| `Complete_Testfr_Act_Send` | 本端主动发送的 TESTFR_ACT |
`Prepare_*` 负责准备报文或推进准备流程,`Handle_*` 负责处理接收事件;二者名称保持原有职责划分。收到 TESTFR_CON 使用 `Handle_Testfr_Con()`,不能用本地发送完成替代对端确认。
主动 TESTFR 使用 `IDLE → ACT_SEND_PENDING → CON_PENDING → IDLE` 三态,与控制站主动发起的 STARTDT/STOPDT 一致:ACT 完整发送后先调用 Complete,再处理对应 CON。ACT 尚未 Complete 时处理到的合法 TESTFR_CON 返回 IGNORED,不缓存;只有 CON_PENDING 接受确认。被动 TESTFR_ACT 仍独立生成 CON,应答对端请求不结束本端探测。
完整状态校验集中在 `IEC104_Link.c`,主站、子站的状态组合规则由该文件内的 `static` 函数检查。角色实现通过 `IEC104_Link_Internal.h` 调用公共辅助函数,公共链路模块不再反向调用角色实现。构建时包含公共链路及基础编解码实现,再按实际使用的启停接口加入对应角色 `.c`;同时使用两端接口时加入两个角色实现。
I/S的启停准入和停止阶段立即确认规则集中在 `IEC104_Link.c` 的私有判断函数中;I/S处理与规划通过这些判断继续执行序号、窗口及确认检查。I帧接收、I帧预留、S帧接收和普通S确认分别判断,保留各自的方向与阶段差异。子站在ACT受理时提交启停状态,S确认与CON发送完成分别维护各自的完成事实。具体规则见 [I/S与启停的内部边界](IEC104_Link_设计架构.md#16-is与启停的内部边界)。
启停和TESTFR在对应入口中直接检查状态、编码并提交;I/S帧保留序号规划。内部确认提交复用公开入口的状态校验,STOPDT直接返回各等待原因,不再经过私有计划对象和结果转换。
## ASDU信息对象模块
信息对象、ASDU容器、公共头解析及参数类型统一使用 `IEC60870_` 前缀,供101/104共用。旧信息对象/ASDU文件和符号直接迁移,不提供兼容头、宏或类型别名;调用方须更新包含路径、函数/类型名称和构建源文件并重新编译。104的APCI、链路、TCP及发送队列仍使用 `IEC104_` 前缀。
公共层仅需 `IEC60870_Asdu.c`、`IEC60870_InformationObjects.c`、`IEC60870_Codec.c`、`IEC60870_Time.c`;头文件依赖也不包含任何104模块。101接入提供约定的字段宽度和容量,104接入提供标准2/2/3及249字节上限。两种接入共用同一套对象构造和解析接口。
业务包含`IEC104_Slave.h`,取得`Slave_NextEvent`交付的I帧后可直接使用ASDU视图并解析具体对象,或先构造ASDU再交给发送接口。ASDU编解码层不分配内存、不操作连接、不读取或设置系统时钟;共用连接接口可构造并提交确认/终止应答。站/分组总召可交给IEC104_Interrogation管理校验、分批、撤销、期限和重试,应用响应业务事件提交数据,自行维护IOA、分组及遍历状态;Type45–51/58–64由IEC104_Command管理,Type103由独立校时模块管理,权限策略及设备动作仍由应用接入。
| 层次 | 职责 |
|---|---|
| `IEC60870_Codec.c/h` | 101/104共用的参数校验、ASDU公共头解析及已知类型最低载荷长度检查 |
| `IEC104_Codec.c/h` | 104专用APCI检查、I/S/U帧编码和帧类型 |
| `IEC60870_Asdu.c/h` | ASDU创建/添加/解析/取对象,维护Type ID、VSQ/SQ、IOA布局和容量,构造原文应答 |
| `IEC60870_InformationObjects.c/h` | 每种类型独立结构、Create和Get_From_Buffer;通过对象函数表编码,按Type ID选择具体解析器 |
| `IEC60870_Time.c/h` | CP16经过时间、CP24短时标及CP56日历的编解码、范围和保留位校验 |
| 能力 | 当前范围 |
|---|---|
| 监视对象编解码 | 1–21、30–40,共32种;含步位置、比特串、成组单点、CP24及保护事件;支持SQ=0/1 |
| 命令对象编解码 | 45–51、58–64、100–107,共22种;SQ=0、一个对象、精确长度 |
| 初始化结束 | 70;COI保留原因与本地参数改变标志;SQ=0、一个对象 |
| 参数对象编解码 | 110–113;QPM/QPA按独立限定词处理;允许多个对象但SQ必须为0 |
| 文件对象编解码 | 120–127;含变长文件段125与归档查询127;126文件目录支持多个对象及SQ,其余只允许一个非SQ对象 |
| 应答构造 | COT=7激活确认、9停止激活确认、10激活终止和44–47错误应答;P/N显式传入,否定激活确认使用7和1 |
上述共67种类型都有独立结构、构造和解析接口。安全扩展、厂家私有类型不在当前范围内。本层提供无状态报文编解码;站总召状态机由IEC104_Interrogation提供,参数生效、文件传输、文件读写及归档检索仍由业务接入。类型编号可在公共对象头文件的`IEC60870_AsduType_t`中查到。
应用层参数`IEC60870_AppLayerParameters_t`定义在`IEC60870_Types.h`:
| 字段 | 合法范围及含义 |
|---|---|
| `sizeOfTypeId`、`sizeOfVSQ` | 固定为1 |
| `sizeOfCOT` | 1或2;1字节时省略OA,解析OA为0 |
| `originatorAddress` | 0..255,供业务作为默认OA;构造以header中的显式OA为准 |
| `sizeOfCA` | 1或2;1字节配置拒绝大于255的公共地址 |
| `sizeOfIOA` | 1、2或3;IOA及SQ连续地址上限随宽度变化 |
| `maxSizeOfASDU` | 至少容纳公共头、最多254字节;104传输不得超过249字节 |
标准104包含 `IEC104_Types.h` 后使用`IEC104_APP_LAYER_PARAMETERS_DEFAULT`,即`{1,1,2,0,2,3,249}`。所有ASDU创建/解析入口必须显式传入非空参数。容器和解析视图借用参数,使用期间参数必须有效且不变。通用ASDU可复用到采用其他宽度的101接入,但本工程尚未提供101串口链路;Receiver、APCI、TcpServer和发送队列继续使用104布局及249字节上限。
每类对象通过自己的`Create`写入调用方提供的结构体,并绑定类型及只读编码函数表。例如单点用`IEC60870_SinglePointInformation_t`,双命令用`IEC60870_DoubleCommand_t`。普通监视和控制的带CP24/CP56对象用`base`保存基础对象、`time`保存时标,并复用基础值处理;保护事件则显式保存事件、QDP、CP16经过时间和时标。对象可按值复制,不需要销毁;业务可修改值或IOA,重新编码时仍会校验,类型和函数表不得手工修改。
容器调用顺序为`IEC60870_Asdu_Create → IEC60870_Asdu_Add_InformationObject → IEC60870_Asdu_Get_Data`。Create固定参数、SQ和公共头,首个成功添加的对象确定Type ID;后续对象必须同类型。SQ=1要求IOA连续且不越过所选宽度上限,SQ=0逐对象携带地址。Add成功后已经复制对象字节,可以立即复用对象存储。
容量取实际缓冲区与`maxSizeOfASDU`的较小值。空间不足、对象达到127个或单对象类型已加入一个对象时返回`IEC60870_ASDU_FULL`;业务提交当前非空ASDU后另起一条重试。空容器放不下首对象时须调整容量;文件数据须按可用长度分成更小的段。类型不匹配、SQ地址不连续或字段非法返回ERROR。所有失败保留容器、原文和输出参数,首对象失败不会提前设置Type ID。结果必须显式比较`IEC60870_ASDU_OK`,不可按布尔值判断。
业务包含 `IEC60870_Asdu.h`;本例使用104默认配置,还需包含 `IEC104_Types.h`。以下代码放入业务函数,`data`在原缓冲区未修改期间有效:
```c
uint8_t buffer[IEC104_MAX_ASDU_LEN];
const IEC60870_AppLayerParameters_t parameters = IEC104_APP_LAYER_PARAMETERS_DEFAULT;
IEC60870_Asdu_t asdu;
IEC60870_AsduHeader_t header = {
.cause_of_transmission = IEC60870_ASDU_COT_SPONTANEOUS,
.common_address = 1
};
IEC60870_SinglePointInformation_t point;
const uint8_t *data = NULL;
uint16_t length = 0;
IEC60870_AsduResult_t result =
IEC60870_SinglePointInformation_Create(&point, 100, 1, 0);
if (result == IEC60870_ASDU_OK) {
result = IEC60870_Asdu_Create(
&asdu, &parameters, buffer, sizeof(buffer), &header, 0);
}
if (result == IEC60870_ASDU_OK) {
result = IEC60870_Asdu_Add_InformationObject(&asdu, &point.object);
}
if (result == IEC60870_ASDU_OK) {
result = IEC60870_Asdu_Get_Data(&asdu, &data, &length);
}
/* OK时可将data/length交给Enqueue_Event;提交返回WAIT时保留原文重试。 */
```
监视和命令统一使用`IEC60870_Asdu_Parse`校验整条报文,再通过`IEC60870_Asdu_Get_Element`取得独立类型的对象。例如处理单命令:
```c
/* parameters、data、length对应收到的ASDU;处理期间原文保持有效。 */
IEC60870_AsduInfo_t info;
IEC60870_AsduResult_t status =
IEC60870_Asdu_Parse(&parameters, data, length, &info);
if (status == IEC60870_ASDU_OK && info.type_id == IEC60870_ASDU_C_SC_NA_1) {
IEC60870_SingleCommand_t command;
status = IEC60870_Asdu_Get_Element(&info, 0, &command, sizeof(command));
if (status == IEC60870_ASDU_OK) {
/* info保存COT/CA/OA;command.object.ioa、state、is_select、
* qualifier供业务判断地址、限定词、选择/执行及权限。 */
}
}
```
Get_Element输出无需预先Create,必须按`info.type_id`选择匹配的具体结构体并传入其尺寸;尺寸检查不能替代C类型匹配。解析视图借用原文和参数,取出的具体对象按值保存。只处理单个载荷时,可直接调用对应类型的`Get_From_Buffer`,传入已解析IOA及不含IOA的精确载荷;读命令载荷为0字节,允许NULL。
归一化值使用原始`int16_t`(实际值为原值/32768),标度值使用`int16_t`,短浮点保留IEEE 754 binary32位模式。累计量用`IEC60870_BinaryCounterReading_t`保存32位计数、5位序号及CY/CA/IV。对象层校验品质保留位、SCO、DCO/RCO状态、VTI有符号范围、保护事件QDP和CP24/CP56;COT、CA、IOA及限定词的业务适用性由应用判断。
`Build_Response(&parameters, request, length, cause, is_negative, out, capacity, &out_length)`仍以`IEC60870_Asdu_`为前缀。它复制请求,只替换COT/P/N,保留TEST、存在时的OA、CA、VSQ及全部对象原文,支持原地和部分重叠。它仅作公共头及最低长度检查,可回显未知类型。生成后携带原请求generation调用Send_Response,并处理WAIT/ERROR/FATAL。
`IEC60870_Asdu_Create_From_Buffer(&asdu, &parameters, buffer, length)`直接借用可写原文,建立可修改的ASDU对象。它只做公共头及最低长度检查,未知类型也能建立对象,用于COT=44等错误应答;执行业务前仍须使用完整Parse和业务规则校验。借用对象不能Add,`Set_COT`、`Set_Negative`、`Set_CA`原地修改相应字段;Get_Data可以导出原文。Create创建的构造容器也可使用这三个设置接口,空构造容器仍不能导出。Set_CA按COT宽度定位、CA宽度写入,1字节仅允许0..255,2字节允许0..65535;非法容器或地址溢出返回ERROR且不修改原文,业务地址限制仍由业务层判断。ASDU结构新增来源标志,调用方须一起重新编译。
### 文件段和新增时间格式
`IEC60870_FileSegment_t`含255字节的数据数组。Create和Get_From_Buffer把有效数据复制进对象,调用结束后即可复用输入;对象无需销毁。它的线上载荷为`NOF(2)+NOS(1)+LOS(1)+数据(LOS)`,解析精确检查LOS与报文长度,不接受截短和多余尾字节。
先用`IEC60870_FileSegment_Get_Max_Data_Size(parameters)`计算当前配置允许的数据长度。默认104配置返回236;最短101头、1字节IOA、254字节上限时返回245。返回0表示只能容纳空段,-1表示配置非法或连空段都装不下。Create允许0..255字节,实际加入ASDU时仍检查总长;超过容量返回FULL,调用方负责分段。
`IEC60870_Time_To_CP16/From_CP16`处理0..65535毫秒的经过时间。`IEC60870_Time_To_CP24/From_CP24`处理分钟0..59、分钟内毫秒0..59999和IV;bit6是保留位,CP24没有SU、小时或日期。CP24编解码保留IV,不自动推断完整日历。所有接口均沿用成功1、失败0且保留输出的约定。
### 子站统一入口
业务包含`IEC104_Slave.h`,持有一个不透明的`IEC104_Slave_t *`。Slave内部创建、驱动和销毁TcpServer;配置、调度、状态和结果仍复用`IEC104_TcpTypes.h`中的值类型,业务头文件不引入TcpServer操作接口或内部布局。
生命周期为`IEC104_Slave_Create → Config_Default/Init → Prepare/Process或Poll → Close → NextEvent读空 → Destroy`。所有操作在同一事件循环线程串行执行。静态池容量沿用`IEC104_TCP_SERVER_CAPACITY`,与直接使用TcpServer共享底层名额,无动态分配。创建失败返回NULL;其他生命周期接口显式比较`IEC104_TCP_OK`。
`IEC104_Slave_NextEvent(slave, &event)`按原FIFO交付`IEC104_SlaveEvent_t`:`event.transport`是TCP事件副本;I_FRAME且`event.asdu_result == IEC60870_ASDU_OK`时,`event.asdu`已经借用事件自身的ASDU字节,`event.connection`已经绑定原服务实例及原连接代次。业务不再传入server/generation,也不再手工Create_From_Buffer。字段/地址/命令权限仍由业务校验,未知类型可以原文交付,视图创建成功不等于允许执行命令。
普通循环使用以下顺序;Application_Handle_Event为应用自己的同步处理函数,其中可以提交应答或复制事件到延迟任务:
```c
IEC104_TcpResult_t result = IEC104_Slave_Poll(slave, 10, NULL);
if (result == IEC104_TCP_OK) {
IEC104_SlaveEvent_t event;
IEC104_TcpEventResult_t next;
while ((next = IEC104_Slave_NextEvent(slave, &event)) == IEC104_TCP_EVENT_READY) {
Application_Handle_Event(slave, &event);
}
/* next为ERROR时处理内部读取错误;EMPTY表示本轮事件已读空。 */
}
```
外部事件循环使用Slave_Prepare/Process,继续保留原service_token和socket token;pending_event_count或need_process非零时先处理本地工作。主动上报通过`IEC104_Slave_Get_Application_Layer_Parameters`取得参数构造ASDU,再调用`IEC104_Slave_Enqueue_Event(slave, &asdu)`进入NORMAL。关闭单连接传原事件给`IEC104_Slave_Close_Connection(slave, &event, error)`,库内部使用该事件的原代次。
### 共用连接上的激活确认和激活终止
共用接口`IEC60870_MasterConnection_t`继续供101/104业务复用;当前104的接口由Slave_NextEvent自动安装,101串口后端仍未提供。下例放在业务已验证请求并决定肯定确认的分支,event为地址稳定的Slave事件:
```c
if (event.transport.type == IEC104_TCP_EVENT_I_FRAME &&
event.asdu_result == IEC60870_ASDU_OK) {
bool accepted = IEC60870_MasterConnection_Send_ACT_CON(
&event.connection, &event.asdu, false);
if (!accepted) {
IEC104_TcpSubmitResult_t submit =
IEC104_Slave_Get_Last_Submit_Result(&event.connection);
/* 按下表处理submit;WAIT需要保留事件及原文,不能忽略或盲目重发。 */
(void)submit;
}
}
```
| 共用操作 | 行为 |
|---|---|
| Send_ASDU | 提交给定ASDU对象的当前原文,也可提交业务另外构造的数据ASDU |
| Send_ACT_CON | 原地设置COT=7及指定P/N,然后提交 |
| Send_ACT_TERM | 原地设置COT=10、P/N=0,然后提交 |
| Get_Application_Layer_Parameters | 返回该连接的标准104只读参数,最大249字节 |
确认及终止先修改事件副本的字节再发送,失败也不回滚;TEST/OA/CA/VSQ和对象原文保留。需要原请求内容时,在修改前保存副本。
| 详细结果 | 布尔返回 | 业务处理 |
|---|---|---|
| OK | true | 已复制到发送缓冲或HIGH,可复用发送原文;不代表已写完或已ACK,不重复提交 |
| WAIT | false | 本次未接手;保留已修改的ASDU及原事件,待容量恢复后重试 |
| ERROR | false | 参数、布局、旧代次或调用阶段非法,不能按容量等待盲目重试 |
| FATAL | false | 发送路径故障,可能已写出字节;结合RECORD/DISCONNECTED处理 |
事件含有指向自身的ASDU视图和连接上下文,不能用赋值或memcpy搬到另一地址。延迟任务可让NextEvent直接写入自身稳定存储,或调用`IEC104_Slave_Copy_Event(&task.event, &event)`;复制时同时保存字节、重建自引用,并保留原服务实例和原连接代次。源事件随后可被下一次NextEvent覆盖,任务副本仍独立可用;复制不代表需要重发已经OK的报文。多个任务的事件存储由应用提供,库不额外引入待办任务池。
总召内部使用紧凑存储:只保留`Get_Event_Identity`取得的原身份及10字节请求ASDU,按需建立视图,通过`Slave_Send_Response / Slave_Try_Send_Response`应答,关闭原连接用`Slave_Close_Connection_By_Identity`。任务槽、事件队列和发送容量保持不变,内存测量见[固定存储](IEC104_Interrogation_设计架构.md#2-对象身份与固定存储)。其他需要完整事件的业务仍可用Copy_Event。
NextEvent返回EMPTY/ERROR时保持输出不变。Close、重连、重新Init或Destroy后复用服务槽,都不会把旧任务改绑到新连接;发送继续检查原身份。Destroy成功后旧服务句柄失效,但旧事件的字节仍由任务自身保存。所有延迟任务也在所属事件循环串行提交。
多帧总召的数据和终止应经同一事件的connection依次提交,逐帧处理WAIT;把数据放NORMAL而终止放HIGH会造成优先级越序。门面不实现点表、总召状态机、遥控选择/执行或系统校时。
[子站示例](../../examples/iec104_slave_example.c)通过BEGIN/NEED_DATA/FINISHED事件与显式应答接入独立的[总召模块](../../104/business/IEC104_Interrogation.h)推进业务;Slave和TcpServer继续负责服务、连接及传输。总召接入顺序见[完整调用流程](IEC104_完整调用流程.md#flow-interrogation)。
调用`IEC104_Interrogation_Process(gi)`自动推进当前就绪任务,无需传入预算。每个实例至多一个未终结总召,包含等待生成BEGIN的请求;忙时相同和不同QOI的新激活均否定,不保存后续总召。4个任务槽共用于这个总召和待发短应答。Handle_Event在原会话有效、Slave通知已读空且无更早REPLY时尝试立即提交短应答,未接手再保存。满槽时尝试否定确认;不能安全提交/保存则仅丢新请求并累计dropped_requests,保留当前任务及TCP。Process在空闲、等待应用/事件空位、发送WAIT或Slave通知未读空时返回。对象、状态、调度及接入约定见[总召模块设计](IEC104_Interrogation_设计架构.md)。
总召Get_Status的`need_process`用于决定是否立即续跑,不能从`waiting`取反推断。应答或读取业务事件后重新查询;发送WAIT后等待网络推进再重试。每次Slave_Poll/Process及Slave通知处理结束仍须推进总召,即使need_process为false。外部事件循环同时合并Slave_Prepare的调度条件;新增状态字段后调用方须重新编译。
BEGIN由调用者判断是否接受,遍历状态可延至首个NEED_DATA建立;NEED_DATA由调用者选择本批IOA并读取当前值和品质,协议库不登记或冻结点表。Submit_Batch立即尝试不排队提交,WAIT不保存数据、不推进游标,应用在网络推进后重新取当前值重试;OK才固定帧顺序并推进批次。点值更新及突发发布须与总召读值及提交串行化,持续突发可能推迟总召,详见[数据批次与发送路径](IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。
BEGIN携带实际CA、station_id和QOI=20..36;应用检查数据源并决定接受,首个NEED_DATA才需建立取数游标。多目标广播先完成全部BEGIN/ACT_CON,再逐站取数,最后全部ACT_TERM;每站状态区保证FINISHED无损交付。Submit_Batch只接受事件目标CA。新增/替代站下轮参加,尚未选入BEGIN便删除的站跳过;已经选中尤其已确认的原站必须完成或明确终结。各站独立计时,总期限默认关闭;FINISHING的终结事件读完前仍忙拒绝新激活。详见[总召设计](IEC104_Interrogation_设计架构.md#33-广播地址与本站地址)。
主站COT=8按原连接、目标CA/OA/TEST/QOI匹配总召,包括已确认但尚未取数或等终止的站。定向撤销匹配站,其他站继续;广播撤销停止整轮,对各仍活动的站回肯定COT=9。FINISHED(DEACTIVATED)通知应用清理,匹配站旧批次失效。已被发送层接手的帧仍按顺序发送,撤销不补发成功ACT_TERM。
业务接受和每批取数期限默认各15秒,执行整体期限默认0(禁用),各项均可单独配置;这些是业务默认策略,不是标准规定的总召时限。关闭整体期限允许慢链路累计传输超过原60秒上限,底层单帧发送、确认和链路探测期限仍有效。BEGIN生成时启动已启用的业务期限,此前等待业务事件空位没有独立超时,但已占用总召资格,应及时读取FINISHED。总期限显式设为非零后仍按固定整体耗时计算,不因新批次或发送重试而延长。START/CONFIRM超时及BEGIN阶段Fail_Request复用原槽发送否定ACT_CON,保留TCP及其他短应答;肯定ACT_CON被发送层接手后,取数超时、总超时和数据源失败仍关闭原连接。终结事件保留具体原因,BEGIN未取出则只移除工作事件。接受BEGIN前检查数据源及必要业务资源,仍可逐批取值并编码。等待前查询timer_active/timeout_remaining_ms并持续驱动Process。此次失败处理修复不新增接口或配置开关;先前已移除queue_timeout_ms及多总召排队状态/统计,保留negative_submitted和dropped_requests,使用旧字段的调用方须调整并重新编译。
原`IEC104_SlaveConnection_Init`和手动接入对象已移除,调用方改用Slave服务及其事件接口并重新编译。TCP底层接口仍供独立通信接入和维护测试使用,业务的默认入口统一为Slave。
本次接口直接替换为独立对象与统一ASDU入口,删除旧通用监视/命令联合体及分类解析、Builder接口,不提供兼容包装;调用方须按上述示例迁移并重新编译。构建须新增`IEC60870_InformationObjects.c`。
时标保留IV/SU;IV=1可解码但不代表可校时。业务新上报应提供本地标准时间且SU=0,应答回显保留请求时标。验证命令和覆盖范围见[测试说明](../test/IEC104测试文档.md)。
### 时钟同步业务
[ClockSynchronization](../../104/business/IEC104_ClockSynchronization.h)处理Type 103、COT=6、IOA=0,按共用站点表路由;COT=5读时和COT=8撤销回45。独立静态池默认与TCP服务容量相同,每实例4个任务槽,与总召/计数量分别创建、分别取消。Handle_Event校验保存,Process处理期限及FIFO应答,只有应用显式Execute_Next会调用本次传入的同步action,不保存函数指针。
默认源标准时区480分钟、执行期限15000ms,0禁用期限;IV/SU及时区由公共时间工具处理,64位毫秒计算只补偿受理后的站内滞留。action收到的local_time已经是固定UTC+8,base_ms为执行检查时刻。肯定/否定确认保留原CP56字节,不回读时钟或提供自定义确认时标。
调用过action的确认走Try,错误校验应答走普通HIGH;WAIT只重试确认,STOP取消未交接任务但不回滚时钟。广播动作一次,快照目标分别确认;满槽丢新并计数,不断链。同内容的新请求仍执行,可能再次回拨。持续NORMAL发布可能推迟Try确认,应用通过包含回调执行期的confirm_pending串行协调新的带时标上报。默认示例只设模拟时钟,`--clock system`才调用Windows时间源。完整契约见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md)和[调用流程](IEC104_完整调用流程.md#flow-clock-synchronization)。
### 遥控、遥调与带时标命令业务
[Command](../../104/business/IEC104_Command.h)处理Type45–51及58–64、COT=6激活与COT=8撤销;默认type_mask=0x3FFF,启用全部14种。51/64没有S/E位,只能直接执行,不能加入select_required_mask;其余12种默认要求先选择,默认选择掩码为0x1FBF。应用可裁剪type_mask,但须同步裁剪选择掩码。每个Slave只能绑定一个实例,同一实例从创建事务到读取FINISHED并交接完应答期间一直忙,所有站点的新命令互斥。未启用类型继续交给后续处理器或44兜底。
设定值由request.value交付应用;选择和执行比较原始值字节及屏蔽S/E后的QU/QL,不比较CP56。带时标激活默认按10秒窗口检查,应用通过Set_Time_Base提供UTC纪元毫秒及对应Net单调毫秒;过期默认静默丢弃,超前、无基准或无效时间回否定。max=0只关闭时差检查,仍拒绝IV及UTC换算失败。校时成功后应在动作回调外更新基准;STOP/断线保留基准,失去可信时间时Clear_Time_Base。
Handle_Event校验并生成SELECT/EXECUTE事件,Accept先消费事件再至多调用一次本次传入的动作;不保存回调。EXECUTE启动后先Report_Result记录设备结果,再逐帧Submit_Return_Info,全部OK后Complete。确认未交接时仍可报告结果以停止执行期限;返回信息WAIT时由应用保存ASDU重试。执行确认、返回信息与ACT_TERM走Try,模块内应答按全局FIFO提交。
已取走工作事件的事务恰好产生一次FINISHED;FINISHED只结束协议事务,应用自行维护尚未结束的物理输出和站点引用。退出时Cancel、读完FINISHED,再关闭Slave并分派余下通知,最后Destroy。完整规则见[遥控设计](IEC104_Command_设计架构.md)、[API第25节](IEC104_API接口汇总.md#file-25)和[遥控调用流程](IEC104_完整调用流程.md#flow-command)。
## 启停主流程
| 角色与过程 | 调用顺序 |
|---|---|
| 主站启动 | `Prepare_Startdt_Act` → 实际发送 → `Complete_Startdt_Act_Send` → 收CON → `Handle_Startdt_Con` |
| 主站停止 | `Request_Stopdt` → `Prepare_Stopdt_Request` → 依次发送S/ACT并提交完成 → 收CON → `Handle_Stopdt_Con` |
| 子站启动 | 收ACT → `Handle_Startdt_Act`(立即STARTED/IDLE)→ 发送CON → `Complete_Startdt_Con_Send` |
| 子站停止 | 收ACT → `Handle_Stopdt_Act`(立即STOPPED/STOP_DRAINING)→ `Prepare_Stopdt_Response` → 按需发送S、等待I帧确认 → 发送CON → `Complete_Stopdt_Con_Send` |
上表省略统一前缀 `IEC104_Link_`。只有具体接口返回值表明已生成报文时,才将本次输出交给发送层;生成不等于已发送。部分 Handle 只受理请求或提交对端确认,例如 `Handle_Stopdt_Act` 不生成应答,后续由 `Prepare_Stopdt_Response` 推进。S帧完成交给 `Complete_S_Ack_Send`。被控站不暂存相反方向ACT;停止等待期间成功受理START即恢复STARTED/IDLE,结束尚未生成STOPDT_CON的停止等待。
正常控制流程应串行执行启停,例如 `STOPDT_ACT → STOPDT_CON → STARTDT_ACT → STARTDT_CON`。主站的忙或重复请求结果不表示可以再次发送ACT。子站交错请求按实际受理顺序改变状态:START激活,STOP停用。已经生成的CON始终按FIFO发送,后到请求不撤回旧CON,旧CON完成也不覆盖后到ACT已经提交的状态。具体语义见 [链路设计第4.2节](IEC104_Link_设计架构.md#42-被控站受理启动)。
被控站成功受理STARTDT_ACT时立即进入STARTED/IDLE,并生成STARTDT_CON。首个CON尚未Complete时,后续每个成功受理的重复START也生成一帧CON;同状态重复请求不再次产生激活通知。所有CON按FIFO发送,Complete只确认对应发送结果,不重新提交启停状态。首个STARTDT_CON尚未完成也能受理STOP,立即停用并进入STOP_DRAINING。具体分支见 [链路设计第4.2节](IEC104_Link_设计架构.md#42-被控站受理启动)。
## 子站发送模块(已实现)
三个模块采用变长业务队列、发送前编号和独立确认记录组织,HIGH满时拒绝、NORMAL覆盖最旧原文,同时支持无malloc、部分写入续传、本地发送期限、严格启停/TESTFR、逐帧回执及可配置防饿死配额。
ASDU队列的状态以业务条目为单位:`unsubmitted_*` 表示队列尚未登记发送交接的待发条目。操作顺序是 **SendBuffer接手完整I帧 → Manager建立发送记录 → Mark_Submitted完成队列记账**。最后一步执行前,队列仍保留待发旧状态,不能据此判断SendBuffer是否已经接手。这里的“已提交”不表示整帧已写完或对端已ACK。
HIGH在完成交接记账时消费队列原文,SendBuffer中的完整I帧及Manager记录继续有效;NORMAL则保留原文等待ACK。`retained_unacked_count` 统计原文仍保留、已完成交接记账且待确认的ASDU条目,HIGH恒为0;`overwritten_unacked_count` 统计NORMAL原文已覆盖、仍待确认的条目。全部已编号未ACK的I帧数等于Manager的 `high_unacked_count` 加NORMAL保留及覆盖待确认数。`is_storage_empty` 只表示ASDU原文环为空,HIGH和NORMAL仍可能有对应I帧待确认;SendBuffer的 `is_idle` 表示当前无帧和预留。接口和行为变化见 [发送模块接口迁移](IEC104_发送模块_设计架构.md#migration),调用方须同步迁移、重新编译并初始化。
- **AsduQueue**:HIGH/NORMAL各用一个调用方提供的字节数组。两级使用不同条目头:HIGH仅存2字节长度;NORMAL存条目编号、状态及长度,头大小为 `sizeof(IEC104_AsduNormalEntryHeader_t)`,当前Windows 64位GCC下为16字节。条目恰好占本级头长加ASDU实际长度,头和原文均可跨尾部回绕,无额外回绕填充。两级都不保存user_tag,只有NORMAL分配条目编号。未提交条目通过待发游标直接定位。HIGH空间不足返回FULL,提交后立即释放原文;NORMAL保留原文到有效ACK,空间不足时覆盖最旧原文,包括已提交未ACK条目,成功返回OK且无逐条丢弃通知。条目超过目标桶总容量返回ERROR,须修正容量或组帧,不清空旧数据。
- **SendBuffer**:结构内只存一份255字节当前I帧,另用调用方提供的 `IEC104_SendBufferControlFrame_t[]` 保存S/U,每槽APDU仅6字节。当前帧连续写完;控制队列排在下一未编号业务之前。取消了APDU节点池、控制预留量和重复的协议序号边界。
- **SendManager**:Connection在准备实际写socket时调用 `Prepare_Next_Send`,无当前帧才重新选业务并分配N(S)。重复调用只返回当前帧剩余部分,最多一帧I尚未本地完成。实际写入后用 `Handle_Send_Progress`;解析后的I/S用 `Handle_I_S_Frame`。有效ACK更新所有对应发送记录;HIGH只核减Manager的未确认计数,NORMAL释放仍保留的原文或核减覆盖待确认数。确认记录由TcpServer复制为RECORD事件后经Connection移除,业务稍后处理事件副本。原文出队或覆盖不撤回当前帧、不释放k窗口,也不重置期限。
业务入口为Slave;其下的TCP及发送内部调用主线为:
```text
主动上报:TcpServer_Enqueue_Event → 服务ASDU队列(首次连接前、断线期间均可缓存)
连接应答:TcpServer_Send_Response → Connection_Submit_Response → Manager.Submit_Response(直接准备或缓存HIGH)
总召数据:TcpServer_Try_Send_Response → Connection_Try_Submit_Response → Manager.Try_Submit_Response(只能直接准备,不能排队则WAIT)
内部发送:Connection经Manager准备字节 → TcpServer经Net写socket → Connection回报Manager进度
内部接收:TcpServer经Net收字节 → Connection经Receiver成帧、由Manager统一提交链路事件
事件交付:TcpServer复制I帧/回执到事件队列并释放内部资源 → 业务通过NextEvent取副本
```
内部模块读取AsduQueue时,`AsduHeadInfo` 通过 `asdu_first/asdu_first_length` 和可选的 `asdu_second` 返回原文,`asdu_length` 是总长度;需要连续副本可用 `Copy_Asdu`。HIGH的 `entry_id.entry_serial` 恒为0,仅核对当前代次;从Peek到Mark/Discard之间不得移除、清空或替换队首,0编号不能当作可长期保存的条目身份。HIGH提交或清空后、NORMAL原文被覆盖或ACK释放后,对应借用立即失效。Manager已处理跨界原文,传输层取得的APDU仍连续。每级需同时保留N条、ASDU最长L字节时,HIGH配置 `N × (2 + L)`,NORMAL配置 `N × (IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN + L)`,无需另留回绕空隙;HIGH的N只计待发条目,NORMAL的N还包括原文仍保留的未ACK条目。Manager的构帧函数另使用249字节局部拼接数组,需计入栈预算。
仅NORMAL使用64位非零循环条目编号:`UINT64_MAX → 1`,回绕不改变连接代次,也不清空队列。先后顺序按编号环距离判断;NORMAL保留条目及覆盖待ACK区间不能跨越半个编号环,旧记录阻止窗口前进时NORMAL入队返回FULL,处理旧条目或ACK后可重试,HIGH不受该编号窗口阻塞。NORMAL条目释放后身份失效,调用方不得保存旧身份跨完整编号周期再次使用。NORMAL头内状态在Mark时改为已提交,Rebind时恢复未提交;接口说明见[发送模块设计](IEC104_发送模块_设计架构.md#aq-functions)。
`Prepare_Next_Send` 的成功值是 **OK**;`Submit_Response`以OK表示直接准备、QUEUED表示已缓存HIGH;控制帧入队成功仍为QUEUED,不能按布尔值判断。两条I帧路径共用组帧和发送记录,直接应答不经过ASDU队列,也不绕过旧HIGH、当前帧、窗口或公平配额。TcpServer的`Send_Response`最多尝试一次非阻塞写入;EAGAIN保留当前帧和期限,部分写入不能交错其他APDU。当前I帧已经准备后,即使暂时写出0字节,也要先完成,再发后来排队的控制帧。
`Try_Submit_Response`还要求NORMAL没有未提交条目;无发送机会时只返回WAIT,不降级为HIGH排队。Slave提供事件连接入口`Try_Send_ASDU`和纯值身份入口`Try_Send_Response`;总召使用后者,WAIT后重读数据。它们仍通过原SendBuffer、发送记录、k窗口和链路期限,不要求等待所有已提交帧的ACK。已有普通应答和主动上报接口的排队语义不变。
业务提交统一返回`IEC104_TcpSubmitResult_t`:`IEC104_TCP_SUBMIT_OK`表示已接手ASDU,不保证已写完或已ACK;WAIT表示未接手,业务保留重试;FATAL可能发生在已写出字节后,须处理收尾事件。`Enqueue_Event`只缓存服务事件,`Send_Response`尝试直接发送或缓存连接应答;调用后重新Prepare刷新调度条件。旧Enqueue_Normal/Enqueue_High入口与TCP_ENQUEUE结果名已移除,不保留兼容包装。
子站受理STARTDT_ACT即进入STARTED,STARTDT_CON作为发送任务按FIFO完成。STOPDT受理即停用并停止选取新业务,最多完成一帧此前已编号的当前I帧,等待已发帧确认后返回CON;未提交的NORMAL保留未编号,HIGH按下述规则清理。公共S、t2、TESTFR、t3及子站启停继续由Manager封装。接收接口的WAIT表示请求尚未受理,接入方须有界保留原帧并按接收顺序重试。
Manager初始化成功会清空HIGH待发原文并重置高优先配额;新STOP受理时清空HIGH。从停用状态受理START时,生成本轮首个STARTDT_CON并清空HIGH、重置高优先配额;已激活状态下的重复START仅追加CON,保留HIGH和配额。WAIT时请求尚未受理,不清理HIGH。以上清理均不影响NORMAL,也不撤销已提交的当前I帧及其发送记录。
初始化使用字节容量和控制槽数组。此次移除 `Enqueue` 的最后一个user_tag参数,以及条目查询、发送存储记录和记录查询中的user_tag字段;旧调用方须迁移、重新编译并初始化。HIGH发送记录的 `asdu_entry_serial` 恒为0,NORMAL继续保存条目编号,两级都用独立frame_id处理发送完成和帧回执。发送记录数组使用 `IEC104_IFrameSendRecordStorage_t`,连接代次由Manager统一保存;查询输出用 `IEC104_IFrameSendRecord_t`,返回完整 `frame_id`。当前Windows 64位GCC下,移除user_tag后每条存储记录由56字节降为48字节,查询快照由64字节降为56字节:
```c
static IEC104_IFrameSendRecordStorage_t records[12];
IEC104_AsduQueue_Init(&queue, high_bytes, sizeof high_bytes,
normal_bytes, sizeof normal_bytes, generation);
IEC104_SendBuffer_Init(&buffer, control_frames, control_count, generation);
IEC104_SendManager_Init(&manager, &queue, &buffer, &link,
records, 12, k, high_burst_limit);
```
以上省略返回值检查,完整示例、接口替换及容量单位见 [发送模块设计](IEC104_发送模块_设计架构.md#integration) 和 [接口迁移](IEC104_发送模块_设计架构.md#migration)。Link须先按CONTROLLED初始化。Manager允许绑定有未编号业务的队列,绑定成功会清空HIGH、保留NORMAL,不接管运行中的旧连接。
`Manager.Abort` 保留AsduQueue;结束旧I/O并核对记录中的ACK事实后,TcpServer调用 `AsduQueue.Detach` 解除绑定,清空HIGH并恢复NORMAL离线缓存;新连接再 `AsduQueue.Rebind` 到新代次并初始化。**HIGH固定用于当前连接的请求应答,重连清空;NORMAL固定用于允许跨连接恢复的事件,保留现存原文并恢复未提交。** 内部队列不提供逐条重连策略,使用 `Enqueue(queue, generation, priority, asdu, length)`;业务只调用TcpServer的Enqueue_Event/Send_Response。Rebind先完整检查两级条目,再将NORMAL头内已提交状态恢复为未提交并更新控制字段,不搬移ASDU原文,不使用整理数组。NORMAL头含编译器位域及对齐,必须按对应头长宏配置容量,不能沿用旧18字节头的缓冲内容或跨ABI解释。使用约定见 [队列生命周期](IEC104_发送模块_设计架构.md#aq-reconnect-policy)。
Rebind清除旧覆盖待确认计数,所有旧身份和借用失效;已覆盖原文不能恢复。它本身不发送,新连接仍须完成STARTDT;同一TCP上的STOPDT/STARTDT恢复不调用Rebind。未ACK不证明对端未收到,NORMAL只用于业务允许向同一接收方或冗余域恢复的事件;旧请求应答应放HIGH。关闭整个服务时由TcpServer调用Queue.Abort清空缓存,不提供掉电持久化。未编号条目被覆盖或重连丢弃时没有帧回执,旧帧回执须在Manager.Abort前读取;尚不提供冗余组切换或应用业务去重。
Connection通过`SendManager_Get_Deadlines`一次取得当前队首身份、本地发送及I帧t1剩余时间、控制槽容量;对应活动标志为1且剩余时间为0表示到期。原始queued_at_ms仍保存在SendBuffer,written_at_ms仍保存在Manager记录,部分ACK后选下一条的原始时刻。Connection的`Collect_Deadlines`汇合TESTFR、STOP、t2及t3,供超时检查、发送准入、控制推进和等待查询共用;不创建系统定时器,也不缓存第二份计时起点。原有底层单项超时接口保留。TcpServer在Process中执行TCP读写并驱动Connection,Poll复用该推进核心。
发送准备和写入回报仅因本地发送及STOP期限直接报告超时;ACK/TESTFR的t1故障交给接收阶段判定。旧确认t1已到时,统一发送准备入口暂停为HIGH/NORMAL创建、编号新I帧;已有帧及控制发送仍可推进,避免控制槽解阻后继续新增业务帧。有效确认解除所有已到期等待后自动恢复准入。Send_Response和常规发送仍按send后的真实时刻登记完成,发送期间跨过确认期限时安排立即Process,先有界处理已到达的确认,再按原始t1检查。Prepare对到期协议期限返回零等待;不增加恢复期限或固定宽限时间,也不改变真实容量背压下的超时策略。
## 子站连接协调层(已实现)
TcpServer内部的`IEC104_Connection_t`内含一个Receiver、一个Link、一个SendBuffer和一个SendManager,借用TcpServer提供的ASDU队列、控制槽数组及发送记录数组。初始化后保持对象地址不变,同一连接的所有调用串行执行。内部接口、时间规则及重连顺序见[连接层设计](IEC104_Connection_设计架构.md)。
Connection_Init先准备Receiver及配置,再由`SendManager_Init_Connection`校验并向最终地址提交Link、SendBuffer和绑定关系,最后提交其余连接字段;失败保留原Connection和队列。
控制帧是否可准备由Link的`Can_Prepare_*`只读接口判断,并与实际构帧共用规则。Connection对Link保留只读访问,运行期修改统一经SendManager,TESTFR_CON也经`SendManager_Handle_Testfr_Con`提交;只有返回OK才撤销本端探测t1,IGNORED不取消计时。Connection不直接查询SendBuffer,也不操作Manager的内部指针。Connection及SendManager的完整结构体供内部静态嵌入;TcpServer公开头文件隐藏这些布局,业务只使用TCP句柄、公开值类型及事件接口。
以下步骤均在TcpServer及Connection内部执行:
1. `Connection_Init`以CONTROLLED角色初始化STOPPED链路。成功受理STARTDT_ACT即进入STARTED/IDLE并开放I帧准入,发送队列保证已有CON先于随后准备的本端I帧写出。
2. 收到TCP字节后调用`Connection_Feed`,按`consumed_length`推进输入。一次最多处理一帧;未消费尾部由TcpServer保留。半帧留在Receiver中;S/U受理后自动释放;I帧返回FRAME_READY后由TcpServer经Peek复制为事件,再按`receive_serial`显式Release。
3. WAIT表示当前接收请求尚未受理,Connection保留原文并在后续驱动中重试。成功受理的START/STOP按控制帧路径自动释放原帧;重复驱动不会再次提交同一I帧的序号或ACK。
4. `Connection_Prepare_Next_Send == SEND_READY`时取剩余字节实际写入,再用`Handle_Send_Progress`回报增量;0字节和部分写入保留当前帧及原期限。整帧完成自动提交对应Link完成和Manager记录,不能另调底层Complete。本地已知发送完成必须先回报,再Feed覆盖该帧的ACK。
5. 定期调用`Connection_Tick`推进本地发送期限、I帧/TESTFR的t1、w/t2确认、t3探测和STOP总期限。停止过程中的重复STOP和WAIT不延长原期限;START成功受理取消旧STOP总计时。TESTFR等待CON的t1不因其他链路活动续期。t2保留连续未确认接收区间的最早起点,前缀被确认时不续期。
6. FATAL后连接冻结,TcpServer停止真实I/O,把剩余记录和断开通知复制为事件,再调用Connection_Close。Connection_Close保留内部ASDU队列;TcpServer随后Queue.Detach清空HIGH并保留NORMAL,离线期间继续接受NORMAL;重连时Queue.Rebind到新代次,再Connection.Init。仅Close服务清空全部ASDU。所有旧代次事件返回ERROR,不修改新连接。业务只处理TcpServer交付的事件并执行ASDU业务逻辑。
旧 `IEC104_Rx_Feed_Byte`、`IEC104_Rx_Reset`、`IEC104_Legacy_Link_Reset`及相关类型保持原样;旧入口继续使用全局单路状态、预设STARTED,并在成帧后检查I/S序号,只返回接收结果。Connection使用新Receiver并共享自身Link与SendManager,不调用旧全局接收入口。同一条连接不能同时交给这两条接收路径。
因接口过期而无法编译的旧回归测试及专用辅助文件已删除。当前有`iec104_tcp_event_ring_test.c`的13个字节环场景、`iec104_tcp_scheduler_test.c`的25个调度场景,以及`iec104_net_pending_input_test.c`的Windows回环检查;构建和运行见[测试说明](../test/IEC104测试文档.md#2-编译与运行)。
2026-10-20调度修复:预算让出时固定已到达输入范围并立即续跑,处理完后再判定ACK/TESTFR超时;后续新输入不能扩大旧批次。发送使用独立预算,持续接收不再跳过发送阶段。业务API及配置宏不变,内部Connection和Net新增续收/输入快照支持,自定义Net后端需实现`IEC104_Net_Pending_Input`。真正容量背压仍遵守原有严格期限;回执与启停通知的顺序继续按B方案。
以下为历史变更与验证记录,不代表当前保留的测试范围:
2026-09-23:NORMAL空间不足时改为覆盖最旧原文(含已提交未ACK条目),HIGH继续满时拒绝;当前APDU和发送记录独立保留,ACK兼容原文已覆盖。条目支持跨尾部存放,无填充碎片。仅更新代码和设计文档,测试文件保持原样。
2026-09-23:三发送模块改为变长ASDU字节环、单当前I帧和紧凑控制队列,发送前编号,有效ACK释放原文,业务回执独立消费,支持显式Rebind。保留ACK不得越过本地完成位置的修复。原有全部119个用例通过;本次补充校验从标准输入编译执行,未修改或新增测试源码、测试配置和测试文档。
2026-09-18:LINK测试改为报文回放用例,当时默认LINK套件68个、全部套件119个场景通过。该套件已随过期测试清理移除,历史记录保存在`.build`中。
2026-09-16:TESTFR改为三态,取消提前CON的缓存;当时旧用例因状态枚举变化而无法编译,未计为该次变更的验证结果。
2026-09-14:完善STARTDT期间的收发方向门控,当时新增LINK 36至40。算法契约见[链路设计](IEC104_Link_设计架构.md)。
@@ -0,0 +1,188 @@
# 104子站示例
[iec104_slave_example.c](../../examples/iec104_slave_example.c) 包含模拟点表、业务事件处理、其他命令兜底、事件循环及main。总召状态机使用独立库模块[IEC104_Interrogation.c/h](../../104/business/IEC104_Interrogation.h),不在示例内重复实现。
当前实现站总召(`C_IC_NA_1`,QOI=20),依次返回8个单点遥信、8个双点遥信和8个标度化遥测值;分组1(QOI=21)返回单点及双点遥信,分组2(QOI=22)只返回遥测。库支持全部16组,示例未配置的其余组由应用拒绝。通信通过`IEC104_Slave`,总召通过`IEC104_Interrogation`接入,所有任务使用固定存储、在主循环线程串行推进。示例另含计数量召唤、Type 103校时、45/46遥控、49标度化设定值及58/59/62带时标命令;默认时钟为模拟值,命令只更新内存点值,实际设备数据源和物理输出仍由应用接入。
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。
主循环向总召、计数量、校时、遥控实例广播生命周期通知,读空Slave通知后优先推进遥控,再交替推进召唤业务并显式执行校时;其他类型在四个模块均未接手后兜底。原Example_Init仍可用于仅总召测试,命令行入口分别调用计数量、校时及Example_Init_Command补齐实例,两个召唤实例各需独立广播状态区,校时使用共用站点快照。协议及应用动作限制见[计数量设计](../104/IEC104_CounterInterrogation_设计架构.md)、[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)和[遥控设计](../104/IEC104_Command_设计架构.md)。
在项目根目录的 PowerShell 中执行,需要 MinGW GCC:
示例通过 `IEC104_Interrogation.h`、`IEC104_CounterInterrogation.h`、`IEC104_ClockSynchronization.h` 和 `IEC104_Command.h` 引入业务声明;构建脚本添加 `104/`、`104/business/` 和 `common/` 头文件搜索路径,编辑器也应配置这三个目录。构建可执行程序仍使用下面的脚本,它会链接所需库源文件、Clock适配及Windows网络库;只编译当前C文件会缺少库函数实现。编辑器的C语言标准应与构建保持一致,使用C11,并选择实际的MinGW GCC编译器。
```powershell
./scripts/build.ps1
./.build/slave-example/iec104_slave_example.exe
```
默认监听 `127.0.0.1:2404`,公共地址 CA=1;按 Ctrl+C 退出并关闭服务。也可以指定监听地址、端口和公共地址:
```powershell
./.build/slave-example/iec104_slave_example.exe --bind 0.0.0.0 --port 2404 --ca 1
```
| 参数 | 含义 |
|---|---|
| `--bind IPv4` | 数字IPv4监听地址,默认127.0.0.1 |
| `--port 0..65535` | 默认2404;0由系统分配,启动时输出实际端口 |
| `--ca 1..65534` | 本站公共地址,默认1;总召请求也接受65535,广播请求的应答使用本站地址 |
| `--window 1..32767` | 发送窗口k,默认12;设为1可观察逐帧ACK后续发 |
| `--clock simulated\|system` | 默认simulated,只更新示例模拟时钟;system显式调用Windows系统设时接口 |
| `--run-ms N` | 运行约N毫秒后正常关闭;默认0表示持续运行 |
| `--help` | 打印用法后退出 |
启动输出 `LISTEN 地址:端口 CA=站地址`。退出保留原有STOP统计字段:`submitted`累计总召与兜底已接手ASDU,`gi_submitted`为已提交完毕的总召数,二者均不表示主站已经ACK。`waits`只统计总召发送WAIT;兜底HIGH满时丢新,计入`overloads`和`dropped_requests`,不记录待重试次数。兜底ERROR/FATAL计入`cancelled`,其中FATAL可能已写出字节,该计数不表示确定未发送。拒绝、主站撤销及业务超时统计继续保留,计数量另见`counter_submitted`和`counter_completed`。末尾校时字段`clock_executed`为实际action调用数、`clock_synchronized`为action返回true次数、`clock_submitted`为已交接校时ASDU数,包含错误及否定应答,广播按各提交目标分别计数;不混入原字段。
命令统计追加`command_selected`、`command_executed`、`command_completed`、`command_submitted`和`command_returns`,分别表示已接受的选择、已启动的执行、成功完成事务、已交接应答和返回信息帧数;`command_time_expired`统计过期激活,`command_time_rejected`统计无效时间、无基准、推算失败或超前造成的时间否定。静默过期不会增加选择/执行次数;保留原有统计字段的含义。
### Type 103校时
主站发送Type=103、SQ=0、对象数=1、COT=6、P/N=0、TEST=0、CA匹配或65535、IOA=0及有效CP56。默认按主站UTC+8标准时间解释,SU由时间工具处理;模块补偿受理至执行检查的等待。应用显式Execute_Next后回COT=7,原CP56字节原样回显。TEST否定7且不执行;COT=5读时否定45,不返回本站当前时间。
默认`--clock simulated`把请求的utc_ms/base_ms保存为模拟时钟基准,不改变系统时间。`--clock system`通过`IEC60870_Clock_Set(&request->local_time)`设置Windows时间;local_time已是固定UTC+8且已补偿,示例不再手工换时区或加等待。Clock内部转换为UTC后调用系统接口,权限不足时动作报告失败并回否定ACT_CON。真实TCP自动回归只运行模拟模式;system接线另用mock时钟单测,不设置宿主系统时间。
两种模式在成功校时动作中都记录utc_ms/base_ms及待更新标志;`Example_Drain_Clock`在Execute_Next返回后、回调之外把基准交给Command。system模式还在启动及每60秒、请求分派之前读取Clock,读成功且IV=0时按固定UTC+8转换(Clock读值不叠加夏令时),关联同一时刻的Net单调毫秒;读取/换算/设置基准失败或IV=1时清除Command基准。仅总召测试未创建Command,相关接线先检查空句柄。
确认发送WAIT时只重试原确认,不再次设时;STOP取消未交接确认而不撤销已完成的动作。同内容的新请求会再执行,旧时标重发可能回拨。真实数据发布方应在confirm_pending期间暂停新的带时标上报并继续驱动网络,避免持续新增NORMAL推迟确认。完整范围及期限见[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)。
### 遥控、设定值与带时标命令
`Example_Init_Command`在Config_Default后用赋值把type_mask明确裁剪为已接入的45、46、49、58、59、62,select_required_mask等于此type_mask,六种类型均须先选择。库默认配置启用全部14种命令(0x3FFF),默认选择掩码排除51/64(0x1FBF);示例不能用`|=`裁剪全类型默认值。决定期限15秒、选择保持30秒、执行期限60秒,成功发送ACT_TERM,失败默认不发。整个实例只允许一个事务,所有站点的新命令互斥;TEST请求否定,CA=65535回UNKNOWN_CA,其他命令IOA由应用Reject(UNKNOWN_IOA)。
| 命令 | 受控点及返回信息 | 初值与总召 |
|---|---|---|
| Type45/58,IOA6001..6008 | Type1,IOA1001..1008,COT=11;状态0分/1合 | 初值见下表,执行后总召读取更新值 |
| Type46/59,IOA6101..6108 | Type3,IOA1101..1108,COT=11;状态1分/2合 | 初值全部为1;执行后站总召及遥信分组读取更新值 |
| Type49/62,IOA6301..6308 | Type11,IOA2001..2008,COT=11;写入标度化值 | 初值见下表,执行后站总召及QOI=22读取更新值 |
表中IOA均为十进制。单点命令6001..6008分别对应遥信1001..1008;双点命令6101..6108分别对应返回位置1101..1108。例如对Type46、IOA6103完成状态2的选择与执行后,后续QOI=20或21读取的Type3、IOA1103为2;FINISHED清理不会把它恢复为初值1。
选择S/E=1、执行S/E=0,均使用COT=6;执行的类型、IOA、OA、值及QU/QL须与选择一致,带时标命令的选择和执行可使用不同时标。主站收到选择的肯定ACT_CON后再执行;执行依次返回ACT_CON、返回信息、ACT_TERM。撤销使用COT=8,同类型/IOA/OA匹配,不要求值相同。示例未启用47、48、50、51、60、61、63、64,仍由兜底回44;这些类型由模块专项测试覆盖。
49/62允许的示例标度值为−10000..10000(包含端点),选择和执行都检查限值,越限调用Reject(NEGATIVE),动作内另作守卫。这个范围属于模拟点表策略,不是协议或Command模块的统一限值。比如对6301设为−1234,Type11返回信息及后续QOI=20/22的2001均为−1234;总召对象数与帧序不变。
58/59/62的CP56按主站UTC+8及SU解释,默认时差上限10秒,IV=1拒绝,时差恰好10秒也拒绝或丢弃。simulated模式启用时尚无可信基准,须先成功校时;system模式可由启动读时建立基准。过期激活默认静默丢弃,不占事务槽或调用动作;无基准、超前及换算失败回否定7。只在受理时检查时效,默认决定期限内继续等待可能使启动动作时的命令年龄接近25秒。撤销仍检查CP56格式,但不判时差。
`Example_Drain_Command`先读取SELECT/EXECUTE/FINISHED;首次保存事务context前Acquire应用站点引用。点表、动作和返回信息按基础类型分派;`Example_Command_Action`模拟返校或修改`ExampleApp.signal_values/double_values/scaled_values`并返回STARTED,不在动作中上报。Type11单对象返回需要12字节缓冲。Accept的OK只表示事件已消费,示例通过Get_Transaction确认EXECUTING后,依次Report_Result(SUCCEEDED)、Example_Build_Command_Return、Submit_Return_Info和Complete;设备结果可以早于确认交接。
每一步WAIT都保留原事件和当前步骤;返回信息使用独立ASDU缓冲,WAIT时保留同一帧,OK后才推进,避免重复提交。STALE停止该事务重试,继续读取FINISHED。FINISHED清除模拟返校/输出标志并Release应用引用,不恢复已修改的点值;真实输出若仍未停止,应用必须继续保持设备忙状态和引用。Example_Wait_Time同时计入遥控期限、模块推进和应用挂起重试,发送WAIT时允许等待网络而不空转。
## 主站操作和模拟点表
主站先建立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 |
| 1101..1108 | 双点信息 `M_DP_NA_1`,Type=3 | 1、1、1、1、1、1、1、1 |
| 2001..2008 | 标度化测量值 `M_ME_NB_1`,Type=11 | 2300、2310、2290、5000、-125、0、1234、42 |
所有品质为0。遥测值为原始有符号16位标度值,本示例没有定义物理单位和倍率。
一次成功站总召依次提交8个ASDU:
1. Type=100,COT=7,肯定激活确认。
2. 两帧Type=1、COT=20的单点数据,每帧4个对象。
3. 两帧Type=3、COT=20的双点数据,每帧4个对象。
4. 两帧Type=11、COT=20的标度化遥测,每帧4个对象。
5. Type=100,COT=10,激活终止。
分组1(QOI=21)共提交6个ASDU:ACT_CON、两帧Type=1单点数据、两帧Type=3双点数据、ACT_TERM,数据COT均为21,每帧4个对象。分组2(QOI=22)仍共提交4个ASDU:ACT_CON、两帧Type=11/COT=22遥测、ACT_TERM,每帧4个对象。确认和终止保留请求QOI。BEGIN校验分组,首个NEED_DATA按该组初始化应用游标,不把模拟点表写进库。
全部应答使用原请求的连接身份。总召确认、终止及短应答走普通HIGH路径,总召数据由Submit_Batch尝试不排队提交;校时调用过action的确认也使用Try。遥控执行动作产生副作用后的确认、返回信息和ACT_TERM均走Try,选择确认及校验否定走普通HIGH。未提交HIGH/NORMAL、当前帧或窗口背压时Try返回WAIT;遥控的模块FIFO还会保留更早应答优先。总召数据和遥控返回信息不使用Enqueue_Event,只有本帧接手后才推进应用步骤。
主站撤销使用相同的实际站CA/OA/TEST/QOI,COT改为8、P/N=0。激活和撤销均接受本站地址或65535,匹配时65535映射为本站地址,允许广播激活配本站撤销及本站激活配广播撤销。尚有匹配总召时模块停止任务、回复肯定COT=9,并通过FINISHED(DEACTIVATED)通知应用;不匹配或已完成则否定COT=9。已提交发送层的帧可能仍在确认前发出,撤销后不再提交旧批次和ACT_TERM。示例点数很少,总召可能在主站发撤销前已全部提交;观察中途撤销和各阶段拥塞可使用下面的可控网络测试。
广播总召的BEGIN/NEED_DATA/FINISHED携带本站CA,示例直接用该字段构造数据。确认、数据、终止、否定及错误应答的CA都为本站地址,OA/TEST/QOI保留原请求值;原始Slave事件不被修改。该支持属于Type100总召处理器,其他类型仍按各自业务规则分派。
## 请求校验及任务处理
| 情况 | 本示例的处理 |
|---|---|
| 不支持的Type ID | 同步调用Reject_Request回显原文,COT=44、P/N=1;HIGH满时丢新并计数,不因连续请求断链 |
| 总召COT不是6或8 | COT=45、P/N=1 |
| 总召CA既不是本站地址也不是65535 | COT=46、P/N=1,保留原错误CA |
| 总召IOA不是0 | COT=47、P/N=1 |
| 激活QOI超出20..36、请求P/N非法或当前已有未终结总召 | 否定ACT_CON,COT=7、P/N=1 |
| 空闲时收到合法激活请求 | 有空槽则建立唯一总召;BEGIN后应用接受才发肯定确认,忙时不同QOI也拒绝 |
| 激活QOI为23..36 | 合法请求交给应用,示例因未配置点表而Reply_Begin(false),回否定ACT_CON |
| 撤销不匹配、QOI超范围或P/N非法 | 否定DEACT_CON,COT=9、P/N=1 |
| 匹配的主站撤销 | 取消对应总召,肯定DEACT_CON,COT=9、P/N=0 |
| 总召唯一对象完整,但SQ=1或有多余尾字节 | 回否定7/9,COT/CA/IOA错误优先45/46/47;不执行激活或撤销 |
| 总召零对象/多对象等无法可靠应答的布局错误 | 计入rejected并丢弃;保留连接和已有总召 |
| 需要新增总召且4个任务槽已占满 | 尝试否定ACT_CON;无法安全提交/保存则丢新请求并计数,保留连接与原任务;匹配撤销复用原槽 |
| 接受/取数/整体业务期限到点 | 肯定ACT_CON提交前回否定确认并保留连接,提交后关闭原连接;已取出BEGIN才交付对应TIMEOUT终结事件 |
Example_Handle_Event先过滤asdu_result失败的I帧视图,避免进入业务分派或未知类型兜底;正常线上基础头/载荷截短已由Receiver和连接层处理。当前I帧依次交给总召、计数量、校时及Command;都未接手时,在读取下一事件前同步调用`IEC104_Slave_Reject_Request`回UNKNOWN_TYPE。该接口借用当前事件生成原文错误应答,复用库的普通HIGH路径;示例不保存other_request,也没有other_pending、other_waiting或other_waits。未启用类型的连续及相同内容有效新请求逐条尝试应答,允许穿插正在进行的总召。103由校时接手,45/46/49/58/59/62由Command接手,102、47及106仍回44;模块之间不承诺跨业务应答按接收顺序排列。
OK表示已接手;WAIT只因HIGH容量不足,示例丢弃本条并增加overloads/dropped_requests,保持连接、已有HIGH内容及当前业务。ERROR按失效请求处理并继续读通知;FATAL可能已写出字节,不得重试,继续排空收尾通知。接口不修改输入事件或其最近提交结果,不能通过Get_Last_Submit_Result读取本次返回值。未读启停通知会阻止本次应答;状态计数不能追溯已消费的STOP/START,禁止先保存事件再读完后续通知后调用。需要异步业务应答时另用Copy_Event、Check_Request并自行处理STOP取消,完整契约见[同步拒绝当前请求](../104/IEC104_API接口汇总.md#reject-request)。
校验从类型开始;已知总召先检查是否有唯一完整对象,再按COT、CA、IOA、布局/QOI/P/N及事务状态决定。畸形单对象的否定副本规范为10字节、VSQ=1,保留OA/TEST/IOA/QOI,不修改原输入。广播应答使用实际站CA。错误请求不取消原总召或刷新期限;否定应答沿现有队列重试,满槽只丢新请求。这里只实现上述命令集合,不代表所有类型的业务规则。
`Example_Drain_Business`主动读取总召业务事件:BEGIN按QOI选点表,Reply_Begin接受或拒绝请求;NEED_DATA调用普通辅助函数Example_Build_Batch构造数据,再Submit_Batch,数据耗尽则Complete_Data;FINISHED按实例/请求编号清理对应资源,撤销和超时也走该分支。没有向库注册业务函数指针。数据源暂未就绪时可保存事件,后续就绪再应答;发送WAIT也保留事件,下一次调用重新组包,模块不缓存本批原文或重复生成NEED_DATA。
本例BEGIN只判断接受,首个NEED_DATA初始化游标并读取这一批;接真实数据源时,在Example_Build_Batch的逐点组包处读取当前值和品质。IOA、类型、分组成员及遍历策略均由调用者决定,协议库不登记或冻结完整IOA列表。单点和遥测常量表保存地址、初值及品质,双点初值均为1;实际值分别存放在ExampleApp.signal_values、double_values和scaled_values中,站总召及对应分组逐批读取这些可写数组的当前值,包括遥控及设定值执行后的更新;这些地址不是库规定的范围。逐批采样不保证整轮同一时刻,也不自动解决与突发上报的新旧排序;完整边界见[总召模块设计](../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成功代替资源清理。
遥控同样执行`Command_Cancel → 读完FINISHED → Slave_Close → 读空并分派通知 → Command_Destroy`。示例先取消全部业务并清理应用引用,再关闭Slave;关闭后的生命周期通知仍交付全部模块。遥控FINISHED可以在离线时读取,事务槽及独立应答清空后才允许Destroy,最后移除站点并销毁Slave。
## 验证
```powershell
./scripts/build.ps1 -Test
```
`-Test` 需要预先安装 Node.js。脚本运行Slave/站点测试、总召/计数量/兜底/校时可控网络测试、规则单测、遥控容量矩阵和真实本机TCP测试;实跑数量及记录见[测试说明](../test/IEC104测试文档.md):
- `iec104_slave_test.c`:站点注册/移除、延迟回收、重复CA、13站共享服务、CA/context隔离、注册编号防替换、快照增删及嵌套集合锁、关闭重启保留,以及原有应答、事件身份和背压回归。Reject_Request另覆盖原文回显、非法参数、连续请求、窗口背压、HIGH满、启停门禁、旧会话及FATAL。
- `iec104_slave_example_test.c`:完整报文、部分写入、各发送阶段HIGH满与WAIT、请求校验、STOP/重连、非法请求、传输通知预算及API生命周期;业务事件覆盖延迟应答、批次校验、混合类型、容量边界、拒绝/失败/取消、终结预留和实例复用。总召覆盖全部16组、各阶段撤销、三项期限、迟到应答及通知优先。调度覆盖短应答立即提交及顺序保护、非队首槽复用、满槽否定或计数丢弃且当前任务有效、各阶段相同/不同QOI均忙拒绝、结束后重发成功、撤销对象匹配、停止清理、BEGIN前等事件空位及BEGIN后完整执行期限,并验证忙状态不拦截Type101总电度请求。测试设置HIGH=251字节、业务事件FIFO=2槽,使用真实容量背压限制发送。
- `iec104_command_test.c`:独立可控Net/单调时钟测试,TCP服务容量1/2和独立应答槽容量1/4交叉构建,HIGH=251字节;覆盖14种命令布局、值和CP56校验、选择执行、匹配、时效及时间基准、变长FIFO、返回信息、动作结果、期限和FINISHED/引用生命周期。
- `slave_example_loopback_test.cjs`:启动实际可执行程序,使用Windows网络后端和127.0.0.1随机端口,以独立主站字节验证分片请求、k=1时逐帧ACK、站总召、忙拒绝及结束后重发、撤销、错误应答、106与102同批分别回44、STOP/START、重连和正常退出。校时核对正常ACT_CON、TEST否定7、读时COT=5否定45、广播实际CA,以及103与102同批时先102回44、再103回7。命令核对45/46/49/58/59/62完整流程,返回信息在确认和终止之间,越限/TEST/地址拒绝、撤销、47兜底及操作后的总召新值。正常带时标命令由最近成功校时和测试单调经过时间推算时标;过期58与106同批,结合后续全部计数量帧、动作/选择计数和time_expired验证没有58应答。服务约12秒后自行关闭,使用默认模拟时钟。
原114组基线由98组总召、12组计数量及4组兜底回归组成。兜底改用未支持的106/102,覆盖同批及反序、相同内容的有效新请求、k=1与写阻塞排队、HIGH满后丢新且不补发、STOP/STOPSTART取消,以及总召未结束时立即提交错误应答。启用校时的独立回归另外核对103业务。
17组校时及时间接线回归覆盖无站广播45/46、同批及重复103逐条执行、FIFO中间否定、满槽丢新、Try等待不重执行、STOP/重连、Defer/FATAL、同一次采样检查期限及补偿、action内Get_Status与防重入、广播WAIT期间删除当前站及同CA替代不加入。其中新增1组只替换示例的Clock_Read/Set,验证system启动、60秒刷新、失败清基准、校时成功后回调外移交及NULL Command;Clock库仍正常链接,所有动作均不设置宿主系统时间。示例测试合计132组(99总召、12计数量、4兜底、17校时/接线)。
畸形请求回归覆盖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、撤销组合及连续兜底;当前ASDU数量及STOP统计见[测试记录](../test/IEC104测试文档.md#5-验证记录),计数量和命令独立计数。5个校时应答中3次执行并成功,另2次为TEST及COT拒绝;多CA由可控Net回归覆盖。
这些检查覆盖本示例的模拟点表流程;实际主站设备、现场点表和设备执行接口仍需联调。
@@ -0,0 +1,335 @@
# IEC104 测试说明
> **目录与命令同步日期**:2026-10-29。测试代码位于根目录的 `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后端,共32组统一入口测试,覆盖ASDU视图、共用发送分派、确认/终止字节、HIGH背压、非法参数、原连接身份、延迟复制、服务复用、生命周期、外部调度及离线上报。站点测试覆盖13节点、CA/context隔离、删除即时退出路由与引用延迟回收、同CA替代节点的注册编号、快照期间增删/嵌套/结束回收、显式集合锁及跨服务误用;新增9组同步拒绝回归。共用层非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)。
同步拒绝接口回归检查COT=44..47的原文回显、TEST/OA/CA/VSQ及对象字节保留、输入事件和最近提交结果不变、非法参数及原服务/会话隔离。未读启停通知拒绝提交;已消费STOP/START后的旧事件不在同步接口允许的调用范围内,不能用状态计数为0证明其仍有效。示例回归覆盖连续及重复内容的新命令、HIGH真满时丢新且保留连接/旧应答、活动总召中的兜底、启停与发送故障收尾;原同批兜底回归继续保留。接入校时后的真实TCP对103与102同批发送分别核对ACT_CON和COT=44,不保证跨业务响应顺序。接口与丢新语义见[同步拒绝当前请求](../104/IEC104_API接口汇总.md#reject-request)。
校时测试使用模拟action及可控单调时钟,不设置Windows时间。覆盖Handle/Process不执行应用、显式执行一次、原CP56回显、COT/CA/IOA校验优先级、TEST/IV/非法CP56拒绝、SU/时区转换、受理起滞留补偿、日期上界和64位运算、期限与倒退、Try背压不重复动作、FIFO/满槽、STOP/断线/旧会话、广播快照及删除目标,以及action内只读状态。真实设备写入耗时、权限及失败后的硬件状态不由这些模拟测试证明,边界见[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)。
## 2. 编译与运行
命令专项入口为`test/iec104_command_test.c`,复用TCP事件环测试的可控Net后端,但独立驱动Slave和Command,不依赖示例的应用状态。`./scripts/build.ps1 -Test`同时构建和执行TCP容量1/2 × 命令独立应答槽1/4的四种组合,每种46组;原45/46基线为每种27组。库源文件须包含`104/business/IEC104_Command.c`,测试本身包含可控TCP实现,不再额外链接Windows Net或第二份TcpServer。
命令原有覆盖45/46选择和直接执行、匹配/忙拒绝/撤销、三类动作结果、成功失败及终止配置、零/多帧返回信息、完整ASDU校验、全局FIFO、回复池满、窗口/部分写入、结果先于确认、重复及迟到事件、FINISHED配对、三个期限和时钟倒退(含期限配置0)、STOP提交竞态/FATAL、NORMAL覆盖反例、站点删除重注册、引用和单/双服务生命周期。扩展覆盖47–51/58–64;默认配置检查全部14种type_mask=0x3FFF及排除51/64的select_required_mask=0x1FBF。未启用47/58返回IGNORED的用例显式裁剪实例掩码,不依赖库默认值;非法配置使用保留bit14/15,另拒绝51/64加入select_required_mask和时区越界。
默认配置另有1组完整流程测试,不覆盖Config_Default产生的掩码,逐一受理14种类型:51/64直接执行,其他类型先选择后执行;7种带时标类型先验证无时间基准时否定,再设置基准走完流程。该用例与显式启用全部类型的测试分开,避免仅证明可配置而未证明默认可用。
变长输入采用测试侧独立手写长度表及报文字节,不共用Command布局清单。核对规范长度、额外尾字节、SQ、非法状态/CP56及NaN/Inf、带值动作参数、完整确认/撤销/终止回显;已知类型截短由Receiver拒绝,不能误记为Command接手并计rejected。匹配比较值与QU/QL并屏蔽S/E,CP56可不同,浮点+0/−0或跨类型仍失配;51/64数据位及CP56首字节不能被误读为S/E。回复池容量1/4下核对混合长度FIFO、WAIT重试和槽复用后的有效长度。
时标回归覆盖max=0、IV及UTC换算、无基准、等于/小于max、毫秒余数、超前、SU和时区、单调倒退与推算越界、回调内Set/Clear限制、失败设置保留原基准及STOP/断线保留基准。检查决定期间同点丢弃、忙时过期、SELECTED过期执行及寻址错误的顺序,区分time_expired、time_rejected、rejected与dropped_requests。真实回环核对45/46/49/58/59/62完整流程、限值拒绝、操作后总召当前值、47兜底,以及过期58没有应答、事件或动作。
决定期间同点激活的4组专项回归覆盖45/46、SELECT_WAIT与直接/选择后EXECUTE_WAIT、事件已取/未取、同批输入及发送背压。检查相同或不同命令字段均只增加dropped_requests,原事件、动作参数、回显和决定期限不变,不产生抢先的忙否定;另验证到期FINISHED配对、格式校验/type_mask/撤销/其他点忙拒绝,以及同CA重注册、STOP和重连的身份边界。
当前业务测试由99组总召、12组计数量、4组兜底及17组校时/时间接线组成,共132组,单/双服务配置各跑一次。总召在原98组基础上新增双点批次发送WAIT后读取最新值的回归,验证站总召和遥信分组的单点/双点帧序、游标只在提交OK后推进;真实TCP另核对双点遥控后总召读到更新值。计数量覆盖独立实例、请求校验、Type15/37批次边界、忙隔离、冻结/复位、动作重复调用和WAIT/STALE/TEST、业务期限、STOP/断线、共享连接失败、发送背压及多站广播;兜底改用未支持的106/102,覆盖同批与反序/重复内容、窗口与写阻塞、HIGH满丢新及恢复、STOP/STOPSTART取消和活动总召中的应答。校时专项检查独立受理与执行、原文确认、补偿/期限、广播及生命周期;新增1组用mock Clock_Read/Set验证system启动、60秒刷新、失败清基准、校时回调外移交和NULL Command,仍链接真实Clock库,但不设置宿主系统时间。`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: 32 groups passed (replies, identities, stations, lifecycle, backpressure, rejection)`。默认及`-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: 32 groups passed ...`、各`slave example + event interrogation tests: 132 groups passed ...`,以及四组命令专项、`interrogation rules: 3 groups passed ...`和`Slave example loopback: passed ...`。可控后端使用251字节HIGH和2槽召唤业务事件FIFO。真实回环使用默认存储、k=1、随机本机端口,验证总召与计数量交错、冻结/复位及后续读取、分组/广播、校时、命令操作、兜底、STOP/START、重连及正常退出。真实TCP校时使用模拟模式,system接线用mock单测,命令操作内存点表;实际主站、设备输出及宿主系统设时未纳入这组自动回归,不构成完整互操作认证。当前实际通过数量、STOP统计和测量产物见第5节。
## 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`并使用独立输出文件名即可。
### 校时固定存储与栈测量
2026-10-29 Windows x64 GCC默认配置实测:ClockTask为104字节,含4槽的ClockSynchronization实例为568字节,Request为80字节,Status为72字节;默认单服务的校时静态实例数组占568字节,不是完整协议库RAM总量。探针及产物位于`.build/clock-synchronization-20261029/size_probe.c/exe`。
`-O2 -fstack-usage`报告的单函数栈帧:Handle_Event 256、Execute_Next 240、Submit_Head 224、Process 64、Get_Status 80字节;示例main由修改前768增至800字节,Clock_Action为48字节。记录为同目录的`example-before.su`、`example-after.su`及`clock.su`。这些是x64宿主ABI和单函数测量,不是最坏调用链,也不代表ARMCC/MCU的RAM或栈占用;嵌入式接入须按实际工具链重新核算。
### 遥控固定存储与栈测量
下表是2026-10-30的45/46版本历史基线,Windows x64、GCC 16.1.0、`-O2`实测(单位字节);不是扩展后的当前容量:
| 对象 | 应答槽1 | 应答槽4(默认) |
|---|---|---|
| Command实例 | 448 | 616 |
| 单事务槽 | 168 | 168 |
| 单独立应答槽 | 56 | 56 |
| Request / Event / Status | 48 / 88 / 120 | 48 / 88 / 120 |
该历史版本在默认TCP服务容量1时Command静态实例数组为616字节;容量2时实例数组按实例数增长。上述不含既有Slave、TCP和发送队列。探针源文件及可执行程序位于`.build/command-v1/size_probe.c`、`size_probe_1.exe`及`size_probe_4.exe`,探针包含Command实现以测量私有结构,只作本地测量,不增加公开接口。
历史`-fstack-usage`单函数栈帧:Command Handle_Event 208、Accept 128、Submit_Return_Info 144、Submit_Head 144、Process 64字节;示例main由800增至848字节,模拟动作8字节。原始记录为`.build/command-v1/command-stack.su`、`example-before.su`及`example-after.su`。这些是Windows宿主ABI的单函数测量,不是最坏调用链或其他目标工具链的内存预算。
2026-10-31的5a内部泛化阶段仍只启用45/46,但已由14种布局生成21字节最大缓冲。Windows x64实测实例为480/672字节(回复槽1/4),事务槽192、回复槽64,Request/Event/Config/Status仍为48/88/20/120。`-O2 -fstack-usage`单函数帧为Decode_Element 144、Copy_Request 96、Submit_Head 176、Handle_Event 304、Process 64、NextEvent 144、Accept 144、Submit_Return_Info 144字节;修改前同次探针的NextEvent为160字节。最终启用遥调和时标后的数值另记,不能把这一中间阶段当作最终内存预算。
5b增加value并启用47–51后,上述sizeof及栈帧测量均与5a相同;在此x64 ABI中value恰好使用Request原有对齐空隙,不代表其他目标布局不变。该阶段全量构建通过Slave 32组×2、示例131组×2、Command 33组×4、规则3组及真实TCP设定值流程,49执行后的总召读回通过。
5c启用58–64并加入时间基准后的最终Windows x64测量:
| 对象 | 应答槽1 | 应答槽4(默认) |
|---|---|---|
| Command实例 | 512 | 704 |
| 单事务槽 / 单独立应答槽 | 192 / 64 | 192 / 64 |
| Request / Event | 80 / 120 | 80 / 120 |
| Config / Status | 28 / 128 | 28 / 128 |
相对45/46历史基线,每实例增加64/88字节;默认单服务Command静态实例数组为704字节。容量仍由布局清单最大规范长度推导,当前为21,不是接收时分配。最终GCC `-O2 -fstack-usage`单函数帧:Decode_Element 144、Copy_Request 112、Submit_Head 176、Handle_Event 288、Process 64、NextEvent 176、Accept 176、Submit_Return_Info 144字节。公开头C++11严格编译检查通过。以上仍为宿主单函数帧,不是调用链上界;STM32F4须以目标工具链重新测量,不能用这些数字代替目标RAM/栈预算。
本轮产物统一保存在`.build/command-extension-20261031/`,包含修改前`before/`、三个阶段的`5a/`、`5b/`、`5c/`及`RESULTS.txt`。私有故障探针另验证保存原文损坏时的两条收尾路径:未交付工作事件时NextEvent返回ERROR且不产生事件;已交付后Accept返回ERROR、不调用动作或发肯定确认,FINISHED(SERVICE_ERROR)恰好一次,读完后事务槽可复用。
## 5. 验证记录
2026-10-31 默认启用全部控制命令:按应用接入要求,Config_Default的type_mask改为45–51/58–64全部14位(0x3FFF),select_required_mask默认排除没有S/E的51/64(0x1FBF),其余12种仍须先选择。应用裁剪类型时同步裁剪选择掩码;示例显式赋值为已接入的45/46/49/58/59/62,保留未接入47等类型的44兜底。10秒窗口、UTC+8及过期静默策略不变。新增默认14种类型完整受理与执行回归,`./scripts/build.ps1 -Test`严格GCC构建零警告、退出码0:Command 46组×4、示例132组×2、Slave 32组×2、规则3组和真实TCP六类命令流程均通过。此变更只调整默认掩码,不改变结构布局;Request/Event/Status保持80/120/128字节。修改前快照位于`.build/command-default-all-20261031/before/`;下方三阶段记录保留变更前事实。
2026-10-31 遥调与带时标命令:在原Command模块内分5a布局泛化、5b遥调、5c时标三阶段完成45–51/58–64扩展。5a原45/46逐字节回归四配置各27组保持预期通过,5b增加到33组;最终`./scripts/build.ps1 -Test`通过严格GCC构建及全部运行,Slave 32组×2、示例132组×2、Command 45组×4及规则3组均通过,真实Windows TCP回环通过。该次验收时默认类型为45/46,额外类型显式启用;X-macro统一推导规范长度和21字节容量,原文、回复及FIFO使用有效长度。
本轮回环覆盖45/46/49/58/59/62选择、执行、返回信息及ACT_TERM;49/62写入的遥测和58/59更新的遥信由既有总召读回,初值与帧序保持兼容。过期58与106同批发送,核对106应答、后续计数量全部帧和统计,确认没有58应答或动作。STOP实测submitted=83、gi_submitted=11,command_selected=7、command_executed=6、command_completed=6、command_returns=6、command_time_expired=1、command_time_rejected=0。时间边界、IV/SU、时区、基准异常、撤销和匹配顺序由可控时钟专项覆盖;system读时接线使用mock,未设置宿主时钟。
三阶段sizeof及GCC栈帧见第4节;公开头C++11检查、内部原文损坏收尾探针通过。17份Markdown的356个本地链接及锚点、UTF-8/LF、16个公开函数在API文档中的覆盖和diff空白检查均通过。基线快照、阶段产物和RESULTS记录位于`.build/command-extension-20261031/`。所有修改及验证限于IEC60870-Slave目录,未执行父工程Keil构建、目标RAM/栈测量或硬件主站联调;宿主结果不代替这些验收。
2026-10-30 恢复双点遥控地址映射:Type46命令恢复为十进制6101..6108,对应Type3遥信仍为1101..1108。示例帮助、当前说明和回环请求同步更新。原EXE被运行进程占用时首次构建链接失败;用户关闭后,`./scripts/build.ps1`成功更新常规示例EXE,严格GCC构建无诊断。真实TCP回环验证6103选择/执行后返回1103,后续站总召、遥信分组及重连均读到新值,并通过既有单点遥控、撤销、错误应答及生命周期场景。沙箱内首次回环因Node启动子进程EPERM失败,获准在沙箱外重跑相同命令后退出码0。
2026-10-30 双点遥信加入总召:QOI20按Type1、Type3、Type11依次返回各8点,共8帧;QOI21返回单点及双点共6帧,QOI22仅遥测仍4帧。沿用1101..1108及double_values,每批4点、每次发送重试重取值;只新增双点遍历游标,不增加点值数组或发送缓冲。TCP容量1/2下各131组示例C测试通过,严格GCC构建无诊断;真实TCP验证Type46操作1103后,站总召、遥信分组及重连读回均为新值,STOP submitted=83、gi_submitted=11。原示例进程运行中,`./scripts/build.ps1`因无法覆盖被占用的exe而链接失败;随后以相同源文件及编译参数生成`.build/slave-example/iec104_slave_example_double_gi.exe`并通过回环,未中断当前连接。旧进程退出后,常规`./scripts/build.ps1`重跑成功,更新`iec104_slave_example.exe`,退出码0。可控测试产物为同目录`iec104_slave_example_double_test_1.exe`及`_2.exe`。
2026-10-30 恢复单点遥控地址映射:Type45恢复6001..6008控制Type1遥信1001..1008;Type46仍使用1101..1108。`./scripts/build.ps1`严格GCC构建无诊断;沿用已获准的真实TCP回环命令,验证6001选择/执行后返回1001及总召新值、1001作为Type45命令时回47、双点1103返回1103,并通过原有撤销、错误应答和生命周期场景,退出码0。示例帮助、说明和回环期望同步更新。
2026-10-30 示例遥控地址对齐遥信:Type45改用1001..1008,Type46改用1101..1108,命令与返回信息使用同一IOA。严格GCC构建无诊断,Slave单/双服务各32组、既有业务各130组、遥控四配置各27组及3组规则测试通过。真实TCP核对新地址的选择/执行/返回/终止、撤销及单点操作后的总召新值,旧地址6001回47。`./scripts/build.ps1 -Test`末尾回环受沙箱子进程EPERM限制,首次整体退出码1;获准单独重跑原回环命令后退出码0。帮助文本、示例说明和回环断言同步更新,协议库点址校验保持通用。
2026-10-30 遥控决定窗口同点激活:SELECT_WAIT/EXECUTE_WAIT内,通过校验且同会话、同站点注册编号、同IOA的激活只计dropped_requests并HANDLED,不覆盖请求、不刷新期限、不生成新事件或忙否定。新增4组回归,TCP容量1/2与回复槽1/4的四组合各27组通过;`./scripts/build.ps1 -Test`的严格GCC构建无诊断,Slave单/双服务各32组、既有业务各130组及3组规则测试通过。脚本末尾真实TCP回环因沙箱阻止Node子进程而报EPERM,首次整体退出码1;随后获准单独重跑原回环命令,退出码0,真实TCP全流程通过。Command公开头C++11严格语法检查通过,文档链接与UTF-8/LF检查通过;未改公开接口或固定存储容量,修改仅限IEC60870-Slave。
2026-10-30 Type45/46遥控:新增独立单事务模块、固定回复池、选择/执行/撤销、显式动作与设备结果、原连接返回信息及全局FIFO收尾。`./scripts/build.ps1 -Test`以GCC `-std=c11 -O2 -Wall -Wextra -Werror`构建无诊断,Slave单/双服务各32组、既有业务单/双服务各130组、遥控四配置各23组和3组召唤规则测试全部通过;真实Windows TCP回环通过45/46完整流程、撤销、TEST/地址拒绝、未支持类型、总召新值及原有计数量/校时/启停重连,脚本最终退出码0。沙箱内首次Node启动因EPERM失败,随后在获准环境重跑完整原脚本通过。校时使用模拟模式,遥控操作模拟点。修改前84个文件的快照、容量测试、sizeof探针及栈记录保存在`.build/command-v1/`;所有实现与验证均限定在IEC60870-Slave目录。
2026-10-29 Type 103时钟同步:新增独立静态池模块、显式Execute_Next动作、64位受理后滞留补偿、执行期限、原文ACT_CON及广播快照确认;示例默认模拟,显式system模式才设置Windows时间。整合`build.ps1 -Test`中GCC `-std=c11 -O2 -Wall -Wextra -Werror`构建无诊断,Slave单/双服务各32组、业务单/双服务各130组(98总召、12计数量、4兜底、16校时)及3组规则测试均通过。脚本内首次Node启动因沙箱`spawnSync EPERM`失败,因此该次脚本整体不是成功退出;随后获准单独重跑相同Node回环命令,退出码0,真实本机TCP通过,核对submitted=65、clock_executed=3、clock_synchronized=3、clock_submitted=5。默认模拟校时的正常/TEST/COT=5/广播及103+102同批均覆盖,未执行system模式或真实设备设时。改前基线、大小探针及栈测量位于`.build/clock-synchronization-20261029/`。
2026-10-28 业务目录整理:共用召唤引擎、内部规则头、总召和计数量共7个源码/头文件移到`104/business/`,迁移前后SHA256逐字节一致;构建加入该目录的头文件搜索路径,同步测试直接包含路径及文档源码链接。严格GCC构建通过,单/双服务各110组业务测试、23组Slave测试及3组规则测试通过,公开头C++检查和真实Windows TCP回环通过。沙箱限制Node启动子进程时,回环使用原授权命令在沙箱外执行。迁移清单及备份位于`.build/business-layout-20261028/`,修改仅限IEC60870-Slave;未执行Keil目标构建或硬件联调。
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,340 @@
/*
* 模块:IEC104控制命令业务层,45..51及58..64,COT=6激活、COT=8撤销。
* 职责:校验请求、选择/执行匹配、撤销、三类期限、原文确认和事务收尾。
* 点表、闭锁、远方/就地策略及实际输出由应用负责;本模块不判定设备操作是否安全。
* 每个Slave至多一个实例,每实例至多一个未释放事务;其他业务仍独立推进。
* 静态存储、无动态分配、不保存回调;所有入口须与Slave在同一事件循环串行调用。
* 只有Accept调用应用动作,先消费事件、再至多调用一次;Handle_Event/Process不调用应用。
* 返回信息由Submit_Return_Info沿原连接提交,确认/返回信息/终止保持顺序。
*
* 接入顺序:先Create,再按Slave通知FIFO分派Handle_Event;每轮Process后读空NextEvent。
* SELECT/EXECUTE事件由Accept或Reject消费;EXECUTE启动成功后,先Report_Result,
* 再逐帧Submit_Return_Info,全部OK后Complete;最终读取FINISHED清理应用资源。
* Report_Result也可晚于返回信息,但必须早于Complete,且设备结果产生后应及时报告。
* WAIT表示本次应用操作尚未接手,保留输入并在读取通知/推进网络后重试;OK不得重复提交。
* 动作/确认/设备结果/FINISHED是不同节点:FINISHED只结束协议事务,不撤销物理输出。
* STOP、断线或Cancel不回滚动作,也不把原连接的返回信息转交到新连接。
*
* 退出顺序:Cancel、读完FINISHED、Slave_Close、读空并分派通知、Destroy,
* 然后由应用移除站点并销毁Slave。模块不负责Slave或应用持有引用的释放。
*/
#ifndef IEC104_COMMAND_H
#define IEC104_COMMAND_H
#include "IEC104_Slave.h"
#include "IEC60870_Time.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 不透明静态池句柄;Destroy成功后失效,不得按值复制内部对象或继续使用旧指针。 */
typedef struct IEC104_Command IEC104_Command_t;
/* 供无副作用的类型常量使用;45..51映射bit0..6,58..64映射bit7..13。
* 范围外为0,不作非法移位。Create接受全部14位,默认只启用45、46。 */
#define IEC104_COMMAND_TYPE_BIT(type) ((uint16_t)( \
((type) >= 45U && (type) <= 51U) ? (1U << ((type) - 45U)) : \
(((type) >= 58U && (type) <= 64U) ? (1U << ((type) - 51U)) : 0U)))
/* 创建时复制的配置。期限以Net单调时钟计算,等于边界即到期;
* 0仅禁用时长限制,等待阶段遇到时钟倒退仍终结。选择期限包含确认的发送等待;
* Report_Result之后没有业务期限,应用漏调Complete会一直占用事务槽。 */
typedef struct {
uint16_t type_mask; /* 默认45|46,不能为空;可显式启用45..51、58..64。 */
uint16_t select_required_mask; /* 默认45|46,必须是type_mask子集;51/64无S/E,禁止要求选择。 */
uint32_t select_timeout_ms; /* 选择动作返回至匹配执行受理,默认30000,不得为0。 */
uint32_t decision_timeout_ms; /* 工作事件生成至Accept/Reject,默认15000;0禁用。 */
uint32_t execute_timeout_ms; /* 动作STARTED至Report_Result,默认60000;0禁用。 */
bool send_act_term; /* 成功后发ACT_TERM,默认true;false时失败也不发。 */
bool act_term_on_failure; /* 默认false;true且send_act_term时,启动后报告失败也发COT=10、P/N=0。 */
uint32_t time_tag_max_delay_ms; /* 默认10000;0只关闭时差检查,仍拒绝IV=1/UTC换算失败。 */
int16_t master_utc_offset_minutes; /* 主站标准时区,默认480,范围-1439..1439;SU另由公共层处理。 */
bool negative_on_expired; /* 默认false,过期激活静默丢弃;true时回否定ACT_CON。 */
} IEC104_CommandConfig_t;
/* 可按值保存的请求快照;不含站点/ASDU等借用指针。
* state为单命令0/1、双命令1/2、步调节1降/2升;具体操作含义由应用点表定义。
* accepted_ms为Handle_Event受理时刻,包含Slave通知排队时间,不是网络接收时间。 */
typedef struct {
uint64_t generation, receive_serial; /* 原连接代次、当前工作请求的I帧接收编号。 */
uint64_t station_id; /* 站点注册身份;同CA重新注册后此值不同。 */
uint16_t common_address; /* 请求公共地址CA,不接受广播65535。 */
uint8_t originator_address, type_id; /* 请求源发地址OA、类型45..51或58..64。 */
uint32_t ioa; /* 非零24位信息对象地址;存在性由应用点表确认。 */
bool is_select, selected; /* S/E及本事务匹配选择标志;51/64两者恒为false。 */
uint8_t state, qualifier; /* 基础45..47:状态/QU;48..50:0/QL(0..127);51:0/0。 */
union {
int16_t normalized; /* 48/61:原始定点编码,实际值为normalized/32768.0。 */
int16_t scaled; /* 49/62:有符号标度值。 */
float short_float; /* 50/63:有限IEEE 754 binary32。 */
uint32_t bitstring; /* 51/64:32位数据,不包含选择位。 */
} value; /* 按type_id读取;状态命令清零。 */
bool has_time; /* 58..64为true,字段含义与对应基础类型相同。 */
IEC60870_CP56Time2a_t time; /* 解码后的原始时标,保留IV/SU,不是可直接发送的字节布局。 */
uint64_t time_utc_ms; /* UTC纪元毫秒,含毫秒余数;无时标为0。受理后不再判时差。 */
uint64_t accepted_ms; /* 当前工作请求受理时的单调毫秒。 */
} IEC104_CommandRequest_t;
/* SELECT和EXECUTE为待决定工作事件;FINISHED仅通知清理,无需应答。 */
typedef enum {
IEC104_COMMAND_SELECT = 1, /* 应用检查策略并Accept/Reject选择,可执行返校动作。 */
IEC104_COMMAND_EXECUTE, /* 应用Accept/Reject执行;selected区分选择后执行和直接执行。 */
IEC104_COMMAND_FINISHED /* 该事务唯一的结束通知,原因及副作用标志在事件中给出。 */
} IEC104_CommandEventType_t;
/* 首次结束时冻结,后续STOP/Cancel不会覆盖;枚举不表示设备输出已停止。 */
typedef enum {
IEC104_COMMAND_FINISH_NONE = 0, /* 工作事件的占位值,不是FINISHED的正常原因。 */
IEC104_COMMAND_FINISH_COMPLETED, /* 应用报告成功,Complete后最后应答已交接。 */
IEC104_COMMAND_FINISH_EXECUTION_FAILED, /* 输出已启动,但应用报告失败,协议收尾已交接。 */
IEC104_COMMAND_FINISH_REJECTED, /* 应用Reject,或动作返回NOT_STARTED。 */
IEC104_COMMAND_FINISH_ACTION_FAILED, /* 动作返回FAILED/非法值,可能存在副作用。 */
IEC104_COMMAND_FINISH_MISMATCH, /* 选择确认交接后,同一点执行的类型/OA/值/限定词不符。 */
IEC104_COMMAND_FINISH_DEACTIVATED, /* 主站COT=8成功撤销尚未启动执行的事务。 */
IEC104_COMMAND_FINISH_SELECT_TIMEOUT, /* 选择保持到期,释放选择,已交接的确认无法撤回。 */
IEC104_COMMAND_FINISH_DECISION_TIMEOUT, /* 未及时Accept/Reject,模块安排否定确认。 */
IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT, /* 未及时Report_Result;保留肯定确认,不发ACT_TERM。 */
IEC104_COMMAND_FINISH_CANCELLED, /* 本地Cancel结束事务。 */
IEC104_COMMAND_FINISH_STOPPED, /* 原连接STOPDT停用。 */
IEC104_COMMAND_FINISH_DISCONNECTED, /* 原连接断开。 */
IEC104_COMMAND_FINISH_CONNECTION_CHANGED, /* 新连接代次或会话身份已失效。 */
IEC104_COMMAND_FINISH_SEND_ERROR, /* 发送ERROR/FATAL,且不是等待分派正常STOP通知。 */
IEC104_COMMAND_FINISH_SERVICE_ERROR /* Slave服务错误或内部身份/应答资源无法继续使用。 */
} IEC104_CommandFinishReason_t;
/* 事件是可保存的纯值,应答须原样传回;调用方不能用修改request改变实际执行内容。
* 任何工作事件曾被取走的事务,恰好交付一次FINISHED;后者无需应答。
* 应用跨循环持有站点/context或异步动作时,应在首个工作事件时自行Acquire,
* FINISHED清理且物理输出确已结束后Release,不能等读到FINISHED后才申请引用。 */
typedef struct {
IEC104_CommandEventType_t type; /* 决定事件的处理方式,参与身份校验。 */
uint64_t instance_id, transaction_id, event_serial; /* 实例、事务、事件三层身份,原样回传。 */
IEC104_CommandRequest_t request; /* 最后接纳的工作请求,不被撤销/不匹配请求覆盖。 */
IEC104_CommandFinishReason_t reason; /* 仅FINISHED有效;工作事件为NONE。 */
bool select_effect, execute_effect; /* 仅FINISHED有效:对应动作曾STARTED/FAILED,可能需清理。 */
} IEC104_CommandEvent_t;
typedef enum {
IEC104_COMMAND_ACTION_NOT_STARTED = 0, /* 必须保证没有任何副作用。 */
IEC104_COMMAND_ACTION_STARTED, /* 选择生效或输出启动;输出结果另行报告。 */
IEC104_COMMAND_ACTION_FAILED /* 未报告启动成功,但可能已留下副作用。 */
} IEC104_CommandActionResult_t;
/* Accept同步调用的动作,不保存函数/context指针。
* context为应用传入的上下文,可为NULL;request由模块内部快照构造,仅本次调用有效。
* 须短时返回,不得保存request指针、推进Slave或发布上报;设备的异步完成用Report_Result报告。
* 回调期间仅可Get_Status/Get_Transaction;其他修改入口拒绝重入。
* 返回值须准确说明是否有副作用,非法值按FAILED处理,模块不会重试动作。 */
typedef IEC104_CommandActionResult_t (*IEC104_CommandAction_t)(void *context,
const IEC104_CommandRequest_t *request);
typedef enum {
IEC104_COMMAND_RESULT_SUCCEEDED = 1, /* 启动后设备实际执行成功。 */
IEC104_COMMAND_RESULT_FAILED /* 启动后设备实际执行失败,不等同于拒绝启动。 */
} IEC104_CommandResult_t;
typedef enum {
IEC104_COMMAND_REJECT_NEGATIVE = 1, /* 通用策略拒绝,回COT=7、P/N=1。 */
IEC104_COMMAND_REJECT_UNKNOWN_IOA /* 点表不存在此IOA,回COT=47、P/N=1。 */
} IEC104_CommandRejectCause_t;
typedef enum {
IEC104_COMMAND_EVENT_ERROR = -1, /* 参数/来源/服务错误;不能据此重放已产生的动作。 */
IEC104_COMMAND_EVENT_IGNORED = 0, /* 不接手,I帧可继续交后续处理器。 */
IEC104_COMMAND_EVENT_HANDLED = 1 /* 已接手,包括校验拒绝/计数丢弃,不应再走44兜底。 */
} IEC104_CommandEventResult_t;
typedef enum {
IEC104_COMMAND_NEXT_ERROR = -1, /* 句柄/参数/重入或服务错误,输出未写入。 */
IEC104_COMMAND_NEXT_EMPTY = 0, /* 暂无可交付事件,可能须先读Slave通知;输出未写入。 */
IEC104_COMMAND_NEXT_READY = 1 /* 已交付纯值事件,同一事件不会再次出队。 */
} IEC104_CommandNextResult_t;
/* 以下为共同分类;OK究竟消费事件、记录结果还是固定帧序,以各接口说明为准。
* WAIT均未接手本次应用输入;只有Submit_Return_Info的OK直接代表该ASDU帧序已固定。 */
typedef enum {
IEC104_COMMAND_REPLY_STALE = -2, /* 身份/阶段已失效、事件已消费或事务结束,不得重试旧操作。 */
IEC104_COMMAND_REPLY_ERROR = -1, /* 参数/用法/重入或服务错误,检查接口具体前置条件。 */
IEC104_COMMAND_REPLY_WAIT = 0, /* 等待通知/确认/FIFO/网络,保留输入,推进事件循环后重试。 */
IEC104_COMMAND_REPLY_OK = 1 /* 本次操作已接手;不保证设备成功、网络ACK或FINISHED已就绪。 */
} IEC104_CommandReplyResult_t;
typedef enum {
IEC104_COMMAND_PHASE_SELECT_WAIT = 1, /* 等应用决定选择;决定期限从事件生成开始。 */
IEC104_COMMAND_PHASE_SELECTED, /* 已选择,等待匹配执行;选择确认可能仍未交接。 */
IEC104_COMMAND_PHASE_EXECUTE_WAIT, /* 等应用决定执行;已停止选择期限。 */
IEC104_COMMAND_PHASE_EXECUTING, /* 已启动,等待结果/返回信息/Complete/最后应答交接。 */
IEC104_COMMAND_PHASE_ENDED /* 协议已结束,可能仍保留FINISHED或未交接应答。 */
} IEC104_CommandPhase_t;
typedef struct {
bool active; /* 尚未ENDED;不是当前连接是否可操作的保证。 */
IEC104_CommandPhase_t phase; /* 当前协议阶段;结果报告和Complete不另建阶段。 */
bool confirm_submitted; /* 当前工作请求的确认已交给发送层,不表示写完或ACK。 */
/* result_reported:结果已冻结、执行期限停止;completed:已接手Complete,返回阶段关闭;
* term_submitted:ACT_TERM已交接。不配置ACT_TERM时此值一直false,不能据此等事务结束。 */
bool result_reported, completed, term_submitted;
} IEC104_CommandTransactionInfo_t;
/* 累计计数从Create起维护;查询不改变阶段、不做超时转换。
* busy包括待消费FINISHED和未交接的事务应答,不包含独立短应答。
* need_process不因发送WAIT持续置true;每轮网络推进后仍须Process重试。
* 计数用途是诊断,不是可靠账单;各计数可重叠,unsigned long到达上限后自然回绕。 */
typedef struct {
uint64_t generation; /* 当前已分派的连接代次,断线后为0。 */
/* active:模块已处理ACTIVATED且尚未停用,不代替操作前的实时会话校验;
* events_pending:须优先读取Slave通知;need_process:有可立即推进的内部工作;
* timer_active:当前启用了非零时长期限,时钟倒退检查不受此标志限制。 */
bool active, events_pending, need_process, timer_active;
bool busy; /* 唯一事务槽仍占用,此时其他站点/其他IOA也不能新建事务。 */
bool time_base_valid; /* 持有时间基准;不表示推算时尚未越界,STOP/断线不清除。 */
uint32_t timeout_remaining_ms; /* 当前期限剩余毫秒;无期限/已到期/时钟倒退时为0。 */
/* pending_replies为模块仍持有的应答数,不含发送层已接手的帧和应用保留的返回信息;
* pending_events为尚未取走的工作事件或FINISHED数,本版最多1。 */
unsigned pending_replies, pending_events;
/* accepted:生成的工作事件数(选择后执行计两次);selected/executed:对应STARTED次数;
* completed/execution_failed:成功/失败收尾数;action_failed:动作FAILED结束数。 */
unsigned long accepted, selected, executed, completed, execution_failed, action_failed;
/* submitted:所有已交接帧,含返回信息;negative_submitted:其中P/N=1的应答;
* return_info_submitted:已交接返回信息;waits:发送接口明确返回WAIT的次数。 */
unsigned long submitted, negative_submitted, return_info_submitted, waits;
/* rejected:接收校验/匹配/忙/无选择等拒绝数;app_rejected:Reject或NOT_STARTED数;
* busy_rejected:因事务槽占用拒绝数;overloads:独立应答池满或事务/事件序号耗尽次数;
* dropped_requests:过载丢新数及决定期间同点合法激活的静默丢弃数,后者不计rejected/busy_rejected/overloads;
* cancelled:本地取消、停用、断线、换代、发送/服务错误导致的事务结束总数。 */
unsigned long rejected, app_rejected, busy_rejected, overloads, dropped_requests, cancelled;
/* deactivated:成功撤销数;mismatches:选择失配数;其余三个为各期限导致的结束数。 */
unsigned long deactivated, mismatches, select_timeouts, decision_timeouts, execute_timeouts;
/* 事务期限或时间基准检查发现单调时钟倒退的次数;两个检查的计数可能重叠。 */
unsigned long clock_errors;
unsigned long time_expired; /* 过期激活,静默时不计rejected/dropped_requests;否定时另计rejected。 */
unsigned long time_rejected; /* IV/换算/基准/倒退/推算/超前错误;同时计rejected,不含过期。 */
} IEC104_CommandStatus_t;
/* 填写本版默认配置:45/46均要求先选择,决定15s、选择30s、执行60s,成功发ACT_TERM。
* 时标窗口10s,主站时区UTC+8,过期激活静默丢弃;默认不启用带时标类型。
* 入参:config为调用方可写结构;NULL时无操作。返回:无;不创建实例或启动计时。 */
void IEC104_Command_Config_Default(IEC104_CommandConfig_t *config);
/* 从静态池创建并绑定一个遥控实例;不自动开启Slave或消费其通知。
* 入参:slave为有效Slave,寿命须覆盖实例;config非NULL,按值复制,调用后可修改原结构。
* 必须在开始消费Slave通知之前创建,以完整接收CONNECTED/ACTIVATED。
* 池容量IEC104_COMMAND_CAPACITY默认等于TCP服务容量;独立应答容量
* IEC104_COMMAND_REPLY_CAPACITY默认4,均在编译模块时通过宏配置。
* 启用51/64时要求选择的掩码应为type_mask & ~(TYPE_BIT(51) | TYPE_BIT(64)),
* 其中TYPE_BIT表示IEC104_COMMAND_TYPE_BIT;不能直接令select_required_mask等于type_mask。
* 返回:成功为实例句柄;配置/Slave非法、重复绑定、池或实例身份序号耗尽时为NULL。 */
IEC104_Command_t *IEC104_Command_Create(IEC104_Slave_t *slave, const IEC104_CommandConfig_t *config);
/* 设置可信站内时间基准;utc_ms为UTC纪元毫秒,base_ms为同一时刻的Net单调毫秒。
* 不读Clock、不保存回调;只影响之后受理的激活,STOP/断线保留基准。
* 句柄无效、动作回调内、utc_ms超出TIME_MAX_SECONDS*1000+999或base_ms晚于当前Net时间时
* 返回false且保留原基准;成功返回true。允许尚未激活或离线时设置。
* 时效只在Handle_Event受理时判断;默认配置下再等待决定,动作可在时标后接近25s启动。 */
bool IEC104_Command_Set_Time_Base(IEC104_Command_t *self, uint64_t utc_ms, uint64_t base_ms);
/* 本站时间不可信时清除基准;不取消已受理事务。无效句柄或动作回调内无操作。 */
void IEC104_Command_Clear_Time_Base(IEC104_Command_t *self);
/* 归还静态实例槽;不隐式Cancel,不关闭Slave,也不代替应用释放站点引用。
* 入参:self为待销毁实例;事务槽、未读事件及全部独立应答槽必须均空,且不在动作回调内。
* 返回:成功true,句柄随即失效;条件不满足或句柄无效为false,实例保留。 */
bool IEC104_Command_Destroy(IEC104_Command_t *self);
/* 本地结束事务,原因CANCELLED;保留欠应用的FINISHED,清空模块尚持有的全部应答。
* 已交发送层的帧无法撤回;保持会话激活,不回滚物理动作。
* 入参:self为实例;回调中/无效句柄无操作。返回:无;应用仍须读完FINISHED。
* ENDED事务保持首次终结原因,不重复产生FINISHED。 */
void IEC104_Command_Cancel(IEC104_Command_t *self);
/* 接收Slave通知并推进会话/请求状态;不调用应用动作,不保存event或其ASDU借用指针。
* 入参:self为实例;event是所属Slave按FIFO取出的通知,须在通知内容仍有效时分派。
* 全部连接/启停通知必须送达;通知消费不受队列里后续WAIT_EVENTS阻挡。
* 返回:激活且视图可用时接手配置内的控制类型,当前会话状态通知也为HANDLED;
* 未激活的I帧、视图不可用、未接手类型或旧代次通知为IGNORED;
* 参数、来源、重入或服务错误为ERROR。非法请求可否定或丢新计数,HANDLED不表示已应答。
* SELECT_WAIT/EXECUTE_WAIT期间,同会话、同station_id、同IOA的合法激活静默丢弃,计dropped_requests,
* 返回HANDLED;不比较其他命令字段,不覆盖原请求/事件、不刷新期限。格式校验、撤销及其他点忙拒绝不变。
* 对IGNORED的I帧继续分派后续业务,最后由应用兜底;不要对HANDLED再发44。 */
IEC104_CommandEventResult_t IEC104_Command_Handle_Event(IEC104_Command_t *self,
const IEC104_SlaveEvent_t *event);
/* 推进会话检查、到期转换、发送队首及事务回收;不调用应用代码。
* 入参:self为实例;每轮网络/通知处理后调用,最多推进2*(1+独立应答槽数)步。
* 队首WAIT立即让出,不能用紧循环等待发送;需处理Slave通知/网络后再次调用。
* 返回:true包括无事、背压等待或事务已终结;false为参数/重入或服务错误。
* 发送故障也可能已经转成FINISHED,此函数返回true不代表链路或设备执行成功。 */
bool IEC104_Command_Process(IEC104_Command_t *self);
/* 取出一条应用事件,同一工作事件最多交付一次;取出不等于Accept/Reject消费。
* 入参:self为实例;event非NULL,接收可按值保存的事件副本,仅READY时写入。
* 返回:READY/EMPTY/ERROR,见NextResult;EMPTY也可能表示须先读Slave通知。
* 工作事件交付前检查会话/期限,已有FINISHED可在离线或尚有Slave通知时读取。
* 读取FINISHED可能立即释放模块的站点引用,应用跨循环引用必须此前已自行Acquire。 */
IEC104_CommandNextResult_t IEC104_Command_NextEvent(IEC104_Command_t *self,
IEC104_CommandEvent_t *event);
/* 接受一个已由NextEvent交付的工作事件;先消费身份,再同步至多调用一次action。
* 入参:self为实例;event须原样传回SELECT/EXECUTE事件;context原样转交action,不保存。
* SELECT的action可为NULL,选择生效但select_effect=false;EXECUTE必须提供action。
* 返回:OK只表示工作事件已消费,不表示动作STARTED或确认已交接;动作失败也返回OK。
* WAIT为先读Slave通知,事件未消费;STALE为身份/阶段失效,不得执行输出;
* ERROR为参数/重入/服务或确认资源错误,动作未调用。
* 即使action返回STARTED,提交确认时仍可能结束事务;应用应查询事务并继续接收FINISHED。 */
IEC104_CommandReplyResult_t IEC104_Command_Accept(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandAction_t action, void *context);
/* 不调用动作,消费工作事件,安排否定ACT_CON或未知IOA应答,以REJECTED结束事务。
* 入参:self为实例;event为尚未消费的SELECT/EXECUTE;cause为两个RejectCause之一。
* 返回:OK表示事件已消费且应答已安排,可能已提交或因随后故障被取消;
* WAIT未消费、须先处理通知;STALE身份/阶段已失效;ERROR参数/原因/重入或服务错误。
* FINISHED可先于否定应答交接,读取FINISHED不代表槽已释放。 */
IEC104_CommandReplyResult_t IEC104_Command_Reject(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandRejectCause_t cause);
/* 报告设备结果,结果只记录一次并冻结;不发返回信息,也不隐式Complete。
* 本函数及下列Submit_Return_Info/Complete只接受Accept动作返回STARTED的原EXECUTE事件。
* 入参:self为实例,event为上述事件,result为SUCCEEDED或FAILED。
* 可早于ACT_CON交接;设备完成后应及时报告,OK即停止执行期限,此后发送等待无业务超时。
* 返回:OK已记录;WAIT因未读通知尚未记录;STALE事务结束/超时或事件不符;
* ERROR参数/结果非法、已报告/已Complete、重入或服务错误。 */
IEC104_CommandReplyResult_t IEC104_Command_Report_Result(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result);
/* 通过原连接Try路径提交一帧返回信息,不经过可覆盖的NORMAL队列。
* 入参:self为实例;event为启动成功的原EXECUTE;asdu为有效、完整的监视方向ASDU。
* 类型1..21/30..40,至少一对象,COT=11或3,P/N=0,TEST/CA与命令一致;完整校验对象。
* IOA可与命令IOA不同;本版命令TEST恒为0。调用可以早于或晚于Report_Result,但须早于Complete。
* 模块有更早待交应答、确认未交接、网络背压或未读通知时WAIT,不缓存ASDU、也不占排序位置。
* 返回:OK为发送层已接手且帧序固定,可释放/复用ASDU缓冲,不能重交;WAIT须保留ASDU重试;
* ERROR为ASDU/参数/重入/服务错误或Complete后仍待收尾,尚未接手;STALE为事务/事件失效。
* 正常STOP且通知待分派时先WAIT;其他发送ERROR/FATAL终结事务并返回STALE,不可重试旧ASDU。
* 必要的后续变位由应用另按COT=3上报。
* 同一变化勿再经Enqueue_Event重复发布;成功提交不表示对端ACK,断线不会迁移到新连接。 */
IEC104_CommandReplyResult_t IEC104_Command_Submit_Return_Info(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, const IEC60870_Asdu_t *asdu);
/* 声明本事务不再产生返回信息;按配置安排ACT_TERM并由Process完成后续交接。
* 入参:self为实例,event为启动成功的原EXECUTE;结果须已报告,全部返回信息须已提交OK。
* 允许零帧返回信息;可在确认未交接时调用,模块保持确认先于ACT_TERM的顺序。
* 返回:OK立即关闭返回阶段,不必重调;WAIT为未读通知,尚未接手;
* ERROR为尚未报告结果、仍待收尾时重复调用、参数/重入/服务错误;已结束/事件不符为STALE。
* 不发ACT_TERM时也须等待执行确认交接,之后才产生FINISHED;无发送等待时可能立即结束。
* 失败配置发TERM时P/N=0表示流程结束,不能据此认定设备成功。 */
IEC104_CommandReplyResult_t IEC104_Command_Complete(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event);
/* 只读查询仍占槽的事务,可在动作回调内使用,不做会话推进或期限转换。
* 入参:self为实例;event可为该事务任一已交付的SELECT/EXECUTE/FINISHED;info非NULL。
* 返回:true写入快照,ENDED的active=false;无效参数/身份、槽已回收时false且不修改info。
* 不保留历史事务;刚读出的FINISHED若已触发槽回收,使用它查询也会false。 */
bool IEC104_Command_Get_Transaction(const IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandTransactionInfo_t *info);
/* 只读查询会话、调度提示及累计计数,可在动作回调内使用。
* 入参:self为实例,status非NULL接收快照;计算剩余时间但不转换已到期事务。
* 返回:true写入快照;句柄/参数/Slave服务无效时false且不修改status。
* events_pending优先读Slave通知;need_process用于立即调度,timer_active配合剩余时间等待。
* need_process=false不免除每轮网络推进后的Process,也不代表应用没有待重试的ASDU/Complete。 */
bool IEC104_Command_Get_Status(const IEC104_Command_t *self, IEC104_CommandStatus_t *status);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_COMMAND_H */
@@ -0,0 +1,77 @@
# IEC60870-Slave
IEC 60870 子站协议库,当前提供 IEC104 子站、Windows TCP 后端、总召、计数量召唤、Type 103时钟同步,以及Type 45–51遥控/遥调和Type 58–64带时标命令。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_ClockSynchronization.c/h # 校时校验、显式动作与确认
│ │ └── IEC104_Command.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/`。
`-Test` 包含独立的 `test/iec104_command_test.c`:TCP服务容量1/2与遥控独立应答槽容量1/4组合成四组构建,HIGH容量为251字节。真实TCP回环入口为 `test/slave_example_loopback_test.cjs`,验证选择、执行、返回信息、撤销及遥控后总召读值。
接入自己的工程时,将 `104/` 和 `common/` 加入头文件搜索路径,使用总召、计数量、校时或遥控业务时再加入 `104/business/`;按所用模块选择源文件,仅使用公共模块时只需 `common/`。Windows 网络后端需链接 `Ws2_32`。统一入口为 `IEC104_Slave.h`,总召使用 `IEC104_Interrogation.h`(编译 `104/business/` 下的引擎和GeneralInterrogation.c),计数量使用 `IEC104_CounterInterrogation.h`,校时使用独立的 `IEC104_ClockSynchronization.c/h`,遥控使用独立的 `IEC104_Command.c/h`。接入约定见[计数量设计](docs/104/IEC104_CounterInterrogation_设计架构.md)、[时钟同步设计](docs/104/IEC104_ClockSynchronization_设计架构.md)和[遥控设计](docs/104/IEC104_Command_设计架构.md)。
子站示例默认使用模拟时钟;收到校时命令只更新模拟值。需要修改Windows系统时钟时显式选择`--clock system`,默认模式及自动测试均不修改系统时间。
命令模块的默认配置仍只启用Type 45/46;应用显式选择其他类型。示例启用45/46/49/58/59/62,全部要求先选择后执行:45/58的6001..6008控制Type 1遥信1001..1008;46/59的6101..6108控制Type 3双点位置1101..1108,双点初值均为1;49/62的6301..6308写入Type 11遥测2001..2008。站总召及对应分组读取更新后的运行值,帧序与初值不变。
带时标激活默认按10秒时差窗口检查,主站时区为UTC+8;校时成功后应用向Command提供UTC/单调时间基准,过期激活默认静默丢弃。示例只更新内存点值,设备输出由应用动作接入。Type 51/64没有选择位,只能直接执行;完整接入与时效边界见[子站示例](docs/examples/IEC104子站示例.md)和[遥控设计](docs/104/IEC104_Command_设计架构.md)。公开结构扩展后,库与调用方须一起重新编译。
详细资料见[文档索引](docs/README.md)、[模块说明与接入示例](docs/104/README.md)、[子站示例说明](docs/examples/IEC104子站示例.md)和[完整测试命令](docs/test/IEC104测试文档.md#2-编译与运行)。
@@ -0,0 +1,94 @@
# IEC104 时钟同步模块设计
`IEC104_ClockSynchronization.c/h`处理Type 103(`C_CS_NA_1`)时钟同步请求。模块负责请求校验、排队、站点路由、执行期限及ACT_CON;实际设时仅由应用显式调用`Execute_Next`时提供的同步action完成。`Handle_Event`和`Process`不会调用应用代码,模块不保存action/context,不直接依赖`IEC60870_Clock`或修改系统时钟。
源码:[IEC104_ClockSynchronization.h](../../104/business/IEC104_ClockSynchronization.h)、[IEC104_ClockSynchronization.c](../../104/business/IEC104_ClockSynchronization.c)。接入流程见[完整调用流程](IEC104_完整调用流程.md#flow-clock-synchronization),接口见[API汇总](IEC104_API接口汇总.md#file-24),运行入口见[子站示例](../examples/IEC104子站示例.md)。
## 1. 业务边界
- 只执行Type 103、COT=6的校时激活,动作结束只回ACT_CON,不发送ACT_TERM。
- ACT_CON回显原请求的16字节ASDU,替换COT、P/N并规范VSQ=1;多站广播另替换为目标站CA。CP56原文、SU、OA、TEST及其余对象字节保持原值,不以本站读时或补偿后的时间改写确认。
- 不支持COT=5读时、COT=8撤销或自定义确认时间;这些COT按45处理。没有NTP客户端、网络时延测量、时间质量老化或周期校时线程。
- 同内容的新请求不合并,也不保证幂等。主站重发旧时标可能再次回拨时钟;权限、允许步进幅度及设备副作用由应用action决定。
原文回显是对请求的确认,不是本次执行时刻的测量值。已有`IEC60870_Time`负责CP56/SU/时区转换,`IEC60870_Clock_Set`可作为应用的设备适配;设置失败也不承诺硬件未部分改变,协议模块不提供回滚。
## 2. 对象与固定存储
ClockSynchronization使用独立静态实例池,不占用总召/计数量的召唤引擎实例。`IEC104_CLOCK_SYNCHRONIZATION_CAPACITY`默认等于TCP服务容量,`IEC104_CLOCK_SYNCHRONIZATION_TASK_CAPACITY`默认4。一个Slave至多绑定一个校时实例;Create复制配置并借用Slave,须在开始消费该Slave的通知之前创建。每实例固定任务槽由合法待执行请求及待发否定/错误应答共用,不使用malloc,也不复制整份SlaveEvent。
任务保存原服务身份、必要请求字段、16字节原文及发送状态。应答严格按受理顺序FIFO提交,不越过等待执行或等待发送的旧任务。Execute_Next选最早EXECUTE,可越过较早的REPLY执行下一动作,但对应确认仍在原位等待,不插队。需要新槽而已满时丢弃新请求并增加overloads和dropped_requests;保留连接与已有任务,不覆盖旧任务、不额外执行动作,也不承诺每个超载请求都有响应。
所有调用在同一事件循环串行执行。action必须同步返回,不得保存请求指针、等待异步完成或重入任何修改本模块的入口;action内允许只读`Get_Status`。不为应用创建线程、定时器或时钟锁。Destroy之前先Cancel并完成清理,业务对象的寿命不得超过借用的Slave。
2026-10-29 Windows x64 GCC测得ClockTask为104字节、默认4槽实例为568字节、Request为80字节、Status为72字节;默认单服务的静态实例数组为568字节。此数值不含既有Slave/TCP/发送队列,不能作为MCU预算。示例main的x64单函数栈帧由768增至800字节,执行入口为240字节;完整单函数测量及产物见[测试说明](../test/IEC104测试文档.md#校时固定存储与栈测量),未将单帧结果当作最坏调用链。
### 2.1 请求校验顺序
非103返回IGNORED;以下规则按表中顺序判断,命中后不执行后续业务动作,输入Slave事件保持不变。
| 检查 | 处理 |
| --- | --- |
| 数量不是1,或不足16字节、无法保留唯一完整对象 | rejected加一,HANDLED丢弃,无应答 |
| COT不是6 | COT=45、P/N=1 |
| 定向CA未注册,或广播没有任何可用站点 | COT=46、P/N=1;无站广播保留65535 |
| IOA不是0 | COT=47、P/N=1 |
| SQ、尾字节、P/N、CP56字段、IV、TEST或本站时间范围非法 | 否定ACT_CON,COT=7、P/N=1 |
| 请求有效且有槽 | 保存EXECUTE,accepted加一,等待应用显式执行 |
| 需要保存而槽满 | 丢新并计overloads/dropped_requests,不断链 |
可应答的畸形单对象规范为16字节、VSQ=1,保留原16字节中的其他内容,仅修改COT/P/N及必要的广播目标CA。COT/CA/IOA错误优先于时标等内容错误;错误请求不刷新已有任务期限。HANDLED包含拒绝和丢弃,不表示已经发送响应。
## 3. 时间基准与执行期限
配置`master_utc_offset_minutes`默认为480,表示源报文标准当地时间减UTC的分钟数,合法范围-1439..1439,不包含夏令时。`master_time`保留原CP56解析值,转换时由`CP56_To_Seconds`检查IV并扣除SU对应的一小时;不得先清SU、先减小时或重复补时区。
`accepted_ms`是Handle_Event受理请求时的64位单调毫秒值,`base_ms`是本次执行检查所用的同一单调时钟采样。请求交给action时,`utc_ms`和固定UTC+8的`local_time`均对应`base_ms`。它不是设备设置成功或硬件真正生效的时刻。
计算使用已通过检查的UTC完整秒数:
```c
base_utc_ms = (uint64_t)utc_seconds * UINT64_C(1000) + master_time.time.ms;
limit_ms = ((uint64_t)IEC60870_TIME_MAX_SECONDS - UINT64_C(480) * 60U)
* UINT64_C(1000) + 999U;
```
受理前检查`base_utc_ms <= limit_ms`,保证该时刻可转换为本站日历;CP56转换成合法UTC秒数本身不保证固定UTC+8仍在2099年内。执行时先检查`base_ms >= accepted_ms`,再检查经过时间不大于`limit_ms - base_utc_ms`,之后才相加。最后以`utc_ms / 1000`和`utc_ms % 1000`调用`Seconds_To_Local`,成功后才调用action。不能先窄化为32位再检查,也不能在32位域中乘1000。
这里只补偿受理至执行检查之间的本站滞留,不补偿网络传输、Slave通知FIFO等待及action内部耗时。两次执行不同请求分别按各自受理时刻计算;重新收到旧时标不会自动补上它在主站重试前经过的时间。
`execute_timeout_ms`默认15000,0表示禁用。期限只覆盖尚未调用action的请求,从accepted_ms起算,`elapsed >= timeout`即到期。过期检查和执行补偿使用同一次单调时间采样,避免刚通过期限后又取时、越过边界仍执行动作。动作一旦调用,无论返回true或false,均不再受此执行期限控制;发送等待继续受链路及单帧期限约束。
到期请求转为否定ACT_CON并计timeouts,不调用action。单调时钟倒退显式判断:开启执行期限时按超时处理,禁用期限时按time_errors处理;补偿越界或日历转换失败也不执行action。0及连续相等的单调读数有效。期限不是标准规定的固定时限,也不替代链路t0..t3。
## 4. 执行、应答与发布顺序
正常任务从EXECUTE转为REPLY:`Execute_Next`先检查当前会话、未读通知和期限,再调用本次传入的action一次。true表示动作报告成功,false表示动作报告失败;二者分别生成肯定、否定ACT_CON。action调用期间`confirm_pending`已为true,调用返回后直到确认被发送层接手或任务取消仍保持该屏障。
执行过action的ACT_CON一律通过`Slave_Try_Send_Response`尝试提交,false结果也使用Try。已有HIGH/NORMAL待发原文、当前帧、窗口或记录背压时返回WAIT,模块保留原确认并在Process中重试,**不得再次调用action**。原请求校验失败产生的错误/否定应答通过普通`Slave_Send_Response`直接提交或进入HIGH。
Try返回OK表示原文已被发送层接手、帧顺序已确定,不表示写完socket或对端ACK。已经编号的I帧继续由发送层处理;业务不能因后续STOP撤回它。ERROR若确认来自原会话正常STOP、且通知尚待消费,则先让出等待通知取消;其余ERROR及FATAL取消尚未交接任务。FATAL可能已经写出字节,不重试、不回滚时钟,并继续排空连接通知。
Try只等待提交时已经存在的待发内容,没有为校时建立发送队列隔离区。持续新增NORMAL可能使确认一直WAIT。需要保证校时确认先于新的带时标上报时,应用在`confirm_pending`期间暂停产生这类新上报,继续推进网络以排空已有内容。取时、执行校时、发布及Try提交必须由应用串行协调;该状态不是跨线程锁。
## 5. 站点与广播
定向请求按Slave共用站点表匹配CA,动作请求保留原注册编号station_id;广播station_id为0。应用根据自己的context和该身份取得设备资源。CA=65535使用共用站点快照:受理时确定目标集合,之后新增站点不加入,同CA替代节点不承接旧请求。
广播只执行一次action,因为同一Slave的这些逻辑站共用本次校时动作;随后按快照顺序对仍有效的目标站分别提交同一结果,各应答CA替换为目标站CA。不按站重复设时。发送WAIT期间当前站被删除时,跳过该目标继续后续站;快照/引用按生命周期归还。
广播没有任何可用站点时不调用action,不建立虚构站点;保留原CA=65535生成一帧错误应答。错误COT优先45,COT合法而没有站点则46。具体校验失败不影响已有任务,也不刷新其执行期限。
## 6. 生命周期与事件循环
连接、启停和断线通知须交付所有业务实例;I帧依次交给总召、计数量、校时,均未接手才调用同步Reject_Request兜底。Slave通知优先处理,WAIT_EVENTS表示先继续读通知,不能绕过STOP/START历史仅凭最终激活状态执行旧任务。
每轮读空通知后调用校时Process处理超时及队首应答,再由应用显式调用Execute_Next执行当前允许的请求,继续Process提交确认。Process每步先检查会话和通知,单次最多推进2倍任务容量的步数;队首EXECUTE、发送WAIT或未读通知时让出,不在内部等待应用。等待前重新查询状态:有本地工作时立即推进,有执行期限时用最近剩余值限制等待;发送WAIT后可以等待网络,但网络推进并读空通知后仍要重试Process。
STOP、断线、服务实例/连接代次失效及Cancel取消尚未交接的任务和广播遍历。已经执行的时钟修改保留,不生成补偿动作,也不通过否定应答伪装成未执行。已交接的确认继续按发送层实际状态收尾。模块没有BEGIN/FINISHED业务事件队列;应用以显式执行结果及只读状态观察进度。
## 7. 构建、示例与验证
在Slave及其依赖之外加入`104/business/IEC104_ClockSynchronization.c`,头文件搜索路径包含`104/`、`104/business/`和`common/`。本模块使用公共时间转换工具和Net单调时钟,不要求链接`IEC60870_Clock.c`;应用需要实际设备读写时再选择Clock或自身平台适配。
示例默认只更新模拟时钟,不修改Windows系统时间。实际系统设时必须通过示例的显式模式选择,并由Windows权限决定是否成功;自动回归及本机TCP测试使用模拟模式。协议测试、内存测量和运行限制统一见[测试说明](../test/IEC104测试文档.md)及[示例说明](../examples/IEC104子站示例.md)。
@@ -0,0 +1,139 @@
# IEC104 遥控、遥调与带时标命令设计
`104/business/IEC104_Command.c/h`统一提供Type 45–51及58–64这14种控制命令。业务入口为Slave,实例使用静态池,不动态分配内存、不保存应用回调;所有入口与所属Slave在同一事件循环中串行调用。一个Slave至多绑定一个Command实例,一个实例至多保留一个命令事务。默认只启用45/46;未启用或范围外的类型返回IGNORED,交给后续业务或未知类型兜底。
## 1. 职责与默认配置
模块管理请求校验、选择与执行匹配、撤销、三类期限、应答排序、事务身份及 FINISHED。应用管理点表、权限、闭锁、返校和设备输出,显式报告设备结果并构造监视方向返回信息。协议结束不代表物理输出已经停止。
| 配置 | 默认值 | 含义 |
|---|---|---|
| type_mask | Type 45、46 | 可显式启用45–51及58–64;非零,bit14/15保留且不能置位 |
| select_required_mask | Type 45、46 | 必须是type_mask子集,且不能包含无S/E位的51/64;其他类型清除此位允许直接执行 |
| decision_timeout_ms | 15000 | 工作事件生成至应用决定;0 禁用 |
| select_timeout_ms | 30000 | 选择生效至匹配执行被模块受理;不得为0 |
| execute_timeout_ms | 60000 | 执行动作返回 STARTED 至 Report_Result;0 禁用 |
| send_act_term | true | 成功执行后发送 ACT_TERM |
| act_term_on_failure | false | 显式启用时也发送失败执行的终止;send_act_term=false 时忽略 |
| time_tag_max_delay_ms | 10000 | 带时标激活的时差上限;0只关闭时差检查,仍拒绝IV及UTC换算失败 |
| master_utc_offset_minutes | 480 | 主站时标标准时区,范围−1439..1439分钟;SU由公共时间工具处理 |
| negative_on_expired | false | 过期激活默认静默丢弃;true时回否定ACT_CON |
TEST=1 不执行动作,返回否定确认。广播 CA=65535 不接受。失败终止可选策略发送原执行请求的 COT=10、P/N=0,表示执行过程终止,不表示设备执行成功;结果由应用监视信息和 FINISHED(EXECUTION_FAILED) 表达。该策略为本模块接入约定,默认不开启。
## 2. 请求校验和选择匹配
标准104采用2/2/3参数,6字节公共头后接3字节IOA;命令元素从`IEC104_ASDU_HEADER_LEN + 3U`开始。规范ASDU长度由类型决定:
| 类型(无时标/带时标) | 命令元素 | ASDU长度 | 选择与校验 |
|---|---|---|---|
| 45 / 58 | SCO | 10 / 17 | 有S/E;保留位必须为0 |
| 46 / 59 | DCO | 10 / 17 | 有S/E;DCS只能为1、2 |
| 47 / 60 | RCO | 10 / 17 | 有S/E;RCS为1降一步、2升一步 |
| 48 / 61 | NVA + QOS | 12 / 19 | 有S/E;原始有符号16位定点值 |
| 49 / 62 | SVA + QOS | 12 / 19 | 有S/E;有符号16位标度值 |
| 50 / 63 | R32 + QOS | 14 / 21 | 有S/E;拒绝NaN及正负Inf |
| 51 / 64 | BSI | 13 / 20 | 无S/E,只能直接执行 |
配置内请求先检查当前事务到期,再检查唯一完整对象。COT非6/8回45,CA未注册或广播回46,IOA=0回47;之后检查SQ、规范长度、P/N、TEST、命令值及CP56格式,错误回否定7/9。公共解析器负责SCO/DCO/RCO及CP56校验;QU/QL的业务含义交应用解释。对象数量不符、或到达模块的原文不足规范长度时计rejected并丢弃,不能补零伪造对象。线上已知类型的截短通常更早由Receiver拒绝,不会到达Command,不能据此期待Command计数增加或连接继续。
过长请求只回显规范长度前缀,VSQ规范为1;除VSQ、COT和P/N外保留原文字节,包括非法值。回显不重新构造合法对象。已识别的拒绝和丢弃均返回HANDLED。
只有SELECT_WAIT、EXECUTE_WAIT两个决定阶段,对同会话generation、同station_id、同IOA且通过全部现有校验的COT=6激活静默丢弃,返回HANDLED。是否已被应用取走工作事件不影响此规则;不比较类型、OA、S/E、状态或QU,但新请求仍须通过type_mask、COT、寻址、格式和值校验。只增加dropped_requests,不增加rejected、busy_rejected或overloads;不覆盖原请求、不刷新决定期限、不生成新事件或应答。先检查到期,已到期事务不再适用此规则;其他点仍忙拒绝,COT=8仍按撤销处理。
决定期间同点激活的丢弃在时标判定之前;其他带时标激活在SELECTED匹配和忙判断之前执行第8节的时效检查。这样匹配的过期执行不会绕过检查,忙时其他点的过期请求也不会产生迟到的忙否定。
SELECTED阶段的选择执行要求同会话、原站点注册编号、IOA、类型、OA,且值字节及屏蔽S/E后的命令字节相同。状态、QU/QL均参与比较,CP56不比较,允许选择和执行使用不同时标;45与58等跨类型组合仍为MISMATCH。浮点按原始字节比较,+0与−0不同。此阶段选择确认尚未交接时,通过时效检查的执行按忙拒绝并保留选择;确认交接后同点不匹配执行终结为MISMATCH,否定回显本次执行。其他点的新激活只忙拒绝。匹配前检查选择期限,到期选择不能复活。
撤销按会话、站点编号、IOA、类型、OA匹配,不比较值、状态或限定词,不判时差;带时标撤销仍须通过CP56格式检查。SELECT_WAIT、SELECTED、EXECUTE_WAIT中的匹配撤销复用事务存储,肯定DEACT_CON回显撤销请求。执行已经启动、事务已结束或没有匹配时否定撤销。发送接口已经返回OK的选择确认不能撤回,随后DEACT_CON按顺序交接。
## 3. 应用交互与设备结果
事件为纯值副本,身份包含模块实例、事务、工作事件序号和类型。Accept/Reject只接受已经取出且未消费的工作事件。模块每次从已保存的规范原文解码请求;调用者修改事件副本不能改变执行参数。
状态类45–47及对应时标类型使用state和QU;48–50使用`value.normalized/scaled/short_float`及QL,state为0;51/64使用`value.bitstring`,state、qualifier及is_select均为0。normalized保留16位定点原始值,实际值为normalized/32768.0。带时标请求另含原始CP56(保留IV/SU)及换算后的UTC毫秒,非时标请求相关字段清零。毫秒值包含CP56的毫秒余数,不能只把整秒乘1000。
保存的原文再次解码失败属于内部错误。Accept在安排肯定确认和消费事件之前解码,失败按SERVICE_ERROR收尾且不调用动作;NextEvent失败不把未交付工作事件标为已读,已交付事务仍保留FINISHED义务。若终结时原文仍不可解码,只交付事务身份及SERVICE_ERROR,不把值字段当作动作参数;应用仍可读取终结事件并释放资源,避免坏原文永久占槽。
| 入口 | 成功后的含义 |
|---|---|
| Accept(SELECT) | 消费选择事件;可无动作,仅建立协议选择 |
| Accept(EXECUTE) | 消费执行事件并至多调用一次非NULL动作 |
| Report_Result | 冻结成功/失败结果并停止执行期限,重复报告报错 |
| Submit_Return_Info | 一帧已被发送层接手并固定顺序;WAIT未接手 |
| Complete | 关闭返回信息阶段,模块按配置继续交接终止;重复调用报错 |
动作返回NOT_STARTED必须保证没有副作用;STARTED表示选择生效或执行启动;FAILED表示尝试失败且可能留下副作用。SELECT的NULL动作不置物理副作用标志。执行动作返回STARTED后,应用可以在确认尚未发送时调用Report_Result,不把发送等待算成设备执行耗时。动作返回FAILED的否定确认同样使用Try,不能越过已有过程信息。
Accept的OK只表示事件被消费,不表示动作成功。应用应检查自己的动作结果或Get_Transaction中的执行阶段。报告结果后可以提交零帧或多帧返回信息,全部交接后Complete。每个WAIT都须保存当前交互,先处理网络和通知再重试;不能越过失败的Report_Result直接Complete。
Get_Transaction仅查询当前尚占槽的事务:ENDED仍可查询active=false,槽释放后旧事件查询返回false且不修改输出。FINISHED、旧SELECT及旧EXECUTE不能用于启动新动作;已消费EXECUTE仅供其对应的结果、返回信息和完成接口使用。
## 4. 状态与期限
状态依次为SELECT_WAIT、SELECTED、EXECUTE_WAIT、EXECUTING、ENDED;直接执行跳过选择阶段。EXECUTING内部另记录结果是否已报告、返回阶段是否关闭和确认/终止交接状态,避免重复动作、覆盖结果及重复终止。
期限采用Net单调毫秒;已用时间大于或等于期限即到期,时钟倒退按到期并计clock_errors。期限配置为0只禁用时长限制,正在决定、选择或等待设备结果时仍检查单调时钟倒退。决定期限从工作事件生成开始,包含事件尚未被应用取走的时间;选择和执行期限从动作返回后重新采样开始。这里的请求到达指Handle_Event受理时刻,包含Slave通知排队时间。
Accept/Reject消费交互后停止决定期限;Report_Result后停止执行期限。选择期限有意覆盖选择确认的发送等待,作为返校保持上限。执行超时不发ACT_TERM,已经执行动作所对应的肯定确认仍可继续提交;随后STOP或Cancel可以清除尚未提交的应答。结果报告后没有额外业务收尾期限,应用漏调Complete会持续占槽,直到明确取消或会话结束。
## 5. 通知、FIFO与返回信息
Handle_Event先校验来源并按Slave的FIFO消费连接/启停通知,不因后续未读通知而拒绝当前通知。有效I帧在匹配前检查期限;动作及发送入口先Check_Request,再处理到期。WAIT_EVENTS时不消费应用交互。NextEvent在离线时仍能交付已有FINISHED。
事务必要应答保存在事务槽,独立校验/忙/撤销否定保存在固定回复池,各自取得全局顺序号。Process只提交最早应答;队首WAIT时停止本轮。返回信息直接Try前也检查模块内是否存在更早应答,避免绕过本地FIFO。WAIT的应用ASDU未被接纳、不占持久排序位置。ACT_TERM在Complete被接受后安排,保证确认和已经接手的返回信息在先。
返回信息必须能完整解析,为监视方向Type 1–21或30–40,COT=11或3、P/N=0、TEST与命令一致、CA等于命令CA;监视IOA可以不同。它绑定原连接并走Try,不进入允许覆盖及跨连接保留的NORMAL队列。断线时尚未提交部分由应用放弃;如仍需报告设备变化,可独立用COT=3发布过程事件,不继续旧命令事务。
提交返回ERROR但原连接健康、仅因STOP通知待消费时让出;其他ERROR/FATAL终结为SEND_ERROR并清理待交应答,FATAL不可重试。Try必须等已有HIGH/NORMAL及当前帧让出,持续积压时不承诺有界完成。全部等待时调度不能空转。
## 6. FINISHED、引用和退出
事务级“曾交付工作事件”与当前事件已读状态分开保存。曾取走SELECT或EXECUTE的事务恰好产生一次FINISHED;从未取走任何工作事件就结束的事务不产生FINISHED。此规则也适用于直接执行尚未读就撤销,以及已读SELECT但后续EXECUTE未读时撤销/超时。
成功和执行失败在最终应答交接后交付FINISHED;其他终结原因立即交付,不等待否定或DEACT_CON。终结原因一旦确定不被后续STOP/Cancel改写,清理不重复通知。FINISHED保存最后一个接纳的工作请求和副作用标志,不被撤销或不匹配请求的回显覆盖。
事务只有FINISHED已读(或无需交付)且所有所属应答已交接/取消后才释放。释放前实例仍忙。独立应答池也属于实例生命周期:Cancel/STOP/发送失败清理它,Destroy要求事务和整个回复池均空且不在动作回调中。
事务创建时Acquire原站点,释放时Release。应用首次工作事件需要跨循环保留context或启动异步输出时,另行Acquire;在FINISHED清理且设备实际空闲后Release,不能等模块释放引用后再申请。站点删除阻止新请求,已有事务持原注册编号完成;同CA新节点不接续旧选择。跨协议结束仍在运行的输出由应用继续维护忙状态。
退出顺序:Command_Cancel,读空FINISHED并清理应用引用,Slave_Close,读空并分派Slave通知,Command_Destroy,移除站点,Slave_Destroy。动作回调只允许Get_Status/Get_Transaction,不得重入修改入口、推进网络或发布上报。
## 7. 布局、固定存储与验证
私有`COMMAND_LAYOUTS(X)`是唯一类型布局清单,记录type_id、value_length、has_se、has_time。X-macro分别展开只读布局表和字节数组union,规范长度按“9 + 值长度 + S/E字节数 + CP56长度”生成;`COMMAND_MAX_LENGTH`取该union的sizeof,目前为21字节,并静态检查不超过104的249字节ASDU上限。这是本模块14种控制命令的容量上界,不代表所有ASDU或其他业务的存储模型。
事务request_bytes、reply_bytes、独立回复槽及提交局部缓冲统一使用该容量,持久记录另存有效长度。复制、解析、包装和发送只使用规范有效长度,不能把sizeof(buffer)当作报文长度。原工作请求与撤销/失配回显分别保存,长短槽复用不会发送旧尾部。
S/E偏移只由`Se_Offset`计算,读取只能在已确认has_se的`Is_Select`或`Same_Command`中发生。51没有该字节,64相同偏移是CP56首字节,不能先读再判断。`Decode_Element`调用公共层公开的类型解码接口,不依赖内部信息对象布局表。布局、阶段支持掩码和实例type_mask分别表达“认识”“允许启用”和“本实例启用”,互不替代。
`IEC104_COMMAND_CAPACITY`默认跟随TCP服务容量;每实例一个事务槽,`IEC104_COMMAND_REPLY_CAPACITY`默认4。事务必要应答不依赖回复池剩余容量。池满只拒绝/丢新,不覆盖活动事务,不主动断开连接。Process每轮推进次数有界。实际sizeof及配置矩阵结果统一记录在[测试说明](../test/IEC104测试文档.md),不把宿主机结果当作其他目标的预算。
示例地址、动作与返回信息见[子站示例](../examples/IEC104子站示例.md)。45/58命令6001..6008对应Type1遥信1001..1008;46/59命令6101..6108对应Type3位置1101..1108,双点初值均为1;49/62命令6301..6308写入Type11遥测2001..2008。总召及对应分组逐批读取应用当前值,反映执行后的状态;点表、限值及分组属于应用,不改变Command模块职责。
验证覆盖TCP容量1/2与回复容量1/4、独立手写的14种类型长度、值与时标校验、匹配、变长FIFO、期限边界、动作只调用一次、通知优先、部分写入、结果先于确认、撤销及站点引用,并使用真实本机TCP核对遥控、设定值、带时标执行和过期丢弃。测试期望不共用实现布局清单,避免表错时输入和期望一起错。决定窗口内的同点激活丢弃不依赖内容相同,也不提供窗口外去重;事务释放后,相同ASDU按新请求处理,允许直接执行时仍可能再次输出。
扩展仍须保持事件身份、引用与交接契约;公开Request、Event、Config、Status新增字段后,库及调用方须一起重新编译,不承诺二进制布局兼容。新增类型需同步清单、解码分支和独立测试;新值类型或掩码范围外类型还涉及公开接口。容量会随清单最大长度增长,须重新测量目标内存;这里不提供跨业务统一存储管理,也不实现并发事务、Type70/102/105/107或突发上送。
## 8. 带时标激活与时间基准
应用调用`IEC104_Command_Set_Time_Base(self, utc_ms, base_ms)`提供同一时刻的UTC纪元毫秒和Net单调毫秒。句柄无效、动作回调内调用、UTC超出公共时间工具范围或base_ms晚于当前单调时间时返回false;失败不替换原基准。`Clear_Time_Base`在本站时间不可信时清除基准,动作回调内调用不生效。模块不读取Clock,不保存时间回调;STOP和断线保留基准,更新只影响随后受理的请求。
判定仅针对58–64的COT=6激活,在Handle_Event受理时做一次。公共CP56格式错误已经按请求校验回否定;格式合法后先检查IV及UTC换算,随后才应用max=`time_tag_max_delay_ms`:
| 条件 | 处理与计数 |
|---|---|
| IV=1或UTC换算失败 | 否定7,time_rejected与rejected加一;max=0仍拒绝 |
| max=0 | 不比较时差,不要求时间基准 |
| max非零且无基准、now早于base_ms、推算溢出或超出时间范围 | 否定7,time_rejected与rejected加一;单调时钟倒退另计clock_errors |
| 站内UTC − 命令UTC ≥ max | time_expired加一;默认静默丢弃,negative_on_expired=true时回否定7并计rejected |
| 命令UTC − 站内UTC ≥ max | 否定7,time_rejected与rejected加一 |
| 其他 | 继续匹配选择、忙检查或创建事务 |
站内UTC由`utc_ms + (now - base_ms)`推算,先检查回退和加法范围。CP56按master_utc_offset_minutes换算,SU由公共工具处理,并加回毫秒余数。静默过期不计rejected或dropped_requests,不占事务槽、不生成工作事件或FINISHED;time_base_valid仅表示持有基准,并非每次查询都重新验证其时效。
clock_errors按发现异常的检查次数累计,事务期限检查和时间基准检查可能在同一请求中分别增加,不能把它当作异常请求数量。
执行请求过期时不改变已有选择,它继续保留至select_timeout_ms。撤销不做时差、IV或UTC换算判定,但仍检查CP56编码格式。决定期间同点激活先按原规则丢弃,不增加时间计数。默认静默丢弃避免迟到否定被主站误认为重试结果;max、IV策略、时区、过期响应和ACT_TERM应与主站互操作约定一致。
时效只在受理时检查,Accept前不再检查命令时标;受理后的等待由决定期限控制。默认10秒时差窗口加15秒决定期限,动作启动时命令可能已接近25秒旧。需要更严格限制时,应用可在动作中检查request.time_utc_ms并返回NOT_STARTED;这不是模块承诺的动作时效上限。
@@ -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,534 @@
# 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信息对象模块)。
> **业务同步**:2026-10-31。总召、计数量、Type103校时及Command业务已实现;Command统一处理Type45–51及58–64的遥控、遥调和带时标命令,默认仍只启用45/46。采用单事务、显式动作及返回信息提交,设备策略和物理输出由应用提供,见[遥控设计](IEC104_Command_设计架构.md)。
> **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后立即回报增量;部分发送下轮续传,业务不再直接调用底层完成接口 |
| 总召、计数量召唤与Type 103校时 | 独立业务模块(已实现) | 通过Slave事件及共用站点表分派,总召/计数量使用业务事件;校时由Execute_Next显式执行,确认回显原时标,见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md) |
| 单点、双点遥控(Type45/46) | IEC104_Command(已实现) | 独立静态池、单事务互斥;管理选择执行、撤销、三类期限、原文确认及按事务提交返回信息,见[遥控设计](IEC104_Command_设计架构.md) |
| 遥调及带时标命令(47..51、58..64) | IEC104_Command(已实现) | 与45/46共用事务和FIFO;按类型解析值与限定词,51/64直接执行;应用提供时间基准,受理带时标激活时检查时效 |
| 实际遥信/遥测数据源、输出及设备设时 | 应用接入层 | 按业务模块请求读取点表或执行设备动作;示例提供模拟点表、遥控输出和模拟时钟,协议库不自动操作硬件 |
### 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(对象及四类业务已实现) | 公共对象编解码、总召/计数量、校时及Type45/46遥控;继续接入真实数据源和设备输出 | 验证地址路由、召唤进度、校时原文确认、遥控选择执行/撤销/期限及返回信息顺序;模拟模式不设系统时间,硬件设时、实际输出和主站联调须另行验收 |
| 5(已实现) | 遥调47..51及带时标命令58..64 | 私有布局统一推导长度与容量,公共解析器校验值和CP56,按值及QU/QL匹配;时间基准与过期策略由配置和应用接入,实际主站互操作仍须联调 |
---
@@ -0,0 +1,444 @@
# IEC104 链路模块
当前子站业务统一包含`IEC104_Slave.h`:Slave内部持有不透明TcpServer,提供生命周期、事件循环、主动上报和状态查询,并在NextEvent交付I帧时自动建立ASDU视图及原连接的共用应答接口。业务不再手动创建TcpServer、绑定generation或安装函数指针。入口和延迟任务示例见[子站统一入口](#子站统一入口)。
Slave提供[共用站点表](IEC104_API接口汇总.md#station-registry):应用提供固定地址节点,动态注册多个CA和业务context,重复CA拒绝。删除立即退出路由,已有引用和遍历结束后才回收;Is_Reusable为true后应用才能释放存储。总召设置config.use_station_registry=true后按请求CA选站;多目标广播配置每站状态区,以一个任务执行“全部确认、逐站数据、全部终止”。其他业务可复用相同的查找、引用及快照遍历接口。
总召由独立库模块[IEC104_Interrogation.c/h](../../104/business/IEC104_Interrogation.h)实现,支持QOI=20站总召、21..36分组总召及多类型无时标监视数据,负责批次校验、发送等待、主站撤销、业务超时及停用/断线取消。应用通过BEGIN/NEED_DATA/FINISHED事件和显式应答管理IOA、分组、遍历、逐批取值及终结;[子站示例](../../examples/iec104_slave_example.c)提供模拟点表、事件分派和main,构建运行见[示例说明](../examples/IEC104子站示例.md)。
内部调用关系为`Slave → TcpServer → Connection → SendManager/Receiver/Link`,网络I/O仍经Net执行,当前提供Windows后端。现有单主站、静态内存、HIGH/NORMAL队列、事件FIFO、Prepare/Process或Poll调度策略保持不变。共用ASDU及Slave发送接口继续使用IEC60870前缀;公共层不依赖104传输实现。下文的底层接口说明供维护和独立TCP接入使用,业务默认通过Slave操作服务。
`Get_Status`同时提供`high_queue`和`normal_queue`的待发条目数、保留原文条目数及总容量/已用/空闲字节数,先检查`asdu_queue_status_valid`。待发数不包含已交给发送模块的ASDU;NORMAL保留到ACK前的原文仍计入存储量,字节统计包含条目头。Init成功后离线也可查询,Close后统计无效;队列为空不代表发送或确认全部完成。公开状态结构已扩展,库与调用方须一起重新编译,字段含义见[ASDU队列积压查询](IEC104_TcpServer_设计架构.md#21-asdu队列积压查询)。
`NextEvent`区分连接建立(CONNECTED)、数据传输激活(ACTIVATED)、停用(DEACTIVATED)和连接断开(DISCONNECTED),四者共用`event.transport.data.connection`快照及对端地址。启停事件在对应ACT成功受理并提交状态变化后产生,CON可能仍待发送;同状态重复ACT不重复通知,直接断开不补造停用事件。业务可按事件类型处理启停,无需通过查询状态比较变化。
`Get_Status`的`pending_state_event_count`报告尚未消费的ACTIVATED/DEACTIVATED数量,不能追溯已消费的启停历史。当前请求的错误应答可在同步分派中调用`IEC104_Slave_Reject_Request`,支持COT=44..47,复用HIGH且不改输入事件;不新增示例应答队列。调用时机、WAIT丢新策略与FATAL不可重试约定见[同步拒绝当前请求](IEC104_API接口汇总.md#reject-request)。
发送队列NORMAL与`NextEvent`业务通知FIFO是两份独立存储:NORMAL满时可覆盖旧ASDU原文;已入队业务通知逐条保留,不覆盖、不合并启停变化。通知按有效载荷长度存放于字节环,配置宏仍表示保证条数:默认普通30条最长通知、启停4条及收尾32条记录加1个最长I帧和1个断开通知。当前x64 ABI下环占11243字节,接收暂存默认2048字节,两块合计13291字节,比原来减少43.9%;16字节APDU可在普通区缓存185条。普通通知不能占用启停及收尾预留;启停配额满时暂缓受理后续改变状态的ACT,已有CON继续发送。详细公式见[TCP服务设计](IEC104_TcpServer_设计架构.md)。
新I帧无普通空间可交付时保留原帧并暂停续收,`Prepare`及`Get_Status`的`receive_paused`为1;NextEvent腾出空间后安排Process恢复,等待中的I帧先于回执交付。已确认回执因普通空间或本轮预算不足暂留内部时,可能晚于随后发生的启停通知交付,保持不因回执积压而暂缓启停CON的行为。通知积压不直接断连,但保留I帧会阻挡后续S/U;长期不消费仍可能触发原有协议超时。应用应在每轮Process/Poll后及时取出事件。此次存储调整保持公开API及事件结构不变。
`IEC104_TcpServer_Prepare(server, &info, error)`只读导出socket关注集合、最近协议等待上限及本地续跑标志;应用统一等待其他模块和TCP事件后,用`IEC104_TcpServer_Process(server, &ready, error)`有界推进,Process不执行等待。socket只借给事件循环注册,关闭/重连后的旧通知按注册token过滤;真实I/O与Connection调度仍由TCP内部处理。活动连接未EOF时入队成功、背压解除和预算耗尽后按need_process安排续跑,不能只等待新的socket边沿;离线或EOF后新增NORMAL只缓存,不因此触发空转。EOF后也停止为此前入队但尚未编号的NORMAL创建新I帧,将这些原文留给下一连接;当前已编号帧、已受理HIGH及控制帧继续按原规则收尾。接入约定见[TCP事件循环说明](IEC104_TcpServer_设计架构.md#31-接入应用自己的事件循环)。
`IEC104_TcpServer_Poll(server, max_wait_ms, error)`组合相同的准备和推进核心,中间至多等待一次:0只查询就绪,正数允许等待并按最近协议期限缩短。原poll_interval_ms配置项已移除,业务无需分别驱动内部收发和Tick。
当前实现支持控制方向提前发送:控制站可在ACT成功入队后预留命令I帧,并在ACT之后发送,不必等待CON;被控站成功受理STARTDT_ACT即进入STARTED/IDLE,开放I/S处理与发送准备,已生成的CON按FIFO先于随后准备的I帧和S确认发送。初始STOPPED/IDLE仍禁止I/S接收及新I帧发送。标准适用条件与连接组管理边界集中说明于 [链路设计第3.2节](IEC104_Link_设计架构.md#32-接收-i-帧)。
| 阅读入口 | 负责内容 |
|---|---|
| `IEC60870_Types.h` | 101/104共用的应用层参数、ASDU存储上限;不包含传输状态 |
| `IEC104_Types.h` | 104尺寸常量、标准104应用层初始化值、ASDU优先级及链路配置 |
| `IEC60870_Codec.c/h` | 101/104共用的参数校验、ASDU公共头解析及已知类型最低载荷长度检查 |
| `IEC104_Codec.c/h` | 104专用APCI检查、I/S/U帧编码和帧类型 |
| `IEC60870_Asdu.c/h` | 通用ASDU容器、布局校验、对象分派与原文应答;使用说明见下文“ASDU信息对象模块” |
| `IEC60870_Slave.c/h` | 101/104共用的主站连接接口,分派ASDU发送、激活确认和激活终止 |
| `IEC104_Slave.c/h` | 子站统一入口:TCP服务、ASDU/应答接口、原连接隔离及各业务共用的CA站点表 |
| `business/IEC104_Interrogation.c/h` | 共用召唤引擎:任务、广播、期限、事件、取消与发送等待;接入见[总召流程](IEC104_完整调用流程.md#flow-interrogation) |
| `business/IEC104_GeneralInterrogation.c/h` | Type100规则及兼容的Interrogation_Create入口 |
| `business/IEC104_CounterInterrogation.c/h` | Type101规则、创建及一次动作执行入口;见[计数量设计](IEC104_CounterInterrogation_设计架构.md) |
| `business/IEC104_ClockSynchronization.c/h` | Type103校时校验、显式动作及原文确认;见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md) |
| `business/IEC104_Command.c/h` | Type45–51/58–64遥控、遥调、选择执行、撤销、时标判定及返回信息;见[遥控设计](IEC104_Command_设计架构.md) |
| `IEC104_TcpTypes.h` | Slave与TcpServer共用的配置、调度、事件和状态值类型,无服务操作接口 |
| `IEC60870_InformationObjects.c/h` | 67种独立信息对象,覆盖监视、命令、初始化、参数和文件;编码通过只读函数表分派,支持变长文件段 |
| `IEC60870_Time.c/h` | 101/104共用时间工具:CP16/CP24/CP56Time2a编解码、本地标准时间转换、SU修正、日历/星期和纪元秒数工具;使用约定见[时间工具设计](../common/IEC60870_Time_设计架构.md) |
| `IEC60870_Clock.c/h` | 101/104共用时间源接口:直接读取和设置固定UTC+8日历;Clock.c内置Windows读写,Clock.h预留RTC接口,由编译选项二选一 |
| `IEC104_Receiver.c/h` | 每连接严格帧头检查、成帧、APCI及ASDU最低长度检查、完整APDU借用视图和显式释放;不处理链路序号 |
| `IEC104_Connection.c/h` | TCP内部连接协调:I/S/U分派、接收保留与释放、部分发送、超时推进及旧代次隔离;内部设计见[连接层设计](IEC104_Connection_设计架构.md) |
| `IEC104_TcpServer.c/h` | 底层TCP服务:IPv4单主站、静态池句柄、事件FIFO;Prepare/Process或Poll推进,Get_Remote_Address查询对端,Close_Connection按代次断开单连接 |
| `IEC104_TcpServer_Config.h` | 实现的编译期容量配置;默认一个服务器静态槽,可调整池及缓冲容量 |
| `IEC104_Net.h` / `IEC104_Net_Windows.c` | 通用网络接口及当前Windows实现:非阻塞监听/接入/收发、就绪等待、关闭及单调计时 |
| `IEC104.c/h` | 已恢复的旧单路逐字节接收入口,保留原有全局接收器及私有Link状态 |
| `IEC104_SendBuffer.c/h` | 唯一当前I帧、独立紧凑S/U队列、部分写入续传及本地发送期限 |
| `IEC104_AsduQueue.c/h` | 调用方字节缓冲的两级变长队列,HIGH满时拒绝、NORMAL覆盖最旧原文,支持显式重连恢复 |
| `IEC104_SendManager.c/h` | Connection内部的Link/SendBuffer修改入口:会话初始化、发送调度、控制收发、完成分派、ACK和回执,以及发送期限查询 |
| `IEC104_Link.c`、`IEC104_Link_Common.h` | 初始化、完整状态校验、I/S序号、k/w窗口、S确认、公共超时判定和TESTFR |
| `IEC104_Link_Master_StartStop.c/h` | 主站发STARTDT/STOPDT_ACT,收CON,处理等待超时 |
| `IEC104_Link_Slave_StartStop.c/h` | 子站收STARTDT/STOPDT_ACT,受理时更新启停状态,生成CON并处理停止排空及交错启停 |
| `IEC104_Link_Internal.h` | 三个实现文件共用的私有声明,不供业务调用 |
| `IEC104_Link.h` | 兼容总头文件,汇总公共、主站、子站接口 |
维护内部链路实现时,可按本端角色包含 `IEC104_Link_Master_StartStop.h` 或 `IEC104_Link_Slave_StartStop.h`,两者都包含公共接口。只处理I/S或TESTFR时包含 `IEC104_Link_Common.h`。旧内部代码可继续包含 `IEC104_Link.h`;模块发送完成接口统一以 `_Send` 结尾,该命名调整本身不改变参数和处理行为。改名不保留旧名兼容入口,内部调用方须同步更新名称并重新编译。
公共ASDU基础解析由 `IEC60870_Codec.c/h` 提供,业务需要解析公共头时包含 `IEC60870_Codec.h`;104的APCI检查和I/S/U编码由 `IEC104_Codec.c/h` 提供。Receiver分别调用两层:先检查APCI,再显式传入104参数调用 `IEC60870_Parse_ASDU_Header`。遇到非法帧头或截短的已知固定长度对象时返回FRAME错误,不推进序号或ACK,也不生成I_FRAME。未知/变长类型及具体字段继续由完整ASDU解析器和应用检查,边界见[基础设计](IEC104_设计架构.md#43-已知类型的最低载荷长度)。旧 `IEC104.c/h` 的全局接收入口保留原行为,不参与TcpServer路径。两个Codec均只依赖标准库及值类型,公共Codec不依赖104实现。
| 模块及构建依赖范围 | 生产源文件 |
|---|---|
| 101/104公共ASDU头解析 | `IEC60870_Codec.c` |
| 104 APCI与I/S/U编解码 | `IEC104_Codec.c` |
| ASDU信息对象编解码 | `IEC60870_Asdu.c`、`IEC60870_InformationObjects.c`、`IEC60870_Codec.c`、`IEC60870_Time.c`;C11及IEEE 754 binary32,不依赖TcpServer或Clock |
| 子站统一入口 | 在ASDU及TCP依赖之外增加`IEC60870_Slave.c`、`IEC104_Slave.c`;共用接口本身不依赖TCP或串口实现 |
| 104站/分组总召 | 在Slave及其依赖之外加入`104/business/IEC104_Interrogation.c`和`104/business/IEC104_GeneralInterrogation.c`,头文件搜索路径增加`104/business/`;应用按QOI选点表,响应NEED_DATA提交数据 |
| 104计数量召唤 | 引擎加`104/business/IEC104_CounterInterrogation.c`,头文件搜索路径增加`104/business/`;与总召分别创建,状态通知交付两边,动作通过Execute_Begin执行 |
| 104时钟同步 | Slave依赖加`104/business/IEC104_ClockSynchronization.c`,独立实例及静态池;只由Execute_Next执行同步action,模块不依赖Clock设备适配 |
| 104遥控/遥调 | Slave依赖加`104/business/IEC104_Command.c`,使用公共信息对象和Time解析;每Slave一个实例、一个事务,Accept显式执行动作,应用报告结果、提交返回信息并提供带时标命令的时间基准;模块不读取Clock |
| 101/104公共时间工具 | `IEC60870_Time.c`,对已有时间值进行日历校验、时间转换及编解码 |
| 101/104时间源:Windows系统时钟 | `IEC60870_Clock.c`、`IEC60870_Time.c`;`IEC60870_CLOCK_USE_RTC=0`(默认),Clock.c直接使用Windows的Kernel32接口 |
| 101/104时间源:RTC预留接口 | 上述两个源文件,定义`IEC60870_CLOCK_USE_RTC=1`,另需板级驱动实现`IEC60870_Clock_RTC_Read/Set`;具体芯片驱动暂不提供 |
| 独立子站发送缓冲 | `IEC104_Codec.c`、`IEC104_SendBuffer.c`;公共发送类型仍由Link公共头文件提供,无需链接Link实现 |
| 链路处理 | `IEC104_Codec.c`、`IEC104_Link.c`,再按所需启停接口加入主站/子站实现 |
| 子站发送管理 | `IEC104_SendManager.c`、`IEC104_AsduQueue.c`、`IEC104_SendBuffer.c`、`IEC104_Codec.c`、`IEC104_Link.c`、`IEC104_Link_Slave_StartStop.c` |
| 子站连接协调 | 上述子站发送管理源文件,加 `IEC104_Receiver.c`、`IEC104_Connection.c` 和 `IEC60870_Codec.c`;无需主站启停或日志实现 |
| Windows TCP子站模块 | 上述子站连接协调源文件,加 `IEC104_TcpServer.c`和`IEC104_Net_Windows.c`,应用工程链接`Ws2_32`;构建与接入见[TCP服务设计](IEC104_TcpServer_设计架构.md) |
| 独立流式接收 | `IEC104_Receiver.c`、`IEC104_Codec.c`、`IEC60870_Codec.c`;不依赖Link、SendManager或日志 |
| 旧单路逐字节接收 | `IEC104.c`、`IEC104_Codec.c`、`IEC104_Link.c`、`IEC104_Log.c` |
时间工具以`IEC60870_Time`作为公共入口,供101和104的ASDU业务层及平台接入层按需调用,不在Connection中自动执行校时。`IEC60870_InformationObjects.c`复用CP56Time2a处理带时标信息对象和命令;Type 103校验及确认使用独立ClockSynchronization模块,实际设时由应用显式执行。**新生成业务时标使用本地标准时间日历并设置SU=0;Type 103确认回显原CP56及SU。** 公共编解码继续支持SU=0和1,按输入保留标志,不强制或自动清零。生成新时标前须保证日历本身已是本地标准时间,不能只清除夏令时标志而保留原小时值。
时间源提供UTC纪元秒数时,直接调用`IEC60870_Time_Seconds_To_Local(utc_seconds, ms, &local_time)`即可取得固定UTC+8的本地年月日、时分秒、毫秒和星期,调用前后都无需手动加八小时。接口内部统一处理跨日/月/年及范围检查;原始UTC秒数上限为4102415999,超过时返回0并保留输出。RTC已直接提供本地标准时间日历时,可直接构造CP56解析值并编码。
当前时间直接通过`IEC60870_Clock_Read(&value)`取得,设置实际时钟调用`IEC60870_Clock_Set(&local_time)`,均使用固定UTC+8日历。**时间源范围仅限Windows系统时钟和RTC;`IEC60870_CLOCK_USE_RTC=0`默认使用Clock.c内置的Windows实现,`=1`调用Clock.h声明的`IEC60870_Clock_RTC_Read/Set`接口。RTC具体芯片驱动暂不实现。** 编译选项须作用于`IEC60870_Clock.c`,不规划其他操作系统适配。公共入口无需时钟对象或绑定初始化;以后接RTC时,硬件仍按驱动要求初始化,已有本地标准时间日历可直接填入,无需转换成时间戳再还原。读取结果分别表示日历和`is_invalid`,读取成功不等于时间有效;失败不修改输出。RTC的BCD格式、时间基准、毫秒来源及掉电状态由具体驱动处理。接入示例见[统一时间源读写](../common/IEC60870_Time_设计架构.md#统一时间源读写),业务待接入项见[当前接入状态](../common/IEC60870_Time_设计架构.md#当前接入状态与待补齐项)。
时间戳转日期保留两种实现:Windows默认调用CRT的64位`_gmtime64_s`;编译`IEC60870_Time.c`时定义`IEC60870_TIME_USE_PLATFORM=0`即可使用原有整数算法,其他平台默认采用此配置。配置在编译时生效,业务接口、UTC含义及1970..2099年的范围一致。此选项只决定如何转换传入的时间戳,与是否接入RTC无关;应用使用RTC作为时间源时,仍需通过相应驱动读取时间,再按返回值的格式和时间基准调用转换或编码接口。详见[实现选择](../common/IEC60870_Time_设计架构.md#时间戳转日期的实现选择)。
业务接收按`From_CP56 → CP56_To_Local`调用:原始解码保留源日历及IV/SU,转换接口返回本地标准时间年月日、时分秒、毫秒和星期。双方使用本地标准时间基准时,源标准时区参数传`IEC60870_LOCAL_UTC_OFFSET_MINUTES`(480):SU=0保持原时间,SU=1减一小时并处理跨日/月/年,IV=1则转换失败。其他源时区须传入其标准偏移,输出仍为本地标准时间;调用方无需再加八小时或重复扣除夏令时。接收SU=1的兼容处理与本地发送SU=0的约定分别生效。
日历范围为1970..2099年,CP56Time2a采用2000..2099年的世纪约定;接口以1/0表示成功/失败,失败保留输出。通用纪元秒数接口保留UTC语义,不能将本地标准时间日历直接当UTC使用。RTC读写仍由平台实现,Link和Connection的`now_ms`继续使用单调计时,不受校时影响。当前提供CP16经过时间、CP24短时标、CP56及日历工具。CP24仅含分钟、分钟内毫秒及IV,不能单独恢复小时和日期,需由业务结合已有时间基准解释。完整范围、星期和保留位策略见[时间工具设计](../common/IEC60870_Time_设计架构.md)。
Connection内部持有独立 `IEC104_Receiver_t`,先用非零连接代次调用 `IEC104_Receiver_Init`,再调用 `Feed(receiver, generation, data, length, &consumed)`。Feed最多收齐一帧,TcpServer保留未消费的输入尾部;`FRAME_READY`后由Connection通过 `Peek_Frame`取得APDU和APCI并分派。S/U受理后释放,I帧在TcpServer成功复制为事件后经Connection释放。帧未释放时Feed消费0字节,不覆盖原文;`Reset`丢弃当前半帧或完整帧,但不改变连接代次或Link状态。内部接口和迁移说明见 [编解码与接收器设计](IEC104_设计架构.md)。
接收器校验APCI格式和ASDU最低长度。Connection将I/S帧交给同连接的SendManager处理序号及ACK,并按功能分派U帧;WAIT时保留接收帧。Slave内部通过TcpServer_NextEvent读取I_FRAME副本,无需释放底层帧。连接重建由TcpServer先结束旧I/O并Close,随即Detach队列回到离线状态;新连接以新代次Rebind队列并Init连接;旧代次事件返回ERROR。
I 帧本地发送结果通过 `IEC104_Link_Complete_I_Send()` 提交,`Send` 明确限定为本地发送,成功不表示对端已经确认。同一次发送结果只提交一次。具体契约见 [链路设计第3.4.4节](IEC104_Link_设计架构.md#344-complete_i_send-的本地发送完成提交)。
发送完成接口如下,表中省略统一前缀 `IEC104_Link_`。它们报告最终发送结果,包括失败;返回 OK 表示本地发送结果已成功提交,是否仍需等待对端确认由对应状态决定。
| 接口 | 对应的本地发送结果 |
|---|---|
| `Complete_I_Send` | I 帧 |
| `Complete_S_Ack_Send` | S 确认帧 |
| `Complete_Startdt_Act_Send` | 本端主动发送的 STARTDT_ACT |
| `Complete_Startdt_Con_Send` | 本端应答的 STARTDT_CON;完成不回写启停状态 |
| `Complete_Stopdt_Act_Send` | 本端主动发送的 STOPDT_ACT |
| `Complete_Stopdt_Con_Send` | 本端应答的 STOPDT_CON;完成不回写启停状态 |
| `Complete_Testfr_Act_Send` | 本端主动发送的 TESTFR_ACT |
`Prepare_*` 负责准备报文或推进准备流程,`Handle_*` 负责处理接收事件;二者名称保持原有职责划分。收到 TESTFR_CON 使用 `Handle_Testfr_Con()`,不能用本地发送完成替代对端确认。
主动 TESTFR 使用 `IDLE → ACT_SEND_PENDING → CON_PENDING → IDLE` 三态,与控制站主动发起的 STARTDT/STOPDT 一致:ACT 完整发送后先调用 Complete,再处理对应 CON。ACT 尚未 Complete 时处理到的合法 TESTFR_CON 返回 IGNORED,不缓存;只有 CON_PENDING 接受确认。被动 TESTFR_ACT 仍独立生成 CON,应答对端请求不结束本端探测。
完整状态校验集中在 `IEC104_Link.c`,主站、子站的状态组合规则由该文件内的 `static` 函数检查。角色实现通过 `IEC104_Link_Internal.h` 调用公共辅助函数,公共链路模块不再反向调用角色实现。构建时包含公共链路及基础编解码实现,再按实际使用的启停接口加入对应角色 `.c`;同时使用两端接口时加入两个角色实现。
I/S的启停准入和停止阶段立即确认规则集中在 `IEC104_Link.c` 的私有判断函数中;I/S处理与规划通过这些判断继续执行序号、窗口及确认检查。I帧接收、I帧预留、S帧接收和普通S确认分别判断,保留各自的方向与阶段差异。子站在ACT受理时提交启停状态,S确认与CON发送完成分别维护各自的完成事实。具体规则见 [I/S与启停的内部边界](IEC104_Link_设计架构.md#16-is与启停的内部边界)。
启停和TESTFR在对应入口中直接检查状态、编码并提交;I/S帧保留序号规划。内部确认提交复用公开入口的状态校验,STOPDT直接返回各等待原因,不再经过私有计划对象和结果转换。
## ASDU信息对象模块
信息对象、ASDU容器、公共头解析及参数类型统一使用 `IEC60870_` 前缀,供101/104共用。旧信息对象/ASDU文件和符号直接迁移,不提供兼容头、宏或类型别名;调用方须更新包含路径、函数/类型名称和构建源文件并重新编译。104的APCI、链路、TCP及发送队列仍使用 `IEC104_` 前缀。
公共层仅需 `IEC60870_Asdu.c`、`IEC60870_InformationObjects.c`、`IEC60870_Codec.c`、`IEC60870_Time.c`;头文件依赖也不包含任何104模块。101接入提供约定的字段宽度和容量,104接入提供标准2/2/3及249字节上限。两种接入共用同一套对象构造和解析接口。
业务包含`IEC104_Slave.h`,取得`Slave_NextEvent`交付的I帧后可直接使用ASDU视图并解析具体对象,或先构造ASDU再交给发送接口。ASDU编解码层不分配内存、不操作连接、不读取或设置系统时钟;共用连接接口可构造并提交确认/终止应答。站/分组总召可交给IEC104_Interrogation管理校验、分批、撤销、期限和重试,应用响应业务事件提交数据,自行维护IOA、分组及遍历状态;Type45–51/58–64由IEC104_Command管理,Type103由独立校时模块管理,权限策略及设备动作仍由应用接入。
| 层次 | 职责 |
|---|---|
| `IEC60870_Codec.c/h` | 101/104共用的参数校验、ASDU公共头解析及已知类型最低载荷长度检查 |
| `IEC104_Codec.c/h` | 104专用APCI检查、I/S/U帧编码和帧类型 |
| `IEC60870_Asdu.c/h` | ASDU创建/添加/解析/取对象,维护Type ID、VSQ/SQ、IOA布局和容量,构造原文应答 |
| `IEC60870_InformationObjects.c/h` | 每种类型独立结构、Create和Get_From_Buffer;通过对象函数表编码,按Type ID选择具体解析器 |
| `IEC60870_Time.c/h` | CP16经过时间、CP24短时标及CP56日历的编解码、范围和保留位校验 |
| 能力 | 当前范围 |
|---|---|
| 监视对象编解码 | 1–21、30–40,共32种;含步位置、比特串、成组单点、CP24及保护事件;支持SQ=0/1 |
| 命令对象编解码 | 45–51、58–64、100–107,共22种;SQ=0、一个对象、精确长度 |
| 初始化结束 | 70;COI保留原因与本地参数改变标志;SQ=0、一个对象 |
| 参数对象编解码 | 110–113;QPM/QPA按独立限定词处理;允许多个对象但SQ必须为0 |
| 文件对象编解码 | 120–127;含变长文件段125与归档查询127;126文件目录支持多个对象及SQ,其余只允许一个非SQ对象 |
| 应答构造 | COT=7激活确认、9停止激活确认、10激活终止和44–47错误应答;P/N显式传入,否定激活确认使用7和1 |
上述共67种类型都有独立结构、构造和解析接口。安全扩展、厂家私有类型不在当前范围内。本层提供无状态报文编解码;站总召状态机由IEC104_Interrogation提供,参数生效、文件传输、文件读写及归档检索仍由业务接入。类型编号可在公共对象头文件的`IEC60870_AsduType_t`中查到。
应用层参数`IEC60870_AppLayerParameters_t`定义在`IEC60870_Types.h`:
| 字段 | 合法范围及含义 |
|---|---|
| `sizeOfTypeId`、`sizeOfVSQ` | 固定为1 |
| `sizeOfCOT` | 1或2;1字节时省略OA,解析OA为0 |
| `originatorAddress` | 0..255,供业务作为默认OA;构造以header中的显式OA为准 |
| `sizeOfCA` | 1或2;1字节配置拒绝大于255的公共地址 |
| `sizeOfIOA` | 1、2或3;IOA及SQ连续地址上限随宽度变化 |
| `maxSizeOfASDU` | 至少容纳公共头、最多254字节;104传输不得超过249字节 |
标准104包含 `IEC104_Types.h` 后使用`IEC104_APP_LAYER_PARAMETERS_DEFAULT`,即`{1,1,2,0,2,3,249}`。所有ASDU创建/解析入口必须显式传入非空参数。容器和解析视图借用参数,使用期间参数必须有效且不变。通用ASDU可复用到采用其他宽度的101接入,但本工程尚未提供101串口链路;Receiver、APCI、TcpServer和发送队列继续使用104布局及249字节上限。
每类对象通过自己的`Create`写入调用方提供的结构体,并绑定类型及只读编码函数表。例如单点用`IEC60870_SinglePointInformation_t`,双命令用`IEC60870_DoubleCommand_t`。普通监视和控制的带CP24/CP56对象用`base`保存基础对象、`time`保存时标,并复用基础值处理;保护事件则显式保存事件、QDP、CP16经过时间和时标。对象可按值复制,不需要销毁;业务可修改值或IOA,重新编码时仍会校验,类型和函数表不得手工修改。
容器调用顺序为`IEC60870_Asdu_Create → IEC60870_Asdu_Add_InformationObject → IEC60870_Asdu_Get_Data`。Create固定参数、SQ和公共头,首个成功添加的对象确定Type ID;后续对象必须同类型。SQ=1要求IOA连续且不越过所选宽度上限,SQ=0逐对象携带地址。Add成功后已经复制对象字节,可以立即复用对象存储。
容量取实际缓冲区与`maxSizeOfASDU`的较小值。空间不足、对象达到127个或单对象类型已加入一个对象时返回`IEC60870_ASDU_FULL`;业务提交当前非空ASDU后另起一条重试。空容器放不下首对象时须调整容量;文件数据须按可用长度分成更小的段。类型不匹配、SQ地址不连续或字段非法返回ERROR。所有失败保留容器、原文和输出参数,首对象失败不会提前设置Type ID。结果必须显式比较`IEC60870_ASDU_OK`,不可按布尔值判断。
业务包含 `IEC60870_Asdu.h`;本例使用104默认配置,还需包含 `IEC104_Types.h`。以下代码放入业务函数,`data`在原缓冲区未修改期间有效:
```c
uint8_t buffer[IEC104_MAX_ASDU_LEN];
const IEC60870_AppLayerParameters_t parameters = IEC104_APP_LAYER_PARAMETERS_DEFAULT;
IEC60870_Asdu_t asdu;
IEC60870_AsduHeader_t header = {
.cause_of_transmission = IEC60870_ASDU_COT_SPONTANEOUS,
.common_address = 1
};
IEC60870_SinglePointInformation_t point;
const uint8_t *data = NULL;
uint16_t length = 0;
IEC60870_AsduResult_t result =
IEC60870_SinglePointInformation_Create(&point, 100, 1, 0);
if (result == IEC60870_ASDU_OK) {
result = IEC60870_Asdu_Create(
&asdu, &parameters, buffer, sizeof(buffer), &header, 0);
}
if (result == IEC60870_ASDU_OK) {
result = IEC60870_Asdu_Add_InformationObject(&asdu, &point.object);
}
if (result == IEC60870_ASDU_OK) {
result = IEC60870_Asdu_Get_Data(&asdu, &data, &length);
}
/* OK时可将data/length交给Enqueue_Event;提交返回WAIT时保留原文重试。 */
```
监视和命令统一使用`IEC60870_Asdu_Parse`校验整条报文,再通过`IEC60870_Asdu_Get_Element`取得独立类型的对象。例如处理单命令:
```c
/* parameters、data、length对应收到的ASDU;处理期间原文保持有效。 */
IEC60870_AsduInfo_t info;
IEC60870_AsduResult_t status =
IEC60870_Asdu_Parse(&parameters, data, length, &info);
if (status == IEC60870_ASDU_OK && info.type_id == IEC60870_ASDU_C_SC_NA_1) {
IEC60870_SingleCommand_t command;
status = IEC60870_Asdu_Get_Element(&info, 0, &command, sizeof(command));
if (status == IEC60870_ASDU_OK) {
/* info保存COT/CA/OA;command.object.ioa、state、is_select、
* qualifier供业务判断地址、限定词、选择/执行及权限。 */
}
}
```
Get_Element输出无需预先Create,必须按`info.type_id`选择匹配的具体结构体并传入其尺寸;尺寸检查不能替代C类型匹配。解析视图借用原文和参数,取出的具体对象按值保存。只处理单个载荷时,可直接调用对应类型的`Get_From_Buffer`,传入已解析IOA及不含IOA的精确载荷;读命令载荷为0字节,允许NULL。
归一化值使用原始`int16_t`(实际值为原值/32768),标度值使用`int16_t`,短浮点保留IEEE 754 binary32位模式。累计量用`IEC60870_BinaryCounterReading_t`保存32位计数、5位序号及CY/CA/IV。对象层校验品质保留位、SCO、DCO/RCO状态、VTI有符号范围、保护事件QDP和CP24/CP56;COT、CA、IOA及限定词的业务适用性由应用判断。
`Build_Response(&parameters, request, length, cause, is_negative, out, capacity, &out_length)`仍以`IEC60870_Asdu_`为前缀。它复制请求,只替换COT/P/N,保留TEST、存在时的OA、CA、VSQ及全部对象原文,支持原地和部分重叠。它仅作公共头及最低长度检查,可回显未知类型。生成后携带原请求generation调用Send_Response,并处理WAIT/ERROR/FATAL。
`IEC60870_Asdu_Create_From_Buffer(&asdu, &parameters, buffer, length)`直接借用可写原文,建立可修改的ASDU对象。它只做公共头及最低长度检查,未知类型也能建立对象,用于COT=44等错误应答;执行业务前仍须使用完整Parse和业务规则校验。借用对象不能Add,`Set_COT`、`Set_Negative`、`Set_CA`原地修改相应字段;Get_Data可以导出原文。Create创建的构造容器也可使用这三个设置接口,空构造容器仍不能导出。Set_CA按COT宽度定位、CA宽度写入,1字节仅允许0..255,2字节允许0..65535;非法容器或地址溢出返回ERROR且不修改原文,业务地址限制仍由业务层判断。ASDU结构新增来源标志,调用方须一起重新编译。
### 文件段和新增时间格式
`IEC60870_FileSegment_t`含255字节的数据数组。Create和Get_From_Buffer把有效数据复制进对象,调用结束后即可复用输入;对象无需销毁。它的线上载荷为`NOF(2)+NOS(1)+LOS(1)+数据(LOS)`,解析精确检查LOS与报文长度,不接受截短和多余尾字节。
先用`IEC60870_FileSegment_Get_Max_Data_Size(parameters)`计算当前配置允许的数据长度。默认104配置返回236;最短101头、1字节IOA、254字节上限时返回245。返回0表示只能容纳空段,-1表示配置非法或连空段都装不下。Create允许0..255字节,实际加入ASDU时仍检查总长;超过容量返回FULL,调用方负责分段。
`IEC60870_Time_To_CP16/From_CP16`处理0..65535毫秒的经过时间。`IEC60870_Time_To_CP24/From_CP24`处理分钟0..59、分钟内毫秒0..59999和IV;bit6是保留位,CP24没有SU、小时或日期。CP24编解码保留IV,不自动推断完整日历。所有接口均沿用成功1、失败0且保留输出的约定。
### 子站统一入口
业务包含`IEC104_Slave.h`,持有一个不透明的`IEC104_Slave_t *`。Slave内部创建、驱动和销毁TcpServer;配置、调度、状态和结果仍复用`IEC104_TcpTypes.h`中的值类型,业务头文件不引入TcpServer操作接口或内部布局。
生命周期为`IEC104_Slave_Create → Config_Default/Init → Prepare/Process或Poll → Close → NextEvent读空 → Destroy`。所有操作在同一事件循环线程串行执行。静态池容量沿用`IEC104_TCP_SERVER_CAPACITY`,与直接使用TcpServer共享底层名额,无动态分配。创建失败返回NULL;其他生命周期接口显式比较`IEC104_TCP_OK`。
`IEC104_Slave_NextEvent(slave, &event)`按原FIFO交付`IEC104_SlaveEvent_t`:`event.transport`是TCP事件副本;I_FRAME且`event.asdu_result == IEC60870_ASDU_OK`时,`event.asdu`已经借用事件自身的ASDU字节,`event.connection`已经绑定原服务实例及原连接代次。业务不再传入server/generation,也不再手工Create_From_Buffer。字段/地址/命令权限仍由业务校验,未知类型可以原文交付,视图创建成功不等于允许执行命令。
普通循环使用以下顺序;Application_Handle_Event为应用自己的同步处理函数,其中可以提交应答或复制事件到延迟任务:
```c
IEC104_TcpResult_t result = IEC104_Slave_Poll(slave, 10, NULL);
if (result == IEC104_TCP_OK) {
IEC104_SlaveEvent_t event;
IEC104_TcpEventResult_t next;
while ((next = IEC104_Slave_NextEvent(slave, &event)) == IEC104_TCP_EVENT_READY) {
Application_Handle_Event(slave, &event);
}
/* next为ERROR时处理内部读取错误;EMPTY表示本轮事件已读空。 */
}
```
外部事件循环使用Slave_Prepare/Process,继续保留原service_token和socket token;pending_event_count或need_process非零时先处理本地工作。主动上报通过`IEC104_Slave_Get_Application_Layer_Parameters`取得参数构造ASDU,再调用`IEC104_Slave_Enqueue_Event(slave, &asdu)`进入NORMAL。关闭单连接传原事件给`IEC104_Slave_Close_Connection(slave, &event, error)`,库内部使用该事件的原代次。
### 共用连接上的激活确认和激活终止
共用接口`IEC60870_MasterConnection_t`继续供101/104业务复用;当前104的接口由Slave_NextEvent自动安装,101串口后端仍未提供。下例放在业务已验证请求并决定肯定确认的分支,event为地址稳定的Slave事件:
```c
if (event.transport.type == IEC104_TCP_EVENT_I_FRAME &&
event.asdu_result == IEC60870_ASDU_OK) {
bool accepted = IEC60870_MasterConnection_Send_ACT_CON(
&event.connection, &event.asdu, false);
if (!accepted) {
IEC104_TcpSubmitResult_t submit =
IEC104_Slave_Get_Last_Submit_Result(&event.connection);
/* 按下表处理submit;WAIT需要保留事件及原文,不能忽略或盲目重发。 */
(void)submit;
}
}
```
| 共用操作 | 行为 |
|---|---|
| Send_ASDU | 提交给定ASDU对象的当前原文,也可提交业务另外构造的数据ASDU |
| Send_ACT_CON | 原地设置COT=7及指定P/N,然后提交 |
| Send_ACT_TERM | 原地设置COT=10、P/N=0,然后提交 |
| Get_Application_Layer_Parameters | 返回该连接的标准104只读参数,最大249字节 |
确认及终止先修改事件副本的字节再发送,失败也不回滚;TEST/OA/CA/VSQ和对象原文保留。需要原请求内容时,在修改前保存副本。
| 详细结果 | 布尔返回 | 业务处理 |
|---|---|---|
| OK | true | 已复制到发送缓冲或HIGH,可复用发送原文;不代表已写完或已ACK,不重复提交 |
| WAIT | false | 本次未接手;保留已修改的ASDU及原事件,待容量恢复后重试 |
| ERROR | false | 参数、布局、旧代次或调用阶段非法,不能按容量等待盲目重试 |
| FATAL | false | 发送路径故障,可能已写出字节;结合RECORD/DISCONNECTED处理 |
事件含有指向自身的ASDU视图和连接上下文,不能用赋值或memcpy搬到另一地址。延迟任务可让NextEvent直接写入自身稳定存储,或调用`IEC104_Slave_Copy_Event(&task.event, &event)`;复制时同时保存字节、重建自引用,并保留原服务实例和原连接代次。源事件随后可被下一次NextEvent覆盖,任务副本仍独立可用;复制不代表需要重发已经OK的报文。多个任务的事件存储由应用提供,库不额外引入待办任务池。
总召内部使用紧凑存储:只保留`Get_Event_Identity`取得的原身份及10字节请求ASDU,按需建立视图,通过`Slave_Send_Response / Slave_Try_Send_Response`应答,关闭原连接用`Slave_Close_Connection_By_Identity`。任务槽、事件队列和发送容量保持不变,内存测量见[固定存储](IEC104_Interrogation_设计架构.md#2-对象身份与固定存储)。其他需要完整事件的业务仍可用Copy_Event。
NextEvent返回EMPTY/ERROR时保持输出不变。Close、重连、重新Init或Destroy后复用服务槽,都不会把旧任务改绑到新连接;发送继续检查原身份。Destroy成功后旧服务句柄失效,但旧事件的字节仍由任务自身保存。所有延迟任务也在所属事件循环串行提交。
多帧总召的数据和终止应经同一事件的connection依次提交,逐帧处理WAIT;把数据放NORMAL而终止放HIGH会造成优先级越序。门面不实现点表、总召状态机、遥控选择/执行或系统校时。
[子站示例](../../examples/iec104_slave_example.c)通过BEGIN/NEED_DATA/FINISHED事件与显式应答接入独立的[总召模块](../../104/business/IEC104_Interrogation.h)推进业务;Slave和TcpServer继续负责服务、连接及传输。总召接入顺序见[完整调用流程](IEC104_完整调用流程.md#flow-interrogation)。
调用`IEC104_Interrogation_Process(gi)`自动推进当前就绪任务,无需传入预算。每个实例至多一个未终结总召,包含等待生成BEGIN的请求;忙时相同和不同QOI的新激活均否定,不保存后续总召。4个任务槽共用于这个总召和待发短应答。Handle_Event在原会话有效、Slave通知已读空且无更早REPLY时尝试立即提交短应答,未接手再保存。满槽时尝试否定确认;不能安全提交/保存则仅丢新请求并累计dropped_requests,保留当前任务及TCP。Process在空闲、等待应用/事件空位、发送WAIT或Slave通知未读空时返回。对象、状态、调度及接入约定见[总召模块设计](IEC104_Interrogation_设计架构.md)。
总召Get_Status的`need_process`用于决定是否立即续跑,不能从`waiting`取反推断。应答或读取业务事件后重新查询;发送WAIT后等待网络推进再重试。每次Slave_Poll/Process及Slave通知处理结束仍须推进总召,即使need_process为false。外部事件循环同时合并Slave_Prepare的调度条件;新增状态字段后调用方须重新编译。
BEGIN由调用者判断是否接受,遍历状态可延至首个NEED_DATA建立;NEED_DATA由调用者选择本批IOA并读取当前值和品质,协议库不登记或冻结点表。Submit_Batch立即尝试不排队提交,WAIT不保存数据、不推进游标,应用在网络推进后重新取当前值重试;OK才固定帧顺序并推进批次。点值更新及突发发布须与总召读值及提交串行化,持续突发可能推迟总召,详见[数据批次与发送路径](IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。
BEGIN携带实际CA、station_id和QOI=20..36;应用检查数据源并决定接受,首个NEED_DATA才需建立取数游标。多目标广播先完成全部BEGIN/ACT_CON,再逐站取数,最后全部ACT_TERM;每站状态区保证FINISHED无损交付。Submit_Batch只接受事件目标CA。新增/替代站下轮参加,尚未选入BEGIN便删除的站跳过;已经选中尤其已确认的原站必须完成或明确终结。各站独立计时,总期限默认关闭;FINISHING的终结事件读完前仍忙拒绝新激活。详见[总召设计](IEC104_Interrogation_设计架构.md#33-广播地址与本站地址)。
主站COT=8按原连接、目标CA/OA/TEST/QOI匹配总召,包括已确认但尚未取数或等终止的站。定向撤销匹配站,其他站继续;广播撤销停止整轮,对各仍活动的站回肯定COT=9。FINISHED(DEACTIVATED)通知应用清理,匹配站旧批次失效。已被发送层接手的帧仍按顺序发送,撤销不补发成功ACT_TERM。
业务接受和每批取数期限默认各15秒,执行整体期限默认0(禁用),各项均可单独配置;这些是业务默认策略,不是标准规定的总召时限。关闭整体期限允许慢链路累计传输超过原60秒上限,底层单帧发送、确认和链路探测期限仍有效。BEGIN生成时启动已启用的业务期限,此前等待业务事件空位没有独立超时,但已占用总召资格,应及时读取FINISHED。总期限显式设为非零后仍按固定整体耗时计算,不因新批次或发送重试而延长。START/CONFIRM超时及BEGIN阶段Fail_Request复用原槽发送否定ACT_CON,保留TCP及其他短应答;肯定ACT_CON被发送层接手后,取数超时、总超时和数据源失败仍关闭原连接。终结事件保留具体原因,BEGIN未取出则只移除工作事件。接受BEGIN前检查数据源及必要业务资源,仍可逐批取值并编码。等待前查询timer_active/timeout_remaining_ms并持续驱动Process。此次失败处理修复不新增接口或配置开关;先前已移除queue_timeout_ms及多总召排队状态/统计,保留negative_submitted和dropped_requests,使用旧字段的调用方须调整并重新编译。
原`IEC104_SlaveConnection_Init`和手动接入对象已移除,调用方改用Slave服务及其事件接口并重新编译。TCP底层接口仍供独立通信接入和维护测试使用,业务的默认入口统一为Slave。
本次接口直接替换为独立对象与统一ASDU入口,删除旧通用监视/命令联合体及分类解析、Builder接口,不提供兼容包装;调用方须按上述示例迁移并重新编译。构建须新增`IEC60870_InformationObjects.c`。
时标保留IV/SU;IV=1可解码但不代表可校时。业务新上报应提供本地标准时间且SU=0,应答回显保留请求时标。验证命令和覆盖范围见[测试说明](../test/IEC104测试文档.md)。
### 时钟同步业务
[ClockSynchronization](../../104/business/IEC104_ClockSynchronization.h)处理Type 103、COT=6、IOA=0,按共用站点表路由;COT=5读时和COT=8撤销回45。独立静态池默认与TCP服务容量相同,每实例4个任务槽,与总召/计数量分别创建、分别取消。Handle_Event校验保存,Process处理期限及FIFO应答,只有应用显式Execute_Next会调用本次传入的同步action,不保存函数指针。
默认源标准时区480分钟、执行期限15000ms,0禁用期限;IV/SU及时区由公共时间工具处理,64位毫秒计算只补偿受理后的站内滞留。action收到的local_time已经是固定UTC+8,base_ms为执行检查时刻。肯定/否定确认保留原CP56字节,不回读时钟或提供自定义确认时标。
调用过action的确认走Try,错误校验应答走普通HIGH;WAIT只重试确认,STOP取消未交接任务但不回滚时钟。广播动作一次,快照目标分别确认;满槽丢新并计数,不断链。同内容的新请求仍执行,可能再次回拨。持续NORMAL发布可能推迟Try确认,应用通过包含回调执行期的confirm_pending串行协调新的带时标上报。默认示例只设模拟时钟,`--clock system`才调用Windows时间源。完整契约见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md)和[调用流程](IEC104_完整调用流程.md#flow-clock-synchronization)。
### 遥控、遥调与带时标命令业务
[Command](../../104/business/IEC104_Command.h)处理Type45–51及58–64、COT=6激活与COT=8撤销;默认只启用45/46,均须先选择。其他类型由应用通过type_mask显式启用;51/64没有S/E位,只能直接执行,不能加入select_required_mask。每个Slave只能绑定一个实例,同一实例从创建事务到读取FINISHED并交接完应答期间一直忙,所有站点的新命令互斥。未启用类型继续交给后续处理器或44兜底。
设定值由request.value交付应用;选择和执行比较原始值字节及屏蔽S/E后的QU/QL,不比较CP56。带时标激活默认按10秒窗口检查,应用通过Set_Time_Base提供UTC纪元毫秒及对应Net单调毫秒;过期默认静默丢弃,超前、无基准或无效时间回否定。max=0只关闭时差检查,仍拒绝IV及UTC换算失败。校时成功后应在动作回调外更新基准;STOP/断线保留基准,失去可信时间时Clear_Time_Base。
Handle_Event校验并生成SELECT/EXECUTE事件,Accept先消费事件再至多调用一次本次传入的动作;不保存回调。EXECUTE启动后先Report_Result记录设备结果,再逐帧Submit_Return_Info,全部OK后Complete。确认未交接时仍可报告结果以停止执行期限;返回信息WAIT时由应用保存ASDU重试。执行确认、返回信息与ACT_TERM走Try,模块内应答按全局FIFO提交。
已取走工作事件的事务恰好产生一次FINISHED;FINISHED只结束协议事务,应用自行维护尚未结束的物理输出和站点引用。退出时Cancel、读完FINISHED,再关闭Slave并分派余下通知,最后Destroy。完整规则见[遥控设计](IEC104_Command_设计架构.md)、[API第25节](IEC104_API接口汇总.md#file-25)和[遥控调用流程](IEC104_完整调用流程.md#flow-command)。
## 启停主流程
| 角色与过程 | 调用顺序 |
|---|---|
| 主站启动 | `Prepare_Startdt_Act` → 实际发送 → `Complete_Startdt_Act_Send` → 收CON → `Handle_Startdt_Con` |
| 主站停止 | `Request_Stopdt` → `Prepare_Stopdt_Request` → 依次发送S/ACT并提交完成 → 收CON → `Handle_Stopdt_Con` |
| 子站启动 | 收ACT → `Handle_Startdt_Act`(立即STARTED/IDLE)→ 发送CON → `Complete_Startdt_Con_Send` |
| 子站停止 | 收ACT → `Handle_Stopdt_Act`(立即STOPPED/STOP_DRAINING)→ `Prepare_Stopdt_Response` → 按需发送S、等待I帧确认 → 发送CON → `Complete_Stopdt_Con_Send` |
上表省略统一前缀 `IEC104_Link_`。只有具体接口返回值表明已生成报文时,才将本次输出交给发送层;生成不等于已发送。部分 Handle 只受理请求或提交对端确认,例如 `Handle_Stopdt_Act` 不生成应答,后续由 `Prepare_Stopdt_Response` 推进。S帧完成交给 `Complete_S_Ack_Send`。被控站不暂存相反方向ACT;停止等待期间成功受理START即恢复STARTED/IDLE,结束尚未生成STOPDT_CON的停止等待。
正常控制流程应串行执行启停,例如 `STOPDT_ACT → STOPDT_CON → STARTDT_ACT → STARTDT_CON`。主站的忙或重复请求结果不表示可以再次发送ACT。子站交错请求按实际受理顺序改变状态:START激活,STOP停用。已经生成的CON始终按FIFO发送,后到请求不撤回旧CON,旧CON完成也不覆盖后到ACT已经提交的状态。具体语义见 [链路设计第4.2节](IEC104_Link_设计架构.md#42-被控站受理启动)。
被控站成功受理STARTDT_ACT时立即进入STARTED/IDLE,并生成STARTDT_CON。首个CON尚未Complete时,后续每个成功受理的重复START也生成一帧CON;同状态重复请求不再次产生激活通知。所有CON按FIFO发送,Complete只确认对应发送结果,不重新提交启停状态。首个STARTDT_CON尚未完成也能受理STOP,立即停用并进入STOP_DRAINING。具体分支见 [链路设计第4.2节](IEC104_Link_设计架构.md#42-被控站受理启动)。
## 子站发送模块(已实现)
三个模块采用变长业务队列、发送前编号和独立确认记录组织,HIGH满时拒绝、NORMAL覆盖最旧原文,同时支持无malloc、部分写入续传、本地发送期限、严格启停/TESTFR、逐帧回执及可配置防饿死配额。
ASDU队列的状态以业务条目为单位:`unsubmitted_*` 表示队列尚未登记发送交接的待发条目。操作顺序是 **SendBuffer接手完整I帧 → Manager建立发送记录 → Mark_Submitted完成队列记账**。最后一步执行前,队列仍保留待发旧状态,不能据此判断SendBuffer是否已经接手。这里的“已提交”不表示整帧已写完或对端已ACK。
HIGH在完成交接记账时消费队列原文,SendBuffer中的完整I帧及Manager记录继续有效;NORMAL则保留原文等待ACK。`retained_unacked_count` 统计原文仍保留、已完成交接记账且待确认的ASDU条目,HIGH恒为0;`overwritten_unacked_count` 统计NORMAL原文已覆盖、仍待确认的条目。全部已编号未ACK的I帧数等于Manager的 `high_unacked_count` 加NORMAL保留及覆盖待确认数。`is_storage_empty` 只表示ASDU原文环为空,HIGH和NORMAL仍可能有对应I帧待确认;SendBuffer的 `is_idle` 表示当前无帧和预留。接口和行为变化见 [发送模块接口迁移](IEC104_发送模块_设计架构.md#migration),调用方须同步迁移、重新编译并初始化。
- **AsduQueue**:HIGH/NORMAL各用一个调用方提供的字节数组。两级使用不同条目头:HIGH仅存2字节长度;NORMAL存条目编号、状态及长度,头大小为 `sizeof(IEC104_AsduNormalEntryHeader_t)`,当前Windows 64位GCC下为16字节。条目恰好占本级头长加ASDU实际长度,头和原文均可跨尾部回绕,无额外回绕填充。两级都不保存user_tag,只有NORMAL分配条目编号。未提交条目通过待发游标直接定位。HIGH空间不足返回FULL,提交后立即释放原文;NORMAL保留原文到有效ACK,空间不足时覆盖最旧原文,包括已提交未ACK条目,成功返回OK且无逐条丢弃通知。条目超过目标桶总容量返回ERROR,须修正容量或组帧,不清空旧数据。
- **SendBuffer**:结构内只存一份255字节当前I帧,另用调用方提供的 `IEC104_SendBufferControlFrame_t[]` 保存S/U,每槽APDU仅6字节。当前帧连续写完;控制队列排在下一未编号业务之前。取消了APDU节点池、控制预留量和重复的协议序号边界。
- **SendManager**:Connection在准备实际写socket时调用 `Prepare_Next_Send`,无当前帧才重新选业务并分配N(S)。重复调用只返回当前帧剩余部分,最多一帧I尚未本地完成。实际写入后用 `Handle_Send_Progress`;解析后的I/S用 `Handle_I_S_Frame`。有效ACK更新所有对应发送记录;HIGH只核减Manager的未确认计数,NORMAL释放仍保留的原文或核减覆盖待确认数。确认记录由TcpServer复制为RECORD事件后经Connection移除,业务稍后处理事件副本。原文出队或覆盖不撤回当前帧、不释放k窗口,也不重置期限。
业务入口为Slave;其下的TCP及发送内部调用主线为:
```text
主动上报:TcpServer_Enqueue_Event → 服务ASDU队列(首次连接前、断线期间均可缓存)
连接应答:TcpServer_Send_Response → Connection_Submit_Response → Manager.Submit_Response(直接准备或缓存HIGH)
总召数据:TcpServer_Try_Send_Response → Connection_Try_Submit_Response → Manager.Try_Submit_Response(只能直接准备,不能排队则WAIT)
内部发送:Connection经Manager准备字节 → TcpServer经Net写socket → Connection回报Manager进度
内部接收:TcpServer经Net收字节 → Connection经Receiver成帧、由Manager统一提交链路事件
事件交付:TcpServer复制I帧/回执到事件队列并释放内部资源 → 业务通过NextEvent取副本
```
内部模块读取AsduQueue时,`AsduHeadInfo` 通过 `asdu_first/asdu_first_length` 和可选的 `asdu_second` 返回原文,`asdu_length` 是总长度;需要连续副本可用 `Copy_Asdu`。HIGH的 `entry_id.entry_serial` 恒为0,仅核对当前代次;从Peek到Mark/Discard之间不得移除、清空或替换队首,0编号不能当作可长期保存的条目身份。HIGH提交或清空后、NORMAL原文被覆盖或ACK释放后,对应借用立即失效。Manager已处理跨界原文,传输层取得的APDU仍连续。每级需同时保留N条、ASDU最长L字节时,HIGH配置 `N × (2 + L)`,NORMAL配置 `N × (IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN + L)`,无需另留回绕空隙;HIGH的N只计待发条目,NORMAL的N还包括原文仍保留的未ACK条目。Manager的构帧函数另使用249字节局部拼接数组,需计入栈预算。
仅NORMAL使用64位非零循环条目编号:`UINT64_MAX → 1`,回绕不改变连接代次,也不清空队列。先后顺序按编号环距离判断;NORMAL保留条目及覆盖待ACK区间不能跨越半个编号环,旧记录阻止窗口前进时NORMAL入队返回FULL,处理旧条目或ACK后可重试,HIGH不受该编号窗口阻塞。NORMAL条目释放后身份失效,调用方不得保存旧身份跨完整编号周期再次使用。NORMAL头内状态在Mark时改为已提交,Rebind时恢复未提交;接口说明见[发送模块设计](IEC104_发送模块_设计架构.md#aq-functions)。
`Prepare_Next_Send` 的成功值是 **OK**;`Submit_Response`以OK表示直接准备、QUEUED表示已缓存HIGH;控制帧入队成功仍为QUEUED,不能按布尔值判断。两条I帧路径共用组帧和发送记录,直接应答不经过ASDU队列,也不绕过旧HIGH、当前帧、窗口或公平配额。TcpServer的`Send_Response`最多尝试一次非阻塞写入;EAGAIN保留当前帧和期限,部分写入不能交错其他APDU。当前I帧已经准备后,即使暂时写出0字节,也要先完成,再发后来排队的控制帧。
`Try_Submit_Response`还要求NORMAL没有未提交条目;无发送机会时只返回WAIT,不降级为HIGH排队。Slave提供事件连接入口`Try_Send_ASDU`和纯值身份入口`Try_Send_Response`;总召使用后者,WAIT后重读数据。它们仍通过原SendBuffer、发送记录、k窗口和链路期限,不要求等待所有已提交帧的ACK。已有普通应答和主动上报接口的排队语义不变。
业务提交统一返回`IEC104_TcpSubmitResult_t`:`IEC104_TCP_SUBMIT_OK`表示已接手ASDU,不保证已写完或已ACK;WAIT表示未接手,业务保留重试;FATAL可能发生在已写出字节后,须处理收尾事件。`Enqueue_Event`只缓存服务事件,`Send_Response`尝试直接发送或缓存连接应答;调用后重新Prepare刷新调度条件。旧Enqueue_Normal/Enqueue_High入口与TCP_ENQUEUE结果名已移除,不保留兼容包装。
子站受理STARTDT_ACT即进入STARTED,STARTDT_CON作为发送任务按FIFO完成。STOPDT受理即停用并停止选取新业务,最多完成一帧此前已编号的当前I帧,等待已发帧确认后返回CON;未提交的NORMAL保留未编号,HIGH按下述规则清理。公共S、t2、TESTFR、t3及子站启停继续由Manager封装。接收接口的WAIT表示请求尚未受理,接入方须有界保留原帧并按接收顺序重试。
Manager初始化成功会清空HIGH待发原文并重置高优先配额;新STOP受理时清空HIGH。从停用状态受理START时,生成本轮首个STARTDT_CON并清空HIGH、重置高优先配额;已激活状态下的重复START仅追加CON,保留HIGH和配额。WAIT时请求尚未受理,不清理HIGH。以上清理均不影响NORMAL,也不撤销已提交的当前I帧及其发送记录。
初始化使用字节容量和控制槽数组。此次移除 `Enqueue` 的最后一个user_tag参数,以及条目查询、发送存储记录和记录查询中的user_tag字段;旧调用方须迁移、重新编译并初始化。HIGH发送记录的 `asdu_entry_serial` 恒为0,NORMAL继续保存条目编号,两级都用独立frame_id处理发送完成和帧回执。发送记录数组使用 `IEC104_IFrameSendRecordStorage_t`,连接代次由Manager统一保存;查询输出用 `IEC104_IFrameSendRecord_t`,返回完整 `frame_id`。当前Windows 64位GCC下,移除user_tag后每条存储记录由56字节降为48字节,查询快照由64字节降为56字节:
```c
static IEC104_IFrameSendRecordStorage_t records[12];
IEC104_AsduQueue_Init(&queue, high_bytes, sizeof high_bytes,
normal_bytes, sizeof normal_bytes, generation);
IEC104_SendBuffer_Init(&buffer, control_frames, control_count, generation);
IEC104_SendManager_Init(&manager, &queue, &buffer, &link,
records, 12, k, high_burst_limit);
```
以上省略返回值检查,完整示例、接口替换及容量单位见 [发送模块设计](IEC104_发送模块_设计架构.md#integration) 和 [接口迁移](IEC104_发送模块_设计架构.md#migration)。Link须先按CONTROLLED初始化。Manager允许绑定有未编号业务的队列,绑定成功会清空HIGH、保留NORMAL,不接管运行中的旧连接。
`Manager.Abort` 保留AsduQueue;结束旧I/O并核对记录中的ACK事实后,TcpServer调用 `AsduQueue.Detach` 解除绑定,清空HIGH并恢复NORMAL离线缓存;新连接再 `AsduQueue.Rebind` 到新代次并初始化。**HIGH固定用于当前连接的请求应答,重连清空;NORMAL固定用于允许跨连接恢复的事件,保留现存原文并恢复未提交。** 内部队列不提供逐条重连策略,使用 `Enqueue(queue, generation, priority, asdu, length)`;业务只调用TcpServer的Enqueue_Event/Send_Response。Rebind先完整检查两级条目,再将NORMAL头内已提交状态恢复为未提交并更新控制字段,不搬移ASDU原文,不使用整理数组。NORMAL头含编译器位域及对齐,必须按对应头长宏配置容量,不能沿用旧18字节头的缓冲内容或跨ABI解释。使用约定见 [队列生命周期](IEC104_发送模块_设计架构.md#aq-reconnect-policy)。
Rebind清除旧覆盖待确认计数,所有旧身份和借用失效;已覆盖原文不能恢复。它本身不发送,新连接仍须完成STARTDT;同一TCP上的STOPDT/STARTDT恢复不调用Rebind。未ACK不证明对端未收到,NORMAL只用于业务允许向同一接收方或冗余域恢复的事件;旧请求应答应放HIGH。关闭整个服务时由TcpServer调用Queue.Abort清空缓存,不提供掉电持久化。未编号条目被覆盖或重连丢弃时没有帧回执,旧帧回执须在Manager.Abort前读取;尚不提供冗余组切换或应用业务去重。
Connection通过`SendManager_Get_Deadlines`一次取得当前队首身份、本地发送及I帧t1剩余时间、控制槽容量;对应活动标志为1且剩余时间为0表示到期。原始queued_at_ms仍保存在SendBuffer,written_at_ms仍保存在Manager记录,部分ACK后选下一条的原始时刻。Connection的`Collect_Deadlines`汇合TESTFR、STOP、t2及t3,供超时检查、发送准入、控制推进和等待查询共用;不创建系统定时器,也不缓存第二份计时起点。原有底层单项超时接口保留。TcpServer在Process中执行TCP读写并驱动Connection,Poll复用该推进核心。
发送准备和写入回报仅因本地发送及STOP期限直接报告超时;ACK/TESTFR的t1故障交给接收阶段判定。旧确认t1已到时,统一发送准备入口暂停为HIGH/NORMAL创建、编号新I帧;已有帧及控制发送仍可推进,避免控制槽解阻后继续新增业务帧。有效确认解除所有已到期等待后自动恢复准入。Send_Response和常规发送仍按send后的真实时刻登记完成,发送期间跨过确认期限时安排立即Process,先有界处理已到达的确认,再按原始t1检查。Prepare对到期协议期限返回零等待;不增加恢复期限或固定宽限时间,也不改变真实容量背压下的超时策略。
## 子站连接协调层(已实现)
TcpServer内部的`IEC104_Connection_t`内含一个Receiver、一个Link、一个SendBuffer和一个SendManager,借用TcpServer提供的ASDU队列、控制槽数组及发送记录数组。初始化后保持对象地址不变,同一连接的所有调用串行执行。内部接口、时间规则及重连顺序见[连接层设计](IEC104_Connection_设计架构.md)。
Connection_Init先准备Receiver及配置,再由`SendManager_Init_Connection`校验并向最终地址提交Link、SendBuffer和绑定关系,最后提交其余连接字段;失败保留原Connection和队列。
控制帧是否可准备由Link的`Can_Prepare_*`只读接口判断,并与实际构帧共用规则。Connection对Link保留只读访问,运行期修改统一经SendManager,TESTFR_CON也经`SendManager_Handle_Testfr_Con`提交;只有返回OK才撤销本端探测t1,IGNORED不取消计时。Connection不直接查询SendBuffer,也不操作Manager的内部指针。Connection及SendManager的完整结构体供内部静态嵌入;TcpServer公开头文件隐藏这些布局,业务只使用TCP句柄、公开值类型及事件接口。
以下步骤均在TcpServer及Connection内部执行:
1. `Connection_Init`以CONTROLLED角色初始化STOPPED链路。成功受理STARTDT_ACT即进入STARTED/IDLE并开放I帧准入,发送队列保证已有CON先于随后准备的本端I帧写出。
2. 收到TCP字节后调用`Connection_Feed`,按`consumed_length`推进输入。一次最多处理一帧;未消费尾部由TcpServer保留。半帧留在Receiver中;S/U受理后自动释放;I帧返回FRAME_READY后由TcpServer经Peek复制为事件,再按`receive_serial`显式Release。
3. WAIT表示当前接收请求尚未受理,Connection保留原文并在后续驱动中重试。成功受理的START/STOP按控制帧路径自动释放原帧;重复驱动不会再次提交同一I帧的序号或ACK。
4. `Connection_Prepare_Next_Send == SEND_READY`时取剩余字节实际写入,再用`Handle_Send_Progress`回报增量;0字节和部分写入保留当前帧及原期限。整帧完成自动提交对应Link完成和Manager记录,不能另调底层Complete。本地已知发送完成必须先回报,再Feed覆盖该帧的ACK。
5. 定期调用`Connection_Tick`推进本地发送期限、I帧/TESTFR的t1、w/t2确认、t3探测和STOP总期限。停止过程中的重复STOP和WAIT不延长原期限;START成功受理取消旧STOP总计时。TESTFR等待CON的t1不因其他链路活动续期。t2保留连续未确认接收区间的最早起点,前缀被确认时不续期。
6. FATAL后连接冻结,TcpServer停止真实I/O,把剩余记录和断开通知复制为事件,再调用Connection_Close。Connection_Close保留内部ASDU队列;TcpServer随后Queue.Detach清空HIGH并保留NORMAL,离线期间继续接受NORMAL;重连时Queue.Rebind到新代次,再Connection.Init。仅Close服务清空全部ASDU。所有旧代次事件返回ERROR,不修改新连接。业务只处理TcpServer交付的事件并执行ASDU业务逻辑。
旧 `IEC104_Rx_Feed_Byte`、`IEC104_Rx_Reset`、`IEC104_Legacy_Link_Reset`及相关类型保持原样;旧入口继续使用全局单路状态、预设STARTED,并在成帧后检查I/S序号,只返回接收结果。Connection使用新Receiver并共享自身Link与SendManager,不调用旧全局接收入口。同一条连接不能同时交给这两条接收路径。
因接口过期而无法编译的旧回归测试及专用辅助文件已删除。当前有`iec104_tcp_event_ring_test.c`的13个字节环场景、`iec104_tcp_scheduler_test.c`的25个调度场景,以及`iec104_net_pending_input_test.c`的Windows回环检查;构建和运行见[测试说明](../test/IEC104测试文档.md#2-编译与运行)。
2026-10-20调度修复:预算让出时固定已到达输入范围并立即续跑,处理完后再判定ACK/TESTFR超时;后续新输入不能扩大旧批次。发送使用独立预算,持续接收不再跳过发送阶段。业务API及配置宏不变,内部Connection和Net新增续收/输入快照支持,自定义Net后端需实现`IEC104_Net_Pending_Input`。真正容量背压仍遵守原有严格期限;回执与启停通知的顺序继续按B方案。
以下为历史变更与验证记录,不代表当前保留的测试范围:
2026-09-23:NORMAL空间不足时改为覆盖最旧原文(含已提交未ACK条目),HIGH继续满时拒绝;当前APDU和发送记录独立保留,ACK兼容原文已覆盖。条目支持跨尾部存放,无填充碎片。仅更新代码和设计文档,测试文件保持原样。
2026-09-23:三发送模块改为变长ASDU字节环、单当前I帧和紧凑控制队列,发送前编号,有效ACK释放原文,业务回执独立消费,支持显式Rebind。保留ACK不得越过本地完成位置的修复。原有全部119个用例通过;本次补充校验从标准输入编译执行,未修改或新增测试源码、测试配置和测试文档。
2026-09-18:LINK测试改为报文回放用例,当时默认LINK套件68个、全部套件119个场景通过。该套件已随过期测试清理移除,历史记录保存在`.build`中。
2026-09-16:TESTFR改为三态,取消提前CON的缓存;当时旧用例因状态枚举变化而无法编译,未计为该次变更的验证结果。
2026-09-14:完善STARTDT期间的收发方向门控,当时新增LINK 36至40。算法契约见[链路设计](IEC104_Link_设计架构.md)。
@@ -0,0 +1,31 @@
# 文档索引
说明文档统一放在 `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_ClockSynchronization_设计架构.md) | Type 103、显式设时动作、滞留补偿、期限及广播确认 |
| 104 | [遥控设计](104/IEC104_Command_设计架构.md) | Type 45–51/58–64、选择执行、撤销、时标判定、返回信息及FINISHED清理 |
| 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具体芯片驱动暂不实现,也不规划其他操作系统适配。Type 103业务已由独立的[ClockSynchronization模块](../104/IEC104_ClockSynchronization_设计架构.md)接入,应用显式执行设时动作;公共Time/Clock仍不自行分派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。Type 103的ACT_CON属于原请求回显,保留原CP56及SU,不套用新生成时标的SU=0规则。本文中的通用发送函数仍为调用示例。
业务发送时,输入日历须已经是本地标准时间。若来源是夏令时日历,应先完成时间转换,再以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。ClockSynchronization使用`CP56_To_Seconds`取得UTC秒数,在64位毫秒域补偿受理后的滞留,再调用`Seconds_To_Local`;确认直接回显原请求,不重编码补偿后的日历。
转换接口的`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业务层,Type 103确认由ClockSynchronization按原文生成 |
| 校时ASDU校验、期限、滞留补偿及确认 | `IEC104_ClockSynchronization`;设备权限、实际设时及事件时间戳仍由应用接入 |
| 实现`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系统时钟适配及Type 103业务模块。公共工具、协议处理及设备接入的边界如下:
| 项目 | 当前状态与待补齐内容 |
|---|---|
| 协议时间源读取及设置 | 已提供直接调用的`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`复用CP16/CP24/CP56编解码,`IEC60870_Asdu.c`维护容器与原文应答;ClockSynchronization处理Type 103校验、排队、补偿、显式执行及成功/失败确认。事件采集、校时权限及平台设时仍由应用action接入;示例默认模拟,显式system模式调用Clock_Set |
| 日期转换实现 | 保留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或校时权限;不能跳过转换直接把解码的源日历交给设时接口。以上片段演示公共接口的直接用法。通过ClockSynchronization接入时,action收到的`request->local_time`已完成校验、时区换算及受理后滞留补偿,可直接交给Clock_Set,不再次换算;肯定/否定确认由业务模块生成。
## 时间戳转日期的实现选择
业务层统一调用`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,Type 103确认保留原SU;接收时将完整解析值交给转换接口,不丢弃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以供业务记录,但转成有效时间戳时会拒绝。已有接收、链路和发送模块的接口及行为不变,时间工具由需要处理时间戳的业务按需接入。
@@ -0,0 +1,188 @@
# 104子站示例
[iec104_slave_example.c](../../examples/iec104_slave_example.c) 包含模拟点表、业务事件处理、其他命令兜底、事件循环及main。总召状态机使用独立库模块[IEC104_Interrogation.c/h](../../104/business/IEC104_Interrogation.h),不在示例内重复实现。
当前实现站总召(`C_IC_NA_1`,QOI=20),依次返回8个单点遥信、8个双点遥信和8个标度化遥测值;分组1(QOI=21)返回单点及双点遥信,分组2(QOI=22)只返回遥测。库支持全部16组,示例未配置的其余组由应用拒绝。通信通过`IEC104_Slave`,总召通过`IEC104_Interrogation`接入,所有任务使用固定存储、在主循环线程串行推进。示例另含计数量召唤、Type 103校时、45/46遥控、49标度化设定值及58/59/62带时标命令;默认时钟为模拟值,命令只更新内存点值,实际设备数据源和物理输出仍由应用接入。
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。
主循环向总召、计数量、校时、遥控实例广播生命周期通知,读空Slave通知后优先推进遥控,再交替推进召唤业务并显式执行校时;其他类型在四个模块均未接手后兜底。原Example_Init仍可用于仅总召测试,命令行入口分别调用计数量、校时及Example_Init_Command补齐实例,两个召唤实例各需独立广播状态区,校时使用共用站点快照。协议及应用动作限制见[计数量设计](../104/IEC104_CounterInterrogation_设计架构.md)、[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)和[遥控设计](../104/IEC104_Command_设计架构.md)。
在项目根目录的 PowerShell 中执行,需要 MinGW GCC:
示例通过 `IEC104_Interrogation.h`、`IEC104_CounterInterrogation.h`、`IEC104_ClockSynchronization.h` 和 `IEC104_Command.h` 引入业务声明;构建脚本添加 `104/`、`104/business/` 和 `common/` 头文件搜索路径,编辑器也应配置这三个目录。构建可执行程序仍使用下面的脚本,它会链接所需库源文件、Clock适配及Windows网络库;只编译当前C文件会缺少库函数实现。编辑器的C语言标准应与构建保持一致,使用C11,并选择实际的MinGW GCC编译器。
```powershell
./scripts/build.ps1
./.build/slave-example/iec104_slave_example.exe
```
默认监听 `127.0.0.1:2404`,公共地址 CA=1;按 Ctrl+C 退出并关闭服务。也可以指定监听地址、端口和公共地址:
```powershell
./.build/slave-example/iec104_slave_example.exe --bind 0.0.0.0 --port 2404 --ca 1
```
| 参数 | 含义 |
|---|---|
| `--bind IPv4` | 数字IPv4监听地址,默认127.0.0.1 |
| `--port 0..65535` | 默认2404;0由系统分配,启动时输出实际端口 |
| `--ca 1..65534` | 本站公共地址,默认1;总召请求也接受65535,广播请求的应答使用本站地址 |
| `--window 1..32767` | 发送窗口k,默认12;设为1可观察逐帧ACK后续发 |
| `--clock simulated\|system` | 默认simulated,只更新示例模拟时钟;system显式调用Windows系统设时接口 |
| `--run-ms N` | 运行约N毫秒后正常关闭;默认0表示持续运行 |
| `--help` | 打印用法后退出 |
启动输出 `LISTEN 地址:端口 CA=站地址`。退出保留原有STOP统计字段:`submitted`累计总召与兜底已接手ASDU,`gi_submitted`为已提交完毕的总召数,二者均不表示主站已经ACK。`waits`只统计总召发送WAIT;兜底HIGH满时丢新,计入`overloads`和`dropped_requests`,不记录待重试次数。兜底ERROR/FATAL计入`cancelled`,其中FATAL可能已写出字节,该计数不表示确定未发送。拒绝、主站撤销及业务超时统计继续保留,计数量另见`counter_submitted`和`counter_completed`。末尾校时字段`clock_executed`为实际action调用数、`clock_synchronized`为action返回true次数、`clock_submitted`为已交接校时ASDU数,包含错误及否定应答,广播按各提交目标分别计数;不混入原字段。
命令统计追加`command_selected`、`command_executed`、`command_completed`、`command_submitted`和`command_returns`,分别表示已接受的选择、已启动的执行、成功完成事务、已交接应答和返回信息帧数;`command_time_expired`统计过期激活,`command_time_rejected`统计无效时间、无基准、推算失败或超前造成的时间否定。静默过期不会增加选择/执行次数;保留原有统计字段的含义。
### Type 103校时
主站发送Type=103、SQ=0、对象数=1、COT=6、P/N=0、TEST=0、CA匹配或65535、IOA=0及有效CP56。默认按主站UTC+8标准时间解释,SU由时间工具处理;模块补偿受理至执行检查的等待。应用显式Execute_Next后回COT=7,原CP56字节原样回显。TEST否定7且不执行;COT=5读时否定45,不返回本站当前时间。
默认`--clock simulated`把请求的utc_ms/base_ms保存为模拟时钟基准,不改变系统时间。`--clock system`通过`IEC60870_Clock_Set(&request->local_time)`设置Windows时间;local_time已是固定UTC+8且已补偿,示例不再手工换时区或加等待。Clock内部转换为UTC后调用系统接口,权限不足时动作报告失败并回否定ACT_CON。真实TCP自动回归只运行模拟模式;system接线另用mock时钟单测,不设置宿主系统时间。
两种模式在成功校时动作中都记录utc_ms/base_ms及待更新标志;`Example_Drain_Clock`在Execute_Next返回后、回调之外把基准交给Command。system模式还在启动及每60秒、请求分派之前读取Clock,读成功且IV=0时按固定UTC+8转换(Clock读值不叠加夏令时),关联同一时刻的Net单调毫秒;读取/换算/设置基准失败或IV=1时清除Command基准。仅总召测试未创建Command,相关接线先检查空句柄。
确认发送WAIT时只重试原确认,不再次设时;STOP取消未交接确认而不撤销已完成的动作。同内容的新请求会再执行,旧时标重发可能回拨。真实数据发布方应在confirm_pending期间暂停新的带时标上报并继续驱动网络,避免持续新增NORMAL推迟确认。完整范围及期限见[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)。
### 遥控、设定值与带时标命令
`Example_Init_Command`在默认配置基础上显式启用45、46、49、58、59、62,select_required_mask等于type_mask,六种类型均须先选择;库默认配置本身仍只启用45/46。决定期限15秒、选择保持30秒、执行期限60秒,成功发送ACT_TERM,失败默认不发。整个实例只允许一个事务,所有站点的新命令互斥;TEST请求否定,CA=65535回UNKNOWN_CA,其他命令IOA由应用Reject(UNKNOWN_IOA)。
| 命令 | 受控点及返回信息 | 初值与总召 |
|---|---|---|
| Type45/58,IOA6001..6008 | Type1,IOA1001..1008,COT=11;状态0分/1合 | 初值见下表,执行后总召读取更新值 |
| Type46/59,IOA6101..6108 | Type3,IOA1101..1108,COT=11;状态1分/2合 | 初值全部为1;执行后站总召及遥信分组读取更新值 |
| Type49/62,IOA6301..6308 | Type11,IOA2001..2008,COT=11;写入标度化值 | 初值见下表,执行后站总召及QOI=22读取更新值 |
表中IOA均为十进制。单点命令6001..6008分别对应遥信1001..1008;双点命令6101..6108分别对应返回位置1101..1108。例如对Type46、IOA6103完成状态2的选择与执行后,后续QOI=20或21读取的Type3、IOA1103为2;FINISHED清理不会把它恢复为初值1。
选择S/E=1、执行S/E=0,均使用COT=6;执行的类型、IOA、OA、值及QU/QL须与选择一致,带时标命令的选择和执行可使用不同时标。主站收到选择的肯定ACT_CON后再执行;执行依次返回ACT_CON、返回信息、ACT_TERM。撤销使用COT=8,同类型/IOA/OA匹配,不要求值相同。示例未启用47、48、50、51、60、61、63、64,仍由兜底回44;这些类型由模块专项测试覆盖。
49/62允许的示例标度值为−10000..10000(包含端点),选择和执行都检查限值,越限调用Reject(NEGATIVE),动作内另作守卫。这个范围属于模拟点表策略,不是协议或Command模块的统一限值。比如对6301设为−1234,Type11返回信息及后续QOI=20/22的2001均为−1234;总召对象数与帧序不变。
58/59/62的CP56按主站UTC+8及SU解释,默认时差上限10秒,IV=1拒绝,时差恰好10秒也拒绝或丢弃。simulated模式启用时尚无可信基准,须先成功校时;system模式可由启动读时建立基准。过期激活默认静默丢弃,不占事务槽或调用动作;无基准、超前及换算失败回否定7。只在受理时检查时效,默认决定期限内继续等待可能使启动动作时的命令年龄接近25秒。撤销仍检查CP56格式,但不判时差。
`Example_Drain_Command`先读取SELECT/EXECUTE/FINISHED;首次保存事务context前Acquire应用站点引用。点表、动作和返回信息按基础类型分派;`Example_Command_Action`模拟返校或修改`ExampleApp.signal_values/double_values/scaled_values`并返回STARTED,不在动作中上报。Type11单对象返回需要12字节缓冲。Accept的OK只表示事件已消费,示例通过Get_Transaction确认EXECUTING后,依次Report_Result(SUCCEEDED)、Example_Build_Command_Return、Submit_Return_Info和Complete;设备结果可以早于确认交接。
每一步WAIT都保留原事件和当前步骤;返回信息使用独立ASDU缓冲,WAIT时保留同一帧,OK后才推进,避免重复提交。STALE停止该事务重试,继续读取FINISHED。FINISHED清除模拟返校/输出标志并Release应用引用,不恢复已修改的点值;真实输出若仍未停止,应用必须继续保持设备忙状态和引用。Example_Wait_Time同时计入遥控期限、模块推进和应用挂起重试,发送WAIT时允许等待网络而不空转。
## 主站操作和模拟点表
主站先建立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 |
| 1101..1108 | 双点信息 `M_DP_NA_1`,Type=3 | 1、1、1、1、1、1、1、1 |
| 2001..2008 | 标度化测量值 `M_ME_NB_1`,Type=11 | 2300、2310、2290、5000、-125、0、1234、42 |
所有品质为0。遥测值为原始有符号16位标度值,本示例没有定义物理单位和倍率。
一次成功站总召依次提交8个ASDU:
1. Type=100,COT=7,肯定激活确认。
2. 两帧Type=1、COT=20的单点数据,每帧4个对象。
3. 两帧Type=3、COT=20的双点数据,每帧4个对象。
4. 两帧Type=11、COT=20的标度化遥测,每帧4个对象。
5. Type=100,COT=10,激活终止。
分组1(QOI=21)共提交6个ASDU:ACT_CON、两帧Type=1单点数据、两帧Type=3双点数据、ACT_TERM,数据COT均为21,每帧4个对象。分组2(QOI=22)仍共提交4个ASDU:ACT_CON、两帧Type=11/COT=22遥测、ACT_TERM,每帧4个对象。确认和终止保留请求QOI。BEGIN校验分组,首个NEED_DATA按该组初始化应用游标,不把模拟点表写进库。
全部应答使用原请求的连接身份。总召确认、终止及短应答走普通HIGH路径,总召数据由Submit_Batch尝试不排队提交;校时调用过action的确认也使用Try。遥控执行动作产生副作用后的确认、返回信息和ACT_TERM均走Try,选择确认及校验否定走普通HIGH。未提交HIGH/NORMAL、当前帧或窗口背压时Try返回WAIT;遥控的模块FIFO还会保留更早应答优先。总召数据和遥控返回信息不使用Enqueue_Event,只有本帧接手后才推进应用步骤。
主站撤销使用相同的实际站CA/OA/TEST/QOI,COT改为8、P/N=0。激活和撤销均接受本站地址或65535,匹配时65535映射为本站地址,允许广播激活配本站撤销及本站激活配广播撤销。尚有匹配总召时模块停止任务、回复肯定COT=9,并通过FINISHED(DEACTIVATED)通知应用;不匹配或已完成则否定COT=9。已提交发送层的帧可能仍在确认前发出,撤销后不再提交旧批次和ACT_TERM。示例点数很少,总召可能在主站发撤销前已全部提交;观察中途撤销和各阶段拥塞可使用下面的可控网络测试。
广播总召的BEGIN/NEED_DATA/FINISHED携带本站CA,示例直接用该字段构造数据。确认、数据、终止、否定及错误应答的CA都为本站地址,OA/TEST/QOI保留原请求值;原始Slave事件不被修改。该支持属于Type100总召处理器,其他类型仍按各自业务规则分派。
## 请求校验及任务处理
| 情况 | 本示例的处理 |
|---|---|
| 不支持的Type ID | 同步调用Reject_Request回显原文,COT=44、P/N=1;HIGH满时丢新并计数,不因连续请求断链 |
| 总召COT不是6或8 | COT=45、P/N=1 |
| 总召CA既不是本站地址也不是65535 | COT=46、P/N=1,保留原错误CA |
| 总召IOA不是0 | COT=47、P/N=1 |
| 激活QOI超出20..36、请求P/N非法或当前已有未终结总召 | 否定ACT_CON,COT=7、P/N=1 |
| 空闲时收到合法激活请求 | 有空槽则建立唯一总召;BEGIN后应用接受才发肯定确认,忙时不同QOI也拒绝 |
| 激活QOI为23..36 | 合法请求交给应用,示例因未配置点表而Reply_Begin(false),回否定ACT_CON |
| 撤销不匹配、QOI超范围或P/N非法 | 否定DEACT_CON,COT=9、P/N=1 |
| 匹配的主站撤销 | 取消对应总召,肯定DEACT_CON,COT=9、P/N=0 |
| 总召唯一对象完整,但SQ=1或有多余尾字节 | 回否定7/9,COT/CA/IOA错误优先45/46/47;不执行激活或撤销 |
| 总召零对象/多对象等无法可靠应答的布局错误 | 计入rejected并丢弃;保留连接和已有总召 |
| 需要新增总召且4个任务槽已占满 | 尝试否定ACT_CON;无法安全提交/保存则丢新请求并计数,保留连接与原任务;匹配撤销复用原槽 |
| 接受/取数/整体业务期限到点 | 肯定ACT_CON提交前回否定确认并保留连接,提交后关闭原连接;已取出BEGIN才交付对应TIMEOUT终结事件 |
Example_Handle_Event先过滤asdu_result失败的I帧视图,避免进入业务分派或未知类型兜底;正常线上基础头/载荷截短已由Receiver和连接层处理。当前I帧依次交给总召、计数量、校时及Command;都未接手时,在读取下一事件前同步调用`IEC104_Slave_Reject_Request`回UNKNOWN_TYPE。该接口借用当前事件生成原文错误应答,复用库的普通HIGH路径;示例不保存other_request,也没有other_pending、other_waiting或other_waits。未启用类型的连续及相同内容有效新请求逐条尝试应答,允许穿插正在进行的总召。103由校时接手,45/46/49/58/59/62由Command接手,102、47及106仍回44;模块之间不承诺跨业务应答按接收顺序排列。
OK表示已接手;WAIT只因HIGH容量不足,示例丢弃本条并增加overloads/dropped_requests,保持连接、已有HIGH内容及当前业务。ERROR按失效请求处理并继续读通知;FATAL可能已写出字节,不得重试,继续排空收尾通知。接口不修改输入事件或其最近提交结果,不能通过Get_Last_Submit_Result读取本次返回值。未读启停通知会阻止本次应答;状态计数不能追溯已消费的STOP/START,禁止先保存事件再读完后续通知后调用。需要异步业务应答时另用Copy_Event、Check_Request并自行处理STOP取消,完整契约见[同步拒绝当前请求](../104/IEC104_API接口汇总.md#reject-request)。
校验从类型开始;已知总召先检查是否有唯一完整对象,再按COT、CA、IOA、布局/QOI/P/N及事务状态决定。畸形单对象的否定副本规范为10字节、VSQ=1,保留OA/TEST/IOA/QOI,不修改原输入。广播应答使用实际站CA。错误请求不取消原总召或刷新期限;否定应答沿现有队列重试,满槽只丢新请求。这里只实现上述命令集合,不代表所有类型的业务规则。
`Example_Drain_Business`主动读取总召业务事件:BEGIN按QOI选点表,Reply_Begin接受或拒绝请求;NEED_DATA调用普通辅助函数Example_Build_Batch构造数据,再Submit_Batch,数据耗尽则Complete_Data;FINISHED按实例/请求编号清理对应资源,撤销和超时也走该分支。没有向库注册业务函数指针。数据源暂未就绪时可保存事件,后续就绪再应答;发送WAIT也保留事件,下一次调用重新组包,模块不缓存本批原文或重复生成NEED_DATA。
本例BEGIN只判断接受,首个NEED_DATA初始化游标并读取这一批;接真实数据源时,在Example_Build_Batch的逐点组包处读取当前值和品质。IOA、类型、分组成员及遍历策略均由调用者决定,协议库不登记或冻结完整IOA列表。单点和遥测常量表保存地址、初值及品质,双点初值均为1;实际值分别存放在ExampleApp.signal_values、double_values和scaled_values中,站总召及对应分组逐批读取这些可写数组的当前值,包括遥控及设定值执行后的更新;这些地址不是库规定的范围。逐批采样不保证整轮同一时刻,也不自动解决与突发上报的新旧排序;完整边界见[总召模块设计](../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成功代替资源清理。
遥控同样执行`Command_Cancel → 读完FINISHED → Slave_Close → 读空并分派通知 → Command_Destroy`。示例先取消全部业务并清理应用引用,再关闭Slave;关闭后的生命周期通知仍交付全部模块。遥控FINISHED可以在离线时读取,事务槽及独立应答清空后才允许Destroy,最后移除站点并销毁Slave。
## 验证
```powershell
./scripts/build.ps1 -Test
```
`-Test` 需要预先安装 Node.js。脚本运行Slave/站点测试、总召/计数量/兜底/校时可控网络测试、规则单测、遥控容量矩阵和真实本机TCP测试;实跑数量及记录见[测试说明](../test/IEC104测试文档.md):
- `iec104_slave_test.c`:站点注册/移除、延迟回收、重复CA、13站共享服务、CA/context隔离、注册编号防替换、快照增删及嵌套集合锁、关闭重启保留,以及原有应答、事件身份和背压回归。Reject_Request另覆盖原文回显、非法参数、连续请求、窗口背压、HIGH满、启停门禁、旧会话及FATAL。
- `iec104_slave_example_test.c`:完整报文、部分写入、各发送阶段HIGH满与WAIT、请求校验、STOP/重连、非法请求、传输通知预算及API生命周期;业务事件覆盖延迟应答、批次校验、混合类型、容量边界、拒绝/失败/取消、终结预留和实例复用。总召覆盖全部16组、各阶段撤销、三项期限、迟到应答及通知优先。调度覆盖短应答立即提交及顺序保护、非队首槽复用、满槽否定或计数丢弃且当前任务有效、各阶段相同/不同QOI均忙拒绝、结束后重发成功、撤销对象匹配、停止清理、BEGIN前等事件空位及BEGIN后完整执行期限,并验证忙状态不拦截Type101总电度请求。测试设置HIGH=251字节、业务事件FIFO=2槽,使用真实容量背压限制发送。
- `iec104_command_test.c`:独立可控Net/单调时钟测试,TCP服务容量1/2和独立应答槽容量1/4交叉构建,HIGH=251字节;覆盖14种命令布局、值和CP56校验、选择执行、匹配、时效及时间基准、变长FIFO、返回信息、动作结果、期限和FINISHED/引用生命周期。
- `slave_example_loopback_test.cjs`:启动实际可执行程序,使用Windows网络后端和127.0.0.1随机端口,以独立主站字节验证分片请求、k=1时逐帧ACK、站总召、忙拒绝及结束后重发、撤销、错误应答、106与102同批分别回44、STOP/START、重连和正常退出。校时核对正常ACT_CON、TEST否定7、读时COT=5否定45、广播实际CA,以及103与102同批时先102回44、再103回7。命令核对45/46/49/58/59/62完整流程,返回信息在确认和终止之间,越限/TEST/地址拒绝、撤销、47兜底及操作后的总召新值。正常带时标命令由最近成功校时和测试单调经过时间推算时标;过期58与106同批,结合后续全部计数量帧、动作/选择计数和time_expired验证没有58应答。服务约12秒后自行关闭,使用默认模拟时钟。
原114组基线由98组总召、12组计数量及4组兜底回归组成。兜底改用未支持的106/102,覆盖同批及反序、相同内容的有效新请求、k=1与写阻塞排队、HIGH满后丢新且不补发、STOP/STOPSTART取消,以及总召未结束时立即提交错误应答。启用校时的独立回归另外核对103业务。
17组校时及时间接线回归覆盖无站广播45/46、同批及重复103逐条执行、FIFO中间否定、满槽丢新、Try等待不重执行、STOP/重连、Defer/FATAL、同一次采样检查期限及补偿、action内Get_Status与防重入、广播WAIT期间删除当前站及同CA替代不加入。其中新增1组只替换示例的Clock_Read/Set,验证system启动、60秒刷新、失败清基准、校时成功后回调外移交及NULL Command;Clock库仍正常链接,所有动作均不设置宿主系统时间。示例测试合计132组(99总召、12计数量、4兜底、17校时/接线)。
畸形请求回归覆盖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、撤销组合及连续兜底;当前ASDU数量及STOP统计见[测试记录](../test/IEC104测试文档.md#5-验证记录),计数量和命令独立计数。5个校时应答中3次执行并成功,另2次为TEST及COT拒绝;多CA由可控Net回归覆盖。
这些检查覆盖本示例的模拟点表流程;实际主站设备、现场点表和设备执行接口仍需联调。
@@ -0,0 +1,331 @@
# IEC104 测试说明
> **目录与命令同步日期**:2026-10-29。测试代码位于根目录的 `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后端,共32组统一入口测试,覆盖ASDU视图、共用发送分派、确认/终止字节、HIGH背压、非法参数、原连接身份、延迟复制、服务复用、生命周期、外部调度及离线上报。站点测试覆盖13节点、CA/context隔离、删除即时退出路由与引用延迟回收、同CA替代节点的注册编号、快照期间增删/嵌套/结束回收、显式集合锁及跨服务误用;新增9组同步拒绝回归。共用层非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)。
同步拒绝接口回归检查COT=44..47的原文回显、TEST/OA/CA/VSQ及对象字节保留、输入事件和最近提交结果不变、非法参数及原服务/会话隔离。未读启停通知拒绝提交;已消费STOP/START后的旧事件不在同步接口允许的调用范围内,不能用状态计数为0证明其仍有效。示例回归覆盖连续及重复内容的新命令、HIGH真满时丢新且保留连接/旧应答、活动总召中的兜底、启停与发送故障收尾;原同批兜底回归继续保留。接入校时后的真实TCP对103与102同批发送分别核对ACT_CON和COT=44,不保证跨业务响应顺序。接口与丢新语义见[同步拒绝当前请求](../104/IEC104_API接口汇总.md#reject-request)。
校时测试使用模拟action及可控单调时钟,不设置Windows时间。覆盖Handle/Process不执行应用、显式执行一次、原CP56回显、COT/CA/IOA校验优先级、TEST/IV/非法CP56拒绝、SU/时区转换、受理起滞留补偿、日期上界和64位运算、期限与倒退、Try背压不重复动作、FIFO/满槽、STOP/断线/旧会话、广播快照及删除目标,以及action内只读状态。真实设备写入耗时、权限及失败后的硬件状态不由这些模拟测试证明,边界见[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)。
## 2. 编译与运行
命令专项入口为`test/iec104_command_test.c`,复用TCP事件环测试的可控Net后端,但独立驱动Slave和Command,不依赖示例的应用状态。`./scripts/build.ps1 -Test`同时构建和执行TCP容量1/2 × 命令独立应答槽1/4的四种组合,每种45组;原45/46基线为每种27组。库源文件须包含`104/business/IEC104_Command.c`,测试本身包含可控TCP实现,不再额外链接Windows Net或第二份TcpServer。
命令原有覆盖45/46选择和直接执行、匹配/忙拒绝/撤销、三类动作结果、成功失败及终止配置、零/多帧返回信息、完整ASDU校验、全局FIFO、回复池满、窗口/部分写入、结果先于确认、重复及迟到事件、FINISHED配对、三个期限和时钟倒退(含期限配置0)、STOP提交竞态/FATAL、NORMAL覆盖反例、站点删除重注册、引用和单/双服务生命周期。扩展覆盖47–51/58–64,默认未启用47/58仍IGNORED;非法配置改用保留bit14/15,另拒绝51/64加入select_required_mask和时区越界。
变长输入采用测试侧独立手写长度表及报文字节,不共用Command布局清单。核对规范长度、额外尾字节、SQ、非法状态/CP56及NaN/Inf、带值动作参数、完整确认/撤销/终止回显;已知类型截短由Receiver拒绝,不能误记为Command接手并计rejected。匹配比较值与QU/QL并屏蔽S/E,CP56可不同,浮点+0/−0或跨类型仍失配;51/64数据位及CP56首字节不能被误读为S/E。回复池容量1/4下核对混合长度FIFO、WAIT重试和槽复用后的有效长度。
时标回归覆盖max=0、IV及UTC换算、无基准、等于/小于max、毫秒余数、超前、SU和时区、单调倒退与推算越界、回调内Set/Clear限制、失败设置保留原基准及STOP/断线保留基准。检查决定期间同点丢弃、忙时过期、SELECTED过期执行及寻址错误的顺序,区分time_expired、time_rejected、rejected与dropped_requests。真实回环核对45/46/49/58/59/62完整流程、限值拒绝、操作后总召当前值、47兜底,以及过期58没有应答、事件或动作。
决定期间同点激活的4组专项回归覆盖45/46、SELECT_WAIT与直接/选择后EXECUTE_WAIT、事件已取/未取、同批输入及发送背压。检查相同或不同命令字段均只增加dropped_requests,原事件、动作参数、回显和决定期限不变,不产生抢先的忙否定;另验证到期FINISHED配对、格式校验/type_mask/撤销/其他点忙拒绝,以及同CA重注册、STOP和重连的身份边界。
当前业务测试由99组总召、12组计数量、4组兜底及17组校时/时间接线组成,共132组,单/双服务配置各跑一次。总召在原98组基础上新增双点批次发送WAIT后读取最新值的回归,验证站总召和遥信分组的单点/双点帧序、游标只在提交OK后推进;真实TCP另核对双点遥控后总召读到更新值。计数量覆盖独立实例、请求校验、Type15/37批次边界、忙隔离、冻结/复位、动作重复调用和WAIT/STALE/TEST、业务期限、STOP/断线、共享连接失败、发送背压及多站广播;兜底改用未支持的106/102,覆盖同批与反序/重复内容、窗口与写阻塞、HIGH满丢新及恢复、STOP/STOPSTART取消和活动总召中的应答。校时专项检查独立受理与执行、原文确认、补偿/期限、广播及生命周期;新增1组用mock Clock_Read/Set验证system启动、60秒刷新、失败清基准、校时回调外移交和NULL Command,仍链接真实Clock库,但不设置宿主系统时间。`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: 32 groups passed (replies, identities, stations, lifecycle, backpressure, rejection)`。默认及`-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: 32 groups passed ...`、各`slave example + event interrogation tests: 132 groups passed ...`,以及四组命令专项、`interrogation rules: 3 groups passed ...`和`Slave example loopback: passed ...`。可控后端使用251字节HIGH和2槽召唤业务事件FIFO。真实回环使用默认存储、k=1、随机本机端口,验证总召与计数量交错、冻结/复位及后续读取、分组/广播、校时、命令操作、兜底、STOP/START、重连及正常退出。真实TCP校时使用模拟模式,system接线用mock单测,命令操作内存点表;实际主站、设备输出及宿主系统设时未纳入这组自动回归,不构成完整互操作认证。当前实际通过数量、STOP统计和测量产物见第5节。
## 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`并使用独立输出文件名即可。
### 校时固定存储与栈测量
2026-10-29 Windows x64 GCC默认配置实测:ClockTask为104字节,含4槽的ClockSynchronization实例为568字节,Request为80字节,Status为72字节;默认单服务的校时静态实例数组占568字节,不是完整协议库RAM总量。探针及产物位于`.build/clock-synchronization-20261029/size_probe.c/exe`。
`-O2 -fstack-usage`报告的单函数栈帧:Handle_Event 256、Execute_Next 240、Submit_Head 224、Process 64、Get_Status 80字节;示例main由修改前768增至800字节,Clock_Action为48字节。记录为同目录的`example-before.su`、`example-after.su`及`clock.su`。这些是x64宿主ABI和单函数测量,不是最坏调用链,也不代表ARMCC/MCU的RAM或栈占用;嵌入式接入须按实际工具链重新核算。
### 遥控固定存储与栈测量
下表是2026-10-30的45/46版本历史基线,Windows x64、GCC 16.1.0、`-O2`实测(单位字节);不是扩展后的当前容量:
| 对象 | 应答槽1 | 应答槽4(默认) |
|---|---|---|
| Command实例 | 448 | 616 |
| 单事务槽 | 168 | 168 |
| 单独立应答槽 | 56 | 56 |
| Request / Event / Status | 48 / 88 / 120 | 48 / 88 / 120 |
该历史版本在默认TCP服务容量1时Command静态实例数组为616字节;容量2时实例数组按实例数增长。上述不含既有Slave、TCP和发送队列。探针源文件及可执行程序位于`.build/command-v1/size_probe.c`、`size_probe_1.exe`及`size_probe_4.exe`,探针包含Command实现以测量私有结构,只作本地测量,不增加公开接口。
历史`-fstack-usage`单函数栈帧:Command Handle_Event 208、Accept 128、Submit_Return_Info 144、Submit_Head 144、Process 64字节;示例main由800增至848字节,模拟动作8字节。原始记录为`.build/command-v1/command-stack.su`、`example-before.su`及`example-after.su`。这些是Windows宿主ABI的单函数测量,不是最坏调用链或其他目标工具链的内存预算。
2026-10-31的5a内部泛化阶段仍只启用45/46,但已由14种布局生成21字节最大缓冲。Windows x64实测实例为480/672字节(回复槽1/4),事务槽192、回复槽64,Request/Event/Config/Status仍为48/88/20/120。`-O2 -fstack-usage`单函数帧为Decode_Element 144、Copy_Request 96、Submit_Head 176、Handle_Event 304、Process 64、NextEvent 144、Accept 144、Submit_Return_Info 144字节;修改前同次探针的NextEvent为160字节。最终启用遥调和时标后的数值另记,不能把这一中间阶段当作最终内存预算。
5b增加value并启用47–51后,上述sizeof及栈帧测量均与5a相同;在此x64 ABI中value恰好使用Request原有对齐空隙,不代表其他目标布局不变。该阶段全量构建通过Slave 32组×2、示例131组×2、Command 33组×4、规则3组及真实TCP设定值流程,49执行后的总召读回通过。
5c启用58–64并加入时间基准后的最终Windows x64测量:
| 对象 | 应答槽1 | 应答槽4(默认) |
|---|---|---|
| Command实例 | 512 | 704 |
| 单事务槽 / 单独立应答槽 | 192 / 64 | 192 / 64 |
| Request / Event | 80 / 120 | 80 / 120 |
| Config / Status | 28 / 128 | 28 / 128 |
相对45/46历史基线,每实例增加64/88字节;默认单服务Command静态实例数组为704字节。容量仍由布局清单最大规范长度推导,当前为21,不是接收时分配。最终GCC `-O2 -fstack-usage`单函数帧:Decode_Element 144、Copy_Request 112、Submit_Head 176、Handle_Event 288、Process 64、NextEvent 176、Accept 176、Submit_Return_Info 144字节。公开头C++11严格编译检查通过。以上仍为宿主单函数帧,不是调用链上界;STM32F4须以目标工具链重新测量,不能用这些数字代替目标RAM/栈预算。
本轮产物统一保存在`.build/command-extension-20261031/`,包含修改前`before/`、三个阶段的`5a/`、`5b/`、`5c/`及`RESULTS.txt`。私有故障探针另验证保存原文损坏时的两条收尾路径:未交付工作事件时NextEvent返回ERROR且不产生事件;已交付后Accept返回ERROR、不调用动作或发肯定确认,FINISHED(SERVICE_ERROR)恰好一次,读完后事务槽可复用。
## 5. 验证记录
2026-10-31 遥调与带时标命令:在原Command模块内分5a布局泛化、5b遥调、5c时标三阶段完成45–51/58–64扩展。5a原45/46逐字节回归四配置各27组保持预期通过,5b增加到33组;最终`./scripts/build.ps1 -Test`通过严格GCC构建及全部运行,Slave 32组×2、示例132组×2、Command 45组×4及规则3组均通过,真实Windows TCP回环通过。默认类型仍为45/46,额外类型显式启用;X-macro统一推导规范长度和21字节容量,原文、回复及FIFO使用有效长度。
本轮回环覆盖45/46/49/58/59/62选择、执行、返回信息及ACT_TERM;49/62写入的遥测和58/59更新的遥信由既有总召读回,初值与帧序保持兼容。过期58与106同批发送,核对106应答、后续计数量全部帧和统计,确认没有58应答或动作。STOP实测submitted=83、gi_submitted=11,command_selected=7、command_executed=6、command_completed=6、command_returns=6、command_time_expired=1、command_time_rejected=0。时间边界、IV/SU、时区、基准异常、撤销和匹配顺序由可控时钟专项覆盖;system读时接线使用mock,未设置宿主时钟。
三阶段sizeof及GCC栈帧见第4节;公开头C++11检查、内部原文损坏收尾探针通过。17份Markdown的356个本地链接及锚点、UTF-8/LF、16个公开函数在API文档中的覆盖和diff空白检查均通过。基线快照、阶段产物和RESULTS记录位于`.build/command-extension-20261031/`。所有修改及验证限于IEC60870-Slave目录,未执行父工程Keil构建、目标RAM/栈测量或硬件主站联调;宿主结果不代替这些验收。
2026-10-30 恢复双点遥控地址映射:Type46命令恢复为十进制6101..6108,对应Type3遥信仍为1101..1108。示例帮助、当前说明和回环请求同步更新。原EXE被运行进程占用时首次构建链接失败;用户关闭后,`./scripts/build.ps1`成功更新常规示例EXE,严格GCC构建无诊断。真实TCP回环验证6103选择/执行后返回1103,后续站总召、遥信分组及重连均读到新值,并通过既有单点遥控、撤销、错误应答及生命周期场景。沙箱内首次回环因Node启动子进程EPERM失败,获准在沙箱外重跑相同命令后退出码0。
2026-10-30 双点遥信加入总召:QOI20按Type1、Type3、Type11依次返回各8点,共8帧;QOI21返回单点及双点共6帧,QOI22仅遥测仍4帧。沿用1101..1108及double_values,每批4点、每次发送重试重取值;只新增双点遍历游标,不增加点值数组或发送缓冲。TCP容量1/2下各131组示例C测试通过,严格GCC构建无诊断;真实TCP验证Type46操作1103后,站总召、遥信分组及重连读回均为新值,STOP submitted=83、gi_submitted=11。原示例进程运行中,`./scripts/build.ps1`因无法覆盖被占用的exe而链接失败;随后以相同源文件及编译参数生成`.build/slave-example/iec104_slave_example_double_gi.exe`并通过回环,未中断当前连接。旧进程退出后,常规`./scripts/build.ps1`重跑成功,更新`iec104_slave_example.exe`,退出码0。可控测试产物为同目录`iec104_slave_example_double_test_1.exe`及`_2.exe`。
2026-10-30 恢复单点遥控地址映射:Type45恢复6001..6008控制Type1遥信1001..1008;Type46仍使用1101..1108。`./scripts/build.ps1`严格GCC构建无诊断;沿用已获准的真实TCP回环命令,验证6001选择/执行后返回1001及总召新值、1001作为Type45命令时回47、双点1103返回1103,并通过原有撤销、错误应答和生命周期场景,退出码0。示例帮助、说明和回环期望同步更新。
2026-10-30 示例遥控地址对齐遥信:Type45改用1001..1008,Type46改用1101..1108,命令与返回信息使用同一IOA。严格GCC构建无诊断,Slave单/双服务各32组、既有业务各130组、遥控四配置各27组及3组规则测试通过。真实TCP核对新地址的选择/执行/返回/终止、撤销及单点操作后的总召新值,旧地址6001回47。`./scripts/build.ps1 -Test`末尾回环受沙箱子进程EPERM限制,首次整体退出码1;获准单独重跑原回环命令后退出码0。帮助文本、示例说明和回环断言同步更新,协议库点址校验保持通用。
2026-10-30 遥控决定窗口同点激活:SELECT_WAIT/EXECUTE_WAIT内,通过校验且同会话、同站点注册编号、同IOA的激活只计dropped_requests并HANDLED,不覆盖请求、不刷新期限、不生成新事件或忙否定。新增4组回归,TCP容量1/2与回复槽1/4的四组合各27组通过;`./scripts/build.ps1 -Test`的严格GCC构建无诊断,Slave单/双服务各32组、既有业务各130组及3组规则测试通过。脚本末尾真实TCP回环因沙箱阻止Node子进程而报EPERM,首次整体退出码1;随后获准单独重跑原回环命令,退出码0,真实TCP全流程通过。Command公开头C++11严格语法检查通过,文档链接与UTF-8/LF检查通过;未改公开接口或固定存储容量,修改仅限IEC60870-Slave。
2026-10-30 Type45/46遥控:新增独立单事务模块、固定回复池、选择/执行/撤销、显式动作与设备结果、原连接返回信息及全局FIFO收尾。`./scripts/build.ps1 -Test`以GCC `-std=c11 -O2 -Wall -Wextra -Werror`构建无诊断,Slave单/双服务各32组、既有业务单/双服务各130组、遥控四配置各23组和3组召唤规则测试全部通过;真实Windows TCP回环通过45/46完整流程、撤销、TEST/地址拒绝、未支持类型、总召新值及原有计数量/校时/启停重连,脚本最终退出码0。沙箱内首次Node启动因EPERM失败,随后在获准环境重跑完整原脚本通过。校时使用模拟模式,遥控操作模拟点。修改前84个文件的快照、容量测试、sizeof探针及栈记录保存在`.build/command-v1/`;所有实现与验证均限定在IEC60870-Slave目录。
2026-10-29 Type 103时钟同步:新增独立静态池模块、显式Execute_Next动作、64位受理后滞留补偿、执行期限、原文ACT_CON及广播快照确认;示例默认模拟,显式system模式才设置Windows时间。整合`build.ps1 -Test`中GCC `-std=c11 -O2 -Wall -Wextra -Werror`构建无诊断,Slave单/双服务各32组、业务单/双服务各130组(98总召、12计数量、4兜底、16校时)及3组规则测试均通过。脚本内首次Node启动因沙箱`spawnSync EPERM`失败,因此该次脚本整体不是成功退出;随后获准单独重跑相同Node回环命令,退出码0,真实本机TCP通过,核对submitted=65、clock_executed=3、clock_synchronized=3、clock_submitted=5。默认模拟校时的正常/TEST/COT=5/广播及103+102同批均覆盖,未执行system模式或真实设备设时。改前基线、大小探针及栈测量位于`.build/clock-synchronization-20261029/`。
2026-10-28 业务目录整理:共用召唤引擎、内部规则头、总召和计数量共7个源码/头文件移到`104/business/`,迁移前后SHA256逐字节一致;构建加入该目录的头文件搜索路径,同步测试直接包含路径及文档源码链接。严格GCC构建通过,单/双服务各110组业务测试、23组Slave测试及3组规则测试通过,公开头C++检查和真实Windows TCP回环通过。沙箱限制Node启动子进程时,回环使用原授权命令在沙箱外执行。迁移清单及备份位于`.build/business-layout-20261028/`,修改仅限IEC60870-Slave;未执行Keil目标构建或硬件联调。
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,46 @@
Explicit example command points - 2026-10-31
Scope: IEC60870-Slave only. No commit, parent firmware build, or hardware test.
Change
Example_Init_Command now lists all 24 command points explicitly.
Every row specifies runtime CA, literal IOA, allowed types, and execution mode.
No loop or last-in-group condition generates the point configuration.
Type mask is explicitly assigned to 45/46/49/58/59/62.
--ca remains supported; the existing CA=7 integration test is unchanged.
6008/6108/6308 remain DIRECT; the other 21 points remain SELECT_EXECUTE.
The local initializer is copied to the existing app.command_points array.
Command borrows the stable app array, never the automatic initializer.
A static assertion checks initializer and destination sizes.
Point count comes from sizeof(initializer) / sizeof(initializer[0]).
example_command_groups remains only for application runtime-value lookup.
Example and test documentation were synchronized; library APIs are unchanged.
Verification
./scripts/build.ps1 -Test: PASS, exit 0, no warnings/errors.
GCC flags: -std=c11 -O2 -Wall -Wextra -Werror.
Command: 53 groups x 4 TCP/reply capacity configurations.
Example: 133 groups x 2 TCP capacities; all 24 point rows checked at CA=7.
Slave: 32 groups x 2 TCP capacities.
Interrogation rules: 3 groups.
Real Windows TCP loopback: PASS, including direct/SBO policies, all six
command types, returns/termination, timestamp checks, changed GI values,
cancellation, fallback, counters, STOP/START, reconnect, and cleanup.
Existing test inputs and expectations were not changed; no new tests added.
git diff --check -- .: PASS.
Modified source/docs: UTF-8, LF, no BOM, no replacement characters.
Memory evidence (Windows x64 GCC -O2; not a target firmware budget)
Existing ExampleApp and its persistent point array retain their layout.
The automatic initializer contains 24 x 16 = 384 bytes of point data.
Actual -fstack-usage: Example_Init_Command is inlined in main in both builds.
main: 864 bytes before and after; Example_Command_Point: 40 bytes both.
This optimization does not guarantee identical stack use on other compilers.
Object size: before after
text 12832 13584
data 0 0
bss 1856 1856
Original evidence: example-before/after.o and example-before/after.su.
Snapshots
before/ and after/ contain the example source and the two updated documents.
@@ -0,0 +1,190 @@
# 104子站示例
[iec104_slave_example.c](../../examples/iec104_slave_example.c) 包含模拟点表、业务事件处理、其他命令兜底、事件循环及main。总召状态机使用独立库模块[IEC104_Interrogation.c/h](../../104/business/IEC104_Interrogation.h),不在示例内重复实现。
当前实现站总召(`C_IC_NA_1`,QOI=20),依次返回8个单点遥信、8个双点遥信和8个标度化遥测值;分组1(QOI=21)返回单点及双点遥信,分组2(QOI=22)只返回遥测。库支持全部16组,示例未配置的其余组由应用拒绝。通信通过`IEC104_Slave`,总召通过`IEC104_Interrogation`接入,所有任务使用固定存储、在主循环线程串行推进。示例另含计数量召唤、Type 103校时、45/46遥控、49标度化设定值及58/59/62带时标命令;默认时钟为模拟值,命令只更新内存点值,实际设备数据源和物理输出仍由应用接入。
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。
主循环向总召、计数量、校时、遥控实例广播生命周期通知,读空Slave通知后优先推进遥控,再交替推进召唤业务并显式执行校时;其他类型在四个模块均未接手后兜底。原Example_Init仍可用于仅总召测试,命令行入口分别调用计数量、校时及Example_Init_Command补齐实例,两个召唤实例各需独立广播状态区,校时使用共用站点快照。协议及应用动作限制见[计数量设计](../104/IEC104_CounterInterrogation_设计架构.md)、[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)和[遥控设计](../104/IEC104_Command_设计架构.md)。
在项目根目录的 PowerShell 中执行,需要 MinGW GCC:
示例通过 `IEC104_Interrogation.h`、`IEC104_CounterInterrogation.h`、`IEC104_ClockSynchronization.h` 和 `IEC104_Command.h` 引入业务声明;构建脚本添加 `104/`、`104/business/` 和 `common/` 头文件搜索路径,编辑器也应配置这三个目录。构建可执行程序仍使用下面的脚本,它会链接所需库源文件、Clock适配及Windows网络库;只编译当前C文件会缺少库函数实现。编辑器的C语言标准应与构建保持一致,使用C11,并选择实际的MinGW GCC编译器。
```powershell
./scripts/build.ps1
./.build/slave-example/iec104_slave_example.exe
```
默认监听 `127.0.0.1:2404`,公共地址 CA=1;按 Ctrl+C 退出并关闭服务。也可以指定监听地址、端口和公共地址:
```powershell
./.build/slave-example/iec104_slave_example.exe --bind 0.0.0.0 --port 2404 --ca 1
```
| 参数 | 含义 |
|---|---|
| `--bind IPv4` | 数字IPv4监听地址,默认127.0.0.1 |
| `--port 0..65535` | 默认2404;0由系统分配,启动时输出实际端口 |
| `--ca 1..65534` | 本站公共地址,默认1;总召请求也接受65535,广播请求的应答使用本站地址 |
| `--window 1..32767` | 发送窗口k,默认12;设为1可观察逐帧ACK后续发 |
| `--clock simulated\|system` | 默认simulated,只更新示例模拟时钟;system显式调用Windows系统设时接口 |
| `--run-ms N` | 运行约N毫秒后正常关闭;默认0表示持续运行 |
| `--help` | 打印用法后退出 |
启动输出 `LISTEN 地址:端口 CA=站地址`。退出保留原有STOP统计字段:`submitted`累计总召与兜底已接手ASDU,`gi_submitted`为已提交完毕的总召数,二者均不表示主站已经ACK。`waits`只统计总召发送WAIT;兜底HIGH满时丢新,计入`overloads`和`dropped_requests`,不记录待重试次数。兜底ERROR/FATAL计入`cancelled`,其中FATAL可能已写出字节,该计数不表示确定未发送。拒绝、主站撤销及业务超时统计继续保留,计数量另见`counter_submitted`和`counter_completed`。末尾校时字段`clock_executed`为实际action调用数、`clock_synchronized`为action返回true次数、`clock_submitted`为已交接校时ASDU数,包含错误及否定应答,广播按各提交目标分别计数;不混入原字段。
命令统计追加`command_selected`、`command_executed`、`command_completed`、`command_submitted`和`command_returns`,分别表示已接受的选择、已启动的执行、成功完成事务、已交接应答和返回信息帧数;`command_time_expired`统计过期激活,`command_time_rejected`统计无效时间、无基准、推算失败或超前造成的时间否定。静默过期不会增加选择/执行次数;保留原有统计字段的含义。
### Type 103校时
主站发送Type=103、SQ=0、对象数=1、COT=6、P/N=0、TEST=0、CA匹配或65535、IOA=0及有效CP56。默认按主站UTC+8标准时间解释,SU由时间工具处理;模块补偿受理至执行检查的等待。应用显式Execute_Next后回COT=7,原CP56字节原样回显。TEST否定7且不执行;COT=5读时否定45,不返回本站当前时间。
默认`--clock simulated`把请求的utc_ms/base_ms保存为模拟时钟基准,不改变系统时间。`--clock system`通过`IEC60870_Clock_Set(&request->local_time)`设置Windows时间;local_time已是固定UTC+8且已补偿,示例不再手工换时区或加等待。Clock内部转换为UTC后调用系统接口,权限不足时动作报告失败并回否定ACT_CON。真实TCP自动回归只运行模拟模式;system接线另用mock时钟单测,不设置宿主系统时间。
两种模式在成功校时动作中都记录utc_ms/base_ms及待更新标志;`Example_Drain_Clock`在Execute_Next返回后、回调之外把基准交给Command。system模式还在启动及每60秒、请求分派之前读取Clock,读成功且IV=0时按固定UTC+8转换(Clock读值不叠加夏令时),关联同一时刻的Net单调毫秒;读取/换算/设置基准失败或IV=1时清除Command基准。仅总召测试未创建Command,相关接线先检查空句柄。
确认发送WAIT时只重试原确认,不再次设时;STOP取消未交接确认而不撤销已完成的动作。同内容的新请求会再执行,旧时标重发可能回拨。真实数据发布方应在confirm_pending期间暂停新的带时标上报并继续驱动网络,避免持续新增NORMAL推迟确认。完整范围及期限见[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)。
### 遥控、设定值与带时标命令
`Example_Init_Command`在Config_Default后用赋值把全局type_mask裁剪为已接入的45、46、49、58、59、62,并提供24行命令点表。库默认全局启用14种命令(0x3FFF),但points=NULL、point_count=0;未提供具体点不会执行命令。示例不能用`|=`裁剪全类型默认值。决定期限15秒、选择保持30秒、执行期限60秒,成功发送ACT_TERM,失败默认不发。整个实例只允许一个事务,所有站点的新命令互斥;TEST请求否定,CA=65535回UNKNOWN_CA,未配置命令IOA的新激活走到点检查时由模块回47。
`Example_Init_Command`用局部const点表逐行明确列出24个点的CA、IOA、允许类型和模式,不通过循环或末点判断生成配置。CA取运行时common_address,保留`--ca`支持;各行按CA/IOA升序,只允许对应的基础类型及带时标类型。6008、6108、6308为DIRECT,其余21点为SELECT_EXECUTE。Create之前用memcpy将该表复制到ExampleApp拥有的command_points稳定数组,config.points指向该数组,point_count取显式表的元素数;模块不会借用局部数组。稳定数组从Create至Destroy保持只读,三组地址描述仅负责设备运行值映射。
| 命令 | 受控点及返回信息 | 模式 | 初值与总召 |
|---|---|---|---|
| Type45/58,IOA6001..6008 | Type1,IOA1001..1008,COT=11;状态0分/1合 | 6001..6007先选择,6008直接执行 | 初值见下表,执行后总召读取更新值 |
| Type46/59,IOA6101..6108 | Type3,IOA1101..1108,COT=11;状态1分/2合 | 6101..6107先选择,6108直接执行 | 初值全部为1;执行后站总召及遥信分组读取更新值 |
| Type49/62,IOA6301..6308 | Type11,IOA2001..2008,COT=11;写入标度化值 | 6301..6307先选择,6308直接执行 | 初值见下表,执行后站总召及QOI=22读取更新值 |
表中IOA均为十进制。单点命令6001..6008分别对应遥信1001..1008;双点命令6101..6108分别对应返回位置1101..1108。例如对Type46、IOA6103完成状态2的选择与执行后,后续QOI=20或21读取的Type3、IOA1103为2;FINISHED清理不会把它恢复为初值1。
SELECT_EXECUTE点先发送S/E=1选择,收到肯定ACT_CON后再发S/E=0执行,均使用COT=6;执行的类型、IOA、OA、值及QU/QL须与选择一致,带时标命令可使用不同时标。DIRECT点只发送S/E=0执行,不接受选择;其SELECT请求回否定7,SELECT_EXECUTE点未选择便执行也回否定7。点类型不匹配同样回否定7,均不会调用应用动作。执行依次返回ACT_CON、返回信息、ACT_TERM。撤销使用COT=8,同类型/IOA/OA匹配,不要求值相同。示例未启用47、48、50、51、60、61、63、64,仍由兜底回44;这些类型由模块专项测试覆盖。
49/62允许的示例标度值为−10000..10000(包含端点),选择和执行都检查限值,越限调用Reject(NEGATIVE),动作内另作守卫。这个范围属于模拟点表策略,不是协议或Command模块的统一限值。比如对6301设为−1234,Type11返回信息及后续QOI=20/22的2001均为−1234;总召对象数与帧序不变。
58/59/62的CP56按主站UTC+8及SU解释,默认时差上限10秒,IV=1拒绝,时差恰好10秒也拒绝或丢弃。simulated模式启用时尚无可信基准,须先成功校时;system模式可由启动读时建立基准。过期激活默认静默丢弃,不占事务槽或调用动作;无基准、超前及换算失败回否定7。只在受理时检查时效,默认决定期限内继续等待可能使启动动作时的命令年龄接近25秒。撤销仍检查CP56格式,但不判时差。
`Example_Drain_Command`先读取SELECT/EXECUTE/FINISHED;首次保存事务context前Acquire应用站点引用。点表、动作和返回信息按基础类型分派;`Example_Command_Action`模拟返校或修改`ExampleApp.signal_values/double_values/scaled_values`并返回STARTED,不在动作中上报。Type11单对象返回需要12字节缓冲。Accept的OK只表示事件已消费,示例通过Get_Transaction确认EXECUTING后,依次Report_Result(SUCCEEDED)、Example_Build_Command_Return、Submit_Return_Info和Complete;设备结果可以早于确认交接。
每一步WAIT都保留原事件和当前步骤;返回信息使用独立ASDU缓冲,WAIT时保留同一帧,OK后才推进,避免重复提交。STALE停止该事务重试,继续读取FINISHED。FINISHED清除模拟返校/输出标志并Release应用引用,不恢复已修改的点值;真实输出若仍未停止,应用必须继续保持设备忙状态和引用。Example_Wait_Time同时计入遥控期限、模块推进和应用挂起重试,发送WAIT时允许等待网络而不空转。
## 主站操作和模拟点表
主站先建立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 |
| 1101..1108 | 双点信息 `M_DP_NA_1`,Type=3 | 1、1、1、1、1、1、1、1 |
| 2001..2008 | 标度化测量值 `M_ME_NB_1`,Type=11 | 2300、2310、2290、5000、-125、0、1234、42 |
所有品质为0。遥测值为原始有符号16位标度值,本示例没有定义物理单位和倍率。
一次成功站总召依次提交8个ASDU:
1. Type=100,COT=7,肯定激活确认。
2. 两帧Type=1、COT=20的单点数据,每帧4个对象。
3. 两帧Type=3、COT=20的双点数据,每帧4个对象。
4. 两帧Type=11、COT=20的标度化遥测,每帧4个对象。
5. Type=100,COT=10,激活终止。
分组1(QOI=21)共提交6个ASDU:ACT_CON、两帧Type=1单点数据、两帧Type=3双点数据、ACT_TERM,数据COT均为21,每帧4个对象。分组2(QOI=22)仍共提交4个ASDU:ACT_CON、两帧Type=11/COT=22遥测、ACT_TERM,每帧4个对象。确认和终止保留请求QOI。BEGIN校验分组,首个NEED_DATA按该组初始化应用游标,不把模拟点表写进库。
全部应答使用原请求的连接身份。总召确认、终止及短应答走普通HIGH路径,总召数据由Submit_Batch尝试不排队提交;校时调用过action的确认也使用Try。遥控执行动作产生副作用后的确认、返回信息和ACT_TERM均走Try,选择确认及校验否定走普通HIGH。未提交HIGH/NORMAL、当前帧或窗口背压时Try返回WAIT;遥控的模块FIFO还会保留更早应答优先。总召数据和遥控返回信息不使用Enqueue_Event,只有本帧接手后才推进应用步骤。
主站撤销使用相同的实际站CA/OA/TEST/QOI,COT改为8、P/N=0。激活和撤销均接受本站地址或65535,匹配时65535映射为本站地址,允许广播激活配本站撤销及本站激活配广播撤销。尚有匹配总召时模块停止任务、回复肯定COT=9,并通过FINISHED(DEACTIVATED)通知应用;不匹配或已完成则否定COT=9。已提交发送层的帧可能仍在确认前发出,撤销后不再提交旧批次和ACT_TERM。示例点数很少,总召可能在主站发撤销前已全部提交;观察中途撤销和各阶段拥塞可使用下面的可控网络测试。
广播总召的BEGIN/NEED_DATA/FINISHED携带本站CA,示例直接用该字段构造数据。确认、数据、终止、否定及错误应答的CA都为本站地址,OA/TEST/QOI保留原请求值;原始Slave事件不被修改。该支持属于Type100总召处理器,其他类型仍按各自业务规则分派。
## 请求校验及任务处理
| 情况 | 本示例的处理 |
|---|---|
| 不支持的Type ID | 同步调用Reject_Request回显原文,COT=44、P/N=1;HIGH满时丢新并计数,不因连续请求断链 |
| 总召COT不是6或8 | COT=45、P/N=1 |
| 总召CA既不是本站地址也不是65535 | COT=46、P/N=1,保留原错误CA |
| 总召IOA不是0 | COT=47、P/N=1 |
| 激活QOI超出20..36、请求P/N非法或当前已有未终结总召 | 否定ACT_CON,COT=7、P/N=1 |
| 空闲时收到合法激活请求 | 有空槽则建立唯一总召;BEGIN后应用接受才发肯定确认,忙时不同QOI也拒绝 |
| 激活QOI为23..36 | 合法请求交给应用,示例因未配置点表而Reply_Begin(false),回否定ACT_CON |
| 撤销不匹配、QOI超范围或P/N非法 | 否定DEACT_CON,COT=9、P/N=1 |
| 匹配的主站撤销 | 取消对应总召,肯定DEACT_CON,COT=9、P/N=0 |
| 总召唯一对象完整,但SQ=1或有多余尾字节 | 回否定7/9,COT/CA/IOA错误优先45/46/47;不执行激活或撤销 |
| 总召零对象/多对象等无法可靠应答的布局错误 | 计入rejected并丢弃;保留连接和已有总召 |
| 需要新增总召且4个任务槽已占满 | 尝试否定ACT_CON;无法安全提交/保存则丢新请求并计数,保留连接与原任务;匹配撤销复用原槽 |
| 接受/取数/整体业务期限到点 | 肯定ACT_CON提交前回否定确认并保留连接,提交后关闭原连接;已取出BEGIN才交付对应TIMEOUT终结事件 |
Example_Handle_Event先过滤asdu_result失败的I帧视图,避免进入业务分派或未知类型兜底;正常线上基础头/载荷截短已由Receiver和连接层处理。当前I帧依次交给总召、计数量、校时及Command;都未接手时,在读取下一事件前同步调用`IEC104_Slave_Reject_Request`回UNKNOWN_TYPE。该接口借用当前事件生成原文错误应答,复用库的普通HIGH路径;示例不保存other_request,也没有other_pending、other_waiting或other_waits。未启用类型的连续及相同内容有效新请求逐条尝试应答,允许穿插正在进行的总召。103由校时接手,45/46/49/58/59/62由Command接手,102、47及106仍回44;模块之间不承诺跨业务应答按接收顺序排列。
OK表示已接手;WAIT只因HIGH容量不足,示例丢弃本条并增加overloads/dropped_requests,保持连接、已有HIGH内容及当前业务。ERROR按失效请求处理并继续读通知;FATAL可能已写出字节,不得重试,继续排空收尾通知。接口不修改输入事件或其最近提交结果,不能通过Get_Last_Submit_Result读取本次返回值。未读启停通知会阻止本次应答;状态计数不能追溯已消费的STOP/START,禁止先保存事件再读完后续通知后调用。需要异步业务应答时另用Copy_Event、Check_Request并自行处理STOP取消,完整契约见[同步拒绝当前请求](../104/IEC104_API接口汇总.md#reject-request)。
校验从类型开始;已知总召先检查是否有唯一完整对象,再按COT、CA、IOA、布局/QOI/P/N及事务状态决定。畸形单对象的否定副本规范为10字节、VSQ=1,保留OA/TEST/IOA/QOI,不修改原输入。广播应答使用实际站CA。错误请求不取消原总召或刷新期限;否定应答沿现有队列重试,满槽只丢新请求。这里只实现上述命令集合,不代表所有类型的业务规则。
`Example_Drain_Business`主动读取总召业务事件:BEGIN按QOI选点表,Reply_Begin接受或拒绝请求;NEED_DATA调用普通辅助函数Example_Build_Batch构造数据,再Submit_Batch,数据耗尽则Complete_Data;FINISHED按实例/请求编号清理对应资源,撤销和超时也走该分支。没有向库注册业务函数指针。数据源暂未就绪时可保存事件,后续就绪再应答;发送WAIT也保留事件,下一次调用重新组包,模块不缓存本批原文或重复生成NEED_DATA。
本例BEGIN只判断接受,首个NEED_DATA初始化游标并读取这一批;接真实数据源时,在Example_Build_Batch的逐点组包处读取当前值和品质。IOA、类型、分组成员及遍历策略均由调用者决定,协议库不登记或冻结完整IOA列表。单点和遥测常量表保存地址、初值及品质,双点初值均为1;实际值分别存放在ExampleApp.signal_values、double_values和scaled_values中,站总召及对应分组逐批读取这些可写数组的当前值,包括遥控及设定值执行后的更新;这些地址不是库规定的范围。逐批采样不保证整轮同一时刻,也不自动解决与突发上报的新旧排序;完整边界见[总召模块设计](../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成功代替资源清理。
遥控同样执行`Command_Cancel → 读完FINISHED → Slave_Close → 读空并分派通知 → Command_Destroy`。示例先取消全部业务并清理应用引用,再关闭Slave;关闭后的生命周期通知仍交付全部模块。遥控FINISHED可以在离线时读取,事务槽及独立应答清空后才允许Destroy,最后移除站点并销毁Slave。
## 验证
```powershell
./scripts/build.ps1 -Test
```
`-Test` 需要预先安装 Node.js。脚本运行Slave/站点测试、总召/计数量/兜底/校时可控网络测试、规则单测、遥控容量矩阵和真实本机TCP测试;实跑数量及记录见[测试说明](../test/IEC104测试文档.md):
- `iec104_slave_test.c`:站点注册/移除、延迟回收、重复CA、13站共享服务、CA/context隔离、注册编号防替换、快照增删及嵌套集合锁、关闭重启保留,以及原有应答、事件身份和背压回归。Reject_Request另覆盖原文回显、非法参数、连续请求、窗口背压、HIGH满、启停门禁、旧会话及FATAL。
- `iec104_slave_example_test.c`:完整报文、部分写入、各发送阶段HIGH满与WAIT、请求校验、STOP/重连、非法请求、传输通知预算及API生命周期;业务事件覆盖延迟应答、批次校验、混合类型、容量边界、拒绝/失败/取消、终结预留和实例复用。总召覆盖全部16组、各阶段撤销、三项期限、迟到应答及通知优先。调度覆盖短应答立即提交及顺序保护、非队首槽复用、满槽否定或计数丢弃且当前任务有效、各阶段相同/不同QOI均忙拒绝、结束后重发成功、撤销对象匹配、停止清理、BEGIN前等事件空位及BEGIN后完整执行期限,并验证忙状态不拦截Type101总电度请求。测试设置HIGH=251字节、业务事件FIFO=2槽,使用真实容量背压限制发送。
- `iec104_command_test.c`:独立可控Net/单调时钟测试,TCP服务容量1/2和独立应答槽容量1/4交叉构建,HIGH=251字节;覆盖命令点表校验与按点模式、14种命令布局、值和CP56校验、选择执行、匹配、时效及时间基准、变长FIFO、返回信息、动作结果、期限和FINISHED/引用生命周期。
- `slave_example_loopback_test.cjs`:启动实际可执行程序,使用Windows网络后端和127.0.0.1随机端口,以独立主站字节验证分片请求、k=1时逐帧ACK、站总召、忙拒绝及结束后重发、撤销、错误应答、106与102同批分别回44、STOP/START、重连和正常退出。校时核对正常ACT_CON、TEST否定7、读时COT=5否定45、广播实际CA,以及103与102同批时先102回44、再103回7。命令核对45/46/49/58/59/62完整流程,以及6008/6108/6308直接执行、直接点SELECT否定、未选执行及点类型不符否定;返回信息在确认和终止之间,另有越限/TEST/地址拒绝、撤销、47兜底及总召新值。正常带时标命令由最近成功校时和测试单调经过时间推算时标;过期58与106同批,结合后续全部计数量帧、动作/选择计数和time_expired验证没有58应答。服务约12秒后自行关闭,使用默认模拟时钟。
原114组基线由98组总召、12组计数量及4组兜底回归组成。兜底改用未支持的106/102,覆盖同批及反序、相同内容的有效新请求、k=1与写阻塞排队、HIGH满后丢新且不补发、STOP/STOPSTART取消,以及总召未结束时立即提交错误应答。启用校时的独立回归另外核对103业务。
17组校时及时间接线回归覆盖无站广播45/46、同批及重复103逐条执行、FIFO中间否定、满槽丢新、Try等待不重执行、STOP/重连、Defer/FATAL、同一次采样检查期限及补偿、action内Get_Status与防重入、广播WAIT期间删除当前站及同CA替代不加入。其中1组只替换示例的Clock_Read/Set,验证system启动、60秒刷新、失败清基准、校时成功后回调外移交及NULL Command;Clock库仍正常链接,所有动作均不设置宿主系统时间。另有1组命令点表接线回归,检查24点、可配置CA及混合执行模式。示例测试合计133组(99总召、12计数量、4兜底、17校时/接线、1命令接线),本轮实际验收见[测试记录](../test/IEC104测试文档.md#5-验证记录)。
畸形请求回归覆盖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、撤销组合及连续兜底;当前ASDU数量及STOP统计见[测试记录](../test/IEC104测试文档.md#5-验证记录),计数量和命令独立计数。5个校时应答中3次执行并成功,另2次为TEST及COT拒绝;多CA由可控Net回归覆盖。
这些检查覆盖本示例的模拟点表流程;实际主站设备、现场点表和设备执行接口仍需联调。
@@ -0,0 +1,351 @@
# IEC104 测试说明
> **目录与命令同步日期**:2026-10-29。测试代码位于根目录的 `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后端,共32组统一入口测试,覆盖ASDU视图、共用发送分派、确认/终止字节、HIGH背压、非法参数、原连接身份、延迟复制、服务复用、生命周期、外部调度及离线上报。站点测试覆盖13节点、CA/context隔离、删除即时退出路由与引用延迟回收、同CA替代节点的注册编号、快照期间增删/嵌套/结束回收、显式集合锁及跨服务误用;新增9组同步拒绝回归。共用层非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)。
同步拒绝接口回归检查COT=44..47的原文回显、TEST/OA/CA/VSQ及对象字节保留、输入事件和最近提交结果不变、非法参数及原服务/会话隔离。未读启停通知拒绝提交;已消费STOP/START后的旧事件不在同步接口允许的调用范围内,不能用状态计数为0证明其仍有效。示例回归覆盖连续及重复内容的新命令、HIGH真满时丢新且保留连接/旧应答、活动总召中的兜底、启停与发送故障收尾;原同批兜底回归继续保留。接入校时后的真实TCP对103与102同批发送分别核对ACT_CON和COT=44,不保证跨业务响应顺序。接口与丢新语义见[同步拒绝当前请求](../104/IEC104_API接口汇总.md#reject-request)。
校时测试使用模拟action及可控单调时钟,不设置Windows时间。覆盖Handle/Process不执行应用、显式执行一次、原CP56回显、COT/CA/IOA校验优先级、TEST/IV/非法CP56拒绝、SU/时区转换、受理起滞留补偿、日期上界和64位运算、期限与倒退、Try背压不重复动作、FIFO/满槽、STOP/断线/旧会话、广播快照及删除目标,以及action内只读状态。真实设备写入耗时、权限及失败后的硬件状态不由这些模拟测试证明,边界见[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)。
## 2. 编译与运行
命令专项入口为`test/iec104_command_test.c`,复用TCP事件环测试的可控Net后端,但独立驱动Slave和Command,不依赖示例的应用状态。`./scripts/build.ps1 -Test`同时构建和执行TCP容量1/2 × 命令独立应答槽1/4的四种组合,每种53组(原46组迁移及7组按点回归);原45/46基线为每种27组。库源文件须包含`104/business/IEC104_Command.c`,测试本身包含可控TCP实现,不再额外链接Windows Net或第二份TcpServer。
命令原有覆盖45/46选择和直接执行、匹配/忙拒绝/撤销、三类动作结果、成功失败及终止配置、零/多帧返回信息、完整ASDU校验、全局FIFO、回复池满、窗口/部分写入、结果先于确认、重复及迟到事件、FINISHED配对、三个期限和时钟倒退(含期限配置0)、STOP提交竞态/FATAL、NORMAL覆盖反例、站点删除重注册、引用和单/双服务生命周期。扩展覆盖47–51/58–64;当前默认配置检查全局type_mask=0x3FFF及points=NULL、point_count=0。未启用47/58返回IGNORED的用例显式裁剪全局掩码,直接/选择执行由各点模式配置;非法配置使用保留bit14/15,另拒绝SELECT_EXECUTE点包含51/64、非法点表和时区越界。
全局默认掩码的14种完整流程测试提供显式点表:51/64配置DIRECT,其他类型配置SELECT_EXECUTE;7种带时标类型先验证无时间基准时否定,再设置基准走完流程。另单独检查原样默认空表不会执行点,避免把全局类型启用误认为已经配置命令点。
新增7组按点回归:原样默认空表逐14型;17项非法配置覆盖NULL/count配对、元素数乘法上限、CA/IOA范围、严格排序/唯一性、非法模式/点掩码及51/64模式限制;同类型不同点混合DIRECT/SELECT_EXECUTE及CA隔离;全局/点掩码交集及Config按值复制、点表只读借用;查点早于忙、决定期间同点静默/SELECTED跨类型MISMATCH/时效先于点策略;二分查找首中末、空隙及CA/IOA边界;选择超时、STOP和站点重新注册后的策略及身份隔离。完整实跑结果见最新验证记录。
变长输入采用测试侧独立手写长度表及报文字节,不共用Command布局清单。核对规范长度、额外尾字节、SQ、非法状态/CP56及NaN/Inf、带值动作参数、完整确认/撤销/终止回显;已知类型截短由Receiver拒绝,不能误记为Command接手并计rejected。匹配比较值与QU/QL并屏蔽S/E,CP56可不同,浮点+0/−0或跨类型仍失配;51/64数据位及CP56首字节不能被误读为S/E。回复池容量1/4下核对混合长度FIFO、WAIT重试和槽复用后的有效长度。
时标回归覆盖max=0、IV及UTC换算、无基准、等于/小于max、毫秒余数、超前、SU和时区、单调倒退与推算越界、回调内Set/Clear限制、失败设置保留原基准及STOP/断线保留基准。检查决定期间同点丢弃、忙时过期、SELECTED过期执行及寻址错误的顺序,区分time_expired、time_rejected、rejected与dropped_requests。真实回环核对45/46/49/58/59/62完整流程、限值拒绝、操作后总召当前值、47兜底,以及过期58没有应答、事件或动作。
决定期间同点激活的4组专项回归覆盖45/46、SELECT_WAIT与直接/选择后EXECUTE_WAIT、事件已取/未取、同批输入及发送背压。检查相同或不同命令字段均只增加dropped_requests,原事件、动作参数、回显和决定期限不变,不产生抢先的忙否定;另验证到期FINISHED配对、格式校验/type_mask/撤销/其他点忙拒绝,以及同CA重注册、STOP和重连的身份边界。
当前业务测试由99组总召、12组计数量、4组兜底、17组校时/时间接线和1组命令点表接线组成,共133组,单/双服务配置各跑一次。总召在原98组基础上新增双点批次发送WAIT后读取最新值的回归,验证站总召和遥信分组的单点/双点帧序、游标只在提交OK后推进;真实TCP另核对双点遥控后总召读到更新值。计数量覆盖独立实例、请求校验、Type15/37批次边界、忙隔离、冻结/复位、动作重复调用和WAIT/STALE/TEST、业务期限、STOP/断线、共享连接失败、发送背压及多站广播;兜底改用未支持的106/102,覆盖同批与反序/重复内容、窗口与写阻塞、HIGH满丢新及恢复、STOP/STOPSTART取消和活动总召中的应答。校时专项检查独立受理与执行、原文确认、补偿/期限、广播及生命周期;1组用mock Clock_Read/Set验证system启动、60秒刷新、失败清基准、校时回调外移交和NULL Command,仍链接真实Clock库,但不设置宿主系统时间。命令接线验证24个点、可配置CA及三组末点直接执行。`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: 32 groups passed (replies, identities, stations, lifecycle, backpressure, rejection)`。默认及`-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: 32 groups passed ...`、各`slave example + event interrogation tests: 133 groups passed ...`,以及四组命令专项、`interrogation rules: 3 groups passed ...`和`Slave example loopback: passed ...`。可控后端使用251字节HIGH和2槽召唤业务事件FIFO。真实回环使用默认存储、k=1、随机本机端口,验证总召与计数量交错、冻结/复位及后续读取、分组/广播、校时、混合点模式的命令操作、兜底、STOP/START、重连及正常退出。真实TCP校时使用模拟模式,system接线用mock单测,命令操作内存点表;实际主站、设备输出及宿主系统设时未纳入这组自动回归,不构成完整互操作认证。当前实际通过数量、STOP统计和测量产物见第5节。
## 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`并使用独立输出文件名即可。
### 校时固定存储与栈测量
2026-10-29 Windows x64 GCC默认配置实测:ClockTask为104字节,含4槽的ClockSynchronization实例为568字节,Request为80字节,Status为72字节;默认单服务的校时静态实例数组占568字节,不是完整协议库RAM总量。探针及产物位于`.build/clock-synchronization-20261029/size_probe.c/exe`。
`-O2 -fstack-usage`报告的单函数栈帧:Handle_Event 256、Execute_Next 240、Submit_Head 224、Process 64、Get_Status 80字节;示例main由修改前768增至800字节,Clock_Action为48字节。记录为同目录的`example-before.su`、`example-after.su`及`clock.su`。这些是x64宿主ABI和单函数测量,不是最坏调用链,也不代表ARMCC/MCU的RAM或栈占用;嵌入式接入须按实际工具链重新核算。
### 遥控固定存储与栈测量
下表是2026-10-30的45/46版本历史基线,Windows x64、GCC 16.1.0、`-O2`实测(单位字节);不是扩展后的当前容量:
| 对象 | 应答槽1 | 应答槽4(默认) |
|---|---|---|
| Command实例 | 448 | 616 |
| 单事务槽 | 168 | 168 |
| 单独立应答槽 | 56 | 56 |
| Request / Event / Status | 48 / 88 / 120 | 48 / 88 / 120 |
该历史版本在默认TCP服务容量1时Command静态实例数组为616字节;容量2时实例数组按实例数增长。上述不含既有Slave、TCP和发送队列。探针源文件及可执行程序位于`.build/command-v1/size_probe.c`、`size_probe_1.exe`及`size_probe_4.exe`,探针包含Command实现以测量私有结构,只作本地测量,不增加公开接口。
历史`-fstack-usage`单函数栈帧:Command Handle_Event 208、Accept 128、Submit_Return_Info 144、Submit_Head 144、Process 64字节;示例main由800增至848字节,模拟动作8字节。原始记录为`.build/command-v1/command-stack.su`、`example-before.su`及`example-after.su`。这些是Windows宿主ABI的单函数测量,不是最坏调用链或其他目标工具链的内存预算。
2026-10-31的5a内部泛化阶段仍只启用45/46,但已由14种布局生成21字节最大缓冲。Windows x64实测实例为480/672字节(回复槽1/4),事务槽192、回复槽64,Request/Event/Config/Status仍为48/88/20/120。`-O2 -fstack-usage`单函数帧为Decode_Element 144、Copy_Request 96、Submit_Head 176、Handle_Event 304、Process 64、NextEvent 144、Accept 144、Submit_Return_Info 144字节;修改前同次探针的NextEvent为160字节。最终启用遥调和时标后的数值另记,不能把这一中间阶段当作最终内存预算。
5b增加value并启用47–51后,上述sizeof及栈帧测量均与5a相同;在此x64 ABI中value恰好使用Request原有对齐空隙,不代表其他目标布局不变。该阶段全量构建通过Slave 32组×2、示例131组×2、Command 33组×4、规则3组及真实TCP设定值流程,49执行后的总召读回通过。
5c启用58–64并加入时间基准后的最终Windows x64测量:
| 对象 | 应答槽1 | 应答槽4(默认) |
|---|---|---|
| Command实例 | 512 | 704 |
| 单事务槽 / 单独立应答槽 | 192 / 64 | 192 / 64 |
| Request / Event | 80 / 120 | 80 / 120 |
| Config / Status | 28 / 128 | 28 / 128 |
相对45/46历史基线,每实例增加64/88字节;默认单服务Command静态实例数组为704字节。容量仍由布局清单最大规范长度推导,当前为21,不是接收时分配。最终GCC `-O2 -fstack-usage`单函数帧:Decode_Element 144、Copy_Request 112、Submit_Head 176、Handle_Event 288、Process 64、NextEvent 176、Accept 176、Submit_Return_Info 144字节。公开头C++11严格编译检查通过。以上仍为宿主单函数帧,不是调用链上界;STM32F4须以目标工具链重新测量,不能用这些数字代替目标RAM/栈预算。
本轮产物统一保存在`.build/command-extension-20261031/`,包含修改前`before/`、三个阶段的`5a/`、`5b/`、`5c/`及`RESULTS.txt`。私有故障探针另验证保存原文损坏时的两条收尾路径:未交付工作事件时NextEvent返回ERROR且不产生事件;已交付后Accept返回ERROR、不调用动作或发肯定确认,FINISHED(SERVICE_ERROR)恰好一次,读完后事务槽可复用。
按点配置版的当前Windows x64实测:Command实例由512/704增至528/720字节(回复槽1/4),Config由28增至48字节;事务192、回复槽64、Request80、Event120、Status128字节保持不变。每个IEC104_CommandPoint_t为16字节,由应用持有;示例ExampleApp为1848字节,其中24项点表占384字节静态RAM,因main中的ExampleApp为static,不计作main局部栈。
本版GCC `-O2 -fstack-usage`测得Create从48增至64字节、Handle_Event从288增至304字节;Decode_Element 144、Copy_Request 112、Submit_Head 176、Process 64、NextEvent 176、Accept 176、Submit_Return_Info 144字节保持不变。C++11公开头和点表初始化检查通过。产物目录为`.build/command-point-config-20261031/`;这些数值仍只针对宿主ABI,完整构建及回环验收见第5节。
## 5. 验证记录
2026-10-31 示例逐点显式配置:Example_Init_Command改为局部const点表逐行列出24个CA/IOA、类型掩码及模式,移除配置生成的嵌套循环和末点判断;运行时common_address保留`--ca`支持。Create前复制到ExampleApp.command_points稳定数组供模块借用,三组地址描述只作运行值映射。既有IOA、类型、执行模式和点表寿命均不变,不新增测试组。`./scripts/build.ps1 -Test`严格GCC构建无警告、退出0:Command 53组×4、示例133组×2、Slave 32组×2、规则3组及真实TCP点模式与六种命令完整流程全部通过。修改前快照及证据位于`.build/command-explicit-points-20261031/`。
本次GCC `-O2 -fstack-usage`修改前后测量:Example_Init_Command均内联到main,没有独立帧;main均为864字节,Example_Command_Point均为40字节。局部表理论大小384字节,但该编译配置下最终main帧未增加;ExampleApp和持久点数组布局未改。编译无警告,原始测量为任务目录内的`example-before.su`和`example-after.su`,不能将该优化结果当作其他工具链的栈保证。
2026-10-31 原生按点配置:移除全局select_required_mask,Config新增借用的points/point_count;按CA/IOA严格有序且唯一的点表配置允许类型及DIRECT/SELECT_EXECUTE。全局默认14种但空点表不执行命令;51/64只能DIRECT。查点放在撤销、决定期间同点丢弃、时标及SELECTED匹配之后、忙检查之前,保留跨类型MISMATCH和现有事务语义。示例24点中6008/6108/6308直接执行,其余21点先选择;时间策略不变。
本轮`./scripts/build.ps1 -Test`退出0,GCC `-std=c11 -O2 -Wall -Wextra -Werror`构建零警告;Command 53组×4、示例133组×2、Slave 32组×2、规则3组及真实Windows TCP回环全部通过。回环覆盖点级直接/选择执行、六种命令完整流程、直接点SELECT/未选执行/点类型拒绝、数值限值、时效、总召新值、STOP及重连;STOP为submitted=83、gi_submitted=11、command_selected=7、command_executed=9、command_completed=9、command_returns=9、command_time_expired=1、command_time_rejected=0。
尺寸及栈增量见第4节;C++11头文件及点表初始化、调用流程内实际两点示例的C11/C++11严格语法编译均通过。17份Markdown的357个本地链接及锚点、UTF-8/LF、16个公开函数覆盖和diff空白检查通过。修改前快照及验证证据位于`.build/command-point-config-20261031/`;修改与检查仅限IEC60870-Slave,未设置宿主时钟或进行目标硬件验收。下方记录中的类型选择掩码仅代表当时版本。
2026-10-31 默认启用全部控制命令:按应用接入要求,Config_Default的type_mask改为45–51/58–64全部14位(0x3FFF),select_required_mask默认排除没有S/E的51/64(0x1FBF),其余12种仍须先选择。应用裁剪类型时同步裁剪选择掩码;示例显式赋值为已接入的45/46/49/58/59/62,保留未接入47等类型的44兜底。10秒窗口、UTC+8及过期静默策略不变。新增默认14种类型完整受理与执行回归,`./scripts/build.ps1 -Test`严格GCC构建零警告、退出码0:Command 46组×4、示例132组×2、Slave 32组×2、规则3组和真实TCP六类命令流程均通过。此变更只调整默认掩码,不改变结构布局;Request/Event/Status保持80/120/128字节。修改前快照位于`.build/command-default-all-20261031/before/`;下方三阶段记录保留变更前事实。
2026-10-31 遥调与带时标命令:在原Command模块内分5a布局泛化、5b遥调、5c时标三阶段完成45–51/58–64扩展。5a原45/46逐字节回归四配置各27组保持预期通过,5b增加到33组;最终`./scripts/build.ps1 -Test`通过严格GCC构建及全部运行,Slave 32组×2、示例132组×2、Command 45组×4及规则3组均通过,真实Windows TCP回环通过。该次验收时默认类型为45/46,额外类型显式启用;X-macro统一推导规范长度和21字节容量,原文、回复及FIFO使用有效长度。
本轮回环覆盖45/46/49/58/59/62选择、执行、返回信息及ACT_TERM;49/62写入的遥测和58/59更新的遥信由既有总召读回,初值与帧序保持兼容。过期58与106同批发送,核对106应答、后续计数量全部帧和统计,确认没有58应答或动作。STOP实测submitted=83、gi_submitted=11,command_selected=7、command_executed=6、command_completed=6、command_returns=6、command_time_expired=1、command_time_rejected=0。时间边界、IV/SU、时区、基准异常、撤销和匹配顺序由可控时钟专项覆盖;system读时接线使用mock,未设置宿主时钟。
三阶段sizeof及GCC栈帧见第4节;公开头C++11检查、内部原文损坏收尾探针通过。17份Markdown的356个本地链接及锚点、UTF-8/LF、16个公开函数在API文档中的覆盖和diff空白检查均通过。基线快照、阶段产物和RESULTS记录位于`.build/command-extension-20261031/`。所有修改及验证限于IEC60870-Slave目录,未执行父工程Keil构建、目标RAM/栈测量或硬件主站联调;宿主结果不代替这些验收。
2026-10-30 恢复双点遥控地址映射:Type46命令恢复为十进制6101..6108,对应Type3遥信仍为1101..1108。示例帮助、当前说明和回环请求同步更新。原EXE被运行进程占用时首次构建链接失败;用户关闭后,`./scripts/build.ps1`成功更新常规示例EXE,严格GCC构建无诊断。真实TCP回环验证6103选择/执行后返回1103,后续站总召、遥信分组及重连均读到新值,并通过既有单点遥控、撤销、错误应答及生命周期场景。沙箱内首次回环因Node启动子进程EPERM失败,获准在沙箱外重跑相同命令后退出码0。
2026-10-30 双点遥信加入总召:QOI20按Type1、Type3、Type11依次返回各8点,共8帧;QOI21返回单点及双点共6帧,QOI22仅遥测仍4帧。沿用1101..1108及double_values,每批4点、每次发送重试重取值;只新增双点遍历游标,不增加点值数组或发送缓冲。TCP容量1/2下各131组示例C测试通过,严格GCC构建无诊断;真实TCP验证Type46操作1103后,站总召、遥信分组及重连读回均为新值,STOP submitted=83、gi_submitted=11。原示例进程运行中,`./scripts/build.ps1`因无法覆盖被占用的exe而链接失败;随后以相同源文件及编译参数生成`.build/slave-example/iec104_slave_example_double_gi.exe`并通过回环,未中断当前连接。旧进程退出后,常规`./scripts/build.ps1`重跑成功,更新`iec104_slave_example.exe`,退出码0。可控测试产物为同目录`iec104_slave_example_double_test_1.exe`及`_2.exe`。
2026-10-30 恢复单点遥控地址映射:Type45恢复6001..6008控制Type1遥信1001..1008;Type46仍使用1101..1108。`./scripts/build.ps1`严格GCC构建无诊断;沿用已获准的真实TCP回环命令,验证6001选择/执行后返回1001及总召新值、1001作为Type45命令时回47、双点1103返回1103,并通过原有撤销、错误应答和生命周期场景,退出码0。示例帮助、说明和回环期望同步更新。
2026-10-30 示例遥控地址对齐遥信:Type45改用1001..1008,Type46改用1101..1108,命令与返回信息使用同一IOA。严格GCC构建无诊断,Slave单/双服务各32组、既有业务各130组、遥控四配置各27组及3组规则测试通过。真实TCP核对新地址的选择/执行/返回/终止、撤销及单点操作后的总召新值,旧地址6001回47。`./scripts/build.ps1 -Test`末尾回环受沙箱子进程EPERM限制,首次整体退出码1;获准单独重跑原回环命令后退出码0。帮助文本、示例说明和回环断言同步更新,协议库点址校验保持通用。
2026-10-30 遥控决定窗口同点激活:SELECT_WAIT/EXECUTE_WAIT内,通过校验且同会话、同站点注册编号、同IOA的激活只计dropped_requests并HANDLED,不覆盖请求、不刷新期限、不生成新事件或忙否定。新增4组回归,TCP容量1/2与回复槽1/4的四组合各27组通过;`./scripts/build.ps1 -Test`的严格GCC构建无诊断,Slave单/双服务各32组、既有业务各130组及3组规则测试通过。脚本末尾真实TCP回环因沙箱阻止Node子进程而报EPERM,首次整体退出码1;随后获准单独重跑原回环命令,退出码0,真实TCP全流程通过。Command公开头C++11严格语法检查通过,文档链接与UTF-8/LF检查通过;未改公开接口或固定存储容量,修改仅限IEC60870-Slave。
2026-10-30 Type45/46遥控:新增独立单事务模块、固定回复池、选择/执行/撤销、显式动作与设备结果、原连接返回信息及全局FIFO收尾。`./scripts/build.ps1 -Test`以GCC `-std=c11 -O2 -Wall -Wextra -Werror`构建无诊断,Slave单/双服务各32组、既有业务单/双服务各130组、遥控四配置各23组和3组召唤规则测试全部通过;真实Windows TCP回环通过45/46完整流程、撤销、TEST/地址拒绝、未支持类型、总召新值及原有计数量/校时/启停重连,脚本最终退出码0。沙箱内首次Node启动因EPERM失败,随后在获准环境重跑完整原脚本通过。校时使用模拟模式,遥控操作模拟点。修改前84个文件的快照、容量测试、sizeof探针及栈记录保存在`.build/command-v1/`;所有实现与验证均限定在IEC60870-Slave目录。
2026-10-29 Type 103时钟同步:新增独立静态池模块、显式Execute_Next动作、64位受理后滞留补偿、执行期限、原文ACT_CON及广播快照确认;示例默认模拟,显式system模式才设置Windows时间。整合`build.ps1 -Test`中GCC `-std=c11 -O2 -Wall -Wextra -Werror`构建无诊断,Slave单/双服务各32组、业务单/双服务各130组(98总召、12计数量、4兜底、16校时)及3组规则测试均通过。脚本内首次Node启动因沙箱`spawnSync EPERM`失败,因此该次脚本整体不是成功退出;随后获准单独重跑相同Node回环命令,退出码0,真实本机TCP通过,核对submitted=65、clock_executed=3、clock_synchronized=3、clock_submitted=5。默认模拟校时的正常/TEST/COT=5/广播及103+102同批均覆盖,未执行system模式或真实设备设时。改前基线、大小探针及栈测量位于`.build/clock-synchronization-20261029/`。
2026-10-28 业务目录整理:共用召唤引擎、内部规则头、总召和计数量共7个源码/头文件移到`104/business/`,迁移前后SHA256逐字节一致;构建加入该目录的头文件搜索路径,同步测试直接包含路径及文档源码链接。严格GCC构建通过,单/双服务各110组业务测试、23组Slave测试及3组规则测试通过,公开头C++检查和真实Windows TCP回环通过。沙箱限制Node启动子进程时,回环使用原授权命令在沙箱外执行。迁移清单及备份位于`.build/business-layout-20261028/`,修改仅限IEC60870-Slave;未执行Keil目标构建或硬件联调。
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,190 @@
# 104子站示例
[iec104_slave_example.c](../../examples/iec104_slave_example.c) 包含模拟点表、业务事件处理、其他命令兜底、事件循环及main。总召状态机使用独立库模块[IEC104_Interrogation.c/h](../../104/business/IEC104_Interrogation.h),不在示例内重复实现。
当前实现站总召(`C_IC_NA_1`,QOI=20),依次返回8个单点遥信、8个双点遥信和8个标度化遥测值;分组1(QOI=21)返回单点及双点遥信,分组2(QOI=22)只返回遥测。库支持全部16组,示例未配置的其余组由应用拒绝。通信通过`IEC104_Slave`,总召通过`IEC104_Interrogation`接入,所有任务使用固定存储、在主循环线程串行推进。示例另含计数量召唤、Type 103校时、45/46遥控、49标度化设定值及58/59/62带时标命令;默认时钟为模拟值,命令只更新内存点值,实际设备数据源和物理输出仍由应用接入。
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。
主循环向总召、计数量、校时、遥控实例广播生命周期通知,读空Slave通知后优先推进遥控,再交替推进召唤业务并显式执行校时;其他类型在四个模块均未接手后兜底。原Example_Init仍可用于仅总召测试,命令行入口分别调用计数量、校时及Example_Init_Command补齐实例,两个召唤实例各需独立广播状态区,校时使用共用站点快照。协议及应用动作限制见[计数量设计](../104/IEC104_CounterInterrogation_设计架构.md)、[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)和[遥控设计](../104/IEC104_Command_设计架构.md)。
在项目根目录的 PowerShell 中执行,需要 MinGW GCC:
示例通过 `IEC104_Interrogation.h`、`IEC104_CounterInterrogation.h`、`IEC104_ClockSynchronization.h` 和 `IEC104_Command.h` 引入业务声明;构建脚本添加 `104/`、`104/business/` 和 `common/` 头文件搜索路径,编辑器也应配置这三个目录。构建可执行程序仍使用下面的脚本,它会链接所需库源文件、Clock适配及Windows网络库;只编译当前C文件会缺少库函数实现。编辑器的C语言标准应与构建保持一致,使用C11,并选择实际的MinGW GCC编译器。
```powershell
./scripts/build.ps1
./.build/slave-example/iec104_slave_example.exe
```
默认监听 `127.0.0.1:2404`,公共地址 CA=1;按 Ctrl+C 退出并关闭服务。也可以指定监听地址、端口和公共地址:
```powershell
./.build/slave-example/iec104_slave_example.exe --bind 0.0.0.0 --port 2404 --ca 1
```
| 参数 | 含义 |
|---|---|
| `--bind IPv4` | 数字IPv4监听地址,默认127.0.0.1 |
| `--port 0..65535` | 默认2404;0由系统分配,启动时输出实际端口 |
| `--ca 1..65534` | 本站公共地址,默认1;总召请求也接受65535,广播请求的应答使用本站地址 |
| `--window 1..32767` | 发送窗口k,默认12;设为1可观察逐帧ACK后续发 |
| `--clock simulated\|system` | 默认simulated,只更新示例模拟时钟;system显式调用Windows系统设时接口 |
| `--run-ms N` | 运行约N毫秒后正常关闭;默认0表示持续运行 |
| `--help` | 打印用法后退出 |
启动输出 `LISTEN 地址:端口 CA=站地址`。退出保留原有STOP统计字段:`submitted`累计总召与兜底已接手ASDU,`gi_submitted`为已提交完毕的总召数,二者均不表示主站已经ACK。`waits`只统计总召发送WAIT;兜底HIGH满时丢新,计入`overloads`和`dropped_requests`,不记录待重试次数。兜底ERROR/FATAL计入`cancelled`,其中FATAL可能已写出字节,该计数不表示确定未发送。拒绝、主站撤销及业务超时统计继续保留,计数量另见`counter_submitted`和`counter_completed`。末尾校时字段`clock_executed`为实际action调用数、`clock_synchronized`为action返回true次数、`clock_submitted`为已交接校时ASDU数,包含错误及否定应答,广播按各提交目标分别计数;不混入原字段。
命令统计追加`command_selected`、`command_executed`、`command_completed`、`command_submitted`和`command_returns`,分别表示已接受的选择、已启动的执行、成功完成事务、已交接应答和返回信息帧数;`command_time_expired`统计过期激活,`command_time_rejected`统计无效时间、无基准、推算失败或超前造成的时间否定。静默过期不会增加选择/执行次数;保留原有统计字段的含义。
### Type 103校时
主站发送Type=103、SQ=0、对象数=1、COT=6、P/N=0、TEST=0、CA匹配或65535、IOA=0及有效CP56。默认按主站UTC+8标准时间解释,SU由时间工具处理;模块补偿受理至执行检查的等待。应用显式Execute_Next后回COT=7,原CP56字节原样回显。TEST否定7且不执行;COT=5读时否定45,不返回本站当前时间。
默认`--clock simulated`把请求的utc_ms/base_ms保存为模拟时钟基准,不改变系统时间。`--clock system`通过`IEC60870_Clock_Set(&request->local_time)`设置Windows时间;local_time已是固定UTC+8且已补偿,示例不再手工换时区或加等待。Clock内部转换为UTC后调用系统接口,权限不足时动作报告失败并回否定ACT_CON。真实TCP自动回归只运行模拟模式;system接线另用mock时钟单测,不设置宿主系统时间。
两种模式在成功校时动作中都记录utc_ms/base_ms及待更新标志;`Example_Drain_Clock`在Execute_Next返回后、回调之外把基准交给Command。system模式还在启动及每60秒、请求分派之前读取Clock,读成功且IV=0时按固定UTC+8转换(Clock读值不叠加夏令时),关联同一时刻的Net单调毫秒;读取/换算/设置基准失败或IV=1时清除Command基准。仅总召测试未创建Command,相关接线先检查空句柄。
确认发送WAIT时只重试原确认,不再次设时;STOP取消未交接确认而不撤销已完成的动作。同内容的新请求会再执行,旧时标重发可能回拨。真实数据发布方应在confirm_pending期间暂停新的带时标上报并继续驱动网络,避免持续新增NORMAL推迟确认。完整范围及期限见[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)。
### 遥控、设定值与带时标命令
`Example_Init_Command`在Config_Default后用赋值把全局type_mask裁剪为已接入的45、46、49、58、59、62,并提供24行命令点表。库默认全局启用14种命令(0x3FFF),但points=NULL、point_count=0;未提供具体点不会执行命令。示例不能用`|=`裁剪全类型默认值。决定期限15秒、选择保持30秒、执行期限60秒,成功发送ACT_TERM,失败默认不发。整个实例只允许一个事务,所有站点的新命令互斥;TEST请求否定,CA=65535回UNKNOWN_CA,未配置命令IOA的新激活走到点检查时由模块回47。
ExampleApp拥有点数组,在Command_Create之前按当前站点CA初始化;三组地址描述同时生成点许可和应用映射,24行按CA/IOA升序。各行只允许对应的基础类型及带时标类型。6008、6108、6308为DIRECT,其余21点为SELECT_EXECUTE;不再使用全局select_required_mask。数组从Create借用到Destroy,运行中不修改,`--ca`改变的仅是创建前填写的CA。
| 命令 | 受控点及返回信息 | 模式 | 初值与总召 |
|---|---|---|---|
| Type45/58,IOA6001..6008 | Type1,IOA1001..1008,COT=11;状态0分/1合 | 6001..6007先选择,6008直接执行 | 初值见下表,执行后总召读取更新值 |
| Type46/59,IOA6101..6108 | Type3,IOA1101..1108,COT=11;状态1分/2合 | 6101..6107先选择,6108直接执行 | 初值全部为1;执行后站总召及遥信分组读取更新值 |
| Type49/62,IOA6301..6308 | Type11,IOA2001..2008,COT=11;写入标度化值 | 6301..6307先选择,6308直接执行 | 初值见下表,执行后站总召及QOI=22读取更新值 |
表中IOA均为十进制。单点命令6001..6008分别对应遥信1001..1008;双点命令6101..6108分别对应返回位置1101..1108。例如对Type46、IOA6103完成状态2的选择与执行后,后续QOI=20或21读取的Type3、IOA1103为2;FINISHED清理不会把它恢复为初值1。
SELECT_EXECUTE点先发送S/E=1选择,收到肯定ACT_CON后再发S/E=0执行,均使用COT=6;执行的类型、IOA、OA、值及QU/QL须与选择一致,带时标命令可使用不同时标。DIRECT点只发送S/E=0执行,不接受选择;其SELECT请求回否定7,SELECT_EXECUTE点未选择便执行也回否定7。点类型不匹配同样回否定7,均不会调用应用动作。执行依次返回ACT_CON、返回信息、ACT_TERM。撤销使用COT=8,同类型/IOA/OA匹配,不要求值相同。示例未启用47、48、50、51、60、61、63、64,仍由兜底回44;这些类型由模块专项测试覆盖。
49/62允许的示例标度值为−10000..10000(包含端点),选择和执行都检查限值,越限调用Reject(NEGATIVE),动作内另作守卫。这个范围属于模拟点表策略,不是协议或Command模块的统一限值。比如对6301设为−1234,Type11返回信息及后续QOI=20/22的2001均为−1234;总召对象数与帧序不变。
58/59/62的CP56按主站UTC+8及SU解释,默认时差上限10秒,IV=1拒绝,时差恰好10秒也拒绝或丢弃。simulated模式启用时尚无可信基准,须先成功校时;system模式可由启动读时建立基准。过期激活默认静默丢弃,不占事务槽或调用动作;无基准、超前及换算失败回否定7。只在受理时检查时效,默认决定期限内继续等待可能使启动动作时的命令年龄接近25秒。撤销仍检查CP56格式,但不判时差。
`Example_Drain_Command`先读取SELECT/EXECUTE/FINISHED;首次保存事务context前Acquire应用站点引用。点表、动作和返回信息按基础类型分派;`Example_Command_Action`模拟返校或修改`ExampleApp.signal_values/double_values/scaled_values`并返回STARTED,不在动作中上报。Type11单对象返回需要12字节缓冲。Accept的OK只表示事件已消费,示例通过Get_Transaction确认EXECUTING后,依次Report_Result(SUCCEEDED)、Example_Build_Command_Return、Submit_Return_Info和Complete;设备结果可以早于确认交接。
每一步WAIT都保留原事件和当前步骤;返回信息使用独立ASDU缓冲,WAIT时保留同一帧,OK后才推进,避免重复提交。STALE停止该事务重试,继续读取FINISHED。FINISHED清除模拟返校/输出标志并Release应用引用,不恢复已修改的点值;真实输出若仍未停止,应用必须继续保持设备忙状态和引用。Example_Wait_Time同时计入遥控期限、模块推进和应用挂起重试,发送WAIT时允许等待网络而不空转。
## 主站操作和模拟点表
主站先建立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 |
| 1101..1108 | 双点信息 `M_DP_NA_1`,Type=3 | 1、1、1、1、1、1、1、1 |
| 2001..2008 | 标度化测量值 `M_ME_NB_1`,Type=11 | 2300、2310、2290、5000、-125、0、1234、42 |
所有品质为0。遥测值为原始有符号16位标度值,本示例没有定义物理单位和倍率。
一次成功站总召依次提交8个ASDU:
1. Type=100,COT=7,肯定激活确认。
2. 两帧Type=1、COT=20的单点数据,每帧4个对象。
3. 两帧Type=3、COT=20的双点数据,每帧4个对象。
4. 两帧Type=11、COT=20的标度化遥测,每帧4个对象。
5. Type=100,COT=10,激活终止。
分组1(QOI=21)共提交6个ASDU:ACT_CON、两帧Type=1单点数据、两帧Type=3双点数据、ACT_TERM,数据COT均为21,每帧4个对象。分组2(QOI=22)仍共提交4个ASDU:ACT_CON、两帧Type=11/COT=22遥测、ACT_TERM,每帧4个对象。确认和终止保留请求QOI。BEGIN校验分组,首个NEED_DATA按该组初始化应用游标,不把模拟点表写进库。
全部应答使用原请求的连接身份。总召确认、终止及短应答走普通HIGH路径,总召数据由Submit_Batch尝试不排队提交;校时调用过action的确认也使用Try。遥控执行动作产生副作用后的确认、返回信息和ACT_TERM均走Try,选择确认及校验否定走普通HIGH。未提交HIGH/NORMAL、当前帧或窗口背压时Try返回WAIT;遥控的模块FIFO还会保留更早应答优先。总召数据和遥控返回信息不使用Enqueue_Event,只有本帧接手后才推进应用步骤。
主站撤销使用相同的实际站CA/OA/TEST/QOI,COT改为8、P/N=0。激活和撤销均接受本站地址或65535,匹配时65535映射为本站地址,允许广播激活配本站撤销及本站激活配广播撤销。尚有匹配总召时模块停止任务、回复肯定COT=9,并通过FINISHED(DEACTIVATED)通知应用;不匹配或已完成则否定COT=9。已提交发送层的帧可能仍在确认前发出,撤销后不再提交旧批次和ACT_TERM。示例点数很少,总召可能在主站发撤销前已全部提交;观察中途撤销和各阶段拥塞可使用下面的可控网络测试。
广播总召的BEGIN/NEED_DATA/FINISHED携带本站CA,示例直接用该字段构造数据。确认、数据、终止、否定及错误应答的CA都为本站地址,OA/TEST/QOI保留原请求值;原始Slave事件不被修改。该支持属于Type100总召处理器,其他类型仍按各自业务规则分派。
## 请求校验及任务处理
| 情况 | 本示例的处理 |
|---|---|
| 不支持的Type ID | 同步调用Reject_Request回显原文,COT=44、P/N=1;HIGH满时丢新并计数,不因连续请求断链 |
| 总召COT不是6或8 | COT=45、P/N=1 |
| 总召CA既不是本站地址也不是65535 | COT=46、P/N=1,保留原错误CA |
| 总召IOA不是0 | COT=47、P/N=1 |
| 激活QOI超出20..36、请求P/N非法或当前已有未终结总召 | 否定ACT_CON,COT=7、P/N=1 |
| 空闲时收到合法激活请求 | 有空槽则建立唯一总召;BEGIN后应用接受才发肯定确认,忙时不同QOI也拒绝 |
| 激活QOI为23..36 | 合法请求交给应用,示例因未配置点表而Reply_Begin(false),回否定ACT_CON |
| 撤销不匹配、QOI超范围或P/N非法 | 否定DEACT_CON,COT=9、P/N=1 |
| 匹配的主站撤销 | 取消对应总召,肯定DEACT_CON,COT=9、P/N=0 |
| 总召唯一对象完整,但SQ=1或有多余尾字节 | 回否定7/9,COT/CA/IOA错误优先45/46/47;不执行激活或撤销 |
| 总召零对象/多对象等无法可靠应答的布局错误 | 计入rejected并丢弃;保留连接和已有总召 |
| 需要新增总召且4个任务槽已占满 | 尝试否定ACT_CON;无法安全提交/保存则丢新请求并计数,保留连接与原任务;匹配撤销复用原槽 |
| 接受/取数/整体业务期限到点 | 肯定ACT_CON提交前回否定确认并保留连接,提交后关闭原连接;已取出BEGIN才交付对应TIMEOUT终结事件 |
Example_Handle_Event先过滤asdu_result失败的I帧视图,避免进入业务分派或未知类型兜底;正常线上基础头/载荷截短已由Receiver和连接层处理。当前I帧依次交给总召、计数量、校时及Command;都未接手时,在读取下一事件前同步调用`IEC104_Slave_Reject_Request`回UNKNOWN_TYPE。该接口借用当前事件生成原文错误应答,复用库的普通HIGH路径;示例不保存other_request,也没有other_pending、other_waiting或other_waits。未启用类型的连续及相同内容有效新请求逐条尝试应答,允许穿插正在进行的总召。103由校时接手,45/46/49/58/59/62由Command接手,102、47及106仍回44;模块之间不承诺跨业务应答按接收顺序排列。
OK表示已接手;WAIT只因HIGH容量不足,示例丢弃本条并增加overloads/dropped_requests,保持连接、已有HIGH内容及当前业务。ERROR按失效请求处理并继续读通知;FATAL可能已写出字节,不得重试,继续排空收尾通知。接口不修改输入事件或其最近提交结果,不能通过Get_Last_Submit_Result读取本次返回值。未读启停通知会阻止本次应答;状态计数不能追溯已消费的STOP/START,禁止先保存事件再读完后续通知后调用。需要异步业务应答时另用Copy_Event、Check_Request并自行处理STOP取消,完整契约见[同步拒绝当前请求](../104/IEC104_API接口汇总.md#reject-request)。
校验从类型开始;已知总召先检查是否有唯一完整对象,再按COT、CA、IOA、布局/QOI/P/N及事务状态决定。畸形单对象的否定副本规范为10字节、VSQ=1,保留OA/TEST/IOA/QOI,不修改原输入。广播应答使用实际站CA。错误请求不取消原总召或刷新期限;否定应答沿现有队列重试,满槽只丢新请求。这里只实现上述命令集合,不代表所有类型的业务规则。
`Example_Drain_Business`主动读取总召业务事件:BEGIN按QOI选点表,Reply_Begin接受或拒绝请求;NEED_DATA调用普通辅助函数Example_Build_Batch构造数据,再Submit_Batch,数据耗尽则Complete_Data;FINISHED按实例/请求编号清理对应资源,撤销和超时也走该分支。没有向库注册业务函数指针。数据源暂未就绪时可保存事件,后续就绪再应答;发送WAIT也保留事件,下一次调用重新组包,模块不缓存本批原文或重复生成NEED_DATA。
本例BEGIN只判断接受,首个NEED_DATA初始化游标并读取这一批;接真实数据源时,在Example_Build_Batch的逐点组包处读取当前值和品质。IOA、类型、分组成员及遍历策略均由调用者决定,协议库不登记或冻结完整IOA列表。单点和遥测常量表保存地址、初值及品质,双点初值均为1;实际值分别存放在ExampleApp.signal_values、double_values和scaled_values中,站总召及对应分组逐批读取这些可写数组的当前值,包括遥控及设定值执行后的更新;这些地址不是库规定的范围。逐批采样不保证整轮同一时刻,也不自动解决与突发上报的新旧排序;完整边界见[总召模块设计](../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成功代替资源清理。
遥控同样执行`Command_Cancel → 读完FINISHED → Slave_Close → 读空并分派通知 → Command_Destroy`。示例先取消全部业务并清理应用引用,再关闭Slave;关闭后的生命周期通知仍交付全部模块。遥控FINISHED可以在离线时读取,事务槽及独立应答清空后才允许Destroy,最后移除站点并销毁Slave。
## 验证
```powershell
./scripts/build.ps1 -Test
```
`-Test` 需要预先安装 Node.js。脚本运行Slave/站点测试、总召/计数量/兜底/校时可控网络测试、规则单测、遥控容量矩阵和真实本机TCP测试;实跑数量及记录见[测试说明](../test/IEC104测试文档.md):
- `iec104_slave_test.c`:站点注册/移除、延迟回收、重复CA、13站共享服务、CA/context隔离、注册编号防替换、快照增删及嵌套集合锁、关闭重启保留,以及原有应答、事件身份和背压回归。Reject_Request另覆盖原文回显、非法参数、连续请求、窗口背压、HIGH满、启停门禁、旧会话及FATAL。
- `iec104_slave_example_test.c`:完整报文、部分写入、各发送阶段HIGH满与WAIT、请求校验、STOP/重连、非法请求、传输通知预算及API生命周期;业务事件覆盖延迟应答、批次校验、混合类型、容量边界、拒绝/失败/取消、终结预留和实例复用。总召覆盖全部16组、各阶段撤销、三项期限、迟到应答及通知优先。调度覆盖短应答立即提交及顺序保护、非队首槽复用、满槽否定或计数丢弃且当前任务有效、各阶段相同/不同QOI均忙拒绝、结束后重发成功、撤销对象匹配、停止清理、BEGIN前等事件空位及BEGIN后完整执行期限,并验证忙状态不拦截Type101总电度请求。测试设置HIGH=251字节、业务事件FIFO=2槽,使用真实容量背压限制发送。
- `iec104_command_test.c`:独立可控Net/单调时钟测试,TCP服务容量1/2和独立应答槽容量1/4交叉构建,HIGH=251字节;覆盖命令点表校验与按点模式、14种命令布局、值和CP56校验、选择执行、匹配、时效及时间基准、变长FIFO、返回信息、动作结果、期限和FINISHED/引用生命周期。
- `slave_example_loopback_test.cjs`:启动实际可执行程序,使用Windows网络后端和127.0.0.1随机端口,以独立主站字节验证分片请求、k=1时逐帧ACK、站总召、忙拒绝及结束后重发、撤销、错误应答、106与102同批分别回44、STOP/START、重连和正常退出。校时核对正常ACT_CON、TEST否定7、读时COT=5否定45、广播实际CA,以及103与102同批时先102回44、再103回7。命令核对45/46/49/58/59/62完整流程,以及6008/6108/6308直接执行、直接点SELECT否定、未选执行及点类型不符否定;返回信息在确认和终止之间,另有越限/TEST/地址拒绝、撤销、47兜底及总召新值。正常带时标命令由最近成功校时和测试单调经过时间推算时标;过期58与106同批,结合后续全部计数量帧、动作/选择计数和time_expired验证没有58应答。服务约12秒后自行关闭,使用默认模拟时钟。
原114组基线由98组总召、12组计数量及4组兜底回归组成。兜底改用未支持的106/102,覆盖同批及反序、相同内容的有效新请求、k=1与写阻塞排队、HIGH满后丢新且不补发、STOP/STOPSTART取消,以及总召未结束时立即提交错误应答。启用校时的独立回归另外核对103业务。
17组校时及时间接线回归覆盖无站广播45/46、同批及重复103逐条执行、FIFO中间否定、满槽丢新、Try等待不重执行、STOP/重连、Defer/FATAL、同一次采样检查期限及补偿、action内Get_Status与防重入、广播WAIT期间删除当前站及同CA替代不加入。其中1组只替换示例的Clock_Read/Set,验证system启动、60秒刷新、失败清基准、校时成功后回调外移交及NULL Command;Clock库仍正常链接,所有动作均不设置宿主系统时间。另有1组命令点表接线回归,检查24点、可配置CA及混合执行模式。示例测试合计133组(99总召、12计数量、4兜底、17校时/接线、1命令接线),本轮实际验收见[测试记录](../test/IEC104测试文档.md#5-验证记录)。
畸形请求回归覆盖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、撤销组合及连续兜底;当前ASDU数量及STOP统计见[测试记录](../test/IEC104测试文档.md#5-验证记录),计数量和命令独立计数。5个校时应答中3次执行并成功,另2次为TEST及COT拒绝;多CA由可控Net回归覆盖。
这些检查覆盖本示例的模拟点表流程;实际主站设备、现场点表和设备执行接口仍需联调。
@@ -0,0 +1,347 @@
# IEC104 测试说明
> **目录与命令同步日期**:2026-10-29。测试代码位于根目录的 `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后端,共32组统一入口测试,覆盖ASDU视图、共用发送分派、确认/终止字节、HIGH背压、非法参数、原连接身份、延迟复制、服务复用、生命周期、外部调度及离线上报。站点测试覆盖13节点、CA/context隔离、删除即时退出路由与引用延迟回收、同CA替代节点的注册编号、快照期间增删/嵌套/结束回收、显式集合锁及跨服务误用;新增9组同步拒绝回归。共用层非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)。
同步拒绝接口回归检查COT=44..47的原文回显、TEST/OA/CA/VSQ及对象字节保留、输入事件和最近提交结果不变、非法参数及原服务/会话隔离。未读启停通知拒绝提交;已消费STOP/START后的旧事件不在同步接口允许的调用范围内,不能用状态计数为0证明其仍有效。示例回归覆盖连续及重复内容的新命令、HIGH真满时丢新且保留连接/旧应答、活动总召中的兜底、启停与发送故障收尾;原同批兜底回归继续保留。接入校时后的真实TCP对103与102同批发送分别核对ACT_CON和COT=44,不保证跨业务响应顺序。接口与丢新语义见[同步拒绝当前请求](../104/IEC104_API接口汇总.md#reject-request)。
校时测试使用模拟action及可控单调时钟,不设置Windows时间。覆盖Handle/Process不执行应用、显式执行一次、原CP56回显、COT/CA/IOA校验优先级、TEST/IV/非法CP56拒绝、SU/时区转换、受理起滞留补偿、日期上界和64位运算、期限与倒退、Try背压不重复动作、FIFO/满槽、STOP/断线/旧会话、广播快照及删除目标,以及action内只读状态。真实设备写入耗时、权限及失败后的硬件状态不由这些模拟测试证明,边界见[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)。
## 2. 编译与运行
命令专项入口为`test/iec104_command_test.c`,复用TCP事件环测试的可控Net后端,但独立驱动Slave和Command,不依赖示例的应用状态。`./scripts/build.ps1 -Test`同时构建和执行TCP容量1/2 × 命令独立应答槽1/4的四种组合,每种53组(原46组迁移及7组按点回归);原45/46基线为每种27组。库源文件须包含`104/business/IEC104_Command.c`,测试本身包含可控TCP实现,不再额外链接Windows Net或第二份TcpServer。
命令原有覆盖45/46选择和直接执行、匹配/忙拒绝/撤销、三类动作结果、成功失败及终止配置、零/多帧返回信息、完整ASDU校验、全局FIFO、回复池满、窗口/部分写入、结果先于确认、重复及迟到事件、FINISHED配对、三个期限和时钟倒退(含期限配置0)、STOP提交竞态/FATAL、NORMAL覆盖反例、站点删除重注册、引用和单/双服务生命周期。扩展覆盖47–51/58–64;当前默认配置检查全局type_mask=0x3FFF及points=NULL、point_count=0。未启用47/58返回IGNORED的用例显式裁剪全局掩码,直接/选择执行由各点模式配置;非法配置使用保留bit14/15,另拒绝SELECT_EXECUTE点包含51/64、非法点表和时区越界。
全局默认掩码的14种完整流程测试提供显式点表:51/64配置DIRECT,其他类型配置SELECT_EXECUTE;7种带时标类型先验证无时间基准时否定,再设置基准走完流程。另单独检查原样默认空表不会执行点,避免把全局类型启用误认为已经配置命令点。
新增7组按点回归:原样默认空表逐14型;17项非法配置覆盖NULL/count配对、元素数乘法上限、CA/IOA范围、严格排序/唯一性、非法模式/点掩码及51/64模式限制;同类型不同点混合DIRECT/SELECT_EXECUTE及CA隔离;全局/点掩码交集及Config按值复制、点表只读借用;查点早于忙、决定期间同点静默/SELECTED跨类型MISMATCH/时效先于点策略;二分查找首中末、空隙及CA/IOA边界;选择超时、STOP和站点重新注册后的策略及身份隔离。完整实跑结果见最新验证记录。
变长输入采用测试侧独立手写长度表及报文字节,不共用Command布局清单。核对规范长度、额外尾字节、SQ、非法状态/CP56及NaN/Inf、带值动作参数、完整确认/撤销/终止回显;已知类型截短由Receiver拒绝,不能误记为Command接手并计rejected。匹配比较值与QU/QL并屏蔽S/E,CP56可不同,浮点+0/−0或跨类型仍失配;51/64数据位及CP56首字节不能被误读为S/E。回复池容量1/4下核对混合长度FIFO、WAIT重试和槽复用后的有效长度。
时标回归覆盖max=0、IV及UTC换算、无基准、等于/小于max、毫秒余数、超前、SU和时区、单调倒退与推算越界、回调内Set/Clear限制、失败设置保留原基准及STOP/断线保留基准。检查决定期间同点丢弃、忙时过期、SELECTED过期执行及寻址错误的顺序,区分time_expired、time_rejected、rejected与dropped_requests。真实回环核对45/46/49/58/59/62完整流程、限值拒绝、操作后总召当前值、47兜底,以及过期58没有应答、事件或动作。
决定期间同点激活的4组专项回归覆盖45/46、SELECT_WAIT与直接/选择后EXECUTE_WAIT、事件已取/未取、同批输入及发送背压。检查相同或不同命令字段均只增加dropped_requests,原事件、动作参数、回显和决定期限不变,不产生抢先的忙否定;另验证到期FINISHED配对、格式校验/type_mask/撤销/其他点忙拒绝,以及同CA重注册、STOP和重连的身份边界。
当前业务测试由99组总召、12组计数量、4组兜底、17组校时/时间接线和1组命令点表接线组成,共133组,单/双服务配置各跑一次。总召在原98组基础上新增双点批次发送WAIT后读取最新值的回归,验证站总召和遥信分组的单点/双点帧序、游标只在提交OK后推进;真实TCP另核对双点遥控后总召读到更新值。计数量覆盖独立实例、请求校验、Type15/37批次边界、忙隔离、冻结/复位、动作重复调用和WAIT/STALE/TEST、业务期限、STOP/断线、共享连接失败、发送背压及多站广播;兜底改用未支持的106/102,覆盖同批与反序/重复内容、窗口与写阻塞、HIGH满丢新及恢复、STOP/STOPSTART取消和活动总召中的应答。校时专项检查独立受理与执行、原文确认、补偿/期限、广播及生命周期;1组用mock Clock_Read/Set验证system启动、60秒刷新、失败清基准、校时回调外移交和NULL Command,仍链接真实Clock库,但不设置宿主系统时间。命令接线验证24个点、可配置CA及三组末点直接执行。`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: 32 groups passed (replies, identities, stations, lifecycle, backpressure, rejection)`。默认及`-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: 32 groups passed ...`、各`slave example + event interrogation tests: 133 groups passed ...`,以及四组命令专项、`interrogation rules: 3 groups passed ...`和`Slave example loopback: passed ...`。可控后端使用251字节HIGH和2槽召唤业务事件FIFO。真实回环使用默认存储、k=1、随机本机端口,验证总召与计数量交错、冻结/复位及后续读取、分组/广播、校时、混合点模式的命令操作、兜底、STOP/START、重连及正常退出。真实TCP校时使用模拟模式,system接线用mock单测,命令操作内存点表;实际主站、设备输出及宿主系统设时未纳入这组自动回归,不构成完整互操作认证。当前实际通过数量、STOP统计和测量产物见第5节。
## 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`并使用独立输出文件名即可。
### 校时固定存储与栈测量
2026-10-29 Windows x64 GCC默认配置实测:ClockTask为104字节,含4槽的ClockSynchronization实例为568字节,Request为80字节,Status为72字节;默认单服务的校时静态实例数组占568字节,不是完整协议库RAM总量。探针及产物位于`.build/clock-synchronization-20261029/size_probe.c/exe`。
`-O2 -fstack-usage`报告的单函数栈帧:Handle_Event 256、Execute_Next 240、Submit_Head 224、Process 64、Get_Status 80字节;示例main由修改前768增至800字节,Clock_Action为48字节。记录为同目录的`example-before.su`、`example-after.su`及`clock.su`。这些是x64宿主ABI和单函数测量,不是最坏调用链,也不代表ARMCC/MCU的RAM或栈占用;嵌入式接入须按实际工具链重新核算。
### 遥控固定存储与栈测量
下表是2026-10-30的45/46版本历史基线,Windows x64、GCC 16.1.0、`-O2`实测(单位字节);不是扩展后的当前容量:
| 对象 | 应答槽1 | 应答槽4(默认) |
|---|---|---|
| Command实例 | 448 | 616 |
| 单事务槽 | 168 | 168 |
| 单独立应答槽 | 56 | 56 |
| Request / Event / Status | 48 / 88 / 120 | 48 / 88 / 120 |
该历史版本在默认TCP服务容量1时Command静态实例数组为616字节;容量2时实例数组按实例数增长。上述不含既有Slave、TCP和发送队列。探针源文件及可执行程序位于`.build/command-v1/size_probe.c`、`size_probe_1.exe`及`size_probe_4.exe`,探针包含Command实现以测量私有结构,只作本地测量,不增加公开接口。
历史`-fstack-usage`单函数栈帧:Command Handle_Event 208、Accept 128、Submit_Return_Info 144、Submit_Head 144、Process 64字节;示例main由800增至848字节,模拟动作8字节。原始记录为`.build/command-v1/command-stack.su`、`example-before.su`及`example-after.su`。这些是Windows宿主ABI的单函数测量,不是最坏调用链或其他目标工具链的内存预算。
2026-10-31的5a内部泛化阶段仍只启用45/46,但已由14种布局生成21字节最大缓冲。Windows x64实测实例为480/672字节(回复槽1/4),事务槽192、回复槽64,Request/Event/Config/Status仍为48/88/20/120。`-O2 -fstack-usage`单函数帧为Decode_Element 144、Copy_Request 96、Submit_Head 176、Handle_Event 304、Process 64、NextEvent 144、Accept 144、Submit_Return_Info 144字节;修改前同次探针的NextEvent为160字节。最终启用遥调和时标后的数值另记,不能把这一中间阶段当作最终内存预算。
5b增加value并启用47–51后,上述sizeof及栈帧测量均与5a相同;在此x64 ABI中value恰好使用Request原有对齐空隙,不代表其他目标布局不变。该阶段全量构建通过Slave 32组×2、示例131组×2、Command 33组×4、规则3组及真实TCP设定值流程,49执行后的总召读回通过。
5c启用58–64并加入时间基准后的最终Windows x64测量:
| 对象 | 应答槽1 | 应答槽4(默认) |
|---|---|---|
| Command实例 | 512 | 704 |
| 单事务槽 / 单独立应答槽 | 192 / 64 | 192 / 64 |
| Request / Event | 80 / 120 | 80 / 120 |
| Config / Status | 28 / 128 | 28 / 128 |
相对45/46历史基线,每实例增加64/88字节;默认单服务Command静态实例数组为704字节。容量仍由布局清单最大规范长度推导,当前为21,不是接收时分配。最终GCC `-O2 -fstack-usage`单函数帧:Decode_Element 144、Copy_Request 112、Submit_Head 176、Handle_Event 288、Process 64、NextEvent 176、Accept 176、Submit_Return_Info 144字节。公开头C++11严格编译检查通过。以上仍为宿主单函数帧,不是调用链上界;STM32F4须以目标工具链重新测量,不能用这些数字代替目标RAM/栈预算。
本轮产物统一保存在`.build/command-extension-20261031/`,包含修改前`before/`、三个阶段的`5a/`、`5b/`、`5c/`及`RESULTS.txt`。私有故障探针另验证保存原文损坏时的两条收尾路径:未交付工作事件时NextEvent返回ERROR且不产生事件;已交付后Accept返回ERROR、不调用动作或发肯定确认,FINISHED(SERVICE_ERROR)恰好一次,读完后事务槽可复用。
按点配置版的当前Windows x64实测:Command实例由512/704增至528/720字节(回复槽1/4),Config由28增至48字节;事务192、回复槽64、Request80、Event120、Status128字节保持不变。每个IEC104_CommandPoint_t为16字节,由应用持有;示例ExampleApp为1848字节,其中24项点表占384字节静态RAM,因main中的ExampleApp为static,不计作main局部栈。
本版GCC `-O2 -fstack-usage`测得Create从48增至64字节、Handle_Event从288增至304字节;Decode_Element 144、Copy_Request 112、Submit_Head 176、Process 64、NextEvent 176、Accept 176、Submit_Return_Info 144字节保持不变。C++11公开头和点表初始化检查通过。产物目录为`.build/command-point-config-20261031/`;这些数值仍只针对宿主ABI,完整构建及回环验收见第5节。
## 5. 验证记录
2026-10-31 原生按点配置:移除全局select_required_mask,Config新增借用的points/point_count;按CA/IOA严格有序且唯一的点表配置允许类型及DIRECT/SELECT_EXECUTE。全局默认14种但空点表不执行命令;51/64只能DIRECT。查点放在撤销、决定期间同点丢弃、时标及SELECTED匹配之后、忙检查之前,保留跨类型MISMATCH和现有事务语义。示例24点中6008/6108/6308直接执行,其余21点先选择;时间策略不变。
本轮`./scripts/build.ps1 -Test`退出0,GCC `-std=c11 -O2 -Wall -Wextra -Werror`构建零警告;Command 53组×4、示例133组×2、Slave 32组×2、规则3组及真实Windows TCP回环全部通过。回环覆盖点级直接/选择执行、六种命令完整流程、直接点SELECT/未选执行/点类型拒绝、数值限值、时效、总召新值、STOP及重连;STOP为submitted=83、gi_submitted=11、command_selected=7、command_executed=9、command_completed=9、command_returns=9、command_time_expired=1、command_time_rejected=0。
尺寸及栈增量见第4节;C++11头文件及点表初始化、调用流程内实际两点示例的C11/C++11严格语法编译均通过。17份Markdown的357个本地链接及锚点、UTF-8/LF、16个公开函数覆盖和diff空白检查通过。修改前快照及验证证据位于`.build/command-point-config-20261031/`;修改与检查仅限IEC60870-Slave,未设置宿主时钟或进行目标硬件验收。下方记录中的类型选择掩码仅代表当时版本。
2026-10-31 默认启用全部控制命令:按应用接入要求,Config_Default的type_mask改为45–51/58–64全部14位(0x3FFF),select_required_mask默认排除没有S/E的51/64(0x1FBF),其余12种仍须先选择。应用裁剪类型时同步裁剪选择掩码;示例显式赋值为已接入的45/46/49/58/59/62,保留未接入47等类型的44兜底。10秒窗口、UTC+8及过期静默策略不变。新增默认14种类型完整受理与执行回归,`./scripts/build.ps1 -Test`严格GCC构建零警告、退出码0:Command 46组×4、示例132组×2、Slave 32组×2、规则3组和真实TCP六类命令流程均通过。此变更只调整默认掩码,不改变结构布局;Request/Event/Status保持80/120/128字节。修改前快照位于`.build/command-default-all-20261031/before/`;下方三阶段记录保留变更前事实。
2026-10-31 遥调与带时标命令:在原Command模块内分5a布局泛化、5b遥调、5c时标三阶段完成45–51/58–64扩展。5a原45/46逐字节回归四配置各27组保持预期通过,5b增加到33组;最终`./scripts/build.ps1 -Test`通过严格GCC构建及全部运行,Slave 32组×2、示例132组×2、Command 45组×4及规则3组均通过,真实Windows TCP回环通过。该次验收时默认类型为45/46,额外类型显式启用;X-macro统一推导规范长度和21字节容量,原文、回复及FIFO使用有效长度。
本轮回环覆盖45/46/49/58/59/62选择、执行、返回信息及ACT_TERM;49/62写入的遥测和58/59更新的遥信由既有总召读回,初值与帧序保持兼容。过期58与106同批发送,核对106应答、后续计数量全部帧和统计,确认没有58应答或动作。STOP实测submitted=83、gi_submitted=11,command_selected=7、command_executed=6、command_completed=6、command_returns=6、command_time_expired=1、command_time_rejected=0。时间边界、IV/SU、时区、基准异常、撤销和匹配顺序由可控时钟专项覆盖;system读时接线使用mock,未设置宿主时钟。
三阶段sizeof及GCC栈帧见第4节;公开头C++11检查、内部原文损坏收尾探针通过。17份Markdown的356个本地链接及锚点、UTF-8/LF、16个公开函数在API文档中的覆盖和diff空白检查均通过。基线快照、阶段产物和RESULTS记录位于`.build/command-extension-20261031/`。所有修改及验证限于IEC60870-Slave目录,未执行父工程Keil构建、目标RAM/栈测量或硬件主站联调;宿主结果不代替这些验收。
2026-10-30 恢复双点遥控地址映射:Type46命令恢复为十进制6101..6108,对应Type3遥信仍为1101..1108。示例帮助、当前说明和回环请求同步更新。原EXE被运行进程占用时首次构建链接失败;用户关闭后,`./scripts/build.ps1`成功更新常规示例EXE,严格GCC构建无诊断。真实TCP回环验证6103选择/执行后返回1103,后续站总召、遥信分组及重连均读到新值,并通过既有单点遥控、撤销、错误应答及生命周期场景。沙箱内首次回环因Node启动子进程EPERM失败,获准在沙箱外重跑相同命令后退出码0。
2026-10-30 双点遥信加入总召:QOI20按Type1、Type3、Type11依次返回各8点,共8帧;QOI21返回单点及双点共6帧,QOI22仅遥测仍4帧。沿用1101..1108及double_values,每批4点、每次发送重试重取值;只新增双点遍历游标,不增加点值数组或发送缓冲。TCP容量1/2下各131组示例C测试通过,严格GCC构建无诊断;真实TCP验证Type46操作1103后,站总召、遥信分组及重连读回均为新值,STOP submitted=83、gi_submitted=11。原示例进程运行中,`./scripts/build.ps1`因无法覆盖被占用的exe而链接失败;随后以相同源文件及编译参数生成`.build/slave-example/iec104_slave_example_double_gi.exe`并通过回环,未中断当前连接。旧进程退出后,常规`./scripts/build.ps1`重跑成功,更新`iec104_slave_example.exe`,退出码0。可控测试产物为同目录`iec104_slave_example_double_test_1.exe`及`_2.exe`。
2026-10-30 恢复单点遥控地址映射:Type45恢复6001..6008控制Type1遥信1001..1008;Type46仍使用1101..1108。`./scripts/build.ps1`严格GCC构建无诊断;沿用已获准的真实TCP回环命令,验证6001选择/执行后返回1001及总召新值、1001作为Type45命令时回47、双点1103返回1103,并通过原有撤销、错误应答和生命周期场景,退出码0。示例帮助、说明和回环期望同步更新。
2026-10-30 示例遥控地址对齐遥信:Type45改用1001..1008,Type46改用1101..1108,命令与返回信息使用同一IOA。严格GCC构建无诊断,Slave单/双服务各32组、既有业务各130组、遥控四配置各27组及3组规则测试通过。真实TCP核对新地址的选择/执行/返回/终止、撤销及单点操作后的总召新值,旧地址6001回47。`./scripts/build.ps1 -Test`末尾回环受沙箱子进程EPERM限制,首次整体退出码1;获准单独重跑原回环命令后退出码0。帮助文本、示例说明和回环断言同步更新,协议库点址校验保持通用。
2026-10-30 遥控决定窗口同点激活:SELECT_WAIT/EXECUTE_WAIT内,通过校验且同会话、同站点注册编号、同IOA的激活只计dropped_requests并HANDLED,不覆盖请求、不刷新期限、不生成新事件或忙否定。新增4组回归,TCP容量1/2与回复槽1/4的四组合各27组通过;`./scripts/build.ps1 -Test`的严格GCC构建无诊断,Slave单/双服务各32组、既有业务各130组及3组规则测试通过。脚本末尾真实TCP回环因沙箱阻止Node子进程而报EPERM,首次整体退出码1;随后获准单独重跑原回环命令,退出码0,真实TCP全流程通过。Command公开头C++11严格语法检查通过,文档链接与UTF-8/LF检查通过;未改公开接口或固定存储容量,修改仅限IEC60870-Slave。
2026-10-30 Type45/46遥控:新增独立单事务模块、固定回复池、选择/执行/撤销、显式动作与设备结果、原连接返回信息及全局FIFO收尾。`./scripts/build.ps1 -Test`以GCC `-std=c11 -O2 -Wall -Wextra -Werror`构建无诊断,Slave单/双服务各32组、既有业务单/双服务各130组、遥控四配置各23组和3组召唤规则测试全部通过;真实Windows TCP回环通过45/46完整流程、撤销、TEST/地址拒绝、未支持类型、总召新值及原有计数量/校时/启停重连,脚本最终退出码0。沙箱内首次Node启动因EPERM失败,随后在获准环境重跑完整原脚本通过。校时使用模拟模式,遥控操作模拟点。修改前84个文件的快照、容量测试、sizeof探针及栈记录保存在`.build/command-v1/`;所有实现与验证均限定在IEC60870-Slave目录。
2026-10-29 Type 103时钟同步:新增独立静态池模块、显式Execute_Next动作、64位受理后滞留补偿、执行期限、原文ACT_CON及广播快照确认;示例默认模拟,显式system模式才设置Windows时间。整合`build.ps1 -Test`中GCC `-std=c11 -O2 -Wall -Wextra -Werror`构建无诊断,Slave单/双服务各32组、业务单/双服务各130组(98总召、12计数量、4兜底、16校时)及3组规则测试均通过。脚本内首次Node启动因沙箱`spawnSync EPERM`失败,因此该次脚本整体不是成功退出;随后获准单独重跑相同Node回环命令,退出码0,真实本机TCP通过,核对submitted=65、clock_executed=3、clock_synchronized=3、clock_submitted=5。默认模拟校时的正常/TEST/COT=5/广播及103+102同批均覆盖,未执行system模式或真实设备设时。改前基线、大小探针及栈测量位于`.build/clock-synchronization-20261029/`。
2026-10-28 业务目录整理:共用召唤引擎、内部规则头、总召和计数量共7个源码/头文件移到`104/business/`,迁移前后SHA256逐字节一致;构建加入该目录的头文件搜索路径,同步测试直接包含路径及文档源码链接。严格GCC构建通过,单/双服务各110组业务测试、23组Slave测试及3组规则测试通过,公开头C++检查和真实Windows TCP回环通过。沙箱限制Node启动子进程时,回环使用原授权命令在沙箱外执行。迁移清单及备份位于`.build/business-layout-20261028/`,修改仅限IEC60870-Slave;未执行Keil目标构建或硬件联调。
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,12 @@
examples/iec104_slave_example.c:268:13:Example_Counter_Action 8 static
examples/iec104_slave_example.c:542:13:Example_Command_Point 40 static
examples/iec104_slave_example.c:981:13:Example_Stop 8 static
examples/iec104_slave_example.c:482:13:Example_Clock_Action 48 static
examples/iec104_slave_example.c:991:13:Example_Number 80 static
examples/iec104_slave_example.c:641:13:Example_Drain_Command.part.0.constprop 288 static
examples/iec104_slave_example.c:450:13:Example_Refresh_System_Clock.constprop 112 static
examples/iec104_slave_example.c:571:37:Example_Command_Action 80 static
examples/iec104_slave_example.c:328:13:Example_Drain_Counter.part.0.constprop 144 static
examples/iec104_slave_example.c:909:13:Example_Step_Counter.constprop 48 static
examples/iec104_slave_example.c:193:13:Example_Drain_Business.constprop 160 static
examples/iec104_slave_example.c:1043:5:main 864 static
@@ -0,0 +1,12 @@
examples/iec104_slave_example.c:268:13:Example_Counter_Action 8 static
examples/iec104_slave_example.c:542:13:Example_Command_Point 40 static
examples/iec104_slave_example.c:958:13:Example_Stop 8 static
examples/iec104_slave_example.c:482:13:Example_Clock_Action 48 static
examples/iec104_slave_example.c:968:13:Example_Number 80 static
examples/iec104_slave_example.c:641:13:Example_Drain_Command.part.0.constprop 288 static
examples/iec104_slave_example.c:450:13:Example_Refresh_System_Clock.constprop 112 static
examples/iec104_slave_example.c:571:37:Example_Command_Action 80 static
examples/iec104_slave_example.c:328:13:Example_Drain_Counter.part.0.constprop 144 static
examples/iec104_slave_example.c:886:13:Example_Step_Counter.constprop 48 static
examples/iec104_slave_example.c:193:13:Example_Drain_Business.constprop 160 static
examples/iec104_slave_example.c:1020:5:main 864 static
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,311 @@
/*
* 模块:IEC104遥控业务层,单命令45、双命令46,COT=6激活、COT=8撤销。
* 职责:校验请求、选择/执行匹配、撤销、三类期限、原文确认和事务收尾。
* 点表、闭锁、远方/就地策略及实际输出由应用负责;本模块不判定设备操作是否安全。
* 每个Slave至多一个实例,每实例至多一个未释放事务;其他业务仍独立推进。
* 静态存储、无动态分配、不保存回调;所有入口须与Slave在同一事件循环串行调用。
* 只有Accept调用应用动作,先消费事件、再至多调用一次;Handle_Event/Process不调用应用。
* 返回信息由Submit_Return_Info沿原连接提交,确认/返回信息/终止保持顺序。
*
* 接入顺序:先Create,再按Slave通知FIFO分派Handle_Event;每轮Process后读空NextEvent。
* SELECT/EXECUTE事件由Accept或Reject消费;EXECUTE启动成功后,先Report_Result,
* 再逐帧Submit_Return_Info,全部OK后Complete;最终读取FINISHED清理应用资源。
* Report_Result也可晚于返回信息,但必须早于Complete,且设备结果产生后应及时报告。
* WAIT表示本次应用操作尚未接手,保留输入并在读取通知/推进网络后重试;OK不得重复提交。
* 动作/确认/设备结果/FINISHED是不同节点:FINISHED只结束协议事务,不撤销物理输出。
* STOP、断线或Cancel不回滚动作,也不把原连接的返回信息转交到新连接。
*
* 退出顺序:Cancel、读完FINISHED、Slave_Close、读空并分派通知、Destroy,
* 然后由应用移除站点并销毁Slave。模块不负责Slave或应用持有引用的释放。
*/
#ifndef IEC104_COMMAND_H
#define IEC104_COMMAND_H
#include "IEC104_Slave.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 不透明静态池句柄;Destroy成功后失效,不得按值复制内部对象或继续使用旧指针。 */
typedef struct IEC104_Command IEC104_Command_t;
/* 供无副作用的类型常量使用;45..51映射bit0..6,58..64映射bit7..13。
* 范围外为0,不作非法移位。第一版Create只接受45、46两位。 */
#define IEC104_COMMAND_TYPE_BIT(type) ((uint16_t)( \
((type) >= 45U && (type) <= 51U) ? (1U << ((type) - 45U)) : \
(((type) >= 58U && (type) <= 64U) ? (1U << ((type) - 51U)) : 0U)))
/* 创建时复制的配置。期限以Net单调时钟计算,等于边界即到期;
* 0仅禁用时长限制,等待阶段遇到时钟倒退仍终结。选择期限包含确认的发送等待;
* Report_Result之后没有业务期限,应用漏调Complete会一直占用事务槽。 */
typedef struct {
uint16_t type_mask; /* 默认45|46,不能为空,不接受本版不支持的位。 */
uint16_t select_required_mask; /* 默认45|46,必须是type_mask子集;清除对应位允许直接执行。 */
uint32_t select_timeout_ms; /* 选择动作返回至匹配执行受理,默认30000,不得为0。 */
uint32_t decision_timeout_ms; /* 工作事件生成至Accept/Reject,默认15000;0禁用。 */
uint32_t execute_timeout_ms; /* 动作STARTED至Report_Result,默认60000;0禁用。 */
bool send_act_term; /* 成功后发ACT_TERM,默认true;false时失败也不发。 */
bool act_term_on_failure; /* 默认false;true且send_act_term时,启动后报告失败也发COT=10、P/N=0。 */
} IEC104_CommandConfig_t;
/* 可按值保存的请求快照;不含站点/ASDU等借用指针。
* state编码为单命令0/1、双命令1/2,通常对应分/合;具体操作含义由应用点表定义。
* accepted_ms为Handle_Event受理时刻,包含Slave通知排队时间,不是网络接收时间。 */
typedef struct {
uint64_t generation, receive_serial; /* 原连接代次、当前工作请求的I帧接收编号。 */
uint64_t station_id; /* 站点注册身份;同CA重新注册后此值不同。 */
uint16_t common_address; /* 请求公共地址CA,不接受广播65535。 */
uint8_t originator_address, type_id; /* 请求源发地址OA、类型45或46。 */
uint32_t ioa; /* 非零24位信息对象地址;存在性由应用点表确认。 */
bool is_select, selected; /* is_select为S/E;selected表示执行匹配了本事务的选择。 */
uint8_t state, qualifier; /* 原始状态编码、QU(0..31);1短脉冲/2长脉冲/3持续,其他由应用解释。 */
uint64_t accepted_ms; /* 当前工作请求受理时的单调毫秒。 */
} IEC104_CommandRequest_t;
/* SELECT和EXECUTE为待决定工作事件;FINISHED仅通知清理,无需应答。 */
typedef enum {
IEC104_COMMAND_SELECT = 1, /* 应用检查策略并Accept/Reject选择,可执行返校动作。 */
IEC104_COMMAND_EXECUTE, /* 应用Accept/Reject执行;selected区分选择后执行和直接执行。 */
IEC104_COMMAND_FINISHED /* 该事务唯一的结束通知,原因及副作用标志在事件中给出。 */
} IEC104_CommandEventType_t;
/* 首次结束时冻结,后续STOP/Cancel不会覆盖;枚举不表示设备输出已停止。 */
typedef enum {
IEC104_COMMAND_FINISH_NONE = 0, /* 工作事件的占位值,不是FINISHED的正常原因。 */
IEC104_COMMAND_FINISH_COMPLETED, /* 应用报告成功,Complete后最后应答已交接。 */
IEC104_COMMAND_FINISH_EXECUTION_FAILED, /* 输出已启动,但应用报告失败,协议收尾已交接。 */
IEC104_COMMAND_FINISH_REJECTED, /* 应用Reject,或动作返回NOT_STARTED。 */
IEC104_COMMAND_FINISH_ACTION_FAILED, /* 动作返回FAILED/非法值,可能存在副作用。 */
IEC104_COMMAND_FINISH_MISMATCH, /* 选择确认交接后,同一点执行的类型/OA/状态/QU不符。 */
IEC104_COMMAND_FINISH_DEACTIVATED, /* 主站COT=8成功撤销尚未启动执行的事务。 */
IEC104_COMMAND_FINISH_SELECT_TIMEOUT, /* 选择保持到期,释放选择,已交接的确认无法撤回。 */
IEC104_COMMAND_FINISH_DECISION_TIMEOUT, /* 未及时Accept/Reject,模块安排否定确认。 */
IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT, /* 未及时Report_Result;保留肯定确认,不发ACT_TERM。 */
IEC104_COMMAND_FINISH_CANCELLED, /* 本地Cancel结束事务。 */
IEC104_COMMAND_FINISH_STOPPED, /* 原连接STOPDT停用。 */
IEC104_COMMAND_FINISH_DISCONNECTED, /* 原连接断开。 */
IEC104_COMMAND_FINISH_CONNECTION_CHANGED, /* 新连接代次或会话身份已失效。 */
IEC104_COMMAND_FINISH_SEND_ERROR, /* 发送ERROR/FATAL,且不是等待分派正常STOP通知。 */
IEC104_COMMAND_FINISH_SERVICE_ERROR /* Slave服务错误或内部身份/应答资源无法继续使用。 */
} IEC104_CommandFinishReason_t;
/* 事件是可保存的纯值,应答须原样传回;调用方不能用修改request改变实际执行内容。
* 任何工作事件曾被取走的事务,恰好交付一次FINISHED;后者无需应答。
* 应用跨循环持有站点/context或异步动作时,应在首个工作事件时自行Acquire,
* FINISHED清理且物理输出确已结束后Release,不能等读到FINISHED后才申请引用。 */
typedef struct {
IEC104_CommandEventType_t type; /* 决定事件的处理方式,参与身份校验。 */
uint64_t instance_id, transaction_id, event_serial; /* 实例、事务、事件三层身份,原样回传。 */
IEC104_CommandRequest_t request; /* 最后接纳的工作请求,不被撤销/不匹配请求覆盖。 */
IEC104_CommandFinishReason_t reason; /* 仅FINISHED有效;工作事件为NONE。 */
bool select_effect, execute_effect; /* 仅FINISHED有效:对应动作曾STARTED/FAILED,可能需清理。 */
} IEC104_CommandEvent_t;
typedef enum {
IEC104_COMMAND_ACTION_NOT_STARTED = 0, /* 必须保证没有任何副作用。 */
IEC104_COMMAND_ACTION_STARTED, /* 选择生效或输出启动;输出结果另行报告。 */
IEC104_COMMAND_ACTION_FAILED /* 未报告启动成功,但可能已留下副作用。 */
} IEC104_CommandActionResult_t;
/* Accept同步调用的动作,不保存函数/context指针。
* context为应用传入的上下文,可为NULL;request由模块内部快照构造,仅本次调用有效。
* 须短时返回,不得保存request指针、推进Slave或发布上报;设备的异步完成用Report_Result报告。
* 回调期间仅可Get_Status/Get_Transaction;其他修改入口拒绝重入。
* 返回值须准确说明是否有副作用,非法值按FAILED处理,模块不会重试动作。 */
typedef IEC104_CommandActionResult_t (*IEC104_CommandAction_t)(void *context,
const IEC104_CommandRequest_t *request);
typedef enum {
IEC104_COMMAND_RESULT_SUCCEEDED = 1, /* 启动后设备实际执行成功。 */
IEC104_COMMAND_RESULT_FAILED /* 启动后设备实际执行失败,不等同于拒绝启动。 */
} IEC104_CommandResult_t;
typedef enum {
IEC104_COMMAND_REJECT_NEGATIVE = 1, /* 通用策略拒绝,回COT=7、P/N=1。 */
IEC104_COMMAND_REJECT_UNKNOWN_IOA /* 点表不存在此IOA,回COT=47、P/N=1。 */
} IEC104_CommandRejectCause_t;
typedef enum {
IEC104_COMMAND_EVENT_ERROR = -1, /* 参数/来源/服务错误;不能据此重放已产生的动作。 */
IEC104_COMMAND_EVENT_IGNORED = 0, /* 不接手,I帧可继续交后续处理器。 */
IEC104_COMMAND_EVENT_HANDLED = 1 /* 已接手,包括校验拒绝/计数丢弃,不应再走44兜底。 */
} IEC104_CommandEventResult_t;
typedef enum {
IEC104_COMMAND_NEXT_ERROR = -1, /* 句柄/参数/重入或服务错误,输出未写入。 */
IEC104_COMMAND_NEXT_EMPTY = 0, /* 暂无可交付事件,可能须先读Slave通知;输出未写入。 */
IEC104_COMMAND_NEXT_READY = 1 /* 已交付纯值事件,同一事件不会再次出队。 */
} IEC104_CommandNextResult_t;
/* 以下为共同分类;OK究竟消费事件、记录结果还是固定帧序,以各接口说明为准。
* WAIT均未接手本次应用输入;只有Submit_Return_Info的OK直接代表该ASDU帧序已固定。 */
typedef enum {
IEC104_COMMAND_REPLY_STALE = -2, /* 身份/阶段已失效、事件已消费或事务结束,不得重试旧操作。 */
IEC104_COMMAND_REPLY_ERROR = -1, /* 参数/用法/重入或服务错误,检查接口具体前置条件。 */
IEC104_COMMAND_REPLY_WAIT = 0, /* 等待通知/确认/FIFO/网络,保留输入,推进事件循环后重试。 */
IEC104_COMMAND_REPLY_OK = 1 /* 本次操作已接手;不保证设备成功、网络ACK或FINISHED已就绪。 */
} IEC104_CommandReplyResult_t;
typedef enum {
IEC104_COMMAND_PHASE_SELECT_WAIT = 1, /* 等应用决定选择;决定期限从事件生成开始。 */
IEC104_COMMAND_PHASE_SELECTED, /* 已选择,等待匹配执行;选择确认可能仍未交接。 */
IEC104_COMMAND_PHASE_EXECUTE_WAIT, /* 等应用决定执行;已停止选择期限。 */
IEC104_COMMAND_PHASE_EXECUTING, /* 已启动,等待结果/返回信息/Complete/最后应答交接。 */
IEC104_COMMAND_PHASE_ENDED /* 协议已结束,可能仍保留FINISHED或未交接应答。 */
} IEC104_CommandPhase_t;
typedef struct {
bool active; /* 尚未ENDED;不是当前连接是否可操作的保证。 */
IEC104_CommandPhase_t phase; /* 当前协议阶段;结果报告和Complete不另建阶段。 */
bool confirm_submitted; /* 当前工作请求的确认已交给发送层,不表示写完或ACK。 */
/* result_reported:结果已冻结、执行期限停止;completed:已接手Complete,返回阶段关闭;
* term_submitted:ACT_TERM已交接。不配置ACT_TERM时此值一直false,不能据此等事务结束。 */
bool result_reported, completed, term_submitted;
} IEC104_CommandTransactionInfo_t;
/* 累计计数从Create起维护;查询不改变阶段、不做超时转换。
* busy包括待消费FINISHED和未交接的事务应答,不包含独立短应答。
* need_process不因发送WAIT持续置true;每轮网络推进后仍须Process重试。
* 计数用途是诊断,不是可靠账单;各计数可重叠,unsigned long到达上限后自然回绕。 */
typedef struct {
uint64_t generation; /* 当前已分派的连接代次,断线后为0。 */
/* active:模块已处理ACTIVATED且尚未停用,不代替操作前的实时会话校验;
* events_pending:须优先读取Slave通知;need_process:有可立即推进的内部工作;
* timer_active:当前启用了非零时长期限,时钟倒退检查不受此标志限制。 */
bool active, events_pending, need_process, timer_active;
bool busy; /* 唯一事务槽仍占用,此时其他站点/其他IOA也不能新建事务。 */
uint32_t timeout_remaining_ms; /* 当前期限剩余毫秒;无期限/已到期/时钟倒退时为0。 */
/* pending_replies为模块仍持有的应答数,不含发送层已接手的帧和应用保留的返回信息;
* pending_events为尚未取走的工作事件或FINISHED数,本版最多1。 */
unsigned pending_replies, pending_events;
/* accepted:生成的工作事件数(选择后执行计两次);selected/executed:对应STARTED次数;
* completed/execution_failed:成功/失败收尾数;action_failed:动作FAILED结束数。 */
unsigned long accepted, selected, executed, completed, execution_failed, action_failed;
/* submitted:所有已交接帧,含返回信息;negative_submitted:其中P/N=1的应答;
* return_info_submitted:已交接返回信息;waits:发送接口明确返回WAIT的次数。 */
unsigned long submitted, negative_submitted, return_info_submitted, waits;
/* rejected:接收校验/匹配/忙/无选择等拒绝数;app_rejected:Reject或NOT_STARTED数;
* busy_rejected:因事务槽占用拒绝数;overloads:独立应答池满或事务/事件序号耗尽次数;
* dropped_requests:过载丢新数及决定期间同点合法激活的静默丢弃数,后者不计rejected/busy_rejected/overloads;
* cancelled:本地取消、停用、断线、换代、发送/服务错误导致的事务结束总数。 */
unsigned long rejected, app_rejected, busy_rejected, overloads, dropped_requests, cancelled;
/* deactivated:成功撤销数;mismatches:选择失配数;其余三个为各期限导致的结束数。 */
unsigned long deactivated, mismatches, select_timeouts, decision_timeouts, execute_timeouts;
/* 等待阶段发现单调时钟倒退的次数;按到期路径处理,必要确认资源失败时可能改记SERVICE_ERROR。 */
unsigned long clock_errors;
} IEC104_CommandStatus_t;
/* 填写本版默认配置:45/46均要求先选择,决定15s、选择30s、执行60s,成功发ACT_TERM。
* 入参:config为调用方可写结构;NULL时无操作。返回:无;不创建实例或启动计时。 */
void IEC104_Command_Config_Default(IEC104_CommandConfig_t *config);
/* 从静态池创建并绑定一个遥控实例;不自动开启Slave或消费其通知。
* 入参:slave为有效Slave,寿命须覆盖实例;config非NULL,按值复制,调用后可修改原结构。
* 必须在开始消费Slave通知之前创建,以完整接收CONNECTED/ACTIVATED。
* 池容量IEC104_COMMAND_CAPACITY默认等于TCP服务容量;独立应答容量
* IEC104_COMMAND_REPLY_CAPACITY默认4,均在编译模块时通过宏配置。
* 返回:成功为实例句柄;配置/Slave非法、重复绑定、池或实例身份序号耗尽时为NULL。 */
IEC104_Command_t *IEC104_Command_Create(IEC104_Slave_t *slave, const IEC104_CommandConfig_t *config);
/* 归还静态实例槽;不隐式Cancel,不关闭Slave,也不代替应用释放站点引用。
* 入参:self为待销毁实例;事务槽、未读事件及全部独立应答槽必须均空,且不在动作回调内。
* 返回:成功true,句柄随即失效;条件不满足或句柄无效为false,实例保留。 */
bool IEC104_Command_Destroy(IEC104_Command_t *self);
/* 本地结束事务,原因CANCELLED;保留欠应用的FINISHED,清空模块尚持有的全部应答。
* 已交发送层的帧无法撤回;保持会话激活,不回滚物理动作。
* 入参:self为实例;回调中/无效句柄无操作。返回:无;应用仍须读完FINISHED。
* ENDED事务保持首次终结原因,不重复产生FINISHED。 */
void IEC104_Command_Cancel(IEC104_Command_t *self);
/* 接收Slave通知并推进会话/请求状态;不调用应用动作,不保存event或其ASDU借用指针。
* 入参:self为实例;event是所属Slave按FIFO取出的通知,须在通知内容仍有效时分派。
* 全部连接/启停通知必须送达;通知消费不受队列里后续WAIT_EVENTS阻挡。
* 返回:激活且视图可用时接手配置内45/46,当前会话状态通知也为HANDLED;
* 未激活的I帧、视图不可用、未接手类型或旧代次通知为IGNORED;
* 参数、来源、重入或服务错误为ERROR。非法请求可否定或丢新计数,HANDLED不表示已应答。
* SELECT_WAIT/EXECUTE_WAIT期间,同会话、同station_id、同IOA的合法激活静默丢弃,计dropped_requests,
* 返回HANDLED;不比较其他命令字段,不覆盖原请求/事件、不刷新期限。格式校验、撤销及其他点忙拒绝不变。
* 对IGNORED的I帧继续分派后续业务,最后由应用兜底;不要对HANDLED再发44。 */
IEC104_CommandEventResult_t IEC104_Command_Handle_Event(IEC104_Command_t *self,
const IEC104_SlaveEvent_t *event);
/* 推进会话检查、到期转换、发送队首及事务回收;不调用应用代码。
* 入参:self为实例;每轮网络/通知处理后调用,最多推进2*(1+独立应答槽数)步。
* 队首WAIT立即让出,不能用紧循环等待发送;需处理Slave通知/网络后再次调用。
* 返回:true包括无事、背压等待或事务已终结;false为参数/重入或服务错误。
* 发送故障也可能已经转成FINISHED,此函数返回true不代表链路或设备执行成功。 */
bool IEC104_Command_Process(IEC104_Command_t *self);
/* 取出一条应用事件,同一工作事件最多交付一次;取出不等于Accept/Reject消费。
* 入参:self为实例;event非NULL,接收可按值保存的事件副本,仅READY时写入。
* 返回:READY/EMPTY/ERROR,见NextResult;EMPTY也可能表示须先读Slave通知。
* 工作事件交付前检查会话/期限,已有FINISHED可在离线或尚有Slave通知时读取。
* 读取FINISHED可能立即释放模块的站点引用,应用跨循环引用必须此前已自行Acquire。 */
IEC104_CommandNextResult_t IEC104_Command_NextEvent(IEC104_Command_t *self,
IEC104_CommandEvent_t *event);
/* 接受一个已由NextEvent交付的工作事件;先消费身份,再同步至多调用一次action。
* 入参:self为实例;event须原样传回SELECT/EXECUTE事件;context原样转交action,不保存。
* SELECT的action可为NULL,选择生效但select_effect=false;EXECUTE必须提供action。
* 返回:OK只表示工作事件已消费,不表示动作STARTED或确认已交接;动作失败也返回OK。
* WAIT为先读Slave通知,事件未消费;STALE为身份/阶段失效,不得执行输出;
* ERROR为参数/重入/服务或确认资源错误,动作未调用。
* 即使action返回STARTED,提交确认时仍可能结束事务;应用应查询事务并继续接收FINISHED。 */
IEC104_CommandReplyResult_t IEC104_Command_Accept(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandAction_t action, void *context);
/* 不调用动作,消费工作事件,安排否定ACT_CON或未知IOA应答,以REJECTED结束事务。
* 入参:self为实例;event为尚未消费的SELECT/EXECUTE;cause为两个RejectCause之一。
* 返回:OK表示事件已消费且应答已安排,可能已提交或因随后故障被取消;
* WAIT未消费、须先处理通知;STALE身份/阶段已失效;ERROR参数/原因/重入或服务错误。
* FINISHED可先于否定应答交接,读取FINISHED不代表槽已释放。 */
IEC104_CommandReplyResult_t IEC104_Command_Reject(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandRejectCause_t cause);
/* 报告设备结果,结果只记录一次并冻结;不发返回信息,也不隐式Complete。
* 本函数及下列Submit_Return_Info/Complete只接受Accept动作返回STARTED的原EXECUTE事件。
* 入参:self为实例,event为上述事件,result为SUCCEEDED或FAILED。
* 可早于ACT_CON交接;设备完成后应及时报告,OK即停止执行期限,此后发送等待无业务超时。
* 返回:OK已记录;WAIT因未读通知尚未记录;STALE事务结束/超时或事件不符;
* ERROR参数/结果非法、已报告/已Complete、重入或服务错误。 */
IEC104_CommandReplyResult_t IEC104_Command_Report_Result(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result);
/* 通过原连接Try路径提交一帧返回信息,不经过可覆盖的NORMAL队列。
* 入参:self为实例;event为启动成功的原EXECUTE;asdu为有效、完整的监视方向ASDU。
* 类型1..21/30..40,至少一对象,COT=11或3,P/N=0,TEST/CA与命令一致;完整校验对象。
* IOA可与命令IOA不同;本版命令TEST恒为0。调用可以早于或晚于Report_Result,但须早于Complete。
* 模块有更早待交应答、确认未交接、网络背压或未读通知时WAIT,不缓存ASDU、也不占排序位置。
* 返回:OK为发送层已接手且帧序固定,可释放/复用ASDU缓冲,不能重交;WAIT须保留ASDU重试;
* ERROR为ASDU/参数/重入/服务错误或Complete后仍待收尾,尚未接手;STALE为事务/事件失效。
* 正常STOP且通知待分派时先WAIT;其他发送ERROR/FATAL终结事务并返回STALE,不可重试旧ASDU。
* 必要的后续变位由应用另按COT=3上报。
* 同一变化勿再经Enqueue_Event重复发布;成功提交不表示对端ACK,断线不会迁移到新连接。 */
IEC104_CommandReplyResult_t IEC104_Command_Submit_Return_Info(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, const IEC60870_Asdu_t *asdu);
/* 声明本事务不再产生返回信息;按配置安排ACT_TERM并由Process完成后续交接。
* 入参:self为实例,event为启动成功的原EXECUTE;结果须已报告,全部返回信息须已提交OK。
* 允许零帧返回信息;可在确认未交接时调用,模块保持确认先于ACT_TERM的顺序。
* 返回:OK立即关闭返回阶段,不必重调;WAIT为未读通知,尚未接手;
* ERROR为尚未报告结果、仍待收尾时重复调用、参数/重入/服务错误;已结束/事件不符为STALE。
* 不发ACT_TERM时也须等待执行确认交接,之后才产生FINISHED;无发送等待时可能立即结束。
* 失败配置发TERM时P/N=0表示流程结束,不能据此认定设备成功。 */
IEC104_CommandReplyResult_t IEC104_Command_Complete(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event);
/* 只读查询仍占槽的事务,可在动作回调内使用,不做会话推进或期限转换。
* 入参:self为实例;event可为该事务任一已交付的SELECT/EXECUTE/FINISHED;info非NULL。
* 返回:true写入快照,ENDED的active=false;无效参数/身份、槽已回收时false且不修改info。
* 不保留历史事务;刚读出的FINISHED若已触发槽回收,使用它查询也会false。 */
bool IEC104_Command_Get_Transaction(const IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandTransactionInfo_t *info);
/* 只读查询会话、调度提示及累计计数,可在动作回调内使用。
* 入参:self为实例,status非NULL接收快照;计算剩余时间但不转换已到期事务。
* 返回:true写入快照;句柄/参数/Slave服务无效时false且不修改status。
* events_pending优先读Slave通知;need_process用于立即调度,timer_active配合剩余时间等待。
* need_process=false不免除每轮网络推进后的Process,也不代表应用没有待重试的ASDU/Complete。 */
bool IEC104_Command_Get_Status(const IEC104_Command_t *self, IEC104_CommandStatus_t *status);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_COMMAND_H */
@@ -0,0 +1,317 @@
/*
* 模块:IEC104控制命令业务层,45..51,COT=6激活、COT=8撤销。
* 职责:校验请求、选择/执行匹配、撤销、三类期限、原文确认和事务收尾。
* 点表、闭锁、远方/就地策略及实际输出由应用负责;本模块不判定设备操作是否安全。
* 每个Slave至多一个实例,每实例至多一个未释放事务;其他业务仍独立推进。
* 静态存储、无动态分配、不保存回调;所有入口须与Slave在同一事件循环串行调用。
* 只有Accept调用应用动作,先消费事件、再至多调用一次;Handle_Event/Process不调用应用。
* 返回信息由Submit_Return_Info沿原连接提交,确认/返回信息/终止保持顺序。
*
* 接入顺序:先Create,再按Slave通知FIFO分派Handle_Event;每轮Process后读空NextEvent。
* SELECT/EXECUTE事件由Accept或Reject消费;EXECUTE启动成功后,先Report_Result,
* 再逐帧Submit_Return_Info,全部OK后Complete;最终读取FINISHED清理应用资源。
* Report_Result也可晚于返回信息,但必须早于Complete,且设备结果产生后应及时报告。
* WAIT表示本次应用操作尚未接手,保留输入并在读取通知/推进网络后重试;OK不得重复提交。
* 动作/确认/设备结果/FINISHED是不同节点:FINISHED只结束协议事务,不撤销物理输出。
* STOP、断线或Cancel不回滚动作,也不把原连接的返回信息转交到新连接。
*
* 退出顺序:Cancel、读完FINISHED、Slave_Close、读空并分派通知、Destroy,
* 然后由应用移除站点并销毁Slave。模块不负责Slave或应用持有引用的释放。
*/
#ifndef IEC104_COMMAND_H
#define IEC104_COMMAND_H
#include "IEC104_Slave.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 不透明静态池句柄;Destroy成功后失效,不得按值复制内部对象或继续使用旧指针。 */
typedef struct IEC104_Command IEC104_Command_t;
/* 供无副作用的类型常量使用;45..51映射bit0..6,58..64映射bit7..13。
* 范围外为0,不作非法移位。Create当前接受45..51,默认只启用45、46。 */
#define IEC104_COMMAND_TYPE_BIT(type) ((uint16_t)( \
((type) >= 45U && (type) <= 51U) ? (1U << ((type) - 45U)) : \
(((type) >= 58U && (type) <= 64U) ? (1U << ((type) - 51U)) : 0U)))
/* 创建时复制的配置。期限以Net单调时钟计算,等于边界即到期;
* 0仅禁用时长限制,等待阶段遇到时钟倒退仍终结。选择期限包含确认的发送等待;
* Report_Result之后没有业务期限,应用漏调Complete会一直占用事务槽。 */
typedef struct {
uint16_t type_mask; /* 默认45|46,不能为空;可显式启用45..51。 */
uint16_t select_required_mask; /* 默认45|46,必须是type_mask子集;51无S/E,禁止要求选择。 */
uint32_t select_timeout_ms; /* 选择动作返回至匹配执行受理,默认30000,不得为0。 */
uint32_t decision_timeout_ms; /* 工作事件生成至Accept/Reject,默认15000;0禁用。 */
uint32_t execute_timeout_ms; /* 动作STARTED至Report_Result,默认60000;0禁用。 */
bool send_act_term; /* 成功后发ACT_TERM,默认true;false时失败也不发。 */
bool act_term_on_failure; /* 默认false;true且send_act_term时,启动后报告失败也发COT=10、P/N=0。 */
} IEC104_CommandConfig_t;
/* 可按值保存的请求快照;不含站点/ASDU等借用指针。
* state为单命令0/1、双命令1/2、步调节1降/2升;具体操作含义由应用点表定义。
* accepted_ms为Handle_Event受理时刻,包含Slave通知排队时间,不是网络接收时间。 */
typedef struct {
uint64_t generation, receive_serial; /* 原连接代次、当前工作请求的I帧接收编号。 */
uint64_t station_id; /* 站点注册身份;同CA重新注册后此值不同。 */
uint16_t common_address; /* 请求公共地址CA,不接受广播65535。 */
uint8_t originator_address, type_id; /* 请求源发地址OA、类型45..51。 */
uint32_t ioa; /* 非零24位信息对象地址;存在性由应用点表确认。 */
bool is_select, selected; /* is_select为S/E;selected表示执行匹配了本事务的选择。 */
uint8_t state, qualifier; /* 45..47:状态/QU;48..50:0/QL(0..127);51:0/0。 */
union {
int16_t normalized; /* 48:原始定点编码,实际值为normalized/32768.0。 */
int16_t scaled; /* 49:有符号标度值。 */
float short_float; /* 50:有限IEEE 754 binary32。 */
uint32_t bitstring; /* 51:32位数据,不包含选择位。 */
} value; /* 按type_id读取;状态命令清零。 */
uint64_t accepted_ms; /* 当前工作请求受理时的单调毫秒。 */
} IEC104_CommandRequest_t;
/* SELECT和EXECUTE为待决定工作事件;FINISHED仅通知清理,无需应答。 */
typedef enum {
IEC104_COMMAND_SELECT = 1, /* 应用检查策略并Accept/Reject选择,可执行返校动作。 */
IEC104_COMMAND_EXECUTE, /* 应用Accept/Reject执行;selected区分选择后执行和直接执行。 */
IEC104_COMMAND_FINISHED /* 该事务唯一的结束通知,原因及副作用标志在事件中给出。 */
} IEC104_CommandEventType_t;
/* 首次结束时冻结,后续STOP/Cancel不会覆盖;枚举不表示设备输出已停止。 */
typedef enum {
IEC104_COMMAND_FINISH_NONE = 0, /* 工作事件的占位值,不是FINISHED的正常原因。 */
IEC104_COMMAND_FINISH_COMPLETED, /* 应用报告成功,Complete后最后应答已交接。 */
IEC104_COMMAND_FINISH_EXECUTION_FAILED, /* 输出已启动,但应用报告失败,协议收尾已交接。 */
IEC104_COMMAND_FINISH_REJECTED, /* 应用Reject,或动作返回NOT_STARTED。 */
IEC104_COMMAND_FINISH_ACTION_FAILED, /* 动作返回FAILED/非法值,可能存在副作用。 */
IEC104_COMMAND_FINISH_MISMATCH, /* 选择确认交接后,同一点执行的类型/OA/值/限定词不符。 */
IEC104_COMMAND_FINISH_DEACTIVATED, /* 主站COT=8成功撤销尚未启动执行的事务。 */
IEC104_COMMAND_FINISH_SELECT_TIMEOUT, /* 选择保持到期,释放选择,已交接的确认无法撤回。 */
IEC104_COMMAND_FINISH_DECISION_TIMEOUT, /* 未及时Accept/Reject,模块安排否定确认。 */
IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT, /* 未及时Report_Result;保留肯定确认,不发ACT_TERM。 */
IEC104_COMMAND_FINISH_CANCELLED, /* 本地Cancel结束事务。 */
IEC104_COMMAND_FINISH_STOPPED, /* 原连接STOPDT停用。 */
IEC104_COMMAND_FINISH_DISCONNECTED, /* 原连接断开。 */
IEC104_COMMAND_FINISH_CONNECTION_CHANGED, /* 新连接代次或会话身份已失效。 */
IEC104_COMMAND_FINISH_SEND_ERROR, /* 发送ERROR/FATAL,且不是等待分派正常STOP通知。 */
IEC104_COMMAND_FINISH_SERVICE_ERROR /* Slave服务错误或内部身份/应答资源无法继续使用。 */
} IEC104_CommandFinishReason_t;
/* 事件是可保存的纯值,应答须原样传回;调用方不能用修改request改变实际执行内容。
* 任何工作事件曾被取走的事务,恰好交付一次FINISHED;后者无需应答。
* 应用跨循环持有站点/context或异步动作时,应在首个工作事件时自行Acquire,
* FINISHED清理且物理输出确已结束后Release,不能等读到FINISHED后才申请引用。 */
typedef struct {
IEC104_CommandEventType_t type; /* 决定事件的处理方式,参与身份校验。 */
uint64_t instance_id, transaction_id, event_serial; /* 实例、事务、事件三层身份,原样回传。 */
IEC104_CommandRequest_t request; /* 最后接纳的工作请求,不被撤销/不匹配请求覆盖。 */
IEC104_CommandFinishReason_t reason; /* 仅FINISHED有效;工作事件为NONE。 */
bool select_effect, execute_effect; /* 仅FINISHED有效:对应动作曾STARTED/FAILED,可能需清理。 */
} IEC104_CommandEvent_t;
typedef enum {
IEC104_COMMAND_ACTION_NOT_STARTED = 0, /* 必须保证没有任何副作用。 */
IEC104_COMMAND_ACTION_STARTED, /* 选择生效或输出启动;输出结果另行报告。 */
IEC104_COMMAND_ACTION_FAILED /* 未报告启动成功,但可能已留下副作用。 */
} IEC104_CommandActionResult_t;
/* Accept同步调用的动作,不保存函数/context指针。
* context为应用传入的上下文,可为NULL;request由模块内部快照构造,仅本次调用有效。
* 须短时返回,不得保存request指针、推进Slave或发布上报;设备的异步完成用Report_Result报告。
* 回调期间仅可Get_Status/Get_Transaction;其他修改入口拒绝重入。
* 返回值须准确说明是否有副作用,非法值按FAILED处理,模块不会重试动作。 */
typedef IEC104_CommandActionResult_t (*IEC104_CommandAction_t)(void *context,
const IEC104_CommandRequest_t *request);
typedef enum {
IEC104_COMMAND_RESULT_SUCCEEDED = 1, /* 启动后设备实际执行成功。 */
IEC104_COMMAND_RESULT_FAILED /* 启动后设备实际执行失败,不等同于拒绝启动。 */
} IEC104_CommandResult_t;
typedef enum {
IEC104_COMMAND_REJECT_NEGATIVE = 1, /* 通用策略拒绝,回COT=7、P/N=1。 */
IEC104_COMMAND_REJECT_UNKNOWN_IOA /* 点表不存在此IOA,回COT=47、P/N=1。 */
} IEC104_CommandRejectCause_t;
typedef enum {
IEC104_COMMAND_EVENT_ERROR = -1, /* 参数/来源/服务错误;不能据此重放已产生的动作。 */
IEC104_COMMAND_EVENT_IGNORED = 0, /* 不接手,I帧可继续交后续处理器。 */
IEC104_COMMAND_EVENT_HANDLED = 1 /* 已接手,包括校验拒绝/计数丢弃,不应再走44兜底。 */
} IEC104_CommandEventResult_t;
typedef enum {
IEC104_COMMAND_NEXT_ERROR = -1, /* 句柄/参数/重入或服务错误,输出未写入。 */
IEC104_COMMAND_NEXT_EMPTY = 0, /* 暂无可交付事件,可能须先读Slave通知;输出未写入。 */
IEC104_COMMAND_NEXT_READY = 1 /* 已交付纯值事件,同一事件不会再次出队。 */
} IEC104_CommandNextResult_t;
/* 以下为共同分类;OK究竟消费事件、记录结果还是固定帧序,以各接口说明为准。
* WAIT均未接手本次应用输入;只有Submit_Return_Info的OK直接代表该ASDU帧序已固定。 */
typedef enum {
IEC104_COMMAND_REPLY_STALE = -2, /* 身份/阶段已失效、事件已消费或事务结束,不得重试旧操作。 */
IEC104_COMMAND_REPLY_ERROR = -1, /* 参数/用法/重入或服务错误,检查接口具体前置条件。 */
IEC104_COMMAND_REPLY_WAIT = 0, /* 等待通知/确认/FIFO/网络,保留输入,推进事件循环后重试。 */
IEC104_COMMAND_REPLY_OK = 1 /* 本次操作已接手;不保证设备成功、网络ACK或FINISHED已就绪。 */
} IEC104_CommandReplyResult_t;
typedef enum {
IEC104_COMMAND_PHASE_SELECT_WAIT = 1, /* 等应用决定选择;决定期限从事件生成开始。 */
IEC104_COMMAND_PHASE_SELECTED, /* 已选择,等待匹配执行;选择确认可能仍未交接。 */
IEC104_COMMAND_PHASE_EXECUTE_WAIT, /* 等应用决定执行;已停止选择期限。 */
IEC104_COMMAND_PHASE_EXECUTING, /* 已启动,等待结果/返回信息/Complete/最后应答交接。 */
IEC104_COMMAND_PHASE_ENDED /* 协议已结束,可能仍保留FINISHED或未交接应答。 */
} IEC104_CommandPhase_t;
typedef struct {
bool active; /* 尚未ENDED;不是当前连接是否可操作的保证。 */
IEC104_CommandPhase_t phase; /* 当前协议阶段;结果报告和Complete不另建阶段。 */
bool confirm_submitted; /* 当前工作请求的确认已交给发送层,不表示写完或ACK。 */
/* result_reported:结果已冻结、执行期限停止;completed:已接手Complete,返回阶段关闭;
* term_submitted:ACT_TERM已交接。不配置ACT_TERM时此值一直false,不能据此等事务结束。 */
bool result_reported, completed, term_submitted;
} IEC104_CommandTransactionInfo_t;
/* 累计计数从Create起维护;查询不改变阶段、不做超时转换。
* busy包括待消费FINISHED和未交接的事务应答,不包含独立短应答。
* need_process不因发送WAIT持续置true;每轮网络推进后仍须Process重试。
* 计数用途是诊断,不是可靠账单;各计数可重叠,unsigned long到达上限后自然回绕。 */
typedef struct {
uint64_t generation; /* 当前已分派的连接代次,断线后为0。 */
/* active:模块已处理ACTIVATED且尚未停用,不代替操作前的实时会话校验;
* events_pending:须优先读取Slave通知;need_process:有可立即推进的内部工作;
* timer_active:当前启用了非零时长期限,时钟倒退检查不受此标志限制。 */
bool active, events_pending, need_process, timer_active;
bool busy; /* 唯一事务槽仍占用,此时其他站点/其他IOA也不能新建事务。 */
uint32_t timeout_remaining_ms; /* 当前期限剩余毫秒;无期限/已到期/时钟倒退时为0。 */
/* pending_replies为模块仍持有的应答数,不含发送层已接手的帧和应用保留的返回信息;
* pending_events为尚未取走的工作事件或FINISHED数,本版最多1。 */
unsigned pending_replies, pending_events;
/* accepted:生成的工作事件数(选择后执行计两次);selected/executed:对应STARTED次数;
* completed/execution_failed:成功/失败收尾数;action_failed:动作FAILED结束数。 */
unsigned long accepted, selected, executed, completed, execution_failed, action_failed;
/* submitted:所有已交接帧,含返回信息;negative_submitted:其中P/N=1的应答;
* return_info_submitted:已交接返回信息;waits:发送接口明确返回WAIT的次数。 */
unsigned long submitted, negative_submitted, return_info_submitted, waits;
/* rejected:接收校验/匹配/忙/无选择等拒绝数;app_rejected:Reject或NOT_STARTED数;
* busy_rejected:因事务槽占用拒绝数;overloads:独立应答池满或事务/事件序号耗尽次数;
* dropped_requests:过载丢新数及决定期间同点合法激活的静默丢弃数,后者不计rejected/busy_rejected/overloads;
* cancelled:本地取消、停用、断线、换代、发送/服务错误导致的事务结束总数。 */
unsigned long rejected, app_rejected, busy_rejected, overloads, dropped_requests, cancelled;
/* deactivated:成功撤销数;mismatches:选择失配数;其余三个为各期限导致的结束数。 */
unsigned long deactivated, mismatches, select_timeouts, decision_timeouts, execute_timeouts;
/* 等待阶段发现单调时钟倒退的次数;按到期路径处理,必要确认资源失败时可能改记SERVICE_ERROR。 */
unsigned long clock_errors;
} IEC104_CommandStatus_t;
/* 填写本版默认配置:45/46均要求先选择,决定15s、选择30s、执行60s,成功发ACT_TERM。
* 入参:config为调用方可写结构;NULL时无操作。返回:无;不创建实例或启动计时。 */
void IEC104_Command_Config_Default(IEC104_CommandConfig_t *config);
/* 从静态池创建并绑定一个遥控实例;不自动开启Slave或消费其通知。
* 入参:slave为有效Slave,寿命须覆盖实例;config非NULL,按值复制,调用后可修改原结构。
* 必须在开始消费Slave通知之前创建,以完整接收CONNECTED/ACTIVATED。
* 池容量IEC104_COMMAND_CAPACITY默认等于TCP服务容量;独立应答容量
* IEC104_COMMAND_REPLY_CAPACITY默认4,均在编译模块时通过宏配置。
* 返回:成功为实例句柄;配置/Slave非法、重复绑定、池或实例身份序号耗尽时为NULL。 */
IEC104_Command_t *IEC104_Command_Create(IEC104_Slave_t *slave, const IEC104_CommandConfig_t *config);
/* 归还静态实例槽;不隐式Cancel,不关闭Slave,也不代替应用释放站点引用。
* 入参:self为待销毁实例;事务槽、未读事件及全部独立应答槽必须均空,且不在动作回调内。
* 返回:成功true,句柄随即失效;条件不满足或句柄无效为false,实例保留。 */
bool IEC104_Command_Destroy(IEC104_Command_t *self);
/* 本地结束事务,原因CANCELLED;保留欠应用的FINISHED,清空模块尚持有的全部应答。
* 已交发送层的帧无法撤回;保持会话激活,不回滚物理动作。
* 入参:self为实例;回调中/无效句柄无操作。返回:无;应用仍须读完FINISHED。
* ENDED事务保持首次终结原因,不重复产生FINISHED。 */
void IEC104_Command_Cancel(IEC104_Command_t *self);
/* 接收Slave通知并推进会话/请求状态;不调用应用动作,不保存event或其ASDU借用指针。
* 入参:self为实例;event是所属Slave按FIFO取出的通知,须在通知内容仍有效时分派。
* 全部连接/启停通知必须送达;通知消费不受队列里后续WAIT_EVENTS阻挡。
* 返回:激活且视图可用时接手配置内45/46,当前会话状态通知也为HANDLED;
* 未激活的I帧、视图不可用、未接手类型或旧代次通知为IGNORED;
* 参数、来源、重入或服务错误为ERROR。非法请求可否定或丢新计数,HANDLED不表示已应答。
* SELECT_WAIT/EXECUTE_WAIT期间,同会话、同station_id、同IOA的合法激活静默丢弃,计dropped_requests,
* 返回HANDLED;不比较其他命令字段,不覆盖原请求/事件、不刷新期限。格式校验、撤销及其他点忙拒绝不变。
* 对IGNORED的I帧继续分派后续业务,最后由应用兜底;不要对HANDLED再发44。 */
IEC104_CommandEventResult_t IEC104_Command_Handle_Event(IEC104_Command_t *self,
const IEC104_SlaveEvent_t *event);
/* 推进会话检查、到期转换、发送队首及事务回收;不调用应用代码。
* 入参:self为实例;每轮网络/通知处理后调用,最多推进2*(1+独立应答槽数)步。
* 队首WAIT立即让出,不能用紧循环等待发送;需处理Slave通知/网络后再次调用。
* 返回:true包括无事、背压等待或事务已终结;false为参数/重入或服务错误。
* 发送故障也可能已经转成FINISHED,此函数返回true不代表链路或设备执行成功。 */
bool IEC104_Command_Process(IEC104_Command_t *self);
/* 取出一条应用事件,同一工作事件最多交付一次;取出不等于Accept/Reject消费。
* 入参:self为实例;event非NULL,接收可按值保存的事件副本,仅READY时写入。
* 返回:READY/EMPTY/ERROR,见NextResult;EMPTY也可能表示须先读Slave通知。
* 工作事件交付前检查会话/期限,已有FINISHED可在离线或尚有Slave通知时读取。
* 读取FINISHED可能立即释放模块的站点引用,应用跨循环引用必须此前已自行Acquire。 */
IEC104_CommandNextResult_t IEC104_Command_NextEvent(IEC104_Command_t *self,
IEC104_CommandEvent_t *event);
/* 接受一个已由NextEvent交付的工作事件;先消费身份,再同步至多调用一次action。
* 入参:self为实例;event须原样传回SELECT/EXECUTE事件;context原样转交action,不保存。
* SELECT的action可为NULL,选择生效但select_effect=false;EXECUTE必须提供action。
* 返回:OK只表示工作事件已消费,不表示动作STARTED或确认已交接;动作失败也返回OK。
* WAIT为先读Slave通知,事件未消费;STALE为身份/阶段失效,不得执行输出;
* ERROR为参数/重入/服务或确认资源错误,动作未调用。
* 即使action返回STARTED,提交确认时仍可能结束事务;应用应查询事务并继续接收FINISHED。 */
IEC104_CommandReplyResult_t IEC104_Command_Accept(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandAction_t action, void *context);
/* 不调用动作,消费工作事件,安排否定ACT_CON或未知IOA应答,以REJECTED结束事务。
* 入参:self为实例;event为尚未消费的SELECT/EXECUTE;cause为两个RejectCause之一。
* 返回:OK表示事件已消费且应答已安排,可能已提交或因随后故障被取消;
* WAIT未消费、须先处理通知;STALE身份/阶段已失效;ERROR参数/原因/重入或服务错误。
* FINISHED可先于否定应答交接,读取FINISHED不代表槽已释放。 */
IEC104_CommandReplyResult_t IEC104_Command_Reject(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandRejectCause_t cause);
/* 报告设备结果,结果只记录一次并冻结;不发返回信息,也不隐式Complete。
* 本函数及下列Submit_Return_Info/Complete只接受Accept动作返回STARTED的原EXECUTE事件。
* 入参:self为实例,event为上述事件,result为SUCCEEDED或FAILED。
* 可早于ACT_CON交接;设备完成后应及时报告,OK即停止执行期限,此后发送等待无业务超时。
* 返回:OK已记录;WAIT因未读通知尚未记录;STALE事务结束/超时或事件不符;
* ERROR参数/结果非法、已报告/已Complete、重入或服务错误。 */
IEC104_CommandReplyResult_t IEC104_Command_Report_Result(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result);
/* 通过原连接Try路径提交一帧返回信息,不经过可覆盖的NORMAL队列。
* 入参:self为实例;event为启动成功的原EXECUTE;asdu为有效、完整的监视方向ASDU。
* 类型1..21/30..40,至少一对象,COT=11或3,P/N=0,TEST/CA与命令一致;完整校验对象。
* IOA可与命令IOA不同;本版命令TEST恒为0。调用可以早于或晚于Report_Result,但须早于Complete。
* 模块有更早待交应答、确认未交接、网络背压或未读通知时WAIT,不缓存ASDU、也不占排序位置。
* 返回:OK为发送层已接手且帧序固定,可释放/复用ASDU缓冲,不能重交;WAIT须保留ASDU重试;
* ERROR为ASDU/参数/重入/服务错误或Complete后仍待收尾,尚未接手;STALE为事务/事件失效。
* 正常STOP且通知待分派时先WAIT;其他发送ERROR/FATAL终结事务并返回STALE,不可重试旧ASDU。
* 必要的后续变位由应用另按COT=3上报。
* 同一变化勿再经Enqueue_Event重复发布;成功提交不表示对端ACK,断线不会迁移到新连接。 */
IEC104_CommandReplyResult_t IEC104_Command_Submit_Return_Info(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, const IEC60870_Asdu_t *asdu);
/* 声明本事务不再产生返回信息;按配置安排ACT_TERM并由Process完成后续交接。
* 入参:self为实例,event为启动成功的原EXECUTE;结果须已报告,全部返回信息须已提交OK。
* 允许零帧返回信息;可在确认未交接时调用,模块保持确认先于ACT_TERM的顺序。
* 返回:OK立即关闭返回阶段,不必重调;WAIT为未读通知,尚未接手;
* ERROR为尚未报告结果、仍待收尾时重复调用、参数/重入/服务错误;已结束/事件不符为STALE。
* 不发ACT_TERM时也须等待执行确认交接,之后才产生FINISHED;无发送等待时可能立即结束。
* 失败配置发TERM时P/N=0表示流程结束,不能据此认定设备成功。 */
IEC104_CommandReplyResult_t IEC104_Command_Complete(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event);
/* 只读查询仍占槽的事务,可在动作回调内使用,不做会话推进或期限转换。
* 入参:self为实例;event可为该事务任一已交付的SELECT/EXECUTE/FINISHED;info非NULL。
* 返回:true写入快照,ENDED的active=false;无效参数/身份、槽已回收时false且不修改info。
* 不保留历史事务;刚读出的FINISHED若已触发槽回收,使用它查询也会false。 */
bool IEC104_Command_Get_Transaction(const IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandTransactionInfo_t *info);
/* 只读查询会话、调度提示及累计计数,可在动作回调内使用。
* 入参:self为实例,status非NULL接收快照;计算剩余时间但不转换已到期事务。
* 返回:true写入快照;句柄/参数/Slave服务无效时false且不修改status。
* events_pending优先读Slave通知;need_process用于立即调度,timer_active配合剩余时间等待。
* need_process=false不免除每轮网络推进后的Process,也不代表应用没有待重试的ASDU/Complete。 */
bool IEC104_Command_Get_Status(const IEC104_Command_t *self, IEC104_CommandStatus_t *status);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_COMMAND_H */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,430 @@
/* 独立主站字节测试:启动示例自己的Windows Net后端,仅连接127.0.0.1随机端口。
* Node内置模块即可运行,不安装包、不复用待测ASDU编码器,不更改系统时钟。
*/
'use strict';
const assert = require('node:assert/strict');
const net = require('node:net');
const { spawn, spawnSync } = require('node:child_process');
const { once } = require('node:events');
const { setTimeout: delay } = require('node:timers/promises');
const executable = process.argv[2];
if (!executable) throw new Error('Usage: node slave_example_loopback_test.cjs <example.exe>');
class Master {
constructor(socket) {
this.socket = socket;
this.buffer = Buffer.alloc(0);
this.frames = [];
this.waiters = [];
this.ns = this.nr = 0;
socket.on('data', data => {
this.buffer = Buffer.concat([this.buffer, data]);
try {
while (this.buffer.length >= 2) {
assert.equal(this.buffer[0], 0x68);
assert(this.buffer[1] >= 4 && this.buffer[1] <= 253);
const length = this.buffer[1] + 2;
if (this.buffer.length < length) break;
const frame = this.buffer.subarray(0, length);
this.buffer = this.buffer.subarray(length);
const waiter = this.waiters.shift();
if (waiter) { clearTimeout(waiter.timer); waiter.resolve(frame); }
else this.frames.push(frame);
}
} catch (error) { this.fail(error); socket.destroy(); }
});
socket.on('error', error => this.fail(error));
socket.on('close', () => this.fail(new Error('Connection closed')));
}
fail(error) {
this.error = error;
for (const waiter of this.waiters.splice(0)) {
clearTimeout(waiter.timer);
waiter.reject(error);
}
}
next() {
if (this.frames.length) return Promise.resolve(this.frames.shift());
if (this.error) return Promise.reject(this.error);
return new Promise((resolve, reject) => {
const waiter = { resolve, reject };
waiter.timer = setTimeout(() => {
this.waiters.splice(this.waiters.indexOf(waiter), 1);
reject(new Error('Timed out waiting for an APDU'));
}, 1500);
this.waiters.push(waiter);
});
}
async control(code, expected) {
this.socket.write(Buffer.from([0x68, 4, code, 0, 0, 0]));
for (let i = 0; i < 8; i++) {
const frame = await this.next();
if (frame[2] === 1) continue; // 主站I帧的S确认允许穿插。
assert.deepEqual([...frame], [0x68, 4, expected, 0, 0, 0]);
return;
}
throw new Error('Missing U-frame confirmation');
}
frame(payload) {
const frame = Buffer.alloc(payload.length + 6);
frame[0] = 0x68;
frame[1] = payload.length + 4;
frame.writeUInt16LE(this.ns++ << 1, 2);
frame.writeUInt16LE(this.nr << 1, 4);
Buffer.from(payload).copy(frame, 6);
return frame;
}
async send(payload, fragmented = false) {
const frame = this.frame(payload);
if (fragmented) {
this.socket.write(frame.subarray(0, 3));
await delay(10);
this.socket.write(frame.subarray(3));
} else this.socket.write(frame);
}
async readASDU() {
for (let i = 0; i < 8; i++) {
const frame = await this.next();
if (frame[2] === 1) continue;
assert.equal(frame[2] & 1, 0);
assert.equal(frame.readUInt16LE(2) >> 1, this.nr++);
assert.equal(frame.readUInt16LE(4) >> 1, this.ns);
const ack = Buffer.from([0x68, 4, 1, 0, 0, 0]);
ack.writeUInt16LE(this.nr << 1, 4);
this.socket.write(ack); // k=1,每帧ACK后服务才可继续发下一帧。
return frame.subarray(6);
}
throw new Error('Missing I frame');
}
async close() {
const closed = once(this.socket, 'close');
this.socket.end();
await closed;
}
}
const signals = [0, 1, 0, 1, 1, 0, 1, 0];
const doubleSignals = Array(8).fill(1);
const measures = [2300, 2310, 2290, 5000, -125, 0, 1234, 42];
const request = (oa, qoi = 20, ca = 7) => [100, 1, 0x86, oa, ca & 0xff, ca >> 8, 0, 0, 0, qoi];
// 构造单对象45/46命令原文:CA/IOA按小端编码,QU固定为1(短脉冲)。
// type/oa/ioa/state来自用例,select决定S/E;cot/ca默认激活原因6和示例站点7。
// 返回普通字节数组,也用于故意构造不支持类型的兜底请求,不在此执行业务校验。
const commandRequest = (type, oa, ioa, state, select, cot = 6, ca = 7) =>
[type, 1, cot, oa, ca & 0xff, ca >> 8, ioa & 0xff, (ioa >> 8) & 0xff, ioa >> 16,
state | 4 | (select ? 0x80 : 0)];
// 发送payload并断言紧随其后的应答;除COT/P/N外须回显原请求,TEST位也须保留。
// master负责链路收发及确认,cot/negative描述预期应答;Promise完成表示该帧已核对。
async function commandReply(master, payload, cot, negative = false) {
await master.send(payload);
const expected = [...payload]; expected[2] = (payload[2] & 0x80) | cot | (negative ? 0x40 : 0);
assert.deepEqual([...(await master.readASDU())], expected);
}
// 验证一次选择后执行的真实TCP全流程;monitorIoa为该命令对应的监视方向地址。
// 顺序逐帧读取:选择ACT_CON、执行ACT_CON、COT=11返回信息、执行原文ACT_TERM。
// 链路窗口k=1,只有前一帧处理完才能推进下一帧,可暴露返回信息/终止抢先的问题。
async function commandExecute(master, type, oa, ioa, state, monitorIoa) {
await commandReply(master, commandRequest(type, oa, ioa, state, true), 7);
const execute = commandRequest(type, oa, ioa, state, false);
await commandReply(master, execute, 7);
assert.deepEqual([...(await master.readASDU())],
[type === 45 ? 1 : 3, 1, 11, oa, 7, 0, monitorIoa & 0xff, (monitorIoa >> 8) & 0xff, 0, state]);
const terminated = [...execute]; terminated[2] = 10;
assert.deepEqual([...(await master.readASDU())], terminated);
}
// 独立构造Type49:有符号16位设定值及QOS,QL固定为0,不复用待测编解码器。
const scaledCommandRequest = (oa, ioa, value, select) =>
[49, 1, 6, oa, 7, 0, ioa & 0xff, (ioa >> 8) & 0xff, ioa >> 16,
value & 0xff, (value >> 8) & 0xff, select ? 0x80 : 0];
async function scaledCommandExecute(master, oa, ioa, value, monitorIoa) {
await commandReply(master, scaledCommandRequest(oa, ioa, value, true), 7);
const execute = scaledCommandRequest(oa, ioa, value, false);
await commandReply(master, execute, 7);
assert.deepEqual([...(await master.readASDU())],
[11, 1, 11, oa, 7, 0, monitorIoa & 0xff, (monitorIoa >> 8) & 0xff, 0,
value & 0xff, (value >> 8) & 0xff, 0]);
const terminated = [...execute]; terminated[2] = 10;
assert.deepEqual([...(await master.readASDU())], terminated);
}
const initialCounters = [120000, 345600, 7890, 0, 1, 65535, 2147483647, 42];
const counterRequest = (oa, qcc = 5, cot = 6, test = false, ca = 7) =>
[101, 1, cot | (test ? 0x80 : 0), oa, ca & 0xff, ca >> 8, 0, 0, 0, qcc];
const clockRequest = (oa, cot = 6, test = false, ca = 7) =>
[103, 1, cot | (test ? 0x80 : 0), oa, ca & 0xff, ca >> 8, 0, 0, 0, 0x39, 0x30, 34, 12, 28, 10, 26];
let counterSubmitted = 0, counterCompleted = 0;
async function readCounters(master, oa, qcc = 5, values = initialCounters, sequence = 0) {
const group = qcc & 0x3f;
const first = group === 2 ? 4 : 0, end = group === 1 ? 4 : 8;
const count = 2 + (end - first) / 4;
for (let frame = 0; frame < count; frame++) {
const asdu = await master.readASDU();
if (frame === 0 || frame === count - 1) {
assert.deepEqual([...asdu], counterRequest(oa, qcc, frame === 0 ? 7 : 10));
} else {
assert.equal(asdu.length, 38);
assert.deepEqual([...asdu.subarray(0, 6)], [15, 4, group === 5 ? 37 : 37 + group, oa, 7, 0]);
for (let point = 0; point < 4; point++) {
const offset = 6 + 8 * point, index = first + 4 * (frame - 1) + point;
assert.equal(asdu.readUIntLE(offset, 3), 3001 + index);
assert.equal(asdu.readInt32LE(offset + 3), values[index]);
assert.equal(asdu[offset + 7], sequence);
}
}
}
counterSubmitted += count;
counterCompleted++;
}
async function counterRead(master, oa, qcc = 5, values = initialCounters, sequence = 0, ca = 7) {
await master.send(counterRequest(oa, qcc, 6, false, ca));
await readCounters(master, oa, qcc, values, sequence);
}
async function counterReply(master, payload, cot, negative = false) {
await master.send(payload);
const expected = [...payload]; expected[2] = (payload[2] & 0x80) | cot | (negative ? 0x40 : 0);
assert.deepEqual([...(await master.readASDU())], expected);
counterSubmitted++;
if (!negative) counterCompleted++;
}
async function totalCall(master, oa, fragmented = false, qoi = 20, ca = 7) {
await master.send(request(oa, qoi, ca), fragmented);
await readTotalCall(master, oa, qoi);
}
async function readTotalCall(master, oa, qoi = 20, busyRequests = []) {
const original = request(oa, qoi);
// 站总召依次返回单点、双点、遥测;遥信分组含前两类,遥测分组只含最后一类。
// 每类8点、每帧4点,首尾另有ACT_CON与ACT_TERM。
const types = qoi === 20 ? [1, 3, 11] : qoi === 21 ? [1, 3] : [11];
const count = 2 + types.length * 2;
const busyReplies = busyRequests.map(payload => {
const expected = [...payload]; expected[2] = 0xc7;
if (expected[4] === 0xff && expected[5] === 0xff) { expected[4] = 7; expected[5] = 0; }
return Buffer.from(expected);
});
for (let f = 0; f < count || busyReplies.length;) {
const asdu = await master.readASDU();
if ((asdu[2] & 0x40) !== 0) {
const index = busyReplies.findIndex(expected => expected.equals(asdu));
assert(index >= 0, `Unexpected negative reply: ${asdu.toString('hex')}`);
busyReplies.splice(index, 1);
continue;
}
assert(f < count, 'Unexpected response after ACT_TERM');
if (f === 0 || f === count - 1) {
const expected = [...original];
expected[2] = f === 0 ? 0x87 : 0x8a;
assert.deepEqual([...asdu], expected);
} else {
const type = types[Math.floor((f - 1) / 2)];
const digital = type !== 11;
const values = type === 1 ? signals : type === 3 ? doubleSignals : measures;
const baseIoa = type === 1 ? 1001 : type === 3 ? 1101 : 2001;
const width = digital ? 4 : 6;
const first = ((f - 1) % 2) * 4;
assert.equal(asdu.length, 6 + width * 4);
assert.deepEqual([...asdu.subarray(0, 6)], [type, 4, 0x80 | qoi, oa, 7, 0]);
for (let p = 0; p < 4; p++) {
const offset = 6 + p * width;
assert.equal(asdu.readUIntLE(offset, 3), baseIoa + first + p);
if (digital) assert.equal(asdu[offset + 3], values[first + p]);
else {
assert.equal(asdu.readInt16LE(offset + 3), values[first + p]);
assert.equal(asdu[offset + 5], 0);
}
}
}
f++;
}
}
async function main() {
const help = spawnSync(executable, ['--help'], { windowsHide: true, encoding: 'utf8' });
assert.ifError(help.error);
assert.equal(help.status, 0, help.stderr);
assert(help.stdout.includes('--run-ms'));
assert(help.stdout.includes('--clock simulated|system'));
assert(help.stdout.includes('Type45 IOA6001..6008'));
assert(help.stdout.includes('Type46 IOA6101..6108'));
assert(help.stdout.includes('Type49 IOA6301..6308'));
const invalid = spawnSync(executable, ['--ca', '0'], { windowsHide: true, encoding: 'utf8' });
assert.ifError(invalid.error);
assert.equal(invalid.status, 1);
const invalidClock = spawnSync(executable, ['--clock', 'invalid'], { windowsHide: true, encoding: 'utf8' });
assert.ifError(invalidClock.error);
assert.equal(invalidClock.status, 1);
const child = spawn(executable, ['--port', '0', '--ca', '7', '--window', '1', '--run-ms', '12000'],
{ windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
let output = '', errors = '';
const sockets = [];
child.stdout.on('data', data => { output += data.toString(); });
child.stderr.on('data', data => { errors += data.toString(); });
const exit = once(child, 'exit');
const deadline = setTimeout(() => { child.kill(); }, 20000);
try {
const port = await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`No listening port: ${output} ${errors}`)), 2000);
child.once('error', error => { clearTimeout(timer); reject(error); });
child.stdout.on('data', () => {
const match = output.match(/LISTEN 127\.0\.0\.1:(\d+) CA=7/);
if (match) { clearTimeout(timer); resolve(Number(match[1])); }
});
});
async function connect() {
const socket = net.connect({ host: '127.0.0.1', port });
sockets.push(socket);
const master = new Master(socket);
await once(socket, 'connect');
await master.control(7, 0x0b);
return master;
}
const master = await connect();
// 一次write内的106延时获得和102读取逐条回显,k=1时第二条由HIGH等待首条ACK。
const unsupportedBatch = [
[106, 1, 0x86, 0x18, 7, 0, 0, 0, 0, 0x39, 0x30],
[102, 1, 5, 0x19, 7, 0, 0xe9, 3, 0]
];
master.socket.write(Buffer.concat(unsupportedBatch.map(payload => master.frame(payload))));
for (const payload of unsupportedBatch) {
const expected = [...payload]; expected[2] = (payload[2] & 0x80) | 0x40 | 44;
assert.deepEqual([...(await master.readASDU())], expected);
}
// 默认模拟时钟:成功、TEST拒绝、错误COT及广播实际CA确认都保持请求时标原文。
for (const [payload, cot, negative] of [
[clockRequest(0x10), 7, false],
[clockRequest(0x11, 6, true), 7, true],
[clockRequest(0x12, 5), 45, true],
[clockRequest(0x13, 6, false, 65535), 7, false]
]) {
await master.send(payload);
const expected = [...payload];
expected[2] = (payload[2] & 0x80) | cot | (negative ? 0x40 : 0);
expected[4] = 7; expected[5] = 0;
assert.deepEqual([...(await master.readASDU())], expected);
}
// 同批103先保存执行任务,102当场兜底;Try确认必须等已提交的44之后发送。
const clockBatch = clockRequest(0x14);
const readBatch = [102, 1, 5, 0x15, 7, 0, 0xe9, 3, 0];
master.socket.write(Buffer.concat([master.frame(clockBatch), master.frame(readBatch)]));
const readRejected = [...readBatch]; readRejected[2] = 0x40 | 44;
assert.deepEqual([...(await master.readASDU())], readRejected);
const clockConfirmed = [...clockBatch]; clockConfirmed[2] = 7;
assert.deepEqual([...(await master.readASDU())], clockConfirmed);
// 遥控默认先选择;k=1验证确认、返回信息和终止逐帧交接,后续总召必须读到新值。
await commandExecute(master, 45, 0x71, 6001, 1, 1001);
// 同步后续总召的预期值,验证遥控修改的是示例应用点表而非仅构造一次返回帧。
signals[0] = 1;
await commandExecute(master, 46, 0x72, 6103, 2, 1103);
// 站总召和遥信分组均须读回双点1103的新位置,其余双点仍为初值1。
doubleSignals[2] = 2;
// 设定值先验证应用限值拒绝,再走完整事务;后面的站总召和QOI=22必须读到新运行值。
await commandReply(master, scaledCommandRequest(0x79, 6301, 10001, true), 7, true);
await scaledCommandExecute(master, 0x7a, 6301, -1234, 2001);
measures[0] = -1234;
// 撤销故意使用不同状态值:匹配只要求类型/站点/IOA/OA,不比较命令状态。
await commandReply(master, commandRequest(45, 0x73, 6002, 0, true), 7);
await commandReply(master, commandRequest(45, 0x73, 6002, 1, true, 8), 9);
// 选择已撤销,默认不允许直接执行,因此后续执行必须被否定。
await commandReply(master, commandRequest(45, 0x73, 6002, 0, false), 7, true);
// 公共地址错误由模块判定;遥信地址1001不是Type45命令地址,由应用点表返回47。
await commandReply(master, commandRequest(46, 0x74, 6101, 1, true, 6, 65535), 46, true);
await commandReply(master, commandRequest(45, 0x75, 1001, 1, true), 47, true);
// TEST命令须原样保留TEST位并否定,不能启动模拟输出。
const commandTest = commandRequest(45, 0x76, 6001, 0, true); commandTest[2] |= 0x80;
await commandReply(master, commandTest, 7, true);
// 示例只启用45/46/49;47及附完整CP56字节的58须继续交给44兜底。
await commandReply(master, commandRequest(47, 0x77, 1001, 1, false), 44, true);
const timedCommand = commandRequest(58, 0x78, 1001, 0, true).concat([0, 0, 0, 0, 1, 1, 26]);
await commandReply(master, timedCommand, 44, true);
await counterRead(master, 0x20);
await counterReply(master, counterRequest(0x21, 0x45), 7);
await counterRead(master, 0x22, 5, initialCounters, 1);
await counterRead(master, 0x23, 1, initialCounters, 1);
await counterRead(master, 0x24, 2, initialCounters, 1);
await counterReply(master, counterRequest(0x25, 3), 7, true);
await counterReply(master, counterRequest(0x26, 4), 7, true);
await counterReply(master, counterRequest(0x27, 5, 8), 45, true);
// TEST requests must not freeze or clear real counters.
await counterReply(master, counterRequest(0x28, 0x85, 6, true), 7, true);
await counterRead(master, 0x29, 5, initialCounters, 1, 65535);
await counterReply(master, counterRequest(0x2a, 0x85), 7);
await counterRead(master, 0x2b, 5, initialCounters, 2);
await counterReply(master, counterRequest(0x2c, 0x45), 7);
const zeroCounters = initialCounters.map(() => 0);
await counterRead(master, 0x2d, 5, zeroCounters, 3);
await counterReply(master, counterRequest(0x2e, 0xc5), 7);
await counterRead(master, 0x2f, 5, zeroCounters, 3);
await totalCall(master, 0x5a, true);
await totalCall(master, 0x5b, false, 21);
await totalCall(master, 0x5c, false, 22);
// 广播请求的确认、数据和终止均按实际本站CA=7核对。
await totalCall(master, 0x5d, true, 20, 65535);
await totalCall(master, 0x5e, false, 21, 65535);
await totalCall(master, 0x5f, false, 22, 65535);
// 连续提交不同QOI:只执行第一个,后两个否定;结束后由主站重发才执行。
const burst = [[0x31, 22, 65535], [0x32, 20, 65535], [0x33, 21, 7]];
master.socket.write(Buffer.concat(burst.map(([oa, qoi, ca]) => master.frame(request(oa, qoi, ca)))));
await readTotalCall(master, 0x31, 22, burst.slice(1).map(([oa, qoi, ca]) => request(oa, qoi, ca)));
for (const [oa, qoi] of burst.slice(1)) await totalCall(master, oa, false, qoi);
for (const [index, value, cot] of [[4, 8, 46], [9, 37, 7], [9, 36, 7], [0, 200, 44]]) {
const invalidRequest = request(0x55);
invalidRequest[index] = value;
if (index === 9) { invalidRequest[4] = invalidRequest[5] = 0xff; }
await master.send(invalidRequest);
const expected = [...invalidRequest];
expected[2] = 0xc0 | cot;
if (index === 9) { expected[4] = 7; expected[5] = 0; }
assert.deepEqual([...(await master.readASDU())], expected);
}
// 同一接收批次的激活/撤销在驱动业务前按FIFO处理;随后重复撤销应否定确认。
const activation = request(0x4a, 21, 65535);
const deactivation = request(0x4a, 21); deactivation[2] = 0x88;
master.socket.write(Buffer.concat([master.frame(activation), master.frame(deactivation)]));
const deactCon = [...deactivation]; deactCon[2] = 0x89;
assert.deepEqual([...(await master.readASDU())], deactCon);
deactivation[4] = deactivation[5] = 0xff;
await master.send(deactivation);
deactCon[2] = 0xc9;
assert.deepEqual([...(await master.readASDU())], deactCon);
await master.control(0x13, 0x23);
await master.control(7, 0x0b);
// k=1: both instances must make progress, preserving their own response order.
master.socket.write(Buffer.concat([master.frame(request(0x6b)), master.frame(counterRequest(0x60))]));
const generalFrames = [], counterFrames = [];
for (let index = 0; index < 12; index++) {
const asdu = await master.readASDU();
(asdu[0] === 101 || asdu[0] === 15 ? counterFrames : generalFrames).push(asdu);
}
assert.equal(generalFrames.length, 8);
assert.equal(counterFrames.length, 4);
await readTotalCall({ readASDU: async () => generalFrames.shift() }, 0x6b);
await readCounters({ readASDU: async () => counterFrames.shift() }, 0x60, 5, zeroCounters, 3);
await master.close();
const next = await connect();
await totalCall(next, 0x7c);
await next.close();
const [code] = await exit;
assert.equal(code, 0, `${output}\n${errors}`);
assert.match(output, /STOP submitted=83 gi_submitted=11 /);
assert.match(output, /deactivated=1 timeouts=0/);
assert.match(output, /negative_submitted=\d+ dropped_requests=0/);
assert.match(output, new RegExp(`counter_submitted=${counterSubmitted} counter_completed=${counterCompleted}`));
assert.match(output, /clock_executed=3 clock_synchronized=3 clock_submitted=5/);
// 四次成功选择含一次撤销;三个执行均返回当前值,越限49不启动任何动作。
assert.match(output, /command_selected=4 command_executed=3 command_completed=3 command_submitted=\d+ command_returns=3/);
assert.equal(errors, '');
console.log('Slave example loopback: passed (real TCP, Type45/46/49 select/execute/return/termination, scaled limits, command cancel/TEST/address/unsupported types, changed single/double/scaled GI values, batched 106+102 and 103+102, simulated clock, counters, k=1, STOP/START, reconnect, cleanup)');
} finally {
clearTimeout(deadline);
for (const socket of sockets) socket.destroy();
if (child.exitCode === null) child.kill();
}
}
main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -0,0 +1,340 @@
/*
* 模块:IEC104控制命令业务层,45..51及58..64,COT=6激活、COT=8撤销。
* 职责:校验请求、选择/执行匹配、撤销、三类期限、原文确认和事务收尾。
* 点表、闭锁、远方/就地策略及实际输出由应用负责;本模块不判定设备操作是否安全。
* 每个Slave至多一个实例,每实例至多一个未释放事务;其他业务仍独立推进。
* 静态存储、无动态分配、不保存回调;所有入口须与Slave在同一事件循环串行调用。
* 只有Accept调用应用动作,先消费事件、再至多调用一次;Handle_Event/Process不调用应用。
* 返回信息由Submit_Return_Info沿原连接提交,确认/返回信息/终止保持顺序。
*
* 接入顺序:先Create,再按Slave通知FIFO分派Handle_Event;每轮Process后读空NextEvent。
* SELECT/EXECUTE事件由Accept或Reject消费;EXECUTE启动成功后,先Report_Result,
* 再逐帧Submit_Return_Info,全部OK后Complete;最终读取FINISHED清理应用资源。
* Report_Result也可晚于返回信息,但必须早于Complete,且设备结果产生后应及时报告。
* WAIT表示本次应用操作尚未接手,保留输入并在读取通知/推进网络后重试;OK不得重复提交。
* 动作/确认/设备结果/FINISHED是不同节点:FINISHED只结束协议事务,不撤销物理输出。
* STOP、断线或Cancel不回滚动作,也不把原连接的返回信息转交到新连接。
*
* 退出顺序:Cancel、读完FINISHED、Slave_Close、读空并分派通知、Destroy,
* 然后由应用移除站点并销毁Slave。模块不负责Slave或应用持有引用的释放。
*/
#ifndef IEC104_COMMAND_H
#define IEC104_COMMAND_H
#include "IEC104_Slave.h"
#include "IEC60870_Time.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 不透明静态池句柄;Destroy成功后失效,不得按值复制内部对象或继续使用旧指针。 */
typedef struct IEC104_Command IEC104_Command_t;
/* 供无副作用的类型常量使用;45..51映射bit0..6,58..64映射bit7..13。
* 范围外为0,不作非法移位。Create接受全部14位,默认只启用45、46。 */
#define IEC104_COMMAND_TYPE_BIT(type) ((uint16_t)( \
((type) >= 45U && (type) <= 51U) ? (1U << ((type) - 45U)) : \
(((type) >= 58U && (type) <= 64U) ? (1U << ((type) - 51U)) : 0U)))
/* 创建时复制的配置。期限以Net单调时钟计算,等于边界即到期;
* 0仅禁用时长限制,等待阶段遇到时钟倒退仍终结。选择期限包含确认的发送等待;
* Report_Result之后没有业务期限,应用漏调Complete会一直占用事务槽。 */
typedef struct {
uint16_t type_mask; /* 默认45|46,不能为空;可显式启用45..51、58..64。 */
uint16_t select_required_mask; /* 默认45|46,必须是type_mask子集;51/64无S/E,禁止要求选择。 */
uint32_t select_timeout_ms; /* 选择动作返回至匹配执行受理,默认30000,不得为0。 */
uint32_t decision_timeout_ms; /* 工作事件生成至Accept/Reject,默认15000;0禁用。 */
uint32_t execute_timeout_ms; /* 动作STARTED至Report_Result,默认60000;0禁用。 */
bool send_act_term; /* 成功后发ACT_TERM,默认true;false时失败也不发。 */
bool act_term_on_failure; /* 默认false;true且send_act_term时,启动后报告失败也发COT=10、P/N=0。 */
uint32_t time_tag_max_delay_ms; /* 默认10000;0只关闭时差检查,仍拒绝IV=1/UTC换算失败。 */
int16_t master_utc_offset_minutes; /* 主站标准时区,默认480,范围-1439..1439;SU另由公共层处理。 */
bool negative_on_expired; /* 默认false,过期激活静默丢弃;true时回否定ACT_CON。 */
} IEC104_CommandConfig_t;
/* 可按值保存的请求快照;不含站点/ASDU等借用指针。
* state为单命令0/1、双命令1/2、步调节1降/2升;具体操作含义由应用点表定义。
* accepted_ms为Handle_Event受理时刻,包含Slave通知排队时间,不是网络接收时间。 */
typedef struct {
uint64_t generation, receive_serial; /* 原连接代次、当前工作请求的I帧接收编号。 */
uint64_t station_id; /* 站点注册身份;同CA重新注册后此值不同。 */
uint16_t common_address; /* 请求公共地址CA,不接受广播65535。 */
uint8_t originator_address, type_id; /* 请求源发地址OA、类型45..51或58..64。 */
uint32_t ioa; /* 非零24位信息对象地址;存在性由应用点表确认。 */
bool is_select, selected; /* S/E及本事务匹配选择标志;51/64两者恒为false。 */
uint8_t state, qualifier; /* 基础45..47:状态/QU;48..50:0/QL(0..127);51:0/0。 */
union {
int16_t normalized; /* 48/61:原始定点编码,实际值为normalized/32768.0。 */
int16_t scaled; /* 49/62:有符号标度值。 */
float short_float; /* 50/63:有限IEEE 754 binary32。 */
uint32_t bitstring; /* 51/64:32位数据,不包含选择位。 */
} value; /* 按type_id读取;状态命令清零。 */
bool has_time; /* 58..64为true,字段含义与对应基础类型相同。 */
IEC60870_CP56Time2a_t time; /* 解码后的原始时标,保留IV/SU,不是可直接发送的字节布局。 */
uint64_t time_utc_ms; /* UTC纪元毫秒,含毫秒余数;无时标为0。受理后不再判时差。 */
uint64_t accepted_ms; /* 当前工作请求受理时的单调毫秒。 */
} IEC104_CommandRequest_t;
/* SELECT和EXECUTE为待决定工作事件;FINISHED仅通知清理,无需应答。 */
typedef enum {
IEC104_COMMAND_SELECT = 1, /* 应用检查策略并Accept/Reject选择,可执行返校动作。 */
IEC104_COMMAND_EXECUTE, /* 应用Accept/Reject执行;selected区分选择后执行和直接执行。 */
IEC104_COMMAND_FINISHED /* 该事务唯一的结束通知,原因及副作用标志在事件中给出。 */
} IEC104_CommandEventType_t;
/* 首次结束时冻结,后续STOP/Cancel不会覆盖;枚举不表示设备输出已停止。 */
typedef enum {
IEC104_COMMAND_FINISH_NONE = 0, /* 工作事件的占位值,不是FINISHED的正常原因。 */
IEC104_COMMAND_FINISH_COMPLETED, /* 应用报告成功,Complete后最后应答已交接。 */
IEC104_COMMAND_FINISH_EXECUTION_FAILED, /* 输出已启动,但应用报告失败,协议收尾已交接。 */
IEC104_COMMAND_FINISH_REJECTED, /* 应用Reject,或动作返回NOT_STARTED。 */
IEC104_COMMAND_FINISH_ACTION_FAILED, /* 动作返回FAILED/非法值,可能存在副作用。 */
IEC104_COMMAND_FINISH_MISMATCH, /* 选择确认交接后,同一点执行的类型/OA/值/限定词不符。 */
IEC104_COMMAND_FINISH_DEACTIVATED, /* 主站COT=8成功撤销尚未启动执行的事务。 */
IEC104_COMMAND_FINISH_SELECT_TIMEOUT, /* 选择保持到期,释放选择,已交接的确认无法撤回。 */
IEC104_COMMAND_FINISH_DECISION_TIMEOUT, /* 未及时Accept/Reject,模块安排否定确认。 */
IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT, /* 未及时Report_Result;保留肯定确认,不发ACT_TERM。 */
IEC104_COMMAND_FINISH_CANCELLED, /* 本地Cancel结束事务。 */
IEC104_COMMAND_FINISH_STOPPED, /* 原连接STOPDT停用。 */
IEC104_COMMAND_FINISH_DISCONNECTED, /* 原连接断开。 */
IEC104_COMMAND_FINISH_CONNECTION_CHANGED, /* 新连接代次或会话身份已失效。 */
IEC104_COMMAND_FINISH_SEND_ERROR, /* 发送ERROR/FATAL,且不是等待分派正常STOP通知。 */
IEC104_COMMAND_FINISH_SERVICE_ERROR /* Slave服务错误或内部身份/应答资源无法继续使用。 */
} IEC104_CommandFinishReason_t;
/* 事件是可保存的纯值,应答须原样传回;调用方不能用修改request改变实际执行内容。
* 任何工作事件曾被取走的事务,恰好交付一次FINISHED;后者无需应答。
* 应用跨循环持有站点/context或异步动作时,应在首个工作事件时自行Acquire,
* FINISHED清理且物理输出确已结束后Release,不能等读到FINISHED后才申请引用。 */
typedef struct {
IEC104_CommandEventType_t type; /* 决定事件的处理方式,参与身份校验。 */
uint64_t instance_id, transaction_id, event_serial; /* 实例、事务、事件三层身份,原样回传。 */
IEC104_CommandRequest_t request; /* 最后接纳的工作请求,不被撤销/不匹配请求覆盖。 */
IEC104_CommandFinishReason_t reason; /* 仅FINISHED有效;工作事件为NONE。 */
bool select_effect, execute_effect; /* 仅FINISHED有效:对应动作曾STARTED/FAILED,可能需清理。 */
} IEC104_CommandEvent_t;
typedef enum {
IEC104_COMMAND_ACTION_NOT_STARTED = 0, /* 必须保证没有任何副作用。 */
IEC104_COMMAND_ACTION_STARTED, /* 选择生效或输出启动;输出结果另行报告。 */
IEC104_COMMAND_ACTION_FAILED /* 未报告启动成功,但可能已留下副作用。 */
} IEC104_CommandActionResult_t;
/* Accept同步调用的动作,不保存函数/context指针。
* context为应用传入的上下文,可为NULL;request由模块内部快照构造,仅本次调用有效。
* 须短时返回,不得保存request指针、推进Slave或发布上报;设备的异步完成用Report_Result报告。
* 回调期间仅可Get_Status/Get_Transaction;其他修改入口拒绝重入。
* 返回值须准确说明是否有副作用,非法值按FAILED处理,模块不会重试动作。 */
typedef IEC104_CommandActionResult_t (*IEC104_CommandAction_t)(void *context,
const IEC104_CommandRequest_t *request);
typedef enum {
IEC104_COMMAND_RESULT_SUCCEEDED = 1, /* 启动后设备实际执行成功。 */
IEC104_COMMAND_RESULT_FAILED /* 启动后设备实际执行失败,不等同于拒绝启动。 */
} IEC104_CommandResult_t;
typedef enum {
IEC104_COMMAND_REJECT_NEGATIVE = 1, /* 通用策略拒绝,回COT=7、P/N=1。 */
IEC104_COMMAND_REJECT_UNKNOWN_IOA /* 点表不存在此IOA,回COT=47、P/N=1。 */
} IEC104_CommandRejectCause_t;
typedef enum {
IEC104_COMMAND_EVENT_ERROR = -1, /* 参数/来源/服务错误;不能据此重放已产生的动作。 */
IEC104_COMMAND_EVENT_IGNORED = 0, /* 不接手,I帧可继续交后续处理器。 */
IEC104_COMMAND_EVENT_HANDLED = 1 /* 已接手,包括校验拒绝/计数丢弃,不应再走44兜底。 */
} IEC104_CommandEventResult_t;
typedef enum {
IEC104_COMMAND_NEXT_ERROR = -1, /* 句柄/参数/重入或服务错误,输出未写入。 */
IEC104_COMMAND_NEXT_EMPTY = 0, /* 暂无可交付事件,可能须先读Slave通知;输出未写入。 */
IEC104_COMMAND_NEXT_READY = 1 /* 已交付纯值事件,同一事件不会再次出队。 */
} IEC104_CommandNextResult_t;
/* 以下为共同分类;OK究竟消费事件、记录结果还是固定帧序,以各接口说明为准。
* WAIT均未接手本次应用输入;只有Submit_Return_Info的OK直接代表该ASDU帧序已固定。 */
typedef enum {
IEC104_COMMAND_REPLY_STALE = -2, /* 身份/阶段已失效、事件已消费或事务结束,不得重试旧操作。 */
IEC104_COMMAND_REPLY_ERROR = -1, /* 参数/用法/重入或服务错误,检查接口具体前置条件。 */
IEC104_COMMAND_REPLY_WAIT = 0, /* 等待通知/确认/FIFO/网络,保留输入,推进事件循环后重试。 */
IEC104_COMMAND_REPLY_OK = 1 /* 本次操作已接手;不保证设备成功、网络ACK或FINISHED已就绪。 */
} IEC104_CommandReplyResult_t;
typedef enum {
IEC104_COMMAND_PHASE_SELECT_WAIT = 1, /* 等应用决定选择;决定期限从事件生成开始。 */
IEC104_COMMAND_PHASE_SELECTED, /* 已选择,等待匹配执行;选择确认可能仍未交接。 */
IEC104_COMMAND_PHASE_EXECUTE_WAIT, /* 等应用决定执行;已停止选择期限。 */
IEC104_COMMAND_PHASE_EXECUTING, /* 已启动,等待结果/返回信息/Complete/最后应答交接。 */
IEC104_COMMAND_PHASE_ENDED /* 协议已结束,可能仍保留FINISHED或未交接应答。 */
} IEC104_CommandPhase_t;
typedef struct {
bool active; /* 尚未ENDED;不是当前连接是否可操作的保证。 */
IEC104_CommandPhase_t phase; /* 当前协议阶段;结果报告和Complete不另建阶段。 */
bool confirm_submitted; /* 当前工作请求的确认已交给发送层,不表示写完或ACK。 */
/* result_reported:结果已冻结、执行期限停止;completed:已接手Complete,返回阶段关闭;
* term_submitted:ACT_TERM已交接。不配置ACT_TERM时此值一直false,不能据此等事务结束。 */
bool result_reported, completed, term_submitted;
} IEC104_CommandTransactionInfo_t;
/* 累计计数从Create起维护;查询不改变阶段、不做超时转换。
* busy包括待消费FINISHED和未交接的事务应答,不包含独立短应答。
* need_process不因发送WAIT持续置true;每轮网络推进后仍须Process重试。
* 计数用途是诊断,不是可靠账单;各计数可重叠,unsigned long到达上限后自然回绕。 */
typedef struct {
uint64_t generation; /* 当前已分派的连接代次,断线后为0。 */
/* active:模块已处理ACTIVATED且尚未停用,不代替操作前的实时会话校验;
* events_pending:须优先读取Slave通知;need_process:有可立即推进的内部工作;
* timer_active:当前启用了非零时长期限,时钟倒退检查不受此标志限制。 */
bool active, events_pending, need_process, timer_active;
bool busy; /* 唯一事务槽仍占用,此时其他站点/其他IOA也不能新建事务。 */
bool time_base_valid; /* 持有时间基准;不表示推算时尚未越界,STOP/断线不清除。 */
uint32_t timeout_remaining_ms; /* 当前期限剩余毫秒;无期限/已到期/时钟倒退时为0。 */
/* pending_replies为模块仍持有的应答数,不含发送层已接手的帧和应用保留的返回信息;
* pending_events为尚未取走的工作事件或FINISHED数,本版最多1。 */
unsigned pending_replies, pending_events;
/* accepted:生成的工作事件数(选择后执行计两次);selected/executed:对应STARTED次数;
* completed/execution_failed:成功/失败收尾数;action_failed:动作FAILED结束数。 */
unsigned long accepted, selected, executed, completed, execution_failed, action_failed;
/* submitted:所有已交接帧,含返回信息;negative_submitted:其中P/N=1的应答;
* return_info_submitted:已交接返回信息;waits:发送接口明确返回WAIT的次数。 */
unsigned long submitted, negative_submitted, return_info_submitted, waits;
/* rejected:接收校验/匹配/忙/无选择等拒绝数;app_rejected:Reject或NOT_STARTED数;
* busy_rejected:因事务槽占用拒绝数;overloads:独立应答池满或事务/事件序号耗尽次数;
* dropped_requests:过载丢新数及决定期间同点合法激活的静默丢弃数,后者不计rejected/busy_rejected/overloads;
* cancelled:本地取消、停用、断线、换代、发送/服务错误导致的事务结束总数。 */
unsigned long rejected, app_rejected, busy_rejected, overloads, dropped_requests, cancelled;
/* deactivated:成功撤销数;mismatches:选择失配数;其余三个为各期限导致的结束数。 */
unsigned long deactivated, mismatches, select_timeouts, decision_timeouts, execute_timeouts;
/* 事务期限或时间基准检查发现单调时钟倒退的次数;两个检查的计数可能重叠。 */
unsigned long clock_errors;
unsigned long time_expired; /* 过期激活,静默时不计rejected/dropped_requests;否定时另计rejected。 */
unsigned long time_rejected; /* IV/换算/基准/倒退/推算/超前错误;同时计rejected,不含过期。 */
} IEC104_CommandStatus_t;
/* 填写本版默认配置:45/46均要求先选择,决定15s、选择30s、执行60s,成功发ACT_TERM。
* 时标窗口10s,主站时区UTC+8,过期激活静默丢弃;默认不启用带时标类型。
* 入参:config为调用方可写结构;NULL时无操作。返回:无;不创建实例或启动计时。 */
void IEC104_Command_Config_Default(IEC104_CommandConfig_t *config);
/* 从静态池创建并绑定一个遥控实例;不自动开启Slave或消费其通知。
* 入参:slave为有效Slave,寿命须覆盖实例;config非NULL,按值复制,调用后可修改原结构。
* 必须在开始消费Slave通知之前创建,以完整接收CONNECTED/ACTIVATED。
* 池容量IEC104_COMMAND_CAPACITY默认等于TCP服务容量;独立应答容量
* IEC104_COMMAND_REPLY_CAPACITY默认4,均在编译模块时通过宏配置。
* 启用51/64时要求选择的掩码应为type_mask & ~(TYPE_BIT(51) | TYPE_BIT(64)),
* 其中TYPE_BIT表示IEC104_COMMAND_TYPE_BIT;不能直接令select_required_mask等于type_mask。
* 返回:成功为实例句柄;配置/Slave非法、重复绑定、池或实例身份序号耗尽时为NULL。 */
IEC104_Command_t *IEC104_Command_Create(IEC104_Slave_t *slave, const IEC104_CommandConfig_t *config);
/* 设置可信站内时间基准;utc_ms为UTC纪元毫秒,base_ms为同一时刻的Net单调毫秒。
* 不读Clock、不保存回调;只影响之后受理的激活,STOP/断线保留基准。
* 句柄无效、动作回调内、utc_ms超出TIME_MAX_SECONDS*1000+999或base_ms晚于当前Net时间时
* 返回false且保留原基准;成功返回true。允许尚未激活或离线时设置。
* 时效只在Handle_Event受理时判断;默认配置下再等待决定,动作可在时标后接近25s启动。 */
bool IEC104_Command_Set_Time_Base(IEC104_Command_t *self, uint64_t utc_ms, uint64_t base_ms);
/* 本站时间不可信时清除基准;不取消已受理事务。无效句柄或动作回调内无操作。 */
void IEC104_Command_Clear_Time_Base(IEC104_Command_t *self);
/* 归还静态实例槽;不隐式Cancel,不关闭Slave,也不代替应用释放站点引用。
* 入参:self为待销毁实例;事务槽、未读事件及全部独立应答槽必须均空,且不在动作回调内。
* 返回:成功true,句柄随即失效;条件不满足或句柄无效为false,实例保留。 */
bool IEC104_Command_Destroy(IEC104_Command_t *self);
/* 本地结束事务,原因CANCELLED;保留欠应用的FINISHED,清空模块尚持有的全部应答。
* 已交发送层的帧无法撤回;保持会话激活,不回滚物理动作。
* 入参:self为实例;回调中/无效句柄无操作。返回:无;应用仍须读完FINISHED。
* ENDED事务保持首次终结原因,不重复产生FINISHED。 */
void IEC104_Command_Cancel(IEC104_Command_t *self);
/* 接收Slave通知并推进会话/请求状态;不调用应用动作,不保存event或其ASDU借用指针。
* 入参:self为实例;event是所属Slave按FIFO取出的通知,须在通知内容仍有效时分派。
* 全部连接/启停通知必须送达;通知消费不受队列里后续WAIT_EVENTS阻挡。
* 返回:激活且视图可用时接手配置内的控制类型,当前会话状态通知也为HANDLED;
* 未激活的I帧、视图不可用、未接手类型或旧代次通知为IGNORED;
* 参数、来源、重入或服务错误为ERROR。非法请求可否定或丢新计数,HANDLED不表示已应答。
* SELECT_WAIT/EXECUTE_WAIT期间,同会话、同station_id、同IOA的合法激活静默丢弃,计dropped_requests,
* 返回HANDLED;不比较其他命令字段,不覆盖原请求/事件、不刷新期限。格式校验、撤销及其他点忙拒绝不变。
* 对IGNORED的I帧继续分派后续业务,最后由应用兜底;不要对HANDLED再发44。 */
IEC104_CommandEventResult_t IEC104_Command_Handle_Event(IEC104_Command_t *self,
const IEC104_SlaveEvent_t *event);
/* 推进会话检查、到期转换、发送队首及事务回收;不调用应用代码。
* 入参:self为实例;每轮网络/通知处理后调用,最多推进2*(1+独立应答槽数)步。
* 队首WAIT立即让出,不能用紧循环等待发送;需处理Slave通知/网络后再次调用。
* 返回:true包括无事、背压等待或事务已终结;false为参数/重入或服务错误。
* 发送故障也可能已经转成FINISHED,此函数返回true不代表链路或设备执行成功。 */
bool IEC104_Command_Process(IEC104_Command_t *self);
/* 取出一条应用事件,同一工作事件最多交付一次;取出不等于Accept/Reject消费。
* 入参:self为实例;event非NULL,接收可按值保存的事件副本,仅READY时写入。
* 返回:READY/EMPTY/ERROR,见NextResult;EMPTY也可能表示须先读Slave通知。
* 工作事件交付前检查会话/期限,已有FINISHED可在离线或尚有Slave通知时读取。
* 读取FINISHED可能立即释放模块的站点引用,应用跨循环引用必须此前已自行Acquire。 */
IEC104_CommandNextResult_t IEC104_Command_NextEvent(IEC104_Command_t *self,
IEC104_CommandEvent_t *event);
/* 接受一个已由NextEvent交付的工作事件;先消费身份,再同步至多调用一次action。
* 入参:self为实例;event须原样传回SELECT/EXECUTE事件;context原样转交action,不保存。
* SELECT的action可为NULL,选择生效但select_effect=false;EXECUTE必须提供action。
* 返回:OK只表示工作事件已消费,不表示动作STARTED或确认已交接;动作失败也返回OK。
* WAIT为先读Slave通知,事件未消费;STALE为身份/阶段失效,不得执行输出;
* ERROR为参数/重入/服务或确认资源错误,动作未调用。
* 即使action返回STARTED,提交确认时仍可能结束事务;应用应查询事务并继续接收FINISHED。 */
IEC104_CommandReplyResult_t IEC104_Command_Accept(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandAction_t action, void *context);
/* 不调用动作,消费工作事件,安排否定ACT_CON或未知IOA应答,以REJECTED结束事务。
* 入参:self为实例;event为尚未消费的SELECT/EXECUTE;cause为两个RejectCause之一。
* 返回:OK表示事件已消费且应答已安排,可能已提交或因随后故障被取消;
* WAIT未消费、须先处理通知;STALE身份/阶段已失效;ERROR参数/原因/重入或服务错误。
* FINISHED可先于否定应答交接,读取FINISHED不代表槽已释放。 */
IEC104_CommandReplyResult_t IEC104_Command_Reject(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandRejectCause_t cause);
/* 报告设备结果,结果只记录一次并冻结;不发返回信息,也不隐式Complete。
* 本函数及下列Submit_Return_Info/Complete只接受Accept动作返回STARTED的原EXECUTE事件。
* 入参:self为实例,event为上述事件,result为SUCCEEDED或FAILED。
* 可早于ACT_CON交接;设备完成后应及时报告,OK即停止执行期限,此后发送等待无业务超时。
* 返回:OK已记录;WAIT因未读通知尚未记录;STALE事务结束/超时或事件不符;
* ERROR参数/结果非法、已报告/已Complete、重入或服务错误。 */
IEC104_CommandReplyResult_t IEC104_Command_Report_Result(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result);
/* 通过原连接Try路径提交一帧返回信息,不经过可覆盖的NORMAL队列。
* 入参:self为实例;event为启动成功的原EXECUTE;asdu为有效、完整的监视方向ASDU。
* 类型1..21/30..40,至少一对象,COT=11或3,P/N=0,TEST/CA与命令一致;完整校验对象。
* IOA可与命令IOA不同;本版命令TEST恒为0。调用可以早于或晚于Report_Result,但须早于Complete。
* 模块有更早待交应答、确认未交接、网络背压或未读通知时WAIT,不缓存ASDU、也不占排序位置。
* 返回:OK为发送层已接手且帧序固定,可释放/复用ASDU缓冲,不能重交;WAIT须保留ASDU重试;
* ERROR为ASDU/参数/重入/服务错误或Complete后仍待收尾,尚未接手;STALE为事务/事件失效。
* 正常STOP且通知待分派时先WAIT;其他发送ERROR/FATAL终结事务并返回STALE,不可重试旧ASDU。
* 必要的后续变位由应用另按COT=3上报。
* 同一变化勿再经Enqueue_Event重复发布;成功提交不表示对端ACK,断线不会迁移到新连接。 */
IEC104_CommandReplyResult_t IEC104_Command_Submit_Return_Info(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, const IEC60870_Asdu_t *asdu);
/* 声明本事务不再产生返回信息;按配置安排ACT_TERM并由Process完成后续交接。
* 入参:self为实例,event为启动成功的原EXECUTE;结果须已报告,全部返回信息须已提交OK。
* 允许零帧返回信息;可在确认未交接时调用,模块保持确认先于ACT_TERM的顺序。
* 返回:OK立即关闭返回阶段,不必重调;WAIT为未读通知,尚未接手;
* ERROR为尚未报告结果、仍待收尾时重复调用、参数/重入/服务错误;已结束/事件不符为STALE。
* 不发ACT_TERM时也须等待执行确认交接,之后才产生FINISHED;无发送等待时可能立即结束。
* 失败配置发TERM时P/N=0表示流程结束,不能据此认定设备成功。 */
IEC104_CommandReplyResult_t IEC104_Command_Complete(IEC104_Command_t *self,
const IEC104_CommandEvent_t *event);
/* 只读查询仍占槽的事务,可在动作回调内使用,不做会话推进或期限转换。
* 入参:self为实例;event可为该事务任一已交付的SELECT/EXECUTE/FINISHED;info非NULL。
* 返回:true写入快照,ENDED的active=false;无效参数/身份、槽已回收时false且不修改info。
* 不保留历史事务;刚读出的FINISHED若已触发槽回收,使用它查询也会false。 */
bool IEC104_Command_Get_Transaction(const IEC104_Command_t *self,
const IEC104_CommandEvent_t *event, IEC104_CommandTransactionInfo_t *info);
/* 只读查询会话、调度提示及累计计数,可在动作回调内使用。
* 入参:self为实例,status非NULL接收快照;计算剩余时间但不转换已到期事务。
* 返回:true写入快照;句柄/参数/Slave服务无效时false且不修改status。
* events_pending优先读Slave通知;need_process用于立即调度,timer_active配合剩余时间等待。
* need_process=false不免除每轮网络推进后的Process,也不代表应用没有待重试的ASDU/Complete。 */
bool IEC104_Command_Get_Status(const IEC104_Command_t *self, IEC104_CommandStatus_t *status);
#ifdef __cplusplus
}
#endif
#endif /* IEC104_COMMAND_H */
@@ -0,0 +1,77 @@
# IEC60870-Slave
IEC 60870 子站协议库,当前提供 IEC104 子站、Windows TCP 后端、总召、计数量召唤、Type 103时钟同步,以及Type 45–51遥控/遥调和Type 58–64带时标命令。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_ClockSynchronization.c/h # 校时校验、显式动作与确认
│ │ └── IEC104_Command.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/`。
`-Test` 包含独立的 `test/iec104_command_test.c`:TCP服务容量1/2与遥控独立应答槽容量1/4组合成四组构建,HIGH容量为251字节。真实TCP回环入口为 `test/slave_example_loopback_test.cjs`,验证选择、执行、返回信息、撤销及遥控后总召读值。
接入自己的工程时,将 `104/` 和 `common/` 加入头文件搜索路径,使用总召、计数量、校时或遥控业务时再加入 `104/business/`;按所用模块选择源文件,仅使用公共模块时只需 `common/`。Windows 网络后端需链接 `Ws2_32`。统一入口为 `IEC104_Slave.h`,总召使用 `IEC104_Interrogation.h`(编译 `104/business/` 下的引擎和GeneralInterrogation.c),计数量使用 `IEC104_CounterInterrogation.h`,校时使用独立的 `IEC104_ClockSynchronization.c/h`,遥控使用独立的 `IEC104_Command.c/h`。接入约定见[计数量设计](docs/104/IEC104_CounterInterrogation_设计架构.md)、[时钟同步设计](docs/104/IEC104_ClockSynchronization_设计架构.md)和[遥控设计](docs/104/IEC104_Command_设计架构.md)。
子站示例默认使用模拟时钟;收到校时命令只更新模拟值。需要修改Windows系统时钟时显式选择`--clock system`,默认模式及自动测试均不修改系统时间。
命令模块的默认配置仍只启用Type 45/46;应用显式选择其他类型。示例启用45/46/49/58/59/62,全部要求先选择后执行:45/58的6001..6008控制Type 1遥信1001..1008;46/59的6101..6108控制Type 3双点位置1101..1108,双点初值均为1;49/62的6301..6308写入Type 11遥测2001..2008。站总召及对应分组读取更新后的运行值,帧序与初值不变。
带时标激活默认按10秒时差窗口检查,主站时区为UTC+8;校时成功后应用向Command提供UTC/单调时间基准,过期激活默认静默丢弃。示例只更新内存点值,设备输出由应用动作接入。Type 51/64没有选择位,只能直接执行;完整接入与时效边界见[子站示例](docs/examples/IEC104子站示例.md)和[遥控设计](docs/104/IEC104_Command_设计架构.md)。公开结构扩展后,库与调用方须一起重新编译。
详细资料见[文档索引](docs/README.md)、[模块说明与接入示例](docs/104/README.md)、[子站示例说明](docs/examples/IEC104子站示例.md)和[完整测试命令](docs/test/IEC104测试文档.md#2-编译与运行)。
@@ -0,0 +1,139 @@
# IEC104 遥控、遥调与带时标命令设计
`104/business/IEC104_Command.c/h`统一提供Type 45–51及58–64这14种控制命令。业务入口为Slave,实例使用静态池,不动态分配内存、不保存应用回调;所有入口与所属Slave在同一事件循环中串行调用。一个Slave至多绑定一个Command实例,一个实例至多保留一个命令事务。默认只启用45/46;未启用或范围外的类型返回IGNORED,交给后续业务或未知类型兜底。
## 1. 职责与默认配置
模块管理请求校验、选择与执行匹配、撤销、三类期限、应答排序、事务身份及 FINISHED。应用管理点表、权限、闭锁、返校和设备输出,显式报告设备结果并构造监视方向返回信息。协议结束不代表物理输出已经停止。
| 配置 | 默认值 | 含义 |
|---|---|---|
| type_mask | Type 45、46 | 可显式启用45–51及58–64;非零,bit14/15保留且不能置位 |
| select_required_mask | Type 45、46 | 必须是type_mask子集,且不能包含无S/E位的51/64;其他类型清除此位允许直接执行 |
| decision_timeout_ms | 15000 | 工作事件生成至应用决定;0 禁用 |
| select_timeout_ms | 30000 | 选择生效至匹配执行被模块受理;不得为0 |
| execute_timeout_ms | 60000 | 执行动作返回 STARTED 至 Report_Result;0 禁用 |
| send_act_term | true | 成功执行后发送 ACT_TERM |
| act_term_on_failure | false | 显式启用时也发送失败执行的终止;send_act_term=false 时忽略 |
| time_tag_max_delay_ms | 10000 | 带时标激活的时差上限;0只关闭时差检查,仍拒绝IV及UTC换算失败 |
| master_utc_offset_minutes | 480 | 主站时标标准时区,范围−1439..1439分钟;SU由公共时间工具处理 |
| negative_on_expired | false | 过期激活默认静默丢弃;true时回否定ACT_CON |
TEST=1 不执行动作,返回否定确认。广播 CA=65535 不接受。失败终止可选策略发送原执行请求的 COT=10、P/N=0,表示执行过程终止,不表示设备执行成功;结果由应用监视信息和 FINISHED(EXECUTION_FAILED) 表达。该策略为本模块接入约定,默认不开启。
## 2. 请求校验和选择匹配
标准104采用2/2/3参数,6字节公共头后接3字节IOA;命令元素从`IEC104_ASDU_HEADER_LEN + 3U`开始。规范ASDU长度由类型决定:
| 类型(无时标/带时标) | 命令元素 | ASDU长度 | 选择与校验 |
|---|---|---|---|
| 45 / 58 | SCO | 10 / 17 | 有S/E;保留位必须为0 |
| 46 / 59 | DCO | 10 / 17 | 有S/E;DCS只能为1、2 |
| 47 / 60 | RCO | 10 / 17 | 有S/E;RCS为1降一步、2升一步 |
| 48 / 61 | NVA + QOS | 12 / 19 | 有S/E;原始有符号16位定点值 |
| 49 / 62 | SVA + QOS | 12 / 19 | 有S/E;有符号16位标度值 |
| 50 / 63 | R32 + QOS | 14 / 21 | 有S/E;拒绝NaN及正负Inf |
| 51 / 64 | BSI | 13 / 20 | 无S/E,只能直接执行 |
配置内请求先检查当前事务到期,再检查唯一完整对象。COT非6/8回45,CA未注册或广播回46,IOA=0回47;之后检查SQ、规范长度、P/N、TEST、命令值及CP56格式,错误回否定7/9。公共解析器负责SCO/DCO/RCO及CP56校验;QU/QL的业务含义交应用解释。对象数量不符、或到达模块的原文不足规范长度时计rejected并丢弃,不能补零伪造对象。线上已知类型的截短通常更早由Receiver拒绝,不会到达Command,不能据此期待Command计数增加或连接继续。
过长请求只回显规范长度前缀,VSQ规范为1;除VSQ、COT和P/N外保留原文字节,包括非法值。回显不重新构造合法对象。已识别的拒绝和丢弃均返回HANDLED。
只有SELECT_WAIT、EXECUTE_WAIT两个决定阶段,对同会话generation、同station_id、同IOA且通过全部现有校验的COT=6激活静默丢弃,返回HANDLED。是否已被应用取走工作事件不影响此规则;不比较类型、OA、S/E、状态或QU,但新请求仍须通过type_mask、COT、寻址、格式和值校验。只增加dropped_requests,不增加rejected、busy_rejected或overloads;不覆盖原请求、不刷新决定期限、不生成新事件或应答。先检查到期,已到期事务不再适用此规则;其他点仍忙拒绝,COT=8仍按撤销处理。
决定期间同点激活的丢弃在时标判定之前;其他带时标激活在SELECTED匹配和忙判断之前执行第8节的时效检查。这样匹配的过期执行不会绕过检查,忙时其他点的过期请求也不会产生迟到的忙否定。
SELECTED阶段的选择执行要求同会话、原站点注册编号、IOA、类型、OA,且值字节及屏蔽S/E后的命令字节相同。状态、QU/QL均参与比较,CP56不比较,允许选择和执行使用不同时标;45与58等跨类型组合仍为MISMATCH。浮点按原始字节比较,+0与−0不同。此阶段选择确认尚未交接时,通过时效检查的执行按忙拒绝并保留选择;确认交接后同点不匹配执行终结为MISMATCH,否定回显本次执行。其他点的新激活只忙拒绝。匹配前检查选择期限,到期选择不能复活。
撤销按会话、站点编号、IOA、类型、OA匹配,不比较值、状态或限定词,不判时差;带时标撤销仍须通过CP56格式检查。SELECT_WAIT、SELECTED、EXECUTE_WAIT中的匹配撤销复用事务存储,肯定DEACT_CON回显撤销请求。执行已经启动、事务已结束或没有匹配时否定撤销。发送接口已经返回OK的选择确认不能撤回,随后DEACT_CON按顺序交接。
## 3. 应用交互与设备结果
事件为纯值副本,身份包含模块实例、事务、工作事件序号和类型。Accept/Reject只接受已经取出且未消费的工作事件。模块每次从已保存的规范原文解码请求;调用者修改事件副本不能改变执行参数。
状态类45–47及对应时标类型使用state和QU;48–50使用`value.normalized/scaled/short_float`及QL,state为0;51/64使用`value.bitstring`,state、qualifier及is_select均为0。normalized保留16位定点原始值,实际值为normalized/32768.0。带时标请求另含原始CP56(保留IV/SU)及换算后的UTC毫秒,非时标请求相关字段清零。毫秒值包含CP56的毫秒余数,不能只把整秒乘1000。
保存的原文再次解码失败属于内部错误。Accept在安排肯定确认和消费事件之前解码,失败按SERVICE_ERROR收尾且不调用动作;NextEvent失败不把未交付工作事件标为已读,已交付事务仍保留FINISHED义务。若终结时原文仍不可解码,只交付事务身份及SERVICE_ERROR,不把值字段当作动作参数;应用仍可读取终结事件并释放资源,避免坏原文永久占槽。
| 入口 | 成功后的含义 |
|---|---|
| Accept(SELECT) | 消费选择事件;可无动作,仅建立协议选择 |
| Accept(EXECUTE) | 消费执行事件并至多调用一次非NULL动作 |
| Report_Result | 冻结成功/失败结果并停止执行期限,重复报告报错 |
| Submit_Return_Info | 一帧已被发送层接手并固定顺序;WAIT未接手 |
| Complete | 关闭返回信息阶段,模块按配置继续交接终止;重复调用报错 |
动作返回NOT_STARTED必须保证没有副作用;STARTED表示选择生效或执行启动;FAILED表示尝试失败且可能留下副作用。SELECT的NULL动作不置物理副作用标志。执行动作返回STARTED后,应用可以在确认尚未发送时调用Report_Result,不把发送等待算成设备执行耗时。动作返回FAILED的否定确认同样使用Try,不能越过已有过程信息。
Accept的OK只表示事件被消费,不表示动作成功。应用应检查自己的动作结果或Get_Transaction中的执行阶段。报告结果后可以提交零帧或多帧返回信息,全部交接后Complete。每个WAIT都须保存当前交互,先处理网络和通知再重试;不能越过失败的Report_Result直接Complete。
Get_Transaction仅查询当前尚占槽的事务:ENDED仍可查询active=false,槽释放后旧事件查询返回false且不修改输出。FINISHED、旧SELECT及旧EXECUTE不能用于启动新动作;已消费EXECUTE仅供其对应的结果、返回信息和完成接口使用。
## 4. 状态与期限
状态依次为SELECT_WAIT、SELECTED、EXECUTE_WAIT、EXECUTING、ENDED;直接执行跳过选择阶段。EXECUTING内部另记录结果是否已报告、返回阶段是否关闭和确认/终止交接状态,避免重复动作、覆盖结果及重复终止。
期限采用Net单调毫秒;已用时间大于或等于期限即到期,时钟倒退按到期并计clock_errors。期限配置为0只禁用时长限制,正在决定、选择或等待设备结果时仍检查单调时钟倒退。决定期限从工作事件生成开始,包含事件尚未被应用取走的时间;选择和执行期限从动作返回后重新采样开始。这里的请求到达指Handle_Event受理时刻,包含Slave通知排队时间。
Accept/Reject消费交互后停止决定期限;Report_Result后停止执行期限。选择期限有意覆盖选择确认的发送等待,作为返校保持上限。执行超时不发ACT_TERM,已经执行动作所对应的肯定确认仍可继续提交;随后STOP或Cancel可以清除尚未提交的应答。结果报告后没有额外业务收尾期限,应用漏调Complete会持续占槽,直到明确取消或会话结束。
## 5. 通知、FIFO与返回信息
Handle_Event先校验来源并按Slave的FIFO消费连接/启停通知,不因后续未读通知而拒绝当前通知。有效I帧在匹配前检查期限;动作及发送入口先Check_Request,再处理到期。WAIT_EVENTS时不消费应用交互。NextEvent在离线时仍能交付已有FINISHED。
事务必要应答保存在事务槽,独立校验/忙/撤销否定保存在固定回复池,各自取得全局顺序号。Process只提交最早应答;队首WAIT时停止本轮。返回信息直接Try前也检查模块内是否存在更早应答,避免绕过本地FIFO。WAIT的应用ASDU未被接纳、不占持久排序位置。ACT_TERM在Complete被接受后安排,保证确认和已经接手的返回信息在先。
返回信息必须能完整解析,为监视方向Type 1–21或30–40,COT=11或3、P/N=0、TEST与命令一致、CA等于命令CA;监视IOA可以不同。它绑定原连接并走Try,不进入允许覆盖及跨连接保留的NORMAL队列。断线时尚未提交部分由应用放弃;如仍需报告设备变化,可独立用COT=3发布过程事件,不继续旧命令事务。
提交返回ERROR但原连接健康、仅因STOP通知待消费时让出;其他ERROR/FATAL终结为SEND_ERROR并清理待交应答,FATAL不可重试。Try必须等已有HIGH/NORMAL及当前帧让出,持续积压时不承诺有界完成。全部等待时调度不能空转。
## 6. FINISHED、引用和退出
事务级“曾交付工作事件”与当前事件已读状态分开保存。曾取走SELECT或EXECUTE的事务恰好产生一次FINISHED;从未取走任何工作事件就结束的事务不产生FINISHED。此规则也适用于直接执行尚未读就撤销,以及已读SELECT但后续EXECUTE未读时撤销/超时。
成功和执行失败在最终应答交接后交付FINISHED;其他终结原因立即交付,不等待否定或DEACT_CON。终结原因一旦确定不被后续STOP/Cancel改写,清理不重复通知。FINISHED保存最后一个接纳的工作请求和副作用标志,不被撤销或不匹配请求的回显覆盖。
事务只有FINISHED已读(或无需交付)且所有所属应答已交接/取消后才释放。释放前实例仍忙。独立应答池也属于实例生命周期:Cancel/STOP/发送失败清理它,Destroy要求事务和整个回复池均空且不在动作回调中。
事务创建时Acquire原站点,释放时Release。应用首次工作事件需要跨循环保留context或启动异步输出时,另行Acquire;在FINISHED清理且设备实际空闲后Release,不能等模块释放引用后再申请。站点删除阻止新请求,已有事务持原注册编号完成;同CA新节点不接续旧选择。跨协议结束仍在运行的输出由应用继续维护忙状态。
退出顺序:Command_Cancel,读空FINISHED并清理应用引用,Slave_Close,读空并分派Slave通知,Command_Destroy,移除站点,Slave_Destroy。动作回调只允许Get_Status/Get_Transaction,不得重入修改入口、推进网络或发布上报。
## 7. 布局、固定存储与验证
私有`COMMAND_LAYOUTS(X)`是唯一类型布局清单,记录type_id、value_length、has_se、has_time。X-macro分别展开只读布局表和字节数组union,规范长度按“9 + 值长度 + S/E字节数 + CP56长度”生成;`COMMAND_MAX_LENGTH`取该union的sizeof,目前为21字节,并静态检查不超过104的249字节ASDU上限。这是本模块14种控制命令的容量上界,不代表所有ASDU或其他业务的存储模型。
事务request_bytes、reply_bytes、独立回复槽及提交局部缓冲统一使用该容量,持久记录另存有效长度。复制、解析、包装和发送只使用规范有效长度,不能把sizeof(buffer)当作报文长度。原工作请求与撤销/失配回显分别保存,长短槽复用不会发送旧尾部。
S/E偏移只由`Se_Offset`计算,读取只能在已确认has_se的`Is_Select`或`Same_Command`中发生。51没有该字节,64相同偏移是CP56首字节,不能先读再判断。`Decode_Element`调用公共层公开的类型解码接口,不依赖内部信息对象布局表。布局、阶段支持掩码和实例type_mask分别表达“认识”“允许启用”和“本实例启用”,互不替代。
`IEC104_COMMAND_CAPACITY`默认跟随TCP服务容量;每实例一个事务槽,`IEC104_COMMAND_REPLY_CAPACITY`默认4。事务必要应答不依赖回复池剩余容量。池满只拒绝/丢新,不覆盖活动事务,不主动断开连接。Process每轮推进次数有界。实际sizeof及配置矩阵结果统一记录在[测试说明](../test/IEC104测试文档.md),不把宿主机结果当作其他目标的预算。
示例地址、动作与返回信息见[子站示例](../examples/IEC104子站示例.md)。45/58命令6001..6008对应Type1遥信1001..1008;46/59命令6101..6108对应Type3位置1101..1108,双点初值均为1;49/62命令6301..6308写入Type11遥测2001..2008。总召及对应分组逐批读取应用当前值,反映执行后的状态;点表、限值及分组属于应用,不改变Command模块职责。
验证覆盖TCP容量1/2与回复容量1/4、独立手写的14种类型长度、值与时标校验、匹配、变长FIFO、期限边界、动作只调用一次、通知优先、部分写入、结果先于确认、撤销及站点引用,并使用真实本机TCP核对遥控、设定值、带时标执行和过期丢弃。测试期望不共用实现布局清单,避免表错时输入和期望一起错。决定窗口内的同点激活丢弃不依赖内容相同,也不提供窗口外去重;事务释放后,相同ASDU按新请求处理,允许直接执行时仍可能再次输出。
扩展仍须保持事件身份、引用与交接契约;公开Request、Event、Config、Status新增字段后,库及调用方须一起重新编译,不承诺二进制布局兼容。新增类型需同步清单、解码分支和独立测试;新值类型或掩码范围外类型还涉及公开接口。容量会随清单最大长度增长,须重新测量目标内存;这里不提供跨业务统一存储管理,也不实现并发事务、Type70/102/105/107或突发上送。
## 8. 带时标激活与时间基准
应用调用`IEC104_Command_Set_Time_Base(self, utc_ms, base_ms)`提供同一时刻的UTC纪元毫秒和Net单调毫秒。句柄无效、动作回调内调用、UTC超出公共时间工具范围或base_ms晚于当前单调时间时返回false;失败不替换原基准。`Clear_Time_Base`在本站时间不可信时清除基准,动作回调内调用不生效。模块不读取Clock,不保存时间回调;STOP和断线保留基准,更新只影响随后受理的请求。
判定仅针对58–64的COT=6激活,在Handle_Event受理时做一次。公共CP56格式错误已经按请求校验回否定;格式合法后先检查IV及UTC换算,随后才应用max=`time_tag_max_delay_ms`:
| 条件 | 处理与计数 |
|---|---|
| IV=1或UTC换算失败 | 否定7,time_rejected与rejected加一;max=0仍拒绝 |
| max=0 | 不比较时差,不要求时间基准 |
| max非零且无基准、now早于base_ms、推算溢出或超出时间范围 | 否定7,time_rejected与rejected加一;单调时钟倒退另计clock_errors |
| 站内UTC − 命令UTC ≥ max | time_expired加一;默认静默丢弃,negative_on_expired=true时回否定7并计rejected |
| 命令UTC − 站内UTC ≥ max | 否定7,time_rejected与rejected加一 |
| 其他 | 继续匹配选择、忙检查或创建事务 |
站内UTC由`utc_ms + (now - base_ms)`推算,先检查回退和加法范围。CP56按master_utc_offset_minutes换算,SU由公共工具处理,并加回毫秒余数。静默过期不计rejected或dropped_requests,不占事务槽、不生成工作事件或FINISHED;time_base_valid仅表示持有基准,并非每次查询都重新验证其时效。
clock_errors按发现异常的检查次数累计,事务期限检查和时间基准检查可能在同一请求中分别增加,不能把它当作异常请求数量。
执行请求过期时不改变已有选择,它继续保留至select_timeout_ms。撤销不做时差、IV或UTC换算判定,但仍检查CP56编码格式。决定期间同点激活先按原规则丢弃,不增加时间计数。默认静默丢弃避免迟到否定被主站误认为重试结果;max、IV策略、时区、过期响应和ACT_TERM应与主站互操作约定一致。
时效只在受理时检查,Accept前不再检查命令时标;受理后的等待由决定期限控制。默认10秒时差窗口加15秒决定期限,动作启动时命令可能已接近25秒旧。需要更严格限制时,应用可在动作中检查request.time_utc_ms并返回NOT_STARTED;这不是模块承诺的动作时效上限。
@@ -0,0 +1,534 @@
# 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信息对象模块)。
> **业务同步**:2026-10-31。总召、计数量、Type103校时及Command业务已实现;Command统一处理Type45–51及58–64的遥控、遥调和带时标命令,默认仍只启用45/46。采用单事务、显式动作及返回信息提交,设备策略和物理输出由应用提供,见[遥控设计](IEC104_Command_设计架构.md)。
> **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后立即回报增量;部分发送下轮续传,业务不再直接调用底层完成接口 |
| 总召、计数量召唤与Type 103校时 | 独立业务模块(已实现) | 通过Slave事件及共用站点表分派,总召/计数量使用业务事件;校时由Execute_Next显式执行,确认回显原时标,见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md) |
| 单点、双点遥控(Type45/46) | IEC104_Command(已实现) | 独立静态池、单事务互斥;管理选择执行、撤销、三类期限、原文确认及按事务提交返回信息,见[遥控设计](IEC104_Command_设计架构.md) |
| 遥调及带时标命令(47..51、58..64) | IEC104_Command(已实现) | 与45/46共用事务和FIFO;按类型解析值与限定词,51/64直接执行;应用提供时间基准,受理带时标激活时检查时效 |
| 实际遥信/遥测数据源、输出及设备设时 | 应用接入层 | 按业务模块请求读取点表或执行设备动作;示例提供模拟点表、遥控输出和模拟时钟,协议库不自动操作硬件 |
### 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(对象及四类业务已实现) | 公共对象编解码、总召/计数量、校时及Type45/46遥控;继续接入真实数据源和设备输出 | 验证地址路由、召唤进度、校时原文确认、遥控选择执行/撤销/期限及返回信息顺序;模拟模式不设系统时间,硬件设时、实际输出和主站联调须另行验收 |
| 5(已实现) | 遥调47..51及带时标命令58..64 | 私有布局统一推导长度与容量,公共解析器校验值和CP56,按值及QU/QL匹配;时间基准与过期策略由配置和应用接入,实际主站互操作仍须联调 |
---
@@ -0,0 +1,444 @@
# IEC104 链路模块
当前子站业务统一包含`IEC104_Slave.h`:Slave内部持有不透明TcpServer,提供生命周期、事件循环、主动上报和状态查询,并在NextEvent交付I帧时自动建立ASDU视图及原连接的共用应答接口。业务不再手动创建TcpServer、绑定generation或安装函数指针。入口和延迟任务示例见[子站统一入口](#子站统一入口)。
Slave提供[共用站点表](IEC104_API接口汇总.md#station-registry):应用提供固定地址节点,动态注册多个CA和业务context,重复CA拒绝。删除立即退出路由,已有引用和遍历结束后才回收;Is_Reusable为true后应用才能释放存储。总召设置config.use_station_registry=true后按请求CA选站;多目标广播配置每站状态区,以一个任务执行“全部确认、逐站数据、全部终止”。其他业务可复用相同的查找、引用及快照遍历接口。
总召由独立库模块[IEC104_Interrogation.c/h](../../104/business/IEC104_Interrogation.h)实现,支持QOI=20站总召、21..36分组总召及多类型无时标监视数据,负责批次校验、发送等待、主站撤销、业务超时及停用/断线取消。应用通过BEGIN/NEED_DATA/FINISHED事件和显式应答管理IOA、分组、遍历、逐批取值及终结;[子站示例](../../examples/iec104_slave_example.c)提供模拟点表、事件分派和main,构建运行见[示例说明](../examples/IEC104子站示例.md)。
内部调用关系为`Slave → TcpServer → Connection → SendManager/Receiver/Link`,网络I/O仍经Net执行,当前提供Windows后端。现有单主站、静态内存、HIGH/NORMAL队列、事件FIFO、Prepare/Process或Poll调度策略保持不变。共用ASDU及Slave发送接口继续使用IEC60870前缀;公共层不依赖104传输实现。下文的底层接口说明供维护和独立TCP接入使用,业务默认通过Slave操作服务。
`Get_Status`同时提供`high_queue`和`normal_queue`的待发条目数、保留原文条目数及总容量/已用/空闲字节数,先检查`asdu_queue_status_valid`。待发数不包含已交给发送模块的ASDU;NORMAL保留到ACK前的原文仍计入存储量,字节统计包含条目头。Init成功后离线也可查询,Close后统计无效;队列为空不代表发送或确认全部完成。公开状态结构已扩展,库与调用方须一起重新编译,字段含义见[ASDU队列积压查询](IEC104_TcpServer_设计架构.md#21-asdu队列积压查询)。
`NextEvent`区分连接建立(CONNECTED)、数据传输激活(ACTIVATED)、停用(DEACTIVATED)和连接断开(DISCONNECTED),四者共用`event.transport.data.connection`快照及对端地址。启停事件在对应ACT成功受理并提交状态变化后产生,CON可能仍待发送;同状态重复ACT不重复通知,直接断开不补造停用事件。业务可按事件类型处理启停,无需通过查询状态比较变化。
`Get_Status`的`pending_state_event_count`报告尚未消费的ACTIVATED/DEACTIVATED数量,不能追溯已消费的启停历史。当前请求的错误应答可在同步分派中调用`IEC104_Slave_Reject_Request`,支持COT=44..47,复用HIGH且不改输入事件;不新增示例应答队列。调用时机、WAIT丢新策略与FATAL不可重试约定见[同步拒绝当前请求](IEC104_API接口汇总.md#reject-request)。
发送队列NORMAL与`NextEvent`业务通知FIFO是两份独立存储:NORMAL满时可覆盖旧ASDU原文;已入队业务通知逐条保留,不覆盖、不合并启停变化。通知按有效载荷长度存放于字节环,配置宏仍表示保证条数:默认普通30条最长通知、启停4条及收尾32条记录加1个最长I帧和1个断开通知。当前x64 ABI下环占11243字节,接收暂存默认2048字节,两块合计13291字节,比原来减少43.9%;16字节APDU可在普通区缓存185条。普通通知不能占用启停及收尾预留;启停配额满时暂缓受理后续改变状态的ACT,已有CON继续发送。详细公式见[TCP服务设计](IEC104_TcpServer_设计架构.md)。
新I帧无普通空间可交付时保留原帧并暂停续收,`Prepare`及`Get_Status`的`receive_paused`为1;NextEvent腾出空间后安排Process恢复,等待中的I帧先于回执交付。已确认回执因普通空间或本轮预算不足暂留内部时,可能晚于随后发生的启停通知交付,保持不因回执积压而暂缓启停CON的行为。通知积压不直接断连,但保留I帧会阻挡后续S/U;长期不消费仍可能触发原有协议超时。应用应在每轮Process/Poll后及时取出事件。此次存储调整保持公开API及事件结构不变。
`IEC104_TcpServer_Prepare(server, &info, error)`只读导出socket关注集合、最近协议等待上限及本地续跑标志;应用统一等待其他模块和TCP事件后,用`IEC104_TcpServer_Process(server, &ready, error)`有界推进,Process不执行等待。socket只借给事件循环注册,关闭/重连后的旧通知按注册token过滤;真实I/O与Connection调度仍由TCP内部处理。活动连接未EOF时入队成功、背压解除和预算耗尽后按need_process安排续跑,不能只等待新的socket边沿;离线或EOF后新增NORMAL只缓存,不因此触发空转。EOF后也停止为此前入队但尚未编号的NORMAL创建新I帧,将这些原文留给下一连接;当前已编号帧、已受理HIGH及控制帧继续按原规则收尾。接入约定见[TCP事件循环说明](IEC104_TcpServer_设计架构.md#31-接入应用自己的事件循环)。
`IEC104_TcpServer_Poll(server, max_wait_ms, error)`组合相同的准备和推进核心,中间至多等待一次:0只查询就绪,正数允许等待并按最近协议期限缩短。原poll_interval_ms配置项已移除,业务无需分别驱动内部收发和Tick。
当前实现支持控制方向提前发送:控制站可在ACT成功入队后预留命令I帧,并在ACT之后发送,不必等待CON;被控站成功受理STARTDT_ACT即进入STARTED/IDLE,开放I/S处理与发送准备,已生成的CON按FIFO先于随后准备的I帧和S确认发送。初始STOPPED/IDLE仍禁止I/S接收及新I帧发送。标准适用条件与连接组管理边界集中说明于 [链路设计第3.2节](IEC104_Link_设计架构.md#32-接收-i-帧)。
| 阅读入口 | 负责内容 |
|---|---|
| `IEC60870_Types.h` | 101/104共用的应用层参数、ASDU存储上限;不包含传输状态 |
| `IEC104_Types.h` | 104尺寸常量、标准104应用层初始化值、ASDU优先级及链路配置 |
| `IEC60870_Codec.c/h` | 101/104共用的参数校验、ASDU公共头解析及已知类型最低载荷长度检查 |
| `IEC104_Codec.c/h` | 104专用APCI检查、I/S/U帧编码和帧类型 |
| `IEC60870_Asdu.c/h` | 通用ASDU容器、布局校验、对象分派与原文应答;使用说明见下文“ASDU信息对象模块” |
| `IEC60870_Slave.c/h` | 101/104共用的主站连接接口,分派ASDU发送、激活确认和激活终止 |
| `IEC104_Slave.c/h` | 子站统一入口:TCP服务、ASDU/应答接口、原连接隔离及各业务共用的CA站点表 |
| `business/IEC104_Interrogation.c/h` | 共用召唤引擎:任务、广播、期限、事件、取消与发送等待;接入见[总召流程](IEC104_完整调用流程.md#flow-interrogation) |
| `business/IEC104_GeneralInterrogation.c/h` | Type100规则及兼容的Interrogation_Create入口 |
| `business/IEC104_CounterInterrogation.c/h` | Type101规则、创建及一次动作执行入口;见[计数量设计](IEC104_CounterInterrogation_设计架构.md) |
| `business/IEC104_ClockSynchronization.c/h` | Type103校时校验、显式动作及原文确认;见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md) |
| `business/IEC104_Command.c/h` | Type45–51/58–64遥控、遥调、选择执行、撤销、时标判定及返回信息;见[遥控设计](IEC104_Command_设计架构.md) |
| `IEC104_TcpTypes.h` | Slave与TcpServer共用的配置、调度、事件和状态值类型,无服务操作接口 |
| `IEC60870_InformationObjects.c/h` | 67种独立信息对象,覆盖监视、命令、初始化、参数和文件;编码通过只读函数表分派,支持变长文件段 |
| `IEC60870_Time.c/h` | 101/104共用时间工具:CP16/CP24/CP56Time2a编解码、本地标准时间转换、SU修正、日历/星期和纪元秒数工具;使用约定见[时间工具设计](../common/IEC60870_Time_设计架构.md) |
| `IEC60870_Clock.c/h` | 101/104共用时间源接口:直接读取和设置固定UTC+8日历;Clock.c内置Windows读写,Clock.h预留RTC接口,由编译选项二选一 |
| `IEC104_Receiver.c/h` | 每连接严格帧头检查、成帧、APCI及ASDU最低长度检查、完整APDU借用视图和显式释放;不处理链路序号 |
| `IEC104_Connection.c/h` | TCP内部连接协调:I/S/U分派、接收保留与释放、部分发送、超时推进及旧代次隔离;内部设计见[连接层设计](IEC104_Connection_设计架构.md) |
| `IEC104_TcpServer.c/h` | 底层TCP服务:IPv4单主站、静态池句柄、事件FIFO;Prepare/Process或Poll推进,Get_Remote_Address查询对端,Close_Connection按代次断开单连接 |
| `IEC104_TcpServer_Config.h` | 实现的编译期容量配置;默认一个服务器静态槽,可调整池及缓冲容量 |
| `IEC104_Net.h` / `IEC104_Net_Windows.c` | 通用网络接口及当前Windows实现:非阻塞监听/接入/收发、就绪等待、关闭及单调计时 |
| `IEC104.c/h` | 已恢复的旧单路逐字节接收入口,保留原有全局接收器及私有Link状态 |
| `IEC104_SendBuffer.c/h` | 唯一当前I帧、独立紧凑S/U队列、部分写入续传及本地发送期限 |
| `IEC104_AsduQueue.c/h` | 调用方字节缓冲的两级变长队列,HIGH满时拒绝、NORMAL覆盖最旧原文,支持显式重连恢复 |
| `IEC104_SendManager.c/h` | Connection内部的Link/SendBuffer修改入口:会话初始化、发送调度、控制收发、完成分派、ACK和回执,以及发送期限查询 |
| `IEC104_Link.c`、`IEC104_Link_Common.h` | 初始化、完整状态校验、I/S序号、k/w窗口、S确认、公共超时判定和TESTFR |
| `IEC104_Link_Master_StartStop.c/h` | 主站发STARTDT/STOPDT_ACT,收CON,处理等待超时 |
| `IEC104_Link_Slave_StartStop.c/h` | 子站收STARTDT/STOPDT_ACT,受理时更新启停状态,生成CON并处理停止排空及交错启停 |
| `IEC104_Link_Internal.h` | 三个实现文件共用的私有声明,不供业务调用 |
| `IEC104_Link.h` | 兼容总头文件,汇总公共、主站、子站接口 |
维护内部链路实现时,可按本端角色包含 `IEC104_Link_Master_StartStop.h` 或 `IEC104_Link_Slave_StartStop.h`,两者都包含公共接口。只处理I/S或TESTFR时包含 `IEC104_Link_Common.h`。旧内部代码可继续包含 `IEC104_Link.h`;模块发送完成接口统一以 `_Send` 结尾,该命名调整本身不改变参数和处理行为。改名不保留旧名兼容入口,内部调用方须同步更新名称并重新编译。
公共ASDU基础解析由 `IEC60870_Codec.c/h` 提供,业务需要解析公共头时包含 `IEC60870_Codec.h`;104的APCI检查和I/S/U编码由 `IEC104_Codec.c/h` 提供。Receiver分别调用两层:先检查APCI,再显式传入104参数调用 `IEC60870_Parse_ASDU_Header`。遇到非法帧头或截短的已知固定长度对象时返回FRAME错误,不推进序号或ACK,也不生成I_FRAME。未知/变长类型及具体字段继续由完整ASDU解析器和应用检查,边界见[基础设计](IEC104_设计架构.md#43-已知类型的最低载荷长度)。旧 `IEC104.c/h` 的全局接收入口保留原行为,不参与TcpServer路径。两个Codec均只依赖标准库及值类型,公共Codec不依赖104实现。
| 模块及构建依赖范围 | 生产源文件 |
|---|---|
| 101/104公共ASDU头解析 | `IEC60870_Codec.c` |
| 104 APCI与I/S/U编解码 | `IEC104_Codec.c` |
| ASDU信息对象编解码 | `IEC60870_Asdu.c`、`IEC60870_InformationObjects.c`、`IEC60870_Codec.c`、`IEC60870_Time.c`;C11及IEEE 754 binary32,不依赖TcpServer或Clock |
| 子站统一入口 | 在ASDU及TCP依赖之外增加`IEC60870_Slave.c`、`IEC104_Slave.c`;共用接口本身不依赖TCP或串口实现 |
| 104站/分组总召 | 在Slave及其依赖之外加入`104/business/IEC104_Interrogation.c`和`104/business/IEC104_GeneralInterrogation.c`,头文件搜索路径增加`104/business/`;应用按QOI选点表,响应NEED_DATA提交数据 |
| 104计数量召唤 | 引擎加`104/business/IEC104_CounterInterrogation.c`,头文件搜索路径增加`104/business/`;与总召分别创建,状态通知交付两边,动作通过Execute_Begin执行 |
| 104时钟同步 | Slave依赖加`104/business/IEC104_ClockSynchronization.c`,独立实例及静态池;只由Execute_Next执行同步action,模块不依赖Clock设备适配 |
| 104遥控/遥调 | Slave依赖加`104/business/IEC104_Command.c`,使用公共信息对象和Time解析;每Slave一个实例、一个事务,Accept显式执行动作,应用报告结果、提交返回信息并提供带时标命令的时间基准;模块不读取Clock |
| 101/104公共时间工具 | `IEC60870_Time.c`,对已有时间值进行日历校验、时间转换及编解码 |
| 101/104时间源:Windows系统时钟 | `IEC60870_Clock.c`、`IEC60870_Time.c`;`IEC60870_CLOCK_USE_RTC=0`(默认),Clock.c直接使用Windows的Kernel32接口 |
| 101/104时间源:RTC预留接口 | 上述两个源文件,定义`IEC60870_CLOCK_USE_RTC=1`,另需板级驱动实现`IEC60870_Clock_RTC_Read/Set`;具体芯片驱动暂不提供 |
| 独立子站发送缓冲 | `IEC104_Codec.c`、`IEC104_SendBuffer.c`;公共发送类型仍由Link公共头文件提供,无需链接Link实现 |
| 链路处理 | `IEC104_Codec.c`、`IEC104_Link.c`,再按所需启停接口加入主站/子站实现 |
| 子站发送管理 | `IEC104_SendManager.c`、`IEC104_AsduQueue.c`、`IEC104_SendBuffer.c`、`IEC104_Codec.c`、`IEC104_Link.c`、`IEC104_Link_Slave_StartStop.c` |
| 子站连接协调 | 上述子站发送管理源文件,加 `IEC104_Receiver.c`、`IEC104_Connection.c` 和 `IEC60870_Codec.c`;无需主站启停或日志实现 |
| Windows TCP子站模块 | 上述子站连接协调源文件,加 `IEC104_TcpServer.c`和`IEC104_Net_Windows.c`,应用工程链接`Ws2_32`;构建与接入见[TCP服务设计](IEC104_TcpServer_设计架构.md) |
| 独立流式接收 | `IEC104_Receiver.c`、`IEC104_Codec.c`、`IEC60870_Codec.c`;不依赖Link、SendManager或日志 |
| 旧单路逐字节接收 | `IEC104.c`、`IEC104_Codec.c`、`IEC104_Link.c`、`IEC104_Log.c` |
时间工具以`IEC60870_Time`作为公共入口,供101和104的ASDU业务层及平台接入层按需调用,不在Connection中自动执行校时。`IEC60870_InformationObjects.c`复用CP56Time2a处理带时标信息对象和命令;Type 103校验及确认使用独立ClockSynchronization模块,实际设时由应用显式执行。**新生成业务时标使用本地标准时间日历并设置SU=0;Type 103确认回显原CP56及SU。** 公共编解码继续支持SU=0和1,按输入保留标志,不强制或自动清零。生成新时标前须保证日历本身已是本地标准时间,不能只清除夏令时标志而保留原小时值。
时间源提供UTC纪元秒数时,直接调用`IEC60870_Time_Seconds_To_Local(utc_seconds, ms, &local_time)`即可取得固定UTC+8的本地年月日、时分秒、毫秒和星期,调用前后都无需手动加八小时。接口内部统一处理跨日/月/年及范围检查;原始UTC秒数上限为4102415999,超过时返回0并保留输出。RTC已直接提供本地标准时间日历时,可直接构造CP56解析值并编码。
当前时间直接通过`IEC60870_Clock_Read(&value)`取得,设置实际时钟调用`IEC60870_Clock_Set(&local_time)`,均使用固定UTC+8日历。**时间源范围仅限Windows系统时钟和RTC;`IEC60870_CLOCK_USE_RTC=0`默认使用Clock.c内置的Windows实现,`=1`调用Clock.h声明的`IEC60870_Clock_RTC_Read/Set`接口。RTC具体芯片驱动暂不实现。** 编译选项须作用于`IEC60870_Clock.c`,不规划其他操作系统适配。公共入口无需时钟对象或绑定初始化;以后接RTC时,硬件仍按驱动要求初始化,已有本地标准时间日历可直接填入,无需转换成时间戳再还原。读取结果分别表示日历和`is_invalid`,读取成功不等于时间有效;失败不修改输出。RTC的BCD格式、时间基准、毫秒来源及掉电状态由具体驱动处理。接入示例见[统一时间源读写](../common/IEC60870_Time_设计架构.md#统一时间源读写),业务待接入项见[当前接入状态](../common/IEC60870_Time_设计架构.md#当前接入状态与待补齐项)。
时间戳转日期保留两种实现:Windows默认调用CRT的64位`_gmtime64_s`;编译`IEC60870_Time.c`时定义`IEC60870_TIME_USE_PLATFORM=0`即可使用原有整数算法,其他平台默认采用此配置。配置在编译时生效,业务接口、UTC含义及1970..2099年的范围一致。此选项只决定如何转换传入的时间戳,与是否接入RTC无关;应用使用RTC作为时间源时,仍需通过相应驱动读取时间,再按返回值的格式和时间基准调用转换或编码接口。详见[实现选择](../common/IEC60870_Time_设计架构.md#时间戳转日期的实现选择)。
业务接收按`From_CP56 → CP56_To_Local`调用:原始解码保留源日历及IV/SU,转换接口返回本地标准时间年月日、时分秒、毫秒和星期。双方使用本地标准时间基准时,源标准时区参数传`IEC60870_LOCAL_UTC_OFFSET_MINUTES`(480):SU=0保持原时间,SU=1减一小时并处理跨日/月/年,IV=1则转换失败。其他源时区须传入其标准偏移,输出仍为本地标准时间;调用方无需再加八小时或重复扣除夏令时。接收SU=1的兼容处理与本地发送SU=0的约定分别生效。
日历范围为1970..2099年,CP56Time2a采用2000..2099年的世纪约定;接口以1/0表示成功/失败,失败保留输出。通用纪元秒数接口保留UTC语义,不能将本地标准时间日历直接当UTC使用。RTC读写仍由平台实现,Link和Connection的`now_ms`继续使用单调计时,不受校时影响。当前提供CP16经过时间、CP24短时标、CP56及日历工具。CP24仅含分钟、分钟内毫秒及IV,不能单独恢复小时和日期,需由业务结合已有时间基准解释。完整范围、星期和保留位策略见[时间工具设计](../common/IEC60870_Time_设计架构.md)。
Connection内部持有独立 `IEC104_Receiver_t`,先用非零连接代次调用 `IEC104_Receiver_Init`,再调用 `Feed(receiver, generation, data, length, &consumed)`。Feed最多收齐一帧,TcpServer保留未消费的输入尾部;`FRAME_READY`后由Connection通过 `Peek_Frame`取得APDU和APCI并分派。S/U受理后释放,I帧在TcpServer成功复制为事件后经Connection释放。帧未释放时Feed消费0字节,不覆盖原文;`Reset`丢弃当前半帧或完整帧,但不改变连接代次或Link状态。内部接口和迁移说明见 [编解码与接收器设计](IEC104_设计架构.md)。
接收器校验APCI格式和ASDU最低长度。Connection将I/S帧交给同连接的SendManager处理序号及ACK,并按功能分派U帧;WAIT时保留接收帧。Slave内部通过TcpServer_NextEvent读取I_FRAME副本,无需释放底层帧。连接重建由TcpServer先结束旧I/O并Close,随即Detach队列回到离线状态;新连接以新代次Rebind队列并Init连接;旧代次事件返回ERROR。
I 帧本地发送结果通过 `IEC104_Link_Complete_I_Send()` 提交,`Send` 明确限定为本地发送,成功不表示对端已经确认。同一次发送结果只提交一次。具体契约见 [链路设计第3.4.4节](IEC104_Link_设计架构.md#344-complete_i_send-的本地发送完成提交)。
发送完成接口如下,表中省略统一前缀 `IEC104_Link_`。它们报告最终发送结果,包括失败;返回 OK 表示本地发送结果已成功提交,是否仍需等待对端确认由对应状态决定。
| 接口 | 对应的本地发送结果 |
|---|---|
| `Complete_I_Send` | I 帧 |
| `Complete_S_Ack_Send` | S 确认帧 |
| `Complete_Startdt_Act_Send` | 本端主动发送的 STARTDT_ACT |
| `Complete_Startdt_Con_Send` | 本端应答的 STARTDT_CON;完成不回写启停状态 |
| `Complete_Stopdt_Act_Send` | 本端主动发送的 STOPDT_ACT |
| `Complete_Stopdt_Con_Send` | 本端应答的 STOPDT_CON;完成不回写启停状态 |
| `Complete_Testfr_Act_Send` | 本端主动发送的 TESTFR_ACT |
`Prepare_*` 负责准备报文或推进准备流程,`Handle_*` 负责处理接收事件;二者名称保持原有职责划分。收到 TESTFR_CON 使用 `Handle_Testfr_Con()`,不能用本地发送完成替代对端确认。
主动 TESTFR 使用 `IDLE → ACT_SEND_PENDING → CON_PENDING → IDLE` 三态,与控制站主动发起的 STARTDT/STOPDT 一致:ACT 完整发送后先调用 Complete,再处理对应 CON。ACT 尚未 Complete 时处理到的合法 TESTFR_CON 返回 IGNORED,不缓存;只有 CON_PENDING 接受确认。被动 TESTFR_ACT 仍独立生成 CON,应答对端请求不结束本端探测。
完整状态校验集中在 `IEC104_Link.c`,主站、子站的状态组合规则由该文件内的 `static` 函数检查。角色实现通过 `IEC104_Link_Internal.h` 调用公共辅助函数,公共链路模块不再反向调用角色实现。构建时包含公共链路及基础编解码实现,再按实际使用的启停接口加入对应角色 `.c`;同时使用两端接口时加入两个角色实现。
I/S的启停准入和停止阶段立即确认规则集中在 `IEC104_Link.c` 的私有判断函数中;I/S处理与规划通过这些判断继续执行序号、窗口及确认检查。I帧接收、I帧预留、S帧接收和普通S确认分别判断,保留各自的方向与阶段差异。子站在ACT受理时提交启停状态,S确认与CON发送完成分别维护各自的完成事实。具体规则见 [I/S与启停的内部边界](IEC104_Link_设计架构.md#16-is与启停的内部边界)。
启停和TESTFR在对应入口中直接检查状态、编码并提交;I/S帧保留序号规划。内部确认提交复用公开入口的状态校验,STOPDT直接返回各等待原因,不再经过私有计划对象和结果转换。
## ASDU信息对象模块
信息对象、ASDU容器、公共头解析及参数类型统一使用 `IEC60870_` 前缀,供101/104共用。旧信息对象/ASDU文件和符号直接迁移,不提供兼容头、宏或类型别名;调用方须更新包含路径、函数/类型名称和构建源文件并重新编译。104的APCI、链路、TCP及发送队列仍使用 `IEC104_` 前缀。
公共层仅需 `IEC60870_Asdu.c`、`IEC60870_InformationObjects.c`、`IEC60870_Codec.c`、`IEC60870_Time.c`;头文件依赖也不包含任何104模块。101接入提供约定的字段宽度和容量,104接入提供标准2/2/3及249字节上限。两种接入共用同一套对象构造和解析接口。
业务包含`IEC104_Slave.h`,取得`Slave_NextEvent`交付的I帧后可直接使用ASDU视图并解析具体对象,或先构造ASDU再交给发送接口。ASDU编解码层不分配内存、不操作连接、不读取或设置系统时钟;共用连接接口可构造并提交确认/终止应答。站/分组总召可交给IEC104_Interrogation管理校验、分批、撤销、期限和重试,应用响应业务事件提交数据,自行维护IOA、分组及遍历状态;Type45–51/58–64由IEC104_Command管理,Type103由独立校时模块管理,权限策略及设备动作仍由应用接入。
| 层次 | 职责 |
|---|---|
| `IEC60870_Codec.c/h` | 101/104共用的参数校验、ASDU公共头解析及已知类型最低载荷长度检查 |
| `IEC104_Codec.c/h` | 104专用APCI检查、I/S/U帧编码和帧类型 |
| `IEC60870_Asdu.c/h` | ASDU创建/添加/解析/取对象,维护Type ID、VSQ/SQ、IOA布局和容量,构造原文应答 |
| `IEC60870_InformationObjects.c/h` | 每种类型独立结构、Create和Get_From_Buffer;通过对象函数表编码,按Type ID选择具体解析器 |
| `IEC60870_Time.c/h` | CP16经过时间、CP24短时标及CP56日历的编解码、范围和保留位校验 |
| 能力 | 当前范围 |
|---|---|
| 监视对象编解码 | 1–21、30–40,共32种;含步位置、比特串、成组单点、CP24及保护事件;支持SQ=0/1 |
| 命令对象编解码 | 45–51、58–64、100–107,共22种;SQ=0、一个对象、精确长度 |
| 初始化结束 | 70;COI保留原因与本地参数改变标志;SQ=0、一个对象 |
| 参数对象编解码 | 110–113;QPM/QPA按独立限定词处理;允许多个对象但SQ必须为0 |
| 文件对象编解码 | 120–127;含变长文件段125与归档查询127;126文件目录支持多个对象及SQ,其余只允许一个非SQ对象 |
| 应答构造 | COT=7激活确认、9停止激活确认、10激活终止和44–47错误应答;P/N显式传入,否定激活确认使用7和1 |
上述共67种类型都有独立结构、构造和解析接口。安全扩展、厂家私有类型不在当前范围内。本层提供无状态报文编解码;站总召状态机由IEC104_Interrogation提供,参数生效、文件传输、文件读写及归档检索仍由业务接入。类型编号可在公共对象头文件的`IEC60870_AsduType_t`中查到。
应用层参数`IEC60870_AppLayerParameters_t`定义在`IEC60870_Types.h`:
| 字段 | 合法范围及含义 |
|---|---|
| `sizeOfTypeId`、`sizeOfVSQ` | 固定为1 |
| `sizeOfCOT` | 1或2;1字节时省略OA,解析OA为0 |
| `originatorAddress` | 0..255,供业务作为默认OA;构造以header中的显式OA为准 |
| `sizeOfCA` | 1或2;1字节配置拒绝大于255的公共地址 |
| `sizeOfIOA` | 1、2或3;IOA及SQ连续地址上限随宽度变化 |
| `maxSizeOfASDU` | 至少容纳公共头、最多254字节;104传输不得超过249字节 |
标准104包含 `IEC104_Types.h` 后使用`IEC104_APP_LAYER_PARAMETERS_DEFAULT`,即`{1,1,2,0,2,3,249}`。所有ASDU创建/解析入口必须显式传入非空参数。容器和解析视图借用参数,使用期间参数必须有效且不变。通用ASDU可复用到采用其他宽度的101接入,但本工程尚未提供101串口链路;Receiver、APCI、TcpServer和发送队列继续使用104布局及249字节上限。
每类对象通过自己的`Create`写入调用方提供的结构体,并绑定类型及只读编码函数表。例如单点用`IEC60870_SinglePointInformation_t`,双命令用`IEC60870_DoubleCommand_t`。普通监视和控制的带CP24/CP56对象用`base`保存基础对象、`time`保存时标,并复用基础值处理;保护事件则显式保存事件、QDP、CP16经过时间和时标。对象可按值复制,不需要销毁;业务可修改值或IOA,重新编码时仍会校验,类型和函数表不得手工修改。
容器调用顺序为`IEC60870_Asdu_Create → IEC60870_Asdu_Add_InformationObject → IEC60870_Asdu_Get_Data`。Create固定参数、SQ和公共头,首个成功添加的对象确定Type ID;后续对象必须同类型。SQ=1要求IOA连续且不越过所选宽度上限,SQ=0逐对象携带地址。Add成功后已经复制对象字节,可以立即复用对象存储。
容量取实际缓冲区与`maxSizeOfASDU`的较小值。空间不足、对象达到127个或单对象类型已加入一个对象时返回`IEC60870_ASDU_FULL`;业务提交当前非空ASDU后另起一条重试。空容器放不下首对象时须调整容量;文件数据须按可用长度分成更小的段。类型不匹配、SQ地址不连续或字段非法返回ERROR。所有失败保留容器、原文和输出参数,首对象失败不会提前设置Type ID。结果必须显式比较`IEC60870_ASDU_OK`,不可按布尔值判断。
业务包含 `IEC60870_Asdu.h`;本例使用104默认配置,还需包含 `IEC104_Types.h`。以下代码放入业务函数,`data`在原缓冲区未修改期间有效:
```c
uint8_t buffer[IEC104_MAX_ASDU_LEN];
const IEC60870_AppLayerParameters_t parameters = IEC104_APP_LAYER_PARAMETERS_DEFAULT;
IEC60870_Asdu_t asdu;
IEC60870_AsduHeader_t header = {
.cause_of_transmission = IEC60870_ASDU_COT_SPONTANEOUS,
.common_address = 1
};
IEC60870_SinglePointInformation_t point;
const uint8_t *data = NULL;
uint16_t length = 0;
IEC60870_AsduResult_t result =
IEC60870_SinglePointInformation_Create(&point, 100, 1, 0);
if (result == IEC60870_ASDU_OK) {
result = IEC60870_Asdu_Create(
&asdu, &parameters, buffer, sizeof(buffer), &header, 0);
}
if (result == IEC60870_ASDU_OK) {
result = IEC60870_Asdu_Add_InformationObject(&asdu, &point.object);
}
if (result == IEC60870_ASDU_OK) {
result = IEC60870_Asdu_Get_Data(&asdu, &data, &length);
}
/* OK时可将data/length交给Enqueue_Event;提交返回WAIT时保留原文重试。 */
```
监视和命令统一使用`IEC60870_Asdu_Parse`校验整条报文,再通过`IEC60870_Asdu_Get_Element`取得独立类型的对象。例如处理单命令:
```c
/* parameters、data、length对应收到的ASDU;处理期间原文保持有效。 */
IEC60870_AsduInfo_t info;
IEC60870_AsduResult_t status =
IEC60870_Asdu_Parse(&parameters, data, length, &info);
if (status == IEC60870_ASDU_OK && info.type_id == IEC60870_ASDU_C_SC_NA_1) {
IEC60870_SingleCommand_t command;
status = IEC60870_Asdu_Get_Element(&info, 0, &command, sizeof(command));
if (status == IEC60870_ASDU_OK) {
/* info保存COT/CA/OA;command.object.ioa、state、is_select、
* qualifier供业务判断地址、限定词、选择/执行及权限。 */
}
}
```
Get_Element输出无需预先Create,必须按`info.type_id`选择匹配的具体结构体并传入其尺寸;尺寸检查不能替代C类型匹配。解析视图借用原文和参数,取出的具体对象按值保存。只处理单个载荷时,可直接调用对应类型的`Get_From_Buffer`,传入已解析IOA及不含IOA的精确载荷;读命令载荷为0字节,允许NULL。
归一化值使用原始`int16_t`(实际值为原值/32768),标度值使用`int16_t`,短浮点保留IEEE 754 binary32位模式。累计量用`IEC60870_BinaryCounterReading_t`保存32位计数、5位序号及CY/CA/IV。对象层校验品质保留位、SCO、DCO/RCO状态、VTI有符号范围、保护事件QDP和CP24/CP56;COT、CA、IOA及限定词的业务适用性由应用判断。
`Build_Response(&parameters, request, length, cause, is_negative, out, capacity, &out_length)`仍以`IEC60870_Asdu_`为前缀。它复制请求,只替换COT/P/N,保留TEST、存在时的OA、CA、VSQ及全部对象原文,支持原地和部分重叠。它仅作公共头及最低长度检查,可回显未知类型。生成后携带原请求generation调用Send_Response,并处理WAIT/ERROR/FATAL。
`IEC60870_Asdu_Create_From_Buffer(&asdu, &parameters, buffer, length)`直接借用可写原文,建立可修改的ASDU对象。它只做公共头及最低长度检查,未知类型也能建立对象,用于COT=44等错误应答;执行业务前仍须使用完整Parse和业务规则校验。借用对象不能Add,`Set_COT`、`Set_Negative`、`Set_CA`原地修改相应字段;Get_Data可以导出原文。Create创建的构造容器也可使用这三个设置接口,空构造容器仍不能导出。Set_CA按COT宽度定位、CA宽度写入,1字节仅允许0..255,2字节允许0..65535;非法容器或地址溢出返回ERROR且不修改原文,业务地址限制仍由业务层判断。ASDU结构新增来源标志,调用方须一起重新编译。
### 文件段和新增时间格式
`IEC60870_FileSegment_t`含255字节的数据数组。Create和Get_From_Buffer把有效数据复制进对象,调用结束后即可复用输入;对象无需销毁。它的线上载荷为`NOF(2)+NOS(1)+LOS(1)+数据(LOS)`,解析精确检查LOS与报文长度,不接受截短和多余尾字节。
先用`IEC60870_FileSegment_Get_Max_Data_Size(parameters)`计算当前配置允许的数据长度。默认104配置返回236;最短101头、1字节IOA、254字节上限时返回245。返回0表示只能容纳空段,-1表示配置非法或连空段都装不下。Create允许0..255字节,实际加入ASDU时仍检查总长;超过容量返回FULL,调用方负责分段。
`IEC60870_Time_To_CP16/From_CP16`处理0..65535毫秒的经过时间。`IEC60870_Time_To_CP24/From_CP24`处理分钟0..59、分钟内毫秒0..59999和IV;bit6是保留位,CP24没有SU、小时或日期。CP24编解码保留IV,不自动推断完整日历。所有接口均沿用成功1、失败0且保留输出的约定。
### 子站统一入口
业务包含`IEC104_Slave.h`,持有一个不透明的`IEC104_Slave_t *`。Slave内部创建、驱动和销毁TcpServer;配置、调度、状态和结果仍复用`IEC104_TcpTypes.h`中的值类型,业务头文件不引入TcpServer操作接口或内部布局。
生命周期为`IEC104_Slave_Create → Config_Default/Init → Prepare/Process或Poll → Close → NextEvent读空 → Destroy`。所有操作在同一事件循环线程串行执行。静态池容量沿用`IEC104_TCP_SERVER_CAPACITY`,与直接使用TcpServer共享底层名额,无动态分配。创建失败返回NULL;其他生命周期接口显式比较`IEC104_TCP_OK`。
`IEC104_Slave_NextEvent(slave, &event)`按原FIFO交付`IEC104_SlaveEvent_t`:`event.transport`是TCP事件副本;I_FRAME且`event.asdu_result == IEC60870_ASDU_OK`时,`event.asdu`已经借用事件自身的ASDU字节,`event.connection`已经绑定原服务实例及原连接代次。业务不再传入server/generation,也不再手工Create_From_Buffer。字段/地址/命令权限仍由业务校验,未知类型可以原文交付,视图创建成功不等于允许执行命令。
普通循环使用以下顺序;Application_Handle_Event为应用自己的同步处理函数,其中可以提交应答或复制事件到延迟任务:
```c
IEC104_TcpResult_t result = IEC104_Slave_Poll(slave, 10, NULL);
if (result == IEC104_TCP_OK) {
IEC104_SlaveEvent_t event;
IEC104_TcpEventResult_t next;
while ((next = IEC104_Slave_NextEvent(slave, &event)) == IEC104_TCP_EVENT_READY) {
Application_Handle_Event(slave, &event);
}
/* next为ERROR时处理内部读取错误;EMPTY表示本轮事件已读空。 */
}
```
外部事件循环使用Slave_Prepare/Process,继续保留原service_token和socket token;pending_event_count或need_process非零时先处理本地工作。主动上报通过`IEC104_Slave_Get_Application_Layer_Parameters`取得参数构造ASDU,再调用`IEC104_Slave_Enqueue_Event(slave, &asdu)`进入NORMAL。关闭单连接传原事件给`IEC104_Slave_Close_Connection(slave, &event, error)`,库内部使用该事件的原代次。
### 共用连接上的激活确认和激活终止
共用接口`IEC60870_MasterConnection_t`继续供101/104业务复用;当前104的接口由Slave_NextEvent自动安装,101串口后端仍未提供。下例放在业务已验证请求并决定肯定确认的分支,event为地址稳定的Slave事件:
```c
if (event.transport.type == IEC104_TCP_EVENT_I_FRAME &&
event.asdu_result == IEC60870_ASDU_OK) {
bool accepted = IEC60870_MasterConnection_Send_ACT_CON(
&event.connection, &event.asdu, false);
if (!accepted) {
IEC104_TcpSubmitResult_t submit =
IEC104_Slave_Get_Last_Submit_Result(&event.connection);
/* 按下表处理submit;WAIT需要保留事件及原文,不能忽略或盲目重发。 */
(void)submit;
}
}
```
| 共用操作 | 行为 |
|---|---|
| Send_ASDU | 提交给定ASDU对象的当前原文,也可提交业务另外构造的数据ASDU |
| Send_ACT_CON | 原地设置COT=7及指定P/N,然后提交 |
| Send_ACT_TERM | 原地设置COT=10、P/N=0,然后提交 |
| Get_Application_Layer_Parameters | 返回该连接的标准104只读参数,最大249字节 |
确认及终止先修改事件副本的字节再发送,失败也不回滚;TEST/OA/CA/VSQ和对象原文保留。需要原请求内容时,在修改前保存副本。
| 详细结果 | 布尔返回 | 业务处理 |
|---|---|---|
| OK | true | 已复制到发送缓冲或HIGH,可复用发送原文;不代表已写完或已ACK,不重复提交 |
| WAIT | false | 本次未接手;保留已修改的ASDU及原事件,待容量恢复后重试 |
| ERROR | false | 参数、布局、旧代次或调用阶段非法,不能按容量等待盲目重试 |
| FATAL | false | 发送路径故障,可能已写出字节;结合RECORD/DISCONNECTED处理 |
事件含有指向自身的ASDU视图和连接上下文,不能用赋值或memcpy搬到另一地址。延迟任务可让NextEvent直接写入自身稳定存储,或调用`IEC104_Slave_Copy_Event(&task.event, &event)`;复制时同时保存字节、重建自引用,并保留原服务实例和原连接代次。源事件随后可被下一次NextEvent覆盖,任务副本仍独立可用;复制不代表需要重发已经OK的报文。多个任务的事件存储由应用提供,库不额外引入待办任务池。
总召内部使用紧凑存储:只保留`Get_Event_Identity`取得的原身份及10字节请求ASDU,按需建立视图,通过`Slave_Send_Response / Slave_Try_Send_Response`应答,关闭原连接用`Slave_Close_Connection_By_Identity`。任务槽、事件队列和发送容量保持不变,内存测量见[固定存储](IEC104_Interrogation_设计架构.md#2-对象身份与固定存储)。其他需要完整事件的业务仍可用Copy_Event。
NextEvent返回EMPTY/ERROR时保持输出不变。Close、重连、重新Init或Destroy后复用服务槽,都不会把旧任务改绑到新连接;发送继续检查原身份。Destroy成功后旧服务句柄失效,但旧事件的字节仍由任务自身保存。所有延迟任务也在所属事件循环串行提交。
多帧总召的数据和终止应经同一事件的connection依次提交,逐帧处理WAIT;把数据放NORMAL而终止放HIGH会造成优先级越序。门面不实现点表、总召状态机、遥控选择/执行或系统校时。
[子站示例](../../examples/iec104_slave_example.c)通过BEGIN/NEED_DATA/FINISHED事件与显式应答接入独立的[总召模块](../../104/business/IEC104_Interrogation.h)推进业务;Slave和TcpServer继续负责服务、连接及传输。总召接入顺序见[完整调用流程](IEC104_完整调用流程.md#flow-interrogation)。
调用`IEC104_Interrogation_Process(gi)`自动推进当前就绪任务,无需传入预算。每个实例至多一个未终结总召,包含等待生成BEGIN的请求;忙时相同和不同QOI的新激活均否定,不保存后续总召。4个任务槽共用于这个总召和待发短应答。Handle_Event在原会话有效、Slave通知已读空且无更早REPLY时尝试立即提交短应答,未接手再保存。满槽时尝试否定确认;不能安全提交/保存则仅丢新请求并累计dropped_requests,保留当前任务及TCP。Process在空闲、等待应用/事件空位、发送WAIT或Slave通知未读空时返回。对象、状态、调度及接入约定见[总召模块设计](IEC104_Interrogation_设计架构.md)。
总召Get_Status的`need_process`用于决定是否立即续跑,不能从`waiting`取反推断。应答或读取业务事件后重新查询;发送WAIT后等待网络推进再重试。每次Slave_Poll/Process及Slave通知处理结束仍须推进总召,即使need_process为false。外部事件循环同时合并Slave_Prepare的调度条件;新增状态字段后调用方须重新编译。
BEGIN由调用者判断是否接受,遍历状态可延至首个NEED_DATA建立;NEED_DATA由调用者选择本批IOA并读取当前值和品质,协议库不登记或冻结点表。Submit_Batch立即尝试不排队提交,WAIT不保存数据、不推进游标,应用在网络推进后重新取当前值重试;OK才固定帧顺序并推进批次。点值更新及突发发布须与总召读值及提交串行化,持续突发可能推迟总召,详见[数据批次与发送路径](IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。
BEGIN携带实际CA、station_id和QOI=20..36;应用检查数据源并决定接受,首个NEED_DATA才需建立取数游标。多目标广播先完成全部BEGIN/ACT_CON,再逐站取数,最后全部ACT_TERM;每站状态区保证FINISHED无损交付。Submit_Batch只接受事件目标CA。新增/替代站下轮参加,尚未选入BEGIN便删除的站跳过;已经选中尤其已确认的原站必须完成或明确终结。各站独立计时,总期限默认关闭;FINISHING的终结事件读完前仍忙拒绝新激活。详见[总召设计](IEC104_Interrogation_设计架构.md#33-广播地址与本站地址)。
主站COT=8按原连接、目标CA/OA/TEST/QOI匹配总召,包括已确认但尚未取数或等终止的站。定向撤销匹配站,其他站继续;广播撤销停止整轮,对各仍活动的站回肯定COT=9。FINISHED(DEACTIVATED)通知应用清理,匹配站旧批次失效。已被发送层接手的帧仍按顺序发送,撤销不补发成功ACT_TERM。
业务接受和每批取数期限默认各15秒,执行整体期限默认0(禁用),各项均可单独配置;这些是业务默认策略,不是标准规定的总召时限。关闭整体期限允许慢链路累计传输超过原60秒上限,底层单帧发送、确认和链路探测期限仍有效。BEGIN生成时启动已启用的业务期限,此前等待业务事件空位没有独立超时,但已占用总召资格,应及时读取FINISHED。总期限显式设为非零后仍按固定整体耗时计算,不因新批次或发送重试而延长。START/CONFIRM超时及BEGIN阶段Fail_Request复用原槽发送否定ACT_CON,保留TCP及其他短应答;肯定ACT_CON被发送层接手后,取数超时、总超时和数据源失败仍关闭原连接。终结事件保留具体原因,BEGIN未取出则只移除工作事件。接受BEGIN前检查数据源及必要业务资源,仍可逐批取值并编码。等待前查询timer_active/timeout_remaining_ms并持续驱动Process。此次失败处理修复不新增接口或配置开关;先前已移除queue_timeout_ms及多总召排队状态/统计,保留negative_submitted和dropped_requests,使用旧字段的调用方须调整并重新编译。
原`IEC104_SlaveConnection_Init`和手动接入对象已移除,调用方改用Slave服务及其事件接口并重新编译。TCP底层接口仍供独立通信接入和维护测试使用,业务的默认入口统一为Slave。
本次接口直接替换为独立对象与统一ASDU入口,删除旧通用监视/命令联合体及分类解析、Builder接口,不提供兼容包装;调用方须按上述示例迁移并重新编译。构建须新增`IEC60870_InformationObjects.c`。
时标保留IV/SU;IV=1可解码但不代表可校时。业务新上报应提供本地标准时间且SU=0,应答回显保留请求时标。验证命令和覆盖范围见[测试说明](../test/IEC104测试文档.md)。
### 时钟同步业务
[ClockSynchronization](../../104/business/IEC104_ClockSynchronization.h)处理Type 103、COT=6、IOA=0,按共用站点表路由;COT=5读时和COT=8撤销回45。独立静态池默认与TCP服务容量相同,每实例4个任务槽,与总召/计数量分别创建、分别取消。Handle_Event校验保存,Process处理期限及FIFO应答,只有应用显式Execute_Next会调用本次传入的同步action,不保存函数指针。
默认源标准时区480分钟、执行期限15000ms,0禁用期限;IV/SU及时区由公共时间工具处理,64位毫秒计算只补偿受理后的站内滞留。action收到的local_time已经是固定UTC+8,base_ms为执行检查时刻。肯定/否定确认保留原CP56字节,不回读时钟或提供自定义确认时标。
调用过action的确认走Try,错误校验应答走普通HIGH;WAIT只重试确认,STOP取消未交接任务但不回滚时钟。广播动作一次,快照目标分别确认;满槽丢新并计数,不断链。同内容的新请求仍执行,可能再次回拨。持续NORMAL发布可能推迟Try确认,应用通过包含回调执行期的confirm_pending串行协调新的带时标上报。默认示例只设模拟时钟,`--clock system`才调用Windows时间源。完整契约见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md)和[调用流程](IEC104_完整调用流程.md#flow-clock-synchronization)。
### 遥控、遥调与带时标命令业务
[Command](../../104/business/IEC104_Command.h)处理Type45–51及58–64、COT=6激活与COT=8撤销;默认只启用45/46,均须先选择。其他类型由应用通过type_mask显式启用;51/64没有S/E位,只能直接执行,不能加入select_required_mask。每个Slave只能绑定一个实例,同一实例从创建事务到读取FINISHED并交接完应答期间一直忙,所有站点的新命令互斥。未启用类型继续交给后续处理器或44兜底。
设定值由request.value交付应用;选择和执行比较原始值字节及屏蔽S/E后的QU/QL,不比较CP56。带时标激活默认按10秒窗口检查,应用通过Set_Time_Base提供UTC纪元毫秒及对应Net单调毫秒;过期默认静默丢弃,超前、无基准或无效时间回否定。max=0只关闭时差检查,仍拒绝IV及UTC换算失败。校时成功后应在动作回调外更新基准;STOP/断线保留基准,失去可信时间时Clear_Time_Base。
Handle_Event校验并生成SELECT/EXECUTE事件,Accept先消费事件再至多调用一次本次传入的动作;不保存回调。EXECUTE启动后先Report_Result记录设备结果,再逐帧Submit_Return_Info,全部OK后Complete。确认未交接时仍可报告结果以停止执行期限;返回信息WAIT时由应用保存ASDU重试。执行确认、返回信息与ACT_TERM走Try,模块内应答按全局FIFO提交。
已取走工作事件的事务恰好产生一次FINISHED;FINISHED只结束协议事务,应用自行维护尚未结束的物理输出和站点引用。退出时Cancel、读完FINISHED,再关闭Slave并分派余下通知,最后Destroy。完整规则见[遥控设计](IEC104_Command_设计架构.md)、[API第25节](IEC104_API接口汇总.md#file-25)和[遥控调用流程](IEC104_完整调用流程.md#flow-command)。
## 启停主流程
| 角色与过程 | 调用顺序 |
|---|---|
| 主站启动 | `Prepare_Startdt_Act` → 实际发送 → `Complete_Startdt_Act_Send` → 收CON → `Handle_Startdt_Con` |
| 主站停止 | `Request_Stopdt` → `Prepare_Stopdt_Request` → 依次发送S/ACT并提交完成 → 收CON → `Handle_Stopdt_Con` |
| 子站启动 | 收ACT → `Handle_Startdt_Act`(立即STARTED/IDLE)→ 发送CON → `Complete_Startdt_Con_Send` |
| 子站停止 | 收ACT → `Handle_Stopdt_Act`(立即STOPPED/STOP_DRAINING)→ `Prepare_Stopdt_Response` → 按需发送S、等待I帧确认 → 发送CON → `Complete_Stopdt_Con_Send` |
上表省略统一前缀 `IEC104_Link_`。只有具体接口返回值表明已生成报文时,才将本次输出交给发送层;生成不等于已发送。部分 Handle 只受理请求或提交对端确认,例如 `Handle_Stopdt_Act` 不生成应答,后续由 `Prepare_Stopdt_Response` 推进。S帧完成交给 `Complete_S_Ack_Send`。被控站不暂存相反方向ACT;停止等待期间成功受理START即恢复STARTED/IDLE,结束尚未生成STOPDT_CON的停止等待。
正常控制流程应串行执行启停,例如 `STOPDT_ACT → STOPDT_CON → STARTDT_ACT → STARTDT_CON`。主站的忙或重复请求结果不表示可以再次发送ACT。子站交错请求按实际受理顺序改变状态:START激活,STOP停用。已经生成的CON始终按FIFO发送,后到请求不撤回旧CON,旧CON完成也不覆盖后到ACT已经提交的状态。具体语义见 [链路设计第4.2节](IEC104_Link_设计架构.md#42-被控站受理启动)。
被控站成功受理STARTDT_ACT时立即进入STARTED/IDLE,并生成STARTDT_CON。首个CON尚未Complete时,后续每个成功受理的重复START也生成一帧CON;同状态重复请求不再次产生激活通知。所有CON按FIFO发送,Complete只确认对应发送结果,不重新提交启停状态。首个STARTDT_CON尚未完成也能受理STOP,立即停用并进入STOP_DRAINING。具体分支见 [链路设计第4.2节](IEC104_Link_设计架构.md#42-被控站受理启动)。
## 子站发送模块(已实现)
三个模块采用变长业务队列、发送前编号和独立确认记录组织,HIGH满时拒绝、NORMAL覆盖最旧原文,同时支持无malloc、部分写入续传、本地发送期限、严格启停/TESTFR、逐帧回执及可配置防饿死配额。
ASDU队列的状态以业务条目为单位:`unsubmitted_*` 表示队列尚未登记发送交接的待发条目。操作顺序是 **SendBuffer接手完整I帧 → Manager建立发送记录 → Mark_Submitted完成队列记账**。最后一步执行前,队列仍保留待发旧状态,不能据此判断SendBuffer是否已经接手。这里的“已提交”不表示整帧已写完或对端已ACK。
HIGH在完成交接记账时消费队列原文,SendBuffer中的完整I帧及Manager记录继续有效;NORMAL则保留原文等待ACK。`retained_unacked_count` 统计原文仍保留、已完成交接记账且待确认的ASDU条目,HIGH恒为0;`overwritten_unacked_count` 统计NORMAL原文已覆盖、仍待确认的条目。全部已编号未ACK的I帧数等于Manager的 `high_unacked_count` 加NORMAL保留及覆盖待确认数。`is_storage_empty` 只表示ASDU原文环为空,HIGH和NORMAL仍可能有对应I帧待确认;SendBuffer的 `is_idle` 表示当前无帧和预留。接口和行为变化见 [发送模块接口迁移](IEC104_发送模块_设计架构.md#migration),调用方须同步迁移、重新编译并初始化。
- **AsduQueue**:HIGH/NORMAL各用一个调用方提供的字节数组。两级使用不同条目头:HIGH仅存2字节长度;NORMAL存条目编号、状态及长度,头大小为 `sizeof(IEC104_AsduNormalEntryHeader_t)`,当前Windows 64位GCC下为16字节。条目恰好占本级头长加ASDU实际长度,头和原文均可跨尾部回绕,无额外回绕填充。两级都不保存user_tag,只有NORMAL分配条目编号。未提交条目通过待发游标直接定位。HIGH空间不足返回FULL,提交后立即释放原文;NORMAL保留原文到有效ACK,空间不足时覆盖最旧原文,包括已提交未ACK条目,成功返回OK且无逐条丢弃通知。条目超过目标桶总容量返回ERROR,须修正容量或组帧,不清空旧数据。
- **SendBuffer**:结构内只存一份255字节当前I帧,另用调用方提供的 `IEC104_SendBufferControlFrame_t[]` 保存S/U,每槽APDU仅6字节。当前帧连续写完;控制队列排在下一未编号业务之前。取消了APDU节点池、控制预留量和重复的协议序号边界。
- **SendManager**:Connection在准备实际写socket时调用 `Prepare_Next_Send`,无当前帧才重新选业务并分配N(S)。重复调用只返回当前帧剩余部分,最多一帧I尚未本地完成。实际写入后用 `Handle_Send_Progress`;解析后的I/S用 `Handle_I_S_Frame`。有效ACK更新所有对应发送记录;HIGH只核减Manager的未确认计数,NORMAL释放仍保留的原文或核减覆盖待确认数。确认记录由TcpServer复制为RECORD事件后经Connection移除,业务稍后处理事件副本。原文出队或覆盖不撤回当前帧、不释放k窗口,也不重置期限。
业务入口为Slave;其下的TCP及发送内部调用主线为:
```text
主动上报:TcpServer_Enqueue_Event → 服务ASDU队列(首次连接前、断线期间均可缓存)
连接应答:TcpServer_Send_Response → Connection_Submit_Response → Manager.Submit_Response(直接准备或缓存HIGH)
总召数据:TcpServer_Try_Send_Response → Connection_Try_Submit_Response → Manager.Try_Submit_Response(只能直接准备,不能排队则WAIT)
内部发送:Connection经Manager准备字节 → TcpServer经Net写socket → Connection回报Manager进度
内部接收:TcpServer经Net收字节 → Connection经Receiver成帧、由Manager统一提交链路事件
事件交付:TcpServer复制I帧/回执到事件队列并释放内部资源 → 业务通过NextEvent取副本
```
内部模块读取AsduQueue时,`AsduHeadInfo` 通过 `asdu_first/asdu_first_length` 和可选的 `asdu_second` 返回原文,`asdu_length` 是总长度;需要连续副本可用 `Copy_Asdu`。HIGH的 `entry_id.entry_serial` 恒为0,仅核对当前代次;从Peek到Mark/Discard之间不得移除、清空或替换队首,0编号不能当作可长期保存的条目身份。HIGH提交或清空后、NORMAL原文被覆盖或ACK释放后,对应借用立即失效。Manager已处理跨界原文,传输层取得的APDU仍连续。每级需同时保留N条、ASDU最长L字节时,HIGH配置 `N × (2 + L)`,NORMAL配置 `N × (IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN + L)`,无需另留回绕空隙;HIGH的N只计待发条目,NORMAL的N还包括原文仍保留的未ACK条目。Manager的构帧函数另使用249字节局部拼接数组,需计入栈预算。
仅NORMAL使用64位非零循环条目编号:`UINT64_MAX → 1`,回绕不改变连接代次,也不清空队列。先后顺序按编号环距离判断;NORMAL保留条目及覆盖待ACK区间不能跨越半个编号环,旧记录阻止窗口前进时NORMAL入队返回FULL,处理旧条目或ACK后可重试,HIGH不受该编号窗口阻塞。NORMAL条目释放后身份失效,调用方不得保存旧身份跨完整编号周期再次使用。NORMAL头内状态在Mark时改为已提交,Rebind时恢复未提交;接口说明见[发送模块设计](IEC104_发送模块_设计架构.md#aq-functions)。
`Prepare_Next_Send` 的成功值是 **OK**;`Submit_Response`以OK表示直接准备、QUEUED表示已缓存HIGH;控制帧入队成功仍为QUEUED,不能按布尔值判断。两条I帧路径共用组帧和发送记录,直接应答不经过ASDU队列,也不绕过旧HIGH、当前帧、窗口或公平配额。TcpServer的`Send_Response`最多尝试一次非阻塞写入;EAGAIN保留当前帧和期限,部分写入不能交错其他APDU。当前I帧已经准备后,即使暂时写出0字节,也要先完成,再发后来排队的控制帧。
`Try_Submit_Response`还要求NORMAL没有未提交条目;无发送机会时只返回WAIT,不降级为HIGH排队。Slave提供事件连接入口`Try_Send_ASDU`和纯值身份入口`Try_Send_Response`;总召使用后者,WAIT后重读数据。它们仍通过原SendBuffer、发送记录、k窗口和链路期限,不要求等待所有已提交帧的ACK。已有普通应答和主动上报接口的排队语义不变。
业务提交统一返回`IEC104_TcpSubmitResult_t`:`IEC104_TCP_SUBMIT_OK`表示已接手ASDU,不保证已写完或已ACK;WAIT表示未接手,业务保留重试;FATAL可能发生在已写出字节后,须处理收尾事件。`Enqueue_Event`只缓存服务事件,`Send_Response`尝试直接发送或缓存连接应答;调用后重新Prepare刷新调度条件。旧Enqueue_Normal/Enqueue_High入口与TCP_ENQUEUE结果名已移除,不保留兼容包装。
子站受理STARTDT_ACT即进入STARTED,STARTDT_CON作为发送任务按FIFO完成。STOPDT受理即停用并停止选取新业务,最多完成一帧此前已编号的当前I帧,等待已发帧确认后返回CON;未提交的NORMAL保留未编号,HIGH按下述规则清理。公共S、t2、TESTFR、t3及子站启停继续由Manager封装。接收接口的WAIT表示请求尚未受理,接入方须有界保留原帧并按接收顺序重试。
Manager初始化成功会清空HIGH待发原文并重置高优先配额;新STOP受理时清空HIGH。从停用状态受理START时,生成本轮首个STARTDT_CON并清空HIGH、重置高优先配额;已激活状态下的重复START仅追加CON,保留HIGH和配额。WAIT时请求尚未受理,不清理HIGH。以上清理均不影响NORMAL,也不撤销已提交的当前I帧及其发送记录。
初始化使用字节容量和控制槽数组。此次移除 `Enqueue` 的最后一个user_tag参数,以及条目查询、发送存储记录和记录查询中的user_tag字段;旧调用方须迁移、重新编译并初始化。HIGH发送记录的 `asdu_entry_serial` 恒为0,NORMAL继续保存条目编号,两级都用独立frame_id处理发送完成和帧回执。发送记录数组使用 `IEC104_IFrameSendRecordStorage_t`,连接代次由Manager统一保存;查询输出用 `IEC104_IFrameSendRecord_t`,返回完整 `frame_id`。当前Windows 64位GCC下,移除user_tag后每条存储记录由56字节降为48字节,查询快照由64字节降为56字节:
```c
static IEC104_IFrameSendRecordStorage_t records[12];
IEC104_AsduQueue_Init(&queue, high_bytes, sizeof high_bytes,
normal_bytes, sizeof normal_bytes, generation);
IEC104_SendBuffer_Init(&buffer, control_frames, control_count, generation);
IEC104_SendManager_Init(&manager, &queue, &buffer, &link,
records, 12, k, high_burst_limit);
```
以上省略返回值检查,完整示例、接口替换及容量单位见 [发送模块设计](IEC104_发送模块_设计架构.md#integration) 和 [接口迁移](IEC104_发送模块_设计架构.md#migration)。Link须先按CONTROLLED初始化。Manager允许绑定有未编号业务的队列,绑定成功会清空HIGH、保留NORMAL,不接管运行中的旧连接。
`Manager.Abort` 保留AsduQueue;结束旧I/O并核对记录中的ACK事实后,TcpServer调用 `AsduQueue.Detach` 解除绑定,清空HIGH并恢复NORMAL离线缓存;新连接再 `AsduQueue.Rebind` 到新代次并初始化。**HIGH固定用于当前连接的请求应答,重连清空;NORMAL固定用于允许跨连接恢复的事件,保留现存原文并恢复未提交。** 内部队列不提供逐条重连策略,使用 `Enqueue(queue, generation, priority, asdu, length)`;业务只调用TcpServer的Enqueue_Event/Send_Response。Rebind先完整检查两级条目,再将NORMAL头内已提交状态恢复为未提交并更新控制字段,不搬移ASDU原文,不使用整理数组。NORMAL头含编译器位域及对齐,必须按对应头长宏配置容量,不能沿用旧18字节头的缓冲内容或跨ABI解释。使用约定见 [队列生命周期](IEC104_发送模块_设计架构.md#aq-reconnect-policy)。
Rebind清除旧覆盖待确认计数,所有旧身份和借用失效;已覆盖原文不能恢复。它本身不发送,新连接仍须完成STARTDT;同一TCP上的STOPDT/STARTDT恢复不调用Rebind。未ACK不证明对端未收到,NORMAL只用于业务允许向同一接收方或冗余域恢复的事件;旧请求应答应放HIGH。关闭整个服务时由TcpServer调用Queue.Abort清空缓存,不提供掉电持久化。未编号条目被覆盖或重连丢弃时没有帧回执,旧帧回执须在Manager.Abort前读取;尚不提供冗余组切换或应用业务去重。
Connection通过`SendManager_Get_Deadlines`一次取得当前队首身份、本地发送及I帧t1剩余时间、控制槽容量;对应活动标志为1且剩余时间为0表示到期。原始queued_at_ms仍保存在SendBuffer,written_at_ms仍保存在Manager记录,部分ACK后选下一条的原始时刻。Connection的`Collect_Deadlines`汇合TESTFR、STOP、t2及t3,供超时检查、发送准入、控制推进和等待查询共用;不创建系统定时器,也不缓存第二份计时起点。原有底层单项超时接口保留。TcpServer在Process中执行TCP读写并驱动Connection,Poll复用该推进核心。
发送准备和写入回报仅因本地发送及STOP期限直接报告超时;ACK/TESTFR的t1故障交给接收阶段判定。旧确认t1已到时,统一发送准备入口暂停为HIGH/NORMAL创建、编号新I帧;已有帧及控制发送仍可推进,避免控制槽解阻后继续新增业务帧。有效确认解除所有已到期等待后自动恢复准入。Send_Response和常规发送仍按send后的真实时刻登记完成,发送期间跨过确认期限时安排立即Process,先有界处理已到达的确认,再按原始t1检查。Prepare对到期协议期限返回零等待;不增加恢复期限或固定宽限时间,也不改变真实容量背压下的超时策略。
## 子站连接协调层(已实现)
TcpServer内部的`IEC104_Connection_t`内含一个Receiver、一个Link、一个SendBuffer和一个SendManager,借用TcpServer提供的ASDU队列、控制槽数组及发送记录数组。初始化后保持对象地址不变,同一连接的所有调用串行执行。内部接口、时间规则及重连顺序见[连接层设计](IEC104_Connection_设计架构.md)。
Connection_Init先准备Receiver及配置,再由`SendManager_Init_Connection`校验并向最终地址提交Link、SendBuffer和绑定关系,最后提交其余连接字段;失败保留原Connection和队列。
控制帧是否可准备由Link的`Can_Prepare_*`只读接口判断,并与实际构帧共用规则。Connection对Link保留只读访问,运行期修改统一经SendManager,TESTFR_CON也经`SendManager_Handle_Testfr_Con`提交;只有返回OK才撤销本端探测t1,IGNORED不取消计时。Connection不直接查询SendBuffer,也不操作Manager的内部指针。Connection及SendManager的完整结构体供内部静态嵌入;TcpServer公开头文件隐藏这些布局,业务只使用TCP句柄、公开值类型及事件接口。
以下步骤均在TcpServer及Connection内部执行:
1. `Connection_Init`以CONTROLLED角色初始化STOPPED链路。成功受理STARTDT_ACT即进入STARTED/IDLE并开放I帧准入,发送队列保证已有CON先于随后准备的本端I帧写出。
2. 收到TCP字节后调用`Connection_Feed`,按`consumed_length`推进输入。一次最多处理一帧;未消费尾部由TcpServer保留。半帧留在Receiver中;S/U受理后自动释放;I帧返回FRAME_READY后由TcpServer经Peek复制为事件,再按`receive_serial`显式Release。
3. WAIT表示当前接收请求尚未受理,Connection保留原文并在后续驱动中重试。成功受理的START/STOP按控制帧路径自动释放原帧;重复驱动不会再次提交同一I帧的序号或ACK。
4. `Connection_Prepare_Next_Send == SEND_READY`时取剩余字节实际写入,再用`Handle_Send_Progress`回报增量;0字节和部分写入保留当前帧及原期限。整帧完成自动提交对应Link完成和Manager记录,不能另调底层Complete。本地已知发送完成必须先回报,再Feed覆盖该帧的ACK。
5. 定期调用`Connection_Tick`推进本地发送期限、I帧/TESTFR的t1、w/t2确认、t3探测和STOP总期限。停止过程中的重复STOP和WAIT不延长原期限;START成功受理取消旧STOP总计时。TESTFR等待CON的t1不因其他链路活动续期。t2保留连续未确认接收区间的最早起点,前缀被确认时不续期。
6. FATAL后连接冻结,TcpServer停止真实I/O,把剩余记录和断开通知复制为事件,再调用Connection_Close。Connection_Close保留内部ASDU队列;TcpServer随后Queue.Detach清空HIGH并保留NORMAL,离线期间继续接受NORMAL;重连时Queue.Rebind到新代次,再Connection.Init。仅Close服务清空全部ASDU。所有旧代次事件返回ERROR,不修改新连接。业务只处理TcpServer交付的事件并执行ASDU业务逻辑。
旧 `IEC104_Rx_Feed_Byte`、`IEC104_Rx_Reset`、`IEC104_Legacy_Link_Reset`及相关类型保持原样;旧入口继续使用全局单路状态、预设STARTED,并在成帧后检查I/S序号,只返回接收结果。Connection使用新Receiver并共享自身Link与SendManager,不调用旧全局接收入口。同一条连接不能同时交给这两条接收路径。
因接口过期而无法编译的旧回归测试及专用辅助文件已删除。当前有`iec104_tcp_event_ring_test.c`的13个字节环场景、`iec104_tcp_scheduler_test.c`的25个调度场景,以及`iec104_net_pending_input_test.c`的Windows回环检查;构建和运行见[测试说明](../test/IEC104测试文档.md#2-编译与运行)。
2026-10-20调度修复:预算让出时固定已到达输入范围并立即续跑,处理完后再判定ACK/TESTFR超时;后续新输入不能扩大旧批次。发送使用独立预算,持续接收不再跳过发送阶段。业务API及配置宏不变,内部Connection和Net新增续收/输入快照支持,自定义Net后端需实现`IEC104_Net_Pending_Input`。真正容量背压仍遵守原有严格期限;回执与启停通知的顺序继续按B方案。
以下为历史变更与验证记录,不代表当前保留的测试范围:
2026-09-23:NORMAL空间不足时改为覆盖最旧原文(含已提交未ACK条目),HIGH继续满时拒绝;当前APDU和发送记录独立保留,ACK兼容原文已覆盖。条目支持跨尾部存放,无填充碎片。仅更新代码和设计文档,测试文件保持原样。
2026-09-23:三发送模块改为变长ASDU字节环、单当前I帧和紧凑控制队列,发送前编号,有效ACK释放原文,业务回执独立消费,支持显式Rebind。保留ACK不得越过本地完成位置的修复。原有全部119个用例通过;本次补充校验从标准输入编译执行,未修改或新增测试源码、测试配置和测试文档。
2026-09-18:LINK测试改为报文回放用例,当时默认LINK套件68个、全部套件119个场景通过。该套件已随过期测试清理移除,历史记录保存在`.build`中。
2026-09-16:TESTFR改为三态,取消提前CON的缓存;当时旧用例因状态枚举变化而无法编译,未计为该次变更的验证结果。
2026-09-14:完善STARTDT期间的收发方向门控,当时新增LINK 36至40。算法契约见[链路设计](IEC104_Link_设计架构.md)。
@@ -0,0 +1,31 @@
# 文档索引
说明文档统一放在 `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_ClockSynchronization_设计架构.md) | Type 103、显式设时动作、滞留补偿、期限及广播确认 |
| 104 | [遥控设计](104/IEC104_Command_设计架构.md) | Type 45–51/58–64、选择执行、撤销、时标判定、返回信息及FINISHED清理 |
| 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,188 @@
# 104子站示例
[iec104_slave_example.c](../../examples/iec104_slave_example.c) 包含模拟点表、业务事件处理、其他命令兜底、事件循环及main。总召状态机使用独立库模块[IEC104_Interrogation.c/h](../../104/business/IEC104_Interrogation.h),不在示例内重复实现。
当前实现站总召(`C_IC_NA_1`,QOI=20),依次返回8个单点遥信、8个双点遥信和8个标度化遥测值;分组1(QOI=21)返回单点及双点遥信,分组2(QOI=22)只返回遥测。库支持全部16组,示例未配置的其余组由应用拒绝。通信通过`IEC104_Slave`,总召通过`IEC104_Interrogation`接入,所有任务使用固定存储、在主循环线程串行推进。示例另含计数量召唤、Type 103校时、45/46遥控、49标度化设定值及58/59/62带时标命令;默认时钟为模拟值,命令只更新内存点值,实际设备数据源和物理输出仍由应用接入。
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。
主循环向总召、计数量、校时、遥控实例广播生命周期通知,读空Slave通知后优先推进遥控,再交替推进召唤业务并显式执行校时;其他类型在四个模块均未接手后兜底。原Example_Init仍可用于仅总召测试,命令行入口分别调用计数量、校时及Example_Init_Command补齐实例,两个召唤实例各需独立广播状态区,校时使用共用站点快照。协议及应用动作限制见[计数量设计](../104/IEC104_CounterInterrogation_设计架构.md)、[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)和[遥控设计](../104/IEC104_Command_设计架构.md)。
在项目根目录的 PowerShell 中执行,需要 MinGW GCC:
示例通过 `IEC104_Interrogation.h`、`IEC104_CounterInterrogation.h`、`IEC104_ClockSynchronization.h` 和 `IEC104_Command.h` 引入业务声明;构建脚本添加 `104/`、`104/business/` 和 `common/` 头文件搜索路径,编辑器也应配置这三个目录。构建可执行程序仍使用下面的脚本,它会链接所需库源文件、Clock适配及Windows网络库;只编译当前C文件会缺少库函数实现。编辑器的C语言标准应与构建保持一致,使用C11,并选择实际的MinGW GCC编译器。
```powershell
./scripts/build.ps1
./.build/slave-example/iec104_slave_example.exe
```
默认监听 `127.0.0.1:2404`,公共地址 CA=1;按 Ctrl+C 退出并关闭服务。也可以指定监听地址、端口和公共地址:
```powershell
./.build/slave-example/iec104_slave_example.exe --bind 0.0.0.0 --port 2404 --ca 1
```
| 参数 | 含义 |
|---|---|
| `--bind IPv4` | 数字IPv4监听地址,默认127.0.0.1 |
| `--port 0..65535` | 默认2404;0由系统分配,启动时输出实际端口 |
| `--ca 1..65534` | 本站公共地址,默认1;总召请求也接受65535,广播请求的应答使用本站地址 |
| `--window 1..32767` | 发送窗口k,默认12;设为1可观察逐帧ACK后续发 |
| `--clock simulated\|system` | 默认simulated,只更新示例模拟时钟;system显式调用Windows系统设时接口 |
| `--run-ms N` | 运行约N毫秒后正常关闭;默认0表示持续运行 |
| `--help` | 打印用法后退出 |
启动输出 `LISTEN 地址:端口 CA=站地址`。退出保留原有STOP统计字段:`submitted`累计总召与兜底已接手ASDU,`gi_submitted`为已提交完毕的总召数,二者均不表示主站已经ACK。`waits`只统计总召发送WAIT;兜底HIGH满时丢新,计入`overloads`和`dropped_requests`,不记录待重试次数。兜底ERROR/FATAL计入`cancelled`,其中FATAL可能已写出字节,该计数不表示确定未发送。拒绝、主站撤销及业务超时统计继续保留,计数量另见`counter_submitted`和`counter_completed`。末尾校时字段`clock_executed`为实际action调用数、`clock_synchronized`为action返回true次数、`clock_submitted`为已交接校时ASDU数,包含错误及否定应答,广播按各提交目标分别计数;不混入原字段。
命令统计追加`command_selected`、`command_executed`、`command_completed`、`command_submitted`和`command_returns`,分别表示已接受的选择、已启动的执行、成功完成事务、已交接应答和返回信息帧数;`command_time_expired`统计过期激活,`command_time_rejected`统计无效时间、无基准、推算失败或超前造成的时间否定。静默过期不会增加选择/执行次数;保留原有统计字段的含义。
### Type 103校时
主站发送Type=103、SQ=0、对象数=1、COT=6、P/N=0、TEST=0、CA匹配或65535、IOA=0及有效CP56。默认按主站UTC+8标准时间解释,SU由时间工具处理;模块补偿受理至执行检查的等待。应用显式Execute_Next后回COT=7,原CP56字节原样回显。TEST否定7且不执行;COT=5读时否定45,不返回本站当前时间。
默认`--clock simulated`把请求的utc_ms/base_ms保存为模拟时钟基准,不改变系统时间。`--clock system`通过`IEC60870_Clock_Set(&request->local_time)`设置Windows时间;local_time已是固定UTC+8且已补偿,示例不再手工换时区或加等待。Clock内部转换为UTC后调用系统接口,权限不足时动作报告失败并回否定ACT_CON。真实TCP自动回归只运行模拟模式;system接线另用mock时钟单测,不设置宿主系统时间。
两种模式在成功校时动作中都记录utc_ms/base_ms及待更新标志;`Example_Drain_Clock`在Execute_Next返回后、回调之外把基准交给Command。system模式还在启动及每60秒、请求分派之前读取Clock,读成功且IV=0时按固定UTC+8转换(Clock读值不叠加夏令时),关联同一时刻的Net单调毫秒;读取/换算/设置基准失败或IV=1时清除Command基准。仅总召测试未创建Command,相关接线先检查空句柄。
确认发送WAIT时只重试原确认,不再次设时;STOP取消未交接确认而不撤销已完成的动作。同内容的新请求会再执行,旧时标重发可能回拨。真实数据发布方应在confirm_pending期间暂停新的带时标上报并继续驱动网络,避免持续新增NORMAL推迟确认。完整范围及期限见[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)。
### 遥控、设定值与带时标命令
`Example_Init_Command`在默认配置基础上显式启用45、46、49、58、59、62,select_required_mask等于type_mask,六种类型均须先选择;库默认配置本身仍只启用45/46。决定期限15秒、选择保持30秒、执行期限60秒,成功发送ACT_TERM,失败默认不发。整个实例只允许一个事务,所有站点的新命令互斥;TEST请求否定,CA=65535回UNKNOWN_CA,其他命令IOA由应用Reject(UNKNOWN_IOA)。
| 命令 | 受控点及返回信息 | 初值与总召 |
|---|---|---|
| Type45/58,IOA6001..6008 | Type1,IOA1001..1008,COT=11;状态0分/1合 | 初值见下表,执行后总召读取更新值 |
| Type46/59,IOA6101..6108 | Type3,IOA1101..1108,COT=11;状态1分/2合 | 初值全部为1;执行后站总召及遥信分组读取更新值 |
| Type49/62,IOA6301..6308 | Type11,IOA2001..2008,COT=11;写入标度化值 | 初值见下表,执行后站总召及QOI=22读取更新值 |
表中IOA均为十进制。单点命令6001..6008分别对应遥信1001..1008;双点命令6101..6108分别对应返回位置1101..1108。例如对Type46、IOA6103完成状态2的选择与执行后,后续QOI=20或21读取的Type3、IOA1103为2;FINISHED清理不会把它恢复为初值1。
选择S/E=1、执行S/E=0,均使用COT=6;执行的类型、IOA、OA、值及QU/QL须与选择一致,带时标命令的选择和执行可使用不同时标。主站收到选择的肯定ACT_CON后再执行;执行依次返回ACT_CON、返回信息、ACT_TERM。撤销使用COT=8,同类型/IOA/OA匹配,不要求值相同。示例未启用47、48、50、51、60、61、63、64,仍由兜底回44;这些类型由模块专项测试覆盖。
49/62允许的示例标度值为−10000..10000(包含端点),选择和执行都检查限值,越限调用Reject(NEGATIVE),动作内另作守卫。这个范围属于模拟点表策略,不是协议或Command模块的统一限值。比如对6301设为−1234,Type11返回信息及后续QOI=20/22的2001均为−1234;总召对象数与帧序不变。
58/59/62的CP56按主站UTC+8及SU解释,默认时差上限10秒,IV=1拒绝,时差恰好10秒也拒绝或丢弃。simulated模式启用时尚无可信基准,须先成功校时;system模式可由启动读时建立基准。过期激活默认静默丢弃,不占事务槽或调用动作;无基准、超前及换算失败回否定7。只在受理时检查时效,默认决定期限内继续等待可能使启动动作时的命令年龄接近25秒。撤销仍检查CP56格式,但不判时差。
`Example_Drain_Command`先读取SELECT/EXECUTE/FINISHED;首次保存事务context前Acquire应用站点引用。点表、动作和返回信息按基础类型分派;`Example_Command_Action`模拟返校或修改`ExampleApp.signal_values/double_values/scaled_values`并返回STARTED,不在动作中上报。Type11单对象返回需要12字节缓冲。Accept的OK只表示事件已消费,示例通过Get_Transaction确认EXECUTING后,依次Report_Result(SUCCEEDED)、Example_Build_Command_Return、Submit_Return_Info和Complete;设备结果可以早于确认交接。
每一步WAIT都保留原事件和当前步骤;返回信息使用独立ASDU缓冲,WAIT时保留同一帧,OK后才推进,避免重复提交。STALE停止该事务重试,继续读取FINISHED。FINISHED清除模拟返校/输出标志并Release应用引用,不恢复已修改的点值;真实输出若仍未停止,应用必须继续保持设备忙状态和引用。Example_Wait_Time同时计入遥控期限、模块推进和应用挂起重试,发送WAIT时允许等待网络而不空转。
## 主站操作和模拟点表
主站先建立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 |
| 1101..1108 | 双点信息 `M_DP_NA_1`,Type=3 | 1、1、1、1、1、1、1、1 |
| 2001..2008 | 标度化测量值 `M_ME_NB_1`,Type=11 | 2300、2310、2290、5000、-125、0、1234、42 |
所有品质为0。遥测值为原始有符号16位标度值,本示例没有定义物理单位和倍率。
一次成功站总召依次提交8个ASDU:
1. Type=100,COT=7,肯定激活确认。
2. 两帧Type=1、COT=20的单点数据,每帧4个对象。
3. 两帧Type=3、COT=20的双点数据,每帧4个对象。
4. 两帧Type=11、COT=20的标度化遥测,每帧4个对象。
5. Type=100,COT=10,激活终止。
分组1(QOI=21)共提交6个ASDU:ACT_CON、两帧Type=1单点数据、两帧Type=3双点数据、ACT_TERM,数据COT均为21,每帧4个对象。分组2(QOI=22)仍共提交4个ASDU:ACT_CON、两帧Type=11/COT=22遥测、ACT_TERM,每帧4个对象。确认和终止保留请求QOI。BEGIN校验分组,首个NEED_DATA按该组初始化应用游标,不把模拟点表写进库。
全部应答使用原请求的连接身份。总召确认、终止及短应答走普通HIGH路径,总召数据由Submit_Batch尝试不排队提交;校时调用过action的确认也使用Try。遥控执行动作产生副作用后的确认、返回信息和ACT_TERM均走Try,选择确认及校验否定走普通HIGH。未提交HIGH/NORMAL、当前帧或窗口背压时Try返回WAIT;遥控的模块FIFO还会保留更早应答优先。总召数据和遥控返回信息不使用Enqueue_Event,只有本帧接手后才推进应用步骤。
主站撤销使用相同的实际站CA/OA/TEST/QOI,COT改为8、P/N=0。激活和撤销均接受本站地址或65535,匹配时65535映射为本站地址,允许广播激活配本站撤销及本站激活配广播撤销。尚有匹配总召时模块停止任务、回复肯定COT=9,并通过FINISHED(DEACTIVATED)通知应用;不匹配或已完成则否定COT=9。已提交发送层的帧可能仍在确认前发出,撤销后不再提交旧批次和ACT_TERM。示例点数很少,总召可能在主站发撤销前已全部提交;观察中途撤销和各阶段拥塞可使用下面的可控网络测试。
广播总召的BEGIN/NEED_DATA/FINISHED携带本站CA,示例直接用该字段构造数据。确认、数据、终止、否定及错误应答的CA都为本站地址,OA/TEST/QOI保留原请求值;原始Slave事件不被修改。该支持属于Type100总召处理器,其他类型仍按各自业务规则分派。
## 请求校验及任务处理
| 情况 | 本示例的处理 |
|---|---|
| 不支持的Type ID | 同步调用Reject_Request回显原文,COT=44、P/N=1;HIGH满时丢新并计数,不因连续请求断链 |
| 总召COT不是6或8 | COT=45、P/N=1 |
| 总召CA既不是本站地址也不是65535 | COT=46、P/N=1,保留原错误CA |
| 总召IOA不是0 | COT=47、P/N=1 |
| 激活QOI超出20..36、请求P/N非法或当前已有未终结总召 | 否定ACT_CON,COT=7、P/N=1 |
| 空闲时收到合法激活请求 | 有空槽则建立唯一总召;BEGIN后应用接受才发肯定确认,忙时不同QOI也拒绝 |
| 激活QOI为23..36 | 合法请求交给应用,示例因未配置点表而Reply_Begin(false),回否定ACT_CON |
| 撤销不匹配、QOI超范围或P/N非法 | 否定DEACT_CON,COT=9、P/N=1 |
| 匹配的主站撤销 | 取消对应总召,肯定DEACT_CON,COT=9、P/N=0 |
| 总召唯一对象完整,但SQ=1或有多余尾字节 | 回否定7/9,COT/CA/IOA错误优先45/46/47;不执行激活或撤销 |
| 总召零对象/多对象等无法可靠应答的布局错误 | 计入rejected并丢弃;保留连接和已有总召 |
| 需要新增总召且4个任务槽已占满 | 尝试否定ACT_CON;无法安全提交/保存则丢新请求并计数,保留连接与原任务;匹配撤销复用原槽 |
| 接受/取数/整体业务期限到点 | 肯定ACT_CON提交前回否定确认并保留连接,提交后关闭原连接;已取出BEGIN才交付对应TIMEOUT终结事件 |
Example_Handle_Event先过滤asdu_result失败的I帧视图,避免进入业务分派或未知类型兜底;正常线上基础头/载荷截短已由Receiver和连接层处理。当前I帧依次交给总召、计数量、校时及Command;都未接手时,在读取下一事件前同步调用`IEC104_Slave_Reject_Request`回UNKNOWN_TYPE。该接口借用当前事件生成原文错误应答,复用库的普通HIGH路径;示例不保存other_request,也没有other_pending、other_waiting或other_waits。未启用类型的连续及相同内容有效新请求逐条尝试应答,允许穿插正在进行的总召。103由校时接手,45/46/49/58/59/62由Command接手,102、47及106仍回44;模块之间不承诺跨业务应答按接收顺序排列。
OK表示已接手;WAIT只因HIGH容量不足,示例丢弃本条并增加overloads/dropped_requests,保持连接、已有HIGH内容及当前业务。ERROR按失效请求处理并继续读通知;FATAL可能已写出字节,不得重试,继续排空收尾通知。接口不修改输入事件或其最近提交结果,不能通过Get_Last_Submit_Result读取本次返回值。未读启停通知会阻止本次应答;状态计数不能追溯已消费的STOP/START,禁止先保存事件再读完后续通知后调用。需要异步业务应答时另用Copy_Event、Check_Request并自行处理STOP取消,完整契约见[同步拒绝当前请求](../104/IEC104_API接口汇总.md#reject-request)。
校验从类型开始;已知总召先检查是否有唯一完整对象,再按COT、CA、IOA、布局/QOI/P/N及事务状态决定。畸形单对象的否定副本规范为10字节、VSQ=1,保留OA/TEST/IOA/QOI,不修改原输入。广播应答使用实际站CA。错误请求不取消原总召或刷新期限;否定应答沿现有队列重试,满槽只丢新请求。这里只实现上述命令集合,不代表所有类型的业务规则。
`Example_Drain_Business`主动读取总召业务事件:BEGIN按QOI选点表,Reply_Begin接受或拒绝请求;NEED_DATA调用普通辅助函数Example_Build_Batch构造数据,再Submit_Batch,数据耗尽则Complete_Data;FINISHED按实例/请求编号清理对应资源,撤销和超时也走该分支。没有向库注册业务函数指针。数据源暂未就绪时可保存事件,后续就绪再应答;发送WAIT也保留事件,下一次调用重新组包,模块不缓存本批原文或重复生成NEED_DATA。
本例BEGIN只判断接受,首个NEED_DATA初始化游标并读取这一批;接真实数据源时,在Example_Build_Batch的逐点组包处读取当前值和品质。IOA、类型、分组成员及遍历策略均由调用者决定,协议库不登记或冻结完整IOA列表。单点和遥测常量表保存地址、初值及品质,双点初值均为1;实际值分别存放在ExampleApp.signal_values、double_values和scaled_values中,站总召及对应分组逐批读取这些可写数组的当前值,包括遥控及设定值执行后的更新;这些地址不是库规定的范围。逐批采样不保证整轮同一时刻,也不自动解决与突发上报的新旧排序;完整边界见[总召模块设计](../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成功代替资源清理。
遥控同样执行`Command_Cancel → 读完FINISHED → Slave_Close → 读空并分派通知 → Command_Destroy`。示例先取消全部业务并清理应用引用,再关闭Slave;关闭后的生命周期通知仍交付全部模块。遥控FINISHED可以在离线时读取,事务槽及独立应答清空后才允许Destroy,最后移除站点并销毁Slave。
## 验证
```powershell
./scripts/build.ps1 -Test
```
`-Test` 需要预先安装 Node.js。脚本运行Slave/站点测试、总召/计数量/兜底/校时可控网络测试、规则单测、遥控容量矩阵和真实本机TCP测试;实跑数量及记录见[测试说明](../test/IEC104测试文档.md):
- `iec104_slave_test.c`:站点注册/移除、延迟回收、重复CA、13站共享服务、CA/context隔离、注册编号防替换、快照增删及嵌套集合锁、关闭重启保留,以及原有应答、事件身份和背压回归。Reject_Request另覆盖原文回显、非法参数、连续请求、窗口背压、HIGH满、启停门禁、旧会话及FATAL。
- `iec104_slave_example_test.c`:完整报文、部分写入、各发送阶段HIGH满与WAIT、请求校验、STOP/重连、非法请求、传输通知预算及API生命周期;业务事件覆盖延迟应答、批次校验、混合类型、容量边界、拒绝/失败/取消、终结预留和实例复用。总召覆盖全部16组、各阶段撤销、三项期限、迟到应答及通知优先。调度覆盖短应答立即提交及顺序保护、非队首槽复用、满槽否定或计数丢弃且当前任务有效、各阶段相同/不同QOI均忙拒绝、结束后重发成功、撤销对象匹配、停止清理、BEGIN前等事件空位及BEGIN后完整执行期限,并验证忙状态不拦截Type101总电度请求。测试设置HIGH=251字节、业务事件FIFO=2槽,使用真实容量背压限制发送。
- `iec104_command_test.c`:独立可控Net/单调时钟测试,TCP服务容量1/2和独立应答槽容量1/4交叉构建,HIGH=251字节;覆盖14种命令布局、值和CP56校验、选择执行、匹配、时效及时间基准、变长FIFO、返回信息、动作结果、期限和FINISHED/引用生命周期。
- `slave_example_loopback_test.cjs`:启动实际可执行程序,使用Windows网络后端和127.0.0.1随机端口,以独立主站字节验证分片请求、k=1时逐帧ACK、站总召、忙拒绝及结束后重发、撤销、错误应答、106与102同批分别回44、STOP/START、重连和正常退出。校时核对正常ACT_CON、TEST否定7、读时COT=5否定45、广播实际CA,以及103与102同批时先102回44、再103回7。命令核对45/46/49/58/59/62完整流程,返回信息在确认和终止之间,越限/TEST/地址拒绝、撤销、47兜底及操作后的总召新值。正常带时标命令由最近成功校时和测试单调经过时间推算时标;过期58与106同批,结合后续全部计数量帧、动作/选择计数和time_expired验证没有58应答。服务约12秒后自行关闭,使用默认模拟时钟。
原114组基线由98组总召、12组计数量及4组兜底回归组成。兜底改用未支持的106/102,覆盖同批及反序、相同内容的有效新请求、k=1与写阻塞排队、HIGH满后丢新且不补发、STOP/STOPSTART取消,以及总召未结束时立即提交错误应答。启用校时的独立回归另外核对103业务。
17组校时及时间接线回归覆盖无站广播45/46、同批及重复103逐条执行、FIFO中间否定、满槽丢新、Try等待不重执行、STOP/重连、Defer/FATAL、同一次采样检查期限及补偿、action内Get_Status与防重入、广播WAIT期间删除当前站及同CA替代不加入。其中新增1组只替换示例的Clock_Read/Set,验证system启动、60秒刷新、失败清基准、校时成功后回调外移交及NULL Command;Clock库仍正常链接,所有动作均不设置宿主系统时间。示例测试合计132组(99总召、12计数量、4兜底、17校时/接线)。
畸形请求回归覆盖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、撤销组合及连续兜底;当前ASDU数量及STOP统计见[测试记录](../test/IEC104测试文档.md#5-验证记录),计数量和命令独立计数。5个校时应答中3次执行并成功,另2次为TEST及COT拒绝;多CA由可控Net回归覆盖。
这些检查覆盖本示例的模拟点表流程;实际主站设备、现场点表和设备执行接口仍需联调。
@@ -0,0 +1,331 @@
# IEC104 测试说明
> **目录与命令同步日期**:2026-10-29。测试代码位于根目录的 `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后端,共32组统一入口测试,覆盖ASDU视图、共用发送分派、确认/终止字节、HIGH背压、非法参数、原连接身份、延迟复制、服务复用、生命周期、外部调度及离线上报。站点测试覆盖13节点、CA/context隔离、删除即时退出路由与引用延迟回收、同CA替代节点的注册编号、快照期间增删/嵌套/结束回收、显式集合锁及跨服务误用;新增9组同步拒绝回归。共用层非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)。
同步拒绝接口回归检查COT=44..47的原文回显、TEST/OA/CA/VSQ及对象字节保留、输入事件和最近提交结果不变、非法参数及原服务/会话隔离。未读启停通知拒绝提交;已消费STOP/START后的旧事件不在同步接口允许的调用范围内,不能用状态计数为0证明其仍有效。示例回归覆盖连续及重复内容的新命令、HIGH真满时丢新且保留连接/旧应答、活动总召中的兜底、启停与发送故障收尾;原同批兜底回归继续保留。接入校时后的真实TCP对103与102同批发送分别核对ACT_CON和COT=44,不保证跨业务响应顺序。接口与丢新语义见[同步拒绝当前请求](../104/IEC104_API接口汇总.md#reject-request)。
校时测试使用模拟action及可控单调时钟,不设置Windows时间。覆盖Handle/Process不执行应用、显式执行一次、原CP56回显、COT/CA/IOA校验优先级、TEST/IV/非法CP56拒绝、SU/时区转换、受理起滞留补偿、日期上界和64位运算、期限与倒退、Try背压不重复动作、FIFO/满槽、STOP/断线/旧会话、广播快照及删除目标,以及action内只读状态。真实设备写入耗时、权限及失败后的硬件状态不由这些模拟测试证明,边界见[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)。
## 2. 编译与运行
命令专项入口为`test/iec104_command_test.c`,复用TCP事件环测试的可控Net后端,但独立驱动Slave和Command,不依赖示例的应用状态。`./scripts/build.ps1 -Test`同时构建和执行TCP容量1/2 × 命令独立应答槽1/4的四种组合,每种45组;原45/46基线为每种27组。库源文件须包含`104/business/IEC104_Command.c`,测试本身包含可控TCP实现,不再额外链接Windows Net或第二份TcpServer。
命令原有覆盖45/46选择和直接执行、匹配/忙拒绝/撤销、三类动作结果、成功失败及终止配置、零/多帧返回信息、完整ASDU校验、全局FIFO、回复池满、窗口/部分写入、结果先于确认、重复及迟到事件、FINISHED配对、三个期限和时钟倒退(含期限配置0)、STOP提交竞态/FATAL、NORMAL覆盖反例、站点删除重注册、引用和单/双服务生命周期。扩展覆盖47–51/58–64,默认未启用47/58仍IGNORED;非法配置改用保留bit14/15,另拒绝51/64加入select_required_mask和时区越界。
变长输入采用测试侧独立手写长度表及报文字节,不共用Command布局清单。核对规范长度、额外尾字节、SQ、非法状态/CP56及NaN/Inf、带值动作参数、完整确认/撤销/终止回显;已知类型截短由Receiver拒绝,不能误记为Command接手并计rejected。匹配比较值与QU/QL并屏蔽S/E,CP56可不同,浮点+0/−0或跨类型仍失配;51/64数据位及CP56首字节不能被误读为S/E。回复池容量1/4下核对混合长度FIFO、WAIT重试和槽复用后的有效长度。
时标回归覆盖max=0、IV及UTC换算、无基准、等于/小于max、毫秒余数、超前、SU和时区、单调倒退与推算越界、回调内Set/Clear限制、失败设置保留原基准及STOP/断线保留基准。检查决定期间同点丢弃、忙时过期、SELECTED过期执行及寻址错误的顺序,区分time_expired、time_rejected、rejected与dropped_requests。真实回环核对45/46/49/58/59/62完整流程、限值拒绝、操作后总召当前值、47兜底,以及过期58没有应答、事件或动作。
决定期间同点激活的4组专项回归覆盖45/46、SELECT_WAIT与直接/选择后EXECUTE_WAIT、事件已取/未取、同批输入及发送背压。检查相同或不同命令字段均只增加dropped_requests,原事件、动作参数、回显和决定期限不变,不产生抢先的忙否定;另验证到期FINISHED配对、格式校验/type_mask/撤销/其他点忙拒绝,以及同CA重注册、STOP和重连的身份边界。
当前业务测试由99组总召、12组计数量、4组兜底及17组校时/时间接线组成,共132组,单/双服务配置各跑一次。总召在原98组基础上新增双点批次发送WAIT后读取最新值的回归,验证站总召和遥信分组的单点/双点帧序、游标只在提交OK后推进;真实TCP另核对双点遥控后总召读到更新值。计数量覆盖独立实例、请求校验、Type15/37批次边界、忙隔离、冻结/复位、动作重复调用和WAIT/STALE/TEST、业务期限、STOP/断线、共享连接失败、发送背压及多站广播;兜底改用未支持的106/102,覆盖同批与反序/重复内容、窗口与写阻塞、HIGH满丢新及恢复、STOP/STOPSTART取消和活动总召中的应答。校时专项检查独立受理与执行、原文确认、补偿/期限、广播及生命周期;新增1组用mock Clock_Read/Set验证system启动、60秒刷新、失败清基准、校时回调外移交和NULL Command,仍链接真实Clock库,但不设置宿主系统时间。`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: 32 groups passed (replies, identities, stations, lifecycle, backpressure, rejection)`。默认及`-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: 32 groups passed ...`、各`slave example + event interrogation tests: 132 groups passed ...`,以及四组命令专项、`interrogation rules: 3 groups passed ...`和`Slave example loopback: passed ...`。可控后端使用251字节HIGH和2槽召唤业务事件FIFO。真实回环使用默认存储、k=1、随机本机端口,验证总召与计数量交错、冻结/复位及后续读取、分组/广播、校时、命令操作、兜底、STOP/START、重连及正常退出。真实TCP校时使用模拟模式,system接线用mock单测,命令操作内存点表;实际主站、设备输出及宿主系统设时未纳入这组自动回归,不构成完整互操作认证。当前实际通过数量、STOP统计和测量产物见第5节。
## 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`并使用独立输出文件名即可。
### 校时固定存储与栈测量
2026-10-29 Windows x64 GCC默认配置实测:ClockTask为104字节,含4槽的ClockSynchronization实例为568字节,Request为80字节,Status为72字节;默认单服务的校时静态实例数组占568字节,不是完整协议库RAM总量。探针及产物位于`.build/clock-synchronization-20261029/size_probe.c/exe`。
`-O2 -fstack-usage`报告的单函数栈帧:Handle_Event 256、Execute_Next 240、Submit_Head 224、Process 64、Get_Status 80字节;示例main由修改前768增至800字节,Clock_Action为48字节。记录为同目录的`example-before.su`、`example-after.su`及`clock.su`。这些是x64宿主ABI和单函数测量,不是最坏调用链,也不代表ARMCC/MCU的RAM或栈占用;嵌入式接入须按实际工具链重新核算。
### 遥控固定存储与栈测量
下表是2026-10-30的45/46版本历史基线,Windows x64、GCC 16.1.0、`-O2`实测(单位字节);不是扩展后的当前容量:
| 对象 | 应答槽1 | 应答槽4(默认) |
|---|---|---|
| Command实例 | 448 | 616 |
| 单事务槽 | 168 | 168 |
| 单独立应答槽 | 56 | 56 |
| Request / Event / Status | 48 / 88 / 120 | 48 / 88 / 120 |
该历史版本在默认TCP服务容量1时Command静态实例数组为616字节;容量2时实例数组按实例数增长。上述不含既有Slave、TCP和发送队列。探针源文件及可执行程序位于`.build/command-v1/size_probe.c`、`size_probe_1.exe`及`size_probe_4.exe`,探针包含Command实现以测量私有结构,只作本地测量,不增加公开接口。
历史`-fstack-usage`单函数栈帧:Command Handle_Event 208、Accept 128、Submit_Return_Info 144、Submit_Head 144、Process 64字节;示例main由800增至848字节,模拟动作8字节。原始记录为`.build/command-v1/command-stack.su`、`example-before.su`及`example-after.su`。这些是Windows宿主ABI的单函数测量,不是最坏调用链或其他目标工具链的内存预算。
2026-10-31的5a内部泛化阶段仍只启用45/46,但已由14种布局生成21字节最大缓冲。Windows x64实测实例为480/672字节(回复槽1/4),事务槽192、回复槽64,Request/Event/Config/Status仍为48/88/20/120。`-O2 -fstack-usage`单函数帧为Decode_Element 144、Copy_Request 96、Submit_Head 176、Handle_Event 304、Process 64、NextEvent 144、Accept 144、Submit_Return_Info 144字节;修改前同次探针的NextEvent为160字节。最终启用遥调和时标后的数值另记,不能把这一中间阶段当作最终内存预算。
5b增加value并启用47–51后,上述sizeof及栈帧测量均与5a相同;在此x64 ABI中value恰好使用Request原有对齐空隙,不代表其他目标布局不变。该阶段全量构建通过Slave 32组×2、示例131组×2、Command 33组×4、规则3组及真实TCP设定值流程,49执行后的总召读回通过。
5c启用58–64并加入时间基准后的最终Windows x64测量:
| 对象 | 应答槽1 | 应答槽4(默认) |
|---|---|---|
| Command实例 | 512 | 704 |
| 单事务槽 / 单独立应答槽 | 192 / 64 | 192 / 64 |
| Request / Event | 80 / 120 | 80 / 120 |
| Config / Status | 28 / 128 | 28 / 128 |
相对45/46历史基线,每实例增加64/88字节;默认单服务Command静态实例数组为704字节。容量仍由布局清单最大规范长度推导,当前为21,不是接收时分配。最终GCC `-O2 -fstack-usage`单函数帧:Decode_Element 144、Copy_Request 112、Submit_Head 176、Handle_Event 288、Process 64、NextEvent 176、Accept 176、Submit_Return_Info 144字节。公开头C++11严格编译检查通过。以上仍为宿主单函数帧,不是调用链上界;STM32F4须以目标工具链重新测量,不能用这些数字代替目标RAM/栈预算。
本轮产物统一保存在`.build/command-extension-20261031/`,包含修改前`before/`、三个阶段的`5a/`、`5b/`、`5c/`及`RESULTS.txt`。私有故障探针另验证保存原文损坏时的两条收尾路径:未交付工作事件时NextEvent返回ERROR且不产生事件;已交付后Accept返回ERROR、不调用动作或发肯定确认,FINISHED(SERVICE_ERROR)恰好一次,读完后事务槽可复用。
## 5. 验证记录
2026-10-31 遥调与带时标命令:在原Command模块内分5a布局泛化、5b遥调、5c时标三阶段完成45–51/58–64扩展。5a原45/46逐字节回归四配置各27组保持预期通过,5b增加到33组;最终`./scripts/build.ps1 -Test`通过严格GCC构建及全部运行,Slave 32组×2、示例132组×2、Command 45组×4及规则3组均通过,真实Windows TCP回环通过。默认类型仍为45/46,额外类型显式启用;X-macro统一推导规范长度和21字节容量,原文、回复及FIFO使用有效长度。
本轮回环覆盖45/46/49/58/59/62选择、执行、返回信息及ACT_TERM;49/62写入的遥测和58/59更新的遥信由既有总召读回,初值与帧序保持兼容。过期58与106同批发送,核对106应答、后续计数量全部帧和统计,确认没有58应答或动作。STOP实测submitted=83、gi_submitted=11,command_selected=7、command_executed=6、command_completed=6、command_returns=6、command_time_expired=1、command_time_rejected=0。时间边界、IV/SU、时区、基准异常、撤销和匹配顺序由可控时钟专项覆盖;system读时接线使用mock,未设置宿主时钟。
三阶段sizeof及GCC栈帧见第4节;公开头C++11检查、内部原文损坏收尾探针通过。17份Markdown的356个本地链接及锚点、UTF-8/LF、16个公开函数在API文档中的覆盖和diff空白检查均通过。基线快照、阶段产物和RESULTS记录位于`.build/command-extension-20261031/`。所有修改及验证限于IEC60870-Slave目录,未执行父工程Keil构建、目标RAM/栈测量或硬件主站联调;宿主结果不代替这些验收。
2026-10-30 恢复双点遥控地址映射:Type46命令恢复为十进制6101..6108,对应Type3遥信仍为1101..1108。示例帮助、当前说明和回环请求同步更新。原EXE被运行进程占用时首次构建链接失败;用户关闭后,`./scripts/build.ps1`成功更新常规示例EXE,严格GCC构建无诊断。真实TCP回环验证6103选择/执行后返回1103,后续站总召、遥信分组及重连均读到新值,并通过既有单点遥控、撤销、错误应答及生命周期场景。沙箱内首次回环因Node启动子进程EPERM失败,获准在沙箱外重跑相同命令后退出码0。
2026-10-30 双点遥信加入总召:QOI20按Type1、Type3、Type11依次返回各8点,共8帧;QOI21返回单点及双点共6帧,QOI22仅遥测仍4帧。沿用1101..1108及double_values,每批4点、每次发送重试重取值;只新增双点遍历游标,不增加点值数组或发送缓冲。TCP容量1/2下各131组示例C测试通过,严格GCC构建无诊断;真实TCP验证Type46操作1103后,站总召、遥信分组及重连读回均为新值,STOP submitted=83、gi_submitted=11。原示例进程运行中,`./scripts/build.ps1`因无法覆盖被占用的exe而链接失败;随后以相同源文件及编译参数生成`.build/slave-example/iec104_slave_example_double_gi.exe`并通过回环,未中断当前连接。旧进程退出后,常规`./scripts/build.ps1`重跑成功,更新`iec104_slave_example.exe`,退出码0。可控测试产物为同目录`iec104_slave_example_double_test_1.exe`及`_2.exe`。
2026-10-30 恢复单点遥控地址映射:Type45恢复6001..6008控制Type1遥信1001..1008;Type46仍使用1101..1108。`./scripts/build.ps1`严格GCC构建无诊断;沿用已获准的真实TCP回环命令,验证6001选择/执行后返回1001及总召新值、1001作为Type45命令时回47、双点1103返回1103,并通过原有撤销、错误应答和生命周期场景,退出码0。示例帮助、说明和回环期望同步更新。
2026-10-30 示例遥控地址对齐遥信:Type45改用1001..1008,Type46改用1101..1108,命令与返回信息使用同一IOA。严格GCC构建无诊断,Slave单/双服务各32组、既有业务各130组、遥控四配置各27组及3组规则测试通过。真实TCP核对新地址的选择/执行/返回/终止、撤销及单点操作后的总召新值,旧地址6001回47。`./scripts/build.ps1 -Test`末尾回环受沙箱子进程EPERM限制,首次整体退出码1;获准单独重跑原回环命令后退出码0。帮助文本、示例说明和回环断言同步更新,协议库点址校验保持通用。
2026-10-30 遥控决定窗口同点激活:SELECT_WAIT/EXECUTE_WAIT内,通过校验且同会话、同站点注册编号、同IOA的激活只计dropped_requests并HANDLED,不覆盖请求、不刷新期限、不生成新事件或忙否定。新增4组回归,TCP容量1/2与回复槽1/4的四组合各27组通过;`./scripts/build.ps1 -Test`的严格GCC构建无诊断,Slave单/双服务各32组、既有业务各130组及3组规则测试通过。脚本末尾真实TCP回环因沙箱阻止Node子进程而报EPERM,首次整体退出码1;随后获准单独重跑原回环命令,退出码0,真实TCP全流程通过。Command公开头C++11严格语法检查通过,文档链接与UTF-8/LF检查通过;未改公开接口或固定存储容量,修改仅限IEC60870-Slave。
2026-10-30 Type45/46遥控:新增独立单事务模块、固定回复池、选择/执行/撤销、显式动作与设备结果、原连接返回信息及全局FIFO收尾。`./scripts/build.ps1 -Test`以GCC `-std=c11 -O2 -Wall -Wextra -Werror`构建无诊断,Slave单/双服务各32组、既有业务单/双服务各130组、遥控四配置各23组和3组召唤规则测试全部通过;真实Windows TCP回环通过45/46完整流程、撤销、TEST/地址拒绝、未支持类型、总召新值及原有计数量/校时/启停重连,脚本最终退出码0。沙箱内首次Node启动因EPERM失败,随后在获准环境重跑完整原脚本通过。校时使用模拟模式,遥控操作模拟点。修改前84个文件的快照、容量测试、sizeof探针及栈记录保存在`.build/command-v1/`;所有实现与验证均限定在IEC60870-Slave目录。
2026-10-29 Type 103时钟同步:新增独立静态池模块、显式Execute_Next动作、64位受理后滞留补偿、执行期限、原文ACT_CON及广播快照确认;示例默认模拟,显式system模式才设置Windows时间。整合`build.ps1 -Test`中GCC `-std=c11 -O2 -Wall -Wextra -Werror`构建无诊断,Slave单/双服务各32组、业务单/双服务各130组(98总召、12计数量、4兜底、16校时)及3组规则测试均通过。脚本内首次Node启动因沙箱`spawnSync EPERM`失败,因此该次脚本整体不是成功退出;随后获准单独重跑相同Node回环命令,退出码0,真实本机TCP通过,核对submitted=65、clock_executed=3、clock_synchronized=3、clock_submitted=5。默认模拟校时的正常/TEST/COT=5/广播及103+102同批均覆盖,未执行system模式或真实设备设时。改前基线、大小探针及栈测量位于`.build/clock-synchronization-20261029/`。
2026-10-28 业务目录整理:共用召唤引擎、内部规则头、总召和计数量共7个源码/头文件移到`104/business/`,迁移前后SHA256逐字节一致;构建加入该目录的头文件搜索路径,同步测试直接包含路径及文档源码链接。严格GCC构建通过,单/双服务各110组业务测试、23组Slave测试及3组规则测试通过,公开头C++检查和真实Windows TCP回环通过。沙箱限制Node启动子进程时,回环使用原授权命令在沙箱外执行。迁移清单及备份位于`.build/business-layout-20261028/`,修改仅限IEC60870-Slave;未执行Keil目标构建或硬件联调。
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`中,仅描述当时的源码版本,不属于当前可运行测试。本文不再提供已删除测试的构建命令,也不将其历史通过数计入当前结果。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,461 @@
/* 独立主站字节测试:启动示例自己的Windows Net后端,仅连接127.0.0.1随机端口。
* Node内置模块即可运行,不安装包、不复用待测ASDU编码器,不更改系统时钟。
*/
'use strict';
const assert = require('node:assert/strict');
const net = require('node:net');
const { spawn, spawnSync } = require('node:child_process');
const { once } = require('node:events');
const { setTimeout: delay } = require('node:timers/promises');
const executable = process.argv[2];
if (!executable) throw new Error('Usage: node slave_example_loopback_test.cjs <example.exe>');
class Master {
constructor(socket) {
this.socket = socket;
this.buffer = Buffer.alloc(0);
this.frames = [];
this.waiters = [];
this.ns = this.nr = 0;
socket.on('data', data => {
this.buffer = Buffer.concat([this.buffer, data]);
try {
while (this.buffer.length >= 2) {
assert.equal(this.buffer[0], 0x68);
assert(this.buffer[1] >= 4 && this.buffer[1] <= 253);
const length = this.buffer[1] + 2;
if (this.buffer.length < length) break;
const frame = this.buffer.subarray(0, length);
this.buffer = this.buffer.subarray(length);
const waiter = this.waiters.shift();
if (waiter) { clearTimeout(waiter.timer); waiter.resolve(frame); }
else this.frames.push(frame);
}
} catch (error) { this.fail(error); socket.destroy(); }
});
socket.on('error', error => this.fail(error));
socket.on('close', () => this.fail(new Error('Connection closed')));
}
fail(error) {
this.error = error;
for (const waiter of this.waiters.splice(0)) {
clearTimeout(waiter.timer);
waiter.reject(error);
}
}
next() {
if (this.frames.length) return Promise.resolve(this.frames.shift());
if (this.error) return Promise.reject(this.error);
return new Promise((resolve, reject) => {
const waiter = { resolve, reject };
waiter.timer = setTimeout(() => {
this.waiters.splice(this.waiters.indexOf(waiter), 1);
reject(new Error('Timed out waiting for an APDU'));
}, 1500);
this.waiters.push(waiter);
});
}
async control(code, expected) {
this.socket.write(Buffer.from([0x68, 4, code, 0, 0, 0]));
for (let i = 0; i < 8; i++) {
const frame = await this.next();
if (frame[2] === 1) continue; // 主站I帧的S确认允许穿插。
assert.deepEqual([...frame], [0x68, 4, expected, 0, 0, 0]);
return;
}
throw new Error('Missing U-frame confirmation');
}
frame(payload) {
const frame = Buffer.alloc(payload.length + 6);
frame[0] = 0x68;
frame[1] = payload.length + 4;
frame.writeUInt16LE(this.ns++ << 1, 2);
frame.writeUInt16LE(this.nr << 1, 4);
Buffer.from(payload).copy(frame, 6);
return frame;
}
async send(payload, fragmented = false) {
const frame = this.frame(payload);
if (fragmented) {
this.socket.write(frame.subarray(0, 3));
await delay(10);
this.socket.write(frame.subarray(3));
} else this.socket.write(frame);
}
async readASDU() {
for (let i = 0; i < 8; i++) {
const frame = await this.next();
if (frame[2] === 1) continue;
assert.equal(frame[2] & 1, 0);
assert.equal(frame.readUInt16LE(2) >> 1, this.nr++);
assert.equal(frame.readUInt16LE(4) >> 1, this.ns);
const ack = Buffer.from([0x68, 4, 1, 0, 0, 0]);
ack.writeUInt16LE(this.nr << 1, 4);
this.socket.write(ack); // k=1,每帧ACK后服务才可继续发下一帧。
return frame.subarray(6);
}
throw new Error('Missing I frame');
}
async close() {
const closed = once(this.socket, 'close');
this.socket.end();
await closed;
}
}
const signals = [0, 1, 0, 1, 1, 0, 1, 0];
const doubleSignals = Array(8).fill(1);
const measures = [2300, 2310, 2290, 5000, -125, 0, 1234, 42];
const request = (oa, qoi = 20, ca = 7) => [100, 1, 0x86, oa, ca & 0xff, ca >> 8, 0, 0, 0, qoi];
// 构造单对象45/46命令原文:CA/IOA按小端编码,QU固定为1(短脉冲)。
// type/oa/ioa/state来自用例,select决定S/E;cot/ca默认激活原因6和示例站点7。
// 返回普通字节数组,也用于故意构造不支持类型的兜底请求,不在此执行业务校验。
const commandRequest = (type, oa, ioa, state, select, cot = 6, ca = 7) =>
[type, 1, cot, oa, ca & 0xff, ca >> 8, ioa & 0xff, (ioa >> 8) & 0xff, ioa >> 16,
state | 4 | (select ? 0x80 : 0)];
// 发送payload并断言紧随其后的应答;除COT/P/N外须回显原请求,TEST位也须保留。
// master负责链路收发及确认,cot/negative描述预期应答;Promise完成表示该帧已核对。
async function commandReply(master, payload, cot, negative = false) {
await master.send(payload);
const expected = [...payload]; expected[2] = (payload[2] & 0x80) | cot | (negative ? 0x40 : 0);
assert.deepEqual([...(await master.readASDU())], expected);
}
// 验证一次选择后执行的真实TCP全流程;monitorIoa为该命令对应的监视方向地址。
// 顺序逐帧读取:选择ACT_CON、执行ACT_CON、COT=11返回信息、执行原文ACT_TERM。
// 链路窗口k=1,只有前一帧处理完才能推进下一帧,可暴露返回信息/终止抢先的问题。
async function commandExecute(master, type, oa, ioa, state, monitorIoa, timeTag = null) {
const selection = commandRequest(type, oa, ioa, state, true).concat(timeTag ? timeTag() : []);
await commandReply(master, selection, 7);
const execute = commandRequest(type, oa, ioa, state, false).concat(timeTag ? timeTag() : []);
await commandReply(master, execute, 7);
assert.deepEqual([...(await master.readASDU())],
[type === 45 || type === 58 ? 1 : 3, 1, 11, oa, 7, 0, monitorIoa & 0xff, (monitorIoa >> 8) & 0xff, 0, state]);
const terminated = [...execute]; terminated[2] = 10;
assert.deepEqual([...(await master.readASDU())], terminated);
}
// 独立构造Type49/62的基础命令元素:有符号16位设定值及QOS,QL固定为0。
const scaledCommandRequest = (oa, ioa, value, select, type = 49) =>
[type, 1, 6, oa, 7, 0, ioa & 0xff, (ioa >> 8) & 0xff, ioa >> 16,
value & 0xff, (value >> 8) & 0xff, select ? 0x80 : 0];
async function scaledCommandExecute(master, oa, ioa, value, monitorIoa, type = 49, timeTag = null) {
const selection = scaledCommandRequest(oa, ioa, value, true, type).concat(timeTag ? timeTag() : []);
await commandReply(master, selection, 7);
const execute = scaledCommandRequest(oa, ioa, value, false, type).concat(timeTag ? timeTag() : []);
await commandReply(master, execute, 7);
assert.deepEqual([...(await master.readASDU())],
[11, 1, 11, oa, 7, 0, monitorIoa & 0xff, (monitorIoa >> 8) & 0xff, 0,
value & 0xff, (value >> 8) & 0xff, 0]);
const terminated = [...execute]; terminated[2] = 10;
assert.deepEqual([...(await master.readASDU())], terminated);
}
const initialCounters = [120000, 345600, 7890, 0, 1, 65535, 2147483647, 42];
const counterRequest = (oa, qcc = 5, cot = 6, test = false, ca = 7) =>
[101, 1, cot | (test ? 0x80 : 0), oa, ca & 0xff, ca >> 8, 0, 0, 0, qcc];
const clockRequest = (oa, cot = 6, test = false, ca = 7) =>
[103, 1, cot | (test ? 0x80 : 0), oa, ca & 0xff, ca >> 8, 0, 0, 0, 0x39, 0x30, 34, 12, 28, 10, 26];
// 主站独立计算UTC基准,逐次按单调经过时间生成UTC+8/无SU的CP56,选择与执行不复用旧时标。
function clockTagFactory(payload, baseNs) {
const tag = payload.slice(-7), milliseconds = tag[0] | (tag[1] << 8);
const utcMs = Date.UTC(2000 + tag[6], (tag[5] & 15) - 1, tag[4] & 31,
(tag[3] & 31) - 8 - ((tag[3] >> 7) & 1), tag[2] & 63,
Math.floor(milliseconds / 1000), milliseconds % 1000);
return (adjustMs = 0) => {
const elapsedMs = Number((process.hrtime.bigint() - baseNs) / 1000000n);
const local = new Date(utcMs + elapsedMs + adjustMs + 8 * 60 * 60 * 1000);
const minuteMs = local.getUTCSeconds() * 1000 + local.getUTCMilliseconds();
return [minuteMs & 0xff, minuteMs >> 8, local.getUTCMinutes(), local.getUTCHours(),
local.getUTCDate(), local.getUTCMonth() + 1, local.getUTCFullYear() - 2000];
};
}
let counterSubmitted = 0, counterCompleted = 0;
async function readCounters(master, oa, qcc = 5, values = initialCounters, sequence = 0) {
const group = qcc & 0x3f;
const first = group === 2 ? 4 : 0, end = group === 1 ? 4 : 8;
const count = 2 + (end - first) / 4;
for (let frame = 0; frame < count; frame++) {
const asdu = await master.readASDU();
if (frame === 0 || frame === count - 1) {
assert.deepEqual([...asdu], counterRequest(oa, qcc, frame === 0 ? 7 : 10));
} else {
assert.equal(asdu.length, 38);
assert.deepEqual([...asdu.subarray(0, 6)], [15, 4, group === 5 ? 37 : 37 + group, oa, 7, 0]);
for (let point = 0; point < 4; point++) {
const offset = 6 + 8 * point, index = first + 4 * (frame - 1) + point;
assert.equal(asdu.readUIntLE(offset, 3), 3001 + index);
assert.equal(asdu.readInt32LE(offset + 3), values[index]);
assert.equal(asdu[offset + 7], sequence);
}
}
}
counterSubmitted += count;
counterCompleted++;
}
async function counterRead(master, oa, qcc = 5, values = initialCounters, sequence = 0, ca = 7) {
await master.send(counterRequest(oa, qcc, 6, false, ca));
await readCounters(master, oa, qcc, values, sequence);
}
async function counterReply(master, payload, cot, negative = false) {
await master.send(payload);
const expected = [...payload]; expected[2] = (payload[2] & 0x80) | cot | (negative ? 0x40 : 0);
assert.deepEqual([...(await master.readASDU())], expected);
counterSubmitted++;
if (!negative) counterCompleted++;
}
async function totalCall(master, oa, fragmented = false, qoi = 20, ca = 7) {
await master.send(request(oa, qoi, ca), fragmented);
await readTotalCall(master, oa, qoi);
}
async function readTotalCall(master, oa, qoi = 20, busyRequests = []) {
const original = request(oa, qoi);
// 站总召依次返回单点、双点、遥测;遥信分组含前两类,遥测分组只含最后一类。
// 每类8点、每帧4点,首尾另有ACT_CON与ACT_TERM。
const types = qoi === 20 ? [1, 3, 11] : qoi === 21 ? [1, 3] : [11];
const count = 2 + types.length * 2;
const busyReplies = busyRequests.map(payload => {
const expected = [...payload]; expected[2] = 0xc7;
if (expected[4] === 0xff && expected[5] === 0xff) { expected[4] = 7; expected[5] = 0; }
return Buffer.from(expected);
});
for (let f = 0; f < count || busyReplies.length;) {
const asdu = await master.readASDU();
if ((asdu[2] & 0x40) !== 0) {
const index = busyReplies.findIndex(expected => expected.equals(asdu));
assert(index >= 0, `Unexpected negative reply: ${asdu.toString('hex')}`);
busyReplies.splice(index, 1);
continue;
}
assert(f < count, 'Unexpected response after ACT_TERM');
if (f === 0 || f === count - 1) {
const expected = [...original];
expected[2] = f === 0 ? 0x87 : 0x8a;
assert.deepEqual([...asdu], expected);
} else {
const type = types[Math.floor((f - 1) / 2)];
const digital = type !== 11;
const values = type === 1 ? signals : type === 3 ? doubleSignals : measures;
const baseIoa = type === 1 ? 1001 : type === 3 ? 1101 : 2001;
const width = digital ? 4 : 6;
const first = ((f - 1) % 2) * 4;
assert.equal(asdu.length, 6 + width * 4);
assert.deepEqual([...asdu.subarray(0, 6)], [type, 4, 0x80 | qoi, oa, 7, 0]);
for (let p = 0; p < 4; p++) {
const offset = 6 + p * width;
assert.equal(asdu.readUIntLE(offset, 3), baseIoa + first + p);
if (digital) assert.equal(asdu[offset + 3], values[first + p]);
else {
assert.equal(asdu.readInt16LE(offset + 3), values[first + p]);
assert.equal(asdu[offset + 5], 0);
}
}
}
f++;
}
}
async function main() {
const help = spawnSync(executable, ['--help'], { windowsHide: true, encoding: 'utf8' });
assert.ifError(help.error);
assert.equal(help.status, 0, help.stderr);
assert(help.stdout.includes('--run-ms'));
assert(help.stdout.includes('--clock simulated|system'));
assert(help.stdout.includes('Type45 IOA6001..6008'));
assert(help.stdout.includes('Type46 IOA6101..6108'));
assert(help.stdout.includes('Type49 IOA6301..6308'));
assert(help.stdout.includes('Type58/59/62'));
const invalid = spawnSync(executable, ['--ca', '0'], { windowsHide: true, encoding: 'utf8' });
assert.ifError(invalid.error);
assert.equal(invalid.status, 1);
const invalidClock = spawnSync(executable, ['--clock', 'invalid'], { windowsHide: true, encoding: 'utf8' });
assert.ifError(invalidClock.error);
assert.equal(invalidClock.status, 1);
const child = spawn(executable, ['--port', '0', '--ca', '7', '--window', '1', '--run-ms', '12000'],
{ windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
let output = '', errors = '';
const sockets = [];
child.stdout.on('data', data => { output += data.toString(); });
child.stderr.on('data', data => { errors += data.toString(); });
const exit = once(child, 'exit');
const deadline = setTimeout(() => { child.kill(); }, 20000);
try {
const port = await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`No listening port: ${output} ${errors}`)), 2000);
child.once('error', error => { clearTimeout(timer); reject(error); });
child.stdout.on('data', () => {
const match = output.match(/LISTEN 127\.0\.0\.1:(\d+) CA=7/);
if (match) { clearTimeout(timer); resolve(Number(match[1])); }
});
});
async function connect() {
const socket = net.connect({ host: '127.0.0.1', port });
sockets.push(socket);
const master = new Master(socket);
await once(socket, 'connect');
await master.control(7, 0x0b);
return master;
}
const master = await connect();
// 一次write内的106延时获得和102读取逐条回显,k=1时第二条由HIGH等待首条ACK。
const unsupportedBatch = [
[106, 1, 0x86, 0x18, 7, 0, 0, 0, 0, 0x39, 0x30],
[102, 1, 5, 0x19, 7, 0, 0xe9, 3, 0]
];
master.socket.write(Buffer.concat(unsupportedBatch.map(payload => master.frame(payload))));
for (const payload of unsupportedBatch) {
const expected = [...payload]; expected[2] = (payload[2] & 0x80) | 0x40 | 44;
assert.deepEqual([...(await master.readASDU())], expected);
}
// 默认模拟时钟:成功、TEST拒绝、错误COT及广播实际CA确认都保持请求时标原文。
for (const [payload, cot, negative] of [
[clockRequest(0x10), 7, false],
[clockRequest(0x11, 6, true), 7, true],
[clockRequest(0x12, 5), 45, true],
[clockRequest(0x13, 6, false, 65535), 7, false]
]) {
await master.send(payload);
const expected = [...payload];
expected[2] = (payload[2] & 0x80) | cot | (negative ? 0x40 : 0);
expected[4] = 7; expected[5] = 0;
assert.deepEqual([...(await master.readASDU())], expected);
}
// 同批103先保存执行任务,102当场兜底;Try确认必须等已提交的44之后发送。
const clockBatch = clockRequest(0x14);
const readBatch = [102, 1, 5, 0x15, 7, 0, 0xe9, 3, 0];
const clockBaseNs = process.hrtime.bigint();
master.socket.write(Buffer.concat([master.frame(clockBatch), master.frame(readBatch)]));
const readRejected = [...readBatch]; readRejected[2] = 0x40 | 44;
assert.deepEqual([...(await master.readASDU())], readRejected);
const clockConfirmed = [...clockBatch]; clockConfirmed[2] = 7;
assert.deepEqual([...(await master.readASDU())], clockConfirmed);
const currentClockTag = clockTagFactory(clockBatch, clockBaseNs);
// 遥控默认先选择;k=1验证确认、返回信息和终止逐帧交接,后续总召必须读到新值。
await commandExecute(master, 45, 0x71, 6001, 1, 1001);
// 同步后续总召的预期值,验证遥控修改的是示例应用点表而非仅构造一次返回帧。
signals[0] = 1;
await commandExecute(master, 46, 0x72, 6103, 2, 1103);
// 站总召和遥信分组均须读回双点1103的新位置,其余双点仍为初值1。
doubleSignals[2] = 2;
// 设定值先验证应用限值拒绝,再走完整事务;后面的站总召和QOI=22必须读到新运行值。
await commandReply(master, scaledCommandRequest(0x79, 6301, 10001, true), 7, true);
await scaledCommandExecute(master, 0x7a, 6301, -1234, 2001);
measures[0] = -1234;
// 撤销故意使用不同状态值:匹配只要求类型/站点/IOA/OA,不比较命令状态。
await commandReply(master, commandRequest(45, 0x73, 6002, 0, true), 7);
await commandReply(master, commandRequest(45, 0x73, 6002, 1, true, 8), 9);
// 选择已撤销,默认不允许直接执行,因此后续执行必须被否定。
await commandReply(master, commandRequest(45, 0x73, 6002, 0, false), 7, true);
// 公共地址错误由模块判定;遥信地址1001不是Type45命令地址,由应用点表返回47。
await commandReply(master, commandRequest(46, 0x74, 6101, 1, true, 6, 65535), 46, true);
await commandReply(master, commandRequest(45, 0x75, 1001, 1, true), 47, true);
// TEST命令须原样保留TEST位并否定,不能启动模拟输出。
const commandTest = commandRequest(45, 0x76, 6001, 0, true); commandTest[2] |= 0x80;
await commandReply(master, commandTest, 7, true);
// 示例仍不启用47;58/59/62使用新鲜时标,确认和终止逐字节保留各自原CP56。
await commandReply(master, commandRequest(47, 0x77, 1001, 1, false), 44, true);
await commandExecute(master, 58, 0x78, 6002, 0, 1002, currentClockTag);
signals[1] = 0;
await commandExecute(master, 59, 0x7b, 6104, 2, 1104, currentClockTag);
doubleSignals[3] = 2;
await scaledCommandExecute(master, 0x7d, 6302, -4321, 2002, 62, currentClockTag);
measures[1] = -4321;
// 过期58与106同批:先断言106,再严格读取完整计数量流程,任何迟到58帧都会失败。
// STOP统计另证明过期选择未执行应用动作;旧58兜底替换为106兜底,总召统计保持原值。
const expiredCommand = commandRequest(58, 0x7e, 6003, 1, true).concat(currentClockTag(-60000));
const barrier = [106, 1, 6, 0x7f, 7, 0, 0, 0, 0, 0, 0];
master.socket.write(Buffer.concat([master.frame(expiredCommand), master.frame(barrier)]));
const barrierReply = [...barrier]; barrierReply[2] = 0x40 | 44;
assert.deepEqual([...(await master.readASDU())], barrierReply);
await counterRead(master, 0x20);
await counterReply(master, counterRequest(0x21, 0x45), 7);
await counterRead(master, 0x22, 5, initialCounters, 1);
await counterRead(master, 0x23, 1, initialCounters, 1);
await counterRead(master, 0x24, 2, initialCounters, 1);
await counterReply(master, counterRequest(0x25, 3), 7, true);
await counterReply(master, counterRequest(0x26, 4), 7, true);
await counterReply(master, counterRequest(0x27, 5, 8), 45, true);
// TEST requests must not freeze or clear real counters.
await counterReply(master, counterRequest(0x28, 0x85, 6, true), 7, true);
await counterRead(master, 0x29, 5, initialCounters, 1, 65535);
await counterReply(master, counterRequest(0x2a, 0x85), 7);
await counterRead(master, 0x2b, 5, initialCounters, 2);
await counterReply(master, counterRequest(0x2c, 0x45), 7);
const zeroCounters = initialCounters.map(() => 0);
await counterRead(master, 0x2d, 5, zeroCounters, 3);
await counterReply(master, counterRequest(0x2e, 0xc5), 7);
await counterRead(master, 0x2f, 5, zeroCounters, 3);
await totalCall(master, 0x5a, true);
await totalCall(master, 0x5b, false, 21);
await totalCall(master, 0x5c, false, 22);
// 广播请求的确认、数据和终止均按实际本站CA=7核对。
await totalCall(master, 0x5d, true, 20, 65535);
await totalCall(master, 0x5e, false, 21, 65535);
await totalCall(master, 0x5f, false, 22, 65535);
// 连续提交不同QOI:只执行第一个,后两个否定;结束后由主站重发才执行。
const burst = [[0x31, 22, 65535], [0x32, 20, 65535], [0x33, 21, 7]];
master.socket.write(Buffer.concat(burst.map(([oa, qoi, ca]) => master.frame(request(oa, qoi, ca)))));
await readTotalCall(master, 0x31, 22, burst.slice(1).map(([oa, qoi, ca]) => request(oa, qoi, ca)));
for (const [oa, qoi] of burst.slice(1)) await totalCall(master, oa, false, qoi);
for (const [index, value, cot] of [[4, 8, 46], [9, 37, 7], [9, 36, 7], [0, 200, 44]]) {
const invalidRequest = request(0x55);
invalidRequest[index] = value;
if (index === 9) { invalidRequest[4] = invalidRequest[5] = 0xff; }
await master.send(invalidRequest);
const expected = [...invalidRequest];
expected[2] = 0xc0 | cot;
if (index === 9) { expected[4] = 7; expected[5] = 0; }
assert.deepEqual([...(await master.readASDU())], expected);
}
// 同一接收批次的激活/撤销在驱动业务前按FIFO处理;随后重复撤销应否定确认。
const activation = request(0x4a, 21, 65535);
const deactivation = request(0x4a, 21); deactivation[2] = 0x88;
master.socket.write(Buffer.concat([master.frame(activation), master.frame(deactivation)]));
const deactCon = [...deactivation]; deactCon[2] = 0x89;
assert.deepEqual([...(await master.readASDU())], deactCon);
deactivation[4] = deactivation[5] = 0xff;
await master.send(deactivation);
deactCon[2] = 0xc9;
assert.deepEqual([...(await master.readASDU())], deactCon);
await master.control(0x13, 0x23);
await master.control(7, 0x0b);
// k=1: both instances must make progress, preserving their own response order.
master.socket.write(Buffer.concat([master.frame(request(0x6b)), master.frame(counterRequest(0x60))]));
const generalFrames = [], counterFrames = [];
for (let index = 0; index < 12; index++) {
const asdu = await master.readASDU();
(asdu[0] === 101 || asdu[0] === 15 ? counterFrames : generalFrames).push(asdu);
}
assert.equal(generalFrames.length, 8);
assert.equal(counterFrames.length, 4);
await readTotalCall({ readASDU: async () => generalFrames.shift() }, 0x6b);
await readCounters({ readASDU: async () => counterFrames.shift() }, 0x60, 5, zeroCounters, 3);
await master.close();
const next = await connect();
await totalCall(next, 0x7c);
await next.close();
const [code] = await exit;
assert.equal(code, 0, `${output}\n${errors}`);
assert.match(output, /STOP submitted=83 gi_submitted=11 /);
assert.match(output, /deactivated=1 timeouts=0/);
assert.match(output, /negative_submitted=\d+ dropped_requests=0/);
assert.match(output, new RegExp(`counter_submitted=${counterSubmitted} counter_completed=${counterCompleted}`));
assert.match(output, /clock_executed=3 clock_synchronized=3 clock_submitted=5/);
// 七次成功选择含一次撤销;六个执行均返回当前值,越限49和过期58不增加选择/执行次数。
assert.match(output, /command_selected=7 command_executed=6 command_completed=6 command_submitted=\d+ command_returns=6 command_time_expired=1 command_time_rejected=0/);
assert.equal(errors, '');
console.log('Slave example loopback: passed (real TCP, Type45/46/49/58/59/62 select/execute/return/termination, scaled limits, fresh timestamps and silent expiry, command cancel/TEST/address/unsupported types, changed single/double/scaled GI values, batched 106+102 and 103+102, simulated clock, counters, k=1, STOP/START, reconnect, cleanup)');
} finally {
clearTimeout(deadline);
for (const socket of sockets) socket.destroy();
if (child.exitCode === null) child.kill();
}
}
main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -0,0 +1,53 @@
Command extension, 2026-10-31. Scope: IEC60870-Slave only.
Before snapshots: before/ (production sources, tests, documentation, build script).
Baseline: Slave 32 groups x2; example 131 groups x2;
Command 27 groups x4; interrogation rules 3 groups: PASS.
Real TCP loopback: PASS after rerun outside sandbox; sandbox child spawn returned EPERM.
Baseline x64: instance=616 (reply capacity4), transaction=168, reply=56,
request=48, event=88, status=120.
Baseline GCC -O2 stack frames (bytes): Submit_Head144, Handle_Event208,
Process64, NextEvent160, Accept128, Submit_Return_Info144.
These are Windows host results, not ARM target measurements.
5a: old tests unchanged. Slave32 x2, example131 x2, Command27 x4,
rules3: PASS. Real TCP loopback: PASS (run outside sandbox).
5a x64: instance=480/672 (reply capacity1/4), transaction=192, reply=64,
request=48, event=88, config=20, status=120. C++11 header: PASS.
5a GCC -O2 frames: Decode_Element144, Copy_Request96, Submit_Head176,
Handle_Event304, Process64, NextEvent144, Accept144, Submit_Return_Info144.
Fault handling: decode before Set_Confirm; failed work decode cancels the
transaction, and FINISHED can still release ownership using identity fields.
5b verification:
Full scripts/build.ps1 -Test: PASS, including real TCP loopback.
Slave 32 groups x2; example 131 x2; Command 33 x4; rules 3.
Type 49 selection/execution/Type 11 return/termination and updated GI values: PASS.
x64 instance=480/672 (reply capacity 1/4), transaction=192, reply=64.
Request=48, Event=88, Config=20, Status=120; C++11 header: PASS.
Stack frames unchanged from 5a (GCC -O2, -fstack-usage).
5c verification:
Full scripts/build.ps1 -Test: PASS (exit 0, strict compiler warnings).
Slave 32 groups x2; example 132 x2; Command 45 x4; rules 3.
Real TCP: Type 45/46/49/58/59/62 complete flows; changed GI values;
expired Type 58 silent, no action; STOP/START/reconnect: PASS.
STOP submitted=83, gi_submitted=11; command selected=7, executed=6,
completed=6, returns=6, time_expired=1, time_rejected=0.
System-clock example tests use mock Read/Set; host clock not changed.
x64 instance=512/704 (reply capacity 1/4), transaction=192, reply=64.
Request=80, Event=120, Config=28, Status=128; C++11 header: PASS.
GCC -O2 frames: Decode_Element144, Copy_Request112, Submit_Head176,
Handle_Event288, Process64, NextEvent176, Accept176, Submit_Return_Info144.
Private decode-failure probe: PASS (two corruption paths, no action or
positive confirmation, owed FINISHED delivered once, transaction released).
Monotonic reversal also closes TCP: test checks rejection counters and no
action, not successful transmission over the failed connection.
MCU build, target RAM/stack and hardware interoperation not performed.
All changes and artifacts remain within IEC60870-Slave.
Final Command-only matrix rerun after extra assertions: PASS, 45 groups x4.
Added explicit 49->48 / 45->58 / Type63 OA mismatch and selected timeout
after an expired execute; production sources unchanged from full -Test PASS.
Final snapshots: 5c/ (all 15 modified tracked files).
Documentation: 17 Markdown files, 356 local links/anchors and 16 public
function declarations checked. UTF-8/LF/BOM and git diff --check: PASS.
@@ -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 */
File diff suppressed because it is too large Load Diff
@@ -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 */
File diff suppressed because it is too large Load Diff
@@ -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 */
File diff suppressed because it is too large Load Diff
@@ -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 */
File diff suppressed because it is too large Load Diff
@@ -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,559 @@
/*
* IEC104时钟同步业务(Type 103,C_CS_NA_1)。
* 用途:将主站给出的时间交给应用校准设备时钟,使故障事件、SOE和日志采用一致时间。
* 本模块只处理协议及时间换算,不直接写系统时钟或RTC;实际设时由应用提供的action完成。
*
* 推进顺序:Handle_Event校验并保存 -> Execute_Next调用一次action -> Process续交应答。
* 校验失败直接进入待应答;校验成功先等应用执行。两类任务共用固定槽,回复严格按受理顺序。
* 时间计算使用Net单调毫秒,不能用正在被校准的日历时钟计算期限和站内等待时间。
* 所有接口由同一事件循环串行调用;STOP/断线取消未交接任务,不回滚已完成的设时。
*/
#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");
/* EXECUTE等待应用显式设时;REPLY等待提交确认,发送WAIT不会退回EXECUTE。 */
enum { CLOCK_EXECUTE = 1, CLOCK_REPLY = 2 };
typedef struct {
IEC104_SlaveEventIdentity_t identity; /* 原Slave实例及连接代次,防止旧确认发到新连接。 */
uint8_t bytes[16]; /* 6字节ASDU头+3字节IOA+7字节CP56;仅规范VSQ及改写应答头。 */
IEC104_StationSnapshot_t stations; /* 广播受理时固定目标集合;含自引用,禁止移动槽。 */
IEC104_Station_t *station; /* 定向Acquire;广播由活动快照保护当前节点。 */
uint64_t receive_serial, accepted_ms; /* 原I帧编号;Handle_Event受理时的单调毫秒。 */
uint16_t response_ca; /* 定向原CA;广播当前应答站的CA,WAIT期间保持目标不变。 */
uint8_t phase, cause; /* EXECUTE/REPLY;待回传的COT,0表示尚未生成应答。 */
bool negative, executed, send_waiting, occupied; /* P/N、动作已消费、上次提交WAIT、槽已占用。 */
} ClockTask;
struct IEC104_ClockSynchronization {
IEC104_Slave_t *slave; /* 借用服务,必须先销毁本模块再销毁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; /* FIFO槽下标;占用数量。 */
uint64_t serial; /* 进程内递增的实例编号,销毁后不回收编号。 */
bool in_use, in_action; /* 实例池占用;动作期间阻止修改模块或递归执行其他任务。 */
};
static IEC104_ClockSynchronization_t clock_pool[IEC104_CLOCK_SYNCHRONIZATION_CAPACITY];
static uint64_t next_serial;
/* 先比对池内地址再解引用,NULL或不属于本池的句柄均返回false。 */
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;
}
/* 结束一个任务:广播End快照,定向Release持有的站点,再归还固定槽。
* 正常提交、超时应答完成和所有取消路径均使用此入口,避免遗留站点引用。
* 活动快照含自引用,任务永不搬移;仅压缩顺序索引。 */
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));
}
/* 内部统一清理:cancelled按任务数累计,不按广播站数累计。
* 只清理本模块仍持有的任务;发送层已接手的字节和应用已改过的时钟不受影响。 */
static void Cancel(IEC104_ClockSynchronization_t *self)
{
while (self->count != 0U) {
++self->status.cancelled;
Remove_Task(self, &self->tasks[self->order[0]]);
}
}
/* 从空闲固定槽取一个并追加FIFO下标;满槽返回NULL,既不覆盖也不移动旧任务。 */
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;
}
/* UTC纪元毫秒上限:Seconds_To_Local还要加本站UTC+8,须预留这8小时。
* 先转64位再乘1000;尾部999允许最后一个合法整秒内的全部毫秒。 */
static uint64_t Limit_Ms(void)
{
return ((uint64_t)IEC60870_TIME_MAX_SECONDS -
(uint64_t)IEC60870_LOCAL_UTC_OFFSET_MINUTES * 60U) * 1000U + 999U;
}
/* 将完整16字节请求中的CP56换算为UTC毫秒,输出仅在全部检查成功时更新。
* 对象解码检查日期及保留位;CP56_To_Seconds检查IV、源时区并扣除SU对应的一小时。
* UTC合法之后还须确认固定UTC+8的本站日历没有越界,此处尚不补偿站内等待时间。
* 错误回复仍使用原文字节,不要求非法时标重新编码,也不会用换算值改写确认时标。 */
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;
}
/* 查询当前身份是否还可执行/提交,不推进网络I/O。
* WAIT_EVENTS先交回应用读通知:即使最终链路又已START,也不能跳过中间的STOP。
* 查询失败或通知读空后发现会话失效,统一取消任务;通知优先于业务期限归因。 */
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;
}
/* 尚未执行的任务因期限或时间错误改为否定ACT_CON,保留原槽和FIFO位置。
* 不置executed,因此它走普通应答路径;后续Execute_Next不会再选中此任务。 */
static void Negative_Reply(ClockTask *task)
{
task->phase = CLOCK_REPLY;
task->cause = IEC60870_ASDU_COT_ACTIVATION_CON;
task->negative = true;
task->send_waiting = false;
}
/* 返回启用中的执行期限剩余毫秒;0为到期(含单调时间倒退)。
* 同一采样供到期判断与执行补偿使用;倒退显式归因,不依赖无符号下溢。 */
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);
}
/* Process和Execute_Next共用的期限处理;就地转换,不调用应用、不直接发包。
* 只约束尚未执行的任务;action一旦调用,其确认等待不再受此期限约束。 */
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;
}
}
}
/* 提交入口可能先处理网络输入,导致前一次检查READY、实际提交却遇到STOP。
* 仅当同一连接仍健康、传输已停用且有未读通知时,将ERROR归为等待STOP通知。
* 此路径保留任务,不计发送WAIT;应用消费DEACTIVATED后统一取消。
* 其他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; }
/* 调用过action的肯定/否定确认都走Try,等HIGH/NORMAL及当前帧让出位置,
* 防止确认越过已排队的旧时标数据;未执行动作的校验/期限错误可进入HIGH。 */
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) {
/* OK只表示发送层接手,不等于socket写完或主站ACK;此时不可再次提交同一目标。 */
++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;
}
/* 受理一条103:先确认能对应完整对象,再按COT -> CA -> IOA -> 其他原因判错。
* 非本模块请求交后续处理器;已识别但畸形/满槽的请求返回HANDLED,不能再走44兜底。
* 本函数不调用action,合法请求只进入EXECUTE,其他可应答请求进入REPLY。 */
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) {
/* 无法保留唯一完整的IOA+CP56,不能靠截取或补零伪造可回显的请求。 */
++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);
/* 即使时间非法也保持错误优先级;例如COT=5必须回45,不能被非法日期改成7。 */
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;
/* 原事件仍由调用方持有,只保存对象完整的前16字节;SQ/尾字节错误也可规范回显。 */
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;
/* 事件没有网络接收时刻,补偿从此处受理开始,无法补上此前FIFO及网络等待。 */
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; }
/* 只有唯一的单目标错误应答可尝试当场提交;Submit_Head仍先检查所有未读通知。 */
if (cause != 0U && !broadcast && self->count == 1U && Submit_Head(self) < 0) {
return IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR;
}
return IEC104_CLOCK_SYNCHRONIZATION_EVENT_HANDLED;
}
/* 默认主站使用UTC+8标准时,受理后最多等待15秒执行;NULL配置指针不作处理。 */
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;
}
}
/* 创建只分配静态实例并复制配置,不初始化Slave、不消费通知,也不设置设备时间。
* 同一Slave不能同时绑定两个校时实例;编号耗尽时拒绝创建,避免回绕复用编号。 */
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;
}
/* 有未完成任务时由调用方先Cancel;回调期间不能释放action正在使用的实例。 */
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); }
}
/* 与其他业务模块一同按Slave FIFO顺序接收连接及启停通知,再分派I帧。
* 服务实例身份一旦绑定不得替换;CONNECTED重置连接代次,旧代次通知不推进任务。
* STOP/断线清理待执行及待确认任务,已调用action的设备时间不会被回滚。 */
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;
}
}
/* 有界推进:每步先核对会话,再处理期限、尝试交接队首回复,绝不调用action。
* 广播每交接一个站或结束一次遍历均消耗一步;预算耗尽交回应用,避免独占事件循环。
* ERROR/FATAL已按提交结果清理任务时返回true,false仅表示接口/查询/组包故障。 */
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;
}
/* 唯一的设时动作入口:每次最多调用一次action,不保存回调和context。
* 可以越过前面的REPLY寻找最早EXECUTE,但只允许队首提交应答,回复顺序不变。
* 已过期或时间不可用的请求转成否定回复后继续寻找,最终可能EMPTY且仍有回复待交。 */
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));
/* Decode_Time成功保证utc_ms不大于上限;先比较剩余空间,再加等待毫秒,
* 同时防止64位溢出及本站日历越2099年。期限关闭也不能跳过这些检查。 */
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;
/* 只将补偿后的时间交给action;task->bytes中的主站CP56保持原样供确认回显。 */
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;
}
}
/* 只读快照:重算未读通知、待执行/确认以及最近期限,不触发Expire或取消任务。
* 回调中也允许查询;此时当前动作已消费,confirm_pending为true,且不再计它的期限。 */
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]];
/* 发送WAIT不要求空转;应用每次Poll并读空通知后仍必须Process重试。
* 需要清理的失效会话、到期任务或尚未等待发送的队首回复则应立即推进。 */
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;
}

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