Files
STM32F4-Base/IEC60870-Slave/104/IEC104_Slave.h
T
STF 7596f3371a
CI / L0 静态门禁 (pre-commit) (push) Failing after 44s
CI / L0.5 主机侧单测 (MSVC) (push) Failing after 42s
补充总召唤的广播地址,电度召唤+时钟同步业务的实现
2026-10-29 03:20:19 -07:00

287 lines
21 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 */