补充遥调业务代码
This commit is contained in:
@@ -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.
|
||||
+1617
File diff suppressed because it is too large
Load Diff
+342
@@ -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-编译与运行)。
|
||||
File diff suppressed because it is too large
Load Diff
+139
@@ -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;这不是模块承诺的动作时效上限。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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, ¶meters, buffer, sizeof(buffer), &header, 0);
|
||||
}
|
||||
if (result == IEC60870_ASDU_OK) {
|
||||
result = IEC60870_Asdu_Add_InformationObject(&asdu, &point.object);
|
||||
}
|
||||
if (result == IEC60870_ASDU_OK) {
|
||||
result = IEC60870_Asdu_Get_Data(&asdu, &data, &length);
|
||||
}
|
||||
/* OK时可将data/length交给Enqueue_Event;提交返回WAIT时保留原文重试。 */
|
||||
```
|
||||
|
||||
监视和命令统一使用`IEC60870_Asdu_Parse`校验整条报文,再通过`IEC60870_Asdu_Get_Element`取得独立类型的对象。例如处理单命令:
|
||||
|
||||
```c
|
||||
/* parameters、data、length对应收到的ASDU;处理期间原文保持有效。 */
|
||||
IEC60870_AsduInfo_t info;
|
||||
IEC60870_AsduResult_t status =
|
||||
IEC60870_Asdu_Parse(¶meters, data, length, &info);
|
||||
if (status == IEC60870_ASDU_OK && info.type_id == IEC60870_ASDU_C_SC_NA_1) {
|
||||
IEC60870_SingleCommand_t command;
|
||||
status = IEC60870_Asdu_Get_Element(&info, 0, &command, sizeof(command));
|
||||
if (status == IEC60870_ASDU_OK) {
|
||||
/* info保存COT/CA/OA;command.object.ioa、state、is_select、
|
||||
* qualifier供业务判断地址、限定词、选择/执行及权限。 */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Get_Element输出无需预先Create,必须按`info.type_id`选择匹配的具体结构体并传入其尺寸;尺寸检查不能替代C类型匹配。解析视图借用原文和参数,取出的具体对象按值保存。只处理单个载荷时,可直接调用对应类型的`Get_From_Buffer`,传入已解析IOA及不含IOA的精确载荷;读命令载荷为0字节,允许NULL。
|
||||
|
||||
归一化值使用原始`int16_t`(实际值为原值/32768),标度值使用`int16_t`,短浮点保留IEEE 754 binary32位模式。累计量用`IEC60870_BinaryCounterReading_t`保存32位计数、5位序号及CY/CA/IV。对象层校验品质保留位、SCO、DCO/RCO状态、VTI有符号范围、保护事件QDP和CP24/CP56;COT、CA、IOA及限定词的业务适用性由应用判断。
|
||||
|
||||
`Build_Response(¶meters, request, length, cause, is_negative, out, capacity, &out_length)`仍以`IEC60870_Asdu_`为前缀。它复制请求,只替换COT/P/N,保留TEST、存在时的OA、CA、VSQ及全部对象原文,支持原地和部分重叠。它仅作公共头及最低长度检查,可回显未知类型。生成后携带原请求generation调用Send_Response,并处理WAIT/ERROR/FATAL。
|
||||
|
||||
`IEC60870_Asdu_Create_From_Buffer(&asdu, ¶meters, buffer, length)`直接借用可写原文,建立可修改的ASDU对象。它只做公共头及最低长度检查,未知类型也能建立对象,用于COT=44等错误应答;执行业务前仍须使用完整Parse和业务规则校验。借用对象不能Add,`Set_COT`、`Set_Negative`、`Set_CA`原地修改相应字段;Get_Data可以导出原文。Create创建的构造容器也可使用这三个设置接口,空构造容器仍不能导出。Set_CA按COT宽度定位、CA宽度写入,1字节仅允许0..255,2字节允许0..65535;非法容器或地址溢出返回ERROR且不修改原文,业务地址限制仍由业务层判断。ASDU结构新增来源标志,调用方须一起重新编译。
|
||||
|
||||
### 文件段和新增时间格式
|
||||
|
||||
`IEC60870_FileSegment_t`含255字节的数据数组。Create和Get_From_Buffer把有效数据复制进对象,调用结束后即可复用输入;对象无需销毁。它的线上载荷为`NOF(2)+NOS(1)+LOS(1)+数据(LOS)`,解析精确检查LOS与报文长度,不接受截短和多余尾字节。
|
||||
|
||||
先用`IEC60870_FileSegment_Get_Max_Data_Size(parameters)`计算当前配置允许的数据长度。默认104配置返回236;最短101头、1字节IOA、254字节上限时返回245。返回0表示只能容纳空段,-1表示配置非法或连空段都装不下。Create允许0..255字节,实际加入ASDU时仍检查总长;超过容量返回FULL,调用方负责分段。
|
||||
|
||||
`IEC60870_Time_To_CP16/From_CP16`处理0..65535毫秒的经过时间。`IEC60870_Time_To_CP24/From_CP24`处理分钟0..59、分钟内毫秒0..59999和IV;bit6是保留位,CP24没有SU、小时或日期。CP24编解码保留IV,不自动推断完整日历。所有接口均沿用成功1、失败0且保留输出的约定。
|
||||
|
||||
### 子站统一入口
|
||||
|
||||
业务包含`IEC104_Slave.h`,持有一个不透明的`IEC104_Slave_t *`。Slave内部创建、驱动和销毁TcpServer;配置、调度、状态和结果仍复用`IEC104_TcpTypes.h`中的值类型,业务头文件不引入TcpServer操作接口或内部布局。
|
||||
|
||||
生命周期为`IEC104_Slave_Create → Config_Default/Init → Prepare/Process或Poll → Close → NextEvent读空 → Destroy`。所有操作在同一事件循环线程串行执行。静态池容量沿用`IEC104_TCP_SERVER_CAPACITY`,与直接使用TcpServer共享底层名额,无动态分配。创建失败返回NULL;其他生命周期接口显式比较`IEC104_TCP_OK`。
|
||||
|
||||
`IEC104_Slave_NextEvent(slave, &event)`按原FIFO交付`IEC104_SlaveEvent_t`:`event.transport`是TCP事件副本;I_FRAME且`event.asdu_result == IEC60870_ASDU_OK`时,`event.asdu`已经借用事件自身的ASDU字节,`event.connection`已经绑定原服务实例及原连接代次。业务不再传入server/generation,也不再手工Create_From_Buffer。字段/地址/命令权限仍由业务校验,未知类型可以原文交付,视图创建成功不等于允许执行命令。
|
||||
|
||||
普通循环使用以下顺序;Application_Handle_Event为应用自己的同步处理函数,其中可以提交应答或复制事件到延迟任务:
|
||||
|
||||
```c
|
||||
IEC104_TcpResult_t result = IEC104_Slave_Poll(slave, 10, NULL);
|
||||
if (result == IEC104_TCP_OK) {
|
||||
IEC104_SlaveEvent_t event;
|
||||
IEC104_TcpEventResult_t next;
|
||||
while ((next = IEC104_Slave_NextEvent(slave, &event)) == IEC104_TCP_EVENT_READY) {
|
||||
Application_Handle_Event(slave, &event);
|
||||
}
|
||||
/* next为ERROR时处理内部读取错误;EMPTY表示本轮事件已读空。 */
|
||||
}
|
||||
```
|
||||
|
||||
外部事件循环使用Slave_Prepare/Process,继续保留原service_token和socket token;pending_event_count或need_process非零时先处理本地工作。主动上报通过`IEC104_Slave_Get_Application_Layer_Parameters`取得参数构造ASDU,再调用`IEC104_Slave_Enqueue_Event(slave, &asdu)`进入NORMAL。关闭单连接传原事件给`IEC104_Slave_Close_Connection(slave, &event, error)`,库内部使用该事件的原代次。
|
||||
|
||||
### 共用连接上的激活确认和激活终止
|
||||
|
||||
共用接口`IEC60870_MasterConnection_t`继续供101/104业务复用;当前104的接口由Slave_NextEvent自动安装,101串口后端仍未提供。下例放在业务已验证请求并决定肯定确认的分支,event为地址稳定的Slave事件:
|
||||
|
||||
```c
|
||||
if (event.transport.type == IEC104_TCP_EVENT_I_FRAME &&
|
||||
event.asdu_result == IEC60870_ASDU_OK) {
|
||||
bool accepted = IEC60870_MasterConnection_Send_ACT_CON(
|
||||
&event.connection, &event.asdu, false);
|
||||
if (!accepted) {
|
||||
IEC104_TcpSubmitResult_t submit =
|
||||
IEC104_Slave_Get_Last_Submit_Result(&event.connection);
|
||||
/* 按下表处理submit;WAIT需要保留事件及原文,不能忽略或盲目重发。 */
|
||||
(void)submit;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 共用操作 | 行为 |
|
||||
|---|---|
|
||||
| Send_ASDU | 提交给定ASDU对象的当前原文,也可提交业务另外构造的数据ASDU |
|
||||
| Send_ACT_CON | 原地设置COT=7及指定P/N,然后提交 |
|
||||
| Send_ACT_TERM | 原地设置COT=10、P/N=0,然后提交 |
|
||||
| Get_Application_Layer_Parameters | 返回该连接的标准104只读参数,最大249字节 |
|
||||
|
||||
确认及终止先修改事件副本的字节再发送,失败也不回滚;TEST/OA/CA/VSQ和对象原文保留。需要原请求内容时,在修改前保存副本。
|
||||
|
||||
| 详细结果 | 布尔返回 | 业务处理 |
|
||||
|---|---|---|
|
||||
| OK | true | 已复制到发送缓冲或HIGH,可复用发送原文;不代表已写完或已ACK,不重复提交 |
|
||||
| WAIT | false | 本次未接手;保留已修改的ASDU及原事件,待容量恢复后重试 |
|
||||
| ERROR | false | 参数、布局、旧代次或调用阶段非法,不能按容量等待盲目重试 |
|
||||
| FATAL | false | 发送路径故障,可能已写出字节;结合RECORD/DISCONNECTED处理 |
|
||||
|
||||
事件含有指向自身的ASDU视图和连接上下文,不能用赋值或memcpy搬到另一地址。延迟任务可让NextEvent直接写入自身稳定存储,或调用`IEC104_Slave_Copy_Event(&task.event, &event)`;复制时同时保存字节、重建自引用,并保留原服务实例和原连接代次。源事件随后可被下一次NextEvent覆盖,任务副本仍独立可用;复制不代表需要重发已经OK的报文。多个任务的事件存储由应用提供,库不额外引入待办任务池。
|
||||
|
||||
总召内部使用紧凑存储:只保留`Get_Event_Identity`取得的原身份及10字节请求ASDU,按需建立视图,通过`Slave_Send_Response / Slave_Try_Send_Response`应答,关闭原连接用`Slave_Close_Connection_By_Identity`。任务槽、事件队列和发送容量保持不变,内存测量见[固定存储](IEC104_Interrogation_设计架构.md#2-对象身份与固定存储)。其他需要完整事件的业务仍可用Copy_Event。
|
||||
|
||||
NextEvent返回EMPTY/ERROR时保持输出不变。Close、重连、重新Init或Destroy后复用服务槽,都不会把旧任务改绑到新连接;发送继续检查原身份。Destroy成功后旧服务句柄失效,但旧事件的字节仍由任务自身保存。所有延迟任务也在所属事件循环串行提交。
|
||||
|
||||
多帧总召的数据和终止应经同一事件的connection依次提交,逐帧处理WAIT;把数据放NORMAL而终止放HIGH会造成优先级越序。门面不实现点表、总召状态机、遥控选择/执行或系统校时。
|
||||
|
||||
[子站示例](../../examples/iec104_slave_example.c)通过BEGIN/NEED_DATA/FINISHED事件与显式应答接入独立的[总召模块](../../104/business/IEC104_Interrogation.h)推进业务;Slave和TcpServer继续负责服务、连接及传输。总召接入顺序见[完整调用流程](IEC104_完整调用流程.md#flow-interrogation)。
|
||||
|
||||
调用`IEC104_Interrogation_Process(gi)`自动推进当前就绪任务,无需传入预算。每个实例至多一个未终结总召,包含等待生成BEGIN的请求;忙时相同和不同QOI的新激活均否定,不保存后续总召。4个任务槽共用于这个总召和待发短应答。Handle_Event在原会话有效、Slave通知已读空且无更早REPLY时尝试立即提交短应答,未接手再保存。满槽时尝试否定确认;不能安全提交/保存则仅丢新请求并累计dropped_requests,保留当前任务及TCP。Process在空闲、等待应用/事件空位、发送WAIT或Slave通知未读空时返回。对象、状态、调度及接入约定见[总召模块设计](IEC104_Interrogation_设计架构.md)。
|
||||
|
||||
总召Get_Status的`need_process`用于决定是否立即续跑,不能从`waiting`取反推断。应答或读取业务事件后重新查询;发送WAIT后等待网络推进再重试。每次Slave_Poll/Process及Slave通知处理结束仍须推进总召,即使need_process为false。外部事件循环同时合并Slave_Prepare的调度条件;新增状态字段后调用方须重新编译。
|
||||
|
||||
BEGIN由调用者判断是否接受,遍历状态可延至首个NEED_DATA建立;NEED_DATA由调用者选择本批IOA并读取当前值和品质,协议库不登记或冻结点表。Submit_Batch立即尝试不排队提交,WAIT不保存数据、不推进游标,应用在网络推进后重新取当前值重试;OK才固定帧顺序并推进批次。点值更新及突发发布须与总召读值及提交串行化,持续突发可能推迟总召,详见[数据批次与发送路径](IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。
|
||||
|
||||
BEGIN携带实际CA、station_id和QOI=20..36;应用检查数据源并决定接受,首个NEED_DATA才需建立取数游标。多目标广播先完成全部BEGIN/ACT_CON,再逐站取数,最后全部ACT_TERM;每站状态区保证FINISHED无损交付。Submit_Batch只接受事件目标CA。新增/替代站下轮参加,尚未选入BEGIN便删除的站跳过;已经选中尤其已确认的原站必须完成或明确终结。各站独立计时,总期限默认关闭;FINISHING的终结事件读完前仍忙拒绝新激活。详见[总召设计](IEC104_Interrogation_设计架构.md#33-广播地址与本站地址)。
|
||||
|
||||
主站COT=8按原连接、目标CA/OA/TEST/QOI匹配总召,包括已确认但尚未取数或等终止的站。定向撤销匹配站,其他站继续;广播撤销停止整轮,对各仍活动的站回肯定COT=9。FINISHED(DEACTIVATED)通知应用清理,匹配站旧批次失效。已被发送层接手的帧仍按顺序发送,撤销不补发成功ACT_TERM。
|
||||
|
||||
业务接受和每批取数期限默认各15秒,执行整体期限默认0(禁用),各项均可单独配置;这些是业务默认策略,不是标准规定的总召时限。关闭整体期限允许慢链路累计传输超过原60秒上限,底层单帧发送、确认和链路探测期限仍有效。BEGIN生成时启动已启用的业务期限,此前等待业务事件空位没有独立超时,但已占用总召资格,应及时读取FINISHED。总期限显式设为非零后仍按固定整体耗时计算,不因新批次或发送重试而延长。START/CONFIRM超时及BEGIN阶段Fail_Request复用原槽发送否定ACT_CON,保留TCP及其他短应答;肯定ACT_CON被发送层接手后,取数超时、总超时和数据源失败仍关闭原连接。终结事件保留具体原因,BEGIN未取出则只移除工作事件。接受BEGIN前检查数据源及必要业务资源,仍可逐批取值并编码。等待前查询timer_active/timeout_remaining_ms并持续驱动Process。此次失败处理修复不新增接口或配置开关;先前已移除queue_timeout_ms及多总召排队状态/统计,保留negative_submitted和dropped_requests,使用旧字段的调用方须调整并重新编译。
|
||||
|
||||
原`IEC104_SlaveConnection_Init`和手动接入对象已移除,调用方改用Slave服务及其事件接口并重新编译。TCP底层接口仍供独立通信接入和维护测试使用,业务的默认入口统一为Slave。
|
||||
|
||||
本次接口直接替换为独立对象与统一ASDU入口,删除旧通用监视/命令联合体及分类解析、Builder接口,不提供兼容包装;调用方须按上述示例迁移并重新编译。构建须新增`IEC60870_InformationObjects.c`。
|
||||
|
||||
时标保留IV/SU;IV=1可解码但不代表可校时。业务新上报应提供本地标准时间且SU=0,应答回显保留请求时标。验证命令和覆盖范围见[测试说明](../test/IEC104测试文档.md)。
|
||||
|
||||
### 时钟同步业务
|
||||
|
||||
[ClockSynchronization](../../104/business/IEC104_ClockSynchronization.h)处理Type 103、COT=6、IOA=0,按共用站点表路由;COT=5读时和COT=8撤销回45。独立静态池默认与TCP服务容量相同,每实例4个任务槽,与总召/计数量分别创建、分别取消。Handle_Event校验保存,Process处理期限及FIFO应答,只有应用显式Execute_Next会调用本次传入的同步action,不保存函数指针。
|
||||
|
||||
默认源标准时区480分钟、执行期限15000ms,0禁用期限;IV/SU及时区由公共时间工具处理,64位毫秒计算只补偿受理后的站内滞留。action收到的local_time已经是固定UTC+8,base_ms为执行检查时刻。肯定/否定确认保留原CP56字节,不回读时钟或提供自定义确认时标。
|
||||
|
||||
调用过action的确认走Try,错误校验应答走普通HIGH;WAIT只重试确认,STOP取消未交接任务但不回滚时钟。广播动作一次,快照目标分别确认;满槽丢新并计数,不断链。同内容的新请求仍执行,可能再次回拨。持续NORMAL发布可能推迟Try确认,应用通过包含回调执行期的confirm_pending串行协调新的带时标上报。默认示例只设模拟时钟,`--clock system`才调用Windows时间源。完整契约见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md)和[调用流程](IEC104_完整调用流程.md#flow-clock-synchronization)。
|
||||
|
||||
### 遥控、遥调与带时标命令业务
|
||||
|
||||
[Command](../../104/business/IEC104_Command.h)处理Type45–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`中,仅描述当时的源码版本,不属于当前可运行测试。本文不再提供已删除测试的构建命令,也不将其历史通过数计入当前结果。
|
||||
|
||||
+1164
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1617
File diff suppressed because it is too large
Load Diff
+340
@@ -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-编译与运行)。
|
||||
+2891
File diff suppressed because it is too large
Load Diff
+94
@@ -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)。
|
||||
+139
@@ -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;这不是模块承诺的动作时效上限。
|
||||
+218
@@ -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接入路径,同一会话不能重复喂给旧全局接收器。
|
||||
+48
@@ -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)。
|
||||
+458
@@ -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、错误地址、原事件不变、广播/本站交叉撤销和确认前失败,避免内部重构改变协议行为。
|
||||
|
||||
测试验证本模块的功能和边界,不代表已通过正式协议一致性认证。现场仍须确定主站对否定确认的处理、各分组点表、采样一致性及业务期限取值。
|
||||
+1037
File diff suppressed because it is too large
Load Diff
+550
@@ -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)
|
||||
+728
@@ -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内部驱动,没有后台自动同步。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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, ¶meters, buffer, sizeof(buffer), &header, 0);
|
||||
}
|
||||
if (result == IEC60870_ASDU_OK) {
|
||||
result = IEC60870_Asdu_Add_InformationObject(&asdu, &point.object);
|
||||
}
|
||||
if (result == IEC60870_ASDU_OK) {
|
||||
result = IEC60870_Asdu_Get_Data(&asdu, &data, &length);
|
||||
}
|
||||
/* OK时可将data/length交给Enqueue_Event;提交返回WAIT时保留原文重试。 */
|
||||
```
|
||||
|
||||
监视和命令统一使用`IEC60870_Asdu_Parse`校验整条报文,再通过`IEC60870_Asdu_Get_Element`取得独立类型的对象。例如处理单命令:
|
||||
|
||||
```c
|
||||
/* parameters、data、length对应收到的ASDU;处理期间原文保持有效。 */
|
||||
IEC60870_AsduInfo_t info;
|
||||
IEC60870_AsduResult_t status =
|
||||
IEC60870_Asdu_Parse(¶meters, data, length, &info);
|
||||
if (status == IEC60870_ASDU_OK && info.type_id == IEC60870_ASDU_C_SC_NA_1) {
|
||||
IEC60870_SingleCommand_t command;
|
||||
status = IEC60870_Asdu_Get_Element(&info, 0, &command, sizeof(command));
|
||||
if (status == IEC60870_ASDU_OK) {
|
||||
/* info保存COT/CA/OA;command.object.ioa、state、is_select、
|
||||
* qualifier供业务判断地址、限定词、选择/执行及权限。 */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Get_Element输出无需预先Create,必须按`info.type_id`选择匹配的具体结构体并传入其尺寸;尺寸检查不能替代C类型匹配。解析视图借用原文和参数,取出的具体对象按值保存。只处理单个载荷时,可直接调用对应类型的`Get_From_Buffer`,传入已解析IOA及不含IOA的精确载荷;读命令载荷为0字节,允许NULL。
|
||||
|
||||
归一化值使用原始`int16_t`(实际值为原值/32768),标度值使用`int16_t`,短浮点保留IEEE 754 binary32位模式。累计量用`IEC60870_BinaryCounterReading_t`保存32位计数、5位序号及CY/CA/IV。对象层校验品质保留位、SCO、DCO/RCO状态、VTI有符号范围、保护事件QDP和CP24/CP56;COT、CA、IOA及限定词的业务适用性由应用判断。
|
||||
|
||||
`Build_Response(¶meters, request, length, cause, is_negative, out, capacity, &out_length)`仍以`IEC60870_Asdu_`为前缀。它复制请求,只替换COT/P/N,保留TEST、存在时的OA、CA、VSQ及全部对象原文,支持原地和部分重叠。它仅作公共头及最低长度检查,可回显未知类型。生成后携带原请求generation调用Send_Response,并处理WAIT/ERROR/FATAL。
|
||||
|
||||
`IEC60870_Asdu_Create_From_Buffer(&asdu, ¶meters, buffer, length)`直接借用可写原文,建立可修改的ASDU对象。它只做公共头及最低长度检查,未知类型也能建立对象,用于COT=44等错误应答;执行业务前仍须使用完整Parse和业务规则校验。借用对象不能Add,`Set_COT`、`Set_Negative`、`Set_CA`原地修改相应字段;Get_Data可以导出原文。Create创建的构造容器也可使用这三个设置接口,空构造容器仍不能导出。Set_CA按COT宽度定位、CA宽度写入,1字节仅允许0..255,2字节允许0..65535;非法容器或地址溢出返回ERROR且不修改原文,业务地址限制仍由业务层判断。ASDU结构新增来源标志,调用方须一起重新编译。
|
||||
|
||||
### 文件段和新增时间格式
|
||||
|
||||
`IEC60870_FileSegment_t`含255字节的数据数组。Create和Get_From_Buffer把有效数据复制进对象,调用结束后即可复用输入;对象无需销毁。它的线上载荷为`NOF(2)+NOS(1)+LOS(1)+数据(LOS)`,解析精确检查LOS与报文长度,不接受截短和多余尾字节。
|
||||
|
||||
先用`IEC60870_FileSegment_Get_Max_Data_Size(parameters)`计算当前配置允许的数据长度。默认104配置返回236;最短101头、1字节IOA、254字节上限时返回245。返回0表示只能容纳空段,-1表示配置非法或连空段都装不下。Create允许0..255字节,实际加入ASDU时仍检查总长;超过容量返回FULL,调用方负责分段。
|
||||
|
||||
`IEC60870_Time_To_CP16/From_CP16`处理0..65535毫秒的经过时间。`IEC60870_Time_To_CP24/From_CP24`处理分钟0..59、分钟内毫秒0..59999和IV;bit6是保留位,CP24没有SU、小时或日期。CP24编解码保留IV,不自动推断完整日历。所有接口均沿用成功1、失败0且保留输出的约定。
|
||||
|
||||
### 子站统一入口
|
||||
|
||||
业务包含`IEC104_Slave.h`,持有一个不透明的`IEC104_Slave_t *`。Slave内部创建、驱动和销毁TcpServer;配置、调度、状态和结果仍复用`IEC104_TcpTypes.h`中的值类型,业务头文件不引入TcpServer操作接口或内部布局。
|
||||
|
||||
生命周期为`IEC104_Slave_Create → Config_Default/Init → Prepare/Process或Poll → Close → NextEvent读空 → Destroy`。所有操作在同一事件循环线程串行执行。静态池容量沿用`IEC104_TCP_SERVER_CAPACITY`,与直接使用TcpServer共享底层名额,无动态分配。创建失败返回NULL;其他生命周期接口显式比较`IEC104_TCP_OK`。
|
||||
|
||||
`IEC104_Slave_NextEvent(slave, &event)`按原FIFO交付`IEC104_SlaveEvent_t`:`event.transport`是TCP事件副本;I_FRAME且`event.asdu_result == IEC60870_ASDU_OK`时,`event.asdu`已经借用事件自身的ASDU字节,`event.connection`已经绑定原服务实例及原连接代次。业务不再传入server/generation,也不再手工Create_From_Buffer。字段/地址/命令权限仍由业务校验,未知类型可以原文交付,视图创建成功不等于允许执行命令。
|
||||
|
||||
普通循环使用以下顺序;Application_Handle_Event为应用自己的同步处理函数,其中可以提交应答或复制事件到延迟任务:
|
||||
|
||||
```c
|
||||
IEC104_TcpResult_t result = IEC104_Slave_Poll(slave, 10, NULL);
|
||||
if (result == IEC104_TCP_OK) {
|
||||
IEC104_SlaveEvent_t event;
|
||||
IEC104_TcpEventResult_t next;
|
||||
while ((next = IEC104_Slave_NextEvent(slave, &event)) == IEC104_TCP_EVENT_READY) {
|
||||
Application_Handle_Event(slave, &event);
|
||||
}
|
||||
/* next为ERROR时处理内部读取错误;EMPTY表示本轮事件已读空。 */
|
||||
}
|
||||
```
|
||||
|
||||
外部事件循环使用Slave_Prepare/Process,继续保留原service_token和socket token;pending_event_count或need_process非零时先处理本地工作。主动上报通过`IEC104_Slave_Get_Application_Layer_Parameters`取得参数构造ASDU,再调用`IEC104_Slave_Enqueue_Event(slave, &asdu)`进入NORMAL。关闭单连接传原事件给`IEC104_Slave_Close_Connection(slave, &event, error)`,库内部使用该事件的原代次。
|
||||
|
||||
### 共用连接上的激活确认和激活终止
|
||||
|
||||
共用接口`IEC60870_MasterConnection_t`继续供101/104业务复用;当前104的接口由Slave_NextEvent自动安装,101串口后端仍未提供。下例放在业务已验证请求并决定肯定确认的分支,event为地址稳定的Slave事件:
|
||||
|
||||
```c
|
||||
if (event.transport.type == IEC104_TCP_EVENT_I_FRAME &&
|
||||
event.asdu_result == IEC60870_ASDU_OK) {
|
||||
bool accepted = IEC60870_MasterConnection_Send_ACT_CON(
|
||||
&event.connection, &event.asdu, false);
|
||||
if (!accepted) {
|
||||
IEC104_TcpSubmitResult_t submit =
|
||||
IEC104_Slave_Get_Last_Submit_Result(&event.connection);
|
||||
/* 按下表处理submit;WAIT需要保留事件及原文,不能忽略或盲目重发。 */
|
||||
(void)submit;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 共用操作 | 行为 |
|
||||
|---|---|
|
||||
| Send_ASDU | 提交给定ASDU对象的当前原文,也可提交业务另外构造的数据ASDU |
|
||||
| Send_ACT_CON | 原地设置COT=7及指定P/N,然后提交 |
|
||||
| Send_ACT_TERM | 原地设置COT=10、P/N=0,然后提交 |
|
||||
| Get_Application_Layer_Parameters | 返回该连接的标准104只读参数,最大249字节 |
|
||||
|
||||
确认及终止先修改事件副本的字节再发送,失败也不回滚;TEST/OA/CA/VSQ和对象原文保留。需要原请求内容时,在修改前保存副本。
|
||||
|
||||
| 详细结果 | 布尔返回 | 业务处理 |
|
||||
|---|---|---|
|
||||
| OK | true | 已复制到发送缓冲或HIGH,可复用发送原文;不代表已写完或已ACK,不重复提交 |
|
||||
| WAIT | false | 本次未接手;保留已修改的ASDU及原事件,待容量恢复后重试 |
|
||||
| ERROR | false | 参数、布局、旧代次或调用阶段非法,不能按容量等待盲目重试 |
|
||||
| FATAL | false | 发送路径故障,可能已写出字节;结合RECORD/DISCONNECTED处理 |
|
||||
|
||||
事件含有指向自身的ASDU视图和连接上下文,不能用赋值或memcpy搬到另一地址。延迟任务可让NextEvent直接写入自身稳定存储,或调用`IEC104_Slave_Copy_Event(&task.event, &event)`;复制时同时保存字节、重建自引用,并保留原服务实例和原连接代次。源事件随后可被下一次NextEvent覆盖,任务副本仍独立可用;复制不代表需要重发已经OK的报文。多个任务的事件存储由应用提供,库不额外引入待办任务池。
|
||||
|
||||
总召内部使用紧凑存储:只保留`Get_Event_Identity`取得的原身份及10字节请求ASDU,按需建立视图,通过`Slave_Send_Response / Slave_Try_Send_Response`应答,关闭原连接用`Slave_Close_Connection_By_Identity`。任务槽、事件队列和发送容量保持不变,内存测量见[固定存储](IEC104_Interrogation_设计架构.md#2-对象身份与固定存储)。其他需要完整事件的业务仍可用Copy_Event。
|
||||
|
||||
NextEvent返回EMPTY/ERROR时保持输出不变。Close、重连、重新Init或Destroy后复用服务槽,都不会把旧任务改绑到新连接;发送继续检查原身份。Destroy成功后旧服务句柄失效,但旧事件的字节仍由任务自身保存。所有延迟任务也在所属事件循环串行提交。
|
||||
|
||||
多帧总召的数据和终止应经同一事件的connection依次提交,逐帧处理WAIT;把数据放NORMAL而终止放HIGH会造成优先级越序。门面不实现点表、总召状态机、遥控选择/执行或系统校时。
|
||||
|
||||
[子站示例](../../examples/iec104_slave_example.c)通过BEGIN/NEED_DATA/FINISHED事件与显式应答接入独立的[总召模块](../../104/business/IEC104_Interrogation.h)推进业务;Slave和TcpServer继续负责服务、连接及传输。总召接入顺序见[完整调用流程](IEC104_完整调用流程.md#flow-interrogation)。
|
||||
|
||||
调用`IEC104_Interrogation_Process(gi)`自动推进当前就绪任务,无需传入预算。每个实例至多一个未终结总召,包含等待生成BEGIN的请求;忙时相同和不同QOI的新激活均否定,不保存后续总召。4个任务槽共用于这个总召和待发短应答。Handle_Event在原会话有效、Slave通知已读空且无更早REPLY时尝试立即提交短应答,未接手再保存。满槽时尝试否定确认;不能安全提交/保存则仅丢新请求并累计dropped_requests,保留当前任务及TCP。Process在空闲、等待应用/事件空位、发送WAIT或Slave通知未读空时返回。对象、状态、调度及接入约定见[总召模块设计](IEC104_Interrogation_设计架构.md)。
|
||||
|
||||
总召Get_Status的`need_process`用于决定是否立即续跑,不能从`waiting`取反推断。应答或读取业务事件后重新查询;发送WAIT后等待网络推进再重试。每次Slave_Poll/Process及Slave通知处理结束仍须推进总召,即使need_process为false。外部事件循环同时合并Slave_Prepare的调度条件;新增状态字段后调用方须重新编译。
|
||||
|
||||
BEGIN由调用者判断是否接受,遍历状态可延至首个NEED_DATA建立;NEED_DATA由调用者选择本批IOA并读取当前值和品质,协议库不登记或冻结点表。Submit_Batch立即尝试不排队提交,WAIT不保存数据、不推进游标,应用在网络推进后重新取当前值重试;OK才固定帧顺序并推进批次。点值更新及突发发布须与总召读值及提交串行化,持续突发可能推迟总召,详见[数据批次与发送路径](IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。
|
||||
|
||||
BEGIN携带实际CA、station_id和QOI=20..36;应用检查数据源并决定接受,首个NEED_DATA才需建立取数游标。多目标广播先完成全部BEGIN/ACT_CON,再逐站取数,最后全部ACT_TERM;每站状态区保证FINISHED无损交付。Submit_Batch只接受事件目标CA。新增/替代站下轮参加,尚未选入BEGIN便删除的站跳过;已经选中尤其已确认的原站必须完成或明确终结。各站独立计时,总期限默认关闭;FINISHING的终结事件读完前仍忙拒绝新激活。详见[总召设计](IEC104_Interrogation_设计架构.md#33-广播地址与本站地址)。
|
||||
|
||||
主站COT=8按原连接、目标CA/OA/TEST/QOI匹配总召,包括已确认但尚未取数或等终止的站。定向撤销匹配站,其他站继续;广播撤销停止整轮,对各仍活动的站回肯定COT=9。FINISHED(DEACTIVATED)通知应用清理,匹配站旧批次失效。已被发送层接手的帧仍按顺序发送,撤销不补发成功ACT_TERM。
|
||||
|
||||
业务接受和每批取数期限默认各15秒,执行整体期限默认0(禁用),各项均可单独配置;这些是业务默认策略,不是标准规定的总召时限。关闭整体期限允许慢链路累计传输超过原60秒上限,底层单帧发送、确认和链路探测期限仍有效。BEGIN生成时启动已启用的业务期限,此前等待业务事件空位没有独立超时,但已占用总召资格,应及时读取FINISHED。总期限显式设为非零后仍按固定整体耗时计算,不因新批次或发送重试而延长。START/CONFIRM超时及BEGIN阶段Fail_Request复用原槽发送否定ACT_CON,保留TCP及其他短应答;肯定ACT_CON被发送层接手后,取数超时、总超时和数据源失败仍关闭原连接。终结事件保留具体原因,BEGIN未取出则只移除工作事件。接受BEGIN前检查数据源及必要业务资源,仍可逐批取值并编码。等待前查询timer_active/timeout_remaining_ms并持续驱动Process。此次失败处理修复不新增接口或配置开关;先前已移除queue_timeout_ms及多总召排队状态/统计,保留negative_submitted和dropped_requests,使用旧字段的调用方须调整并重新编译。
|
||||
|
||||
原`IEC104_SlaveConnection_Init`和手动接入对象已移除,调用方改用Slave服务及其事件接口并重新编译。TCP底层接口仍供独立通信接入和维护测试使用,业务的默认入口统一为Slave。
|
||||
|
||||
本次接口直接替换为独立对象与统一ASDU入口,删除旧通用监视/命令联合体及分类解析、Builder接口,不提供兼容包装;调用方须按上述示例迁移并重新编译。构建须新增`IEC60870_InformationObjects.c`。
|
||||
|
||||
时标保留IV/SU;IV=1可解码但不代表可校时。业务新上报应提供本地标准时间且SU=0,应答回显保留请求时标。验证命令和覆盖范围见[测试说明](../test/IEC104测试文档.md)。
|
||||
|
||||
### 时钟同步业务
|
||||
|
||||
[ClockSynchronization](../../104/business/IEC104_ClockSynchronization.h)处理Type 103、COT=6、IOA=0,按共用站点表路由;COT=5读时和COT=8撤销回45。独立静态池默认与TCP服务容量相同,每实例4个任务槽,与总召/计数量分别创建、分别取消。Handle_Event校验保存,Process处理期限及FIFO应答,只有应用显式Execute_Next会调用本次传入的同步action,不保存函数指针。
|
||||
|
||||
默认源标准时区480分钟、执行期限15000ms,0禁用期限;IV/SU及时区由公共时间工具处理,64位毫秒计算只补偿受理后的站内滞留。action收到的local_time已经是固定UTC+8,base_ms为执行检查时刻。肯定/否定确认保留原CP56字节,不回读时钟或提供自定义确认时标。
|
||||
|
||||
调用过action的确认走Try,错误校验应答走普通HIGH;WAIT只重试确认,STOP取消未交接任务但不回滚时钟。广播动作一次,快照目标分别确认;满槽丢新并计数,不断链。同内容的新请求仍执行,可能再次回拨。持续NORMAL发布可能推迟Try确认,应用通过包含回调执行期的confirm_pending串行协调新的带时标上报。默认示例只设模拟时钟,`--clock system`才调用Windows时间源。完整契约见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md)和[调用流程](IEC104_完整调用流程.md#flow-clock-synchronization)。
|
||||
|
||||
### 遥控、遥调与带时标命令业务
|
||||
|
||||
[Command](../../104/business/IEC104_Command.h)处理Type45–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 回环测试 |
|
||||
+301
@@ -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(×tamp, 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(×tamp,
|
||||
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(×tamp, 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, ×tamp)) {
|
||||
return 0;
|
||||
}
|
||||
/* 源标准时区与本地均为UTC+8,只需按SU归一化,输出即为本地标准时间。 */
|
||||
return IEC60870_Time_CP56_To_Local(×tamp,
|
||||
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以供业务记录,但转成有效时间戳时会拒绝。已有接收、链路和发送模块的接口及行为不变,时间工具由需要处理时间戳的业务按需接入。
|
||||
+188
@@ -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`中,仅描述当时的源码版本,不属于当前可运行测试。本文不再提供已删除测试的构建命令,也不将其历史通过数计入当前结果。
|
||||
|
||||
+1163
File diff suppressed because it is too large
Load Diff
+2438
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
+190
@@ -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`中,仅描述当时的源码版本,不属于当前可运行测试。本文不再提供已删除测试的构建命令,也不将其历史通过数计入当前结果。
|
||||
|
||||
+1209
File diff suppressed because it is too large
Load Diff
+190
@@ -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回归覆盖。
|
||||
|
||||
这些检查覆盖本示例的模拟点表流程;实际主站设备、现场点表和设备执行接口仍需联调。
|
||||
+347
@@ -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`中,仅描述当时的源码版本,不属于当前可运行测试。本文不再提供已删除测试的构建命令,也不将其历史通过数计入当前结果。
|
||||
|
||||
+1186
File diff suppressed because it is too large
Load Diff
@@ -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 */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
+430
@@ -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; });
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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-编译与运行)。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;这不是模块承诺的动作时效上限。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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, ¶meters, buffer, sizeof(buffer), &header, 0);
|
||||
}
|
||||
if (result == IEC60870_ASDU_OK) {
|
||||
result = IEC60870_Asdu_Add_InformationObject(&asdu, &point.object);
|
||||
}
|
||||
if (result == IEC60870_ASDU_OK) {
|
||||
result = IEC60870_Asdu_Get_Data(&asdu, &data, &length);
|
||||
}
|
||||
/* OK时可将data/length交给Enqueue_Event;提交返回WAIT时保留原文重试。 */
|
||||
```
|
||||
|
||||
监视和命令统一使用`IEC60870_Asdu_Parse`校验整条报文,再通过`IEC60870_Asdu_Get_Element`取得独立类型的对象。例如处理单命令:
|
||||
|
||||
```c
|
||||
/* parameters、data、length对应收到的ASDU;处理期间原文保持有效。 */
|
||||
IEC60870_AsduInfo_t info;
|
||||
IEC60870_AsduResult_t status =
|
||||
IEC60870_Asdu_Parse(¶meters, data, length, &info);
|
||||
if (status == IEC60870_ASDU_OK && info.type_id == IEC60870_ASDU_C_SC_NA_1) {
|
||||
IEC60870_SingleCommand_t command;
|
||||
status = IEC60870_Asdu_Get_Element(&info, 0, &command, sizeof(command));
|
||||
if (status == IEC60870_ASDU_OK) {
|
||||
/* info保存COT/CA/OA;command.object.ioa、state、is_select、
|
||||
* qualifier供业务判断地址、限定词、选择/执行及权限。 */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Get_Element输出无需预先Create,必须按`info.type_id`选择匹配的具体结构体并传入其尺寸;尺寸检查不能替代C类型匹配。解析视图借用原文和参数,取出的具体对象按值保存。只处理单个载荷时,可直接调用对应类型的`Get_From_Buffer`,传入已解析IOA及不含IOA的精确载荷;读命令载荷为0字节,允许NULL。
|
||||
|
||||
归一化值使用原始`int16_t`(实际值为原值/32768),标度值使用`int16_t`,短浮点保留IEEE 754 binary32位模式。累计量用`IEC60870_BinaryCounterReading_t`保存32位计数、5位序号及CY/CA/IV。对象层校验品质保留位、SCO、DCO/RCO状态、VTI有符号范围、保护事件QDP和CP24/CP56;COT、CA、IOA及限定词的业务适用性由应用判断。
|
||||
|
||||
`Build_Response(¶meters, request, length, cause, is_negative, out, capacity, &out_length)`仍以`IEC60870_Asdu_`为前缀。它复制请求,只替换COT/P/N,保留TEST、存在时的OA、CA、VSQ及全部对象原文,支持原地和部分重叠。它仅作公共头及最低长度检查,可回显未知类型。生成后携带原请求generation调用Send_Response,并处理WAIT/ERROR/FATAL。
|
||||
|
||||
`IEC60870_Asdu_Create_From_Buffer(&asdu, ¶meters, buffer, length)`直接借用可写原文,建立可修改的ASDU对象。它只做公共头及最低长度检查,未知类型也能建立对象,用于COT=44等错误应答;执行业务前仍须使用完整Parse和业务规则校验。借用对象不能Add,`Set_COT`、`Set_Negative`、`Set_CA`原地修改相应字段;Get_Data可以导出原文。Create创建的构造容器也可使用这三个设置接口,空构造容器仍不能导出。Set_CA按COT宽度定位、CA宽度写入,1字节仅允许0..255,2字节允许0..65535;非法容器或地址溢出返回ERROR且不修改原文,业务地址限制仍由业务层判断。ASDU结构新增来源标志,调用方须一起重新编译。
|
||||
|
||||
### 文件段和新增时间格式
|
||||
|
||||
`IEC60870_FileSegment_t`含255字节的数据数组。Create和Get_From_Buffer把有效数据复制进对象,调用结束后即可复用输入;对象无需销毁。它的线上载荷为`NOF(2)+NOS(1)+LOS(1)+数据(LOS)`,解析精确检查LOS与报文长度,不接受截短和多余尾字节。
|
||||
|
||||
先用`IEC60870_FileSegment_Get_Max_Data_Size(parameters)`计算当前配置允许的数据长度。默认104配置返回236;最短101头、1字节IOA、254字节上限时返回245。返回0表示只能容纳空段,-1表示配置非法或连空段都装不下。Create允许0..255字节,实际加入ASDU时仍检查总长;超过容量返回FULL,调用方负责分段。
|
||||
|
||||
`IEC60870_Time_To_CP16/From_CP16`处理0..65535毫秒的经过时间。`IEC60870_Time_To_CP24/From_CP24`处理分钟0..59、分钟内毫秒0..59999和IV;bit6是保留位,CP24没有SU、小时或日期。CP24编解码保留IV,不自动推断完整日历。所有接口均沿用成功1、失败0且保留输出的约定。
|
||||
|
||||
### 子站统一入口
|
||||
|
||||
业务包含`IEC104_Slave.h`,持有一个不透明的`IEC104_Slave_t *`。Slave内部创建、驱动和销毁TcpServer;配置、调度、状态和结果仍复用`IEC104_TcpTypes.h`中的值类型,业务头文件不引入TcpServer操作接口或内部布局。
|
||||
|
||||
生命周期为`IEC104_Slave_Create → Config_Default/Init → Prepare/Process或Poll → Close → NextEvent读空 → Destroy`。所有操作在同一事件循环线程串行执行。静态池容量沿用`IEC104_TCP_SERVER_CAPACITY`,与直接使用TcpServer共享底层名额,无动态分配。创建失败返回NULL;其他生命周期接口显式比较`IEC104_TCP_OK`。
|
||||
|
||||
`IEC104_Slave_NextEvent(slave, &event)`按原FIFO交付`IEC104_SlaveEvent_t`:`event.transport`是TCP事件副本;I_FRAME且`event.asdu_result == IEC60870_ASDU_OK`时,`event.asdu`已经借用事件自身的ASDU字节,`event.connection`已经绑定原服务实例及原连接代次。业务不再传入server/generation,也不再手工Create_From_Buffer。字段/地址/命令权限仍由业务校验,未知类型可以原文交付,视图创建成功不等于允许执行命令。
|
||||
|
||||
普通循环使用以下顺序;Application_Handle_Event为应用自己的同步处理函数,其中可以提交应答或复制事件到延迟任务:
|
||||
|
||||
```c
|
||||
IEC104_TcpResult_t result = IEC104_Slave_Poll(slave, 10, NULL);
|
||||
if (result == IEC104_TCP_OK) {
|
||||
IEC104_SlaveEvent_t event;
|
||||
IEC104_TcpEventResult_t next;
|
||||
while ((next = IEC104_Slave_NextEvent(slave, &event)) == IEC104_TCP_EVENT_READY) {
|
||||
Application_Handle_Event(slave, &event);
|
||||
}
|
||||
/* next为ERROR时处理内部读取错误;EMPTY表示本轮事件已读空。 */
|
||||
}
|
||||
```
|
||||
|
||||
外部事件循环使用Slave_Prepare/Process,继续保留原service_token和socket token;pending_event_count或need_process非零时先处理本地工作。主动上报通过`IEC104_Slave_Get_Application_Layer_Parameters`取得参数构造ASDU,再调用`IEC104_Slave_Enqueue_Event(slave, &asdu)`进入NORMAL。关闭单连接传原事件给`IEC104_Slave_Close_Connection(slave, &event, error)`,库内部使用该事件的原代次。
|
||||
|
||||
### 共用连接上的激活确认和激活终止
|
||||
|
||||
共用接口`IEC60870_MasterConnection_t`继续供101/104业务复用;当前104的接口由Slave_NextEvent自动安装,101串口后端仍未提供。下例放在业务已验证请求并决定肯定确认的分支,event为地址稳定的Slave事件:
|
||||
|
||||
```c
|
||||
if (event.transport.type == IEC104_TCP_EVENT_I_FRAME &&
|
||||
event.asdu_result == IEC60870_ASDU_OK) {
|
||||
bool accepted = IEC60870_MasterConnection_Send_ACT_CON(
|
||||
&event.connection, &event.asdu, false);
|
||||
if (!accepted) {
|
||||
IEC104_TcpSubmitResult_t submit =
|
||||
IEC104_Slave_Get_Last_Submit_Result(&event.connection);
|
||||
/* 按下表处理submit;WAIT需要保留事件及原文,不能忽略或盲目重发。 */
|
||||
(void)submit;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 共用操作 | 行为 |
|
||||
|---|---|
|
||||
| Send_ASDU | 提交给定ASDU对象的当前原文,也可提交业务另外构造的数据ASDU |
|
||||
| Send_ACT_CON | 原地设置COT=7及指定P/N,然后提交 |
|
||||
| Send_ACT_TERM | 原地设置COT=10、P/N=0,然后提交 |
|
||||
| Get_Application_Layer_Parameters | 返回该连接的标准104只读参数,最大249字节 |
|
||||
|
||||
确认及终止先修改事件副本的字节再发送,失败也不回滚;TEST/OA/CA/VSQ和对象原文保留。需要原请求内容时,在修改前保存副本。
|
||||
|
||||
| 详细结果 | 布尔返回 | 业务处理 |
|
||||
|---|---|---|
|
||||
| OK | true | 已复制到发送缓冲或HIGH,可复用发送原文;不代表已写完或已ACK,不重复提交 |
|
||||
| WAIT | false | 本次未接手;保留已修改的ASDU及原事件,待容量恢复后重试 |
|
||||
| ERROR | false | 参数、布局、旧代次或调用阶段非法,不能按容量等待盲目重试 |
|
||||
| FATAL | false | 发送路径故障,可能已写出字节;结合RECORD/DISCONNECTED处理 |
|
||||
|
||||
事件含有指向自身的ASDU视图和连接上下文,不能用赋值或memcpy搬到另一地址。延迟任务可让NextEvent直接写入自身稳定存储,或调用`IEC104_Slave_Copy_Event(&task.event, &event)`;复制时同时保存字节、重建自引用,并保留原服务实例和原连接代次。源事件随后可被下一次NextEvent覆盖,任务副本仍独立可用;复制不代表需要重发已经OK的报文。多个任务的事件存储由应用提供,库不额外引入待办任务池。
|
||||
|
||||
总召内部使用紧凑存储:只保留`Get_Event_Identity`取得的原身份及10字节请求ASDU,按需建立视图,通过`Slave_Send_Response / Slave_Try_Send_Response`应答,关闭原连接用`Slave_Close_Connection_By_Identity`。任务槽、事件队列和发送容量保持不变,内存测量见[固定存储](IEC104_Interrogation_设计架构.md#2-对象身份与固定存储)。其他需要完整事件的业务仍可用Copy_Event。
|
||||
|
||||
NextEvent返回EMPTY/ERROR时保持输出不变。Close、重连、重新Init或Destroy后复用服务槽,都不会把旧任务改绑到新连接;发送继续检查原身份。Destroy成功后旧服务句柄失效,但旧事件的字节仍由任务自身保存。所有延迟任务也在所属事件循环串行提交。
|
||||
|
||||
多帧总召的数据和终止应经同一事件的connection依次提交,逐帧处理WAIT;把数据放NORMAL而终止放HIGH会造成优先级越序。门面不实现点表、总召状态机、遥控选择/执行或系统校时。
|
||||
|
||||
[子站示例](../../examples/iec104_slave_example.c)通过BEGIN/NEED_DATA/FINISHED事件与显式应答接入独立的[总召模块](../../104/business/IEC104_Interrogation.h)推进业务;Slave和TcpServer继续负责服务、连接及传输。总召接入顺序见[完整调用流程](IEC104_完整调用流程.md#flow-interrogation)。
|
||||
|
||||
调用`IEC104_Interrogation_Process(gi)`自动推进当前就绪任务,无需传入预算。每个实例至多一个未终结总召,包含等待生成BEGIN的请求;忙时相同和不同QOI的新激活均否定,不保存后续总召。4个任务槽共用于这个总召和待发短应答。Handle_Event在原会话有效、Slave通知已读空且无更早REPLY时尝试立即提交短应答,未接手再保存。满槽时尝试否定确认;不能安全提交/保存则仅丢新请求并累计dropped_requests,保留当前任务及TCP。Process在空闲、等待应用/事件空位、发送WAIT或Slave通知未读空时返回。对象、状态、调度及接入约定见[总召模块设计](IEC104_Interrogation_设计架构.md)。
|
||||
|
||||
总召Get_Status的`need_process`用于决定是否立即续跑,不能从`waiting`取反推断。应答或读取业务事件后重新查询;发送WAIT后等待网络推进再重试。每次Slave_Poll/Process及Slave通知处理结束仍须推进总召,即使need_process为false。外部事件循环同时合并Slave_Prepare的调度条件;新增状态字段后调用方须重新编译。
|
||||
|
||||
BEGIN由调用者判断是否接受,遍历状态可延至首个NEED_DATA建立;NEED_DATA由调用者选择本批IOA并读取当前值和品质,协议库不登记或冻结点表。Submit_Batch立即尝试不排队提交,WAIT不保存数据、不推进游标,应用在网络推进后重新取当前值重试;OK才固定帧顺序并推进批次。点值更新及突发发布须与总召读值及提交串行化,持续突发可能推迟总召,详见[数据批次与发送路径](IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。
|
||||
|
||||
BEGIN携带实际CA、station_id和QOI=20..36;应用检查数据源并决定接受,首个NEED_DATA才需建立取数游标。多目标广播先完成全部BEGIN/ACT_CON,再逐站取数,最后全部ACT_TERM;每站状态区保证FINISHED无损交付。Submit_Batch只接受事件目标CA。新增/替代站下轮参加,尚未选入BEGIN便删除的站跳过;已经选中尤其已确认的原站必须完成或明确终结。各站独立计时,总期限默认关闭;FINISHING的终结事件读完前仍忙拒绝新激活。详见[总召设计](IEC104_Interrogation_设计架构.md#33-广播地址与本站地址)。
|
||||
|
||||
主站COT=8按原连接、目标CA/OA/TEST/QOI匹配总召,包括已确认但尚未取数或等终止的站。定向撤销匹配站,其他站继续;广播撤销停止整轮,对各仍活动的站回肯定COT=9。FINISHED(DEACTIVATED)通知应用清理,匹配站旧批次失效。已被发送层接手的帧仍按顺序发送,撤销不补发成功ACT_TERM。
|
||||
|
||||
业务接受和每批取数期限默认各15秒,执行整体期限默认0(禁用),各项均可单独配置;这些是业务默认策略,不是标准规定的总召时限。关闭整体期限允许慢链路累计传输超过原60秒上限,底层单帧发送、确认和链路探测期限仍有效。BEGIN生成时启动已启用的业务期限,此前等待业务事件空位没有独立超时,但已占用总召资格,应及时读取FINISHED。总期限显式设为非零后仍按固定整体耗时计算,不因新批次或发送重试而延长。START/CONFIRM超时及BEGIN阶段Fail_Request复用原槽发送否定ACT_CON,保留TCP及其他短应答;肯定ACT_CON被发送层接手后,取数超时、总超时和数据源失败仍关闭原连接。终结事件保留具体原因,BEGIN未取出则只移除工作事件。接受BEGIN前检查数据源及必要业务资源,仍可逐批取值并编码。等待前查询timer_active/timeout_remaining_ms并持续驱动Process。此次失败处理修复不新增接口或配置开关;先前已移除queue_timeout_ms及多总召排队状态/统计,保留negative_submitted和dropped_requests,使用旧字段的调用方须调整并重新编译。
|
||||
|
||||
原`IEC104_SlaveConnection_Init`和手动接入对象已移除,调用方改用Slave服务及其事件接口并重新编译。TCP底层接口仍供独立通信接入和维护测试使用,业务的默认入口统一为Slave。
|
||||
|
||||
本次接口直接替换为独立对象与统一ASDU入口,删除旧通用监视/命令联合体及分类解析、Builder接口,不提供兼容包装;调用方须按上述示例迁移并重新编译。构建须新增`IEC60870_InformationObjects.c`。
|
||||
|
||||
时标保留IV/SU;IV=1可解码但不代表可校时。业务新上报应提供本地标准时间且SU=0,应答回显保留请求时标。验证命令和覆盖范围见[测试说明](../test/IEC104测试文档.md)。
|
||||
|
||||
### 时钟同步业务
|
||||
|
||||
[ClockSynchronization](../../104/business/IEC104_ClockSynchronization.h)处理Type 103、COT=6、IOA=0,按共用站点表路由;COT=5读时和COT=8撤销回45。独立静态池默认与TCP服务容量相同,每实例4个任务槽,与总召/计数量分别创建、分别取消。Handle_Event校验保存,Process处理期限及FIFO应答,只有应用显式Execute_Next会调用本次传入的同步action,不保存函数指针。
|
||||
|
||||
默认源标准时区480分钟、执行期限15000ms,0禁用期限;IV/SU及时区由公共时间工具处理,64位毫秒计算只补偿受理后的站内滞留。action收到的local_time已经是固定UTC+8,base_ms为执行检查时刻。肯定/否定确认保留原CP56字节,不回读时钟或提供自定义确认时标。
|
||||
|
||||
调用过action的确认走Try,错误校验应答走普通HIGH;WAIT只重试确认,STOP取消未交接任务但不回滚时钟。广播动作一次,快照目标分别确认;满槽丢新并计数,不断链。同内容的新请求仍执行,可能再次回拨。持续NORMAL发布可能推迟Try确认,应用通过包含回调执行期的confirm_pending串行协调新的带时标上报。默认示例只设模拟时钟,`--clock system`才调用Windows时间源。完整契约见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md)和[调用流程](IEC104_完整调用流程.md#flow-clock-synchronization)。
|
||||
|
||||
### 遥控、遥调与带时标命令业务
|
||||
|
||||
[Command](../../104/business/IEC104_Command.h)处理Type45–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
File diff suppressed because it is too large
Load Diff
+5094
File diff suppressed because it is too large
Load Diff
+461
@@ -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 */
|
||||
+305
@@ -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;
|
||||
}
|
||||
+174
@@ -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 */
|
||||
+111
@@ -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;
|
||||
}
|
||||
+56
@@ -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, ×tamp) != 0 ||
|
||||
localtime_r(×tamp.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(¶meters, 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(¶meters104, data, length, cause, 1,
|
||||
bytes, sizeof(bytes), &reply_length) != IEC60870_ASDU_OK ||
|
||||
IEC60870_Asdu_Create_From_Buffer(&reply, ¶meters104, 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 ? ¶meters104 : 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, ¶meters104,
|
||||
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 != ¶meters104 ||
|
||||
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(©, 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) ? ¶meters104 : 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 */
|
||||
+559
@@ -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
Reference in New Issue
Block a user