Files
STM32F4-Base/IEC60870-Slave/104/IEC104_Slave.c
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

803 lines
40 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,并把底层I帧通知转换为ASDU视图和应答接口。
* 职责分工:Slave管理业务入口和事件绑定;TcpServer管理socket、调度、队列及协议会话。
* 本层只保存不透明的TcpServer指针,通过公开函数操作它,不访问其成员或复制底层对象。
*
* 生命周期:Create -> Config_Default/Init -> Poll或Prepare/Process -> Close -> 读空事件 -> Destroy。
* 接收路径:TcpServer_NextEvent -> 复制原文及来源身份 -> Bind_Event -> 应用处理。
* 应答路径:IEC60870_MasterConnection_Send_* -> 本文件的发送适配 -> TcpServer_Send_Response。
* 主动上报:Slave_Enqueue_Event -> 服务级NORMAL队列,不借用某个请求的连接身份。
* 本层不自动执行业务命令或发送确认;ASDU检查及业务许可仍由应用负责。
*
* 两种身份分别解决不同的复用问题:serial标识本次Slave_Create,generation标识原TCP会话。
* 事件保存两者,防止延迟应答误发给重连的主站或静态槽复用后的另一服务。
* 共用站点表只管理CA路由和节点寿命;总召等业务按注册编号保留原站,点表仍由应用管理。
* 节点的引用寿命与TCP会话寿命分开,持有节点并不表示原连接仍可应答。
* 所有调用在所属事件循环线程串行执行;静态池没有锁,Create/Destroy也须跨服务串行。
*/
#include "IEC104_Slave.h"
#include "IEC104_TcpServer.h"
#include "IEC104_TcpServer_Config.h"
#include <string.h>
/* 104专用默认布局来自IEC104_Types.h;共用ASDU模块通过参数工作,不硬编码104布局。
* 静态只读存储供接收视图和主动上报共用,返回的参数指针不依赖某个服务或事件的寿命。 */
static const IEC60870_AppLayerParameters_t parameters104 = IEC104_APP_LAYER_PARAMETERS_DEFAULT;
/* 完整类型仅在本文件可见;业务只保存Create返回的指针。 */
struct IEC104_Slave {
IEC104_TcpServer_t *server; /* 私有TCP服务;NULL表示该Slave槽空闲,Close不会清除此成员。 */
uint64_t serial; /* 每次成功Create递增,Destroy保留;到UINT64_MAX后不再复用该槽。 */
IEC104_Station_t *stations; /* 调用方节点按注册顺序排列,含待回收的退役节点;不另分配站点池。 */
uint32_t station_locks; /* 固定站点集合的嵌套借用计数,不是线程锁。 */
uint32_t station_snapshots; /* 活动遍历期间不回收退役节点,保护所有游标。 */
uint64_t station_serial; /* 注册编号不随Destroy清零,避免同CA替换/服务槽复用混淆。 */
};
_Static_assert(IEC104_TCP_SERVER_CAPACITY > 0U, "Slave needs at least one service slot");
/* Slave和TcpServer各有对象池;每个Slave占用一个底层名额,直接创建TCP服务也占该名额。 */
static IEC104_Slave_t slave_pool[IEC104_TCP_SERVER_CAPACITY];
static bool Send_ASDU(IEC60870_MasterConnection_t *connection, IEC60870_Asdu_t *asdu);
/**
* 功能:判断句柄是否指向当前占用的Slave静态槽,NULL和池外地址返回false。
* 说明:先按地址查池再解引用;server非空只表示拥有底层对象,不代表已Init或已连接。
* 槽复用后,单独的旧句柄副本无法与新句柄区分;Destroy成功后业务必须停止使用旧句柄。
*/
static bool Slave_Owned(const IEC104_Slave_t *self)
{
size_t index;
for (index = 0U; index < IEC104_TCP_SERVER_CAPACITY; ++index) {
if (self == &slave_pool[index]) { return self->server != NULL; }
}
return false;
}
/* 先在本服务的链表中按地址查找,再读取目标节点;外来节点不借用其_owner字段找服务。
* 同时接受尚未回收的退役节点,以便原业务继续Acquire/Release;这里不判断CA路由资格。 */
static IEC104_Station_t *find_owned_station(const IEC104_Slave_t *self,
const IEC104_Station_t *station)
{
IEC104_Station_t *current;
if (!Slave_Owned(self) || station == NULL) { return NULL; }
for (current = self->stations; current != NULL; current = current->_next) {
if (current == station) { return current; }
}
return NULL;
}
/* 活动快照可能停在任意节点,因此全部快照End前都不摘除退役节点。
* 无快照且已删除、引用归零时才摘链并清零;不释放调用方的节点或context存储。
* Remove/Release/快照End均尝试回收,调用方据Is_Reusable决定何时释放自己的存储。 */
static void collect_retired_stations(IEC104_Slave_t *self)
{
IEC104_Station_t **entry = &self->stations;
if (self->station_snapshots != 0U) { return; }
while (*entry != NULL) {
IEC104_Station_t *station = *entry;
if (!station->_registered && station->_users == 0U) {
*entry = station->_next;
memset(station, 0, sizeof(*station));
} else { entry = &station->_next; }
}
}
/* 借用调用方节点,成功前不改链表或节点;不创建通信资源,不分配点表。 */
IEC104_StationResult_t IEC104_Slave_Add_Station(IEC104_Slave_t *self,
IEC104_Station_t *station, uint16_t common_address, void *context)
{
IEC104_Station_t **tail;
if (!Slave_Owned(self) || station == NULL || common_address == 0U ||
common_address == IEC104_CA_BROADCAST) { return IEC104_STATION_INVALID_ARGUMENT; }
if (station->_owner != NULL) { return IEC104_STATION_ALREADY_REGISTERED; }
if (self->station_locks != 0U) { return IEC104_STATION_BUSY; }
if (self->station_serial == UINT64_MAX) { return IEC104_STATION_LIMIT; }
for (tail = &self->stations; *tail != NULL; tail = &(*tail)->_next) {
if ((*tail)->_registered && (*tail)->_common_address == common_address) {
return IEC104_STATION_DUPLICATE_CA;
}
}
station->_next = NULL;
station->_owner = self;
station->_context = context;
station->_users = 0U;
station->_common_address = common_address;
station->_id = ++self->station_serial;
station->_registered = true;
*tail = station;
return IEC104_STATION_OK;
}
/* 删除先退出路由,引用和快照决定实际回收时机;OK不代表节点已可释放。
* 保留中的旧节点不会挡住相同CA的新注册,原业务仍通过注册编号找到旧节点。 */
IEC104_StationResult_t IEC104_Slave_Remove_Station(IEC104_Slave_t *self, IEC104_Station_t *station)
{
if (!Slave_Owned(self) || station == NULL) { return IEC104_STATION_INVALID_ARGUMENT; }
if (find_owned_station(self, station) == NULL || !station->_registered) { return IEC104_STATION_NOT_FOUND; }
if (self->station_locks != 0U) { return IEC104_STATION_BUSY; }
station->_registered = false;
collect_retired_stations(self);
return IEC104_STATION_OK;
}
/* 新请求只匹配仍注册的节点;返回借用指针,不自动增加引用或决定广播范围。 */
IEC104_Station_t *IEC104_Slave_Get_Station(const IEC104_Slave_t *self, uint16_t common_address)
{
IEC104_Station_t *station;
if (!Slave_Owned(self) || common_address == 0U || common_address == IEC104_CA_BROADCAST) { return NULL; }
for (station = self->stations; station != NULL; station = station->_next) {
if (station->_registered && station->_common_address == common_address) { return station; }
}
return NULL;
}
/* 不另存可失配的数量;上限仅由可注册CA和调用方存储决定。 */
size_t IEC104_Slave_Get_Station_Count(const IEC104_Slave_t *self)
{
const IEC104_Station_t *station;
size_t count = 0U;
if (!Slave_Owned(self)) { return 0U; }
for (station = self->stations; station != NULL; station = station->_next) {
if (station->_registered) { ++count; }
}
return count;
}
/* 普通遍历读取当前注册集合;跨循环固定集合须Lock,允许增删的遍历须用Snapshot。 */
IEC104_Station_t *IEC104_Slave_Next_Station(const IEC104_Slave_t *self, const IEC104_Station_t *current)
{
IEC104_Station_t *station;
if (!Slave_Owned(self)) { return NULL; }
station = current == NULL ? self->stations : find_owned_station(self, current);
if (current != NULL && station != NULL) { station = station->_next; }
while (station != NULL && !station->_registered) { station = station->_next; }
return station;
}
/* 固定遍历集合,不锁点值;串行事件循环中的嵌套借用必须显式配对。 */
IEC104_StationResult_t IEC104_Slave_Lock_Stations(IEC104_Slave_t *self)
{
if (!Slave_Owned(self)) { return IEC104_STATION_INVALID_ARGUMENT; }
if (self->station_locks == UINT32_MAX) { return IEC104_STATION_LIMIT; }
++self->station_locks;
return IEC104_STATION_OK;
}
IEC104_StationResult_t IEC104_Slave_Unlock_Stations(IEC104_Slave_t *self)
{
if (!Slave_Owned(self) || self->station_locks == 0U) { return IEC104_STATION_INVALID_ARGUMENT; }
--self->station_locks;
return IEC104_STATION_OK;
}
/* 持有具体节点,不重新按CA路由;尚被保留的退役节点也能取得引用,供原业务继续处理。 */
IEC104_StationResult_t IEC104_Slave_Acquire_Station(IEC104_Slave_t *self, IEC104_Station_t *station)
{
if (!Slave_Owned(self) || station == NULL) { return IEC104_STATION_INVALID_ARGUMENT; }
if (find_owned_station(self, station) == NULL) { return IEC104_STATION_NOT_FOUND; }
if (station->_users == UINT32_MAX) { return IEC104_STATION_LIMIT; }
++station->_users;
return IEC104_STATION_OK;
}
/* 归还自己的引用后可能立即摘链清零;没有其他寿命保护时,不再使用该节点或context。 */
IEC104_StationResult_t IEC104_Slave_Release_Station(IEC104_Slave_t *self, IEC104_Station_t *station)
{
if (!Slave_Owned(self) || station == NULL) { return IEC104_STATION_INVALID_ARGUMENT; }
if (find_owned_station(self, station) == NULL) { return IEC104_STATION_NOT_FOUND; }
if (station->_users == 0U) { return IEC104_STATION_INVALID_ARGUMENT; }
--station->_users;
collect_retired_stations(self);
return IEC104_STATION_OK;
}
/* 延迟事件按原注册编号找回节点,包括被引用/快照保留的退役节点,避免同CA替换串站。
* 与按CA查找一样只返回借用指针;需要独立持有时仍须Acquire。 */
IEC104_Station_t *IEC104_Slave_Get_Station_By_Id(const IEC104_Slave_t *self, uint64_t station_id)
{
IEC104_Station_t *station;
if (!Slave_Owned(self) || station_id == 0U) { return NULL; }
for (station = self->stations; station != NULL; station = station->_next) {
if (station->_id == station_id) { return station; }
}
return NULL;
}
uint64_t IEC104_Station_Get_Id(const IEC104_Station_t *station)
{
return station != NULL && station->_owner != NULL ? station->_id : 0U;
}
bool IEC104_Station_Is_Registered(const IEC104_Station_t *station)
{
return station != NULL && station->_owner != NULL && station->_registered;
}
bool IEC104_Station_Is_Reusable(const IEC104_Station_t *station)
{
return station != NULL && station->_owner == NULL;
}
/* 只保存注册编号上界和遍历位置,不冻结IOA或点值,也不阻止站点增删。
* 注册编号单调递增,后来加入的同CA替代节点也会落在本次上界之外。 */
IEC104_StationResult_t IEC104_Slave_StationSnapshot_Begin(IEC104_Slave_t *self,
IEC104_StationSnapshot_t *snapshot)
{
if (!Slave_Owned(self) || snapshot == NULL || snapshot->_owner != NULL) {
return IEC104_STATION_INVALID_ARGUMENT;
}
if (self->station_snapshots == UINT32_MAX) { return IEC104_STATION_LIMIT; }
snapshot->_owner = self;
snapshot->_next = self->stations;
snapshot->_last_id = self->station_serial;
snapshot->_self = snapshot;
++self->station_snapshots;
return IEC104_STATION_OK;
}
/* 先推进游标再返回当前仍注册的节点;删除的未来节点跳过,新增节点不纳入。
* 快照保护节点存储但不自动Acquire;返回NULL只表示遍历完毕,仍须End归还保护。 */
IEC104_Station_t *IEC104_Slave_StationSnapshot_Next(IEC104_StationSnapshot_t *snapshot)
{
if (snapshot == NULL || snapshot->_self != snapshot || !Slave_Owned(snapshot->_owner)) { return NULL; }
while (snapshot->_next != NULL) {
IEC104_Station_t *station = snapshot->_next;
snapshot->_next = station->_next;
if (station->_id > snapshot->_last_id) { snapshot->_next = NULL; return NULL; }
if (station->_registered) { return station; }
}
return NULL;
}
/* 与Begin配对一次;最后一个快照结束会尝试回收所有无引用的退役节点。
* 业务若要跨End继续使用Next返回的节点,必须提前Acquire。 */
IEC104_StationResult_t IEC104_Slave_StationSnapshot_End(IEC104_StationSnapshot_t *snapshot)
{
IEC104_Slave_t *self;
if (snapshot == NULL || snapshot->_self != snapshot || !Slave_Owned(snapshot->_owner)) {
return IEC104_STATION_INVALID_ARGUMENT;
}
self = snapshot->_owner;
--self->station_snapshots;
memset(snapshot, 0, sizeof(*snapshot));
collect_retired_stations(self);
return IEC104_STATION_OK;
}
uint16_t IEC104_Station_Get_Common_Address(const IEC104_Station_t *station)
{
return station != NULL && station->_owner != NULL ? station->_common_address : 0U;
}
void *IEC104_Station_Get_Context(const IEC104_Station_t *station)
{
return station != NULL && station->_owner != NULL ? station->_context : NULL;
}
/**
* 功能:返回本层校验失败的结果,同时完整填写可选的error。
* 输入参数:result——本层检测出的错误;error——调用方诊断存储,允许NULL。
* 说明:阶段统一为VALIDATE,socket_error为0;底层操作的诊断直接透传,不在这里改写。
*/
static IEC104_TcpResult_t Slave_Error(IEC104_TcpResult_t result, IEC104_TcpError_t *error)
{
if (error != NULL) {
error->result = result;
error->stage = IEC104_TCP_STAGE_VALIDATE;
error->socket_error = 0;
}
return result;
}
/**
* 功能:从共用连接接口取回它所属的Slave事件;接口不匹配时返回NULL。
* 说明:同时核对发送函数和事件内接口的地址,拒绝赋值/memcpy产生的未修复副本。
* 此处只检查接口绑定,服务所有权、serial和连接代次由发送路径继续检查。
* connection及其object必须仍指向有效存储,不能用此检查代替业务的对象寿命管理。
*/
static IEC104_SlaveEvent_t *Get_Context(const IEC60870_MasterConnection_t *connection)
{
IEC104_SlaveEvent_t *event;
if (connection == NULL || connection->send_asdu != Send_ASDU || connection->object == NULL) {
return NULL;
}
event = (IEC104_SlaveEvent_t *)connection->object;
return &event->connection == connection ? event : NULL;
}
/**
* 功能:确认待发送ASDU使用可由本接入层处理的104应用层布局。
* 返回值:基础参数合法、COT/CA/IOA为2/2/3字节且ASDU上限不超过249时为true。
* 说明:允许业务采用更小的最大长度或不同默认OA;这里不检查具体信息对象和业务权限。
*/
static bool Parameters_Compatible(const IEC60870_AppLayerParameters_t *parameters)
{
return IEC60870_AppLayerParameters_Valid(parameters) &&
parameters->sizeOfCOT == 2 && parameters->sizeOfCA == 2 && parameters->sizeOfIOA == 3 &&
parameters->maxSizeOfASDU <= IEC104_MAX_ASDU_LEN;
}
/* 完整事件和紧凑身份共用发送校验,不把旧身份替换成当前连接。
* self只在确认仍被静态池占用后解引用;队列策略、发送期限和FATAL语义保持由TcpServer处理。 */
static IEC104_TcpSubmitResult_t Submit_Response(IEC104_Slave_t *self,
const IEC104_SlaveEventIdentity_t *identity, const IEC60870_Asdu_t *asdu, int bypass_queue)
{
const uint8_t *data;
uint16_t length;
if (!Slave_Owned(self) || identity == NULL || identity->slave != self ||
identity->instance_id != self->serial || identity->generation == 0U ||
asdu == NULL || !Parameters_Compatible(asdu->parameters) ||
IEC60870_Asdu_Get_Data(asdu, &data, &length) != IEC60870_ASDU_OK) {
return IEC104_TCP_SUBMIT_ERROR;
}
return bypass_queue ? IEC104_TcpServer_Try_Send_Response(self->server, identity->generation, data, length) :
IEC104_TcpServer_Send_Response(self->server, identity->generation, data, length);
}
IEC104_TcpSubmitResult_t IEC104_Slave_Send_Response(IEC104_Slave_t *self,
const IEC104_SlaveEventIdentity_t *identity, const IEC60870_Asdu_t *asdu)
{
return Submit_Response(self, identity, asdu, 0);
}
IEC104_TcpSubmitResult_t IEC104_Slave_Try_Send_Response(IEC104_Slave_t *self,
const IEC104_SlaveEventIdentity_t *identity, const IEC60870_Asdu_t *asdu)
{
return Submit_Response(self, identity, asdu, 1);
}
IEC104_TcpSubmitResult_t IEC104_Slave_Reject_Request(IEC104_Slave_t *self,
const IEC104_SlaveEvent_t *event, int cause)
{
uint8_t bytes[IEC104_MAX_ASDU_LEN];
const uint8_t *data;
uint16_t length, reply_length;
IEC60870_Asdu_t reply;
IEC104_SlaveEventIdentity_t identity;
IEC104_TcpServerStatus_t status;
if (cause < IEC60870_ASDU_COT_UNKNOWN_TYPE || cause > IEC60870_ASDU_COT_UNKNOWN_IOA ||
!IEC104_Slave_Get_Event_Identity(self, event, &identity) ||
event->transport.type != IEC104_TCP_EVENT_I_FRAME || event->asdu_result != IEC60870_ASDU_OK ||
IEC104_TcpServer_Get_Status(self->server, &status, NULL) != IEC104_TCP_OK) {
return IEC104_TCP_SUBMIT_ERROR;
}
/* 本次事件之后尚未消费的启停通知表明链路已停用或曾停用,不能跨启停提交应答。 */
if (status.pending_state_event_count != 0U ||
IEC60870_Asdu_Get_Data(&event->asdu, &data, &length) != IEC60870_ASDU_OK ||
IEC60870_Asdu_Build_Response(&parameters104, data, length, cause, 1,
bytes, sizeof(bytes), &reply_length) != IEC60870_ASDU_OK ||
IEC60870_Asdu_Create_From_Buffer(&reply, &parameters104, bytes, reply_length) != IEC60870_ASDU_OK) {
return IEC104_TCP_SUBMIT_ERROR;
}
return Submit_Response(self, &identity, &reply, 0);
}
/**
* 功能:把普通/不排队应答交给产生该事件的原服务、原会话。
* 输入参数:connection——事件内已绑定的接口;asdu——布局兼容的完整ASDU,可独立于接收视图。
* 返回值:透传OK/WAIT/ERROR/FATAL并记录在原事件中;普通send_asdu包装为bool。
* 说明:先检查Slave所有权和serial,再由TcpServer检查generation、启停状态及发送容量。
* OK表示已复制接手,不代表写完或收到ACK;WAIT未接手,业务保存原事件/ASDU后择机重试。
* 不查找当前新连接来替换旧身份;FATAL可能已经写出字节,不能作为未发送直接重放。
*/
static IEC104_TcpSubmitResult_t Submit_ASDU(IEC60870_MasterConnection_t *connection,
const IEC60870_Asdu_t *asdu, int bypass_queue)
{
IEC104_SlaveEvent_t *event = Get_Context(connection);
IEC104_SlaveEventIdentity_t identity;
if (event == NULL) { return IEC104_TCP_SUBMIT_ERROR; }
identity.slave = event->_owner;
identity.instance_id = event->_owner_serial;
identity.generation = event->_generation;
event->_last_submit_result = Submit_Response(event->_owner, &identity, asdu, bypass_queue);
return event->_last_submit_result;
}
static bool Send_ASDU(IEC60870_MasterConnection_t *connection, IEC60870_Asdu_t *asdu)
{
return Submit_ASDU(connection, asdu, 0) == IEC104_TCP_SUBMIT_OK;
}
/* 104专用不排队入口;不改变共用连接接口的函数指针或普通应答排队约定。 */
IEC104_TcpSubmitResult_t IEC104_Slave_Try_Send_ASDU(
IEC60870_MasterConnection_t *connection, const IEC60870_Asdu_t *asdu)
{
return Submit_ASDU(connection, asdu, 1);
}
/**
* 功能:修改ASDU的传送原因和否定标志,再沿原连接提交确认或终止。
* 输入参数:cause——目标COT;negative——目标P/N;asdu——允许原地修改的ASDU。
* 返回值:true已接手;false表示字段修改或提交失败,详细提交结果保存在事件中。
* 说明:按COT、P/N顺序修改,后续失败不回滚已经成功的修改;不执行业务,也不自动重试。
*/
static bool Send_Confirmation(IEC60870_MasterConnection_t *connection,
IEC60870_Asdu_t *asdu, int cause, bool negative)
{
IEC104_SlaveEvent_t *event = Get_Context(connection);
if (event == NULL) { return false; }
event->_last_submit_result = IEC104_TCP_SUBMIT_ERROR;
if (IEC60870_Asdu_Set_COT(asdu, cause) != IEC60870_ASDU_OK ||
IEC60870_Asdu_Set_Negative(asdu, negative ? 1 : 0) != IEC60870_ASDU_OK) {
return false;
}
return Send_ASDU(connection, asdu);
}
/* 共用激活确认入口:COT=7,P/N由业务指定;沿用Send_Confirmation的修改及提交契约。 */
static bool Send_ACT_CON(IEC60870_MasterConnection_t *connection, IEC60870_Asdu_t *asdu, bool negative)
{
return Send_Confirmation(connection, asdu, IEC60870_ASDU_COT_ACTIVATION_CON, negative);
}
/* 共用激活终止入口:COT=10、P/N=0;由业务处理完成后调用,不自动跟随ACT_CON发送。 */
static bool Send_ACT_TERM(IEC60870_MasterConnection_t *connection, IEC60870_Asdu_t *asdu)
{
return Send_Confirmation(connection, asdu, IEC60870_ASDU_COT_ACTIVATION_TERMINATION, false);
}
/* 共用参数查询入口:有效事件接口返回静态只读参数;不要求原连接仍在线,也不表示可以发送。 */
static const IEC60870_AppLayerParameters_t *Get_Parameters(IEC60870_MasterConnection_t *connection)
{
return Get_Context(connection) != NULL ? &parameters104 : NULL;
}
/**
* 功能:让目标事件的ASDU视图和共用发送接口指向目标自身的存储。
* 前提:transport原文已复制完毕,事件地址在后续使用期间保持稳定。
* 输出参数:event->asdu/connection/asdu_result;不改来源身份、原文和历史提交结果。
* 说明:非I帧不提供ASDU接口;I帧视图构造失败时保留原通知供诊断,connection保持清零。
* 视图借用本事件中APCI之后的ASDU字节,不再借用TcpServer或Receiver的内部缓冲。
*/
static void Bind_Event(IEC104_SlaveEvent_t *event)
{
uint16_t length;
memset(&event->asdu, 0, sizeof(event->asdu));
memset(&event->connection, 0, sizeof(event->connection));
event->asdu_result = IEC60870_ASDU_UNSUPPORTED;
if (event->transport.type != IEC104_TCP_EVENT_I_FRAME) { return; }
event->asdu_result = IEC60870_ASDU_ERROR;
length = event->transport.data.i_frame.apdu_length;
if (length < IEC104_APCI_LEN || length > IEC104_MAX_APDU_LEN) { return; }
event->asdu_result = IEC60870_Asdu_Create_From_Buffer(&event->asdu, &parameters104,
event->transport.data.i_frame.apdu + IEC104_APCI_LEN, (uint16_t)(length - IEC104_APCI_LEN));
if (event->asdu_result != IEC60870_ASDU_OK) { return; }
event->connection.object = event;
event->connection.send_asdu = Send_ASDU;
event->connection.send_act_con = Send_ACT_CON;
event->connection.send_act_term = Send_ACT_TERM;
event->connection.get_application_layer_parameters = Get_Parameters;
}
/**
* 功能:从静态池取得一个Slave,并为它取得一个私有TcpServer。
* 返回值:成功返回不透明句柄;任一池无可用槽或身份编号耗尽时返回NULL。
* 说明:不创建socket、不分配堆内存;只有取得底层服务后才占用Slave槽并递增serial。
* 成功后由业务配对Destroy;不能自行分配、复制、清零或free句柄指向的对象。
*/
IEC104_Slave_t *IEC104_Slave_Create(void)
{
size_t index;
for (index = 0U; index < IEC104_TCP_SERVER_CAPACITY; ++index) {
IEC104_Slave_t *self = &slave_pool[index];
if (self->server == NULL && self->serial != UINT64_MAX) {
IEC104_TcpServer_t *server = IEC104_TcpServer_Create();
if (server == NULL) { return NULL; }
self->server = server;
++self->serial;
return self;
}
}
return NULL;
}
/**
* 功能:归还已关闭且通知已读空的Slave及其底层服务。
* 输入参数:self——Create取得且未Destroy的句柄;error——可选的本次调用诊断。
* 返回值:OK时句柄失效;运行中为INVALID_STATE,通知未读空为EVENTS_PENDING,失败保留对象。
* 说明:先由TcpServer检查并归还底层槽,再释放Slave槽;保留serial隔离旧事件。
* 常规顺序为Close -> NextEvent读空 -> Destroy;尚未Init或Init失败后也可直接归还。
*/
IEC104_TcpResult_t IEC104_Slave_Destroy(IEC104_Slave_t *self, IEC104_TcpError_t *error)
{
IEC104_TcpResult_t result;
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
if (self->stations != NULL || self->station_locks != 0U || self->station_snapshots != 0U) {
return Slave_Error(IEC104_TCP_INVALID_STATE, error);
}
result = IEC104_TcpServer_Destroy(self->server, error);
if (result == IEC104_TCP_OK) { self->server = NULL; }
return result;
}
/**
* 功能:填写默认监听地址0.0.0.0、端口2404及协议参数。
* 输出参数:config——调用方可继续调整的配置;NULL时无操作。
* 说明:复用TcpServer默认值,不占用服务槽;ASDU布局另由parameters104提供。
*/
void IEC104_Slave_Config_Default(IEC104_SlaveConfig_t *config)
{
IEC104_TcpServer_Config_Default(config);
}
/**
* 功能:初始化所拥有的TCP服务,开始监听并建立服务级发送队列。
* 输入参数:self——有效Slave句柄;config——本次监听和协议配置,底层复制后使用。
* 输出参数:error——本次诊断,允许NULL;底层返回的阶段和网络错误保持原样。
* 返回值:OK已监听,其他结果区分参数、配置、生命周期或网络失败。
* 说明:成功不表示已有主站,之后必须驱动Poll或Prepare/Process;失败不销毁句柄。
* 再次Init前须Close并读空旧通知,旧事件的连接身份不会因重新初始化而更新。
*/
IEC104_TcpResult_t IEC104_Slave_Init(IEC104_Slave_t *self,
const IEC104_SlaveConfig_t *config, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
return IEC104_TcpServer_Init(self->server, config, error);
}
/**
* 功能:为外部事件循环查询socket关注集合、等待期限及本地待处理工作。
* 输入参数:self——已Init的服务;info——非NULL快照存储;error——可选诊断。
* 返回值:OK时写出info;失败保持info不变,不进行收发或协议推进。
* 说明:有待消费事件或need_process时不得阻塞等待;状态改变后须重新Prepare。
* 快照中的socket仅供注册,所有权仍属于内部TcpServer,业务不能直接读写或关闭。
*/
IEC104_TcpResult_t IEC104_Slave_Prepare(const IEC104_Slave_t *self,
IEC104_TcpWaitInfo_t *info, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
return IEC104_TcpServer_Prepare(self->server, info, error);
}
/**
* 功能:把外部事件循环的一批就绪、定时或本地通知交给底层有界推进。
* 输入参数:self——已Init的服务;ready——必填就绪批次,身份来自对应Prepare/注册快照。
* 输出参数:error——可选诊断;返回值与TcpServer_Process一致,OK也包括没有进展。
* 说明:不等待;返回后消费NextEvent,再Prepare更新关注集合。旧通知不得替换为新token。
* 客户端故障通过收尾事件交付,服务级失败则由调用方保存诊断后Close。
*/
IEC104_TcpResult_t IEC104_Slave_Process(IEC104_Slave_t *self,
const IEC104_TcpReady_t *ready, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
return IEC104_TcpServer_Process(self->server, ready, error);
}
/**
* 功能:独立主循环使用的准备、至多一次网络等待和有界推进组合入口。
* 输入参数:self——已Init的服务;max_wait_ms——本轮网络等待上限,0不等待;error可为NULL。
* 返回值:OK表示服务可继续;客户端断线不一定使本函数失败,须读取NextEvent。
* 说明:等待还受协议期限和本地工作约束;max_wait_ms不保证整个函数的执行耗时。
* 不创建后台线程、不调用业务处理函数;每轮返回后由应用消费通知。
*/
IEC104_TcpResult_t IEC104_Slave_Poll(IEC104_Slave_t *self,
uint32_t max_wait_ms, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
return IEC104_TcpServer_Poll(self->server, max_wait_ms, error);
}
/**
* 功能:取出最早的TCP通知,并为有效I帧建立ASDU视图和原连接的发送接口。
* 输入参数:self——有效句柄,Close后仍可读取;event——地址稳定的非NULL输出存储。
* 返回值:READY替换整个event;EMPTY/ERROR保持上次事件及其视图不变。
* 说明:先读入临时量,只有取得通知后才覆盖输出;保存原服务serial和连接generation。
* READY仅表示取得事件,I帧还须检查asdu_result;视图失败仍保留原文,不自动确认。
* 不进行I/O;下一次READY会覆盖同一event,延迟处理须另存并调用Copy_Event修复自引用。
*/
IEC104_TcpEventResult_t IEC104_Slave_NextEvent(IEC104_Slave_t *self, IEC104_SlaveEvent_t *event)
{
IEC104_TcpEvent_t received;
IEC104_TcpEventResult_t result;
if (!Slave_Owned(self) || event == NULL) { return IEC104_TCP_EVENT_ERROR; }
result = IEC104_TcpServer_NextEvent(self->server, &received);
if (result != IEC104_TCP_EVENT_READY) { return result; }
memset(event, 0, sizeof(*event));
event->transport = received;
event->_owner = self;
event->_owner_serial = self->serial;
event->_generation = received.generation;
event->_last_submit_result = IEC104_TCP_SUBMIT_ERROR;
Bind_Event(event);
return IEC104_TCP_EVENT_READY;
}
/* 只校验事件自身布局和自引用,不要求原服务或TCP会话仍存活。
* Copy_Event允许保存历史原文;Get_Event_Identity在此基础上额外校验当前服务实例。
* 解析失败的I帧仍是有效通知,由业务处理其asdu_result,不在此丢弃诊断信息。 */
static bool Event_Valid(const IEC104_SlaveEvent_t *event)
{
const uint8_t *data;
uint16_t length;
if (event == NULL || event->_owner == NULL || event->_owner_serial == 0U ||
event->_generation == 0U || event->_generation != event->transport.generation ||
event->transport.type < IEC104_TCP_EVENT_CONNECTED ||
event->transport.type > IEC104_TCP_EVENT_DEACTIVATED) {
return false;
}
if (event->transport.type == IEC104_TCP_EVENT_I_FRAME && event->asdu_result == IEC60870_ASDU_OK) {
if (event->connection.object != event || Get_Context(&event->connection) != event ||
event->asdu.parameters != &parameters104 ||
IEC60870_Asdu_Get_Data(&event->asdu, &data, &length) != IEC60870_ASDU_OK ||
data != event->transport.data.i_frame.apdu + IEC104_APCI_LEN ||
(uint32_t)length + IEC104_APCI_LEN != event->transport.data.i_frame.apdu_length) {
return false;
}
}
return true;
}
/**
* 功能:把事件复制到稳定的目标存储,同时重建目标ASDU视图及发送接口的自引用。
* 输入参数:source——NextEvent或本函数产生的有效事件;destination——目标事件存储。
* 返回值:true已复制或源/目标为同一合法对象;false校验失败且不修改目标。
* 说明:先核对源视图仍借用源原文,避免传播普通赋值形成的悬空绑定。
* 保留原服务、代次和最近提交结果,不查询当前主站、不发送;历史事件也可复制保存。
* 复制只延长原文和接口存储的寿命,不能使已关闭或已被替换的原连接重新可用。
*/
bool IEC104_Slave_Copy_Event(IEC104_SlaveEvent_t *destination, const IEC104_SlaveEvent_t *source)
{
if (destination == NULL || !Event_Valid(source)) { return false; }
if (destination != source) { *destination = *source; Bind_Event(destination); }
return true;
}
/**
* 功能:验证业务收到的Slave事件,交付可保存的服务实例和原连接身份。
* 参数:self为所属服务;event为原事件或Copy_Event副本;identity为非NULL输出。
* 返回:true写出身份;false为非法来源/布局/视图或参数,保持输出不变。
* 说明:只匹配服务实例,不用当前TCP代次拒绝历史通知;是否仍可推进另由Check_Request判断。
*/
bool IEC104_Slave_Get_Event_Identity(const IEC104_Slave_t *self,
const IEC104_SlaveEvent_t *event, IEC104_SlaveEventIdentity_t *identity)
{
IEC104_SlaveEventIdentity_t copy;
if (!Slave_Owned(self) || identity == NULL || !Event_Valid(event) ||
event->_owner != self || event->_owner_serial != self->serial) {
return false;
}
memset(&copy, 0, sizeof(copy));
copy.slave = self;
copy.instance_id = event->_owner_serial;
copy.generation = event->_generation;
*identity = copy;
return true;
}
/**
* 功能:统一判断业务是否应先处理通知、放弃失效请求,或继续当前会话的工作。
* 参数:identity为Get_Event_Identity的只读结果,允许零初始化表示尚未绑定会话。
* 返回:ERROR为参数/查询失败;WAIT_EVENTS优先于会话判断;STALE为原身份不再可用;READY可继续。
* 说明:检查通知FIFO、服务实例、TCP代次和传输激活状态,不执行I/O或保留发送窗口。
* 先读空通知可避免同一代次STOP后又START时,仅凭最终激活状态误续旧业务。
*/
IEC104_SlaveRequestState_t IEC104_Slave_Check_Request(const IEC104_Slave_t *self,
const IEC104_SlaveEventIdentity_t *identity)
{
IEC104_TcpServerStatus_t status;
if (!Slave_Owned(self) || identity == NULL ||
IEC104_TcpServer_Get_Status(self->server, &status, NULL) != IEC104_TCP_OK) {
return IEC104_SLAVE_REQUEST_ERROR;
}
if (status.pending_event_count != 0U) { return IEC104_SLAVE_REQUEST_WAIT_EVENTS; }
if (identity->slave != self || identity->instance_id != self->serial ||
identity->generation == 0U || identity->generation != status.generation ||
!status.connected || !status.connection_status_valid || !status.connection.data_transfer_started) {
return IEC104_SLAVE_REQUEST_STALE;
}
return IEC104_SLAVE_REQUEST_READY;
}
/**
* 功能:补充共用发送接口bool结果,查询这个事件连接最近一次提交的具体状态。
* 输入参数:connection——event.connection的地址;不是Slave句柄,也不是底层socket。
* 返回值:OK/WAIT/ERROR/FATAL;尚未发送或接口绑定无效时返回ERROR。
* 说明:记录只在提交时更新,不随后续Poll或主站ACK更新;发送完成事实应查看RECORD事件。
*/
IEC104_TcpSubmitResult_t IEC104_Slave_Get_Last_Submit_Result(
const IEC60870_MasterConnection_t *connection)
{
const IEC104_SlaveEvent_t *event = Get_Context(connection);
return event != NULL ? event->_last_submit_result : IEC104_TCP_SUBMIT_ERROR;
}
/**
* 功能:取得本接入使用的104应用层参数,供业务构造主动上报或独立应答ASDU。
* 输入参数:self——有效Slave句柄,不要求已Init或当前在线。
* 返回值:静态只读参数地址;无效句柄返回NULL,调用方不能修改或释放该参数。
*/
const IEC60870_AppLayerParameters_t *IEC104_Slave_Get_Application_Layer_Parameters(
const IEC104_Slave_t *self)
{
return Slave_Owned(self) ? &parameters104 : NULL;
}
/**
* 功能:校验ASDU布局后复制到服务级NORMAL队列,用于主动上报。
* 输入参数:self——已Init的服务;asdu——合法且与104布局兼容的ASDU,不包含APCI。
* 返回值:OK已复制,WAIT未接手可重试,ERROR参数/状态非法,FATAL底层队列异常。
* 说明:允许首次接入前及断线期间缓存,本次不直接写socket;空间不足可覆盖最旧NORMAL。
* Close清空缓存;重连恢复可能重复,业务应按自身语义处理。请求应答使用事件connection。
*/
IEC104_TcpSubmitResult_t IEC104_Slave_Enqueue_Event(IEC104_Slave_t *self, IEC60870_Asdu_t *asdu)
{
const uint8_t *data;
uint16_t length;
if (!Slave_Owned(self) || asdu == NULL || !Parameters_Compatible(asdu->parameters) ||
IEC60870_Asdu_Get_Data(asdu, &data, &length) != IEC60870_ASDU_OK) {
return IEC104_TCP_SUBMIT_ERROR;
}
return IEC104_TcpServer_Enqueue_Event(self->server, data, length);
}
/**
* 功能:读取当前主站的IPv4、源端口和连接代次副本,不执行网络查询。
* 输入参数:self——已Init的服务;peer——非NULL输出存储;error——可选诊断。
* 返回值:仅OK写peer;无当前连接为NOT_CONNECTED,其他失败也保持peer原值。
* 说明:这是当前状态;历史事件的地址应从事件自身读取,不能用新主站地址替代旧来源。
*/
IEC104_TcpResult_t IEC104_Slave_Get_Remote_Address(const IEC104_Slave_t *self,
IEC104_TcpPeerInfo_t *peer, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
return IEC104_TcpServer_Get_Remote_Address(self->server, peer, error);
}
/**
* 功能:按事件捕获的原连接身份主动断开主站,保留监听和服务级NORMAL。
* 输入参数:self——所属Slave;event——本服务事件或Copy_Event副本;error——可选诊断。
* 返回值:OK已断开;其他服务/已复用Slave的事件为INVALID_ARGUMENT;当前无连接为NOT_CONNECTED;
* 仍有连接但代次不同为STALE_GENERATION;底层服务异常通过原结果和error返回。
* 说明:来源serial先由本层检查,原generation再由TcpServer核对;不用当前代次代替旧身份。
* 不等待STOPDT或ACK,成功后继续读取收尾事件,再Prepare更新监听条件。
*/
IEC104_TcpResult_t IEC104_Slave_Close_Connection(IEC104_Slave_t *self,
const IEC104_SlaveEvent_t *event, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
if (event == NULL || event->_owner != self || event->_owner_serial != self->serial ||
event->_generation == 0U) {
return Slave_Error(IEC104_TCP_INVALID_ARGUMENT, error);
}
return IEC104_TcpServer_Close_Connection(self->server, event->_generation, error);
}
/* 紧凑业务上下文只需保留来源身份即可关闭原会话;校验与完整事件关闭入口一致。 */
IEC104_TcpResult_t IEC104_Slave_Close_Connection_By_Identity(IEC104_Slave_t *self,
const IEC104_SlaveEventIdentity_t *identity, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
if (identity == NULL || identity->slave != self || identity->instance_id != self->serial ||
identity->generation == 0U) {
return Slave_Error(IEC104_TCP_INVALID_ARGUMENT, error);
}
return IEC104_TcpServer_Close_Connection(self->server, identity->generation, error);
}
/**
* 功能:取得内部TCP服务、队列和最近会话的状态副本,不推进协议或消费队列。
* 输入参数:self——有效句柄;status——非NULL输出存储;error——可选诊断。
* 返回值:OK写入status,失败保持输出;Create后、Init失败后和Close后均可查询。
* 说明:会话和队列各自有有效标志,connected与data_transfer_started含义不同。
*/
IEC104_TcpResult_t IEC104_Slave_Get_Status(const IEC104_Slave_t *self,
IEC104_TcpServerStatus_t *status, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
return IEC104_TcpServer_Get_Status(self->server, status, error);
}
/**
* 功能:关闭监听和当前连接,清空待发ASDU,但保留已生成的业务及收尾通知。
* 输入参数:self——尚未Destroy的句柄;error——可选的本次调用诊断。
* 返回值:OK已收尾或本来未初始化;无效句柄、重入按底层约定返回错误。
* 说明:不等待业务发送/ACK,不归还对象池名额;读空事件后可重新Init或Destroy。
* 自建事件循环还须撤销旧socket注册和定时器,不能将其通知用于下一次Init。
*/
IEC104_TcpResult_t IEC104_Slave_Close(IEC104_Slave_t *self, IEC104_TcpError_t *error)
{
if (!Slave_Owned(self)) { return Slave_Error(IEC104_TCP_INVALID_HANDLE, error); }
return IEC104_TcpServer_Close(self->server, error);
}