/* * 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 */