diff --git a/IEC60870-Slave/.build/command-comments/IEC104_Command.c.before b/IEC60870-Slave/.build/command-comments/IEC104_Command.c.before new file mode 100644 index 00000000..56af4037 --- /dev/null +++ b/IEC60870-Slave/.build/command-comments/IEC104_Command.c.before @@ -0,0 +1,927 @@ +/* + * IEC104单点/双点遥控(45/46)。 + * 一个事务槽保存工作事件及必要回复,独立短应答使用固定池;全局序号决定提交先后。 + * 应用显式Accept启动动作,Report_Result结束设备期限,Complete结束返回信息阶段。 + * FINISHED和发送收尾分别完成后才释放事务;取消不回滚设备,迟到事件不能复用新事务。 + */ +#include "IEC104_Command.h" +#include "IEC104_TcpServer_Config.h" +#include "IEC104_Net.h" +#include + +#ifndef IEC104_COMMAND_CAPACITY +#define IEC104_COMMAND_CAPACITY IEC104_TCP_SERVER_CAPACITY +#endif +#ifndef IEC104_COMMAND_REPLY_CAPACITY +#define IEC104_COMMAND_REPLY_CAPACITY 4U +#endif +_Static_assert(IEC104_COMMAND_CAPACITY > 0U, "Command needs instance storage"); +_Static_assert(IEC104_COMMAND_REPLY_CAPACITY > 0U && + IEC104_COMMAND_REPLY_CAPACITY < UINT32_MAX / 2U, "Command needs bounded reply storage"); + +typedef struct { + IEC104_SlaveEventIdentity_t identity; + uint8_t request_bytes[10], reply_bytes[10]; + IEC104_Station_t *station; + uint64_t transaction_id, receive_serial, accepted_ms, timer_start_ms; + uint64_t event_serial, accepted_event_serial, select_event_serial, execute_event_serial; + uint64_t confirm_serial, term_serial, reply_serial; + uint8_t phase, event_type, finish_reason, result, confirm_cot, reply_cot; + bool occupied, event_taken, ever_work_delivered, finish_pending; + bool select_delivered, execute_delivered, selected; + bool confirm_negative, confirm_try, send_waiting, return_waiting; + bool confirm_submitted, completed, term_submitted, select_effect, execute_effect; +} CommandTransaction; + +typedef struct { + IEC104_SlaveEventIdentity_t identity; + uint8_t bytes[10]; + uint64_t serial; + uint8_t cot; + bool occupied, send_waiting; +} CommandReply; + +struct IEC104_Command { + IEC104_Slave_t *slave; + IEC104_SlaveEventIdentity_t session; + IEC104_CommandConfig_t config; + IEC104_CommandStatus_t status; + CommandTransaction transaction; + CommandReply replies[IEC104_COMMAND_REPLY_CAPACITY]; + uint64_t serial, next_transaction, next_event, next_reply, last_generation; + bool in_use, in_action; +}; + +static IEC104_Command_t command_pool[IEC104_COMMAND_CAPACITY]; +static uint64_t next_instance; + +/* 先验证静态池归属,再解引用。 */ +static bool Owned(const IEC104_Command_t *self) +{ + unsigned i; + for (i = 0U; i < IEC104_COMMAND_CAPACITY; ++i) { + if (self == &command_pool[i]) { return self->in_use; } + } + return false; +} + +static uint32_t Read_IOA(const uint8_t *bytes) +{ + return (uint32_t)bytes[6] | ((uint32_t)bytes[7] << 8) | ((uint32_t)bytes[8] << 16); +} + +static uint16_t Read_CA(const uint8_t *bytes) +{ + return (uint16_t)((uint16_t)bytes[4] | ((uint16_t)bytes[5] << 8)); +} + +/* 所有事件及动作使用模块保留的请求重建,不能信任回传event中的可修改request。 */ +static void Copy_Request(const CommandTransaction *task, IEC104_CommandRequest_t *request) +{ + memset(request, 0, sizeof(*request)); + request->generation = task->identity.generation; + request->receive_serial = task->receive_serial; + request->station_id = IEC104_Station_Get_Id(task->station); + request->common_address = Read_CA(task->request_bytes); + request->originator_address = task->request_bytes[3]; + request->type_id = task->request_bytes[0]; + request->ioa = Read_IOA(task->request_bytes); + request->is_select = (task->request_bytes[9] & 0x80U) != 0U; + request->selected = task->selected; + request->state = task->request_bytes[9] & (request->type_id == 45U ? 1U : 3U); + request->qualifier = (task->request_bytes[9] >> 2) & 31U; + request->accepted_ms = task->accepted_ms; +} + +static unsigned Pending_Replies(const IEC104_Command_t *self) +{ + const CommandTransaction *task = &self->transaction; + unsigned i, count = (task->confirm_serial != 0U ? 1U : 0U) + + (task->term_serial != 0U ? 1U : 0U) + (task->reply_serial != 0U ? 1U : 0U); + for (i = 0U; i < IEC104_COMMAND_REPLY_CAPACITY; ++i) { + if (self->replies[i].occupied) { ++count; } + } + return count; +} + +/* FINISHED消费与本事务回复交接是两个独立条件;独立短应答不借用事务内存。 */ +static void Reap(IEC104_Command_t *self) +{ + CommandTransaction *task = &self->transaction; + if (!task->occupied || task->phase != IEC104_COMMAND_PHASE_ENDED || task->finish_pending || + task->confirm_serial != 0U || task->term_serial != 0U || task->reply_serial != 0U) { return; } + (void)IEC104_Slave_Release_Station(self->slave, task->station); + memset(task, 0, sizeof(*task)); +} + +/* 第一次终结冻结原因并移除未读工作事件。后续STOP/Cancel只可清回复,不能重发FINISHED。 */ +static void Finish(IEC104_Command_t *self, IEC104_CommandFinishReason_t reason) +{ + CommandTransaction *task = &self->transaction; + if (!task->occupied || task->phase == IEC104_COMMAND_PHASE_ENDED) { Reap(self); return; } + task->phase = IEC104_COMMAND_PHASE_ENDED; + task->finish_reason = (uint8_t)reason; + task->event_type = IEC104_COMMAND_FINISHED; + task->event_taken = false; + task->finish_pending = task->ever_work_delivered; + task->return_waiting = false; + /* 创建事务时为最多两个工作事件和一个FINISHED检查了序号余量。 */ + if (task->finish_pending) { task->event_serial = ++self->next_event; } + switch (reason) { + case IEC104_COMMAND_FINISH_COMPLETED: ++self->status.completed; break; + case IEC104_COMMAND_FINISH_EXECUTION_FAILED: ++self->status.execution_failed; break; + case IEC104_COMMAND_FINISH_ACTION_FAILED: ++self->status.action_failed; break; + case IEC104_COMMAND_FINISH_MISMATCH: ++self->status.mismatches; break; + case IEC104_COMMAND_FINISH_DEACTIVATED: ++self->status.deactivated; break; + case IEC104_COMMAND_FINISH_SELECT_TIMEOUT: ++self->status.select_timeouts; break; + case IEC104_COMMAND_FINISH_DECISION_TIMEOUT: ++self->status.decision_timeouts; break; + case IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT: ++self->status.execute_timeouts; break; + case IEC104_COMMAND_FINISH_CANCELLED: + case IEC104_COMMAND_FINISH_STOPPED: + case IEC104_COMMAND_FINISH_DISCONNECTED: + case IEC104_COMMAND_FINISH_CONNECTION_CHANGED: + case IEC104_COMMAND_FINISH_SEND_ERROR: + case IEC104_COMMAND_FINISH_SERVICE_ERROR: ++self->status.cancelled; break; + default: break; + } + Reap(self); +} + +static void Cancel_All(IEC104_Command_t *self, IEC104_CommandFinishReason_t reason) +{ + CommandTransaction *task = &self->transaction; + task->confirm_serial = task->term_serial = task->reply_serial = 0U; + task->send_waiting = false; + memset(self->replies, 0, sizeof(self->replies)); + Finish(self, reason); +} + +/* 动作/提交门禁,不用于消费Handle_Event本身,否则FIFO中尚有通知时无法读空。 */ +static IEC104_SlaveRequestState_t Check_Session(IEC104_Command_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) { + Cancel_All(self, IEC104_COMMAND_FINISH_SERVICE_ERROR); + self->status.active = false; + } else if (state != IEC104_SLAVE_REQUEST_WAIT_EVENTS && + (!self->status.active || state != IEC104_SLAVE_REQUEST_READY)) { + Cancel_All(self, IEC104_COMMAND_FINISH_CONNECTION_CHANGED); + self->status.active = false; + state = IEC104_SLAVE_REQUEST_STALE; + } + return state; +} + +/* 期限关闭返回0;动作回调中事件已消费,不再计算决定期限。 */ +static uint32_t Timer_Limit(const IEC104_Command_t *self) +{ + const CommandTransaction *task = &self->transaction; + if (!task->occupied || self->in_action) { return 0U; } + switch (task->phase) { + case IEC104_COMMAND_PHASE_SELECT_WAIT: + case IEC104_COMMAND_PHASE_EXECUTE_WAIT: return self->config.decision_timeout_ms; + case IEC104_COMMAND_PHASE_SELECTED: return self->config.select_timeout_ms; + case IEC104_COMMAND_PHASE_EXECUTING: + return task->result == 0U ? self->config.execute_timeout_ms : 0U; + default: return 0U; + } +} + +static uint32_t Remaining(const CommandTransaction *task, uint32_t limit, uint64_t now) +{ + if (now < task->timer_start_ms || now - task->timer_start_ms >= limit) { return 0U; } + return limit - (uint32_t)(now - task->timer_start_ms); +} + +/* 0关闭经过时长限制,不允许正在等待的状态依赖倒退的单调时间继续执行。 */ +static bool Clock_Reversed(const IEC104_Command_t *self, uint64_t now) +{ + const CommandTransaction *task = &self->transaction; + return !self->in_action && task->occupied && now < task->timer_start_ms && + (task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT || task->phase == IEC104_COMMAND_PHASE_SELECTED || + task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT || + (task->phase == IEC104_COMMAND_PHASE_EXECUTING && task->result == 0U)); +} + +/* 0是未排队哨兵;耗尽时终结,绝不回绕到可能与旧排队身份相同的序号。 */ +static uint64_t Reply_Serial(IEC104_Command_t *self) +{ + if (self->next_reply == UINT64_MAX) { + Cancel_All(self, IEC104_COMMAND_FINISH_SERVICE_ERROR); + self->status.active = false; + return 0U; + } + return ++self->next_reply; +} + +static bool Set_Confirm(IEC104_Command_t *self, uint8_t cause, bool negative, bool try_send) +{ + CommandTransaction *task = &self->transaction; + uint64_t serial = Reply_Serial(self); + if (serial == 0U) { return false; } + task->confirm_serial = serial; + task->confirm_cot = cause; + task->confirm_negative = negative; + task->confirm_try = try_send; + task->confirm_submitted = false; + task->send_waiting = false; + return true; +} + +/* 只在已处理会话门禁的调用中使用。I帧在FIFO受理时也必须先检查,避免过期选择复活。 */ +static void Expire(IEC104_Command_t *self, uint64_t now) +{ + CommandTransaction *task = &self->transaction; + uint32_t limit = Timer_Limit(self); + bool reversed = Clock_Reversed(self, now); + if (!reversed && (limit == 0U || Remaining(task, limit, now) != 0U)) { return; } + if (reversed) { ++self->status.clock_errors; } + switch (task->phase) { + case IEC104_COMMAND_PHASE_SELECT_WAIT: + case IEC104_COMMAND_PHASE_EXECUTE_WAIT: + if (Set_Confirm(self, IEC60870_ASDU_COT_ACTIVATION_CON, true, false)) { + Finish(self, IEC104_COMMAND_FINISH_DECISION_TIMEOUT); + } + break; + case IEC104_COMMAND_PHASE_SELECTED: + task->confirm_serial = 0U; /* 只能撤回模块尚持有的确认。 */ + Finish(self, IEC104_COMMAND_FINISH_SELECT_TIMEOUT); + break; + case IEC104_COMMAND_PHASE_EXECUTING: + /* 动作已启动,不能因结果超时把原肯定确认改成否定。 */ + Finish(self, IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT); + break; + default: break; + } +} + +/* 提交入口可能接收STOP,ERROR只能在同连接健康且通知待处理时解释为等待。 */ +static bool Defer_For_Stop(IEC104_Command_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; +} + +/* 三种事务内回复和独立回复共享一个序号域,不另建可溢出的指针FIFO。 */ +enum { COMMAND_HEAD_NONE, COMMAND_HEAD_CONFIRM, COMMAND_HEAD_TERM, COMMAND_HEAD_REPLY, COMMAND_HEAD_SHORT }; +static unsigned Reply_Head(const IEC104_Command_t *self, unsigned *slot) +{ + const CommandTransaction *task = &self->transaction; + uint64_t first = 0U; + unsigned head = COMMAND_HEAD_NONE, i; + if (task->confirm_serial != 0U) { first = task->confirm_serial; head = COMMAND_HEAD_CONFIRM; } + if (task->term_serial != 0U && (first == 0U || task->term_serial < first)) { + first = task->term_serial; head = COMMAND_HEAD_TERM; + } + if (task->reply_serial != 0U && (first == 0U || task->reply_serial < first)) { + first = task->reply_serial; head = COMMAND_HEAD_REPLY; + } + for (i = 0U; i < IEC104_COMMAND_REPLY_CAPACITY; ++i) { + if (self->replies[i].occupied && (first == 0U || self->replies[i].serial < first)) { + first = self->replies[i].serial; head = COMMAND_HEAD_SHORT; *slot = i; + } + } + return head; +} + +/* Complete关闭返回阶段后,确认和可选TERM均交接才向应用报告协议完成。 */ +static void Complete_If_Sent(IEC104_Command_t *self) +{ + CommandTransaction *task = &self->transaction; + if (task->occupied && task->phase == IEC104_COMMAND_PHASE_EXECUTING && task->completed && + task->confirm_submitted && task->confirm_serial == 0U && task->term_serial == 0U) { + Finish(self, task->result == IEC104_COMMAND_RESULT_SUCCEEDED ? + IEC104_COMMAND_FINISH_COMPLETED : IEC104_COMMAND_FINISH_EXECUTION_FAILED); + } + Reap(self); +} + +/* 只提交一个队首。1有进展,0让出,-1组包/服务错误;发送故障已清理后返回0。 */ +static int Submit_Head(IEC104_Command_t *self) +{ + CommandTransaction *task = &self->transaction; + const IEC104_SlaveEventIdentity_t *identity; + const uint8_t *source; + uint8_t bytes[10], cause; + IEC60870_Asdu_t asdu; + IEC104_TcpSubmitResult_t submitted; + IEC104_SlaveRequestState_t state = Check_Session(self); + unsigned slot = 0U, head; + bool negative, try_send; + if (state == IEC104_SLAVE_REQUEST_ERROR) { return -1; } + if (state != IEC104_SLAVE_REQUEST_READY) { return 0; } + head = Reply_Head(self, &slot); + if (head == COMMAND_HEAD_NONE) { return 0; } + identity = &task->identity; + source = task->request_bytes; + cause = IEC60870_ASDU_COT_ACTIVATION_TERMINATION; + negative = false; + try_send = true; + if (head == COMMAND_HEAD_CONFIRM) { + cause = task->confirm_cot; negative = task->confirm_negative; try_send = task->confirm_try; + } else if (head == COMMAND_HEAD_REPLY) { + source = task->reply_bytes; cause = task->reply_cot; + negative = cause != IEC60870_ASDU_COT_DEACTIVATION_CON; try_send = false; + } else if (head == COMMAND_HEAD_SHORT) { + identity = &self->replies[slot].identity; + source = self->replies[slot].bytes; cause = self->replies[slot].cot; + negative = true; try_send = false; + } + memcpy(bytes, source, sizeof(bytes)); + if (IEC60870_Asdu_Create_From_Buffer(&asdu, IEC104_Slave_Get_Application_Layer_Parameters(self->slave), + bytes, sizeof(bytes)) != IEC60870_ASDU_OK || + IEC60870_Asdu_Set_COT(&asdu, cause) != IEC60870_ASDU_OK || + IEC60870_Asdu_Set_Negative(&asdu, negative) != IEC60870_ASDU_OK) { + Cancel_All(self, IEC104_COMMAND_FINISH_SERVICE_ERROR); + return -1; + } + submitted = try_send ? IEC104_Slave_Try_Send_Response(self->slave, identity, &asdu) : + IEC104_Slave_Send_Response(self->slave, identity, &asdu); + if (submitted == IEC104_TCP_SUBMIT_OK) { + ++self->status.submitted; + if (negative) { ++self->status.negative_submitted; } + if (head == COMMAND_HEAD_SHORT) { memset(&self->replies[slot], 0, sizeof(self->replies[slot])); } + else { + task->send_waiting = false; + if (head == COMMAND_HEAD_CONFIRM) { task->confirm_serial = 0U; task->confirm_submitted = true; } + else if (head == COMMAND_HEAD_TERM) { task->term_serial = 0U; task->term_submitted = true; } + else { task->reply_serial = 0U; } + } + Complete_If_Sent(self); + return 1; + } + if (submitted == IEC104_TCP_SUBMIT_WAIT) { + if (head == COMMAND_HEAD_SHORT) { self->replies[slot].send_waiting = true; } + else { task->send_waiting = true; } + ++self->status.waits; + return 0; + } + if (Defer_For_Stop(self, identity, submitted)) { return 0; } + Cancel_All(self, IEC104_COMMAND_FINISH_SEND_ERROR); + self->status.active = false; + return 0; +} + +/* 新短回复不能抢占活动事务的确认存储;池满丢新,不覆盖旧回复,也不断开连接。 */ +static bool Short_Reply(IEC104_Command_t *self, const IEC104_SlaveEventIdentity_t *identity, + const uint8_t *data, uint8_t cause) +{ + unsigned slot; + bool empty = Pending_Replies(self) == 0U; + uint64_t serial; + for (slot = 0U; slot < IEC104_COMMAND_REPLY_CAPACITY; ++slot) { + if (!self->replies[slot].occupied) { break; } + } + if (slot == IEC104_COMMAND_REPLY_CAPACITY) { + ++self->status.overloads; + ++self->status.dropped_requests; + return true; + } + serial = Reply_Serial(self); + if (serial == 0U) { return false; } + self->replies[slot].identity = *identity; + memcpy(self->replies[slot].bytes, data, sizeof(self->replies[slot].bytes)); + self->replies[slot].bytes[1] = 1U; + self->replies[slot].serial = serial; + self->replies[slot].cot = cause; + self->replies[slot].occupied = true; + self->replies[slot].send_waiting = false; + return !empty || Submit_Head(self) >= 0; +} + +/* 替换当前工作请求,仅匹配执行可从选择走到这里。事务身份和选择资源标志保持不变。 */ +static void Work_Event(IEC104_Command_t *self, const uint8_t *data, uint64_t receive_serial, + uint64_t now, bool selected) +{ + CommandTransaction *task = &self->transaction; + memcpy(task->request_bytes, data, sizeof(task->request_bytes)); + task->request_bytes[1] = 1U; + task->receive_serial = receive_serial; + task->accepted_ms = task->timer_start_ms = now; + task->event_type = (data[9] & 0x80U) != 0U ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE; + task->phase = task->event_type == IEC104_COMMAND_SELECT ? + IEC104_COMMAND_PHASE_SELECT_WAIT : IEC104_COMMAND_PHASE_EXECUTE_WAIT; + task->event_serial = ++self->next_event; + task->accepted_event_serial = 0U; + if (task->event_type == IEC104_COMMAND_SELECT) { task->select_event_serial = task->event_serial; } + else { task->execute_event_serial = task->event_serial; } + task->event_taken = false; + task->selected = selected; + task->confirm_submitted = false; + task->send_waiting = false; + ++self->status.accepted; +} + +/* 撤销和不匹配使用本事务预留的短回复存储,不依赖独立池空闲。 + * 原工作请求不覆盖,FINISHED仍能标识需要清理的选择/输出。 */ +static bool End_With_Reply(IEC104_Command_t *self, const uint8_t *data, uint8_t cause, + IEC104_CommandFinishReason_t reason) +{ + CommandTransaction *task = &self->transaction; + uint64_t serial = Reply_Serial(self); + if (serial == 0U) { return false; } + task->confirm_serial = task->term_serial = 0U; + memcpy(task->reply_bytes, data, sizeof(task->reply_bytes)); + task->reply_bytes[1] = 1U; + task->reply_cot = cause; + task->reply_serial = serial; + task->send_waiting = false; + Finish(self, reason); + return Submit_Head(self) >= 0; +} + +/* 按COT、CA、IOA、对象的顺序判错。完整10字节仍可规范回显,截短或多对象不能伪造回显。 */ +static IEC104_CommandEventResult_t Request(IEC104_Command_t *self, const IEC104_SlaveEvent_t *event, + const IEC104_SlaveEventIdentity_t *identity) +{ + CommandTransaction *task = &self->transaction; + IEC60870_AsduInfo_t info; + IEC104_Station_t *station; + const uint8_t *data; + uint16_t length; + uint8_t cause = 0U, state; + uint32_t ioa; + uint64_t now; + bool same_point, is_select; + 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] != 45U && data[0] != 46U) || + (self->config.type_mask & IEC104_COMMAND_TYPE_BIT(data[0])) == 0U || + !IEC60870_Parse_ASDU_Header(event->asdu.parameters, data, length, &info)) { + return IEC104_COMMAND_EVENT_IGNORED; + } + now = IEC104_Net_Now_Ms(); + Expire(self, now); + if (info.object_count != 1U || info.information_objects_len < 4U) { + ++self->status.rejected; + return IEC104_COMMAND_EVENT_HANDLED; + } + ioa = Read_IOA(data); + station = IEC104_Slave_Get_Station(self->slave, info.common_address); + state = data[9] & 3U; + is_select = (data[9] & 0x80U) != 0U; + if (info.cause_of_transmission != IEC60870_ASDU_COT_ACTIVATION && + info.cause_of_transmission != IEC60870_ASDU_COT_DEACTIVATION) { + cause = IEC60870_ASDU_COT_UNKNOWN_CAUSE; + } else if (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 != 10U || info.is_negative_confirm || info.is_test || + (data[0] == 45U ? (data[9] & 2U) != 0U : (state == 0U || state == 3U))) { + cause = info.cause_of_transmission == IEC60870_ASDU_COT_DEACTIVATION ? + IEC60870_ASDU_COT_DEACTIVATION_CON : IEC60870_ASDU_COT_ACTIVATION_CON; + } + if (cause != 0U) { + ++self->status.rejected; + return Short_Reply(self, identity, data, cause) ? IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + same_point = task->occupied && task->identity.generation == identity->generation && + IEC104_Station_Get_Id(task->station) == IEC104_Station_Get_Id(station) && + Read_IOA(task->request_bytes) == ioa; + if (info.cause_of_transmission == IEC60870_ASDU_COT_DEACTIVATION) { + if (same_point && task->request_bytes[0] == data[0] && task->request_bytes[3] == data[3] && + (task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT || task->phase == IEC104_COMMAND_PHASE_SELECTED || + task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT)) { + return End_With_Reply(self, data, IEC60870_ASDU_COT_DEACTIVATION_CON, + IEC104_COMMAND_FINISH_DEACTIVATED) ? IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + ++self->status.rejected; + return Short_Reply(self, identity, data, IEC60870_ASDU_COT_DEACTIVATION_CON) ? + IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + /* 选择确认尚未交接时一律忙拒绝;确认交接后同点执行先匹配,其余新激活不影响选择。 */ + if (same_point && !is_select && task->phase == IEC104_COMMAND_PHASE_SELECTED && task->confirm_submitted) { + if (task->request_bytes[0] != data[0] || task->request_bytes[3] != data[3] || + (task->request_bytes[9] & 0x7FU) != (data[9] & 0x7FU)) { + ++self->status.rejected; + return End_With_Reply(self, data, IEC60870_ASDU_COT_ACTIVATION_CON, + IEC104_COMMAND_FINISH_MISMATCH) ? IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + Work_Event(self, data, event->transport.data.i_frame.receive_serial, now, true); + return IEC104_COMMAND_EVENT_HANDLED; + } + if (task->occupied || (!is_select && (self->config.select_required_mask & IEC104_COMMAND_TYPE_BIT(data[0])) != 0U)) { + ++self->status.rejected; + if (task->occupied) { ++self->status.busy_rejected; } + return Short_Reply(self, identity, data, IEC60870_ASDU_COT_ACTIVATION_CON) ? + IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + if (self->next_transaction == UINT64_MAX || self->next_event > UINT64_MAX - 3U) { + ++self->status.overloads; + ++self->status.dropped_requests; + return IEC104_COMMAND_EVENT_HANDLED; + } + if (IEC104_Slave_Acquire_Station(self->slave, station) != IEC104_STATION_OK) { return IEC104_COMMAND_EVENT_ERROR; } + memset(task, 0, sizeof(*task)); + task->occupied = true; + task->identity = *identity; + task->station = station; + task->transaction_id = ++self->next_transaction; + Work_Event(self, data, event->transport.data.i_frame.receive_serial, now, false); + return IEC104_COMMAND_EVENT_HANDLED; +} + +void IEC104_Command_Config_Default(IEC104_CommandConfig_t *config) +{ + if (config == NULL) { return; } + memset(config, 0, sizeof(*config)); + config->type_mask = IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(46U); + config->select_required_mask = config->type_mask; + config->select_timeout_ms = 30000U; + config->decision_timeout_ms = 15000U; + config->execute_timeout_ms = 60000U; + config->send_act_term = true; +} + +IEC104_Command_t *IEC104_Command_Create(IEC104_Slave_t *slave, const IEC104_CommandConfig_t *config) +{ + const uint16_t supported = IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(46U); + IEC104_Command_t *free_slot = NULL; + unsigned i; + if (config == NULL || config->type_mask == 0U || (config->type_mask & (uint16_t)~supported) != 0U || + (config->select_required_mask & (uint16_t)~config->type_mask) != 0U || config->select_timeout_ms == 0U || + next_instance == UINT64_MAX || IEC104_Slave_Get_Application_Layer_Parameters(slave) == NULL) { return NULL; } + for (i = 0U; i < IEC104_COMMAND_CAPACITY; ++i) { + if (command_pool[i].in_use && command_pool[i].slave == slave) { return NULL; } + if (!command_pool[i].in_use && free_slot == NULL) { free_slot = &command_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_instance; + free_slot->in_use = true; + return free_slot; +} + +bool IEC104_Command_Destroy(IEC104_Command_t *self) +{ + if (!Owned(self) || self->in_action || self->transaction.occupied || Pending_Replies(self) != 0U) { return false; } + memset(self, 0, sizeof(*self)); + return true; +} + +void IEC104_Command_Cancel(IEC104_Command_t *self) +{ + if (Owned(self) && !self->in_action) { Cancel_All(self, IEC104_COMMAND_FINISH_CANCELLED); } +} + +/* 状态通知先处理,不能统一套Check_Request;后续尚未出队的STOP不应阻挡当前通知消费。 */ +IEC104_CommandEventResult_t IEC104_Command_Handle_Event(IEC104_Command_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_COMMAND_EVENT_ERROR; + } + self->session.slave = identity.slave; + self->session.instance_id = identity.instance_id; + if (event->transport.type == IEC104_TCP_EVENT_CONNECTED) { + if (identity.generation <= self->last_generation) { return IEC104_COMMAND_EVENT_IGNORED; } + Cancel_All(self, IEC104_COMMAND_FINISH_CONNECTION_CHANGED); + self->session.generation = self->last_generation = identity.generation; + self->status.active = false; + return IEC104_COMMAND_EVENT_HANDLED; + } + if (identity.generation != self->session.generation || identity.generation == 0U) { + return IEC104_COMMAND_EVENT_IGNORED; + } + switch (event->transport.type) { + case IEC104_TCP_EVENT_ACTIVATED: + self->status.active = true; + return IEC104_COMMAND_EVENT_HANDLED; + case IEC104_TCP_EVENT_DEACTIVATED: + Cancel_All(self, IEC104_COMMAND_FINISH_STOPPED); + self->status.active = false; + return IEC104_COMMAND_EVENT_HANDLED; + case IEC104_TCP_EVENT_DISCONNECTED: + Cancel_All(self, IEC104_COMMAND_FINISH_DISCONNECTED); + self->session.generation = 0U; + self->status.active = false; + return IEC104_COMMAND_EVENT_HANDLED; + case IEC104_TCP_EVENT_I_FRAME: return Request(self, event, &identity); + default: return IEC104_COMMAND_EVENT_IGNORED; + } +} + +bool IEC104_Command_Process(IEC104_Command_t *self) +{ + unsigned step; + if (!Owned(self) || self->in_action) { return false; } + for (step = 0U; step < 2U * (1U + IEC104_COMMAND_REPLY_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) { return true; } + Expire(self, IEC104_Net_Now_Ms()); + Complete_If_Sent(self); + submitted = Submit_Head(self); + if (submitted <= 0) { return submitted == 0; } + } + return true; +} + +IEC104_CommandNextResult_t IEC104_Command_NextEvent(IEC104_Command_t *self, IEC104_CommandEvent_t *event) +{ + CommandTransaction *task; + IEC104_CommandEvent_t result; + IEC104_SlaveRequestState_t state = IEC104_SLAVE_REQUEST_READY; + if (!Owned(self) || self->in_action || event == NULL) { return IEC104_COMMAND_NEXT_ERROR; } + task = &self->transaction; + /* 已存在的FINISHED不受离线或未读通知门禁阻挡;它只清理已结束的协议交互。 */ + if (!task->finish_pending) { + state = Check_Session(self); + if (state == IEC104_SLAVE_REQUEST_READY) { Expire(self, IEC104_Net_Now_Ms()); } + } + if (!task->finish_pending && state == IEC104_SLAVE_REQUEST_ERROR) { return IEC104_COMMAND_NEXT_ERROR; } + if (!task->occupied || (!task->finish_pending && + (state != IEC104_SLAVE_REQUEST_READY || task->event_taken || + (task->phase != IEC104_COMMAND_PHASE_SELECT_WAIT && task->phase != IEC104_COMMAND_PHASE_EXECUTE_WAIT)))) { + return IEC104_COMMAND_NEXT_EMPTY; + } + memset(&result, 0, sizeof(result)); + result.type = (IEC104_CommandEventType_t)task->event_type; + result.instance_id = self->serial; + result.transaction_id = task->transaction_id; + result.event_serial = task->event_serial; + Copy_Request(task, &result.request); + if (task->finish_pending) { + result.reason = (IEC104_CommandFinishReason_t)task->finish_reason; + result.select_effect = task->select_effect; + result.execute_effect = task->execute_effect; + task->finish_pending = false; + } else { + task->ever_work_delivered = true; + if (result.type == IEC104_COMMAND_SELECT) { task->select_delivered = true; } + else { task->execute_delivered = true; } + } + task->event_taken = true; + *event = result; + Reap(self); + return IEC104_COMMAND_NEXT_READY; +} + +/* 校验应用交互的独立身份;Slave身份只解决会话归属,不能代替事务及工作事件序号。 */ +static bool Current_Event(const IEC104_Command_t *self, const IEC104_CommandEvent_t *event) +{ + const CommandTransaction *task = &self->transaction; + return event != NULL && task->occupied && event->instance_id == self->serial && + event->transaction_id == task->transaction_id && event->event_serial == task->event_serial && + event->type == task->event_type && task->event_taken; +} + +/* work=true用于未消费的SELECT/EXECUTE;false用于启动成功后可多次提交返回信息的EXECUTE令牌。 */ +static bool Reply_Stage(const IEC104_Command_t *self, const IEC104_CommandEvent_t *event, bool work) +{ + const CommandTransaction *task = &self->transaction; + if (!Current_Event(self, event)) { return false; } + if (work) { + return task->accepted_event_serial == 0U && + ((event->type == IEC104_COMMAND_SELECT && task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT) || + (event->type == IEC104_COMMAND_EXECUTE && task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT)); + } + return event->type == IEC104_COMMAND_EXECUTE && task->phase == IEC104_COMMAND_PHASE_EXECUTING && + task->accepted_event_serial == event->event_serial && task->execute_effect; +} + +static IEC104_CommandReplyResult_t Check_Reply(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, bool work) +{ + IEC104_SlaveRequestState_t state; + if (!Owned(self) || self->in_action || event == NULL) { return IEC104_COMMAND_REPLY_ERROR; } + if (!Reply_Stage(self, event, work)) { return IEC104_COMMAND_REPLY_STALE; } + state = Check_Session(self); + if (state == IEC104_SLAVE_REQUEST_ERROR) { return IEC104_COMMAND_REPLY_ERROR; } + if (state == IEC104_SLAVE_REQUEST_WAIT_EVENTS) { return IEC104_COMMAND_REPLY_WAIT; } + if (state != IEC104_SLAVE_REQUEST_READY) { return IEC104_COMMAND_REPLY_STALE; } + Expire(self, IEC104_Net_Now_Ms()); + return Reply_Stage(self, event, work) ? IEC104_COMMAND_REPLY_OK : IEC104_COMMAND_REPLY_STALE; +} + +IEC104_CommandReplyResult_t IEC104_Command_Accept(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandAction_t action, void *context) +{ + CommandTransaction *task; + IEC104_CommandRequest_t request; + IEC104_CommandActionResult_t action_result; + IEC104_CommandReplyResult_t checked; + bool selecting; + if (event != NULL && event->type == IEC104_COMMAND_EXECUTE && action == NULL) { + return IEC104_COMMAND_REPLY_ERROR; + } + checked = Check_Reply(self, event, true); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + task = &self->transaction; + selecting = task->event_type == IEC104_COMMAND_SELECT; + /* 确认资源和序号在动作前准备。事务自己的回复不会被独立短回复洪泛挤占。 */ + if (!Set_Confirm(self, IEC60870_ASDU_COT_ACTIVATION_CON, false, !selecting)) { + return IEC104_COMMAND_REPLY_ERROR; + } + Copy_Request(task, &request); + task->accepted_event_serial = task->event_serial; + self->in_action = true; + action_result = action == NULL ? IEC104_COMMAND_ACTION_STARTED : action(context, &request); + self->in_action = false; + /* 非法动作返回值按可能有副作用的失败处理;已消费的动作绝不重试。 */ + if (action_result != IEC104_COMMAND_ACTION_STARTED && action_result != IEC104_COMMAND_ACTION_NOT_STARTED) { + action_result = IEC104_COMMAND_ACTION_FAILED; + } + if (selecting) { task->select_effect = action != NULL && action_result != IEC104_COMMAND_ACTION_NOT_STARTED; } + else { task->execute_effect = action_result != IEC104_COMMAND_ACTION_NOT_STARTED; } + if (action_result == IEC104_COMMAND_ACTION_STARTED) { + task->phase = selecting ? IEC104_COMMAND_PHASE_SELECTED : IEC104_COMMAND_PHASE_EXECUTING; + task->timer_start_ms = IEC104_Net_Now_Ms(); + if (selecting) { ++self->status.selected; } + else { ++self->status.executed; } + } else { + task->confirm_negative = true; + task->confirm_try = !selecting && action_result == IEC104_COMMAND_ACTION_FAILED; + if (action_result == IEC104_COMMAND_ACTION_NOT_STARTED) { + ++self->status.app_rejected; + Finish(self, IEC104_COMMAND_FINISH_REJECTED); + } else { Finish(self, IEC104_COMMAND_FINISH_ACTION_FAILED); } + } + /* 提交可能接收STOP或产生故障;OK仍只描述动作交互已消费,应用继续读取FINISHED。 */ + (void)Submit_Head(self); + return IEC104_COMMAND_REPLY_OK; +} + +IEC104_CommandReplyResult_t IEC104_Command_Reject(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandRejectCause_t cause) +{ + IEC104_CommandReplyResult_t checked; + if (cause != IEC104_COMMAND_REJECT_NEGATIVE && cause != IEC104_COMMAND_REJECT_UNKNOWN_IOA) { + return IEC104_COMMAND_REPLY_ERROR; + } + checked = Check_Reply(self, event, true); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + if (!Set_Confirm(self, cause == IEC104_COMMAND_REJECT_UNKNOWN_IOA ? + IEC60870_ASDU_COT_UNKNOWN_IOA : IEC60870_ASDU_COT_ACTIVATION_CON, true, false)) { + return IEC104_COMMAND_REPLY_ERROR; + } + self->transaction.accepted_event_serial = self->transaction.event_serial; + ++self->status.app_rejected; + Finish(self, IEC104_COMMAND_FINISH_REJECTED); + (void)Submit_Head(self); + return IEC104_COMMAND_REPLY_OK; +} + +IEC104_CommandReplyResult_t IEC104_Command_Report_Result(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result) +{ + IEC104_CommandReplyResult_t checked; + if (result != IEC104_COMMAND_RESULT_SUCCEEDED && result != IEC104_COMMAND_RESULT_FAILED) { + return IEC104_COMMAND_REPLY_ERROR; + } + checked = Check_Reply(self, event, false); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + if (self->transaction.result != 0U || self->transaction.completed) { return IEC104_COMMAND_REPLY_ERROR; } + self->transaction.result = (uint8_t)result; + return IEC104_COMMAND_REPLY_OK; +} + +IEC104_CommandReplyResult_t IEC104_Command_Submit_Return_Info(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, const IEC60870_Asdu_t *asdu) +{ + CommandTransaction *task; + IEC60870_AsduInfo_t info; + const uint8_t *data; + uint16_t length; + IEC104_TcpSubmitResult_t submitted; + IEC104_CommandReplyResult_t checked = Check_Reply(self, event, false); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + task = &self->transaction; + if (task->completed || asdu == NULL || !IEC60870_AppLayerParameters_Valid(asdu->parameters) || + asdu->parameters->sizeOfCOT != 2 || asdu->parameters->sizeOfCA != 2 || asdu->parameters->sizeOfIOA != 3 || + asdu->parameters->maxSizeOfASDU > IEC104_MAX_ASDU_LEN || + IEC60870_Asdu_Get_Data(asdu, &data, &length) != IEC60870_ASDU_OK || length > IEC104_MAX_ASDU_LEN || + IEC60870_Asdu_Parse(IEC104_Slave_Get_Application_Layer_Parameters(self->slave), data, length, &info) != + IEC60870_ASDU_OK || + !((info.type_id >= 1U && info.type_id <= 21U) || (info.type_id >= 30U && info.type_id <= 40U)) || + info.object_count == 0U || + (info.cause_of_transmission != IEC60870_ASDU_COT_RETURN_INFO_REMOTE && + info.cause_of_transmission != IEC60870_ASDU_COT_SPONTANEOUS) || + info.is_negative_confirm || info.is_test != ((task->request_bytes[2] >> 7) & 1U) || + info.common_address != Read_CA(task->request_bytes)) { return IEC104_COMMAND_REPLY_ERROR; } + /* 底层Try只知道传输队列;这里还必须检查模块内更早的独立/事务应答。 */ + if (!task->confirm_submitted || Pending_Replies(self) != 0U) { return IEC104_COMMAND_REPLY_WAIT; } + submitted = IEC104_Slave_Try_Send_Response(self->slave, &task->identity, asdu); + if (submitted == IEC104_TCP_SUBMIT_WAIT) { + task->return_waiting = true; + ++self->status.waits; + return IEC104_COMMAND_REPLY_WAIT; + } + if (submitted == IEC104_TCP_SUBMIT_OK) { + task->return_waiting = false; + ++self->status.submitted; + ++self->status.return_info_submitted; + return IEC104_COMMAND_REPLY_OK; + } + if (Defer_For_Stop(self, &task->identity, submitted)) { return IEC104_COMMAND_REPLY_WAIT; } + Cancel_All(self, IEC104_COMMAND_FINISH_SEND_ERROR); + self->status.active = false; + return IEC104_COMMAND_REPLY_STALE; +} + +IEC104_CommandReplyResult_t IEC104_Command_Complete(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event) +{ + CommandTransaction *task; + IEC104_CommandReplyResult_t checked = Check_Reply(self, event, false); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + task = &self->transaction; + if (task->result == 0U || task->completed) { return IEC104_COMMAND_REPLY_ERROR; } + if (self->config.send_act_term && + (task->result == IEC104_COMMAND_RESULT_SUCCEEDED || self->config.act_term_on_failure)) { + uint64_t serial = Reply_Serial(self); + if (serial == 0U) { return IEC104_COMMAND_REPLY_ERROR; } + task->term_serial = serial; + } + task->completed = true; + task->return_waiting = false; + /* 无TERM且确认已交接可立即结束;否则Process只重试协议收尾,不要求应用重复Complete。 */ + Complete_If_Sent(self); + return IEC104_COMMAND_REPLY_OK; +} + +bool IEC104_Command_Get_Transaction(const IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandTransactionInfo_t *info) +{ + const CommandTransaction *task; + IEC104_CommandTransactionInfo_t result; + bool delivered; + if (!Owned(self) || event == NULL || info == NULL) { return false; } + task = &self->transaction; + if (!task->occupied || event->instance_id != self->serial || event->transaction_id != task->transaction_id) { + return false; + } + delivered = (event->type == IEC104_COMMAND_SELECT && task->select_delivered && + event->event_serial == task->select_event_serial) || + (event->type == IEC104_COMMAND_EXECUTE && task->execute_delivered && + event->event_serial == task->execute_event_serial) || + (event->type == IEC104_COMMAND_FINISHED && task->event_type == IEC104_COMMAND_FINISHED && + task->event_taken && event->event_serial == task->event_serial); + if (!delivered) { return false; } + memset(&result, 0, sizeof(result)); + result.active = task->phase != IEC104_COMMAND_PHASE_ENDED; + result.phase = (IEC104_CommandPhase_t)task->phase; + result.confirm_submitted = task->confirm_submitted; + result.result_reported = task->result != 0U; + result.completed = task->completed; + result.term_submitted = task->term_submitted; + *info = result; + return true; +} + +bool IEC104_Command_Get_Status(const IEC104_Command_t *self, IEC104_CommandStatus_t *status) +{ + const CommandTransaction *task; + IEC104_CommandStatus_t result; + IEC104_SlaveRequestState_t state; + uint32_t limit; + uint64_t now; + unsigned slot = 0U, head; + bool waiting; + if (!Owned(self) || status == NULL) { return false; } + state = IEC104_Slave_Check_Request(self->slave, &self->session); + if (state == IEC104_SLAVE_REQUEST_ERROR) { return false; } + task = &self->transaction; + result = self->status; + result.generation = self->session.generation; + result.events_pending = state == IEC104_SLAVE_REQUEST_WAIT_EVENTS; + result.busy = task->occupied; + result.pending_replies = Pending_Replies(self); + result.pending_events = task->occupied && (task->finish_pending || + (!task->event_taken && (task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT || + task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT))) ? 1U : 0U; + limit = Timer_Limit(self); + now = IEC104_Net_Now_Ms(); + result.timer_active = limit != 0U; + result.timeout_remaining_ms = limit != 0U ? Remaining(task, limit, now) : 0U; + result.need_process = false; + head = Reply_Head(self, &slot); + waiting = head == COMMAND_HEAD_SHORT ? self->replies[slot].send_waiting : task->send_waiting; + if (!self->in_action && !result.events_pending) { + if (state != IEC104_SLAVE_REQUEST_READY || !result.active) { + result.need_process = result.pending_replies != 0U || + (task->occupied && task->phase != IEC104_COMMAND_PHASE_ENDED); + } else { + result.need_process = Clock_Reversed(self, now) || + (result.timer_active && result.timeout_remaining_ms == 0U) || + (head != COMMAND_HEAD_NONE && !waiting); + } + } + *status = result; + return true; +} diff --git a/IEC60870-Slave/.build/command-comments/IEC104_Command.h.before b/IEC60870-Slave/.build/command-comments/IEC104_Command.h.before new file mode 100644 index 00000000..0d1d9a21 --- /dev/null +++ b/IEC60870-Slave/.build/command-comments/IEC104_Command.h.before @@ -0,0 +1,202 @@ +/* + * IEC104遥控:单命令45、双命令46,COT=6激活、COT=8撤销。 + * 每个Slave至多一个实例,每实例至多一个未释放事务;其他业务仍独立推进。 + * 静态存储、串行事件循环;仅Accept显式调用一次应用动作,不保存回调。 + * 返回信息由Submit_Return_Info沿原连接提交,确认/返回信息/终止保持顺序。 + */ +#ifndef IEC104_COMMAND_H +#define IEC104_COMMAND_H + +#include "IEC104_Slave.h" + +#ifdef __cplusplus +extern "C" { +#endif + +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仅禁用时长限制,等待阶段遇到时钟倒退仍终结。 */ +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。 */ + bool act_term_on_failure; /* 默认false;true且send_act_term时失败也发COT=10、P/N=0。 */ +} IEC104_CommandConfig_t; + +/* 纯值请求。state编码为单命令0/1、双命令1/2;具体操作含义由应用点表定义。 + * accepted_ms为Handle_Event受理时刻,不是报文在网络上的实际到达时刻。 */ +typedef struct { + uint64_t generation, receive_serial; + uint64_t station_id; + uint16_t common_address; + uint8_t originator_address, type_id; + uint32_t ioa; + bool is_select, selected; + uint8_t state, qualifier; + uint64_t accepted_ms; +} IEC104_CommandRequest_t; + +typedef enum { + IEC104_COMMAND_SELECT = 1, + IEC104_COMMAND_EXECUTE, + IEC104_COMMAND_FINISHED +} IEC104_CommandEventType_t; + +typedef enum { + IEC104_COMMAND_FINISH_NONE = 0, + IEC104_COMMAND_FINISH_COMPLETED, + IEC104_COMMAND_FINISH_EXECUTION_FAILED, + IEC104_COMMAND_FINISH_REJECTED, + IEC104_COMMAND_FINISH_ACTION_FAILED, + IEC104_COMMAND_FINISH_MISMATCH, + IEC104_COMMAND_FINISH_DEACTIVATED, + IEC104_COMMAND_FINISH_SELECT_TIMEOUT, + IEC104_COMMAND_FINISH_DECISION_TIMEOUT, + IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT, + IEC104_COMMAND_FINISH_CANCELLED, + IEC104_COMMAND_FINISH_STOPPED, + IEC104_COMMAND_FINISH_DISCONNECTED, + IEC104_COMMAND_FINISH_CONNECTION_CHANGED, + IEC104_COMMAND_FINISH_SEND_ERROR, + IEC104_COMMAND_FINISH_SERVICE_ERROR +} 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; + bool select_effect, execute_effect; +} IEC104_CommandEvent_t; + +typedef enum { + IEC104_COMMAND_ACTION_NOT_STARTED = 0, /* 必须保证没有任何副作用。 */ + IEC104_COMMAND_ACTION_STARTED, /* 选择生效或输出启动;输出结果另行报告。 */ + IEC104_COMMAND_ACTION_FAILED /* 未报告启动成功,但可能已留下副作用。 */ +} IEC104_CommandActionResult_t; + +/* 须短时返回;request只在本次调用有效,不得保存指针、推进Slave或发布上报。 + * 回调期间仅可Get_Status/Get_Transaction;其他修改入口拒绝重入。 */ +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, + IEC104_COMMAND_REJECT_UNKNOWN_IOA +} IEC104_CommandRejectCause_t; +typedef enum { + IEC104_COMMAND_EVENT_ERROR = -1, + IEC104_COMMAND_EVENT_IGNORED = 0, + IEC104_COMMAND_EVENT_HANDLED = 1 +} IEC104_CommandEventResult_t; +typedef enum { + IEC104_COMMAND_NEXT_ERROR = -1, + IEC104_COMMAND_NEXT_EMPTY = 0, + IEC104_COMMAND_NEXT_READY = 1 +} IEC104_CommandNextResult_t; +typedef enum { + IEC104_COMMAND_REPLY_STALE = -2, + IEC104_COMMAND_REPLY_ERROR = -1, + IEC104_COMMAND_REPLY_WAIT = 0, + IEC104_COMMAND_REPLY_OK = 1 +} IEC104_CommandReplyResult_t; +typedef enum { + IEC104_COMMAND_PHASE_SELECT_WAIT = 1, + IEC104_COMMAND_PHASE_SELECTED, + IEC104_COMMAND_PHASE_EXECUTE_WAIT, + IEC104_COMMAND_PHASE_EXECUTING, + IEC104_COMMAND_PHASE_ENDED +} IEC104_CommandPhase_t; + +typedef struct { + bool active; /* 尚未ENDED;不是当前连接是否可操作的保证。 */ + IEC104_CommandPhase_t phase; + bool confirm_submitted; /* 当前工作请求的确认已交给发送层,不表示写完或ACK。 */ + bool result_reported, completed, term_submitted; +} IEC104_CommandTransactionInfo_t; + +/* 累计计数从Create起维护;查询不改变阶段、不做超时转换。 + * busy包括待消费FINISHED和未交接的事务应答,不包含独立短应答。 + * need_process不因发送WAIT持续置true;每轮网络推进后仍须Process重试。 */ +typedef struct { + uint64_t generation; + bool active, events_pending, need_process, timer_active; + bool busy; + uint32_t timeout_remaining_ms; + unsigned pending_replies, pending_events; + unsigned long accepted, selected, executed, completed, execution_failed, action_failed; + unsigned long submitted, negative_submitted, return_info_submitted, waits; + unsigned long rejected, app_rejected, busy_rejected, overloads, dropped_requests, cancelled; + unsigned long deactivated, mismatches, select_timeouts, decision_timeouts, execute_timeouts; + unsigned long clock_errors; +} IEC104_CommandStatus_t; + +void IEC104_Command_Config_Default(IEC104_CommandConfig_t *config); +/* config按值复制,必须非NULL;slave寿命覆盖实例。创建早于消费Slave通知。 + * 池容量IEC104_COMMAND_CAPACITY默认等于TCP服务容量;独立应答容量 + * IEC104_COMMAND_REPLY_CAPACITY默认4。配置/归属非法、重复绑定或池耗尽返回NULL。 */ +IEC104_Command_t *IEC104_Command_Create(IEC104_Slave_t *slave, const IEC104_CommandConfig_t *config); +/* 事务槽、未读事件和全部独立应答槽均空才成功;不隐式Cancel/Close。 */ +bool IEC104_Command_Destroy(IEC104_Command_t *self); +/* 结束事务并保留欠应用的FINISHED;清空未提交应答,保持会话激活,不回滚动作。 + * 回调中/无效句柄无操作。ENDED事务保持首次终结原因,不重复产生FINISHED。 */ +void IEC104_Command_Cancel(IEC104_Command_t *self); +/* 按Slave FIFO分派全部连接/启停通知;通知消费不受后续WAIT_EVENTS阻挡。 + * 仅45/46 I帧接手;非法请求可否定或丢新计数,HANDLED不表示已应答。 */ +IEC104_CommandEventResult_t IEC104_Command_Handle_Event(IEC104_Command_t *self, + const IEC104_SlaveEvent_t *event); +/* 有界处理期限和全局FIFO队首;不调用应用。true包含等待或已终结,false为服务/参数错误。 */ +bool IEC104_Command_Process(IEC104_Command_t *self); +/* READY才修改event。工作事件先检查会话/期限;已有FINISHED可在离线时读取。 + * 最后FINISHED读取可能释放事务站点引用;应用自己的引用仍需自行归还。 */ +IEC104_CommandNextResult_t IEC104_Command_NextEvent(IEC104_Command_t *self, + IEC104_CommandEvent_t *event); +/* OK仅表示工作事件已消费,动作最多执行一次,不表示动作成功或确认已交接。 + * SELECT的action可为NULL,选择生效但select_effect=false;EXECUTE必须提供action。 + * WAIT未消费,STALE不得重试,ERROR为参数/重入错误且未调用动作。 */ +IEC104_CommandReplyResult_t IEC104_Command_Accept(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandAction_t action, void *context); +IEC104_CommandReplyResult_t IEC104_Command_Reject(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandRejectCause_t cause); +/* 以下三个仅接受启动成功的原EXECUTE事件。结果报告一次后冻结;重复报告ERROR。 + * Report_Result可早于ACT_CON交接,OK即停止执行期限。 */ +IEC104_CommandReplyResult_t IEC104_Command_Report_Result(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result); +/* 监视类型1..21/30..40,COT=11或3,P/N=0,TEST/CA与命令一致;完整校验对象。 + * 模块有更早待交应答、确认未交接、网络背压或未读通知时WAIT,不缓存ASDU、也不占排序位置。 + * OK固定帧序,不可重交;Complete后仍待收尾时ERROR,事务已结束则STALE。 + * 发送错误终结事务,不可重试旧ASDU。 */ +IEC104_CommandReplyResult_t IEC104_Command_Submit_Return_Info(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, const IEC60870_Asdu_t *asdu); +/* 结果须已报告;OK立即关闭返回阶段、冻结终止策略,Process负责后续发送。 + * 仍待收尾时重复调用ERROR,事务已结束则STALE;WAIT未消费。 + * 失败配置发TERM时P/N=0表示流程结束,不能据此认定设备成功。 */ +IEC104_CommandReplyResult_t IEC104_Command_Complete(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event); +/* 可查询仍保留事务的任一已交付事件;ENDED返回active=false。 + * 槽已回收/事件不属于事务时false且不改info,不保留历史事务。 */ +bool IEC104_Command_Get_Transaction(const IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandTransactionInfo_t *info); +bool IEC104_Command_Get_Status(const IEC104_Command_t *self, IEC104_CommandStatus_t *status); + +#ifdef __cplusplus +} +#endif +#endif /* IEC104_COMMAND_H */ diff --git a/IEC60870-Slave/.build/command-comments/check-comments.cjs b/IEC60870-Slave/.build/command-comments/check-comments.cjs new file mode 100644 index 00000000..ebe0c07a --- /dev/null +++ b/IEC60870-Slave/.build/command-comments/check-comments.cjs @@ -0,0 +1,66 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const assert = require('node:assert/strict'); +const root = path.resolve(__dirname, '../..'); +const files = [ + '104/business/IEC104_Command.c', + '104/business/IEC104_Command.h', + 'examples/iec104_slave_example.c', + 'test/iec104_command_test.c', +]; + +// Preserve literals and token boundaries while ignoring only comments/whitespace. +function tokens(source) { + const input = source.replace(/\\\r?\n/g, ''); + const result = []; + let offset = 0; + while (offset < input.length) { + if (/\s/.test(input[offset])) { offset++; continue; } + if (input.startsWith('//', offset)) { + const end = input.indexOf('\n', offset + 2); + offset = end < 0 ? input.length : end + 1; + continue; + } + if (input.startsWith('/*', offset)) { + const end = input.indexOf('*/', offset + 2); + assert(end >= 0, 'unterminated comment'); + offset = end + 2; + continue; + } + const tail = input.slice(offset); + const literal = /^(?:u8|u|U|L)?(?:"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*')/.exec(tail); + const word = literal || /^(?:[A-Za-z_][A-Za-z_0-9]*|(?:[0-9]|\.[0-9])(?:[eEpP][+-]|[A-Za-z_0-9.])*)/.exec(tail); + const token = word || /^(?:>>=|<<=|\.\.\.|->|\+\+|--|<<|>>|<=|>=|==|!=|&&|\|\||\*=|\/=|%=|\+=|-=|&=|\^=|\|=|##|.)/.exec(tail); + assert(token, `cannot tokenize at ${offset}`); + result.push(token[0]); + offset += token[0].length; + } + return result; +} + +for (const file of files) { + const originalBytes = fs.readFileSync(path.join(__dirname, `${path.basename(file)}.before`)); + const currentBytes = fs.readFileSync(path.join(root, file)); + const decoder = new TextDecoder('utf-8', { fatal: true }); + const original = decoder.decode(originalBytes); + const current = decoder.decode(currentBytes); + assert.deepEqual(tokens(current), tokens(original), `${file}: code tokens changed`); + assert.equal(currentBytes.subarray(0, 3).equals(Buffer.from([0xef, 0xbb, 0xbf])), + originalBytes.subarray(0, 3).equals(Buffer.from([0xef, 0xbb, 0xbf])), `${file}: BOM changed`); + assert(!current.includes('\r'), `${file}: CR line endings`); + assert(!current.includes('\ufffd'), `${file}: replacement character`); + assert(!/[\t ]+$/m.test(current), `${file}: trailing whitespace`); + assert(current.endsWith('\n'), `${file}: missing terminal newline`); + console.log(`${file}: ${tokens(current).length} unchanged tokens, UTF-8/LF OK`); +} + +// The loopback update only adds standalone line comments; compare all other bytes. +const loopbackPath = 'test/slave_example_loopback_test.cjs'; +const loopbackBefore = fs.readFileSync(path.join(__dirname, 'slave_example_loopback_test.cjs.before'), 'utf8'); +const loopbackAfter = fs.readFileSync(path.join(root, loopbackPath), 'utf8'); +const withoutCommentLines = (source) => source.split('\n').filter(line => !/^\s*\/\//.test(line)).join('\n'); +assert.equal(withoutCommentLines(loopbackAfter), withoutCommentLines(loopbackBefore)); +assert(!loopbackAfter.includes('\r') && !loopbackAfter.includes('\ufffd')); +assert(!/[\t ]+$/m.test(loopbackAfter) && loopbackAfter.endsWith('\n')); +new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(path.join(root, loopbackPath))); +console.log(`${loopbackPath}: only standalone comments changed, UTF-8/LF OK`); diff --git a/IEC60870-Slave/.build/command-comments/command-after.i b/IEC60870-Slave/.build/command-comments/command-after.i new file mode 100644 index 00000000..2e5b900f --- /dev/null +++ b/IEC60870-Slave/.build/command-comments/command-after.i @@ -0,0 +1,2425 @@ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" + + + + +#pragma pack(push,_CRT_PACKING) + typedef __builtin_va_list __gnuc_va_list; + typedef __gnuc_va_list va_list; +#pragma pack(pop) +void __attribute__((__cdecl__)) __debugbreak(void); +extern __inline__ __attribute__((__always_inline__,__gnu_inline__)) void __attribute__((__cdecl__)) __debugbreak(void) +{ + __asm__ __volatile__("int {$}3":); +} +void __attribute__((__cdecl__)) __attribute__ ((__noreturn__)) __fastfail(unsigned int _Code); +extern __inline__ __attribute__((__always_inline__,__gnu_inline__)) void __attribute__((__cdecl__)) __attribute__ ((__noreturn__)) __fastfail(unsigned int _Code) +{ + __asm__ __volatile__("int {$}0x29"::"c"(_Code)); + __builtin_unreachable(); +} +const char *__mingw_get_crt_info (void); +#pragma pack(push,_CRT_PACKING) +__extension__ typedef unsigned long long size_t; +__extension__ typedef long long ssize_t; +typedef size_t rsize_t; +__extension__ typedef long long intptr_t; +__extension__ typedef unsigned long long uintptr_t; +__extension__ typedef long long ptrdiff_t; +typedef unsigned short wchar_t; +typedef unsigned short wint_t; +typedef unsigned short wctype_t; +typedef int errno_t; +typedef long __time32_t; +__extension__ typedef long long __time64_t; +typedef __time64_t time_t; +__attribute__ ((__dllimport__)) void __attribute__((__cdecl__)) _invalid_parameter_noinfo(void); +__attribute__ ((__dllimport__)) __attribute__ ((__noreturn__)) void __attribute__((__cdecl__)) _invalid_parameter_noinfo_noreturn(void); +__attribute__ ((__dllimport__)) __attribute__ ((__noreturn__)) void __attribute__((__cdecl__)) _invoke_watson(const wchar_t *expression, const wchar_t *function_name, const wchar_t *file_name, unsigned int line_number, long long unsigned int reserved); +struct threadlocaleinfostruct; +struct threadmbcinfostruct; +typedef struct threadlocaleinfostruct *pthreadlocinfo; +typedef struct threadmbcinfostruct *pthreadmbcinfo; +struct __lc_time_data; +typedef struct localeinfo_struct { + pthreadlocinfo locinfo; + pthreadmbcinfo mbcinfo; +} _locale_tstruct,*_locale_t; +typedef struct tagLC_ID { + unsigned short wLanguage; + unsigned short wCountry; + unsigned short wCodePage; +} LC_ID,*LPLC_ID; +typedef struct threadlocaleinfostruct { + const unsigned short *_locale_pctype; + int _locale_mb_cur_max; + unsigned int _locale_lc_codepage; +} threadlocinfo; +#pragma pack(pop) + __attribute__ ((__dllimport__)) extern int *__attribute__((__cdecl__)) _errno(void); + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _set_errno(int _Value); + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _get_errno(int *_Value); + __attribute__ ((__dllimport__)) extern unsigned long __attribute__((__cdecl__)) __threadid(void); + __attribute__ ((__dllimport__)) extern uintptr_t __attribute__((__cdecl__)) __threadhandle(void); +typedef struct { + long long __max_align_ll __attribute__((__aligned__(__alignof__(long long)))); + long double __max_align_ld __attribute__((__aligned__(__alignof__(long double)))); +} max_align_t; +typedef signed char int8_t; +typedef unsigned char uint8_t; +typedef short int16_t; +typedef unsigned short uint16_t; +typedef int int32_t; +typedef unsigned uint32_t; +__extension__ typedef long long int64_t; +__extension__ typedef unsigned long long uint64_t; +typedef signed char int_least8_t; +typedef unsigned char uint_least8_t; +typedef short int_least16_t; +typedef unsigned short uint_least16_t; +typedef int int_least32_t; +typedef unsigned uint_least32_t; +__extension__ typedef long long int_least64_t; +__extension__ typedef unsigned long long uint_least64_t; +typedef signed char int_fast8_t; +typedef unsigned char uint_fast8_t; +typedef short int_fast16_t; +typedef unsigned short uint_fast16_t; +typedef int int_fast32_t; +typedef unsigned int uint_fast32_t; +__extension__ typedef long long int_fast64_t; +__extension__ typedef unsigned long long uint_fast64_t; +__extension__ typedef long long intmax_t; +__extension__ typedef unsigned long long uintmax_t; +#pragma GCC diagnostic pop +typedef struct { + int sizeOfTypeId; + int sizeOfVSQ; + int sizeOfCOT; + int originatorAddress; + int sizeOfCA; + int sizeOfIOA; + int maxSizeOfASDU; +} IEC60870_AppLayerParameters_t; +typedef struct { + uint8_t type_id; + uint8_t object_count; + uint8_t is_sequence; + uint8_t cause_of_transmission; + uint8_t is_negative_confirm; + uint8_t is_test; + uint8_t originator_address; + uint16_t common_address; + const uint8_t *information_objects; + uint16_t information_objects_len; + const IEC60870_AppLayerParameters_t *parameters; +} IEC60870_AsduInfo_t; +extern int IEC60870_AppLayerParameters_Valid(const IEC60870_AppLayerParameters_t *parameters); +extern int IEC60870_Parse_ASDU_Header(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *asdu, + uint16_t asdu_len, + IEC60870_AsduInfo_t *asdu_info); +typedef struct { + uint16_t milliseconds; +} IEC60870_CP16Time2a_t; +typedef struct { + uint16_t milliseconds; + uint8_t minute; + uint8_t is_invalid; +} IEC60870_CP24Time2a_t; +int IEC60870_Time_To_CP16(const IEC60870_CP16Time2a_t *value, uint8_t *out, size_t capacity); +int IEC60870_Time_To_CP24(const IEC60870_CP24Time2a_t *value, uint8_t *out, size_t capacity); +int IEC60870_Time_From_CP16(const uint8_t *in, size_t length, IEC60870_CP16Time2a_t *value); +int IEC60870_Time_From_CP24(const uint8_t *in, size_t length, IEC60870_CP24Time2a_t *value); +typedef struct { + uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; + uint8_t second; + uint16_t ms; + uint8_t weekday; +} IEC60870_WallTime_t; +typedef struct { + IEC60870_WallTime_t time; + uint8_t is_invalid; + uint8_t is_summer_time; + uint8_t has_weekday; +} IEC60870_CP56Time2a_t; +int IEC60870_Time_Is_Valid(const IEC60870_WallTime_t *time); +int IEC60870_Time_Get_Weekday(uint16_t year, uint8_t month, uint8_t day, uint8_t *weekday); +int IEC60870_Time_To_Seconds(const IEC60870_WallTime_t *time, uint32_t *seconds); +int IEC60870_Time_From_Seconds(uint32_t seconds, uint16_t ms, IEC60870_WallTime_t *time); +int IEC60870_Time_Seconds_To_Local(uint32_t utc_seconds, uint16_t ms, + IEC60870_WallTime_t *local_time); +int IEC60870_Time_CP56_To_Seconds(const IEC60870_CP56Time2a_t *value, + int16_t standard_utc_offset_minutes, uint32_t *seconds); +int IEC60870_Time_CP56_To_Local(const IEC60870_CP56Time2a_t *value, + int16_t source_standard_utc_offset_minutes, + IEC60870_WallTime_t *local_time); +int IEC60870_Time_To_CP56(const IEC60870_CP56Time2a_t *value, uint8_t *out, size_t capacity); +int IEC60870_Time_From_CP56(const uint8_t *in, size_t length, IEC60870_CP56Time2a_t *value); +typedef enum { + IEC60870_ASDU_UNSUPPORTED = -2, + IEC60870_ASDU_ERROR = -1, + IEC60870_ASDU_FULL = 0, + IEC60870_ASDU_OK = 1 +} IEC60870_AsduResult_t; +typedef enum { + IEC60870_ASDU_M_SP_NA_1 = 1, + IEC60870_ASDU_M_SP_TA_1 = 2, + IEC60870_ASDU_M_DP_NA_1 = 3, + IEC60870_ASDU_M_DP_TA_1 = 4, + IEC60870_ASDU_M_ST_NA_1 = 5, + IEC60870_ASDU_M_ST_TA_1 = 6, + IEC60870_ASDU_M_BO_NA_1 = 7, + IEC60870_ASDU_M_BO_TA_1 = 8, + IEC60870_ASDU_M_ME_NA_1 = 9, + IEC60870_ASDU_M_ME_TA_1 = 10, + IEC60870_ASDU_M_ME_NB_1 = 11, + IEC60870_ASDU_M_ME_TB_1 = 12, + IEC60870_ASDU_M_ME_NC_1 = 13, + IEC60870_ASDU_M_ME_TC_1 = 14, + IEC60870_ASDU_M_IT_NA_1 = 15, + IEC60870_ASDU_M_IT_TA_1 = 16, + IEC60870_ASDU_M_EP_TA_1 = 17, + IEC60870_ASDU_M_EP_TB_1 = 18, + IEC60870_ASDU_M_EP_TC_1 = 19, + IEC60870_ASDU_M_PS_NA_1 = 20, + IEC60870_ASDU_M_ME_ND_1 = 21, + IEC60870_ASDU_M_SP_TB_1 = 30, + IEC60870_ASDU_M_DP_TB_1 = 31, + IEC60870_ASDU_M_ST_TB_1 = 32, + IEC60870_ASDU_M_BO_TB_1 = 33, + IEC60870_ASDU_M_ME_TD_1 = 34, + IEC60870_ASDU_M_ME_TE_1 = 35, + IEC60870_ASDU_M_ME_TF_1 = 36, + IEC60870_ASDU_M_IT_TB_1 = 37, + IEC60870_ASDU_M_EP_TD_1 = 38, + IEC60870_ASDU_M_EP_TE_1 = 39, + IEC60870_ASDU_M_EP_TF_1 = 40, + IEC60870_ASDU_C_SC_NA_1 = 45, + IEC60870_ASDU_C_DC_NA_1 = 46, + IEC60870_ASDU_C_RC_NA_1 = 47, + IEC60870_ASDU_C_SE_NA_1 = 48, + IEC60870_ASDU_C_SE_NB_1 = 49, + IEC60870_ASDU_C_SE_NC_1 = 50, + IEC60870_ASDU_C_BO_NA_1 = 51, + IEC60870_ASDU_C_SC_TA_1 = 58, + IEC60870_ASDU_C_DC_TA_1 = 59, + IEC60870_ASDU_C_RC_TA_1 = 60, + IEC60870_ASDU_C_SE_TA_1 = 61, + IEC60870_ASDU_C_SE_TB_1 = 62, + IEC60870_ASDU_C_SE_TC_1 = 63, + IEC60870_ASDU_C_BO_TA_1 = 64, + IEC60870_ASDU_M_EI_NA_1 = 70, + IEC60870_ASDU_C_IC_NA_1 = 100, + IEC60870_ASDU_C_CI_NA_1 = 101, + IEC60870_ASDU_C_RD_NA_1 = 102, + IEC60870_ASDU_C_CS_NA_1 = 103, + IEC60870_ASDU_C_TS_NA_1 = 104, + IEC60870_ASDU_C_RP_NA_1 = 105, + IEC60870_ASDU_C_CD_NA_1 = 106, + IEC60870_ASDU_C_TS_TA_1 = 107, + IEC60870_ASDU_P_ME_NA_1 = 110, + IEC60870_ASDU_P_ME_NB_1 = 111, + IEC60870_ASDU_P_ME_NC_1 = 112, + IEC60870_ASDU_P_AC_NA_1 = 113, + IEC60870_ASDU_F_FR_NA_1 = 120, + IEC60870_ASDU_F_SR_NA_1 = 121, + IEC60870_ASDU_F_SC_NA_1 = 122, + IEC60870_ASDU_F_LS_NA_1 = 123, + IEC60870_ASDU_F_AF_NA_1 = 124, + IEC60870_ASDU_F_SG_NA_1 = 125, + IEC60870_ASDU_F_DR_TA_1 = 126, + IEC60870_ASDU_F_SC_NB_1 = 127 +} IEC60870_AsduType_t; +enum { + IEC60870_ASDU_QUALITY_OV = 0x01, + IEC60870_ASDU_QUALITY_EI = 0x08, + IEC60870_ASDU_QUALITY_BL = 0x10, + IEC60870_ASDU_QUALITY_SB = 0x20, + IEC60870_ASDU_QUALITY_NT = 0x40, + IEC60870_ASDU_QUALITY_IV = 0x80 +}; +typedef struct { + int32_t value; + uint8_t sequence_number; + uint8_t has_carry; + uint8_t is_adjusted; + uint8_t is_invalid; +} IEC60870_BinaryCounterReading_t; +struct IEC60870_InformationObjectVTable; +typedef struct { + uint32_t ioa; + uint8_t type_id; + const struct IEC60870_InformationObjectVTable *virtual_function_table; +} IEC60870_InformationObject_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t value; + uint8_t quality; +} IEC60870_SinglePointInformation_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t value; + uint8_t quality; +} IEC60870_DoublePointInformation_t; +typedef struct { + IEC60870_InformationObject_t object; + int16_t value; + uint8_t quality; +} IEC60870_MeasuredValueNormalized_t; +typedef struct { + IEC60870_InformationObject_t object; + int16_t value; + uint8_t quality; +} IEC60870_MeasuredValueScaled_t; +typedef struct { + IEC60870_InformationObject_t object; + float value; + uint8_t quality; +} IEC60870_MeasuredValueShort_t; +typedef struct { + IEC60870_InformationObject_t object; + IEC60870_BinaryCounterReading_t value; +} IEC60870_IntegratedTotals_t; +typedef struct { + IEC60870_SinglePointInformation_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_SinglePointWithCP56Time2a_t; +typedef struct { + IEC60870_DoublePointInformation_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_DoublePointWithCP56Time2a_t; +typedef struct { + IEC60870_MeasuredValueNormalized_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_MeasuredValueNormalizedWithCP56Time2a_t; +typedef struct { + IEC60870_MeasuredValueScaled_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_MeasuredValueScaledWithCP56Time2a_t; +typedef struct { + IEC60870_MeasuredValueShort_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_MeasuredValueShortWithCP56Time2a_t; +typedef struct { + IEC60870_IntegratedTotals_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_IntegratedTotalsWithCP56Time2a_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t state; + uint8_t is_select; + uint8_t qualifier; +} IEC60870_SingleCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t state; + uint8_t is_select; + uint8_t qualifier; +} IEC60870_DoubleCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t state; + uint8_t is_select; + uint8_t qualifier; +} IEC60870_StepCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + int16_t value; + uint8_t is_select; + uint8_t qualifier; +} IEC60870_SetpointCommandNormalized_t; +typedef struct { + IEC60870_InformationObject_t object; + int16_t value; + uint8_t is_select; + uint8_t qualifier; +} IEC60870_SetpointCommandScaled_t; +typedef struct { + IEC60870_InformationObject_t object; + float value; + uint8_t is_select; + uint8_t qualifier; +} IEC60870_SetpointCommandShort_t; +typedef struct { + IEC60870_SingleCommand_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_SingleCommandWithCP56Time2a_t; +typedef struct { + IEC60870_DoubleCommand_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_DoubleCommandWithCP56Time2a_t; +typedef struct { + IEC60870_StepCommand_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_StepCommandWithCP56Time2a_t; +typedef struct { + IEC60870_SetpointCommandNormalized_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_SetpointCommandNormalizedWithCP56Time2a_t; +typedef struct { + IEC60870_SetpointCommandScaled_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_SetpointCommandScaledWithCP56Time2a_t; +typedef struct { + IEC60870_SetpointCommandShort_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_SetpointCommandShortWithCP56Time2a_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t qualifier; +} IEC60870_InterrogationCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t qualifier; + uint8_t freeze; +} IEC60870_CounterInterrogationCommand_t; +typedef struct { + IEC60870_InformationObject_t object; +} IEC60870_ReadCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + IEC60870_CP56Time2a_t time; +} IEC60870_ClockSynchronizationCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t qualifier; +} IEC60870_ResetProcessCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t sequence; + IEC60870_CP56Time2a_t time; +} IEC60870_TestCommandWithCP56Time2a_t; +typedef struct { + IEC60870_InformationObject_t object; + int8_t value; + uint8_t is_transient; + uint8_t quality; +} IEC60870_StepPositionInformation_t; +typedef struct { + IEC60870_InformationObject_t object; + uint32_t value; + uint8_t quality; +} IEC60870_BitString32_t; +typedef struct { + IEC60870_SinglePointInformation_t base; + IEC60870_CP24Time2a_t time; +} IEC60870_SinglePointWithCP24Time2a_t; +typedef struct { + IEC60870_DoublePointInformation_t base; + IEC60870_CP24Time2a_t time; +} IEC60870_DoublePointWithCP24Time2a_t; +typedef struct { + IEC60870_StepPositionInformation_t base; + IEC60870_CP24Time2a_t time; +} IEC60870_StepPositionWithCP24Time2a_t; +typedef struct { + IEC60870_BitString32_t base; + IEC60870_CP24Time2a_t time; +} IEC60870_Bitstring32WithCP24Time2a_t; +typedef struct { + IEC60870_MeasuredValueNormalized_t base; + IEC60870_CP24Time2a_t time; +} IEC60870_MeasuredValueNormalizedWithCP24Time2a_t; +typedef struct { + IEC60870_MeasuredValueScaled_t base; + IEC60870_CP24Time2a_t time; +} IEC60870_MeasuredValueScaledWithCP24Time2a_t; +typedef struct { + IEC60870_MeasuredValueShort_t base; + IEC60870_CP24Time2a_t time; +} IEC60870_MeasuredValueShortWithCP24Time2a_t; +typedef struct { + IEC60870_IntegratedTotals_t base; + IEC60870_CP24Time2a_t time; +} IEC60870_IntegratedTotalsWithCP24Time2a_t; +typedef struct { + IEC60870_StepPositionInformation_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_StepPositionWithCP56Time2a_t; +typedef struct { + IEC60870_BitString32_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_Bitstring32WithCP56Time2a_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t event; + uint8_t quality; + IEC60870_CP16Time2a_t elapsed_time; + IEC60870_CP24Time2a_t time; +} IEC60870_EventOfProtectionEquipment_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t event; + uint8_t quality; + IEC60870_CP16Time2a_t elapsed_time; + IEC60870_CP24Time2a_t time; +} IEC60870_PackedStartEventsOfProtectionEquipment_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t event; + uint8_t quality; + IEC60870_CP16Time2a_t elapsed_time; + IEC60870_CP24Time2a_t time; +} IEC60870_PackedOutputCircuitInfo_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t event; + uint8_t quality; + IEC60870_CP16Time2a_t elapsed_time; + IEC60870_CP56Time2a_t time; +} IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t event; + uint8_t quality; + IEC60870_CP16Time2a_t elapsed_time; + IEC60870_CP56Time2a_t time; +} IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t event; + uint8_t quality; + IEC60870_CP16Time2a_t elapsed_time; + IEC60870_CP56Time2a_t time; +} IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t status; + uint16_t change; + uint8_t quality; +} IEC60870_PackedSinglePointWithSCD_t; +typedef struct { + IEC60870_InformationObject_t object; + int16_t value; +} IEC60870_MeasuredValueNormalizedWithoutQuality_t; +typedef struct { + IEC60870_InformationObject_t object; + uint32_t value; +} IEC60870_Bitstring32Command_t; +typedef struct { + IEC60870_Bitstring32Command_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_Bitstring32CommandWithCP56Time2a_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t cause; +} IEC60870_EndOfInitialization_t; +typedef struct { + IEC60870_InformationObject_t object; +} IEC60870_TestCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + IEC60870_CP16Time2a_t delay; +} IEC60870_DelayAcquisitionCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + int16_t value; + uint8_t qualifier; +} IEC60870_ParameterNormalizedValue_t; +typedef struct { + IEC60870_InformationObject_t object; + int16_t value; + uint8_t qualifier; +} IEC60870_ParameterScaledValue_t; +typedef struct { + IEC60870_InformationObject_t object; + float value; + uint8_t qualifier; +} IEC60870_ParameterFloatValue_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t qualifier; +} IEC60870_ParameterActivation_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t file_name; + uint32_t file_length; + uint8_t qualifier; +} IEC60870_FileReady_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t file_name; + uint8_t section; + uint32_t section_length; + uint8_t qualifier; +} IEC60870_SectionReady_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t file_name; + uint8_t section; + uint8_t qualifier; +} IEC60870_FileCallOrSelect_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t file_name; + uint8_t section; + uint8_t qualifier; +} IEC60870_FileACK_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t file_name; + uint8_t section; + uint8_t qualifier; + uint8_t checksum; +} IEC60870_FileLastSegmentOrSection_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t file_name; + uint8_t section; + uint8_t length; + uint8_t data[255]; +} IEC60870_FileSegment_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t file_name; + uint32_t file_length; + uint8_t status; + IEC60870_CP56Time2a_t creation_time; +} IEC60870_FileDirectory_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t file_name; + IEC60870_CP56Time2a_t start_time; + IEC60870_CP56Time2a_t stop_time; +} IEC60870_QueryLog_t; +IEC60870_AsduResult_t IEC60870_StepPositionInformation_Create(IEC60870_StepPositionInformation_t *self, + uint32_t ioa, int value, int is_transient, int quality); +IEC60870_AsduResult_t IEC60870_StepPositionInformation_Get_From_Buffer(IEC60870_StepPositionInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_BitString32_Create(IEC60870_BitString32_t *self, + uint32_t ioa, uint32_t value, int quality); +IEC60870_AsduResult_t IEC60870_BitString32_Get_From_Buffer(IEC60870_BitString32_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SinglePointWithCP24Time2a_Create(IEC60870_SinglePointWithCP24Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_SinglePointWithCP24Time2a_Get_From_Buffer(IEC60870_SinglePointWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_DoublePointWithCP24Time2a_Create(IEC60870_DoublePointWithCP24Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_DoublePointWithCP24Time2a_Get_From_Buffer(IEC60870_DoublePointWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_StepPositionWithCP24Time2a_Create(IEC60870_StepPositionWithCP24Time2a_t *self, + uint32_t ioa, int value, int is_transient, int quality, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_StepPositionWithCP24Time2a_Get_From_Buffer(IEC60870_StepPositionWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP24Time2a_Create(IEC60870_Bitstring32WithCP24Time2a_t *self, + uint32_t ioa, uint32_t value, int quality, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP24Time2a_Get_From_Buffer(IEC60870_Bitstring32WithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP24Time2a_Create(IEC60870_MeasuredValueNormalizedWithCP24Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP24Time2a_Create(IEC60870_MeasuredValueScaledWithCP24Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueScaledWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP24Time2a_Create(IEC60870_MeasuredValueShortWithCP24Time2a_t *self, + uint32_t ioa, float value, int quality, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueShortWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP24Time2a_Create(IEC60870_IntegratedTotalsWithCP24Time2a_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP24Time2a_Get_From_Buffer(IEC60870_IntegratedTotalsWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_StepPositionWithCP56Time2a_Create(IEC60870_StepPositionWithCP56Time2a_t *self, + uint32_t ioa, int value, int is_transient, int quality, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_StepPositionWithCP56Time2a_Get_From_Buffer(IEC60870_StepPositionWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP56Time2a_Create(IEC60870_Bitstring32WithCP56Time2a_t *self, + uint32_t ioa, uint32_t value, int quality, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP56Time2a_Get_From_Buffer(IEC60870_Bitstring32WithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipment_Create(IEC60870_EventOfProtectionEquipment_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipment_Get_From_Buffer(IEC60870_EventOfProtectionEquipment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipment_Create(IEC60870_PackedStartEventsOfProtectionEquipment_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipment_Get_From_Buffer(IEC60870_PackedStartEventsOfProtectionEquipment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfo_Create(IEC60870_PackedOutputCircuitInfo_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfo_Get_From_Buffer(IEC60870_PackedOutputCircuitInfo_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Create(IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer(IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Create(IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer(IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Create(IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Get_From_Buffer(IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_PackedSinglePointWithSCD_Create(IEC60870_PackedSinglePointWithSCD_t *self, + uint32_t ioa, uint16_t status, uint16_t change, int quality); +IEC60870_AsduResult_t IEC60870_PackedSinglePointWithSCD_Get_From_Buffer(IEC60870_PackedSinglePointWithSCD_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithoutQuality_Create(IEC60870_MeasuredValueNormalizedWithoutQuality_t *self, + uint32_t ioa, int16_t value); +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithoutQuality_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithoutQuality_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_Bitstring32Command_Create(IEC60870_Bitstring32Command_t *self, + uint32_t ioa, uint32_t value); +IEC60870_AsduResult_t IEC60870_Bitstring32Command_Get_From_Buffer(IEC60870_Bitstring32Command_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_Bitstring32CommandWithCP56Time2a_Create(IEC60870_Bitstring32CommandWithCP56Time2a_t *self, + uint32_t ioa, uint32_t value, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_Bitstring32CommandWithCP56Time2a_Get_From_Buffer(IEC60870_Bitstring32CommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_EndOfInitialization_Create(IEC60870_EndOfInitialization_t *self, + uint32_t ioa, int cause); +IEC60870_AsduResult_t IEC60870_EndOfInitialization_Get_From_Buffer(IEC60870_EndOfInitialization_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_TestCommand_Create(IEC60870_TestCommand_t *self, + uint32_t ioa); +IEC60870_AsduResult_t IEC60870_TestCommand_Get_From_Buffer(IEC60870_TestCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_DelayAcquisitionCommand_Create(IEC60870_DelayAcquisitionCommand_t *self, + uint32_t ioa, const IEC60870_CP16Time2a_t *delay); +IEC60870_AsduResult_t IEC60870_DelayAcquisitionCommand_Get_From_Buffer(IEC60870_DelayAcquisitionCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_ParameterNormalizedValue_Create(IEC60870_ParameterNormalizedValue_t *self, + uint32_t ioa, int16_t value, int qualifier); +IEC60870_AsduResult_t IEC60870_ParameterNormalizedValue_Get_From_Buffer(IEC60870_ParameterNormalizedValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_ParameterScaledValue_Create(IEC60870_ParameterScaledValue_t *self, + uint32_t ioa, int16_t value, int qualifier); +IEC60870_AsduResult_t IEC60870_ParameterScaledValue_Get_From_Buffer(IEC60870_ParameterScaledValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_ParameterFloatValue_Create(IEC60870_ParameterFloatValue_t *self, + uint32_t ioa, float value, int qualifier); +IEC60870_AsduResult_t IEC60870_ParameterFloatValue_Get_From_Buffer(IEC60870_ParameterFloatValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_ParameterActivation_Create(IEC60870_ParameterActivation_t *self, + uint32_t ioa, int qualifier); +IEC60870_AsduResult_t IEC60870_ParameterActivation_Get_From_Buffer(IEC60870_ParameterActivation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_FileReady_Create(IEC60870_FileReady_t *self, + uint32_t ioa, uint16_t file_name, uint32_t file_length, int qualifier); +IEC60870_AsduResult_t IEC60870_FileReady_Get_From_Buffer(IEC60870_FileReady_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SectionReady_Create(IEC60870_SectionReady_t *self, + uint32_t ioa, uint16_t file_name, int section, uint32_t section_length, int qualifier); +IEC60870_AsduResult_t IEC60870_SectionReady_Get_From_Buffer(IEC60870_SectionReady_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_FileCallOrSelect_Create(IEC60870_FileCallOrSelect_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier); +IEC60870_AsduResult_t IEC60870_FileCallOrSelect_Get_From_Buffer(IEC60870_FileCallOrSelect_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_FileACK_Create(IEC60870_FileACK_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier); +IEC60870_AsduResult_t IEC60870_FileACK_Get_From_Buffer(IEC60870_FileACK_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_FileLastSegmentOrSection_Create(IEC60870_FileLastSegmentOrSection_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier, int checksum); +IEC60870_AsduResult_t IEC60870_FileLastSegmentOrSection_Get_From_Buffer(IEC60870_FileLastSegmentOrSection_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_FileSegment_Create(IEC60870_FileSegment_t *self, + uint32_t ioa, uint16_t file_name, int section, const uint8_t *data, uint16_t data_length); +IEC60870_AsduResult_t IEC60870_FileSegment_Get_From_Buffer(IEC60870_FileSegment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_FileDirectory_Create(IEC60870_FileDirectory_t *self, + uint32_t ioa, uint16_t file_name, uint32_t file_length, int status, const IEC60870_CP56Time2a_t *creation_time); +IEC60870_AsduResult_t IEC60870_FileDirectory_Get_From_Buffer(IEC60870_FileDirectory_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_QueryLog_Create(IEC60870_QueryLog_t *self, + uint32_t ioa, uint16_t file_name, const IEC60870_CP56Time2a_t *start_time, const IEC60870_CP56Time2a_t *stop_time); +IEC60870_AsduResult_t IEC60870_QueryLog_Get_From_Buffer(IEC60870_QueryLog_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +int IEC60870_FileSegment_Get_Max_Data_Size(const IEC60870_AppLayerParameters_t *parameters); +uint8_t IEC60870_InformationObject_Get_Type(const IEC60870_InformationObject_t *object); +IEC60870_AsduResult_t IEC60870_InformationObject_Encode( + const IEC60870_InformationObject_t *object, const IEC60870_AppLayerParameters_t *parameters, + int is_sequence, uint8_t *buffer, uint16_t capacity, uint16_t *length); +IEC60870_AsduResult_t IEC60870_SinglePointInformation_Create(IEC60870_SinglePointInformation_t *self, + uint32_t ioa, int value, int quality); +IEC60870_AsduResult_t IEC60870_SinglePointInformation_Get_From_Buffer(IEC60870_SinglePointInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_DoublePointInformation_Create(IEC60870_DoublePointInformation_t *self, + uint32_t ioa, int value, int quality); +IEC60870_AsduResult_t IEC60870_DoublePointInformation_Get_From_Buffer(IEC60870_DoublePointInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalized_Create(IEC60870_MeasuredValueNormalized_t *self, + uint32_t ioa, int16_t value, int quality); +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalized_Get_From_Buffer(IEC60870_MeasuredValueNormalized_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueScaled_Create(IEC60870_MeasuredValueScaled_t *self, + uint32_t ioa, int16_t value, int quality); +IEC60870_AsduResult_t IEC60870_MeasuredValueScaled_Get_From_Buffer(IEC60870_MeasuredValueScaled_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueShort_Create(IEC60870_MeasuredValueShort_t *self, + uint32_t ioa, float value, int quality); +IEC60870_AsduResult_t IEC60870_MeasuredValueShort_Get_From_Buffer(IEC60870_MeasuredValueShort_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_IntegratedTotals_Create(IEC60870_IntegratedTotals_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value); +IEC60870_AsduResult_t IEC60870_IntegratedTotals_Get_From_Buffer(IEC60870_IntegratedTotals_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SinglePointWithCP56Time2a_Create(IEC60870_SinglePointWithCP56Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_SinglePointWithCP56Time2a_Get_From_Buffer(IEC60870_SinglePointWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_DoublePointWithCP56Time2a_Create(IEC60870_DoublePointWithCP56Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_DoublePointWithCP56Time2a_Get_From_Buffer(IEC60870_DoublePointWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP56Time2a_Create(IEC60870_MeasuredValueNormalizedWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP56Time2a_Create(IEC60870_MeasuredValueScaledWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueScaledWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP56Time2a_Create(IEC60870_MeasuredValueShortWithCP56Time2a_t *self, + uint32_t ioa, float value, int quality, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueShortWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP56Time2a_Create(IEC60870_IntegratedTotalsWithCP56Time2a_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP56Time2a_Get_From_Buffer(IEC60870_IntegratedTotalsWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SingleCommand_Create(IEC60870_SingleCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier); +IEC60870_AsduResult_t IEC60870_SingleCommand_Get_From_Buffer(IEC60870_SingleCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_DoubleCommand_Create(IEC60870_DoubleCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier); +IEC60870_AsduResult_t IEC60870_DoubleCommand_Get_From_Buffer(IEC60870_DoubleCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_StepCommand_Create(IEC60870_StepCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier); +IEC60870_AsduResult_t IEC60870_StepCommand_Get_From_Buffer(IEC60870_StepCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalized_Create(IEC60870_SetpointCommandNormalized_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier); +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalized_Get_From_Buffer(IEC60870_SetpointCommandNormalized_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SetpointCommandScaled_Create(IEC60870_SetpointCommandScaled_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier); +IEC60870_AsduResult_t IEC60870_SetpointCommandScaled_Get_From_Buffer(IEC60870_SetpointCommandScaled_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SetpointCommandShort_Create(IEC60870_SetpointCommandShort_t *self, + uint32_t ioa, float value, int is_select, int qualifier); +IEC60870_AsduResult_t IEC60870_SetpointCommandShort_Get_From_Buffer(IEC60870_SetpointCommandShort_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SingleCommandWithCP56Time2a_Create(IEC60870_SingleCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_SingleCommandWithCP56Time2a_Get_From_Buffer(IEC60870_SingleCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_DoubleCommandWithCP56Time2a_Create(IEC60870_DoubleCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_DoubleCommandWithCP56Time2a_Get_From_Buffer(IEC60870_DoubleCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_StepCommandWithCP56Time2a_Create(IEC60870_StepCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_StepCommandWithCP56Time2a_Get_From_Buffer(IEC60870_StepCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalizedWithCP56Time2a_Create(IEC60870_SetpointCommandNormalizedWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalizedWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandNormalizedWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SetpointCommandScaledWithCP56Time2a_Create(IEC60870_SetpointCommandScaledWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_SetpointCommandScaledWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandScaledWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SetpointCommandShortWithCP56Time2a_Create(IEC60870_SetpointCommandShortWithCP56Time2a_t *self, + uint32_t ioa, float value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_SetpointCommandShortWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandShortWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_InterrogationCommand_Create(IEC60870_InterrogationCommand_t *self, + uint32_t ioa, int qualifier); +IEC60870_AsduResult_t IEC60870_InterrogationCommand_Get_From_Buffer(IEC60870_InterrogationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_CounterInterrogationCommand_Create(IEC60870_CounterInterrogationCommand_t *self, + uint32_t ioa, int qualifier, int freeze); +IEC60870_AsduResult_t IEC60870_CounterInterrogationCommand_Get_From_Buffer(IEC60870_CounterInterrogationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_ReadCommand_Create(IEC60870_ReadCommand_t *self, + uint32_t ioa); +IEC60870_AsduResult_t IEC60870_ReadCommand_Get_From_Buffer(IEC60870_ReadCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_ClockSynchronizationCommand_Create(IEC60870_ClockSynchronizationCommand_t *self, + uint32_t ioa, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_ClockSynchronizationCommand_Get_From_Buffer(IEC60870_ClockSynchronizationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_ResetProcessCommand_Create(IEC60870_ResetProcessCommand_t *self, + uint32_t ioa, int qualifier); +IEC60870_AsduResult_t IEC60870_ResetProcessCommand_Get_From_Buffer(IEC60870_ResetProcessCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_TestCommandWithCP56Time2a_Create(IEC60870_TestCommandWithCP56Time2a_t *self, + uint32_t ioa, uint16_t sequence, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_TestCommandWithCP56Time2a_Get_From_Buffer(IEC60870_TestCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +typedef enum { + IEC60870_ASDU_COT_PERIODIC = 1, + IEC60870_ASDU_COT_BACKGROUND = 2, + IEC60870_ASDU_COT_SPONTANEOUS = 3, + IEC60870_ASDU_COT_INITIALIZED = 4, + IEC60870_ASDU_COT_REQUEST = 5, + IEC60870_ASDU_COT_ACTIVATION = 6, + IEC60870_ASDU_COT_ACTIVATION_CON = 7, + IEC60870_ASDU_COT_DEACTIVATION = 8, + IEC60870_ASDU_COT_DEACTIVATION_CON = 9, + IEC60870_ASDU_COT_ACTIVATION_TERMINATION = 10, + IEC60870_ASDU_COT_RETURN_INFO_REMOTE = 11, + IEC60870_ASDU_COT_RETURN_INFO_LOCAL = 12, + IEC60870_ASDU_COT_FILE_TRANSFER = 13, + IEC60870_ASDU_COT_INTERROGATED_BY_STATION = 20, + IEC60870_ASDU_COT_COUNTER_INTERROGATION = 37, + IEC60870_ASDU_COT_COUNTER_GROUP_1 = 38, + IEC60870_ASDU_COT_COUNTER_GROUP_2 = 39, + IEC60870_ASDU_COT_COUNTER_GROUP_3 = 40, + IEC60870_ASDU_COT_COUNTER_GROUP_4 = 41, + IEC60870_ASDU_COT_UNKNOWN_TYPE = 44, + IEC60870_ASDU_COT_UNKNOWN_CAUSE = 45, + IEC60870_ASDU_COT_UNKNOWN_COMMON_ADDRESS = 46, + IEC60870_ASDU_COT_UNKNOWN_IOA = 47 +} IEC60870_AsduCause_t; +typedef struct { + uint8_t cause_of_transmission; + uint8_t is_negative_confirm; + uint8_t is_test; + uint8_t originator_address; + uint16_t common_address; +} IEC60870_AsduHeader_t; +typedef struct { + uint8_t *buffer; + uint16_t capacity; + uint16_t length; + uint8_t type_id; + uint8_t object_count; + uint8_t is_sequence; + uint8_t is_from_buffer; + uint32_t first_ioa; + const IEC60870_AppLayerParameters_t *parameters; +} IEC60870_Asdu_t; +IEC60870_AsduResult_t IEC60870_Asdu_Create(IEC60870_Asdu_t *self, + const IEC60870_AppLayerParameters_t *parameters, + uint8_t *buffer, uint16_t capacity, const IEC60870_AsduHeader_t *header, int is_sequence); +IEC60870_AsduResult_t IEC60870_Asdu_Create_From_Buffer(IEC60870_Asdu_t *self, + const IEC60870_AppLayerParameters_t *parameters, uint8_t *buffer, uint16_t length); +IEC60870_AsduResult_t IEC60870_Asdu_Set_COT(IEC60870_Asdu_t *self, int cause); +IEC60870_AsduResult_t IEC60870_Asdu_Set_Negative(IEC60870_Asdu_t *self, int is_negative); +IEC60870_AsduResult_t IEC60870_Asdu_Set_CA(IEC60870_Asdu_t *self, uint16_t common_address); +IEC60870_AsduResult_t IEC60870_Asdu_Add_InformationObject(IEC60870_Asdu_t *self, + const IEC60870_InformationObject_t *object); +IEC60870_AsduResult_t IEC60870_Asdu_Get_Data(const IEC60870_Asdu_t *self, + const uint8_t **asdu, uint16_t *length); +IEC60870_AsduResult_t IEC60870_Asdu_Parse(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *asdu, uint16_t length, IEC60870_AsduInfo_t *info); +IEC60870_AsduResult_t IEC60870_Asdu_Get_Element(const IEC60870_AsduInfo_t *info, + uint8_t index, void *object, size_t object_size); +IEC60870_AsduResult_t IEC60870_Asdu_Build_Response(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *request, uint16_t length, + int cause, int is_negative, uint8_t *out, uint16_t capacity, uint16_t *out_length); +typedef struct IEC60870_MasterConnection IEC60870_MasterConnection_t; +struct IEC60870_MasterConnection { + void *object; + _Bool (*send_asdu)(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu); + _Bool (*send_act_con)(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu, _Bool negative); + _Bool (*send_act_term)(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu); + const IEC60870_AppLayerParameters_t *(*get_application_layer_parameters)( + IEC60870_MasterConnection_t *self); +}; +_Bool IEC60870_MasterConnection_Send_ASDU(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu); +_Bool IEC60870_MasterConnection_Send_ACT_CON(IEC60870_MasterConnection_t *self, + IEC60870_Asdu_t *asdu, _Bool negative); +_Bool IEC60870_MasterConnection_Send_ACT_TERM(IEC60870_MasterConnection_t *self, + IEC60870_Asdu_t *asdu); +const IEC60870_AppLayerParameters_t *IEC60870_MasterConnection_Get_Application_Layer_Parameters( + IEC60870_MasterConnection_t *self); +typedef enum { + IEC104_ASDU_PRIORITY_HIGH = 0, + IEC104_ASDU_PRIORITY_NORMAL = 1, + IEC104_ASDU_PRIORITY_COUNT = 2 +} IEC104_AsduPriority_t; +typedef struct { + uint16_t k; + uint16_t w; + uint32_t t1_ms; + uint32_t t2_ms; + uint32_t t3_ms; +} IEC104_LinkConfig_t; +typedef struct { + IEC104_LinkConfig_t link; + uint32_t local_send_timeout_ms; + uint32_t stop_timeout_ms; + uint16_t high_priority_burst_limit; +} IEC104_ProtocolConfig_t; +typedef enum { + IEC104_TCP_OK = 0, + IEC104_TCP_INVALID_ARGUMENT = -1, + IEC104_TCP_INVALID_HANDLE = -2, + IEC104_TCP_INVALID_CONFIG = -3, + IEC104_TCP_INVALID_STATE = -4, + IEC104_TCP_BUSY = -5, + IEC104_TCP_EVENTS_PENDING = -6, + IEC104_TCP_NETWORK_ERROR = -7, + IEC104_TCP_INTERNAL_ERROR = -8, + IEC104_TCP_GENERATION_EXHAUSTED = -9, + IEC104_TCP_REGISTRATION_EXHAUSTED = -10, + IEC104_TCP_NOT_CONNECTED = -11, + IEC104_TCP_STALE_GENERATION = -12 +} IEC104_TcpResult_t; +typedef enum { + IEC104_TCP_STAGE_NONE = 0, + IEC104_TCP_STAGE_VALIDATE, + IEC104_TCP_STAGE_LIFECYCLE, + IEC104_TCP_STAGE_NET_INIT, + IEC104_TCP_STAGE_LISTEN, + IEC104_TCP_STAGE_WAIT, + IEC104_TCP_STAGE_ACCEPT, + IEC104_TCP_STAGE_GENERATION, + IEC104_TCP_STAGE_QUEUE_INIT, + IEC104_TCP_STAGE_QUEUE_STATE, + IEC104_TCP_STAGE_CONNECTION_INIT, + IEC104_TCP_STAGE_SCHEDULE, + IEC104_TCP_STAGE_REGISTRATION +} IEC104_TcpErrorStage_t; +typedef struct { + IEC104_TcpResult_t result; + IEC104_TcpErrorStage_t stage; + int socket_error; +} IEC104_TcpError_t; +typedef uintptr_t IEC104_TcpSocket_t; +typedef struct { + IEC104_TcpSocket_t socket; + uint64_t token; + uint32_t events; +} IEC104_TcpSocketEvent_t; +typedef struct { + uint64_t service_token; + IEC104_TcpSocketEvent_t sockets[2U]; + uint32_t socket_count; + uint32_t timeout_ms; + uint16_t pending_event_count; + uint8_t receive_paused; + uint8_t timer_active; + uint8_t need_process; +} IEC104_TcpWaitInfo_t; +typedef struct { + uint64_t service_token; + IEC104_TcpSocketEvent_t sockets[2U]; + uint32_t socket_count; + uint8_t timer_expired; +} IEC104_TcpReady_t; +typedef enum { + IEC104_TCP_EVENT_CONNECTED = 1, + IEC104_TCP_EVENT_I_FRAME, + IEC104_TCP_EVENT_RECORD, + IEC104_TCP_EVENT_DISCONNECTED, + IEC104_TCP_EVENT_ACTIVATED, + IEC104_TCP_EVENT_DEACTIVATED +} IEC104_TcpEventType_t; +typedef enum { + IEC104_TCP_EVENT_ERROR = -1, + IEC104_TCP_EVENT_EMPTY = 0, + IEC104_TCP_EVENT_READY = 1 +} IEC104_TcpEventResult_t; +typedef enum { + IEC104_TCP_SUBMIT_FATAL = -2, + IEC104_TCP_SUBMIT_ERROR = -1, + IEC104_TCP_SUBMIT_OK = 1, + 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; +typedef enum { + IEC104_TCP_FAULT_NONE = 0, + IEC104_TCP_FAULT_FRAME, + IEC104_TCP_FAULT_PROTOCOL, + IEC104_TCP_FAULT_INTERNAL, + IEC104_TCP_FAULT_TRANSPORT, + IEC104_TCP_FAULT_LOCAL_SEND_TIMEOUT, + IEC104_TCP_FAULT_I_ACK_TIMEOUT, + IEC104_TCP_FAULT_TESTFR_TIMEOUT, + IEC104_TCP_FAULT_STOP_TIMEOUT +} IEC104_TcpFault_t; +typedef struct { + IEC104_TcpSessionPhase_t phase; + IEC104_TcpFault_t fault; + uint16_t pending_send_records; + uint8_t data_transfer_started; +} IEC104_TcpConnectionStatus_t; +typedef struct { + uint64_t generation; + char address[16]; + uint16_t port; +} IEC104_TcpPeerInfo_t; +typedef struct { + uint64_t receive_serial; + uint16_t apdu_length; + uint16_t send_number; + uint16_t receive_number; + uint8_t apdu[255]; +} IEC104_TcpIFrameEvent_t; +typedef struct { + uint64_t frame_serial; + uint64_t queued_at_ms; + uint64_t written_at_ms; + uint16_t send_number; + uint16_t receive_number; + uint16_t apdu_length; + IEC104_AsduPriority_t priority; + uint8_t local_send_completed; + uint8_t peer_acknowledged; +} IEC104_TcpSendRecord_t; +typedef struct { + IEC104_TcpSendRecord_t value; + uint8_t closing; +} IEC104_TcpRecordEvent_t; +typedef struct { + IEC104_TcpConnectionStatus_t status; + IEC104_TcpPeerInfo_t peer; + int socket_error; + uint8_t status_valid; +} IEC104_TcpConnectionEvent_t; +typedef struct { + IEC104_TcpEventType_t type; + uint64_t generation; + union { + IEC104_TcpIFrameEvent_t i_frame; + IEC104_TcpRecordEvent_t record; + IEC104_TcpConnectionEvent_t connection; + } data; +} IEC104_TcpEvent_t; +typedef struct { + char bind_address[16]; + uint16_t port; + IEC104_ProtocolConfig_t protocol; +} IEC104_TcpServerConfig_t; +typedef struct { + uint32_t pending_asdu_count; + uint32_t stored_asdu_count; + uint32_t capacity_bytes; + uint32_t used_bytes; + uint32_t free_bytes; +} IEC104_TcpAsduQueueStatus_t; +typedef struct { + uint64_t generation; + uint16_t bound_port; + uint16_t pending_event_count; + uint16_t pending_state_event_count; + uint8_t receive_paused; + int last_socket_error; + uint8_t listening; + uint8_t connected; + uint8_t connection_status_valid; + IEC104_TcpConnectionStatus_t connection; + uint8_t asdu_queue_status_valid; + IEC104_TcpAsduQueueStatus_t high_queue; + IEC104_TcpAsduQueueStatus_t normal_queue; + uint8_t accept_paused; + uint32_t accept_retry_remaining_ms; + int last_accept_error; + uint32_t consecutive_accept_failures; +} IEC104_TcpServerStatus_t; +typedef struct IEC104_Slave IEC104_Slave_t; +typedef IEC104_TcpServerConfig_t IEC104_SlaveConfig_t; +typedef struct IEC104_Station { + struct IEC104_Station *_next; + IEC104_Slave_t *_owner; + void *_context; + uint64_t _id; + uint32_t _users; + uint16_t _common_address; + _Bool _registered; +} IEC104_Station_t; +typedef struct IEC104_StationSnapshot { + IEC104_Slave_t *_owner; + IEC104_Station_t *_next; + uint64_t _last_id; + const struct IEC104_StationSnapshot *_self; +} IEC104_StationSnapshot_t; +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; +IEC104_StationResult_t IEC104_Slave_Add_Station(IEC104_Slave_t *self, + IEC104_Station_t *station, uint16_t common_address, void *context); +IEC104_StationResult_t IEC104_Slave_Remove_Station(IEC104_Slave_t *self, IEC104_Station_t *station); +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); +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); +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); +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); +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); +uint16_t IEC104_Station_Get_Common_Address(const IEC104_Station_t *station); +void *IEC104_Station_Get_Context(const IEC104_Station_t *station); +typedef struct { + IEC104_TcpEvent_t transport; + IEC60870_Asdu_t asdu; + IEC60870_AsduResult_t asdu_result; + IEC60870_MasterConnection_t connection; + IEC104_Slave_t *_owner; + uint64_t _owner_serial; + uint64_t _generation; + IEC104_TcpSubmitResult_t _last_submit_result; +} IEC104_SlaveEvent_t; +typedef struct { + const IEC104_Slave_t *slave; + uint64_t instance_id; + uint64_t generation; +} IEC104_SlaveEventIdentity_t; +typedef enum { + IEC104_SLAVE_REQUEST_ERROR = -1, + IEC104_SLAVE_REQUEST_STALE = 0, + IEC104_SLAVE_REQUEST_WAIT_EVENTS = 1, + IEC104_SLAVE_REQUEST_READY = 2 +} IEC104_SlaveRequestState_t; +IEC104_Slave_t *IEC104_Slave_Create(void); +IEC104_TcpResult_t IEC104_Slave_Destroy(IEC104_Slave_t *self, IEC104_TcpError_t *error); +void IEC104_Slave_Config_Default(IEC104_SlaveConfig_t *config); +IEC104_TcpResult_t IEC104_Slave_Init(IEC104_Slave_t *self, + const IEC104_SlaveConfig_t *config, IEC104_TcpError_t *error); +IEC104_TcpResult_t IEC104_Slave_Prepare(const IEC104_Slave_t *self, + IEC104_TcpWaitInfo_t *info, IEC104_TcpError_t *error); +IEC104_TcpResult_t IEC104_Slave_Process(IEC104_Slave_t *self, + const IEC104_TcpReady_t *ready, IEC104_TcpError_t *error); +IEC104_TcpResult_t IEC104_Slave_Poll(IEC104_Slave_t *self, + uint32_t max_wait_ms, IEC104_TcpError_t *error); +IEC104_TcpEventResult_t IEC104_Slave_NextEvent(IEC104_Slave_t *self, IEC104_SlaveEvent_t *event); +_Bool IEC104_Slave_Copy_Event(IEC104_SlaveEvent_t *destination, const IEC104_SlaveEvent_t *source); +_Bool IEC104_Slave_Get_Event_Identity(const IEC104_Slave_t *self, + const IEC104_SlaveEvent_t *event, IEC104_SlaveEventIdentity_t *identity); +IEC104_SlaveRequestState_t IEC104_Slave_Check_Request(const IEC104_Slave_t *self, + const IEC104_SlaveEventIdentity_t *identity); +IEC104_TcpSubmitResult_t IEC104_Slave_Get_Last_Submit_Result( + const IEC60870_MasterConnection_t *connection); +IEC104_TcpSubmitResult_t IEC104_Slave_Try_Send_ASDU( + IEC60870_MasterConnection_t *connection, const IEC60870_Asdu_t *asdu); +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); +IEC104_TcpSubmitResult_t IEC104_Slave_Reject_Request(IEC104_Slave_t *self, + const IEC104_SlaveEvent_t *event, int cause); +const IEC60870_AppLayerParameters_t *IEC104_Slave_Get_Application_Layer_Parameters( + const IEC104_Slave_t *self); +IEC104_TcpSubmitResult_t IEC104_Slave_Enqueue_Event(IEC104_Slave_t *self, IEC60870_Asdu_t *asdu); +IEC104_TcpResult_t IEC104_Slave_Get_Remote_Address(const IEC104_Slave_t *self, + IEC104_TcpPeerInfo_t *peer, IEC104_TcpError_t *error); +IEC104_TcpResult_t IEC104_Slave_Close_Connection(IEC104_Slave_t *self, + const IEC104_SlaveEvent_t *event, IEC104_TcpError_t *error); +IEC104_TcpResult_t IEC104_Slave_Close_Connection_By_Identity(IEC104_Slave_t *self, + const IEC104_SlaveEventIdentity_t *identity, IEC104_TcpError_t *error); +IEC104_TcpResult_t IEC104_Slave_Get_Status(const IEC104_Slave_t *self, + IEC104_TcpServerStatus_t *status, IEC104_TcpError_t *error); +IEC104_TcpResult_t IEC104_Slave_Close(IEC104_Slave_t *self, IEC104_TcpError_t *error); +typedef struct IEC104_Command IEC104_Command_t; +typedef struct { + uint16_t type_mask; + uint16_t select_required_mask; + uint32_t select_timeout_ms; + uint32_t decision_timeout_ms; + uint32_t execute_timeout_ms; + _Bool send_act_term; + _Bool act_term_on_failure; +} IEC104_CommandConfig_t; +typedef struct { + uint64_t generation, receive_serial; + uint64_t station_id; + uint16_t common_address; + uint8_t originator_address, type_id; + uint32_t ioa; + _Bool is_select, selected; + uint8_t state, qualifier; + uint64_t accepted_ms; +} IEC104_CommandRequest_t; +typedef enum { + IEC104_COMMAND_SELECT = 1, + IEC104_COMMAND_EXECUTE, + IEC104_COMMAND_FINISHED +} IEC104_CommandEventType_t; +typedef enum { + IEC104_COMMAND_FINISH_NONE = 0, + IEC104_COMMAND_FINISH_COMPLETED, + IEC104_COMMAND_FINISH_EXECUTION_FAILED, + IEC104_COMMAND_FINISH_REJECTED, + IEC104_COMMAND_FINISH_ACTION_FAILED, + IEC104_COMMAND_FINISH_MISMATCH, + IEC104_COMMAND_FINISH_DEACTIVATED, + IEC104_COMMAND_FINISH_SELECT_TIMEOUT, + IEC104_COMMAND_FINISH_DECISION_TIMEOUT, + IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT, + IEC104_COMMAND_FINISH_CANCELLED, + IEC104_COMMAND_FINISH_STOPPED, + IEC104_COMMAND_FINISH_DISCONNECTED, + IEC104_COMMAND_FINISH_CONNECTION_CHANGED, + IEC104_COMMAND_FINISH_SEND_ERROR, + IEC104_COMMAND_FINISH_SERVICE_ERROR +} IEC104_CommandFinishReason_t; +typedef struct { + IEC104_CommandEventType_t type; + uint64_t instance_id, transaction_id, event_serial; + IEC104_CommandRequest_t request; + IEC104_CommandFinishReason_t reason; + _Bool select_effect, execute_effect; +} IEC104_CommandEvent_t; +typedef enum { + IEC104_COMMAND_ACTION_NOT_STARTED = 0, + IEC104_COMMAND_ACTION_STARTED, + IEC104_COMMAND_ACTION_FAILED +} IEC104_CommandActionResult_t; +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, + IEC104_COMMAND_REJECT_UNKNOWN_IOA +} IEC104_CommandRejectCause_t; +typedef enum { + IEC104_COMMAND_EVENT_ERROR = -1, + IEC104_COMMAND_EVENT_IGNORED = 0, + IEC104_COMMAND_EVENT_HANDLED = 1 +} IEC104_CommandEventResult_t; +typedef enum { + IEC104_COMMAND_NEXT_ERROR = -1, + IEC104_COMMAND_NEXT_EMPTY = 0, + IEC104_COMMAND_NEXT_READY = 1 +} IEC104_CommandNextResult_t; +typedef enum { + IEC104_COMMAND_REPLY_STALE = -2, + IEC104_COMMAND_REPLY_ERROR = -1, + IEC104_COMMAND_REPLY_WAIT = 0, + IEC104_COMMAND_REPLY_OK = 1 +} IEC104_CommandReplyResult_t; +typedef enum { + IEC104_COMMAND_PHASE_SELECT_WAIT = 1, + IEC104_COMMAND_PHASE_SELECTED, + IEC104_COMMAND_PHASE_EXECUTE_WAIT, + IEC104_COMMAND_PHASE_EXECUTING, + IEC104_COMMAND_PHASE_ENDED +} IEC104_CommandPhase_t; +typedef struct { + _Bool active; + IEC104_CommandPhase_t phase; + _Bool confirm_submitted; + _Bool result_reported, completed, term_submitted; +} IEC104_CommandTransactionInfo_t; +typedef struct { + uint64_t generation; + _Bool active, events_pending, need_process, timer_active; + _Bool busy; + uint32_t timeout_remaining_ms; + unsigned pending_replies, pending_events; + unsigned long accepted, selected, executed, completed, execution_failed, action_failed; + unsigned long submitted, negative_submitted, return_info_submitted, waits; + unsigned long rejected, app_rejected, busy_rejected, overloads, dropped_requests, cancelled; + unsigned long deactivated, mismatches, select_timeouts, decision_timeouts, execute_timeouts; + unsigned long clock_errors; +} IEC104_CommandStatus_t; +void IEC104_Command_Config_Default(IEC104_CommandConfig_t *config); +IEC104_Command_t *IEC104_Command_Create(IEC104_Slave_t *slave, const IEC104_CommandConfig_t *config); +_Bool IEC104_Command_Destroy(IEC104_Command_t *self); +void IEC104_Command_Cancel(IEC104_Command_t *self); +IEC104_CommandEventResult_t IEC104_Command_Handle_Event(IEC104_Command_t *self, + const IEC104_SlaveEvent_t *event); +_Bool IEC104_Command_Process(IEC104_Command_t *self); +IEC104_CommandNextResult_t IEC104_Command_NextEvent(IEC104_Command_t *self, + IEC104_CommandEvent_t *event); +IEC104_CommandReplyResult_t IEC104_Command_Accept(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandAction_t action, void *context); +IEC104_CommandReplyResult_t IEC104_Command_Reject(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandRejectCause_t cause); +IEC104_CommandReplyResult_t IEC104_Command_Report_Result(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result); +IEC104_CommandReplyResult_t IEC104_Command_Submit_Return_Info(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, const IEC60870_Asdu_t *asdu); +IEC104_CommandReplyResult_t IEC104_Command_Complete(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event); +_Bool IEC104_Command_Get_Transaction(const IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandTransactionInfo_t *info); +_Bool IEC104_Command_Get_Status(const IEC104_Command_t *self, IEC104_CommandStatus_t *status); +typedef uintptr_t IEC104_NetSocket_t; +typedef enum { + IEC104_NET_ERROR = -1, + IEC104_NET_OK = 0, + IEC104_NET_WOULD_BLOCK, + IEC104_NET_RETRY, + IEC104_NET_EOF, + IEC104_NET_RETRY_LATER +} IEC104_NetResult_t; +typedef struct { + IEC104_NetResult_t result; + size_t transferred; + int native_error; +} IEC104_NetIoResult_t; +typedef struct { + char address[16]; + uint16_t port; +} IEC104_NetPeer_t; +extern IEC104_NetResult_t IEC104_Net_Init(int *native_error); +extern void IEC104_Net_Cleanup(void); +extern int IEC104_Net_Invalid_Argument_Error(void); +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); +extern IEC104_NetResult_t IEC104_Net_Accept( + IEC104_NetSocket_t listener, IEC104_NetSocket_t *client, IEC104_NetPeer_t *peer, int *native_error); +extern IEC104_NetIoResult_t IEC104_Net_Recv( + IEC104_NetSocket_t socket_handle, void *data, size_t capacity); +extern IEC104_NetResult_t IEC104_Net_Pending_Input( + IEC104_NetSocket_t socket_handle, uint32_t *available, int *native_error); +extern IEC104_NetIoResult_t IEC104_Net_Send( + IEC104_NetSocket_t socket_handle, const void *data, size_t length); +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); +extern IEC104_NetResult_t IEC104_Net_Close( + IEC104_NetSocket_t socket_handle, int *native_error); +extern uint64_t IEC104_Net_Now_Ms(void); + __attribute__ ((__dllimport__)) void *__attribute__((__cdecl__)) _memccpy(void *_Dst,const void *_Src,int _Val,size_t _MaxCount); + void *__attribute__((__cdecl__)) memchr(const void *_Buf ,int _Val,size_t _MaxCount); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _memicmp(const void *_Buf1,const void *_Buf2,size_t _Size); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _memicmp_l(const void *_Buf1,const void *_Buf2,size_t _Size,_locale_t _Locale); + int __attribute__((__cdecl__)) memcmp(const void *_Buf1,const void *_Buf2,size_t _Size); + void * __attribute__((__cdecl__)) memcpy(void * __restrict__ _Dst,const void * __restrict__ _Src,size_t _Size) ; + __attribute__((dllimport)) errno_t __attribute__((__cdecl__)) memcpy_s (void *_dest,size_t _numberOfElements,const void *_src,size_t _count); + void * __attribute__((__cdecl__)) mempcpy (void *_Dst, const void *_Src, size_t _Size); + void * __attribute__((__cdecl__)) memset(void *_Dst,int _Val,size_t _Size); + void * __attribute__((__cdecl__)) memccpy(void *_Dst,const void *_Src,int _Val,size_t _Size) ; + int __attribute__((__cdecl__)) memicmp(const void *_Buf1,const void *_Buf2,size_t _Size) ; + char * __attribute__((__cdecl__)) _strset(char *_Str,int _Val) ; + char * __attribute__((__cdecl__)) _strset_l(char *_Str,int _Val,_locale_t _Locale) ; + char * __attribute__((__cdecl__)) strcpy(char * __restrict__ _Dest,const char * __restrict__ _Source); + char * __attribute__((__cdecl__)) strcat(char * __restrict__ _Dest,const char * __restrict__ _Source); + int __attribute__((__cdecl__)) strcmp(const char *_Str1,const char *_Str2); + size_t __attribute__((__cdecl__)) strlen(const char *_Str); + size_t __attribute__((__cdecl__)) strnlen(const char *_Str,size_t _MaxCount); + void *__attribute__((__cdecl__)) memmove(void *_Dst,const void *_Src,size_t _Size) ; + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) _strdup(const char *_Src); + char *__attribute__((__cdecl__)) strchr(const char *_Str,int _Val); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _stricmp(const char *_Str1,const char *_Str2); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _strcmpi(const char *_Str1,const char *_Str2); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _stricmp_l(const char *_Str1,const char *_Str2,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) strcoll(const char *_Str1,const char *_Str2); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _strcoll_l(const char *_Str1,const char *_Str2,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _stricoll(const char *_Str1,const char *_Str2); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _stricoll_l(const char *_Str1,const char *_Str2,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _strncoll (const char *_Str1,const char *_Str2,size_t _MaxCount); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _strncoll_l(const char *_Str1,const char *_Str2,size_t _MaxCount,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _strnicoll (const char *_Str1,const char *_Str2,size_t _MaxCount); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _strnicoll_l(const char *_Str1,const char *_Str2,size_t _MaxCount,_locale_t _Locale); + size_t __attribute__((__cdecl__)) strcspn(const char *_Str,const char *_Control); + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) _strerror(const char *_ErrMsg) ; + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) strerror(int) ; + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) _strlwr(char *_String) ; + char *strlwr_l(char *_String,_locale_t _Locale) ; + char *__attribute__((__cdecl__)) strncat(char * __restrict__ _Dest,const char * __restrict__ _Source,size_t _Count) ; + int __attribute__((__cdecl__)) strncmp(const char *_Str1,const char *_Str2,size_t _MaxCount); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _strnicmp(const char *_Str1,const char *_Str2,size_t _MaxCount); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _strnicmp_l(const char *_Str1,const char *_Str2,size_t _MaxCount,_locale_t _Locale); + char *strncpy(char * __restrict__ _Dest,const char * __restrict__ _Source,size_t _Count) ; + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) _strnset(char *_Str,int _Val,size_t _MaxCount) ; + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) _strnset_l(char *str,int c,size_t count,_locale_t _Locale) ; + char *__attribute__((__cdecl__)) strpbrk(const char *_Str,const char *_Control); + char *__attribute__((__cdecl__)) strrchr(const char *_Str,int _Ch); + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) _strrev(char *_Str); + size_t __attribute__((__cdecl__)) strspn(const char *_Str,const char *_Control); + char *__attribute__((__cdecl__)) strstr(const char *_Str,const char *_SubStr); + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) strtok(char * __restrict__ _Str,const char * __restrict__ _Delim) ; + + char *strtok_r(char * __restrict__ _Str, const char * __restrict__ _Delim, char ** __restrict__ __last); + + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) _strupr(char *_String) ; + __attribute__ ((__dllimport__)) char *_strupr_l(char *_String,_locale_t _Locale) ; + __attribute__ ((__dllimport__)) size_t __attribute__((__cdecl__)) strxfrm(char * __restrict__ _Dst,const char * __restrict__ _Src,size_t _MaxCount); + __attribute__ ((__dllimport__)) size_t __attribute__((__cdecl__)) _strxfrm_l(char * __restrict__ _Dst,const char * __restrict__ _Src,size_t _MaxCount,_locale_t _Locale); + char *__attribute__((__cdecl__)) strdup(const char *_Src) ; + int __attribute__((__cdecl__)) strcmpi(const char *_Str1,const char *_Str2) ; + int __attribute__((__cdecl__)) stricmp(const char *_Str1,const char *_Str2) ; + char *__attribute__((__cdecl__)) strlwr(char *_Str) ; + int __attribute__((__cdecl__)) strnicmp(const char *_Str1,const char *_Str,size_t _MaxCount) ; + int __attribute__((__cdecl__)) strncasecmp (const char *, const char *, size_t); + int __attribute__((__cdecl__)) strcasecmp (const char *, const char *); + char *__attribute__((__cdecl__)) strnset(char *_Str,int _Val,size_t _MaxCount) ; + char *__attribute__((__cdecl__)) strrev(char *_Str) ; + char *__attribute__((__cdecl__)) strset(char *_Str,int _Val) ; + char *__attribute__((__cdecl__)) strupr(char *_Str) ; + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcsdup(const wchar_t *_Str); + wchar_t *__attribute__((__cdecl__)) wcscat(wchar_t * __restrict__ _Dest,const wchar_t * __restrict__ _Source) ; + wchar_t *__attribute__((__cdecl__)) wcschr(const wchar_t *_Str,wchar_t _Ch); + int __attribute__((__cdecl__)) wcscmp(const wchar_t *_Str1,const wchar_t *_Str2); + wchar_t *__attribute__((__cdecl__)) wcscpy(wchar_t * __restrict__ _Dest,const wchar_t * __restrict__ _Source) ; + size_t __attribute__((__cdecl__)) wcscspn(const wchar_t *_Str,const wchar_t *_Control); + size_t __attribute__((__cdecl__)) wcslen(const wchar_t *_Str); + size_t __attribute__((__cdecl__)) wcsnlen(const wchar_t *_Src,size_t _MaxCount); + wchar_t *wcsncat(wchar_t * __restrict__ _Dest,const wchar_t * __restrict__ _Source,size_t _Count) ; + int __attribute__((__cdecl__)) wcsncmp(const wchar_t *_Str1,const wchar_t *_Str2,size_t _MaxCount); + wchar_t *wcsncpy(wchar_t * __restrict__ _Dest,const wchar_t * __restrict__ _Source,size_t _Count) ; + wchar_t *__attribute__((__cdecl__)) _wcsncpy_l(wchar_t * __restrict__ _Dest,const wchar_t * __restrict__ _Source,size_t _Count,_locale_t _Locale) ; + wchar_t *__attribute__((__cdecl__)) wcspbrk(const wchar_t *_Str,const wchar_t *_Control); + wchar_t *__attribute__((__cdecl__)) wcsrchr(const wchar_t *_Str,wchar_t _Ch); + size_t __attribute__((__cdecl__)) wcsspn(const wchar_t *_Str,const wchar_t *_Control); + wchar_t *__attribute__((__cdecl__)) wcsstr(const wchar_t *_Str,const wchar_t *_SubStr); + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) wcstok(wchar_t * __restrict__ _Str,const wchar_t * __restrict__ _Delim,wchar_t ** __restrict__ _Ptr) ; + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcstok(wchar_t * __restrict__ _Str,const wchar_t * __restrict__ _Delim) ; + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcserror(int _ErrNum) ; + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) __wcserror(const wchar_t *_Str) ; + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsicmp(const wchar_t *_Str1,const wchar_t *_Str2); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsicmp_l(const wchar_t *_Str1,const wchar_t *_Str2,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsnicmp(const wchar_t *_Str1,const wchar_t *_Str2,size_t _MaxCount); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsnicmp_l(const wchar_t *_Str1,const wchar_t *_Str2,size_t _MaxCount,_locale_t _Locale); + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcsnset(wchar_t *_Str,wchar_t _Val,size_t _MaxCount) ; + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcsrev(wchar_t *_Str); + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcsset(wchar_t *_Str,wchar_t _Val) ; + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcslwr(wchar_t *_String) ; + __attribute__ ((__dllimport__)) wchar_t *_wcslwr_l(wchar_t *_String,_locale_t _Locale) ; + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcsupr(wchar_t *_String) ; + __attribute__ ((__dllimport__)) wchar_t *_wcsupr_l(wchar_t *_String,_locale_t _Locale) ; + __attribute__ ((__dllimport__)) size_t __attribute__((__cdecl__)) wcsxfrm(wchar_t * __restrict__ _Dst,const wchar_t * __restrict__ _Src,size_t _MaxCount); + __attribute__ ((__dllimport__)) size_t __attribute__((__cdecl__)) _wcsxfrm_l(wchar_t * __restrict__ _Dst,const wchar_t * __restrict__ _Src,size_t _MaxCount,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) wcscoll(const wchar_t *_Str1,const wchar_t *_Str2); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcscoll_l(const wchar_t *_Str1,const wchar_t *_Str2,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsicoll(const wchar_t *_Str1,const wchar_t *_Str2); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsicoll_l(const wchar_t *_Str1,const wchar_t *_Str2,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsncoll(const wchar_t *_Str1,const wchar_t *_Str2,size_t _MaxCount); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsncoll_l(const wchar_t *_Str1,const wchar_t *_Str2,size_t _MaxCount,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsnicoll(const wchar_t *_Str1,const wchar_t *_Str2,size_t _MaxCount); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsnicoll_l(const wchar_t *_Str1,const wchar_t *_Str2,size_t _MaxCount,_locale_t _Locale); + wchar_t *__attribute__((__cdecl__)) wcsdup(const wchar_t *_Str) ; + int __attribute__((__cdecl__)) wcsicmp(const wchar_t *_Str1,const wchar_t *_Str2) ; + int __attribute__((__cdecl__)) wcsnicmp(const wchar_t *_Str1,const wchar_t *_Str2,size_t _MaxCount) ; + wchar_t *__attribute__((__cdecl__)) wcsnset(wchar_t *_Str,wchar_t _Val,size_t _MaxCount) ; + wchar_t *__attribute__((__cdecl__)) wcsrev(wchar_t *_Str) ; + wchar_t *__attribute__((__cdecl__)) wcsset(wchar_t *_Str,wchar_t _Val) ; + wchar_t *__attribute__((__cdecl__)) wcslwr(wchar_t *_Str) ; + wchar_t *__attribute__((__cdecl__)) wcsupr(wchar_t *_Str) ; + int __attribute__((__cdecl__)) wcsicoll(const wchar_t *_Str1,const wchar_t *_Str2) ; + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strset_s(char *_Dst,size_t _DstSize,int _Value); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strerror_s(char *_Buf,size_t _SizeInBytes,const char *_ErrMsg); + + __attribute__((dllimport)) errno_t __attribute__((__cdecl__)) strerror_s(char *_Buf,size_t _SizeInBytes,int _ErrNum); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strlwr_s(char *_Str,size_t _Size); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strlwr_s_l(char *_Str,size_t _Size,_locale_t _Locale); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strnset_s(char *_Str,size_t _Size,int _Val,size_t _MaxCount); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strupr_s(char *_Str,size_t _Size); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strupr_s_l(char *_Str,size_t _Size,_locale_t _Locale); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) strncat_s(char *_Dst,size_t _DstSizeInChars,const char *_Src,size_t _MaxCount); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strncat_s_l(char *_Dst,size_t _DstSizeInChars,const char *_Src,size_t _MaxCount,_locale_t _Locale); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) strcpy_s(char *_Dst, rsize_t _SizeInBytes, const char *_Src); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) strncpy_s(char *_Dst, size_t _DstSizeInChars, const char *_Src, size_t _MaxCount); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strncpy_s_l(char *_Dst, size_t _DstSizeInChars, const char *_Src, size_t _MaxCount, _locale_t _Locale); + + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) strtok_s(char *_Str,const char *_Delim,char **_Context); + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) _strtok_s_l(char *_Str,const char *_Delim,char **_Context,_locale_t _Locale); + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) strcat_s(char *_Dst, rsize_t _SizeInBytes, const char * _Src); + + extern __inline__ __attribute__((__always_inline__,__gnu_inline__)) size_t __attribute__((__cdecl__)) strnlen_s(const char * _src, size_t _count) { + return _src ? strnlen(_src, _count) : 0; + } + __attribute__((dllimport)) errno_t __attribute__((__cdecl__)) memmove_s(void *_dest,size_t _numberOfElements,const void *_src,size_t _count); + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) wcstok_s(wchar_t *_Str,const wchar_t *_Delim,wchar_t **_Context); + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcserror_s(wchar_t *_Buf,size_t _SizeInWords,int _ErrNum); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) __wcserror_s(wchar_t *_Buffer,size_t _SizeInWords,const wchar_t *_ErrMsg); + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcsnset_s(wchar_t *_Dst,size_t _DstSizeInWords,wchar_t _Val,size_t _MaxCount); + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcsset_s(wchar_t *_Str,size_t _SizeInWords,wchar_t _Val); + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcslwr_s(wchar_t *_Str,size_t _SizeInWords); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcslwr_s_l(wchar_t *_Str,size_t _SizeInWords,_locale_t _Locale); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcsupr_s(wchar_t *_Str,size_t _Size); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcsupr_s_l(wchar_t *_Str,size_t _Size,_locale_t _Locale); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) wcscpy_s(wchar_t *_Dst, rsize_t _SizeInWords, const wchar_t *_Src); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) wcscat_s(wchar_t * _Dst, rsize_t _SizeInWords, const wchar_t *_Src); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) wcsncat_s(wchar_t *_Dst,size_t _DstSizeInChars,const wchar_t *_Src,size_t _MaxCount); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcsncat_s_l(wchar_t *_Dst,size_t _DstSizeInChars,const wchar_t *_Src,size_t _MaxCount,_locale_t _Locale); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) wcsncpy_s(wchar_t *_Dst, size_t _DstSizeInChars, const wchar_t *_Src, size_t _MaxCount); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcsncpy_s_l(wchar_t *_Dst, size_t _DstSizeInChars, const wchar_t *_Src, size_t _MaxCount, _locale_t _Locale); + + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcstok_s_l(wchar_t *_Str,const wchar_t *_Delim,wchar_t **_Context,_locale_t _Locale); + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcsset_s_l(wchar_t *_Str,size_t _SizeInChars,wchar_t _Val,_locale_t _Locale); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcsnset_s_l(wchar_t *_Str,size_t _SizeInChars,wchar_t _Val, size_t _Count,_locale_t _Locale); + + extern __inline__ __attribute__((__always_inline__,__gnu_inline__)) size_t __attribute__((__cdecl__)) wcsnlen_s(const wchar_t * _src, size_t _count) { + return _src ? wcsnlen(_src, _count) : 0; + } +_Static_assert(1U > 0U, "Command needs instance storage"); +_Static_assert(4U > 0U && + 4U < 0xffffffffU / 2U, "Command needs bounded reply storage"); +typedef struct { + IEC104_SlaveEventIdentity_t identity; + uint8_t request_bytes[10], reply_bytes[10]; + IEC104_Station_t *station; + uint64_t transaction_id, receive_serial, accepted_ms, timer_start_ms; + uint64_t event_serial, accepted_event_serial, select_event_serial, execute_event_serial; + uint64_t confirm_serial, term_serial, reply_serial; + uint8_t phase, event_type, finish_reason, result, confirm_cot, reply_cot; + _Bool occupied, event_taken, ever_work_delivered, finish_pending; + _Bool select_delivered, execute_delivered, selected; + _Bool confirm_negative, confirm_try, send_waiting, return_waiting; + _Bool confirm_submitted, completed, term_submitted, select_effect, execute_effect; +} CommandTransaction; +typedef struct { + IEC104_SlaveEventIdentity_t identity; + uint8_t bytes[10]; + uint64_t serial; + uint8_t cot; + _Bool occupied, send_waiting; +} CommandReply; +struct IEC104_Command { + IEC104_Slave_t *slave; + IEC104_SlaveEventIdentity_t session; + IEC104_CommandConfig_t config; + IEC104_CommandStatus_t status; + CommandTransaction transaction; + CommandReply replies[4U]; + uint64_t serial, next_transaction, next_event, next_reply, last_generation; + _Bool in_use, in_action; +}; +static IEC104_Command_t command_pool[1U]; +static uint64_t next_instance; +static _Bool Owned(const IEC104_Command_t *self) +{ + unsigned i; + for (i = 0U; i < 1U; ++i) { + if (self == &command_pool[i]) { return self->in_use; } + } + return 0; +} +static uint32_t Read_IOA(const uint8_t *bytes) +{ + return (uint32_t)bytes[6] | ((uint32_t)bytes[7] << 8) | ((uint32_t)bytes[8] << 16); +} +static uint16_t Read_CA(const uint8_t *bytes) +{ + return (uint16_t)((uint16_t)bytes[4] | ((uint16_t)bytes[5] << 8)); +} +static void Copy_Request(const CommandTransaction *task, IEC104_CommandRequest_t *request) +{ + memset(request, 0, sizeof(*request)); + request->generation = task->identity.generation; + request->receive_serial = task->receive_serial; + request->station_id = IEC104_Station_Get_Id(task->station); + request->common_address = Read_CA(task->request_bytes); + request->originator_address = task->request_bytes[3]; + request->type_id = task->request_bytes[0]; + request->ioa = Read_IOA(task->request_bytes); + request->is_select = (task->request_bytes[9] & 0x80U) != 0U; + request->selected = task->selected; + request->state = task->request_bytes[9] & (request->type_id == 45U ? 1U : 3U); + request->qualifier = (task->request_bytes[9] >> 2) & 31U; + request->accepted_ms = task->accepted_ms; +} +static unsigned Pending_Replies(const IEC104_Command_t *self) +{ + const CommandTransaction *task = &self->transaction; + unsigned i, count = (task->confirm_serial != 0U ? 1U : 0U) + + (task->term_serial != 0U ? 1U : 0U) + (task->reply_serial != 0U ? 1U : 0U); + for (i = 0U; i < 4U; ++i) { + if (self->replies[i].occupied) { ++count; } + } + return count; +} +static void Reap(IEC104_Command_t *self) +{ + CommandTransaction *task = &self->transaction; + if (!task->occupied || task->phase != IEC104_COMMAND_PHASE_ENDED || task->finish_pending || + task->confirm_serial != 0U || task->term_serial != 0U || task->reply_serial != 0U) { return; } + (void)IEC104_Slave_Release_Station(self->slave, task->station); + memset(task, 0, sizeof(*task)); +} +static void Finish(IEC104_Command_t *self, IEC104_CommandFinishReason_t reason) +{ + CommandTransaction *task = &self->transaction; + if (!task->occupied || task->phase == IEC104_COMMAND_PHASE_ENDED) { Reap(self); return; } + task->phase = IEC104_COMMAND_PHASE_ENDED; + task->finish_reason = (uint8_t)reason; + task->event_type = IEC104_COMMAND_FINISHED; + task->event_taken = 0; + task->finish_pending = task->ever_work_delivered; + task->return_waiting = 0; + if (task->finish_pending) { task->event_serial = ++self->next_event; } + switch (reason) { + case IEC104_COMMAND_FINISH_COMPLETED: ++self->status.completed; break; + case IEC104_COMMAND_FINISH_EXECUTION_FAILED: ++self->status.execution_failed; break; + case IEC104_COMMAND_FINISH_ACTION_FAILED: ++self->status.action_failed; break; + case IEC104_COMMAND_FINISH_MISMATCH: ++self->status.mismatches; break; + case IEC104_COMMAND_FINISH_DEACTIVATED: ++self->status.deactivated; break; + case IEC104_COMMAND_FINISH_SELECT_TIMEOUT: ++self->status.select_timeouts; break; + case IEC104_COMMAND_FINISH_DECISION_TIMEOUT: ++self->status.decision_timeouts; break; + case IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT: ++self->status.execute_timeouts; break; + case IEC104_COMMAND_FINISH_CANCELLED: + case IEC104_COMMAND_FINISH_STOPPED: + case IEC104_COMMAND_FINISH_DISCONNECTED: + case IEC104_COMMAND_FINISH_CONNECTION_CHANGED: + case IEC104_COMMAND_FINISH_SEND_ERROR: + case IEC104_COMMAND_FINISH_SERVICE_ERROR: ++self->status.cancelled; break; + default: break; + } + Reap(self); +} +static void Cancel_All(IEC104_Command_t *self, IEC104_CommandFinishReason_t reason) +{ + CommandTransaction *task = &self->transaction; + task->confirm_serial = task->term_serial = task->reply_serial = 0U; + task->send_waiting = 0; + memset(self->replies, 0, sizeof(self->replies)); + Finish(self, reason); +} +static IEC104_SlaveRequestState_t Check_Session(IEC104_Command_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) { + Cancel_All(self, IEC104_COMMAND_FINISH_SERVICE_ERROR); + self->status.active = 0; + } else if (state != IEC104_SLAVE_REQUEST_WAIT_EVENTS && + (!self->status.active || state != IEC104_SLAVE_REQUEST_READY)) { + Cancel_All(self, IEC104_COMMAND_FINISH_CONNECTION_CHANGED); + self->status.active = 0; + state = IEC104_SLAVE_REQUEST_STALE; + } + return state; +} +static uint32_t Timer_Limit(const IEC104_Command_t *self) +{ + const CommandTransaction *task = &self->transaction; + if (!task->occupied || self->in_action) { return 0U; } + switch (task->phase) { + case IEC104_COMMAND_PHASE_SELECT_WAIT: + case IEC104_COMMAND_PHASE_EXECUTE_WAIT: return self->config.decision_timeout_ms; + case IEC104_COMMAND_PHASE_SELECTED: return self->config.select_timeout_ms; + case IEC104_COMMAND_PHASE_EXECUTING: + return task->result == 0U ? self->config.execute_timeout_ms : 0U; + default: return 0U; + } +} +static uint32_t Remaining(const CommandTransaction *task, uint32_t limit, uint64_t now) +{ + if (now < task->timer_start_ms || now - task->timer_start_ms >= limit) { return 0U; } + return limit - (uint32_t)(now - task->timer_start_ms); +} +static _Bool Clock_Reversed(const IEC104_Command_t *self, uint64_t now) +{ + const CommandTransaction *task = &self->transaction; + return !self->in_action && task->occupied && now < task->timer_start_ms && + (task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT || task->phase == IEC104_COMMAND_PHASE_SELECTED || + task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT || + (task->phase == IEC104_COMMAND_PHASE_EXECUTING && task->result == 0U)); +} +static uint64_t Reply_Serial(IEC104_Command_t *self) +{ + if (self->next_reply == 0xffffffffffffffffULL) { + Cancel_All(self, IEC104_COMMAND_FINISH_SERVICE_ERROR); + self->status.active = 0; + return 0U; + } + return ++self->next_reply; +} +static _Bool Set_Confirm(IEC104_Command_t *self, uint8_t cause, _Bool negative, _Bool try_send) +{ + CommandTransaction *task = &self->transaction; + uint64_t serial = Reply_Serial(self); + if (serial == 0U) { return 0; } + task->confirm_serial = serial; + task->confirm_cot = cause; + task->confirm_negative = negative; + task->confirm_try = try_send; + task->confirm_submitted = 0; + task->send_waiting = 0; + return 1; +} +static void Expire(IEC104_Command_t *self, uint64_t now) +{ + CommandTransaction *task = &self->transaction; + uint32_t limit = Timer_Limit(self); + _Bool reversed = Clock_Reversed(self, now); + if (!reversed && (limit == 0U || Remaining(task, limit, now) != 0U)) { return; } + if (reversed) { ++self->status.clock_errors; } + switch (task->phase) { + case IEC104_COMMAND_PHASE_SELECT_WAIT: + case IEC104_COMMAND_PHASE_EXECUTE_WAIT: + if (Set_Confirm(self, IEC60870_ASDU_COT_ACTIVATION_CON, 1, 0)) { + Finish(self, IEC104_COMMAND_FINISH_DECISION_TIMEOUT); + } + break; + case IEC104_COMMAND_PHASE_SELECTED: + task->confirm_serial = 0U; + Finish(self, IEC104_COMMAND_FINISH_SELECT_TIMEOUT); + break; + case IEC104_COMMAND_PHASE_EXECUTING: + Finish(self, IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT); + break; + default: break; + } +} +static _Bool Defer_For_Stop(IEC104_Command_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, ((void *)0)) != 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 0; } + self->status.events_pending = 1; + return 1; +} +enum { COMMAND_HEAD_NONE, COMMAND_HEAD_CONFIRM, COMMAND_HEAD_TERM, COMMAND_HEAD_REPLY, COMMAND_HEAD_SHORT }; +static unsigned Reply_Head(const IEC104_Command_t *self, unsigned *slot) +{ + const CommandTransaction *task = &self->transaction; + uint64_t first = 0U; + unsigned head = COMMAND_HEAD_NONE, i; + if (task->confirm_serial != 0U) { first = task->confirm_serial; head = COMMAND_HEAD_CONFIRM; } + if (task->term_serial != 0U && (first == 0U || task->term_serial < first)) { + first = task->term_serial; head = COMMAND_HEAD_TERM; + } + if (task->reply_serial != 0U && (first == 0U || task->reply_serial < first)) { + first = task->reply_serial; head = COMMAND_HEAD_REPLY; + } + for (i = 0U; i < 4U; ++i) { + if (self->replies[i].occupied && (first == 0U || self->replies[i].serial < first)) { + first = self->replies[i].serial; head = COMMAND_HEAD_SHORT; *slot = i; + } + } + return head; +} +static void Complete_If_Sent(IEC104_Command_t *self) +{ + CommandTransaction *task = &self->transaction; + if (task->occupied && task->phase == IEC104_COMMAND_PHASE_EXECUTING && task->completed && + task->confirm_submitted && task->confirm_serial == 0U && task->term_serial == 0U) { + Finish(self, task->result == IEC104_COMMAND_RESULT_SUCCEEDED ? + IEC104_COMMAND_FINISH_COMPLETED : IEC104_COMMAND_FINISH_EXECUTION_FAILED); + } + Reap(self); +} +static int Submit_Head(IEC104_Command_t *self) +{ + CommandTransaction *task = &self->transaction; + const IEC104_SlaveEventIdentity_t *identity; + const uint8_t *source; + uint8_t bytes[10], cause; + IEC60870_Asdu_t asdu; + IEC104_TcpSubmitResult_t submitted; + IEC104_SlaveRequestState_t state = Check_Session(self); + unsigned slot = 0U, head; + _Bool negative, try_send; + if (state == IEC104_SLAVE_REQUEST_ERROR) { return -1; } + if (state != IEC104_SLAVE_REQUEST_READY) { return 0; } + head = Reply_Head(self, &slot); + if (head == COMMAND_HEAD_NONE) { return 0; } + identity = &task->identity; + source = task->request_bytes; + cause = IEC60870_ASDU_COT_ACTIVATION_TERMINATION; + negative = 0; + try_send = 1; + if (head == COMMAND_HEAD_CONFIRM) { + cause = task->confirm_cot; negative = task->confirm_negative; try_send = task->confirm_try; + } else if (head == COMMAND_HEAD_REPLY) { + source = task->reply_bytes; cause = task->reply_cot; + negative = cause != IEC60870_ASDU_COT_DEACTIVATION_CON; try_send = 0; + } else if (head == COMMAND_HEAD_SHORT) { + identity = &self->replies[slot].identity; + source = self->replies[slot].bytes; cause = self->replies[slot].cot; + negative = 1; try_send = 0; + } + memcpy(bytes, source, sizeof(bytes)); + if (IEC60870_Asdu_Create_From_Buffer(&asdu, IEC104_Slave_Get_Application_Layer_Parameters(self->slave), + bytes, sizeof(bytes)) != IEC60870_ASDU_OK || + IEC60870_Asdu_Set_COT(&asdu, cause) != IEC60870_ASDU_OK || + IEC60870_Asdu_Set_Negative(&asdu, negative) != IEC60870_ASDU_OK) { + Cancel_All(self, IEC104_COMMAND_FINISH_SERVICE_ERROR); + return -1; + } + submitted = try_send ? IEC104_Slave_Try_Send_Response(self->slave, identity, &asdu) : + IEC104_Slave_Send_Response(self->slave, identity, &asdu); + if (submitted == IEC104_TCP_SUBMIT_OK) { + ++self->status.submitted; + if (negative) { ++self->status.negative_submitted; } + if (head == COMMAND_HEAD_SHORT) { memset(&self->replies[slot], 0, sizeof(self->replies[slot])); } + else { + task->send_waiting = 0; + if (head == COMMAND_HEAD_CONFIRM) { task->confirm_serial = 0U; task->confirm_submitted = 1; } + else if (head == COMMAND_HEAD_TERM) { task->term_serial = 0U; task->term_submitted = 1; } + else { task->reply_serial = 0U; } + } + Complete_If_Sent(self); + return 1; + } + if (submitted == IEC104_TCP_SUBMIT_WAIT) { + if (head == COMMAND_HEAD_SHORT) { self->replies[slot].send_waiting = 1; } + else { task->send_waiting = 1; } + ++self->status.waits; + return 0; + } + if (Defer_For_Stop(self, identity, submitted)) { return 0; } + Cancel_All(self, IEC104_COMMAND_FINISH_SEND_ERROR); + self->status.active = 0; + return 0; +} +static _Bool Short_Reply(IEC104_Command_t *self, const IEC104_SlaveEventIdentity_t *identity, + const uint8_t *data, uint8_t cause) +{ + unsigned slot; + _Bool empty = Pending_Replies(self) == 0U; + uint64_t serial; + for (slot = 0U; slot < 4U; ++slot) { + if (!self->replies[slot].occupied) { break; } + } + if (slot == 4U) { + ++self->status.overloads; + ++self->status.dropped_requests; + return 1; + } + serial = Reply_Serial(self); + if (serial == 0U) { return 0; } + self->replies[slot].identity = *identity; + memcpy(self->replies[slot].bytes, data, sizeof(self->replies[slot].bytes)); + self->replies[slot].bytes[1] = 1U; + self->replies[slot].serial = serial; + self->replies[slot].cot = cause; + self->replies[slot].occupied = 1; + self->replies[slot].send_waiting = 0; + return !empty || Submit_Head(self) >= 0; +} +static void Work_Event(IEC104_Command_t *self, const uint8_t *data, uint64_t receive_serial, + uint64_t now, _Bool selected) +{ + CommandTransaction *task = &self->transaction; + memcpy(task->request_bytes, data, sizeof(task->request_bytes)); + task->request_bytes[1] = 1U; + task->receive_serial = receive_serial; + task->accepted_ms = task->timer_start_ms = now; + task->event_type = (data[9] & 0x80U) != 0U ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE; + task->phase = task->event_type == IEC104_COMMAND_SELECT ? + IEC104_COMMAND_PHASE_SELECT_WAIT : IEC104_COMMAND_PHASE_EXECUTE_WAIT; + task->event_serial = ++self->next_event; + task->accepted_event_serial = 0U; + if (task->event_type == IEC104_COMMAND_SELECT) { task->select_event_serial = task->event_serial; } + else { task->execute_event_serial = task->event_serial; } + task->event_taken = 0; + task->selected = selected; + task->confirm_submitted = 0; + task->send_waiting = 0; + ++self->status.accepted; +} +static _Bool End_With_Reply(IEC104_Command_t *self, const uint8_t *data, uint8_t cause, + IEC104_CommandFinishReason_t reason) +{ + CommandTransaction *task = &self->transaction; + uint64_t serial = Reply_Serial(self); + if (serial == 0U) { return 0; } + task->confirm_serial = task->term_serial = 0U; + memcpy(task->reply_bytes, data, sizeof(task->reply_bytes)); + task->reply_bytes[1] = 1U; + task->reply_cot = cause; + task->reply_serial = serial; + task->send_waiting = 0; + Finish(self, reason); + return Submit_Head(self) >= 0; +} +static IEC104_CommandEventResult_t Request(IEC104_Command_t *self, const IEC104_SlaveEvent_t *event, + const IEC104_SlaveEventIdentity_t *identity) +{ + CommandTransaction *task = &self->transaction; + IEC60870_AsduInfo_t info; + IEC104_Station_t *station; + const uint8_t *data; + uint16_t length; + uint8_t cause = 0U, state; + uint32_t ioa; + uint64_t now; + _Bool same_point, is_select; + 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] != 45U && data[0] != 46U) || + (self->config.type_mask & ((uint16_t)( ((data[0]) >= 45U && (data[0]) <= 51U) ? (1U << ((data[0]) - 45U)) : (((data[0]) >= 58U && (data[0]) <= 64U) ? (1U << ((data[0]) - 51U)) : 0U)))) == 0U || + !IEC60870_Parse_ASDU_Header(event->asdu.parameters, data, length, &info)) { + return IEC104_COMMAND_EVENT_IGNORED; + } + now = IEC104_Net_Now_Ms(); + Expire(self, now); + if (info.object_count != 1U || info.information_objects_len < 4U) { + ++self->status.rejected; + return IEC104_COMMAND_EVENT_HANDLED; + } + ioa = Read_IOA(data); + station = IEC104_Slave_Get_Station(self->slave, info.common_address); + state = data[9] & 3U; + is_select = (data[9] & 0x80U) != 0U; + if (info.cause_of_transmission != IEC60870_ASDU_COT_ACTIVATION && + info.cause_of_transmission != IEC60870_ASDU_COT_DEACTIVATION) { + cause = IEC60870_ASDU_COT_UNKNOWN_CAUSE; + } else if (station == ((void *)0)) { cause = IEC60870_ASDU_COT_UNKNOWN_COMMON_ADDRESS; } + else if (ioa == 0U) { cause = IEC60870_ASDU_COT_UNKNOWN_IOA; } + else if (info.is_sequence || length != 10U || info.is_negative_confirm || info.is_test || + (data[0] == 45U ? (data[9] & 2U) != 0U : (state == 0U || state == 3U))) { + cause = info.cause_of_transmission == IEC60870_ASDU_COT_DEACTIVATION ? + IEC60870_ASDU_COT_DEACTIVATION_CON : IEC60870_ASDU_COT_ACTIVATION_CON; + } + if (cause != 0U) { + ++self->status.rejected; + return Short_Reply(self, identity, data, cause) ? IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + same_point = task->occupied && task->identity.generation == identity->generation && + IEC104_Station_Get_Id(task->station) == IEC104_Station_Get_Id(station) && + Read_IOA(task->request_bytes) == ioa; + if (info.cause_of_transmission == IEC60870_ASDU_COT_DEACTIVATION) { + if (same_point && task->request_bytes[0] == data[0] && task->request_bytes[3] == data[3] && + (task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT || task->phase == IEC104_COMMAND_PHASE_SELECTED || + task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT)) { + return End_With_Reply(self, data, IEC60870_ASDU_COT_DEACTIVATION_CON, + IEC104_COMMAND_FINISH_DEACTIVATED) ? IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + ++self->status.rejected; + return Short_Reply(self, identity, data, IEC60870_ASDU_COT_DEACTIVATION_CON) ? + IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + if (same_point && !is_select && task->phase == IEC104_COMMAND_PHASE_SELECTED && task->confirm_submitted) { + if (task->request_bytes[0] != data[0] || task->request_bytes[3] != data[3] || + (task->request_bytes[9] & 0x7FU) != (data[9] & 0x7FU)) { + ++self->status.rejected; + return End_With_Reply(self, data, IEC60870_ASDU_COT_ACTIVATION_CON, + IEC104_COMMAND_FINISH_MISMATCH) ? IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + Work_Event(self, data, event->transport.data.i_frame.receive_serial, now, 1); + return IEC104_COMMAND_EVENT_HANDLED; + } + if (task->occupied || (!is_select && (self->config.select_required_mask & ((uint16_t)( ((data[0]) >= 45U && (data[0]) <= 51U) ? (1U << ((data[0]) - 45U)) : (((data[0]) >= 58U && (data[0]) <= 64U) ? (1U << ((data[0]) - 51U)) : 0U)))) != 0U)) { + ++self->status.rejected; + if (task->occupied) { ++self->status.busy_rejected; } + return Short_Reply(self, identity, data, IEC60870_ASDU_COT_ACTIVATION_CON) ? + IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + if (self->next_transaction == 0xffffffffffffffffULL || self->next_event > 0xffffffffffffffffULL - 3U) { + ++self->status.overloads; + ++self->status.dropped_requests; + return IEC104_COMMAND_EVENT_HANDLED; + } + if (IEC104_Slave_Acquire_Station(self->slave, station) != IEC104_STATION_OK) { return IEC104_COMMAND_EVENT_ERROR; } + memset(task, 0, sizeof(*task)); + task->occupied = 1; + task->identity = *identity; + task->station = station; + task->transaction_id = ++self->next_transaction; + Work_Event(self, data, event->transport.data.i_frame.receive_serial, now, 0); + return IEC104_COMMAND_EVENT_HANDLED; +} +void IEC104_Command_Config_Default(IEC104_CommandConfig_t *config) +{ + if (config == ((void *)0)) { return; } + memset(config, 0, sizeof(*config)); + config->type_mask = ((uint16_t)( ((45U) >= 45U && (45U) <= 51U) ? (1U << ((45U) - 45U)) : (((45U) >= 58U && (45U) <= 64U) ? (1U << ((45U) - 51U)) : 0U))) | ((uint16_t)( ((46U) >= 45U && (46U) <= 51U) ? (1U << ((46U) - 45U)) : (((46U) >= 58U && (46U) <= 64U) ? (1U << ((46U) - 51U)) : 0U))); + config->select_required_mask = config->type_mask; + config->select_timeout_ms = 30000U; + config->decision_timeout_ms = 15000U; + config->execute_timeout_ms = 60000U; + config->send_act_term = 1; +} +IEC104_Command_t *IEC104_Command_Create(IEC104_Slave_t *slave, const IEC104_CommandConfig_t *config) +{ + const uint16_t supported = ((uint16_t)( ((45U) >= 45U && (45U) <= 51U) ? (1U << ((45U) - 45U)) : (((45U) >= 58U && (45U) <= 64U) ? (1U << ((45U) - 51U)) : 0U))) | ((uint16_t)( ((46U) >= 45U && (46U) <= 51U) ? (1U << ((46U) - 45U)) : (((46U) >= 58U && (46U) <= 64U) ? (1U << ((46U) - 51U)) : 0U))); + IEC104_Command_t *free_slot = ((void *)0); + unsigned i; + if (config == ((void *)0) || config->type_mask == 0U || (config->type_mask & (uint16_t)~supported) != 0U || + (config->select_required_mask & (uint16_t)~config->type_mask) != 0U || config->select_timeout_ms == 0U || + next_instance == 0xffffffffffffffffULL || IEC104_Slave_Get_Application_Layer_Parameters(slave) == ((void *)0)) { return ((void *)0); } + for (i = 0U; i < 1U; ++i) { + if (command_pool[i].in_use && command_pool[i].slave == slave) { return ((void *)0); } + if (!command_pool[i].in_use && free_slot == ((void *)0)) { free_slot = &command_pool[i]; } + } + if (free_slot == ((void *)0)) { return ((void *)0); } + memset(free_slot, 0, sizeof(*free_slot)); + free_slot->slave = slave; + free_slot->config = *config; + free_slot->serial = ++next_instance; + free_slot->in_use = 1; + return free_slot; +} +_Bool IEC104_Command_Destroy(IEC104_Command_t *self) +{ + if (!Owned(self) || self->in_action || self->transaction.occupied || Pending_Replies(self) != 0U) { return 0; } + memset(self, 0, sizeof(*self)); + return 1; +} +void IEC104_Command_Cancel(IEC104_Command_t *self) +{ + if (Owned(self) && !self->in_action) { Cancel_All(self, IEC104_COMMAND_FINISH_CANCELLED); } +} +IEC104_CommandEventResult_t IEC104_Command_Handle_Event(IEC104_Command_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_COMMAND_EVENT_ERROR; + } + self->session.slave = identity.slave; + self->session.instance_id = identity.instance_id; + if (event->transport.type == IEC104_TCP_EVENT_CONNECTED) { + if (identity.generation <= self->last_generation) { return IEC104_COMMAND_EVENT_IGNORED; } + Cancel_All(self, IEC104_COMMAND_FINISH_CONNECTION_CHANGED); + self->session.generation = self->last_generation = identity.generation; + self->status.active = 0; + return IEC104_COMMAND_EVENT_HANDLED; + } + if (identity.generation != self->session.generation || identity.generation == 0U) { + return IEC104_COMMAND_EVENT_IGNORED; + } + switch (event->transport.type) { + case IEC104_TCP_EVENT_ACTIVATED: + self->status.active = 1; + return IEC104_COMMAND_EVENT_HANDLED; + case IEC104_TCP_EVENT_DEACTIVATED: + Cancel_All(self, IEC104_COMMAND_FINISH_STOPPED); + self->status.active = 0; + return IEC104_COMMAND_EVENT_HANDLED; + case IEC104_TCP_EVENT_DISCONNECTED: + Cancel_All(self, IEC104_COMMAND_FINISH_DISCONNECTED); + self->session.generation = 0U; + self->status.active = 0; + return IEC104_COMMAND_EVENT_HANDLED; + case IEC104_TCP_EVENT_I_FRAME: return Request(self, event, &identity); + default: return IEC104_COMMAND_EVENT_IGNORED; + } +} +_Bool IEC104_Command_Process(IEC104_Command_t *self) +{ + unsigned step; + if (!Owned(self) || self->in_action) { return 0; } + for (step = 0U; step < 2U * (1U + 4U); ++step) { + IEC104_SlaveRequestState_t state = Check_Session(self); + int submitted; + if (state == IEC104_SLAVE_REQUEST_ERROR) { return 0; } + if (state != IEC104_SLAVE_REQUEST_READY) { return 1; } + Expire(self, IEC104_Net_Now_Ms()); + Complete_If_Sent(self); + submitted = Submit_Head(self); + if (submitted <= 0) { return submitted == 0; } + } + return 1; +} +IEC104_CommandNextResult_t IEC104_Command_NextEvent(IEC104_Command_t *self, IEC104_CommandEvent_t *event) +{ + CommandTransaction *task; + IEC104_CommandEvent_t result; + IEC104_SlaveRequestState_t state = IEC104_SLAVE_REQUEST_READY; + if (!Owned(self) || self->in_action || event == ((void *)0)) { return IEC104_COMMAND_NEXT_ERROR; } + task = &self->transaction; + if (!task->finish_pending) { + state = Check_Session(self); + if (state == IEC104_SLAVE_REQUEST_READY) { Expire(self, IEC104_Net_Now_Ms()); } + } + if (!task->finish_pending && state == IEC104_SLAVE_REQUEST_ERROR) { return IEC104_COMMAND_NEXT_ERROR; } + if (!task->occupied || (!task->finish_pending && + (state != IEC104_SLAVE_REQUEST_READY || task->event_taken || + (task->phase != IEC104_COMMAND_PHASE_SELECT_WAIT && task->phase != IEC104_COMMAND_PHASE_EXECUTE_WAIT)))) { + return IEC104_COMMAND_NEXT_EMPTY; + } + memset(&result, 0, sizeof(result)); + result.type = (IEC104_CommandEventType_t)task->event_type; + result.instance_id = self->serial; + result.transaction_id = task->transaction_id; + result.event_serial = task->event_serial; + Copy_Request(task, &result.request); + if (task->finish_pending) { + result.reason = (IEC104_CommandFinishReason_t)task->finish_reason; + result.select_effect = task->select_effect; + result.execute_effect = task->execute_effect; + task->finish_pending = 0; + } else { + task->ever_work_delivered = 1; + if (result.type == IEC104_COMMAND_SELECT) { task->select_delivered = 1; } + else { task->execute_delivered = 1; } + } + task->event_taken = 1; + *event = result; + Reap(self); + return IEC104_COMMAND_NEXT_READY; +} +static _Bool Current_Event(const IEC104_Command_t *self, const IEC104_CommandEvent_t *event) +{ + const CommandTransaction *task = &self->transaction; + return event != ((void *)0) && task->occupied && event->instance_id == self->serial && + event->transaction_id == task->transaction_id && event->event_serial == task->event_serial && + event->type == task->event_type && task->event_taken; +} +static _Bool Reply_Stage(const IEC104_Command_t *self, const IEC104_CommandEvent_t *event, _Bool work) +{ + const CommandTransaction *task = &self->transaction; + if (!Current_Event(self, event)) { return 0; } + if (work) { + return task->accepted_event_serial == 0U && + ((event->type == IEC104_COMMAND_SELECT && task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT) || + (event->type == IEC104_COMMAND_EXECUTE && task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT)); + } + return event->type == IEC104_COMMAND_EXECUTE && task->phase == IEC104_COMMAND_PHASE_EXECUTING && + task->accepted_event_serial == event->event_serial && task->execute_effect; +} +static IEC104_CommandReplyResult_t Check_Reply(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, _Bool work) +{ + IEC104_SlaveRequestState_t state; + if (!Owned(self) || self->in_action || event == ((void *)0)) { return IEC104_COMMAND_REPLY_ERROR; } + if (!Reply_Stage(self, event, work)) { return IEC104_COMMAND_REPLY_STALE; } + state = Check_Session(self); + if (state == IEC104_SLAVE_REQUEST_ERROR) { return IEC104_COMMAND_REPLY_ERROR; } + if (state == IEC104_SLAVE_REQUEST_WAIT_EVENTS) { return IEC104_COMMAND_REPLY_WAIT; } + if (state != IEC104_SLAVE_REQUEST_READY) { return IEC104_COMMAND_REPLY_STALE; } + Expire(self, IEC104_Net_Now_Ms()); + return Reply_Stage(self, event, work) ? IEC104_COMMAND_REPLY_OK : IEC104_COMMAND_REPLY_STALE; +} +IEC104_CommandReplyResult_t IEC104_Command_Accept(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandAction_t action, void *context) +{ + CommandTransaction *task; + IEC104_CommandRequest_t request; + IEC104_CommandActionResult_t action_result; + IEC104_CommandReplyResult_t checked; + _Bool selecting; + if (event != ((void *)0) && event->type == IEC104_COMMAND_EXECUTE && action == ((void *)0)) { + return IEC104_COMMAND_REPLY_ERROR; + } + checked = Check_Reply(self, event, 1); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + task = &self->transaction; + selecting = task->event_type == IEC104_COMMAND_SELECT; + if (!Set_Confirm(self, IEC60870_ASDU_COT_ACTIVATION_CON, 0, !selecting)) { + return IEC104_COMMAND_REPLY_ERROR; + } + Copy_Request(task, &request); + task->accepted_event_serial = task->event_serial; + self->in_action = 1; + action_result = action == ((void *)0) ? IEC104_COMMAND_ACTION_STARTED : action(context, &request); + self->in_action = 0; + if (action_result != IEC104_COMMAND_ACTION_STARTED && action_result != IEC104_COMMAND_ACTION_NOT_STARTED) { + action_result = IEC104_COMMAND_ACTION_FAILED; + } + if (selecting) { task->select_effect = action != ((void *)0) && action_result != IEC104_COMMAND_ACTION_NOT_STARTED; } + else { task->execute_effect = action_result != IEC104_COMMAND_ACTION_NOT_STARTED; } + if (action_result == IEC104_COMMAND_ACTION_STARTED) { + task->phase = selecting ? IEC104_COMMAND_PHASE_SELECTED : IEC104_COMMAND_PHASE_EXECUTING; + task->timer_start_ms = IEC104_Net_Now_Ms(); + if (selecting) { ++self->status.selected; } + else { ++self->status.executed; } + } else { + task->confirm_negative = 1; + task->confirm_try = !selecting && action_result == IEC104_COMMAND_ACTION_FAILED; + if (action_result == IEC104_COMMAND_ACTION_NOT_STARTED) { + ++self->status.app_rejected; + Finish(self, IEC104_COMMAND_FINISH_REJECTED); + } else { Finish(self, IEC104_COMMAND_FINISH_ACTION_FAILED); } + } + (void)Submit_Head(self); + return IEC104_COMMAND_REPLY_OK; +} +IEC104_CommandReplyResult_t IEC104_Command_Reject(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandRejectCause_t cause) +{ + IEC104_CommandReplyResult_t checked; + if (cause != IEC104_COMMAND_REJECT_NEGATIVE && cause != IEC104_COMMAND_REJECT_UNKNOWN_IOA) { + return IEC104_COMMAND_REPLY_ERROR; + } + checked = Check_Reply(self, event, 1); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + if (!Set_Confirm(self, cause == IEC104_COMMAND_REJECT_UNKNOWN_IOA ? + IEC60870_ASDU_COT_UNKNOWN_IOA : IEC60870_ASDU_COT_ACTIVATION_CON, 1, 0)) { + return IEC104_COMMAND_REPLY_ERROR; + } + self->transaction.accepted_event_serial = self->transaction.event_serial; + ++self->status.app_rejected; + Finish(self, IEC104_COMMAND_FINISH_REJECTED); + (void)Submit_Head(self); + return IEC104_COMMAND_REPLY_OK; +} +IEC104_CommandReplyResult_t IEC104_Command_Report_Result(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result) +{ + IEC104_CommandReplyResult_t checked; + if (result != IEC104_COMMAND_RESULT_SUCCEEDED && result != IEC104_COMMAND_RESULT_FAILED) { + return IEC104_COMMAND_REPLY_ERROR; + } + checked = Check_Reply(self, event, 0); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + if (self->transaction.result != 0U || self->transaction.completed) { return IEC104_COMMAND_REPLY_ERROR; } + self->transaction.result = (uint8_t)result; + return IEC104_COMMAND_REPLY_OK; +} +IEC104_CommandReplyResult_t IEC104_Command_Submit_Return_Info(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, const IEC60870_Asdu_t *asdu) +{ + CommandTransaction *task; + IEC60870_AsduInfo_t info; + const uint8_t *data; + uint16_t length; + IEC104_TcpSubmitResult_t submitted; + IEC104_CommandReplyResult_t checked = Check_Reply(self, event, 0); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + task = &self->transaction; + if (task->completed || asdu == ((void *)0) || !IEC60870_AppLayerParameters_Valid(asdu->parameters) || + asdu->parameters->sizeOfCOT != 2 || asdu->parameters->sizeOfCA != 2 || asdu->parameters->sizeOfIOA != 3 || + asdu->parameters->maxSizeOfASDU > 249 || + IEC60870_Asdu_Get_Data(asdu, &data, &length) != IEC60870_ASDU_OK || length > 249 || + IEC60870_Asdu_Parse(IEC104_Slave_Get_Application_Layer_Parameters(self->slave), data, length, &info) != + IEC60870_ASDU_OK || + !((info.type_id >= 1U && info.type_id <= 21U) || (info.type_id >= 30U && info.type_id <= 40U)) || + info.object_count == 0U || + (info.cause_of_transmission != IEC60870_ASDU_COT_RETURN_INFO_REMOTE && + info.cause_of_transmission != IEC60870_ASDU_COT_SPONTANEOUS) || + info.is_negative_confirm || info.is_test != ((task->request_bytes[2] >> 7) & 1U) || + info.common_address != Read_CA(task->request_bytes)) { return IEC104_COMMAND_REPLY_ERROR; } + if (!task->confirm_submitted || Pending_Replies(self) != 0U) { return IEC104_COMMAND_REPLY_WAIT; } + submitted = IEC104_Slave_Try_Send_Response(self->slave, &task->identity, asdu); + if (submitted == IEC104_TCP_SUBMIT_WAIT) { + task->return_waiting = 1; + ++self->status.waits; + return IEC104_COMMAND_REPLY_WAIT; + } + if (submitted == IEC104_TCP_SUBMIT_OK) { + task->return_waiting = 0; + ++self->status.submitted; + ++self->status.return_info_submitted; + return IEC104_COMMAND_REPLY_OK; + } + if (Defer_For_Stop(self, &task->identity, submitted)) { return IEC104_COMMAND_REPLY_WAIT; } + Cancel_All(self, IEC104_COMMAND_FINISH_SEND_ERROR); + self->status.active = 0; + return IEC104_COMMAND_REPLY_STALE; +} +IEC104_CommandReplyResult_t IEC104_Command_Complete(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event) +{ + CommandTransaction *task; + IEC104_CommandReplyResult_t checked = Check_Reply(self, event, 0); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + task = &self->transaction; + if (task->result == 0U || task->completed) { return IEC104_COMMAND_REPLY_ERROR; } + if (self->config.send_act_term && + (task->result == IEC104_COMMAND_RESULT_SUCCEEDED || self->config.act_term_on_failure)) { + uint64_t serial = Reply_Serial(self); + if (serial == 0U) { return IEC104_COMMAND_REPLY_ERROR; } + task->term_serial = serial; + } + task->completed = 1; + task->return_waiting = 0; + Complete_If_Sent(self); + return IEC104_COMMAND_REPLY_OK; +} +_Bool IEC104_Command_Get_Transaction(const IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandTransactionInfo_t *info) +{ + const CommandTransaction *task; + IEC104_CommandTransactionInfo_t result; + _Bool delivered; + if (!Owned(self) || event == ((void *)0) || info == ((void *)0)) { return 0; } + task = &self->transaction; + if (!task->occupied || event->instance_id != self->serial || event->transaction_id != task->transaction_id) { + return 0; + } + delivered = (event->type == IEC104_COMMAND_SELECT && task->select_delivered && + event->event_serial == task->select_event_serial) || + (event->type == IEC104_COMMAND_EXECUTE && task->execute_delivered && + event->event_serial == task->execute_event_serial) || + (event->type == IEC104_COMMAND_FINISHED && task->event_type == IEC104_COMMAND_FINISHED && + task->event_taken && event->event_serial == task->event_serial); + if (!delivered) { return 0; } + memset(&result, 0, sizeof(result)); + result.active = task->phase != IEC104_COMMAND_PHASE_ENDED; + result.phase = (IEC104_CommandPhase_t)task->phase; + result.confirm_submitted = task->confirm_submitted; + result.result_reported = task->result != 0U; + result.completed = task->completed; + result.term_submitted = task->term_submitted; + *info = result; + return 1; +} +_Bool IEC104_Command_Get_Status(const IEC104_Command_t *self, IEC104_CommandStatus_t *status) +{ + const CommandTransaction *task; + IEC104_CommandStatus_t result; + IEC104_SlaveRequestState_t state; + uint32_t limit; + uint64_t now; + unsigned slot = 0U, head; + _Bool waiting; + if (!Owned(self) || status == ((void *)0)) { return 0; } + state = IEC104_Slave_Check_Request(self->slave, &self->session); + if (state == IEC104_SLAVE_REQUEST_ERROR) { return 0; } + task = &self->transaction; + result = self->status; + result.generation = self->session.generation; + result.events_pending = state == IEC104_SLAVE_REQUEST_WAIT_EVENTS; + result.busy = task->occupied; + result.pending_replies = Pending_Replies(self); + result.pending_events = task->occupied && (task->finish_pending || + (!task->event_taken && (task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT || + task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT))) ? 1U : 0U; + limit = Timer_Limit(self); + now = IEC104_Net_Now_Ms(); + result.timer_active = limit != 0U; + result.timeout_remaining_ms = limit != 0U ? Remaining(task, limit, now) : 0U; + result.need_process = 0; + head = Reply_Head(self, &slot); + waiting = head == COMMAND_HEAD_SHORT ? self->replies[slot].send_waiting : task->send_waiting; + if (!self->in_action && !result.events_pending) { + if (state != IEC104_SLAVE_REQUEST_READY || !result.active) { + result.need_process = result.pending_replies != 0U || + (task->occupied && task->phase != IEC104_COMMAND_PHASE_ENDED); + } else { + result.need_process = Clock_Reversed(self, now) || + (result.timer_active && result.timeout_remaining_ms == 0U) || + (head != COMMAND_HEAD_NONE && !waiting); + } + } + *status = result; + return 1; +} diff --git a/IEC60870-Slave/.build/command-comments/command-before.i b/IEC60870-Slave/.build/command-comments/command-before.i new file mode 100644 index 00000000..2e5b900f --- /dev/null +++ b/IEC60870-Slave/.build/command-comments/command-before.i @@ -0,0 +1,2425 @@ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" + + + + +#pragma pack(push,_CRT_PACKING) + typedef __builtin_va_list __gnuc_va_list; + typedef __gnuc_va_list va_list; +#pragma pack(pop) +void __attribute__((__cdecl__)) __debugbreak(void); +extern __inline__ __attribute__((__always_inline__,__gnu_inline__)) void __attribute__((__cdecl__)) __debugbreak(void) +{ + __asm__ __volatile__("int {$}3":); +} +void __attribute__((__cdecl__)) __attribute__ ((__noreturn__)) __fastfail(unsigned int _Code); +extern __inline__ __attribute__((__always_inline__,__gnu_inline__)) void __attribute__((__cdecl__)) __attribute__ ((__noreturn__)) __fastfail(unsigned int _Code) +{ + __asm__ __volatile__("int {$}0x29"::"c"(_Code)); + __builtin_unreachable(); +} +const char *__mingw_get_crt_info (void); +#pragma pack(push,_CRT_PACKING) +__extension__ typedef unsigned long long size_t; +__extension__ typedef long long ssize_t; +typedef size_t rsize_t; +__extension__ typedef long long intptr_t; +__extension__ typedef unsigned long long uintptr_t; +__extension__ typedef long long ptrdiff_t; +typedef unsigned short wchar_t; +typedef unsigned short wint_t; +typedef unsigned short wctype_t; +typedef int errno_t; +typedef long __time32_t; +__extension__ typedef long long __time64_t; +typedef __time64_t time_t; +__attribute__ ((__dllimport__)) void __attribute__((__cdecl__)) _invalid_parameter_noinfo(void); +__attribute__ ((__dllimport__)) __attribute__ ((__noreturn__)) void __attribute__((__cdecl__)) _invalid_parameter_noinfo_noreturn(void); +__attribute__ ((__dllimport__)) __attribute__ ((__noreturn__)) void __attribute__((__cdecl__)) _invoke_watson(const wchar_t *expression, const wchar_t *function_name, const wchar_t *file_name, unsigned int line_number, long long unsigned int reserved); +struct threadlocaleinfostruct; +struct threadmbcinfostruct; +typedef struct threadlocaleinfostruct *pthreadlocinfo; +typedef struct threadmbcinfostruct *pthreadmbcinfo; +struct __lc_time_data; +typedef struct localeinfo_struct { + pthreadlocinfo locinfo; + pthreadmbcinfo mbcinfo; +} _locale_tstruct,*_locale_t; +typedef struct tagLC_ID { + unsigned short wLanguage; + unsigned short wCountry; + unsigned short wCodePage; +} LC_ID,*LPLC_ID; +typedef struct threadlocaleinfostruct { + const unsigned short *_locale_pctype; + int _locale_mb_cur_max; + unsigned int _locale_lc_codepage; +} threadlocinfo; +#pragma pack(pop) + __attribute__ ((__dllimport__)) extern int *__attribute__((__cdecl__)) _errno(void); + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _set_errno(int _Value); + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _get_errno(int *_Value); + __attribute__ ((__dllimport__)) extern unsigned long __attribute__((__cdecl__)) __threadid(void); + __attribute__ ((__dllimport__)) extern uintptr_t __attribute__((__cdecl__)) __threadhandle(void); +typedef struct { + long long __max_align_ll __attribute__((__aligned__(__alignof__(long long)))); + long double __max_align_ld __attribute__((__aligned__(__alignof__(long double)))); +} max_align_t; +typedef signed char int8_t; +typedef unsigned char uint8_t; +typedef short int16_t; +typedef unsigned short uint16_t; +typedef int int32_t; +typedef unsigned uint32_t; +__extension__ typedef long long int64_t; +__extension__ typedef unsigned long long uint64_t; +typedef signed char int_least8_t; +typedef unsigned char uint_least8_t; +typedef short int_least16_t; +typedef unsigned short uint_least16_t; +typedef int int_least32_t; +typedef unsigned uint_least32_t; +__extension__ typedef long long int_least64_t; +__extension__ typedef unsigned long long uint_least64_t; +typedef signed char int_fast8_t; +typedef unsigned char uint_fast8_t; +typedef short int_fast16_t; +typedef unsigned short uint_fast16_t; +typedef int int_fast32_t; +typedef unsigned int uint_fast32_t; +__extension__ typedef long long int_fast64_t; +__extension__ typedef unsigned long long uint_fast64_t; +__extension__ typedef long long intmax_t; +__extension__ typedef unsigned long long uintmax_t; +#pragma GCC diagnostic pop +typedef struct { + int sizeOfTypeId; + int sizeOfVSQ; + int sizeOfCOT; + int originatorAddress; + int sizeOfCA; + int sizeOfIOA; + int maxSizeOfASDU; +} IEC60870_AppLayerParameters_t; +typedef struct { + uint8_t type_id; + uint8_t object_count; + uint8_t is_sequence; + uint8_t cause_of_transmission; + uint8_t is_negative_confirm; + uint8_t is_test; + uint8_t originator_address; + uint16_t common_address; + const uint8_t *information_objects; + uint16_t information_objects_len; + const IEC60870_AppLayerParameters_t *parameters; +} IEC60870_AsduInfo_t; +extern int IEC60870_AppLayerParameters_Valid(const IEC60870_AppLayerParameters_t *parameters); +extern int IEC60870_Parse_ASDU_Header(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *asdu, + uint16_t asdu_len, + IEC60870_AsduInfo_t *asdu_info); +typedef struct { + uint16_t milliseconds; +} IEC60870_CP16Time2a_t; +typedef struct { + uint16_t milliseconds; + uint8_t minute; + uint8_t is_invalid; +} IEC60870_CP24Time2a_t; +int IEC60870_Time_To_CP16(const IEC60870_CP16Time2a_t *value, uint8_t *out, size_t capacity); +int IEC60870_Time_To_CP24(const IEC60870_CP24Time2a_t *value, uint8_t *out, size_t capacity); +int IEC60870_Time_From_CP16(const uint8_t *in, size_t length, IEC60870_CP16Time2a_t *value); +int IEC60870_Time_From_CP24(const uint8_t *in, size_t length, IEC60870_CP24Time2a_t *value); +typedef struct { + uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; + uint8_t second; + uint16_t ms; + uint8_t weekday; +} IEC60870_WallTime_t; +typedef struct { + IEC60870_WallTime_t time; + uint8_t is_invalid; + uint8_t is_summer_time; + uint8_t has_weekday; +} IEC60870_CP56Time2a_t; +int IEC60870_Time_Is_Valid(const IEC60870_WallTime_t *time); +int IEC60870_Time_Get_Weekday(uint16_t year, uint8_t month, uint8_t day, uint8_t *weekday); +int IEC60870_Time_To_Seconds(const IEC60870_WallTime_t *time, uint32_t *seconds); +int IEC60870_Time_From_Seconds(uint32_t seconds, uint16_t ms, IEC60870_WallTime_t *time); +int IEC60870_Time_Seconds_To_Local(uint32_t utc_seconds, uint16_t ms, + IEC60870_WallTime_t *local_time); +int IEC60870_Time_CP56_To_Seconds(const IEC60870_CP56Time2a_t *value, + int16_t standard_utc_offset_minutes, uint32_t *seconds); +int IEC60870_Time_CP56_To_Local(const IEC60870_CP56Time2a_t *value, + int16_t source_standard_utc_offset_minutes, + IEC60870_WallTime_t *local_time); +int IEC60870_Time_To_CP56(const IEC60870_CP56Time2a_t *value, uint8_t *out, size_t capacity); +int IEC60870_Time_From_CP56(const uint8_t *in, size_t length, IEC60870_CP56Time2a_t *value); +typedef enum { + IEC60870_ASDU_UNSUPPORTED = -2, + IEC60870_ASDU_ERROR = -1, + IEC60870_ASDU_FULL = 0, + IEC60870_ASDU_OK = 1 +} IEC60870_AsduResult_t; +typedef enum { + IEC60870_ASDU_M_SP_NA_1 = 1, + IEC60870_ASDU_M_SP_TA_1 = 2, + IEC60870_ASDU_M_DP_NA_1 = 3, + IEC60870_ASDU_M_DP_TA_1 = 4, + IEC60870_ASDU_M_ST_NA_1 = 5, + IEC60870_ASDU_M_ST_TA_1 = 6, + IEC60870_ASDU_M_BO_NA_1 = 7, + IEC60870_ASDU_M_BO_TA_1 = 8, + IEC60870_ASDU_M_ME_NA_1 = 9, + IEC60870_ASDU_M_ME_TA_1 = 10, + IEC60870_ASDU_M_ME_NB_1 = 11, + IEC60870_ASDU_M_ME_TB_1 = 12, + IEC60870_ASDU_M_ME_NC_1 = 13, + IEC60870_ASDU_M_ME_TC_1 = 14, + IEC60870_ASDU_M_IT_NA_1 = 15, + IEC60870_ASDU_M_IT_TA_1 = 16, + IEC60870_ASDU_M_EP_TA_1 = 17, + IEC60870_ASDU_M_EP_TB_1 = 18, + IEC60870_ASDU_M_EP_TC_1 = 19, + IEC60870_ASDU_M_PS_NA_1 = 20, + IEC60870_ASDU_M_ME_ND_1 = 21, + IEC60870_ASDU_M_SP_TB_1 = 30, + IEC60870_ASDU_M_DP_TB_1 = 31, + IEC60870_ASDU_M_ST_TB_1 = 32, + IEC60870_ASDU_M_BO_TB_1 = 33, + IEC60870_ASDU_M_ME_TD_1 = 34, + IEC60870_ASDU_M_ME_TE_1 = 35, + IEC60870_ASDU_M_ME_TF_1 = 36, + IEC60870_ASDU_M_IT_TB_1 = 37, + IEC60870_ASDU_M_EP_TD_1 = 38, + IEC60870_ASDU_M_EP_TE_1 = 39, + IEC60870_ASDU_M_EP_TF_1 = 40, + IEC60870_ASDU_C_SC_NA_1 = 45, + IEC60870_ASDU_C_DC_NA_1 = 46, + IEC60870_ASDU_C_RC_NA_1 = 47, + IEC60870_ASDU_C_SE_NA_1 = 48, + IEC60870_ASDU_C_SE_NB_1 = 49, + IEC60870_ASDU_C_SE_NC_1 = 50, + IEC60870_ASDU_C_BO_NA_1 = 51, + IEC60870_ASDU_C_SC_TA_1 = 58, + IEC60870_ASDU_C_DC_TA_1 = 59, + IEC60870_ASDU_C_RC_TA_1 = 60, + IEC60870_ASDU_C_SE_TA_1 = 61, + IEC60870_ASDU_C_SE_TB_1 = 62, + IEC60870_ASDU_C_SE_TC_1 = 63, + IEC60870_ASDU_C_BO_TA_1 = 64, + IEC60870_ASDU_M_EI_NA_1 = 70, + IEC60870_ASDU_C_IC_NA_1 = 100, + IEC60870_ASDU_C_CI_NA_1 = 101, + IEC60870_ASDU_C_RD_NA_1 = 102, + IEC60870_ASDU_C_CS_NA_1 = 103, + IEC60870_ASDU_C_TS_NA_1 = 104, + IEC60870_ASDU_C_RP_NA_1 = 105, + IEC60870_ASDU_C_CD_NA_1 = 106, + IEC60870_ASDU_C_TS_TA_1 = 107, + IEC60870_ASDU_P_ME_NA_1 = 110, + IEC60870_ASDU_P_ME_NB_1 = 111, + IEC60870_ASDU_P_ME_NC_1 = 112, + IEC60870_ASDU_P_AC_NA_1 = 113, + IEC60870_ASDU_F_FR_NA_1 = 120, + IEC60870_ASDU_F_SR_NA_1 = 121, + IEC60870_ASDU_F_SC_NA_1 = 122, + IEC60870_ASDU_F_LS_NA_1 = 123, + IEC60870_ASDU_F_AF_NA_1 = 124, + IEC60870_ASDU_F_SG_NA_1 = 125, + IEC60870_ASDU_F_DR_TA_1 = 126, + IEC60870_ASDU_F_SC_NB_1 = 127 +} IEC60870_AsduType_t; +enum { + IEC60870_ASDU_QUALITY_OV = 0x01, + IEC60870_ASDU_QUALITY_EI = 0x08, + IEC60870_ASDU_QUALITY_BL = 0x10, + IEC60870_ASDU_QUALITY_SB = 0x20, + IEC60870_ASDU_QUALITY_NT = 0x40, + IEC60870_ASDU_QUALITY_IV = 0x80 +}; +typedef struct { + int32_t value; + uint8_t sequence_number; + uint8_t has_carry; + uint8_t is_adjusted; + uint8_t is_invalid; +} IEC60870_BinaryCounterReading_t; +struct IEC60870_InformationObjectVTable; +typedef struct { + uint32_t ioa; + uint8_t type_id; + const struct IEC60870_InformationObjectVTable *virtual_function_table; +} IEC60870_InformationObject_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t value; + uint8_t quality; +} IEC60870_SinglePointInformation_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t value; + uint8_t quality; +} IEC60870_DoublePointInformation_t; +typedef struct { + IEC60870_InformationObject_t object; + int16_t value; + uint8_t quality; +} IEC60870_MeasuredValueNormalized_t; +typedef struct { + IEC60870_InformationObject_t object; + int16_t value; + uint8_t quality; +} IEC60870_MeasuredValueScaled_t; +typedef struct { + IEC60870_InformationObject_t object; + float value; + uint8_t quality; +} IEC60870_MeasuredValueShort_t; +typedef struct { + IEC60870_InformationObject_t object; + IEC60870_BinaryCounterReading_t value; +} IEC60870_IntegratedTotals_t; +typedef struct { + IEC60870_SinglePointInformation_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_SinglePointWithCP56Time2a_t; +typedef struct { + IEC60870_DoublePointInformation_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_DoublePointWithCP56Time2a_t; +typedef struct { + IEC60870_MeasuredValueNormalized_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_MeasuredValueNormalizedWithCP56Time2a_t; +typedef struct { + IEC60870_MeasuredValueScaled_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_MeasuredValueScaledWithCP56Time2a_t; +typedef struct { + IEC60870_MeasuredValueShort_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_MeasuredValueShortWithCP56Time2a_t; +typedef struct { + IEC60870_IntegratedTotals_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_IntegratedTotalsWithCP56Time2a_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t state; + uint8_t is_select; + uint8_t qualifier; +} IEC60870_SingleCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t state; + uint8_t is_select; + uint8_t qualifier; +} IEC60870_DoubleCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t state; + uint8_t is_select; + uint8_t qualifier; +} IEC60870_StepCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + int16_t value; + uint8_t is_select; + uint8_t qualifier; +} IEC60870_SetpointCommandNormalized_t; +typedef struct { + IEC60870_InformationObject_t object; + int16_t value; + uint8_t is_select; + uint8_t qualifier; +} IEC60870_SetpointCommandScaled_t; +typedef struct { + IEC60870_InformationObject_t object; + float value; + uint8_t is_select; + uint8_t qualifier; +} IEC60870_SetpointCommandShort_t; +typedef struct { + IEC60870_SingleCommand_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_SingleCommandWithCP56Time2a_t; +typedef struct { + IEC60870_DoubleCommand_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_DoubleCommandWithCP56Time2a_t; +typedef struct { + IEC60870_StepCommand_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_StepCommandWithCP56Time2a_t; +typedef struct { + IEC60870_SetpointCommandNormalized_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_SetpointCommandNormalizedWithCP56Time2a_t; +typedef struct { + IEC60870_SetpointCommandScaled_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_SetpointCommandScaledWithCP56Time2a_t; +typedef struct { + IEC60870_SetpointCommandShort_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_SetpointCommandShortWithCP56Time2a_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t qualifier; +} IEC60870_InterrogationCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t qualifier; + uint8_t freeze; +} IEC60870_CounterInterrogationCommand_t; +typedef struct { + IEC60870_InformationObject_t object; +} IEC60870_ReadCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + IEC60870_CP56Time2a_t time; +} IEC60870_ClockSynchronizationCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t qualifier; +} IEC60870_ResetProcessCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t sequence; + IEC60870_CP56Time2a_t time; +} IEC60870_TestCommandWithCP56Time2a_t; +typedef struct { + IEC60870_InformationObject_t object; + int8_t value; + uint8_t is_transient; + uint8_t quality; +} IEC60870_StepPositionInformation_t; +typedef struct { + IEC60870_InformationObject_t object; + uint32_t value; + uint8_t quality; +} IEC60870_BitString32_t; +typedef struct { + IEC60870_SinglePointInformation_t base; + IEC60870_CP24Time2a_t time; +} IEC60870_SinglePointWithCP24Time2a_t; +typedef struct { + IEC60870_DoublePointInformation_t base; + IEC60870_CP24Time2a_t time; +} IEC60870_DoublePointWithCP24Time2a_t; +typedef struct { + IEC60870_StepPositionInformation_t base; + IEC60870_CP24Time2a_t time; +} IEC60870_StepPositionWithCP24Time2a_t; +typedef struct { + IEC60870_BitString32_t base; + IEC60870_CP24Time2a_t time; +} IEC60870_Bitstring32WithCP24Time2a_t; +typedef struct { + IEC60870_MeasuredValueNormalized_t base; + IEC60870_CP24Time2a_t time; +} IEC60870_MeasuredValueNormalizedWithCP24Time2a_t; +typedef struct { + IEC60870_MeasuredValueScaled_t base; + IEC60870_CP24Time2a_t time; +} IEC60870_MeasuredValueScaledWithCP24Time2a_t; +typedef struct { + IEC60870_MeasuredValueShort_t base; + IEC60870_CP24Time2a_t time; +} IEC60870_MeasuredValueShortWithCP24Time2a_t; +typedef struct { + IEC60870_IntegratedTotals_t base; + IEC60870_CP24Time2a_t time; +} IEC60870_IntegratedTotalsWithCP24Time2a_t; +typedef struct { + IEC60870_StepPositionInformation_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_StepPositionWithCP56Time2a_t; +typedef struct { + IEC60870_BitString32_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_Bitstring32WithCP56Time2a_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t event; + uint8_t quality; + IEC60870_CP16Time2a_t elapsed_time; + IEC60870_CP24Time2a_t time; +} IEC60870_EventOfProtectionEquipment_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t event; + uint8_t quality; + IEC60870_CP16Time2a_t elapsed_time; + IEC60870_CP24Time2a_t time; +} IEC60870_PackedStartEventsOfProtectionEquipment_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t event; + uint8_t quality; + IEC60870_CP16Time2a_t elapsed_time; + IEC60870_CP24Time2a_t time; +} IEC60870_PackedOutputCircuitInfo_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t event; + uint8_t quality; + IEC60870_CP16Time2a_t elapsed_time; + IEC60870_CP56Time2a_t time; +} IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t event; + uint8_t quality; + IEC60870_CP16Time2a_t elapsed_time; + IEC60870_CP56Time2a_t time; +} IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t event; + uint8_t quality; + IEC60870_CP16Time2a_t elapsed_time; + IEC60870_CP56Time2a_t time; +} IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t status; + uint16_t change; + uint8_t quality; +} IEC60870_PackedSinglePointWithSCD_t; +typedef struct { + IEC60870_InformationObject_t object; + int16_t value; +} IEC60870_MeasuredValueNormalizedWithoutQuality_t; +typedef struct { + IEC60870_InformationObject_t object; + uint32_t value; +} IEC60870_Bitstring32Command_t; +typedef struct { + IEC60870_Bitstring32Command_t base; + IEC60870_CP56Time2a_t time; +} IEC60870_Bitstring32CommandWithCP56Time2a_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t cause; +} IEC60870_EndOfInitialization_t; +typedef struct { + IEC60870_InformationObject_t object; +} IEC60870_TestCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + IEC60870_CP16Time2a_t delay; +} IEC60870_DelayAcquisitionCommand_t; +typedef struct { + IEC60870_InformationObject_t object; + int16_t value; + uint8_t qualifier; +} IEC60870_ParameterNormalizedValue_t; +typedef struct { + IEC60870_InformationObject_t object; + int16_t value; + uint8_t qualifier; +} IEC60870_ParameterScaledValue_t; +typedef struct { + IEC60870_InformationObject_t object; + float value; + uint8_t qualifier; +} IEC60870_ParameterFloatValue_t; +typedef struct { + IEC60870_InformationObject_t object; + uint8_t qualifier; +} IEC60870_ParameterActivation_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t file_name; + uint32_t file_length; + uint8_t qualifier; +} IEC60870_FileReady_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t file_name; + uint8_t section; + uint32_t section_length; + uint8_t qualifier; +} IEC60870_SectionReady_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t file_name; + uint8_t section; + uint8_t qualifier; +} IEC60870_FileCallOrSelect_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t file_name; + uint8_t section; + uint8_t qualifier; +} IEC60870_FileACK_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t file_name; + uint8_t section; + uint8_t qualifier; + uint8_t checksum; +} IEC60870_FileLastSegmentOrSection_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t file_name; + uint8_t section; + uint8_t length; + uint8_t data[255]; +} IEC60870_FileSegment_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t file_name; + uint32_t file_length; + uint8_t status; + IEC60870_CP56Time2a_t creation_time; +} IEC60870_FileDirectory_t; +typedef struct { + IEC60870_InformationObject_t object; + uint16_t file_name; + IEC60870_CP56Time2a_t start_time; + IEC60870_CP56Time2a_t stop_time; +} IEC60870_QueryLog_t; +IEC60870_AsduResult_t IEC60870_StepPositionInformation_Create(IEC60870_StepPositionInformation_t *self, + uint32_t ioa, int value, int is_transient, int quality); +IEC60870_AsduResult_t IEC60870_StepPositionInformation_Get_From_Buffer(IEC60870_StepPositionInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_BitString32_Create(IEC60870_BitString32_t *self, + uint32_t ioa, uint32_t value, int quality); +IEC60870_AsduResult_t IEC60870_BitString32_Get_From_Buffer(IEC60870_BitString32_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SinglePointWithCP24Time2a_Create(IEC60870_SinglePointWithCP24Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_SinglePointWithCP24Time2a_Get_From_Buffer(IEC60870_SinglePointWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_DoublePointWithCP24Time2a_Create(IEC60870_DoublePointWithCP24Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_DoublePointWithCP24Time2a_Get_From_Buffer(IEC60870_DoublePointWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_StepPositionWithCP24Time2a_Create(IEC60870_StepPositionWithCP24Time2a_t *self, + uint32_t ioa, int value, int is_transient, int quality, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_StepPositionWithCP24Time2a_Get_From_Buffer(IEC60870_StepPositionWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP24Time2a_Create(IEC60870_Bitstring32WithCP24Time2a_t *self, + uint32_t ioa, uint32_t value, int quality, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP24Time2a_Get_From_Buffer(IEC60870_Bitstring32WithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP24Time2a_Create(IEC60870_MeasuredValueNormalizedWithCP24Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP24Time2a_Create(IEC60870_MeasuredValueScaledWithCP24Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueScaledWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP24Time2a_Create(IEC60870_MeasuredValueShortWithCP24Time2a_t *self, + uint32_t ioa, float value, int quality, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueShortWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP24Time2a_Create(IEC60870_IntegratedTotalsWithCP24Time2a_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP24Time2a_Get_From_Buffer(IEC60870_IntegratedTotalsWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_StepPositionWithCP56Time2a_Create(IEC60870_StepPositionWithCP56Time2a_t *self, + uint32_t ioa, int value, int is_transient, int quality, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_StepPositionWithCP56Time2a_Get_From_Buffer(IEC60870_StepPositionWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP56Time2a_Create(IEC60870_Bitstring32WithCP56Time2a_t *self, + uint32_t ioa, uint32_t value, int quality, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP56Time2a_Get_From_Buffer(IEC60870_Bitstring32WithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipment_Create(IEC60870_EventOfProtectionEquipment_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipment_Get_From_Buffer(IEC60870_EventOfProtectionEquipment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipment_Create(IEC60870_PackedStartEventsOfProtectionEquipment_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipment_Get_From_Buffer(IEC60870_PackedStartEventsOfProtectionEquipment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfo_Create(IEC60870_PackedOutputCircuitInfo_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time); +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfo_Get_From_Buffer(IEC60870_PackedOutputCircuitInfo_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Create(IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer(IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Create(IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer(IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Create(IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Get_From_Buffer(IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_PackedSinglePointWithSCD_Create(IEC60870_PackedSinglePointWithSCD_t *self, + uint32_t ioa, uint16_t status, uint16_t change, int quality); +IEC60870_AsduResult_t IEC60870_PackedSinglePointWithSCD_Get_From_Buffer(IEC60870_PackedSinglePointWithSCD_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithoutQuality_Create(IEC60870_MeasuredValueNormalizedWithoutQuality_t *self, + uint32_t ioa, int16_t value); +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithoutQuality_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithoutQuality_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_Bitstring32Command_Create(IEC60870_Bitstring32Command_t *self, + uint32_t ioa, uint32_t value); +IEC60870_AsduResult_t IEC60870_Bitstring32Command_Get_From_Buffer(IEC60870_Bitstring32Command_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_Bitstring32CommandWithCP56Time2a_Create(IEC60870_Bitstring32CommandWithCP56Time2a_t *self, + uint32_t ioa, uint32_t value, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_Bitstring32CommandWithCP56Time2a_Get_From_Buffer(IEC60870_Bitstring32CommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_EndOfInitialization_Create(IEC60870_EndOfInitialization_t *self, + uint32_t ioa, int cause); +IEC60870_AsduResult_t IEC60870_EndOfInitialization_Get_From_Buffer(IEC60870_EndOfInitialization_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_TestCommand_Create(IEC60870_TestCommand_t *self, + uint32_t ioa); +IEC60870_AsduResult_t IEC60870_TestCommand_Get_From_Buffer(IEC60870_TestCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_DelayAcquisitionCommand_Create(IEC60870_DelayAcquisitionCommand_t *self, + uint32_t ioa, const IEC60870_CP16Time2a_t *delay); +IEC60870_AsduResult_t IEC60870_DelayAcquisitionCommand_Get_From_Buffer(IEC60870_DelayAcquisitionCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_ParameterNormalizedValue_Create(IEC60870_ParameterNormalizedValue_t *self, + uint32_t ioa, int16_t value, int qualifier); +IEC60870_AsduResult_t IEC60870_ParameterNormalizedValue_Get_From_Buffer(IEC60870_ParameterNormalizedValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_ParameterScaledValue_Create(IEC60870_ParameterScaledValue_t *self, + uint32_t ioa, int16_t value, int qualifier); +IEC60870_AsduResult_t IEC60870_ParameterScaledValue_Get_From_Buffer(IEC60870_ParameterScaledValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_ParameterFloatValue_Create(IEC60870_ParameterFloatValue_t *self, + uint32_t ioa, float value, int qualifier); +IEC60870_AsduResult_t IEC60870_ParameterFloatValue_Get_From_Buffer(IEC60870_ParameterFloatValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_ParameterActivation_Create(IEC60870_ParameterActivation_t *self, + uint32_t ioa, int qualifier); +IEC60870_AsduResult_t IEC60870_ParameterActivation_Get_From_Buffer(IEC60870_ParameterActivation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_FileReady_Create(IEC60870_FileReady_t *self, + uint32_t ioa, uint16_t file_name, uint32_t file_length, int qualifier); +IEC60870_AsduResult_t IEC60870_FileReady_Get_From_Buffer(IEC60870_FileReady_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SectionReady_Create(IEC60870_SectionReady_t *self, + uint32_t ioa, uint16_t file_name, int section, uint32_t section_length, int qualifier); +IEC60870_AsduResult_t IEC60870_SectionReady_Get_From_Buffer(IEC60870_SectionReady_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_FileCallOrSelect_Create(IEC60870_FileCallOrSelect_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier); +IEC60870_AsduResult_t IEC60870_FileCallOrSelect_Get_From_Buffer(IEC60870_FileCallOrSelect_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_FileACK_Create(IEC60870_FileACK_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier); +IEC60870_AsduResult_t IEC60870_FileACK_Get_From_Buffer(IEC60870_FileACK_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_FileLastSegmentOrSection_Create(IEC60870_FileLastSegmentOrSection_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier, int checksum); +IEC60870_AsduResult_t IEC60870_FileLastSegmentOrSection_Get_From_Buffer(IEC60870_FileLastSegmentOrSection_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_FileSegment_Create(IEC60870_FileSegment_t *self, + uint32_t ioa, uint16_t file_name, int section, const uint8_t *data, uint16_t data_length); +IEC60870_AsduResult_t IEC60870_FileSegment_Get_From_Buffer(IEC60870_FileSegment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_FileDirectory_Create(IEC60870_FileDirectory_t *self, + uint32_t ioa, uint16_t file_name, uint32_t file_length, int status, const IEC60870_CP56Time2a_t *creation_time); +IEC60870_AsduResult_t IEC60870_FileDirectory_Get_From_Buffer(IEC60870_FileDirectory_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_QueryLog_Create(IEC60870_QueryLog_t *self, + uint32_t ioa, uint16_t file_name, const IEC60870_CP56Time2a_t *start_time, const IEC60870_CP56Time2a_t *stop_time); +IEC60870_AsduResult_t IEC60870_QueryLog_Get_From_Buffer(IEC60870_QueryLog_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +int IEC60870_FileSegment_Get_Max_Data_Size(const IEC60870_AppLayerParameters_t *parameters); +uint8_t IEC60870_InformationObject_Get_Type(const IEC60870_InformationObject_t *object); +IEC60870_AsduResult_t IEC60870_InformationObject_Encode( + const IEC60870_InformationObject_t *object, const IEC60870_AppLayerParameters_t *parameters, + int is_sequence, uint8_t *buffer, uint16_t capacity, uint16_t *length); +IEC60870_AsduResult_t IEC60870_SinglePointInformation_Create(IEC60870_SinglePointInformation_t *self, + uint32_t ioa, int value, int quality); +IEC60870_AsduResult_t IEC60870_SinglePointInformation_Get_From_Buffer(IEC60870_SinglePointInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_DoublePointInformation_Create(IEC60870_DoublePointInformation_t *self, + uint32_t ioa, int value, int quality); +IEC60870_AsduResult_t IEC60870_DoublePointInformation_Get_From_Buffer(IEC60870_DoublePointInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalized_Create(IEC60870_MeasuredValueNormalized_t *self, + uint32_t ioa, int16_t value, int quality); +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalized_Get_From_Buffer(IEC60870_MeasuredValueNormalized_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueScaled_Create(IEC60870_MeasuredValueScaled_t *self, + uint32_t ioa, int16_t value, int quality); +IEC60870_AsduResult_t IEC60870_MeasuredValueScaled_Get_From_Buffer(IEC60870_MeasuredValueScaled_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueShort_Create(IEC60870_MeasuredValueShort_t *self, + uint32_t ioa, float value, int quality); +IEC60870_AsduResult_t IEC60870_MeasuredValueShort_Get_From_Buffer(IEC60870_MeasuredValueShort_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_IntegratedTotals_Create(IEC60870_IntegratedTotals_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value); +IEC60870_AsduResult_t IEC60870_IntegratedTotals_Get_From_Buffer(IEC60870_IntegratedTotals_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SinglePointWithCP56Time2a_Create(IEC60870_SinglePointWithCP56Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_SinglePointWithCP56Time2a_Get_From_Buffer(IEC60870_SinglePointWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_DoublePointWithCP56Time2a_Create(IEC60870_DoublePointWithCP56Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_DoublePointWithCP56Time2a_Get_From_Buffer(IEC60870_DoublePointWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP56Time2a_Create(IEC60870_MeasuredValueNormalizedWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP56Time2a_Create(IEC60870_MeasuredValueScaledWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueScaledWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP56Time2a_Create(IEC60870_MeasuredValueShortWithCP56Time2a_t *self, + uint32_t ioa, float value, int quality, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueShortWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP56Time2a_Create(IEC60870_IntegratedTotalsWithCP56Time2a_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP56Time2a_Get_From_Buffer(IEC60870_IntegratedTotalsWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SingleCommand_Create(IEC60870_SingleCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier); +IEC60870_AsduResult_t IEC60870_SingleCommand_Get_From_Buffer(IEC60870_SingleCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_DoubleCommand_Create(IEC60870_DoubleCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier); +IEC60870_AsduResult_t IEC60870_DoubleCommand_Get_From_Buffer(IEC60870_DoubleCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_StepCommand_Create(IEC60870_StepCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier); +IEC60870_AsduResult_t IEC60870_StepCommand_Get_From_Buffer(IEC60870_StepCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalized_Create(IEC60870_SetpointCommandNormalized_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier); +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalized_Get_From_Buffer(IEC60870_SetpointCommandNormalized_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SetpointCommandScaled_Create(IEC60870_SetpointCommandScaled_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier); +IEC60870_AsduResult_t IEC60870_SetpointCommandScaled_Get_From_Buffer(IEC60870_SetpointCommandScaled_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SetpointCommandShort_Create(IEC60870_SetpointCommandShort_t *self, + uint32_t ioa, float value, int is_select, int qualifier); +IEC60870_AsduResult_t IEC60870_SetpointCommandShort_Get_From_Buffer(IEC60870_SetpointCommandShort_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SingleCommandWithCP56Time2a_Create(IEC60870_SingleCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_SingleCommandWithCP56Time2a_Get_From_Buffer(IEC60870_SingleCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_DoubleCommandWithCP56Time2a_Create(IEC60870_DoubleCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_DoubleCommandWithCP56Time2a_Get_From_Buffer(IEC60870_DoubleCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_StepCommandWithCP56Time2a_Create(IEC60870_StepCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_StepCommandWithCP56Time2a_Get_From_Buffer(IEC60870_StepCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalizedWithCP56Time2a_Create(IEC60870_SetpointCommandNormalizedWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalizedWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandNormalizedWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SetpointCommandScaledWithCP56Time2a_Create(IEC60870_SetpointCommandScaledWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_SetpointCommandScaledWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandScaledWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_SetpointCommandShortWithCP56Time2a_Create(IEC60870_SetpointCommandShortWithCP56Time2a_t *self, + uint32_t ioa, float value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_SetpointCommandShortWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandShortWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_InterrogationCommand_Create(IEC60870_InterrogationCommand_t *self, + uint32_t ioa, int qualifier); +IEC60870_AsduResult_t IEC60870_InterrogationCommand_Get_From_Buffer(IEC60870_InterrogationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_CounterInterrogationCommand_Create(IEC60870_CounterInterrogationCommand_t *self, + uint32_t ioa, int qualifier, int freeze); +IEC60870_AsduResult_t IEC60870_CounterInterrogationCommand_Get_From_Buffer(IEC60870_CounterInterrogationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_ReadCommand_Create(IEC60870_ReadCommand_t *self, + uint32_t ioa); +IEC60870_AsduResult_t IEC60870_ReadCommand_Get_From_Buffer(IEC60870_ReadCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_ClockSynchronizationCommand_Create(IEC60870_ClockSynchronizationCommand_t *self, + uint32_t ioa, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_ClockSynchronizationCommand_Get_From_Buffer(IEC60870_ClockSynchronizationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_ResetProcessCommand_Create(IEC60870_ResetProcessCommand_t *self, + uint32_t ioa, int qualifier); +IEC60870_AsduResult_t IEC60870_ResetProcessCommand_Get_From_Buffer(IEC60870_ResetProcessCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +IEC60870_AsduResult_t IEC60870_TestCommandWithCP56Time2a_Create(IEC60870_TestCommandWithCP56Time2a_t *self, + uint32_t ioa, uint16_t sequence, const IEC60870_CP56Time2a_t *time); +IEC60870_AsduResult_t IEC60870_TestCommandWithCP56Time2a_Get_From_Buffer(IEC60870_TestCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); +typedef enum { + IEC60870_ASDU_COT_PERIODIC = 1, + IEC60870_ASDU_COT_BACKGROUND = 2, + IEC60870_ASDU_COT_SPONTANEOUS = 3, + IEC60870_ASDU_COT_INITIALIZED = 4, + IEC60870_ASDU_COT_REQUEST = 5, + IEC60870_ASDU_COT_ACTIVATION = 6, + IEC60870_ASDU_COT_ACTIVATION_CON = 7, + IEC60870_ASDU_COT_DEACTIVATION = 8, + IEC60870_ASDU_COT_DEACTIVATION_CON = 9, + IEC60870_ASDU_COT_ACTIVATION_TERMINATION = 10, + IEC60870_ASDU_COT_RETURN_INFO_REMOTE = 11, + IEC60870_ASDU_COT_RETURN_INFO_LOCAL = 12, + IEC60870_ASDU_COT_FILE_TRANSFER = 13, + IEC60870_ASDU_COT_INTERROGATED_BY_STATION = 20, + IEC60870_ASDU_COT_COUNTER_INTERROGATION = 37, + IEC60870_ASDU_COT_COUNTER_GROUP_1 = 38, + IEC60870_ASDU_COT_COUNTER_GROUP_2 = 39, + IEC60870_ASDU_COT_COUNTER_GROUP_3 = 40, + IEC60870_ASDU_COT_COUNTER_GROUP_4 = 41, + IEC60870_ASDU_COT_UNKNOWN_TYPE = 44, + IEC60870_ASDU_COT_UNKNOWN_CAUSE = 45, + IEC60870_ASDU_COT_UNKNOWN_COMMON_ADDRESS = 46, + IEC60870_ASDU_COT_UNKNOWN_IOA = 47 +} IEC60870_AsduCause_t; +typedef struct { + uint8_t cause_of_transmission; + uint8_t is_negative_confirm; + uint8_t is_test; + uint8_t originator_address; + uint16_t common_address; +} IEC60870_AsduHeader_t; +typedef struct { + uint8_t *buffer; + uint16_t capacity; + uint16_t length; + uint8_t type_id; + uint8_t object_count; + uint8_t is_sequence; + uint8_t is_from_buffer; + uint32_t first_ioa; + const IEC60870_AppLayerParameters_t *parameters; +} IEC60870_Asdu_t; +IEC60870_AsduResult_t IEC60870_Asdu_Create(IEC60870_Asdu_t *self, + const IEC60870_AppLayerParameters_t *parameters, + uint8_t *buffer, uint16_t capacity, const IEC60870_AsduHeader_t *header, int is_sequence); +IEC60870_AsduResult_t IEC60870_Asdu_Create_From_Buffer(IEC60870_Asdu_t *self, + const IEC60870_AppLayerParameters_t *parameters, uint8_t *buffer, uint16_t length); +IEC60870_AsduResult_t IEC60870_Asdu_Set_COT(IEC60870_Asdu_t *self, int cause); +IEC60870_AsduResult_t IEC60870_Asdu_Set_Negative(IEC60870_Asdu_t *self, int is_negative); +IEC60870_AsduResult_t IEC60870_Asdu_Set_CA(IEC60870_Asdu_t *self, uint16_t common_address); +IEC60870_AsduResult_t IEC60870_Asdu_Add_InformationObject(IEC60870_Asdu_t *self, + const IEC60870_InformationObject_t *object); +IEC60870_AsduResult_t IEC60870_Asdu_Get_Data(const IEC60870_Asdu_t *self, + const uint8_t **asdu, uint16_t *length); +IEC60870_AsduResult_t IEC60870_Asdu_Parse(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *asdu, uint16_t length, IEC60870_AsduInfo_t *info); +IEC60870_AsduResult_t IEC60870_Asdu_Get_Element(const IEC60870_AsduInfo_t *info, + uint8_t index, void *object, size_t object_size); +IEC60870_AsduResult_t IEC60870_Asdu_Build_Response(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *request, uint16_t length, + int cause, int is_negative, uint8_t *out, uint16_t capacity, uint16_t *out_length); +typedef struct IEC60870_MasterConnection IEC60870_MasterConnection_t; +struct IEC60870_MasterConnection { + void *object; + _Bool (*send_asdu)(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu); + _Bool (*send_act_con)(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu, _Bool negative); + _Bool (*send_act_term)(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu); + const IEC60870_AppLayerParameters_t *(*get_application_layer_parameters)( + IEC60870_MasterConnection_t *self); +}; +_Bool IEC60870_MasterConnection_Send_ASDU(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu); +_Bool IEC60870_MasterConnection_Send_ACT_CON(IEC60870_MasterConnection_t *self, + IEC60870_Asdu_t *asdu, _Bool negative); +_Bool IEC60870_MasterConnection_Send_ACT_TERM(IEC60870_MasterConnection_t *self, + IEC60870_Asdu_t *asdu); +const IEC60870_AppLayerParameters_t *IEC60870_MasterConnection_Get_Application_Layer_Parameters( + IEC60870_MasterConnection_t *self); +typedef enum { + IEC104_ASDU_PRIORITY_HIGH = 0, + IEC104_ASDU_PRIORITY_NORMAL = 1, + IEC104_ASDU_PRIORITY_COUNT = 2 +} IEC104_AsduPriority_t; +typedef struct { + uint16_t k; + uint16_t w; + uint32_t t1_ms; + uint32_t t2_ms; + uint32_t t3_ms; +} IEC104_LinkConfig_t; +typedef struct { + IEC104_LinkConfig_t link; + uint32_t local_send_timeout_ms; + uint32_t stop_timeout_ms; + uint16_t high_priority_burst_limit; +} IEC104_ProtocolConfig_t; +typedef enum { + IEC104_TCP_OK = 0, + IEC104_TCP_INVALID_ARGUMENT = -1, + IEC104_TCP_INVALID_HANDLE = -2, + IEC104_TCP_INVALID_CONFIG = -3, + IEC104_TCP_INVALID_STATE = -4, + IEC104_TCP_BUSY = -5, + IEC104_TCP_EVENTS_PENDING = -6, + IEC104_TCP_NETWORK_ERROR = -7, + IEC104_TCP_INTERNAL_ERROR = -8, + IEC104_TCP_GENERATION_EXHAUSTED = -9, + IEC104_TCP_REGISTRATION_EXHAUSTED = -10, + IEC104_TCP_NOT_CONNECTED = -11, + IEC104_TCP_STALE_GENERATION = -12 +} IEC104_TcpResult_t; +typedef enum { + IEC104_TCP_STAGE_NONE = 0, + IEC104_TCP_STAGE_VALIDATE, + IEC104_TCP_STAGE_LIFECYCLE, + IEC104_TCP_STAGE_NET_INIT, + IEC104_TCP_STAGE_LISTEN, + IEC104_TCP_STAGE_WAIT, + IEC104_TCP_STAGE_ACCEPT, + IEC104_TCP_STAGE_GENERATION, + IEC104_TCP_STAGE_QUEUE_INIT, + IEC104_TCP_STAGE_QUEUE_STATE, + IEC104_TCP_STAGE_CONNECTION_INIT, + IEC104_TCP_STAGE_SCHEDULE, + IEC104_TCP_STAGE_REGISTRATION +} IEC104_TcpErrorStage_t; +typedef struct { + IEC104_TcpResult_t result; + IEC104_TcpErrorStage_t stage; + int socket_error; +} IEC104_TcpError_t; +typedef uintptr_t IEC104_TcpSocket_t; +typedef struct { + IEC104_TcpSocket_t socket; + uint64_t token; + uint32_t events; +} IEC104_TcpSocketEvent_t; +typedef struct { + uint64_t service_token; + IEC104_TcpSocketEvent_t sockets[2U]; + uint32_t socket_count; + uint32_t timeout_ms; + uint16_t pending_event_count; + uint8_t receive_paused; + uint8_t timer_active; + uint8_t need_process; +} IEC104_TcpWaitInfo_t; +typedef struct { + uint64_t service_token; + IEC104_TcpSocketEvent_t sockets[2U]; + uint32_t socket_count; + uint8_t timer_expired; +} IEC104_TcpReady_t; +typedef enum { + IEC104_TCP_EVENT_CONNECTED = 1, + IEC104_TCP_EVENT_I_FRAME, + IEC104_TCP_EVENT_RECORD, + IEC104_TCP_EVENT_DISCONNECTED, + IEC104_TCP_EVENT_ACTIVATED, + IEC104_TCP_EVENT_DEACTIVATED +} IEC104_TcpEventType_t; +typedef enum { + IEC104_TCP_EVENT_ERROR = -1, + IEC104_TCP_EVENT_EMPTY = 0, + IEC104_TCP_EVENT_READY = 1 +} IEC104_TcpEventResult_t; +typedef enum { + IEC104_TCP_SUBMIT_FATAL = -2, + IEC104_TCP_SUBMIT_ERROR = -1, + IEC104_TCP_SUBMIT_OK = 1, + 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; +typedef enum { + IEC104_TCP_FAULT_NONE = 0, + IEC104_TCP_FAULT_FRAME, + IEC104_TCP_FAULT_PROTOCOL, + IEC104_TCP_FAULT_INTERNAL, + IEC104_TCP_FAULT_TRANSPORT, + IEC104_TCP_FAULT_LOCAL_SEND_TIMEOUT, + IEC104_TCP_FAULT_I_ACK_TIMEOUT, + IEC104_TCP_FAULT_TESTFR_TIMEOUT, + IEC104_TCP_FAULT_STOP_TIMEOUT +} IEC104_TcpFault_t; +typedef struct { + IEC104_TcpSessionPhase_t phase; + IEC104_TcpFault_t fault; + uint16_t pending_send_records; + uint8_t data_transfer_started; +} IEC104_TcpConnectionStatus_t; +typedef struct { + uint64_t generation; + char address[16]; + uint16_t port; +} IEC104_TcpPeerInfo_t; +typedef struct { + uint64_t receive_serial; + uint16_t apdu_length; + uint16_t send_number; + uint16_t receive_number; + uint8_t apdu[255]; +} IEC104_TcpIFrameEvent_t; +typedef struct { + uint64_t frame_serial; + uint64_t queued_at_ms; + uint64_t written_at_ms; + uint16_t send_number; + uint16_t receive_number; + uint16_t apdu_length; + IEC104_AsduPriority_t priority; + uint8_t local_send_completed; + uint8_t peer_acknowledged; +} IEC104_TcpSendRecord_t; +typedef struct { + IEC104_TcpSendRecord_t value; + uint8_t closing; +} IEC104_TcpRecordEvent_t; +typedef struct { + IEC104_TcpConnectionStatus_t status; + IEC104_TcpPeerInfo_t peer; + int socket_error; + uint8_t status_valid; +} IEC104_TcpConnectionEvent_t; +typedef struct { + IEC104_TcpEventType_t type; + uint64_t generation; + union { + IEC104_TcpIFrameEvent_t i_frame; + IEC104_TcpRecordEvent_t record; + IEC104_TcpConnectionEvent_t connection; + } data; +} IEC104_TcpEvent_t; +typedef struct { + char bind_address[16]; + uint16_t port; + IEC104_ProtocolConfig_t protocol; +} IEC104_TcpServerConfig_t; +typedef struct { + uint32_t pending_asdu_count; + uint32_t stored_asdu_count; + uint32_t capacity_bytes; + uint32_t used_bytes; + uint32_t free_bytes; +} IEC104_TcpAsduQueueStatus_t; +typedef struct { + uint64_t generation; + uint16_t bound_port; + uint16_t pending_event_count; + uint16_t pending_state_event_count; + uint8_t receive_paused; + int last_socket_error; + uint8_t listening; + uint8_t connected; + uint8_t connection_status_valid; + IEC104_TcpConnectionStatus_t connection; + uint8_t asdu_queue_status_valid; + IEC104_TcpAsduQueueStatus_t high_queue; + IEC104_TcpAsduQueueStatus_t normal_queue; + uint8_t accept_paused; + uint32_t accept_retry_remaining_ms; + int last_accept_error; + uint32_t consecutive_accept_failures; +} IEC104_TcpServerStatus_t; +typedef struct IEC104_Slave IEC104_Slave_t; +typedef IEC104_TcpServerConfig_t IEC104_SlaveConfig_t; +typedef struct IEC104_Station { + struct IEC104_Station *_next; + IEC104_Slave_t *_owner; + void *_context; + uint64_t _id; + uint32_t _users; + uint16_t _common_address; + _Bool _registered; +} IEC104_Station_t; +typedef struct IEC104_StationSnapshot { + IEC104_Slave_t *_owner; + IEC104_Station_t *_next; + uint64_t _last_id; + const struct IEC104_StationSnapshot *_self; +} IEC104_StationSnapshot_t; +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; +IEC104_StationResult_t IEC104_Slave_Add_Station(IEC104_Slave_t *self, + IEC104_Station_t *station, uint16_t common_address, void *context); +IEC104_StationResult_t IEC104_Slave_Remove_Station(IEC104_Slave_t *self, IEC104_Station_t *station); +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); +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); +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); +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); +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); +uint16_t IEC104_Station_Get_Common_Address(const IEC104_Station_t *station); +void *IEC104_Station_Get_Context(const IEC104_Station_t *station); +typedef struct { + IEC104_TcpEvent_t transport; + IEC60870_Asdu_t asdu; + IEC60870_AsduResult_t asdu_result; + IEC60870_MasterConnection_t connection; + IEC104_Slave_t *_owner; + uint64_t _owner_serial; + uint64_t _generation; + IEC104_TcpSubmitResult_t _last_submit_result; +} IEC104_SlaveEvent_t; +typedef struct { + const IEC104_Slave_t *slave; + uint64_t instance_id; + uint64_t generation; +} IEC104_SlaveEventIdentity_t; +typedef enum { + IEC104_SLAVE_REQUEST_ERROR = -1, + IEC104_SLAVE_REQUEST_STALE = 0, + IEC104_SLAVE_REQUEST_WAIT_EVENTS = 1, + IEC104_SLAVE_REQUEST_READY = 2 +} IEC104_SlaveRequestState_t; +IEC104_Slave_t *IEC104_Slave_Create(void); +IEC104_TcpResult_t IEC104_Slave_Destroy(IEC104_Slave_t *self, IEC104_TcpError_t *error); +void IEC104_Slave_Config_Default(IEC104_SlaveConfig_t *config); +IEC104_TcpResult_t IEC104_Slave_Init(IEC104_Slave_t *self, + const IEC104_SlaveConfig_t *config, IEC104_TcpError_t *error); +IEC104_TcpResult_t IEC104_Slave_Prepare(const IEC104_Slave_t *self, + IEC104_TcpWaitInfo_t *info, IEC104_TcpError_t *error); +IEC104_TcpResult_t IEC104_Slave_Process(IEC104_Slave_t *self, + const IEC104_TcpReady_t *ready, IEC104_TcpError_t *error); +IEC104_TcpResult_t IEC104_Slave_Poll(IEC104_Slave_t *self, + uint32_t max_wait_ms, IEC104_TcpError_t *error); +IEC104_TcpEventResult_t IEC104_Slave_NextEvent(IEC104_Slave_t *self, IEC104_SlaveEvent_t *event); +_Bool IEC104_Slave_Copy_Event(IEC104_SlaveEvent_t *destination, const IEC104_SlaveEvent_t *source); +_Bool IEC104_Slave_Get_Event_Identity(const IEC104_Slave_t *self, + const IEC104_SlaveEvent_t *event, IEC104_SlaveEventIdentity_t *identity); +IEC104_SlaveRequestState_t IEC104_Slave_Check_Request(const IEC104_Slave_t *self, + const IEC104_SlaveEventIdentity_t *identity); +IEC104_TcpSubmitResult_t IEC104_Slave_Get_Last_Submit_Result( + const IEC60870_MasterConnection_t *connection); +IEC104_TcpSubmitResult_t IEC104_Slave_Try_Send_ASDU( + IEC60870_MasterConnection_t *connection, const IEC60870_Asdu_t *asdu); +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); +IEC104_TcpSubmitResult_t IEC104_Slave_Reject_Request(IEC104_Slave_t *self, + const IEC104_SlaveEvent_t *event, int cause); +const IEC60870_AppLayerParameters_t *IEC104_Slave_Get_Application_Layer_Parameters( + const IEC104_Slave_t *self); +IEC104_TcpSubmitResult_t IEC104_Slave_Enqueue_Event(IEC104_Slave_t *self, IEC60870_Asdu_t *asdu); +IEC104_TcpResult_t IEC104_Slave_Get_Remote_Address(const IEC104_Slave_t *self, + IEC104_TcpPeerInfo_t *peer, IEC104_TcpError_t *error); +IEC104_TcpResult_t IEC104_Slave_Close_Connection(IEC104_Slave_t *self, + const IEC104_SlaveEvent_t *event, IEC104_TcpError_t *error); +IEC104_TcpResult_t IEC104_Slave_Close_Connection_By_Identity(IEC104_Slave_t *self, + const IEC104_SlaveEventIdentity_t *identity, IEC104_TcpError_t *error); +IEC104_TcpResult_t IEC104_Slave_Get_Status(const IEC104_Slave_t *self, + IEC104_TcpServerStatus_t *status, IEC104_TcpError_t *error); +IEC104_TcpResult_t IEC104_Slave_Close(IEC104_Slave_t *self, IEC104_TcpError_t *error); +typedef struct IEC104_Command IEC104_Command_t; +typedef struct { + uint16_t type_mask; + uint16_t select_required_mask; + uint32_t select_timeout_ms; + uint32_t decision_timeout_ms; + uint32_t execute_timeout_ms; + _Bool send_act_term; + _Bool act_term_on_failure; +} IEC104_CommandConfig_t; +typedef struct { + uint64_t generation, receive_serial; + uint64_t station_id; + uint16_t common_address; + uint8_t originator_address, type_id; + uint32_t ioa; + _Bool is_select, selected; + uint8_t state, qualifier; + uint64_t accepted_ms; +} IEC104_CommandRequest_t; +typedef enum { + IEC104_COMMAND_SELECT = 1, + IEC104_COMMAND_EXECUTE, + IEC104_COMMAND_FINISHED +} IEC104_CommandEventType_t; +typedef enum { + IEC104_COMMAND_FINISH_NONE = 0, + IEC104_COMMAND_FINISH_COMPLETED, + IEC104_COMMAND_FINISH_EXECUTION_FAILED, + IEC104_COMMAND_FINISH_REJECTED, + IEC104_COMMAND_FINISH_ACTION_FAILED, + IEC104_COMMAND_FINISH_MISMATCH, + IEC104_COMMAND_FINISH_DEACTIVATED, + IEC104_COMMAND_FINISH_SELECT_TIMEOUT, + IEC104_COMMAND_FINISH_DECISION_TIMEOUT, + IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT, + IEC104_COMMAND_FINISH_CANCELLED, + IEC104_COMMAND_FINISH_STOPPED, + IEC104_COMMAND_FINISH_DISCONNECTED, + IEC104_COMMAND_FINISH_CONNECTION_CHANGED, + IEC104_COMMAND_FINISH_SEND_ERROR, + IEC104_COMMAND_FINISH_SERVICE_ERROR +} IEC104_CommandFinishReason_t; +typedef struct { + IEC104_CommandEventType_t type; + uint64_t instance_id, transaction_id, event_serial; + IEC104_CommandRequest_t request; + IEC104_CommandFinishReason_t reason; + _Bool select_effect, execute_effect; +} IEC104_CommandEvent_t; +typedef enum { + IEC104_COMMAND_ACTION_NOT_STARTED = 0, + IEC104_COMMAND_ACTION_STARTED, + IEC104_COMMAND_ACTION_FAILED +} IEC104_CommandActionResult_t; +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, + IEC104_COMMAND_REJECT_UNKNOWN_IOA +} IEC104_CommandRejectCause_t; +typedef enum { + IEC104_COMMAND_EVENT_ERROR = -1, + IEC104_COMMAND_EVENT_IGNORED = 0, + IEC104_COMMAND_EVENT_HANDLED = 1 +} IEC104_CommandEventResult_t; +typedef enum { + IEC104_COMMAND_NEXT_ERROR = -1, + IEC104_COMMAND_NEXT_EMPTY = 0, + IEC104_COMMAND_NEXT_READY = 1 +} IEC104_CommandNextResult_t; +typedef enum { + IEC104_COMMAND_REPLY_STALE = -2, + IEC104_COMMAND_REPLY_ERROR = -1, + IEC104_COMMAND_REPLY_WAIT = 0, + IEC104_COMMAND_REPLY_OK = 1 +} IEC104_CommandReplyResult_t; +typedef enum { + IEC104_COMMAND_PHASE_SELECT_WAIT = 1, + IEC104_COMMAND_PHASE_SELECTED, + IEC104_COMMAND_PHASE_EXECUTE_WAIT, + IEC104_COMMAND_PHASE_EXECUTING, + IEC104_COMMAND_PHASE_ENDED +} IEC104_CommandPhase_t; +typedef struct { + _Bool active; + IEC104_CommandPhase_t phase; + _Bool confirm_submitted; + _Bool result_reported, completed, term_submitted; +} IEC104_CommandTransactionInfo_t; +typedef struct { + uint64_t generation; + _Bool active, events_pending, need_process, timer_active; + _Bool busy; + uint32_t timeout_remaining_ms; + unsigned pending_replies, pending_events; + unsigned long accepted, selected, executed, completed, execution_failed, action_failed; + unsigned long submitted, negative_submitted, return_info_submitted, waits; + unsigned long rejected, app_rejected, busy_rejected, overloads, dropped_requests, cancelled; + unsigned long deactivated, mismatches, select_timeouts, decision_timeouts, execute_timeouts; + unsigned long clock_errors; +} IEC104_CommandStatus_t; +void IEC104_Command_Config_Default(IEC104_CommandConfig_t *config); +IEC104_Command_t *IEC104_Command_Create(IEC104_Slave_t *slave, const IEC104_CommandConfig_t *config); +_Bool IEC104_Command_Destroy(IEC104_Command_t *self); +void IEC104_Command_Cancel(IEC104_Command_t *self); +IEC104_CommandEventResult_t IEC104_Command_Handle_Event(IEC104_Command_t *self, + const IEC104_SlaveEvent_t *event); +_Bool IEC104_Command_Process(IEC104_Command_t *self); +IEC104_CommandNextResult_t IEC104_Command_NextEvent(IEC104_Command_t *self, + IEC104_CommandEvent_t *event); +IEC104_CommandReplyResult_t IEC104_Command_Accept(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandAction_t action, void *context); +IEC104_CommandReplyResult_t IEC104_Command_Reject(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandRejectCause_t cause); +IEC104_CommandReplyResult_t IEC104_Command_Report_Result(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result); +IEC104_CommandReplyResult_t IEC104_Command_Submit_Return_Info(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, const IEC60870_Asdu_t *asdu); +IEC104_CommandReplyResult_t IEC104_Command_Complete(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event); +_Bool IEC104_Command_Get_Transaction(const IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandTransactionInfo_t *info); +_Bool IEC104_Command_Get_Status(const IEC104_Command_t *self, IEC104_CommandStatus_t *status); +typedef uintptr_t IEC104_NetSocket_t; +typedef enum { + IEC104_NET_ERROR = -1, + IEC104_NET_OK = 0, + IEC104_NET_WOULD_BLOCK, + IEC104_NET_RETRY, + IEC104_NET_EOF, + IEC104_NET_RETRY_LATER +} IEC104_NetResult_t; +typedef struct { + IEC104_NetResult_t result; + size_t transferred; + int native_error; +} IEC104_NetIoResult_t; +typedef struct { + char address[16]; + uint16_t port; +} IEC104_NetPeer_t; +extern IEC104_NetResult_t IEC104_Net_Init(int *native_error); +extern void IEC104_Net_Cleanup(void); +extern int IEC104_Net_Invalid_Argument_Error(void); +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); +extern IEC104_NetResult_t IEC104_Net_Accept( + IEC104_NetSocket_t listener, IEC104_NetSocket_t *client, IEC104_NetPeer_t *peer, int *native_error); +extern IEC104_NetIoResult_t IEC104_Net_Recv( + IEC104_NetSocket_t socket_handle, void *data, size_t capacity); +extern IEC104_NetResult_t IEC104_Net_Pending_Input( + IEC104_NetSocket_t socket_handle, uint32_t *available, int *native_error); +extern IEC104_NetIoResult_t IEC104_Net_Send( + IEC104_NetSocket_t socket_handle, const void *data, size_t length); +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); +extern IEC104_NetResult_t IEC104_Net_Close( + IEC104_NetSocket_t socket_handle, int *native_error); +extern uint64_t IEC104_Net_Now_Ms(void); + __attribute__ ((__dllimport__)) void *__attribute__((__cdecl__)) _memccpy(void *_Dst,const void *_Src,int _Val,size_t _MaxCount); + void *__attribute__((__cdecl__)) memchr(const void *_Buf ,int _Val,size_t _MaxCount); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _memicmp(const void *_Buf1,const void *_Buf2,size_t _Size); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _memicmp_l(const void *_Buf1,const void *_Buf2,size_t _Size,_locale_t _Locale); + int __attribute__((__cdecl__)) memcmp(const void *_Buf1,const void *_Buf2,size_t _Size); + void * __attribute__((__cdecl__)) memcpy(void * __restrict__ _Dst,const void * __restrict__ _Src,size_t _Size) ; + __attribute__((dllimport)) errno_t __attribute__((__cdecl__)) memcpy_s (void *_dest,size_t _numberOfElements,const void *_src,size_t _count); + void * __attribute__((__cdecl__)) mempcpy (void *_Dst, const void *_Src, size_t _Size); + void * __attribute__((__cdecl__)) memset(void *_Dst,int _Val,size_t _Size); + void * __attribute__((__cdecl__)) memccpy(void *_Dst,const void *_Src,int _Val,size_t _Size) ; + int __attribute__((__cdecl__)) memicmp(const void *_Buf1,const void *_Buf2,size_t _Size) ; + char * __attribute__((__cdecl__)) _strset(char *_Str,int _Val) ; + char * __attribute__((__cdecl__)) _strset_l(char *_Str,int _Val,_locale_t _Locale) ; + char * __attribute__((__cdecl__)) strcpy(char * __restrict__ _Dest,const char * __restrict__ _Source); + char * __attribute__((__cdecl__)) strcat(char * __restrict__ _Dest,const char * __restrict__ _Source); + int __attribute__((__cdecl__)) strcmp(const char *_Str1,const char *_Str2); + size_t __attribute__((__cdecl__)) strlen(const char *_Str); + size_t __attribute__((__cdecl__)) strnlen(const char *_Str,size_t _MaxCount); + void *__attribute__((__cdecl__)) memmove(void *_Dst,const void *_Src,size_t _Size) ; + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) _strdup(const char *_Src); + char *__attribute__((__cdecl__)) strchr(const char *_Str,int _Val); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _stricmp(const char *_Str1,const char *_Str2); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _strcmpi(const char *_Str1,const char *_Str2); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _stricmp_l(const char *_Str1,const char *_Str2,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) strcoll(const char *_Str1,const char *_Str2); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _strcoll_l(const char *_Str1,const char *_Str2,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _stricoll(const char *_Str1,const char *_Str2); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _stricoll_l(const char *_Str1,const char *_Str2,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _strncoll (const char *_Str1,const char *_Str2,size_t _MaxCount); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _strncoll_l(const char *_Str1,const char *_Str2,size_t _MaxCount,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _strnicoll (const char *_Str1,const char *_Str2,size_t _MaxCount); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _strnicoll_l(const char *_Str1,const char *_Str2,size_t _MaxCount,_locale_t _Locale); + size_t __attribute__((__cdecl__)) strcspn(const char *_Str,const char *_Control); + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) _strerror(const char *_ErrMsg) ; + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) strerror(int) ; + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) _strlwr(char *_String) ; + char *strlwr_l(char *_String,_locale_t _Locale) ; + char *__attribute__((__cdecl__)) strncat(char * __restrict__ _Dest,const char * __restrict__ _Source,size_t _Count) ; + int __attribute__((__cdecl__)) strncmp(const char *_Str1,const char *_Str2,size_t _MaxCount); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _strnicmp(const char *_Str1,const char *_Str2,size_t _MaxCount); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _strnicmp_l(const char *_Str1,const char *_Str2,size_t _MaxCount,_locale_t _Locale); + char *strncpy(char * __restrict__ _Dest,const char * __restrict__ _Source,size_t _Count) ; + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) _strnset(char *_Str,int _Val,size_t _MaxCount) ; + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) _strnset_l(char *str,int c,size_t count,_locale_t _Locale) ; + char *__attribute__((__cdecl__)) strpbrk(const char *_Str,const char *_Control); + char *__attribute__((__cdecl__)) strrchr(const char *_Str,int _Ch); + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) _strrev(char *_Str); + size_t __attribute__((__cdecl__)) strspn(const char *_Str,const char *_Control); + char *__attribute__((__cdecl__)) strstr(const char *_Str,const char *_SubStr); + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) strtok(char * __restrict__ _Str,const char * __restrict__ _Delim) ; + + char *strtok_r(char * __restrict__ _Str, const char * __restrict__ _Delim, char ** __restrict__ __last); + + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) _strupr(char *_String) ; + __attribute__ ((__dllimport__)) char *_strupr_l(char *_String,_locale_t _Locale) ; + __attribute__ ((__dllimport__)) size_t __attribute__((__cdecl__)) strxfrm(char * __restrict__ _Dst,const char * __restrict__ _Src,size_t _MaxCount); + __attribute__ ((__dllimport__)) size_t __attribute__((__cdecl__)) _strxfrm_l(char * __restrict__ _Dst,const char * __restrict__ _Src,size_t _MaxCount,_locale_t _Locale); + char *__attribute__((__cdecl__)) strdup(const char *_Src) ; + int __attribute__((__cdecl__)) strcmpi(const char *_Str1,const char *_Str2) ; + int __attribute__((__cdecl__)) stricmp(const char *_Str1,const char *_Str2) ; + char *__attribute__((__cdecl__)) strlwr(char *_Str) ; + int __attribute__((__cdecl__)) strnicmp(const char *_Str1,const char *_Str,size_t _MaxCount) ; + int __attribute__((__cdecl__)) strncasecmp (const char *, const char *, size_t); + int __attribute__((__cdecl__)) strcasecmp (const char *, const char *); + char *__attribute__((__cdecl__)) strnset(char *_Str,int _Val,size_t _MaxCount) ; + char *__attribute__((__cdecl__)) strrev(char *_Str) ; + char *__attribute__((__cdecl__)) strset(char *_Str,int _Val) ; + char *__attribute__((__cdecl__)) strupr(char *_Str) ; + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcsdup(const wchar_t *_Str); + wchar_t *__attribute__((__cdecl__)) wcscat(wchar_t * __restrict__ _Dest,const wchar_t * __restrict__ _Source) ; + wchar_t *__attribute__((__cdecl__)) wcschr(const wchar_t *_Str,wchar_t _Ch); + int __attribute__((__cdecl__)) wcscmp(const wchar_t *_Str1,const wchar_t *_Str2); + wchar_t *__attribute__((__cdecl__)) wcscpy(wchar_t * __restrict__ _Dest,const wchar_t * __restrict__ _Source) ; + size_t __attribute__((__cdecl__)) wcscspn(const wchar_t *_Str,const wchar_t *_Control); + size_t __attribute__((__cdecl__)) wcslen(const wchar_t *_Str); + size_t __attribute__((__cdecl__)) wcsnlen(const wchar_t *_Src,size_t _MaxCount); + wchar_t *wcsncat(wchar_t * __restrict__ _Dest,const wchar_t * __restrict__ _Source,size_t _Count) ; + int __attribute__((__cdecl__)) wcsncmp(const wchar_t *_Str1,const wchar_t *_Str2,size_t _MaxCount); + wchar_t *wcsncpy(wchar_t * __restrict__ _Dest,const wchar_t * __restrict__ _Source,size_t _Count) ; + wchar_t *__attribute__((__cdecl__)) _wcsncpy_l(wchar_t * __restrict__ _Dest,const wchar_t * __restrict__ _Source,size_t _Count,_locale_t _Locale) ; + wchar_t *__attribute__((__cdecl__)) wcspbrk(const wchar_t *_Str,const wchar_t *_Control); + wchar_t *__attribute__((__cdecl__)) wcsrchr(const wchar_t *_Str,wchar_t _Ch); + size_t __attribute__((__cdecl__)) wcsspn(const wchar_t *_Str,const wchar_t *_Control); + wchar_t *__attribute__((__cdecl__)) wcsstr(const wchar_t *_Str,const wchar_t *_SubStr); + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) wcstok(wchar_t * __restrict__ _Str,const wchar_t * __restrict__ _Delim,wchar_t ** __restrict__ _Ptr) ; + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcstok(wchar_t * __restrict__ _Str,const wchar_t * __restrict__ _Delim) ; + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcserror(int _ErrNum) ; + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) __wcserror(const wchar_t *_Str) ; + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsicmp(const wchar_t *_Str1,const wchar_t *_Str2); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsicmp_l(const wchar_t *_Str1,const wchar_t *_Str2,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsnicmp(const wchar_t *_Str1,const wchar_t *_Str2,size_t _MaxCount); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsnicmp_l(const wchar_t *_Str1,const wchar_t *_Str2,size_t _MaxCount,_locale_t _Locale); + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcsnset(wchar_t *_Str,wchar_t _Val,size_t _MaxCount) ; + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcsrev(wchar_t *_Str); + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcsset(wchar_t *_Str,wchar_t _Val) ; + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcslwr(wchar_t *_String) ; + __attribute__ ((__dllimport__)) wchar_t *_wcslwr_l(wchar_t *_String,_locale_t _Locale) ; + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcsupr(wchar_t *_String) ; + __attribute__ ((__dllimport__)) wchar_t *_wcsupr_l(wchar_t *_String,_locale_t _Locale) ; + __attribute__ ((__dllimport__)) size_t __attribute__((__cdecl__)) wcsxfrm(wchar_t * __restrict__ _Dst,const wchar_t * __restrict__ _Src,size_t _MaxCount); + __attribute__ ((__dllimport__)) size_t __attribute__((__cdecl__)) _wcsxfrm_l(wchar_t * __restrict__ _Dst,const wchar_t * __restrict__ _Src,size_t _MaxCount,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) wcscoll(const wchar_t *_Str1,const wchar_t *_Str2); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcscoll_l(const wchar_t *_Str1,const wchar_t *_Str2,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsicoll(const wchar_t *_Str1,const wchar_t *_Str2); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsicoll_l(const wchar_t *_Str1,const wchar_t *_Str2,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsncoll(const wchar_t *_Str1,const wchar_t *_Str2,size_t _MaxCount); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsncoll_l(const wchar_t *_Str1,const wchar_t *_Str2,size_t _MaxCount,_locale_t _Locale); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsnicoll(const wchar_t *_Str1,const wchar_t *_Str2,size_t _MaxCount); + __attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wcsnicoll_l(const wchar_t *_Str1,const wchar_t *_Str2,size_t _MaxCount,_locale_t _Locale); + wchar_t *__attribute__((__cdecl__)) wcsdup(const wchar_t *_Str) ; + int __attribute__((__cdecl__)) wcsicmp(const wchar_t *_Str1,const wchar_t *_Str2) ; + int __attribute__((__cdecl__)) wcsnicmp(const wchar_t *_Str1,const wchar_t *_Str2,size_t _MaxCount) ; + wchar_t *__attribute__((__cdecl__)) wcsnset(wchar_t *_Str,wchar_t _Val,size_t _MaxCount) ; + wchar_t *__attribute__((__cdecl__)) wcsrev(wchar_t *_Str) ; + wchar_t *__attribute__((__cdecl__)) wcsset(wchar_t *_Str,wchar_t _Val) ; + wchar_t *__attribute__((__cdecl__)) wcslwr(wchar_t *_Str) ; + wchar_t *__attribute__((__cdecl__)) wcsupr(wchar_t *_Str) ; + int __attribute__((__cdecl__)) wcsicoll(const wchar_t *_Str1,const wchar_t *_Str2) ; + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strset_s(char *_Dst,size_t _DstSize,int _Value); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strerror_s(char *_Buf,size_t _SizeInBytes,const char *_ErrMsg); + + __attribute__((dllimport)) errno_t __attribute__((__cdecl__)) strerror_s(char *_Buf,size_t _SizeInBytes,int _ErrNum); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strlwr_s(char *_Str,size_t _Size); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strlwr_s_l(char *_Str,size_t _Size,_locale_t _Locale); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strnset_s(char *_Str,size_t _Size,int _Val,size_t _MaxCount); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strupr_s(char *_Str,size_t _Size); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strupr_s_l(char *_Str,size_t _Size,_locale_t _Locale); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) strncat_s(char *_Dst,size_t _DstSizeInChars,const char *_Src,size_t _MaxCount); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strncat_s_l(char *_Dst,size_t _DstSizeInChars,const char *_Src,size_t _MaxCount,_locale_t _Locale); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) strcpy_s(char *_Dst, rsize_t _SizeInBytes, const char *_Src); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) strncpy_s(char *_Dst, size_t _DstSizeInChars, const char *_Src, size_t _MaxCount); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _strncpy_s_l(char *_Dst, size_t _DstSizeInChars, const char *_Src, size_t _MaxCount, _locale_t _Locale); + + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) strtok_s(char *_Str,const char *_Delim,char **_Context); + __attribute__ ((__dllimport__)) char *__attribute__((__cdecl__)) _strtok_s_l(char *_Str,const char *_Delim,char **_Context,_locale_t _Locale); + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) strcat_s(char *_Dst, rsize_t _SizeInBytes, const char * _Src); + + extern __inline__ __attribute__((__always_inline__,__gnu_inline__)) size_t __attribute__((__cdecl__)) strnlen_s(const char * _src, size_t _count) { + return _src ? strnlen(_src, _count) : 0; + } + __attribute__((dllimport)) errno_t __attribute__((__cdecl__)) memmove_s(void *_dest,size_t _numberOfElements,const void *_src,size_t _count); + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) wcstok_s(wchar_t *_Str,const wchar_t *_Delim,wchar_t **_Context); + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcserror_s(wchar_t *_Buf,size_t _SizeInWords,int _ErrNum); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) __wcserror_s(wchar_t *_Buffer,size_t _SizeInWords,const wchar_t *_ErrMsg); + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcsnset_s(wchar_t *_Dst,size_t _DstSizeInWords,wchar_t _Val,size_t _MaxCount); + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcsset_s(wchar_t *_Str,size_t _SizeInWords,wchar_t _Val); + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcslwr_s(wchar_t *_Str,size_t _SizeInWords); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcslwr_s_l(wchar_t *_Str,size_t _SizeInWords,_locale_t _Locale); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcsupr_s(wchar_t *_Str,size_t _Size); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcsupr_s_l(wchar_t *_Str,size_t _Size,_locale_t _Locale); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) wcscpy_s(wchar_t *_Dst, rsize_t _SizeInWords, const wchar_t *_Src); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) wcscat_s(wchar_t * _Dst, rsize_t _SizeInWords, const wchar_t *_Src); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) wcsncat_s(wchar_t *_Dst,size_t _DstSizeInChars,const wchar_t *_Src,size_t _MaxCount); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcsncat_s_l(wchar_t *_Dst,size_t _DstSizeInChars,const wchar_t *_Src,size_t _MaxCount,_locale_t _Locale); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) wcsncpy_s(wchar_t *_Dst, size_t _DstSizeInChars, const wchar_t *_Src, size_t _MaxCount); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcsncpy_s_l(wchar_t *_Dst, size_t _DstSizeInChars, const wchar_t *_Src, size_t _MaxCount, _locale_t _Locale); + + __attribute__ ((__dllimport__)) wchar_t *__attribute__((__cdecl__)) _wcstok_s_l(wchar_t *_Str,const wchar_t *_Delim,wchar_t **_Context,_locale_t _Locale); + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcsset_s_l(wchar_t *_Str,size_t _SizeInChars,wchar_t _Val,_locale_t _Locale); + + __attribute__ ((__dllimport__)) errno_t __attribute__((__cdecl__)) _wcsnset_s_l(wchar_t *_Str,size_t _SizeInChars,wchar_t _Val, size_t _Count,_locale_t _Locale); + + extern __inline__ __attribute__((__always_inline__,__gnu_inline__)) size_t __attribute__((__cdecl__)) wcsnlen_s(const wchar_t * _src, size_t _count) { + return _src ? wcsnlen(_src, _count) : 0; + } +_Static_assert(1U > 0U, "Command needs instance storage"); +_Static_assert(4U > 0U && + 4U < 0xffffffffU / 2U, "Command needs bounded reply storage"); +typedef struct { + IEC104_SlaveEventIdentity_t identity; + uint8_t request_bytes[10], reply_bytes[10]; + IEC104_Station_t *station; + uint64_t transaction_id, receive_serial, accepted_ms, timer_start_ms; + uint64_t event_serial, accepted_event_serial, select_event_serial, execute_event_serial; + uint64_t confirm_serial, term_serial, reply_serial; + uint8_t phase, event_type, finish_reason, result, confirm_cot, reply_cot; + _Bool occupied, event_taken, ever_work_delivered, finish_pending; + _Bool select_delivered, execute_delivered, selected; + _Bool confirm_negative, confirm_try, send_waiting, return_waiting; + _Bool confirm_submitted, completed, term_submitted, select_effect, execute_effect; +} CommandTransaction; +typedef struct { + IEC104_SlaveEventIdentity_t identity; + uint8_t bytes[10]; + uint64_t serial; + uint8_t cot; + _Bool occupied, send_waiting; +} CommandReply; +struct IEC104_Command { + IEC104_Slave_t *slave; + IEC104_SlaveEventIdentity_t session; + IEC104_CommandConfig_t config; + IEC104_CommandStatus_t status; + CommandTransaction transaction; + CommandReply replies[4U]; + uint64_t serial, next_transaction, next_event, next_reply, last_generation; + _Bool in_use, in_action; +}; +static IEC104_Command_t command_pool[1U]; +static uint64_t next_instance; +static _Bool Owned(const IEC104_Command_t *self) +{ + unsigned i; + for (i = 0U; i < 1U; ++i) { + if (self == &command_pool[i]) { return self->in_use; } + } + return 0; +} +static uint32_t Read_IOA(const uint8_t *bytes) +{ + return (uint32_t)bytes[6] | ((uint32_t)bytes[7] << 8) | ((uint32_t)bytes[8] << 16); +} +static uint16_t Read_CA(const uint8_t *bytes) +{ + return (uint16_t)((uint16_t)bytes[4] | ((uint16_t)bytes[5] << 8)); +} +static void Copy_Request(const CommandTransaction *task, IEC104_CommandRequest_t *request) +{ + memset(request, 0, sizeof(*request)); + request->generation = task->identity.generation; + request->receive_serial = task->receive_serial; + request->station_id = IEC104_Station_Get_Id(task->station); + request->common_address = Read_CA(task->request_bytes); + request->originator_address = task->request_bytes[3]; + request->type_id = task->request_bytes[0]; + request->ioa = Read_IOA(task->request_bytes); + request->is_select = (task->request_bytes[9] & 0x80U) != 0U; + request->selected = task->selected; + request->state = task->request_bytes[9] & (request->type_id == 45U ? 1U : 3U); + request->qualifier = (task->request_bytes[9] >> 2) & 31U; + request->accepted_ms = task->accepted_ms; +} +static unsigned Pending_Replies(const IEC104_Command_t *self) +{ + const CommandTransaction *task = &self->transaction; + unsigned i, count = (task->confirm_serial != 0U ? 1U : 0U) + + (task->term_serial != 0U ? 1U : 0U) + (task->reply_serial != 0U ? 1U : 0U); + for (i = 0U; i < 4U; ++i) { + if (self->replies[i].occupied) { ++count; } + } + return count; +} +static void Reap(IEC104_Command_t *self) +{ + CommandTransaction *task = &self->transaction; + if (!task->occupied || task->phase != IEC104_COMMAND_PHASE_ENDED || task->finish_pending || + task->confirm_serial != 0U || task->term_serial != 0U || task->reply_serial != 0U) { return; } + (void)IEC104_Slave_Release_Station(self->slave, task->station); + memset(task, 0, sizeof(*task)); +} +static void Finish(IEC104_Command_t *self, IEC104_CommandFinishReason_t reason) +{ + CommandTransaction *task = &self->transaction; + if (!task->occupied || task->phase == IEC104_COMMAND_PHASE_ENDED) { Reap(self); return; } + task->phase = IEC104_COMMAND_PHASE_ENDED; + task->finish_reason = (uint8_t)reason; + task->event_type = IEC104_COMMAND_FINISHED; + task->event_taken = 0; + task->finish_pending = task->ever_work_delivered; + task->return_waiting = 0; + if (task->finish_pending) { task->event_serial = ++self->next_event; } + switch (reason) { + case IEC104_COMMAND_FINISH_COMPLETED: ++self->status.completed; break; + case IEC104_COMMAND_FINISH_EXECUTION_FAILED: ++self->status.execution_failed; break; + case IEC104_COMMAND_FINISH_ACTION_FAILED: ++self->status.action_failed; break; + case IEC104_COMMAND_FINISH_MISMATCH: ++self->status.mismatches; break; + case IEC104_COMMAND_FINISH_DEACTIVATED: ++self->status.deactivated; break; + case IEC104_COMMAND_FINISH_SELECT_TIMEOUT: ++self->status.select_timeouts; break; + case IEC104_COMMAND_FINISH_DECISION_TIMEOUT: ++self->status.decision_timeouts; break; + case IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT: ++self->status.execute_timeouts; break; + case IEC104_COMMAND_FINISH_CANCELLED: + case IEC104_COMMAND_FINISH_STOPPED: + case IEC104_COMMAND_FINISH_DISCONNECTED: + case IEC104_COMMAND_FINISH_CONNECTION_CHANGED: + case IEC104_COMMAND_FINISH_SEND_ERROR: + case IEC104_COMMAND_FINISH_SERVICE_ERROR: ++self->status.cancelled; break; + default: break; + } + Reap(self); +} +static void Cancel_All(IEC104_Command_t *self, IEC104_CommandFinishReason_t reason) +{ + CommandTransaction *task = &self->transaction; + task->confirm_serial = task->term_serial = task->reply_serial = 0U; + task->send_waiting = 0; + memset(self->replies, 0, sizeof(self->replies)); + Finish(self, reason); +} +static IEC104_SlaveRequestState_t Check_Session(IEC104_Command_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) { + Cancel_All(self, IEC104_COMMAND_FINISH_SERVICE_ERROR); + self->status.active = 0; + } else if (state != IEC104_SLAVE_REQUEST_WAIT_EVENTS && + (!self->status.active || state != IEC104_SLAVE_REQUEST_READY)) { + Cancel_All(self, IEC104_COMMAND_FINISH_CONNECTION_CHANGED); + self->status.active = 0; + state = IEC104_SLAVE_REQUEST_STALE; + } + return state; +} +static uint32_t Timer_Limit(const IEC104_Command_t *self) +{ + const CommandTransaction *task = &self->transaction; + if (!task->occupied || self->in_action) { return 0U; } + switch (task->phase) { + case IEC104_COMMAND_PHASE_SELECT_WAIT: + case IEC104_COMMAND_PHASE_EXECUTE_WAIT: return self->config.decision_timeout_ms; + case IEC104_COMMAND_PHASE_SELECTED: return self->config.select_timeout_ms; + case IEC104_COMMAND_PHASE_EXECUTING: + return task->result == 0U ? self->config.execute_timeout_ms : 0U; + default: return 0U; + } +} +static uint32_t Remaining(const CommandTransaction *task, uint32_t limit, uint64_t now) +{ + if (now < task->timer_start_ms || now - task->timer_start_ms >= limit) { return 0U; } + return limit - (uint32_t)(now - task->timer_start_ms); +} +static _Bool Clock_Reversed(const IEC104_Command_t *self, uint64_t now) +{ + const CommandTransaction *task = &self->transaction; + return !self->in_action && task->occupied && now < task->timer_start_ms && + (task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT || task->phase == IEC104_COMMAND_PHASE_SELECTED || + task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT || + (task->phase == IEC104_COMMAND_PHASE_EXECUTING && task->result == 0U)); +} +static uint64_t Reply_Serial(IEC104_Command_t *self) +{ + if (self->next_reply == 0xffffffffffffffffULL) { + Cancel_All(self, IEC104_COMMAND_FINISH_SERVICE_ERROR); + self->status.active = 0; + return 0U; + } + return ++self->next_reply; +} +static _Bool Set_Confirm(IEC104_Command_t *self, uint8_t cause, _Bool negative, _Bool try_send) +{ + CommandTransaction *task = &self->transaction; + uint64_t serial = Reply_Serial(self); + if (serial == 0U) { return 0; } + task->confirm_serial = serial; + task->confirm_cot = cause; + task->confirm_negative = negative; + task->confirm_try = try_send; + task->confirm_submitted = 0; + task->send_waiting = 0; + return 1; +} +static void Expire(IEC104_Command_t *self, uint64_t now) +{ + CommandTransaction *task = &self->transaction; + uint32_t limit = Timer_Limit(self); + _Bool reversed = Clock_Reversed(self, now); + if (!reversed && (limit == 0U || Remaining(task, limit, now) != 0U)) { return; } + if (reversed) { ++self->status.clock_errors; } + switch (task->phase) { + case IEC104_COMMAND_PHASE_SELECT_WAIT: + case IEC104_COMMAND_PHASE_EXECUTE_WAIT: + if (Set_Confirm(self, IEC60870_ASDU_COT_ACTIVATION_CON, 1, 0)) { + Finish(self, IEC104_COMMAND_FINISH_DECISION_TIMEOUT); + } + break; + case IEC104_COMMAND_PHASE_SELECTED: + task->confirm_serial = 0U; + Finish(self, IEC104_COMMAND_FINISH_SELECT_TIMEOUT); + break; + case IEC104_COMMAND_PHASE_EXECUTING: + Finish(self, IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT); + break; + default: break; + } +} +static _Bool Defer_For_Stop(IEC104_Command_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, ((void *)0)) != 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 0; } + self->status.events_pending = 1; + return 1; +} +enum { COMMAND_HEAD_NONE, COMMAND_HEAD_CONFIRM, COMMAND_HEAD_TERM, COMMAND_HEAD_REPLY, COMMAND_HEAD_SHORT }; +static unsigned Reply_Head(const IEC104_Command_t *self, unsigned *slot) +{ + const CommandTransaction *task = &self->transaction; + uint64_t first = 0U; + unsigned head = COMMAND_HEAD_NONE, i; + if (task->confirm_serial != 0U) { first = task->confirm_serial; head = COMMAND_HEAD_CONFIRM; } + if (task->term_serial != 0U && (first == 0U || task->term_serial < first)) { + first = task->term_serial; head = COMMAND_HEAD_TERM; + } + if (task->reply_serial != 0U && (first == 0U || task->reply_serial < first)) { + first = task->reply_serial; head = COMMAND_HEAD_REPLY; + } + for (i = 0U; i < 4U; ++i) { + if (self->replies[i].occupied && (first == 0U || self->replies[i].serial < first)) { + first = self->replies[i].serial; head = COMMAND_HEAD_SHORT; *slot = i; + } + } + return head; +} +static void Complete_If_Sent(IEC104_Command_t *self) +{ + CommandTransaction *task = &self->transaction; + if (task->occupied && task->phase == IEC104_COMMAND_PHASE_EXECUTING && task->completed && + task->confirm_submitted && task->confirm_serial == 0U && task->term_serial == 0U) { + Finish(self, task->result == IEC104_COMMAND_RESULT_SUCCEEDED ? + IEC104_COMMAND_FINISH_COMPLETED : IEC104_COMMAND_FINISH_EXECUTION_FAILED); + } + Reap(self); +} +static int Submit_Head(IEC104_Command_t *self) +{ + CommandTransaction *task = &self->transaction; + const IEC104_SlaveEventIdentity_t *identity; + const uint8_t *source; + uint8_t bytes[10], cause; + IEC60870_Asdu_t asdu; + IEC104_TcpSubmitResult_t submitted; + IEC104_SlaveRequestState_t state = Check_Session(self); + unsigned slot = 0U, head; + _Bool negative, try_send; + if (state == IEC104_SLAVE_REQUEST_ERROR) { return -1; } + if (state != IEC104_SLAVE_REQUEST_READY) { return 0; } + head = Reply_Head(self, &slot); + if (head == COMMAND_HEAD_NONE) { return 0; } + identity = &task->identity; + source = task->request_bytes; + cause = IEC60870_ASDU_COT_ACTIVATION_TERMINATION; + negative = 0; + try_send = 1; + if (head == COMMAND_HEAD_CONFIRM) { + cause = task->confirm_cot; negative = task->confirm_negative; try_send = task->confirm_try; + } else if (head == COMMAND_HEAD_REPLY) { + source = task->reply_bytes; cause = task->reply_cot; + negative = cause != IEC60870_ASDU_COT_DEACTIVATION_CON; try_send = 0; + } else if (head == COMMAND_HEAD_SHORT) { + identity = &self->replies[slot].identity; + source = self->replies[slot].bytes; cause = self->replies[slot].cot; + negative = 1; try_send = 0; + } + memcpy(bytes, source, sizeof(bytes)); + if (IEC60870_Asdu_Create_From_Buffer(&asdu, IEC104_Slave_Get_Application_Layer_Parameters(self->slave), + bytes, sizeof(bytes)) != IEC60870_ASDU_OK || + IEC60870_Asdu_Set_COT(&asdu, cause) != IEC60870_ASDU_OK || + IEC60870_Asdu_Set_Negative(&asdu, negative) != IEC60870_ASDU_OK) { + Cancel_All(self, IEC104_COMMAND_FINISH_SERVICE_ERROR); + return -1; + } + submitted = try_send ? IEC104_Slave_Try_Send_Response(self->slave, identity, &asdu) : + IEC104_Slave_Send_Response(self->slave, identity, &asdu); + if (submitted == IEC104_TCP_SUBMIT_OK) { + ++self->status.submitted; + if (negative) { ++self->status.negative_submitted; } + if (head == COMMAND_HEAD_SHORT) { memset(&self->replies[slot], 0, sizeof(self->replies[slot])); } + else { + task->send_waiting = 0; + if (head == COMMAND_HEAD_CONFIRM) { task->confirm_serial = 0U; task->confirm_submitted = 1; } + else if (head == COMMAND_HEAD_TERM) { task->term_serial = 0U; task->term_submitted = 1; } + else { task->reply_serial = 0U; } + } + Complete_If_Sent(self); + return 1; + } + if (submitted == IEC104_TCP_SUBMIT_WAIT) { + if (head == COMMAND_HEAD_SHORT) { self->replies[slot].send_waiting = 1; } + else { task->send_waiting = 1; } + ++self->status.waits; + return 0; + } + if (Defer_For_Stop(self, identity, submitted)) { return 0; } + Cancel_All(self, IEC104_COMMAND_FINISH_SEND_ERROR); + self->status.active = 0; + return 0; +} +static _Bool Short_Reply(IEC104_Command_t *self, const IEC104_SlaveEventIdentity_t *identity, + const uint8_t *data, uint8_t cause) +{ + unsigned slot; + _Bool empty = Pending_Replies(self) == 0U; + uint64_t serial; + for (slot = 0U; slot < 4U; ++slot) { + if (!self->replies[slot].occupied) { break; } + } + if (slot == 4U) { + ++self->status.overloads; + ++self->status.dropped_requests; + return 1; + } + serial = Reply_Serial(self); + if (serial == 0U) { return 0; } + self->replies[slot].identity = *identity; + memcpy(self->replies[slot].bytes, data, sizeof(self->replies[slot].bytes)); + self->replies[slot].bytes[1] = 1U; + self->replies[slot].serial = serial; + self->replies[slot].cot = cause; + self->replies[slot].occupied = 1; + self->replies[slot].send_waiting = 0; + return !empty || Submit_Head(self) >= 0; +} +static void Work_Event(IEC104_Command_t *self, const uint8_t *data, uint64_t receive_serial, + uint64_t now, _Bool selected) +{ + CommandTransaction *task = &self->transaction; + memcpy(task->request_bytes, data, sizeof(task->request_bytes)); + task->request_bytes[1] = 1U; + task->receive_serial = receive_serial; + task->accepted_ms = task->timer_start_ms = now; + task->event_type = (data[9] & 0x80U) != 0U ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE; + task->phase = task->event_type == IEC104_COMMAND_SELECT ? + IEC104_COMMAND_PHASE_SELECT_WAIT : IEC104_COMMAND_PHASE_EXECUTE_WAIT; + task->event_serial = ++self->next_event; + task->accepted_event_serial = 0U; + if (task->event_type == IEC104_COMMAND_SELECT) { task->select_event_serial = task->event_serial; } + else { task->execute_event_serial = task->event_serial; } + task->event_taken = 0; + task->selected = selected; + task->confirm_submitted = 0; + task->send_waiting = 0; + ++self->status.accepted; +} +static _Bool End_With_Reply(IEC104_Command_t *self, const uint8_t *data, uint8_t cause, + IEC104_CommandFinishReason_t reason) +{ + CommandTransaction *task = &self->transaction; + uint64_t serial = Reply_Serial(self); + if (serial == 0U) { return 0; } + task->confirm_serial = task->term_serial = 0U; + memcpy(task->reply_bytes, data, sizeof(task->reply_bytes)); + task->reply_bytes[1] = 1U; + task->reply_cot = cause; + task->reply_serial = serial; + task->send_waiting = 0; + Finish(self, reason); + return Submit_Head(self) >= 0; +} +static IEC104_CommandEventResult_t Request(IEC104_Command_t *self, const IEC104_SlaveEvent_t *event, + const IEC104_SlaveEventIdentity_t *identity) +{ + CommandTransaction *task = &self->transaction; + IEC60870_AsduInfo_t info; + IEC104_Station_t *station; + const uint8_t *data; + uint16_t length; + uint8_t cause = 0U, state; + uint32_t ioa; + uint64_t now; + _Bool same_point, is_select; + 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] != 45U && data[0] != 46U) || + (self->config.type_mask & ((uint16_t)( ((data[0]) >= 45U && (data[0]) <= 51U) ? (1U << ((data[0]) - 45U)) : (((data[0]) >= 58U && (data[0]) <= 64U) ? (1U << ((data[0]) - 51U)) : 0U)))) == 0U || + !IEC60870_Parse_ASDU_Header(event->asdu.parameters, data, length, &info)) { + return IEC104_COMMAND_EVENT_IGNORED; + } + now = IEC104_Net_Now_Ms(); + Expire(self, now); + if (info.object_count != 1U || info.information_objects_len < 4U) { + ++self->status.rejected; + return IEC104_COMMAND_EVENT_HANDLED; + } + ioa = Read_IOA(data); + station = IEC104_Slave_Get_Station(self->slave, info.common_address); + state = data[9] & 3U; + is_select = (data[9] & 0x80U) != 0U; + if (info.cause_of_transmission != IEC60870_ASDU_COT_ACTIVATION && + info.cause_of_transmission != IEC60870_ASDU_COT_DEACTIVATION) { + cause = IEC60870_ASDU_COT_UNKNOWN_CAUSE; + } else if (station == ((void *)0)) { cause = IEC60870_ASDU_COT_UNKNOWN_COMMON_ADDRESS; } + else if (ioa == 0U) { cause = IEC60870_ASDU_COT_UNKNOWN_IOA; } + else if (info.is_sequence || length != 10U || info.is_negative_confirm || info.is_test || + (data[0] == 45U ? (data[9] & 2U) != 0U : (state == 0U || state == 3U))) { + cause = info.cause_of_transmission == IEC60870_ASDU_COT_DEACTIVATION ? + IEC60870_ASDU_COT_DEACTIVATION_CON : IEC60870_ASDU_COT_ACTIVATION_CON; + } + if (cause != 0U) { + ++self->status.rejected; + return Short_Reply(self, identity, data, cause) ? IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + same_point = task->occupied && task->identity.generation == identity->generation && + IEC104_Station_Get_Id(task->station) == IEC104_Station_Get_Id(station) && + Read_IOA(task->request_bytes) == ioa; + if (info.cause_of_transmission == IEC60870_ASDU_COT_DEACTIVATION) { + if (same_point && task->request_bytes[0] == data[0] && task->request_bytes[3] == data[3] && + (task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT || task->phase == IEC104_COMMAND_PHASE_SELECTED || + task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT)) { + return End_With_Reply(self, data, IEC60870_ASDU_COT_DEACTIVATION_CON, + IEC104_COMMAND_FINISH_DEACTIVATED) ? IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + ++self->status.rejected; + return Short_Reply(self, identity, data, IEC60870_ASDU_COT_DEACTIVATION_CON) ? + IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + if (same_point && !is_select && task->phase == IEC104_COMMAND_PHASE_SELECTED && task->confirm_submitted) { + if (task->request_bytes[0] != data[0] || task->request_bytes[3] != data[3] || + (task->request_bytes[9] & 0x7FU) != (data[9] & 0x7FU)) { + ++self->status.rejected; + return End_With_Reply(self, data, IEC60870_ASDU_COT_ACTIVATION_CON, + IEC104_COMMAND_FINISH_MISMATCH) ? IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + Work_Event(self, data, event->transport.data.i_frame.receive_serial, now, 1); + return IEC104_COMMAND_EVENT_HANDLED; + } + if (task->occupied || (!is_select && (self->config.select_required_mask & ((uint16_t)( ((data[0]) >= 45U && (data[0]) <= 51U) ? (1U << ((data[0]) - 45U)) : (((data[0]) >= 58U && (data[0]) <= 64U) ? (1U << ((data[0]) - 51U)) : 0U)))) != 0U)) { + ++self->status.rejected; + if (task->occupied) { ++self->status.busy_rejected; } + return Short_Reply(self, identity, data, IEC60870_ASDU_COT_ACTIVATION_CON) ? + IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + if (self->next_transaction == 0xffffffffffffffffULL || self->next_event > 0xffffffffffffffffULL - 3U) { + ++self->status.overloads; + ++self->status.dropped_requests; + return IEC104_COMMAND_EVENT_HANDLED; + } + if (IEC104_Slave_Acquire_Station(self->slave, station) != IEC104_STATION_OK) { return IEC104_COMMAND_EVENT_ERROR; } + memset(task, 0, sizeof(*task)); + task->occupied = 1; + task->identity = *identity; + task->station = station; + task->transaction_id = ++self->next_transaction; + Work_Event(self, data, event->transport.data.i_frame.receive_serial, now, 0); + return IEC104_COMMAND_EVENT_HANDLED; +} +void IEC104_Command_Config_Default(IEC104_CommandConfig_t *config) +{ + if (config == ((void *)0)) { return; } + memset(config, 0, sizeof(*config)); + config->type_mask = ((uint16_t)( ((45U) >= 45U && (45U) <= 51U) ? (1U << ((45U) - 45U)) : (((45U) >= 58U && (45U) <= 64U) ? (1U << ((45U) - 51U)) : 0U))) | ((uint16_t)( ((46U) >= 45U && (46U) <= 51U) ? (1U << ((46U) - 45U)) : (((46U) >= 58U && (46U) <= 64U) ? (1U << ((46U) - 51U)) : 0U))); + config->select_required_mask = config->type_mask; + config->select_timeout_ms = 30000U; + config->decision_timeout_ms = 15000U; + config->execute_timeout_ms = 60000U; + config->send_act_term = 1; +} +IEC104_Command_t *IEC104_Command_Create(IEC104_Slave_t *slave, const IEC104_CommandConfig_t *config) +{ + const uint16_t supported = ((uint16_t)( ((45U) >= 45U && (45U) <= 51U) ? (1U << ((45U) - 45U)) : (((45U) >= 58U && (45U) <= 64U) ? (1U << ((45U) - 51U)) : 0U))) | ((uint16_t)( ((46U) >= 45U && (46U) <= 51U) ? (1U << ((46U) - 45U)) : (((46U) >= 58U && (46U) <= 64U) ? (1U << ((46U) - 51U)) : 0U))); + IEC104_Command_t *free_slot = ((void *)0); + unsigned i; + if (config == ((void *)0) || config->type_mask == 0U || (config->type_mask & (uint16_t)~supported) != 0U || + (config->select_required_mask & (uint16_t)~config->type_mask) != 0U || config->select_timeout_ms == 0U || + next_instance == 0xffffffffffffffffULL || IEC104_Slave_Get_Application_Layer_Parameters(slave) == ((void *)0)) { return ((void *)0); } + for (i = 0U; i < 1U; ++i) { + if (command_pool[i].in_use && command_pool[i].slave == slave) { return ((void *)0); } + if (!command_pool[i].in_use && free_slot == ((void *)0)) { free_slot = &command_pool[i]; } + } + if (free_slot == ((void *)0)) { return ((void *)0); } + memset(free_slot, 0, sizeof(*free_slot)); + free_slot->slave = slave; + free_slot->config = *config; + free_slot->serial = ++next_instance; + free_slot->in_use = 1; + return free_slot; +} +_Bool IEC104_Command_Destroy(IEC104_Command_t *self) +{ + if (!Owned(self) || self->in_action || self->transaction.occupied || Pending_Replies(self) != 0U) { return 0; } + memset(self, 0, sizeof(*self)); + return 1; +} +void IEC104_Command_Cancel(IEC104_Command_t *self) +{ + if (Owned(self) && !self->in_action) { Cancel_All(self, IEC104_COMMAND_FINISH_CANCELLED); } +} +IEC104_CommandEventResult_t IEC104_Command_Handle_Event(IEC104_Command_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_COMMAND_EVENT_ERROR; + } + self->session.slave = identity.slave; + self->session.instance_id = identity.instance_id; + if (event->transport.type == IEC104_TCP_EVENT_CONNECTED) { + if (identity.generation <= self->last_generation) { return IEC104_COMMAND_EVENT_IGNORED; } + Cancel_All(self, IEC104_COMMAND_FINISH_CONNECTION_CHANGED); + self->session.generation = self->last_generation = identity.generation; + self->status.active = 0; + return IEC104_COMMAND_EVENT_HANDLED; + } + if (identity.generation != self->session.generation || identity.generation == 0U) { + return IEC104_COMMAND_EVENT_IGNORED; + } + switch (event->transport.type) { + case IEC104_TCP_EVENT_ACTIVATED: + self->status.active = 1; + return IEC104_COMMAND_EVENT_HANDLED; + case IEC104_TCP_EVENT_DEACTIVATED: + Cancel_All(self, IEC104_COMMAND_FINISH_STOPPED); + self->status.active = 0; + return IEC104_COMMAND_EVENT_HANDLED; + case IEC104_TCP_EVENT_DISCONNECTED: + Cancel_All(self, IEC104_COMMAND_FINISH_DISCONNECTED); + self->session.generation = 0U; + self->status.active = 0; + return IEC104_COMMAND_EVENT_HANDLED; + case IEC104_TCP_EVENT_I_FRAME: return Request(self, event, &identity); + default: return IEC104_COMMAND_EVENT_IGNORED; + } +} +_Bool IEC104_Command_Process(IEC104_Command_t *self) +{ + unsigned step; + if (!Owned(self) || self->in_action) { return 0; } + for (step = 0U; step < 2U * (1U + 4U); ++step) { + IEC104_SlaveRequestState_t state = Check_Session(self); + int submitted; + if (state == IEC104_SLAVE_REQUEST_ERROR) { return 0; } + if (state != IEC104_SLAVE_REQUEST_READY) { return 1; } + Expire(self, IEC104_Net_Now_Ms()); + Complete_If_Sent(self); + submitted = Submit_Head(self); + if (submitted <= 0) { return submitted == 0; } + } + return 1; +} +IEC104_CommandNextResult_t IEC104_Command_NextEvent(IEC104_Command_t *self, IEC104_CommandEvent_t *event) +{ + CommandTransaction *task; + IEC104_CommandEvent_t result; + IEC104_SlaveRequestState_t state = IEC104_SLAVE_REQUEST_READY; + if (!Owned(self) || self->in_action || event == ((void *)0)) { return IEC104_COMMAND_NEXT_ERROR; } + task = &self->transaction; + if (!task->finish_pending) { + state = Check_Session(self); + if (state == IEC104_SLAVE_REQUEST_READY) { Expire(self, IEC104_Net_Now_Ms()); } + } + if (!task->finish_pending && state == IEC104_SLAVE_REQUEST_ERROR) { return IEC104_COMMAND_NEXT_ERROR; } + if (!task->occupied || (!task->finish_pending && + (state != IEC104_SLAVE_REQUEST_READY || task->event_taken || + (task->phase != IEC104_COMMAND_PHASE_SELECT_WAIT && task->phase != IEC104_COMMAND_PHASE_EXECUTE_WAIT)))) { + return IEC104_COMMAND_NEXT_EMPTY; + } + memset(&result, 0, sizeof(result)); + result.type = (IEC104_CommandEventType_t)task->event_type; + result.instance_id = self->serial; + result.transaction_id = task->transaction_id; + result.event_serial = task->event_serial; + Copy_Request(task, &result.request); + if (task->finish_pending) { + result.reason = (IEC104_CommandFinishReason_t)task->finish_reason; + result.select_effect = task->select_effect; + result.execute_effect = task->execute_effect; + task->finish_pending = 0; + } else { + task->ever_work_delivered = 1; + if (result.type == IEC104_COMMAND_SELECT) { task->select_delivered = 1; } + else { task->execute_delivered = 1; } + } + task->event_taken = 1; + *event = result; + Reap(self); + return IEC104_COMMAND_NEXT_READY; +} +static _Bool Current_Event(const IEC104_Command_t *self, const IEC104_CommandEvent_t *event) +{ + const CommandTransaction *task = &self->transaction; + return event != ((void *)0) && task->occupied && event->instance_id == self->serial && + event->transaction_id == task->transaction_id && event->event_serial == task->event_serial && + event->type == task->event_type && task->event_taken; +} +static _Bool Reply_Stage(const IEC104_Command_t *self, const IEC104_CommandEvent_t *event, _Bool work) +{ + const CommandTransaction *task = &self->transaction; + if (!Current_Event(self, event)) { return 0; } + if (work) { + return task->accepted_event_serial == 0U && + ((event->type == IEC104_COMMAND_SELECT && task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT) || + (event->type == IEC104_COMMAND_EXECUTE && task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT)); + } + return event->type == IEC104_COMMAND_EXECUTE && task->phase == IEC104_COMMAND_PHASE_EXECUTING && + task->accepted_event_serial == event->event_serial && task->execute_effect; +} +static IEC104_CommandReplyResult_t Check_Reply(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, _Bool work) +{ + IEC104_SlaveRequestState_t state; + if (!Owned(self) || self->in_action || event == ((void *)0)) { return IEC104_COMMAND_REPLY_ERROR; } + if (!Reply_Stage(self, event, work)) { return IEC104_COMMAND_REPLY_STALE; } + state = Check_Session(self); + if (state == IEC104_SLAVE_REQUEST_ERROR) { return IEC104_COMMAND_REPLY_ERROR; } + if (state == IEC104_SLAVE_REQUEST_WAIT_EVENTS) { return IEC104_COMMAND_REPLY_WAIT; } + if (state != IEC104_SLAVE_REQUEST_READY) { return IEC104_COMMAND_REPLY_STALE; } + Expire(self, IEC104_Net_Now_Ms()); + return Reply_Stage(self, event, work) ? IEC104_COMMAND_REPLY_OK : IEC104_COMMAND_REPLY_STALE; +} +IEC104_CommandReplyResult_t IEC104_Command_Accept(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandAction_t action, void *context) +{ + CommandTransaction *task; + IEC104_CommandRequest_t request; + IEC104_CommandActionResult_t action_result; + IEC104_CommandReplyResult_t checked; + _Bool selecting; + if (event != ((void *)0) && event->type == IEC104_COMMAND_EXECUTE && action == ((void *)0)) { + return IEC104_COMMAND_REPLY_ERROR; + } + checked = Check_Reply(self, event, 1); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + task = &self->transaction; + selecting = task->event_type == IEC104_COMMAND_SELECT; + if (!Set_Confirm(self, IEC60870_ASDU_COT_ACTIVATION_CON, 0, !selecting)) { + return IEC104_COMMAND_REPLY_ERROR; + } + Copy_Request(task, &request); + task->accepted_event_serial = task->event_serial; + self->in_action = 1; + action_result = action == ((void *)0) ? IEC104_COMMAND_ACTION_STARTED : action(context, &request); + self->in_action = 0; + if (action_result != IEC104_COMMAND_ACTION_STARTED && action_result != IEC104_COMMAND_ACTION_NOT_STARTED) { + action_result = IEC104_COMMAND_ACTION_FAILED; + } + if (selecting) { task->select_effect = action != ((void *)0) && action_result != IEC104_COMMAND_ACTION_NOT_STARTED; } + else { task->execute_effect = action_result != IEC104_COMMAND_ACTION_NOT_STARTED; } + if (action_result == IEC104_COMMAND_ACTION_STARTED) { + task->phase = selecting ? IEC104_COMMAND_PHASE_SELECTED : IEC104_COMMAND_PHASE_EXECUTING; + task->timer_start_ms = IEC104_Net_Now_Ms(); + if (selecting) { ++self->status.selected; } + else { ++self->status.executed; } + } else { + task->confirm_negative = 1; + task->confirm_try = !selecting && action_result == IEC104_COMMAND_ACTION_FAILED; + if (action_result == IEC104_COMMAND_ACTION_NOT_STARTED) { + ++self->status.app_rejected; + Finish(self, IEC104_COMMAND_FINISH_REJECTED); + } else { Finish(self, IEC104_COMMAND_FINISH_ACTION_FAILED); } + } + (void)Submit_Head(self); + return IEC104_COMMAND_REPLY_OK; +} +IEC104_CommandReplyResult_t IEC104_Command_Reject(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandRejectCause_t cause) +{ + IEC104_CommandReplyResult_t checked; + if (cause != IEC104_COMMAND_REJECT_NEGATIVE && cause != IEC104_COMMAND_REJECT_UNKNOWN_IOA) { + return IEC104_COMMAND_REPLY_ERROR; + } + checked = Check_Reply(self, event, 1); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + if (!Set_Confirm(self, cause == IEC104_COMMAND_REJECT_UNKNOWN_IOA ? + IEC60870_ASDU_COT_UNKNOWN_IOA : IEC60870_ASDU_COT_ACTIVATION_CON, 1, 0)) { + return IEC104_COMMAND_REPLY_ERROR; + } + self->transaction.accepted_event_serial = self->transaction.event_serial; + ++self->status.app_rejected; + Finish(self, IEC104_COMMAND_FINISH_REJECTED); + (void)Submit_Head(self); + return IEC104_COMMAND_REPLY_OK; +} +IEC104_CommandReplyResult_t IEC104_Command_Report_Result(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result) +{ + IEC104_CommandReplyResult_t checked; + if (result != IEC104_COMMAND_RESULT_SUCCEEDED && result != IEC104_COMMAND_RESULT_FAILED) { + return IEC104_COMMAND_REPLY_ERROR; + } + checked = Check_Reply(self, event, 0); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + if (self->transaction.result != 0U || self->transaction.completed) { return IEC104_COMMAND_REPLY_ERROR; } + self->transaction.result = (uint8_t)result; + return IEC104_COMMAND_REPLY_OK; +} +IEC104_CommandReplyResult_t IEC104_Command_Submit_Return_Info(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, const IEC60870_Asdu_t *asdu) +{ + CommandTransaction *task; + IEC60870_AsduInfo_t info; + const uint8_t *data; + uint16_t length; + IEC104_TcpSubmitResult_t submitted; + IEC104_CommandReplyResult_t checked = Check_Reply(self, event, 0); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + task = &self->transaction; + if (task->completed || asdu == ((void *)0) || !IEC60870_AppLayerParameters_Valid(asdu->parameters) || + asdu->parameters->sizeOfCOT != 2 || asdu->parameters->sizeOfCA != 2 || asdu->parameters->sizeOfIOA != 3 || + asdu->parameters->maxSizeOfASDU > 249 || + IEC60870_Asdu_Get_Data(asdu, &data, &length) != IEC60870_ASDU_OK || length > 249 || + IEC60870_Asdu_Parse(IEC104_Slave_Get_Application_Layer_Parameters(self->slave), data, length, &info) != + IEC60870_ASDU_OK || + !((info.type_id >= 1U && info.type_id <= 21U) || (info.type_id >= 30U && info.type_id <= 40U)) || + info.object_count == 0U || + (info.cause_of_transmission != IEC60870_ASDU_COT_RETURN_INFO_REMOTE && + info.cause_of_transmission != IEC60870_ASDU_COT_SPONTANEOUS) || + info.is_negative_confirm || info.is_test != ((task->request_bytes[2] >> 7) & 1U) || + info.common_address != Read_CA(task->request_bytes)) { return IEC104_COMMAND_REPLY_ERROR; } + if (!task->confirm_submitted || Pending_Replies(self) != 0U) { return IEC104_COMMAND_REPLY_WAIT; } + submitted = IEC104_Slave_Try_Send_Response(self->slave, &task->identity, asdu); + if (submitted == IEC104_TCP_SUBMIT_WAIT) { + task->return_waiting = 1; + ++self->status.waits; + return IEC104_COMMAND_REPLY_WAIT; + } + if (submitted == IEC104_TCP_SUBMIT_OK) { + task->return_waiting = 0; + ++self->status.submitted; + ++self->status.return_info_submitted; + return IEC104_COMMAND_REPLY_OK; + } + if (Defer_For_Stop(self, &task->identity, submitted)) { return IEC104_COMMAND_REPLY_WAIT; } + Cancel_All(self, IEC104_COMMAND_FINISH_SEND_ERROR); + self->status.active = 0; + return IEC104_COMMAND_REPLY_STALE; +} +IEC104_CommandReplyResult_t IEC104_Command_Complete(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event) +{ + CommandTransaction *task; + IEC104_CommandReplyResult_t checked = Check_Reply(self, event, 0); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + task = &self->transaction; + if (task->result == 0U || task->completed) { return IEC104_COMMAND_REPLY_ERROR; } + if (self->config.send_act_term && + (task->result == IEC104_COMMAND_RESULT_SUCCEEDED || self->config.act_term_on_failure)) { + uint64_t serial = Reply_Serial(self); + if (serial == 0U) { return IEC104_COMMAND_REPLY_ERROR; } + task->term_serial = serial; + } + task->completed = 1; + task->return_waiting = 0; + Complete_If_Sent(self); + return IEC104_COMMAND_REPLY_OK; +} +_Bool IEC104_Command_Get_Transaction(const IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandTransactionInfo_t *info) +{ + const CommandTransaction *task; + IEC104_CommandTransactionInfo_t result; + _Bool delivered; + if (!Owned(self) || event == ((void *)0) || info == ((void *)0)) { return 0; } + task = &self->transaction; + if (!task->occupied || event->instance_id != self->serial || event->transaction_id != task->transaction_id) { + return 0; + } + delivered = (event->type == IEC104_COMMAND_SELECT && task->select_delivered && + event->event_serial == task->select_event_serial) || + (event->type == IEC104_COMMAND_EXECUTE && task->execute_delivered && + event->event_serial == task->execute_event_serial) || + (event->type == IEC104_COMMAND_FINISHED && task->event_type == IEC104_COMMAND_FINISHED && + task->event_taken && event->event_serial == task->event_serial); + if (!delivered) { return 0; } + memset(&result, 0, sizeof(result)); + result.active = task->phase != IEC104_COMMAND_PHASE_ENDED; + result.phase = (IEC104_CommandPhase_t)task->phase; + result.confirm_submitted = task->confirm_submitted; + result.result_reported = task->result != 0U; + result.completed = task->completed; + result.term_submitted = task->term_submitted; + *info = result; + return 1; +} +_Bool IEC104_Command_Get_Status(const IEC104_Command_t *self, IEC104_CommandStatus_t *status) +{ + const CommandTransaction *task; + IEC104_CommandStatus_t result; + IEC104_SlaveRequestState_t state; + uint32_t limit; + uint64_t now; + unsigned slot = 0U, head; + _Bool waiting; + if (!Owned(self) || status == ((void *)0)) { return 0; } + state = IEC104_Slave_Check_Request(self->slave, &self->session); + if (state == IEC104_SLAVE_REQUEST_ERROR) { return 0; } + task = &self->transaction; + result = self->status; + result.generation = self->session.generation; + result.events_pending = state == IEC104_SLAVE_REQUEST_WAIT_EVENTS; + result.busy = task->occupied; + result.pending_replies = Pending_Replies(self); + result.pending_events = task->occupied && (task->finish_pending || + (!task->event_taken && (task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT || + task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT))) ? 1U : 0U; + limit = Timer_Limit(self); + now = IEC104_Net_Now_Ms(); + result.timer_active = limit != 0U; + result.timeout_remaining_ms = limit != 0U ? Remaining(task, limit, now) : 0U; + result.need_process = 0; + head = Reply_Head(self, &slot); + waiting = head == COMMAND_HEAD_SHORT ? self->replies[slot].send_waiting : task->send_waiting; + if (!self->in_action && !result.events_pending) { + if (state != IEC104_SLAVE_REQUEST_READY || !result.active) { + result.need_process = result.pending_replies != 0U || + (task->occupied && task->phase != IEC104_COMMAND_PHASE_ENDED); + } else { + result.need_process = Clock_Reversed(self, now) || + (result.timer_active && result.timeout_remaining_ms == 0U) || + (head != COMMAND_HEAD_NONE && !waiting); + } + } + *status = result; + return 1; +} diff --git a/IEC60870-Slave/.build/command-comments/iec104_command_test.c.before b/IEC60870-Slave/.build/command-comments/iec104_command_test.c.before new file mode 100644 index 00000000..12249150 --- /dev/null +++ b/IEC60870-Slave/.build/command-comments/iec104_command_test.c.before @@ -0,0 +1,1101 @@ +/* Command transactions over the real Slave/TCP/SendManager path with controlled I/O. + * Reuse only the existing Net fixture; its renamed test entry is not run here. + * Do not separately link TcpServer.c or a platform Net backend for this target. */ +#define main command_net_fixture_main +#include "iec104_tcp_event_ring_test.c" +#undef main +#include "IEC104_Command.h" + +#ifndef IEC104_COMMAND_REPLY_CAPACITY +#define IEC104_COMMAND_REPLY_CAPACITY 4U +#endif + +static IEC104_Slave_t *command_slave; +static IEC104_Command_t *command; +static IEC104_Station_t command_station; +static IEC104_CommandStatus_t command_status; +static IEC104_SlaveEvent_t command_last_input; +static IEC104_CommandEventResult_t command_last_handled; +static unsigned command_master_ns, command_action_calls, command_checks; +static IEC104_CommandActionResult_t command_action_result; +static IEC104_CommandRequest_t command_action_request; +static uint16_t command_window = 32U; + +static void command_drain_transport(void) +{ + IEC104_SlaveEvent_t event; + IEC104_TcpEventResult_t result; + while ((result = IEC104_Slave_NextEvent(command_slave, &event)) == IEC104_TCP_EVENT_READY) { + IEC104_CommandEventResult_t handled; + if (event.transport.type == IEC104_TCP_EVENT_I_FRAME) { + CHECK(IEC104_Slave_Copy_Event(&command_last_input, &event)); + } + handled = IEC104_Command_Handle_Event(command, &event); + CHECK(handled != IEC104_COMMAND_EVENT_ERROR); + if (event.transport.type == IEC104_TCP_EVENT_I_FRAME) { command_last_handled = handled; } + } + CHECK(result == IEC104_TCP_EVENT_EMPTY); +} + +static void command_poll(void) +{ + CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK); + command_drain_transport(); +} + +static void command_process(void) +{ + CHECK(IEC104_Command_Process(command)); + CHECK(IEC104_Command_Get_Status(command, &command_status)); +} + +static void command_flush(void) +{ + unsigned index; + for (index = 0U; index < 16U; ++index) { + command_poll(); + command_process(); + } +} + +static void command_connect(void) +{ + fake.accept_pending = 1U; + command_poll(); + push(start_act, sizeof(start_act)); + command_poll(); + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(command_status.active); + command_master_ns = 0U; +} + +static void command_open(const IEC104_CommandConfig_t *configuration) +{ + IEC104_SlaveConfig_t server_configuration; + IEC104_CommandConfig_t defaults; + unsigned slot; + memset(&fake, 0, sizeof(fake)); + memset(&command_station, 0, sizeof(command_station)); + memset(&command_last_input, 0, sizeof(command_last_input)); + fake.now = 1000U; + command_action_calls = 0U; + command_action_result = IEC104_COMMAND_ACTION_STARTED; + command_slave = IEC104_Slave_Create(); + CHECK(command_slave != NULL); + IEC104_Slave_Config_Default(&server_configuration); + server_configuration.protocol.link.k = command_window; + server_configuration.protocol.link.t1_ms = 600000U; + server_configuration.protocol.link.t3_ms = 600000U; + server_configuration.protocol.local_send_timeout_ms = 600000U; + CHECK(IEC104_Slave_Init(command_slave, &server_configuration, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Add_Station(command_slave, &command_station, 1U, &command_action_calls) == IEC104_STATION_OK); + IEC104_Command_Config_Default(&defaults); + command = IEC104_Command_Create(command_slave, configuration == NULL ? &defaults : configuration); + CHECK(command != NULL); + sut = NULL; + for (slot = 0U; slot < IEC104_TCP_SERVER_CAPACITY; ++slot) { + if (server_pool[slot].in_use) { sut = &server_pool[slot].server; break; } + } + CHECK(sut != NULL); + command_connect(); +} + +static IEC104_CommandEvent_t command_take(IEC104_CommandEventType_t type) +{ + IEC104_CommandEvent_t event; + memset(&event, 0, sizeof(event)); + CHECK(IEC104_Command_NextEvent(command, &event) == IEC104_COMMAND_NEXT_READY); + CHECK(event.type == type); + return event; +} + +static void command_empty(void) +{ + IEC104_CommandEvent_t event, before; + memset(&event, 0x5a, sizeof(event)); + before = event; + CHECK(IEC104_Command_NextEvent(command, &event) == IEC104_COMMAND_NEXT_EMPTY); + CHECK(memcmp(&event, &before, sizeof(event)) == 0); +} + +static void command_close(void) +{ + IEC104_CommandEvent_t event; + IEC104_CommandNextResult_t result; + IEC104_Command_Cancel(command); + while ((result = IEC104_Command_NextEvent(command, &event)) == IEC104_COMMAND_NEXT_READY) { + CHECK(event.type == IEC104_COMMAND_FINISHED); + } + CHECK(result == IEC104_COMMAND_NEXT_EMPTY); + CHECK(IEC104_Slave_Close(command_slave, NULL) == IEC104_TCP_OK); + command_drain_transport(); + CHECK(IEC104_Command_Destroy(command)); + if (IEC104_Station_Is_Registered(&command_station)) { + CHECK(IEC104_Slave_Remove_Station(command_slave, &command_station) == IEC104_STATION_OK); + } + CHECK(IEC104_Station_Is_Reusable(&command_station)); + CHECK(IEC104_Slave_Destroy(command_slave, NULL) == IEC104_TCP_OK); + command = NULL; + command_slave = NULL; + command_window = 32U; +} + +/* Input and expected responses are byte literals, independent of object encoders. */ +static void command_queue(const uint8_t *payload, size_t length) +{ + uint8_t frame[255] = {0x68}; + CHECK(length <= 249U); + frame[1] = (uint8_t)(length + 4U); + frame[2] = (uint8_t)(command_master_ns << 1); + frame[3] = (uint8_t)(command_master_ns >> 7); + ++command_master_ns; + memcpy(frame + 6U, payload, length); + push(frame, length + 6U); +} + +static void command_bytes(uint8_t bytes[10], unsigned type, unsigned cause, + unsigned oa, unsigned ca, unsigned ioa, unsigned value) +{ + bytes[0] = (uint8_t)type; bytes[1] = 1U; bytes[2] = (uint8_t)cause; bytes[3] = (uint8_t)oa; + bytes[4] = (uint8_t)ca; bytes[5] = (uint8_t)(ca >> 8); + bytes[6] = (uint8_t)ioa; bytes[7] = (uint8_t)(ioa >> 8); bytes[8] = (uint8_t)(ioa >> 16); + bytes[9] = (uint8_t)value; +} + +static void command_send(unsigned type, unsigned cause, unsigned oa, unsigned ca, unsigned ioa, unsigned value) +{ + uint8_t bytes[10]; + command_bytes(bytes, type, cause, oa, ca, ioa, value); + command_queue(bytes, sizeof(bytes)); + command_poll(); +} + +static unsigned command_collect(const uint8_t **frames, size_t *lengths) +{ + size_t offset = 0U; + unsigned count = 0U; + while (offset < fake.sent_bytes) { + const uint8_t *frame = fake.output + offset; + size_t length; + CHECK(fake.sent_bytes - offset >= 6U && frame[0] == 0x68U); + length = (size_t)frame[1] + 2U; + CHECK(length >= 6U && offset + length <= fake.sent_bytes); + if ((frame[2] & 1U) == 0U && frame[6] != 200U) { + CHECK(count < 64U); + frames[count] = frame + 6U; + lengths[count++] = length - 6U; + } + offset += length; + } + return count; +} + +static void command_reply_at(unsigned index, unsigned type, unsigned cause, + unsigned oa, unsigned ca, unsigned ioa, unsigned value) +{ + const uint8_t *frames[64]; + size_t lengths[64]; + uint8_t expected[10]; + CHECK(command_collect(frames, lengths) > index); + command_bytes(expected, type, cause, oa, ca, ioa, value); + CHECK(lengths[index] == sizeof(expected)); + CHECK(memcmp(frames[index], expected, sizeof(expected)) == 0); +} + +static unsigned command_frame_count(void) +{ + const uint8_t *frames[64]; size_t lengths[64]; + return command_collect(frames, lengths); +} + +static IEC104_CommandActionResult_t command_action(void *context, const IEC104_CommandRequest_t *request) +{ + CHECK(context == &command_action_calls); + ++command_action_calls; + command_action_request = *request; + return command_action_result; +} + +static IEC104_CommandEvent_t command_select(unsigned type, unsigned value) +{ + IEC104_CommandEvent_t event; + command_send(type, 6U, 0x51U, 1U, 6001U, value | 0x80U); + event = command_take(IEC104_COMMAND_SELECT); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_flush(); + return event; +} + +static IEC104_CommandEvent_t command_execute(unsigned type, unsigned value) +{ + IEC104_CommandEvent_t event; + command_send(type, 6U, 0x51U, 1U, 6001U, value); + event = command_take(IEC104_COMMAND_EXECUTE); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + return event; +} + +static void command_return(IEC60870_Asdu_t *object, uint8_t bytes[10], unsigned type, unsigned value) +{ + command_bytes(bytes, type, 11U, 0x51U, 1U, 1001U, value); + CHECK(IEC60870_Asdu_Create_From_Buffer(object, + IEC104_Slave_Get_Application_Layer_Parameters(command_slave), bytes, 10U) == IEC60870_ASDU_OK); +} + +static IEC104_CommandEvent_t command_done(const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result) +{ + IEC104_CommandEvent_t finished; + command_flush(); + CHECK(IEC104_Command_Report_Result(command, event, result) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Complete(command, event) == IEC104_COMMAND_REPLY_OK); + command_flush(); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == (result == IEC104_COMMAND_RESULT_SUCCEEDED ? + IEC104_COMMAND_FINISH_COMPLETED : IEC104_COMMAND_FINISH_EXECUTION_FAILED)); + command_empty(); + return finished; +} + +/* Fill the real transport buffers; the business layer never receives a fake WAIT. */ +static void command_congest(void) +{ + uint8_t bytes[249] = {200, 0, 3, 0, 1, 0}; + IEC60870_Asdu_t filler; + unsigned index; + fake.send_blocked = 1U; + CHECK(IEC60870_Asdu_Create_From_Buffer(&filler, + IEC104_Slave_Get_Application_Layer_Parameters(command_slave), bytes, sizeof(bytes)) == IEC60870_ASDU_OK); + for (index = 0U; index < 32U; ++index) { + if (!IEC60870_MasterConnection_Send_ASDU(&command_last_input.connection, &filler)) { + CHECK(IEC104_Slave_Get_Last_Submit_Result(&command_last_input.connection) == IEC104_TCP_SUBMIT_WAIT); + return; + } + } + CHECK(0); +} + +static void test_command_configuration(void) +{ + IEC104_CommandConfig_t config, invalid; + IEC104_CommandStatus_t status; + unsigned index; + IEC104_Command_Config_Default(&config); + CHECK(config.type_mask == 3U && config.select_required_mask == 3U); + CHECK(config.select_timeout_ms == 30000U && config.decision_timeout_ms == 15000U); + CHECK(config.execute_timeout_ms == 60000U && config.send_act_term && !config.act_term_on_failure); + CHECK(IEC104_COMMAND_TYPE_BIT(44U) == 0U && IEC104_COMMAND_TYPE_BIT(52U) == 0U); + CHECK(IEC104_COMMAND_TYPE_BIT(58U) == 128U && IEC104_COMMAND_TYPE_BIT(64U) == 8192U); + CHECK(IEC104_COMMAND_TYPE_BIT(65U) == 0U); + CHECK(IEC104_Command_Create(NULL, &config) == NULL); + CHECK(!IEC104_Command_Destroy(NULL)); + CHECK(!IEC104_Command_Get_Status(NULL, &status)); + command_open(NULL); + CHECK(IEC104_Command_Create(command_slave, &config) == NULL); + CHECK(IEC104_Command_Destroy(command)); + for (index = 0U; index < 5U; ++index) { + invalid = config; + switch (index) { + case 0U: invalid.type_mask = 0U; break; + case 1U: invalid.type_mask |= IEC104_COMMAND_TYPE_BIT(47U); break; + case 2U: invalid.select_timeout_ms = 0U; break; + case 3U: invalid.type_mask = 1U; invalid.select_required_mask = 2U; break; + default: invalid.select_required_mask = UINT16_MAX; break; + } + CHECK(IEC104_Command_Create(command_slave, &invalid) == NULL); + } + CHECK(IEC104_Command_Create(command_slave, NULL) == NULL); + command = IEC104_Command_Create(command_slave, &config); + CHECK(command != NULL); + command_close(); + ++command_checks; +} + +static void test_command_wire_validation(void) +{ + static const struct { unsigned type, cot, ca, ioa, value, cause; } cases[] = { + {45, 5, 65535, 0, 2, 45}, {45, 8, 65535, 0, 2, 46}, + {45, 6, 2, 0, 2, 46}, {45, 6, 1, 0, 2, 47}, + {45, 6, 1, 6001, 2, 7}, {46, 6, 1, 6001, 0, 7}, + {46, 6, 1, 6001, 3, 7}, {46, 8, 1, 6001, 3, 9}, + {45, 0x46, 1, 6001, 1, 7}, {45, 0x86, 1, 6001, 1, 0x87} + }; + unsigned index; + for (index = 0U; index < sizeof(cases) / sizeof(cases[0]); ++index) { + command_open(NULL); + command_send(cases[index].type, cases[index].cot, 0x61U, cases[index].ca, + cases[index].ioa, cases[index].value); + command_flush(); + CHECK(command_frame_count() == 1U); + command_reply_at(0U, cases[index].type, cases[index].cause | 0x40U, + 0x61U, cases[index].ca, cases[index].ioa, cases[index].value); + CHECK(command_status.rejected == 1U && command_action_calls == 0U); + command_empty(); + command_close(); + } + for (index = 0U; index < 5U; ++index) { + uint8_t bytes[14]; + size_t length = 10U; + command_open(NULL); + command_bytes(bytes, 45U, 6U, 0x62U, 1U, 6001U, 0x81U); + switch (index) { + case 0U: bytes[1] = 0U; break; + case 1U: bytes[1] = 2U; length = 14U; memset(bytes + 10U, 0, 4U); break; + case 2U: length = 9U; break; + case 3U: bytes[1] = 0x81U; break; + default: bytes[10] = 0x55U; length = 11U; break; + } + command_queue(bytes, length); command_poll(); command_flush(); + CHECK(command_frame_count() == (index < 3U ? 0U : 1U)); + if (index >= 3U) { command_reply_at(0U, 45U, 0x47U, 0x62U, 1U, 6001U, 0x81U); } + command_empty(); command_close(); + } + command_open(NULL); + command_send(47U, 6U, 0U, 1U, 6001U, 1U); + CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED); + { + uint8_t timed[17] = {0}; + command_bytes(timed, 58U, 6U, 0U, 1U, 6001U, 1U); + timed[14] = 1U; timed[15] = 1U; timed[16] = 26U; + command_queue(timed, sizeof(timed)); command_poll(); + } + CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED); + command_empty(); command_close(); + { + IEC104_CommandConfig_t config; + IEC104_Command_Config_Default(&config); + config.type_mask = config.select_required_mask = IEC104_COMMAND_TYPE_BIT(46U); + command_open(&config); + command_send(45U, 6U, 0U, 1U, 6001U, 0x81U); + CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED); + command_empty(); + command_send(46U, 6U, 0U, 1U, 6001U, 0x82U); + CHECK(command_take(IEC104_COMMAND_SELECT).request.type_id == 46U); + command_close(); + } + ++command_checks; +} + +static void test_command_complete_flows(void) +{ + unsigned type, count; + for (type = 45U; type <= 46U; ++type) { + for (count = 0U; count <= 2U; ++count) { + IEC104_CommandEvent_t selection, execution, finished; + IEC104_CommandTransactionInfo_t info; + IEC60870_Asdu_t data; + uint8_t bytes[10]; + unsigned index, value = type == 45U ? 1U : 2U; + command_open(NULL); + selection = command_select(type, value | 4U); + CHECK(selection.request.state == value && selection.request.qualifier == 1U); + CHECK(selection.request.is_select && !selection.request.selected); + execution = command_execute(type, value | 4U); + CHECK(execution.request.selected && !execution.request.is_select); + CHECK(execution.transaction_id == selection.transaction_id); + CHECK(execution.event_serial != selection.event_serial); + command_flush(); + CHECK(IEC104_Command_Get_Transaction(command, &selection, &info) && info.active); + CHECK(info.phase == IEC104_COMMAND_PHASE_EXECUTING && info.confirm_submitted); + CHECK(IEC104_Command_Report_Result(command, &selection, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + CHECK(IEC104_Command_Complete(command, &execution) == IEC104_COMMAND_REPLY_ERROR); + CHECK(IEC104_Command_Report_Result(command, &execution, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Report_Result(command, &execution, IEC104_COMMAND_RESULT_FAILED) == IEC104_COMMAND_REPLY_ERROR); + for (index = 0U; index < count; ++index) { + command_return(&data, bytes, type == 45U ? 1U : 3U, value); + CHECK(IEC104_Command_Submit_Return_Info(command, &execution, &data) == IEC104_COMMAND_REPLY_OK); + command_poll(); + } + CHECK(IEC104_Command_Complete(command, &execution) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Complete(command, &execution) == IEC104_COMMAND_REPLY_ERROR); + command_return(&data, bytes, 1U, 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &execution, &data) == IEC104_COMMAND_REPLY_ERROR); + command_flush(); + CHECK(command_frame_count() == count + 3U); + command_reply_at(0U, type, 7U, 0x51U, 1U, 6001U, value | 0x84U); + command_reply_at(1U, type, 7U, 0x51U, 1U, 6001U, value | 4U); + for (index = 0U; index < count; ++index) { + command_reply_at(index + 2U, type == 45U ? 1U : 3U, 11U, 0x51U, 1U, 1001U, value); + } + command_reply_at(count + 2U, type, 10U, 0x51U, 1U, 6001U, value | 4U); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_COMPLETED && finished.select_effect && finished.execute_effect); + CHECK(finished.request.receive_serial == execution.request.receive_serial); + CHECK(command_action_calls == 2U); + CHECK(!IEC104_Command_Get_Transaction(command, &execution, &info)); + CHECK(IEC104_Command_Complete(command, &execution) == IEC104_COMMAND_REPLY_STALE); + command_empty(); command_close(); + } + } + ++command_checks; +} + +static void test_command_result_policies(void) +{ + unsigned success, send_term, failure_term; + for (success = 0U; success < 2U; ++success) { + for (send_term = 0U; send_term < 2U; ++send_term) { + for (failure_term = 0U; failure_term < 2U; ++failure_term) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); + config.select_required_mask = 0U; + config.send_act_term = send_term != 0U; + config.act_term_on_failure = failure_term != 0U; + command_open(&config); + event = command_execute(45U, 1U); + CHECK(!event.request.selected); + finished = command_done(&event, success ? IEC104_COMMAND_RESULT_SUCCEEDED : IEC104_COMMAND_RESULT_FAILED); + CHECK(finished.execute_effect && !finished.select_effect); + CHECK(command_frame_count() == 1U + (send_term && (success || failure_term) ? 1U : 0U)); + command_reply_at(0U, 45U, 7U, 0x51U, 1U, 6001U, 1U); + if (send_term && (success || failure_term)) { command_reply_at(1U, 45U, 10U, 0x51U, 1U, 6001U, 1U); } + command_close(); + } + } + } + ++command_checks; +} + +static void test_command_action_outcomes(void) +{ + unsigned selected, outcome; + for (selected = 0U; selected < 2U; ++selected) { + for (outcome = 0U; outcome < 3U; ++outcome) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); + command_send(45U, 6U, 0x51U, 1U, 6001U, selected ? 0x81U : 1U); + event = command_take(selected ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE); + command_action_result = (IEC104_CommandActionResult_t)outcome; + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + CHECK(command_action_calls == 1U); + command_flush(); + command_reply_at(0U, 45U, outcome == IEC104_COMMAND_ACTION_STARTED ? 7U : 0x47U, + 0x51U, 1U, 6001U, selected ? 0x81U : 1U); + if (outcome == IEC104_COMMAND_ACTION_STARTED) { + IEC104_Command_Cancel(command); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_CANCELLED); + } else { + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == (outcome == IEC104_COMMAND_ACTION_FAILED ? + IEC104_COMMAND_FINISH_ACTION_FAILED : IEC104_COMMAND_FINISH_REJECTED)); + } + CHECK(finished.select_effect == (selected && outcome != IEC104_COMMAND_ACTION_NOT_STARTED)); + CHECK(finished.execute_effect == (!selected && outcome != IEC104_COMMAND_ACTION_NOT_STARTED)); + command_close(); + } + } + { + IEC104_CommandEvent_t event, finished; + command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); + CHECK(IEC104_Command_Accept(command, &event, NULL, NULL) == IEC104_COMMAND_REPLY_OK); + command_flush(); IEC104_Command_Cancel(command); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(!finished.select_effect && command_action_calls == 0U); + command_close(); + } + ++command_checks; +} + +static void test_command_rejection_and_stale(void) +{ + IEC104_CommandEvent_t event, stale, finished; + IEC104_CommandTransactionInfo_t info, before; + unsigned index; + command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); + for (index = 0U; index < 4U; ++index) { + stale = event; + switch (index) { + case 0U: ++stale.instance_id; break; + case 1U: ++stale.transaction_id; break; + case 2U: ++stale.event_serial; break; + default: stale.type = IEC104_COMMAND_EXECUTE; break; + } + CHECK(IEC104_Command_Accept(command, &stale, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + } + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + CHECK(IEC104_Command_Reject(command, &event, (IEC104_CommandRejectCause_t)0) == IEC104_COMMAND_REPLY_ERROR); + CHECK(IEC104_Command_Reject(command, &event, IEC104_COMMAND_REJECT_UNKNOWN_IOA) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Reject(command, &event, IEC104_COMMAND_REJECT_NEGATIVE) == IEC104_COMMAND_REPLY_STALE); + command_flush(); command_reply_at(0U, 45U, 0x6fU, 0x51U, 1U, 6001U, 0x81U); + CHECK(IEC104_Command_Get_Transaction(command, &event, &info) && !info.active); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_REJECTED && command_action_calls == 0U); + memset(&info, 0x5a, sizeof(info)); before = info; + CHECK(!IEC104_Command_Get_Transaction(command, &event, &info)); + CHECK(memcmp(&before, &info, sizeof(info)) == 0); + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + command_close(); + ++command_checks; +} + +static void test_command_matching_and_busy(void) +{ + unsigned change; + for (change = 0U; change < 4U; ++change) { + IEC104_CommandEvent_t selected, finished; + unsigned type = change == 0U ? 46U : 45U; + unsigned value = change == 0U ? 2U : (change == 1U ? 0U : (change == 2U ? 5U : 1U)); + command_open(NULL); selected = command_select(45U, 1U); + command_send(type, 6U, change == 3U ? 0x52U : 0x51U, 1U, 6001U, value); + command_flush(); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_MISMATCH); + CHECK(finished.request.receive_serial == selected.request.receive_serial && finished.request.type_id == 45U); + command_reply_at(1U, type, 0x47U, change == 3U ? 0x52U : 0x51U, 1U, 6001U, value); + CHECK(command_action_calls == 1U); command_close(); + } + { + IEC104_CommandEvent_t event; + command_open(NULL); (void)command_select(45U, 1U); + command_send(45U, 6U, 0x51U, 1U, 6002U, 0x81U); command_flush(); + CHECK(command_status.busy && command_status.busy_rejected == 1U); + command_reply_at(1U, 45U, 0x47U, 0x51U, 1U, 6002U, 0x81U); + event = command_execute(45U, 1U); (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED); + command_close(); + } + command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); command_flush(); + CHECK(!command_status.busy); command_reply_at(0U, 45U, 0x47U, 0x51U, 1U, 6001U, 1U); + command_empty(); command_close(); + ++command_checks; +} + +static void test_command_finished_pairing(void) +{ + unsigned mode; + for (mode = 0U; mode < 4U; ++mode) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); + if (mode >= 2U) { + event = command_select(45U, 1U); + command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); + } else { + command_send(45U, 6U, 0x51U, 1U, 6001U, mode == 0U ? 0x81U : 1U); + } + if (mode == 3U) { event = command_take(IEC104_COMMAND_EXECUTE); } + command_send(45U, 8U, 0x51U, 1U, 6001U, 0U); command_flush(); + if (mode < 2U) { command_empty(); } + else { + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_DEACTIVATED); + CHECK(finished.transaction_id == event.transaction_id && finished.select_effect); + command_empty(); + } + command_reply_at(mode >= 2U ? 1U : 0U, 45U, 9U, 0x51U, 1U, 6001U, 0U); + command_close(); + } + ++command_checks; +} + +static void test_command_unsubmitted_selection(void) +{ + IEC104_CommandEvent_t selected, executed; + IEC104_CommandTransactionInfo_t info; + command_open(NULL); + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + selected = command_take(IEC104_COMMAND_SELECT); command_congest(); + CHECK(IEC104_Command_Accept(command, &selected, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_process(); + CHECK(IEC104_Command_Get_Transaction(command, &selected, &info) && !info.confirm_submitted); + /* Confirmation not handed over yet: even a mismatching execution is merely busy. */ + command_send(46U, 6U, 0x51U, 1U, 6001U, 2U); + CHECK(IEC104_Command_Get_Transaction(command, &selected, &info) && info.active); + CHECK(info.phase == IEC104_COMMAND_PHASE_SELECTED); + fake.send_blocked = 0U; command_flush(); + CHECK(command_status.mismatches == 0U && command_status.busy_rejected == 1U); + command_reply_at(0U, 45U, 7U, 0x51U, 1U, 6001U, 0x81U); + command_reply_at(1U, 46U, 0x47U, 0x51U, 1U, 6001U, 2U); + executed = command_execute(45U, 1U); + (void)command_done(&executed, IEC104_COMMAND_RESULT_SUCCEEDED); + CHECK(command_action_calls == 2U); + command_close(); ++command_checks; +} + +static void test_command_deactivation_wait(void) +{ + IEC104_CommandEvent_t event, finished; + command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); command_congest(); + command_send(45U, 8U, 0x51U, 1U, 6001U, 0U); command_process(); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_DEACTIVATED); + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(command_status.busy && command_status.pending_replies != 0U); + CHECK(!IEC104_Command_Destroy(command)); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + command_send(45U, 6U, 0x52U, 1U, 6002U, 0x81U); + fake.send_blocked = 0U; command_flush(); + CHECK(!command_status.busy); + command_reply_at(0U, 45U, 9U, 0x51U, 1U, 6001U, 0U); + command_reply_at(1U, 45U, 0x47U, 0x52U, 1U, 6002U, 0x81U); + command_empty(); command_close(); + ++command_checks; +} + +static void test_command_deadlines(void) +{ + unsigned phase, boundary; + for (phase = 0U; phase < 3U; ++phase) { + for (boundary = 0U; boundary < 3U; ++boundary) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + uint64_t started; + IEC104_Command_Config_Default(&config); + config.decision_timeout_ms = config.select_timeout_ms = config.execute_timeout_ms = 10U; + command_open(&config); + if (phase == 0U) { + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); + } else { + event = command_select(45U, 1U); + if (phase == 2U) { event = command_execute(45U, 1U); command_flush(); } + } + started = fake.now; + if (phase == 1U) { + uint8_t bytes[10]; + command_bytes(bytes, 45U, 6U, 0x51U, 1U, 6001U, 1U); + command_queue(bytes, sizeof(bytes)); + CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK); + } + /* Receive at a valid transport time, then test business acceptance time. */ + fake.now = boundary == 2U ? started - 1U : started + 9U + boundary; + if (phase == 0U) { + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == + (boundary == 0U ? IEC104_COMMAND_REPLY_OK : IEC104_COMMAND_REPLY_STALE)); + } else if (phase == 1U) { + command_drain_transport(); + } else { + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == + (boundary == 0U ? IEC104_COMMAND_REPLY_OK : IEC104_COMMAND_REPLY_STALE)); + } + if (boundary != 0U) { + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == (phase == 0U ? IEC104_COMMAND_FINISH_DECISION_TIMEOUT : + (phase == 1U ? IEC104_COMMAND_FINISH_SELECT_TIMEOUT : IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT))); + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(command_status.clock_errors == (boundary == 2U ? 1U : 0U)); + } else if (phase == 1U) { + event = command_take(IEC104_COMMAND_EXECUTE); CHECK(event.request.selected); + } + fake.now = started + 10U; + command_close(); + } + } + /* Disabled duration limits still reject a reversed monotonic time source. */ + for (phase = 0U; phase < 2U; ++phase) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); + config.select_required_mask = 0U; + config.decision_timeout_ms = config.execute_timeout_ms = 0U; + command_open(&config); + command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); + event = command_take(IEC104_COMMAND_EXECUTE); + if (phase != 0U) { + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_flush(); + } + --fake.now; + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(!command_status.timer_active && command_status.need_process); + if (phase == 0U) { + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + CHECK(command_action_calls == 0U); + } else { + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + } + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == (phase == 0U ? IEC104_COMMAND_FINISH_DECISION_TIMEOUT : IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT)); + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(command_status.clock_errors == 1U); + ++fake.now; + command_close(); + } + /* A previously delivered SELECT still owes FINISHED when unread EXECUTE expires. */ + { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event; + IEC104_Command_Config_Default(&config); config.decision_timeout_ms = 10U; + command_open(&config); (void)command_select(45U, 1U); + command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); fake.now += 10U; + event = command_take(IEC104_COMMAND_FINISHED); + CHECK(event.reason == IEC104_COMMAND_FINISH_DECISION_TIMEOUT); + command_empty(); command_close(); + } + ++command_checks; +} + +static void test_command_result_before_confirm(void) +{ + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_CommandTransactionInfo_t info; + IEC60870_Asdu_t data; + uint8_t bytes[10]; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; config.execute_timeout_ms = 10U; + command_open(&config); command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); + event = command_take(IEC104_COMMAND_EXECUTE); command_congest(); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_process(); + CHECK(IEC104_Command_Get_Transaction(command, &event, &info) && !info.confirm_submitted); + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_OK); + command_return(&data, bytes, 1U, 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_WAIT); + fake.now += 100U; command_process(); + CHECK(command_status.execute_timeouts == 0U && command_action_calls == 1U); + fake.send_blocked = 0U; command_flush(); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Complete(command, &event) == IEC104_COMMAND_REPLY_OK); + command_flush(); finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_COMPLETED && command_frame_count() == 3U); + command_reply_at(0U, 45U, 7U, 0x51U, 1U, 6001U, 1U); + command_reply_at(1U, 1U, 11U, 0x51U, 1U, 1001U, 1U); + command_reply_at(2U, 45U, 10U, 0x51U, 1U, 6001U, 1U); + command_close(); ++command_checks; +} + +static void test_command_fifo_and_overload(void) +{ + IEC104_CommandEvent_t event; + IEC60870_Asdu_t data; + uint8_t bytes[10]; + unsigned index; + command_open(NULL); (void)command_select(45U, 1U); event = command_execute(45U, 1U); command_flush(); + command_congest(); + for (index = 0U; index < IEC104_COMMAND_REPLY_CAPACITY + 2U; ++index) { + command_send(45U, 6U, 0x60U + index, 1U, 6002U, 0x81U); + } + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(command_status.pending_replies == IEC104_COMMAND_REPLY_CAPACITY); + CHECK(command_status.overloads == 2U && command_status.dropped_requests == 2U); + fake.send_blocked = 0U; + /* Drain only TCP: pending Command replies must still prevent direct Try insertion. */ + for (index = 0U; index < 16U; ++index) { command_poll(); } + command_return(&data, bytes, 1U, 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_WAIT); + command_flush(); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_OK); + command_poll(); (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED); + for (index = 0U; index < IEC104_COMMAND_REPLY_CAPACITY; ++index) { + command_reply_at(index + 2U, 45U, 0x47U, 0x60U + index, 1U, 6002U, 0x81U); + } + command_reply_at(IEC104_COMMAND_REPLY_CAPACITY + 2U, 1U, 11U, 0x51U, 1U, 1001U, 1U); + command_close(); ++command_checks; +} + +static void test_command_return_validation(void) +{ + IEC104_CommandEvent_t event; + IEC60870_Asdu_t object; + uint8_t bytes[11]; + unsigned index; + command_open(NULL); (void)command_select(45U, 1U); event = command_execute(45U, 1U); command_flush(); + for (index = 0U; index < 7U; ++index) { + size_t length = 10U; + command_bytes(bytes, 1U, 11U, 0x51U, 1U, 1001U, 1U); + switch (index) { + case 0U: bytes[0] = 45U; break; + case 1U: bytes[2] = 5U; break; + case 2U: bytes[2] |= 0x40U; break; + case 3U: bytes[2] |= 0x80U; break; + case 4U: bytes[4] = 2U; break; + case 5U: bytes[9] = 2U; break; + default: bytes[10] = 1U; length = 11U; break; + } + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, + IEC104_Slave_Get_Application_Layer_Parameters(command_slave), bytes, length) == IEC60870_ASDU_OK); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &object) == IEC104_COMMAND_REPLY_ERROR); + } + command_bytes(bytes, 1U, 3U, 0x51U, 1U, 9876U, 0U); + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, + IEC104_Slave_Get_Application_Layer_Parameters(command_slave), bytes, 10U) == IEC60870_ASDU_OK); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &object) == IEC104_COMMAND_REPLY_OK); + command_poll(); (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED); + command_reply_at(2U, 1U, 3U, 0x51U, 1U, 9876U, 0U); + command_close(); ++command_checks; +} + +static void test_command_stop_notifications(void) +{ + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); config.decision_timeout_ms = 10U; + command_open(&config); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); + fake.now += 10U; + push(stop_act, sizeof(stop_act)); push(start_act, sizeof(start_act)); + CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_WAIT); + command_process(); CHECK(command_status.events_pending && command_status.decision_timeouts == 0U); + command_drain_transport(); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_STOPPED && command_action_calls == 0U); + command_empty(); CHECK(IEC104_Command_Get_Status(command, &command_status) && command_status.active); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + command_close(); + ++command_checks; +} + +static void test_command_disconnect_and_reuse(void) +{ + IEC104_CommandEvent_t event, next, finished; + IEC104_CommandConfig_t config; + uint64_t generation; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); event = command_execute(45U, 1U); command_flush(); + generation = event.request.generation; + fake.recv_eof = 1U; command_poll(); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_DISCONNECTED && finished.execute_effect); + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + command_empty(); fake.recv_eof = 0U; command_connect(); + next = command_execute(45U, 1U); + CHECK(next.request.generation != generation && next.transaction_id != event.transaction_id); + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + (void)command_done(&next, IEC104_COMMAND_RESULT_SUCCEEDED); command_close(); + ++command_checks; +} + +static void test_command_station_lifetime(void) +{ + IEC104_CommandEvent_t event, finished; + IEC104_Station_t replacement = {0}; + uint64_t original; + command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); original = event.request.station_id; + CHECK(IEC104_Slave_Acquire_Station(command_slave, &command_station) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(command_slave, &command_station) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&command_station)); + CHECK(IEC104_Slave_Add_Station(command_slave, &replacement, 1U, &replacement) == IEC104_STATION_OK); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_flush(); CHECK(command_action_request.station_id == original); + command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); command_flush(); + CHECK(command_action_calls == 1U && command_status.busy); + command_reply_at(1U, 45U, 0x47U, 0x51U, 1U, 6001U, 1U); + IEC104_Command_Cancel(command); finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_CANCELLED && finished.request.station_id == original); + CHECK(IEC104_Slave_Get_Station_By_Id(command_slave, original) == &command_station); + CHECK(IEC104_Slave_Release_Station(command_slave, &command_station) == IEC104_STATION_OK); + CHECK(IEC104_Station_Is_Reusable(&command_station)); + CHECK(IEC104_Slave_Remove_Station(command_slave, &replacement) == IEC104_STATION_OK); + command_close(); ++command_checks; +} + +static IEC104_CommandEvent_t command_reentry_event; +static IEC104_CommandActionResult_t command_reentry_action(void *context, const IEC104_CommandRequest_t *request) +{ + IEC104_CommandTransactionInfo_t info; + IEC104_CommandEvent_t ignored; + IEC104_CommandStatus_t status; + CHECK(IEC104_Command_Get_Status(command, &status)); + CHECK(IEC104_Command_Get_Transaction(command, &command_reentry_event, &info)); + CHECK(!IEC104_Command_Destroy(command)); + CHECK(!IEC104_Command_Process(command)); + CHECK(IEC104_Command_NextEvent(command, &ignored) == IEC104_COMMAND_NEXT_ERROR); + CHECK(IEC104_Command_Accept(command, &command_reentry_event, command_action, context) == IEC104_COMMAND_REPLY_ERROR); + CHECK(IEC104_Command_Reject(command, &command_reentry_event, IEC104_COMMAND_REJECT_NEGATIVE) == IEC104_COMMAND_REPLY_ERROR); + CHECK(IEC104_Command_Handle_Event(command, &command_last_input) == IEC104_COMMAND_EVENT_ERROR); + IEC104_Command_Cancel(command); + return command_action(context, request); +} + +static void test_command_reentry(void) +{ + IEC104_CommandConfig_t config; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); + command_reentry_event = command_take(IEC104_COMMAND_EXECUTE); + CHECK(IEC104_Command_Accept(command, &command_reentry_event, NULL, NULL) == IEC104_COMMAND_REPLY_ERROR); + CHECK(IEC104_Command_Accept(command, &command_reentry_event, command_reentry_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + CHECK(command_action_calls == 1U); + (void)command_done(&command_reentry_event, IEC104_COMMAND_RESULT_SUCCEEDED); + command_close(); ++command_checks; +} + +static void test_command_window_and_cancel(void) +{ + IEC104_CommandEvent_t event; + IEC60870_Asdu_t data; + uint8_t bytes[10]; + command_window = 1U; command_open(NULL); (void)command_select(45U, 1U); + event = command_execute(45U, 1U); command_process(); + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_OK); + command_return(&data, bytes, 1U, 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_WAIT); + push_ack(1U); command_poll(); command_flush(); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_WAIT); + push_ack(2U); command_poll(); command_flush(); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Complete(command, &event) == IEC104_COMMAND_REPLY_OK); + command_flush(); CHECK(command_frame_count() == 3U); + push_ack(3U); command_poll(); command_flush(); + CHECK(command_take(IEC104_COMMAND_FINISHED).reason == IEC104_COMMAND_FINISH_COMPLETED); + CHECK(command_frame_count() == 4U); command_close(); + ++command_checks; +} + +static void test_command_two_services(void) +{ +#if IEC104_TCP_SERVER_CAPACITY >= 2 + IEC104_Slave_t *other; + IEC104_Command_t *other_command; + IEC104_CommandConfig_t config; + IEC104_SlaveConfig_t service_config; + IEC104_CommandEvent_t event; + command_open(NULL); + other = IEC104_Slave_Create(); CHECK(other != NULL); + IEC104_Slave_Config_Default(&service_config); + CHECK(IEC104_Slave_Init(other, &service_config, NULL) == IEC104_TCP_OK); + IEC104_Command_Config_Default(&config); + other_command = IEC104_Command_Create(other, &config); CHECK(other_command != NULL); + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); + CHECK(IEC104_Command_Handle_Event(other_command, &command_last_input) == IEC104_COMMAND_EVENT_ERROR); + CHECK(IEC104_Command_Accept(other_command, &event, command_action, &command_action_calls) != IEC104_COMMAND_REPLY_OK); + CHECK(command_action_calls == 0U); + CHECK(IEC104_Command_Destroy(other_command)); + CHECK(IEC104_Slave_Close(other, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Destroy(other, NULL) == IEC104_TCP_OK); + command_close(); +#endif + ++command_checks; +} + +static void test_command_normal_overwrite(void) +{ + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event; + IEC60870_Asdu_t object, filler; + uint8_t bytes[10], fill_bytes[249] = {200, 0, 3, 0, 1, 0}; + unsigned index; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); event = command_execute(45U, 1U); command_flush(); + command_return(&object, bytes, 1U, 0U); + CHECK(IEC104_Slave_Enqueue_Event(command_slave, &object) == IEC104_TCP_SUBMIT_OK); + CHECK(IEC60870_Asdu_Create_From_Buffer(&filler, + IEC104_Slave_Get_Application_Layer_Parameters(command_slave), fill_bytes, sizeof(fill_bytes)) == IEC60870_ASDU_OK); + for (index = 0U; index < IEC104_TCP_NORMAL_CAPACITY / sizeof(fill_bytes) + 4U; ++index) { + CHECK(IEC104_Slave_Enqueue_Event(command_slave, &filler) == IEC104_TCP_SUBMIT_OK); + } + command_return(&object, bytes, 1U, 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &object) == IEC104_COMMAND_REPLY_WAIT); + command_flush(); + /* The first NORMAL return (value=0) was overwritten; only ACT_CON remains. */ + CHECK(command_frame_count() == 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &object) == IEC104_COMMAND_REPLY_OK); + command_poll(); (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED); + CHECK(command_frame_count() == 3U); + command_reply_at(1U, 1U, 11U, 0x51U, 1U, 1001U, 1U); + command_close(); ++command_checks; +} + +static int command_hold_transfer(void *context) { (void)context; return 0; } + +static void command_retain_stop(void) +{ + IEC104_ConnectionTransferObserver_t observer = sut->connection.transfer_observer; + IEC104_TcpServerStatus_t status; + sut->connection.transfer_observer.can_accept = command_hold_transfer; + push(stop_act, sizeof(stop_act)); + CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK); + sut->connection.transfer_observer = observer; + CHECK(sut->rx_blocked && !sut->connection.receive_batch_pending); + CHECK(IEC104_Slave_Get_Status(command_slave, &status, NULL) == IEC104_TCP_OK); + CHECK(status.pending_event_count == 0U && status.connection.data_transfer_started); +} + +static int command_fail_after_stop(void *context, IEC104_DataTransferState_t state) +{ + CHECK(Notify_Transfer_State(context, state)); + return 0; +} + +static void test_command_stop_during_submit(void) +{ + unsigned fatal; + for (fatal = 0U; fatal < 2U; ++fatal) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); + event = command_take(IEC104_COMMAND_EXECUTE); command_congest(); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_process(); + command_send(45U, 6U, 0x62U, 1U, 6002U, 0x81U); + CHECK(IEC104_Command_Get_Status(command, &command_status) && command_status.pending_replies == 2U); + command_retain_stop(); fake.send_blocked = 0U; + if (fatal) { sut->connection.transfer_observer.notify = command_fail_after_stop; } + command_process(); + if (fatal) { + /* FATAL must not be mistaken for benign STOP even when a STOP event exists. */ + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_SEND_ERROR); + sut->connection.transfer_observer.notify = Notify_Transfer_State; + command_drain_transport(); + } else { + CHECK(command_status.events_pending && command_status.busy && !command_status.need_process); + command_drain_transport(); finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_STOPPED); + } + CHECK(finished.execute_effect && command_action_calls == 1U); + CHECK(IEC104_Command_Get_Status(command, &command_status) && command_status.pending_replies == 0U); + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + command_empty(); command_close(); + } + ++command_checks; +} + +static void test_command_independent_reply_lifetime(void) +{ + command_open(NULL); + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + command_congest(); + /* Cancel the unread transaction, then leave only an independent error response. */ + IEC104_Command_Cancel(command); + command_send(45U, 6U, 0x61U, 2U, 6001U, 0x81U); + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(!command_status.busy && command_status.pending_replies == 1U); + CHECK(!IEC104_Command_Destroy(command)); + IEC104_Command_Cancel(command); + CHECK(IEC104_Command_Get_Status(command, &command_status) && command_status.pending_replies == 0U); + command_empty(); command_close(); ++command_checks; +} + +int main(void) +{ + test_command_configuration(); + test_command_wire_validation(); + test_command_complete_flows(); + test_command_result_policies(); + test_command_action_outcomes(); + test_command_rejection_and_stale(); + test_command_matching_and_busy(); + test_command_finished_pairing(); + test_command_unsubmitted_selection(); + test_command_deactivation_wait(); + test_command_deadlines(); + test_command_result_before_confirm(); + test_command_fifo_and_overload(); + test_command_return_validation(); + test_command_stop_notifications(); + test_command_disconnect_and_reuse(); + test_command_station_lifetime(); + test_command_reentry(); + test_command_window_and_cancel(); + test_command_two_services(); + test_command_normal_overwrite(); + test_command_stop_during_submit(); + test_command_independent_reply_lifetime(); + printf("Command tests passed: groups=%u tcp=%u replies=%u request=%zu event=%zu status=%zu\n", + command_checks, (unsigned)IEC104_TCP_SERVER_CAPACITY, (unsigned)IEC104_COMMAND_REPLY_CAPACITY, + sizeof(IEC104_CommandRequest_t), sizeof(IEC104_CommandEvent_t), sizeof(IEC104_CommandStatus_t)); + return 0; +} diff --git a/IEC60870-Slave/.build/command-comments/iec104_slave_example.c.before b/IEC60870-Slave/.build/command-comments/iec104_slave_example.c.before new file mode 100644 index 00000000..b1b49685 --- /dev/null +++ b/IEC60870-Slave/.build/command-comments/iec104_slave_example.c.before @@ -0,0 +1,974 @@ +/* + * 104子站示例:总召、计数量召唤、时钟同步、45/46遥控、模拟点表及main。 + * 两种召唤共用IEC104_Interrogation.c状态机,各自有独立实例和业务事件。 + * 每轮Slave_Poll -> NextEvent逐条交付 -> Interrogation_Process;所有调用串行。 + * 总召每批4点,模块自动推进至需要等待;8个遥信和8个遥测组成6个响应ASDU。 + * QOI=21只读遥信,22只读遥测,其余分组由应用拒绝;分组点表策略不写进协议库。 + * 同一连接至多一个未终结总召,忙时不区分QOI均否定;任务满时拒绝/计数丢弃新请求,保留当前连接。 + * 总召模块处理WAIT、STOPDT及断线;应用读取业务事件,选择类型并显式交付ASDU。 + * 计数量使用8个模拟计数器;读取快照,冻结/复位由经过事件校验的同步动作入口执行。 + * 对时默认只更新模拟UTC基准;--clock system显式选择实际时钟,协议仍使用单调时钟。 + * 不支持的其他命令当场提交COT=44否定应答,复用发送层HIGH;容量不足只丢本条并计数。 + * 默认127.0.0.1:2404、CA=1;构建命令见同目录README.md。 + */ +#include "IEC104_Interrogation.h" +#include "IEC104_CounterInterrogation.h" +#include "IEC104_ClockSynchronization.h" +#include "IEC104_Command.h" +#include "IEC60870_Clock.h" +#include +#include +#include + +#define EXAMPLE_EVENT_BUDGET 32U +#define EXAMPLE_COUNTER_COUNT 8U + +/* 模拟遥信点表:ioa为24位信息对象地址,value为0/1,quality为SIQ品质位(这里均为0)。 + * 表项不是线上报文,后续由SinglePointInformation_Create及ASDU容器编码成Type=1。 + * 此表提供地址、初值和品质;运行值保存在各ExampleApp中,遥控与总召共用同一数据源。 */ +static const struct { uint32_t ioa; uint8_t value, quality; } example_signals[] = { + {1001, 0, 0}, {1002, 1, 0}, {1003, 0, 0}, {1004, 1, 0}, + {1005, 1, 0}, {1006, 0, 0}, {1007, 1, 0}, {1008, 0, 0} +}; +/* 模拟遥测点表:ioa为信息对象地址,value为原始有符号16位标度值,quality为QDS品质位。 + * 编码为不带时标的Type=11;本示例不定义物理单位和倍率,负数也按有符号值传输。 */ +static const struct { uint32_t ioa; int16_t value; uint8_t quality; } example_measurements[] = { + {2001, 2300, 0}, {2002, 2310, 0}, {2003, 2290, 0}, {2004, 5000, 0}, + {2005, -125, 0}, {2006, 0, 0}, {2007, 1234, 0}, {2008, 42, 0} +}; +/* 点数直接取数组长度;分批游标使用size_t,不另维护可能与点表不一致的手写数量。 */ +#define EXAMPLE_SIGNAL_COUNT (sizeof(example_signals) / sizeof(example_signals[0])) +#define EXAMPLE_MEASUREMENT_COUNT (sizeof(example_measurements) / sizeof(example_measurements[0])) + + +/* 当前取数站的应用游标;首个NEED_DATA才初始化,协议库不登记或冻结业务IOA列表。 */ +typedef struct { size_t single_index, scaled_index; } ExampleGiData; + +typedef struct { + IEC104_Slave_t *slave; /* main拥有;本上下文仅借用。 */ + IEC104_Station_t station; /* 应用拥有的固定地址站点节点,供各业务共用CA和context。 */ + IEC104_Interrogation_t *interrogation; /* 库内不透明总召任务对象。 */ + IEC104_InterrogationStatus_t gi; /* 只读快照,用于调度等待及统计。 */ + IEC104_Interrogation_t *counter; /* 独立计数量实例,与总召共用Slave,不共用任务和广播状态区。 */ + IEC104_InterrogationStatus_t ci; /* 计数量调度及统计快照,每次推进后重新查询。 */ + IEC104_ClockSynchronization_t *clock; /* 独立时钟同步模块,共用Slave及站点表。 */ + IEC104_ClockSynchronizationStatus_t cs; /* 对时执行、确认及期限的只读快照。 */ + IEC104_Command_t *command; /* CLI启用;仅总召的旧测试可以不创建遥控实例。 */ + IEC104_CommandStatus_t command_status; + IEC104_CommandEvent_t command_event; /* 工作事件可跨WAIT保留,执行收尾继续使用原EXECUTE身份。 */ + IEC104_Station_t *command_station; /* 首个工作事件即Acquire,FINISHED清理后Release。 */ + uint64_t command_instance, command_transaction; + bool command_pending, command_waiting, command_selected, command_output; + unsigned command_step; /* 0等待事件/终结,1报告设备结果,2返回信息,3关闭返回阶段。 */ + IEC60870_Asdu_t command_return; + uint8_t command_return_bytes[10]; /* 单个Type1/3对象,WAIT保持原文,不借用总召缓冲。 */ + uint8_t signal_values[EXAMPLE_SIGNAL_COUNT], double_values[EXAMPLE_SIGNAL_COUNT]; + bool clock_system, clock_valid; /* 实际设时为显式选项;clock_valid只描述模拟基准。 */ + uint64_t clock_utc_ms, clock_base_ms; /* 模拟UTC毫秒及其单调基准,不影响协议计时。 */ + IEC104_InterrogationEvent_t counter_event; /* WAIT时保留的原业务事件,重试不能换成新身份。 */ + bool counter_pending, counter_first; /* 是否持有待应答事件、本轮是否先推进计数量。 */ + int32_t counter_current[EXAMPLE_COUNTER_COUNT]; /* 模拟当前累计值,冻结及复位动作的数据源。 */ + IEC60870_BinaryCounterReading_t counter_frozen[EXAMPLE_COUNTER_COUNT]; /* 最近冻结值及BCR序号、品质。 */ + IEC60870_BinaryCounterReading_t counter_snapshot[EXAMPLE_COUNTER_COUNT]; /* 本轮读取固定的冻结副本。 */ + size_t counter_cursor, counter_end; /* 当前读取区间,只有Submit_Batch成功才推进游标。 */ + uint64_t counter_instance, counter_request; /* 快照归属;旧FINISHED不得清理新请求。 */ + IEC104_Station_t *counter_station; /* 读取期间额外持有的原站节点,结束后归还引用。 */ + unsigned long counter_completed; /* 已消费的COMPLETED通知数,包含读取及冻结/复位。 */ + ExampleGiData gi_data; /* 应用拥有的游标,库内没有session或回调参数。 */ + IEC104_Station_t *gi_station; /* 取数期间持有原站,Complete_Data或FINISHED时释放。 */ + uint64_t session_instance, session_request; /* 只清理对应请求的遍历状态及业务资源。 */ + IEC104_InterrogationEvent_t business_event; /* 纯值,可保存到后续循环应答。 */ + bool business_pending; + int batch_result; /* 本例构造结果:-1失败、0数据结束、1有数据。 */ + IEC60870_Asdu_t batch; /* 借用本应用bytes;Submit_Batch(OK)后可复用。 */ + uint8_t batch_bytes[IEC104_MAX_ASDU_LEN]; + bool events_pending; + unsigned long other_submitted, other_rejected, other_cancelled, other_overloads; +} ExampleApp; + +/* 普通应用函数,只由下面的事件分支直接调用,不注册到库中。 + * 应用自行选择本批IOA、类型及分组成员;为NEED_DATA构造一种类型的一批。 + * 真实取值应放在下列逐点组包处,值与品质的读取一致性由数据源保证;不预读整轮数据。 + * FULL不跳过当前点;本函数不推进持久游标,Submit_Batch(OK)后才推进。 + * WAIT仅保留事件,每次重试重读当前值。点值更新/突发发布与本次组包/提交须串行协调。 + * 逐批取值不保证整轮同一采样时刻。 */ +static int Example_Build_Batch(ExampleApp *app, const IEC104_InterrogationRequest_t *request) +{ + ExampleGiData *data = &app->gi_data; + IEC60870_Asdu_t *asdu = &app->batch; + IEC60870_AsduHeader_t header; + IEC104_Station_t *station = request->station_id != 0U ? + IEC104_Slave_Get_Station_By_Id(app->slave, request->station_id) : + IEC104_Slave_Get_Station(app->slave, request->common_address); + if (IEC104_Station_Get_Context(station) != app) { + return -1; + } + memset(&header, 0, sizeof(header)); + header.cause_of_transmission = request->qualifier; /* 站总召20,分组总召21..36。 */ + header.common_address = request->common_address; + header.originator_address = request->originator_address; + header.is_test = request->is_test; + if (IEC60870_Asdu_Create(asdu, IEC104_Slave_Get_Application_Layer_Parameters(app->slave), + app->batch_bytes, sizeof(app->batch_bytes), &header, 0) != IEC60870_ASDU_OK) { return -1; } + if (data->single_index < EXAMPLE_SIGNAL_COUNT) { + size_t index = data->single_index; + while (index < EXAMPLE_SIGNAL_COUNT && asdu->object_count < request->max_objects_per_asdu) { + IEC60870_SinglePointInformation_t point; + IEC60870_AsduResult_t added; + if (IEC60870_SinglePointInformation_Create(&point, example_signals[index].ioa, + app->signal_values[index], example_signals[index].quality) != IEC60870_ASDU_OK) { + return -1; + } + added = IEC60870_Asdu_Add_InformationObject(asdu, &point.object); + if (added == IEC60870_ASDU_FULL) { break; } + if (added != IEC60870_ASDU_OK) { return -1; } + ++index; + } + } else if (data->scaled_index < EXAMPLE_MEASUREMENT_COUNT) { + size_t index = data->scaled_index; + while (index < EXAMPLE_MEASUREMENT_COUNT && asdu->object_count < request->max_objects_per_asdu) { + IEC60870_MeasuredValueScaled_t point; + IEC60870_AsduResult_t added; + if (IEC60870_MeasuredValueScaled_Create(&point, example_measurements[index].ioa, + example_measurements[index].value, example_measurements[index].quality) != IEC60870_ASDU_OK) { + return -1; + } + added = IEC60870_Asdu_Add_InformationObject(asdu, &point.object); + if (added == IEC60870_ASDU_FULL) { break; } + if (added != IEC60870_ASDU_OK) { return -1; } + ++index; + } + } else { return 0; } + return asdu->object_count != 0U ? 1 : -1; +} + +/* 释放应用为当前取数持有的节点引用并清空游标,库的广播状态区仍独立保存协议进度。 + * Complete_Data成功即可调用,让下一站复用游标;不需要等本轮所有站的FINISHED。 */ +static bool Example_End_Data(ExampleApp *app) +{ + if (app->gi_station != NULL && + IEC104_Slave_Release_Station(app->slave, app->gi_station) != IEC104_STATION_OK) { return false; } + app->gi_station = NULL; + memset(&app->gi_data, 0, sizeof(app->gi_data)); + app->session_instance = app->session_request = 0U; + return true; +} + +/* 第二条事件流:应用主动取业务事件,再显式应答;没有库调用应用函数的路径。 + * 常量点表立即就绪;异步数据源可在NEED_DATA保留business_event,完成采样后再Submit。 + * WAIT先处理Slave通知并推进网络,下次重取数据;STALE表示交互已失效,不重试过期事件。 */ +static bool Example_Drain_Business(ExampleApp *app) +{ + unsigned count; + for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) { + IEC104_InterrogationEvent_t *event = &app->business_event; + IEC104_InterrogationReplyResult_t reply; + if (!app->business_pending) { + IEC104_InterrogationNextResult_t next = IEC104_Interrogation_NextEvent(app->interrogation, event); + if (next == IEC104_INTERROGATION_NEXT_EMPTY) { return true; } + if (next != IEC104_INTERROGATION_NEXT_READY) { return false; } + app->business_pending = true; + } + switch (event->type) { + case IEC104_INTERROGATION_BEGIN: { + bool accept = event->request.qualifier <= 22U; + /* 示例仅定义组1=遥信、组2=遥测;拒绝未配置的组。协议库支持全部16组。 + * 广播会先交付所有站的BEGIN;这里只作准入判断,不占用唯一取数游标。 */ + reply = IEC104_Interrogation_Reply_Begin(app->interrogation, event, accept); + break; + } + case IEC104_INTERROGATION_NEED_DATA: + /* 广播逐站取数,因此首个NEED_DATA才建立游标;用注册编号取得原站,避免同CA替换串站。 + * 应用单独Acquire,确保库交付FINISHED并归还自己的引用时,应用清理仍有有效节点。 */ + if (event->request_id != app->session_request || event->instance_id != app->session_instance) { + if (app->session_request != 0U) { return false; } + if (event->request.station_id != 0U) { + app->gi_station = IEC104_Slave_Get_Station_By_Id(app->slave, event->request.station_id); + if (app->gi_station == NULL || + IEC104_Slave_Acquire_Station(app->slave, app->gi_station) != IEC104_STATION_OK) { return false; } + } + memset(&app->gi_data, 0, sizeof(app->gi_data)); + if (event->request.qualifier == 21U) { app->gi_data.scaled_index = EXAMPLE_MEASUREMENT_COUNT; } + if (event->request.qualifier == 22U) { app->gi_data.single_index = EXAMPLE_SIGNAL_COUNT; } + app->session_instance = event->instance_id; + app->session_request = event->request_id; + } + app->batch_result = Example_Build_Batch(app, &event->request); + if (app->batch_result < 0) { reply = IEC104_Interrogation_Fail_Request(app->interrogation, event); } + else if (app->batch_result == 0) { reply = IEC104_Interrogation_Complete_Data(app->interrogation, event); } + else { reply = IEC104_Interrogation_Submit_Batch(app->interrogation, event, &app->batch); } + if (reply == IEC104_INTERROGATION_REPLY_OK && app->batch_result > 0) { + if (app->gi_data.single_index < EXAMPLE_SIGNAL_COUNT) { + app->gi_data.single_index += app->batch.object_count; + } else { app->gi_data.scaled_index += app->batch.object_count; } + } else if (reply == IEC104_INTERROGATION_REPLY_OK && app->batch_result == 0) { + /* 其他站的数据随后才开始;无需等到所有站的ACT_TERM才复用游标。 */ + if (!Example_End_Data(app)) { return false; } + } + break; + case IEC104_INTERROGATION_FINISHED: + /* 完成、本地取消、主站撤销、超时、断线和失败统一清理。 + * 真实异步采样还需在这里停止对应任务;迟到的旧FINISHED不清理新请求资源。 */ + if (event->instance_id == app->session_instance && event->request_id == app->session_request) { + if (!Example_End_Data(app)) { return false; } + } + reply = IEC104_INTERROGATION_REPLY_OK; /* 终结通知无需调用任何应答接口。 */ + break; + default: return false; + } + /* WAIT保留business_event且游标不动;下轮重新Build_Batch,不重发缓存的旧点值。 */ + if (reply == IEC104_INTERROGATION_REPLY_WAIT) { return true; } + if (reply != IEC104_INTERROGATION_REPLY_OK && reply != IEC104_INTERROGATION_REPLY_STALE) { return false; } + app->business_pending = false; + } + return true; +} + +/* 同步模拟计数器动作:执行入口先验证事件,WAIT/STALE不调用本函数。 + * 只在内存中一次完成选中组的操作;不调用网络,不再次进入召唤模块。 + * 实际外部电表命令需要独立的执行结果和重复请求策略,不能套用此同步函数。 */ +static bool Example_Counter_Action(void *context, const IEC104_InterrogationRequest_t *request) +{ + ExampleApp *app = (ExampleApp *)context; + size_t first, end, index; + if (request->is_test || (request->qualifier != 1U && request->qualifier != 2U && + request->qualifier != 5U) || request->freeze == 0U || request->freeze > 3U) { return false; } + first = request->qualifier == 2U ? 4U : 0U; + end = request->qualifier == 1U ? 4U : EXAMPLE_COUNTER_COUNT; + for (index = first; index < end; ++index) { + if (request->freeze != 3U) { + app->counter_frozen[index].value = app->counter_current[index]; + app->counter_frozen[index].sequence_number = + (uint8_t)((app->counter_frozen[index].sequence_number + 1U) & 31U); + } + if (request->freeze >= 2U) { app->counter_current[index] = 0; } + } + return true; +} + +/* 释放当前读取的节点引用和快照归属;快照字节无需清零,下次读取会整体覆盖。 + * Complete_Data成功或匹配的FINISHED都可收尾,冻结动作本身不占用读取游标。 */ +static bool Example_End_Counter(ExampleApp *app) +{ + if (app->counter_station != NULL && + IEC104_Slave_Release_Station(app->slave, app->counter_station) != IEC104_STATION_OK) { return false; } + app->counter_station = NULL; + app->counter_instance = app->counter_request = 0U; + app->counter_cursor = app->counter_end = 0U; + return true; +} + +/* 从本轮固定快照构造一个Type15、SQ=0批次,共用ASDU缓冲但不推进游标。 + * 返回1表示非空批次,0表示所选组已读完,-1表示构造失败;WAIT后仍取同一快照重组。 */ +static int Example_Build_Counter_Batch(ExampleApp *app, const IEC104_InterrogationRequest_t *request) +{ + IEC60870_AsduHeader_t header; + size_t index; + if (app->counter_cursor == app->counter_end) { return 0; } + memset(&header, 0, sizeof(header)); + header.cause_of_transmission = request->data_cause; + header.common_address = request->common_address; + header.originator_address = request->originator_address; + header.is_test = request->is_test; + if (IEC60870_Asdu_Create(&app->batch, IEC104_Slave_Get_Application_Layer_Parameters(app->slave), + app->batch_bytes, sizeof(app->batch_bytes), &header, 0) != IEC60870_ASDU_OK) { return -1; } + for (index = app->counter_cursor; index < app->counter_end && + app->batch.object_count < request->max_objects_per_asdu; ++index) { + IEC60870_IntegratedTotals_t point; + IEC60870_AsduResult_t result; + if (IEC60870_IntegratedTotals_Create(&point, (uint32_t)(3001U + index), + &app->counter_snapshot[index]) != IEC60870_ASDU_OK) { return -1; } + result = IEC60870_Asdu_Add_InformationObject(&app->batch, &point.object); + if (result == IEC60870_ASDU_FULL) { break; } + if (result != IEC60870_ASDU_OK) { return -1; } + } + return app->batch.object_count != 0U ? 1 : -1; +} + +/* 冻结值在首个NEED_DATA固定为本轮快照;WAIT重建报文也使用同一份值、序号和品质。 + * 模拟点值属于本应用;真实共享数据源应在其锁内取得快照后再进行网络提交。 */ +static bool Example_Drain_Counter(ExampleApp *app) +{ + unsigned count; + if (app->counter == NULL) { return true; } + for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) { + IEC104_InterrogationEvent_t *event = &app->counter_event; + IEC104_InterrogationReplyResult_t reply; + if (!app->counter_pending) { + IEC104_InterrogationNextResult_t next = IEC104_Interrogation_NextEvent(app->counter, event); + if (next == IEC104_INTERROGATION_NEXT_EMPTY) { return true; } + if (next != IEC104_INTERROGATION_NEXT_READY) { return false; } + app->counter_pending = true; + } + switch (event->type) { + case IEC104_INTERROGATION_BEGIN: { + IEC104_Station_t *station = IEC104_Slave_Get_Station_By_Id(app->slave, event->request.station_id); + bool accept = IEC104_Station_Get_Context(station) == app && + (event->request.qualifier == 1U || event->request.qualifier == 2U || event->request.qualifier == 5U); + if (accept && event->request.freeze != 0U && !event->request.is_test) { + reply = IEC104_CounterInterrogation_Execute_Begin(app->counter, event, Example_Counter_Action, app); + } else { + reply = IEC104_Interrogation_Reply_Begin(app->counter, event, + accept && event->request.freeze == 0U); + } + break; + } + case IEC104_INTERROGATION_NEED_DATA: { + int built; + if (event->instance_id != app->counter_instance || event->request_id != app->counter_request) { + IEC104_Station_t *station; + if (app->counter_request != 0U) { return false; } + station = IEC104_Slave_Get_Station_By_Id(app->slave, event->request.station_id); + if (station == NULL || IEC104_Station_Get_Context(station) != app || + IEC104_Slave_Acquire_Station(app->slave, station) != IEC104_STATION_OK) { return false; } + app->counter_station = station; + memcpy(app->counter_snapshot, app->counter_frozen, sizeof(app->counter_snapshot)); + app->counter_cursor = event->request.qualifier == 2U ? 4U : 0U; + app->counter_end = event->request.qualifier == 1U ? 4U : EXAMPLE_COUNTER_COUNT; + app->counter_instance = event->instance_id; + app->counter_request = event->request_id; + } + built = Example_Build_Counter_Batch(app, &event->request); + if (built < 0) { reply = IEC104_Interrogation_Fail_Request(app->counter, event); } + else if (built == 0) { reply = IEC104_Interrogation_Complete_Data(app->counter, event); } + else { reply = IEC104_Interrogation_Submit_Batch(app->counter, event, &app->batch); } + if (reply == IEC104_INTERROGATION_REPLY_OK) { + if (built > 0) { app->counter_cursor += app->batch.object_count; } + else if (built == 0 && !Example_End_Counter(app)) { return false; } + } + break; + } + case IEC104_INTERROGATION_FINISHED: + if (event->reason == IEC104_INTERROGATION_FINISH_COMPLETED) { ++app->counter_completed; } + if (event->instance_id == app->counter_instance && event->request_id == app->counter_request && + !Example_End_Counter(app)) { return false; } + reply = IEC104_INTERROGATION_REPLY_OK; + break; + default: return false; + } + if (reply == IEC104_INTERROGATION_REPLY_WAIT) { return true; } + if (reply != IEC104_INTERROGATION_REPLY_OK && reply != IEC104_INTERROGATION_REPLY_STALE) { return false; } + app->counter_pending = false; + } + return true; +} + +/* 必须在开始消费Slave事件之前创建业务模块;config本地对象按值复制,广播数组仍为借用。 + * 单站可传NULL/0;多站广播须提供覆盖全部目标的数组,保持独占和地址稳定直到模块Destroy。 */ +static bool Example_Init(ExampleApp *app, IEC104_Slave_t *slave, uint16_t common_address, + IEC104_InterrogationStationState_t *broadcast_states, size_t broadcast_capacity) +{ + IEC104_InterrogationConfig_t config; + size_t index; + memset(app, 0, sizeof(*app)); + app->slave = slave; + for (index = 0U; index < EXAMPLE_SIGNAL_COUNT; ++index) { + app->signal_values[index] = example_signals[index].value; + app->double_values[index] = 1U; + } + if (IEC104_Slave_Add_Station(slave, &app->station, common_address, app) != IEC104_STATION_OK) { return false; } + IEC104_Interrogation_Config_Default(&config); + config.use_station_registry = true; /* CA只在注册时指定;多站广播需提供每站状态区。 */ + config.broadcast_states = broadcast_states; + config.broadcast_capacity = broadcast_capacity; + config.max_objects_per_asdu = 4U; + app->interrogation = IEC104_Interrogation_Create(slave, &config); + if (app->interrogation == NULL) { (void)IEC104_Slave_Remove_Station(slave, &app->station); } + return app->interrogation != NULL; +} + +/* 计数量实例可选,独立提供广播状态区;不共享总召的数组或取数游标。 + * CLI仅注册一站。测试可给多个CA注册同一app,它们会指向同一组模拟计数器; + * 真实多站应用应让各站context分别拥有计数器数据,尤其不要逐站复位同一物理源。 */ +static bool Example_Init_Counters(ExampleApp *app, + IEC104_InterrogationStationState_t *broadcast_states, size_t broadcast_capacity) +{ + static const int32_t initial[EXAMPLE_COUNTER_COUNT] = {120000, 345600, 7890, 0, 1, 65535, INT32_MAX, 42}; + IEC104_InterrogationConfig_t config; + size_t index; + if (app->counter != NULL) { return false; } + IEC104_Interrogation_Config_Default(&config); + config.use_station_registry = true; + config.broadcast_states = broadcast_states; + config.broadcast_capacity = broadcast_capacity; + config.max_objects_per_asdu = 4U; + app->counter = IEC104_CounterInterrogation_Create(app->slave, &config); + if (app->counter == NULL) { return false; } + for (index = 0U; index < EXAMPLE_COUNTER_COUNT; ++index) { + app->counter_current[index] = initial[index]; + app->counter_frozen[index].value = initial[index]; + } + return true; +} + +/* 执行检查已完成时区换算和滞留补偿;动作不重复补偿,也不推进网络事件循环。 */ +static bool Example_Clock_Action(void *context, const IEC104_ClockSynchronizationRequest_t *request) +{ + ExampleApp *app = (ExampleApp *)context; + if (app->clock_system) { return IEC60870_Clock_Set(&request->local_time) != 0; } + app->clock_utc_ms = request->utc_ms; + app->clock_base_ms = request->base_ms; + app->clock_valid = true; + return true; +} + +/* config可为NULL采用默认值;创建须早于消费Slave通知,实例独占自身请求和确认状态。 */ +static bool Example_Init_Clock(ExampleApp *app, const IEC104_ClockSynchronizationConfig_t *config) +{ + IEC104_ClockSynchronizationConfig_t defaults; + if (app->clock != NULL) { return false; } + if (config == NULL) { + IEC104_ClockSynchronization_Config_Default(&defaults); + config = &defaults; + } + app->clock = IEC104_ClockSynchronization_Create(app->slave, config); + return app->clock != NULL; +} + +/* 每轮最多执行事件预算条请求;WAIT先交回Slave消费通知,不重放已经执行的动作。 */ +static bool Example_Drain_Clock(ExampleApp *app) +{ + unsigned count; + if (app->clock == NULL) { return true; } + for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) { + IEC104_ClockSynchronizationExecuteResult_t result = + IEC104_ClockSynchronization_Execute_Next(app->clock, Example_Clock_Action, app); + if (result == IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY || + result == IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_WAIT) { return true; } + if (result != IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_OK) { return false; } + } + return true; +} + +/* 示例只接自己的模拟对象;未知IOA由应用明确拒绝,协议模块不拥有点表。 */ +static bool Example_Command_Point(const IEC104_CommandRequest_t *request, size_t *index) +{ + uint32_t first = request->type_id == 45U ? 6001U : 6101U; + if ((request->type_id != 45U && request->type_id != 46U) || request->ioa < first || + request->ioa - first >= EXAMPLE_SIGNAL_COUNT) { return false; } + *index = (size_t)(request->ioa - first); + return true; +} + +/* 仅修改模拟设备,不发包、不推进事件循环;真实异步输出应在设备完成后再报告结果。 */ +static IEC104_CommandActionResult_t Example_Command_Action(void *context, + const IEC104_CommandRequest_t *request) +{ + ExampleApp *app = (ExampleApp *)context; + size_t index; + if (!Example_Command_Point(request, &index)) { return IEC104_COMMAND_ACTION_NOT_STARTED; } + if (request->is_select) { app->command_selected = true; } + else { + app->command_output = true; + if (request->type_id == 45U) { app->signal_values[index] = request->state; } + else { app->double_values[index] = request->state; } + } + return IEC104_COMMAND_ACTION_STARTED; +} + +/* 返回信息使用独立的固定缓冲;只有一次Submit成功,发送WAIT不重新发布或改变原文。 */ +static bool Example_Build_Command_Return(ExampleApp *app) +{ + const IEC104_CommandRequest_t *request = &app->command_event.request; + IEC60870_AsduHeader_t header; + size_t index; + if (!Example_Command_Point(request, &index)) { return false; } + memset(&header, 0, sizeof(header)); + header.cause_of_transmission = IEC60870_ASDU_COT_RETURN_INFO_REMOTE; + header.common_address = request->common_address; + header.originator_address = request->originator_address; + if (IEC60870_Asdu_Create(&app->command_return, + IEC104_Slave_Get_Application_Layer_Parameters(app->slave), app->command_return_bytes, + sizeof(app->command_return_bytes), &header, 0) != IEC60870_ASDU_OK) { return false; } + if (request->type_id == 45U) { + IEC60870_SinglePointInformation_t point; + return IEC60870_SinglePointInformation_Create(&point, (uint32_t)(1001U + index), + app->signal_values[index], 0U) == IEC60870_ASDU_OK && + IEC60870_Asdu_Add_InformationObject(&app->command_return, &point.object) == IEC60870_ASDU_OK; + } else { + IEC60870_DoublePointInformation_t point; + return IEC60870_DoublePointInformation_Create(&point, (uint32_t)(1101U + index), + app->double_values[index], 0U) == IEC60870_ASDU_OK && + IEC60870_Asdu_Add_InformationObject(&app->command_return, &point.object) == IEC60870_ASDU_OK; + } +} + +/* 先读取FINISHED再重试旧交互;取消/断线也能及时清理,不受发送等待阻挡。 + * 设备值不回滚;这里清除的只是模拟返校/输出标志。真实未完成输出需继续持有自己的忙锁和引用。 */ +static bool Example_Drain_Command(ExampleApp *app) +{ + unsigned count; + if (app->command == NULL) { return true; } + for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) { + IEC104_CommandEvent_t event; + IEC104_CommandNextResult_t next = IEC104_Command_NextEvent(app->command, &event); + if (next == IEC104_COMMAND_NEXT_EMPTY) { break; } + if (next != IEC104_COMMAND_NEXT_READY) { return false; } + if (event.type == IEC104_COMMAND_FINISHED) { + if (event.instance_id != app->command_instance || + event.transaction_id != app->command_transaction) { return false; } + if (event.select_effect) { app->command_selected = false; } + if (event.execute_effect) { app->command_output = false; } + if (app->command_station != NULL && + IEC104_Slave_Release_Station(app->slave, app->command_station) != IEC104_STATION_OK) { return false; } + app->command_station = NULL; + app->command_instance = app->command_transaction = 0U; + app->command_pending = app->command_waiting = false; + app->command_step = 0U; + } else { + if (app->command_transaction == 0U) { + IEC104_Station_t *station = IEC104_Slave_Get_Station_By_Id(app->slave, event.request.station_id); + if (station == NULL || IEC104_Slave_Acquire_Station(app->slave, station) != IEC104_STATION_OK) { + return false; + } + app->command_station = station; + app->command_instance = event.instance_id; + app->command_transaction = event.transaction_id; + } else if (app->command_instance != event.instance_id || + app->command_transaction != event.transaction_id || app->command_pending) { return false; } + app->command_event = event; + app->command_pending = true; + app->command_waiting = false; + } + } + if (app->command_pending) { + IEC104_CommandReplyResult_t reply; + size_t index; + if (IEC104_Station_Get_Context(app->command_station) != app || + !Example_Command_Point(&app->command_event.request, &index)) { + reply = IEC104_Command_Reject(app->command, &app->command_event, IEC104_COMMAND_REJECT_UNKNOWN_IOA); + } else { reply = IEC104_Command_Accept(app->command, &app->command_event, Example_Command_Action, app); } + app->command_waiting = reply == IEC104_COMMAND_REPLY_WAIT; + if (app->command_waiting) { return true; } + if (reply != IEC104_COMMAND_REPLY_OK && reply != IEC104_COMMAND_REPLY_STALE) { return false; } + app->command_pending = false; + if (reply == IEC104_COMMAND_REPLY_OK && app->command_event.type == IEC104_COMMAND_EXECUTE) { + IEC104_CommandTransactionInfo_t info; + if (IEC104_Command_Get_Transaction(app->command, &app->command_event, &info) && info.active && + info.phase == IEC104_COMMAND_PHASE_EXECUTING) { + if (!Example_Build_Command_Return(app)) { return false; } + app->command_step = 1U; + } + } + } + /* 模拟动作已经完成,先报告结果停止执行计时;确认是否已交接不影响本次报告。 */ + for (count = 0U; count < 3U && app->command_step != 0U; ++count) { + IEC104_CommandReplyResult_t reply; + if (app->command_step == 1U) { + reply = IEC104_Command_Report_Result(app->command, &app->command_event, IEC104_COMMAND_RESULT_SUCCEEDED); + } else if (app->command_step == 2U) { + reply = IEC104_Command_Submit_Return_Info(app->command, &app->command_event, &app->command_return); + } else { reply = IEC104_Command_Complete(app->command, &app->command_event); } + app->command_waiting = reply == IEC104_COMMAND_REPLY_WAIT; + if (app->command_waiting) { return true; } + if (reply == IEC104_COMMAND_REPLY_STALE) { app->command_step = 0U; return true; } + if (reply != IEC104_COMMAND_REPLY_OK) { return false; } + app->command_step = app->command_step == 3U ? 0U : app->command_step + 1U; + } + return true; +} + +#ifndef IEC104_SLAVE_EXAMPLE_NO_MAIN +static bool Example_Init_Command(ExampleApp *app) +{ + IEC104_CommandConfig_t config; + if (app->command != NULL) { return false; } + IEC104_Command_Config_Default(&config); + app->command = IEC104_Command_Create(app->slave, &config); + return app->command != NULL; +} +#endif + +static void Example_Cancel(ExampleApp *app) +{ + IEC104_Interrogation_Cancel(app->interrogation); + if (app->counter != NULL) { + IEC104_Interrogation_Cancel(app->counter); + (void)IEC104_Interrogation_Get_Status(app->counter, &app->ci); + } + if (app->clock != NULL) { + IEC104_ClockSynchronization_Cancel(app->clock); + (void)IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs); + } + if (app->command != NULL) { + IEC104_Command_Cancel(app->command); + (void)IEC104_Command_Get_Status(app->command, &app->command_status); + } + (void)IEC104_Interrogation_Get_Status(app->interrogation, &app->gi); +} + +/* 每次等待前重新查询剩余期限,避免采样/业务处理耗时后继续使用旧的毫秒数。 + * 本地可推进时不休眠;否则本例网络等待最多50ms,并受最近业务期限限制。 */ +static uint32_t Example_Wait_Time(ExampleApp *app, uint32_t maximum_ms) +{ + if (app->command != NULL) { + if (!IEC104_Command_Get_Status(app->command, &app->command_status)) { return 0U; } + if (app->command_status.events_pending || app->command_status.pending_events != 0U || + app->command_status.need_process || + ((app->command_pending || app->command_step != 0U) && !app->command_waiting)) { return 0U; } + if (app->command_status.timer_active && app->command_status.timeout_remaining_ms < maximum_ms) { + maximum_ms = app->command_status.timeout_remaining_ms; + } + } + if (!IEC104_Interrogation_Get_Status(app->interrogation, &app->gi)) { return 0U; } + if (app->counter != NULL) { + if (!IEC104_Interrogation_Get_Status(app->counter, &app->ci)) { return 0U; } + if (app->ci.events_pending || app->ci.pending_events != 0U || app->ci.need_process) { return 0U; } + if (app->ci.timer_active && app->ci.timeout_remaining_ms < maximum_ms) { + maximum_ms = app->ci.timeout_remaining_ms; + } + } + if (app->clock != NULL) { + if (!IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs)) { return 0U; } + if (app->cs.events_pending || app->cs.execute_pending || app->cs.need_process) { return 0U; } + if (app->cs.timer_active && app->cs.timeout_remaining_ms < maximum_ms) { + maximum_ms = app->cs.timeout_remaining_ms; + } + } + if (app->events_pending || app->gi.events_pending || app->gi.pending_events != 0U || + app->gi.need_process) { return 0U; } + if (app->gi.timer_active && app->gi.timeout_remaining_ms < maximum_ms) { return app->gi.timeout_remaining_ms; } + return maximum_ms; +} + +/* 状态事件交给所有实例;I帧依次由总召、计数量、时钟同步、遥控及兜底接手。 */ +static bool Example_Handle_Event(ExampleApp *app, const IEC104_SlaveEvent_t *event) +{ + IEC104_InterrogationEventResult_t result; + /* Receiver已过滤线上基础格式错误;仍防御性过滤不可用视图,避免误交给业务分派或未知类型兜底。 */ + if (event->transport.type == IEC104_TCP_EVENT_I_FRAME && event->asdu_result != IEC60870_ASDU_OK) { return true; } + result = IEC104_Interrogation_Handle_Event(app->interrogation, event); + if (app->counter != NULL && (event->transport.type != IEC104_TCP_EVENT_I_FRAME || + result == IEC104_INTERROGATION_EVENT_IGNORED)) { + IEC104_InterrogationEventResult_t counter_result = IEC104_Interrogation_Handle_Event(app->counter, event); + if (counter_result == IEC104_INTERROGATION_EVENT_ERROR || + !IEC104_Interrogation_Get_Status(app->counter, &app->ci)) { return false; } + if (result == IEC104_INTERROGATION_EVENT_IGNORED) { result = counter_result; } + } + if (result == IEC104_INTERROGATION_EVENT_ERROR || + !IEC104_Interrogation_Get_Status(app->interrogation, &app->gi)) { return false; } + if (app->clock != NULL && (event->transport.type != IEC104_TCP_EVENT_I_FRAME || + result == IEC104_INTERROGATION_EVENT_IGNORED)) { + IEC104_ClockSynchronizationEventResult_t clock_result = + IEC104_ClockSynchronization_Handle_Event(app->clock, event); + if (clock_result == IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR || + !IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs)) { return false; } + if (clock_result != IEC104_CLOCK_SYNCHRONIZATION_EVENT_IGNORED && + event->transport.type == IEC104_TCP_EVENT_I_FRAME) { return true; } + } + if (app->command != NULL && (event->transport.type != IEC104_TCP_EVENT_I_FRAME || + result == IEC104_INTERROGATION_EVENT_IGNORED)) { + IEC104_CommandEventResult_t command_result = IEC104_Command_Handle_Event(app->command, event); + if (command_result == IEC104_COMMAND_EVENT_ERROR || + !IEC104_Command_Get_Status(app->command, &app->command_status)) { return false; } + if (command_result != IEC104_COMMAND_EVENT_IGNORED && + event->transport.type == IEC104_TCP_EVENT_I_FRAME) { return true; } + } + if (result != IEC104_INTERROGATION_EVENT_IGNORED || + event->transport.type != IEC104_TCP_EVENT_I_FRAME || !app->gi.active || + event->transport.generation != app->gi.generation) { return true; } + /* 当前事件当场提交,必须在NextEvent读取后续通知之前调用;应用不保存兜底请求。 + * HIGH满只丢本条并计数;原连接停用/断开则取消,不跨启停续发。 */ + ++app->other_rejected; + switch (IEC104_Slave_Reject_Request(app->slave, event, IEC60870_ASDU_COT_UNKNOWN_TYPE)) { + case IEC104_TCP_SUBMIT_OK: ++app->other_submitted; break; + case IEC104_TCP_SUBMIT_WAIT: ++app->other_overloads; break; + case IEC104_TCP_SUBMIT_FATAL: + /* 可能已写出字节;cancelled仅表示不再重试,继续读取断线通知。 */ + ++app->other_cancelled; + break; + default: ++app->other_cancelled; break; + } + return true; +} + +/* 两种业务各自推进协议、消费业务事件并刷新状态,供外层轮换调用次序。 */ +static bool Example_Step_General(ExampleApp *app) +{ + return IEC104_Interrogation_Process(app->interrogation) && Example_Drain_Business(app) && + IEC104_Interrogation_Get_Status(app->interrogation, &app->gi); +} + +/* 仅总召测试不创建计数量实例,此时直接完成本轮;已创建时采用相同推进顺序。 */ +static bool Example_Step_Counter(ExampleApp *app) +{ + return app->counter == NULL || (IEC104_Interrogation_Process(app->counter) && Example_Drain_Counter(app) && + IEC104_Interrogation_Get_Status(app->counter, &app->ci)); +} + +static bool Example_Step_Clock(ExampleApp *app) +{ + return app->clock == NULL || (IEC104_ClockSynchronization_Process(app->clock) && + Example_Drain_Clock(app) && IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs)); +} + +/* 先推进遥控,随后两类召唤轮换、对时;发送WAIT后下一轮继续,不只靠need_process重试。 */ +static bool Example_Step(ExampleApp *app) +{ + if (app->events_pending) { return true; } + if (app->command != NULL && (!IEC104_Command_Process(app->command) || !Example_Drain_Command(app) || + !IEC104_Command_Get_Status(app->command, &app->command_status))) { return false; } + app->counter_first = !app->counter_first; + if (app->counter_first) { + if (!Example_Step_Counter(app) || !Example_Step_General(app)) { return false; } + } else { + if (!Example_Step_General(app) || !Example_Step_Counter(app)) { return false; } + } + return Example_Step_Clock(app); +} + +/* 有界消费通知。预算用完先续读,不能跨过排在后面的STOP/START直接续发旧总召。 */ +static bool Example_Drain(ExampleApp *app) +{ + unsigned count; + IEC104_SlaveEvent_t event; + for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) { + IEC104_TcpEventResult_t result = IEC104_Slave_NextEvent(app->slave, &event); + if (result == IEC104_TCP_EVENT_EMPTY) { app->events_pending = false; return true; } + if (result != IEC104_TCP_EVENT_READY || !Example_Handle_Event(app, &event)) { return false; } + } + app->events_pending = true; + return true; +} + +#ifndef IEC104_SLAVE_EXAMPLE_NO_MAIN +#ifndef _WIN32 +#error "This executable uses the Windows network backend" +#endif +#ifndef _WIN32_WINNT +/* GetTickCount64所需的最低Windows API声明;这里只用于示例运行时长,不处理业务时标。 */ +#define _WIN32_WINNT 0x0600 +#endif +#ifndef WIN32_LEAN_AND_MEAN +/* 只引入本入口需要的Windows基础声明,socket操作仍全部留在协议库的Net后端。 */ +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include + +/* 信号处理器和主循环之间只交换停止标志;静态初始化为0,不在信号处理器中访问任务或网络。 */ +static volatile sig_atomic_t example_stop; + +/** + * 功能:在已注册的停止信号到来时请求主循环退出。 + * 输入参数:signal_number——运行库传入的信号编号,这里不区分SIGINT与SIGTERM。 + * 输出参数:example_stop置1。 + * 返回值:无。 + * 说明:信号上下文只写sig_atomic_t,不打印、不分配、不调用Slave;资源由main正常收尾。 + */ +static void Example_Stop(int signal_number) { (void)signal_number; example_stop = 1; } + +/** + * 功能:把纯十进制命令行文本解析为不超过max的unsigned long。 + * 输入参数:text——非NULL且以NUL结束的字符串;max——此选项允许的最大值。 + * 输出参数:value——非NULL输出存储,仅成功时写入;失败保持原值。 + * 返回值:true为整个字符串解析成功且未越界;false为空串、符号/空白前缀、尾部字符或溢出。 + * 说明:先检查再截窄为端口、CA或窗口类型,避免越界数值被静默截断;允许前导零及数值0。 + * 某些选项禁止0,由main另查;端口0表示自动分配,run-ms=0表示持续运行。 + */ +static bool Example_Number(const char *text, unsigned long max, unsigned long *value) +{ + char *end; + unsigned long parsed; + if (text[0] < '0' || text[0] > '9') { return false; } + /* strtoul通过errno报告数值溢出;先清零,避免沿用上一次库调用的错误。 */ + errno = 0; + parsed = strtoul(text, &end, 10); + if (errno != 0 || *end != '\0' || parsed > max) { return false; } + *value = parsed; + return true; +} + +/** + * 功能:向标准输出打印本示例支持的选项、默认监听配置及模拟点地址范围。 + * 输入参数:program——非NULL程序名称,main传入argv[0]。 + * 输出参数:无,文本写入stdout。 + * 返回值:无。 + * 说明:本函数不创建服务或退出进程;main根据--help或缺少参数分别选择成功/失败退出。 + */ +static void Example_Usage(const char *program) +{ + printf("Usage: %s [--bind IPv4] [--port 0..65535] [--ca 1..65534]\n" + " [--window 1..32767] [--run-ms milliseconds] [--clock simulated|system]\n" + "Defaults: 127.0.0.1:2404, CA=1, k=12, run until Ctrl+C.\n" + "GI: QOI=20 (all), 21 (signals), 22 (scaled); IOA=0.\n" + "Signals 1001..1008, scaled values 2001..2008; COT=8 cancels matching GI.\n" + "Counters 3001..3008: RQT=5 (all), 1/2 (groups); Type 15, four per batch.\n" + "FRZ=0 reads frozen values; 1 freezes; 2 freezes/resets; 3 resets.\n" + "Freeze/reset completes with ACT_CON only; TEST actions and COT=8 are rejected.\n" + "Clock: simulated by default; system sets the actual clock and requires permission.\n" + "Clock requests use UTC+8 standard time; TEST is rejected without setting time.\n" + "Commands: Type45 IOA6001..6008 -> signals1001..1008; Type46 IOA6101..6108 -> double1101..1108.\n" + "Commands require select then execute; COT8 cancels selection; TEST is rejected.\n", program); +} + +/** + * 功能:配置并运行子站示例,退出时关闭服务、读空通知并归还静态槽。 + * 输入参数:argc/argv——命令行;除--help外,每个选项后都须有一个值。 + * 输出参数:stdout输出帮助、监听端点和累计统计;stderr输出参数或运行失败信息。 + * 返回值:EXIT_SUCCESS为帮助输出或正常停止;参数、初始化、运行或清理失败为EXIT_FAILURE。 + * 说明:服务生命周期为Create -> Init -> 循环Poll/Drain/Step -> Close -> 读空事件 -> Destroy。 + * main拥有Slave句柄,app只借用;静态app和库内任务在整个运行期间保持地址不变。 + * run-ms使用单调毫秒时钟,在循环边界检查,表示约定运行时长而非精确的退出截止时刻。 + */ +int main(int argc, char **argv) +{ + static ExampleApp app; /* 注册站点及业务借用的存储在退出前保持地址不变。 */ + IEC104_SlaveConfig_t config; /* 本地配置,Slave_Init会复制,不在运行中借用它。 */ + IEC104_TcpError_t error; /* 最近一次服务接口调用的诊断,与历史网络错误分开。 */ + IEC104_TcpServerStatus_t status; /* 启动后查询实际端口,尤其用于--port=0。 */ + IEC104_Slave_t *slave; /* main持有的唯一服务句柄,清理结束后不再使用。 */ + IEC104_TcpResult_t result; /* 服务结果须显式比较IEC104_TCP_OK,不能作布尔判断。 */ + uint16_t common_address = 1U; /* 应用地址校验条件,不是TCP端口或单点IOA。 */ + unsigned long run_ms = 0U; /* 0为持续运行,非零为命令行指定的运行毫秒数。 */ + bool clock_system = false; /* 默认模拟;解析后再写入Example_Init清零后的app。 */ + uint64_t started; /* 开始主循环前的单调时刻,与CP56日历时间无关。 */ + int arg, exit_code = EXIT_SUCCESS; /* arg遍历选项和值;任一阶段失败后保留失败退出码。 */ + /* 第1步:采用库的协议默认值,只把示例默认监听地址收窄到本机回环地址。 */ + IEC104_Slave_Config_Default(&config); + memcpy(config.bind_address, "127.0.0.1", sizeof("127.0.0.1")); + for (arg = 1; arg < argc; ++arg) { + unsigned long value; /* 先在宽整数中验证范围,再赋给配置中的较窄字段。 */ + const char *option = argv[arg]; + if (strcmp(option, "--help") == 0) { Example_Usage(argv[0]); return EXIT_SUCCESS; } + /* 非帮助选项必须有值;此处前进一次取值,for尾部再前进一次到下一个选项。 */ + if (++arg == argc) { Example_Usage(argv[0]); return EXIT_FAILURE; } + if (strcmp(option, "--bind") == 0) { + /* 这里只保证固定数组能容纳含NUL的文本,IPv4语法由Slave_Init中的Net后端检查。 */ + if (strlen(argv[arg]) >= sizeof(config.bind_address)) { return EXIT_FAILURE; } + strcpy(config.bind_address, argv[arg]); + } else if (strcmp(option, "--port") == 0 && Example_Number(argv[arg], UINT16_MAX, &value)) { + config.port = (uint16_t)value; + } else if (strcmp(option, "--ca") == 0 && Example_Number(argv[arg], 65534U, &value) && value != 0U) { + common_address = (uint16_t)value; + } else if (strcmp(option, "--window") == 0 && + Example_Number(argv[arg], IEC104_SEQUENCE_MASK, &value) && value != 0U) { + config.protocol.link.k = (uint16_t)value; + } else if (strcmp(option, "--run-ms") == 0 && Example_Number(argv[arg], UINT32_MAX, &value)) { + run_ms = value; + } else if (strcmp(option, "--clock") == 0 && strcmp(argv[arg], "simulated") == 0) { + clock_system = false; + } else if (strcmp(option, "--clock") == 0 && strcmp(argv[arg], "system") == 0) { + clock_system = true; + } else { + fprintf(stderr, "Invalid option/value: %s %s\n", option, argv[arg]); + return EXIT_FAILURE; + } + } + /* 第2步:取得静态池句柄后绑定应用上下文,再初始化实际网络监听。 */ + slave = IEC104_Slave_Create(); + if (slave == NULL) { fputs("No free Slave service slot\n", stderr); return EXIT_FAILURE; } + if (!Example_Init(&app, slave, common_address, NULL, 0U)) { + (void)IEC104_Slave_Destroy(slave, NULL); + return EXIT_FAILURE; + } + if (!Example_Init_Counters(&app, NULL, 0U)) { + (void)IEC104_Interrogation_Destroy(app.interrogation); + (void)IEC104_Slave_Remove_Station(slave, &app.station); + (void)IEC104_Slave_Destroy(slave, NULL); + return EXIT_FAILURE; + } + if (!Example_Init_Clock(&app, NULL)) { + (void)IEC104_Interrogation_Destroy(app.interrogation); + (void)IEC104_Interrogation_Destroy(app.counter); + (void)IEC104_Slave_Remove_Station(slave, &app.station); + (void)IEC104_Slave_Destroy(slave, NULL); + return EXIT_FAILURE; + } + if (!Example_Init_Command(&app)) { + (void)IEC104_ClockSynchronization_Destroy(app.clock); + (void)IEC104_Interrogation_Destroy(app.counter); + (void)IEC104_Interrogation_Destroy(app.interrogation); + (void)IEC104_Slave_Remove_Station(slave, &app.station); + (void)IEC104_Slave_Destroy(slave, NULL); + return EXIT_FAILURE; + } + app.clock_system = clock_system; + result = IEC104_Slave_Init(slave, &config, &error); + if (result != IEC104_TCP_OK) { + fprintf(stderr, "Init failed: result=%d stage=%d socket=%d\n", result, error.stage, error.socket_error); + /* Init失败仍须释放已创建的业务模块和Slave槽。 */ + (void)IEC104_Interrogation_Destroy(app.interrogation); + (void)IEC104_Interrogation_Destroy(app.counter); + (void)IEC104_ClockSynchronization_Destroy(app.clock); + (void)IEC104_Command_Destroy(app.command); + (void)IEC104_Slave_Remove_Station(slave, &app.station); + (void)IEC104_Slave_Destroy(slave, NULL); + return EXIT_FAILURE; + } + /* 监听成功后取得系统实际绑定的端口,不能把配置中的0当作可连接端口输出。 */ + if (IEC104_Slave_Get_Status(slave, &status, &error) != IEC104_TCP_OK) { + exit_code = EXIT_FAILURE; + goto cleanup; + } + /* 第3步:安装仅置标志的信号处理器;真正的资源关闭仍在主线程执行。 */ + (void)signal(SIGINT, Example_Stop); + (void)signal(SIGTERM, Example_Stop); + started = GetTickCount64(); + printf("LISTEN %s:%u CA=%u\n", config.bind_address, (unsigned)status.bound_port, (unsigned)common_address); + fflush(stdout); /* 让终端或通过管道启动本程序的测试立即取得实际监听端点。 */ + while (!example_stop && (run_ms == 0U || GetTickCount64() - started < run_ms)) { + /* 有待续读通知或可立即提交的任务时安排0等待;WAIT则交回网络循环,等待上限50ms。 + * Poll还会按库内本地工作和协议期限缩短等待;50ms只约束等待,不保证整轮执行耗时。 */ + uint32_t wait_ms = Example_Wait_Time(&app, 50U); + result = IEC104_Slave_Poll(slave, wait_ms, &error); + if (result != IEC104_TCP_OK) { + /* 客户端普通断线由事件表达;这里收到的是服务接口失败,结束循环统一清理。 */ + fprintf(stderr, "Poll failed: result=%d stage=%d socket=%d\n", result, error.stage, error.socket_error); + exit_code = EXIT_FAILURE; + break; + } + /* 先消费状态变化再推进业务;Drain失败会短路,不继续对不可信状态提交应答。 */ + if (!Example_Drain(&app) || !Example_Step(&app)) { + fputs("Application event/build failure\n", stderr); + exit_code = EXIT_FAILURE; + break; + } + } +cleanup: + /* 第4步:停止应用任务,再结束网络;取消只丢弃尚未提交完毕的任务,不重发已接手数据。 */ + Example_Cancel(&app); + /* Cancel只将终结通知排队;释放总召实例前必须读完,并清理应用自己的遍历状态及资源。 */ + if (!Example_Drain_Business(&app) || !Example_Drain_Counter(&app) || !Example_Drain_Command(&app)) { + exit_code = EXIT_FAILURE; + } + if (IEC104_Slave_Close(slave, &error) != IEC104_TCP_OK) { exit_code = EXIT_FAILURE; } + { + IEC104_SlaveEvent_t event; + IEC104_TcpEventResult_t next; + /* Close保留收尾通知。对时只消费非I帧状态,不再受理请求或执行动作。 */ + while ((next = IEC104_Slave_NextEvent(slave, &event)) == IEC104_TCP_EVENT_READY) { + if (app.clock != NULL && event.transport.type != IEC104_TCP_EVENT_I_FRAME && + IEC104_ClockSynchronization_Handle_Event(app.clock, &event) == + IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR) { exit_code = EXIT_FAILURE; } + if (app.command != NULL && event.transport.type != IEC104_TCP_EVENT_I_FRAME && + IEC104_Command_Handle_Event(app.command, &event) == IEC104_COMMAND_EVENT_ERROR) { + exit_code = EXIT_FAILURE; + } + } + if (next != IEC104_TCP_EVENT_EMPTY) { exit_code = EXIT_FAILURE; } + } + if (!IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs)) { exit_code = EXIT_FAILURE; } + if (!Example_Drain_Command(&app) || !IEC104_Command_Get_Status(app.command, &app.command_status)) { + exit_code = EXIT_FAILURE; + } + /* 只有关闭且FIFO读空后才能归还槽;清理失败也必须体现在程序退出码中。 */ + if (!IEC104_Interrogation_Destroy(app.interrogation)) { exit_code = EXIT_FAILURE; } + if (!IEC104_Interrogation_Destroy(app.counter)) { exit_code = EXIT_FAILURE; } + if (!IEC104_ClockSynchronization_Destroy(app.clock)) { exit_code = EXIT_FAILURE; } + if (!IEC104_Command_Destroy(app.command)) { exit_code = EXIT_FAILURE; } + if (IEC104_Slave_Remove_Station(slave, &app.station) != IEC104_STATION_OK) { exit_code = EXIT_FAILURE; } + if (IEC104_Slave_Destroy(slave, &error) != IEC104_TCP_OK) { exit_code = EXIT_FAILURE; } + /* 打印应用提交统计;这些值不用于推断未确认帧是否已到达主站,也不作为自动重试依据。 */ + printf("STOP submitted=%lu gi_submitted=%lu waits=%lu rejected=%lu cancelled=%lu overloads=%lu deactivated=%lu timeouts=%lu negative_submitted=%lu dropped_requests=%lu counter_submitted=%lu counter_completed=%lu clock_executed=%lu clock_synchronized=%lu clock_submitted=%lu command_selected=%lu command_executed=%lu command_completed=%lu command_submitted=%lu command_returns=%lu\n", + app.gi.submitted + app.other_submitted, app.gi.gi_submitted, app.gi.waits, + app.gi.rejected + app.other_rejected, app.gi.cancelled + app.other_cancelled, + app.gi.overloads + app.other_overloads, app.gi.deactivated, app.gi.timeouts, + app.gi.negative_submitted, app.gi.dropped_requests + app.other_overloads, + app.ci.submitted, app.counter_completed, app.cs.executed, app.cs.synchronized, app.cs.submitted, + app.command_status.selected, app.command_status.executed, app.command_status.completed, + app.command_status.submitted, app.command_status.return_info_submitted); + return exit_code; +} +#endif diff --git a/IEC60870-Slave/.build/command-comments/slave_example_loopback_test.cjs.before b/IEC60870-Slave/.build/command-comments/slave_example_loopback_test.cjs.before new file mode 100644 index 00000000..3cadd24b --- /dev/null +++ b/IEC60870-Slave/.build/command-comments/slave_example_loopback_test.cjs.before @@ -0,0 +1,384 @@ +/* 独立主站字节测试:启动示例自己的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 '); + +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 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]; +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)]; + +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); +} + +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); +} +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); + const count = qoi === 20 ? 6 : 4; + 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 digital = qoi === 20 ? f < 3 : qoi === 21; + const width = digital ? 4 : 6; + const first = (qoi === 20 && !digital ? f - 3 : f - 1) * 4; + assert.equal(asdu.length, 6 + width * 4); + assert.deepEqual([...asdu.subarray(0, 6)], [digital ? 1 : 11, 4, 0x80 | qoi, oa, 7, 0]); + for (let p = 0; p < 4; p++) { + const offset = 6 + p * width; + assert.equal(asdu.readUIntLE(offset, 3), (digital ? 1001 : 2001) + first + p); + if (digital) assert.equal(asdu[offset + 3], signals[first + p]); + else { + assert.equal(asdu.readInt16LE(offset + 3), measures[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')); + 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); + 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); + await commandReply(master, commandRequest(46, 0x74, 6101, 1, true, 6, 65535), 46, true); + await commandReply(master, commandRequest(45, 0x75, 6999, 1, true), 47, true); + const commandTest = commandRequest(45, 0x76, 6001, 0, true); commandTest[2] |= 0x80; + await commandReply(master, commandTest, 7, true); + await commandReply(master, commandRequest(47, 0x77, 6001, 1, false), 44, true); + const timedCommand = commandRequest(58, 0x78, 6001, 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 < 10; index++) { + const asdu = await master.readASDU(); + (asdu[0] === 101 || asdu[0] === 15 ? counterFrames : generalFrames).push(asdu); + } + assert.equal(generalFrames.length, 6); + 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=67 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/); + assert.match(output, /command_selected=3 command_executed=2 command_completed=2 command_submitted=\d+ command_returns=2/); + assert.equal(errors, ''); + console.log('Slave example loopback: passed (real TCP, Type45/46 select/execute/return/termination, command cancel/TEST/address/unsupported types, changed GI value, 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; }); diff --git a/IEC60870-Slave/.build/command-docs/IEC104_API接口汇总.md.before b/IEC60870-Slave/.build/command-docs/IEC104_API接口汇总.md.before new file mode 100644 index 00000000..aa7bdd96 --- /dev/null +++ b/IEC60870-Slave/.build/command-docs/IEC104_API接口汇总.md.before @@ -0,0 +1,2545 @@ +# IEC104 函数 API 接口汇总(按实现文件) + +依据当前工作区的协议库源码及对应头文件整理,按实现所在的 `.c` 文件分节。范围不包含示范程序、测试程序及其业务辅助函数;2026-10-30补充独立Type 45/46遥控模块。 + +共覆盖 **25 个实现文件**,其中内部跨文件协作函数为Link 5个、信息对象层2个、召唤引擎2个。表中标有“内部协作”的函数不属于业务 API;文件内的 `static` 辅助函数、宏和类型定义不作为函数接口列出,各模块入口以对应小节和声明头文件为准。 + +模块接口并不都供业务直接调用:子站业务入口使用 `IEC104_Slave.h`,TcpServer 由 Slave 持有,Connection、SendManager 及其底层接口供相应实现层协作。运行顺序见 [完整调用流程](IEC104_完整调用流程.md)。各文件小节先说明使用约定,再列功能表和可展开的完整函数原型;原型中的参数名、类型和返回类型与声明头文件一致。 + + + +## 业务接口速查 + +| 要完成的操作 | 应使用的接口 | 前提与结果 | +| --- | --- | --- | +| 创建和启动子站 | [Slave_Create / Config_Default / Init](#file-19) | Create 返回不透明指针;Init 成功后可以监听和缓存 NORMAL | +| 共用站点与业务路由 | [Slave_Add_Station / Get_Station](#station-registry) | 调用方提供节点,按CA取得业务context;异步持有用Acquire/Release,固定集合用Lock/Unlock | +| 独立循环驱动 | [Slave_Poll / NextEvent](#file-19) | Poll 推进网络及协议,NextEvent 按 FIFO 交付通知,须逐条读到 EMPTY | +| 接入已有事件循环 | [Slave_Prepare / Process](#file-19) | Prepare 导出关注条件;按原 token 汇集就绪后 Process,再读取事件 | +| 接入站总召/分组总召 | [Interrogation](#file-21) | Handle_Event接收激活/撤销及状态通知,Process推进并检查期限;NextEvent取业务事件,以Reply_Begin/Submit_Batch/Complete_Data显式应答 | +| 接入计数量召唤 | [CounterInterrogation](#file-23) | 独立创建,复用驱动;读取用Reply_Begin和数据接口,冻结/复位用Execute_Begin | +| 接入时钟同步 | [ClockSynchronization](#file-24) | Handle_Event校验保存,Process推进应答及期限,只有显式Execute_Next调用一次应用动作;确认回显原时标 | +| 接入单点/双点遥控 | [Command](#file-25) | NextEvent取得SELECT/EXECUTE,Accept/Reject消费事件;执行后Report_Result、Submit_Return_Info、Complete,FINISHED清理资源 | +| 解析主站请求 | [Asdu_Get_Data / Parse / Get_Element](#file-16) | 先检查 I_FRAME 及 `asdu_result`,再做精确解析与业务条件检查 | +| 回复任意 ASDU | [MasterConnection_Send_ASDU](#file-18) | 使用 `&event.connection`;ASDU 可以是单独构造的对象 | +| 回复激活确认、终止 | [MasterConnection_Send_ACT_CON / Send_ACT_TERM](#file-18) | 修改传入 ASDU 的 COT/P/N 后提交,执行时机由业务决定 | +| 保存请求稍后处理 | [Slave_Copy_Event](#file-19) | 目标存储地址保持稳定,保留原服务实例和连接代次 | +| 校验事件来源及请求会话 | [Slave_Get_Event_Identity / Check_Request](#file-19) | 来源有效与当前可处理分别判断;先消费未读通知,再检查原身份和激活状态 | +| 区分提交失败原因 | [Slave_Get_Last_Submit_Result](#file-19) | 共用发送返回 false 后查询;仅 WAIT 表示尚未接手、可等待后重试 | +| 构造主动上报 | [具体信息对象 Create](#file-17) → [Asdu_Create / Add_InformationObject](#file-16) → [Slave_Enqueue_Event](#file-19) | 使用 Slave 提供的应用层参数;提交到 NORMAL,OK 后原文已复制 | +| 查看当前主站和容量 | [Slave_Get_Remote_Address / Get_Status](#file-19) | 取得只读副本;状态查询不预留容量,不保证下一次提交成功 | +| 断开原主站,继续监听 | [Slave_Close_Connection](#file-19) | 传原连接事件,不手工替换 generation | +| 停止并释放服务 | [Slave_Close → NextEvent 读空 → Destroy](#file-19) | Close 保留通知但清除 ASDU 缓存;Destroy 不隐式关闭或丢弃通知 | +| 使用时标、读取实际时间 | [Time](#file-15)、[Clock](#file-14) | Time 负责转换;Clock 负责实际读写。命令校验与执行由业务负责 | + +“后续只调 Slave”指服务的创建、驱动、事件、状态和关闭统一从 `IEC104_Slave_*` 进入。ASDU 与信息对象仍使用共用 `IEC60870_*`;请求应答通过 Slave 事件提供的 `IEC60870_MasterConnection_t` 分派,无需业务再操作 TcpServer。 + + + +## 类型与模块边界 + +| 类型或头文件 | 职责与所有权 | +| --- | --- | +| `IEC104_Slave_t` / `IEC104_Slave.h` | 不透明服务句柄,Create/Destroy 管理静态槽;内部持有私有 TcpServer 指针 | +| `IEC104_Station_t` | 调用方拥有的固定地址节点,Add/Remove管理注册关系;CA、context及引用由Slave统一管理,字段仅库可改写 | +| `IEC104_Interrogation_t` / `IEC104_Interrogation.h` | 不透明总召业务句柄,借用Slave;管理请求副本、分批、撤销、期限和WAIT进度,不拥有网络服务 | +| `IEC104_Command_t` / `IEC104_Command.h` | 不透明遥控句柄,借用Slave;每Slave至多一个实例、每实例一个事务,工作事件及FINISHED均为纯值副本 | +| `IEC104_SlaveEventIdentity_t` | Slave校验后交付的来源身份,包含服务句柄、实例编号和连接代次;可按值保存,不借用事件原文;字段只读 | +| `IEC104_TcpServer_t` / `IEC104_TcpServer.h` | 不透明底层 TCP 服务,持有连接、队列、帧缓冲、记录和事件 FIFO;可独立接入,但应用须自行管理原请求身份 | +| `IEC60870_MasterConnection_t` / `IEC60870_Slave.h` | 共用发送函数表及上下文;104 实现由 Slave 安装,不创建第二个服务、socket 或线程 | +| `IEC104_SlaveConfig_t` | `IEC104_TcpServerConfig_t` 的值类型别名;Init 复制配置,与服务对象布局无关 | +| `IEC104_TcpTypes.h` | 配置、结果、诊断、状态、原始事件和事件循环值类型;不声明 TcpServer 对象操作 | +| `IEC104_Types.h` | 104 的 APDU 尺寸、标准应用层参数宏、优先级和协议配置 | +| `IEC60870_Types.h` | 共用应用层布局和解析值类型,不选择默认传输 | +| `IEC104_TcpServer_Config.h` | TCP 实现的静态容量配置;编译选项须作用于相应库源文件 | +| Connection / SendManager / Receiver / Link / SendBuffer / AsduQueue | 库内部协作模块;已由 Slave/TcpServer 驱动时,业务不再手工调用它们推进同一会话 | +| `*_Internal.h` | 仅供库实现跨文件协作;不作为业务接入头文件 | + +不透明类型可以声明和传递指针,不能在调用方按值定义、访问成员或计算对象大小。Slave 持有 TcpServer 指针并调用其公开函数,因此无需了解它的内部布局;这两个句柄也不能相互强转。全部服务操作由所属事件循环串行执行,Create/Destroy 还需跨服务串行,BUSY 检查不是多线程互斥。 + + + +## 返回值、错误与发送阶段 + +| 返回类型 | 值与含义 | 使用规则 | +| --- | --- | --- | +| `IEC104_TcpResult_t` | OK=0,其他为负值 | Init/Poll/Prepare/Process/Close 等显式比较 `IEC104_TCP_OK` | +| `IEC104_TcpEventResult_t` | READY=1、EMPTY=0、ERROR=-1 | 仅 READY 提供新事件;EMPTY/ERROR 保持输出原值 | +| `IEC104_TcpSubmitResult_t` | OK=1、WAIT=2、ERROR=-1、FATAL=-2 | OK 已接手;WAIT 未接手;ERROR 为身份/参数/阶段错误;FATAL 需按事实收尾 | +| `IEC104_SlaveRequestState_t` | READY=2、WAIT_EVENTS=1、STALE=0、ERROR=-1 | READY仅说明会话可处理;WAIT_EVENTS先读空通知;STALE终结旧任务;ERROR处理参数或查询失败 | +| `IEC104_CommandReplyResult_t` | OK=1、WAIT=0、ERROR=-1、STALE=-2 | Accept的OK只表示消费事件;Submit_Return_Info的OK才表示帧序固定。WAIT保留输入,STALE禁止再执行旧命令 | +| 共用发送的 `bool` | true 为已接手,false 为未成功 | 104 通过 `Slave_Get_Last_Submit_Result` 进一步分类,不能把 false 一律当作 WAIT | +| `IEC60870_AsduResult_t` | OK=1、FULL=0、ERROR=-1、UNSUPPORTED=-2 | 单次 ASDU/对象操作只有 OK 修改输出;FULL 时处理容量或数量限制 | +| 时间与时钟的 `int` | 1 成功,0 失败 | 编解码成功与时间品质有效分开判断 | + +不同模块的枚举取值不同,不能统一使用 `if (result)`、`result > 0` 或混用枚举。下文各底层模块还区分 READY、QUEUED、EMPTY、FRAME_WRITTEN 等阶段。 + +| 服务错误枚举(省略 `IEC104_TCP_` 前缀) | 含义与处理 | +| --- | --- | +| INVALID_ARGUMENT / INVALID_HANDLE / INVALID_CONFIG | 检查必填参数、服务句柄和配置;不隐式销毁句柄 | +| INVALID_STATE / BUSY | 尚未初始化、运行中不允许的生命周期操作,或发生重入;按生命周期串行调用 | +| EVENTS_PENDING | Init/Destroy 前先 NextEvent 读空旧通知 | +| NETWORK_ERROR | 保存本次 `error.stage` 与 `socket_error`,定位网络初始化、监听、等待或接入失败 | +| INTERNAL_ERROR | 库内部队列、会话初始化或状态异常;保留诊断并检查状态、关闭和回收 | +| GENERATION_EXHAUSTED / REGISTRATION_EXHAUSTED | 身份计数器用尽,关闭并归还句柄,不回绕复用 | +| NOT_CONNECTED / STALE_GENERATION | 当前无主站,或指定的原连接已经过期;不能通过改成新 generation 来重试旧请求 | + +带 `IEC104_TcpError_t *error` 的接口允许 NULL;提供存储时完整写入本次 `result/stage/socket_error`,成功为 OK/NONE/0。它表示本次 API 调用结果;连接故障另见事件或状态里的 `fault`。例如客户端断开时 Poll 仍可返回 OK,再通过 DISCONNECTED 交付原因。诊断存储不得与其他输入/输出重叠。 + +发送分三个阶段:**提交接手 → 本地整帧写出 → 对端链路 ACK**。OK 只承诺第一阶段;正常 RECORD 的两项完成标志为真,收尾 RECORD 则按实际事实填写。ACK 也不等于对端业务执行成功。`Get_Last_Submit_Result` 是最近一次提交的快照,不随后续 Poll 或 ACK 更新;FATAL 可能发生在部分乃至全部字节已经写出之后。 + + + +## 缓冲、事件与参数寿命 + +| 对象 | 借用、复制与失效规则 | +| --- | --- | +| Slave / TcpServer 句柄 | 静态池持有,地址稳定;应用不 malloc/free、不复制,Destroy 成功后丢弃旧指针 | +| 原始 `IEC104_TcpEvent_t`、状态及地址快照 | 独立值副本,可跨 Poll/Close 保存;只读 type 对应的 union 成员,状态字段须先检查有效标志 | +| `IEC104_SlaveEvent_t` | APDU 是副本,但 asdu 与 connection 含自引用;用 Copy_Event 保存,禁止结构赋值/memcpy/搬移,地址必须稳定 | +| `event.connection` | 使用 `&event.connection`;它绑定原 Slave 实例和原 generation。单独复制 connection 无法重建上下文 | +| `IEC60870_Asdu_t` / `IEC60870_AsduInfo_t` | 借用参数和原文字节,使用期间保持有效;解析视图使用期间原文不可改写 | +| 具体信息对象 | Create/Get_Element/Get_From_Buffer 使用调用方存储,保存数值及只读函数表,可复制完整对象,无需 Destroy | +| Add / 发送 / 入队 | Add 成功已编码复制信息对象;发送或入队 OK 已复制 ASDU,可复用本次输入。WAIT 须保存原 connection、ASDU 和业务进度 | +| Slave 返回的应用层参数 | 静态只读常量,已取得的指针不借用服务存储;Get_Data 返回的报文指针则借用 ASDU 缓冲 | + +`Send_ACT_CON/TERM` 是组合操作:先改 COT/P/N,再提交传输。即使提交 WAIT/ERROR/FATAL,已改字段也不会回滚,不能套用“单次 ASDU setter 失败保持原值”的约定。事件被 NextEvent 的下一次 READY 替换后,其旧视图和应答上下文不能继续使用。 + + + +## 104 默认参数与容量 + +`IEC104_APP_LAYER_PARAMETERS_DEFAULT` 位于 [IEC104_Types.h](../../104/IEC104_Types.h),值为 `{1, 1, 2, 0, 2, 3, IEC104_MAX_ASDU_LEN}`,依次为 Type ID、VSQ、COT 宽度、默认 OA、CA 宽度、IOA 宽度及最大 ASDU 长度。标准 104 为 COT/CA/IOA=2/2/3,ASDU 最长 249 字节,APCI 6 字节,APDU 最长 255 字节。共用 ASDU 模块也支持其他布局,104 发送适配器会拒绝不兼容参数。 + +`IEC104_CA_BROADCAST`也位于IEC104_Types.h,固定值为`0xFFFFU`,表示104两字节CA的广播地址。它不是配置项;站点注册及总召的兼容common_address配置使用1..65534的实际站地址。绑定station时总召从节点取得CA。共用Asdu_Set_CA只负责字段编码,广播请求的接收与应答规则由对应业务模块决定。 + +`Slave_Config_Default` 填入监听 `0.0.0.0:2404`、k=12、w=8、t1/t2/t3=15000/10000/20000 毫秒、本地发送与 STOP 总期限各 15000 毫秒、HIGH 配额 0(严格优先)。配置要求 k/w 在 1~32767,期限非零且 t2 + +## 事件载荷与状态读取 + +`Slave_NextEvent` 的原始通知位于 `event.transport`。先按 `transport.type` 选择 union 成员,再读取载荷;外层 `transport.generation` 标识来源连接。 + +| 事件类型(省略 `IEC104_TCP_EVENT_`) | 载荷 | 含义 | +| --- | --- | --- | +| CONNECTED | `data.connection` | TCP 已连接,尚不代表 STARTDT 已启用 | +| ACTIVATED | `data.connection` | STARTDT_ACT 已受理,CON 可能尚未写完 | +| DEACTIVATED | `data.connection` | STOPDT_ACT 已受理,停止排空可能尚未完成,TCP 仍保持 | +| I_FRAME | `data.i_frame`;Slave 另提供 `asdu/asdu_result/connection` | 完整 APDU 副本及接收序号;视图成功后仍需完整 ASDU 和业务校验 | +| RECORD | `data.record` | `value` 保存发送事实,`closing=0` 为正常 ACK 回执,`closing=1` 为收尾剩余记录 | +| DISCONNECTED | `data.connection` | 本 generation 的最后一条通知;快照保存关闭前状态,phase 可能仍为 ACTIVE | + +CONNECTED 先于同代次其他通知,DISCONNECTED 最后;实际启停变化逐条保存,初始 STOPPED、同状态重复 ACT、CON 写完或单纯断线不额外产生启停事件。普通空间不足时回执可能暂留 Manager,晚于随后入队的启停事件交付;FIFO 表示入队顺序,不是所有网络事实的严格时间排序。 + +`Get_Status` 的 `connected` 表示当前 TCP 是否存在;`connection_status_valid` 为 1 才读取 connection,`asdu_queue_status_valid` 为 1 才读取两级队列。`pending_asdu_count` 只数尚未交接组帧的原文,`stored_asdu_count` 数仍保留的原文;二者都不覆盖全部在途帧。`pending_send_records` 数尚未转为 RECORD 通知的记录,`pending_event_count` 数已入 FIFO 的未读通知,`pending_state_event_count` 只数其中未读的 ACTIVATED/DEACTIVATED。后者为0不能证明过去没有发生已消费的STOP/START。事件中的连接状态则用 `data.connection.status_valid` 判断,不能与当前快照混用。 + +## 文件索引 + +- [IEC104.c](#file-1):3 个函数。 +- [IEC104_AsduQueue.c](#file-2):16 个函数。 +- [IEC104_Codec.c](#file-3):5 个函数。 +- [IEC104_Connection.c](#file-4):21 个函数。 +- [IEC104_Link.c](#file-5):28 个函数(含 5 个内部协作函数)。 +- [IEC104_Link_Master_StartStop.c](#file-6):9 个函数。 +- [IEC104_Link_Slave_StartStop.c](#file-7):6 个函数。 +- [IEC104_Log.c](#file-8):3 个函数。 +- [IEC104_Net_Windows.c](#file-9):11 个函数。 +- [IEC104_Receiver.c](#file-10):7 个函数。 +- [IEC104_SendBuffer.c](#file-11):11 个函数。 +- [IEC104_SendManager.c](#file-12):25 个函数。 +- [IEC104_TcpServer.c](#file-13):14 个函数。 +- [IEC60870_Clock.c](#file-14):2 个函数。 +- [IEC60870_Time.c](#file-15):13 个函数。 +- [IEC60870_Asdu.c](#file-16):10 个函数。 +- [IEC60870_InformationObjects.c](#file-17):139 个函数(含 2 个内部协作函数)。 +- [IEC60870_Slave.c](#file-18):4 个函数。 +- [IEC104_Slave.c](#file-19):41 个函数。 +- [IEC60870_Codec.c](#file-20):2 个函数。 +- [IEC104_Interrogation.c](#file-21):11 个模块接口及2个内部协作函数;总召Create已移至种类文件。 +- [IEC104_GeneralInterrogation.c](#file-22):1 个函数(保留旧Create名称)。 +- [IEC104_CounterInterrogation.c](#file-23):2 个函数。 +- [IEC104_ClockSynchronization.c](#file-24):8 个函数。 + + + +## 1. IEC104.c + +实现:[IEC104.c](../../104/IEC104.c);声明:[IEC104.h](../../104/IEC104.h)。 + +旧版单路接收兼容入口,使用模块内的全局接收器和私有链路状态。 + +调用约定:全局接收状态只适合单路串行输入。`Rx_Reset` 与 `Legacy_Link_Reset` 的作用不同;需要同时丢弃半帧并重置序号时,两者均须调用。`Rx_Feed_Byte` 返回 `IEC104_RxResult_t` 中的进度或错误分类,不交付可供业务使用的事件,也不替代 Slave 的事件循环。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Rx_Reset` | 丢弃当前未完成 APDU,复位接收阶段和长度;保留链路序号及接收缓冲中的旧字节。 | +| `IEC104_Legacy_Link_Reset` | 复位兼容入口的私有 I/S 序号状态,并恢复 STARTED 预设;不改变当前 APDU 的接收进度。 | +| `IEC104_Rx_Feed_Byte` | 逐字节组装 APDU,检查 APCI 及适用的 I/S 序号,返回接收进度或错误分类;不输出报文内容,也不处理 U 帧启停。 | + + +
+完整函数原型(3 个;参数和返回类型见声明) + +```c +#include "IEC104.h" + +extern void IEC104_Rx_Reset(void); + +extern void IEC104_Legacy_Link_Reset(void); + +extern IEC104_RxResult_t IEC104_Rx_Feed_Byte(uint8_t byte); +``` + +
+ + + +## 2. IEC104_AsduQueue.c + +实现:[IEC104_AsduQueue.c](../../104/IEC104_AsduQueue.c);声明:[IEC104_AsduQueue.h](../../104/IEC104_AsduQueue.h)。 + +管理 HIGH、NORMAL 两级变长 ASDU 字节环,保存业务原文,不执行构帧或网络发送。 + +调用约定:Init 借用调用方的 HIGH/NORMAL 字节数组,数组必须持续有效;Abort 解除引用但不释放数组。`connection_generation=0` 只用于离线 NORMAL;在线操作须使用当前绑定代次。Peek 取得的最多两段原文是短期借用,队列变更后应重新查询;需要保存时调用 Copy_Asdu。 + +结果为 `OK=1 / FULL=0 / EMPTY=2 / ERROR=-1`。FULL 与 EMPTY 均不是成功输出。Mark_Submitted 必须晚于完整帧和发送记录的接手;确认 NORMAL 使用原条目身份,不能只按“出队一条”处理。NORMAL 原文可覆盖,而 ACK 义务和发送记录独立保存。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_AsduQueue_Init` | 绑定调用方提供的两块字节数组,建立空的 HIGH/NORMAL 队列;代次 0 用于离线 NORMAL 缓存,不分配内存。 | +| `IEC104_AsduQueue_Get_Status` | 读取队列代次、各级保留条目数、保留待 ACK 数和已覆盖待 ACK 数等快照;原文为空不表示所有帧已确认。 | +| `IEC104_AsduQueue_Enqueue` | 复制 ASDU 到指定优先级队尾;HIGH 空间不足时返回 FULL,NORMAL 空间不足时覆盖最旧原文,编号窗口受阻时也返回 FULL。 | +| `IEC104_AsduQueue_Peek_Head` | 查看指定优先级最旧保留条目的身份、状态及最多两段原文借用;NORMAL 队首可能已经提交、正在等待 ACK。 | +| `IEC104_AsduQueue_Peek_Unsubmitted` | 查看指定优先级当前可交接的第一条未提交 ASDU;不移动游标,NORMAL 因 EOF 限制发送时返回 EMPTY。 | +| `IEC104_AsduQueue_Copy_Asdu` | 把有效条目视图中的一段或两段 ASDU 复制到连续输出缓冲;不添加 APCI,也不改变条目状态。 | +| `IEC104_AsduQueue_Mark_Submitted` | 在发送缓冲已接手完整 I 帧且 Manager 已建立记录后登记交接:HIGH 原文出队,NORMAL 标记为已提交并保留待 ACK。 | +| `IEC104_AsduQueue_Confirm_Next_Normal_Entry` | 按有效 ACK 对应的条目身份清理下一条 NORMAL;原文仍在时释放匹配队首,已覆盖时只核减覆盖待确认记账。 | +| `IEC104_AsduQueue_Discard_Unsubmitted_Head` | 主动丢弃尚未登记发送交接的物理队首条目;不能越过 NORMAL 待 ACK 前缀,也不能撤回已编号的 I 帧。 | +| `IEC104_AsduQueue_Get_Entry_By_Offset` | 按相对保留队首的条目偏移读取原文视图和提交状态,供检查或恢复使用;偏移单位为条目。 | +| `IEC104_AsduQueue_Get_Free_Bytes` | 查询指定优先级字节环当前未占用的字节数;查询不预留容量,NORMAL 仍可能通过覆盖旧原文入队。 | +| `IEC104_AsduQueue_Reset_High` | 清空 HIGH 待发原文及其队列游标;保留 NORMAL、连接绑定和已提交帧的发送记录。 | +| `IEC104_AsduQueue_Limit_Normal_Send` | 停止当前连接全部未提交 NORMAL 的新交接,用于 EOF 收尾;已有和后来新增的未提交原文保留给下一连接。 | +| `IEC104_AsduQueue_Detach` | 解除当前非零连接代次的绑定并回到离线代次 0;清空 HIGH,保留现存 NORMAL 并恢复待发,清除旧 ACK 记账和发送限制。 | +| `IEC104_AsduQueue_Rebind` | 绑定新的非零连接代次;清空 HIGH,把现存 NORMAL 按原顺序恢复为待发,保留条目编号及原文,不能恢复已被覆盖的数据。 | +| `IEC104_AsduQueue_Abort` | 清零队列控制结构并丢弃全部条目引用;不清零、不释放调用方提供的字节数组,重新使用须 Init。 | + + +
+完整函数原型(16 个;参数和返回类型见声明) + +```c +#include "IEC104_AsduQueue.h" + +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); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Get_Status( + const IEC104_AsduQueue_t *queue, IEC104_AsduQueueStatus_t *status); + +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); + +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); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Copy_Asdu( + const IEC104_AsduHeadInfo_t *head_info, uint8_t *destination, uint16_t capacity); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Mark_Submitted( + IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduEntryId_t entry_id); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Confirm_Next_Normal_Entry( + IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduEntryId_t entry_id); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Discard_Unsubmitted_Head( + IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduEntryId_t entry_id); + +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); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Get_Free_Bytes( + const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, uint32_t *free_bytes); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Reset_High( + IEC104_AsduQueue_t *queue, uint64_t connection_generation); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Limit_Normal_Send( + IEC104_AsduQueue_t *queue, uint64_t connection_generation); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Detach( + IEC104_AsduQueue_t *queue, uint64_t connection_generation); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Rebind( + IEC104_AsduQueue_t *queue, uint64_t connection_generation); + +extern void IEC104_AsduQueue_Abort(IEC104_AsduQueue_t *queue); +``` + +
+ + + +## 3. IEC104_Codec.c + +实现:[IEC104_Codec.c](../../104/IEC104_Codec.c);声明:[IEC104_Codec.h](../../104/IEC104_Codec.h)。 + +104专用无状态编解码,只检查完整APDU的APCI并构造I/S/U帧。公共ASDU参数和公共头解析位于 `IEC60870_Types.h`、`IEC60870_Codec.c/h`,本模块不提供公共ASDU解析声明。 + +调用约定:Parse_APCI 返回 1/0,失败不保证输出结构内容;其中 asdu 指针借用输入 APDU。Build_Iframe 返回完整长度或 -1,Build_Sframe/Build_Uframe 返回 6 或 -1。构帧函数没有输出容量参数,调用方必须预先保证 I 帧缓冲至少为 `6 + asdu_len` 字节,S/U 缓冲至少 6 字节;序号须在 0~32767。构帧不更新链路状态,也不表示实际写出。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Get_Frame_Type` | 根据控制域首字节的低位识别 I、S、U 帧;不验证完整控制域、功能码或长度。 | +| `IEC104_Parse_APCI` | 检查完整 APDU 的起始字节、长度和控制域,解析帧类型、N(S)、N(R)、U 功能码及 ASDU 位置;I 帧须容纳完整 ASDU 公共头。 | +| `IEC104_Build_Iframe` | 按给定 N(S)、N(R) 和 6~249 字节 ASDU 编码完整 I 帧,返回 APDU 长度;不修改链路状态。 | +| `IEC104_Build_Sframe` | 按给定 N(R) 编码固定 6 字节 S 帧;不推进链路接收确认位置。 | +| `IEC104_Build_Uframe` | 校验整数功能码并编码固定 6 字节 U 帧,只接受六种标准功能值;短枚举构建下也会拒绝 0x107 等非法原值。 | + + +
+完整函数原型(5 个;参数和返回类型见声明) + +```c +#include "IEC104_Codec.h" + +extern IEC104_FrameType_t IEC104_Get_Frame_Type(uint8_t control); + +extern int IEC104_Parse_APCI(const uint8_t *frame, + uint16_t frame_len, + IEC104_ApciInfo_t *apci); + +extern int IEC104_Build_Iframe(uint8_t *buf, + uint16_t send_seq, + uint16_t recv_seq, + const uint8_t *asdu, + int asdu_len); + +extern int IEC104_Build_Sframe(uint8_t *buf, uint16_t recv_seq); + +extern int IEC104_Build_Uframe(uint8_t *buf, int function); +``` + +
+ + + +## 4. IEC104_Connection.c + +实现:[IEC104_Connection.c](../../104/IEC104_Connection.c);声明:[IEC104_Connection.h](../../104/IEC104_Connection.h)。 + +子站单连接协调层,串行衔接 Receiver、Link 和 SendManager,管理期限与接收批次;由 TcpServer 调用,不执行 socket I/O。 + +调用约定:初始化后保持 Connection 和外部队列、控制槽、记录数组的地址及寿命稳定。调用方提供同一单调时钟基准的毫秒值;Feed 的 consumed 表示本次已经消费的输入,尚未消费的尾部须保留。已受理 I 帧只能在上层成功复制后按原 generation/receive_serial 释放。 + +结果区分 `FRAME_READY`(已受理的接收 I 帧)、`SEND_READY`(待写字节)、`FRAME_WRITTEN`(整帧完成处理)、`WAIT`、`EMPTY`、`ERROR` 和 `FATAL`,不能把任何正值都当作可发送。Begin/Yield/Tick 的批次协议用于先处理已到达确认,不改变原始期限;FAILED 后仍可查询受理帧和发送记录供收尾,Close 不关闭 socket 或清除服务级队列。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Connection_Config_Default` | 填写默认协议参数;本地发送总期限和 STOP 总期限取默认 t1,HIGH 调度默认为严格优先。 | +| `IEC104_Connection_Init` | 建立初始 STOPPED 的子站连接,绑定接收器、链路、发送缓冲、ASDU 队列及记录存储;由 Manager 在最终地址建立绑定,失败不修改原连接和队列。 | +| `IEC104_Connection_Begin_Receive_Batch` | 开始一轮有界接收批次,暂缓 I 帧 ACK 和 TESTFR_CON 的 t1 故障判定,让已到达的确认先被处理;保留原期限。 | +| `IEC104_Connection_Yield_Receive_Batch` | 因调度预算用尽而结束本轮接收,标记固定输入快照仍待处理并要求立即续收;继续暂缓确认 t1 判定,本地发送和 STOP 期限仍生效。 | +| `IEC104_Connection_Set_Transfer_Observer` | 在初始化后、尚未形成首个 APDU 时安装启停通知容量查询和状态变化通知适配器;供 TcpServer 内部使用。 | +| `IEC104_Connection_Feed` | 按序递入 TCP 字节,每次最多形成并分派一帧;I 帧经链路受理后保留给上层,S/U 受理后释放,资源不足时保留原帧并返回 WAIT。 | +| `IEC104_Connection_Peek_Received_Frame` | 读取已完成链路受理的 I 帧借用视图及 receive_serial;不重复处理序号或 ACK,FAILED 阶段仍可用于回收。 | +| `IEC104_Connection_Release_Received_Frame` | 按连接代次和 receive_serial 释放已受理 I 帧的接收空间;用于上层成功复制事件后,不再次提交链路事件。 | +| `IEC104_Connection_Submit_Response` | 校验当前连接和应答准入,尝试直接准备应答 I 帧,暂不能准备时复制到 HIGH;成功表示原文已接手,不执行实际网络发送。 | +| `IEC104_Connection_Try_Submit_Response` | 同一会话和期限检查下仅尝试直接准备;有待发HIGH/NORMAL、缓冲或窗口背压以及到期协议工作时WAIT,不缓存ASDU。 | +| `IEC104_Connection_Prepare_Next_Send` | 推进保留帧、控制义务及本地期限,再取得当前待写字节;无当前帧时才选择 ASDU,确认 t1 已到期时暂停新 I 帧编号。 | +| `IEC104_Connection_Get_Record_Wait` | 在 EOF 收尾的发送 WAIT 后,只读判断是否仅因可回收的已确认记录占满容量而阻塞,供 TcpServer 决定是否等待事件空间后续发。 | +| `IEC104_Connection_Handle_Send_Progress` | 提交真实写入增量,经 Manager 完成整帧记账,并更新活动、接收确认、TESTFR 或 STOP 计时;随后重试保留帧和控制流程。 | +| `IEC104_Connection_Handle_Send_Failure` | 核对当前发送帧身份后提交最终写失败,冻结连接并保留记录供回收;错误帧身份不影响当前帧。 | +| `IEC104_Connection_Tick` | 结束接收批次,重试保留帧、检查全部活动期限并推进 S 确认、STOP 应答和 TESTFR;不准备新的业务 I 帧。 | +| `IEC104_Connection_Get_Wait_Time` | 汇总本地发送、I 帧 ACK、TESTFR、STOP、t2/t3 的适用期限,计算不超过调用方上限的等待毫秒数;只读,不续期。 | +| `IEC104_Connection_Peek_Confirmed_Record` | 读取最早一条同时满足本地完整发送和有效对端 ACK 的回执副本;不自动移除,FAILED 阶段仍可查询。 | +| `IEC104_Connection_Remove_Confirmed_Record` | 按 frame_id 移除上层已成功复制的就绪队首回执,释放记录槽;不再次释放 ASDU 原文。 | +| `IEC104_Connection_Get_Record_By_Offset` | 按发送交接顺序读取指定偏移的剩余记录,包含本地完成及 ACK 事实,供观察和故障恢复使用。 | +| `IEC104_Connection_Get_Status` | 读取连接代次、生命周期、首次故障、启停/探测状态、记录数量和保留接收帧概况;CLOSED 后链路字段为历史状态。 | +| `IEC104_Connection_Handle_Transport_Closed` | 收到外部传输关闭或故障通知后冻结连接,保留首次故障和现有记录;不关闭 socket,也不推定发送完成。 | +| `IEC104_Connection_Close` | 结束内部接收、发送及计时状态,解除借用并置 CLOSED;保留外部 ASDU 队列,不关闭 socket,也不隐式执行队列重绑。 | + + +
+完整函数原型(22 个;参数和返回类型见声明) + +```c +#include "IEC104_Connection.h" + +extern void IEC104_Connection_Config_Default(IEC104_ConnectionConfig_t *config); + +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); + +extern IEC104_ConnectionResult_t IEC104_Connection_Begin_Receive_Batch( + IEC104_Connection_t *connection, uint64_t connection_generation); + +extern IEC104_ConnectionResult_t IEC104_Connection_Yield_Receive_Batch( + IEC104_Connection_t *connection, uint64_t connection_generation, uint64_t now_ms); + +extern IEC104_ConnectionResult_t IEC104_Connection_Set_Transfer_Observer( + IEC104_Connection_t *connection, uint64_t connection_generation, + const IEC104_ConnectionTransferObserver_t *observer); + +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); + +extern IEC104_ConnectionResult_t IEC104_Connection_Peek_Received_Frame( + const IEC104_Connection_t *connection, uint64_t connection_generation, + IEC104_ConnectionReceivedFrame_t *frame); + +extern IEC104_ConnectionResult_t IEC104_Connection_Release_Received_Frame( + IEC104_Connection_t *connection, uint64_t connection_generation, uint64_t receive_serial); + +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); + +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); + +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); + +extern IEC104_ConnectionResult_t IEC104_Connection_Get_Record_Wait( + const IEC104_Connection_t *connection, uint64_t connection_generation, uint8_t *record_wait); + +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); + +extern IEC104_ConnectionResult_t IEC104_Connection_Handle_Send_Failure( + IEC104_Connection_t *connection, IEC104_SendFrameId_t frame_id); + +extern IEC104_ConnectionResult_t IEC104_Connection_Tick( + IEC104_Connection_t *connection, uint64_t connection_generation, uint64_t now_ms); + +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); + +extern IEC104_ConnectionResult_t IEC104_Connection_Peek_Confirmed_Record( + const IEC104_Connection_t *connection, uint64_t connection_generation, + IEC104_IFrameSendRecord_t *record); + +extern IEC104_ConnectionResult_t IEC104_Connection_Remove_Confirmed_Record( + IEC104_Connection_t *connection, IEC104_SendFrameId_t frame_id); + +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); + +extern IEC104_ConnectionResult_t IEC104_Connection_Get_Status( + const IEC104_Connection_t *connection, uint64_t connection_generation, + IEC104_ConnectionStatus_t *status); + +extern IEC104_ConnectionResult_t IEC104_Connection_Handle_Transport_Closed( + IEC104_Connection_t *connection, uint64_t connection_generation); + +extern IEC104_ConnectionResult_t IEC104_Connection_Close( + IEC104_Connection_t *connection, uint64_t connection_generation); +``` + +
+ + + +## 5. IEC104_Link.c + +实现:[IEC104_Link.c](../../104/IEC104_Link.c);声明:[IEC104_Link_Common.h](../../104/IEC104_Link_Common.h)、[IEC104_Link_Internal.h](../../104/IEC104_Link_Internal.h)。 + +主站与子站共用的链路状态、序号、窗口、S 确认和 TESTFR 算法;不执行网络 I/O。标有“内部协作”的函数供链路实现跨文件调用。 + +调用约定:每条连接独立初始化 LinkState,角色在连接期间固定。Link 只根据调用方递入的帧、时长和发送完成事实更新状态,不读取 socket 或系统时钟。`Prepare_*` 成功时保存输出描述,实际发送结束后用对应 `Complete_*` 回报一次,不能用最新状态替换原 N(S)/N(R) 快照。 + +接收 ACK 前必须已经提交其覆盖帧的本地发送完成;N(R) 不能越过已完成发送边界。`IEC104_LinkResult_t`、STARTDT/STOPDT、S 确认和 TESTFR 各结果枚举含义不同,按对应头文件的命名值分支。内部协作函数仅供 Link 实现,不供应用绕过角色或状态检查。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Link_Unconfirmed_Rx_Internal` | 【内部协作】计算 acked_nr 到 V(R) 的 15 位循环距离,得到已接收但尚未由本地完整发送的 I/S 帧确认的 I 帧数。 | +| `IEC104_Link_Rx_Ack_Covered_Internal` | 【内部协作】判断当前 V(R) 已被确认,或已由在途 S 帧的 N(R) 完整覆盖;不据此推进 acked_nr。 | +| `IEC104_Link_State_Valid_Internal` | 【内部协作】检查链路字段范围、序号关系、S 确认状态及当前角色的启停状态组合是否合法。 | +| `IEC104_Link_Get_Send_Progress` | 读取 I 帧发送进度快照:下一构帧序号、下一本地完成序号和对端累计确认位置。 | +| `IEC104_Link_Get_Status` | 读取角色、数据传输状态、启停过程、TESTFR 状态及未确认接收数量等链路快照。 | +| `IEC104_Link_Is_Initial_State` | 判断链路是否处于可绑定新 Manager 的初始状态:序号清零、STOPPED,启停、S 确认和 TESTFR 均空闲。 | +| `IEC104_Link_Config_Default` | 填写默认链路参数:k=12、w=8、t1=15000 ms、t2=10000 ms、t3=20000 ms。 | +| `IEC104_Link_State_Init` | 按指定主站或子站角色初始化链路,清零序号和确认快照,进入 STOPPED,各过程置空闲。 | +| `IEC104_Link_State_Reset` | 复位序号、S 确认、启停和 TESTFR 算法状态;保留有效角色并回到 STOPPED,非法角色规范化为 INVALID。 | +| `IEC104_Link_Handle_I_Frame` | 检查 I 帧接收阶段、N(S) 连续性及 N(R) 有效范围;成功后推进 V(R) 和对端确认位置 V(A)。 | +| `IEC104_Link_Handle_S_Frame` | 检查 S 帧接收阶段及 N(R),成功后推进 V(A);子站停止排空期间仍可接受 S 确认。 | +| `IEC104_Link_Can_Prepare_I_Frame` | 只读判断启停门控、k 窗口及序号边界是否允许准备新的 I 帧;不分配 N(S)。 | +| `IEC104_Link_Prepare_I_Frame` | 检查发送条件后构造 I 帧,保存原始 N(S)/N(R)/长度快照,并立即推进 V(S) 预留序号;成功后序号不可撤回。 | +| `IEC104_Link_Complete_I_Send` | 按原构帧快照和完整发送长度提交 I 帧本地完成,推进 sent_vs 及实际捎带的接收确认位置;不表示对端已 ACK。 | +| `IEC104_Link_T1_I_Ack_Expired` | 用调用方提供的经过时间判断是否有已完整发送、尚未确认的 I 帧达到 t1;仅预留但未完成的帧不计入。 | +| `IEC104_Link_Can_Prepare_S_Ack` | 只读判断是否需要且允许生成 S 确认,考虑 w 门限、立即确认、停止排空和已有 S 在途状态。 | +| `IEC104_Link_Prepare_S_Ack_Internal` | 【内部协作】按已规划的 N(R) 编码 S 帧并保存确认快照,置 S 待发送状态;实际发送完成前不推进 acked_nr。 | +| `IEC104_Link_Prepare_S_Ack` | 按 w 门限、立即确认请求或停止排空需要生成 S 帧,冻结本帧 N(R) 并登记待发送状态。 | +| `IEC104_Link_Complete_S_Ack_Send` | 提交 S 帧最终发送结果;成功时按保存的 N(R) 推进接收确认位置,并清除 S 在途状态和快照。 | +| `IEC104_Link_Prepare_T2_Ack` | 在未确认接收数据等待达到 t2 后,按 S 确认规则生成报文;仍受启停门控和已有 S 在途状态约束。 | +| `IEC104_Link_Role_Valid_Internal` | 【内部协作】在完整链路状态校验通过后,确认本端角色与主站或子站专用入口要求一致。 | +| `IEC104_Link_Handle_Testfr_Act` | 校验收到的 TESTFR_ACT 并生成 TESTFR_CON 应答;不修改本端主动探测状态,也不执行发送。 | +| `IEC104_Link_Can_Prepare_Testfr_Act` | 只读判断当前是否可启动一轮主动 TESTFR;已有探测时不允许重复准备,不判断 t3 时间。 | +| `IEC104_Link_Prepare_Testfr_Act` | 生成主动 TESTFR_ACT,并把探测状态设为等待本地发送完成。 | +| `IEC104_Link_Prepare_T3_Testfr` | 在调用方给出的空闲时长达到 t3 且无探测进行时,生成 TESTFR_ACT 并登记待发送状态。 | +| `IEC104_Link_Complete_Testfr_Act_Send` | 提交 TESTFR_ACT 最终发送结果;完整发送后进入等待 TESTFR_CON 的状态,失败时结束本轮探测并返回致命错误。 | +| `IEC104_Link_Handle_Testfr_Con` | 接受当前正在等待的 TESTFR_CON 并结束主动探测;没有匹配等待过程时忽略,不缓存提前到达的 CON。 | +| `IEC104_Link_T1_Testfr_Con_Expired` | 用调用方提供的经过时间判断本端等待 TESTFR_CON 是否达到 t1;ACT 尚未完整发送时不判该确认超时。 | + + +
+完整函数原型(28 个;参数和返回类型见声明) + +```c +#include "IEC104_Link_Internal.h" +#include "IEC104_Link_Common.h" + +/* 内部协作:仅供库实现。 */ +uint16_t IEC104_Link_Unconfirmed_Rx_Internal(const IEC104_LinkState_t *state); + +/* 内部协作:仅供库实现。 */ +int IEC104_Link_Rx_Ack_Covered_Internal(const IEC104_LinkState_t *state); + +/* 内部协作:仅供库实现。 */ +int IEC104_Link_State_Valid_Internal(const IEC104_LinkState_t *state); + +extern int IEC104_Link_Get_Send_Progress( + const IEC104_LinkState_t *state, IEC104_IFrameSendProgress_t *progress); + +extern int IEC104_Link_Get_Status(const IEC104_LinkState_t *state, IEC104_LinkStatus_t *status); + +extern int IEC104_Link_Is_Initial_State(const IEC104_LinkState_t *state); + +extern void IEC104_Link_Config_Default(IEC104_LinkConfig_t *config); + +extern int IEC104_Link_State_Init(IEC104_LinkState_t *state, + IEC104_LinkRole_t role); + +extern void IEC104_Link_State_Reset(IEC104_LinkState_t *state); + +extern IEC104_LinkResult_t IEC104_Link_Handle_I_Frame( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci); + +extern IEC104_LinkResult_t IEC104_Link_Handle_S_Frame( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci); + +extern int IEC104_Link_Can_Prepare_I_Frame(const IEC104_LinkState_t *state, uint16_t k); + +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); + +extern IEC104_LinkResult_t IEC104_Link_Complete_I_Send( + IEC104_LinkState_t *state, const IEC104_IFrameTx_t *tx, int sent_len); + +extern int IEC104_Link_T1_I_Ack_Expired( + const IEC104_LinkState_t *state, + uint32_t elapsed_ms, uint32_t t1_ms); + +extern int IEC104_Link_Can_Prepare_S_Ack(const IEC104_LinkState_t *state, uint16_t w, int immediate); + +/* 内部协作:仅供库实现。 */ +int IEC104_Link_Prepare_S_Ack_Internal(IEC104_LinkState_t *state, + uint8_t *response, uint16_t receive_number); + +extern int IEC104_Link_Prepare_S_Ack(IEC104_LinkState_t *state, + uint16_t w, int immediate, + uint8_t *response, uint16_t capacity); + +extern IEC104_LinkResult_t IEC104_Link_Complete_S_Ack_Send( + IEC104_LinkState_t *state, int sent_len); + +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); + +/* 内部协作:仅供库实现。 */ +int IEC104_Link_Role_Valid_Internal(const IEC104_LinkState_t *state, IEC104_LinkRole_t role); + +extern IEC104_LinkResult_t IEC104_Link_Handle_Testfr_Act( + const IEC104_ApciInfo_t *apci, uint8_t *response, uint16_t capacity); + +extern int IEC104_Link_Can_Prepare_Testfr_Act(const IEC104_LinkState_t *state); + +extern int IEC104_Link_Prepare_Testfr_Act(IEC104_LinkState_t *state, + uint8_t *request, + uint16_t capacity); + +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); + +extern IEC104_LinkResult_t IEC104_Link_Complete_Testfr_Act_Send( + IEC104_LinkState_t *state, int sent_len); + +extern IEC104_LinkResult_t IEC104_Link_Handle_Testfr_Con( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci); + +extern int IEC104_Link_T1_Testfr_Con_Expired( + const IEC104_LinkState_t *state, + uint32_t elapsed_ms, uint32_t t1_ms); +``` + +
+ + + +## 6. IEC104_Link_Master_StartStop.c + +实现:[IEC104_Link_Master_StartStop.c](../../104/IEC104_Link_Master_StartStop.c);声明:[IEC104_Link_Master_StartStop.h](../../104/IEC104_Link_Master_StartStop.h)。 + +控制站主动 STARTDT/STOPDT 流程;当前 TcpServer 是子站实现,不通过这些入口发起启停。 + +调用约定:仅适用于 `IEC104_ROLE_CONTROLLING`。START 使用 Prepare → 实际发送 → Complete → Handle_CON;STOP 使用 Request → Prepare(可能先生成 S 帧)→ 对应 Complete → Handle_CON。请求已受理、报文已准备、等待发送及等待对端确认是不同阶段;保存原发送描述并使用匹配的完成接口。此文件不提供主站 TCP 客户端、自动计时或业务调度。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Link_Prepare_Startdt_Act` | 在主站已停止且无启停过程时生成 STARTDT_ACT,登记 ACT 待发送状态;已经启动或过程忙时不生成。 | +| `IEC104_Link_Complete_Startdt_Act_Send` | 提交 STARTDT_ACT 最终发送结果,完整发送后转为等待 STARTDT_CON;失败时清理本次过程并返回致命错误。 | +| `IEC104_Link_Handle_Startdt_Con` | 接受主站当前正在等待的 STARTDT_CON,进入 STARTED 并结束启动过程;不重置链路序号。 | +| `IEC104_Link_T1_Startdt_Con_Expired` | 检查主站等待 STARTDT_CON 的时间是否达到 t1;未处于该等待阶段时返回未到期。 | +| `IEC104_Link_Request_Stopdt` | 受理主站主动停止意图,进入 STOP_REQUESTED 并立即禁止准备新业务 I 帧;此时不生成报文。 | +| `IEC104_Link_Prepare_Stopdt_Request` | 推进已受理的停止请求,按需先生成 S 确认,再生成 STOPDT_ACT;输出实际帧类型供后续完成分派。 | +| `IEC104_Link_Complete_Stopdt_Act_Send` | 提交 STOPDT_ACT 最终发送结果,完整发送后转为等待 STOPDT_CON。 | +| `IEC104_Link_Handle_Stopdt_Con` | 在匹配停止等待、全部预留 I 帧已获 ACK 且接收确认已覆盖时,接受 STOPDT_CON 并进入 STOPPED。 | +| `IEC104_Link_T1_Stopdt_Con_Expired` | 检查主站等待 STOPDT_CON 是否达到 t1;不用于子站 STOP_DRAINING 排空计时。 | + + +
+完整函数原型(9 个;参数和返回类型见声明) + +```c +#include "IEC104_Link_Master_StartStop.h" + +extern int IEC104_Link_Prepare_Startdt_Act(IEC104_LinkState_t *state, + uint8_t *request, + uint16_t capacity); + +extern IEC104_LinkResult_t IEC104_Link_Complete_Startdt_Act_Send( + IEC104_LinkState_t *state, int sent_len); + +extern IEC104_LinkResult_t IEC104_Link_Handle_Startdt_Con( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci); + +extern int IEC104_Link_T1_Startdt_Con_Expired( + const IEC104_LinkState_t *state, + uint32_t elapsed_ms, uint32_t t1_ms); + +extern IEC104_StopdtRequestResult_t IEC104_Link_Request_Stopdt( + IEC104_LinkState_t *state); + +extern IEC104_StopdtPrepareResult_t IEC104_Link_Prepare_Stopdt_Request( + IEC104_LinkState_t *state, uint8_t *request, uint16_t capacity, + IEC104_StopdtTx_t *tx); + +extern IEC104_LinkResult_t IEC104_Link_Complete_Stopdt_Act_Send( + IEC104_LinkState_t *state, int sent_len); + +extern IEC104_LinkResult_t IEC104_Link_Handle_Stopdt_Con( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci); + +extern int IEC104_Link_T1_Stopdt_Con_Expired( + const IEC104_LinkState_t *state, + uint32_t elapsed_ms, uint32_t t1_ms); +``` + +
+ + + +## 7. IEC104_Link_Slave_StartStop.c + +实现:[IEC104_Link_Slave_StartStop.c](../../104/IEC104_Link_Slave_StartStop.c);声明:[IEC104_Link_Slave_StartStop.h](../../104/IEC104_Link_Slave_StartStop.h)。 + +被控站启停采用 ACT 受理即改变状态的模型;CON 的发送完成不回写启停状态。 + +调用约定:仅适用于 `IEC104_ROLE_CONTROLLED`。START/STOP 的状态在 ACT 受理时提交,CON 写出完成不再次覆盖后来的启停状态;STOP 还需排空已编号 I 帧及确认义务。通过 Slave/TcpServer 使用时,本层由 SendManager/Connection 调用,应用只消费 ACTIVATED/DEACTIVATED。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Link_Handle_Startdt_Act` | 校验子站收到的 STARTDT_ACT,生成 STARTDT_CON,并立即进入 STARTED、结束停止排空;重复 START 也生成确认。 | +| `IEC104_Link_Complete_Startdt_Con_Send` | 检查子站角色、链路状态和 STARTDT_CON 完整发送长度;不在完成时重新激活链路。 | +| `IEC104_Link_Handle_Stopdt_Act` | 受理 STOPDT_ACT 后立即进入 STOPPED 和 STOP_DRAINING;排空中的重复 STOP 返回 IGNORED,不重启过程。 | +| `IEC104_Link_Can_Prepare_Stopdt_Response` | 只读判断停止排空当前能否准备下一帧 S 确认或 STOPDT_CON。 | +| `IEC104_Link_Prepare_Stopdt_Response` | 推进子站停止排空:先按需生成 S 确认,全部预留 I 帧获 ACK 后生成 STOPDT_CON;生成 CON 时结束排空过程。 | +| `IEC104_Link_Complete_Stopdt_Con_Send` | 检查子站角色、链路状态和 STOPDT_CON 完整发送长度;不覆盖后来 START 已提交的状态。 | + + +
+完整函数原型(6 个;参数和返回类型见声明) + +```c +#include "IEC104_Link_Slave_StartStop.h" + +extern IEC104_LinkResult_t IEC104_Link_Handle_Startdt_Act( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci, + uint8_t *response, uint16_t capacity); + +extern IEC104_LinkResult_t IEC104_Link_Complete_Startdt_Con_Send( + IEC104_LinkState_t *state, int sent_len); + +extern IEC104_LinkResult_t IEC104_Link_Handle_Stopdt_Act( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci); + +extern int IEC104_Link_Can_Prepare_Stopdt_Response(const IEC104_LinkState_t *state); + +extern IEC104_StopdtPrepareResult_t IEC104_Link_Prepare_Stopdt_Response( + IEC104_LinkState_t *state, uint8_t *response, uint16_t capacity, + IEC104_StopdtTx_t *tx); + +extern IEC104_LinkResult_t IEC104_Link_Complete_Stopdt_Con_Send( + IEC104_LinkState_t *state, int sent_len); +``` + +
+ + + +## 8. IEC104_Log.c + +实现:[IEC104_Log.c](../../104/IEC104_Log.c);声明:[IEC104_Log.h](../../104/IEC104_Log.h)。 + +通用日志输出,使用固定栈缓冲;日志配置不带内部锁。 + +调用约定:在并发输出开始前配置级别和回调;回调接收的字符串只在回调期间有效,保存时复制。消息最多保留 511 字节,超出按字节截断,默认输出另加换行;格式串和参数须符合 printf 规则。日志全局配置和输出不是服务事件接口,不参与协议推进。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Set_Log_Callback` | 设置日志输出回调;传 NULL 时恢复默认标准流输出。 | +| `IEC104_Set_Log_Level` | 设置允许输出的最低日志级别,低于该级别的消息在格式化前被过滤。 | +| `IEC104_Log_Emit` | 格式化带本地时间戳和级别的日志,优先调用用户回调;无回调时 DEBUG/INFO 输出到 stdout,WARN/ERR 输出到 stderr。 | + + +
+完整函数原型(3 个;参数和返回类型见声明) + +```c +#include "IEC104_Log.h" + +extern void IEC104_Set_Log_Callback(IEC104_LogCb_t cb); + +extern void IEC104_Set_Log_Level(int min_level); + +extern void IEC104_Log_Emit(int level, const char *fmt, ...); +``` + +
+ + + +## 9. IEC104_Net_Windows.c + +实现:[IEC104_Net_Windows.c](../../104/IEC104_Net_Windows.c);声明:[IEC104_Net.h](../../104/IEC104_Net.h)。 + +Windows 同步非阻塞网络后端,将 Winsock 操作和错误转换为通用网络接口结果。 + +调用约定:Init/Cleanup 配对维护平台网络引用;Listen/Accept 交付的 socket 由所属服务 Close。Recv/Send 的 transferred 是本次增量,不是累计数;WOULD_BLOCK、RETRY、RETRY_LATER、EOF 和 ERROR 必须分别调度,EOF 仅表示接收方向关闭。Wait 只报告可尝试的操作,后续非阻塞 I/O 才确定实际结果。 + +Now_Ms 提供协议单调毫秒;Pending_Input 查询当时已排队字节数,供接收预算让出时固定范围,不读取这些字节。Net 层不理解 ASDU、ACK 或业务连接身份,应用也不能通过 Prepare 导出的 socket 接管收发。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Net_Init` | 调用 WSAStartup 初始化 Winsock 2.2,取得一次需要与 Cleanup 配对释放的平台引用。 | +| `IEC104_Net_Cleanup` | 调用 WSACleanup 释放一次成功 Init 取得的 Winsock 引用;不代替关闭各个 socket。 | +| `IEC104_Net_Invalid_Argument_Error` | 返回 Windows 后端的参数错误码 WSAEINVAL,避免上层依赖平台错误常量。 | +| `IEC104_Net_Listen` | 解析数字 IPv4 地址,创建独占绑定的非阻塞 TCP 监听 socket,并返回实际监听端口;端口 0 由系统分配。 | +| `IEC104_Net_Accept` | 尝试接入一个客户端,设置非阻塞和 TCP_NODELAY,返回客户端句柄及对端 IPv4/端口;区分暂无连接、可重试和资源暂缺。 | +| `IEC104_Net_Recv` | 执行一次非阻塞接收,返回实际字节数、EOF 或错误分类;不组帧,不在 EOF 时自行关闭 socket。 | +| `IEC104_Net_Pending_Input` | 通过 FIONREAD 查询当前 socket 接收缓冲中的可读字节数;不读取或消费这些字节。 | +| `IEC104_Net_Send` | 执行一次非阻塞发送,返回本次实际交给本机网络栈的字节数及错误分类;允许部分写入,不自动补发。 | +| `IEC104_Net_Wait` | 用一次 select 等待监听接入和客户端读写就绪,返回通用事件位;关注集合为空时执行有限定时等待。 | +| `IEC104_Net_Close` | 关闭指定 socket;无效句柄按成功处理,不释放 Winsock 初始化引用。 | +| `IEC104_Net_Now_Ms` | 读取 GetTickCount64 的系统启动后毫秒计数,用于单调期限计算,不受系统日历校时影响。 | + + +
+完整函数原型(11 个;参数和返回类型见声明) + +```c +#include "IEC104_Net.h" + +extern IEC104_NetResult_t IEC104_Net_Init(int *native_error); + +extern void IEC104_Net_Cleanup(void); + +extern int IEC104_Net_Invalid_Argument_Error(void); + +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); + +extern IEC104_NetResult_t IEC104_Net_Accept( + IEC104_NetSocket_t listener, IEC104_NetSocket_t *client, IEC104_NetPeer_t *peer, int *native_error); + +extern IEC104_NetIoResult_t IEC104_Net_Recv( + IEC104_NetSocket_t socket_handle, void *data, size_t capacity); + +extern IEC104_NetResult_t IEC104_Net_Pending_Input( + IEC104_NetSocket_t socket_handle, uint32_t *available, int *native_error); + +extern IEC104_NetIoResult_t IEC104_Net_Send( + IEC104_NetSocket_t socket_handle, const void *data, size_t length); + +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); + +extern IEC104_NetResult_t IEC104_Net_Close( + IEC104_NetSocket_t socket_handle, int *native_error); + +extern uint64_t IEC104_Net_Now_Ms(void); +``` + +
+ + + +## 10. IEC104_Receiver.c + +实现:[IEC104_Receiver.c](../../104/IEC104_Receiver.c);声明:[IEC104_Receiver.h](../../104/IEC104_Receiver.h)。 + +每连接独立的流式接收器,一次保留一帧完整 APDU,不处理链路序号或协议 ACK。 + +调用约定:每条连接使用独立实例与非零 generation。Feed 每次最多交付一个完整 APDU,通过 consumed 返回实际消费量;FRAME_READY 后先 Peek,处理并 Release,才能继续后续字节。Peek 的指针借用 Receiver 缓冲,在 Release/Reset/Abort/重新 Init 后失效。 + +`IN_PROGRESS` 表示半帧待续,`FRAME_READY` 表示有完整帧,`FRAME_ERROR` 为报文格式错误,`ERROR` 为参数/身份/状态错误。成帧只做格式及最低长度检查;I/S 序号由 Link 校验,完整 ASDU 及业务条件由上层处理。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Receiver_Init` | 清零接收器并绑定非零连接代次,用于首次使用或新连接换代。 | +| `IEC104_Receiver_Get_Status` | 校验连接代次和组帧状态后,读取已收长度、预期长度和完整帧就绪标志。 | +| `IEC104_Receiver_Abort` | 清零接收器及其绑定,使全部旧帧视图失效;之后须重新 Init。 | +| `IEC104_Receiver_Feed` | 按序消费字节片段,支持半包和粘包,每次最多收齐一帧;检查 APCI 和 ASDU 最低长度,返回实际消费量,完整帧未释放时停止消费。 | +| `IEC104_Receiver_Peek_Frame` | 取得当前完整 APDU 及 APCI 的只读借用视图,允许重复读取,不释放报文。 | +| `IEC104_Receiver_Release_Frame` | 释放已经受理的完整帧并清除组帧进度,使接收器可以继续接收下一帧;不代表协议 ACK 已发送。 | +| `IEC104_Receiver_Reset` | 显式丢弃当前半帧或完整帧,保留连接代次并恢复等待帧头;允许清理异常组帧进度,不复位 Link。 | + + +
+完整函数原型(7 个;参数和返回类型见声明) + +```c +#include "IEC104_Receiver.h" + +extern IEC104_ReceiverResult_t IEC104_Receiver_Init( + IEC104_Receiver_t *receiver, uint64_t connection_generation); + +extern IEC104_ReceiverResult_t IEC104_Receiver_Get_Status( + const IEC104_Receiver_t *receiver, uint64_t connection_generation, IEC104_ReceiverStatus_t *status); + +extern void IEC104_Receiver_Abort(IEC104_Receiver_t *receiver); + +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); + +extern IEC104_ReceiverResult_t IEC104_Receiver_Peek_Frame( + const IEC104_Receiver_t *receiver, uint64_t connection_generation, + IEC104_ReceivedFrame_t *frame); + +extern IEC104_ReceiverResult_t IEC104_Receiver_Release_Frame( + IEC104_Receiver_t *receiver, uint64_t connection_generation); + +extern IEC104_ReceiverResult_t IEC104_Receiver_Reset( + IEC104_Receiver_t *receiver, uint64_t connection_generation); +``` + +
+ + + +## 11. IEC104_SendBuffer.c + +实现:[IEC104_SendBuffer.c](../../104/IEC104_SendBuffer.c);声明:[IEC104_SendBuffer.h](../../104/IEC104_SendBuffer.h)。 + +保存一个当前 I 帧及独立的 S/U 控制 FIFO,管理预留、写入进度和本地发送期限,不执行网络 I/O。 + +调用约定:Reserve 获得短期可写 APDU 存储和 frame_id → 编码 → Commit_Frame;取消只适用于 Link 尚未成功分配序号的预留。Peek_Send_Head 的 pending_data 是剩余字节借用,部分写入后重新取得偏移,实际写出多少就 Record_Written_Bytes 多少,不重复登记。 + +`FRAME_WRITTEN` 仅表示字节已写完,须先完成适用的 Link/记录提交,再 Commit_Send_Completion 释放槽;不能把一次 send 成功当作整帧完成。结果还区分 FULL、BUSY、EMPTY、ERROR。初始化借用控制槽数组,Abort 只解除绑定;调用方不要同时绕过 Manager 手工提交完成。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_SendBuffer_Init` | 绑定调用方提供的控制帧数组和非零连接代次,初始化当前 I 槽、控制 FIFO 和帧编号分配状态。 | +| `IEC104_SendBuffer_Get_Status` | 读取控制帧数量、当前 I 帧及其 N(S)、预留状态、空闲状态和控制槽可用情况。 | +| `IEC104_SendBuffer_Reserve_S_U_Frame` | 预留控制 FIFO 队尾的一个 6 字节 S/U 槽,并分配不复用的 frame_id;容量不足或已有预留时不接手。 | +| `IEC104_SendBuffer_Reserve_I_Frame` | 在无当前 I 帧、无控制帧且无预留时,预留唯一 I 帧缓冲并分配 frame_id。 | +| `IEC104_SendBuffer_Commit_Frame` | 校验预留身份、APCI 类型及 I 帧原始快照后提交完整报文,登记原始入队时刻;I 帧成为当前帧,S/U 进入控制 FIFO。 | +| `IEC104_SendBuffer_Cancel_Reservation` | 核对身份后取消尚未提交的预留,已用帧编号不复用;仅在 Link 尚未成功构帧时使用,不回滚协议状态。 | +| `IEC104_SendBuffer_Peek_Send_Head` | 查看当前 I 帧或最早控制帧的身份、元数据及剩余字节借用;已经写完但尚未提交完成的帧仍可查询。 | +| `IEC104_SendBuffer_Record_Written_Bytes` | 按 frame_id 登记本次实际写入增量;全部字节写完时保存完成时刻并返回 FRAME_WRITTEN,不调用 Link 完成接口。 | +| `IEC104_SendBuffer_Commit_Send_Completion` | 在对应链路完成及 I 帧记录提交后,释放已经写完的当前 I 槽或控制 FIFO 队首;不等待对端 ACK。 | +| `IEC104_SendBuffer_Head_Send_Expired` | 按队首原始 queued_at_ms 判断本地排队及发送总期限是否到达;部分写入不续期。 | +| `IEC104_SendBuffer_Abort` | 清零发送缓冲控制结构并解除外部数组绑定;不关闭 TCP,也不释放调用方数组。 | + + +
+完整函数原型(11 个;参数和返回类型见声明) + +```c +#include "IEC104_SendBuffer.h" + +extern IEC104_SendBufferResult_t IEC104_SendBuffer_Init( + IEC104_SendBuffer_t *buffer, IEC104_SendBufferControlFrame_t *control_frames, + uint16_t control_capacity, uint64_t connection_generation); + +extern IEC104_SendBufferResult_t IEC104_SendBuffer_Get_Status( + const IEC104_SendBuffer_t *buffer, IEC104_SendBufferStatus_t *status); + +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); + +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); + +extern IEC104_SendBufferResult_t IEC104_SendBuffer_Cancel_Reservation( + IEC104_SendBuffer_t *buffer, IEC104_SendFrameId_t frame_id); + +extern IEC104_SendBufferResult_t IEC104_SendBuffer_Peek_Send_Head( + const IEC104_SendBuffer_t *buffer, IEC104_SendBufferHeadInfo_t *head_info); + +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); + +extern IEC104_SendBufferResult_t IEC104_SendBuffer_Commit_Send_Completion( + IEC104_SendBuffer_t *buffer, IEC104_SendFrameId_t frame_id); + +extern int IEC104_SendBuffer_Head_Send_Expired( + const IEC104_SendBuffer_t *buffer, uint64_t connection_generation, + uint64_t now_ms, uint32_t timeout_ms); + +extern void IEC104_SendBuffer_Abort(IEC104_SendBuffer_t *buffer); +``` + +
+ + + +## 12. IEC104_SendManager.c + +实现:[IEC104_SendManager.c](../../104/IEC104_SendManager.c);声明:[IEC104_SendManager.h](../../104/IEC104_SendManager.h)。 + +子站发送管理层,统一协调 Link、SendBuffer、AsduQueue 和 I 帧记录;当前只绑定 CONTROLLED 角色,不执行网络 I/O。 + +调用约定:Manager 绑定地址稳定、同代次的队列、Link、SendBuffer 和记录数组。发送准备固定为“记录槽检查 → 预留缓冲 → Link 分配序号 → 提交 APDU → 建记录 → 队列交接”;Link 已分配 N(S) 后的交接失败不能取消编号继续使用连接。 + +`QUEUED` 已接手但尚未写出;`WAIT` 尚待门控/容量;`FRAME_WRITTEN` 已完成本地记账,无需再手工 Complete。记录同时满足本地完成和对端 ACK 后,由上层复制再 Remove;删除记录不再次确认 ASDU。PROTOCOL_ERROR/FATAL 结束当前会话,Abort 前仍需保存剩余记录事实。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_SendManager_Get_Status` | 读取连接代次、记录容量、未消费记录数及 HIGH 未 ACK 数;不要求故障后的 Link 仍可运行。 | +| `IEC104_SendManager_Enqueue` | 校验当前会话后复制 ASDU 到绑定队列;HIGH 只在 STARTED/IDLE 接手,NORMAL 按队列策略缓存,不生成 I 帧。 | +| `IEC104_SendManager_Queue_S_Ack` | 先取得控制槽,再按 w 门限、立即确认或停止排空要求生成 S 帧并入队;完整发送前不提交接收确认。 | +| `IEC104_SendManager_Queue_T2_Ack` | 在调用方提供的接收确认等待时间达到 t2 后,尝试生成并排队 S 帧;不自行读取时钟或重置起点。 | +| `IEC104_SendManager_Queue_Testfr_Act` | 尝试生成并排队主动 TESTFR_ACT;已有探测或控制资源不足时等待,ACT 完整发送后才进入等待 CON。 | +| `IEC104_SendManager_Queue_T3_Testfr` | 在调用方提供的空闲时间达到 t3 时,尝试生成并排队 TESTFR_ACT。 | +| `IEC104_SendManager_Handle_Testfr_Act` | 处理收到的 TESTFR_ACT,为每次受理生成并排队 TESTFR_CON;控制槽不足返回 WAIT,请求尚未受理。 | +| `IEC104_SendManager_Handle_Testfr_Con` | 通过 Manager 统一接收 TESTFR_CON,结束匹配的主动探测;无需预留控制槽,当前未等待 CON 时返回 IGNORED。 | +| `IEC104_SendManager_Prepare_Next_Send` | 优先取得已有帧的剩余字节;无已有帧时按 HIGH/NORMAL 优先级及配额选择 ASDU,构帧、建立发送记录并完成队列交接。 | +| `IEC104_SendManager_Submit_Response` | 只在 STARTED/IDLE 接手应答;条件允许时直接准备 HIGH I 帧,否则缓存到 HIGH 队列,返回已准备、已入队或尚未接手。 | +| `IEC104_SendManager_Try_Submit_Response` | 仅在发送缓冲空闲、HIGH/NORMAL无未提交条目且窗口和记录容量允许时准备I帧;WAIT不入队、不分配N(S)。 | +| `IEC104_SendManager_Handle_Send_Progress` | 登记实际写入增量;整帧写完后按原帧类型提交 Link 完成、记录 I 帧本地完成事实,并释放发送缓冲槽。 | +| `IEC104_SendManager_Handle_Send_Failure` | 对匹配的未完成队首提交最终发送失败或本地发送超时,调用适用的链路失败完成接口并返回 FATAL;保留记录供收尾。 | +| `IEC104_SendManager_Handle_I_S_Frame` | 把收到的 I/S 帧交给 Link 校验序号和累计 ACK,登记全部新确认事实,核减 HIGH 未确认数,并释放 NORMAL 原文或覆盖记账。 | +| `IEC104_SendManager_Get_Record_By_Offset` | 按交接顺序读取指定偏移的 I 帧记录副本,包含完整 frame_id、来源、序号快照、时间及完成/ACK 标志。 | +| `IEC104_SendManager_Peek_Oldest_Sent_Unacked_Record` | 读取最早已经完整本地发送、尚未获对端 ACK 的 I 帧记录;只有未完成发送帧时返回 EMPTY。 | +| `IEC104_SendManager_Get_Deadlines` | 按同一时刻计算发送队首的本地发送剩余时间、最早已写完未 ACK 帧的 t1 剩余时间,并返回队首身份和控制槽容量状态。 | +| `IEC104_SendManager_T1_I_Ack_Expired` | 按最早已写完未 ACK 记录的原始 written_at_ms 判断 t1 是否到期;重复 ACK、部分 ACK 或后续发送不重置原起点。 | +| `IEC104_SendManager_Peek_Confirmed_Record` | 查看同时满足本地完整发送和对端有效 ACK 的队首回执;未就绪返回 WAIT,不跳过旧记录。 | +| `IEC104_SendManager_Remove_Confirmed_Record` | 按 frame_id 删除已就绪且已被上层复制的队首回执,释放记录容量;不改变协议序号或再次处理原文。 | +| `IEC104_SendManager_Abort` | 终止 Manager,在自身存储有效时同时终止绑定 SendBuffer;保留 AsduQueue 和调用方记录数组,不关闭 socket。 | +| `IEC104_SendManager_Slave_Handle_Startdt_Act` | 先预留控制槽再受理 START,立即激活并排队 STARTDT_CON;仅从停用状态启动时清空 HIGH 和重置连续 HIGH 配额。 | +| `IEC104_SendManager_Slave_Handle_Stopdt_Act` | 受理 STOP 后立即停用并清空 HIGH 待发原文;排空中的重复 STOP 返回 IGNORED,控制队列满也不阻止该入口受理。 | +| `IEC104_SendManager_Slave_Queue_Stopdt_Response` | 为停止排空生成并排队 S 确认或 STOPDT_CON;仅 STOPDT_CON 入队成功时输出其 frame_id,供连接层关联 STOP 总期限。 | +| `IEC104_SendManager_Init` | 绑定已初始化、同代次的子站 Link、SendBuffer、ASDU 队列及记录数组;要求新会话初始状态,成功清空 HIGH 并保留待发 NORMAL。 | +| `IEC104_SendManager_Init_Connection` | 统一准备新的子站 Link、SendBuffer 和 Manager,校验成功后向最终地址提交并建立指针绑定;供 Connection 初始化使用。 | + + +
+完整函数原型(26 个;参数和返回类型见声明) + +```c +#include "IEC104_SendManager.h" + +extern IEC104_SendManagerResult_t IEC104_SendManager_Get_Status( + const IEC104_SendManager_t *manager, IEC104_SendManagerStatus_t *status); + +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); + +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); + +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); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Queue_Testfr_Act( + IEC104_SendManager_t *manager, uint64_t connection_generation, uint64_t now_ms); + +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); + +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); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Handle_Testfr_Con( + IEC104_SendManager_t *manager, uint64_t connection_generation, const IEC104_ApciInfo_t *apci); + +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); + +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); + +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); + +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); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Handle_Send_Failure( + IEC104_SendManager_t *manager, IEC104_SendFrameId_t frame_id); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Handle_I_S_Frame( + IEC104_SendManager_t *manager, uint64_t connection_generation, const IEC104_ApciInfo_t *apci); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Get_Record_By_Offset( + const IEC104_SendManager_t *manager, uint16_t offset, IEC104_IFrameSendRecord_t *record); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Peek_Oldest_Sent_Unacked_Record( + const IEC104_SendManager_t *manager, IEC104_IFrameSendRecord_t *record); + +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); + +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); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Peek_Confirmed_Record( + const IEC104_SendManager_t *manager, IEC104_IFrameSendRecord_t *record); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Remove_Confirmed_Record( + IEC104_SendManager_t *manager, IEC104_SendFrameId_t frame_id); + +extern void IEC104_SendManager_Abort(IEC104_SendManager_t *manager); + +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); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Slave_Handle_Stopdt_Act( + IEC104_SendManager_t *manager, uint64_t connection_generation, + const IEC104_ApciInfo_t *apci); + +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); + +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); + +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); +``` + +
+ + + +## 13. IEC104_TcpServer.c + +实现:[IEC104_TcpServer.c](../../104/IEC104_TcpServer.c);声明:[IEC104_TcpServer.h](../../104/IEC104_TcpServer.h)。 + +IPv4 子站的底层 TCP 服务,供 Slave 调用或由集成方独立使用,内部持有连接、队列、缓冲和事件 FIFO。一个服务同时管理一个活动主站;全部操作由所属线程串行驱动。共用值类型声明在 [IEC104_TcpTypes.h](../../104/IEC104_TcpTypes.h),业务主入口见 [Slave](#file-19)。 + +调用约定:Create → Config_Default/Init → Poll/NextEvent(或 Prepare/Process)→ Close → NextEvent 读空 → Destroy。Send_Response 输入原请求 generation 及不含 APCI 的 ASDU 字节;Enqueue_Event 同样输入 ASDU 原文,不能传完整 I 帧。直接使用本层时,应答来源身份由集成方保存;通过 Slave 时这些步骤已封装。 + +Prepare 的 socket 仅供注册,等待前落实 pending_event_count/need_process;Process 回传实际注册时的 service_token 和 socket/token。旧服务通知整批忽略,旧 socket 通知逐项忽略。Close_Connection 保留监听/NORMAL,Close 清除服务级缓存,两者均不等待 STOPDT 或 ACK。状态查询先看 connected 及各有效标志,历史 phase 不能代表当前连通。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_TcpServer_Create` | 从模块内部静态池取得地址固定的不透明服务句柄;不分配堆内存、不创建 socket,无可用槽时返回 NULL。 | +| `IEC104_TcpServer_Destroy` | 归还尚未 Init 或已经 Close 且事件已读空的服务对象;不隐式关闭运行中的服务,也不丢弃未读事件。 | +| `IEC104_TcpServer_Config_Default` | 填写默认监听地址 0.0.0.0、端口 2404 及默认协议配置,不申请网络资源。 | +| `IEC104_TcpServer_Init` | 初始化网络后端、非阻塞 IPv4 监听和服务级 ASDU 队列;成功后即可缓存离线 NORMAL,客户端协议连接在实际接入时建立。 | +| `IEC104_TcpServer_Prepare` | 只读导出事件循环所需的 socket/token/关注位、最近等待期限、未读事件数和本地推进要求;不接入、收发或推进协议。 | +| `IEC104_TcpServer_Process` | 根据一批有效就绪、定时或本地通知,有界执行客户端接入、非阻塞收发、协议期限检查和事件交付;不调用网络等待。 | +| `IEC104_TcpServer_Poll` | 组合 Prepare、至多一次平台网络等待和 Process,供独立循环使用;max_wait_ms 限制本轮网络等待时间。 | +| `IEC104_TcpServer_NextEvent` | 按 FIFO 复制并取出最早一条建连、启用、停用、I 帧、发送记录或断开事件;副本无需释放,Close 后仍可继续读取。 | +| `IEC104_TcpServer_Enqueue_Event` | 把主动上报 ASDU 复制到服务级 NORMAL 队列,支持首次连接前及断线期间缓存;空间不足覆盖最旧原文,不立即写 socket。 | +| `IEC104_TcpServer_Send_Response` | 按原请求事件的连接代次提交应答;仅启动状态接手,条件允许时直接准备并尝试一次非阻塞发送,否则入 HIGH,成功不表示已写完或已 ACK。 | +| `IEC104_TcpServer_Try_Send_Response` | 按原代次尝试不排队提交;WAIT未接手,OK已固定帧顺序,之后沿原部分写入和ACK路径推进;FATAL不可直接重放。 | +| `IEC104_TcpServer_Get_Remote_Address` | 读取当前主站的缓存 IPv4 地址、源端口及连接代次副本;不查询 socket,无当前连接时拒绝。 | +| `IEC104_TcpServer_Close_Connection` | 立即关闭指定代次的当前主站并交付收尾事件;保留监听和现存 NORMAL,清空 HIGH,不等待 STOPDT、发送完成或 ACK。 | +| `IEC104_TcpServer_Get_Status` | 读取监听端口、当前连接、最近会话、未读事件、HIGH/NORMAL 队列和接入重试诊断等快照;不消费数据或推进协议。 | +| `IEC104_TcpServer_Close` | 结束当前客户端和监听,清空 ASDU 缓存并释放网络后端引用;保留未读事件,事件读空后可重新 Init 或 Destroy。 | + + +
+完整函数原型(15 个;参数和返回类型见声明) + +```c +#include "IEC104_TcpServer.h" + +extern IEC104_TcpServer_t * IEC104_TcpServer_Create(void); + +extern IEC104_TcpResult_t IEC104_TcpServer_Destroy( + IEC104_TcpServer_t *server, IEC104_TcpError_t *error); + +extern void IEC104_TcpServer_Config_Default(IEC104_TcpServerConfig_t *config); + +extern IEC104_TcpResult_t IEC104_TcpServer_Init( + IEC104_TcpServer_t *server, const IEC104_TcpServerConfig_t *config, IEC104_TcpError_t *error); + +extern IEC104_TcpResult_t IEC104_TcpServer_Prepare( + const IEC104_TcpServer_t *server, IEC104_TcpWaitInfo_t *info, IEC104_TcpError_t *error); + +extern IEC104_TcpResult_t IEC104_TcpServer_Process( + IEC104_TcpServer_t *server, const IEC104_TcpReady_t *ready, IEC104_TcpError_t *error); + +extern IEC104_TcpResult_t IEC104_TcpServer_Poll( + IEC104_TcpServer_t *server, uint32_t max_wait_ms, IEC104_TcpError_t *error); + +extern IEC104_TcpEventResult_t IEC104_TcpServer_NextEvent( + IEC104_TcpServer_t *server, IEC104_TcpEvent_t *event); + +extern IEC104_TcpSubmitResult_t IEC104_TcpServer_Enqueue_Event( + IEC104_TcpServer_t *server, const uint8_t *asdu, uint16_t asdu_length); + +extern IEC104_TcpSubmitResult_t IEC104_TcpServer_Send_Response( + IEC104_TcpServer_t *server, uint64_t generation, + const uint8_t *asdu, uint16_t asdu_length); + +extern IEC104_TcpSubmitResult_t IEC104_TcpServer_Try_Send_Response( + IEC104_TcpServer_t *server, uint64_t generation, + const uint8_t *asdu, uint16_t asdu_length); + +extern IEC104_TcpResult_t IEC104_TcpServer_Get_Remote_Address( + const IEC104_TcpServer_t *server, IEC104_TcpPeerInfo_t *peer, IEC104_TcpError_t *error); + +extern IEC104_TcpResult_t IEC104_TcpServer_Close_Connection( + IEC104_TcpServer_t *server, uint64_t generation, IEC104_TcpError_t *error); + +extern IEC104_TcpResult_t IEC104_TcpServer_Get_Status( + const IEC104_TcpServer_t *server, IEC104_TcpServerStatus_t *status, IEC104_TcpError_t *error); + +extern IEC104_TcpResult_t IEC104_TcpServer_Close( + IEC104_TcpServer_t *server, IEC104_TcpError_t *error); +``` + +
+ + + +## 14. IEC60870_Clock.c + +实现:[IEC60870_Clock.c](../../common/IEC60870_Clock.c);声明:[IEC60870_Clock.h](../../common/IEC60870_Clock.h)。 + +读取或设置实际时钟,公共日历统一为固定 UTC+8 标准时间。Windows 实现已提供;RTC 由编译选项选择并调用外部驱动接口。 + +调用约定:返回 1/0;Read 失败保持输出,成功后仍检查 is_invalid。Set 会调用实际时间源,无法保证底层硬件部分写入的回滚。`IEC60870_CLOCK_USE_RTC` 选择 Windows/RTC 时间源,与 `IEC60870_TIME_USE_PLATFORM` 的日期转换算法选择独立;RTC 驱动和硬件初始化由平台提供。Clock 无需创建对象或注册回调,也不自动响应校时命令。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC60870_Clock_Read` | 从选定的 Windows 或 RTC 时间源读取固定 UTC+8 日历,校验字段和质量标志、重算星期后输出;读取成功仍需检查 is_invalid。 | +| `IEC60870_Clock_Set` | 校验固定 UTC+8 日历并重算星期,再设置实际时钟;Windows 路径减去八小时后调用 SetSystemTime,RTC 路径交给驱动。 | + +`IEC60870_Clock_RTC_Read` 和 `IEC60870_Clock_RTC_Set` 当前仅在头文件声明,需由板级驱动实现,因此不计入本文件已实现的 API。 + + +
+完整函数原型(2 个;参数和返回类型见声明) + +```c +#include "IEC60870_Clock.h" + +int IEC60870_Clock_Read(IEC60870_ClockValue_t *value); + +int IEC60870_Clock_Set(const IEC60870_WallTime_t *local_time); +``` + +
+ + + +## 15. IEC60870_Time.c + +实现:[IEC60870_Time.c](../../common/IEC60870_Time.c);声明:[IEC60870_Time.h](../../common/IEC60870_Time.h)。 + +101/104 共用的日历、纪元秒数和 CP16/CP24/CP56Time2a 工具;普通日历范围为 1970~2099 年,CP56 范围为 2000~2099 年,不读取或设置实际时钟。 + +调用约定:所有转换返回 1/0,失败保持输出;输入带 length 时只读取该时标所需的前 2/3/7 字节。CP16 是经过毫秒,CP24 仅有分钟及分钟内毫秒,不能独立推断完整日期。CP56 解码允许 IV=1,并保留 SU;CP56_To_Seconds/Local 则拒绝无效时间,按源标准时区和 SU 处理偏移。 + +To_Seconds 的输入是 UTC,Seconds_To_Local 的输出固定 UTC+8,不采用 Windows 本地时区。To_CP56/From_CP56 只编解码,不自动转换时区;设置实际时钟使用 Clock,并先完成品质和业务检查。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC60870_Time_To_CP16` | 将0..65535毫秒经过时间编码为2字节,不包含日历或质量标志。 | +| `IEC60870_Time_From_CP16` | 解码前2字节经过时间,失败不修改输出。 | +| `IEC60870_Time_To_CP24` | 将分钟、分钟内毫秒和IV编码为3字节短时标;不推断小时及日期。 | +| `IEC60870_Time_From_CP24` | 解码前3字节短时标,校验bit6保留位及数值范围,保留IV。 | +| `IEC60870_Time_Is_Valid` | 检查年月日、时分秒和毫秒的数值是否合法;忽略派生字段 weekday,不判断时区或时钟准确性。 | +| `IEC60870_Time_Get_Weekday` | 校验日期并计算星期,输出 0~6,其中 0 表示周日。 | +| `IEC60870_Time_To_Seconds` | 将调用方提供的 UTC 日历转换为自 1970-01-01 00:00:00 UTC 起的完整秒数;毫秒只校验、不计入结果。 | +| `IEC60870_Time_From_Seconds` | 将 UTC 纪元秒数和毫秒余数转换为完整 UTC 日历,并计算星期;可选 Windows 平台库或纯整数算法。 | +| `IEC60870_Time_Seconds_To_Local` | 将 UTC 纪元秒数及毫秒转换为固定 UTC+8 日历,自动处理跨日、跨月、跨年及星期。 | +| `IEC60870_Time_CP56_To_Seconds` | 将 CP56 解析值结合源标准时区偏移转换为 UTC 秒数;拒绝 IV=1,SU=1 时额外扣除一小时。 | +| `IEC60870_Time_CP56_To_Local` | 把 CP56 解析值转换为固定 UTC+8 标准时间日历,统一处理 IV、SU、源时区偏移,并保留毫秒、重算星期。 | +| `IEC60870_Time_To_CP56` | 把合法日历及 IV/SU/星期使用标志编码为 7 字节 CP56Time2a;按日期计算所需星期,保留 IV/SU,不自动转换时区。 | +| `IEC60870_Time_From_CP56` | 校验并解码前 7 字节 CP56Time2a,检查保留位、日期时间及非零星期,保留 IV/SU 并重算星期;解码成功不等于时间可用于校时。 | + + +
+完整函数原型(13 个;参数和返回类型见声明) + +```c +#include "IEC60870_Time.h" + +int IEC60870_Time_To_CP16(const IEC60870_CP16Time2a_t *value, uint8_t *out, size_t capacity); + +int IEC60870_Time_From_CP16(const uint8_t *in, size_t length, IEC60870_CP16Time2a_t *value); + +int IEC60870_Time_To_CP24(const IEC60870_CP24Time2a_t *value, uint8_t *out, size_t capacity); + +int IEC60870_Time_From_CP24(const uint8_t *in, size_t length, IEC60870_CP24Time2a_t *value); + +int IEC60870_Time_Is_Valid(const IEC60870_WallTime_t *time); + +int IEC60870_Time_Get_Weekday(uint16_t year, uint8_t month, uint8_t day, uint8_t *weekday); + +int IEC60870_Time_To_Seconds(const IEC60870_WallTime_t *time, uint32_t *seconds); + +int IEC60870_Time_From_Seconds(uint32_t seconds, uint16_t ms, IEC60870_WallTime_t *time); + +int IEC60870_Time_Seconds_To_Local(uint32_t utc_seconds, uint16_t ms, + IEC60870_WallTime_t *local_time); + +int IEC60870_Time_CP56_To_Seconds(const IEC60870_CP56Time2a_t *value, + int16_t standard_utc_offset_minutes, uint32_t *seconds); + +int IEC60870_Time_CP56_To_Local(const IEC60870_CP56Time2a_t *value, + int16_t source_standard_utc_offset_minutes, + IEC60870_WallTime_t *local_time); + +int IEC60870_Time_To_CP56(const IEC60870_CP56Time2a_t *value, uint8_t *out, size_t capacity); + +int IEC60870_Time_From_CP56(const uint8_t *in, size_t length, IEC60870_CP56Time2a_t *value); +``` + +
+ + + +## 16. IEC60870_Asdu.c + +实现:[IEC60870_Asdu.c](../../common/IEC60870_Asdu.c);声明:[IEC60870_Asdu.h](../../common/IEC60870_Asdu.h)。 + +通用ASDU容器使用调用方存储和显式非空参数,参数在使用期间必须有效且不变。所有结果须显式比较`IEC60870_ASDU_OK`;FULL为容量或数量已满,ERROR为参数/格式错误,UNSUPPORTED为未知类型。单次ASDU接口失败不修改输出;Create产生的空构造容器不能导出,Create_From_Buffer借用对象沿用公共头最低长度校验。 + +调用约定:Create 产生空构造容器,Add 成功才固定 Type ID;FULL 保持当前 ASDU,先提交当前非空内容,另起容器重试未加入对象。若空容器也放不下对象,须检查容量和对象大小,不能循环提交空 ASDU。SQ=1 要求连续 IOA,跨类型、地址溢出或不支持 SQ 时返回 ERROR。Create_From_Buffer 的借用视图只能修改公共头,不能 Add。 + +Parse 验证精确长度及全部对象;Get_Element 的 index 从 0 开始,输出大小至少为对应具体结构,但调用方仍须按 Type ID 选择类型。Get_Data 不执行完整对象验证。除 Build_Response 明确支持输入/输出字节区重叠外,控制结构、输出参数和字节区不应相互重叠。 + +| 函数 API 接口名 | 功能作用 | +|---|---| +| `IEC60870_Asdu_Create` | 创建空容器,固定参数、公共头及SQ;首个成功添加的对象确定Type ID。头长2+COT+CA,有效容量取实际容量与配置上限的较小值。 | +| `IEC60870_Asdu_Create_From_Buffer` | 借用可写原文建立ASDU对象,只校验公共头和最低长度;允许未知类型,不允许追加对象,不复制或分配。 | +| `IEC60870_Asdu_Set_COT` | 原地设置COT低6位,范围0..63,保留P/N、TEST和其他原文。 | +| `IEC60870_Asdu_Set_Negative` | 原地设置P/N位,参数0或1,保留COT、TEST和其他原文。 | +| `IEC60870_Asdu_Set_CA` | 原地设置公共地址,按COT宽度定位、CA宽度写入;1字节允许0..255,2字节允许0..65535,失败不修改原文。 | +| `IEC60870_Asdu_Add_InformationObject` | 通过对象函数表编码,检查同类型和SQ地址连续性,成功后更新VSQ;按类型独立检查数量和SQ,文件段按实际数据长度检查容量。 | +| `IEC60870_Asdu_Get_Data` | 返回非空构造容器或借用原文对象的ASDU原文及当前长度,不含APCI,不复制或发送。 | +| `IEC60870_Asdu_Parse` | 统一检查67种类型的精确布局、变长文件段LOS与所有对象内容,返回借用参数/原文的公共头及对象区视图。 | +| `IEC60870_Asdu_Get_Element` | 按Type ID解析指定索引,向对应类型的调用方存储写入按值对象;检查输出尺寸,调用方仍须选择正确结构类型。 | +| `IEC60870_Asdu_Build_Response` | 复制请求并只替换COT/P/N,支持7/9/10/44–47及未知类型回显,允许字节区重叠。 | + +Set_CA可用于空/非空构造容器和借用可写原文,只修改CA字节,不改变COT/P/N/TEST、存在时的OA、长度、对象数量或载荷。无效容器或1字节CA溢出时返回ERROR且保持原文。它只检查编码范围;0或65535能否用于某项业务由对应处理器判断。Build_Response仍只修改COT/P/N,不自动处理广播地址。 + +应用从 info 读取 COT/CA/OA,从具体对象读取 IOA 和值。地址匹配、限定词适用性、校时和选择/执行均由业务决定。具体类型见 [信息对象层](#file-17),业务调用顺序见 [事件与 ASDU 流程](IEC104_完整调用流程.md#flow-6)。 + + +
+完整函数原型(10 个;参数和返回类型见声明) + +```c +#include "IEC60870_Asdu.h" + +IEC60870_AsduResult_t IEC60870_Asdu_Create(IEC60870_Asdu_t *self, + const IEC60870_AppLayerParameters_t *parameters, + uint8_t *buffer, uint16_t capacity, const IEC60870_AsduHeader_t *header, int is_sequence); + +IEC60870_AsduResult_t IEC60870_Asdu_Create_From_Buffer(IEC60870_Asdu_t *self, + const IEC60870_AppLayerParameters_t *parameters, uint8_t *buffer, uint16_t length); + +IEC60870_AsduResult_t IEC60870_Asdu_Set_COT(IEC60870_Asdu_t *self, int cause); + +IEC60870_AsduResult_t IEC60870_Asdu_Set_Negative(IEC60870_Asdu_t *self, int is_negative); + +IEC60870_AsduResult_t IEC60870_Asdu_Set_CA(IEC60870_Asdu_t *self, uint16_t common_address); + +IEC60870_AsduResult_t IEC60870_Asdu_Add_InformationObject(IEC60870_Asdu_t *self, + const IEC60870_InformationObject_t *object); + +IEC60870_AsduResult_t IEC60870_Asdu_Get_Data(const IEC60870_Asdu_t *self, + const uint8_t **asdu, uint16_t *length); + +IEC60870_AsduResult_t IEC60870_Asdu_Parse(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *asdu, uint16_t length, IEC60870_AsduInfo_t *info); + +IEC60870_AsduResult_t IEC60870_Asdu_Get_Element(const IEC60870_AsduInfo_t *info, + uint8_t index, void *object, size_t object_size); + +IEC60870_AsduResult_t IEC60870_Asdu_Build_Response(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *request, uint16_t length, + int cause, int is_negative, uint8_t *out, uint16_t capacity, uint16_t *out_length); +``` + +
+ + + +## 17. IEC60870_InformationObjects.c + +实现:[IEC60870_InformationObjects.c](../../common/IEC60870_InformationObjects.c);公开声明:[IEC60870_InformationObjects.h](../../common/IEC60870_InformationObjects.h);内部协作声明:[IEC60870_InformationObjects_Internal.h](../../common/IEC60870_InformationObjects_Internal.h)。 + +每种类型独立结构与Create/Get_From_Buffer,编码通过公共基类绑定的只读函数表。Create不分配内存,Get_From_Buffer只接收不含IOA的精确载荷和已解析IOA;结果按值保存,不借用输入。带CP24/CP56的普通监视与控制对象复用基础值处理;保护类型保留QDP及CP16。文件段对象有255字节独立存储,Create和解析复制段数据;ASDU按配置计算实际可发送长度。所有失败保持输出不变。 + +调用约定:先用具体 Create/Get_From_Buffer 初始化类型身份和只读函数表,再向 Add/Encode 传对象的公共基类地址。普通对象使用 `&object.object`,带 base 的类型使用 `&object.base.object`;保留完整具体对象,不把 base 截成独立对象后编码。各具体构造/解析返回 ASDU_OK/FULL/ERROR/UNSUPPORTED,均按枚举判断。 + +| 已实现 Type ID | 类型组 | ASDU 数量与 SQ 约束 | +| --- | --- | --- | +| 1~21、30~40 | 监视、累计量、保护信息 | 1~127 个,SQ=0/1 | +| 45~51、58~64 | 命令、设定值及带时标命令 | 1 个,SQ=0 | +| 70、100~107 | 初始化结束、总召、读、校时、测试、复位、延时 | 1 个,SQ=0 | +| 110~113 | 参数与参数激活 | 1~127 个,SQ=0 | +| 120~125、127 | 文件操作、文件段、归档查询 | 1 个,SQ=0 | +| 126 | 文件目录 | 1~127 个,SQ=0/1 | + + + +共 67 种类型,每种各有一组 Create/Get_From_Buffer;再加文件段容量查询、公共 Get_Type/Encode 和两个内部函数,共 139 个。编码支持某种 Type ID 不代表已经实现该类业务过程;字段与范围约束以对应具体结构及函数声明注释为准。 + +| 函数 API 接口名 | 功能作用 | +|---|---| +| `IEC60870_SinglePointInformation_Create` | 在调用方存储中校验并构造Type 1(单点信息),绑定类型及编码函数表。 | +| `IEC60870_SinglePointInformation_Get_From_Buffer` | 解析Type 1的1字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_DoublePointInformation_Create` | 在调用方存储中校验并构造Type 3(双点信息),绑定类型及编码函数表。 | +| `IEC60870_DoublePointInformation_Get_From_Buffer` | 解析Type 3的1字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_MeasuredValueNormalized_Create` | 在调用方存储中校验并构造Type 9(归一化测量值(原始16位定点值)),绑定类型及编码函数表。 | +| `IEC60870_MeasuredValueNormalized_Get_From_Buffer` | 解析Type 9的3字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_MeasuredValueScaled_Create` | 在调用方存储中校验并构造Type 11(标度化测量值),绑定类型及编码函数表。 | +| `IEC60870_MeasuredValueScaled_Get_From_Buffer` | 解析Type 11的3字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_MeasuredValueShort_Create` | 在调用方存储中校验并构造Type 13(短浮点测量值),绑定类型及编码函数表。 | +| `IEC60870_MeasuredValueShort_Get_From_Buffer` | 解析Type 13的5字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_IntegratedTotals_Create` | 在调用方存储中校验并构造Type 15(累计量),绑定类型及编码函数表。 | +| `IEC60870_IntegratedTotals_Get_From_Buffer` | 解析Type 15的5字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SinglePointWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 30(带时标单点信息),绑定类型及编码函数表。 | +| `IEC60870_SinglePointWithCP56Time2a_Get_From_Buffer` | 解析Type 30的8字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_DoublePointWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 31(带时标双点信息),绑定类型及编码函数表。 | +| `IEC60870_DoublePointWithCP56Time2a_Get_From_Buffer` | 解析Type 31的8字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_MeasuredValueNormalizedWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 34(带时标归一化测量值),绑定类型及编码函数表。 | +| `IEC60870_MeasuredValueNormalizedWithCP56Time2a_Get_From_Buffer` | 解析Type 34的10字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_MeasuredValueScaledWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 35(带时标标度化测量值),绑定类型及编码函数表。 | +| `IEC60870_MeasuredValueScaledWithCP56Time2a_Get_From_Buffer` | 解析Type 35的10字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_MeasuredValueShortWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 36(带时标短浮点测量值),绑定类型及编码函数表。 | +| `IEC60870_MeasuredValueShortWithCP56Time2a_Get_From_Buffer` | 解析Type 36的12字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_IntegratedTotalsWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 37(带时标累计量),绑定类型及编码函数表。 | +| `IEC60870_IntegratedTotalsWithCP56Time2a_Get_From_Buffer` | 解析Type 37的12字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SingleCommand_Create` | 在调用方存储中校验并构造Type 45(单命令),绑定类型及编码函数表。 | +| `IEC60870_SingleCommand_Get_From_Buffer` | 解析Type 45的1字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_DoubleCommand_Create` | 在调用方存储中校验并构造Type 46(双命令),绑定类型及编码函数表。 | +| `IEC60870_DoubleCommand_Get_From_Buffer` | 解析Type 46的1字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_StepCommand_Create` | 在调用方存储中校验并构造Type 47(步调节命令),绑定类型及编码函数表。 | +| `IEC60870_StepCommand_Get_From_Buffer` | 解析Type 47的1字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SetpointCommandNormalized_Create` | 在调用方存储中校验并构造Type 48(归一化设定值命令(原始16位定点值)),绑定类型及编码函数表。 | +| `IEC60870_SetpointCommandNormalized_Get_From_Buffer` | 解析Type 48的3字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SetpointCommandScaled_Create` | 在调用方存储中校验并构造Type 49(标度化设定值命令),绑定类型及编码函数表。 | +| `IEC60870_SetpointCommandScaled_Get_From_Buffer` | 解析Type 49的3字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SetpointCommandShort_Create` | 在调用方存储中校验并构造Type 50(短浮点设定值命令),绑定类型及编码函数表。 | +| `IEC60870_SetpointCommandShort_Get_From_Buffer` | 解析Type 50的5字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SingleCommandWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 58(带时标单命令),绑定类型及编码函数表。 | +| `IEC60870_SingleCommandWithCP56Time2a_Get_From_Buffer` | 解析Type 58的8字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_DoubleCommandWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 59(带时标双命令),绑定类型及编码函数表。 | +| `IEC60870_DoubleCommandWithCP56Time2a_Get_From_Buffer` | 解析Type 59的8字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_StepCommandWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 60(带时标步调节命令),绑定类型及编码函数表。 | +| `IEC60870_StepCommandWithCP56Time2a_Get_From_Buffer` | 解析Type 60的8字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SetpointCommandNormalizedWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 61(带时标归一化设定值命令),绑定类型及编码函数表。 | +| `IEC60870_SetpointCommandNormalizedWithCP56Time2a_Get_From_Buffer` | 解析Type 61的10字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SetpointCommandScaledWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 62(带时标标度化设定值命令),绑定类型及编码函数表。 | +| `IEC60870_SetpointCommandScaledWithCP56Time2a_Get_From_Buffer` | 解析Type 62的10字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SetpointCommandShortWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 63(带时标短浮点设定值命令),绑定类型及编码函数表。 | +| `IEC60870_SetpointCommandShortWithCP56Time2a_Get_From_Buffer` | 解析Type 63的12字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_InterrogationCommand_Create` | 在调用方存储中校验并构造Type 100(总召唤命令),绑定类型及编码函数表。 | +| `IEC60870_InterrogationCommand_Get_From_Buffer` | 解析Type 100的1字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_CounterInterrogationCommand_Create` | 在调用方存储中校验并构造Type 101(电能脉冲召唤命令),绑定类型及编码函数表。 | +| `IEC60870_CounterInterrogationCommand_Get_From_Buffer` | 解析Type 101的1字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_ReadCommand_Create` | 在调用方存储中校验并构造Type 102(读命令),绑定类型及编码函数表。 | +| `IEC60870_ReadCommand_Get_From_Buffer` | 解析Type 102的0字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_ClockSynchronizationCommand_Create` | 在调用方存储中校验并构造Type 103(时钟同步命令),绑定类型及编码函数表。 | +| `IEC60870_ClockSynchronizationCommand_Get_From_Buffer` | 解析Type 103的7字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_ResetProcessCommand_Create` | 在调用方存储中校验并构造Type 105(复位进程命令),绑定类型及编码函数表。 | +| `IEC60870_ResetProcessCommand_Get_From_Buffer` | 解析Type 105的1字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_TestCommandWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 107(带时标测试命令),绑定类型及编码函数表。 | +| `IEC60870_TestCommandWithCP56Time2a_Get_From_Buffer` | 解析Type 107的9字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_StepPositionInformation_Create` | 构造Type 5(步位置信息),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_StepPositionInformation_Get_From_Buffer` | 解析Type 5的2字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_BitString32_Create` | 构造Type 7(32位比特串监视),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_BitString32_Get_From_Buffer` | 解析Type 7的5字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_SinglePointWithCP24Time2a_Create` | 构造Type 2(带CP24单点信息),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_SinglePointWithCP24Time2a_Get_From_Buffer` | 解析Type 2的4字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_DoublePointWithCP24Time2a_Create` | 构造Type 4(带CP24双点信息),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_DoublePointWithCP24Time2a_Get_From_Buffer` | 解析Type 4的4字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_StepPositionWithCP24Time2a_Create` | 构造Type 6(带CP24步位置信息),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_StepPositionWithCP24Time2a_Get_From_Buffer` | 解析Type 6的5字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_Bitstring32WithCP24Time2a_Create` | 构造Type 8(带CP24比特串监视),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_Bitstring32WithCP24Time2a_Get_From_Buffer` | 解析Type 8的8字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_MeasuredValueNormalizedWithCP24Time2a_Create` | 构造Type 10(带CP24归一化测量值),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_MeasuredValueNormalizedWithCP24Time2a_Get_From_Buffer` | 解析Type 10的6字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_MeasuredValueScaledWithCP24Time2a_Create` | 构造Type 12(带CP24标度化测量值),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_MeasuredValueScaledWithCP24Time2a_Get_From_Buffer` | 解析Type 12的6字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_MeasuredValueShortWithCP24Time2a_Create` | 构造Type 14(带CP24短浮点测量值),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_MeasuredValueShortWithCP24Time2a_Get_From_Buffer` | 解析Type 14的8字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_IntegratedTotalsWithCP24Time2a_Create` | 构造Type 16(带CP24累计量),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_IntegratedTotalsWithCP24Time2a_Get_From_Buffer` | 解析Type 16的8字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_StepPositionWithCP56Time2a_Create` | 构造Type 32(带CP56步位置信息),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_StepPositionWithCP56Time2a_Get_From_Buffer` | 解析Type 32的9字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_Bitstring32WithCP56Time2a_Create` | 构造Type 33(带CP56比特串监视),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_Bitstring32WithCP56Time2a_Get_From_Buffer` | 解析Type 33的12字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_EventOfProtectionEquipment_Create` | 构造Type 17(带CP24保护事件),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_EventOfProtectionEquipment_Get_From_Buffer` | 解析Type 17的6字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_PackedStartEventsOfProtectionEquipment_Create` | 构造Type 18(带CP24成组保护启动事件),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_PackedStartEventsOfProtectionEquipment_Get_From_Buffer` | 解析Type 18的7字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_PackedOutputCircuitInfo_Create` | 构造Type 19(带CP24保护输出电路信息),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_PackedOutputCircuitInfo_Get_From_Buffer` | 解析Type 19的7字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Create` | 构造Type 38(带CP56保护事件),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer` | 解析Type 38的10字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Create` | 构造Type 39(带CP56成组保护启动事件),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer` | 解析Type 39的11字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Create` | 构造Type 40(带CP56保护输出电路信息),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Get_From_Buffer` | 解析Type 40的11字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_PackedSinglePointWithSCD_Create` | 构造Type 20(带变位检测的成组单点),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_PackedSinglePointWithSCD_Get_From_Buffer` | 解析Type 20的5字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_MeasuredValueNormalizedWithoutQuality_Create` | 构造Type 21(无品质归一化测量值),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_MeasuredValueNormalizedWithoutQuality_Get_From_Buffer` | 解析Type 21的2字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_Bitstring32Command_Create` | 构造Type 51(32位比特串命令),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_Bitstring32Command_Get_From_Buffer` | 解析Type 51的4字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_Bitstring32CommandWithCP56Time2a_Create` | 构造Type 64(带CP56比特串命令),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_Bitstring32CommandWithCP56Time2a_Get_From_Buffer` | 解析Type 64的11字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_EndOfInitialization_Create` | 构造Type 70(初始化结束),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_EndOfInitialization_Get_From_Buffer` | 解析Type 70的1字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_TestCommand_Create` | 构造Type 104(无时标测试命令),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_TestCommand_Get_From_Buffer` | 解析Type 104的2字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_DelayAcquisitionCommand_Create` | 构造Type 106(延时获得命令),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_DelayAcquisitionCommand_Get_From_Buffer` | 解析Type 106的2字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_ParameterNormalizedValue_Create` | 构造Type 110(归一化测量值参数),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_ParameterNormalizedValue_Get_From_Buffer` | 解析Type 110的3字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_ParameterScaledValue_Create` | 构造Type 111(标度化测量值参数),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_ParameterScaledValue_Get_From_Buffer` | 解析Type 111的3字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_ParameterFloatValue_Create` | 构造Type 112(短浮点测量值参数),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_ParameterFloatValue_Get_From_Buffer` | 解析Type 112的5字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_ParameterActivation_Create` | 构造Type 113(参数激活),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_ParameterActivation_Get_From_Buffer` | 解析Type 113的1字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_FileReady_Create` | 构造Type 120(文件准备就绪),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_FileReady_Get_From_Buffer` | 解析Type 120的6字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_SectionReady_Create` | 构造Type 121(节准备就绪),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_SectionReady_Get_From_Buffer` | 解析Type 121的7字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_FileCallOrSelect_Create` | 构造Type 122(文件/节召唤或选择),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_FileCallOrSelect_Get_From_Buffer` | 解析Type 122的4字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_FileACK_Create` | 构造Type 124(文件/节确认),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_FileACK_Get_From_Buffer` | 解析Type 124的4字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_FileLastSegmentOrSection_Create` | 构造Type 123(最后文件段或节),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_FileLastSegmentOrSection_Get_From_Buffer` | 解析Type 123的5字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_FileSegment_Create` | 构造Type 125(文件段),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_FileSegment_Get_From_Buffer` | 解析Type 125的4+LOS变长载荷,IOA另传,结果按值保存。 | +| `IEC60870_FileDirectory_Create` | 构造Type 126(文件目录),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_FileDirectory_Get_From_Buffer` | 解析Type 126的13字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_QueryLog_Create` | 构造Type 127(归档查询),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_QueryLog_Get_From_Buffer` | 解析Type 127的16字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_FileSegment_Get_Max_Data_Size` | 计算配置下可用的文件段数据容量;默认104为236,无法容纳空段或参数非法返回-1。 | +| `IEC60870_InformationObject_Get_Descriptor` | **内部协作**:按Type ID取得只读尺寸、数量约束及编码函数表,未知类型返回NULL。 | +| `IEC60870_InformationObject_Get_Type` | 读取已构造/解析对象的Type ID;类型与函数表不匹配或NULL返回0。 | +| `IEC60870_InformationObject_Encode` | 按参数校验IOA,经对象函数表编码,按is_sequence决定是否省略IOA;容量不足返回FULL,失败保持缓冲和长度。 | +| `IEC60870_InformationObject_Decode` | **内部协作**:按Type ID分派具体Get_From_Buffer,验证输出尺寸;输出NULL表示仅验证载荷。 | + + +
+完整函数原型(139 个;参数和返回类型见声明) + +```c +#include "IEC60870_InformationObjects.h" +#include "IEC60870_InformationObjects_Internal.h" + +IEC60870_AsduResult_t IEC60870_SinglePointInformation_Create(IEC60870_SinglePointInformation_t *self, + uint32_t ioa, int value, int quality); + +IEC60870_AsduResult_t IEC60870_SinglePointInformation_Get_From_Buffer(IEC60870_SinglePointInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_DoublePointInformation_Create(IEC60870_DoublePointInformation_t *self, + uint32_t ioa, int value, int quality); + +IEC60870_AsduResult_t IEC60870_DoublePointInformation_Get_From_Buffer(IEC60870_DoublePointInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalized_Create(IEC60870_MeasuredValueNormalized_t *self, + uint32_t ioa, int16_t value, int quality); + +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalized_Get_From_Buffer(IEC60870_MeasuredValueNormalized_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueScaled_Create(IEC60870_MeasuredValueScaled_t *self, + uint32_t ioa, int16_t value, int quality); + +IEC60870_AsduResult_t IEC60870_MeasuredValueScaled_Get_From_Buffer(IEC60870_MeasuredValueScaled_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueShort_Create(IEC60870_MeasuredValueShort_t *self, + uint32_t ioa, float value, int quality); + +IEC60870_AsduResult_t IEC60870_MeasuredValueShort_Get_From_Buffer(IEC60870_MeasuredValueShort_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_IntegratedTotals_Create(IEC60870_IntegratedTotals_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value); + +IEC60870_AsduResult_t IEC60870_IntegratedTotals_Get_From_Buffer(IEC60870_IntegratedTotals_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SinglePointWithCP56Time2a_Create(IEC60870_SinglePointWithCP56Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_SinglePointWithCP56Time2a_Get_From_Buffer(IEC60870_SinglePointWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_DoublePointWithCP56Time2a_Create(IEC60870_DoublePointWithCP56Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_DoublePointWithCP56Time2a_Get_From_Buffer(IEC60870_DoublePointWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP56Time2a_Create(IEC60870_MeasuredValueNormalizedWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP56Time2a_Create(IEC60870_MeasuredValueScaledWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueScaledWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP56Time2a_Create(IEC60870_MeasuredValueShortWithCP56Time2a_t *self, + uint32_t ioa, float value, int quality, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueShortWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP56Time2a_Create(IEC60870_IntegratedTotalsWithCP56Time2a_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP56Time2a_Get_From_Buffer(IEC60870_IntegratedTotalsWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SingleCommand_Create(IEC60870_SingleCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier); + +IEC60870_AsduResult_t IEC60870_SingleCommand_Get_From_Buffer(IEC60870_SingleCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_DoubleCommand_Create(IEC60870_DoubleCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier); + +IEC60870_AsduResult_t IEC60870_DoubleCommand_Get_From_Buffer(IEC60870_DoubleCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_StepCommand_Create(IEC60870_StepCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier); + +IEC60870_AsduResult_t IEC60870_StepCommand_Get_From_Buffer(IEC60870_StepCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalized_Create(IEC60870_SetpointCommandNormalized_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier); + +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalized_Get_From_Buffer(IEC60870_SetpointCommandNormalized_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SetpointCommandScaled_Create(IEC60870_SetpointCommandScaled_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier); + +IEC60870_AsduResult_t IEC60870_SetpointCommandScaled_Get_From_Buffer(IEC60870_SetpointCommandScaled_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SetpointCommandShort_Create(IEC60870_SetpointCommandShort_t *self, + uint32_t ioa, float value, int is_select, int qualifier); + +IEC60870_AsduResult_t IEC60870_SetpointCommandShort_Get_From_Buffer(IEC60870_SetpointCommandShort_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SingleCommandWithCP56Time2a_Create(IEC60870_SingleCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_SingleCommandWithCP56Time2a_Get_From_Buffer(IEC60870_SingleCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_DoubleCommandWithCP56Time2a_Create(IEC60870_DoubleCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_DoubleCommandWithCP56Time2a_Get_From_Buffer(IEC60870_DoubleCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_StepCommandWithCP56Time2a_Create(IEC60870_StepCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_StepCommandWithCP56Time2a_Get_From_Buffer(IEC60870_StepCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalizedWithCP56Time2a_Create(IEC60870_SetpointCommandNormalizedWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalizedWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandNormalizedWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SetpointCommandScaledWithCP56Time2a_Create(IEC60870_SetpointCommandScaledWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_SetpointCommandScaledWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandScaledWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SetpointCommandShortWithCP56Time2a_Create(IEC60870_SetpointCommandShortWithCP56Time2a_t *self, + uint32_t ioa, float value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_SetpointCommandShortWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandShortWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_InterrogationCommand_Create(IEC60870_InterrogationCommand_t *self, + uint32_t ioa, int qualifier); + +IEC60870_AsduResult_t IEC60870_InterrogationCommand_Get_From_Buffer(IEC60870_InterrogationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_CounterInterrogationCommand_Create(IEC60870_CounterInterrogationCommand_t *self, + uint32_t ioa, int qualifier, int freeze); + +IEC60870_AsduResult_t IEC60870_CounterInterrogationCommand_Get_From_Buffer(IEC60870_CounterInterrogationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_ReadCommand_Create(IEC60870_ReadCommand_t *self, + uint32_t ioa); + +IEC60870_AsduResult_t IEC60870_ReadCommand_Get_From_Buffer(IEC60870_ReadCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_ClockSynchronizationCommand_Create(IEC60870_ClockSynchronizationCommand_t *self, + uint32_t ioa, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_ClockSynchronizationCommand_Get_From_Buffer(IEC60870_ClockSynchronizationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_ResetProcessCommand_Create(IEC60870_ResetProcessCommand_t *self, + uint32_t ioa, int qualifier); + +IEC60870_AsduResult_t IEC60870_ResetProcessCommand_Get_From_Buffer(IEC60870_ResetProcessCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_TestCommandWithCP56Time2a_Create(IEC60870_TestCommandWithCP56Time2a_t *self, + uint32_t ioa, uint16_t sequence, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_TestCommandWithCP56Time2a_Get_From_Buffer(IEC60870_TestCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_StepPositionInformation_Create(IEC60870_StepPositionInformation_t *self, + uint32_t ioa, int value, int is_transient, int quality); + +IEC60870_AsduResult_t IEC60870_StepPositionInformation_Get_From_Buffer(IEC60870_StepPositionInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_BitString32_Create(IEC60870_BitString32_t *self, + uint32_t ioa, uint32_t value, int quality); + +IEC60870_AsduResult_t IEC60870_BitString32_Get_From_Buffer(IEC60870_BitString32_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SinglePointWithCP24Time2a_Create(IEC60870_SinglePointWithCP24Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_SinglePointWithCP24Time2a_Get_From_Buffer(IEC60870_SinglePointWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_DoublePointWithCP24Time2a_Create(IEC60870_DoublePointWithCP24Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_DoublePointWithCP24Time2a_Get_From_Buffer(IEC60870_DoublePointWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_StepPositionWithCP24Time2a_Create(IEC60870_StepPositionWithCP24Time2a_t *self, + uint32_t ioa, int value, int is_transient, int quality, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_StepPositionWithCP24Time2a_Get_From_Buffer(IEC60870_StepPositionWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP24Time2a_Create(IEC60870_Bitstring32WithCP24Time2a_t *self, + uint32_t ioa, uint32_t value, int quality, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP24Time2a_Get_From_Buffer(IEC60870_Bitstring32WithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP24Time2a_Create(IEC60870_MeasuredValueNormalizedWithCP24Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP24Time2a_Create(IEC60870_MeasuredValueScaledWithCP24Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueScaledWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP24Time2a_Create(IEC60870_MeasuredValueShortWithCP24Time2a_t *self, + uint32_t ioa, float value, int quality, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueShortWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP24Time2a_Create(IEC60870_IntegratedTotalsWithCP24Time2a_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP24Time2a_Get_From_Buffer(IEC60870_IntegratedTotalsWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_StepPositionWithCP56Time2a_Create(IEC60870_StepPositionWithCP56Time2a_t *self, + uint32_t ioa, int value, int is_transient, int quality, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_StepPositionWithCP56Time2a_Get_From_Buffer(IEC60870_StepPositionWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP56Time2a_Create(IEC60870_Bitstring32WithCP56Time2a_t *self, + uint32_t ioa, uint32_t value, int quality, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP56Time2a_Get_From_Buffer(IEC60870_Bitstring32WithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipment_Create(IEC60870_EventOfProtectionEquipment_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipment_Get_From_Buffer(IEC60870_EventOfProtectionEquipment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipment_Create(IEC60870_PackedStartEventsOfProtectionEquipment_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipment_Get_From_Buffer(IEC60870_PackedStartEventsOfProtectionEquipment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfo_Create(IEC60870_PackedOutputCircuitInfo_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfo_Get_From_Buffer(IEC60870_PackedOutputCircuitInfo_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Create(IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer(IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Create(IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer(IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Create(IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Get_From_Buffer(IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_PackedSinglePointWithSCD_Create(IEC60870_PackedSinglePointWithSCD_t *self, + uint32_t ioa, uint16_t status, uint16_t change, int quality); + +IEC60870_AsduResult_t IEC60870_PackedSinglePointWithSCD_Get_From_Buffer(IEC60870_PackedSinglePointWithSCD_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithoutQuality_Create(IEC60870_MeasuredValueNormalizedWithoutQuality_t *self, + uint32_t ioa, int16_t value); + +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithoutQuality_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithoutQuality_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_Bitstring32Command_Create(IEC60870_Bitstring32Command_t *self, + uint32_t ioa, uint32_t value); + +IEC60870_AsduResult_t IEC60870_Bitstring32Command_Get_From_Buffer(IEC60870_Bitstring32Command_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_Bitstring32CommandWithCP56Time2a_Create(IEC60870_Bitstring32CommandWithCP56Time2a_t *self, + uint32_t ioa, uint32_t value, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_Bitstring32CommandWithCP56Time2a_Get_From_Buffer(IEC60870_Bitstring32CommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_EndOfInitialization_Create(IEC60870_EndOfInitialization_t *self, + uint32_t ioa, int cause); + +IEC60870_AsduResult_t IEC60870_EndOfInitialization_Get_From_Buffer(IEC60870_EndOfInitialization_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_TestCommand_Create(IEC60870_TestCommand_t *self, + uint32_t ioa); + +IEC60870_AsduResult_t IEC60870_TestCommand_Get_From_Buffer(IEC60870_TestCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_DelayAcquisitionCommand_Create(IEC60870_DelayAcquisitionCommand_t *self, + uint32_t ioa, const IEC60870_CP16Time2a_t *delay); + +IEC60870_AsduResult_t IEC60870_DelayAcquisitionCommand_Get_From_Buffer(IEC60870_DelayAcquisitionCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_ParameterNormalizedValue_Create(IEC60870_ParameterNormalizedValue_t *self, + uint32_t ioa, int16_t value, int qualifier); + +IEC60870_AsduResult_t IEC60870_ParameterNormalizedValue_Get_From_Buffer(IEC60870_ParameterNormalizedValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_ParameterScaledValue_Create(IEC60870_ParameterScaledValue_t *self, + uint32_t ioa, int16_t value, int qualifier); + +IEC60870_AsduResult_t IEC60870_ParameterScaledValue_Get_From_Buffer(IEC60870_ParameterScaledValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_ParameterFloatValue_Create(IEC60870_ParameterFloatValue_t *self, + uint32_t ioa, float value, int qualifier); + +IEC60870_AsduResult_t IEC60870_ParameterFloatValue_Get_From_Buffer(IEC60870_ParameterFloatValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_ParameterActivation_Create(IEC60870_ParameterActivation_t *self, + uint32_t ioa, int qualifier); + +IEC60870_AsduResult_t IEC60870_ParameterActivation_Get_From_Buffer(IEC60870_ParameterActivation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_FileReady_Create(IEC60870_FileReady_t *self, + uint32_t ioa, uint16_t file_name, uint32_t file_length, int qualifier); + +IEC60870_AsduResult_t IEC60870_FileReady_Get_From_Buffer(IEC60870_FileReady_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SectionReady_Create(IEC60870_SectionReady_t *self, + uint32_t ioa, uint16_t file_name, int section, uint32_t section_length, int qualifier); + +IEC60870_AsduResult_t IEC60870_SectionReady_Get_From_Buffer(IEC60870_SectionReady_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_FileCallOrSelect_Create(IEC60870_FileCallOrSelect_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier); + +IEC60870_AsduResult_t IEC60870_FileCallOrSelect_Get_From_Buffer(IEC60870_FileCallOrSelect_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_FileACK_Create(IEC60870_FileACK_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier); + +IEC60870_AsduResult_t IEC60870_FileACK_Get_From_Buffer(IEC60870_FileACK_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_FileLastSegmentOrSection_Create(IEC60870_FileLastSegmentOrSection_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier, int checksum); + +IEC60870_AsduResult_t IEC60870_FileLastSegmentOrSection_Get_From_Buffer(IEC60870_FileLastSegmentOrSection_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_FileSegment_Create(IEC60870_FileSegment_t *self, + uint32_t ioa, uint16_t file_name, int section, const uint8_t *data, uint16_t data_length); + +IEC60870_AsduResult_t IEC60870_FileSegment_Get_From_Buffer(IEC60870_FileSegment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_FileDirectory_Create(IEC60870_FileDirectory_t *self, + uint32_t ioa, uint16_t file_name, uint32_t file_length, int status, const IEC60870_CP56Time2a_t *creation_time); + +IEC60870_AsduResult_t IEC60870_FileDirectory_Get_From_Buffer(IEC60870_FileDirectory_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_QueryLog_Create(IEC60870_QueryLog_t *self, + uint32_t ioa, uint16_t file_name, const IEC60870_CP56Time2a_t *start_time, const IEC60870_CP56Time2a_t *stop_time); + +IEC60870_AsduResult_t IEC60870_QueryLog_Get_From_Buffer(IEC60870_QueryLog_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +int IEC60870_FileSegment_Get_Max_Data_Size(const IEC60870_AppLayerParameters_t *parameters); + +/* 内部协作:仅供库实现。 */ +const IEC60870_InformationObjectVTable * IEC60870_InformationObject_Get_Descriptor(uint8_t type_id); + +uint8_t IEC60870_InformationObject_Get_Type(const IEC60870_InformationObject_t *object); + +IEC60870_AsduResult_t IEC60870_InformationObject_Encode( + const IEC60870_InformationObject_t *object, const IEC60870_AppLayerParameters_t *parameters, + int is_sequence, uint8_t *buffer, uint16_t capacity, uint16_t *length); + +/* 内部协作:仅供库实现。 */ +IEC60870_AsduResult_t IEC60870_InformationObject_Decode(uint8_t type_id, uint32_t ioa, + const uint8_t *payload, uint16_t length, void *object, size_t object_size); +``` + +
+ + + +## 18. IEC60870_Slave.c + +实现:[IEC60870_Slave.c](../../common/IEC60870_Slave.c);声明:[IEC60870_Slave.h](../../common/IEC60870_Slave.h)。 + +共用连接接口通过函数指针分派,供101/104接入复用。发送返回布尔值;true表示已接手,不等于主站ACK。确认和终止先修改ASDU再提交,提交失败不恢复字段。 + +调用约定:self 必须指向接入层提供的原接口;空接口或缺少对应函数指针时发送返回 false、参数查询返回 NULL。104 直接使用 `&event.connection`,不由业务自行填充 object 或函数指针。true 前接入实现已复制或消耗 ASDU;false 的含义由接入层定义,104 使用 Slave_Get_Last_Submit_Result。共用层不保存请求、不进行重试,也不实现网络服务。 + +| 函数 API 接口名 | 功能作用 | +|---|---| +| `IEC60870_MasterConnection_Send_ASDU` | 分派普通ASDU发送,不修改公共头。 | +| `IEC60870_MasterConnection_Send_ACT_CON` | 分派激活确认,由对应协议设置COT=7及指定P/N后提交。 | +| `IEC60870_MasterConnection_Send_ACT_TERM` | 分派激活终止,由对应协议设置COT=10、P/N=0后提交。 | +| `IEC60870_MasterConnection_Get_Application_Layer_Parameters` | 返回协议接入提供的只读参数,空接口或未实现时返回NULL。 | + + +
+完整函数原型(4 个;参数和返回类型见声明) + +```c +#include "IEC60870_Slave.h" + +bool IEC60870_MasterConnection_Send_ASDU(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu); + +bool IEC60870_MasterConnection_Send_ACT_CON(IEC60870_MasterConnection_t *self, + IEC60870_Asdu_t *asdu, bool negative); + +bool IEC60870_MasterConnection_Send_ACT_TERM(IEC60870_MasterConnection_t *self, + IEC60870_Asdu_t *asdu); + +const IEC60870_AppLayerParameters_t * IEC60870_MasterConnection_Get_Application_Layer_Parameters( + IEC60870_MasterConnection_t *self); +``` + +
+ + + +## 19. IEC104_Slave.c + +实现:[IEC104_Slave.c](../../104/IEC104_Slave.c);声明:[IEC104_Slave.h](../../104/IEC104_Slave.h)。 + +Slave是业务统一入口,持有静态池TCP服务。NextEvent自动安装共用应答接口;业务仅管理事件存储,不手动绑定TCP句柄或generation。需要完整事件的延迟任务用Copy_Event复制,自引用不得直接赋值搬移;只需部分请求数据时可使用下述纯值身份入口。确认/终止失败仍保留已修改的ASDU。 + +调用约定:NextEvent 的 READY 交付原通知,I_FRAME 还需检查 asdu_result;完整对象合法性再由 Asdu_Parse 验证。Copy_Event 返回 true/false,失败不修改目标;它修复自引用、保留历史提交结果,不提交报文或改变请求身份。Get_Last_Submit_Result 在首次提交前、空/无效接口时为 ERROR。 + +业务模块通过 Get_Event_Identity 校验事件的服务归属、身份一致性和有效I帧视图,再保存返回的来源身份;失败保持输出不变。它允许同一服务的历史连接通知,但拒绝其他服务、已销毁复用实例和未经Copy_Event修复的I帧副本。无需业务读取事件的 `_owner`、`_owner_serial` 或 `_generation`。 + +推进任务前使用 Check_Request:参数/状态查询失败返回 REQUEST_ERROR;Slave尚有未读通知优先返回 REQUEST_WAIT_EVENTS;读空后,来源/实例/代次不匹配或连接未激活返回 REQUEST_STALE,否则返回 REQUEST_READY。零初始化身份可表示尚未绑定。READY不预留窗口或队列容量,也不代替业务自己的阶段、撤销和超时判断;同一代次STOP后又START必须逐条处理通知,不能只根据最终READY恢复旧任务。两接口均不执行网络I/O或消费通知。 + +Close_Connection 按事件保存的原服务实例及 generation 定位:其他实例为 INVALID_ARGUMENT,当前无连接为 NOT_CONNECTED,已有新连接时旧代次为 STALE_GENERATION。Get_Application_Layer_Parameters 对有效已创建句柄即可使用,不要求主站在线;返回参数不表示应答准入。 + +需要精简延迟任务时,可只保存Get_Event_Identity取得的纯值身份及业务需要的请求字节,随后使用Send_Response/Try_Send_Response提交独立ASDU,或Close_Connection_By_Identity关闭原连接。三个身份字段保持原样,self须为原服务;发送拒绝旧实例/旧代次,关闭也不能作用于新连接。Send_Response沿普通HIGH路径,Try_Send_Response保持不排队语义;两者直接返回提交结果,不修改某个事件的最近结果。调用方仍须先处理通知并检查撤销、期限及业务状态,身份本身不表示请求仍在执行。 + +Init 失败不销毁对象;Create 后、Init 失败后和 Close 后仍可 Get_Status。Destroy 成功才丢弃句柄;运行中返回 INVALID_STATE,未读通知返回 EVENTS_PENDING。旧裸指针不能识别静态地址复用,必须由应用停止使用;旧事件则通过实例编号及原 generation 防止误发到新会话。 + + + +### 同步拒绝当前请求 + +`IEC104_Slave_Reject_Request(self, event, cause)`用于分派当前I帧时生成原文错误应答。`cause`只接受44..47,分别表示未知类型、COT、CA和IOA;接口不代替业务决定拒绝原因。仅复制当前ASDU并替换COT、置P/N=1,保留TEST、OA、CA、VSQ和全部对象字节。不修改输入事件、ASDU视图或该事件的`Get_Last_Submit_Result`,调用方直接检查返回值。原文副本沿已有普通发送入口直接接手或进入HIGH,不新增待应答队列,不做内容去重,不使用NORMAL。 + +调用限于当前`NextEvent`交付后的同步分派,并且必须在读取后续事件之前完成。原服务/会话及启停状态仍须有效;未读ACTIVATED/DEACTIVATED会使接口返回ERROR,后续仅有普通I帧或RECORD不要求先读空。`pending_state_event_count`只反映未读启停通知,不能发现应用已经消费的历史STOP/START;**禁止保存旧事件、读完后续通知后再调用本接口**。需要延迟应答时使用`Copy_Event`或保存原身份,自行处理STOP取消、撤销和期限,再经`Check_Request`及普通发送入口提交;最终READY也不能恢复已经取消的旧任务。 + +| 返回值 | 含义及调用方处理 | +|---|---| +| `IEC104_TCP_SUBMIT_OK` | 已复制接手,不代表写完或收到ACK;不得重复提交 | +| `IEC104_TCP_SUBMIT_WAIT` | HIGH剩余容量不足,本条未接手;示例丢弃新请求并累计overloads、dropped_requests,保留连接及已接手应答 | +| `IEC104_TCP_SUBMIT_ERROR` | 参数/事件/cause非法、原会话失效或存在未读启停通知;不按容量不足计数,继续处理通知及相应失效原因 | +| `IEC104_TCP_SUBMIT_FATAL` | 发送故障,可能已经写出字节;不得重试,继续排空RECORD/DISCONNECTED等收尾通知 | + +示例在总召、计数量、校时和遥控均未接手后显式调用此接口;Slave不会根据类型自动吞掉或拒绝NextEvent交付的请求。HIGH真正满时允许丢弃本条错误应答,因此不承诺所有负载下都有响应;已接手条目不因新兜底请求到达而被覆盖或主动断链。 + + + +### 共用站点表 + +一个Slave可以注册多个逻辑站点,各业务按CA取得同一节点及应用context。组织方式参考[iec104-python的Server站点管理](https://github.com/Fraunhofer-FIT-DIEN/iec104-python/blob/main/src/Server.cpp):按CA查找并拒绝重复注册。本实现采用调用方节点链表,不使用C堆或为每个站点复制通信资源;没有额外的固定站点数量上限,实际受1..65534地址范围与可用内存约束。 + +每个`IEC104_Station_t`须零初始化,注册成功后直到`Is_Reusable`返回true均须保持节点及context的地址和寿命稳定,不得赋值搬移、清零或直接改字段。CA及context在注册时指定;context内容、IOA点表、权限和业务状态归应用管理。同一Slave内已注册CA唯一,不同Slave可以注册相同CA,但不能共用同一个节点。查找和注册为线性扫描,适合当前少量站点规模。 + +`Get_Station`只查具体的已注册站点,0、65535及未知CA返回NULL。地址检查与业务分派分离的思路亦见[lib60870的CA许可检查](https://github.com/mz-automation/lib60870/blob/master/lib60870-C/src/iec60870/cs104/cs104_slave.c)。NextEvent不自动丢弃或应答报文:业务负责对象布局、COT、CA、权限及广播规则。总召开启`use_station_registry`后已接入按CA查找和广播遍历,其他业务可复用同一基础接口。 + +延迟业务保存节点前调用`Acquire_Station`,清理完成后调用`Release_Station`。Remove立即使节点退出CA查找、计数和普通遍历;已有引用仍可读取原CA/context,最后一个引用和快照归还后才清零节点。Remove返回OK不等于可释放存储,须检查`Is_Reusable`。同CA可立即注册另一个节点;`Get_Id/Get_Station_By_Id`以不复用的注册编号定位原节点,避免旧事务查到替代站点。引用仍不能代替原连接身份检查。 + +`StationSnapshot_Begin/Next/End`提供跨循环遍历:Begin记录注册编号上界,Next跳过已删除的未来站点,并排除后来新增或同CA重建的节点;不复制或冻结IOA和点值。快照须零初始化、地址稳定、配对End,Next返回NULL也不自动结束。任一快照活动时退役节点延迟回收,因此调用方不得提前释放这些节点。需要跨End保留Next返回值时另行Acquire。`Lock_Stations/Unlock_Stations`仍作为显式冻结集合的兼容接口,锁定期间Add/Remove返回BUSY;总召广播不使用集合锁,合法多目标请求在调用方状态区中持有各节点引用,错误回复遍历使用快照。所有操作在同一事件循环串行执行,不是线程锁。 + +Close/Init不改变站点表。Slave_Destroy要求站点全部Remove、引用及快照归还、集合锁解除,否则返回INVALID_STATE。Remove不释放应用context,也不撤回已接手报文;已选中的总召可持原引用完成。若业务要求立即停止该事务,应先显式取消并处理FINISHED;不能靠删除再注册同一CA清空旧报文。 + +`IEC104_StationResult_t`须显式比较:OK成功;INVALID_ARGUMENT参数/配对非法;DUPLICATE_CA已有注册CA;ALREADY_REGISTERED节点仍由某服务持有;NOT_FOUND节点不属于本服务或重复删除;BUSY显式集合锁阻止修改;LIMIT引用/快照/锁计数或注册编号耗尽。失败不改变注册关系;零计数Release/Unlock返回INVALID_ARGUMENT。 + +Windows x64 GCC测量(默认4任务、4业务事件):节点40字节、快照32字节、Slave40字节、总召1240字节、任务168字节、紧凑请求40字节。多目标广播另需调用方提供每站48字节状态区,13站为624字节;不扩大任务或事件队列。HIGH/NORMAL容量及覆盖策略不变,此数值不是目标板RAM实测。 + +后续业务的接入顺序为:解析请求 → 按CA查站点 → 检查该站点的业务权限 → 如需延迟则Acquire → 用context内的IOA点表处理 → 沿原连接应答 → 完成清理后Release。主动上报使用所属节点的CA构造ASDU;不同CA下相同IOA属于不同点。 + +| 函数 API 接口名 | 功能作用 | +|---|---| +| `IEC104_Slave_Add_Station` | 借用零初始化节点并按CA注册到末尾;拒绝重复CA,不分配堆。 | +| `IEC104_Slave_Remove_Station` | 立即退出路由,引用和快照归还后回收;OK后仍须检查Is_Reusable。 | +| `IEC104_Slave_Get_Station` | 按具体CA取得借用节点;未知CA或广播返回NULL。 | +| `IEC104_Slave_Get_Station_Count` | 取得当前注册数量,无效服务返回0。 | +| `IEC104_Slave_Get_Station_By_Id` | 按注册编号查当前或被保留的退役节点,不误取同CA的新站。 | +| `IEC104_Station_Get_Id` | 取得注册编号,NULL或已回收节点返回0。 | +| `IEC104_Station_Is_Registered` | 是否仍参与CA查找;NULL返回false。 | +| `IEC104_Station_Is_Reusable` | 节点是否已回收,可由调用方复用/释放;NULL返回false。 | +| `IEC104_Slave_StationSnapshot_Begin` | 开始一次不阻止增删的遍历,固定注册编号上界。 | +| `IEC104_Slave_StationSnapshot_Next` | 取得下一个未删除的原有站点,借用指针。 | +| `IEC104_Slave_StationSnapshot_End` | 归还快照并尝试回收退役节点。 | +| `IEC104_Slave_Next_Station` | 按注册顺序遍历;current=NULL取首站,末尾/外来节点返回NULL。 | +| `IEC104_Slave_Lock_Stations` | 固定当前集合,阻止增删与服务销毁,不冻结点值。 | +| `IEC104_Slave_Unlock_Stations` | 归还一次集合借用,零计数释放报错。 | +| `IEC104_Slave_Acquire_Station` | 增加节点引用,延迟删除后的存储回收;拒绝外来节点及溢出。 | +| `IEC104_Slave_Release_Station` | 归还一次节点引用,零计数释放报错。 | +| `IEC104_Station_Get_Common_Address` | 取得CA,退役但被保留的节点仍可读;NULL或已回收时返回0。 | +| `IEC104_Station_Get_Context` | 取得应用context,退役但被保留的节点仍可读;NULL或已回收时返回NULL。 | +| `IEC104_Slave_Create` | 从静态池取得子站门面及其私有TCP服务;无malloc、无网络操作。 | +| `IEC104_Slave_Destroy` | 已关闭、事件读空、站点移除且集合锁归还后释放服务;失败保留对象。 | +| `IEC104_Slave_Config_Default` | 设置默认监听及协议配置。 | +| `IEC104_Slave_Init` | 通过内部TCP服务初始化监听和队列;不创建业务回调线程。 | +| `IEC104_Slave_Prepare` | 只读查询socket、定时及本地工作条件,继续使用原token规则。 | +| `IEC104_Slave_Process` | 有界推进一批就绪事件,不等待,业务随后读取NextEvent。 | +| `IEC104_Slave_Poll` | 组合准备、有限等待与推进,适合独立事件循环。 | +| `IEC104_Slave_NextEvent` | 按FIFO交付事件,I帧自动提供ASDU视图和绑定原服务实例/代次的发送接口;EMPTY/ERROR保持输出。 | +| `IEC104_Slave_Copy_Event` | 复制事件及原文,重建目标视图和接口的自引用,保留原身份供延迟应答。 | +| `IEC104_Slave_Get_Event_Identity` | 校验事件并取得来源身份;允许同实例历史会话通知,失败保持输出。 | +| `IEC104_Slave_Check_Request` | 统一检查未读通知、原服务实例、连接代次和激活状态,返回业务推进条件。 | +| `IEC104_Slave_Get_Last_Submit_Result` | 按事件connection查询最近发送的OK/WAIT/ERROR/FATAL,仅WAIT可等待容量重试。 | +| `IEC104_Slave_Try_Send_ASDU` | 沿事件原connection尝试不排队提交ASDU,直接返回并记录OK/WAIT/ERROR/FATAL;WAIT不保存原文,应用重读后重试,普通共用发送接口不变。 | +| `IEC104_Slave_Send_Response` | 使用保存的原事件身份提交独立ASDU;保留原实例/代次检查,直接发送或进入HIGH,返回提交结果,不要求保留完整事件。 | +| `IEC104_Slave_Try_Send_Response` | 使用保存的原事件身份尝试不排队提交;WAIT不缓存数据,原队列、窗口和期限规则保持不变。 | +| `IEC104_Slave_Reject_Request` | 同步拒绝当前事件,原文仅改COT=44..47/P/N=1,复用HIGH;直接返回提交结果,输入及最近提交结果不变。 | +| `IEC104_Slave_Get_Application_Layer_Parameters` | 返回标准104静态只读参数,供主动上报构造ASDU。 | +| `IEC104_Slave_Enqueue_Event` | 校验ASDU对象并提交NORMAL,支持离线缓存和原有覆盖策略。 | +| `IEC104_Slave_Get_Remote_Address` | 取得当前主站地址副本,不借用内部连接对象。 | +| `IEC104_Slave_Close_Connection` | 按原事件关闭指定连接,内部提取身份;旧代次不能关闭新连接。 | +| `IEC104_Slave_Close_Connection_By_Identity` | 按保存的原事件身份关闭原连接;与完整事件入口同样检查服务实例/代次,保留监听和NORMAL。 | +| `IEC104_Slave_Get_Status` | 取得服务、连接及两级队列的只读快照。 | +| `IEC104_Slave_Close` | 关闭服务并保留收尾通知;继续NextEvent读空,再Destroy。 | + + +
+完整函数原型(41 个;参数和返回类型见声明) + +```c +#include "IEC104_Slave.h" + +IEC104_StationResult_t IEC104_Slave_Add_Station(IEC104_Slave_t *self, + IEC104_Station_t *station, uint16_t common_address, void *context); +IEC104_StationResult_t IEC104_Slave_Remove_Station(IEC104_Slave_t *self, IEC104_Station_t *station); +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); +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); +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); +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); +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); +uint16_t IEC104_Station_Get_Common_Address(const IEC104_Station_t *station); +void *IEC104_Station_Get_Context(const IEC104_Station_t *station); + +IEC104_Slave_t * IEC104_Slave_Create(void); + +IEC104_TcpResult_t IEC104_Slave_Destroy(IEC104_Slave_t *self, IEC104_TcpError_t *error); + +void IEC104_Slave_Config_Default(IEC104_SlaveConfig_t *config); + +IEC104_TcpResult_t IEC104_Slave_Init(IEC104_Slave_t *self, + const IEC104_SlaveConfig_t *config, IEC104_TcpError_t *error); + +IEC104_TcpResult_t IEC104_Slave_Prepare(const IEC104_Slave_t *self, + IEC104_TcpWaitInfo_t *info, IEC104_TcpError_t *error); + +IEC104_TcpResult_t IEC104_Slave_Process(IEC104_Slave_t *self, + const IEC104_TcpReady_t *ready, IEC104_TcpError_t *error); + +IEC104_TcpResult_t IEC104_Slave_Poll(IEC104_Slave_t *self, + uint32_t max_wait_ms, IEC104_TcpError_t *error); + +IEC104_TcpEventResult_t IEC104_Slave_NextEvent(IEC104_Slave_t *self, IEC104_SlaveEvent_t *event); + +bool IEC104_Slave_Copy_Event(IEC104_SlaveEvent_t *destination, const IEC104_SlaveEvent_t *source); + +bool IEC104_Slave_Get_Event_Identity(const IEC104_Slave_t *self, + const IEC104_SlaveEvent_t *event, IEC104_SlaveEventIdentity_t *identity); + +IEC104_SlaveRequestState_t IEC104_Slave_Check_Request(const IEC104_Slave_t *self, + const IEC104_SlaveEventIdentity_t *identity); + +IEC104_TcpSubmitResult_t IEC104_Slave_Get_Last_Submit_Result( + const IEC60870_MasterConnection_t *connection); + +IEC104_TcpSubmitResult_t IEC104_Slave_Try_Send_ASDU( + IEC60870_MasterConnection_t *connection, const IEC60870_Asdu_t *asdu); + +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); + +IEC104_TcpSubmitResult_t IEC104_Slave_Reject_Request(IEC104_Slave_t *self, + const IEC104_SlaveEvent_t *event, int cause); + +const IEC60870_AppLayerParameters_t * IEC104_Slave_Get_Application_Layer_Parameters( + const IEC104_Slave_t *self); + +IEC104_TcpSubmitResult_t IEC104_Slave_Enqueue_Event(IEC104_Slave_t *self, IEC60870_Asdu_t *asdu); + +IEC104_TcpResult_t IEC104_Slave_Get_Remote_Address(const IEC104_Slave_t *self, + IEC104_TcpPeerInfo_t *peer, IEC104_TcpError_t *error); + +IEC104_TcpResult_t IEC104_Slave_Close_Connection(IEC104_Slave_t *self, + const IEC104_SlaveEvent_t *event, IEC104_TcpError_t *error); + +IEC104_TcpResult_t IEC104_Slave_Close_Connection_By_Identity(IEC104_Slave_t *self, + const IEC104_SlaveEventIdentity_t *identity, IEC104_TcpError_t *error); + +IEC104_TcpResult_t IEC104_Slave_Get_Status(const IEC104_Slave_t *self, + IEC104_TcpServerStatus_t *status, IEC104_TcpError_t *error); + +IEC104_TcpResult_t IEC104_Slave_Close(IEC104_Slave_t *self, IEC104_TcpError_t *error); +``` + +
+ + + +## 20. IEC60870_Codec.c + +实现:[IEC60870_Codec.c](../../common/IEC60870_Codec.c);声明:[IEC60870_Codec.h](../../common/IEC60870_Codec.h)。 + +101/104共用的ASDU基础解析。应用层参数定义在 `IEC60870_Types.h`;检查公共头和最低载荷,不依赖104帧、TCP或串口,也不执行具体对象或业务校验。 + +调用约定:两个函数均返回 1/0。Valid 检查参数布局;Parse_ASDU_Header 还检查完整公共头及已知类型最低载荷,不要求精确对象区长度,也允许未知类型和零对象在此层保留。info 借用参数和原文,失败保持输出;需要执行具体业务时继续 Asdu_Parse 和应用级校验。104 参数从接入层获取,公共解析器不内置 104 默认配置。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC60870_AppLayerParameters_Valid` | 检查Type ID/VSQ为1、COT/CA为1或2、IOA为1..3、默认OA为0..255、总长至少容纳头且不超过254;合法返回1,非法或NULL返回0。 | +| `IEC60870_Parse_ASDU_Header` | 首参显式传入应用层参数,按可变COT/CA/IOA宽度解析公共头及最低载荷;结果借用参数与报文,COT为1字节时OA输出0,失败保持输出不变。 | + + +
+完整函数原型(2 个;参数和返回类型见声明) + +```c +#include "IEC60870_Codec.h" + +extern int IEC60870_AppLayerParameters_Valid(const IEC60870_AppLayerParameters_t *parameters); + +extern int IEC60870_Parse_ASDU_Header(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *asdu, + uint16_t asdu_len, + IEC60870_AsduInfo_t *asdu_info); +``` + +
+ + + + +## 21. IEC104_Interrogation.c + +实现:[IEC104_Interrogation.c](../../104/business/IEC104_Interrogation.c);声明:[IEC104_Interrogation.h](../../104/business/IEC104_Interrogation.h)。 + +本节保留总召原12个接口的接入说明;Create的实现已移至GeneralInterrogation.c。请求追加freeze/data_cause,数据COT应取data_cause。内部头声明Create_With_Rules及Execute_Action,仅供种类文件协作,业务不直接调用。默认实例池为TCP服务容量的两倍,同一Slave允许总召、计数量各一实例,广播数组不得重叠。 + +事件式总召模块:处理Type100、QOI=20..36,沿原connection按ACT_CON → 多类型数据批次 → ACT_TERM发送;确认和终止可排入HIGH,数据尝试不排队提交。支持主站COT=8撤销及业务超时。Slave负责通信,应用通过NextEvent取得BEGIN/NEED_DATA/FINISHED并显式应答;不注册业务回调。完整顺序见[总召接入流程](IEC104_完整调用流程.md#flow-interrogation)。 + +### 配置、所有权与事件 + +Create复制配置、借用Slave,一个Slave只绑定一个总召实例,且须在消费Slave通知前创建。每个实例至多一个未终结总召:空闲时接受任意合法QOI=20..36并保存为START;从保存起到终结,所有新激活都否定确认,不区分QOI,不替换原请求。Process有业务事件空位后生成BEGIN,应用接受后才提交肯定ACT_CON。总召忙只约束Type100激活,其他业务及匹配撤销继续分派。所有调用在同一事件循环串行执行,不创建线程或socket,无malloc。详细状态和调度规则见[总召模块设计](IEC104_Interrogation_设计架构.md)。 + +Handle_Event通过Slave_Get_Event_Identity统一验证来源并绑定服务身份;Process及应用应答入口通过Slave_Check_Request判断是否应先处理通知或终结失效会话。总召不直接访问Slave事件内部身份字段,也不依赖TCP状态快照的组合条件;总召阶段、业务期限和FINISHED交付仍由本模块管理。 + +| IEC104_InterrogationConfig_t字段 | 默认值及范围 | 含义 | +| --- | --- | --- | +| common_address | 1;1..65534 | 不使用站点表且station=NULL时的兼容站地址;其他模式忽略 | +| max_objects_per_asdu | 127;1..127 | 单批对象数上限,实际还受249字节容量限制 | +| use_station_registry | false | true按请求CA查找共用站点,65535逐站遍历;此时station须NULL | +| broadcast_states / broadcast_capacity | NULL / 0 | 调用方提供的IEC104_InterrogationStationState_t数组及项数,多目标广播须足够容纳本轮目标 | +| begin_timeout_ms | 15000ms;0禁用 | BEGIN入队至应用应答,含未取事件的等待 | +| data_timeout_ms | 15000ms;0禁用 | 每次NEED_DATA入队至首次有效批次尝试/Complete_Data/Fail_Request;有效批次发送WAIT也停止本批计时,每批重新开始 | +| total_timeout_ms | 0(默认禁用) | 启用后从BEGIN生成至ACT_TERM被发送层接手,包含应用取数和发送等待,不包含生成BEGIN前等事件空位,不等待ACK | +| station | NULL | 兼容固定站绑定,Create持有引用至Destroy;删除后不接新请求,已接受任务可完成 | + +新应用先Slave_Add_Station,再设置`config.use_station_registry=true`;CA只在注册时指定。多目标广播配置broadcast_states/broadcast_capacity,按“全部确认、逐站数据、全部终止”共用一个任务。数组借用至Destroy,地址稳定、期间不可读写或与其他实例重叠;NULL/0须配对,内部字段无需初始化。容量不足会在任何肯定确认前整轮否定,不部分执行;单目标无需此数组。BEGIN携带`request.station_id`,应用需保留context时按编号Acquire,使用结束后Release。`use_station_registry=false`保留原common_address或固定station模式;true与非NULL station组合拒绝创建。配置布局改变,库与调用方须一起重新编译。 + +三项期限是独立于链路t0..t3的可配置业务策略,默认值不是标准规定的总召时限。执行总期限默认禁用,BEGIN和每批取数期限仍各为15000ms,底层链路期限不受影响。BEGIN成功生成时启动已启用的应用接受及执行总期限;此前START等业务FIFO空位不计业务期限,但已占用总召资格,须及时读取历史FINISHED。总期限设置为非零后仍为固定整体期限,不因提交新批次或忙应答而延长。用Config_Default初始化,再按业务需要修改。 + +| 类型或字段 | 约定 | +| --- | --- | +| IEC104_InterrogationRequest_t | CA/OA/QOI/TEST、generation/receive_serial、station_id及对象数上限;CA为当前目标站,station_id为注册编号(裸CA模式为0),qualifier为20全站或21..36分组 | +| IEC104_InterrogationEvent_t | 纯值,可赋值及跨循环保存;无Slave事件的自引用,不需要Copy_Event | +| instance_id / request_id / batch_index | 实例、请求和批次身份,只读,调用应答接口时原样传回;拒绝重复、迟到和取消后的应答 | +| BEGIN | 按qualifier检查数据源并接受/拒绝;可延至首个NEED_DATA建立游标,不预先采样全部数据 | +| NEED_DATA | 应用选择本批IOA和类型,读取当前值/品质后组包;未就绪时保存事件,稍后提交,不重复生成请求 | +| FINISHED | reason说明终结原因,带已提交批次/对象进度;应用停止对应采样任务并清理资源,不需要应答 | + +每个已取出的BEGIN保证且仅保证一次FINISHED;BEGIN未生成或未取出便取消时直接移除工作,不生成FINISHED。参数校验、总召忙和满槽拒绝不交付BEGIN。取消移除未取出的工作事件,但保留所有FINISHED。模块不保存应用session或数据源指针;遍历状态及业务资源归应用所有,建议按实例和请求编号管理。 + +IOA及分组成员由调用者决定,协议库不规定点表或强制固定IOA集合。NEED_DATA及每次WAIT重试都读取当前值;Submit_Batch不缓存待发数据,OK才固定帧顺序。点值更新及突发发布须与总召读值及提交串行化,具体排序保证及持续突发可能推迟总召的边界见[总召模块设计](IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。 + +编译实现文件时可覆盖IEC104_INTERROGATION_CAPACITY(默认IEC104_TCP_SERVER_CAPACITY)、IEC104_INTERROGATION_TASK_CAPACITY(默认4)和IEC104_INTERROGATION_EVENT_CAPACITY(默认4,最小2)。任务池保存至多一轮总召及短应答,没有后续总召等待队列或数据批次缓冲。任务只保存原事件身份及10字节Type100原文,不保存完整SlaveEvent或自引用视图。单目标生成BEGIN时在FIFO预留FINISHED;多目标由每站状态区保留终结记录,NextEvent随空位逐个交付,2槽FIFO也能无损处理13站。网络流程结束后进入FINISHING,读完终结事件才释放该任务;此时仍忙拒绝新激活。只有任务及事件均为空才允许Destroy;退出须Cancel → NextEvent读完并清理 → Destroy,再销毁Slave。测量见[固定存储](IEC104_Interrogation_设计架构.md#2-对象身份与固定存储)。 + +### 广播公共地址 + +开启站点表时,多目标Type100广播(CA=65535)先按注册顺序完成全部BEGIN/ACT_CON,再逐站完成数据,最后发送全部ACT_TERM。每站独立BEGIN/FINISHED、request_id和计时。接收时持有目标集合;尚未选入BEGIN便删除的站跳过,新增或同CA替代节点下轮才参加。已选中尤其已确认的站持原引用完成或明确失败,不因删除而静默跳过。只占一个任务槽,另需每站轻量状态区。没有注册站时回复COT=46并保留65535;单目标保持原流程。 + +广播每站的确认、数据、终止和错误应答使用该站实际CA,保留OA/TEST/QOI和原连接身份。每次选站时更新紧凑请求的CA,同一站的发送重试沿用它,输入Slave事件不修改。忙/错误广播也用一个遍历任务逐站回复;无空槽保存游标时整体丢弃新广播并计数,不先发送部分站的应答。未知具体CA按原值回COT=46;校验优先级仍为COT、CA、IOA、QOI/P/N。Type101等其他类型继续返回IGNORED。 + +广播三阶段顺序与[c104的Server.cpp](https://github.com/Fraunhofer-FIT-DIEN/iec104-python/blob/main/src/Server.cpp)一致。对象寿命使用调用方节点及显式引用,保留本库的WAIT重试、业务事件、QOI=20..36及撤销/失败收尾规则;并不复制C++对象模型或动态分配方式。 + +### 数据批次及返回值 + +Submit_Batch接受应用构造的非空ASDU,参数须2/2/3,COT=请求qualifier(全站20,分组21..36)、P/N=0,CA必须等于当前任务的request.common_address,OA/TEST与请求一致;提交65535或其他站CA返回ERROR并保留交互。支持Type 1/3/5/7/9/11/13/20/21,SQ=0/1,批间允许切换类型,最多249字节且不超配置对象数。Slave_Try_Send_Response按原身份尝试提交,当前发送缓冲及HIGH/NORMAL未提交条目均为空、链路和窗口允许才接手;否则WAIT,不缓存数据。OK后应用可推进游标;WAIT保留事件和游标,网络推进后重读当前值重试。Add返回FULL时提交已有非空批次,未加入对象留给下批;数据耗尽用Complete_Data。 + +组号与IOA的对应关系由应用维护,模块没有内置点表,也不校验点是否属于请求的组。未配置组可Reply_Begin(false);接受的空组仍可正常确认并终止。ACT_CON/ACT_TERM的COT保持7/10,回显原QOI。 + +| 返回类型 | 值及语义 | +| --- | --- | +| IEC104_InterrogationEventResult_t | EVENT_HANDLED=1表示请求/通知已处理,包含拒绝或畸形/满槽丢弃(查看rejected、dropped_requests);EVENT_IGNORED=0交公共入口或后续处理器;EVENT_ERROR=-1参数或处理失败 | +| IEC104_InterrogationNextResult_t | NEXT_READY=1提供一个业务事件;NEXT_EMPTY=0或NEXT_ERROR=-1保持输出不变 | +| IEC104_InterrogationReplyResult_t | REPLY_OK=1已接手应答;REPLY_WAIT=0先推进网络、消费Slave通知,保留事件和游标,数据批次重新取值后重试;REPLY_STALE=-2丢弃旧应答;REPLY_ERROR=-1修正参数/批次或处理查询错误 | + +应答只接受已经NextEvent取出且尚未回答的BEGIN或NEED_DATA,按实例、请求和批次身份匹配原任务,不依赖任务是否位于队首。Submit_Batch的REPLY_OK表示数据已被发送层接手,其余应答仍由Process推进;均不表示写完或ACK。REPLY_WAIT可能来自Slave FIFO未读空,Submit_Batch还可能因发送背压返回WAIT;不能跳过STOP/START或撤销请求。有效交互还会检查期限,到期即终结当前业务并返回STALE,不能用迟到应答绕过超时;确认前保留TCP,调用方仍须驱动Process发送否定确认。参数或批次校验REPLY_ERROR不消费交互,可修正后重试;Submit_Batch的发送ERROR若确认为正常STOP拒绝,先返回WAIT等待通知,随后FINISHED(STOPPED),其他发送ERROR/FATAL仍以SEND_ERROR终结并返回STALE。NEED_DATA阶段Fail_Request即使关闭连接失败仍取消任务、保留FINISHED,此时也不可重试。不要把任意非零结果当作成功。 + +### 主站撤销 + +Handle_Event接受COT=6和8。撤销按原连接代次、目标CA、OA、TEST和QOI匹配,包括已选中但BEGIN尚未生成的START,以及已确认但未取数/等终止的站。定向撤销匹配站,其他站继续;广播撤销停止整轮,按各仍活动的站回复肯定COT=9。尚未选入BEGIN的未来站及已结束站不补发确认;已删除但仍执行的原站可匹配。撤销先于忙拒绝处理,被忙拒绝的新QOI没有可撤销任务。 + +匹配后停止原任务,旧工作事件应答返回STALE,丢弃模块尚未提交的批次;原任务槽改为肯定DEACT_CON(COT=9、P/N=0),将其索引移到单帧应答队尾,条件允许时立即提交,否则由Process重试。不会再提交该任务的ACT_TERM。已交付BEGIN则生成一次FINISHED(DEACTIVATED),应用据此停止采样并清理资源;无需额外撤销回调或应用确认。BEGIN从未交付时只移除工作事件。FINISHED说明业务任务已停止,不说明撤销确认已发出或已ACK。 + +已被发送层接手的帧不能撤回,仍在撤销确认之前按原顺序发送。队列已满或原总召不在队首时仍能匹配撤销,不需要新增任务槽。未匹配、重复撤销或原ACT_TERM已被接手时,按单帧应答规则提交或保存否定COT=9;错误CA/IOA仍优先使用46/47。COT=8仅撤销对应总召,不停用整个TCP传输;Slave的DEACTIVATED通知表示STOPDT,对应总召终结原因是STOPPED。本地Cancel不回复DEACT_CON。 + +### 异常、调度及状态 + +| 情况 | 行为 | +| --- | --- | +| COT不是6/8、CA既非本站也非65535、IOA不是0 | 依次回45/46/47、P/N=1;广播请求的应答CA改为本站地址 | +| 激活QOI不在20..36、P/N非法或已有未终结总召 | 否定ACT_CON,不交付BEGIN;忙时相同和不同QOI均拒绝 | +| 空闲时收到合法激活 | 有空槽则建立唯一START任务;Process生成BEGIN后由应用决定 | +| 撤销QOI不在20..36、P/N非法或未匹配 | 否定DEACT_CON,当前总召不变 | +| 匹配的主站撤销 | 只停止匹配任务;已交付BEGIN才有FINISHED(DEACTIVATED),提交或保存肯定DEACT_CON | +| Reply_Begin(false) | FINISHED(REJECTED),排队否定ACT_CON,连接保持 | +| 需要新增总召但任务池满 | 尝试提交否定ACT_CON;无法安全提交/保存则丢新请求并计数,保留已有任务与TCP | +| 否定/错误应答未接手且任务池满 | 丢弃该新请求,增加overloads和dropped_requests,不越过旧REPLY,不取消已有任务 | +| 非100或公共ASDU视图不可用 | 安全读取Type后返回IGNORED;公共入口先过滤不可用视图,底层帧错误由Receiver/连接层处理 | +| Type100唯一对象完整,但SQ=1或有多余尾字节 | 回否定7/9,COT/CA/IOA错误优先45/46/47;应答规范为10字节、VSQ=1,保留OA/TEST/IOA/QOI;不执行激活/撤销 | +| Type100零对象/多对象等无法可靠应答的布局错误 | 计入rejected后HANDLED丢弃;不生成应答,不影响已有总召及其期限 | +| Submit_Batch非法 | REPLY_ERROR,保留NEED_DATA供修正,连接保持 | +| BEGIN阶段Fail_Request | 原槽转否定ACT_CON,保留连接及其他任务,FINISHED(SOURCE_ERROR) | +| START/CONFIRM阶段期限到点 | 原槽转否定ACT_CON,保留连接及其他任务;已交付BEGIN才有FINISHED(BEGIN_TIMEOUT / TOTAL_TIMEOUT) | +| NEED_DATA阶段Fail_Request或DATA/TERMINATE超时 | 关闭原连接、取消本模块任务,FINISHED(SOURCE_ERROR / DATA_TIMEOUT / TOTAL_TIMEOUT),不发成功ACT_TERM | +| STOPDT / 断线 / Cancel | FINISHED(STOPPED / DISCONNECTED / CANCELLED),未交付工作事件移除 | +| Destroy时尚有任务或业务事件 | false,保持状态;必须先取消和读取终结事件 | +| 发送ERROR且确认为原连接正常STOP拒绝、通知待处理 | 保留任务并让出,由DEACTIVATED终结为STOPPED;Submit_Batch返回WAIT,不算发送背压 | +| 其他发送ERROR/FATAL、会话变化、查询失败 | 取消并分别终结为SEND_ERROR、CONNECTION_CHANGED、SERVICE_ERROR | + +Handle_Event仅在空闲时保存合法总召,忙拒绝及可应答的畸形请求只保存应答;对单帧应答,只有Slave通知已读空、原会话仍有效且没有更早REPLY时,才沿现有HIGH接口立即尝试提交。使用请求副本,不修改调用方事件;OK后不再入任务池,WAIT或暂不能提交时保存,满槽则只丢新请求并累计dropped_requests。布局错误不取消已有总召或刷新其期限;规范化副本只能用于否定应答。合法匹配撤销复用原槽,因此即使满池也能保存确认。丢弃的请求没有应答,主站可能等待超时或重试;EVENT_HANDLED不承诺主站已收到响应。 + +调用IEC104_Interrogation_Process(self)即可推进保存的任务。REPLY按形成顺序发送;正常总召与短应答轮转,总召等应用/事件空位不阻塞短应答。每站保持ACT_CON → 数据 → ACT_TERM,多目标还保证全部确认先于数据、全部数据先于终止。ACT_TERM接手后终结,不等ACK。广播某站拒绝、超时或定向撤销后继续其他站;整轮结束并读完FINISHED才接受新激活。此前忙时拒绝的请求须由主站重新发起。 + +确认前后以肯定ACT_CON被发送层接手为边界。Reply_Begin(true)只进入CONFIRM;发送WAIT时总超时仍可改为否定确认,原槽复用并排到已有REPLY之后。已接手但尚未写出或ACK的肯定确认也算提交成功,DATA/TERMINATE失败仍关闭原连接。Process检测到确认前超时后重新检查Slave通知,在本轮继续调度否定确认;发送WAIT保留应答,不再累计已终结请求的业务超时。终结原因保留为具体TIMEOUT或SOURCE_ERROR,不统一改成REJECTED;未取出的BEGIN移除且不生成FINISHED。 + +Process在任务为空、全部等待应用/事件空位、发送WAIT或达到2倍任务容量步数时返回,余下本地工作由need_process提示。确认、终止及短应答WAIT保留当前发送项,下次调用优先尝试最早REPLY;DATA只生成NEED_DATA,应用Submit_Batch直接提交。广播共用任务遍历三个阶段,每站独立业务期限;启用总期限时,等待其他站的已确认目标也受检查,到期关闭原连接。保存业务事件期间须持续推进网络,并重读、重试数据WAIT。 + +调度只改变模块尚未提交任务的先后,已经被HIGH队列或发送缓冲接手的帧仍按原顺序发送,不能被新应答越过,也不能通过Cancel撤回。COMPLETED仅表示ACT_TERM提交成功。 + +Process每一步先检查Slave通知,再检查业务期限和选择任务;有未读通知即让出,包含本次推进期间新产生的通知。若已收到撤销/STOP/断线则按对应原因结束,不再重复生成超时事件。单帧应答的调度不会重置活动总召的期限;总期限与阶段期限都到点时优先报告TOTAL_TIMEOUT。 + +发送返回ERROR时额外复核正常STOP状态,三个提交入口共用此判断;不因任意新通知而忽略错误。OK照常记账,WAIT让出,FATAL仍失败。STOP拒绝的等待不累计waits、不设置data_send_waiting、不停止取数计时;完整条件见[提交结果处理](IEC104_Interrogation_设计架构.md#72-process中的选择规则)。 + +等待前重新Get_Status;need_process为true时立即推进,不能用!waiting替代。timer_active为true时,以timeout_remaining_ms限制Poll/应用等待上限,0表示立即推进。查询只读取状态和时间,不执行发送或超时处理;模块没有自动定时线程。BEGIN尚未生成时没有业务计时器,应继续读业务事件并驱动Process。确认前超时保留连接;肯定确认提交后的超时关闭原连接,会中断该连接其他业务。三项期限可以配置或逐项设0禁用,不增加独立的断链策略开关。 + +data_timeout_ms只计算NEED_DATA入队至首次有效批次尝试、结束或失败。有效批次因发送背压WAIT后,data_send_waiting置位,本批取数计时停止且不恢复,下一批NEED_DATA重新计时。无效批次或未读Slave通知导致的提前WAIT不停止计时;显式启用的total_timeout_ms及底层链路期限仍有效。 + +业务事件释放空位或应答接手后重查need_process;发送WAIT后不因保留任务而持续置位。每次Slave_Poll/Process推进网络并读空Slave通知后仍须调用总召Process重试,即使need_process为false。Get_Status实时检查Slave FIFO,未读通知使events_pending为true、need_process为false,先转交STOP/撤销等通知。外部事件循环同时遵守Slave_Prepare的等待条件;need_process仅表示需要尝试本地推进,不保证发送成功。新增状态字段后调用方须与库一起重新编译。 + +数据WAIT时,还须在网络推进后重读并重试应用持有的NEED_DATA,不能等待新事件或need_process置位;Process不保存或代为重发本批。 + +| 状态 | 含义 | +| --- | --- | +| generation / active / gi_pending | 已消费的连接代次、激活状态、是否存在唯一未终结总召(含BEGIN尚未生成) | +| phase | 当前站START / CONFIRM / DATA / TERMINATE,收尾FINISHING;仅有应答REPLY,无任务IDLE | +| waiting / application_waiting | 等发送容量、应用应答或业务FIFO空位;后者表示等待应用首次有效应答。总召等待时仍可调度单帧应答,waiting不表示所有任务都无法推进 | +| data_send_waiting | 有效批次曾遇发送WAIT;应用仍持有NEED_DATA,须在网络推进后重新取值提交 | +| need_process | 应立即推进本地任务、到期期限或失效会话;未读Slave通知优先,发送WAIT后等网络推进再重试 | +| events_pending / pending_events | Slave未读通知标志 / 未取出的业务事件数(含状态区待交付FINISHED,可大于FIFO容量) | +| timer_active / timeout_remaining_ms | 是否存在启用的业务期限 / 最近期限距查询时刻的剩余毫秒数 | +| pending_requests | 总召、FINISHING及排队单帧应答数量(含撤销确认);FINISHING的gi_pending虽为false,仍忙拒绝新激活 | +| batch_index / objects_submitted | 优先报告活动总召已提交的数据批次/对象数;仅有单帧应答时报告最早应答保留的进度,无任务时为0;发送WAIT不增加 | +| submitted / gi_submitted | 发送层已接手ASDU数 / 已提交ACT_TERM的总召数 | +| waits / cancelled / rejected / overloads | 发送WAIT、取消任务、拒绝及任务容量超限次数;rejected含无法可靠应答的畸形请求;确认前失败计入rejected,超时同时计入timeouts,不计入cancelled | +| negative_submitted / dropped_requests | 发送层已接手的否定应答数 / 无法安全提交或保存而丢弃的新请求数;前者不等于主站已接收 | +| deactivated / timeouts | 成功匹配的主站撤销数 / 执行超时数 | +| last_finish_reason | 最近合法总召的终结原因,BEGIN尚未交付也会更新 | + +旧begin/read_batch/finish和parameter字段已移除;用业务事件与应答接口替代。模块仍提供12个公开函数,Process只接收self。当前只保留一个未终结总召,忙时否定后续激活,不自动执行此前被拒绝的请求。已删除queue_timeout_ms、QUEUED/QUEUE_TIMEOUT、queued_interrogations和queue_timeouts;调用方删除相关字段及分支,使用Config_Default初始化配置并重新编译。negative_submitted和dropped_requests继续记录否定提交与满槽丢弃;FINISH_OVERLOAD及FINISH_INVALID_REQUEST保留枚举值,满槽或畸形新请求不以此终结已有任务。 + +### 函数功能 + +| 函数 | 功能与约定 | +| --- | --- | +| `IEC104_Interrogation_Config_Default` | CA=1、每批127对象,接受/取数期限各15000ms,执行整体期限0(禁用);NULL无操作。 | +| `IEC104_Interrogation_Create` | 复制配置并借用Slave;非法配置、重复绑定、池满或实例编号耗尽返回NULL。 | +| `IEC104_Interrogation_Destroy` | 无协议任务且无业务事件时归还槽;否则false且不修改状态,不关闭Slave。 | +| `IEC104_Interrogation_Cancel` | 取消未提交任务,为已交付BEGIN保留FINISHED(CANCELLED);不补发终止、不撤回已接手帧。 | +| `IEC104_Interrogation_Handle_Event` | 按FIFO接收Slave通知,空闲时保存总召、忙时否定新激活,处理匹配撤销及会话变化;单帧应答可安全提交时立即尝试,满槽拒绝/计数丢弃;其他I帧IGNORED。 | +| `IEC104_Interrogation_Process` | 自动推进当前就绪的单帧应答与总召,直到空闲或需等待;每步检查Slave通知与期限。true可为空闲/等待/取消/超时,false为参数、组包、关闭连接或服务查询错误。 | +| `IEC104_Interrogation_NextEvent` | 取最早的业务事件,仅READY写输出;无网络操作。 | +| `IEC104_Interrogation_Reply_Begin` | 接受或拒绝已取出的BEGIN;接受后Process才可提交ACT_CON。 | +| `IEC104_Interrogation_Submit_Batch` | 应答NEED_DATA,验证并立即尝试不排队提交非空ASDU;OK推进进度,WAIT保留事件重读再试,参数ERROR可修正,发送故障终结为STALE。 | +| `IEC104_Interrogation_Complete_Data` | 应答NEED_DATA声明本站数据耗尽,可复用取数游标;多目标全部数据结束后才逐站ACT_TERM及FINISHED。 | +| `IEC104_Interrogation_Fail_Request` | 报告数据失败:BEGIN转否定ACT_CON并保留连接,NEED_DATA关闭原连接;均终结为SOURCE_ERROR,关闭失败也交付终结事件。 | +| `IEC104_Interrogation_Get_Status` | 取得状态、累计统计及距最近业务期限的剩余毫秒;失败保持输出,不推进业务。 | + +
+完整函数原型(12 个) + +```c +#include "IEC104_Interrogation.h" + +void IEC104_Interrogation_Config_Default(IEC104_InterrogationConfig_t *config); + +IEC104_Interrogation_t *IEC104_Interrogation_Create(IEC104_Slave_t *slave, + const IEC104_InterrogationConfig_t *config); + +bool IEC104_Interrogation_Destroy(IEC104_Interrogation_t *self); + +void IEC104_Interrogation_Cancel(IEC104_Interrogation_t *self); + +IEC104_InterrogationEventResult_t IEC104_Interrogation_Handle_Event( + IEC104_Interrogation_t *self, const IEC104_SlaveEvent_t *event); + +bool IEC104_Interrogation_Process(IEC104_Interrogation_t *self); + +IEC104_InterrogationNextResult_t IEC104_Interrogation_NextEvent( + IEC104_Interrogation_t *self, IEC104_InterrogationEvent_t *event); + +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Reply_Begin( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, bool accept); + +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Submit_Batch( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, + const IEC60870_Asdu_t *asdu); + +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Complete_Data( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event); + +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Fail_Request( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event); + +bool IEC104_Interrogation_Get_Status(const IEC104_Interrogation_t *self, + IEC104_InterrogationStatus_t *status); +``` + +
+ + + +## 22. IEC104_GeneralInterrogation.c + +实现:[IEC104_GeneralInterrogation.c](../../104/business/IEC104_GeneralInterrogation.c)。保留原`IEC104_Interrogation_Create(slave, config)`签名和总召行为,内部传入总召规则。原声明继续由Interrogation.h提供;工程必须把这个新的源文件加入构建。 + + + +## 23. IEC104_CounterInterrogation.c + +实现:[IEC104_CounterInterrogation.c](../../104/business/IEC104_CounterInterrogation.c);声明:[IEC104_CounterInterrogation.h](../../104/business/IEC104_CounterInterrogation.h)。 + +```c +#include "IEC104_CounterInterrogation.h" +IEC104_Interrogation_t *IEC104_CounterInterrogation_Create(IEC104_Slave_t *slave, + const IEC104_InterrogationConfig_t *config); +typedef bool (*IEC104_CounterInterrogationAction_t)(void *context, + const IEC104_InterrogationRequest_t *request); +IEC104_InterrogationReplyResult_t IEC104_CounterInterrogation_Execute_Begin( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, + IEC104_CounterInterrogationAction_t action, void *context); +``` + +Create使用共用配置并返回共用驱动句柄,同一Slave可以同时存在总召及计数量实例。读取沿用Reply_Begin、Submit_Batch、Complete_Data;FRZ=1..3通过Execute_Begin先校验再执行一次同步动作,WAIT/STALE/ERROR不执行回调。动作成功后只发ACT_CON即完成,TEST动作拒绝;传输失败不撤销已经执行的动作。回调失败必须保证未改变计数器,不得重入或等待外部I/O。完整约定见[计数量设计](IEC104_CounterInterrogation_设计架构.md)。 + + + +## 24. IEC104_ClockSynchronization.c + +实现:[IEC104_ClockSynchronization.c](../../104/business/IEC104_ClockSynchronization.c);声明:[IEC104_ClockSynchronization.h](../../104/business/IEC104_ClockSynchronization.h)。独立静态池默认与TCP服务容量相同,每实例4个任务槽,只使用共用站点表。完整校验、时间计算及广播规则见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md)。 + +Create复制配置、借用Slave,每服务至多一个实例,须在消费通知前创建。`master_utc_offset_minutes`默认480、范围-1439..1439,不含SU;`execute_timeout_ms`默认15000,0禁用执行期限。原事件不修改,可靠的单对象错误应答规范为16字节、VSQ=1,仅改COT/P/N及广播目标CA,保留CP56原文。COT/CA/IOA错误依次优先45/46/47,其余可对应的非法请求否定7,不能对应唯一完整对象时丢弃。 + +请求结构是回调期间借用的纯值,包含原`master_time`、站点/请求身份、`accepted_ms`、`base_ms`及对应base_ms的`utc_ms/local_time`。先完整校验IV/SU/时区及本地日历上界,再用同一次64位单调采样检查期限和补偿受理后的滞留;网络、Slave通知FIFO及硬件写入耗时不补偿。action只由Execute_Next同步调用,不存储回调或context;不得重入本模块或推进Slave事件循环,只有Get_Status允许在action中只读调用。 + +action的true/false分别报告成功/未报告成功,false不保证硬件没有改变。调用过action的肯定或否定ACT_CON均走Try;WAIT只重试确认,不重复动作,也不再计算执行期限。校验错误走普通HIGH。应答按请求FIFO提交;Execute_Next可以越过较早REPLY执行最早EXECUTE,其确认仍不插队。广播只执行一次动作,按受理快照逐个目标CA确认;删除的未提交目标跳过,新注册站不参与。满槽丢新并计overloads/dropped_requests,保持原连接和任务。 + +| 返回类型 | 含义 | +| --- | --- | +| EventResult | ERROR=-1为入口错误;IGNORED=0非本模块请求;HANDLED=1含拒绝/丢弃,不代表已应答 | +| ExecuteResult | ERROR=-1为参数、重入或服务查询错误;EMPTY=0无可执行请求但可能仍有应答(包括刚因期限/时间错误转为否定);WAIT=1先读Slave通知;OK=2调用过一次action,不代表动作成功或确认到达主站 | +| Process / Get_Status / Destroy | bool结果;Process不执行action,Get_Status只读且失败不改输出,Destroy要求先清理任务 | + +| 状态 | 含义 | +| --- | --- | +| active / generation / events_pending | 本模块已消费的会话状态及是否须先处理Slave通知 | +| execute_pending / pending_requests | 是否存在待执行动作 / 尚未交接完成的任务数 | +| confirm_pending | 包含正在执行的action,以及已执行但尚未交接完成的ACT_CON;应用据此协调新的带时标发布 | +| need_process / timer_active / timeout_remaining_ms | 是否需立即推进Process,以及最近执行期限的剩余毫秒;查询不触发发送或超时 | +| accepted / executed / synchronized | 合法请求受理数 / 实际action调用数 / action报告成功数;广播动作只计一次 | +| submitted / negative_submitted | 已交接ASDU数 / 其中否定应答数;广播每个实际提交目标分别计数,不代表ACK | +| rejected / waits / cancelled | 请求拒绝、发送WAIT及任务取消统计;取消不能证明设时或发送从未发生 | +| overloads / dropped_requests / timeouts / time_errors | 满槽及丢新、未执行到期、倒退或时间换算错误;0超时配置不禁用范围检查 | + +Try等到当前HIGH/NORMAL可提交条件满足,不隔离之后新增的NORMAL;持续发布可能推迟确认。`confirm_pending`供发布方串行协调,不是跨线程锁。STOP/断线/Cancel只取消未交接任务和站点遍历,不回滚时钟、不撤回已编号I帧。同内容的新请求不去重,重发旧时标可能再次回拨。 + +```c +#include "IEC104_ClockSynchronization.h" + +typedef bool (*IEC104_ClockSynchronizationAction_t)(void *context, + const IEC104_ClockSynchronizationRequest_t *request); + +void IEC104_ClockSynchronization_Config_Default(IEC104_ClockSynchronizationConfig_t *config); +IEC104_ClockSynchronization_t *IEC104_ClockSynchronization_Create(IEC104_Slave_t *slave, + const IEC104_ClockSynchronizationConfig_t *config); +bool IEC104_ClockSynchronization_Destroy(IEC104_ClockSynchronization_t *self); +void IEC104_ClockSynchronization_Cancel(IEC104_ClockSynchronization_t *self); +IEC104_ClockSynchronizationEventResult_t IEC104_ClockSynchronization_Handle_Event( + IEC104_ClockSynchronization_t *self, const IEC104_SlaveEvent_t *event); +bool IEC104_ClockSynchronization_Process(IEC104_ClockSynchronization_t *self); +IEC104_ClockSynchronizationExecuteResult_t IEC104_ClockSynchronization_Execute_Next( + IEC104_ClockSynchronization_t *self, IEC104_ClockSynchronizationAction_t action, void *context); +bool IEC104_ClockSynchronization_Get_Status(const IEC104_ClockSynchronization_t *self, + IEC104_ClockSynchronizationStatus_t *status); +``` + + + +## 25. IEC104_Command.c + +实现:[IEC104_Command.c](../../104/business/IEC104_Command.c);声明:[IEC104_Command.h](../../104/business/IEC104_Command.h)。只接手Type45/46,默认均须先选择;不支持的类型返回IGNORED。实例池默认等于TCP服务容量,每实例一个事务和4个独立应答槽。完整状态机与配置见[遥控设计](IEC104_Command_设计架构.md),接入顺序见[遥控调用流程](IEC104_完整调用流程.md#flow-command)。 + +| 接口 | 契约 | +|---|---| +| Config_Default / Create | 配置默认决定15s、选择30s、执行60s;决定/执行可用0禁用。成功发ACT_TERM、失败默认不发。Create校验类型掩码,拒绝同Slave重复实例,须在消费Slave通知前创建 | +| Handle_Event | 校验来源,逐条消费生命周期通知;合法I帧先检查期限再匹配。识别后的拒绝或丢弃也返回HANDLED,不再回44 | +| Process | 让出未读Slave通知,检查期限,按全局FIFO交接应答;WAIT保留队首,不调用应用动作 | +| NextEvent | 取得SELECT、EXECUTE或FINISHED纯值副本。工作事件取走后才可应答;离线仍能读取FINISHED | +| Accept | 校验会话、身份、阶段和期限,先消费事件再至多调用一次action;SELECT可传NULL,EXECUTE必须提供动作 | +| Reject | 消费工作事件,保存否定ACT_CON或UNKNOWN_IOA,并终结事务 | +| Report_Result | 仅接受已STARTED的EXECUTE事件,记录成功/失败并停止执行期限;可以早于执行确认交接,重复报告返回ERROR | +| Submit_Return_Info | 肯定执行确认交接后、Complete前提交一帧返回信息;Try返回OK才固定帧序,WAIT不接手ASDU、不占FIFO位置 | +| Complete | 必须先报告结果;关闭返回信息阶段,按策略由Process提交ACT_TERM。重复调用在尚未结束时返回ERROR,已结束则STALE | +| Get_Transaction | 任一属于当前事务的事件可查询;ENDED时active=false,槽释放后返回false并保持输出不变 | +| Get_Status | 查询通知、应答、期限、忙状态及统计,不触发到期迁移;发送WAIT后仍须在下一轮网络推进后重试Process | +| Cancel / Destroy | Cancel取消模块持有应答并保留应交付FINISHED,会话仍激活。Destroy要求事务和独立应答均已清空,不能在动作回调中执行 | + +应答返回值必须显式比较:`REPLY_OK=1`、`REPLY_WAIT=0`、`REPLY_ERROR=-1`、`REPLY_STALE=-2`。Accept的OK仅表示事件已消费,动作返回NOT_STARTED或FAILED仍是OK;确认可尚未发送。WAIT时事件/结果/ASDU未消费,应先读空Slave通知、推进网络后重试;STALE时不能再启动设备动作或补交该事务数据。参数非法、禁止的回调重入,以及有效事务中的重复报告/Complete返回ERROR;事务已经结束时返回STALE。 + +动作结果为NOT_STARTED(保证没有副作用)、STARTED(已生效/启动)或FAILED(尝试失败,可能有副作用)。回调不得推进Slave或发布上报,模块内只允许调用Get_Status/Get_Transaction;模块不保存回调。Accept后通过Get_Transaction判断是否仍在EXECUTING,再安排设备结果和返回信息。 + +返回信息须为监视方向Type1..21或30..40,COT=11或3、P/N=0,CA/TEST与命令一致,IOA可不同。使用原连接身份,经过模块FIFO与Try准入,不进入NORMAL、不跨重连保留;WAIT时应用保留完整ASDU。动作FAILED或事务STALE后仍需上报变化时,应用另用Enqueue_Event/COT=3。COT常量为`IEC60870_ASDU_COT_RETURN_INFO_REMOTE`(11)及`IEC60870_ASDU_COT_RETURN_INFO_LOCAL`(12)。 + +应用取走过工作事件的事务恰好交付一次FINISHED。除成功/设备失败等待最后应答交接外,其余终结立即可读;request保持最后接纳的工作请求,撤销和不匹配请求只影响应答原文。FINISHED已读或无需交付,且所有事务应答已交接/取消后才释放槽。`select_effect/execute_effect`表示动作可能留下副作用,FINISHED不保证设备空闲;应用须持有自己的站点引用到物理输出结束。正常退出依次Cancel、读完FINISHED、Slave_Close、读空并分派通知、Destroy、Remove_Station、Slave_Destroy。 + +```c +#include "IEC104_Command.h" + +void IEC104_Command_Config_Default(IEC104_CommandConfig_t *config); +IEC104_Command_t *IEC104_Command_Create(IEC104_Slave_t *slave, + const IEC104_CommandConfig_t *config); +bool IEC104_Command_Destroy(IEC104_Command_t *self); +void IEC104_Command_Cancel(IEC104_Command_t *self); +IEC104_CommandEventResult_t IEC104_Command_Handle_Event(IEC104_Command_t *self, + const IEC104_SlaveEvent_t *event); +bool IEC104_Command_Process(IEC104_Command_t *self); +IEC104_CommandNextResult_t IEC104_Command_NextEvent(IEC104_Command_t *self, + IEC104_CommandEvent_t *event); +IEC104_CommandReplyResult_t IEC104_Command_Accept(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandAction_t action, void *context); +IEC104_CommandReplyResult_t IEC104_Command_Reject(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandRejectCause_t cause); +IEC104_CommandReplyResult_t IEC104_Command_Report_Result(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result); +IEC104_CommandReplyResult_t IEC104_Command_Submit_Return_Info(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, const IEC60870_Asdu_t *asdu); +IEC104_CommandReplyResult_t IEC104_Command_Complete(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event); +bool IEC104_Command_Get_Transaction(const IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandTransactionInfo_t *info); +bool IEC104_Command_Get_Status(const IEC104_Command_t *self, IEC104_CommandStatus_t *status); +``` diff --git a/IEC60870-Slave/.build/command-docs/IEC104_完整调用流程.md.before b/IEC60870-Slave/.build/command-docs/IEC104_完整调用流程.md.before new file mode 100644 index 00000000..3da25c9c --- /dev/null +++ b/IEC60870-Slave/.build/command-docs/IEC104_完整调用流程.md.before @@ -0,0 +1,1075 @@ +# IEC104 完整调用流程 + +本文依据 2026-10-30 当前工作区的协议库源码,说明从服务创建到关闭释放的调用顺序,与 [API 接口汇总](IEC104_API接口汇总.md) 配合阅读。范围仅为库本身:不包含示范程序、测试程序或应用业务实现。接口汇总按实现文件查函数,本文按运行过程追踪函数之间的关系。 + +主线为当前已实现的 **IPv4 TCP 子站**:应用通过 `IEC104_Slave_*` 接入,Slave内部持有并调用 `IEC104_TcpServer_*`,内部使用 Connection、SendManager、Link、Receiver、SendBuffer 和 AsduQueue。当前网络后端为 Windows;主站文件只提供 Link 层启停过程,未接入这一套 TCP 子站调度。 + +调用树中的 `[内部]` 表示对应 `.c` 内的静态辅助函数,不是应用可直接调用的 API。调用树表示主要步骤;具体的等待、失败和恢复分支在后文说明。 + +可先读[应用主线](#flow-1)和[对象关系](#layers),需要业务时读[总召接入](#flow-interrogation)、[时钟同步接入](#flow-clock-synchronization)和[遥控接入](#flow-command),再查[事件与 ASDU](#flow-6)、[应答与上报](#flow-7)、[容量等待](#flow-12)、[断线重连](#flow-13)、[自有事件循环](#flow-14)和[关闭释放](#flow-15)。 + +业务调用主线为 `Slave_Create → Slave_Config_Default/Init → Slave_Prepare/Process 或 Poll → Slave_NextEvent → 共用发送接口 → Slave_Close → 读空事件 → Slave_Destroy`。NextEvent 直接交付 ASDU 及原连接接口;延迟处理可用Slave_Copy_Event,也可保存来源身份及必要请求数据,通过Slave身份入口应答。以下展开 Slave 调用到的 TCP 内部流程;其中 `server` 和应答所用的 `generation` 由 Slave 维护。 + + + +## 1. 应用层完整主线 + +```text +IEC104_Slave_Create() + → IEC104_Slave_Config_Default() + → 按需要修改监听地址、端口和协议配置 + → IEC104_Slave_Init() + → 循环: + IEC104_Slave_Poll() + IEC104_Slave_NextEvent(),逐条读到 EMPTY + CONNECTED → 记录主站地址和连接代次;此时尚未启用数据传输 + ACTIVATED → 处理数据传输启用通知 + I_FRAME → 检查 asdu_result,完整解析并校验业务条件 + 通过 &event.connection 调用共用 Send_ASDU / ACT_CON / ACT_TERM + RECORD → 处理发送事实回执 + DEACTIVATED → 处理数据传输停用通知 + DISCONNECTED → 结束该 generation 的连接业务 + 有主动上报时构造 ASDU,调用 IEC104_Slave_Enqueue_Event() + 对此前返回 WAIT 且仍有效的业务提交,择机重试 + → IEC104_Slave_Close() + → IEC104_Slave_NextEvent(),处理收尾事件直到 EMPTY + → IEC104_Slave_Destroy() +``` + +以上是调用顺序伪代码。事件不会每轮都出现,排列也不是固定脚本;实际以 `NextEvent` 返回的 FIFO 顺序为准。 + +```mermaid +flowchart TD + A["Slave_Create / Config_Default / Init"] --> B["Slave_Poll:等待并推进一次"] + B --> C["NextEvent:逐条取事件"] + C --> D{"事件类型"} + D -->|I_FRAME| E["验证 ASDU / 通过 event.connection 应答"] + D -->|RECORD| F["读取本地完成和对端 ACK 事实"] + D -->|连接或启停| G["按 generation 处理状态变化"] + E --> C + F --> C + G --> C + C -->|EMPTY| H["提交主动上报 / 安排有效的 WAIT 重试"] + H --> I{"继续运行?"} + I -->|是| B + I -->|否| J["Close"] + J --> K["NextEvent 读完收尾事件"] + K --> L["Destroy"] +``` + +使用这条主线时: + +- 同一服务的全部接口在同一事件循环线程串行调用;`Create/Destroy` 也须跨服务串行。模块内的 BUSY 检查用于拒绝重入,不是线程锁。应用负责循环和业务执行。 +- `Create` 同时取得 Slave 槽及它私有的 TcpServer 槽,无动态内存分配和网络操作;取得失败返回 NULL。最后配对 `Destroy`。每个服务同时维护一条主站连接。 +- `Init` 成功即能缓存主动上报,连接尚未建立也可调用 `Enqueue_Event`。 +- `Poll` 返回后及时消费事件。仅反复调用 `Poll` 而不调用 `NextEvent`,会积压通知并触发后文的容量等待。 +- 子站业务包含 [IEC104_Slave.h](../../104/IEC104_Slave.h),即可使用服务、ASDU 和共用发送接口。底层的帧预留、完成提交、队列重绑定由内部调度完成。 + +各组返回值必须分别判断: + +| 接口组 | 成功或有结果 | 需要区别处理的结果 | +| --- | --- | --- | +| Init、Poll、Prepare、Process、Close 等 | `IEC104_TCP_OK`,数值为 0 | 错误时保存该次 `IEC104_TcpError_t`;客户端断开本身可正常返回 OK,由事件说明 | +| NextEvent | `IEC104_TCP_EVENT_READY` | EMPTY 表示已读空;ERROR 不是有效事件 | +| Slave_Enqueue_Event、底层 TcpServer_Send_Response | `IEC104_TCP_SUBMIT_OK` | WAIT 尚未接手;ERROR 为参数、代次或准入等错误;FATAL 须结合收尾事实处理 | +| 共用连接的Send_ASDU、Send_ACT_CON、Send_ACT_TERM | `true` | 104侧用Get_Last_Submit_Result区分WAIT/ERROR/FATAL;false不等于可以直接重试 | + +`SUBMIT_OK` 表示已经复制接手,应避免重复提交;`SUBMIT_WAIT` 表示未接手,由业务稍后重试。普通应答保留原文,总召数据使用不排队接口,WAIT后重读当前值再试。应答返回 FATAL 时可能已经写出部分甚至全部字节,不能直接按“完全未发送”处理。 + + + +### 1.1 Slave、共用发送接口与 TcpServer 的关系 + +```mermaid +flowchart TD + App["应用业务"] --> Slave["IEC104_Slave:生命周期、事件封装、连接身份绑定"] + App --> Common["IEC60870_MasterConnection:共用发送接口"] + Common -->|函数指针分派| Slave + Slave -->|内部持有指针| TCP["IEC104_TcpServer:监听、socket、FIFO、调度"] + TCP --> Conn["IEC104_Connection:接收、状态机、期限协调"] + Conn --> Rx["Receiver / Codec:成帧与校验"] + Conn --> Mgr["SendManager:组帧、发送记录、ACK"] + Mgr --> Link["Link:序号、窗口、启停、探测"] + Mgr --> Buffer["SendBuffer:完整 APDU 与写出偏移"] + Mgr --> Queue["AsduQueue:HIGH / NORMAL 原文"] + TCP --> Net["Net_Windows:非阻塞网络与单调时钟"] +``` + +`IEC104_Slave_t` 和 `IEC104_TcpServer_t` 都是不透明类型,完整结构分别定义在各自 `.c` 文件。Slave 内部保存 `IEC104_TcpServer_t *` 并调用公开函数;持有这种指针不需要知道结构大小。应用不能声明这两种结构的实体、访问成员、按值复制或互相强转,也不需要额外创建一个 TcpServer。 + +`IEC60870_Slave.c` 只实现四个 `IEC60870_MasterConnection_*` 函数的分派,不是第二套 104 服务。`IEC60870_MasterConnection_t` 是“向原主站应答”的接口值,由 Slave 放在接收事件中;它既不是 socket,也不负责启动、停止服务。ASDU 构造、解析仍使用 `IEC60870_*`,服务运行则统一使用 `IEC104_Slave_*`。 + +直接使用 TcpServer 是另一种较底层的接入方式:调用方自行解析原始事件并携带原 generation 提交应答。本文应用主线使用 Slave;下文出现 TcpServer 函数时,均表示它承接的内部步骤。 + +业务模块的公共校验也通过Slave完成:`IEC104_Slave_Get_Event_Identity`验证事件所属服务、实例和自引用视图,返回可保存的来源身份;`IEC104_Slave_Check_Request`统一查询请求会话是否仍可处理。前者允许同一服务的历史连接通知,后者才判断当前可用性,避免因连接已经变化而跳过FIFO中的收尾事件。 + +`Check_Request`返回`WAIT_EVENTS`时先逐条处理Slave通知,返回`STALE`时终结原业务,返回`ERROR`时处理查询或参数错误;只有`READY`才继续业务阶段。READY不保证发送容量,也不能替代业务撤销和期限判断。后续业务模块复用这两个接口,不读取事件的内部身份字段,也不自行组合TCP连接状态。 + +此门禁用于推进原请求,不得挡住Handle_Event正在消费的生命周期通知。遥控模块先按事件来源分派CONNECTED/ACTIVATED/DEACTIVATED/DISCONNECTED,按FIFO处理完后再推进事务;NextEvent中的FINISHED即使离线也必须能够读取,才能完成资源清理。 + +### 1.2 配置入口与默认值 + +运行配置先调用 `IEC104_Slave_Config_Default(&config)`,再修改所需字段,最后交给 Init。Init 复制配置;修改原配置变量不会改变已运行的服务。 + +| 配置位置 | 默认值或约束 | +| --- | --- | +| `config.bind_address` / `config.port` | `0.0.0.0:2404`;地址为数字 IPv4。端口 0 表示由系统选取,可从状态中的 `bound_port` 读取 | +| `config.protocol.link.k` / `w` | 12 / 8;均为 1~32767。发送还会受实际记录和缓冲容量限制 | +| `t1_ms` / `t2_ms` / `t3_ms` | 15000 / 10000 / 20000 毫秒;非零,且 `t2_ms < t1_ms` | +| `local_send_timeout_ms` / `stop_timeout_ms` | 默认各 15000 毫秒;非零。改动 t1 后不会自动重新计算这两个已填入的字段 | +| `high_priority_burst_limit` | 0:严格 HIGH 优先;非零:有 NORMAL 待发时限制连续 HIGH 帧数 | +| `IEC104_APP_LAYER_PARAMETERS_DEFAULT` | 定义在 [IEC104_Types.h](../../104/IEC104_Types.h):Type ID 1、VSQ 1、COT 2、默认 OA 0、CA 2、IOA 3、ASDU 最大 249 字节 | +| 静态存储容量 | 编译期由 [IEC104_TcpServer_Config.h](../../104/IEC104_TcpServer_Config.h) 配置,默认可创建 1 个服务。与上述运行参数分开 | + +104 专用的默认参数宏保留在 104 接入层。共用 `IEC60870_AppLayerParameters_t` 只描述布局,不替调用方选择传输配置。业务一般通过 `IEC104_Slave_Get_Application_Layer_Parameters` 或事件连接的对应 getter 取得标准 104 参数,无需自行维护宏副本。 + + + +### 1.3 接入独立总召模块 + +[IEC104_Interrogation.h](../../104/business/IEC104_Interrogation.h) 提供站总召、分组总召、主站撤销及业务超时处理,接口详见[总召API](IEC104_API接口汇总.md#file-21)。配置包括CA、每批对象上限和三项业务期限,不注册业务回调。Slave交付网络通知,总召模块交付业务事件;应用在同一事件循环中主动读取这两条FIFO,再显式接受请求、提交批次或报告结束。 + +总召内部已经接入上述来源和会话校验:Handle_Event保存经Slave验证的身份,Process及Reply_Begin/Submit_Batch/Complete_Data/Fail_Request在推进前检查请求状态。应用仍按下列流程驱动,无需为总召重复调用这些校验接口;应用只负责业务数据、接受决定及资源清理。 + +```text +Slave_Create + → Slave_Add_Station,注册应用提供的节点、CA和业务context + → Interrogation_Config_Default,设置use_station_registry=true、每批上限和业务期限 + → 多目标广播配置broadcast_states/broadcast_capacity(调用方数组借用至Destroy) + → Interrogation_Create(slave, &config),检查非NULL + → Slave_Init + → 循环: + Interrogation_Get_Status,按最近业务期限限制本轮等待 + Slave_Poll 或 Slave_Prepare/Process + Slave_NextEvent,逐条处理到EMPTY + → Interrogation_Handle_Event(gi, &transport_event) + → IGNORED的I帧继续交给其他业务处理器 + → 连接、启停和断线通知也须交付 + Interrogation_Process(gi) 自动推进当前就绪任务 + Interrogation_NextEvent,逐条处理业务事件: + BEGIN → 按request.qualifier检查数据源 → Reply_Begin(accept) + NEED_DATA → 应用选择本批IOA,读取当前值/品质并组包 → Submit_Batch;无剩余数据则Complete_Data + FINISHED → 清理对应请求资源,无需应答 + 数据尚未就绪:保存NEED_DATA,后续循环就绪时再应答 + Interrogation_Get_Status,安排下轮调度 + → Interrogation_Cancel + → Interrogation_NextEvent读空,处理FINISHED并释放应用资源 + → Slave_Close → Slave_NextEvent读空收尾通知 + → Interrogation_Destroy → Slave_Remove_Station → Slave_Destroy +``` + +必须在开始消费Slave通知前创建总召模块,一个Slave只绑定一个实例。配置按值复制,Slave须存活到总召Destroy之后;Init失败也要释放已创建实例。所有接口串行调用;异步采样结果须回到所属事件循环再提交。模块不创建线程,也不会调用应用函数。 + +上面的顺序使用共用站点表,CA只在注册时指定。多目标广播先确认全部站,再逐站取数,最后终止全部站;各站BEGIN可先于首条FINISHED。应用可在首个NEED_DATA建立唯一取数游标,Complete_Data返回OK后复用;异常时按FINISHED清理。需保留context则按station_id查原节点并Acquire,使用结束后Release。删除立即退出路由,已有引用结束后才可释放存储;同CA重建不改变旧数据源。兼容应用可保留use_station_registry=false的单CA或固定station模式。详见[共用站点表](IEC104_API接口汇总.md#station-registry)。 + +请求范围为Type100、SQ=0、一个对象、COT=6(激活)或8(撤销)、P/N=0、CA为本站地址或65535、IOA=0、QOI=20..36。QOI=20选择全站,21..36选择第1..16组。每个实例至多一个未终结总召:空闲时保存合法激活为START;从保存起到终结,相同和不同QOI的新激活均否定,不覆盖原QOI/OA/TEST及进度。被拒绝的请求不会自动执行,主站须在之后重新发起。总召忙仅约束Type100激活,Type101等其他业务继续分派,匹配的COT=8仍可撤销。详细设计见[总召模块设计](IEC104_Interrogation_设计架构.md)。Process在Slave通知FIFO读空后推进: + +```text +空闲收到激活 → START(已占用总召资格)→ 业务事件有空位 → BEGIN(开始已启用的业务期限) + ├─ Reply_Begin(false) → FINISHED(REJECTED);排队否定ACT_CON + └─ Reply_Begin(true) → Process提交ACT_CON(COT=7) + → NEED_DATA(batch_index=0) + Submit_Batch → 当场尝试不排队提交COT=请求QOI + → WAIT时保留事件与游标,重读当前值后重试 + → 提交OK后增加batch_index → 下一个NEED_DATA + Complete_Data → Process提交ACT_TERM(COT=10) + → FINISHED(COMPLETED) +BEGIN → Fail_Request → FINISHED(SOURCE_ERROR) → 否定ACT_CON,保留连接 +NEED_DATA → Fail_Request → 关闭原连接、取消 → FINISHED(SOURCE_ERROR) +已交付BEGIN后遇STOPDT/断线/Cancel → FINISHED(对应原因) +主站匹配的COT=8 → 只停止匹配项 → 已交付BEGIN才有FINISHED(DEACTIVATED);提交或保存肯定COT=9 +已有未终结总召 → 新激活转为否定ACT_CON,不建立第二个总召 +START/CONFIRM期限到期 → 否定ACT_CON,保留连接;已交付BEGIN才有FINISHED(对应超时原因) +DATA/TERMINATE期限到期 → 关闭原连接、取消 → FINISHED(DATA_TIMEOUT/TOTAL_TIMEOUT) +``` + +分组点表及IOA由应用维护,BEGIN不向库登记或冻结完整IOA列表。request.qualifier指定请求的组,未配置的组可Reply_Begin(false);接受的空组可在NEED_DATA直接Complete_Data,仍按ACT_CON → ACT_TERM结束。数据ASDU的COT必须等于请求QOI:全站为20,第1组为21,直至第16组为36;确认/终止仍用COT=7/10并回显原QOI。模块验证批次头部,但不判断某个IOA是否属于该组,点表筛选、遍历及取值由应用负责。 + +实际站CA为1..65534。多目标广播使用一个任务执行“全部ACT_CON → 逐站数据 → 全部ACT_TERM”,各站独立BEGIN/FINISHED及request_id;上图描述其中单站的先后关系。调用方状态区须容纳接收时的注册站数,不足时整轮否定,不部分执行。request.common_address是实际站CA,Submit_Batch拒绝65535及其他站CA。紧凑副本仍只保存原身份及10字节ASDU,原OA/TEST/QOI保持,输入事件不修改。新增/替代节点下轮参加,尚未选入BEGIN便删除的站跳过;已经选中尤其已确认的原站必须完成或明确终结。空站表广播及未知具体CA回复46。详见[广播设计](IEC104_Interrogation_设计架构.md#33-广播地址与本站地址),Type101等业务自行决定广播规则。 + +业务事件是纯值结构,可直接复制、存入应用队列或跨循环保存;这与含自引用、必须用Copy_Event复制的Slave事件不同。事件中的instance_id标识总召实例,request_id标识请求,batch_index标识当前批次。应答须原样传回取出的事件,不能修改标识。模块按这些身份定位原任务,不依赖任务是否在队首,处理其他请求的单帧应答不会使当前交互失效。重复应答、旧批次、旧请求、取消/撤销/超时后的事件以及静态槽复用前的事件均返回STALE。request还提供CA/OA/QOI/TEST、generation/receive_serial及每批上限,实际发送身份始终取模块保存的原请求。 + +| 业务事件 | 应用职责 | 对应应答 | +| --- | --- | --- | +| BEGIN | 按QOI检查数据源并决定是否接受;取数游标可延至首个NEED_DATA建立,不预先采样全部数据 | Reply_Begin(true/false),数据源失败也可Fail_Request | +| NEED_DATA | 应用选择本批IOA和类型,读取当前值/品质,用自己的缓冲构造ASDU;未就绪时保留事件,继续驱动网络 | Submit_Batch、Complete_Data或Fail_Request,三者只选其一 | +| FINISHED | 停止对应采样任务,按实例/请求编号清理游标及业务资源,记录reason和已提交进度 | 无需应答 | + +每个已经取出的BEGIN恰好配对一个FINISHED。BEGIN尚未生成或未被取出时取消,不产生终结事件,因为应用尚未建立资源;参数校验、总召忙及满槽拒绝也不交付BEGIN。取消会清除尚未取出的NEED_DATA,已经取出的旧事件应答失效。FINISHED始终保留到应用读取;即使没有协议任务,未读终结事件仍会使Destroy返回false。COMPLETED仅说明终止帧已被发送层接手,不说明已写完或已ACK。 + +多目标广播的FINISHED先保存在每站状态区,NextEvent按FIFO空位逐个交付;连续读取即可,无需Process。网络流程完成后phase为FINISHING、gi_pending为false,但仍占用任务及状态区,读完全部终结事件之前新激活仍忙拒绝。 + +应用使用Asdu_Create、具体对象Create及Asdu_Add_InformationObject构造批次。参数为104的2/2/3,数据COT=事件request.qualifier、P/N=0,CA/OA/TEST与事件一致;一个ASDU只能同类型,批间可切换类型。支持Type 1/3/5/7/9/11/13/20/21,SQ=0/1均可,单批最多配置数量且不超过249字节。Add返回FULL时提交已有的非空批次,未加入对象留给下一批;空数据集直接Complete_Data。累计量由独立CounterInterrogation实例处理。 + +Submit_Batch返回OK后发送层已复制原文并确定帧顺序,应用可复用ASDU、缓冲及参数,并推进自己的数据游标。WAIT不保存原文、不推进计数,也不再次生成NEED_DATA;应用保留当前事件及游标,网络推进后重新读取当前值组包重试。只有本批OK后才索取下一批;数据源未就绪时也保存事件,不重复提交空批次。点值更新及突发发布须与总召读值及提交串行化;逐批取值不保证整轮同一采样时刻,具体排序保证及持续积压的边界见[总召模块设计](IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。 + +| 应答结果(省略IEC104_INTERROGATION_REPLY_) | 处理 | +| --- | --- | +| OK=1 | 本次应答已被接手,不再重复应答;还不代表发帧/ACK | +| WAIT=0 | Slave有未读通知,或Submit_Batch暂不能直接发送;保留事件和游标,推进通信并读空通知后重读当前值重试 | +| STALE=-2 | 交互已经失效,丢弃该应答;对应资源按FINISHED清理 | +| ERROR=-1 | 参数、批次或服务查询错误;修正后可使用同一事件重试。NEED_DATA阶段Fail_Request关闭失败是例外:任务已取消,应处理FINISHED | + +Handle_Event仅在空闲时保存合法总召,忙拒绝保留的只是单帧应答。单帧应答在Slave通知已读空、原会话有效且没有更早REPLY时,使用请求副本沿原HIGH接口立即尝试提交;成功便不占任务槽。WAIT或暂不能安全提交时保存,满槽无法保存则只丢新请求并计数。立即提交也不会修改调用方的原事件,不会跳过STOP/START或越过旧REPLY。 + +IEC104_Interrogation_Process(gi)在单帧REPLY与唯一总召之间轮转,DATA只生成NEED_DATA,应用调用Submit_Batch提交。总召等应用或事件空位不阻塞短应答;其他REPLY可穿插三阶段流程。ACT_TERM接手才正常终结,不等待ACK。拒绝、确认前失败或定向撤销只结束该站,多目标广播仍为其他站保留总召资格。Process发送WAIT保留请求和阶段,下次优先尝试最早REPLY;数据WAIT由应用重读后重试。已交给HIGH或发送缓冲的帧保持原顺序。 + +Process每一步和应答入口都检查Slave通知,避免跨过STOP/START或撤销。响应沿原connection发送;确认、终止及短应答可排入HIGH,数据只尝试直接提交。waiting包含发送、应用和事件空位等待;application_waiting表示等首次有效应答,data_send_waiting表示有效批次曾遇发送WAIT。events_pending是Slave通知标志,pending_events包含业务FIFO事件和状态区中的待交付FINISHED。phase和进度优先反映当前站,收尾为FINISHING,仅有短应答为REPLY,无任务为IDLE。waits只统计发送WAIT,批次/对象计数只在接手后增加。 + +Send_Response内部也可能受理STOP。发送ERROR时若确认原连接正常停用且通知待处理,模块先让出,由DEACTIVATED终结为STOPPED;Submit_Batch暂返WAIT,不停止取数计时。其他ERROR/FATAL仍按SEND_ERROR处理。成功提交照常记账,下一轮检查通知,不重复发送;判定条件见[提交结果处理](IEC104_Interrogation_设计架构.md#72-process中的选择规则)。 + +Process在任务为空、所有任务等应用/事件空位、发送WAIT或有未读Slave通知时返回;每轮另有2倍任务容量的步数上限,默认8步,余下本地工作由need_process提示。应用无需传入步数,仍须读取BEGIN/NEED_DATA并显式应答;数据未就绪时持续驱动网络和业务期限。 + +每次业务应答或读取业务事件后重查Get_Status。need_process为true时不休眠:新请求可生成BEGIN、旧FINISHED出队释放空位、下一帧待提交或业务期限到期都可能要求立即推进。它不是waiting的反值;发送WAIT后停止本地续跑,每次Slave_Poll/Process推进网络并读空Slave通知后仍须调用总召Process重试。Get_Status实时更新events_pending,未读Slave通知优先于need_process;外部等待同时遵守Slave_Prepare的网络调度条件。 + +数据WAIT时,同一网络推进之后还须重试应用保存的NEED_DATA,不能只等待need_process或新业务事件;Process不会替应用再次读取数据。 + +主站撤销按连接代次、目标CA、OA、TEST和QOI匹配,先于忙拒绝处理。定向撤销可匹配当前站、已确认但尚未取数或等终止的站,其他站继续;广播撤销停止整轮,对各仍活动站回肯定COT=9。尚未选入BEGIN或已结束的站不补发确认。匹配站旧应答STALE,已取BEGIN时有FINISHED(DEACTIVATED),满槽也复用原任务,不补发该站ACT_TERM;已接手帧不撤回。删除不妨碍原活动事务被撤销。详见[撤销与取消](IEC104_Interrogation_设计架构.md#9-撤销取消与连接变化)。 + +| 配置期限 | 默认值 | 起止点 | +| --- | --- | --- | +| begin_timeout_ms | 15000ms | BEGIN入业务FIFO至应用接受、拒绝或报告失败,包含事件未被取出的时间 | +| data_timeout_ms | 15000ms | 每次NEED_DATA入队至首次有效批次尝试、声明结束或报告失败;有效批次遇发送WAIT也停止本批计时,下一批重新计时 | +| total_timeout_ms | 0(默认禁用) | 启用后从BEGIN生成至ACT_TERM被发送层接手,包含取数和发送等待,不包含生成BEGIN前等事件空位,不等待ACK | + +每项配置为0即禁用该项;执行总期限默认关闭,接受和每批取数期限仍各为15000ms。默认值是可调整的业务策略,不是标准规定的总召时限,也不替代104链路t0..t3及单帧发送期限。使用Net提供的64位单调时钟,不受校时影响。Process及有效事件的应答接口都会检查期限。START在BEGIN生成前不计业务期限,但已占用总召资格;历史FINISHED挤占业务FIFO时须及时读取,否则当前请求会一直等空位,主站可能先超时。BEGIN生成后开始已启用的应用接受和执行总期限。START/CONFIRM超时保留TCP并转为否定ACT_CON;DATA/TERMINATE超时关闭原连接并取消本模块任务,不补发成功ACT_TERM。迟到应答返回STALE。显式启用后,提交新批次或单帧应答不延长执行总期限;总期限与阶段期限同时到点优先报告TOTAL_TIMEOUT。未读Slave通知优先处理,避免把已到达的撤销误算为超时。 + +确认边界是肯定ACT_CON被发送层接手,而不是Reply_Begin(true)、TCP写完或收到ACK。CONFIRM提交WAIT时仍可否定激活;已进入HIGH或当前发送缓冲的肯定确认不能改为否定。确认前失败复用原任务槽,保留QOI/OA/TEST并使用本站CA,排在已有REPLY之后,满槽也无需丢弃其他应答。已取出BEGIN才产生一次FINISHED,保留具体TIMEOUT或SOURCE_ERROR;未取出的BEGIN移除。Process转换后重查Slave通知并在本轮继续调度,发送WAIT保留重试,不再计算已终结业务的期限。确认前超时增加timeouts及rejected,BEGIN失败增加rejected,均不增加cancelled。 + +等待前重新Get_Status;timer_active为true时,Poll或应用事件循环的等待上限取原上限与timeout_remaining_ms的较小值,剩余0则立即驱动Process。Get_Status只查询,不自动执行超时。gi_pending表示唯一总召尚未终结,含BEGIN尚未生成;phase优先报告当前总召阶段,仅有短应答时为REPLY。deactivated记录匹配撤销,timeouts记录执行超时;last_finish_reason也记录BEGIN尚未交付便取消的总召。 + +COT/CA/IOA错误回45/46/47;激活的非法QOI、P/N、总召忙或应用拒绝回否定ACT_CON,撤销的非法QOI/P/N回否定DEACT_CON。合法新总召满槽时尝试否定ACT_CON;若不能安全提交或保存,就仅丢弃新请求并增加dropped_requests,保留当前任务和TCP。overloads统计需要保存但槽满的次数,negative_submitted统计发送层已接手的否定应答数。丢弃请求没有响应,主站可能超时或重试;Handle_Event返回HANDLED包含这种已处理的丢弃。 + +总召入口先安全读取类型,非100或公共视图不可用返回IGNORED,公共入口先过滤不可用视图再分派业务。Type100唯一对象完整但SQ=1或有尾字节时,仅回否定7/9,COT/CA/IOA错误仍优先45/46/47;副本规范为10字节、VSQ=1,保留OA/TEST/IOA/QOI及原输入事件。零对象/多对象等无法可靠对应请求的情况计入rejected并HANDLED丢弃。两类请求均不执行激活/撤销,不影响原总召及其期限;真实截短/帧错误仍由Receiver和连接层处理。完整策略见[请求校验](IEC104_Interrogation_设计架构.md#31-接收范围及校验顺序)。 + +非法Submit_Batch返回ERROR,保留交互供修正。数据源无法继续时调用Fail_Request:BEGIN阶段保存否定确认并保留连接,NEED_DATA阶段结束原连接;两者均交付FINISHED(SOURCE_ERROR)。BEGIN失败返回OK或超时应答返回STALE后,仍须继续驱动Process,不能假定连接已经断开。 + +构建在Slave及依赖之外加入104/business/IEC104_Interrogation.c及104/business/IEC104_GeneralInterrogation.c,头文件搜索路径包含104/、104/business/和common/。静态容量宏为IEC104_INTERROGATION_CAPACITY(默认TCP实例容量的两倍)、IEC104_INTERROGATION_TASK_CAPACITY(默认4)、IEC104_INTERROGATION_EVENT_CAPACITY(默认4,最小2)。每实例4个任务槽共用于至多一个召唤及短应答,不是后续召唤队列。任务使用固定槽位和顺序索引,完成的非队首应答可立即释放槽位,其他任务的自引用不受影响。生成BEGIN时同时预留一个终结槽;未读业务事件挤满队列时暂停生成工作事件,已排队单帧应答仍可调度,取消仍可交付FINISHED,不使用malloc。 + +同时接入计数量时另编译104/business/IEC104_CounterInterrogation.c,创建独立实例,配置独占广播状态区。连接及启停通知必须同时交付两边;I帧先总召、再计数量,未接手再交校时等后续模块或兜底。每轮读空Slave通知后推进两个召唤实例,轮换先后顺序,合并need_process、events_pending及最小剩余期限。FRZ=0走确认、数据、终止;FRZ=1..3通过Execute_Begin执行一次本地同步动作,确认后直接FINISHED。两个实例分别Cancel、读取FINISHED、Destroy。动作失败及快照约定见[计数量设计](IEC104_CounterInterrogation_设计架构.md)。 + +本接口替换旧begin/read_batch/finish及parameter配置:应用使用NextEvent和显式应答,遍历状态自行持有。Destroy前须Cancel并读完业务事件。Process只接收实例,旧调用删除第二个预算实参。当前只保留一个未终结总召,忙时否定后续激活,不自动执行此前被拒绝的请求。已删除queue_timeout_ms、QUEUED/QUEUE_TIMEOUT、queued_interrogations和queue_timeouts;调用方删除相关字段及分支,使用Config_Default初始化配置并重新编译。negative_submitted和dropped_requests继续记录否定提交与满槽丢弃;FINISH_OVERLOAD保留枚举值,满槽不以此终结已有任务。 + + + +### 1.4 接入独立时钟同步模块 + +编译`104/business/IEC104_ClockSynchronization.c`并包含对应头文件,在Slave创建并初始化后、消费任何Slave通知前创建校时实例。配置只含源标准时区和执行期限,默认480分钟及15000ms;模块使用独立静态池和每实例4个任务槽,不与召唤引擎共享任务。详情见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md)。 + +1. 将CONNECTED、ACTIVATED、DEACTIVATED及DISCONNECTED按FIFO交付总召、计数量、校时及遥控所有实例。I帧按总召→计数量→校时→遥控分派,均IGNORED才用Reject_Request兜底;HANDLED也可能表示拒绝或满槽丢弃。 +2. `Handle_Event`校验Type 103的唯一对象、COT/CA/IOA、CP56/IV/TEST、源时区及本站日期范围。合法请求保存原文及accepted_ms,不设置时钟;广播在受理时建立目标快照。 +3. 读空Slave通知后调用`Process`推进队首应答和未执行请求期限。Process不调用action,不越过等待执行的队首提交后续应答。 +4. 应用明确准备执行时调用`Execute_Next(cs, action, context)`。入口检查原会话,使用同一单调采样检查期限及计算滞留补偿,向action交付对应base_ms的utc_ms和固定UTC+8 local_time。只调用最早EXECUTE一次;可以越过较早REPLY执行,但确认保持原FIFO位置。 +5. action返回true/false分别保存肯定/否定ACT_CON,均仅回显原CP56并走Try;等待发送不重复动作。应用可把local_time交给Clock_Set,也可更新模拟时钟;不再次处理SU/时区或把local_time当UTC。false和后续取消均不保证硬件没有改变。 +6. 重查Get_Status,用execute_pending安排显式执行,用need_process及最近期限安排本地推进和等待。网络推进并读空通知后仍调用Process重试发送WAIT。confirm_pending包含action执行中,需排序的新带时标发布在此期间暂停,既有HIGH/NORMAL继续排空。 +7. 退出时校时Cancel,与其他业务分别清理,再Slave_Close、读空并分派通知、校时Destroy,最后移除站点并销毁Slave。STOP取消未交接请求和广播遍历,不回滚时钟,也不撤回已编号确认。 + +Execute结果须按枚举分支:WAIT先处理Slave通知,EMPTY只表示没有可执行动作、仍可能有待发确认;OK表示调用过action一次,不表示action成功、确认写完或主站ACK。ERROR不应当作发送背压盲目重试。不能在action内推进Slave或重入修改校时对象;Get_Status是唯一允许的本模块只读入口。 + +时间只补偿Handle_Event受理后的本站滞留;base_ms不是设备生效时刻,不补网络、通知FIFO和驱动耗时。新请求不按内容去重,重发旧时标可能再次回拨。Try不隔离新增NORMAL,持续发布可能使确认延迟;confirm_pending是应用协调依据,不是传输层预留或线程锁。 + + + +### 1.5 接入单点、双点遥控模块 + +编译`104/business/IEC104_Command.c`并包含`IEC104_Command.h`,在Slave初始化后、消费第一条Slave通知前创建实例。同一Slave至多一个实例,第一版只接手45/46且同一时刻只有一个遥控事务;默认两种类型均须选择。点表、闭锁、远方/就地及实际输出由应用决定,协议模块不注册或保存回调。完整约定见[遥控设计](IEC104_Command_设计架构.md)和[API第25节](IEC104_API接口汇总.md#file-25)。 + +1. Slave_NextEvent按FIFO读到EMPTY。生命周期通知交付全部业务实例,I帧交给Handle_Event校验与受理;不能为等待Check_Request就跳过STOP或撤销。通知预算耗尽时续读通知,暂不推进动作。 +2. 读空通知后调用Command_Process,再用Command_NextEvent读取业务事件。SELECT/EXECUTE取得后按原样保存纯值事件;如需保留站点context,先Acquire应用自己的站点引用,再保存context和事务身份。 +3. 对工作事件调用Accept或Reject。Accept在消费事件之后至多调用一次action,WAIT时事件未消费,STALE时不得执行。SELECT可无动作;EXECUTE必须提供短时动作,返回NOT_STARTED、STARTED或FAILED。回调内不发布上报、不推进Slave,只允许查询Get_Status/Get_Transaction。 +4. Accept返回OK后检查动作结果或Get_Transaction。只有仍处于EXECUTING的事务进入设备结果处理;OK不代表动作成功。收到设备结果就Report_Result,允许确认仍在WAIT,成功记录后执行期限停止。 +5. 本事务的肯定确认交接后,逐帧调用Submit_Return_Info。返回WAIT保留同一事件与ASDU,读通知并推进网络后重试;返回OK才可复用ASDU存储、提交下一帧。模块FIFO中更早的应答仍优先;该接口不进入NORMAL,返回信息绑定原连接。 +6. 所有返回信息均为OK(或无需返回信息)后调用Complete。WAIT时仍未关闭返回信息阶段,稍后重试;OK后不得再提交返回信息。Process按策略交接ACT_TERM,随后产生COMPLETED或EXECUTION_FAILED的FINISHED。 +7. NextEvent持续读取FINISHED,包括STOP、断线、撤销及期限失败。通过事务身份清理对应返校/应用状态;select_effect和execute_effect提示可能仍有物理副作用。若输出尚未结束,继续持有应用站点引用及设备忙状态,设备空闲后再Release。 +8. 每轮合并Get_Status的events_pending、need_process、timer_active/timeout_remaining_ms与应用挂起重试,决定等待时间。发送WAIT后仍须在网络推进并读空通知后调用Process;Report_Result之后没有业务期限,应用必须继续提交返回信息及Complete。 + +正常成功路径为`SELECT → Accept → 选择确认 → EXECUTE → Accept → Report_Result → Submit_Return_Info(0..多帧)→ Complete → FINISHED`;报设备结果和确认交接不要求固定先后,但返回信息必须在肯定执行确认之后。确认、返回信息、ACT_TERM的交接仅确定帧序,不等于本地写完或主站ACK。 + +应用取走任一工作事件后,该事务恰好对应一个FINISHED;未取走任何工作事件就结束则直接移除事件。槽须等FINISHED读完(或无需交付)且事务应答交接/取消后释放,期间所有站点仍算忙。结束原因及最后接纳的工作请求不会被后来的撤销、不匹配请求或STOP覆盖。 + +退出按`Command_Cancel → 读完FINISHED并清理应用资源 → Slave_Close → 读空并分派通知 → Command_Destroy → Remove_Station → Slave_Destroy`执行;其他模块也分别取消和销毁。Destroy不会丢弃未读FINISHED或独立应答。动作已经执行时,STOP/断线/Cancel不回滚输出,也不补发ACT_TERM。 + + + +## 2. 创建服务与接入主站 + +实现入口:[IEC104_Slave.c](../../104/IEC104_Slave.c)、[IEC104_TcpServer.c](../../104/IEC104_TcpServer.c)、[IEC104_Connection.c](../../104/IEC104_Connection.c)、[IEC104_SendManager.c](../../104/IEC104_SendManager.c)。 + +### 2.1 服务初始化 + +```text +应用:IEC104_Slave_Create() + ├─ 取得 Slave 静态槽并分配本次实例编号 + └─ IEC104_TcpServer_Create(),取得并私有持有 TCP 服务 + +应用:IEC104_Slave_Config_Default() + └─ IEC104_TcpServer_Config_Default(),填入默认监听和协议配置 + +应用:IEC104_Slave_Init() → IEC104_TcpServer_Init() + ├─ 检查配置、运行状态、未读旧事件 + ├─ IEC104_Net_Init() + ├─ IEC104_Net_Listen() + ├─ IEC104_AsduQueue_Init(..., generation = 0) + └─ 保存监听身份,进入可调度状态 +``` + +监听与协议参数见上节;初始化失败时检查返回值及 `error.result/stage/socket_error`。失败不销毁句柄,完成相应清理后可重试 Init,或对尚未运行的服务调用 Destroy。 + +这一阶段只创建服务级队列和监听资源,没有创建活动协议会话。`generation = 0` 表示队列处于离线状态,NORMAL 原文可以先缓存。 + +### 2.2 接受 TCP 连接 + +```text +Poll / Process + └─ [内部,TcpServer] Accept_Clients() + ├─ 检查 CONNECTED 通知空间和旧启停事件是否已消费 + ├─ IEC104_Net_Accept() + ├─ 分配新的非零 generation + ├─ IEC104_AsduQueue_Rebind(..., 新 generation) + ├─ IEC104_Connection_Init() + │ ├─ IEC104_Receiver_Init() + │ └─ IEC104_SendManager_Init_Connection() + │ ├─ IEC104_Link_State_Init(..., IEC104_ROLE_CONTROLLED) + │ ├─ IEC104_SendBuffer_Init() + │ └─ IEC104_SendManager_Init() + ├─ IEC104_Connection_Set_Transfer_Observer() + ├─ 保存客户端 socket、注册 token、主站地址 + └─ [内部] Notify_Connection() → CONNECTED 入事件 FIFO +``` + +Connection 初始化通过 Manager 将内部引用绑定到最终对象地址。初始化后的 Connection 含内部引用,调用方应保持对象地址稳定。 + +接入后的状态是: + +| 项目 | 值或含义 | +| --- | --- | +| TCP | 已连接 | +| Connection 阶段 | ACTIVE,表示协议会话可运行 | +| Link 数据传输状态 | STOPPED | +| Link 启停过程 | IDLE | +| 序号 | 本次新连接从初始值开始 | +| 首条业务事件 | CONNECTED | +| 已缓存 NORMAL | 保留,等待 STARTDT_ACT 受理后取得发送机会 | + +因此,收到 CONNECTED 不等于数据传输已启用。当前有活动主站时,额外接入的客户端会被关闭,不替换正在使用的连接。 + +内部的 transfer observer 用于检查启停通知容量和保存状态变化,应用仍统一通过 `NextEvent` 取通知。 + +### 2.3 三类身份不能互换 + +| 身份 | 生命周期与用途 | 维护方 | +| --- | --- | --- | +| Slave 实例编号 `_owner_serial` | 一次成功 Create;防止静态槽销毁、复用后旧事件指向新实例 | Slave 保存到事件内部,应用只读 | +| `generation` | 一次 TCP 接入;区分重连前后的请求、接收帧和发送记录 | TcpServer 分配,Slave 自动带入应答和 Close_Connection | +| `service_token`、socket `token` | 一次 Init、一次 socket 注册生命周期;过滤旧网络、定时器和本地调度通知 | Prepare 提供,应用事件循环按原注册值回传 | + +`receive_serial` 和 `frame_serial` 分别标识某个 generation 内的接收帧和发送记录;协议 N(S)/N(R) 会按模 32768 回绕,不能替代这些身份。各编号只在所属服务范围内解释,不是跨服务的全局 ID。 + +Destroy 成功后必须丢弃旧服务指针。裸指针本身不能识别静态地址后来是否被复用;事件额外保存的实例编号用于阻止旧请求发送到新服务。计数器用尽时不会回绕重用身份。 + + + +## 3. 每一轮 Poll / Process 如何推进 + +实现入口:[IEC104_TcpServer.c](../../104/IEC104_TcpServer.c)。 + +```text +IEC104_Slave_Poll() → IEC104_TcpServer_Poll() + ├─ [内部] Prepare_Events() 查询本轮关注条件 + ├─ [内部] Wait_For_Events() + │ └─ IEC104_Net_Wait() 至多等待一次 + └─ [内部] Process_Events() 与公开 Process 共用 + ├─ 核对 service_token 和 socket/token + ├─ 处理当前连接的输入与协议期限 + ├─ [内部] Flush_Output() 推进已有及可新准备的发送 + ├─ [内部] Schedule_Protocol_Work() + └─ [内部] Accept_Clients() 最后处理接入 +``` + +当前连接未收到 EOF 时,输入处理进入 `Receive_Input`;EOF 收尾阶段通过 `IEC104_Connection_Tick` 推进协议,并继续尝试发送和交付记录。新接入的连接安排下一轮本地推进,不继承旧连接本轮的就绪标志。 + +每轮推进有预算限制,避免单个连接长期占用事件循环。产生输入事件后也会适时返回应用,让业务先处理事件。预算让出后由 `need_process` 安排续跑,不要求再收到一个新的 socket 边沿。 + +`Poll(max_wait_ms)` 的参数只限制网络等待时间。实际等待还受最近协议期限、重试期限约束;已有业务通知或本地工作时不等待。 + +### 3.1 接收先于确认超时检查 + +```text +[内部,TcpServer] Receive_Input() + ├─ IEC104_Connection_Begin_Receive_Batch() + ├─ 尝试交付上一轮保留的 I 帧 + ├─ [内部] Read_Client() → IEC104_Net_Recv() + ├─ [内部] Process_Input() → Connection_Feed() + └─ [内部] Finish_Receive_Slice() + ├─ 仅因预算/交付让出且仍有既定输入: + │ 固定尚待处理的已到达字节范围 + │ → IEC104_Connection_Yield_Receive_Batch() + │ → 安排立即续跑 + └─ 输入处理完、真正容量背压或接收无进展: + → IEC104_Connection_Tick() + → 按原期限完成超时判定 +``` + +这一顺序用于先处理已经到达的 ACK 或 TESTFR_CON。后到的数据不会持续扩大旧批次,也不会重新计算一个更晚的 t1 起点。本地发送期限和 STOP 总期限继续约束推进。 + +ACK/TESTFR 的 t1 已到期而尚未解除时,暂缓断线判定也不允许为 HIGH/NORMAL 分配新的 I 帧序号;已有帧和控制发送仍可推进,让接收路径有机会处理已到达的确认。 + +## 4. 字节接收、成帧与分派 + +实现入口:[IEC104_Receiver.c](../../104/IEC104_Receiver.c)、[IEC104_Codec.c](../../104/IEC104_Codec.c)、[IEC60870_Codec.c](../../common/IEC60870_Codec.c)、[IEC104_Connection.c](../../104/IEC104_Connection.c)。 + +```text +IEC104_Net_Recv() + → TcpServer 保存收到的字节及未消费尾部 + → IEC104_Connection_Feed() + → IEC104_Receiver_Feed() + → 收集一帧完整 APDU + → IEC104_Parse_APCI() + → I 帧另调用 IEC60870_Parse_ASDU_Header(¶meters104, ...) + → [内部,Connection] Drive() + 1. Dispatch_Received_Frame() + 2. Check_Timeouts() + 3. Advance_Control() +``` + +Receiver 一次保留一帧。未释放的帧会先被重试,后续 TCP 字节留在缓冲或网络接收队列中;半帧等待后续字节。TCP 路径按帧边界严格校验,不通过扫描丢弃任意错误字节来恢复。 + +I 帧在进入 Link 前,已检查 ASDU 公共头和已知固定长度类型的最低载荷长度。具体业务含义、字段合法性,以及未知或变长类型的进一步校验由应用承担。 + +`Dispatch_Received_Frame` 的主要分支为: + +| 收到的帧 | 调用方向 | 结果 | +| --- | --- | --- | +| I | Manager_Handle_I_S_Frame → Link_Handle_I_Frame | 校验 N(S)、N(R),受理接收及累计 ACK,保留 I 帧供上层复制 | +| S | Manager_Handle_I_S_Frame → Link_Handle_S_Frame | 受理累计 ACK,随后释放该接收帧 | +| STARTDT_ACT | Manager_Slave_Handle_Startdt_Act → Link_Handle_Startdt_Act | 准入后提交 STARTED 并排入 CON | +| STOPDT_ACT | Manager_Slave_Handle_Stopdt_Act → Link_Handle_Stopdt_Act | 准入后提交 STOPPED,推进停止排空 | +| TESTFR_ACT | Manager_Handle_Testfr_Act → Link_Handle_Testfr_Act | 排入 TESTFR_CON | +| TESTFR_CON | Manager_Handle_Testfr_Con → Link_Handle_Testfr_Con | 匹配当前探测时结束等待 | +| 合法 STARTDT_CON / STOPDT_CON | 当前子站直接释放 | 不改变启停状态,不产生启停通知,也不刷新 t3 活动时刻 | + +表中 Manager 和 Link 名称省略了统一的 `IEC104_SendManager_`、`IEC104_Link_` 前缀;完整名称见后续调用链及接口汇总。 + +## 5. STARTDT:从受理到确认写出 + +实现入口:[IEC104_Link_Slave_StartStop.c](../../104/IEC104_Link_Slave_StartStop.c)、[IEC104_SendManager.c](../../104/IEC104_SendManager.c)。 + +```text +主站发送 STARTDT_ACT + → Connection 分派前检查:本次是否改变状态、启停通知是否有空间 + → IEC104_SendManager_Slave_Handle_Startdt_Act() + ├─ IEC104_SendBuffer_Reserve_S_U_Frame() + ├─ IEC104_Link_Handle_Startdt_Act() + │ ├─ IEC104_Build_Uframe(..., STARTDT_CON) + │ └─ dt_state = STARTED;dt_procedure = IDLE + ├─ 若原状态为 STOPPED:IEC104_AsduQueue_Reset_High() + └─ IEC104_SendBuffer_Commit_Frame(),保存待发 STARTDT_CON + → Connection 保存实际启停变化,取消旧 STOP 总计时 + → TcpServer 将 ACTIVATED 入事件 FIFO + → 后续发送推进把 CON 按发送顺序写出 + → IEC104_Link_Complete_Startdt_Con_Send() +``` + +**状态在 ACT 成功受理时就已经是 STARTED。** `Complete_Startdt_Con_Send` 只处理该 CON 的发送完成,不再次设置 STARTED,防止旧 CON 覆盖后来 STOP 已提交的状态。 + +这里有三个不同的时点: + +1. ACT 受理:Link 状态已改变,ACTIVATED 通知已经保存。 +2. CON 写出:控制帧按发送顺序完成。 +3. 应用读到 ACTIVATED:由应用何时调用 `NextEvent` 决定,可能在 CON 写出之前,也可能在之后。 + +STARTDT_CON 尚待发送时,业务可以提交符合准入条件的应答。应答进入 HIGH,实际新 I 帧排在已有控制 FIFO 之后;已经占用发送缓冲的旧 I 帧仍先完成。 + +已是 STARTED 时重复收到 STARTDT_ACT,仍生成 CON,不产生新的 ACTIVATED,也不清空 HIGH。它不占用新的启停通知槽,但仍需要控制帧发送空间。 + + + +## 6. 收到 I 帧后交给业务 + +### 6.1 从 TCP 原文到 Slave 事件 + +```text +IEC104_Connection_Feed() + → IEC104_SendManager_Handle_I_S_Frame() + → IEC104_Link_Handle_I_Frame() + 校验并受理 N(S) 与累计确认 N(R) + → 更新被确认的发送记录及 NORMAL 记账 + → Connection 标记该 I 帧已经应用到 Link + → [内部,TcpServer] Deliver_I_Frame() + ├─ IEC104_Connection_Peek_Received_Frame() + ├─ 将完整 APDU 复制为 I_FRAME 事件 + └─ IEC104_Connection_Release_Received_Frame() + └─ IEC104_Receiver_Release_Frame() + → 应用调用 IEC104_Slave_NextEvent() + ├─ 内部 IEC104_TcpServer_NextEvent() 取得原事件 + ├─ 保存原 Slave 实例编号及 generation + └─ I_FRAME:IEC60870_Asdu_Create_From_Buffer() + 为 event.asdu 建立借用本事件 APDU 字节的可写视图 + 成功后安装 event.connection 的发送函数及上下文 +``` + +`NextEvent` 返回 READY 只表示交付了事件。业务先判断 `event.transport.type == IEC104_TCP_EVENT_I_FRAME`,再检查 `event.asdu_result == IEC60870_ASDU_OK`,才能使用 `event.asdu` 和 `event.connection`。非 I_FRAME 的 `asdu_result` 为 UNSUPPORTED;I_FRAME 的视图创建失败仍交付原通知用于诊断,此时 connection 为空。 + +事件已经保存完整 APDU,不需要业务调用 Receiver 或 Connection 的 Release。`transport.data.i_frame.apdu` 的前 6 字节是 APCI,余下字节为 ASDU;`event.asdu` 引用的正是这份本地副本,确认函数会修改其中的 COT/P/N。 + +### 6.2 完整解析与业务校验 + +`Create_From_Buffer` 只检查公共头及已知类型的最低长度,允许未知类型保留原文以便回复错误原因。`asdu_result == OK` 不代表所有信息对象已验证,更不表示命令可以执行。 + +完整处理顺序为: + +```text +IEC60870_Asdu_Get_Data(&event.asdu, &bytes, &length) + → IEC60870_MasterConnection_Get_Application_Layer_Parameters(&event.connection) + → IEC60870_Asdu_Parse(parameters, bytes, length, &info) + 验证精确长度、VSQ/SQ、所有对象字段;未知类型返回 UNSUPPORTED + → 依据 info.type_id 选择正确的具体对象结构 + → IEC60870_Asdu_Get_Element(&info, index, &object, sizeof(object)) + → 应用检查 COT、TEST、CA、IOA、限定词、选择/执行状态和业务权限 + → 执行业务,并按对应业务过程提交应答 +``` + +解析视图 `info` 借用参数和原文字节,二者在取对象期间必须有效且不变。`Get_Element` 复制出具体对象,但输出结构须由 Type ID 选择;尺寸足够不能代替正确的 C 类型。若随后改写事件的 COT/P/N,不能继续把旧解析视图当作更新后的公共头。 + +### 6.3 延迟处理、事件复制与连接寿命 + +`IEC104_TcpEvent_t` 只是值与字节数组,可以按值复制;`IEC104_SlaveEvent_t` 含指向自身的 ASDU 视图和 connection 上下文,**不能用结构赋值、memcpy 或搬移数组元素来保存**。事件使用期间地址必须稳定。 + +```text +需要延期处理: + 方式一:NextEvent 直接写入地址稳定的任务事件存储 + 方式二:IEC104_Slave_Copy_Event(&task_event, &event) + 复制原文字节并重建目标自引用,保留原实例与 generation + → 使用 &task_event.connection 和 &task_event.asdu 继续应答 +``` + +Copy_Event 不发送报文,也不把历史请求重新绑定到当前主站。原连接断开、重新 Init 或服务槽复用之后,旧事件字节仍可用于诊断,旧发送接口会被身份校验拒绝。复制失败保持目标不变;源和目标相同时只执行有效性校验。 + +延迟任务须逐条处理STOP/START并在STOP时取消原任务,提交前再用`Check_Request`检查当前条件。同一代次恢复START后,最终READY不代表STOP之前的旧任务重新有效。`Reject_Request`只用于当前事件的同步分派,不用于此处的延迟应答;它检查的`pending_state_event_count`不能追溯已经消费的历史启停通知。 + +READY 会替换传给 NextEvent 的整个输出;EMPTY/ERROR 保持输出原值。把事件地址交给待处理任务后,不应再用同一存储接收下一条事件。多个应答可以使用同一个原请求 connection,待发 ASDU 也可以单独构造,不必都复用 `event.asdu`。 + +需要精简任务存储时,另一种方式是保存`Slave_Get_Event_Identity`返回的纯值身份,以及业务需要的请求字段/字节。随后先处理通知并检查原请求,再用`Slave_Send_Response / Slave_Try_Send_Response`提交独立ASDU;失败需关闭原连接时用`Slave_Close_Connection_By_Identity`。这些入口保留原服务实例和原连接代次检查。总召采用此方式;完整事件及Copy_Event接口继续保留,容量及排序不因内部存储精简改变。 + + + +## 7. 应答与主动上报如何进入同一发送流程 + +实现入口:[IEC60870_Slave.c](../../common/IEC60870_Slave.c)、[IEC104_Slave.c](../../104/IEC104_Slave.c)、[IEC104_TcpServer.c](../../104/IEC104_TcpServer.c)、[IEC104_SendManager.c](../../104/IEC104_SendManager.c)、[IEC104_AsduQueue.c](../../104/IEC104_AsduQueue.c)。 + +### 7.1 请求应答:HIGH 或直接发送缓冲 + +下图为普通事件应答路径。总召确认/终止经`IEC104_Slave_Send_Response`按保存的原身份进入同一普通HIGH路径;数据经`IEC104_Slave_Try_Send_Response → IEC104_TcpServer_Try_Send_Response → IEC104_Connection_Try_Submit_Response → IEC104_SendManager_Try_Submit_Response`:当前发送缓冲空闲、两级队列均无未提交条目且链路/窗口允许时才直接组帧;否则WAIT,不缓存HIGH。到期协议工作也先返回WAIT,由网络Process处理已到达的ACK。OK后沿同一发送缓冲、记录和非阻塞写入路径推进。 + +```text +应用:IEC60870_MasterConnection_Send_ASDU / Send_ACT_CON / Send_ACT_TERM + → 共用层检查连接及函数指针,分派给 Slave 的发送适配器 + → ACT_CON 设置 COT=7、P/N=negative;ACT_TERM 设置 COT=10、P/N=0 + → 检查原服务实例、ASDU 参数与原文 + → IEC60870_Asdu_Get_Data(),取得不含 APCI 的 6~249 字节 ASDU + → IEC104_TcpServer_Send_Response(私有 server, 原 generation, bytes, length) + → 检查连接、generation、EOF 和参数 + → IEC104_Connection_Submit_Response() + ├─ 已到需要协议推进的时点: + │ IEC104_SendManager_Enqueue(..., HIGH) + └─ 可正常尝试: + IEC104_SendManager_Submit_Response() + ├─ 可以直接组帧: + │ [内部] Prepare_I_Asdu() + │ → Connection 返回 SEND_READY + └─ 暂时不能组帧: + IEC104_SendManager_Enqueue(..., HIGH) + → 入 HIGH,或容量不足返回 WAIT + → 若返回 SEND_READY: + [内部,TcpServer] Write_Output(),至多尝试一次 send + → 本次提交结果保存到该 event.connection 对应的事件中 + → 返回应用;后续 Poll / Process 继续推进 +``` + +104 适配器要求 COT/CA/IOA 为 2/2/3,`maxSizeOfASDU <= 249`;共用参数合法不代表适用于 104。`Send_ASDU` 保留公共头,确认/终止接口先修改字段再提交,WAIT、ERROR 或 FATAL 都不会回滚已经修改的字段。它们不自动执行命令、不自动组织确认后的数据或终止序列。 + +共用发送返回 true 时表示已复制接手,之后可复用本次提交的 ASDU 存储;false 时立即查询 `IEC104_Slave_Get_Last_Submit_Result(&event.connection)`。这个结果只记录该接口最近一次提交,不会随 Poll、整帧完成或 ACK 自动变化。 + +WAIT 时保留原 connection、已经准备好的 ASDU 和业务进度,继续消费通知并驱动 Poll/Process,再重试同一步。OK 后推进业务进度;ERROR 需重新判断请求是否仍有效及准入条件;FATAL 结合 RECORD/DISCONNECTED 收尾。不能重新执行已产生副作用的业务操作,也不能把旧请求的重试转入 NORMAL 或绑定到新连接。 + +两条接手路径都由 Manager 检查 STARTED + IDLE 准入。直接组帧需要发送窗口、记录槽和发送缓冲都有余量,并满足没有旧 HIGH、已有帧或公平配额阻挡等条件。存在已到期的协议检查时,Connection 可先将应答放入 HIGH,交由 Process 先处理已到达确认再判定期限。 + +HIGH 是当前连接的应答队列。STOP 受理后尚留在 HIGH 的应答会清空,没有逐条丢弃通知;已经组帧的 I 帧和发送记录继续按实际状态排空。新的应答在停止状态下返回 ERROR。 + +其他业务均未接手当前I帧时,示例在读取下一事件前同步调用`IEC104_Slave_Reject_Request(slave, &event, IEC60870_ASDU_COT_UNKNOWN_TYPE)`。该入口只将原文COT改为44并置否定位,复用上述普通HIGH路径;输入事件及其最近提交结果保持不变,直接检查返回值。两条连续命令可逐条提交,允许在总召期间应答,不设置示例待应答槽。WAIT仅表示HIGH容量不足,示例丢新并累计overloads/dropped_requests,保留连接与已接手内容;ERROR继续处理失效原因及通知,FATAL可能已写出字节,不重试并排空收尾通知。cause范围、未读启停检查和同步调用限制见[同步拒绝当前请求](IEC104_API接口汇总.md#reject-request)。 + +### 7.2 主动上报:NORMAL + +```text +应用:取得 Slave 的应用层参数 + → 具体信息对象 Create() + → IEC60870_Asdu_Create(),设置公共头和 SQ + → IEC60870_Asdu_Add_InformationObject(),逐个编码;首个对象确定 Type ID + → IEC104_Slave_Enqueue_Event(slave, &asdu) + → 检查 104 参数布局,IEC60870_Asdu_Get_Data() + → IEC104_TcpServer_Enqueue_Event() + → IEC104_AsduQueue_Enqueue(..., NORMAL) + → 有活动连接且未 EOF 时,安排本地推进 + → Poll / Process + → IEC104_Connection_Prepare_Next_Send() + → IEC104_SendManager_Prepare_Next_Send() + → 从可发送的 HIGH / NORMAL 中选择条目 +``` + +这个入口只缓存 ASDU,不立即写 socket。NORMAL 可以跨断线保留;空间不足时允许覆盖最旧的原文,包含尚未收到 ACK 的原文。覆盖原文不等于确认对应 I 帧,独立发送记录和 k 窗口义务仍保留。 + +Add 返回 FULL 时,当前非空 ASDU 保持有效:先提交当前 ASDU,重新创建下一条,再重试尚未加入的那个对象。若空容器也装不下该对象,应检查缓冲容量、参数上限和对象大小,不能反复提交空容器。库不自动拆分 ASDU。Add 成功后可复用信息对象;Enqueue_Event 返回 OK 后可复用 ASDU 字节区。ASDU 参数由容器借用,仍须在容器使用期间有效且不变。 + +默认 HIGH 优先。设置非零 `high_priority_burst_limit` 后,NORMAL 有待发条目时按配额获得发送机会。优先级只影响下一条尚未组帧的 ASDU,不改变已经准备好的帧顺序。 + +### 7.3 准备新 I 帧的固定提交顺序 + +```text +IEC104_SendManager_Prepare_Next_Send() + ├─ 已有发送队首 → 直接返回该帧剩余字节 + └─ 没有已有帧: + IEC104_AsduQueue_Peek_Unsubmitted() + 原文跨环尾时:IEC104_AsduQueue_Copy_Asdu();连续原文直接借用 + → [内部,SendManager] Prepare_I_Asdu() + 1. 检查独立发送记录槽 + 2. IEC104_SendBuffer_Reserve_I_Frame() + 3. IEC104_Link_Prepare_I_Frame() + → IEC104_Build_Iframe() + → 固定 N(S)、N(R) 快照,推进下一个预留 N(S) + 4. IEC104_SendBuffer_Commit_Frame() + 5. Manager 建立 I 帧发送记录 + 6. 队列来源才调用 IEC104_AsduQueue_Mark_Submitted() + 7. 返回当前帧的待写字节 +``` + +直接应答复用同一个 `Prepare_I_Asdu`,但没有队列条目,因此跳过第 6 步。 + +一旦 Link Prepare 成功,N(S) 已经分配,后续交接异常需结束连接,不能撤回编号再用。SendBuffer 接手完整 APDU、Manager 建记录、队列 Mark 的先后顺序由 Manager 内部维护。 + +| 对象 | 组帧提交后的状态 | +| --- | --- | +| HIGH 原文 | 队列条目立即释放,APDU 已由 SendBuffer 保存 | +| NORMAL 原文 | 标记已提交,通常继续保留到 ACK;仍可能因覆盖策略被覆盖 | +| SendBuffer | 持有当前待写 APDU | +| Manager 记录 | 保存帧身份、序号快照、优先级、时间与后续完成事实 | + +## 8. 实际写 socket、部分发送与整帧完成 + +实现入口:[IEC104_SendBuffer.c](../../104/IEC104_SendBuffer.c)、[IEC104_SendManager.c](../../104/IEC104_SendManager.c)、[IEC104_Connection.c](../../104/IEC104_Connection.c)。 + +```text +[内部,TcpServer] Flush_Output() + → Prepare_Output() + → IEC104_Connection_Prepare_Next_Send() + → IEC104_SendManager_Prepare_Next_Send() + → IEC104_SendBuffer_Peek_Send_Head() + → Write_Output() + → IEC104_Net_Send(pending_data, remaining_bytes) + → IEC104_Connection_Handle_Send_Progress(本次实际新增字节数) + → IEC104_SendManager_Handle_Send_Progress() + → IEC104_SendBuffer_Record_Written_Bytes() + ├─ 未写完整:保存偏移,后续继续 + └─ 已写完整: + 1. 按原帧类型调用对应 Link Complete + 2. I 帧记录标记 local_send_completed,保存 written_at_ms + 3. IEC104_SendBuffer_Commit_Send_Completion() + → 更新活动时刻、接收确认及相关计时 + → 再尝试受理保留的控制请求、推进控制发送 +``` + +整帧完成时的分派为: + +| 已写完的帧 | Link 完成接口 | +| --- | --- | +| I | `IEC104_Link_Complete_I_Send` | +| S | `IEC104_Link_Complete_S_Ack_Send` | +| STARTDT_CON | `IEC104_Link_Complete_Startdt_Con_Send` | +| STOPDT_CON | `IEC104_Link_Complete_Stopdt_Con_Send` | +| TESTFR_ACT | `IEC104_Link_Complete_Testfr_Act_Send` | +| TESTFR_CON | 无需对应 Link Complete,完成发送缓冲提交即可 | + +部分写入只推进偏移,不提交整帧 Complete;暂不可写保留原帧,等待后续推进。同一段实际写出的字节只能登记一次,即使随后协议推进返回 FATAL,也不能重报。 + +发送顺序受以下规则约束: + +- 已占用 I 帧槽的帧先完成,即使第一次 send 还没写出任何字节。 +- S/U 控制帧按已有 FIFO 顺序发送。 +- 当前 I 帧和已有控制帧处理完后,才选择下一条新 I 帧。 +- 整帧写出后,SendBuffer 可释放帧字节;Manager 的 I 帧记录继续等待 ACK 和事件转移。 + +真实发送错误走 `IEC104_Connection_Handle_Send_Failure`,随后执行断线收尾。连续无进展的重试采用有界推进和发送重试期限,原有本地发送期限不会因此重置。 + +## 9. 对端 ACK、NORMAL 释放与 RECORD 事件 + +ACK 可以由对端的 S 帧携带,也可以由 I 帧的 N(R) 捎带。 + +```text +接收到含 N(R) 的 I / S 帧 + → IEC104_SendManager_Handle_I_S_Frame() + 1. 检查 ACK 覆盖的记录和本地完成事实 + 2. IEC104_Link_Handle_I_Frame() / IEC104_Link_Handle_S_Frame() + → Link 接受累计确认 + 3. 对全部本次确认的记录设置 peer_acknowledged + 4. NORMAL: + IEC104_AsduQueue_Confirm_Next_Normal_Entry() + → 释放保留原文,或核减已经覆盖的原文记账 + → [内部,TcpServer] Consume_Records() + ├─ IEC104_Connection_Peek_Confirmed_Record() + ├─ 将记录复制为 RECORD 事件,closing = 0 + └─ IEC104_Connection_Remove_Confirmed_Record() + → 应用 IEC104_Slave_NextEvent() + → 取得 event.transport.data.record 中的 RECORD 副本 +``` + +正常 RECORD 表示对应 I 帧已经整帧写出,并已被对端累计确认。对端 ACK 只能覆盖已经提交本地 Complete 的 I 帧;不能确认一个只是预留了 N(S)、尚未完成写出的帧。 + +**记录槽在 RECORD 成功进入事件 FIFO 后就释放,不必等应用取走该事件。** 普通通知空间或本轮交付预算不足时,已确认记录暂留 Manager,仍占记录槽;之后继续转移。 + +需要区分四个进度: + +| 进度 | 判断依据 | 业务含义 | +| --- | --- | --- | +| 已接手 ASDU | 提交返回 SUBMIT_OK | 库已复制,业务原始缓冲可复用 | +| 已整帧写出 | 记录的 local_send_completed | APDU 已交给本地网络发送路径 | +| 已获链路确认 | 记录的 peer_acknowledged | 对端已累计确认该 I 帧 | +| 已转为事件 | NextEvent 可取到 RECORD | 记录事实已交付应用 | + +链路 ACK 不代表对端业务已经执行。RECORD 不包含 ASDU 原文或应用自定义任务标识,业务需要的命令完成状态仍由业务协议和自身上下文判断。 + +## 10. STOPDT、重复 ACT 与交错启停 + +```text +主站发送 STOPDT_ACT + → Connection 检查实际状态变化需要的通知容量 + → IEC104_SendManager_Slave_Handle_Stopdt_Act() + ├─ IEC104_Link_Handle_Stopdt_Act() + │ └─ STOPPED + STOP_DRAINING + └─ IEC104_AsduQueue_Reset_High() + → 保存 DEACTIVATED 事件,启动或保留 STOP 总期限 + → [内部,Connection] Advance_Control() + → Queue_Pending_Control() + → IEC104_SendManager_Slave_Queue_Stopdt_Response() + → IEC104_Link_Prepare_Stopdt_Response() + ├─ 需要确认接收数据:先准备 S + ├─ 已有 S 未覆盖最新接收位置:等待该 S 完成后继续 + ├─ 还有已预留 I 帧未被对端 ACK:等待 + └─ 条件满足:生成 STOPDT_CON,过程回到 IDLE + → 将生成的 S / CON 提交到控制 FIFO + → 实际写出 STOPDT_CON + → IEC104_Link_Complete_Stopdt_Con_Send() + → 匹配本轮记录的 STOP_CON 完成后,结束 STOP 总计时 +``` + +STOP ACT 受理即停止新 I 帧准入,TCP 仍保持连接。停止排空期间继续处理所需的 S 确认和已有发送,不把状态切换推迟到 STOPDT_CON 写完。 + +覆盖全部接收数据的 S 已在 FIFO 中时,可以把 STOPDT_CON 排在它后面,不必等待该 S 实际写完才生成 CON。 + +| 场景 | 当前实现行为 | +| --- | --- | +| STARTED 时重复 START | 仍排入 STARTDT_CON;不产生新启用通知,不清空 HIGH | +| STOPPED 且已结束排空时重复 STOP | 再进行一次停止应答,条件满足后排入 STOPDT_CON;不产生新停用通知 | +| STOP_DRAINING 时重复 STOP | 忽略重复请求,保留原停止流程,不逐条追加 CON、不延长期限 | +| START_CON 尚未写完又来 STOP | STOP 可以受理;旧 START_CON 后续完成不会重新激活 | +| STOP 尚未生成 CON 又来 START | START 准入后立即重新激活,并取消旧 STOP 等待,不暂存为未来请求 | +| STOP_CON 已生成后又来 START | 旧 CON 保持 FIFO 顺序;旧 STOP_CON 完成不覆盖新的 STARTED | + +启停不重置本连接的 I 帧序号。被 STOP 清掉的 HIGH 不会因再次 START 而恢复。 + +## 11. S 确认、TESTFR 与所有期限如何驱动 + +实现入口:[IEC104_Connection.c](../../104/IEC104_Connection.c)、[IEC104_Link.c](../../104/IEC104_Link.c)。 + +### 11.1 接收确认 + +I 帧受理后,Connection 从首个尚未确认 I 帧的成帧时刻记录 t2 起点。 + +```text +[内部,Connection] Advance_Control() + → Queue_Pending_Control() + → IEC104_Link_Can_Prepare_S_Ack(w, t2 是否到期) + → IEC104_SendManager_Queue_S_Ack() + → IEC104_Link_Prepare_S_Ack() + → 提交 S 到 SendBuffer + → 实际发送完整 + → IEC104_Link_Complete_S_Ack_Send() +``` + +收到的未确认 I 帧达到 w,或 t2 到期时,会尝试生成 S。发送 I 帧也能捎带 N(R),但只有整帧完成提交后才推进实际发出的确认位置。仍有未确认接收数据时,不因后来又收到 I 帧而续期。 + +### 11.2 链路探测 + +```text +空闲达到 t3,且当前允许发起探测 + → IEC104_SendManager_Queue_Testfr_Act() + → IEC104_Link_Prepare_Testfr_Act() + → 控制 FIFO + → ACT 整帧写完 + → IEC104_Link_Complete_Testfr_Act_Send() + → Connection 开始 TESTFR 的 t1 + → 收到 TESTFR_CON + → IEC104_SendManager_Handle_Testfr_Con() + → IEC104_Link_Handle_Testfr_Con() + → 匹配等待过程后取消该 t1 +``` + +对端主动发来的 TESTFR_ACT 走另一条应答路径:`IEC104_SendManager_Handle_Testfr_Act` 调用 `IEC104_Link_Handle_Testfr_Act`,生成 TESTFR_CON 并进入控制 FIFO。提前或没有匹配过程的 TESTFR_CON 不缓存为未来确认。 + +### 11.3 期限查询与执行 + +```text +TcpServer 查询等待条件 + → IEC104_Connection_Get_Wait_Time() + → [内部] Collect_Deadlines() + → IEC104_SendManager_Get_Deadlines() + → 汇合 Connection 的 TESTFR、STOP、t2、t3 期限 + → 计算最近需要唤醒的时刻 + +输入或定时/本地唤醒推进 + → IEC104_Connection_Tick() / 其他推进入口 + → Drive() + → Check_Timeouts() + → Advance_Control() +``` + +当前实现统一在 Connection 形成期限视图,发送事实仍由 Manager/SendBuffer 保存;并非把所有起点都复制成 Connection 的独立字段。 + +| 期限 | 起点或依据 | 到期动作 | +| --- | --- | --- | +| 本地发送总期限 | 当前发送队首的 queued_at_ms | 该帧未完成则登记故障并收尾 | +| I 帧 ACK t1 | 最老已写完未 ACK 的 I 帧 written_at_ms | 确认超时故障 | +| TESTFR t1 | 本端 TESTFR_ACT 实际写完 | 未获匹配 CON 则探测超时 | +| STOP 总期限 | STOP ACT 的原成帧时刻,在受理时启用;受理前的 WAIT 也计入 | 未完成该停止应答则停止超时 | +| t2 | 首个尚未确认 I 帧的原成帧时刻 | 尝试发 S 确认 | +| t3 | 最近有效接收活动或本地整帧发送完成 | 尝试发 TESTFR_ACT | + +t2/t3 只有在控制容量和 Link 条件允许时才作为可调度动作,避免无容量时按已到期时间不断空转。协议时间使用单调毫秒时钟,Windows 后端通过 `IEC104_Net_Now_Ms` 提供;与业务的 CP56Time2a/RTC 时间分开。 + + + +## 12. 容量不足时,流程在哪里等待 + +通知 FIFO、ASDU 队列、发送缓冲和发送记录是不同的存储,等待位置也不同。容量配置见 [IEC104_TcpServer_Config.h](../../104/IEC104_TcpServer_Config.h)。 + +| 等待原因 | 保留内容与停止位置 | 继续条件 | +| --- | --- | --- | +| 普通通知空间不足,已有受理 I 帧 | Receiver 保留该 I 帧,TcpServer 置 receive_paused,暂停继续接收 | NextEvent 释放空间,再由 Process/Poll 复制并释放 I 帧 | +| 普通通知空间不足,只有已确认记录 | 记录暂留 Manager;不会单凭这项积压阻挡前面的 S/U 处理或 CON 发送 | 有普通空间和交付预算后转为 RECORD | +| 启停专用槽满,下一 ACT 会改变状态 | 该 ACT 留在 Receiver,尚未改变 Link 状态,也不为它生成 CON | NextEvent 消费启停通知后,由 Process/Poll 重试受理 | +| 控制帧槽满 | 需要预留槽的 START/TESTFR 请求暂缓,已有控制发送保留 | 既有帧完成并释放控制槽后重试 | +| HIGH 空间不足 | 本次业务应答未接手,提交返回 WAIT | 应用保留原文,在连接和准入仍有效时重试 | +| k 窗口用尽 | 已提交发送继续等待确认,不为新业务分配 N(S) | 收到合法累计 ACK | +| 发送记录槽用尽 | 即使有些帧已 ACK,也不能再建新 I 帧记录 | 已确认记录成功复制到事件 FIFO 后释放槽 | +| socket 暂不可写 | 当前 APDU 和偏移保留 | 可写或适用的重试调度后继续 | + +其中,**启停槽满不再扣住已经生成的 CON**。限制发生在下一次改变状态的 ACT 受理之前;已有 CON 仍可续写。同状态重复 ACT 不需要新增启停通知槽,仍按自身控制发送条件处理。 + +不过,保留的 ACT 或 I 帧仍占据同一 TCP 字节流的前部,后续帧不能越过它。当前实现保留这个顺序约束,应用应及时消费通知。等待不直接等于断线,但不会延长原协议期限。 + +`NextEvent` 本身只复制、移除最早事件并安排后续工作,不执行接收、发送或协议受理。因此“读走一条通知”和“被暂缓的 ACT 得到受理”之间,还需要一次 Process/Poll 推进。 + +通知 FIFO 保证已经入队的通知逐条交付。启停变化不合并、不覆盖;但 ACK 回执若因普通容量或预算暂留内部,可能晚于随后入队的启停通知。不能把所有 RECORD 与启停事件理解为严格按网络发生时刻排序。 + + + +## 13. EOF、异常断开与重连 + +### 13.1 对端正常 EOF + +```text +IEC104_Net_Recv() 返回 EOF + → IEC104_AsduQueue_Limit_Normal_Send() + → 标记 rx_eof,结束接收批次并检查原期限 + → 继续发送当前已编号帧、已受理 HIGH 和控制帧 + → [内部,TcpServer] Prepare_Output() + ├─ 只是已确认记录等待事件转移而没有记录槽: + │ 保留应答,等预算恢复或 NextEvent 释放空间 + └─ 已排空,或继续需要新的 ACK / STARTDT: + Drop_Client() +``` + +EOF 后,尚未编号的 NORMAL 和后来新入队的 NORMAL 留给下一连接;已经编号的当前 NORMAL I 帧仍属于发送收尾范围。新提交的业务应答被底层 Send_Response 拒绝。收尾不再等待对端送来新的 ACK 或 STARTDT,原发送和协议期限仍生效。 + +### 13.2 统一断线收尾 + +接收错误、发送错误、协议故障、超时、`Close_Connection` 和服务 `Close` 都会进入相应断线收尾。 + +```text +[内部,TcpServer] Drop_Client() + 1. 必要时 IEC104_Connection_Handle_Transport_Closed(),登记传输故障 + 2. IEC104_Net_Close(),关闭客户端并使旧 socket token 失效 + 3. 复制尚未交付、但 Link 已受理的 I 帧 + 4. IEC104_Connection_Get_Record_By_Offset() + → 将剩余发送记录转为 RECORD,closing = 1 + 5. 保存 DISCONNECTED,作为本 generation 的最后一条事件 + 6. IEC104_Connection_Close() + → 结束 Receiver、Manager、SendBuffer + 7. IEC104_AsduQueue_Detach() + → 清 HIGH,保留现存 NORMAL,恢复为离线待发 + 8. 清理客户端运行状态 +``` + +收尾使用专用预留,不覆盖已有通知。`closing = 1` 只表示记录由收尾交付;是否完整写出、是否已 ACK,仍看记录中的两个事实字段,不能把所有收尾记录都当作未发送。 + +DISCONNECTED 保存关闭前的会话快照,主动关闭时该快照的 phase 也可能仍为 ACTIVE。判断当前是否持有 TCP 连接,应查询服务状态中的 `connected`;历史事件不会随新连接改写。 + +单纯断开不额外产生 DEACTIVATED。需要结束当前主站但继续监听时,应用调用: + +```text +IEC104_Slave_Close_Connection(slave, &原连接事件, error) + → 校验原 Slave 实例及事件身份 + → IEC104_TcpServer_Close_Connection(私有 server, 原 generation, error) +``` + +该接口立即收尾,不执行 STOPDT 握手或等待 ACK;它保留监听和 NORMAL。事件可以是原连接的连接/启停通知,也可以是 I_FRAME,不要求存在可用的 ASDU。其他服务实例的事件返回 INVALID_ARGUMENT;当前无连接返回 NOT_CONNECTED;已有新连接但原 generation 过期返回 STALE_GENERATION,均不会关闭新主站。 + +### 13.3 接入下一连接 + +```text +旧连接 I/O 结束 + → 旧事件已保存 + → Connection_Close + → AsduQueue_Detach + → 应用消费旧连接尚未读完的启停通知 + → 有足够 CONNECTED 普通通知空间 + → Net_Accept + → 分配新 generation + → AsduQueue_Rebind + → Connection_Init + → 保存 CONNECTED + → 新主站 STARTDT_ACT 受理 + → 保存 ACTIVATED,并按原顺序重新选择现存 NORMAL 组帧 +``` + +旧启停事件没读完时,不把新客户端接入协议会话;读完后,旧 DISCONNECTED 可以还在 FIFO 中,只要新建连通知所需空间足够,就能接入。 + +该场景保留的通知顺序为: + +```text +DISCONNECTED(旧 generation) + → CONNECTED(新 generation) + → ACTIVATED(新 generation,之后发生时) +``` + +NORMAL 恢复依据是仍保留的原文,不会恢复已经覆盖的旧条目。未 ACK 不保证对端没有收到,因此跨连接恢复可能重复,业务应按自身协议处理重复。 + + + +## 14. 接入应用自己的事件循环 + +独立循环使用 `IEC104_Slave_Poll` 即可;已有 select、GUI 或其他统一事件循环时,使用 `IEC104_Slave_Prepare / Process` 替代 Poll。Slave 复用 TCP 层的调度值类型,不需要应用取得私有 TcpServer。 + +```text +1. IEC104_Slave_NextEvent() + 先处理已有业务通知,逐条读到 EMPTY。 + +2. IEC104_Slave_Prepare(slave, &info, &error) + 取得 service_token、socket/token、关注位、定时条件。 + +3. 判断是否能等待: + pending_event_count > 0 → 继续消费事件,再重新 Prepare。 + need_process != 0 → 使用当前 service_token 构造本地空批次, + 调用 Process;之后处理事件并重新 Prepare。 + 其余情况 → 更新完整 socket 注册集合及一次性定时器, + 与应用其他任务一起等待。 + +4. 将本轮 socket 就绪和定时到期汇成一个 IEC104_TcpReady_t: + 同一 socket 的读写位合并。 + token 使用实际注册时保存的值。 + timer_expired 只在定时器真正到期时置 1。 + +5. IEC104_Slave_Process(slave, &ready, &error) + 不等待,有界推进当前输入、发送、期限和接入。 + +6. 回到第 1 步。 +``` + +本地唤醒可使用以下字段组合,不伪造网络就绪: + +```c +IEC104_TcpReady_t ready = {0}; +ready.service_token = info.service_token; +/* socket_count = 0,timer_expired = 0;随后调用 Process。 */ +``` + +应用保存 socket 只用于注册。recv、send、close 均由 TcpServer 执行。 + +`Slave_Enqueue_Event`、共用连接发送、NextEvent、Process、Close_Connection、Init/Close 都可能改变等待条件,下一次等待前应重新查询。更新时移除旧 socket 以及已撤销的读写位;没有有效定时条件时取消旧定时器。 + +`timer_active == 1 && timeout_ms == 0` 表示应立即调度;`timer_active == 0` 表示没有定时器,两者不能混淆。`UINT32_MAX` 仍是有限毫秒数。超时从 Prepare 查询时刻计起,若中途处理其他任务耗时较长,应在真正等待前重新 Prepare。采用边沿或一次性通知的事件循环也必须落实 `need_process`,并按最新快照重新注册,避免预算让出后无人续跑。 + +旧服务 token 的整批通知被忽略;已关闭或替换 socket 的旧注册通知也被忽略。不要把旧通知的 token 换成新值,否则会破坏隔离。 + + + +## 15. 整个服务关闭与对象释放 + +接入总召模块时,先Interrogation_Cancel,再读取所有业务FINISHED并清理应用资源;最后在Slave_Destroy之前释放总召实例。总召的业务FIFO和Slave的通知FIFO须分别读空,详见[总召接入](#flow-interrogation)。 + +使用共用站点表时,还须归还业务引用、结束全部快照、解除集合锁,并Remove所有注册节点后才可Slave_Destroy。节点Is_Reusable为true后才可释放;Close/Init保留站点表,不能用网络关闭代替业务清理。 + +```text +应用停止提交新业务 + → IEC104_Slave_Close() → IEC104_TcpServer_Close() + ├─ 当前客户端按 Drop_Client 流程收尾 + ├─ 关闭监听 socket + ├─ IEC104_AsduQueue_Abort(),清空服务级缓存 + └─ IEC104_Net_Cleanup() + → 应用撤销自有事件循环中的 socket 注册和定时器 + → IEC104_Slave_NextEvent() + 逐条处理保留下来的事件,直到 EMPTY + → IEC104_Slave_Destroy() + ├─ IEC104_TcpServer_Destroy(),归还私有 TCP 槽 + └─ 归还 Slave 槽,保留不回绕的实例编号历史 + → 应用保存的句柄置 NULL +``` + +Close 不等待业务发送或 ACK,保留未读事件,但不保留 NORMAL 缓存。Destroy 要求服务已经关闭或尚未 Init,且事件已读空;不会隐式丢弃事件或替调用者关闭运行中的服务。 + +Close 后读空事件,也可以再次 Init 重开;新服务和连接的身份编号不复用旧编号。Init 失败时句柄仍归应用持有,可处理错误后重试或 Destroy。 + +运行中 Destroy 返回 INVALID_STATE;已经关闭但仍有事件返回 EVENTS_PENDING。失败时句柄仍有效,不能假定资源已经归还。Close 即使返回错误也应保留当次诊断并检查状态、继续处理可读取的收尾通知,不能跳过返回值直接丢弃句柄。 + +生命周期与业务发送状态应分别判断: + +| 状态 | 可以做什么 | 需要等待或先处理什么 | +| --- | --- | --- | +| Create 成功,未 Init | 配置、Get_Status、取得标准应用层参数,或 Destroy | 网络及队列尚未运行 | +| Init 成功,尚无主站 | Poll/Prepare/Process、NORMAL 入队、状态查询 | 等待 CONNECTED,再等待 STARTDT | +| 已连接,Link STOPPED | 处理事件、缓存 NORMAL、继续驱动控制帧 | 新业务应答不能通过准入 | +| STARTED 且准入允许 | 原连接应答和 NORMAL 调度 | 仍受窗口、记录、队列和 socket 容量约束 | +| 单连接收尾、监听仍在 | 读取历史事件、保留 NORMAL,继续驱动下一次接入 | 已有任务不得绑定到新主站 | +| Close 后 | 读取剩余事件、Get_Status;读空后 Init 或 Destroy | Close 已清除 NORMAL/HIGH,不再执行网络推进 | +| Destroy 成功 | 将保存的句柄置 NULL | 旧服务指针不能继续使用 | + +## 16. 不在 TCP 子站主线中的已有接口 + +以下实现可独立使用,按需要由对应集成方调用: + +| 实现文件 | 已有调用关系或用途 | +| --- | --- | +| [IEC104.c](../../104/IEC104.c) | 旧全局逐字节入口:Rx_Reset / Legacy_Link_Reset 后调用 Rx_Feed_Byte。它不是 TcpServer 当前使用的 Receiver 路径,也不提供上述完整事件与发送调度 | +| [IEC104_Link_Master_StartStop.c](../../104/IEC104_Link_Master_StartStop.c) | 主站 Link 层 START:Prepare_Startdt_Act → 外部实际发送 → Complete_Startdt_Act_Send → Handle_Startdt_Con | +| 同上 | 主站 Link 层 STOP:Request_Stopdt → Prepare_Stopdt_Request,必要时先 S → 对应 Complete → Handle_Stopdt_Con。TCP 连接、发送缓冲和计时驱动仍需主站集成方提供 | +| [IEC60870_Time.c](../../common/IEC60870_Time.c) | 业务需要时调用日期校验、秒数/本地时间和 CP56Time2a 转换,不负责链路调度 | +| [IEC60870_Clock.c](../../common/IEC60870_Clock.c) | 提供业务时钟读取/设置;RTC 适配钩子由平台补充,不自动完成校时 ASDU 业务 | +| [IEC104_Log.c](../../104/IEC104_Log.c) | 应用按需要配置日志级别和日志输出回调,不承担收发事件调度 | + +阅读源码时,可沿以下顺序定位主要行为: + +```text +子站入口与身份绑定:IEC104_Slave.c +站总召/分组总召、撤销、业务期限与事件:104/business/IEC104_Interrogation.c +共用发送接口分派: IEC60870_Slave.c → IEC104_Slave.c 内的发送适配器 +ASDU 与信息对象: IEC60870_Asdu.c → IEC60870_InformationObjects.c +监听和网络调度: IEC104_TcpServer.c +接收字节与校验: IEC104_Receiver.c → IEC104_Codec.c + IEC60870_Codec.c +连接协议推进: IEC104_Connection.c +成组发送与 ACK: IEC104_SendManager.c +序号和启停规则: IEC104_Link.c + IEC104_Link_Slave_StartStop.c +帧字节和发送进度: IEC104_SendBuffer.c +业务 ASDU 缓存: IEC104_AsduQueue.c +平台非阻塞 I/O: IEC104_Net_Windows.c +``` + +库提供命令、校时、总召、参数和文件等信息对象的编解码,以及响应提交接口。独立召唤引擎已实现站/分组总召和计数量读取/冻结/复位,应用通过NextEvent与显式应答管理数据源。独立ClockSynchronization已实现Type 103校验、补偿、期限及原文确认,仅由Execute_Next调用应用动作。遥控、参数和文件业务尚未接入;业务级权限及硬件操作仍由应用提供。Time的编码转换、Clock的墙上时间与Net的协议单调时钟属于不同用途;默认示例校时只更新模拟值。 + diff --git a/IEC60870-Slave/.build/command-docs/check-api.cjs b/IEC60870-Slave/.build/command-docs/check-api.cjs new file mode 100644 index 00000000..760746fb --- /dev/null +++ b/IEC60870-Slave/.build/command-docs/check-api.cjs @@ -0,0 +1,58 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const root = path.resolve(__dirname, '../..'); +const document = fs.readFileSync(path.join(root, 'docs/104/IEC104_API接口汇总.md'), 'utf8'); +const headers = []; +function walk(folder) { + for (const entry of fs.readdirSync(folder, { withFileTypes: true })) { + const location = path.join(folder, entry.name); + if (entry.isDirectory()) walk(location); + else if (entry.name.endsWith('.h') && !entry.name.endsWith('_Internal.h')) headers.push(location); + } +} +walk(path.join(root, '104')); +walk(path.join(root, 'common')); +const stripComments = source => source.replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/\/\/[^\n]*/g, '') + .replace(/__attribute__\s*\(\(format\(printf,\s*2,\s*3\)\)\)/g, ''); +const signaturePattern = /^[ \t]*((?:extern\s+)?(?:const\s+)?[A-Za-z_]\w*(?:[ \t]+[A-Za-z_]\w*)*[ \t*]+(IEC(?:104|60870)_[A-Za-z0-9_]+)\s*\([^;{}]*\)\s*;)/gm; +const tokens = source => (source.replace(/^extern\s+/, '').match(/[A-Za-z_][A-Za-z0-9_]*|[0-9]+|[^\s]/g) || []).join(' '); +const declarationOnly = new Set(['IEC60870_Clock_RTC_Read', 'IEC60870_Clock_RTC_Set']); +const documented = new Map(); +for (const block of document.matchAll(/```c\n([\s\S]*?)```/g)) { + for (const match of stripComments(block[1]).matchAll(signaturePattern)) { + if (/^typedef\b/.test(match[1])) continue; + if (!documented.has(match[2])) documented.set(match[2], new Set()); + documented.get(match[2]).add(tokens(match[1])); + } +} +const missing = [], mismatched = []; +let checked = 0; +for (const header of headers) { + for (const match of stripComments(fs.readFileSync(header, 'utf8')).matchAll(signaturePattern)) { + if (/^typedef\b/.test(match[1])) continue; + const name = match[2]; + if (declarationOnly.has(name)) continue; + ++checked; + if (!documented.has(name)) missing.push({ header: path.relative(root, header), name }); + else if (!documented.get(name).has(tokens(match[1]))) { + mismatched.push({ header: path.relative(root, header), name, declaration: match[1], documented: [...documented.get(name)] }); + } + } +} +const commandHeader = stripComments(fs.readFileSync(path.join(root, '104/business/IEC104_Command.h'), 'utf8')); +const commandReference = document.slice(document.indexOf('## 25. IEC104_Command.c')); +const missingFields = []; +let checkedCommandFields = 0; +for (const structure of commandHeader.matchAll(/typedef\s+struct\s*\{([^{}]*)\}\s*(IEC104_Command\w+_t)\s*;/g)) { + for (const declaration of structure[1].split(';').map(x => x.trim()).filter(Boolean)) { + const names = declaration.split(',').map(x => /([A-Za-z_]\w*)\s*$/.exec(x)[1]); + for (const name of names) { + ++checkedCommandFields; + if (!new RegExp(`\\b${name}\\b`).test(commandReference)) missingFields.push(`${structure[2]}.${name}`); + } + } +} +console.log(JSON.stringify({ checked, documented: documented.size, declarationOnly: [...declarationOnly], + missing, mismatched, checkedCommandFields, missingFields }, null, 2)); +process.exitCode = missing.length || mismatched.length || missingFields.length ? 1 : 0; diff --git a/IEC60870-Slave/.build/command-docs/check-structure.cjs b/IEC60870-Slave/.build/command-docs/check-structure.cjs new file mode 100644 index 00000000..197a517a --- /dev/null +++ b/IEC60870-Slave/.build/command-docs/check-structure.cjs @@ -0,0 +1,46 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const assert = require('node:assert/strict'); +const root = path.resolve(__dirname, '../..'); +const files = ['docs/104/IEC104_API接口汇总.md', 'docs/104/IEC104_完整调用流程.md']; +for (const file of files) { + const bytes = fs.readFileSync(path.join(root, file)); + const source = new TextDecoder('utf-8', { fatal: true }).decode(bytes); + const baseline = fs.readFileSync(path.join(__dirname, path.basename(file) + '.before')); + assert.equal(bytes.subarray(0, 3).equals(Buffer.from([0xef, 0xbb, 0xbf])), + baseline.subarray(0, 3).equals(Buffer.from([0xef, 0xbb, 0xbf])), `${file}: BOM changed`); + assert(!source.includes('\ufffd') && !source.includes('\r'), `${file}: encoding or line ending`); + assert(source.endsWith('\n') && !/[\t ]+$/m.test(source), `${file}: whitespace`); + const ids = new Set(); + let fence = '', details = 0, blocks = 0; + const tableProblems = []; + let tableColumns = 0; + for (const [index, line] of source.split('\n').entries()) { + const marker = /^(`{3,}|~{3,})/.exec(line); + if (marker) { + if (!fence) { fence = marker[1]; blocks++; } + else if (marker[1][0] === fence[0] && marker[1].length >= fence.length) fence = ''; + tableColumns = 0; + continue; + } + if (fence) continue; + for (const match of line.matchAll(//g)) { + assert(!ids.has(match[1]), `${file}:${index + 1}: duplicate id ${match[1]}`); + ids.add(match[1]); + } + if (line.includes('
')) details++; + if (line.includes('
')) details--; + assert(details >= 0, `${file}:${index + 1}: unexpected details close`); + if (/^\|/.test(line)) { + const columns = (line.replace(/\\\|/g, '').match(/\|/g) || []).length; + if (tableColumns && columns !== tableColumns) tableProblems.push(index + 1); + tableColumns = columns; + } else tableColumns = 0; + } + assert(!fence && details === 0, `${file}: unclosed fence/details`); + assert.deepEqual(tableProblems, [], `${file}: inconsistent table column count`); + for (const id of baseline.toString('utf8').matchAll(//g)) { + assert(ids.has(id[1]), `${file}: removed existing anchor ${id[1]}`); + } + console.log(`${file}: ${blocks} fenced blocks, ${ids.size} stable anchors, UTF-8/LF and tables OK`); +} diff --git a/IEC60870-Slave/.build/command-docs/command_create_example.c b/IEC60870-Slave/.build/command-docs/command_create_example.c new file mode 100644 index 00000000..55def845 --- /dev/null +++ b/IEC60870-Slave/.build/command-docs/command_create_example.c @@ -0,0 +1,13 @@ +#include "IEC104_Command.h" + +static IEC104_Command_t *Create_Command(IEC104_Slave_t *slave) +{ + IEC104_CommandConfig_t config; + IEC104_Command_Config_Default(&config); + return IEC104_Command_Create(slave, &config); +} + +int main(void) +{ + return Create_Command(NULL) != NULL; +} diff --git a/IEC60870-Slave/.build/command-duplicate-order-20261030/command_fixture.c b/IEC60870-Slave/.build/command-duplicate-order-20261030/command_fixture.c new file mode 100644 index 00000000..de8c4ad4 --- /dev/null +++ b/IEC60870-Slave/.build/command-duplicate-order-20261030/command_fixture.c @@ -0,0 +1,1212 @@ +/* + * IEC104遥控业务回归:通过真实Slave、TCP及SendManager路径验证45/46事务。 + * 复用已有测试的可控Net、单调时钟和收发缓冲;重命名后的原测试入口不在本文件执行。 + * 输入及期望应答由字节手工构造,避免输入与被测编码器共享同一种错误。 + * 本文件已间接包含TcpServer.c,构建时不得再链接TcpServer.c或平台Net后端。 + * 同一组用例分别运行于TCP容量1/2、独立应答槽容量1/4,输出用例组数与公开结构尺寸。 + */ +#define main command_net_fixture_main +#include "iec104_tcp_event_ring_test.c" +#undef main +#include "IEC104_Command.h" + +#ifndef IEC104_COMMAND_REPLY_CAPACITY +#define IEC104_COMMAND_REPLY_CAPACITY 4U +#endif + +/* 每个用例独占一套服务、业务实例和站点;open/close负责隔离上一轮状态。 + * last_input保留修复自引用后的真实Slave事件,供构造发送拥塞或重入校验使用。 + * action_result控制模拟动作结果,action_calls和action_request核对至多一次执行及参数。 + * window只在窗口背压用例中缩小,close恢复默认值,防止影响下一组测试。 */ +static IEC104_Slave_t *command_slave; +static IEC104_Command_t *command; +static IEC104_Station_t command_station; +static IEC104_CommandStatus_t command_status; +static IEC104_SlaveEvent_t command_last_input; +static IEC104_CommandEventResult_t command_last_handled; +static unsigned command_master_ns, command_action_calls, command_checks; +static IEC104_CommandActionResult_t command_action_result; +static IEC104_CommandRequest_t command_action_request; +static uint16_t command_window = 32U; + +/* 消费全部Slave通知并按原FIFO交给Command,不在此消费业务事件或执行应用动作。 + * last_handled只记录最近一条I帧的分派结果,后续RECORD/启停通知不得覆盖它。 + * 用于验证“先读空通知,再推进动作”的接入约定。 */ +static void command_drain_transport(void) +{ + IEC104_SlaveEvent_t event; + IEC104_TcpEventResult_t result; + while ((result = IEC104_Slave_NextEvent(command_slave, &event)) == IEC104_TCP_EVENT_READY) { + IEC104_CommandEventResult_t handled; + if (event.transport.type == IEC104_TCP_EVENT_I_FRAME) { + CHECK(IEC104_Slave_Copy_Event(&command_last_input, &event)); + } + handled = IEC104_Command_Handle_Event(command, &event); + CHECK(handled != IEC104_COMMAND_EVENT_ERROR); + if (event.transport.type == IEC104_TCP_EVENT_I_FRAME) { command_last_handled = handled; } + } + CHECK(result == IEC104_TCP_EVENT_EMPTY); +} + +/* 推进一次真实传输层并读空通知;等待参数为0,可控后端不会真实阻塞。 */ +static void command_poll(void) +{ + CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK); + command_drain_transport(); +} + +/* 推进一步业务并刷新只读状态;等待或已正常终结也必须返回处理成功。 */ +static void command_process(void) +{ + CHECK(IEC104_Command_Process(command)); + CHECK(IEC104_Command_Get_Status(command, &command_status)); +} + +/* 有界交替驱动传输与业务,使已就绪应答交接、写出及通知消费能够完成。 + * 不自动Accept、不读取FINISHED,也不伪造主站ACK;仍然等待的事务由各用例显式处理。 */ +static void command_flush(void) +{ + unsigned index; + for (index = 0U; index < 16U; ++index) { + command_poll(); + command_process(); + } +} + +/* 经可控accept和真实STARTDT建立新会话,核对Command收到激活通知。 + * 新TCP会话的主站发送序号从0开始;事务及旧事件身份由业务模块自行隔离。 */ +static void command_connect(void) +{ + fake.accept_pending = 1U; + command_poll(); + push(start_act, sizeof(start_act)); + command_poll(); + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(command_status.active); + command_master_ns = 0U; +} + +/* 创建并激活测试服务,configuration为NULL时使用Command默认配置。 + * 链路和本地发送期限刻意放宽,使业务期限测试不会先触发无关的传输超时。 + * 查找内部TCP实例仅用于现有夹具的容量背压和故障注入,不替换公开收发入口。 */ +static void command_open(const IEC104_CommandConfig_t *configuration) +{ + IEC104_SlaveConfig_t server_configuration; + IEC104_CommandConfig_t defaults; + unsigned slot; + memset(&fake, 0, sizeof(fake)); + memset(&command_station, 0, sizeof(command_station)); + memset(&command_last_input, 0, sizeof(command_last_input)); + fake.now = 1000U; + command_action_calls = 0U; + command_action_result = IEC104_COMMAND_ACTION_STARTED; + command_slave = IEC104_Slave_Create(); + CHECK(command_slave != NULL); + IEC104_Slave_Config_Default(&server_configuration); + server_configuration.protocol.link.k = command_window; + server_configuration.protocol.link.t1_ms = 600000U; + server_configuration.protocol.link.t3_ms = 600000U; + server_configuration.protocol.local_send_timeout_ms = 600000U; + CHECK(IEC104_Slave_Init(command_slave, &server_configuration, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Add_Station(command_slave, &command_station, 1U, &command_action_calls) == IEC104_STATION_OK); + IEC104_Command_Config_Default(&defaults); + command = IEC104_Command_Create(command_slave, configuration == NULL ? &defaults : configuration); + CHECK(command != NULL); + sut = NULL; + for (slot = 0U; slot < IEC104_TCP_SERVER_CAPACITY; ++slot) { + if (server_pool[slot].in_use) { sut = &server_pool[slot].server; break; } + } + CHECK(sut != NULL); + command_connect(); +} + +/* 读取且核对恰好一个指定类型的业务事件,返回可跨循环保存的纯值副本。 + * 调用后事件才算已交付,后续是否欠应用FINISHED以此为界。 */ +static IEC104_CommandEvent_t command_take(IEC104_CommandEventType_t type) +{ + IEC104_CommandEvent_t event; + memset(&event, 0, sizeof(event)); + CHECK(IEC104_Command_NextEvent(command, &event) == IEC104_COMMAND_NEXT_READY); + CHECK(event.type == type); + return event; +} + +/* 核对业务队列确实为空,并验证EMPTY不改写调用方输出存储的接口契约。 */ +static void command_empty(void) +{ + IEC104_CommandEvent_t event, before; + memset(&event, 0x5a, sizeof(event)); + before = event; + CHECK(IEC104_Command_NextEvent(command, &event) == IEC104_COMMAND_NEXT_EMPTY); + CHECK(memcmp(&event, &before, sizeof(event)) == 0); +} + +/* 按Cancel、读完FINISHED、关闭服务、读空通知、Destroy、移除站点的顺序收尾。 + * 节点必须已可复用,才能销毁Slave;此处不替应用归还额外Acquire的引用。 */ +static void command_close(void) +{ + IEC104_CommandEvent_t event; + IEC104_CommandNextResult_t result; + IEC104_Command_Cancel(command); + while ((result = IEC104_Command_NextEvent(command, &event)) == IEC104_COMMAND_NEXT_READY) { + CHECK(event.type == IEC104_COMMAND_FINISHED); + } + CHECK(result == IEC104_COMMAND_NEXT_EMPTY); + CHECK(IEC104_Slave_Close(command_slave, NULL) == IEC104_TCP_OK); + command_drain_transport(); + CHECK(IEC104_Command_Destroy(command)); + if (IEC104_Station_Is_Registered(&command_station)) { + CHECK(IEC104_Slave_Remove_Station(command_slave, &command_station) == IEC104_STATION_OK); + } + CHECK(IEC104_Station_Is_Reusable(&command_station)); + CHECK(IEC104_Slave_Destroy(command_slave, NULL) == IEC104_TCP_OK); + command = NULL; + command_slave = NULL; + command_window = 32U; +} + +/* 将调用方提供的ASDU字节封装成主站I帧,维护主站N(S),不自动确认子站数据。 + * 只写入可控Net输入队列;何时接收、交付和推进业务由用例决定。 */ +static void command_queue(const uint8_t *payload, size_t length) +{ + uint8_t frame[255] = {0x68}; + CHECK(length <= 249U); + frame[1] = (uint8_t)(length + 4U); + frame[2] = (uint8_t)(command_master_ns << 1); + frame[3] = (uint8_t)(command_master_ns >> 7); + ++command_master_ns; + memcpy(frame + 6U, payload, length); + push(frame, length + 6U); +} + +/* 手工生成固定10字节的45/46请求或等长监视对象,用于输入和独立的期望结果。 + * cause包含完整COT字节(包括TEST/P/N),value保留原SCO/DCO或监视对象字节。 */ +static void command_bytes(uint8_t bytes[10], unsigned type, unsigned cause, + unsigned oa, unsigned ca, unsigned ioa, unsigned value) +{ + bytes[0] = (uint8_t)type; bytes[1] = 1U; bytes[2] = (uint8_t)cause; bytes[3] = (uint8_t)oa; + bytes[4] = (uint8_t)ca; bytes[5] = (uint8_t)(ca >> 8); + bytes[6] = (uint8_t)ioa; bytes[7] = (uint8_t)(ioa >> 8); bytes[8] = (uint8_t)(ioa >> 16); + bytes[9] = (uint8_t)value; +} + +/* 发送一条手工构造的请求并消费传输通知;不会自动读取或应答工作事件。 */ +static void command_send(unsigned type, unsigned cause, unsigned oa, unsigned ca, unsigned ioa, unsigned value) +{ + uint8_t bytes[10]; + command_bytes(bytes, type, cause, oa, ca, ioa, value); + command_queue(bytes, sizeof(bytes)); + command_poll(); +} + +/* 扫描已经写出的APDU,返回真实业务ASDU的地址及长度,保持线上原顺序。 + * 忽略S/U帧和制造拥塞的Type200填充帧;输入必须已写成完整帧。 + * 输出指针借用fake.output,各用例应在缓冲内容改变前完成断言。 */ +static unsigned command_collect(const uint8_t **frames, size_t *lengths) +{ + size_t offset = 0U; + unsigned count = 0U; + while (offset < fake.sent_bytes) { + const uint8_t *frame = fake.output + offset; + size_t length; + CHECK(fake.sent_bytes - offset >= 6U && frame[0] == 0x68U); + length = (size_t)frame[1] + 2U; + CHECK(length >= 6U && offset + length <= fake.sent_bytes); + if ((frame[2] & 1U) == 0U && frame[6] != 200U) { + CHECK(count < 64U); + frames[count] = frame + 6U; + lengths[count++] = length - 6U; + } + offset += length; + } + return count; +} + +/* 核对第index条业务输出的完整原文;同时检查长度、OA/CA/IOA、原命令值及COT。 + * 期望原文由手工字节构造,避免只检查状态计数而遗漏回显或帧序错误。 */ +static void command_reply_at(unsigned index, unsigned type, unsigned cause, + unsigned oa, unsigned ca, unsigned ioa, unsigned value) +{ + const uint8_t *frames[64]; + size_t lengths[64]; + uint8_t expected[10]; + CHECK(command_collect(frames, lengths) > index); + command_bytes(expected, type, cause, oa, ca, ioa, value); + CHECK(lengths[index] == sizeof(expected)); + CHECK(memcmp(frames[index], expected, sizeof(expected)) == 0); +} + +/* 返回已写出的业务I帧数量,过滤规则与command_reply_at一致。 */ +static unsigned command_frame_count(void) +{ + const uint8_t *frames[64]; size_t lengths[64]; + return command_collect(frames, lengths); +} + +/* 模拟短时应用动作:记录调用次数与模块实际交付的请求,返回用例预设结果。 + * 不发布数据、不推进网络,因此重复调用可直接识别为事务执行错误。 */ +static IEC104_CommandActionResult_t command_action(void *context, const IEC104_CommandRequest_t *request) +{ + CHECK(context == &command_action_calls); + ++command_action_calls; + command_action_request = *request; + return command_action_result; +} + +/* 受理并接受一次选择,推进其确认后返回原SELECT事件。 + * 固定使用CA=1、IOA=6001、OA=0x51,方便后续匹配与逐字段变异测试。 */ +static IEC104_CommandEvent_t command_select(unsigned type, unsigned value) +{ + IEC104_CommandEvent_t event; + command_send(type, 6U, 0x51U, 1U, 6001U, value | 0x80U); + event = command_take(IEC104_COMMAND_SELECT); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_flush(); + return event; +} + +/* 接受一次执行并返回其事件;刻意不等待确认交接,支持结果先于确认的测试。 */ +static IEC104_CommandEvent_t command_execute(unsigned type, unsigned value) +{ + IEC104_CommandEvent_t event; + command_send(type, 6U, 0x51U, 1U, 6001U, value); + event = command_take(IEC104_COMMAND_EXECUTE); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + return event; +} + +/* 构造CA=1、监视IOA=1001、COT=11的返回信息视图;监视IOA与遥控IOA不同。 + * object借用bytes,调用方在提交返回WAIT期间必须保留并维护该缓冲。 */ +static void command_return(IEC60870_Asdu_t *object, uint8_t bytes[10], unsigned type, unsigned value) +{ + command_bytes(bytes, type, 11U, 0x51U, 1U, 1001U, value); + CHECK(IEC60870_Asdu_Create_From_Buffer(object, + IEC104_Slave_Get_Application_Layer_Parameters(command_slave), bytes, 10U) == IEC60870_ASDU_OK); +} + +/* 报告给定设备结果、关闭返回信息阶段并读出FINISHED,核对成功/失败终结原因。 + * 只适用于需要的返回信息已经提交完毕、且无需额外ACK才能推进的用例。 */ +static IEC104_CommandEvent_t command_done(const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result) +{ + IEC104_CommandEvent_t finished; + command_flush(); + CHECK(IEC104_Command_Report_Result(command, event, result) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Complete(command, event) == IEC104_COMMAND_REPLY_OK); + command_flush(); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == (result == IEC104_COMMAND_RESULT_SUCCEEDED ? + IEC104_COMMAND_FINISH_COMPLETED : IEC104_COMMAND_FINISH_EXECUTION_FAILED)); + command_empty(); + return finished; +} + +/* 阻塞实际Net写入,再填满当前帧与HIGH容量,直到公开发送接口真实返回WAIT。 + * 业务层没有伪造的提交结果;解除send_blocked后由原发送管理器恢复推进。 */ +static void command_congest(void) +{ + uint8_t bytes[249] = {200, 0, 3, 0, 1, 0}; + IEC60870_Asdu_t filler; + unsigned index; + fake.send_blocked = 1U; + CHECK(IEC60870_Asdu_Create_From_Buffer(&filler, + IEC104_Slave_Get_Application_Layer_Parameters(command_slave), bytes, sizeof(bytes)) == IEC60870_ASDU_OK); + for (index = 0U; index < 32U; ++index) { + if (!IEC60870_MasterConnection_Send_ASDU(&command_last_input.connection, &filler)) { + CHECK(IEC104_Slave_Get_Last_Submit_Result(&command_last_input.connection) == IEC104_TCP_SUBMIT_WAIT); + return; + } + } + CHECK(0); +} + +/* 配置与实例契约:默认值、类型位边界、非法配置、重复绑定及NULL参数。 + * 先销毁合法空实例再逐项尝试非法配置,确保拒绝不是被已有实例掩盖。 */ +static void test_command_configuration(void) +{ + IEC104_CommandConfig_t config, invalid; + IEC104_CommandStatus_t status; + unsigned index; + IEC104_Command_Config_Default(&config); + CHECK(config.type_mask == 3U && config.select_required_mask == 3U); + CHECK(config.select_timeout_ms == 30000U && config.decision_timeout_ms == 15000U); + CHECK(config.execute_timeout_ms == 60000U && config.send_act_term && !config.act_term_on_failure); + CHECK(IEC104_COMMAND_TYPE_BIT(44U) == 0U && IEC104_COMMAND_TYPE_BIT(52U) == 0U); + CHECK(IEC104_COMMAND_TYPE_BIT(58U) == 128U && IEC104_COMMAND_TYPE_BIT(64U) == 8192U); + CHECK(IEC104_COMMAND_TYPE_BIT(65U) == 0U); + CHECK(IEC104_Command_Create(NULL, &config) == NULL); + CHECK(!IEC104_Command_Destroy(NULL)); + CHECK(!IEC104_Command_Get_Status(NULL, &status)); + command_open(NULL); + CHECK(IEC104_Command_Create(command_slave, &config) == NULL); + CHECK(IEC104_Command_Destroy(command)); + for (index = 0U; index < 5U; ++index) { + invalid = config; + switch (index) { + case 0U: invalid.type_mask = 0U; break; + case 1U: invalid.type_mask |= IEC104_COMMAND_TYPE_BIT(47U); break; + case 2U: invalid.select_timeout_ms = 0U; break; + case 3U: invalid.type_mask = 1U; invalid.select_required_mask = 2U; break; + default: invalid.select_required_mask = UINT16_MAX; break; + } + CHECK(IEC104_Command_Create(command_slave, &invalid) == NULL); + } + CHECK(IEC104_Command_Create(command_slave, NULL) == NULL); + command = IEC104_Command_Create(command_slave, &config); + CHECK(command != NULL); + command_close(); + ++command_checks; +} + +/* 接收校验顺序:先COT、再CA、再IOA、最后对象内容;TEST/P/N随否定回显保留。 + * 无唯一完整对象时静默丢弃,SQ和尾字节异常则规范成单对象10字节否定。 + * 另验证未支持或被type_mask禁用的类型返回IGNORED,不误交付工作事件。 */ +static void test_command_wire_validation(void) +{ + static const struct { unsigned type, cot, ca, ioa, value, cause; } cases[] = { + {45, 5, 65535, 0, 2, 45}, {45, 8, 65535, 0, 2, 46}, + {45, 6, 2, 0, 2, 46}, {45, 6, 1, 0, 2, 47}, + {45, 6, 1, 6001, 2, 7}, {46, 6, 1, 6001, 0, 7}, + {46, 6, 1, 6001, 3, 7}, {46, 8, 1, 6001, 3, 9}, + {45, 0x46, 1, 6001, 1, 7}, {45, 0x86, 1, 6001, 1, 0x87} + }; + unsigned index; + for (index = 0U; index < sizeof(cases) / sizeof(cases[0]); ++index) { + command_open(NULL); + command_send(cases[index].type, cases[index].cot, 0x61U, cases[index].ca, + cases[index].ioa, cases[index].value); + command_flush(); + CHECK(command_frame_count() == 1U); + command_reply_at(0U, cases[index].type, cases[index].cause | 0x40U, + 0x61U, cases[index].ca, cases[index].ioa, cases[index].value); + CHECK(command_status.rejected == 1U && command_action_calls == 0U); + command_empty(); + command_close(); + } + for (index = 0U; index < 5U; ++index) { + uint8_t bytes[14]; + size_t length = 10U; + command_open(NULL); + command_bytes(bytes, 45U, 6U, 0x62U, 1U, 6001U, 0x81U); + switch (index) { + case 0U: bytes[1] = 0U; break; + case 1U: bytes[1] = 2U; length = 14U; memset(bytes + 10U, 0, 4U); break; + case 2U: length = 9U; break; + case 3U: bytes[1] = 0x81U; break; + default: bytes[10] = 0x55U; length = 11U; break; + } + command_queue(bytes, length); command_poll(); command_flush(); + CHECK(command_frame_count() == (index < 3U ? 0U : 1U)); + if (index >= 3U) { command_reply_at(0U, 45U, 0x47U, 0x62U, 1U, 6001U, 0x81U); } + command_empty(); command_close(); + } + command_open(NULL); + command_send(47U, 6U, 0U, 1U, 6001U, 1U); + CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED); + { + uint8_t timed[17] = {0}; + command_bytes(timed, 58U, 6U, 0U, 1U, 6001U, 1U); + timed[14] = 1U; timed[15] = 1U; timed[16] = 26U; + command_queue(timed, sizeof(timed)); command_poll(); + } + CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED); + command_empty(); command_close(); + { + IEC104_CommandConfig_t config; + IEC104_Command_Config_Default(&config); + config.type_mask = config.select_required_mask = IEC104_COMMAND_TYPE_BIT(46U); + command_open(&config); + command_send(45U, 6U, 0U, 1U, 6001U, 0x81U); + CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED); + command_empty(); + command_send(46U, 6U, 0U, 1U, 6001U, 0x82U); + CHECK(command_take(IEC104_COMMAND_SELECT).request.type_id == 46U); + command_close(); + } + ++command_checks; +} + +/* 单/双命令的完整选择执行链:分别提交0、1、2帧返回信息,逐字节检查帧序。 + * 同时核对选择与执行属于同一事务但使用不同事件令牌、结果冻结、Complete一次性、 + * 旧SELECT只能查询事务不能报告执行结果,以及槽回收后旧令牌失效。 */ +static void test_command_complete_flows(void) +{ + unsigned type, count; + for (type = 45U; type <= 46U; ++type) { + for (count = 0U; count <= 2U; ++count) { + IEC104_CommandEvent_t selection, execution, finished; + IEC104_CommandTransactionInfo_t info; + IEC60870_Asdu_t data; + uint8_t bytes[10]; + unsigned index, value = type == 45U ? 1U : 2U; + command_open(NULL); + selection = command_select(type, value | 4U); + CHECK(selection.request.state == value && selection.request.qualifier == 1U); + CHECK(selection.request.is_select && !selection.request.selected); + execution = command_execute(type, value | 4U); + CHECK(execution.request.selected && !execution.request.is_select); + CHECK(execution.transaction_id == selection.transaction_id); + CHECK(execution.event_serial != selection.event_serial); + command_flush(); + CHECK(IEC104_Command_Get_Transaction(command, &selection, &info) && info.active); + CHECK(info.phase == IEC104_COMMAND_PHASE_EXECUTING && info.confirm_submitted); + CHECK(IEC104_Command_Report_Result(command, &selection, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + CHECK(IEC104_Command_Complete(command, &execution) == IEC104_COMMAND_REPLY_ERROR); + CHECK(IEC104_Command_Report_Result(command, &execution, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Report_Result(command, &execution, IEC104_COMMAND_RESULT_FAILED) == IEC104_COMMAND_REPLY_ERROR); + for (index = 0U; index < count; ++index) { + command_return(&data, bytes, type == 45U ? 1U : 3U, value); + CHECK(IEC104_Command_Submit_Return_Info(command, &execution, &data) == IEC104_COMMAND_REPLY_OK); + command_poll(); + } + CHECK(IEC104_Command_Complete(command, &execution) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Complete(command, &execution) == IEC104_COMMAND_REPLY_ERROR); + command_return(&data, bytes, 1U, 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &execution, &data) == IEC104_COMMAND_REPLY_ERROR); + command_flush(); + CHECK(command_frame_count() == count + 3U); + command_reply_at(0U, type, 7U, 0x51U, 1U, 6001U, value | 0x84U); + command_reply_at(1U, type, 7U, 0x51U, 1U, 6001U, value | 4U); + for (index = 0U; index < count; ++index) { + command_reply_at(index + 2U, type == 45U ? 1U : 3U, 11U, 0x51U, 1U, 1001U, value); + } + command_reply_at(count + 2U, type, 10U, 0x51U, 1U, 6001U, value | 4U); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_COMPLETED && finished.select_effect && finished.execute_effect); + CHECK(finished.request.receive_serial == execution.request.receive_serial); + CHECK(command_action_calls == 2U); + CHECK(!IEC104_Command_Get_Transaction(command, &execution, &info)); + CHECK(IEC104_Command_Complete(command, &execution) == IEC104_COMMAND_REPLY_STALE); + command_empty(); command_close(); + } + } + ++command_checks; +} + +/* 直接执行的结果/终止策略矩阵:设备成功或失败,组合两个ACT_TERM配置开关。 + * 明确失败终止的COT=10/P/N=0只表示流程结束,FINISHED仍保留EXECUTION_FAILED。 */ +static void test_command_result_policies(void) +{ + unsigned success, send_term, failure_term; + for (success = 0U; success < 2U; ++success) { + for (send_term = 0U; send_term < 2U; ++send_term) { + for (failure_term = 0U; failure_term < 2U; ++failure_term) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); + config.select_required_mask = 0U; + config.send_act_term = send_term != 0U; + config.act_term_on_failure = failure_term != 0U; + command_open(&config); + event = command_execute(45U, 1U); + CHECK(!event.request.selected); + finished = command_done(&event, success ? IEC104_COMMAND_RESULT_SUCCEEDED : IEC104_COMMAND_RESULT_FAILED); + CHECK(finished.execute_effect && !finished.select_effect); + CHECK(command_frame_count() == 1U + (send_term && (success || failure_term) ? 1U : 0U)); + command_reply_at(0U, 45U, 7U, 0x51U, 1U, 6001U, 1U); + if (send_term && (success || failure_term)) { command_reply_at(1U, 45U, 10U, 0x51U, 1U, 6001U, 1U); } + command_close(); + } + } + } + ++command_checks; +} + +/* 选择与执行各覆盖NOT_STARTED、STARTED、FAILED,检查确认、原因及副作用标志。 + * Accept的OK只代表事件消费;再次Accept不能重复动作。无返校动作的NULL选择 + * 可以生效,但不得错误标记为曾产生物理选择副作用。 */ +static void test_command_action_outcomes(void) +{ + unsigned selected, outcome; + for (selected = 0U; selected < 2U; ++selected) { + for (outcome = 0U; outcome < 3U; ++outcome) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); + command_send(45U, 6U, 0x51U, 1U, 6001U, selected ? 0x81U : 1U); + event = command_take(selected ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE); + command_action_result = (IEC104_CommandActionResult_t)outcome; + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + CHECK(command_action_calls == 1U); + command_flush(); + command_reply_at(0U, 45U, outcome == IEC104_COMMAND_ACTION_STARTED ? 7U : 0x47U, + 0x51U, 1U, 6001U, selected ? 0x81U : 1U); + if (outcome == IEC104_COMMAND_ACTION_STARTED) { + IEC104_Command_Cancel(command); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_CANCELLED); + } else { + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == (outcome == IEC104_COMMAND_ACTION_FAILED ? + IEC104_COMMAND_FINISH_ACTION_FAILED : IEC104_COMMAND_FINISH_REJECTED)); + } + CHECK(finished.select_effect == (selected && outcome != IEC104_COMMAND_ACTION_NOT_STARTED)); + CHECK(finished.execute_effect == (!selected && outcome != IEC104_COMMAND_ACTION_NOT_STARTED)); + command_close(); + } + } + { + IEC104_CommandEvent_t event, finished; + command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); + CHECK(IEC104_Command_Accept(command, &event, NULL, NULL) == IEC104_COMMAND_REPLY_OK); + command_flush(); IEC104_Command_Cancel(command); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(!finished.select_effect && command_action_calls == 0U); + command_close(); + } + ++command_checks; +} + +/* 事件身份和拒绝接口:分别篡改实例、事务、事件序号和类型,旧事件不得执行动作。 + * Reject只能消费一次;ENDED仍可查询,槽回收后查询失败且输出不变,新事务也不能 + * 重新接纳上一事务的令牌。 */ +static void test_command_rejection_and_stale(void) +{ + IEC104_CommandEvent_t event, stale, finished; + IEC104_CommandTransactionInfo_t info, before; + unsigned index; + command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); + for (index = 0U; index < 4U; ++index) { + stale = event; + switch (index) { + case 0U: ++stale.instance_id; break; + case 1U: ++stale.transaction_id; break; + case 2U: ++stale.event_serial; break; + default: stale.type = IEC104_COMMAND_EXECUTE; break; + } + CHECK(IEC104_Command_Accept(command, &stale, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + } + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + CHECK(IEC104_Command_Reject(command, &event, (IEC104_CommandRejectCause_t)0) == IEC104_COMMAND_REPLY_ERROR); + CHECK(IEC104_Command_Reject(command, &event, IEC104_COMMAND_REJECT_UNKNOWN_IOA) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Reject(command, &event, IEC104_COMMAND_REJECT_NEGATIVE) == IEC104_COMMAND_REPLY_STALE); + command_flush(); command_reply_at(0U, 45U, 0x6fU, 0x51U, 1U, 6001U, 0x81U); + CHECK(IEC104_Command_Get_Transaction(command, &event, &info) && !info.active); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_REJECTED && command_action_calls == 0U); + memset(&info, 0x5a, sizeof(info)); before = info; + CHECK(!IEC104_Command_Get_Transaction(command, &event, &info)); + CHECK(memcmp(&before, &info, sizeof(info)) == 0); + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + command_close(); + ++command_checks; +} + +/* 选择已确认后逐项改变执行类型、状态、QU、OA,必须取消原选择并回显新请求。 + * 其他点的新选择只得到忙否定,原选择仍可正常执行;默认禁止无选择直接执行。 */ +static void test_command_matching_and_busy(void) +{ + unsigned change; + for (change = 0U; change < 4U; ++change) { + IEC104_CommandEvent_t selected, finished; + unsigned type = change == 0U ? 46U : 45U; + unsigned value = change == 0U ? 2U : (change == 1U ? 0U : (change == 2U ? 5U : 1U)); + command_open(NULL); selected = command_select(45U, 1U); + command_send(type, 6U, change == 3U ? 0x52U : 0x51U, 1U, 6001U, value); + command_flush(); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_MISMATCH); + CHECK(finished.request.receive_serial == selected.request.receive_serial && finished.request.type_id == 45U); + command_reply_at(1U, type, 0x47U, change == 3U ? 0x52U : 0x51U, 1U, 6001U, value); + CHECK(command_action_calls == 1U); command_close(); + } + { + IEC104_CommandEvent_t event; + command_open(NULL); (void)command_select(45U, 1U); + command_send(45U, 6U, 0x51U, 1U, 6002U, 0x81U); command_flush(); + CHECK(command_status.busy && command_status.busy_rejected == 1U); + command_reply_at(1U, 45U, 0x47U, 0x51U, 1U, 6002U, 0x81U); + event = command_execute(45U, 1U); (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED); + command_close(); + } + command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); command_flush(); + CHECK(!command_status.busy); command_reply_at(0U, 45U, 0x47U, 0x51U, 1U, 6001U, 1U); + command_empty(); command_close(); + ++command_checks; +} + +/* FINISHED配对按事务历史判断:未读SELECT、未读直接EXECUTE不欠终结通知; + * SELECT曾交付后,即使匹配的EXECUTE尚未读,撤销也必须恰好交付一次FINISHED。 + * 撤销不要求命令值相同,DEACT_CON应回显撤销请求自身的值。 */ +static void test_command_finished_pairing(void) +{ + unsigned mode; + for (mode = 0U; mode < 4U; ++mode) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); + if (mode >= 2U) { + event = command_select(45U, 1U); + command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); + } else { + command_send(45U, 6U, 0x51U, 1U, 6001U, mode == 0U ? 0x81U : 1U); + } + if (mode == 3U) { event = command_take(IEC104_COMMAND_EXECUTE); } + command_send(45U, 8U, 0x51U, 1U, 6001U, 0U); command_flush(); + if (mode < 2U) { command_empty(); } + else { + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_DEACTIVATED); + CHECK(finished.transaction_id == event.transaction_id && finished.select_effect); + command_empty(); + } + command_reply_at(mode >= 2U ? 1U : 0U, 45U, 9U, 0x51U, 1U, 6001U, 0U); + command_close(); + } + ++command_checks; +} + +/* 选择确认未交接时的优先级:即使新执行值或类型不匹配,也只按实例忙拒绝。 + * 发送恢复后原选择仍有效,再发匹配执行应成功,不能提前走MISMATCH取消路径。 */ +static void test_command_unsubmitted_selection(void) +{ + IEC104_CommandEvent_t selected, executed; + IEC104_CommandTransactionInfo_t info; + command_open(NULL); + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + selected = command_take(IEC104_COMMAND_SELECT); command_congest(); + CHECK(IEC104_Command_Accept(command, &selected, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_process(); + CHECK(IEC104_Command_Get_Transaction(command, &selected, &info) && !info.confirm_submitted); + /* 选择确认尚在模块内等待;此时不匹配执行也不能使已生效选择作废。 */ + command_send(46U, 6U, 0x51U, 1U, 6001U, 2U); + CHECK(IEC104_Command_Get_Transaction(command, &selected, &info) && info.active); + CHECK(info.phase == IEC104_COMMAND_PHASE_SELECTED); + fake.send_blocked = 0U; command_flush(); + CHECK(command_status.mismatches == 0U && command_status.busy_rejected == 1U); + command_reply_at(0U, 45U, 7U, 0x51U, 1U, 6001U, 0x81U); + command_reply_at(1U, 46U, 0x47U, 0x51U, 1U, 6001U, 2U); + executed = command_execute(45U, 1U); + (void)command_done(&executed, IEC104_COMMAND_RESULT_SUCCEEDED); + CHECK(command_action_calls == 2U); + command_close(); ++command_checks; +} + +/* 应用结束与槽释放分离:先读取撤销FINISHED,但DEACT_CON仍被HIGH背压阻塞。 + * 事务必须继续占槽,Destroy失败,新选择忙拒绝;确认交接后才允许释放。 */ +static void test_command_deactivation_wait(void) +{ + IEC104_CommandEvent_t event, finished; + command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); command_congest(); + command_send(45U, 8U, 0x51U, 1U, 6001U, 0U); command_process(); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_DEACTIVATED); + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(command_status.busy && command_status.pending_replies != 0U); + CHECK(!IEC104_Command_Destroy(command)); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + command_send(45U, 6U, 0x52U, 1U, 6002U, 0x81U); + fake.send_blocked = 0U; command_flush(); + CHECK(!command_status.busy); + command_reply_at(0U, 45U, 9U, 0x51U, 1U, 6001U, 0U); + command_reply_at(1U, 45U, 0x47U, 0x52U, 1U, 6002U, 0x81U); + command_empty(); command_close(); + ++command_checks; +} + +/* 决定、选择、执行三种期限分别验证到期前、恰好到期和单调时间倒退。 + * 不先调用Process,而由Accept、Handle_Event、Report_Result或NextEvent触发检查, + * 防止过期状态被新的操作复活;关闭持续时长限制仍不能掩盖时钟倒退。 */ +static void test_command_deadlines(void) +{ + unsigned phase, boundary; + for (phase = 0U; phase < 3U; ++phase) { + for (boundary = 0U; boundary < 3U; ++boundary) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + uint64_t started; + IEC104_Command_Config_Default(&config); + config.decision_timeout_ms = config.select_timeout_ms = config.execute_timeout_ms = 10U; + command_open(&config); + if (phase == 0U) { + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); + } else { + event = command_select(45U, 1U); + if (phase == 2U) { event = command_execute(45U, 1U); command_flush(); } + } + started = fake.now; + if (phase == 1U) { + uint8_t bytes[10]; + command_bytes(bytes, 45U, 6U, 0x51U, 1U, 6001U, 1U); + command_queue(bytes, sizeof(bytes)); + CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK); + } + /* 传输接收时保持正常时间,再改变业务受理时刻,隔离链路计时的影响。 */ + fake.now = boundary == 2U ? started - 1U : started + 9U + boundary; + if (phase == 0U) { + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == + (boundary == 0U ? IEC104_COMMAND_REPLY_OK : IEC104_COMMAND_REPLY_STALE)); + } else if (phase == 1U) { + command_drain_transport(); + } else { + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == + (boundary == 0U ? IEC104_COMMAND_REPLY_OK : IEC104_COMMAND_REPLY_STALE)); + } + if (boundary != 0U) { + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == (phase == 0U ? IEC104_COMMAND_FINISH_DECISION_TIMEOUT : + (phase == 1U ? IEC104_COMMAND_FINISH_SELECT_TIMEOUT : IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT))); + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(command_status.clock_errors == (boundary == 2U ? 1U : 0U)); + } else if (phase == 1U) { + event = command_take(IEC104_COMMAND_EXECUTE); CHECK(event.request.selected); + } + fake.now = started + 10U; + command_close(); + } + } + /* 期限配置0仅关闭持续时长限制;单调时钟倒退仍须终结并累计clock_errors。 */ + for (phase = 0U; phase < 2U; ++phase) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); + config.select_required_mask = 0U; + config.decision_timeout_ms = config.execute_timeout_ms = 0U; + command_open(&config); + command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); + event = command_take(IEC104_COMMAND_EXECUTE); + if (phase != 0U) { + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_flush(); + } + --fake.now; + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(!command_status.timer_active && command_status.need_process); + if (phase == 0U) { + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + CHECK(command_action_calls == 0U); + } else { + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + } + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == (phase == 0U ? IEC104_COMMAND_FINISH_DECISION_TIMEOUT : IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT)); + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(command_status.clock_errors == 1U); + ++fake.now; + command_close(); + } + /* SELECT已经交付,即使后续EXECUTE尚未读取,决定超时仍欠应用一次FINISHED。 */ + { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event; + IEC104_Command_Config_Default(&config); config.decision_timeout_ms = 10U; + command_open(&config); (void)command_select(45U, 1U); + command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); fake.now += 10U; + event = command_take(IEC104_COMMAND_FINISHED); + CHECK(event.reason == IEC104_COMMAND_FINISH_DECISION_TIMEOUT); + command_empty(); command_close(); + } + ++command_checks; +} + +/* 设备结果与发送等待分离:ACT_CON被真实发送背压阻塞时先报告设备成功。 + * 随后跨过执行期限也不得超时或重复动作;解除背压后依次输出确认、返回信息、终止。 */ +static void test_command_result_before_confirm(void) +{ + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_CommandTransactionInfo_t info; + IEC60870_Asdu_t data; + uint8_t bytes[10]; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; config.execute_timeout_ms = 10U; + command_open(&config); command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); + event = command_take(IEC104_COMMAND_EXECUTE); command_congest(); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_process(); + CHECK(IEC104_Command_Get_Transaction(command, &event, &info) && !info.confirm_submitted); + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_OK); + command_return(&data, bytes, 1U, 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_WAIT); + fake.now += 100U; command_process(); + CHECK(command_status.execute_timeouts == 0U && command_action_calls == 1U); + fake.send_blocked = 0U; command_flush(); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Complete(command, &event) == IEC104_COMMAND_REPLY_OK); + command_flush(); finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_COMPLETED && command_frame_count() == 3U); + command_reply_at(0U, 45U, 7U, 0x51U, 1U, 6001U, 1U); + command_reply_at(1U, 1U, 11U, 0x51U, 1U, 1001U, 1U); + command_reply_at(2U, 45U, 10U, 0x51U, 1U, 6001U, 1U); + command_close(); ++command_checks; +} + +/* 全局FIFO与独立应答满池:连续忙请求填满固定槽,多出的新请求丢弃并计数。 + * 只排空传输缓冲而不推进Command时,Submit_Return_Info仍须等待模块内旧应答, + * 证明直接Try路径不能绕过业务FIFO;解除后按原请求OA顺序回包。 */ +static void test_command_fifo_and_overload(void) +{ + IEC104_CommandEvent_t event; + IEC60870_Asdu_t data; + uint8_t bytes[10]; + unsigned index; + command_open(NULL); (void)command_select(45U, 1U); event = command_execute(45U, 1U); command_flush(); + command_congest(); + for (index = 0U; index < IEC104_COMMAND_REPLY_CAPACITY + 2U; ++index) { + command_send(45U, 6U, 0x60U + index, 1U, 6002U, 0x81U); + } + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(command_status.pending_replies == IEC104_COMMAND_REPLY_CAPACITY); + CHECK(command_status.overloads == 2U && command_status.dropped_requests == 2U); + fake.send_blocked = 0U; + /* 只推进TCP,保留Command队首;底层空闲不能成为返回信息插队的理由。 */ + for (index = 0U; index < 16U; ++index) { command_poll(); } + command_return(&data, bytes, 1U, 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_WAIT); + command_flush(); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_OK); + command_poll(); (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED); + for (index = 0U; index < IEC104_COMMAND_REPLY_CAPACITY; ++index) { + command_reply_at(index + 2U, 45U, 0x47U, 0x60U + index, 1U, 6002U, 0x81U); + } + command_reply_at(IEC104_COMMAND_REPLY_CAPACITY + 2U, 1U, 11U, 0x51U, 1U, 1001U, 1U); + command_close(); ++command_checks; +} + +/* 返回信息必须完整验证类型、COT、P/N、TEST、CA、对象品质和精确长度。 + * 错误ASDU不得接手;合法的COT=3及与遥控点不同的监视IOA应可提交。 */ +static void test_command_return_validation(void) +{ + IEC104_CommandEvent_t event; + IEC60870_Asdu_t object; + uint8_t bytes[11]; + unsigned index; + command_open(NULL); (void)command_select(45U, 1U); event = command_execute(45U, 1U); command_flush(); + for (index = 0U; index < 7U; ++index) { + size_t length = 10U; + command_bytes(bytes, 1U, 11U, 0x51U, 1U, 1001U, 1U); + switch (index) { + case 0U: bytes[0] = 45U; break; + case 1U: bytes[2] = 5U; break; + case 2U: bytes[2] |= 0x40U; break; + case 3U: bytes[2] |= 0x80U; break; + case 4U: bytes[4] = 2U; break; + case 5U: bytes[9] = 2U; break; + default: bytes[10] = 1U; length = 11U; break; + } + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, + IEC104_Slave_Get_Application_Layer_Parameters(command_slave), bytes, length) == IEC60870_ASDU_OK); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &object) == IEC104_COMMAND_REPLY_ERROR); + } + command_bytes(bytes, 1U, 3U, 0x51U, 1U, 9876U, 0U); + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, + IEC104_Slave_Get_Application_Layer_Parameters(command_slave), bytes, 10U) == IEC60870_ASDU_OK); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &object) == IEC104_COMMAND_REPLY_OK); + command_poll(); (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED); + command_reply_at(2U, 1U, 3U, 0x51U, 1U, 9876U, 0U); + command_close(); ++command_checks; +} + +/* STOP/START通知优先于同一时刻的决定超时:未读通知时Accept只返回WAIT且不动作。 + * 按FIFO消费后,旧事务原因保持STOPPED,重新激活也不能复活旧事件。 */ +static void test_command_stop_notifications(void) +{ + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); config.decision_timeout_ms = 10U; + command_open(&config); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); + fake.now += 10U; + push(stop_act, sizeof(stop_act)); push(start_act, sizeof(start_act)); + CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_WAIT); + command_process(); CHECK(command_status.events_pending && command_status.decision_timeouts == 0U); + command_drain_transport(); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_STOPPED && command_action_calls == 0U); + command_empty(); CHECK(IEC104_Command_Get_Status(command, &command_status) && command_status.active); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + command_close(); + ++command_checks; +} + +/* 执行中断线不回滚动作,但必须交付DISCONNECTED并拒绝迟到结果。 + * 重连后的同点新事务具有新连接代次与事务号,旧令牌不能作用于新输出。 */ +static void test_command_disconnect_and_reuse(void) +{ + IEC104_CommandEvent_t event, next, finished; + IEC104_CommandConfig_t config; + uint64_t generation; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); event = command_execute(45U, 1U); command_flush(); + generation = event.request.generation; + fake.recv_eof = 1U; command_poll(); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_DISCONNECTED && finished.execute_effect); + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + command_empty(); fake.recv_eof = 0U; command_connect(); + next = command_execute(45U, 1U); + CHECK(next.request.generation != generation && next.transaction_id != event.transaction_id); + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + (void)command_done(&next, IEC104_COMMAND_RESULT_SUCCEEDED); command_close(); + ++command_checks; +} + +/* 原站点删除、同CA重新注册时,已受理事务仍持原节点,不能匹配替代站点的执行。 + * 应用在工作事件阶段额外Acquire,保证FINISHED出队并释放模块引用之后仍能清理; + * 最后一次应用Release才允许旧节点复用。 */ +static void test_command_station_lifetime(void) +{ + IEC104_CommandEvent_t event, finished; + IEC104_Station_t replacement = {0}; + uint64_t original; + command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); original = event.request.station_id; + CHECK(IEC104_Slave_Acquire_Station(command_slave, &command_station) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(command_slave, &command_station) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&command_station)); + CHECK(IEC104_Slave_Add_Station(command_slave, &replacement, 1U, &replacement) == IEC104_STATION_OK); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_flush(); CHECK(command_action_request.station_id == original); + command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); command_flush(); + CHECK(command_action_calls == 1U && command_status.busy); + command_reply_at(1U, 45U, 0x47U, 0x51U, 1U, 6001U, 1U); + IEC104_Command_Cancel(command); finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_CANCELLED && finished.request.station_id == original); + CHECK(IEC104_Slave_Get_Station_By_Id(command_slave, original) == &command_station); + CHECK(IEC104_Slave_Release_Station(command_slave, &command_station) == IEC104_STATION_OK); + CHECK(IEC104_Station_Is_Reusable(&command_station)); + CHECK(IEC104_Slave_Remove_Station(command_slave, &replacement) == IEC104_STATION_OK); + command_close(); ++command_checks; +} + +/* 供动作回调使用的已交付事件,专门验证重入时不能再次消费或销毁当前事务。 */ +static IEC104_CommandEvent_t command_reentry_event; +/* 回调中只读查询应成功,推进、消费、拒绝、销毁及重复动作应被拒绝。 + * Cancel为无效果操作;返回后原动作仍只执行一次,并可正常报告结果。 */ +static IEC104_CommandActionResult_t command_reentry_action(void *context, const IEC104_CommandRequest_t *request) +{ + IEC104_CommandTransactionInfo_t info; + IEC104_CommandEvent_t ignored; + IEC104_CommandStatus_t status; + CHECK(IEC104_Command_Get_Status(command, &status)); + CHECK(IEC104_Command_Get_Transaction(command, &command_reentry_event, &info)); + CHECK(!IEC104_Command_Destroy(command)); + CHECK(!IEC104_Command_Process(command)); + CHECK(IEC104_Command_NextEvent(command, &ignored) == IEC104_COMMAND_NEXT_ERROR); + CHECK(IEC104_Command_Accept(command, &command_reentry_event, command_action, context) == IEC104_COMMAND_REPLY_ERROR); + CHECK(IEC104_Command_Reject(command, &command_reentry_event, IEC104_COMMAND_REJECT_NEGATIVE) == IEC104_COMMAND_REPLY_ERROR); + CHECK(IEC104_Command_Handle_Event(command, &command_last_input) == IEC104_COMMAND_EVENT_ERROR); + IEC104_Command_Cancel(command); + return command_action(context, request); +} + +/* EXECUTE必须提供动作;回调重入受测入口不会改变当前事务或触发第二次动作。 */ +static void test_command_reentry(void) +{ + IEC104_CommandConfig_t config; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); + command_reentry_event = command_take(IEC104_COMMAND_EXECUTE); + CHECK(IEC104_Command_Accept(command, &command_reentry_event, NULL, NULL) == IEC104_COMMAND_REPLY_ERROR); + CHECK(IEC104_Command_Accept(command, &command_reentry_event, command_reentry_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + CHECK(command_action_calls == 1U); + (void)command_done(&command_reentry_event, IEC104_COMMAND_RESULT_SUCCEEDED); + command_close(); ++command_checks; +} + +/* 将k缩为1,逐帧给出真实主站ACK:选择确认、执行确认、返回信息各占一个窗口。 + * 未获得容量时Submit_Return_Info返回WAIT,Complete可关闭阶段但TERM仍等待窗口, + * 最后交接TERM后才交付完成通知,随后按统一生命周期释放资源。 */ +static void test_command_window_and_cancel(void) +{ + IEC104_CommandEvent_t event; + IEC60870_Asdu_t data; + uint8_t bytes[10]; + command_window = 1U; command_open(NULL); (void)command_select(45U, 1U); + event = command_execute(45U, 1U); command_process(); + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_OK); + command_return(&data, bytes, 1U, 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_WAIT); + push_ack(1U); command_poll(); command_flush(); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_WAIT); + push_ack(2U); command_poll(); command_flush(); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Complete(command, &event) == IEC104_COMMAND_REPLY_OK); + command_flush(); CHECK(command_frame_count() == 3U); + push_ack(3U); command_poll(); command_flush(); + CHECK(command_take(IEC104_COMMAND_FINISHED).reason == IEC104_COMMAND_FINISH_COMPLETED); + CHECK(command_frame_count() == 4U); command_close(); + ++command_checks; +} + +/* TCP容量至少为2时创建第二个独立Slave,验证实例池可分配且事件归属不串服务。 + * 外来Slave通知被拒绝,外来工作事件不能触发动作;容量1构建仍保留其余测试。 */ +static void test_command_two_services(void) +{ +#if IEC104_TCP_SERVER_CAPACITY >= 2 + IEC104_Slave_t *other; + IEC104_Command_t *other_command; + IEC104_CommandConfig_t config; + IEC104_SlaveConfig_t service_config; + IEC104_CommandEvent_t event; + command_open(NULL); + other = IEC104_Slave_Create(); CHECK(other != NULL); + IEC104_Slave_Config_Default(&service_config); + CHECK(IEC104_Slave_Init(other, &service_config, NULL) == IEC104_TCP_OK); + IEC104_Command_Config_Default(&config); + other_command = IEC104_Command_Create(other, &config); CHECK(other_command != NULL); + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); + CHECK(IEC104_Command_Handle_Event(other_command, &command_last_input) == IEC104_COMMAND_EVENT_ERROR); + CHECK(IEC104_Command_Accept(other_command, &event, command_action, &command_action_calls) != IEC104_COMMAND_REPLY_OK); + CHECK(command_action_calls == 0U); + CHECK(IEC104_Command_Destroy(other_command)); + CHECK(IEC104_Slave_Close(other, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Destroy(other, NULL) == IEC104_TCP_OK); + command_close(); +#endif + ++command_checks; +} + +/* 对照NORMAL覆盖行为:先把一条返回信息入NORMAL,再以填充事件将其覆盖。 + * Command的返回接口在队列未排空时返回WAIT且不接手,排空后直接提交的返回帧 + * 仍能按顺序送出,证明命令返回路径没有依赖可覆盖缓存。 */ +static void test_command_normal_overwrite(void) +{ + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event; + IEC60870_Asdu_t object, filler; + uint8_t bytes[10], fill_bytes[249] = {200, 0, 3, 0, 1, 0}; + unsigned index; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); event = command_execute(45U, 1U); command_flush(); + command_return(&object, bytes, 1U, 0U); + CHECK(IEC104_Slave_Enqueue_Event(command_slave, &object) == IEC104_TCP_SUBMIT_OK); + CHECK(IEC60870_Asdu_Create_From_Buffer(&filler, + IEC104_Slave_Get_Application_Layer_Parameters(command_slave), fill_bytes, sizeof(fill_bytes)) == IEC60870_ASDU_OK); + for (index = 0U; index < IEC104_TCP_NORMAL_CAPACITY / sizeof(fill_bytes) + 4U; ++index) { + CHECK(IEC104_Slave_Enqueue_Event(command_slave, &filler) == IEC104_TCP_SUBMIT_OK); + } + command_return(&object, bytes, 1U, 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &object) == IEC104_COMMAND_REPLY_WAIT); + command_flush(); + /* NORMAL中的第一条返回信息(值0)已被覆盖;过滤填充帧后只有ACT_CON。 */ + CHECK(command_frame_count() == 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &object) == IEC104_COMMAND_REPLY_OK); + command_poll(); (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED); + CHECK(command_frame_count() == 3U); + command_reply_at(1U, 1U, 11U, 0x51U, 1U, 1001U, 1U); + command_close(); ++command_checks; +} + +/* 临时拒绝启停通知准入,让真实STOP停留在传输层,供提交入口再次处理。 */ +static int command_hold_transfer(void *context) { (void)context; return 0; } + +/* 利用原传输观察者的背压入口保留STOP,随后恢复观察者。 + * 此时服务仍显示已启动且通知队列为空,下一次提交必须亲自处理STOP竞态。 */ +static void command_retain_stop(void) +{ + IEC104_ConnectionTransferObserver_t observer = sut->connection.transfer_observer; + IEC104_TcpServerStatus_t status; + sut->connection.transfer_observer.can_accept = command_hold_transfer; + push(stop_act, sizeof(stop_act)); + CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK); + sut->connection.transfer_observer = observer; + CHECK(sut->rx_blocked && !sut->connection.receive_batch_pending); + CHECK(IEC104_Slave_Get_Status(command_slave, &status, NULL) == IEC104_TCP_OK); + CHECK(status.pending_event_count == 0U && status.connection.data_transfer_started); +} + +/* 先真实交付启停通知,再报告观察者失败,构造“已有STOP通知但结果为FATAL”。 + * 用于区分可以让出的正常STOP与必须立即终结的传输故障。 */ +static int command_fail_after_stop(void *context, IEC104_DataTransferState_t state) +{ + CHECK(Notify_Transfer_State(context, state)); + return 0; +} + +/* 事务确认与独立忙否定同时挂起时,在提交入口内才受理STOP。 + * 正常STOP应等待通知再以STOPPED收尾;注入FATAL则立即记SEND_ERROR,不能降为WAIT。 + * 两种路径都清理独立应答、保留副作用事实,并拒绝迟到Report_Result。 */ +static void test_command_stop_during_submit(void) +{ + unsigned fatal; + for (fatal = 0U; fatal < 2U; ++fatal) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); + event = command_take(IEC104_COMMAND_EXECUTE); command_congest(); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_process(); + command_send(45U, 6U, 0x62U, 1U, 6002U, 0x81U); + CHECK(IEC104_Command_Get_Status(command, &command_status) && command_status.pending_replies == 2U); + command_retain_stop(); fake.send_blocked = 0U; + if (fatal) { sut->connection.transfer_observer.notify = command_fail_after_stop; } + command_process(); + if (fatal) { + /* 即使已有STOP通知,FATAL仍必须保留发送错误语义,不能当成普通让出。 */ + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_SEND_ERROR); + sut->connection.transfer_observer.notify = Notify_Transfer_State; + command_drain_transport(); + } else { + CHECK(command_status.events_pending && command_status.busy && !command_status.need_process); + command_drain_transport(); finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_STOPPED); + } + CHECK(finished.execute_effect && command_action_calls == 1U); + CHECK(IEC104_Command_Get_Status(command, &command_status) && command_status.pending_replies == 0U); + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + command_empty(); command_close(); + } + ++command_checks; +} + +/* 无活动事务但仍有独立错误应答时,实例也不能销毁。 + * Cancel必须清理该应答池;不会因不存在工作事件而凭空产生FINISHED。 */ +static void test_command_independent_reply_lifetime(void) +{ + command_open(NULL); + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + command_congest(); + /* 取消尚未交付的事务,再制造独立错误应答,隔离应答池自身的生命周期。 */ + IEC104_Command_Cancel(command); + command_send(45U, 6U, 0x61U, 2U, 6001U, 0x81U); + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(!command_status.busy && command_status.pending_replies == 1U); + CHECK(!IEC104_Command_Destroy(command)); + IEC104_Command_Cancel(command); + CHECK(IEC104_Command_Get_Status(command, &command_status) && command_status.pending_replies == 0U); + command_empty(); command_close(); ++command_checks; +} + +/* 执行23组契约测试;各组内部的类型、动作结果、期限及策略组合分别断言。 + * 尺寸输出用于记录当前宿主ABI的公开结构布局,不当作MCU内存预算。 */ +int command_regression_main(void) +{ + test_command_configuration(); + test_command_wire_validation(); + test_command_complete_flows(); + test_command_result_policies(); + test_command_action_outcomes(); + test_command_rejection_and_stale(); + test_command_matching_and_busy(); + test_command_finished_pairing(); + test_command_unsubmitted_selection(); + test_command_deactivation_wait(); + test_command_deadlines(); + test_command_result_before_confirm(); + test_command_fifo_and_overload(); + test_command_return_validation(); + test_command_stop_notifications(); + test_command_disconnect_and_reuse(); + test_command_station_lifetime(); + test_command_reentry(); + test_command_window_and_cancel(); + test_command_two_services(); + test_command_normal_overwrite(); + test_command_stop_during_submit(); + test_command_independent_reply_lifetime(); + printf("Command tests passed: groups=%u tcp=%u replies=%u request=%zu event=%zu status=%zu\n", + command_checks, (unsigned)IEC104_TCP_SERVER_CAPACITY, (unsigned)IEC104_COMMAND_REPLY_CAPACITY, + sizeof(IEC104_CommandRequest_t), sizeof(IEC104_CommandEvent_t), sizeof(IEC104_CommandStatus_t)); + return 0; +} diff --git a/IEC60870-Slave/.build/command-duplicate-order-20261030/duplicate_order_probe.c b/IEC60870-Slave/.build/command-duplicate-order-20261030/duplicate_order_probe.c new file mode 100644 index 00000000..8abcf00b --- /dev/null +++ b/IEC60870-Slave/.build/command-duplicate-order-20261030/duplicate_order_probe.c @@ -0,0 +1,59 @@ +/* Reproduce the current behavior without changing production code or regression tests. */ +#include "command_fixture.c" + +static void duplicate_order_probe(unsigned type, unsigned mode, bool take_before_duplicate) +{ + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event; + IEC104_CommandTransactionInfo_t info; + const uint8_t *frames[64]; + size_t lengths[64]; + unsigned state = type == 45U ? 1U : 2U; + unsigned value = mode == 0U ? state | 0x80U : state; + unsigned base, before_accept, count; + + IEC104_Command_Config_Default(&config); + if (mode == 1U) { config.select_required_mask = 0U; } + command_open(&config); + if (mode == 2U) { (void)command_select(type, state); } + base = command_frame_count(); + command_send(type, 6U, 0x51U, 1U, 6001U, value); + if (take_before_duplicate) { + event = command_take(mode == 0U ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE); + } + CHECK(command_frame_count() == base); + command_send(type, 6U, 0x51U, 1U, 6001U, value); + command_flush(); + before_accept = command_frame_count(); + CHECK(before_accept == base + 1U); + command_reply_at(base, type, 0x47U, 0x51U, 1U, 6001U, value); + if (!take_before_duplicate) { + event = command_take(mode == 0U ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE); + } + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_flush(); + count = command_collect(frames, lengths); + CHECK(count == base + 2U); + command_reply_at(base + 1U, type, 0x07U, 0x51U, 1U, 6001U, value); + CHECK(IEC104_Command_Get_Transaction(command, &event, &info)); + CHECK(info.active); + CHECK(info.phase == (mode == 0U ? IEC104_COMMAND_PHASE_SELECTED : IEC104_COMMAND_PHASE_EXECUTING)); + printf("type=%u phase=%s event_taken=%u replies_before_accept=%u COT=0x%02X -> 0x%02X active_after_accept=%u\n", + type, mode == 0U ? "SELECT_WAIT" : (mode == 1U ? "EXECUTE_WAIT_direct" : "EXECUTE_WAIT_selected"), + (unsigned)take_before_duplicate, before_accept - base, + (unsigned)frames[base][2], (unsigned)frames[base + 1U][2], (unsigned)info.active); + command_close(); +} + +int main(void) +{ + unsigned type, mode, taken; + for (type = 45U; type <= 46U; ++type) { + for (mode = 0U; mode < 3U; ++mode) { + for (taken = 0U; taken < 2U; ++taken) { + duplicate_order_probe(type, mode, taken != 0U); + } + } + } + return 0; +} diff --git a/IEC60870-Slave/.build/command-lf-add-probe.txt b/IEC60870-Slave/.build/command-lf-add-probe.txt new file mode 100644 index 00000000..fbbee861 --- /dev/null +++ b/IEC60870-Slave/.build/command-lf-add-probe.txt @@ -0,0 +1,2 @@ +alpha +beta diff --git a/IEC60870-Slave/.build/command-v1/audit.cjs b/IEC60870-Slave/.build/command-v1/audit.cjs new file mode 100644 index 00000000..b6668e66 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/audit.cjs @@ -0,0 +1,75 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const crypto = require('node:crypto'); +const root = path.resolve(__dirname, '../..'); +const baseline = path.join(__dirname, 'before'); +const manifest = JSON.parse(fs.readFileSync(path.join(baseline, 'manifest.json'), 'utf8').replace(/^\uFEFF/, '')); +const prior = new Map(manifest.map(x => [x.Path.replaceAll('\\', '/'), x])); +const files = []; +function walk(dir) { + for (const entry of fs.readdirSync(dir, {withFileTypes: true})) { + if (entry.name.startsWith('.')) continue; + const file = path.join(dir, entry.name); + if (entry.isDirectory()) walk(file); + else if (entry.isFile()) files.push(file); + } +} +walk(root); +const errors = [], changed = []; +const utf8 = new TextDecoder('utf-8', {fatal: true}); +for (const file of files) { + const relative = path.relative(root, file).replaceAll('\\', '/'); + const bytes = fs.readFileSync(file); + const hash = crypto.createHash('sha256').update(bytes).digest('hex').toUpperCase(); + if (prior.get(relative)?.Hash === hash) continue; + changed.push(relative); + if (!/\.(c|h|md|ps1|cjs)$/.test(file)) continue; + let text; + try { text = utf8.decode(bytes); } catch { errors.push(`${relative}: invalid UTF-8`); continue; } + const bom = bytes.length >= 3 && bytes.subarray(0, 3).equals(Buffer.from([239, 187, 191])); + if (prior.has(relative) && prior.get(relative).Bom !== bom) errors.push(`${relative}: BOM changed`); + if (text.includes('\uFFFD')) errors.push(`${relative}: replacement character`); + if (text.includes('\r')) errors.push(`${relative}: non-LF newline`); + if (!text.endsWith('\n')) errors.push(`${relative}: missing terminal newline`); + if (/[^\S\n]+\n/.test(text)) errors.push(`${relative}: trailing whitespace`); +} +let linkCount = 0, anchorCount = 0; +const anchorCache = new Map(); +function anchors(file) { + if (anchorCache.has(file)) return anchorCache.get(file); + const text = fs.readFileSync(file, 'utf8').replace(/```[^\n]*\n[\s\S]*?```/g, ''); + const found = new Set(), duplicates = new Map(); + for (const match of text.matchAll(/\bid=["']([^"']+)["']/g)) found.add(match[1]); + for (const match of text.matchAll(/^#{1,6}\s+(.+)$/gm)) { + const slug = match[1].trim().toLowerCase().replace(/[^\p{L}\p{N}_\-\s]/gu, '').replace(/\s/g, '-'); + const count = duplicates.get(slug) || 0; + found.add(count ? `${slug}-${count}` : slug); + duplicates.set(slug, count + 1); + } + anchorCache.set(file, found); + return found; +} +for (const file of files.filter(f => f.endsWith('.md'))) { + const text = fs.readFileSync(file, 'utf8').replace(/```[^\n]*\n[\s\S]*?```/g, ''); + for (const match of text.matchAll(/\[[^\]\n]*\]\(([^)\n]+)\)/g)) { + let target = match[1].replace(/^<|>$/g, '').trim(); + if (/^[a-z]+:/i.test(target)) continue; + const fragment = decodeURIComponent(target.split('#')[1] || ''); + target = decodeURIComponent(target.split('#')[0]); + const resolved = target ? path.resolve(path.dirname(file), target) : file; + if (!resolved.toLowerCase().startsWith(root.toLowerCase() + path.sep)) { + errors.push(`${path.relative(root, file)}: link outside workspace ${target}`); continue; + } + ++linkCount; + if (!fs.existsSync(resolved)) errors.push(`${path.relative(root, file)}: missing ${target}`); + else if (fragment && resolved.endsWith('.md')) { + ++anchorCount; + if (!anchors(resolved).has(fragment)) errors.push(`${path.relative(root, file)}: missing anchor ${target}#${fragment}`); + } + } +} +for (const old of prior.keys()) { + if (!fs.existsSync(path.join(root, old))) errors.push(`Missing original file: ${old}`); +} +console.log(JSON.stringify({changed, checkedLocalLinks: linkCount, checkedAnchors: anchorCount, errors}, null, 2)); +process.exitCode = errors.length ? 1 : 0; diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104.c b/IEC60870-Slave/.build/command-v1/before/104/IEC104.c new file mode 100644 index 00000000..9e0b1d17 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104.c @@ -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; + } +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104.h new file mode 100644 index 00000000..8318878b --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104.h @@ -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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_AsduQueue.c b/IEC60870-Slave/.build/command-v1/before/104/IEC104_AsduQueue.c new file mode 100644 index 00000000..e5666a24 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_AsduQueue.c @@ -0,0 +1,1031 @@ +/** + * 子站两级ASDU字节环:HIGH只保存队列待发条目;NORMAL另保留已登记发送交接、仍待ACK的前缀。 + * 条目在逻辑上连续,头与原文均可跨数组尾部回绕,不为尾部空隙浪费容量。 + * HIGH空间不足时拒绝;NORMAL覆盖最旧原文,包括已提交条目,但保留其待确认计数。 + * SendBuffer接手I帧且Manager建立记录后,Mark_Submitted才完成队列记账:HIGH消费原文,NORMAL保留原文。 + * Manager初始化成功、受理新STOP或从STOPPED受理START时清空HIGH;已启用时重复START保留HIGH。 + * Rebind清空HIGH并恢复NORMAL现存原文。 + * 本模块负责存储及条目身份;选择优先级、分配N(S)、确认合法性由Manager和Link处理。 + * HIGH只存长度;NORMAL存编号、状态及长度。仅NORMAL入队分配entry_serial,与I帧的N(S)不同。 + * 所有存储由调用方提供;同连接操作串行,输入/输出与内部存储不得重叠。 + */ +#include "IEC104_AsduQueue.h" + +#include +#include + +/* + * 编号环为1..UINT64_MAX,跳过无效值0;活跃编号到next_entry_serial的前向距离至多半环。 + * 队列活跃编号包括仍有原文的条目和NORMAL覆盖待ACK条目,不能按普通整数大小判断先后。 + * HIGH不分配条目编号,也不占编号窗口;Manager使用独立frame_id和N(S)处理完成与ACK。 + */ +#define ENTRY_SERIAL_WINDOW_MAX (UINT64_MAX / 2U) + +/* 队列记账状态,对应公开快照is_submitted的0/1值;不是NORMAL条目头里的编码。 + * PENDING表示队列尚未登记发送交接,不能据此断言SendBuffer是否已经接手。 + * HIGH在Mark_Submitted中直接消费原文,因此环内没有RETAINED_UNACKED状态。 */ +enum { + QUEUE_ENTRY_PENDING = 0, + QUEUE_ENTRY_RETAINED_UNACKED = 1 +}; + +/* NORMAL条目头的实际存储编码:1为队列待发,2为已登记交接且保留原文等待ACK。 */ +enum { + NORMAL_HEADER_PENDING = 1, + NORMAL_HEADER_RETAINED_UNACKED = 2 +}; + +/* 编译期检查:NORMAL头的asdu_length仅8位,可表示0..255,当前ASDU长度上限249满足要求。 + * 若以后将上限提高到255以上,此处会编译报错,提醒同步调整长度字段及该约束,避免长度被截断。 + * 此检查不生成运行时代码,也不增加条目存储开销。 */ +_Static_assert(IEC104_MAX_ASDU_LEN <= UINT8_MAX, "NORMAL ASDU length must fit in eight bits"); + +/** + * 功能:取得指定优先级每条记录的管理头长度,用于容量计算和定位ASDU起点。 + * 输入参数:priority——调用方已验证为HIGH或NORMAL。 + * 输出参数:无。 + * 返回值:HIGH为sizeof(uint16_t),NORMAL为sizeof(IEC104_AsduNormalEntryHeader_t),单位为字节。 + * 说明:长度不包含ASDU原文;NORMAL结果包含当前编译器的结构体填充,不是固定的协议长度。 + */ +static uint32_t Entry_Header_Length(IEC104_AsduPriority_t priority) +{ + return priority == IEC104_ASDU_PRIORITY_HIGH ? + IEC104_ASDU_HIGH_ENTRY_HEADER_LEN : IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN; +} + +/** + * 功能:计算编号环中的后继编号,跳过保留值0。 + * 输入参数:serial——1..UINT64_MAX内的有效编号。 + * 输出参数:无,不修改队列或连接代次。 + * 返回值:普通编号加1;UINT64_MAX的后继为1。 + * 说明:0留作无效编号;回绕只计算编号,不会清空队列或触发连接重绑。 + */ +static uint64_t Entry_Serial_Next(uint64_t serial) +{ + return serial == UINT64_MAX ? 1U : serial + 1U; +} + +/** + * 功能:计算从from沿编号递增方向到to所需的步数,允许跨上限回绕。 + * 输入参数:from/to——调用方保证均在1..UINT64_MAX内。 + * 输出参数:无。 + * 返回值:0..UINT64_MAX-1内的前向距离;编号相同时为0。 + * 说明:有效编号共UINT64_MAX个,按此环长计算;例如UINT64_MAX到1的距离为1。 + * 直接用uint64_t减法会按2^64取模,包含已跳过的0,不能用于本编号环。 + */ +static uint64_t Entry_Serial_Distance(uint64_t from, uint64_t to) +{ + return to >= from ? to - from : UINT64_MAX - from + to; +} + +/** + * 功能:按半环规则判断older是否严格早于newer。 + * 输入参数:older/newer——待比较的内部条目编号,0按无效编号处理。 + * 输出参数:无。 + * 返回值:两者非零且前向距离在1..ENTRY_SERIAL_WINDOW_MAX内时为1,否则0。 + * 说明:先后关系仅适用于跨度受限的活跃窗口,不能判断任意历史编号的年代。 + * UINT64_MAX早于1,但1不早于UINT64_MAX;相同编号不构成先后关系。 + * 限制在半环内是为了避免回绕时混淆活跃编号的新旧关系;这里比较的是编号距离,不是条目数量。 + */ +static int Entry_Serial_Before(uint64_t older, uint64_t newer) +{ + uint64_t distance; + + if (older == 0U || newer == 0U) { + return 0; + } + distance = Entry_Serial_Distance(older, newer); + return distance != 0U && distance <= ENTRY_SERIAL_WINDOW_MAX; +} + +/** + * 功能:检查业务优先级是否为HIGH或NORMAL。 + * 输入参数:priority——调用方传入的优先级。 + * 输出参数:无。 + * 返回值:有效返回1,否则0;COUNT只用于数组长度,不能作为业务优先级。 + */ +static int Priority_Valid(IEC104_AsduPriority_t priority) +{ + return priority == IEC104_ASDU_PRIORITY_HIGH || priority == IEC104_ASDU_PRIORITY_NORMAL; +} + +/** + * 功能:沿字节环前进指定长度,跨数组尾部时从下标0继续。 + * 输入参数:bucket——容量非零的桶;index——有效下标;length——0..byte_capacity。 + * 输出参数:无。 + * 返回值:前进后的有效数组下标,length等于总容量时回到原位置。 + * 说明:先比较尾部剩余长度再计算,避免index+length在32位边界溢出。 + * 例如容量100、index为90、length为20,结果为10;本函数不校验占用范围或移动实际字节。 + */ +static uint32_t Ring_Advance(const IEC104_AsduPriorityQueue_t *bucket, uint32_t index, uint32_t length) +{ + uint32_t tail_bytes = bucket->byte_capacity - index; + return length >= tail_bytes ? length - tail_bytes : index + length; +} + +/** + * 功能:从环中读取一段逻辑连续字节,必要时分两次复制。 + * 输入参数:bucket/index——已验证的字节环及起点;length——不超过总容量的读取长度。 + * 输出参数:destination——至少length字节、与环不重叠的可写区。 + * 返回值:无。 + * 说明:调用方先验证逻辑占用范围;本函数只处理物理回绕,不检查条目身份。 + * 例如容量100,从90读取20字节,先复制尾部10字节,再复制开头10字节。 + * 通过memcpy读取到调用方对象,条目头跨尾部或环内地址未对齐时也无需强转结构体指针。 + */ +static void Ring_Read(const IEC104_AsduPriorityQueue_t *bucket, uint32_t index, + void *destination, uint32_t length) +{ + uint32_t first_length = bucket->byte_capacity - index; + if (first_length > length) { + first_length = length; + } + memcpy(destination, bucket->bytes + index, first_length); + if (length > first_length) { + memcpy((uint8_t *)destination + first_length, bucket->bytes, length - first_length); + } +} + +/** + * 功能:将一段逻辑连续字节写入环,必要时分两次复制。 + * 输入参数:bucket/index——已验证的目标环及起点;source——不与环重叠的输入; + * length——不超过总容量且已获得空间保障的长度。 + * 输出参数:bucket->bytes——写入对应的尾部和起始部分,不改变任何游标。 + * 返回值:无。 + * 说明:Enqueue先完成全部容量检查,再写头和原文,最后统一发布控制字段。 + * 本函数只复制字节,不决定哪些旧条目可覆盖,也不更新used_bytes或条目计数。 + */ +static void Ring_Write(const IEC104_AsduPriorityQueue_t *bucket, uint32_t index, + const void *source, uint32_t length) +{ + uint32_t first_length = bucket->byte_capacity - index; + if (first_length > length) { + first_length = length; + } + memcpy(bucket->bytes + index, source, first_length); + if (length > first_length) { + memcpy(bucket->bytes, (const uint8_t *)source + first_length, length - first_length); + } +} + +/** + * 功能:检查单个优先级字节环的边界、占用量和待发游标是否自洽。 + * 输入参数:bucket——非NULL控制结构;priority——已验证的优先级,用于确定本级头长。 + * 输出参数:无,不读取条目头或修改字节。 + * 返回值:控制字段自洽返回1,否则0。 + * 说明:used_bytes只计条目头和ASDU,无回绕空隙;它区分head==tail时的满与空。 + * head==tail且used_bytes为0表示空环,为byte_capacity表示满环。 + * 检查为常数时间,不验证每条记录的头;完整条目布局由Bucket_Entries_Valid检查。 + */ +static int Bucket_Valid(const IEC104_AsduPriorityQueue_t *bucket, IEC104_AsduPriority_t priority) +{ + uint32_t unsubmitted_distance; + uint32_t minimum_size = Entry_Header_Length(priority) + IEC104_ASDU_HEADER_LEN; + + if (bucket->bytes == NULL || bucket->byte_capacity < minimum_size || + bucket->head_index >= bucket->byte_capacity || bucket->tail_index >= bucket->byte_capacity || + bucket->unsubmitted_index >= bucket->byte_capacity || bucket->used_bytes > bucket->byte_capacity || + bucket->unsubmitted_count > bucket->entry_count || + bucket->entry_count > bucket->used_bytes / minimum_size) { + return 0; + } + if (bucket->entry_count == 0U) { + return bucket->used_bytes == 0U && bucket->head_index == 0U && + bucket->tail_index == 0U && bucket->unsubmitted_index == 0U; + } + /* 条目紧接存放,从队首经过全部占用字节后必须正好到队尾。 */ + if (Ring_Advance(bucket, bucket->head_index, bucket->used_bytes) != bucket->tail_index) { + return 0; + } + if (bucket->unsubmitted_count == 0U) { + return bucket->unsubmitted_index == bucket->tail_index; + } + if (bucket->unsubmitted_count == bucket->entry_count) { + return bucket->unsubmitted_index == bucket->head_index; + } + /* 同时存在保留待ACK前缀和待发后缀时,待发游标应严格位于占用区内部。 */ + unsubmitted_distance = bucket->unsubmitted_index >= bucket->head_index ? + bucket->unsubmitted_index - bucket->head_index : + bucket->byte_capacity - bucket->head_index + bucket->unsubmitted_index; + return unsubmitted_distance != 0U && unsubmitted_distance < bucket->used_bytes; +} + +/** + * 功能:检查业务队列的初始化标志、连接身份及两个字节环。 + * 输入参数:queue——待检查对象,允许NULL。 + * 输出参数:无。 + * 返回值:整体控制状态合法返回1,否则0;不检查TCP状态或逐条遍历ASDU。 + * 说明:NORMAL覆盖待确认计数与身份边界须一致;原文已覆盖不代表协议已确认。 + * HIGH原文在登记交接时出队,因此其保留条目数必须等于待发条目数。 + * 代次0表示离线缓存,HIGH、已提交记账及EOF发送限制必须为空;具体代次匹配由入口核对。 + */ +static int Queue_State_Valid(const IEC104_AsduQueue_t *queue) +{ + uint64_t overwritten_span; + + if (queue == NULL || queue->is_initialized != 1U || + queue->next_entry_serial == 0U || + !Bucket_Valid(&queue->priority_queues[IEC104_ASDU_PRIORITY_HIGH], IEC104_ASDU_PRIORITY_HIGH) || + !Bucket_Valid(&queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL], IEC104_ASDU_PRIORITY_NORMAL) || + queue->priority_queues[IEC104_ASDU_PRIORITY_HIGH].entry_count != + queue->priority_queues[IEC104_ASDU_PRIORITY_HIGH].unsubmitted_count) { + return 0; + } + if (queue->normal_send_limited > 1U) { + return 0; + } + if (queue->connection_generation == 0U && + (queue->priority_queues[IEC104_ASDU_PRIORITY_HIGH].entry_count != 0U || + queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL].entry_count != + queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL].unsubmitted_count || + queue->overwritten_unacked_count != 0U || queue->normal_send_limited != 0U)) { + return 0; + } + if (queue->overwritten_unacked_count == 0U) { + /* 覆盖前缀已清完时不保留旧边界,避免已确认的历史编号继续占住窗口。 */ + return queue->overwritten_unacked_serial_end == 0U && queue->overwritten_unacked_serial_begin == 0U; + } + if (!Entry_Serial_Before(queue->overwritten_unacked_serial_begin, queue->next_entry_serial) || + !Entry_Serial_Before(queue->overwritten_unacked_serial_end, queue->next_entry_serial)) { + return 0; + } + overwritten_span = Entry_Serial_Distance(queue->overwritten_unacked_serial_begin, + queue->overwritten_unacked_serial_end); + /* 区间含两端且止于next之前;丢弃未提交NORMAL可留下间隔,数量不超过span+1。 */ + return overwritten_span < Entry_Serial_Distance(queue->overwritten_unacked_serial_begin, queue->next_entry_serial) && + queue->overwritten_unacked_count <= overwritten_span + 1U; +} + +/** + * 功能:读取条目并核对预期的队列旧状态,返回最多两段ASDU原文的借用指针。 + * 输入参数:queue/priority——已通过容器和优先级检查的队列与桶; + * index——候选条目头下标;expected_queue_state——预期的QUEUE_ENTRY_*记账状态。 + * 输出参数:info——非NULL,仅成功时写入身份、长度及两段只读借用指针。 + * 返回值:条目头和占用范围通过检查返回1,否则0,失败不改info;不校验ASDU业务内容。 + * 说明:先检查整个条目头属于占用区,再用Ring_Read处理回绕和未对齐数值。 + * index由有效游标或条目边界推导;不扫描前序条目,也不移动原文。 + * 该参数只校验读取时的状态,不设置新状态,也不检查SendBuffer或发送结果。 + * NORMAL头编码映射为记账状态后比较;HIGH环内条目恒为PENDING且没有条目编号。 + * NORMAL头内的1/2对应输出is_submitted的0/1,不能直接拿头内编码与预期记账状态比较。 + * NORMAL编号须位于next之前的半环窗口内,并晚于已覆盖待ACK前缀。 + * 输出指针指向队列存储,不复制ASDU;调用方只能在条目借用仍有效期间读取原文。 + */ +static int Read_Entry_With_Expected_State(const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, + uint32_t index, uint8_t expected_queue_state, + IEC104_AsduHeadInfo_t *info) +{ + const IEC104_AsduPriorityQueue_t *bucket = &queue->priority_queues[priority]; + IEC104_AsduHeadInfo_t prepared; + IEC104_AsduNormalEntryHeader_t header; + uint32_t header_length = Entry_Header_Length(priority); + uint32_t distance, asdu_index, first_length; + + if (index >= bucket->byte_capacity) { + return 0; + } + distance = index >= bucket->head_index ? index - bucket->head_index : + bucket->byte_capacity - bucket->head_index + index; + if (distance >= bucket->used_bytes || bucket->used_bytes - distance < header_length) { + return 0; + } + if (priority == IEC104_ASDU_PRIORITY_HIGH) { + Ring_Read(bucket, index, &prepared.asdu_length, sizeof(prepared.asdu_length)); + prepared.entry_id.entry_serial = 0U; + prepared.is_submitted = QUEUE_ENTRY_PENDING; + } else { + Ring_Read(bucket, index, &header, sizeof(header)); + if ((header.entry_state != NORMAL_HEADER_PENDING && header.entry_state != NORMAL_HEADER_RETAINED_UNACKED) || + !Entry_Serial_Before(header.entry_serial, queue->next_entry_serial) || + (queue->overwritten_unacked_count != 0U && + !Entry_Serial_Before(queue->overwritten_unacked_serial_end, header.entry_serial))) { + return 0; + } + prepared.asdu_length = (uint16_t)header.asdu_length; + prepared.entry_id.entry_serial = header.entry_serial; + prepared.is_submitted = header.entry_state == NORMAL_HEADER_RETAINED_UNACKED; + } + if (prepared.asdu_length < IEC104_ASDU_HEADER_LEN || prepared.asdu_length > IEC104_MAX_ASDU_LEN || + (uint32_t)prepared.asdu_length + header_length > bucket->used_bytes - distance || + prepared.is_submitted != expected_queue_state) { + return 0; + } + asdu_index = Ring_Advance(bucket, index, header_length); + first_length = bucket->byte_capacity - asdu_index; + if (first_length > prepared.asdu_length) { + first_length = prepared.asdu_length; + } + prepared.entry_id.connection_generation = queue->connection_generation; + prepared.priority = priority; + prepared.asdu_first = bucket->bytes + asdu_index; + prepared.asdu_first_length = (uint16_t)first_length; + prepared.asdu_second = first_length < prepared.asdu_length ? bucket->bytes : NULL; + *info = prepared; + return 1; +} + +/** + * 功能:在入队写字节前,检查分配本次编号后能否继续维持半环窗口。 + * 输入参数:queue——非NULL、控制字段已检查的副本,已完成本次NORMAL覆盖预处理。 + * 输出参数:无,不修改副本或字节数组。 + * 返回值:OK允许推进;FULL表示旧编号占住窗口;ERROR表示物理队首条目非法。 + * 说明:next是NORMAL本次入队后的下一编号;NORMAL物理队首及覆盖区间下界均须早于它。 + * NORMAL保持FIFO编号顺序,只需检查队首,开销与条目总数无关;HIGH不使用本检查。 + * 已丢弃的未提交条目不再占窗口;已覆盖但待ACK的条目仍占窗口。 + * FULL时须先处理旧条目或ACK再重试,不能越过窗口继续分配编号。 + * 本函数只预检,不推进next_entry_serial;因此即使字节空间足够,也可能因编号窗口返回FULL。 + */ +static IEC104_AsduQueueResult_t Check_Entry_Serial_Advance(const IEC104_AsduQueue_t *queue) +{ + IEC104_AsduHeadInfo_t oldest; + const IEC104_AsduPriorityQueue_t *bucket = &queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL]; + uint64_t next = Entry_Serial_Next(queue->next_entry_serial); + + if (queue->overwritten_unacked_count != 0U && + !Entry_Serial_Before(queue->overwritten_unacked_serial_begin, next)) { + return IEC104_ASDU_QUEUE_FULL; + } + if (bucket->entry_count != 0U) { + if (!Read_Entry_With_Expected_State(queue, IEC104_ASDU_PRIORITY_NORMAL, bucket->head_index, + bucket->entry_count > bucket->unsubmitted_count ? + QUEUE_ENTRY_RETAINED_UNACKED : QUEUE_ENTRY_PENDING, &oldest)) { + return IEC104_ASDU_QUEUE_ERROR; + } + if (!Entry_Serial_Before(oldest.entry_id.entry_serial, next)) { + return IEC104_ASDU_QUEUE_FULL; + } + } + return IEC104_ASDU_QUEUE_OK; +} + +/** + * 功能:计算当前条目之后的位置,完整条目按实际字节数跨尾部回绕。 + * 输入参数:bucket——有效字节环;priority——已验证的优先级,用于确定条目头长; + * index/asdu_length——已验证条目头的位置和ASDU长度,头长加原文长度不超过环容量。 + * 输出参数:无。 + * 返回值:下一逻辑位置,可能是下一条目或队尾。 + * 说明:条目之间没有填充;有后继条目时,由调用方另行验证其头和占用范围。 + */ +static uint32_t Next_Entry_Index(const IEC104_AsduPriorityQueue_t *bucket, IEC104_AsduPriority_t priority, + uint32_t index, uint16_t asdu_length) +{ + return Ring_Advance(bucket, index, Entry_Header_Length(priority) + (uint32_t)asdu_length); +} + +/** + * 功能:将NORMAL条目头从待发状态改为保留原文等待ACK。 + * 输入参数:bucket/index——有效NORMAL字节环和已验证的待发条目头位置。 + * 输出参数:bucket->bytes中的entry_state改为NORMAL_HEADER_RETAINED_UNACKED,编号、长度及原文不变。 + * 返回值:无;本函数不再检查身份和旧状态,调用方须先完成相关预检及发送交接。 + * 说明:写入的是头内编码2;对应公开视图is_submitted为1,两者数值含义不同。 + * 只更新条目头,待发计数和游标由Mark_Submitted随后更新;memcpy处理未对齐及回绕。 + */ +static void Mark_Normal_Entry_Retained_Unacked(const IEC104_AsduPriorityQueue_t *bucket, uint32_t index) +{ + IEC104_AsduNormalEntryHeader_t header; + + Ring_Read(bucket, index, &header, sizeof(header)); + header.entry_state = NORMAL_HEADER_RETAINED_UNACKED; + Ring_Write(bucket, index, &header, sizeof(header)); +} + +/** + * 功能:绑定调用方提供的两块字节数组,建立空的两级业务队列。 + * 输入参数:high_bytes/normal_bytes——持续有效且互不重叠的数组; + * high_capacity/normal_capacity——字节容量,分别至少对应优先级的MIN_CAPACITY; + * connection_generation——0建立服务离线队列;非零值表示绑定到不复用的连接代次。 + * 输出参数:queue——非NULL,仅成功时整体写入初始化结果。 + * 返回值:OK表示成功,ERROR表示参数非法;失败不修改queue或字节数组。 + * 说明:不分配内存,不清零原文字节;重新Init会丢弃旧条目引用,恢复业务应使用Rebind。 + * 最小容量只保证最短ASDU;支持249字节ASDU时,至少配置本级HEADER_LEN+249字节。 + * 数组在队列使用期间须持续有效;本函数只检查两起始地址不同,完整地址区间不重叠由调用方保证。 + * 成功后NORMAL的next_entry_serial从1开始;所有条目计数、占用字节数和游标从0开始。 + */ +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) +{ + IEC104_AsduQueue_t prepared; + + if (queue == NULL || high_bytes == NULL || normal_bytes == NULL || high_bytes == normal_bytes || + high_capacity < IEC104_ASDU_HIGH_QUEUE_MIN_CAPACITY || + normal_capacity < IEC104_ASDU_NORMAL_QUEUE_MIN_CAPACITY) { + return IEC104_ASDU_QUEUE_ERROR; + } + memset(&prepared, 0, sizeof(prepared)); + prepared.priority_queues[IEC104_ASDU_PRIORITY_HIGH].bytes = high_bytes; + prepared.priority_queues[IEC104_ASDU_PRIORITY_HIGH].byte_capacity = high_capacity; + prepared.priority_queues[IEC104_ASDU_PRIORITY_NORMAL].bytes = normal_bytes; + prepared.priority_queues[IEC104_ASDU_PRIORITY_NORMAL].byte_capacity = normal_capacity; + prepared.connection_generation = connection_generation; + prepared.next_entry_serial = 1U; + prepared.is_initialized = 1U; + *queue = prepared; + return IEC104_ASDU_QUEUE_OK; +} + +/** + * 功能:读取连接代次及ASDU条目数,区分原文仍保留、原文仍保留且已提交待确认、原文已覆盖且待确认。 + * 输入参数:queue——已初始化的业务队列。 + * 输出参数:status——非NULL,仅OK时写入数值副本。 + * 返回值:OK或ERROR;失败不修改输出。 + * 说明:HIGH提交时出队,其retained_unacked_count恒为0,未确认HIGH数量由Manager发送记录维护。 + * NORMAL的retained_unacked_count加overwritten_unacked_count得到NORMAL已提交待确认条目数。 + * is_storage_empty只表示两个ASDU原文环为空,HIGH或NORMAL仍可能有对应I帧未ACK;查询不预留容量。 + * 各count以ASDU条目为单位,不是字节数;只读取控制字段,不遍历或复制ASDU原文。 + */ +IEC104_AsduQueueResult_t IEC104_AsduQueue_Get_Status( + const IEC104_AsduQueue_t *queue, IEC104_AsduQueueStatus_t *status) +{ + IEC104_AsduQueueStatus_t prepared; + unsigned i; + + if (!Queue_State_Valid(queue) || status == NULL) { + return IEC104_ASDU_QUEUE_ERROR; + } + prepared.connection_generation = queue->connection_generation; + prepared.overwritten_unacked_count = queue->overwritten_unacked_count; + prepared.is_storage_empty = 1U; + for (i = 0U; i < IEC104_ASDU_PRIORITY_COUNT; ++i) { + const IEC104_AsduPriorityQueue_t *bucket = &queue->priority_queues[i]; + prepared.entry_count[i] = bucket->entry_count; + prepared.retained_unacked_count[i] = bucket->entry_count - bucket->unsubmitted_count; + if (bucket->entry_count != 0U) { + prepared.is_storage_empty = 0U; + } + } + *status = prepared; + return IEC104_ASDU_QUEUE_OK; +} + +/** + * 功能:核对身份及预期的队列旧状态后,释放指定优先级的物理队首。 + * 输入参数:queue/priority/entry_id——目标队首;expected_queue_state——删除前预期的QUEUE_ENTRY_*状态。 + * 输出参数:queue——成功时更新字节占用、条目数和相关游标,不清零原文字节。 + * 返回值:OK或ERROR;先在副本中检查新状态,失败不修改原队列。 + * 说明:expected_queue_state只是删除前的校验条件,不是出队原因,也不是要写入的新状态。 + * 出队原因由调用方决定:HIGH发送交接完成、NORMAL有效ACK、主动取消或容量覆盖。 + * 本函数不访问SendBuffer或验证ACK;调用方须先满足对应前提。只移除队首,不修改覆盖待确认元数据。 + * HIGH只核对代次及0编号,调用方不得跨队首变化复用旧Peek结果。 + * 多个调用路径共用此入口,因此自行复核队首;即使上层刚Peek过,也保留这里的身份和状态检查。 + */ +static IEC104_AsduQueueResult_t Remove_Head_With_Expected_State( + IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, + IEC104_AsduEntryId_t entry_id, uint8_t expected_queue_state) +{ + IEC104_AsduHeadInfo_t info, next_info; + IEC104_AsduPriorityQueue_t prepared; + uint32_t next, size; + + if (IEC104_AsduQueue_Peek_Head(queue, priority, &info) != IEC104_ASDU_QUEUE_OK || + info.entry_id.connection_generation != entry_id.connection_generation || + info.entry_id.entry_serial != entry_id.entry_serial || info.is_submitted != expected_queue_state) { + return IEC104_ASDU_QUEUE_ERROR; + } + prepared = queue->priority_queues[priority]; + size = Entry_Header_Length(priority) + (uint32_t)info.asdu_length; + next = Next_Entry_Index(&prepared, priority, prepared.head_index, info.asdu_length); + if (prepared.entry_count > 1U && !Read_Entry_With_Expected_State(queue, priority, next, + prepared.entry_count - prepared.unsubmitted_count > 1U ? + QUEUE_ENTRY_RETAINED_UNACKED : QUEUE_ENTRY_PENDING, &next_info)) { + return IEC104_ASDU_QUEUE_ERROR; + } + prepared.used_bytes -= size; + prepared.head_index = next; + prepared.entry_count--; + /* 删除待发队首时同步推进待发区;删除待ACK前缀时,待发区的位置和数量保持不变。 */ + if (expected_queue_state == QUEUE_ENTRY_PENDING) { + prepared.unsubmitted_count--; + prepared.unsubmitted_index = next; + } + /* 最后一条移除后满足空队列的归零约束,下次从下标0追加;不清零原文字节。 */ + if (prepared.entry_count == 0U) { + prepared.head_index = 0U; + prepared.tail_index = 0U; + prepared.unsubmitted_index = 0U; + prepared.used_bytes = 0U; + } + if (!Bucket_Valid(&prepared, priority)) { + return IEC104_ASDU_QUEUE_ERROR; + } + queue->priority_queues[priority] = prepared; + return IEC104_ASDU_QUEUE_OK; +} + +/** + * 功能:复制一条ASDU到指定优先级的字节环队尾,建立尚未登记发送交接的待发条目。 + * 输入参数:queue/connection_generation——有效队列及匹配代次;priority——HIGH或NORMAL; + * asdu/asdu_length——完整ASDU及6..249字节长度,输入区不得与队列及其存储重叠。 + * 输出参数:queue及对应字节数组——仅OK时追加条目,NORMAL另分配条目编号;不存业务标识。 + * 返回值:OK成功,NORMAL可能已覆盖旧条目且不另行通知;FULL表示HIGH空间不足或NORMAL编号窗口受阻; + * ERROR表示条目超过目标桶总容量,或参数、状态、身份/覆盖计数非法。 + * 说明:FULL/ERROR不修改字节、控制字段或编号;生产者仅在OK后推进业务游标。 + * FULL须先释放字节空间或处理占住编号窗口的旧条目/ACK;条目超过总容量不能等待ACK解决。 + * NORMAL按最旧顺序覆盖,含已提交原文;对应I帧及确认记录不删除,仍须完成和等待ACK。 + * 先在控制副本上验证所有必要删除,成功后才写新字节并发布状态,避免失败时丢弃旧业务。 + * 本接口不分配N(S),不解释业务对象;未提交条目被覆盖时不会产生逐帧回执。 + * OK后队列已有独立的ASDU副本,调用方可复用输入缓冲;条目占用为本级头长加实际ASDU长度。 + */ +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) +{ + IEC104_AsduQueue_t prepared; + IEC104_AsduQueueResult_t result; + IEC104_AsduHeadInfo_t oldest; + IEC104_AsduNormalEntryHeader_t header; + IEC104_AsduPriorityQueue_t *bucket; + uint32_t size, index, available; + + if (!Queue_State_Valid(queue) || connection_generation != queue->connection_generation || + !Priority_Valid(priority) || (priority == IEC104_ASDU_PRIORITY_HIGH && connection_generation == 0U) || + asdu == NULL || asdu_length < IEC104_ASDU_HEADER_LEN || + asdu_length > IEC104_MAX_ASDU_LEN) { + return IEC104_ASDU_QUEUE_ERROR; + } + bucket = &queue->priority_queues[priority]; + size = Entry_Header_Length(priority) + (uint32_t)asdu_length; + /* 空队列也装不下属于配置与条目不匹配,不能报告为可等待空间恢复的FULL。 */ + if (size > bucket->byte_capacity) { + return IEC104_ASDU_QUEUE_ERROR; + } + available = bucket->byte_capacity - bucket->used_bytes; + if (size > available && priority == IEC104_ASDU_PRIORITY_HIGH) { + return IEC104_ASDU_QUEUE_FULL; + } + prepared = *queue; + bucket = &prepared.priority_queues[priority]; + /* prepared只复制控制结构,bytes仍指向原数组;此处只模拟删除,不写共享字节。 + * 任一旧头或后继非法时直接返回,原字节、游标及编号都保持原样。 */ + while (size > bucket->byte_capacity - bucket->used_bytes) { + if (IEC104_AsduQueue_Peek_Head(&prepared, priority, &oldest) != IEC104_ASDU_QUEUE_OK || + (oldest.is_submitted != 0U && prepared.overwritten_unacked_count == UINT32_MAX) || + Remove_Head_With_Expected_State(&prepared, priority, oldest.entry_id, + oldest.is_submitted) != IEC104_ASDU_QUEUE_OK) { + return IEC104_ASDU_QUEUE_ERROR; + } + if (oldest.is_submitted != 0U) { + if (prepared.overwritten_unacked_count == 0U) { + /* 首次覆盖已提交原文时建立区间;已有待ACK前缀时保留原下界。 */ + prepared.overwritten_unacked_serial_begin = oldest.entry_id.entry_serial; + } + prepared.overwritten_unacked_count++; + prepared.overwritten_unacked_serial_end = oldest.entry_id.entry_serial; + } + } + /* 副本中的覆盖尚未发布;窗口检查失败时,原队列及原文字节仍保持调用前状态。 */ + if (priority == IEC104_ASDU_PRIORITY_NORMAL) { + result = Check_Entry_Serial_Advance(&prepared); + if (result != IEC104_ASDU_QUEUE_OK) { + return result; + } + } + /* 总空闲已足够,直接分段写入;即使条目头本身跨尾部,也无需额外保留空间。 */ + index = bucket->tail_index; + if (priority == IEC104_ASDU_PRIORITY_HIGH) { + Ring_Write(bucket, index, &asdu_length, sizeof(asdu_length)); + } else { + memset(&header, 0, sizeof(header)); + header.entry_serial = prepared.next_entry_serial; + header.entry_state = NORMAL_HEADER_PENDING; + /* 入口已检查长度为6..249,显式收窄为8位后写入条目头。 */ + header.asdu_length = (uint8_t)asdu_length; + Ring_Write(bucket, index, &header, sizeof(header)); + } + Ring_Write(bucket, Ring_Advance(bucket, index, Entry_Header_Length(priority)), asdu, asdu_length); + if (bucket->unsubmitted_count == 0U) { + bucket->unsubmitted_index = index; + } + bucket->tail_index = Ring_Advance(bucket, index, size); + bucket->used_bytes += size; + bucket->entry_count++; + bucket->unsubmitted_count++; + /* 只有NORMAL消耗编号;HIGH不被旧NORMAL条目占住的编号窗口阻塞。 */ + if (priority == IEC104_ASDU_PRIORITY_NORMAL) { + prepared.next_entry_serial = Entry_Serial_Next(prepared.next_entry_serial); + } + *queue = prepared; + return IEC104_ASDU_QUEUE_OK; +} + +/** + * 功能:查看指定优先级最旧的保留条目;NORMAL队首可能已登记交接并正在等待ACK。 + * 输入参数:queue——有效队列;priority——HIGH或NORMAL。 + * 输出参数:head_info——非NULL,仅OK时提供身份、原文借用及是否已提交。 + * 返回值:OK有条目,EMPTY为空,ERROR表示参数、状态或条目非法。 + * 说明:不移动任何游标;选择第一条未提交ASDU应调用Peek_Unsubmitted。 + * HIGH队首也是待发区首条;NORMAL存在待ACK前缀时,两者可能不是同一条。 + * 返回的是原文借用,不是副本;条目被覆盖或释放后不能继续使用返回的指针。 + */ +IEC104_AsduQueueResult_t IEC104_AsduQueue_Peek_Head( + const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduHeadInfo_t *head_info) +{ + const IEC104_AsduPriorityQueue_t *bucket; + + if (!Queue_State_Valid(queue) || !Priority_Valid(priority) || head_info == NULL) { + return IEC104_ASDU_QUEUE_ERROR; + } + bucket = &queue->priority_queues[priority]; + if (bucket->entry_count == 0U) { + return IEC104_ASDU_QUEUE_EMPTY; + } + return Read_Entry_With_Expected_State(queue, priority, bucket->head_index, + bucket->entry_count > bucket->unsubmitted_count ? + QUEUE_ENTRY_RETAINED_UNACKED : QUEUE_ENTRY_PENDING, head_info) ? + IEC104_ASDU_QUEUE_OK : IEC104_ASDU_QUEUE_ERROR; +} + +/** + * 功能:只读查看指定优先级中尚未完成队列交接记账的第一条ASDU。 + * 输入参数:queue——有效队列;priority——HIGH或NORMAL。 + * 输出参数:head_info——调用方提供的非NULL指针;仅返回OK时写入,is_submitted为0。 + * 返回值:OK表示找到当前允许交接的未提交条目;EMPTY表示没有可交接条目,不代表整个队列为空; + * ERROR表示参数、状态或条目非法。 + * 说明:通过unsubmitted_index常数时间定位,不遍历NORMAL中已提交但未ACK的前缀; + * 不移除条目,不改变记账状态;Manager交接过程中的内部复核也使用本接口。 + * 本队列不查询SendBuffer,调用方不能把is_submitted为0当作发送模块尚未接手的证明。 + * NORMAL全部已提交或已因EOF停止选取时返回EMPTY,Peek_Head仍可查看实际保留原文。 + * 离线查询可返回代次0的NORMAL原文视图,真正提交发送须先Rebind到非零代次。 + */ +IEC104_AsduQueueResult_t IEC104_AsduQueue_Peek_Unsubmitted( + const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduHeadInfo_t *head_info) +{ + const IEC104_AsduPriorityQueue_t *bucket; + + if (!Queue_State_Valid(queue) || !Priority_Valid(priority) || head_info == NULL) { + return IEC104_ASDU_QUEUE_ERROR; + } + bucket = &queue->priority_queues[priority]; + if (bucket->unsubmitted_count == 0U || + (priority == IEC104_ASDU_PRIORITY_NORMAL && queue->normal_send_limited != 0U)) { + return IEC104_ASDU_QUEUE_EMPTY; + } + return Read_Entry_With_Expected_State(queue, priority, bucket->unsubmitted_index, + QUEUE_ENTRY_PENDING, head_info) ? + IEC104_ASDU_QUEUE_OK : IEC104_ASDU_QUEUE_ERROR; +} + +/** + * 功能:把Peek或Get_Entry返回的最多两段ASDU复制到调用方连续缓冲。 + * 输入参数:head_info——借用仍有效的条目描述;capacity——输出缓冲实际字节容量。 + * 输出参数:destination——非NULL、与两段原文及描述不重叠的可写数组,仅OK时写入。 + * 返回值:OK复制了asdu_length字节;ERROR表示描述或容量非法,此时不改输出。 + * 说明:只检查分段描述,不重新查询队列或核对代次;调用方保证条目未覆盖、释放、Detach、Rebind或Abort。 + * 连续ASDU也可使用本接口;正常发送仅在跨界时才需要额外拼接。 + * 复制结果仅含ASDU,不添加APCI;复制成功也不会移除条目或登记发送交接。 + */ +IEC104_AsduQueueResult_t IEC104_AsduQueue_Copy_Asdu( + const IEC104_AsduHeadInfo_t *head_info, uint8_t *destination, uint16_t capacity) +{ + if (head_info == NULL || destination == NULL || + head_info->asdu_length < IEC104_ASDU_HEADER_LEN || head_info->asdu_length > IEC104_MAX_ASDU_LEN || + head_info->asdu_first == NULL || head_info->asdu_first_length == 0U || + head_info->asdu_first_length > head_info->asdu_length || capacity < head_info->asdu_length || + (head_info->asdu_first_length == head_info->asdu_length ? + head_info->asdu_second != NULL : head_info->asdu_second == NULL)) { + return IEC104_ASDU_QUEUE_ERROR; + } + memcpy(destination, head_info->asdu_first, head_info->asdu_first_length); + if (head_info->asdu_second != NULL) { + memcpy(destination + head_info->asdu_first_length, head_info->asdu_second, + head_info->asdu_length - head_info->asdu_first_length); + } + return IEC104_ASDU_QUEUE_OK; +} + +/** + * 功能:在发送模块接手后登记队列交接;HIGH消费当前原文,NORMAL保留原文并推进待发游标。 + * 输入参数:queue/priority——目标队列与优先级;entry_id——NORMAL完整身份,HIGH为当前代次及0编号。 + * 输出参数:queue——HIGH释放队首;NORMAL更新头内状态、unsubmitted_count和unsubmitted_index。 + * 返回值:OK或ERROR;失败不修改队列。 + * 说明:调用方须先把完整I帧交给SendBuffer并建立Manager记录,再串行调用本函数。 + * 调用前队列仍记录PENDING;HIGH不在环内建立已交接状态,直接消费原文完成记账。 + * 本函数不验证发送缓冲是否接手,也不构帧或执行发送,不能提前调用来丢弃待发数据。 + * HIGH原文借用在成功出队后失效;SendBuffer报文及Manager记录继续有效,仍须完成发送和ACK。 + * HIGH仍要求entry_serial等于0,用于拒绝非法标识;它不能区分同一代次中先后入队的两条HIGH。 + */ +IEC104_AsduQueueResult_t IEC104_AsduQueue_Mark_Submitted( + IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduEntryId_t entry_id) +{ + IEC104_AsduHeadInfo_t info, next_info; + IEC104_AsduPriorityQueue_t *bucket; + uint32_t next; + + if (entry_id.connection_generation == 0U || + IEC104_AsduQueue_Peek_Unsubmitted(queue, priority, &info) != IEC104_ASDU_QUEUE_OK || + info.entry_id.connection_generation != entry_id.connection_generation || + info.entry_id.entry_serial != entry_id.entry_serial) { + return IEC104_ASDU_QUEUE_ERROR; + } + if (priority == IEC104_ASDU_PRIORITY_HIGH) { + /* 发送模块已接手;PENDING仅核对删除前的队列旧状态,随后消费原文完成交接记账。 */ + return Remove_Head_With_Expected_State(queue, priority, entry_id, QUEUE_ENTRY_PENDING); + } + bucket = &queue->priority_queues[priority]; + next = Next_Entry_Index(bucket, priority, bucket->unsubmitted_index, info.asdu_length); + if (bucket->unsubmitted_count > 1U && + !Read_Entry_With_Expected_State(queue, priority, next, QUEUE_ENTRY_PENDING, &next_info)) { + return IEC104_ASDU_QUEUE_ERROR; + } + /* 登记NORMAL交接:头编码改为保留待ACK,待发游标移到下一条;原文不出队。 */ + Mark_Normal_Entry_Retained_Unacked(bucket, bucket->unsubmitted_index); + bucket->unsubmitted_count--; + bucket->unsubmitted_index = bucket->unsubmitted_count == 0U ? bucket->tail_index : next; + return IEC104_ASDU_QUEUE_OK; +} + +/** + * 功能:完成NORMAL下一条有效ACK的存储清理,兼容原文已被覆盖;HIGH的ACK只更新Manager记录。 + * 输入参数:queue——有效队列;priority——只允许NORMAL;entry_id——有效ACK对应Manager记录的真实身份。 + * 输出参数:queue——仍有原文时释放匹配队首;已覆盖时更新待确认计数和区间边界,不移除当前队首。 + * 返回值:OK或ERROR;ERROR表示代次、编号区间、覆盖计数或已提交队首身份不符合约束。 + * 说明:仅供Manager在Link验证ACK后按发送顺序调用;本函数不验证协议N(R)。 + * 覆盖后不另存完整身份列表,只有数量和循环编号区间;准确身份及顺序由Manager独立记录保证。 + * 覆盖前缀清完才允许确认仍保留的队首,最后一个覆盖身份必须与边界相等。 + * 只清理ASDU存储及覆盖计数,不自行推进协议确认边界或释放Manager回执记录。 + */ +IEC104_AsduQueueResult_t IEC104_AsduQueue_Confirm_Next_Normal_Entry( + IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduEntryId_t entry_id) +{ + uint64_t confirmed_offset, overwritten_span; + + if (!Queue_State_Valid(queue) || priority != IEC104_ASDU_PRIORITY_NORMAL || + entry_id.connection_generation != queue->connection_generation || + !Entry_Serial_Before(entry_id.entry_serial, queue->next_entry_serial)) { + return IEC104_ASDU_QUEUE_ERROR; + } + if (queue->overwritten_unacked_count != 0U) { + /* 以区间下界为起点比较循环距离,使跨UINT64_MAX的覆盖区间仍能按同一规则检查。 */ + confirmed_offset = Entry_Serial_Distance(queue->overwritten_unacked_serial_begin, entry_id.entry_serial); + overwritten_span = Entry_Serial_Distance(queue->overwritten_unacked_serial_begin, + queue->overwritten_unacked_serial_end); + /* + * ACK须落在区间内,且只有最后一次ACK才可命中上界;后续编号位置须容得下剩余计数。 + * 此处只检查区间和数量,具体ACK身份是否属于发送记录仍由Manager保证。 + */ + if (confirmed_offset > overwritten_span || + ((queue->overwritten_unacked_count == 1U) != + (entry_id.entry_serial == queue->overwritten_unacked_serial_end)) || + queue->overwritten_unacked_count - 1U > overwritten_span - confirmed_offset) { + return IEC104_ASDU_QUEUE_ERROR; + } + /* 原文可能已被新条目占用,绝不能把当前物理队首当成旧帧释放。 */ + queue->overwritten_unacked_count--; + /* 排除已确认及更早的编号;下界可落在编号间隔中,不一定就是下一待ACK条目的编号。 */ + queue->overwritten_unacked_serial_begin = Entry_Serial_Next(entry_id.entry_serial); + if (queue->overwritten_unacked_count == 0U) { + /* 最后一个覆盖条目已确认,清除两端边界,释放这段编号窗口。 */ + queue->overwritten_unacked_serial_end = 0U; + queue->overwritten_unacked_serial_begin = 0U; + } + return IEC104_ASDU_QUEUE_OK; + } + return Remove_Head_With_Expected_State(queue, priority, entry_id, QUEUE_ENTRY_RETAINED_UNACKED); +} + +/** + * 功能:在发送交接开始前,主动丢弃仍处于队列待发状态的物理队首ASDU。 + * 输入参数:queue/priority——目标字节环;entry_id——NORMAL完整身份,HIGH为当前代次及0编号。 + * 输出参数:queue——成功时释放队首并推进待发游标;不清零原文字节,被丢弃条目的借用失效。 + * 返回值:OK或ERROR;队首为空、身份不符或已登记交接时ERROR且不修改队列。 + * 说明:只删除物理队首,不能越过NORMAL的待ACK前缀去删除后面的待发条目。 + * 本函数只检查队列记账状态,无法判断SendBuffer是否已经接手;调用方必须在发送交接前使用, + * 不得插入Manager的交接过程,也不能用它撤回已编号I帧。断线恢复使用Rebind。 + */ +IEC104_AsduQueueResult_t IEC104_AsduQueue_Discard_Unsubmitted_Head( + IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduEntryId_t entry_id) +{ + return Remove_Head_With_Expected_State(queue, priority, entry_id, QUEUE_ENTRY_PENDING); +} + +/** + * 功能:按保留顺序查看指定偏移的原始ASDU,供检查和恢复使用。 + * 输入参数:queue/priority——目标字节环;offset——从最旧保留条目起的条目序位,0为队首,单位不是字节。 + * 输出参数:head_info——非NULL,仅OK时写入对应条目及其已提交状态。 + * 返回值:OK找到条目,EMPTY表示偏移超出已有条目,ERROR表示参数或经过的条目非法。 + * 说明:从队首读取offset+1条记录,逐条计算边界;正常发送选取使用Peek_Unsubmitted直接定位。 + * 同时包含NORMAL的待ACK前缀与待发后缀;输出原文的借用有效期与Peek一致,不生成副本。 + */ +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) +{ + const IEC104_AsduPriorityQueue_t *bucket; + IEC104_AsduHeadInfo_t info; + uint32_t i, index; + + if (!Queue_State_Valid(queue) || !Priority_Valid(priority) || head_info == NULL) { + return IEC104_ASDU_QUEUE_ERROR; + } + bucket = &queue->priority_queues[priority]; + if (offset >= bucket->entry_count) { + return IEC104_ASDU_QUEUE_EMPTY; + } + index = bucket->head_index; + for (i = 0U; i <= offset; ++i) { + if (!Read_Entry_With_Expected_State(queue, priority, index, + i < bucket->entry_count - bucket->unsubmitted_count ? + QUEUE_ENTRY_RETAINED_UNACKED : QUEUE_ENTRY_PENDING, &info)) { + return IEC104_ASDU_QUEUE_ERROR; + } + index = Next_Entry_Index(bucket, priority, index, info.asdu_length); + } + *head_info = info; + return IEC104_ASDU_QUEUE_OK; +} + +/** + * 功能:查询指定优先级当前未占用的字节数。 + * 输入参数:queue/priority——目标字节环。 + * 输出参数:free_bytes——非NULL,仅OK时写入容量减used_bytes。 + * 返回值:OK或ERROR,失败不改输出。 + * 说明:空闲不少于本级HEADER_LEN+ASDU长度时有足够容量;NORMAL另检查编号窗口。 + * 空闲不足时NORMAL仍可能通过覆盖旧原文入队;本查询不预留空间,也不能代替Enqueue的结果检查。 + */ +IEC104_AsduQueueResult_t IEC104_AsduQueue_Get_Free_Bytes( + const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, uint32_t *free_bytes) +{ + if (!Queue_State_Valid(queue) || !Priority_Valid(priority) || free_bytes == NULL) { + return IEC104_ASDU_QUEUE_ERROR; + } + *free_bytes = queue->priority_queues[priority].byte_capacity - queue->priority_queues[priority].used_bytes; + return IEC104_ASDU_QUEUE_OK; +} + +/** + * 功能:逐条验证完整占用区及提交分界;NORMAL另验证条目身份顺序和头内状态。 + * 输入参数:queue/priority——控制字段与优先级均已验证的队列和目标桶。 + * 输出参数:无,不修改控制字段或原文字节。 + * 返回值:全部条目恰好组成有效FIFO返回1,否则0。 + * 说明:Rebind在更新控制字段前验证两个桶;remaining防止错误长度绕回已读区。 + * Read_Entry_With_Expected_State先限制各编号到next的距离,再按循环先后验证相邻条目,允许UINT64_MAX接1。 + * 耗时随条目数增长;只检查存储布局、身份及记账状态,不解析ASDU业务对象。 + */ +static int Bucket_Entries_Valid(const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority) +{ + const IEC104_AsduPriorityQueue_t *bucket = &queue->priority_queues[priority]; + IEC104_AsduHeadInfo_t info; + uint32_t index = bucket->head_index; + uint32_t remaining = bucket->used_bytes; + uint32_t retained_unacked_count = bucket->entry_count - bucket->unsubmitted_count; + uint64_t previous_serial = 0U; + uint32_t i, size; + + for (i = 0U; i < bucket->entry_count; ++i) { + if (remaining < Entry_Header_Length(priority) + IEC104_ASDU_HEADER_LEN || + (i == retained_unacked_count && index != bucket->unsubmitted_index) || + !Read_Entry_With_Expected_State(queue, priority, index, + i < retained_unacked_count ? QUEUE_ENTRY_RETAINED_UNACKED : QUEUE_ENTRY_PENDING, &info)) { + return 0; + } + size = Entry_Header_Length(priority) + (uint32_t)info.asdu_length; + if (size > remaining || (priority == IEC104_ASDU_PRIORITY_NORMAL && i != 0U && + !Entry_Serial_Before(previous_serial, info.entry_id.entry_serial))) { + return 0; + } + remaining -= size; + previous_serial = info.entry_id.entry_serial; + index = Ring_Advance(bucket, index, size); + } + return remaining == 0U && index == bucket->tail_index; +} + +/** + * 功能:将HIGH桶恢复为空环,供Reset_High和Rebind在预检成功后调用。 + * 输入参数:queue——已验证的队列,调用方已满足清空当前HIGH业务的前提。 + * 输出参数:HIGH的头尾下标、字节占用、条目数及待发游标归零,所有HIGH原文借用失效。 + * 返回值:无;此内部函数不重复检查参数或连接代次。 + * 说明:保留借用数组及容量,不清零原文字节;NORMAL、队列代次和编号进度均不在此处修改。 + */ +static void Reset_High_Bucket(IEC104_AsduQueue_t *queue) +{ + IEC104_AsduPriorityQueue_t *bucket = &queue->priority_queues[IEC104_ASDU_PRIORITY_HIGH]; + + bucket->head_index = 0U; + bucket->tail_index = 0U; + bucket->used_bytes = 0U; + bucket->entry_count = 0U; + bucket->unsubmitted_index = 0U; + bucket->unsubmitted_count = 0U; +} + +/** + * 功能:清空当前连接的HIGH待发条目,供Manager初始化、受理新STOP及从STOPPED受理START时调用。 + * 输入参数:queue——有效队列;connection_generation——匹配当前绑定的连接代次。 + * 输出参数:仅HIGH桶的条目计数、字节占用及游标清零;所有HIGH原文借用失效。 + * 返回值:OK或ERROR;非法状态或旧代次不修改队列。 + * 说明:HIGH已提交原文已经出队,因此不影响当前I帧、发送记录、ACK义务和NORMAL数据。 + * 不影响NORMAL的next_entry_serial;HIGH只有代次,没有可跨队首变化保存的条目身份。 + * 这是同一代次内的清理,不执行重连,也不会把NORMAL待ACK条目重新变成待发。 + */ +IEC104_AsduQueueResult_t IEC104_AsduQueue_Reset_High( + IEC104_AsduQueue_t *queue, uint64_t connection_generation) +{ + if (!Queue_State_Valid(queue) || connection_generation != queue->connection_generation) { + return IEC104_ASDU_QUEUE_ERROR; + } + Reset_High_Bucket(queue); + return IEC104_ASDU_QUEUE_OK; +} + +/* EOF后不再可能收到新I帧的ACK,停止选取全部尚未交接的NORMAL,原文留给下一连接。 + * 不改已提交条目、SendBuffer或HIGH;当前已编号帧仍由发送模块完成。 + * 入队、覆盖、取消条目及重复调用都不解除限制,只有Detach/Rebind恢复选取。 */ +IEC104_AsduQueueResult_t IEC104_AsduQueue_Limit_Normal_Send( + IEC104_AsduQueue_t *queue, uint64_t connection_generation) +{ + if (!Queue_State_Valid(queue) || connection_generation == 0U || + connection_generation != queue->connection_generation) { + return IEC104_ASDU_QUEUE_ERROR; + } + queue->normal_send_limited = 1U; + return IEC104_ASDU_QUEUE_OK; +} + +/* Detach/Rebind共用的绑定转换:入口已核对旧状态及目标代次,先验证全部条目再统一修改。 + * 0表示解除绑定,非零表示新会话;均恢复现存NORMAL,清空HIGH和旧连接独有的记账。 */ +static IEC104_AsduQueueResult_t Reset_Connection_Binding( + IEC104_AsduQueue_t *queue, uint64_t connection_generation) +{ + unsigned i; + IEC104_AsduPriorityQueue_t *normal; + IEC104_AsduNormalEntryHeader_t header; + uint32_t index, submitted_count, entry; + + for (i = 0U; i < IEC104_ASDU_PRIORITY_COUNT; ++i) { + if (!Bucket_Entries_Valid(queue, (IEC104_AsduPriority_t)i)) { + return IEC104_ASDU_QUEUE_ERROR; + } + } + /* 全部预检通过后才写头。后续没有可失败分支,保持ERROR不改字节的契约。 */ + normal = &queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL]; + index = normal->head_index; + submitted_count = normal->entry_count - normal->unsubmitted_count; + for (entry = 0U; entry < submitted_count; ++entry) { + Ring_Read(normal, index, &header, sizeof(header)); + header.entry_state = NORMAL_HEADER_PENDING; + Ring_Write(normal, index, &header, sizeof(header)); + index = Next_Entry_Index(normal, IEC104_ASDU_PRIORITY_NORMAL, index, (uint16_t)header.asdu_length); + } + /* NORMAL全部保留条目恢复为待发;HIGH直接清空。 */ + Reset_High_Bucket(queue); + queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL].unsubmitted_index = + queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL].head_index; + queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL].unsubmitted_count = + queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL].entry_count; + queue->connection_generation = connection_generation; + queue->overwritten_unacked_count = 0U; + queue->overwritten_unacked_serial_end = 0U; + queue->overwritten_unacked_serial_begin = 0U; + queue->normal_send_limited = 0U; + return IEC104_ASDU_QUEUE_OK; +} + +/** + * 功能:结束旧会话绑定,立即恢复服务级离线缓存状态。 + * 输入参数:queue——有效队列;connection_generation——要解除的当前非零代次。 + * 返回值:OK保留现存NORMAL并切换到代次0;ERROR不修改队列。 + * 说明:先结束旧I/O及Manager,处理已知ACK事实;随后允许新NORMAL以代次0入队。 + * 清空HIGH、覆盖待ACK记账及EOF限制,不恢复已被容量覆盖的原文,也不复制或移动ASDU。 + */ +IEC104_AsduQueueResult_t IEC104_AsduQueue_Detach( + IEC104_AsduQueue_t *queue, uint64_t connection_generation) +{ + if (!Queue_State_Valid(queue) || connection_generation == 0U || + connection_generation != queue->connection_generation) { + return IEC104_ASDU_QUEUE_ERROR; + } + return Reset_Connection_Binding(queue, 0U); +} + +/** + * 功能:把现存NORMAL绑定到新会话,全部恢复为待发,供新STARTDT完成后重新组帧。 + * 输入参数:queue——有效队列,可以尚未绑定;connection_generation——未使用过的非零新代次。 + * 返回值:OK或ERROR;先完整验证两个环,ERROR不修改控制字段或字节。 + * 说明:跨连接不复用代次由调用方保证;旧I/O及Manager必须先结束,已知ACK事实先处理。 + * 保留原文顺序、serial及next_entry_serial,旧身份和借用失效;清空HIGH、覆盖记账和EOF限制。 + * 未ACK不等于对端未收到,原文重发可能重复;容量已覆盖的原文不能恢复。 + */ +IEC104_AsduQueueResult_t IEC104_AsduQueue_Rebind( + IEC104_AsduQueue_t *queue, uint64_t connection_generation) +{ + if (!Queue_State_Valid(queue) || connection_generation == 0U || + connection_generation == queue->connection_generation) { + return IEC104_ASDU_QUEUE_ERROR; + } + return Reset_Connection_Binding(queue, connection_generation); +} + +/** + * 功能:清除业务队列控制结构,明确丢弃所有保留条目的引用。 + * 输入参数:queue——待终止对象,允许NULL。 + * 输出参数:queue——非NULL时清零;调用方提供的字节数组不清零、不释放。 + * 返回值:无。 + * 说明:先完成业务回收并结束所有借用;Manager.Abort不会隐式调用此接口。 + * 清零后队列不再处于已初始化状态,重新使用须Init;如需保留NORMAL原文恢复连接, + * 应结束旧Manager后使用Rebind,不应先调用本接口丢弃队列引用。 + */ +void IEC104_AsduQueue_Abort(IEC104_AsduQueue_t *queue) +{ + if (queue != NULL) { + memset(queue, 0, sizeof(*queue)); + } +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_AsduQueue.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_AsduQueue.h new file mode 100644 index 00000000..b721ff05 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_AsduQueue.h @@ -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 diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Codec.c b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Codec.c new file mode 100644 index 00000000..c1bc881d --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Codec.c @@ -0,0 +1,300 @@ +/** + * IEC 60870-5-104无状态编解码:完整APDU的APCI检查及I/S/U报文编码。 + * ASDU公共头解析由IEC60870_Codec提供,本模块只处理104传输帧。 + * + * 仅使用调用方提供的报文、序号和输出缓冲,不保存连接状态, + * 不依赖接收状态机、链路状态机、发送队列或日志模块。 + */ +#include "IEC104_Codec.h" + +#include +#include + +/** + * 功能:根据 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; +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Codec.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Codec.h new file mode 100644 index 00000000..b1e7281f --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Codec.h @@ -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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Connection.c b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Connection.c new file mode 100644 index 00000000..bf5b175b --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Connection.c @@ -0,0 +1,1182 @@ +/* + * TCP内部的子站单连接协调:由TcpServer调用Feed、Prepare和Progress,分别处理输入、待发字节及写入结果。 + * Drive统一推进保留接收帧、超时和控制应答;发送阶段不报告对端确认超时,交回接收阶段处理。 + * 旧确认t1到期时暂停新I帧编号,已准备帧仍可发送以解除接收阻塞。 + * 它不会执行TCP读写,也不会创建新业务I帧。 + * 接收I帧只提交一次链路事件,TcpServer复制为事件后Release;发送记录转为事件后Remove,收尾时由Close清理。 + * 业务只使用TcpServer事件副本。内部接口串行调用,connection_generation/frame_id先匹配当前连接, + * 含now_ms的运行接口再检查单调时间;故障后保留记录供回收,结束旧I/O后才可Close。 + */ +#include "IEC104_Connection.h" +#include "IEC104_Link_Slave_StartStop.h" + +#include + +/* 发送取到的新时刻可能跨过t1;确认超时故障须交给能够先处理输入的驱动阶段。 */ +typedef enum { + CHECK_ALL_TIMEOUTS, + CHECK_LOCAL_TIMEOUTS +} ConnectionTimeoutScope_t; + +/* 每次驱动按当前事实重建,连接对象不缓存第二份期限或起点。 */ +typedef enum { + DEADLINE_LOCAL_SEND, + DEADLINE_I_ACK, + DEADLINE_TESTFR, + DEADLINE_STOP, + DEADLINE_T2, + DEADLINE_T3, + DEADLINE_COUNT +} ConnectionDeadlineKind_t; + +typedef struct { + uint32_t remaining_ms; + uint8_t active; +} ConnectionDeadline_t; + +typedef struct { + ConnectionDeadline_t items[DEADLINE_COUNT]; + IEC104_SendFrameId_t head_frame_id; +} ConnectionDeadlines_t; + +/** + * 功能:只读核对连接代次及已初始化的生命周期范围,包括CLOSED。 + * 输入参数:connection——已初始化或全零对象,允许NULL;generation——事件携带的代次。 + * 返回值:1匹配,0不匹配;不判断TCP是否存活、内部模块是否完整或链路是否STARTED。 + * 说明:先隔离旧代次,避免旧事件冻结、推进或清理新连接;首次使用须先Init。 + */ +static int Identity_Matches(const IEC104_Connection_t *connection, uint64_t generation) +{ + return connection != NULL && generation != 0U && + connection->connection_generation == generation && + connection->phase >= IEC104_CONNECTION_ACTIVE && + connection->phase <= IEC104_CONNECTION_CLOSED; +} + +/** + * 功能:判断当前代次是否仍处于允许读取记录、释放业务接收帧的阶段。 + * 输入参数:connection/generation——待核对对象及事件代次。 + * 返回值:ACTIVE或FAILED且代次匹配返回1,否则0;只判断生命周期,不校验底层存储。 + * 说明:FAILED仍保留回收所需数据,CLOSED已解除借用;普通收发还须使用Require_Active。 + */ +static int Storage_Available(const IEC104_Connection_t *connection, uint64_t generation) +{ + return Identity_Matches(connection, generation) && connection->phase != IEC104_CONNECTION_CLOSED; +} + +/** + * 功能:为普通收发和驱动入口检查连接身份及运行阶段,不修改对象。 + * 返回值:匹配ACTIVE为OK,匹配FAILED为FATAL;空指针、旧代次或不可用阶段为ERROR。 + * 说明:FATAL表示旧会话已经冻结,不能像WAIT一样继续重试正常收发。 + */ +static IEC104_ConnectionResult_t Require_Active(const IEC104_Connection_t *connection, uint64_t generation) +{ + if (!Storage_Available(connection, generation)) { + return IEC104_CONNECTION_ERROR; + } + return connection->phase == IEC104_CONNECTION_ACTIVE ? IEC104_CONNECTION_OK : IEC104_CONNECTION_FATAL; +} + +/** + * 功能:先检查运行资格,再拒绝倒退的事件时刻。 + * 输入参数:now_ms——当前串行事件的单调毫秒值,允许等于上次时刻及0。 + * 返回值:OK允许继续,ERROR身份/阶段/时间非法,FATAL当前连接已冻结。 + * 说明:此处不写last_now_ms;例如后续frame_id或写入量校验失败时,时刻也不前移。 + */ +static IEC104_ConnectionResult_t Check_Time( + const IEC104_Connection_t *connection, uint64_t generation, uint64_t now_ms) +{ + IEC104_ConnectionResult_t result = Require_Active(connection, generation); + if (result != IEC104_CONNECTION_OK) { + return result; + } + return now_ms >= connection->last_now_ms ? IEC104_CONNECTION_OK : IEC104_CONNECTION_ERROR; +} + +/** + * 功能:冻结已校验身份的当前连接,登记首次故障原因。 + * 输入参数:connection——ACTIVE或FAILED对象;fault——本次发现的故障。 + * 输出参数:首次调用将phase置FAILED并保存fault,后续调用不覆盖原故障。 + * 返回值:始终FATAL;不关闭TCP、不Abort模块、不修改发送记录或ASDU队列。 + * 说明:底层处理在故障前可能已改变部分状态,业务须先核对记录再Close,不能重放事件。 + */ +static IEC104_ConnectionResult_t Fail(IEC104_Connection_t *connection, IEC104_ConnectionFault_t fault) +{ + if (connection->phase == IEC104_CONNECTION_ACTIVE) { + connection->fault = fault; + connection->phase = IEC104_CONNECTION_FAILED; + } + return IEC104_CONNECTION_FATAL; +} + +/* 原始起点仅保存在所属对象;0是合法起点,active与remaining_ms分开表示。 + * 先校验时钟再作64位差,不计算started_ms + timeout_ms,避免溢出。 */ +static int Set_Deadline(ConnectionDeadline_t *deadline, + uint64_t now_ms, uint64_t started_ms, uint32_t timeout_ms) +{ + uint64_t elapsed; + if (timeout_ms == 0U || now_ms < started_ms) { + return 0; + } + elapsed = now_ms - started_ms; + deadline->active = 1U; + deadline->remaining_ms = elapsed >= timeout_ms ? 0U : timeout_ms - (uint32_t)elapsed; + return 1; +} + +static int Deadline_Expired(const ConnectionDeadlines_t *deadlines, ConnectionDeadlineKind_t kind) +{ + return deadlines->items[kind].active != 0U && deadlines->items[kind].remaining_ms == 0U; +} + +/* Check_Timeouts、发送准入、控制推进和Get_Wait_Time共用的期限视图。 + * 不处理超时、不推进序号、不改计时起点;是否暂缓确认故障由调用方按接收批次决定。 + * t2/t3仅在控制槽和Link准入允许时参与唤醒,容量背压仍由已有发送/确认期限约束。 */ +static int Collect_Deadlines( + const IEC104_Connection_t *connection, uint64_t now_ms, ConnectionDeadlines_t *deadlines) +{ + IEC104_SendManagerDeadlines_t sending; + ConnectionDeadlines_t prepared; + int ready; + + if (IEC104_SendManager_Get_Deadlines(&connection->send_manager, connection->connection_generation, + now_ms, connection->config.local_send_timeout_ms, connection->config.link.t1_ms, + &sending) != IEC104_SEND_MANAGER_OK) { + return 0; + } + memset(&prepared, 0, sizeof(prepared)); + prepared.head_frame_id = sending.head_frame_id; + prepared.items[DEADLINE_LOCAL_SEND].active = sending.has_send_head; + prepared.items[DEADLINE_LOCAL_SEND].remaining_ms = sending.local_send_remaining_ms; + prepared.items[DEADLINE_I_ACK].active = sending.has_i_ack_deadline; + prepared.items[DEADLINE_I_ACK].remaining_ms = sending.i_ack_remaining_ms; + if ((connection->testfr_timer_active != 0U && + !Set_Deadline(&prepared.items[DEADLINE_TESTFR], now_ms, + connection->testfr_started_ms, connection->config.link.t1_ms)) || + (connection->stop_timer_active != 0U && + !Set_Deadline(&prepared.items[DEADLINE_STOP], now_ms, + connection->stop_started_ms, connection->config.stop_timeout_ms))) { + return 0; + } + if (sending.control_has_room != 0U) { + if (connection->rx_ack_timer_active != 0U) { + ready = IEC104_Link_Can_Prepare_S_Ack(&connection->link_state, connection->config.link.w, 1); + if (ready < 0 || (ready == 1 && !Set_Deadline(&prepared.items[DEADLINE_T2], now_ms, + connection->rx_ack_started_ms, connection->config.link.t2_ms))) { + return 0; + } + } + ready = IEC104_Link_Can_Prepare_Testfr_Act(&connection->link_state); + if (ready < 0 || (ready == 1 && !Set_Deadline(&prepared.items[DEADLINE_T3], now_ms, + connection->last_activity_ms, connection->config.link.t3_ms))) { + return 0; + } + } + *deadlines = prepared; + return 1; +} + +/** + * 功能:本地整帧完成后,检查当前连续未确认接收区间是否已经全部确认。 + * 输入参数:connection——对应Link完成处理已提交的连接。 + * 输出参数:Link报告没有未确认接收数据时清除rx_ack_timer_active,否则保留原计时起点。 + * 返回值:OK查询及更新成功;Link状态异常时冻结并返回FATAL。 + * 说明:I帧捎带确认或S确认必须实际写完才计入acked_nr;准备/排队报文不结束t2。 + */ +static IEC104_ConnectionResult_t Update_Receive_Timer(IEC104_Connection_t *connection) +{ + IEC104_LinkStatus_t status; + if (IEC104_Link_Get_Status(&connection->link_state, &status) != 1) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + if (status.unconfirmed_rx_count == 0U) { + connection->rx_ack_timer_active = 0U; + } + return IEC104_CONNECTION_OK; +} + +/** + * 功能:归还当前已受理S/U帧占用的接收空间,允许后续Feed继续输入。 + * 输入参数:connection——控制事件已成功受理,或合法确认已按规则忽略的连接。 + * 返回值:OK释放成功;底层释放异常时冻结并返回FATAL。 + * 说明:释放接收原文不等于控制应答已发送;应答由SendBuffer继续持有。 + * I帧由TcpServer复制为事件后另行Release,本函数不能释放WAIT中的未受理请求。 + */ +static IEC104_ConnectionResult_t Release_Control_Frame(IEC104_Connection_t *connection) +{ + if (IEC104_Receiver_Release_Frame(&connection->receiver, connection->connection_generation) != + IEC104_RECEIVER_OK) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + return IEC104_CONNECTION_OK; +} + +/* 状态改变的ACT受通知容量约束;重复ACT不额外占用状态通知。 */ +static int Transfer_Change_Allowed(const IEC104_Connection_t *connection, const IEC104_ApciInfo_t *apci) +{ + int changes = (apci->u_function == IEC104_U_STARTDT_ACT && + connection->link_state.dt_state != IEC104_DT_STARTED) || + (apci->u_function == IEC104_U_STOPDT_ACT && + connection->link_state.dt_state != IEC104_DT_STOPPED); + return !changes || connection->transfer_observer.can_accept == NULL || + connection->transfer_observer.can_accept(connection->transfer_observer.context); +} + +/* 状态可能在后续提交失败之前已经生效;仍先交付该事实,再由调用方报告故障。 */ +static int Notify_Transfer_Change(IEC104_Connection_t *connection, IEC104_DataTransferState_t before) +{ + return before == connection->link_state.dt_state || connection->transfer_observer.notify == NULL || + connection->transfer_observer.notify(connection->transfer_observer.context, connection->link_state.dt_state); +} + +/** + * 功能:按接收顺序将当前完整I/S交给Manager,将U帧交给对应启停或TESTFR入口。 + * 输入参数:connection——ACTIVE连接;now_ms——本次驱动时刻,用于应答入队。 + * 输出参数:受理后更新Link/Manager;I帧标记rx_link_applied并保留原文,S/U自动释放。 + * 返回值:无完整帧或控制帧已处理为OK;业务I帧为FRAME_READY;请求未受理为WAIT;异常FATAL。 + * 说明:WAIT保留当前请求;启停通知容量不足也返回WAIT,不改变状态、不消费请求。 + * 已受理I帧直接报告就绪,保证后续驱动不会重复推进V(R)或处理累计ACK。 + */ +static IEC104_ConnectionResult_t Dispatch_Received_Frame(IEC104_Connection_t *connection, uint64_t now_ms) +{ + IEC104_ReceivedFrame_t frame; + IEC104_ReceiverResult_t received; + IEC104_SendManagerResult_t handled; + + IEC104_DataTransferState_t before; + uint64_t generation = connection->connection_generation; + + received = IEC104_Receiver_Peek_Frame(&connection->receiver, generation, &frame); + if (received == IEC104_RECEIVER_EMPTY) { + return IEC104_CONNECTION_OK; + } + if (received != IEC104_RECEIVER_FRAME_READY) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + if (connection->rx_link_applied != 0U) { + return IEC104_CONNECTION_FRAME_READY; + } + if (frame.apci.frame_type == IEC104_FRAME_TYPE_U && + (frame.apci.u_function == IEC104_U_STARTDT_CON || frame.apci.u_function == IEC104_U_STOPDT_CON)) { + /* 子站不发启停ACT;格式合法的反向CON仅消费,不改变启停、确认期限或t3活动时刻。 */ + return Release_Control_Frame(connection); + } + /* 使用成帧时刻登记活动;WAIT重试不续期,也不覆盖更晚的本地整帧完成时刻。 */ + if (connection->received_at_ms > connection->last_activity_ms) { + connection->last_activity_ms = connection->received_at_ms; + } + if (frame.apci.frame_type == IEC104_FRAME_TYPE_I || frame.apci.frame_type == IEC104_FRAME_TYPE_S) { + /* I/S的N(R)统一由Manager验证和登记;此处不能再按网络序号直接释放队列原文。 */ + handled = IEC104_SendManager_Handle_I_S_Frame(&connection->send_manager, generation, &frame.apci); + if (handled != IEC104_SEND_MANAGER_OK) { + return Fail(connection, handled == IEC104_SEND_MANAGER_PROTOCOL_ERROR ? + IEC104_CONNECTION_FAULT_PROTOCOL : IEC104_CONNECTION_FAULT_INTERNAL); + } + if (frame.apci.frame_type == IEC104_FRAME_TYPE_S) { + return Release_Control_Frame(connection); + } + connection->rx_link_applied = 1U; + if (connection->rx_ack_timer_active == 0U) { + /* 首个尚未确认I帧开启t2;采用成帧时刻,重复驱动或后续I帧均不续期。 */ + connection->rx_ack_started_ms = connection->received_at_ms; + connection->rx_ack_timer_active = 1U; + } + return IEC104_CONNECTION_FRAME_READY; + } + if (!Transfer_Change_Allowed(connection, &frame.apci)) { + return IEC104_CONNECTION_WAIT; + } + before = connection->link_state.dt_state; + /* ACT和TESTFR_CON已经通过格式校验,合法重复由底层正常受理或忽略; + * 这些分支的处理失败属于内部状态/交接异常,反向启停CON已在上方消费。 */ + switch (frame.apci.u_function) { + case IEC104_U_STARTDT_ACT: + handled = IEC104_SendManager_Slave_Handle_Startdt_Act( + &connection->send_manager, generation, &frame.apci, now_ms); + if (!Notify_Transfer_Change(connection, before)) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + if (handled != IEC104_SEND_MANAGER_QUEUED && handled != IEC104_SEND_MANAGER_WAIT) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + if (handled == IEC104_SEND_MANAGER_QUEUED) { + connection->stop_timer_active = 0U; + memset(&connection->stop_con_frame_id, 0, sizeof(connection->stop_con_frame_id)); + } + break; + case IEC104_U_STOPDT_ACT: + handled = IEC104_SendManager_Slave_Handle_Stopdt_Act(&connection->send_manager, generation, &frame.apci); + if (!Notify_Transfer_Change(connection, before)) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + if (handled == IEC104_SEND_MANAGER_OK) { + /* 本轮首次受理才启用计时,起点取原成帧时刻,通知容量不足造成的受理前WAIT也计入。 + * 重复STOP可追加CON,但不能延长尚未完成的停止总期限。 */ + if (connection->stop_timer_active == 0U) { + connection->stop_started_ms = connection->received_at_ms; + connection->stop_timer_active = 1U; + } + memset(&connection->stop_con_frame_id, 0, sizeof(connection->stop_con_frame_id)); + } else if (handled != IEC104_SEND_MANAGER_IGNORED) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + break; + case IEC104_U_TESTFR_ACT: + handled = IEC104_SendManager_Handle_Testfr_Act(&connection->send_manager, generation, &frame.apci, now_ms); + if (handled != IEC104_SEND_MANAGER_QUEUED && handled != IEC104_SEND_MANAGER_WAIT) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + break; + case IEC104_U_TESTFR_CON: + handled = IEC104_SendManager_Handle_Testfr_Con(&connection->send_manager, generation, &frame.apci); + if (handled == IEC104_SEND_MANAGER_OK) { + /* 只有匹配当前等待过程的CON才取消探测t1;提前/无匹配CON不缓存。 */ + connection->testfr_timer_active = 0U; + } else if (handled != IEC104_SEND_MANAGER_IGNORED) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + return Release_Control_Frame(connection); + default: + /* 其余功能码仍按协议异常处理,不能扩展为忽略任意U帧。 */ + return Fail(connection, IEC104_CONNECTION_FAULT_PROTOCOL); + } + return handled == IEC104_SEND_MANAGER_WAIT ? IEC104_CONNECTION_WAIT : Release_Control_Frame(connection); +} + +/* 从同一份期限视图选择确认故障,保持I帧ACK先于TESTFR的顺序。 + * 到期事实独立于接收批次:可暂缓断连,但发送准备仍须暂停新I帧编号。 */ +static IEC104_ConnectionFault_t Get_Confirmation_Timeout(const ConnectionDeadlines_t *deadlines) +{ + if (Deadline_Expired(deadlines, DEADLINE_I_ACK)) { + return IEC104_CONNECTION_FAULT_I_ACK_TIMEOUT; + } + if (Deadline_Expired(deadlines, DEADLINE_TESTFR)) { + return IEC104_CONNECTION_FAULT_TESTFR_TIMEOUT; + } + return IEC104_CONNECTION_FAULT_NONE; +} + +/** + * 功能:检查本地发送和STOP总期限;按驱动阶段决定是否检查对端确认t1。 + * 输入参数:connection——ACTIVE连接;now_ms——已通过单调检查的当前时刻;scope——期限检查范围。 + * 返回值:OK表示本轮没有发现到期/异常;首个到期或状态异常冻结连接并返回FATAL。 + * 说明:本地发送超时先向Manager提交对应帧失败,其余期限直接冻结并保留记录。 + * 接收/写入事实在调用本函数前已提交,检查的是这些事实处理后仍存在的等待义务。 + */ +static IEC104_ConnectionResult_t Check_Timeouts( + IEC104_Connection_t *connection, uint64_t now_ms, ConnectionTimeoutScope_t scope) +{ + ConnectionDeadlines_t deadlines; + + if (!Collect_Deadlines(connection, now_ms, &deadlines)) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + if (Deadline_Expired(&deadlines, DEADLINE_LOCAL_SEND)) { + if (IEC104_SendManager_Handle_Send_Failure(&connection->send_manager, deadlines.head_frame_id) != + IEC104_SEND_MANAGER_FATAL) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + return Fail(connection, IEC104_CONNECTION_FAULT_LOCAL_SEND_TIMEOUT); + } + /* 发送阶段只登记真实写入事实,不凭新取的时刻抢先判定未解析的确认丢失。 + * TCP先有界处理已到达的一批输入;预算让出时可立即续收同一份固定快照。 + * 只暂缓对端确认期限,快照结束时Tick仍按原始起点判定,新流量不能扩大旧批次。 */ + if (scope == CHECK_ALL_TIMEOUTS && + connection->receive_batch_active == 0U && connection->receive_batch_pending == 0U) { + IEC104_ConnectionFault_t fault = Get_Confirmation_Timeout(&deadlines); + if (fault != IEC104_CONNECTION_FAULT_NONE) { + return Fail(connection, fault); + } + } + if (Deadline_Expired(&deadlines, DEADLINE_STOP)) { + return Fail(connection, IEC104_CONNECTION_FAULT_STOP_TIMEOUT); + } + return IEC104_CONNECTION_OK; +} + +/** + * 功能:按STOP、普通确认的顺序,尝试准备本轮的一项控制义务。 + * 输入参数:connection——ACTIVE连接;now_ms——本次时刻。 + * 返回值:Manager的QUEUED/WAIT/ERROR/FATAL;没有当前可准备义务时EMPTY。 + * 说明:Link只读查询与实际构帧共用准入规则;Connection只决定驱动顺序和t2是否到期。 + * 查询不消耗发送编号,只有确有义务才向Manager请求控制槽;容量不足留待后续驱动。 + */ +static IEC104_SendManagerResult_t Queue_Pending_Control(IEC104_Connection_t *connection, uint64_t now_ms) +{ + const IEC104_LinkState_t *link = &connection->link_state; + ConnectionDeadlines_t deadlines; + uint64_t generation = connection->connection_generation; + int ready = IEC104_Link_Can_Prepare_Stopdt_Response(link); + int t2_expired; + + if (ready < 0) { + return IEC104_SEND_MANAGER_ERROR; + } + if (ready == 1) { + IEC104_SendFrameId_t stop_con_id = {0U, 0U}; + IEC104_SendManagerResult_t result = IEC104_SendManager_Slave_Queue_Stopdt_Response( + &connection->send_manager, generation, now_ms, &stop_con_id); + if (result == IEC104_SEND_MANAGER_QUEUED && stop_con_id.frame_serial != 0U) { + connection->stop_con_frame_id = stop_con_id; + } + return result; + } + + /* t2到期只请求立即确认;是否准入、是否已有S在途、w是否满足均交给Link。 */ + if (!Collect_Deadlines(connection, now_ms, &deadlines)) { + return IEC104_SEND_MANAGER_ERROR; + } + t2_expired = Deadline_Expired(&deadlines, DEADLINE_T2); + ready = IEC104_Link_Can_Prepare_S_Ack(link, connection->config.link.w, t2_expired); + if (ready < 0) { + return IEC104_SEND_MANAGER_ERROR; + } + if (ready == 1) { + return IEC104_SendManager_Queue_S_Ack( + &connection->send_manager, generation, connection->config.link.w, t2_expired, now_ms); + } + return IEC104_SEND_MANAGER_EMPTY; +} + +/** + * 功能:最多推进两项控制义务,使S及STOP_CON可依次排队,再按t3安排主动探测。 + * 输入参数:connection——ACTIVE且本轮超时检查通过;now_ms——控制帧入队时刻。 + * 返回值:OK本轮正常,包括无义务或容量等待;底层异常冻结并返回FATAL。 + * 说明:不组合Link内部枚举和序号,不循环等待资源,也不准备新的业务I帧。 + */ +static IEC104_ConnectionResult_t Advance_Control(IEC104_Connection_t *connection, uint64_t now_ms) +{ + IEC104_SendManagerResult_t result; + ConnectionDeadlines_t deadlines; + unsigned count; + /* 一次最多两项:S确认后可立即将STOP_CON排在它后面,不等网络完成。 */ + for (count = 0U; count < 2U; ++count) { + result = Queue_Pending_Control(connection, now_ms); + if (result != IEC104_SEND_MANAGER_EMPTY && result != IEC104_SEND_MANAGER_QUEUED && + result != IEC104_SEND_MANAGER_WAIT) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + if (result != IEC104_SEND_MANAGER_QUEUED) { + break; + } + } + /* 前面的S/STOP_CON可能已用掉控制槽,重新收集后再决定是否启动探测。 */ + if (!Collect_Deadlines(connection, now_ms, &deadlines)) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + if (Deadline_Expired(&deadlines, DEADLINE_T3)) { + result = IEC104_SendManager_Queue_Testfr_Act( + &connection->send_manager, connection->connection_generation, now_ms); + if (result != IEC104_SEND_MANAGER_QUEUED && result != IEC104_SEND_MANAGER_WAIT) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + } + return IEC104_CONNECTION_OK; +} + +/** + * 功能:为Feed、Prepare、发送进度回报和Tick执行一次统一调度。 + * 输入参数:connection——已提交本次时刻的ACTIVE连接;now_ms——同一本次时刻;scope——期限检查范围。 + * 返回值:正常时返回接收分派的OK/FRAME_READY/WAIT;任一步骤FATAL则立即停止后续步骤。 + * 说明:一次有界推进,不循环等资源;接收WAIT仍检查超时并推进控制发送,避免互相等待。 + * 发送入口只检查本地期限;接收批次内也暂缓确认超时,批次结束由Tick检查,不改变起点。 + * 返回OK只描述本轮处理结果,不保证发送缓冲为空;调用方仍可Prepare取得控制帧。 + */ +static IEC104_ConnectionResult_t Drive( + IEC104_Connection_t *connection, uint64_t now_ms, ConnectionTimeoutScope_t scope) +{ + IEC104_ConnectionResult_t received = Dispatch_Received_Frame(connection, now_ms); + if (received == IEC104_CONNECTION_FATAL || + Check_Timeouts(connection, now_ms, scope) == IEC104_CONNECTION_FATAL || + Advance_Control(connection, now_ms) == IEC104_CONNECTION_FATAL) { + return IEC104_CONNECTION_FATAL; + } + return received; +} + +/** + * 功能:将Manager的普通操作结果转换为Connection结果,本函数不修改任何对象。 + * 返回值:OK/EMPTY/WAIT分别映射同义结果,其他值映射ERROR。 + * 说明:用于记录查询、记录移除以及发送准备的EMPTY/WAIT;底层调用本身可能有副作用。 + * SEND_READY、FRAME_WRITTEN及FATAL另由调用方处理,不能借此映射推断发送完成。 + */ +static IEC104_ConnectionResult_t Record_Result(IEC104_SendManagerResult_t result) +{ + switch (result) { + case IEC104_SEND_MANAGER_OK: return IEC104_CONNECTION_OK; + case IEC104_SEND_MANAGER_EMPTY: return IEC104_CONNECTION_EMPTY; + case IEC104_SEND_MANAGER_WAIT: return IEC104_CONNECTION_WAIT; + default: return IEC104_CONNECTION_ERROR; + } +} + +/** + * 功能:填写可用于Init的子站连接默认配置。 + * 输入参数:config——调用方提供的配置存储,允许NULL。 + * 输出参数:写入Link默认k/w/t1/t2/t3,本地发送和STOP总期限取默认t1,HIGH严格优先。 + * 返回值:无;NULL时不操作。 + * 说明:只填写配置,不初始化连接、不启动计时;调用方可在Init前修改配置字段。 + */ +void IEC104_Connection_Config_Default(IEC104_ConnectionConfig_t *config) +{ + if (config != NULL) { + memset(config, 0, sizeof(*config)); + IEC104_Link_Config_Default(&config->link); + config->local_send_timeout_ms = config->link.t1_ms; + config->stop_timeout_ms = config->link.t1_ms; + } +} + +/** + * 功能:建立一条初始STOPPED的子站连接,绑定接收、链路、发送缓冲及发送记录。 + * 输入参数:connection——首次使用或已Close的对象;queue——已Init/Rebind到本代次的ASDU队列。 + * control_frames/control_capacity——控制槽数组及槽数;records/record_capacity——I帧记录数组及条数。 + * config——有效配置;connection_generation——非零且跨连接不复用;now_ms——初始单调时刻。 + * 输出参数:成功写入完整Connection;Manager成功绑定时清空HIGH待发原文,保留待发NORMAL。 + * 返回值:OK初始化成功;ERROR参数、配置、队列或绑定条件非法,connection和队列保持原值。 + * 调用方:TcpServer接受新连接时;旧连接必须先结束I/O和借用并Close,不能运行中直接重新Init。 + * 说明:Receiver及配置先准备;Manager校验成功后在最终地址建立Link/Buffer,再提交其余连接字段。 + * 最后一次可能失败的调用之后仅复制已校验对象;最终地址必须稳定,所有借用存储持续有效。 + */ +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) +{ + IEC104_Connection_t prepared; + if (connection == NULL || config == NULL || config->link.k == 0U || config->link.k > IEC104_SEQUENCE_MASK || + config->link.w == 0U || config->link.w > IEC104_SEQUENCE_MASK || config->link.t1_ms == 0U || + config->link.t2_ms == 0U || config->link.t2_ms >= config->link.t1_ms || + config->link.t3_ms == 0U || config->local_send_timeout_ms == 0U || + config->stop_timeout_ms == 0U) { + return IEC104_CONNECTION_ERROR; + } + memset(&prepared, 0, sizeof(prepared)); + if (IEC104_Receiver_Init(&prepared.receiver, connection_generation) != IEC104_RECEIVER_OK) { + return IEC104_CONNECTION_ERROR; + } + prepared.config = *config; + prepared.connection_generation = connection_generation; + prepared.last_now_ms = now_ms; + prepared.last_activity_ms = now_ms; + prepared.phase = IEC104_CONNECTION_ACTIVE; + /* Manager在最终地址提交子对象;失败时connection、queue及借用数组均保持原值。 + * 成功之后没有可失败步骤,prepared中的Manager已绑定最终地址,不需要修补内部字段。 */ + if (IEC104_SendManager_Init_Connection(&prepared.send_manager, queue, + &connection->send_buffer, &connection->link_state, control_frames, control_capacity, + records, record_capacity, prepared.config.link.k, prepared.config.high_priority_burst_limit, + connection_generation) != IEC104_SEND_MANAGER_OK) { + return IEC104_CONNECTION_ERROR; + } + prepared.send_buffer = connection->send_buffer; + prepared.link_state = connection->link_state; + *connection = prepared; + return IEC104_CONNECTION_OK; +} + +/* 接收批次只改变确认超时的检查时机;原有单帧接口和所有时间起点保持不变。 */ +IEC104_ConnectionResult_t IEC104_Connection_Begin_Receive_Batch( + IEC104_Connection_t *connection, uint64_t connection_generation) +{ + IEC104_ConnectionResult_t checked = Require_Active(connection, connection_generation); + if (checked != IEC104_CONNECTION_OK) { + return checked; + } + if (connection->receive_batch_active != 0U) { + return IEC104_CONNECTION_ERROR; + } + connection->receive_batch_active = 1U; + return IEC104_CONNECTION_OK; +} + +IEC104_ConnectionResult_t IEC104_Connection_Yield_Receive_Batch( + IEC104_Connection_t *connection, uint64_t connection_generation, uint64_t now_ms) +{ + IEC104_ConnectionResult_t checked = Check_Time(connection, connection_generation, now_ms); + if (checked != IEC104_CONNECTION_OK) { + return checked; + } + if (connection->receive_batch_active == 0U) { + return IEC104_CONNECTION_ERROR; + } + connection->last_now_ms = now_ms; + connection->receive_batch_active = 0U; + connection->receive_batch_pending = 1U; + return Drive(connection, now_ms, CHECK_ALL_TIMEOUTS); +} + +/* 安装后仅由串行Drive调用;默认空适配器保留Connection独立使用方式。 */ +IEC104_ConnectionResult_t IEC104_Connection_Set_Transfer_Observer( + IEC104_Connection_t *connection, uint64_t connection_generation, + const IEC104_ConnectionTransferObserver_t *observer) +{ + if (Require_Active(connection, connection_generation) != IEC104_CONNECTION_OK || + observer == NULL || (observer->can_accept == NULL) != (observer->notify == NULL) || + connection->receive_serial != 0U) { + return IEC104_CONNECTION_ERROR; + } + connection->transfer_observer = *observer; + return IEC104_CONNECTION_OK; +} + +/** + * 功能:接收一段TCP字节,最多组装一帧并推进链路受理、控制应答及超时检查。 + * 输入参数:connection/connection_generation——本连接及输入所属代次;data/length——本次可用字节。 + * now_ms——本次单调时刻;length为0时data可为NULL,用于重试保留帧。 + * 输出参数:consumed_length必填,写实际消费量;未消费尾部由调用方保留,不能按length直接丢弃。 + * 返回值:OK半帧或控制帧已处理;FRAME_READY业务I帧就绪;WAIT请求尚未受理;ERROR接入非法;FATAL冻结。 + * 调用方:TcpServer接收调度;I帧就绪后Peek并复制为事件,再Release;WAIT时驱动发送或Tick后重试。 + * 说明:保留完整帧时消费0字节,即使本轮重试成功释放该帧,也要另一次Feed才能消费新输入。 + * FRAME_READY只表示链路受理,ASDU内容解析和业务处理由应用取得TcpServer事件副本后完成。 + */ +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) +{ + IEC104_ConnectionResult_t checked; + IEC104_ReceiverResult_t received; + IEC104_ReceiverStatus_t before_receive; + if (consumed_length == NULL) { + return IEC104_CONNECTION_ERROR; + } + *consumed_length = 0U; + if (data == NULL && length != 0U) { + return IEC104_CONNECTION_ERROR; + } + checked = Check_Time(connection, connection_generation, now_ms); + if (checked != IEC104_CONNECTION_OK) { + return checked; + } + /* 区分新成帧与旧帧重试;分派时按原成帧时刻更新活动,反向启停CON不刷新t3。 */ + if (IEC104_Receiver_Get_Status(&connection->receiver, connection_generation, &before_receive) != + IEC104_RECEIVER_OK) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + received = IEC104_Receiver_Feed(&connection->receiver, connection_generation, data, length, consumed_length); + connection->last_now_ms = now_ms; + if (received == IEC104_RECEIVER_FRAME_ERROR) { + return Fail(connection, IEC104_CONNECTION_FAULT_FRAME); + } + if (received != IEC104_RECEIVER_FRAME_READY && received != IEC104_RECEIVER_IN_PROGRESS) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + if (received == IEC104_RECEIVER_FRAME_READY && before_receive.has_complete_frame == 0U) { + if (connection->receive_serial == UINT64_MAX) { + /* 不回绕复用身份,否则同一连接上迟到的Release可能匹配另一帧。 */ + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + connection->receive_serial++; + connection->received_at_ms = now_ms; + } + return Drive(connection, now_ms, CHECK_ALL_TIMEOUTS); +} + +/** + * 功能:向TcpServer提供已通过链路受理、尚未释放的内部I帧只读视图。 + * 输入参数:connection/connection_generation——本连接及代次,允许ACTIVE或FAILED。 + * 输出参数:仅FRAME_READY时写frame,包含完整APDU/APCI借用及本次receive_serial。 + * 返回值:FRAME_READY可读取;EMPTY无完整业务帧;WAIT保留帧尚未受理;ERROR身份、输出或存储非法。 + * 说明:不消费原文、不提交链路事件;借用在匹配Release或Close后失效。 + * FAILED下WAIT仅描述遗留未受理帧,不表示可以继续驱动故障连接。 + */ +IEC104_ConnectionResult_t IEC104_Connection_Peek_Received_Frame( + const IEC104_Connection_t *connection, uint64_t connection_generation, IEC104_ConnectionReceivedFrame_t *frame) +{ + IEC104_ConnectionReceivedFrame_t prepared; + IEC104_ReceiverResult_t received; + if (!Storage_Available(connection, connection_generation) || frame == NULL) { + return IEC104_CONNECTION_ERROR; + } + received = IEC104_Receiver_Peek_Frame(&connection->receiver, connection_generation, &prepared.frame); + if (received == IEC104_RECEIVER_EMPTY) { + return IEC104_CONNECTION_EMPTY; + } + if (received != IEC104_RECEIVER_FRAME_READY) { + return IEC104_CONNECTION_ERROR; + } + if (connection->rx_link_applied == 0U) { + return IEC104_CONNECTION_WAIT; + } + prepared.receive_serial = connection->receive_serial; + *frame = prepared; + return IEC104_CONNECTION_FRAME_READY; +} + +/** + * 功能:TcpServer成功复制I_FRAME事件后归还当前I帧接收空间,允许下一次Feed继续消费输入。 + * 输入参数:connection_generation/receive_serial——本次Peek返回的连接代次及接收身份。 + * 输出参数:成功清除接收进度和rx_link_applied;所有指向该APDU/ASDU的旧借用立即失效。 + * 返回值:OK释放成功;ERROR身份不符、没有已受理I帧、已Close或底层释放失败。 + * 说明:ACTIVE和FAILED均可回收;不再次处理N(S)/N(R),不撤回已经安排的链路确认。 + */ +IEC104_ConnectionResult_t IEC104_Connection_Release_Received_Frame( + IEC104_Connection_t *connection, uint64_t connection_generation, uint64_t receive_serial) +{ + if (!Storage_Available(connection, connection_generation) || connection->rx_link_applied == 0U || + receive_serial != connection->receive_serial || + IEC104_Receiver_Release_Frame(&connection->receiver, connection_generation) != IEC104_RECEIVER_OK) { + return IEC104_CONNECTION_ERROR; + } + connection->rx_link_applied = 0U; + return IEC104_CONNECTION_OK; +} + +/** + * 功能:核对当前会话后提交应答,通过启停准入后尝试直接准备I帧;暂不能准备时复制到HIGH。 + * 输入参数:connection/connection_generation——ACTIVE连接及匹配代次;asdu/asdu_length——应答原文。 + * now_ms——单调时刻;head_info——非NULL输出,仅SEND_READY时有效。 + * 返回值:SEND_READY已准备APDU,OK已入HIGH,WAIT尚未接手,ERROR参数/阶段/容量非法,FATAL冻结。 + * 调用关系:TcpServer_Send_Response -> 本接口 -> SendManager_Submit_Response或Enqueue(HIGH);本层不执行socket I/O。 + * 说明:到期时由Enqueue检查准入后仅入队,交还Process先接收ACK再检查期限,不能在应答入口抢先判定远端超时。 + * 未到期时先Drive处理保留接收及控制流程,再由Manager检查准入;禁止阶段返回ERROR且不入HIGH。 + * 两条路径仅在STARTED+IDLE时接手;准入先于容量检查。 + * 受理新STOPDT_ACT时HIGH待发应答作废,无逐条回执;当前I帧、发送记录及NORMAL不受清理影响。 + * 服务级NORMAL由TcpServer直接缓存,不经过此接口;本函数返回后不再借用输入原文。 + */ +static IEC104_ConnectionResult_t 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, int bypass_queue) +{ + IEC104_SendBufferHeadInfo_t prepared; + IEC104_SendManagerResult_t result; + uint32_t wait_ms; + IEC104_ConnectionResult_t checked = Check_Time(connection, connection_generation, now_ms); + if (head_info == NULL || asdu == NULL || asdu_length < IEC104_ASDU_HEADER_LEN || + asdu_length > IEC104_MAX_ASDU_LEN) { + return IEC104_CONNECTION_ERROR; + } + /* 旧代次及失败会话不能接收应答;快速入口不能插入一个尚未结束的接收批次。 */ + if (checked != IEC104_CONNECTION_OK) { + return checked; + } + if (connection->receive_batch_active != 0U) { + return IEC104_CONNECTION_ERROR; + } + if (IEC104_Connection_Get_Wait_Time(connection, connection_generation, now_ms, + UINT32_MAX, &wait_ms) != IEC104_CONNECTION_OK) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + connection->last_now_ms = now_ms; + if (wait_ms == 0U) { + /* 不排队模式交还Process处理到期协议工作,不缓存这次应用取值。 */ + if (bypass_queue) { return IEC104_CONNECTION_WAIT; } + /* 此入口不读socket;ACK须由正常接收批次处理后再检查t1,Manager仍须检查应答准入。 */ + return Record_Result(IEC104_SendManager_Enqueue( + &connection->send_manager, connection_generation, IEC104_ASDU_PRIORITY_HIGH, asdu, asdu_length)); + } + if (Drive(connection, now_ms, CHECK_LOCAL_TIMEOUTS) == IEC104_CONNECTION_FATAL) { + return IEC104_CONNECTION_FATAL; + } + result = bypass_queue ? + IEC104_SendManager_Try_Submit_Response(&connection->send_manager, connection_generation, + asdu, asdu_length, now_ms, &prepared) : + IEC104_SendManager_Submit_Response(&connection->send_manager, connection_generation, + asdu, asdu_length, now_ms, &prepared); + if (result == IEC104_SEND_MANAGER_OK) { + *head_info = prepared; + return IEC104_CONNECTION_SEND_READY; + } + if (result == IEC104_SEND_MANAGER_QUEUED) { + return IEC104_CONNECTION_OK; + } + if (result == IEC104_SEND_MANAGER_FATAL) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + return Record_Result(result); +} + +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) +{ + return Submit_Response(connection, connection_generation, asdu, asdu_length, now_ms, head_info, 0); +} + +/* 复用会话、协议期限和构帧检查,任何等待均不转入HIGH。 */ +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) +{ + return Submit_Response(connection, connection_generation, asdu, asdu_length, now_ms, head_info, 1); +} + +/** + * 功能:先驱动保留帧、控制及本地期限,再取得当前待发送APDU的剩余字节。 + * 输入参数:connection/connection_generation——当前连接及代次;now_ms——本次单调时刻。 + * 输出参数:仅SEND_READY时写head_info,其中pending_data是SendBuffer字节的只读借用。 + * 返回值:SEND_READY有可写字节;EMPTY无待发项;WAIT受门控/窗口/容量或确认到期限制;ERROR非法;FATAL冻结。 + * 调用方:TcpServer实际写TCP前;无当前帧时才由Manager选择ASDU、分配N(S)并建立发送记录。 + * 说明:当前帧须连续写完;重复Prepare只取得剩余部分,不代替真实发送或完成回报。 + * 旧ACK/TESTFR确认t1到期时只允许已有帧发送;无已有帧返回WAIT,不选择HIGH/NORMAL或编号。 + * 对端确认超时故障交回接收阶段;调用方须持续调度Feed/Tick,不能只驱动发送。 + */ +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) +{ + IEC104_SendBufferHeadInfo_t prepared; + IEC104_SendManagerResult_t result; + ConnectionDeadlines_t deadlines; + IEC104_ConnectionResult_t checked = Check_Time(connection, connection_generation, now_ms); + if (head_info == NULL) { + return IEC104_CONNECTION_ERROR; + } + if (checked != IEC104_CONNECTION_OK) { + return checked; + } + connection->last_now_ms = now_ms; + /* 接收WAIT可能正等待控制槽释放,仍须允许取得已有发送帧。 */ + if (Drive(connection, now_ms, CHECK_LOCAL_TIMEOUTS) == IEC104_CONNECTION_FATAL) { + return IEC104_CONNECTION_FATAL; + } + if (!Collect_Deadlines(connection, now_ms, &deadlines)) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + if (deadlines.items[DEADLINE_LOCAL_SEND].active == 0U && + Get_Confirmation_Timeout(&deadlines) != IEC104_CONNECTION_FAULT_NONE) { + /* 暂缓断连不等于允许新增业务发送。控制槽解阻后先交还接收处理确认, + * 不能继续从HIGH/NORMAL编号;没有新状态或计时器,后续ACK/CON会自然解除限制。 */ + return IEC104_CONNECTION_WAIT; + } + result = IEC104_SendManager_Prepare_Next_Send(&connection->send_manager, connection_generation, now_ms, &prepared); + if (result == IEC104_SEND_MANAGER_OK) { + *head_info = prepared; + return IEC104_CONNECTION_SEND_READY; + } + if (result == IEC104_SEND_MANAGER_EMPTY || result == IEC104_SEND_MANAGER_WAIT) { + return Record_Result(result); + } + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); +} + +/* EOF后只有记录槽可在本地回收、其他新I帧条件均满足时,才值得等待通知空间。 + * 调用方已取得WAIT;查询不创建帧、不移除记录,也不改变Prepare保存的时刻。 */ +IEC104_ConnectionResult_t IEC104_Connection_Get_Record_Wait( + const IEC104_Connection_t *connection, uint64_t connection_generation, uint8_t *record_wait) +{ + IEC104_SendManagerStatus_t status; + IEC104_IFrameSendRecord_t record; + IEC104_SendManagerResult_t result; + ConnectionDeadlines_t deadlines; + IEC104_ConnectionResult_t checked = Require_Active(connection, connection_generation); + uint8_t waiting = 0U; + int can_prepare; + if (record_wait == NULL) { + return IEC104_CONNECTION_ERROR; + } + if (checked != IEC104_CONNECTION_OK) { + return checked; + } + if (IEC104_SendManager_Get_Status(&connection->send_manager, &status) != IEC104_SEND_MANAGER_OK) { + return IEC104_CONNECTION_ERROR; + } + if (status.record_count == status.record_capacity) { + /* 使用Link的实际构帧准入查询,不能从观察快照另拼一套启停/窗口判断。 */ + can_prepare = IEC104_Link_Can_Prepare_I_Frame(&connection->link_state, connection->config.link.k); + if (can_prepare < 0) { + return IEC104_CONNECTION_ERROR; + } + if (can_prepare != 0) { + if (!Collect_Deadlines(connection, connection->last_now_ms, &deadlines)) { + return IEC104_CONNECTION_ERROR; + } + if (Get_Confirmation_Timeout(&deadlines) == IEC104_CONNECTION_FAULT_NONE) { + result = IEC104_SendManager_Peek_Confirmed_Record(&connection->send_manager, &record); + if (result != IEC104_SEND_MANAGER_OK && result != IEC104_SEND_MANAGER_EMPTY && + result != IEC104_SEND_MANAGER_WAIT) { + return IEC104_CONNECTION_ERROR; + } + waiting = result == IEC104_SEND_MANAGER_OK; + } + } + } + *record_wait = waiting; + return IEC104_CONNECTION_OK; +} + +/** + * 功能:回报一次真实TCP写入增量,并在整帧完成后推进连接计时和控制过程。 + * 输入参数:frame_id——Prepare返回的当前帧身份;bytes_written_now——本次增量,非累计值,允许0。 + * now_ms——本次单调时刻,最后一段的时刻作为整帧完成时刻;completion必填。 + * 输出参数:Manager更新写入进度;仅FRAME_WRITTEN时写completion数值副本。 + * 返回值:OK尚未整帧完成;FRAME_WRITTEN完成流程成功;ERROR身份/增量/时间非法;FATAL冻结。 + * 调用方:TcpServer取得实际写入结果后;先回报本地已知完成,再Feed依赖该完成的ACK或U帧。 + * 说明:Manager负责恰好一次Link完成、I帧记录更新及发送槽释放,本层不能再重复调用Complete。 + * 使用真实完成时刻更新记录,只检查本地发送和STOP期限;对端确认t1交回接收阶段判定。 + * 后续Drive可能在写入已记账后失败;即使返回FATAL也不能重报增量,须读取记录回收。 + */ +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) +{ + IEC104_LocalSendCompletion_t completed; + IEC104_SendManagerResult_t result; + IEC104_ConnectionResult_t checked = Check_Time(connection, frame_id.connection_generation, now_ms); + if (completion == NULL) { + return IEC104_CONNECTION_ERROR; + } + if (checked != IEC104_CONNECTION_OK) { + return checked; + } + result = IEC104_SendManager_Handle_Send_Progress( + &connection->send_manager, frame_id, bytes_written_now, now_ms, &completed); + if (result == IEC104_SEND_MANAGER_ERROR) { + return IEC104_CONNECTION_ERROR; + } + connection->last_now_ms = now_ms; + if (result != IEC104_SEND_MANAGER_OK && result != IEC104_SEND_MANAGER_FRAME_WRITTEN) { + return Fail(connection, IEC104_CONNECTION_FAULT_INTERNAL); + } + if (result == IEC104_SEND_MANAGER_FRAME_WRITTEN) { + /* 只有整帧完成才刷新发送方向活动时间;0字节和部分写入不重置t3或本地发送期限。 */ + connection->last_activity_ms = now_ms; + if (Update_Receive_Timer(connection) == IEC104_CONNECTION_FATAL) { + return IEC104_CONNECTION_FATAL; + } + if (completed.frame_type == IEC104_SEND_BUFFER_FRAME_TESTFR_ACT && + completed.link_completion_result == IEC104_LINK_RESULT_OK) { + /* TESTFR等待CON的t1从ACT实际发完起算,与t3空闲活动时间独立。 */ + connection->testfr_started_ms = completed.written_at_ms; + connection->testfr_timer_active = 1U; + } + if (completed.frame_type == IEC104_SEND_BUFFER_FRAME_STOPDT_CON && + completed.link_completion_result == IEC104_LINK_RESULT_OK && + completed.frame_id.connection_generation == connection->stop_con_frame_id.connection_generation && + completed.frame_id.frame_serial == connection->stop_con_frame_id.frame_serial) { + /* 仅本轮最后一个STOP_CON完成结束期限;旧CON不能干扰后到START/STOP。 */ + connection->stop_timer_active = 0U; + memset(&connection->stop_con_frame_id, 0, sizeof(connection->stop_con_frame_id)); + } + } + /* 发送完成可能释放控制槽,立即尝试受阻请求;不能把重试延后到下一帧输入。 */ + if (Drive(connection, now_ms, CHECK_LOCAL_TIMEOUTS) == IEC104_CONNECTION_FATAL) { + return IEC104_CONNECTION_FATAL; + } + if (result == IEC104_SEND_MANAGER_FRAME_WRITTEN) { + *completion = completed; + return IEC104_CONNECTION_FRAME_WRITTEN; + } + return IEC104_CONNECTION_OK; +} + +/** + * 功能:把当前帧的最终传输失败交给Manager,再冻结连接供业务回收。 + * 输入参数:connection——当前连接;frame_id——尚未整帧完成的当前发送帧身份。 + * 返回值:匹配并处理失败后FATAL;错误代次、帧身份或调用阶段为ERROR;已冻结同代次仍为FATAL。 + * 说明:部分字节可能已到达对端,不能撤回帧、重新分配N(S)或重发同一增量继续旧会话。 + * 不自动关闭socket或清空业务记录;EAGAIN应走等待/0字节进度,不能当作最终失败。 + */ +IEC104_ConnectionResult_t IEC104_Connection_Handle_Send_Failure( + IEC104_Connection_t *connection, IEC104_SendFrameId_t frame_id) +{ + IEC104_ConnectionResult_t checked = Require_Active(connection, frame_id.connection_generation); + IEC104_SendManagerResult_t result; + if (checked != IEC104_CONNECTION_OK) { + return checked; + } + result = IEC104_SendManager_Handle_Send_Failure(&connection->send_manager, frame_id); + if (result == IEC104_SEND_MANAGER_ERROR) { + return IEC104_CONNECTION_ERROR; + } + return Fail(connection, result == IEC104_SEND_MANAGER_FATAL ? + IEC104_CONNECTION_FAULT_TRANSPORT : IEC104_CONNECTION_FAULT_INTERNAL); +} + +/** + * 功能:由外部周期或事件唤醒,执行一轮保留帧重试、超时检查和控制应答推进。 + * 输入参数:connection/connection_generation——本次唤醒所属连接;now_ms——当前单调时刻。 + * 返回值:OK本轮正常;FRAME_READY仍有业务I帧;WAIT当前接收请求未受理;ERROR接入非法;FATAL冻结。 + * 说明:不创建系统定时器、不执行I/O、不准备新的业务I帧;可能排队S或TESTFR等控制帧。 + * 每次按当前有效过程的起点重新计算,旧回调不能携带旧时间差覆盖新一轮探测。 + */ +IEC104_ConnectionResult_t IEC104_Connection_Tick( + IEC104_Connection_t *connection, uint64_t connection_generation, uint64_t now_ms) +{ + IEC104_ConnectionResult_t checked = Check_Time(connection, connection_generation, now_ms); + if (checked != IEC104_CONNECTION_OK) { + return checked; + } + connection->last_now_ms = now_ms; + connection->receive_batch_active = 0U; + connection->receive_batch_pending = 0U; + return Drive(connection, now_ms, CHECK_ALL_TIMEOUTS); +} + +/** + * 功能:只读汇总各活动期限,给TcpServer提供本轮等待上限。 + * 输出参数:仅OK写wait_ms;查询不改变last_now_ms、期限起点、发送队列或故障状态。 + * 说明:只查询当前发送头和最早已完成未ACK记录,与现有超时检查选取同一对象。 + * t2/t3同时检查Link准入和控制槽容量;已有控制帧/等待CON时由对应发送或t1期限唤醒。 + * 接收批次必须先用Tick结束,不能用等待查询代替推进协议或结束批次。 + */ +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) +{ + ConnectionDeadlines_t deadlines; + IEC104_ConnectionResult_t checked; + uint32_t limited = max_wait_ms; + unsigned i; + + if (wait_ms == NULL) { + return IEC104_CONNECTION_ERROR; + } + checked = Check_Time(connection, connection_generation, now_ms); + if (checked != IEC104_CONNECTION_OK) { + return checked; + } + if (connection->receive_batch_active != 0U || !Collect_Deadlines(connection, now_ms, &deadlines)) { + return IEC104_CONNECTION_ERROR; + } + for (i = 0U; i < DEADLINE_COUNT; ++i) { + if (deadlines.items[i].active != 0U && deadlines.items[i].remaining_ms < limited) { + limited = deadlines.items[i].remaining_ms; + } + } + *wait_ms = connection->receive_batch_pending != 0U ? 0U : limited; + return IEC104_CONNECTION_OK; +} + +/** + * 功能:查看最早一条同时满足本地整帧完成和有效对端ACK的I帧回执。 + * 输入参数:connection/connection_generation——匹配代次且尚未Close的连接,允许FAILED。 + * 输出参数:仅OK时写record独立数值副本;不会自动消费记录或重新释放ASDU原文。 + * 返回值:OK队首回执就绪;EMPTY无记录;WAIT最早记录尚未同时满足两个完成条件;ERROR接入非法。 + * 调用方:TcpServer查询并成功复制RECORD事件后,再按返回的frame_id调用Remove_Confirmed_Record。 + */ +IEC104_ConnectionResult_t IEC104_Connection_Peek_Confirmed_Record( + const IEC104_Connection_t *connection, uint64_t connection_generation, IEC104_IFrameSendRecord_t *record) +{ + if (!Storage_Available(connection, connection_generation)) { + return IEC104_CONNECTION_ERROR; + } + return Record_Result(IEC104_SendManager_Peek_Confirmed_Record(&connection->send_manager, record)); +} + +/** + * 功能:TcpServer已复制RECORD事件后,移除匹配的已完成、已ACK队首记录,归还一条记录容量。 + * 输入参数:connection——ACTIVE或FAILED连接;frame_id——Peek取得并已复制到事件中的回执身份。 + * 返回值:OK移除成功;ERROR身份不符、队首未就绪或记录不可用。 + * 说明:这里释放的是发送记录,不是ASDU原文;NORMAL原文已在ACK处理时确认释放或核减覆盖计数。 + */ +IEC104_ConnectionResult_t IEC104_Connection_Remove_Confirmed_Record( + IEC104_Connection_t *connection, IEC104_SendFrameId_t frame_id) +{ + if (!Storage_Available(connection, frame_id.connection_generation)) { + return IEC104_CONNECTION_ERROR; + } + return Record_Result(IEC104_SendManager_Remove_Confirmed_Record(&connection->send_manager, frame_id)); +} + +/** + * 功能:按交接先后读取任意尚未消费的I帧记录,供故障检查和恢复决策使用。 + * 输入参数:connection/connection_generation——匹配且尚未Close;offset——从当前最早记录起的条目偏移。 + * 输出参数:仅OK时写record数值副本,保留原优先级、帧身份、本地完成及peer_acknowledged事实。 + * 返回值:OK读到记录;EMPTY偏移超出当前条数;ERROR参数、身份或记录状态非法。 + * 说明:ACTIVE/FAILED均可读取,不要求该记录已确认,也不消费;未ACK不能证明对端未收到。 + */ +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) +{ + if (!Storage_Available(connection, connection_generation)) { + return IEC104_CONNECTION_ERROR; + } + return Record_Result(IEC104_SendManager_Get_Record_By_Offset(&connection->send_manager, offset, record)); +} + +/** + * 功能:只读取得连接生命周期、首次故障、链路过程和保留接收帧的概况。 + * 输入参数:connection/connection_generation——匹配对象及代次,允许ACTIVE、FAILED和CLOSED。 + * 输出参数:仅OK时写status数值快照,不暴露底层原文指针。 + * 返回值:OK查询成功;ERROR身份、初始化阶段、输出指针或模块状态非法,不改输出。 + * 说明:CLOSED后的链路字段是收尾时的历史值,不能据此判断当前存在可用TCP连接。 + */ +IEC104_ConnectionResult_t IEC104_Connection_Get_Status( + const IEC104_Connection_t *connection, uint64_t connection_generation, IEC104_ConnectionStatus_t *status) +{ + IEC104_ConnectionStatus_t prepared; + IEC104_LinkStatus_t link_status; + IEC104_SendManagerStatus_t manager_status; + IEC104_ReceiverStatus_t receiver_status; + if (!Identity_Matches(connection, connection_generation) || status == NULL) { + return IEC104_CONNECTION_ERROR; + } + memset(&prepared, 0, sizeof(prepared)); + if (IEC104_Link_Get_Status(&connection->link_state, &link_status) != 1) { + return IEC104_CONNECTION_ERROR; + } + prepared.connection_generation = connection_generation; + prepared.phase = connection->phase; + prepared.fault = connection->fault; + prepared.dt_state = link_status.dt_state; + prepared.dt_procedure = link_status.dt_procedure; + prepared.testfr_state = link_status.testfr_state; + prepared.has_received_i_frame = connection->rx_link_applied; + /* Close已解除模块存储绑定;此时记录数和接收保留数为0,仅展示历史链路状态。 */ + if (connection->phase != IEC104_CONNECTION_CLOSED) { + if (IEC104_SendManager_Get_Status(&connection->send_manager, &manager_status) != IEC104_SEND_MANAGER_OK || + IEC104_Receiver_Get_Status(&connection->receiver, connection_generation, &receiver_status) != + IEC104_RECEIVER_OK) { + return IEC104_CONNECTION_ERROR; + } + prepared.send_record_count = manager_status.record_count; + prepared.has_unhandled_frame = (uint8_t)(receiver_status.has_complete_frame != 0U && + connection->rx_link_applied == 0U); + } + *status = prepared; + return IEC104_CONNECTION_OK; +} + +/** + * 功能:在EOF、socket关闭通知或外部传输故障到达时冻结当前连接。 + * 输入参数:connection/connection_generation——通知所属连接,允许ACTIVE或已FAILED。 + * 输出参数:ACTIVE时登记TRANSPORT故障;已FAILED保留首次原因,不清空记录和接收原文。 + * 返回值:匹配未Close的连接返回FATAL;旧代次、未初始化或已CLOSED返回ERROR。 + * 说明:本入口没有当前帧的写入结果,不能据此推定已发送字节或提交该帧Complete。 + * 调用方负责结束真实I/O、回收所需记录,再Close解除借用。 + */ +IEC104_ConnectionResult_t IEC104_Connection_Handle_Transport_Closed( + IEC104_Connection_t *connection, uint64_t connection_generation) +{ + if (!Storage_Available(connection, connection_generation)) { + return IEC104_CONNECTION_ERROR; + } + return Fail(connection, IEC104_CONNECTION_FAULT_TRANSPORT); +} + +/** + * 功能:结束连接内部存储借用,保留外部ASDU队列供显式重连恢复或丢弃。 + * 输入参数:connection/connection_generation——准备收尾的连接;必须已结束旧I/O及所有字节借用。 + * 输出参数:Abort发送管理及缓冲,清除接收进度和计时有效标志,phase置CLOSED;保留首次故障。 + * 返回值:OK收尾完成,包括同代次重复Close;ERROR对象、代次或初始化阶段非法。 + * 调用方:TcpServer先复制剩余帧、记录及断开通知,再Close;此后记录查询及原文借用均不能继续使用。 + * 说明:不关闭socket、不隐式Rebind;恢复顺序为Close -> Queue.Rebind新代次 -> Init -> 新STARTDT。 + */ +IEC104_ConnectionResult_t IEC104_Connection_Close( + IEC104_Connection_t *connection, uint64_t connection_generation) +{ + if (!Identity_Matches(connection, connection_generation)) { + return IEC104_CONNECTION_ERROR; + } + if (connection->phase == IEC104_CONNECTION_CLOSED) { + return IEC104_CONNECTION_OK; + } + IEC104_SendManager_Abort(&connection->send_manager); + /* Close以结束旧I/O为前提,清除接收缓冲;不触碰独立持有的ASDU队列。 */ + IEC104_Receiver_Abort(&connection->receiver); + connection->rx_link_applied = 0U; + connection->rx_ack_timer_active = 0U; + connection->testfr_timer_active = 0U; + connection->stop_timer_active = 0U; + memset(&connection->stop_con_frame_id, 0, sizeof(connection->stop_con_frame_id)); + memset(&connection->transfer_observer, 0, sizeof(connection->transfer_observer)); + connection->receive_batch_active = 0U; + connection->receive_batch_pending = 0U; + connection->phase = IEC104_CONNECTION_CLOSED; + return IEC104_CONNECTION_OK; +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Connection.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Connection.h new file mode 100644 index 00000000..8bd4d2fd --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Connection.h @@ -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 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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link.c b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link.c new file mode 100644 index 00000000..fa71afc6 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link.c @@ -0,0 +1,1105 @@ +/** + * 公共链路逻辑:初始化、完整状态校验、I/S序号、k/w窗口、S确认与TESTFR。 + * 主站启停见IEC104_Link_Master_StartStop.c,子站启停见IEC104_Link_Slave_StartStop.c。 + * 本文件不执行TCP收发;连接调度层串行提交接收、发送完成和时间事件。 + * + * 协作边界:本文件定义Internal.h中的跨文件辅助函数,供两个角色实现调用。 + * State_Valid按本端角色调用本文件内的static过程校验,不反向依赖角色实现。 + * 带_Internal后缀的函数具有外部链接,但只供链路实现使用;其余局部辅助函数 + * 采用static。公开入口负责校验,内部步骤按各自前置条件复用已校验的状态。 + * 主要提交点:Prepare_I推进vs;Handle_I/S推进va(I另推进vr); + * Complete_I_Send/Complete_S_Ack_Send提交实际发送的N(R),其中I帧完成还推进sent_vs。 + */ +#include "IEC104_Link_Common.h" +#include "IEC104_Link_Internal.h" + +#include + +/* 仅用于本文件内I/S帧的序号规划;只有READY时才读取快照并编码。 */ +typedef enum { + IEC104_PLAN_ERROR, + IEC104_PLAN_SKIPPED, + IEC104_PLAN_READY +} IEC104_PlanResult_t; + +/** + * 功能:计算15位IEC-104序号从from前进到to的距离。 + * 输入参数:from——起始序号;to——目标序号。 + * 输出参数:无。 + * 返回值:0至32767范围内的循环距离。 + */ +static uint16_t Number_Distance(uint16_t from, uint16_t to) +{ + /* 按15位序号环计算距离,使32767到0的距离为1。 */ + return (uint16_t)((to - from) & IEC104_SEQUENCE_MASK); +} + +/** + * 功能:将15位IEC-104序号循环加1。 + * 输入参数:value——当前序号。 + * 输出参数:无。 + * 返回值:递增后的序号;32767的下一个序号为0。 + */ +static uint16_t Number_Increment(uint16_t value) +{ + /* 与0x7FFF相与,自动将32768折回0。 */ + return (uint16_t)((value + 1U) & IEC104_SEQUENCE_MASK); +} + +/** + * 功能:检查序号、稳定状态、标志和TESTFR等公共基础字段。 + * 输入参数:state——链路状态。 + * 输出参数:无。 + * 返回值:公共基础字段合法返回1,否则返回0。 + * 说明:S确认、角色和启停过程由后续完整校验检查。 + */ +static int Link_State_Fields_Valid(const IEC104_LinkState_t *state) +{ + if (state == NULL || + state->vs > IEC104_SEQUENCE_MASK || + state->sent_vs > IEC104_SEQUENCE_MASK || + state->va > IEC104_SEQUENCE_MASK || + state->vr > IEC104_SEQUENCE_MASK || + state->acked_nr > IEC104_SEQUENCE_MASK || + state->s_ack_nr > IEC104_SEQUENCE_MASK) { + return 0; + } + + /* 确认位置不得越过本地完成位置;用共同尾端的循环距离检查,兼容序号回绕。 */ + if (Number_Distance(state->sent_vs, state->vs) > Number_Distance(state->va, state->vs)) { + return 0; + } + + switch (state->dt_state) { + case IEC104_DT_STOPPED: + case IEC104_DT_STARTED: + break; + default: + return 0; + } + + switch (state->testfr_state) { + case IEC104_TESTFR_IDLE: + case IEC104_TESTFR_ACT_SEND_PENDING: + case IEC104_TESTFR_CON_PENDING: + return 1; + default: + return 0; + } +} + +/** + * 功能:计算已经接收但本端尚未通过I/S帧确认的I帧数量。 + * 输入参数:state——序号字段有效的链路状态。 + * 输出参数:无。 + * 返回值:acked_nr到V(R)之间的15位循环距离。 + * 前置条件:state非NULL,acked_nr和vr为15位序号,未确认区间未占满序号环。 + * 调用关系:公共确认逻辑及Slave停止排空共用,因此保留外部链接;不重复校验。 + */ +uint16_t IEC104_Link_Unconfirmed_Rx_Internal(const IEC104_LinkState_t *state) +{ + return Number_Distance(state->acked_nr, state->vr); +} + +/** + * 功能:判断接收确认已完成,或已由在途S帧覆盖当前接收位置。 + * 输入参数:state——已通过完整合法性检查的链路状态。 + * 输出参数:无,不修改state。 + * 返回值:1表示acked_nr或在途S帧已覆盖当前V(R),否则返回0。 + * 说明:不要求S完成回调已提交;不能用只覆盖旧V(R)的S帧替代最新确认。 + * 调用关系:Master准备STOPDT_ACT和接收STOPDT_CON时分别检查,不能缓存旧结果。 + * 返回1不推进acked_nr;在途S仍须按FIFO完成,失败仍须断连。 + */ +int IEC104_Link_Rx_Ack_Covered_Internal(const IEC104_LinkState_t *state) +{ + return state->acked_nr == state->vr || + (state->s_ack_state == IEC104_S_ACK_SEND_PENDING && + state->s_ack_nr == state->vr); +} + +/** + * 功能:计算已提交发送完成、且尚未被对端确认的I帧数量。 + * 输入参数:state——合法链路状态,确认位置没有越过本地完成位置。 + * 输出参数:无,不修改state。 + * 返回值:需等待对端确认的已完成帧数;仅有排队帧时返回0。 + * 说明:ACK必须在对应Complete之后处理,已完成未确认区间为va..sent_vs。 + */ +static uint16_t Sent_Unacknowledged_I_Count(const IEC104_LinkState_t *state) +{ + return Number_Distance(state->va, state->sent_vs); +} + +/** + * 功能:检查普通S确认帧的发送状态和快照组合是否一致。 + * 输入参数:state——待检查的链路状态。 + * 输出参数:无。 + * 返回值:组合一致返回1,否则返回0。 + * 说明:调用前必须已经通过Link_State_Fields_Valid()。 + */ +static int S_Ack_State_Valid(const IEC104_LinkState_t *state) +{ + switch (state->s_ack_state) { + case IEC104_S_ACK_IDLE: + return state->s_ack_nr == 0U; + case IEC104_S_ACK_SEND_PENDING: + /* N(R)=0是合法快照,是否在当前确认区间内留到完成时检查。 */ + return 1; + default: + return 0; + } +} + +/** + * 功能:检查主站dt_state与dt_procedure组合,拒绝子站专用过程。 + * 前置条件:state非NULL,公共字段及S状态已合法,role为CONTROLLING。 + * 返回值:IDLE允许两种稳定状态;启动过程须STOPPED,停止过程须STARTED; + * 满足时返回1,其余返回0。只校验组合,不修改状态或验证报文。 + * 调用关系:仅供本文件内State_Valid按角色分派,使用static内部链接。 + * 不得反调State_Valid/Role_Valid,否则形成递归。 + */ +static int Master_State_Consistent(const IEC104_LinkState_t *state) +{ + switch (state->dt_procedure) { + case IEC104_DT_PROC_IDLE: + return 1; + case IEC104_DT_PROC_START_ACT_SEND_PENDING: + case IEC104_DT_PROC_START_CON_WAITING: + return state->dt_state == IEC104_DT_STOPPED; + case IEC104_DT_PROC_STOP_REQUESTED: + case IEC104_DT_PROC_STOP_ACT_SEND_PENDING: + case IEC104_DT_PROC_STOP_CON_WAITING: + return state->dt_state == IEC104_DT_STARTED; + default: + return 0; + } +} + +/** + * 子站只使用IDLE和STOP_DRAINING;后者表示已停用、等待确认完成。 + * CON发送进度归SendBuffer,不参与启停组合校验。 + */ +static int Slave_State_Consistent(const IEC104_LinkState_t *state) +{ + return state->dt_procedure == IEC104_DT_PROC_IDLE || + (state->dt_procedure == IEC104_DT_PROC_STOP_DRAINING && state->dt_state == IEC104_DT_STOPPED); +} + +/** + * 功能:完整检查链路状态的字段定义域和跨流程一致性。 + * 输入参数:state——链路状态;允许NULL,返回0。 + * 输出参数:无,不修改state。 + * 返回值:公共字段、S状态及本端角色的过程组合合法返回1,否则返回0。 + * 调用关系:公共入口直接调用,角色入口经Role_Valid调用;本函数依次检查 + * Fields、S_Ack,再分派本文件内的Master/Slave_State_Consistent,不会推进过程。 + * 说明:角色末级校验不得反调本函数或Role_Valid,避免递归;本检查不证明事件 + * 属于当前连接,也不替代本次N(S)/N(R)、发送长度和FIFO顺序检查。 + */ +int IEC104_Link_State_Valid_Internal(const IEC104_LinkState_t *state) +{ + if (!Link_State_Fields_Valid(state) || !S_Ack_State_Valid(state)) { + return 0; + } + switch (state->role) { + case IEC104_ROLE_CONTROLLING: + return Master_State_Consistent(state); + case IEC104_ROLE_CONTROLLED: + return Slave_State_Consistent(state); + default: + return 0; + } +} + +/** + * 功能:校验链路后生成只含数值的I帧发送进度快照。 + * 输入参数:state——本连接链路状态;所有访问须串行。 + * 输出参数:progress——仅返回1时写入,不得与state重叠;失败时保持原值。 + * 返回值:1表示成功;0表示指针或完整链路状态非法。 + * 说明:由本模块把内部序号映射为公开的进度含义,不提交任何接收或完成事件。 + */ +int IEC104_Link_Get_Send_Progress( + const IEC104_LinkState_t *state, IEC104_IFrameSendProgress_t *progress) +{ + IEC104_IFrameSendProgress_t prepared; + + if (progress == NULL || !IEC104_Link_State_Valid_Internal(state)) { + return 0; + } + prepared.next_frame_ns = state->vs; + prepared.next_completion_ns = state->sent_vs; + prepared.peer_ack_nr = state->va; + *progress = prepared; + return 1; +} + +/** + * 功能:生成链路观察快照,由Link统一解释未确认接收数量及启停/探测状态。 + * 输入参数:state——当前链路;status——非NULL且不与state重叠的输出。 + * 返回值:1成功并写status,0参数或完整状态非法;失败不改输出,不预留或推进链路。 + * 说明:连接层可据未确认数量结束t2区间;控制动作准入另用Can_Prepare接口。 + */ +int IEC104_Link_Get_Status(const IEC104_LinkState_t *state, IEC104_LinkStatus_t *status) +{ + IEC104_LinkStatus_t prepared; + if (status == NULL || !IEC104_Link_State_Valid_Internal(state)) { + return 0; + } + prepared.role = state->role; + prepared.dt_state = state->dt_state; + prepared.dt_procedure = state->dt_procedure; + prepared.testfr_state = state->testfr_state; + prepared.unconfirmed_rx_count = IEC104_Link_Unconfirmed_Rx_Internal(state); + *status = prepared; + return 1; +} + +/** + * 功能:检查链路是否符合绑定新连接发送管理器的初始状态条件。 + * 输入参数:state——待检查状态,允许NULL。 + * 输出参数:无,不修改链路。 + * 返回值:状态合法、所有序号为0、STOPPED且各过程空闲时返回1,否则0。 + * 说明:完整校验同时保证空闲S快照为0;不检查连接历史或实际TCP状态。 + */ +int IEC104_Link_Is_Initial_State(const IEC104_LinkState_t *state) +{ + return IEC104_Link_State_Valid_Internal(state) && + state->vs == 0U && state->sent_vs == 0U && state->va == 0U && + state->vr == 0U && state->acked_nr == 0U && + state->dt_state == IEC104_DT_STOPPED && state->dt_procedure == IEC104_DT_PROC_IDLE && + state->testfr_state == IEC104_TESTFR_IDLE && state->s_ack_state == IEC104_S_ACK_IDLE; +} + +/** + * 功能:判断对端确认号是否位于V(A)到已提交本地完成位置sent_vs之间。 + * 输入参数:state——有效链路状态;receive_number——对端携带的N(R)。 + * 输出参数:无。 + * 返回值:N(R)为15位且位于va到sent_vs的前向闭区间时返回1;越过完成位置或退回va之前均返回0。 + */ +static int Acknowledge_Number_Valid(const IEC104_LinkState_t *state, + uint16_t receive_number) +{ + uint16_t outstanding_count; + uint16_t acknowledged_count; + + if (receive_number > IEC104_SEQUENCE_MASK) { + return 0; + } + + outstanding_count = Number_Distance(state->va, state->sent_vs); + acknowledged_count = Number_Distance(state->va, receive_number); + + /* Prepare只预留序号;必须先提交对应Complete,才允许ACK覆盖该帧。 */ + return acknowledged_count <= outstanding_count; +} + +/** + * 功能:提交一份本地已经完整发送的I/S帧所携带的N(R)快照。 + * 输入参数:state——已通过完整校验的链路状态;receive_number——实际上线报文携带的N(R)。 + * 输出参数:快照合法时推进acked_nr;非法时不修改状态。 + * 返回值:OK表示快照已提交;FATAL表示快照非法。 + * 说明:这是I/S完成流程共用的内部操作,不判断具体帧类型或发送长度。 + * Complete_I_Send和Complete_S_Ack_Send已校验state;这里仅校验确认号定义域 + * 和acked_nr..vr区间。返回OK表示本地发送提交,不表示对端已处理该确认。 + */ +static IEC104_LinkResult_t Commit_Sent_NR( + IEC104_LinkState_t *state, uint16_t receive_number) +{ + uint16_t acknowledged_count; + uint16_t outstanding_count; + + if (receive_number > IEC104_SEQUENCE_MASK) { + return IEC104_LINK_RESULT_FATAL; + } + acknowledged_count = Number_Distance(state->acked_nr, receive_number); + outstanding_count = IEC104_Link_Unconfirmed_Rx_Internal(state); + if (acknowledged_count > outstanding_count) { + return IEC104_LINK_RESULT_FATAL; + } + + state->acked_nr = receive_number; + return IEC104_LINK_RESULT_OK; +} + +/** + * 功能:把链路状态对象设置为指定角色的新连接初始状态。 + * 输入参数:state——非NULL状态对象;role——已由调用方校验或规范化的角色。 + * 输出参数:保留传入角色,清零序号并复位确认、启停和探测状态。 + * 返回值:无。 + * 说明:仅供State_Init与State_Reset复用,不负责校验指针和角色。 + */ +static void Initialize_State_Fields(IEC104_LinkState_t *state, + IEC104_LinkRole_t role) +{ + state->role = role; + state->vs = 0U; + state->sent_vs = 0U; + state->va = 0U; + state->vr = 0U; + state->acked_nr = 0U; + state->s_ack_state = IEC104_S_ACK_IDLE; + state->s_ack_nr = 0U; + state->dt_state = IEC104_DT_STOPPED; + state->dt_procedure = IEC104_DT_PROC_IDLE; + state->testfr_state = IEC104_TESTFR_IDLE; +} + +/** + * 功能:初始化一条连接的链路窗口与超时默认配置。 + * 输入参数:config——待初始化的配置对象;允许为NULL。 + * 输出参数:有效对象写入k、w、t1、t2和t3默认值。 + * 返回值:无。 + */ +/* 适用端:控制站与被控站。 */ +void IEC104_Link_Config_Default(IEC104_LinkConfig_t *config) +{ + if (config == NULL) { + return; + } + + config->k = IEC104_DEFAULT_K; + config->w = IEC104_DEFAULT_W; + config->t1_ms = IEC104_DEFAULT_T1_MS; + config->t2_ms = IEC104_DEFAULT_T2_MS; + config->t3_ms = IEC104_DEFAULT_T3_MS; +} + +/** + * 功能:为新连接初始化链路状态,并固定本端角色。 + * 输入参数:state——待初始化的状态对象;role——CONTROLLING或CONTROLLED。 + * 输出参数:成功时写入role,清零序号和确认快照,数据传输置STOPPED, + * 启停过程、S确认和TESTFR置IDLE。 + * 返回值:1表示初始化成功;0表示state为NULL或role非法,此时不修改状态。 + * 说明:仅初始化链路算法状态,连接配置由Config_Default单独初始化。 + * 新连接处理任何链路事件前须初始化成功,不要求在TCP建连成功后才调用。 + * 本模块未实现连接生命周期管理;旧事件隔离和状态内存安全复用由接入方负责。 + */ +/* 适用端:控制站与被控站,由role参数确定本端角色。 */ +int IEC104_Link_State_Init(IEC104_LinkState_t *state, + IEC104_LinkRole_t role) +{ + if (state == NULL || + (role != IEC104_ROLE_CONTROLLING && + role != IEC104_ROLE_CONTROLLED)) { + return 0; + } + + Initialize_State_Fields(state, role); + return 1; +} + +/** + * 功能:复位链路序号、S确认、启停和探测状态,清除待完成过程的算法记录。 + * 输入参数:state——待复位的链路状态;允许为NULL。 + * 输出参数:state——保留有效角色,序号及S快照清零,回到STOPPED/IDLE; + * S确认和TESTFR置IDLE;非法角色规范化为INVALID。 + * 返回值:无。 + * 说明:本函数只复位链路算法字段,不能用于致命故障后在原连接上继续通信。 + * 外部接入要求:调用方负责关闭故障连接,并防止旧事件提交到重新初始化后的状态; + * 对应连接管理、任务调度和定时器管理当前未实现,不属于本函数行为。 + * Reset会读取原role,非空state须具有确定的字段内容,不能传入未初始化的自动变量。 + */ +/* 适用端:控制站与被控站。 */ +void IEC104_Link_State_Reset(IEC104_LinkState_t *state) +{ + IEC104_LinkRole_t role; + + if (state == NULL) { + return; + } + + role = state->role; + if (role != IEC104_ROLE_CONTROLLING && + role != IEC104_ROLE_CONTROLLED) { + role = IEC104_ROLE_INVALID; + } + Initialize_State_Fields(state, role); +} + +/* I/S启停策略集中于下方判断函数;只查询当前状态,不缓存收发权限。 */ +/* 子站START受理后即可接收I帧;STOP受理即关闭,后到START重新开放。 */ +static int I_Frame_Receive_Allowed(const IEC104_LinkState_t *state) +{ + return state->dt_state == IEC104_DT_STARTED && + (state->role != IEC104_ROLE_CONTROLLED || state->dt_procedure == IEC104_DT_PROC_IDLE); +} + +/** + * 功能:判断当前是否允许业务层预留新的I帧。 + * 输入参数:state——已通过完整合法性检查的链路状态。 + * 输出参数:无,不修改state。 + * 返回值:1表示可预留新I帧,0表示当前方向尚未开放或正在停止。 + * 说明:控制站在STARTDT_ACT之后可提前发送命令,无须等待STARTDT_CON。 + * ACT待完成时新I帧必须排在ACT后,FIFO发送及完成顺序由调用方保证。 + * 被控站受理START即开放准备;发送存储须保证新I帧不越过已有CON。 + * 此处只检查启停门控,k窗口和序号环边界由Plan_I_Frame继续检查。 + */ +static int New_I_Send_Allowed(const IEC104_LinkState_t *state) +{ + if (state->dt_state == IEC104_DT_STARTED) { + return state->dt_procedure == IEC104_DT_PROC_IDLE; + } + + return state->role == IEC104_ROLE_CONTROLLING && + (state->dt_procedure == IEC104_DT_PROC_START_ACT_SEND_PENDING || + state->dt_procedure == IEC104_DT_PROC_START_CON_WAITING); +} + +/* 子站停止等待ACK时允许S;STOP_CON已生成后的STOPPED/IDLE拒绝S。 */ +static int S_Frame_Receive_Allowed(const IEC104_LinkState_t *state) +{ + return state->dt_state == IEC104_DT_STARTED || + (state->role == IEC104_ROLE_CONTROLLED && + state->dt_procedure == IEC104_DT_PROC_STOP_DRAINING); +} + +/* 子站的S和CON共享FIFO;无需再等待START_CON的网络完成。 */ +static int Dt_Allows_S_Ack(const IEC104_LinkState_t *state) +{ + return state->role != IEC104_ROLE_CONTROLLED || state->dt_state == IEC104_DT_STARTED || + state->dt_procedure == IEC104_DT_PROC_STOP_DRAINING; +} + +/** + * 功能:判断停止阶段是否要求普通S确认绕过w门限。 + * 输入参数:state——已通过完整合法性检查的链路状态。 + * 输出参数:无,不修改state。 + * 返回值:停止请求及排空的四个阶段为1,其余为0。 + * 说明:只决定确认紧迫性,不绕过在途S或无待确认数据检查。 + */ +static int Dt_Requires_Immediate_Ack(const IEC104_LinkState_t *state) +{ + return state->dt_procedure == IEC104_DT_PROC_STOP_REQUESTED || + state->dt_procedure == IEC104_DT_PROC_STOP_ACT_SEND_PENDING || + state->dt_procedure == IEC104_DT_PROC_STOP_CON_WAITING || + state->dt_procedure == IEC104_DT_PROC_STOP_DRAINING; +} + + + +/** + * 功能:检查I帧的发送序号和确认号,并更新链路状态。 + * 输入参数:state——当前链路状态;apci——已完成基础解析的I帧APCI。 + * 输出参数:成功时更新V(A)和V(R);待确认数量由acked_nr到V(R)推导。 + * 返回值:OK表示接收方向已经开放、序号合法并提交;FATAL表示接收方向未开放, + * 或状态、参数、帧类型、序号非法。 + */ +/* 适用端:控制站与被控站。 */ +IEC104_LinkResult_t IEC104_Link_Handle_I_Frame( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci) +{ + uint16_t next_receive_number; + + /* 先完成所有检查;合法状态的V(R)为15位,N(S)==V(R)也保证N(S)未越界。 */ + if (!IEC104_Link_State_Valid_Internal(state) || apci == NULL || + !I_Frame_Receive_Allowed(state) || + apci->frame_type != IEC104_FRAME_TYPE_I || + apci->send_number != state->vr || + !Acknowledge_Number_Valid(state, apci->receive_number)) { + return IEC104_LINK_RESULT_FATAL; + } + + next_receive_number = Number_Increment(state->vr); + /* 未确认区间不得占满15位序号环,否则acked_nr==V(R)会被误判为没有积压。 */ + if (next_receive_number == state->acked_nr) { + return IEC104_LINK_RESULT_FATAL; + } + + /* I帧中的N(R)确认本端发送数据,合法N(S)使本端的期望接收序号V(R)加1。 */ + state->va = apci->receive_number; + state->vr = next_receive_number; + + return IEC104_LINK_RESULT_OK; +} + +/** + * 功能:检查S帧的确认号并更新已确认发送序号。 + * 输入参数:state——当前链路状态;apci——已完成基础解析的S帧APCI。 + * 输出参数:成功时更新V(A)。 + * 返回值:OK表示接收阶段和确认号合法并提交;其他情况返回FATAL且不修改状态。 + * 说明:STARTED下允许接收S;被控站STOPPED下仅STOP_DRAINING允许,以完成停止排空。 + * 被控站START受理即STARTED,无须等待START_CON完成;STOPPED/IDLE拒绝S。 + */ +/* 适用端:控制站与被控站。 */ +IEC104_LinkResult_t IEC104_Link_Handle_S_Frame( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci) +{ + if (!IEC104_Link_State_Valid_Internal(state) || apci == NULL || + apci->frame_type != IEC104_FRAME_TYPE_S || + !S_Frame_Receive_Allowed(state) || + !Acknowledge_Number_Valid(state, apci->receive_number)) { + return IEC104_LINK_RESULT_FATAL; + } + + /* S帧不携带用户数据,只用N(R)推进本端的发送确认位置。 */ + state->va = apci->receive_number; + return IEC104_LINK_RESULT_OK; +} + +/** + * 功能:只根据链路状态规划下一帧I帧的序号快照,不编码报文也不修改状态。 + * 输入参数:state——当前链路状态;k——发送窗口上限; + * tx——调用方提供的非NULL临时快照。 + * 输出参数:仅READY时tx写入当前V(S)、V(R),frame_length暂置0,等待编码层填写。 + * 返回值:READY表示可生成;SKIPPED表示门控或窗口阻止发送;ERROR表示参数/状态非法。 + * 说明:本函数是只读规划阶段。调用方必须在同一串行临界区内完成编码,并且 + * 只有编码成功后才能把tx提交给发送队列并推进V(S)。 + * Can_Prepare_I_Frame与Prepare_I_Frame共用本函数;tx均指向栈上临时对象,故此处不再判空。 + */ +static IEC104_PlanResult_t Plan_I_Frame(const IEC104_LinkState_t *state, + uint16_t k, + IEC104_IFrameTx_t *tx) +{ + uint16_t next_send_number; + uint16_t outstanding_count; + + if (!IEC104_Link_State_Valid_Internal(state) || + k == 0U || k > IEC104_SEQUENCE_MASK) { + return IEC104_PLAN_ERROR; + } + if (!New_I_Send_Allowed(state)) { + return IEC104_PLAN_SKIPPED; + } + + outstanding_count = Number_Distance(state->va, state->vs); + if (outstanding_count >= k) { + return IEC104_PLAN_SKIPPED; + } + + next_send_number = Number_Increment(state->vs); + /* 未完成区间包含在未确认区间内;保留显式检查,避免完成边界的回绕歧义。 */ + if (next_send_number == state->sent_vs) { + return IEC104_PLAN_SKIPPED; + } + + tx->send_number = state->vs; + tx->receive_number = state->vr; + tx->frame_length = 0U; + return IEC104_PLAN_READY; +} + +/* 只读复用实际构帧的启停、k窗口及序号边界检查,不编码、不预留N(S)。 */ +int IEC104_Link_Can_Prepare_I_Frame(const IEC104_LinkState_t *state, uint16_t k) +{ + IEC104_IFrameTx_t planned_tx; + IEC104_PlanResult_t planned = Plan_I_Frame(state, k, &planned_tx); + return planned == IEC104_PLAN_READY ? 1 : (planned == IEC104_PLAN_SKIPPED ? 0 : -1); +} + +/** + * 功能:检查启停状态和k窗口,构造I帧并立即预留其N(S)。 + * 输入参数:state——链路状态;k——发送窗口;asdu/asdu_len——完整ASDU; + * frame/capacity——输出缓冲及容量;tx——发送队列保存的构帧快照。 + * 输出参数:成功时写入frame和tx,并推进V(S)。 + * 返回值:成功返回完整APDU长度;0表示当前不允许新I帧或窗口已满; + * -1表示参数或状态非法。 + * 说明:成功后N(S)不可撤销;报文须进入严格FIFO队列,发送失败由上层断连。 + */ +/* 适用端:控制站与被控站。 */ +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) +{ + IEC104_IFrameTx_t planned_tx; + IEC104_PlanResult_t planned; + int length; + + if (asdu == NULL || + asdu_len < IEC104_ASDU_HEADER_LEN || + asdu_len > IEC104_MAX_ASDU_LEN || + frame == NULL || + capacity < (uint16_t)(IEC104_APCI_LEN + asdu_len) || + tx == NULL) { + return -1; + } + + planned = Plan_I_Frame(state, k, &planned_tx); + if (planned != IEC104_PLAN_READY) { + return planned == IEC104_PLAN_SKIPPED ? 0 : -1; + } + + length = IEC104_Build_Iframe(frame, + planned_tx.send_number, + planned_tx.receive_number, + asdu, (int)asdu_len); + if (length < 0) { + return -1; + } + + /* 编码成功后才一次性提交状态和发送队列快照。 */ + planned_tx.frame_length = (uint16_t)length; + *tx = planned_tx; + state->vs = Number_Increment(planned_tx.send_number); + return length; +} + +/** + * 功能:提交一帧由Prepare_I_Frame生成的I帧最终本地发送结果。 + * 输入参数:state——链路状态;tx——构帧时保存的I帧快照; + * sent_len——最终累计成功发送的字节数,等于快照长度才算成功;发送失败也可传-1。 + * 输出参数:完整发送且快照合法时推进acked_nr和sent_vs;失败时不修改状态。 + * 返回值:OK表示完整发送并提交;FATAL表示参数、状态、长度、顺序或快照非法。 + * 说明:Send限定为本地发送提交;返回OK不表示对端已经确认本帧。 + * V(S)已在Prepare阶段推进;本接口按FIFO推进发送完成边界,不再次推进V(S)。 + */ +/* 适用端:控制站与被控站。 */ +IEC104_LinkResult_t IEC104_Link_Complete_I_Send( + IEC104_LinkState_t *state, const IEC104_IFrameTx_t *tx, int sent_len) +{ + IEC104_LinkResult_t result; + + if (!IEC104_Link_State_Valid_Internal(state) || tx == NULL || + tx->send_number > IEC104_SEQUENCE_MASK || + tx->receive_number > IEC104_SEQUENCE_MASK || + tx->frame_length < + (uint16_t)(IEC104_APCI_LEN + IEC104_ASDU_HEADER_LEN) || + tx->frame_length > + (uint16_t)(IEC104_APCI_LEN + IEC104_MAX_ASDU_LEN) || + sent_len != (int)tx->frame_length) { + return IEC104_LINK_RESULT_FATAL; + } + + /* 没有待完成帧或N(S)不匹配时拒绝,避免重复/乱序回调提交到别的帧。 */ + if (state->sent_vs == state->vs || + tx->send_number != state->sent_vs) { + return IEC104_LINK_RESULT_FATAL; + } + + result = Commit_Sent_NR(state, tx->receive_number); + if (result != IEC104_LINK_RESULT_OK) { + return result; + } + state->sent_vs = Number_Increment(tx->send_number); + return IEC104_LINK_RESULT_OK; +} + +/** + * 功能:判断最早已完成发送且尚未被对端确认的I帧是否达到t1超时。 + * 输入参数:state——链路状态;elapsed_ms——该帧实际完成发送后经过的毫秒数; + * t1_ms——t1配置值。 + * 输出参数:无,不修改state。 + * 返回值:1表示已经超时;0表示无已完成未确认帧或尚未超时;-1表示参数非法。 + * 说明:只预留未Complete的帧不参与t1;时间戳和队列仍由调用方维护。 + */ +/* 适用端:控制站与被控站。 */ +int IEC104_Link_T1_I_Ack_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 (Sent_Unacknowledged_I_Count(state) == 0U) { + return 0; + } + + return elapsed_ms >= t1_ms ? 1 : 0; +} + +/** + * 功能:只根据链路状态规划S确认的N(R),不编码报文也不修改状态。 + * 输入参数:state——当前链路状态;w——接收确认门限; + * immediate——非0表示绕过w门限立即确认; + * receive_number——用于接收待编码N(R)的输出地址。 + * 输出参数:仅READY时写入当前V(R)。 + * 返回值:READY表示应生成;SKIPPED表示当前无需或已有S帧在途。 + * 前置条件:state已通过完整校验,w在1..32767内,receive_number为有效输出地址。 + * 说明:STOPDT排空半态会自动绕过w门限。实际S帧编码成功后,构帧步骤才把 + * receive_number保存到s_ack_nr并进入SEND_PENDING。 + */ +static IEC104_PlanResult_t Plan_S_Ack(const IEC104_LinkState_t *state, + uint16_t w, int immediate, + uint16_t *receive_number) +{ + uint16_t unconfirmed_count; + + if (!Dt_Allows_S_Ack(state)) { + return IEC104_PLAN_SKIPPED; + } + if (state->s_ack_state == IEC104_S_ACK_SEND_PENDING) { + return IEC104_PLAN_SKIPPED; + } + + unconfirmed_count = IEC104_Link_Unconfirmed_Rx_Internal(state); + if (unconfirmed_count == 0U) { + return IEC104_PLAN_SKIPPED; + } + + if (!immediate && unconfirmed_count < w && + !Dt_Requires_Immediate_Ack(state)) { + return IEC104_PLAN_SKIPPED; + } + + *receive_number = state->vr; + return IEC104_PLAN_READY; +} + +/** + * 功能:只读查询普通S确认需求,供连接层在取得控制槽前决定是否调用发送准备接口。 + * 输入参数:state——当前链路;w——合法接收门限;immediate——非0立即确认。 + * 返回值:1需要且允许准备,0当前无需/不能准备,-1参数或状态非法。 + * 说明:直接复用构帧使用的Plan_S_Ack;查询不消耗发送编号、不改变S快照或acked_nr。 + */ +int IEC104_Link_Can_Prepare_S_Ack(const IEC104_LinkState_t *state, uint16_t w, int immediate) +{ + uint16_t receive_number; + if (!IEC104_Link_State_Valid_Internal(state) || w == 0U || w > IEC104_SEQUENCE_MASK) { + return -1; + } + return Plan_S_Ack(state, w, immediate, &receive_number) == IEC104_PLAN_READY; +} + +/** + * 功能:按已规划的N(R)编码S帧,成功后提交公共S确认状态。 + * 输入参数:state——链路状态;response——输出缓冲;receive_number——规划的N(R)。 + * 输出参数:成功时写入6字节S帧,将N(R)保存到s_ack_nr并置SEND_PENDING。 + * 前置条件:state已通过完整校验且无S在途;response非NULL且至少6字节; + * receive_number为本次选定的有效15位确认号。 + * 说明:普通S确认和STOPDT共用此步骤;使用传入快照,不重新规划或读取V(R)。 + * 本步骤不推进acked_nr,须等Complete_S_Ack_Send确认发送成功后提交。 + * 返回值:6表示编码并提交成功;-1表示编码失败,此时不提交状态。 + * 调用关系:本文件的普通确认/t2以及Master、Slave的停止准备均调用本函数, + * 所以不能直接设为static;是否应发送、容量和角色均由调用入口检查。 + */ +int IEC104_Link_Prepare_S_Ack_Internal(IEC104_LinkState_t *state, + uint8_t *response, uint16_t receive_number) +{ + int length = IEC104_Build_Sframe(response, receive_number); + + if (length != IEC104_APCI_LEN) { + return -1; + } + + /* 编码成功后提交与报文字节完全一致的N(R)快照。 */ + state->s_ack_nr = receive_number; + state->s_ack_state = IEC104_S_ACK_SEND_PENDING; + return length; +} + +/** + * 功能:普通S确认与t2共用的规划、编码和提交路径。 + * 输入参数:state——链路状态;w——确认门限;immediate——非0立即确认; + * response——输出缓冲。 + * 输出参数:生成时写入S帧并记录其N(R)快照和在途状态;未生成时保持原值。 + * 前置条件:state和w已校验,response至少有IEC104_APCI_LEN字节空间。 + * 返回值:6表示生成,0表示当前不生成,-1表示编码失败。 + */ +static int Prepare_S_Ack_Validated(IEC104_LinkState_t *state, + uint16_t w, int immediate, + uint8_t *response) +{ + uint16_t receive_number; + IEC104_PlanResult_t planned = Plan_S_Ack(state, w, immediate, &receive_number); + + if (planned != IEC104_PLAN_READY) { + return 0; + } + return IEC104_Link_Prepare_S_Ack_Internal(state, response, receive_number); +} + +/** + * 功能:按w门限、显式立即确认或STOPDT排空要求生成普通S帧并保存N(R)快照。 + * 输入参数:state——链路状态;w——接收确认门限;immediate——非0跳过w门限,仍受启停及S在途限制; + * response——输出缓冲;capacity——至少6字节。 + * 输出参数:成功时写入S帧并记录SEND_PENDING及构帧时V(R)。 + * 返回值:6表示生成;0表示当前无需或不能由普通流程生成;-1表示参数/状态非法。 + */ +/* 适用端:控制站与被控站。 */ +int IEC104_Link_Prepare_S_Ack(IEC104_LinkState_t *state, + uint16_t w, int immediate, + uint8_t *response, uint16_t capacity) +{ + if (!IEC104_Link_State_Valid_Internal(state) || w == 0U || w > IEC104_SEQUENCE_MASK || + response == NULL || capacity < IEC104_APCI_LEN) { + return -1; + } + + return Prepare_S_Ack_Validated(state, w, immediate, response); +} + +/** + * 功能:提交公共S确认帧的最终发送结果,普通确认与两端停止流程共用。 + * 输入参数:state——链路状态;sent_len——最终累计发送字节数。 + * 输出参数:成功时按快照推进acked_nr并回IDLE;失败或快照失效时不推进确认。 + * 返回值:OK表示发送成功并提交;IGNORED表示没有S帧等待完成; + * FATAL表示状态非法、发送失败或快照失效。 + * 说明:状态非法直接返回且不清理;状态合法但无S在途时先返回IGNORED, + * 不检查sent_len。有S在途时,发送失败或快照失效均清除S状态和快照。 + * 成功同样清除S状态/快照,所有结果均保留当前启停过程。 + * 普通确认及两端STOPDT中的S帧均由本接口完成。本接口不携带帧身份, + * 调用方须按原发送节点恰好分派一次。 + */ +/* 适用端:控制站与被控站。 */ +IEC104_LinkResult_t IEC104_Link_Complete_S_Ack_Send( + IEC104_LinkState_t *state, int sent_len) +{ + IEC104_LinkResult_t result; + + if (!IEC104_Link_State_Valid_Internal(state)) { + return IEC104_LINK_RESULT_FATAL; + } + if (state->s_ack_state != IEC104_S_ACK_SEND_PENDING) { + return IEC104_LINK_RESULT_IGNORED; + } + + if (sent_len != IEC104_APCI_LEN) { + state->s_ack_state = IEC104_S_ACK_IDLE; + state->s_ack_nr = 0U; + return IEC104_LINK_RESULT_FATAL; + } + + result = Commit_Sent_NR(state, state->s_ack_nr); + state->s_ack_state = IEC104_S_ACK_IDLE; + state->s_ack_nr = 0U; + return result; +} + +/** + * 功能:在最早已接收但本端尚未确认的I帧达到t2等待时间后生成S确认帧。 + * 输入参数:state——链路状态;elapsed_ms——已经等待的毫秒数; + * t2_ms——t2配置值;response——输出缓冲;capacity——至少6字节。 + * 输出参数:到期且可生成时写入S帧,并保存普通S帧的N(R)快照。 + * 返回值:6表示生成;0表示尚不需要或当前已有流程负责;-1表示参数/状态非法。 + */ +/* 适用端:控制站与被控站。 */ +int IEC104_Link_Prepare_T2_Ack(IEC104_LinkState_t *state, + uint32_t elapsed_ms, uint32_t t2_ms, + uint8_t *response, uint16_t capacity) +{ + if (!IEC104_Link_State_Valid_Internal(state) || response == NULL || + capacity < IEC104_APCI_LEN || t2_ms == 0U) { + return -1; + } + + if (elapsed_ms < t2_ms) { + return 0; + } + + /* 已校验输入;到期后由共用规划判断待确认数据和在途S帧。 */ + return Prepare_S_Ack_Validated(state, 1U, 1, response); +} + +/** + * 功能:检查链路状态自洽且本端固定角色与调用接口要求一致。 + * 输入参数:state——待检查的链路状态对象;role——接口要求的本端角色。 + * 输出参数:无,不修改state。 + * 返回值:状态自洽且角色匹配返回1,否则返回0。 + * 调用关系:Master/Slave角色专用公开入口使用;先调用State_Valid再比较角色, + * 短路求值保证state为NULL时不会解引用。末级角色校验不得反调此函数。 + */ +int IEC104_Link_Role_Valid_Internal(const IEC104_LinkState_t *state, + IEC104_LinkRole_t role) +{ + return IEC104_Link_State_Valid_Internal(state) && state->role == role; +} + +/** + * 功能:处理TESTFR_ACT,复用U帧构造接口生成TESTFR_CON应答。 + * 输入参数:apci——已通过APCI解析的TESTFR_ACT;response——输出缓冲; + * capacity——缓冲实际容量,至少6字节。 + * 输出参数:response——写入68 04 83 00 00 00。 + * 返回值:OK表示生成固定6字节应答;FATAL表示参数或报文非法。 + * 说明:不读写链路状态,不执行网络发送,不处理主动探测或超时。 + * 此应答没有对应Complete接口;发送队列负责发送结果和失败断连,不能 + * 把TESTFR_CON发送完成交给处理本端主动ACT的Complete_Testfr_Act_Send。 + */ +/* 适用端:控制站与被控站。 */ +IEC104_LinkResult_t IEC104_Link_Handle_Testfr_Act( + const IEC104_ApciInfo_t *apci, uint8_t *response, uint16_t capacity) +{ + if (apci == NULL || response == NULL || capacity < IEC104_APCI_LEN || + apci->frame_type != IEC104_FRAME_TYPE_U || + apci->u_function != IEC104_U_TESTFR_ACT) { + return IEC104_LINK_RESULT_FATAL; + } + + /* 缓冲已验证且功能码固定,编码必定生成6字节;(void)表示有意忽略返回值。 */ + (void)IEC104_Build_Uframe(response, IEC104_U_TESTFR_CON); + return IEC104_LINK_RESULT_OK; +} + +/* 已通过完整状态检查;查询与构帧共用唯一的主动探测准入条件。 */ +static int Testfr_Act_Allowed(const IEC104_LinkState_t *state) +{ + return state->testfr_state == IEC104_TESTFR_IDLE; +} + +/** + * 功能:查询当前是否允许新一轮主动TESTFR,不读取时钟或改变状态。 + * 输入参数:state——待检查链路;调用方另外判断是否已满足t3空闲时长。 + * 返回值:1可准备,0当前已有探测,-1参数或完整状态非法。 + * 说明:普通主动探测和t3构帧均复用Testfr_Act_Allowed,不在连接层另判枚举值。 + */ +int IEC104_Link_Can_Prepare_Testfr_Act(const IEC104_LinkState_t *state) +{ + if (!IEC104_Link_State_Valid_Internal(state)) { + return -1; + } + return Testfr_Act_Allowed(state); +} + +/** + * 功能:主动TESTFR与t3共用的状态检查、编码和提交路径。 + * 输入参数:state——链路状态;request——输出缓冲。 + * 输出参数:生成时写入6字节TESTFR_ACT,并进入TESTFR_ACT_SEND_PENDING。 + * 前置条件:state已校验,request至少有IEC104_APCI_LEN字节空间。 + * 返回值:6表示生成,0表示已有探测。 + * 说明:与只读查询共用Testfr_Act_Allowed,真正发送完成后才开始等待CON; + * 本函数不判断链路空闲时间,参数校验和t3时间比较由公开入口负责。 + */ +static int Prepare_Testfr_Act_Validated(IEC104_LinkState_t *state, + uint8_t *request) +{ + if (!Testfr_Act_Allowed(state)) { + return 0; + } + + /* + * 缓冲已验证且TESTFR_ACT为合法常量,编码器必定写入6字节并返回6。 + * (void)只显式丢弃这次调用的返回值,函数仍会执行编码并写入request。 + */ + (void)IEC104_Build_Uframe(request, IEC104_U_TESTFR_ACT); + state->testfr_state = IEC104_TESTFR_ACT_SEND_PENDING; + return IEC104_APCI_LEN; +} + +/** + * 功能:生成主动TESTFR_ACT请求,并记录等待发送完成的状态。 + * 输入参数:state——链路状态;request——输出缓冲;capacity——实际容量,至少6字节。 + * 输出参数:request——写入68 04 43 00 00 00;state——探测转为ACT_SEND_PENDING。 + * 返回值:6表示生成请求;0表示已有探测进行中;-1表示参数或链路状态非法。 + * 说明:返回0或-1时不修改状态和缓冲;不执行发送,不修改启停、序号及确认位置。 + * 返回6后发送层必须保证恰好提交一次Complete;发送超时也以失败长度提交。 + */ +/* 适用端:控制站与被控站。 */ +int IEC104_Link_Prepare_Testfr_Act(IEC104_LinkState_t *state, + uint8_t *request, uint16_t capacity) +{ + if (!IEC104_Link_State_Valid_Internal(state) || request == NULL || capacity < IEC104_APCI_LEN) { + return -1; + } + + return Prepare_Testfr_Act_Validated(state, request); +} + +/** + * 功能:在链路空闲达到t3后生成主动TESTFR_ACT请求。 + * 输入参数:state——链路状态;idle_elapsed_ms——空闲毫秒数; + * t3_ms——t3配置值;request——输出缓冲;capacity——至少6字节。 + * 输出参数:到期且允许生成时写入TESTFR_ACT,并复用主动探测发送状态。 + * 返回值:6表示生成;0表示尚未到期或已有探测;-1表示参数或链路状态非法。 + * 说明:先校验完整状态和参数,再比较t3时间;返回6只表示生成报文,尚未发送。 + * 连接层统一定义有效链路活动,并更新t3的单调计时起点;本函数不保存时刻。 + * t3活动更新不得重置正在等待TESTFR_CON的t1起点。 + */ +/* 适用端:控制站与被控站。 */ +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) +{ + if (!IEC104_Link_State_Valid_Internal(state) || request == NULL || + capacity < IEC104_APCI_LEN || t3_ms == 0U) { + return -1; + } + + if (idle_elapsed_ms < t3_ms) { + return 0; + } + + /* 已校验输入;t3到期后直接复用主动探测准备路径。 */ + return Prepare_Testfr_Act_Validated(state, request); +} + +/** + * 功能:提交TESTFR_ACT最终发送结果,成功后开始等待TESTFR_CON。 + * 输入参数:state——链路状态;sent_len——最终累计成功发送字节数,恰好6表示成功。 + * 输出参数:有匹配ACT时,完整发送成功转CON_PENDING;发送失败回IDLE并由上层断连。 + * 返回值:OK表示完整发送并提交;IGNORED表示没有待完成的ACT; + * FATAL表示状态非法或发送失败。 + * 说明:部分写入仍在继续时不要调用;状态合法但无匹配ACT时先返回IGNORED,不检查长度。 + * 调用方须先提交ACT完整发送结果,再分派对端CON;不缓存提前CON。 + * 返回OK后,由连接层以本次ACT实际完整发送时刻为等待CON的t1起点, + * 不能用延迟处理Complete时的当前时刻代替;本函数不操作外部定时器。 + */ +/* 适用端:控制站与被控站。 */ +IEC104_LinkResult_t IEC104_Link_Complete_Testfr_Act_Send( + IEC104_LinkState_t *state, int sent_len) +{ + if (!IEC104_Link_State_Valid_Internal(state)) { + return IEC104_LINK_RESULT_FATAL; + } + + if (state->testfr_state != IEC104_TESTFR_ACT_SEND_PENDING) { + return IEC104_LINK_RESULT_IGNORED; + } + + if (sent_len != IEC104_APCI_LEN) { + state->testfr_state = IEC104_TESTFR_IDLE; + return IEC104_LINK_RESULT_FATAL; + } + + /* 完整发送成功只开始等待对端确认;收到CON后才结束本轮探测。 */ + state->testfr_state = IEC104_TESTFR_CON_PENDING; + return IEC104_LINK_RESULT_OK; +} + +/** + * 功能:接受正在等待的TESTFR_CON,结束本端主动探测。 + * 输入参数:state——链路状态;apci——已通过APCI解析的TESTFR_CON。 + * 输出参数:仅CON_PENDING时回IDLE;非OK时不修改状态。 + * 返回值:OK表示接受确认;IGNORED表示当前未等待CON; + * FATAL表示参数、报文或链路状态非法。 + * 说明:输入校验先于等待状态检查;ACT_SEND_PENDING时返回IGNORED且不缓存CON。 + * 调用方须先提交ACT完整发送结果,再处理CON;本接口不修改启停状态、 + * 链路序号及确认位置,也不生成应答报文。返回OK后由连接层取消本轮等待计时, + * 本接口不操作外部定时器。 + */ +/* 适用端:控制站与被控站。 */ +IEC104_LinkResult_t IEC104_Link_Handle_Testfr_Con( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci) +{ + if (!IEC104_Link_State_Valid_Internal(state) || apci == NULL || + apci->frame_type != IEC104_FRAME_TYPE_U || + apci->u_function != IEC104_U_TESTFR_CON) { + return IEC104_LINK_RESULT_FATAL; + } + + if (state->testfr_state != IEC104_TESTFR_CON_PENDING) { + return IEC104_LINK_RESULT_IGNORED; + } + + state->testfr_state = IEC104_TESTFR_IDLE; + return IEC104_LINK_RESULT_OK; +} + +/** + * 功能:判断主动TESTFR流程等待TESTFR_CON是否达到t1超时。 + * 输入参数:state——链路状态;elapsed_ms——距当前探测ACT实际完整发送的毫秒数; + * t1_ms——t1配置值,必须大于0。 + * 输出参数:无,不修改state。 + * 返回值:1表示已经超时;0表示未等待CON或尚未超时;-1表示参数或链路状态非法。 + * 说明:先校验状态及t1_ms;合法时仅对CON_PENDING判定t1,ACT_SEND_PENDING返回0。 + * ACT_SEND_PENDING表示本地发送结果尚未提交,发送层须限制排队和发送等待时间。 + * 连接层定时器只负责唤醒;回调须校验连接代次,并在连接串行调度中按当前 + * 有效探测的起点重新计算elapsed_ms,不能沿用同一连接上旧探测的时间差。 + * 本函数不识别探测轮次,也不操作定时器或连接;只有返回1才按t1超时断连。 + */ +/* 适用端:控制站与被控站。 */ +int IEC104_Link_T1_Testfr_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->testfr_state != IEC104_TESTFR_CON_PENDING) { + return 0; + } + + return elapsed_ms >= t1_ms ? 1 : 0; +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link.h new file mode 100644 index 00000000..1322b047 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link.h @@ -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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Common.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Common.h new file mode 100644 index 00000000..c8d54fb4 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Common.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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Internal.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Internal.h new file mode 100644 index 00000000..1052f83e --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Internal.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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Master_StartStop.c b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Master_StartStop.c new file mode 100644 index 00000000..60dfeb8a --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Master_StartStop.c @@ -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 + +/** + * 功能:生成主动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; +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Master_StartStop.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Master_StartStop.h new file mode 100644 index 00000000..417da649 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Master_StartStop.h @@ -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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Slave_StartStop.c b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Slave_StartStop.c new file mode 100644 index 00000000..7ada3396 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Slave_StartStop.c @@ -0,0 +1,111 @@ +/** + * 子站启停的受理语义:START立即激活,STOP立即停用并等待确认。 + * CON是发送任务,完成时不回写启停状态;后到START可结束尚未生成CON的停止等待。 + * 已生成的CON始终按FIFO完成;不暂存相反方向ACT,不重置序号,所有调用串行。 + */ +#include "IEC104_Link_Slave_StartStop.h" +#include "IEC104_Link_Internal.h" + +#include + +/* 调用方先预留发送空间;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; +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Slave_StartStop.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Slave_StartStop.h new file mode 100644 index 00000000..124faf1b --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Link_Slave_StartStop.h @@ -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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Log.c b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Log.c new file mode 100644 index 00000000..e06235b4 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Log.c @@ -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 +#include +#include + +#ifdef _WIN32 +/* 减少 引入的非必要声明,并避免旧版 Winsock 头文件干扰。 */ +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# include +#else +/* 非 Windows 平台使用 POSIX 实时时钟和线程安全的本地时间转换接口。 */ +# include +#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); + } +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Log.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Log.h new file mode 100644 index 00000000..a7e263c2 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Log.h @@ -0,0 +1,95 @@ +/** + * IEC104_Log.h —— IEC 60870-5-104 通用日志接口。 + * + * 本模块独立于报文解析和链路状态,只负责: + * 1. 按最低日志级别过滤消息; + * 2. 为消息添加本地时间和级别前缀; + * 3. 将完整消息交给用户回调或默认标准流。 + * + * 公共名称全部使用 IEC104_ 前缀,可与 POSIX 同时使用。 + * 构建与测试命令见 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 的 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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Net.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Net.h new file mode 100644 index 00000000..0ea6c4e6 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Net.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 +#include + +#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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Net_Windows.c b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Net_Windows.c new file mode 100644 index 00000000..ee1ef742 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Net_Windows.c @@ -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 +#include +#include +#include +#include + +/* 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(); +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Receiver.c b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Receiver.c new file mode 100644 index 00000000..6d0a7a49 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Receiver.c @@ -0,0 +1,299 @@ +/** + * 每连接独立的IEC-104流式接收器。 + * 每个实例只保留一帧;完整帧经APCI及ASDU最低长度检查后暂停输入,显式释放后再处理剩余字节。 + * Codec负责报文格式,Link/SendManager负责序号、ACK和启停;本模块不改变链路状态。 + */ +#include "IEC104_Receiver.h" +#include "IEC60870_Codec.h" + +#include +#include + +/** + * 功能:丢弃当前候选帧,将接收器恢复到等待帧头的阶段。 + * 输入参数: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; +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Receiver.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Receiver.h new file mode 100644 index 00000000..970fcb76 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Receiver.h @@ -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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_SendBuffer.c b/IEC60870-Slave/.build/command-v1/before/104/IEC104_SendBuffer.c new file mode 100644 index 00000000..b8fbca69 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_SendBuffer.c @@ -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 +#include + +/** + * 功能:将控制队列的逻辑偏移换算为数组下标。 + * 输入参数: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)); + } +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_SendBuffer.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_SendBuffer.h new file mode 100644 index 00000000..0ff04f43 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_SendBuffer.h @@ -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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_SendManager.c b/IEC60870-Slave/.build/command-v1/before/104/IEC104_SendManager.c new file mode 100644 index 00000000..12e9a0e4 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_SendManager.c @@ -0,0 +1,1331 @@ +/** + * 子站串行发送管理:发送流程、子站启停封装和初始化入口集中在本文件。 + * 本模块由Connection内部调用;业务通过TcpServer提交ASDU并读取事件副本,不直接操作Manager。 + * 完成分派按原帧类型直接调用公共或子站Link接口,不经过角色回调。 + * S/TESTFR与子站启停构帧入口复用控制槽预留、取消和提交,状态机仍由Link维护。 + * Init只绑定CONTROLLED链路;启停流程接收ACT并发送CON。 + */ +#include "IEC104_SendManager.h" +#include "IEC104_Link_Slave_StartStop.h" + +#include +#include + +/* ==================== 公共管理:调度、进度、确认与回收 ==================== */ + +/** + * 功能:计算15位序号沿发送方向的循环距离。 + * 输入参数:from——起始序号,范围0..32767。 + * to——目标序号,范围0..32767。 + * 输出参数:无。 + * 返回值:0..32767范围内的模32768距离。 + * 说明:调用方须先保证区间受窗口或容量约束;不能直接用序号大小判断先后。 + */ +static uint16_t Sequence_Distance(uint16_t from, uint16_t to) +{ + return (uint16_t)(((uint32_t)to + 32768U - from) & IEC104_SEQUENCE_MASK); +} + +/** + * 功能:将业务记录相对队首的逻辑偏移换算为数组下标。 + * 输入参数:manager——存储边界已验证且record_capacity非零的发送管理器。 + * offset——相对记录队首的槽数偏移,不是字节数,可定位现有记录或下一个空槽。 + * 输出参数:无。 + * 返回值:0..record_capacity-1范围内的物理下标。 + * 说明:用32位加法避免索引相加溢出;本函数不验证该槽是否已有有效记录。 + */ +static uint16_t Record_Index_From_Offset(const IEC104_SendManager_t *manager, uint16_t offset) +{ + return (uint16_t)(((uint32_t)manager->record_head_index + offset) % manager->record_capacity); +} + +/** + * 功能:检查发送管理器自身的存储边界、绑定指针和配置。 + * 输入参数:manager——待检查的发送管理器,允许NULL。 + * 输出参数:无。 + * 返回值:控制结构合法返回1,否则返回0。 + * 说明:不要求Link及两个队列仍可运行,也不扫描业务记录。 + * 故障回收仍须读取帧身份、NORMAL来源编号和ACK事实,不能被故障后的会话状态阻断。 + */ +static int Manager_Storage_Valid(const IEC104_SendManager_t *manager) +{ + return manager != NULL && manager->is_initialized == 1U && manager->asdu_queue != NULL && + manager->send_buffer != NULL && manager->link_state != NULL && manager->records != NULL && + manager->record_capacity != 0U && manager->record_capacity <= IEC104_SEQUENCE_MASK && + manager->record_head_index < manager->record_capacity && + manager->record_count <= manager->record_capacity && manager->connection_generation != 0U && + manager->high_unacked_count <= manager->record_count && + manager->send_window_k != 0U && manager->send_window_k <= IEC104_SEQUENCE_MASK && + manager->next_record_ns <= IEC104_SEQUENCE_MASK && + manager->high_priority_burst_count <= manager->high_priority_burst_limit; +} + +/** + * 功能:检查业务记录的身份、序号快照、完成标志及有效发送时刻。 + * 输入参数:manager——已通过Manager_Storage_Valid检查的发送管理器。 + * record——指向已登记记录的非NULL指针。 + * offset——该记录相对队首的偏移,调用方保证小于record_count。 + * 输出参数:无。 + * 返回值:记录符合当前位置的约束返回1,否则返回0。 + * 说明:队列发送及直接应答共用Prepare_I_Asdu连续登记,记录只从队首释放,因此首条N(S)=next_record_ns-record_count。 + * 记录容量小于序号环,可按offset推导唯一N(S)。 + * 本地完成和对端ACK分别记录;已确认的业务记录必须已经提交本地完成。 + */ +static int I_Frame_Record_Valid(const IEC104_SendManager_t *manager, + const IEC104_IFrameSendRecordStorage_t *record, uint16_t offset) +{ + uint16_t expected = (uint16_t)(((uint32_t)manager->next_record_ns + 32768U - + manager->record_count + offset) & IEC104_SEQUENCE_MASK); + return record->frame_serial != 0U && + ((record->asdu_priority == IEC104_ASDU_PRIORITY_HIGH && record->asdu_entry_serial == 0U) || + (record->asdu_priority == IEC104_ASDU_PRIORITY_NORMAL && record->asdu_entry_serial != 0U)) && + record->i_frame_snapshot.send_number == expected && record->i_frame_snapshot.receive_number <= IEC104_SEQUENCE_MASK && + record->i_frame_snapshot.frame_length >= IEC104_APCI_LEN + IEC104_ASDU_HEADER_LEN && + record->i_frame_snapshot.frame_length <= IEC104_MAX_APDU_LEN && + record->local_send_completed <= 1U && record->peer_acknowledged <= 1U && + (record->peer_acknowledged == 0U || record->local_send_completed != 0U) && + (record->local_send_completed == 0U || record->written_at_ms >= record->queued_at_ms); +} + +/** + * 功能:检查当前连接的Link序号、当前I帧、保留ASDU和记录数量是否一致。 + * 输入参数:manager——待检查对象,允许NULL;connection_generation——本次事件的代次。 + * 输出参数:无,通过公开查询读取各模块数值,不修改它们的控制字段。 + * 返回值:运行状态自洽返回1,否则0;不证明TCP仍然可用。 + * 说明:Link独占协议边界;Buffer报告至多一个当前I帧;HIGH未确认数量由Manager记录维护。 + * 本模块只服务CONTROLLED角色,绑定后的角色变化属于本地状态非法。 + * HIGH未确认记录数加NORMAL保留及覆盖待确认条目数,应等于Link已编号未ACK的I帧数。 + * 未消费的已ACK回执仍占记录,所以record_count可以大于未ACK帧数。 + * 公开入口不允许遗留预留;整个跨模块事务完成后才检查,不能检查提交中间态。 + */ +static int Manager_Session_Valid(const IEC104_SendManager_t *manager, uint64_t connection_generation) +{ + IEC104_IFrameSendProgress_t link_progress; + IEC104_LinkStatus_t link_status; + IEC104_AsduQueueStatus_t asdu_status; + IEC104_SendBufferStatus_t buffer_status; + uint16_t pending, outstanding; + + if (!Manager_Storage_Valid(manager) || connection_generation != manager->connection_generation || + IEC104_Link_Get_Status(manager->link_state, &link_status) != 1 || + link_status.role != IEC104_ROLE_CONTROLLED || + IEC104_Link_Get_Send_Progress(manager->link_state, &link_progress) != 1 || + IEC104_AsduQueue_Get_Status(manager->asdu_queue, &asdu_status) != IEC104_ASDU_QUEUE_OK || + IEC104_SendBuffer_Get_Status(manager->send_buffer, &buffer_status) != IEC104_SEND_BUFFER_OK || + asdu_status.connection_generation != connection_generation || + buffer_status.connection_generation != connection_generation) { + return 0; + } + /* pending只统计唯一当前I帧;outstanding还包含已经写完但尚未ACK的I帧。 */ + pending = Sequence_Distance(link_progress.next_completion_ns, link_progress.next_frame_ns); + outstanding = Sequence_Distance(link_progress.peer_ack_nr, link_progress.next_frame_ns); + return manager->next_record_ns == link_progress.next_frame_ns && + pending <= 1U && buffer_status.has_i_frame == pending && + (pending == 0U || buffer_status.i_frame_ns == link_progress.next_completion_ns) && + buffer_status.has_reservation == 0U && outstanding <= manager->send_window_k && + manager->record_count >= outstanding && + asdu_status.retained_unacked_count[IEC104_ASDU_PRIORITY_HIGH] == 0U && + (uint64_t)manager->high_unacked_count + + asdu_status.retained_unacked_count[IEC104_ASDU_PRIORITY_NORMAL] + + asdu_status.overwritten_unacked_count == outstanding; +} + +/* 已校验会话;START受理即允许应答,STOP受理即拒绝;实际发送仍遵守FIFO。 */ +static int Response_Submit_Allowed(const IEC104_SendManager_t *manager) +{ + const IEC104_LinkState_t *state = manager->link_state; + return state->dt_state == IEC104_DT_STARTED && state->dt_procedure == IEC104_DT_PROC_IDLE; +} + +/** + * 功能:返回发送记录存储概况,使连接层无需读取Manager内部字段。 + * 输入参数:manager——尚未Abort的管理器;status——不重叠的输出存储。 + * 返回值:OK写数值副本,ERROR参数/存储非法且不改输出。 + * 说明:不依赖故障后的Link运行状态,可在连接冻结期间用于记录回收和状态展示。 + */ +IEC104_SendManagerResult_t IEC104_SendManager_Get_Status( + const IEC104_SendManager_t *manager, IEC104_SendManagerStatus_t *status) +{ + IEC104_SendManagerStatus_t prepared; + if (status == NULL || !Manager_Storage_Valid(manager)) { + return IEC104_SEND_MANAGER_ERROR; + } + prepared.connection_generation = manager->connection_generation; + prepared.record_capacity = manager->record_capacity; + prepared.record_count = manager->record_count; + prepared.high_unacked_count = manager->high_unacked_count; + *status = prepared; + return IEC104_SEND_MANAGER_OK; +} + +/** + * 功能:校验当前发送会话后,将业务ASDU交给绑定队列复制保存。 + * 输入参数:manager/connection_generation——有效会话及代次;priority/asdu/asdu_length——待入队业务。 + * 返回值:OK成功,WAIT容量/条目编号窗口不足,ERROR参数、会话或HIGH准入阶段非法。 + * 调用关系:Connection推迟应答及Submit_Response回退时使用,向上隐藏队列指针及专用返回值。 + * 说明:HIGH与Submit_Response共用准入检查,先于队列容量判断;NORMAL不受此检查限制。 + * 内部条目编号由队列管理,协议N(S)在实际准备I帧时分配;不推进发送进度。 + */ +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) +{ + IEC104_AsduQueueResult_t result; + if (!Manager_Session_Valid(manager, connection_generation)) { + return IEC104_SEND_MANAGER_ERROR; + } + if (priority == IEC104_ASDU_PRIORITY_HIGH && !Response_Submit_Allowed(manager)) { + return IEC104_SEND_MANAGER_ERROR; + } + /* 队列同步复制原文;这里不创建APDU,后续Prepare_Next_Send才选择业务并分配协议序号。 */ + result = IEC104_AsduQueue_Enqueue(manager->asdu_queue, connection_generation, priority, asdu, asdu_length); + if (result == IEC104_ASDU_QUEUE_OK) { + return IEC104_SEND_MANAGER_OK; + } + /* FULL是可等待的背压;其他非法输入或状态统一返回ERROR,不作为容量不足反复重试。 */ + return result == IEC104_ASDU_QUEUE_FULL ? IEC104_SEND_MANAGER_WAIT : IEC104_SEND_MANAGER_ERROR; +} + +/** + * 功能:为发送队首定位尚未提交本地完成的I帧业务记录。 + * 输入参数:manager——会话及存储边界已验证的发送管理器。 + * head_info——当前发送队首的有效I帧信息,代次已由调用入口验证。 + * 输出参数:无,不修改记录。 + * 返回值:成功返回内部记录指针;找不到或身份、快照不匹配时返回NULL。 + * 说明:先按N(S)计算偏移,再核对frame_id的frame_serial、原始N(R)、长度和入队时刻。 + * 不能只用可能回绕的N(S)识别完成事件;返回指针只供当前串行操作使用。 + */ +static IEC104_IFrameSendRecordStorage_t *Find_Pending_Send_Record(IEC104_SendManager_t *manager, + const IEC104_SendBufferHeadInfo_t *head_info) +{ + uint16_t first = (uint16_t)(((uint32_t)manager->next_record_ns + 32768U - + manager->record_count) & IEC104_SEQUENCE_MASK); + uint16_t offset = Sequence_Distance(first, head_info->i_frame_snapshot.send_number); + IEC104_IFrameSendRecordStorage_t *record; + + if (offset >= manager->record_count) { + return NULL; + } + record = &manager->records[Record_Index_From_Offset(manager, offset)]; + if (!I_Frame_Record_Valid(manager, record, offset) || record->local_send_completed != 0U || + record->frame_serial != head_info->frame_id.frame_serial || + record->i_frame_snapshot.receive_number != head_info->i_frame_snapshot.receive_number || + record->i_frame_snapshot.frame_length != head_info->apdu_length || record->queued_at_ms != head_info->queued_at_ms) { + return NULL; + } + return record; +} + +/** + * 功能:按保存的帧类型将完整发送或发送失败结果交给对应链路完成接口。 + * 输入参数:manager——已校验的子站管理器,绑定本连接CONTROLLED链路。 + * head_info——有效队首信息,保存原始帧类型及I帧快照。 + * sent_len——实际整帧发送长度;发送失败时传-1。 + * 输出参数:manager->link_state——由匹配的链路完成接口按其约定提交或清理状态。 + * 返回值:返回对应接口的OK、IGNORED或FATAL;本端不支持的帧类型返回FATAL。 + * 说明:不按当前dt_procedure猜测报文身份,不执行I/O或发送队列出队。 + * STARTDT_CON/STOPDT_CON直接提交子站完成接口;帧类型在缓冲提交时已完成校验。 + * TESTFR_CON为无状态应答,没有Link完成接口;长度为6返回OK,否则返回FATAL。 + */ +static IEC104_LinkResult_t Dispatch_Link_Send_Completion(const IEC104_SendManager_t *manager, + const IEC104_SendBufferHeadInfo_t *head_info, int sent_len) +{ + IEC104_LinkState_t *link_state = manager->link_state; + + switch (head_info->frame_type) { + case IEC104_SEND_BUFFER_FRAME_I: + return IEC104_Link_Complete_I_Send(link_state, &head_info->i_frame_snapshot, sent_len); + case IEC104_SEND_BUFFER_FRAME_S_ACK: + return IEC104_Link_Complete_S_Ack_Send(link_state, sent_len); + case IEC104_SEND_BUFFER_FRAME_STARTDT_CON: + return IEC104_Link_Complete_Startdt_Con_Send(link_state, sent_len); + case IEC104_SEND_BUFFER_FRAME_STOPDT_CON: + return IEC104_Link_Complete_Stopdt_Con_Send(link_state, sent_len); + case IEC104_SEND_BUFFER_FRAME_TESTFR_ACT: + return IEC104_Link_Complete_Testfr_Act_Send(link_state, sent_len); + case IEC104_SEND_BUFFER_FRAME_TESTFR_CON: + return sent_len == IEC104_APCI_LEN ? IEC104_LINK_RESULT_OK : IEC104_LINK_RESULT_FATAL; + default: + return IEC104_LINK_RESULT_FATAL; + } +} + +/** + * 功能:校验同连接各模块的状态,并为一帧S/U取得控制槽。 + * 输入参数:manager——待校验的管理器;connection_generation——本次调用所属连接代次。 + * 输出参数:reservation——非NULL,仅OK时写入帧身份、可写APDU地址及容量。 + * 返回值:OK表示预留成功;缓冲报告FULL/BUSY时返回WAIT;会话或缓冲非法返回ERROR。 + * 调用关系:公共S/TESTFR及子站应答入口在调用Link构帧前使用,成功后须提交或取消预留。 + * 说明:本函数不调用Link构帧或接收处理,不改变链路过程;接收入口返回WAIT时请求尚未受理。 + * 预留只占用控制槽,不进入发送FIFO,也不占用Manager的I帧业务记录槽。 + * 会话入口要求无外部遗留预留,因此外部未结束的预留会先被判为ERROR。 + */ +static IEC104_SendManagerResult_t Reserve_Control_Frame( + IEC104_SendManager_t *manager, uint64_t connection_generation, + IEC104_SendBufferReservation_t *reservation) +{ + IEC104_SendBufferResult_t result; + + if (!Manager_Session_Valid(manager, connection_generation)) { + return IEC104_SEND_MANAGER_ERROR; + } + result = IEC104_SendBuffer_Reserve_S_U_Frame(manager->send_buffer, reservation); + if (result == IEC104_SEND_BUFFER_FULL || result == IEC104_SEND_BUFFER_BUSY) { + return IEC104_SEND_MANAGER_WAIT; + } + return result == IEC104_SEND_BUFFER_OK ? IEC104_SEND_MANAGER_OK : IEC104_SEND_MANAGER_ERROR; +} + +/** + * 功能:归还本次未生成S/U报文的预留控制槽,并传回调用方选定的处理结果。 + * 输入参数:manager——已校验的管理器;reservation——本次有效预留;result——取消成功后的返回值。 + * 输出参数:无独立输出对象;成功时由SendBuffer释放预留,已使用的frame_serial不复用。 + * 返回值:取消成功原样返回result,取消失败返回FATAL,要求结束连接。 + * 前置条件:指针有效,Link尚未为本次预留生成报文;不可用于撤回已经构帧成功的发送义务。 + * 说明:只归还尚未提交的控制槽,不回滚Link状态。 + */ +static IEC104_SendManagerResult_t Cancel_Control_Reservation( + IEC104_SendManager_t *manager, const IEC104_SendBufferReservation_t *reservation, + IEC104_SendManagerResult_t result) +{ + return IEC104_SendBuffer_Cancel_Reservation(manager->send_buffer, reservation->frame_id) == + IEC104_SEND_BUFFER_OK ? result : IEC104_SEND_MANAGER_FATAL; +} + +/** + * 功能:将预留控制槽内已生成的固定6字节S/U报文追加到发送FIFO。 + * 输入参数:manager——已校验的管理器;reservation——本次有效预留,APDU已由Link写入; + * frame_type——与APDU一致的S/U完成分派类型;now_ms——本次入队单调时刻,允许0。 + * 输出参数:无独立输出对象;SendBuffer保存帧类型、长度及入队时间,不登记I帧业务记录。 + * 返回值:QUEUED表示提交成功;FATAL表示提交失败,须结束连接,不能按容量不足重试。 + * 前置条件:指针有效且Link已生成报文;S/U不传I帧快照,容量已由Reserve_Control_Frame保障。 + * 说明:失败时不取消预留、不回滚Link;成功后实际I/O及Handle_Send_Progress另由接入方驱动。 + */ +static IEC104_SendManagerResult_t Commit_Control_Frame( + IEC104_SendManager_t *manager, const IEC104_SendBufferReservation_t *reservation, + IEC104_SendBufferFrameType_t frame_type, uint64_t now_ms) +{ + return IEC104_SendBuffer_Commit_Frame(manager->send_buffer, reservation->frame_id, + frame_type, IEC104_APCI_LEN, NULL, now_ms) == IEC104_SEND_BUFFER_OK ? + IEC104_SEND_MANAGER_QUEUED : IEC104_SEND_MANAGER_FATAL; +} + +/** + * 功能:将公共S/TESTFR准备接口返回的长度转换为提交或取消操作。 + * 输入参数:manager/reservation——已校验的管理器及本次有效预留;frame_type——预期帧类型; + * length——Link.Prepare的原始返回值;now_ms——生成报文时使用的入队时刻。 + * 输出参数:无独立输出对象;按length提交FIFO或归还预留,不执行网络发送。 + * 返回值:length为6时提交并返回QUEUED;为0时取消并返回WAIT;为负时取消并返回ERROR; + * 提交/取消失败或其他正长度返回FATAL。 + * 说明:0/负值表示Link未生成报文,才允许取消;异常正长度可能已有发送义务,保留现场并断连。 + */ +static IEC104_SendManagerResult_t Finish_Control_Prepare( + IEC104_SendManager_t *manager, const IEC104_SendBufferReservation_t *reservation, + IEC104_SendBufferFrameType_t frame_type, int length, uint64_t now_ms) +{ + if (length <= 0) { + return Cancel_Control_Reservation(manager, reservation, + length == 0 ? IEC104_SEND_MANAGER_WAIT : IEC104_SEND_MANAGER_ERROR); + } + if (length != IEC104_APCI_LEN) { + return IEC104_SEND_MANAGER_FATAL; + } + return Commit_Control_Frame(manager, reservation, frame_type, now_ms); +} + +/** + * 功能:按接收门限、立即确认或子站停止排空规则生成S确认并入队。 + * 输入参数:manager——当前连接管理器;connection_generation——连接代次;w——1..32767的门限; + * immediate——非0表示立即确认;now_ms——入队单调时刻,允许0。 + * 输出参数:无独立输出对象;生成成功时Link保存本帧N(R)及S在途状态,SendBuffer持有报文。 + * 返回值:QUEUED表示S入队;WAIT表示控制容量不足,或Link判断当前无需、不能生成; + * ERROR表示参数、会话或准备结果非法;提交/取消异常返回FATAL。 + * 调用时机:接收I帧后、启动CON完成后或确认条件变化时,由连接调度代码驱动。 + * 说明:immediate只跳过数量门限,不跳过启停门控、已有S在途及无待确认数据检查。 + * 构帧只冻结N(R),完整发送后由Handle_Send_Progress提交确认;WAIT不等于已确认。 + */ +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) +{ + IEC104_SendBufferReservation_t reservation; + IEC104_SendManagerResult_t result; + int length; + + if (w == 0U || w > IEC104_SEQUENCE_MASK) { + return IEC104_SEND_MANAGER_ERROR; + } + result = Reserve_Control_Frame(manager, connection_generation, &reservation); + if (result != IEC104_SEND_MANAGER_OK) { + return result; + } + length = IEC104_Link_Prepare_S_Ack(manager->link_state, w, immediate, + reservation.apdu, reservation.apdu_capacity); + return Finish_Control_Prepare(manager, &reservation, IEC104_SEND_BUFFER_FRAME_S_ACK, length, now_ms); +} + +/** + * 功能:待确认数据的t2到期后,按公共S确认规则构帧并入队。 + * 输入参数:manager——当前连接管理器;connection_generation——连接代次; + * elapsed_ms——从原待确认数据起点计算的经过毫秒数;t2_ms——非0的t2配置; + * now_ms——本次入队单调时刻,与elapsed_ms的相对时间含义不同。 + * 输出参数:无独立输出对象;成功时Link记录S在途状态,报文进入SendBuffer。 + * 返回值:QUEUED表示S入队;WAIT表示控制容量不足、未到期或Link当前不生成S; + * ERROR表示参数、会话或准备结果非法;提交/取消异常返回FATAL。 + * 调用时机:接入方的定时唤醒或连接调度;elapsed_ms等于t2_ms即满足到期条件。 + * 说明:到期仍受启停及已有S在途约束;函数不读取时钟、不重置起点,重试须沿用原计时。 + */ +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) +{ + IEC104_SendBufferReservation_t reservation; + IEC104_SendManagerResult_t result; + int length; + + if (t2_ms == 0U) { + return IEC104_SEND_MANAGER_ERROR; + } + result = Reserve_Control_Frame(manager, connection_generation, &reservation); + if (result != IEC104_SEND_MANAGER_OK) { + return result; + } + length = IEC104_Link_Prepare_T2_Ack(manager->link_state, elapsed_ms, t2_ms, + reservation.apdu, reservation.apdu_capacity); + return Finish_Control_Prepare(manager, &reservation, IEC104_SEND_BUFFER_FRAME_S_ACK, length, now_ms); +} + +/** + * 功能:子站主动生成并排队一帧TESTFR_ACT。 + * 输入参数:manager——当前连接管理器;connection_generation——连接代次;now_ms——入队单调时刻。 + * 输出参数:无独立输出对象;生成后Link进入ACT_SEND_PENDING,SendBuffer持有ACT报文。 + * 返回值:QUEUED表示ACT入队;WAIT表示控制容量不足或已有探测;ERROR表示会话或准备非法; + * 提交/取消异常返回FATAL。 + * 调用时机:连接调度代码决定主动探测时;按空闲时间触发可使用Queue_T3_Testfr。 + * 说明:本入口不检查空闲时长。ACT完整发送后才由Handle_Send_Progress提交CON_PENDING, + * 接入方以实际written_at_ms作为等待CON的t1起点;不能在入队时提前提交发送完成。 + */ +IEC104_SendManagerResult_t IEC104_SendManager_Queue_Testfr_Act( + IEC104_SendManager_t *manager, uint64_t connection_generation, uint64_t now_ms) +{ + IEC104_SendBufferReservation_t reservation; + IEC104_SendManagerResult_t result = Reserve_Control_Frame(manager, connection_generation, &reservation); + int length; + + if (result != IEC104_SEND_MANAGER_OK) { + return result; + } + length = IEC104_Link_Prepare_Testfr_Act(manager->link_state, reservation.apdu, reservation.apdu_capacity); + return Finish_Control_Prepare(manager, &reservation, IEC104_SEND_BUFFER_FRAME_TESTFR_ACT, length, now_ms); +} + +/** + * 功能:链路空闲达到t3后,复用Link的主动探测流程生成TESTFR_ACT并入队。 + * 输入参数:manager——当前连接管理器;connection_generation——连接代次; + * idle_elapsed_ms——接入方计算的空闲毫秒数;t3_ms——非0的t3配置;now_ms——入队单调时刻。 + * 输出参数:无独立输出对象;生成成功后记录ACT待发送状态,并发布到同一FIFO。 + * 返回值:QUEUED表示ACT入队;WAIT表示控制容量不足、未到期或已有探测; + * ERROR表示参数、会话或准备结果非法;提交/取消异常返回FATAL。 + * 调用时机:接入方定时唤醒后,以当前有效活动起点计算时间差;等于t3_ms即满足到期条件。 + * 说明:不创建定时器、不更新活动起点;已有探测的t1不能因活动、重复调用或WAIT而续期。 + */ +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) +{ + IEC104_SendBufferReservation_t reservation; + IEC104_SendManagerResult_t result; + int length; + + if (t3_ms == 0U) { + return IEC104_SEND_MANAGER_ERROR; + } + result = Reserve_Control_Frame(manager, connection_generation, &reservation); + if (result != IEC104_SEND_MANAGER_OK) { + return result; + } + length = IEC104_Link_Prepare_T3_Testfr(manager->link_state, idle_elapsed_ms, t3_ms, + reservation.apdu, reservation.apdu_capacity); + return Finish_Control_Prepare(manager, &reservation, IEC104_SEND_BUFFER_FRAME_TESTFR_ACT, length, now_ms); +} + +/** + * 功能:子站接收TESTFR_ACT,为其生成并排队一帧TESTFR_CON。 + * 输入参数:manager——当前连接管理器;connection_generation——接收所属连接代次; + * apci——已通过Parse_APCI且功能码为TESTFR_ACT的U帧;now_ms——应答入队单调时刻。 + * 输出参数:无独立输出对象;QUEUED时SendBuffer持有CON,Link的主动探测状态不变。 + * 返回值:QUEUED表示应答入队;WAIT表示预留失败、本次请求尚未受理; + * ERROR表示输入、会话或预留状态非法;Link拒绝或提交/取消异常返回FATAL。 + * 调用链:接收路由 -> Reserve_Control_Frame -> Link.Handle_Testfr_Act -> Commit_Control_Frame。 + * 说明:WAIT须按序保留请求后重试,无法保留则断连;收到多个ACT时分别调用并逐个应答。 + * TESTFR_CON没有Link完成接口,但实际发送仍交给Handle_Send_Progress/Failure统一处理。 + */ +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) +{ + IEC104_SendBufferReservation_t reservation; + IEC104_SendManagerResult_t result; + + if (apci == NULL || apci->frame_type != IEC104_FRAME_TYPE_U || apci->u_function != IEC104_U_TESTFR_ACT) { + return IEC104_SEND_MANAGER_ERROR; + } + result = Reserve_Control_Frame(manager, connection_generation, &reservation); + if (result != IEC104_SEND_MANAGER_OK) { + return result; + } + if (IEC104_Link_Handle_Testfr_Act(apci, reservation.apdu, reservation.apdu_capacity) != IEC104_LINK_RESULT_OK) { + return Cancel_Control_Reservation(manager, &reservation, IEC104_SEND_MANAGER_FATAL); + } + return Commit_Control_Frame(manager, &reservation, IEC104_SEND_BUFFER_FRAME_TESTFR_CON, now_ms); +} + +/* TESTFR的接收确认与发送完成使用同一个Manager入口边界。 + * 不预留控制槽,槽满不影响接受确认;只有CON_PENDING才结束本端探测。 */ +IEC104_SendManagerResult_t IEC104_SendManager_Handle_Testfr_Con( + IEC104_SendManager_t *manager, uint64_t connection_generation, const IEC104_ApciInfo_t *apci) +{ + IEC104_LinkResult_t result; + if (apci == NULL || apci->frame_type != IEC104_FRAME_TYPE_U || apci->u_function != IEC104_U_TESTFR_CON || + !Manager_Session_Valid(manager, connection_generation)) { + return IEC104_SEND_MANAGER_ERROR; + } + result = IEC104_Link_Handle_Testfr_Con(manager->link_state, apci); + if (result == IEC104_LINK_RESULT_OK) { + return IEC104_SEND_MANAGER_OK; + } + return result == IEC104_LINK_RESULT_IGNORED ? IEC104_SEND_MANAGER_IGNORED : IEC104_SEND_MANAGER_FATAL; +} + +/* HIGH快速路径也遵守事件公平配额,不能通过反复调用应答接口绕过已配置的调度规则。 */ +static int High_Has_Priority(const IEC104_SendManager_t *manager, int normal_waiting) +{ + return !normal_waiting || manager->high_priority_burst_limit == 0U || + manager->high_priority_burst_count < manager->high_priority_burst_limit; +} + +/* 统一交接一帧I报文:预留、Link编号、提交APDU、建立记录、更新来源及调度额度。 + * queue_entry非NULL表示来自队列;NULL仅供直接HIGH应答使用,不虚构队列条目或执行Mark。 + * 调用方已验证会话、ASDU及发送顺序;WAIT尚未接手原文,Link编号后的失败必须FATAL收尾。 */ +static IEC104_SendManagerResult_t Prepare_I_Asdu( + IEC104_SendManager_t *manager, const uint8_t *asdu, uint16_t asdu_length, + IEC104_AsduPriority_t priority, const IEC104_AsduEntryId_t *queue_entry, + int normal_waiting, uint64_t now_ms, IEC104_SendBufferHeadInfo_t *head_info) +{ + IEC104_SendBufferReservation_t reservation; + IEC104_SendBufferResult_t tx_result; + IEC104_SendBufferHeadInfo_t prepared_head; + IEC104_IFrameSendRecordStorage_t prepared; + int apdu_length; + + /* 已ACK但未消费的记录仍占槽;没有记录容量时不得分配协议序号。 */ + if (manager->record_count == manager->record_capacity) { + return IEC104_SEND_MANAGER_WAIT; + } + tx_result = IEC104_SendBuffer_Reserve_I_Frame(manager->send_buffer, &reservation); + if (tx_result == IEC104_SEND_BUFFER_FULL || tx_result == IEC104_SEND_BUFFER_BUSY) { + return IEC104_SEND_MANAGER_WAIT; + } + if (tx_result != IEC104_SEND_BUFFER_OK) { + return IEC104_SEND_MANAGER_ERROR; + } + memset(&prepared, 0, sizeof(prepared)); + apdu_length = IEC104_Link_Prepare_I_Frame(manager->link_state, manager->send_window_k, + asdu, asdu_length, + reservation.apdu, reservation.apdu_capacity, &prepared.i_frame_snapshot); + if (apdu_length <= 0) { + if (IEC104_SendBuffer_Cancel_Reservation(manager->send_buffer, reservation.frame_id) != IEC104_SEND_BUFFER_OK) { + return IEC104_SEND_MANAGER_FATAL; + } + return apdu_length == 0 ? IEC104_SEND_MANAGER_WAIT : IEC104_SEND_MANAGER_ERROR; + } + /* 从Prepare成功起N(S)不可撤回,后续异常必须结束连接;此时原始ASDU尚未释放。 */ + if (prepared.i_frame_snapshot.send_number != manager->next_record_ns || + IEC104_SendBuffer_Commit_Frame(manager->send_buffer, reservation.frame_id, IEC104_SEND_BUFFER_FRAME_I, + (uint16_t)apdu_length, &prepared.i_frame_snapshot, now_ms) != IEC104_SEND_BUFFER_OK) { + return IEC104_SEND_MANAGER_FATAL; + } + /* 两条路径共用同一发送记录及ACK记账。队列来源随后Mark;直接应答没有队列原文,serial为0。 + * SendBuffer已复制完整APDU,成功后不再借用业务输入;本步骤尚未执行socket发送。 */ + prepared.frame_serial = reservation.frame_id.frame_serial; + prepared.asdu_entry_serial = queue_entry != NULL ? queue_entry->entry_serial : 0U; + prepared.asdu_priority = priority; + prepared.queued_at_ms = now_ms; + manager->records[Record_Index_From_Offset(manager, manager->record_count)] = prepared; + manager->record_count++; + if (priority == IEC104_ASDU_PRIORITY_HIGH) { + manager->high_unacked_count++; + } + manager->next_record_ns = (uint16_t)((manager->next_record_ns + 1U) & IEC104_SEQUENCE_MASK); + if (queue_entry != NULL && IEC104_AsduQueue_Mark_Submitted(manager->asdu_queue, priority, + *queue_entry) != IEC104_ASDU_QUEUE_OK) { + return IEC104_SEND_MANAGER_FATAL; + } + /* 只计本次成功选择;NORMAL无未提交条目时清零,不为未来业务累积高优先欠额。 */ + if (priority == IEC104_ASDU_PRIORITY_HIGH && + normal_waiting && manager->high_priority_burst_limit != 0U) { + manager->high_priority_burst_count++; + } else { + manager->high_priority_burst_count = 0U; + } + if (IEC104_SendBuffer_Peek_Send_Head(manager->send_buffer, &prepared_head) != IEC104_SEND_BUFFER_OK) { + return IEC104_SEND_MANAGER_FATAL; + } + *head_info = prepared_head; + return IEC104_SEND_MANAGER_OK; +} + +/** + * 功能:紧邻实际写socket前取得待写字节;没有已准备帧时选择ASDU并分配一个N(S)。 + * 输入参数:manager/connection_generation——当前管理器及匹配代次; + * now_ms——单调毫秒,仅准备新I帧时成为本地发送期限起点。 + * 输出参数:head_info——非NULL,仅OK时写入当前帧身份、描述和剩余字节借用指针。 + * 返回值:OK返回当前帧剩余字节;EMPTY表示既无已准备帧,也无未提交ASDU;WAIT表示门控、窗口或记录容量阻塞; + * ERROR表示输入、会话或准备非法;Link准备成功后的提交异常返回FATAL。 + * 调用链:续传当前I/控制帧;无当前帧时Peek_Unsubmitted -> Reserve_I_Frame -> Link.Prepare + * -> Buffer.Commit_Frame接手报文 -> 登记记录 -> Queue.Mark_Submitted完成队列交接记账 -> 返回待写字节。 + * 说明:APDU提交且发送记录建立后,HIGH原文立即出队,NORMAL标记已提交并保留原文至ACK或覆盖。 + * 控制帧先于尚未分配协议N(S)的ASDU,已准备的I帧即使EAGAIN零进度也必须先完成。 + * 重复取得当前帧不编号、不改变配额或期限;函数不执行实际I/O。 + * 跨尾部原文用249字节局部缓冲拼接后调用Link;连续原文直接借用,不增加动态分配。 + */ +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) +{ + IEC104_AsduHeadInfo_t high_head_info, normal_head_info, selected_head_info; + IEC104_AsduQueueResult_t high_result, normal_result; + IEC104_SendBufferResult_t tx_result; + IEC104_SendBufferHeadInfo_t prepared_head; + uint8_t wrapped_asdu[IEC104_MAX_ASDU_LEN]; + const uint8_t *selected_asdu; + + if (head_info == NULL || !Manager_Session_Valid(manager, connection_generation)) { + return IEC104_SEND_MANAGER_ERROR; + } + /* 续传已准备帧,不为后续业务预建APDU;控制FIFO在选择新I帧之前排空。 */ + tx_result = IEC104_SendBuffer_Peek_Send_Head(manager->send_buffer, &prepared_head); + if (tx_result == IEC104_SEND_BUFFER_OK) { + *head_info = prepared_head; + return IEC104_SEND_MANAGER_OK; + } + if (tx_result != IEC104_SEND_BUFFER_EMPTY) { + return IEC104_SEND_MANAGER_ERROR; + } + high_result = IEC104_AsduQueue_Peek_Unsubmitted(manager->asdu_queue, IEC104_ASDU_PRIORITY_HIGH, &high_head_info); + normal_result = IEC104_AsduQueue_Peek_Unsubmitted(manager->asdu_queue, IEC104_ASDU_PRIORITY_NORMAL, &normal_head_info); + if (high_result == IEC104_ASDU_QUEUE_ERROR || normal_result == IEC104_ASDU_QUEUE_ERROR) { + return IEC104_SEND_MANAGER_ERROR; + } + if (high_result == IEC104_ASDU_QUEUE_EMPTY && normal_result == IEC104_ASDU_QUEUE_EMPTY) { + return IEC104_SEND_MANAGER_EMPTY; + } + /* limit为0时严格HIGH优先;NORMAL有未提交条目且配额用尽时,选择NORMAL。 */ + if (high_result == IEC104_ASDU_QUEUE_OK && + High_Has_Priority(manager, normal_result == IEC104_ASDU_QUEUE_OK)) { + selected_head_info = high_head_info; + } else { + selected_head_info = normal_head_info; + } + /* 连续原文直接借用;跨尾部的两段仅在此拼成连续ASDU,原队列数据不移动。 */ + selected_asdu = selected_head_info.asdu_first; + if (selected_head_info.asdu_second != NULL) { + if (IEC104_AsduQueue_Copy_Asdu(&selected_head_info, wrapped_asdu, sizeof(wrapped_asdu)) != IEC104_ASDU_QUEUE_OK) { + return IEC104_SEND_MANAGER_ERROR; + } + selected_asdu = wrapped_asdu; + } + return Prepare_I_Asdu(manager, selected_asdu, selected_head_info.asdu_length, + selected_head_info.priority, &selected_head_info.entry_id, + normal_result == IEC104_ASDU_QUEUE_OK, now_ms, head_info); +} + +/** + * 功能:按启停阶段接手当前连接应答;能开始一帧时直接交给SendBuffer,否则复制到HIGH。 + * 输入参数:asdu/asdu_length——业务原文;now_ms——准备帧时的单调时刻。 + * 输出参数:仅OK写head_info,返回直接准备好的应答帧;成功后不再借用业务原文。 + * 返回值:OK已准备、尚未实际写;QUEUED已缓存HIGH;WAIT未接手、由调用方保留重试; + * ERROR非法参数/状态或HIGH总容量装不下;FATAL表示编号后的交接失败,必须结束连接。 + * 说明:仅STARTED/IDLE时接手;否则ERROR且不入HIGH。 + * 准入先于容量及窗口判断;通过后已有I/S/U、旧HIGH、配额或记录/k窗口限制均走队列。 + * 新STOP受理时取消HIGH待发应答;已经交给SendBuffer的I帧继续原发送及确认流程。 + * 直接路径与队列路径共用Prepare_I_Asdu,不另建序号、帧缓冲或ACK逻辑。 + */ +static IEC104_SendManagerResult_t 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, int bypass_queue) +{ + IEC104_AsduHeadInfo_t high_head, normal_head; + IEC104_SendBufferHeadInfo_t current_head; + IEC104_AsduQueueResult_t high_result, normal_result; + IEC104_SendBufferResult_t current_result; + IEC104_SendManagerResult_t result; + + if (head_info == NULL || asdu == NULL || asdu_length < IEC104_ASDU_HEADER_LEN || + asdu_length > IEC104_MAX_ASDU_LEN || !Manager_Session_Valid(manager, connection_generation)) { + return IEC104_SEND_MANAGER_ERROR; + } + if (!Response_Submit_Allowed(manager)) { + return IEC104_SEND_MANAGER_ERROR; + } + current_result = IEC104_SendBuffer_Peek_Send_Head(manager->send_buffer, ¤t_head); + high_result = IEC104_AsduQueue_Peek_Unsubmitted(manager->asdu_queue, IEC104_ASDU_PRIORITY_HIGH, &high_head); + normal_result = IEC104_AsduQueue_Peek_Unsubmitted(manager->asdu_queue, IEC104_ASDU_PRIORITY_NORMAL, &normal_head); + if ((current_result != IEC104_SEND_BUFFER_OK && current_result != IEC104_SEND_BUFFER_EMPTY) || + high_result == IEC104_ASDU_QUEUE_ERROR || normal_result == IEC104_ASDU_QUEUE_ERROR) { + return IEC104_SEND_MANAGER_ERROR; + } + if (current_result == IEC104_SEND_BUFFER_EMPTY && high_result == IEC104_ASDU_QUEUE_EMPTY && + (bypass_queue ? normal_result == IEC104_ASDU_QUEUE_EMPTY : + High_Has_Priority(manager, normal_result == IEC104_ASDU_QUEUE_OK))) { + result = Prepare_I_Asdu(manager, asdu, asdu_length, IEC104_ASDU_PRIORITY_HIGH, NULL, + normal_result == IEC104_ASDU_QUEUE_OK, now_ms, head_info); + if (result != IEC104_SEND_MANAGER_WAIT) { + return result; + } + } + /* 不排队路径不得越过旧NORMAL,也不得在窗口/记录/缓冲背压时保存旧业务值。 */ + if (bypass_queue) { return IEC104_SEND_MANAGER_WAIT; } + result = IEC104_SendManager_Enqueue(manager, connection_generation, IEC104_ASDU_PRIORITY_HIGH, asdu, asdu_length); + return result == IEC104_SEND_MANAGER_OK ? IEC104_SEND_MANAGER_QUEUED : result; +} + +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) +{ + return Submit_Response(manager, connection_generation, asdu, asdu_length, now_ms, head_info, 0); +} + +/* 仅在两级队列无待发数据且能准备新I帧时接手;WAIT不缓存、不分配N(S)。 */ +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) +{ + return Submit_Response(manager, connection_generation, asdu, asdu_length, now_ms, head_info, 1); +} + +/** + * 功能:提交队首实际写入增量,整帧写完后提交适用的链路完成结果并释放发送缓冲槽。 + * 输入参数:manager——有效的本连接发送管理器。 + * frame_id——此前Prepare_Next_Send或Submit_Response返回的当前帧身份。 + * bytes_written_now——本次实际成功写入的增量,不能超过剩余长度;0不推进进度。 + * now_ms——本次完成时刻;最后一段须传实际整帧写完的单调时刻。 + * 输出参数:completion——仅FRAME_WRITTEN时写入数值副本;其他结果保持原值。 + * 发送队列、链路及I帧记录——按实际发送进度提交状态。 + * 返回值:OK表示尚未写完;FRAME_WRITTEN表示本地完成处理及缓冲释放成功;ERROR表示输入或匹配非法; + * FATAL表示整帧写完后的状态提交失败,必须结束当前连接。 + * 调用方:Connection接收TcpServer的实际写入结果后,用准备帧时取得的frame_id提交本次增量。 + * 调用链:SendBuffer.Record_Written_Bytes;仅整帧写完时继续分派适用的Link.Complete + * -> Manager登记I帧本地完成 -> SendBuffer.Commit_Send_Completion。 + * 说明:本函数不写socket;调用方先执行实际I/O,再恰好提交一次对应进度。 + * TESTFR_CON没有Link完成接口;完整写入后直接进入缓冲完成处理。 + * 整帧完成时刻不能早于入队时刻;部分写入不更新计时起点。 + * 同一增量不能重复提交,也不能另行调用Buffer.Record_Written_Bytes、Link.Complete或Buffer完成接口。 + * 完成本函数后再分派依赖本地TESTFR_ACT完成的TESTFR_CON;不自动创建t1定时器。 + */ +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) +{ + IEC104_SendBufferHeadInfo_t head_info; + IEC104_SendBufferResult_t tx_result; + IEC104_LinkResult_t link_completion_result; + IEC104_IFrameSendRecordStorage_t *record = NULL; + IEC104_LocalSendCompletion_t prepared; + + if (completion == NULL || !Manager_Session_Valid(manager, frame_id.connection_generation) || + IEC104_SendBuffer_Peek_Send_Head(manager->send_buffer, &head_info) != IEC104_SEND_BUFFER_OK || + head_info.frame_id.frame_serial != frame_id.frame_serial || head_info.remaining_bytes == 0U) { + return IEC104_SEND_MANAGER_ERROR; + } + /* 在推进写入进度之前核对业务记录,避免错误frame_id或关联造成部分状态更新。 */ + if (head_info.frame_type == IEC104_SEND_BUFFER_FRAME_I) { + record = Find_Pending_Send_Record(manager, &head_info); + if (record == NULL) { + return IEC104_SEND_MANAGER_ERROR; + } + } + tx_result = IEC104_SendBuffer_Record_Written_Bytes(manager->send_buffer, frame_id, bytes_written_now, now_ms); + if (tx_result == IEC104_SEND_BUFFER_OK) { + return IEC104_SEND_MANAGER_OK; + } + if (tx_result != IEC104_SEND_BUFFER_FRAME_WRITTEN) { + return IEC104_SEND_MANAGER_ERROR; + } + /* + * 整帧完成第1步:按原始类型分派适用的Link完成接口;TESTFR_CON无需调用,I帧推进sent_vs及捎带确认。 + * head_info保留构帧时的类型和I帧快照,实际完整发送时刻取本次now_ms。 + */ + link_completion_result = Dispatch_Link_Send_Completion(manager, &head_info, head_info.apdu_length); + if (link_completion_result != IEC104_LINK_RESULT_OK && link_completion_result != IEC104_LINK_RESULT_IGNORED) { + return IEC104_SEND_MANAGER_FATAL; + } + if (record != NULL) { + if (link_completion_result != IEC104_LINK_RESULT_OK) { + return IEC104_SEND_MANAGER_FATAL; + } + /* 第2步:登记Manager业务记录的本地完成;后续接收路径才允许ACK覆盖该帧。 */ + record->local_send_completed = 1U; + record->written_at_ms = now_ms; + } + /* + * 第3步:释放当前I槽或控制FIFO队首;独立业务记录继续等待ACK,即使NORMAL原文已覆盖。 + * 这里只处理报文存储,就绪记录由TcpServer转为事件后再移除。Complete已提交,缓冲提交失败不能重做Complete。 + */ + if (IEC104_SendBuffer_Commit_Send_Completion(manager->send_buffer, frame_id) != IEC104_SEND_BUFFER_OK) { + return IEC104_SEND_MANAGER_FATAL; + } + /* 输出只复制数值,避免把已释放帧的借用指针交给后续定时器或业务。 */ + prepared.frame_id = frame_id; + prepared.frame_type = head_info.frame_type; + prepared.link_completion_result = link_completion_result; + prepared.i_frame_snapshot = head_info.i_frame_snapshot; + prepared.queued_at_ms = head_info.queued_at_ms; + prepared.written_at_ms = now_ms; + *completion = prepared; + return IEC104_SEND_MANAGER_FRAME_WRITTEN; +} + +/** + * 功能:将匹配队首的发送失败或本地发送超时交给链路完成接口。 + * 输入参数:manager——有效的本连接发送管理器。 + * frame_id——尚未整帧完成的发送队首身份。 + * 输出参数:link_state——匹配时按Complete(-1)约定清理;队列及业务记录保留供故障回收。 + * 返回值:ERROR表示参数、会话、队首或身份非法;匹配并提交失败后始终返回FATAL。 + * 说明:即使底层Complete返回IGNORED,实际发送失败仍要求连接层断连。 + * 不自动Abort;调用方须回收业务、关闭TCP并结束或隔离旧I/O。 + */ +IEC104_SendManagerResult_t IEC104_SendManager_Handle_Send_Failure( + IEC104_SendManager_t *manager, IEC104_SendFrameId_t frame_id) +{ + IEC104_SendBufferHeadInfo_t head_info; + + if (!Manager_Session_Valid(manager, frame_id.connection_generation) || + IEC104_SendBuffer_Peek_Send_Head(manager->send_buffer, &head_info) != IEC104_SEND_BUFFER_OK || + head_info.frame_id.frame_serial != frame_id.frame_serial || head_info.remaining_bytes == 0U || + (head_info.frame_type == IEC104_SEND_BUFFER_FRAME_I && Find_Pending_Send_Record(manager, &head_info) == NULL)) { + return IEC104_SEND_MANAGER_ERROR; + } + /* 网络失败已决定必须断连,不因底层完成接口返回IGNORED而继续发送。 */ + (void)Dispatch_Link_Send_Completion(manager, &head_info, -1); + return IEC104_SEND_MANAGER_FATAL; +} + +/** + * 功能:处理已解析I/S帧,验证累计ACK并登记确认事实;仅NORMAL继续确认原文队列。 + * 输入参数:manager/connection_generation——当前管理器及匹配代次; + * apci——成功Parse_APCI得到的完整I/S帧描述。 + * 输出参数:Link——提交有效ACK及I帧接收位置;records——标记新确认事实; + * Manager——核减HIGH未确认记录数;AsduQueue——释放NORMAL原文或核减覆盖待确认数;不消费业务回执。 + * 返回值:OK处理成功;ERROR表示参数、会话或新确认区间的记录非法; + * PROTOCOL_ERROR表示合法本地会话下Link拒绝报文;FATAL表示受理后的确认同步或原文释放失败。 + * 调用链:读取旧位置 -> 预检新ACK记录 -> Link.Handle -> 登记全部新ACK事实 -> NORMAL逐条Confirm_Next_Normal_Entry。 + * 说明:ACK只能覆盖已提交本地完成的I帧;异步接入须先提交真实发送结果。 + * 重复ACK的新确认数量为0,不重复释放、不重置期限;I帧接收仍由Link处理。 + * 确认处理只访问新确认区间,复杂度O(新确认数)。释放中途失败仍保留整个已接受ACK区间供恢复。 + * NORMAL原文覆盖不撤销帧;记录仍严格匹配序号和本地完成,已覆盖原文由Queue跳过释放。 + */ +IEC104_SendManagerResult_t IEC104_SendManager_Handle_I_S_Frame( + IEC104_SendManager_t *manager, uint64_t connection_generation, const IEC104_ApciInfo_t *apci) +{ + IEC104_IFrameSendProgress_t before_progress, after_progress; + uint16_t acknowledged, first_offset, i, high_acknowledged = 0U; + int ack_in_range; + IEC104_LinkResult_t result; + + if (!Manager_Session_Valid(manager, connection_generation) || apci == NULL || + (apci->frame_type != IEC104_FRAME_TYPE_I && apci->frame_type != IEC104_FRAME_TYPE_S) || + IEC104_Link_Get_Send_Progress(manager->link_state, &before_progress) != 1) { + return IEC104_SEND_MANAGER_ERROR; + } + acknowledged = Sequence_Distance(before_progress.peer_ack_nr, apci->receive_number); + ack_in_range = apci->receive_number <= IEC104_SEQUENCE_MASK && + acknowledged <= Sequence_Distance(before_progress.peer_ack_nr, before_progress.next_completion_ns); + /* 记录前缀是已ACK未消费回执,后缀是未ACK帧;相减直接定位旧V(A)对应记录。 */ + first_offset = (uint16_t)(manager->record_count - + Sequence_Distance(before_progress.peer_ack_nr, before_progress.next_frame_ns)); + /* 非法网络N(R)不用于访问记录,仍由Link处理并拒绝。 */ + if (ack_in_range) { + for (i = 0U; i < acknowledged; ++i) { + uint16_t offset = (uint16_t)(first_offset + i); + const IEC104_IFrameSendRecordStorage_t *record = &manager->records[Record_Index_From_Offset(manager, offset)]; + if (!I_Frame_Record_Valid(manager, record, offset) || + record->local_send_completed != 1U || record->peer_acknowledged != 0U) { + return IEC104_SEND_MANAGER_ERROR; + } + if (record->asdu_priority == IEC104_ASDU_PRIORITY_HIGH) { + high_acknowledged++; + } + } + if (high_acknowledged > manager->high_unacked_count) { + return IEC104_SEND_MANAGER_ERROR; + } + } + result = apci->frame_type == IEC104_FRAME_TYPE_I ? + IEC104_Link_Handle_I_Frame(manager->link_state, apci) : + IEC104_Link_Handle_S_Frame(manager->link_state, apci); + /* 本地会话及记录已预检;Link的拒绝才表示接收阶段、序号或ACK违反协议。 */ + if (result == IEC104_LINK_RESULT_FATAL) { + return IEC104_SEND_MANAGER_PROTOCOL_ERROR; + } + if (result != IEC104_LINK_RESULT_OK || !ack_in_range || + IEC104_Link_Get_Send_Progress(manager->link_state, &after_progress) != 1 || + after_progress.peer_ack_nr != apci->receive_number) { + return IEC104_SEND_MANAGER_FATAL; + } + /* 即使之后释放原文失败,也不能丢失Link已经接受的累计确认事实。 */ + for (i = 0U; i < acknowledged; ++i) { + manager->records[Record_Index_From_Offset(manager, (uint16_t)(first_offset + i))].peer_acknowledged = 1U; + } + manager->high_unacked_count = (uint16_t)(manager->high_unacked_count - high_acknowledged); + /* HIGH原文在提交时已出队,ACK不再访问HIGH队列;NORMAL仍按真实身份释放原文或核减覆盖计数。 */ + for (i = 0U; i < acknowledged; ++i) { + const IEC104_IFrameSendRecordStorage_t *record = + &manager->records[Record_Index_From_Offset(manager, (uint16_t)(first_offset + i))]; + IEC104_AsduEntryId_t entry_id; + if (record->asdu_priority == IEC104_ASDU_PRIORITY_HIGH) { + continue; + } + entry_id.connection_generation = connection_generation; + entry_id.entry_serial = record->asdu_entry_serial; + if (IEC104_AsduQueue_Confirm_Next_Normal_Entry(manager->asdu_queue, record->asdu_priority, entry_id) != IEC104_ASDU_QUEUE_OK) { + return IEC104_SEND_MANAGER_FATAL; + } + } + return IEC104_SEND_MANAGER_OK; +} + +/** + * 功能:按交接顺序复制指定偏移处的业务记录,供观察或故障回收。 + * 输入参数:manager——存储结构有效的发送管理器,不要求故障后的Link仍合法。 + * offset——相对当前记录队首的记录数偏移,0表示最早记录,单位不是字节。 + * 输出参数:record——仅OK时写入带完整frame_id的数值副本;EMPTY和ERROR时保持原值。 + * 返回值:OK表示取得记录;EMPTY表示偏移处无记录;ERROR表示参数、存储或记录非法。 + * 说明:frame_id由Manager共享代次和记录frame_serial组合,不依赖Link或原文借用。 + * 不删除记录、不修改完成标志;已关闭TCP不妨碍读取保存的ACK事实。 + * 已确认与未确认事实须供业务回收策略使用;未确认不等于对端未收到。 + */ +IEC104_SendManagerResult_t IEC104_SendManager_Get_Record_By_Offset( + const IEC104_SendManager_t *manager, uint16_t offset, IEC104_IFrameSendRecord_t *record) +{ + const IEC104_IFrameSendRecordStorage_t *stored; + IEC104_IFrameSendRecord_t prepared; + + if (!Manager_Storage_Valid(manager) || record == NULL) { + return IEC104_SEND_MANAGER_ERROR; + } + if (offset >= manager->record_count) { + return IEC104_SEND_MANAGER_EMPTY; + } + stored = &manager->records[Record_Index_From_Offset(manager, offset)]; + if (!I_Frame_Record_Valid(manager, stored, offset)) { + return IEC104_SEND_MANAGER_ERROR; + } + memset(&prepared, 0, sizeof(prepared)); + prepared.frame_id.connection_generation = manager->connection_generation; + prepared.frame_id.frame_serial = stored->frame_serial; + prepared.asdu_entry_serial = stored->asdu_entry_serial; + prepared.queued_at_ms = stored->queued_at_ms; + prepared.written_at_ms = stored->written_at_ms; + prepared.i_frame_snapshot = stored->i_frame_snapshot; + prepared.asdu_priority = stored->asdu_priority; + prepared.local_send_completed = stored->local_send_completed; + prepared.peer_acknowledged = stored->peer_acknowledged; + *record = prepared; + return IEC104_SEND_MANAGER_OK; +} + +/** + * 功能:读取最早已完成本地发送、尚未被对端确认的I帧记录。 + * 输入参数:manager——当前会话有效的子站管理器;record——非NULL输出。 + * 输出参数:仅OK时复制记录,不返回APDU指针,不改变队列。 + * 返回值:OK、EMPTY(包括只有待发送I帧)或ERROR。 + * 说明:记录环形数组中,已确认未消费的前缀后就是V(A)对应的记录,可直接定位。 + */ +IEC104_SendManagerResult_t IEC104_SendManager_Peek_Oldest_Sent_Unacked_Record( + const IEC104_SendManager_t *manager, IEC104_IFrameSendRecord_t *record) +{ + IEC104_IFrameSendProgress_t progress; + IEC104_IFrameSendRecord_t prepared; + uint16_t offset; + + if (record == NULL || !Manager_Storage_Valid(manager) || + !Manager_Session_Valid(manager, manager->connection_generation) || + IEC104_Link_Get_Send_Progress(manager->link_state, &progress) != 1) { + return IEC104_SEND_MANAGER_ERROR; + } + if (progress.peer_ack_nr == progress.next_completion_ns) { + return IEC104_SEND_MANAGER_EMPTY; + } + offset = (uint16_t)(manager->record_count - + Sequence_Distance(progress.peer_ack_nr, progress.next_frame_ns)); + if (IEC104_SendManager_Get_Record_By_Offset(manager, offset, &prepared) != IEC104_SEND_MANAGER_OK || + prepared.i_frame_snapshot.send_number != progress.peer_ack_nr || + prepared.local_send_completed != 1U || prepared.peer_acknowledged != 0U) { + return IEC104_SEND_MANAGER_ERROR; + } + *record = prepared; + return IEC104_SEND_MANAGER_OK; +} + +/* 起点与期限已校验;先作64位差值再截断,避免绝对期限加法或长时间差溢出。 */ +static uint32_t Deadline_Remaining(uint64_t now_ms, uint64_t started_ms, uint32_t timeout_ms) +{ + uint64_t elapsed = now_ms - started_ms; + return elapsed >= timeout_ms ? 0U : timeout_ms - (uint32_t)elapsed; +} + +/* 向Connection提供同一时刻的队首期限、帧身份、I帧t1和控制容量。 + * 原始起点仍分别来自Buffer和发送记录;此快照只在当前串行调用中使用,不缓存到对象内。 */ +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) +{ + IEC104_SendManagerDeadlines_t prepared; + IEC104_SendBufferStatus_t buffer_status; + IEC104_SendBufferHeadInfo_t head; + IEC104_IFrameSendRecord_t record; + IEC104_SendBufferResult_t buffered; + IEC104_SendManagerResult_t recorded; + + if (deadlines == NULL || local_send_timeout_ms == 0U || t1_ms == 0U || + !Manager_Session_Valid(manager, connection_generation) || + IEC104_SendBuffer_Get_Status(manager->send_buffer, &buffer_status) != IEC104_SEND_BUFFER_OK) { + return IEC104_SEND_MANAGER_ERROR; + } + memset(&prepared, 0, sizeof(prepared)); + prepared.control_has_room = buffer_status.control_has_room; + buffered = IEC104_SendBuffer_Peek_Send_Head(manager->send_buffer, &head); + if (buffered == IEC104_SEND_BUFFER_OK) { + if (now_ms < head.queued_at_ms) { + return IEC104_SEND_MANAGER_ERROR; + } + prepared.has_send_head = 1U; + prepared.head_frame_id = head.frame_id; + prepared.local_send_remaining_ms = Deadline_Remaining(now_ms, head.queued_at_ms, local_send_timeout_ms); + } else if (buffered != IEC104_SEND_BUFFER_EMPTY) { + return IEC104_SEND_MANAGER_ERROR; + } + recorded = IEC104_SendManager_Peek_Oldest_Sent_Unacked_Record(manager, &record); + if (recorded == IEC104_SEND_MANAGER_OK) { + if (now_ms < record.written_at_ms) { + return IEC104_SEND_MANAGER_ERROR; + } + prepared.has_i_ack_deadline = 1U; + prepared.i_ack_remaining_ms = Deadline_Remaining(now_ms, record.written_at_ms, t1_ms); + } else if (recorded != IEC104_SEND_MANAGER_EMPTY) { + return IEC104_SEND_MANAGER_ERROR; + } + *deadlines = prepared; + return IEC104_SEND_MANAGER_OK; +} + +/** + * 功能:检查最早已本地完成、尚未收到ACK的I帧是否达到t1。 + * 输入参数:manager/connection_generation——当前管理器及匹配代次; + * now_ms——当前单调毫秒;t1_ms——非零确认等待期限。 + * 输出参数:无,不更新记录、不启动定时器或关闭连接。 + * 返回值:1到期;0未到期或没有已完成未确认I帧;-1参数、状态或时间非法。 + * 说明:起点取最早未ACK记录的原written_at_ms,等于期限即到期。 + * 部分ACK后选下一帧的原时间,重复ACK或后续帧发送不为前帧续期; + * 尚未整帧完成的当前I帧使用Buffer的本地发送期限。 + */ +int IEC104_SendManager_T1_I_Ack_Expired( + const IEC104_SendManager_t *manager, uint64_t connection_generation, + uint64_t now_ms, uint32_t t1_ms) +{ + IEC104_IFrameSendRecord_t record; + IEC104_SendManagerResult_t result; + + if (!Manager_Storage_Valid(manager) || connection_generation != manager->connection_generation || t1_ms == 0U) { + return -1; + } + result = IEC104_SendManager_Peek_Oldest_Sent_Unacked_Record(manager, &record); + if (result == IEC104_SEND_MANAGER_EMPTY) { + return 0; + } + if (result != IEC104_SEND_MANAGER_OK || now_ms < record.written_at_ms) { + return -1; + } + return Deadline_Remaining(now_ms, record.written_at_ms, t1_ms) == 0U; +} + +/** + * 功能:读取同时满足本地完成和对端确认的队首业务回执。 + * 输入参数:manager——存储结构有效的发送管理器。 + * 输出参数:record——仅OK时写入回执副本;其余结果保持原值。 + * 返回值:OK表示队首回执就绪;EMPTY表示无记录;WAIT表示队首尚未同时满足两个条件; + * ERROR表示参数、存储或记录非法。 + * 调用方:Connection代TcpServer查询;返回OK后,TcpServer复制RECORD事件,再经Connection调用Remove。 + * 说明:只检查队首,不跳过尚未完成的旧记录;不依赖运行中的Link状态。 + * 不自动释放记录;事件成功保存后才调用Remove_Confirmed_Record,尚未移除的回执仍占容量。 + */ +IEC104_SendManagerResult_t IEC104_SendManager_Peek_Confirmed_Record( + const IEC104_SendManager_t *manager, IEC104_IFrameSendRecord_t *record) +{ + IEC104_IFrameSendRecord_t prepared; + IEC104_SendManagerResult_t result; + + if (record == NULL) { + return IEC104_SEND_MANAGER_ERROR; + } + result = IEC104_SendManager_Get_Record_By_Offset(manager, 0U, &prepared); + if (result != IEC104_SEND_MANAGER_OK) { + return result; + } + if (prepared.local_send_completed == 0U || prepared.peer_acknowledged == 0U) { + return IEC104_SEND_MANAGER_WAIT; + } + *record = prepared; + return IEC104_SEND_MANAGER_OK; +} + +/** + * 功能:在TcpServer成功保存RECORD事件后,按frame_id释放已就绪的队首内部记录。 + * 输入参数:manager——存储结构有效的发送管理器。 + * frame_id——已读取并成功复制为事件的队首回执身份。 + * 输出参数:manager——成功时推进record_head_index并减少record_count。 + * 返回值:OK表示释放成功;ERROR表示参数、记录、frame_id非法,或队首尚未就绪/不存在。 + * 调用方:Connection接到TcpServer的记录移除请求后,用Peek_Confirmed_Record返回的frame_id调用一次。 + * 说明:失败不修改记录;成功只释放业务记录槽,不改变Link序号、ACK或原始ASDU存储。 + */ +IEC104_SendManagerResult_t IEC104_SendManager_Remove_Confirmed_Record( + IEC104_SendManager_t *manager, IEC104_SendFrameId_t frame_id) +{ + IEC104_IFrameSendRecord_t record; + + if (IEC104_SendManager_Peek_Confirmed_Record(manager, &record) != IEC104_SEND_MANAGER_OK || + frame_id.connection_generation != record.frame_id.connection_generation || frame_id.frame_serial != record.frame_id.frame_serial) { + return IEC104_SEND_MANAGER_ERROR; + } + manager->record_head_index = Record_Index_From_Offset(manager, 1U); + manager->record_count--; + return IEC104_SEND_MANAGER_OK; +} + +/** + * 功能:终止Manager,并在自身存储结构有效时终止绑定的SendBuffer。 + * 输入参数:manager——待终止对象,允许NULL。 + * 输出参数:manager——非NULL时清零;AsduQueue及调用方记录数组保留。 + * 返回值:无。 + * 说明:先结束旧I/O并读取需要的业务记录;清零后不能再通过Manager取得这些回执。 + * AsduQueue.Rebind清空HIGH并恢复NORMAL现存原文;明确全部丢弃须另调AsduQueue.Abort。 + * 本函数不关闭socket、不回滚Link,不能代替旧异步操作的结束或隔离。 + */ +void IEC104_SendManager_Abort(IEC104_SendManager_t *manager) +{ + if (manager != NULL) { + if (Manager_Storage_Valid(manager)) { + IEC104_SendBuffer_Abort(manager->send_buffer); + } + memset(manager, 0, sizeof(*manager)); + } +} + +/* ==================== 子站:STARTDT_CON / STOPDT_CON ==================== */ + +/* Link已生成启动应答后提交;仅每轮首个CON清理HIGH,重复应答保留队列及公平配额。 */ +static IEC104_SendManagerResult_t Commit_Startdt_Con( + IEC104_SendManager_t *manager, const IEC104_SendBufferReservation_t *reservation, + uint64_t now_ms, int first_start) +{ + if (first_start) { + if (IEC104_AsduQueue_Reset_High(manager->asdu_queue, manager->connection_generation) != IEC104_ASDU_QUEUE_OK) { + return IEC104_SEND_MANAGER_FATAL; + } + manager->high_priority_burst_count = 0U; + } + return Commit_Control_Frame(manager, reservation, IEC104_SEND_BUFFER_FRAME_STARTDT_CON, now_ms); +} + +/** + * 先取得控制槽再受理START;受理时立即激活并结束停止等待,CON按FIFO发送。 + * WAIT保留原请求、状态和期限;相反方向ACT不暂存。 + * 保留本项目的队列约定:从停用状态重新启动清理HIGH,已激活时重复START保留HIGH。 + */ +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) +{ + IEC104_SendBufferReservation_t reservation; + IEC104_SendManagerResult_t result; + int first_start; + + if (apci == NULL || apci->frame_type != IEC104_FRAME_TYPE_U || apci->u_function != IEC104_U_STARTDT_ACT) { + return IEC104_SEND_MANAGER_ERROR; + } + result = Reserve_Control_Frame(manager, connection_generation, &reservation); + if (result != IEC104_SEND_MANAGER_OK) { + return result; + } + first_start = manager->link_state->dt_state == IEC104_DT_STOPPED; + if (IEC104_Link_Handle_Startdt_Act(manager->link_state, apci, + reservation.apdu, reservation.apdu_capacity) != IEC104_LINK_RESULT_OK) { + return Cancel_Control_Reservation(manager, &reservation, IEC104_SEND_MANAGER_FATAL); + } + return Commit_Startdt_Con(manager, &reservation, now_ms, first_start); +} + +/** + * STOP受理立即停用并清理HIGH;当前I帧和已有控制帧仍须按原顺序完成。 + * 排空中的重复STOP返回IGNORED;控制队列满也可受理STOP,不暂存相反请求。 + */ +IEC104_SendManagerResult_t IEC104_SendManager_Slave_Handle_Stopdt_Act( + IEC104_SendManager_t *manager, uint64_t connection_generation, const IEC104_ApciInfo_t *apci) +{ + IEC104_LinkResult_t result; + if (apci == NULL || apci->frame_type != IEC104_FRAME_TYPE_U || apci->u_function != IEC104_U_STOPDT_ACT || + !Manager_Session_Valid(manager, connection_generation)) { + return IEC104_SEND_MANAGER_ERROR; + } + result = IEC104_Link_Handle_Stopdt_Act(manager->link_state, apci); + if (result == IEC104_LINK_RESULT_OK) { + if (IEC104_AsduQueue_Reset_High(manager->asdu_queue, connection_generation) != IEC104_ASDU_QUEUE_OK) { + return IEC104_SEND_MANAGER_FATAL; + } + return IEC104_SEND_MANAGER_OK; + } + return result == IEC104_LINK_RESULT_IGNORED ? IEC104_SEND_MANAGER_IGNORED : IEC104_SEND_MANAGER_FATAL; +} + +/** + * 为STOP排队S或CON;stop_con_id仅在STOP_CON成功入队时写出,S/等待/失败不修改。 + * Connection用该帧身份结束本轮STOP总计时,旧CON完成不能结束后续STOP的期限。 + */ +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) +{ + IEC104_SendBufferReservation_t reservation; + IEC104_SendManagerResult_t result = Reserve_Control_Frame(manager, connection_generation, &reservation); + IEC104_StopdtPrepareResult_t prepared; + IEC104_StopdtTx_t tx; + + if (result != IEC104_SEND_MANAGER_OK) { + return result; + } + prepared = IEC104_Link_Prepare_Stopdt_Response(manager->link_state, reservation.apdu, + reservation.apdu_capacity, &tx); + switch (prepared) { + case IEC104_STOPDT_FRAME_READY: + if (tx.frame_length != IEC104_APCI_LEN || + (tx.frame_type != IEC104_STOPDT_FRAME_S_ACK && tx.frame_type != IEC104_STOPDT_FRAME_CON)) { + return IEC104_SEND_MANAGER_FATAL; + } + result = Commit_Control_Frame(manager, &reservation, + tx.frame_type == IEC104_STOPDT_FRAME_S_ACK ? IEC104_SEND_BUFFER_FRAME_S_ACK : + IEC104_SEND_BUFFER_FRAME_STOPDT_CON, now_ms); + if (result == IEC104_SEND_MANAGER_QUEUED && tx.frame_type == IEC104_STOPDT_FRAME_CON && + stop_con_id != NULL) { + *stop_con_id = reservation.frame_id; + } + return result; + case IEC104_STOPDT_NOT_ACTIVE: + result = IEC104_SEND_MANAGER_EMPTY; + break; + case IEC104_STOPDT_WAIT_SEND: + case IEC104_STOPDT_WAIT_PEER_ACK: + result = IEC104_SEND_MANAGER_WAIT; + break; + default: + result = IEC104_SEND_MANAGER_ERROR; + break; + } + return Cancel_Control_Reservation(manager, &reservation, result); +} + + + +/* ==================== 子站初始化 ==================== */ + +/** + * 功能:绑定子站新连接的业务队列、发送队列、链路状态和业务记录数组。 + * 输入参数:asdu_queue——已初始化或Rebind的业务队列;允许非空,但不能有保留或已覆盖的待确认条目。 + * send_buffer——已初始化的发送缓冲,须与业务队列同代次且无当前I帧、控制帧或预留。 + * link_state——已按CONTROLLED角色State_Init的初始链路,序号为0,各过程空闲。 + * records——调用方持有的固定存储记录数组;帧身份只存frame_serial,共用Manager代次。 + * record_capacity——记录容量,范围1..32767。 + * send_window_k——未确认I帧窗口上限,范围1..32767。 + * high_priority_burst_limit——0表示严格高优先;非0限制NORMAL有未提交条目时连续HIGH交接次数。 + * 输出参数:manager——仅成功时整体写入绑定结果;asdu_queue——成功时清空HIGH未提交条目,保留NORMAL;ERROR均不修改。 + * 返回值:OK表示初始化成功;ERROR表示参数或初始状态非法。 + * 说明:所有对象不得重叠,须持续有效且按连接串行访问;不清零records数组。 + * 绑定后I帧由Prepare_Next_Send或Submit_Response统一准备,I/S接收经Handle_I_S_Frame,发送进度经Handle_Send_Progress提交。 + * S/TESTFR及子站启停由本模块封装构帧入队,完成结果直接提交对应Link接口。 + * 不接管运行中的连接,不创建TCP、线程或定时器。 + * 发送缓冲空闲及链路初始状态由各模块查询判断;查询成功后核对Link的固定角色配置。 + * 非CONTROLLED链路返回ERROR;初始化后不得改写Link角色或绑定对象。 + */ +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) +{ + IEC104_SendManager_t prepared; + IEC104_LinkStatus_t link_status; + IEC104_AsduQueueStatus_t asdu_status; + IEC104_SendBufferStatus_t buffer_status; + + if (manager == NULL || records == NULL || record_capacity == 0U || + record_capacity > IEC104_SEQUENCE_MASK || send_window_k == 0U || send_window_k > IEC104_SEQUENCE_MASK || + IEC104_Link_Is_Initial_State(link_state) != 1 || IEC104_Link_Get_Status(link_state, &link_status) != 1 || + link_status.role != IEC104_ROLE_CONTROLLED || + IEC104_AsduQueue_Get_Status(asdu_queue, &asdu_status) != IEC104_ASDU_QUEUE_OK || + IEC104_SendBuffer_Get_Status(send_buffer, &buffer_status) != IEC104_SEND_BUFFER_OK || + asdu_status.connection_generation != buffer_status.connection_generation || + asdu_status.retained_unacked_count[IEC104_ASDU_PRIORITY_HIGH] != 0U || + asdu_status.retained_unacked_count[IEC104_ASDU_PRIORITY_NORMAL] != 0U || + asdu_status.overwritten_unacked_count != 0U || buffer_status.is_idle != 1U) { + return IEC104_SEND_MANAGER_ERROR; + } + memset(&prepared, 0, sizeof(prepared)); + prepared.asdu_queue = asdu_queue; + prepared.send_buffer = send_buffer; + prepared.link_state = link_state; + prepared.records = records; + prepared.record_capacity = record_capacity; + prepared.send_window_k = send_window_k; + prepared.high_priority_burst_limit = high_priority_burst_limit; + prepared.connection_generation = buffer_status.connection_generation; + prepared.is_initialized = 1U; + if (IEC104_AsduQueue_Reset_High(asdu_queue, prepared.connection_generation) != IEC104_ASDU_QUEUE_OK) { + return IEC104_SEND_MANAGER_ERROR; + } + *manager = prepared; + return IEC104_SEND_MANAGER_OK; +} + +/* 先完成所有可失败的校验和HIGH清理,再向最终地址提交;不读取旧目标对象的未初始化内容。 + * 绑定关系只在Manager内部建立,不提供对运行会话重新绑定指针的入口。 */ +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) +{ + IEC104_SendManager_t prepared; + IEC104_LinkState_t prepared_link; + IEC104_SendBuffer_t prepared_buffer; + + if (manager == NULL || send_buffer == NULL || link_state == NULL || + IEC104_Link_State_Init(&prepared_link, IEC104_ROLE_CONTROLLED) != 1 || + IEC104_SendBuffer_Init(&prepared_buffer, control_frames, control_capacity, connection_generation) != + IEC104_SEND_BUFFER_OK || + IEC104_SendManager_Init(&prepared, asdu_queue, &prepared_buffer, &prepared_link, records, record_capacity, + send_window_k, high_priority_burst_limit) != IEC104_SEND_MANAGER_OK) { + return IEC104_SEND_MANAGER_ERROR; + } + *link_state = prepared_link; + *send_buffer = prepared_buffer; + prepared.link_state = link_state; + prepared.send_buffer = send_buffer; + *manager = prepared; + return IEC104_SEND_MANAGER_OK; +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_SendManager.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_SendManager.h new file mode 100644 index 00000000..ad684f6f --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_SendManager.h @@ -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 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 + +/* 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); +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Slave.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Slave.h new file mode 100644 index 00000000..596618b8 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Slave.h @@ -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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_TcpServer.c b/IEC60870-Slave/.build/command-v1/before/104/IEC104_TcpServer.c new file mode 100644 index 00000000..fa99ca5b --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_TcpServer.c @@ -0,0 +1,2160 @@ +/* + * 子站TCP服务调度:管理监听调度、单条活动连接、收发预算及业务事件队列。 + * 上层IEC104_Slave拥有本模块句柄,并把原始I帧事件包装为ASDU视图和共用发送接口。 + * 本模块不依赖Slave的结构或业务函数;Slave只通过本模块的公开API驱动同一套调度。 + * 本文件通过IEC104_Net执行网络操作,不包含操作系统socket类型或错误码。 + * IEC104启停、序号、组帧和超时规则由Connection及其内部模块维护。 + * + * 调用主线:Create -> Init -> Prepare/应用等待/Process/NextEvent -> Close/NextEvent -> Destroy。 + * Poll组合相同的准备、等待、推进过程;只有等待步骤调用Net_Wait。 + * 接收主线:recv -> Feed按消费量推进 -> 复制I帧事件 -> Release结束原文借用。 + * 发送主线:事件入队、应答直接准备或入HIGH -> 共用Write_Output写入及Progress记账。 + * 启停通知:START/STOP受理后立即排入ACTIVATED/DEACTIVATED,与CON发送完成解耦。 + * 通知容量:普通、启停及收尾分别保证空间;全部通知按顺序保留,不覆盖、不合并。 + * 启停专用槽满时暂缓受理下一个改变状态的ACT,NextEvent释放槽位后继续;已有CON仍可发送。 + * I帧无普通空间时保留原帧并暂停续收,NextEvent释放容量后恢复;不因通知积压主动断连。 + * 请求、回执及连接建立/断开事件不覆盖;协议期限不因等待业务消费而延长。 + * 对象边界:Slave或独立TCP调用方只持有本模块句柄;Connection内含Manager、Receiver、Link及SendBuffer。 + * NORMAL由本层缓存到服务队列;应答经Connection/SendManager验证会话后直接准备或入HIGH。 + * 事件由NextEvent按值取出,应用主循环自行处理;TCP内部不调用业务处理函数。 + * + * 平台差异由链接的Net实现处理;Connection只接收字节、发送进度和单调时间值。 + * 暂不可读写、部分发送、EOF排空、故障收尾和重连策略仍由本调度层统一处理。 + */ +#include "IEC104_TcpServer.h" +#include "IEC104_TcpServer_Config.h" +#include "IEC104_Connection.h" +#include "IEC104_Net.h" + +#include +#include + +/* 环内只保存头部和对应公开载荷的有效前缀;包含ABI填充,通过memcpy访问,允许跨环尾。 */ +typedef struct { + uint64_t generation; /* 入队时的连接代次;断线后仍按原身份交付,不能替换为当前代次。 */ + uint16_t payload_length; /* 头部之后的实际字节数,不含本结构;I帧只保存有效APDU前缀。 */ + IEC104_TcpEventType_t type; /* 决定载荷布局和出队校验规则,不能把环中数据直接当公开union使用。 */ +} TcpEventHeader_t; + +/* 事件按有效字节存储,配置条数按最长载荷折算保证容量,短事件可容纳更多条。 + * 普通区用于I帧、正常回执和建连;启停区保留状态通知;收尾区容纳一个保留I帧、全部记录和断开。 + * 三类逻辑配额共用一个FIFO,避免断线或状态变化时因普通区已满而丢通知。 */ +#define TCP_EVENT_I_PREFIX offsetof(IEC104_TcpIFrameEvent_t, apdu) +#define TCP_EVENT_I_MAX_PAYLOAD (TCP_EVENT_I_PREFIX + sizeof(((IEC104_TcpIFrameEvent_t *)0)->apdu)) +#define TCP_EVENT_I_MAX_BYTES (sizeof(TcpEventHeader_t) + TCP_EVENT_I_MAX_PAYLOAD) +#define TCP_EVENT_I_MIN_BYTES (sizeof(TcpEventHeader_t) + TCP_EVENT_I_PREFIX + IEC104_APCI_LEN) +#define TCP_EVENT_RECORD_BYTES (sizeof(TcpEventHeader_t) + sizeof(IEC104_TcpRecordEvent_t)) +#define TCP_EVENT_CONNECTION_BYTES (sizeof(TcpEventHeader_t) + sizeof(IEC104_TcpConnectionEvent_t)) +#define TCP_EVENT_NORMAL_BYTES (IEC104_TCP_EVENT_NORMAL_CAPACITY * TCP_EVENT_I_MAX_BYTES) +#define TCP_EVENT_STATE_BYTES (IEC104_TCP_EVENT_STATE_RESERVE * TCP_EVENT_CONNECTION_BYTES) +#define TCP_EVENT_CLOSE_BYTES \ + (TCP_EVENT_I_MAX_BYTES + IEC104_TCP_RECORD_CAPACITY * TCP_EVENT_RECORD_BYTES + TCP_EVENT_CONNECTION_BYTES) +#define TCP_EVENT_CAPACITY_BYTES (TCP_EVENT_NORMAL_BYTES + TCP_EVENT_STATE_BYTES + TCP_EVENT_CLOSE_BYTES) + +/* 公开头文件只声明struct名称,完整成员在此定义;业务只能使用指针和公开接口。 + * 静态池保存真实类型的对象,尺寸、对齐和地址稳定性由编译器保证。 + * Connection内部有指向自身成员的借用指针,初始化后不能复制或移动整个server。 + * queue及其字节数组随服务初始化,独立于Connection保存;无客户端时仍可缓存NORMAL原文。 + * generation用于业务应答绑定;listen_token/client_token用于排除迟到的事件循环通知。 + * 这些编号均不能只用socket数值替代,因为操作系统可能很快复用已关闭的socket值。 */ +struct IEC104_TcpServer { + IEC104_NetSocket_t listen_socket; /* 监听句柄,客户端断开后继续保留。 */ + IEC104_NetSocket_t client_socket; /* 唯一活动主站;无连接时为NET_INVALID_SOCKET。 */ + IEC104_TcpPeerInfo_t peer; /* 接入时保存的对端地址及代次;收尾事件复制后清零。 */ + IEC104_TcpServerConfig_t config; /* Init时复制的配置,运行期间不借用业务配置地址。 */ + IEC104_Connection_t connection; /* 当前协议会话;负责104分帧、序号、启停和期限。 */ + IEC104_AsduQueue_t queue; /* 两级ASDU原文队列,由本层管理跨会话生命周期。 */ + IEC104_SendBufferControlFrame_t controls[IEC104_TCP_CONTROL_CAPACITY]; /* 借给SendBuffer的S/U帧槽。 */ + IEC104_IFrameSendRecordStorage_t records[IEC104_TCP_RECORD_CAPACITY]; /* 借给Manager的I帧记录槽。 */ + uint8_t event_bytes[TCP_EVENT_CAPACITY_BYTES]; /* 变长业务事件FIFO,Close后仍可读取。 */ + uint8_t high_bytes[IEC104_TCP_HIGH_CAPACITY]; /* HIGH原文环,保存当前会话的请求应答。 */ + uint8_t normal_bytes[IEC104_TCP_NORMAL_CAPACITY]; /* NORMAL服务原文环,跨断线保留,Close清空。 */ + uint8_t rx_bytes[IEC104_TCP_RX_CAPACITY]; /* recv批量暂存区,包含尚未Feed的输入尾部。 */ + uint32_t rx_begin; /* 未消费输入的起始下标。 */ + uint32_t rx_end; /* 有效输入末尾的后一位置;区间为[rx_begin, rx_end)。 */ + uint32_t rx_snapshot_remaining; /* 预算让出时已到达内核、尚未recv的固定字节数。 */ + uint32_t event_head; /* 最早未读事件头部的字节下标。 */ + uint32_t event_used; /* 头部及有效载荷占用字节数,跨环尾不留空隙。 */ + uint16_t event_count; /* 普通、启停及收尾通知的未读总数。 */ + uint16_t state_event_count; /* FIFO中尚未消费的启停通知数,单独占用启停配额。 */ + uint64_t generation; /* 本静态槽的会话代次;Close、重新Init及槽复用均不重置。 */ + uint64_t registration_serial; /* 本静态槽已分配的socket注册编号,Reset及槽复用不清零。 */ + uint64_t listen_token; /* 当前监听的注册身份,也标识本次Init。 */ + uint64_t client_token; /* 当前客户端的注册身份,不能以socket数值代替。 */ + uint64_t accept_retry_at_ms; /* 仅now小于此期限时暂停接入;到期不依赖容量或额外解除动作。 */ + uint64_t send_retry_at_ms; /* 发送无进展后的重试期限;到期主动重试,不依赖新的可写边沿。 */ + uint32_t consecutive_accept_failures; /* 接入连续错误返回次数,饱和递增,OK/WOULD_BLOCK清零。 */ + unsigned send_retry_count; /* 跨Process及快速应答累计连续OK+0/RETRY次数,达到预算后暂停。 */ + unsigned event_budget; /* 本次推进剩余通知交付数;收尾交付不受预算限制。 */ + IEC104_TcpFault_t local_fault; /* TcpServer自身的首次故障;不要求Connection了解通知FIFO。 */ + uint16_t bound_port; /* 实际监听端口,配置port=0时记录系统分配值。 */ + int last_socket_error; /* 历史后端诊断;本次API的失败原因通过独立error输出。 */ + int last_accept_error; /* 最近一次接入错误,客户端收尾及接入成功不清除。 */ + uint8_t is_initialized; /* 已成功初始化网络并监听;不表示当前有客户端。 */ + uint8_t in_poll; /* 推进、应答发送、Close_Connection及Close执行中的重入标志,不是线程锁。 */ + uint8_t queue_initialized; /* 服务队列已绑定存储且可用;解除旧绑定失败时置0,必须Close。 */ + uint8_t rx_blocked; /* 最近Feed返回WAIT;暂不recv,先推进发送或期限。 */ + uint8_t rx_event_pending; /* 已受理I帧因普通通知空间不足仍留在Receiver,暂停续收。 */ + uint8_t rx_eof; /* 已收到有序EOF;拒绝新HIGH,停止选取未编号NORMAL后继续收尾。 */ + uint8_t accept_pending; /* 接入就绪尚未耗尽,跨预算及暂停保留,支持边沿触发续跑。 */ + uint8_t rx_ready; /* 接收就绪尚未读至WOULD_BLOCK,跨事件交付/预算保留。 */ + uint8_t rx_snapshot_active; /* 续收旧快照,不把后来到达的输入加入旧批次。 */ + uint8_t write_pending; /* 待发字节;发送暂停时屏蔽可写关注。 */ + uint8_t event_yield; /* 本轮已交付足量通知,返回应用一次;下轮无条件恢复推进。 */ + uint8_t process_pending; /* 本地状态改变后还需推进一次,不依赖新的socket边沿。 */ +}; + +/* 池的占用信息与运行对象分开,Init重置运行状态不会归还正在使用的槽。 */ +typedef struct { + IEC104_TcpServer_t server; /* 地址固定的运行对象,Create返回此成员地址。 */ + uint8_t in_use; /* Create占用、Destroy归还;Close不会清除此标志。 */ +} TcpServerSlot_t; + +_Static_assert(IEC104_TCP_SERVER_CAPACITY > 0U, "Server pool must not be empty"); +_Static_assert(sizeof(((IEC104_TcpPeerInfo_t *)0)->address) == sizeof(((IEC104_NetPeer_t *)0)->address), + "Public peer address storage must match the network endpoint"); +_Static_assert(IEC104_TCP_RX_CAPACITY > 0U && IEC104_TCP_RX_CAPACITY <= UINT32_MAX, + "Receive storage must fit its indices"); +/* 公开提交接口支持全部合法ASDU长度;每级均须容纳一条最大ASDU,包括条目头。 */ +_Static_assert(IEC104_TCP_HIGH_CAPACITY >= IEC104_ASDU_HIGH_ENTRY_HEADER_LEN + IEC104_MAX_ASDU_LEN && + IEC104_TCP_HIGH_CAPACITY <= UINT32_MAX, "HIGH queue must hold a maximum-length ASDU and fit uint32_t"); +_Static_assert(IEC104_TCP_NORMAL_CAPACITY >= IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN + IEC104_MAX_ASDU_LEN && + IEC104_TCP_NORMAL_CAPACITY <= UINT32_MAX, "NORMAL queue must hold a maximum-length ASDU and fit uint32_t"); +_Static_assert(IEC104_TCP_CONTROL_CAPACITY > 0U && IEC104_TCP_CONTROL_CAPACITY <= UINT16_MAX, + "Control storage must fit its indices"); +_Static_assert(IEC104_TCP_RECORD_CAPACITY > 0U && IEC104_TCP_RECORD_CAPACITY <= IEC104_SEQUENCE_MASK, + "Invalid send record capacity"); + +static TcpServerSlot_t server_pool[IEC104_TCP_SERVER_CAPACITY]; + +/* 先比较池内地址,再读取占用状态;公开接口不解引用未经Create取得的对象。 + * 只能判断该地址当前是否被占用;槽复用后无法识别旧指针副本,业务须在Destroy后停止使用旧句柄。 */ +static int Server_Owned(const IEC104_TcpServer_t *server) +{ + size_t index; + for (index = 0U; index < IEC104_TCP_SERVER_CAPACITY; ++index) { + if (server == &server_pool[index].server) { + return server_pool[index].in_use != 0U; + } + } + return 0; +} + +/* 在调用方提供的存储中完整写出本次结果;不修改server,也不读取历史网络错误。 + * 无效句柄也能返回详情,Destroy重置对象后仍可交付结果;成功明确清除旧诊断。 */ +static IEC104_TcpResult_t Result_With_Error( + IEC104_TcpResult_t result, IEC104_TcpErrorStage_t stage, int socket_error, IEC104_TcpError_t *error) +{ + if (error != NULL) { + error->result = result; + error->stage = stage; + error->socket_error = socket_error; + } + return result; +} + +/* 服务操作共用句柄/重入检查;必须先确认池内所有权,再解引用server。 + * 此处不要求已经Init,便于初始化失败后查询、Close以及Destroy。 */ +static IEC104_TcpResult_t Check_Server(const IEC104_TcpServer_t *server, IEC104_TcpError_t *error) +{ + if (!Server_Owned(server)) { + return Result_With_Error(IEC104_TCP_INVALID_HANDLE, IEC104_TCP_STAGE_VALIDATE, 0, error); + } + if (server->in_poll != 0U) { + return Result_With_Error(IEC104_TCP_BUSY, IEC104_TCP_STAGE_LIFECYCLE, 0, error); + } + return Result_With_Error(IEC104_TCP_OK, IEC104_TCP_STAGE_NONE, 0, error); +} + +/* 只用于没有网络资源及未读事件的对象;保留业务代次和注册编号,隔离槽复用前的通知。 */ +static void Reset_Server(IEC104_TcpServer_t *server) +{ + uint64_t generation = server->generation; + uint64_t registration_serial = server->registration_serial; + memset(server, 0, sizeof(*server)); + server->generation = generation; + server->registration_serial = registration_serial; + server->listen_socket = IEC104_NET_INVALID_SOCKET; + server->client_socket = IEC104_NET_INVALID_SOCKET; +} + +/* 每轮Process最多执行32次Feed和32次send;Poll复用同一轮Process。 + * Feed和send各有独立预算;接收WAIT时的解阻发送与正常发送共用send预算,持续输入也不能无限占用线程。 */ +#define TCP_IO_BUDGET 32U +/* 额外连接也计入accept预算,避免连接请求持续到达时饿死当前主站收发。 */ +#define TCP_ACCEPT_BUDGET 4U +/* 接入资源暂缺或整轮仅有RETRY时暂停100毫秒,不阻塞当前客户端收发,不提供业务配置项。 */ +#define TCP_ACCEPT_RETRY_DELAY_MS 100U +/* 连续无进展最多尝试4次,然后暂停发送10毫秒;正常WOULD_BLOCK仍等待可写通知。 */ +#define TCP_SEND_RETRY_BUDGET 4U +#define TCP_SEND_RETRY_DELAY_MS 10U +/* 有界交付后及时交还业务消费NextEvent,避免一批粘包长期占用事件循环。 */ +#define TCP_EVENT_BUDGET 8U + +/* 配置仍表示保证条数;字节索引用uint32_t,公开未读通知数仍用uint16_t。 */ +_Static_assert(IEC104_TCP_EVENT_STATE_RESERVE > 0U && IEC104_TCP_EVENT_STATE_RESERVE <= UINT16_MAX, + "State event reserve must fit in uint16_t"); +_Static_assert(IEC104_TCP_EVENT_CAPACITY > IEC104_TCP_EVENT_CLOSE_RESERVE + IEC104_TCP_EVENT_STATE_RESERVE, + "Event queue must leave room for normal events"); +_Static_assert(IEC104_TCP_EVENT_CAPACITY <= UINT16_MAX, + "Event guarantees must fit in uint16_t"); +_Static_assert(TCP_EVENT_I_MAX_PAYLOAD <= UINT16_MAX && + sizeof(IEC104_TcpRecordEvent_t) <= UINT16_MAX && sizeof(IEC104_TcpConnectionEvent_t) <= UINT16_MAX, + "Event payload lengths must fit in uint16_t"); +_Static_assert(TCP_EVENT_I_MAX_BYTES >= TCP_EVENT_RECORD_BYTES && + TCP_EVENT_I_MAX_BYTES >= TCP_EVENT_CONNECTION_BYTES && + TCP_EVENT_I_MIN_BYTES <= TCP_EVENT_RECORD_BYTES && TCP_EVENT_I_MIN_BYTES <= TCP_EVENT_CONNECTION_BYTES, + "I frame bounds must cover all event sizes"); +_Static_assert(TCP_EVENT_CAPACITY_BYTES <= UINT32_MAX, + "Event byte storage must fit its indices"); +_Static_assert(TCP_EVENT_CAPACITY_BYTES / TCP_EVENT_I_MIN_BYTES <= UINT16_MAX, + "Even shortest events must fit the public event count"); + +/* 仅ACTIVATED/DEACTIVATED占启停配额;CONNECTED/DISCONNECTED沿普通/收尾容量规则计账。 */ +static int Is_State_Event(IEC104_TcpEventType_t type) +{ + return type == IEC104_TCP_EVENT_ACTIVATED || type == IEC104_TCP_EVENT_DEACTIVATED; +} + +/* 只检查游标和计数;每项的类型、长度在出队前校验,不遍历整个FIFO。 */ +static int Event_Queue_Valid(const IEC104_TcpServer_t *server) +{ + return server->event_head < sizeof(server->event_bytes) && + server->event_used <= sizeof(server->event_bytes) && + server->state_event_count <= IEC104_TCP_EVENT_STATE_RESERVE && + server->state_event_count <= server->event_count && + server->state_event_count * TCP_EVENT_CONNECTION_BYTES <= server->event_used && + server->event_count <= server->event_used / TCP_EVENT_I_MIN_BYTES && + (server->event_count == 0U) == (server->event_used == 0U); +} + +/* 返回环内偏移前进length后的下标;调用方保证index在环内、length不超过整个环容量。 + * 先比较尾部余量,避免index+length溢出;头部和载荷均允许跨环尾。 */ +static uint32_t Event_Ring_Advance(uint32_t index, uint32_t length) +{ + uint32_t tail_bytes = (uint32_t)TCP_EVENT_CAPACITY_BYTES - index; + return length >= tail_bytes ? length - tail_bytes : index + length; +} + +/** + * 功能:从事件字节环复制length字节,跨环尾时分两段读取,不移动FIFO游标。 + * 前提:index在环内,length不超过环容量,data可写空间充足且不与环存储重叠。 + * 说明:调用方负责校验队列和条目长度;memcpy避免未对齐的结构体指针访问。 + */ +static void Event_Ring_Read(const IEC104_TcpServer_t *server, uint32_t index, void *data, uint32_t length) +{ + uint32_t first_length = (uint32_t)sizeof(server->event_bytes) - index; + if (first_length > length) { + first_length = length; + } + memcpy(data, server->event_bytes + index, first_length); + if (length > first_length) { + memcpy((uint8_t *)data + first_length, server->event_bytes, length - first_length); + } +} + +/** + * 功能:将length字节复制到事件环,跨环尾时分两段写入,不修改事件计数或占用量。 + * 前提:调用方已检查目标空间,index在环内、length不超过环容量,输入不与环存储重叠。 + * 说明:Append_Event完成头部和载荷复制后统一提交占用量,避免把未写完的条目发布给出队接口。 + */ +static void Event_Ring_Write(IEC104_TcpServer_t *server, uint32_t index, const void *data, uint32_t length) +{ + uint32_t first_length = (uint32_t)sizeof(server->event_bytes) - index; + if (first_length > length) { + first_length = length; + } + memcpy(server->event_bytes + index, data, first_length); + if (length > first_length) { + memcpy(server->event_bytes, (const uint8_t *)data + first_length, length - first_length); + } +} + +/** + * 功能:按事件类型检查环中载荷长度,入队和出队共用此格式约束。 + * 返回值:1为长度匹配,0为未知类型或长度越界;本函数不检查载荷内部字段。 + * 说明:I帧可变长,其余载荷为固定结构;APDU长度与载荷长度的一致性由NextEvent另行核对。 + */ +static int Event_Payload_Length_Valid(IEC104_TcpEventType_t type, uint16_t length) +{ + switch (type) { + case IEC104_TCP_EVENT_I_FRAME: + return length >= TCP_EVENT_I_PREFIX + IEC104_APCI_LEN && length <= TCP_EVENT_I_MAX_PAYLOAD; + case IEC104_TCP_EVENT_RECORD: + return length == sizeof(IEC104_TcpRecordEvent_t); + case IEC104_TCP_EVENT_CONNECTED: + case IEC104_TCP_EVENT_DISCONNECTED: + case IEC104_TCP_EVENT_ACTIVATED: + case IEC104_TCP_EVENT_DEACTIVATED: + return length == sizeof(IEC104_TcpConnectionEvent_t); + default: + return 0; + } +} + +/* 非启停项均计入普通占用,包括上一连接尚未读完的收尾项。 + * 收尾后该占用可超过普通配额;降回配额内才能接入新连接并复用收尾预留。 + * 此配额与允许覆盖旧ASDU原文的NORMAL发送队列独立,通知FIFO不覆盖旧项。 */ +static int Ordinary_Event_Room(const IEC104_TcpServer_t *server, size_t event_bytes) +{ + size_t state_used = server->state_event_count * TCP_EVENT_CONNECTION_BYTES; + size_t ordinary_used; + if (server->event_used < state_used) { + return 0; + } + ordinary_used = server->event_used - state_used; + return ordinary_used <= TCP_EVENT_NORMAL_BYTES && event_bytes <= TCP_EVENT_NORMAL_BYTES - ordinary_used && + server->event_used <= TCP_EVENT_CAPACITY_BYTES - TCP_EVENT_CLOSE_BYTES && + event_bytes <= TCP_EVENT_CAPACITY_BYTES - TCP_EVENT_CLOSE_BYTES - server->event_used; +} + +/* 三个启停入口共用检查:保留条数配额,写入后仍须留下完整收尾空间。 */ +static int State_Event_Room(const IEC104_TcpServer_t *server) +{ + return server->state_event_count < IEC104_TCP_EVENT_STATE_RESERVE && + server->event_used <= TCP_EVENT_CAPACITY_BYTES - TCP_EVENT_CLOSE_BYTES && + TCP_EVENT_CONNECTION_BYTES <= TCP_EVENT_CAPACITY_BYTES - TCP_EVENT_CLOSE_BYTES - server->event_used; +} + +/* 新会话要有建连通知容量、完整启停配额及完整收尾预留;旧启停通知须先读完。 */ +static int Can_Accept_Connection(const IEC104_TcpServer_t *server) +{ + return Ordinary_Event_Room(server, TCP_EVENT_CONNECTION_BYTES) && server->state_event_count == 0U; +} + +/* 预算或本类配额刚用尽时只让出当前一轮,让应用及时消费通知。 + * 普通区放不下最长通知才视为配额用尽;下一轮仍按实际长度交付,启停独立检查。 */ +static void Note_Event_Delivery(IEC104_TcpServer_t *server, int state_event) +{ + if (server->event_budget > 0U) { + --server->event_budget; + } + if (server->event_budget == 0U || (!state_event && !Ordinary_Event_Room(server, TCP_EVENT_I_MAX_BYTES)) || + (state_event && server->state_event_count >= IEC104_TCP_EVENT_STATE_RESERVE)) { + server->event_yield = 1U; + server->process_pending = 1U; + } +} + +/* 调用方先检查对应配额;这里再检查物理空间,不直接覆盖已有通知。 + * 活动连接最多占普通字节区+启停字节区;Drop_Client另外最多追加RECORD_CAPACITY+2项。 + * 改变状态的ACT在受理前检查启停配额,确保变化后有空间保存通知。 + * 先完成全部检查,再复制头部和载荷,最后提交占用量;不向调用方暴露环内地址。 */ +static int Append_Event(IEC104_TcpServer_t *server, IEC104_TcpEventType_t type, + const void *payload, uint16_t payload_length) +{ + TcpEventHeader_t header; + uint32_t index; + uint32_t event_bytes = (uint32_t)sizeof(header) + payload_length; + int state_event = Is_State_Event(type); + if (payload == NULL || server->generation == 0U || !Event_Payload_Length_Valid(type, payload_length) || + !Event_Queue_Valid(server) || server->event_count == UINT16_MAX || + event_bytes > sizeof(server->event_bytes) - server->event_used || + (state_event && !State_Event_Room(server))) { + return 0; + } + memset(&header, 0, sizeof(header)); + header.generation = server->generation; + header.payload_length = payload_length; + header.type = type; + index = Event_Ring_Advance(server->event_head, server->event_used); + Event_Ring_Write(server, index, &header, sizeof(header)); + index = Event_Ring_Advance(index, sizeof(header)); + Event_Ring_Write(server, index, payload, payload_length); + server->event_used += event_bytes; + ++server->event_count; + if (state_event) { + ++server->state_event_count; + } + return 1; +} + +/* 复制完整APDU和必要的数值元数据,绝不把Receiver内部地址保存到事件中。 */ +static int Queue_I_Frame(IEC104_TcpServer_t *server, const IEC104_ConnectionReceivedFrame_t *frame) +{ + IEC104_TcpIFrameEvent_t copy; + if (frame->frame.apdu == NULL || frame->frame.apdu_length < IEC104_APCI_LEN || + frame->frame.apdu_length > sizeof(copy.apdu)) { + return 0; + } + memset(©, 0, TCP_EVENT_I_PREFIX); + copy.receive_serial = frame->receive_serial; + copy.apdu_length = frame->frame.apdu_length; + copy.send_number = frame->frame.apci.send_number; + copy.receive_number = frame->frame.apci.receive_number; + memcpy(copy.apdu, frame->frame.apdu, copy.apdu_length); + return Append_Event(server, IEC104_TCP_EVENT_I_FRAME, ©, + (uint16_t)(TCP_EVENT_I_PREFIX + copy.apdu_length)); +} + +/* 对外发送记录只保留业务需要的事实,逐字段转换,不绑定Manager记录布局。 */ +static void Copy_Send_Record(IEC104_TcpSendRecord_t *copy, const IEC104_IFrameSendRecord_t *record) +{ + copy->frame_serial = record->frame_id.frame_serial; + copy->queued_at_ms = record->queued_at_ms; + copy->written_at_ms = record->written_at_ms; + copy->send_number = record->i_frame_snapshot.send_number; + copy->receive_number = record->i_frame_snapshot.receive_number; + copy->apdu_length = record->i_frame_snapshot.frame_length; + copy->priority = record->asdu_priority; + copy->local_send_completed = record->local_send_completed; + copy->peer_acknowledged = record->peer_acknowledged; +} + +/* 显式映射故障分类,内部新增/调整枚举不会悄悄改变业务接口的数值语义。 */ +static IEC104_TcpFault_t Public_Fault(IEC104_ConnectionFault_t fault) +{ + switch (fault) { + case IEC104_CONNECTION_FAULT_NONE: return IEC104_TCP_FAULT_NONE; + case IEC104_CONNECTION_FAULT_FRAME: return IEC104_TCP_FAULT_FRAME; + case IEC104_CONNECTION_FAULT_PROTOCOL: return IEC104_TCP_FAULT_PROTOCOL; + case IEC104_CONNECTION_FAULT_INTERNAL: return IEC104_TCP_FAULT_INTERNAL; + case IEC104_CONNECTION_FAULT_TRANSPORT: return IEC104_TCP_FAULT_TRANSPORT; + case IEC104_CONNECTION_FAULT_LOCAL_SEND_TIMEOUT: return IEC104_TCP_FAULT_LOCAL_SEND_TIMEOUT; + case IEC104_CONNECTION_FAULT_I_ACK_TIMEOUT: return IEC104_TCP_FAULT_I_ACK_TIMEOUT; + case IEC104_CONNECTION_FAULT_TESTFR_TIMEOUT: return IEC104_TCP_FAULT_TESTFR_TIMEOUT; + case IEC104_CONNECTION_FAULT_STOP_TIMEOUT: return IEC104_TCP_FAULT_STOP_TIMEOUT; + default: return IEC104_TCP_FAULT_INTERNAL; + } +} + +/* 只登记本层首次故障;公开快照仍优先保留Connection已经登记的更早故障。 */ +static void Note_Local_Fault(IEC104_TcpServer_t *server, IEC104_TcpFault_t fault) +{ + if (server->local_fault == IEC104_TCP_FAULT_NONE) { + server->local_fault = fault; + } +} + +/* 通过Connection查询,再生成稳定的业务快照;查询失败时不修改输出。 */ +static int Read_Connection_Status(const IEC104_TcpServer_t *server, IEC104_TcpConnectionStatus_t *copy) +{ + IEC104_ConnectionStatus_t status; + IEC104_TcpConnectionStatus_t snapshot; + if (server->generation == 0U || IEC104_Connection_Get_Status( + &server->connection, server->generation, &status) != IEC104_CONNECTION_OK) { + return 0; + } + memset(&snapshot, 0, sizeof(snapshot)); + switch (status.phase) { + case IEC104_CONNECTION_UNINITIALIZED: snapshot.phase = IEC104_TCP_SESSION_NONE; break; + case IEC104_CONNECTION_ACTIVE: snapshot.phase = IEC104_TCP_SESSION_ACTIVE; break; + case IEC104_CONNECTION_FAILED: snapshot.phase = IEC104_TCP_SESSION_FAILED; break; + case IEC104_CONNECTION_CLOSED: snapshot.phase = IEC104_TCP_SESSION_CLOSED; break; + default: return 0; + } + snapshot.fault = Public_Fault(status.fault); + if (snapshot.fault == IEC104_TCP_FAULT_NONE && server->local_fault != IEC104_TCP_FAULT_NONE) { + snapshot.fault = server->local_fault; + if (snapshot.phase == IEC104_TCP_SESSION_ACTIVE) { + snapshot.phase = IEC104_TCP_SESSION_FAILED; + } + } + snapshot.pending_send_records = status.send_record_count; + snapshot.data_transfer_started = status.dt_state == IEC104_DT_STARTED; + *copy = snapshot; + return 1; +} + +/** + * 功能:通过队列的只读接口汇总HIGH/NORMAL积压及容量,不向业务暴露队列布局。 + * 输出参数:high/normal仅在两个优先级均查询成功时写入,失败保持原值。 + * 返回值:1统计有效;0表示服务队列尚不可用、代次不符或内部查询异常。 + * 说明:队列随服务存在,离线代次0也能查询;不能要求Connection仍然有效。 + * 待发数=保留原文数-其中已提交待ACK数,不把NORMAL已提交待ACK原文或HIGH发送记录混入待发数。 + * 容量来自本服务实际借给队列的数组,空闲量通过队列接口查询;不读取内部环下标。 + * 所有服务接口串行执行,期间无修改或回调,多个常数时间查询属于同一状态快照。 + */ +static int Read_Asdu_Queues_Status( + const IEC104_TcpServer_t *server, IEC104_TcpAsduQueueStatus_t *high, IEC104_TcpAsduQueueStatus_t *normal) +{ + IEC104_AsduQueueStatus_t status; + IEC104_TcpAsduQueueStatus_t snapshots[IEC104_ASDU_PRIORITY_COUNT]; + unsigned priority; + if (server->is_initialized == 0U || server->queue_initialized == 0U || + IEC104_AsduQueue_Get_Status(&server->queue, &status) != IEC104_ASDU_QUEUE_OK || + status.connection_generation != + (server->client_socket == IEC104_NET_INVALID_SOCKET ? 0U : server->generation)) { + return 0; + } + memset(snapshots, 0, sizeof(snapshots)); + snapshots[IEC104_ASDU_PRIORITY_HIGH].capacity_bytes = (uint32_t)sizeof(server->high_bytes); + snapshots[IEC104_ASDU_PRIORITY_NORMAL].capacity_bytes = (uint32_t)sizeof(server->normal_bytes); + for (priority = 0U; priority < IEC104_ASDU_PRIORITY_COUNT; ++priority) { + IEC104_TcpAsduQueueStatus_t *snapshot = &snapshots[priority]; + if (IEC104_AsduQueue_Get_Free_Bytes(&server->queue, (IEC104_AsduPriority_t)priority, + &snapshot->free_bytes) != IEC104_ASDU_QUEUE_OK || + snapshot->free_bytes > snapshot->capacity_bytes || + status.retained_unacked_count[priority] > status.entry_count[priority]) { + return 0; + } + snapshot->pending_asdu_count = status.entry_count[priority] - status.retained_unacked_count[priority]; + snapshot->stored_asdu_count = status.entry_count[priority]; + snapshot->used_bytes = snapshot->capacity_bytes - snapshot->free_bytes; + } + *high = snapshots[IEC104_ASDU_PRIORITY_HIGH]; + *normal = snapshots[IEC104_ASDU_PRIORITY_NORMAL]; + return 1; +} + +/** + * 功能:读取协议及接入/发送重试期限使用的单调毫秒时刻。 + * 返回值:Net后端提供的单调毫秒值,不是日历时间。 + * 说明:所有Connection时间参数使用同一时间源,修改系统日期不改变计时基准。 + */ +static uint64_t Now_Ms(void) +{ + return IEC104_Net_Now_Ms(); +} + +/* Prepare、实际接入和状态查询共用严格小于判断;now等于期限即已到期。 */ +static int Accept_Retry_Paused(const IEC104_TcpServer_t *server, uint64_t now_ms) +{ + return now_ms < server->accept_retry_at_ms; +} + +/* 仅在实际接入失败后调用,保留待处理标志,到期后无需新的监听边沿即可重试。 */ +static void Pause_Accept(IEC104_TcpServer_t *server) +{ + server->accept_retry_at_ms = Now_Ms() + TCP_ACCEPT_RETRY_DELAY_MS; + server->accept_pending = 1U; +} + +/* Prepare与真正send前共用比较;等于期限即允许重试,不能用就绪提示提前解除暂停。 */ +static int Send_Retry_Paused(const IEC104_TcpServer_t *server, uint64_t now_ms) +{ + return now_ms < server->send_retry_at_ms; +} + +/* 清除无进展计数及暂停期限;实际写出、转为等待可写、无待发帧或连接切换时不沿用旧退避状态。 + * 不清process_pending,其他收发或通知处理可能仍需要下一轮推进。 */ +static void Reset_Send_Retry(IEC104_TcpServer_t *server) +{ + server->send_retry_at_ms = 0U; + server->send_retry_count = 0U; +} + +/* 只在实际send返回OK+0或RETRY后调用,预算跨所有发送入口累计。 + * 暂停期间不再新增发送续跑,也不清除其他工作已经登记的process_pending。 */ +static void Schedule_Send_Retry(IEC104_TcpServer_t *server) +{ + if (++server->send_retry_count >= TCP_SEND_RETRY_BUDGET) { + server->send_retry_count = 0U; + server->send_retry_at_ms = Now_Ms() + TCP_SEND_RETRY_DELAY_MS; + } else { + server->process_pending = 1U; + } +} + +/** + * 功能:在申请系统资源前检查配置范围及地址字符串是否有结束符。 + * 输入参数:config——待检查配置,允许NULL。 + * 返回值:1通过基础检查,0非法;不修改配置或协议状态。 + * 说明:此处不解析IPv4内容,具体地址格式由Init中的Net_Listen继续检查。 + */ +static int Config_Valid(const IEC104_TcpServerConfig_t *config) +{ + const IEC104_ProtocolConfig_t *protocol; + if (config == NULL || memchr(config->bind_address, '\0', sizeof(config->bind_address)) == NULL) { + return 0; + } + protocol = &config->protocol; + return protocol->link.k > 0U && protocol->link.k <= IEC104_SEQUENCE_MASK && + protocol->link.w > 0U && protocol->link.w <= IEC104_SEQUENCE_MASK && + protocol->link.t1_ms > 0U && protocol->link.t2_ms > 0U && protocol->link.t3_ms > 0U && + protocol->link.t2_ms < protocol->link.t1_ms && + protocol->local_send_timeout_ms > 0U && protocol->stop_timeout_ms > 0U; +} + +/** + * 功能:排入TCP建连或断开事件,复制协议状态和该连接的对端地址。 + * 输入参数:server——当前服务;connected——1建连,0断开。 + * records_saved——收尾时已成功保存为RECORD事件的内部记录数;建连传0。 + * 返回值:1已入队,0物理容量或收尾计数异常;调用方须结束会话或停止服务。 + * 说明:调用方已保证事件容量;查询失败或收尾计数不一致时status_valid为0。 + * 收尾记录在Close前仍占内部槽,断开快照须扣除其中已成功转为事件的记录。 + * socket_error为0不保证没有协议故障,仍须检查有效快照中的fault。 + */ +static int Notify_Connection(IEC104_TcpServer_t *server, int connected, uint16_t records_saved) +{ + IEC104_TcpConnectionEvent_t event; + int count_valid = 1; + memset(&event, 0, sizeof(event)); + event.status_valid = (uint8_t)Read_Connection_Status(server, &event.status); + if (!connected && event.status_valid != 0U) { + if (records_saved > event.status.pending_send_records) { + /* 不让计数下溢,也不把不一致的快照当成有效状态交付。 */ + event.status_valid = 0U; + count_valid = 0; + } else { + event.status.pending_send_records = (uint16_t)(event.status.pending_send_records - records_saved); + } + } + event.peer = server->peer; + event.socket_error = server->last_socket_error; + if (!Append_Event(server, connected ? IEC104_TCP_EVENT_CONNECTED : IEC104_TCP_EVENT_DISCONNECTED, + &event, sizeof(event))) { + return 0; + } + if (connected) { + Note_Event_Delivery(server, 0); + } + return count_valid; +} + +/** + * 功能:实现Connection启停观察接口的容量检查,在受理改变状态的ACT前确认通知可保存。 + * 输入参数:context——注册观察接口时传入的TcpServer地址,生命周期由本服务保证。 + * 返回值:1可产生状态通知;0暂缓受理,等待NextEvent释放启停专用配额。 + * 说明:此处只查容量,不通知业务、不改变协议;已有CON的发送不重新占用这个配额。 + */ +static int Transfer_Event_Room(void *context) +{ + const IEC104_TcpServer_t *server = (const IEC104_TcpServer_t *)context; + return State_Event_Room(server); +} + +/** + * 功能:实现Connection启停观察接口,在状态提交后复制ACTIVATED/DEACTIVATED通知。 + * 输入参数:context——所属TcpServer;state——Connection已经提交的新数据传输状态。 + * 返回值:1通知已保存并扣减交付预算;0表示容量、状态或内部入队异常。 + * 说明:只读取得协议快照,核对后立即入FIFO;不调用业务,不等CON发送完成。 + * 同状态的重复ACT不触发本通知,是否真正改变状态由Connection决定。 + */ +static int Notify_Transfer_State(void *context, IEC104_DataTransferState_t state) +{ + IEC104_TcpServer_t *server = (IEC104_TcpServer_t *)context; + IEC104_TcpConnectionEvent_t event; + if (!State_Event_Room(server)) { + return 0; + } + memset(&event, 0, sizeof(event)); + if (!Read_Connection_Status(server, &event.status) || + event.status.data_transfer_started != (state == IEC104_DT_STARTED)) { + return 0; + } + event.status_valid = 1U; + event.peer = server->peer; + if (!Append_Event(server, state == IEC104_DT_STARTED ? + IEC104_TCP_EVENT_ACTIVATED : IEC104_TCP_EVENT_DEACTIVATED, &event, sizeof(event))) { + return 0; + } + Note_Event_Delivery(server, 1); + return 1; +} + +/** + * 功能:结束当前客户端,保留监听socket供后续连接使用。 + * 输入参数:transport_closed——非零时登记传输故障,不表示普通有序EOF; + * socket_error——本次网络错误码,正常关闭或纯协议故障时可为0。 + * 说明:须在同步send/recv返回、无未完成I/O借用时调用;无客户端时直接返回。 + * 先关闭socket并禁止HIGH入队,再复制剩余I帧、记录及断开通知,最后Close协议并解除队列绑定。 + * 使用专门预留的事件容量,即使普通区满也不覆盖旧事件;断开事件最后入队。 + * 只统计成功保存的记录供断开快照扣除;读取或入队失败仍按收尾异常处理。 + * 已登记的协议/发送故障保持不变;记录中的本地完成和ACK事实不做补造。 + */ +static void Drop_Client(IEC104_TcpServer_t *server, int transport_closed, int socket_error) +{ + IEC104_IFrameSendRecord_t record; + IEC104_ConnectionReceivedFrame_t frame; + uint16_t offset; + uint16_t records_saved = 0U; + int events_saved = 1; + if (server->client_socket == IEC104_NET_INVALID_SOCKET) { + return; + } + server->last_socket_error = socket_error; + if (transport_closed && IEC104_Connection_Handle_Transport_Closed( + &server->connection, server->generation) != IEC104_CONNECTION_FATAL) { + Note_Local_Fault(server, IEC104_TCP_FAULT_INTERNAL); + } + (void)IEC104_Net_Close(server->client_socket, NULL); + server->client_socket = IEC104_NET_INVALID_SOCKET; + server->client_token = 0U; + /* Feed/Tick可能已受理一个尚未转移的I帧;使用收尾预留保存,不覆盖普通通知。 */ + if (IEC104_Connection_Peek_Received_Frame(&server->connection, server->generation, &frame) == + IEC104_CONNECTION_FRAME_READY) { + events_saved = Queue_I_Frame(server, &frame); + } + for (offset = 0U; offset < IEC104_TCP_RECORD_CAPACITY; ++offset) { + IEC104_TcpRecordEvent_t event; + IEC104_ConnectionResult_t result = IEC104_Connection_Get_Record_By_Offset( + &server->connection, server->generation, offset, &record); + if (result == IEC104_CONNECTION_EMPTY) { + break; + } + if (result != IEC104_CONNECTION_OK) { + events_saved = 0; + break; + } + memset(&event, 0, sizeof(event)); + Copy_Send_Record(&event.value, &record); + event.closing = 1U; + if (!Append_Event(server, IEC104_TCP_EVENT_RECORD, &event, sizeof(event))) { + events_saved = 0; + break; + } + records_saved++; + } + if (!events_saved) { + Note_Local_Fault(server, IEC104_TCP_FAULT_INTERNAL); + } + if (!Notify_Connection(server, 0, records_saved)) { + events_saved = 0; + Note_Local_Fault(server, IEC104_TCP_FAULT_INTERNAL); + } + /* socket已关闭,事件已取得独立副本;不让Get_Remote_Address把旧地址当成下一会话的当前地址。 */ + memset(&server->peer, 0, sizeof(server->peer)); + /* Manager结束后才能解除队列的旧会话记账;NORMAL立即恢复为离线待发,HIGH不跨连接。 + * 失败时保留原存储供Close清理,停止服务推进,不能悄悄Init而丢弃已缓存的NORMAL。 */ + if (IEC104_Connection_Close(&server->connection, server->generation) != IEC104_CONNECTION_OK || + IEC104_AsduQueue_Detach(&server->queue, server->generation) != IEC104_ASDU_QUEUE_OK || !events_saved) { + server->queue_initialized = 0U; + } + server->rx_begin = 0U; + server->rx_end = 0U; + server->rx_snapshot_remaining = 0U; + server->rx_snapshot_active = 0U; + server->rx_blocked = 0U; + server->rx_event_pending = 0U; + server->rx_eof = 0U; + server->rx_ready = 0U; + server->write_pending = 0U; + Reset_Send_Retry(server); + server->event_yield = 0U; + server->process_pending = 0U; +} + +/* 本地内部状态异常单独登记,不伪造socket错误或协议ACK;通知容量不足不调用本函数。 + * Connection已有的更早故障仍由Read_Connection_Status优先交付;本地原因保留到下一会话。 + * 收尾只复制当前已受理I帧及实际发送记录,不为未受理TCP字节生成通知。 */ +static int Drop_Local_Fault(IEC104_TcpServer_t *server, IEC104_TcpFault_t fault) +{ + Note_Local_Fault(server, fault); + Drop_Client(server, 0, 0); + return 0; +} + +/** + * 功能:统一处理内部Connection调用的致命结果,避免关闭后继续访问当前连接。 + * 返回值:ERROR/FATAL时收尾并返回0;其余结果返回1,包括正常EMPTY和WAIT。 + * 说明:FATAL已带首次故障,直接保留;内部调用出现ERROR时按INTERNAL收尾,不伪造传输故障。 + * 返回1仅表示可以继续调度,不表示有数据、操作已完成或业务已经成功。 + */ +static int Connection_Result_Valid(IEC104_TcpServer_t *server, IEC104_ConnectionResult_t result) +{ + if (result == IEC104_CONNECTION_ERROR) { + return Drop_Local_Fault(server, IEC104_TCP_FAULT_INTERNAL); + } + if (result == IEC104_CONNECTION_FATAL) { + Drop_Client(server, 0, 0); + return 0; + } + return 1; +} + +/** + * 功能:将当前已受理I帧复制到事件队列,再结束该帧原文的借用。 + * 返回值:1连接仍可继续,无就绪帧或本轮交付预算已用完也返回1;0表示已收尾。 + * 说明:普通通知空间不足时保留已受理帧,暂停后续Feed/recv;不丢弃、不回放、不主动断连。 + * NextEvent腾出空间后由Process重试;等待期间继续发送及检查原有协议期限。 + * TCP有序,保留I帧会挡住后续S/U,应用须及时消费;关闭时由收尾预留保存该帧。 + * 复制成功后业务仅使用事件副本,无需再Release或操作内部Connection。 + */ +static int Deliver_I_Frame(IEC104_TcpServer_t *server) +{ + IEC104_ConnectionReceivedFrame_t frame; + IEC104_ConnectionResult_t result = IEC104_Connection_Peek_Received_Frame( + &server->connection, server->generation, &frame); + if (result == IEC104_CONNECTION_FRAME_READY) { + if (!Ordinary_Event_Room(server, sizeof(TcpEventHeader_t) + TCP_EVENT_I_PREFIX + frame.frame.apdu_length)) { + server->rx_event_pending = 1U; + return 1; + } + server->rx_event_pending = 0U; + if (server->event_yield != 0U) { + server->process_pending = 1U; + return 1; + } + if (!Queue_I_Frame(server, &frame)) { + return Drop_Local_Fault(server, IEC104_TCP_FAULT_INTERNAL); + } + result = IEC104_Connection_Release_Received_Frame( + &server->connection, server->generation, frame.receive_serial); + /* 本轮会把新事件交还业务;业务处理后还须续收,哪怕此前的就绪已暂时耗尽。 */ + server->process_pending = 1U; + Note_Event_Delivery(server, 0); + } + return Connection_Result_Valid(server, result); +} + +/** + * 功能:按发送顺序交付已完整写出且收到有效ACK的记录,并释放记录槽。 + * 输出参数:made_progress非NULL时,成功回收至少一条记录则置1;否则保留原值。 + * 返回值:1处理完成或队首尚不可消费,0表示内部错误已触发收尾。 + * 说明:每次最多检查记录总容量次;EMPTY表示无记录,WAIT表示队首尚待确认。 + * 复制为closing=0的记录事件后才回收记录槽;通知满/预算用完则保留记录。 + * 有效ACK在协议处理中已登记,回执暂存不会要求主站重发ACK或阻止STOP确认。 + * 等待空间的I帧先交付,回执不得利用较短载荷插到它前面。 + */ +static int Consume_Records(IEC104_TcpServer_t *server, int *made_progress) +{ + IEC104_IFrameSendRecord_t record; + unsigned count; + for (count = 0U; count < IEC104_TCP_RECORD_CAPACITY && server->rx_event_pending == 0U && + server->event_yield == 0U && Ordinary_Event_Room(server, TCP_EVENT_RECORD_BYTES); ++count) { + IEC104_TcpRecordEvent_t event; + IEC104_ConnectionResult_t result = IEC104_Connection_Peek_Confirmed_Record( + &server->connection, server->generation, &record); + if (result == IEC104_CONNECTION_EMPTY || result == IEC104_CONNECTION_WAIT) { + return 1; + } + if (!Connection_Result_Valid(server, result)) { + return 0; + } + memset(&event, 0, sizeof(event)); + Copy_Send_Record(&event.value, &record); + if (!Append_Event(server, IEC104_TCP_EVENT_RECORD, &event, sizeof(event))) { + return Drop_Local_Fault(server, IEC104_TCP_FAULT_INTERNAL); + } + if (!Connection_Result_Valid(server, IEC104_Connection_Remove_Confirmed_Record( + &server->connection, record.frame_id))) { + return 0; + } + if (made_progress != NULL) { + *made_progress = 1; + } + Note_Event_Delivery(server, 0); + } + return 1; +} + +/** + * 功能:将rx_bytes中的未消费字节交给Connection,处理分包、粘包及协议背压。 + * 输入/输出参数:budget——本轮Process剩余Feed次数,每次尝试都会扣减。 + * 返回值:1连接仍可继续,包含WAIT及预算耗尽;0表示连接已经收尾。 + * 说明:半帧由Receiver保存,未消费尾部保留在[rx_begin, rx_end),不能覆盖。 + * WAIT表示协议暂不能受理当前帧,须允许控制发送推进后再重试。 + * 普通通知满仍可处理排在前面的S/U;遇到无处交付的I帧时暂停,TCP尾部仍保留。 + */ +static int Process_Input(IEC104_TcpServer_t *server, unsigned *budget) +{ + while (*budget > 0U && server->event_yield == 0U && server->rx_event_pending == 0U) { + uint32_t consumed = 0U; + uint32_t available = server->rx_end - server->rx_begin; + IEC104_ConnectionResult_t result; + --*budget; + /* 允许零长度Feed重试Connection中保留的帧,不能仅凭TCP缓冲为空跳过。 */ + result = IEC104_Connection_Feed(&server->connection, server->generation, + available != 0U ? server->rx_bytes + server->rx_begin : NULL, available, Now_Ms(), &consumed); + server->rx_begin += consumed; + if (server->rx_begin == server->rx_end) { + server->rx_begin = 0U; + server->rx_end = 0U; + } + if (!Connection_Result_Valid(server, result) || !Deliver_I_Frame(server) || !Consume_Records(server, NULL)) { + return 0; + } + server->rx_blocked = result == IEC104_CONNECTION_WAIT; + if (server->rx_blocked != 0U || server->rx_event_pending != 0U || server->rx_end == 0U) { + break; + } + /* 旧S/U可能本轮刚释放且consumed=0;预算内再次Feed尾部,不丢字节、不无限重试。 */ + } + return 1; +} + +/** + * 功能:推进协议并取得当前可发送帧的剩余字节,本函数不调用send。 + * 输出参数:head——仅返回1时有效,数据借用自Connection内部发送缓冲。 + * 返回值:1可发送,0暂无数据或暂受门控,-1当前连接已经收尾。 + * 说明:部分发送后仍返回同一帧的尾部;EOF后区分记录回收等待与不可解除的发送门控。 + * 新取的时刻用于本地期限及新I帧准入,对端确认超时故障交回接收阶段。 + */ +static int Prepare_Output(IEC104_TcpServer_t *server, IEC104_SendBufferHeadInfo_t *head) +{ + IEC104_ConnectionResult_t result; + int records_released; + do { + result = IEC104_Connection_Prepare_Next_Send( + &server->connection, server->generation, Now_Ms(), head); + if (!Connection_Result_Valid(server, result)) { + return -1; + } + /* Prepare也可能受理保留的ACK;回收后必须重新准备,不能沿用旧WAIT收尾。 + * 仅实际释放记录才重试,次数受本轮通知预算约束,不因WAIT本身空转。 */ + records_released = 0; + if (!Deliver_I_Frame(server) || !Consume_Records(server, &records_released)) { + return -1; + } + } while (result == IEC104_CONNECTION_WAIT && records_released != 0); + server->write_pending = result == IEC104_CONNECTION_SEND_READY; + if (server->write_pending == 0U) { + Reset_Send_Retry(server); + } + if (result == IEC104_CONNECTION_SEND_READY) { + return 1; + } + if (server->rx_eof != 0U) { + uint8_t record_wait = 0U; + if (result == IEC104_CONNECTION_WAIT) { + if (!Connection_Result_Valid(server, IEC104_Connection_Get_Record_Wait( + &server->connection, server->generation, &record_wait))) { + return -1; + } + if (record_wait != 0U) { + /* 只是交付预算不足时下一轮继续;容量背压由NextEvent释放后唤醒。 + * 没有待写字节,不关注可写,也不为满FIFO反复安排本地续跑。 */ + if (server->rx_event_pending == 0U && Ordinary_Event_Room(server, TCP_EVENT_RECORD_BYTES)) { + server->process_pending = 1U; + } + return 0; + } + } + /* EMPTY表示已排空;其余WAIT仍需新的ACK/STARTDT等对端输入,EOF后无法解除。 + * 未确认记录及未提交NORMAL仍按原有断线规则交付/保留。 */ + Drop_Client(server, 0, 0); + return -1; + } + return 0; +} + +/** + * 功能:对已准备帧执行一次非阻塞写入,并恰好一次提交实际字节增量。 + * 输入参数:head——当前SendBuffer帧的剩余字节;不能传业务ASDU或另行构造的APDU。 + * 输出参数:made_progress非NULL且写入正数字节时置1;无进展时保留原值,便于预算循环累计。 + * 返回值:1有进展,0暂不可继续,-1当前连接已收尾;不等待socket,不重试send。 + * 说明:Process发送和应答快速发送共用本函数,部分写入、完成记账及故障回收保持一致。 + * 已有CON按FIFO继续发送;状态通知容量只约束新ACT的受理。 + * CON写入不产生启停通知;内部Drive受理新ACT时可立即产生通知。 + * Drive内的状态变化立即保存,随后才交付新I帧及记录。 + * 发送暂停覆盖全部调用入口;接收、业务提交或迟到的可写通知不能提前重试。 + */ +static int Write_Output( + IEC104_TcpServer_t *server, const IEC104_SendBufferHeadInfo_t *head, int *made_progress) +{ + IEC104_LocalSendCompletion_t completion; + IEC104_ConnectionResult_t result; + IEC104_NetIoResult_t written; + if (Send_Retry_Paused(server, Now_Ms())) { + return 0; + } + /* CON发送不再占用启停通知槽;容量背压在受理ACT前检查。 */ + server->send_retry_at_ms = 0U; + written = IEC104_Net_Send(server->client_socket, head->pending_data, head->remaining_bytes); + if (written.result == IEC104_NET_WOULD_BLOCK || written.result == IEC104_NET_RETRY) { + /* 中断可能没有新的可写边沿,只允许有界本地重试;WOULD_BLOCK等待可写通知。 */ + if (written.result == IEC104_NET_RETRY) { + Schedule_Send_Retry(server); + } else { + Reset_Send_Retry(server); + } + return 0; + } + if (written.result != IEC104_NET_OK) { + result = IEC104_Connection_Handle_Send_Failure(&server->connection, head->frame_id); + if (result != IEC104_CONNECTION_FATAL) { + /* 失败回报未能登记故障时补记内部异常,同时保留实际send的原生错误码。 */ + Note_Local_Fault(server, IEC104_TCP_FAULT_INTERNAL); + } + Drop_Client(server, 0, written.native_error); + return -1; + } + if (written.transferred > 0U) { + Reset_Send_Retry(server); + if (made_progress != NULL) { + *made_progress = 1; + } + } + /* 使用send后的真实时刻登记写入;此处不判定ACK/TESTFR超时,由接收阶段先处理确认。 + * 即使后续Drive返回FATAL也不能重报;Drop_Client会按已登记的发送事实产生收尾记录。 */ + result = IEC104_Connection_Handle_Send_Progress( + &server->connection, head->frame_id, (uint16_t)written.transferred, Now_Ms(), &completion); + if (!Connection_Result_Valid(server, result) || !Deliver_I_Frame(server) || !Consume_Records(server, NULL)) { + return -1; + } + server->write_pending = result != IEC104_CONNECTION_FRAME_WRITTEN; + /* 0字节不推进也不续期,跨轮累计无进展次数;部分写入可能解除接收WAIT。 + * Flush_Output在预算内继续写,预算耗尽由Process安排续跑;快速入口已登记本地续跑。 */ + if (written.transferred == 0U) { + Schedule_Send_Retry(server); + } else if (server->rx_blocked != 0U) { + server->process_pending = 1U; + } + return written.transferred > 0U; +} + +/** + * 功能:在本轮发送预算内准备并写入帧,每次Write_Output消耗一次send预算。 + * 输出参数:made_progress可选,至少一次正数字节写入时为1,供接收WAIT路径判断重试价值。 + * 返回值:1连接仍有效,包括暂不可写或预算耗尽;0当前连接已收尾。 + * 说明:只有此调度入口循环发送;Send_Response快速入口最多调用一次Write_Output。 + * 旧确认t1已到时,Connection在已有帧排空后返回WAIT,不能继续为HIGH/NORMAL编号。 + */ +static int Flush_Output(IEC104_TcpServer_t *server, unsigned *budget, int *made_progress) +{ + if (made_progress != NULL) { + *made_progress = 0; + } + while (*budget > 0U) { + IEC104_SendBufferHeadInfo_t head; + int written; + int prepared = Prepare_Output(server, &head); + if (prepared <= 0) { + return prepared == 0; + } + --*budget; + written = Write_Output(server, &head, made_progress); + if (written <= 0) { + return written == 0; + } + } + return 1; +} + +typedef enum { + TCP_READ_CLOSED = -1, /* recv最终失败,客户端已经由Read_Client收尾。 */ + TCP_READ_BLOCKED = 0, /* 目前无可读字节,连接仍有效。 */ + TCP_READ_DATA = 1, /* 新字节已放入rx_bytes,等待Feed消费。 */ + TCP_READ_EOF = 2 /* 对端发送方向关闭,尚未关闭本端发送方向。 */ +} TcpReadResult_t; + +/** + * 功能:从当前客户端非阻塞读取一批字节,不解析APCI或ASDU。 + * 前提:旧TCP缓冲已消费完且没有阻塞中的协议帧,调用方尚未观察到EOF。 + * 返回值:DATA/BLOCKED/EOF/CLOSED,分别表示有数据、暂不可读、有序EOF及错误收尾。 + * 说明:recv返回0只表示对端不再发送,已生成或可继续准备的应答仍需发送排空。 + */ +static TcpReadResult_t Read_Client(IEC104_TcpServer_t *server) +{ + size_t capacity = sizeof(server->rx_bytes); + IEC104_NetIoResult_t received; + if (server->rx_snapshot_active != 0U && capacity > server->rx_snapshot_remaining) { + capacity = server->rx_snapshot_remaining; + } + /* 旧快照已读完时不能顺便读取后来到达的数据,也不能用零容量recv探测EOF。 */ + if (capacity == 0U) { + return TCP_READ_BLOCKED; + } + received = IEC104_Net_Recv(server->client_socket, server->rx_bytes, capacity); + if (received.result == IEC104_NET_OK) { + if (received.transferred == 0U || received.transferred > capacity) { + (void)Drop_Local_Fault(server, IEC104_TCP_FAULT_INTERNAL); + return TCP_READ_CLOSED; + } + if (server->rx_snapshot_active != 0U) { + server->rx_snapshot_remaining -= (uint32_t)received.transferred; + } + server->rx_begin = 0U; + server->rx_end = (uint32_t)received.transferred; + server->rx_ready = 1U; + return TCP_READ_DATA; + } else if (received.result == IEC104_NET_EOF) { + server->rx_ready = 0U; + return TCP_READ_EOF; + } else if (received.result != IEC104_NET_WOULD_BLOCK && received.result != IEC104_NET_RETRY) { + Drop_Client(server, 1, received.native_error); + return TCP_READ_CLOSED; + } + /* WOULD_BLOCK才耗尽当前就绪;中断后保留续跑,仍结束本批检查期限。 */ + server->rx_ready = received.result == IEC104_NET_RETRY; + return TCP_READ_BLOCKED; +} + +/** + * 功能:结束本轮接收片段,决定立即检查全部期限,还是保留固定输入快照到下一轮处理。 + * 输入参数:may_continue——仅因预算/事件交付让出时为真;read_socket——本批是否允许读取socket。 + * 返回值:1连接可继续,0表示查询输入、协议推进或事件交付失败并已收尾。 + * 说明:首次让出时记录已在内核等待的字节数;后续轮只消耗该快照,不纳入新到达数据。 + * 有旧输入待处理时调用Yield_Receive_Batch并安排立即续跑,先处理可能解除t1的ACK。 + * 快照读完、真正容量背压或接收无进展时调用Tick,按原期限判定,不能靠新流量无限推迟t1。 + */ +static int Finish_Receive_Slice(IEC104_TcpServer_t *server, int may_continue, int read_socket) +{ + IEC104_ConnectionReceivedFrame_t frame; + int pending = 0; + if (may_continue && server->rx_eof == 0U && server->rx_blocked == 0U && + server->rx_event_pending == 0U) { + if (server->rx_snapshot_active == 0U && read_socket) { + int socket_error = 0; + if (IEC104_Net_Pending_Input(server->client_socket, &server->rx_snapshot_remaining, + &socket_error) != IEC104_NET_OK) { + Drop_Client(server, 1, socket_error); + return 0; + } + } + pending = server->rx_snapshot_remaining != 0U || server->rx_begin != server->rx_end || + IEC104_Connection_Peek_Received_Frame(&server->connection, server->generation, &frame) == + IEC104_CONNECTION_FRAME_READY; + } + if (pending) { + server->rx_snapshot_active = 1U; + server->process_pending = 1U; + return Connection_Result_Valid(server, IEC104_Connection_Yield_Receive_Batch( + &server->connection, server->generation, Now_Ms())) && + Deliver_I_Frame(server) && Consume_Records(server, NULL); + } + server->rx_snapshot_active = 0U; + server->rx_snapshot_remaining = 0U; + return Connection_Result_Valid(server, IEC104_Connection_Tick( + &server->connection, server->generation, Now_Ms())) && + Deliver_I_Frame(server) && Consume_Records(server, NULL); +} + +/** + * 功能:执行一批有界接收,优先受理已有输入,再检查对端确认是否超时。 + * 输入/输出参数:rx_budget/tx_budget——本轮Process共享的Feed/send剩余次数。 + * 输入参数:read_socket——本轮有接收就绪,或由本地/定时唤醒允许非阻塞检查内核输入。 + * 返回值:1本批结束且连接仍可继续,0表示连接已因错误或故障收尾。 + * 说明:Begin_Receive_Batch只暂缓I帧ACK和TESTFR_CON期限,本地发送及STOP期限仍有效。 + * 控制槽满时尝试有界发送以解除WAIT;无进展则交还调度,不阻塞等待socket。 + * 预算让出时固定剩余输入范围并立即续跑;快照耗尽、WAIT或无进展时用Tick检查全部期限。 + * 一批输入产生业务事件后不再继续recv,让业务有机会在下一次接收EOF前提交应答。 + */ +static int Receive_Input( + IEC104_TcpServer_t *server, unsigned *rx_budget, unsigned *tx_budget, int read_socket) +{ + uint16_t events_before = server->event_count; + int stalled = 0; + read_socket = read_socket || server->rx_snapshot_active != 0U; + if (!Connection_Result_Valid(server, IEC104_Connection_Begin_Receive_Batch( + &server->connection, server->generation))) { + return 0; + } + /* 先交付预算让出时保留的I帧;旧回执留到输入处理后,避免阻挡已到达的ACK。 */ + if (!Deliver_I_Frame(server)) { + return 0; + } + while (*rx_budget > 0U && server->event_yield == 0U && server->rx_event_pending == 0U && + server->event_count == events_before) { + if (server->rx_end == 0U && server->rx_blocked == 0U) { + TcpReadResult_t received; + if (server->rx_snapshot_active != 0U && server->rx_snapshot_remaining == 0U) { + break; + } + if (!read_socket) { + break; + } + received = Read_Client(server); + if (received == TCP_READ_CLOSED) { + return 0; + } + if (received == TCP_READ_EOF) { + /* 停止为已有及后来新增的NORMAL创建新I帧,原文留给下一连接。 + * 当前已编号帧、已受理HIGH及控制帧仍按原期限和发送调度收尾。 + * 仍经下面的Tick结束接收批次,不遗留被暂缓的确认超时。 */ + if (IEC104_AsduQueue_Limit_Normal_Send(&server->queue, server->generation) != IEC104_ASDU_QUEUE_OK) { + (void)Drop_Local_Fault(server, IEC104_TCP_FAULT_INTERNAL); + server->queue_initialized = 0U; + return 0; + } + server->rx_eof = 1U; + break; + } + if (received == TCP_READ_BLOCKED) { + stalled = 1; + break; + } + } + if (!Process_Input(server, rx_budget)) { + return 0; + } + if (server->rx_blocked != 0U) { + int made_progress; + IEC104_ConnectionReceivedFrame_t frame; + IEC104_ConnectionResult_t pending; + if (!Flush_Output(server, tx_budget, &made_progress)) { + return 0; + } + if (!made_progress) { + stalled = 1; + break; + } + /* 写出字节不一定解除ACT的通知背压;按保留帧的实际受理状态决定是否续收。 */ + pending = IEC104_Connection_Peek_Received_Frame(&server->connection, server->generation, &frame); + if (!Connection_Result_Valid(server, pending)) { + return 0; + } + server->rx_blocked = pending == IEC104_CONNECTION_WAIT; + } + } + return Finish_Receive_Slice(server, !stalled && (*rx_budget == 0U || + server->event_yield != 0U || server->event_count != events_before), read_socket); +} + +/** + * 功能:有界接受连接,并为允许接入的主站建立新的协议会话。 + * 返回值:OK表示监听可继续,包括当前无连接待接受;其他结果为服务级失败,应Close。 + * 说明:只允许一条活动连接,额外连接接受后关闭,不替换当前主站。 + * 新generation不复用旧身份;服务NORMAL通过Rebind绑定,仍须重新STARTDT。 + * 资源暂缺立即暂停;整轮均为RETRY也暂停,其他接入进度仍按预算续跑。 + * 个别客户端设置失败时只关闭该socket;监听、代次或内部初始化失败按阶段交付原因。 + */ +static IEC104_TcpResult_t Accept_Clients(IEC104_TcpServer_t *server, IEC104_TcpError_t *error) +{ + unsigned count, retry_count = 0U; + for (count = 0U; count < TCP_ACCEPT_BUDGET; ++count) { + IEC104_NetSocket_t client; + IEC104_NetPeer_t peer; + int socket_error; + IEC104_NetResult_t accepted; + IEC104_AsduQueueResult_t queued; + /* 旧通知须释放新会话所需容量;建连通知也计入本轮预算。 */ + if (server->client_socket == IEC104_NET_INVALID_SOCKET) { + if (!Can_Accept_Connection(server)) { + return IEC104_TCP_OK; + } + if (server->event_budget == 0U) { + server->process_pending = 1U; + return IEC104_TCP_OK; + } + } + accepted = IEC104_Net_Accept(server->listen_socket, &client, &peer, &socket_error); + if (accepted == IEC104_NET_WOULD_BLOCK) { + server->accept_pending = 0U; + server->accept_retry_at_ms = 0U; + server->consecutive_accept_failures = 0U; + return IEC104_TCP_OK; + } + if (accepted != IEC104_NET_OK) { + server->last_socket_error = socket_error; + server->last_accept_error = socket_error; + if (server->consecutive_accept_failures < UINT32_MAX) { + ++server->consecutive_accept_failures; + } + if (accepted == IEC104_NET_RETRY_LATER) { + Pause_Accept(server); + return IEC104_TCP_OK; + } + if (accepted == IEC104_NET_RETRY) { + ++retry_count; + continue; + } + return Result_With_Error(IEC104_TCP_NETWORK_ERROR, IEC104_TCP_STAGE_ACCEPT, socket_error, error); + } + /* Net已成功交付客户端即有接入进度,额外客户端随后关闭也算成功;历史错误另行保留。 */ + server->accept_retry_at_ms = 0U; + server->consecutive_accept_failures = 0U; + if (server->client_socket != IEC104_NET_INVALID_SOCKET) { + (void)IEC104_Net_Close(client, NULL); + continue; + } + /* 代次不回绕,避免迟到的旧业务事件误匹配未来连接。 */ + if (server->generation == UINT64_MAX) { + (void)IEC104_Net_Close(client, NULL); + server->last_socket_error = 0; + return Result_With_Error(IEC104_TCP_GENERATION_EXHAUSTED, IEC104_TCP_STAGE_GENERATION, 0, error); + } + if (server->registration_serial == UINT64_MAX) { + (void)IEC104_Net_Close(client, NULL); + return Result_With_Error(IEC104_TCP_REGISTRATION_EXHAUSTED, IEC104_TCP_STAGE_REGISTRATION, 0, error); + } + ++server->generation; + /* 队列早已随服务初始化;首次接入与重连都只绑定现存NORMAL,不能重新Init覆盖离线缓存。 */ + queued = IEC104_AsduQueue_Rebind(&server->queue, server->generation); + if (queued != IEC104_ASDU_QUEUE_OK) { + (void)IEC104_Net_Close(client, NULL); + server->last_socket_error = 0; + server->queue_initialized = 0U; + return Result_With_Error(IEC104_TCP_INTERNAL_ERROR, IEC104_TCP_STAGE_QUEUE_INIT, 0, error); + } + if (IEC104_Connection_Init(&server->connection, &server->queue, + server->controls, IEC104_TCP_CONTROL_CAPACITY, server->records, IEC104_TCP_RECORD_CAPACITY, + &server->config.protocol, server->generation, Now_Ms()) != IEC104_CONNECTION_OK) { + (void)IEC104_Net_Close(client, NULL); + server->last_socket_error = 0; + /* Connection_Init失败不发布新会话;归还队列到离线状态,保留已缓存原文。 */ + if (IEC104_AsduQueue_Detach(&server->queue, server->generation) != IEC104_ASDU_QUEUE_OK) { + server->queue_initialized = 0U; + } + return Result_With_Error(IEC104_TCP_INTERNAL_ERROR, IEC104_TCP_STAGE_CONNECTION_INIT, 0, error); + } + { + const IEC104_ConnectionTransferObserver_t observer = { + server, Transfer_Event_Room, Notify_Transfer_State + }; + if (IEC104_Connection_Set_Transfer_Observer(&server->connection, server->generation, &observer) != + IEC104_CONNECTION_OK) { + (void)IEC104_Connection_Close(&server->connection, server->generation); + (void)IEC104_AsduQueue_Detach(&server->queue, server->generation); + (void)IEC104_Net_Close(client, NULL); + server->queue_initialized = 0U; + return Result_With_Error(IEC104_TCP_INTERNAL_ERROR, IEC104_TCP_STAGE_CONNECTION_INIT, 0, error); + } + } + server->client_socket = client; + server->client_token = ++server->registration_serial; + /* 只为成功初始化的当前连接发布地址;额外客户端或接入失败不会覆盖旧主站快照。 */ + server->peer.generation = server->generation; + memcpy(server->peer.address, peer.address, sizeof(server->peer.address)); + server->peer.port = peer.port; + server->last_socket_error = 0; + server->rx_begin = 0U; + server->rx_end = 0U; + server->rx_snapshot_remaining = 0U; + server->rx_snapshot_active = 0U; + server->rx_blocked = 0U; + server->rx_event_pending = 0U; + server->rx_eof = 0U; + server->rx_ready = 1U; + server->write_pending = 0U; + Reset_Send_Retry(server); + server->local_fault = IEC104_TCP_FAULT_NONE; + server->process_pending = 1U; + /* 此时只表示TCP已建立;Connection初始STOPPED,尚未完成104启动握手。 */ + if (!Notify_Connection(server, 1, 0U)) { + (void)Drop_Local_Fault(server, IEC104_TCP_FAULT_INTERNAL); + return Result_With_Error(IEC104_TCP_INTERNAL_ERROR, IEC104_TCP_STAGE_QUEUE_STATE, 0, error); + } + } + /* 没有读到WOULD_BLOCK,不能假定监听队列已空;整轮失败须退避,避免跨轮忙重试。 */ + server->accept_pending = 1U; + if (retry_count == TCP_ACCEPT_BUDGET) { + Pause_Accept(server); + } + return IEC104_TCP_OK; +} + +/** + * 功能:只读判断是否存在无需等待新socket通知就能尝试的本地工作。 + * 输入参数:now_ms——本次Prepare共用的单调时刻;返回1时上层须安排Process,不阻塞等待。 + * 说明:包括已登记续跑、到期发送重试、有容量的接入重试,以及未耗尽的接收就绪/缓存。 + * 无处交付I帧或协议WAIT时保留接收线索,但不以其反复空转;由释放容量或其他进展恢复。 + * 纯协议期限另由Prepare_Events合并,不能把本函数返回0理解为可以无限等待。 + */ +static int Has_Local_Work(const IEC104_TcpServer_t *server, uint64_t now_ms) +{ + return server->process_pending != 0U || + (server->client_socket != IEC104_NET_INVALID_SOCKET && server->send_retry_at_ms != 0U && + !Send_Retry_Paused(server, now_ms)) || + (server->accept_pending != 0U && !Accept_Retry_Paused(server, now_ms) && + (server->client_socket != IEC104_NET_INVALID_SOCKET || Can_Accept_Connection(server))) || + (server->client_socket != IEC104_NET_INVALID_SOCKET && server->rx_eof == 0U && + server->rx_blocked == 0U && server->rx_event_pending == 0U && + (server->rx_ready != 0U || server->rx_end != 0U)); +} + +/** + * 功能:在发送之后重新检查最近协议期限,需要立即处理时登记process_pending。 + * 返回值:1可继续,0表示期限查询产生致命错误且连接已收尾;不进行等待或接收。 + * 前提:接收批次已结束,客户端仍有效;已有本地续跑时不重复查询。 + * 说明:发送可能跨过确认期限,下一轮须先接收已到达的ACK/TESTFR_CON,再按原期限判定。 + * 这里只安排调度,不延长期限,也不在发送结束后直接Tick而跳过待处理确认。 + */ +static int Schedule_Protocol_Work(IEC104_TcpServer_t *server) +{ + uint32_t wait_ms; + if (server->process_pending != 0U) { + return 1; + } + if (!Connection_Result_Valid(server, IEC104_Connection_Get_Wait_Time( + &server->connection, server->generation, Now_Ms(), UINT32_MAX, &wait_ms))) { + return 0; + } + if (wait_ms == 0U) { + server->process_pending = 1U; + } + return 1; +} + +/** + * 功能:Prepare与Poll共用的只读准备核心,生成socket关注集合及最近等待期限。 + * 输入参数:server——公开入口已验证句柄、重入及初始化状态;error——可选失败诊断。 + * 输出参数:info仅OK时一次性写出,失败不交付部分有效的socket列表。 + * 返回值:OK快照有效;INTERNAL_ERROR表示服务队列或协议调度查询异常。 + * 说明:一次取时后合并协议、接入重试、发送重试期限,撤销暂停方向的socket关注。 + * write_pending由实际发送准备维护,业务入队/消费通知通过process_pending唤醒推进。 + * 不调用Tick或Prepare_Next_Send,已失败会话也仅安排立即Process,由推进路径完成收尾。 + */ +static IEC104_TcpResult_t Prepare_Events( + const IEC104_TcpServer_t *server, IEC104_TcpWaitInfo_t *info, IEC104_TcpError_t *error) +{ + IEC104_TcpWaitInfo_t snapshot; + uint32_t client_interest = 0U; + uint64_t now_ms; + int accept_paused; + int send_paused; + if (server->queue_initialized == 0U) { + return Result_With_Error(IEC104_TCP_INTERNAL_ERROR, IEC104_TCP_STAGE_QUEUE_STATE, 0, error); + } + /* 本次快照只取一次时钟,监听门控、本地续跑和全部相对期限使用同一时刻。 */ + now_ms = Now_Ms(); + accept_paused = Accept_Retry_Paused(server, now_ms); + send_paused = Send_Retry_Paused(server, now_ms); + memset(&snapshot, 0, sizeof(snapshot)); + snapshot.service_token = server->listen_token; + snapshot.pending_event_count = server->event_count; + snapshot.receive_paused = server->rx_event_pending; + snapshot.need_process = (uint8_t)Has_Local_Work(server, now_ms); + if (!accept_paused && + (server->client_socket != IEC104_NET_INVALID_SOCKET || Can_Accept_Connection(server))) { + IEC104_TcpSocketEvent_t *watch = &snapshot.sockets[snapshot.socket_count++]; + watch->socket = (IEC104_TcpSocket_t)server->listen_socket; + watch->token = server->listen_token; + watch->events = IEC104_TCP_IO_READ; + } + if (server->client_socket != IEC104_NET_INVALID_SOCKET) { + IEC104_ConnectionResult_t scheduled; + if (server->rx_eof == 0U && server->rx_blocked == 0U && server->rx_event_pending == 0U) { + client_interest |= IEC104_TCP_IO_READ; + } + if (server->write_pending != 0U && !send_paused) { + client_interest |= IEC104_TCP_IO_WRITE; + } + if (client_interest != 0U) { + IEC104_TcpSocketEvent_t *watch = &snapshot.sockets[snapshot.socket_count++]; + watch->socket = (IEC104_TcpSocket_t)server->client_socket; + watch->token = server->client_token; + watch->events = client_interest; + } + snapshot.timer_active = 1U; + scheduled = IEC104_Connection_Get_Wait_Time(&server->connection, server->generation, + now_ms, UINT32_MAX, &snapshot.timeout_ms); + if (scheduled == IEC104_CONNECTION_FATAL) { + /* 已失败会话交给Process收尾;只读查询本身不关闭socket或追加业务事件。 */ + snapshot.timeout_ms = 0U; + } else if (scheduled != IEC104_CONNECTION_OK) { + return Result_With_Error(IEC104_TCP_INTERNAL_ERROR, IEC104_TCP_STAGE_SCHEDULE, 0, error); + } + if (snapshot.timeout_ms == 0U) { + snapshot.need_process = 1U; + } + if (send_paused) { + uint32_t remaining_ms = (uint32_t)(server->send_retry_at_ms - now_ms); + /* 暂停只限制实际写入,不能推迟更早的本地发送或协议期限。 */ + if (remaining_ms < snapshot.timeout_ms) { + snapshot.timeout_ms = remaining_ms; + } + } + } + if (accept_paused) { + uint32_t remaining_ms = (uint32_t)(server->accept_retry_at_ms - now_ms); + /* 无客户端也有接入期限;已到期的期限不再合并,容量不足时不能留下零等待定时器。 */ + if (snapshot.timer_active == 0U || remaining_ms < snapshot.timeout_ms) { + snapshot.timeout_ms = remaining_ms; + } + snapshot.timer_active = 1U; + } + *info = snapshot; + return IEC104_TCP_OK; +} + +/** + * 功能:Process和Poll共用的唯一推进核心,有界处理当前客户端,再尝试接入新客户端。 + * 输入参数:server——公开入口已校验并设置重入保护;ready——本批就绪及定时信息,不能为NULL。 + * 输出参数:error——服务级失败的诊断;客户端故障另通过事件交付。 + * 返回值:OK表示服务可继续,包括忽略旧通知或当前客户端已收尾;其他结果由调用方处理。 + * 说明:先校验整批格式,再匹配service_token和socket/token,最后才改变状态。 + * 收到的就绪只是尝试I/O的提示;先接收并提交事实,再检查期限、推进发送,最后accept。 + * 每轮重新分配收发和通知预算;未完成工作登记本地续跑,不等待、不操作应用的注册表。 + * 新接入连接仅登记下一轮工作,不继承本批旧socket的就绪位。 + */ +static IEC104_TcpResult_t Process_Events( + IEC104_TcpServer_t *server, const IEC104_TcpReady_t *ready, IEC104_TcpError_t *error) +{ + unsigned rx_budget = TCP_IO_BUDGET, tx_budget = TCP_IO_BUDGET; + uint32_t index, previous, listen_events = 0U, client_events = 0U; + int matched = 0; + int local_wake; + IEC104_TcpResult_t result = IEC104_TCP_OK; + if (ready->socket_count > IEC104_TCP_MAX_SOCKETS || ready->timer_expired > 1U || + ready->service_token == 0U) { + return Result_With_Error(IEC104_TCP_INVALID_ARGUMENT, IEC104_TCP_STAGE_VALIDATE, 0, error); + } + for (index = 0U; index < ready->socket_count; ++index) { + const IEC104_TcpSocketEvent_t *event = &ready->sockets[index]; + if (event->token == 0U || event->events == 0U || + (event->events & ~(IEC104_TCP_IO_READ | IEC104_TCP_IO_WRITE | IEC104_TCP_IO_ERROR)) != 0U) { + return Result_With_Error(IEC104_TCP_INVALID_ARGUMENT, IEC104_TCP_STAGE_VALIDATE, 0, error); + } + for (previous = 0U; previous < index; ++previous) { + if (event->socket == ready->sockets[previous].socket && event->token == ready->sockets[previous].token) { + return Result_With_Error(IEC104_TCP_INVALID_ARGUMENT, IEC104_TCP_STAGE_VALIDATE, 0, error); + } + } + } + if (ready->service_token != server->listen_token) { + return IEC104_TCP_OK; + } + if (server->queue_initialized == 0U) { + return Result_With_Error(IEC104_TCP_INTERNAL_ERROR, IEC104_TCP_STAGE_QUEUE_STATE, 0, error); + } + for (index = 0U; index < ready->socket_count; ++index) { + const IEC104_TcpSocketEvent_t *event = &ready->sockets[index]; + if (event->socket == (IEC104_TcpSocket_t)server->listen_socket && event->token == server->listen_token) { + if ((event->events & IEC104_TCP_IO_WRITE) != 0U) { + return Result_With_Error(IEC104_TCP_INVALID_ARGUMENT, IEC104_TCP_STAGE_VALIDATE, 0, error); + } + listen_events |= event->events; + matched = 1; + } else if (server->client_socket != IEC104_NET_INVALID_SOCKET && + event->socket == (IEC104_TcpSocket_t)server->client_socket && event->token == server->client_token) { + client_events |= event->events; + matched = 1; + } + } + if (ready->socket_count != 0U && !matched && ready->timer_expired == 0U) { + return IEC104_TCP_OK; + } + + server->event_budget = TCP_EVENT_BUDGET; + server->event_yield = 0U; + local_wake = server->process_pending != 0U || ready->socket_count == 0U; + server->process_pending = 0U; + if ((listen_events & (IEC104_TCP_IO_READ | IEC104_TCP_IO_ERROR)) != 0U) { + server->accept_pending = 1U; + } + if ((client_events & (IEC104_TCP_IO_READ | IEC104_TCP_IO_ERROR)) != 0U) { + server->rx_ready = 1U; + } + if (server->client_socket != IEC104_NET_INVALID_SOCKET) { + if (server->rx_eof == 0U) { + /* 同批read/timeout先输入后期限。仅write就绪也先重试已缓存输入, + * 本地/定时唤醒额外允许非阻塞recv,避免检查期限之前遗漏已到达的ACK。 */ + (void)Receive_Input(server, &rx_budget, &tx_budget, + server->rx_ready != 0U || local_wake || ready->timer_expired != 0U); + } else if (Connection_Result_Valid(server, IEC104_Connection_Tick( + &server->connection, server->generation, Now_Ms()))) { + (void)(Deliver_I_Frame(server) && Consume_Records(server, NULL)); + } + if (server->client_socket != IEC104_NET_INVALID_SOCKET) { + (void)Flush_Output(server, &tx_budget, NULL); + if (server->client_socket != IEC104_NET_INVALID_SOCKET && tx_budget == 0U) { + /* 可能尚有后续业务帧,下一轮重新取得预算,不能依赖新的可写边沿。 */ + server->process_pending = 1U; + } + if (server->client_socket != IEC104_NET_INVALID_SOCKET) { + (void)Schedule_Protocol_Work(server); + } + } + } + /* 收尾无法解除旧队列绑定时停止本轮,不能让新客户端继续使用异常队列。 */ + if (server->queue_initialized == 0U) { + return Result_With_Error(IEC104_TCP_INTERNAL_ERROR, IEC104_TCP_STAGE_QUEUE_STATE, 0, error); + } + /* 接入放在旧客户端处理之后;任何唤醒原因均须通过暂停期限检查。 + * 新客户端只登记本地唤醒,不继承本批旧客户端就绪位。 */ + if (server->accept_pending != 0U && !Accept_Retry_Paused(server, Now_Ms())) { + result = Accept_Clients(server, error); + } + return result; +} + +/** + * 功能:Poll专用平台等待适配,把公开快照转为Net关注位,等待一次后还原就绪批次。 + * 输入参数:info——本次Prepare_Events快照;max_wait_ms——调用方允许的网络等待上限。 + * 输出参数:ready——内部使用的就绪批次,仅返回OK时交给Process_Events;error——可选诊断。 + * 返回值:OK包括无就绪或可恢复中断;NETWORK_ERROR表示平台等待失败。 + * 说明:实际等待取上限和最近期限的较小值;已有业务事件或本地工作时使用0毫秒等待。 + * service_token及socket/token沿用等待前快照,timer_expired按实际经过时间计算。 + * 外部事件循环不调用这里,可把本模块socket和其他模块事件合并到一次平台等待中。 + */ +static IEC104_TcpResult_t Wait_For_Events( + IEC104_TcpServer_t *server, const IEC104_TcpWaitInfo_t *info, uint32_t max_wait_ms, + IEC104_TcpReady_t *ready, IEC104_TcpError_t *error) +{ + uint32_t index, interest = 0U, ready_events = 0U, wait_ms = max_wait_ms; + uint64_t wait_started = Now_Ms(); + IEC104_NetResult_t waited; + int socket_error; + memset(ready, 0, sizeof(*ready)); + ready->service_token = info->service_token; + if (info->timer_active != 0U && wait_ms > info->timeout_ms) { + wait_ms = info->timeout_ms; + } + if (info->need_process != 0U || info->pending_event_count != 0U) { + wait_ms = 0U; + } + for (index = 0U; index < info->socket_count; ++index) { + const IEC104_TcpSocketEvent_t *watch = &info->sockets[index]; + if (watch->token == server->listen_token) { + interest |= IEC104_NET_EVENT_ACCEPT; + } else { + if ((watch->events & IEC104_TCP_IO_READ) != 0U) { + interest |= IEC104_NET_EVENT_READ; + } + if ((watch->events & IEC104_TCP_IO_WRITE) != 0U) { + interest |= IEC104_NET_EVENT_WRITE; + } + } + } + /* 空集合可在接入暂停时作有限定时等待;有未读事件时wait_ms已为0,仍立即交还业务。 */ + waited = IEC104_Net_Wait(server->listen_socket, server->client_socket, interest, + wait_ms, &ready_events, &socket_error); + if (waited != IEC104_NET_OK && waited != IEC104_NET_RETRY) { + server->last_socket_error = socket_error; + return Result_With_Error(IEC104_TCP_NETWORK_ERROR, IEC104_TCP_STAGE_WAIT, socket_error, error); + } + for (index = 0U; index < info->socket_count; ++index) { + const IEC104_TcpSocketEvent_t *watch = &info->sockets[index]; + uint32_t events = 0U; + if (watch->token == server->listen_token) { + if ((ready_events & IEC104_NET_EVENT_ACCEPT) != 0U) { + events = IEC104_TCP_IO_READ; + } + } else { + if ((ready_events & IEC104_NET_EVENT_READ) != 0U) { + events |= IEC104_TCP_IO_READ; + } + if ((ready_events & IEC104_NET_EVENT_WRITE) != 0U) { + events |= IEC104_TCP_IO_WRITE; + } + } + if (events != 0U) { + IEC104_TcpSocketEvent_t *event = &ready->sockets[ready->socket_count++]; + *event = *watch; + event->events = events; + } + } + ready->timer_expired = (uint8_t)(info->timer_active != 0U && + Now_Ms() - wait_started >= info->timeout_ms); + return IEC104_TCP_OK; +} + +/** + * 功能:从模块内部静态池取得一个地址固定的服务对象。 + * 返回值:成功返回不透明句柄;无空闲槽或候选槽的代次/注册编号耗尽时返回NULL。 + * 说明:只重置可复用运行状态,不创建socket;监听由后续Init完成。 + * generation和注册编号随静态槽保留,不随句柄归还清零;耗尽的槽不能重新分配。 + * 成功后由业务负责配对Destroy;各服务之间的Create/Destroy也必须串行。 + */ +IEC104_TcpServer_t *IEC104_TcpServer_Create(void) +{ + size_t index; + for (index = 0U; index < IEC104_TCP_SERVER_CAPACITY; ++index) { + TcpServerSlot_t *slot = &server_pool[index]; + if (slot->in_use == 0U && slot->server.generation != UINT64_MAX && + slot->server.registration_serial != UINT64_MAX) { + Reset_Server(&slot->server); + slot->in_use = 1U; + return &slot->server; + } + } + return NULL; +} + +/** + * 功能:归还一个已停止且无未读事件的静态槽,不执行网络收尾。 + * 输入参数:server——Create返回且尚未Destroy的句柄。 + * 返回值:OK归还成功;INVALID_HANDLE/BUSY/INVALID_STATE/EVENTS_PENDING区分拒绝原因。 + * 输出参数:error可为NULL;即使句柄无效或已成功归还,也交付本次诊断,不借用对象字段。 + * 说明:正常顺序为Close -> NextEvent读空 -> Destroy;尚未Init或Init失败的句柄也可归还。 + * Close释放网络资源,Destroy释放对象池名额,二者处理的资源不同。 + * 成功后所有旧句柄副本失效;调用方应清空保存的指针,不得自行free。 + */ +IEC104_TcpResult_t IEC104_TcpServer_Destroy(IEC104_TcpServer_t *server, IEC104_TcpError_t *error) +{ + size_t index; + IEC104_TcpResult_t result = Check_Server(server, error); + if (result != IEC104_TCP_OK) { + return result; + } + if (server->is_initialized != 0U) { + return Result_With_Error(IEC104_TCP_INVALID_STATE, IEC104_TCP_STAGE_LIFECYCLE, 0, error); + } + if (server->event_count != 0U) { + return Result_With_Error(IEC104_TCP_EVENTS_PENDING, IEC104_TCP_STAGE_LIFECYCLE, 0, error); + } + for (index = 0U; index < IEC104_TCP_SERVER_CAPACITY; ++index) { + if (server == &server_pool[index].server) { + /* 保留会话代次后再归还名额;下一次Create仍使用同一块固定地址存储。 */ + Reset_Server(server); + server_pool[index].in_use = 0U; + return IEC104_TCP_OK; + } + } + return Result_With_Error(IEC104_TCP_INTERNAL_ERROR, IEC104_TCP_STAGE_LIFECYCLE, 0, error); +} + +/** + * 功能:填写默认监听配置及公开协议参数。 + * 输出参数:config——默认监听0.0.0.0:2404;NORMAL随服务保留,等待上限由每次Poll指定。 + * 说明:传NULL时无操作,不申请系统资源,也不注册业务函数。 + */ +void IEC104_TcpServer_Config_Default(IEC104_TcpServerConfig_t *config) +{ + if (config != NULL) { + memset(config, 0, sizeof(*config)); + memcpy(config->bind_address, "0.0.0.0", sizeof("0.0.0.0")); + config->port = 2404U; + IEC104_Connection_Config_Default(&config->protocol); + } +} + +/** + * 功能:初始化网络后端、非阻塞IPv4监听及服务队列;客户端协议会话延后到accept时初始化。 + * 输入参数:server——Create取得或已Close且事件已读空的对象;config——公开配置。 + * 输出参数:成功时复制配置并设置bound_port;error按本次操作填写,允许NULL。 + * 返回值:OK成功;参数、配置、生命周期和网络错误分别返回对应的公开结果。 + * 网络失败保留NET_INIT/LISTEN阶段及原生错误,参数错误不再伪装为平台socket错误。 + * 说明:首次初始化失败会释放本次申请的网络资源;成功后由应用线程驱动Process或Poll。 + */ +IEC104_TcpResult_t IEC104_TcpServer_Init( + IEC104_TcpServer_t *server, const IEC104_TcpServerConfig_t *config, IEC104_TcpError_t *error) +{ + int socket_error; + IEC104_TcpResult_t result = Check_Server(server, error); + if (result != IEC104_TCP_OK) { + return result; + } + if (config == NULL) { + return Result_With_Error(IEC104_TCP_INVALID_ARGUMENT, IEC104_TCP_STAGE_VALIDATE, 0, error); + } + if (server->is_initialized != 0U) { + return Result_With_Error(IEC104_TCP_INVALID_STATE, IEC104_TCP_STAGE_LIFECYCLE, 0, error); + } + if (server->event_count != 0U) { + return Result_With_Error(IEC104_TCP_EVENTS_PENDING, IEC104_TCP_STAGE_LIFECYCLE, 0, error); + } + if (!Config_Valid(config)) { + return Result_With_Error(IEC104_TCP_INVALID_CONFIG, IEC104_TCP_STAGE_VALIDATE, 0, error); + } + if (server->registration_serial == UINT64_MAX) { + return Result_With_Error(IEC104_TCP_REGISTRATION_EXHAUSTED, IEC104_TCP_STAGE_REGISTRATION, 0, error); + } + /* Close后外部仍可持有旧事件副本;重新Init不复用旧代次,拒绝迟到的旧应答。 */ + Reset_Server(server); + server->config = *config; + /* 成功取得的平台引用须与初始化失败清理或Close配对释放。 */ + if (IEC104_Net_Init(&socket_error) != IEC104_NET_OK) { + server->last_socket_error = socket_error; + return Result_With_Error(IEC104_TCP_NETWORK_ERROR, IEC104_TCP_STAGE_NET_INIT, socket_error, error); + } + if (IEC104_Net_Listen(config->bind_address, config->port, + &server->listen_socket, &server->bound_port, &socket_error) != IEC104_NET_OK) { + /* Net_Listen已回收其申请的句柄;调度层只释放自己的平台引用。 */ + IEC104_Net_Cleanup(); + server->last_socket_error = socket_error; + return Result_With_Error(IEC104_TCP_NETWORK_ERROR, IEC104_TCP_STAGE_LISTEN, socket_error, error); + } + /* 代次0仅表示队列尚未绑定客户端;此时已经可以缓存NORMAL,不虚构协议会话代次。 */ + if (IEC104_AsduQueue_Init(&server->queue, server->high_bytes, sizeof(server->high_bytes), + server->normal_bytes, sizeof(server->normal_bytes), 0U) != IEC104_ASDU_QUEUE_OK) { + (void)IEC104_Net_Close(server->listen_socket, NULL); + server->listen_socket = IEC104_NET_INVALID_SOCKET; + server->bound_port = 0U; + IEC104_Net_Cleanup(); + return Result_With_Error(IEC104_TCP_INTERNAL_ERROR, IEC104_TCP_STAGE_QUEUE_INIT, 0, error); + } + server->queue_initialized = 1U; + server->listen_token = ++server->registration_serial; + server->is_initialized = 1U; + return IEC104_TCP_OK; +} + +/** + * 功能:只读导出当前socket监听集合、协议/接入等待上限及本地续跑要求。 + * 输入参数:server——已Init的句柄;info——非NULL快照存储;error——可选诊断。 + * 返回值:OK时完整写出info,参数、生命周期或内部查询失败时保持info原值。 + * 说明:不提前Tick、构帧、读写或接入,重复查询不会改变状态或刷新协议期限。 + * 本次快照不是长期配置;任何推进/入队/事件消费后,等待前都要重新获取。 + */ +IEC104_TcpResult_t IEC104_TcpServer_Prepare( + const IEC104_TcpServer_t *server, IEC104_TcpWaitInfo_t *info, IEC104_TcpError_t *error) +{ + IEC104_TcpResult_t result = Check_Server(server, error); + if (result != IEC104_TCP_OK) { + return result; + } + if (info == NULL) { + return Result_With_Error(IEC104_TCP_INVALID_ARGUMENT, IEC104_TCP_STAGE_VALIDATE, 0, error); + } + if (server->is_initialized == 0U) { + return Result_With_Error(IEC104_TCP_INVALID_STATE, IEC104_TCP_STAGE_LIFECYCLE, 0, error); + } + return Prepare_Events(server, info, error); +} + +/** + * 功能:把应用收集的一批事件交给唯一的有界推进核心,本函数不执行等待。 + * 输入参数:server——已Init的句柄;ready——必填批次,本地唤醒也须携带有效service_token。 + * 输出参数:error——可选的本次诊断;返回值为OK或参数、生命周期及服务级错误。 + * 说明:业务回调不进入模块;每轮返回后应用读取NextEvent,再更新监听与一次性定时器。 + * 对非法批次、服务级失败和客户端故障使用与Poll相同的结果/事件交付约定。 + */ +IEC104_TcpResult_t IEC104_TcpServer_Process( + IEC104_TcpServer_t *server, const IEC104_TcpReady_t *ready, IEC104_TcpError_t *error) +{ + IEC104_TcpResult_t result = Check_Server(server, error); + if (result != IEC104_TCP_OK) { + return result; + } + if (ready == NULL) { + return Result_With_Error(IEC104_TCP_INVALID_ARGUMENT, IEC104_TCP_STAGE_VALIDATE, 0, error); + } + if (server->is_initialized == 0U) { + return Result_With_Error(IEC104_TCP_INVALID_STATE, IEC104_TCP_STAGE_LIFECYCLE, 0, error); + } + server->in_poll = 1U; + result = Process_Events(server, ready, error); + server->in_poll = 0U; + return result; +} + +/** + * 功能:为独立TCP循环组合准备、一次平台等待及同一套有界推进。 + * 输入参数:server——已Init的句柄;max_wait_ms——网络等待上限,0不等待,UINT32_MAX仍为有限值。 + * 输出参数:error——可选诊断;返回值沿用Prepare/等待/Process中首个失败结果,全部成功为OK。 + * 说明:等待只出现在Wait_For_Events;不再保留另一套接收/发送/超时调度。 + * 即使本地工作或协议期限要求立即处理,也先做0毫秒就绪查询,合并已有输入再推进。 + * max_wait_ms只约束网络等待;客户端故障保留监听,服务级失败由应用Close。 + */ +IEC104_TcpResult_t IEC104_TcpServer_Poll( + IEC104_TcpServer_t *server, uint32_t max_wait_ms, IEC104_TcpError_t *error) +{ + IEC104_TcpWaitInfo_t info; + IEC104_TcpReady_t ready; + IEC104_TcpResult_t result = Check_Server(server, error); + if (result != IEC104_TCP_OK) { + return result; + } + if (server->is_initialized == 0U) { + return Result_With_Error(IEC104_TCP_INVALID_STATE, IEC104_TCP_STAGE_LIFECYCLE, 0, error); + } + server->in_poll = 1U; + result = Prepare_Events(server, &info, error); + if (result == IEC104_TCP_OK) { + result = Wait_For_Events(server, &info, max_wait_ms, &ready, error); + } + if (result == IEC104_TCP_OK) { + result = Process_Events(server, &ready, error); + } + server->in_poll = 0U; + return result; +} + +/** + * 功能:从FIFO复制并移除一个完整事件;不依赖网络资源仍然打开。 + * 输入参数:server——尚未Destroy的句柄;event——业务提供的非NULL输出存储。 + * 返回值:READY写出独立副本,EMPTY/ERROR保持输出不变。 + * 说明:这里只消费队列并安排启停续发/回执转移/接入重试,不补做I/O、释放协议帧或收尾。 + * READY表示事件已取出;应用处理失败时自行保存副本,本函数不会重新投递。 + */ +IEC104_TcpEventResult_t IEC104_TcpServer_NextEvent(IEC104_TcpServer_t *server, IEC104_TcpEvent_t *event) +{ + TcpEventHeader_t header; + IEC104_TcpEvent_t copy; + uint32_t event_bytes, remaining_used; + uint16_t remaining_count, remaining_state_count; + int state_event; + if (!Server_Owned(server) || event == NULL || server->in_poll != 0U || !Event_Queue_Valid(server)) { + return IEC104_TCP_EVENT_ERROR; + } + if (server->event_count == 0U) { + return IEC104_TCP_EVENT_EMPTY; + } + Event_Ring_Read(server, server->event_head, &header, sizeof(header)); + if (header.generation == 0U || !Event_Payload_Length_Valid(header.type, header.payload_length)) { + return IEC104_TCP_EVENT_ERROR; + } + event_bytes = (uint32_t)sizeof(header) + header.payload_length; + state_event = Is_State_Event(header.type); + if (event_bytes > server->event_used || (state_event && server->state_event_count == 0U)) { + return IEC104_TCP_EVENT_ERROR; + } + remaining_used = server->event_used - event_bytes; + remaining_count = (uint16_t)(server->event_count - 1U); + remaining_state_count = (uint16_t)(server->state_event_count - (state_event ? 1U : 0U)); + if (remaining_state_count > remaining_count || + remaining_used < remaining_state_count * TCP_EVENT_CONNECTION_BYTES + + (remaining_count - remaining_state_count) * TCP_EVENT_I_MIN_BYTES || + remaining_used > remaining_state_count * TCP_EVENT_CONNECTION_BYTES + + (remaining_count - remaining_state_count) * TCP_EVENT_I_MAX_BYTES) { + return IEC104_TCP_EVENT_ERROR; + } + /* 展开到清零的公开结构;短APDU后的数组尾部不泄漏旧环内容,历史代次照常交付。 */ + memset(©, 0, sizeof(copy)); + copy.type = header.type; + copy.generation = header.generation; + Event_Ring_Read(server, Event_Ring_Advance(server->event_head, sizeof(header)), + ©.data, header.payload_length); + if (copy.type == IEC104_TCP_EVENT_I_FRAME && + (copy.data.i_frame.apdu_length < IEC104_APCI_LEN || + copy.data.i_frame.apdu_length > sizeof(copy.data.i_frame.apdu) || + TCP_EVENT_I_PREFIX + copy.data.i_frame.apdu_length != header.payload_length)) { + return IEC104_TCP_EVENT_ERROR; + } + /* 所有检查通过后才写输出并提交出队;ERROR时游标、计数和输出均保持原样。 */ + *event = copy; + server->event_head = Event_Ring_Advance(server->event_head, event_bytes); + server->event_used = remaining_used; + server->event_count = remaining_count; + server->state_event_count = remaining_state_count; + if (server->is_initialized != 0U) { + /* 新空位可交付保留I帧、继续启停CON、转移回执或接入;不能只等新的socket边沿。 */ + server->process_pending = 1U; + if (server->client_socket == IEC104_NET_INVALID_SOCKET) { + server->accept_pending = 1U; + } + } + return IEC104_TCP_EVENT_READY; +} + +/** + * 功能:把主动上报复制到服务级NORMAL队列,不要求已经建立TCP或完成STARTDT。 + * 输入参数:server——已Init且未Close的服务;asdu/asdu_length——完整ASDU及长度,不含APCI。 + * 返回值:OK已复制,WAIT编号窗口暂受阻,ERROR参数或生命周期非法,FATAL服务队列异常。 + * 说明:仅本层访问服务队列;有连接时队列使用当前代次,离线时使用0,业务不携带内部绑定身份。 + * EOF后的新事件也可入队;全部未编号NORMAL均留给下一连接,离线入队不触发推进空转。 + */ +IEC104_TcpSubmitResult_t IEC104_TcpServer_Enqueue_Event( + IEC104_TcpServer_t *server, const uint8_t *asdu, uint16_t asdu_length) +{ + IEC104_AsduQueueStatus_t status; + if (!Server_Owned(server) || server->is_initialized == 0U || server->in_poll != 0U) { + return IEC104_TCP_SUBMIT_ERROR; + } + if (server->queue_initialized == 0U || + IEC104_AsduQueue_Get_Status(&server->queue, &status) != IEC104_ASDU_QUEUE_OK || + status.connection_generation != + (server->client_socket == IEC104_NET_INVALID_SOCKET ? 0U : server->generation)) { + server->queue_initialized = 0U; + return IEC104_TCP_SUBMIT_FATAL; + } + switch (IEC104_AsduQueue_Enqueue(&server->queue, status.connection_generation, + IEC104_ASDU_PRIORITY_NORMAL, asdu, asdu_length)) { + case IEC104_ASDU_QUEUE_OK: + if (server->client_socket != IEC104_NET_INVALID_SOCKET && server->rx_eof == 0U) { + server->process_pending = 1U; + } + return IEC104_TCP_SUBMIT_OK; + case IEC104_ASDU_QUEUE_FULL: return IEC104_TCP_SUBMIT_WAIT; + default: return IEC104_TCP_SUBMIT_ERROR; + } +} + +/** + * 功能:提交当前会话的应答,通过启停准入后尝试直接准备并非阻塞写入一次,暂不能准备时复制到HIGH。 + * 输入参数:generation——原I_FRAME事件的连接代次;asdu/asdu_length——完整ASDU及长度。 + * 返回值:OK已接手原文,WAIT未接手、需业务保留重试;ERROR参数/身份/阶段非法,FATAL服务或会话失败。 + * 调用关系:业务 -> Connection_Submit_Response -> SendManager统一交接;SEND_READY时调用Write_Output一次。 + * 说明:OK包括排队、暂不可写、部分写入和本地整帧完成,不表示对端已ACK,不能重复提交同一应答。 + * 仅STARTED+IDLE时接手;否则ERROR且不入HIGH,不因拒绝断连。 + * 新STOP受理时HIGH待发应答作废,无逐条事件;当前I帧及ACK记录继续排空,重复START不清理HIGH。 + * 部分写入及无进展保留SendBuffer中的当前帧,绝不重新放入HIGH;快速发送也遵守重试暂停。 + * FATAL可能发生在已写出部分或整帧之后,须结合RECORD/DISCONNECTED处理,不能直接重放。 + * 在事件循环线程处理I_FRAME时调用,携带该事件generation;调用后重新Prepare更新调度条件。 + */ +static IEC104_TcpSubmitResult_t Send_Response( + IEC104_TcpServer_t *server, uint64_t generation, + const uint8_t *asdu, uint16_t asdu_length, int bypass_queue) +{ + IEC104_SendBufferHeadInfo_t head; + IEC104_ConnectionResult_t submitted; + IEC104_TcpSubmitResult_t result; + if (!Server_Owned(server) || server->is_initialized == 0U || server->in_poll != 0U || + server->client_socket == IEC104_NET_INVALID_SOCKET || server->rx_eof != 0U || + generation != server->generation || asdu == NULL || + asdu_length < IEC104_ASDU_HEADER_LEN || asdu_length > IEC104_MAX_ASDU_LEN) { + return IEC104_TCP_SUBMIT_ERROR; + } + if (server->queue_initialized == 0U) { + return IEC104_TCP_SUBMIT_FATAL; + } + /* 与Process共享串行调度保护;本次不accept/recv、不等待、不循环排空其他帧。 */ + server->in_poll = 1U; + server->event_budget = TCP_EVENT_BUDGET; + server->event_yield = 0U; + submitted = bypass_queue ? + IEC104_Connection_Try_Submit_Response(&server->connection, generation, + asdu, asdu_length, Now_Ms(), &head) : + IEC104_Connection_Submit_Response(&server->connection, generation, + asdu, asdu_length, Now_Ms(), &head); + switch (submitted) { + case IEC104_CONNECTION_SEND_READY: + case IEC104_CONNECTION_OK: + /* 只有接手应答才新增提交续跑;WAIT/ERROR保留已有唤醒及后续事件交付。 */ + server->process_pending = 1U; + result = IEC104_TCP_SUBMIT_OK; + break; + case IEC104_CONNECTION_WAIT: result = IEC104_TCP_SUBMIT_WAIT; break; + case IEC104_CONNECTION_ERROR: result = IEC104_TCP_SUBMIT_ERROR; break; + case IEC104_CONNECTION_FATAL: + Drop_Client(server, 0, 0); + result = IEC104_TCP_SUBMIT_FATAL; + break; + default: + (void)Drop_Local_Fault(server, IEC104_TCP_FAULT_INTERNAL); + result = IEC104_TCP_SUBMIT_FATAL; + break; + } + if (server->client_socket != IEC104_NET_INVALID_SOCKET) { + /* 提交时的Drive也可能释放接收帧或确认记录,即使本次应答未接手仍须交付这些事实。 */ + if (!Deliver_I_Frame(server) || !Consume_Records(server, NULL)) { + result = IEC104_TCP_SUBMIT_FATAL; + } else if (submitted == IEC104_CONNECTION_SEND_READY) { + server->write_pending = 1U; + if (Write_Output(server, &head, NULL) < 0) { + result = IEC104_TCP_SUBMIT_FATAL; + } + } + if (server->client_socket != IEC104_NET_INVALID_SOCKET && !Schedule_Protocol_Work(server)) { + result = IEC104_TCP_SUBMIT_FATAL; + } + } + server->in_poll = 0U; + return result; +} + +IEC104_TcpSubmitResult_t IEC104_TcpServer_Send_Response( + IEC104_TcpServer_t *server, uint64_t generation, + const uint8_t *asdu, uint16_t asdu_length) +{ + return Send_Response(server, generation, asdu, asdu_length, 0); +} + +/* 等待时不保存ASDU;成功后仍使用原部分写入、发送期限、记录和ACK路径。 */ +IEC104_TcpSubmitResult_t IEC104_TcpServer_Try_Send_Response( + IEC104_TcpServer_t *server, uint64_t generation, + const uint8_t *asdu, uint16_t asdu_length) +{ + return Send_Response(server, generation, asdu, asdu_length, 1); +} + +/** + * 功能:返回当前客户端的对端地址及代次,不访问Net或Connection内部对象。 + * 输出参数:peer仅OK时完整写出;失败保持原值,error按统一服务结果填写。 + * 返回值:OK、INVALID_HANDLE/ARGUMENT/STATE、BUSY或NOT_CONNECTED。 + * 说明:地址在Accept成功时转换并保存,查询不执行系统调用;关闭后的历史地址从事件副本读取。 + */ +IEC104_TcpResult_t IEC104_TcpServer_Get_Remote_Address( + const IEC104_TcpServer_t *server, IEC104_TcpPeerInfo_t *peer, IEC104_TcpError_t *error) +{ + IEC104_TcpResult_t result = Check_Server(server, error); + if (result != IEC104_TCP_OK) { + return result; + } + if (peer == NULL) { + return Result_With_Error(IEC104_TCP_INVALID_ARGUMENT, IEC104_TCP_STAGE_VALIDATE, 0, error); + } + if (server->is_initialized == 0U) { + return Result_With_Error(IEC104_TCP_INVALID_STATE, IEC104_TCP_STAGE_LIFECYCLE, 0, error); + } + if (server->client_socket == IEC104_NET_INVALID_SOCKET) { + return Result_With_Error(IEC104_TCP_NOT_CONNECTED, IEC104_TCP_STAGE_LIFECYCLE, 0, error); + } + *peer = server->peer; + return IEC104_TCP_OK; +} + +/** + * 功能:主动关闭匹配代次的当前主站,继续监听并保留服务级NORMAL。 + * 输入参数:generation——原事件或Get_Remote_Address取得的非零身份,不能用最新代次替换旧操作的目标。 + * 返回值:OK已断开;INVALID_*、BUSY、NOT_CONNECTED、STALE_GENERATION为操作前拒绝,不改状态。 + * INTERNAL_ERROR/QUEUE_STATE表示客户端已收尾,但服务队列异常,调用方须Close服务。 + * 说明:复用Drop_Client交付记录和断开事件,不发送STOPDT、不排空应答、不伪造故障或ACK。 + * 本函数不accept;登记本地续跑,使下一次Process查看监听,支持已有接入就绪和边沿触发。 + */ +IEC104_TcpResult_t IEC104_TcpServer_Close_Connection( + IEC104_TcpServer_t *server, uint64_t generation, IEC104_TcpError_t *error) +{ + IEC104_TcpResult_t result = Check_Server(server, error); + if (result != IEC104_TCP_OK) { + return result; + } + if (generation == 0U) { + return Result_With_Error(IEC104_TCP_INVALID_ARGUMENT, IEC104_TCP_STAGE_VALIDATE, 0, error); + } + if (server->is_initialized == 0U) { + return Result_With_Error(IEC104_TCP_INVALID_STATE, IEC104_TCP_STAGE_LIFECYCLE, 0, error); + } + if (server->client_socket == IEC104_NET_INVALID_SOCKET) { + return Result_With_Error(IEC104_TCP_NOT_CONNECTED, IEC104_TCP_STAGE_LIFECYCLE, 0, error); + } + if (generation != server->generation) { + return Result_With_Error(IEC104_TCP_STALE_GENERATION, IEC104_TCP_STAGE_GENERATION, 0, error); + } + server->in_poll = 1U; + Drop_Client(server, 0, 0); + /* Close整个服务才清空队列或撤销监听。预留事件未消费完时,原接入背压规则仍然有效。 */ + server->accept_pending = 1U; + server->process_pending = 1U; + server->in_poll = 0U; + if (server->queue_initialized == 0U) { + return Result_With_Error(IEC104_TCP_INTERNAL_ERROR, IEC104_TCP_STAGE_QUEUE_STATE, 0, error); + } + return IEC104_TCP_OK; +} + +/** + * 功能:读取服务、最近协议会话及两级ASDU队列的公开状态,所有数据均按值交付。 + * 输入参数:server——尚未Destroy的句柄;status——业务提供的非NULL输出存储。 + * 返回值:OK已写入快照;INVALID_HANDLE/INVALID_ARGUMENT/BUSY时status保持原值。 + * 输出参数:error始终表达本次查询结果,允许NULL;snapshot中的last_socket_error是历史网络诊断。 + * 说明:Create后、Init失败后及Close后均可查询,不执行I/O,也不推进协议状态。 + * 接入暂停按当前时钟判断;接入错误独立保留,Close不清历史诊断,下一次Init重置。 + * connected反映当前TCP连接;connection仅在connection_status_valid为1时有效,可能为历史会话。 + * 队列快照独立于连接,离线仍可查询NORMAL;asdu_queue_status_valid为0时两级统计均清零。 + * 队列/会话查询失败不隐式Close或修改内部状态,通过各自有效标志区分,其他服务字段仍有效。 + * 不泄露socket、Connection地址或Manager布局,业务无需读取内部成员来判断状态。 + */ +IEC104_TcpResult_t IEC104_TcpServer_Get_Status( + const IEC104_TcpServer_t *server, IEC104_TcpServerStatus_t *status, IEC104_TcpError_t *error) +{ + IEC104_TcpServerStatus_t snapshot; + uint64_t now_ms; + IEC104_TcpResult_t result = Check_Server(server, error); + if (result != IEC104_TCP_OK) { + return result; + } + if (status == NULL) { + return Result_With_Error(IEC104_TCP_INVALID_ARGUMENT, IEC104_TCP_STAGE_VALIDATE, 0, error); + } + memset(&snapshot, 0, sizeof(snapshot)); + snapshot.generation = server->generation; + snapshot.bound_port = server->bound_port; + snapshot.pending_event_count = server->event_count; + snapshot.pending_state_event_count = server->state_event_count; + snapshot.receive_paused = server->rx_event_pending; + snapshot.last_socket_error = server->last_socket_error; + now_ms = Now_Ms(); + snapshot.accept_paused = (uint8_t)Accept_Retry_Paused(server, now_ms); + if (snapshot.accept_paused != 0U) { + snapshot.accept_retry_remaining_ms = (uint32_t)(server->accept_retry_at_ms - now_ms); + } + snapshot.last_accept_error = server->last_accept_error; + snapshot.consecutive_accept_failures = server->consecutive_accept_failures; + snapshot.listening = server->is_initialized; + snapshot.connected = server->client_socket != IEC104_NET_INVALID_SOCKET; + snapshot.connection_status_valid = (uint8_t)Read_Connection_Status(server, &snapshot.connection); + snapshot.asdu_queue_status_valid = (uint8_t)Read_Asdu_Queues_Status( + server, &snapshot.high_queue, &snapshot.normal_queue); + /* 先在局部对象中整理所有值,最后一次性写出;无协议会话时仍可返回服务状态。 */ + *status = snapshot; + return IEC104_TCP_OK; +} + +/** + * 功能:主动结束客户端和监听,清空内置队列,并释放本服务持有的网络后端引用。 + * 输入参数:server——Create取得且尚未Destroy的对象,同一线程串行调用。 + * 返回值:OK完成或本就未初始化,INVALID_HANDLE/BUSY表示无效句柄或非法重入。 + * 输出参数:error记录本次接口操作结果,允许NULL;不会把历史网络诊断误作本次失败。 + * 说明:不等待发送排空或对端ACK;剩余记录先复制为closing事件,再结束协议会话。 + * 主动Close清空ASDU队列,但保留事件FIFO,应用可在资源关闭后继续NextEvent。 + * Close不归还静态槽;事件读空后可重新Init,或调用Destroy结束句柄生命周期。 + */ +IEC104_TcpResult_t IEC104_TcpServer_Close(IEC104_TcpServer_t *server, IEC104_TcpError_t *error) +{ + IEC104_TcpResult_t result = Check_Server(server, error); + if (result != IEC104_TCP_OK) { + return result; + } + if (server->is_initialized == 0U) { + return IEC104_TCP_OK; + } + server->in_poll = 1U; + Drop_Client(server, 0, 0); + (void)IEC104_Net_Close(server->listen_socket, NULL); + server->listen_socket = IEC104_NET_INVALID_SOCKET; + server->listen_token = 0U; + server->accept_pending = 0U; + server->accept_retry_at_ms = 0U; + server->process_pending = 0U; + server->bound_port = 0U; + IEC104_AsduQueue_Abort(&server->queue); + server->queue_initialized = 0U; + IEC104_Net_Cleanup(); + server->is_initialized = 0U; + server->in_poll = 0U; + return IEC104_TCP_OK; +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_TcpServer.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_TcpServer.h new file mode 100644 index 00000000..5380e34f --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_TcpServer.h @@ -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 至多一次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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_TcpServer_Config.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_TcpServer_Config.h new file mode 100644 index 00000000..0d1417a5 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_TcpServer_Config.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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_TcpTypes.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_TcpTypes.h new file mode 100644 index 00000000..6fdc369f --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_TcpTypes.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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/IEC104_Types.h b/IEC60870-Slave/.build/command-v1/before/104/IEC104_Types.h new file mode 100644 index 00000000..6741e7c4 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/IEC104_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 */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_ClockSynchronization.c b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_ClockSynchronization.c new file mode 100644 index 00000000..4c2e6382 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_ClockSynchronization.c @@ -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 + +#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; +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_ClockSynchronization.h b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_ClockSynchronization.h new file mode 100644 index 00000000..493cc0e4 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_ClockSynchronization.h @@ -0,0 +1,140 @@ +/* + * 104时钟同步(C_CS_NA_1):接收主站校时请求,帮助应用校准供SOE、日志等使用的设备时间。 + * 本模块负责校验、时区换算、站内滞留补偿及ACT_CON;实际设时由应用提供的action完成。 + * Handle_Event/Process不调用应用代码;Execute_Next每次至多同步执行一个action,不保存回调。 + * 设备日历与协议单调计时分开:设置设备时间不应改变Net提供的单调毫秒基准。 + * 所有接口与Slave在同一线程串行调用;模块内应答严格按请求顺序。 + * 执行后的ACT_CON使用Try路径,不越过HIGH/NORMAL待发原文;确认只回显请求时标。 + * 退出顺序:Cancel -> Slave_Close -> 读空并分派通知 -> Destroy -> Remove_Station -> Slave_Destroy。 + */ +#ifndef IEC104_CLOCK_SYNCHRONIZATION_H +#define IEC104_CLOCK_SYNCHRONIZATION_H + +#include "IEC104_Slave.h" +#include "IEC60870_Time.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct IEC104_ClockSynchronization IEC104_ClockSynchronization_t; + +typedef struct { + int16_t master_utc_offset_minutes; /* 主站标准当地时间减UTC的分钟数,-1439..1439;默认480,不含SU。 */ + uint32_t execute_timeout_ms; /* 受理至执行检查的期限,单位ms,默认15000;0禁用期限,仍检查时间范围。 */ +} IEC104_ClockSynchronizationConfig_t; + +/* 交给action的只读纯值请求,指针仅在本次回调期间有效。 + * utc_ms/local_time已补偿受理至base_ms的等待;应用不再叠加时区或同一段等待。 + * 不补偿网络、Slave通知FIFO及action内部耗时;base_ms不是硬件实际生效时刻。 */ +typedef struct { + uint64_t generation, receive_serial; /* generation:原TCP会话代次;receive_serial:原I帧接收流水号。 */ + uint64_t station_id; /* 定向请求的原站点注册编号,不是CA;广播为0,不绑定某个目标站。 */ + uint16_t common_address; /* 请求原CA,广播保留65535;逐站确认的CA由模块另行填写。 */ + uint8_t originator_address; /* 请求原OA,供应用识别发起方;本字段不是权限校验结果。 */ + IEC60870_CP56Time2a_t master_time; /* 原CP56解码值,IV=0,保留主站日历和SU;未作时区换算及滞留补偿。 */ + IEC60870_WallTime_t local_time; /* 对应base_ms的固定UTC+8标准日历,可直接交给Clock_Set或RTC适配。 */ + uint64_t utc_ms; /* 对应base_ms的UTC纪元毫秒,自1970-01-01 00:00:00 UTC起算。 */ + /* accepted_ms:Handle_Event受理时刻;base_ms:执行检查时刻。均为Net单调毫秒,允许为0。 */ + uint64_t accepted_ms, base_ms; +} IEC104_ClockSynchronizationRequest_t; + +/* 应用同步设时动作:context由Execute_Next原样传入,允许NULL;request为本次校验后的只读请求。 + * true报告设置成功,false未报告成功;false不保证时钟未变,模块不提供硬件回滚。 + * 须短时完成,不保留request指针等待异步执行;权限、允许回拨幅度和设备操作由应用判断。 + * 不得重入本模块的修改入口或推进Slave事件循环;回调内仅允许查询本模块Get_Status。 + * 每条通过执行检查的新请求各调用一次,不按内容去重;主站重发旧时标仍可能再次回拨。 */ +typedef bool (*IEC104_ClockSynchronizationAction_t)(void *context, + const IEC104_ClockSynchronizationRequest_t *request); + +typedef enum { + IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR = -1, /* 参数、事件归属、重入或内部准备失败。 */ + IEC104_CLOCK_SYNCHRONIZATION_EVENT_IGNORED = 0, /* 非103 I帧或本模块当前不能接手的通知。 */ + IEC104_CLOCK_SYNCHRONIZATION_EVENT_HANDLED = 1 /* 含拒绝和丢新,不表示已应答。 */ +} IEC104_ClockSynchronizationEventResult_t; + +typedef enum { + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_ERROR = -1, /* 参数、重入或服务状态查询失败;未调用action。 */ + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY = 0, /* 无可执行请求;仍可能有待提交应答。 */ + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_WAIT = 1, /* 先处理未读Slave通知。 */ + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_OK = 2 /* 已调用一次动作;确认可能因后续STOP/故障取消。 */ +} IEC104_ClockSynchronizationExecuteResult_t; + +/* 只读调度快照;计数从Create起累计,Cancel及连接启停不清零,不代表对端已经收到应答。 */ +typedef struct { + uint64_t generation; /* 已分派给模块的会话代次;消费DISCONNECTED后为0。 */ + bool active; /* 模块根据已分派通知及推进结果维护的激活状态,不替代当前网络状态查询。 */ + bool events_pending; /* Slave尚有未读通知;应先NextEvent并按FIFO分派,再执行或续发。 */ + bool execute_pending; /* 仍有占槽的待执行请求;不保证当前会话、期限已允许调用action。 */ + bool confirm_pending; /* action已开始且确认尚未全部交接,含回调期间;不含仅校验失败的回复。 */ + bool need_process; /* 当前有状态清理、到期或待尝试回复;false仍不能跳过每轮Process的WAIT重试。 */ + bool timer_active; /* 存在尚未执行且启用了执行期限的请求。 */ + uint32_t timeout_remaining_ms; /* 所有执行期限的最小剩余ms;到期为0,无计时任务时也为0。 */ + unsigned pending_requests; /* 占用任务槽数,含待执行、回调中及待回复;一条广播始终只占一槽。 */ + /* accepted:合法且成功入槽的请求数,含后来超时/取消者。 + * executed:action已返回的次数;synchronized:其中返回true的次数。 */ + unsigned long accepted, executed, synchronized; + /* submitted:发送层返回OK的ASDU数,广播逐目标计数;negative_submitted为其中P/N=1的数量。 */ + unsigned long submitted, negative_submitted; + /* rejected:受理校验拒绝数,含无法应答的畸形请求,不含后续超时或action失败。 + * waits:发送返回WAIT的次数,重试可重复累计;不计等待Slave通知的次数。 + * cancelled:取消的占槽请求数,广播部分确认后取消也只计一次,不保证设备时间未变。 + * overloads/dropped_requests:槽满丢新的次数/请求数,同时增加;对象数错误/对象截短只计rejected。 + * timeouts:启用期限时,执行前到期或单调倒退而转否定的请求数。 + * time_errors:执行检查的换算/范围失败,以及禁用期限时的单调倒退,不含受理校验拒绝。 */ + unsigned long rejected, waits, cancelled, overloads, dropped_requests, timeouts, time_errors; +} IEC104_ClockSynchronizationStatus_t; + +/* 向config写默认源时区480分钟、执行期限15000ms;config为NULL时无操作。 + * 只填写配置,不创建实例;调用方可修改后交给Create。 */ +void IEC104_ClockSynchronization_Config_Default(IEC104_ClockSynchronizationConfig_t *config); +/* 为有效slave创建独立校时实例;config须非NULL,按值复制,返回后不再借用配置指针。 + * slave由应用拥有,寿命须覆盖本实例;可在Slave_Init前创建,但必须早于消费该服务的通知。 + * 每服务只允许一个实例,按共用站点表路由;不分配堆内存、不建立监听、不保存应用action。 + * 成功返回池内句柄;参数/配置非法、重复绑定或池/身份资源耗尽返回NULL。 + * IEC104_CLOCK_SYNCHRONIZATION_CAPACITY默认等于TCP服务容量; + * IEC104_CLOCK_SYNCHRONIZATION_TASK_CAPACITY默认4,限定每实例的任务槽数。 */ +IEC104_ClockSynchronization_t *IEC104_ClockSynchronization_Create(IEC104_Slave_t *slave, + const IEC104_ClockSynchronizationConfig_t *config); +/* 归还self占用的静态池槽;true成功,成功后必须停止使用旧句柄。 + * 句柄无效、仍有占槽任务或处于action回调中返回false;不隐式Cancel,也不关闭Slave。 */ +bool IEC104_ClockSynchronization_Destroy(IEC104_ClockSynchronization_t *self); +/* 取消self的全部占槽任务并归还站点引用/快照,每条请求增加一次cancelled。 + * 不停用连接,可继续接手后续请求;不撤回已交接帧、不回滚设备时间。 + * 无返回值;无效句柄或action回调中调用均无操作。 */ +void IEC104_ClockSynchronization_Cancel(IEC104_ClockSynchronization_t *self); +/* 将event按Slave FIFO次序交给self;event须来自所属Slave的NextEvent或合法Copy_Event副本。 + * 所有连接/启停通知都须分派,103 I帧由本模块接手;不修改输入事件,不调用action。 + * 合法请求保存为待执行任务;错误回复可当场提交。COT/CA/IOA优先回45/46/47, + * 其余可对应的非法请求否定7;不能保留唯一完整对象时仅计拒绝,槽满丢新并计数、不断链。 + * 广播只执行一次动作,按受理时的快照逐站确认;已删除的待提交目标跳过,新注册站不参与。 + * 返回HANDLED包含保存、拒绝、丢新和状态通知处理,不表示已应答;IGNORED表示未接手; + * ERROR表示参数/归属/重入或内部准备失败,不可作为IGNORED继续生成兜底应答。 */ +IEC104_ClockSynchronizationEventResult_t IEC104_ClockSynchronization_Handle_Event( + IEC104_ClockSynchronization_t *self, const IEC104_SlaveEvent_t *event); +/* 为self有界推进执行期限及队首回复,不调用action;须在每轮网络推进、通知分派后调用。 + * true表示完成本轮检查,可能仍在等待、已取消任务或没有任务,不表示应答成功。 + * 参数、重入、查询或组包错误返回false。发送ERROR/FATAL取消任务后返回true, + * 正常STOP待通知的ERROR先让出;应用继续消费Slave通知处理后续断线/启停。 + * 即使need_process为false也须每轮调用,以重试发送WAIT;不能以忙循环代替网络推进。 */ +bool IEC104_ClockSynchronization_Process(IEC104_ClockSynchronization_t *self); +/* 为self执行最早的待执行请求:action须非NULL,context原样传给它,本函数不保存两者。 + * 先检查会话、未读通知、期限及补偿范围;每次至多调用一次action,过期请求只转为否定回复。 + * 可越过较早的待回复任务执行后续请求,但确认仍按模块内请求顺序提交,不插队。 + * OK表示action已经返回,可能报告失败,确认也可能待发或被后续STOP/故障取消; + * WAIT表示先处理Slave通知;EMPTY表示已无可执行请求,仍可能有待回复任务; + * ERROR为参数、重入或服务查询失败。WAIT/EMPTY/ERROR均未调用action。 + * 动作开始后不再判该任务的执行期限;确认发送WAIT只重试确认,绝不重复动作。 */ +IEC104_ClockSynchronizationExecuteResult_t IEC104_ClockSynchronization_Execute_Next( + IEC104_ClockSynchronization_t *self, IEC104_ClockSynchronizationAction_t action, void *context); +/* 将self的当前只读快照写入非NULL的status;true成功,句柄/参数或服务查询失败返回false且不改输出。 + * 可在action回调内查询,不推进任务、不预留发送资源;正在执行的任务不计期限,confirm_pending为true。 + * 执行后的确认走Try,已有HIGH/NORMAL待发内容会阻止提交;持续发布可能推迟确认。 + * 需要确认先于新的带时标上报时,由发布方在confirm_pending期间暂停此类新上报并继续推进网络。 */ +bool IEC104_ClockSynchronization_Get_Status(const IEC104_ClockSynchronization_t *self, + IEC104_ClockSynchronizationStatus_t *status); + +#ifdef __cplusplus +} +#endif +#endif /* IEC104_CLOCK_SYNCHRONIZATION_H */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_CounterInterrogation.c b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_CounterInterrogation.c new file mode 100644 index 00000000..9e3bb985 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_CounterInterrogation.c @@ -0,0 +1,58 @@ +/* + * 104计数量种类规则:解析C_CI_NA_1的QCC,限定RQT和读取数据类型,选择完成方式。 + * 规则函数不保存状态、不访问网络;计数器存储及冻结/复位由应用提供。 + * 创建和动作入口只委托共用引擎,避免另建一套任务、广播、期限及取消流程。 + */ +#include "IEC104_CounterInterrogation.h" +#include "IEC104_Interrogation_Internal.h" + +/* 从首对象取IOA和QCC;对象层已将低6位RQT与高2位FRZ拆开。 + * 这里只解析,不校验是否支持该分组;失败时不写调用方的输出。 */ +static bool Counter_Decode(const IEC60870_AsduInfo_t *info, uint32_t *ioa, uint8_t *qualifier, uint8_t *freeze) +{ + IEC60870_CounterInterrogationCommand_t command; + if (IEC60870_Asdu_Get_Element(info, 0U, &command, sizeof(command)) != IEC60870_ASDU_OK) { return false; } + *ioa = command.object.ioa; + *qualifier = command.qualifier; + *freeze = command.freeze; + return true; +} + +/* RQT=1..4为分组,5为总请求;0未用、6..63保留。具体分组是否配置由应用在BEGIN决定。 */ +static bool Counter_Qualifier_Valid(uint8_t qualifier) { return qualifier >= 1U && qualifier <= 5U; } +/* 引擎准入后使用:总请求的数据COT为37,第1..4组依次为38..41。 */ +static uint8_t Counter_Data_Cause(uint8_t qualifier) +{ + return (uint8_t)(qualifier == 5U ? IEC60870_ASDU_COT_COUNTER_INTERROGATION : + IEC60870_ASDU_COT_COUNTER_INTERROGATION + qualifier); +} +/* 只有读取存在数据阶段;冻结/复位在肯定确认提交后直接完成。 */ +static bool Counter_Has_Data(uint8_t freeze) { return freeze == 0U; } +/* 读取接受无时标Type15和CP56时标Type37;Type37仅SQ=0,Type16不用于104。 + * 报文完整布局、对象数和头部一致性仍由共用引擎及ASDU层检查。 */ +static bool Counter_Data_Allowed(const IEC60870_AsduInfo_t *info, uint8_t freeze) +{ + return freeze == 0U && (info->type_id == IEC60870_ASDU_M_IT_NA_1 || + (info->type_id == IEC60870_ASDU_M_IT_TB_1 && info->is_sequence == 0U)); +} + +/* 只读规则存活至进程结束;has_deactivation=false使COT=8按未知传送原因处理。 */ +const IEC104_InterrogationRules_t IEC104_COUNTER_INTERROGATION_RULES = { + IEC60870_ASDU_C_CI_NA_1, false, Counter_Decode, Counter_Qualifier_Valid, + Counter_Data_Cause, Counter_Data_Allowed, Counter_Has_Data +}; + +/* 用计数量规则创建独立实例;配置、站点引用、重复实例和状态区重叠统一由引擎校验。 */ +IEC104_Interrogation_t *IEC104_CounterInterrogation_Create(IEC104_Slave_t *slave, + const IEC104_InterrogationConfig_t *config) +{ + return IEC104_Interrogation_Create_With_Rules(slave, config, &IEC104_COUNTER_INTERROGATION_RULES); +} + +/* 传入本种类规则作为身份约束,防止误把总召句柄交给动作入口。 */ +IEC104_InterrogationReplyResult_t IEC104_CounterInterrogation_Execute_Begin( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, + IEC104_CounterInterrogationAction_t action, void *context) +{ + return IEC104_Interrogation_Execute_Action(self, event, &IEC104_COUNTER_INTERROGATION_RULES, action, context); +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_CounterInterrogation.h b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_CounterInterrogation.h new file mode 100644 index 00000000..7ba1ab9e --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_CounterInterrogation.h @@ -0,0 +1,48 @@ +/* + * 104计数量召唤(C_CI_NA_1):读取冻结值,或由应用执行冻结/复位。 + * 本模块提供种类规则和创建/动作入口;事件、调度、广播、期限与清理由共用引擎维护。 + * 返回的句柄沿用Interrogation的驱动接口,所有调用在所属事件循环中串行执行。 + */ +#ifndef IEC104_COUNTER_INTERROGATION_H +#define IEC104_COUNTER_INTERROGATION_H +#include "IEC104_Interrogation.h" +#ifdef __cplusplus +extern "C" { +#endif + +/* 创建计数量实例:复制配置、借用Slave;参数或容量不满足要求时返回NULL,不操作网络。 + * 同一Slave可同时拥有总召和计数量各一个实例,须分别提供广播状态区并接收全部连接/启停通知。 + * RQT=1..4选择分组,5选择全部;FRZ=0读取冻结值,接受Type15(SQ=0/1)或Type37(SQ=0), + * 数据COT使用request.data_cause;104计数量不接受带CP24时标的Type16。 + * FRZ=1冻结,2冻结并复位,3仅复位;肯定ACT_CON提交后即完成,不生成NEED_DATA或ACT_TERM。 + * gi_submitted在此实例统计完成响应的提交次数:读取为ACT_TERM,动作命令为ACT_CON。 + * 不支持COT=8撤销,回COT=45;TEST冻结/复位请求否定且不执行动作。 + * 销毁前先Cancel并读完FINISHED;连接关闭会同时影响共享该连接的其他业务。 */ +IEC104_Interrogation_t *IEC104_CounterInterrogation_Create(IEC104_Slave_t *slave, + const IEC104_InterrogationConfig_t *config); + +/* 同步动作回调:context为应用上下文,request为引擎保存的请求信息,仅在回调期间借用。 + * true表示所选组的动作已完成;false必须保证未改变任何计数器,不能部分清零后报告失败。 + * 应先检查整个组的资源和条件,再提交修改;动作须短时完成,不访问网络或执行阻塞I/O。 + * 不得重入引擎或推进协议事件循环;真实计数器值、冻结值、序号和品质均由应用维护。 */ +typedef bool (*IEC104_CounterInterrogationAction_t)(void *context, + const IEC104_InterrogationRequest_t *request); + +/* 应答已取出的FRZ=1..3 BEGIN;event须原样传回,action非NULL,context原样传给回调。 + * 先校验实例、事件、会话和期限;WAIT/STALE/ERROR均不调用action,也不会修改计数器。 + * 校验通过后先消费BEGIN,再调用一次action;返回OK表示交互已消费,不表示动作一定成功。 + * action返回true排队肯定确认,返回false排队否定确认;TEST请求直接否定,不调用action。 + * 同一事件再次提交返回STALE;只保证本次BEGIN不重复执行,主站重发的新请求仍可能再次执行。 + * 动作成功后等待肯定确认期间停止业务期限,避免计数器已改变却因排队超时改发否定确认。 + * 传输超时、STOP、断线及本地Cancel仍可终结协议任务并交付FINISHED,但不回滚已执行动作。 + * 本接口不管理异步外部电表事务;这类事务须另定义执行状态、迟到结果和重复请求策略。 + * FRZ=0使用Reply_Begin及数据接口,整轮读取(含WAIT重试)应保持同一冻结快照。 + * 动作命令没有NEED_DATA,拿其BEGIN调用Submit_Batch会返回STALE,不发送数据。 */ +IEC104_InterrogationReplyResult_t IEC104_CounterInterrogation_Execute_Begin( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, + IEC104_CounterInterrogationAction_t action, void *context); + +#ifdef __cplusplus +} +#endif +#endif /* IEC104_COUNTER_INTERROGATION_H */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_GeneralInterrogation.c b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_GeneralInterrogation.c new file mode 100644 index 00000000..5ed39820 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_GeneralInterrogation.c @@ -0,0 +1,54 @@ +/* + * 104总召种类规则:C_IC_NA_1、QOI=20..36、无时标监视数据及主站撤销。 + * 原IEC104_Interrogation_Create入口在此保留;调度、生命周期及发送由共用引擎维护。 + */ +#include "IEC104_GeneralInterrogation.h" +#include "IEC104_Interrogation_Internal.h" + +/* 解析首对象的IOA和QOI;总召没有FRZ,统一输出0供共用请求结构使用。 */ +static bool General_Decode(const IEC60870_AsduInfo_t *info, uint32_t *ioa, uint8_t *qualifier, uint8_t *freeze) +{ + IEC60870_InterrogationCommand_t command; + if (IEC60870_Asdu_Get_Element(info, 0U, &command, sizeof(command)) != IEC60870_ASDU_OK) { return false; } + *ioa = command.object.ioa; + *qualifier = command.qualifier; + *freeze = 0U; + return true; +} + +/* QOI=20为站总召,21..36为16个分组;数据COT与合法QOI相同。 */ +static bool General_Qualifier_Valid(uint8_t qualifier) { return qualifier >= 20U && qualifier <= 36U; } +static uint8_t General_Data_Cause(uint8_t qualifier) { return qualifier; } +/* 总召总是进入数据阶段;空点表也由应用Complete_Data结束,再发送ACT_TERM。 */ +static bool General_Has_Data(uint8_t freeze) { (void)freeze; return true; } + +/* 保持原总召的9种无时标监视类型,SQ=0/1由ASDU层校验;累计量由计数量实例处理。 */ +static bool General_Data_Allowed(const IEC60870_AsduInfo_t *info, uint8_t freeze) +{ + (void)freeze; + switch (info->type_id) { + case IEC60870_ASDU_M_SP_NA_1: + case IEC60870_ASDU_M_DP_NA_1: + case IEC60870_ASDU_M_ST_NA_1: + case IEC60870_ASDU_M_BO_NA_1: + case IEC60870_ASDU_M_ME_NA_1: + case IEC60870_ASDU_M_ME_NB_1: + case IEC60870_ASDU_M_ME_NC_1: + case IEC60870_ASDU_M_PS_NA_1: + case IEC60870_ASDU_M_ME_ND_1: return true; + default: return false; + } +} + +/* 总召支持COT=8撤销;规则只负责声明能力,匹配请求及撤销清理由引擎执行。 */ +const IEC104_InterrogationRules_t IEC104_GENERAL_INTERROGATION_RULES = { + IEC60870_ASDU_C_IC_NA_1, true, General_Decode, General_Qualifier_Valid, + General_Data_Cause, General_Data_Allowed, General_Has_Data +}; + +/* 保留旧公开函数名和签名;工程需同时编译本文件与共用引擎。 */ +IEC104_Interrogation_t *IEC104_Interrogation_Create(IEC104_Slave_t *slave, + const IEC104_InterrogationConfig_t *config) +{ + return IEC104_Interrogation_Create_With_Rules(slave, config, &IEC104_GENERAL_INTERROGATION_RULES); +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_GeneralInterrogation.h b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_GeneralInterrogation.h new file mode 100644 index 00000000..d793683b --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_GeneralInterrogation.h @@ -0,0 +1,9 @@ +/* + * 104总召(C_IC_NA_1)的种类入口。 + * 创建函数仍名为IEC104_Interrogation_Create,声明和共用驱动接口由下列头文件提供。 + * 只使用总召的工程无需链接CounterInterrogation.c。 + */ +#ifndef IEC104_GENERAL_INTERROGATION_H +#define IEC104_GENERAL_INTERROGATION_H +#include "IEC104_Interrogation.h" +#endif diff --git a/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_Interrogation.c b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_Interrogation.c new file mode 100644 index 00000000..fb3e3b47 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_Interrogation.c @@ -0,0 +1,1706 @@ +/* + * 事件式召唤共用状态机。Slave负责通信,应用通过BEGIN/NEED_DATA/FINISHED交互。 + * 请求任务仅保存原身份及10字节应答副本;业务事件为纯值,可以移动和复制。 + * 单目标预留FINISHED槽,多目标使用每站终结记录,断线/STOP/取消/撤销/超时不丢清理通知。 + * 确认/终止沿原身份走HIGH,数据尝试直接提交,避免跨连接重放和确认/数据/终止乱序。 + * + * 正常阶段:START生成BEGIN并等待应用接受 -> CONFIRM提交激活确认 -> DATA反复索取并提交批次 + * -> TERMINATE提交激活终止 -> 交付FINISHED。多目标按全部确认、逐站数据、全部终止推进。 + * 只执行动作的种类在CONFIRM成功后直接FINISHED;种类差异仅由创建时的只读规则表决定。 + * FINISHING保留多目标状态区至终结事件读完,不阻塞网络或其他短应答。 + * REPLY表示当前站只需提交确认或错误应答;广播继续下一站,否则释放任务。 + * Handle_Event更新会话和接收请求,条件允许时立即提交短应答;Process推进保存的任务。 + * NextEvent把业务事件交给应用;一个实例至多一个未终结召唤,同种类忙时否定新激活。 + * Reply_Begin/Complete_Data接收决定;Submit_Batch当场尝试不排队提交,WAIT后应用重新组包。 + * 总召可重取当前值,计数量读取须保持本轮冻结快照;引擎不保存业务点值。 + * 事件来源和会话可用性统一由Slave接口校验,本模块维护各实例自己的阶段和业务期限。 + * 单帧应答与召唤任务分开选择,双方可推进时轮转;召唤等待应用不阻塞单帧应答。 + * 请求解析后按CA选站,合法多目标广播持有节点集合;错误广播仍用共享快照遍历。 + */ +#include "IEC104_Interrogation_Internal.h" +#include "IEC104_TcpServer_Config.h" +#include "IEC104_Net.h" +#include + +/* 库内三类存储均为静态有界容量:实例池、每实例协议任务池、每实例业务事件FIFO。 + * 多目标广播另外借用调用方提供的轻量状态数组,不分配数据快照或每站发送队列。 + * 单目标路径需同时容纳BEGIN及预留的FINISHED,因此业务事件容量至少为两个。 */ +#ifndef IEC104_INTERROGATION_CAPACITY +#define IEC104_INTERROGATION_CAPACITY (2U * IEC104_TCP_SERVER_CAPACITY) +#endif +#ifndef IEC104_INTERROGATION_TASK_CAPACITY +#define IEC104_INTERROGATION_TASK_CAPACITY 4U +#endif +#ifndef IEC104_INTERROGATION_EVENT_CAPACITY +#define IEC104_INTERROGATION_EVENT_CAPACITY 4U +#endif +_Static_assert(IEC104_INTERROGATION_CAPACITY > 0U, "Interrogation needs instance storage"); +_Static_assert(IEC104_INTERROGATION_TASK_CAPACITY > 0U, "Interrogation needs request storage"); +_Static_assert(IEC104_INTERROGATION_EVENT_CAPACITY >= 2U, "BEGIN needs a reserved FINISHED slot"); + +/* 支持的104召唤命令固定为6字节公共头、3字节IOA和1字节限定词;入口完整校验后才保存。 + * 独立保存原身份和必要原文,不借用SlaveEvent、ASDU视图或发送接口中的自引用。 + * 不保存接收APCI;发送序号、确认号及链路期限由底层连接管理。 */ +typedef struct { + IEC104_SlaveEventIdentity_t identity; + uint8_t bytes[IEC104_ASDU_HEADER_LEN + 3U + 1U]; +} InterrogationResponse; + +/* 任务使用固定槽及顺序索引;压缩存储不改变任务数量、调度顺序和背压策略。 */ +typedef struct { + InterrogationResponse request; /* 原服务/代次和独立10字节请求,保存时已设置应答CA。 */ + IEC104_InterrogationRequest_t info; /* 唯一业务元数据;供事件交付及批次校验,终结时仍描述原激活。 */ + IEC104_Station_t *station; /* 当前站额外引用,独立于广播数组持有的引用和CA路由资格。 */ + IEC104_StationSnapshot_t stations; /* 单目标广播/错误广播的遍历快照;正常多目标广播使用每站状态数组。 */ + uint64_t request_id; /* 激活任务编号;未建立总召事务的单帧应答为0。 */ + uint64_t started_ms, application_since_ms; /* 单调毫秒:BEGIN生成、当前工作事件入队。 */ + size_t batch_index, objects_submitted; /* 已被发送层接手的批次数、对象总数。 */ + IEC104_InterrogationPhase_t phase; /* 当前站下一步动作;整轮广播阶段另由broadcast.stage管理。 */ + uint8_t reply_cause; /* 非零为单帧确认/错误应答,肯定撤销确认也使用此路径。 */ + bool reply_negative; /* REPLY阶段发送的P/N标志;肯定撤销确认为false。 */ + bool data_send_waiting; /* 本批已及时提供有效数据但发送WAIT;不缓存原文,不再计本批取数期限。 */ + bool send_waiting; /* 当前帧提交曾返回WAIT;无新的本地工作时等网络推进后再重试。 */ + bool event_outstanding, event_delivered; /* 当前交互待应答、该工作事件已被应用取出。 */ + bool begin_delivered, finish_reserved; /* 应用已取过当前BEGIN;finish_reserved仅用于单目标终结预留。 */ + bool occupied; /* 固定任务槽正在使用;任务可非队首完成,释放后立即复用此槽。 */ +} InterrogationTask; + +/* 整轮依次完成全部站确认、逐站取数、全部站终止;FINISH只等待终结事件交付和资源回收。 */ +enum { BROADCAST_ACCEPT, BROADCAST_DATA, BROADCAST_TERMINATE, BROADCAST_FINISH }; +/* ACCEPTED仅表示应用接受;CONFIRMED表示肯定ACT_CON已被发送层接手,尚不代表主站ACK。 + * DATA_DONE等其他站取数完成后再发ACT_TERM;ENDED保留终结原因,不能立即回收状态项。 */ +enum { STATION_PENDING, STATION_ACCEPTED, STATION_CONFIRMED, STATION_DATA_DONE, STATION_ENDED }; +/* BEGIN_READ决定是否欠应用一次FINISHED;QUEUED防止重复入队,READ才表示已经交付。 + * DEACTIVATE单独记录待发送COT=9,站点ENDED或FINISHED已读均不能代替撤销确认提交。 */ +enum { STATION_BEGIN_READ = 1U, STATION_FINISH_QUEUED = 2U, STATION_FINISH_READ = 4U, STATION_DEACTIVATE = 8U }; + +/* 一个实例至多一轮广播,三阶段共用同一协议任务,应用可逐站复用取数游标。 */ +typedef struct { + InterrogationTask *task; /* 三阶段复用同一固定槽,FINISHING期间仍占用。 */ + size_t count, index; /* 已捕获目标数、当前站在调用方状态数组中的位置。 */ + uint8_t stage; /* 整轮阶段;当前站的等待/发送动作保存在task->phase。 */ + bool cancelling, cancel_all; /* 有一组撤销确认待提交;cancel_all区分整轮撤销与定向撤销。 */ +} InterrogationBroadcast; + +struct IEC104_Interrogation { + IEC104_Slave_t *slave; /* NULL为空闲槽;有效期间Slave必须存活。 */ + const IEC104_InterrogationRules_t *rules; /* 创建时借用的只读种类规则,寿命覆盖实例。 */ + IEC104_InterrogationConfig_t config; /* 配置对象按值复制,broadcast_states指向的数组仍借用调用方存储。 */ + IEC104_InterrogationStatus_t status; /* 会话状态和累计统计;即时进度由Get_Status补齐。 */ + IEC104_SlaveEventIdentity_t session; /* Slave校验后交付的服务身份和已处理的连接代次。 */ + uint64_t instance_id, next_request_id; + InterrogationBroadcast broadcast; + /* instance_id区分总召静态槽的历次使用;next_request_id在本实例内递增, + * 两者共同标识一次总召,独立于Slave的服务实例和连接身份。 */ + unsigned count, event_count, reserved_finishes; + /* count包含等待清理的广播任务;event_count仅统计已入FIFO的事件。 + * reserved_finishes仅为单目标预留,二者之和不超过FIFO容量;广播延迟FINISHED另存每站状态。 */ + unsigned order[IEC104_INTERROGATION_TASK_CAPACITY]; /* 有效任务槽下标,保持应答形成的先后顺序。 */ + bool prefer_interrogation; /* 单帧提交后给可推进总召一次机会,跨Process返回保留。 */ + InterrogationTask tasks[IEC104_INTERROGATION_TASK_CAPACITY]; /* 固定地址的协议任务池。 */ + IEC104_InterrogationEvent_t events[IEC104_INTERROGATION_EVENT_CAPACITY]; /* 可移动的纯值事件FIFO。 */ +}; +static IEC104_Interrogation_t interrogation_pool[IEC104_INTERROGATION_CAPACITY]; +/* 不随槽释放清零,避免旧事件误命中新实例;达到上限拒绝创建,不回绕。 */ +static uint64_t instance_serial; +static bool Advance(IEC104_Interrogation_t *self, InterrogationTask *task); +static bool Next_Broadcast_Station(IEC104_Interrogation_t *self); +static void Flush_Broadcast_Finishes(IEC104_Interrogation_t *self); + +/* 仅识别使用状态数组的正常多目标广播,单目标广播和错误广播不走此分支。 */ +static bool Is_Broadcast(const IEC104_Interrogation_t *self, const InterrogationTask *task) +{ + return task != NULL && self->broadcast.task == task; +} + +static IEC104_InterrogationStationState_t *Broadcast_Current(IEC104_Interrogation_t *self) +{ + return &self->config.broadcast_states[self->broadcast.index]; +} + +/* 切站或终结前保存共享任务的当前进度;这里只存统计,不保存应用游标或点值快照。 */ +static void Save_Broadcast_Progress(IEC104_Interrogation_t *self) +{ + IEC104_InterrogationStationState_t *entry = Broadcast_Current(self); + entry->_batch_index = self->broadcast.task->batch_index; + entry->_objects_submitted = self->broadcast.task->objects_submitted; +} + +/* 检查句柄是否指向仍被占用的实例槽;先比较地址再解引用,NULL和池外指针返回false。 + * 这里只验证实例占用,Slave的寿命仍由调用方保证。 */ +static bool Owned(const IEC104_Interrogation_t *self) +{ + size_t i; + for (i = 0U; i < IEC104_INTERROGATION_CAPACITY; ++i) { + if (self == &interrogation_pool[i]) { return self->slave != NULL; } + } + return false; +} + +/* 取得固定任务槽并追加顺序索引;调用方完成请求复制,失败时须立即Remove_Task。 + * 不移动已有任务,保持快照的_self校验及broadcast.task指针有效。 */ +static InterrogationTask *Allocate_Task(IEC104_Interrogation_t *self) +{ + unsigned slot; + if (self->count == IEC104_INTERROGATION_TASK_CAPACITY) { return NULL; } + for (slot = 0U; slot < IEC104_INTERROGATION_TASK_CAPACITY; ++slot) { + InterrogationTask *task = &self->tasks[slot]; + if (!task->occupied) { + memset(task, 0, sizeof(*task)); + task->occupied = true; + self->order[self->count++] = slot; + return task; + } + } + return NULL; +} + +/* 分别归还当前站引用、遍历快照及每个广播状态项的引用,随后清空本轮状态。 + * 当前站与对应状态项各持有一次引用,因此两次Release均须保留。 */ +static void Release_Stations(IEC104_Interrogation_t *self, InterrogationTask *task) +{ + size_t i; + if (task->station != NULL) { + (void)IEC104_Slave_Release_Station(self->slave, task->station); + task->station = NULL; + } + if (task->stations._owner != NULL) { (void)IEC104_Slave_StationSnapshot_End(&task->stations); } + if (Is_Broadcast(self, task)) { + for (i = 0U; i < self->broadcast.count; ++i) { + IEC104_InterrogationStationState_t *entry = &self->config.broadcast_states[i]; + (void)IEC104_Slave_Release_Station(self->slave, entry->_station); + memset(entry, 0, sizeof(*entry)); + } + memset(&self->broadcast, 0, sizeof(self->broadcast)); + } +} + +/* 仅压紧顺序索引,不移动其他任务;调用前须完成终结通知安排。 + * 多目标广播须经Reap_Broadcast确认FINISHED已读完,才能释放任务及借用状态项。 */ +static void Remove_Task(IEC104_Interrogation_t *self, InterrogationTask *task) +{ + unsigned i; + for (i = 0U; i < self->count; ++i) { + if (&self->tasks[self->order[i]] == task) { + Release_Stations(self, task); + memset(task, 0, sizeof(*task)); + memmove(self->order + i, self->order + i + 1U, + (self->count - i - 1U) * sizeof(self->order[0])); + self->order[--self->count] = 0U; + return; + } + } +} + +/* 原总召转为拒绝/撤销确认时,复用原槽并按应答形成时刻放到顺序末尾。 + * 满池也能转换;不让这条新应答越过已经等待的其他单帧应答。 */ +static void Move_Reply_To_Tail(IEC104_Interrogation_t *self, const InterrogationTask *task) +{ + unsigned i; + for (i = 0U; i < self->count; ++i) { + unsigned slot = self->order[i]; + if (&self->tasks[slot] == task) { + memmove(self->order + i, self->order + i + 1U, + (self->count - i - 1U) * sizeof(self->order[0])); + self->order[self->count - 1U] = slot; + self->prefer_interrogation = false; + return; + } + } +} + +/* 返回唯一未终结总召的固定槽下标;request_id为0查当前总召,非0核对指定请求。 + * 无匹配返回TASK_CAPACITY;广播在当前站否定确认等待期间仍占总召资格。 + * FINISHING不参与业务推进,接纳新请求时另检查broadcast.task,防止复用未清理的状态区。 */ +static unsigned Find_Interrogation(const IEC104_Interrogation_t *self, uint64_t request_id) +{ + unsigned i; + for (i = 0U; i < self->count; ++i) { + unsigned slot = self->order[i]; + const InterrogationTask *task = &self->tasks[slot]; + if (task->request_id != 0U && task->phase != IEC104_INTERROGATION_FINISHING && + (task->phase != IEC104_INTERROGATION_REPLY || task->stations._owner != NULL || Is_Broadcast(self, task)) && + (request_id == 0U || task->request_id == request_id)) { return slot; } + } + return IEC104_INTERROGATION_TASK_CAPACITY; +} + +/* 最早形成的单帧应答;立即提交也必须遵守此顺序,不能越过旧REPLY。 + * 广播撤销借用原任务发送一组确认,即使当前站phase未转REPLY,也按短应答参与排序。 */ +static unsigned Find_Reply(const IEC104_Interrogation_t *self) +{ + unsigned i; + for (i = 0U; i < self->count; ++i) { + unsigned slot = self->order[i]; + if (self->tasks[slot].phase == IEC104_INTERROGATION_REPLY || + (Is_Broadcast(self, &self->tasks[slot]) && self->broadcast.cancelling)) { return slot; } + } + return IEC104_INTERROGATION_TASK_CAPACITY; +} + +/* 业务推进无需等待应用或事件空位时为true;发送容量仍由实际提交结果决定。 */ +static bool Interrogation_Can_Progress(const IEC104_Interrogation_t *self, const InterrogationTask *task) +{ + if (task->phase == IEC104_INTERROGATION_START || task->phase == IEC104_INTERROGATION_DATA) { + unsigned needed = task->phase == IEC104_INTERROGATION_DATA || Is_Broadcast(self, task) ? 1U : 2U; + return !task->event_outstanding && + self->event_count + self->reserved_finishes + needed <= IEC104_INTERROGATION_EVENT_CAPACITY; + } + return task->phase != IEC104_INTERROGATION_FINISHING; +} + +/* 单帧保持FIFO;双方可推进时先单帧再总召,轮转位置跨调用保存。 + * 总召等应用或业务FIFO空位时只选单帧;均不能推进时返回容量哨兵。 + * Process与状态查询共用选择规则;send_waiting不阻止网络推进后的显式重试。 */ +static unsigned Select_Task(const IEC104_Interrogation_t *self) +{ + unsigned slot = Find_Interrogation(self, 0U), reply_slot = Find_Reply(self); + if (slot != IEC104_INTERROGATION_TASK_CAPACITY && + (self->tasks[slot].phase == IEC104_INTERROGATION_REPLY || + (Is_Broadcast(self, &self->tasks[slot]) && self->broadcast.cancelling) || + !Interrogation_Can_Progress(self, &self->tasks[slot]))) { slot = IEC104_INTERROGATION_TASK_CAPACITY; } + if (reply_slot != IEC104_INTERROGATION_TASK_CAPACITY && + (!self->prefer_interrogation || slot == IEC104_INTERROGATION_TASK_CAPACITY)) { return reply_slot; } + return slot != IEC104_INTERROGATION_TASK_CAPACITY ? slot : reply_slot; +} + +/* 将任务身份、请求元数据和当前进度复制成一条业务事件;本函数不执行应用代码。 + * type决定应用需要接受、交付数据还是清理;reason仅在FINISHED时有业务意义。 + * 调用方须已检查容量,或已将本任务的终结预留释放为可写位置。 */ +static void Push_Event(IEC104_Interrogation_t *self, const InterrogationTask *task, + IEC104_InterrogationEventType_t type, IEC104_InterrogationFinishReason_t reason) +{ + IEC104_InterrogationEvent_t *event = &self->events[self->event_count++]; + memset(event, 0, sizeof(*event)); + event->type = type; + event->instance_id = self->instance_id; + event->request_id = task->request_id; + event->request = task->info; + event->batch_index = task->batch_index; + event->objects_submitted = task->objects_submitted; + event->reason = reason; +} + +/* 终结记录留在每站状态区,FIFO有空位才入队;2槽FIFO也可交付全部已配置目标的终结通知。 + * 仅为应用已读BEGIN的站点生成一次FINISHED,未入队的记录保留到后续调用继续交付。 + * 共享任务此时可能已切到其他站,故公共请求字段沿用task,身份/CA/进度须从原站状态覆盖。 */ +static void Flush_Broadcast_Finishes(IEC104_Interrogation_t *self) +{ + size_t i; + InterrogationTask *task = self->broadcast.task; + if (task == NULL) { return; } + for (i = 0U; i < self->broadcast.count && + self->event_count + self->reserved_finishes < IEC104_INTERROGATION_EVENT_CAPACITY; ++i) { + IEC104_InterrogationStationState_t *entry = &self->config.broadcast_states[i]; + IEC104_InterrogationEvent_t *event; + if (entry->_reason == IEC104_INTERROGATION_FINISH_NONE || + (entry->_flags & (STATION_BEGIN_READ | STATION_FINISH_QUEUED)) != STATION_BEGIN_READ) { continue; } + Push_Event(self, task, IEC104_INTERROGATION_FINISHED, (IEC104_InterrogationFinishReason_t)entry->_reason); + event = &self->events[self->event_count - 1U]; + event->request_id = entry->_request_id; + event->request.station_id = IEC104_Station_Get_Id(entry->_station); + event->request.common_address = IEC104_Station_Get_Common_Address(entry->_station); + event->batch_index = entry->_batch_index; + event->objects_submitted = entry->_objects_submitted; + entry->_flags |= STATION_FINISH_QUEUED; + } +} + +/* 只计尚未入FIFO的终结记录;状态查询再加event_count,避免重复统计已排队的FINISHED。 */ +static unsigned Pending_Broadcast_Finishes(const IEC104_Interrogation_t *self) +{ + size_t i; + unsigned pending = 0U; + if (self->broadcast.task == NULL) { return 0U; } + for (i = 0U; i < self->broadcast.count; ++i) { + const IEC104_InterrogationStationState_t *entry = &self->config.broadcast_states[i]; + if (entry->_reason != IEC104_INTERROGATION_FINISH_NONE && + (entry->_flags & (STATION_BEGIN_READ | STATION_FINISH_QUEUED)) == STATION_BEGIN_READ) { ++pending; } + } + return pending; +} + +/* 整轮结束、撤销确认提交完且每个已读BEGIN的FINISHED也已读出后,才归还状态区和引用。 + * 入FIFO不等于交付;此函数可能清空broadcast.task,调用方不能假定原任务仍有效。 */ +static void Reap_Broadcast(IEC104_Interrogation_t *self) +{ + size_t i; + if (self->broadcast.task == NULL || self->broadcast.stage != BROADCAST_FINISH || self->broadcast.cancelling) { return; } + for (i = 0U; i < self->broadcast.count; ++i) { + uint8_t flags = self->config.broadcast_states[i]._flags; + if ((flags & (STATION_BEGIN_READ | STATION_FINISH_READ)) == STATION_BEGIN_READ) { return; } + } + Remove_Task(self, self->broadcast.task); +} + +/* 取消当前请求尚未消费的工作事件,保留所有FINISHED;已交给应用的旧应答由身份/状态拒绝。 */ +static void Remove_Work_Events(IEC104_Interrogation_t *self, InterrogationTask *task) +{ + unsigned read, write = 0U; + for (read = 0U; read < self->event_count; ++read) { + const IEC104_InterrogationEvent_t *event = &self->events[read]; + if (event->request_id == task->request_id && event->type != IEC104_INTERROGATION_FINISHED) { continue; } + self->events[write++] = *event; + } + self->event_count = write; + task->event_outstanding = task->event_delivered = false; +} + +/* 可终结当前站或其他已确认站;仅当前站需要保存共享进度、撤销未完成的工作交互。 + * 首次终结原因不被后续整轮取消覆盖;节点引用及待发撤销标志留到各自清理边界。 */ +static void End_Broadcast_Station(IEC104_Interrogation_t *self, size_t index, + IEC104_InterrogationFinishReason_t reason) +{ + IEC104_InterrogationStationState_t *entry = &self->config.broadcast_states[index]; + if (entry->_state == STATION_ENDED) { return; } + if (index == self->broadcast.index) { + Save_Broadcast_Progress(self); + Remove_Work_Events(self, self->broadcast.task); + } + entry->_reason = (uint8_t)reason; + entry->_state = STATION_ENDED; + self->status.last_finish_reason = reason; +} + +/* 结束一个总召的应用交互,reason保存为该请求的终结原因;不自行释放协议任务。 + * 多目标广播把终结记录写入调用方状态区,FIFO有空位时才交付。 + * 移除本请求尚未取出的BEGIN/NEED_DATA,保留其他事件和所有历史FINISHED。 + * 单目标BEGIN已取出时,用预留位置交付一次FINISHED;BEGIN未取出时只撤销事件和预留, + * 因为应用尚未获得请求,无需通知它清理资源。finish_reserved同时防止重复终结。 */ +static void Finish(IEC104_Interrogation_t *self, InterrogationTask *task, + IEC104_InterrogationFinishReason_t reason) +{ + if (Is_Broadcast(self, task)) { + End_Broadcast_Station(self, self->broadcast.index, reason); + Flush_Broadcast_Finishes(self); + return; + } + if (task->request_id != 0U && task->phase != IEC104_INTERROGATION_REPLY) { + self->status.last_finish_reason = reason; + } + if (!task->finish_reserved) { return; } + Remove_Work_Events(self, task); + --self->reserved_finishes; + task->finish_reserved = false; + task->event_outstanding = task->event_delivered = false; + if (task->begin_delivered) { + Push_Event(self, task, IEC104_INTERROGATION_FINISHED, reason); + task->begin_delivered = false; + } +} + +/* 肯定ACT_CON尚未被发送层接手时,结束本次业务并复用原槽保存否定确认。 + * 应用主动拒绝、BEGIN失败以及START/CONFIRM超时共用此路径,保留各自终结原因。 + * Finish清除未读工作事件并兑现终结预留;不取消其他任务、不额外占槽、不关闭连接。 + * 新形成的否定确认排在已有REPLY之后;后续Process提交,WAIT时保留重试。 */ +static void Reject_Request(IEC104_Interrogation_t *self, InterrogationTask *task, + IEC104_InterrogationFinishReason_t reason) +{ + ++self->status.rejected; + Finish(self, task, reason); + task->reply_cause = IEC60870_ASDU_COT_ACTIVATION_CON; + task->reply_negative = true; + task->phase = IEC104_INTERROGATION_REPLY; + task->send_waiting = false; + self->status.waiting = false; + Move_Reply_To_Tail(self, task); +} + +/* 按reason结束本实例全部任务,不关闭Slave或连接;Slave已接手的帧不撤销。 + * 单目标任务可立即释放,多目标留在FINISHING,待各站FINISHED读完再归还状态区。 + * cancelled按取消的协议任务数累计,包含单帧应答;整轮广播只计一个,不按站点或事件计数。 */ +static void Cancel(IEC104_Interrogation_t *self, IEC104_InterrogationFinishReason_t reason) +{ + unsigned i = 0U; + while (i < self->count) { + InterrogationTask *task = &self->tasks[self->order[i]]; + if (Is_Broadcast(self, task)) { + size_t station; + if (task->phase != IEC104_INTERROGATION_FINISHING) { ++self->status.cancelled; } + for (station = 0U; station < self->broadcast.count; ++station) { + End_Broadcast_Station(self, station, reason); + self->config.broadcast_states[station]._flags &= (uint8_t)~STATION_DEACTIVATE; + } + self->broadcast.stage = BROADCAST_FINISH; + self->broadcast.cancelling = false; + task->phase = IEC104_INTERROGATION_FINISHING; + task->send_waiting = task->data_send_waiting = false; + ++i; + } else { + Finish(self, task, reason); + Remove_Task(self, task); + ++self->status.cancelled; + } + } + Flush_Broadcast_Finishes(self); + Reap_Broadcast(self); + self->prefer_interrogation = false; + self->status.waiting = false; +} + +/* 内部服务故障或肯定确认提交后的执行失败,关闭identity所属连接并按reason取消全部任务。 + * identity可能位于任务中,必须先关闭原连接再清任务;旧代次不会关闭新连接。 + * true包括成功关闭、原连接已断开和代次已过期;false表示底层关闭失败,任务仍已取消。 */ +static bool Reject_Connection(IEC104_Interrogation_t *self, const IEC104_SlaveEventIdentity_t *identity, + IEC104_InterrogationFinishReason_t reason) +{ + IEC104_TcpResult_t result = IEC104_Slave_Close_Connection_By_Identity(self->slave, identity, NULL); + Cancel(self, reason); + self->status.active = false; + return result == IEC104_TCP_OK || result == IEC104_TCP_NOT_CONNECTED || + result == IEC104_TCP_STALE_GENERATION; +} + +/* 按肯定确认的提交边界处理业务失败:START/CONFIRM仍可否定激活并保留连接; + * 肯定ACT_CON被发送层接手即越过边界:单目标转DATA,多目标标记CONFIRMED并继续确认其他站。 + * DATA/TERMINATE失败沿用关闭原连接的策略,不补发成功ACT_TERM。 + * 调用方已校验会话与任务;true表示失败已处理,并不表示否定确认已经提交。 */ +static bool Fail_Interrogation(IEC104_Interrogation_t *self, InterrogationTask *task, + IEC104_InterrogationFinishReason_t reason) +{ + if (task->phase == IEC104_INTERROGATION_START || task->phase == IEC104_INTERROGATION_CONFIRM) { + Reject_Request(self, task, reason); + return true; + } + return Reject_Connection(self, &task->request.identity, reason); +} + +/* DEFER表示暂不能尝试提交,WAIT表示已尝试但发送层未接手;均保存原请求,或在满槽时丢弃并计数。 + * 只使用现有HIGH接口,不越过Slave通知或旧REPLY,不改变总召数据的发送顺序。 */ +typedef enum { REPLY_DEFER, REPLY_WAIT, REPLY_SENT, REPLY_FAILED } ImmediateReplyResult; + +/* 为请求校验选择首个实际CA;广播任务随后建立快照逐站推进,未知CA保留原值。 */ +static uint16_t Resolve_Common_Address(const IEC104_Interrogation_t *self, uint16_t address) +{ + if (self->config.use_station_registry) { + IEC104_Station_t *station = address == IEC104_CA_BROADCAST ? + IEC104_Slave_Next_Station(self->slave, NULL) : IEC104_Slave_Get_Station(self->slave, address); + return station != NULL ? IEC104_Station_Get_Common_Address(station) : address; + } + return address == IEC104_CA_BROADCAST ? self->config.common_address : address; +} + +/* 保存可明确识别的单对象Type100及原身份;IOA/QOI必须完整,零对象/多对象不在此构造应答。 + * 应答固定为10字节、VSQ=1,保留唯一对象、OA和TEST;CA按原规则处理广播。 + * 调用方须记住原SQ/尾字节错误并仅作否定应答,不能把规范化副本当作合法激活或撤销执行。 */ +static bool Copy_Response_Request(IEC104_Interrogation_t *self, InterrogationResponse *destination, + const IEC104_SlaveEventIdentity_t *identity, const uint8_t *data, uint16_t length, uint16_t response_ca) +{ + IEC60870_Asdu_t asdu; + if (length < sizeof(destination->bytes) || data[0] != self->rules->type_id || + (data[1] & 0x7fU) != 1U) { return false; } /* VSQ低7位为对象数。 */ + destination->identity = *identity; + memcpy(destination->bytes, data, sizeof(destination->bytes)); + destination->bytes[1] = 1U; + return IEC60870_Asdu_Create_From_Buffer(&asdu, IEC104_Slave_Get_Application_Layer_Parameters(self->slave), + destination->bytes, sizeof(destination->bytes)) == IEC60870_ASDU_OK && + IEC60870_Asdu_Set_CA(&asdu, response_ca) == IEC60870_ASDU_OK; +} + +/* 选中节点后持有引用;删除只退出路由,当前事务仍使用这个注册编号及context。 */ +/* 当前任务持有原节点,并把请求副本CA改为实际目标站;连接身份、OA、QOI等保持原请求语义。 */ +static bool Bind_Station(IEC104_Interrogation_t *self, InterrogationTask *task, IEC104_Station_t *station) +{ + IEC60870_Asdu_t asdu; + if (station == NULL || IEC104_Slave_Acquire_Station(self->slave, station) != IEC104_STATION_OK) { return false; } + task->station = station; + task->info.common_address = IEC104_Station_Get_Common_Address(station); + task->info.station_id = IEC104_Station_Get_Id(station); + return IEC60870_Asdu_Create_From_Buffer(&asdu, IEC104_Slave_Get_Application_Layer_Parameters(self->slave), + task->request.bytes, sizeof(task->request.bytes)) == IEC60870_ASDU_OK && + IEC60870_Asdu_Set_CA(&asdu, task->info.common_address) == IEC60870_ASDU_OK; +} + +/* 在同一任务槽内切站:归还当前额外引用,再从状态项恢复本站身份、起始时间和提交进度。 + * 状态项本身仍持有原节点,故删除或同CA替换不会使切换误绑定新站;此处不重新按CA查找。 + * 每站只恢复协议状态,BEGIN/NEED_DATA的交互标志重新开始,应用取数游标由应用另行维护。 */ +static bool Select_Broadcast_Station(IEC104_Interrogation_t *self, size_t index) +{ + InterrogationTask *task = self->broadcast.task; + IEC104_InterrogationStationState_t *entry = &self->config.broadcast_states[index]; + if (task->station != NULL) { + (void)IEC104_Slave_Release_Station(self->slave, task->station); + task->station = NULL; + } + self->broadcast.index = index; + if (!Bind_Station(self, task, entry->_station)) { return false; } + task->request_id = entry->_request_id; + task->started_ms = entry->_started_ms; + task->application_since_ms = 0U; + task->batch_index = entry->_batch_index; + task->objects_submitted = entry->_objects_submitted; + task->phase = self->broadcast.stage == BROADCAST_ACCEPT ? IEC104_INTERROGATION_START : + self->broadcast.stage == BROADCAST_DATA ? IEC104_INTERROGATION_DATA : IEC104_INTERROGATION_TERMINATE; + task->reply_cause = 0U; + task->reply_negative = task->send_waiting = task->data_send_waiting = false; + task->event_outstanding = task->event_delivered = task->finish_reserved = false; + task->begin_delivered = (entry->_flags & STATION_BEGIN_READ) != 0U; + return true; +} + +/* 接收时一次取得所有节点引用;后续确认、数据、终止使用同一集合,不混入新增/替代节点。 + * 入口已检查整轮容量和请求编号余量;本段串行执行且不调用应用,捕获期间注册集合不变。 + * 每成功持有一项便计入count,部分失败时由整轮取消/回收路径归还已取得的引用。 */ +static bool Capture_Broadcast(IEC104_Interrogation_t *self, InterrogationTask *task) +{ + IEC104_Station_t *station = NULL; + self->broadcast.task = task; + while ((station = IEC104_Slave_Next_Station(self->slave, station)) != NULL) { + IEC104_InterrogationStationState_t *entry; + if (self->broadcast.count == self->config.broadcast_capacity || + IEC104_Slave_Acquire_Station(self->slave, station) != IEC104_STATION_OK) { return false; } + entry = &self->config.broadcast_states[self->broadcast.count]; + memset(entry, 0, sizeof(*entry)); + entry->_station = station; + entry->_request_id = self->broadcast.count == 0U ? task->request_id : ++self->next_request_id; + ++self->broadcast.count; + } + return self->broadcast.count != 0U && Select_Broadcast_Station(self, 0U); +} + +/* 先完成当前阶段的余下站点,再从数组首项进入下一阶段,保证全部确认先于数据、全部数据先于终止。 + * 仅跳过尚未选中便被删除的未来站;已确认站即使退役,也按原引用继续完成或显式终结。 + * 最后一遍完成后进入FINISHING;若没有待交付终结事件,本次调用即可回收任务。 */ +static bool Next_Broadcast_Station(IEC104_Interrogation_t *self) +{ + size_t index = self->broadcast.index + 1U; + while (self->broadcast.stage < BROADCAST_FINISH) { + for (; index < self->broadcast.count; ++index) { + IEC104_InterrogationStationState_t *entry = &self->config.broadcast_states[index]; + if (self->broadcast.stage == BROADCAST_ACCEPT && entry->_state == STATION_PENDING && + !IEC104_Station_Is_Registered(entry->_station)) { + entry->_state = STATION_ENDED; /* 未选择/未确认的退役站不启动,不发成功终止。 */ + } + if ((self->broadcast.stage == BROADCAST_ACCEPT && entry->_state == STATION_PENDING) || + (self->broadcast.stage == BROADCAST_DATA && entry->_state == STATION_CONFIRMED) || + (self->broadcast.stage == BROADCAST_TERMINATE && entry->_state == STATION_DATA_DONE)) { + return Select_Broadcast_Station(self, index); + } + } + ++self->broadcast.stage; + index = 0U; + } + self->broadcast.task->phase = IEC104_INTERROGATION_FINISHING; + self->broadcast.task->send_waiting = false; + Flush_Broadcast_Finishes(self); + Reap_Broadcast(self); /* 无待读终结记录时立即回收,不依赖下一轮网络唤醒。 */ + return true; +} + +/* 已确认但尚未轮到取数、或等最后终止的站也仍是活动事务;尚未选中的PENDING站不算活动。 */ +static bool Broadcast_Station_Active(const IEC104_Interrogation_t *self, size_t index) +{ + const IEC104_InterrogationStationState_t *entry = &self->config.broadcast_states[index]; + return entry->_state != STATION_ENDED && + (entry->_state != STATION_PENDING || index == self->broadcast.index); +} + +/* 撤销匹配本轮原目标,允许找到已退役但仍活动的站;广播地址匹配任一活动站,count表示无匹配。 */ +static size_t Find_Broadcast_Activation(const IEC104_Interrogation_t *self, uint16_t common_address) +{ + size_t i; + for (i = 0U; i < self->broadcast.count; ++i) { + if (Broadcast_Station_Active(self, i) && + (common_address == IEC104_CA_BROADCAST || + IEC104_Station_Get_Common_Address(self->config.broadcast_states[i]._station) == common_address)) { return i; } + } + return self->broadcast.count; +} + +static size_t Pending_Broadcast_Deactivation(const IEC104_Interrogation_t *self) +{ + size_t i; + for (i = 0U; i < self->broadcast.count; ++i) { + if ((self->config.broadcast_states[i]._flags & STATION_DEACTIVATE) != 0U) { return i; } + } + return self->broadcast.count; +} + +/* 在原任务内保存撤销应答,不要求额外队列槽;第二条重叠撤销在此应答完成前按未匹配拒绝。 + * 先记录各站原活动状态再终结,只有活动站欠一次肯定COT=9;全局撤销也结束尚未启动的未来站。 + * 定向撤销保留其他站进度,待这组撤销确认提交后继续原阶段;不覆盖其他站已有终结原因。 */ +static void Deactivate_Broadcast(IEC104_Interrogation_t *self, uint16_t common_address) +{ + size_t i; + self->broadcast.cancel_all = common_address == IEC104_CA_BROADCAST; + for (i = 0U; i < self->broadcast.count; ++i) { + IEC104_InterrogationStationState_t *entry = &self->config.broadcast_states[i]; + bool active = Broadcast_Station_Active(self, i); + if (self->broadcast.cancel_all || + (active && IEC104_Station_Get_Common_Address(entry->_station) == common_address)) { + if (active) { entry->_flags |= STATION_DEACTIVATE; } + End_Broadcast_Station(self, i, IEC104_INTERROGATION_FINISH_DEACTIVATED); + } + } + self->broadcast.cancelling = true; + self->broadcast.task->send_waiting = false; + Move_Reply_To_Tail(self, self->broadcast.task); + Flush_Broadcast_Finishes(self); + ++self->status.deactivated; + ++self->status.cancelled; +} + +/* 只在对应COT=9被发送层接手后清标志;最后一条完成后收尾整轮,或恢复未被定向撤销的工作。 */ +static bool Advance_Broadcast_Deactivation(IEC104_Interrogation_t *self, size_t index) +{ + self->config.broadcast_states[index]._flags &= (uint8_t)~STATION_DEACTIVATE; + self->broadcast.task->send_waiting = false; + if (Pending_Broadcast_Deactivation(self) != self->broadcast.count) { return true; } + self->broadcast.cancelling = false; + if (self->broadcast.cancel_all) { + self->broadcast.stage = BROADCAST_FINISH; + self->broadcast.task->phase = IEC104_INTERROGATION_FINISHING; + Reap_Broadcast(self); + return true; + } + return Broadcast_Current(self)->_state == STATION_ENDED ? Next_Broadcast_Station(self) : true; +} + +/* 每次发送只建立临时ASDU视图;WAIT保留紧凑原文和身份,输入Slave事件始终不变。 */ +static bool Prepare_Response(IEC104_Interrogation_t *self, InterrogationResponse *request, + uint8_t cause, bool negative, IEC60870_Asdu_t *asdu) +{ + return IEC60870_Asdu_Create_From_Buffer(asdu, IEC104_Slave_Get_Application_Layer_Parameters(self->slave), + request->bytes, sizeof(request->bytes)) == IEC60870_ASDU_OK && + IEC60870_Asdu_Set_COT(asdu, cause) == IEC60870_ASDU_OK && + IEC60870_Asdu_Set_Negative(asdu, negative) == IEC60870_ASDU_OK; +} + +/* 仅对ERROR复核正常STOP拒绝,避免在消费DEACTIVATED前误记SEND_ERROR。 + * 须同时确认原代次仍连接、会话/队列无故障、传输已停用且有未读通知;普通新通知不足以放行。 + * OK/WAIT沿原路径处理,FATAL可能已发送字节,绝不降级为等待;本函数不消费通知或重置期限。 */ +static bool Defer_For_Stop(IEC104_Interrogation_t *self, + const IEC104_SlaveEventIdentity_t *identity, IEC104_TcpSubmitResult_t submitted) +{ + IEC104_TcpServerStatus_t status; + if (submitted != IEC104_TCP_SUBMIT_ERROR) { return false; } + if (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; +} + +static ImmediateReplyResult Try_Immediate_Reply(IEC104_Interrogation_t *self, + InterrogationResponse *request, + uint8_t cause, bool negative, const InterrogationTask *queued) +{ + IEC60870_Asdu_t asdu; + IEC104_TcpSubmitResult_t submitted; + IEC104_SlaveRequestState_t state; + unsigned first = Find_Reply(self); + if (first != IEC104_INTERROGATION_TASK_CAPACITY && &self->tasks[first] != queued) { return REPLY_DEFER; } + state = IEC104_Slave_Check_Request(self->slave, &self->session); + self->status.events_pending = state == IEC104_SLAVE_REQUEST_WAIT_EVENTS; + if (self->status.events_pending) { return REPLY_DEFER; } + if (state == IEC104_SLAVE_REQUEST_ERROR || !self->status.active || state != IEC104_SLAVE_REQUEST_READY) { + Cancel(self, state == IEC104_SLAVE_REQUEST_ERROR ? IEC104_INTERROGATION_FINISH_SERVICE_ERROR : + IEC104_INTERROGATION_FINISH_CONNECTION_CHANGED); + self->status.active = false; + return REPLY_FAILED; + } + if (!Prepare_Response(self, request, cause, negative, &asdu)) { return REPLY_FAILED; } + submitted = IEC104_Slave_Send_Response(self->slave, &request->identity, &asdu); + if (submitted == IEC104_TCP_SUBMIT_OK) { + ++self->status.submitted; + if (negative) { ++self->status.negative_submitted; } + self->prefer_interrogation = true; + return REPLY_SENT; + } + if (submitted == IEC104_TCP_SUBMIT_WAIT) { + ++self->status.waits; + self->status.waiting = true; + return REPLY_WAIT; + } + if (Defer_For_Stop(self, &request->identity, submitted)) { return REPLY_DEFER; } + Cancel(self, IEC104_INTERROGATION_FINISH_SEND_ERROR); + self->status.active = false; + return REPLY_FAILED; +} + +/* 单次只读期限计算的结果,不缓存到任务;已确认广播站总超时决定整轮必须断连。 */ +typedef struct { + IEC104_InterrogationFinishReason_t reason; + uint32_t remaining_ms; + bool confirmed_total_expired; +} InterrogationTimeout; + +/* 将一项期限并入当前最近期限:now/since为单调毫秒,duration为允许时长。 + * timeout是累积输入输出;reason为NONE表示此前还没有有效候选。 + * 用经过时间比较,避免绝对期限相加溢出;0时长禁用,到期剩余值为0。 + * 相同剩余时间保留先检查的原因,因此Task_Timeout先检查总期限以确定并发超时的归因。 */ +static void Limit_Timeout(uint64_t now, uint64_t since, uint32_t duration, + IEC104_InterrogationFinishReason_t candidate, InterrogationTimeout *timeout) +{ + uint64_t elapsed; + uint32_t left; + if (duration == 0U) { return; } + elapsed = now - since; + left = elapsed >= duration ? 0U : duration - (uint32_t)elapsed; + if (timeout->reason == IEC104_INTERROGATION_FINISH_NONE || left < timeout->remaining_ms) { + timeout->remaining_ms = left; + timeout->reason = candidate; + } +} + +/* 只读计算task在同一now时刻的最近期限及已确认站超时标志;reason为NONE表示没有启用的期限。 + * 只有尚未终结的激活业务受定时器约束,单帧应答不参加业务超时计算。 + * START尚未生成BEGIN时不计业务期限;读取任务的总期限从BEGIN生成起覆盖取数及发送等待。 + * 动作命令接受后已执行副作用,CONFIRM不再计业务期限;发送层仍处理连接期限。 + * BEGIN/NEED_DATA期限从工作事件入队起计算;首次有效批次发送WAIT后不再计本批取数期限。 + * 本函数不触发超时动作,供Expire执行处理和Get_Status计算唤醒时间共用。 */ +static InterrogationTimeout Task_Timeout(const IEC104_Interrogation_t *self, + const InterrogationTask *task, uint64_t now) +{ + InterrogationTimeout timeout = {IEC104_INTERROGATION_FINISH_NONE, 0U, false}; + if (Is_Broadcast(self, task)) { + size_t i; + /* 当前任务可能已切到后续站的BEGIN,先前已确认站的总期限仍独立有效,不能只查当前站。 */ + for (i = 0U; i < self->broadcast.count; ++i) { + const IEC104_InterrogationStationState_t *entry = &self->config.broadcast_states[i]; + if (entry->_state == STATION_CONFIRMED || entry->_state == STATION_DATA_DONE) { + Limit_Timeout(now, entry->_started_ms, self->config.total_timeout_ms, + IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT, &timeout); + } + } + /* 此时只汇入了已确认站的总期限;先保存到期事实,再合入当前站的期限。 */ + timeout.confirmed_total_expired = timeout.reason == IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT && + timeout.remaining_ms == 0U; + if (self->config.broadcast_states[self->broadcast.index]._state == STATION_ENDED) { return timeout; } + } + if (task->request_id == 0U || task->phase == IEC104_INTERROGATION_REPLY || + task->phase == IEC104_INTERROGATION_FINISHING) { return timeout; } + if (task->phase == IEC104_INTERROGATION_START && !task->event_outstanding) { return timeout; } + /* 已接受的动作可能已经改变计数器,不能因确认排队超时再改发否定确认。 + * 这里只停止当前动作站的业务期限;传输期限、STOP和断线仍有效。 */ + if (task->phase == IEC104_INTERROGATION_CONFIRM && !self->rules->has_data(task->info.freeze)) { return timeout; } + Limit_Timeout(now, task->started_ms, self->config.total_timeout_ms, + IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT, &timeout); + if (task->event_outstanding && !task->data_send_waiting) { + bool begin = task->phase == IEC104_INTERROGATION_START; + Limit_Timeout(now, task->application_since_ms, + begin ? self->config.begin_timeout_ms : self->config.data_timeout_ms, + begin ? IEC104_INTERROGATION_FINISH_BEGIN_TIMEOUT : IEC104_INTERROGATION_FINISH_DATA_TIMEOUT, + &timeout); + } + return timeout; +} + +/* 只检查唯一未终结总召;确认前超时转为否定REPLY,确认后超时关闭原连接并取消。 + * 已交付BEGIN的FINISHED有预留槽或每站记录,迟到应答随即失效;已终结站不再计算期限。 + * 调用方须先确认Slave通知已读空,使先到的STOP/断线/撤销先于超时处理。 + * 返回0为未超时,1为已处理超时,-1为任务已取消但底层关闭失败。 */ +static int Expire(IEC104_Interrogation_t *self) +{ + unsigned slot = Find_Interrogation(self, 0U); + InterrogationTask *task; + InterrogationTimeout timeout; + if (slot == IEC104_INTERROGATION_TASK_CAPACITY) { return 0; } + task = &self->tasks[slot]; + timeout = Task_Timeout(self, task, IEC104_Net_Now_Ms()); + if (timeout.reason != IEC104_INTERROGATION_FINISH_NONE && timeout.remaining_ms == 0U) { + ++self->status.timeouts; + if (timeout.confirmed_total_expired) { + /* 若超时的是先前已确认站,即使当前站尚在START,也必须按确认后失败结束原连接。 */ + return Reject_Connection(self, &task->request.identity, timeout.reason) ? 1 : -1; + } + return Fail_Interrogation(self, task, timeout.reason) ? 1 : -1; + } + return 0; +} + +/* 查找唯一未终结总召;它可能等待BEGIN或排在错误应答后面,不能只看任务队首。 + * event/info/qualifier来自已校验的撤销请求,response_ca已解析为实际站CA; + * 原连接代次、实际站CA、OA、TEST和QOI须全部相同。 + * 广播与定向地址解析到同一本站,允许广播激活后定向撤销,也允许广播撤销本站任务。 + * 返回内部任务指针供Request原位改为撤销确认;NULL表示没有匹配的未终结激活任务。 */ +static InterrogationTask *Find_Activation(IEC104_Interrogation_t *self, + const IEC104_SlaveEvent_t *event, const IEC60870_AsduInfo_t *info, + uint16_t response_ca, uint8_t qualifier) +{ + unsigned i; + for (i = 0U; i < self->count; ++i) { + InterrogationTask *task = &self->tasks[self->order[i]]; + if (Is_Broadcast(self, task)) { + if (!self->broadcast.cancelling && task->phase != IEC104_INTERROGATION_FINISHING && + task->info.generation == event->transport.generation && + task->info.originator_address == info->originator_address && + task->info.is_test == (info->is_test != 0U) && task->info.qualifier == qualifier && + Find_Broadcast_Activation(self, info->common_address) != self->broadcast.count) { return task; } + continue; + } + if (task->request_id != 0U && + (task->phase != IEC104_INTERROGATION_REPLY || + (task->stations._owner != NULL && info->common_address == IEC104_CA_BROADCAST)) && + task->info.generation == event->transport.generation && + (task->info.common_address == response_ca || info->common_address == IEC104_CA_BROADCAST) && + task->info.originator_address == info->originator_address && + task->info.is_test == (info->is_test != 0U) && task->info.qualifier == qualifier) { return task; } + } + return NULL; +} + +/* 解析一个I帧通知中的总召请求,完成布局、地址、QOI和事务状态校验。 + * 未启用时或有效的其他Type返回IGNORED;接手、拒绝或满槽丢弃请求均返回HANDLED。 + * 公共头不可用交公共入口处理;总召对象不明确则丢弃,单对象完整但布局非法则仅否定。 + * 请求错误和容量耗尽均不取消已有任务;只有原会话或内部服务故障沿用原失败策略。 + * 单帧应答先尝试安全提交,未接手才保存;匹配的撤销复用原任务槽。 + * 只有空闲时才建立START任务;此时已占用总召资格,Process有事件空位后生成BEGIN。 */ +static IEC104_InterrogationEventResult_t Request(IEC104_Interrogation_t *self, + const IEC104_SlaveEvent_t *event, const IEC104_SlaveEventIdentity_t *identity) +{ + IEC60870_AsduInfo_t info; + uint32_t ioa; + uint8_t qualifier, freeze; + InterrogationResponse response; + const uint8_t *data; + uint16_t length; + uint16_t response_ca; + uint8_t reply_cause = 0U; + bool deactivation; + bool overload_counted = false; + bool send_waiting = false; + bool broadcast; + bool known_address; + bool invalid_layout; + size_t station_count; + InterrogationTask *activation; + InterrogationTask *task; + if (!self->status.active) { return IEC104_INTERROGATION_EVENT_IGNORED; } + /* 来源/身份已由Handle_Event验证;先保护长度再读取类型,不替其他业务处理ASDU错误。 */ + if (event->transport.data.i_frame.apdu_length <= IEC104_APCI_LEN || + event->transport.data.i_frame.apdu_length > sizeof(event->transport.data.i_frame.apdu) || + event->transport.data.i_frame.apdu[IEC104_APCI_LEN] != self->rules->type_id) { + return IEC104_INTERROGATION_EVENT_IGNORED; + } + if (event->asdu_result != IEC60870_ASDU_OK || + IEC60870_Asdu_Get_Data(&event->asdu, &data, &length) != IEC60870_ASDU_OK || + !IEC60870_Parse_ASDU_Header(event->asdu.parameters, data, length, &info)) { + return IEC104_INTERROGATION_EVENT_IGNORED; + } + if (info.object_count != 1U || info.information_objects_len < 3U + 1U) { + /* 无唯一完整IOA/QOI,不能猜测或选取多个对象之一应答;HANDLED防止进入未知类型兜底。 */ + ++self->status.rejected; + return IEC104_INTERROGATION_EVENT_HANDLED; + } + invalid_layout = info.is_sequence != 0U || length != sizeof(response.bytes); + if (invalid_layout) { + /* 只为拒绝应答读取唯一对象;不修改原文。原布局错误必须在匹配撤销/建立激活前拦截。 */ + info.is_sequence = 0U; + info.information_objects_len = 3U + 1U; + } + if ((!invalid_layout && IEC60870_Asdu_Parse(event->asdu.parameters, data, length, &info) != IEC60870_ASDU_OK) || + !self->rules->decode(&info, &ioa, &qualifier, &freeze)) { + ++self->status.rejected; + return IEC104_INTERROGATION_EVENT_HANDLED; + } + /* 只决定应答地址,不改变原请求;后续仍按COT优先级决定是否接受及如何否定。 */ + response_ca = Resolve_Common_Address(self, info.common_address); + deactivation = self->rules->has_deactivation && info.cause_of_transmission == IEC60870_ASDU_COT_DEACTIVATION; + activation = deactivation && !invalid_layout ? Find_Activation(self, event, &info, response_ca, qualifier) : NULL; + if (activation != NULL) { + response_ca = Is_Broadcast(self, activation) && info.common_address != IEC104_CA_BROADCAST ? + info.common_address : activation->info.common_address; + } + station_count = IEC104_Slave_Get_Station_Count(self->slave); + broadcast = self->config.use_station_registry && info.common_address == IEC104_CA_BROADCAST && + station_count != 0U; + known_address = activation != NULL || (self->config.use_station_registry ? + IEC104_Slave_Get_Station(self->slave, response_ca) != NULL : + response_ca == self->config.common_address && + (self->config.station == NULL || IEC104_Station_Is_Registered(self->config.station))); + if (!Copy_Response_Request(self, &response, identity, data, length, response_ca)) { + return IEC104_INTERROGATION_EVENT_ERROR; + } + /* 无歧义对象才可回应;错误原因优先级COT -> CA -> IOA -> 布局/QOI/P/N/事务状态。 */ + if (info.cause_of_transmission != IEC60870_ASDU_COT_ACTIVATION && !deactivation) { + reply_cause = IEC60870_ASDU_COT_UNKNOWN_CAUSE; + } else if (!known_address) { + reply_cause = IEC60870_ASDU_COT_UNKNOWN_COMMON_ADDRESS; + } else if (ioa != 0U) { + reply_cause = IEC60870_ASDU_COT_UNKNOWN_IOA; + } else if (invalid_layout || !self->rules->qualifier_valid(qualifier) || info.is_negative_confirm != 0U) { + reply_cause = deactivation ? IEC60870_ASDU_COT_DEACTIVATION_CON : IEC60870_ASDU_COT_ACTIVATION_CON; + } else if (deactivation) { + task = activation; + if (task != NULL) { + ImmediateReplyResult sent; + if (Is_Broadcast(self, task)) { + Deactivate_Broadcast(self, info.common_address); + return IEC104_INTERROGATION_EVENT_HANDLED; + } + /* 原位替换为撤销确认,不要求额外任务槽。 + * 先保存撤销请求,Finish仍使用原激活的info/id/进度通知应用清理。 + * 已提交HIGH的帧按原顺序在确认前发出;本地未提交批次就此丢弃。 */ + task->request = response; + Finish(self, task, IEC104_INTERROGATION_FINISH_DEACTIVATED); + task->phase = IEC104_INTERROGATION_REPLY; + task->reply_cause = IEC60870_ASDU_COT_DEACTIVATION_CON; + task->reply_negative = false; + task->data_send_waiting = false; + task->send_waiting = false; + /* 广播撤销终结余下遍历;定向撤销只终结当前站,确认后继续下一站。 */ + if (info.common_address == IEC104_CA_BROADCAST && task->stations._owner != NULL) { + (void)IEC104_Slave_StationSnapshot_End(&task->stations); + } + if (task->stations._owner == NULL) { task->request_id = 0U; } + Move_Reply_To_Tail(self, task); + self->status.waiting = false; + ++self->status.deactivated; + ++self->status.cancelled; + sent = Try_Immediate_Reply(self, &task->request, task->reply_cause, false, task); + if (sent == REPLY_SENT && !Advance(self, task)) { return IEC104_INTERROGATION_EVENT_ERROR; } + if (sent == REPLY_FAILED) { return IEC104_INTERROGATION_EVENT_ERROR; } + if (sent == REPLY_WAIT) { task->send_waiting = true; } + return IEC104_INTERROGATION_EVENT_HANDLED; + } + reply_cause = IEC60870_ASDU_COT_DEACTIVATION_CON; + } else if (Find_Interrogation(self, 0U) != IEC104_INTERROGATION_TASK_CAPACITY || self->broadcast.task != NULL) { + /* 从保存激活到终结全程占用总召资格;相同/不同QOI都否定,不替换原请求。 + * FINISHING虽不再有取数任务,广播状态区仍被未读FINISHED占用,必须继续拒绝新激活。 + * 此判断仅作用于Type100激活,其他业务及匹配的COT=8撤销不受忙状态拦截。 */ + reply_cause = IEC60870_ASDU_COT_ACTIVATION_CON; + } + /* 发出任何肯定确认前检查整轮状态容量;不足时整轮拒绝,不启动只能覆盖部分站点的广播。 */ + if (reply_cause == 0U && broadcast && station_count > 1U && + station_count > self->config.broadcast_capacity) { + ++self->status.overloads; + overload_counted = true; + reply_cause = IEC60870_ASDU_COT_ACTIVATION_CON; + } + /* 合法的新总召没有空槽时改为否定ACT_CON,仍可尝试提交,不影响当前任务。 */ + if (reply_cause == 0U && self->count == IEC104_INTERROGATION_TASK_CAPACITY) { + ++self->status.overloads; + overload_counted = true; + reply_cause = IEC60870_ASDU_COT_ACTIVATION_CON; + } + if (reply_cause != 0U) { + ImmediateReplyResult sent; + ++self->status.rejected; + /* 多站应答先占一个遍历任务,不能发出部分站应答后才发现无处保存余下游标。 */ + sent = broadcast ? REPLY_DEFER : Try_Immediate_Reply(self, &response, reply_cause, true, NULL); + if (sent == REPLY_SENT) { return IEC104_INTERROGATION_EVENT_HANDLED; } + if (sent == REPLY_FAILED) { return IEC104_INTERROGATION_EVENT_ERROR; } + send_waiting = sent == REPLY_WAIT; + if (self->count == IEC104_INTERROGATION_TASK_CAPACITY) { + if (!overload_counted) { ++self->status.overloads; } + ++self->status.dropped_requests; + return IEC104_INTERROGATION_EVENT_HANDLED; /* 无法保存/提交的新请求已丢弃;不能重投或断链。 */ + } + } + if (reply_cause == 0U && (self->next_request_id == UINT64_MAX || + (broadcast && station_count > UINT64_MAX - self->next_request_id))) { + return Reject_Connection(self, identity, IEC104_INTERROGATION_FINISH_SERVICE_ERROR) ? + IEC104_INTERROGATION_EVENT_HANDLED : IEC104_INTERROGATION_EVENT_ERROR; + } + task = Allocate_Task(self); + if (task == NULL) { + (void)Reject_Connection(self, identity, IEC104_INTERROGATION_FINISH_SERVICE_ERROR); + return IEC104_INTERROGATION_EVENT_ERROR; + } + task->request = response; + task->reply_cause = reply_cause; + task->reply_negative = reply_cause != 0U; + task->phase = reply_cause != 0U ? IEC104_INTERROGATION_REPLY : IEC104_INTERROGATION_START; + task->send_waiting = send_waiting; + task->info.generation = event->transport.generation; + task->info.receive_serial = event->transport.data.i_frame.receive_serial; + task->info.common_address = response_ca; /* 合法总召均为本站CA;错误应答保留原错误CA且不交付BEGIN。 */ + task->info.originator_address = info.originator_address; + task->info.qualifier = qualifier; + task->info.freeze = freeze; + task->info.data_cause = self->rules->data_cause(qualifier); + task->info.is_test = info.is_test != 0U; + task->info.max_objects_per_asdu = self->config.max_objects_per_asdu; + if (reply_cause == 0U) { + task->request_id = ++self->next_request_id; + } + if (broadcast && reply_cause == 0U && station_count > 1U) { + if (!Capture_Broadcast(self, task)) { + (void)Reject_Connection(self, identity, IEC104_INTERROGATION_FINISH_SERVICE_ERROR); + return IEC104_INTERROGATION_EVENT_ERROR; + } + } else if (broadcast) { + /* 单目标无须三阶段状态数组;错误广播仅逐站应答,使用共用快照保存范围和游标即可。 */ + if (IEC104_Slave_StationSnapshot_Begin(self->slave, &task->stations) != IEC104_STATION_OK || + !Bind_Station(self, task, IEC104_Slave_StationSnapshot_Next(&task->stations))) { + (void)Reject_Connection(self, identity, IEC104_INTERROGATION_FINISH_SERVICE_ERROR); + return IEC104_INTERROGATION_EVENT_ERROR; + } + } else if (reply_cause == 0U && (self->config.use_station_registry || self->config.station != NULL)) { + IEC104_Station_t *station = self->config.use_station_registry ? + IEC104_Slave_Get_Station(self->slave, response_ca) : self->config.station; + if (!Bind_Station(self, task, station)) { + (void)Reject_Connection(self, identity, IEC104_INTERROGATION_FINISH_SERVICE_ERROR); + return IEC104_INTERROGATION_EVENT_ERROR; + } + } + return IEC104_INTERROGATION_EVENT_HANDLED; +} + +/* 为总召任务生成一次BEGIN或NEED_DATA,并标记等待应用应答;task须处于START或DATA。 + * event_outstanding为true时复用现有交互,不重复入队或重置等待起点。 + * 单目标BEGIN需要自身和未来FINISHED两个位置;多目标终结另存状态项,BEGIN仅占一个位置。 + * 优先将已有终结记录入队;新工作事件不能占用单目标预留位置或挤掉尚未交付的清理通知。 + * BEGIN前等FIFO空位不计业务期限,但已占用总召资格;已启用的整体期限从BEGIN生成起算。 + * NEED_DATA等FIFO空位仍受已启用的整体期限约束;每次工作事件入队才启动本次应用应答期限。 */ +static void Request_Application(IEC104_Interrogation_t *self, InterrogationTask *task) +{ + self->status.waiting = true; + if (task->event_outstanding) { return; } + Flush_Broadcast_Finishes(self); + if (task->phase == IEC104_INTERROGATION_START) { + unsigned needed = Is_Broadcast(self, task) ? 1U : 2U; + if (self->event_count + self->reserved_finishes + needed > IEC104_INTERROGATION_EVENT_CAPACITY) { return; } + if (!Is_Broadcast(self, task)) { + task->finish_reserved = true; + ++self->reserved_finishes; + } + Push_Event(self, task, IEC104_INTERROGATION_BEGIN, IEC104_INTERROGATION_FINISH_NONE); + } else { + if (self->event_count + self->reserved_finishes >= IEC104_INTERROGATION_EVENT_CAPACITY) { return; } + Push_Event(self, task, IEC104_INTERROGATION_NEED_DATA, IEC104_INTERROGATION_FINISH_NONE); + } + task->event_outstanding = true; + task->event_delivered = false; + task->data_send_waiting = false; + task->application_since_ms = IEC104_Net_Now_Ms(); + if (task->phase == IEC104_INTERROGATION_START) { + task->started_ms = task->application_since_ms; + if (Is_Broadcast(self, task)) { Broadcast_Current(self)->_started_ms = task->started_ms; } + } +} + +/* 应用应答的共同入口检查;expected指定应答BEGIN还是NEED_DATA,不在这里消费交互。 + * 核对实例、请求、批次、类型、阶段和已取出标记,旧事件或重复应答返回STALE。 + * Slave仍有通知时返回WAIT,调用方先按FIFO处理它们,防止跳过STOP/撤销/断线。 + * 当前会话失效时取消;业务超时按确认边界终结,两者均返回STALE;参数/状态查询失败返回ERROR。 + * OK只表示本次可以继续校验和应用应答;实际请求元数据始终取内部副本,不信任回传字段。 */ +static IEC104_InterrogationReplyResult_t Check_Reply(IEC104_Interrogation_t *self, + const IEC104_InterrogationEvent_t *event, IEC104_InterrogationEventType_t expected, + InterrogationTask **matched_task) +{ + InterrogationTask *task; + IEC104_SlaveRequestState_t state; + unsigned slot; + if (!Owned(self) || event == NULL) { return IEC104_INTERROGATION_REPLY_ERROR; } + slot = Find_Interrogation(self, event->request_id); + if (slot == IEC104_INTERROGATION_TASK_CAPACITY) { return IEC104_INTERROGATION_REPLY_STALE; } + task = &self->tasks[slot]; + if (event->instance_id != self->instance_id || event->request_id != task->request_id || + event->type != expected || event->batch_index != task->batch_index || + !task->event_outstanding || !task->event_delivered || + (expected == IEC104_INTERROGATION_BEGIN && task->phase != IEC104_INTERROGATION_START) || + (expected == IEC104_INTERROGATION_NEED_DATA && task->phase != IEC104_INTERROGATION_DATA)) { + return IEC104_INTERROGATION_REPLY_STALE; + } + state = IEC104_Slave_Check_Request(self->slave, &self->session); + if (state == IEC104_SLAVE_REQUEST_ERROR) { + return IEC104_INTERROGATION_REPLY_ERROR; + } + self->status.events_pending = state == IEC104_SLAVE_REQUEST_WAIT_EVENTS; + if (self->status.events_pending) { return IEC104_INTERROGATION_REPLY_WAIT; } + if (!self->status.active || state != IEC104_SLAVE_REQUEST_READY) { + Cancel(self, IEC104_INTERROGATION_FINISH_CONNECTION_CHANGED); + self->status.active = false; + return IEC104_INTERROGATION_REPLY_STALE; + } + if (Expire(self) != 0) { return IEC104_INTERROGATION_REPLY_STALE; } + *matched_task = task; /* 仅OK交付内部任务地址,调用方不再假定它位于队首。 */ + return IEC104_INTERROGATION_REPLY_OK; +} + +/* 标记当前工作事件已经应答,解除应用等待;只在应答内容校验并接手后调用。 + * 关闭event_outstanding也停止本次BEGIN/NEED_DATA期限,后续事件会设置新的计时起点。 */ +static void Consume_Reply(IEC104_Interrogation_t *self, InterrogationTask *task) +{ + task->event_outstanding = task->event_delivered = false; + task->data_send_waiting = false; + task->send_waiting = false; + self->status.waiting = false; +} + +/* Process或Submit_Batch确认发送层已接手ASDU后推进所选任务;发送WAIT/失败时不得调用。 + * 单目标CONFIRM转入DATA;多目标依次遍历确认、数据、终止三阶段。 + * DATA由Submit_Batch累计进度;TERMINATE生成FINISHED(COMPLETED),REPLY结束当前站。 + * 释放时只移动顺序索引,保持其他任务的槽位和调度顺序;此处不等待主站ACK。 */ +static bool Advance(IEC104_Interrogation_t *self, InterrogationTask *task) +{ + bool action_complete = task->phase == IEC104_INTERROGATION_CONFIRM && !self->rules->has_data(task->info.freeze); + task->send_waiting = false; + if (Is_Broadcast(self, task)) { + if (task->phase == IEC104_INTERROGATION_DATA) { + ++task->batch_index; + Save_Broadcast_Progress(self); + return true; + } + if (task->phase == IEC104_INTERROGATION_REPLY && self->broadcast.cancelling) { + task->reply_cause = 0U; /* 当前站的否定确认已接手,接着兑现其他站的撤销确认。 */ + return true; + } + if (task->phase == IEC104_INTERROGATION_CONFIRM && !action_complete) { + Broadcast_Current(self)->_state = STATION_CONFIRMED; + } else if (task->phase == IEC104_INTERROGATION_TERMINATE || action_complete) { + ++self->status.gi_submitted; + Finish(self, task, IEC104_INTERROGATION_FINISH_COMPLETED); + } + return Next_Broadcast_Station(self); + } + if (task->phase == IEC104_INTERROGATION_CONFIRM && !action_complete) { + task->phase = IEC104_INTERROGATION_DATA; + } else if (task->phase == IEC104_INTERROGATION_DATA) { + ++task->batch_index; + } else { + if (task->phase == IEC104_INTERROGATION_TERMINATE || action_complete) { + ++self->status.gi_submitted; + Finish(self, task, IEC104_INTERROGATION_FINISH_COMPLETED); + } + if (task->stations._owner != NULL) { + IEC104_Station_t *next; + if (task->station != NULL) { + (void)IEC104_Slave_Release_Station(self->slave, task->station); + task->station = NULL; + } + next = IEC104_Slave_StationSnapshot_Next(&task->stations); + if (next != NULL) { + if (!Bind_Station(self, task, next) || + (task->request_id != 0U && self->next_request_id == UINT64_MAX)) { + return false; + } + if (task->request_id != 0U) { + task->request_id = ++self->next_request_id; + task->phase = IEC104_INTERROGATION_START; + task->reply_cause = 0U; + task->reply_negative = false; + } + task->started_ms = task->application_since_ms = 0U; + task->batch_index = task->objects_submitted = 0U; + task->data_send_waiting = false; + return true; + } + } + Remove_Task(self, task); + } + return true; +} + +/** + * 功能:初始化调用方提供的配置,设置站地址、每批对象数以及应用应答和执行期限。 + * 参数:config为可写配置对象;NULL时不执行任何操作。 + * 说明:先清零再设置默认值,不创建实例或占用资源;应用可在Create前修改配置。 + */ +void IEC104_Interrogation_Config_Default(IEC104_InterrogationConfig_t *config) +{ + if (config == NULL) { return; } + memset(config, 0, sizeof(*config)); + config->common_address = 1U; + config->max_objects_per_asdu = 127U; + config->begin_timeout_ms = 15000U; + config->data_timeout_ms = 15000U; + config->total_timeout_ms = 0U; +} + +/** + * 功能:从静态池创建总召实例,复制配置并借用一个有效Slave。 + * 参数:slave须在本实例整个寿命内保持有效;config可在成功返回后复用。 + * 返回:新实例句柄;配置非法、Slave无效、重复绑定、池满或编号耗尽时返回NULL。 + * 说明:扫描完整静态池拒绝重复绑定,失败不占槽;实例编号在整个进程内不重复。 + * 创建时尚无活动会话,后续依靠Handle_Event接收CONNECTED/ACTIVATED建立状态。 + */ +IEC104_Interrogation_t *IEC104_Interrogation_Create_With_Rules(IEC104_Slave_t *slave, + const IEC104_InterrogationConfig_t *config, const IEC104_InterrogationRules_t *rules) +{ + size_t i; + uintptr_t storage_begin, storage_end; + IEC104_Interrogation_t *free_slot = NULL; + if (rules == NULL || rules->decode == NULL || rules->qualifier_valid == NULL || + rules->data_cause == NULL || rules->data_allowed == NULL || rules->has_data == NULL || + config == NULL || (config->use_station_registry && config->station != NULL) || + (!config->use_station_registry && config->station == NULL && + (config->common_address == 0U || config->common_address == IEC104_CA_BROADCAST)) || + config->max_objects_per_asdu == 0U || config->max_objects_per_asdu > 127U || + IEC104_Slave_Get_Application_Layer_Parameters(slave) == NULL || instance_serial == UINT64_MAX) { return NULL; } + if ((config->broadcast_states == NULL) != (config->broadcast_capacity == 0U) || + config->broadcast_capacity > SIZE_MAX / sizeof(*config->broadcast_states)) { return NULL; } + storage_begin = (uintptr_t)config->broadcast_states; + if (storage_begin > UINTPTR_MAX - config->broadcast_capacity * sizeof(*config->broadcast_states)) { return NULL; } + storage_end = storage_begin + config->broadcast_capacity * sizeof(*config->broadcast_states); + for (i = 0U; i < IEC104_INTERROGATION_CAPACITY; ++i) { + if (interrogation_pool[i].slave == slave && interrogation_pool[i].rules == rules) { return NULL; } + if (config->broadcast_states != NULL && interrogation_pool[i].slave != NULL) { + const IEC104_InterrogationConfig_t *other = &interrogation_pool[i].config; + uintptr_t other_begin = (uintptr_t)other->broadcast_states; + uintptr_t other_end = other_begin + other->broadcast_capacity * sizeof(*other->broadcast_states); + if (storage_begin < other_end && other_begin < storage_end) { return NULL; } + } + if (free_slot == NULL && interrogation_pool[i].slave == NULL) { free_slot = &interrogation_pool[i]; } + } + if (free_slot != NULL) { + if (config->station != NULL && !IEC104_Station_Is_Registered(config->station)) { return NULL; } + if (config->station != NULL && + IEC104_Slave_Acquire_Station(slave, config->station) != IEC104_STATION_OK) { return NULL; } + memset(free_slot, 0, sizeof(*free_slot)); + free_slot->slave = slave; + free_slot->rules = rules; + free_slot->config = *config; + if (config->station != NULL) { + free_slot->config.common_address = IEC104_Station_Get_Common_Address(config->station); + } + free_slot->instance_id = ++instance_serial; + } + return free_slot; +} + +/** + * 功能:释放已经没有协议任务和业务事件的实例槽,不销毁借用的Slave。 + * 参数:self为Create返回的有效实例。 + * 返回:true已释放,句柄失效;false为非法句柄或仍有任务/事件,原状态不变。 + * 说明:不会隐式Cancel或丢弃通知;调用方须先取消任务、读完FINISHED并清理应用资源。 + */ +bool IEC104_Interrogation_Destroy(IEC104_Interrogation_t *self) +{ + if (!Owned(self) || self->count != 0U || self->event_count != 0U) { return false; } + if (self->config.station != NULL && + IEC104_Slave_Release_Station(self->slave, self->config.station) != IEC104_STATION_OK) { return false; } + memset(self, 0, sizeof(*self)); + return true; +} + +/** + * 功能:以CANCELLED原因放弃本实例尚未提交的工作,保留终结事件供应用清理。 + * 参数:self为总召实例;非法句柄无操作。 + * 说明:不关闭连接、不发送确认或终止,也不撤回发送层已经接手的ASDU。 + * 该入口是应用本地取消;主站撤销请求由Handle_Event执行匹配和确认流程。 + */ +void IEC104_Interrogation_Cancel(IEC104_Interrogation_t *self) +{ + if (Owned(self)) { Cancel(self, IEC104_INTERROGATION_FINISH_CANCELLED); } +} + +/** + * 功能:处理应用转交的一个Slave历史通知,更新会话状态或保存总召请求。 + * 参数:self为有效实例;event须为同一Slave按FIFO取出的原事件或Copy_Event副本。 + * 返回:HANDLED已处理请求(含拒绝/满槽丢弃)或会话变化;IGNORED可交给其他业务;ERROR为参数/处理失败。 + * 说明:校验事件所属服务及代次,不自行读取Slave FIFO,不保存调用方事件指针。 + * 不调用应用代码;短应答在Slave通知已读空、会话有效且没有旧REPLY时尝试立即提交。 + * 布局错误或满槽只拒绝/丢弃新请求;取消时保留FINISHED,内部服务故障沿用失败策略。 + */ +IEC104_InterrogationEventResult_t IEC104_Interrogation_Handle_Event( + IEC104_Interrogation_t *self, const IEC104_SlaveEvent_t *event) +{ + IEC104_SlaveEventIdentity_t identity; + /* Slave负责来源和视图校验;本模块额外保持首次绑定的服务实例,不接管复用后的另一服务。 */ + if (!Owned(self) || !IEC104_Slave_Get_Event_Identity(self->slave, event, &identity) || + (self->session.instance_id != 0U && self->session.instance_id != identity.instance_id)) { + return IEC104_INTERROGATION_EVENT_ERROR; + } + self->session.slave = identity.slave; + self->session.instance_id = identity.instance_id; + if (event->transport.type == IEC104_TCP_EVENT_CONNECTED) { + /* TCP连通仅建立新代次;必须再收到ACTIVATED,才能接受本代次总召。 */ + Cancel(self, IEC104_INTERROGATION_FINISH_CONNECTION_CHANGED); + self->session.generation = identity.generation; + self->status.active = false; + return IEC104_INTERROGATION_EVENT_HANDLED; + } + if (identity.generation != self->session.generation) { + /* 旧连接遗留的断开、回执或I帧不能改变新连接的业务状态。 */ + return IEC104_INTERROGATION_EVENT_IGNORED; + } + switch (event->transport.type) { + case IEC104_TCP_EVENT_ACTIVATED: + self->status.active = true; + return IEC104_INTERROGATION_EVENT_HANDLED; + case IEC104_TCP_EVENT_DEACTIVATED: + case IEC104_TCP_EVENT_DISCONNECTED: + Cancel(self, event->transport.type == IEC104_TCP_EVENT_DEACTIVATED ? + IEC104_INTERROGATION_FINISH_STOPPED : IEC104_INTERROGATION_FINISH_DISCONNECTED); + self->status.active = false; + if (event->transport.type == IEC104_TCP_EVENT_DISCONNECTED) { self->session.generation = 0U; } + return IEC104_INTERROGATION_EVENT_HANDLED; + case IEC104_TCP_EVENT_I_FRAME: return Request(self, event, &identity); + default: return IEC104_INTERROGATION_EVENT_IGNORED; /* ACK回执不触发业务重发。 */ + } +} + +/** + * 功能:自动推进当前可处理的单帧应答和总召任务,直到空闲或需要等待。 + * 参数:self为有效实例;调用方不需要提供处理步数。 + * 返回:true为本轮正常结束,包括等待、取消或成功处理超时;false为参数、状态查询、组包或关闭失败。 + * 说明:Slave通知未读空时仅设置events_pending,应用先转交网络通知再继续调用。 + * START有事件空位时生成BEGIN并开始执行计时;DATA生成NEED_DATA,应用直接提交批次。 + * 双方可推进时轮转;总召等应用或业务FIFO空位时仍处理单帧应答。 + * 每步重查Slave通知,发送过程中新交付的通知也须先处理;只有接手ASDU后才Advance。 + * ERROR若确认为原连接正常STOP拒绝,则先让出交付通知;其他ERROR/FATAL仍为SEND_ERROR。 + * 发送WAIT保留任务并返回,下轮优先检查单帧应答。 + * 本函数不接收新请求、不执行应用代码;广播逐站复用任务,每轮至多2倍任务容量步。 + * 网络推进并读空Slave通知后须调用本函数重试;本地变化后按need_process及业务期限安排唤醒。 + */ +bool IEC104_Interrogation_Process(IEC104_Interrogation_t *self) +{ + IEC104_SlaveRequestState_t state; + unsigned steps; + if (!Owned(self)) { return false; } + for (steps = 0U; steps < 2U * IEC104_INTERROGATION_TASK_CAPACITY; ++steps) { + InterrogationTask *task; + IEC60870_Asdu_t asdu; + InterrogationResponse cancellation; + InterrogationResponse *response; + IEC104_TcpSubmitResult_t submitted; + uint8_t cause; + bool negative = false; + unsigned slot; + size_t cancel_index = 0U; + bool cancelling; + int expired; + Flush_Broadcast_Finishes(self); + Reap_Broadcast(self); + state = IEC104_Slave_Check_Request(self->slave, &self->session); + if (state == IEC104_SLAVE_REQUEST_ERROR) { + Cancel(self, IEC104_INTERROGATION_FINISH_SERVICE_ERROR); + self->status.active = false; + return false; + } + self->status.events_pending = state == IEC104_SLAVE_REQUEST_WAIT_EVENTS; + if (self->status.events_pending) { return true; } + self->status.waiting = false; + if (self->count == 0U) { return true; } + if (!self->status.active || state != IEC104_SLAVE_REQUEST_READY) { + Cancel(self, IEC104_INTERROGATION_FINISH_CONNECTION_CHANGED); + self->status.active = false; + return true; + } + expired = Expire(self); + if (expired != 0) { + if (expired < 0 || !self->status.active) { return expired > 0; } + continue; /* 确认前超时仅转为REPLY;重查通知后,本轮即可尝试提交否定确认。 */ + } + slot = Select_Task(self); + if (slot == IEC104_INTERROGATION_TASK_CAPACITY) { self->status.waiting = true; return true; } + task = &self->tasks[slot]; + /* 当前站的否定ACT_CON可能与另一站撤销并存,先提交原否定确认,再继续待发COT=9。 */ + cancelling = Is_Broadcast(self, task) && self->broadcast.cancelling && + !(task->phase == IEC104_INTERROGATION_REPLY && task->reply_cause != 0U); + response = &task->request; + if (!cancelling && (task->phase == IEC104_INTERROGATION_START || task->phase == IEC104_INTERROGATION_DATA)) { + Request_Application(self, task); + self->prefer_interrogation = false; + continue; /* 应用稍后取事件;继续处理当前已有的单帧应答。 */ + } + /* 普通应答沿用当前站CA;撤销可能针对其他站,须用临时副本改CA,不能改坏当前站请求。 + * WAIT后原任务和撤销标志仍保留,下次按同一目标重建应答。 */ + if (cancelling) { + cancel_index = Pending_Broadcast_Deactivation(self); + if (cancel_index == self->broadcast.count || + !Copy_Response_Request(self, &cancellation, &task->request.identity, task->request.bytes, + sizeof(task->request.bytes), + IEC104_Station_Get_Common_Address(self->config.broadcast_states[cancel_index]._station))) { + Cancel(self, IEC104_INTERROGATION_FINISH_SERVICE_ERROR); + return false; + } + response = &cancellation; + cause = IEC60870_ASDU_COT_DEACTIVATION_CON; + } else if (task->phase == IEC104_INTERROGATION_CONFIRM) { + cause = IEC60870_ASDU_COT_ACTIVATION_CON; + } else if (task->phase == IEC104_INTERROGATION_TERMINATE) { + cause = IEC60870_ASDU_COT_ACTIVATION_TERMINATION; + } else if (task->phase == IEC104_INTERROGATION_REPLY) { + cause = task->reply_cause; + negative = task->reply_negative; + } else { + Cancel(self, IEC104_INTERROGATION_FINISH_SERVICE_ERROR); + return false; + } + if (!Prepare_Response(self, response, cause, negative, &asdu)) { + (void)Reject_Connection(self, &task->request.identity, IEC104_INTERROGATION_FINISH_SERVICE_ERROR); + return false; + } + submitted = IEC104_Slave_Send_Response(self->slave, &task->request.identity, &asdu); + if (submitted != IEC104_TCP_SUBMIT_OK) { + if (Defer_For_Stop(self, &task->request.identity, submitted)) { return true; } + if (submitted == IEC104_TCP_SUBMIT_WAIT) { + /* 确认/终止/短应答保留原文重试;总召数据仅由Submit_Batch直接提交。 */ + ++self->status.waits; + self->status.waiting = true; + task->send_waiting = true; + self->prefer_interrogation = false; + } else { + Cancel(self, IEC104_INTERROGATION_FINISH_SEND_ERROR); + self->status.active = false; + } + return true; + } + self->prefer_interrogation = cancelling || task->phase == IEC104_INTERROGATION_REPLY; + ++self->status.submitted; + if (negative) { ++self->status.negative_submitted; } + if (!(cancelling ? Advance_Broadcast_Deactivation(self, cancel_index) : Advance(self, task))) { + (void)Reject_Connection(self, &task->request.identity, IEC104_INTERROGATION_FINISH_SERVICE_ERROR); + return false; + } + } + return true; +} + +/** + * 功能:将最早一条业务事件复制到调用方并从FIFO移除,不执行网络操作。 + * 参数:self为有效实例;event为非NULL输出对象,接收可独立保存的纯值副本。 + * 返回:READY写出事件;EMPTY表示暂时无事件;ERROR表示参数非法,后两者不改输出。 + * 说明:工作事件出队后才允许应答;BEGIN出队同时标记应用已持有本请求,后续须配对FINISHED。 + * 业务事件不含自引用指针,因此可以memmove压紧FIFO;协议任务仍通过order维护槽位顺序。 + * 每次先补入延迟FINISHED,因此仅反复NextEvent也能读完全部终结通知,无须靠Process补齐。 + * 读出最后一条时可能释放广播持有的站点引用;应用若仍需节点/context,须事先独立Acquire。 + */ +IEC104_InterrogationNextResult_t IEC104_Interrogation_NextEvent( + IEC104_Interrogation_t *self, IEC104_InterrogationEvent_t *event) +{ + InterrogationTask *task = NULL; + if (!Owned(self) || event == NULL) { return IEC104_INTERROGATION_NEXT_ERROR; } + Flush_Broadcast_Finishes(self); + Reap_Broadcast(self); + if (self->event_count == 0U) { return IEC104_INTERROGATION_NEXT_EMPTY; } + if (self->events[0].type != IEC104_INTERROGATION_FINISHED) { + unsigned slot = Find_Interrogation(self, self->events[0].request_id); + if (slot == IEC104_INTERROGATION_TASK_CAPACITY) { return IEC104_INTERROGATION_NEXT_ERROR; } + task = &self->tasks[slot]; + } + *event = self->events[0]; + --self->event_count; + memmove(self->events, self->events + 1, self->event_count * sizeof(self->events[0])); + if (task != NULL) { + task->event_delivered = true; + if (event->type == IEC104_INTERROGATION_BEGIN) { task->begin_delivered = true; } + if (event->type == IEC104_INTERROGATION_BEGIN && Is_Broadcast(self, task)) { + Broadcast_Current(self)->_flags |= STATION_BEGIN_READ; + } + } else if (self->broadcast.task != NULL && event->type == IEC104_INTERROGATION_FINISHED) { + size_t i; + for (i = 0U; i < self->broadcast.count; ++i) { + IEC104_InterrogationStationState_t *entry = &self->config.broadcast_states[i]; + if (entry->_request_id == event->request_id) { entry->_flags |= STATION_FINISH_READ; break; } + } + Reap_Broadcast(self); + } + return IEC104_INTERROGATION_NEXT_READY; +} + +/** + * 功能:接收应用对已取出BEGIN事件的接受或拒绝决定。 + * 参数:event为本实例当前BEGIN的原样副本;accept为true接受、false拒绝。 + * 返回:沿用Check_Reply的WAIT/STALE/ERROR;OK表示决定已消费,不可用同一事件重复应答。 + * 说明:接受后转为CONFIRM,等待Process提交肯定ACT_CON;拒绝先生成FINISHED(REJECTED), + * 再将任务转为否定ACT_CON应答。资源由应用读取FINISHED后释放,本函数不执行清理回调。 + */ +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Reply_Begin( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, bool accept) +{ + InterrogationTask *task; + IEC104_InterrogationReplyResult_t result = Check_Reply(self, event, IEC104_INTERROGATION_BEGIN, &task); + if (result != IEC104_INTERROGATION_REPLY_OK) { return result; } + Consume_Reply(self, task); + if (task->info.is_test && !self->rules->has_data(task->info.freeze)) { accept = false; } + if (accept) { + task->phase = IEC104_INTERROGATION_CONFIRM; + if (Is_Broadcast(self, task)) { Broadcast_Current(self)->_state = STATION_ACCEPTED; } + } + else { Reject_Request(self, task, IEC104_INTERROGATION_FINISH_REJECTED); } + return IEC104_INTERROGATION_REPLY_OK; +} + +/* 校验种类、事件、会话及期限后,先消费BEGIN再执行同步动作,重试同一事件不会再次执行。 + * action只接收引擎保存的请求元数据,不信任调用方可改写的event.request;禁止回调重入。 + * TEST直接拒绝;action失败须保证业务数据未变,成功后保持肯定确认,不在回调之后重新判超时。 */ +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Execute_Action( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, + const IEC104_InterrogationRules_t *rules, + bool (*action)(void *context, const IEC104_InterrogationRequest_t *request), void *context) +{ + InterrogationTask *task; + IEC104_InterrogationReplyResult_t result; + bool accepted; + if (!Owned(self) || self->rules != rules || action == NULL) { return IEC104_INTERROGATION_REPLY_ERROR; } + result = Check_Reply(self, event, IEC104_INTERROGATION_BEGIN, &task); + if (result != IEC104_INTERROGATION_REPLY_OK) { return result; } + if (self->rules->has_data(task->info.freeze)) { return IEC104_INTERROGATION_REPLY_ERROR; } + Consume_Reply(self, task); + accepted = !task->info.is_test && action(context, &task->info); + if (accepted) { + task->phase = IEC104_INTERROGATION_CONFIRM; + if (Is_Broadcast(self, task)) { Broadcast_Current(self)->_state = STATION_ACCEPTED; } + } else { Reject_Request(self, task, IEC104_INTERROGATION_FINISH_REJECTED); } + return IEC104_INTERROGATION_REPLY_OK; +} + +/** + * 功能:验证一次NEED_DATA对应的非空批次,当场尝试不排队提交。 + * 参数:event为当前已取出的NEED_DATA;asdu为应用构造的完整监视信息ASDU。 + * 返回:OK发送层已接手;WAIT通知未读空或发送背压;STALE交互失效/发送失败已取消;ERROR批次非法。 + * 说明:按当前目标站验证CA,按原请求验证COT、OA、TEST,按配置检查对象数及计数溢出。 + * 发送OK才消费事件并累计进度。发送WAIT不缓存、不推进游标,应用下次重新组包; + * 总召可重取当前值,计数量仍须使用本轮冻结快照。 + * 首次有效批次发生发送WAIT后结束本批取数计时;同一交互的重试由应用在网络推进后驱动。 + * 非法批次不结束取数计时。成功后原文由发送层持有,不借用应用缓冲;空数据调用Complete_Data。 + */ +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Submit_Batch( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, + const IEC60870_Asdu_t *asdu) +{ + InterrogationTask *task; + IEC60870_AsduInfo_t info; + const uint8_t *data; + uint16_t length; + IEC104_TcpSubmitResult_t submitted; + const IEC60870_AppLayerParameters_t *parameters; + IEC104_InterrogationReplyResult_t result = Check_Reply(self, event, IEC104_INTERROGATION_NEED_DATA, &task); + if (result != IEC104_INTERROGATION_REPLY_OK) { return result; } + parameters = IEC104_Slave_Get_Application_Layer_Parameters(self->slave); + if (asdu == NULL || asdu->parameters == NULL || + asdu->parameters->sizeOfCOT != 2 || asdu->parameters->sizeOfCA != 2 || + asdu->parameters->sizeOfIOA != 3 || + IEC60870_Asdu_Get_Data(asdu, &data, &length) != IEC60870_ASDU_OK || + length > IEC104_MAX_ASDU_LEN || + IEC60870_Asdu_Parse(parameters, data, length, &info) != IEC60870_ASDU_OK || + !self->rules->data_allowed(&info, task->info.freeze) || info.object_count == 0U || + info.object_count > self->config.max_objects_per_asdu || + info.cause_of_transmission != task->info.data_cause || + info.common_address != task->info.common_address || + info.originator_address != task->info.originator_address || + info.is_test != (uint8_t)task->info.is_test || info.is_negative_confirm != 0U || + task->batch_index == SIZE_MAX || task->objects_submitted > SIZE_MAX - info.object_count) { + return IEC104_INTERROGATION_REPLY_ERROR; + } + /* 直接提交须等待既有发送工作先行;WAIT不缓存本批旧值,应用重读实时值后再试。 + * 点值更新/突发入队与本次读值/提交须由应用串行协调,才能保持同一点的新旧发送顺序。 */ + submitted = IEC104_Slave_Try_Send_Response(self->slave, &task->request.identity, asdu); + if (submitted == IEC104_TCP_SUBMIT_WAIT) { + task->data_send_waiting = true; + task->send_waiting = true; + self->status.waiting = true; + ++self->status.waits; + return IEC104_INTERROGATION_REPLY_WAIT; + } + if (submitted != IEC104_TCP_SUBMIT_OK) { + if (Defer_For_Stop(self, &task->request.identity, submitted)) { + return IEC104_INTERROGATION_REPLY_WAIT; /* 仅等待STOP通知,不算发送背压,不停止取数计时。 */ + } + Cancel(self, IEC104_INTERROGATION_FINISH_SEND_ERROR); + self->status.active = false; + return IEC104_INTERROGATION_REPLY_STALE; + } + task->objects_submitted += info.object_count; + ++self->status.submitted; + Consume_Reply(self, task); + (void)Advance(self, task); /* DATA仅递增批次,不切换站点或释放资源。 */ + self->prefer_interrogation = false; + return IEC104_INTERROGATION_REPLY_OK; +} + +/** + * 功能:应答当前NEED_DATA,声明当前站已没有更多总召数据。 + * 参数:event须为当前已取出的NEED_DATA,不能使用BEGIN或已经交付过批次的旧事件。 + * 返回:OK已消费交互;单目标转TERMINATE,多目标继续下一站数据或进入终止遍历。 + * 说明:此处不发送ACT_TERM;后续Process提交成功才产生FINISHED(COMPLETED),发送等待时保留任务。 + * 应用可在OK后释放本站取数游标供下一站使用;迟到的FINISHED须按请求身份清理,避免误清新站。 + */ +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Complete_Data( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event) +{ + InterrogationTask *task; + IEC104_InterrogationReplyResult_t result = Check_Reply(self, event, IEC104_INTERROGATION_NEED_DATA, &task); + if (result != IEC104_INTERROGATION_REPLY_OK) { return result; } + Consume_Reply(self, task); + if (Is_Broadcast(self, task)) { + Save_Broadcast_Progress(self); + Broadcast_Current(self)->_state = STATION_DATA_DONE; + if (!Next_Broadcast_Station(self)) { + (void)Reject_Connection(self, &task->request.identity, IEC104_INTERROGATION_FINISH_SERVICE_ERROR); + return IEC104_INTERROGATION_REPLY_ERROR; + } + return IEC104_INTERROGATION_REPLY_OK; + } + task->phase = IEC104_INTERROGATION_TERMINATE; + return IEC104_INTERROGATION_REPLY_OK; +} + +/** + * 功能:应用在准备数据源或取得数据失败时,按肯定确认的提交边界终结本次总召。 + * 参数:event为当前已取出且尚未应答的BEGIN或NEED_DATA。 + * 返回:WAIT要求先处理Slave通知;STALE表示交互已失效;ERROR为检查或关闭失败。 + * 通过检查后,BEGIN失败转为否定REPLY并保留连接;NEED_DATA失败关闭原连接。 + * OK表示失败已处理,否定确认由Process提交;若关闭失败返回ERROR,任务仍已取消,不能重试。 + * 说明:两种路径均保留SOURCE_ERROR并交付FINISHED供应用清理,不发送成功ACT_TERM。 + */ +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Fail_Request( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event) +{ + IEC104_InterrogationReplyResult_t result; + InterrogationTask *task; + if (event == NULL) { return IEC104_INTERROGATION_REPLY_ERROR; } + if (event->type != IEC104_INTERROGATION_BEGIN && event->type != IEC104_INTERROGATION_NEED_DATA) { + return Owned(self) ? IEC104_INTERROGATION_REPLY_STALE : IEC104_INTERROGATION_REPLY_ERROR; + } + result = Check_Reply(self, event, event->type, &task); + if (result != IEC104_INTERROGATION_REPLY_OK) { return result; } + return Fail_Interrogation(self, task, IEC104_INTERROGATION_FINISH_SOURCE_ERROR) ? + IEC104_INTERROGATION_REPLY_OK : IEC104_INTERROGATION_REPLY_ERROR; +} + +/** + * 功能:组合会话状态、累计统计、活动总召进度及最近业务期限,生成独立状态副本。 + * 参数:self为有效实例;status为非NULL输出对象。 + * 返回:true已写出快照;false为非法句柄/NULL,输出保持不变。 + * 说明:phase和进度优先描述当前站;收尾为FINISHING,仅有应答时REPLY,无任务时IDLE。 + * 尚未生成BEGIN不计业务期限;timer_active为false时剩余值无意义。 + * 实时查询Slave通知和本地可推进任务;need_process不等于!waiting,不保证发送接手。 + * 只读取当前单调时钟及服务状态,不触发超时、发送或事件入队。 + */ +bool IEC104_Interrogation_Get_Status(const IEC104_Interrogation_t *self, + IEC104_InterrogationStatus_t *status) +{ + const InterrogationTask *task; + IEC104_SlaveRequestState_t state; + unsigned slot; + InterrogationTimeout timeout; + if (!Owned(self) || status == NULL) { return false; } + slot = Find_Interrogation(self, 0U); + task = slot != IEC104_INTERROGATION_TASK_CAPACITY ? &self->tasks[slot] : + (self->count != 0U ? &self->tasks[self->order[0]] : NULL); + *status = self->status; + status->gi_pending = slot != IEC104_INTERROGATION_TASK_CAPACITY; + status->generation = self->session.generation; + status->pending_requests = self->count; + status->pending_events = self->event_count + Pending_Broadcast_Finishes(self); + status->phase = task != NULL ? task->phase : IEC104_INTERROGATION_IDLE; + status->batch_index = task != NULL ? task->batch_index : 0U; + status->objects_submitted = task != NULL ? task->objects_submitted : 0U; + status->data_send_waiting = task != NULL && task->data_send_waiting; + status->application_waiting = task != NULL && task->event_outstanding && !task->data_send_waiting; + if (task != NULL && task->phase != IEC104_INTERROGATION_REPLY && + !Interrogation_Can_Progress(self, task)) { status->waiting = true; } + status->timer_active = false; + status->timeout_remaining_ms = 0U; + if (task != NULL) { + timeout = Task_Timeout(self, task, IEC104_Net_Now_Ms()); + if (timeout.reason != IEC104_INTERROGATION_FINISH_NONE) { + status->timer_active = true; + status->timeout_remaining_ms = timeout.remaining_ms; + } + } + /* 只查询Slave通知,不消费或发送;不能跨过未读STOP/撤销推进旧业务。 + * 选择规则与Process一致,避免把等应用的总召误当成阻塞所有短应答的队首。 + * WAIT只抑制本地空转;网络推进后调用方仍须显式Process重试,不以本标志跳过。 */ + state = IEC104_Slave_Check_Request(self->slave, &self->session); + status->events_pending = state == IEC104_SLAVE_REQUEST_WAIT_EVENTS; + slot = Select_Task(self); + status->need_process = self->count != 0U && !status->events_pending && + (state != IEC104_SLAVE_REQUEST_READY || !status->active || + (status->timer_active && status->timeout_remaining_ms == 0U) || + (slot != IEC104_INTERROGATION_TASK_CAPACITY && !self->tasks[slot].send_waiting)); + /* 仅剩终结事件交付时由NextEvent推进;pending_events负责唤醒,重复Process不能代替应用读取。 */ + if (self->broadcast.task != NULL && self->broadcast.stage == BROADCAST_FINISH && + !self->broadcast.cancelling && self->count == 1U) { status->need_process = false; } + return true; +} diff --git a/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_Interrogation.h b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_Interrogation.h new file mode 100644 index 00000000..18cc5844 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_Interrogation.h @@ -0,0 +1,326 @@ +/* + * 104召唤共用引擎:具体种类规则由GeneralInterrogation/CounterInterrogation创建入口提供。 + * 总召旧入口Create保持兼容;计数量召唤入口见IEC104_CounterInterrogation.h。 + * 借用一个Slave,不创建socket/线程,不保存业务回调;动作入口仅同步借用回调执行一次。 + * 所有接口在同一事件循环串行调用,动作回调不得重入引擎。 + * 先把Slave通知按FIFO交给Handle_Event,读空后Process,再NextEvent并显式应答。 + * 事件来源和会话状态由Slave公共接口校验;本模块维护召唤阶段、业务期限和应用事件。 + * 退出顺序:Cancel -> 读完业务FINISHED并清理应用资源 -> Destroy -> Destroy Slave。 + */ +#ifndef IEC104_INTERROGATION_H +#define IEC104_INTERROGATION_H +#include "IEC104_Slave.h" +#ifdef __cplusplus +extern "C" { +#endif + +/* 不透明静态池句柄,只能由Create取得、Destroy释放,不能自行分配、复制对象或free。 + * 销毁成功后停止使用旧句柄;同一实例的全部接口须在所属事件循环中串行调用。 */ +typedef struct IEC104_Interrogation IEC104_Interrogation_t; + +/* 请求信息随事件按值复制,不暴露可发送的connection。 + * 应用据此选择数据源、构造数据头;IOA、分组成员及遍历状态均由应用维护。 + * 引擎不登记或冻结IOA列表;总召由应用逐批取当前值,计数量读取由应用固定本轮冻结快照。 + * 冻结/复位命令在BEGIN执行应用动作,不进入NEED_DATA。 */ +typedef struct { + uint64_t generation, receive_serial; /* 原TCP会话代次、接收I帧标识,供应用关联和诊断。 */ + uint64_t station_id; /* 本次选中的注册编号,兼容裸CA模式为0;按编号查找避免同CA替换串站。 */ + uint16_t common_address; /* 实际本站地址;广播65535解析为此地址,应用数据批次须使用同一CA。 */ + uint8_t originator_address; /* 请求的源发地址OA,应答数据须原样保留。 */ + uint8_t qualifier; /* 总召QOI=20..36;计数量RQT=1..5,应用维护分组点表。 */ + bool is_test; /* 请求的TEST标志,数据批次须保持一致。 */ + uint8_t max_objects_per_asdu; /* 1..127,实际另受249字节容量限制。 */ + uint8_t freeze; /* 计数量FRZ:0读取,1冻结,2冻结并复位,3仅复位;总召恒为0。 */ + uint8_t data_cause; /* 数据COT:总召等于QOI,计数量总请求37、分组38..41。 */ +} IEC104_InterrogationRequest_t; + +/* FINISHED通知应用清理本次游标及业务资源;终结原因不等于网络发送回执。 + * COMPLETED表示结束帧被发送层接手,不表示写完或收到ACK;读取为ACT_TERM,动作命令为ACT_CON。 */ +typedef enum { + IEC104_INTERROGATION_FINISH_NONE = 0, /* 未终结;BEGIN/NEED_DATA事件也使用此值。 */ + IEC104_INTERROGATION_FINISH_COMPLETED, /* 读取的全部批次和ACT_TERM,或动作命令的ACT_CON已提交。 */ + IEC104_INTERROGATION_FINISH_REJECTED, /* 应用拒绝;否定ACT_CON可能仍在排队。 */ + IEC104_INTERROGATION_FINISH_CANCELLED, /* 应用调用本地Cancel放弃任务。 */ + IEC104_INTERROGATION_FINISH_STOPPED, /* 收到Slave的停用通知,结束本次数据传输。 */ + IEC104_INTERROGATION_FINISH_DISCONNECTED, /* 原TCP会话断开。 */ + IEC104_INTERROGATION_FINISH_CONNECTION_CHANGED, /* 连接代次或当前传输状态已不再适用于任务。 */ + IEC104_INTERROGATION_FINISH_SOURCE_ERROR, /* 应用通过Fail_Request报告数据源准备/取数失败。 */ + IEC104_INTERROGATION_FINISH_SEND_ERROR, /* 提交应答失败且不是可等待的发送容量不足。 */ + IEC104_INTERROGATION_FINISH_OVERLOAD, /* 兼容保留;满槽现在拒绝/丢弃新请求,不终结已有任务。 */ + IEC104_INTERROGATION_FINISH_INVALID_REQUEST, /* 保留枚举值兼容;畸形新请求不再终结已有总召。 */ + IEC104_INTERROGATION_FINISH_SERVICE_ERROR, /* 服务查询、内部状态或构造应答失败。 */ + IEC104_INTERROGATION_FINISH_DEACTIVATED, /* 主站COT=8撤销;COT=9可能仍待发送。 */ + IEC104_INTERROGATION_FINISH_BEGIN_TIMEOUT, /* BEGIN入队后未在期限内得到应用决定。 */ + IEC104_INTERROGATION_FINISH_DATA_TIMEOUT, /* NEED_DATA入队后未在期限内得到应用应答。 */ + IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT /* BEGIN生成至ACT_TERM提交超过执行整体期限。 */ +} IEC104_InterrogationFinishReason_t; + +typedef enum { + IEC104_INTERROGATION_BEGIN = 1, /* 检查数据源并接受/拒绝;取数游标可延至首个NEED_DATA建立。 */ + IEC104_INTERROGATION_NEED_DATA, /* 应用选择本批IOA并读取当前值/品质;暂未就绪时保存事件。 */ + IEC104_INTERROGATION_FINISHED /* 清理游标及业务资源,无需应答。 */ +} IEC104_InterrogationEventType_t; + +/* 纯值事件,无借用指针和自引用,可赋值、排入应用队列、保存到后续循环。 + * 字段只读,应答时原样传回;实例/请求/批次标识用于拒绝迟到和重复应答。 + * 每个已取出的BEGIN恰好配对一次FINISHED,取消也不丢失终结通知。 + * BEGIN尚未取出便取消时移除BEGIN,不产生FINISHED(应用尚未建立资源)。 + * 取消移除队列中的NEED_DATA;已取出的旧事件仍可读取,应答返回STALE。 */ +typedef struct { + IEC104_InterrogationEventType_t type; /* 决定调用Reply_Begin、提交数据或只清理资源。 */ + uint64_t instance_id, request_id; /* 实例及请求身份;必须与事件其他字段一起原样传回。 */ + IEC104_InterrogationRequest_t request; /* 原激活请求的元数据,FINISHED仍保留此副本。 */ + size_t batch_index, objects_submitted; /* 已被发送层接手的批次/对象数;首个NEED_DATA的批次号为0。 */ + IEC104_InterrogationFinishReason_t reason; /* 仅FINISHED有意义。 */ +} IEC104_InterrogationEvent_t; + +/* 多站广播的调用方存储:每个目标站一项,不保存点值、ASDU或独立发送任务。 + * config借用整个数组至Destroy,期间地址稳定、不得读写或与其他实例重叠。 + * 模块初始化/清理内部字段,不要求调用方逐项初始化。 */ +typedef struct { + IEC104_Station_t *_station; /* 本轮持有的原节点引用,直到整轮终结事件读完才释放。 */ + uint64_t _request_id, _started_ms; /* 每站独立的请求身份和BEGIN时刻,切换当前站不丢失。 */ + size_t _batch_index, _objects_submitted; /* 已提交进度,不是应用的IOA遍历游标。 */ + uint8_t _state, _flags, _reason; /* 协议阶段、事件交付/撤销标志、终结原因,仅库内部使用。 */ +} IEC104_InterrogationStationState_t; + +/* Create复制此配置;创建后修改调用方对象不会改变已有实例,指向的广播数组仍为借用。 + * 接受/取数期限从事件入队开始,包含应用尚未NextEvent读取它的时间。 */ +typedef struct { + uint16_t common_address; /* station为NULL时的兼容站地址1..65534;绑定站点时从节点取得CA并忽略此值。 */ + uint8_t max_objects_per_asdu; /* 每批1..127个对象,配置无业务函数指针。 */ + bool use_station_registry; /* true按请求CA查共用站点表,广播分三阶段;station须NULL,common_address忽略。 */ + uint32_t begin_timeout_ms; /* BEGIN入队至Reply_Begin/Fail_Request,默认15000ms。 */ + uint32_t data_timeout_ms; /* NEED_DATA至首次有效批次/Complete/Fail,默认15000ms;发送WAIT后的重试不计。 */ + uint32_t total_timeout_ms; /* 读取:每站BEGIN至ACT_TERM提交;动作:BEGIN至应用接受。默认0禁用。 */ + /* 上述期限是业务策略,独立于104链路t0..t3;每项0表示禁用该项。 + * BEGIN尚未生成时不计业务期限;START/CONFIRM超时转为否定ACT_CON并保留连接。 + * 动作命令接受后的CONFIRM不再受业务期限约束,避免执行成功后被改发否定;连接期限仍生效。 + * 肯定ACT_CON被发送层接手后,DATA/TERMINATE超时关闭原连接,不发送成功ACT_TERM。 + * 同一执行任务的总期限与阶段期限同时到点时优先报告总超时。 */ + IEC104_Station_t *station; /* 兼容固定站绑定:Create持有至Destroy;删除后不接新请求,已接受任务可完成。 */ + IEC104_InterrogationStationState_t *broadcast_states; /* 调用方独占提供的状态数组,寿命覆盖实例。 */ + size_t broadcast_capacity; /* 数组项数,即单轮多站广播能接纳的目标数,不是注册站数上限。 */ + /* 默认NULL/0。广播涉及多个站时,容量须覆盖请求时的全部注册站,否则整轮否定确认。 + * 单站/定向总召不需要数组。容量由应用决定,不限制注册站数;数组含已确认目标及终结记录。 */ +} IEC104_InterrogationConfig_t; + +/* 输入Slave通知的处理结果;其他类型I帧返回IGNORED,交后续业务处理。 */ +typedef enum { + IEC104_INTERROGATION_EVENT_ERROR = -1, /* 参数、复制、关闭连接等处理失败。 */ + IEC104_INTERROGATION_EVENT_IGNORED = 0, /* 本模块未接手,可转交其他业务处理器。 */ + IEC104_INTERROGATION_EVENT_HANDLED = 1 /* 已处理,含畸形/满槽丢弃;不保证应用接受或已发送响应。 */ +} IEC104_InterrogationEventResult_t; + +typedef enum { + IEC104_INTERROGATION_NEXT_ERROR = -1, /* 非法句柄或NULL输出。 */ + IEC104_INTERROGATION_NEXT_EMPTY = 0, /* 当前无业务事件,不代表总召已经结束。 */ + IEC104_INTERROGATION_NEXT_READY = 1 /* 已复制并移除最早事件,输出可独立保存。 */ +} IEC104_InterrogationNextResult_t; + +/* 显式比较,不能按真假判断。 + * OK:交互已消费;Submit_Batch已交发送层并固定顺序,其余应答由Process推进;不表示写完/ACK。 + * WAIT:先处理Slave通知并推进网络;Submit_Batch还可能因发送背压WAIT,不缓存本次原文。 + * 保留同一事件,批次游标不动,下次重读当前值组包。首次有效批次的发送WAIT结束本批取数计时。 + * STALE:实例/请求/批次/类型不匹配、未取出、已应答、已取消或已超时;不要重试旧事件。 + * ERROR:参数/批次/服务查询错误,不消费交互,可修正再提交。 + * NEED_DATA阶段Fail_Request例外:即使关闭连接失败,仍取消并交付FINISHED,不能重试。 */ +typedef enum { + IEC104_INTERROGATION_REPLY_STALE = -2, + IEC104_INTERROGATION_REPLY_ERROR = -1, + IEC104_INTERROGATION_REPLY_WAIT = 0, + IEC104_INTERROGATION_REPLY_OK = 1 +} IEC104_InterrogationReplyResult_t; + +/* 优先描述唯一未终结总召的诊断阶段;仅有应答时REPLY,无任务时IDLE。 + * 阶段由模块推进,应用通过事件交互,不直接修改。 */ +typedef enum { + IEC104_INTERROGATION_IDLE, /* 没有协议任务。 */ + IEC104_INTERROGATION_REPLY, /* 等待提交单帧确认(含撤销确认)或错误应答。 */ + IEC104_INTERROGATION_START, /* 等事件空位生成BEGIN,或等待应用接受/拒绝;两者均占用总召资格。 */ + IEC104_INTERROGATION_CONFIRM, /* 应用已接受,等待提交肯定ACT_CON。 */ + IEC104_INTERROGATION_DATA, /* 生成NEED_DATA,等待应用取值并尝试直接提交。 */ + IEC104_INTERROGATION_TERMINATE, /* 应用声明数据结束,等待提交ACT_TERM。 */ + IEC104_INTERROGATION_FINISHING /* 多站广播网络任务已结束,等NextEvent读完各站FINISHED后归还状态区。 */ +} IEC104_InterrogationPhase_t; + +/* Get_Status返回的独立副本;计数自Create起累计,Cancel和重连不清零。 + * active/waiting反映最近已处理的通知/推进结果;events_pending及need_process在查询时计算。 */ +typedef struct { + uint64_t generation; /* 已消费的连接代次,断线后0。 */ + bool active; /* 已消费ACTIVATED且未停用,不是socket实时状态。 */ + bool gi_pending; /* 总召网络流程尚未终结;FINISHING虽为false,读完终结事件前仍拒绝新激活。 */ + bool waiting; /* 存在发送、应用或事件空位等待;总召等应用时仍可推进单帧应答。 */ + bool application_waiting; /* 等待应用首次有效应答,包含事件尚未取出;发送背压另见data_send_waiting。 */ + bool data_send_waiting; /* 有效批次提交曾遇发送WAIT;应用仍持有NEED_DATA,网络推进后重取再试。 */ + bool events_pending; /* 当前Slave FIFO未空,须先处理网络通知。 */ + bool need_process; /* 有本地工作/到期/会话失效待处理,应立即Process;未读Slave通知优先。 */ + bool timer_active; /* 存在启用的业务期限;为false时timeout_remaining_ms无意义。 */ + uint32_t timeout_remaining_ms; /* 最近期限距当前单调时钟的毫秒数;0表示应立即Process。 */ + unsigned pending_requests, pending_events; /* 任务数含FINISHING;事件数含状态区中待交付的FINISHED,不含预留。 */ + IEC104_InterrogationPhase_t phase; /* 当前站阶段;收尾为FINISHING,仅有应答为REPLY,无任务为IDLE。 */ + size_t batch_index, objects_submitted; /* 活动总召已提交的批次/对象数;仅有应答时取最早应答保留的进度。 */ + unsigned long submitted, gi_submitted; /* 接手ASDU数、完成协议提交的召唤数;动作命令以ACT_CON计。 */ + unsigned long waits, cancelled, rejected, overloads; + /* waits统计发送层返回WAIT;cancelled统计取消的协议任务(含单帧应答); + * rejected统计校验拒绝(含无法应答的畸形请求)/总召忙/应用/确认前失败/容量拒绝; + * overloads含任务槽满或广播状态区不足。 */ + unsigned long negative_submitted, dropped_requests; /* 已提交否定应答数、无法保存或提交而丢弃的新请求数。 */ + unsigned long deactivated, timeouts; /* 成功匹配的主站撤销、执行阶段业务超时次数。 */ + IEC104_InterrogationFinishReason_t last_finish_reason; /* 最近合法总召的终结原因,含BEGIN尚未交付的任务。 */ +} IEC104_InterrogationStatus_t; + +/* 初始化调用方的可写配置:station=NULL、兼容CA=1,broadcast_states=NULL、broadcast_capacity=0,每批最多127个对象, + * 接受/取数期限各15000ms,执行整体期限为0(禁用);NULL无操作。 */ +void IEC104_Interrogation_Config_Default(IEC104_InterrogationConfig_t *config); + +/* 复制配置,借用Slave;同一Slave的同一种类只能绑定一个实例,在消费Slave通知前创建。 + * slave须保持有效直到本实例Destroy;config在返回后可释放或复用。 + * station非NULL时校验其属于本Slave并占用节点,本站CA只取节点值;创建失败不泄漏引用。 + * use_station_registry开启时按CA选站;广播共用一个任务,依次完成全部确认、各站数据、全部终止。 + * 各站独立BEGIN/FINISHED和request_id;多目标广播须有足够broadcast_states,容量不足整轮否定。 + * 接收时持有目标节点引用;尚未选入BEGIN便删除的站跳过,已经选中的站持原引用完成。 + * 新增/替代节点不加入本轮;已确认站即使尚未取数或被删除,也必须完成或明确终结。 + * BEGIN后需跨循环保留context的应用须按station_id查节点并Acquire,FINISHED清理后Release。 + * 兼容模式use_station_registry=false保留单CA或固定station行为。 + * NULL:参数非法、重复绑定、状态区重叠、池满或实例编号耗尽;无网络操作,无malloc。 + * 编译.c可覆盖IEC104_INTERROGATION_CAPACITY(默认为TCP实例容量的两倍)、 + * IEC104_INTERROGATION_TASK_CAPACITY(默认4)、IEC104_INTERROGATION_EVENT_CAPACITY + * (默认4,最小2)。任务池容纳至多一个总召及短应答,不是多个总召的等待队列。 + * 单目标流程在FIFO预留FINISHED;多目标用每站状态区保留终结记录,NextEvent按空位交付。 */ +IEC104_Interrogation_t *IEC104_Interrogation_Create(IEC104_Slave_t *slave, + const IEC104_InterrogationConfig_t *config); + +/* 协议任务和业务事件均为空才释放槽,否则false并保持原状态。 + * 先Cancel,再NextEvent读完FINISHED并清理应用资源,最后Destroy;成功后句柄失效。 */ +bool IEC104_Interrogation_Destroy(IEC104_Interrogation_t *self); + +/* 放弃未提交任务;保留已交付BEGIN的FINISHED(CANCELLED),移除未交付工作事件。 + * 不撤回已接手帧、不补发ACT_TERM,也不回滚已执行的冻结/复位;非法句柄无操作。 */ +void IEC104_Interrogation_Cancel(IEC104_Interrogation_t *self); + +/* 按FIFO接收本Slave的每个通知,不消费Slave FIFO,不保存调用方指针。 + * 下列QOI和撤销约定针对总召;计数量的RQT/FRZ规则见CounterInterrogation.h。 + * 同时使用两个实例时,连接/启停通知交给两边;I帧仅在当前实例IGNORED后转交下一个。 + * 总召须SQ=0、一个对象、COT=6或8、P/N=0、CA为本站或65535、IOA=0、QOI=20..36。 + * 广播常量为IEC104_CA_BROADCAST;应用事件、确认、数据、否定应答及终止均使用实际本站CA。 + * 原输入事件不修改;每次选站设置应答CA,同一站发送及重试沿用该地址。 + * 空闲时保存一个COT=6激活并进入START;从保存起至终结,拒绝所有后续激活,不区分QOI。 + * 忙或QOI/P/N非法时回否定ACT_CON;保留已有请求的QOI/OA/TEST、工作事件及数据进度。 + * 单帧应答在无未读Slave通知、原会话有效且无更早REPLY时尝试立即提交,未接手再保存。 + * 多站广播应答先保存一个遍历任务,再由Process逐站提交;无槽时整体丢新请求并计数。 + * 满槽时尝试否定确认;无法安全提交/保存则仅丢新请求并累计dropped_requests,不断链。 + * 撤销COT=8不受忙拒绝限制,按连接代次、实际站CA、OA、TEST、QOI匹配当前总召; + * 定向撤销停止匹配站,包括已确认但尚未取数/等待终止的站;其他站继续。 + * 尚未选入BEGIN的站没有可撤销的活动事务。广播撤销停止整轮,按各活动站CA确认。 + * 已完成/已拒绝站不补发撤销确认;无注册站且无匹配事务时,广播请求回复未知CA。 + * 为已交付BEGIN保留FINISHED(DEACTIVATED),按单帧应答规则提交肯定COT=9; + * 不匹配/非法QOI或P/N回否定COT=9。 + * 撤销无需应用再次批准;应用收到FINISHED后停止采样并释放本次资源。 + * 匹配撤销复用原任务槽,并将确认排到单帧应答队尾;满池也不需要额外槽。 + * 一组撤销确认尚未提交完时,重叠的新撤销按未匹配拒绝,不覆盖已有确认。 + * COT/CA/IOA错误回45/46/47;撤销不撤回发送层已接手的帧,不补发ACT_TERM。 + * 尚未生成/交付BEGIN的撤销不产生FINISHED;已有BEGIN交付才通知应用清理。 + * 先安全读取Type,非100返回IGNORED;公共视图不可用也返回IGNORED,由公共入口处理。 + * Type100仅一个对象且IOA/QOI完整时,SQ=1或多余尾字节回否定7/9(COT/CA/IOA错误优先45/46/47)。 + * 此应答规范为10字节、VSQ=1,保留OA/TEST/IOA/QOI;绝不据规范化副本执行激活或撤销。 + * 零对象/多对象等无法可靠对应请求的布局错误计入rejected并返回HANDLED,不生成应答。 + * 上述请求错误不取消已有总召、不刷新其期限;基础帧错误仍由Receiver/连接层处理。 + * STOPDT/断线取消总召及待发短应答并保留终结事件;不替其他业务回答UNKNOWN_TYPE。 */ +IEC104_InterrogationEventResult_t IEC104_Interrogation_Handle_Event( + IEC104_Interrogation_t *self, const IEC104_SlaveEvent_t *event); + +/* 自动推进当前就绪任务,不等待应用或网络;调用方不需要指定处理步数。 + * Slave FIFO未空仅设置events_pending。顺序:START -> BEGIN -> 应用接受 -> ACT_CON -> + * NEED_DATA/提交批次(可重复)-> 应用Complete_Data -> ACT_TERM -> FINISHED。 + * 动作命令例外:ACT_CON被发送层接手后直接FINISHED,不生成NEED_DATA或ACT_TERM。 + * 多目标广播先完成所有站的BEGIN/ACT_CON,再逐站NEED_DATA,全部数据结束后才逐站ACT_TERM。 + * 工作事件只交付一次;应用暂未就绪可保存事件,后续Process不会重复请求。 + * 单帧应答保持FIFO,可在总召等待应用/业务事件空位时提交;双方可推进时先应答再总召轮转, + * 继续处理当前就绪任务,直至空闲、所有任务等待应用/事件空位,或发送返回WAIT。 + * 每个站的REPLY成功后继续广播或释放任务;不保存后续主站新发来的总召激活。 + * 每轮最多推进2倍任务容量步,广播余下站点由need_process提示继续,避免大量站点独占事件循环。 + * 应答可穿插总召各批次,本次总召自身ACT_CON/数据/ACT_TERM顺序不变。 + * 确认、终止和短应答沿原connection走HIGH,WAIT保留请求;DATA只生成NEED_DATA, + * 由应用Submit_Batch直接提交,OK才增加批次/对象计数,WAIT须重新取值后重试。 + * WAIT让出本轮,下轮优先检查单帧应答;已被发送层接手的帧不被后到应答越过。 + * 发送ERROR时复核原连接;正常STOP拒绝且通知未读时仅置events_pending并让出,由通知终结为STOPPED。 + * 不因此增加waits、重置期限或撤销已接手帧;其他ERROR及FATAL仍按SEND_ERROR处理。 + * 使用平台单调时钟检查期限,先处理Slave通知;START/CONFIRM超时转为否定REPLY并继续调度, + * 肯定ACT_CON提交后的DATA/TERMINATE超时关闭原连接;FINISHED保留具体超时原因。 + * 执行总期限默认禁用,启用后按每站BEGIN独立开始,含等待其他站完成的时间。 + * 已确认站总期限到点即关闭原连接;此前等待事件空位不计期限,应及时读FINISHED。 + * 网络推进并读空Slave通知后须Process重试,即使need_process为false;发送WAIT不自行触发空转。 + * 数据WAIT还须重试应用持有的NEED_DATA;本函数不会代为重读或重发数据。 + * 本地应答/事件消费后重查Get_Status,need_process为true不得休眠;并用剩余期限限制等待时长。 + * true本轮正常(可等待/取消/超时);false参数、组包、关闭连接或服务查询失败。 */ +bool IEC104_Interrogation_Process(IEC104_Interrogation_t *self); + +/* 取最早业务事件;event须为非NULL输出,只有READY写入,EMPTY/ERROR保持输出不变。 + * READY后工作事件才允许应答;FINISHED仅用于应用清理,不调用应答接口。 + * 多站终结记录超过FIFO容量时,本函数按需补入,连续NextEvent即可读完,无需Process。 + * 最后一条FINISHED出队后归还广播状态区和节点引用;应用持有的节点引用仍须自行Release。 + * 事件是可保存的纯值副本,本函数无网络操作,也不自动执行应用代码。 */ +IEC104_InterrogationNextResult_t IEC104_Interrogation_NextEvent( + IEC104_Interrogation_t *self, IEC104_InterrogationEvent_t *event); + +/* 应答已取出的BEGIN。true允许提交ACT_CON;false立即FINISHED(REJECTED), + * 并排队否定ACT_CON;不支持的分组可在这里拒绝。应用按instance_id/request_id管理遍历状态。 + * 接受前检查数据源及业务资源;无需登记完整IOA列表或预先采样全部数据。 + * event须原样传回;OK表示接受/拒绝决定已消费,实际ACT_CON由后续Process提交。 + * 所有应答接口也检查期限,不能通过迟到应答绕过超时;Slave通知未读空时仍优先返回WAIT。 + * 计数量FRZ!=0建议使用CounterInterrogation_Execute_Begin,在校验通过后才执行副作用。 + * 不要在本函数之前无条件执行冻结/复位:WAIT会重试,STALE也不能撤销已经发生的动作。 + * 动作命令接受后停止业务期限,保留确认等待;TEST动作始终拒绝。 */ +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Reply_Begin( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, bool accept); + +/* 应答已取出的NEED_DATA,验证非空ASDU后当场尝试不排队提交。 + * OK表示已复制进发送缓冲并固定发送顺序,当场累计批次/对象数;输入缓冲及参数可立即复用。 + * WAIT不接手、不推进游标;应用保存同一事件,网络推进后重新组包,不保留旧编码等待重发。 + * 总召可重取当前值;计数量必须保持本轮冻结版本,值、序号、品质及可选时标一起固定。 + * 首次有效批次发生发送WAIT后停止本批取数计时;下一NEED_DATA重新计时,显式整体期限仍有效。 + * 参数须2/2/3,COT=request.data_cause、P/N=0、CA=本事件目标站地址、OA/TEST与事件相同。 + * 广播请求的数据CA也不能使用65535;对象数不超配置。 + * 总召接受Type 1/3/5/7/9/11/13/20/21,SQ=0/1均可;计数量读取接受Type 15(SQ=0/1)、37(SQ=0)。 + * IOA及分组归属由调用方决定;库校验编码布局,不规定点表或强制固定IOA集合。 + * 总召建议每次NEED_DATA读取当前值/品质,整轮不保证同一采样时刻;计数量由应用保证快照一致。 + * 点值更新及突发发布、总召取值及本调用须由同一发布流程串行协调,不能迟交旧事件。 + * HIGH/NORMAL有待发数据时总召退让;持续事件拥塞可能延后总召。OK后不再重新取值或重复提交。 + * 发送ERROR若确认为原连接正常STOP拒绝且有未读通知,返回WAIT并保留事件,先处理DEACTIVATED。 + * 此等待不设置data_send_waiting、不停止取数计时;通知处理后旧事件STALE,FINISHED为STOPPED。 + * 其他发送ERROR/FATAL取消请求并返回STALE;FATAL可能已发出字节,不允许重试旧批次。 + * 非法批次ERROR,保留NEED_DATA供修正,不断线;无法继续则Fail_Request。 + * 应用使用Asdu_Create/InformationObject_Create/Add自行构造业务ASDU。 */ +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Submit_Batch( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, + const IEC60870_Asdu_t *asdu); + +/* 应答已取出的NEED_DATA,声明数据耗尽;不以空ASDU作为结束标记。 + * OK后本请求不再取数,可归还其取数游标;多目标广播继续下一站数据,全部结束后才发终止。 + * Process提交本站ACT_TERM成功后才交付FINISHED(COMPLETED)。 */ +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Complete_Data( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event); + +/* 应答已取出的BEGIN/NEED_DATA,报告数据源失败,两者均交付FINISHED(SOURCE_ERROR)。 + * BEGIN阶段复用原槽保存否定ACT_CON,保留连接和其他任务;OK不表示否定确认已发送,须继续Process。 + * NEED_DATA阶段肯定ACT_CON已经提交:关闭原连接并取消任务,不发送成功终止,不影响后来新代次。 + * 检查通过后即终结交互;即使NEED_DATA阶段关闭失败返回ERROR,任务仍已取消,不能重试。 */ +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Fail_Request( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event); + +/* 取得独立状态副本,实时查询Slave通知、可推进任务和最近期限;不消费事件或触发I/O/超时。 + * need_process表示无需等新网络事件就应尝试推进,不保证发送接手,也不是waiting的反值。 + * 生成工作事件有空位、应答接手后可提交下一帧、业务期限到期时置位;Slave通知未读空时不置位。 + * 当前帧提交WAIT后停止本地续跑,网络推进后仍须调用Process重试;不能仅凭本标志决定是否处理网络进展。 + * data_send_waiting时应用仍须在网络推进后重取并重试保留的NEED_DATA,不能等新事件或need_process置位。 + * pending_events非零先消费业务事件;释放空位或完成应答后须重查,不能沿用旧状态休眠。 + * timer_active时,调用方等待时长不得超过timeout_remaining_ms;false时不限制等待。 + * 返回false为非法句柄/NULL,输出保持不变。 */ +bool IEC104_Interrogation_Get_Status(const IEC104_Interrogation_t *self, + IEC104_InterrogationStatus_t *status); + +#ifdef __cplusplus +} +#endif +#endif /* IEC104_INTERROGATION_H */ diff --git a/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_Interrogation_Internal.h b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_Interrogation_Internal.h new file mode 100644 index 00000000..2d281eee --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/104/business/IEC104_Interrogation_Internal.h @@ -0,0 +1,39 @@ +/* + * 104共用召唤引擎与种类文件之间的私有接口,不供业务直接调用。 + * 规则函数必须无状态:不访问网络、不保存业务进度、不回调引擎。 + * 规则表在实例整个寿命内保持有效且只读;现有两种请求共用6字节公共头、3字节IOA和1字节限定词。 + */ +#ifndef IEC104_INTERROGATION_INTERNAL_H +#define IEC104_INTERROGATION_INTERNAL_H +#include "IEC104_Interrogation.h" + +typedef struct { + uint8_t type_id; /* 请求类型,用于入口分派和应答副本校验。 */ + bool has_deactivation; /* 是否受理COT=8撤销;不支持时按未知传送原因应答。 */ + /* 解析首对象,输出IOA、种类限定词及FRZ;总召FRZ恒为0,失败不改输出。 */ + bool (*decode)(const IEC60870_AsduInfo_t *info, uint32_t *ioa, uint8_t *qualifier, uint8_t *freeze); + /* 校验协议允许的限定词范围;应用是否配置该组留到BEGIN判断。 */ + bool (*qualifier_valid)(uint8_t qualifier); + /* 返回准入请求应使用的数据COT;只读元数据,不改变原请求。 */ + uint8_t (*data_cause)(uint8_t qualifier); + /* 校验数据类型、SQ及动作是否允许携带数据;公共头和容量由引擎另行检查。 */ + bool (*data_allowed)(const IEC60870_AsduInfo_t *info, uint8_t freeze); + bool (*has_data)(uint8_t freeze); /* false表示肯定ACT_CON提交后直接完成,不产生数据事件或ACT_TERM。 */ +} IEC104_InterrogationRules_t; + +/* 种类文件各自定义,只读规则也供不经过网络的规则单测使用;引擎不引用这两个具体符号。 */ +extern const IEC104_InterrogationRules_t IEC104_GENERAL_INTERROGATION_RULES; +extern const IEC104_InterrogationRules_t IEC104_COUNTER_INTERROGATION_RULES; + +/* 复制配置并借用rules;校验全部规则回调,同一Slave与同一规则表只能创建一个实例。 + * 其他创建/存储契约与公开Create一致,失败返回NULL,不触发网络操作。 */ +IEC104_Interrogation_t *IEC104_Interrogation_Create_With_Rules(IEC104_Slave_t *slave, + const IEC104_InterrogationConfig_t *config, const IEC104_InterrogationRules_t *rules); + +/* 动作入口的内部实现:额外匹配rules身份,校验并消费BEGIN后才调用一次同步action。 + * WAIT/STALE/ERROR不调用action;TEST不执行动作而生成否定确认。成功与失败均不保留回调指针。 */ +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Execute_Action( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, + const IEC104_InterrogationRules_t *rules, + bool (*action)(void *context, const IEC104_InterrogationRequest_t *request), void *context); +#endif diff --git a/IEC60870-Slave/.build/command-v1/before/README.md b/IEC60870-Slave/.build/command-v1/before/README.md new file mode 100644 index 00000000..64c3a498 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/README.md @@ -0,0 +1,70 @@ +# IEC60870-Slave + +IEC 60870 子站协议库,当前提供 IEC104 子站、Windows TCP 后端、总召、计数量召唤和Type 103时钟同步。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_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/`。 + +接入自己的工程时,将 `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`。接入约定见[计数量设计](docs/104/IEC104_CounterInterrogation_设计架构.md)和[时钟同步设计](docs/104/IEC104_ClockSynchronization_设计架构.md)。 + +子站示例默认使用模拟时钟;收到校时命令只更新模拟值。需要修改Windows系统时钟时显式选择`--clock system`,默认模式及自动测试均不修改系统时间。 + +详细资料见[文档索引](docs/README.md)、[模块说明与接入示例](docs/104/README.md)、[子站示例说明](docs/examples/IEC104子站示例.md)和[完整测试命令](docs/test/IEC104测试文档.md#2-编译与运行)。 diff --git a/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Asdu.c b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Asdu.c new file mode 100644 index 00000000..26a33248 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Asdu.c @@ -0,0 +1,461 @@ +/* + * ASDU层仅处理公共头、对象布局、容量和分派。 + * 对象编码调用基类函数表;对象解码按Type ID调用独立类型解析器。 + * 所有外部可见写入都在字段、布局和容量验证完成后提交,失败不留下半条报文。 + */ +#include "IEC60870_Asdu.h" +#include "IEC60870_InformationObjects_Internal.h" + +#include + +/** + * 功能:计算当前配置的ASDU公共头字节数。 + * 输入参数:parameters——已通过IEC60870_AppLayerParameters_Valid检查的非空参数。 + * 输出参数:无。 + * 返回值:2+sizeOfCOT+sizeOfCA,即4、5或6字节。 + * 说明:固定的2字节是Type ID和VSQ;OA已包含在2字节COT中,不能再次累加。 + */ +static uint16_t Header_Size(const IEC60870_AppLayerParameters_t *parameters) +{ + return (uint16_t)(2 + parameters->sizeOfCOT + parameters->sizeOfCA); +} + +/** + * 功能:检查信息对象区的精确长度、数量、SQ约束及连续地址上限。 + * 输入参数:info——非空公共头视图,参数和对象原文在使用期间必须有效且不变。 + * type——与info->type_id匹配的非空只读类型描述。 + * 输出参数:无。 + * 返回值:1表示对象区布局合法;0表示参数、数量、长度或隐含地址范围非法。 + * 说明:先检查长度,再读取首IOA;这里只验证布局,不检查值、品质或时标。 + */ +static int Layout_Valid(const IEC60870_AsduInfo_t *info, + const IEC60870_InformationObjectVTable *type) +{ + uint16_t expected; + if (!IEC60870_AppLayerParameters_Valid(info->parameters) || + info->information_objects == NULL || info->object_count == 0U || + info->object_count > IEC60870_ASDU_MAX_OBJECTS || info->is_sequence > 1U || + (info->is_sequence && !type->supports_sequence) || + (type->single_object && info->object_count != 1U)) { + return 0; + } + /* 设N为数量、D为值载荷长度、L为IOA宽度:SQ=0占N*(L+D),SQ=1占L+N*D。 + * 精确相等同时拒绝截短对象和额外尾字节;这里不计ASDU公共头。 */ + expected = (uint16_t)(info->object_count * type->payload_size + + (info->is_sequence ? info->parameters->sizeOfIOA : + info->object_count * info->parameters->sizeOfIOA)); + if (type->get_payload_size_from_buffer != NULL) { + uint16_t payload_size; + uint16_t address_size = (uint16_t)info->parameters->sizeOfIOA; + /* 当前变长类型是单对象文件段;先保护段头读取,再依据LOS计算实际长度。 */ + if (info->object_count != 1U || info->is_sequence || + info->information_objects_len < address_size) { return 0; } + payload_size = type->get_payload_size_from_buffer(info->information_objects + address_size, + (uint16_t)(info->information_objects_len - address_size)); + if (payload_size == 0U) { return 0; } + expected = (uint16_t)(address_size + payload_size); + } + if (expected != info->information_objects_len || + expected > info->parameters->maxSizeOfASDU - Header_Size(info->parameters)) { + return 0; + } + /* SQ首地址本身可表示,但末地址首IOA+N-1也必须能由同样的IOA宽度表示。 */ + return !info->is_sequence || + IEC60870_Object_Read_LE(info->information_objects, (unsigned int)info->parameters->sizeOfIOA) <= + IEC60870_Object_Max_IOA(info->parameters) - (info->object_count - 1U); +} + +/** + * 功能:定位指定对象的值载荷,同时恢复其信息对象地址。 + * 输入参数:info——已通过Layout_Valid校验的非空ASDU视图。 + * type——与info->type_id匹配的非空只读类型描述。 + * index——从0开始的索引,调用方必须保证小于object_count。 + * 输出参数:ioa——非空地址输出;SQ=1时使用首地址加索引还原。 + * payload_length——非空输出,返回固定或变长载荷的实际长度。 + * 返回值:输入原文中的载荷借用指针,不含IOA,不复制字节。 + * 说明:本内部函数不再验证长度/索引;返回指针的有效期与原ASDU一致。 + */ +static const uint8_t *Element_Position(const IEC60870_AsduInfo_t *info, + const IEC60870_InformationObjectVTable *type, uint8_t index, uint32_t *ioa, + uint16_t *payload_length) +{ + unsigned int address_size = (unsigned int)info->parameters->sizeOfIOA; + const uint8_t *data = info->information_objects; + *payload_length = type->payload_size; + if (type->get_payload_size_from_buffer != NULL) { + /* Layout_Valid已确认变长对象为SQ=0、数量1且载荷完整。 */ + *payload_length = type->get_payload_size_from_buffer(data + address_size, + (uint16_t)(info->information_objects_len - address_size)); + } + /* SQ=1的对象区为:首IOA、载荷0、载荷1……;后续地址不占报文字节。 */ + if (info->is_sequence) { + *ioa = IEC60870_Object_Read_LE(data, address_size) + index; + return data + address_size + index * type->payload_size; + } + /* SQ=0每个对象都有独立IOA,故步长包含地址;取值前先跳过该地址。 */ + data += index * (address_size + type->payload_size); + *ioa = IEC60870_Object_Read_LE(data, address_size); + return data + address_size; +} + +/** + * 功能:核对容器控制字段与已构造ASDU布局是否一致。 + * 输入参数:self——待检查容器或借用原文对象,可为NULL。 + * 输出参数:无。 + * 返回值:1表示结构状态一致;0表示参数、容量、类型、数量、长度或首地址非法。 + * 说明:构造容器按类型重算精确长度;借用原文对象只检查公共头及最低长度。 + * 不重新解码值,参数、控制字段及对象区须由调用方保持有效且不变。 + */ +static int Container_Valid(const IEC60870_Asdu_t *self) +{ + uint16_t expected; + const IEC60870_InformationObjectVTable *type; + if (self == NULL || self->buffer == NULL || + !IEC60870_AppLayerParameters_Valid(self->parameters) || + self->capacity < Header_Size(self->parameters) || + self->capacity > self->parameters->maxSizeOfASDU || + self->length < Header_Size(self->parameters) || + self->length > self->capacity || self->is_sequence > 1U || + self->object_count > IEC60870_ASDU_MAX_OBJECTS || self->is_from_buffer > 1U) { + return 0; + } + if (self->is_from_buffer) { + IEC60870_AsduInfo_t info; + /* 已有原文的容量固定为其长度,不将未知对象当作可追加的构造容器。 + * 不读取未知类型的IOA;first_ioa仅供构造时的连续地址检查使用。 */ + return self->capacity == self->length && self->first_ioa == 0U && + IEC60870_Parse_ASDU_Header(self->parameters, self->buffer, self->length, &info) && + self->type_id == info.type_id && self->object_count == info.object_count && + self->is_sequence == info.is_sequence; + } + expected = Header_Size(self->parameters); + if (self->object_count == 0U) { + if (self->type_id != 0U) { return 0; } + } else { + type = IEC60870_InformationObject_Get_Descriptor(self->type_id); + if (type == NULL || (self->is_sequence && !type->supports_sequence) || + (type->single_object && self->object_count != 1U)) { + return 0; + } + expected = (uint16_t)(expected + self->object_count * type->payload_size + + (self->is_sequence ? self->parameters->sizeOfIOA : + self->object_count * self->parameters->sizeOfIOA)); + if (type->get_payload_size_from_buffer != NULL) { + IEC60870_AsduInfo_t info; + if (!IEC60870_Parse_ASDU_Header(self->parameters, self->buffer, self->length, &info) || + !Layout_Valid(&info, type)) { return 0; } + expected = self->length; + } + } + if (self->length != expected || self->buffer[0] != self->type_id || + self->buffer[1] != (uint8_t)(self->object_count | (self->is_sequence << 7))) { + return 0; + } + if (self->object_count != 0U && + (self->first_ioa != IEC60870_Object_Read_LE(self->buffer + Header_Size(self->parameters), + (unsigned int)self->parameters->sizeOfIOA) || + (self->is_sequence && self->first_ioa > + IEC60870_Object_Max_IOA(self->parameters) - (self->object_count - 1U)))) { + return 0; + } + return 1; +} + +/** + * 功能:在调用方缓冲区中创建一个尚未加入对象的ASDU。 + * 输入参数:parameters——非空合法参数;容器借用指针,使用期间必须有效且不变。 + * capacity——buffer实际可用字节数;有效容量取此值与maxSizeOfASDU的较小值。 + * header——非空公共头配置;COT为0..63,P/N和TEST为0/1,CA须适合所选宽度。 + * is_sequence——0为逐对象IOA,1为连续地址;一条ASDU内不自动切换。 + * 输出参数:self——非空容器存储,成功后记录缓冲区、参数及初始状态。 + * buffer——非空可写ASDU缓冲区,初始只写公共头,不含APCI。 + * 返回值:OK为成功,FULL为实际缓冲区不足头长,ERROR为参数或字段非法。 + * 说明:全部检查通过后才写回;首对象尚未确定,Type ID和对象数均为0。 + * 本函数不分配内存,也不发送;SQ适用性按具体类型检查,空容器不能Get_Data。 + */ +IEC60870_AsduResult_t IEC60870_Asdu_Create(IEC60870_Asdu_t *self, + const IEC60870_AppLayerParameters_t *parameters, + uint8_t *buffer, uint16_t capacity, const IEC60870_AsduHeader_t *header, int is_sequence) +{ + IEC60870_Asdu_t result; + uint16_t header_length; + if (self == NULL || buffer == NULL || header == NULL || + !IEC60870_AppLayerParameters_Valid(parameters) || + (is_sequence != 0 && is_sequence != 1) || + header->cause_of_transmission > 63U || header->is_negative_confirm > 1U || + header->is_test > 1U || (parameters->sizeOfCA == 1 && header->common_address > 255U)) { + return IEC60870_ASDU_ERROR; + } + header_length = Header_Size(parameters); + if (capacity < header_length) { return IEC60870_ASDU_FULL; } + memset(&result, 0, sizeof(result)); + result.buffer = buffer; + /* 缓冲区较大也不能突破协议配置上限;仅保存参数指针,不复制配置。 */ + result.capacity = capacity > parameters->maxSizeOfASDU ? + (uint16_t)parameters->maxSizeOfASDU : capacity; + result.length = header_length; + result.is_sequence = (uint8_t)is_sequence; + result.parameters = parameters; + /* 此后没有可能失败的校验;按所选宽度恰好写入header_length字节。 */ + /* Type ID暂置0;VSQ bit7为SQ、低7位为数量,初始数量为0。 */ + buffer[0] = 0; + buffer[1] = (uint8_t)(is_sequence << 7); + /* COT首字节:bit0..5为原因、bit6为P/N、bit7为TEST;2字节COT才写OA。 */ + buffer[2] = (uint8_t)(header->cause_of_transmission | + (header->is_negative_confirm << 6) | (header->is_test << 7)); + if (parameters->sizeOfCOT == 2) { buffer[3] = header->originator_address; } + IEC60870_Object_Write_LE(buffer + 2 + parameters->sizeOfCOT, + header->common_address, (unsigned int)parameters->sizeOfCA); + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:将已有可写ASDU原文包装成调用方持有的对象。 + * 输入参数:parameters——非空应用层参数;buffer——可写原文;length——原文长度。 + * 输出参数:self——全部检查成功后写入;buffer不变。 + * 返回值:OK为成功,ERROR为参数或最低长度非法。 + * 说明:借用参数和原文,不分配、不复制;本入口不替代业务使用的完整Parse。 + * 容量固定为原文长度,后续只允许修改公共头,不能追加信息对象。 + */ +IEC60870_AsduResult_t IEC60870_Asdu_Create_From_Buffer(IEC60870_Asdu_t *self, + const IEC60870_AppLayerParameters_t *parameters, uint8_t *buffer, uint16_t length) +{ + IEC60870_AsduInfo_t info; + IEC60870_Asdu_t result; + if (self == NULL || !IEC60870_Parse_ASDU_Header(parameters, buffer, length, &info)) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.buffer = buffer; + result.capacity = length; + result.length = length; + result.type_id = info.type_id; + result.object_count = info.object_count; + result.is_sequence = info.is_sequence; + result.is_from_buffer = 1U; + result.parameters = parameters; + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:修改ASDU传送原因,保留COT字节中的两个独立标志。 + * 输入参数:self——合法ASDU对象;cause——0..63。 + * 输出参数:self->buffer[2]——校验成功后更新低6位。 + * 返回值:OK为成功,ERROR为参数或对象状态非法。 + * 说明:所有字段宽度配置中COT首字节均位于偏移2;不改变OA或载荷。 + */ +IEC60870_AsduResult_t IEC60870_Asdu_Set_COT(IEC60870_Asdu_t *self, int cause) +{ + if (cause < 0 || cause > 63 || !Container_Valid(self)) { return IEC60870_ASDU_ERROR; } + self->buffer[2] = (uint8_t)((self->buffer[2] & 0xC0U) | (uint8_t)cause); + return IEC60870_ASDU_OK; +} + +/** + * 功能:修改ASDU的P/N标志,保留传送原因及TEST。 + * 输入参数:self——合法ASDU对象;is_negative——0或1。 + * 输出参数:self->buffer[2]——校验成功后仅更新bit6。 + * 返回值:OK为成功,ERROR为参数或对象状态非法。 + * 说明:不执行命令,不提交发送;失败保持字节原值。 + */ +IEC60870_AsduResult_t IEC60870_Asdu_Set_Negative(IEC60870_Asdu_t *self, int is_negative) +{ + if ((is_negative != 0 && is_negative != 1) || !Container_Valid(self)) { + return IEC60870_ASDU_ERROR; + } + self->buffer[2] = (uint8_t)((self->buffer[2] & 0xBFU) | (uint8_t)(is_negative << 6)); + return IEC60870_ASDU_OK; +} + +/** + * 功能:修改ASDU公共地址,保留其他公共头字段与对象区。 + * 输入参数:self——合法ASDU对象;common_address——须适合sizeOfCA的无符号地址。 + * 输出参数:self->buffer——在2+sizeOfCOT偏移按小端写入sizeOfCA字节。 + * 返回值:OK为成功,ERROR为非法容器或1字节CA无法表示的地址;失败不写入。 + * 说明:沿用Container_Valid检查构造及借用对象,不截断地址,不决定是否允许广播。 + */ +IEC60870_AsduResult_t IEC60870_Asdu_Set_CA(IEC60870_Asdu_t *self, uint16_t common_address) +{ + if (!Container_Valid(self) || (self->parameters->sizeOfCA == 1 && common_address > 255U)) { + return IEC60870_ASDU_ERROR; + } + IEC60870_Object_Write_LE(self->buffer + 2 + self->parameters->sizeOfCOT, + common_address, (unsigned int)self->parameters->sizeOfCA); + return IEC60870_ASDU_OK; +} + +/** + * 功能:编码并追加一个同类型对象,成功后更新ASDU长度及VSQ。 + * 输入参数:self——已成功Create且控制字段/参数/原文未被修改的容器。 + * object——已由具体类型Create或解析器初始化的公共基类首成员。 + * 输出参数:self——成功后更新容器及其字节缓冲区,失败保持原值。 + * 返回值:OK为追加成功;FULL为字节空间或对象数已满;UNSUPPORTED为未知类型; + * ERROR为容器、身份、字段、地址或SQ约束非法。 + * 说明:在有界临时区完成IOA和值编码(含变长文件段),确认容量后才写入。 + * 首个成功对象固定Type ID;SQ=1首对象仍带IOA,其后只写载荷且地址必须递增1。 + * FULL时由调用方提交已有非空ASDU,另起一条重试同一对象;不会自动拆包。 + * 成功后仅保存对象的报文字节,不借用object,调用方可以立即复用其存储。 + */ +IEC60870_AsduResult_t IEC60870_Asdu_Add_InformationObject(IEC60870_Asdu_t *self, + const IEC60870_InformationObject_t *object) +{ + const IEC60870_InformationObjectVTable *type; + uint8_t encoded[IEC60870_OBJECT_MAX_ENCODED]; + uint16_t added; + IEC60870_AsduResult_t status; + if (!Container_Valid(self) || self->is_from_buffer || object == NULL) { return IEC60870_ASDU_ERROR; } + type = IEC60870_InformationObject_Get_Descriptor(object->type_id); + if (type == NULL) { return IEC60870_ASDU_UNSUPPORTED; } + if (object->virtual_function_table != type || + (self->object_count != 0U && self->type_id != type->type_id) || + (self->is_sequence && !type->supports_sequence) || + object->ioa > IEC60870_Object_Max_IOA(self->parameters)) { + return IEC60870_ASDU_ERROR; + } + /* 用减法先判断地址范围,再比较首IOA+已有数量,避免允许隐含地址越界。 */ + if (self->is_sequence && self->object_count != 0U && + (self->first_ioa > IEC60870_Object_Max_IOA(self->parameters) - self->object_count || + object->ioa != self->first_ioa + self->object_count)) { + return IEC60870_ASDU_ERROR; + } + /* 字段错误优先于容量已满,以免把非法对象误当作下一条ASDU的可重试输入。 */ + status = IEC60870_InformationObject_Encode(object, self->parameters, + self->is_sequence && self->object_count != 0U, encoded, sizeof(encoded), &added); + if (status != IEC60870_ASDU_OK) { return status; } + if (self->object_count == IEC60870_ASDU_MAX_OBJECTS || + (type->single_object && self->object_count != 0U) || + added > self->capacity - self->length) { + return IEC60870_ASDU_FULL; + } + /* 从此处开始统一提交,没有后续失败分支,容量不足不会留下半个对象。 */ + memcpy(self->buffer + self->length, encoded, added); + if (self->object_count == 0U) { + self->first_ioa = object->ioa; + self->type_id = type->type_id; + self->buffer[0] = type->type_id; + } + self->length = (uint16_t)(self->length + added); + ++self->object_count; + self->buffer[1] = (uint8_t)(self->object_count | (self->is_sequence << 7)); + return IEC60870_ASDU_OK; +} + +/** + * 功能:导出构造完成或直接借用的ASDU原文。 + * 输入参数:self——非空合法对象;Create创建的容器必须至少加入一个对象。 + * 输出参数:asdu——非空输出指针,成功时指向调用方原有缓冲区。 + * length——非空输出指针,成功时得到当前ASDU字节数,不含APCI。 + * 返回值:OK为成功;ERROR为容器、输出参数非法或容器为空;失败不修改输出。 + * 说明:返回借用视图,不复制或发送。借用原文对象不额外进行完整对象校验。 + * 后续Add/Create/Set会影响原文,length只是本次快照。 + */ +IEC60870_AsduResult_t IEC60870_Asdu_Get_Data(const IEC60870_Asdu_t *self, + const uint8_t **asdu, uint16_t *length) +{ + if (!Container_Valid(self) || (!self->is_from_buffer && self->object_count == 0U) || + asdu == NULL || length == NULL) { + return IEC60870_ASDU_ERROR; + } + *asdu = self->buffer; + *length = self->length; + return IEC60870_ASDU_OK; +} + +/** + * 功能:校验完整ASDU(含参数和文件类型),生成可用于逐对象提取的只读视图。 + * 输入参数:parameters——显式非空应用层参数,字段宽度必须对应收到的报文。 + * asdu——非空ASDU原文,不含APCI。 + * length——完整ASDU字节数,不能超过配置上限,不能包含多余尾字节。 + * 输出参数:info——非空视图输出,借用parameters和asdu,二者须在取对象期间有效且不变。 + * 返回值:OK为成功;UNSUPPORTED为未支持类型;ERROR为公共头、布局或任一对象非法。 + * 说明:先检查公共头和布局,再验证所有对象,全部成功后才发布info,失败保留旧输出。 + * 不执行命令,也不判断COT/CA/IOA的业务适用性;具体对象通过Get_Element获取。 + */ +IEC60870_AsduResult_t IEC60870_Asdu_Parse(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *asdu, uint16_t length, IEC60870_AsduInfo_t *info) +{ + IEC60870_AsduInfo_t result; + const IEC60870_InformationObjectVTable *type; + uint8_t i; + if (info == NULL || !IEC60870_Parse_ASDU_Header(parameters, asdu, length, &result)) { + return IEC60870_ASDU_ERROR; + } + type = IEC60870_InformationObject_Get_Descriptor(result.type_id); + if (type == NULL) { return IEC60870_ASDU_UNSUPPORTED; } + if (!Layout_Valid(&result, type)) { return IEC60870_ASDU_ERROR; } + /* 输出传NULL使类型解析器仅校验,不分配对象数组;任一对象失败就丢弃整个临时视图。 */ + for (i = 0; i < result.object_count; ++i) { + uint32_t ioa; + uint16_t payload_length; + const uint8_t *payload = Element_Position(&result, type, i, &ioa, &payload_length); + IEC60870_AsduResult_t status = IEC60870_InformationObject_Decode(result.type_id, ioa, + payload, payload_length, NULL, 0); + if (status != IEC60870_ASDU_OK) { return status; } + } + *info = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从已解析视图中按索引取得一个独立类型的对象副本。 + * 输入参数:info——由Asdu_Parse生成且未修改的视图,其原文和参数仍须有效。 + * index——从0开始的对象索引,必须小于object_count。 + * object_size——object实际可用字节数,至少为对应具体结构的sizeof。 + * 输出参数:object——非空具体对象存储,结构类型必须由info->type_id正确选择。 + * 返回值:OK为成功;UNSUPPORTED为未知类型;ERROR为布局、索引、载荷或输出尺寸非法。 + * 说明:输出无需预先Create,成功时按值保存且不借用原文,失败保持原输出不变。 + * 只重新检查布局和选中对象;尺寸校验不能替代C语言类型匹配,公共头仍从info读取。 + */ +IEC60870_AsduResult_t IEC60870_Asdu_Get_Element(const IEC60870_AsduInfo_t *info, + uint8_t index, void *object, size_t object_size) +{ + const IEC60870_InformationObjectVTable *type; + const uint8_t *payload; + uint32_t ioa; + uint16_t payload_length; + if (info == NULL || object == NULL) { return IEC60870_ASDU_ERROR; } + type = IEC60870_InformationObject_Get_Descriptor(info->type_id); + if (type == NULL) { return IEC60870_ASDU_UNSUPPORTED; } + if (!Layout_Valid(info, type) || index >= info->object_count) { return IEC60870_ASDU_ERROR; } + payload = Element_Position(info, type, index, &ioa, &payload_length); + return IEC60870_InformationObject_Decode(info->type_id, ioa, payload, + payload_length, object, object_size); +} + +/** + * 功能:复制请求原文并改写COT/P/N,构造确认、终止或未知字段错误应答。 + * 输入参数:parameters——与请求布局匹配的非空合法应用层参数。 + * request——非空请求ASDU原文,不含APCI。 + * length——请求ASDU的完整字节数。 + * cause——接受7、9、10及44..47;分别用于确认、终止及未知类型/原因/地址。 + * is_negative——P/N标志,0或1;否定激活确认使用cause=7、is_negative=1。 + * capacity——out实际可写字节数,至少为length。 + * 输出参数:out——非空应答缓冲区,可与request原地或部分重叠。 + * out_length——非空长度输出,成功时等于length。 + * 返回值:OK为成功,FULL为输出容量不足,ERROR为参数或请求基础格式非法。 + * 说明:只依赖公共头和最低长度检查,不要求具体对象解析成功,因此可回显未知类型。 + * 保留TEST、OA、CA、VSQ及所有对象字节;失败不修改out/out_length,不自动发送。 + */ +IEC60870_AsduResult_t IEC60870_Asdu_Build_Response(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *request, uint16_t length, + int cause, int is_negative, uint8_t *out, uint16_t capacity, uint16_t *out_length) +{ + IEC60870_AsduInfo_t info; + uint8_t cot; + if (out == NULL || out_length == NULL || (is_negative != 0 && is_negative != 1) || + (cause != IEC60870_ASDU_COT_ACTIVATION_CON && cause != IEC60870_ASDU_COT_DEACTIVATION_CON && + cause != IEC60870_ASDU_COT_ACTIVATION_TERMINATION && + (cause < IEC60870_ASDU_COT_UNKNOWN_TYPE || cause > IEC60870_ASDU_COT_UNKNOWN_IOA)) || + !IEC60870_Parse_ASDU_Header(parameters, request, length, &info)) { + return IEC60870_ASDU_ERROR; + } + if (capacity < length) { return IEC60870_ASDU_FULL; } + /* 先保存新COT,再memmove原文;即使输入输出重叠,也不会丢失请求的TEST。 */ + cot = (uint8_t)(cause | (is_negative << 6) | (info.is_test << 7)); + memmove(out, request, length); + out[2] = cot; + *out_length = length; + return IEC60870_ASDU_OK; +} diff --git a/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Asdu.h b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Asdu.h new file mode 100644 index 00000000..c93e2450 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Asdu.h @@ -0,0 +1,167 @@ +/* + * 101/104共用ASDU容器。公共头、VSQ/SQ、容量、IOA布局由本层维护, + * 数值、品质、命令限定词和时标的编解码由具体信息对象完成。 + * 参数与字节缓冲区均由调用方提供;无动态内存或可变全局状态。 + * + * 发送:具体类型Create -> Asdu_Create -> Add_InformationObject(逐个)-> Get_Data。 + * 接收:Asdu_Parse -> 按info.type_id选择具体结构 -> Get_Element。 + * Type ID决定载荷格式,COT表示传送原因,CA是站公共地址,IOA是点/对象地址; + * VSQ低7位是对象数,bit7是SQ。SQ=1只保存首IOA,随后各对象地址依次加1。 + */ +#ifndef IEC60870_ASDU_H +#define IEC60870_ASDU_H + +#include "IEC60870_Codec.h" +#include "IEC60870_InformationObjects.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* COT首字节的低6位,不含独立的P/N和TEST标志;不是完整的COT适用规则表。 */ +typedef enum { + IEC60870_ASDU_COT_PERIODIC = 1, /* 周期/循环上报。 */ + IEC60870_ASDU_COT_BACKGROUND = 2, /* 背景扫描。 */ + IEC60870_ASDU_COT_SPONTANEOUS = 3, /* 突发上报。 */ + IEC60870_ASDU_COT_INITIALIZED = 4, /* 初始化结束。 */ + IEC60870_ASDU_COT_REQUEST = 5, /* 请求或被请求的数据。 */ + IEC60870_ASDU_COT_ACTIVATION = 6, /* 激活请求。 */ + IEC60870_ASDU_COT_ACTIVATION_CON = 7, /* 激活确认,P/N另行指定。 */ + IEC60870_ASDU_COT_DEACTIVATION = 8, /* 停止激活请求。 */ + IEC60870_ASDU_COT_DEACTIVATION_CON = 9, /* 停止激活确认。 */ + IEC60870_ASDU_COT_ACTIVATION_TERMINATION = 10, /* 激活终止。 */ + IEC60870_ASDU_COT_FILE_TRANSFER = 13, /* 文件传输。 */ + IEC60870_ASDU_COT_INTERROGATED_BY_STATION = 20, /* 响应站总召唤的数据。 */ + IEC60870_ASDU_COT_COUNTER_INTERROGATION = 37, /* 响应电能脉冲总召唤的数据。 */ + IEC60870_ASDU_COT_COUNTER_GROUP_1 = 38, + IEC60870_ASDU_COT_COUNTER_GROUP_2 = 39, + IEC60870_ASDU_COT_COUNTER_GROUP_3 = 40, + IEC60870_ASDU_COT_COUNTER_GROUP_4 = 41, + IEC60870_ASDU_COT_UNKNOWN_TYPE = 44, /* 未知类型标识。 */ + IEC60870_ASDU_COT_UNKNOWN_CAUSE = 45, /* 未知传送原因。 */ + IEC60870_ASDU_COT_UNKNOWN_COMMON_ADDRESS = 46, /* 未知公共地址。 */ + IEC60870_ASDU_COT_UNKNOWN_IOA = 47 /* 未知信息对象地址。 */ +} IEC60870_AsduCause_t; + +/* 创建ASDU时仅提供公共头;Type ID由首个成功添加的对象决定,VSQ由容器维护。 + * 字段赋值前不得截窄越界整数。COT/CA/IOA的业务适用性由应用层检查。 */ +typedef struct { + uint8_t cause_of_transmission; /* COT低6位,0..63,不含P/N及TEST。 */ + uint8_t is_negative_confirm; /* P/N:0肯定、1否定。 */ + uint8_t is_test; /* TEST:0/1。 */ + uint8_t originator_address; /* 显式OA;COT为1字节时忽略,解析输出0。 */ + uint16_t common_address; /* 1字节CA拒绝超过255的值,不截断。 */ +} IEC60870_AsduHeader_t; + +/* 调用方保存的容器;控制字段由Create/Create_From_Buffer初始化,不得手工修改。 + * 字节区通过Add或字段设置接口修改;parameters在使用期间必须有效且不变。 + * Create_From_Buffer借用可写原文,只允许设置公共头,不允许继续追加对象。 + * 同一实例由调用方串行使用;对象不分配或释放其借用的缓冲区。 */ +typedef struct { + uint8_t *buffer; /* ASDU原文,不含APCI。 */ + uint16_t capacity; /* min(调用方容量, maxSizeOfASDU)。 */ + uint16_t length; /* 当前字节数,空容器仅有公共头。 */ + uint8_t type_id; /* 空容器为0,首个对象成功加入后固定。 */ + uint8_t object_count; /* 按类型限制为0..127或0..1;借用时记录原VSQ数量。 */ + uint8_t is_sequence; /* SQ:0逐对象地址,1连续地址。 */ + uint8_t is_from_buffer;/* 1为借用收到的原文;0为Create创建的构造容器。 */ + uint32_t first_ioa; /* 构造时供SQ连续性校验;借用原文时为0且不使用。 */ + const IEC60870_AppLayerParameters_t *parameters; /* 借用参数,不复制。 */ +} IEC60870_Asdu_t; + +/* 结果须显式比较IEC60870_ASDU_OK;FULL=0,ERROR/UNSUPPORTED为负值。 + * 所有失败保留调用方输出。结构体、输出参数和字节区不得互相重叠; + * 唯一例外是Build_Response允许输入/输出字节区重叠。 */ + +/* 创建空容器并固定参数、SQ和公共头;参数必须非空,头长=2+COT宽度+CA宽度。 + * 有效容量不足头长返回FULL;SQ=1只适用于监视信息和文件目录。 + * 初始Type ID为0,首个对象失败不会改变类型;空容器不能Get_Data。 */ +IEC60870_AsduResult_t IEC60870_Asdu_Create(IEC60870_Asdu_t *self, + const IEC60870_AppLayerParameters_t *parameters, + uint8_t *buffer, uint16_t capacity, const IEC60870_AsduHeader_t *header, int is_sequence); + +/** + * 功能:在调用方存储中建立可修改的ASDU对象,直接借用已有报文字节。 + * 输入参数:parameters——对应原文布局的非空合法参数,使用期间有效且不变。 + * buffer——非空可写ASDU原文,不含APCI;length——原文完整长度。 + * 输出参数:self——成功时写入容器,失败保持原值;buffer不变。 + * 返回值:OK为成功,ERROR为参数或最低长度非法。 + * 说明:只检查公共头和已知类型最低长度,允许未知类型回显错误应答。 + * 业务执行前仍应调用Parse进行完整校验;不复制、不分配、不追加对象。 + * 字段设置会修改buffer;必须保证buffer和parameters在使用期间有效。 + */ +IEC60870_AsduResult_t IEC60870_Asdu_Create_From_Buffer(IEC60870_Asdu_t *self, + const IEC60870_AppLayerParameters_t *parameters, uint8_t *buffer, uint16_t length); + +/** + * 功能:原地设置ASDU的传送原因。 + * 输入参数:self——合法构造容器或借用原文的对象;cause——0..63。 + * 输出参数:self->buffer——仅更新COT低6位,保留P/N、TEST及其余字节。 + * 返回值:OK为成功,ERROR为参数非法;失败不修改原文。 + * 说明:不判断传送原因的业务适用性,也不发送报文。 + */ +IEC60870_AsduResult_t IEC60870_Asdu_Set_COT(IEC60870_Asdu_t *self, int cause); + +/** + * 功能:原地设置ASDU的肯定或否定确认标志。 + * 输入参数:self——合法ASDU对象;is_negative——0肯定、1否定。 + * 输出参数:self->buffer——仅更新P/N位,保留COT、TEST及其余字节。 + * 返回值:OK为成功,ERROR为参数非法;失败不修改原文。 + * 说明:设置字段不代表业务已接受命令,也不提交发送。 + */ +IEC60870_AsduResult_t IEC60870_Asdu_Set_Negative(IEC60870_Asdu_t *self, int is_negative); + +/** + * 功能:原地设置ASDU公共地址,适用于合法构造容器或借用可写原文的对象。 + * 输入参数:self——合法ASDU对象;common_address——0..65535,须适合配置的CA宽度。 + * 输出参数:self->buffer——仅更新CA字段,按小端序写入1或2字节。 + * 返回值:OK为成功,ERROR为对象状态或字段宽度非法;失败不修改任何原文字节。 + * 说明:偏移由COT宽度确定,保留OA、TEST、P/N及对象原文;不限制业务上的站地址范围。 + * 不分配、不改变容器长度、不发送;原文修改后,旧解析视图须重新解析。 + */ +IEC60870_AsduResult_t IEC60870_Asdu_Set_CA(IEC60870_Asdu_t *self, uint16_t common_address); + +/* 通过对象函数表编码并复制到ASDU,成功后不再借用对象本身。 + * 单类型约束、SQ连续地址和IOA宽度统一检查。字节容量不足、对象达到127个, + * 或单对象类型已加入对象时返回FULL;类型不匹配、SQ或字段非法返回ERROR。 + * FULL时由调用方提交当前非空ASDU,另起一条重试同一对象,不自动发送或拆包。 */ +IEC60870_AsduResult_t IEC60870_Asdu_Add_InformationObject(IEC60870_Asdu_t *self, + const IEC60870_InformationObject_t *object); + +/* 获取构造容器或Create_From_Buffer对象的借用原文及当前长度。 + * Create创建的空容器不可导出;借用原文对象沿用公共头最低长度检查。 + * 不分配、不复制、不发送;需完整检查对象值时先调用Parse。 + * 缓冲区修改后原视图失效;提交104传输时应使用2/2/3配置及<=249的总长。 */ +IEC60870_AsduResult_t IEC60870_Asdu_Get_Data(const IEC60870_Asdu_t *self, + const uint8_t **asdu, uint16_t *length); + +/* 统一解析67种监视、命令、初始化结束、参数和文件类型。 + * 验证精确长度和所有对象,文件段按LOS检查变长载荷。 + * 监视与文件目录支持SQ=0/1;参数允许多个对象但要求SQ=0。 + * 其余类型要求SQ=0、数量=1;未知类型返回UNSUPPORTED。 + * info借用参数和输入原文,二者在取对象期间均须有效且不变。 */ +IEC60870_AsduResult_t IEC60870_Asdu_Parse(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *asdu, uint16_t length, IEC60870_AsduInfo_t *info); + +/* 按Type ID调用具体解析器,向对应类型的存储写入一个按值对象。 + * 例如Type 1传入&point、sizeof(point),其中point为IEC60870_SinglePointInformation_t。 + * 调用方必须依据info->type_id选择正确的结构类型;object_size至少为该结构尺寸, + * 尺寸检查不能替代C语言中的类型匹配。index从0开始,输出无需预先Create。 + * 对象不借用原文;公共头继续从info读取,选中对象之外不再次重复解码。 */ +IEC60870_AsduResult_t IEC60870_Asdu_Get_Element(const IEC60870_AsduInfo_t *info, + uint8_t index, void *object, size_t object_size); + +/* 复制请求,只更新COT及P/N,保留TEST、OA、CA、VSQ和对象原文。 + * cause接受7/9/10/44..47,is_negative为0/1;否定激活确认传7、1。 + * 仅检查公共头及最低长度,允许未知类型回显,不要求对象解析成功。 + * 参数须对应请求布局。容量不足返回FULL,所有失败保留out及out_length。 + * 输入/输出字节区支持原地与部分重叠;不执行命令或自动发送。 */ +IEC60870_AsduResult_t IEC60870_Asdu_Build_Response(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *request, uint16_t length, + int cause, int is_negative, uint8_t *out, uint16_t capacity, uint16_t *out_length); + +#ifdef __cplusplus +} +#endif + +#endif /* IEC60870_ASDU_H */ diff --git a/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Clock.c b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Clock.c new file mode 100644 index 00000000..fc0444b8 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Clock.c @@ -0,0 +1,204 @@ +/* + * IEC 60870-5-101/104 共用时间源实现:读取当前日历、设置实际时钟。 + * IEC60870_CLOCK_USE_RTC=0 使用本文件内的 Windows 读写,=1 调用 RTC 预留接口。 + * 公共入口统一使用固定 UTC+8 标准时间,检查日历、读取质量并重算星期。 + * Windows 分支负责 UTC 与 UTC+8 的转换,不使用计算机配置的本地时区或夏令时。 + * RTC 分支直接收发约定的 UTC+8 日历;芯片初始化、寄存器读写由板级驱动负责。 + */ +#include "IEC60870_Clock.h" + +/* 编译时选择时间源;RTC 模式不编译下面的 Windows 实现,也不包含 windows.h。 */ +#if !IEC60870_CLOCK_USE_RTC + +#ifndef _WIN32 +#error "Windows clock requires _WIN32; define IEC60870_CLOCK_USE_RTC=1 for RTC" +#else + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +/* + * 函数功能:在日历数值上加减固定八小时,完成 UTC 与 UTC+8 标准时间的转换。 + * 入口参数:source——源日历;to_local——非 0 表示 UTC 加八小时,0 表示 UTC+8 减八小时。 + * 出口参数:result——转换后的日历,仅返回成功时可使用;两个指针均由内部调用方保证非空。 + * 返回值:1 转换成功;0 Windows 日期转换失败,或加减偏移会使中间计数溢出。 + * 说明:FILETIME 以 100 纳秒为单位,从 1601-01-01 起计数;此处只用它进行日期运算。 + * 本地日历也按字段数值展开,平移后的中间计数不作为 UTC 时间戳对外返回。 + * 加减后交给 Windows 还原年月日,自然处理跨日、跨月、跨年及闰日。 + * 1970..2099 的业务日历范围由读写入口检查,本函数不额外限制年份。 + */ +static int Shift_Calendar(const SYSTEMTIME *source, int to_local, SYSTEMTIME *result) +{ + /* 分钟数 × 每分钟秒数 × 每秒 100 纳秒单位数;先转为 64 位再乘,避免中间结果溢出。 */ + const uint64_t offset_ticks = (uint64_t)IEC60870_LOCAL_UTC_OFFSET_MINUTES * + UINT64_C(60) * UINT64_C(10000000); + FILETIME encoded; + uint64_t ticks; + + /* 只展开日历数值,不查询系统本地时区;转换失败时不使用 encoded。 */ + if (!SystemTimeToFileTime(source, &encoded)) { + return 0; + } + /* FILETIME 分为高、低两个 32 位字段,按位组合可避免指针强转带来的对齐和别名问题。 */ + ticks = ((uint64_t)encoded.dwHighDateTime << 32) | encoded.dwLowDateTime; + if (to_local != 0) { + /* 先检查再加,防止无符号整数回绕为较小的时间值。 */ + if (ticks > UINT64_MAX - offset_ticks) { + return 0; + } + ticks += offset_ticks; + } else { + /* 先检查再减,防止越过计数原点后回绕为很大的时间值。 */ + if (ticks < offset_ticks) { + return 0; + } + ticks -= offset_ticks; + } + /* 拆回 FILETIME,由系统重新计算全部日历字段,毫秒随计数一起保留。 */ + encoded.dwLowDateTime = (DWORD)(ticks & UINT64_C(0xFFFFFFFF)); + encoded.dwHighDateTime = (DWORD)(ticks >> 32); + return FileTimeToSystemTime(&encoded, result) != 0; +} + +/* + * 函数功能:读取 Windows 系统时间,输出固定 UTC+8 标准时间。 + * 入口参数:value——接收结果的指针,不得为空。 + * 出口参数:value——合法日历及 is_invalid=0;失败时不修改调用方结果。 + * 返回值:1 取得合法结果;0 空指针、偏移转换失败或日历超出模块支持范围。 + * 说明:一次 GetSystemTime 取得完整 UTC 日历及毫秒,避免分次取字段时遇到秒或日期进位。 + * 固定加八小时,与 Windows 设置的本地时区无关;最终结果仍交由公共入口校验。 + */ +static int Windows_Read(IEC60870_ClockValue_t *value) +{ + IEC60870_ClockValue_t result = {0}; + SYSTEMTIME utc, local; + + if (value == NULL) { + return 0; + } + GetSystemTime(&utc); + if (!Shift_Calendar(&utc, 1, &local)) { + return 0; + } + result.time.year = local.wYear; + result.time.month = (uint8_t)local.wMonth; + result.time.day = (uint8_t)local.wDay; + result.time.hour = (uint8_t)local.wHour; + result.time.minute = (uint8_t)local.wMinute; + result.time.second = (uint8_t)local.wSecond; + result.time.ms = local.wMilliseconds; + result.time.weekday = (uint8_t)local.wDayOfWeek; + /* 加八小时可能跨到下一年,必须检查转换后的日历是否仍在 1970..2099 内。 */ + if (!IEC60870_Time_Is_Valid(&result.time)) { + return 0; + } + /* Windows 读时接口不提供同步质量;0 仅表示未标记无效,不代表已校准或已同步。 */ + result.is_invalid = 0U; + *value = result; + return 1; +} + +/* + * 函数功能:将固定 UTC+8 标准时间转换为 UTC,并实际修改 Windows 系统时钟。 + * 入口参数:local_time——1970..2099 的合法 UTC+8 日历,原始输入不会被修改。 + * 返回值:1 SetSystemTime 报告成功;0 参数非法、转换失败或系统调用失败。 + * 说明:SetSystemTime 接收 UTC,因此必须先减八小时;它不使用 wDayOfWeek 字段。 + * 调用进程需要系统设时权限,权限不足时返回 0;成功与否以系统调用结果为准。 + */ +static int Windows_Set(const IEC60870_WallTime_t *local_time) +{ + SYSTEMTIME local = {0}, utc; + + if (!IEC60870_Time_Is_Valid(local_time)) { + return 0; + } + local.wYear = local_time->year; + local.wMonth = local_time->month; + local.wDay = local_time->day; + local.wHour = local_time->hour; + local.wMinute = local_time->minute; + local.wSecond = local_time->second; + local.wMilliseconds = local_time->ms; + if (!Shift_Calendar(&local, 0, &utc)) { + return 0; + } + return SetSystemTime(&utc) != 0; +} + +#endif /* _WIN32 */ +#endif /* !IEC60870_CLOCK_USE_RTC */ + +/* + * 函数功能:检查完整日历,再根据年月日重算星期,保证日期与星期一致。 + * 入口参数:time——公共读写入口持有的局部日历副本。 + * 出口参数:time->weekday——0=周日,1=周一,...,6=周六;其他字段保持原值。 + * 返回值:1 日历合法且星期计算成功;0 空指针、日历非法或星期计算失败。 + * 说明:传入的 weekday 不参与合法性判断;本函数不校准时钟,也不转换时区。 + */ +static int Normalize_Calendar(IEC60870_WallTime_t *time) +{ + if (!IEC60870_Time_Is_Valid(time)) { + return 0; + } + return IEC60870_Time_Get_Weekday(time->year, time->month, time->day, &time->weekday); +} + +/* + * 函数功能:从选定的 Windows 或 RTC 时间源读取日历,检查完成后再提交输出。 + * 入口参数:value——接收结果的指针,不得为空。 + * 出口参数:value——固定 UTC+8 日历、重算的星期及时间源提供的 is_invalid。 + * 返回值:1 得到合法日历和 0/1 质量标志;0 参数非法、底层读取失败或结果非法。 + * 说明:返回 1 只表示本次读取完成,is_invalid=1 的结果仍需业务按无效时间处理。 + * 底层仅操作局部结果,失败前即使写入了部分字段,也不会修改调用方输出。 + */ +int IEC60870_Clock_Read(IEC60870_ClockValue_t *value) +{ + IEC60870_ClockValue_t result = {0}; + int status; + + if (value == NULL) { + return 0; + } + /* 驱动未主动报告有效状态时,保留无效标志,不因零初始化而假定时钟有效。 */ + result.is_invalid = 1U; +#if IEC60870_CLOCK_USE_RTC + /* RTC 直接提供 UTC+8 日历,公共层无需将其转成时间戳再还原。 */ + status = IEC60870_Clock_RTC_Read(&result); +#else + status = Windows_Read(&result); +#endif + /* 底层必须明确返回 1;质量值只能为 0/1,日历通过检查后才覆盖输出。 */ + if (status != 1 || result.is_invalid > 1U || !Normalize_Calendar(&result.time)) { + return 0; + } + *value = result; + return 1; +} + +/* + * 函数功能:检查固定 UTC+8 日历,将重算星期的副本交给选定的时间源设置。 + * 入口参数:local_time——待设置的完整日历,不得为空;传入的 weekday 会被重新计算。 + * 返回值:1 底层明确报告设置成功;0 参数非法或底层未报告成功。 + * 说明:先复制并校验,检查失败时不调用底层,也不修改原始输入。 + * 校时报文的 IV/SU、源时区及业务校时条件应由上层处理,本入口只接收日历。 + * 返回 0 不保证硬件未发生部分写入,公共层不提供设备写入回滚。 + */ +int IEC60870_Clock_Set(const IEC60870_WallTime_t *local_time) +{ + IEC60870_WallTime_t prepared; + + if (local_time == NULL) { + return 0; + } + prepared = *local_time; + if (!Normalize_Calendar(&prepared)) { + return 0; + } +#if IEC60870_CLOCK_USE_RTC + return IEC60870_Clock_RTC_Set(&prepared) == 1; +#else + return Windows_Set(&prepared) == 1; +#endif +} diff --git a/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Clock.h b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Clock.h new file mode 100644 index 00000000..c89f838d --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Clock.h @@ -0,0 +1,106 @@ +/* + * IEC 60870-5-101/104 共用时间源接口:读取当前日历、设置实际时钟。 + * 读写日历统一采用固定UTC+8标准时间,年份1970..2099;CP56编码另限2000..2099。 + * 此处的本地时间指固定UTC+8,不随Windows本地时区设置或夏令时变化。 + * 时间源仅考虑Windows系统时钟和RTC,构建时二选一,业务直接调用Clock_Read/Clock_Set。 + * Windows实现已提供;RTC目前只保留接入接口,具体芯片驱动暂不实现。 + * 与IEC60870_Time的日期转换、CP56编解码分工;不自动接入ASDU或链路单调计时。 + */ +#ifndef IEC60870_CLOCK_H +#define IEC60870_CLOCK_H + +#include "IEC60870_Time.h" + +/* + * 时间源编译选项:0=Windows系统时钟(默认,须在Windows构建),1=RTC预留接口。 + * 这是编译时选择,不在运行期间切换;RTC模式不编译Windows读写代码。 + * 选项须作用于IEC60870_Clock.c的编译,不能只在业务源文件中定义。 + * RTC模式须由板级驱动实现下方的Clock_RTC_Read/Set;本模块不提供芯片驱动。 + * 此选项与IEC60870_TIME_USE_PLATFORM的日期转换算法选择相互独立: + * 前者决定从哪里读取/设置当前时间,后者决定如何将UTC秒数转换为日历。 + * RTC已提供约定的日历时,Clock读写入口不需要时间戳与日历之间的往返转换。 + */ +#ifndef IEC60870_CLOCK_USE_RTC +#define IEC60870_CLOCK_USE_RTC 0 +#endif + +#if IEC60870_CLOCK_USE_RTC != 0 && IEC60870_CLOCK_USE_RTC != 1 +#error "IEC60870_CLOCK_USE_RTC must be 0 (Windows) or 1 (RTC)" +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * 时间源的读取结果。日历数值合法与时间质量分开表示。 + * is_invalid=1仍须提供合法日历;没有可用日历时,平台读时函数应返回0。 + * is_invalid=0仅表示未标记无效,不证明时钟已经校准或与外部时间源同步。 + */ +typedef struct { + IEC60870_WallTime_t time; /* 固定UTC+8标准时间,ms为0..999,星期由公共入口重算 */ + uint8_t is_invalid; /* 0=未标记无效,1=时间无效;可按业务约定传递给CP56的IV */ +} IEC60870_ClockValue_t; + +/* + * 构建:IEC60870_Clock.c + IEC60870_Time.c;Windows读写实现在Clock.c内部。 + * 以后接RTC时,定义IEC60870_CLOCK_USE_RTC=1并加入实现下方RTC接口的板级驱动。 + * 时间源由构建选择,公共接口不需要时钟对象、函数指针绑定或初始化。 + * RTC等硬件仍须由应用按驱动要求初始化;同一时钟的读写由调用方串行执行。 + * 本模块不提供设备锁,不进行动态内存分配。 + */ + +/** + * 功能:从构建时选定的时间源读取固定UTC+8标准时间,并校验日历和质量标志。 + * 输入:value——接收结果的指针,不得为空。 + * 输出:value——合法日历、星期(0=周日..6=周六)及源提供的is_invalid。 + * 返回:1 取得格式合法的结果,is_invalid可能为1;0 参数非法、底层失败或结果非法。 + * 说明:先读取局部变量,检查完成后才提交输出,失败不修改value。 + * 局部结果的is_invalid初始为1,驱动应按实际状态明确填写0或1。 + * 调用方须同时检查返回值和is_invalid,读取成功不等于时钟准确或已同步。 + */ +int IEC60870_Clock_Read(IEC60870_ClockValue_t *value); + +/** + * 功能:将固定UTC+8标准时间交给构建时选定的时间源设置实际时钟。 + * 输入:local_time——非空,1970..2099的合法UTC+8标准时间;输入的weekday可忽略。 + * 返回:1 底层报告设置成功;0 参数非法、平台不支持设时或底层未报告成功。 + * 说明:先检查日历并重算星期,再将副本交给时间源实现,原始输入保持不变。 + * Windows分支转换为UTC后调用系统设时接口;RTC分支直接将日历交给驱动。 + * 本接口不接收IV/SU;校时报文须先通过CP56转换及业务校时条件检查。 + * 不提供硬件写入回滚;失败时设备是否已有部分修改由具体驱动决定。 + */ +int IEC60870_Clock_Set(const IEC60870_WallTime_t *local_time); + +/* + * RTC预留接入接口:仅RTC模式需要由板级驱动实现,Windows模式不引用这两个符号。 + * 业务调用上方公共入口,下面的函数由公共层同步调用,无需注册回调或绑定时钟对象。 + * 不得保留输入/输出指针供返回后使用,也不能反向调用公共读写入口。 + * 本模块仅声明,不提供空实现或默认成功结果;RTC硬件由应用按驱动要求初始化。 + */ + +/** + * 功能:由板级驱动读取RTC日历,供IEC60870_Clock_Read调用。 + * 输入:value——公共层提供的非空局部结果,其is_invalid初始为1。 + * 输出:value——一组一致的固定UTC+8日历和is_invalid=0/1;weekday由公共层重算。 + * 返回:1 取得可供检查的日历;0 读取失败或没有可用日历。 + * 说明:驱动负责寄存器格式转换及读取一致性,避免读到跨秒、跨日拼接的字段。 + * RTC日历的时间基准由设备约定,驱动须保证输出为UTC+8;无毫秒能力时可填写ms=0。 + * 即使日历数值合法,停振、掉电等状态仍可能使时间无效,须按实际状态填写质量。 + */ +int IEC60870_Clock_RTC_Read(IEC60870_ClockValue_t *value); + +/** + * 功能:由板级驱动设置RTC实际时钟,供IEC60870_Clock_Set调用。 + * 输入:local_time——非空,公共层已校验并重算星期的UTC+8日历副本,只在调用期间有效。 + * 返回:1 驱动报告设置成功;0 设置失败或设备不支持设置。 + * 说明:驱动负责将日历转换为芯片要求的寄存器格式,按芯片能力处理毫秒。 + * 只读驱动直接返回0;写入失败时可能已有部分寄存器改变,公共层不提供回滚。 + */ +int IEC60870_Clock_RTC_Set(const IEC60870_WallTime_t *local_time); + +#ifdef __cplusplus +} +#endif + +#endif /* IEC60870_CLOCK_H */ diff --git a/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Codec.c b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Codec.c new file mode 100644 index 00000000..d2ef55f9 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Codec.c @@ -0,0 +1,190 @@ +/* + * 101/104共用的无状态ASDU基础解析:参数校验、公共头及最低载荷长度检查。 + * 只处理调用方提供的字节和参数,不依赖APCI、TCP、串口或连接状态。 + * 具体信息对象的完整校验由IEC60870_Asdu和InformationObjects完成。 + */ +#include "IEC60870_Codec.h" + +#include +#include + +/* ASDU公共头中各组合字段的位掩码。 */ +#define IEC60870_ASDU_OBJECT_COUNT_MASK 0x7FU +#define IEC60870_ASDU_SEQUENCE_MASK 0x80U +#define IEC60870_ASDU_CAUSE_MASK 0x3FU +#define IEC60870_ASDU_NEGATIVE_MASK 0x40U +#define IEC60870_ASDU_TEST_MASK 0x80U +#define IEC60870_ASDU_IOA_LEN 3U + +/* 已知固定长度类型的单个信息对象总长,包含3字节IOA;0表示未在本表检查。 + * 保留IOA使读命令C_RD_NA_1的零数据长度与未知类型可区分。 + * Type ID是线上的协议值;变长文件段F_SG_NA_1及其他未列类型由业务解析。 */ +static const uint8_t asdu_object_lengths[] = { + [1] = IEC60870_ASDU_IOA_LEN + 1U, /* M_SP_NA_1 */ + [2] = IEC60870_ASDU_IOA_LEN + 4U, /* M_SP_TA_1 */ + [3] = IEC60870_ASDU_IOA_LEN + 1U, /* M_DP_NA_1 */ + [4] = IEC60870_ASDU_IOA_LEN + 4U, /* M_DP_TA_1 */ + [5] = IEC60870_ASDU_IOA_LEN + 2U, /* M_ST_NA_1 */ + [6] = IEC60870_ASDU_IOA_LEN + 5U, /* M_ST_TA_1 */ + [7] = IEC60870_ASDU_IOA_LEN + 5U, /* M_BO_NA_1 */ + [8] = IEC60870_ASDU_IOA_LEN + 8U, /* M_BO_TA_1 */ + [9] = IEC60870_ASDU_IOA_LEN + 3U, /* M_ME_NA_1 */ + [10] = IEC60870_ASDU_IOA_LEN + 6U, /* M_ME_TA_1 */ + [11] = IEC60870_ASDU_IOA_LEN + 3U, /* M_ME_NB_1 */ + [12] = IEC60870_ASDU_IOA_LEN + 6U, /* M_ME_TB_1 */ + [13] = IEC60870_ASDU_IOA_LEN + 5U, /* M_ME_NC_1 */ + [14] = IEC60870_ASDU_IOA_LEN + 8U, /* M_ME_TC_1 */ + [15] = IEC60870_ASDU_IOA_LEN + 5U, /* M_IT_NA_1 */ + [16] = IEC60870_ASDU_IOA_LEN + 8U, /* M_IT_TA_1 */ + [17] = IEC60870_ASDU_IOA_LEN + 6U, /* M_EP_TA_1 */ + [18] = IEC60870_ASDU_IOA_LEN + 7U, /* M_EP_TB_1 */ + [19] = IEC60870_ASDU_IOA_LEN + 7U, /* M_EP_TC_1 */ + [20] = IEC60870_ASDU_IOA_LEN + 5U, /* M_PS_NA_1 */ + [21] = IEC60870_ASDU_IOA_LEN + 2U, /* M_ME_ND_1 */ + [30] = IEC60870_ASDU_IOA_LEN + 8U, /* M_SP_TB_1 */ + [31] = IEC60870_ASDU_IOA_LEN + 8U, /* M_DP_TB_1 */ + [32] = IEC60870_ASDU_IOA_LEN + 9U, /* M_ST_TB_1 */ + [33] = IEC60870_ASDU_IOA_LEN + 12U, /* M_BO_TB_1 */ + [34] = IEC60870_ASDU_IOA_LEN + 10U, /* M_ME_TD_1 */ + [35] = IEC60870_ASDU_IOA_LEN + 10U, /* M_ME_TE_1 */ + [36] = IEC60870_ASDU_IOA_LEN + 12U, /* M_ME_TF_1 */ + [37] = IEC60870_ASDU_IOA_LEN + 12U, /* M_IT_TB_1 */ + [38] = IEC60870_ASDU_IOA_LEN + 10U, /* M_EP_TD_1 */ + [39] = IEC60870_ASDU_IOA_LEN + 11U, /* M_EP_TE_1 */ + [40] = IEC60870_ASDU_IOA_LEN + 11U, /* M_EP_TF_1 */ + [41] = IEC60870_ASDU_IOA_LEN + 14U, /* S_IT_TC_1 */ + [45] = IEC60870_ASDU_IOA_LEN + 1U, /* C_SC_NA_1 */ + [46] = IEC60870_ASDU_IOA_LEN + 1U, /* C_DC_NA_1 */ + [47] = IEC60870_ASDU_IOA_LEN + 1U, /* C_RC_NA_1 */ + [48] = IEC60870_ASDU_IOA_LEN + 3U, /* C_SE_NA_1 */ + [49] = IEC60870_ASDU_IOA_LEN + 3U, /* C_SE_NB_1 */ + [50] = IEC60870_ASDU_IOA_LEN + 5U, /* C_SE_NC_1 */ + [51] = IEC60870_ASDU_IOA_LEN + 4U, /* C_BO_NA_1 */ + [58] = IEC60870_ASDU_IOA_LEN + 8U, /* C_SC_TA_1 */ + [59] = IEC60870_ASDU_IOA_LEN + 8U, /* C_DC_TA_1 */ + [60] = IEC60870_ASDU_IOA_LEN + 8U, /* C_RC_TA_1 */ + [61] = IEC60870_ASDU_IOA_LEN + 10U, /* C_SE_TA_1 */ + [62] = IEC60870_ASDU_IOA_LEN + 10U, /* C_SE_TB_1 */ + [63] = IEC60870_ASDU_IOA_LEN + 12U, /* C_SE_TC_1 */ + [64] = IEC60870_ASDU_IOA_LEN + 11U, /* C_BO_TA_1 */ + [70] = IEC60870_ASDU_IOA_LEN + 1U, /* M_EI_NA_1 */ + [100] = IEC60870_ASDU_IOA_LEN + 1U, /* C_IC_NA_1 */ + [101] = IEC60870_ASDU_IOA_LEN + 1U, /* C_CI_NA_1 */ + [102] = IEC60870_ASDU_IOA_LEN, /* C_RD_NA_1 */ + [103] = IEC60870_ASDU_IOA_LEN + 7U, /* C_CS_NA_1 */ + [104] = IEC60870_ASDU_IOA_LEN + 2U, /* C_TS_NA_1 */ + [105] = IEC60870_ASDU_IOA_LEN + 1U, /* C_RP_NA_1 */ + [106] = IEC60870_ASDU_IOA_LEN + 2U, /* C_CD_NA_1 */ + [107] = IEC60870_ASDU_IOA_LEN + 9U, /* C_TS_TA_1 */ + [110] = IEC60870_ASDU_IOA_LEN + 3U, /* P_ME_NA_1 */ + [111] = IEC60870_ASDU_IOA_LEN + 3U, /* P_ME_NB_1 */ + [112] = IEC60870_ASDU_IOA_LEN + 5U, /* P_ME_NC_1 */ + [113] = IEC60870_ASDU_IOA_LEN + 1U, /* P_AC_NA_1 */ + [120] = IEC60870_ASDU_IOA_LEN + 6U, /* F_FR_NA_1 */ + [121] = IEC60870_ASDU_IOA_LEN + 7U, /* F_SR_NA_1 */ + [122] = IEC60870_ASDU_IOA_LEN + 4U, /* F_SC_NA_1 */ + [123] = IEC60870_ASDU_IOA_LEN + 5U, /* F_LS_NA_1 */ + [124] = IEC60870_ASDU_IOA_LEN + 4U, /* F_AF_NA_1 */ + [126] = IEC60870_ASDU_IOA_LEN + 13U, /* F_DR_TA_1 */ + [127] = IEC60870_ASDU_IOA_LEN + 16U /* F_SC_NB_1 */ +}; + +/* 调用前已检查参数、可变公共头及ASDU总长。表中仍以3字节IOA为基准, + * 计算时先减去基准IOA再加上配置宽度。仅检查已知固定长度对象是否被截短; + * 不要求长度恰好相等,不解释对象内容,也不在这里判定零对象或SQ的业务合法性。 */ +static int ASDU_Payload_Length_Valid(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *asdu, uint16_t asdu_len, uint16_t header_length) +{ + uint32_t object_count = asdu[1] & IEC60870_ASDU_OBJECT_COUNT_MASK; + uint32_t object_length; + uint32_t required_length; + + if (object_count == 0U || asdu[0] >= sizeof(asdu_object_lengths)) { + return 1; + } + object_length = asdu_object_lengths[asdu[0]]; + if (object_length == 0U) { + return 1; + } + if ((asdu[1] & IEC60870_ASDU_SEQUENCE_MASK) != 0U) { + /* 连续地址只传首个IOA;非连续地址则每个对象均携带IOA。 */ + required_length = (uint32_t)parameters->sizeOfIOA + + object_count * (object_length - IEC60870_ASDU_IOA_LEN); + } else { + required_length = object_count * + (object_length - IEC60870_ASDU_IOA_LEN + (uint32_t)parameters->sizeOfIOA); + } + return (uint32_t)(asdu_len - header_length) >= required_length; +} + +/** + * 功能:检查101/104共用应用层参数的字段宽度、源发地址和总长上限。 + * 输入参数:parameters——待检查参数,允许NULL。 + * 输出参数:无。 + * 返回值:合法返回1,NULL或非法字段返回0。 + * 说明:先校验小范围字段再计算头长,不选择默认配置或修改参数。 + */ +int IEC60870_AppLayerParameters_Valid(const IEC60870_AppLayerParameters_t *parameters) +{ + return parameters != NULL && parameters->sizeOfTypeId == 1 && + parameters->sizeOfVSQ == 1 && + (parameters->sizeOfCOT == 1 || parameters->sizeOfCOT == 2) && + (parameters->sizeOfCA == 1 || parameters->sizeOfCA == 2) && + parameters->sizeOfIOA >= 1 && parameters->sizeOfIOA <= 3 && + parameters->originatorAddress >= 0 && parameters->originatorAddress <= 255 && + parameters->maxSizeOfASDU >= 2 + parameters->sizeOfCOT + parameters->sizeOfCA && + parameters->maxSizeOfASDU <= IEC60870_MAX_ASDU_LEN; +} + +/** + * 功能:检查ASDU公共头及已知固定长度对象的最低载荷长度,解析公共头。 + * 输入参数:parameters——双方约定的非空应用层参数;asdu/asdu_len——完整ASDU。 + * 输出参数:asdu_info——解析成功时写入公共头字段和信息对象区位置。 + * 返回值:解析成功返回1,参数或长度非法返回0且不修改asdu_info。 + * 说明:按Type ID、VSQ数量及SQ位检查最低载荷长度;未知/变长类型及零对象不做此检查。 + * 不检查多余尾字节、SQ/数量的类型约束、传送原因、地址或信息对象内容是否合法。 + */ +int IEC60870_Parse_ASDU_Header(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *asdu, + uint16_t asdu_len, + IEC60870_AsduInfo_t *asdu_info) +{ + IEC60870_AsduInfo_t result; + uint16_t header_length; + unsigned int ca_index; + + if (asdu == NULL || asdu_info == NULL || !IEC60870_AppLayerParameters_Valid(parameters)) { + return 0; + } + header_length = (uint16_t)(2 + parameters->sizeOfCOT + parameters->sizeOfCA); + if (asdu_len < header_length || asdu_len > parameters->maxSizeOfASDU || + !ASDU_Payload_Length_Valid(parameters, asdu, asdu_len, header_length)) { + return 0; + } + /* COT为1字节时CA紧跟COT,不存在OA;先组装局部结果,再一次性发布。 */ + memset(&result, 0, sizeof(result)); + ca_index = (unsigned int)(2 + parameters->sizeOfCOT); + result.common_address = asdu[ca_index]; + if (parameters->sizeOfCA == 2) { + result.common_address |= (uint16_t)((uint16_t)asdu[ca_index + 1U] << 8); + } + result.type_id = asdu[0]; + result.object_count = + (uint8_t)(asdu[1] & IEC60870_ASDU_OBJECT_COUNT_MASK); + result.is_sequence = + (uint8_t)((asdu[1] & IEC60870_ASDU_SEQUENCE_MASK) != 0U); + result.cause_of_transmission = + (uint8_t)(asdu[2] & IEC60870_ASDU_CAUSE_MASK); + result.is_negative_confirm = + (uint8_t)((asdu[2] & IEC60870_ASDU_NEGATIVE_MASK) != 0U); + result.is_test = + (uint8_t)((asdu[2] & IEC60870_ASDU_TEST_MASK) != 0U); + result.originator_address = parameters->sizeOfCOT == 2 ? asdu[3] : 0; + result.information_objects = &asdu[header_length]; + result.information_objects_len = (uint16_t)(asdu_len - header_length); + result.parameters = parameters; + *asdu_info = result; + + return 1; +} + diff --git a/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Codec.h b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Codec.h new file mode 100644 index 00000000..219fcc8f --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Codec.h @@ -0,0 +1,54 @@ +/* 101/104共用的ASDU公共头解析与参数检查,不包含传输帧接口。 */ +#ifndef IEC60870_CODEC_H +#define IEC60870_CODEC_H + +#include "IEC60870_Types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ASDU公共头的解析结果;本模块不解析具体信息对象,调用方按type_id另行处理。 */ +typedef struct { + uint8_t type_id; /* 类型标识 */ + uint8_t object_count; /* VSQ中的信息对象数量 */ + uint8_t is_sequence; /* VSQ中的SQ标志 */ + uint8_t cause_of_transmission; /* 传送原因,不含标志位 */ + uint8_t is_negative_confirm; /* 传送原因中的P/N标志 */ + uint8_t is_test; /* 传送原因中的TEST标志 */ + uint8_t originator_address; /* 源发地址 */ + uint16_t common_address; /* ASDU公共地址 */ + const uint8_t *information_objects; /* 信息对象区首地址 */ + uint16_t information_objects_len; /* 信息对象区长度 */ + const IEC60870_AppLayerParameters_t *parameters; /* 借用本次解析参数,不可修改。 */ +} IEC60870_AsduInfo_t; + +/** + * 功能:校验应用层字段宽度、默认源发地址及ASDU总长上限。 + * 输入参数:parameters——待检查参数,允许NULL。 + * 输出参数:无。 + * 返回值:合法返回1,NULL或非法值返回0。 + * 说明:不修改参数,不替调用方选择101/104的传输配置。 + */ +extern int IEC60870_AppLayerParameters_Valid(const IEC60870_AppLayerParameters_t *parameters); + +/** + * 功能:检查ASDU公共头及已知固定长度对象的最低载荷长度,解析公共头。 + * 输入参数:parameters——非空应用层参数;asdu——ASDU首地址;asdu_len——总长度。 + * 输出参数:asdu_info——解析成功时写入公共头字段和信息对象区位置。 + * 返回值:解析成功返回1,参数或长度非法返回0且不修改asdu_info。 + * 说明:头长为2+sizeOfCOT+sizeOfCA,IOA及最低载荷长度按sizeOfIOA计算。 + * 未知/变长类型及零对象不做载荷检查;不检查多余尾字节、SQ/数量的类型约束、 + * 传送原因、地址或信息对象内容是否合法。接收器在链路受理前调用本函数。 + * information_objects及parameters均为借用,取对象期间报文和参数必须有效且不变。 + */ +extern int IEC60870_Parse_ASDU_Header(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *asdu, + uint16_t asdu_len, + IEC60870_AsduInfo_t *asdu_info); + +#ifdef __cplusplus +} +#endif + +#endif /* IEC60870_CODEC_H */ diff --git a/IEC60870-Slave/.build/command-v1/before/common/IEC60870_InformationObjects.c b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_InformationObjects.c new file mode 100644 index 00000000..764bfe38 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_InformationObjects.c @@ -0,0 +1,7352 @@ +/* + * 信息对象的具体实现。每个类型分别定义值编解码、只读编码函数表及公开构造/解析入口。 + * 带CP24/CP56的普通监视和控制对象复用基础值处理;保护事件另处理CP16和QDP。 + * 公共入口先在局部对象/字节数组中完成验证,成功后才写回调用方存储。 + */ +#include "IEC60870_InformationObjects_Internal.h" +#include "IEC60870_Codec.h" + +#include +#include +#include + +_Static_assert(CHAR_BIT == 8, "IEC60870 requires 8-bit bytes"); +_Static_assert(sizeof(float) == 4 && FLT_RADIX == 2 && FLT_MANT_DIG == 24 && + FLT_MAX_EXP == 128, "IEC60870 short float requires IEEE 754 binary32"); + +/** + * 功能:按低字节在前读取16位补码值,用于归一化和标度值。 + * 输入参数:data——至少2字节的有效输入;调用方已验证长度。 + * 输出参数:无。 + * 返回值:-32768..32767范围内的int16_t值。 + * 说明:先扩展到int32_t再减65536恢复负值,避免越界无符号到有符号转换。 + */ +static int16_t Read_I16(const uint8_t *data) +{ + uint32_t raw = IEC60870_Object_Read_LE(data, 2); + return (int16_t)(raw < 0x8000U ? (int32_t)raw : (int32_t)raw - 65536); +} + +/** + * 功能:按低字节在前读取32位补码累计值。 + * 输入参数:data——至少4字节的有效输入;调用方已验证长度。 + * 输出参数:无。 + * 返回值:完整int32_t范围内的计数值,包括INT32_MIN。 + * 说明:负数通过低31位减去2^31恢复,每一步运算均在int32_t范围内。 + */ +static int32_t Read_I32(const uint8_t *data) +{ + uint32_t raw = IEC60870_Object_Read_LE(data, 4); + return raw <= INT32_MAX ? (int32_t)raw : + (int32_t)(raw & UINT32_C(0x7FFFFFFF)) - INT32_MAX - 1; +} + +/** + * 功能:将4字节报文恢复为float位模式。 + * 输入参数:data——至少4字节的有效输入,低字节在前。 + * 输出参数:value——非空float对象,保存解码后的IEEE 754 binary32位模式。 + * 返回值:无。 + * 说明:memcpy避免未对齐访问和指针别名问题,不进行浮点运算或NaN/Inf过滤。 + */ +static void Read_Float(const uint8_t *data, float *value) +{ + uint32_t bits = IEC60870_Object_Read_LE(data, 4); + memcpy(value, &bits, sizeof(bits)); +} + +/** + * 功能:将float位模式编码为低字节在前的4字节载荷。 + * 输入参数:value——非空float指针,按位复制,不作浮点到整数的数值转换。 + * 输出参数:data——至少4字节的可写空间。 + * 返回值:无。 + * 说明:先用memcpy取得uint32_t位表示,再按协议字节序写入。 + */ +static void Write_Float(uint8_t *data, const float *value) +{ + uint32_t bits; + memcpy(&bits, value, sizeof(bits)); + IEC60870_Object_Write_LE(data, bits, 4); +} + +/** + * 功能:初始化具体对象中的公共基类身份。 + * 输入参数:ioa——已验证为0..0xFFFFFF的信息对象地址。 + * vtable——与具体结构匹配的非空静态只读函数表。 + * 输出参数:object——非空公共基类首成员;此函数只填写地址、类型和函数表。 + * 返回值:无。 + * 说明:对象仅借用永久有效的函数表;不分配内存,不保留请求或连接状态。 + */ +static void Initialize_Object(IEC60870_InformationObject_t *object, uint32_t ioa, + const IEC60870_InformationObjectVTable *vtable) +{ + object->ioa = ioa; + object->type_id = vtable->type_id; + object->virtual_function_table = vtable; +} + +/* ---- Type 1:单点信息 ---- */ +/** + * 功能:编码Type 1(单点信息)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少1字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:SIQ(1):bit0为单点状态,bit1..3为保留位,高4位为品质。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int SinglePointInformation_Encode_Value(const IEC60870_SinglePointInformation_t *self, uint8_t *payload) +{ + /* SIQ/DIQ仅允许高4位品质,点状态分别占1/2位。 */ + if (self->value > 1U || (self->quality & 0x0FU) != 0U) { return 0; } + payload[0] = (uint8_t)(self->value | self->quality); + return 1; +} + +/** + * 功能:解码Type 1(单点信息)的值字段。 + * 输入参数:payload——已确认有1字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:SIQ(1):bit0为单点状态,bit1..3为保留位,高4位为品质。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int SinglePointInformation_Decode_Value(const uint8_t *payload, IEC60870_SinglePointInformation_t *self) +{ + if ((payload[0] & 0x0EU) != 0U) { return 0; } + self->value = payload[0] & 1U; + self->quality = payload[0] & 0xF0U; + return 1; +} + +/** + * 功能:Type 1编码函数表的适配入口,将公共基类还原为SinglePointInformation对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少1字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int SinglePointInformation_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return SinglePointInformation_Encode_Value((const IEC60870_SinglePointInformation_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable SinglePointInformation_VTable = { + 1, 1, 0, sizeof(IEC60870_SinglePointInformation_t), SinglePointInformation_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 1(单点信息)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——单点状态,0或1。 + * quality——品质仅允许BL/SB/NT/IV(0xF0范围内的位),不得夹带状态或保留位。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * 仅创建内存对象;加入ASDU时才按应用层参数编码IOA并检查报文容量。 + */ +IEC60870_AsduResult_t IEC60870_SinglePointInformation_Create(IEC60870_SinglePointInformation_t *self, + uint32_t ioa, int value, int quality) +{ + IEC60870_SinglePointInformation_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + value < 0 || value > 1 || + quality < 0 || quality > 255) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.value = (uint8_t)value; + result.quality = (uint8_t)quality; + Initialize_Object(&result.object, ioa, &SinglePointInformation_VTable); + if (!SinglePointInformation_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 1(单点信息)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的1字节载荷,必须非空。 + * length——必须恰好为1;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_SinglePointInformation_Get_From_Buffer(IEC60870_SinglePointInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_SinglePointInformation_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 1U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!SinglePointInformation_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &SinglePointInformation_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 3:双点信息 ---- */ +/** + * 功能:编码Type 3(双点信息)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少1字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:DIQ(1):低2位为双点状态,bit2..3为保留位,高4位为品质。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int DoublePointInformation_Encode_Value(const IEC60870_DoublePointInformation_t *self, uint8_t *payload) +{ + /* SIQ/DIQ仅允许高4位品质,点状态分别占1/2位。 */ + if (self->value > 3U || (self->quality & 0x0FU) != 0U) { return 0; } + payload[0] = (uint8_t)(self->value | self->quality); + return 1; +} + +/** + * 功能:解码Type 3(双点信息)的值字段。 + * 输入参数:payload——已确认有1字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:DIQ(1):低2位为双点状态,bit2..3为保留位,高4位为品质。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int DoublePointInformation_Decode_Value(const uint8_t *payload, IEC60870_DoublePointInformation_t *self) +{ + if ((payload[0] & 0x0CU) != 0U) { return 0; } + self->value = payload[0] & 3U; + self->quality = payload[0] & 0xF0U; + return 1; +} + +/** + * 功能:Type 3编码函数表的适配入口,将公共基类还原为DoublePointInformation对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少1字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int DoublePointInformation_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return DoublePointInformation_Encode_Value((const IEC60870_DoublePointInformation_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable DoublePointInformation_VTable = { + 3, 1, 0, sizeof(IEC60870_DoublePointInformation_t), DoublePointInformation_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 3(双点信息)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——双点状态,0..3,包含中间状态和不确定状态。 + * quality——品质仅允许BL/SB/NT/IV(0xF0范围内的位),不得夹带状态或保留位。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * 仅创建内存对象;加入ASDU时才按应用层参数编码IOA并检查报文容量。 + */ +IEC60870_AsduResult_t IEC60870_DoublePointInformation_Create(IEC60870_DoublePointInformation_t *self, + uint32_t ioa, int value, int quality) +{ + IEC60870_DoublePointInformation_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + value < 0 || value > 3 || + quality < 0 || quality > 255) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.value = (uint8_t)value; + result.quality = (uint8_t)quality; + Initialize_Object(&result.object, ioa, &DoublePointInformation_VTable); + if (!DoublePointInformation_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 3(双点信息)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的1字节载荷,必须非空。 + * length——必须恰好为1;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_DoublePointInformation_Get_From_Buffer(IEC60870_DoublePointInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_DoublePointInformation_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 1U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!DoublePointInformation_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &DoublePointInformation_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 9:归一化测量值(原始16位定点值) ---- */ +/** + * 功能:编码Type 9(归一化测量值(原始16位定点值))的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少3字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:NVA(2)+QDS(1):有符号16位归一化原始定点值,随后为品质。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int MeasuredValueNormalized_Encode_Value(const IEC60870_MeasuredValueNormalized_t *self, uint8_t *payload) +{ + /* QDS的bit1..3为保留位;bit0为溢出,高4位为品质。 */ + if ((self->quality & 0x0EU) != 0U) { return 0; } + IEC60870_Object_Write_LE(payload, (uint16_t)self->value, 2); + payload[2] = self->quality; + return 1; +} + +/** + * 功能:解码Type 9(归一化测量值(原始16位定点值))的值字段。 + * 输入参数:payload——已确认有3字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:NVA(2)+QDS(1):有符号16位归一化原始定点值,随后为品质。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int MeasuredValueNormalized_Decode_Value(const uint8_t *payload, IEC60870_MeasuredValueNormalized_t *self) +{ + if ((payload[2] & 0x0EU) != 0U) { return 0; } + self->value = Read_I16(payload); + self->quality = payload[2]; + return 1; +} + +/** + * 功能:Type 9编码函数表的适配入口,将公共基类还原为MeasuredValueNormalized对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少3字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int MeasuredValueNormalized_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return MeasuredValueNormalized_Encode_Value((const IEC60870_MeasuredValueNormalized_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable MeasuredValueNormalized_VTable = { + 9, 3, 0, sizeof(IEC60870_MeasuredValueNormalized_t), MeasuredValueNormalized_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 9(归一化测量值(原始16位定点值))。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——归一化原始int16_t定点值,实际数值为value/32768.0。 + * quality——QDS品质仅允许OV/BL/SB/NT/IV(0xF1范围内的位),bit1..3必须为0。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * 仅创建内存对象;加入ASDU时才按应用层参数编码IOA并检查报文容量。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalized_Create(IEC60870_MeasuredValueNormalized_t *self, + uint32_t ioa, int16_t value, int quality) +{ + IEC60870_MeasuredValueNormalized_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + quality < 0 || quality > 255) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.value = value; + result.quality = (uint8_t)quality; + Initialize_Object(&result.object, ioa, &MeasuredValueNormalized_VTable); + if (!MeasuredValueNormalized_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 9(归一化测量值(原始16位定点值))。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的3字节载荷,必须非空。 + * length——必须恰好为3;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalized_Get_From_Buffer(IEC60870_MeasuredValueNormalized_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_MeasuredValueNormalized_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 3U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!MeasuredValueNormalized_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &MeasuredValueNormalized_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 11:标度化测量值 ---- */ +/** + * 功能:编码Type 11(标度化测量值)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少3字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:SVA(2)+QDS(1):有符号16位标度值,随后为品质。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int MeasuredValueScaled_Encode_Value(const IEC60870_MeasuredValueScaled_t *self, uint8_t *payload) +{ + /* QDS的bit1..3为保留位;bit0为溢出,高4位为品质。 */ + if ((self->quality & 0x0EU) != 0U) { return 0; } + IEC60870_Object_Write_LE(payload, (uint16_t)self->value, 2); + payload[2] = self->quality; + return 1; +} + +/** + * 功能:解码Type 11(标度化测量值)的值字段。 + * 输入参数:payload——已确认有3字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:SVA(2)+QDS(1):有符号16位标度值,随后为品质。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int MeasuredValueScaled_Decode_Value(const uint8_t *payload, IEC60870_MeasuredValueScaled_t *self) +{ + if ((payload[2] & 0x0EU) != 0U) { return 0; } + self->value = Read_I16(payload); + self->quality = payload[2]; + return 1; +} + +/** + * 功能:Type 11编码函数表的适配入口,将公共基类还原为MeasuredValueScaled对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少3字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int MeasuredValueScaled_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return MeasuredValueScaled_Encode_Value((const IEC60870_MeasuredValueScaled_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable MeasuredValueScaled_VTable = { + 11, 3, 0, sizeof(IEC60870_MeasuredValueScaled_t), MeasuredValueScaled_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 11(标度化测量值)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——有符号16位标度值,范围-32768..32767。 + * quality——QDS品质仅允许OV/BL/SB/NT/IV(0xF1范围内的位),bit1..3必须为0。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * 仅创建内存对象;加入ASDU时才按应用层参数编码IOA并检查报文容量。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueScaled_Create(IEC60870_MeasuredValueScaled_t *self, + uint32_t ioa, int16_t value, int quality) +{ + IEC60870_MeasuredValueScaled_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + quality < 0 || quality > 255) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.value = value; + result.quality = (uint8_t)quality; + Initialize_Object(&result.object, ioa, &MeasuredValueScaled_VTable); + if (!MeasuredValueScaled_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 11(标度化测量值)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的3字节载荷,必须非空。 + * length——必须恰好为3;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueScaled_Get_From_Buffer(IEC60870_MeasuredValueScaled_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_MeasuredValueScaled_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 3U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!MeasuredValueScaled_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &MeasuredValueScaled_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 13:短浮点测量值 ---- */ +/** + * 功能:编码Type 13(短浮点测量值)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少5字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:短浮点(4)+QDS(1):IEEE 754 binary32位模式,随后为品质。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int MeasuredValueShort_Encode_Value(const IEC60870_MeasuredValueShort_t *self, uint8_t *payload) +{ + /* QDS的bit1..3为保留位;bit0为溢出,高4位为品质。 */ + if ((self->quality & 0x0EU) != 0U) { return 0; } + Write_Float(payload, &self->value); + payload[4] = self->quality; + return 1; +} + +/** + * 功能:解码Type 13(短浮点测量值)的值字段。 + * 输入参数:payload——已确认有5字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:短浮点(4)+QDS(1):IEEE 754 binary32位模式,随后为品质。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int MeasuredValueShort_Decode_Value(const uint8_t *payload, IEC60870_MeasuredValueShort_t *self) +{ + if ((payload[4] & 0x0EU) != 0U) { return 0; } + Read_Float(payload, &self->value); + self->quality = payload[4]; + return 1; +} + +/** + * 功能:Type 13编码函数表的适配入口,将公共基类还原为MeasuredValueShort对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少5字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int MeasuredValueShort_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return MeasuredValueShort_Encode_Value((const IEC60870_MeasuredValueShort_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable MeasuredValueShort_VTable = { + 13, 5, 0, sizeof(IEC60870_MeasuredValueShort_t), MeasuredValueShort_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 13(短浮点测量值)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——短浮点值,采用IEEE 754 binary32,不进行NaN/Inf业务过滤。 + * quality——QDS品质仅允许OV/BL/SB/NT/IV(0xF1范围内的位),bit1..3必须为0。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * 仅创建内存对象;加入ASDU时才按应用层参数编码IOA并检查报文容量。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueShort_Create(IEC60870_MeasuredValueShort_t *self, + uint32_t ioa, float value, int quality) +{ + IEC60870_MeasuredValueShort_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + quality < 0 || quality > 255) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.value = value; + result.quality = (uint8_t)quality; + Initialize_Object(&result.object, ioa, &MeasuredValueShort_VTable); + if (!MeasuredValueShort_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 13(短浮点测量值)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的5字节载荷,必须非空。 + * length——必须恰好为5;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueShort_Get_From_Buffer(IEC60870_MeasuredValueShort_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_MeasuredValueShort_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 5U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!MeasuredValueShort_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &MeasuredValueShort_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 15:累计量 ---- */ +/** + * 功能:编码Type 15(累计量)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少5字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:BCR(5):前4字节为有符号累计值,末字节为5位序号及CY/CA/IV。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int IntegratedTotals_Encode_Value(const IEC60870_IntegratedTotals_t *self, uint8_t *payload) +{ + const IEC60870_BinaryCounterReading_t *value = &self->value; + if (value->sequence_number > 31U || value->has_carry > 1U || + value->is_adjusted > 1U || value->is_invalid > 1U) { return 0; } + IEC60870_Object_Write_LE(payload, (uint32_t)value->value, 4); + /* BCR末字节:低5位序号、bit5进位、bit6调整、bit7无效。 */ + payload[4] = (uint8_t)(value->sequence_number | (value->has_carry << 5) | + (value->is_adjusted << 6) | (value->is_invalid << 7)); + return 1; +} + +/** + * 功能:解码Type 15(累计量)的值字段。 + * 输入参数:payload——已确认有5字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:BCR(5):前4字节为有符号累计值,末字节为5位序号及CY/CA/IV。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int IntegratedTotals_Decode_Value(const uint8_t *payload, IEC60870_IntegratedTotals_t *self) +{ + self->value.value = Read_I32(payload); + self->value.sequence_number = payload[4] & 31U; + self->value.has_carry = (payload[4] >> 5) & 1U; + self->value.is_adjusted = (payload[4] >> 6) & 1U; + self->value.is_invalid = payload[4] >> 7; + return 1; +} + +/** + * 功能:Type 15编码函数表的适配入口,将公共基类还原为IntegratedTotals对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少5字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int IntegratedTotals_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return IntegratedTotals_Encode_Value((const IEC60870_IntegratedTotals_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable IntegratedTotals_VTable = { + 15, 5, 0, sizeof(IEC60870_IntegratedTotals_t), IntegratedTotals_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 15(累计量)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——非空BCR指针;计数为int32_t,序号0..31,CY/CA/IV均为0/1。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * BCR/CP56输入按值复制,返回后调用方可以复用输入;构造成功不表示命令已执行。 + */ +IEC60870_AsduResult_t IEC60870_IntegratedTotals_Create(IEC60870_IntegratedTotals_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value) +{ + IEC60870_IntegratedTotals_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + value == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.value = *value; + Initialize_Object(&result.object, ioa, &IntegratedTotals_VTable); + if (!IntegratedTotals_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 15(累计量)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的5字节载荷,必须非空。 + * length——必须恰好为5;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_IntegratedTotals_Get_From_Buffer(IEC60870_IntegratedTotals_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_IntegratedTotals_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 5U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!IntegratedTotals_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &IntegratedTotals_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 30:带时标单点信息 ---- */ +/** + * 功能:编码Type 30(带时标单点信息)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少8字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:SIQ(1):bit0为单点状态,bit1..3为保留位,高4位为品质,之后追加CP56Time2a(7)。 + * 值部分调用SinglePointInformation_Encode_Value,成功后追加7字节CP56。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int SinglePointWithCP56Time2a_Encode_Value(const IEC60870_SinglePointWithCP56Time2a_t *self, uint8_t *payload) +{ + return SinglePointInformation_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP56(&self->time, payload + 1, IEC60870_CP56_LEN); +} + +/** + * 功能:解码Type 30(带时标单点信息)的值字段和CP56时标。 + * 输入参数:payload——已确认有8字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:SIQ(1):bit0为单点状态,bit1..3为保留位,高4位为品质,之后追加CP56Time2a(7)。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int SinglePointWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_SinglePointWithCP56Time2a_t *self) +{ + return SinglePointInformation_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP56(payload + 1, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:Type 30编码函数表的适配入口,将公共基类还原为SinglePointWithCP56Time2a对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少8字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int SinglePointWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return SinglePointWithCP56Time2a_Encode_Value((const IEC60870_SinglePointWithCP56Time2a_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable SinglePointWithCP56Time2a_VTable = { + 30, 8, 0, sizeof(IEC60870_SinglePointWithCP56Time2a_t), SinglePointWithCP56Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 30(带时标单点信息)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——单点状态,0或1。 + * quality——品质仅允许BL/SB/NT/IV(0xF0范围内的位),不得夹带状态或保留位。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * BCR/CP56输入按值复制,返回后调用方可以复用输入;构造成功不表示命令已执行。 + */ +IEC60870_AsduResult_t IEC60870_SinglePointWithCP56Time2a_Create(IEC60870_SinglePointWithCP56Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_SinglePointWithCP56Time2a_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + value < 0 || value > 1 || + quality < 0 || quality > 255 || + time == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.base.value = (uint8_t)value; + result.base.quality = (uint8_t)quality; + result.time = *time; + Initialize_Object(&result.base.object, ioa, &SinglePointWithCP56Time2a_VTable); + if (!SinglePointWithCP56Time2a_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 30(带时标单点信息)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的8字节载荷,必须非空。 + * length——必须恰好为8;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_SinglePointWithCP56Time2a_Get_From_Buffer(IEC60870_SinglePointWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_SinglePointWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 8U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!SinglePointWithCP56Time2a_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.base.object, ioa, &SinglePointWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 31:带时标双点信息 ---- */ +/** + * 功能:编码Type 31(带时标双点信息)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少8字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:DIQ(1):低2位为双点状态,bit2..3为保留位,高4位为品质,之后追加CP56Time2a(7)。 + * 值部分调用DoublePointInformation_Encode_Value,成功后追加7字节CP56。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int DoublePointWithCP56Time2a_Encode_Value(const IEC60870_DoublePointWithCP56Time2a_t *self, uint8_t *payload) +{ + return DoublePointInformation_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP56(&self->time, payload + 1, IEC60870_CP56_LEN); +} + +/** + * 功能:解码Type 31(带时标双点信息)的值字段和CP56时标。 + * 输入参数:payload——已确认有8字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:DIQ(1):低2位为双点状态,bit2..3为保留位,高4位为品质,之后追加CP56Time2a(7)。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int DoublePointWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_DoublePointWithCP56Time2a_t *self) +{ + return DoublePointInformation_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP56(payload + 1, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:Type 31编码函数表的适配入口,将公共基类还原为DoublePointWithCP56Time2a对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少8字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int DoublePointWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return DoublePointWithCP56Time2a_Encode_Value((const IEC60870_DoublePointWithCP56Time2a_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable DoublePointWithCP56Time2a_VTable = { + 31, 8, 0, sizeof(IEC60870_DoublePointWithCP56Time2a_t), DoublePointWithCP56Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 31(带时标双点信息)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——双点状态,0..3,包含中间状态和不确定状态。 + * quality——品质仅允许BL/SB/NT/IV(0xF0范围内的位),不得夹带状态或保留位。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * BCR/CP56输入按值复制,返回后调用方可以复用输入;构造成功不表示命令已执行。 + */ +IEC60870_AsduResult_t IEC60870_DoublePointWithCP56Time2a_Create(IEC60870_DoublePointWithCP56Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_DoublePointWithCP56Time2a_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + value < 0 || value > 3 || + quality < 0 || quality > 255 || + time == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.base.value = (uint8_t)value; + result.base.quality = (uint8_t)quality; + result.time = *time; + Initialize_Object(&result.base.object, ioa, &DoublePointWithCP56Time2a_VTable); + if (!DoublePointWithCP56Time2a_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 31(带时标双点信息)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的8字节载荷,必须非空。 + * length——必须恰好为8;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_DoublePointWithCP56Time2a_Get_From_Buffer(IEC60870_DoublePointWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_DoublePointWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 8U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!DoublePointWithCP56Time2a_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.base.object, ioa, &DoublePointWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 34:带时标归一化测量值 ---- */ +/** + * 功能:编码Type 34(带时标归一化测量值)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少10字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:NVA(2)+QDS(1):有符号16位归一化原始定点值,随后为品质,之后追加CP56Time2a(7)。 + * 值部分调用MeasuredValueNormalized_Encode_Value,成功后追加7字节CP56。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int MeasuredValueNormalizedWithCP56Time2a_Encode_Value(const IEC60870_MeasuredValueNormalizedWithCP56Time2a_t *self, uint8_t *payload) +{ + return MeasuredValueNormalized_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP56(&self->time, payload + 3, IEC60870_CP56_LEN); +} + +/** + * 功能:解码Type 34(带时标归一化测量值)的值字段和CP56时标。 + * 输入参数:payload——已确认有10字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:NVA(2)+QDS(1):有符号16位归一化原始定点值,随后为品质,之后追加CP56Time2a(7)。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int MeasuredValueNormalizedWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_MeasuredValueNormalizedWithCP56Time2a_t *self) +{ + return MeasuredValueNormalized_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP56(payload + 3, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:Type 34编码函数表的适配入口,将公共基类还原为MeasuredValueNormalizedWithCP56Time2a对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少10字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int MeasuredValueNormalizedWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return MeasuredValueNormalizedWithCP56Time2a_Encode_Value((const IEC60870_MeasuredValueNormalizedWithCP56Time2a_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable MeasuredValueNormalizedWithCP56Time2a_VTable = { + 34, 10, 0, sizeof(IEC60870_MeasuredValueNormalizedWithCP56Time2a_t), MeasuredValueNormalizedWithCP56Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 34(带时标归一化测量值)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——归一化原始int16_t定点值,实际数值为value/32768.0。 + * quality——QDS品质仅允许OV/BL/SB/NT/IV(0xF1范围内的位),bit1..3必须为0。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * BCR/CP56输入按值复制,返回后调用方可以复用输入;构造成功不表示命令已执行。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP56Time2a_Create(IEC60870_MeasuredValueNormalizedWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_MeasuredValueNormalizedWithCP56Time2a_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + quality < 0 || quality > 255 || + time == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.base.value = value; + result.base.quality = (uint8_t)quality; + result.time = *time; + Initialize_Object(&result.base.object, ioa, &MeasuredValueNormalizedWithCP56Time2a_VTable); + if (!MeasuredValueNormalizedWithCP56Time2a_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 34(带时标归一化测量值)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的10字节载荷,必须非空。 + * length——必须恰好为10;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_MeasuredValueNormalizedWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 10U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!MeasuredValueNormalizedWithCP56Time2a_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.base.object, ioa, &MeasuredValueNormalizedWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 35:带时标标度化测量值 ---- */ +/** + * 功能:编码Type 35(带时标标度化测量值)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少10字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:SVA(2)+QDS(1):有符号16位标度值,随后为品质,之后追加CP56Time2a(7)。 + * 值部分调用MeasuredValueScaled_Encode_Value,成功后追加7字节CP56。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int MeasuredValueScaledWithCP56Time2a_Encode_Value(const IEC60870_MeasuredValueScaledWithCP56Time2a_t *self, uint8_t *payload) +{ + return MeasuredValueScaled_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP56(&self->time, payload + 3, IEC60870_CP56_LEN); +} + +/** + * 功能:解码Type 35(带时标标度化测量值)的值字段和CP56时标。 + * 输入参数:payload——已确认有10字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:SVA(2)+QDS(1):有符号16位标度值,随后为品质,之后追加CP56Time2a(7)。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int MeasuredValueScaledWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_MeasuredValueScaledWithCP56Time2a_t *self) +{ + return MeasuredValueScaled_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP56(payload + 3, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:Type 35编码函数表的适配入口,将公共基类还原为MeasuredValueScaledWithCP56Time2a对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少10字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int MeasuredValueScaledWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return MeasuredValueScaledWithCP56Time2a_Encode_Value((const IEC60870_MeasuredValueScaledWithCP56Time2a_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable MeasuredValueScaledWithCP56Time2a_VTable = { + 35, 10, 0, sizeof(IEC60870_MeasuredValueScaledWithCP56Time2a_t), MeasuredValueScaledWithCP56Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 35(带时标标度化测量值)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——有符号16位标度值,范围-32768..32767。 + * quality——QDS品质仅允许OV/BL/SB/NT/IV(0xF1范围内的位),bit1..3必须为0。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * BCR/CP56输入按值复制,返回后调用方可以复用输入;构造成功不表示命令已执行。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP56Time2a_Create(IEC60870_MeasuredValueScaledWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_MeasuredValueScaledWithCP56Time2a_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + quality < 0 || quality > 255 || + time == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.base.value = value; + result.base.quality = (uint8_t)quality; + result.time = *time; + Initialize_Object(&result.base.object, ioa, &MeasuredValueScaledWithCP56Time2a_VTable); + if (!MeasuredValueScaledWithCP56Time2a_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 35(带时标标度化测量值)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的10字节载荷,必须非空。 + * length——必须恰好为10;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueScaledWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_MeasuredValueScaledWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 10U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!MeasuredValueScaledWithCP56Time2a_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.base.object, ioa, &MeasuredValueScaledWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 36:带时标短浮点测量值 ---- */ +/** + * 功能:编码Type 36(带时标短浮点测量值)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少12字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:短浮点(4)+QDS(1):IEEE 754 binary32位模式,随后为品质,之后追加CP56Time2a(7)。 + * 值部分调用MeasuredValueShort_Encode_Value,成功后追加7字节CP56。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int MeasuredValueShortWithCP56Time2a_Encode_Value(const IEC60870_MeasuredValueShortWithCP56Time2a_t *self, uint8_t *payload) +{ + return MeasuredValueShort_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP56(&self->time, payload + 5, IEC60870_CP56_LEN); +} + +/** + * 功能:解码Type 36(带时标短浮点测量值)的值字段和CP56时标。 + * 输入参数:payload——已确认有12字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:短浮点(4)+QDS(1):IEEE 754 binary32位模式,随后为品质,之后追加CP56Time2a(7)。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int MeasuredValueShortWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_MeasuredValueShortWithCP56Time2a_t *self) +{ + return MeasuredValueShort_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP56(payload + 5, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:Type 36编码函数表的适配入口,将公共基类还原为MeasuredValueShortWithCP56Time2a对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少12字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int MeasuredValueShortWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return MeasuredValueShortWithCP56Time2a_Encode_Value((const IEC60870_MeasuredValueShortWithCP56Time2a_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable MeasuredValueShortWithCP56Time2a_VTable = { + 36, 12, 0, sizeof(IEC60870_MeasuredValueShortWithCP56Time2a_t), MeasuredValueShortWithCP56Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 36(带时标短浮点测量值)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——短浮点值,采用IEEE 754 binary32,不进行NaN/Inf业务过滤。 + * quality——QDS品质仅允许OV/BL/SB/NT/IV(0xF1范围内的位),bit1..3必须为0。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * BCR/CP56输入按值复制,返回后调用方可以复用输入;构造成功不表示命令已执行。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP56Time2a_Create(IEC60870_MeasuredValueShortWithCP56Time2a_t *self, + uint32_t ioa, float value, int quality, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_MeasuredValueShortWithCP56Time2a_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + quality < 0 || quality > 255 || + time == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.base.value = value; + result.base.quality = (uint8_t)quality; + result.time = *time; + Initialize_Object(&result.base.object, ioa, &MeasuredValueShortWithCP56Time2a_VTable); + if (!MeasuredValueShortWithCP56Time2a_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 36(带时标短浮点测量值)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的12字节载荷,必须非空。 + * length——必须恰好为12;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueShortWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_MeasuredValueShortWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 12U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!MeasuredValueShortWithCP56Time2a_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.base.object, ioa, &MeasuredValueShortWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 37:带时标累计量 ---- */ +/** + * 功能:编码Type 37(带时标累计量)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少12字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:BCR(5):前4字节为有符号累计值,末字节为5位序号及CY/CA/IV,之后追加CP56Time2a(7)。 + * 值部分调用IntegratedTotals_Encode_Value,成功后追加7字节CP56。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int IntegratedTotalsWithCP56Time2a_Encode_Value(const IEC60870_IntegratedTotalsWithCP56Time2a_t *self, uint8_t *payload) +{ + return IntegratedTotals_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP56(&self->time, payload + 5, IEC60870_CP56_LEN); +} + +/** + * 功能:解码Type 37(带时标累计量)的值字段和CP56时标。 + * 输入参数:payload——已确认有12字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:BCR(5):前4字节为有符号累计值,末字节为5位序号及CY/CA/IV,之后追加CP56Time2a(7)。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int IntegratedTotalsWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_IntegratedTotalsWithCP56Time2a_t *self) +{ + return IntegratedTotals_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP56(payload + 5, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:Type 37编码函数表的适配入口,将公共基类还原为IntegratedTotalsWithCP56Time2a对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少12字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int IntegratedTotalsWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return IntegratedTotalsWithCP56Time2a_Encode_Value((const IEC60870_IntegratedTotalsWithCP56Time2a_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable IntegratedTotalsWithCP56Time2a_VTable = { + 37, 12, 0, sizeof(IEC60870_IntegratedTotalsWithCP56Time2a_t), IntegratedTotalsWithCP56Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 37(带时标累计量)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——非空BCR指针;计数为int32_t,序号0..31,CY/CA/IV均为0/1。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * BCR/CP56输入按值复制,返回后调用方可以复用输入;构造成功不表示命令已执行。 + */ +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP56Time2a_Create(IEC60870_IntegratedTotalsWithCP56Time2a_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_IntegratedTotalsWithCP56Time2a_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + value == NULL || + time == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.base.value = *value; + result.time = *time; + Initialize_Object(&result.base.object, ioa, &IntegratedTotalsWithCP56Time2a_VTable); + if (!IntegratedTotalsWithCP56Time2a_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 37(带时标累计量)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的12字节载荷,必须非空。 + * length——必须恰好为12;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP56Time2a_Get_From_Buffer(IEC60870_IntegratedTotalsWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_IntegratedTotalsWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 12U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!IntegratedTotalsWithCP56Time2a_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.base.object, ioa, &IntegratedTotalsWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 45:单命令 ---- */ +/** + * 功能:编码Type 45(单命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少1字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:SCO(1):bit0为状态,bit1保留,bit2..6为QU,bit7为S/E。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int SingleCommand_Encode_Value(const IEC60870_SingleCommand_t *self, uint8_t *payload) +{ + /* SCO的bit1必须为0;单命令状态为0/1;QU占bit2..6,S/E占bit7。 */ + if (self->state > 1U || + self->is_select > 1U || self->qualifier > 31U) { return 0; } + payload[0] = (uint8_t)(self->state | (self->qualifier << 2) | (self->is_select << 7)); + return 1; +} + +/** + * 功能:解码Type 45(单命令)的值字段。 + * 输入参数:payload——已确认有1字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:SCO(1):bit0为状态,bit1保留,bit2..6为QU,bit7为S/E。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int SingleCommand_Decode_Value(const uint8_t *payload, IEC60870_SingleCommand_t *self) +{ + if ((payload[0] & 2U) != 0U) { return 0; } + self->state = payload[0] & 1U; + self->is_select = payload[0] >> 7; + self->qualifier = (payload[0] >> 2) & 31U; + return 1; +} + +/** + * 功能:Type 45编码函数表的适配入口,将公共基类还原为SingleCommand对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少1字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int SingleCommand_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return SingleCommand_Encode_Value((const IEC60870_SingleCommand_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable SingleCommand_VTable = { + 45, 1, 1, sizeof(IEC60870_SingleCommand_t), SingleCommand_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 45(单命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * state——单命令状态,0或1;SCO保留位由编码器保持为0。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——命令限定词QU,0..31;具体业务含义由应用判断。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * 仅创建内存对象;加入ASDU时才按应用层参数编码IOA并检查报文容量。 + */ +IEC60870_AsduResult_t IEC60870_SingleCommand_Create(IEC60870_SingleCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier) +{ + IEC60870_SingleCommand_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + state < 0 || state > 1 || + is_select < 0 || is_select > 1 || + qualifier < 0 || qualifier > 31) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.state = (uint8_t)state; + result.is_select = (uint8_t)is_select; + result.qualifier = (uint8_t)qualifier; + Initialize_Object(&result.object, ioa, &SingleCommand_VTable); + if (!SingleCommand_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 45(单命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的1字节载荷,必须非空。 + * length——必须恰好为1;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_SingleCommand_Get_From_Buffer(IEC60870_SingleCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_SingleCommand_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 1U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!SingleCommand_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &SingleCommand_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 46:双命令 ---- */ +/** + * 功能:编码Type 46(双命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少1字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:DCO(1):低2位为状态(仅1/2),bit2..6为QU,bit7为S/E。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int DoubleCommand_Encode_Value(const IEC60870_DoubleCommand_t *self, uint8_t *payload) +{ + /* DCO/RCO仅接收状态1/2;QU占bit2..6,S/E占bit7。 */ + if ((self->state != 1U && self->state != 2U) || + self->is_select > 1U || self->qualifier > 31U) { return 0; } + payload[0] = (uint8_t)(self->state | (self->qualifier << 2) | (self->is_select << 7)); + return 1; +} + +/** + * 功能:解码Type 46(双命令)的值字段。 + * 输入参数:payload——已确认有1字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:DCO(1):低2位为状态(仅1/2),bit2..6为QU,bit7为S/E。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int DoubleCommand_Decode_Value(const uint8_t *payload, IEC60870_DoubleCommand_t *self) +{ + self->state = payload[0] & 3U; + if (self->state != 1U && self->state != 2U) { return 0; } + self->is_select = payload[0] >> 7; + self->qualifier = (payload[0] >> 2) & 31U; + return 1; +} + +/** + * 功能:Type 46编码函数表的适配入口,将公共基类还原为DoubleCommand对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少1字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int DoubleCommand_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return DoubleCommand_Encode_Value((const IEC60870_DoubleCommand_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable DoubleCommand_VTable = { + 46, 1, 1, sizeof(IEC60870_DoubleCommand_t), DoubleCommand_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 46(双命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * state——双命令/步调节状态,仅允许1或2。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——命令限定词QU,0..31;具体业务含义由应用判断。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * 仅创建内存对象;加入ASDU时才按应用层参数编码IOA并检查报文容量。 + */ +IEC60870_AsduResult_t IEC60870_DoubleCommand_Create(IEC60870_DoubleCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier) +{ + IEC60870_DoubleCommand_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + state < 1 || state > 2 || + is_select < 0 || is_select > 1 || + qualifier < 0 || qualifier > 31) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.state = (uint8_t)state; + result.is_select = (uint8_t)is_select; + result.qualifier = (uint8_t)qualifier; + Initialize_Object(&result.object, ioa, &DoubleCommand_VTable); + if (!DoubleCommand_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 46(双命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的1字节载荷,必须非空。 + * length——必须恰好为1;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_DoubleCommand_Get_From_Buffer(IEC60870_DoubleCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_DoubleCommand_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 1U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!DoubleCommand_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &DoubleCommand_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 47:步调节命令 ---- */ +/** + * 功能:编码Type 47(步调节命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少1字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:RCO(1):低2位为调节状态(仅1/2),bit2..6为QU,bit7为S/E。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int StepCommand_Encode_Value(const IEC60870_StepCommand_t *self, uint8_t *payload) +{ + /* DCO/RCO仅接收状态1/2;QU占bit2..6,S/E占bit7。 */ + if ((self->state != 1U && self->state != 2U) || + self->is_select > 1U || self->qualifier > 31U) { return 0; } + payload[0] = (uint8_t)(self->state | (self->qualifier << 2) | (self->is_select << 7)); + return 1; +} + +/** + * 功能:解码Type 47(步调节命令)的值字段。 + * 输入参数:payload——已确认有1字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:RCO(1):低2位为调节状态(仅1/2),bit2..6为QU,bit7为S/E。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int StepCommand_Decode_Value(const uint8_t *payload, IEC60870_StepCommand_t *self) +{ + self->state = payload[0] & 3U; + if (self->state != 1U && self->state != 2U) { return 0; } + self->is_select = payload[0] >> 7; + self->qualifier = (payload[0] >> 2) & 31U; + return 1; +} + +/** + * 功能:Type 47编码函数表的适配入口,将公共基类还原为StepCommand对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少1字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int StepCommand_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return StepCommand_Encode_Value((const IEC60870_StepCommand_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable StepCommand_VTable = { + 47, 1, 1, sizeof(IEC60870_StepCommand_t), StepCommand_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 47(步调节命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * state——双命令/步调节状态,仅允许1或2。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——命令限定词QU,0..31;具体业务含义由应用判断。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * 仅创建内存对象;加入ASDU时才按应用层参数编码IOA并检查报文容量。 + */ +IEC60870_AsduResult_t IEC60870_StepCommand_Create(IEC60870_StepCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier) +{ + IEC60870_StepCommand_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + state < 1 || state > 2 || + is_select < 0 || is_select > 1 || + qualifier < 0 || qualifier > 31) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.state = (uint8_t)state; + result.is_select = (uint8_t)is_select; + result.qualifier = (uint8_t)qualifier; + Initialize_Object(&result.object, ioa, &StepCommand_VTable); + if (!StepCommand_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 47(步调节命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的1字节载荷,必须非空。 + * length——必须恰好为1;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_StepCommand_Get_From_Buffer(IEC60870_StepCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_StepCommand_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 1U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!StepCommand_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &StepCommand_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 48:归一化设定值命令(原始16位定点值) ---- */ +/** + * 功能:编码Type 48(归一化设定值命令(原始16位定点值))的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少3字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:NVA(2)+QOS(1):归一化原始定点值;QOS低7位为QL,bit7为S/E。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int SetpointCommandNormalized_Encode_Value(const IEC60870_SetpointCommandNormalized_t *self, uint8_t *payload) +{ + if (self->is_select > 1U || self->qualifier > 127U) { return 0; } + IEC60870_Object_Write_LE(payload, (uint16_t)self->value, 2); + /* 数值后接QOS:低7位QL,高位S/E。 */ + payload[2] = (uint8_t)(self->qualifier | (self->is_select << 7)); + return 1; +} + +/** + * 功能:解码Type 48(归一化设定值命令(原始16位定点值))的值字段。 + * 输入参数:payload——已确认有3字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:NVA(2)+QOS(1):归一化原始定点值;QOS低7位为QL,bit7为S/E。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int SetpointCommandNormalized_Decode_Value(const uint8_t *payload, IEC60870_SetpointCommandNormalized_t *self) +{ + self->value = Read_I16(payload); + self->qualifier = payload[2] & 127U; + self->is_select = payload[2] >> 7; + return 1; +} + +/** + * 功能:Type 48编码函数表的适配入口,将公共基类还原为SetpointCommandNormalized对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少3字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int SetpointCommandNormalized_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return SetpointCommandNormalized_Encode_Value((const IEC60870_SetpointCommandNormalized_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable SetpointCommandNormalized_VTable = { + 48, 3, 1, sizeof(IEC60870_SetpointCommandNormalized_t), SetpointCommandNormalized_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 48(归一化设定值命令(原始16位定点值))。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——归一化原始int16_t定点值,实际数值为value/32768.0。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——设定值限定词QL,0..127;编码到QOS低7位。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * 仅创建内存对象;加入ASDU时才按应用层参数编码IOA并检查报文容量。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalized_Create(IEC60870_SetpointCommandNormalized_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier) +{ + IEC60870_SetpointCommandNormalized_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + is_select < 0 || is_select > 1 || + qualifier < 0 || qualifier > 127) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.value = value; + result.is_select = (uint8_t)is_select; + result.qualifier = (uint8_t)qualifier; + Initialize_Object(&result.object, ioa, &SetpointCommandNormalized_VTable); + if (!SetpointCommandNormalized_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 48(归一化设定值命令(原始16位定点值))。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的3字节载荷,必须非空。 + * length——必须恰好为3;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalized_Get_From_Buffer(IEC60870_SetpointCommandNormalized_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_SetpointCommandNormalized_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 3U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!SetpointCommandNormalized_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &SetpointCommandNormalized_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 49:标度化设定值命令 ---- */ +/** + * 功能:编码Type 49(标度化设定值命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少3字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:SVA(2)+QOS(1):标度值;QOS低7位为QL,bit7为S/E。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int SetpointCommandScaled_Encode_Value(const IEC60870_SetpointCommandScaled_t *self, uint8_t *payload) +{ + if (self->is_select > 1U || self->qualifier > 127U) { return 0; } + IEC60870_Object_Write_LE(payload, (uint16_t)self->value, 2); + /* 数值后接QOS:低7位QL,高位S/E。 */ + payload[2] = (uint8_t)(self->qualifier | (self->is_select << 7)); + return 1; +} + +/** + * 功能:解码Type 49(标度化设定值命令)的值字段。 + * 输入参数:payload——已确认有3字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:SVA(2)+QOS(1):标度值;QOS低7位为QL,bit7为S/E。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int SetpointCommandScaled_Decode_Value(const uint8_t *payload, IEC60870_SetpointCommandScaled_t *self) +{ + self->value = Read_I16(payload); + self->qualifier = payload[2] & 127U; + self->is_select = payload[2] >> 7; + return 1; +} + +/** + * 功能:Type 49编码函数表的适配入口,将公共基类还原为SetpointCommandScaled对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少3字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int SetpointCommandScaled_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return SetpointCommandScaled_Encode_Value((const IEC60870_SetpointCommandScaled_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable SetpointCommandScaled_VTable = { + 49, 3, 1, sizeof(IEC60870_SetpointCommandScaled_t), SetpointCommandScaled_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 49(标度化设定值命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——有符号16位标度值,范围-32768..32767。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——设定值限定词QL,0..127;编码到QOS低7位。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * 仅创建内存对象;加入ASDU时才按应用层参数编码IOA并检查报文容量。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandScaled_Create(IEC60870_SetpointCommandScaled_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier) +{ + IEC60870_SetpointCommandScaled_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + is_select < 0 || is_select > 1 || + qualifier < 0 || qualifier > 127) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.value = value; + result.is_select = (uint8_t)is_select; + result.qualifier = (uint8_t)qualifier; + Initialize_Object(&result.object, ioa, &SetpointCommandScaled_VTable); + if (!SetpointCommandScaled_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 49(标度化设定值命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的3字节载荷,必须非空。 + * length——必须恰好为3;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandScaled_Get_From_Buffer(IEC60870_SetpointCommandScaled_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_SetpointCommandScaled_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 3U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!SetpointCommandScaled_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &SetpointCommandScaled_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 50:短浮点设定值命令 ---- */ +/** + * 功能:编码Type 50(短浮点设定值命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少5字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:短浮点(4)+QOS(1):浮点位模式;QOS低7位为QL,bit7为S/E。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int SetpointCommandShort_Encode_Value(const IEC60870_SetpointCommandShort_t *self, uint8_t *payload) +{ + if (self->is_select > 1U || self->qualifier > 127U) { return 0; } + Write_Float(payload, &self->value); + /* 数值后接QOS:低7位QL,高位S/E。 */ + payload[4] = (uint8_t)(self->qualifier | (self->is_select << 7)); + return 1; +} + +/** + * 功能:解码Type 50(短浮点设定值命令)的值字段。 + * 输入参数:payload——已确认有5字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:短浮点(4)+QOS(1):浮点位模式;QOS低7位为QL,bit7为S/E。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int SetpointCommandShort_Decode_Value(const uint8_t *payload, IEC60870_SetpointCommandShort_t *self) +{ + Read_Float(payload, &self->value); + self->qualifier = payload[4] & 127U; + self->is_select = payload[4] >> 7; + return 1; +} + +/** + * 功能:Type 50编码函数表的适配入口,将公共基类还原为SetpointCommandShort对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少5字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int SetpointCommandShort_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return SetpointCommandShort_Encode_Value((const IEC60870_SetpointCommandShort_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable SetpointCommandShort_VTable = { + 50, 5, 1, sizeof(IEC60870_SetpointCommandShort_t), SetpointCommandShort_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 50(短浮点设定值命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——短浮点值,采用IEEE 754 binary32,不进行NaN/Inf业务过滤。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——设定值限定词QL,0..127;编码到QOS低7位。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * 仅创建内存对象;加入ASDU时才按应用层参数编码IOA并检查报文容量。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandShort_Create(IEC60870_SetpointCommandShort_t *self, + uint32_t ioa, float value, int is_select, int qualifier) +{ + IEC60870_SetpointCommandShort_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + is_select < 0 || is_select > 1 || + qualifier < 0 || qualifier > 127) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.value = value; + result.is_select = (uint8_t)is_select; + result.qualifier = (uint8_t)qualifier; + Initialize_Object(&result.object, ioa, &SetpointCommandShort_VTable); + if (!SetpointCommandShort_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 50(短浮点设定值命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的5字节载荷,必须非空。 + * length——必须恰好为5;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandShort_Get_From_Buffer(IEC60870_SetpointCommandShort_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_SetpointCommandShort_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 5U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!SetpointCommandShort_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &SetpointCommandShort_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 58:带时标单命令 ---- */ +/** + * 功能:编码Type 58(带时标单命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少8字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:SCO(1):bit0为状态,bit1保留,bit2..6为QU,bit7为S/E,之后追加CP56Time2a(7)。 + * 值部分调用SingleCommand_Encode_Value,成功后追加7字节CP56。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int SingleCommandWithCP56Time2a_Encode_Value(const IEC60870_SingleCommandWithCP56Time2a_t *self, uint8_t *payload) +{ + return SingleCommand_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP56(&self->time, payload + 1, IEC60870_CP56_LEN); +} + +/** + * 功能:解码Type 58(带时标单命令)的值字段和CP56时标。 + * 输入参数:payload——已确认有8字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:SCO(1):bit0为状态,bit1保留,bit2..6为QU,bit7为S/E,之后追加CP56Time2a(7)。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int SingleCommandWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_SingleCommandWithCP56Time2a_t *self) +{ + return SingleCommand_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP56(payload + 1, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:Type 58编码函数表的适配入口,将公共基类还原为SingleCommandWithCP56Time2a对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少8字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int SingleCommandWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return SingleCommandWithCP56Time2a_Encode_Value((const IEC60870_SingleCommandWithCP56Time2a_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable SingleCommandWithCP56Time2a_VTable = { + 58, 8, 1, sizeof(IEC60870_SingleCommandWithCP56Time2a_t), SingleCommandWithCP56Time2a_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 58(带时标单命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * state——单命令状态,0或1;SCO保留位由编码器保持为0。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——命令限定词QU,0..31;具体业务含义由应用判断。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * BCR/CP56输入按值复制,返回后调用方可以复用输入;构造成功不表示命令已执行。 + */ +IEC60870_AsduResult_t IEC60870_SingleCommandWithCP56Time2a_Create(IEC60870_SingleCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_SingleCommandWithCP56Time2a_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + state < 0 || state > 1 || + is_select < 0 || is_select > 1 || + qualifier < 0 || qualifier > 31 || + time == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.base.state = (uint8_t)state; + result.base.is_select = (uint8_t)is_select; + result.base.qualifier = (uint8_t)qualifier; + result.time = *time; + Initialize_Object(&result.base.object, ioa, &SingleCommandWithCP56Time2a_VTable); + if (!SingleCommandWithCP56Time2a_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 58(带时标单命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的8字节载荷,必须非空。 + * length——必须恰好为8;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_SingleCommandWithCP56Time2a_Get_From_Buffer(IEC60870_SingleCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_SingleCommandWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 8U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!SingleCommandWithCP56Time2a_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.base.object, ioa, &SingleCommandWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 59:带时标双命令 ---- */ +/** + * 功能:编码Type 59(带时标双命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少8字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:DCO(1):低2位为状态(仅1/2),bit2..6为QU,bit7为S/E,之后追加CP56Time2a(7)。 + * 值部分调用DoubleCommand_Encode_Value,成功后追加7字节CP56。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int DoubleCommandWithCP56Time2a_Encode_Value(const IEC60870_DoubleCommandWithCP56Time2a_t *self, uint8_t *payload) +{ + return DoubleCommand_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP56(&self->time, payload + 1, IEC60870_CP56_LEN); +} + +/** + * 功能:解码Type 59(带时标双命令)的值字段和CP56时标。 + * 输入参数:payload——已确认有8字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:DCO(1):低2位为状态(仅1/2),bit2..6为QU,bit7为S/E,之后追加CP56Time2a(7)。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int DoubleCommandWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_DoubleCommandWithCP56Time2a_t *self) +{ + return DoubleCommand_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP56(payload + 1, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:Type 59编码函数表的适配入口,将公共基类还原为DoubleCommandWithCP56Time2a对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少8字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int DoubleCommandWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return DoubleCommandWithCP56Time2a_Encode_Value((const IEC60870_DoubleCommandWithCP56Time2a_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable DoubleCommandWithCP56Time2a_VTable = { + 59, 8, 1, sizeof(IEC60870_DoubleCommandWithCP56Time2a_t), DoubleCommandWithCP56Time2a_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 59(带时标双命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * state——双命令/步调节状态,仅允许1或2。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——命令限定词QU,0..31;具体业务含义由应用判断。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * BCR/CP56输入按值复制,返回后调用方可以复用输入;构造成功不表示命令已执行。 + */ +IEC60870_AsduResult_t IEC60870_DoubleCommandWithCP56Time2a_Create(IEC60870_DoubleCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_DoubleCommandWithCP56Time2a_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + state < 1 || state > 2 || + is_select < 0 || is_select > 1 || + qualifier < 0 || qualifier > 31 || + time == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.base.state = (uint8_t)state; + result.base.is_select = (uint8_t)is_select; + result.base.qualifier = (uint8_t)qualifier; + result.time = *time; + Initialize_Object(&result.base.object, ioa, &DoubleCommandWithCP56Time2a_VTable); + if (!DoubleCommandWithCP56Time2a_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 59(带时标双命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的8字节载荷,必须非空。 + * length——必须恰好为8;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_DoubleCommandWithCP56Time2a_Get_From_Buffer(IEC60870_DoubleCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_DoubleCommandWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 8U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!DoubleCommandWithCP56Time2a_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.base.object, ioa, &DoubleCommandWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 60:带时标步调节命令 ---- */ +/** + * 功能:编码Type 60(带时标步调节命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少8字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:RCO(1):低2位为调节状态(仅1/2),bit2..6为QU,bit7为S/E,之后追加CP56Time2a(7)。 + * 值部分调用StepCommand_Encode_Value,成功后追加7字节CP56。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int StepCommandWithCP56Time2a_Encode_Value(const IEC60870_StepCommandWithCP56Time2a_t *self, uint8_t *payload) +{ + return StepCommand_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP56(&self->time, payload + 1, IEC60870_CP56_LEN); +} + +/** + * 功能:解码Type 60(带时标步调节命令)的值字段和CP56时标。 + * 输入参数:payload——已确认有8字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:RCO(1):低2位为调节状态(仅1/2),bit2..6为QU,bit7为S/E,之后追加CP56Time2a(7)。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int StepCommandWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_StepCommandWithCP56Time2a_t *self) +{ + return StepCommand_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP56(payload + 1, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:Type 60编码函数表的适配入口,将公共基类还原为StepCommandWithCP56Time2a对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少8字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int StepCommandWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return StepCommandWithCP56Time2a_Encode_Value((const IEC60870_StepCommandWithCP56Time2a_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable StepCommandWithCP56Time2a_VTable = { + 60, 8, 1, sizeof(IEC60870_StepCommandWithCP56Time2a_t), StepCommandWithCP56Time2a_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 60(带时标步调节命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * state——双命令/步调节状态,仅允许1或2。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——命令限定词QU,0..31;具体业务含义由应用判断。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * BCR/CP56输入按值复制,返回后调用方可以复用输入;构造成功不表示命令已执行。 + */ +IEC60870_AsduResult_t IEC60870_StepCommandWithCP56Time2a_Create(IEC60870_StepCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_StepCommandWithCP56Time2a_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + state < 1 || state > 2 || + is_select < 0 || is_select > 1 || + qualifier < 0 || qualifier > 31 || + time == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.base.state = (uint8_t)state; + result.base.is_select = (uint8_t)is_select; + result.base.qualifier = (uint8_t)qualifier; + result.time = *time; + Initialize_Object(&result.base.object, ioa, &StepCommandWithCP56Time2a_VTable); + if (!StepCommandWithCP56Time2a_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 60(带时标步调节命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的8字节载荷,必须非空。 + * length——必须恰好为8;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_StepCommandWithCP56Time2a_Get_From_Buffer(IEC60870_StepCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_StepCommandWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 8U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!StepCommandWithCP56Time2a_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.base.object, ioa, &StepCommandWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 61:带时标归一化设定值命令 ---- */ +/** + * 功能:编码Type 61(带时标归一化设定值命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少10字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:NVA(2)+QOS(1):归一化原始定点值;QOS低7位为QL,bit7为S/E,之后追加CP56Time2a(7)。 + * 值部分调用SetpointCommandNormalized_Encode_Value,成功后追加7字节CP56。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int SetpointCommandNormalizedWithCP56Time2a_Encode_Value(const IEC60870_SetpointCommandNormalizedWithCP56Time2a_t *self, uint8_t *payload) +{ + return SetpointCommandNormalized_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP56(&self->time, payload + 3, IEC60870_CP56_LEN); +} + +/** + * 功能:解码Type 61(带时标归一化设定值命令)的值字段和CP56时标。 + * 输入参数:payload——已确认有10字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:NVA(2)+QOS(1):归一化原始定点值;QOS低7位为QL,bit7为S/E,之后追加CP56Time2a(7)。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int SetpointCommandNormalizedWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_SetpointCommandNormalizedWithCP56Time2a_t *self) +{ + return SetpointCommandNormalized_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP56(payload + 3, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:Type 61编码函数表的适配入口,将公共基类还原为SetpointCommandNormalizedWithCP56Time2a对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少10字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int SetpointCommandNormalizedWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return SetpointCommandNormalizedWithCP56Time2a_Encode_Value((const IEC60870_SetpointCommandNormalizedWithCP56Time2a_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable SetpointCommandNormalizedWithCP56Time2a_VTable = { + 61, 10, 1, sizeof(IEC60870_SetpointCommandNormalizedWithCP56Time2a_t), SetpointCommandNormalizedWithCP56Time2a_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 61(带时标归一化设定值命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——归一化原始int16_t定点值,实际数值为value/32768.0。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——设定值限定词QL,0..127;编码到QOS低7位。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * BCR/CP56输入按值复制,返回后调用方可以复用输入;构造成功不表示命令已执行。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalizedWithCP56Time2a_Create(IEC60870_SetpointCommandNormalizedWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_SetpointCommandNormalizedWithCP56Time2a_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + is_select < 0 || is_select > 1 || + qualifier < 0 || qualifier > 127 || + time == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.base.value = value; + result.base.is_select = (uint8_t)is_select; + result.base.qualifier = (uint8_t)qualifier; + result.time = *time; + Initialize_Object(&result.base.object, ioa, &SetpointCommandNormalizedWithCP56Time2a_VTable); + if (!SetpointCommandNormalizedWithCP56Time2a_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 61(带时标归一化设定值命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的10字节载荷,必须非空。 + * length——必须恰好为10;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalizedWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandNormalizedWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_SetpointCommandNormalizedWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 10U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!SetpointCommandNormalizedWithCP56Time2a_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.base.object, ioa, &SetpointCommandNormalizedWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 62:带时标标度化设定值命令 ---- */ +/** + * 功能:编码Type 62(带时标标度化设定值命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少10字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:SVA(2)+QOS(1):标度值;QOS低7位为QL,bit7为S/E,之后追加CP56Time2a(7)。 + * 值部分调用SetpointCommandScaled_Encode_Value,成功后追加7字节CP56。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int SetpointCommandScaledWithCP56Time2a_Encode_Value(const IEC60870_SetpointCommandScaledWithCP56Time2a_t *self, uint8_t *payload) +{ + return SetpointCommandScaled_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP56(&self->time, payload + 3, IEC60870_CP56_LEN); +} + +/** + * 功能:解码Type 62(带时标标度化设定值命令)的值字段和CP56时标。 + * 输入参数:payload——已确认有10字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:SVA(2)+QOS(1):标度值;QOS低7位为QL,bit7为S/E,之后追加CP56Time2a(7)。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int SetpointCommandScaledWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_SetpointCommandScaledWithCP56Time2a_t *self) +{ + return SetpointCommandScaled_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP56(payload + 3, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:Type 62编码函数表的适配入口,将公共基类还原为SetpointCommandScaledWithCP56Time2a对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少10字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int SetpointCommandScaledWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return SetpointCommandScaledWithCP56Time2a_Encode_Value((const IEC60870_SetpointCommandScaledWithCP56Time2a_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable SetpointCommandScaledWithCP56Time2a_VTable = { + 62, 10, 1, sizeof(IEC60870_SetpointCommandScaledWithCP56Time2a_t), SetpointCommandScaledWithCP56Time2a_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 62(带时标标度化设定值命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——有符号16位标度值,范围-32768..32767。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——设定值限定词QL,0..127;编码到QOS低7位。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * BCR/CP56输入按值复制,返回后调用方可以复用输入;构造成功不表示命令已执行。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandScaledWithCP56Time2a_Create(IEC60870_SetpointCommandScaledWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_SetpointCommandScaledWithCP56Time2a_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + is_select < 0 || is_select > 1 || + qualifier < 0 || qualifier > 127 || + time == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.base.value = value; + result.base.is_select = (uint8_t)is_select; + result.base.qualifier = (uint8_t)qualifier; + result.time = *time; + Initialize_Object(&result.base.object, ioa, &SetpointCommandScaledWithCP56Time2a_VTable); + if (!SetpointCommandScaledWithCP56Time2a_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 62(带时标标度化设定值命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的10字节载荷,必须非空。 + * length——必须恰好为10;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandScaledWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandScaledWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_SetpointCommandScaledWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 10U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!SetpointCommandScaledWithCP56Time2a_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.base.object, ioa, &SetpointCommandScaledWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 63:带时标短浮点设定值命令 ---- */ +/** + * 功能:编码Type 63(带时标短浮点设定值命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少12字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:短浮点(4)+QOS(1):浮点位模式;QOS低7位为QL,bit7为S/E,之后追加CP56Time2a(7)。 + * 值部分调用SetpointCommandShort_Encode_Value,成功后追加7字节CP56。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int SetpointCommandShortWithCP56Time2a_Encode_Value(const IEC60870_SetpointCommandShortWithCP56Time2a_t *self, uint8_t *payload) +{ + return SetpointCommandShort_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP56(&self->time, payload + 5, IEC60870_CP56_LEN); +} + +/** + * 功能:解码Type 63(带时标短浮点设定值命令)的值字段和CP56时标。 + * 输入参数:payload——已确认有12字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:短浮点(4)+QOS(1):浮点位模式;QOS低7位为QL,bit7为S/E,之后追加CP56Time2a(7)。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int SetpointCommandShortWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_SetpointCommandShortWithCP56Time2a_t *self) +{ + return SetpointCommandShort_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP56(payload + 5, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:Type 63编码函数表的适配入口,将公共基类还原为SetpointCommandShortWithCP56Time2a对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少12字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int SetpointCommandShortWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return SetpointCommandShortWithCP56Time2a_Encode_Value((const IEC60870_SetpointCommandShortWithCP56Time2a_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable SetpointCommandShortWithCP56Time2a_VTable = { + 63, 12, 1, sizeof(IEC60870_SetpointCommandShortWithCP56Time2a_t), SetpointCommandShortWithCP56Time2a_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 63(带时标短浮点设定值命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——短浮点值,采用IEEE 754 binary32,不进行NaN/Inf业务过滤。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——设定值限定词QL,0..127;编码到QOS低7位。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * BCR/CP56输入按值复制,返回后调用方可以复用输入;构造成功不表示命令已执行。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandShortWithCP56Time2a_Create(IEC60870_SetpointCommandShortWithCP56Time2a_t *self, + uint32_t ioa, float value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_SetpointCommandShortWithCP56Time2a_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + is_select < 0 || is_select > 1 || + qualifier < 0 || qualifier > 127 || + time == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.base.value = value; + result.base.is_select = (uint8_t)is_select; + result.base.qualifier = (uint8_t)qualifier; + result.time = *time; + Initialize_Object(&result.base.object, ioa, &SetpointCommandShortWithCP56Time2a_VTable); + if (!SetpointCommandShortWithCP56Time2a_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 63(带时标短浮点设定值命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的12字节载荷,必须非空。 + * length——必须恰好为12;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandShortWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandShortWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_SetpointCommandShortWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 12U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!SetpointCommandShortWithCP56Time2a_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.base.object, ioa, &SetpointCommandShortWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 100:总召唤命令 ---- */ +/** + * 功能:编码Type 100(总召唤命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少1字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:QOI(1):总召唤限定词,整字节保存,业务层判断召唤范围。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int InterrogationCommand_Encode_Value(const IEC60870_InterrogationCommand_t *self, uint8_t *payload) +{ + payload[0] = self->qualifier; + return 1; +} + +/** + * 功能:解码Type 100(总召唤命令)的值字段。 + * 输入参数:payload——已确认有1字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:QOI(1):总召唤限定词,整字节保存,业务层判断召唤范围。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int InterrogationCommand_Decode_Value(const uint8_t *payload, IEC60870_InterrogationCommand_t *self) +{ + self->qualifier = payload[0]; + return 1; +} + +/** + * 功能:Type 100编码函数表的适配入口,将公共基类还原为InterrogationCommand对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少1字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int InterrogationCommand_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return InterrogationCommand_Encode_Value((const IEC60870_InterrogationCommand_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable InterrogationCommand_VTable = { + 100, 1, 1, sizeof(IEC60870_InterrogationCommand_t), InterrogationCommand_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 100(总召唤命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * qualifier——总召唤限定词QOI,0..255;本层保留全部编码值。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * 仅创建内存对象;加入ASDU时才按应用层参数编码IOA并检查报文容量。 + */ +IEC60870_AsduResult_t IEC60870_InterrogationCommand_Create(IEC60870_InterrogationCommand_t *self, + uint32_t ioa, int qualifier) +{ + IEC60870_InterrogationCommand_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + qualifier < 0 || qualifier > 255) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.qualifier = (uint8_t)qualifier; + Initialize_Object(&result.object, ioa, &InterrogationCommand_VTable); + if (!InterrogationCommand_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 100(总召唤命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的1字节载荷,必须非空。 + * length——必须恰好为1;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_InterrogationCommand_Get_From_Buffer(IEC60870_InterrogationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_InterrogationCommand_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 1U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!InterrogationCommand_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &InterrogationCommand_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 101:电能脉冲召唤命令 ---- */ +/** + * 功能:编码Type 101(电能脉冲召唤命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少1字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:QCC(1):低6位为请求限定词RQT,高2位为冻结限定词FR。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int CounterInterrogationCommand_Encode_Value(const IEC60870_CounterInterrogationCommand_t *self, uint8_t *payload) +{ + if (self->qualifier > 63U || self->freeze > 3U) { return 0; } + /* QCC:低6位RQT,高2位FR。 */ + payload[0] = (uint8_t)(self->qualifier | (self->freeze << 6)); + return 1; +} + +/** + * 功能:解码Type 101(电能脉冲召唤命令)的值字段。 + * 输入参数:payload——已确认有1字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:QCC(1):低6位为请求限定词RQT,高2位为冻结限定词FR。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int CounterInterrogationCommand_Decode_Value(const uint8_t *payload, IEC60870_CounterInterrogationCommand_t *self) +{ + self->qualifier = payload[0] & 63U; + self->freeze = payload[0] >> 6; + return 1; +} + +/** + * 功能:Type 101编码函数表的适配入口,将公共基类还原为CounterInterrogationCommand对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少1字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int CounterInterrogationCommand_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return CounterInterrogationCommand_Encode_Value((const IEC60870_CounterInterrogationCommand_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable CounterInterrogationCommand_VTable = { + 101, 1, 1, sizeof(IEC60870_CounterInterrogationCommand_t), CounterInterrogationCommand_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 101(电能脉冲召唤命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * qualifier——电能脉冲请求限定词RQT,0..63。 + * freeze——冻结限定词FR,0..3,编码到QCC高2位。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * 仅创建内存对象;加入ASDU时才按应用层参数编码IOA并检查报文容量。 + */ +IEC60870_AsduResult_t IEC60870_CounterInterrogationCommand_Create(IEC60870_CounterInterrogationCommand_t *self, + uint32_t ioa, int qualifier, int freeze) +{ + IEC60870_CounterInterrogationCommand_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + qualifier < 0 || qualifier > 63 || + freeze < 0 || freeze > 3) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.qualifier = (uint8_t)qualifier; + result.freeze = (uint8_t)freeze; + Initialize_Object(&result.object, ioa, &CounterInterrogationCommand_VTable); + if (!CounterInterrogationCommand_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 101(电能脉冲召唤命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的1字节载荷,必须非空。 + * length——必须恰好为1;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_CounterInterrogationCommand_Get_From_Buffer(IEC60870_CounterInterrogationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_CounterInterrogationCommand_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 1U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!CounterInterrogationCommand_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &CounterInterrogationCommand_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 102:读命令 ---- */ +/** + * 功能:编码Type 102(读命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少0字节的临时区;本类型不访问该区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:无值载荷:报文只包含由ASDU层处理的IOA。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int ReadCommand_Encode_Value(const IEC60870_ReadCommand_t *self, uint8_t *payload) +{ + /* 读命令仅含IOA,无值载荷;不解引用payload。 */ + (void)self; + (void)payload; + return 1; +} + +/** + * 功能:解码Type 102(读命令)的值字段。 + * 输入参数:payload——已确认有0字节的载荷;允许NULL,函数不解引用。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:无值载荷:报文只包含由ASDU层处理的IOA。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int ReadCommand_Decode_Value(const uint8_t *payload, IEC60870_ReadCommand_t *self) +{ + /* 读命令仅含IOA,无值载荷;不解引用payload。 */ + (void)self; + (void)payload; + return 1; +} + +/** + * 功能:Type 102编码函数表的适配入口,将公共基类还原为ReadCommand对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少0字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int ReadCommand_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return ReadCommand_Encode_Value((const IEC60870_ReadCommand_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable ReadCommand_VTable = { + 102, 0, 1, sizeof(IEC60870_ReadCommand_t), ReadCommand_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 102(读命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * 仅创建内存对象;加入ASDU时才按应用层参数编码IOA并检查报文容量。 + */ +IEC60870_AsduResult_t IEC60870_ReadCommand_Create(IEC60870_ReadCommand_t *self, + uint32_t ioa) +{ + IEC60870_ReadCommand_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + Initialize_Object(&result.object, ioa, &ReadCommand_VTable); + if (!ReadCommand_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 102(读命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的0字节载荷,允许NULL。 + * length——必须恰好为0;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_ReadCommand_Get_From_Buffer(IEC60870_ReadCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_ReadCommand_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 0U) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!ReadCommand_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &ReadCommand_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 103:时钟同步命令 ---- */ +/** + * 功能:编码Type 103(时钟同步命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少7字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:CP56Time2a(7):源时间及IV/SU,不执行系统校时。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int ClockSynchronizationCommand_Encode_Value(const IEC60870_ClockSynchronizationCommand_t *self, uint8_t *payload) +{ + return IEC60870_Time_To_CP56(&self->time, payload, IEC60870_CP56_LEN); +} + +/** + * 功能:解码Type 103(时钟同步命令)的值字段和CP56时标。 + * 输入参数:payload——已确认有7字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:CP56Time2a(7):源时间及IV/SU,不执行系统校时。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int ClockSynchronizationCommand_Decode_Value(const uint8_t *payload, IEC60870_ClockSynchronizationCommand_t *self) +{ + return IEC60870_Time_From_CP56(payload, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:Type 103编码函数表的适配入口,将公共基类还原为ClockSynchronizationCommand对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少7字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int ClockSynchronizationCommand_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return ClockSynchronizationCommand_Encode_Value((const IEC60870_ClockSynchronizationCommand_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable ClockSynchronizationCommand_VTable = { + 103, 7, 1, sizeof(IEC60870_ClockSynchronizationCommand_t), ClockSynchronizationCommand_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 103(时钟同步命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * BCR/CP56输入按值复制,返回后调用方可以复用输入;构造成功不表示命令已执行。 + */ +IEC60870_AsduResult_t IEC60870_ClockSynchronizationCommand_Create(IEC60870_ClockSynchronizationCommand_t *self, + uint32_t ioa, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_ClockSynchronizationCommand_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + time == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.time = *time; + Initialize_Object(&result.object, ioa, &ClockSynchronizationCommand_VTable); + if (!ClockSynchronizationCommand_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 103(时钟同步命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的7字节载荷,必须非空。 + * length——必须恰好为7;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_ClockSynchronizationCommand_Get_From_Buffer(IEC60870_ClockSynchronizationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_ClockSynchronizationCommand_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 7U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!ClockSynchronizationCommand_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &ClockSynchronizationCommand_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 105:复位进程命令 ---- */ +/** + * 功能:编码Type 105(复位进程命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少1字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:QRP(1):复位进程限定词,整字节保存,业务层判断操作含义。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int ResetProcessCommand_Encode_Value(const IEC60870_ResetProcessCommand_t *self, uint8_t *payload) +{ + payload[0] = self->qualifier; + return 1; +} + +/** + * 功能:解码Type 105(复位进程命令)的值字段。 + * 输入参数:payload——已确认有1字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:QRP(1):复位进程限定词,整字节保存,业务层判断操作含义。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int ResetProcessCommand_Decode_Value(const uint8_t *payload, IEC60870_ResetProcessCommand_t *self) +{ + self->qualifier = payload[0]; + return 1; +} + +/** + * 功能:Type 105编码函数表的适配入口,将公共基类还原为ResetProcessCommand对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少1字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int ResetProcessCommand_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return ResetProcessCommand_Encode_Value((const IEC60870_ResetProcessCommand_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable ResetProcessCommand_VTable = { + 105, 1, 1, sizeof(IEC60870_ResetProcessCommand_t), ResetProcessCommand_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 105(复位进程命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * qualifier——复位进程限定词QRP,0..255;本层不执行复位。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * 仅创建内存对象;加入ASDU时才按应用层参数编码IOA并检查报文容量。 + */ +IEC60870_AsduResult_t IEC60870_ResetProcessCommand_Create(IEC60870_ResetProcessCommand_t *self, + uint32_t ioa, int qualifier) +{ + IEC60870_ResetProcessCommand_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + qualifier < 0 || qualifier > 255) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.qualifier = (uint8_t)qualifier; + Initialize_Object(&result.object, ioa, &ResetProcessCommand_VTable); + if (!ResetProcessCommand_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 105(复位进程命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的1字节载荷,必须非空。 + * length——必须恰好为1;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_ResetProcessCommand_Get_From_Buffer(IEC60870_ResetProcessCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_ResetProcessCommand_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 1U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!ResetProcessCommand_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &ResetProcessCommand_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 107:带时标测试命令 ---- */ +/** + * 功能:编码Type 107(带时标测试命令)的值载荷,不处理IOA或ASDU头。 + * 输入参数:self——非空具体对象;只检查本类型的值字段,不核对公共基类身份。 + * 输出参数:payload——至少9字节的临时区。 + * 返回值:1表示字段合法且编码完成;0表示字段或时标非法。 + * 说明:布局:测试序号(2)+CP56Time2a(7):测试序号与链路发送序号无关。 + * 多字节整数和浮点字节均低字节在前,结构体不直接映射报文。 + * 失败可以留下部分临时字节;公开入口只在全部验证成功后写入用户缓冲区。 + */ +static int TestCommandWithCP56Time2a_Encode_Value(const IEC60870_TestCommandWithCP56Time2a_t *self, uint8_t *payload) +{ + IEC60870_Object_Write_LE(payload, self->sequence, 2); + return IEC60870_Time_To_CP56(&self->time, payload + 2, IEC60870_CP56_LEN); +} + +/** + * 功能:解码Type 107(带时标测试命令)的值字段和CP56时标。 + * 输入参数:payload——已确认有9字节的载荷。 + * 输出参数:self——调用方已清零的临时具体对象;不填写IOA、Type ID或函数表。 + * 返回值:1表示解码成功;0表示保留位、状态或时标不合法。 + * 说明:布局:测试序号(2)+CP56Time2a(7):测试序号与链路发送序号无关。 + * 本内部函数不重复检查指针/长度,失败可留下部分字段,由公开解析器丢弃临时结果。 + */ +static int TestCommandWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_TestCommandWithCP56Time2a_t *self) +{ + self->sequence = (uint16_t)IEC60870_Object_Read_LE(payload, 2); + return IEC60870_Time_From_CP56(payload + 2, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:Type 107编码函数表的适配入口,将公共基类还原为TestCommandWithCP56Time2a对象。 + * 输入参数:object——来自该具体类型的公共首成员指针;调用前已核对Type ID和函数表。 + * 输出参数:payload——至少9字节的临时载荷区,不包含IOA。 + * 返回值:1表示值编码成功;0表示字段或时标非法。 + * 说明:类型转换依赖结构体首成员地址相同的约定,不能向此入口传入伪造基类。 + */ +static int TestCommandWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return TestCommandWithCP56Time2a_Encode_Value((const IEC60870_TestCommandWithCP56Time2a_t *)object, payload); +} + +/* 表项依次为Type ID、线上载荷字节数、单对象标志、内存结构尺寸、编码入口。 + * sizeof仅用于检查解码输出存储,不能作为报文载荷长度。 */ +static const IEC60870_InformationObjectVTable TestCommandWithCP56Time2a_VTable = { + 107, 9, 1, sizeof(IEC60870_TestCommandWithCP56Time2a_t), TestCommandWithCP56Time2a_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 107(带时标测试命令)。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * sequence——16位测试序号,0..65535,与APCI的N(S)/N(R)无关。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——非空具体对象存储;成功时写入值、IOA、类型和只读编码函数表。 + * 返回值:IEC60870_ASDU_OK表示构造完成;IEC60870_ASDU_ERROR表示参数、字段或时标非法。 + * 说明:不分配堆内存。先在局部对象中试编码校验,再整体写回self;失败保持原对象不变。 + * BCR/CP56输入按值复制,返回后调用方可以复用输入;构造成功不表示命令已执行。 + */ +IEC60870_AsduResult_t IEC60870_TestCommandWithCP56Time2a_Create(IEC60870_TestCommandWithCP56Time2a_t *self, + uint32_t ioa, uint16_t sequence, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_TestCommandWithCP56Time2a_t result; + uint8_t payload[12]; + if (self == NULL || + ioa > IEC60870_ASDU_MAX_IOA || + time == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + result.sequence = sequence; + result.time = *time; + Initialize_Object(&result.object, ioa, &TestCommandWithCP56Time2a_VTable); + if (!TestCommandWithCP56Time2a_Encode_Value(&result, payload)) { + return IEC60870_ASDU_ERROR; + } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:从精确长度载荷中解析Type 107(带时标测试命令)。 + * 输入参数:ioa——已从对象区提取的地址,0..0xFFFFFF;SQ隐含地址由ASDU层还原。 + * payload——不包含IOA或ASDU头的9字节载荷,必须非空。 + * length——必须恰好为9;截短和多余尾字节均拒绝。 + * 输出参数:self——非空具体对象存储;成功结果按值保存,不借用输入。 + * 返回值:IEC60870_ASDU_OK表示解析完成;IEC60870_ASDU_ERROR表示地址、长度或字段非法。 + * 说明:仅在值与可选时标全部通过校验后绑定基类并写回self,任何失败保持原对象不变。 + */ +IEC60870_AsduResult_t IEC60870_TestCommandWithCP56Time2a_Get_From_Buffer(IEC60870_TestCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_TestCommandWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || length != 9U || payload == NULL) { + return IEC60870_ASDU_ERROR; + } + memset(&result, 0, sizeof(result)); + if (!TestCommandWithCP56Time2a_Decode_Value(payload, &result)) { + return IEC60870_ASDU_ERROR; + } + Initialize_Object(&result.object, ioa, &TestCommandWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 5:步位置信息 ---- */ +/** + * 功能:编码步位置信息的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少2字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:VTI(1:低7位补码位置、bit7瞬态) + QDS(1);失败只影响临时区。 + */ +static int StepPositionInformation_Encode_Value(const IEC60870_StepPositionInformation_t *self, uint8_t *payload) +{ + if (self->value < -64 || self->value > 63 || self->is_transient > 1U || (self->quality & 0x0EU)) { return 0; } + payload[0] = (uint8_t)(((uint8_t)self->value & 0x7FU) | (self->is_transient << 7)); + payload[1] = self->quality; + return 1; +} + +/** + * 功能:解码步位置信息的值字段。 + * 输入参数:payload——已校验完整长度的2字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:VTI(1:低7位补码位置、bit7瞬态) + QDS(1);公开入口在成功后统一提交。 + */ +static int StepPositionInformation_Decode_Value(const uint8_t *payload, IEC60870_StepPositionInformation_t *self) +{ + if (payload[1] & 0x0EU) { return 0; } + self->value = (int8_t)((payload[0] & 0x40U) ? (int)(payload[0] & 0x7FU) - 128 : (int)(payload[0] & 0x7FU)); + self->is_transient = payload[0] >> 7; + self->quality = payload[1]; + return 1; +} + +/** + * 功能:将公共基类分派到StepPositionInformation编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int StepPositionInformation_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return StepPositionInformation_Encode_Value((const IEC60870_StepPositionInformation_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable StepPositionInformation_VTable = { + 5, 2, 0, sizeof(IEC60870_StepPositionInformation_t), StepPositionInformation_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 5(步位置信息)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_StepPositionInformation_Create(IEC60870_StepPositionInformation_t *self, + uint32_t ioa, int value, int is_transient, int quality) +{ + IEC60870_StepPositionInformation_t result; + uint8_t payload[2]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (value < -64 || value > 63) || + (is_transient < 0 || is_transient > 1) || + (quality < 0 || quality > 255)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.value = (int8_t)value; + result.is_transient = (uint8_t)is_transient; + result.quality = (uint8_t)quality; + Initialize_Object(&result.object, ioa, &StepPositionInformation_VTable); + if (!StepPositionInformation_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 5(步位置信息)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_StepPositionInformation_Get_From_Buffer(IEC60870_StepPositionInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_StepPositionInformation_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 2U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!StepPositionInformation_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &StepPositionInformation_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 7:32位比特串监视 ---- */ +/** + * 功能:编码32位比特串监视的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少5字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:BSI(4) + QDS(1);失败只影响临时区。 + */ +static int BitString32_Encode_Value(const IEC60870_BitString32_t *self, uint8_t *payload) +{ + if (self->quality & 0x0EU) { return 0; } + IEC60870_Object_Write_LE(payload + 0, self->value, 4); + payload[4] = self->quality; + return 1; +} + +/** + * 功能:解码32位比特串监视的值字段。 + * 输入参数:payload——已校验完整长度的5字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:BSI(4) + QDS(1);公开入口在成功后统一提交。 + */ +static int BitString32_Decode_Value(const uint8_t *payload, IEC60870_BitString32_t *self) +{ + if (payload[4] & 0x0EU) { return 0; } + self->value = (uint32_t)IEC60870_Object_Read_LE(payload + 0, 4); + self->quality = payload[4]; + return 1; +} + +/** + * 功能:将公共基类分派到BitString32编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int BitString32_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return BitString32_Encode_Value((const IEC60870_BitString32_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable BitString32_VTable = { + 7, 5, 0, sizeof(IEC60870_BitString32_t), BitString32_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 7(32位比特串监视)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_BitString32_Create(IEC60870_BitString32_t *self, + uint32_t ioa, uint32_t value, int quality) +{ + IEC60870_BitString32_t result; + uint8_t payload[5]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (quality < 0 || quality > 255)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.value = value; + result.quality = (uint8_t)quality; + Initialize_Object(&result.object, ioa, &BitString32_VTable); + if (!BitString32_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 7(32位比特串监视)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_BitString32_Get_From_Buffer(IEC60870_BitString32_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_BitString32_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 5U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!BitString32_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &BitString32_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 2:带CP24单点信息 ---- */ +/** + * 功能:编码带CP24单点信息的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少4字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:基础值(1) + CP24(3);失败只影响临时区。 + */ +static int SinglePointWithCP24Time2a_Encode_Value(const IEC60870_SinglePointWithCP24Time2a_t *self, uint8_t *payload) +{ + return SinglePointInformation_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP24(&self->time, payload + 1, IEC60870_CP24_LEN); +} + +/** + * 功能:解码带CP24单点信息的值字段。 + * 输入参数:payload——已校验完整长度的4字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:基础值(1) + CP24(3);公开入口在成功后统一提交。 + */ +static int SinglePointWithCP24Time2a_Decode_Value(const uint8_t *payload, IEC60870_SinglePointWithCP24Time2a_t *self) +{ + return SinglePointInformation_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP24(payload + 1, IEC60870_CP24_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到SinglePointWithCP24Time2a编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int SinglePointWithCP24Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return SinglePointWithCP24Time2a_Encode_Value((const IEC60870_SinglePointWithCP24Time2a_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable SinglePointWithCP24Time2a_VTable = { + 2, 4, 0, sizeof(IEC60870_SinglePointWithCP24Time2a_t), SinglePointWithCP24Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 2(带CP24单点信息)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_SinglePointWithCP24Time2a_Create(IEC60870_SinglePointWithCP24Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP24Time2a_t *time) +{ + IEC60870_SinglePointWithCP24Time2a_t result; + uint8_t payload[4]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (IEC60870_SinglePointInformation_Create(&result.base, ioa, value, quality) != IEC60870_ASDU_OK) { return IEC60870_ASDU_ERROR; } + result.time = *time; + Initialize_Object(&result.base.object, ioa, &SinglePointWithCP24Time2a_VTable); + if (!SinglePointWithCP24Time2a_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 2(带CP24单点信息)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_SinglePointWithCP24Time2a_Get_From_Buffer(IEC60870_SinglePointWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_SinglePointWithCP24Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 4U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!SinglePointWithCP24Time2a_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.base.object, ioa, &SinglePointWithCP24Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 4:带CP24双点信息 ---- */ +/** + * 功能:编码带CP24双点信息的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少4字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:基础值(1) + CP24(3);失败只影响临时区。 + */ +static int DoublePointWithCP24Time2a_Encode_Value(const IEC60870_DoublePointWithCP24Time2a_t *self, uint8_t *payload) +{ + return DoublePointInformation_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP24(&self->time, payload + 1, IEC60870_CP24_LEN); +} + +/** + * 功能:解码带CP24双点信息的值字段。 + * 输入参数:payload——已校验完整长度的4字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:基础值(1) + CP24(3);公开入口在成功后统一提交。 + */ +static int DoublePointWithCP24Time2a_Decode_Value(const uint8_t *payload, IEC60870_DoublePointWithCP24Time2a_t *self) +{ + return DoublePointInformation_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP24(payload + 1, IEC60870_CP24_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到DoublePointWithCP24Time2a编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int DoublePointWithCP24Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return DoublePointWithCP24Time2a_Encode_Value((const IEC60870_DoublePointWithCP24Time2a_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable DoublePointWithCP24Time2a_VTable = { + 4, 4, 0, sizeof(IEC60870_DoublePointWithCP24Time2a_t), DoublePointWithCP24Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 4(带CP24双点信息)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_DoublePointWithCP24Time2a_Create(IEC60870_DoublePointWithCP24Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP24Time2a_t *time) +{ + IEC60870_DoublePointWithCP24Time2a_t result; + uint8_t payload[4]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (IEC60870_DoublePointInformation_Create(&result.base, ioa, value, quality) != IEC60870_ASDU_OK) { return IEC60870_ASDU_ERROR; } + result.time = *time; + Initialize_Object(&result.base.object, ioa, &DoublePointWithCP24Time2a_VTable); + if (!DoublePointWithCP24Time2a_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 4(带CP24双点信息)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_DoublePointWithCP24Time2a_Get_From_Buffer(IEC60870_DoublePointWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_DoublePointWithCP24Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 4U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!DoublePointWithCP24Time2a_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.base.object, ioa, &DoublePointWithCP24Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 6:带CP24步位置信息 ---- */ +/** + * 功能:编码带CP24步位置信息的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少5字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:基础值(2) + CP24(3);失败只影响临时区。 + */ +static int StepPositionWithCP24Time2a_Encode_Value(const IEC60870_StepPositionWithCP24Time2a_t *self, uint8_t *payload) +{ + return StepPositionInformation_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP24(&self->time, payload + 2, IEC60870_CP24_LEN); +} + +/** + * 功能:解码带CP24步位置信息的值字段。 + * 输入参数:payload——已校验完整长度的5字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:基础值(2) + CP24(3);公开入口在成功后统一提交。 + */ +static int StepPositionWithCP24Time2a_Decode_Value(const uint8_t *payload, IEC60870_StepPositionWithCP24Time2a_t *self) +{ + return StepPositionInformation_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP24(payload + 2, IEC60870_CP24_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到StepPositionWithCP24Time2a编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int StepPositionWithCP24Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return StepPositionWithCP24Time2a_Encode_Value((const IEC60870_StepPositionWithCP24Time2a_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable StepPositionWithCP24Time2a_VTable = { + 6, 5, 0, sizeof(IEC60870_StepPositionWithCP24Time2a_t), StepPositionWithCP24Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 6(带CP24步位置信息)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_StepPositionWithCP24Time2a_Create(IEC60870_StepPositionWithCP24Time2a_t *self, + uint32_t ioa, int value, int is_transient, int quality, const IEC60870_CP24Time2a_t *time) +{ + IEC60870_StepPositionWithCP24Time2a_t result; + uint8_t payload[5]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (IEC60870_StepPositionInformation_Create(&result.base, ioa, value, is_transient, quality) != IEC60870_ASDU_OK) { return IEC60870_ASDU_ERROR; } + result.time = *time; + Initialize_Object(&result.base.object, ioa, &StepPositionWithCP24Time2a_VTable); + if (!StepPositionWithCP24Time2a_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 6(带CP24步位置信息)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_StepPositionWithCP24Time2a_Get_From_Buffer(IEC60870_StepPositionWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_StepPositionWithCP24Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 5U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!StepPositionWithCP24Time2a_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.base.object, ioa, &StepPositionWithCP24Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 8:带CP24比特串监视 ---- */ +/** + * 功能:编码带CP24比特串监视的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少8字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:基础值(5) + CP24(3);失败只影响临时区。 + */ +static int Bitstring32WithCP24Time2a_Encode_Value(const IEC60870_Bitstring32WithCP24Time2a_t *self, uint8_t *payload) +{ + return BitString32_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP24(&self->time, payload + 5, IEC60870_CP24_LEN); +} + +/** + * 功能:解码带CP24比特串监视的值字段。 + * 输入参数:payload——已校验完整长度的8字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:基础值(5) + CP24(3);公开入口在成功后统一提交。 + */ +static int Bitstring32WithCP24Time2a_Decode_Value(const uint8_t *payload, IEC60870_Bitstring32WithCP24Time2a_t *self) +{ + return BitString32_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP24(payload + 5, IEC60870_CP24_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到Bitstring32WithCP24Time2a编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int Bitstring32WithCP24Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return Bitstring32WithCP24Time2a_Encode_Value((const IEC60870_Bitstring32WithCP24Time2a_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable Bitstring32WithCP24Time2a_VTable = { + 8, 8, 0, sizeof(IEC60870_Bitstring32WithCP24Time2a_t), Bitstring32WithCP24Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 8(带CP24比特串监视)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP24Time2a_Create(IEC60870_Bitstring32WithCP24Time2a_t *self, + uint32_t ioa, uint32_t value, int quality, const IEC60870_CP24Time2a_t *time) +{ + IEC60870_Bitstring32WithCP24Time2a_t result; + uint8_t payload[8]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (IEC60870_BitString32_Create(&result.base, ioa, value, quality) != IEC60870_ASDU_OK) { return IEC60870_ASDU_ERROR; } + result.time = *time; + Initialize_Object(&result.base.object, ioa, &Bitstring32WithCP24Time2a_VTable); + if (!Bitstring32WithCP24Time2a_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 8(带CP24比特串监视)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP24Time2a_Get_From_Buffer(IEC60870_Bitstring32WithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_Bitstring32WithCP24Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 8U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!Bitstring32WithCP24Time2a_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.base.object, ioa, &Bitstring32WithCP24Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 10:带CP24归一化测量值 ---- */ +/** + * 功能:编码带CP24归一化测量值的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少6字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:基础值(3) + CP24(3);失败只影响临时区。 + */ +static int MeasuredValueNormalizedWithCP24Time2a_Encode_Value(const IEC60870_MeasuredValueNormalizedWithCP24Time2a_t *self, uint8_t *payload) +{ + return MeasuredValueNormalized_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP24(&self->time, payload + 3, IEC60870_CP24_LEN); +} + +/** + * 功能:解码带CP24归一化测量值的值字段。 + * 输入参数:payload——已校验完整长度的6字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:基础值(3) + CP24(3);公开入口在成功后统一提交。 + */ +static int MeasuredValueNormalizedWithCP24Time2a_Decode_Value(const uint8_t *payload, IEC60870_MeasuredValueNormalizedWithCP24Time2a_t *self) +{ + return MeasuredValueNormalized_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP24(payload + 3, IEC60870_CP24_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到MeasuredValueNormalizedWithCP24Time2a编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int MeasuredValueNormalizedWithCP24Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return MeasuredValueNormalizedWithCP24Time2a_Encode_Value((const IEC60870_MeasuredValueNormalizedWithCP24Time2a_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable MeasuredValueNormalizedWithCP24Time2a_VTable = { + 10, 6, 0, sizeof(IEC60870_MeasuredValueNormalizedWithCP24Time2a_t), MeasuredValueNormalizedWithCP24Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 10(带CP24归一化测量值)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP24Time2a_Create(IEC60870_MeasuredValueNormalizedWithCP24Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP24Time2a_t *time) +{ + IEC60870_MeasuredValueNormalizedWithCP24Time2a_t result; + uint8_t payload[6]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (IEC60870_MeasuredValueNormalized_Create(&result.base, ioa, value, quality) != IEC60870_ASDU_OK) { return IEC60870_ASDU_ERROR; } + result.time = *time; + Initialize_Object(&result.base.object, ioa, &MeasuredValueNormalizedWithCP24Time2a_VTable); + if (!MeasuredValueNormalizedWithCP24Time2a_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 10(带CP24归一化测量值)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_MeasuredValueNormalizedWithCP24Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 6U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!MeasuredValueNormalizedWithCP24Time2a_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.base.object, ioa, &MeasuredValueNormalizedWithCP24Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 12:带CP24标度化测量值 ---- */ +/** + * 功能:编码带CP24标度化测量值的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少6字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:基础值(3) + CP24(3);失败只影响临时区。 + */ +static int MeasuredValueScaledWithCP24Time2a_Encode_Value(const IEC60870_MeasuredValueScaledWithCP24Time2a_t *self, uint8_t *payload) +{ + return MeasuredValueScaled_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP24(&self->time, payload + 3, IEC60870_CP24_LEN); +} + +/** + * 功能:解码带CP24标度化测量值的值字段。 + * 输入参数:payload——已校验完整长度的6字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:基础值(3) + CP24(3);公开入口在成功后统一提交。 + */ +static int MeasuredValueScaledWithCP24Time2a_Decode_Value(const uint8_t *payload, IEC60870_MeasuredValueScaledWithCP24Time2a_t *self) +{ + return MeasuredValueScaled_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP24(payload + 3, IEC60870_CP24_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到MeasuredValueScaledWithCP24Time2a编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int MeasuredValueScaledWithCP24Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return MeasuredValueScaledWithCP24Time2a_Encode_Value((const IEC60870_MeasuredValueScaledWithCP24Time2a_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable MeasuredValueScaledWithCP24Time2a_VTable = { + 12, 6, 0, sizeof(IEC60870_MeasuredValueScaledWithCP24Time2a_t), MeasuredValueScaledWithCP24Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 12(带CP24标度化测量值)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP24Time2a_Create(IEC60870_MeasuredValueScaledWithCP24Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP24Time2a_t *time) +{ + IEC60870_MeasuredValueScaledWithCP24Time2a_t result; + uint8_t payload[6]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (IEC60870_MeasuredValueScaled_Create(&result.base, ioa, value, quality) != IEC60870_ASDU_OK) { return IEC60870_ASDU_ERROR; } + result.time = *time; + Initialize_Object(&result.base.object, ioa, &MeasuredValueScaledWithCP24Time2a_VTable); + if (!MeasuredValueScaledWithCP24Time2a_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 12(带CP24标度化测量值)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueScaledWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_MeasuredValueScaledWithCP24Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 6U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!MeasuredValueScaledWithCP24Time2a_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.base.object, ioa, &MeasuredValueScaledWithCP24Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 14:带CP24短浮点测量值 ---- */ +/** + * 功能:编码带CP24短浮点测量值的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少8字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:基础值(5) + CP24(3);失败只影响临时区。 + */ +static int MeasuredValueShortWithCP24Time2a_Encode_Value(const IEC60870_MeasuredValueShortWithCP24Time2a_t *self, uint8_t *payload) +{ + return MeasuredValueShort_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP24(&self->time, payload + 5, IEC60870_CP24_LEN); +} + +/** + * 功能:解码带CP24短浮点测量值的值字段。 + * 输入参数:payload——已校验完整长度的8字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:基础值(5) + CP24(3);公开入口在成功后统一提交。 + */ +static int MeasuredValueShortWithCP24Time2a_Decode_Value(const uint8_t *payload, IEC60870_MeasuredValueShortWithCP24Time2a_t *self) +{ + return MeasuredValueShort_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP24(payload + 5, IEC60870_CP24_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到MeasuredValueShortWithCP24Time2a编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int MeasuredValueShortWithCP24Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return MeasuredValueShortWithCP24Time2a_Encode_Value((const IEC60870_MeasuredValueShortWithCP24Time2a_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable MeasuredValueShortWithCP24Time2a_VTable = { + 14, 8, 0, sizeof(IEC60870_MeasuredValueShortWithCP24Time2a_t), MeasuredValueShortWithCP24Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 14(带CP24短浮点测量值)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP24Time2a_Create(IEC60870_MeasuredValueShortWithCP24Time2a_t *self, + uint32_t ioa, float value, int quality, const IEC60870_CP24Time2a_t *time) +{ + IEC60870_MeasuredValueShortWithCP24Time2a_t result; + uint8_t payload[8]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (IEC60870_MeasuredValueShort_Create(&result.base, ioa, value, quality) != IEC60870_ASDU_OK) { return IEC60870_ASDU_ERROR; } + result.time = *time; + Initialize_Object(&result.base.object, ioa, &MeasuredValueShortWithCP24Time2a_VTable); + if (!MeasuredValueShortWithCP24Time2a_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 14(带CP24短浮点测量值)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueShortWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_MeasuredValueShortWithCP24Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 8U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!MeasuredValueShortWithCP24Time2a_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.base.object, ioa, &MeasuredValueShortWithCP24Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 16:带CP24累计量 ---- */ +/** + * 功能:编码带CP24累计量的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少8字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:基础值(5) + CP24(3);失败只影响临时区。 + */ +static int IntegratedTotalsWithCP24Time2a_Encode_Value(const IEC60870_IntegratedTotalsWithCP24Time2a_t *self, uint8_t *payload) +{ + return IntegratedTotals_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP24(&self->time, payload + 5, IEC60870_CP24_LEN); +} + +/** + * 功能:解码带CP24累计量的值字段。 + * 输入参数:payload——已校验完整长度的8字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:基础值(5) + CP24(3);公开入口在成功后统一提交。 + */ +static int IntegratedTotalsWithCP24Time2a_Decode_Value(const uint8_t *payload, IEC60870_IntegratedTotalsWithCP24Time2a_t *self) +{ + return IntegratedTotals_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP24(payload + 5, IEC60870_CP24_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到IntegratedTotalsWithCP24Time2a编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int IntegratedTotalsWithCP24Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return IntegratedTotalsWithCP24Time2a_Encode_Value((const IEC60870_IntegratedTotalsWithCP24Time2a_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable IntegratedTotalsWithCP24Time2a_VTable = { + 16, 8, 0, sizeof(IEC60870_IntegratedTotalsWithCP24Time2a_t), IntegratedTotalsWithCP24Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 16(带CP24累计量)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP24Time2a_Create(IEC60870_IntegratedTotalsWithCP24Time2a_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value, const IEC60870_CP24Time2a_t *time) +{ + IEC60870_IntegratedTotalsWithCP24Time2a_t result; + uint8_t payload[8]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (IEC60870_IntegratedTotals_Create(&result.base, ioa, value) != IEC60870_ASDU_OK) { return IEC60870_ASDU_ERROR; } + result.time = *time; + Initialize_Object(&result.base.object, ioa, &IntegratedTotalsWithCP24Time2a_VTable); + if (!IntegratedTotalsWithCP24Time2a_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 16(带CP24累计量)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP24Time2a_Get_From_Buffer(IEC60870_IntegratedTotalsWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_IntegratedTotalsWithCP24Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 8U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!IntegratedTotalsWithCP24Time2a_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.base.object, ioa, &IntegratedTotalsWithCP24Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 32:带CP56步位置信息 ---- */ +/** + * 功能:编码带CP56步位置信息的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少9字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:基础值(2) + CP56(7);失败只影响临时区。 + */ +static int StepPositionWithCP56Time2a_Encode_Value(const IEC60870_StepPositionWithCP56Time2a_t *self, uint8_t *payload) +{ + return StepPositionInformation_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP56(&self->time, payload + 2, IEC60870_CP56_LEN); +} + +/** + * 功能:解码带CP56步位置信息的值字段。 + * 输入参数:payload——已校验完整长度的9字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:基础值(2) + CP56(7);公开入口在成功后统一提交。 + */ +static int StepPositionWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_StepPositionWithCP56Time2a_t *self) +{ + return StepPositionInformation_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP56(payload + 2, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到StepPositionWithCP56Time2a编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int StepPositionWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return StepPositionWithCP56Time2a_Encode_Value((const IEC60870_StepPositionWithCP56Time2a_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable StepPositionWithCP56Time2a_VTable = { + 32, 9, 0, sizeof(IEC60870_StepPositionWithCP56Time2a_t), StepPositionWithCP56Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 32(带CP56步位置信息)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_StepPositionWithCP56Time2a_Create(IEC60870_StepPositionWithCP56Time2a_t *self, + uint32_t ioa, int value, int is_transient, int quality, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_StepPositionWithCP56Time2a_t result; + uint8_t payload[9]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (IEC60870_StepPositionInformation_Create(&result.base, ioa, value, is_transient, quality) != IEC60870_ASDU_OK) { return IEC60870_ASDU_ERROR; } + result.time = *time; + Initialize_Object(&result.base.object, ioa, &StepPositionWithCP56Time2a_VTable); + if (!StepPositionWithCP56Time2a_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 32(带CP56步位置信息)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_StepPositionWithCP56Time2a_Get_From_Buffer(IEC60870_StepPositionWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_StepPositionWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 9U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!StepPositionWithCP56Time2a_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.base.object, ioa, &StepPositionWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 33:带CP56比特串监视 ---- */ +/** + * 功能:编码带CP56比特串监视的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少12字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:基础值(5) + CP56(7);失败只影响临时区。 + */ +static int Bitstring32WithCP56Time2a_Encode_Value(const IEC60870_Bitstring32WithCP56Time2a_t *self, uint8_t *payload) +{ + return BitString32_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP56(&self->time, payload + 5, IEC60870_CP56_LEN); +} + +/** + * 功能:解码带CP56比特串监视的值字段。 + * 输入参数:payload——已校验完整长度的12字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:基础值(5) + CP56(7);公开入口在成功后统一提交。 + */ +static int Bitstring32WithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_Bitstring32WithCP56Time2a_t *self) +{ + return BitString32_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP56(payload + 5, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到Bitstring32WithCP56Time2a编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int Bitstring32WithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return Bitstring32WithCP56Time2a_Encode_Value((const IEC60870_Bitstring32WithCP56Time2a_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable Bitstring32WithCP56Time2a_VTable = { + 33, 12, 0, sizeof(IEC60870_Bitstring32WithCP56Time2a_t), Bitstring32WithCP56Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 33(带CP56比特串监视)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP56Time2a_Create(IEC60870_Bitstring32WithCP56Time2a_t *self, + uint32_t ioa, uint32_t value, int quality, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_Bitstring32WithCP56Time2a_t result; + uint8_t payload[12]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (IEC60870_BitString32_Create(&result.base, ioa, value, quality) != IEC60870_ASDU_OK) { return IEC60870_ASDU_ERROR; } + result.time = *time; + Initialize_Object(&result.base.object, ioa, &Bitstring32WithCP56Time2a_VTable); + if (!Bitstring32WithCP56Time2a_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 33(带CP56比特串监视)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP56Time2a_Get_From_Buffer(IEC60870_Bitstring32WithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_Bitstring32WithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 12U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!Bitstring32WithCP56Time2a_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.base.object, ioa, &Bitstring32WithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 17:带CP24保护事件 ---- */ +/** + * 功能:编码带CP24保护事件的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少6字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:SEP(1:ES低2位,QDP高5位) + CP16(2) + CP24(3);失败只影响临时区。 + */ +static int EventOfProtectionEquipment_Encode_Value(const IEC60870_EventOfProtectionEquipment_t *self, uint8_t *payload) +{ + if (self->event > 3U || (self->quality & 0x07U)) { return 0; } + payload[0] = (uint8_t)(self->event | self->quality); + return IEC60870_Time_To_CP16(&self->elapsed_time, payload + 1, IEC60870_CP16_LEN) && + IEC60870_Time_To_CP24(&self->time, payload + 3, IEC60870_CP24_LEN); +} + +/** + * 功能:解码带CP24保护事件的值字段。 + * 输入参数:payload——已校验完整长度的6字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:SEP(1:ES低2位,QDP高5位) + CP16(2) + CP24(3);公开入口在成功后统一提交。 + */ +static int EventOfProtectionEquipment_Decode_Value(const uint8_t *payload, IEC60870_EventOfProtectionEquipment_t *self) +{ + if (payload[0] & 0x04U) { return 0; } + self->event = payload[0] & 0x03U; + self->quality = payload[0] & 0xF8U; + return IEC60870_Time_From_CP16(payload + 1, IEC60870_CP16_LEN, &self->elapsed_time) && + IEC60870_Time_From_CP24(payload + 3, IEC60870_CP24_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到EventOfProtectionEquipment编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int EventOfProtectionEquipment_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return EventOfProtectionEquipment_Encode_Value((const IEC60870_EventOfProtectionEquipment_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable EventOfProtectionEquipment_VTable = { + 17, 6, 0, sizeof(IEC60870_EventOfProtectionEquipment_t), EventOfProtectionEquipment_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 17(带CP24保护事件)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipment_Create(IEC60870_EventOfProtectionEquipment_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time) +{ + IEC60870_EventOfProtectionEquipment_t result; + uint8_t payload[6]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (event < 0 || event > 3) || + (quality < 0 || quality > 255) || + (elapsed_time == NULL) || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.event = (uint8_t)event; + result.quality = (uint8_t)quality; + result.elapsed_time = *elapsed_time; + result.time = *time; + Initialize_Object(&result.object, ioa, &EventOfProtectionEquipment_VTable); + if (!EventOfProtectionEquipment_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 17(带CP24保护事件)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipment_Get_From_Buffer(IEC60870_EventOfProtectionEquipment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_EventOfProtectionEquipment_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 6U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!EventOfProtectionEquipment_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &EventOfProtectionEquipment_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 18:带CP24成组保护启动事件 ---- */ +/** + * 功能:编码带CP24成组保护启动事件的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少7字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:事件(1) + QDP(1) + CP16(2) + CP24(3);失败只影响临时区。 + */ +static int PackedStartEventsOfProtectionEquipment_Encode_Value(const IEC60870_PackedStartEventsOfProtectionEquipment_t *self, uint8_t *payload) +{ + if (self->event > 63U || (self->quality & 0x07U)) { return 0; } + payload[0] = self->event; + payload[1] = self->quality; + return IEC60870_Time_To_CP16(&self->elapsed_time, payload + 2, IEC60870_CP16_LEN) && + IEC60870_Time_To_CP24(&self->time, payload + 4, IEC60870_CP24_LEN); +} + +/** + * 功能:解码带CP24成组保护启动事件的值字段。 + * 输入参数:payload——已校验完整长度的7字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:事件(1) + QDP(1) + CP16(2) + CP24(3);公开入口在成功后统一提交。 + */ +static int PackedStartEventsOfProtectionEquipment_Decode_Value(const uint8_t *payload, IEC60870_PackedStartEventsOfProtectionEquipment_t *self) +{ + if (payload[0] > 63U || (payload[1] & 0x07U)) { return 0; } + self->event = payload[0]; + self->quality = payload[1]; + return IEC60870_Time_From_CP16(payload + 2, IEC60870_CP16_LEN, &self->elapsed_time) && + IEC60870_Time_From_CP24(payload + 4, IEC60870_CP24_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到PackedStartEventsOfProtectionEquipment编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int PackedStartEventsOfProtectionEquipment_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return PackedStartEventsOfProtectionEquipment_Encode_Value((const IEC60870_PackedStartEventsOfProtectionEquipment_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable PackedStartEventsOfProtectionEquipment_VTable = { + 18, 7, 0, sizeof(IEC60870_PackedStartEventsOfProtectionEquipment_t), PackedStartEventsOfProtectionEquipment_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 18(带CP24成组保护启动事件)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipment_Create(IEC60870_PackedStartEventsOfProtectionEquipment_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time) +{ + IEC60870_PackedStartEventsOfProtectionEquipment_t result; + uint8_t payload[7]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (event < 0 || event > 63) || + (quality < 0 || quality > 255) || + (elapsed_time == NULL) || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.event = (uint8_t)event; + result.quality = (uint8_t)quality; + result.elapsed_time = *elapsed_time; + result.time = *time; + Initialize_Object(&result.object, ioa, &PackedStartEventsOfProtectionEquipment_VTable); + if (!PackedStartEventsOfProtectionEquipment_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 18(带CP24成组保护启动事件)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipment_Get_From_Buffer(IEC60870_PackedStartEventsOfProtectionEquipment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_PackedStartEventsOfProtectionEquipment_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 7U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!PackedStartEventsOfProtectionEquipment_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &PackedStartEventsOfProtectionEquipment_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 19:带CP24保护输出电路信息 ---- */ +/** + * 功能:编码带CP24保护输出电路信息的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少7字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:事件(1) + QDP(1) + CP16(2) + CP24(3);失败只影响临时区。 + */ +static int PackedOutputCircuitInfo_Encode_Value(const IEC60870_PackedOutputCircuitInfo_t *self, uint8_t *payload) +{ + if (self->event > 15U || (self->quality & 0x07U)) { return 0; } + payload[0] = self->event; + payload[1] = self->quality; + return IEC60870_Time_To_CP16(&self->elapsed_time, payload + 2, IEC60870_CP16_LEN) && + IEC60870_Time_To_CP24(&self->time, payload + 4, IEC60870_CP24_LEN); +} + +/** + * 功能:解码带CP24保护输出电路信息的值字段。 + * 输入参数:payload——已校验完整长度的7字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:事件(1) + QDP(1) + CP16(2) + CP24(3);公开入口在成功后统一提交。 + */ +static int PackedOutputCircuitInfo_Decode_Value(const uint8_t *payload, IEC60870_PackedOutputCircuitInfo_t *self) +{ + if (payload[0] > 15U || (payload[1] & 0x07U)) { return 0; } + self->event = payload[0]; + self->quality = payload[1]; + return IEC60870_Time_From_CP16(payload + 2, IEC60870_CP16_LEN, &self->elapsed_time) && + IEC60870_Time_From_CP24(payload + 4, IEC60870_CP24_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到PackedOutputCircuitInfo编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int PackedOutputCircuitInfo_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return PackedOutputCircuitInfo_Encode_Value((const IEC60870_PackedOutputCircuitInfo_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable PackedOutputCircuitInfo_VTable = { + 19, 7, 0, sizeof(IEC60870_PackedOutputCircuitInfo_t), PackedOutputCircuitInfo_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 19(带CP24保护输出电路信息)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfo_Create(IEC60870_PackedOutputCircuitInfo_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time) +{ + IEC60870_PackedOutputCircuitInfo_t result; + uint8_t payload[7]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (event < 0 || event > 15) || + (quality < 0 || quality > 255) || + (elapsed_time == NULL) || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.event = (uint8_t)event; + result.quality = (uint8_t)quality; + result.elapsed_time = *elapsed_time; + result.time = *time; + Initialize_Object(&result.object, ioa, &PackedOutputCircuitInfo_VTable); + if (!PackedOutputCircuitInfo_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 19(带CP24保护输出电路信息)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfo_Get_From_Buffer(IEC60870_PackedOutputCircuitInfo_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_PackedOutputCircuitInfo_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 7U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!PackedOutputCircuitInfo_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &PackedOutputCircuitInfo_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 38:带CP56保护事件 ---- */ +/** + * 功能:编码带CP56保护事件的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少10字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:SEP(1:ES低2位,QDP高5位) + CP16(2) + CP56(7);失败只影响临时区。 + */ +static int EventOfProtectionEquipmentWithCP56Time2a_Encode_Value(const IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t *self, uint8_t *payload) +{ + if (self->event > 3U || (self->quality & 0x07U)) { return 0; } + payload[0] = (uint8_t)(self->event | self->quality); + return IEC60870_Time_To_CP16(&self->elapsed_time, payload + 1, IEC60870_CP16_LEN) && + IEC60870_Time_To_CP56(&self->time, payload + 3, IEC60870_CP56_LEN); +} + +/** + * 功能:解码带CP56保护事件的值字段。 + * 输入参数:payload——已校验完整长度的10字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:SEP(1:ES低2位,QDP高5位) + CP16(2) + CP56(7);公开入口在成功后统一提交。 + */ +static int EventOfProtectionEquipmentWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t *self) +{ + if (payload[0] & 0x04U) { return 0; } + self->event = payload[0] & 0x03U; + self->quality = payload[0] & 0xF8U; + return IEC60870_Time_From_CP16(payload + 1, IEC60870_CP16_LEN, &self->elapsed_time) && + IEC60870_Time_From_CP56(payload + 3, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到EventOfProtectionEquipmentWithCP56Time2a编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int EventOfProtectionEquipmentWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return EventOfProtectionEquipmentWithCP56Time2a_Encode_Value((const IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable EventOfProtectionEquipmentWithCP56Time2a_VTable = { + 38, 10, 0, sizeof(IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t), EventOfProtectionEquipmentWithCP56Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 38(带CP56保护事件)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Create(IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t result; + uint8_t payload[10]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (event < 0 || event > 3) || + (quality < 0 || quality > 255) || + (elapsed_time == NULL) || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.event = (uint8_t)event; + result.quality = (uint8_t)quality; + result.elapsed_time = *elapsed_time; + result.time = *time; + Initialize_Object(&result.object, ioa, &EventOfProtectionEquipmentWithCP56Time2a_VTable); + if (!EventOfProtectionEquipmentWithCP56Time2a_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 38(带CP56保护事件)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer(IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 10U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!EventOfProtectionEquipmentWithCP56Time2a_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &EventOfProtectionEquipmentWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 39:带CP56成组保护启动事件 ---- */ +/** + * 功能:编码带CP56成组保护启动事件的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少11字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:事件(1) + QDP(1) + CP16(2) + CP56(7);失败只影响临时区。 + */ +static int PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Encode_Value(const IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t *self, uint8_t *payload) +{ + if (self->event > 63U || (self->quality & 0x07U)) { return 0; } + payload[0] = self->event; + payload[1] = self->quality; + return IEC60870_Time_To_CP16(&self->elapsed_time, payload + 2, IEC60870_CP16_LEN) && + IEC60870_Time_To_CP56(&self->time, payload + 4, IEC60870_CP56_LEN); +} + +/** + * 功能:解码带CP56成组保护启动事件的值字段。 + * 输入参数:payload——已校验完整长度的11字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:事件(1) + QDP(1) + CP16(2) + CP56(7);公开入口在成功后统一提交。 + */ +static int PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t *self) +{ + if (payload[0] > 63U || (payload[1] & 0x07U)) { return 0; } + self->event = payload[0]; + self->quality = payload[1]; + return IEC60870_Time_From_CP16(payload + 2, IEC60870_CP16_LEN, &self->elapsed_time) && + IEC60870_Time_From_CP56(payload + 4, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到PackedStartEventsOfProtectionEquipmentWithCP56Time2a编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Encode_Value((const IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable PackedStartEventsOfProtectionEquipmentWithCP56Time2a_VTable = { + 39, 11, 0, sizeof(IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t), PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 39(带CP56成组保护启动事件)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Create(IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t result; + uint8_t payload[11]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (event < 0 || event > 63) || + (quality < 0 || quality > 255) || + (elapsed_time == NULL) || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.event = (uint8_t)event; + result.quality = (uint8_t)quality; + result.elapsed_time = *elapsed_time; + result.time = *time; + Initialize_Object(&result.object, ioa, &PackedStartEventsOfProtectionEquipmentWithCP56Time2a_VTable); + if (!PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 39(带CP56成组保护启动事件)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer(IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 11U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &PackedStartEventsOfProtectionEquipmentWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 40:带CP56保护输出电路信息 ---- */ +/** + * 功能:编码带CP56保护输出电路信息的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少11字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:事件(1) + QDP(1) + CP16(2) + CP56(7);失败只影响临时区。 + */ +static int PackedOutputCircuitInfoWithCP56Time2a_Encode_Value(const IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t *self, uint8_t *payload) +{ + if (self->event > 15U || (self->quality & 0x07U)) { return 0; } + payload[0] = self->event; + payload[1] = self->quality; + return IEC60870_Time_To_CP16(&self->elapsed_time, payload + 2, IEC60870_CP16_LEN) && + IEC60870_Time_To_CP56(&self->time, payload + 4, IEC60870_CP56_LEN); +} + +/** + * 功能:解码带CP56保护输出电路信息的值字段。 + * 输入参数:payload——已校验完整长度的11字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:事件(1) + QDP(1) + CP16(2) + CP56(7);公开入口在成功后统一提交。 + */ +static int PackedOutputCircuitInfoWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t *self) +{ + if (payload[0] > 15U || (payload[1] & 0x07U)) { return 0; } + self->event = payload[0]; + self->quality = payload[1]; + return IEC60870_Time_From_CP16(payload + 2, IEC60870_CP16_LEN, &self->elapsed_time) && + IEC60870_Time_From_CP56(payload + 4, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到PackedOutputCircuitInfoWithCP56Time2a编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int PackedOutputCircuitInfoWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return PackedOutputCircuitInfoWithCP56Time2a_Encode_Value((const IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable PackedOutputCircuitInfoWithCP56Time2a_VTable = { + 40, 11, 0, sizeof(IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t), PackedOutputCircuitInfoWithCP56Time2a_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 40(带CP56保护输出电路信息)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Create(IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t result; + uint8_t payload[11]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (event < 0 || event > 15) || + (quality < 0 || quality > 255) || + (elapsed_time == NULL) || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.event = (uint8_t)event; + result.quality = (uint8_t)quality; + result.elapsed_time = *elapsed_time; + result.time = *time; + Initialize_Object(&result.object, ioa, &PackedOutputCircuitInfoWithCP56Time2a_VTable); + if (!PackedOutputCircuitInfoWithCP56Time2a_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 40(带CP56保护输出电路信息)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Get_From_Buffer(IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 11U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!PackedOutputCircuitInfoWithCP56Time2a_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &PackedOutputCircuitInfoWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 20:带变位检测的成组单点 ---- */ +/** + * 功能:编码带变位检测的成组单点的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少5字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:SCD(4:状态16位 + 变位16位) + QDS(1);失败只影响临时区。 + */ +static int PackedSinglePointWithSCD_Encode_Value(const IEC60870_PackedSinglePointWithSCD_t *self, uint8_t *payload) +{ + if (self->quality & 0x0EU) { return 0; } + IEC60870_Object_Write_LE(payload + 0, self->status, 2); + IEC60870_Object_Write_LE(payload + 2, self->change, 2); + payload[4] = self->quality; + return 1; +} + +/** + * 功能:解码带变位检测的成组单点的值字段。 + * 输入参数:payload——已校验完整长度的5字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:SCD(4:状态16位 + 变位16位) + QDS(1);公开入口在成功后统一提交。 + */ +static int PackedSinglePointWithSCD_Decode_Value(const uint8_t *payload, IEC60870_PackedSinglePointWithSCD_t *self) +{ + if (payload[4] & 0x0EU) { return 0; } + self->status = (uint16_t)IEC60870_Object_Read_LE(payload + 0, 2); + self->change = (uint16_t)IEC60870_Object_Read_LE(payload + 2, 2); + self->quality = payload[4]; + return 1; +} + +/** + * 功能:将公共基类分派到PackedSinglePointWithSCD编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int PackedSinglePointWithSCD_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return PackedSinglePointWithSCD_Encode_Value((const IEC60870_PackedSinglePointWithSCD_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable PackedSinglePointWithSCD_VTable = { + 20, 5, 0, sizeof(IEC60870_PackedSinglePointWithSCD_t), PackedSinglePointWithSCD_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 20(带变位检测的成组单点)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_PackedSinglePointWithSCD_Create(IEC60870_PackedSinglePointWithSCD_t *self, + uint32_t ioa, uint16_t status, uint16_t change, int quality) +{ + IEC60870_PackedSinglePointWithSCD_t result; + uint8_t payload[5]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (quality < 0 || quality > 255)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.status = status; + result.change = change; + result.quality = (uint8_t)quality; + Initialize_Object(&result.object, ioa, &PackedSinglePointWithSCD_VTable); + if (!PackedSinglePointWithSCD_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 20(带变位检测的成组单点)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_PackedSinglePointWithSCD_Get_From_Buffer(IEC60870_PackedSinglePointWithSCD_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_PackedSinglePointWithSCD_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 5U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!PackedSinglePointWithSCD_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &PackedSinglePointWithSCD_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 21:无品质归一化测量值 ---- */ +/** + * 功能:编码无品质归一化测量值的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少2字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:NVA(2),不包含QDS;失败只影响临时区。 + */ +static int MeasuredValueNormalizedWithoutQuality_Encode_Value(const IEC60870_MeasuredValueNormalizedWithoutQuality_t *self, uint8_t *payload) +{ + IEC60870_Object_Write_LE(payload, (uint16_t)self->value, 2); + return 1; +} + +/** + * 功能:解码无品质归一化测量值的值字段。 + * 输入参数:payload——已校验完整长度的2字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:NVA(2),不包含QDS;公开入口在成功后统一提交。 + */ +static int MeasuredValueNormalizedWithoutQuality_Decode_Value(const uint8_t *payload, IEC60870_MeasuredValueNormalizedWithoutQuality_t *self) +{ + self->value = Read_I16(payload); + return 1; +} + +/** + * 功能:将公共基类分派到MeasuredValueNormalizedWithoutQuality编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int MeasuredValueNormalizedWithoutQuality_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return MeasuredValueNormalizedWithoutQuality_Encode_Value((const IEC60870_MeasuredValueNormalizedWithoutQuality_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable MeasuredValueNormalizedWithoutQuality_VTable = { + 21, 2, 0, sizeof(IEC60870_MeasuredValueNormalizedWithoutQuality_t), MeasuredValueNormalizedWithoutQuality_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 21(无品质归一化测量值)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithoutQuality_Create(IEC60870_MeasuredValueNormalizedWithoutQuality_t *self, + uint32_t ioa, int16_t value) +{ + IEC60870_MeasuredValueNormalizedWithoutQuality_t result; + uint8_t payload[2]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.value = value; + Initialize_Object(&result.object, ioa, &MeasuredValueNormalizedWithoutQuality_VTable); + if (!MeasuredValueNormalizedWithoutQuality_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 21(无品质归一化测量值)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithoutQuality_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithoutQuality_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_MeasuredValueNormalizedWithoutQuality_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 2U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!MeasuredValueNormalizedWithoutQuality_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &MeasuredValueNormalizedWithoutQuality_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 51:32位比特串命令 ---- */ +/** + * 功能:编码32位比特串命令的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少4字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:BSI(4),不包含QDS;失败只影响临时区。 + */ +static int Bitstring32Command_Encode_Value(const IEC60870_Bitstring32Command_t *self, uint8_t *payload) +{ + IEC60870_Object_Write_LE(payload + 0, self->value, 4); + return 1; +} + +/** + * 功能:解码32位比特串命令的值字段。 + * 输入参数:payload——已校验完整长度的4字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:BSI(4),不包含QDS;公开入口在成功后统一提交。 + */ +static int Bitstring32Command_Decode_Value(const uint8_t *payload, IEC60870_Bitstring32Command_t *self) +{ + self->value = (uint32_t)IEC60870_Object_Read_LE(payload + 0, 4); + return 1; +} + +/** + * 功能:将公共基类分派到Bitstring32Command编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int Bitstring32Command_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return Bitstring32Command_Encode_Value((const IEC60870_Bitstring32Command_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable Bitstring32Command_VTable = { + 51, 4, 1, sizeof(IEC60870_Bitstring32Command_t), Bitstring32Command_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 51(32位比特串命令)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_Bitstring32Command_Create(IEC60870_Bitstring32Command_t *self, + uint32_t ioa, uint32_t value) +{ + IEC60870_Bitstring32Command_t result; + uint8_t payload[4]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.value = value; + Initialize_Object(&result.object, ioa, &Bitstring32Command_VTable); + if (!Bitstring32Command_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 51(32位比特串命令)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_Bitstring32Command_Get_From_Buffer(IEC60870_Bitstring32Command_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_Bitstring32Command_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 4U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!Bitstring32Command_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &Bitstring32Command_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 64:带CP56比特串命令 ---- */ +/** + * 功能:编码带CP56比特串命令的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少11字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:基础值(4) + CP56(7);失败只影响临时区。 + */ +static int Bitstring32CommandWithCP56Time2a_Encode_Value(const IEC60870_Bitstring32CommandWithCP56Time2a_t *self, uint8_t *payload) +{ + return Bitstring32Command_Encode_Value(&self->base, payload) && + IEC60870_Time_To_CP56(&self->time, payload + 4, IEC60870_CP56_LEN); +} + +/** + * 功能:解码带CP56比特串命令的值字段。 + * 输入参数:payload——已校验完整长度的11字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:基础值(4) + CP56(7);公开入口在成功后统一提交。 + */ +static int Bitstring32CommandWithCP56Time2a_Decode_Value(const uint8_t *payload, IEC60870_Bitstring32CommandWithCP56Time2a_t *self) +{ + return Bitstring32Command_Decode_Value(payload, &self->base) && + IEC60870_Time_From_CP56(payload + 4, IEC60870_CP56_LEN, &self->time); +} + +/** + * 功能:将公共基类分派到Bitstring32CommandWithCP56Time2a编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int Bitstring32CommandWithCP56Time2a_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return Bitstring32CommandWithCP56Time2a_Encode_Value((const IEC60870_Bitstring32CommandWithCP56Time2a_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable Bitstring32CommandWithCP56Time2a_VTable = { + 64, 11, 1, sizeof(IEC60870_Bitstring32CommandWithCP56Time2a_t), Bitstring32CommandWithCP56Time2a_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 64(带CP56比特串命令)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_Bitstring32CommandWithCP56Time2a_Create(IEC60870_Bitstring32CommandWithCP56Time2a_t *self, + uint32_t ioa, uint32_t value, const IEC60870_CP56Time2a_t *time) +{ + IEC60870_Bitstring32CommandWithCP56Time2a_t result; + uint8_t payload[11]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (IEC60870_Bitstring32Command_Create(&result.base, ioa, value) != IEC60870_ASDU_OK) { return IEC60870_ASDU_ERROR; } + result.time = *time; + Initialize_Object(&result.base.object, ioa, &Bitstring32CommandWithCP56Time2a_VTable); + if (!Bitstring32CommandWithCP56Time2a_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 64(带CP56比特串命令)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_Bitstring32CommandWithCP56Time2a_Get_From_Buffer(IEC60870_Bitstring32CommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_Bitstring32CommandWithCP56Time2a_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 11U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!Bitstring32CommandWithCP56Time2a_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.base.object, ioa, &Bitstring32CommandWithCP56Time2a_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 70:初始化结束 ---- */ +/** + * 功能:编码初始化结束的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少1字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:COI(1),地址适用性由应用判断;失败只影响临时区。 + */ +static int EndOfInitialization_Encode_Value(const IEC60870_EndOfInitialization_t *self, uint8_t *payload) +{ + payload[0] = self->cause; + return 1; +} + +/** + * 功能:解码初始化结束的值字段。 + * 输入参数:payload——已校验完整长度的1字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:COI(1),地址适用性由应用判断;公开入口在成功后统一提交。 + */ +static int EndOfInitialization_Decode_Value(const uint8_t *payload, IEC60870_EndOfInitialization_t *self) +{ + self->cause = payload[0]; + return 1; +} + +/** + * 功能:将公共基类分派到EndOfInitialization编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int EndOfInitialization_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return EndOfInitialization_Encode_Value((const IEC60870_EndOfInitialization_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable EndOfInitialization_VTable = { + 70, 1, 1, sizeof(IEC60870_EndOfInitialization_t), EndOfInitialization_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 70(初始化结束)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_EndOfInitialization_Create(IEC60870_EndOfInitialization_t *self, + uint32_t ioa, int cause) +{ + IEC60870_EndOfInitialization_t result; + uint8_t payload[1]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (cause < 0 || cause > 255)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.cause = (uint8_t)cause; + Initialize_Object(&result.object, ioa, &EndOfInitialization_VTable); + if (!EndOfInitialization_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 70(初始化结束)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_EndOfInitialization_Get_From_Buffer(IEC60870_EndOfInitialization_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_EndOfInitialization_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 1U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!EndOfInitialization_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &EndOfInitialization_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 104:无时标测试命令 ---- */ +/** + * 功能:编码无时标测试命令的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少2字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:固定测试图样AA 55;失败只影响临时区。 + */ +static int TestCommand_Encode_Value(const IEC60870_TestCommand_t *self, uint8_t *payload) +{ + (void)self; + payload[0] = 0xAAU; + payload[1] = 0x55U; + return 1; +} + +/** + * 功能:解码无时标测试命令的值字段。 + * 输入参数:payload——已校验完整长度的2字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:固定测试图样AA 55;公开入口在成功后统一提交。 + */ +static int TestCommand_Decode_Value(const uint8_t *payload, IEC60870_TestCommand_t *self) +{ + (void)self; + return payload[0] == 0xAAU && payload[1] == 0x55U; +} + +/** + * 功能:将公共基类分派到TestCommand编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int TestCommand_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return TestCommand_Encode_Value((const IEC60870_TestCommand_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable TestCommand_VTable = { + 104, 2, 1, sizeof(IEC60870_TestCommand_t), TestCommand_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 104(无时标测试命令)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_TestCommand_Create(IEC60870_TestCommand_t *self, + uint32_t ioa) +{ + IEC60870_TestCommand_t result; + uint8_t payload[2]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + Initialize_Object(&result.object, ioa, &TestCommand_VTable); + if (!TestCommand_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 104(无时标测试命令)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_TestCommand_Get_From_Buffer(IEC60870_TestCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_TestCommand_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 2U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!TestCommand_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &TestCommand_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 106:延时获得命令 ---- */ +/** + * 功能:编码延时获得命令的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少2字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:CP16(2);失败只影响临时区。 + */ +static int DelayAcquisitionCommand_Encode_Value(const IEC60870_DelayAcquisitionCommand_t *self, uint8_t *payload) +{ + return IEC60870_Time_To_CP16(&self->delay, payload, IEC60870_CP16_LEN); +} + +/** + * 功能:解码延时获得命令的值字段。 + * 输入参数:payload——已校验完整长度的2字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:CP16(2);公开入口在成功后统一提交。 + */ +static int DelayAcquisitionCommand_Decode_Value(const uint8_t *payload, IEC60870_DelayAcquisitionCommand_t *self) +{ + return IEC60870_Time_From_CP16(payload, IEC60870_CP16_LEN, &self->delay); +} + +/** + * 功能:将公共基类分派到DelayAcquisitionCommand编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int DelayAcquisitionCommand_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return DelayAcquisitionCommand_Encode_Value((const IEC60870_DelayAcquisitionCommand_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable DelayAcquisitionCommand_VTable = { + 106, 2, 1, sizeof(IEC60870_DelayAcquisitionCommand_t), DelayAcquisitionCommand_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 106(延时获得命令)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_DelayAcquisitionCommand_Create(IEC60870_DelayAcquisitionCommand_t *self, + uint32_t ioa, const IEC60870_CP16Time2a_t *delay) +{ + IEC60870_DelayAcquisitionCommand_t result; + uint8_t payload[2]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (delay == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.delay = *delay; + Initialize_Object(&result.object, ioa, &DelayAcquisitionCommand_VTable); + if (!DelayAcquisitionCommand_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 106(延时获得命令)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_DelayAcquisitionCommand_Get_From_Buffer(IEC60870_DelayAcquisitionCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_DelayAcquisitionCommand_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 2U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!DelayAcquisitionCommand_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &DelayAcquisitionCommand_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 110:归一化测量值参数 ---- */ +/** + * 功能:编码归一化测量值参数的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少3字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:参数值(2) + QPM(1),QPM不是QDS;失败只影响临时区。 + */ +static int ParameterNormalizedValue_Encode_Value(const IEC60870_ParameterNormalizedValue_t *self, uint8_t *payload) +{ + IEC60870_Object_Write_LE(payload, (uint16_t)self->value, 2); + payload[2] = self->qualifier; + return 1; +} + +/** + * 功能:解码归一化测量值参数的值字段。 + * 输入参数:payload——已校验完整长度的3字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:参数值(2) + QPM(1),QPM不是QDS;公开入口在成功后统一提交。 + */ +static int ParameterNormalizedValue_Decode_Value(const uint8_t *payload, IEC60870_ParameterNormalizedValue_t *self) +{ + self->value = Read_I16(payload); + self->qualifier = payload[2]; + return 1; +} + +/** + * 功能:将公共基类分派到ParameterNormalizedValue编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int ParameterNormalizedValue_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return ParameterNormalizedValue_Encode_Value((const IEC60870_ParameterNormalizedValue_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable ParameterNormalizedValue_VTable = { + 110, 3, 0, sizeof(IEC60870_ParameterNormalizedValue_t), ParameterNormalizedValue_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 110(归一化测量值参数)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_ParameterNormalizedValue_Create(IEC60870_ParameterNormalizedValue_t *self, + uint32_t ioa, int16_t value, int qualifier) +{ + IEC60870_ParameterNormalizedValue_t result; + uint8_t payload[3]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (qualifier < 0 || qualifier > 255)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.value = value; + result.qualifier = (uint8_t)qualifier; + Initialize_Object(&result.object, ioa, &ParameterNormalizedValue_VTable); + if (!ParameterNormalizedValue_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 110(归一化测量值参数)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_ParameterNormalizedValue_Get_From_Buffer(IEC60870_ParameterNormalizedValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_ParameterNormalizedValue_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 3U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!ParameterNormalizedValue_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &ParameterNormalizedValue_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 111:标度化测量值参数 ---- */ +/** + * 功能:编码标度化测量值参数的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少3字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:参数值(2) + QPM(1),QPM不是QDS;失败只影响临时区。 + */ +static int ParameterScaledValue_Encode_Value(const IEC60870_ParameterScaledValue_t *self, uint8_t *payload) +{ + IEC60870_Object_Write_LE(payload, (uint16_t)self->value, 2); + payload[2] = self->qualifier; + return 1; +} + +/** + * 功能:解码标度化测量值参数的值字段。 + * 输入参数:payload——已校验完整长度的3字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:参数值(2) + QPM(1),QPM不是QDS;公开入口在成功后统一提交。 + */ +static int ParameterScaledValue_Decode_Value(const uint8_t *payload, IEC60870_ParameterScaledValue_t *self) +{ + self->value = Read_I16(payload); + self->qualifier = payload[2]; + return 1; +} + +/** + * 功能:将公共基类分派到ParameterScaledValue编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int ParameterScaledValue_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return ParameterScaledValue_Encode_Value((const IEC60870_ParameterScaledValue_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable ParameterScaledValue_VTable = { + 111, 3, 0, sizeof(IEC60870_ParameterScaledValue_t), ParameterScaledValue_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 111(标度化测量值参数)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_ParameterScaledValue_Create(IEC60870_ParameterScaledValue_t *self, + uint32_t ioa, int16_t value, int qualifier) +{ + IEC60870_ParameterScaledValue_t result; + uint8_t payload[3]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (qualifier < 0 || qualifier > 255)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.value = value; + result.qualifier = (uint8_t)qualifier; + Initialize_Object(&result.object, ioa, &ParameterScaledValue_VTable); + if (!ParameterScaledValue_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 111(标度化测量值参数)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_ParameterScaledValue_Get_From_Buffer(IEC60870_ParameterScaledValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_ParameterScaledValue_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 3U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!ParameterScaledValue_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &ParameterScaledValue_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 112:短浮点测量值参数 ---- */ +/** + * 功能:编码短浮点测量值参数的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少5字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:参数值(4) + QPM(1),QPM不是QDS;失败只影响临时区。 + */ +static int ParameterFloatValue_Encode_Value(const IEC60870_ParameterFloatValue_t *self, uint8_t *payload) +{ + Write_Float(payload, &self->value); + payload[4] = self->qualifier; + return 1; +} + +/** + * 功能:解码短浮点测量值参数的值字段。 + * 输入参数:payload——已校验完整长度的5字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:参数值(4) + QPM(1),QPM不是QDS;公开入口在成功后统一提交。 + */ +static int ParameterFloatValue_Decode_Value(const uint8_t *payload, IEC60870_ParameterFloatValue_t *self) +{ + Read_Float(payload, &self->value); + self->qualifier = payload[4]; + return 1; +} + +/** + * 功能:将公共基类分派到ParameterFloatValue编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int ParameterFloatValue_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return ParameterFloatValue_Encode_Value((const IEC60870_ParameterFloatValue_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable ParameterFloatValue_VTable = { + 112, 5, 0, sizeof(IEC60870_ParameterFloatValue_t), ParameterFloatValue_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 112(短浮点测量值参数)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_ParameterFloatValue_Create(IEC60870_ParameterFloatValue_t *self, + uint32_t ioa, float value, int qualifier) +{ + IEC60870_ParameterFloatValue_t result; + uint8_t payload[5]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (qualifier < 0 || qualifier > 255)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.value = value; + result.qualifier = (uint8_t)qualifier; + Initialize_Object(&result.object, ioa, &ParameterFloatValue_VTable); + if (!ParameterFloatValue_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 112(短浮点测量值参数)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_ParameterFloatValue_Get_From_Buffer(IEC60870_ParameterFloatValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_ParameterFloatValue_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 5U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!ParameterFloatValue_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &ParameterFloatValue_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 113:参数激活 ---- */ +/** + * 功能:编码参数激活的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少1字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:QPA(1);失败只影响临时区。 + */ +static int ParameterActivation_Encode_Value(const IEC60870_ParameterActivation_t *self, uint8_t *payload) +{ + payload[0] = self->qualifier; + return 1; +} + +/** + * 功能:解码参数激活的值字段。 + * 输入参数:payload——已校验完整长度的1字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:QPA(1);公开入口在成功后统一提交。 + */ +static int ParameterActivation_Decode_Value(const uint8_t *payload, IEC60870_ParameterActivation_t *self) +{ + self->qualifier = payload[0]; + return 1; +} + +/** + * 功能:将公共基类分派到ParameterActivation编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int ParameterActivation_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return ParameterActivation_Encode_Value((const IEC60870_ParameterActivation_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable ParameterActivation_VTable = { + 113, 1, 0, sizeof(IEC60870_ParameterActivation_t), ParameterActivation_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 113(参数激活)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_ParameterActivation_Create(IEC60870_ParameterActivation_t *self, + uint32_t ioa, int qualifier) +{ + IEC60870_ParameterActivation_t result; + uint8_t payload[1]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (qualifier < 0 || qualifier > 255)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.qualifier = (uint8_t)qualifier; + Initialize_Object(&result.object, ioa, &ParameterActivation_VTable); + if (!ParameterActivation_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 113(参数激活)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_ParameterActivation_Get_From_Buffer(IEC60870_ParameterActivation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_ParameterActivation_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 1U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!ParameterActivation_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &ParameterActivation_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 120:文件准备就绪 ---- */ +/** + * 功能:编码文件准备就绪的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少6字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:NOF(2) + LOF(3) + FRQ(1);失败只影响临时区。 + */ +static int FileReady_Encode_Value(const IEC60870_FileReady_t *self, uint8_t *payload) +{ + if (self->file_length > UINT32_C(0xFFFFFF) || (self->qualifier & 0x7FU)) { return 0; } + IEC60870_Object_Write_LE(payload, self->file_name, 2); + IEC60870_Object_Write_LE(payload + 2, self->file_length, 3); + payload[5] = self->qualifier; + return 1; +} + +/** + * 功能:解码文件准备就绪的值字段。 + * 输入参数:payload——已校验完整长度的6字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:NOF(2) + LOF(3) + FRQ(1);公开入口在成功后统一提交。 + */ +static int FileReady_Decode_Value(const uint8_t *payload, IEC60870_FileReady_t *self) +{ + if (payload[5] & 0x7FU) { return 0; } + self->file_name = (uint16_t)IEC60870_Object_Read_LE(payload, 2); + self->file_length = IEC60870_Object_Read_LE(payload + 2, 3); + self->qualifier = payload[5]; + return 1; +} + +/** + * 功能:将公共基类分派到FileReady编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int FileReady_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return FileReady_Encode_Value((const IEC60870_FileReady_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable FileReady_VTable = { + 120, 6, 1, sizeof(IEC60870_FileReady_t), FileReady_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 120(文件准备就绪)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_FileReady_Create(IEC60870_FileReady_t *self, + uint32_t ioa, uint16_t file_name, uint32_t file_length, int qualifier) +{ + IEC60870_FileReady_t result; + uint8_t payload[6]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (file_length > UINT32_C(0xFFFFFF)) || + (qualifier < 0 || qualifier > 255)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.file_name = file_name; + result.file_length = file_length; + result.qualifier = (uint8_t)qualifier; + Initialize_Object(&result.object, ioa, &FileReady_VTable); + if (!FileReady_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 120(文件准备就绪)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_FileReady_Get_From_Buffer(IEC60870_FileReady_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_FileReady_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 6U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!FileReady_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &FileReady_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 121:节准备就绪 ---- */ +/** + * 功能:编码节准备就绪的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少7字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:NOF(2) + NOS(1) + LOS(3) + SRQ(1);失败只影响临时区。 + */ +static int SectionReady_Encode_Value(const IEC60870_SectionReady_t *self, uint8_t *payload) +{ + if (self->section_length > UINT32_C(0xFFFFFF) || (self->qualifier & 0x7FU)) { return 0; } + IEC60870_Object_Write_LE(payload, self->file_name, 2); + payload[2] = self->section; + IEC60870_Object_Write_LE(payload + 3, self->section_length, 3); + payload[6] = self->qualifier; + return 1; +} + +/** + * 功能:解码节准备就绪的值字段。 + * 输入参数:payload——已校验完整长度的7字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:NOF(2) + NOS(1) + LOS(3) + SRQ(1);公开入口在成功后统一提交。 + */ +static int SectionReady_Decode_Value(const uint8_t *payload, IEC60870_SectionReady_t *self) +{ + if (payload[6] & 0x7FU) { return 0; } + self->file_name = (uint16_t)IEC60870_Object_Read_LE(payload, 2); + self->section = payload[2]; + self->section_length = IEC60870_Object_Read_LE(payload + 3, 3); + self->qualifier = payload[6]; + return 1; +} + +/** + * 功能:将公共基类分派到SectionReady编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int SectionReady_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return SectionReady_Encode_Value((const IEC60870_SectionReady_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable SectionReady_VTable = { + 121, 7, 1, sizeof(IEC60870_SectionReady_t), SectionReady_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 121(节准备就绪)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_SectionReady_Create(IEC60870_SectionReady_t *self, + uint32_t ioa, uint16_t file_name, int section, uint32_t section_length, int qualifier) +{ + IEC60870_SectionReady_t result; + uint8_t payload[7]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (section < 0 || section > 255) || + (section_length > UINT32_C(0xFFFFFF)) || + (qualifier < 0 || qualifier > 255)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.file_name = file_name; + result.section = (uint8_t)section; + result.section_length = section_length; + result.qualifier = (uint8_t)qualifier; + Initialize_Object(&result.object, ioa, &SectionReady_VTable); + if (!SectionReady_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 121(节准备就绪)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_SectionReady_Get_From_Buffer(IEC60870_SectionReady_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_SectionReady_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 7U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!SectionReady_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &SectionReady_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 122:文件/节召唤或选择 ---- */ +/** + * 功能:编码文件/节召唤或选择的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少4字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:NOF(2) + NOS(1) + SCQ(1);失败只影响临时区。 + */ +static int FileCallOrSelect_Encode_Value(const IEC60870_FileCallOrSelect_t *self, uint8_t *payload) +{ + IEC60870_Object_Write_LE(payload, self->file_name, 2); + payload[2] = self->section; + payload[3] = self->qualifier; + return 1; +} + +/** + * 功能:解码文件/节召唤或选择的值字段。 + * 输入参数:payload——已校验完整长度的4字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:NOF(2) + NOS(1) + SCQ(1);公开入口在成功后统一提交。 + */ +static int FileCallOrSelect_Decode_Value(const uint8_t *payload, IEC60870_FileCallOrSelect_t *self) +{ + self->file_name = (uint16_t)IEC60870_Object_Read_LE(payload, 2); + self->section = payload[2]; + self->qualifier = payload[3]; + return 1; +} + +/** + * 功能:将公共基类分派到FileCallOrSelect编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int FileCallOrSelect_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return FileCallOrSelect_Encode_Value((const IEC60870_FileCallOrSelect_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable FileCallOrSelect_VTable = { + 122, 4, 1, sizeof(IEC60870_FileCallOrSelect_t), FileCallOrSelect_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 122(文件/节召唤或选择)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_FileCallOrSelect_Create(IEC60870_FileCallOrSelect_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier) +{ + IEC60870_FileCallOrSelect_t result; + uint8_t payload[4]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (section < 0 || section > 255) || + (qualifier < 0 || qualifier > 255)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.file_name = file_name; + result.section = (uint8_t)section; + result.qualifier = (uint8_t)qualifier; + Initialize_Object(&result.object, ioa, &FileCallOrSelect_VTable); + if (!FileCallOrSelect_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 122(文件/节召唤或选择)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_FileCallOrSelect_Get_From_Buffer(IEC60870_FileCallOrSelect_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_FileCallOrSelect_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 4U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!FileCallOrSelect_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &FileCallOrSelect_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 124:文件/节确认 ---- */ +/** + * 功能:编码文件/节确认的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少4字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:NOF(2) + NOS(1) + AFQ(1);失败只影响临时区。 + */ +static int FileACK_Encode_Value(const IEC60870_FileACK_t *self, uint8_t *payload) +{ + IEC60870_Object_Write_LE(payload, self->file_name, 2); + payload[2] = self->section; + payload[3] = self->qualifier; + return 1; +} + +/** + * 功能:解码文件/节确认的值字段。 + * 输入参数:payload——已校验完整长度的4字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:NOF(2) + NOS(1) + AFQ(1);公开入口在成功后统一提交。 + */ +static int FileACK_Decode_Value(const uint8_t *payload, IEC60870_FileACK_t *self) +{ + self->file_name = (uint16_t)IEC60870_Object_Read_LE(payload, 2); + self->section = payload[2]; + self->qualifier = payload[3]; + return 1; +} + +/** + * 功能:将公共基类分派到FileACK编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int FileACK_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return FileACK_Encode_Value((const IEC60870_FileACK_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable FileACK_VTable = { + 124, 4, 1, sizeof(IEC60870_FileACK_t), FileACK_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 124(文件/节确认)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_FileACK_Create(IEC60870_FileACK_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier) +{ + IEC60870_FileACK_t result; + uint8_t payload[4]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (section < 0 || section > 255) || + (qualifier < 0 || qualifier > 255)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.file_name = file_name; + result.section = (uint8_t)section; + result.qualifier = (uint8_t)qualifier; + Initialize_Object(&result.object, ioa, &FileACK_VTable); + if (!FileACK_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 124(文件/节确认)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_FileACK_Get_From_Buffer(IEC60870_FileACK_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_FileACK_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 4U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!FileACK_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &FileACK_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 123:最后文件段或节 ---- */ +/** + * 功能:编码最后文件段或节的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少5字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:NOF(2) + NOS(1) + LSQ(1) + CHS(1),这里只传递校验和;失败只影响临时区。 + */ +static int FileLastSegmentOrSection_Encode_Value(const IEC60870_FileLastSegmentOrSection_t *self, uint8_t *payload) +{ + IEC60870_Object_Write_LE(payload, self->file_name, 2); + payload[2] = self->section; + payload[3] = self->qualifier; + payload[4] = self->checksum; + return 1; +} + +/** + * 功能:解码最后文件段或节的值字段。 + * 输入参数:payload——已校验完整长度的5字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:NOF(2) + NOS(1) + LSQ(1) + CHS(1),这里只传递校验和;公开入口在成功后统一提交。 + */ +static int FileLastSegmentOrSection_Decode_Value(const uint8_t *payload, IEC60870_FileLastSegmentOrSection_t *self) +{ + self->file_name = (uint16_t)IEC60870_Object_Read_LE(payload, 2); + self->section = payload[2]; + self->qualifier = payload[3]; + self->checksum = payload[4]; + return 1; +} + +/** + * 功能:将公共基类分派到FileLastSegmentOrSection编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int FileLastSegmentOrSection_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return FileLastSegmentOrSection_Encode_Value((const IEC60870_FileLastSegmentOrSection_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable FileLastSegmentOrSection_VTable = { + 123, 5, 1, sizeof(IEC60870_FileLastSegmentOrSection_t), FileLastSegmentOrSection_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 123(最后文件段或节)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_FileLastSegmentOrSection_Create(IEC60870_FileLastSegmentOrSection_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier, int checksum) +{ + IEC60870_FileLastSegmentOrSection_t result; + uint8_t payload[5]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (section < 0 || section > 255) || + (qualifier < 0 || qualifier > 255) || + (checksum < 0 || checksum > 255)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.file_name = file_name; + result.section = (uint8_t)section; + result.qualifier = (uint8_t)qualifier; + result.checksum = (uint8_t)checksum; + Initialize_Object(&result.object, ioa, &FileLastSegmentOrSection_VTable); + if (!FileLastSegmentOrSection_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 123(最后文件段或节)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_FileLastSegmentOrSection_Get_From_Buffer(IEC60870_FileLastSegmentOrSection_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_FileLastSegmentOrSection_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 5U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!FileLastSegmentOrSection_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &FileLastSegmentOrSection_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 125:文件段 ---- */ +/** + * 功能:编码文件段的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少4+LOS字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:NOF(2) + NOS(1) + LOS(1) + 段数据(LOS),SQ=0且仅一个对象;失败只影响临时区。 + */ +static int FileSegment_Encode_Value(const IEC60870_FileSegment_t *self, uint8_t *payload) +{ + IEC60870_Object_Write_LE(payload, self->file_name, 2); + payload[2] = self->section; + payload[3] = self->length; + memcpy(payload + 4, self->data, self->length); + return 1; +} + +/** + * 功能:解码文件段的值字段。 + * 输入参数:payload——已校验完整长度的变长载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:NOF(2) + NOS(1) + LOS(1) + 段数据(LOS),SQ=0且仅一个对象;公开入口在成功后统一提交。 + */ +static int FileSegment_Decode_Value(const uint8_t *payload, IEC60870_FileSegment_t *self) +{ + self->file_name = (uint16_t)IEC60870_Object_Read_LE(payload, 2); + self->section = payload[2]; + self->length = payload[3]; + memcpy(self->data, payload + 4, self->length); + return 1; +} + +/** + * 功能:将公共基类分派到FileSegment编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int FileSegment_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return FileSegment_Encode_Value((const IEC60870_FileSegment_t *)object, payload); +} + +/** + * 功能:取得文件段的实际值载荷长度。 + * 输入参数:object——已核对身份的文件段基类。 + * 输出参数:无。 + * 返回值:4+LOS,最大259字节。 + * 说明:只计算线上长度;不包含IOA或ASDU公共头。 + */ +static uint16_t FileSegment_Payload_Size(const IEC60870_InformationObject_t *object) +{ + const IEC60870_FileSegment_t *self = (const IEC60870_FileSegment_t *)object; + return (uint16_t)(4U + self->length); +} + +/** + * 功能:从文件段原文安全取得载荷长度。 + * 输入参数:payload——值载荷;length——实际可读字节数。 + * 输出参数:无。 + * 返回值:完整段返回4+LOS,截短或空指针返回0。 + * 说明:读取LOS前要求至少4字节;精确尾长由ASDU层检查。 + */ +static uint16_t FileSegment_Payload_Size_From_Buffer(const uint8_t *payload, uint16_t length) +{ + uint16_t required; + if (payload == NULL || length < 4U) { return 0; } + required = (uint16_t)(4U + payload[3]); + return required <= length ? required : 0U; +} + +static const IEC60870_InformationObjectVTable FileSegment_VTable = { + 125, 4, 1, sizeof(IEC60870_FileSegment_t), FileSegment_Encode, + 0, FileSegment_Payload_Size, FileSegment_Payload_Size_From_Buffer +}; + +/** + * 功能:在调用方存储中构造Type 125(文件段)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_FileSegment_Create(IEC60870_FileSegment_t *self, + uint32_t ioa, uint16_t file_name, int section, const uint8_t *data, uint16_t data_length) +{ + IEC60870_FileSegment_t result; + uint8_t payload[IEC60870_OBJECT_MAX_PAYLOAD]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (section < 0 || section > 255) || + (data_length > 255U) || + ((data_length != 0U && data == NULL))) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.file_name = file_name; + result.section = (uint8_t)section; + result.length = (uint8_t)data_length; + if (data_length != 0U) { memcpy(result.data, data, data_length); } + Initialize_Object(&result.object, ioa, &FileSegment_VTable); + if (!FileSegment_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 125(文件段)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:先验证LOS与载荷长度,再复制数据;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_FileSegment_Get_From_Buffer(IEC60870_FileSegment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_FileSegment_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length < 4U || length != (uint16_t)(4U + payload[3])) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!FileSegment_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &FileSegment_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 126:文件目录 ---- */ +/** + * 功能:编码文件目录的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少13字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:NOF(2) + LOF(3) + SOF(1) + CP56(7);失败只影响临时区。 + */ +static int FileDirectory_Encode_Value(const IEC60870_FileDirectory_t *self, uint8_t *payload) +{ + if (self->file_length > UINT32_C(0xFFFFFF)) { return 0; } + IEC60870_Object_Write_LE(payload, self->file_name, 2); + IEC60870_Object_Write_LE(payload + 2, self->file_length, 3); + payload[5] = self->status; + return IEC60870_Time_To_CP56(&self->creation_time, payload + 6, IEC60870_CP56_LEN); +} + +/** + * 功能:解码文件目录的值字段。 + * 输入参数:payload——已校验完整长度的13字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:NOF(2) + LOF(3) + SOF(1) + CP56(7);公开入口在成功后统一提交。 + */ +static int FileDirectory_Decode_Value(const uint8_t *payload, IEC60870_FileDirectory_t *self) +{ + self->file_name = (uint16_t)IEC60870_Object_Read_LE(payload, 2); + self->file_length = IEC60870_Object_Read_LE(payload + 2, 3); + self->status = payload[5]; + return IEC60870_Time_From_CP56(payload + 6, IEC60870_CP56_LEN, &self->creation_time); +} + +/** + * 功能:将公共基类分派到FileDirectory编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int FileDirectory_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return FileDirectory_Encode_Value((const IEC60870_FileDirectory_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable FileDirectory_VTable = { + 126, 13, 0, sizeof(IEC60870_FileDirectory_t), FileDirectory_Encode, + 1, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 126(文件目录)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_FileDirectory_Create(IEC60870_FileDirectory_t *self, + uint32_t ioa, uint16_t file_name, uint32_t file_length, int status, const IEC60870_CP56Time2a_t *creation_time) +{ + IEC60870_FileDirectory_t result; + uint8_t payload[13]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (file_length > UINT32_C(0xFFFFFF)) || + (status < 0 || status > 255) || + (creation_time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.file_name = file_name; + result.file_length = file_length; + result.status = (uint8_t)status; + result.creation_time = *creation_time; + Initialize_Object(&result.object, ioa, &FileDirectory_VTable); + if (!FileDirectory_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 126(文件目录)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_FileDirectory_Get_From_Buffer(IEC60870_FileDirectory_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_FileDirectory_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 13U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!FileDirectory_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &FileDirectory_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/* ---- Type 127:归档查询 ---- */ +/** + * 功能:编码归档查询的值字段。 + * 输入参数:self——具体对象,已核对存储类型。 + * 输出参数:payload——至少16字节的临时区。 + * 返回值:1合法;0字段或时间非法。 + * 说明:NOF(2) + 查询起始CP56(7) + 查询结束CP56(7);失败只影响临时区。 + */ +static int QueryLog_Encode_Value(const IEC60870_QueryLog_t *self, uint8_t *payload) +{ + IEC60870_Object_Write_LE(payload, self->file_name, 2); + return IEC60870_Time_To_CP56(&self->start_time, payload + 2, IEC60870_CP56_LEN) && + IEC60870_Time_To_CP56(&self->stop_time, payload + 9, IEC60870_CP56_LEN); +} + +/** + * 功能:解码归档查询的值字段。 + * 输入参数:payload——已校验完整长度的16字节载荷。 + * 输出参数:self——已清零的局部对象,不填写基类身份。 + * 返回值:1合法;0保留位或时间非法。 + * 说明:NOF(2) + 查询起始CP56(7) + 查询结束CP56(7);公开入口在成功后统一提交。 + */ +static int QueryLog_Decode_Value(const uint8_t *payload, IEC60870_QueryLog_t *self) +{ + self->file_name = (uint16_t)IEC60870_Object_Read_LE(payload, 2); + return IEC60870_Time_From_CP56(payload + 2, IEC60870_CP56_LEN, &self->start_time) && + IEC60870_Time_From_CP56(payload + 9, IEC60870_CP56_LEN, &self->stop_time); +} + +/** + * 功能:将公共基类分派到QueryLog编码。 + * 输入参数:object——已核对类型和函数表的具体对象首成员。 + * 输出参数:payload——足够大的临时载荷区。 + * 返回值:1成功;0字段非法。 + * 说明:类型转换依赖首成员布局,禁止伪造公共基类。 + */ +static int QueryLog_Encode(const IEC60870_InformationObject_t *object, uint8_t *payload) +{ + return QueryLog_Encode_Value((const IEC60870_QueryLog_t *)object, payload); +} + +static const IEC60870_InformationObjectVTable QueryLog_VTable = { + 127, 16, 1, sizeof(IEC60870_QueryLog_t), QueryLog_Encode, + 0, NULL, NULL +}; + +/** + * 功能:在调用方存储中构造Type 127(归档查询)。 + * 输入参数:ioa——24位地址;其余值的范围见公开结构及声明注释。 + * 输出参数:self——成功后整体保存对象,失败不变。 + * 返回值:OK成功;ERROR参数、字段或时间非法。 + * 说明:局部对象验证通过后提交,不分配内存;ASDU容量另行检查。 + */ +IEC60870_AsduResult_t IEC60870_QueryLog_Create(IEC60870_QueryLog_t *self, + uint32_t ioa, uint16_t file_name, const IEC60870_CP56Time2a_t *start_time, const IEC60870_CP56Time2a_t *stop_time) +{ + IEC60870_QueryLog_t result; + uint8_t payload[16]; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || + (start_time == NULL) || + (stop_time == NULL)) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + result.file_name = file_name; + result.start_time = *start_time; + result.stop_time = *stop_time; + Initialize_Object(&result.object, ioa, &QueryLog_VTable); + if (!QueryLog_Encode_Value(&result, payload)) { return IEC60870_ASDU_ERROR; } + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:解析Type 127(归档查询)。 + * 输入参数:ioa——已还原地址;payload——值载荷;length——精确字节数。 + * 输出参数:self——成功时整体写入,失败不变。 + * 返回值:OK成功;ERROR参数、长度、保留位或时间非法。 + * 说明:逐字段恢复数值并初始化基类;结果不借用输入。 + */ +IEC60870_AsduResult_t IEC60870_QueryLog_Get_From_Buffer(IEC60870_QueryLog_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length) +{ + IEC60870_QueryLog_t result; + if (self == NULL || ioa > IEC60870_ASDU_MAX_IOA || payload == NULL || + length != 16U) { return IEC60870_ASDU_ERROR; } + memset(&result, 0, sizeof(result)); + if (!QueryLog_Decode_Value(payload, &result)) { return IEC60870_ASDU_ERROR; } + Initialize_Object(&result.object, ioa, &QueryLog_VTable); + *self = result; + return IEC60870_ASDU_OK; +} + +/** + * 功能:取得当前配置下一条ASDU能容纳的最大文件段数据长度。 + * 输入参数:parameters——合法应用层参数,包含最大ASDU长度和字段宽度。 + * 输出参数:无。 + * 返回值:0..255为数据容量;-1表示参数非法或连空文件段也容纳不下。 + * 说明:扣除公共头、IOA和4字节段头;104默认配置返回236。 + */ +int IEC60870_FileSegment_Get_Max_Data_Size(const IEC60870_AppLayerParameters_t *parameters) +{ + int size; + if (!IEC60870_AppLayerParameters_Valid(parameters)) { return -1; } + size = parameters->maxSizeOfASDU - (2 + parameters->sizeOfCOT + parameters->sizeOfCA) - parameters->sizeOfIOA - 4; + if (size < 0) { return -1; } + return size > 255 ? 255 : size; +} + + +/** + * 功能:根据Type ID取得内部只读类型描述。 + * 输入参数:type_id——报文或已初始化对象中的类型标识。 + * 输出参数:无。 + * 返回值:支持时返回静态描述表指针,未知类型返回NULL。 + * 说明:表中保存载荷长度、对象存储尺寸、单对象限制和编码函数;不解析值字段。 + */ +const IEC60870_InformationObjectVTable *IEC60870_InformationObject_Get_Descriptor(uint8_t type_id) +{ + switch (type_id) { + case 1: return &SinglePointInformation_VTable; + case 3: return &DoublePointInformation_VTable; + case 9: return &MeasuredValueNormalized_VTable; + case 11: return &MeasuredValueScaled_VTable; + case 13: return &MeasuredValueShort_VTable; + case 15: return &IntegratedTotals_VTable; + case 30: return &SinglePointWithCP56Time2a_VTable; + case 31: return &DoublePointWithCP56Time2a_VTable; + case 34: return &MeasuredValueNormalizedWithCP56Time2a_VTable; + case 35: return &MeasuredValueScaledWithCP56Time2a_VTable; + case 36: return &MeasuredValueShortWithCP56Time2a_VTable; + case 37: return &IntegratedTotalsWithCP56Time2a_VTable; + case 45: return &SingleCommand_VTable; + case 46: return &DoubleCommand_VTable; + case 47: return &StepCommand_VTable; + case 48: return &SetpointCommandNormalized_VTable; + case 49: return &SetpointCommandScaled_VTable; + case 50: return &SetpointCommandShort_VTable; + case 58: return &SingleCommandWithCP56Time2a_VTable; + case 59: return &DoubleCommandWithCP56Time2a_VTable; + case 60: return &StepCommandWithCP56Time2a_VTable; + case 61: return &SetpointCommandNormalizedWithCP56Time2a_VTable; + case 62: return &SetpointCommandScaledWithCP56Time2a_VTable; + case 63: return &SetpointCommandShortWithCP56Time2a_VTable; + case 100: return &InterrogationCommand_VTable; + case 101: return &CounterInterrogationCommand_VTable; + case 102: return &ReadCommand_VTable; + case 103: return &ClockSynchronizationCommand_VTable; + case 105: return &ResetProcessCommand_VTable; + case 107: return &TestCommandWithCP56Time2a_VTable; + case 5: return &StepPositionInformation_VTable; + case 7: return &BitString32_VTable; + case 2: return &SinglePointWithCP24Time2a_VTable; + case 4: return &DoublePointWithCP24Time2a_VTable; + case 6: return &StepPositionWithCP24Time2a_VTable; + case 8: return &Bitstring32WithCP24Time2a_VTable; + case 10: return &MeasuredValueNormalizedWithCP24Time2a_VTable; + case 12: return &MeasuredValueScaledWithCP24Time2a_VTable; + case 14: return &MeasuredValueShortWithCP24Time2a_VTable; + case 16: return &IntegratedTotalsWithCP24Time2a_VTable; + case 32: return &StepPositionWithCP56Time2a_VTable; + case 33: return &Bitstring32WithCP56Time2a_VTable; + case 17: return &EventOfProtectionEquipment_VTable; + case 18: return &PackedStartEventsOfProtectionEquipment_VTable; + case 19: return &PackedOutputCircuitInfo_VTable; + case 38: return &EventOfProtectionEquipmentWithCP56Time2a_VTable; + case 39: return &PackedStartEventsOfProtectionEquipmentWithCP56Time2a_VTable; + case 40: return &PackedOutputCircuitInfoWithCP56Time2a_VTable; + case 20: return &PackedSinglePointWithSCD_VTable; + case 21: return &MeasuredValueNormalizedWithoutQuality_VTable; + case 51: return &Bitstring32Command_VTable; + case 64: return &Bitstring32CommandWithCP56Time2a_VTable; + case 70: return &EndOfInitialization_VTable; + case 104: return &TestCommand_VTable; + case 106: return &DelayAcquisitionCommand_VTable; + case 110: return &ParameterNormalizedValue_VTable; + case 111: return &ParameterScaledValue_VTable; + case 112: return &ParameterFloatValue_VTable; + case 113: return &ParameterActivation_VTable; + case 120: return &FileReady_VTable; + case 121: return &SectionReady_VTable; + case 122: return &FileCallOrSelect_VTable; + case 124: return &FileACK_VTable; + case 123: return &FileLastSegmentOrSection_VTable; + case 125: return &FileSegment_VTable; + case 126: return &FileDirectory_VTable; + case 127: return &QueryLog_VTable; + default: return NULL; + } +} + +/** + * 功能:读取已初始化对象的类型,并核对其编码函数表身份。 + * 输入参数:object——已由具体Create或Get_From_Buffer初始化的基类指针,可为NULL。 + * 输出参数:无。 + * 返回值:身份匹配时返回Type ID;NULL、未知类型或函数表不匹配时返回0。 + * 说明:不检查IOA、数值、品质或时标;这些字段在实际编码时验证。 + */ +uint8_t IEC60870_InformationObject_Get_Type(const IEC60870_InformationObject_t *object) +{ + const IEC60870_InformationObjectVTable *type; + if (object == NULL) { return 0; } + type = IEC60870_InformationObject_Get_Descriptor(object->type_id); + return type != NULL && object->virtual_function_table == type ? type->type_id : 0; +} + +/** + * 功能:通过具体对象的函数表编码一个对象,可按SQ需要省略IOA。 + * 输入参数:object——已初始化的非空具体对象基类,不能手工伪造类型或函数表。 + * parameters——非空且合法的应用层参数,本函数使用其IOA宽度。 + * is_sequence——0写入IOA;1仅写值载荷,适用于监视对象和文件目录。 + * capacity——buffer实际可写容量,单位字节。 + * 输出参数:buffer——调用方提供的对象字节缓冲区,不含ASDU头;不得与对象重叠。 + * length——非空输出指针,成功时填写实际写入的IOA加载荷字节数。 + * 返回值:OK为成功,FULL为空间不足,UNSUPPORTED为未知类型,ERROR为参数或字段非法。 + * 说明:先核对身份并在临时区完成值编码,再检查容量并提交,失败保留buffer和length。 + * SQ首对象仍须传0;地址连续性、对象数量和整条ASDU的上限由容器层维护。 + */ +IEC60870_AsduResult_t IEC60870_InformationObject_Encode( + const IEC60870_InformationObject_t *object, const IEC60870_AppLayerParameters_t *parameters, + int is_sequence, uint8_t *buffer, uint16_t capacity, uint16_t *length) +{ + const IEC60870_InformationObjectVTable *type; + uint8_t payload[IEC60870_OBJECT_MAX_PAYLOAD]; + uint16_t address_size, payload_size, required; + if (object == NULL || buffer == NULL || length == NULL || + !IEC60870_AppLayerParameters_Valid(parameters) || + (is_sequence != 0 && is_sequence != 1)) { + return IEC60870_ASDU_ERROR; + } + type = IEC60870_InformationObject_Get_Descriptor(object->type_id); + if (type == NULL) { return IEC60870_ASDU_UNSUPPORTED; } + /* 先比较受控静态表的地址,再解引用函数表,拒绝身份不匹配的对象。 */ + if (object->virtual_function_table != type || + object->ioa > IEC60870_Object_Max_IOA(parameters) || + (is_sequence && !type->supports_sequence) || + !object->virtual_function_table->encode(object, payload)) { + return IEC60870_ASDU_ERROR; + } + /* 函数表只生成值载荷;统一入口在前面补IOA,避免各类型重复处理宽度。 */ + address_size = is_sequence ? 0U : (uint16_t)parameters->sizeOfIOA; + payload_size = type->get_payload_size ? type->get_payload_size(object) : type->payload_size; + if (payload_size > sizeof(payload)) { return IEC60870_ASDU_ERROR; } + required = (uint16_t)(address_size + payload_size); + if (capacity < required) { return IEC60870_ASDU_FULL; } + if (address_size != 0U) { + IEC60870_Object_Write_LE(buffer, object->ioa, address_size); + } + memcpy(buffer + address_size, payload, payload_size); + *length = required; + return IEC60870_ASDU_OK; +} + +/** + * 功能:按Type ID分派具体载荷解析器,供ASDU校验和取对象共用。 + * 输入参数:type_id——需要解析的具体类型。 + * ioa——已还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷;只有0字节载荷的读命令允许NULL。 + * length——载荷字节数,须精确匹配该类型。 + * object_size——非空object的可用字节数,至少为该具体结构的sizeof。 + * 输出参数:object——匹配具体类型的输出存储;NULL表示仅校验,不输出对象。 + * 返回值:OK为成功,UNSUPPORTED为未知类型,ERROR为地址、长度、字段或存储尺寸非法。 + * 说明:每个分支使用对应的局部具体对象,成功后才memcpy写回,不向调用方暴露部分结果。 + * 存储尺寸检查不能替代类型匹配;调用方须依据Type ID选择正确的结构体。 + */ +IEC60870_AsduResult_t IEC60870_InformationObject_Decode(uint8_t type_id, uint32_t ioa, + const uint8_t *payload, uint16_t length, void *object, size_t object_size) +{ + const IEC60870_InformationObjectVTable *type = IEC60870_InformationObject_Get_Descriptor(type_id); + if (type == NULL) { return IEC60870_ASDU_UNSUPPORTED; } + if (object != NULL && object_size < type->object_size) { return IEC60870_ASDU_ERROR; } + /* 编码时对象已经有函数表;解码时只有Type ID,必须先选择对应结构及解析器。 */ + switch (type_id) { + case 1: { + IEC60870_SinglePointInformation_t result; + IEC60870_AsduResult_t status = IEC60870_SinglePointInformation_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 3: { + IEC60870_DoublePointInformation_t result; + IEC60870_AsduResult_t status = IEC60870_DoublePointInformation_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 9: { + IEC60870_MeasuredValueNormalized_t result; + IEC60870_AsduResult_t status = IEC60870_MeasuredValueNormalized_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 11: { + IEC60870_MeasuredValueScaled_t result; + IEC60870_AsduResult_t status = IEC60870_MeasuredValueScaled_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 13: { + IEC60870_MeasuredValueShort_t result; + IEC60870_AsduResult_t status = IEC60870_MeasuredValueShort_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 15: { + IEC60870_IntegratedTotals_t result; + IEC60870_AsduResult_t status = IEC60870_IntegratedTotals_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 30: { + IEC60870_SinglePointWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_SinglePointWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 31: { + IEC60870_DoublePointWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_DoublePointWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 34: { + IEC60870_MeasuredValueNormalizedWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_MeasuredValueNormalizedWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 35: { + IEC60870_MeasuredValueScaledWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_MeasuredValueScaledWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 36: { + IEC60870_MeasuredValueShortWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_MeasuredValueShortWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 37: { + IEC60870_IntegratedTotalsWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_IntegratedTotalsWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 45: { + IEC60870_SingleCommand_t result; + IEC60870_AsduResult_t status = IEC60870_SingleCommand_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 46: { + IEC60870_DoubleCommand_t result; + IEC60870_AsduResult_t status = IEC60870_DoubleCommand_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 47: { + IEC60870_StepCommand_t result; + IEC60870_AsduResult_t status = IEC60870_StepCommand_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 48: { + IEC60870_SetpointCommandNormalized_t result; + IEC60870_AsduResult_t status = IEC60870_SetpointCommandNormalized_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 49: { + IEC60870_SetpointCommandScaled_t result; + IEC60870_AsduResult_t status = IEC60870_SetpointCommandScaled_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 50: { + IEC60870_SetpointCommandShort_t result; + IEC60870_AsduResult_t status = IEC60870_SetpointCommandShort_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 58: { + IEC60870_SingleCommandWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_SingleCommandWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 59: { + IEC60870_DoubleCommandWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_DoubleCommandWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 60: { + IEC60870_StepCommandWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_StepCommandWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 61: { + IEC60870_SetpointCommandNormalizedWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_SetpointCommandNormalizedWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 62: { + IEC60870_SetpointCommandScaledWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_SetpointCommandScaledWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 63: { + IEC60870_SetpointCommandShortWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_SetpointCommandShortWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 100: { + IEC60870_InterrogationCommand_t result; + IEC60870_AsduResult_t status = IEC60870_InterrogationCommand_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 101: { + IEC60870_CounterInterrogationCommand_t result; + IEC60870_AsduResult_t status = IEC60870_CounterInterrogationCommand_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 102: { + IEC60870_ReadCommand_t result; + IEC60870_AsduResult_t status = IEC60870_ReadCommand_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 103: { + IEC60870_ClockSynchronizationCommand_t result; + IEC60870_AsduResult_t status = IEC60870_ClockSynchronizationCommand_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 105: { + IEC60870_ResetProcessCommand_t result; + IEC60870_AsduResult_t status = IEC60870_ResetProcessCommand_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 107: { + IEC60870_TestCommandWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_TestCommandWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 5: { + IEC60870_StepPositionInformation_t result; + IEC60870_AsduResult_t status = IEC60870_StepPositionInformation_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 7: { + IEC60870_BitString32_t result; + IEC60870_AsduResult_t status = IEC60870_BitString32_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 2: { + IEC60870_SinglePointWithCP24Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_SinglePointWithCP24Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 4: { + IEC60870_DoublePointWithCP24Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_DoublePointWithCP24Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 6: { + IEC60870_StepPositionWithCP24Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_StepPositionWithCP24Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 8: { + IEC60870_Bitstring32WithCP24Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_Bitstring32WithCP24Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 10: { + IEC60870_MeasuredValueNormalizedWithCP24Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_MeasuredValueNormalizedWithCP24Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 12: { + IEC60870_MeasuredValueScaledWithCP24Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_MeasuredValueScaledWithCP24Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 14: { + IEC60870_MeasuredValueShortWithCP24Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_MeasuredValueShortWithCP24Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 16: { + IEC60870_IntegratedTotalsWithCP24Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_IntegratedTotalsWithCP24Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 32: { + IEC60870_StepPositionWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_StepPositionWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 33: { + IEC60870_Bitstring32WithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_Bitstring32WithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 17: { + IEC60870_EventOfProtectionEquipment_t result; + IEC60870_AsduResult_t status = IEC60870_EventOfProtectionEquipment_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 18: { + IEC60870_PackedStartEventsOfProtectionEquipment_t result; + IEC60870_AsduResult_t status = IEC60870_PackedStartEventsOfProtectionEquipment_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 19: { + IEC60870_PackedOutputCircuitInfo_t result; + IEC60870_AsduResult_t status = IEC60870_PackedOutputCircuitInfo_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 38: { + IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 39: { + IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 40: { + IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 20: { + IEC60870_PackedSinglePointWithSCD_t result; + IEC60870_AsduResult_t status = IEC60870_PackedSinglePointWithSCD_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 21: { + IEC60870_MeasuredValueNormalizedWithoutQuality_t result; + IEC60870_AsduResult_t status = IEC60870_MeasuredValueNormalizedWithoutQuality_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 51: { + IEC60870_Bitstring32Command_t result; + IEC60870_AsduResult_t status = IEC60870_Bitstring32Command_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 64: { + IEC60870_Bitstring32CommandWithCP56Time2a_t result; + IEC60870_AsduResult_t status = IEC60870_Bitstring32CommandWithCP56Time2a_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 70: { + IEC60870_EndOfInitialization_t result; + IEC60870_AsduResult_t status = IEC60870_EndOfInitialization_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 104: { + IEC60870_TestCommand_t result; + IEC60870_AsduResult_t status = IEC60870_TestCommand_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 106: { + IEC60870_DelayAcquisitionCommand_t result; + IEC60870_AsduResult_t status = IEC60870_DelayAcquisitionCommand_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 110: { + IEC60870_ParameterNormalizedValue_t result; + IEC60870_AsduResult_t status = IEC60870_ParameterNormalizedValue_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 111: { + IEC60870_ParameterScaledValue_t result; + IEC60870_AsduResult_t status = IEC60870_ParameterScaledValue_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 112: { + IEC60870_ParameterFloatValue_t result; + IEC60870_AsduResult_t status = IEC60870_ParameterFloatValue_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 113: { + IEC60870_ParameterActivation_t result; + IEC60870_AsduResult_t status = IEC60870_ParameterActivation_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 120: { + IEC60870_FileReady_t result; + IEC60870_AsduResult_t status = IEC60870_FileReady_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 121: { + IEC60870_SectionReady_t result; + IEC60870_AsduResult_t status = IEC60870_SectionReady_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 122: { + IEC60870_FileCallOrSelect_t result; + IEC60870_AsduResult_t status = IEC60870_FileCallOrSelect_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 124: { + IEC60870_FileACK_t result; + IEC60870_AsduResult_t status = IEC60870_FileACK_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 123: { + IEC60870_FileLastSegmentOrSection_t result; + IEC60870_AsduResult_t status = IEC60870_FileLastSegmentOrSection_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 125: { + IEC60870_FileSegment_t result; + IEC60870_AsduResult_t status = IEC60870_FileSegment_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 126: { + IEC60870_FileDirectory_t result; + IEC60870_AsduResult_t status = IEC60870_FileDirectory_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + case 127: { + IEC60870_QueryLog_t result; + IEC60870_AsduResult_t status = IEC60870_QueryLog_Get_From_Buffer(&result, ioa, payload, length); + if (status == IEC60870_ASDU_OK && object != NULL) { memcpy(object, &result, sizeof(result)); } + return status; + } + default: return IEC60870_ASDU_UNSUPPORTED; + } +} + diff --git a/IEC60870-Slave/.build/command-v1/before/common/IEC60870_InformationObjects.h b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_InformationObjects.h new file mode 100644 index 00000000..fb77bc65 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_InformationObjects.h @@ -0,0 +1,2026 @@ +/* + * 101/104共用的信息对象层:每种类型有独立结构、构造器和解析器;编码经只读函数表分派。 + * 所有内存由调用方提供,Create不会分配内存,也无需Destroy。对象可按值复制。 + * Create/Get_From_Buffer失败不修改对象;修改数值后再次编码仍会检查字段合法性。 + * 所有接口只处理数据,不操作网络、点表、系统时钟或选择/执行状态机。 + * + * 使用顺序:具体类型Create -> ASDU添加对象;含base成员的类型传base.object,其余传object。 + * 接收顺序:ASDU按Type ID选择具体解析器 -> Get_From_Buffer -> 独立具体对象。 + * Create/Get_From_Buffer设置身份,编码时借此找到函数表;程序不使用C位域映射报文。 + */ +#ifndef IEC60870_INFORMATION_OBJECTS_H +#define IEC60870_INFORMATION_OBJECTS_H + +#include "IEC60870_Types.h" +#include "IEC60870_Time.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define IEC60870_ASDU_MAX_OBJECTS 127U /* VSQ低7位保存对象数量,最高位为SQ。 */ +#define IEC60870_ASDU_MAX_IOA UINT32_C(0xFFFFFF) /* 3字节信息对象地址的最大值。 */ + +/* 不可按布尔值判断。只有 OK 修改输出;FULL/ERROR/UNSUPPORTED 保持输出不变。 */ +typedef enum { + IEC60870_ASDU_UNSUPPORTED = -2, /* 当前入口不支持该Type ID或该类型类别。 */ + IEC60870_ASDU_ERROR = -1, /* 参数、布局、字段内容或构造器状态非法。 */ + IEC60870_ASDU_FULL = 0, /* 本次未写入:字节容量不足或对象数量已达上限。 */ + IEC60870_ASDU_OK = 1 /* 操作成功;组包成功不表示已经发送或执行。 */ +} IEC60870_AsduResult_t; + +/* 数值就是ASDU首字节的Type ID;本枚举只列本模块已实现的类型。 */ +typedef enum { + IEC60870_ASDU_M_SP_NA_1 = 1, /* 单点信息,SIQ。 */ + IEC60870_ASDU_M_SP_TA_1 = 2, /* 带CP24单点信息。 */ + IEC60870_ASDU_M_DP_NA_1 = 3, /* 双点信息,DIQ。 */ + IEC60870_ASDU_M_DP_TA_1 = 4, /* 带CP24双点信息。 */ + IEC60870_ASDU_M_ST_NA_1 = 5, /* 步位置信息。 */ + IEC60870_ASDU_M_ST_TA_1 = 6, /* 带CP24步位置信息。 */ + IEC60870_ASDU_M_BO_NA_1 = 7, /* 32位比特串监视。 */ + IEC60870_ASDU_M_BO_TA_1 = 8, /* 带CP24比特串监视。 */ + IEC60870_ASDU_M_ME_NA_1 = 9, /* 归一化测量值,NVA和QDS。 */ + IEC60870_ASDU_M_ME_TA_1 = 10, /* 带CP24归一化测量值。 */ + IEC60870_ASDU_M_ME_NB_1 = 11, /* 标度化测量值,SVA和QDS。 */ + IEC60870_ASDU_M_ME_TB_1 = 12, /* 带CP24标度化测量值。 */ + IEC60870_ASDU_M_ME_NC_1 = 13, /* 短浮点测量值,IEEE 754 binary32和QDS。 */ + IEC60870_ASDU_M_ME_TC_1 = 14, /* 带CP24短浮点测量值。 */ + IEC60870_ASDU_M_IT_NA_1 = 15, /* 累计量,BCR。 */ + IEC60870_ASDU_M_IT_TA_1 = 16, /* 带CP24累计量。 */ + IEC60870_ASDU_M_EP_TA_1 = 17, /* 带CP24保护事件。 */ + IEC60870_ASDU_M_EP_TB_1 = 18, /* 带CP24成组保护启动事件。 */ + IEC60870_ASDU_M_EP_TC_1 = 19, /* 带CP24保护输出电路信息。 */ + IEC60870_ASDU_M_PS_NA_1 = 20, /* 带变位检测的成组单点。 */ + IEC60870_ASDU_M_ME_ND_1 = 21, /* 无品质归一化测量值。 */ + IEC60870_ASDU_M_SP_TB_1 = 30, /* 单点信息及CP56Time2a。 */ + IEC60870_ASDU_M_DP_TB_1 = 31, /* 双点信息及CP56Time2a。 */ + IEC60870_ASDU_M_ST_TB_1 = 32, /* 带CP56步位置信息。 */ + IEC60870_ASDU_M_BO_TB_1 = 33, /* 带CP56比特串监视。 */ + IEC60870_ASDU_M_ME_TD_1 = 34, /* 归一化测量值及CP56Time2a。 */ + IEC60870_ASDU_M_ME_TE_1 = 35, /* 标度化测量值及CP56Time2a。 */ + IEC60870_ASDU_M_ME_TF_1 = 36, /* 短浮点测量值及CP56Time2a。 */ + IEC60870_ASDU_M_IT_TB_1 = 37, /* 累计量及CP56Time2a。 */ + IEC60870_ASDU_M_EP_TD_1 = 38, /* 带CP56保护事件。 */ + IEC60870_ASDU_M_EP_TE_1 = 39, /* 带CP56成组保护启动事件。 */ + IEC60870_ASDU_M_EP_TF_1 = 40, /* 带CP56保护输出电路信息。 */ + IEC60870_ASDU_C_SC_NA_1 = 45, /* 单命令,SCO。 */ + IEC60870_ASDU_C_DC_NA_1 = 46, /* 双命令,DCO。 */ + IEC60870_ASDU_C_RC_NA_1 = 47, /* 步调节命令,RCO。 */ + IEC60870_ASDU_C_SE_NA_1 = 48, /* 归一化设定值命令,NVA和QOS。 */ + IEC60870_ASDU_C_SE_NB_1 = 49, /* 标度化设定值命令,SVA和QOS。 */ + IEC60870_ASDU_C_SE_NC_1 = 50, /* 短浮点设定值命令,浮点值和QOS。 */ + IEC60870_ASDU_C_BO_NA_1 = 51, /* 32位比特串命令。 */ + IEC60870_ASDU_C_SC_TA_1 = 58, /* 单命令及CP56Time2a。 */ + IEC60870_ASDU_C_DC_TA_1 = 59, /* 双命令及CP56Time2a。 */ + IEC60870_ASDU_C_RC_TA_1 = 60, /* 步调节命令及CP56Time2a。 */ + IEC60870_ASDU_C_SE_TA_1 = 61, /* 归一化设定值命令及CP56Time2a。 */ + IEC60870_ASDU_C_SE_TB_1 = 62, /* 标度化设定值命令及CP56Time2a。 */ + IEC60870_ASDU_C_SE_TC_1 = 63, /* 短浮点设定值命令及CP56Time2a。 */ + IEC60870_ASDU_C_BO_TA_1 = 64, /* 带CP56比特串命令。 */ + IEC60870_ASDU_M_EI_NA_1 = 70, /* 初始化结束。 */ + IEC60870_ASDU_C_IC_NA_1 = 100, /* 总召唤命令,QOI。 */ + IEC60870_ASDU_C_CI_NA_1 = 101, /* 电能脉冲召唤命令,QCC。 */ + IEC60870_ASDU_C_RD_NA_1 = 102, /* 读命令,仅携带IOA。 */ + IEC60870_ASDU_C_CS_NA_1 = 103, /* 时钟同步命令,携带CP56Time2a。 */ + IEC60870_ASDU_C_TS_NA_1 = 104, /* 无时标测试命令。 */ + IEC60870_ASDU_C_RP_NA_1 = 105, /* 复位进程命令,QRP。 */ + IEC60870_ASDU_C_CD_NA_1 = 106, /* 延时获得命令。 */ + IEC60870_ASDU_C_TS_TA_1 = 107, /* 带CP56Time2a的测试命令,另含2字节测试序号。 */ + IEC60870_ASDU_P_ME_NA_1 = 110, /* 归一化测量值参数。 */ + IEC60870_ASDU_P_ME_NB_1 = 111, /* 标度化测量值参数。 */ + IEC60870_ASDU_P_ME_NC_1 = 112, /* 短浮点测量值参数。 */ + IEC60870_ASDU_P_AC_NA_1 = 113, /* 参数激活。 */ + IEC60870_ASDU_F_FR_NA_1 = 120, /* 文件准备就绪。 */ + IEC60870_ASDU_F_SR_NA_1 = 121, /* 节准备就绪。 */ + IEC60870_ASDU_F_SC_NA_1 = 122, /* 文件/节召唤或选择。 */ + IEC60870_ASDU_F_LS_NA_1 = 123, /* 最后文件段或节。 */ + IEC60870_ASDU_F_AF_NA_1 = 124, /* 文件/节确认。 */ + IEC60870_ASDU_F_SG_NA_1 = 125, /* 文件段。 */ + IEC60870_ASDU_F_DR_TA_1 = 126, /* 文件目录。 */ + IEC60870_ASDU_F_SC_NB_1 = 127 /* 归档查询。 */ +} IEC60870_AsduType_t; + +/* 单点/双点仅允许 BL/SB/NT/IV;测量值还允许 OV;累计量使用独立 BCR 字段。 */ +enum { + IEC60870_ASDU_QUALITY_OV = 0x01, /* 溢出;用于带QDS的对象,不用于SIQ/DIQ/QDP。 */ + IEC60870_ASDU_QUALITY_EI = 0x08, /* 保护事件的经过时间无效;仅用于QDP。 */ + IEC60870_ASDU_QUALITY_BL = 0x10, /* 被封锁。 */ + IEC60870_ASDU_QUALITY_SB = 0x20, /* 被取代。 */ + IEC60870_ASDU_QUALITY_NT = 0x40, /* 非当前值。 */ + IEC60870_ASDU_QUALITY_IV = 0x80 /* 无效。 */ +}; + +/* BCR的4字节计数和1字节状态按字段保存,不直接把结构体复制到报文。 */ +typedef struct { + int32_t value; /* 32位有符号累计值。 */ + uint8_t sequence_number; /* BCR 序号 0..31,与 ASDU 的 SQ 无关 */ + uint8_t has_carry; /* CY:发生进位,0/1。 */ + uint8_t is_adjusted; /* CA:计数已调整,0/1;此处不是ASDU公共地址。 */ + uint8_t is_invalid; /* IV:计数无效,0/1。 */ +} IEC60870_BinaryCounterReading_t; +/* 公共基类只保存身份、地址和编码分派表;由具体类型的Create或解析器初始化。 + * type_id和virtual_function_table只读,禁止手工伪造。ioa可在所选宽度内修改。 + * 每个具体类型必须以本基类(或以本基类开头的基础类型)为首成员。 + * 带时标对象的base.object仍保存带时标Type ID和函数表。只能复制完整具体对象; + * 不得把base截取成独立基础对象后编码,否则函数表会按较大的带时标结构访问。 + * 要转换成无时标类型,应调用对应基础类型的Create,显式传入值、品质及地址。 */ +struct IEC60870_InformationObjectVTable; +typedef struct { + uint32_t ioa; /* 构造时允许24位,实际编码再按1/2/3字节配置检查;不是公共地址CA。 */ + uint8_t type_id; /* 对应ASDU首字节;由具体Create/解析器设置,调用方只读。 */ + const struct IEC60870_InformationObjectVTable *virtual_function_table; /* 静态只读编码函数表,无需释放。 */ +} IEC60870_InformationObject_t; + +/* Type 1:单点信息。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + uint8_t value; /* 0/1。 */ + uint8_t quality; /* 点信息仅高4位;测量值另允许bit0溢出。 */ +} IEC60870_SinglePointInformation_t; + +/* Type 3:双点信息。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + uint8_t value; /* 0..3,含中间及不确定状态。 */ + uint8_t quality; /* 点信息仅高4位;测量值另允许bit0溢出。 */ +} IEC60870_DoublePointInformation_t; + +/* Type 9:归一化测量值(原始16位定点值)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + int16_t value; /* 实际值=value/32768.0。 */ + uint8_t quality; /* 点信息仅高4位;测量值另允许bit0溢出。 */ +} IEC60870_MeasuredValueNormalized_t; + +/* Type 11:标度化测量值。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + int16_t value; /* 有符号16位整数。 */ + uint8_t quality; /* 点信息仅高4位;测量值另允许bit0溢出。 */ +} IEC60870_MeasuredValueScaled_t; + +/* Type 13:短浮点测量值。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + float value; /* IEEE 754 binary32,保留NaN/Inf位模式。 */ + uint8_t quality; /* 点信息仅高4位;测量值另允许bit0溢出。 */ +} IEC60870_MeasuredValueShort_t; + +/* Type 15:累计量。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + IEC60870_BinaryCounterReading_t value; /* 计数及BCR标志,不使用QDS。 */ +} IEC60870_IntegratedTotals_t; + +/* Type 30:带时标单点信息。 */ +typedef struct { + IEC60870_SinglePointInformation_t base; /* 首成员为基础对象;时标复用其值编解码。 */ + IEC60870_CP56Time2a_t time; /* CP56保留IV/SU,不执行时区转换或校时。 */ +} IEC60870_SinglePointWithCP56Time2a_t; + +/* Type 31:带时标双点信息。 */ +typedef struct { + IEC60870_DoublePointInformation_t base; /* 首成员为基础对象;时标复用其值编解码。 */ + IEC60870_CP56Time2a_t time; /* CP56保留IV/SU,不执行时区转换或校时。 */ +} IEC60870_DoublePointWithCP56Time2a_t; + +/* Type 34:带时标归一化测量值。 */ +typedef struct { + IEC60870_MeasuredValueNormalized_t base; /* 首成员为基础对象;时标复用其值编解码。 */ + IEC60870_CP56Time2a_t time; /* CP56保留IV/SU,不执行时区转换或校时。 */ +} IEC60870_MeasuredValueNormalizedWithCP56Time2a_t; + +/* Type 35:带时标标度化测量值。 */ +typedef struct { + IEC60870_MeasuredValueScaled_t base; /* 首成员为基础对象;时标复用其值编解码。 */ + IEC60870_CP56Time2a_t time; /* CP56保留IV/SU,不执行时区转换或校时。 */ +} IEC60870_MeasuredValueScaledWithCP56Time2a_t; + +/* Type 36:带时标短浮点测量值。 */ +typedef struct { + IEC60870_MeasuredValueShort_t base; /* 首成员为基础对象;时标复用其值编解码。 */ + IEC60870_CP56Time2a_t time; /* CP56保留IV/SU,不执行时区转换或校时。 */ +} IEC60870_MeasuredValueShortWithCP56Time2a_t; + +/* Type 37:带时标累计量。 */ +typedef struct { + IEC60870_IntegratedTotals_t base; /* 首成员为基础对象;时标复用其值编解码。 */ + IEC60870_CP56Time2a_t time; /* CP56保留IV/SU,不执行时区转换或校时。 */ +} IEC60870_IntegratedTotalsWithCP56Time2a_t; + +/* Type 45:单命令。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + uint8_t state; /* 0/1。 */ + uint8_t is_select; /* S/E:1选择、0执行。 */ + uint8_t qualifier; /* QU:0..31,业务判断具体支持值。 */ +} IEC60870_SingleCommand_t; + +/* Type 46:双命令。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + uint8_t state; /* 仅1/2。 */ + uint8_t is_select; /* S/E:1选择、0执行。 */ + uint8_t qualifier; /* QU:0..31,业务判断具体支持值。 */ +} IEC60870_DoubleCommand_t; + +/* Type 47:步调节命令。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + uint8_t state; /* 仅1/2。 */ + uint8_t is_select; /* S/E:1选择、0执行。 */ + uint8_t qualifier; /* QU:0..31,业务判断具体支持值。 */ +} IEC60870_StepCommand_t; + +/* Type 48:归一化设定值命令(原始16位定点值)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + int16_t value; /* 归一化原始16位定点值。 */ + uint8_t is_select; /* S/E:0/1。 */ + uint8_t qualifier; /* QL:0..127。 */ +} IEC60870_SetpointCommandNormalized_t; + +/* Type 49:标度化设定值命令。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + int16_t value; /* 有符号16位标度值。 */ + uint8_t is_select; /* S/E:0/1。 */ + uint8_t qualifier; /* QL:0..127。 */ +} IEC60870_SetpointCommandScaled_t; + +/* Type 50:短浮点设定值命令。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + float value; /* IEEE 754 binary32。 */ + uint8_t is_select; /* S/E:0/1。 */ + uint8_t qualifier; /* QL:0..127。 */ +} IEC60870_SetpointCommandShort_t; + +/* Type 58:带时标单命令。 */ +typedef struct { + IEC60870_SingleCommand_t base; /* 首成员为基础对象;时标复用其值编解码。 */ + IEC60870_CP56Time2a_t time; /* CP56保留IV/SU,不执行时区转换或校时。 */ +} IEC60870_SingleCommandWithCP56Time2a_t; + +/* Type 59:带时标双命令。 */ +typedef struct { + IEC60870_DoubleCommand_t base; /* 首成员为基础对象;时标复用其值编解码。 */ + IEC60870_CP56Time2a_t time; /* CP56保留IV/SU,不执行时区转换或校时。 */ +} IEC60870_DoubleCommandWithCP56Time2a_t; + +/* Type 60:带时标步调节命令。 */ +typedef struct { + IEC60870_StepCommand_t base; /* 首成员为基础对象;时标复用其值编解码。 */ + IEC60870_CP56Time2a_t time; /* CP56保留IV/SU,不执行时区转换或校时。 */ +} IEC60870_StepCommandWithCP56Time2a_t; + +/* Type 61:带时标归一化设定值命令。 */ +typedef struct { + IEC60870_SetpointCommandNormalized_t base; /* 首成员为基础对象;时标复用其值编解码。 */ + IEC60870_CP56Time2a_t time; /* CP56保留IV/SU,不执行时区转换或校时。 */ +} IEC60870_SetpointCommandNormalizedWithCP56Time2a_t; + +/* Type 62:带时标标度化设定值命令。 */ +typedef struct { + IEC60870_SetpointCommandScaled_t base; /* 首成员为基础对象;时标复用其值编解码。 */ + IEC60870_CP56Time2a_t time; /* CP56保留IV/SU,不执行时区转换或校时。 */ +} IEC60870_SetpointCommandScaledWithCP56Time2a_t; + +/* Type 63:带时标短浮点设定值命令。 */ +typedef struct { + IEC60870_SetpointCommandShort_t base; /* 首成员为基础对象;时标复用其值编解码。 */ + IEC60870_CP56Time2a_t time; /* CP56保留IV/SU,不执行时区转换或校时。 */ +} IEC60870_SetpointCommandShortWithCP56Time2a_t; + +/* Type 100:总召唤命令。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + uint8_t qualifier; /* QOI:0..255,业务判断适用性。 */ +} IEC60870_InterrogationCommand_t; + +/* Type 101:电能脉冲召唤命令。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + uint8_t qualifier; /* RQT:0..63。 */ + uint8_t freeze; /* FR:0..3。 */ +} IEC60870_CounterInterrogationCommand_t; + +/* Type 102:读命令。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ +} IEC60870_ReadCommand_t; + +/* Type 103:时钟同步命令。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + IEC60870_CP56Time2a_t time; /* 时间按值保存;允许IV=1,不代表可执行校时。 */ +} IEC60870_ClockSynchronizationCommand_t; + +/* Type 105:复位进程命令。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + uint8_t qualifier; /* QRP:0..255,业务判断适用性。 */ +} IEC60870_ResetProcessCommand_t; + +/* Type 107:带时标测试命令。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须位于首成员。 */ + uint16_t sequence; /* 2字节测试序号,与链路序号无关。 */ + IEC60870_CP56Time2a_t time; /* 时间按值保存;允许IV=1,不代表可执行校时。 */ +} IEC60870_TestCommandWithCP56Time2a_t; + +/* Type 5:步位置信息;布局:VTI(1:低7位补码位置、bit7瞬态) + QDS(1)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + int8_t value; /* VTI有符号步位置,-64..63。 */ + uint8_t is_transient; /* VTI瞬态标志,0/1。 */ + uint8_t quality; /* QDS,仅允许0xF1范围内的位。 */ +} IEC60870_StepPositionInformation_t; + +/* Type 7:32位比特串监视;布局:BSI(4) + QDS(1)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint32_t value; /* 32位状态比特串。 */ + uint8_t quality; /* QDS,仅允许0xF1范围内的位。 */ +} IEC60870_BitString32_t; + +/* Type 2:带CP24单点信息;布局:基础值(1) + CP24(3)。 */ +typedef struct { + IEC60870_SinglePointInformation_t base; /* 首成员为基础对象,值处理复用基础类型。 */ + IEC60870_CP24Time2a_t time; /* CP24时标,按值保存。 */ +} IEC60870_SinglePointWithCP24Time2a_t; + +/* Type 4:带CP24双点信息;布局:基础值(1) + CP24(3)。 */ +typedef struct { + IEC60870_DoublePointInformation_t base; /* 首成员为基础对象,值处理复用基础类型。 */ + IEC60870_CP24Time2a_t time; /* CP24时标,按值保存。 */ +} IEC60870_DoublePointWithCP24Time2a_t; + +/* Type 6:带CP24步位置信息;布局:基础值(2) + CP24(3)。 */ +typedef struct { + IEC60870_StepPositionInformation_t base; /* 首成员为基础对象,值处理复用基础类型。 */ + IEC60870_CP24Time2a_t time; /* CP24时标,按值保存。 */ +} IEC60870_StepPositionWithCP24Time2a_t; + +/* Type 8:带CP24比特串监视;布局:基础值(5) + CP24(3)。 */ +typedef struct { + IEC60870_BitString32_t base; /* 首成员为基础对象,值处理复用基础类型。 */ + IEC60870_CP24Time2a_t time; /* CP24时标,按值保存。 */ +} IEC60870_Bitstring32WithCP24Time2a_t; + +/* Type 10:带CP24归一化测量值;布局:基础值(3) + CP24(3)。 */ +typedef struct { + IEC60870_MeasuredValueNormalized_t base; /* 首成员为基础对象,值处理复用基础类型。 */ + IEC60870_CP24Time2a_t time; /* CP24时标,按值保存。 */ +} IEC60870_MeasuredValueNormalizedWithCP24Time2a_t; + +/* Type 12:带CP24标度化测量值;布局:基础值(3) + CP24(3)。 */ +typedef struct { + IEC60870_MeasuredValueScaled_t base; /* 首成员为基础对象,值处理复用基础类型。 */ + IEC60870_CP24Time2a_t time; /* CP24时标,按值保存。 */ +} IEC60870_MeasuredValueScaledWithCP24Time2a_t; + +/* Type 14:带CP24短浮点测量值;布局:基础值(5) + CP24(3)。 */ +typedef struct { + IEC60870_MeasuredValueShort_t base; /* 首成员为基础对象,值处理复用基础类型。 */ + IEC60870_CP24Time2a_t time; /* CP24时标,按值保存。 */ +} IEC60870_MeasuredValueShortWithCP24Time2a_t; + +/* Type 16:带CP24累计量;布局:基础值(5) + CP24(3)。 */ +typedef struct { + IEC60870_IntegratedTotals_t base; /* 首成员为基础对象,值处理复用基础类型。 */ + IEC60870_CP24Time2a_t time; /* CP24时标,按值保存。 */ +} IEC60870_IntegratedTotalsWithCP24Time2a_t; + +/* Type 32:带CP56步位置信息;布局:基础值(2) + CP56(7)。 */ +typedef struct { + IEC60870_StepPositionInformation_t base; /* 首成员为基础对象,值处理复用基础类型。 */ + IEC60870_CP56Time2a_t time; /* CP56时标,按值保存。 */ +} IEC60870_StepPositionWithCP56Time2a_t; + +/* Type 33:带CP56比特串监视;布局:基础值(5) + CP56(7)。 */ +typedef struct { + IEC60870_BitString32_t base; /* 首成员为基础对象,值处理复用基础类型。 */ + IEC60870_CP56Time2a_t time; /* CP56时标,按值保存。 */ +} IEC60870_Bitstring32WithCP56Time2a_t; + +/* Type 17:带CP24保护事件;布局:SEP(1:ES低2位,QDP高5位) + CP16(2) + CP24(3)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint8_t event; /* 事件状态/位掩码,0..3。 */ + uint8_t quality; /* 保护品质QDP,EI/BL/SB/NT/IV,掩码0xF8。 */ + IEC60870_CP16Time2a_t elapsed_time; /* 事件经过时间,0..65535毫秒。 */ + IEC60870_CP24Time2a_t time; /* CP24事件时标。 */ +} IEC60870_EventOfProtectionEquipment_t; + +/* Type 18:带CP24成组保护启动事件;布局:事件(1) + QDP(1) + CP16(2) + CP24(3)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint8_t event; /* 事件状态/位掩码,0..63。 */ + uint8_t quality; /* 保护品质QDP,EI/BL/SB/NT/IV,掩码0xF8。 */ + IEC60870_CP16Time2a_t elapsed_time; /* 事件经过时间,0..65535毫秒。 */ + IEC60870_CP24Time2a_t time; /* CP24事件时标。 */ +} IEC60870_PackedStartEventsOfProtectionEquipment_t; + +/* Type 19:带CP24保护输出电路信息;布局:事件(1) + QDP(1) + CP16(2) + CP24(3)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint8_t event; /* 事件状态/位掩码,0..15。 */ + uint8_t quality; /* 保护品质QDP,EI/BL/SB/NT/IV,掩码0xF8。 */ + IEC60870_CP16Time2a_t elapsed_time; /* 输出电路动作时间,0..65535毫秒。 */ + IEC60870_CP24Time2a_t time; /* CP24事件时标。 */ +} IEC60870_PackedOutputCircuitInfo_t; + +/* Type 38:带CP56保护事件;布局:SEP(1:ES低2位,QDP高5位) + CP16(2) + CP56(7)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint8_t event; /* 事件状态/位掩码,0..3。 */ + uint8_t quality; /* 保护品质QDP,EI/BL/SB/NT/IV,掩码0xF8。 */ + IEC60870_CP16Time2a_t elapsed_time; /* 事件经过时间,0..65535毫秒。 */ + IEC60870_CP56Time2a_t time; /* CP56事件时标。 */ +} IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t; + +/* Type 39:带CP56成组保护启动事件;布局:事件(1) + QDP(1) + CP16(2) + CP56(7)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint8_t event; /* 事件状态/位掩码,0..63。 */ + uint8_t quality; /* 保护品质QDP,EI/BL/SB/NT/IV,掩码0xF8。 */ + IEC60870_CP16Time2a_t elapsed_time; /* 事件经过时间,0..65535毫秒。 */ + IEC60870_CP56Time2a_t time; /* CP56事件时标。 */ +} IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t; + +/* Type 40:带CP56保护输出电路信息;布局:事件(1) + QDP(1) + CP16(2) + CP56(7)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint8_t event; /* 事件状态/位掩码,0..15。 */ + uint8_t quality; /* 保护品质QDP,EI/BL/SB/NT/IV,掩码0xF8。 */ + IEC60870_CP16Time2a_t elapsed_time; /* 输出电路动作时间,0..65535毫秒。 */ + IEC60870_CP56Time2a_t time; /* CP56事件时标。 */ +} IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t; + +/* Type 20:带变位检测的成组单点;布局:SCD(4:状态16位 + 变位16位) + QDS(1)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint16_t status; /* 16个单点当前状态,低位对应组内首点。 */ + uint16_t change; /* 16个变位检测标志,与status逐位对应。 */ + uint8_t quality; /* QDS,仅允许0xF1范围内的位。 */ +} IEC60870_PackedSinglePointWithSCD_t; + +/* Type 21:无品质归一化测量值;布局:NVA(2),不包含QDS。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + int16_t value; /* 归一化原始定点值,实际值=value/32768.0。 */ +} IEC60870_MeasuredValueNormalizedWithoutQuality_t; + +/* Type 51:32位比特串命令;布局:BSI(4),不包含QDS。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint32_t value; /* 完整32位命令比特串,无品质或选择位。 */ +} IEC60870_Bitstring32Command_t; + +/* Type 64:带CP56比特串命令;布局:基础值(4) + CP56(7)。 */ +typedef struct { + IEC60870_Bitstring32Command_t base; /* 首成员为基础对象,值处理复用基础类型。 */ + IEC60870_CP56Time2a_t time; /* CP56时标,按值保存。 */ +} IEC60870_Bitstring32CommandWithCP56Time2a_t; + +/* Type 70:初始化结束;布局:COI(1),地址适用性由应用判断。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint8_t cause; /* COI:低7位初始化原因,bit7表示本地参数改变。 */ +} IEC60870_EndOfInitialization_t; + +/* Type 104:无时标测试命令;布局:固定测试图样AA 55。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + +} IEC60870_TestCommand_t; + +/* Type 106:延时获得命令;布局:CP16(2)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + IEC60870_CP16Time2a_t delay; /* 延时时间,0..65535毫秒。 */ +} IEC60870_DelayAcquisitionCommand_t; + +/* Type 110:归一化测量值参数;布局:参数值(2) + QPM(1),QPM不是QDS。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + int16_t value; /* 16位参数值;归一化类型按原始定点值保存。 */ + uint8_t qualifier; /* QPM完整字节:低6位参数种类,bit6本地改变、bit7运行状态。 */ +} IEC60870_ParameterNormalizedValue_t; + +/* Type 111:标度化测量值参数;布局:参数值(2) + QPM(1),QPM不是QDS。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + int16_t value; /* 16位参数值;归一化类型按原始定点值保存。 */ + uint8_t qualifier; /* QPM完整字节:低6位参数种类,bit6本地改变、bit7运行状态。 */ +} IEC60870_ParameterScaledValue_t; + +/* Type 112:短浮点测量值参数;布局:参数值(4) + QPM(1),QPM不是QDS。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + float value; /* IEEE 754 binary32参数值。 */ + uint8_t qualifier; /* QPM完整字节:低6位参数种类,bit6本地改变、bit7运行状态。 */ +} IEC60870_ParameterFloatValue_t; + +/* Type 113:参数激活;布局:QPA(1)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint8_t qualifier; /* QPA完整字节,限定词的业务适用性由应用判断。 */ +} IEC60870_ParameterActivation_t; + +/* Type 120:文件准备就绪;布局:NOF(2) + LOF(3) + FRQ(1)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint16_t file_name; /* NOF,文件标识,不是字符串文件名。 */ + uint32_t file_length; /* LOF,24位文件长度,0..0xFFFFFF。 */ + uint8_t qualifier; /* FRQ,仅bit7可置1,表示否定;低7位保留。 */ +} IEC60870_FileReady_t; + +/* Type 121:节准备就绪;布局:NOF(2) + NOS(1) + LOS(3) + SRQ(1)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint16_t file_name; /* NOF,文件标识,不是字符串文件名。 */ + uint8_t section; /* NOS,节编号,0..255。 */ + uint32_t section_length; /* LOS,24位节长度,0..0xFFFFFF。 */ + uint8_t qualifier; /* SRQ,仅bit7可置1,表示否定;低7位保留。 */ +} IEC60870_SectionReady_t; + +/* Type 122:文件/节召唤或选择;布局:NOF(2) + NOS(1) + SCQ(1)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint16_t file_name; /* NOF,文件标识,不是字符串文件名。 */ + uint8_t section; /* NOS,节编号,0..255。 */ + uint8_t qualifier; /* SCQ完整字节,具体限定词由业务判断。 */ +} IEC60870_FileCallOrSelect_t; + +/* Type 124:文件/节确认;布局:NOF(2) + NOS(1) + AFQ(1)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint16_t file_name; /* NOF,文件标识,不是字符串文件名。 */ + uint8_t section; /* NOS,节编号,0..255。 */ + uint8_t qualifier; /* AFQ完整字节,具体限定词由业务判断。 */ +} IEC60870_FileACK_t; + +/* Type 123:最后文件段或节;布局:NOF(2) + NOS(1) + LSQ(1) + CHS(1),这里只传递校验和。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint16_t file_name; /* NOF,文件标识,不是字符串文件名。 */ + uint8_t section; /* NOS,节编号,0..255。 */ + uint8_t qualifier; /* LSQ,最后段/节限定词。 */ + uint8_t checksum; /* CHS,文件/节校验和;业务按传输范围计算。 */ +} IEC60870_FileLastSegmentOrSection_t; + +/* Type 125:文件段;布局:NOF(2) + NOS(1) + LOS(1) + 段数据(LOS),SQ=0且仅一个对象。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint16_t file_name; /* NOF,文件标识,不是字符串文件名。 */ + uint8_t section; /* NOS,节编号,0..255。 */ + uint8_t length; /* LOS,段数据长度0..255;能否装入ASDU由所选配置决定。 */ + uint8_t data[255]; /* 独立保存段数据,不借用输入。 */ +} IEC60870_FileSegment_t; + +/* Type 126:文件目录;布局:NOF(2) + LOF(3) + SOF(1) + CP56(7)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint16_t file_name; /* NOF,文件标识,不是字符串文件名。 */ + uint32_t file_length; /* LOF,24位文件长度。 */ + uint8_t status; /* SOF,低5位文件状态,bit5/6/7为LFD/FOR/FA。 */ + IEC60870_CP56Time2a_t creation_time; /* 文件创建时间。 */ +} IEC60870_FileDirectory_t; + +/* Type 127:归档查询;布局:NOF(2) + 查询起始CP56(7) + 查询结束CP56(7)。 */ +typedef struct { + IEC60870_InformationObject_t object; /* 公共基类,必须为首成员。 */ + uint16_t file_name; /* NOF,文件标识,不是字符串文件名。 */ + IEC60870_CP56Time2a_t start_time; /* 查询开始时间。 */ + IEC60870_CP56Time2a_t stop_time; /* 查询结束时间,区间适用性由业务判断。 */ +} IEC60870_QueryLog_t; + +/** + * 功能:构造Type 5(步位置信息)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:VTI(1:低7位补码位置、bit7瞬态) + QDS(1);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_StepPositionInformation_Create(IEC60870_StepPositionInformation_t *self, + uint32_t ioa, int value, int is_transient, int quality); + +/** + * 功能:解析Type 5(步位置信息)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为2字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_StepPositionInformation_Get_From_Buffer(IEC60870_StepPositionInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 7(32位比特串监视)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:BSI(4) + QDS(1);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_BitString32_Create(IEC60870_BitString32_t *self, + uint32_t ioa, uint32_t value, int quality); + +/** + * 功能:解析Type 7(32位比特串监视)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为5字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_BitString32_Get_From_Buffer(IEC60870_BitString32_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 2(带CP24单点信息)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:基础值(1) + CP24(3);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_SinglePointWithCP24Time2a_Create(IEC60870_SinglePointWithCP24Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP24Time2a_t *time); + +/** + * 功能:解析Type 2(带CP24单点信息)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为4字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_SinglePointWithCP24Time2a_Get_From_Buffer(IEC60870_SinglePointWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 4(带CP24双点信息)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:基础值(1) + CP24(3);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_DoublePointWithCP24Time2a_Create(IEC60870_DoublePointWithCP24Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP24Time2a_t *time); + +/** + * 功能:解析Type 4(带CP24双点信息)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为4字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_DoublePointWithCP24Time2a_Get_From_Buffer(IEC60870_DoublePointWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 6(带CP24步位置信息)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:基础值(2) + CP24(3);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_StepPositionWithCP24Time2a_Create(IEC60870_StepPositionWithCP24Time2a_t *self, + uint32_t ioa, int value, int is_transient, int quality, const IEC60870_CP24Time2a_t *time); + +/** + * 功能:解析Type 6(带CP24步位置信息)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为5字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_StepPositionWithCP24Time2a_Get_From_Buffer(IEC60870_StepPositionWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 8(带CP24比特串监视)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:基础值(5) + CP24(3);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP24Time2a_Create(IEC60870_Bitstring32WithCP24Time2a_t *self, + uint32_t ioa, uint32_t value, int quality, const IEC60870_CP24Time2a_t *time); + +/** + * 功能:解析Type 8(带CP24比特串监视)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为8字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP24Time2a_Get_From_Buffer(IEC60870_Bitstring32WithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 10(带CP24归一化测量值)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:基础值(3) + CP24(3);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP24Time2a_Create(IEC60870_MeasuredValueNormalizedWithCP24Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP24Time2a_t *time); + +/** + * 功能:解析Type 10(带CP24归一化测量值)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为6字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 12(带CP24标度化测量值)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:基础值(3) + CP24(3);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP24Time2a_Create(IEC60870_MeasuredValueScaledWithCP24Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP24Time2a_t *time); + +/** + * 功能:解析Type 12(带CP24标度化测量值)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为6字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueScaledWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 14(带CP24短浮点测量值)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:基础值(5) + CP24(3);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP24Time2a_Create(IEC60870_MeasuredValueShortWithCP24Time2a_t *self, + uint32_t ioa, float value, int quality, const IEC60870_CP24Time2a_t *time); + +/** + * 功能:解析Type 14(带CP24短浮点测量值)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为8字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueShortWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 16(带CP24累计量)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:基础值(5) + CP24(3);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP24Time2a_Create(IEC60870_IntegratedTotalsWithCP24Time2a_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value, const IEC60870_CP24Time2a_t *time); + +/** + * 功能:解析Type 16(带CP24累计量)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为8字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP24Time2a_Get_From_Buffer(IEC60870_IntegratedTotalsWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 32(带CP56步位置信息)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:基础值(2) + CP56(7);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_StepPositionWithCP56Time2a_Create(IEC60870_StepPositionWithCP56Time2a_t *self, + uint32_t ioa, int value, int is_transient, int quality, const IEC60870_CP56Time2a_t *time); + +/** + * 功能:解析Type 32(带CP56步位置信息)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为9字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_StepPositionWithCP56Time2a_Get_From_Buffer(IEC60870_StepPositionWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 33(带CP56比特串监视)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:基础值(5) + CP56(7);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP56Time2a_Create(IEC60870_Bitstring32WithCP56Time2a_t *self, + uint32_t ioa, uint32_t value, int quality, const IEC60870_CP56Time2a_t *time); + +/** + * 功能:解析Type 33(带CP56比特串监视)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为12字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP56Time2a_Get_From_Buffer(IEC60870_Bitstring32WithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 17(带CP24保护事件)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:SEP(1:ES低2位,QDP高5位) + CP16(2) + CP24(3);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipment_Create(IEC60870_EventOfProtectionEquipment_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time); + +/** + * 功能:解析Type 17(带CP24保护事件)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为6字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipment_Get_From_Buffer(IEC60870_EventOfProtectionEquipment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 18(带CP24成组保护启动事件)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:事件(1) + QDP(1) + CP16(2) + CP24(3);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipment_Create(IEC60870_PackedStartEventsOfProtectionEquipment_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time); + +/** + * 功能:解析Type 18(带CP24成组保护启动事件)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为7字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipment_Get_From_Buffer(IEC60870_PackedStartEventsOfProtectionEquipment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 19(带CP24保护输出电路信息)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:事件(1) + QDP(1) + CP16(2) + CP24(3);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfo_Create(IEC60870_PackedOutputCircuitInfo_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time); + +/** + * 功能:解析Type 19(带CP24保护输出电路信息)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为7字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfo_Get_From_Buffer(IEC60870_PackedOutputCircuitInfo_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 38(带CP56保护事件)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:SEP(1:ES低2位,QDP高5位) + CP16(2) + CP56(7);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Create(IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time); + +/** + * 功能:解析Type 38(带CP56保护事件)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为10字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer(IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 39(带CP56成组保护启动事件)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:事件(1) + QDP(1) + CP16(2) + CP56(7);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Create(IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time); + +/** + * 功能:解析Type 39(带CP56成组保护启动事件)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为11字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer(IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 40(带CP56保护输出电路信息)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:事件(1) + QDP(1) + CP16(2) + CP56(7);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Create(IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time); + +/** + * 功能:解析Type 40(带CP56保护输出电路信息)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为11字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Get_From_Buffer(IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 20(带变位检测的成组单点)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:SCD(4:状态16位 + 变位16位) + QDS(1);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_PackedSinglePointWithSCD_Create(IEC60870_PackedSinglePointWithSCD_t *self, + uint32_t ioa, uint16_t status, uint16_t change, int quality); + +/** + * 功能:解析Type 20(带变位检测的成组单点)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为5字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_PackedSinglePointWithSCD_Get_From_Buffer(IEC60870_PackedSinglePointWithSCD_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 21(无品质归一化测量值)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:NVA(2),不包含QDS;不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithoutQuality_Create(IEC60870_MeasuredValueNormalizedWithoutQuality_t *self, + uint32_t ioa, int16_t value); + +/** + * 功能:解析Type 21(无品质归一化测量值)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为2字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithoutQuality_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithoutQuality_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 51(32位比特串命令)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:BSI(4),不包含QDS;不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_Bitstring32Command_Create(IEC60870_Bitstring32Command_t *self, + uint32_t ioa, uint32_t value); + +/** + * 功能:解析Type 51(32位比特串命令)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为4字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_Bitstring32Command_Get_From_Buffer(IEC60870_Bitstring32Command_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 64(带CP56比特串命令)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:基础值(4) + CP56(7);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_Bitstring32CommandWithCP56Time2a_Create(IEC60870_Bitstring32CommandWithCP56Time2a_t *self, + uint32_t ioa, uint32_t value, const IEC60870_CP56Time2a_t *time); + +/** + * 功能:解析Type 64(带CP56比特串命令)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为11字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_Bitstring32CommandWithCP56Time2a_Get_From_Buffer(IEC60870_Bitstring32CommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 70(初始化结束)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:COI(1),地址适用性由应用判断;不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_EndOfInitialization_Create(IEC60870_EndOfInitialization_t *self, + uint32_t ioa, int cause); + +/** + * 功能:解析Type 70(初始化结束)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为1字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_EndOfInitialization_Get_From_Buffer(IEC60870_EndOfInitialization_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 104(无时标测试命令)。 + * 输入参数:ioa——0..0xFFFFFF;无附加值参数。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:固定测试图样AA 55;不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_TestCommand_Create(IEC60870_TestCommand_t *self, + uint32_t ioa); + +/** + * 功能:解析Type 104(无时标测试命令)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为2字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_TestCommand_Get_From_Buffer(IEC60870_TestCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 106(延时获得命令)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:CP16(2);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_DelayAcquisitionCommand_Create(IEC60870_DelayAcquisitionCommand_t *self, + uint32_t ioa, const IEC60870_CP16Time2a_t *delay); + +/** + * 功能:解析Type 106(延时获得命令)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为2字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_DelayAcquisitionCommand_Get_From_Buffer(IEC60870_DelayAcquisitionCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 110(归一化测量值参数)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:参数值(2) + QPM(1),QPM不是QDS;不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_ParameterNormalizedValue_Create(IEC60870_ParameterNormalizedValue_t *self, + uint32_t ioa, int16_t value, int qualifier); + +/** + * 功能:解析Type 110(归一化测量值参数)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为3字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_ParameterNormalizedValue_Get_From_Buffer(IEC60870_ParameterNormalizedValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 111(标度化测量值参数)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:参数值(2) + QPM(1),QPM不是QDS;不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_ParameterScaledValue_Create(IEC60870_ParameterScaledValue_t *self, + uint32_t ioa, int16_t value, int qualifier); + +/** + * 功能:解析Type 111(标度化测量值参数)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为3字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_ParameterScaledValue_Get_From_Buffer(IEC60870_ParameterScaledValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 112(短浮点测量值参数)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:参数值(4) + QPM(1),QPM不是QDS;不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_ParameterFloatValue_Create(IEC60870_ParameterFloatValue_t *self, + uint32_t ioa, float value, int qualifier); + +/** + * 功能:解析Type 112(短浮点测量值参数)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为5字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_ParameterFloatValue_Get_From_Buffer(IEC60870_ParameterFloatValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 113(参数激活)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:QPA(1);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_ParameterActivation_Create(IEC60870_ParameterActivation_t *self, + uint32_t ioa, int qualifier); + +/** + * 功能:解析Type 113(参数激活)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为1字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_ParameterActivation_Get_From_Buffer(IEC60870_ParameterActivation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 120(文件准备就绪)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:NOF(2) + LOF(3) + FRQ(1);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_FileReady_Create(IEC60870_FileReady_t *self, + uint32_t ioa, uint16_t file_name, uint32_t file_length, int qualifier); + +/** + * 功能:解析Type 120(文件准备就绪)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为6字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_FileReady_Get_From_Buffer(IEC60870_FileReady_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 121(节准备就绪)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:NOF(2) + NOS(1) + LOS(3) + SRQ(1);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_SectionReady_Create(IEC60870_SectionReady_t *self, + uint32_t ioa, uint16_t file_name, int section, uint32_t section_length, int qualifier); + +/** + * 功能:解析Type 121(节准备就绪)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为7字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_SectionReady_Get_From_Buffer(IEC60870_SectionReady_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 122(文件/节召唤或选择)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:NOF(2) + NOS(1) + SCQ(1);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_FileCallOrSelect_Create(IEC60870_FileCallOrSelect_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier); + +/** + * 功能:解析Type 122(文件/节召唤或选择)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为4字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_FileCallOrSelect_Get_From_Buffer(IEC60870_FileCallOrSelect_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 124(文件/节确认)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:NOF(2) + NOS(1) + AFQ(1);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_FileACK_Create(IEC60870_FileACK_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier); + +/** + * 功能:解析Type 124(文件/节确认)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为4字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_FileACK_Get_From_Buffer(IEC60870_FileACK_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 123(最后文件段或节)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:NOF(2) + NOS(1) + LSQ(1) + CHS(1),这里只传递校验和;不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_FileLastSegmentOrSection_Create(IEC60870_FileLastSegmentOrSection_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier, int checksum); + +/** + * 功能:解析Type 123(最后文件段或节)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为5字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_FileLastSegmentOrSection_Get_From_Buffer(IEC60870_FileLastSegmentOrSection_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 125(文件段)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:NOF(2) + NOS(1) + LOS(1) + 段数据(LOS),SQ=0且仅一个对象;不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_FileSegment_Create(IEC60870_FileSegment_t *self, + uint32_t ioa, uint16_t file_name, int section, const uint8_t *data, uint16_t data_length); + +/** + * 功能:解析Type 125(文件段)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须等于4+LOS,先校验长度再复制段数据;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_FileSegment_Get_From_Buffer(IEC60870_FileSegment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 126(文件目录)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:NOF(2) + LOF(3) + SOF(1) + CP56(7);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_FileDirectory_Create(IEC60870_FileDirectory_t *self, + uint32_t ioa, uint16_t file_name, uint32_t file_length, int status, const IEC60870_CP56Time2a_t *creation_time); + +/** + * 功能:解析Type 126(文件目录)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为13字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_FileDirectory_Get_From_Buffer(IEC60870_FileDirectory_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 127(归档查询)。 + * 输入参数:ioa——0..0xFFFFFF;其余参数见对应结构字段注释,时间和数据指针只在调用期间借用。 + * 输出参数:self——调用方提供的具体对象存储。 + * 返回值:OK成功;ERROR参数、字段或时间非法,输出不变。 + * 说明:NOF(2) + 查询起始CP56(7) + 查询结束CP56(7);不分配内存,不执行业务。 + */ +IEC60870_AsduResult_t IEC60870_QueryLog_Create(IEC60870_QueryLog_t *self, + uint32_t ioa, uint16_t file_name, const IEC60870_CP56Time2a_t *start_time, const IEC60870_CP56Time2a_t *stop_time); + +/** + * 功能:解析Type 127(归档查询)的独立值载荷。 + * 输入参数:ioa——已解析地址;payload——不含IOA;length——载荷实际字节数。 + * 输出参数:self——按值保存的独立对象,失败保持原值。 + * 返回值:OK成功;ERROR地址、长度、字段或时间非法。 + * 说明:长度必须恰好为16字节;不借用原报文。 + */ +IEC60870_AsduResult_t IEC60870_QueryLog_Get_From_Buffer(IEC60870_QueryLog_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:取得当前配置下一条ASDU能容纳的最大文件段数据长度。 + * 输入参数:parameters——合法应用层参数,包含最大ASDU长度和字段宽度。 + * 输出参数:无。 + * 返回值:0..255为数据容量;-1表示参数非法或连空文件段也容纳不下。 + * 说明:扣除公共头、IOA和4字节段头;104默认配置返回236。 + */ +int IEC60870_FileSegment_Get_Max_Data_Size(const IEC60870_AppLayerParameters_t *parameters); + +/** + * 功能:取得对象Type ID,并核对类型与函数表是否匹配。 + * 输入参数:object——已构造/解析的基类指针,可为NULL。 + * 输出参数:无。 + * 返回值:合法身份的Type ID;NULL、未知类型或函数表不匹配返回0。 + * 说明:不验证数值、品质、IOA和时标;实际编码入口负责这些检查。 + */ +uint8_t IEC60870_InformationObject_Get_Type(const IEC60870_InformationObject_t *object); + +/* 编码一个对象(可带IOA),不包含ASDU头。is_sequence=1表示省略本对象IOA, + * 仅供连续地址监视对象使用;SQ=1的首对象仍必须传0。ASDU入口负责连续性。 + * 参数须非空;IOA按sizeOfIOA检查。FULL表示空间不足,所有失败保留buffer/length。 + * buffer、length与对象不得重叠;length只在成功时写入实际字节数。 */ +IEC60870_AsduResult_t IEC60870_InformationObject_Encode( + const IEC60870_InformationObject_t *object, const IEC60870_AppLayerParameters_t *parameters, + int is_sequence, uint8_t *buffer, uint16_t capacity, uint16_t *length); + +/* 各类型Create都写入调用方提供的非空对象,ioa范围0..0xFFFFFF。 + * 参数中的指针(BCR、CP56)必须非空,成功后按值复制,不借用这些输入。 + * Get_From_Buffer只解码IOA之后的载荷,长度必须精确;ioa由ASDU层解析后传入。 + * 读命令的载荷长度为0,可以传NULL;其他类型要求非空载荷。 + * 所有失败均保留对象,调用方的输出对象不得与输入载荷重叠。 + * 归一化值采用原始int16_t;QU/QL/QOI/RQT/QRP与COT/CA/IOA的业务适用性由应用判断。 */ + +/** + * 功能:构造Type 1:单点信息,线上值载荷1字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——单点状态,0或1。 + * quality——品质仅允许BL/SB/NT/IV(0xF0范围内的位),不得夹带状态或保留位。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_SinglePointInformation_Create(IEC60870_SinglePointInformation_t *self, + uint32_t ioa, int value, int quality); +/** + * 功能:解析Type 1的1字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为1。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_SinglePointInformation_Get_From_Buffer(IEC60870_SinglePointInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 3:双点信息,线上值载荷1字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——双点状态,0..3,包含中间状态和不确定状态。 + * quality——品质仅允许BL/SB/NT/IV(0xF0范围内的位),不得夹带状态或保留位。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_DoublePointInformation_Create(IEC60870_DoublePointInformation_t *self, + uint32_t ioa, int value, int quality); +/** + * 功能:解析Type 3的1字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为1。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_DoublePointInformation_Get_From_Buffer(IEC60870_DoublePointInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 9:归一化测量值(原始16位定点值),线上值载荷3字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——归一化原始int16_t定点值,实际数值为value/32768.0。 + * quality——QDS品质仅允许OV/BL/SB/NT/IV(0xF1范围内的位),bit1..3必须为0。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalized_Create(IEC60870_MeasuredValueNormalized_t *self, + uint32_t ioa, int16_t value, int quality); +/** + * 功能:解析Type 9的3字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为3。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalized_Get_From_Buffer(IEC60870_MeasuredValueNormalized_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 11:标度化测量值,线上值载荷3字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——有符号16位标度值,范围-32768..32767。 + * quality——QDS品质仅允许OV/BL/SB/NT/IV(0xF1范围内的位),bit1..3必须为0。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueScaled_Create(IEC60870_MeasuredValueScaled_t *self, + uint32_t ioa, int16_t value, int quality); +/** + * 功能:解析Type 11的3字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为3。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueScaled_Get_From_Buffer(IEC60870_MeasuredValueScaled_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 13:短浮点测量值,线上值载荷5字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——短浮点值,采用IEEE 754 binary32,不进行NaN/Inf业务过滤。 + * quality——QDS品质仅允许OV/BL/SB/NT/IV(0xF1范围内的位),bit1..3必须为0。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueShort_Create(IEC60870_MeasuredValueShort_t *self, + uint32_t ioa, float value, int quality); +/** + * 功能:解析Type 13的5字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为5。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueShort_Get_From_Buffer(IEC60870_MeasuredValueShort_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 15:累计量,线上值载荷5字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——非空BCR指针;计数为int32_t,序号0..31,CY/CA/IV均为0/1。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_IntegratedTotals_Create(IEC60870_IntegratedTotals_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value); +/** + * 功能:解析Type 15的5字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为5。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_IntegratedTotals_Get_From_Buffer(IEC60870_IntegratedTotals_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 30:带时标单点信息,线上值载荷8字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——单点状态,0或1。 + * quality——品质仅允许BL/SB/NT/IV(0xF0范围内的位),不得夹带状态或保留位。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_SinglePointWithCP56Time2a_Create(IEC60870_SinglePointWithCP56Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP56Time2a_t *time); +/** + * 功能:解析Type 30的8字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为8。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_SinglePointWithCP56Time2a_Get_From_Buffer(IEC60870_SinglePointWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 31:带时标双点信息,线上值载荷8字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——双点状态,0..3,包含中间状态和不确定状态。 + * quality——品质仅允许BL/SB/NT/IV(0xF0范围内的位),不得夹带状态或保留位。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_DoublePointWithCP56Time2a_Create(IEC60870_DoublePointWithCP56Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP56Time2a_t *time); +/** + * 功能:解析Type 31的8字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为8。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_DoublePointWithCP56Time2a_Get_From_Buffer(IEC60870_DoublePointWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 34:带时标归一化测量值,线上值载荷10字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——归一化原始int16_t定点值,实际数值为value/32768.0。 + * quality——QDS品质仅允许OV/BL/SB/NT/IV(0xF1范围内的位),bit1..3必须为0。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP56Time2a_Create(IEC60870_MeasuredValueNormalizedWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP56Time2a_t *time); +/** + * 功能:解析Type 34的10字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为10。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 35:带时标标度化测量值,线上值载荷10字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——有符号16位标度值,范围-32768..32767。 + * quality——QDS品质仅允许OV/BL/SB/NT/IV(0xF1范围内的位),bit1..3必须为0。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP56Time2a_Create(IEC60870_MeasuredValueScaledWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP56Time2a_t *time); +/** + * 功能:解析Type 35的10字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为10。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueScaledWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 36:带时标短浮点测量值,线上值载荷12字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——短浮点值,采用IEEE 754 binary32,不进行NaN/Inf业务过滤。 + * quality——QDS品质仅允许OV/BL/SB/NT/IV(0xF1范围内的位),bit1..3必须为0。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP56Time2a_Create(IEC60870_MeasuredValueShortWithCP56Time2a_t *self, + uint32_t ioa, float value, int quality, const IEC60870_CP56Time2a_t *time); +/** + * 功能:解析Type 36的12字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为12。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueShortWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 37:带时标累计量,线上值载荷12字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——非空BCR指针;计数为int32_t,序号0..31,CY/CA/IV均为0/1。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP56Time2a_Create(IEC60870_IntegratedTotalsWithCP56Time2a_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value, const IEC60870_CP56Time2a_t *time); +/** + * 功能:解析Type 37的12字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为12。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP56Time2a_Get_From_Buffer(IEC60870_IntegratedTotalsWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 45:单命令,线上值载荷1字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * state——单命令状态,0或1;SCO保留位由编码器保持为0。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——命令限定词QU,0..31;具体业务含义由应用判断。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_SingleCommand_Create(IEC60870_SingleCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier); +/** + * 功能:解析Type 45的1字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为1。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_SingleCommand_Get_From_Buffer(IEC60870_SingleCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 46:双命令,线上值载荷1字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * state——双命令/步调节状态,仅允许1或2。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——命令限定词QU,0..31;具体业务含义由应用判断。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_DoubleCommand_Create(IEC60870_DoubleCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier); +/** + * 功能:解析Type 46的1字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为1。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_DoubleCommand_Get_From_Buffer(IEC60870_DoubleCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 47:步调节命令,线上值载荷1字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * state——双命令/步调节状态,仅允许1或2。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——命令限定词QU,0..31;具体业务含义由应用判断。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_StepCommand_Create(IEC60870_StepCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier); +/** + * 功能:解析Type 47的1字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为1。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_StepCommand_Get_From_Buffer(IEC60870_StepCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 48:归一化设定值命令(原始16位定点值),线上值载荷3字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——归一化原始int16_t定点值,实际数值为value/32768.0。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——设定值限定词QL,0..127;编码到QOS低7位。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalized_Create(IEC60870_SetpointCommandNormalized_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier); +/** + * 功能:解析Type 48的3字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为3。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalized_Get_From_Buffer(IEC60870_SetpointCommandNormalized_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 49:标度化设定值命令,线上值载荷3字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——有符号16位标度值,范围-32768..32767。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——设定值限定词QL,0..127;编码到QOS低7位。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandScaled_Create(IEC60870_SetpointCommandScaled_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier); +/** + * 功能:解析Type 49的3字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为3。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandScaled_Get_From_Buffer(IEC60870_SetpointCommandScaled_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 50:短浮点设定值命令,线上值载荷5字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——短浮点值,采用IEEE 754 binary32,不进行NaN/Inf业务过滤。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——设定值限定词QL,0..127;编码到QOS低7位。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandShort_Create(IEC60870_SetpointCommandShort_t *self, + uint32_t ioa, float value, int is_select, int qualifier); +/** + * 功能:解析Type 50的5字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为5。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandShort_Get_From_Buffer(IEC60870_SetpointCommandShort_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 58:带时标单命令,线上值载荷8字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * state——单命令状态,0或1;SCO保留位由编码器保持为0。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——命令限定词QU,0..31;具体业务含义由应用判断。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_SingleCommandWithCP56Time2a_Create(IEC60870_SingleCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +/** + * 功能:解析Type 58的8字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为8。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_SingleCommandWithCP56Time2a_Get_From_Buffer(IEC60870_SingleCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 59:带时标双命令,线上值载荷8字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * state——双命令/步调节状态,仅允许1或2。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——命令限定词QU,0..31;具体业务含义由应用判断。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_DoubleCommandWithCP56Time2a_Create(IEC60870_DoubleCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +/** + * 功能:解析Type 59的8字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为8。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_DoubleCommandWithCP56Time2a_Get_From_Buffer(IEC60870_DoubleCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 60:带时标步调节命令,线上值载荷8字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * state——双命令/步调节状态,仅允许1或2。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——命令限定词QU,0..31;具体业务含义由应用判断。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_StepCommandWithCP56Time2a_Create(IEC60870_StepCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +/** + * 功能:解析Type 60的8字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为8。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_StepCommandWithCP56Time2a_Get_From_Buffer(IEC60870_StepCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 61:带时标归一化设定值命令,线上值载荷10字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——归一化原始int16_t定点值,实际数值为value/32768.0。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——设定值限定词QL,0..127;编码到QOS低7位。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalizedWithCP56Time2a_Create(IEC60870_SetpointCommandNormalizedWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +/** + * 功能:解析Type 61的10字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为10。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalizedWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandNormalizedWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 62:带时标标度化设定值命令,线上值载荷10字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——有符号16位标度值,范围-32768..32767。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——设定值限定词QL,0..127;编码到QOS低7位。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandScaledWithCP56Time2a_Create(IEC60870_SetpointCommandScaledWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +/** + * 功能:解析Type 62的10字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为10。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandScaledWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandScaledWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 63:带时标短浮点设定值命令,线上值载荷12字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * value——短浮点值,采用IEEE 754 binary32,不进行NaN/Inf业务过滤。 + * is_select——选择/执行标志S/E,1为选择、0为执行;本函数不执行命令。 + * qualifier——设定值限定词QL,0..127;编码到QOS低7位。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandShortWithCP56Time2a_Create(IEC60870_SetpointCommandShortWithCP56Time2a_t *self, + uint32_t ioa, float value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); +/** + * 功能:解析Type 63的12字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为12。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_SetpointCommandShortWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandShortWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 100:总召唤命令,线上值载荷1字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * qualifier——总召唤限定词QOI,0..255;本层保留全部编码值。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_InterrogationCommand_Create(IEC60870_InterrogationCommand_t *self, + uint32_t ioa, int qualifier); +/** + * 功能:解析Type 100的1字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为1。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_InterrogationCommand_Get_From_Buffer(IEC60870_InterrogationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 101:电能脉冲召唤命令,线上值载荷1字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * qualifier——电能脉冲请求限定词RQT,0..63。 + * freeze——冻结限定词FR,0..3,编码到QCC高2位。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_CounterInterrogationCommand_Create(IEC60870_CounterInterrogationCommand_t *self, + uint32_t ioa, int qualifier, int freeze); +/** + * 功能:解析Type 101的1字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为1。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_CounterInterrogationCommand_Get_From_Buffer(IEC60870_CounterInterrogationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 102:读命令,线上值载荷0字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_ReadCommand_Create(IEC60870_ReadCommand_t *self, + uint32_t ioa); +/** + * 功能:解析Type 102的0字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,本类型允许NULL。 + * length——必须恰好为0。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_ReadCommand_Get_From_Buffer(IEC60870_ReadCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 103:时钟同步命令,线上值载荷7字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_ClockSynchronizationCommand_Create(IEC60870_ClockSynchronizationCommand_t *self, + uint32_t ioa, const IEC60870_CP56Time2a_t *time); +/** + * 功能:解析Type 103的7字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为7。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_ClockSynchronizationCommand_Get_From_Buffer(IEC60870_ClockSynchronizationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 105:复位进程命令,线上值载荷1字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * qualifier——复位进程限定词QRP,0..255;本层不执行复位。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_ResetProcessCommand_Create(IEC60870_ResetProcessCommand_t *self, + uint32_t ioa, int qualifier); +/** + * 功能:解析Type 105的1字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为1。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_ResetProcessCommand_Get_From_Buffer(IEC60870_ResetProcessCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +/** + * 功能:构造Type 107:带时标测试命令,线上值载荷9字节,不含IOA。 + * 输入参数:ioa——信息对象地址,0..0xFFFFFF;编码时再按配置的IOA宽度检查上限。 + * sequence——16位测试序号,0..65535,与APCI的N(S)/N(R)无关。 + * time——非空CP56日历指针,采用2000..2099年约定;保留IV/SU,成功后按值复制。 + * 输出参数:self——调用方提供的非空具体对象;成功后可直接传给ASDU添加入口。 + * 返回值:OK表示构造成功,ERROR表示参数或字段非法;失败保留原对象,不分配内存。 + */ +IEC60870_AsduResult_t IEC60870_TestCommandWithCP56Time2a_Create(IEC60870_TestCommandWithCP56Time2a_t *self, + uint32_t ioa, uint16_t sequence, const IEC60870_CP56Time2a_t *time); +/** + * 功能:解析Type 107的9字节值载荷,IOA由调用方单独传入。 + * 输入参数:ioa——已解析/还原的地址,0..0xFFFFFF。 + * payload——不含IOA的载荷,必须非空。 + * length——必须恰好为9。 + * 输出参数:self——非空具体对象,成功后不借用输入;失败保持原值。 + * 返回值:OK表示解析成功,ERROR表示参数、长度或字段非法。 + */ +IEC60870_AsduResult_t IEC60870_TestCommandWithCP56Time2a_Get_From_Buffer(IEC60870_TestCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +#ifdef __cplusplus +} +#endif + +#endif /* IEC60870_INFORMATION_OBJECTS_H */ + diff --git a/IEC60870-Slave/.build/command-v1/before/common/IEC60870_InformationObjects_Internal.h b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_InformationObjects_Internal.h new file mode 100644 index 00000000..231680fe --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_InformationObjects_Internal.h @@ -0,0 +1,83 @@ +/* 信息对象与ASDU的内部协作,不作为业务API。 + * ASDU使用类型描述计算布局;具体对象通过只读函数表编码;解码按Type ID另行分派。 + * 字节辅助函数不读写连接状态,也不依赖结构体对齐或主机字节序。 */ +#ifndef IEC60870_INFORMATION_OBJECTS_INTERNAL_H +#define IEC60870_INFORMATION_OBJECTS_INTERNAL_H + +#include "IEC60870_InformationObjects.h" + +/* 单个文件段LOS最多255字节,另有4字节文件段头和最多3字节IOA。 + * 即使对象可表示更长载荷,ASDU容器仍按配置的总长上限返回FULL。 */ +#define IEC60870_OBJECT_MAX_PAYLOAD 259U +#define IEC60870_OBJECT_MAX_ENCODED (IEC60870_OBJECT_MAX_PAYLOAD + 3U) + +/* 每个类型独立的只读编码函数表;解码仍由Type ID选择具体类型的解析函数。 + * encode仅写临时载荷,公共入口验证后原子提交;变长类型提供两个长度回调。 */ +typedef struct IEC60870_InformationObjectVTable { + uint8_t type_id; /* 线上类型标识;同时用于核对对象基类身份。 */ + uint16_t payload_size; /* 固定类型的精确长度;变长类型的最低长度,不含IOA。 */ + uint8_t single_object; /* 1表示仅允许一个对象;SQ约束由supports_sequence独立决定。 */ + size_t object_size; /* 具体C结构的sizeof,用于输出存储检查,与线上长度无关。 */ + int (*encode)(const IEC60870_InformationObject_t *object, uint8_t *payload); /* 写临时载荷,1成功/0字段非法。 */ + uint8_t supports_sequence; /* 1允许SQ=1;参数类型允许多个对象但仅支持SQ=0。 */ + uint16_t (*get_payload_size)(const IEC60870_InformationObject_t *object); /* NULL为固定长度。 */ + uint16_t (*get_payload_size_from_buffer)(const uint8_t *payload, uint16_t length); /* 变长载荷精确长度;0非法。 */ +} IEC60870_InformationObjectVTable; + +/* 未支持的Type ID返回NULL;地址和长度校验由使用方在读报文前完成。 */ +const IEC60870_InformationObjectVTable *IEC60870_InformationObject_Get_Descriptor(uint8_t type_id); + +/* 按Type ID调用具体Get_From_Buffer;object==NULL时仅验证,不输出。 + * object_size是调用方实际提供的容量,必须至少为所选具体类型的sizeof; + * 非空object必须对应该具体结构,尺寸足够不能替代类型匹配。失败不修改输出。 */ +IEC60870_AsduResult_t IEC60870_InformationObject_Decode(uint8_t type_id, uint32_t ioa, + const uint8_t *payload, uint16_t length, void *object, size_t object_size); + +/** + * 功能:从低字节在前的字节序列读取无符号值。 + * 输入参数:data——至少length字节的有效输入,调用方先完成边界检查。 + * length——1..4,用于1/2/3字节地址或2/4字节数值。 + * 输出参数:无。 + * 返回值:按位拼接的uint32_t,未使用高位为0。 + * 说明:逐字节读,允许未对齐输入;不检查指针或范围。 + */ +static inline uint32_t IEC60870_Object_Read_LE(const uint8_t *data, unsigned int length) +{ + uint32_t value = 0; + unsigned int i; + for (i = 0; i < length; ++i) { + value |= (uint32_t)data[i] << (8U * i); + } + return value; +} + +/** + * 功能:将数值低length字节按低字节在前写入报文。 + * 输入参数:value——待写数值;需要保留的高位是否越界由调用方预先检查。 + * length——1..4;只写指定字节数,不写尾部填充。 + * 输出参数:data——至少length字节的可写区域。 + * 返回值:无。 + * 说明:逐字节写,不要求对齐;IOA必须先按所选宽度校验,防止被静默截断。 + */ +static inline void IEC60870_Object_Write_LE(uint8_t *data, uint32_t value, unsigned int length) +{ + unsigned int i; + for (i = 0; i < length; ++i) { + data[i] = (uint8_t)(value >> (8U * i)); + } +} + +/** + * 功能:计算所选IOA字段宽度能够表示的最大地址。 + * 输入参数:parameters——非空且已通过AppLayerParameters_Valid校验,sizeOfIOA为1..3。 + * 输出参数:无。 + * 返回值:分别为0xFF、0xFFFF或0xFFFFFF。 + * 说明:位移最多24位;本函数不检查参数,非法宽度不得传入。 + */ +static inline uint32_t IEC60870_Object_Max_IOA(const IEC60870_AppLayerParameters_t *parameters) +{ + return (UINT32_C(1) << (8U * (unsigned int)parameters->sizeOfIOA)) - 1U; +} + +#endif /* IEC60870_INFORMATION_OBJECTS_INTERNAL_H */ + diff --git a/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Slave.c b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Slave.c new file mode 100644 index 00000000..7ce602d6 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Slave.c @@ -0,0 +1,53 @@ +/* 共用发送入口只做接口分派;101/104分别提供其实际处理函数。 */ +#include "IEC60870_Slave.h" + +/** + * 功能:将普通ASDU发送请求分派给对应协议。 + * 输入参数:self——连接接口;asdu——待提交ASDU。 + * 输出参数:由接入实现记录提交结果。 + * 返回值:接入函数的布尔结果;连接或函数指针为空时返回false。 + * 说明:ASDU合法性由接入实现检查,便于同一次调用记录准确的失败原因。 + */ +bool IEC60870_MasterConnection_Send_ASDU(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu) +{ + return self != NULL && self->send_asdu != NULL && self->send_asdu(self, asdu); +} + +/** + * 功能:将激活确认请求分派给对应协议。 + * 输入参数:self——连接接口;asdu——可修改ASDU;negative——否定标志。 + * 输出参数:由接入实现修改ASDU并记录提交结果。 + * 返回值:接入函数的布尔结果;连接或函数指针为空时返回false。 + * 说明:共用入口不依赖TCP或串口,不保存请求或执行重试。 + */ +bool IEC60870_MasterConnection_Send_ACT_CON(IEC60870_MasterConnection_t *self, + IEC60870_Asdu_t *asdu, bool negative) +{ + return self != NULL && self->send_act_con != NULL && self->send_act_con(self, asdu, negative); +} + +/** + * 功能:将激活终止请求分派给对应协议。 + * 输入参数:self——连接接口;asdu——可修改ASDU。 + * 输出参数:由接入实现修改ASDU并记录提交结果。 + * 返回值:接入函数的布尔结果;连接或函数指针为空时返回false。 + * 说明:本入口不判断业务是否完成,调用时机由业务决定。 + */ +bool IEC60870_MasterConnection_Send_ACT_TERM(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu) +{ + return self != NULL && self->send_act_term != NULL && self->send_act_term(self, asdu); +} + +/** + * 功能:通过协议接入获取应用层参数。 + * 输入参数:self——连接接口。 + * 输出参数:无。 + * 返回值:接入提供的只读参数指针;连接或函数指针为空时返回NULL。 + * 说明:不创建默认参数,不改变连接状态,也不延长参数的生命周期。 + */ +const IEC60870_AppLayerParameters_t *IEC60870_MasterConnection_Get_Application_Layer_Parameters( + IEC60870_MasterConnection_t *self) +{ + if (self == NULL || self->get_application_layer_parameters == NULL) { return NULL; } + return self->get_application_layer_parameters(self); +} diff --git a/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Slave.h b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Slave.h new file mode 100644 index 00000000..7a01ae75 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Slave.h @@ -0,0 +1,73 @@ +/* + * 101/104共用的子站发送接口,表示一条面向主站的连接。 + * 共用入口通过函数指针分派;具体发送、确认和终止由对应协议接入实现。 + * 本层不包含TCP、串口、队列或连接状态机,不分配内存。 + */ +#ifndef IEC60870_SLAVE_H +#define IEC60870_SLAVE_H + +#include "IEC60870_Asdu.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct IEC60870_MasterConnection IEC60870_MasterConnection_t; + +/* 接入实现初始化此接口并保证object及函数指针有效;业务通过下面的函数调用。 + * 参数及ASDU的实际布局必须与所接协议一致。所有调用由所属事件循环串行执行。 + * send返回true前必须复制或消耗ASDU原文,不能在返回后继续借用调用方字节。 + * false不统一表示可重试,具体接入实现负责提供失败原因和处理约定。 */ +struct IEC60870_MasterConnection { + void *object; /* 协议接入上下文;共用入口不解释此指针。 */ + bool (*send_asdu)(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu); + bool (*send_act_con)(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu, bool negative); + bool (*send_act_term)(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu); + const IEC60870_AppLayerParameters_t *(*get_application_layer_parameters)( + IEC60870_MasterConnection_t *self); +}; + +/** + * 功能:通过当前连接提交一条已构造好的ASDU。 + * 输入参数:self——已由协议接入初始化的连接;asdu——有效ASDU对象。 + * 输出参数:具体接入实现记录本次提交结果;本入口不修改ASDU字段。 + * 返回值:true表示已发送或已被发送模块接手;false表示失败。 + * 说明:true不等于主站已确认;false后按具体协议的结果处理,不自动重试。 + */ +bool IEC60870_MasterConnection_Send_ASDU(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu); + +/** + * 功能:通过当前连接发送激活确认。 + * 输入参数:self——已初始化连接;asdu——可修改ASDU;negative——是否否定确认。 + * 输出参数:asdu原地设置COT=7及指定P/N;具体接入实现记录提交结果。 + * 返回值:true表示发送模块已接手;false表示失败。 + * 说明:字段在提交前修改,提交失败也不会恢复;不执行请求中的业务命令。 + */ +bool IEC60870_MasterConnection_Send_ACT_CON(IEC60870_MasterConnection_t *self, + IEC60870_Asdu_t *asdu, bool negative); + +/** + * 功能:通过当前连接发送激活终止。 + * 输入参数:self——已初始化连接;asdu——对应请求的可修改ASDU对象。 + * 输出参数:asdu原地设置COT=10、P/N=0;具体接入实现记录提交结果。 + * 返回值:true表示发送模块已接手;false表示失败。 + * 说明:业务在相应处理完成后调用;提交失败也不恢复已修改的字段。 + */ +bool IEC60870_MasterConnection_Send_ACT_TERM(IEC60870_MasterConnection_t *self, + IEC60870_Asdu_t *asdu); + +/** + * 功能:获取当前连接使用的应用层参数。 + * 输入参数:self——已初始化连接。 + * 输出参数:无。 + * 返回值:借用的只读参数指针;接口为空或未实现时返回NULL。 + * 说明:返回指针的有效期由协议接入定义;调用方不能修改参数。 + */ +const IEC60870_AppLayerParameters_t *IEC60870_MasterConnection_Get_Application_Layer_Parameters( + IEC60870_MasterConnection_t *self); + +#ifdef __cplusplus +} +#endif +#endif /* IEC60870_SLAVE_H */ diff --git a/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Time.c b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Time.c new file mode 100644 index 00000000..b716032f --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Time.c @@ -0,0 +1,502 @@ +/* + * IEC 60870-5-101/104 公共时间工具:日历计算、CP56Time2a 编解码及本地标准时间转换。 + * 供 101 和 104 的业务层按需调用,编解码不读取协议类型或链路状态。 + * CP56 编解码保留 IV 和 SU;转本地标准时间时检查 IV,处理 SU 和源时区,返回完整日历。 + * 公共编码允许 SU=0/1;后续业务发送入口负责提供本地标准时间日历并设置 SU=0。 + * 本文件只提供公共工具,不自动接入 ASDU 业务处理或执行校时。 + * 整数公历换算按 400 年周期展开,将三月作为年内起点以统一处理二月和闰日。 + * 仅在校验后的 1970..2099 年范围内运算,天数及秒数均可用 uint32_t 表示。 + * 时间戳转日历可选平台库或纯整数算法;接入层提供约定的标准时区偏移,负责系统时钟、RTC 和单调计时。 + */ +#include "IEC60870_Time.h" + +#include + +/** + * 功能:编码两字节经过时间。 + * 输入参数:value——毫秒值;capacity——out的实际容量。 + * 输出参数:out——成功写入2字节,失败不变。 + * 返回值:1成功;0空指针或容量不足。 + * 说明:先保存输入,允许输入输出重叠;不将经过时间解释为日历时间。 + */ +int IEC60870_Time_To_CP16(const IEC60870_CP16Time2a_t *value, uint8_t *out, size_t capacity) +{ + uint16_t milliseconds; + if (value == NULL || out == NULL || capacity < IEC60870_CP16_LEN) { return 0; } + milliseconds = value->milliseconds; + out[0] = (uint8_t)milliseconds; + out[1] = (uint8_t)(milliseconds >> 8); + return 1; +} + +/** + * 功能:解码两字节经过时间。 + * 输入参数:in——低字节在前的输入;length——至少2字节。 + * 输出参数:value——经过时间,失败不变。 + * 返回值:1成功;0空指针或长度不足。 + * 说明:CP16使用完整16位数值范围,不含质量标志。 + */ +int IEC60870_Time_From_CP16(const uint8_t *in, size_t length, IEC60870_CP16Time2a_t *value) +{ + IEC60870_CP16Time2a_t result; + if (in == NULL || value == NULL || length < IEC60870_CP16_LEN) { return 0; } + result.milliseconds = (uint16_t)((uint16_t)in[0] | ((uint16_t)in[1] << 8)); + *value = result; + return 1; +} + +/** + * 功能:编码三字节短时标。 + * 输入参数:value——分钟、分钟内毫秒和IV;capacity——out容量。 + * 输出参数:out——成功写入3字节,失败不变。 + * 返回值:1成功;0参数或字段非法。 + * 说明:分钟0..59,毫秒0..59999,IV为0/1;第三字节bit6始终为0。 + */ +int IEC60870_Time_To_CP24(const IEC60870_CP24Time2a_t *value, uint8_t *out, size_t capacity) +{ + uint8_t encoded[IEC60870_CP24_LEN]; + if (value == NULL || out == NULL || capacity < sizeof(encoded) || + value->milliseconds > 59999U || value->minute > 59U || value->is_invalid > 1U) { return 0; } + encoded[0] = (uint8_t)value->milliseconds; + encoded[1] = (uint8_t)(value->milliseconds >> 8); + encoded[2] = (uint8_t)(value->minute | (value->is_invalid << 7)); + memcpy(out, encoded, sizeof(encoded)); + return 1; +} + +/** + * 功能:校验并解码三字节短时标。 + * 输入参数:in——至少3字节;length——实际可读长度。 + * 输出参数:value——成功保存解析值;失败保持原值。 + * 返回值:1成功;0长度、保留位、分钟或毫秒非法。 + * 说明:IV保留;调用方结合已有时间基准恢复小时及日期。 + */ +int IEC60870_Time_From_CP24(const uint8_t *in, size_t length, IEC60870_CP24Time2a_t *value) +{ + IEC60870_CP24Time2a_t result = {0}; + if (in == NULL || value == NULL || length < IEC60870_CP24_LEN || (in[2] & 0x40U)) { return 0; } + result.milliseconds = (uint16_t)((uint16_t)in[0] | ((uint16_t)in[1] << 8)); + result.minute = in[2] & 0x3FU; + result.is_invalid = in[2] >> 7; + if (result.milliseconds > 59999U || result.minute > 59U) { return 0; } + *value = result; + return 1; +} + +#if IEC60870_TIME_USE_PLATFORM != 0 && IEC60870_TIME_USE_PLATFORM != 1 +#error "IEC60870_TIME_USE_PLATFORM must be 0 or 1" +#endif + +#if IEC60870_TIME_USE_PLATFORM +#ifndef _WIN32 +#error "Platform time conversion currently requires Windows; use IEC60870_TIME_USE_PLATFORM=0" +#endif +#include +#endif + +/* + * 函数功能:判断公历闰年。 + * 入口参数:year——公历年份;本函数只执行闰年规则,不检查模块支持的年份范围。 + * 返回值:1 为闰年,0 为平年。 + * 说明:能被 4 整除且不能被 100 整除,或能被 400 整除的年份为闰年。 + */ +static int Is_Leap(uint32_t year) +{ + return ((year % 4U) == 0U && (year % 100U) != 0U) || (year % 400U) == 0U; +} + +/* + * 函数功能:取得指定公历月份的实际天数。 + * 入口参数:year——公历年份;month——月份,合法范围为 1..12。 + * 返回值:合法月份返回 28..31,非法月份返回 0。 + * 说明:先检查月份再访问表,闰年二月单独返回 29;年份范围由上层日期检查负责。 + */ +static uint32_t Days_In_Month(uint32_t year, uint32_t month) +{ + static const uint8_t days[12] = {31U, 28U, 31U, 30U, 31U, 30U, + 31U, 31U, 30U, 31U, 30U, 31U}; + + if (month < 1U || month > 12U) { + return 0U; + } + if (month == 2U && Is_Leap(year)) { + return 29U; + } + return days[month - 1U]; +} + +/* + * 函数功能:统一检查年份范围和日期是否真实存在。 + * 入口参数:year——1970..2099;month——1..12;day——待检查的日号。 + * 返回值:1 为合法日期,0 为年份、月份或日号非法。 + * 说明:按当月实际天数检查 day,不接受平年二月二十九;非法月份的天数为 0,也会被拒绝。 + */ +static int Date_Is_Valid(uint32_t year, uint32_t month, uint32_t day) +{ + uint32_t limit; + + if (year < IEC60870_TIME_MIN_YEAR || year > IEC60870_TIME_MAX_YEAR) { + return 0; + } + limit = Days_In_Month(year, month); + return day >= 1U && day <= limit; +} + +/* + * 函数功能:检查完整日历值的日期、时分秒和毫秒范围。 + * 入口参数:time——待检查的日历指针,可为空。 + * 返回值:1 为数值合法,0 为空指针、无效日期或时间字段越界。 + * 说明:小时 0..23,分/秒 0..59,毫秒 0..999;weekday 为派生字段,不参与检查。 + * 这里只检查数值,不能据此判断时间基准、时钟是否准确或该值是否允许用于校时。 + */ +static int WallTime_Is_Valid(const IEC60870_WallTime_t *time) +{ + return time != NULL && Date_Is_Valid(time->year, time->month, time->day) && + time->hour <= 23U && time->minute <= 59U && time->second <= 59U && time->ms <= 999U; +} + +/* + * 函数功能:将公历日期转换为自 1970-01-01 起的完整天数。 + * 入口参数:year、month、day——已校验的合法日期,年份须在 1970..2099 内。 + * 返回值:完整天数,0 对应 1970-01-01;本内部函数不重复校验参数。 + * 说明:将三月作为年首,一、二月归入上一年,使闰日统一落在年末。 + * era 为 400 年周期编号,yoe 为周期内年份,mp 为从三月起算的月份编号 0..11。 + * doy 为从三月起算的年内天数,doe 为周期内天数;每 400 年有 146097 天。 + * 最后减去 719468,将算法的天数原点平移到 1970-01-01。 + */ +static uint32_t Days_From_Civil(uint32_t year, uint32_t month, uint32_t day) +{ + uint32_t y = (month <= 2U) ? (year - 1U) : year; + uint32_t era = y / 400U; + uint32_t yoe = y - era * 400U; + uint32_t mp = (month > 2U) ? (month - 3U) : (month + 9U); + uint32_t doy = (153U * mp + 2U) / 5U + day - 1U; + uint32_t doe = yoe * 365U + yoe / 4U - yoe / 100U + doy; + + return era * 146097U + doe - 719468U; +} + +/* + * 函数功能:将已校验的日历字段展开为相对 1970-01-01 00:00:00 的线性秒数。 + * 入口参数:time——非空且已通过 WallTime_Is_Valid 检查的日历值。 + * 返回值:0..IEC60870_TIME_MAX_SECONDS 的完整秒数,不包含毫秒。 + * 说明:此处只计算天数和日内秒数,忽略 ms、weekday,不处理时区或 SU。 + * 只有输入日历本身采用 UTC 时,结果才可直接作为 UTC 纪元秒数使用。 + */ +static uint32_t Seconds_From_Civil(const IEC60870_WallTime_t *time) +{ + return Days_From_Civil(time->year, time->month, time->day) * 86400U + + (uint32_t)time->hour * 3600U + (uint32_t)time->minute * 60U + (uint32_t)time->second; +} + +/* + * 函数功能:根据自 1970-01-01 起的完整天数计算星期。 + * 入口参数:days——模块支持年份内的天数,0 对应 1970-01-01。 + * 返回值:0=周日,1=周一,...,6=周六。 + * 说明:纪元日是周四,因此先加 4 再对 7 取余;本内部函数不检查日期范围。 + */ +static uint8_t Weekday_From_Days(uint32_t days) +{ + return (uint8_t)((days + 4U) % 7U); +} + +/* + * 函数功能:供时间源适配等调用方复用日历合法性检查,不把本地日历当作UTC转秒数。 + * 入口参数:time——待检查的日历指针,可以为空;weekday不参与检查。 + * 返回值:1 年月日、时分秒和毫秒在模块范围内;0 参数非法。 + */ +int IEC60870_Time_Is_Valid(const IEC60870_WallTime_t *time) +{ + return WallTime_Is_Valid(time); +} + +/* + * 函数功能:校验公历日期并计算星期。 + * 入口参数:year——1970..2099;month——1..12;day——当月日号。 + * 出口参数:weekday——接收 0..6 的星期值,0 表示周日。 + * 返回值:1 成功;0 日期非法或输出指针为空,失败时输出不变。 + */ +int IEC60870_Time_Get_Weekday(uint16_t year, uint8_t month, uint8_t day, uint8_t *weekday) +{ + if (weekday == NULL || !Date_Is_Valid(year, month, day)) { + return 0; + } + *weekday = Weekday_From_Days(Days_From_Civil(year, month, day)); + return 1; +} + +/* + * 函数功能:将 UTC 日历转换为自 1970-01-01 00:00:00 UTC 起的完整秒数。 + * 入口参数:time——1970..2099 年的合法 UTC 日历;ms 须在 0..999 内。 + * 出口参数:seconds——接收 0..IEC60870_TIME_MAX_SECONDS;0 是有效时间值。 + * 返回值:1 成功;0 参数非法,失败时输出不变。 + * 说明:日历类型没有时区或 SU,调用方须保证输入采用 UTC;ms 只检查范围,不计入完整秒数。 + * 含 IV/SU 的 CP56 解析值应使用 CP56_To_Seconds,不能直接丢弃标志后调用本函数。 + */ +int IEC60870_Time_To_Seconds(const IEC60870_WallTime_t *time, uint32_t *seconds) +{ + uint32_t result; + + if (seconds == NULL || !WallTime_Is_Valid(time)) { + return 0; + } + result = Seconds_From_Civil(time); + *seconds = result; + return 1; +} + +/* + * 函数功能:将 UTC 纪元秒数和毫秒余数还原为完整公历时间。 + * 入口参数:seconds——0..IEC60870_TIME_MAX_SECONDS;ms——0..999。 + * 出口参数:time——接收 UTC 年月日、时分秒、毫秒及重新计算的 weekday。 + * 返回值:1 成功;0 参数非法或平台转换失败,失败时输出不变,不对越界毫秒进行截断或进位。 + * 说明:编译时选择平台库或整数算法;使用局部结果,全部完成后才写回调用方输出。 + */ +int IEC60870_Time_From_Seconds(uint32_t seconds, uint16_t ms, IEC60870_WallTime_t *time) +{ + IEC60870_WallTime_t result = {0}; +#if IEC60870_TIME_USE_PLATFORM + __time64_t stamp; + struct tm utc; +#else + uint32_t days, rem, z, era, doe, yoe, doy, mp, month, year; +#endif + + if (time == NULL || seconds > IEC60870_TIME_MAX_SECONDS || ms > 999U) { + return 0; + } +#if IEC60870_TIME_USE_PLATFORM + /* 显式使用 64 位接口,避免 32 位 time_t 的 2038 年上限;转换结果始终为 UTC。 */ + stamp = (__time64_t)seconds; + if (_gmtime64_s(&utc, &stamp) != 0) { + return 0; + } + result.year = (uint16_t)(utc.tm_year + 1900); + result.month = (uint8_t)(utc.tm_mon + 1); + result.day = (uint8_t)utc.tm_mday; + result.hour = (uint8_t)utc.tm_hour; + result.minute = (uint8_t)utc.tm_min; + result.second = (uint8_t)utc.tm_sec; + result.weekday = (uint8_t)utc.tm_wday; +#else + /* 先分成完整天数和日内秒数,日期换算与时分秒拆分分别进行。 */ + days = seconds / 86400U; + rem = seconds % 86400U; + /* + * 与 Days_From_Civil 反向对应:先恢复以三月为年首的天数,再求 400 年周期及周期内年份。 + * yoe 中的修正项处理闰日、世纪和周期末日,避免将周期最后一天归入下一周期。 + * mp 先得到从三月起算的月份编号,再还原为 1..12;一、二月归回下一公历年。 + */ + z = days + 719468U; + era = z / 146097U; + doe = z - era * 146097U; + yoe = (doe - doe / 1460U + doe / 36524U - doe / 146096U) / 365U; + doy = doe - (365U * yoe + yoe / 4U - yoe / 100U); + mp = (5U * doy + 2U) / 153U; + month = (mp < 10U) ? (mp + 3U) : (mp - 9U); + year = yoe + era * 400U + ((month <= 2U) ? 1U : 0U); + + result.year = (uint16_t)year; + result.month = (uint8_t)month; + result.day = (uint8_t)(doy - (153U * mp + 2U) / 5U + 1U); + result.hour = (uint8_t)(rem / 3600U); + result.minute = (uint8_t)((rem % 3600U) / 60U); + result.second = (uint8_t)(rem % 60U); + result.weekday = Weekday_From_Days(days); +#endif + result.ms = ms; + *time = result; + return 1; +} + +/* + * 函数功能:将 UTC 纪元秒数和毫秒直接展开为固定 UTC+8 的本地标准时间。 + * 入口参数:utc_seconds——未加时区偏移的 UTC 纪元秒数;ms——0..999。 + * 出口参数:local_time——本地年月日、时分秒、毫秒及重新计算的星期。 + * 返回值:1 成功;0 参数非法、本地日期超出 1970..2099 或平台转换失败,失败时输出不变。 + * 说明:先检查再平移秒数,只展开一次日历;调用方无需手动调整小时、日期或星期。 + */ +int IEC60870_Time_Seconds_To_Local(uint32_t utc_seconds, uint16_t ms, + IEC60870_WallTime_t *local_time) +{ + const uint32_t local_offset_seconds = (uint32_t)IEC60870_LOCAL_UTC_OFFSET_MINUTES * 60U; + + if (local_time == NULL || ms > 999U || + utc_seconds > IEC60870_TIME_MAX_SECONDS - local_offset_seconds) { + return 0; + } + /* 平移值只用于展开本地日历,不作为新的 UTC 时间戳保存或返回。 */ + return IEC60870_Time_From_Seconds(utc_seconds + local_offset_seconds, ms, local_time); +} + +/* + * 函数功能:将源日历、SU 和源标准时区一起换算为有效的 UTC 纪元秒数。 + * 入口参数:value——合法的 2000..2099 年 CP56 解析值,IV=0,SU/has_weekday 为 0/1; + * standard_utc_offset_minutes——源标准当地时间减 UTC 的分钟数,范围 -1439..1439, + * 不包含夏令时;源标准时区 UTC+8 填 480,即使 SU=1 也不改变该偏移。 + * 出口参数:seconds——接收 UTC 完整秒数,毫秒仍由输入日历保存。 + * 返回值:1 成功;0 参数非法、IV=1 或结果越界,失败时输出不变。 + * 说明:原始解码尚未修正 SU;在本函数中扣除一小时后,不应再由业务重复扣减。 + */ +int IEC60870_Time_CP56_To_Seconds(const IEC60870_CP56Time2a_t *value, + int16_t standard_utc_offset_minutes, uint32_t *seconds) +{ + int64_t utc_seconds; + + /* + * 标准时区偏移不包含夏令时,绝对值须小于一天;这是本接口的参数范围。 + * IV=1 的时间可被原始编解码保留,但不能在这里作为有效时间转换;SU=1 则正常参与换算。 + */ + if (value == NULL || seconds == NULL || !WallTime_Is_Valid(&value->time) || + value->time.year < IEC60870_CP56_MIN_YEAR || value->time.year > IEC60870_CP56_MAX_YEAR || + value->is_invalid != 0U || value->is_summer_time > 1U || value->has_weekday > 1U || + standard_utc_offset_minutes < -1439 || standard_utc_offset_minutes > 1439) { + return 0; + } + /* + * 标准时间 = 报文日历 - SU * 3600;UTC = 标准时间 - 标准时区偏移。 + * 在秒数上调整可跨日/月/年;有符号 64 位中间值避免 2038 年问题及加减回绕。 + * 原日历和 SU 保持配套,不能先改小时再保留 SU,否则再次换算会重复扣减。 + */ + utc_seconds = (int64_t)Seconds_From_Civil(&value->time) - + (int64_t)value->is_summer_time * INT64_C(3600) - + (int64_t)standard_utc_offset_minutes * INT64_C(60); + /* 在有符号域中检查最终值,再转 uint32_t,避免负数或超范围结果被当成有效纪元秒数。 */ + if (utc_seconds < 0 || utc_seconds > (int64_t)IEC60870_TIME_MAX_SECONDS) { + return 0; + } + /* 读取输入和检查结果范围后才提交输出;毫秒余数仍取 value->time.ms。 */ + *seconds = (uint32_t)utc_seconds; + return 1; +} + +/* + * 函数功能:将 CP56 解析值转换为固定 UTC+8 的本地标准时间日历。 + * 入口参数:value——合法 CP56 解析值,须保留配套的源日历及 IV/SU; + * source_standard_utc_offset_minutes——源标准时区偏移,单位为分钟,范围 -1439..1439, + * 不包含夏令时;源标准时区与本地相同时传 IEC60870_LOCAL_UTC_OFFSET_MINUTES。 + * 出口参数:local_time——接收本地标准时间年月日、时分秒、毫秒和重新计算的星期。 + * 返回值:1 成功;0 参数非法、IV=1、转换结果越界或平台转换失败,失败时输出不变。 + * 说明:通常在 From_CP56 成功后调用;输出已处理 SU 和时区,业务不再加八小时或减一小时。 + */ +int IEC60870_Time_CP56_To_Local(const IEC60870_CP56Time2a_t *value, + int16_t source_standard_utc_offset_minutes, + IEC60870_WallTime_t *local_time) +{ + uint32_t utc_seconds; + + /* 下层已统一检查 IV、SU、源偏移和日期,并完成夏令时修正;本层不再重复扣除一小时。 */ + if (local_time == NULL || + !IEC60870_Time_CP56_To_Seconds(value, source_standard_utc_offset_minutes, &utc_seconds)) { + return 0; + } + /* 与直接从时间戳取本地时间共用入口,统一处理上限、跨日/月/年和星期。 */ + return IEC60870_Time_Seconds_To_Local(utc_seconds, value->time.ms, local_time); +} + +/* + * 函数功能:将日历及 IV/SU/星期标志编码为七字节 CP56Time2a。 + * 入口参数:value——合法的 2000..2099 年日历,三个标志均为 0/1; + * capacity——out 的实际可写字节数,至少 IEC60870_CP56_LEN。 + * 出口参数:out——只写前七字节,超出部分保持原样。 + * 返回值:1 成功;0 参数非法,失败时整个输出缓冲区不变。 + * 说明:公共编码只校验并写入日历和标志;业务发送统一设置 SU=0 的约束由后续业务入口落实。 + */ +int IEC60870_Time_To_CP56(const IEC60870_CP56Time2a_t *value, uint8_t *out, size_t capacity) +{ + uint8_t encoded[IEC60870_CP56_LEN]; + uint8_t weekday = 0U; + uint16_t msec; + + if (value == NULL || out == NULL || capacity < IEC60870_CP56_LEN || + !WallTime_Is_Valid(&value->time) || + value->time.year < IEC60870_CP56_MIN_YEAR || value->time.year > IEC60870_CP56_MAX_YEAR || + value->is_invalid > 1U || value->is_summer_time > 1U || value->has_weekday > 1U) { + return 0; + } + /* 报文星期 0 表示未使用;携带星期时由日期重算,将内存中的周日 0 转成报文值 7。 */ + if (value->has_weekday != 0U) { + weekday = Weekday_From_Days(Days_From_Civil(value->time.year, value->time.month, value->time.day)); + weekday = (uint8_t)((weekday == 0U) ? 7U : weekday); + } + /* CP56 前两字节保存分钟内毫秒,范围 0..59999,由秒和毫秒余数组合而成。 */ + msec = (uint16_t)((uint32_t)value->time.second * 1000U + value->time.ms); + /* 逐字节布局,不把结构体转成报文,也不要求输出缓冲区对齐。 */ + encoded[0] = (uint8_t)(msec & 0xFFU); + encoded[1] = (uint8_t)(msec >> 8); + encoded[2] = (uint8_t)(value->time.minute | (uint8_t)(value->is_invalid << 7)); + /* + * SU 按输入写入 bit7,不改变小时值;小时字段及 SU 已校验,保留位保持为 0。 + * 这里不能直接清除 SU=1:若小时仍是夏令时值,单独清标志会改变报文所表达的时刻。 + */ + encoded[3] = (uint8_t)(value->time.hour | (uint8_t)(value->is_summer_time << 7)); + /* 日号占低五位、星期占高三位;月份和两位年份单独存储,未使用的高位保持为 0。 */ + encoded[4] = (uint8_t)(value->time.day | (uint8_t)(weekday << 5)); + encoded[5] = value->time.month; + encoded[6] = (uint8_t)(value->time.year - IEC60870_CP56_MIN_YEAR); + /* 校验和读取输入完成后才统一提交输出;也允许 out 与 value 的存储重叠。 */ + memcpy(out, encoded, IEC60870_CP56_LEN); + return 1; +} + +/* + * 函数功能:校验并解码前七字节 CP56Time2a,保留源日历及 IV/SU。 + * 入口参数:in——报文起点;length——实际可读字节数,至少 IEC60870_CP56_LEN。 + * 出口参数:value——接收日历、IV、SU 和是否携带星期;weekday 始终按日期重新计算。 + * 返回值:1 成功;0 指针、长度、保留位、日期时间或非零星期不合法,失败时输出不变。 + * 说明:后续字节不处理;SU=1 或 IV=1 本身不导致解码失败,仍要求日期及其余字段合法。 + * 成功只表示满足格式约束,尚未进行时间转换,也不表示该时间可以用于校时。 + */ +int IEC60870_Time_From_CP56(const uint8_t *in, size_t length, IEC60870_CP56Time2a_t *value) +{ + IEC60870_CP56Time2a_t result = {0}; + uint16_t msec; + uint8_t received_weekday, expected_weekday; + + if (in == NULL || value == NULL || length < IEC60870_CP56_LEN) { + return 0; + } + /* + * 本模块要求分钟 bit6、小时 bit5..6、月份 bit4..7、年份 bit7 为 0。 + * 非零保留位直接拒绝,不能用掩码静默丢弃;IV 和 SU 为有效标志,另外提取。 + */ + if ((in[2] & 0x40U) != 0U || (in[3] & 0x60U) != 0U || + (in[5] & 0xF0U) != 0U || (in[6] & 0x80U) != 0U) { + return 0; + } + /* 按低字节在前还原分钟内毫秒;超过 59999 不能表示合法的秒与毫秒余数。 */ + msec = (uint16_t)((uint16_t)in[0] | ((uint16_t)in[1] << 8)); + if (msec > 59999U) { + return 0; + } + result.time.year = (uint16_t)(IEC60870_CP56_MIN_YEAR + in[6]); + result.time.month = (uint8_t)(in[5] & 0x0FU); + result.time.day = (uint8_t)(in[4] & 0x1FU); + result.time.hour = (uint8_t)(in[3] & 0x1FU); + result.time.minute = (uint8_t)(in[2] & 0x3FU); + result.time.second = (uint8_t)(msec / 1000U); + result.time.ms = (uint16_t)(msec % 1000U); + result.is_invalid = (uint8_t)((in[2] >> 7) & 1U); + /* 与原始小时配套保留 SU,交给后续转换接口处理,避免丢失信息或重复扣除一小时。 */ + result.is_summer_time = (uint8_t)((in[3] >> 7) & 1U); + /* 报文星期 0 表示未提供星期;内存中的 weekday=0 表示周日,需由 has_weekday 区分。 */ + received_weekday = (uint8_t)(in[4] >> 5); + result.has_weekday = (uint8_t)(received_weekday != 0U); + /* 年份 100..127 会展开为 2100..2127,在此随其他无效日期一起拒绝,不自动映射世纪。 */ + if (!WallTime_Is_Valid(&result.time)) { + return 0; + } + /* 无论报文是否携带星期,都从日期计算输出;非零报文星期还须与计算结果一致。 */ + result.time.weekday = Weekday_From_Days(Days_From_Civil(result.time.year, result.time.month, + result.time.day)); + expected_weekday = (uint8_t)((result.time.weekday == 0U) ? 7U : result.time.weekday); + if (received_weekday != 0U && received_weekday != expected_weekday) { + return 0; + } + /* + * IV=1 仍可供业务记录;取有效的本地标准时间时,应将完整结果交给 CP56_To_Local 检查和转换。 + * 是否允许校时、远端时间是否合理仍由业务判断,此处不设置 RTC。 + */ + *value = result; + return 1; +} diff --git a/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Time.h b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Time.h new file mode 100644 index 00000000..de9c86ad --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Time.h @@ -0,0 +1,244 @@ +/* + * 模块名称:IEC 60870-5-101/104 公共时间工具 + * 模块功能:CP16/CP24/CP56编解码、公历日期/星期、本地标准时间及纪元秒数工具。 + * 适用协议:101 和 104 的时间格式共用同一实现,独立于各自的帧格式和链路层。 + * 职责边界:公共编解码保留 IV/SU,允许合法的 SU=0/1,不调整输入日历。 + * 业务接收时通过 CP56_To_Local 检查 IV、处理 SU 和源时区,取得本地标准时间日历。 + * 后续业务发送入口须保证日历已是本地标准时间,并统一设置 SU=0。 + * 接入状态:IEC60870_Asdu 已复用 CP56 编解码;事件采集、时间源读取和实际校时仍由业务接入。 + * 适用平台:可选平台库或纯整数日期转换;不读取系统时钟、不设置 RTC、不推断时区规则或处理链路定时器。 + */ +#ifndef IEC60870_TIME_H +#define IEC60870_TIME_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* CP16为经过时间;CP24仅含分钟及分钟内毫秒,不能单独恢复完整日期。 */ +#define IEC60870_CP16_LEN 2U +#define IEC60870_CP24_LEN 3U +#define IEC60870_CP56_LEN 7U +#define IEC60870_TIME_MIN_YEAR 1970U +#define IEC60870_TIME_MAX_YEAR 2099U +/* 两位年份按 2000 年起解释是本实现的世纪约定,须与通信对端一致。 */ +#define IEC60870_CP56_MIN_YEAR 2000U +#define IEC60870_CP56_MAX_YEAR 2099U +/* 2099-12-31 23:59:59 UTC;本模块不接受 uint32_t 可表示但超出约定年份的秒数。 */ +#define IEC60870_TIME_MAX_SECONDS UINT32_C(4102444799) +/* + * 本地标准时区固定为 UTC+8,不读取系统时区。 + * 源标准时区也为 UTC+8 时,转换接口传入此值;即使报文 SU=1,源标准偏移仍为 480。 + * 其他源时区由调用方按双方约定提供标准偏移,不能一律传入此值。 + */ +#define IEC60870_LOCAL_UTC_OFFSET_MINUTES 480 + +/* + * 时间戳转日历的编译选项:1 使用平台库,0 使用本模块保留的纯整数算法。 + * 当前平台库后端为 Windows CRT 的 _gmtime64_s;Windows 默认 1,其他平台默认 0。 + * 无合适时间库时,在编译 IEC60870_Time.c 时定义 IEC60870_TIME_USE_PLATFORM=0。 + * 两种实现均输出 UTC,支持相同的 1970..2099 年范围,不读取当前时间或本地时区。 + */ +#ifndef IEC60870_TIME_USE_PLATFORM +#ifdef _WIN32 +#define IEC60870_TIME_USE_PLATFORM 1 +#else +#define IEC60870_TIME_USE_PLATFORM 0 +#endif +#endif + +typedef struct { + uint16_t milliseconds; /* CP16经过时间,0..65535毫秒,无IV或SU。 */ +} IEC60870_CP16Time2a_t; + +typedef struct { + uint16_t milliseconds; /* CP24分钟内毫秒,0..59999,包含秒。 */ + uint8_t minute; /* 0..59;不含小时、日期或时区。 */ + uint8_t is_invalid; /* IV,0/1;编码和解码保留。 */ +} IEC60870_CP24Time2a_t; + +/** + * 功能:编码CP16经过时间或CP24短时标。 + * 输入参数:value——非空解析值;capacity——输出容量,分别至少2/3字节。 + * 输出参数:out——低字节在前的协议字节,不改变输入时间基准。 + * 返回值:1成功;0参数或字段非法,输出不变。 + * 说明:CP24保留IV但检查分钟和毫秒范围,不推断小时或日期,不携带SU。 + */ +int IEC60870_Time_To_CP16(const IEC60870_CP16Time2a_t *value, uint8_t *out, size_t capacity); +int IEC60870_Time_To_CP24(const IEC60870_CP24Time2a_t *value, uint8_t *out, size_t capacity); + +/** + * 功能:解码CP16经过时间或CP24短时标。 + * 输入参数:in——非空字节串;length——可读长度,分别至少2/3字节。 + * 输出参数:value——成功后保存独立解析值,失败不变。 + * 返回值:1成功;0参数、长度、CP24保留位或字段非法。 + * 说明:只读取前2/3字节;CP24的IV=1仍可解析,但不表示时间有效。 + */ +int IEC60870_Time_From_CP16(const uint8_t *in, size_t length, IEC60870_CP16Time2a_t *value); +int IEC60870_Time_From_CP24(const uint8_t *in, size_t length, IEC60870_CP24Time2a_t *value); + +/* + * 公历时间值,不携带时区或 SU;Seconds_To_Local、CP56_To_Local 输出本地标准时间, + * From_Seconds 输出 UTC。 + * 调用 To_Seconds 时须提供 UTC 日历,不能把本地标准时间日历直接当成 UTC 转秒数。 + * weekday 为派生字段:0=周日,1=周一,...,6=周六。 + * 编码及转秒数时忽略传入的 weekday;解码及从秒数转换时按日期重新计算。 + * 此类型仅供内存中传值,不能直接作为 CP56Time2a 的报文字节布局。 + */ +typedef struct { + uint16_t year; /* 普通日历为 1970..2099;CP56 编解码限于 2000..2099 */ + uint8_t month; /* 1..12 */ + uint8_t day; /* 1..当月实际天数 */ + uint8_t hour; /* 0..23 */ + uint8_t minute; /* 0..59 */ + uint8_t second; /* 0..59,不表示闰秒 */ + uint16_t ms; /* 0..999 */ + uint8_t weekday; /* 0..6,输出时由日期推导 */ +} IEC60870_WallTime_t; + +/* + * CP56Time2a 的解析值;标志与普通日历值分开,避免丢失报文中的时间质量信息。 + * is_invalid、is_summer_time 和 has_weekday 只能为 0 或 1。 + * 编码由 has_weekday 决定是否携带星期,IV 和 SU 按输入保留。 + * IV=1 仍可编解码,但日期字段仍须合法;成功不表示该时间可用于校时。 + * 标准允许选用 SU 但不推荐使用;本项目后续业务发送入口负责统一设置 SU=0。 + * 公共编解码支持 SU=0/1,不强制清零;发送前须确保日历与标志一致,不能只清 SU 而保留夏令时日历。 + * SU 不携带时区信息;取本地标准时间时将完整解析值及源标准时区偏移交给 CP56_To_Local。 + */ +typedef struct { + IEC60870_WallTime_t time; /* 2000..2099;按双方约定的时间基准及 SU 解释 */ + uint8_t is_invalid; /* IV:1=时间无效,0=未置无效标志 */ + uint8_t is_summer_time; /* SU:0=标准时间,1=夏令时;原始编解码保留,CP56 转换接口在为 1 时扣除一小时 */ + uint8_t has_weekday; /* 1=报文带星期,0=报文星期域为 0(未使用) */ +} IEC60870_CP56Time2a_t; + +/* + * 所有接口:成功返回 1;参数/格式非法、结果越界、平台转换失败或将 IV=1 转为有效时间时返回 0。 + * 返回 0 时不修改任何输出;调用方仍须保证指针有效且实际空间不少于所报长度。 + * 模块不维护共享可变状态或动态内存。输入和输出允许重叠。 + * 成功写回输出时,会覆盖与输出重叠的输入存储;需要保留原始输入时应使用独立输出存储。 + */ + +/** + * 功能:检查日历的年月日、时分秒及毫秒是否合法,不进行时间转换。 + * 输入:time——待检查的日历值;年份1970..2099,毫秒0..999。 + * 返回:1 数值合法;0 空指针、无效日期或字段越界。 + * 说明:忽略派生字段weekday;不判断时区、时钟是否准确或是否允许校时。 + */ +int IEC60870_Time_Is_Valid(const IEC60870_WallTime_t *time); + +/** + * 功能:检查公历日期并计算星期。 + * 输入:year——1970..2099;month、day——合法月份和日期。 + * 输出:weekday——0=周日,1=周一,...,6=周六。 + * 返回:1 成功;0 日期非法或输出为空,输出不变。 + */ +int IEC60870_Time_Get_Weekday(uint16_t year, uint8_t month, uint8_t day, uint8_t *weekday); + +/** + * 功能:将 UTC 日期和时间转换为自 1970-01-01 00:00:00 UTC 起的完整秒数。 + * 输入:time——年份 1970..2099,毫秒 0..999,其余字段须组成合法时间。 + * 输出:seconds——0..IEC60870_TIME_MAX_SECONDS;0 是有效纪元时间,不表示失败。 + * 返回:1 成功;0 参数非法,输出不变。 + * 说明:ms 参与范围校验,但不计入完整秒数;需保留亚秒精度时另行保存 time->ms。 + * 忽略 weekday,不进行本地时区换算,也不计闰秒。 + * time 没有时区或 SU,函数无法检查其是否真的采用 UTC,须由调用方保证。 + * 对 CP56 解析值,取本地标准时间调用 CP56_To_Local;取纪元秒数调用 CP56_To_Seconds。 + */ +int IEC60870_Time_To_Seconds(const IEC60870_WallTime_t *time, uint32_t *seconds); + +/** + * 功能:将纪元秒数和毫秒余数转换为 UTC 日期和时间。 + * 输入:seconds——0..IEC60870_TIME_MAX_SECONDS;ms——0..999。 + * 输出:time——完整公历时间及按日期计算的 weekday。 + * 返回:1 成功;0 参数非法或平台转换失败,输出不变;不会把越界毫秒截断成 999。 + * 说明:由 IEC60870_TIME_USE_PLATFORM 选择平台库或纯整数算法;均不做本地时区换算。 + */ +int IEC60870_Time_From_Seconds(uint32_t seconds, uint16_t ms, IEC60870_WallTime_t *time); + +/** + * 功能:将 UTC 纪元秒数和毫秒余数直接转换为本地标准时间日历(固定 UTC+8)。 + * 输入:utc_seconds——未加时区偏移的 UTC 纪元秒数;ms——0..999。 + * 本地日期须在 1970..2099 年内,因此 utc_seconds 上限为 + * IEC60870_TIME_MAX_SECONDS - IEC60870_LOCAL_UTC_OFFSET_MINUTES * 60。 + * 输出:local_time——本地年月日、时分秒、毫秒及重新计算的星期。 + * 返回:1 成功;0 参数非法、本地日期越界或平台转换失败,输出不变。 + * 说明:内部统一处理时区偏移及跨日/月/年,调用方不再加八小时。 + * 不读取系统时区或开启夏令时;已是固定 UTC+8 标准时间的 RTC 日历可直接用于 CP56 编码。 + */ +int IEC60870_Time_Seconds_To_Local(uint32_t utc_seconds, uint16_t ms, + IEC60870_WallTime_t *local_time); + +/** + * 功能:将 CP56Time2a 解析值转换为 UTC 纪元完整秒数,SU=1 时扣除一小时。 + * 输入:value——合法的 2000..2099 年日期时间,IV=0,SU/has_weekday 为 0/1; + * standard_utc_offset_minutes——源报文标准当地时间减 UTC 的分钟数,范围 -1439..1439, + * 向东为正、向西为负;UTC 填 0,UTC+8 填 480,UTC-5 填 -300。 + * 此偏移必须不含夏令时的一小时;SU 会单独扣除,不能重复计算。 + * 输出:seconds——0..IEC60870_TIME_MAX_SECONDS 的 UTC 秒数。 + * 返回:1 成功;0 参数非法、IV=1 或换算结果越界,输出不变。 + * 说明:UTC = 报文日历对应秒数 - SU * 3600 - 标准时区偏移 * 60。 + * 在秒数上运算,支持跨日/月/年;年份 2000 的输入可换算为 1999 年的 UTC。 + * ms 校验后不计入完整秒数;未与输出重叠时,输入日期、SU 和毫秒均保持不变。 + * 忽略传入的 weekday;不根据日期猜测夏令时,不访问系统时区或设置 RTC。 + */ +int IEC60870_Time_CP56_To_Seconds(const IEC60870_CP56Time2a_t *value, + int16_t standard_utc_offset_minutes, uint32_t *seconds); + +/** + * 功能:将 CP56Time2a 解析值转换为本地标准时间日历(固定 UTC+8),并处理 SU。 + * 输入:value——合法的 2000..2099 年日期时间,IV=0,SU/has_weekday 为 0/1; + * source_standard_utc_offset_minutes——源报文标准当地时间减 UTC 的分钟数, + * 范围 -1439..1439,不含夏令时;双方约定本地标准时间时传 + * IEC60870_LOCAL_UTC_OFFSET_MINUTES(480),源报文为 UTC 时传 0。 + * 输出:local_time——1970..2099 年的本地标准时间年月日、时分秒、毫秒及重新计算的星期。 + * 返回:1 成功;0 参数非法、IV=1、本地标准时间越界或平台转换失败,输出不变。 + * 说明:本地标准时间 = 报文日历 - SU * 一小时 - 源标准时区偏移 + 八小时。 + * 源标准时区为 UTC+8 时,SU=0 保持原时间,SU=1 减一小时,自动处理跨日/月/年。 + * 毫秒原样保留;日历结果通过 local_time 输出,函数返回值仅表示成功或失败。 + * 输入与输出未重叠时,不改写输入日历或 SU。 + * 结果已是本地标准时间,调用方无需再次加八小时或减夏令时的一小时。 + * 业务再次发送此日历时应构造新的 CP56 值并置 SU=0,不能沿用原报文的 SU=1。 + */ +int IEC60870_Time_CP56_To_Local(const IEC60870_CP56Time2a_t *value, + int16_t source_standard_utc_offset_minutes, + IEC60870_WallTime_t *local_time); + +/** + * 功能:将 CP56Time2a 解析值编码为 7 字节报文,毫秒域按低字节在前写入。 + * 输入:value——合法的 2000..2099 年日期时间,三个标志字段均为 0/1; + * capacity——out 可写字节数,至少 IEC60870_CP56_LEN。 + * 输出:out——仅写前 7 字节,保留位清零,IV 和 SU 按输入保留。 + * 返回:1 成功;0 参数非法,整个输出缓冲区不变。 + * 说明:has_weekday=1 时由日期计算报文星期(1=周一..7=周日),否则填 0。 + * 不校时、不转换时区,也不因 SU=1 而调整小时值。 + * SU=0/1 均可编码,本接口不会自动清零或因 SU=1 而拒绝合法输入。 + * 后续业务发送入口负责传入已是本地标准时间的日历并设置 SU=0;此处只执行公共编码。 + * IV=1 不放宽日期、时间及标志取值的检查。 + */ +int IEC60870_Time_To_CP56(const IEC60870_CP56Time2a_t *value, uint8_t *out, size_t capacity); + +/** + * 功能:检查并解析前 7 字节 CP56Time2a,后续字节留给调用方处理。 + * 输入:in——报文起点;length——可读字节数,至少 IEC60870_CP56_LEN。 + * 输出:value——源报文的日历时间、IV、SU 及报文是否携带星期,尚未转换为本地标准时间; + * weekday 始终由日期计算。 + * 返回:1 成功;0 参数、保留位、日期时间非法,输出不变。 + * 说明:年份域只接受 0..99,解释为 2000..2099;星期域 0 表示未使用, + * 非零星期必须与日期一致。分钟 bit6 等保留位必须为 0,不接收扩展含义。 + * SU=1 本身不导致失败,IV/SU 均原样返回;不转换时区或夏令时。 + * IV=1 且其余字段合法时仍可成功解析,供业务记录;两个 CP56 转换接口会拒绝该无效时间。 + * 解码后取本地标准时间调用 CP56_To_Local;需要纪元秒数时调用 CP56_To_Seconds。 + * 转换前须保留完整的日历及 SU,不可先清 SU,也不可先减一小时再携带 SU=1 转换。 + * 业务仍须检查时间约定及校时权限;不自动设置 RTC。 + */ +int IEC60870_Time_From_CP56(const uint8_t *in, size_t length, IEC60870_CP56Time2a_t *value); + +#ifdef __cplusplus +} +#endif + +#endif /* IEC60870_TIME_H */ diff --git a/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Types.h b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Types.h new file mode 100644 index 00000000..50c30d22 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/common/IEC60870_Types.h @@ -0,0 +1,29 @@ +/* + * 101/104共用的应用层值类型:ASDU字段宽度和总长上限。 + * 只依赖标准整数类型,不包含传输帧、连接、队列或平台接口。 + * 调用方按接入协议显式选择参数;104的默认配置由104接入层提供。 + */ +#ifndef IEC60870_TYPES_H +#define IEC60870_TYPES_H + +#include + +#define IEC60870_MAX_ASDU_LEN 254 /* 通用101/104 ASDU存储上限;104传输仍限249。 */ + +/* 应用层参数描述ASDU字段宽度及长度上限,字段使用int以便校验原始范围。 + * Type ID/VSQ固定为1字节;COT/CA可为1或2字节,IOA可为1、2或3字节。 + * 通用ASDU上限为254;接入本工程104 TCP时使用2/2/3及不超过249的上限。 + * 参数由调用方持有,构造器及解析视图借用它;使用期间必须有效且不可修改。 + * originatorAddress供业务作为发送OA的默认值,报文构造仍以header中显式OA为准; + * COT为1字节时不编码OA,解析得到的OA为0。配置需要双方一致,不自动协商。 */ +typedef struct { + int sizeOfTypeId; /* 固定1,不支持修改。 */ + int sizeOfVSQ; /* 固定1,不支持修改。 */ + int sizeOfCOT; /* 1或2;2字节时包含OA。 */ + int originatorAddress; /* 业务默认源发地址,0..255。 */ + int sizeOfCA; /* 公共地址长度,1或2。 */ + int sizeOfIOA; /* 信息对象地址长度,1、2或3。 */ + int maxSizeOfASDU; /* 包含公共头;至少容纳头,最多254。 */ +} IEC60870_AppLayerParameters_t; + +#endif /* IEC60870_TYPES_H */ diff --git a/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_API接口汇总.md b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_API接口汇总.md new file mode 100644 index 00000000..b5c56930 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_API接口汇总.md @@ -0,0 +1,2485 @@ +# IEC104 函数 API 接口汇总(按实现文件) + +依据当前工作区的协议库源码及对应头文件整理,按实现所在的 `.c` 文件分节。范围不包含示范程序、测试程序及其业务辅助函数;2026-10-29补充独立Type 103时钟同步模块。 + +共覆盖 **24 个实现文件**,其中内部跨文件协作函数为Link 5个、信息对象层2个、召唤引擎2个。表中标有“内部协作”的函数不属于业务 API;文件内的 `static` 辅助函数、宏和类型定义不作为函数接口列出,各模块入口以对应小节和声明头文件为准。 + +模块接口并不都供业务直接调用:子站业务入口使用 `IEC104_Slave.h`,TcpServer 由 Slave 持有,Connection、SendManager 及其底层接口供相应实现层协作。运行顺序见 [完整调用流程](IEC104_完整调用流程.md)。各文件小节先说明使用约定,再列功能表和可展开的完整函数原型;原型中的参数名、类型和返回类型与声明头文件一致。 + + + +## 业务接口速查 + +| 要完成的操作 | 应使用的接口 | 前提与结果 | +| --- | --- | --- | +| 创建和启动子站 | [Slave_Create / Config_Default / Init](#file-19) | Create 返回不透明指针;Init 成功后可以监听和缓存 NORMAL | +| 共用站点与业务路由 | [Slave_Add_Station / Get_Station](#station-registry) | 调用方提供节点,按CA取得业务context;异步持有用Acquire/Release,固定集合用Lock/Unlock | +| 独立循环驱动 | [Slave_Poll / NextEvent](#file-19) | Poll 推进网络及协议,NextEvent 按 FIFO 交付通知,须逐条读到 EMPTY | +| 接入已有事件循环 | [Slave_Prepare / Process](#file-19) | Prepare 导出关注条件;按原 token 汇集就绪后 Process,再读取事件 | +| 接入站总召/分组总召 | [Interrogation](#file-21) | Handle_Event接收激活/撤销及状态通知,Process推进并检查期限;NextEvent取业务事件,以Reply_Begin/Submit_Batch/Complete_Data显式应答 | +| 接入计数量召唤 | [CounterInterrogation](#file-23) | 独立创建,复用驱动;读取用Reply_Begin和数据接口,冻结/复位用Execute_Begin | +| 接入时钟同步 | [ClockSynchronization](#file-24) | Handle_Event校验保存,Process推进应答及期限,只有显式Execute_Next调用一次应用动作;确认回显原时标 | +| 解析主站请求 | [Asdu_Get_Data / Parse / Get_Element](#file-16) | 先检查 I_FRAME 及 `asdu_result`,再做精确解析与业务条件检查 | +| 回复任意 ASDU | [MasterConnection_Send_ASDU](#file-18) | 使用 `&event.connection`;ASDU 可以是单独构造的对象 | +| 回复激活确认、终止 | [MasterConnection_Send_ACT_CON / Send_ACT_TERM](#file-18) | 修改传入 ASDU 的 COT/P/N 后提交,执行时机由业务决定 | +| 保存请求稍后处理 | [Slave_Copy_Event](#file-19) | 目标存储地址保持稳定,保留原服务实例和连接代次 | +| 校验事件来源及请求会话 | [Slave_Get_Event_Identity / Check_Request](#file-19) | 来源有效与当前可处理分别判断;先消费未读通知,再检查原身份和激活状态 | +| 区分提交失败原因 | [Slave_Get_Last_Submit_Result](#file-19) | 共用发送返回 false 后查询;仅 WAIT 表示尚未接手、可等待后重试 | +| 构造主动上报 | [具体信息对象 Create](#file-17) → [Asdu_Create / Add_InformationObject](#file-16) → [Slave_Enqueue_Event](#file-19) | 使用 Slave 提供的应用层参数;提交到 NORMAL,OK 后原文已复制 | +| 查看当前主站和容量 | [Slave_Get_Remote_Address / Get_Status](#file-19) | 取得只读副本;状态查询不预留容量,不保证下一次提交成功 | +| 断开原主站,继续监听 | [Slave_Close_Connection](#file-19) | 传原连接事件,不手工替换 generation | +| 停止并释放服务 | [Slave_Close → NextEvent 读空 → Destroy](#file-19) | Close 保留通知但清除 ASDU 缓存;Destroy 不隐式关闭或丢弃通知 | +| 使用时标、读取实际时间 | [Time](#file-15)、[Clock](#file-14) | Time 负责转换;Clock 负责实际读写。命令校验与执行由业务负责 | + +“后续只调 Slave”指服务的创建、驱动、事件、状态和关闭统一从 `IEC104_Slave_*` 进入。ASDU 与信息对象仍使用共用 `IEC60870_*`;请求应答通过 Slave 事件提供的 `IEC60870_MasterConnection_t` 分派,无需业务再操作 TcpServer。 + + + +## 类型与模块边界 + +| 类型或头文件 | 职责与所有权 | +| --- | --- | +| `IEC104_Slave_t` / `IEC104_Slave.h` | 不透明服务句柄,Create/Destroy 管理静态槽;内部持有私有 TcpServer 指针 | +| `IEC104_Station_t` | 调用方拥有的固定地址节点,Add/Remove管理注册关系;CA、context及引用由Slave统一管理,字段仅库可改写 | +| `IEC104_Interrogation_t` / `IEC104_Interrogation.h` | 不透明总召业务句柄,借用Slave;管理请求副本、分批、撤销、期限和WAIT进度,不拥有网络服务 | +| `IEC104_SlaveEventIdentity_t` | Slave校验后交付的来源身份,包含服务句柄、实例编号和连接代次;可按值保存,不借用事件原文;字段只读 | +| `IEC104_TcpServer_t` / `IEC104_TcpServer.h` | 不透明底层 TCP 服务,持有连接、队列、帧缓冲、记录和事件 FIFO;可独立接入,但应用须自行管理原请求身份 | +| `IEC60870_MasterConnection_t` / `IEC60870_Slave.h` | 共用发送函数表及上下文;104 实现由 Slave 安装,不创建第二个服务、socket 或线程 | +| `IEC104_SlaveConfig_t` | `IEC104_TcpServerConfig_t` 的值类型别名;Init 复制配置,与服务对象布局无关 | +| `IEC104_TcpTypes.h` | 配置、结果、诊断、状态、原始事件和事件循环值类型;不声明 TcpServer 对象操作 | +| `IEC104_Types.h` | 104 的 APDU 尺寸、标准应用层参数宏、优先级和协议配置 | +| `IEC60870_Types.h` | 共用应用层布局和解析值类型,不选择默认传输 | +| `IEC104_TcpServer_Config.h` | TCP 实现的静态容量配置;编译选项须作用于相应库源文件 | +| Connection / SendManager / Receiver / Link / SendBuffer / AsduQueue | 库内部协作模块;已由 Slave/TcpServer 驱动时,业务不再手工调用它们推进同一会话 | +| `*_Internal.h` | 仅供库实现跨文件协作;不作为业务接入头文件 | + +不透明类型可以声明和传递指针,不能在调用方按值定义、访问成员或计算对象大小。Slave 持有 TcpServer 指针并调用其公开函数,因此无需了解它的内部布局;这两个句柄也不能相互强转。全部服务操作由所属事件循环串行执行,Create/Destroy 还需跨服务串行,BUSY 检查不是多线程互斥。 + + + +## 返回值、错误与发送阶段 + +| 返回类型 | 值与含义 | 使用规则 | +| --- | --- | --- | +| `IEC104_TcpResult_t` | OK=0,其他为负值 | Init/Poll/Prepare/Process/Close 等显式比较 `IEC104_TCP_OK` | +| `IEC104_TcpEventResult_t` | READY=1、EMPTY=0、ERROR=-1 | 仅 READY 提供新事件;EMPTY/ERROR 保持输出原值 | +| `IEC104_TcpSubmitResult_t` | OK=1、WAIT=2、ERROR=-1、FATAL=-2 | OK 已接手;WAIT 未接手;ERROR 为身份/参数/阶段错误;FATAL 需按事实收尾 | +| `IEC104_SlaveRequestState_t` | READY=2、WAIT_EVENTS=1、STALE=0、ERROR=-1 | READY仅说明会话可处理;WAIT_EVENTS先读空通知;STALE终结旧任务;ERROR处理参数或查询失败 | +| 共用发送的 `bool` | true 为已接手,false 为未成功 | 104 通过 `Slave_Get_Last_Submit_Result` 进一步分类,不能把 false 一律当作 WAIT | +| `IEC60870_AsduResult_t` | OK=1、FULL=0、ERROR=-1、UNSUPPORTED=-2 | 单次 ASDU/对象操作只有 OK 修改输出;FULL 时处理容量或数量限制 | +| 时间与时钟的 `int` | 1 成功,0 失败 | 编解码成功与时间品质有效分开判断 | + +不同模块的枚举取值不同,不能统一使用 `if (result)`、`result > 0` 或混用枚举。下文各底层模块还区分 READY、QUEUED、EMPTY、FRAME_WRITTEN 等阶段。 + +| 服务错误枚举(省略 `IEC104_TCP_` 前缀) | 含义与处理 | +| --- | --- | +| INVALID_ARGUMENT / INVALID_HANDLE / INVALID_CONFIG | 检查必填参数、服务句柄和配置;不隐式销毁句柄 | +| INVALID_STATE / BUSY | 尚未初始化、运行中不允许的生命周期操作,或发生重入;按生命周期串行调用 | +| EVENTS_PENDING | Init/Destroy 前先 NextEvent 读空旧通知 | +| NETWORK_ERROR | 保存本次 `error.stage` 与 `socket_error`,定位网络初始化、监听、等待或接入失败 | +| INTERNAL_ERROR | 库内部队列、会话初始化或状态异常;保留诊断并检查状态、关闭和回收 | +| GENERATION_EXHAUSTED / REGISTRATION_EXHAUSTED | 身份计数器用尽,关闭并归还句柄,不回绕复用 | +| NOT_CONNECTED / STALE_GENERATION | 当前无主站,或指定的原连接已经过期;不能通过改成新 generation 来重试旧请求 | + +带 `IEC104_TcpError_t *error` 的接口允许 NULL;提供存储时完整写入本次 `result/stage/socket_error`,成功为 OK/NONE/0。它表示本次 API 调用结果;连接故障另见事件或状态里的 `fault`。例如客户端断开时 Poll 仍可返回 OK,再通过 DISCONNECTED 交付原因。诊断存储不得与其他输入/输出重叠。 + +发送分三个阶段:**提交接手 → 本地整帧写出 → 对端链路 ACK**。OK 只承诺第一阶段;正常 RECORD 的两项完成标志为真,收尾 RECORD 则按实际事实填写。ACK 也不等于对端业务执行成功。`Get_Last_Submit_Result` 是最近一次提交的快照,不随后续 Poll 或 ACK 更新;FATAL 可能发生在部分乃至全部字节已经写出之后。 + + + +## 缓冲、事件与参数寿命 + +| 对象 | 借用、复制与失效规则 | +| --- | --- | +| Slave / TcpServer 句柄 | 静态池持有,地址稳定;应用不 malloc/free、不复制,Destroy 成功后丢弃旧指针 | +| 原始 `IEC104_TcpEvent_t`、状态及地址快照 | 独立值副本,可跨 Poll/Close 保存;只读 type 对应的 union 成员,状态字段须先检查有效标志 | +| `IEC104_SlaveEvent_t` | APDU 是副本,但 asdu 与 connection 含自引用;用 Copy_Event 保存,禁止结构赋值/memcpy/搬移,地址必须稳定 | +| `event.connection` | 使用 `&event.connection`;它绑定原 Slave 实例和原 generation。单独复制 connection 无法重建上下文 | +| `IEC60870_Asdu_t` / `IEC60870_AsduInfo_t` | 借用参数和原文字节,使用期间保持有效;解析视图使用期间原文不可改写 | +| 具体信息对象 | Create/Get_Element/Get_From_Buffer 使用调用方存储,保存数值及只读函数表,可复制完整对象,无需 Destroy | +| Add / 发送 / 入队 | Add 成功已编码复制信息对象;发送或入队 OK 已复制 ASDU,可复用本次输入。WAIT 须保存原 connection、ASDU 和业务进度 | +| Slave 返回的应用层参数 | 静态只读常量,已取得的指针不借用服务存储;Get_Data 返回的报文指针则借用 ASDU 缓冲 | + +`Send_ACT_CON/TERM` 是组合操作:先改 COT/P/N,再提交传输。即使提交 WAIT/ERROR/FATAL,已改字段也不会回滚,不能套用“单次 ASDU setter 失败保持原值”的约定。事件被 NextEvent 的下一次 READY 替换后,其旧视图和应答上下文不能继续使用。 + + + +## 104 默认参数与容量 + +`IEC104_APP_LAYER_PARAMETERS_DEFAULT` 位于 [IEC104_Types.h](../../104/IEC104_Types.h),值为 `{1, 1, 2, 0, 2, 3, IEC104_MAX_ASDU_LEN}`,依次为 Type ID、VSQ、COT 宽度、默认 OA、CA 宽度、IOA 宽度及最大 ASDU 长度。标准 104 为 COT/CA/IOA=2/2/3,ASDU 最长 249 字节,APCI 6 字节,APDU 最长 255 字节。共用 ASDU 模块也支持其他布局,104 发送适配器会拒绝不兼容参数。 + +`IEC104_CA_BROADCAST`也位于IEC104_Types.h,固定值为`0xFFFFU`,表示104两字节CA的广播地址。它不是配置项;站点注册及总召的兼容common_address配置使用1..65534的实际站地址。绑定station时总召从节点取得CA。共用Asdu_Set_CA只负责字段编码,广播请求的接收与应答规则由对应业务模块决定。 + +`Slave_Config_Default` 填入监听 `0.0.0.0:2404`、k=12、w=8、t1/t2/t3=15000/10000/20000 毫秒、本地发送与 STOP 总期限各 15000 毫秒、HIGH 配额 0(严格优先)。配置要求 k/w 在 1~32767,期限非零且 t2 + +## 事件载荷与状态读取 + +`Slave_NextEvent` 的原始通知位于 `event.transport`。先按 `transport.type` 选择 union 成员,再读取载荷;外层 `transport.generation` 标识来源连接。 + +| 事件类型(省略 `IEC104_TCP_EVENT_`) | 载荷 | 含义 | +| --- | --- | --- | +| CONNECTED | `data.connection` | TCP 已连接,尚不代表 STARTDT 已启用 | +| ACTIVATED | `data.connection` | STARTDT_ACT 已受理,CON 可能尚未写完 | +| DEACTIVATED | `data.connection` | STOPDT_ACT 已受理,停止排空可能尚未完成,TCP 仍保持 | +| I_FRAME | `data.i_frame`;Slave 另提供 `asdu/asdu_result/connection` | 完整 APDU 副本及接收序号;视图成功后仍需完整 ASDU 和业务校验 | +| RECORD | `data.record` | `value` 保存发送事实,`closing=0` 为正常 ACK 回执,`closing=1` 为收尾剩余记录 | +| DISCONNECTED | `data.connection` | 本 generation 的最后一条通知;快照保存关闭前状态,phase 可能仍为 ACTIVE | + +CONNECTED 先于同代次其他通知,DISCONNECTED 最后;实际启停变化逐条保存,初始 STOPPED、同状态重复 ACT、CON 写完或单纯断线不额外产生启停事件。普通空间不足时回执可能暂留 Manager,晚于随后入队的启停事件交付;FIFO 表示入队顺序,不是所有网络事实的严格时间排序。 + +`Get_Status` 的 `connected` 表示当前 TCP 是否存在;`connection_status_valid` 为 1 才读取 connection,`asdu_queue_status_valid` 为 1 才读取两级队列。`pending_asdu_count` 只数尚未交接组帧的原文,`stored_asdu_count` 数仍保留的原文;二者都不覆盖全部在途帧。`pending_send_records` 数尚未转为 RECORD 通知的记录,`pending_event_count` 数已入 FIFO 的未读通知,`pending_state_event_count` 只数其中未读的 ACTIVATED/DEACTIVATED。后者为0不能证明过去没有发生已消费的STOP/START。事件中的连接状态则用 `data.connection.status_valid` 判断,不能与当前快照混用。 + +## 文件索引 + +- [IEC104.c](#file-1):3 个函数。 +- [IEC104_AsduQueue.c](#file-2):16 个函数。 +- [IEC104_Codec.c](#file-3):5 个函数。 +- [IEC104_Connection.c](#file-4):21 个函数。 +- [IEC104_Link.c](#file-5):28 个函数(含 5 个内部协作函数)。 +- [IEC104_Link_Master_StartStop.c](#file-6):9 个函数。 +- [IEC104_Link_Slave_StartStop.c](#file-7):6 个函数。 +- [IEC104_Log.c](#file-8):3 个函数。 +- [IEC104_Net_Windows.c](#file-9):11 个函数。 +- [IEC104_Receiver.c](#file-10):7 个函数。 +- [IEC104_SendBuffer.c](#file-11):11 个函数。 +- [IEC104_SendManager.c](#file-12):25 个函数。 +- [IEC104_TcpServer.c](#file-13):14 个函数。 +- [IEC60870_Clock.c](#file-14):2 个函数。 +- [IEC60870_Time.c](#file-15):13 个函数。 +- [IEC60870_Asdu.c](#file-16):10 个函数。 +- [IEC60870_InformationObjects.c](#file-17):139 个函数(含 2 个内部协作函数)。 +- [IEC60870_Slave.c](#file-18):4 个函数。 +- [IEC104_Slave.c](#file-19):41 个函数。 +- [IEC60870_Codec.c](#file-20):2 个函数。 +- [IEC104_Interrogation.c](#file-21):11 个模块接口及2个内部协作函数;总召Create已移至种类文件。 +- [IEC104_GeneralInterrogation.c](#file-22):1 个函数(保留旧Create名称)。 +- [IEC104_CounterInterrogation.c](#file-23):2 个函数。 +- [IEC104_ClockSynchronization.c](#file-24):8 个函数。 + + + +## 1. IEC104.c + +实现:[IEC104.c](../../104/IEC104.c);声明:[IEC104.h](../../104/IEC104.h)。 + +旧版单路接收兼容入口,使用模块内的全局接收器和私有链路状态。 + +调用约定:全局接收状态只适合单路串行输入。`Rx_Reset` 与 `Legacy_Link_Reset` 的作用不同;需要同时丢弃半帧并重置序号时,两者均须调用。`Rx_Feed_Byte` 返回 `IEC104_RxResult_t` 中的进度或错误分类,不交付可供业务使用的事件,也不替代 Slave 的事件循环。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Rx_Reset` | 丢弃当前未完成 APDU,复位接收阶段和长度;保留链路序号及接收缓冲中的旧字节。 | +| `IEC104_Legacy_Link_Reset` | 复位兼容入口的私有 I/S 序号状态,并恢复 STARTED 预设;不改变当前 APDU 的接收进度。 | +| `IEC104_Rx_Feed_Byte` | 逐字节组装 APDU,检查 APCI 及适用的 I/S 序号,返回接收进度或错误分类;不输出报文内容,也不处理 U 帧启停。 | + + +
+完整函数原型(3 个;参数和返回类型见声明) + +```c +#include "IEC104.h" + +extern void IEC104_Rx_Reset(void); + +extern void IEC104_Legacy_Link_Reset(void); + +extern IEC104_RxResult_t IEC104_Rx_Feed_Byte(uint8_t byte); +``` + +
+ + + +## 2. IEC104_AsduQueue.c + +实现:[IEC104_AsduQueue.c](../../104/IEC104_AsduQueue.c);声明:[IEC104_AsduQueue.h](../../104/IEC104_AsduQueue.h)。 + +管理 HIGH、NORMAL 两级变长 ASDU 字节环,保存业务原文,不执行构帧或网络发送。 + +调用约定:Init 借用调用方的 HIGH/NORMAL 字节数组,数组必须持续有效;Abort 解除引用但不释放数组。`connection_generation=0` 只用于离线 NORMAL;在线操作须使用当前绑定代次。Peek 取得的最多两段原文是短期借用,队列变更后应重新查询;需要保存时调用 Copy_Asdu。 + +结果为 `OK=1 / FULL=0 / EMPTY=2 / ERROR=-1`。FULL 与 EMPTY 均不是成功输出。Mark_Submitted 必须晚于完整帧和发送记录的接手;确认 NORMAL 使用原条目身份,不能只按“出队一条”处理。NORMAL 原文可覆盖,而 ACK 义务和发送记录独立保存。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_AsduQueue_Init` | 绑定调用方提供的两块字节数组,建立空的 HIGH/NORMAL 队列;代次 0 用于离线 NORMAL 缓存,不分配内存。 | +| `IEC104_AsduQueue_Get_Status` | 读取队列代次、各级保留条目数、保留待 ACK 数和已覆盖待 ACK 数等快照;原文为空不表示所有帧已确认。 | +| `IEC104_AsduQueue_Enqueue` | 复制 ASDU 到指定优先级队尾;HIGH 空间不足时返回 FULL,NORMAL 空间不足时覆盖最旧原文,编号窗口受阻时也返回 FULL。 | +| `IEC104_AsduQueue_Peek_Head` | 查看指定优先级最旧保留条目的身份、状态及最多两段原文借用;NORMAL 队首可能已经提交、正在等待 ACK。 | +| `IEC104_AsduQueue_Peek_Unsubmitted` | 查看指定优先级当前可交接的第一条未提交 ASDU;不移动游标,NORMAL 因 EOF 限制发送时返回 EMPTY。 | +| `IEC104_AsduQueue_Copy_Asdu` | 把有效条目视图中的一段或两段 ASDU 复制到连续输出缓冲;不添加 APCI,也不改变条目状态。 | +| `IEC104_AsduQueue_Mark_Submitted` | 在发送缓冲已接手完整 I 帧且 Manager 已建立记录后登记交接:HIGH 原文出队,NORMAL 标记为已提交并保留待 ACK。 | +| `IEC104_AsduQueue_Confirm_Next_Normal_Entry` | 按有效 ACK 对应的条目身份清理下一条 NORMAL;原文仍在时释放匹配队首,已覆盖时只核减覆盖待确认记账。 | +| `IEC104_AsduQueue_Discard_Unsubmitted_Head` | 主动丢弃尚未登记发送交接的物理队首条目;不能越过 NORMAL 待 ACK 前缀,也不能撤回已编号的 I 帧。 | +| `IEC104_AsduQueue_Get_Entry_By_Offset` | 按相对保留队首的条目偏移读取原文视图和提交状态,供检查或恢复使用;偏移单位为条目。 | +| `IEC104_AsduQueue_Get_Free_Bytes` | 查询指定优先级字节环当前未占用的字节数;查询不预留容量,NORMAL 仍可能通过覆盖旧原文入队。 | +| `IEC104_AsduQueue_Reset_High` | 清空 HIGH 待发原文及其队列游标;保留 NORMAL、连接绑定和已提交帧的发送记录。 | +| `IEC104_AsduQueue_Limit_Normal_Send` | 停止当前连接全部未提交 NORMAL 的新交接,用于 EOF 收尾;已有和后来新增的未提交原文保留给下一连接。 | +| `IEC104_AsduQueue_Detach` | 解除当前非零连接代次的绑定并回到离线代次 0;清空 HIGH,保留现存 NORMAL 并恢复待发,清除旧 ACK 记账和发送限制。 | +| `IEC104_AsduQueue_Rebind` | 绑定新的非零连接代次;清空 HIGH,把现存 NORMAL 按原顺序恢复为待发,保留条目编号及原文,不能恢复已被覆盖的数据。 | +| `IEC104_AsduQueue_Abort` | 清零队列控制结构并丢弃全部条目引用;不清零、不释放调用方提供的字节数组,重新使用须 Init。 | + + +
+完整函数原型(16 个;参数和返回类型见声明) + +```c +#include "IEC104_AsduQueue.h" + +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); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Get_Status( + const IEC104_AsduQueue_t *queue, IEC104_AsduQueueStatus_t *status); + +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); + +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); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Copy_Asdu( + const IEC104_AsduHeadInfo_t *head_info, uint8_t *destination, uint16_t capacity); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Mark_Submitted( + IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduEntryId_t entry_id); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Confirm_Next_Normal_Entry( + IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduEntryId_t entry_id); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Discard_Unsubmitted_Head( + IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduEntryId_t entry_id); + +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); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Get_Free_Bytes( + const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, uint32_t *free_bytes); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Reset_High( + IEC104_AsduQueue_t *queue, uint64_t connection_generation); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Limit_Normal_Send( + IEC104_AsduQueue_t *queue, uint64_t connection_generation); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Detach( + IEC104_AsduQueue_t *queue, uint64_t connection_generation); + +extern IEC104_AsduQueueResult_t IEC104_AsduQueue_Rebind( + IEC104_AsduQueue_t *queue, uint64_t connection_generation); + +extern void IEC104_AsduQueue_Abort(IEC104_AsduQueue_t *queue); +``` + +
+ + + +## 3. IEC104_Codec.c + +实现:[IEC104_Codec.c](../../104/IEC104_Codec.c);声明:[IEC104_Codec.h](../../104/IEC104_Codec.h)。 + +104专用无状态编解码,只检查完整APDU的APCI并构造I/S/U帧。公共ASDU参数和公共头解析位于 `IEC60870_Types.h`、`IEC60870_Codec.c/h`,本模块不提供公共ASDU解析声明。 + +调用约定:Parse_APCI 返回 1/0,失败不保证输出结构内容;其中 asdu 指针借用输入 APDU。Build_Iframe 返回完整长度或 -1,Build_Sframe/Build_Uframe 返回 6 或 -1。构帧函数没有输出容量参数,调用方必须预先保证 I 帧缓冲至少为 `6 + asdu_len` 字节,S/U 缓冲至少 6 字节;序号须在 0~32767。构帧不更新链路状态,也不表示实际写出。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Get_Frame_Type` | 根据控制域首字节的低位识别 I、S、U 帧;不验证完整控制域、功能码或长度。 | +| `IEC104_Parse_APCI` | 检查完整 APDU 的起始字节、长度和控制域,解析帧类型、N(S)、N(R)、U 功能码及 ASDU 位置;I 帧须容纳完整 ASDU 公共头。 | +| `IEC104_Build_Iframe` | 按给定 N(S)、N(R) 和 6~249 字节 ASDU 编码完整 I 帧,返回 APDU 长度;不修改链路状态。 | +| `IEC104_Build_Sframe` | 按给定 N(R) 编码固定 6 字节 S 帧;不推进链路接收确认位置。 | +| `IEC104_Build_Uframe` | 校验整数功能码并编码固定 6 字节 U 帧,只接受六种标准功能值;短枚举构建下也会拒绝 0x107 等非法原值。 | + + +
+完整函数原型(5 个;参数和返回类型见声明) + +```c +#include "IEC104_Codec.h" + +extern IEC104_FrameType_t IEC104_Get_Frame_Type(uint8_t control); + +extern int IEC104_Parse_APCI(const uint8_t *frame, + uint16_t frame_len, + IEC104_ApciInfo_t *apci); + +extern int IEC104_Build_Iframe(uint8_t *buf, + uint16_t send_seq, + uint16_t recv_seq, + const uint8_t *asdu, + int asdu_len); + +extern int IEC104_Build_Sframe(uint8_t *buf, uint16_t recv_seq); + +extern int IEC104_Build_Uframe(uint8_t *buf, int function); +``` + +
+ + + +## 4. IEC104_Connection.c + +实现:[IEC104_Connection.c](../../104/IEC104_Connection.c);声明:[IEC104_Connection.h](../../104/IEC104_Connection.h)。 + +子站单连接协调层,串行衔接 Receiver、Link 和 SendManager,管理期限与接收批次;由 TcpServer 调用,不执行 socket I/O。 + +调用约定:初始化后保持 Connection 和外部队列、控制槽、记录数组的地址及寿命稳定。调用方提供同一单调时钟基准的毫秒值;Feed 的 consumed 表示本次已经消费的输入,尚未消费的尾部须保留。已受理 I 帧只能在上层成功复制后按原 generation/receive_serial 释放。 + +结果区分 `FRAME_READY`(已受理的接收 I 帧)、`SEND_READY`(待写字节)、`FRAME_WRITTEN`(整帧完成处理)、`WAIT`、`EMPTY`、`ERROR` 和 `FATAL`,不能把任何正值都当作可发送。Begin/Yield/Tick 的批次协议用于先处理已到达确认,不改变原始期限;FAILED 后仍可查询受理帧和发送记录供收尾,Close 不关闭 socket 或清除服务级队列。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Connection_Config_Default` | 填写默认协议参数;本地发送总期限和 STOP 总期限取默认 t1,HIGH 调度默认为严格优先。 | +| `IEC104_Connection_Init` | 建立初始 STOPPED 的子站连接,绑定接收器、链路、发送缓冲、ASDU 队列及记录存储;由 Manager 在最终地址建立绑定,失败不修改原连接和队列。 | +| `IEC104_Connection_Begin_Receive_Batch` | 开始一轮有界接收批次,暂缓 I 帧 ACK 和 TESTFR_CON 的 t1 故障判定,让已到达的确认先被处理;保留原期限。 | +| `IEC104_Connection_Yield_Receive_Batch` | 因调度预算用尽而结束本轮接收,标记固定输入快照仍待处理并要求立即续收;继续暂缓确认 t1 判定,本地发送和 STOP 期限仍生效。 | +| `IEC104_Connection_Set_Transfer_Observer` | 在初始化后、尚未形成首个 APDU 时安装启停通知容量查询和状态变化通知适配器;供 TcpServer 内部使用。 | +| `IEC104_Connection_Feed` | 按序递入 TCP 字节,每次最多形成并分派一帧;I 帧经链路受理后保留给上层,S/U 受理后释放,资源不足时保留原帧并返回 WAIT。 | +| `IEC104_Connection_Peek_Received_Frame` | 读取已完成链路受理的 I 帧借用视图及 receive_serial;不重复处理序号或 ACK,FAILED 阶段仍可用于回收。 | +| `IEC104_Connection_Release_Received_Frame` | 按连接代次和 receive_serial 释放已受理 I 帧的接收空间;用于上层成功复制事件后,不再次提交链路事件。 | +| `IEC104_Connection_Submit_Response` | 校验当前连接和应答准入,尝试直接准备应答 I 帧,暂不能准备时复制到 HIGH;成功表示原文已接手,不执行实际网络发送。 | +| `IEC104_Connection_Try_Submit_Response` | 同一会话和期限检查下仅尝试直接准备;有待发HIGH/NORMAL、缓冲或窗口背压以及到期协议工作时WAIT,不缓存ASDU。 | +| `IEC104_Connection_Prepare_Next_Send` | 推进保留帧、控制义务及本地期限,再取得当前待写字节;无当前帧时才选择 ASDU,确认 t1 已到期时暂停新 I 帧编号。 | +| `IEC104_Connection_Get_Record_Wait` | 在 EOF 收尾的发送 WAIT 后,只读判断是否仅因可回收的已确认记录占满容量而阻塞,供 TcpServer 决定是否等待事件空间后续发。 | +| `IEC104_Connection_Handle_Send_Progress` | 提交真实写入增量,经 Manager 完成整帧记账,并更新活动、接收确认、TESTFR 或 STOP 计时;随后重试保留帧和控制流程。 | +| `IEC104_Connection_Handle_Send_Failure` | 核对当前发送帧身份后提交最终写失败,冻结连接并保留记录供回收;错误帧身份不影响当前帧。 | +| `IEC104_Connection_Tick` | 结束接收批次,重试保留帧、检查全部活动期限并推进 S 确认、STOP 应答和 TESTFR;不准备新的业务 I 帧。 | +| `IEC104_Connection_Get_Wait_Time` | 汇总本地发送、I 帧 ACK、TESTFR、STOP、t2/t3 的适用期限,计算不超过调用方上限的等待毫秒数;只读,不续期。 | +| `IEC104_Connection_Peek_Confirmed_Record` | 读取最早一条同时满足本地完整发送和有效对端 ACK 的回执副本;不自动移除,FAILED 阶段仍可查询。 | +| `IEC104_Connection_Remove_Confirmed_Record` | 按 frame_id 移除上层已成功复制的就绪队首回执,释放记录槽;不再次释放 ASDU 原文。 | +| `IEC104_Connection_Get_Record_By_Offset` | 按发送交接顺序读取指定偏移的剩余记录,包含本地完成及 ACK 事实,供观察和故障恢复使用。 | +| `IEC104_Connection_Get_Status` | 读取连接代次、生命周期、首次故障、启停/探测状态、记录数量和保留接收帧概况;CLOSED 后链路字段为历史状态。 | +| `IEC104_Connection_Handle_Transport_Closed` | 收到外部传输关闭或故障通知后冻结连接,保留首次故障和现有记录;不关闭 socket,也不推定发送完成。 | +| `IEC104_Connection_Close` | 结束内部接收、发送及计时状态,解除借用并置 CLOSED;保留外部 ASDU 队列,不关闭 socket,也不隐式执行队列重绑。 | + + +
+完整函数原型(22 个;参数和返回类型见声明) + +```c +#include "IEC104_Connection.h" + +extern void IEC104_Connection_Config_Default(IEC104_ConnectionConfig_t *config); + +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); + +extern IEC104_ConnectionResult_t IEC104_Connection_Begin_Receive_Batch( + IEC104_Connection_t *connection, uint64_t connection_generation); + +extern IEC104_ConnectionResult_t IEC104_Connection_Yield_Receive_Batch( + IEC104_Connection_t *connection, uint64_t connection_generation, uint64_t now_ms); + +extern IEC104_ConnectionResult_t IEC104_Connection_Set_Transfer_Observer( + IEC104_Connection_t *connection, uint64_t connection_generation, + const IEC104_ConnectionTransferObserver_t *observer); + +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); + +extern IEC104_ConnectionResult_t IEC104_Connection_Peek_Received_Frame( + const IEC104_Connection_t *connection, uint64_t connection_generation, + IEC104_ConnectionReceivedFrame_t *frame); + +extern IEC104_ConnectionResult_t IEC104_Connection_Release_Received_Frame( + IEC104_Connection_t *connection, uint64_t connection_generation, uint64_t receive_serial); + +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); + +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); + +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); + +extern IEC104_ConnectionResult_t IEC104_Connection_Get_Record_Wait( + const IEC104_Connection_t *connection, uint64_t connection_generation, uint8_t *record_wait); + +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); + +extern IEC104_ConnectionResult_t IEC104_Connection_Handle_Send_Failure( + IEC104_Connection_t *connection, IEC104_SendFrameId_t frame_id); + +extern IEC104_ConnectionResult_t IEC104_Connection_Tick( + IEC104_Connection_t *connection, uint64_t connection_generation, uint64_t now_ms); + +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); + +extern IEC104_ConnectionResult_t IEC104_Connection_Peek_Confirmed_Record( + const IEC104_Connection_t *connection, uint64_t connection_generation, + IEC104_IFrameSendRecord_t *record); + +extern IEC104_ConnectionResult_t IEC104_Connection_Remove_Confirmed_Record( + IEC104_Connection_t *connection, IEC104_SendFrameId_t frame_id); + +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); + +extern IEC104_ConnectionResult_t IEC104_Connection_Get_Status( + const IEC104_Connection_t *connection, uint64_t connection_generation, + IEC104_ConnectionStatus_t *status); + +extern IEC104_ConnectionResult_t IEC104_Connection_Handle_Transport_Closed( + IEC104_Connection_t *connection, uint64_t connection_generation); + +extern IEC104_ConnectionResult_t IEC104_Connection_Close( + IEC104_Connection_t *connection, uint64_t connection_generation); +``` + +
+ + + +## 5. IEC104_Link.c + +实现:[IEC104_Link.c](../../104/IEC104_Link.c);声明:[IEC104_Link_Common.h](../../104/IEC104_Link_Common.h)、[IEC104_Link_Internal.h](../../104/IEC104_Link_Internal.h)。 + +主站与子站共用的链路状态、序号、窗口、S 确认和 TESTFR 算法;不执行网络 I/O。标有“内部协作”的函数供链路实现跨文件调用。 + +调用约定:每条连接独立初始化 LinkState,角色在连接期间固定。Link 只根据调用方递入的帧、时长和发送完成事实更新状态,不读取 socket 或系统时钟。`Prepare_*` 成功时保存输出描述,实际发送结束后用对应 `Complete_*` 回报一次,不能用最新状态替换原 N(S)/N(R) 快照。 + +接收 ACK 前必须已经提交其覆盖帧的本地发送完成;N(R) 不能越过已完成发送边界。`IEC104_LinkResult_t`、STARTDT/STOPDT、S 确认和 TESTFR 各结果枚举含义不同,按对应头文件的命名值分支。内部协作函数仅供 Link 实现,不供应用绕过角色或状态检查。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Link_Unconfirmed_Rx_Internal` | 【内部协作】计算 acked_nr 到 V(R) 的 15 位循环距离,得到已接收但尚未由本地完整发送的 I/S 帧确认的 I 帧数。 | +| `IEC104_Link_Rx_Ack_Covered_Internal` | 【内部协作】判断当前 V(R) 已被确认,或已由在途 S 帧的 N(R) 完整覆盖;不据此推进 acked_nr。 | +| `IEC104_Link_State_Valid_Internal` | 【内部协作】检查链路字段范围、序号关系、S 确认状态及当前角色的启停状态组合是否合法。 | +| `IEC104_Link_Get_Send_Progress` | 读取 I 帧发送进度快照:下一构帧序号、下一本地完成序号和对端累计确认位置。 | +| `IEC104_Link_Get_Status` | 读取角色、数据传输状态、启停过程、TESTFR 状态及未确认接收数量等链路快照。 | +| `IEC104_Link_Is_Initial_State` | 判断链路是否处于可绑定新 Manager 的初始状态:序号清零、STOPPED,启停、S 确认和 TESTFR 均空闲。 | +| `IEC104_Link_Config_Default` | 填写默认链路参数:k=12、w=8、t1=15000 ms、t2=10000 ms、t3=20000 ms。 | +| `IEC104_Link_State_Init` | 按指定主站或子站角色初始化链路,清零序号和确认快照,进入 STOPPED,各过程置空闲。 | +| `IEC104_Link_State_Reset` | 复位序号、S 确认、启停和 TESTFR 算法状态;保留有效角色并回到 STOPPED,非法角色规范化为 INVALID。 | +| `IEC104_Link_Handle_I_Frame` | 检查 I 帧接收阶段、N(S) 连续性及 N(R) 有效范围;成功后推进 V(R) 和对端确认位置 V(A)。 | +| `IEC104_Link_Handle_S_Frame` | 检查 S 帧接收阶段及 N(R),成功后推进 V(A);子站停止排空期间仍可接受 S 确认。 | +| `IEC104_Link_Can_Prepare_I_Frame` | 只读判断启停门控、k 窗口及序号边界是否允许准备新的 I 帧;不分配 N(S)。 | +| `IEC104_Link_Prepare_I_Frame` | 检查发送条件后构造 I 帧,保存原始 N(S)/N(R)/长度快照,并立即推进 V(S) 预留序号;成功后序号不可撤回。 | +| `IEC104_Link_Complete_I_Send` | 按原构帧快照和完整发送长度提交 I 帧本地完成,推进 sent_vs 及实际捎带的接收确认位置;不表示对端已 ACK。 | +| `IEC104_Link_T1_I_Ack_Expired` | 用调用方提供的经过时间判断是否有已完整发送、尚未确认的 I 帧达到 t1;仅预留但未完成的帧不计入。 | +| `IEC104_Link_Can_Prepare_S_Ack` | 只读判断是否需要且允许生成 S 确认,考虑 w 门限、立即确认、停止排空和已有 S 在途状态。 | +| `IEC104_Link_Prepare_S_Ack_Internal` | 【内部协作】按已规划的 N(R) 编码 S 帧并保存确认快照,置 S 待发送状态;实际发送完成前不推进 acked_nr。 | +| `IEC104_Link_Prepare_S_Ack` | 按 w 门限、立即确认请求或停止排空需要生成 S 帧,冻结本帧 N(R) 并登记待发送状态。 | +| `IEC104_Link_Complete_S_Ack_Send` | 提交 S 帧最终发送结果;成功时按保存的 N(R) 推进接收确认位置,并清除 S 在途状态和快照。 | +| `IEC104_Link_Prepare_T2_Ack` | 在未确认接收数据等待达到 t2 后,按 S 确认规则生成报文;仍受启停门控和已有 S 在途状态约束。 | +| `IEC104_Link_Role_Valid_Internal` | 【内部协作】在完整链路状态校验通过后,确认本端角色与主站或子站专用入口要求一致。 | +| `IEC104_Link_Handle_Testfr_Act` | 校验收到的 TESTFR_ACT 并生成 TESTFR_CON 应答;不修改本端主动探测状态,也不执行发送。 | +| `IEC104_Link_Can_Prepare_Testfr_Act` | 只读判断当前是否可启动一轮主动 TESTFR;已有探测时不允许重复准备,不判断 t3 时间。 | +| `IEC104_Link_Prepare_Testfr_Act` | 生成主动 TESTFR_ACT,并把探测状态设为等待本地发送完成。 | +| `IEC104_Link_Prepare_T3_Testfr` | 在调用方给出的空闲时长达到 t3 且无探测进行时,生成 TESTFR_ACT 并登记待发送状态。 | +| `IEC104_Link_Complete_Testfr_Act_Send` | 提交 TESTFR_ACT 最终发送结果;完整发送后进入等待 TESTFR_CON 的状态,失败时结束本轮探测并返回致命错误。 | +| `IEC104_Link_Handle_Testfr_Con` | 接受当前正在等待的 TESTFR_CON 并结束主动探测;没有匹配等待过程时忽略,不缓存提前到达的 CON。 | +| `IEC104_Link_T1_Testfr_Con_Expired` | 用调用方提供的经过时间判断本端等待 TESTFR_CON 是否达到 t1;ACT 尚未完整发送时不判该确认超时。 | + + +
+完整函数原型(28 个;参数和返回类型见声明) + +```c +#include "IEC104_Link_Internal.h" +#include "IEC104_Link_Common.h" + +/* 内部协作:仅供库实现。 */ +uint16_t IEC104_Link_Unconfirmed_Rx_Internal(const IEC104_LinkState_t *state); + +/* 内部协作:仅供库实现。 */ +int IEC104_Link_Rx_Ack_Covered_Internal(const IEC104_LinkState_t *state); + +/* 内部协作:仅供库实现。 */ +int IEC104_Link_State_Valid_Internal(const IEC104_LinkState_t *state); + +extern int IEC104_Link_Get_Send_Progress( + const IEC104_LinkState_t *state, IEC104_IFrameSendProgress_t *progress); + +extern int IEC104_Link_Get_Status(const IEC104_LinkState_t *state, IEC104_LinkStatus_t *status); + +extern int IEC104_Link_Is_Initial_State(const IEC104_LinkState_t *state); + +extern void IEC104_Link_Config_Default(IEC104_LinkConfig_t *config); + +extern int IEC104_Link_State_Init(IEC104_LinkState_t *state, + IEC104_LinkRole_t role); + +extern void IEC104_Link_State_Reset(IEC104_LinkState_t *state); + +extern IEC104_LinkResult_t IEC104_Link_Handle_I_Frame( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci); + +extern IEC104_LinkResult_t IEC104_Link_Handle_S_Frame( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci); + +extern int IEC104_Link_Can_Prepare_I_Frame(const IEC104_LinkState_t *state, uint16_t k); + +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); + +extern IEC104_LinkResult_t IEC104_Link_Complete_I_Send( + IEC104_LinkState_t *state, const IEC104_IFrameTx_t *tx, int sent_len); + +extern int IEC104_Link_T1_I_Ack_Expired( + const IEC104_LinkState_t *state, + uint32_t elapsed_ms, uint32_t t1_ms); + +extern int IEC104_Link_Can_Prepare_S_Ack(const IEC104_LinkState_t *state, uint16_t w, int immediate); + +/* 内部协作:仅供库实现。 */ +int IEC104_Link_Prepare_S_Ack_Internal(IEC104_LinkState_t *state, + uint8_t *response, uint16_t receive_number); + +extern int IEC104_Link_Prepare_S_Ack(IEC104_LinkState_t *state, + uint16_t w, int immediate, + uint8_t *response, uint16_t capacity); + +extern IEC104_LinkResult_t IEC104_Link_Complete_S_Ack_Send( + IEC104_LinkState_t *state, int sent_len); + +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); + +/* 内部协作:仅供库实现。 */ +int IEC104_Link_Role_Valid_Internal(const IEC104_LinkState_t *state, IEC104_LinkRole_t role); + +extern IEC104_LinkResult_t IEC104_Link_Handle_Testfr_Act( + const IEC104_ApciInfo_t *apci, uint8_t *response, uint16_t capacity); + +extern int IEC104_Link_Can_Prepare_Testfr_Act(const IEC104_LinkState_t *state); + +extern int IEC104_Link_Prepare_Testfr_Act(IEC104_LinkState_t *state, + uint8_t *request, + uint16_t capacity); + +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); + +extern IEC104_LinkResult_t IEC104_Link_Complete_Testfr_Act_Send( + IEC104_LinkState_t *state, int sent_len); + +extern IEC104_LinkResult_t IEC104_Link_Handle_Testfr_Con( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci); + +extern int IEC104_Link_T1_Testfr_Con_Expired( + const IEC104_LinkState_t *state, + uint32_t elapsed_ms, uint32_t t1_ms); +``` + +
+ + + +## 6. IEC104_Link_Master_StartStop.c + +实现:[IEC104_Link_Master_StartStop.c](../../104/IEC104_Link_Master_StartStop.c);声明:[IEC104_Link_Master_StartStop.h](../../104/IEC104_Link_Master_StartStop.h)。 + +控制站主动 STARTDT/STOPDT 流程;当前 TcpServer 是子站实现,不通过这些入口发起启停。 + +调用约定:仅适用于 `IEC104_ROLE_CONTROLLING`。START 使用 Prepare → 实际发送 → Complete → Handle_CON;STOP 使用 Request → Prepare(可能先生成 S 帧)→ 对应 Complete → Handle_CON。请求已受理、报文已准备、等待发送及等待对端确认是不同阶段;保存原发送描述并使用匹配的完成接口。此文件不提供主站 TCP 客户端、自动计时或业务调度。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Link_Prepare_Startdt_Act` | 在主站已停止且无启停过程时生成 STARTDT_ACT,登记 ACT 待发送状态;已经启动或过程忙时不生成。 | +| `IEC104_Link_Complete_Startdt_Act_Send` | 提交 STARTDT_ACT 最终发送结果,完整发送后转为等待 STARTDT_CON;失败时清理本次过程并返回致命错误。 | +| `IEC104_Link_Handle_Startdt_Con` | 接受主站当前正在等待的 STARTDT_CON,进入 STARTED 并结束启动过程;不重置链路序号。 | +| `IEC104_Link_T1_Startdt_Con_Expired` | 检查主站等待 STARTDT_CON 的时间是否达到 t1;未处于该等待阶段时返回未到期。 | +| `IEC104_Link_Request_Stopdt` | 受理主站主动停止意图,进入 STOP_REQUESTED 并立即禁止准备新业务 I 帧;此时不生成报文。 | +| `IEC104_Link_Prepare_Stopdt_Request` | 推进已受理的停止请求,按需先生成 S 确认,再生成 STOPDT_ACT;输出实际帧类型供后续完成分派。 | +| `IEC104_Link_Complete_Stopdt_Act_Send` | 提交 STOPDT_ACT 最终发送结果,完整发送后转为等待 STOPDT_CON。 | +| `IEC104_Link_Handle_Stopdt_Con` | 在匹配停止等待、全部预留 I 帧已获 ACK 且接收确认已覆盖时,接受 STOPDT_CON 并进入 STOPPED。 | +| `IEC104_Link_T1_Stopdt_Con_Expired` | 检查主站等待 STOPDT_CON 是否达到 t1;不用于子站 STOP_DRAINING 排空计时。 | + + +
+完整函数原型(9 个;参数和返回类型见声明) + +```c +#include "IEC104_Link_Master_StartStop.h" + +extern int IEC104_Link_Prepare_Startdt_Act(IEC104_LinkState_t *state, + uint8_t *request, + uint16_t capacity); + +extern IEC104_LinkResult_t IEC104_Link_Complete_Startdt_Act_Send( + IEC104_LinkState_t *state, int sent_len); + +extern IEC104_LinkResult_t IEC104_Link_Handle_Startdt_Con( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci); + +extern int IEC104_Link_T1_Startdt_Con_Expired( + const IEC104_LinkState_t *state, + uint32_t elapsed_ms, uint32_t t1_ms); + +extern IEC104_StopdtRequestResult_t IEC104_Link_Request_Stopdt( + IEC104_LinkState_t *state); + +extern IEC104_StopdtPrepareResult_t IEC104_Link_Prepare_Stopdt_Request( + IEC104_LinkState_t *state, uint8_t *request, uint16_t capacity, + IEC104_StopdtTx_t *tx); + +extern IEC104_LinkResult_t IEC104_Link_Complete_Stopdt_Act_Send( + IEC104_LinkState_t *state, int sent_len); + +extern IEC104_LinkResult_t IEC104_Link_Handle_Stopdt_Con( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci); + +extern int IEC104_Link_T1_Stopdt_Con_Expired( + const IEC104_LinkState_t *state, + uint32_t elapsed_ms, uint32_t t1_ms); +``` + +
+ + + +## 7. IEC104_Link_Slave_StartStop.c + +实现:[IEC104_Link_Slave_StartStop.c](../../104/IEC104_Link_Slave_StartStop.c);声明:[IEC104_Link_Slave_StartStop.h](../../104/IEC104_Link_Slave_StartStop.h)。 + +被控站启停采用 ACT 受理即改变状态的模型;CON 的发送完成不回写启停状态。 + +调用约定:仅适用于 `IEC104_ROLE_CONTROLLED`。START/STOP 的状态在 ACT 受理时提交,CON 写出完成不再次覆盖后来的启停状态;STOP 还需排空已编号 I 帧及确认义务。通过 Slave/TcpServer 使用时,本层由 SendManager/Connection 调用,应用只消费 ACTIVATED/DEACTIVATED。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Link_Handle_Startdt_Act` | 校验子站收到的 STARTDT_ACT,生成 STARTDT_CON,并立即进入 STARTED、结束停止排空;重复 START 也生成确认。 | +| `IEC104_Link_Complete_Startdt_Con_Send` | 检查子站角色、链路状态和 STARTDT_CON 完整发送长度;不在完成时重新激活链路。 | +| `IEC104_Link_Handle_Stopdt_Act` | 受理 STOPDT_ACT 后立即进入 STOPPED 和 STOP_DRAINING;排空中的重复 STOP 返回 IGNORED,不重启过程。 | +| `IEC104_Link_Can_Prepare_Stopdt_Response` | 只读判断停止排空当前能否准备下一帧 S 确认或 STOPDT_CON。 | +| `IEC104_Link_Prepare_Stopdt_Response` | 推进子站停止排空:先按需生成 S 确认,全部预留 I 帧获 ACK 后生成 STOPDT_CON;生成 CON 时结束排空过程。 | +| `IEC104_Link_Complete_Stopdt_Con_Send` | 检查子站角色、链路状态和 STOPDT_CON 完整发送长度;不覆盖后来 START 已提交的状态。 | + + +
+完整函数原型(6 个;参数和返回类型见声明) + +```c +#include "IEC104_Link_Slave_StartStop.h" + +extern IEC104_LinkResult_t IEC104_Link_Handle_Startdt_Act( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci, + uint8_t *response, uint16_t capacity); + +extern IEC104_LinkResult_t IEC104_Link_Complete_Startdt_Con_Send( + IEC104_LinkState_t *state, int sent_len); + +extern IEC104_LinkResult_t IEC104_Link_Handle_Stopdt_Act( + IEC104_LinkState_t *state, const IEC104_ApciInfo_t *apci); + +extern int IEC104_Link_Can_Prepare_Stopdt_Response(const IEC104_LinkState_t *state); + +extern IEC104_StopdtPrepareResult_t IEC104_Link_Prepare_Stopdt_Response( + IEC104_LinkState_t *state, uint8_t *response, uint16_t capacity, + IEC104_StopdtTx_t *tx); + +extern IEC104_LinkResult_t IEC104_Link_Complete_Stopdt_Con_Send( + IEC104_LinkState_t *state, int sent_len); +``` + +
+ + + +## 8. IEC104_Log.c + +实现:[IEC104_Log.c](../../104/IEC104_Log.c);声明:[IEC104_Log.h](../../104/IEC104_Log.h)。 + +通用日志输出,使用固定栈缓冲;日志配置不带内部锁。 + +调用约定:在并发输出开始前配置级别和回调;回调接收的字符串只在回调期间有效,保存时复制。消息最多保留 511 字节,超出按字节截断,默认输出另加换行;格式串和参数须符合 printf 规则。日志全局配置和输出不是服务事件接口,不参与协议推进。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Set_Log_Callback` | 设置日志输出回调;传 NULL 时恢复默认标准流输出。 | +| `IEC104_Set_Log_Level` | 设置允许输出的最低日志级别,低于该级别的消息在格式化前被过滤。 | +| `IEC104_Log_Emit` | 格式化带本地时间戳和级别的日志,优先调用用户回调;无回调时 DEBUG/INFO 输出到 stdout,WARN/ERR 输出到 stderr。 | + + +
+完整函数原型(3 个;参数和返回类型见声明) + +```c +#include "IEC104_Log.h" + +extern void IEC104_Set_Log_Callback(IEC104_LogCb_t cb); + +extern void IEC104_Set_Log_Level(int min_level); + +extern void IEC104_Log_Emit(int level, const char *fmt, ...); +``` + +
+ + + +## 9. IEC104_Net_Windows.c + +实现:[IEC104_Net_Windows.c](../../104/IEC104_Net_Windows.c);声明:[IEC104_Net.h](../../104/IEC104_Net.h)。 + +Windows 同步非阻塞网络后端,将 Winsock 操作和错误转换为通用网络接口结果。 + +调用约定:Init/Cleanup 配对维护平台网络引用;Listen/Accept 交付的 socket 由所属服务 Close。Recv/Send 的 transferred 是本次增量,不是累计数;WOULD_BLOCK、RETRY、RETRY_LATER、EOF 和 ERROR 必须分别调度,EOF 仅表示接收方向关闭。Wait 只报告可尝试的操作,后续非阻塞 I/O 才确定实际结果。 + +Now_Ms 提供协议单调毫秒;Pending_Input 查询当时已排队字节数,供接收预算让出时固定范围,不读取这些字节。Net 层不理解 ASDU、ACK 或业务连接身份,应用也不能通过 Prepare 导出的 socket 接管收发。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Net_Init` | 调用 WSAStartup 初始化 Winsock 2.2,取得一次需要与 Cleanup 配对释放的平台引用。 | +| `IEC104_Net_Cleanup` | 调用 WSACleanup 释放一次成功 Init 取得的 Winsock 引用;不代替关闭各个 socket。 | +| `IEC104_Net_Invalid_Argument_Error` | 返回 Windows 后端的参数错误码 WSAEINVAL,避免上层依赖平台错误常量。 | +| `IEC104_Net_Listen` | 解析数字 IPv4 地址,创建独占绑定的非阻塞 TCP 监听 socket,并返回实际监听端口;端口 0 由系统分配。 | +| `IEC104_Net_Accept` | 尝试接入一个客户端,设置非阻塞和 TCP_NODELAY,返回客户端句柄及对端 IPv4/端口;区分暂无连接、可重试和资源暂缺。 | +| `IEC104_Net_Recv` | 执行一次非阻塞接收,返回实际字节数、EOF 或错误分类;不组帧,不在 EOF 时自行关闭 socket。 | +| `IEC104_Net_Pending_Input` | 通过 FIONREAD 查询当前 socket 接收缓冲中的可读字节数;不读取或消费这些字节。 | +| `IEC104_Net_Send` | 执行一次非阻塞发送,返回本次实际交给本机网络栈的字节数及错误分类;允许部分写入,不自动补发。 | +| `IEC104_Net_Wait` | 用一次 select 等待监听接入和客户端读写就绪,返回通用事件位;关注集合为空时执行有限定时等待。 | +| `IEC104_Net_Close` | 关闭指定 socket;无效句柄按成功处理,不释放 Winsock 初始化引用。 | +| `IEC104_Net_Now_Ms` | 读取 GetTickCount64 的系统启动后毫秒计数,用于单调期限计算,不受系统日历校时影响。 | + + +
+完整函数原型(11 个;参数和返回类型见声明) + +```c +#include "IEC104_Net.h" + +extern IEC104_NetResult_t IEC104_Net_Init(int *native_error); + +extern void IEC104_Net_Cleanup(void); + +extern int IEC104_Net_Invalid_Argument_Error(void); + +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); + +extern IEC104_NetResult_t IEC104_Net_Accept( + IEC104_NetSocket_t listener, IEC104_NetSocket_t *client, IEC104_NetPeer_t *peer, int *native_error); + +extern IEC104_NetIoResult_t IEC104_Net_Recv( + IEC104_NetSocket_t socket_handle, void *data, size_t capacity); + +extern IEC104_NetResult_t IEC104_Net_Pending_Input( + IEC104_NetSocket_t socket_handle, uint32_t *available, int *native_error); + +extern IEC104_NetIoResult_t IEC104_Net_Send( + IEC104_NetSocket_t socket_handle, const void *data, size_t length); + +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); + +extern IEC104_NetResult_t IEC104_Net_Close( + IEC104_NetSocket_t socket_handle, int *native_error); + +extern uint64_t IEC104_Net_Now_Ms(void); +``` + +
+ + + +## 10. IEC104_Receiver.c + +实现:[IEC104_Receiver.c](../../104/IEC104_Receiver.c);声明:[IEC104_Receiver.h](../../104/IEC104_Receiver.h)。 + +每连接独立的流式接收器,一次保留一帧完整 APDU,不处理链路序号或协议 ACK。 + +调用约定:每条连接使用独立实例与非零 generation。Feed 每次最多交付一个完整 APDU,通过 consumed 返回实际消费量;FRAME_READY 后先 Peek,处理并 Release,才能继续后续字节。Peek 的指针借用 Receiver 缓冲,在 Release/Reset/Abort/重新 Init 后失效。 + +`IN_PROGRESS` 表示半帧待续,`FRAME_READY` 表示有完整帧,`FRAME_ERROR` 为报文格式错误,`ERROR` 为参数/身份/状态错误。成帧只做格式及最低长度检查;I/S 序号由 Link 校验,完整 ASDU 及业务条件由上层处理。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_Receiver_Init` | 清零接收器并绑定非零连接代次,用于首次使用或新连接换代。 | +| `IEC104_Receiver_Get_Status` | 校验连接代次和组帧状态后,读取已收长度、预期长度和完整帧就绪标志。 | +| `IEC104_Receiver_Abort` | 清零接收器及其绑定,使全部旧帧视图失效;之后须重新 Init。 | +| `IEC104_Receiver_Feed` | 按序消费字节片段,支持半包和粘包,每次最多收齐一帧;检查 APCI 和 ASDU 最低长度,返回实际消费量,完整帧未释放时停止消费。 | +| `IEC104_Receiver_Peek_Frame` | 取得当前完整 APDU 及 APCI 的只读借用视图,允许重复读取,不释放报文。 | +| `IEC104_Receiver_Release_Frame` | 释放已经受理的完整帧并清除组帧进度,使接收器可以继续接收下一帧;不代表协议 ACK 已发送。 | +| `IEC104_Receiver_Reset` | 显式丢弃当前半帧或完整帧,保留连接代次并恢复等待帧头;允许清理异常组帧进度,不复位 Link。 | + + +
+完整函数原型(7 个;参数和返回类型见声明) + +```c +#include "IEC104_Receiver.h" + +extern IEC104_ReceiverResult_t IEC104_Receiver_Init( + IEC104_Receiver_t *receiver, uint64_t connection_generation); + +extern IEC104_ReceiverResult_t IEC104_Receiver_Get_Status( + const IEC104_Receiver_t *receiver, uint64_t connection_generation, IEC104_ReceiverStatus_t *status); + +extern void IEC104_Receiver_Abort(IEC104_Receiver_t *receiver); + +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); + +extern IEC104_ReceiverResult_t IEC104_Receiver_Peek_Frame( + const IEC104_Receiver_t *receiver, uint64_t connection_generation, + IEC104_ReceivedFrame_t *frame); + +extern IEC104_ReceiverResult_t IEC104_Receiver_Release_Frame( + IEC104_Receiver_t *receiver, uint64_t connection_generation); + +extern IEC104_ReceiverResult_t IEC104_Receiver_Reset( + IEC104_Receiver_t *receiver, uint64_t connection_generation); +``` + +
+ + + +## 11. IEC104_SendBuffer.c + +实现:[IEC104_SendBuffer.c](../../104/IEC104_SendBuffer.c);声明:[IEC104_SendBuffer.h](../../104/IEC104_SendBuffer.h)。 + +保存一个当前 I 帧及独立的 S/U 控制 FIFO,管理预留、写入进度和本地发送期限,不执行网络 I/O。 + +调用约定:Reserve 获得短期可写 APDU 存储和 frame_id → 编码 → Commit_Frame;取消只适用于 Link 尚未成功分配序号的预留。Peek_Send_Head 的 pending_data 是剩余字节借用,部分写入后重新取得偏移,实际写出多少就 Record_Written_Bytes 多少,不重复登记。 + +`FRAME_WRITTEN` 仅表示字节已写完,须先完成适用的 Link/记录提交,再 Commit_Send_Completion 释放槽;不能把一次 send 成功当作整帧完成。结果还区分 FULL、BUSY、EMPTY、ERROR。初始化借用控制槽数组,Abort 只解除绑定;调用方不要同时绕过 Manager 手工提交完成。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_SendBuffer_Init` | 绑定调用方提供的控制帧数组和非零连接代次,初始化当前 I 槽、控制 FIFO 和帧编号分配状态。 | +| `IEC104_SendBuffer_Get_Status` | 读取控制帧数量、当前 I 帧及其 N(S)、预留状态、空闲状态和控制槽可用情况。 | +| `IEC104_SendBuffer_Reserve_S_U_Frame` | 预留控制 FIFO 队尾的一个 6 字节 S/U 槽,并分配不复用的 frame_id;容量不足或已有预留时不接手。 | +| `IEC104_SendBuffer_Reserve_I_Frame` | 在无当前 I 帧、无控制帧且无预留时,预留唯一 I 帧缓冲并分配 frame_id。 | +| `IEC104_SendBuffer_Commit_Frame` | 校验预留身份、APCI 类型及 I 帧原始快照后提交完整报文,登记原始入队时刻;I 帧成为当前帧,S/U 进入控制 FIFO。 | +| `IEC104_SendBuffer_Cancel_Reservation` | 核对身份后取消尚未提交的预留,已用帧编号不复用;仅在 Link 尚未成功构帧时使用,不回滚协议状态。 | +| `IEC104_SendBuffer_Peek_Send_Head` | 查看当前 I 帧或最早控制帧的身份、元数据及剩余字节借用;已经写完但尚未提交完成的帧仍可查询。 | +| `IEC104_SendBuffer_Record_Written_Bytes` | 按 frame_id 登记本次实际写入增量;全部字节写完时保存完成时刻并返回 FRAME_WRITTEN,不调用 Link 完成接口。 | +| `IEC104_SendBuffer_Commit_Send_Completion` | 在对应链路完成及 I 帧记录提交后,释放已经写完的当前 I 槽或控制 FIFO 队首;不等待对端 ACK。 | +| `IEC104_SendBuffer_Head_Send_Expired` | 按队首原始 queued_at_ms 判断本地排队及发送总期限是否到达;部分写入不续期。 | +| `IEC104_SendBuffer_Abort` | 清零发送缓冲控制结构并解除外部数组绑定;不关闭 TCP,也不释放调用方数组。 | + + +
+完整函数原型(11 个;参数和返回类型见声明) + +```c +#include "IEC104_SendBuffer.h" + +extern IEC104_SendBufferResult_t IEC104_SendBuffer_Init( + IEC104_SendBuffer_t *buffer, IEC104_SendBufferControlFrame_t *control_frames, + uint16_t control_capacity, uint64_t connection_generation); + +extern IEC104_SendBufferResult_t IEC104_SendBuffer_Get_Status( + const IEC104_SendBuffer_t *buffer, IEC104_SendBufferStatus_t *status); + +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); + +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); + +extern IEC104_SendBufferResult_t IEC104_SendBuffer_Cancel_Reservation( + IEC104_SendBuffer_t *buffer, IEC104_SendFrameId_t frame_id); + +extern IEC104_SendBufferResult_t IEC104_SendBuffer_Peek_Send_Head( + const IEC104_SendBuffer_t *buffer, IEC104_SendBufferHeadInfo_t *head_info); + +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); + +extern IEC104_SendBufferResult_t IEC104_SendBuffer_Commit_Send_Completion( + IEC104_SendBuffer_t *buffer, IEC104_SendFrameId_t frame_id); + +extern int IEC104_SendBuffer_Head_Send_Expired( + const IEC104_SendBuffer_t *buffer, uint64_t connection_generation, + uint64_t now_ms, uint32_t timeout_ms); + +extern void IEC104_SendBuffer_Abort(IEC104_SendBuffer_t *buffer); +``` + +
+ + + +## 12. IEC104_SendManager.c + +实现:[IEC104_SendManager.c](../../104/IEC104_SendManager.c);声明:[IEC104_SendManager.h](../../104/IEC104_SendManager.h)。 + +子站发送管理层,统一协调 Link、SendBuffer、AsduQueue 和 I 帧记录;当前只绑定 CONTROLLED 角色,不执行网络 I/O。 + +调用约定:Manager 绑定地址稳定、同代次的队列、Link、SendBuffer 和记录数组。发送准备固定为“记录槽检查 → 预留缓冲 → Link 分配序号 → 提交 APDU → 建记录 → 队列交接”;Link 已分配 N(S) 后的交接失败不能取消编号继续使用连接。 + +`QUEUED` 已接手但尚未写出;`WAIT` 尚待门控/容量;`FRAME_WRITTEN` 已完成本地记账,无需再手工 Complete。记录同时满足本地完成和对端 ACK 后,由上层复制再 Remove;删除记录不再次确认 ASDU。PROTOCOL_ERROR/FATAL 结束当前会话,Abort 前仍需保存剩余记录事实。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_SendManager_Get_Status` | 读取连接代次、记录容量、未消费记录数及 HIGH 未 ACK 数;不要求故障后的 Link 仍可运行。 | +| `IEC104_SendManager_Enqueue` | 校验当前会话后复制 ASDU 到绑定队列;HIGH 只在 STARTED/IDLE 接手,NORMAL 按队列策略缓存,不生成 I 帧。 | +| `IEC104_SendManager_Queue_S_Ack` | 先取得控制槽,再按 w 门限、立即确认或停止排空要求生成 S 帧并入队;完整发送前不提交接收确认。 | +| `IEC104_SendManager_Queue_T2_Ack` | 在调用方提供的接收确认等待时间达到 t2 后,尝试生成并排队 S 帧;不自行读取时钟或重置起点。 | +| `IEC104_SendManager_Queue_Testfr_Act` | 尝试生成并排队主动 TESTFR_ACT;已有探测或控制资源不足时等待,ACT 完整发送后才进入等待 CON。 | +| `IEC104_SendManager_Queue_T3_Testfr` | 在调用方提供的空闲时间达到 t3 时,尝试生成并排队 TESTFR_ACT。 | +| `IEC104_SendManager_Handle_Testfr_Act` | 处理收到的 TESTFR_ACT,为每次受理生成并排队 TESTFR_CON;控制槽不足返回 WAIT,请求尚未受理。 | +| `IEC104_SendManager_Handle_Testfr_Con` | 通过 Manager 统一接收 TESTFR_CON,结束匹配的主动探测;无需预留控制槽,当前未等待 CON 时返回 IGNORED。 | +| `IEC104_SendManager_Prepare_Next_Send` | 优先取得已有帧的剩余字节;无已有帧时按 HIGH/NORMAL 优先级及配额选择 ASDU,构帧、建立发送记录并完成队列交接。 | +| `IEC104_SendManager_Submit_Response` | 只在 STARTED/IDLE 接手应答;条件允许时直接准备 HIGH I 帧,否则缓存到 HIGH 队列,返回已准备、已入队或尚未接手。 | +| `IEC104_SendManager_Try_Submit_Response` | 仅在发送缓冲空闲、HIGH/NORMAL无未提交条目且窗口和记录容量允许时准备I帧;WAIT不入队、不分配N(S)。 | +| `IEC104_SendManager_Handle_Send_Progress` | 登记实际写入增量;整帧写完后按原帧类型提交 Link 完成、记录 I 帧本地完成事实,并释放发送缓冲槽。 | +| `IEC104_SendManager_Handle_Send_Failure` | 对匹配的未完成队首提交最终发送失败或本地发送超时,调用适用的链路失败完成接口并返回 FATAL;保留记录供收尾。 | +| `IEC104_SendManager_Handle_I_S_Frame` | 把收到的 I/S 帧交给 Link 校验序号和累计 ACK,登记全部新确认事实,核减 HIGH 未确认数,并释放 NORMAL 原文或覆盖记账。 | +| `IEC104_SendManager_Get_Record_By_Offset` | 按交接顺序读取指定偏移的 I 帧记录副本,包含完整 frame_id、来源、序号快照、时间及完成/ACK 标志。 | +| `IEC104_SendManager_Peek_Oldest_Sent_Unacked_Record` | 读取最早已经完整本地发送、尚未获对端 ACK 的 I 帧记录;只有未完成发送帧时返回 EMPTY。 | +| `IEC104_SendManager_Get_Deadlines` | 按同一时刻计算发送队首的本地发送剩余时间、最早已写完未 ACK 帧的 t1 剩余时间,并返回队首身份和控制槽容量状态。 | +| `IEC104_SendManager_T1_I_Ack_Expired` | 按最早已写完未 ACK 记录的原始 written_at_ms 判断 t1 是否到期;重复 ACK、部分 ACK 或后续发送不重置原起点。 | +| `IEC104_SendManager_Peek_Confirmed_Record` | 查看同时满足本地完整发送和对端有效 ACK 的队首回执;未就绪返回 WAIT,不跳过旧记录。 | +| `IEC104_SendManager_Remove_Confirmed_Record` | 按 frame_id 删除已就绪且已被上层复制的队首回执,释放记录容量;不改变协议序号或再次处理原文。 | +| `IEC104_SendManager_Abort` | 终止 Manager,在自身存储有效时同时终止绑定 SendBuffer;保留 AsduQueue 和调用方记录数组,不关闭 socket。 | +| `IEC104_SendManager_Slave_Handle_Startdt_Act` | 先预留控制槽再受理 START,立即激活并排队 STARTDT_CON;仅从停用状态启动时清空 HIGH 和重置连续 HIGH 配额。 | +| `IEC104_SendManager_Slave_Handle_Stopdt_Act` | 受理 STOP 后立即停用并清空 HIGH 待发原文;排空中的重复 STOP 返回 IGNORED,控制队列满也不阻止该入口受理。 | +| `IEC104_SendManager_Slave_Queue_Stopdt_Response` | 为停止排空生成并排队 S 确认或 STOPDT_CON;仅 STOPDT_CON 入队成功时输出其 frame_id,供连接层关联 STOP 总期限。 | +| `IEC104_SendManager_Init` | 绑定已初始化、同代次的子站 Link、SendBuffer、ASDU 队列及记录数组;要求新会话初始状态,成功清空 HIGH 并保留待发 NORMAL。 | +| `IEC104_SendManager_Init_Connection` | 统一准备新的子站 Link、SendBuffer 和 Manager,校验成功后向最终地址提交并建立指针绑定;供 Connection 初始化使用。 | + + +
+完整函数原型(26 个;参数和返回类型见声明) + +```c +#include "IEC104_SendManager.h" + +extern IEC104_SendManagerResult_t IEC104_SendManager_Get_Status( + const IEC104_SendManager_t *manager, IEC104_SendManagerStatus_t *status); + +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); + +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); + +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); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Queue_Testfr_Act( + IEC104_SendManager_t *manager, uint64_t connection_generation, uint64_t now_ms); + +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); + +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); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Handle_Testfr_Con( + IEC104_SendManager_t *manager, uint64_t connection_generation, const IEC104_ApciInfo_t *apci); + +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); + +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); + +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); + +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); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Handle_Send_Failure( + IEC104_SendManager_t *manager, IEC104_SendFrameId_t frame_id); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Handle_I_S_Frame( + IEC104_SendManager_t *manager, uint64_t connection_generation, const IEC104_ApciInfo_t *apci); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Get_Record_By_Offset( + const IEC104_SendManager_t *manager, uint16_t offset, IEC104_IFrameSendRecord_t *record); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Peek_Oldest_Sent_Unacked_Record( + const IEC104_SendManager_t *manager, IEC104_IFrameSendRecord_t *record); + +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); + +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); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Peek_Confirmed_Record( + const IEC104_SendManager_t *manager, IEC104_IFrameSendRecord_t *record); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Remove_Confirmed_Record( + IEC104_SendManager_t *manager, IEC104_SendFrameId_t frame_id); + +extern void IEC104_SendManager_Abort(IEC104_SendManager_t *manager); + +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); + +extern IEC104_SendManagerResult_t IEC104_SendManager_Slave_Handle_Stopdt_Act( + IEC104_SendManager_t *manager, uint64_t connection_generation, + const IEC104_ApciInfo_t *apci); + +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); + +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); + +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); +``` + +
+ + + +## 13. IEC104_TcpServer.c + +实现:[IEC104_TcpServer.c](../../104/IEC104_TcpServer.c);声明:[IEC104_TcpServer.h](../../104/IEC104_TcpServer.h)。 + +IPv4 子站的底层 TCP 服务,供 Slave 调用或由集成方独立使用,内部持有连接、队列、缓冲和事件 FIFO。一个服务同时管理一个活动主站;全部操作由所属线程串行驱动。共用值类型声明在 [IEC104_TcpTypes.h](../../104/IEC104_TcpTypes.h),业务主入口见 [Slave](#file-19)。 + +调用约定:Create → Config_Default/Init → Poll/NextEvent(或 Prepare/Process)→ Close → NextEvent 读空 → Destroy。Send_Response 输入原请求 generation 及不含 APCI 的 ASDU 字节;Enqueue_Event 同样输入 ASDU 原文,不能传完整 I 帧。直接使用本层时,应答来源身份由集成方保存;通过 Slave 时这些步骤已封装。 + +Prepare 的 socket 仅供注册,等待前落实 pending_event_count/need_process;Process 回传实际注册时的 service_token 和 socket/token。旧服务通知整批忽略,旧 socket 通知逐项忽略。Close_Connection 保留监听/NORMAL,Close 清除服务级缓存,两者均不等待 STOPDT 或 ACK。状态查询先看 connected 及各有效标志,历史 phase 不能代表当前连通。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC104_TcpServer_Create` | 从模块内部静态池取得地址固定的不透明服务句柄;不分配堆内存、不创建 socket,无可用槽时返回 NULL。 | +| `IEC104_TcpServer_Destroy` | 归还尚未 Init 或已经 Close 且事件已读空的服务对象;不隐式关闭运行中的服务,也不丢弃未读事件。 | +| `IEC104_TcpServer_Config_Default` | 填写默认监听地址 0.0.0.0、端口 2404 及默认协议配置,不申请网络资源。 | +| `IEC104_TcpServer_Init` | 初始化网络后端、非阻塞 IPv4 监听和服务级 ASDU 队列;成功后即可缓存离线 NORMAL,客户端协议连接在实际接入时建立。 | +| `IEC104_TcpServer_Prepare` | 只读导出事件循环所需的 socket/token/关注位、最近等待期限、未读事件数和本地推进要求;不接入、收发或推进协议。 | +| `IEC104_TcpServer_Process` | 根据一批有效就绪、定时或本地通知,有界执行客户端接入、非阻塞收发、协议期限检查和事件交付;不调用网络等待。 | +| `IEC104_TcpServer_Poll` | 组合 Prepare、至多一次平台网络等待和 Process,供独立循环使用;max_wait_ms 限制本轮网络等待时间。 | +| `IEC104_TcpServer_NextEvent` | 按 FIFO 复制并取出最早一条建连、启用、停用、I 帧、发送记录或断开事件;副本无需释放,Close 后仍可继续读取。 | +| `IEC104_TcpServer_Enqueue_Event` | 把主动上报 ASDU 复制到服务级 NORMAL 队列,支持首次连接前及断线期间缓存;空间不足覆盖最旧原文,不立即写 socket。 | +| `IEC104_TcpServer_Send_Response` | 按原请求事件的连接代次提交应答;仅启动状态接手,条件允许时直接准备并尝试一次非阻塞发送,否则入 HIGH,成功不表示已写完或已 ACK。 | +| `IEC104_TcpServer_Try_Send_Response` | 按原代次尝试不排队提交;WAIT未接手,OK已固定帧顺序,之后沿原部分写入和ACK路径推进;FATAL不可直接重放。 | +| `IEC104_TcpServer_Get_Remote_Address` | 读取当前主站的缓存 IPv4 地址、源端口及连接代次副本;不查询 socket,无当前连接时拒绝。 | +| `IEC104_TcpServer_Close_Connection` | 立即关闭指定代次的当前主站并交付收尾事件;保留监听和现存 NORMAL,清空 HIGH,不等待 STOPDT、发送完成或 ACK。 | +| `IEC104_TcpServer_Get_Status` | 读取监听端口、当前连接、最近会话、未读事件、HIGH/NORMAL 队列和接入重试诊断等快照;不消费数据或推进协议。 | +| `IEC104_TcpServer_Close` | 结束当前客户端和监听,清空 ASDU 缓存并释放网络后端引用;保留未读事件,事件读空后可重新 Init 或 Destroy。 | + + +
+完整函数原型(15 个;参数和返回类型见声明) + +```c +#include "IEC104_TcpServer.h" + +extern IEC104_TcpServer_t * IEC104_TcpServer_Create(void); + +extern IEC104_TcpResult_t IEC104_TcpServer_Destroy( + IEC104_TcpServer_t *server, IEC104_TcpError_t *error); + +extern void IEC104_TcpServer_Config_Default(IEC104_TcpServerConfig_t *config); + +extern IEC104_TcpResult_t IEC104_TcpServer_Init( + IEC104_TcpServer_t *server, const IEC104_TcpServerConfig_t *config, IEC104_TcpError_t *error); + +extern IEC104_TcpResult_t IEC104_TcpServer_Prepare( + const IEC104_TcpServer_t *server, IEC104_TcpWaitInfo_t *info, IEC104_TcpError_t *error); + +extern IEC104_TcpResult_t IEC104_TcpServer_Process( + IEC104_TcpServer_t *server, const IEC104_TcpReady_t *ready, IEC104_TcpError_t *error); + +extern IEC104_TcpResult_t IEC104_TcpServer_Poll( + IEC104_TcpServer_t *server, uint32_t max_wait_ms, IEC104_TcpError_t *error); + +extern IEC104_TcpEventResult_t IEC104_TcpServer_NextEvent( + IEC104_TcpServer_t *server, IEC104_TcpEvent_t *event); + +extern IEC104_TcpSubmitResult_t IEC104_TcpServer_Enqueue_Event( + IEC104_TcpServer_t *server, const uint8_t *asdu, uint16_t asdu_length); + +extern IEC104_TcpSubmitResult_t IEC104_TcpServer_Send_Response( + IEC104_TcpServer_t *server, uint64_t generation, + const uint8_t *asdu, uint16_t asdu_length); + +extern IEC104_TcpSubmitResult_t IEC104_TcpServer_Try_Send_Response( + IEC104_TcpServer_t *server, uint64_t generation, + const uint8_t *asdu, uint16_t asdu_length); + +extern IEC104_TcpResult_t IEC104_TcpServer_Get_Remote_Address( + const IEC104_TcpServer_t *server, IEC104_TcpPeerInfo_t *peer, IEC104_TcpError_t *error); + +extern IEC104_TcpResult_t IEC104_TcpServer_Close_Connection( + IEC104_TcpServer_t *server, uint64_t generation, IEC104_TcpError_t *error); + +extern IEC104_TcpResult_t IEC104_TcpServer_Get_Status( + const IEC104_TcpServer_t *server, IEC104_TcpServerStatus_t *status, IEC104_TcpError_t *error); + +extern IEC104_TcpResult_t IEC104_TcpServer_Close( + IEC104_TcpServer_t *server, IEC104_TcpError_t *error); +``` + +
+ + + +## 14. IEC60870_Clock.c + +实现:[IEC60870_Clock.c](../../common/IEC60870_Clock.c);声明:[IEC60870_Clock.h](../../common/IEC60870_Clock.h)。 + +读取或设置实际时钟,公共日历统一为固定 UTC+8 标准时间。Windows 实现已提供;RTC 由编译选项选择并调用外部驱动接口。 + +调用约定:返回 1/0;Read 失败保持输出,成功后仍检查 is_invalid。Set 会调用实际时间源,无法保证底层硬件部分写入的回滚。`IEC60870_CLOCK_USE_RTC` 选择 Windows/RTC 时间源,与 `IEC60870_TIME_USE_PLATFORM` 的日期转换算法选择独立;RTC 驱动和硬件初始化由平台提供。Clock 无需创建对象或注册回调,也不自动响应校时命令。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC60870_Clock_Read` | 从选定的 Windows 或 RTC 时间源读取固定 UTC+8 日历,校验字段和质量标志、重算星期后输出;读取成功仍需检查 is_invalid。 | +| `IEC60870_Clock_Set` | 校验固定 UTC+8 日历并重算星期,再设置实际时钟;Windows 路径减去八小时后调用 SetSystemTime,RTC 路径交给驱动。 | + +`IEC60870_Clock_RTC_Read` 和 `IEC60870_Clock_RTC_Set` 当前仅在头文件声明,需由板级驱动实现,因此不计入本文件已实现的 API。 + + +
+完整函数原型(2 个;参数和返回类型见声明) + +```c +#include "IEC60870_Clock.h" + +int IEC60870_Clock_Read(IEC60870_ClockValue_t *value); + +int IEC60870_Clock_Set(const IEC60870_WallTime_t *local_time); +``` + +
+ + + +## 15. IEC60870_Time.c + +实现:[IEC60870_Time.c](../../common/IEC60870_Time.c);声明:[IEC60870_Time.h](../../common/IEC60870_Time.h)。 + +101/104 共用的日历、纪元秒数和 CP16/CP24/CP56Time2a 工具;普通日历范围为 1970~2099 年,CP56 范围为 2000~2099 年,不读取或设置实际时钟。 + +调用约定:所有转换返回 1/0,失败保持输出;输入带 length 时只读取该时标所需的前 2/3/7 字节。CP16 是经过毫秒,CP24 仅有分钟及分钟内毫秒,不能独立推断完整日期。CP56 解码允许 IV=1,并保留 SU;CP56_To_Seconds/Local 则拒绝无效时间,按源标准时区和 SU 处理偏移。 + +To_Seconds 的输入是 UTC,Seconds_To_Local 的输出固定 UTC+8,不采用 Windows 本地时区。To_CP56/From_CP56 只编解码,不自动转换时区;设置实际时钟使用 Clock,并先完成品质和业务检查。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC60870_Time_To_CP16` | 将0..65535毫秒经过时间编码为2字节,不包含日历或质量标志。 | +| `IEC60870_Time_From_CP16` | 解码前2字节经过时间,失败不修改输出。 | +| `IEC60870_Time_To_CP24` | 将分钟、分钟内毫秒和IV编码为3字节短时标;不推断小时及日期。 | +| `IEC60870_Time_From_CP24` | 解码前3字节短时标,校验bit6保留位及数值范围,保留IV。 | +| `IEC60870_Time_Is_Valid` | 检查年月日、时分秒和毫秒的数值是否合法;忽略派生字段 weekday,不判断时区或时钟准确性。 | +| `IEC60870_Time_Get_Weekday` | 校验日期并计算星期,输出 0~6,其中 0 表示周日。 | +| `IEC60870_Time_To_Seconds` | 将调用方提供的 UTC 日历转换为自 1970-01-01 00:00:00 UTC 起的完整秒数;毫秒只校验、不计入结果。 | +| `IEC60870_Time_From_Seconds` | 将 UTC 纪元秒数和毫秒余数转换为完整 UTC 日历,并计算星期;可选 Windows 平台库或纯整数算法。 | +| `IEC60870_Time_Seconds_To_Local` | 将 UTC 纪元秒数及毫秒转换为固定 UTC+8 日历,自动处理跨日、跨月、跨年及星期。 | +| `IEC60870_Time_CP56_To_Seconds` | 将 CP56 解析值结合源标准时区偏移转换为 UTC 秒数;拒绝 IV=1,SU=1 时额外扣除一小时。 | +| `IEC60870_Time_CP56_To_Local` | 把 CP56 解析值转换为固定 UTC+8 标准时间日历,统一处理 IV、SU、源时区偏移,并保留毫秒、重算星期。 | +| `IEC60870_Time_To_CP56` | 把合法日历及 IV/SU/星期使用标志编码为 7 字节 CP56Time2a;按日期计算所需星期,保留 IV/SU,不自动转换时区。 | +| `IEC60870_Time_From_CP56` | 校验并解码前 7 字节 CP56Time2a,检查保留位、日期时间及非零星期,保留 IV/SU 并重算星期;解码成功不等于时间可用于校时。 | + + +
+完整函数原型(13 个;参数和返回类型见声明) + +```c +#include "IEC60870_Time.h" + +int IEC60870_Time_To_CP16(const IEC60870_CP16Time2a_t *value, uint8_t *out, size_t capacity); + +int IEC60870_Time_From_CP16(const uint8_t *in, size_t length, IEC60870_CP16Time2a_t *value); + +int IEC60870_Time_To_CP24(const IEC60870_CP24Time2a_t *value, uint8_t *out, size_t capacity); + +int IEC60870_Time_From_CP24(const uint8_t *in, size_t length, IEC60870_CP24Time2a_t *value); + +int IEC60870_Time_Is_Valid(const IEC60870_WallTime_t *time); + +int IEC60870_Time_Get_Weekday(uint16_t year, uint8_t month, uint8_t day, uint8_t *weekday); + +int IEC60870_Time_To_Seconds(const IEC60870_WallTime_t *time, uint32_t *seconds); + +int IEC60870_Time_From_Seconds(uint32_t seconds, uint16_t ms, IEC60870_WallTime_t *time); + +int IEC60870_Time_Seconds_To_Local(uint32_t utc_seconds, uint16_t ms, + IEC60870_WallTime_t *local_time); + +int IEC60870_Time_CP56_To_Seconds(const IEC60870_CP56Time2a_t *value, + int16_t standard_utc_offset_minutes, uint32_t *seconds); + +int IEC60870_Time_CP56_To_Local(const IEC60870_CP56Time2a_t *value, + int16_t source_standard_utc_offset_minutes, + IEC60870_WallTime_t *local_time); + +int IEC60870_Time_To_CP56(const IEC60870_CP56Time2a_t *value, uint8_t *out, size_t capacity); + +int IEC60870_Time_From_CP56(const uint8_t *in, size_t length, IEC60870_CP56Time2a_t *value); +``` + +
+ + + +## 16. IEC60870_Asdu.c + +实现:[IEC60870_Asdu.c](../../common/IEC60870_Asdu.c);声明:[IEC60870_Asdu.h](../../common/IEC60870_Asdu.h)。 + +通用ASDU容器使用调用方存储和显式非空参数,参数在使用期间必须有效且不变。所有结果须显式比较`IEC60870_ASDU_OK`;FULL为容量或数量已满,ERROR为参数/格式错误,UNSUPPORTED为未知类型。单次ASDU接口失败不修改输出;Create产生的空构造容器不能导出,Create_From_Buffer借用对象沿用公共头最低长度校验。 + +调用约定:Create 产生空构造容器,Add 成功才固定 Type ID;FULL 保持当前 ASDU,先提交当前非空内容,另起容器重试未加入对象。若空容器也放不下对象,须检查容量和对象大小,不能循环提交空 ASDU。SQ=1 要求连续 IOA,跨类型、地址溢出或不支持 SQ 时返回 ERROR。Create_From_Buffer 的借用视图只能修改公共头,不能 Add。 + +Parse 验证精确长度及全部对象;Get_Element 的 index 从 0 开始,输出大小至少为对应具体结构,但调用方仍须按 Type ID 选择类型。Get_Data 不执行完整对象验证。除 Build_Response 明确支持输入/输出字节区重叠外,控制结构、输出参数和字节区不应相互重叠。 + +| 函数 API 接口名 | 功能作用 | +|---|---| +| `IEC60870_Asdu_Create` | 创建空容器,固定参数、公共头及SQ;首个成功添加的对象确定Type ID。头长2+COT+CA,有效容量取实际容量与配置上限的较小值。 | +| `IEC60870_Asdu_Create_From_Buffer` | 借用可写原文建立ASDU对象,只校验公共头和最低长度;允许未知类型,不允许追加对象,不复制或分配。 | +| `IEC60870_Asdu_Set_COT` | 原地设置COT低6位,范围0..63,保留P/N、TEST和其他原文。 | +| `IEC60870_Asdu_Set_Negative` | 原地设置P/N位,参数0或1,保留COT、TEST和其他原文。 | +| `IEC60870_Asdu_Set_CA` | 原地设置公共地址,按COT宽度定位、CA宽度写入;1字节允许0..255,2字节允许0..65535,失败不修改原文。 | +| `IEC60870_Asdu_Add_InformationObject` | 通过对象函数表编码,检查同类型和SQ地址连续性,成功后更新VSQ;按类型独立检查数量和SQ,文件段按实际数据长度检查容量。 | +| `IEC60870_Asdu_Get_Data` | 返回非空构造容器或借用原文对象的ASDU原文及当前长度,不含APCI,不复制或发送。 | +| `IEC60870_Asdu_Parse` | 统一检查67种类型的精确布局、变长文件段LOS与所有对象内容,返回借用参数/原文的公共头及对象区视图。 | +| `IEC60870_Asdu_Get_Element` | 按Type ID解析指定索引,向对应类型的调用方存储写入按值对象;检查输出尺寸,调用方仍须选择正确结构类型。 | +| `IEC60870_Asdu_Build_Response` | 复制请求并只替换COT/P/N,支持7/9/10/44–47及未知类型回显,允许字节区重叠。 | + +Set_CA可用于空/非空构造容器和借用可写原文,只修改CA字节,不改变COT/P/N/TEST、存在时的OA、长度、对象数量或载荷。无效容器或1字节CA溢出时返回ERROR且保持原文。它只检查编码范围;0或65535能否用于某项业务由对应处理器判断。Build_Response仍只修改COT/P/N,不自动处理广播地址。 + +应用从 info 读取 COT/CA/OA,从具体对象读取 IOA 和值。地址匹配、限定词适用性、校时和选择/执行均由业务决定。具体类型见 [信息对象层](#file-17),业务调用顺序见 [事件与 ASDU 流程](IEC104_完整调用流程.md#flow-6)。 + + +
+完整函数原型(10 个;参数和返回类型见声明) + +```c +#include "IEC60870_Asdu.h" + +IEC60870_AsduResult_t IEC60870_Asdu_Create(IEC60870_Asdu_t *self, + const IEC60870_AppLayerParameters_t *parameters, + uint8_t *buffer, uint16_t capacity, const IEC60870_AsduHeader_t *header, int is_sequence); + +IEC60870_AsduResult_t IEC60870_Asdu_Create_From_Buffer(IEC60870_Asdu_t *self, + const IEC60870_AppLayerParameters_t *parameters, uint8_t *buffer, uint16_t length); + +IEC60870_AsduResult_t IEC60870_Asdu_Set_COT(IEC60870_Asdu_t *self, int cause); + +IEC60870_AsduResult_t IEC60870_Asdu_Set_Negative(IEC60870_Asdu_t *self, int is_negative); + +IEC60870_AsduResult_t IEC60870_Asdu_Set_CA(IEC60870_Asdu_t *self, uint16_t common_address); + +IEC60870_AsduResult_t IEC60870_Asdu_Add_InformationObject(IEC60870_Asdu_t *self, + const IEC60870_InformationObject_t *object); + +IEC60870_AsduResult_t IEC60870_Asdu_Get_Data(const IEC60870_Asdu_t *self, + const uint8_t **asdu, uint16_t *length); + +IEC60870_AsduResult_t IEC60870_Asdu_Parse(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *asdu, uint16_t length, IEC60870_AsduInfo_t *info); + +IEC60870_AsduResult_t IEC60870_Asdu_Get_Element(const IEC60870_AsduInfo_t *info, + uint8_t index, void *object, size_t object_size); + +IEC60870_AsduResult_t IEC60870_Asdu_Build_Response(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *request, uint16_t length, + int cause, int is_negative, uint8_t *out, uint16_t capacity, uint16_t *out_length); +``` + +
+ + + +## 17. IEC60870_InformationObjects.c + +实现:[IEC60870_InformationObjects.c](../../common/IEC60870_InformationObjects.c);公开声明:[IEC60870_InformationObjects.h](../../common/IEC60870_InformationObjects.h);内部协作声明:[IEC60870_InformationObjects_Internal.h](../../common/IEC60870_InformationObjects_Internal.h)。 + +每种类型独立结构与Create/Get_From_Buffer,编码通过公共基类绑定的只读函数表。Create不分配内存,Get_From_Buffer只接收不含IOA的精确载荷和已解析IOA;结果按值保存,不借用输入。带CP24/CP56的普通监视与控制对象复用基础值处理;保护类型保留QDP及CP16。文件段对象有255字节独立存储,Create和解析复制段数据;ASDU按配置计算实际可发送长度。所有失败保持输出不变。 + +调用约定:先用具体 Create/Get_From_Buffer 初始化类型身份和只读函数表,再向 Add/Encode 传对象的公共基类地址。普通对象使用 `&object.object`,带 base 的类型使用 `&object.base.object`;保留完整具体对象,不把 base 截成独立对象后编码。各具体构造/解析返回 ASDU_OK/FULL/ERROR/UNSUPPORTED,均按枚举判断。 + +| 已实现 Type ID | 类型组 | ASDU 数量与 SQ 约束 | +| --- | --- | --- | +| 1~21、30~40 | 监视、累计量、保护信息 | 1~127 个,SQ=0/1 | +| 45~51、58~64 | 命令、设定值及带时标命令 | 1 个,SQ=0 | +| 70、100~107 | 初始化结束、总召、读、校时、测试、复位、延时 | 1 个,SQ=0 | +| 110~113 | 参数与参数激活 | 1~127 个,SQ=0 | +| 120~125、127 | 文件操作、文件段、归档查询 | 1 个,SQ=0 | +| 126 | 文件目录 | 1~127 个,SQ=0/1 | + + + +共 67 种类型,每种各有一组 Create/Get_From_Buffer;再加文件段容量查询、公共 Get_Type/Encode 和两个内部函数,共 139 个。编码支持某种 Type ID 不代表已经实现该类业务过程;字段与范围约束以对应具体结构及函数声明注释为准。 + +| 函数 API 接口名 | 功能作用 | +|---|---| +| `IEC60870_SinglePointInformation_Create` | 在调用方存储中校验并构造Type 1(单点信息),绑定类型及编码函数表。 | +| `IEC60870_SinglePointInformation_Get_From_Buffer` | 解析Type 1的1字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_DoublePointInformation_Create` | 在调用方存储中校验并构造Type 3(双点信息),绑定类型及编码函数表。 | +| `IEC60870_DoublePointInformation_Get_From_Buffer` | 解析Type 3的1字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_MeasuredValueNormalized_Create` | 在调用方存储中校验并构造Type 9(归一化测量值(原始16位定点值)),绑定类型及编码函数表。 | +| `IEC60870_MeasuredValueNormalized_Get_From_Buffer` | 解析Type 9的3字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_MeasuredValueScaled_Create` | 在调用方存储中校验并构造Type 11(标度化测量值),绑定类型及编码函数表。 | +| `IEC60870_MeasuredValueScaled_Get_From_Buffer` | 解析Type 11的3字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_MeasuredValueShort_Create` | 在调用方存储中校验并构造Type 13(短浮点测量值),绑定类型及编码函数表。 | +| `IEC60870_MeasuredValueShort_Get_From_Buffer` | 解析Type 13的5字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_IntegratedTotals_Create` | 在调用方存储中校验并构造Type 15(累计量),绑定类型及编码函数表。 | +| `IEC60870_IntegratedTotals_Get_From_Buffer` | 解析Type 15的5字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SinglePointWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 30(带时标单点信息),绑定类型及编码函数表。 | +| `IEC60870_SinglePointWithCP56Time2a_Get_From_Buffer` | 解析Type 30的8字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_DoublePointWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 31(带时标双点信息),绑定类型及编码函数表。 | +| `IEC60870_DoublePointWithCP56Time2a_Get_From_Buffer` | 解析Type 31的8字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_MeasuredValueNormalizedWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 34(带时标归一化测量值),绑定类型及编码函数表。 | +| `IEC60870_MeasuredValueNormalizedWithCP56Time2a_Get_From_Buffer` | 解析Type 34的10字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_MeasuredValueScaledWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 35(带时标标度化测量值),绑定类型及编码函数表。 | +| `IEC60870_MeasuredValueScaledWithCP56Time2a_Get_From_Buffer` | 解析Type 35的10字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_MeasuredValueShortWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 36(带时标短浮点测量值),绑定类型及编码函数表。 | +| `IEC60870_MeasuredValueShortWithCP56Time2a_Get_From_Buffer` | 解析Type 36的12字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_IntegratedTotalsWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 37(带时标累计量),绑定类型及编码函数表。 | +| `IEC60870_IntegratedTotalsWithCP56Time2a_Get_From_Buffer` | 解析Type 37的12字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SingleCommand_Create` | 在调用方存储中校验并构造Type 45(单命令),绑定类型及编码函数表。 | +| `IEC60870_SingleCommand_Get_From_Buffer` | 解析Type 45的1字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_DoubleCommand_Create` | 在调用方存储中校验并构造Type 46(双命令),绑定类型及编码函数表。 | +| `IEC60870_DoubleCommand_Get_From_Buffer` | 解析Type 46的1字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_StepCommand_Create` | 在调用方存储中校验并构造Type 47(步调节命令),绑定类型及编码函数表。 | +| `IEC60870_StepCommand_Get_From_Buffer` | 解析Type 47的1字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SetpointCommandNormalized_Create` | 在调用方存储中校验并构造Type 48(归一化设定值命令(原始16位定点值)),绑定类型及编码函数表。 | +| `IEC60870_SetpointCommandNormalized_Get_From_Buffer` | 解析Type 48的3字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SetpointCommandScaled_Create` | 在调用方存储中校验并构造Type 49(标度化设定值命令),绑定类型及编码函数表。 | +| `IEC60870_SetpointCommandScaled_Get_From_Buffer` | 解析Type 49的3字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SetpointCommandShort_Create` | 在调用方存储中校验并构造Type 50(短浮点设定值命令),绑定类型及编码函数表。 | +| `IEC60870_SetpointCommandShort_Get_From_Buffer` | 解析Type 50的5字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SingleCommandWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 58(带时标单命令),绑定类型及编码函数表。 | +| `IEC60870_SingleCommandWithCP56Time2a_Get_From_Buffer` | 解析Type 58的8字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_DoubleCommandWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 59(带时标双命令),绑定类型及编码函数表。 | +| `IEC60870_DoubleCommandWithCP56Time2a_Get_From_Buffer` | 解析Type 59的8字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_StepCommandWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 60(带时标步调节命令),绑定类型及编码函数表。 | +| `IEC60870_StepCommandWithCP56Time2a_Get_From_Buffer` | 解析Type 60的8字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SetpointCommandNormalizedWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 61(带时标归一化设定值命令),绑定类型及编码函数表。 | +| `IEC60870_SetpointCommandNormalizedWithCP56Time2a_Get_From_Buffer` | 解析Type 61的10字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SetpointCommandScaledWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 62(带时标标度化设定值命令),绑定类型及编码函数表。 | +| `IEC60870_SetpointCommandScaledWithCP56Time2a_Get_From_Buffer` | 解析Type 62的10字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_SetpointCommandShortWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 63(带时标短浮点设定值命令),绑定类型及编码函数表。 | +| `IEC60870_SetpointCommandShortWithCP56Time2a_Get_From_Buffer` | 解析Type 63的12字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_InterrogationCommand_Create` | 在调用方存储中校验并构造Type 100(总召唤命令),绑定类型及编码函数表。 | +| `IEC60870_InterrogationCommand_Get_From_Buffer` | 解析Type 100的1字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_CounterInterrogationCommand_Create` | 在调用方存储中校验并构造Type 101(电能脉冲召唤命令),绑定类型及编码函数表。 | +| `IEC60870_CounterInterrogationCommand_Get_From_Buffer` | 解析Type 101的1字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_ReadCommand_Create` | 在调用方存储中校验并构造Type 102(读命令),绑定类型及编码函数表。 | +| `IEC60870_ReadCommand_Get_From_Buffer` | 解析Type 102的0字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_ClockSynchronizationCommand_Create` | 在调用方存储中校验并构造Type 103(时钟同步命令),绑定类型及编码函数表。 | +| `IEC60870_ClockSynchronizationCommand_Get_From_Buffer` | 解析Type 103的7字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_ResetProcessCommand_Create` | 在调用方存储中校验并构造Type 105(复位进程命令),绑定类型及编码函数表。 | +| `IEC60870_ResetProcessCommand_Get_From_Buffer` | 解析Type 105的1字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_TestCommandWithCP56Time2a_Create` | 在调用方存储中校验并构造Type 107(带时标测试命令),绑定类型及编码函数表。 | +| `IEC60870_TestCommandWithCP56Time2a_Get_From_Buffer` | 解析Type 107的9字节载荷,输出该具体类型;IOA单独传入。 | +| `IEC60870_StepPositionInformation_Create` | 构造Type 5(步位置信息),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_StepPositionInformation_Get_From_Buffer` | 解析Type 5的2字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_BitString32_Create` | 构造Type 7(32位比特串监视),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_BitString32_Get_From_Buffer` | 解析Type 7的5字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_SinglePointWithCP24Time2a_Create` | 构造Type 2(带CP24单点信息),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_SinglePointWithCP24Time2a_Get_From_Buffer` | 解析Type 2的4字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_DoublePointWithCP24Time2a_Create` | 构造Type 4(带CP24双点信息),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_DoublePointWithCP24Time2a_Get_From_Buffer` | 解析Type 4的4字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_StepPositionWithCP24Time2a_Create` | 构造Type 6(带CP24步位置信息),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_StepPositionWithCP24Time2a_Get_From_Buffer` | 解析Type 6的5字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_Bitstring32WithCP24Time2a_Create` | 构造Type 8(带CP24比特串监视),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_Bitstring32WithCP24Time2a_Get_From_Buffer` | 解析Type 8的8字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_MeasuredValueNormalizedWithCP24Time2a_Create` | 构造Type 10(带CP24归一化测量值),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_MeasuredValueNormalizedWithCP24Time2a_Get_From_Buffer` | 解析Type 10的6字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_MeasuredValueScaledWithCP24Time2a_Create` | 构造Type 12(带CP24标度化测量值),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_MeasuredValueScaledWithCP24Time2a_Get_From_Buffer` | 解析Type 12的6字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_MeasuredValueShortWithCP24Time2a_Create` | 构造Type 14(带CP24短浮点测量值),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_MeasuredValueShortWithCP24Time2a_Get_From_Buffer` | 解析Type 14的8字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_IntegratedTotalsWithCP24Time2a_Create` | 构造Type 16(带CP24累计量),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_IntegratedTotalsWithCP24Time2a_Get_From_Buffer` | 解析Type 16的8字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_StepPositionWithCP56Time2a_Create` | 构造Type 32(带CP56步位置信息),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_StepPositionWithCP56Time2a_Get_From_Buffer` | 解析Type 32的9字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_Bitstring32WithCP56Time2a_Create` | 构造Type 33(带CP56比特串监视),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_Bitstring32WithCP56Time2a_Get_From_Buffer` | 解析Type 33的12字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_EventOfProtectionEquipment_Create` | 构造Type 17(带CP24保护事件),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_EventOfProtectionEquipment_Get_From_Buffer` | 解析Type 17的6字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_PackedStartEventsOfProtectionEquipment_Create` | 构造Type 18(带CP24成组保护启动事件),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_PackedStartEventsOfProtectionEquipment_Get_From_Buffer` | 解析Type 18的7字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_PackedOutputCircuitInfo_Create` | 构造Type 19(带CP24保护输出电路信息),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_PackedOutputCircuitInfo_Get_From_Buffer` | 解析Type 19的7字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Create` | 构造Type 38(带CP56保护事件),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer` | 解析Type 38的10字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Create` | 构造Type 39(带CP56成组保护启动事件),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer` | 解析Type 39的11字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Create` | 构造Type 40(带CP56保护输出电路信息),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Get_From_Buffer` | 解析Type 40的11字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_PackedSinglePointWithSCD_Create` | 构造Type 20(带变位检测的成组单点),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_PackedSinglePointWithSCD_Get_From_Buffer` | 解析Type 20的5字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_MeasuredValueNormalizedWithoutQuality_Create` | 构造Type 21(无品质归一化测量值),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_MeasuredValueNormalizedWithoutQuality_Get_From_Buffer` | 解析Type 21的2字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_Bitstring32Command_Create` | 构造Type 51(32位比特串命令),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_Bitstring32Command_Get_From_Buffer` | 解析Type 51的4字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_Bitstring32CommandWithCP56Time2a_Create` | 构造Type 64(带CP56比特串命令),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_Bitstring32CommandWithCP56Time2a_Get_From_Buffer` | 解析Type 64的11字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_EndOfInitialization_Create` | 构造Type 70(初始化结束),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_EndOfInitialization_Get_From_Buffer` | 解析Type 70的1字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_TestCommand_Create` | 构造Type 104(无时标测试命令),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_TestCommand_Get_From_Buffer` | 解析Type 104的2字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_DelayAcquisitionCommand_Create` | 构造Type 106(延时获得命令),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_DelayAcquisitionCommand_Get_From_Buffer` | 解析Type 106的2字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_ParameterNormalizedValue_Create` | 构造Type 110(归一化测量值参数),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_ParameterNormalizedValue_Get_From_Buffer` | 解析Type 110的3字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_ParameterScaledValue_Create` | 构造Type 111(标度化测量值参数),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_ParameterScaledValue_Get_From_Buffer` | 解析Type 111的3字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_ParameterFloatValue_Create` | 构造Type 112(短浮点测量值参数),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_ParameterFloatValue_Get_From_Buffer` | 解析Type 112的5字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_ParameterActivation_Create` | 构造Type 113(参数激活),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_ParameterActivation_Get_From_Buffer` | 解析Type 113的1字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_FileReady_Create` | 构造Type 120(文件准备就绪),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_FileReady_Get_From_Buffer` | 解析Type 120的6字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_SectionReady_Create` | 构造Type 121(节准备就绪),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_SectionReady_Get_From_Buffer` | 解析Type 121的7字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_FileCallOrSelect_Create` | 构造Type 122(文件/节召唤或选择),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_FileCallOrSelect_Get_From_Buffer` | 解析Type 122的4字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_FileACK_Create` | 构造Type 124(文件/节确认),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_FileACK_Get_From_Buffer` | 解析Type 124的4字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_FileLastSegmentOrSection_Create` | 构造Type 123(最后文件段或节),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_FileLastSegmentOrSection_Get_From_Buffer` | 解析Type 123的5字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_FileSegment_Create` | 构造Type 125(文件段),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_FileSegment_Get_From_Buffer` | 解析Type 125的4+LOS变长载荷,IOA另传,结果按值保存。 | +| `IEC60870_FileDirectory_Create` | 构造Type 126(文件目录),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_FileDirectory_Get_From_Buffer` | 解析Type 126的13字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_QueryLog_Create` | 构造Type 127(归档查询),使用调用方存储并校验字段;失败保留输出。 | +| `IEC60870_QueryLog_Get_From_Buffer` | 解析Type 127的16字节载荷,IOA另传,结果按值保存。 | +| `IEC60870_FileSegment_Get_Max_Data_Size` | 计算配置下可用的文件段数据容量;默认104为236,无法容纳空段或参数非法返回-1。 | +| `IEC60870_InformationObject_Get_Descriptor` | **内部协作**:按Type ID取得只读尺寸、数量约束及编码函数表,未知类型返回NULL。 | +| `IEC60870_InformationObject_Get_Type` | 读取已构造/解析对象的Type ID;类型与函数表不匹配或NULL返回0。 | +| `IEC60870_InformationObject_Encode` | 按参数校验IOA,经对象函数表编码,按is_sequence决定是否省略IOA;容量不足返回FULL,失败保持缓冲和长度。 | +| `IEC60870_InformationObject_Decode` | **内部协作**:按Type ID分派具体Get_From_Buffer,验证输出尺寸;输出NULL表示仅验证载荷。 | + + +
+完整函数原型(139 个;参数和返回类型见声明) + +```c +#include "IEC60870_InformationObjects.h" +#include "IEC60870_InformationObjects_Internal.h" + +IEC60870_AsduResult_t IEC60870_SinglePointInformation_Create(IEC60870_SinglePointInformation_t *self, + uint32_t ioa, int value, int quality); + +IEC60870_AsduResult_t IEC60870_SinglePointInformation_Get_From_Buffer(IEC60870_SinglePointInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_DoublePointInformation_Create(IEC60870_DoublePointInformation_t *self, + uint32_t ioa, int value, int quality); + +IEC60870_AsduResult_t IEC60870_DoublePointInformation_Get_From_Buffer(IEC60870_DoublePointInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalized_Create(IEC60870_MeasuredValueNormalized_t *self, + uint32_t ioa, int16_t value, int quality); + +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalized_Get_From_Buffer(IEC60870_MeasuredValueNormalized_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueScaled_Create(IEC60870_MeasuredValueScaled_t *self, + uint32_t ioa, int16_t value, int quality); + +IEC60870_AsduResult_t IEC60870_MeasuredValueScaled_Get_From_Buffer(IEC60870_MeasuredValueScaled_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueShort_Create(IEC60870_MeasuredValueShort_t *self, + uint32_t ioa, float value, int quality); + +IEC60870_AsduResult_t IEC60870_MeasuredValueShort_Get_From_Buffer(IEC60870_MeasuredValueShort_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_IntegratedTotals_Create(IEC60870_IntegratedTotals_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value); + +IEC60870_AsduResult_t IEC60870_IntegratedTotals_Get_From_Buffer(IEC60870_IntegratedTotals_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SinglePointWithCP56Time2a_Create(IEC60870_SinglePointWithCP56Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_SinglePointWithCP56Time2a_Get_From_Buffer(IEC60870_SinglePointWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_DoublePointWithCP56Time2a_Create(IEC60870_DoublePointWithCP56Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_DoublePointWithCP56Time2a_Get_From_Buffer(IEC60870_DoublePointWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP56Time2a_Create(IEC60870_MeasuredValueNormalizedWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP56Time2a_Create(IEC60870_MeasuredValueScaledWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueScaledWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP56Time2a_Create(IEC60870_MeasuredValueShortWithCP56Time2a_t *self, + uint32_t ioa, float value, int quality, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP56Time2a_Get_From_Buffer(IEC60870_MeasuredValueShortWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP56Time2a_Create(IEC60870_IntegratedTotalsWithCP56Time2a_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP56Time2a_Get_From_Buffer(IEC60870_IntegratedTotalsWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SingleCommand_Create(IEC60870_SingleCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier); + +IEC60870_AsduResult_t IEC60870_SingleCommand_Get_From_Buffer(IEC60870_SingleCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_DoubleCommand_Create(IEC60870_DoubleCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier); + +IEC60870_AsduResult_t IEC60870_DoubleCommand_Get_From_Buffer(IEC60870_DoubleCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_StepCommand_Create(IEC60870_StepCommand_t *self, + uint32_t ioa, int state, int is_select, int qualifier); + +IEC60870_AsduResult_t IEC60870_StepCommand_Get_From_Buffer(IEC60870_StepCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalized_Create(IEC60870_SetpointCommandNormalized_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier); + +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalized_Get_From_Buffer(IEC60870_SetpointCommandNormalized_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SetpointCommandScaled_Create(IEC60870_SetpointCommandScaled_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier); + +IEC60870_AsduResult_t IEC60870_SetpointCommandScaled_Get_From_Buffer(IEC60870_SetpointCommandScaled_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SetpointCommandShort_Create(IEC60870_SetpointCommandShort_t *self, + uint32_t ioa, float value, int is_select, int qualifier); + +IEC60870_AsduResult_t IEC60870_SetpointCommandShort_Get_From_Buffer(IEC60870_SetpointCommandShort_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SingleCommandWithCP56Time2a_Create(IEC60870_SingleCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_SingleCommandWithCP56Time2a_Get_From_Buffer(IEC60870_SingleCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_DoubleCommandWithCP56Time2a_Create(IEC60870_DoubleCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_DoubleCommandWithCP56Time2a_Get_From_Buffer(IEC60870_DoubleCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_StepCommandWithCP56Time2a_Create(IEC60870_StepCommandWithCP56Time2a_t *self, + uint32_t ioa, int state, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_StepCommandWithCP56Time2a_Get_From_Buffer(IEC60870_StepCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalizedWithCP56Time2a_Create(IEC60870_SetpointCommandNormalizedWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_SetpointCommandNormalizedWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandNormalizedWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SetpointCommandScaledWithCP56Time2a_Create(IEC60870_SetpointCommandScaledWithCP56Time2a_t *self, + uint32_t ioa, int16_t value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_SetpointCommandScaledWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandScaledWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SetpointCommandShortWithCP56Time2a_Create(IEC60870_SetpointCommandShortWithCP56Time2a_t *self, + uint32_t ioa, float value, int is_select, int qualifier, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_SetpointCommandShortWithCP56Time2a_Get_From_Buffer(IEC60870_SetpointCommandShortWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_InterrogationCommand_Create(IEC60870_InterrogationCommand_t *self, + uint32_t ioa, int qualifier); + +IEC60870_AsduResult_t IEC60870_InterrogationCommand_Get_From_Buffer(IEC60870_InterrogationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_CounterInterrogationCommand_Create(IEC60870_CounterInterrogationCommand_t *self, + uint32_t ioa, int qualifier, int freeze); + +IEC60870_AsduResult_t IEC60870_CounterInterrogationCommand_Get_From_Buffer(IEC60870_CounterInterrogationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_ReadCommand_Create(IEC60870_ReadCommand_t *self, + uint32_t ioa); + +IEC60870_AsduResult_t IEC60870_ReadCommand_Get_From_Buffer(IEC60870_ReadCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_ClockSynchronizationCommand_Create(IEC60870_ClockSynchronizationCommand_t *self, + uint32_t ioa, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_ClockSynchronizationCommand_Get_From_Buffer(IEC60870_ClockSynchronizationCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_ResetProcessCommand_Create(IEC60870_ResetProcessCommand_t *self, + uint32_t ioa, int qualifier); + +IEC60870_AsduResult_t IEC60870_ResetProcessCommand_Get_From_Buffer(IEC60870_ResetProcessCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_TestCommandWithCP56Time2a_Create(IEC60870_TestCommandWithCP56Time2a_t *self, + uint32_t ioa, uint16_t sequence, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_TestCommandWithCP56Time2a_Get_From_Buffer(IEC60870_TestCommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_StepPositionInformation_Create(IEC60870_StepPositionInformation_t *self, + uint32_t ioa, int value, int is_transient, int quality); + +IEC60870_AsduResult_t IEC60870_StepPositionInformation_Get_From_Buffer(IEC60870_StepPositionInformation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_BitString32_Create(IEC60870_BitString32_t *self, + uint32_t ioa, uint32_t value, int quality); + +IEC60870_AsduResult_t IEC60870_BitString32_Get_From_Buffer(IEC60870_BitString32_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SinglePointWithCP24Time2a_Create(IEC60870_SinglePointWithCP24Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_SinglePointWithCP24Time2a_Get_From_Buffer(IEC60870_SinglePointWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_DoublePointWithCP24Time2a_Create(IEC60870_DoublePointWithCP24Time2a_t *self, + uint32_t ioa, int value, int quality, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_DoublePointWithCP24Time2a_Get_From_Buffer(IEC60870_DoublePointWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_StepPositionWithCP24Time2a_Create(IEC60870_StepPositionWithCP24Time2a_t *self, + uint32_t ioa, int value, int is_transient, int quality, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_StepPositionWithCP24Time2a_Get_From_Buffer(IEC60870_StepPositionWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP24Time2a_Create(IEC60870_Bitstring32WithCP24Time2a_t *self, + uint32_t ioa, uint32_t value, int quality, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP24Time2a_Get_From_Buffer(IEC60870_Bitstring32WithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP24Time2a_Create(IEC60870_MeasuredValueNormalizedWithCP24Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP24Time2a_Create(IEC60870_MeasuredValueScaledWithCP24Time2a_t *self, + uint32_t ioa, int16_t value, int quality, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_MeasuredValueScaledWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueScaledWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP24Time2a_Create(IEC60870_MeasuredValueShortWithCP24Time2a_t *self, + uint32_t ioa, float value, int quality, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_MeasuredValueShortWithCP24Time2a_Get_From_Buffer(IEC60870_MeasuredValueShortWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP24Time2a_Create(IEC60870_IntegratedTotalsWithCP24Time2a_t *self, + uint32_t ioa, const IEC60870_BinaryCounterReading_t *value, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_IntegratedTotalsWithCP24Time2a_Get_From_Buffer(IEC60870_IntegratedTotalsWithCP24Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_StepPositionWithCP56Time2a_Create(IEC60870_StepPositionWithCP56Time2a_t *self, + uint32_t ioa, int value, int is_transient, int quality, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_StepPositionWithCP56Time2a_Get_From_Buffer(IEC60870_StepPositionWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP56Time2a_Create(IEC60870_Bitstring32WithCP56Time2a_t *self, + uint32_t ioa, uint32_t value, int quality, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_Bitstring32WithCP56Time2a_Get_From_Buffer(IEC60870_Bitstring32WithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipment_Create(IEC60870_EventOfProtectionEquipment_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipment_Get_From_Buffer(IEC60870_EventOfProtectionEquipment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipment_Create(IEC60870_PackedStartEventsOfProtectionEquipment_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipment_Get_From_Buffer(IEC60870_PackedStartEventsOfProtectionEquipment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfo_Create(IEC60870_PackedOutputCircuitInfo_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP24Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfo_Get_From_Buffer(IEC60870_PackedOutputCircuitInfo_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Create(IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer(IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Create(IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer(IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Create(IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t *self, + uint32_t ioa, int event, int quality, const IEC60870_CP16Time2a_t *elapsed_time, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Get_From_Buffer(IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_PackedSinglePointWithSCD_Create(IEC60870_PackedSinglePointWithSCD_t *self, + uint32_t ioa, uint16_t status, uint16_t change, int quality); + +IEC60870_AsduResult_t IEC60870_PackedSinglePointWithSCD_Get_From_Buffer(IEC60870_PackedSinglePointWithSCD_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithoutQuality_Create(IEC60870_MeasuredValueNormalizedWithoutQuality_t *self, + uint32_t ioa, int16_t value); + +IEC60870_AsduResult_t IEC60870_MeasuredValueNormalizedWithoutQuality_Get_From_Buffer(IEC60870_MeasuredValueNormalizedWithoutQuality_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_Bitstring32Command_Create(IEC60870_Bitstring32Command_t *self, + uint32_t ioa, uint32_t value); + +IEC60870_AsduResult_t IEC60870_Bitstring32Command_Get_From_Buffer(IEC60870_Bitstring32Command_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_Bitstring32CommandWithCP56Time2a_Create(IEC60870_Bitstring32CommandWithCP56Time2a_t *self, + uint32_t ioa, uint32_t value, const IEC60870_CP56Time2a_t *time); + +IEC60870_AsduResult_t IEC60870_Bitstring32CommandWithCP56Time2a_Get_From_Buffer(IEC60870_Bitstring32CommandWithCP56Time2a_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_EndOfInitialization_Create(IEC60870_EndOfInitialization_t *self, + uint32_t ioa, int cause); + +IEC60870_AsduResult_t IEC60870_EndOfInitialization_Get_From_Buffer(IEC60870_EndOfInitialization_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_TestCommand_Create(IEC60870_TestCommand_t *self, + uint32_t ioa); + +IEC60870_AsduResult_t IEC60870_TestCommand_Get_From_Buffer(IEC60870_TestCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_DelayAcquisitionCommand_Create(IEC60870_DelayAcquisitionCommand_t *self, + uint32_t ioa, const IEC60870_CP16Time2a_t *delay); + +IEC60870_AsduResult_t IEC60870_DelayAcquisitionCommand_Get_From_Buffer(IEC60870_DelayAcquisitionCommand_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_ParameterNormalizedValue_Create(IEC60870_ParameterNormalizedValue_t *self, + uint32_t ioa, int16_t value, int qualifier); + +IEC60870_AsduResult_t IEC60870_ParameterNormalizedValue_Get_From_Buffer(IEC60870_ParameterNormalizedValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_ParameterScaledValue_Create(IEC60870_ParameterScaledValue_t *self, + uint32_t ioa, int16_t value, int qualifier); + +IEC60870_AsduResult_t IEC60870_ParameterScaledValue_Get_From_Buffer(IEC60870_ParameterScaledValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_ParameterFloatValue_Create(IEC60870_ParameterFloatValue_t *self, + uint32_t ioa, float value, int qualifier); + +IEC60870_AsduResult_t IEC60870_ParameterFloatValue_Get_From_Buffer(IEC60870_ParameterFloatValue_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_ParameterActivation_Create(IEC60870_ParameterActivation_t *self, + uint32_t ioa, int qualifier); + +IEC60870_AsduResult_t IEC60870_ParameterActivation_Get_From_Buffer(IEC60870_ParameterActivation_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_FileReady_Create(IEC60870_FileReady_t *self, + uint32_t ioa, uint16_t file_name, uint32_t file_length, int qualifier); + +IEC60870_AsduResult_t IEC60870_FileReady_Get_From_Buffer(IEC60870_FileReady_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_SectionReady_Create(IEC60870_SectionReady_t *self, + uint32_t ioa, uint16_t file_name, int section, uint32_t section_length, int qualifier); + +IEC60870_AsduResult_t IEC60870_SectionReady_Get_From_Buffer(IEC60870_SectionReady_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_FileCallOrSelect_Create(IEC60870_FileCallOrSelect_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier); + +IEC60870_AsduResult_t IEC60870_FileCallOrSelect_Get_From_Buffer(IEC60870_FileCallOrSelect_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_FileACK_Create(IEC60870_FileACK_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier); + +IEC60870_AsduResult_t IEC60870_FileACK_Get_From_Buffer(IEC60870_FileACK_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_FileLastSegmentOrSection_Create(IEC60870_FileLastSegmentOrSection_t *self, + uint32_t ioa, uint16_t file_name, int section, int qualifier, int checksum); + +IEC60870_AsduResult_t IEC60870_FileLastSegmentOrSection_Get_From_Buffer(IEC60870_FileLastSegmentOrSection_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_FileSegment_Create(IEC60870_FileSegment_t *self, + uint32_t ioa, uint16_t file_name, int section, const uint8_t *data, uint16_t data_length); + +IEC60870_AsduResult_t IEC60870_FileSegment_Get_From_Buffer(IEC60870_FileSegment_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_FileDirectory_Create(IEC60870_FileDirectory_t *self, + uint32_t ioa, uint16_t file_name, uint32_t file_length, int status, const IEC60870_CP56Time2a_t *creation_time); + +IEC60870_AsduResult_t IEC60870_FileDirectory_Get_From_Buffer(IEC60870_FileDirectory_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +IEC60870_AsduResult_t IEC60870_QueryLog_Create(IEC60870_QueryLog_t *self, + uint32_t ioa, uint16_t file_name, const IEC60870_CP56Time2a_t *start_time, const IEC60870_CP56Time2a_t *stop_time); + +IEC60870_AsduResult_t IEC60870_QueryLog_Get_From_Buffer(IEC60870_QueryLog_t *self, + uint32_t ioa, const uint8_t *payload, uint16_t length); + +int IEC60870_FileSegment_Get_Max_Data_Size(const IEC60870_AppLayerParameters_t *parameters); + +/* 内部协作:仅供库实现。 */ +const IEC60870_InformationObjectVTable * IEC60870_InformationObject_Get_Descriptor(uint8_t type_id); + +uint8_t IEC60870_InformationObject_Get_Type(const IEC60870_InformationObject_t *object); + +IEC60870_AsduResult_t IEC60870_InformationObject_Encode( + const IEC60870_InformationObject_t *object, const IEC60870_AppLayerParameters_t *parameters, + int is_sequence, uint8_t *buffer, uint16_t capacity, uint16_t *length); + +/* 内部协作:仅供库实现。 */ +IEC60870_AsduResult_t IEC60870_InformationObject_Decode(uint8_t type_id, uint32_t ioa, + const uint8_t *payload, uint16_t length, void *object, size_t object_size); +``` + +
+ + + +## 18. IEC60870_Slave.c + +实现:[IEC60870_Slave.c](../../common/IEC60870_Slave.c);声明:[IEC60870_Slave.h](../../common/IEC60870_Slave.h)。 + +共用连接接口通过函数指针分派,供101/104接入复用。发送返回布尔值;true表示已接手,不等于主站ACK。确认和终止先修改ASDU再提交,提交失败不恢复字段。 + +调用约定:self 必须指向接入层提供的原接口;空接口或缺少对应函数指针时发送返回 false、参数查询返回 NULL。104 直接使用 `&event.connection`,不由业务自行填充 object 或函数指针。true 前接入实现已复制或消耗 ASDU;false 的含义由接入层定义,104 使用 Slave_Get_Last_Submit_Result。共用层不保存请求、不进行重试,也不实现网络服务。 + +| 函数 API 接口名 | 功能作用 | +|---|---| +| `IEC60870_MasterConnection_Send_ASDU` | 分派普通ASDU发送,不修改公共头。 | +| `IEC60870_MasterConnection_Send_ACT_CON` | 分派激活确认,由对应协议设置COT=7及指定P/N后提交。 | +| `IEC60870_MasterConnection_Send_ACT_TERM` | 分派激活终止,由对应协议设置COT=10、P/N=0后提交。 | +| `IEC60870_MasterConnection_Get_Application_Layer_Parameters` | 返回协议接入提供的只读参数,空接口或未实现时返回NULL。 | + + +
+完整函数原型(4 个;参数和返回类型见声明) + +```c +#include "IEC60870_Slave.h" + +bool IEC60870_MasterConnection_Send_ASDU(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *asdu); + +bool IEC60870_MasterConnection_Send_ACT_CON(IEC60870_MasterConnection_t *self, + IEC60870_Asdu_t *asdu, bool negative); + +bool IEC60870_MasterConnection_Send_ACT_TERM(IEC60870_MasterConnection_t *self, + IEC60870_Asdu_t *asdu); + +const IEC60870_AppLayerParameters_t * IEC60870_MasterConnection_Get_Application_Layer_Parameters( + IEC60870_MasterConnection_t *self); +``` + +
+ + + +## 19. IEC104_Slave.c + +实现:[IEC104_Slave.c](../../104/IEC104_Slave.c);声明:[IEC104_Slave.h](../../104/IEC104_Slave.h)。 + +Slave是业务统一入口,持有静态池TCP服务。NextEvent自动安装共用应答接口;业务仅管理事件存储,不手动绑定TCP句柄或generation。需要完整事件的延迟任务用Copy_Event复制,自引用不得直接赋值搬移;只需部分请求数据时可使用下述纯值身份入口。确认/终止失败仍保留已修改的ASDU。 + +调用约定:NextEvent 的 READY 交付原通知,I_FRAME 还需检查 asdu_result;完整对象合法性再由 Asdu_Parse 验证。Copy_Event 返回 true/false,失败不修改目标;它修复自引用、保留历史提交结果,不提交报文或改变请求身份。Get_Last_Submit_Result 在首次提交前、空/无效接口时为 ERROR。 + +业务模块通过 Get_Event_Identity 校验事件的服务归属、身份一致性和有效I帧视图,再保存返回的来源身份;失败保持输出不变。它允许同一服务的历史连接通知,但拒绝其他服务、已销毁复用实例和未经Copy_Event修复的I帧副本。无需业务读取事件的 `_owner`、`_owner_serial` 或 `_generation`。 + +推进任务前使用 Check_Request:参数/状态查询失败返回 REQUEST_ERROR;Slave尚有未读通知优先返回 REQUEST_WAIT_EVENTS;读空后,来源/实例/代次不匹配或连接未激活返回 REQUEST_STALE,否则返回 REQUEST_READY。零初始化身份可表示尚未绑定。READY不预留窗口或队列容量,也不代替业务自己的阶段、撤销和超时判断;同一代次STOP后又START必须逐条处理通知,不能只根据最终READY恢复旧任务。两接口均不执行网络I/O或消费通知。 + +Close_Connection 按事件保存的原服务实例及 generation 定位:其他实例为 INVALID_ARGUMENT,当前无连接为 NOT_CONNECTED,已有新连接时旧代次为 STALE_GENERATION。Get_Application_Layer_Parameters 对有效已创建句柄即可使用,不要求主站在线;返回参数不表示应答准入。 + +需要精简延迟任务时,可只保存Get_Event_Identity取得的纯值身份及业务需要的请求字节,随后使用Send_Response/Try_Send_Response提交独立ASDU,或Close_Connection_By_Identity关闭原连接。三个身份字段保持原样,self须为原服务;发送拒绝旧实例/旧代次,关闭也不能作用于新连接。Send_Response沿普通HIGH路径,Try_Send_Response保持不排队语义;两者直接返回提交结果,不修改某个事件的最近结果。调用方仍须先处理通知并检查撤销、期限及业务状态,身份本身不表示请求仍在执行。 + +Init 失败不销毁对象;Create 后、Init 失败后和 Close 后仍可 Get_Status。Destroy 成功才丢弃句柄;运行中返回 INVALID_STATE,未读通知返回 EVENTS_PENDING。旧裸指针不能识别静态地址复用,必须由应用停止使用;旧事件则通过实例编号及原 generation 防止误发到新会话。 + + + +### 同步拒绝当前请求 + +`IEC104_Slave_Reject_Request(self, event, cause)`用于分派当前I帧时生成原文错误应答。`cause`只接受44..47,分别表示未知类型、COT、CA和IOA;接口不代替业务决定拒绝原因。仅复制当前ASDU并替换COT、置P/N=1,保留TEST、OA、CA、VSQ和全部对象字节。不修改输入事件、ASDU视图或该事件的`Get_Last_Submit_Result`,调用方直接检查返回值。原文副本沿已有普通发送入口直接接手或进入HIGH,不新增待应答队列,不做内容去重,不使用NORMAL。 + +调用限于当前`NextEvent`交付后的同步分派,并且必须在读取后续事件之前完成。原服务/会话及启停状态仍须有效;未读ACTIVATED/DEACTIVATED会使接口返回ERROR,后续仅有普通I帧或RECORD不要求先读空。`pending_state_event_count`只反映未读启停通知,不能发现应用已经消费的历史STOP/START;**禁止保存旧事件、读完后续通知后再调用本接口**。需要延迟应答时使用`Copy_Event`或保存原身份,自行处理STOP取消、撤销和期限,再经`Check_Request`及普通发送入口提交;最终READY也不能恢复已经取消的旧任务。 + +| 返回值 | 含义及调用方处理 | +|---|---| +| `IEC104_TCP_SUBMIT_OK` | 已复制接手,不代表写完或收到ACK;不得重复提交 | +| `IEC104_TCP_SUBMIT_WAIT` | HIGH剩余容量不足,本条未接手;示例丢弃新请求并累计overloads、dropped_requests,保留连接及已接手应答 | +| `IEC104_TCP_SUBMIT_ERROR` | 参数/事件/cause非法、原会话失效或存在未读启停通知;不按容量不足计数,继续处理通知及相应失效原因 | +| `IEC104_TCP_SUBMIT_FATAL` | 发送故障,可能已经写出字节;不得重试,继续排空RECORD/DISCONNECTED等收尾通知 | + +示例在总召、计数量及后续业务均未接手后显式调用此接口;Slave不会根据类型自动吞掉或拒绝NextEvent交付的请求。HIGH真正满时允许丢弃本条错误应答,因此不承诺所有负载下都有响应;已接手条目不因新兜底请求到达而被覆盖或主动断链。 + + + +### 共用站点表 + +一个Slave可以注册多个逻辑站点,各业务按CA取得同一节点及应用context。组织方式参考[iec104-python的Server站点管理](https://github.com/Fraunhofer-FIT-DIEN/iec104-python/blob/main/src/Server.cpp):按CA查找并拒绝重复注册。本实现采用调用方节点链表,不使用C堆或为每个站点复制通信资源;没有额外的固定站点数量上限,实际受1..65534地址范围与可用内存约束。 + +每个`IEC104_Station_t`须零初始化,注册成功后直到`Is_Reusable`返回true均须保持节点及context的地址和寿命稳定,不得赋值搬移、清零或直接改字段。CA及context在注册时指定;context内容、IOA点表、权限和业务状态归应用管理。同一Slave内已注册CA唯一,不同Slave可以注册相同CA,但不能共用同一个节点。查找和注册为线性扫描,适合当前少量站点规模。 + +`Get_Station`只查具体的已注册站点,0、65535及未知CA返回NULL。地址检查与业务分派分离的思路亦见[lib60870的CA许可检查](https://github.com/mz-automation/lib60870/blob/master/lib60870-C/src/iec60870/cs104/cs104_slave.c)。NextEvent不自动丢弃或应答报文:业务负责对象布局、COT、CA、权限及广播规则。总召开启`use_station_registry`后已接入按CA查找和广播遍历,其他业务可复用同一基础接口。 + +延迟业务保存节点前调用`Acquire_Station`,清理完成后调用`Release_Station`。Remove立即使节点退出CA查找、计数和普通遍历;已有引用仍可读取原CA/context,最后一个引用和快照归还后才清零节点。Remove返回OK不等于可释放存储,须检查`Is_Reusable`。同CA可立即注册另一个节点;`Get_Id/Get_Station_By_Id`以不复用的注册编号定位原节点,避免旧事务查到替代站点。引用仍不能代替原连接身份检查。 + +`StationSnapshot_Begin/Next/End`提供跨循环遍历:Begin记录注册编号上界,Next跳过已删除的未来站点,并排除后来新增或同CA重建的节点;不复制或冻结IOA和点值。快照须零初始化、地址稳定、配对End,Next返回NULL也不自动结束。任一快照活动时退役节点延迟回收,因此调用方不得提前释放这些节点。需要跨End保留Next返回值时另行Acquire。`Lock_Stations/Unlock_Stations`仍作为显式冻结集合的兼容接口,锁定期间Add/Remove返回BUSY;总召广播不使用集合锁,合法多目标请求在调用方状态区中持有各节点引用,错误回复遍历使用快照。所有操作在同一事件循环串行执行,不是线程锁。 + +Close/Init不改变站点表。Slave_Destroy要求站点全部Remove、引用及快照归还、集合锁解除,否则返回INVALID_STATE。Remove不释放应用context,也不撤回已接手报文;已选中的总召可持原引用完成。若业务要求立即停止该事务,应先显式取消并处理FINISHED;不能靠删除再注册同一CA清空旧报文。 + +`IEC104_StationResult_t`须显式比较:OK成功;INVALID_ARGUMENT参数/配对非法;DUPLICATE_CA已有注册CA;ALREADY_REGISTERED节点仍由某服务持有;NOT_FOUND节点不属于本服务或重复删除;BUSY显式集合锁阻止修改;LIMIT引用/快照/锁计数或注册编号耗尽。失败不改变注册关系;零计数Release/Unlock返回INVALID_ARGUMENT。 + +Windows x64 GCC测量(默认4任务、4业务事件):节点40字节、快照32字节、Slave40字节、总召1240字节、任务168字节、紧凑请求40字节。多目标广播另需调用方提供每站48字节状态区,13站为624字节;不扩大任务或事件队列。HIGH/NORMAL容量及覆盖策略不变,此数值不是目标板RAM实测。 + +后续业务的接入顺序为:解析请求 → 按CA查站点 → 检查该站点的业务权限 → 如需延迟则Acquire → 用context内的IOA点表处理 → 沿原连接应答 → 完成清理后Release。主动上报使用所属节点的CA构造ASDU;不同CA下相同IOA属于不同点。 + +| 函数 API 接口名 | 功能作用 | +|---|---| +| `IEC104_Slave_Add_Station` | 借用零初始化节点并按CA注册到末尾;拒绝重复CA,不分配堆。 | +| `IEC104_Slave_Remove_Station` | 立即退出路由,引用和快照归还后回收;OK后仍须检查Is_Reusable。 | +| `IEC104_Slave_Get_Station` | 按具体CA取得借用节点;未知CA或广播返回NULL。 | +| `IEC104_Slave_Get_Station_Count` | 取得当前注册数量,无效服务返回0。 | +| `IEC104_Slave_Get_Station_By_Id` | 按注册编号查当前或被保留的退役节点,不误取同CA的新站。 | +| `IEC104_Station_Get_Id` | 取得注册编号,NULL或已回收节点返回0。 | +| `IEC104_Station_Is_Registered` | 是否仍参与CA查找;NULL返回false。 | +| `IEC104_Station_Is_Reusable` | 节点是否已回收,可由调用方复用/释放;NULL返回false。 | +| `IEC104_Slave_StationSnapshot_Begin` | 开始一次不阻止增删的遍历,固定注册编号上界。 | +| `IEC104_Slave_StationSnapshot_Next` | 取得下一个未删除的原有站点,借用指针。 | +| `IEC104_Slave_StationSnapshot_End` | 归还快照并尝试回收退役节点。 | +| `IEC104_Slave_Next_Station` | 按注册顺序遍历;current=NULL取首站,末尾/外来节点返回NULL。 | +| `IEC104_Slave_Lock_Stations` | 固定当前集合,阻止增删与服务销毁,不冻结点值。 | +| `IEC104_Slave_Unlock_Stations` | 归还一次集合借用,零计数释放报错。 | +| `IEC104_Slave_Acquire_Station` | 增加节点引用,延迟删除后的存储回收;拒绝外来节点及溢出。 | +| `IEC104_Slave_Release_Station` | 归还一次节点引用,零计数释放报错。 | +| `IEC104_Station_Get_Common_Address` | 取得CA,退役但被保留的节点仍可读;NULL或已回收时返回0。 | +| `IEC104_Station_Get_Context` | 取得应用context,退役但被保留的节点仍可读;NULL或已回收时返回NULL。 | +| `IEC104_Slave_Create` | 从静态池取得子站门面及其私有TCP服务;无malloc、无网络操作。 | +| `IEC104_Slave_Destroy` | 已关闭、事件读空、站点移除且集合锁归还后释放服务;失败保留对象。 | +| `IEC104_Slave_Config_Default` | 设置默认监听及协议配置。 | +| `IEC104_Slave_Init` | 通过内部TCP服务初始化监听和队列;不创建业务回调线程。 | +| `IEC104_Slave_Prepare` | 只读查询socket、定时及本地工作条件,继续使用原token规则。 | +| `IEC104_Slave_Process` | 有界推进一批就绪事件,不等待,业务随后读取NextEvent。 | +| `IEC104_Slave_Poll` | 组合准备、有限等待与推进,适合独立事件循环。 | +| `IEC104_Slave_NextEvent` | 按FIFO交付事件,I帧自动提供ASDU视图和绑定原服务实例/代次的发送接口;EMPTY/ERROR保持输出。 | +| `IEC104_Slave_Copy_Event` | 复制事件及原文,重建目标视图和接口的自引用,保留原身份供延迟应答。 | +| `IEC104_Slave_Get_Event_Identity` | 校验事件并取得来源身份;允许同实例历史会话通知,失败保持输出。 | +| `IEC104_Slave_Check_Request` | 统一检查未读通知、原服务实例、连接代次和激活状态,返回业务推进条件。 | +| `IEC104_Slave_Get_Last_Submit_Result` | 按事件connection查询最近发送的OK/WAIT/ERROR/FATAL,仅WAIT可等待容量重试。 | +| `IEC104_Slave_Try_Send_ASDU` | 沿事件原connection尝试不排队提交ASDU,直接返回并记录OK/WAIT/ERROR/FATAL;WAIT不保存原文,应用重读后重试,普通共用发送接口不变。 | +| `IEC104_Slave_Send_Response` | 使用保存的原事件身份提交独立ASDU;保留原实例/代次检查,直接发送或进入HIGH,返回提交结果,不要求保留完整事件。 | +| `IEC104_Slave_Try_Send_Response` | 使用保存的原事件身份尝试不排队提交;WAIT不缓存数据,原队列、窗口和期限规则保持不变。 | +| `IEC104_Slave_Reject_Request` | 同步拒绝当前事件,原文仅改COT=44..47/P/N=1,复用HIGH;直接返回提交结果,输入及最近提交结果不变。 | +| `IEC104_Slave_Get_Application_Layer_Parameters` | 返回标准104静态只读参数,供主动上报构造ASDU。 | +| `IEC104_Slave_Enqueue_Event` | 校验ASDU对象并提交NORMAL,支持离线缓存和原有覆盖策略。 | +| `IEC104_Slave_Get_Remote_Address` | 取得当前主站地址副本,不借用内部连接对象。 | +| `IEC104_Slave_Close_Connection` | 按原事件关闭指定连接,内部提取身份;旧代次不能关闭新连接。 | +| `IEC104_Slave_Close_Connection_By_Identity` | 按保存的原事件身份关闭原连接;与完整事件入口同样检查服务实例/代次,保留监听和NORMAL。 | +| `IEC104_Slave_Get_Status` | 取得服务、连接及两级队列的只读快照。 | +| `IEC104_Slave_Close` | 关闭服务并保留收尾通知;继续NextEvent读空,再Destroy。 | + + +
+完整函数原型(41 个;参数和返回类型见声明) + +```c +#include "IEC104_Slave.h" + +IEC104_StationResult_t IEC104_Slave_Add_Station(IEC104_Slave_t *self, + IEC104_Station_t *station, uint16_t common_address, void *context); +IEC104_StationResult_t IEC104_Slave_Remove_Station(IEC104_Slave_t *self, IEC104_Station_t *station); +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); +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); +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); +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); +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); +uint16_t IEC104_Station_Get_Common_Address(const IEC104_Station_t *station); +void *IEC104_Station_Get_Context(const IEC104_Station_t *station); + +IEC104_Slave_t * IEC104_Slave_Create(void); + +IEC104_TcpResult_t IEC104_Slave_Destroy(IEC104_Slave_t *self, IEC104_TcpError_t *error); + +void IEC104_Slave_Config_Default(IEC104_SlaveConfig_t *config); + +IEC104_TcpResult_t IEC104_Slave_Init(IEC104_Slave_t *self, + const IEC104_SlaveConfig_t *config, IEC104_TcpError_t *error); + +IEC104_TcpResult_t IEC104_Slave_Prepare(const IEC104_Slave_t *self, + IEC104_TcpWaitInfo_t *info, IEC104_TcpError_t *error); + +IEC104_TcpResult_t IEC104_Slave_Process(IEC104_Slave_t *self, + const IEC104_TcpReady_t *ready, IEC104_TcpError_t *error); + +IEC104_TcpResult_t IEC104_Slave_Poll(IEC104_Slave_t *self, + uint32_t max_wait_ms, IEC104_TcpError_t *error); + +IEC104_TcpEventResult_t IEC104_Slave_NextEvent(IEC104_Slave_t *self, IEC104_SlaveEvent_t *event); + +bool IEC104_Slave_Copy_Event(IEC104_SlaveEvent_t *destination, const IEC104_SlaveEvent_t *source); + +bool IEC104_Slave_Get_Event_Identity(const IEC104_Slave_t *self, + const IEC104_SlaveEvent_t *event, IEC104_SlaveEventIdentity_t *identity); + +IEC104_SlaveRequestState_t IEC104_Slave_Check_Request(const IEC104_Slave_t *self, + const IEC104_SlaveEventIdentity_t *identity); + +IEC104_TcpSubmitResult_t IEC104_Slave_Get_Last_Submit_Result( + const IEC60870_MasterConnection_t *connection); + +IEC104_TcpSubmitResult_t IEC104_Slave_Try_Send_ASDU( + IEC60870_MasterConnection_t *connection, const IEC60870_Asdu_t *asdu); + +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); + +IEC104_TcpSubmitResult_t IEC104_Slave_Reject_Request(IEC104_Slave_t *self, + const IEC104_SlaveEvent_t *event, int cause); + +const IEC60870_AppLayerParameters_t * IEC104_Slave_Get_Application_Layer_Parameters( + const IEC104_Slave_t *self); + +IEC104_TcpSubmitResult_t IEC104_Slave_Enqueue_Event(IEC104_Slave_t *self, IEC60870_Asdu_t *asdu); + +IEC104_TcpResult_t IEC104_Slave_Get_Remote_Address(const IEC104_Slave_t *self, + IEC104_TcpPeerInfo_t *peer, IEC104_TcpError_t *error); + +IEC104_TcpResult_t IEC104_Slave_Close_Connection(IEC104_Slave_t *self, + const IEC104_SlaveEvent_t *event, IEC104_TcpError_t *error); + +IEC104_TcpResult_t IEC104_Slave_Close_Connection_By_Identity(IEC104_Slave_t *self, + const IEC104_SlaveEventIdentity_t *identity, IEC104_TcpError_t *error); + +IEC104_TcpResult_t IEC104_Slave_Get_Status(const IEC104_Slave_t *self, + IEC104_TcpServerStatus_t *status, IEC104_TcpError_t *error); + +IEC104_TcpResult_t IEC104_Slave_Close(IEC104_Slave_t *self, IEC104_TcpError_t *error); +``` + +
+ + + +## 20. IEC60870_Codec.c + +实现:[IEC60870_Codec.c](../../common/IEC60870_Codec.c);声明:[IEC60870_Codec.h](../../common/IEC60870_Codec.h)。 + +101/104共用的ASDU基础解析。应用层参数定义在 `IEC60870_Types.h`;检查公共头和最低载荷,不依赖104帧、TCP或串口,也不执行具体对象或业务校验。 + +调用约定:两个函数均返回 1/0。Valid 检查参数布局;Parse_ASDU_Header 还检查完整公共头及已知类型最低载荷,不要求精确对象区长度,也允许未知类型和零对象在此层保留。info 借用参数和原文,失败保持输出;需要执行具体业务时继续 Asdu_Parse 和应用级校验。104 参数从接入层获取,公共解析器不内置 104 默认配置。 + +| 函数 API 接口名 | 功能作用 | +| --- | --- | +| `IEC60870_AppLayerParameters_Valid` | 检查Type ID/VSQ为1、COT/CA为1或2、IOA为1..3、默认OA为0..255、总长至少容纳头且不超过254;合法返回1,非法或NULL返回0。 | +| `IEC60870_Parse_ASDU_Header` | 首参显式传入应用层参数,按可变COT/CA/IOA宽度解析公共头及最低载荷;结果借用参数与报文,COT为1字节时OA输出0,失败保持输出不变。 | + + +
+完整函数原型(2 个;参数和返回类型见声明) + +```c +#include "IEC60870_Codec.h" + +extern int IEC60870_AppLayerParameters_Valid(const IEC60870_AppLayerParameters_t *parameters); + +extern int IEC60870_Parse_ASDU_Header(const IEC60870_AppLayerParameters_t *parameters, + const uint8_t *asdu, + uint16_t asdu_len, + IEC60870_AsduInfo_t *asdu_info); +``` + +
+ + + + +## 21. IEC104_Interrogation.c + +实现:[IEC104_Interrogation.c](../../104/business/IEC104_Interrogation.c);声明:[IEC104_Interrogation.h](../../104/business/IEC104_Interrogation.h)。 + +本节保留总召原12个接口的接入说明;Create的实现已移至GeneralInterrogation.c。请求追加freeze/data_cause,数据COT应取data_cause。内部头声明Create_With_Rules及Execute_Action,仅供种类文件协作,业务不直接调用。默认实例池为TCP服务容量的两倍,同一Slave允许总召、计数量各一实例,广播数组不得重叠。 + +事件式总召模块:处理Type100、QOI=20..36,沿原connection按ACT_CON → 多类型数据批次 → ACT_TERM发送;确认和终止可排入HIGH,数据尝试不排队提交。支持主站COT=8撤销及业务超时。Slave负责通信,应用通过NextEvent取得BEGIN/NEED_DATA/FINISHED并显式应答;不注册业务回调。完整顺序见[总召接入流程](IEC104_完整调用流程.md#flow-interrogation)。 + +### 配置、所有权与事件 + +Create复制配置、借用Slave,一个Slave只绑定一个总召实例,且须在消费Slave通知前创建。每个实例至多一个未终结总召:空闲时接受任意合法QOI=20..36并保存为START;从保存起到终结,所有新激活都否定确认,不区分QOI,不替换原请求。Process有业务事件空位后生成BEGIN,应用接受后才提交肯定ACT_CON。总召忙只约束Type100激活,其他业务及匹配撤销继续分派。所有调用在同一事件循环串行执行,不创建线程或socket,无malloc。详细状态和调度规则见[总召模块设计](IEC104_Interrogation_设计架构.md)。 + +Handle_Event通过Slave_Get_Event_Identity统一验证来源并绑定服务身份;Process及应用应答入口通过Slave_Check_Request判断是否应先处理通知或终结失效会话。总召不直接访问Slave事件内部身份字段,也不依赖TCP状态快照的组合条件;总召阶段、业务期限和FINISHED交付仍由本模块管理。 + +| IEC104_InterrogationConfig_t字段 | 默认值及范围 | 含义 | +| --- | --- | --- | +| common_address | 1;1..65534 | 不使用站点表且station=NULL时的兼容站地址;其他模式忽略 | +| max_objects_per_asdu | 127;1..127 | 单批对象数上限,实际还受249字节容量限制 | +| use_station_registry | false | true按请求CA查找共用站点,65535逐站遍历;此时station须NULL | +| broadcast_states / broadcast_capacity | NULL / 0 | 调用方提供的IEC104_InterrogationStationState_t数组及项数,多目标广播须足够容纳本轮目标 | +| begin_timeout_ms | 15000ms;0禁用 | BEGIN入队至应用应答,含未取事件的等待 | +| data_timeout_ms | 15000ms;0禁用 | 每次NEED_DATA入队至首次有效批次尝试/Complete_Data/Fail_Request;有效批次发送WAIT也停止本批计时,每批重新开始 | +| total_timeout_ms | 0(默认禁用) | 启用后从BEGIN生成至ACT_TERM被发送层接手,包含应用取数和发送等待,不包含生成BEGIN前等事件空位,不等待ACK | +| station | NULL | 兼容固定站绑定,Create持有引用至Destroy;删除后不接新请求,已接受任务可完成 | + +新应用先Slave_Add_Station,再设置`config.use_station_registry=true`;CA只在注册时指定。多目标广播配置broadcast_states/broadcast_capacity,按“全部确认、逐站数据、全部终止”共用一个任务。数组借用至Destroy,地址稳定、期间不可读写或与其他实例重叠;NULL/0须配对,内部字段无需初始化。容量不足会在任何肯定确认前整轮否定,不部分执行;单目标无需此数组。BEGIN携带`request.station_id`,应用需保留context时按编号Acquire,使用结束后Release。`use_station_registry=false`保留原common_address或固定station模式;true与非NULL station组合拒绝创建。配置布局改变,库与调用方须一起重新编译。 + +三项期限是独立于链路t0..t3的可配置业务策略,默认值不是标准规定的总召时限。执行总期限默认禁用,BEGIN和每批取数期限仍各为15000ms,底层链路期限不受影响。BEGIN成功生成时启动已启用的应用接受及执行总期限;此前START等业务FIFO空位不计业务期限,但已占用总召资格,须及时读取历史FINISHED。总期限设置为非零后仍为固定整体期限,不因提交新批次或忙应答而延长。用Config_Default初始化,再按业务需要修改。 + +| 类型或字段 | 约定 | +| --- | --- | +| IEC104_InterrogationRequest_t | CA/OA/QOI/TEST、generation/receive_serial、station_id及对象数上限;CA为当前目标站,station_id为注册编号(裸CA模式为0),qualifier为20全站或21..36分组 | +| IEC104_InterrogationEvent_t | 纯值,可赋值及跨循环保存;无Slave事件的自引用,不需要Copy_Event | +| instance_id / request_id / batch_index | 实例、请求和批次身份,只读,调用应答接口时原样传回;拒绝重复、迟到和取消后的应答 | +| BEGIN | 按qualifier检查数据源并接受/拒绝;可延至首个NEED_DATA建立游标,不预先采样全部数据 | +| NEED_DATA | 应用选择本批IOA和类型,读取当前值/品质后组包;未就绪时保存事件,稍后提交,不重复生成请求 | +| FINISHED | reason说明终结原因,带已提交批次/对象进度;应用停止对应采样任务并清理资源,不需要应答 | + +每个已取出的BEGIN保证且仅保证一次FINISHED;BEGIN未生成或未取出便取消时直接移除工作,不生成FINISHED。参数校验、总召忙和满槽拒绝不交付BEGIN。取消移除未取出的工作事件,但保留所有FINISHED。模块不保存应用session或数据源指针;遍历状态及业务资源归应用所有,建议按实例和请求编号管理。 + +IOA及分组成员由调用者决定,协议库不规定点表或强制固定IOA集合。NEED_DATA及每次WAIT重试都读取当前值;Submit_Batch不缓存待发数据,OK才固定帧顺序。点值更新及突发发布须与总召读值及提交串行化,具体排序保证及持续突发可能推迟总召的边界见[总召模块设计](IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。 + +编译实现文件时可覆盖IEC104_INTERROGATION_CAPACITY(默认IEC104_TCP_SERVER_CAPACITY)、IEC104_INTERROGATION_TASK_CAPACITY(默认4)和IEC104_INTERROGATION_EVENT_CAPACITY(默认4,最小2)。任务池保存至多一轮总召及短应答,没有后续总召等待队列或数据批次缓冲。任务只保存原事件身份及10字节Type100原文,不保存完整SlaveEvent或自引用视图。单目标生成BEGIN时在FIFO预留FINISHED;多目标由每站状态区保留终结记录,NextEvent随空位逐个交付,2槽FIFO也能无损处理13站。网络流程结束后进入FINISHING,读完终结事件才释放该任务;此时仍忙拒绝新激活。只有任务及事件均为空才允许Destroy;退出须Cancel → NextEvent读完并清理 → Destroy,再销毁Slave。测量见[固定存储](IEC104_Interrogation_设计架构.md#2-对象身份与固定存储)。 + +### 广播公共地址 + +开启站点表时,多目标Type100广播(CA=65535)先按注册顺序完成全部BEGIN/ACT_CON,再逐站完成数据,最后发送全部ACT_TERM。每站独立BEGIN/FINISHED、request_id和计时。接收时持有目标集合;尚未选入BEGIN便删除的站跳过,新增或同CA替代节点下轮才参加。已选中尤其已确认的站持原引用完成或明确失败,不因删除而静默跳过。只占一个任务槽,另需每站轻量状态区。没有注册站时回复COT=46并保留65535;单目标保持原流程。 + +广播每站的确认、数据、终止和错误应答使用该站实际CA,保留OA/TEST/QOI和原连接身份。每次选站时更新紧凑请求的CA,同一站的发送重试沿用它,输入Slave事件不修改。忙/错误广播也用一个遍历任务逐站回复;无空槽保存游标时整体丢弃新广播并计数,不先发送部分站的应答。未知具体CA按原值回COT=46;校验优先级仍为COT、CA、IOA、QOI/P/N。Type101等其他类型继续返回IGNORED。 + +广播三阶段顺序与[c104的Server.cpp](https://github.com/Fraunhofer-FIT-DIEN/iec104-python/blob/main/src/Server.cpp)一致。对象寿命使用调用方节点及显式引用,保留本库的WAIT重试、业务事件、QOI=20..36及撤销/失败收尾规则;并不复制C++对象模型或动态分配方式。 + +### 数据批次及返回值 + +Submit_Batch接受应用构造的非空ASDU,参数须2/2/3,COT=请求qualifier(全站20,分组21..36)、P/N=0,CA必须等于当前任务的request.common_address,OA/TEST与请求一致;提交65535或其他站CA返回ERROR并保留交互。支持Type 1/3/5/7/9/11/13/20/21,SQ=0/1,批间允许切换类型,最多249字节且不超配置对象数。Slave_Try_Send_Response按原身份尝试提交,当前发送缓冲及HIGH/NORMAL未提交条目均为空、链路和窗口允许才接手;否则WAIT,不缓存数据。OK后应用可推进游标;WAIT保留事件和游标,网络推进后重读当前值重试。Add返回FULL时提交已有非空批次,未加入对象留给下批;数据耗尽用Complete_Data。 + +组号与IOA的对应关系由应用维护,模块没有内置点表,也不校验点是否属于请求的组。未配置组可Reply_Begin(false);接受的空组仍可正常确认并终止。ACT_CON/ACT_TERM的COT保持7/10,回显原QOI。 + +| 返回类型 | 值及语义 | +| --- | --- | +| IEC104_InterrogationEventResult_t | EVENT_HANDLED=1表示请求/通知已处理,包含拒绝或畸形/满槽丢弃(查看rejected、dropped_requests);EVENT_IGNORED=0交公共入口或后续处理器;EVENT_ERROR=-1参数或处理失败 | +| IEC104_InterrogationNextResult_t | NEXT_READY=1提供一个业务事件;NEXT_EMPTY=0或NEXT_ERROR=-1保持输出不变 | +| IEC104_InterrogationReplyResult_t | REPLY_OK=1已接手应答;REPLY_WAIT=0先推进网络、消费Slave通知,保留事件和游标,数据批次重新取值后重试;REPLY_STALE=-2丢弃旧应答;REPLY_ERROR=-1修正参数/批次或处理查询错误 | + +应答只接受已经NextEvent取出且尚未回答的BEGIN或NEED_DATA,按实例、请求和批次身份匹配原任务,不依赖任务是否位于队首。Submit_Batch的REPLY_OK表示数据已被发送层接手,其余应答仍由Process推进;均不表示写完或ACK。REPLY_WAIT可能来自Slave FIFO未读空,Submit_Batch还可能因发送背压返回WAIT;不能跳过STOP/START或撤销请求。有效交互还会检查期限,到期即终结当前业务并返回STALE,不能用迟到应答绕过超时;确认前保留TCP,调用方仍须驱动Process发送否定确认。参数或批次校验REPLY_ERROR不消费交互,可修正后重试;Submit_Batch的发送ERROR若确认为正常STOP拒绝,先返回WAIT等待通知,随后FINISHED(STOPPED),其他发送ERROR/FATAL仍以SEND_ERROR终结并返回STALE。NEED_DATA阶段Fail_Request即使关闭连接失败仍取消任务、保留FINISHED,此时也不可重试。不要把任意非零结果当作成功。 + +### 主站撤销 + +Handle_Event接受COT=6和8。撤销按原连接代次、目标CA、OA、TEST和QOI匹配,包括已选中但BEGIN尚未生成的START,以及已确认但未取数/等终止的站。定向撤销匹配站,其他站继续;广播撤销停止整轮,按各仍活动的站回复肯定COT=9。尚未选入BEGIN的未来站及已结束站不补发确认;已删除但仍执行的原站可匹配。撤销先于忙拒绝处理,被忙拒绝的新QOI没有可撤销任务。 + +匹配后停止原任务,旧工作事件应答返回STALE,丢弃模块尚未提交的批次;原任务槽改为肯定DEACT_CON(COT=9、P/N=0),将其索引移到单帧应答队尾,条件允许时立即提交,否则由Process重试。不会再提交该任务的ACT_TERM。已交付BEGIN则生成一次FINISHED(DEACTIVATED),应用据此停止采样并清理资源;无需额外撤销回调或应用确认。BEGIN从未交付时只移除工作事件。FINISHED说明业务任务已停止,不说明撤销确认已发出或已ACK。 + +已被发送层接手的帧不能撤回,仍在撤销确认之前按原顺序发送。队列已满或原总召不在队首时仍能匹配撤销,不需要新增任务槽。未匹配、重复撤销或原ACT_TERM已被接手时,按单帧应答规则提交或保存否定COT=9;错误CA/IOA仍优先使用46/47。COT=8仅撤销对应总召,不停用整个TCP传输;Slave的DEACTIVATED通知表示STOPDT,对应总召终结原因是STOPPED。本地Cancel不回复DEACT_CON。 + +### 异常、调度及状态 + +| 情况 | 行为 | +| --- | --- | +| COT不是6/8、CA既非本站也非65535、IOA不是0 | 依次回45/46/47、P/N=1;广播请求的应答CA改为本站地址 | +| 激活QOI不在20..36、P/N非法或已有未终结总召 | 否定ACT_CON,不交付BEGIN;忙时相同和不同QOI均拒绝 | +| 空闲时收到合法激活 | 有空槽则建立唯一START任务;Process生成BEGIN后由应用决定 | +| 撤销QOI不在20..36、P/N非法或未匹配 | 否定DEACT_CON,当前总召不变 | +| 匹配的主站撤销 | 只停止匹配任务;已交付BEGIN才有FINISHED(DEACTIVATED),提交或保存肯定DEACT_CON | +| Reply_Begin(false) | FINISHED(REJECTED),排队否定ACT_CON,连接保持 | +| 需要新增总召但任务池满 | 尝试提交否定ACT_CON;无法安全提交/保存则丢新请求并计数,保留已有任务与TCP | +| 否定/错误应答未接手且任务池满 | 丢弃该新请求,增加overloads和dropped_requests,不越过旧REPLY,不取消已有任务 | +| 非100或公共ASDU视图不可用 | 安全读取Type后返回IGNORED;公共入口先过滤不可用视图,底层帧错误由Receiver/连接层处理 | +| Type100唯一对象完整,但SQ=1或有多余尾字节 | 回否定7/9,COT/CA/IOA错误优先45/46/47;应答规范为10字节、VSQ=1,保留OA/TEST/IOA/QOI;不执行激活/撤销 | +| Type100零对象/多对象等无法可靠应答的布局错误 | 计入rejected后HANDLED丢弃;不生成应答,不影响已有总召及其期限 | +| Submit_Batch非法 | REPLY_ERROR,保留NEED_DATA供修正,连接保持 | +| BEGIN阶段Fail_Request | 原槽转否定ACT_CON,保留连接及其他任务,FINISHED(SOURCE_ERROR) | +| START/CONFIRM阶段期限到点 | 原槽转否定ACT_CON,保留连接及其他任务;已交付BEGIN才有FINISHED(BEGIN_TIMEOUT / TOTAL_TIMEOUT) | +| NEED_DATA阶段Fail_Request或DATA/TERMINATE超时 | 关闭原连接、取消本模块任务,FINISHED(SOURCE_ERROR / DATA_TIMEOUT / TOTAL_TIMEOUT),不发成功ACT_TERM | +| STOPDT / 断线 / Cancel | FINISHED(STOPPED / DISCONNECTED / CANCELLED),未交付工作事件移除 | +| Destroy时尚有任务或业务事件 | false,保持状态;必须先取消和读取终结事件 | +| 发送ERROR且确认为原连接正常STOP拒绝、通知待处理 | 保留任务并让出,由DEACTIVATED终结为STOPPED;Submit_Batch返回WAIT,不算发送背压 | +| 其他发送ERROR/FATAL、会话变化、查询失败 | 取消并分别终结为SEND_ERROR、CONNECTION_CHANGED、SERVICE_ERROR | + +Handle_Event仅在空闲时保存合法总召,忙拒绝及可应答的畸形请求只保存应答;对单帧应答,只有Slave通知已读空、原会话仍有效且没有更早REPLY时,才沿现有HIGH接口立即尝试提交。使用请求副本,不修改调用方事件;OK后不再入任务池,WAIT或暂不能提交时保存,满槽则只丢新请求并累计dropped_requests。布局错误不取消已有总召或刷新其期限;规范化副本只能用于否定应答。合法匹配撤销复用原槽,因此即使满池也能保存确认。丢弃的请求没有应答,主站可能等待超时或重试;EVENT_HANDLED不承诺主站已收到响应。 + +调用IEC104_Interrogation_Process(self)即可推进保存的任务。REPLY按形成顺序发送;正常总召与短应答轮转,总召等应用/事件空位不阻塞短应答。每站保持ACT_CON → 数据 → ACT_TERM,多目标还保证全部确认先于数据、全部数据先于终止。ACT_TERM接手后终结,不等ACK。广播某站拒绝、超时或定向撤销后继续其他站;整轮结束并读完FINISHED才接受新激活。此前忙时拒绝的请求须由主站重新发起。 + +确认前后以肯定ACT_CON被发送层接手为边界。Reply_Begin(true)只进入CONFIRM;发送WAIT时总超时仍可改为否定确认,原槽复用并排到已有REPLY之后。已接手但尚未写出或ACK的肯定确认也算提交成功,DATA/TERMINATE失败仍关闭原连接。Process检测到确认前超时后重新检查Slave通知,在本轮继续调度否定确认;发送WAIT保留应答,不再累计已终结请求的业务超时。终结原因保留为具体TIMEOUT或SOURCE_ERROR,不统一改成REJECTED;未取出的BEGIN移除且不生成FINISHED。 + +Process在任务为空、全部等待应用/事件空位、发送WAIT或达到2倍任务容量步数时返回,余下本地工作由need_process提示。确认、终止及短应答WAIT保留当前发送项,下次调用优先尝试最早REPLY;DATA只生成NEED_DATA,应用Submit_Batch直接提交。广播共用任务遍历三个阶段,每站独立业务期限;启用总期限时,等待其他站的已确认目标也受检查,到期关闭原连接。保存业务事件期间须持续推进网络,并重读、重试数据WAIT。 + +调度只改变模块尚未提交任务的先后,已经被HIGH队列或发送缓冲接手的帧仍按原顺序发送,不能被新应答越过,也不能通过Cancel撤回。COMPLETED仅表示ACT_TERM提交成功。 + +Process每一步先检查Slave通知,再检查业务期限和选择任务;有未读通知即让出,包含本次推进期间新产生的通知。若已收到撤销/STOP/断线则按对应原因结束,不再重复生成超时事件。单帧应答的调度不会重置活动总召的期限;总期限与阶段期限都到点时优先报告TOTAL_TIMEOUT。 + +发送返回ERROR时额外复核正常STOP状态,三个提交入口共用此判断;不因任意新通知而忽略错误。OK照常记账,WAIT让出,FATAL仍失败。STOP拒绝的等待不累计waits、不设置data_send_waiting、不停止取数计时;完整条件见[提交结果处理](IEC104_Interrogation_设计架构.md#72-process中的选择规则)。 + +等待前重新Get_Status;need_process为true时立即推进,不能用!waiting替代。timer_active为true时,以timeout_remaining_ms限制Poll/应用等待上限,0表示立即推进。查询只读取状态和时间,不执行发送或超时处理;模块没有自动定时线程。BEGIN尚未生成时没有业务计时器,应继续读业务事件并驱动Process。确认前超时保留连接;肯定确认提交后的超时关闭原连接,会中断该连接其他业务。三项期限可以配置或逐项设0禁用,不增加独立的断链策略开关。 + +data_timeout_ms只计算NEED_DATA入队至首次有效批次尝试、结束或失败。有效批次因发送背压WAIT后,data_send_waiting置位,本批取数计时停止且不恢复,下一批NEED_DATA重新计时。无效批次或未读Slave通知导致的提前WAIT不停止计时;显式启用的total_timeout_ms及底层链路期限仍有效。 + +业务事件释放空位或应答接手后重查need_process;发送WAIT后不因保留任务而持续置位。每次Slave_Poll/Process推进网络并读空Slave通知后仍须调用总召Process重试,即使need_process为false。Get_Status实时检查Slave FIFO,未读通知使events_pending为true、need_process为false,先转交STOP/撤销等通知。外部事件循环同时遵守Slave_Prepare的等待条件;need_process仅表示需要尝试本地推进,不保证发送成功。新增状态字段后调用方须与库一起重新编译。 + +数据WAIT时,还须在网络推进后重读并重试应用持有的NEED_DATA,不能等待新事件或need_process置位;Process不保存或代为重发本批。 + +| 状态 | 含义 | +| --- | --- | +| generation / active / gi_pending | 已消费的连接代次、激活状态、是否存在唯一未终结总召(含BEGIN尚未生成) | +| phase | 当前站START / CONFIRM / DATA / TERMINATE,收尾FINISHING;仅有应答REPLY,无任务IDLE | +| waiting / application_waiting | 等发送容量、应用应答或业务FIFO空位;后者表示等待应用首次有效应答。总召等待时仍可调度单帧应答,waiting不表示所有任务都无法推进 | +| data_send_waiting | 有效批次曾遇发送WAIT;应用仍持有NEED_DATA,须在网络推进后重新取值提交 | +| need_process | 应立即推进本地任务、到期期限或失效会话;未读Slave通知优先,发送WAIT后等网络推进再重试 | +| events_pending / pending_events | Slave未读通知标志 / 未取出的业务事件数(含状态区待交付FINISHED,可大于FIFO容量) | +| timer_active / timeout_remaining_ms | 是否存在启用的业务期限 / 最近期限距查询时刻的剩余毫秒数 | +| pending_requests | 总召、FINISHING及排队单帧应答数量(含撤销确认);FINISHING的gi_pending虽为false,仍忙拒绝新激活 | +| batch_index / objects_submitted | 优先报告活动总召已提交的数据批次/对象数;仅有单帧应答时报告最早应答保留的进度,无任务时为0;发送WAIT不增加 | +| submitted / gi_submitted | 发送层已接手ASDU数 / 已提交ACT_TERM的总召数 | +| waits / cancelled / rejected / overloads | 发送WAIT、取消任务、拒绝及任务容量超限次数;rejected含无法可靠应答的畸形请求;确认前失败计入rejected,超时同时计入timeouts,不计入cancelled | +| negative_submitted / dropped_requests | 发送层已接手的否定应答数 / 无法安全提交或保存而丢弃的新请求数;前者不等于主站已接收 | +| deactivated / timeouts | 成功匹配的主站撤销数 / 执行超时数 | +| last_finish_reason | 最近合法总召的终结原因,BEGIN尚未交付也会更新 | + +旧begin/read_batch/finish和parameter字段已移除;用业务事件与应答接口替代。模块仍提供12个公开函数,Process只接收self。当前只保留一个未终结总召,忙时否定后续激活,不自动执行此前被拒绝的请求。已删除queue_timeout_ms、QUEUED/QUEUE_TIMEOUT、queued_interrogations和queue_timeouts;调用方删除相关字段及分支,使用Config_Default初始化配置并重新编译。negative_submitted和dropped_requests继续记录否定提交与满槽丢弃;FINISH_OVERLOAD及FINISH_INVALID_REQUEST保留枚举值,满槽或畸形新请求不以此终结已有任务。 + +### 函数功能 + +| 函数 | 功能与约定 | +| --- | --- | +| `IEC104_Interrogation_Config_Default` | CA=1、每批127对象,接受/取数期限各15000ms,执行整体期限0(禁用);NULL无操作。 | +| `IEC104_Interrogation_Create` | 复制配置并借用Slave;非法配置、重复绑定、池满或实例编号耗尽返回NULL。 | +| `IEC104_Interrogation_Destroy` | 无协议任务且无业务事件时归还槽;否则false且不修改状态,不关闭Slave。 | +| `IEC104_Interrogation_Cancel` | 取消未提交任务,为已交付BEGIN保留FINISHED(CANCELLED);不补发终止、不撤回已接手帧。 | +| `IEC104_Interrogation_Handle_Event` | 按FIFO接收Slave通知,空闲时保存总召、忙时否定新激活,处理匹配撤销及会话变化;单帧应答可安全提交时立即尝试,满槽拒绝/计数丢弃;其他I帧IGNORED。 | +| `IEC104_Interrogation_Process` | 自动推进当前就绪的单帧应答与总召,直到空闲或需等待;每步检查Slave通知与期限。true可为空闲/等待/取消/超时,false为参数、组包、关闭连接或服务查询错误。 | +| `IEC104_Interrogation_NextEvent` | 取最早的业务事件,仅READY写输出;无网络操作。 | +| `IEC104_Interrogation_Reply_Begin` | 接受或拒绝已取出的BEGIN;接受后Process才可提交ACT_CON。 | +| `IEC104_Interrogation_Submit_Batch` | 应答NEED_DATA,验证并立即尝试不排队提交非空ASDU;OK推进进度,WAIT保留事件重读再试,参数ERROR可修正,发送故障终结为STALE。 | +| `IEC104_Interrogation_Complete_Data` | 应答NEED_DATA声明本站数据耗尽,可复用取数游标;多目标全部数据结束后才逐站ACT_TERM及FINISHED。 | +| `IEC104_Interrogation_Fail_Request` | 报告数据失败:BEGIN转否定ACT_CON并保留连接,NEED_DATA关闭原连接;均终结为SOURCE_ERROR,关闭失败也交付终结事件。 | +| `IEC104_Interrogation_Get_Status` | 取得状态、累计统计及距最近业务期限的剩余毫秒;失败保持输出,不推进业务。 | + +
+完整函数原型(12 个) + +```c +#include "IEC104_Interrogation.h" + +void IEC104_Interrogation_Config_Default(IEC104_InterrogationConfig_t *config); + +IEC104_Interrogation_t *IEC104_Interrogation_Create(IEC104_Slave_t *slave, + const IEC104_InterrogationConfig_t *config); + +bool IEC104_Interrogation_Destroy(IEC104_Interrogation_t *self); + +void IEC104_Interrogation_Cancel(IEC104_Interrogation_t *self); + +IEC104_InterrogationEventResult_t IEC104_Interrogation_Handle_Event( + IEC104_Interrogation_t *self, const IEC104_SlaveEvent_t *event); + +bool IEC104_Interrogation_Process(IEC104_Interrogation_t *self); + +IEC104_InterrogationNextResult_t IEC104_Interrogation_NextEvent( + IEC104_Interrogation_t *self, IEC104_InterrogationEvent_t *event); + +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Reply_Begin( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, bool accept); + +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Submit_Batch( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, + const IEC60870_Asdu_t *asdu); + +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Complete_Data( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event); + +IEC104_InterrogationReplyResult_t IEC104_Interrogation_Fail_Request( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event); + +bool IEC104_Interrogation_Get_Status(const IEC104_Interrogation_t *self, + IEC104_InterrogationStatus_t *status); +``` + +
+ + + +## 22. IEC104_GeneralInterrogation.c + +实现:[IEC104_GeneralInterrogation.c](../../104/business/IEC104_GeneralInterrogation.c)。保留原`IEC104_Interrogation_Create(slave, config)`签名和总召行为,内部传入总召规则。原声明继续由Interrogation.h提供;工程必须把这个新的源文件加入构建。 + + + +## 23. IEC104_CounterInterrogation.c + +实现:[IEC104_CounterInterrogation.c](../../104/business/IEC104_CounterInterrogation.c);声明:[IEC104_CounterInterrogation.h](../../104/business/IEC104_CounterInterrogation.h)。 + +```c +#include "IEC104_CounterInterrogation.h" +IEC104_Interrogation_t *IEC104_CounterInterrogation_Create(IEC104_Slave_t *slave, + const IEC104_InterrogationConfig_t *config); +typedef bool (*IEC104_CounterInterrogationAction_t)(void *context, + const IEC104_InterrogationRequest_t *request); +IEC104_InterrogationReplyResult_t IEC104_CounterInterrogation_Execute_Begin( + IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, + IEC104_CounterInterrogationAction_t action, void *context); +``` + +Create使用共用配置并返回共用驱动句柄,同一Slave可以同时存在总召及计数量实例。读取沿用Reply_Begin、Submit_Batch、Complete_Data;FRZ=1..3通过Execute_Begin先校验再执行一次同步动作,WAIT/STALE/ERROR不执行回调。动作成功后只发ACT_CON即完成,TEST动作拒绝;传输失败不撤销已经执行的动作。回调失败必须保证未改变计数器,不得重入或等待外部I/O。完整约定见[计数量设计](IEC104_CounterInterrogation_设计架构.md)。 + + + +## 24. IEC104_ClockSynchronization.c + +实现:[IEC104_ClockSynchronization.c](../../104/business/IEC104_ClockSynchronization.c);声明:[IEC104_ClockSynchronization.h](../../104/business/IEC104_ClockSynchronization.h)。独立静态池默认与TCP服务容量相同,每实例4个任务槽,只使用共用站点表。完整校验、时间计算及广播规则见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md)。 + +Create复制配置、借用Slave,每服务至多一个实例,须在消费通知前创建。`master_utc_offset_minutes`默认480、范围-1439..1439,不含SU;`execute_timeout_ms`默认15000,0禁用执行期限。原事件不修改,可靠的单对象错误应答规范为16字节、VSQ=1,仅改COT/P/N及广播目标CA,保留CP56原文。COT/CA/IOA错误依次优先45/46/47,其余可对应的非法请求否定7,不能对应唯一完整对象时丢弃。 + +请求结构是回调期间借用的纯值,包含原`master_time`、站点/请求身份、`accepted_ms`、`base_ms`及对应base_ms的`utc_ms/local_time`。先完整校验IV/SU/时区及本地日历上界,再用同一次64位单调采样检查期限和补偿受理后的滞留;网络、Slave通知FIFO及硬件写入耗时不补偿。action只由Execute_Next同步调用,不存储回调或context;不得重入本模块或推进Slave事件循环,只有Get_Status允许在action中只读调用。 + +action的true/false分别报告成功/未报告成功,false不保证硬件没有改变。调用过action的肯定或否定ACT_CON均走Try;WAIT只重试确认,不重复动作,也不再计算执行期限。校验错误走普通HIGH。应答按请求FIFO提交;Execute_Next可以越过较早REPLY执行最早EXECUTE,其确认仍不插队。广播只执行一次动作,按受理快照逐个目标CA确认;删除的未提交目标跳过,新注册站不参与。满槽丢新并计overloads/dropped_requests,保持原连接和任务。 + +| 返回类型 | 含义 | +| --- | --- | +| EventResult | ERROR=-1为入口错误;IGNORED=0非本模块请求;HANDLED=1含拒绝/丢弃,不代表已应答 | +| ExecuteResult | ERROR=-1为参数、重入或服务查询错误;EMPTY=0无可执行请求但可能仍有应答(包括刚因期限/时间错误转为否定);WAIT=1先读Slave通知;OK=2调用过一次action,不代表动作成功或确认到达主站 | +| Process / Get_Status / Destroy | bool结果;Process不执行action,Get_Status只读且失败不改输出,Destroy要求先清理任务 | + +| 状态 | 含义 | +| --- | --- | +| active / generation / events_pending | 本模块已消费的会话状态及是否须先处理Slave通知 | +| execute_pending / pending_requests | 是否存在待执行动作 / 尚未交接完成的任务数 | +| confirm_pending | 包含正在执行的action,以及已执行但尚未交接完成的ACT_CON;应用据此协调新的带时标发布 | +| need_process / timer_active / timeout_remaining_ms | 是否需立即推进Process,以及最近执行期限的剩余毫秒;查询不触发发送或超时 | +| accepted / executed / synchronized | 合法请求受理数 / 实际action调用数 / action报告成功数;广播动作只计一次 | +| submitted / negative_submitted | 已交接ASDU数 / 其中否定应答数;广播每个实际提交目标分别计数,不代表ACK | +| rejected / waits / cancelled | 请求拒绝、发送WAIT及任务取消统计;取消不能证明设时或发送从未发生 | +| overloads / dropped_requests / timeouts / time_errors | 满槽及丢新、未执行到期、倒退或时间换算错误;0超时配置不禁用范围检查 | + +Try等到当前HIGH/NORMAL可提交条件满足,不隔离之后新增的NORMAL;持续发布可能推迟确认。`confirm_pending`供发布方串行协调,不是跨线程锁。STOP/断线/Cancel只取消未交接任务和站点遍历,不回滚时钟、不撤回已编号I帧。同内容的新请求不去重,重发旧时标可能再次回拨。 + +```c +#include "IEC104_ClockSynchronization.h" + +typedef bool (*IEC104_ClockSynchronizationAction_t)(void *context, + const IEC104_ClockSynchronizationRequest_t *request); + +void IEC104_ClockSynchronization_Config_Default(IEC104_ClockSynchronizationConfig_t *config); +IEC104_ClockSynchronization_t *IEC104_ClockSynchronization_Create(IEC104_Slave_t *slave, + const IEC104_ClockSynchronizationConfig_t *config); +bool IEC104_ClockSynchronization_Destroy(IEC104_ClockSynchronization_t *self); +void IEC104_ClockSynchronization_Cancel(IEC104_ClockSynchronization_t *self); +IEC104_ClockSynchronizationEventResult_t IEC104_ClockSynchronization_Handle_Event( + IEC104_ClockSynchronization_t *self, const IEC104_SlaveEvent_t *event); +bool IEC104_ClockSynchronization_Process(IEC104_ClockSynchronization_t *self); +IEC104_ClockSynchronizationExecuteResult_t IEC104_ClockSynchronization_Execute_Next( + IEC104_ClockSynchronization_t *self, IEC104_ClockSynchronizationAction_t action, void *context); +bool IEC104_ClockSynchronization_Get_Status(const IEC104_ClockSynchronization_t *self, + IEC104_ClockSynchronizationStatus_t *status); +``` diff --git a/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_ClockSynchronization_设计架构.md b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_ClockSynchronization_设计架构.md new file mode 100644 index 00000000..1716d1bc --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_ClockSynchronization_设计架构.md @@ -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)。 diff --git a/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_Connection_设计架构.md b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_Connection_设计架构.md new file mode 100644 index 00000000..b12d894a --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_Connection_设计架构.md @@ -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接入路径,同一会话不能重复喂给旧全局接收器。 diff --git a/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_CounterInterrogation_设计架构.md b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_CounterInterrogation_设计架构.md new file mode 100644 index 00000000..6be1bb72 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_CounterInterrogation_设计架构.md @@ -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)。 diff --git a/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_Interrogation_设计架构.md b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_Interrogation_设计架构.md new file mode 100644 index 00000000..f6c9fa15 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_Interrogation_设计架构.md @@ -0,0 +1,458 @@ +# IEC104 总召业务模块 + +`104/business/IEC104_Interrogation.c/h`在`IEC104_Slave`之上实现共用召唤引擎,Type100规则和原Create入口位于同目录的`IEC104_GeneralInterrogation.c/h`。通信服务负责连接、启停、收发和会话身份;引擎负责请求准入、业务阶段、数据批次检查、应答调度及终结通知。本文描述总召行为,应用通过`BEGIN / NEED_DATA / FINISHED`事件提供点表和数据;计数量的差异见第12节。 + +当前采用**每个实例至多一个未终结总召,忙时否定所有新的总召激活**。空闲时支持全站和全部16个分组,忙时不区分相同或不同QOI。任务池仍保留4个槽,用于一个总召及待发送短应答;没有后续总召等待队列,也没有独立排队超时。 + +本文描述库本身的设计,不依赖应用模拟点表或可执行程序。完整接入位置见[完整调用流程](IEC104_完整调用流程.md#flow-interrogation),函数声明及返回值汇总见[API接口汇总](IEC104_API接口汇总.md#file-21)。 + +## 1. 职责与调用关系 + +所有接口由所属事件循环串行调用。模块不创建socket、线程或系统定时器,不动态分配内存,也不会在`Process`中跳转执行应用代码。异步采样可以由应用自己的线程或设备驱动完成,但结果须回到所属事件循环后提交。 + +```text +应用事件循环 + ├─ IEC104_Slave:Poll / Prepare / Process / NextEvent + │ └─ TcpServer → Connection → Link / Receiver / SendManager → Net + └─ IEC104_Interrogation + Handle_Event ← 应用逐条转交Slave通知 + Process → 沿保存的原连接身份提交确认、终止及短应答 + Submit_Batch → 沿保存的原连接身份尝试直接提交数据,不入队等待 + NextEvent → BEGIN / NEED_DATA / FINISHED + Reply_Begin / Submit_Batch / Complete_Data / Fail_Request ← 应用显式应答 +``` + +| 模块 | 自己维护的规则 | 与总召的关系 | +|---|---|---| +| Slave | 服务生命周期、事件来源、连接代次、启停及提交结果 | 总召通过公共接口取得原身份、校验会话和提交应答 | +| TcpServer及内部通信模块 | TCP字节流、APCI序号、k/w、t0..t3、HIGH容量和发送完成 | 总召不直接操作socket、链路状态或发送窗口 | +| Interrogation | 一个总召的准入与阶段、短应答、业务事件及业务期限 | 不持有应用点表,不主动读取设备 | +| 应用 | IOA、分组成员、数据源、遍历状态、资源清理 | 根据业务事件决定接受、逐批取值交付或终结数据 | + +`Slave_Get_Event_Identity`校验事件来源;`Slave_Check_Request`判断是否有未读通知、原会话是否仍可提交。总召不自行拼凑TCP状态快照判断连接是否有效。应用应使用Slave和总召公开接口,不访问不透明对象内部字段。 + +## 2. 对象、身份与固定存储 + +`IEC104_Interrogation_t`是不透明的静态池句柄,通过`Create`取得,`Destroy`归还。一个Slave只能绑定一个总召实例;须在开始消费Slave通知前创建。配置按值复制,调用方配置返回后可复用;Slave须存活到总召实例销毁之后。 + +设置config.use_station_registry=true后,总召按请求CA选取本Slave的已注册站点,65535逐站遍历;config.station须NULL,common_address忽略。当前任务持有节点引用,删除使该站立即退出新请求路由,已有任务继续使用原节点。BEGIN携带station_id,应用跨循环保留context时应立即按编号查节点并Acquire,FINISHED清理后Release,防止同CA替换串站。默认false兼容原单CA调用,可选config.station仍固定绑定一个站点并持有到Destroy。注册、引用、延迟回收及快照契约见[共用站点表](IEC104_API接口汇总.md#station-registry)。 + +| 存储 | 默认容量 | 保存内容与约束 | +|---|---|---| +| 召唤实例池 | `2 * IEC104_TCP_SERVER_CAPACITY` | 同一Slave的总召及计数量各一个;各实例会话、配置、任务及事件独立 | +| 每实例协议任务池 | 4槽 | 至多一个未终结总召;其他槽仅保存单帧应答,也允许4槽全是应答 | +| 每实例业务事件FIFO | 4槽,最小2槽 | 单目标预留FINISHED位置;多目标终结记录保存在调用方状态区,按需交付 | +| 多站广播状态区 | 调用方提供,每目标站一项 | 保存站引用、身份、计时及进度,不保存点值或数据ASDU | +| 总召数据存储 | 不设置私有批次缓冲 | WAIT不接手数据;OK时发送层已复制到当前APDU | + +编译实现文件时可覆盖`IEC104_INTERROGATION_CAPACITY`、`IEC104_INTERROGATION_TASK_CAPACITY`、`IEC104_INTERROGATION_EVENT_CAPACITY`。增加任务槽只增加短应答积压容量,不允许第二个总召。任务池设为1仍可工作,但当前总召占槽且短应答不能立即提交时,新请求只能丢弃并计数。 + +协议任务只保存`InterrogationResponse`:由`Slave_Get_Event_Identity`取得的原服务/实例/连接代次,加10字节Type100 ASDU。合法请求复制原文并处理应答CA;可可靠应答的畸形单对象请求只保存规范为10字节、VSQ=1的否定应答副本,不执行激活或撤销。错误IOA、QOI、OA、TEST等字段仍完整保留。每次发送临时建立ASDU视图,调用`Slave_Send_Response`或`Slave_Try_Send_Response`;任务不保存完整SlaveEvent或自引用接口,源事件出队后可立即复用。固定任务槽及`order[]`调度规则保持不变,非队首短应答完成后仍立即释放对应槽。 + +2026-10-27按默认4任务槽、4业务事件槽测量,Windows x64 GCC的`sizeof`如下;这是结构存储尺寸,不是STM32的链接占用,Keil结果须另行实测。 + +| 项目 | 精简前 | 精简后 | +|---|---:|---:| +| 每任务请求存储 | 400字节(完整SlaveEvent) | 40字节(原身份及10字节ASDU,含对齐) | +| 每任务 | 480字节 | 120字节 | +| 每实例4个任务合计 | 1920字节 | 480字节 | +| 每个总召实例 | 2400字节 | 960字节 | + +每实例减少1440字节(60%)。任务槽数、业务事件FIFO、TCP通知环、HIGH/NORMAL容量、k窗口及事件覆盖策略均保持原样,节省来自请求表示,不减少背压容量。立即应答也不再在栈上复制完整SlaveEvent;GCC `-O2 -fstack-usage`中该函数从464降至96字节,但这不是ARMCC或完整调用链的实测栈水位。 + +上表保留请求存储精简阶段的比较。当前Windows x64默认配置实测:总召1240字节、每任务168字节、紧凑请求40字节、业务事件80字节、配置40字节。相对逐站完整执行版本的1192字节,管理对象增加48字节;多目标广播另需调用方提供每站48字节的状态区,13站为624字节。单目标不要求状态区。广播共用一个任务及一套应用取数游标,HIGH/NORMAL和任务/事件容量不变。数值不是目标板RAM实测;站点及Slave开销见[共用站点表](IEC104_API接口汇总.md#station-registry)。 + +业务事件是纯值结构,可以复制和跨循环保存。`instance_id`区分静态池槽的历次使用,`request_id`在实例内递增,`batch_index`区分本次取数交互;编号耗尽时拒绝新建,不回绕复用。TCP的`generation`属于另一层身份,防止旧请求向新连接发送。 + +## 3. 请求校验与单总召准入 + +### 3.1 接收范围及校验顺序 + +总召使用`C_IC_NA_1`,Type=100、SQ=0、一个对象、IOA=0。支持激活COT=6和撤销COT=8,请求P/N须为0,CA可为已注册站地址或广播65535(0xFFFF);兼容模式只接受其配置站。实际站CA限于1..65534。QOI=20为全站,21..36为第1..16组;分组内容由应用维护。 + +先校验事件身份,再保护长度并读取类型字节。非100返回`IGNORED`,总召不代替其他业务发送UNKNOWN_TYPE;公共ASDU视图不可用时也返回`IGNORED`,由公共入口处理。正常网络路径的公共头及最低载荷已由Receiver检查,真实截短/帧错误仍沿连接层原策略处理。Type100先判断是否存在唯一完整对象,可靠时按`COT → CA → IOA → 布局/QOI/P/N → 事务状态`处理,避免忙状态掩盖地址或传送原因错误。 + +| 条件 | 结果 | +|---|---| +| Type不是100,或公共视图不可用 | `IGNORED`;公共入口应先过滤不可用视图,再分派其他业务 | +| Type100对象数为0或大于1,无法确定唯一完整IOA/QOI | 计入rejected并返回`HANDLED`,丢弃新请求,不生成应答 | +| Type100单对象完整,但SQ=1或有多余尾字节 | 仅回否定COT=7/9,COT/CA/IOA错误仍优先45/46/47;不执行激活或撤销 | +| COT不是6或8 | 回COT=45,P/N=1 | +| CA既不是本站地址也不是65535 | 回COT=46,P/N=1,保留请求中的错误CA | +| IOA不是0 | 回COT=47,P/N=1 | +| 激活QOI超范围或P/N非法 | 回否定ACT_CON,COT=7、P/N=1 | +| 撤销QOI超范围、P/N非法或没有匹配对象 | 回否定DEACT_CON,COT=9、P/N=1 | +| 合法激活,已有未终结总召 | 回否定ACT_CON,不替换原请求,不建立第二个总召 | +| 合法激活,空闲且有任务槽 | 保存请求、分配request_id,进入START | +| 合法激活,空闲但任务槽被短应答占满 | 尝试否定确认;不能提交或保存时丢弃新请求并计数 | + +确认和错误应答调整COT及P/N;请求CA为65535时,应答CA改为本站地址,其他CA原样保留。OA、TEST、IOA和QOI保持原文,包括错误请求中的原值。畸形单对象的否定副本规范为10字节、VSQ=1,原输入事件不修改;该容错应答是本项目策略,不是将畸形命令视作有效命令。忙拒绝同样使用否定ACT_CON,没有另设“忙”的专用传送原因。 + +上述请求错误不会关闭TCP、取消已有总召、重置原期限或生成其FINISHED(INVALID_REQUEST),其他CA任务继续。否定应答复用原HIGH/FIFO背压与满槽丢弃策略,不扩大任务或缓冲容量。无唯一对象的丢弃只计入rejected;dropped_requests仍专指无法安全提交/保存的容量丢弃。内部服务故障、会话失效及发送失败继续走原失败路径。 + +### 3.2 忙状态的起止点 + +忙状态从合法激活被保存为START开始,早于生成BEGIN和提交肯定ACT_CON。因此,同一批网络通知连续包含多个激活时,只有第一个可建立总召,后续请求立即进入否定应答路径。不能等到应用接受后才设置忙,否则等待应用期间会接入多个总召。 + +忙状态覆盖BEGIN尚未生成、等待应用接受、确认等待发送、等待数据、有效批次未获发送机会及终止等待发送。相同QOI、不同QOI、不同OA或TEST的新激活都不能覆盖原请求。 + +| 终结方式 | 何时释放总召资格 | +|---|---| +| 正常完成 | ACT_TERM被发送层接手,释放任务;不等待TCP写完或主站ACK | +| 应用拒绝BEGIN | 生成FINISHED(REJECTED),原任务转为否定REPLY | +| 肯定确认提交前超时或BEGIN数据源失败 | 终结原因保留为对应TIMEOUT或SOURCE_ERROR,原槽转为否定REPLY,保留TCP及其他任务 | +| 主站匹配撤销 | 结束原业务,原任务转为肯定撤销REPLY | +| 本地Cancel、STOP、断线或确认后失败 | 取消总召及本模块待发短应答,保留应有的FINISHED | + +单站终结并转为REPLY后不再占用总召资格;多目标广播中某站拒绝、超时或定向撤销时,仍为其他站保留总召资格。广播网络流程结束后进入FINISHING,gi_pending为false,但仍占用状态区并忙拒绝新激活,直到全部FINISHED出队。此前被拒绝的请求不会自动启动,主站需要重新发起。 + +### 3.3 广播地址与本站地址 + +广播CA表示请求面向所有站。`IEC104_Types.h`以`IEC104_CA_BROADCAST`定义0xFFFF(65535),它不是实际站地址。开启共用站点表后,多目标广播采用c104的三阶段顺序:先按注册顺序完成各站BEGIN及ACT_CON,再逐站处理全部数据,最后逐站ACT_TERM及FINISHED。例如两站线上顺序为CON1、CON2、DATA1、DATA2、TERM1、TERM2;每站数据可有多批或为空。各站有独立request_id、进度和期限,但共享一个任务和一个取数位置。兼容单站及仅含一个目标的广播保持单目标流程。 + +接收合法多目标广播时,把目标节点引用保存到`config.broadcast_states`;`broadcast_capacity`须覆盖当时的注册站数。数组由调用方持有至Destroy,地址稳定,不得读写或与其他实例重叠;无需逐项初始化。容量不足在任何肯定确认前整轮拒绝,各站收到否定ACT_CON,并累计overloads,不截取部分站执行。单目标不需要此数组,注册表本身仍无固定站数上限。 + +目标集合不冻结IOA或点值。后来新增或同CA重建的节点下轮广播才参加;尚未选入BEGIN便被删除的站跳过。已选中站持原引用完成,尤其已确认但尚未取数的站不能因删除而静默消失;应用通过station_id取得原context。无法继续取数时显式Fail_Request。没有注册站时广播回复COT=46并保留65535。忙或错误广播仍使用共用站点快照遍历实际CA,无槽保存遍历时整体丢弃新请求并计数。 + +每次Process最多推进2倍任务容量步,默认8步;剩余可提交应答通过need_process继续调度。多目标每站状态区保留终结原因,NextEvent按FIFO空位逐个交付,即使13站也只需2槽事件FIFO。发送WAIT不推进当前发送项,仍须重试。确认前拒绝或超时只结束该站并继续确认其他站;确认后取数失败或超时关闭原连接,整轮取消,各已交付BEGIN都有FINISHED。CA选站、QOI选组,两者独立,广播支持20..36。 + +以仅注册CA=7为例(广播多站时逐站应用同一规则): + +| 请求或交互 | CA的处理 | +|---|---| +| 激活或撤销CA=7 | 接受本站寻址,后续应答CA=7 | +| 激活或撤销CA=65535 | 映射到本站7,后续应答CA=7 | +| BEGIN / NEED_DATA / FINISHED | request.common_address均为7,应用无需判断原请求是否广播 | +| Submit_Batch | 必须提供CA=7;CA=65535或其他站地址返回ERROR且不消费当前交互 | +| 肯定/否定ACT_CON、ACT_TERM、肯定/否定DEACT_CON、广播请求的错误应答 | CA均为7,发送等待与确认前失败不改变这条规则 | +| 其他CA且前置校验已通过 | COT=46、P/N=1,保留原错误CA,避免伪装成本站请求 | + +输入的`const SlaveEvent`始终保持原文。Request完整解析Type100后选择实际目标站,广播从首站开始;未知CA保留原值。错误原因仍按COT、CA、IOA、QOI/P/N优先检查。撤销匹配本轮活动站集合,不依赖注册表首站或当前取数位置。 + +内部Copy_Response_Request保存原身份及10字节ASDU,再用临时视图调用Asdu_Set_CA。广播每次切换站点才更新副本CA,同一站的WAIT重试只重建视图、设置COT/P/N并沿用原CA。副本不借用调用方事件,也不重新绑定当前连接。 + +任务仅保留info这一份业务元数据,数据COT、OA、TEST及CA均按当前任务info检查;CA必须等于当前目标站。错误短应答不生成BEGIN。数据批次不自动改写地址,非法批次返回ERROR供应用修正;同一IOA在不同CA下属于不同点。 + +`IEC60870_Asdu_Set_CA`是共用ASDU层的字段接口,按COT宽度定位CA并按CA宽度写入小端字节,支持构造容器和借用可写原文。1字节CA只允许0..255,2字节允许0..65535;参数或容器无效时返回ERROR且不修改原文。它不负责站地址的业务合法性,总召配置仍拒绝0和65535。 + +该规则仅属于Type100处理器。即使CA=65535,Type101等其他有效类型仍返回IGNORED,后续业务模块自行决定其寻址及应答规则。 + +## 4. 协议阶段与完成含义 + +```text +空闲收到合法激活 + → START:保存请求,已占用总召资格 + → 业务事件有空位,生成BEGIN,启动业务计时 + ├─ Reply_Begin(false) → FINISHED(REJECTED) → REPLY(否定ACT_CON) + └─ Reply_Begin(true) → CONFIRM + → 提交肯定ACT_CON成功 → DATA + → NEED_DATA → Submit_Batch → OK → 下一个NEED_DATA + → WAIT → 保留事件和游标,重读当前值后重试 + → NEED_DATA → Complete_Data → TERMINATE + → 提交ACT_TERM成功 → FINISHED(COMPLETED) → 释放任务 + +任何未终结阶段:新激活 → 否定REPLY;原总召阶段与进度不变 +匹配撤销:停止当前业务 → FINISHED(已取出BEGIN时)→ REPLY(肯定DEACT_CON) +START/CONFIRM超时或BEGIN失败:终结当前业务 → REPLY(否定ACT_CON),保留连接 +DATA/TERMINATE超时或NEED_DATA失败:关闭原连接,取消本模块任务,不发成功ACT_TERM +``` + +| 阶段 | 等待或动作 | 推进条件 | +|---|---|---| +| START | 等事件空位生成BEGIN,随后等应用决定 | 单目标需要BEGIN及终结预留;多目标终结位置由状态区保证 | +| CONFIRM | 提交肯定ACT_CON | 多目标继续下一站确认,全部确认完成后开始数据 | +| DATA | 生成NEED_DATA,应用读取当前值并尝试直接提交 | Submit_Batch返回OK才消费交互、增加批次和对象计数 | +| TERMINATE | 提交ACT_TERM | 发送层接手后终结;发送WAIT时仍然忙 | +| REPLY | 一条否定确认、错误应答或撤销确认 | 提交成功即释放,不产生新的取数交互 | +| FINISHING | 多目标网络流程结束,等待应用读完FINISHED | 最后一个终结事件出队后释放任务及节点引用,才接受新总召 | +| IDLE | 状态查询时表示没有协议任务 | 不表示业务FIFO一定为空,仍可能有FINISHED待取 | + +`Reply_Begin / Complete_Data`成功表示应用决定已被模块接手,后续`Process`提交确认或终止。`Submit_Batch`返回OK则表示数据已被发送层接手并确定帧顺序;不再等待Process提交本批。`FINISHED(COMPLETED)`表示ACT_TERM已被发送层接手;这些成功状态均不保证报文写完或被主站确认。发送层可能随后因STOP或连接故障终止传输。 + +## 5. 应用事件与资源生命周期 + +### 5.1 三类业务事件 + +| 事件 | 应用需要做什么 | 应答接口 | +|---|---|---| +| BEGIN | 根据QOI检查数据源并决定是否接受;可延至首个NEED_DATA建立取数游标,不采样整轮数据 | `Reply_Begin(true/false)`;数据源失败可`Fail_Request` | +| NEED_DATA | 应用选择本批IOA与类型,读取当前值和品质后构造非空批次;暂未就绪则保存本事件,继续驱动通信 | `Submit_Batch`、`Complete_Data`、`Fail_Request`三者择一 | +| FINISHED | 停止对应采样工作,按实例/请求编号释放资源 | 无需应答 | + +`NextEvent`主动取出事件;事件入队不会执行应用代码。BEGIN或NEED_DATA只有被取出之后才允许应答。工作事件只发出一次,应用暂未就绪时保存事件,不能反复等待模块发出同一事件。 + +模块核对实例、请求、批次、事件类型、当前阶段和已取出标志。应答时须原样传回事件;实际元数据和发送身份始终使用内部保存的原请求,不依赖应用回传字段构造发送目标。 + +| 应答结果 | 调用方处理 | +|---|---| +| OK=1 | 交互已消费,不要重复应答;输入ASDU可复用 | +| WAIT=0 | 有未读Slave通知,或Submit_Batch暂不能直接发送;保留事件及游标,推进通信、读空通知后重试;数据批次须重新读取当前值组包 | +| STALE=-2 | 事件已失效或不匹配,停止重试;按FINISHED清理资源 | +| ERROR=-1 | 参数或批次不合法等;通常不消费交互,可修正后重试。NEED_DATA阶段Fail_Request关闭失败是例外,任务仍已终结 | + +### 5.2 FINISHED预留 + +单目标生成BEGIN需要两个业务位置:一个用于BEGIN,一个预留给未来FINISHED。NEED_DATA不能占用终结预留,始终保持`实际事件数 + 终结预留数 ≤ 事件容量`。多目标广播改由每站状态区保存终结记录,因此BEGIN只需一个FIFO空位,所有站可以先确认而不扩大FIFO。pending_events包含尚在状态区中等待入队的FINISHED,可能大于FIFO容量。 + +应用取出BEGIN后,无论正常完成、拒绝、撤销、STOP、断线、取消或超时,都恰好产生一次FINISHED。BEGIN尚未生成或尚未取出便取消时,移除工作事件并释放预留,不产生FINISHED,因为应用尚未取得请求。取消同时移除未读NEED_DATA;已经取出的旧事件再应答返回STALE。 + +历史FINISHED保留到应用读取。业务FIFO容量不足时,新的唯一总召可能停留在START等待空位;它已经占用总召资格,但尚未生成BEGIN,不启动业务期限。应用必须及时读完终结事件,不能只等待新的网络通知。默认容量4时可以同时保留旧FINISHED及下一次BEGIN;测试使用最小容量2验证这个边界。 + +多目标广播进入FINISHING后,连续NextEvent即可读完全部终结记录,无需靠Process搬运。应用不能假定BEGIN与FINISHED严格交替。示例在首个NEED_DATA建立唯一游标,Complete_Data返回OK后立即归还该站取数资源;失败/取消则按FINISHED清理仍在取数的站。若应用在BEGIN另行建立每站资源,须按request_id管理并等对应FINISHED清理。 + +## 6. 数据批次与发送路径 + +应用以`Asdu_Create`及信息对象接口构造批次,再调用`Submit_Batch`。模块立即尝试不排队提交,不保存待发批次、应用点表、对象指针或数据源回调。WAIT时输入存储也可复用,但同一业务事件和遍历位置须保留到成功或终结。 + +IOA、数据类型、分组成员和遍历方式均由调用者决定。BEGIN用于检查数据源和判断是否接受,遍历状态可延至首个NEED_DATA建立,不向库登记或冻结完整IOA列表。应用自行保证总召或分组点的遍历完整性,包括点表变化时如何避免漏点、重复点;库不强制点表在整轮期间保持不变。 + +推荐接入方式是在每次NEED_DATA组包时读取本批点的当前值和品质,不默认在BEGIN冻结整轮数值。值与品质的读取一致性由数据源保证;逐批取值不承诺整轮所有点属于同一采样时刻。需要固定时刻快照的应用可以自行实现,但必须另行处理快照与突发上报的顺序,不能把快照当成库提供的一致性保证。 + +总召数据通过`Slave_Try_Send_Response → TcpServer_Try_Send_Response → Connection_Try_Submit_Response → SendManager_Try_Submit_Response`提交,Slave按保存的原身份校验服务实例及连接代次。只有当前发送缓冲空闲、HIGH和NORMAL均无未提交条目、链路和发送窗口允许时,才直接准备I帧并确定N(S)。否则返回WAIT,不把旧值放入HIGH等待。NORMAL中仅保留已提交、待ACK的原文不单独阻挡本批,仍须满足k窗口;不要求等待全部ACK或整轮总召结束。 + +应用须把“更新当前点值并发布突发”与“读取本批当前值并调用Submit_Batch”放在同一串行执行上下文,或用等效同步保证二者不交错。WAIT之后只保留事件和游标,每次重试重新取值:已积压的突发先发送,随后总召读取最新值;总召返回OK后,本帧顺序已固定,后来发布的突发不能插入该帧之前。这同时避免旧NORMAL被新总召越过,以及新突发之后重发WAIT期间缓存的旧总召值。 + +这个保证依赖应用发布顺序:库不比较测点版本,无法识别应用在读取之后才迟到发布的旧采样,也无法修正调用者仍使用BEGIN数值快照的做法。突发发送不因总召被暂停;若HIGH/NORMAL持续有待发条目,总召数据可能持续WAIT,本路径没有强制抢占或最长完成时间。应用应按实际负载安排上报频率和批次,不能通过重试旧数值消除等待。 + +| 检查项 | 要求 | +|---|---| +| 应用层参数 | COT/CA/IOA长度分别为2/2/3 | +| 数据Type | 1、3、5、7、9、11、13、20、21,均为本模块支持的无时标监视信息 | +| 数据COT | 等于原请求QOI:全站20,第1..16组为21..36 | +| CA | 等于配置的本站地址,与业务事件request.common_address一致;不能是65535 | +| OA/TEST | 与原请求一致 | +| P/N | 0 | +| 数量与长度 | 至少1个对象,不超过配置上限及249字节;完整布局有效 | +| SQ及类型切换 | 每批允许SQ=0/1;一个ASDU同一Type,不同批次可切换Type | + +返回OK时发送层已复制本批原文,应用可立即复用输入缓冲并推进自己的游标,`batch_index / objects_submitted`也已增加。返回WAIT时不消费NEED_DATA、不推进计数、不保存原文,也不重复生成NEED_DATA;应用先处理通知,交互仍有效时在通信推进后重读同一遍历位置并再次调用Submit_Batch。发送ERROR若确认为正常STOP导致的拒绝,返回WAIT等待通知,随后终结为STOPPED;其他发送ERROR/FATAL以SEND_ERROR终结并返回STALE,不能重试原事件。参数或批次校验ERROR则不消费交互,允许修正。 + +空数据集或已经遍历完毕使用`Complete_Data`,不能用空ASDU代替结束。未配置分组可以在BEGIN时拒绝;接受的空组仍按ACT_CON → ACT_TERM正常结束。累计量数据不在当前总召批次范围,留给总电度业务。模块不检查IOA是否属于请求的分组,分组筛选和取值由应用负责。 + +确认、终止和短应答沿原请求connection走普通HIGH应答接口;数据沿同一connection走不排队提交接口,不使用NORMAL,也不进入HIGH等待。只有上一批返回OK后才能完成数据阶段,因此终止不会越过尚未接手的数据。已接手帧按通信层规则发送,不能被新应答越过,也不能通过Cancel撤回。 + +## 7. 短应答调度与满槽处理 + +### 7.1 Handle_Event中的提前提交 + +忙拒绝、地址或COT错误、非法QOI、撤销确认等只需要单帧应答,先尝试提交现有HIGH接口。提前提交必须同时满足:没有未读Slave通知、原会话仍可发送、没有更早形成的REPLY。 + +```text +形成短应答 + → 有旧REPLY或未读Slave通知:暂不提交 + → 可安全提交:Copy_Response_Request复制请求并写入已确定的应答CA,调整COT/P/N,调用原HIGH接口 + ├─ 已接手:累计submitted,否定时累计negative_submitted,不占新任务槽 + ├─ WAIT:未接手,保留/保存为REPLY + ├─ ERROR且确认为正常STOP拒绝:保留/保存为REPLY,先交付停用通知 + └─ 实际提交错误:按SEND_ERROR等失败路径处理 + → 需要保存但池已满:只丢弃新请求,增加overloads和dropped_requests +``` + +立即提交不修改调用方事件,不增加绕过通信队列的公开模式。发送成功后本地副本即可释放;WAIT后如需保存任务,以已确定的应答CA建立稳定请求副本,后续只按阶段设置COT/P/N并重试提交。实际连接或发送错误仍按失败路径处理,不能当作普通满槽丢弃。 + +### 7.2 Process中的选择规则 + +单帧应答按形成顺序FIFO提交。调度器分别查找唯一总召和最早REPLY,不要求总召位于任务索引队首。总召正在等应用或业务事件空位时,直接处理REPLY;双方都可推进时轮转,避免总召一直挡住短应答,也避免短应答持续占据所有推进机会。 + +Send_Response内部会推进保留的接收帧并交付通知。Process每步检查FIFO;发送返回值还须区分以下情况,Process、立即应答和Submit_Batch共用同一STOP判定: + +| 发送返回值 | 后续处理 | +|---|---| +| OK | 按已接手事实记账/推进,下一次提交前检查通知;不撤回或重发本帧 | +| WAIT | 保留任务并让出;先处理通知,再按原背压规则恢复 | +| ERROR且确认为正常STOP拒绝 | 设置events_pending并让出,保留任务/业务事件,由DEACTIVATED终结为STOPPED | +| 其他ERROR或FATAL | 保留SEND_ERROR失败路径;FATAL可能已写出部分或整帧,不能转成可重试WAIT | + +只有ERROR分支额外查询状态:请求须仍属于原服务/实例/代次,TCP仍连接,会话ACTIVE且无故障、队列状态有效,传输已停用并且通知未读,才按正常STOP拒绝让出。不能仅凭FIFO非空忽略错误。成功路径不增加查询;正常STOP拒绝不累计waits、不推进游标、不停止或刷新取数期限。应用先读取Slave通知,旧NEED_DATA随后返回STALE;立即应答保存的短任务也由STOP清理。 + +Process提交确认、终止或短应答遇到WAIT就结束本轮,保存请求和阶段,下次优先检查最早REPLY。每一步都重新检查Slave通知,发送过程中产生的新通知也必须先处理。不会跨过未读STOP/START或撤销继续发送旧事务。数据WAIT由持有NEED_DATA的应用在通信推进后重读并重试。 + +`Process(gi)`没有预算参数,也不内部等待:当前站REPLY或终止成功后继续广播或释放任务;DATA只生成NEED_DATA。没有就绪任务、发送WAIT、有未读Slave通知或已推进2倍任务容量步时返回;剩余本地工作由need_process提示续跑。每步发送前重查通知,不执行应用代码。 + +### 7.3 4个槽的含义 + +任务池的容量与“同时允许几个总召”是两个独立约束。总召资格上限恒为1,任务池还为无法立即发送的短应答提供存储。忙应答占槽并不代表原激活已被保存,发完否定确认后即释放,不会再生成该请求的BEGIN。 + +满槽时保留已有任务、应用事件及进度,不主动关闭TCP,不挤掉最早应答。只有新的、无法安全提交或保存的请求被丢弃。`EVENT_HANDLED`包括这种丢弃;应用不能把它当成“主站已经收到应答”,也不能再次投递同一Slave通知。`dropped_requests`用于观察这个情况,主站可能等待超时或自行重试。 + +## 8. 业务期限与驱动 + +内部`Task_Timeout`按同一时刻汇总最近剩余时间、超时原因和已确认广播站的总期限到期标志。`Get_Status`读取该结果安排唤醒,`Expire`据此选择拒绝当前站或关闭原连接,不再二次遍历广播站或重新取时判定。结果只在本次调用中使用,不在任务内保存第二份期限状态。 + +| 配置 | 默认值 | 起止点 | +|---|---|---| +| `begin_timeout_ms` | 15000ms | BEGIN入业务FIFO至应用接受、拒绝或报告失败,包含未取事件的时间 | +| `data_timeout_ms` | 15000ms | 每次NEED_DATA入队至首次有效批次尝试、声明结束或报告失败,每批重新计时 | +| `total_timeout_ms` | 0(默认禁用) | 启用后从BEGIN生成至ACT_TERM被发送层接手,覆盖取数、事件容量等待和发送等待,不等待主站ACK | + +三项默认值是本项目的可配置业务策略,不是IEC104规定的总召时限,也不代替链路t0..t3。每项0表示禁用。执行总期限默认关闭,避免慢链路的累计发送等待触发固定整体截止;BEGIN和每批取数期限仍各为15000ms,底层单帧发送、确认和链路探测期限继续有效。需要限制整次总召耗时时,可显式将`total_timeout_ms`设为非零。没有`queue_timeout_ms`;START尚未生成BEGIN时不计业务期限,但此时仍拒绝后续总召。及时消费业务事件是应用的驱动义务,模块不会通过后台线程保证进度。 + +所有期限使用Net提供的64位单调毫秒时钟,以经过时间比较,避免校时和绝对截止相加溢出。0毫秒有效。业务应答停止对应阶段期限,下一次NEED_DATA重新计时;显式总期限在同一站内不因新批次、忙应答或重试而延长。每站从自己的BEGIN独立计时,包含等待其他站确认、取数及终止的时间;已确认但不在当前取数位置的站也会被检查,到期关闭原连接并取消整轮。没有额外跨全部CA的整体定时器,同一时刻两项到点优先报告TOTAL_TIMEOUT。 + +有效批次因发送背压返回WAIT时置`data_send_waiting=true`,说明应用已响应,本批取数计时停止,之后不再恢复,下一批NEED_DATA才重新计时。无效批次或因未读Slave通知而提前返回WAIT不停止取数计时。发送等待仍受底层链路期限及显式启用的总期限约束;模块不替应用重读或重试数据,调用方必须持续驱动保留的事件。 + +Process和有效应用应答都会检查期限。先消费Slave通知,再判断超时,使已经到达的STOP、撤销或断线优先终结事务。到期后旧交互失效,迟到应答返回STALE;具体处理按肯定ACT_CON是否已被发送层接手划分: + +| 失败发生的阶段 | 处理方式 | 连接及其他任务 | +|---|---|---| +| START:BEGIN超时、总超时或Fail_Request | 终结当前业务,复用原槽保存否定ACT_CON;保留BEGIN_TIMEOUT、TOTAL_TIMEOUT或SOURCE_ERROR | 保留TCP及已有短应答 | +| CONFIRM:肯定ACT_CON尚未提交成功,总期限到点 | 转为否定ACT_CON,终结原因仍为TOTAL_TIMEOUT | 保留TCP及已有短应答 | +| DATA / TERMINATE:取数超时、总超时或NEED_DATA阶段Fail_Request | 沿原请求关闭连接,取消本模块任务,不补发成功ACT_TERM | 原连接其他业务也会受影响,不关闭后来复用服务槽的新连接 | + +分界是发送层接手肯定ACT_CON,不是应用调用Reply_Begin(true),也不是TCP写完或主站ACK。CONFIRM提交返回WAIT时仍可以拒绝激活;肯定确认即使仅进入HIGH或当前发送缓冲,也已经进入DATA,不能再改发否定确认。确认后的断链是本项目当前执行失败策略,不增加按超时类型区分的配置开关。 + +内部Reject_Request统一处理应用主动拒绝、BEGIN失败和确认前超时,Fail_Interrogation按阶段选择拒绝请求或关闭连接。不能直接从超时处理调用公开Reply_Begin(false):BEGIN可能尚未取出,该接口的校验也会再次检查期限。内部转换先Finish,再将原槽改为REPLY并移到应答顺序末尾;无需空闲任务槽,保留原QOI/OA/TEST,应答CA使用本站地址,旧工作事件立即失效。BEGIN已取出才产生FINISHED,具体失败原因不会统一改成REJECTED。 + +否定确认继续按现有调度发送,WAIT保留重试,不再计算已终结请求的业务期限。Process检查到确认前超时后会重新检查Slave通知,在本轮继续推进短应答;应用应答接口检查到超时则返回STALE,调用方仍须驱动Process。失败转换完成即释放总召资格,但原槽要到否定确认提交或取消后才归还。主站可以重新发起请求,模块不自动重放旧请求。 + +等待前查询`Get_Status`。若`timer_active=true`,以`timeout_remaining_ms`限制下一次Poll或应用等待的上限,0表示立即推进;false表示当前没有启用的业务期限,不代表业务已经结束。状态查询只读,不触发发送、终结或超时。 + +`need_process=true`表示应立即调用总召Process:例如新请求有空位生成BEGIN、业务应答接手后可提交下一帧、读取旧FINISHED后解除事件容量等待,或业务期限已到期。该值按Process同一选择规则计算,既考虑总召,也考虑排队的短应答;它不是`!waiting`,也不保证发送层一定接手。当前帧提交WAIT后暂停本地续跑,避免空转;每次Slave_Poll/Process推进网络并读空Slave通知后,仍须调用总召Process重试,不能以此标志为false跳过网络进展。 + +应用持有的数据WAIT也须在上述网络推进后重试,不能只等need_process置位或新的NEED_DATA。`data_send_waiting`表示已交付有效数据但等待发送机会;`application_waiting`只表示仍等待应用首次有效应答,两者都不是发送就绪查询。 + +Get_Status实时检查Slave FIFO;`events_pending=true`时`need_process=false`,先处理可能存在的STOP、撤销或断线。`pending_events`表示另一条业务FIFO,应及时读取;每次读取事件或调用应答接口后重新查询状态。外部事件循环还须合并Slave_Prepare的等待条件,总召的标志不替代TCP的need_process及链路期限。 + +## 9. 撤销、取消与连接变化 + +主站撤销为Type100、COT=8,在忙拒绝判断之前按原连接代次、目标CA、OA、TEST和QOI匹配,包括已选中但BEGIN尚未生成的START。定向撤销匹配站,其他站继续;已确认但尚未取数、数据已交完等终止、或已删除的原站均可匹配。尚未选入BEGIN的未来站没有活动事务。广播撤销停止整轮,对每个仍活动的站发送肯定COT=9,不向已结束或尚未启动的未来站补发确认。 + +撤销确认组复用原任务槽,排在已有短应答之后;即使任务池满也能撤销。当前站待发的否定ACT_CON不会被其他站的撤销确认覆盖。一组撤销确认尚未提交完时,新的重叠撤销按未匹配拒绝;不能安全保存否定回复时按既有满槽策略计数丢弃。已被发送层接手的帧不回滚,不再为被撤销站发送ACT_TERM。 + +匹配成功后,先通过Copy_Response_Request保存撤销请求并写入应答CA,使用原激活的info和进度生成FINISHED(DEACTIVATED),随后把原槽转换为肯定DEACT_CON。转换不需要额外任务槽,池满仍可撤销;形成的确认排在已有REPLY之后,条件允许时立即提交。已被发送层接手的帧继续在确认前按原顺序发送,本地未提交批次丢弃,不再提交原总召ACT_TERM。 + +| 操作或通知 | 本模块行为 | 网络确认 | +|---|---|---| +| 匹配COT=8 | 停止当前总召,旧工作事件失效,必要时FINISHED(DEACTIVATED) | 肯定COT=9 | +| 不匹配或重复撤销 | 当前总召保持原状 | 否定COT=9 | +| `Cancel` | 取消总召及待发短应答,保留必要的FINISHED(CANCELLED) | 不自动发送撤销确认或ACT_TERM | +| STOPDT对应DEACTIVATED通知 | 停止本模块任务,必要时FINISHED(STOPPED) | 链路STOP确认由通信层负责 | +| DISCONNECTED | 取消任务、清除已处理代次,必要时FINISHED(DISCONNECTED) | 无业务成功确认 | +| 新CONNECTED或会话已失效 | 旧任务终结为CONNECTION_CHANGED,不跨代次重放 | 后续新激活重新建立事务 | + +COT=8的总召撤销与Slave的DEACTIVATED通知不是同一事件:前者结束一个业务,后者表示STOPDT停用。FINISHED表示应用可以清理资源,不承诺撤销确认已发送完成。 + +非法批次返回ERROR并保留交互,应用可以修正;数据源确实不能继续则调用`Fail_Request`。BEGIN阶段回否定确认并保留连接,NEED_DATA阶段关闭原连接,两者都终结为SOURCE_ERROR。确认后失败仍可能影响同连接其他业务。接受BEGIN前应检查数据源和必要业务资源,减少确认后才发现无法执行的情况;无需预先采样或编码全部数据,仍可逐批交付。 + +## 10. 应用接入与退出顺序 + +创建前使用`Config_Default`初始化配置,再按站地址、单批大小和实际业务耗时调整。公开API共12个,调用功能如下: + +| 接口(省略IEC104_Interrogation_前缀) | 功能 | +|---|---| +| Config_Default | 填写CA=1、单批127对象及三项默认业务期限 | +| Create | 复制配置、借用Slave,取得静态池实例 | +| Destroy | 任务及业务事件全部为空时归还实例,不关闭Slave | +| Cancel | 本地取消任务并保留应有终结事件 | +| Handle_Event | 校验并接收应用转交的Slave通知,处理准入、短应答及会话变化 | +| Process | 自动推进就绪任务,遇等待条件返回 | +| NextEvent | 主动读取业务事件,只有READY写出结果 | +| Reply_Begin | 应答已取出的BEGIN,明确接受或拒绝 | +| Submit_Batch | 校验当前NEED_DATA对应的非空ASDU,立即尝试不排队提交;WAIT须重读数据后重试 | +| Complete_Data | 声明数据耗尽,后续Process提交ACT_TERM | +| Fail_Request | 报告数据源失败;BEGIN转否定确认并保留连接,NEED_DATA关闭原连接,均保留SOURCE_ERROR | +| Get_Status | 查询进度、累计计数及最近业务期限,不产生副作用 | + +```text +Slave_Create / Config_Default / Init + → Interrogation_Config_Default / Create + → 循环 + 1. 驱动Slave,逐条读取并转交连接通知及请求 + 2. Slave通知未读空时继续处理通知 + 3. Interrogation_Process + 4. Interrogation_NextEvent,处理BEGIN / NEED_DATA / FINISHED + 5. 应用数据就绪时显式应答,继续推进 + 6. 查询Slave等待条件及Interrogation的need_process、事件和期限,再安排等待 + → Interrogation_Cancel + → 读完业务事件并清理FINISHED对应资源 + → Slave_Close,读空收尾通知 + → Interrogation_Destroy + → Slave_Destroy +``` + +网络驱动可以使用Slave的Poll,也可以接入应用自己的Prepare/Process事件循环。数据暂未就绪不能停止驱动网络;应用也不能在忙等待循环中反复调用Process而不处理事件。 + +`Destroy`不会隐式取消或丢弃事件。即使没有活动总召,只要还有REPLY或未读FINISHED就返回false;调用方完成清理后再销毁。实例创建失败要清理已创建的Slave,不能继续使用NULL句柄。 + +## 11. 状态与统计 + +| 字段 | 解释 | +|---|---| +| generation / active | 最近已处理的连接代次和启用状态,不能代替实时会话查询 | +| gi_pending | 总召网络流程未终结;FINISHING虽为false,状态区清理前仍忙拒绝新激活 | +| phase | 当前站阶段,收尾为FINISHING;仅有应答为REPLY,无任务为IDLE | +| pending_requests | 总召、FINISHING及短应答占用的任务槽数,不是等待执行的总召数 | +| pending_events | FIFO事件及状态区中待交付的FINISHED,不包含预留位置 | +| waiting / application_waiting | 存在发送、应用或事件容量等待;后者表示仍等待应用首次有效应答 | +| data_send_waiting | 当前批次已有效尝试但发送WAIT;不保存原文,应用须保留事件、重读数据并重试 | +| events_pending | 当前Slave通知尚未读空,应先处理网络通知 | +| need_process | 当前有需立即推进的本地工作、到期期限或失效会话;发送WAIT后等待网络推进再重试 | +| timer_active / timeout_remaining_ms | 是否启用业务期限及距最近期限的剩余毫秒 | +| batch_index / objects_submitted | 当前总召已提交的批次和对象数;仅有REPLY时保留最早应答的历史进度 | +| submitted / gi_submitted | 被发送层接手的ASDU数 / ACT_TERM提交成功的总召次数 | +| waits | 实际提交返回发送WAIT的次数,不统计应用等待循环次数 | +| rejected | 校验、忙、应用决定、确认前失败和容量原因拒绝的请求数,含无法可靠应答而丢弃的畸形请求 | +| negative_submitted | 发送层已接手的否定应答数,不等于主站已收到 | +| overloads / dropped_requests | 任务池满或广播状态区不足的次数 / 无法安全提交或保存的新请求丢弃数 | +| cancelled | 取消的协议任务数,包含待发REPLY;不等于FINISHED数量 | +| deactivated / timeouts | 成功匹配主站撤销次数 / 执行业务超时次数 | +| last_finish_reason | 最近合法总召的终结原因,BEGIN尚未交付时取消也会更新 | + +状态为独立副本,累计计数从Create开始,Cancel和重连不清零。`waiting=true`不代表所有任务都无法推进:总召等数据时仍可提交短应答。`gi_pending=false`也不表示可以直接Destroy,仍须检查待发应答及业务事件。 + +确认前超时增加timeouts及rejected,BEGIN阶段Fail_Request增加rejected;两者都不增加cancelled或gi_submitted。只有否定确认被发送层接手才增加negative_submitted。REPLY等待期间反复Process不会重复累计超时或交付FINISHED,last_finish_reason保留原终结原因。 + +## 12. 总电度扩展与设计边界 + +Type100与Type101分别使用GeneralInterrogation和CounterInterrogation只读规则,共用引擎通过创建时传入的规则执行解析、限定词和数据检查以及完成方式选择,不引用具体种类符号。总召旧Create名称保留;计数量使用CounterInterrogation_Create。同一Slave每种规则至多一个实例;不能复用总召的gi_pending作为全局准入开关。 + +计数量RQT=1..5、FRZ=0..3;读取接受Type15/37,COT为37..41。冻结/复位通过Execute_Begin先检查请求再执行一次同步动作,肯定ACT_CON提交后完成,不产生NEED_DATA或ACT_TERM。TEST动作拒绝;已执行动作不会因超时转为否定,也不因断线回滚。请求结构追加freeze/data_cause,总召分别为0/QOI。完整约定见[计数量设计](IEC104_CounterInterrogation_设计架构.md)。 + +接入多个业务处理器时,连接、启停和断线通知应广播给每个相关模块,不能因某个模块返回HANDLED就停止广播;I帧按业务类型分派,未接手的交给后续处理器,最后再由应用兜底。总召模块不实现总电度冻结、复位或累计量点表。 + +多个业务可以分别持有任务,但仍共享连接、HIGH容量和发送窗口,应用需要持续驱动各模块。业务忙相互独立不代表发送时延相互独立;STOP、断线和任一业务触发的连接关闭会影响同连接所有业务。 + +本设计参考了[lib60870-C多客户端示例](https://github.com/mz-automation/lib60870/blob/master/lib60870-C/examples/multi_client_server/multi_client_server.c)按连接限制活动总召、忙时及会话槽满时否定确认的做法。该示例仅处理QOI=20,本项目保留20..36分组支持,使用应用主动读取事件、固定请求副本和明确的WAIT重试。其会话槽保存不同连接的活动总召,不能理解为同一连接的后续总召队列。 + +业务期限、静态容量和忙拒绝是本项目的实现策略。当前不提供多QOI待执行队列,不自动替主站重试,也不通过应用回调获得下一批数据。若现场要求保存连续分组请求,需要单独定义准入、主站确认等待和取消语义,不能仅扩大任务槽数量。 + +## 13. 验证与调用方迁移 + +总召基线回归98组,计数量另有12组,合计110组,使用真实Slave/TCP提交路径及可控单调时钟;HIGH缩到251字节、业务FIFO缩到2槽。覆盖原有突发排序、部分写入、取数期限、k=1窗口及到期ACK;多站覆盖13站三阶段顺序、非空混合批次、状态区不足与跨实例重叠、已确认未来站删除/同CA重建、超出FIFO容量的无损FINISHED、各阶段定向及满槽广播撤销、拒绝与撤销同时等待、非当前站总期限,以及总期限禁用、BEGIN先到期和两项同时到期的处理优先级。畸形请求覆盖否定/丢弃边界、输入不变、基础截短仍被Receiver拒绝、原任务/期限保持、背压满槽和广播撤销隔离。提交期STOP覆盖三个入口、真实通知容量背压、FATAL与STOP并存、非STOP的ERROR、OK/WAIT伴随新回执及START后重新总召。真实TCP回环验证命令行单站,使用Windows后端和k=1逐帧核对业务字节。 + +| 验证范围 | 应满足的结果 | +|---|---| +| 全站及全部16组 | QOI、数据COT、OA、TEST及点对象验证正确 | +| 广播全站及全部16组 | 业务事件、确认、数据、终止均用本站CA;输入Slave事件保持原文 | +| 广播错误、忙拒绝及确认前失败 | 副本建立时使用本站CA,立即提交和等待重试均保持该地址;其他错误CA仍按原值回显 | +| 广播数据地址 | Submit_Batch拒绝65535及其他站地址,应用修正为本站CA后可重试 | +| BEGIN生成前后、确认/数据/终止等待 | 相同和不同QOI的新激活均否定,原身份和进度不变 | +| 完成后重新发起 | 不自动启动被拒绝请求;主站重发后产生新request_id并获得完整期限 | +| 总电度类型隔离 | 总召空闲和忙时,本站或广播CA的Type101均返回IGNORED,不产生总召否定应答 | +| 短应答及满槽 | 安全时提前提交,积压保持FIFO;满槽只丢新请求,旧工作事件仍有效 | +| 远方撤销 | 本站/广播激活与本站/广播撤销四种组合均匹配实际站CA,其他身份仍严格检查;满槽仍可复用原槽 | +| 业务FIFO满 | FINISHED保留;BEGIN前不计业务期限,读出旧FINISHED后才能启动当前请求 | +| 本地续跑 | 事件空位恢复及业务应答后need_process置位;发送WAIT时不空转,网络推进后继续发送;未读Slave通知优先 | +| 三项执行期限 | 迟到应答不能绕过期限,忙应答不刷新总期限,先处理已到达的STOP/撤销 | +| 确认前失败 | 未读BEGIN超时不交付FINISHED;已交付时恰好终结一次,否定确认保留QOI/OA/TEST并使用本站CA | +| 满槽及发送等待期间失败 | 原槽转REPLY,不挤掉旧应答;按形成顺序发送,保留TCP,同连接重试可以完成 | +| 肯定确认提交边界 | CONFIRM发送WAIT时超时回否定确认;确认已接手但尚未写出时,取数失败及超时仍关闭原连接 | +| STOP、断线及实例复用 | 旧请求不能跨会话发送或接受迟到应用应答 | +| 真实TCP连续分组请求 | 第一个完成,后续得到否定确认;结束后重发的请求分别正常完成 | +| 真实TCP广播流程 | 全站、分组、忙拒绝、非法QOI及撤销均返回本站CA;连接继续工作 | + +从先前多QOI排队版本迁移时,删除`queue_timeout_ms`、`queued_interrogations`、`queue_timeouts`字段访问,以及`QUEUED / FINISH_QUEUE_TIMEOUT`分支。所有调用方须重新编译并以Config_Default初始化配置。`Process`继续只接收实例,不增加预算参数;原12个总召接口保留,其中Create实现迁至GeneralInterrogation.c,工程须加入此源文件。计数量另增2个入口。`FINISH_OVERLOAD`仅保留旧枚举值,当前满槽不以此终结已有总召。 + +状态新增`need_process`和`data_send_waiting`,所有调用方须与库一起重新编译。原来用`pending_requests != 0 && !waiting`决定是否立即续跑的代码应改为读取need_process,并保留网络推进后调用总召Process的步骤。Submit_Batch现立即尝试提交,OK时进度已推进;WAIT时只保留业务事件和游标,每次重试须重新读取当前值,不能继续复用旧构造结果。Reply_Begin和Complete_Data仍由Process发送后续确认或终止。 + +请求存储精简只改变内部表示,总召公开函数、业务事件及配置字段不变。Slave新增按身份发送、尝试发送和关闭原连接的接口;总召内部使用,调用方继续按原方式驱动总召即可。完整SlaveEvent及Copy_Event保留原语义,其他业务无需迁移。 + +确认前失败修复不新增接口、配置字段或枚举值。调用方收到BEGIN阶段Fail_Request的OK或超时后的STALE时,不应再假定TCP已经断开,须继续处理FINISHED并驱动Process发送否定确认。 + +早期单站广播支持新增`IEC60870_Asdu_Set_CA`,当时未改变总召配置及事件布局。当前多CA版本新增use_station_registry与request.station_id,所有调用方须重新编译。新应用启用站点表,按事件实际CA构造数据,不把65535填入数据ASDU;BEGIN获取引用,FINISHED归还。Remove的OK已改为立即退出路由,释放内存须另查Is_Reusable,原先“Remove成功即可释放”的调用代码必须迁移。 + +广播发送顺序与[c104 Server.cpp](https://github.com/Fraunhofer-FIT-DIEN/iec104-python/blob/main/src/Server.cpp)一致:全部站确认、逐站数据、全部站终止。c104使用shared_ptr;这里用调用方节点、显式引用及轻量状态区,并保留可重试WAIT、业务事件和各站明确收尾。仍只允许一轮总召,不引入每站独立发送任务或动态发送队列。新增broadcast_states/broadcast_capacity及FINISHING,调用方须重新编译;启用多目标广播前配置状态区,并适配多个BEGIN先于首个FINISHED的事件顺序。 + +地址处理重构新增语义常量`IEC104_CA_BROADCAST`,将地址准备集中到内部副本创建,并移除任务中重复的header。公开函数签名和事件字段保持原样,应用无需增加调用。59组可控网络及真实TCP回归继续检查确认/数据/终止WAIT、错误地址、原事件不变、广播/本站交叉撤销和确认前失败,避免内部重构改变协议行为。 + +测试验证本模块的功能和边界,不代表已通过正式协议一致性认证。现场仍须确定主站对否定确认的处理、各分组点表、采样一致性及业务期限取值。 diff --git a/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_Link_设计架构.md b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_Link_设计架构.md new file mode 100644 index 00000000..77a5a9ca --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_Link_设计架构.md @@ -0,0 +1,1037 @@ +# IEC104_Link 公共、主站与子站设计架构 + +> **用途**:说明公共链路、主站启停、子站启停三个模块的职责,以及连接角色、I/S序号、窗口、确认、TESTFR和完成结果模型。 +> **范围**:第一至八章描述链路算法,第九章记录设计决策和变更。单当前I帧及独立有界S/U FIFO已实现,见 [发送缓冲设计与接入](IEC104_发送模块_设计架构.md#sendbuffer);当前仍不具备完整 TCP 连接管理或定时器调度。104 Codec的APCI解析与构帧、公共Codec的ASDU解析及Receiver独立成帧见 [编解码与接收器设计](IEC104_设计架构.md)。 +> **实现依据**:`IEC104_Link.c`、`IEC104_Link_Master_StartStop.c`、`IEC104_Link_Slave_StartStop.c`及配套头文件,以及 `IEC104_Codec.c/h` 基础编码器。 +> **同步日期**:2026-09-17。本文描述实现契约;历史验证不代表当前全部行为已覆盖。2026-10-19已清理接口过期的旧回归测试,当前测试入口及范围见[测试说明](../test/IEC104测试文档.md)。 +> **阅读入口**:[模块地图与调用顺序](README.md)。`IEC104_Link.h`现在是兼容总头文件,按角色阅读可直接打开Master或Slave模块。 + +--- + +## 一、模块速览 + +### 1.1 总体职责 + +公共模块 `IEC104_Link.c` 与两个角色模块共同组成单条IEC104连接的链路状态算法层。调用方为每条连接持有一个 `IEC104_LinkState_t`,三个模块在该对象上完成: + +- 固定本端在该连接上的控制站或被控站角色; +- 管理本端发送方向的 `V(S)/V(A)` 和接收方向的 `V(R)/acked_nr`; +- 校验收到的 I 帧 `N(S)` 连续性和 I/S 帧 `N(R)` 合法性; +- 判断 `k` 发送窗口以及 I 帧确认的 `t1`; +- 按 `w` 或 `t2` 生成 S 确认,并保存实际上线报文的 `N(R)` 快照; +- 以固定角色执行 STARTDT/STOPDT,禁止同一连接主动与被动交叉; +- 对两种角色提供独立 TESTFR 主动探测和被动应答; +- 校验状态字段和跨字段组合,通过完成结果向连接层报告致命错误。 + +典型调用关系为: + +```text +连接创建 + → IEC104_Link_State_Init(state, role) + → APCI解析结果进入 I/S/U 对应链路接口 + → Prepare/Handle 生成报文并进入发送半态 + → 传输层完成整帧发送 + → Complete_* 提交发送结果 + → 收到对端确认后推进 V(A) 或结束 U 帧过程 +``` + +### 1.2 当前不负责的内容 + +本模块当前不包含: + +- TCP 建连、监听、连接、断连、重连、故障状态和套接字读写; +- 接收字节流缓存、完整 APDU 交付和 U 帧自动分派; +- 发送队列及传输调度;独立 `IEC104_SendBuffer` 提供唯一当前I帧、紧凑S/U FIFO及部分写入;SendManager提供发送前编号、ACK释放ASDU原文、独立回执和I帧t1; +- 系统时钟读取、定时器创建和超时后的自动断连; +- 按监视方向/控制方向决定具体 ASDU 是否允许发送; +- `t0` 建连超时; +- ASDU 业务语义、Type ID、信息对象和应用服务处理; +- 多线程同步和连接级锁。 + +成功构帧的Prepare/Handle写入调用方缓冲并按接口约定更新状态。被控站START受理即激活、STOP受理即停用,CON完成不回写启停状态。所有接口均不执行网络发送;时间接口使用调用方传入的时间差,不读取系统时钟。 + +### 1.3 文件地图与依赖 + +| 文件 | 职责 | +|---|---| +| `IEC104_Link.c`、`IEC104_Link_Common.h` | 公共类型、初始化、完整状态校验、I/S序号、窗口、S确认、TESTFR及公共超时判定 | +| `IEC104_Link_Master_StartStop.c/h` | 主站ACT发起、CON接收及启停等待超时 | +| `IEC104_Link_Slave_StartStop.c/h` | 子站ACT受理、CON生成和停止确认等待 | +| `IEC104_Link_Internal.h` | 公共模块提供的跨文件确认及校验声明 | +| `IEC104_Link.h` | 保留旧包含方式的公开总入口 | +| `IEC104_Codec.c/h` | 104 APCI解析、I/S/U无状态构帧及帧类型 | +| `IEC60870_Codec.c/h` | 101/104公共ASDU头解析与参数检查,供Receiver或业务调用 | +| `IEC104_Receiver.c/h` | 每连接独立分帧、APCI校验和完整帧视图;不私有保存Link状态 | +| `IEC104.c/h` | 已恢复的旧单路逐字节接收入口,持有私有全局Link状态并预设STARTED | +| `IEC104_SendBuffer.c/h` | 子站唯一当前I帧、独立紧凑S/U FIFO、部分写入及发送期限;不保存协议序号边界 | +| `iec104_tcp_event_ring_test.c` | 当前TCP事件字节环专项,使用可控Net后端;非完整链路算法测试 | + +依赖方向为: + +```text +主站调用方 → IEC104_Link_Master_StartStop.h → IEC104_Link_Common.h → IEC104_Codec.h +子站调用方 → IEC104_Link_Slave_StartStop.h → IEC104_Link_Common.h +旧调用方 → IEC104_Link.h(汇总以上三个公开头文件) + +Master_StartStop.c / Slave_StartStop.c → Link.c(通过Internal.h调用公共校验与确认) +三个链路实现 → IEC104_Build_Iframe/Sframe/Uframe(IEC104_Codec.c) +连接分派层 → Receiver.Peek_Frame → SendManager(子站I/S)或相应U帧处理入口 +IEC104_Receiver.c / IEC104_SendBuffer.c → IEC104_Parse_APCI(IEC104_Codec.c) +IEC104_SendBuffer.h → IEC104_Link_Common.h(共享发送类型);SendBuffer不调用Link实现 +IEC104_Codec.c → C标准库,不依赖Link、SendBuffer、日志或Receiver +``` + +主站文件不包含子站头文件,子站文件不包含主站头文件。完整状态校验集中在 `IEC104_Link.c`:先检查公共基础字段和S状态,再按固定角色调用本文件内的 `static` 过程校验。角色处理入口通过公共辅助函数完成检查,公共模块不再反向依赖角色实现;末级过程校验不反调完整校验,避免递归。STOPDT准备分别在主站和子站实现,不再通过一个带role参数的函数选择ACT或CON。链路模块均不依赖日志模块。 + +本轮删除子站CON发送半态、两个pending字段及暂存START准备接口,调用方统一重新编译。控制站启停及TESTFR流程不变。构建时包含Link和Codec,再按角色加入对应StartStop实现;Receiver独立组帧。发送FIFO、帧身份及完成事件串行顺序由接入层保证,I帧ACK不得越过本地完成边界。 + +### 1.4 使用硬约束 + +以下为链路接口的使用约束。涉及连接关闭、事件隔离和上下文生命周期的要求由外部接入方实现;当前模块不提供连接管理或事件调度器。 + +1. 一个 `IEC104_LinkState_t` 只属于一条 TCP 连接的一端,不能被两个连接共享。 +2. 新连接处理任何链路事件前,必须成功调用 `IEC104_Link_State_Init()` 初始化完整链路状态并指定角色;不能依赖全零对象表示有效链路,也不要求等 TCP 建连成功后才初始化。 +3. 一条连接建立后角色不切换。需要切换主/子站用途时,应结束旧连接并隔离其遗留事件,再以新角色初始化新连接;上下文内存可在满足第 7.4 节的隔离和生命周期要求后复用。 +4. 同一进程可以同时持有多个状态对象,分别作为控制站和被控站;角色是“每连接属性”,不是编译期或进程级属性。 +5. 同一连接的接收、构帧、发送完成和超时事件必须串行分派,或由调用方在外部加锁;模块内部没有锁。 +6. `Prepare/Handle` 成功生成报文不等于发送成功;调用方必须等待最终累计写入结果,再调用匹配的 `Complete_*`。TESTFR_CON 没有链路 Complete 接口,最终长度和发送失败由连接层处理。 +7. 固定 6 字节 U/S 帧只有 `sent_len == IEC104_APCI_LEN` 才算完整成功;部分写入尚未结束时不得提前调用完成接口。 +8. I/S 报文必须按上线顺序提交确认快照,不能让携带较旧 `N(R)` 的报文晚于较新确认上线。 +9. 完成接口返回 `IEC104_LINK_RESULT_FATAL` 或 t1 到期时,上层连接上下文必须关闭当前 TCP 连接;链路状态本身不重复保存连接故障。 +10. 新连接初始化后必须丢弃旧连接遗留的接收事件、超时事件和发送完成回调,不能把旧事件交给新状态对象。 + +### 1.5 状态归属:一套上下文不等于双方共用序号 + +每个通信端点都拥有自己的连接上下文。单个上下文中同时保存本端发送方向和本端接收方向的状态: + +| 本端字段 | 所属方向 | 含义 | +|---|---|---| +| `vs` | 本端 → 对端 | 下一帧尚未分配给发送队列 I 帧的 `N(S)` | +| `sent_vs` | 本端 → 对端 | 下一个待提交本地发送完成事件的 I 帧 `N(S)` | +| `va` | 本端 → 对端 | 对端已经确认到的本端发送位置 | +| `vr` | 对端 → 本端 | 本端期望收到的下一帧 I 帧 `N(S)` | +| `acked_nr` | 对端 → 本端 | 本端最近一次成功上线的接收确认号 `N(R)` | + +假设连接两端为 A、B,则逻辑对应关系为: + +```text +A.vs / A.va ──由 B 发出的 N(R) 确认──> B.vr / B.acked_nr +B.vs / B.va ──由 A 发出的 N(R) 确认──> A.vr / A.acked_nr +``` + +这是两个独立上下文之间的协议对应关系,不是共享内存,也不是四个字段始终相等。报文可能在途、确认可能延迟,因此对应值只会随协议事件逐步收敛。 + +### 1.6 I/S与启停的内部边界 + +I/S是否允许收发仍由当前角色和启停状态决定。相关规则集中在 `IEC104_Link.c` 的同一组私有判断函数中,调用方先完成状态校验,再按当前事件查询,不额外缓存收发权限: + +| 私有判断 | 调用位置 | 负责的启停规则 | +|---|---|---| +| `I_Frame_Receive_Allowed()` | `Handle_I_Frame()` | 被控站仅STARTED/IDLE开放I帧接收,STOP受理立即关闭 | +| `New_I_Send_Allowed()` | `Plan_I_Frame()` | 是否允许预留新I帧;保留控制站ACT之后提前发送和停止期间停止新增预留的规则 | +| `S_Frame_Receive_Allowed()` | `Handle_S_Frame()` | STARTED或被控站STOPPED/STOP_DRAINING允许S确认 | +| `Dt_Allows_S_Ack()` | `Plan_S_Ack()` | 被控站STARTED或STOP_DRAINING允许普通S,STOPPED/IDLE禁止;START受理后不等待CON完成,实际发送遵守已有控制FIFO顺序 | +| `Dt_Requires_Immediate_Ack()` | `Plan_S_Ack()` | STOP_REQUESTED、STOP_ACT_SEND_PENDING、STOP_CON_WAITING、STOP_DRAINING四个阶段要求绕过w门限 | + +这些判断只表达启停规则;返回允许后,I/S路径仍负责序号合法性、窗口、在途S、待确认数量和快照提交。各方向的允许条件不同,不合并成一个统一开关。两个角色的启停模块继续负责状态转换和各自的停止排空顺序,公共链路不反向调用角色实现。 + +启停取消不再需要清理暂存标志。S完成只维护自身确认快照,保留当前启停状态。 + +--- + +### 1.7 连接层使用的只读查询 + +Connection通过以下接口决定是否向Manager请求控制帧,避免在连接层重新组合Link的过程、S状态和序号字段: + +| 查询 | 与实际Prepare共用的判断 | 范围 | +|---|---|---| +| `IEC104_Link_Can_Prepare_S_Ack(state,w,immediate)` | `Plan_S_Ack` | 两端;首次CON门控、在途S、待确认数量、w或立即确认 | +| `IEC104_Link_Can_Prepare_Testfr_Act(state)` | `Testfr_Act_Allowed` | 两端;当前是否已有主动探测,不判断t3时间 | +| `IEC104_Link_Can_Prepare_Stopdt_Response(state)` | `Stopdt_Response_Status` | 子站;停止排空中是否可生成S或CON | + + +这三个查询先校验完整状态及角色,返回1可准备、0无需求或仍需等待、-1非法。不写状态、不预留存储;实际Prepare仍负责构帧并返回相应等待原因。查询结果不保证发送空间可用。 + +`IEC104_Link_Get_Status(state,status)`返回角色、启停/探测状态及`unconfirmed_rx_count`。1表示成功,0表示参数或状态非法;失败不改输出。Connection用未确认接收数量维护t2区间,用枚举值提供诊断展示,控制动作通过上述查询判断。原始Link结构仍用于静态分配,字段由Link维护。 + +## 二、角色、配置与状态模型 + +### 2.1 固定连接角色 + +| 枚举 | 本端职责 | STARTDT/STOPDT 权限 | +|---|---|---| +| `IEC104_ROLE_INVALID` | 未初始化或角色损坏 | 正常收发、完成及时间判定入口拒绝;State_Init 可指定有效角色重新初始化,State_Reset 只保留 INVALID 标记 | +| `IEC104_ROLE_CONTROLLING` | 控制站 | 生成 ACT、提交 ACT 发送结果、接收 CON | +| `IEC104_ROLE_CONTROLLED` | 被控站 | 接收 ACT、生成 CON、提交 CON 发送结果 | + +角色决定启停专用入口及I/S门控。控制站启动期间可提前预留I帧;被控站START受理后立即开放收发准备,由FIFO保证CON先于随后生成的数据写出。两端复用序号、确认和窗口算法;TESTFR与启停独立。 + +固定角色使以下组合在结构层面消失: + +- 控制站一边等待对端的 STARTDT_CON,一边应答对端 STARTDT_ACT; +- 被控站一边发送 STOPDT_CON,一边主动等待对端 STOPDT_CON; +- 为主动/被动启停分别保存两套半态和一致性标志。 + +### 2.2 默认链路配置 + +`IEC104_Link_Config_Default()` 写入: + +| 参数 | 默认值 | 当前模块用途 | +|---|---:|---| +| `k` | 12 | `IEC104_Link_Prepare_I_Frame()` 允许预留的最大发送未确认帧数 | +| `w` | 8 | `IEC104_Link_Prepare_S_Ack()` 的接收确认门限 | +| `t1_ms` | 15000 | I 帧确认和 STARTDT/STOPDT/TESTFR CON 等待边界 | +| `t2_ms` | 10000 | 未达到 `w` 时延迟 S 确认的边界 | +| `t3_ms` | 20000 | 空闲后发起 TESTFR 的边界 | + +配置函数只复制数值,不验证 `t2 < t1` 等跨参数策略,也不创建定时器。调用方可以在初始化后覆盖这些默认值。 + +### 2.3 链路状态字段 + +```c +typedef struct { + IEC104_LinkRole_t role; + + uint16_t vs; + uint16_t sent_vs; + uint16_t va; + uint16_t vr; + uint16_t acked_nr; + + IEC104_SAckState_t s_ack_state; + uint16_t s_ack_nr; + + IEC104_DataTransferState_t dt_state; + IEC104_DtProcedure_t dt_procedure; + + IEC104_TestfrState_t testfr_state; + + +} IEC104_LinkState_t; +``` + +| 字段 | 初始值 | 含义 | +|---|---:|---| +| `role` | 由初始化参数决定 | 当前 TCP 连接上本端的固定角色 | +| `vs` | 0 | 下一帧尚未分配给本端发送队列 I 帧的 `N(S)` | +| `sent_vs` | 0 | 下一个尚未提交发送完成事件的 I 帧 `N(S)` | +| `va` | 0 | 本端发送方向最早尚未获确认的位置 | +| `vr` | 0 | 下一帧期望收到的 `N(S)` | +| `acked_nr` | 0 | 本端最近成功上线的 `N(R)` | +| `s_ack_state` | `IEC104_S_ACK_IDLE` | 公共 S 确认是否等待最终发送结果 | +| `s_ack_nr` | 0 | 已生成 S 帧实际携带的 `N(R)` 快照 | +| `dt_state` | `IEC104_DT_STOPPED` | 已经提交的稳定数据传输状态 | +| `dt_procedure` | `IEC104_DT_PROC_IDLE` | 当前唯一 STARTDT/STOPDT 半态 | +| `testfr_state` | `IEC104_TESTFR_IDLE` | 本端主动 TESTFR 过程 | + + +`IEC104_IFrameTx_t` 是一帧I报文的不可变N(S)/N(R)/长度快照,最终发送结束后原样交回Complete。子站SendManager在实际发送前才选业务并准备唯一当前I帧,完整发送并提交后复用APDU存储;独立记录保存身份、序号、原始发送时刻和业务关联。AsduQueue的HIGH保存当前连接的请求应答,空间不足拒绝,I帧提交且发送记录建立后原文立即出队;NORMAL保存可跨连接恢复的事件,原文保留到ACK,空间不足时覆盖最旧原文,包括已提交未ACK条目。当前APDU及发送记录独立保留,原文出队或覆盖不改变序号、本地完成、ACK、k窗口或期限。有效ACK核减Manager的HIGH未确认计数,释放NORMAL仍保留的原文或核减覆盖待确认数;业务回执仍需显式消费。断线时业务显式Rebind到新代次,清空HIGH,将NORMAL现存原文按原顺序恢复未提交,已覆盖原文不能恢复。统一使用Enqueue,不再提供逐条重连策略。Manager仅在STARTED+IDLE时接手应答,其余阶段返回ERROR且不入HIGH。Manager初始化成功、受理新STOPDT_ACT及从STOPPED受理STARTDT_ACT时清空待发HIGH;STOP受理时尚在HIGH的应答即使已返回OK也作废,无逐条回执。已启用状态下重复START的CON保留HIGH及公平配额,WAIT时不清理;已提交帧及记录不受清理影响。见 [发送模块的累计ACK处理](IEC104_发送模块_设计架构.md#manager-ack)。 + +### 2.4 稳定状态与过程状态分离 + +`dt_state` 只取两个稳定值: + +- `IEC104_DT_STOPPED` +- `IEC104_DT_STARTED` + +控制站ACT发送及等待CON的进度由dt_procedure表示;被控站只保留停止确认等待,CON本地发送进度归SendBuffer。 + +`STARTED` 也不单独保证当前允许收发 I 帧;还须检查过程半态。接收方向与新发送预留的差异见 3.2 节门控表。 + +控制站允许的过程及稳定状态约束: + +| `dt_procedure` | 要求的 `dt_state` | 含义 | +|---|---|---| +| `IEC104_DT_PROC_IDLE` | STARTED 或 STOPPED | 没有启停过程 | +| `IEC104_DT_PROC_START_ACT_SEND_PENDING` | STOPPED | STARTDT_ACT 已构造,等待最终发送结果 | +| `IEC104_DT_PROC_START_CON_WAITING` | STOPPED | ACT 已完整上线,等待 STARTDT_CON | +| `IEC104_DT_PROC_STOP_REQUESTED` | STARTED | 已受理主动停止,准备 S 确认和 ACT;禁止新业务 I 帧 | +| `IEC104_DT_PROC_STOP_ACT_SEND_PENDING` | STARTED | STOPDT_ACT 已构造,等待最终发送结果 | +| `IEC104_DT_PROC_STOP_CON_WAITING` | STARTED | ACT 已完整上线,等待 STOPDT_CON | + +被控站使用以下三个逻辑状态: + +| dt_state / dt_procedure | 含义 | +|---|---| +| STOPPED / IDLE | 停用,CON本地发送进度另由SendBuffer维护 | +| STARTED / IDLE | 启用,可接收I/S并准备新的业务I帧 | +| STOPPED / STOP_DRAINING | 已停用,等待确认后生成STOP_CON | + +START受理立即启用,STOP受理立即停用;Link不再保存子站CON发送半态或相反请求暂存标志。旧CON完成不能覆盖后续请求已经生效的状态。控制站使用的过程枚举和处理顺序保持不变。 + +### 2.5 公共 S 确认状态 + +| 状态 | `s_ack_nr` 约束 | 含义 | +|---|---|---| +| `IEC104_S_ACK_IDLE` | 必须为 0 | 当前没有 S 帧等待发送结果 | +| `IEC104_S_ACK_SEND_PENDING` | `0..32767` | S 帧已构造,保存构帧时 `V(R)` | + +普通 `w/t2` 确认、控制站主动停止准备和被控站停止排空共用这一状态,不维护 `stop_response` 或 `stop_ack_nr` 等停止专用副本。 + +### 2.6 初始化与复位 + +`IEC104_Link_State_Init(state, role)`: + +- 只接受非空状态和两个有效角色; +- 成功返回 `1`,将全部链路运行字段置为初始值; +- 参数非法返回 `0`;角色非法且 `state` 非空时保持原对象不变; +- 新连接处理任何链路事件前,须用本接口成功初始化完整状态并指定角色;不能以 Reset 代替,也不要求等 TCP 建连成功后才调用。 + +同一程序同时承担两种角色时,应使用两个独立连接对象: + +```c +IEC104_LinkState_t upstream; +IEC104_LinkState_t downstream; + +IEC104_Link_State_Init(&upstream, IEC104_ROLE_CONTROLLED); +IEC104_Link_State_Init(&downstream, IEC104_ROLE_CONTROLLING); +``` + +两个对象的序号、启停和探测状态完全独立;TCP 故障状态由各自所属的连接上下文管理。 + +`IEC104_Link_State_Reset(state)`: + +- 空指针直接返回; +- 保留有效的 CONTROLLING/CONTROLLED 角色; +- 非法角色归一为 `IEC104_ROLE_INVALID`; +- 序号和S确认快照清零,S确认、启停过程和TESTFR置IDLE; +- 稳定状态回到 STOPPED。 + +Reset 会读取原对象的 `role`,因此对象必须是已经初始化的状态对象或具有确定存储内容的对象;不能对未初始化的自动变量调用 Reset。Init 和 Reset 都只写链路算法字段,不关闭 TCP,也不清理外部缓冲、任务或定时器。状态内存复用和遗留事件隔离要求见第 7.4 节。 + +### 2.7 只读发送进度与初始状态查询 + +`IEC104_Link_Get_Send_Progress(state,&progress)` 先校验完整链路状态,成功返回1并写入 `IEC104_IFrameSendProgress_t` 数值快照;空指针或状态非法返回0,输出保持原值。三个字段为: + +| 字段 | 含义 | 当前内部来源 | +|---|---|---| +| `next_frame_ns` | 下一帧可分配的发送序号 | `vs` | +| `next_completion_ns` | 下一个尚未提交本地发送完成的I帧序号 | `sent_vs` | +| `peer_ack_nr` | 已接受的对端累计确认位置 | `va` | + +此快照与每帧固定的 `IEC104_IFrameTx_t` 不同:它描述调用时整条链路的发送进度,不对应某一个报文,也不提交事件或预留序号。确认位置不得越过本地完成位置,按循环距离判断,不能用普通整数大小比较回绕前后的序号。 + +`IEC104_Link_Is_Initial_State(state)` 完整校验后检查所有序号与确认位置为0、STOPPED、启停/S确认/TESTFR均空闲且两个暂存标志均为0,满足返回1,否则返回0。它负责维护初始条件,使发送管理器不必枚举链路内部字段;返回1不证明TCP新建或旧事件已经隔离。 + +两接口均只读,输出对象不得与链路存储重叠。子站SendManager通过这些公开接口查询,不使用 `IEC104_Link_Internal.h`;初始化在初始状态检查成功后,额外核对固定角色字段role必须为CONTROLLED。SendBuffer不再复制Link的三个序号位置。Manager在整个事务结束后核对当前I帧、已提交ASDU数量和记录;Prepare/Commit、Complete/完成提交及Handle/原文释放中间不检查一致性。查询不创建锁、定时器或TCP连接管理。 + +--- + +## 三、I/S 序号、窗口与确认 + +### 3.1 15 位序号环 + +序号范围为 `0..32767`,掩码为 `IEC104_SEQUENCE_MASK == 0x7FFF`: + +```text +0 → 1 → ... → 32766 → 32767 → 0 +``` + +循环距离使用: + +```c +(to - from) & IEC104_SEQUENCE_MASK +``` + +接收和发送两侧都禁止相关区间占满整个 15 位环,否则“空区间”和“满区间”都会表现为首尾相等。实际代码均在推进状态前检查: + +- 接收 I 帧:计算 `next_receive_number = Number_Increment(state->vr)`;若 `next_receive_number == state->acked_nr`,返回 `IEC104_LINK_RESULT_FATAL`,不修改 `va/vr`。 +- 预留 I 帧的未确认区间:计算 `outstanding_count = Number_Distance(state->va, state->vs)`;若 `outstanding_count >= k`,返回 `IEC104_PLAN_SKIPPED`。结合已检查的 `k <= 32767`,保证新增一帧后区间仍不占满一圈,代码没有另写 `next_send_number == state->va` 判断。 +- 预留 I 帧的未完成区间:计算 `next_send_number = Number_Increment(state->vs)`;若 `next_send_number == state->sent_vs`,返回 `IEC104_PLAN_SKIPPED`。完整执行顺序见 3.4 节。 + +`k` 直接限制发送预留;`w` 是触发接收确认的门限,不是拒收 I 帧的窗口上限。接收确认是否及时完成由调用方调度,序号环检查继续保护未确认接收区间。 + +I/S 接收共用 `Acknowledge_Number_Valid(state, receive_number)` 校验对端的 `N(R)`。先拒绝 `receive_number > IEC104_SEQUENCE_MASK`,随后按实际局部变量计算: + +```c +outstanding_count = Number_Distance(state->va, state->sent_vs); +acknowledged_count = Number_Distance(state->va, receive_number); + +/* Prepare只预留序号;必须先提交对应Complete,才允许ACK覆盖该帧。 */ +return acknowledged_count <= outstanding_count; +``` + +通过该检查表示确认落在 `va..sent_vs` 已完成的循环区间内;重复确认 `va` 合法。仅Prepare/入队的帧不能被确认,失败不修改状态。异步接入若先取得相关ACK事件,应先取得并提交对应发送结果,再按接收顺序调用Handle;不能用ACK推定发送成功。 + +### 3.2 接收 I 帧 + +`IEC104_Link_Handle_I_Frame()` 在修改状态前统一检查: + +1. `IEC104_Link_State_Valid_Internal(state)` 返回真,且 `apci != NULL`; +2. `I_Frame_Receive_Allowed(state)` 返回真,具体由稳定状态和过程半态共同决定,见下表; +3. `apci->frame_type == IEC104_FRAME_TYPE_I`; +4. `apci->send_number == state->vr`;状态校验已保证 `vr` 为 15 位,因此不再单独重复范围检查; +5. `Acknowledge_Number_Valid(state, apci->receive_number)` 返回真,计算方式见 3.1 节; +6. 计算 `next_receive_number = Number_Increment(state->vr)`,并确认其不等于 `state->acked_nr`。 + +全部通过后一次性提交: + +```text +va = 收到的 N(R) +vr = vr + 1(15位回绕) +``` + +本模块实现每条连接的收发方向门控,支持控制站在STARTDT_ACT之后、收到STARTDT_CON之前下发命令或设定值。这个功能不要求增加主备连接或冗余切换机制,初始或已经确认停止的连接也不能据此直接发送I/S帧。 + +标准适用条件:IEC 60870-5-104:2006 第5.3节允许上述提前发送;当前模块不实现连接组选择/切换,多个独立连接对象不自动构成冗余组。 + +合法状态下的 I 帧门控如下。表中“发送”仅指生成新的业务 I 帧;已经预留的 FIFO 报文仍须按序完成。“允许接收”仍须通过序号和窗口边界校验。 + +| 本端角色 | 稳定状态 / 过程 | 接收 I 帧 | 预留新 I 帧 | +|---|---|---|---| +| 两端 | STARTED / IDLE | 允许 | 允许,继续检查 k 窗口和序号环 | +| 两端 | STOPPED / IDLE | FATAL | 返回 0 | +| 控制站 | STOPPED / START_ACT_SEND_PENDING | FATAL | 允许预留,必须排在本次ACT后,继续检查k和序号环 | +| 控制站 | STOPPED / START_CON_WAITING | FATAL | 允许,继续检查k和序号环,不必等待CON | +| 控制站 | STARTED / STOP_REQUESTED、STOP_ACT_SEND_PENDING、STOP_CON_WAITING | 允许,并立即安排S确认 | 返回0 | +| 被控站 | STOPPED / STOP_DRAINING | FATAL,不交付业务 | 返回0 | + +被控站仅STARTED/IDLE接收I帧;STOP受理后拒绝新I帧,后到START成功受理后重新开放。拒绝不推进va/vr,也不处理该帧捎带的ACK。新的本端I/S报文按FIFO排在已有CON之后,是否可以写出由发送存储和窗口决定。 + +控制站的提前发送不改变其稳定状态,只有匹配CON才能提交STARTED。控制站停止期间继续接收并确认对端在途I帧;本端主动停止禁止预留新的命令I帧。 + +调用方必须先取得ACT队列节点并成功入队,再预留提前I帧;先发送ACT并提交其Complete,再发送后续I帧。ACT失败或启动t1超时后丢弃连接,不回滚vs、不再发送。 + +重复、跳号、超前确认或状态损坏同样返回 `IEC104_LINK_RESULT_FATAL`,不发生部分更新。链路模块不缓存乱序帧,也不自动重传。 + +### 3.3 接收 S 帧 + +`IEC104_Link_Handle_S_Frame()` 只处理对本端发送方向的确认: + +```text +收到合法 N(R) → va = N(R) +``` + +入口依次检查完整链路状态、`apci != NULL`、`apci->frame_type == IEC104_FRAME_TYPE_S`、`S_Frame_Receive_Allowed(state)` 及 `Acknowledge_Number_Valid(state, apci->receive_number)`;任一失败返回 FATAL,全部通过后执行 `state->va = apci->receive_number`。 + +S接收要求STARTED,或本端为被控站且处于STOPPED/STOP_DRAINING。初始及停止完成的STOPPED/IDLE拒绝S;控制站等待START_CON时仍拒绝S。接收S不会产生新的接收数据,故与I帧门控分开。 + +合法范围由从 `va` 到 `sent_vs` 的循环距离判断,允许重复确认当前位置,但不能确认本端尚未提交发送完成的序号。S 帧不改变 `vr`、`acked_nr` 或启停状态;任何检查失败不发生部分更新。 + +### 3.4 I 帧预留与 k 窗口 + +以下按 [IEC104_Link.c](../../104/IEC104_Link.c) 中的函数、变量名和执行顺序说明。`IEC104_Link_Prepare_I_Frame()` 是公开入口,内部依次完成序号规划、编码和提交;入口先检查 ASDU 及输出参数。这里的“原子”指失败时不留下部分提交的链路状态:返回 `0/-1` 时不修改 `state`、调用方的 `tx` 或 `frame`。同一连接仍须串行调用,函数内部不提供锁。 + +#### 3.4.1 公开入口的参数检查 + +`IEC104_Link_Prepare_I_Frame()` 先检查以下条件,任一不满足即返回 `-1`: + +- `asdu != NULL`,且 `asdu_len` 位于 `IEC104_ASDU_HEADER_LEN..IEC104_MAX_ASDU_LEN`,当前为 `6..249`; +- `frame != NULL`,且 `capacity >= IEC104_APCI_LEN + asdu_len`; +- 调用方用于保存最终发送快照的 `tx != NULL`。 + +随后用栈上的临时对象 `planned_tx` 调用规划函数: + +```c +planned = Plan_I_Frame(state, k, &planned_tx); +if (planned != IEC104_PLAN_READY) { + return planned == IEC104_PLAN_SKIPPED ? 0 : -1; +} +``` + +因此,规划结果 `IEC104_PLAN_SKIPPED` 对外返回 `0`,`IEC104_PLAN_ERROR` 对外返回 `-1`,只有 `IEC104_PLAN_READY` 继续编码。`Plan_I_Frame()` 的 `tx` 参数指向这个临时 `planned_tx`,此时尚未写入调用方的最终快照。 + +#### 3.4.2 Plan_I_Frame 的检查和序号规划 + +`Plan_I_Frame()` 按以下顺序执行: + +1. 调用 `IEC104_Link_State_Valid_Internal(state)` 检查链路状态;同时检查 `k == 0U || k > IEC104_SEQUENCE_MASK`。状态非法或 `k` 超出 `1..32767` 时返回 `IEC104_PLAN_ERROR`。 +2. 调用 `New_I_Send_Allowed(state)` 检查本端是否允许预留新 I 帧;返回假时,规划返回 `IEC104_PLAN_SKIPPED`。 +3. 计算 `outstanding_count` 并检查 k 窗口。 +4. 计算 `next_send_number` 并检查未完成区间的序号环边界。 +5. 将当前 `vs`、`vr` 写入临时序号快照,返回 `IEC104_PLAN_READY`。 + +第 2 步的 `New_I_Send_Allowed()` 返回布尔值,合法状态下的判断及后续处理如下。表中省略枚举前缀 `IEC104_DT_` 和 `IEC104_DT_PROC_`: + +| 本端角色 | `dt_state` | `dt_procedure` | 门控返回值 | 后续处理 | +|---|---|---|---|---| +| 控制站或被控站 | STARTED | IDLE | `1` | 继续检查 k 窗口和序号环 | +| 控制站 | STOPPED | START_ACT_SEND_PENDING 或 START_CON_WAITING | `1` | 继续检查 k 窗口和序号环;新 I 帧必须排在本次已入队的 STARTDT_ACT 后面 | +| 任一角色的其他合法组合 | — | — | `0` | `Plan_I_Frame()` 返回 `IEC104_PLAN_SKIPPED`,公开入口返回 `0` | + +门控禁止初始 STOPPED/IDLE 和各 STOPDT 过程中的新 I 帧预留。被控站 START 受理即进入 STARTED/IDLE,Link 允许准备新 I 帧,发送存储负责把它排在已有 CON 之后。控制站两个启动阶段允许提前预留,具体方向规则见 3.2 节。 + +第 3、4 步对应以下实际代码: + +```c +outstanding_count = Number_Distance(state->va, state->vs); +if (outstanding_count >= k) { + return IEC104_PLAN_SKIPPED; +} + +next_send_number = Number_Increment(state->vs); +/* 未完成区间包含在未确认区间内;保留显式检查,避免完成边界的回绕歧义。 */ +if (next_send_number == state->sent_vs) { + return IEC104_PLAN_SKIPPED; +} +``` + +其中 `Number_Distance(from, to)` 计算 `(to - from) & IEC104_SEQUENCE_MASK`,`Number_Increment(value)` 计算 `(value + 1U) & IEC104_SEQUENCE_MASK`;`IEC104_SEQUENCE_MASK` 为 `0x7FFFU`。两个区间的边界分别由以下代码保证: + +| 受保护区间 | 实际检查 | 为什么新增后不会占满 32768 | +|---|---|---| +| `va` 到 `vs`:已预留、尚未被对端确认 | `outstanding_count >= k` 时拒绝 | 通过时 `outstanding_count < k`,结合 `k <= 32767`,新增后最多为 `k`,仍小于 32768 | +| `sent_vs` 到 `vs`:已预留、尚未提交本地完成 | `next_send_number == state->sent_vs` 时拒绝 | 在区间尚未占满一圈的合法状态下,递增后追上 `sent_vs`,就表示新增后将占满一圈 | + +第二项直接比较循环递增后的边界。在上述合法状态前提下,通过条件 `next_send_number != state->sent_vs` 等价于数学条件 `Number_Distance(state->sent_vs, state->vs) + 1 < 32768`。例如 `sent_vs=0、vs=32767` 时,递增结果为 `0`,本次预留被拒绝。 + +`Plan_I_Frame()` 实际使用的局部变量是 `outstanding_count` 和 `next_send_number`。 +所有检查通过后,规划函数只填写临时快照: + +```c +tx->send_number = state->vs; +tx->receive_number = state->vr; +tx->frame_length = 0U; +return IEC104_PLAN_READY; +``` + +该阶段不访问 APDU 输出缓冲,也不修改链路状态;`frame_length` 等编码成功后填写。 + +#### 3.4.3 编码、提交及入队约定 + +公开入口使用计划中的序号调用 [IEC104_Codec.c](../../104/IEC104_Codec.c) 中的无状态编码函数: + +```c +length = IEC104_Build_Iframe(frame, + planned_tx.send_number, + planned_tx.receive_number, + asdu, (int)asdu_len); +if (length < 0) { + return -1; +} + +/* 编码成功后才一次性提交状态和发送队列快照。 */ +planned_tx.frame_length = (uint16_t)length; +*tx = planned_tx; +state->vs = Number_Increment(planned_tx.send_number); +return length; +``` + +`IEC104_Build_Iframe()` 先校验参数,再将序号和 ASDU 编码为 APDU 字节。编码成功后,公开入口填写完整帧长、复制最终快照并推进 `vs`;本次 Prepare 不修改 `sent_vs` 或 `va`。连续预编码的多帧因而取得不同的 `N(S)`,已构造和已排队帧立即占用 k 窗口。 + +接口成功意味着这份序号预留不可撤销。连接调度层应先取得发送队列节点,再调用本接口,并把成功生成的报文无失败地追加到同一连接的严格 FIFO;若此后无法入队或后续发送失败,必须关闭当前连接,不能回滚 `vs` 或复用 `N(S)`。控制站提前预留时,STARTDT_ACT 须已成功入队;发送和完成提交都须先 ACT、后 I 帧,ACT 失败时丢弃连接及后续队列。 + +#### 3.4.4 Complete_I_Send 的本地发送完成提交 + +`Send` 明确表示提交的是本地发送结果。返回 `IEC104_LINK_RESULT_OK` 表示本地完整发送的结果已成功提交;对端是否确认由接收 I/S 帧时推进的 `va` 表示。尚未被对端确认的已完成帧仍须接受 I 帧确认 t1 的超时判断,已经确认的本帧无需再等待确认。 + +I 帧最终发送结束后,调用方把 Prepare 阶段保存的完整快照和最终累计发送长度交回完成接口;部分写入仍在继续时不调用: + +```c +IEC104_Link_Complete_I_Send(state, &tx, sent_len); +``` + +`IEC104_Link_Complete_I_Send()` 按以下顺序执行: + +| 顺序 | 代码中的检查或操作 | 结果 | +|---|---|---| +| 1 | 校验链路状态、`tx != NULL`、快照序号均不大于 `IEC104_SEQUENCE_MASK`、`tx->frame_length` 位于 `IEC104_APCI_LEN + IEC104_ASDU_HEADER_LEN` 到 `IEC104_APCI_LEN + IEC104_MAX_ASDU_LEN`(当前 `12..255`),以及 `sent_len == (int)tx->frame_length` | 任一失败返回 `IEC104_LINK_RESULT_FATAL`,状态不变 | +| 2 | 检查 `state->sent_vs == state->vs` 或 `tx->send_number != state->sent_vs` | 无待完成 I 帧或完成次序不符时返回 `IEC104_LINK_RESULT_FATAL`,状态不变 | +| 3 | 调用 `Commit_Sent_NR(state, tx->receive_number)` | 合法时提交本帧实际携带的 `N(R)` 到 `acked_nr`;快照非法则直接返回失败,状态不变;区间规则见 3.5 节 | +| 4 | 执行 `state->sent_vs = Number_Increment(tx->send_number)` | 返回 `IEC104_LINK_RESULT_OK`,完成本帧的本地提交 | + +完成接口不再次推进 `vs`,也不修改由对端确认推进的 `va`;本接口没有 `IGNORED` 结果。 + +例如连续预留 `N(S)=0、1` 后,`vs=2、sent_vs=0`;此时`N(R)=1`或`2`均被拒绝。先完成0号帧后`sent_vs=1`,允许确认到`N(R)=1`,仍拒绝覆盖1号帧的`N(R)=2`。1号帧的Complete成功后才允许确认到2。三个位置分别记录预留、本地完成提交和对端确认,ACK不能代替本地发送结果。 + +### 3.5 已发确认位置与快照 + +`acked_nr` 记录已经通过本地 I/S 完成接口提交的实际发送 `N(R)` 快照;它不会随 `vr` 增长而自动推进。`IEC104_Link_Unconfirmed_Rx_Internal(state)` 返回未确认接收数量,实际计算为: + +```c +return Number_Distance(state->acked_nr, state->vr); +``` + +报文排队后仍可能收到新 I 帧,因此发送完成时不能直接把 `acked_nr` 设置为最新 `vr`。必须提交报文构造时实际写入的快照。 + +I 帧完成事件携带构帧快照,调用 +`IEC104_Link_Complete_I_Send(state, tx, sent_len)`,普通 S 帧调用 +`IEC104_Link_Complete_S_Ack_Send(state, sent_len)`。二者内部共用私有的 +`Commit_Sent_NR(state, receive_number)`,该内部函数只负责校验并提交实际 +上线报文携带的 `N(R)` 快照: + +- I 帧先校验长度、`N(S)==sent_vs` 和待完成区间;S 帧检查公共 S 在途状态与长度,发送不完整时返回 `IEC104_LINK_RESULT_FATAL`; +- 快照位于当前 `acked_nr..vr` 时,私有提交函数推进 `acked_nr`; +- 快照越界、倒退或越过 `vr` 时返回 `IEC104_LINK_RESULT_FATAL`,不推进确认; +- 参数或链路状态非法同样返回 `IEC104_LINK_RESULT_FATAL`; +- I 帧完成接口没有可忽略的“无在途事件”状态,普通 S 帧没有等待完成时返回 + `IEC104_LINK_RESULT_IGNORED`。 + +`Commit_Sent_NR()` 的实际顺序是:先拒绝大于 `IEC104_SEQUENCE_MASK` 的确认号,再计算 `acknowledged_count = Number_Distance(state->acked_nr, receive_number)` 和 `outstanding_count = IEC104_Link_Unconfirmed_Rx_Internal(state)`。若 `acknowledged_count > outstanding_count`,返回 FATAL;否则执行 `state->acked_nr = receive_number` 并返回 OK。此处使用循环距离,不能用普通整数大小代替区间判断。 + +S 帧完成接口没有报文 ID 或 `tx` 参数,只使用状态内的单份 `s_ack_nr`。它不能识别“旧 S 完成事件误投给下一份 S 帧”,调用方必须按 FIFO 恰好提交一次,并隔离旧连接事件。这是当前接口边界,不能把“同一时刻只有一个 S 帧在途”当作完成事件已绑定报文。 + +成功必须精确比较 `IEC104_LINK_RESULT_OK`,不能把完成结果当布尔值使用,因为 `FATAL == -1` 在 C 中也为真。 + +### 3.6 普通 S 确认 + +`IEC104_Link_Prepare_S_Ack()` 先检查完整链路状态、`w` 位于 `1..32767`、`response != NULL` 及 `capacity >= IEC104_APCI_LEN`,非法时返回 `-1`。通过后调用 `Prepare_S_Ack_Validated(state, w, immediate, response)`,由其先执行 `Plan_S_Ack()`,仅在结果为 `IEC104_PLAN_READY` 时调用 `IEC104_Link_Prepare_S_Ack_Internal()`;规划跳过时公开入口返回 `0`。 + +`Plan_S_Ack()` 使用的数量变量为 `unconfirmed_count`,检查顺序如下。表中过程名省略 `IEC104_DT_PROC_` 前缀: + +| 顺序 | 实际检查或计算 | 结果 | +|---|---|---| +| 1 | `!Dt_Allows_S_Ack(state)`,即被控站处于STOPPED/IDLE | 返回 `IEC104_PLAN_SKIPPED`,停用且无停止确认义务 | +| 2 | `state->s_ack_state == IEC104_S_ACK_SEND_PENDING` | 返回 `IEC104_PLAN_SKIPPED` | +| 3 | `unconfirmed_count = IEC104_Link_Unconfirmed_Rx_Internal(state)`;若 `unconfirmed_count == 0U` | 返回 `IEC104_PLAN_SKIPPED` | +| 4 | `!immediate && unconfirmed_count < w && !Dt_Requires_Immediate_Ack(state)`,即未显式要求立即确认、未达w门限,且过程不属于 STOP_REQUESTED、STOP_ACT_SEND_PENDING、STOP_CON_WAITING、STOP_DRAINING | 返回 `IEC104_PLAN_SKIPPED` | +| 5 | 以上均未跳过,执行 `*receive_number = state->vr` | 返回 `IEC104_PLAN_READY`,输出本次确认号计划 | + +`IEC104_Link_Prepare_S_Ack_Internal(state, response, receive_number)` 调用 `IEC104_Build_Sframe(response, receive_number)`,结果不等于 `IEC104_APCI_LEN` 时返回 `-1` 且不提交状态;等于 6 时设置 `state->s_ack_nr = receive_number`、`state->s_ack_state = IEC104_S_ACK_SEND_PENDING` 并返回帧长。保存的是规划时的 `vr` 快照;报文等待发送期间新收到的 I 帧仍可推进 `vr`,不会改写该快照。 + +上述四个 STOPDT 阶段使普通 `Prepare_S_Ack()` 在有待确认数据且无 S 在途时跳过 w 门限。调用方应在接收事件后立即驱动该接口,无需等到 t2;生成的 S 帧实际发送完成后才推进 `acked_nr`。`Prepare_T2_Ack()` 自身仍先比较 `elapsed_ms`,未到期会返回 `0`,停止阶段不会绕过它的时间检查。 + +子站生成STOP_CON后回STOPPED/IDLE,不再准备普通S;已经排在CON前面的S仍按原快照完成。后到START重新激活不影响旧S的发送和确认记账。TESTFR始终独立。 + +`IEC104_Link_Complete_S_Ack_Send()` 按以下顺序处理: + +1. 完整链路状态非法:返回 `IEC104_LINK_RESULT_FATAL`,不修改状态。 +2. `s_ack_state != IEC104_S_ACK_SEND_PENDING`:返回 `IEC104_LINK_RESULT_IGNORED`,不检查 `sent_len`。 +3. 有S在途但发送长度非6:清除S状态和快照,不推进acked_nr,返回FATAL。 +4. 长度为6:按原快照提交acked_nr,清除S状态和快照,返回提交结果。 + + +S成功完成只推进acked_nr并清除自身在途状态;不会改变dt_state或dt_procedure。 + +### 3.7 t1 与 t2 + +| 接口 | 计时对象 | 到期条件 | +|---|---|---| +| `IEC104_Link_T1_I_Ack_Expired()` | 最早已提交发送完成且尚未被对端确认的本端 I 帧 | 已完成未确认帧数大于 0,且 `elapsed_ms >= t1_ms` | +| `IEC104_Link_Prepare_T2_Ack()` | 最早尚未由本端确认的对端 I 帧 | `elapsed_ms >= t2_ms`,且通过 `Plan_S_Ack()`:不在被控站首次 CON 待完成阶段、有待确认数据、无 S 帧在途 | + +`IEC104_Link_T1_I_Ack_Expired()` 先检查完整链路状态及 `t1_ms != 0U`,再调用 `Sent_Unacknowledged_I_Count(state)`。该辅助函数的实际变量和计算为: + +```c +return Number_Distance(state->va, state->sent_vs); +``` + +合法状态保证确认位置不越过本地完成位置,因此该循环距离就是已完成未确认帧数。计数为 0 时 t1 接口返回 `0`;否则执行 `elapsed_ms >= t1_ms ? 1 : 0`。状态或阈值非法则先返回 `-1`。 + +只有Prepare、尚未Complete的帧不触发I帧确认t1;完成提交后开始等待有效ACK,重复ACK不得重置起点。越过本地完成位置的ACK被拒绝。排队和本地发送等待须由发送层另设时限。 + +`t1_ms/t2_ms` 必须大于 0,且配置须满足 `t2_ms < t1_ms`。TcpServer和Connection在初始化时校验这一关系;直接调用Link时由调用方保证,因为各超时接口只接收自身阈值。`IEC104_Link_Prepare_T2_Ack()` 先校验完整链路状态、输出缓冲、容量及 `t2_ms`,再检查 `elapsed_ms < t2_ms`,未到期返回 `0`;到期后调用 `Prepare_S_Ack_Validated(state, 1U, 1, response)`,通过立即确认参数绕过 w 门限,继续保留首次 CON 门控、无待确认数据及已有 S 在途的检查。首次 CON 完成后应按原接收时间重新驱动确认。 + +模块不保存计时起点。发送层记录整帧实际交给传输层的单调时间 `written_at_ms`,不能改用延迟执行完成回调时的当前时间。每次 I 帧 Complete 或 I/S 接收确认推进 `va` 后,调度层重新选择最早已完成未确认帧;使用该帧原始时间,部分 ACK 或后续完成不能延长其截止时间。没有此类帧时取消 t1。使用子站 Manager 时,可通过 Peek_Oldest_Sent_Unacked_Record 和 T1_I_Ack_Expired 读取与判断,APDU 槽位复用不影响该原始时间。 + +t2 由最早未确认接收数据的时间起算;确认只覆盖一部分数据时,剩余数据保留自己的原始接收时间。到期事件只负责唤醒,调用方应校验连接代次和当前计时对象,再传入当前时间差。时间差可为 0;阈值必须大于 0。宽位时间差超出 `uint32_t` 时应饱和,不能截断后重新变小。 + +--- + +## 四、STARTDT 固定角色流程 + +### 4.1 控制站主动启动 + +控制方向与监视方向分别门控:STARTDT_ACT成功入队后即可预留控制方向I帧;在START_ACT_SEND_PENDING时这些I帧必须排在ACT之后,START_CON_WAITING时也可继续预留。`dt_state`仍保持STOPPED,收到匹配CON才提交STARTED并开放监视方向接收。提前I帧同样消耗k窗口;I帧发送完成后单独计算其确认t1,不得取消或延长等待STARTDT_CON的t1。 + +```text +dt_state=STOPPED, dt_procedure=IDLE + │ Prepare_Startdt_Act:生成 68 04 07 00 00 00 + ▼ +START_ACT_SEND_PENDING + │ Complete_Startdt_Act_Send(6) + ▼ +START_CON_WAITING + │ Handle_Startdt_Con(STARTDT_CON) + ▼ +dt_state=STARTED, dt_procedure=IDLE +``` + +关键提交点: + +- 生成 ACT 后稳定状态仍为 STOPPED; +- ACT 完整发送后只进入 CON 等待,仍未 STARTED; +- 只有收到匹配 STARTDT_CON 才提交 STARTED; +- ACT 发送失败时过程回 IDLE 并返回 `FATAL`,由连接层断开; +- 只有控制站角色可调用主动启动三个接口。 + +等待 CON 的 `t1` 由 `IEC104_Link_T1_Startdt_Con_Expired()` 判定。ACT 仍在 SEND_PENDING 时不算等待 CON。`Handle_Startdt_Con()` 先校验角色、状态、APCI 类型及功能码,再检查过程是否为 `START_CON_WAITING`;在 `START_ACT_SEND_PENDING` 收到合法 CON 时返回 IGNORED,且不暂存该 CON。调用方应先提交本地已知的 ACT 完整发送结果,再分派后续接收事件。 + +### 4.2 被控站受理启动 + +启停状态在请求受理时更新:START受理即激活,STOP受理即停用;非阻塞发送FIFO负责随后完整写出应答。 + +`Handle_Startdt_Act()`先校验角色、状态、APCI和输出缓冲,生成STARTDT_CON,并立即置`STARTED/IDLE`。初次START、已启动时重复START、停止等待期间的START均走同一路径。不暂存相反方向ACT,也不因先前CON仍在途而拒绝新的启停请求。 + +```text +STOPPED/IDLE --受理START、生成CON--> STARTED/IDLE +STOPPED/STOP_DRAINING --受理START、生成CON--> STARTED/IDLE +STARTED/IDLE --重复START、生成CON--> STARTED/IDLE +``` + +Manager先预留控制槽再调用Link;容量不足返回WAIT,当前请求尚未受理,原状态和停止期限保持不变。成功受理后Connection立即结束旧STOP总计时,并交付实际的激活变化。新的I/S报文须排在已有CON之后;已提交的帧不能撤回或交错写入。 + +`Complete_Startdt_Con_Send()`只检查角色、完整状态和发送长度。恰好6字节返回OK,其他结果FATAL;它不再提交STARTED。旧START_CON可能在STOP受理后才发完,完成事件不能重新激活链路。 + +HIGH遵守以下队列约定:从停用状态重新激活时清理;已激活时重复START保留HIGH。 + +### 4.3 交错请求与发送顺序 + +| 接收顺序 | 受理后的状态 | 应答及通知 | +|---|---|---| +| START,然后首个CON未写完就收到STOP | 立即停用并进入STOP_DRAINING | 原START_CON继续发送;确认条件满足后排STOP_CON;受理时依次通知ACTIVATED、DEACTIVATED | +| STOP仍在等I帧确认时收到START | 立即STARTED/IDLE | 结束尚未生成CON的停止等待并排START_CON;旧STOP不再生成CON | +| STOP_CON已经入队后收到START | 立即STARTED/IDLE | 旧STOP_CON与新START_CON按FIFO发送;旧CON完成不改变新状态 | +| 连续重复START | 保持STARTED/IDLE | 每个成功受理的ACT生成CON;不重复通知ACTIVATED | + +`START → STOP → START`的状态变化依次为激活、停用、激活。中间STOP_CON是否生成取决于第三个请求受理前停止确认是否已经满足,不能把尚未生成的CON与已有发送任务混为一谈。完成事件只确认发送事实,实际状态始终由已受理请求决定。被后到START结束的停止等待不会补发STOP_CON;这是交错处理策略,不代表要求主站提前交错发出请求。 + +## 五、STOPDT 与双向排空 + +### 5.1 控制站主动停止 + +控制站先调用 `IEC104_Link_Request_Stopdt()` 受理停止请求。合法 `STARTED/IDLE` 进入 `STOP_REQUESTED`,不受未确认接收数据影响,并立即禁止新业务 I 帧预留。重复请求返回 `IN_PROGRESS`,已停止返回 `ALREADY_STOPPED`,其他启停过程占用时返回 `BUSY`,参数、角色或状态非法返回 `REQUEST_ERROR`。只有 `ACCEPTED` 修改状态。 + +受理后调用 `IEC104_Link_Prepare_Stopdt_Request(state, buffer, capacity, &tx)` 推进。它只处理已受理的请求,不会隐式启动一个新过程: + +先检查角色、完整链路状态、`request`、容量和 `tx`,非法时返回 `PREPARE_ERROR`。其余分支按以下顺序执行,表中过程名省略 `IEC104_DT_PROC_` 前缀: + +| 当前条件 | 结果 | +|---|---| +| `dt_procedure == STOP_ACT_SEND_PENDING` | `WAIT_SEND` | +| `dt_procedure == STOP_CON_WAITING` | `WAIT_PEER_CON` | +| `dt_procedure != STOP_REQUESTED` | `NOT_ACTIVE` | +| 处于 STOP_REQUESTED,接收确认未完整覆盖,且已有 S 帧在途 | `WAIT_SEND`,完成后再次推进并补确认 | +| 处于 STOP_REQUESTED,接收确认未完整覆盖,且无 S 帧在途 | 调用 `IEC104_Link_Prepare_S_Ack_Internal(state, request, state->vr)`;成功则设置 `tx->frame_type = IEC104_STOPDT_FRAME_S_ACK`,编码失败返回 `PREPARE_ERROR` | +| 处于 STOP_REQUESTED,接收数据已确认或在途 S 帧完整覆盖 `V(R)` | 编码 STOPDT_ACT,进入 STOP_ACT_SEND_PENDING,设置 `tx->frame_type = IEC104_STOPDT_FRAME_ACT` | + +两条构帧成功路径最后统一设置 `tx->frame_length = IEC104_APCI_LEN`,返回 `IEC104_STOPDT_FRAME_READY`。 + +在途 S 帧覆盖当前接收位置时,ACT 可以直接排在该 S 帧后面。调用方必须保持严格 FIFO 发送和完成顺序。生成 ACT 不要求 `va == vs`;收到 CON 时同时检查本端已预留 I 帧全部获确认,以及接收确认覆盖最新接收位置。 + +ACT 准备和 CON 接收共用 `IEC104_Link_Rx_Ack_Covered_Internal(state)`。其实际返回条件为: + +```c +return state->acked_nr == state->vr || + (state->s_ack_state == IEC104_S_ACK_SEND_PENDING && + state->s_ack_nr == state->vr); +``` + +等待 CON 期间新收 I 帧可能推进 `vr`,因此必须重新检查,不能沿用生成 ACT 时的结果。在 `STOP_CON_WAITING` 中,接收确认尚未安排、或在途 S 帧只覆盖旧位置时,CON 返回 `FATAL` 且不修改状态。若在途 S 帧已完整覆盖,允许 CON 先于其本地完成回调被处理;进入 `STOPPED/IDLE` 时保留 S 状态与快照,后续 S 完成成功才推进 `acked_nr`,发送失败仍返回 `FATAL` 由连接层断连。该检查不代替S完成事件;I帧确认受公共完成边界约束,`va == vs`时`sent_vs`也已经到达`vs`。 + +```text +STARTED/IDLE + │ Request_Stopdt:受理并保留停止意图 + ▼ +STOP_REQUESTED + │ Prepare_Stopdt_Request:必要时先生成 S 确认;条件满足后生成 ACT + ▼ +STOP_ACT_SEND_PENDING + │ Complete_Stopdt_Act_Send(6) + ▼ +STOP_CON_WAITING + │ 收到 STOPDT_CON 且 va == vs、IEC104_Link_Rx_Ack_Covered_Internal(state)为真 + ▼ +STOPPED/IDLE +``` + +控制站可以在 `va != vs` 时先发送 STOPDT_ACT,但收到 STOPDT_CON 时只有本端已预留 I 帧全部获确认才接受。预留帧必须在同一 FIFO 中排在 ACT 之前,对端可在返回 CON 前通过 I/S 帧完成确认。 + +ACT 发送失败时过程回 IDLE、稳定状态保持 STARTED并返回 `FATAL`。等待 STOPDT_CON 的 `t1` 仍从 ACT 完整发送成功开始,由 `IEC104_Link_T1_Stopdt_Con_Expired()` 判定;请求准备和队列等待阶段的超时由连接层管理。ACT 已生成后新收到的 I 帧继续由公共 `Prepare_S_Ack()` 立即确认。 + +两个 STOPDT Prepare 接口均返回 `IEC104_StopdtPrepareResult_t`,只有 `FRAME_READY` 写入报文和 `IEC104_StopdtTx_t`(类型、长度),其他结果不修改状态和输出。不能把枚举值当作字节数或布尔值。发送层先预留有界 FIFO 节点,再构帧并按生成顺序入队;无法入队或发送失败时关闭连接。FIFO 节点保存 `tx`,发送完成按其固定类型选择接口:S_ACK → `Complete_S_Ack_Send()`,ACT → `Complete_Stopdt_Act_Send()`,CON → `Complete_Stopdt_Con_Send()`;不能根据完成时刻的过程状态猜测帧类型。 + +调用方在受理、S 帧完成及相关接收事件后推进停止,直到等待 ACT/CON 发送或对端事件;等待结果不得在状态不变时忙循环。 + +### 5.2 被控站停止等待 + +`Handle_Stopdt_Act()`受理STOP后立即置`STOPPED/STOP_DRAINING`,表示已停用并等待确认。接收新I帧立即返回FATAL,不推进V(A)/V(R)、不交付业务;仍接收合法S确认。不等待首次START_CON发送完成,不保存pending标志。 + +`Stopdt_Response_Status()`与实际准备共用以下判断: + +1. 非STOP_DRAINING返回NOT_ACTIVE。 +2. 已有S但快照尚未覆盖当前V(R),返回WAIT_SEND。 +3. 无S且有待确认接收数据,生成一帧S。 +4. 接收确认已完成或已由在途S完整覆盖时,若`va != vs`则WAIT_PEER_ACK,否则生成STOP_CON并回STOPPED/IDLE。 + +S和STOP_CON可在同一轮依次排队,发送FIFO必须保证S先完整发送;不能用“已安排S”提前推进acked_nr。已有当前I帧继续完成并等待ACK,停止流程不发送新的业务I帧。生成STOP_CON后,STOPPED/IDLE拒绝新的I/S;后到START受理后重新开放。 + +### 5.3 重复STOP与期限 + +排空中的重复STOP返回IGNORED,保留原停止进度和期限。STOPPED/IDLE下再次收到STOP会重新安排CON;Connection在上一停止期限仍有效时保留原起点,不因重复请求续期。 + +本轮停止总期限在STOP成功受理时启用,起点为该请求的原成帧时刻received_at_ms,包含受理前因通知容量不足的WAIT、确认等待和对应CON的发送。若受理前等待已经用尽期限,可在受理后的同轮检查中超时。Manager在STOP_CON入队时返回帧身份,Connection记录本轮最后一帧STOP_CON;只有匹配的完成才能结束计时。后到START成功受理也会取消旧STOP总计时。旧STOP_CON晚完成不能关闭新的停止期限,旧START_CON晚完成也不能恢复发送状态。 + +### 5.4 发送失败 + +START_CON、STOP_CON发送失败均返回FATAL,由上层断链,不回滚已受理的状态变化。S失败清除自身在途快照、不推进acked_nr,也须断链。控制站的ACT/CON检查、提前CON和发送失败规则保持原有契约。 + +本地发送期限、I帧ACK的t1和STOP总期限保持独立;暂不可写、部分写入及重复请求都不能重置已有帧的期限。已有CON必须完整写出或随连接失败丢弃,不允许截断部分APDU。 + +## 六、TESTFR 与 t3 + +### 6.1 与角色和启停解耦 + +两种有效角色都可以主动发起 TESTFR,收到 TESTFR_ACT 时也都应答 TESTFR_CON。主动 TESTFR 使用独立 `testfr_state`: + +`Complete_Testfr_Act_Send()` 提交的是 ACT 的本地发送结果,`Handle_Testfr_Con()` 处理的是对端确认。调用方必须先提交 ACT 完整发送结果,再分派对应 CON,与主动 STARTDT/STOPDT 的顺序一致。即使 CON 已进入接收缓冲区,也须先完成本次 ACT 的 Complete。Complete 成功只转入 CON_PENDING,发送成功本身不表示已得到对端确认。 + +```text +TESTFR_IDLE + │ Prepare_Testfr_Act:生成 68 04 43 00 00 00 + ▼ +TESTFR_ACT_SEND_PENDING + │ Complete_Testfr_Act_Send(6) + ▼ +TESTFR_CON_PENDING + │ Handle_Testfr_Con(TESTFR_CON) + ▼ +TESTFR_IDLE +``` + +主动探测不修改 `dt_state`、`dt_procedure`、I/S 序号或确认位置。已有主动探测时再次 Prepare 返回 `0`。`Handle_Testfr_Con()` 先校验完整链路状态、APCI 类型和功能码;合法输入仅在 CON_PENDING 时被接受并返回 IDLE。在 IDLE 或 ACT_SEND_PENDING 时返回 IGNORED,不修改状态,也不缓存 CON。调用方若颠倒处理顺序,后续 Complete 不会补处理已经忽略的 CON。 + +每次 Prepare 成功生成 ACT 都建立一次完成义务。发送层必须保证最终恰好调用一次 `Complete_Testfr_Act_Send()`;完整发送传 6,发送失败或发送超时传非 6。只要连接继续存活,就不能静默丢弃本地完成事件。 + +### 6.2 被动应答 + +`IEC104_Link_Handle_Testfr_Act()` 是无状态接口: + +- 验证输入为 TESTFR_ACT; +- 生成 `68 04 83 00 00 00`; +- 不接收 `IEC104_LinkState_t`,因此不改变主动 TESTFR 等待、启停或序号。 + +每轮 TESTFR 都是一方发送 TESTFR_ACT 发起、另一方返回 TESTFR_CON 应答。控制站与被控站均可主动发起测试;如果双方的 t3 在收到对方请求前先后到期,并各自发出了 ACT,就会出现两轮独立测试交叉进行。这不要求每次都由双方发起,只有一方发起时仍是一轮 ACT/CON 交换。 + +例如,双方均已发出 ACT、尚未收到对方请求时,交叉过程可以是: + +```text +A → B:TESTFR_ACT,A 发起自己的测试 +B → A:TESTFR_ACT,B 发起自己的测试 +A → B:TESTFR_CON,A 回答 B 的请求 +B → A:TESTFR_CON,B 回答 A 的请求 +``` + +收到对端 TESTFR_ACT 时,本接口只生成应答,由调用方实际发送;它不结束本端已经发起的测试。本端主动测试仍由对应的 ACT 发送完成事件和收到的 TESTFR_CON 推进,不能把对端的 ACT 或本端发出的 CON 当成本端所等待的 TESTFR_CON。 + +### 6.3 t3 触发和 TESTFR t1 + +`IEC104_Link_Prepare_T3_Testfr()` 先校验完整链路状态、输出缓冲、容量及 `t3_ms != 0U`,非法时返回 `-1`,即使时间尚未到期也一样。校验通过后,未到期返回 `0`;满足: + +```text +idle_elapsed_ms >= t3_ms +``` + +时调用私有 `Prepare_Testfr_Act_Validated(state, request)`,与直接主动探测共用“无正在进行的主动探测”检查、U 帧编码和状态提交。辅助函数不判断链路空闲时间:若 `testfr_state != IEC104_TESTFR_IDLE`,已有探测则返回 `0`;否则编码 TESTFR_ACT、设置 `IEC104_TESTFR_ACT_SEND_PENDING` 并返回 `6`。返回 `6` 只表示生成报文,尚未发送。 + +本接口不定义“链路活动”的事件集合,也不读取时钟或保存最近活动时刻。连接层统一规定哪些有效收发事件属于链路活动,并据此更新 t3 的单调计时起点、计算 `idle_elapsed_ms`。这些活动不得重置正在等待 TESTFR_CON 的 t1 起点。 + +ACT 完整发送并通过 `Complete_Testfr_Act_Send()` 成功提交后,状态进入 `TESTFR_CON_PENDING`,`IEC104_Link_T1_Testfr_Con_Expired()` 才检查等待 CON 的 t1。连接层以本次 ACT 实际完整发送时刻为单调计时起点,不能用延迟处理 Complete 时的当前时刻代替。匹配 CON 被 `Handle_Testfr_Con()` 成功处理后,状态回到 IDLE,由连接层取消本轮等待计时;链路接口本身不操作外部定时器。重复请求和无匹配事件不得重置该 t1 起点。 + +连接层定时器只负责唤醒。回调须先校验连接代次;若仍为有效连接,在该连接的串行调度中重新检查当前探测。当前仍处于 CON_PENDING 时,使用当前探测保存的 ACT 实际完整发送时刻和当前单调时间重新计算 `elapsed_ms`,再调用 `IEC104_Link_T1_Testfr_Con_Expired()`,只有返回 `1` 才按 t1 超时断连。同一连接上,上一轮探测的回调也可能晚到:不得把旧回调携带的起点或时间差用于新一轮探测;当前已无 CON 等待时,不得按旧回调断连。本接口只检查当前状态和传入的时间差,不能识别时间差属于哪一轮探测。 + +ACT_SEND_PENDING 表示本地发送结果尚未提交,ACT 可能仍在排队或发送,也可能已经完整发送但尚未 Complete;此时尚未进入接受 CON 的状态,调用方必须先提交 ACT 的 Complete,再分派对应 CON。状态与 `t1_ms` 合法时,t1 判定在 ACT_SEND_PENDING 返回 `0`;状态非法或 `t1_ms == 0U` 时仍先返回 `-1`。ACT 排队和实际发送的有限等待、最终发送结果的恰好一次回报仍由发送层负责;发送失败或发送超时按非 `6` 的结果提交并断连。 + +--- + +## 七、状态校验、完成结果与原子更新 + +### 7.1 三层校验 + +内部状态检查集中在 `IEC104_Link.c`,分为: + +1. `Link_State_Fields_Valid()`:检查指针、序号范围、稳定状态、TESTFR枚举和暂存标志; +2. `S_Ack_State_Valid()`:检查 S 状态枚举和快照组合,尤其是 IDLE 时 `s_ack_nr == 0`; +3. 本文件内的角色校验:控制站检查原有ACT/CON过程;被控站仅允许IDLE,或STOPPED/STOP_DRAINING。CON发送身份和进度由SendBuffer验证。 + +`IEC104_Link_State_Valid_Internal()` 先检查公共字段和S状态,再校验角色并分派本文件内的过程检查。角色、S状态和启停过程的枚举定义域各检查一次,不在基础字段检查中重复列举。链路算法只判断链路状态是否自洽,不判断TCP连接是否仍可用。 + +角色专用接口再通过 `IEC104_Link_Role_Valid_Internal()` 要求指定角色;STARTDT/STOPDT的t1查询位于主站模块,保留旧接口允许合法被控站查询并返回0的行为。基础字段校验也包括 `sent_vs` 的15位范围和两个pending标志的0/1范围;S快照是否仍可提交则在完成时检查。 + +`Complete_I_Send()` 和 `Complete_S_Ack_Send()` 已在公开入口校验状态,私有 `Commit_Sent_NR()` 只检查实际发送的确认号并提交,不再重复完整状态校验。I帧发送门控允许两端STARTED/IDLE及控制站的两个启动等待阶段,标准适用条件见3.2节;`k <= 32767` 与窗口检查保护未确认区间,另外保留 `sent_vs` 的显式环限检查。公共状态检查要求 `distance(sent_vs,vs) <= distance(va,vs)`,ACK合法范围为`va..sent_vs`。 + +### 7.2 连接故障归属 + +链路状态不保存故障标志;`Handle_*()` 和 `Complete_*()` 通过 `FATAL` 返回值报告致命故障,由连接层负责断连。TCP 连接的建立、可用性和关闭状态由连接上下文管理,具体字段与状态名称由连接层自行定义。 + +链路模块通过返回值把故障交给连接层: + +- `Handle_*()` 或 `Complete_*()` 返回 `IEC104_LINK_RESULT_FATAL`:协议、参数、状态、发送结果或确认快照非法; +- t1 判定接口返回 `1`:等待确认超时; +- 无状态 TESTFR_CON 的发送失败:由实际执行发送的连接层直接处理。 + +连接层收到致命结果后必须停止输入、取消或隔离旧任务与定时器、关闭套接字,并保证旧回调不再进入新连接对象。即使完成接口清理后过程回到 IDLE,也不表示连接恢复可用;不能通过继续调用链路接口或复位链路状态,在原连接上重试。 + +### 7.3 先检查、后更新 + +接收和构帧接口尽量在写状态或输出缓冲前完成参数、角色、功能码和过程检查。I 帧同时更新 `va/vr` 时也先完成所有序号验证,从而避免非法帧造成部分推进。 + +构帧遵循先检查、再编码、最后更新。I/S使用规划快照;被控站START编码成功后立即激活,STOP受理直接停用,后续准备只推进确认和CON。停止准备的只读查询与构帧共用同一判断,不增加独立规划对象。 + +普通S确认与t2、主动TESTFR与t3各自共用一条私有 `_Validated` 准备路径。四个公开入口负责完整校验状态、输出缓冲及各自参数;S路径通过 `Plan_S_Ack()` 判断是否需要确认,TESTFR路径通过 `Testfr_Act_Allowed()` 判断探测是否空闲,相应只读查询也使用同一判断。t2/t3仍在判断到期之前完成校验,因此尚未到期的非法调用仍返回 `-1`。 + +`Complete_Startdt_Act_Send()`、`Complete_Stopdt_Act_Send()`、`Complete_Testfr_Act_Send()` 均先校验状态和适用角色,再检查是否有匹配的 ACT 待完成;无匹配时先返回 IGNORED,不检查 `sent_len`。有匹配时才检查长度,失败清理对应过程并返回 FATAL,成功进入对应的 CON 等待状态。TESTFR 只在 ACT_SEND_PENDING 匹配发送完成;长度为 6 时进入 CON_PENDING,其余长度回 IDLE 并返回 FATAL。 + +子站两个CON完成接口均只检查角色、状态和长度,恰好6字节返回OK,其他结果FATAL,不提交启停状态。旧CON晚完成不会覆盖新ACT已经生效的状态。 + +U帧入口验证输出缓冲及固定功能码,编码器保证写出6字节。START受理提交启用状态,STOP_CON生成结束停止等待;TESTFR_CON不改变链路状态。停止所需S确认仍检查编码结果并保存独立快照。 + +例如 `(void)IEC104_Build_Uframe(request, IEC104_U_TESTFR_ACT);` 仍执行编码并写入 `request`,`(void)` 只表明有意不使用返回值。它既不改变公开编码函数的 `int` 返回类型,也不表示网络发送已经成功。该写法依赖本地已验证的前置条件,不能直接推广到参数尚未校验的外部调用。 + +主站的 `Prepare_Stopdt_Request()` 与子站的 `Prepare_Stopdt_Response()` 检查各自的停止阶段,返回公开的 `NOT_ACTIVE` 或对应 `WAIT_*`。子站的阶段及排空检查通过 `Stopdt_Response_Status()` 与只读查询共用。可以生成报文时,S路径把本次 `V(R)` 传给公共 `IEC104_Link_Prepare_S_Ack_Internal()`,由它编码并保存同一确认号;U路径分别生成本端角色对应的ACT或CON。只有成功生成时才写入公开的 `IEC104_StopdtTx_t`。私有STOPDT计划结构和中间结果转换已移除;一次调用仍由同一执行上下文串行完成。 + +`IEC104_PlanResult_t` 现在只在 `IEC104_Link.c` 内供I/S序号规划使用,不再放入跨模块私有头文件: + +| 内部规划结果 | 含义 | +|---|---| +| `IEC104_PLAN_ERROR` | I帧规划的状态或窗口参数非法 | +| `IEC104_PLAN_SKIPPED` | 门控关闭、窗口满、无需确认或已有S帧在途 | +| `IEC104_PLAN_READY` | 规划输出有效,允许进入编码阶段 | + +I/S路径只在结果精确等于 `IEC104_PLAN_READY` 时读取序号快照并编码,其他结果不写规划输出。 + +被控站START返回OK或FATAL;STOP返回OK、IGNORED或FATAL;没有相反ACT暂存结果。停止Prepare仅FRAME_READY表示生成报文,其他值描述等待或错误。 + +所有 `Handle_*()` 与 `Complete_*()` 接口统一返回 `IEC104_LinkResult_t`: + +- `IEC104_LINK_RESULT_OK (1)`:本次接口处理成功。接收处理可推进序号或只生成应答;完成接口才提交最终发送结果,不能统一理解为报文已发送或已获对端确认; +- `IEC104_LINK_RESULT_IGNORED (0)`:合法重复事件,或当前没有匹配的在途过程; +- `IEC104_LINK_RESULT_FATAL (-1)`:协议、参数、状态、发送结果或确认快照非法,调用方必须关闭当前连接。 + + +调用方必须精确比较枚举值,不能按布尔值或正负判断是否有报文。CON完成只报告发送结果。 + +### 7.4 Reset 不是会话内错误恢复 + +**当前已实现:** State_Reset保留有效角色、清零序号和S快照、置STOPPED及各过程IDLE;角色非法时置INVALID。不执行TCP关闭,不能用Reset在故障连接上继续通信。 + +**外部接入要求(尚未集成):** TCP连接管理、异步I/O事件循环和系统定时器管理尚未实现。单当前I帧与独立控制FIFO由 `IEC104_SendBuffer` 提供;AsduQueue和SendManager提供变长原文保留、发送前编号、I/S接收、完成分派及显式恢复,见 [发送管理设计](IEC104_发送模块_设计架构.md#sendmanager)。Receiver提供每连接独立APDU视图;已恢复的旧全局入口保持原有独立状态,不转交报文给Receiver或SendManager。连接层仍须将Receiver视图按序交给这些接口,Receiver不修改Link。链路Init和Reset不会初始化或终止Receiver及队列。调用方负责关闭故障连接,保证旧事件不会提交到新状态;新连接须成功调用State_Init固定角色,并以新代次初始化Receiver,不要求等待TCP建连后再初始化。上下文的安全复用、实际I/O及U帧接收调度仍由接入方负责。 + +--- + +## 八、公开接口契约 + +### 8.1 初始化、序号与确认 + +| 接口 | 成功结果 | 暂不执行/等待 | 非法或失败 | +|---|---|---|---| +| `IEC104_Link_Config_Default(config)` | 写入默认参数 | `config==NULL` 时直接返回 | 无返回值 | +| `IEC104_Link_State_Init(state,role)` | 返回 `1`,初始化并固定角色 | — | 返回 `0` | +| `IEC104_Link_State_Reset(state)` | 保留有效角色,复位链路运行状态 | `NULL` 时直接返回 | 无返回值 | +| `IEC104_Link_Handle_I_Frame(state,apci)` | `OK`,通过 3.2 节接收门控及序号检查后提交 `va/vr` | — | `FATAL`,包括STOPPED/IDLE、控制站等待启动CON或被控站STOP排空/CON待完成时收到I帧;不部分更新 | +| `IEC104_Link_Handle_S_Frame(state,apci)` | `OK`,通过 3.3 节接收门控及确认号检查后提交 `va` | — | `FATAL`,参数、状态、帧类型、接收阶段或确认号非法 | +| `IEC104_Link_Prepare_I_Frame(...)` | 返回完整APDU长度,构帧并预留 `N(S)`;包括控制站ACT之后的提前命令 | `0` 表示当前方向/阶段不允许、k窗口已满或未完成区间将占满序号环 | `-1` 表示参数/状态非法 | +| `IEC104_Link_Complete_I_Send(state,tx,sent_len)` | `OK`,提交确认快照并按 FIFO 推进 `sent_vs` | — | `FATAL`,包括无待完成帧、`N(S)` 不匹配、参数/状态、长度或快照非法 | +| `IEC104_Link_Prepare_S_Ack(state,w,immediate,response,capacity)` | 返回 `6`,生成 S 帧并保存快照 | `0` 表示启停阶段不允许、无待确认数据、未达到 w 门限或已有 S 帧在途 | `-1` | +| `IEC104_Link_Complete_S_Ack_Send(state,sent_len)` | `OK`,提交有效确认快照 | `IGNORED`,无匹配在途 S 帧 | `FATAL`,状态、发送结果或快照非法,连接层须断连 | + +### 8.2 时间判定 + +| 接口 | 到期并触发 | 未到期/未处于对应等待 | 参数或状态非法 | +|---|---|---|---| +| `IEC104_Link_T1_I_Ack_Expired()` | 返回 `1` | 返回 `0` | 返回 `-1` | +| `IEC104_Link_Prepare_T2_Ack()` | 到期且通过 S 确认规划,返回 `6` 并生成 S 帧 | 返回 `0`:未到期、首次 CON 门控未开放、无待确认数据或已有 S 在途 | 返回 `-1` | +| `IEC104_Link_T1_Startdt_Con_Expired()` | 返回 `1` | 返回 `0` | 返回 `-1` | +| `IEC104_Link_T1_Stopdt_Con_Expired()` | 返回 `1` | 返回 `0` | 返回 `-1` | +| `IEC104_Link_T1_Testfr_Con_Expired()` | 返回 `1` | 返回 `0` | 返回 `-1` | +| `IEC104_Link_Prepare_T3_Testfr()` | 到期且无主动探测,返回 `6` 并生成 TESTFR_ACT | 返回 `0`:未到期或已有主动探测 | 返回 `-1` | + +`t1_ms/t2_ms/t3_ms` 阈值必须大于0,`elapsed_ms/idle_elapsed_ms` 允许为0。STARTDT/STOPDT的t1只有在对应的控制站等待过程才返回 `1`;其他角色或过程只要链路状态合法就返回 `0`。角色与过程的非法组合由对应角色的内部过程检查拒绝。TESTFR t1对两种有效角色开放。 + +### 8.3 STARTDT + +| 接口 | 允许角色 | 成功 | 重复/等待 | 非法 | +|---|---|---|---|---| +| `IEC104_Link_Prepare_Startdt_Act()` | 控制站 | `6`,进入 ACT_SEND_PENDING | `0`,已有过程或已 STARTED | `-1` | +| `IEC104_Link_Complete_Startdt_Act_Send()` | 控制站 | `OK`,开始等待 CON | `IGNORED`,无匹配ACT | `FATAL` | +| `IEC104_Link_Handle_Startdt_Con()` | 控制站 | `OK`,提交 STARTED | `IGNORED`,未等待CON | `FATAL` | +| `IEC104_Link_Handle_Startdt_Act()` | 被控站 | `OK`,生成CON并立即STARTED/IDLE | 无 | `FATAL` | +| `IEC104_Link_Complete_Startdt_Con_Send()` | 被控站 | `OK`,完整发送,状态不变 | 无 | `FATAL`,角色/状态非法或长度非6 | + +### 8.4 STOPDT + +| 接口 | 允许角色 | 成功 | 重复/等待 | 非法 | +|---|---|---|---|---| +| `IEC104_Link_Request_Stopdt()` | 控制站 | `ACCEPTED`,进入 STOP_REQUESTED | `ALREADY_STOPPED/IN_PROGRESS/BUSY` | `REQUEST_ERROR` | +| `IEC104_Link_Prepare_Stopdt_Request()` | 控制站 | `FRAME_READY`,生成 S 或 ACT;类型和长度输出到 tx | `NOT_ACTIVE/WAIT_SEND/WAIT_PEER_CON` | `PREPARE_ERROR` | +| `IEC104_Link_Complete_Stopdt_Act_Send()` | 控制站 | `OK`,开始等待 CON | `IGNORED`,无匹配ACT | `FATAL` | +| `IEC104_Link_Handle_Stopdt_Con()` | 控制站 | `OK`,发送方向排空且接收确认覆盖后提交 STOPPED;保留在途S完成义务 | `IGNORED`,未等待CON | `FATAL`,包括 `va!=vs` 或接收确认未覆盖 | +| `IEC104_Link_Handle_Stopdt_Act()` | 被控站 | `OK`,立即STOPPED/STOP_DRAINING | `IGNORED`,已经在排空 | `FATAL` | +| `IEC104_Link_Prepare_Stopdt_Response()` | 被控站 | `FRAME_READY`,生成 S 或 CON;类型和长度输出到 tx | `NOT_ACTIVE/WAIT_SEND/WAIT_PEER_ACK` | `PREPARE_ERROR` | +| `IEC104_Link_Complete_Stopdt_Con_Send()` | 被控站 | `OK`,完整发送,状态不变 | 无 | `FATAL` | + +### 8.5 TESTFR + +| 接口 | 状态性 | 成功 | 重复/等待 | 非法 | +|---|---|---|---|---| +| `IEC104_Link_Handle_Testfr_Act()` | 无状态 | `OK`,生成固定6字节TESTFR_CON | — | `FATAL` | +| `IEC104_Link_Prepare_Testfr_Act()` | 修改主动探测状态 | `6`,生成 TESTFR_ACT | `0`,已有探测 | `-1` | +| `IEC104_Link_Complete_Testfr_Act_Send()` | 修改主动探测状态 | `OK`,进入CON_PENDING等待对端确认 | `IGNORED`,无匹配ACT | `FATAL` | +| `IEC104_Link_Handle_Testfr_Con()` | 修改主动探测状态 | `OK`,接受等待中的CON并回IDLE | `IGNORED`,当前未等待CON,包括ACT尚未Complete | `FATAL` | + +### 8.6 返回值家族与输出有效期 + +内部 Plan 使用统一私有枚举;公开接口按职责保留以下不同契约,不能相互强制转换: + +| 接口家族 | 判断方式 | 输出何时有效 | +|---|---|---| +| I/S、主动 STARTDT/TESTFR 及 t2/t3 的 Prepare | 正数为报文长度,0 为本次不生成,-1 为错误 | 仅正长度时读取本次报文和发送快照 | +| `IEC104_LinkResult_t` | 精确匹配OK、IGNORED、FATAL | 是否生成报文由具体接口决定 | +| `IEC104_StopdtRequestResult_t` | ACCEPTED 的数值为 0,另有 ALREADY_STOPPED、IN_PROGRESS、BUSY、ERROR | 不生成报文;仅 ACCEPTED 提交停止意图 | +| `IEC104_StopdtPrepareResult_t` | 精确匹配 FRAME_READY、NOT_ACTIVE、WAIT_*、PREPARE_ERROR | 仅 FRAME_READY 写入报文和 `tx`;长度取 `tx.frame_length` | +| `IEC104_Link_State_Init()` | 1 为成功,0 为参数非法 | 成功后状态对象可用于连接;失败不改写对象 | +| `IEC104_Link_T1_*_Expired()` | 1 为超时,0 为未超时或无匹配等待,-1 为参数/状态非法 | 无输出,不修改状态 | +| `IEC104_Link_Can_Prepare_*()` | 1 为可准备,0 为无需求或等待,-1 为参数/角色/状态非法 | 无输出,不修改状态;不保证发送存储可用 | +| `IEC104_Link_Get_Status()` | 1 为成功,0 为参数/状态非法 | 成功才写数值快照,不借用内部存储 | + +没有生成新报文时,缓冲区和 `tx` 可能仍含上一次结果;它们不是本次待发送输出。等待和错误结果不得触发旧缓冲重发。FATAL 也不保证状态完全不变:部分完成接口会先清除发送半态,具体以接口契约为准。 + +--- + +## 九、设计决策与变更记录 + +### 9.1 主要设计决策 + +- **角色按连接固定**:同一二进制可作主站或子站,但一条 TCP 连接不在运行期切换角色。 +- **每端每连接一套状态**:`vs/va` 与 `vr/acked_nr` 分属本端两个方向,不与对端共享内存。 +- **稳定状态与半态分离**:`dt_state` 只保存最终 STARTED/STOPPED,`dt_procedure` 保存唯一在途过程。 +- **按受理顺序启停**:被控站START立即激活,STOP立即停用;后到START结束尚未生成CON的停止等待。已有CON仍按FIFO发送,完成不覆盖新状态。控制站主动启停仍串行。 +- **确认快照只有一套**:普通 S 和 STOPDT 排空共用 `s_ack_state/s_ack_nr`。 +- **连接故障由连接层处理**:链路处理与完成接口通过 `FATAL` 返回值报告故障,TCP 连接上下文负责断连和隔离旧回调;链路状态不保存故障标志。 +- **算法与调度分离**:链路函数不读时钟、不发网络报文,便于确定性测试;集成层仍须串行提交单连接事件,但不要求人为保证首次STARTDT_CON发送完成回调先于对端首个I帧接收事件。 +- **先检查再提交**:I/S构帧保留序号规划,启停和TESTFR构帧直接判断状态;构帧路径在检查通过、编码成功后提交相应待发送状态。只受理请求的分支按自身契约修改状态;发送完成失败可以清理在途过程,但不提前提交成功的稳定状态。 + +### 9.2 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-09-03 | 从 `IEC104.c` 架构中拆出链路序号、启停和测试流程说明 | +| 2026-09-07 | 增加 `k/w/t1/t2/t3`、S 确认快照以及 STARTDT/STOPDT/TESTFR 独立接口 | +| 2026-09-08 | 改为每连接固定角色;稳定 `dt_state` 与统一 `dt_procedure` 分离;删除交叉启停状态和测试;STOPDT 复用公共 S 确认 | +| 2026-09-08 | 完成接口改为 OK/IGNORED/FATAL 三态;非法确认快照纳入致命结果;删除链路层重复的故障标志和故障锁存接口,故障统一归连接上下文管理 | +| 2026-09-09 | Handle 与 Complete 统一使用三态结果;合法重复/无匹配过程返回 IGNORED,协议、参数、状态和提前 STOPDT_CON 返回 FATAL | +| 2026-09-09 | 已STARTED时的重复STARTDT_CON改为无状态应答,不占用dt_procedure;允许随后STOPDT_ACT直接进入排空,完成回调不覆盖停止过程 | +| 2026-09-09 | I帧接收增加dt_state门控;STARTDT完成前及STOPDT完成后收到I帧返回FATAL,STOPDT排空半态仍允许接收并立即确认 | +| 2026-09-10 | I帧接收与发送门控分离;被控站首次STARTDT_CON发送等待期间允许接收对端I帧,但继续禁止生成本端业务I帧 | +| 2026-09-10 | I/S、STARTDT、STOPDT 和主动 TESTFR 的发送准备内部拆成只读状态规划、APCI 编码和成功提交三阶段;公开接口保持兼容 | +| 2026-09-10 | 主动STOPDT拆成Request受理和Prepare推进,增加STOP_REQUESTED;两端停止准备接口改用明确枚举与报文类型/长度输出,调用方须迁移接口 | +| 2026-09-10 | ACT准备与CON接收共用接收确认覆盖检查;拒绝未覆盖的新接收数据,兼容CON先于完整覆盖的S帧完成回调 | +| 2026-09-10 | 被控站停止期间暂存一个STARTDT_ACT并返回DEFERRED;STOPDT_CON成功后通过Prepare_Pending_Startdt_Con推进启动,停止发送失败和Reset取消暂存 | +| 2026-09-10 | 全部Plan函数统一使用私有IEC104_PlanResult_t;仅READY读取规划输出并编码,公开接口显式映射原有返回值和等待原因 | +| 2026-09-10 | 删除入口已校验的U帧路径中不可达的编码失败分支;首次启动应答私有构造函数改为void,STOPDT的S准备检查独立保留,公开接口兼容 | +| 2026-09-10 | STOPDT规划一次性输出报文类型与S帧N(R)快照;普通S确认和停止排空共用Build_S_Ack编码及提交,消除停止流程内的二次S规划 | +| 2026-09-10 | 旧单路全局复位接口更名为IEC104_Legacy_Link_Reset,明确其兼容入口用途;外部调用方须更新名称并重新编译 | +| 2026-09-10 | 统一ACT及停止CON完成分支的失败提前返回写法;T2/T3复用已校验内部准备路径,保留未到期非法参数返回-1;澄清STARTDT/STOPDT的T1允许合法被控站查询并返回0 | +| 2026-09-10 | 同步完整状态字段、I帧FIFO完成边界与t1公式、收发门控表、结果与输出有效期;补充S完成身份边界、有界队列接入责任、兼容完成路径和验证状态链接 | +| 2026-09-10 | 修复后到的STOPDT_ACT未取消旧暂存STARTDT:可受理的停止请求清除pending_start_act,保留停止进度和确认快照 | +| 2026-09-11 | 按公共、主站、子站拆分实现和公开头文件;角色过程校验归各自模块,STOPDT准备去掉跨角色选择;保留旧总头文件和全部公开接口 +| 2026-09-11 | 精简5个启停/TESTFR规划函数,移除私有STOPDT计划和等待结果转换;去除重复状态校验与可推导的I帧判断 | +| 2026-09-14 | 完善STARTDT方向门控:控制站允许ACT后的提前I帧;被控站受理ACT后接收I/S,首次CON完成前暂缓w/immediate/t2确认;补齐初始停止及主站启动等待的S接收门控 | +| 2026-09-15 | 首次STARTDT_CON待完成期间,重复STARTDT_ACT逐个生成CON并返回OK;保持原启动过程,所有CON按FIFO发送并提交完成,首次成功完成提交STARTED,后续重复应答不覆盖启停状态 | +| 2026-09-15 | 统一S确认、STARTDT/STOPDT及TESTFR的本地发送完成接口为Complete_*_Send,不保留旧名转发入口;同步声明、调用和文档,参数、状态结构、返回值及处理逻辑不变 | +| 2026-09-15 | 明确后到STOP取消暂存START属于本实现策略,正常主动启停串行 | +| 2026-09-16 | TESTFR 简化为三态,统一为先提交 ACT 发送完成再处理 CON;移除提前 CON 半态,未等待时忽略且不缓存 CON | +| 2026-09-16 | 澄清 TESTFR t3 活动计时与 t1 等待计时的职责,补充同一连接旧探测定时器回调的处理约定 | +| 2026-09-16 | 澄清 Reset 的字段复位范围、故障会话清理与旧事件隔离要求,明确新连接初始化时机和上下文安全复用条件;仅修改说明和代码注释 | +| 2026-09-16 | 将 Reset 已实现行为与尚未实现的连接层接入要求分开标注,精简外部清理过程描述并同步接口注释 | +| 2026-09-16 | 新增独立有界发送FIFO,支持先预留再构帧、部分写入和队首出队;明确未确认I帧须转存确认记录,实际TCP、Complete分派和定时器尚未集成 | +| 2026-09-16 | 当时增加完整 APDU 保留及最早未确认帧 t1 判定,随后采用单节点池;该存储方案已由下述 2026-09-23 环形队列方案替代 | +| 2026-09-16 | SendBuffer改为调用方单数组节点池;发送FIFO与I帧保留链引用同一节点,删除独立确认数组及APDU复制,I帧Commit_Send_Completion后等ACK再释放,链路Prepare/Complete接口不变 | +| 2026-09-17 | 集中I/S启停准入与停止阶段立即确认判断;公共S完成失败和子站停止流程复用暂存启动取消操作,保持公开接口、状态结构及原有检查和提交顺序 | +| 2026-09-23 | ACK上限收紧为本地完成位置sent_vs,SendBuffer同步按next_complete_ns校验;拒绝确认未发送/未完成提交的排队帧,先检查再提交;简化已完成未确认及保留帧计数,新增10个专项回归 | +| 2026-09-23 | SendBuffer 改为 APDU 环形 FIFO,本地完成后释放槽位;Manager 独立保存确认记录并提供最早未确认记录和 I 帧 t1 查询,原始业务数据由业务层保留 | +| 2026-09-23 | 三发送模块进一步改为变长ASDU字节环、单当前I帧和独立6字节控制槽;发送前编号,Link独占协议位置,有效ACK释放原文,回执独立消费,断线显式Rebind;测试文件保持原样 | +| 2026-10-18 | 收紧被控站I帧接收门控:受理STOPDT_ACT后至再次进入启动接收阶段前,新I帧返回FATAL且不交付业务;保留控制站接收及S确认规则,暂存START不提前开放接收 | +| 2026-10-19 | 先前补齐双向暂存:首个STARTDT_CON在途时以pending_stop_act暂存STOP,受理即关闭数据门控并开始停止总时限;后到START取消暂存STOP及计时,重复STOP不续期。停止排空时暂存START的处理由下条策略替代 | +| 2026-10-19 | 调整启停行为:STOP_DRAINING收到START时取消停止并生成启动应答,结束STOP总计时;仅STOP_CON已生成时仍暂存START。保留发送完成提交、部分写入和FIFO约束,STOPPED下结束排空后仍按首个启动CON清理HIGH和配额 | +| 2026-10-19 | 替代此前子站CON完成提交及双向暂存策略:START/STOP按受理立即激活/停用,取消两个pending字段和子站CON半态;旧CON完成不修改状态;S与STOP_CON可依次排队。Connection按匹配STOP_CON身份结束总期限,TcpServer在受理时交付状态事件。 | diff --git a/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_TcpServer_设计架构.md b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_TcpServer_设计架构.md new file mode 100644 index 00000000..130bfc8a --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_TcpServer_设计架构.md @@ -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 +#include + +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 + +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) diff --git a/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_发送模块_设计架构.md b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_发送模块_设计架构.md new file mode 100644 index 00000000..61f763b0 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_发送模块_设计架构.md @@ -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) + + + +## 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均不续期。 + + + +## 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后不能继续使用。发送异步进行时,同帧最多允许一个尚未回报的写操作。 + + + +### 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。 + + + +### 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插入字节。 + + + +### 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完成接口。 + + + +### 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再复用存储。 + + + +## 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)`。 + + + +### 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回绕的边界。 + + + +### 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才推进业务游标。静态数组的“释放”表示空间可复用,不是归还操作系统内存。 + + + +### 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。 + + + +## 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 | 当前连接必须结束,先保留事实用于业务回收 | + +所有枚举精确比较,不能用“非零为成功”。 + + + +### 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处理。 + + + +### 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。 + + + +### 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。 + + + +### 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,也不会恢复已移除的记录槽。 + + + +### 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。 + + + +### 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完成。 + + + +## 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。 + + + +### 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推进后选下一最早未确认帧的原时间,不从当前时间重新开始。 + + + +### 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字节顺序,不为后续业务编号 | + + + +## 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及嵌入式目标平台仍需接入验证。 + + + + +### 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内部驱动,没有后台自动同步。 diff --git a/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_完整调用流程.md b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_完整调用流程.md new file mode 100644 index 00000000..dc3c6903 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_完整调用流程.md @@ -0,0 +1,1052 @@ +# IEC104 完整调用流程 + +本文依据 2026-10-29 当前工作区的协议库源码,说明从服务创建到关闭释放的调用顺序,与 [API 接口汇总](IEC104_API接口汇总.md) 配合阅读。范围仅为库本身:不包含示范程序、测试程序或应用业务实现。接口汇总按实现文件查函数,本文按运行过程追踪函数之间的关系。 + +主线为当前已实现的 **IPv4 TCP 子站**:应用通过 `IEC104_Slave_*` 接入,Slave内部持有并调用 `IEC104_TcpServer_*`,内部使用 Connection、SendManager、Link、Receiver、SendBuffer 和 AsduQueue。当前网络后端为 Windows;主站文件只提供 Link 层启停过程,未接入这一套 TCP 子站调度。 + +调用树中的 `[内部]` 表示对应 `.c` 内的静态辅助函数,不是应用可直接调用的 API。调用树表示主要步骤;具体的等待、失败和恢复分支在后文说明。 + +可先读[应用主线](#flow-1)和[对象关系](#layers),需要业务时读[总召接入](#flow-interrogation)和[时钟同步接入](#flow-clock-synchronization),再查[事件与 ASDU](#flow-6)、[应答与上报](#flow-7)、[容量等待](#flow-12)、[断线重连](#flow-13)、[自有事件循环](#flow-14)和[关闭释放](#flow-15)。 + +业务调用主线为 `Slave_Create → Slave_Config_Default/Init → Slave_Prepare/Process 或 Poll → Slave_NextEvent → 共用发送接口 → Slave_Close → 读空事件 → Slave_Destroy`。NextEvent 直接交付 ASDU 及原连接接口;延迟处理可用Slave_Copy_Event,也可保存来源身份及必要请求数据,通过Slave身份入口应答。以下展开 Slave 调用到的 TCP 内部流程;其中 `server` 和应答所用的 `generation` 由 Slave 维护。 + + + +## 1. 应用层完整主线 + +```text +IEC104_Slave_Create() + → IEC104_Slave_Config_Default() + → 按需要修改监听地址、端口和协议配置 + → IEC104_Slave_Init() + → 循环: + IEC104_Slave_Poll() + IEC104_Slave_NextEvent(),逐条读到 EMPTY + CONNECTED → 记录主站地址和连接代次;此时尚未启用数据传输 + ACTIVATED → 处理数据传输启用通知 + I_FRAME → 检查 asdu_result,完整解析并校验业务条件 + 通过 &event.connection 调用共用 Send_ASDU / ACT_CON / ACT_TERM + RECORD → 处理发送事实回执 + DEACTIVATED → 处理数据传输停用通知 + DISCONNECTED → 结束该 generation 的连接业务 + 有主动上报时构造 ASDU,调用 IEC104_Slave_Enqueue_Event() + 对此前返回 WAIT 且仍有效的业务提交,择机重试 + → IEC104_Slave_Close() + → IEC104_Slave_NextEvent(),处理收尾事件直到 EMPTY + → IEC104_Slave_Destroy() +``` + +以上是调用顺序伪代码。事件不会每轮都出现,排列也不是固定脚本;实际以 `NextEvent` 返回的 FIFO 顺序为准。 + +```mermaid +flowchart TD + A["Slave_Create / Config_Default / Init"] --> B["Slave_Poll:等待并推进一次"] + B --> C["NextEvent:逐条取事件"] + C --> D{"事件类型"} + D -->|I_FRAME| E["验证 ASDU / 通过 event.connection 应答"] + D -->|RECORD| F["读取本地完成和对端 ACK 事实"] + D -->|连接或启停| G["按 generation 处理状态变化"] + E --> C + F --> C + G --> C + C -->|EMPTY| H["提交主动上报 / 安排有效的 WAIT 重试"] + H --> I{"继续运行?"} + I -->|是| B + I -->|否| J["Close"] + J --> K["NextEvent 读完收尾事件"] + K --> L["Destroy"] +``` + +使用这条主线时: + +- 同一服务的全部接口在同一事件循环线程串行调用;`Create/Destroy` 也须跨服务串行。模块内的 BUSY 检查用于拒绝重入,不是线程锁。应用负责循环和业务执行。 +- `Create` 同时取得 Slave 槽及它私有的 TcpServer 槽,无动态内存分配和网络操作;取得失败返回 NULL。最后配对 `Destroy`。每个服务同时维护一条主站连接。 +- `Init` 成功即能缓存主动上报,连接尚未建立也可调用 `Enqueue_Event`。 +- `Poll` 返回后及时消费事件。仅反复调用 `Poll` 而不调用 `NextEvent`,会积压通知并触发后文的容量等待。 +- 子站业务包含 [IEC104_Slave.h](../../104/IEC104_Slave.h),即可使用服务、ASDU 和共用发送接口。底层的帧预留、完成提交、队列重绑定由内部调度完成。 + +各组返回值必须分别判断: + +| 接口组 | 成功或有结果 | 需要区别处理的结果 | +| --- | --- | --- | +| Init、Poll、Prepare、Process、Close 等 | `IEC104_TCP_OK`,数值为 0 | 错误时保存该次 `IEC104_TcpError_t`;客户端断开本身可正常返回 OK,由事件说明 | +| NextEvent | `IEC104_TCP_EVENT_READY` | EMPTY 表示已读空;ERROR 不是有效事件 | +| Slave_Enqueue_Event、底层 TcpServer_Send_Response | `IEC104_TCP_SUBMIT_OK` | WAIT 尚未接手;ERROR 为参数、代次或准入等错误;FATAL 须结合收尾事实处理 | +| 共用连接的Send_ASDU、Send_ACT_CON、Send_ACT_TERM | `true` | 104侧用Get_Last_Submit_Result区分WAIT/ERROR/FATAL;false不等于可以直接重试 | + +`SUBMIT_OK` 表示已经复制接手,应避免重复提交;`SUBMIT_WAIT` 表示未接手,由业务稍后重试。普通应答保留原文,总召数据使用不排队接口,WAIT后重读当前值再试。应答返回 FATAL 时可能已经写出部分甚至全部字节,不能直接按“完全未发送”处理。 + + + +### 1.1 Slave、共用发送接口与 TcpServer 的关系 + +```mermaid +flowchart TD + App["应用业务"] --> Slave["IEC104_Slave:生命周期、事件封装、连接身份绑定"] + App --> Common["IEC60870_MasterConnection:共用发送接口"] + Common -->|函数指针分派| Slave + Slave -->|内部持有指针| TCP["IEC104_TcpServer:监听、socket、FIFO、调度"] + TCP --> Conn["IEC104_Connection:接收、状态机、期限协调"] + Conn --> Rx["Receiver / Codec:成帧与校验"] + Conn --> Mgr["SendManager:组帧、发送记录、ACK"] + Mgr --> Link["Link:序号、窗口、启停、探测"] + Mgr --> Buffer["SendBuffer:完整 APDU 与写出偏移"] + Mgr --> Queue["AsduQueue:HIGH / NORMAL 原文"] + TCP --> Net["Net_Windows:非阻塞网络与单调时钟"] +``` + +`IEC104_Slave_t` 和 `IEC104_TcpServer_t` 都是不透明类型,完整结构分别定义在各自 `.c` 文件。Slave 内部保存 `IEC104_TcpServer_t *` 并调用公开函数;持有这种指针不需要知道结构大小。应用不能声明这两种结构的实体、访问成员、按值复制或互相强转,也不需要额外创建一个 TcpServer。 + +`IEC60870_Slave.c` 只实现四个 `IEC60870_MasterConnection_*` 函数的分派,不是第二套 104 服务。`IEC60870_MasterConnection_t` 是“向原主站应答”的接口值,由 Slave 放在接收事件中;它既不是 socket,也不负责启动、停止服务。ASDU 构造、解析仍使用 `IEC60870_*`,服务运行则统一使用 `IEC104_Slave_*`。 + +直接使用 TcpServer 是另一种较底层的接入方式:调用方自行解析原始事件并携带原 generation 提交应答。本文应用主线使用 Slave;下文出现 TcpServer 函数时,均表示它承接的内部步骤。 + +业务模块的公共校验也通过Slave完成:`IEC104_Slave_Get_Event_Identity`验证事件所属服务、实例和自引用视图,返回可保存的来源身份;`IEC104_Slave_Check_Request`统一查询请求会话是否仍可处理。前者允许同一服务的历史连接通知,后者才判断当前可用性,避免因连接已经变化而跳过FIFO中的收尾事件。 + +`Check_Request`返回`WAIT_EVENTS`时先逐条处理Slave通知,返回`STALE`时终结原业务,返回`ERROR`时处理查询或参数错误;只有`READY`才继续业务阶段。READY不保证发送容量,也不能替代业务撤销和期限判断。后续业务模块复用这两个接口,不读取事件的内部身份字段,也不自行组合TCP连接状态。 + +### 1.2 配置入口与默认值 + +运行配置先调用 `IEC104_Slave_Config_Default(&config)`,再修改所需字段,最后交给 Init。Init 复制配置;修改原配置变量不会改变已运行的服务。 + +| 配置位置 | 默认值或约束 | +| --- | --- | +| `config.bind_address` / `config.port` | `0.0.0.0:2404`;地址为数字 IPv4。端口 0 表示由系统选取,可从状态中的 `bound_port` 读取 | +| `config.protocol.link.k` / `w` | 12 / 8;均为 1~32767。发送还会受实际记录和缓冲容量限制 | +| `t1_ms` / `t2_ms` / `t3_ms` | 15000 / 10000 / 20000 毫秒;非零,且 `t2_ms < t1_ms` | +| `local_send_timeout_ms` / `stop_timeout_ms` | 默认各 15000 毫秒;非零。改动 t1 后不会自动重新计算这两个已填入的字段 | +| `high_priority_burst_limit` | 0:严格 HIGH 优先;非零:有 NORMAL 待发时限制连续 HIGH 帧数 | +| `IEC104_APP_LAYER_PARAMETERS_DEFAULT` | 定义在 [IEC104_Types.h](../../104/IEC104_Types.h):Type ID 1、VSQ 1、COT 2、默认 OA 0、CA 2、IOA 3、ASDU 最大 249 字节 | +| 静态存储容量 | 编译期由 [IEC104_TcpServer_Config.h](../../104/IEC104_TcpServer_Config.h) 配置,默认可创建 1 个服务。与上述运行参数分开 | + +104 专用的默认参数宏保留在 104 接入层。共用 `IEC60870_AppLayerParameters_t` 只描述布局,不替调用方选择传输配置。业务一般通过 `IEC104_Slave_Get_Application_Layer_Parameters` 或事件连接的对应 getter 取得标准 104 参数,无需自行维护宏副本。 + + + +### 1.3 接入独立总召模块 + +[IEC104_Interrogation.h](../../104/business/IEC104_Interrogation.h) 提供站总召、分组总召、主站撤销及业务超时处理,接口详见[总召API](IEC104_API接口汇总.md#file-21)。配置包括CA、每批对象上限和三项业务期限,不注册业务回调。Slave交付网络通知,总召模块交付业务事件;应用在同一事件循环中主动读取这两条FIFO,再显式接受请求、提交批次或报告结束。 + +总召内部已经接入上述来源和会话校验:Handle_Event保存经Slave验证的身份,Process及Reply_Begin/Submit_Batch/Complete_Data/Fail_Request在推进前检查请求状态。应用仍按下列流程驱动,无需为总召重复调用这些校验接口;应用只负责业务数据、接受决定及资源清理。 + +```text +Slave_Create + → Slave_Add_Station,注册应用提供的节点、CA和业务context + → Interrogation_Config_Default,设置use_station_registry=true、每批上限和业务期限 + → 多目标广播配置broadcast_states/broadcast_capacity(调用方数组借用至Destroy) + → Interrogation_Create(slave, &config),检查非NULL + → Slave_Init + → 循环: + Interrogation_Get_Status,按最近业务期限限制本轮等待 + Slave_Poll 或 Slave_Prepare/Process + Slave_NextEvent,逐条处理到EMPTY + → Interrogation_Handle_Event(gi, &transport_event) + → IGNORED的I帧继续交给其他业务处理器 + → 连接、启停和断线通知也须交付 + Interrogation_Process(gi) 自动推进当前就绪任务 + Interrogation_NextEvent,逐条处理业务事件: + BEGIN → 按request.qualifier检查数据源 → Reply_Begin(accept) + NEED_DATA → 应用选择本批IOA,读取当前值/品质并组包 → Submit_Batch;无剩余数据则Complete_Data + FINISHED → 清理对应请求资源,无需应答 + 数据尚未就绪:保存NEED_DATA,后续循环就绪时再应答 + Interrogation_Get_Status,安排下轮调度 + → Interrogation_Cancel + → Interrogation_NextEvent读空,处理FINISHED并释放应用资源 + → Slave_Close → Slave_NextEvent读空收尾通知 + → Interrogation_Destroy → Slave_Remove_Station → Slave_Destroy +``` + +必须在开始消费Slave通知前创建总召模块,一个Slave只绑定一个实例。配置按值复制,Slave须存活到总召Destroy之后;Init失败也要释放已创建实例。所有接口串行调用;异步采样结果须回到所属事件循环再提交。模块不创建线程,也不会调用应用函数。 + +上面的顺序使用共用站点表,CA只在注册时指定。多目标广播先确认全部站,再逐站取数,最后终止全部站;各站BEGIN可先于首条FINISHED。应用可在首个NEED_DATA建立唯一取数游标,Complete_Data返回OK后复用;异常时按FINISHED清理。需保留context则按station_id查原节点并Acquire,使用结束后Release。删除立即退出路由,已有引用结束后才可释放存储;同CA重建不改变旧数据源。兼容应用可保留use_station_registry=false的单CA或固定station模式。详见[共用站点表](IEC104_API接口汇总.md#station-registry)。 + +请求范围为Type100、SQ=0、一个对象、COT=6(激活)或8(撤销)、P/N=0、CA为本站地址或65535、IOA=0、QOI=20..36。QOI=20选择全站,21..36选择第1..16组。每个实例至多一个未终结总召:空闲时保存合法激活为START;从保存起到终结,相同和不同QOI的新激活均否定,不覆盖原QOI/OA/TEST及进度。被拒绝的请求不会自动执行,主站须在之后重新发起。总召忙仅约束Type100激活,Type101等其他业务继续分派,匹配的COT=8仍可撤销。详细设计见[总召模块设计](IEC104_Interrogation_设计架构.md)。Process在Slave通知FIFO读空后推进: + +```text +空闲收到激活 → START(已占用总召资格)→ 业务事件有空位 → BEGIN(开始已启用的业务期限) + ├─ Reply_Begin(false) → FINISHED(REJECTED);排队否定ACT_CON + └─ Reply_Begin(true) → Process提交ACT_CON(COT=7) + → NEED_DATA(batch_index=0) + Submit_Batch → 当场尝试不排队提交COT=请求QOI + → WAIT时保留事件与游标,重读当前值后重试 + → 提交OK后增加batch_index → 下一个NEED_DATA + Complete_Data → Process提交ACT_TERM(COT=10) + → FINISHED(COMPLETED) +BEGIN → Fail_Request → FINISHED(SOURCE_ERROR) → 否定ACT_CON,保留连接 +NEED_DATA → Fail_Request → 关闭原连接、取消 → FINISHED(SOURCE_ERROR) +已交付BEGIN后遇STOPDT/断线/Cancel → FINISHED(对应原因) +主站匹配的COT=8 → 只停止匹配项 → 已交付BEGIN才有FINISHED(DEACTIVATED);提交或保存肯定COT=9 +已有未终结总召 → 新激活转为否定ACT_CON,不建立第二个总召 +START/CONFIRM期限到期 → 否定ACT_CON,保留连接;已交付BEGIN才有FINISHED(对应超时原因) +DATA/TERMINATE期限到期 → 关闭原连接、取消 → FINISHED(DATA_TIMEOUT/TOTAL_TIMEOUT) +``` + +分组点表及IOA由应用维护,BEGIN不向库登记或冻结完整IOA列表。request.qualifier指定请求的组,未配置的组可Reply_Begin(false);接受的空组可在NEED_DATA直接Complete_Data,仍按ACT_CON → ACT_TERM结束。数据ASDU的COT必须等于请求QOI:全站为20,第1组为21,直至第16组为36;确认/终止仍用COT=7/10并回显原QOI。模块验证批次头部,但不判断某个IOA是否属于该组,点表筛选、遍历及取值由应用负责。 + +实际站CA为1..65534。多目标广播使用一个任务执行“全部ACT_CON → 逐站数据 → 全部ACT_TERM”,各站独立BEGIN/FINISHED及request_id;上图描述其中单站的先后关系。调用方状态区须容纳接收时的注册站数,不足时整轮否定,不部分执行。request.common_address是实际站CA,Submit_Batch拒绝65535及其他站CA。紧凑副本仍只保存原身份及10字节ASDU,原OA/TEST/QOI保持,输入事件不修改。新增/替代节点下轮参加,尚未选入BEGIN便删除的站跳过;已经选中尤其已确认的原站必须完成或明确终结。空站表广播及未知具体CA回复46。详见[广播设计](IEC104_Interrogation_设计架构.md#33-广播地址与本站地址),Type101等业务自行决定广播规则。 + +业务事件是纯值结构,可直接复制、存入应用队列或跨循环保存;这与含自引用、必须用Copy_Event复制的Slave事件不同。事件中的instance_id标识总召实例,request_id标识请求,batch_index标识当前批次。应答须原样传回取出的事件,不能修改标识。模块按这些身份定位原任务,不依赖任务是否在队首,处理其他请求的单帧应答不会使当前交互失效。重复应答、旧批次、旧请求、取消/撤销/超时后的事件以及静态槽复用前的事件均返回STALE。request还提供CA/OA/QOI/TEST、generation/receive_serial及每批上限,实际发送身份始终取模块保存的原请求。 + +| 业务事件 | 应用职责 | 对应应答 | +| --- | --- | --- | +| BEGIN | 按QOI检查数据源并决定是否接受;取数游标可延至首个NEED_DATA建立,不预先采样全部数据 | Reply_Begin(true/false),数据源失败也可Fail_Request | +| NEED_DATA | 应用选择本批IOA和类型,读取当前值/品质,用自己的缓冲构造ASDU;未就绪时保留事件,继续驱动网络 | Submit_Batch、Complete_Data或Fail_Request,三者只选其一 | +| FINISHED | 停止对应采样任务,按实例/请求编号清理游标及业务资源,记录reason和已提交进度 | 无需应答 | + +每个已经取出的BEGIN恰好配对一个FINISHED。BEGIN尚未生成或未被取出时取消,不产生终结事件,因为应用尚未建立资源;参数校验、总召忙及满槽拒绝也不交付BEGIN。取消会清除尚未取出的NEED_DATA,已经取出的旧事件应答失效。FINISHED始终保留到应用读取;即使没有协议任务,未读终结事件仍会使Destroy返回false。COMPLETED仅说明终止帧已被发送层接手,不说明已写完或已ACK。 + +多目标广播的FINISHED先保存在每站状态区,NextEvent按FIFO空位逐个交付;连续读取即可,无需Process。网络流程完成后phase为FINISHING、gi_pending为false,但仍占用任务及状态区,读完全部终结事件之前新激活仍忙拒绝。 + +应用使用Asdu_Create、具体对象Create及Asdu_Add_InformationObject构造批次。参数为104的2/2/3,数据COT=事件request.qualifier、P/N=0,CA/OA/TEST与事件一致;一个ASDU只能同类型,批间可切换类型。支持Type 1/3/5/7/9/11/13/20/21,SQ=0/1均可,单批最多配置数量且不超过249字节。Add返回FULL时提交已有的非空批次,未加入对象留给下一批;空数据集直接Complete_Data。累计量由独立CounterInterrogation实例处理。 + +Submit_Batch返回OK后发送层已复制原文并确定帧顺序,应用可复用ASDU、缓冲及参数,并推进自己的数据游标。WAIT不保存原文、不推进计数,也不再次生成NEED_DATA;应用保留当前事件及游标,网络推进后重新读取当前值组包重试。只有本批OK后才索取下一批;数据源未就绪时也保存事件,不重复提交空批次。点值更新及突发发布须与总召读值及提交串行化;逐批取值不保证整轮同一采样时刻,具体排序保证及持续积压的边界见[总召模块设计](IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。 + +| 应答结果(省略IEC104_INTERROGATION_REPLY_) | 处理 | +| --- | --- | +| OK=1 | 本次应答已被接手,不再重复应答;还不代表发帧/ACK | +| WAIT=0 | Slave有未读通知,或Submit_Batch暂不能直接发送;保留事件和游标,推进通信并读空通知后重读当前值重试 | +| STALE=-2 | 交互已经失效,丢弃该应答;对应资源按FINISHED清理 | +| ERROR=-1 | 参数、批次或服务查询错误;修正后可使用同一事件重试。NEED_DATA阶段Fail_Request关闭失败是例外:任务已取消,应处理FINISHED | + +Handle_Event仅在空闲时保存合法总召,忙拒绝保留的只是单帧应答。单帧应答在Slave通知已读空、原会话有效且没有更早REPLY时,使用请求副本沿原HIGH接口立即尝试提交;成功便不占任务槽。WAIT或暂不能安全提交时保存,满槽无法保存则只丢新请求并计数。立即提交也不会修改调用方的原事件,不会跳过STOP/START或越过旧REPLY。 + +IEC104_Interrogation_Process(gi)在单帧REPLY与唯一总召之间轮转,DATA只生成NEED_DATA,应用调用Submit_Batch提交。总召等应用或事件空位不阻塞短应答;其他REPLY可穿插三阶段流程。ACT_TERM接手才正常终结,不等待ACK。拒绝、确认前失败或定向撤销只结束该站,多目标广播仍为其他站保留总召资格。Process发送WAIT保留请求和阶段,下次优先尝试最早REPLY;数据WAIT由应用重读后重试。已交给HIGH或发送缓冲的帧保持原顺序。 + +Process每一步和应答入口都检查Slave通知,避免跨过STOP/START或撤销。响应沿原connection发送;确认、终止及短应答可排入HIGH,数据只尝试直接提交。waiting包含发送、应用和事件空位等待;application_waiting表示等首次有效应答,data_send_waiting表示有效批次曾遇发送WAIT。events_pending是Slave通知标志,pending_events包含业务FIFO事件和状态区中的待交付FINISHED。phase和进度优先反映当前站,收尾为FINISHING,仅有短应答为REPLY,无任务为IDLE。waits只统计发送WAIT,批次/对象计数只在接手后增加。 + +Send_Response内部也可能受理STOP。发送ERROR时若确认原连接正常停用且通知待处理,模块先让出,由DEACTIVATED终结为STOPPED;Submit_Batch暂返WAIT,不停止取数计时。其他ERROR/FATAL仍按SEND_ERROR处理。成功提交照常记账,下一轮检查通知,不重复发送;判定条件见[提交结果处理](IEC104_Interrogation_设计架构.md#72-process中的选择规则)。 + +Process在任务为空、所有任务等应用/事件空位、发送WAIT或有未读Slave通知时返回;每轮另有2倍任务容量的步数上限,默认8步,余下本地工作由need_process提示。应用无需传入步数,仍须读取BEGIN/NEED_DATA并显式应答;数据未就绪时持续驱动网络和业务期限。 + +每次业务应答或读取业务事件后重查Get_Status。need_process为true时不休眠:新请求可生成BEGIN、旧FINISHED出队释放空位、下一帧待提交或业务期限到期都可能要求立即推进。它不是waiting的反值;发送WAIT后停止本地续跑,每次Slave_Poll/Process推进网络并读空Slave通知后仍须调用总召Process重试。Get_Status实时更新events_pending,未读Slave通知优先于need_process;外部等待同时遵守Slave_Prepare的网络调度条件。 + +数据WAIT时,同一网络推进之后还须重试应用保存的NEED_DATA,不能只等待need_process或新业务事件;Process不会替应用再次读取数据。 + +主站撤销按连接代次、目标CA、OA、TEST和QOI匹配,先于忙拒绝处理。定向撤销可匹配当前站、已确认但尚未取数或等终止的站,其他站继续;广播撤销停止整轮,对各仍活动站回肯定COT=9。尚未选入BEGIN或已结束的站不补发确认。匹配站旧应答STALE,已取BEGIN时有FINISHED(DEACTIVATED),满槽也复用原任务,不补发该站ACT_TERM;已接手帧不撤回。删除不妨碍原活动事务被撤销。详见[撤销与取消](IEC104_Interrogation_设计架构.md#9-撤销取消与连接变化)。 + +| 配置期限 | 默认值 | 起止点 | +| --- | --- | --- | +| begin_timeout_ms | 15000ms | BEGIN入业务FIFO至应用接受、拒绝或报告失败,包含事件未被取出的时间 | +| data_timeout_ms | 15000ms | 每次NEED_DATA入队至首次有效批次尝试、声明结束或报告失败;有效批次遇发送WAIT也停止本批计时,下一批重新计时 | +| total_timeout_ms | 0(默认禁用) | 启用后从BEGIN生成至ACT_TERM被发送层接手,包含取数和发送等待,不包含生成BEGIN前等事件空位,不等待ACK | + +每项配置为0即禁用该项;执行总期限默认关闭,接受和每批取数期限仍各为15000ms。默认值是可调整的业务策略,不是标准规定的总召时限,也不替代104链路t0..t3及单帧发送期限。使用Net提供的64位单调时钟,不受校时影响。Process及有效事件的应答接口都会检查期限。START在BEGIN生成前不计业务期限,但已占用总召资格;历史FINISHED挤占业务FIFO时须及时读取,否则当前请求会一直等空位,主站可能先超时。BEGIN生成后开始已启用的应用接受和执行总期限。START/CONFIRM超时保留TCP并转为否定ACT_CON;DATA/TERMINATE超时关闭原连接并取消本模块任务,不补发成功ACT_TERM。迟到应答返回STALE。显式启用后,提交新批次或单帧应答不延长执行总期限;总期限与阶段期限同时到点优先报告TOTAL_TIMEOUT。未读Slave通知优先处理,避免把已到达的撤销误算为超时。 + +确认边界是肯定ACT_CON被发送层接手,而不是Reply_Begin(true)、TCP写完或收到ACK。CONFIRM提交WAIT时仍可否定激活;已进入HIGH或当前发送缓冲的肯定确认不能改为否定。确认前失败复用原任务槽,保留QOI/OA/TEST并使用本站CA,排在已有REPLY之后,满槽也无需丢弃其他应答。已取出BEGIN才产生一次FINISHED,保留具体TIMEOUT或SOURCE_ERROR;未取出的BEGIN移除。Process转换后重查Slave通知并在本轮继续调度,发送WAIT保留重试,不再计算已终结业务的期限。确认前超时增加timeouts及rejected,BEGIN失败增加rejected,均不增加cancelled。 + +等待前重新Get_Status;timer_active为true时,Poll或应用事件循环的等待上限取原上限与timeout_remaining_ms的较小值,剩余0则立即驱动Process。Get_Status只查询,不自动执行超时。gi_pending表示唯一总召尚未终结,含BEGIN尚未生成;phase优先报告当前总召阶段,仅有短应答时为REPLY。deactivated记录匹配撤销,timeouts记录执行超时;last_finish_reason也记录BEGIN尚未交付便取消的总召。 + +COT/CA/IOA错误回45/46/47;激活的非法QOI、P/N、总召忙或应用拒绝回否定ACT_CON,撤销的非法QOI/P/N回否定DEACT_CON。合法新总召满槽时尝试否定ACT_CON;若不能安全提交或保存,就仅丢弃新请求并增加dropped_requests,保留当前任务和TCP。overloads统计需要保存但槽满的次数,negative_submitted统计发送层已接手的否定应答数。丢弃请求没有响应,主站可能超时或重试;Handle_Event返回HANDLED包含这种已处理的丢弃。 + +总召入口先安全读取类型,非100或公共视图不可用返回IGNORED,公共入口先过滤不可用视图再分派业务。Type100唯一对象完整但SQ=1或有尾字节时,仅回否定7/9,COT/CA/IOA错误仍优先45/46/47;副本规范为10字节、VSQ=1,保留OA/TEST/IOA/QOI及原输入事件。零对象/多对象等无法可靠对应请求的情况计入rejected并HANDLED丢弃。两类请求均不执行激活/撤销,不影响原总召及其期限;真实截短/帧错误仍由Receiver和连接层处理。完整策略见[请求校验](IEC104_Interrogation_设计架构.md#31-接收范围及校验顺序)。 + +非法Submit_Batch返回ERROR,保留交互供修正。数据源无法继续时调用Fail_Request:BEGIN阶段保存否定确认并保留连接,NEED_DATA阶段结束原连接;两者均交付FINISHED(SOURCE_ERROR)。BEGIN失败返回OK或超时应答返回STALE后,仍须继续驱动Process,不能假定连接已经断开。 + +构建在Slave及依赖之外加入104/business/IEC104_Interrogation.c及104/business/IEC104_GeneralInterrogation.c,头文件搜索路径包含104/、104/business/和common/。静态容量宏为IEC104_INTERROGATION_CAPACITY(默认TCP实例容量的两倍)、IEC104_INTERROGATION_TASK_CAPACITY(默认4)、IEC104_INTERROGATION_EVENT_CAPACITY(默认4,最小2)。每实例4个任务槽共用于至多一个召唤及短应答,不是后续召唤队列。任务使用固定槽位和顺序索引,完成的非队首应答可立即释放槽位,其他任务的自引用不受影响。生成BEGIN时同时预留一个终结槽;未读业务事件挤满队列时暂停生成工作事件,已排队单帧应答仍可调度,取消仍可交付FINISHED,不使用malloc。 + +同时接入计数量时另编译104/business/IEC104_CounterInterrogation.c,创建独立实例,配置独占广播状态区。连接及启停通知必须同时交付两边;I帧先总召、再计数量,未接手再交校时等后续模块或兜底。每轮读空Slave通知后推进两个召唤实例,轮换先后顺序,合并need_process、events_pending及最小剩余期限。FRZ=0走确认、数据、终止;FRZ=1..3通过Execute_Begin执行一次本地同步动作,确认后直接FINISHED。两个实例分别Cancel、读取FINISHED、Destroy。动作失败及快照约定见[计数量设计](IEC104_CounterInterrogation_设计架构.md)。 + +本接口替换旧begin/read_batch/finish及parameter配置:应用使用NextEvent和显式应答,遍历状态自行持有。Destroy前须Cancel并读完业务事件。Process只接收实例,旧调用删除第二个预算实参。当前只保留一个未终结总召,忙时否定后续激活,不自动执行此前被拒绝的请求。已删除queue_timeout_ms、QUEUED/QUEUE_TIMEOUT、queued_interrogations和queue_timeouts;调用方删除相关字段及分支,使用Config_Default初始化配置并重新编译。negative_submitted和dropped_requests继续记录否定提交与满槽丢弃;FINISH_OVERLOAD保留枚举值,满槽不以此终结已有任务。 + + + +### 1.4 接入独立时钟同步模块 + +编译`104/business/IEC104_ClockSynchronization.c`并包含对应头文件,在Slave创建并初始化后、消费任何Slave通知前创建校时实例。配置只含源标准时区和执行期限,默认480分钟及15000ms;模块使用独立静态池和每实例4个任务槽,不与召唤引擎共享任务。详情见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md)。 + +1. 将CONNECTED、ACTIVATED、DEACTIVATED及DISCONNECTED按FIFO交付总召、计数量、校时所有实例。I帧按总召→计数量→校时分派,均IGNORED才用Reject_Request兜底;HANDLED也可能表示拒绝或满槽丢弃。 +2. `Handle_Event`校验Type 103的唯一对象、COT/CA/IOA、CP56/IV/TEST、源时区及本站日期范围。合法请求保存原文及accepted_ms,不设置时钟;广播在受理时建立目标快照。 +3. 读空Slave通知后调用`Process`推进队首应答和未执行请求期限。Process不调用action,不越过等待执行的队首提交后续应答。 +4. 应用明确准备执行时调用`Execute_Next(cs, action, context)`。入口检查原会话,使用同一单调采样检查期限及计算滞留补偿,向action交付对应base_ms的utc_ms和固定UTC+8 local_time。只调用最早EXECUTE一次;可以越过较早REPLY执行,但确认保持原FIFO位置。 +5. action返回true/false分别保存肯定/否定ACT_CON,均仅回显原CP56并走Try;等待发送不重复动作。应用可把local_time交给Clock_Set,也可更新模拟时钟;不再次处理SU/时区或把local_time当UTC。false和后续取消均不保证硬件没有改变。 +6. 重查Get_Status,用execute_pending安排显式执行,用need_process及最近期限安排本地推进和等待。网络推进并读空通知后仍调用Process重试发送WAIT。confirm_pending包含action执行中,需排序的新带时标发布在此期间暂停,既有HIGH/NORMAL继续排空。 +7. 退出时校时Cancel,与其他业务分别清理,再Slave_Close、读空并分派通知、校时Destroy,最后移除站点并销毁Slave。STOP取消未交接请求和广播遍历,不回滚时钟,也不撤回已编号确认。 + +Execute结果须按枚举分支:WAIT先处理Slave通知,EMPTY只表示没有可执行动作、仍可能有待发确认;OK表示调用过action一次,不表示action成功、确认写完或主站ACK。ERROR不应当作发送背压盲目重试。不能在action内推进Slave或重入修改校时对象;Get_Status是唯一允许的本模块只读入口。 + +时间只补偿Handle_Event受理后的本站滞留;base_ms不是设备生效时刻,不补网络、通知FIFO和驱动耗时。新请求不按内容去重,重发旧时标可能再次回拨。Try不隔离新增NORMAL,持续发布可能使确认延迟;confirm_pending是应用协调依据,不是传输层预留或线程锁。 + + + +## 2. 创建服务与接入主站 + +实现入口:[IEC104_Slave.c](../../104/IEC104_Slave.c)、[IEC104_TcpServer.c](../../104/IEC104_TcpServer.c)、[IEC104_Connection.c](../../104/IEC104_Connection.c)、[IEC104_SendManager.c](../../104/IEC104_SendManager.c)。 + +### 2.1 服务初始化 + +```text +应用:IEC104_Slave_Create() + ├─ 取得 Slave 静态槽并分配本次实例编号 + └─ IEC104_TcpServer_Create(),取得并私有持有 TCP 服务 + +应用:IEC104_Slave_Config_Default() + └─ IEC104_TcpServer_Config_Default(),填入默认监听和协议配置 + +应用:IEC104_Slave_Init() → IEC104_TcpServer_Init() + ├─ 检查配置、运行状态、未读旧事件 + ├─ IEC104_Net_Init() + ├─ IEC104_Net_Listen() + ├─ IEC104_AsduQueue_Init(..., generation = 0) + └─ 保存监听身份,进入可调度状态 +``` + +监听与协议参数见上节;初始化失败时检查返回值及 `error.result/stage/socket_error`。失败不销毁句柄,完成相应清理后可重试 Init,或对尚未运行的服务调用 Destroy。 + +这一阶段只创建服务级队列和监听资源,没有创建活动协议会话。`generation = 0` 表示队列处于离线状态,NORMAL 原文可以先缓存。 + +### 2.2 接受 TCP 连接 + +```text +Poll / Process + └─ [内部,TcpServer] Accept_Clients() + ├─ 检查 CONNECTED 通知空间和旧启停事件是否已消费 + ├─ IEC104_Net_Accept() + ├─ 分配新的非零 generation + ├─ IEC104_AsduQueue_Rebind(..., 新 generation) + ├─ IEC104_Connection_Init() + │ ├─ IEC104_Receiver_Init() + │ └─ IEC104_SendManager_Init_Connection() + │ ├─ IEC104_Link_State_Init(..., IEC104_ROLE_CONTROLLED) + │ ├─ IEC104_SendBuffer_Init() + │ └─ IEC104_SendManager_Init() + ├─ IEC104_Connection_Set_Transfer_Observer() + ├─ 保存客户端 socket、注册 token、主站地址 + └─ [内部] Notify_Connection() → CONNECTED 入事件 FIFO +``` + +Connection 初始化通过 Manager 将内部引用绑定到最终对象地址。初始化后的 Connection 含内部引用,调用方应保持对象地址稳定。 + +接入后的状态是: + +| 项目 | 值或含义 | +| --- | --- | +| TCP | 已连接 | +| Connection 阶段 | ACTIVE,表示协议会话可运行 | +| Link 数据传输状态 | STOPPED | +| Link 启停过程 | IDLE | +| 序号 | 本次新连接从初始值开始 | +| 首条业务事件 | CONNECTED | +| 已缓存 NORMAL | 保留,等待 STARTDT_ACT 受理后取得发送机会 | + +因此,收到 CONNECTED 不等于数据传输已启用。当前有活动主站时,额外接入的客户端会被关闭,不替换正在使用的连接。 + +内部的 transfer observer 用于检查启停通知容量和保存状态变化,应用仍统一通过 `NextEvent` 取通知。 + +### 2.3 三类身份不能互换 + +| 身份 | 生命周期与用途 | 维护方 | +| --- | --- | --- | +| Slave 实例编号 `_owner_serial` | 一次成功 Create;防止静态槽销毁、复用后旧事件指向新实例 | Slave 保存到事件内部,应用只读 | +| `generation` | 一次 TCP 接入;区分重连前后的请求、接收帧和发送记录 | TcpServer 分配,Slave 自动带入应答和 Close_Connection | +| `service_token`、socket `token` | 一次 Init、一次 socket 注册生命周期;过滤旧网络、定时器和本地调度通知 | Prepare 提供,应用事件循环按原注册值回传 | + +`receive_serial` 和 `frame_serial` 分别标识某个 generation 内的接收帧和发送记录;协议 N(S)/N(R) 会按模 32768 回绕,不能替代这些身份。各编号只在所属服务范围内解释,不是跨服务的全局 ID。 + +Destroy 成功后必须丢弃旧服务指针。裸指针本身不能识别静态地址后来是否被复用;事件额外保存的实例编号用于阻止旧请求发送到新服务。计数器用尽时不会回绕重用身份。 + + + +## 3. 每一轮 Poll / Process 如何推进 + +实现入口:[IEC104_TcpServer.c](../../104/IEC104_TcpServer.c)。 + +```text +IEC104_Slave_Poll() → IEC104_TcpServer_Poll() + ├─ [内部] Prepare_Events() 查询本轮关注条件 + ├─ [内部] Wait_For_Events() + │ └─ IEC104_Net_Wait() 至多等待一次 + └─ [内部] Process_Events() 与公开 Process 共用 + ├─ 核对 service_token 和 socket/token + ├─ 处理当前连接的输入与协议期限 + ├─ [内部] Flush_Output() 推进已有及可新准备的发送 + ├─ [内部] Schedule_Protocol_Work() + └─ [内部] Accept_Clients() 最后处理接入 +``` + +当前连接未收到 EOF 时,输入处理进入 `Receive_Input`;EOF 收尾阶段通过 `IEC104_Connection_Tick` 推进协议,并继续尝试发送和交付记录。新接入的连接安排下一轮本地推进,不继承旧连接本轮的就绪标志。 + +每轮推进有预算限制,避免单个连接长期占用事件循环。产生输入事件后也会适时返回应用,让业务先处理事件。预算让出后由 `need_process` 安排续跑,不要求再收到一个新的 socket 边沿。 + +`Poll(max_wait_ms)` 的参数只限制网络等待时间。实际等待还受最近协议期限、重试期限约束;已有业务通知或本地工作时不等待。 + +### 3.1 接收先于确认超时检查 + +```text +[内部,TcpServer] Receive_Input() + ├─ IEC104_Connection_Begin_Receive_Batch() + ├─ 尝试交付上一轮保留的 I 帧 + ├─ [内部] Read_Client() → IEC104_Net_Recv() + ├─ [内部] Process_Input() → Connection_Feed() + └─ [内部] Finish_Receive_Slice() + ├─ 仅因预算/交付让出且仍有既定输入: + │ 固定尚待处理的已到达字节范围 + │ → IEC104_Connection_Yield_Receive_Batch() + │ → 安排立即续跑 + └─ 输入处理完、真正容量背压或接收无进展: + → IEC104_Connection_Tick() + → 按原期限完成超时判定 +``` + +这一顺序用于先处理已经到达的 ACK 或 TESTFR_CON。后到的数据不会持续扩大旧批次,也不会重新计算一个更晚的 t1 起点。本地发送期限和 STOP 总期限继续约束推进。 + +ACK/TESTFR 的 t1 已到期而尚未解除时,暂缓断线判定也不允许为 HIGH/NORMAL 分配新的 I 帧序号;已有帧和控制发送仍可推进,让接收路径有机会处理已到达的确认。 + +## 4. 字节接收、成帧与分派 + +实现入口:[IEC104_Receiver.c](../../104/IEC104_Receiver.c)、[IEC104_Codec.c](../../104/IEC104_Codec.c)、[IEC60870_Codec.c](../../common/IEC60870_Codec.c)、[IEC104_Connection.c](../../104/IEC104_Connection.c)。 + +```text +IEC104_Net_Recv() + → TcpServer 保存收到的字节及未消费尾部 + → IEC104_Connection_Feed() + → IEC104_Receiver_Feed() + → 收集一帧完整 APDU + → IEC104_Parse_APCI() + → I 帧另调用 IEC60870_Parse_ASDU_Header(¶meters104, ...) + → [内部,Connection] Drive() + 1. Dispatch_Received_Frame() + 2. Check_Timeouts() + 3. Advance_Control() +``` + +Receiver 一次保留一帧。未释放的帧会先被重试,后续 TCP 字节留在缓冲或网络接收队列中;半帧等待后续字节。TCP 路径按帧边界严格校验,不通过扫描丢弃任意错误字节来恢复。 + +I 帧在进入 Link 前,已检查 ASDU 公共头和已知固定长度类型的最低载荷长度。具体业务含义、字段合法性,以及未知或变长类型的进一步校验由应用承担。 + +`Dispatch_Received_Frame` 的主要分支为: + +| 收到的帧 | 调用方向 | 结果 | +| --- | --- | --- | +| I | Manager_Handle_I_S_Frame → Link_Handle_I_Frame | 校验 N(S)、N(R),受理接收及累计 ACK,保留 I 帧供上层复制 | +| S | Manager_Handle_I_S_Frame → Link_Handle_S_Frame | 受理累计 ACK,随后释放该接收帧 | +| STARTDT_ACT | Manager_Slave_Handle_Startdt_Act → Link_Handle_Startdt_Act | 准入后提交 STARTED 并排入 CON | +| STOPDT_ACT | Manager_Slave_Handle_Stopdt_Act → Link_Handle_Stopdt_Act | 准入后提交 STOPPED,推进停止排空 | +| TESTFR_ACT | Manager_Handle_Testfr_Act → Link_Handle_Testfr_Act | 排入 TESTFR_CON | +| TESTFR_CON | Manager_Handle_Testfr_Con → Link_Handle_Testfr_Con | 匹配当前探测时结束等待 | +| 合法 STARTDT_CON / STOPDT_CON | 当前子站直接释放 | 不改变启停状态,不产生启停通知,也不刷新 t3 活动时刻 | + +表中 Manager 和 Link 名称省略了统一的 `IEC104_SendManager_`、`IEC104_Link_` 前缀;完整名称见后续调用链及接口汇总。 + +## 5. STARTDT:从受理到确认写出 + +实现入口:[IEC104_Link_Slave_StartStop.c](../../104/IEC104_Link_Slave_StartStop.c)、[IEC104_SendManager.c](../../104/IEC104_SendManager.c)。 + +```text +主站发送 STARTDT_ACT + → Connection 分派前检查:本次是否改变状态、启停通知是否有空间 + → IEC104_SendManager_Slave_Handle_Startdt_Act() + ├─ IEC104_SendBuffer_Reserve_S_U_Frame() + ├─ IEC104_Link_Handle_Startdt_Act() + │ ├─ IEC104_Build_Uframe(..., STARTDT_CON) + │ └─ dt_state = STARTED;dt_procedure = IDLE + ├─ 若原状态为 STOPPED:IEC104_AsduQueue_Reset_High() + └─ IEC104_SendBuffer_Commit_Frame(),保存待发 STARTDT_CON + → Connection 保存实际启停变化,取消旧 STOP 总计时 + → TcpServer 将 ACTIVATED 入事件 FIFO + → 后续发送推进把 CON 按发送顺序写出 + → IEC104_Link_Complete_Startdt_Con_Send() +``` + +**状态在 ACT 成功受理时就已经是 STARTED。** `Complete_Startdt_Con_Send` 只处理该 CON 的发送完成,不再次设置 STARTED,防止旧 CON 覆盖后来 STOP 已提交的状态。 + +这里有三个不同的时点: + +1. ACT 受理:Link 状态已改变,ACTIVATED 通知已经保存。 +2. CON 写出:控制帧按发送顺序完成。 +3. 应用读到 ACTIVATED:由应用何时调用 `NextEvent` 决定,可能在 CON 写出之前,也可能在之后。 + +STARTDT_CON 尚待发送时,业务可以提交符合准入条件的应答。应答进入 HIGH,实际新 I 帧排在已有控制 FIFO 之后;已经占用发送缓冲的旧 I 帧仍先完成。 + +已是 STARTED 时重复收到 STARTDT_ACT,仍生成 CON,不产生新的 ACTIVATED,也不清空 HIGH。它不占用新的启停通知槽,但仍需要控制帧发送空间。 + + + +## 6. 收到 I 帧后交给业务 + +### 6.1 从 TCP 原文到 Slave 事件 + +```text +IEC104_Connection_Feed() + → IEC104_SendManager_Handle_I_S_Frame() + → IEC104_Link_Handle_I_Frame() + 校验并受理 N(S) 与累计确认 N(R) + → 更新被确认的发送记录及 NORMAL 记账 + → Connection 标记该 I 帧已经应用到 Link + → [内部,TcpServer] Deliver_I_Frame() + ├─ IEC104_Connection_Peek_Received_Frame() + ├─ 将完整 APDU 复制为 I_FRAME 事件 + └─ IEC104_Connection_Release_Received_Frame() + └─ IEC104_Receiver_Release_Frame() + → 应用调用 IEC104_Slave_NextEvent() + ├─ 内部 IEC104_TcpServer_NextEvent() 取得原事件 + ├─ 保存原 Slave 实例编号及 generation + └─ I_FRAME:IEC60870_Asdu_Create_From_Buffer() + 为 event.asdu 建立借用本事件 APDU 字节的可写视图 + 成功后安装 event.connection 的发送函数及上下文 +``` + +`NextEvent` 返回 READY 只表示交付了事件。业务先判断 `event.transport.type == IEC104_TCP_EVENT_I_FRAME`,再检查 `event.asdu_result == IEC60870_ASDU_OK`,才能使用 `event.asdu` 和 `event.connection`。非 I_FRAME 的 `asdu_result` 为 UNSUPPORTED;I_FRAME 的视图创建失败仍交付原通知用于诊断,此时 connection 为空。 + +事件已经保存完整 APDU,不需要业务调用 Receiver 或 Connection 的 Release。`transport.data.i_frame.apdu` 的前 6 字节是 APCI,余下字节为 ASDU;`event.asdu` 引用的正是这份本地副本,确认函数会修改其中的 COT/P/N。 + +### 6.2 完整解析与业务校验 + +`Create_From_Buffer` 只检查公共头及已知类型的最低长度,允许未知类型保留原文以便回复错误原因。`asdu_result == OK` 不代表所有信息对象已验证,更不表示命令可以执行。 + +完整处理顺序为: + +```text +IEC60870_Asdu_Get_Data(&event.asdu, &bytes, &length) + → IEC60870_MasterConnection_Get_Application_Layer_Parameters(&event.connection) + → IEC60870_Asdu_Parse(parameters, bytes, length, &info) + 验证精确长度、VSQ/SQ、所有对象字段;未知类型返回 UNSUPPORTED + → 依据 info.type_id 选择正确的具体对象结构 + → IEC60870_Asdu_Get_Element(&info, index, &object, sizeof(object)) + → 应用检查 COT、TEST、CA、IOA、限定词、选择/执行状态和业务权限 + → 执行业务,并按对应业务过程提交应答 +``` + +解析视图 `info` 借用参数和原文字节,二者在取对象期间必须有效且不变。`Get_Element` 复制出具体对象,但输出结构须由 Type ID 选择;尺寸足够不能代替正确的 C 类型。若随后改写事件的 COT/P/N,不能继续把旧解析视图当作更新后的公共头。 + +### 6.3 延迟处理、事件复制与连接寿命 + +`IEC104_TcpEvent_t` 只是值与字节数组,可以按值复制;`IEC104_SlaveEvent_t` 含指向自身的 ASDU 视图和 connection 上下文,**不能用结构赋值、memcpy 或搬移数组元素来保存**。事件使用期间地址必须稳定。 + +```text +需要延期处理: + 方式一:NextEvent 直接写入地址稳定的任务事件存储 + 方式二:IEC104_Slave_Copy_Event(&task_event, &event) + 复制原文字节并重建目标自引用,保留原实例与 generation + → 使用 &task_event.connection 和 &task_event.asdu 继续应答 +``` + +Copy_Event 不发送报文,也不把历史请求重新绑定到当前主站。原连接断开、重新 Init 或服务槽复用之后,旧事件字节仍可用于诊断,旧发送接口会被身份校验拒绝。复制失败保持目标不变;源和目标相同时只执行有效性校验。 + +延迟任务须逐条处理STOP/START并在STOP时取消原任务,提交前再用`Check_Request`检查当前条件。同一代次恢复START后,最终READY不代表STOP之前的旧任务重新有效。`Reject_Request`只用于当前事件的同步分派,不用于此处的延迟应答;它检查的`pending_state_event_count`不能追溯已经消费的历史启停通知。 + +READY 会替换传给 NextEvent 的整个输出;EMPTY/ERROR 保持输出原值。把事件地址交给待处理任务后,不应再用同一存储接收下一条事件。多个应答可以使用同一个原请求 connection,待发 ASDU 也可以单独构造,不必都复用 `event.asdu`。 + +需要精简任务存储时,另一种方式是保存`Slave_Get_Event_Identity`返回的纯值身份,以及业务需要的请求字段/字节。随后先处理通知并检查原请求,再用`Slave_Send_Response / Slave_Try_Send_Response`提交独立ASDU;失败需关闭原连接时用`Slave_Close_Connection_By_Identity`。这些入口保留原服务实例和原连接代次检查。总召采用此方式;完整事件及Copy_Event接口继续保留,容量及排序不因内部存储精简改变。 + + + +## 7. 应答与主动上报如何进入同一发送流程 + +实现入口:[IEC60870_Slave.c](../../common/IEC60870_Slave.c)、[IEC104_Slave.c](../../104/IEC104_Slave.c)、[IEC104_TcpServer.c](../../104/IEC104_TcpServer.c)、[IEC104_SendManager.c](../../104/IEC104_SendManager.c)、[IEC104_AsduQueue.c](../../104/IEC104_AsduQueue.c)。 + +### 7.1 请求应答:HIGH 或直接发送缓冲 + +下图为普通事件应答路径。总召确认/终止经`IEC104_Slave_Send_Response`按保存的原身份进入同一普通HIGH路径;数据经`IEC104_Slave_Try_Send_Response → IEC104_TcpServer_Try_Send_Response → IEC104_Connection_Try_Submit_Response → IEC104_SendManager_Try_Submit_Response`:当前发送缓冲空闲、两级队列均无未提交条目且链路/窗口允许时才直接组帧;否则WAIT,不缓存HIGH。到期协议工作也先返回WAIT,由网络Process处理已到达的ACK。OK后沿同一发送缓冲、记录和非阻塞写入路径推进。 + +```text +应用:IEC60870_MasterConnection_Send_ASDU / Send_ACT_CON / Send_ACT_TERM + → 共用层检查连接及函数指针,分派给 Slave 的发送适配器 + → ACT_CON 设置 COT=7、P/N=negative;ACT_TERM 设置 COT=10、P/N=0 + → 检查原服务实例、ASDU 参数与原文 + → IEC60870_Asdu_Get_Data(),取得不含 APCI 的 6~249 字节 ASDU + → IEC104_TcpServer_Send_Response(私有 server, 原 generation, bytes, length) + → 检查连接、generation、EOF 和参数 + → IEC104_Connection_Submit_Response() + ├─ 已到需要协议推进的时点: + │ IEC104_SendManager_Enqueue(..., HIGH) + └─ 可正常尝试: + IEC104_SendManager_Submit_Response() + ├─ 可以直接组帧: + │ [内部] Prepare_I_Asdu() + │ → Connection 返回 SEND_READY + └─ 暂时不能组帧: + IEC104_SendManager_Enqueue(..., HIGH) + → 入 HIGH,或容量不足返回 WAIT + → 若返回 SEND_READY: + [内部,TcpServer] Write_Output(),至多尝试一次 send + → 本次提交结果保存到该 event.connection 对应的事件中 + → 返回应用;后续 Poll / Process 继续推进 +``` + +104 适配器要求 COT/CA/IOA 为 2/2/3,`maxSizeOfASDU <= 249`;共用参数合法不代表适用于 104。`Send_ASDU` 保留公共头,确认/终止接口先修改字段再提交,WAIT、ERROR 或 FATAL 都不会回滚已经修改的字段。它们不自动执行命令、不自动组织确认后的数据或终止序列。 + +共用发送返回 true 时表示已复制接手,之后可复用本次提交的 ASDU 存储;false 时立即查询 `IEC104_Slave_Get_Last_Submit_Result(&event.connection)`。这个结果只记录该接口最近一次提交,不会随 Poll、整帧完成或 ACK 自动变化。 + +WAIT 时保留原 connection、已经准备好的 ASDU 和业务进度,继续消费通知并驱动 Poll/Process,再重试同一步。OK 后推进业务进度;ERROR 需重新判断请求是否仍有效及准入条件;FATAL 结合 RECORD/DISCONNECTED 收尾。不能重新执行已产生副作用的业务操作,也不能把旧请求的重试转入 NORMAL 或绑定到新连接。 + +两条接手路径都由 Manager 检查 STARTED + IDLE 准入。直接组帧需要发送窗口、记录槽和发送缓冲都有余量,并满足没有旧 HIGH、已有帧或公平配额阻挡等条件。存在已到期的协议检查时,Connection 可先将应答放入 HIGH,交由 Process 先处理已到达确认再判定期限。 + +HIGH 是当前连接的应答队列。STOP 受理后尚留在 HIGH 的应答会清空,没有逐条丢弃通知;已经组帧的 I 帧和发送记录继续按实际状态排空。新的应答在停止状态下返回 ERROR。 + +其他业务均未接手当前I帧时,示例在读取下一事件前同步调用`IEC104_Slave_Reject_Request(slave, &event, IEC60870_ASDU_COT_UNKNOWN_TYPE)`。该入口只将原文COT改为44并置否定位,复用上述普通HIGH路径;输入事件及其最近提交结果保持不变,直接检查返回值。两条连续命令可逐条提交,允许在总召期间应答,不设置示例待应答槽。WAIT仅表示HIGH容量不足,示例丢新并累计overloads/dropped_requests,保留连接与已接手内容;ERROR继续处理失效原因及通知,FATAL可能已写出字节,不重试并排空收尾通知。cause范围、未读启停检查和同步调用限制见[同步拒绝当前请求](IEC104_API接口汇总.md#reject-request)。 + +### 7.2 主动上报:NORMAL + +```text +应用:取得 Slave 的应用层参数 + → 具体信息对象 Create() + → IEC60870_Asdu_Create(),设置公共头和 SQ + → IEC60870_Asdu_Add_InformationObject(),逐个编码;首个对象确定 Type ID + → IEC104_Slave_Enqueue_Event(slave, &asdu) + → 检查 104 参数布局,IEC60870_Asdu_Get_Data() + → IEC104_TcpServer_Enqueue_Event() + → IEC104_AsduQueue_Enqueue(..., NORMAL) + → 有活动连接且未 EOF 时,安排本地推进 + → Poll / Process + → IEC104_Connection_Prepare_Next_Send() + → IEC104_SendManager_Prepare_Next_Send() + → 从可发送的 HIGH / NORMAL 中选择条目 +``` + +这个入口只缓存 ASDU,不立即写 socket。NORMAL 可以跨断线保留;空间不足时允许覆盖最旧的原文,包含尚未收到 ACK 的原文。覆盖原文不等于确认对应 I 帧,独立发送记录和 k 窗口义务仍保留。 + +Add 返回 FULL 时,当前非空 ASDU 保持有效:先提交当前 ASDU,重新创建下一条,再重试尚未加入的那个对象。若空容器也装不下该对象,应检查缓冲容量、参数上限和对象大小,不能反复提交空容器。库不自动拆分 ASDU。Add 成功后可复用信息对象;Enqueue_Event 返回 OK 后可复用 ASDU 字节区。ASDU 参数由容器借用,仍须在容器使用期间有效且不变。 + +默认 HIGH 优先。设置非零 `high_priority_burst_limit` 后,NORMAL 有待发条目时按配额获得发送机会。优先级只影响下一条尚未组帧的 ASDU,不改变已经准备好的帧顺序。 + +### 7.3 准备新 I 帧的固定提交顺序 + +```text +IEC104_SendManager_Prepare_Next_Send() + ├─ 已有发送队首 → 直接返回该帧剩余字节 + └─ 没有已有帧: + IEC104_AsduQueue_Peek_Unsubmitted() + 原文跨环尾时:IEC104_AsduQueue_Copy_Asdu();连续原文直接借用 + → [内部,SendManager] Prepare_I_Asdu() + 1. 检查独立发送记录槽 + 2. IEC104_SendBuffer_Reserve_I_Frame() + 3. IEC104_Link_Prepare_I_Frame() + → IEC104_Build_Iframe() + → 固定 N(S)、N(R) 快照,推进下一个预留 N(S) + 4. IEC104_SendBuffer_Commit_Frame() + 5. Manager 建立 I 帧发送记录 + 6. 队列来源才调用 IEC104_AsduQueue_Mark_Submitted() + 7. 返回当前帧的待写字节 +``` + +直接应答复用同一个 `Prepare_I_Asdu`,但没有队列条目,因此跳过第 6 步。 + +一旦 Link Prepare 成功,N(S) 已经分配,后续交接异常需结束连接,不能撤回编号再用。SendBuffer 接手完整 APDU、Manager 建记录、队列 Mark 的先后顺序由 Manager 内部维护。 + +| 对象 | 组帧提交后的状态 | +| --- | --- | +| HIGH 原文 | 队列条目立即释放,APDU 已由 SendBuffer 保存 | +| NORMAL 原文 | 标记已提交,通常继续保留到 ACK;仍可能因覆盖策略被覆盖 | +| SendBuffer | 持有当前待写 APDU | +| Manager 记录 | 保存帧身份、序号快照、优先级、时间与后续完成事实 | + +## 8. 实际写 socket、部分发送与整帧完成 + +实现入口:[IEC104_SendBuffer.c](../../104/IEC104_SendBuffer.c)、[IEC104_SendManager.c](../../104/IEC104_SendManager.c)、[IEC104_Connection.c](../../104/IEC104_Connection.c)。 + +```text +[内部,TcpServer] Flush_Output() + → Prepare_Output() + → IEC104_Connection_Prepare_Next_Send() + → IEC104_SendManager_Prepare_Next_Send() + → IEC104_SendBuffer_Peek_Send_Head() + → Write_Output() + → IEC104_Net_Send(pending_data, remaining_bytes) + → IEC104_Connection_Handle_Send_Progress(本次实际新增字节数) + → IEC104_SendManager_Handle_Send_Progress() + → IEC104_SendBuffer_Record_Written_Bytes() + ├─ 未写完整:保存偏移,后续继续 + └─ 已写完整: + 1. 按原帧类型调用对应 Link Complete + 2. I 帧记录标记 local_send_completed,保存 written_at_ms + 3. IEC104_SendBuffer_Commit_Send_Completion() + → 更新活动时刻、接收确认及相关计时 + → 再尝试受理保留的控制请求、推进控制发送 +``` + +整帧完成时的分派为: + +| 已写完的帧 | Link 完成接口 | +| --- | --- | +| I | `IEC104_Link_Complete_I_Send` | +| S | `IEC104_Link_Complete_S_Ack_Send` | +| STARTDT_CON | `IEC104_Link_Complete_Startdt_Con_Send` | +| STOPDT_CON | `IEC104_Link_Complete_Stopdt_Con_Send` | +| TESTFR_ACT | `IEC104_Link_Complete_Testfr_Act_Send` | +| TESTFR_CON | 无需对应 Link Complete,完成发送缓冲提交即可 | + +部分写入只推进偏移,不提交整帧 Complete;暂不可写保留原帧,等待后续推进。同一段实际写出的字节只能登记一次,即使随后协议推进返回 FATAL,也不能重报。 + +发送顺序受以下规则约束: + +- 已占用 I 帧槽的帧先完成,即使第一次 send 还没写出任何字节。 +- S/U 控制帧按已有 FIFO 顺序发送。 +- 当前 I 帧和已有控制帧处理完后,才选择下一条新 I 帧。 +- 整帧写出后,SendBuffer 可释放帧字节;Manager 的 I 帧记录继续等待 ACK 和事件转移。 + +真实发送错误走 `IEC104_Connection_Handle_Send_Failure`,随后执行断线收尾。连续无进展的重试采用有界推进和发送重试期限,原有本地发送期限不会因此重置。 + +## 9. 对端 ACK、NORMAL 释放与 RECORD 事件 + +ACK 可以由对端的 S 帧携带,也可以由 I 帧的 N(R) 捎带。 + +```text +接收到含 N(R) 的 I / S 帧 + → IEC104_SendManager_Handle_I_S_Frame() + 1. 检查 ACK 覆盖的记录和本地完成事实 + 2. IEC104_Link_Handle_I_Frame() / IEC104_Link_Handle_S_Frame() + → Link 接受累计确认 + 3. 对全部本次确认的记录设置 peer_acknowledged + 4. NORMAL: + IEC104_AsduQueue_Confirm_Next_Normal_Entry() + → 释放保留原文,或核减已经覆盖的原文记账 + → [内部,TcpServer] Consume_Records() + ├─ IEC104_Connection_Peek_Confirmed_Record() + ├─ 将记录复制为 RECORD 事件,closing = 0 + └─ IEC104_Connection_Remove_Confirmed_Record() + → 应用 IEC104_Slave_NextEvent() + → 取得 event.transport.data.record 中的 RECORD 副本 +``` + +正常 RECORD 表示对应 I 帧已经整帧写出,并已被对端累计确认。对端 ACK 只能覆盖已经提交本地 Complete 的 I 帧;不能确认一个只是预留了 N(S)、尚未完成写出的帧。 + +**记录槽在 RECORD 成功进入事件 FIFO 后就释放,不必等应用取走该事件。** 普通通知空间或本轮交付预算不足时,已确认记录暂留 Manager,仍占记录槽;之后继续转移。 + +需要区分四个进度: + +| 进度 | 判断依据 | 业务含义 | +| --- | --- | --- | +| 已接手 ASDU | 提交返回 SUBMIT_OK | 库已复制,业务原始缓冲可复用 | +| 已整帧写出 | 记录的 local_send_completed | APDU 已交给本地网络发送路径 | +| 已获链路确认 | 记录的 peer_acknowledged | 对端已累计确认该 I 帧 | +| 已转为事件 | NextEvent 可取到 RECORD | 记录事实已交付应用 | + +链路 ACK 不代表对端业务已经执行。RECORD 不包含 ASDU 原文或应用自定义任务标识,业务需要的命令完成状态仍由业务协议和自身上下文判断。 + +## 10. STOPDT、重复 ACT 与交错启停 + +```text +主站发送 STOPDT_ACT + → Connection 检查实际状态变化需要的通知容量 + → IEC104_SendManager_Slave_Handle_Stopdt_Act() + ├─ IEC104_Link_Handle_Stopdt_Act() + │ └─ STOPPED + STOP_DRAINING + └─ IEC104_AsduQueue_Reset_High() + → 保存 DEACTIVATED 事件,启动或保留 STOP 总期限 + → [内部,Connection] Advance_Control() + → Queue_Pending_Control() + → IEC104_SendManager_Slave_Queue_Stopdt_Response() + → IEC104_Link_Prepare_Stopdt_Response() + ├─ 需要确认接收数据:先准备 S + ├─ 已有 S 未覆盖最新接收位置:等待该 S 完成后继续 + ├─ 还有已预留 I 帧未被对端 ACK:等待 + └─ 条件满足:生成 STOPDT_CON,过程回到 IDLE + → 将生成的 S / CON 提交到控制 FIFO + → 实际写出 STOPDT_CON + → IEC104_Link_Complete_Stopdt_Con_Send() + → 匹配本轮记录的 STOP_CON 完成后,结束 STOP 总计时 +``` + +STOP ACT 受理即停止新 I 帧准入,TCP 仍保持连接。停止排空期间继续处理所需的 S 确认和已有发送,不把状态切换推迟到 STOPDT_CON 写完。 + +覆盖全部接收数据的 S 已在 FIFO 中时,可以把 STOPDT_CON 排在它后面,不必等待该 S 实际写完才生成 CON。 + +| 场景 | 当前实现行为 | +| --- | --- | +| STARTED 时重复 START | 仍排入 STARTDT_CON;不产生新启用通知,不清空 HIGH | +| STOPPED 且已结束排空时重复 STOP | 再进行一次停止应答,条件满足后排入 STOPDT_CON;不产生新停用通知 | +| STOP_DRAINING 时重复 STOP | 忽略重复请求,保留原停止流程,不逐条追加 CON、不延长期限 | +| START_CON 尚未写完又来 STOP | STOP 可以受理;旧 START_CON 后续完成不会重新激活 | +| STOP 尚未生成 CON 又来 START | START 准入后立即重新激活,并取消旧 STOP 等待,不暂存为未来请求 | +| STOP_CON 已生成后又来 START | 旧 CON 保持 FIFO 顺序;旧 STOP_CON 完成不覆盖新的 STARTED | + +启停不重置本连接的 I 帧序号。被 STOP 清掉的 HIGH 不会因再次 START 而恢复。 + +## 11. S 确认、TESTFR 与所有期限如何驱动 + +实现入口:[IEC104_Connection.c](../../104/IEC104_Connection.c)、[IEC104_Link.c](../../104/IEC104_Link.c)。 + +### 11.1 接收确认 + +I 帧受理后,Connection 从首个尚未确认 I 帧的成帧时刻记录 t2 起点。 + +```text +[内部,Connection] Advance_Control() + → Queue_Pending_Control() + → IEC104_Link_Can_Prepare_S_Ack(w, t2 是否到期) + → IEC104_SendManager_Queue_S_Ack() + → IEC104_Link_Prepare_S_Ack() + → 提交 S 到 SendBuffer + → 实际发送完整 + → IEC104_Link_Complete_S_Ack_Send() +``` + +收到的未确认 I 帧达到 w,或 t2 到期时,会尝试生成 S。发送 I 帧也能捎带 N(R),但只有整帧完成提交后才推进实际发出的确认位置。仍有未确认接收数据时,不因后来又收到 I 帧而续期。 + +### 11.2 链路探测 + +```text +空闲达到 t3,且当前允许发起探测 + → IEC104_SendManager_Queue_Testfr_Act() + → IEC104_Link_Prepare_Testfr_Act() + → 控制 FIFO + → ACT 整帧写完 + → IEC104_Link_Complete_Testfr_Act_Send() + → Connection 开始 TESTFR 的 t1 + → 收到 TESTFR_CON + → IEC104_SendManager_Handle_Testfr_Con() + → IEC104_Link_Handle_Testfr_Con() + → 匹配等待过程后取消该 t1 +``` + +对端主动发来的 TESTFR_ACT 走另一条应答路径:`IEC104_SendManager_Handle_Testfr_Act` 调用 `IEC104_Link_Handle_Testfr_Act`,生成 TESTFR_CON 并进入控制 FIFO。提前或没有匹配过程的 TESTFR_CON 不缓存为未来确认。 + +### 11.3 期限查询与执行 + +```text +TcpServer 查询等待条件 + → IEC104_Connection_Get_Wait_Time() + → [内部] Collect_Deadlines() + → IEC104_SendManager_Get_Deadlines() + → 汇合 Connection 的 TESTFR、STOP、t2、t3 期限 + → 计算最近需要唤醒的时刻 + +输入或定时/本地唤醒推进 + → IEC104_Connection_Tick() / 其他推进入口 + → Drive() + → Check_Timeouts() + → Advance_Control() +``` + +当前实现统一在 Connection 形成期限视图,发送事实仍由 Manager/SendBuffer 保存;并非把所有起点都复制成 Connection 的独立字段。 + +| 期限 | 起点或依据 | 到期动作 | +| --- | --- | --- | +| 本地发送总期限 | 当前发送队首的 queued_at_ms | 该帧未完成则登记故障并收尾 | +| I 帧 ACK t1 | 最老已写完未 ACK 的 I 帧 written_at_ms | 确认超时故障 | +| TESTFR t1 | 本端 TESTFR_ACT 实际写完 | 未获匹配 CON 则探测超时 | +| STOP 总期限 | STOP ACT 的原成帧时刻,在受理时启用;受理前的 WAIT 也计入 | 未完成该停止应答则停止超时 | +| t2 | 首个尚未确认 I 帧的原成帧时刻 | 尝试发 S 确认 | +| t3 | 最近有效接收活动或本地整帧发送完成 | 尝试发 TESTFR_ACT | + +t2/t3 只有在控制容量和 Link 条件允许时才作为可调度动作,避免无容量时按已到期时间不断空转。协议时间使用单调毫秒时钟,Windows 后端通过 `IEC104_Net_Now_Ms` 提供;与业务的 CP56Time2a/RTC 时间分开。 + + + +## 12. 容量不足时,流程在哪里等待 + +通知 FIFO、ASDU 队列、发送缓冲和发送记录是不同的存储,等待位置也不同。容量配置见 [IEC104_TcpServer_Config.h](../../104/IEC104_TcpServer_Config.h)。 + +| 等待原因 | 保留内容与停止位置 | 继续条件 | +| --- | --- | --- | +| 普通通知空间不足,已有受理 I 帧 | Receiver 保留该 I 帧,TcpServer 置 receive_paused,暂停继续接收 | NextEvent 释放空间,再由 Process/Poll 复制并释放 I 帧 | +| 普通通知空间不足,只有已确认记录 | 记录暂留 Manager;不会单凭这项积压阻挡前面的 S/U 处理或 CON 发送 | 有普通空间和交付预算后转为 RECORD | +| 启停专用槽满,下一 ACT 会改变状态 | 该 ACT 留在 Receiver,尚未改变 Link 状态,也不为它生成 CON | NextEvent 消费启停通知后,由 Process/Poll 重试受理 | +| 控制帧槽满 | 需要预留槽的 START/TESTFR 请求暂缓,已有控制发送保留 | 既有帧完成并释放控制槽后重试 | +| HIGH 空间不足 | 本次业务应答未接手,提交返回 WAIT | 应用保留原文,在连接和准入仍有效时重试 | +| k 窗口用尽 | 已提交发送继续等待确认,不为新业务分配 N(S) | 收到合法累计 ACK | +| 发送记录槽用尽 | 即使有些帧已 ACK,也不能再建新 I 帧记录 | 已确认记录成功复制到事件 FIFO 后释放槽 | +| socket 暂不可写 | 当前 APDU 和偏移保留 | 可写或适用的重试调度后继续 | + +其中,**启停槽满不再扣住已经生成的 CON**。限制发生在下一次改变状态的 ACT 受理之前;已有 CON 仍可续写。同状态重复 ACT 不需要新增启停通知槽,仍按自身控制发送条件处理。 + +不过,保留的 ACT 或 I 帧仍占据同一 TCP 字节流的前部,后续帧不能越过它。当前实现保留这个顺序约束,应用应及时消费通知。等待不直接等于断线,但不会延长原协议期限。 + +`NextEvent` 本身只复制、移除最早事件并安排后续工作,不执行接收、发送或协议受理。因此“读走一条通知”和“被暂缓的 ACT 得到受理”之间,还需要一次 Process/Poll 推进。 + +通知 FIFO 保证已经入队的通知逐条交付。启停变化不合并、不覆盖;但 ACK 回执若因普通容量或预算暂留内部,可能晚于随后入队的启停通知。不能把所有 RECORD 与启停事件理解为严格按网络发生时刻排序。 + + + +## 13. EOF、异常断开与重连 + +### 13.1 对端正常 EOF + +```text +IEC104_Net_Recv() 返回 EOF + → IEC104_AsduQueue_Limit_Normal_Send() + → 标记 rx_eof,结束接收批次并检查原期限 + → 继续发送当前已编号帧、已受理 HIGH 和控制帧 + → [内部,TcpServer] Prepare_Output() + ├─ 只是已确认记录等待事件转移而没有记录槽: + │ 保留应答,等预算恢复或 NextEvent 释放空间 + └─ 已排空,或继续需要新的 ACK / STARTDT: + Drop_Client() +``` + +EOF 后,尚未编号的 NORMAL 和后来新入队的 NORMAL 留给下一连接;已经编号的当前 NORMAL I 帧仍属于发送收尾范围。新提交的业务应答被底层 Send_Response 拒绝。收尾不再等待对端送来新的 ACK 或 STARTDT,原发送和协议期限仍生效。 + +### 13.2 统一断线收尾 + +接收错误、发送错误、协议故障、超时、`Close_Connection` 和服务 `Close` 都会进入相应断线收尾。 + +```text +[内部,TcpServer] Drop_Client() + 1. 必要时 IEC104_Connection_Handle_Transport_Closed(),登记传输故障 + 2. IEC104_Net_Close(),关闭客户端并使旧 socket token 失效 + 3. 复制尚未交付、但 Link 已受理的 I 帧 + 4. IEC104_Connection_Get_Record_By_Offset() + → 将剩余发送记录转为 RECORD,closing = 1 + 5. 保存 DISCONNECTED,作为本 generation 的最后一条事件 + 6. IEC104_Connection_Close() + → 结束 Receiver、Manager、SendBuffer + 7. IEC104_AsduQueue_Detach() + → 清 HIGH,保留现存 NORMAL,恢复为离线待发 + 8. 清理客户端运行状态 +``` + +收尾使用专用预留,不覆盖已有通知。`closing = 1` 只表示记录由收尾交付;是否完整写出、是否已 ACK,仍看记录中的两个事实字段,不能把所有收尾记录都当作未发送。 + +DISCONNECTED 保存关闭前的会话快照,主动关闭时该快照的 phase 也可能仍为 ACTIVE。判断当前是否持有 TCP 连接,应查询服务状态中的 `connected`;历史事件不会随新连接改写。 + +单纯断开不额外产生 DEACTIVATED。需要结束当前主站但继续监听时,应用调用: + +```text +IEC104_Slave_Close_Connection(slave, &原连接事件, error) + → 校验原 Slave 实例及事件身份 + → IEC104_TcpServer_Close_Connection(私有 server, 原 generation, error) +``` + +该接口立即收尾,不执行 STOPDT 握手或等待 ACK;它保留监听和 NORMAL。事件可以是原连接的连接/启停通知,也可以是 I_FRAME,不要求存在可用的 ASDU。其他服务实例的事件返回 INVALID_ARGUMENT;当前无连接返回 NOT_CONNECTED;已有新连接但原 generation 过期返回 STALE_GENERATION,均不会关闭新主站。 + +### 13.3 接入下一连接 + +```text +旧连接 I/O 结束 + → 旧事件已保存 + → Connection_Close + → AsduQueue_Detach + → 应用消费旧连接尚未读完的启停通知 + → 有足够 CONNECTED 普通通知空间 + → Net_Accept + → 分配新 generation + → AsduQueue_Rebind + → Connection_Init + → 保存 CONNECTED + → 新主站 STARTDT_ACT 受理 + → 保存 ACTIVATED,并按原顺序重新选择现存 NORMAL 组帧 +``` + +旧启停事件没读完时,不把新客户端接入协议会话;读完后,旧 DISCONNECTED 可以还在 FIFO 中,只要新建连通知所需空间足够,就能接入。 + +该场景保留的通知顺序为: + +```text +DISCONNECTED(旧 generation) + → CONNECTED(新 generation) + → ACTIVATED(新 generation,之后发生时) +``` + +NORMAL 恢复依据是仍保留的原文,不会恢复已经覆盖的旧条目。未 ACK 不保证对端没有收到,因此跨连接恢复可能重复,业务应按自身协议处理重复。 + + + +## 14. 接入应用自己的事件循环 + +独立循环使用 `IEC104_Slave_Poll` 即可;已有 select、GUI 或其他统一事件循环时,使用 `IEC104_Slave_Prepare / Process` 替代 Poll。Slave 复用 TCP 层的调度值类型,不需要应用取得私有 TcpServer。 + +```text +1. IEC104_Slave_NextEvent() + 先处理已有业务通知,逐条读到 EMPTY。 + +2. IEC104_Slave_Prepare(slave, &info, &error) + 取得 service_token、socket/token、关注位、定时条件。 + +3. 判断是否能等待: + pending_event_count > 0 → 继续消费事件,再重新 Prepare。 + need_process != 0 → 使用当前 service_token 构造本地空批次, + 调用 Process;之后处理事件并重新 Prepare。 + 其余情况 → 更新完整 socket 注册集合及一次性定时器, + 与应用其他任务一起等待。 + +4. 将本轮 socket 就绪和定时到期汇成一个 IEC104_TcpReady_t: + 同一 socket 的读写位合并。 + token 使用实际注册时保存的值。 + timer_expired 只在定时器真正到期时置 1。 + +5. IEC104_Slave_Process(slave, &ready, &error) + 不等待,有界推进当前输入、发送、期限和接入。 + +6. 回到第 1 步。 +``` + +本地唤醒可使用以下字段组合,不伪造网络就绪: + +```c +IEC104_TcpReady_t ready = {0}; +ready.service_token = info.service_token; +/* socket_count = 0,timer_expired = 0;随后调用 Process。 */ +``` + +应用保存 socket 只用于注册。recv、send、close 均由 TcpServer 执行。 + +`Slave_Enqueue_Event`、共用连接发送、NextEvent、Process、Close_Connection、Init/Close 都可能改变等待条件,下一次等待前应重新查询。更新时移除旧 socket 以及已撤销的读写位;没有有效定时条件时取消旧定时器。 + +`timer_active == 1 && timeout_ms == 0` 表示应立即调度;`timer_active == 0` 表示没有定时器,两者不能混淆。`UINT32_MAX` 仍是有限毫秒数。超时从 Prepare 查询时刻计起,若中途处理其他任务耗时较长,应在真正等待前重新 Prepare。采用边沿或一次性通知的事件循环也必须落实 `need_process`,并按最新快照重新注册,避免预算让出后无人续跑。 + +旧服务 token 的整批通知被忽略;已关闭或替换 socket 的旧注册通知也被忽略。不要把旧通知的 token 换成新值,否则会破坏隔离。 + + + +## 15. 整个服务关闭与对象释放 + +接入总召模块时,先Interrogation_Cancel,再读取所有业务FINISHED并清理应用资源;最后在Slave_Destroy之前释放总召实例。总召的业务FIFO和Slave的通知FIFO须分别读空,详见[总召接入](#flow-interrogation)。 + +使用共用站点表时,还须归还业务引用、结束全部快照、解除集合锁,并Remove所有注册节点后才可Slave_Destroy。节点Is_Reusable为true后才可释放;Close/Init保留站点表,不能用网络关闭代替业务清理。 + +```text +应用停止提交新业务 + → IEC104_Slave_Close() → IEC104_TcpServer_Close() + ├─ 当前客户端按 Drop_Client 流程收尾 + ├─ 关闭监听 socket + ├─ IEC104_AsduQueue_Abort(),清空服务级缓存 + └─ IEC104_Net_Cleanup() + → 应用撤销自有事件循环中的 socket 注册和定时器 + → IEC104_Slave_NextEvent() + 逐条处理保留下来的事件,直到 EMPTY + → IEC104_Slave_Destroy() + ├─ IEC104_TcpServer_Destroy(),归还私有 TCP 槽 + └─ 归还 Slave 槽,保留不回绕的实例编号历史 + → 应用保存的句柄置 NULL +``` + +Close 不等待业务发送或 ACK,保留未读事件,但不保留 NORMAL 缓存。Destroy 要求服务已经关闭或尚未 Init,且事件已读空;不会隐式丢弃事件或替调用者关闭运行中的服务。 + +Close 后读空事件,也可以再次 Init 重开;新服务和连接的身份编号不复用旧编号。Init 失败时句柄仍归应用持有,可处理错误后重试或 Destroy。 + +运行中 Destroy 返回 INVALID_STATE;已经关闭但仍有事件返回 EVENTS_PENDING。失败时句柄仍有效,不能假定资源已经归还。Close 即使返回错误也应保留当次诊断并检查状态、继续处理可读取的收尾通知,不能跳过返回值直接丢弃句柄。 + +生命周期与业务发送状态应分别判断: + +| 状态 | 可以做什么 | 需要等待或先处理什么 | +| --- | --- | --- | +| Create 成功,未 Init | 配置、Get_Status、取得标准应用层参数,或 Destroy | 网络及队列尚未运行 | +| Init 成功,尚无主站 | Poll/Prepare/Process、NORMAL 入队、状态查询 | 等待 CONNECTED,再等待 STARTDT | +| 已连接,Link STOPPED | 处理事件、缓存 NORMAL、继续驱动控制帧 | 新业务应答不能通过准入 | +| STARTED 且准入允许 | 原连接应答和 NORMAL 调度 | 仍受窗口、记录、队列和 socket 容量约束 | +| 单连接收尾、监听仍在 | 读取历史事件、保留 NORMAL,继续驱动下一次接入 | 已有任务不得绑定到新主站 | +| Close 后 | 读取剩余事件、Get_Status;读空后 Init 或 Destroy | Close 已清除 NORMAL/HIGH,不再执行网络推进 | +| Destroy 成功 | 将保存的句柄置 NULL | 旧服务指针不能继续使用 | + +## 16. 不在 TCP 子站主线中的已有接口 + +以下实现可独立使用,按需要由对应集成方调用: + +| 实现文件 | 已有调用关系或用途 | +| --- | --- | +| [IEC104.c](../../104/IEC104.c) | 旧全局逐字节入口:Rx_Reset / Legacy_Link_Reset 后调用 Rx_Feed_Byte。它不是 TcpServer 当前使用的 Receiver 路径,也不提供上述完整事件与发送调度 | +| [IEC104_Link_Master_StartStop.c](../../104/IEC104_Link_Master_StartStop.c) | 主站 Link 层 START:Prepare_Startdt_Act → 外部实际发送 → Complete_Startdt_Act_Send → Handle_Startdt_Con | +| 同上 | 主站 Link 层 STOP:Request_Stopdt → Prepare_Stopdt_Request,必要时先 S → 对应 Complete → Handle_Stopdt_Con。TCP 连接、发送缓冲和计时驱动仍需主站集成方提供 | +| [IEC60870_Time.c](../../common/IEC60870_Time.c) | 业务需要时调用日期校验、秒数/本地时间和 CP56Time2a 转换,不负责链路调度 | +| [IEC60870_Clock.c](../../common/IEC60870_Clock.c) | 提供业务时钟读取/设置;RTC 适配钩子由平台补充,不自动完成校时 ASDU 业务 | +| [IEC104_Log.c](../../104/IEC104_Log.c) | 应用按需要配置日志级别和日志输出回调,不承担收发事件调度 | + +阅读源码时,可沿以下顺序定位主要行为: + +```text +子站入口与身份绑定:IEC104_Slave.c +站总召/分组总召、撤销、业务期限与事件:104/business/IEC104_Interrogation.c +共用发送接口分派: IEC60870_Slave.c → IEC104_Slave.c 内的发送适配器 +ASDU 与信息对象: IEC60870_Asdu.c → IEC60870_InformationObjects.c +监听和网络调度: IEC104_TcpServer.c +接收字节与校验: IEC104_Receiver.c → IEC104_Codec.c + IEC60870_Codec.c +连接协议推进: IEC104_Connection.c +成组发送与 ACK: IEC104_SendManager.c +序号和启停规则: IEC104_Link.c + IEC104_Link_Slave_StartStop.c +帧字节和发送进度: IEC104_SendBuffer.c +业务 ASDU 缓存: IEC104_AsduQueue.c +平台非阻塞 I/O: IEC104_Net_Windows.c +``` + +库提供命令、校时、总召、参数和文件等信息对象的编解码,以及响应提交接口。独立召唤引擎已实现站/分组总召和计数量读取/冻结/复位,应用通过NextEvent与显式应答管理数据源。独立ClockSynchronization已实现Type 103校验、补偿、期限及原文确认,仅由Execute_Next调用应用动作。遥控、参数和文件业务尚未接入;业务级权限及硬件操作仍由应用提供。Time的编码转换、Clock的墙上时间与Net的协议单调时钟属于不同用途;默认示例校时只更新模拟值。 + diff --git a/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_设计架构.md b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_设计架构.md new file mode 100644 index 00000000..4bdba469 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/docs/104/IEC104_设计架构.md @@ -0,0 +1,530 @@ +# 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信息对象模块)。 +> **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≤receivedsizeOfIOA`(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) | +| 实际遥信/遥测数据源、遥控及设备设时 | 应用接入层 | 按业务模块请求读取点表或执行设备动作;示例提供模拟点表和模拟时钟,协议库不自动操作硬件 | + +### 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(对象及三类业务已实现) | 公共对象编解码、总召/计数量、校时校验及显式执行;继续接入真实数据源和遥控 | 自动回归覆盖地址路由、召唤进度、校时原文确认及时间边界;模拟模式不设系统时间,硬件设时和实际主站联调须另行验收 | + +--- diff --git a/IEC60870-Slave/.build/command-v1/before/docs/104/README.md b/IEC60870-Slave/.build/command-v1/before/docs/104/README.md new file mode 100644 index 00000000..264a676c --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/docs/104/README.md @@ -0,0 +1,431 @@ +# 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) | +| `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设备适配 | +| 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、分组及遍历状态;其他命令的选择/执行、校时权限及业务任务由应用接入。 + +| 层次 | 职责 | +|---|---| +| `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)。 + +## 启停主流程 + +| 角色与过程 | 调用顺序 | +|---|---| +| 主站启动 | `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)。 diff --git a/IEC60870-Slave/.build/command-v1/before/docs/README.md b/IEC60870-Slave/.build/command-v1/before/docs/README.md new file mode 100644 index 00000000..a0f1092d --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/docs/README.md @@ -0,0 +1,30 @@ +# 文档索引 + +说明文档统一放在 `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_完整调用流程.md) | 各层调用顺序与业务流程 | +| 104 | [API 接口汇总](104/IEC104_API接口汇总.md) | 104 及其依赖的公共接口 | +| 公共 | [时间工具设计](common/IEC60870_Time_设计架构.md) | CP16/CP24/CP56、日历转换与时钟接口 | +| 示例 | [子站示例说明](examples/IEC104子站示例.md) | 编译运行、命令参数、点表与总召接入 | +| 测试 | [测试说明与验证记录](test/IEC104测试文档.md) | 公共模块、104 及真实 TCP 回环测试 | diff --git a/IEC60870-Slave/.build/command-v1/before/docs/common/IEC60870_Time_设计架构.md b/IEC60870-Slave/.build/command-v1/before/docs/common/IEC60870_Time_设计架构.md new file mode 100644 index 00000000..37c95c89 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/docs/common/IEC60870_Time_设计架构.md @@ -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使用 | 使用原有整数算法计算完整天数、年月日及日内时分秒,不包含``,不调用平台时间函数 | + +这是一项**编译配置**,不会在运行时检测或加载时间库。当前平台库后端只接入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以供业务记录,但转成有效时间戳时会拒绝。已有接收、链路和发送模块的接口及行为不变,时间工具由需要处理时间戳的业务按需接入。 diff --git a/IEC60870-Slave/.build/command-v1/before/docs/examples/IEC104子站示例.md b/IEC60870-Slave/.build/command-v1/before/docs/examples/IEC104子站示例.md new file mode 100644 index 00000000..1f4768ff --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/docs/examples/IEC104子站示例.md @@ -0,0 +1,157 @@ +# 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个标度化遥测值;分组1(QOI=21)只返回遥信,分组2(QOI=22)只返回遥测。库支持全部16组,示例未配置的其余组由应用拒绝。通信通过 `IEC104_Slave`,总召通过 `IEC104_Interrogation` 接入,所有任务使用固定存储、在主循环线程串行推进。示例另含计数量召唤和Type 103校时;默认时钟为模拟值,实际设备数据源及遥控仍由应用接入。 + +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。 + +主循环向总召、计数量、校时实例广播生命周期通知,交替推进召唤业务并显式执行校时;其他类型在三个模块均未接手后兜底。原Example_Init仍可用于仅总召测试,计数量及校时通过各自初始化辅助函数补充,两个召唤实例各需独立广播状态区,校时使用共用站点快照。协议及应用动作限制见[计数量设计](../104/IEC104_CounterInterrogation_设计架构.md)和[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)。 + +在项目根目录的 PowerShell 中执行,需要 MinGW GCC: + +示例通过 `IEC104_Interrogation.h`、`IEC104_CounterInterrogation.h` 和 `IEC104_ClockSynchronization.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数,包含错误及否定应答,广播按各提交目标分别计数;不混入原字段。 + +### 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。自动回归只运行模拟模式。 + +确认发送WAIT时只重试原确认,不再次设时;STOP取消未交接确认而不撤销已完成的动作。同内容的新请求会再执行,旧时标重发可能回拨。真实数据发布方应在confirm_pending期间暂停新的带时标上报并继续驱动网络,避免持续新增NORMAL推迟确认。完整范围及期限见[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)。 + +## 主站操作和模拟点表 + +主站先建立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 | +| 2001..2008 | 标度化测量值 `M_ME_NB_1`,Type=11 | 2300、2310、2290、5000、-125、0、1234、42 | + +所有品质为0。遥测值为原始有符号16位标度值,本示例没有定义物理单位和倍率。 + +一次成功站总召依次提交6个ASDU: + +1. Type=100,COT=7,肯定激活确认。 +2. 两帧Type=1、COT=20的单点数据,每帧4个对象。 +3. 两帧Type=11、COT=20的标度化遥测,每帧4个对象。 +4. Type=100,COT=10,激活终止。 + +分组总召各提交4个ASDU:ACT_CON、两批对应类型数据、ACT_TERM。分组1数据为Type=1/COT=21,分组2数据为Type=11/COT=22;确认和终止保留请求QOI。BEGIN校验分组,首个NEED_DATA按该组初始化应用游标,不把模拟点表写进库。 + +全部应答使用原请求的连接身份。总召确认、终止及短应答走普通HIGH路径,总召数据由Submit_Batch尝试不排队提交;校时调用过action的确认也使用Try。未提交HIGH/NORMAL、当前帧或窗口背压时Try返回WAIT;校验错误仍走普通HIGH。总召数据不使用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帧依次交给总召、计数量、校时及后续业务;都未接手时,在读取下一事件前同步调用`IEC104_Slave_Reject_Request`回UNKNOWN_TYPE。该接口借用当前事件生成原文错误应答,复用库的普通HIGH路径;示例不保存other_request,也没有other_pending、other_waiting或other_waits。未支持类型的连续及相同内容有效新请求逐条尝试应答,允许穿插正在进行的总召。103现在由校时模块接手,102仍回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列表。示例常量表只用于展示业务接入,不是库规定的地址范围。逐批采样不保证整轮同一时刻,也不自动解决与突发上报的新旧排序;完整边界见[总召模块设计](../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成功代替资源清理。 + +## 验证 + +```powershell +./scripts/build.ps1 -Test +``` + +`-Test` 需要预先安装 Node.js。脚本依次运行32组Slave/站点测试(单服务及双服务配置)、130组总召/计数量/兜底/校时可控网络测试(单服务及双服务)、3组规则单测和真实本机TCP测试: + +- `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槽,使用真实容量背压限制发送。 +- `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;连接保留。服务约8秒后自行关闭,使用默认模拟时钟。 + +原114组基线由98组总召、12组计数量及4组兜底回归组成。兜底改用未支持的106/102,覆盖同批及反序、相同内容的有效新请求、k=1与写阻塞排队、HIGH满后丢新且不补发、STOP/STOPSTART取消,以及总召未结束时立即提交错误应答。启用校时的独立回归另外核对103业务。 + +新增16组校时回归覆盖无站广播45/46、同批及重复103逐条执行、FIFO中间否定、满槽丢新、Try等待不重执行、STOP/重连、Defer/FATAL、同一次采样检查期限及补偿、action内Get_Status与防重入、广播WAIT期间删除当前站及同CA替代不加入。所有action均为模拟,不设置系统时间。 + +畸形请求回归覆盖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、撤销组合及连续兜底,共核对65个总召/兜底ASDU及11次正常完成的总召,计数量另行核对;5个校时应答独立计数,其中3次执行并成功,另2次为TEST及COT拒绝。多CA由可控Net回归覆盖。 + +这些检查覆盖本示例的模拟点表流程;实际主站设备、现场点表和设备执行接口仍需联调。 diff --git a/IEC60870-Slave/.build/command-v1/before/docs/test/IEC104测试文档.md b/IEC60870-Slave/.build/command-v1/before/docs/test/IEC104测试文档.md new file mode 100644 index 00000000..e5a2fb46 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/docs/test/IEC104测试文档.md @@ -0,0 +1,271 @@ +# 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. 编译与运行 + +当前业务测试由98组总召、12组计数量、4组兜底及16组校时组成,共130组,单/双服务配置各跑一次。计数量覆盖独立实例、请求校验、Type15/37批次边界、忙隔离、冻结/复位、动作重复调用和WAIT/STALE/TEST、业务期限、STOP/断线、共享连接失败、发送背压及多站广播;兜底改用未支持的106/102,覆盖同批与反序/重复内容、窗口与写阻塞、HIGH满丢新及恢复、STOP/STOPSTART取消和活动总召中的应答。校时专项检查独立受理与执行、原文确认、补偿/期限、广播及生命周期。`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: 130 groups passed ...`、`interrogation rules: 3 groups passed ...`和`Slave example loopback: passed ...`。可控后端使用251字节HIGH和2槽召唤业务事件FIFO。真实回环使用默认存储、k=1、随机本机端口,验证总召与计数量交错、冻结/复位及后续读取、分组/广播、TEST拒绝、COT=8返回45、106/102同批回44、STOP/START、重连及8秒后的正常退出。校时另核对正常7、TEST否定7、COT=5否定45、广播实际CA及103/102同批先44再7,全部使用模拟时钟且连接保留;STOP为submitted=65、clock_executed=3、clock_synchronized=3、clock_submitted=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或栈占用;嵌入式接入须按实际工具链重新核算。 + +## 5. 验证记录 + +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`中,仅描述当时的源码版本,不属于当前可运行测试。本文不再提供已删除测试的构建命令,也不将其历史通过数计入当前结果。 + diff --git a/IEC60870-Slave/.build/command-v1/before/examples/iec104_slave_example.c b/IEC60870-Slave/.build/command-v1/before/examples/iec104_slave_example.c new file mode 100644 index 00000000..eb6b42bd --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/examples/iec104_slave_example.c @@ -0,0 +1,772 @@ +/* + * 104子站示例:总召、计数量召唤、时钟同步、模拟点表、其他命令兜底及main。 + * 两种召唤共用IEC104_Interrogation.c状态机,各自有独立实例和业务事件。 + * 每轮Slave_Poll -> NextEvent逐条交付 -> Interrogation_Process;所有调用串行。 + * 总召每批4点,模块自动推进至需要等待;8个遥信和8个遥测组成6个响应ASDU。 + * QOI=21只读遥信,22只读遥测,其余分组由应用拒绝;分组点表策略不写进协议库。 + * 同一连接至多一个未终结总召,忙时不区分QOI均否定;任务满时拒绝/计数丢弃新请求,保留当前连接。 + * 总召模块处理WAIT、STOPDT及断线;应用读取业务事件,选择类型并显式交付ASDU。 + * 计数量使用8个模拟计数器;读取快照,冻结/复位由经过事件校验的同步动作入口执行。 + * 对时默认只更新模拟UTC基准;--clock system显式选择实际时钟,协议仍使用单调时钟。 + * 不支持的其他命令当场提交COT=44否定应答,复用发送层HIGH;容量不足只丢本条并计数。 + * 默认127.0.0.1:2404、CA=1;构建命令见同目录README.md。 + */ +#include "IEC104_Interrogation.h" +#include "IEC104_CounterInterrogation.h" +#include "IEC104_ClockSynchronization.h" +#include "IEC60870_Clock.h" +#include +#include +#include + +#define EXAMPLE_EVENT_BUDGET 32U +#define EXAMPLE_COUNTER_COUNT 8U + +/* 模拟遥信点表:ioa为24位信息对象地址,value为0/1,quality为SIQ品质位(这里均为0)。 + * 表项不是线上报文,后续由SinglePointInformation_Create及ASDU容器编码成Type=1。 + * 示例点表只读;接真实数据源时,在每次NEED_DATA组包处读取所选点的当前值和品质。 */ +static const struct { uint32_t ioa; uint8_t value, quality; } example_signals[] = { + {1001, 0, 0}, {1002, 1, 0}, {1003, 0, 0}, {1004, 1, 0}, + {1005, 1, 0}, {1006, 0, 0}, {1007, 1, 0}, {1008, 0, 0} +}; +/* 模拟遥测点表:ioa为信息对象地址,value为原始有符号16位标度值,quality为QDS品质位。 + * 编码为不带时标的Type=11;本示例不定义物理单位和倍率,负数也按有符号值传输。 */ +static const struct { uint32_t ioa; int16_t value; uint8_t quality; } example_measurements[] = { + {2001, 2300, 0}, {2002, 2310, 0}, {2003, 2290, 0}, {2004, 5000, 0}, + {2005, -125, 0}, {2006, 0, 0}, {2007, 1234, 0}, {2008, 42, 0} +}; +/* 点数直接取数组长度;分批游标使用size_t,不另维护可能与点表不一致的手写数量。 */ +#define EXAMPLE_SIGNAL_COUNT (sizeof(example_signals) / sizeof(example_signals[0])) +#define EXAMPLE_MEASUREMENT_COUNT (sizeof(example_measurements) / sizeof(example_measurements[0])) + + +/* 当前取数站的应用游标;首个NEED_DATA才初始化,协议库不登记或冻结业务IOA列表。 */ +typedef struct { size_t single_index, scaled_index; } ExampleGiData; + +typedef struct { + IEC104_Slave_t *slave; /* main拥有;本上下文仅借用。 */ + IEC104_Station_t station; /* 应用拥有的固定地址站点节点,供各业务共用CA和context。 */ + IEC104_Interrogation_t *interrogation; /* 库内不透明总召任务对象。 */ + IEC104_InterrogationStatus_t gi; /* 只读快照,用于调度等待及统计。 */ + IEC104_Interrogation_t *counter; /* 独立计数量实例,与总召共用Slave,不共用任务和广播状态区。 */ + IEC104_InterrogationStatus_t ci; /* 计数量调度及统计快照,每次推进后重新查询。 */ + IEC104_ClockSynchronization_t *clock; /* 独立时钟同步模块,共用Slave及站点表。 */ + IEC104_ClockSynchronizationStatus_t cs; /* 对时执行、确认及期限的只读快照。 */ + bool clock_system, clock_valid; /* 实际设时为显式选项;clock_valid只描述模拟基准。 */ + uint64_t clock_utc_ms, clock_base_ms; /* 模拟UTC毫秒及其单调基准,不影响协议计时。 */ + IEC104_InterrogationEvent_t counter_event; /* WAIT时保留的原业务事件,重试不能换成新身份。 */ + bool counter_pending, counter_first; /* 是否持有待应答事件、本轮是否先推进计数量。 */ + int32_t counter_current[EXAMPLE_COUNTER_COUNT]; /* 模拟当前累计值,冻结及复位动作的数据源。 */ + IEC60870_BinaryCounterReading_t counter_frozen[EXAMPLE_COUNTER_COUNT]; /* 最近冻结值及BCR序号、品质。 */ + IEC60870_BinaryCounterReading_t counter_snapshot[EXAMPLE_COUNTER_COUNT]; /* 本轮读取固定的冻结副本。 */ + size_t counter_cursor, counter_end; /* 当前读取区间,只有Submit_Batch成功才推进游标。 */ + uint64_t counter_instance, counter_request; /* 快照归属;旧FINISHED不得清理新请求。 */ + IEC104_Station_t *counter_station; /* 读取期间额外持有的原站节点,结束后归还引用。 */ + unsigned long counter_completed; /* 已消费的COMPLETED通知数,包含读取及冻结/复位。 */ + ExampleGiData gi_data; /* 应用拥有的游标,库内没有session或回调参数。 */ + IEC104_Station_t *gi_station; /* 取数期间持有原站,Complete_Data或FINISHED时释放。 */ + uint64_t session_instance, session_request; /* 只清理对应请求的遍历状态及业务资源。 */ + IEC104_InterrogationEvent_t business_event; /* 纯值,可保存到后续循环应答。 */ + bool business_pending; + int batch_result; /* 本例构造结果:-1失败、0数据结束、1有数据。 */ + IEC60870_Asdu_t batch; /* 借用本应用bytes;Submit_Batch(OK)后可复用。 */ + uint8_t batch_bytes[IEC104_MAX_ASDU_LEN]; + bool events_pending; + unsigned long other_submitted, other_rejected, other_cancelled, other_overloads; +} ExampleApp; + +/* 普通应用函数,只由下面的事件分支直接调用,不注册到库中。 + * 应用自行选择本批IOA、类型及分组成员;为NEED_DATA构造一种类型的一批。 + * 真实取值应放在下列逐点组包处,值与品质的读取一致性由数据源保证;不预读整轮数据。 + * FULL不跳过当前点;本函数不推进持久游标,Submit_Batch(OK)后才推进。 + * WAIT仅保留事件,每次重试重读当前值。点值更新/突发发布与本次组包/提交须串行协调。 + * 逐批取值不保证整轮同一采样时刻。 */ +static int Example_Build_Batch(ExampleApp *app, const IEC104_InterrogationRequest_t *request) +{ + ExampleGiData *data = &app->gi_data; + IEC60870_Asdu_t *asdu = &app->batch; + IEC60870_AsduHeader_t header; + IEC104_Station_t *station = request->station_id != 0U ? + IEC104_Slave_Get_Station_By_Id(app->slave, request->station_id) : + IEC104_Slave_Get_Station(app->slave, request->common_address); + if (IEC104_Station_Get_Context(station) != app) { + return -1; + } + memset(&header, 0, sizeof(header)); + header.cause_of_transmission = request->qualifier; /* 站总召20,分组总召21..36。 */ + header.common_address = request->common_address; + header.originator_address = request->originator_address; + header.is_test = request->is_test; + if (IEC60870_Asdu_Create(asdu, IEC104_Slave_Get_Application_Layer_Parameters(app->slave), + app->batch_bytes, sizeof(app->batch_bytes), &header, 0) != IEC60870_ASDU_OK) { return -1; } + if (data->single_index < EXAMPLE_SIGNAL_COUNT) { + size_t index = data->single_index; + while (index < EXAMPLE_SIGNAL_COUNT && asdu->object_count < request->max_objects_per_asdu) { + IEC60870_SinglePointInformation_t point; + IEC60870_AsduResult_t added; + if (IEC60870_SinglePointInformation_Create(&point, example_signals[index].ioa, + example_signals[index].value, example_signals[index].quality) != IEC60870_ASDU_OK) { + return -1; + } + added = IEC60870_Asdu_Add_InformationObject(asdu, &point.object); + if (added == IEC60870_ASDU_FULL) { break; } + if (added != IEC60870_ASDU_OK) { return -1; } + ++index; + } + } else if (data->scaled_index < EXAMPLE_MEASUREMENT_COUNT) { + size_t index = data->scaled_index; + while (index < EXAMPLE_MEASUREMENT_COUNT && asdu->object_count < request->max_objects_per_asdu) { + IEC60870_MeasuredValueScaled_t point; + IEC60870_AsduResult_t added; + if (IEC60870_MeasuredValueScaled_Create(&point, example_measurements[index].ioa, + example_measurements[index].value, example_measurements[index].quality) != IEC60870_ASDU_OK) { + return -1; + } + added = IEC60870_Asdu_Add_InformationObject(asdu, &point.object); + if (added == IEC60870_ASDU_FULL) { break; } + if (added != IEC60870_ASDU_OK) { return -1; } + ++index; + } + } else { return 0; } + return asdu->object_count != 0U ? 1 : -1; +} + +/* 释放应用为当前取数持有的节点引用并清空游标,库的广播状态区仍独立保存协议进度。 + * Complete_Data成功即可调用,让下一站复用游标;不需要等本轮所有站的FINISHED。 */ +static bool Example_End_Data(ExampleApp *app) +{ + if (app->gi_station != NULL && + IEC104_Slave_Release_Station(app->slave, app->gi_station) != IEC104_STATION_OK) { return false; } + app->gi_station = NULL; + memset(&app->gi_data, 0, sizeof(app->gi_data)); + app->session_instance = app->session_request = 0U; + return true; +} + +/* 第二条事件流:应用主动取业务事件,再显式应答;没有库调用应用函数的路径。 + * 常量点表立即就绪;异步数据源可在NEED_DATA保留business_event,完成采样后再Submit。 + * WAIT先处理Slave通知并推进网络,下次重取数据;STALE表示交互已失效,不重试过期事件。 */ +static bool Example_Drain_Business(ExampleApp *app) +{ + unsigned count; + for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) { + IEC104_InterrogationEvent_t *event = &app->business_event; + IEC104_InterrogationReplyResult_t reply; + if (!app->business_pending) { + IEC104_InterrogationNextResult_t next = IEC104_Interrogation_NextEvent(app->interrogation, event); + if (next == IEC104_INTERROGATION_NEXT_EMPTY) { return true; } + if (next != IEC104_INTERROGATION_NEXT_READY) { return false; } + app->business_pending = true; + } + switch (event->type) { + case IEC104_INTERROGATION_BEGIN: { + bool accept = event->request.qualifier <= 22U; + /* 示例仅定义组1=遥信、组2=遥测;拒绝未配置的组。协议库支持全部16组。 + * 广播会先交付所有站的BEGIN;这里只作准入判断,不占用唯一取数游标。 */ + reply = IEC104_Interrogation_Reply_Begin(app->interrogation, event, accept); + break; + } + case IEC104_INTERROGATION_NEED_DATA: + /* 广播逐站取数,因此首个NEED_DATA才建立游标;用注册编号取得原站,避免同CA替换串站。 + * 应用单独Acquire,确保库交付FINISHED并归还自己的引用时,应用清理仍有有效节点。 */ + if (event->request_id != app->session_request || event->instance_id != app->session_instance) { + if (app->session_request != 0U) { return false; } + if (event->request.station_id != 0U) { + app->gi_station = IEC104_Slave_Get_Station_By_Id(app->slave, event->request.station_id); + if (app->gi_station == NULL || + IEC104_Slave_Acquire_Station(app->slave, app->gi_station) != IEC104_STATION_OK) { return false; } + } + memset(&app->gi_data, 0, sizeof(app->gi_data)); + if (event->request.qualifier == 21U) { app->gi_data.scaled_index = EXAMPLE_MEASUREMENT_COUNT; } + if (event->request.qualifier == 22U) { app->gi_data.single_index = EXAMPLE_SIGNAL_COUNT; } + app->session_instance = event->instance_id; + app->session_request = event->request_id; + } + app->batch_result = Example_Build_Batch(app, &event->request); + if (app->batch_result < 0) { reply = IEC104_Interrogation_Fail_Request(app->interrogation, event); } + else if (app->batch_result == 0) { reply = IEC104_Interrogation_Complete_Data(app->interrogation, event); } + else { reply = IEC104_Interrogation_Submit_Batch(app->interrogation, event, &app->batch); } + if (reply == IEC104_INTERROGATION_REPLY_OK && app->batch_result > 0) { + if (app->gi_data.single_index < EXAMPLE_SIGNAL_COUNT) { + app->gi_data.single_index += app->batch.object_count; + } else { app->gi_data.scaled_index += app->batch.object_count; } + } else if (reply == IEC104_INTERROGATION_REPLY_OK && app->batch_result == 0) { + /* 其他站的数据随后才开始;无需等到所有站的ACT_TERM才复用游标。 */ + if (!Example_End_Data(app)) { return false; } + } + break; + case IEC104_INTERROGATION_FINISHED: + /* 完成、本地取消、主站撤销、超时、断线和失败统一清理。 + * 真实异步采样还需在这里停止对应任务;迟到的旧FINISHED不清理新请求资源。 */ + if (event->instance_id == app->session_instance && event->request_id == app->session_request) { + if (!Example_End_Data(app)) { return false; } + } + reply = IEC104_INTERROGATION_REPLY_OK; /* 终结通知无需调用任何应答接口。 */ + break; + default: return false; + } + /* WAIT保留business_event且游标不动;下轮重新Build_Batch,不重发缓存的旧点值。 */ + if (reply == IEC104_INTERROGATION_REPLY_WAIT) { return true; } + if (reply != IEC104_INTERROGATION_REPLY_OK && reply != IEC104_INTERROGATION_REPLY_STALE) { return false; } + app->business_pending = false; + } + return true; +} + +/* 同步模拟计数器动作:执行入口先验证事件,WAIT/STALE不调用本函数。 + * 只在内存中一次完成选中组的操作;不调用网络,不再次进入召唤模块。 + * 实际外部电表命令需要独立的执行结果和重复请求策略,不能套用此同步函数。 */ +static bool Example_Counter_Action(void *context, const IEC104_InterrogationRequest_t *request) +{ + ExampleApp *app = (ExampleApp *)context; + size_t first, end, index; + if (request->is_test || (request->qualifier != 1U && request->qualifier != 2U && + request->qualifier != 5U) || request->freeze == 0U || request->freeze > 3U) { return false; } + first = request->qualifier == 2U ? 4U : 0U; + end = request->qualifier == 1U ? 4U : EXAMPLE_COUNTER_COUNT; + for (index = first; index < end; ++index) { + if (request->freeze != 3U) { + app->counter_frozen[index].value = app->counter_current[index]; + app->counter_frozen[index].sequence_number = + (uint8_t)((app->counter_frozen[index].sequence_number + 1U) & 31U); + } + if (request->freeze >= 2U) { app->counter_current[index] = 0; } + } + return true; +} + +/* 释放当前读取的节点引用和快照归属;快照字节无需清零,下次读取会整体覆盖。 + * Complete_Data成功或匹配的FINISHED都可收尾,冻结动作本身不占用读取游标。 */ +static bool Example_End_Counter(ExampleApp *app) +{ + if (app->counter_station != NULL && + IEC104_Slave_Release_Station(app->slave, app->counter_station) != IEC104_STATION_OK) { return false; } + app->counter_station = NULL; + app->counter_instance = app->counter_request = 0U; + app->counter_cursor = app->counter_end = 0U; + return true; +} + +/* 从本轮固定快照构造一个Type15、SQ=0批次,共用ASDU缓冲但不推进游标。 + * 返回1表示非空批次,0表示所选组已读完,-1表示构造失败;WAIT后仍取同一快照重组。 */ +static int Example_Build_Counter_Batch(ExampleApp *app, const IEC104_InterrogationRequest_t *request) +{ + IEC60870_AsduHeader_t header; + size_t index; + if (app->counter_cursor == app->counter_end) { return 0; } + memset(&header, 0, sizeof(header)); + header.cause_of_transmission = request->data_cause; + header.common_address = request->common_address; + header.originator_address = request->originator_address; + header.is_test = request->is_test; + if (IEC60870_Asdu_Create(&app->batch, IEC104_Slave_Get_Application_Layer_Parameters(app->slave), + app->batch_bytes, sizeof(app->batch_bytes), &header, 0) != IEC60870_ASDU_OK) { return -1; } + for (index = app->counter_cursor; index < app->counter_end && + app->batch.object_count < request->max_objects_per_asdu; ++index) { + IEC60870_IntegratedTotals_t point; + IEC60870_AsduResult_t result; + if (IEC60870_IntegratedTotals_Create(&point, (uint32_t)(3001U + index), + &app->counter_snapshot[index]) != IEC60870_ASDU_OK) { return -1; } + result = IEC60870_Asdu_Add_InformationObject(&app->batch, &point.object); + if (result == IEC60870_ASDU_FULL) { break; } + if (result != IEC60870_ASDU_OK) { return -1; } + } + return app->batch.object_count != 0U ? 1 : -1; +} + +/* 冻结值在首个NEED_DATA固定为本轮快照;WAIT重建报文也使用同一份值、序号和品质。 + * 模拟点值属于本应用;真实共享数据源应在其锁内取得快照后再进行网络提交。 */ +static bool Example_Drain_Counter(ExampleApp *app) +{ + unsigned count; + if (app->counter == NULL) { return true; } + for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) { + IEC104_InterrogationEvent_t *event = &app->counter_event; + IEC104_InterrogationReplyResult_t reply; + if (!app->counter_pending) { + IEC104_InterrogationNextResult_t next = IEC104_Interrogation_NextEvent(app->counter, event); + if (next == IEC104_INTERROGATION_NEXT_EMPTY) { return true; } + if (next != IEC104_INTERROGATION_NEXT_READY) { return false; } + app->counter_pending = true; + } + switch (event->type) { + case IEC104_INTERROGATION_BEGIN: { + IEC104_Station_t *station = IEC104_Slave_Get_Station_By_Id(app->slave, event->request.station_id); + bool accept = IEC104_Station_Get_Context(station) == app && + (event->request.qualifier == 1U || event->request.qualifier == 2U || event->request.qualifier == 5U); + if (accept && event->request.freeze != 0U && !event->request.is_test) { + reply = IEC104_CounterInterrogation_Execute_Begin(app->counter, event, Example_Counter_Action, app); + } else { + reply = IEC104_Interrogation_Reply_Begin(app->counter, event, + accept && event->request.freeze == 0U); + } + break; + } + case IEC104_INTERROGATION_NEED_DATA: { + int built; + if (event->instance_id != app->counter_instance || event->request_id != app->counter_request) { + IEC104_Station_t *station; + if (app->counter_request != 0U) { return false; } + station = IEC104_Slave_Get_Station_By_Id(app->slave, event->request.station_id); + if (station == NULL || IEC104_Station_Get_Context(station) != app || + IEC104_Slave_Acquire_Station(app->slave, station) != IEC104_STATION_OK) { return false; } + app->counter_station = station; + memcpy(app->counter_snapshot, app->counter_frozen, sizeof(app->counter_snapshot)); + app->counter_cursor = event->request.qualifier == 2U ? 4U : 0U; + app->counter_end = event->request.qualifier == 1U ? 4U : EXAMPLE_COUNTER_COUNT; + app->counter_instance = event->instance_id; + app->counter_request = event->request_id; + } + built = Example_Build_Counter_Batch(app, &event->request); + if (built < 0) { reply = IEC104_Interrogation_Fail_Request(app->counter, event); } + else if (built == 0) { reply = IEC104_Interrogation_Complete_Data(app->counter, event); } + else { reply = IEC104_Interrogation_Submit_Batch(app->counter, event, &app->batch); } + if (reply == IEC104_INTERROGATION_REPLY_OK) { + if (built > 0) { app->counter_cursor += app->batch.object_count; } + else if (built == 0 && !Example_End_Counter(app)) { return false; } + } + break; + } + case IEC104_INTERROGATION_FINISHED: + if (event->reason == IEC104_INTERROGATION_FINISH_COMPLETED) { ++app->counter_completed; } + if (event->instance_id == app->counter_instance && event->request_id == app->counter_request && + !Example_End_Counter(app)) { return false; } + reply = IEC104_INTERROGATION_REPLY_OK; + break; + default: return false; + } + if (reply == IEC104_INTERROGATION_REPLY_WAIT) { return true; } + if (reply != IEC104_INTERROGATION_REPLY_OK && reply != IEC104_INTERROGATION_REPLY_STALE) { return false; } + app->counter_pending = false; + } + return true; +} + +/* 必须在开始消费Slave事件之前创建业务模块;config本地对象按值复制,广播数组仍为借用。 + * 单站可传NULL/0;多站广播须提供覆盖全部目标的数组,保持独占和地址稳定直到模块Destroy。 */ +static bool Example_Init(ExampleApp *app, IEC104_Slave_t *slave, uint16_t common_address, + IEC104_InterrogationStationState_t *broadcast_states, size_t broadcast_capacity) +{ + IEC104_InterrogationConfig_t config; + memset(app, 0, sizeof(*app)); + app->slave = slave; + if (IEC104_Slave_Add_Station(slave, &app->station, common_address, app) != IEC104_STATION_OK) { return false; } + IEC104_Interrogation_Config_Default(&config); + config.use_station_registry = true; /* CA只在注册时指定;多站广播需提供每站状态区。 */ + config.broadcast_states = broadcast_states; + config.broadcast_capacity = broadcast_capacity; + config.max_objects_per_asdu = 4U; + app->interrogation = IEC104_Interrogation_Create(slave, &config); + if (app->interrogation == NULL) { (void)IEC104_Slave_Remove_Station(slave, &app->station); } + return app->interrogation != NULL; +} + +/* 计数量实例可选,独立提供广播状态区;不共享总召的数组或取数游标。 + * CLI仅注册一站。测试可给多个CA注册同一app,它们会指向同一组模拟计数器; + * 真实多站应用应让各站context分别拥有计数器数据,尤其不要逐站复位同一物理源。 */ +static bool Example_Init_Counters(ExampleApp *app, + IEC104_InterrogationStationState_t *broadcast_states, size_t broadcast_capacity) +{ + static const int32_t initial[EXAMPLE_COUNTER_COUNT] = {120000, 345600, 7890, 0, 1, 65535, INT32_MAX, 42}; + IEC104_InterrogationConfig_t config; + size_t index; + if (app->counter != NULL) { return false; } + IEC104_Interrogation_Config_Default(&config); + config.use_station_registry = true; + config.broadcast_states = broadcast_states; + config.broadcast_capacity = broadcast_capacity; + config.max_objects_per_asdu = 4U; + app->counter = IEC104_CounterInterrogation_Create(app->slave, &config); + if (app->counter == NULL) { return false; } + for (index = 0U; index < EXAMPLE_COUNTER_COUNT; ++index) { + app->counter_current[index] = initial[index]; + app->counter_frozen[index].value = initial[index]; + } + return true; +} + +/* 执行检查已完成时区换算和滞留补偿;动作不重复补偿,也不推进网络事件循环。 */ +static bool Example_Clock_Action(void *context, const IEC104_ClockSynchronizationRequest_t *request) +{ + ExampleApp *app = (ExampleApp *)context; + if (app->clock_system) { return IEC60870_Clock_Set(&request->local_time) != 0; } + app->clock_utc_ms = request->utc_ms; + app->clock_base_ms = request->base_ms; + app->clock_valid = true; + return true; +} + +/* config可为NULL采用默认值;创建须早于消费Slave通知,实例独占自身请求和确认状态。 */ +static bool Example_Init_Clock(ExampleApp *app, const IEC104_ClockSynchronizationConfig_t *config) +{ + IEC104_ClockSynchronizationConfig_t defaults; + if (app->clock != NULL) { return false; } + if (config == NULL) { + IEC104_ClockSynchronization_Config_Default(&defaults); + config = &defaults; + } + app->clock = IEC104_ClockSynchronization_Create(app->slave, config); + return app->clock != NULL; +} + +/* 每轮最多执行事件预算条请求;WAIT先交回Slave消费通知,不重放已经执行的动作。 */ +static bool Example_Drain_Clock(ExampleApp *app) +{ + unsigned count; + if (app->clock == NULL) { return true; } + for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) { + IEC104_ClockSynchronizationExecuteResult_t result = + IEC104_ClockSynchronization_Execute_Next(app->clock, Example_Clock_Action, app); + if (result == IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY || + result == IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_WAIT) { return true; } + if (result != IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_OK) { return false; } + } + return true; +} + +static void Example_Cancel(ExampleApp *app) +{ + IEC104_Interrogation_Cancel(app->interrogation); + if (app->counter != NULL) { + IEC104_Interrogation_Cancel(app->counter); + (void)IEC104_Interrogation_Get_Status(app->counter, &app->ci); + } + if (app->clock != NULL) { + IEC104_ClockSynchronization_Cancel(app->clock); + (void)IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs); + } + (void)IEC104_Interrogation_Get_Status(app->interrogation, &app->gi); +} + +/* 每次等待前重新查询剩余期限,避免采样/业务处理耗时后继续使用旧的毫秒数。 + * 本地可推进时不休眠;否则本例网络等待最多50ms,并受最近业务期限限制。 */ +static uint32_t Example_Wait_Time(ExampleApp *app, uint32_t maximum_ms) +{ + if (!IEC104_Interrogation_Get_Status(app->interrogation, &app->gi)) { return 0U; } + if (app->counter != NULL) { + if (!IEC104_Interrogation_Get_Status(app->counter, &app->ci)) { return 0U; } + if (app->ci.events_pending || app->ci.pending_events != 0U || app->ci.need_process) { return 0U; } + if (app->ci.timer_active && app->ci.timeout_remaining_ms < maximum_ms) { + maximum_ms = app->ci.timeout_remaining_ms; + } + } + if (app->clock != NULL) { + if (!IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs)) { return 0U; } + if (app->cs.events_pending || app->cs.execute_pending || app->cs.need_process) { return 0U; } + if (app->cs.timer_active && app->cs.timeout_remaining_ms < maximum_ms) { + maximum_ms = app->cs.timeout_remaining_ms; + } + } + if (app->events_pending || app->gi.events_pending || app->gi.pending_events != 0U || + app->gi.need_process) { return 0U; } + if (app->gi.timer_active && app->gi.timeout_remaining_ms < maximum_ms) { return app->gi.timeout_remaining_ms; } + return maximum_ms; +} + +/* 状态事件交给所有实例;I帧依次由总召、计数量、时钟同步及未知类型兜底接手。 */ +static bool Example_Handle_Event(ExampleApp *app, const IEC104_SlaveEvent_t *event) +{ + IEC104_InterrogationEventResult_t result; + /* Receiver已过滤线上基础格式错误;仍防御性过滤不可用视图,避免误交给业务分派或未知类型兜底。 */ + if (event->transport.type == IEC104_TCP_EVENT_I_FRAME && event->asdu_result != IEC60870_ASDU_OK) { return true; } + result = IEC104_Interrogation_Handle_Event(app->interrogation, event); + if (app->counter != NULL && (event->transport.type != IEC104_TCP_EVENT_I_FRAME || + result == IEC104_INTERROGATION_EVENT_IGNORED)) { + IEC104_InterrogationEventResult_t counter_result = IEC104_Interrogation_Handle_Event(app->counter, event); + if (counter_result == IEC104_INTERROGATION_EVENT_ERROR || + !IEC104_Interrogation_Get_Status(app->counter, &app->ci)) { return false; } + if (result == IEC104_INTERROGATION_EVENT_IGNORED) { result = counter_result; } + } + if (result == IEC104_INTERROGATION_EVENT_ERROR || + !IEC104_Interrogation_Get_Status(app->interrogation, &app->gi)) { return false; } + if (app->clock != NULL && (event->transport.type != IEC104_TCP_EVENT_I_FRAME || + result == IEC104_INTERROGATION_EVENT_IGNORED)) { + IEC104_ClockSynchronizationEventResult_t clock_result = + IEC104_ClockSynchronization_Handle_Event(app->clock, event); + if (clock_result == IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR || + !IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs)) { return false; } + if (clock_result != IEC104_CLOCK_SYNCHRONIZATION_EVENT_IGNORED) { return true; } + } + if (result != IEC104_INTERROGATION_EVENT_IGNORED || + event->transport.type != IEC104_TCP_EVENT_I_FRAME || !app->gi.active || + event->transport.generation != app->gi.generation) { return true; } + /* 当前事件当场提交,必须在NextEvent读取后续通知之前调用;应用不保存兜底请求。 + * HIGH满只丢本条并计数;原连接停用/断开则取消,不跨启停续发。 */ + ++app->other_rejected; + switch (IEC104_Slave_Reject_Request(app->slave, event, IEC60870_ASDU_COT_UNKNOWN_TYPE)) { + case IEC104_TCP_SUBMIT_OK: ++app->other_submitted; break; + case IEC104_TCP_SUBMIT_WAIT: ++app->other_overloads; break; + case IEC104_TCP_SUBMIT_FATAL: + /* 可能已写出字节;cancelled仅表示不再重试,继续读取断线通知。 */ + ++app->other_cancelled; + break; + default: ++app->other_cancelled; break; + } + return true; +} + +/* 两种业务各自推进协议、消费业务事件并刷新状态,供外层轮换调用次序。 */ +static bool Example_Step_General(ExampleApp *app) +{ + return IEC104_Interrogation_Process(app->interrogation) && Example_Drain_Business(app) && + IEC104_Interrogation_Get_Status(app->interrogation, &app->gi); +} + +/* 仅总召测试不创建计数量实例,此时直接完成本轮;已创建时采用相同推进顺序。 */ +static bool Example_Step_Counter(ExampleApp *app) +{ + return app->counter == NULL || (IEC104_Interrogation_Process(app->counter) && Example_Drain_Counter(app) && + IEC104_Interrogation_Get_Status(app->counter, &app->ci)); +} + +static bool Example_Step_Clock(ExampleApp *app) +{ + return app->clock == NULL || (IEC104_ClockSynchronization_Process(app->clock) && + Example_Drain_Clock(app) && IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs)); +} + +/* 两类召唤轮流先执行,再推进对时;发送WAIT后下一轮继续,不只靠need_process重试。 */ +static bool Example_Step(ExampleApp *app) +{ + if (app->events_pending) { return true; } + app->counter_first = !app->counter_first; + if (app->counter_first) { + if (!Example_Step_Counter(app) || !Example_Step_General(app)) { return false; } + } else { + if (!Example_Step_General(app) || !Example_Step_Counter(app)) { return false; } + } + return Example_Step_Clock(app); +} + +/* 有界消费通知。预算用完先续读,不能跨过排在后面的STOP/START直接续发旧总召。 */ +static bool Example_Drain(ExampleApp *app) +{ + unsigned count; + IEC104_SlaveEvent_t event; + for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) { + IEC104_TcpEventResult_t result = IEC104_Slave_NextEvent(app->slave, &event); + if (result == IEC104_TCP_EVENT_EMPTY) { app->events_pending = false; return true; } + if (result != IEC104_TCP_EVENT_READY || !Example_Handle_Event(app, &event)) { return false; } + } + app->events_pending = true; + return true; +} + +#ifndef IEC104_SLAVE_EXAMPLE_NO_MAIN +#ifndef _WIN32 +#error "This executable uses the Windows network backend" +#endif +#ifndef _WIN32_WINNT +/* GetTickCount64所需的最低Windows API声明;这里只用于示例运行时长,不处理业务时标。 */ +#define _WIN32_WINNT 0x0600 +#endif +#ifndef WIN32_LEAN_AND_MEAN +/* 只引入本入口需要的Windows基础声明,socket操作仍全部留在协议库的Net后端。 */ +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include + +/* 信号处理器和主循环之间只交换停止标志;静态初始化为0,不在信号处理器中访问任务或网络。 */ +static volatile sig_atomic_t example_stop; + +/** + * 功能:在已注册的停止信号到来时请求主循环退出。 + * 输入参数:signal_number——运行库传入的信号编号,这里不区分SIGINT与SIGTERM。 + * 输出参数:example_stop置1。 + * 返回值:无。 + * 说明:信号上下文只写sig_atomic_t,不打印、不分配、不调用Slave;资源由main正常收尾。 + */ +static void Example_Stop(int signal_number) { (void)signal_number; example_stop = 1; } + +/** + * 功能:把纯十进制命令行文本解析为不超过max的unsigned long。 + * 输入参数:text——非NULL且以NUL结束的字符串;max——此选项允许的最大值。 + * 输出参数:value——非NULL输出存储,仅成功时写入;失败保持原值。 + * 返回值:true为整个字符串解析成功且未越界;false为空串、符号/空白前缀、尾部字符或溢出。 + * 说明:先检查再截窄为端口、CA或窗口类型,避免越界数值被静默截断;允许前导零及数值0。 + * 某些选项禁止0,由main另查;端口0表示自动分配,run-ms=0表示持续运行。 + */ +static bool Example_Number(const char *text, unsigned long max, unsigned long *value) +{ + char *end; + unsigned long parsed; + if (text[0] < '0' || text[0] > '9') { return false; } + /* strtoul通过errno报告数值溢出;先清零,避免沿用上一次库调用的错误。 */ + errno = 0; + parsed = strtoul(text, &end, 10); + if (errno != 0 || *end != '\0' || parsed > max) { return false; } + *value = parsed; + return true; +} + +/** + * 功能:向标准输出打印本示例支持的选项、默认监听配置及模拟点地址范围。 + * 输入参数:program——非NULL程序名称,main传入argv[0]。 + * 输出参数:无,文本写入stdout。 + * 返回值:无。 + * 说明:本函数不创建服务或退出进程;main根据--help或缺少参数分别选择成功/失败退出。 + */ +static void Example_Usage(const char *program) +{ + printf("Usage: %s [--bind IPv4] [--port 0..65535] [--ca 1..65534]\n" + " [--window 1..32767] [--run-ms milliseconds] [--clock simulated|system]\n" + "Defaults: 127.0.0.1:2404, CA=1, k=12, run until Ctrl+C.\n" + "GI: QOI=20 (all), 21 (signals), 22 (scaled); IOA=0.\n" + "Signals 1001..1008, scaled values 2001..2008; COT=8 cancels matching GI.\n" + "Counters 3001..3008: RQT=5 (all), 1/2 (groups); Type 15, four per batch.\n" + "FRZ=0 reads frozen values; 1 freezes; 2 freezes/resets; 3 resets.\n" + "Freeze/reset completes with ACT_CON only; TEST actions and COT=8 are rejected.\n" + "Clock: simulated by default; system sets the actual clock and requires permission.\n" + "Clock requests use UTC+8 standard time; TEST is rejected without setting time.\n", program); +} + +/** + * 功能:配置并运行子站示例,退出时关闭服务、读空通知并归还静态槽。 + * 输入参数:argc/argv——命令行;除--help外,每个选项后都须有一个值。 + * 输出参数:stdout输出帮助、监听端点和累计统计;stderr输出参数或运行失败信息。 + * 返回值:EXIT_SUCCESS为帮助输出或正常停止;参数、初始化、运行或清理失败为EXIT_FAILURE。 + * 说明:服务生命周期为Create -> Init -> 循环Poll/Drain/Step -> Close -> 读空事件 -> Destroy。 + * main拥有Slave句柄,app只借用;静态app和库内任务在整个运行期间保持地址不变。 + * run-ms使用单调毫秒时钟,在循环边界检查,表示约定运行时长而非精确的退出截止时刻。 + */ +int main(int argc, char **argv) +{ + static ExampleApp app; /* 注册站点及业务借用的存储在退出前保持地址不变。 */ + IEC104_SlaveConfig_t config; /* 本地配置,Slave_Init会复制,不在运行中借用它。 */ + IEC104_TcpError_t error; /* 最近一次服务接口调用的诊断,与历史网络错误分开。 */ + IEC104_TcpServerStatus_t status; /* 启动后查询实际端口,尤其用于--port=0。 */ + IEC104_Slave_t *slave; /* main持有的唯一服务句柄,清理结束后不再使用。 */ + IEC104_TcpResult_t result; /* 服务结果须显式比较IEC104_TCP_OK,不能作布尔判断。 */ + uint16_t common_address = 1U; /* 应用地址校验条件,不是TCP端口或单点IOA。 */ + unsigned long run_ms = 0U; /* 0为持续运行,非零为命令行指定的运行毫秒数。 */ + bool clock_system = false; /* 默认模拟;解析后再写入Example_Init清零后的app。 */ + uint64_t started; /* 开始主循环前的单调时刻,与CP56日历时间无关。 */ + int arg, exit_code = EXIT_SUCCESS; /* arg遍历选项和值;任一阶段失败后保留失败退出码。 */ + /* 第1步:采用库的协议默认值,只把示例默认监听地址收窄到本机回环地址。 */ + IEC104_Slave_Config_Default(&config); + memcpy(config.bind_address, "127.0.0.1", sizeof("127.0.0.1")); + for (arg = 1; arg < argc; ++arg) { + unsigned long value; /* 先在宽整数中验证范围,再赋给配置中的较窄字段。 */ + const char *option = argv[arg]; + if (strcmp(option, "--help") == 0) { Example_Usage(argv[0]); return EXIT_SUCCESS; } + /* 非帮助选项必须有值;此处前进一次取值,for尾部再前进一次到下一个选项。 */ + if (++arg == argc) { Example_Usage(argv[0]); return EXIT_FAILURE; } + if (strcmp(option, "--bind") == 0) { + /* 这里只保证固定数组能容纳含NUL的文本,IPv4语法由Slave_Init中的Net后端检查。 */ + if (strlen(argv[arg]) >= sizeof(config.bind_address)) { return EXIT_FAILURE; } + strcpy(config.bind_address, argv[arg]); + } else if (strcmp(option, "--port") == 0 && Example_Number(argv[arg], UINT16_MAX, &value)) { + config.port = (uint16_t)value; + } else if (strcmp(option, "--ca") == 0 && Example_Number(argv[arg], 65534U, &value) && value != 0U) { + common_address = (uint16_t)value; + } else if (strcmp(option, "--window") == 0 && + Example_Number(argv[arg], IEC104_SEQUENCE_MASK, &value) && value != 0U) { + config.protocol.link.k = (uint16_t)value; + } else if (strcmp(option, "--run-ms") == 0 && Example_Number(argv[arg], UINT32_MAX, &value)) { + run_ms = value; + } else if (strcmp(option, "--clock") == 0 && strcmp(argv[arg], "simulated") == 0) { + clock_system = false; + } else if (strcmp(option, "--clock") == 0 && strcmp(argv[arg], "system") == 0) { + clock_system = true; + } else { + fprintf(stderr, "Invalid option/value: %s %s\n", option, argv[arg]); + return EXIT_FAILURE; + } + } + /* 第2步:取得静态池句柄后绑定应用上下文,再初始化实际网络监听。 */ + slave = IEC104_Slave_Create(); + if (slave == NULL) { fputs("No free Slave service slot\n", stderr); return EXIT_FAILURE; } + if (!Example_Init(&app, slave, common_address, NULL, 0U)) { + (void)IEC104_Slave_Destroy(slave, NULL); + return EXIT_FAILURE; + } + if (!Example_Init_Counters(&app, NULL, 0U)) { + (void)IEC104_Interrogation_Destroy(app.interrogation); + (void)IEC104_Slave_Remove_Station(slave, &app.station); + (void)IEC104_Slave_Destroy(slave, NULL); + return EXIT_FAILURE; + } + if (!Example_Init_Clock(&app, NULL)) { + (void)IEC104_Interrogation_Destroy(app.interrogation); + (void)IEC104_Interrogation_Destroy(app.counter); + (void)IEC104_Slave_Remove_Station(slave, &app.station); + (void)IEC104_Slave_Destroy(slave, NULL); + return EXIT_FAILURE; + } + app.clock_system = clock_system; + result = IEC104_Slave_Init(slave, &config, &error); + if (result != IEC104_TCP_OK) { + fprintf(stderr, "Init failed: result=%d stage=%d socket=%d\n", result, error.stage, error.socket_error); + /* Init失败仍须释放已创建的业务模块和Slave槽。 */ + (void)IEC104_Interrogation_Destroy(app.interrogation); + (void)IEC104_Interrogation_Destroy(app.counter); + (void)IEC104_ClockSynchronization_Destroy(app.clock); + (void)IEC104_Slave_Remove_Station(slave, &app.station); + (void)IEC104_Slave_Destroy(slave, NULL); + return EXIT_FAILURE; + } + /* 监听成功后取得系统实际绑定的端口,不能把配置中的0当作可连接端口输出。 */ + if (IEC104_Slave_Get_Status(slave, &status, &error) != IEC104_TCP_OK) { + exit_code = EXIT_FAILURE; + goto cleanup; + } + /* 第3步:安装仅置标志的信号处理器;真正的资源关闭仍在主线程执行。 */ + (void)signal(SIGINT, Example_Stop); + (void)signal(SIGTERM, Example_Stop); + started = GetTickCount64(); + printf("LISTEN %s:%u CA=%u\n", config.bind_address, (unsigned)status.bound_port, (unsigned)common_address); + fflush(stdout); /* 让终端或通过管道启动本程序的测试立即取得实际监听端点。 */ + while (!example_stop && (run_ms == 0U || GetTickCount64() - started < run_ms)) { + /* 有待续读通知或可立即提交的任务时安排0等待;WAIT则交回网络循环,等待上限50ms。 + * Poll还会按库内本地工作和协议期限缩短等待;50ms只约束等待,不保证整轮执行耗时。 */ + uint32_t wait_ms = Example_Wait_Time(&app, 50U); + result = IEC104_Slave_Poll(slave, wait_ms, &error); + if (result != IEC104_TCP_OK) { + /* 客户端普通断线由事件表达;这里收到的是服务接口失败,结束循环统一清理。 */ + fprintf(stderr, "Poll failed: result=%d stage=%d socket=%d\n", result, error.stage, error.socket_error); + exit_code = EXIT_FAILURE; + break; + } + /* 先消费状态变化再推进业务;Drain失败会短路,不继续对不可信状态提交应答。 */ + if (!Example_Drain(&app) || !Example_Step(&app)) { + fputs("Application event/build failure\n", stderr); + exit_code = EXIT_FAILURE; + break; + } + } +cleanup: + /* 第4步:停止应用任务,再结束网络;取消只丢弃尚未提交完毕的任务,不重发已接手数据。 */ + Example_Cancel(&app); + /* Cancel只将终结通知排队;释放总召实例前必须读完,并清理应用自己的遍历状态及资源。 */ + if (!Example_Drain_Business(&app) || !Example_Drain_Counter(&app)) { exit_code = EXIT_FAILURE; } + if (IEC104_Slave_Close(slave, &error) != IEC104_TCP_OK) { exit_code = EXIT_FAILURE; } + { + IEC104_SlaveEvent_t event; + IEC104_TcpEventResult_t next; + /* Close保留收尾通知。对时只消费非I帧状态,不再受理请求或执行动作。 */ + while ((next = IEC104_Slave_NextEvent(slave, &event)) == IEC104_TCP_EVENT_READY) { + if (app.clock != NULL && event.transport.type != IEC104_TCP_EVENT_I_FRAME && + IEC104_ClockSynchronization_Handle_Event(app.clock, &event) == + IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR) { exit_code = EXIT_FAILURE; } + } + if (next != IEC104_TCP_EVENT_EMPTY) { exit_code = EXIT_FAILURE; } + } + if (!IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs)) { exit_code = EXIT_FAILURE; } + /* 只有关闭且FIFO读空后才能归还槽;清理失败也必须体现在程序退出码中。 */ + if (!IEC104_Interrogation_Destroy(app.interrogation)) { exit_code = EXIT_FAILURE; } + if (!IEC104_Interrogation_Destroy(app.counter)) { exit_code = EXIT_FAILURE; } + if (!IEC104_ClockSynchronization_Destroy(app.clock)) { exit_code = EXIT_FAILURE; } + if (IEC104_Slave_Remove_Station(slave, &app.station) != IEC104_STATION_OK) { exit_code = EXIT_FAILURE; } + if (IEC104_Slave_Destroy(slave, &error) != IEC104_TCP_OK) { exit_code = EXIT_FAILURE; } + /* 打印应用提交统计;这些值不用于推断未确认帧是否已到达主站,也不作为自动重试依据。 */ + printf("STOP submitted=%lu gi_submitted=%lu waits=%lu rejected=%lu cancelled=%lu overloads=%lu deactivated=%lu timeouts=%lu negative_submitted=%lu dropped_requests=%lu counter_submitted=%lu counter_completed=%lu clock_executed=%lu clock_synchronized=%lu clock_submitted=%lu\n", + app.gi.submitted + app.other_submitted, app.gi.gi_submitted, app.gi.waits, + app.gi.rejected + app.other_rejected, app.gi.cancelled + app.other_cancelled, + app.gi.overloads + app.other_overloads, app.gi.deactivated, app.gi.timeouts, + app.gi.negative_submitted, app.gi.dropped_requests + app.other_overloads, + app.ci.submitted, app.counter_completed, app.cs.executed, app.cs.synchronized, app.cs.submitted); + return exit_code; +} +#endif diff --git a/IEC60870-Slave/.build/command-v1/before/manifest.json b/IEC60870-Slave/.build/command-v1/before/manifest.json new file mode 100644 index 00000000..441d689b --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/manifest.json @@ -0,0 +1,422 @@ +[ + { + "Path": "test\\slave_example_loopback_test.cjs", + "Hash": "505081AEB36054F9BAC8D7A117440165265629ED30D3704E8D8D33B180228378", + "Bom": false + }, + { + "Path": "test\\iec60870_asdu_test.c", + "Hash": "299F56BD960EE1195A1F0CAFB158D200119A74252953785A25652595999A6342", + "Bom": false + }, + { + "Path": "test\\iec60870_asdu_extended_test.c", + "Hash": "88C72DD24AB573A61CF7DC2F6721A05493D90B94D285F295C2ED4F03E169097A", + "Bom": false + }, + { + "Path": "test\\iec104_tcp_scheduler_test.c", + "Hash": "9CC126E4B4B43829BB9AB6DB2D4D2B37AC34443DD3040E7AE34F5D81536903C5", + "Bom": false + }, + { + "Path": "test\\iec104_tcp_event_ring_test.c", + "Hash": "556EA4C188BCCDEDD58B26529ADAB163583C786340C6D0B7D347FC368DA6E18A", + "Bom": false + }, + { + "Path": "test\\iec104_slave_test.c", + "Hash": "E0362B0BDEE81F2E95D9552198944E700A22A6C8BFF813E280B50D49536800CC", + "Bom": false + }, + { + "Path": "test\\iec104_slave_example_test.c", + "Hash": "91BC20F3EB3C8C247AB1FB02830A826D7E6C387F7F9B08508A2DD6DF379EFBCE", + "Bom": false + }, + { + "Path": "test\\iec104_net_pending_input_test.c", + "Hash": "3AA0E827B38851ECB409C83A24D2D895E6B877E7B1A07B842781FBBF1AD09F78", + "Bom": false + }, + { + "Path": "test\\iec104_interrogation_rules_test.c", + "Hash": "C87C86AD2E49C7607A0CFE92AA9E933AF9B1A8924C08B7025CEC3BCD584D0C03", + "Bom": false + }, + { + "Path": "test\\iec104_asdu_transport_test.c", + "Hash": "A6F7A5899366730EE706D10675183FE9714268BC81BF4D213B07507D5B58E818", + "Bom": false + }, + { + "Path": "scripts\\build.ps1", + "Hash": "37609C36DC44D3DFC62C2B6B5B9493C3A8BE6A5B40D2CFFD293ABAE52DEA0F7F", + "Bom": false + }, + { + "Path": "README.md", + "Hash": "5E790B4812E67A3C0DBB5F5609FE7C625FBA37D9E9B165A1227C20BB1BE6D75B", + "Bom": false + }, + { + "Path": "examples\\iec104_slave_example.c", + "Hash": "F8814F780324D1E303ABD926FB06BE3E01251AB046A527CC223F283EADEC3BD5", + "Bom": false + }, + { + "Path": "docs\\test\\IEC104测试文档.md", + "Hash": "EE4AE83C262E34611A0E11BD119D52E8128A42F0274D1DDA9962E7C599EE0565", + "Bom": false + }, + { + "Path": "docs\\README.md", + "Hash": "242CE70851D670E00A3C5B93F2AB2017E0E240D90197A90A05D9BBA15FEF1833", + "Bom": false + }, + { + "Path": "docs\\examples\\IEC104子站示例.md", + "Hash": "C1374C4E46957DD04FFE227B96553ABB0CB0A6FBD9D9E6A80BC15C01BE7DF029", + "Bom": false + }, + { + "Path": "104\\IEC104.c", + "Hash": "5FE4EBBAC2994E584B860F032C52BCDC3337DD36509CD7427064C14A35F1510C", + "Bom": false + }, + { + "Path": "common\\IEC60870_Types.h", + "Hash": "F90C12343CCB375AF49CAE322D1E230E00377559C6F4E3C84CE3F554548791A8", + "Bom": false + }, + { + "Path": "common\\IEC60870_Time.h", + "Hash": "3C76A061C1125CE5CF827EBC73B5AA1A14CF4A5470751D7945EF0DE695BE55DC", + "Bom": false + }, + { + "Path": "common\\IEC60870_Time.c", + "Hash": "8A50DC1D1FBAD5BA3852BDDDBAF19F06EE273CEBC22F429D79C013F7938C194B", + "Bom": false + }, + { + "Path": "common\\IEC60870_Slave.h", + "Hash": "CCCE0A5A60F63F18F7102BEB544F3C31BACF84487DE7895513B45015E92B2914", + "Bom": false + }, + { + "Path": "common\\IEC60870_Slave.c", + "Hash": "74742AF727CE571C137AA3EFF49C427E4E9788F4CA55A5A75689329874C33029", + "Bom": false + }, + { + "Path": "common\\IEC60870_InformationObjects_Internal.h", + "Hash": "DB220E3EB9421E7C2F8AB53797EA3F65630F374A91B367F0BAE86FF3B64FC7CE", + "Bom": false + }, + { + "Path": "common\\IEC60870_InformationObjects.h", + "Hash": "3C56528D3E79C5AC7501824474F8E61BCF75C3B5B24EC550F6211868744EA55A", + "Bom": false + }, + { + "Path": "common\\IEC60870_InformationObjects.c", + "Hash": "A88FF31632E783C3074764BE037E5F5DCB71FBCED1FC34E3BB841D97C0A4A2DA", + "Bom": false + }, + { + "Path": "common\\IEC60870_Codec.h", + "Hash": "67134E910A6447746A8F4DECB263A9CC8816D728B7C5EEBEA3CC4CA568821B0E", + "Bom": false + }, + { + "Path": "common\\IEC60870_Codec.c", + "Hash": "90CF53C8C70284C4532B3F4C864D470ED1D9F90EC178C6B74D125C4EBB919B0D", + "Bom": false + }, + { + "Path": "common\\IEC60870_Clock.h", + "Hash": "95706E548B605708C7C398203F65367AF304C394B4A9FC3CEC55066763397B88", + "Bom": false + }, + { + "Path": "common\\IEC60870_Clock.c", + "Hash": "12E98CE423280B0245CC88FB050C75ADE5EF1683B3565C7750D0EFCD2613F694", + "Bom": false + }, + { + "Path": "common\\IEC60870_Asdu.h", + "Hash": "F5242AA5F98EAA96DBD06CB6861A9710AE5A66256D07F44FDB80A058E269EAE2", + "Bom": false + }, + { + "Path": "common\\IEC60870_Asdu.c", + "Hash": "63E2F753D4217D730D57AD0279E353EF9D92E7151B30D4858A2FCDF1F65904BA", + "Bom": false + }, + { + "Path": "104\\IEC104_AsduQueue.h", + "Hash": "C440BCCBF184FA9E92F26C4C4505CB7B8C95C1B83E680B2B6F92FA263D475389", + "Bom": false + }, + { + "Path": "104\\IEC104_AsduQueue.c", + "Hash": "BB1FEEA478E890E6035D722006139CAA79C9231AA9D58C492FAC1B0ABEFF8F85", + "Bom": false + }, + { + "Path": "104\\IEC104.h", + "Hash": "C2895EB330AC965F15BEF3497040C6057750BDC91884E14E9F4B1FBFE2676C40", + "Bom": false + }, + { + "Path": "104\\IEC104_Codec.h", + "Hash": "F8FD4FF0729BFFC8FF61BD5A141F0835A65C3748C0EDB989DA44317CCC92AF91", + "Bom": false + }, + { + "Path": "104\\IEC104_Codec.c", + "Hash": "3879984667805D40EF16B7506C4273389CB7916C2851837EFDB454953A826F29", + "Bom": false + }, + { + "Path": "104\\IEC104_Connection.c", + "Hash": "F2F918E0E209C99E80561EEC9EB0C4D973ACA51C2530D9FAF333106248E58759", + "Bom": false + }, + { + "Path": "docs\\common\\IEC60870_Time_设计架构.md", + "Hash": "A88C7E931C1FA67064BAF690899FE1B55FF5EADCD759888091A7A83B600A0A55", + "Bom": false + }, + { + "Path": "104\\IEC104_Link.h", + "Hash": "AA0B38C75C8D7A45AB025BDA362E9232570AB4D24B20D6E208BCF86957ACF14D", + "Bom": false + }, + { + "Path": "104\\IEC104_Link.c", + "Hash": "B620B80C5C28D2E1703BF406BEFA19B573BC1FB641E4D653F8E935093ED5F869", + "Bom": false + }, + { + "Path": "104\\IEC104_Connection.h", + "Hash": "7FF8A136C1738F0B20EF292B783624ACA82D6A5EF66622398D9A7B222B83946D", + "Bom": false + }, + { + "Path": "104\\IEC104_Link_Internal.h", + "Hash": "9A18B2A2FE20E17298F74E35C48C21E2AD0FF92AB9B2DB3C7844D78817E1FFC9", + "Bom": false + }, + { + "Path": "104\\IEC104_Link_Common.h", + "Hash": "5CF60F9EBD0A16D6C3C657F77BB007F76FED61BE00DD12CB4B75068F27F0F539", + "Bom": false + }, + { + "Path": "104\\IEC104_Link_Master_StartStop.c", + "Hash": "1E91DF3E5B8C8038AAA045A2EF7D5FE198D873A1F5A48AADE32AF0F4439AA2B5", + "Bom": false + }, + { + "Path": "104\\IEC104_Link_Master_StartStop.h", + "Hash": "FCFFDD041A71221CEC654F6F7E03AD697B78361F1123B9D3C506D1B743BF4317", + "Bom": false + }, + { + "Path": "104\\IEC104_Types.h", + "Hash": "25B9347A862D68E5F28340A1787FF551404E498E363B9E23411DDBD32CF03F3E", + "Bom": false + }, + { + "Path": "104\\IEC104_TcpTypes.h", + "Hash": "9F789D7551128538344D2BAE3D56D11F6EA145B91C991FF73704CF3B5A46EA70", + "Bom": false + }, + { + "Path": "104\\IEC104_TcpServer_Config.h", + "Hash": "6DFB1A1698690774AF6C8782BCB366B3951D6C3277E05E2158AF5F1BEBF845BD", + "Bom": false + }, + { + "Path": "104\\IEC104_TcpServer.h", + "Hash": "FD383C289C0405241A7D1F03AFC74594575EC92E2CE511FA54E8A4C756FE1908", + "Bom": false + }, + { + "Path": "104\\IEC104_TcpServer.c", + "Hash": "EBA77A1B11F17F0F8F1A7602BA55FCF4585BDD409589336D25809F04C9470F78", + "Bom": false + }, + { + "Path": "104\\IEC104_Slave.h", + "Hash": "5D023BEB47C062A609F3AD9A6591AE3AE2CD7E259AAC57FB031A0AED1F20CAA2", + "Bom": false + }, + { + "Path": "104\\IEC104_Slave.c", + "Hash": "F00615FBB33AEA7829C55EC6F44B784D900287EBA7C13F3BA8D9EC695A8DFA59", + "Bom": false + }, + { + "Path": "104\\IEC104_SendManager.h", + "Hash": "46B106001BA6E8C428430D37F7E8CA3795643F9DC7A129C31478137031CA9722", + "Bom": false + }, + { + "Path": "104\\IEC104_SendManager.c", + "Hash": "487A0DD881967E7EEF5ADC4FFF0657EBCC964976F70A0D586E510C2552BB1B38", + "Bom": false + }, + { + "Path": "104\\IEC104_SendBuffer.h", + "Hash": "7C3A7ACB355DC8719BB2FF7BBBB5B93F6D81A6482746C75D5A2B90AE111AF2C5", + "Bom": false + }, + { + "Path": "104\\IEC104_SendBuffer.c", + "Hash": "2B1E46491CF0B60ECBB102E6EAACF98B8650E6E27BBA2EF9BA09A811B18A8773", + "Bom": false + }, + { + "Path": "104\\IEC104_Receiver.h", + "Hash": "84FB96B90D82FA2333931B695F7D7FA4C5E83D41BC694C38A71E1469A6055730", + "Bom": false + }, + { + "Path": "104\\IEC104_Receiver.c", + "Hash": "BBDC3D8A69E089C5152241DD34CCBA063A4C220200B2EC9B7D0A9CDEEF30736D", + "Bom": false + }, + { + "Path": "104\\IEC104_Net_Windows.c", + "Hash": "B3A79BE97A8D5B3BF0326F278BF3CD3DAECCB1D623E333F4B9E685D691B8777F", + "Bom": false + }, + { + "Path": "104\\IEC104_Net.h", + "Hash": "A8ECC46CB71885893CC4C02FDFB0FE9F275C04101CAD243D6189C3CFAB13AA06", + "Bom": false + }, + { + "Path": "104\\IEC104_Log.h", + "Hash": "D79C323401D62729F03062EE1675DA6AC36465F17B91930639A8ED0F9214E353", + "Bom": false + }, + { + "Path": "104\\IEC104_Log.c", + "Hash": "9F211FECD7E8A79D3E7EDEE378314248EB924D6497C8C40B6FA765D2132FC126", + "Bom": false + }, + { + "Path": "104\\IEC104_Link_Slave_StartStop.h", + "Hash": "0DE666BF7B917160B2A77D887D823A00B5168EC8570F727B614F3CB7D4A6B931", + "Bom": false + }, + { + "Path": "104\\IEC104_Link_Slave_StartStop.c", + "Hash": "E4382C0CE7145FB1814408F83BEB9C3547A6435EDFA4AC38CF502B889D3C7793", + "Bom": false + }, + { + "Path": "docs\\104\\IEC104_Interrogation_设计架构.md", + "Hash": "5DB40694A2791B3C2E0587B373FAF3E9F125EEB32D8E63D1037AC56A02217A6E", + "Bom": false + }, + { + "Path": "docs\\104\\IEC104_CounterInterrogation_设计架构.md", + "Hash": "CD81DB5B90464B2D26F547C9BB0AD29AB716013D1754DB372FC9DABD201C0104", + "Bom": false + }, + { + "Path": "docs\\104\\README.md", + "Hash": "A58D52550C7819FCDAA6D103808D2F70B46E844937AAEE00ADA0394EA9D18B42", + "Bom": false + }, + { + "Path": "docs\\104\\IEC104_Connection_设计架构.md", + "Hash": "A42E65C1666086C7853F0B91851847D3FAB626BC049BB76394CB982A4C2F05AE", + "Bom": false + }, + { + "Path": "docs\\104\\IEC104_ClockSynchronization_设计架构.md", + "Hash": "DEE00767FEC6C300E72956F5B1BE54E3FC2C5576E8B92DE56C36BCDC900169AA", + "Bom": false + }, + { + "Path": "docs\\104\\IEC104_设计架构.md", + "Hash": "D391AE61D9377111111CAC05D36022E87157BD67B529B61CE6E4A819C82488F5", + "Bom": false + }, + { + "Path": "docs\\104\\IEC104_API接口汇总.md", + "Hash": "52AF0F740F7045BEC9EF5DE9ECCA3D84725D465A04673AAB962415385B4205A4", + "Bom": false + }, + { + "Path": "docs\\104\\IEC104_完整调用流程.md", + "Hash": "72DB2C97B6551ACE36B43CD8A11ABE203E332DC5198DF3328EA15003B7306137", + "Bom": false + }, + { + "Path": "docs\\104\\IEC104_TcpServer_设计架构.md", + "Hash": "893EADDDDEAEDB303EDE4A9EDD4B154E2A1EDDDCF4D9B090E6B0A61E4883D620", + "Bom": false + }, + { + "Path": "docs\\104\\IEC104_发送模块_设计架构.md", + "Hash": "50FDC3ACD57D42F10FE9075B0EC2C1A8DA2F852E37535CFA002D112A24D6A568", + "Bom": false + }, + { + "Path": "docs\\104\\IEC104_Link_设计架构.md", + "Hash": "62C88BFA45F09A6E72ED07C50F9B55A9D5F0B9D4CA8BEDC901CB7CABED976A6F", + "Bom": false + }, + { + "Path": "104\\business\\IEC104_ClockSynchronization.h", + "Hash": "5E95A9EBD5F447012D5FA0ABC0A821EFC1E63A2A2BB6D02C768F821F3BAAE928", + "Bom": false + }, + { + "Path": "104\\business\\IEC104_ClockSynchronization.c", + "Hash": "E7190574F6B3DECD1DCC5ECB17C87B1909B5092482C2B8D39AF07205E0D5D79F", + "Bom": false + }, + { + "Path": "104\\business\\IEC104_CounterInterrogation.c", + "Hash": "F92A064E95A7ADD319BE9197B485D0EBDD549B97C0DE24AE46E380A48344BE40", + "Bom": false + }, + { + "Path": "104\\business\\IEC104_CounterInterrogation.h", + "Hash": "F633522212BFCD00BB72D40BC1F81890A114338CDAB5C1352341CF685085EBA5", + "Bom": false + }, + { + "Path": "104\\business\\IEC104_Interrogation_Internal.h", + "Hash": "95AE4B2C54CA213375647BB28EB54339198ED2CB04D4DA55C9E9CB6FA7FA6BD5", + "Bom": false + }, + { + "Path": "104\\business\\IEC104_Interrogation.h", + "Hash": "466897AA79A5607D65C88C2CC7493EEF72995209C61186365F0361584D7FF3FB", + "Bom": false + }, + { + "Path": "104\\business\\IEC104_Interrogation.c", + "Hash": "D4A4CA0530780A45F83E4C4989289F36D2A63F61F3625B1B550FE74EA9CADDCA", + "Bom": false + }, + { + "Path": "104\\business\\IEC104_GeneralInterrogation.h", + "Hash": "785D298FA21493592B61D72AEDE75F5E48F8909472EAE9A897DC5D1BB8A1DF79", + "Bom": false + }, + { + "Path": "104\\business\\IEC104_GeneralInterrogation.c", + "Hash": "0C2F31EE24F2757136EDD87FD631EBD83CEF0AE3CFA333E8D9A651622767C023", + "Bom": false + } +] diff --git a/IEC60870-Slave/.build/command-v1/before/scripts/build.ps1 b/IEC60870-Slave/.build/command-v1/before/scripts/build.ps1 new file mode 100644 index 00000000..3a1babdc --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/scripts/build.ps1 @@ -0,0 +1,70 @@ +param( + [switch]$Test, + [string]$Compiler = 'gcc' +) +$ErrorActionPreference = 'Stop' +$projectRoot = Split-Path -Parent $PSScriptRoot +$protocolInclude = Join-Path $projectRoot '104' +$businessInclude = Join-Path $protocolInclude 'business' +$commonInclude = Join-Path $projectRoot 'common' +$exampleBuild = Join-Path $projectRoot '.build/slave-example' +New-Item -ItemType Directory -Path $exampleBuild -Force | Out-Null +Get-Command $Compiler -ErrorAction Stop | Out-Null + +# Shared dependencies; the example adds the actual TCP and Windows Net implementations. +$libraryNames = @( + '104/business/IEC104_Interrogation.c', '104/business/IEC104_GeneralInterrogation.c', + '104/business/IEC104_CounterInterrogation.c', '104/business/IEC104_ClockSynchronization.c', + '104/IEC104_Slave.c', + 'common/IEC60870_Slave.c', 'common/IEC60870_Asdu.c', + 'common/IEC60870_InformationObjects.c', 'common/IEC60870_Time.c', 'common/IEC60870_Clock.c', + 'common/IEC60870_Codec.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', + '104/IEC104_Link.c', '104/IEC104_Link_Slave_StartStop.c' +) +$librarySources = @($libraryNames | ForEach-Object { Join-Path $projectRoot $_ }) +$compileFlags = @('-std=c11', '-O2', '-Wall', '-Wextra', '-Werror', "-I$protocolInclude", "-I$businessInclude", "-I$commonInclude") +$exampleOutput = Join-Path $exampleBuild 'iec104_slave_example.exe' +$exampleSources = @( + (Join-Path $projectRoot 'examples/iec104_slave_example.c'), + (Join-Path $projectRoot '104/IEC104_TcpServer.c'), + (Join-Path $projectRoot '104/IEC104_Net_Windows.c') +) + $librarySources +& $Compiler @compileFlags @exampleSources -o $exampleOutput -lws2_32 +if ($LASTEXITCODE -ne 0) { throw 'Slave example build failed' } +Write-Output "Built: $exampleOutput" + +if ($Test) { + # Shared Station/Slave APIs, including isolation between two independent services. + foreach ($capacity in @(1, 2)) { + $slaveTestOutput = Join-Path $exampleBuild "iec104_slave_test_$capacity.exe" + $slaveTestSources = @((Join-Path $projectRoot 'test/iec104_slave_test.c')) + $librarySources + & $Compiler @compileFlags "-DIEC104_TCP_SERVER_CAPACITY=$capacity" @slaveTestSources -o $slaveTestOutput + if ($LASTEXITCODE -ne 0) { throw 'Slave/Station test build failed' } + & $slaveTestOutput + if ($LASTEXITCODE -ne 0) { throw 'Slave/Station tests failed' } + } + # The controlled-backend test includes TcpServer.c itself. Do not link a second TCP/Net implementation. + $testSources = @((Join-Path $projectRoot 'test/iec104_slave_example_test.c')) + $librarySources + foreach ($capacity in @(1, 2)) { + $testOutput = Join-Path $exampleBuild "iec104_slave_example_test_$capacity.exe" + & $Compiler @compileFlags "-DIEC104_TCP_SERVER_CAPACITY=$capacity" -DIEC104_TCP_HIGH_CAPACITY=251 -DIEC104_INTERROGATION_EVENT_CAPACITY=2 @testSources -o $testOutput + if ($LASTEXITCODE -ne 0) { throw 'Slave example test build failed' } + & $testOutput + if ($LASTEXITCODE -ne 0) { throw 'Slave example tests failed' } + } + $rulesOutput = Join-Path $exampleBuild 'iec104_interrogation_rules_test.exe' + $rulesSources = @( + (Join-Path $projectRoot 'test/iec104_interrogation_rules_test.c'), + (Join-Path $projectRoot '104/IEC104_TcpServer.c'), + (Join-Path $projectRoot '104/IEC104_Net_Windows.c') + ) + $librarySources + & $Compiler @compileFlags @rulesSources -o $rulesOutput -lws2_32 + if ($LASTEXITCODE -ne 0) { throw 'Interrogation rules test build failed' } + & $rulesOutput + if ($LASTEXITCODE -ne 0) { throw 'Interrogation rules tests failed' } + Get-Command node -ErrorAction Stop | Out-Null + & node (Join-Path $projectRoot 'test/slave_example_loopback_test.cjs') $exampleOutput + if ($LASTEXITCODE -ne 0) { throw 'Slave example TCP loopback test failed' } +} diff --git a/IEC60870-Slave/.build/command-v1/before/test/iec104_asdu_transport_test.c b/IEC60870-Slave/.build/command-v1/before/test/iec104_asdu_transport_test.c new file mode 100644 index 00000000..8e0ad8ea --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/test/iec104_asdu_transport_test.c @@ -0,0 +1,125 @@ +/* 公共ASDU与104传输的集成回归:独立测试中的字节向量仍用于期望值。 + * 公共套件单独构建时不链接104实现;本文件专门检查I帧接入边界。 */ +#define main common_asdu_suite_main +#include "iec60870_asdu_test.c" +#undef main +#include "IEC104_Codec.h" + +/** + * 功能:验证公共对象构造的ASDU可按标准104布局封装并交付给公共解析器。 + * 输入参数:无。 + * 输出参数:无。 + * 返回值:全部检查成功返回1,断言失败返回0。 + * 说明:复用30种类型的独立载荷向量,覆盖42种类型/SQ组合及真实I帧序号。 + */ +static int test_common_asdu_in_iframe(void) +{ + const IEC60870_AppLayerParameters_t parameters = IEC104_APP_LAYER_PARAMETERS_DEFAULT; + static const uint8_t addresses[2][3] = {{0x56, 0x34, 0x12}, {0x57, 0x34, 0x12}}; + size_t v; + unsigned int layouts = 0; + CHECK(parameters.sizeOfTypeId == 1 && parameters.sizeOfVSQ == 1); + CHECK(parameters.sizeOfCOT == 2 && parameters.sizeOfCA == 2 && parameters.sizeOfIOA == 3); + CHECK(parameters.originatorAddress == 0 && parameters.maxSizeOfASDU == 249); + for (v = 0; v < COUNT(vectors); ++v) { + const Vector *vector = &vectors[v]; + int sq; + for (sq = 0; sq <= (vector->command ? 0 : 1); ++sq) { + uint8_t frame[64], expected[64], apdu[IEC104_MAX_APDU_LEN]; + IEC60870_AsduHeader_t header = Header(); + IEC60870_Asdu_t asdu; + IEC60870_AsduInfo_t info; + IEC104_ApciInfo_t apci; + TestObject input, output; + const uint8_t *data = NULL; + uint16_t length = 0, n; + int i, count = vector->command ? 1 : 2; + header.cause_of_transmission = vector->command ? 6 : 3; + header.is_negative_confirm = vector->command; + n = Wire_Header(¶meters, vector->type, (uint8_t)(count | (sq << 7)), + vector->command ? 0xC6 : 0x83, expected); + CHECK(IEC60870_Asdu_Create(&asdu, ¶meters, frame, sizeof(frame), &header, sq) == IEC60870_ASDU_OK); + for (i = 0; i < count; ++i) { + CHECK(Sample_Object(vector->type, 0x123456U + (uint32_t)i, &input) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, Object_Base(&input, vector->type)) == IEC60870_ASDU_OK); + if (!sq || i == 0) { + memcpy(expected + n, addresses[i], 3); + n += 3; + } + memcpy(expected + n, vector->payload, vector->size); + n += vector->size; + } + CHECK(IEC60870_Asdu_Get_Data(&asdu, &data, &length) == IEC60870_ASDU_OK); + CHECK(length == n && memcmp(data, expected, n) == 0); + CHECK(IEC104_Build_Iframe(apdu, 0x1234, 0x2345, data, length) == length + IEC104_APCI_LEN); + CHECK(apdu[0] == 0x68 && apdu[1] == length + 4); + CHECK(apdu[2] == 0x68 && apdu[3] == 0x24 && apdu[4] == 0x8A && apdu[5] == 0x46); + CHECK(IEC104_Parse_APCI(apdu, (uint16_t)(length + IEC104_APCI_LEN), &apci)); + CHECK(apci.frame_type == IEC104_FRAME_TYPE_I && apci.send_number == 0x1234 && apci.receive_number == 0x2345); + CHECK(apci.asdu_len == length && memcmp(apci.asdu, expected, n) == 0); + CHECK(IEC60870_Asdu_Parse(¶meters, apci.asdu, apci.asdu_len, &info) == IEC60870_ASDU_OK); + for (i = 0; i < count; ++i) { + CHECK(IEC60870_Asdu_Get_Element(&info, (uint8_t)i, &output, Object_Size(vector->type)) == IEC60870_ASDU_OK); + CHECK(Object_Base(&output, vector->type)->ioa == 0x123456U + (uint32_t)i); + CHECK(Check_Object(vector, &output)); + } + ++layouts; + } + } + CHECK(layouts == 42); + return 1; +} + +/** + * 功能:验证公共ASDU容量与104帧容量分别生效。 + * 输入参数:无。 + * 输出参数:无。 + * 返回值:全部检查成功返回1,断言失败返回0。 + * 说明:公共层可构造254字节,104仍只接受至249字节;拒绝时输出保持不变。 + */ +static int test_transport_length_boundary(void) +{ + IEC60870_AppLayerParameters_t parameters = IEC104_APP_LAYER_PARAMETERS_DEFAULT; + IEC60870_AsduHeader_t header = Header(); + IEC60870_Asdu_t asdu; + IEC60870_AsduInfo_t info; + IEC60870_MeasuredValueNormalized_t value; + IEC104_ApciInfo_t apci; + uint8_t frame[IEC60870_MAX_ASDU_LEN], apdu[260], before[260]; + unsigned int i; + CHECK(IEC60870_MeasuredValueNormalized_Create(&value, 0, -1, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Create(&asdu, ¶meters, frame, sizeof(frame), &header, 1) == IEC60870_ASDU_OK); + for (i = 0; i < 80; ++i) { + value.object.ioa = i; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &value.object) == IEC60870_ASDU_OK); + } + CHECK(asdu.length == 249); + CHECK(IEC104_Build_Iframe(apdu, 0, 0, frame, asdu.length) == 255); + CHECK(IEC104_Parse_APCI(apdu, 255, &apci)); + CHECK(apci.asdu_len == 249 && memcmp(apci.asdu, frame, 249) == 0); + + parameters.sizeOfCOT = 1; parameters.sizeOfCA = 1; + parameters.sizeOfIOA = 2; parameters.maxSizeOfASDU = 254; + header.common_address = 0x67; + CHECK(IEC60870_Asdu_Create(&asdu, ¶meters, frame, sizeof(frame), &header, 0) == IEC60870_ASDU_OK); + for (i = 0; i < 50; ++i) { + value.object.ioa = i; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &value.object) == IEC60870_ASDU_OK); + } + CHECK(asdu.length == 254); + CHECK(IEC60870_Asdu_Parse(¶meters, frame, asdu.length, &info) == IEC60870_ASDU_OK); + memset(apdu, 0xA5, sizeof(apdu)); + memcpy(before, apdu, sizeof(before)); + CHECK(IEC104_Build_Iframe(apdu, 0, 0, frame, 250) == -1); + CHECK(memcmp(before, apdu, sizeof(apdu)) == 0); + CHECK(IEC104_Build_Iframe(apdu, 0, 0, frame, 254) == -1); + CHECK(memcmp(before, apdu, sizeof(apdu)) == 0); + return 1; +} + +int main(void) +{ + if (!test_common_asdu_in_iframe() || !test_transport_length_boundary()) { return 1; } + puts("104 ASDU transport: 2 groups passed (42 layouts, 249/254-byte boundary)"); + return 0; +} diff --git a/IEC60870-Slave/.build/command-v1/before/test/iec104_interrogation_rules_test.c b/IEC60870-Slave/.build/command-v1/before/test/iec104_interrogation_rules_test.c new file mode 100644 index 00000000..8272d3a9 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/test/iec104_interrogation_rules_test.c @@ -0,0 +1,87 @@ +/* 召唤种类规则单测:直接调用无状态规则,验证限定词解析、数据COT和允许的数据类型。 + * 使用独立请求字节,不经过网络,也不执行应用的冻结或复位动作。 */ +#include "../104/business/IEC104_Interrogation_Internal.h" +#include +#include +#include + +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #x); exit(1); } } while (0) + +/* QCC同时包含RQT和FRZ;用非零IOA确认解析保留原值,业务合法性由后续校验决定。 */ +static void test_decode(void) +{ + static const uint8_t qcc[] = {0x05, 0x45, 0x85, 0xc5, 0x01, 0x3f}; + IEC60870_AppLayerParameters_t parameters = {1, 1, 2, 0, 2, 3, 249}; + unsigned i; + for (i = 0; i < sizeof(qcc); ++i) { + uint8_t bytes[] = {101, 1, 6, 0x5a, 1, 0, 0x34, 0x12, 0, 0}; + IEC60870_AsduInfo_t info; + uint32_t ioa = 0; + uint8_t qualifier = 0, freeze = 0; + bytes[9] = qcc[i]; + CHECK(IEC60870_Asdu_Parse(¶meters, bytes, sizeof(bytes), &info) == IEC60870_ASDU_OK); + CHECK(IEC104_COUNTER_INTERROGATION_RULES.decode(&info, &ioa, &qualifier, &freeze)); + CHECK(ioa == 0x1234 && qualifier == (qcc[i] & 0x3f) && freeze == (qcc[i] >> 6)); + } + { + uint8_t bytes[] = {100, 1, 6, 0, 1, 0, 0, 0, 0, 36}; + IEC60870_AsduInfo_t info; + uint32_t ioa = 1; + uint8_t qualifier = 0, freeze = 3; + CHECK(IEC60870_Asdu_Parse(¶meters, bytes, sizeof(bytes), &info) == IEC60870_ASDU_OK); + CHECK(IEC104_GENERAL_INTERROGATION_RULES.decode(&info, &ioa, &qualifier, &freeze)); + CHECK(ioa == 0 && qualifier == 36 && freeze == 0); + } +} + +/* 遍历完整字节范围验证限定词边界,并检查总请求及四个分组的数据COT映射。 */ +static void test_qualifiers(void) +{ + unsigned q; + CHECK(IEC104_GENERAL_INTERROGATION_RULES.type_id == 100); + CHECK(IEC104_GENERAL_INTERROGATION_RULES.has_deactivation); + CHECK(IEC104_COUNTER_INTERROGATION_RULES.type_id == 101); + CHECK(!IEC104_COUNTER_INTERROGATION_RULES.has_deactivation); + for (q = 0; q < 256; ++q) { + CHECK(IEC104_GENERAL_INTERROGATION_RULES.qualifier_valid((uint8_t)q) == (q >= 20 && q <= 36)); + CHECK(IEC104_COUNTER_INTERROGATION_RULES.qualifier_valid((uint8_t)q) == (q >= 1 && q <= 5)); + } + for (q = 20; q <= 36; ++q) { CHECK(IEC104_GENERAL_INTERROGATION_RULES.data_cause((uint8_t)q) == q); } + CHECK(IEC104_COUNTER_INTERROGATION_RULES.data_cause(1) == 38); + CHECK(IEC104_COUNTER_INTERROGATION_RULES.data_cause(2) == 39); + CHECK(IEC104_COUNTER_INTERROGATION_RULES.data_cause(3) == 40); + CHECK(IEC104_COUNTER_INTERROGATION_RULES.data_cause(4) == 41); + CHECK(IEC104_COUNTER_INTERROGATION_RULES.data_cause(5) == 37); +} + +/* 交叉验证Type/SQ/FRZ:计数量只在读取时允许Type15或非连续寻址的Type37。 + * 总召保持原九种监视类型,不能因共用引擎而接受累计量或命令类型。 */ +static void test_types(void) +{ + static const uint8_t types[] = {1, 3, 5, 7, 9, 11, 13, 15, 16, 20, 21, 37, 100, 101}; + unsigned i, sq, freeze; + for (i = 0; i < sizeof(types); ++i) { + for (sq = 0; sq < 2; ++sq) { + for (freeze = 0; freeze < 4; ++freeze) { + IEC60870_AsduInfo_t info; + bool expected = freeze == 0 && (types[i] == 15 || (types[i] == 37 && sq == 0)); + memset(&info, 0, sizeof(info)); info.type_id = types[i]; info.is_sequence = (uint8_t)sq; + CHECK(IEC104_COUNTER_INTERROGATION_RULES.data_allowed(&info, (uint8_t)freeze) == expected); + if (freeze == 0) { + bool general = types[i] == 1 || types[i] == 3 || types[i] == 5 || types[i] == 7 || + types[i] == 9 || types[i] == 11 || types[i] == 13 || types[i] == 20 || types[i] == 21; + CHECK(IEC104_GENERAL_INTERROGATION_RULES.data_allowed(&info, 0) == general); + } + } + } + } +} + +int main(void) +{ + test_decode(); test_qualifiers(); test_types(); + printf("interrogation rules: 3 groups passed; request=%zu event=%zu station=%zu\n", + sizeof(IEC104_InterrogationRequest_t), sizeof(IEC104_InterrogationEvent_t), + sizeof(IEC104_InterrogationStationState_t)); + return 0; +} diff --git a/IEC60870-Slave/.build/command-v1/before/test/iec104_net_pending_input_test.c b/IEC60870-Slave/.build/command-v1/before/test/iec104_net_pending_input_test.c new file mode 100644 index 00000000..6752cf6b --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/test/iec104_net_pending_input_test.c @@ -0,0 +1,70 @@ +/* Windows回环验证:Pending_Input只读快照、部分读取、耗尽与EOF。 */ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include "IEC104_Net.h" +#include +#include +#include + +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "line %d: %s\n", __LINE__, #x); exit(1); } } while (0) + +int main(void) +{ + IEC104_NetSocket_t listener, accepted; + IEC104_NetPeer_t peer; + IEC104_NetIoResult_t received; + SOCKET client; + struct sockaddr_in address; + uint8_t sent[64], copy[64]; + uint16_t port; + uint32_t count, ready, index; + uint64_t deadline; + int error; + for (index = 0; index < sizeof(sent); ++index) { sent[index] = (uint8_t)(index * 7U); } + CHECK(IEC104_Net_Init(&error) == IEC104_NET_OK); + CHECK(IEC104_Net_Listen("127.0.0.1", 0, &listener, &port, &error) == IEC104_NET_OK); + client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + CHECK(client != INVALID_SOCKET); + memset(&address, 0, sizeof(address)); + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + address.sin_port = htons(port); + CHECK(connect(client, (const struct sockaddr *)&address, sizeof(address)) == 0); + CHECK(IEC104_Net_Wait(listener, IEC104_NET_INVALID_SOCKET, IEC104_NET_EVENT_ACCEPT, 1000, + &ready, &error) == IEC104_NET_OK); + CHECK(ready & IEC104_NET_EVENT_ACCEPT); + CHECK(IEC104_Net_Accept(listener, &accepted, &peer, &error) == IEC104_NET_OK); + CHECK(IEC104_Net_Pending_Input(accepted, &count, &error) == IEC104_NET_OK && count == 0); + CHECK(send(client, (const char *)sent, sizeof(sent), 0) == sizeof(sent)); + deadline = IEC104_Net_Now_Ms() + 1000; + do { + CHECK(IEC104_Net_Pending_Input(accepted, &count, &error) == IEC104_NET_OK); + if (count == sizeof(sent)) { break; } + Sleep(1); + } while (IEC104_Net_Now_Ms() < deadline); + CHECK(count == sizeof(sent)); + CHECK(IEC104_Net_Pending_Input(accepted, &count, &error) == IEC104_NET_OK && count == sizeof(sent)); + received = IEC104_Net_Recv(accepted, copy, 17); + CHECK(received.result == IEC104_NET_OK && received.transferred == 17); + CHECK(IEC104_Net_Pending_Input(accepted, &count, &error) == IEC104_NET_OK && count == sizeof(sent) - 17); + received = IEC104_Net_Recv(accepted, copy + 17, sizeof(copy) - 17); + CHECK(received.result == IEC104_NET_OK && received.transferred == sizeof(copy) - 17); + CHECK(memcmp(sent, copy, sizeof(copy)) == 0); + CHECK(IEC104_Net_Pending_Input(accepted, &count, &error) == IEC104_NET_OK && count == 0); + CHECK(shutdown(client, SD_SEND) == 0); + CHECK(IEC104_Net_Wait(IEC104_NET_INVALID_SOCKET, accepted, IEC104_NET_EVENT_READ, 1000, + &ready, &error) == IEC104_NET_OK && (ready & IEC104_NET_EVENT_READ)); + CHECK(IEC104_Net_Pending_Input(accepted, &count, &error) == IEC104_NET_OK && count == 0); + CHECK(IEC104_Net_Recv(accepted, copy, sizeof(copy)).result == IEC104_NET_EOF); + count = 123; + CHECK(IEC104_Net_Pending_Input(IEC104_NET_INVALID_SOCKET, &count, &error) == IEC104_NET_ERROR); + CHECK(count == 123 && error != 0); + CHECK(IEC104_Net_Pending_Input(accepted, NULL, &error) == IEC104_NET_ERROR); + CHECK(IEC104_Net_Close(accepted, &error) == IEC104_NET_OK); + CHECK(IEC104_Net_Close(listener, &error) == IEC104_NET_OK); + CHECK(closesocket(client) == 0); + IEC104_Net_Cleanup(); + puts("Windows Net pending input: loopback passed"); + return 0; +} diff --git a/IEC60870-Slave/.build/command-v1/before/test/iec104_slave_example_test.c b/IEC60870-Slave/.build/command-v1/before/test/iec104_slave_example_test.c new file mode 100644 index 00000000..e870763a --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/test/iec104_slave_example_test.c @@ -0,0 +1,4920 @@ +/* 纯事件总召模块及示例接入回归:真实Slave/TCP提交路径 + 可控Net,无系统socket。 + * 仅链接scripts/build.ps1中的公共依赖,不再链接TcpServer.c或Net后端。 + * HIGH缩到251字节,通过应用应答和真实发送背压覆盖各业务阶段及自动推进的停止条件。 */ +#define main existing_event_ring_main +#define IEC104_Net_Now_Ms example_test_base_now +#include "iec104_tcp_event_ring_test.c" +#undef IEC104_Net_Now_Ms +#undef main +/* Advance only when armed: exercise two module time reads crossing a deadline. */ +static uint64_t cs_test_now_step; +uint64_t IEC104_Net_Now_Ms(void) +{ + uint64_t now = example_test_base_now(); + fake.now += cs_test_now_step; + return now; +} +#define IEC104_SLAVE_EXAMPLE_NO_MAIN +#include "../examples/iec104_slave_example.c" +#include "IEC104_ClockSynchronization.h" + +static ExampleApp app; +static IEC104_InterrogationStationState_t broadcast_states[16]; +static unsigned master_ns; +static IEC104_SlaveEvent_t last_request; +static const IEC104_InterrogationConfig_t *custom_config; +static const IEC104_InterrogationConfig_t *counter_custom_config; +static bool counter_enabled; +static bool clock_enabled; +static const IEC104_ClockSynchronizationConfig_t *clock_custom_config; +static IEC104_InterrogationStationState_t counter_broadcast_states[16]; +static uint16_t custom_window = 32; +static const uint8_t gi_request[] = {100, 1, 0x86, 0x5a, 1, 0, 0, 0, 0, 20}; +static const uint8_t fallback_request[] = {106, 1, 0x86, 0x61, 1, 0, 0, 0, 0, 0x39, 0x30}; +static const uint8_t read_request[] = {102, 1, 5, 0x62, 1, 0, 0xe9, 3, 0}; + +static void request_unchanged(const uint8_t *request, size_t size); + +static void app_poll(void) +{ + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + { + IEC104_SlaveEvent_t event; + IEC104_TcpEventResult_t result; + while ((result = IEC104_Slave_NextEvent(app.slave, &event)) == IEC104_TCP_EVENT_READY) { + if (event.transport.type == IEC104_TCP_EVENT_I_FRAME) { + CHECK(IEC104_Slave_Copy_Event(&last_request, &event)); + } + CHECK(Example_Handle_Event(&app, &event)); + } + CHECK(result == IEC104_TCP_EVENT_EMPTY); + } +} + +static void app_connect(void) +{ + fake.accept_pending = 1; + app_poll(); + CHECK(!app.gi.active); + push(start_act, sizeof(start_act)); + app_poll(); + CHECK(app.gi.active); + master_ns = 0; +} + +static void app_open(void) +{ + IEC104_SlaveConfig_t config; + IEC104_Slave_t *slave; + memset(&fake, 0, sizeof(fake)); + fake.now = 1000; + slave = IEC104_Slave_Create(); + CHECK(slave != NULL); + IEC104_Slave_Config_Default(&config); + config.protocol.link.k = custom_window; + CHECK(IEC104_Slave_Init(slave, &config, NULL) == IEC104_TCP_OK); + /* 可控后端仅用此指针调整窗口或注入底层故障,不替换发送路径。 */ + { + unsigned slot; + sut = NULL; + for (slot = 0; slot < IEC104_TCP_SERVER_CAPACITY; ++slot) { + if (server_pool[slot].in_use != 0U) { sut = &server_pool[slot].server; break; } + } + CHECK(sut != NULL); + } + CHECK(Example_Init(&app, slave, custom_config == NULL ? 1U : custom_config->common_address, + broadcast_states, sizeof(broadcast_states) / sizeof(broadcast_states[0]))); + if (custom_config != NULL) { + CHECK(IEC104_Interrogation_Destroy(app.interrogation)); + app.interrogation = IEC104_Interrogation_Create(slave, custom_config); + CHECK(app.interrogation != NULL); + } + if (counter_enabled) { + CHECK(Example_Init_Counters(&app, counter_broadcast_states, 16U)); + if (counter_custom_config != NULL) { + CHECK(IEC104_Interrogation_Destroy(app.counter)); + app.counter = IEC104_CounterInterrogation_Create(slave, counter_custom_config); + CHECK(app.counter != NULL); + } + } + if (clock_enabled) { CHECK(Example_Init_Clock(&app, clock_custom_config)); } + app_connect(); +} + +static void app_close(void) +{ + IEC104_Slave_t *slave = app.slave; + Example_Cancel(&app); + CHECK(Example_Drain_Business(&app)); + CHECK(Example_Drain_Counter(&app)); + CHECK(IEC104_Slave_Close(slave, NULL) == IEC104_TCP_OK); + CHECK(Example_Drain(&app)); + CHECK(IEC104_Interrogation_Destroy(app.interrogation)); + if (app.counter != NULL) { CHECK(IEC104_Interrogation_Destroy(app.counter)); } + if (app.clock != NULL) { CHECK(IEC104_ClockSynchronization_Destroy(app.clock)); } + if (IEC104_Station_Is_Registered(&app.station)) { + CHECK(IEC104_Slave_Remove_Station(slave, &app.station) == IEC104_STATION_OK); + } + CHECK(IEC104_Station_Is_Reusable(&app.station)); + CHECK(IEC104_Slave_Destroy(slave, NULL) == IEC104_TCP_OK); + memset(&app, 0, sizeof(app)); +} + +/* 独立拼接主站I帧,避免用待测ASDU构造器生成输入或期望结果。 */ +static void queue_request(const uint8_t *payload, size_t length) +{ + uint8_t frame[255] = {0x68}; + CHECK(length <= 249); + frame[1] = (uint8_t)(length + 4U); + frame[2] = (uint8_t)(master_ns << 1); + frame[3] = (uint8_t)(master_ns >> 7); + ++master_ns; + memcpy(frame + 6, payload, length); + push(frame, length + 6); +} + +static void app_request(const uint8_t *payload, size_t length) +{ + queue_request(payload, length); + app_poll(); +} + +static void app_finish(void) +{ + unsigned i; + for (i = 0; i < 128; ++i) { + app_poll(); + CHECK(Example_Step(&app)); + } + CHECK(app.gi.pending_requests == 0 && !app.gi.gi_pending); +} + +/* 跳过U/S帧和用于制造HIGH拥塞的Type200填充帧,保留真实业务输出顺序。 */ +static unsigned collect_asdus(const uint8_t **out, size_t *lengths) +{ + size_t offset = 0; + unsigned count = 0; + while (offset < fake.sent_bytes) { + const uint8_t *frame = fake.output + offset; + size_t length; + CHECK(fake.sent_bytes - offset >= 6 && frame[0] == 0x68); + length = (size_t)frame[1] + 2U; + CHECK(length >= 6 && offset + length <= fake.sent_bytes); + if ((frame[2] & 1U) == 0U && frame[6] != 200) { + CHECK(count < 32); + out[count] = frame + 6; + lengths[count++] = length - 6; + } + offset += length; + } + return count; +} + +static void check_gi(unsigned extra_reply, uint8_t oa) +{ + static const uint8_t values[] = {0, 1, 0, 1, 1, 0, 1, 0}; + static const int16_t measures[] = {2300, 2310, 2290, 5000, -125, 0, 1234, 42}; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned f, p, indexes[6], gi_count = 0U; + unsigned total = collect_asdus(frames, lengths); + CHECK(total == 6 + extra_reply); + for (f = 0U; f < total; ++f) { + if (frames[f][3] == oa && (frames[f][2] & 0x40U) == 0U) { + CHECK(gi_count < 6U); + indexes[gi_count++] = f; + } + } + CHECK(gi_count == 6U); /* 其他请求的否定应答可穿插,但本次总召内容及自身顺序不变。 */ + for (f = 0; f < 6; ++f) { + const uint8_t *a = frames[indexes[f]]; + CHECK(a[3] == oa && a[4] == 1 && a[5] == 0); + if (f == 0 || f == 5) { + CHECK(lengths[indexes[f]] == 10 && a[0] == 100 && a[1] == 1); + CHECK(a[2] == (f == 0 ? 0x87 : 0x8a)); + CHECK(a[6] == 0 && a[7] == 0 && a[8] == 0 && a[9] == 20); + } else { + unsigned is_signal = f < 3; + size_t width = is_signal ? 4U : 6U; + unsigned first = (is_signal ? f - 1U : f - 3U) * 4U; + CHECK(a[0] == (is_signal ? 1 : 11) && a[1] == 4 && a[2] == 0x94); + CHECK(lengths[indexes[f]] == 6 + width * 4); + for (p = 0; p < 4; ++p) { + const uint8_t *object = a + 6 + width * p; + unsigned ioa = object[0] | (unsigned)object[1] << 8 | (unsigned)object[2] << 16; + CHECK(ioa == (is_signal ? 1001U : 2001U) + first + p); + if (is_signal) { CHECK(object[3] == values[first + p]); } + else { + CHECK((int16_t)(object[3] | (uint16_t)object[4] << 8) == measures[first + p]); + CHECK(object[5] == 0); + } + } + } + } + CHECK(app.gi.gi_submitted == 1); +} + +static void test_total_call(void) +{ + app_open(); + fake.send_limit = 3; /* 每帧分多次write,仍只有一次提交且原字节顺序不变。 */ + app_request(gi_request, sizeof(gi_request)); + CHECK(app.gi.pending_requests == 1); + app_finish(); + check_gi(0, 0x5a); + app_close(); +} + +/* 把实际HIGH填满,并保留一个尚未写出的当前帧;失败必须是WAIT而不是测试桩伪造。 */ +static void congest(IEC60870_MasterConnection_t *connection) +{ + uint8_t bytes[249] = {200, 0, 3, 0, 1, 0}; + IEC60870_Asdu_t filler; + unsigned i; + CHECK(IEC60870_Asdu_Create_From_Buffer(&filler, + IEC104_Slave_Get_Application_Layer_Parameters(app.slave), bytes, sizeof(bytes)) == IEC60870_ASDU_OK); + fake.send_blocked = 1; + for (i = 0; i < 32; ++i) { + if (!IEC60870_MasterConnection_Send_ASDU(connection, &filler)) { + CHECK(IEC104_Slave_Get_Last_Submit_Result(connection) == IEC104_TCP_SUBMIT_WAIT); + return; + } + } + CHECK(0); +} + +/* 保留一个阻塞的当前帧,HIGH中只留下一个10字节应答及其2字节记录头的空间。 + * 通过真实容量限制在第一条REPLY后产生WAIT,不依赖业务层的处理步数参数。 */ +static void congest_except_one_reply(IEC60870_MasterConnection_t *connection) +{ + uint8_t bytes[249] = {200, 0, 3, 0, 1, 0}; + IEC60870_Asdu_t filler; + fake.send_blocked = 1; + CHECK(IEC60870_Asdu_Create_From_Buffer(&filler, + IEC104_Slave_Get_Application_Layer_Parameters(app.slave), bytes, sizeof(bytes)) == IEC60870_ASDU_OK); + CHECK(IEC60870_MasterConnection_Send_ASDU(connection, &filler)); + CHECK(IEC60870_Asdu_Create_From_Buffer(&filler, + IEC104_Slave_Get_Application_Layer_Parameters(app.slave), bytes, sizeof(bytes) - 12U) == IEC60870_ASDU_OK); + CHECK(IEC60870_MasterConnection_Send_ASDU(connection, &filler)); +} + +static void test_wait_each_phase(void) +{ + static const struct { IEC104_InterrogationPhase_t phase; size_t batch; } phases[] = { + {IEC104_INTERROGATION_CONFIRM, 0}, {IEC104_INTERROGATION_DATA, 0}, + {IEC104_INTERROGATION_DATA, 2}, {IEC104_INTERROGATION_TERMINATE, 4} + }; + unsigned i; + for (i = 0; i < sizeof(phases) / sizeof(phases[0]); ++i) { + size_t point_index; + unsigned steps; + app_open(); + app_request(gi_request, sizeof(gi_request)); + for (steps = 0; steps < 32; ++steps) { + if (app.gi.phase == phases[i].phase && app.gi.batch_index == phases[i].batch) { break; } + CHECK(IEC104_Interrogation_Process(app.interrogation)); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + if (app.gi.phase == phases[i].phase && app.gi.batch_index == phases[i].batch) { break; } + CHECK(Example_Drain_Business(&app)); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + } + CHECK(steps < 32); + congest(&last_request.connection); + point_index = app.gi.batch_index; + /* DATA阶段由应用应答当场尝试发送;WAIT不得推进应用或协议游标。 */ + CHECK(Example_Step(&app)); + if (!app.gi.waiting) { CHECK(Example_Step(&app)); } + CHECK(app.gi.waiting && app.gi.phase == phases[i].phase); + CHECK(Example_Step(&app) && app.gi.waiting && app.gi.batch_index == point_index); + fake.send_blocked = 0; + fake.send_limit = 3; + app_finish(); + CHECK(app.gi.waits >= 2); + check_gi(0, 0x5a); + app_close(); + } +} + +static void test_validation(void) +{ + static const struct { unsigned offset; uint8_t value, cot; } cases[] = { + {0, 200, 44}, {2, 0x83, 45}, {4, 2, 46}, {6, 1, 47}, {9, 19, 7}, {9, 37, 7}, {2, 0xc6, 7} + }; + unsigned i; + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + const uint8_t *out[32]; + size_t lengths[32], j; + uint8_t request[sizeof(gi_request)]; + app_open(); + memcpy(request, gi_request, sizeof(request)); + request[cases[i].offset] = cases[i].value; + app_request(request, sizeof(request)); + CHECK(!app.gi.gi_pending && app.gi.pending_requests == 0); + CHECK((i == 0 && app.other_submitted == 1) || (i != 0 && app.gi.negative_submitted == 1)); + app_finish(); + /* Type200既用作填充也用作未知类型请求;本案例直接检查唯一输出帧。 */ + if (i == 0) { out[0] = fake.output + 12; lengths[0] = fake.output[7] - 4U; } + else { CHECK(collect_asdus(out, lengths) == 1); } + CHECK(lengths[0] == sizeof(request)); + for (j = 0; j < sizeof(request); ++j) { + CHECK(out[0][j] == (j == 2 ? (uint8_t)(0xc0 | cases[i].cot) : request[j])); + } + CHECK((app.gi.rejected + app.other_rejected) == 1 && app.gi.gi_submitted == 0); + app_close(); + } +} + +static void test_busy(void) +{ + const uint8_t *out[32]; + size_t lengths[32]; + uint8_t second[sizeof(gi_request)]; + app_open(); + app_request(gi_request, sizeof(gi_request)); + memcpy(second, gi_request, sizeof(second)); + second[3] = 0x6b; + app_request(second, sizeof(second)); + CHECK(app.gi.pending_requests == 1 && app.gi.negative_submitted == 1); + app_finish(); + check_gi(1, 0x5a); + CHECK(collect_asdus(out, lengths) == 7); + CHECK(lengths[0] == 10 && out[0][2] == 0xc7 && out[0][3] == 0x6b); + app_close(); +} + +static void test_stop(void) +{ + uint8_t second[sizeof(gi_request)]; + app_open(); + app_request(gi_request, sizeof(gi_request)); + push(stop_act, sizeof(stop_act)); + app_poll(); + CHECK(app.gi.pending_requests == 0 && !app.gi.active && app.gi.cancelled == 1); + push(start_act, sizeof(start_act)); + app_poll(); + memcpy(second, gi_request, sizeof(second)); + second[3] = 0x6b; + app_request(second, sizeof(second)); + app_finish(); + check_gi(0, 0x6b); + app_close(); +} + +static void test_disconnect(void) +{ + IEC104_SlaveEvent_t old; + uint64_t generation; + uint8_t second[sizeof(gi_request)]; + app_open(); + app_request(gi_request, sizeof(gi_request)); + generation = app.gi.generation; + CHECK(IEC104_Slave_Copy_Event(&old, &last_request)); + CHECK(IEC104_Slave_Close_Connection(app.slave, &old, NULL) == IEC104_TCP_OK); + CHECK(Example_Drain(&app) && app.gi.pending_requests == 0 && !app.gi.active); + app_connect(); + CHECK(app.gi.generation != generation); + CHECK(!IEC60870_MasterConnection_Send_ACT_CON(&old.connection, &old.asdu, false)); + memcpy(second, gi_request, sizeof(second)); + second[3] = 0x6b; + app_request(second, sizeof(second)); + app_finish(); + check_gi(0, 0x6b); + app_close(); +} + +static void test_malformed(void) +{ + static const struct { size_t length; uint8_t vsq, cot, ca, ioa, qoi, reply; } cases[] = { + {10, 0x81, 0x86, 1, 0, 20, 7}, {11, 1, 0x86, 1, 0, 20, 7}, + {249, 0x81, 0x86, 1, 0, 36, 7}, {11, 1, 0x88, 1, 0, 20, 9}, + {10, 0x81, 0x88, 1, 0, 20, 9}, {11, 1, 6, 1, 0, 20, 7}, + {11, 1, 0x83, 2, 1, 19, 45}, {11, 1, 0x86, 2, 1, 19, 46}, + {11, 1, 0x88, 1, 1, 19, 47}, {11, 1, 0xc6, 1, 0, 19, 7} + }; + unsigned i; + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + IEC104_TcpServerStatus_t status; + const uint8_t *frames[32]; + size_t lengths[32]; + uint8_t request[249], expected[10]; + app_open(); + memset(request, 0xa5, sizeof(request)); memcpy(request, gi_request, sizeof(gi_request)); + request[1] = cases[i].vsq; request[2] = cases[i].cot; request[3] = 0x67; + request[4] = cases[i].ca; request[6] = cases[i].ioa; request[9] = cases[i].qoi; + request_unchanged(request, cases[i].length); app_finish(); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.connected); + CHECK(!app.gi.gi_pending && app.gi.gi_submitted == 0 && app.gi.rejected == 1 && app.gi.cancelled == 0); + CHECK(app.gi.negative_submitted == 1 && app.gi.dropped_requests == 0 && app.other_rejected == 0); + memcpy(expected, request, sizeof(expected)); expected[1] = 1; + expected[2] = (uint8_t)((request[2] & 0x80U) | 0x40U | cases[i].reply); + CHECK(collect_asdus(frames, lengths) == 1 && lengths[0] == sizeof(expected)); + CHECK(memcmp(frames[0], expected, sizeof(expected)) == 0); + app_request(gi_request, sizeof(gi_request)); app_finish(); check_gi(1, 0x5a); + app_close(); + } +} + +/* 头部/最低载荷合法但对象不唯一:总召接手后丢弃,不能猜测首对象或进入UNKNOWN_TYPE兜底。 */ +static void test_malformed_drop(void) +{ + static const struct { size_t length; uint8_t vsq; } cases[] = { + {6, 0}, {10, 0}, {6, 0x80}, {14, 2}, {11, 0x82} + }; + unsigned i; + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + uint8_t request[14] = {0}; + IEC104_TcpServerStatus_t status; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); memcpy(request, gi_request, sizeof(gi_request)); request[1] = cases[i].vsq; + request_unchanged(request, cases[i].length); app_finish(); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.connected); + CHECK(collect_asdus(frames, lengths) == 0 && app.gi.rejected == 1 && app.gi.cancelled == 0); + CHECK(app.gi.negative_submitted == 0 && app.gi.dropped_requests == 0 && app.other_rejected == 0); + app_request(gi_request, sizeof(gi_request)); app_finish(); check_gi(0, 0x5a); + app_close(); + } +} + +/* 真正截短的公共头/载荷仍由Receiver和连接层拒绝;不能放宽为业务层的畸形单对象回复。 */ +static void test_receiver_truncated(void) +{ + static const struct { size_t length; uint8_t type, vsq; } cases[] = { + {5, 100, 1}, {9, 100, 1}, {10, 100, 2}, {10, 100, 0x82}, {5, 101, 1} + }; + unsigned i; + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + uint8_t request[sizeof(gi_request)]; + IEC104_TcpServerStatus_t status; + app_open(); memcpy(request, gi_request, sizeof(request)); + request[0] = cases[i].type; request[1] = cases[i].vsq; + app_request(request, cases[i].length); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && !status.connected); + CHECK(app.gi.rejected == 0 && app.gi.submitted == 0 && app.gi.pending_requests == 0); + app_close(); + } +} + +static void test_overload(void) +{ + unsigned i; + IEC104_TcpServerStatus_t status; + uint8_t request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + memcpy(request, gi_request, sizeof(request)); + request[9] = 19; /* 仅有短应答也能占满池;新总召不得挤掉旧应答。 */ + for (i = 0; i < 4; ++i) { request[3] = (uint8_t)(0x60U + i); queue_request(request, sizeof(request)); } + app_poll(); + app_request(gi_request, sizeof(gi_request)); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.connected); + CHECK(app.gi.overloads == 1 && app.gi.pending_requests == 4 && !app.gi.gi_pending); + CHECK(app.gi.negative_submitted == 0 && app.gi.dropped_requests == 1 && app.gi.cancelled == 0); + app_finish(); + CHECK(collect_asdus(frames, lengths) == 4 && app.gi.negative_submitted == 4); + for (i = 0; i < 4; ++i) { CHECK(frames[i][2] == 0xc7 && frames[i][3] == 0x60U + i); } + app_close(); +} + +/* STOP/START排在一轮通知预算之后时,旧总召也不能在同一TCP重新激活后继续发送。 */ +static void test_event_budget_stop_start(void) +{ + IEC60870_Asdu_t filler; + IEC104_TcpServerStatus_t status; + uint8_t bytes[] = {200, 0, 3, 0, 1, 0}; + unsigned i; + app_open(); + app_request(gi_request, sizeof(gi_request)); + CHECK(IEC60870_Asdu_Create_From_Buffer(&filler, + IEC104_Slave_Get_Application_Layer_Parameters(app.slave), bytes, sizeof(bytes)) == IEC60870_ASDU_OK); + for (i = 0; i < 32; ++i) { + CHECK(IEC60870_MasterConnection_Send_ASDU(&last_request.connection, &filler)); + } + push_ack(32); + for (i = 0; i < 8; ++i) { CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); } + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK); + CHECK(status.pending_event_count == 32); + push(stop_act, sizeof(stop_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + push(start_act, sizeof(start_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + /* 直接验证库入口也会让出,不仅依靠示例自己的事件预算标志。 */ + CHECK(IEC104_Interrogation_Process(app.interrogation)); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.events_pending && app.gi.submitted == 0U && app.gi.pending_requests == 1U); + CHECK(Example_Drain(&app) && app.events_pending && app.gi.pending_requests == 1); + CHECK(Example_Step(&app) && (app.gi.submitted + app.other_submitted) == 0); + CHECK(Example_Drain(&app) && !app.events_pending && app.gi.active && app.gi.pending_requests == 0); + CHECK(Example_Step(&app) && (app.gi.submitted + app.other_submitted) == 0); + app_close(); +} + +static void test_non_gi_ignored(void) +{ + unsigned busy; + for (busy = 0; busy < 4; ++busy) { + IEC104_SlaveEvent_t event; + uint8_t input[] = {101, 1, 6, 0x5a, 1, 0, 0, 0, 0, 5}; + app_open(); + if (busy & 1U) { app_request(gi_request, sizeof(gi_request)); } + if (busy & 2U) { input[4] = input[5] = 0xff; } + queue_request(input, sizeof(input)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_NextEvent(app.slave, &event) == IEC104_TCP_EVENT_READY); + CHECK(IEC104_Interrogation_Handle_Event(app.interrogation, &event) == IEC104_INTERROGATION_EVENT_IGNORED); + CHECK(memcmp(event.transport.data.i_frame.apdu + 6, input, sizeof(input)) == 0); + CHECK(IEC104_Interrogation_Process(app.interrogation)); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi) && app.gi.pending_requests == (busy & 1U)); + CHECK(app.gi.rejected == 0 && app.gi.submitted == 0); /* 总召忙不能替总电度作答。 */ + app_close(); + } +} + +static void test_module_contracts(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationStatus_t status, before; + IEC104_Slave_t *slave = IEC104_Slave_Create(); + IEC104_Interrogation_t *gi; + CHECK(slave != NULL); + IEC104_Interrogation_Config_Default(NULL); + IEC104_Interrogation_Config_Default(&config); + CHECK(IEC104_Interrogation_Create(NULL, &config) == NULL); + CHECK(IEC104_Interrogation_Create(slave, NULL) == NULL); + config.common_address = 0U; + CHECK(IEC104_Interrogation_Create(slave, &config) == NULL); + config.common_address = IEC104_CA_BROADCAST; + CHECK(IEC104_Interrogation_Create(slave, &config) == NULL); + config.common_address = 1U; + config.max_objects_per_asdu = 0U; + CHECK(IEC104_Interrogation_Create(slave, &config) == NULL); + config.max_objects_per_asdu = 128U; + CHECK(IEC104_Interrogation_Create(slave, &config) == NULL); + config.max_objects_per_asdu = 60U; + gi = IEC104_Interrogation_Create(slave, &config); + CHECK(gi != NULL && IEC104_Interrogation_Create(slave, &config) == NULL); + CHECK(!IEC104_Interrogation_Process(NULL)); + CHECK(IEC104_Interrogation_Handle_Event(gi, NULL) == IEC104_INTERROGATION_EVENT_ERROR); + memset(&status, 0xa5, sizeof(status)); + memcpy(&before, &status, sizeof(status)); + CHECK(!IEC104_Interrogation_Get_Status(NULL, &status) && memcmp(&status, &before, sizeof(status)) == 0); + CHECK(IEC104_Interrogation_Get_Status(gi, &status) && status.phase == IEC104_INTERROGATION_IDLE); + IEC104_Interrogation_Cancel(gi); + CHECK(IEC104_Interrogation_Destroy(gi) && !IEC104_Interrogation_Destroy(gi)); + CHECK(!IEC104_Interrogation_Process(gi)); + CHECK(IEC104_Interrogation_Create(slave, &config) == gi); /* 销毁后可重新创建。 */ + CHECK(IEC104_Interrogation_Destroy(gi)); + CHECK(IEC104_Slave_Destroy(slave, NULL) == IEC104_TCP_OK); +} + +/* 以下直接驱动公开业务事件API,不经过示例的自动事件应答。 */ +static void gi_process(void) +{ + app_poll(); + CHECK(IEC104_Interrogation_Process(app.interrogation)); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); +} + +static IEC104_InterrogationEvent_t gi_take(IEC104_InterrogationEventType_t type) +{ + IEC104_InterrogationEvent_t event; + CHECK(IEC104_Interrogation_NextEvent(app.interrogation, &event) == IEC104_INTERROGATION_NEXT_READY); + CHECK(event.type == type && event.instance_id != 0U && event.request_id != 0U); + return event; +} + +static void gi_empty(void) +{ + IEC104_InterrogationEvent_t event, before; + memset(&event, 0xa5, sizeof(event)); + memcpy(&before, &event, sizeof(before)); + CHECK(IEC104_Interrogation_NextEvent(app.interrogation, &event) == IEC104_INTERROGATION_NEXT_EMPTY); + CHECK(memcmp(&event, &before, sizeof(event)) == 0); + CHECK(IEC104_Interrogation_NextEvent(NULL, &event) == IEC104_INTERROGATION_NEXT_ERROR); + CHECK(memcmp(&event, &before, sizeof(event)) == 0); +} + +static IEC104_InterrogationEvent_t gi_begin(void) +{ + app_request(gi_request, sizeof(gi_request)); + gi_process(); + return gi_take(IEC104_INTERROGATION_BEGIN); +} + +static IEC104_InterrogationEvent_t gi_data(void) +{ + IEC104_InterrogationEvent_t event = gi_begin(); + CHECK(event.request.common_address == 1 && event.request.originator_address == 0x5a && + event.request.qualifier == 20 && event.request.is_test && event.request.receive_serial != 0U); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_STALE); + gi_process(); + return gi_take(IEC104_INTERROGATION_NEED_DATA); +} + +static void gi_done(const IEC104_InterrogationEvent_t *event, size_t batches, size_t objects) +{ + IEC104_InterrogationEvent_t finish; + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, event) == IEC104_INTERROGATION_REPLY_OK); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, event) == IEC104_INTERROGATION_REPLY_STALE); + gi_process(); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_COMPLETED && + finish.request_id == event->request_id && finish.batch_index == batches && finish.objects_submitted == objects); + gi_empty(); + app_poll(); +} + +/* 以应用自己的参数/缓冲构造单点ASDU;Submit成功后这些存储可全部修改。 */ +static void single_batch(IEC60870_Asdu_t *asdu, IEC60870_AppLayerParameters_t *parameters, + uint8_t *bytes, unsigned count) +{ + IEC60870_AsduHeader_t header = {20, 0, 1, 0x5a, 1}; + unsigned i; + *parameters = *IEC104_Slave_Get_Application_Layer_Parameters(app.slave); + CHECK(IEC60870_Asdu_Create(asdu, parameters, bytes, 249, &header, 0) == IEC60870_ASDU_OK); + for (i = 0; i < count; ++i) { + IEC60870_SinglePointInformation_t point; + CHECK(IEC60870_SinglePointInformation_Create(&point, 1001 + i, 1, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(asdu, &point.object) == IEC60870_ASDU_OK); + } +} + +/* 仅推进当前业务任务,不隐式Poll或消费Slave通知,便于检查自动推进的停止条件。 */ +static void gi_process_only(void) +{ + CHECK(IEC104_Interrogation_Process(app.interrogation)); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); +} + +/* 同一IOA的突发原文采用独立报文字节,避免由总召构造逻辑生成预期结果。 */ +static void spontaneous_value(uint8_t value) +{ + uint8_t bytes[] = {1, 1, 3, 0, 1, 0, 0xe9, 3, 0, 0}; + IEC60870_Asdu_t asdu; + bytes[9] = value; + CHECK(IEC60870_Asdu_Create_From_Buffer(&asdu, + IEC104_Slave_Get_Application_Layer_Parameters(app.slave), bytes, sizeof(bytes)) == IEC60870_ASDU_OK); + CHECK(IEC104_Slave_Enqueue_Event(app.slave, &asdu) == IEC104_TCP_SUBMIT_OK); +} + +/* 旧突发不能晚于新总召;新突发先发后,WAIT的旧总召必须重新取值,且不阻挡突发。 */ +static void test_gi_spontaneous_order(void) +{ + unsigned mode; + for (mode = 0; mode < 3; ++mode) { + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + IEC104_TcpServerStatus_t before, after; + uint8_t bytes[249]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); event = gi_data(); + single_batch(&batch, ¶meters, bytes, 1); + bytes[9] = mode == 0 ? 1 : 0; + if (mode == 2) { congest(&last_request.connection); } + spontaneous_value(mode == 0 ? 0 : 1); + CHECK(IEC104_Slave_Get_Status(app.slave, &before, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + CHECK(IEC104_Slave_Get_Status(app.slave, &after, NULL) == IEC104_TCP_OK); + CHECK(after.high_queue.stored_asdu_count == before.high_queue.stored_asdu_count && + after.normal_queue.pending_asdu_count == before.normal_queue.pending_asdu_count && + after.connection.pending_send_records == before.connection.pending_send_records); + gi_process_only(); + CHECK(app.gi.data_send_waiting && app.gi.batch_index == 0 && !app.gi.need_process); + memset(bytes, 0xcc, sizeof(bytes)); /* WAIT没有借用或缓存旧批次。 */ + fake.send_blocked = 0; gi_process(); + CHECK(collect_asdus(frames, lengths) == 2 && frames[1][2] == 3 && frames[1][9] == (mode == 0 ? 0 : 1)); + single_batch(&batch, ¶meters, bytes, 1); /* 重新取到当前值1。 */ + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(&event, 1, 1); + CHECK(collect_asdus(frames, lengths) == 4 && frames[2][2] == 0x94 && frames[2][9] == 1); + CHECK(frames[3][2] == 0x8a); + app_close(); + } +} + +/* 总召已接手后,即使只写了部分APDU,后来的新突发也必须排在它后面。 */ +static void test_gi_accepted_order(void) +{ + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + const uint8_t *frames[32]; + size_t lengths[32], written; + app_open(); event = gi_data(); + single_batch(&batch, ¶meters, bytes, 1); bytes[9] = 0; + written = fake.sent_bytes; fake.send_limit = 3; + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + CHECK(fake.sent_bytes > written && fake.sent_bytes < written + 16); + memset(bytes, 0xcc, sizeof(bytes)); + spontaneous_value(1); + fake.send_limit = 0; gi_process(); + CHECK(collect_asdus(frames, lengths) == 3); + CHECK(frames[1][2] == 0x94 && frames[1][9] == 0 && frames[2][2] == 3 && frames[2][9] == 1); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&event, 1, 1); + app_close(); +} + +/* 首次有效数据及时交付后,发送WAIT不耗尽取数期限;下一批仍受新的取数期限保护。 */ +static void test_gi_send_wait_timeout(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t event, finish; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + IEC104_Interrogation_Config_Default(&config); config.data_timeout_ms = 20; + custom_config = &config; app_open(); event = gi_data(); + congest(&last_request.connection); + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC60870_Asdu_Set_CA(&batch, 2) == IEC60870_ASDU_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_ERROR); + fake.now += 19; gi_process_only(); + CHECK(app.gi.application_waiting && !app.gi.data_send_waiting && app.gi.timeout_remaining_ms == 1); + CHECK(IEC60870_Asdu_Set_CA(&batch, 1) == IEC60870_ASDU_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + fake.now += 100; gi_process_only(); + CHECK(app.gi.timeouts == 0 && !app.gi.timer_active && !app.gi.application_waiting && app.gi.data_send_waiting); + fake.send_blocked = 0; gi_process(); + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(app.gi.application_waiting && !app.gi.data_send_waiting && app.gi.timeout_remaining_ms == 20); + fake.now += 20; gi_process_only(); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_DATA_TIMEOUT && finish.batch_index == 1); + app_close(); custom_config = NULL; +} + +/* k=1窗口满时不转入HIGH;ACK释放窗口后先发NORMAL,下一次ACK后才接手总召。 */ +static void test_gi_window_wait(void) +{ + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + IEC104_TcpServerStatus_t status; + uint8_t bytes[249]; + const uint8_t *frames[32]; + size_t lengths[32]; + custom_window = 1; app_open(); event = gi_data(); + single_batch(&batch, ¶meters, bytes, 1); bytes[9] = 0; + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.high_queue.stored_asdu_count == 0); + spontaneous_value(1); push_ack(1); gi_process(); + CHECK(collect_asdus(frames, lengths) == 2 && frames[1][2] == 3); + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + push_ack(2); gi_process(); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + push_ack(3); gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(&event, 1, 1); + CHECK(collect_asdus(frames, lengths) == 4 && frames[2][2] == 0x94 && frames[2][9] == 1); + app_close(); custom_window = 32; +} + +/* 对端确认期限已到时,不排队入口先让正常接收处理已经到达的ACK。 */ +static void test_gi_due_ack_wait(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + IEC104_Interrogation_Config_Default(&config); config.data_timeout_ms = 0; + custom_config = &config; app_open(); event = gi_data(); + single_batch(&batch, ¶meters, bytes, 1); + fake.now += 15000; push_ack(1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + gi_process(); + CHECK(app.gi.active && app.gi.batch_index == 0); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&event, 1, 1); + app_close(); custom_config = NULL; +} + +/* 不排队入口的FATAL使交互失效,不能把可能已发送的批次当成WAIT重试。 */ +static void test_gi_submit_fatal(void) +{ + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + app_open(); event = gi_data(); single_batch(&batch, ¶meters, bytes, 1); + sut->queue_initialized = 0; + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_STALE); + sut->queue_initialized = 1; + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_SEND_ERROR); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_STALE); + app_close(); +} + +/* 真实U帧填满启停通知槽后,新的GI及STOP同批到达;读空FIFO不自动受理保留的STOP。 */ +static void test_stop_in_process_after_state_backpressure(void) +{ + IEC104_InterrogationEvent_t event, finish; + IEC104_ConnectionStatus_t connection; + IEC104_TcpServerStatus_t service; + unsigned i; + app_open(); + CHECK(IEC104_TCP_EVENT_STATE_RESERVE % 2U == 0U); + for (i = 0; i < IEC104_TCP_EVENT_STATE_RESERVE; ++i) { + push(i % 2U == 0U ? stop_act : start_act, sizeof(start_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + } + queue_request(gi_request, sizeof(gi_request)); push(stop_act, sizeof(stop_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK && sut->rx_blocked); + CHECK(Example_Drain(&app)); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.pending_event_count == 0); + CHECK(IEC104_Connection_Get_Status(&sut->connection, sut->generation, &connection) == IEC104_CONNECTION_OK); + CHECK(connection.has_unhandled_frame && app.gi.gi_pending && app.gi.active); + gi_process_only(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process_only(); gi_empty(); + CHECK(app.gi.events_pending && !app.gi.need_process && app.gi.gi_pending && app.gi.cancelled == 0); + CHECK(app.gi.submitted == 0 && app.gi.waits == 0); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); + CHECK(!service.connection.data_transfer_started && service.pending_event_count != 0); + CHECK(Example_Drain(&app)); finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == IEC104_INTERROGATION_FINISH_STOPPED); + gi_empty(); app_close(); +} + +static int hold_transfer_change(void *context) +{ + (void)context; + return 0; +} + +/* 注入一次通知容量背压以覆盖任意业务阶段;STOP仍经真实Receiver/Drive受理,不伪造发送结果。 */ +static void retain_stop_for_submit(void) +{ + IEC104_ConnectionTransferObserver_t observer = sut->connection.transfer_observer; + IEC104_TcpServerStatus_t service; + sut->connection.transfer_observer.can_accept = hold_transfer_change; + push(stop_act, sizeof(stop_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + sut->connection.transfer_observer = observer; + CHECK(sut->rx_blocked && !sut->connection.receive_batch_pending); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK); + CHECK(service.pending_event_count == 0 && service.connection.data_transfer_started); +} + +/* 准备待交给Handle_Event的真实忙请求,测试立即应答入口;不调用业务处理器提前提交。 */ +static void read_busy_request(void) +{ + uint8_t request[sizeof(gi_request)]; + memcpy(request, gi_request, sizeof(request)); request[3] = 0x61; + queue_request(request, sizeof(request)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_NextEvent(app.slave, &last_request) == IEC104_TCP_EVENT_READY); + CHECK(last_request.transport.type == IEC104_TCP_EVENT_I_FRAME); +} + +/* Process、立即应答、Submit_Batch均须先交付STOP;不停止取数计时、不重发、可在START后重新总召。 */ +static void test_stop_during_submit(void) +{ + unsigned mode; + for (mode = 0; mode < 3; ++mode) { + IEC104_InterrogationEvent_t event, finish; + IEC104_InterrogationStatus_t before; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + app_open(); event = mode == 0 ? gi_begin() : gi_data(); + if (mode == 0) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } else if (mode == 1) { read_busy_request(); } + else { single_batch(&batch, ¶meters, bytes, 1); } + retain_stop_for_submit(); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &before)); + if (mode == 0) { CHECK(IEC104_Interrogation_Process(app.interrogation)); } + else if (mode == 1) { + CHECK(IEC104_Interrogation_Handle_Event(app.interrogation, &last_request) == IEC104_INTERROGATION_EVENT_HANDLED); + } else { + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + } + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.events_pending && !app.gi.need_process && app.gi.gi_pending && app.gi.cancelled == before.cancelled); + CHECK(app.gi.submitted == before.submitted && app.gi.waits == before.waits && app.gi.phase == before.phase); + CHECK(app.gi.batch_index == before.batch_index && app.gi.objects_submitted == before.objects_submitted); + CHECK(app.gi.data_send_waiting == before.data_send_waiting && app.gi.timer_active == before.timer_active); + CHECK(!before.timer_active || app.gi.timeout_remaining_ms == before.timeout_remaining_ms); + gi_empty(); gi_process_only(); gi_empty(); /* 通知未读时重复推进也不能抢先定终结原因。 */ + CHECK(Example_Drain(&app)); finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == IEC104_INTERROGATION_FINISH_STOPPED); + if (mode == 2) { + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_STALE); + } + gi_empty(); push(start_act, sizeof(start_act)); app_poll(); + app_request(gi_request, sizeof(gi_request)); app_finish(); + CHECK(app.gi.gi_submitted == 1 && app.gi.negative_submitted == 0); + app_close(); + } +} + +/* 通知已成功交付后注入内部故障,真实FATAL即使伴随STOP也不能降为WAIT。 */ +static int notify_stop_then_fail(void *context, IEC104_DataTransferState_t state) +{ + CHECK(Notify_Transfer_State(context, state)); + return 0; +} + +static void test_fatal_during_stop_submit(void) +{ + unsigned mode; + for (mode = 0; mode < 3; ++mode) { + IEC104_InterrogationEvent_t event, finish; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + IEC104_TcpServerStatus_t service; + uint8_t bytes[249]; + app_open(); event = mode == 0 ? gi_begin() : gi_data(); + if (mode == 0) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } else if (mode == 1) { read_busy_request(); } + else { single_batch(&batch, ¶meters, bytes, 1); } + retain_stop_for_submit(); sut->connection.transfer_observer.notify = notify_stop_then_fail; + if (mode == 0) { CHECK(IEC104_Interrogation_Process(app.interrogation)); } + else if (mode == 1) { + CHECK(IEC104_Interrogation_Handle_Event(app.interrogation, &last_request) == IEC104_INTERROGATION_EVENT_ERROR); + } else { + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_STALE); + } + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK); + CHECK(!service.connected && service.pending_event_count != 0 && service.connection.fault == IEC104_TCP_FAULT_INTERNAL); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == IEC104_INTERROGATION_FINISH_SEND_ERROR); + CHECK(Example_Drain(&app)); gi_empty(); app_close(); + } +} + +/* EOF后的提交属于ERROR;连接仍存在且传输快照仍为STARTED,不能误认成STOP。 */ +static void test_submit_error_without_stop(void) +{ + unsigned mode; + for (mode = 0; mode < 3; ++mode) { + IEC104_InterrogationEvent_t event, finish; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + IEC104_TcpServerStatus_t service; + uint8_t bytes[249]; + app_open(); event = mode == 0 ? gi_begin() : gi_data(); + if (mode == 0) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } else if (mode == 1) { read_busy_request(); } + else { single_batch(&batch, ¶meters, bytes, 1); } + sut->rx_eof = 1; /* 保留连接以完成EOF收尾,但不再允许新应答。 */ + if (mode == 0) { CHECK(IEC104_Interrogation_Process(app.interrogation)); } + else if (mode == 1) { + CHECK(IEC104_Interrogation_Handle_Event(app.interrogation, &last_request) == IEC104_INTERROGATION_EVENT_ERROR); + } else { + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_STALE); + } + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK); + CHECK(service.connected && service.connection.data_transfer_started && service.pending_event_count == 0); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == IEC104_INTERROGATION_FINISH_SEND_ERROR); + gi_empty(); app_close(); + } +} + +/* OK及WAIT也能顺带交付RECORD;成功只提交一次,后续应答必须等新通知被消费。 */ +static void test_submit_with_new_record(void) +{ + static const uint8_t ack[] = {0x68, 4, 1, 0, 2, 0}; + unsigned wait, i; + for (wait = 0; wait < 2; ++wait) { + IEC104_InterrogationEvent_t event; + IEC104_TcpServerStatus_t service; + IEC104_SlaveEvent_t record; + uint8_t request[sizeof(gi_request)]; + uint32_t consumed; + app_open(); event = gi_data(); + memcpy(request, gi_request, sizeof(request)); + for (i = 0; i < 2; ++i) { request[3] = (uint8_t)(0x60U + i); queue_request(request, sizeof(request)); } + app_poll(); CHECK(app.gi.pending_requests == 3 && app.gi.negative_submitted == 0); + if (wait) { congest(&last_request.connection); } + /* ACK已由连接层处理,回执尚未转入Slave FIFO,由Send_Response内的Consume_Records交付。 */ + CHECK(IEC104_Connection_Feed(&sut->connection, sut->generation, ack, sizeof(ack), fake.now, + &consumed) == IEC104_CONNECTION_OK && consumed == sizeof(ack)); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.pending_event_count == 0); + gi_process_only(); + CHECK(app.gi.events_pending && app.gi.negative_submitted == (wait ? 0U : 1U)); + CHECK(app.gi.pending_requests == (wait ? 3U : 2U) && app.gi.gi_pending && app.gi.cancelled == 0); + gi_process_only(); /* 即使再次调用,未读回执也阻止第二个应答提交。 */ + CHECK(app.gi.negative_submitted == (wait ? 0U : 1U)); + CHECK(IEC104_Slave_NextEvent(app.slave, &record) == IEC104_TCP_EVENT_READY); + CHECK(record.transport.type == IEC104_TCP_EVENT_RECORD && Example_Handle_Event(&app, &record)); + gi_empty(); fake.send_blocked = 0; + for (i = 0; i < 8; ++i) { gi_process(); } + CHECK(app.gi.negative_submitted == 2 && app.gi.pending_requests == 1); + gi_done(&event, 0, 0); app_close(); + } +} + +static void queue_busy(uint8_t oa) +{ + uint8_t request[sizeof(gi_request)]; + memcpy(request, gi_request, sizeof(request)); + request[3] = oa; + queue_request(request, sizeof(request)); +} + +static void request_busy(uint8_t oa) +{ + queue_busy(oa); + app_poll(); +} + +/* 本地决定/数据一旦接手,无需新网络事件即可续跑;查询本身不提交帧或消费事件。 */ +static void test_need_process_business(void) +{ + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + app_open(); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi) && !app.gi.need_process); + app_request(gi_request, sizeof(gi_request)); + CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.need_process && app.gi.pending_events == 0); + gi_process_only(); + CHECK(!app.gi.need_process && app.gi.pending_events == 1 && app.gi.submitted == 0); + event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(Example_Wait_Time(&app, 50) == 50 && !app.gi.need_process && app.gi.application_waiting); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.need_process && app.gi.submitted == 0); + gi_process_only(); + CHECK(!app.gi.need_process && app.gi.submitted == 1); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(Example_Wait_Time(&app, 50) == 50 && !app.gi.need_process); + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.need_process && app.gi.batch_index == 1); + CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.submitted == 2 && app.gi.pending_events == 0); + gi_process_only(); + CHECK(!app.gi.need_process && app.gi.batch_index == 1 && app.gi.objects_submitted == 1); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.need_process && app.gi.submitted == 2); + gi_process_only(); + CHECK(!app.gi.need_process && app.gi.pending_requests == 0 && app.gi.pending_events == 1); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_COMPLETED); + CHECK(Example_Wait_Time(&app, 50) == 50 && !app.gi.need_process); + app_close(); +} + +/* CONFIRM/批次/终止/短应答的真实发送背压均停止本地续跑,网络推进后显式重试恢复。 */ +static void test_need_process_send_wait(void) +{ + unsigned mode, i; + for (mode = 0; mode < 4; ++mode) { + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + unsigned long waits; + app_open(); event = mode == 0 ? gi_begin() : gi_data(); + congest(&last_request.connection); + if (mode == 0) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } else if (mode == 1) { + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + } else if (mode == 2) { + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + } else { + request_busy(0x60); + CHECK(!app.gi.need_process && app.gi.waits == 1); /* 入口已尝试并得到WAIT。 */ + } + if (mode == 0 || mode == 2) { CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.need_process); } + gi_process_only(); + CHECK(app.gi.waiting && !app.gi.need_process && app.gi.batch_index == 0); + waits = app.gi.waits; + for (i = 0; i < 3; ++i) { + CHECK(Example_Wait_Time(&app, 50) == 50 && !app.gi.need_process && app.gi.waits == waits); + } + fake.send_blocked = 0; gi_process(); + CHECK(!app.gi.need_process && app.gi.waits == waits); + if (mode == 1) { + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); + } + if (mode == 2) { + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_COMPLETED); + } else { + if (mode < 2) { event = gi_take(IEC104_INTERROGATION_NEED_DATA); } + gi_done(&event, mode == 1 ? 1U : 0U, mode == 1 ? 1U : 0U); + } + app_close(); + } +} + +/* 总召等应用不遮蔽后面的短应答;未读STOP/START始终先于本地续跑。 */ +static void test_need_process_reply_and_transport(void) +{ + IEC104_InterrogationEvent_t event; + app_open(); event = gi_data(); + queue_busy(0x60); queue_busy(0x61); app_poll(); + CHECK(app.gi.pending_requests == 3 && app.gi.application_waiting && app.gi.waiting); + CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.need_process); + gi_process_only(); + CHECK(app.gi.pending_requests == 1 && app.gi.negative_submitted == 2 && !app.gi.need_process); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.need_process); + push(stop_act, sizeof(stop_act)); push(start_act, sizeof(start_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.events_pending && !app.gi.need_process && app.gi.pending_requests == 1); + gi_process_only(); + CHECK(app.gi.events_pending && !app.gi.need_process && app.gi.gi_submitted == 0); + app_poll(); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(!app.gi.events_pending && !app.gi.need_process && app.gi.pending_requests == 0); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_STOPPED); + gi_empty(); app_close(); +} + +/* BEGIN和NEED_DATA均可长期留在应用手中,忙拒绝/错误应答不依赖应用先完成总召。 */ +static void test_reply_while_waiting(void) +{ + unsigned mode; + for (mode = 0U; mode < 2U; ++mode) { + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + uint8_t invalid[sizeof(gi_request)]; + app_open(); + event = mode == 0U ? gi_begin() : gi_data(); + request_busy(0x61); + memcpy(invalid, gi_request, sizeof(invalid)); invalid[4] = 2; + app_request(invalid, sizeof(invalid)); + CHECK(collect_asdus(frames, lengths) == mode + 2U); /* 安全的短应答已在入口提交,不等待Process。 */ + gi_process_only(); + CHECK(collect_asdus(frames, lengths) == mode + 2U); + CHECK(frames[mode][2] == 0xc7 && frames[mode][3] == 0x61); + invalid[2] = 0xee; /* TEST + P/N + UNKNOWN_COMMON_ADDRESS。 */ + CHECK(lengths[mode + 1U] == sizeof(invalid) && memcmp(frames[mode + 1U], invalid, sizeof(invalid)) == 0); + CHECK(app.gi.gi_pending && app.gi.pending_requests == 1 && app.gi.waiting && app.gi.application_waiting); + CHECK(app.gi.phase == (mode == 0U ? IEC104_INTERROGATION_START : IEC104_INTERROGATION_DATA)); + gi_process_only(); + CHECK(collect_asdus(frames, lengths) == mode + 2U); /* 等待应用时立即返回,不重发。 */ + gi_empty(); /* 原工作事件不被重复交付或替换。 */ + if (mode == 0U) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + } + gi_done(&event, 0, 0); + app_close(); + } +} + +/* 旧FINISHED占满最小业务FIFO时,新总召不能生成BEGIN,单帧应答仍可发送。 */ +static void test_reply_event_backpressure(void) +{ + IEC104_InterrogationEvent_t old, event; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); old = gi_begin(); + IEC104_Interrogation_Cancel(app.interrogation); + app_request(gi_request, sizeof(gi_request)); request_busy(0x61); + gi_process(); + CHECK(collect_asdus(frames, lengths) == 1 && frames[0][2] == 0xc7 && frames[0][3] == 0x61); + CHECK(app.gi.pending_requests == 1 && app.gi.pending_events == 1 && app.gi.waiting && !app.gi.application_waiting); + event = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(event.request_id == old.request_id && event.reason == IEC104_INTERROGATION_FINISH_CANCELLED); + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request_id != old.request_id); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &old, true) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(&event, 0, 0); app_close(); +} + +/* 持续释放并复用非队首槽:总召未完成也能接收后续请求,应答自身仍保持FIFO。 */ +static void test_reply_slot_reuse(void) +{ + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); event = gi_data(); + for (i = 0U; i < 3U; ++i) { queue_busy((uint8_t)(0x60U + i)); } + app_poll(); /* 同批通知未读空时先保存,后到应答不能越过已排队应答。 */ + CHECK(app.gi.pending_requests == 4); + for (i = 0U; i < 6U; ++i) { + congest_except_one_reply(&last_request.connection); + gi_process_only(); + fake.send_blocked = 0; + app_poll(); + CHECK(collect_asdus(frames, lengths) == i + 2U); + CHECK(lengths[i + 1U] == 10 && frames[i + 1U][2] == 0xc7 && frames[i + 1U][3] == 0x60U + i); + CHECK(app.gi.phase == IEC104_INTERROGATION_DATA && app.gi.application_waiting && app.gi.overloads == 0); + if (i < 3U) { request_busy((uint8_t)(0x63U + i)); CHECK(app.gi.pending_requests == 4); } + } + CHECK(app.gi.pending_requests == 1 && app.gi.batch_index == 0); + gi_done(&event, 0, 0); app_close(); +} + +/* 总召在旧应答之后建立:BEGIN出队及应用应答必须定位该请求,不能误操作索引队首。 */ +static void test_reply_non_head_events(void) +{ + IEC104_InterrogationEvent_t event; + uint8_t invalid[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + memcpy(invalid, gi_request, sizeof(invalid)); invalid[9] = 19; invalid[3] = 0x61; + queue_request(invalid, sizeof(invalid)); invalid[3] = 0x62; queue_request(invalid, sizeof(invalid)); + queue_request(gi_request, sizeof(gi_request)); app_poll(); + congest_except_one_reply(&last_request.connection); + gi_process_only(); /* 第一条应答、非队首总召BEGIN,再因第二条应答的WAIT返回。 */ + CHECK(app.gi.phase == IEC104_INTERROGATION_START && app.gi.pending_requests == 2); + event = gi_take(IEC104_INTERROGATION_BEGIN); + request_busy(0x63); /* 复用刚释放的槽,不能改变工作事件所指的总召。 */ + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + fake.send_blocked = 0; + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(collect_asdus(frames, lengths) == 4); + CHECK(frames[0][3] == 0x61 && frames[1][3] == 0x62 && frames[2][2] == 0x87 && frames[3][3] == 0x63); + gi_done(&event, 0, 0); app_close(); +} + +/* 一次调用在两类就绪任务之间轮转,自动提交当前批次并索取下一批;后续应答仍可穿插。 */ +static void test_reply_round_robin(void) +{ + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); event = gi_data(); + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + queue_busy(0x60); queue_busy(0x61); queue_busy(0x62); app_poll(); + gi_process_only(); + CHECK(app.gi.batch_index == 1 && app.gi.pending_requests == 1 && app.gi.application_waiting); + CHECK(collect_asdus(frames, lengths) == 5); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(event.batch_index == 1); + request_busy(0x63); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process_only(); CHECK(app.gi.batch_index == 2 && app.gi.application_waiting); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(event.batch_index == 2 && app.gi.pending_requests == 1); + gi_done(&event, 2, 2); + CHECK(collect_asdus(frames, lengths) == 8); + CHECK(frames[0][2] == 0x87 && frames[1][0] == 1 && frames[6][0] == 1 && frames[7][2] == 0x8a); + CHECK(frames[2][3] == 0x60 && frames[3][3] == 0x61 && frames[4][3] == 0x62 && frames[5][3] == 0x63); + app_close(); +} + +/* HIGH满时保留最早应答;后来的应答不能越过它,恢复后每条只提交一次。 */ +static void test_reply_send_wait(void) +{ + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); event = gi_data(); + congest(&last_request.connection); + request_busy(0x60); gi_process_only(); + CHECK(app.gi.pending_requests == 2 && app.gi.waits == 2); /* 入口和Process各尝试一次,均未接手。 */ + request_busy(0x61); gi_process_only(); + CHECK(app.gi.pending_requests == 3 && app.gi.waits == 3); + CHECK(collect_asdus(frames, lengths) == 1); + fake.send_blocked = 0; gi_process(); + CHECK(app.gi.pending_requests == 1 && app.gi.application_waiting); + CHECK(collect_asdus(frames, lengths) == 3 && frames[1][3] == 0x60 && frames[2][3] == 0x61); + gi_process(); CHECK(collect_asdus(frames, lengths) == 3); + gi_done(&event, 0, 0); app_close(); +} + +/* 新应答只能越过尚未提交的总召工作,不能越过发送层已经接手的数据帧。 */ +static void test_reply_preserves_submitted_data(void) +{ + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); event = gi_data(); fake.send_blocked = 1; + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process_only(); CHECK(app.gi.batch_index == 1); + request_busy(0x60); gi_process_only(); + CHECK(collect_asdus(frames, lengths) == 1); + fake.send_blocked = 0; gi_process(); + CHECK(collect_asdus(frames, lengths) == 3 && frames[1][0] == 1 && frames[2][2] == 0xc7); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(&event, 1, 1); app_close(); +} + +/* 短应答已完成一部分后,STOP/断线/超时仍须清掉其余任务,FINISHED恰好一次。 */ +static void test_reply_cleanup(void) +{ + IEC104_InterrogationConfig_t config; + unsigned mode; + IEC104_Interrogation_Config_Default(&config); config.data_timeout_ms = 20; + custom_config = &config; + for (mode = 0U; mode < 3U; ++mode) { + IEC104_InterrogationEvent_t event, finish; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); event = gi_data(); queue_busy(0x60); queue_busy(0x61); app_poll(); + congest_except_one_reply(&last_request.connection); gi_process_only(); + fake.send_blocked = 0; app_poll(); + CHECK(app.gi.pending_requests == 2 && collect_asdus(frames, lengths) == 2); + fake.now += 20; + if (mode == 0U) { + push(stop_act, sizeof(stop_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + } else if (mode == 1U) { + CHECK(IEC104_Slave_Close_Connection(app.slave, &last_request, NULL) == IEC104_TCP_OK); + } + gi_process_only(); + if (mode < 2U) { CHECK(app.gi.events_pending); } + app_poll(); + CHECK(collect_asdus(frames, lengths) == 2 && app.gi.pending_requests == 0); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == (mode == 0U ? IEC104_INTERROGATION_FINISH_STOPPED : + (mode == 1U ? IEC104_INTERROGATION_FINISH_DISCONNECTED : IEC104_INTERROGATION_FINISH_DATA_TIMEOUT))); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + gi_process(); gi_empty(); app_close(); + } + custom_config = NULL; +} + +/* 仅有单帧任务时,一次调用提交整个固定池中的就绪应答并回到空闲;再次调用无副作用。 */ +static void test_process_reply_only(void) +{ + uint8_t invalid[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + memcpy(invalid, gi_request, sizeof(invalid)); invalid[9] = 19; + for (i = 0U; i < 4U; ++i) { + invalid[3] = (uint8_t)(0x60U + i); + queue_request(invalid, sizeof(invalid)); + } + app_poll(); + CHECK(app.gi.pending_requests == 4 && app.gi.submitted == 0); + gi_process_only(); + CHECK(app.gi.pending_requests == 0 && app.gi.phase == IEC104_INTERROGATION_IDLE && !app.gi.waiting); + CHECK(collect_asdus(frames, lengths) == 4); + for (i = 0U; i < 4U; ++i) { + CHECK(lengths[i] == 10 && frames[i][2] == 0xc7 && frames[i][3] == 0x60U + i); + } + gi_process_only(); gi_empty(); + CHECK(app.gi.submitted == 4 && collect_asdus(frames, lengths) == 4); + app_close(); +} + +/* 短应答入口不得越过尚未交付的STOP/START;满槽时也不能用最后激活状态发送旧应答。 */ +static void test_immediate_reply_guard(void) +{ + unsigned mode, i; + for (mode = 0; mode < 2; ++mode) { + IEC104_InterrogationEvent_t event = {0}; + uint8_t request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + memcpy(request, gi_request, sizeof(request)); + if (mode == 0) { event = gi_data(); queue_busy(0x61); } + else { + for (i = 0; i < 4; ++i) { request[9] = (uint8_t)(20U + i); queue_request(request, sizeof(request)); } + app_poll(); /* 一个总召及三个REPLY,未读通知/旧REPLY阻止提前发送。 */ + request[9] = 24; queue_request(request, sizeof(request)); + } + push(stop_act, sizeof(stop_act)); push(start_act, sizeof(start_act)); + for (i = 0; i < 3; ++i) { CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); } + app_poll(); + CHECK(app.gi.active && app.gi.pending_requests == 0 && app.gi.negative_submitted == 0); + CHECK(collect_asdus(frames, lengths) == (mode == 0 ? 1U : 0U)); + if (mode == 0) { + IEC104_InterrogationEvent_t finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == IEC104_INTERROGATION_FINISH_STOPPED); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + } else { CHECK(app.gi.overloads == 1 && app.gi.dropped_requests == 1); } + gi_empty(); + memcpy(request, gi_request, sizeof(request)); request[4] = 2; + app_request(request, sizeof(request)); + CHECK(app.gi.negative_submitted == 1); /* 新启用的会话可立即应答。 */ + CHECK(memcmp(last_request.transport.data.i_frame.apdu + 6, request, sizeof(request)) == 0); + app_close(); + } +} + +/* 旧REPLY不能被新拒绝越过;发送也满时只丢新请求,原NEED_DATA保持有效。 */ +static void test_overload_drop_preserves_active(void) +{ + unsigned mode, i; + for (mode = 0; mode < 2; ++mode) { + IEC104_InterrogationEvent_t event; + IEC104_TcpServerStatus_t status; + uint8_t request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); event = gi_data(); + if (mode == 0) { + for (i = 0; i < 4; ++i) { queue_busy((uint8_t)(0x60U + i)); } + app_poll(); + } else { + memcpy(request, gi_request, sizeof(request)); + congest(&last_request.connection); + for (i = 21; i <= 23; ++i) { request[9] = (uint8_t)i; app_request(request, sizeof(request)); } + request[9] = 24; app_request(request, sizeof(request)); + } + CHECK(app.gi.pending_requests == 4 && app.gi.overloads == 1 && app.gi.dropped_requests == 1); + CHECK(app.gi.cancelled == 0 && app.gi.negative_submitted == 0 && app.gi.application_waiting); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.connected); + gi_empty(); + fake.send_blocked = 0; gi_process(); + if (mode == 0) { + CHECK(collect_asdus(frames, lengths) == 4 && app.gi.negative_submitted == 3); + for (i = 1; i < 4; ++i) { CHECK(frames[i][2] == 0xc7 && frames[i][3] == 0x5fU + i); } + } else { + app_poll(); + CHECK(app.gi.pending_requests == 1 && app.gi.negative_submitted == 3); + CHECK(collect_asdus(frames, lengths) == 4); + for (i = 1; i < 4; ++i) { CHECK(frames[i][2] == 0xc7 && frames[i][9] == 20U + i); } + } + gi_done(&event, 0, 0); /* 过载没有使原应用事件失效。 */ + CHECK(app.gi.dropped_requests == 1 && app.gi.overloads == 1); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.connected); + app_close(); + } +} + +/* 忙时不同组也拒绝且不自动重放;主站在结束后重新请求,每组保留自己的OA/TEST。 */ +static void test_qoi_sequential_retry(void) +{ + static const uint8_t qualifiers[] = {22, 20, 21}; + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_SinglePointInformation_t point; + uint8_t request[sizeof(gi_request)], bytes[249]; + const uint8_t *frames[32]; + size_t lengths[32]; + uint64_t previous_id = 0; + unsigned i; + app_open(); + for (i = 0; i < 3; ++i) { + IEC60870_AsduHeader_t header = {0}; + memcpy(request, gi_request, sizeof(request)); + request[9] = qualifiers[i]; request[3] = (uint8_t)(0x70U + i); request[2] = i == 1 ? 0x86 : 6; + app_request(request, sizeof(request)); + if (i == 0) { + unsigned later; + for (later = 1; later < 3; ++later) { + request[9] = qualifiers[later]; request[3] = (uint8_t)(0x70U + later); + request[2] = later == 1 ? 0x86 : 6; + app_request(request, sizeof(request)); + } + CHECK(app.gi.negative_submitted == 2 && app.gi.pending_requests == 1); + } + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.qualifier == qualifiers[i] && event.request.originator_address == 0x70U + i); + CHECK(event.request.is_test == (i == 1) && event.request_id > previous_id); + previous_id = event.request_id; + gi_empty(); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + header.cause_of_transmission = qualifiers[i]; header.common_address = 1; + header.originator_address = event.request.originator_address; header.is_test = event.request.is_test; + CHECK(IEC60870_Asdu_Create(&batch, IEC104_Slave_Get_Application_Layer_Parameters(app.slave), + bytes, sizeof(bytes), &header, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_SinglePointInformation_Create(&point, 1001U + i, 1, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&batch, &point.object) == IEC60870_ASDU_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(&event, 1, 1); + gi_process(); gi_empty(); CHECK(!app.gi.gi_pending && app.gi.pending_requests == 0); + } + CHECK(collect_asdus(frames, lengths) == 11 && app.gi.gi_submitted == 3 && !app.gi.gi_pending); + CHECK(frames[0][2] == 0xc7 && frames[0][9] == 20 && frames[0][3] == 0x71); + CHECK(frames[1][2] == 0x47 && frames[1][9] == 21 && frames[1][3] == 0x72); + for (i = 0; i < 3; ++i) { + unsigned first = 2U + i * 3U, test = i == 1 ? 0x80U : 0U; + CHECK(frames[first][2] == (test | 7U) && frames[first][9] == qualifiers[i]); + CHECK(frames[first + 1][0] == 1 && frames[first + 1][2] == (test | qualifiers[i])); + CHECK(frames[first + 2][2] == (test | 10U) && frames[first + 2][9] == qualifiers[i]); + CHECK(frames[first][3] == 0x70U + i && frames[first + 1][3] == 0x70U + i && frames[first + 2][3] == 0x70U + i); + } + app_close(); +} + +/* BEGIN生成前、交付前后、确认/数据/终止发送等待期间,均拒绝相同及不同QOI。 */ +static void test_busy_all_phases(void) +{ + unsigned mode, qoi; + for (mode = 0; mode < 8; ++mode) { + IEC104_InterrogationEvent_t event = {0}, finish; + IEC104_InterrogationPhase_t phase; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t request[sizeof(gi_request)], bytes[249]; + app_open(); app_request(gi_request, sizeof(gi_request)); + if (mode > 0) { gi_process(); } + if (mode > 1) { event = gi_take(IEC104_INTERROGATION_BEGIN); } + if (mode > 2) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } + if (mode > 3) { gi_process(); } + if (mode > 4) { event = gi_take(IEC104_INTERROGATION_NEED_DATA); } + if (mode == 6) { + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + } else if (mode == 7) { + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + } + if (mode == 3 || mode >= 6) { congest(&last_request.connection); gi_process_only(); } + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); phase = app.gi.phase; + memcpy(request, gi_request, sizeof(request)); request[3] = 0x71; request[2] = 6; + for (qoi = 20; qoi <= 21; ++qoi) { request[9] = (uint8_t)qoi; app_request(request, sizeof(request)); } + CHECK(app.gi.rejected == 2 && app.gi.gi_pending && app.gi.phase == phase); + CHECK(app.gi.pending_requests == ((mode == 3 || mode >= 6) ? 3U : 1U)); + IEC104_Interrogation_Cancel(app.interrogation); + if (mode > 1) { + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.request.qualifier == 20); + CHECK(finish.request.originator_address == 0x5a && finish.request.is_test); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_CANCELLED); + } + gi_empty(); app_close(); + } +} + +/* 被忙拒绝的QOI没有撤销对象;匹配原请求仍可立即撤销,之后主站可重新发起其他组。 */ +static void test_busy_cancel_match(void) +{ + IEC104_InterrogationEvent_t event, next, finish; + uint8_t request[sizeof(gi_request)]; + app_open(); event = gi_data(); memcpy(request, gi_request, sizeof(request)); + request[9] = 21; app_request(request, sizeof(request)); + request[2] = 0x88; app_request(request, sizeof(request)); + CHECK(app.gi.pending_requests == 1 && app.gi.application_waiting && app.gi.deactivated == 0); + CHECK(app.gi.negative_submitted == 2); gi_empty(); + request[9] = 20; app_request(request, sizeof(request)); + CHECK(!app.gi.gi_pending && app.gi.pending_requests == 0 && app.gi.deactivated == 1); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == IEC104_INTERROGATION_FINISH_DEACTIVATED); + gi_empty(); + app_request(request, sizeof(request)); CHECK(app.gi.negative_submitted == 3); + request[9] = 21; request[2] = 0x86; app_request(request, sizeof(request)); + gi_process(); next = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(next.request.qualifier == 21 && next.request_id != event.request_id); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &next, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); next = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&next, 0, 0); + CHECK(app.gi.gi_submitted == 1); app_close(); +} + +/* 清理当前总召和积压的忙应答,仅已交付BEGIN的原请求产生FINISHED。 */ +static void test_busy_stop_cleanup(void) +{ + unsigned mode, qoi; + for (mode = 0; mode < 3; ++mode) { + IEC104_InterrogationEvent_t event, finish; + uint8_t request[sizeof(gi_request)]; + app_open(); event = gi_data(); memcpy(request, gi_request, sizeof(request)); + for (qoi = 21; qoi <= 22; ++qoi) { request[9] = (uint8_t)qoi; queue_request(request, sizeof(request)); } + app_poll(); CHECK(app.gi.pending_requests == 3); + if (mode == 0) { push(stop_act, sizeof(stop_act)); app_poll(); } + else if (mode == 1) { + CHECK(IEC104_Slave_Close_Connection(app.slave, &last_request, NULL) == IEC104_TCP_OK); app_poll(); + } else { IEC104_Interrogation_Cancel(app.interrogation); } + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.pending_requests == 0 && !app.gi.gi_pending && app.gi.cancelled == 3); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == (mode == 0 ? IEC104_INTERROGATION_FINISH_STOPPED : + (mode == 1 ? IEC104_INTERROGATION_FINISH_DISCONNECTED : IEC104_INTERROGATION_FINISH_CANCELLED))); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + gi_empty(); app_close(); + } +} + +/* 后来的忙应答不会重置原总召的执行期限。 */ +static void test_busy_preserves_deadline(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t event, finish; + IEC104_TcpServerStatus_t status; + uint8_t request[sizeof(gi_request)]; + IEC104_Interrogation_Config_Default(&config); + config.data_timeout_ms = 500; config.total_timeout_ms = 100; + custom_config = &config; app_open(); event = gi_data(); fake.now += 99; + memcpy(request, gi_request, sizeof(request)); request[9] = 21; app_request(request, sizeof(request)); + request[9] = 22; app_request(request, sizeof(request)); + CHECK(app.gi.timeout_remaining_ms == 1 && app.gi.negative_submitted == 2 && app.gi.pending_requests == 1); + ++fake.now; gi_process_only(); + CHECK(app.gi.timeouts == 1 && app.gi.pending_requests == 0 && !app.gi.gi_pending); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT && finish.request_id == event.request_id); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && !status.connected); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + gi_empty(); app_close(); custom_config = NULL; +} + +/* 主站重新发起的下一个组获得完整执行期限,不继承此前被拒绝请求的等待时间。 */ +static void test_next_request_deadline(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t event; + uint8_t request[sizeof(gi_request)]; + IEC104_Interrogation_Config_Default(&config); + config.total_timeout_ms = 100; config.begin_timeout_ms = config.data_timeout_ms = 0; + custom_config = &config; app_open(); event = gi_data(); + memcpy(request, gi_request, sizeof(request)); request[9] = 21; app_request(request, sizeof(request)); + fake.now += 90; gi_done(&event, 0, 0); gi_process(); gi_empty(); + app_request(request, sizeof(request)); gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.qualifier == 21 && app.gi.timeout_remaining_ms == 100); + fake.now += 99; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&event, 0, 0); + CHECK(app.gi.timeouts == 0 && app.gi.gi_submitted == 2); + app_close(); custom_config = NULL; +} + +/* 历史FINISHED占据业务FIFO时,仅当前START等待空位;没有排队计时,但仍拒绝新总召。 */ +static void test_begin_capacity_wait(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t old, next, finish; + uint8_t request[sizeof(gi_request)]; + IEC104_Interrogation_Config_Default(&config); + config.begin_timeout_ms = 10; config.data_timeout_ms = 20; config.total_timeout_ms = 100; + custom_config = &config; app_open(); old = gi_begin(); IEC104_Interrogation_Cancel(app.interrogation); + memcpy(request, gi_request, sizeof(request)); request[9] = 21; app_request(request, sizeof(request)); + gi_process_only(); CHECK(app.gi.gi_pending && app.gi.waiting && !app.gi.timer_active && !app.gi.need_process); + request[9] = 22; app_request(request, sizeof(request)); + CHECK(app.gi.negative_submitted == 1 && app.gi.pending_requests == 1); + fake.now += 60001; gi_process_only(); + CHECK(app.gi.pending_requests == 1 && !app.gi.timer_active && app.gi.timeouts == 0); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == old.request_id && finish.reason == IEC104_INTERROGATION_FINISH_CANCELLED); + CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.need_process && app.gi.waiting && app.gi.pending_events == 0); + gi_process_only(); next = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(next.request.qualifier == 21 && next.request_id != old.request_id); + CHECK(app.gi.timer_active && app.gi.timeout_remaining_ms == 10); + IEC104_Interrogation_Cancel(app.interrogation); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request_id == next.request_id); + gi_empty(); app_close(); custom_config = NULL; +} + +/* BEGIN生成前也可匹配撤销;Slave中已到达的撤销先于业务推进,不交付多余BEGIN。 */ +static void test_cancel_before_begin(void) +{ + uint8_t request[sizeof(gi_request)]; + IEC104_InterrogationEvent_t event; + app_open(); app_request(gi_request, sizeof(gi_request)); + memcpy(request, gi_request, sizeof(request)); request[9] = 21; queue_request(request, sizeof(request)); + request[9] = 20; request[2] = 0x88; queue_request(request, sizeof(request)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + gi_process_only(); CHECK(app.gi.events_pending && app.gi.pending_events == 0); + app_poll(); gi_process(); gi_empty(); + CHECK(app.gi.deactivated == 1 && app.gi.negative_submitted == 1 && !app.gi.gi_pending); + CHECK(app.gi.pending_requests == 0 && app.gi.gi_submitted == 0); + request[9] = 21; request[2] = 0x86; app_request(request, sizeof(request)); + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); CHECK(event.request.qualifier == 21); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&event, 0, 0); + app_close(); +} + +static void test_event_empty_and_wait(void) +{ + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + event = gi_data(); + for (i = 0; i < 20; ++i) { + gi_process(); + CHECK(app.gi.application_waiting && app.gi.waiting && app.gi.batch_index == 0 && app.gi.submitted == 1); + gi_empty(); /* 已取出的NEED_DATA只交付一次,数据源未就绪不重复请求。 */ + } + gi_done(&event, 0, 0); + CHECK(collect_asdus(frames, lengths) == 2); + CHECK(frames[0][2] == 0x87 && frames[1][2] == 0x8a); + app_close(); +} + +static void test_event_copy_and_send_wait(void) +{ + IEC104_InterrogationEvent_t event, next; + IEC60870_Asdu_t asdu; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249], expected[10]; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + event = gi_data(); + single_batch(&asdu, ¶meters, bytes, 1); + memcpy(expected, bytes, sizeof(expected)); + congest(&last_request.connection); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &asdu) == IEC104_INTERROGATION_REPLY_WAIT); + for (i = 0; i < 3; ++i) { + gi_process(); + CHECK(app.gi.waiting && app.gi.data_send_waiting && !app.gi.application_waiting && app.gi.batch_index == 0); + gi_empty(); + } + fake.send_blocked = 0; gi_process(); + single_batch(&asdu, ¶meters, bytes, 1); + fake.send_blocked = 1; /* 成功接手但未写出时仍须复制原文,不能借用应用缓冲。 */ + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &asdu) == IEC104_INTERROGATION_REPLY_OK); + memset(bytes, 0xcc, sizeof(bytes)); + memset(¶meters, 0, sizeof(parameters)); + memset(&asdu, 0, sizeof(asdu)); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + fake.send_blocked = 0; + gi_process(); + next = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(next.batch_index == 1 && next.objects_submitted == 1); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + gi_done(&next, 1, 1); + CHECK(collect_asdus(frames, lengths) == 3 && lengths[1] == sizeof(expected)); + CHECK(memcmp(frames[1], expected, sizeof(expected)) == 0); + app_close(); +} + +static void test_event_batch_validation(void) +{ + unsigned mode; + for (mode = 0; mode < 12; ++mode) { + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t asdu; + IEC60870_AppLayerParameters_t parameters; + IEC104_TcpServerStatus_t status; + uint8_t bytes[249]; + app_open(); + event = gi_data(); + single_batch(&asdu, ¶meters, bytes, mode == 6 ? 5 : mode == 7 ? 0 : 1); + switch (mode) { + case 0: bytes[2] = 0x83; break; /* COT */ + case 1: bytes[2] = 0xd4; break; /* P/N */ + case 2: bytes[2] = 0x14; break; /* TEST */ + case 3: bytes[3] = 0; break; /* OA */ + case 4: bytes[4] = 2; break; /* CA */ + case 5: bytes[0] = 100; bytes[9] = 20; break; /* 命令类型不作为总召数据 */ + case 6: break; /* 超过配置的四点上限 */ + case 7: break; /* 空容器 */ + case 8: parameters.sizeOfIOA = 2; break; + case 9: bytes[1] = 2; break; /* 不完整载荷 */ + case 10: asdu.length = 250; break; + default: asdu.parameters = NULL; break; + } + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &asdu) == IEC104_INTERROGATION_REPLY_ERROR); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.connected); + single_batch(&asdu, ¶meters, bytes, 1); /* 同一事件修正后仍可提交。 */ + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &asdu) == IEC104_INTERROGATION_REPLY_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &asdu) == IEC104_INTERROGATION_REPLY_STALE); + gi_process(); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(&event, 1, 1); + app_close(); + } +} + +/* 不同类型、SQ及容量边界通过独立线缆字节断言,不依赖应用示例点表。 */ +static void test_event_mixed_types(void) +{ + static const uint8_t data[][14] = { + {3, 0x82, 0x94, 0x5a, 1, 0, 0x56, 0x34, 0x12, 0x81, 0x82}, + {13, 1, 0x94, 0x5a, 1, 0, 200, 0, 0, 0, 0, 0xc0, 0x3f, 0x10}, + {9, 1, 0x94, 0x5a, 1, 0, 0x2c, 1, 0, 0, 0xc0, 0} + }; + static const uint16_t sizes[] = {11, 14, 12}; + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + event = gi_data(); + for (i = 0; i < 3; ++i) { + IEC60870_Asdu_t asdu; + uint8_t bytes[14]; + memcpy(bytes, data[i], sizes[i]); + CHECK(IEC60870_Asdu_Create_From_Buffer(&asdu, + IEC104_Slave_Get_Application_Layer_Parameters(app.slave), bytes, sizes[i]) == IEC60870_ASDU_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &asdu) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); + } + gi_done(&event, 3, 4); + CHECK(collect_asdus(frames, lengths) == 5); + for (i = 0; i < 3; ++i) { + CHECK(lengths[i + 1] == sizes[i] && memcmp(frames[i + 1], data[i], sizes[i]) == 0); + } + app_close(); +} + +static void test_event_capacity_batches(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t event; + IEC60870_AppLayerParameters_t parameters; + IEC60870_Asdu_t asdu; + uint8_t bytes[249]; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned batch, index, counts[] = {60, 60, 2, 40, 40, 1}; + IEC104_Interrogation_Config_Default(&config); + custom_config = &config; + app_open(); + event = gi_data(); + parameters = *IEC104_Slave_Get_Application_Layer_Parameters(app.slave); + for (batch = 0; batch < 6; ++batch) { + IEC60870_AsduHeader_t header = {20, 0, 1, 0x5a, 1}; + CHECK(IEC60870_Asdu_Create(&asdu, ¶meters, bytes, sizeof(bytes), &header, 0) == IEC60870_ASDU_OK); + for (index = 0; index < counts[batch]; ++index) { + if (batch < 3) { + IEC60870_SinglePointInformation_t point; + CHECK(IEC60870_SinglePointInformation_Create(&point, 1001 + (batch * 60) + index, 1, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_OK); + } else { + IEC60870_MeasuredValueScaled_t point; + CHECK(IEC60870_MeasuredValueScaled_Create(&point, 2001 + ((batch - 3) * 40) + index, -125, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_OK); + } + } + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &asdu) == IEC104_INTERROGATION_REPLY_OK); + /* 大批次可能填满实际HIGH,循环Poll后再请求下一批。 */ + do { gi_process(); } while (app.gi.batch_index == batch); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); + } + gi_done(&event, 6, 203); + CHECK(collect_asdus(frames, lengths) == 8); + for (batch = 0; batch < 6; ++batch) { + CHECK(frames[batch + 1][1] == counts[batch] && lengths[batch + 1] == 6 + counts[batch] * (batch < 3 ? 4 : 6)); + } + app_close(); + custom_config = NULL; +} + +static void test_event_reject_and_fail(void) +{ + unsigned mode; + for (mode = 0; mode < 3; ++mode) { + IEC104_InterrogationEvent_t event, finish; + IEC104_TcpServerStatus_t service; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + event = mode == 2 ? gi_data() : gi_begin(); + if (mode == 0) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, false) == IEC104_INTERROGATION_REPLY_OK); + } else { + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + } + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == (mode == 0 ? IEC104_INTERROGATION_FINISH_REJECTED : IEC104_INTERROGATION_FINISH_SOURCE_ERROR)); + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + gi_process(); + app_poll(); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK); + if (mode < 2) { + CHECK(service.connected && collect_asdus(frames, lengths) == 1 && frames[0][2] == 0xc7); + } else { + CHECK(!service.connected && app.gi.gi_submitted == 0); + CHECK(collect_asdus(frames, lengths) == (mode == 2 ? 1U : 0U)); + } + gi_empty(); + app_close(); + } +} + +static void test_event_cancellation(void) +{ + static const IEC104_InterrogationFinishReason_t reasons[] = { + IEC104_INTERROGATION_FINISH_CANCELLED, IEC104_INTERROGATION_FINISH_STOPPED, + IEC104_INTERROGATION_FINISH_DISCONNECTED + }; + unsigned mode; + for (mode = 0; mode < sizeof(reasons) / sizeof(reasons[0]); ++mode) { + IEC104_InterrogationEvent_t event, finish; + app_open(); + event = gi_data(); + if (mode == 0) { IEC104_Interrogation_Cancel(app.interrogation); } + if (mode == 1) { push(stop_act, sizeof(stop_act)); app_poll(); } + if (mode == 2) { + CHECK(IEC104_Slave_Close_Connection(app.slave, &last_request, NULL) == IEC104_TCP_OK); + app_poll(); + } + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(!IEC104_Interrogation_Destroy(app.interrogation)); /* 终结事件尚未读取。 */ + IEC104_Interrogation_Cancel(app.interrogation); /* 重复取消不覆盖已排队的终结原因。 */ + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == reasons[mode] && finish.request_id == event.request_id); + gi_empty(); + app_close(); + } +} + +/* 最小两槽FIFO:旧FINISHED未读时暂停新BEGIN,为将来的终结保留容量。 */ +static void test_event_queue_and_destroy(void) +{ + IEC104_InterrogationEvent_t first, second, finish; + app_open(); + first = gi_begin(); + CHECK(!IEC104_Interrogation_Destroy(app.interrogation)); /* 正在等待应用决定。 */ + IEC104_Interrogation_Cancel(app.interrogation); + app_request(gi_request, sizeof(gi_request)); + gi_process(); + CHECK(app.gi.pending_events == 1 && app.gi.waiting && !app.gi.application_waiting); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == first.request_id); + gi_process(); + second = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(second.request_id != first.request_id); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &first, true) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &second, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); /* NEED_DATA已入队,取消必须移除它,只保留FINISHED。 */ + IEC104_Interrogation_Cancel(app.interrogation); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == second.request_id); + gi_empty(); + app_request(gi_request, sizeof(gi_request)); + gi_process(); /* BEGIN未取出便取消,不通知未建立资源的应用。 */ + IEC104_Interrogation_Cancel(app.interrogation); + gi_empty(); + app_close(); +} + +static void test_event_pending_stop_start(void) +{ + IEC104_InterrogationEvent_t event, finish, next, changed; + uint64_t generation; + app_open(); + event = gi_data(); + generation = event.request.generation; + push(stop_act, sizeof(stop_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + push(start_act, sizeof(start_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_WAIT); + CHECK(IEC104_Interrogation_Process(app.interrogation)); + app_poll(); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_STOPPED); + next = gi_begin(); + CHECK(next.request.generation == generation && next.request_id != event.request_id); + changed = next; ++changed.instance_id; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &changed, true) == IEC104_INTERROGATION_REPLY_STALE); + changed = next; ++changed.batch_index; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &changed, true) == IEC104_INTERROGATION_REPLY_STALE); + changed = next; changed.type = IEC104_INTERROGATION_FINISHED; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &changed, true) == IEC104_INTERROGATION_REPLY_STALE); + app_close(); +} + +static void test_event_instance_reuse(void) +{ + IEC104_InterrogationEvent_t old, current; + IEC104_Interrogation_t *slot; + app_open(); + old = gi_begin(); + slot = app.interrogation; + app_close(); + app_open(); + current = gi_begin(); + CHECK(app.interrogation == slot && current.request_id == old.request_id && current.instance_id != old.instance_id); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &old, true) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &old) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, ¤t, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); + current = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(¤t, 0, 0); + app_close(); +} + +/* 总召入口通过Slave统一拒绝错误副本/身份,不能改变已交付BEGIN的任务。 */ +static void test_slave_event_validation(void) +{ + IEC104_InterrogationEvent_t begin, data; + IEC104_InterrogationStatus_t before, after; + IEC104_SlaveEvent_t invalid; + unsigned mode; + app_open(); + begin = gi_begin(); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &before)); + invalid = last_request; + CHECK(IEC104_Interrogation_Handle_Event(app.interrogation, &invalid) == IEC104_INTERROGATION_EVENT_ERROR); + CHECK(IEC104_Slave_Copy_Event(&invalid, &last_request)); + ++invalid.transport.generation; + CHECK(IEC104_Interrogation_Handle_Event(app.interrogation, &invalid) == IEC104_INTERROGATION_EVENT_ERROR); + /* 仅注入公共视图失败;正常线上截短报文在Receiver中被挡住,不会产生这种I帧事件。 */ + for (mode = 0; mode < 4; ++mode) { + CHECK(IEC104_Slave_Copy_Event(&invalid, &last_request)); + invalid.asdu_result = IEC60870_ASDU_ERROR; + invalid.transport.data.i_frame.apdu[6] = mode == 0 ? 101 : 100; + if (mode == 2) { invalid.transport.data.i_frame.apdu_length = 6; } + if (mode == 3) { invalid.transport.data.i_frame.apdu_length = 7; } + CHECK(IEC104_Interrogation_Handle_Event(app.interrogation, &invalid) == IEC104_INTERROGATION_EVENT_IGNORED); + CHECK(Example_Handle_Event(&app, &invalid) && app.other_submitted == 0 && app.other_rejected == 0); + } + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &after)); + CHECK(after.pending_requests == before.pending_requests && after.gi_pending && + after.phase == before.phase && after.cancelled == before.cancelled && after.rejected == before.rejected); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &begin, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); + data = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(&data, 0, 0); + app_close(); +} + +/* 所有16组都由应用通过同一事件API接受;用独立请求/输出字节核对QOI和数据COT。 */ +static void test_group_interrogation(void) +{ + unsigned qoi; + for (qoi = 20; qoi <= 36; ++qoi) { + uint8_t request[sizeof(gi_request)], bytes[249]; + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + memcpy(request, gi_request, sizeof(request)); request[9] = (uint8_t)qoi; + app_request(request, sizeof(request)); + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.qualifier == qoi); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + single_batch(&batch, ¶meters, bytes, 1); + if (qoi != 20) { + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_ERROR); + } + CHECK(IEC60870_Asdu_Set_COT(&batch, (uint8_t)qoi) == IEC60870_ASDU_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(&event, 1, 1); + CHECK(collect_asdus(frames, lengths) == 3); + request[2] = 0x87; CHECK(lengths[0] == 10 && memcmp(frames[0], request, 10) == 0); + CHECK(lengths[1] == 10 && frames[1][0] == 1 && frames[1][2] == (0x80 | qoi)); + CHECK(frames[1][3] == 0x5a && frames[1][4] == 1 && frames[1][6] == 0xe9); + request[2] = 0x8a; CHECK(lengths[2] == 10 && memcmp(frames[2], request, 10) == 0); + app_close(); + } +} + +/* 点表属于应用:示例仅配置组1遥信和组2遥测,其余合法组交给应用否定确认。 */ +static void test_example_groups(void) +{ + static const uint8_t groups[] = {21, 22, 23, 36}; + unsigned i, f; + for (i = 0; i < sizeof(groups); ++i) { + uint8_t request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned count; + app_open(); + memcpy(request, gi_request, sizeof(request)); request[9] = groups[i]; + app_request(request, sizeof(request)); app_finish(); + count = collect_asdus(frames, lengths); + CHECK(count == (groups[i] <= 22 ? 4U : 1U)); + if (count == 1) { CHECK(frames[0][2] == 0xc7 && app.gi.gi_submitted == 0); } + else { + CHECK(frames[0][2] == 0x87 && frames[3][2] == 0x8a); + for (f = 1; f <= 2; ++f) { + const uint8_t *a = frames[f]; + unsigned first = groups[i] == 21 ? 1001 : 2001; + CHECK(a[0] == (groups[i] == 21 ? 1 : 11) && a[1] == 4 && a[2] == (0x80 | groups[i])); + CHECK((unsigned)(a[6] | (unsigned)a[7] << 8 | (unsigned)a[8] << 16) == first + (f - 1) * 4); + } + } + CHECK(frames[0][9] == groups[i] && app.session_request == 0); + app_close(); + } +} + +/* 撤销覆盖所有阶段,包含未读事件、已取事件、HIGH满、已提交部分数据和待发终止。 + * 已提交帧保留顺序;尚未提交批次及ACT_TERM不能越过肯定撤销确认。 */ +static void test_remote_deactivation_phases(void) +{ + unsigned phase; + for (phase = 0; phase <= 8; ++phase) { + uint8_t request[sizeof(gi_request)], bytes[249]; + IEC104_InterrogationEvent_t event = {0}, finish; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned prior = phase >= 4 ? 1U : 0U; + app_open(); + app_request(gi_request, sizeof(gi_request)); + if (phase >= 1) { gi_process(); } + if (phase >= 2) { event = gi_take(IEC104_INTERROGATION_BEGIN); } + if (phase >= 3) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } + if (phase >= 4) { gi_process(); } + if (phase >= 5) { event = gi_take(IEC104_INTERROGATION_NEED_DATA); } + if (phase == 6 || phase == 8) { + single_batch(&batch, ¶meters, bytes, 1); + if (phase == 6) { congest(&last_request.connection); } + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == + (phase == 6 ? IEC104_INTERROGATION_REPLY_WAIT : IEC104_INTERROGATION_REPLY_OK)); + } + if (phase == 7) { + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + } + if (phase == 8) { + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); ++prior; + } + if (phase == 3 || phase == 6 || phase == 7) { + if (phase != 6) { congest(&last_request.connection); } + gi_process(); CHECK(app.gi.waiting); + } + memcpy(request, gi_request, sizeof(request)); request[2] = 0x88; + app_request(request, sizeof(request)); + CHECK(!app.gi.gi_pending && app.gi.active && app.gi.deactivated == 1 && + app.gi.pending_requests == (fake.send_blocked ? 1U : 0U)); + if (phase >= 2) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, NULL) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_DEACTIVATED && finish.request_id == event.request_id); + CHECK(finish.batch_index == (phase == 8 ? 1U : 0U)); + } + gi_empty(); + if (fake.send_blocked) { + gi_process(); CHECK(app.gi.waiting && app.gi.phase == IEC104_INTERROGATION_REPLY); + fake.send_blocked = 0; + } + app_finish(); + CHECK(collect_asdus(frames, lengths) == prior + 1 && app.gi.gi_submitted == 0); + request[2] = 0x89; + CHECK(lengths[prior] == 10 && memcmp(frames[prior], request, 10) == 0); + app_request(gi_request, sizeof(gi_request)); app_finish(); + CHECK(app.gi.gi_submitted == 1 && collect_asdus(frames, lengths) == prior + 7); + app_close(); + } +} + +static void test_remote_deactivation_validation(void) +{ + static const struct { unsigned offset; uint8_t value, cot; } cases[] = { + {9, 21, 9}, {3, 0x6b, 9}, {2, 8, 9}, {9, 19, 9}, {9, 37, 9}, + {2, 0xc8, 9}, {4, 2, 46}, {6, 1, 47}, {2, 0x83, 45} + }; + unsigned i; + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + IEC104_InterrogationEvent_t event; + uint8_t request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); event = gi_data(); + memcpy(request, gi_request, sizeof(request)); request[2] = 0x88; + request[cases[i].offset] = cases[i].value; + app_request(request, sizeof(request)); + CHECK(app.gi.gi_pending && app.gi.deactivated == 0); + gi_done(&event, 0, 0); /* 不匹配撤销不终结当前请求,原任务仍能正常完成。 */ + CHECK(collect_asdus(frames, lengths) == 3); + request[2] = (request[2] & 0x80) | 0x40 | cases[i].cot; + CHECK(lengths[1] == 10 && memcmp(frames[1], request, 10) == 0 && frames[2][2] == 0x8a); + memcpy(request, gi_request, sizeof(request)); request[2] = 0x88; + app_request(request, sizeof(request)); app_finish(); /* 已完成,无可撤销任务。 */ + CHECK(collect_asdus(frames, lengths) == 4 && frames[3][2] == 0xc9); + app_close(); + } +} + +/* 满队列中撤销非队首的分组总召,不能因缺少额外槽而断线;重复撤销回否定COT=9。 */ +static void test_remote_deactivation_full_queue(void) +{ + uint8_t invalid[sizeof(gi_request)], request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + memcpy(invalid, gi_request, sizeof(invalid)); invalid[9] = 19; + memcpy(request, gi_request, sizeof(request)); request[9] = 36; + queue_request(invalid, sizeof(invalid)); queue_request(request, sizeof(request)); + queue_request(invalid, sizeof(invalid)); queue_request(invalid, sizeof(invalid)); app_poll(); + CHECK(app.gi.pending_requests == 4 && app.gi.gi_pending); + request[2] = 0x88; app_request(request, sizeof(request)); + CHECK(app.gi.active && app.gi.deactivated == 1 && app.gi.overloads == 0 && app.gi.pending_requests == 4); + app_finish(); gi_empty(); + CHECK(collect_asdus(frames, lengths) == 4); + CHECK(frames[0][2] == 0xc7 && frames[1][2] == 0xc7 && frames[2][2] == 0xc7 && + frames[3][2] == 0x89 && frames[3][9] == 36); + app_request(request, sizeof(request)); app_finish(); + CHECK(collect_asdus(frames, lengths) == 5 && frames[4][2] == 0xc9); + app_close(); +} + +static void check_timeout(IEC104_InterrogationFinishReason_t reason, unsigned delivered, uint64_t id, + bool keep_connection) +{ + IEC104_TcpServerStatus_t service; + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.timeouts == 1 && app.gi.last_finish_reason == reason && !app.gi.gi_pending && !app.gi.timer_active); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected == keep_connection); + if (delivered) { + IEC104_InterrogationEvent_t finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == reason && finish.request_id == id); + } + gi_process_only(); gi_empty(); /* 重复Process不再产生第二次FINISHED;独立验证业务时钟,不推进链路期限。 */ + CHECK(app.gi.timeouts == 1 && app.gi.gi_submitted == 0); +} + +static void test_begin_timeout(void) +{ + IEC104_InterrogationConfig_t config; + unsigned mode; + IEC104_Interrogation_Config_Default(&config); + CHECK(config.begin_timeout_ms == 15000 && config.data_timeout_ms == 15000 && config.total_timeout_ms == 0); + config.begin_timeout_ms = 100; config.total_timeout_ms = 1000; + custom_config = &config; + for (mode = 0; mode < 5; ++mode) { + IEC104_InterrogationEvent_t event = {0}; + app_open(); app_request(gi_request, sizeof(gi_request)); gi_process(); + if (mode != 0) { event = gi_take(IEC104_INTERROGATION_BEGIN); } + fake.now += 99; gi_process(); + CHECK(app.gi.timer_active && app.gi.timeout_remaining_ms == 1 && app.gi.timeouts == 0); + CHECK(Example_Wait_Time(&app, 50) == (mode == 0 ? 0U : 1U)); + ++fake.now; + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi) && app.gi.timeout_remaining_ms == 0 && app.gi.timeouts == 0); + CHECK(app.gi.need_process && Example_Wait_Time(&app, 50) == 0); + if (mode == 1 || mode == 4) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, mode == 1) == IEC104_INTERROGATION_REPLY_STALE); + } else if (mode == 2) { + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + } else { CHECK(IEC104_Interrogation_Process(app.interrogation)); } + check_timeout(IEC104_INTERROGATION_FINISH_BEGIN_TIMEOUT, mode != 0, event.request_id, true); + { + const uint8_t *frames[32]; + size_t lengths[32]; + uint8_t expected[sizeof(gi_request)]; + memcpy(expected, gi_request, sizeof(expected)); expected[2] = 0xc7; + app_poll(); + CHECK(collect_asdus(frames, lengths) == 1 && lengths[0] == sizeof(expected)); + CHECK(memcmp(frames[0], expected, sizeof(expected)) == 0); + CHECK(app.gi.negative_submitted == 1 && app.gi.rejected == 1 && app.gi.cancelled == 0); + } + app_close(); + } + custom_config = NULL; +} + +static void test_data_timeout(void) +{ + IEC104_InterrogationConfig_t config; + unsigned mode; + IEC104_Interrogation_Config_Default(&config); + config.begin_timeout_ms = 50; config.data_timeout_ms = 100; config.total_timeout_ms = 1000; + custom_config = &config; + for (mode = 0; mode < 5; ++mode) { + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + app_open(); + if (mode == 4) { /* NEED_DATA已排队但应用从未取出。 */ + event = gi_begin(); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); + } else { event = gi_data(); } + single_batch(&batch, ¶meters, bytes, 1); + fake.now += 99; gi_process(); + CHECK(app.gi.timeout_remaining_ms == 1 && app.gi.timeouts == 0); + ++fake.now; + if (mode == 0) { CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_STALE); } + else if (mode == 1) { CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); } + else if (mode == 2) { CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); } + else { CHECK(IEC104_Interrogation_Process(app.interrogation)); } + check_timeout(IEC104_INTERROGATION_FINISH_DATA_TIMEOUT, 1, event.request_id, false); + app_close(); + } + custom_config = NULL; +} + +/* 新一批重启取数期限,但不会延长整个请求的总期限。 */ +static void test_timeout_progress(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + IEC104_Interrogation_Config_Default(&config); + config.begin_timeout_ms = config.data_timeout_ms = 100; config.total_timeout_ms = 250; + custom_config = &config; app_open(); event = gi_begin(); + fake.now += 99; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + fake.now += 99; single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + fake.now += 49; gi_process(); + CHECK(app.gi.timeouts == 0 && app.gi.timeout_remaining_ms == 3 && app.gi.objects_submitted == 1); + fake.now += 3; + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + check_timeout(IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT, 1, event.request_id, false); + app_close(); custom_config = NULL; +} + +/* BEGIN前等业务FIFO空位不计期限;BEGIN生成后,确认/数据/终止的发送等待均受总期限约束。 */ +static void test_total_timeout_waits(void) +{ + IEC104_InterrogationConfig_t config; + unsigned mode; + IEC104_Interrogation_Config_Default(&config); + config.begin_timeout_ms = config.data_timeout_ms = 0; config.total_timeout_ms = 100; + custom_config = &config; + for (mode = 0; mode < 5; ++mode) { + IEC104_InterrogationEvent_t event = {0}; + uint8_t invalid[sizeof(gi_request)], bytes[249]; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + app_open(); + if (mode == 0) { + memcpy(invalid, gi_request, sizeof(invalid)); invalid[9] = 19; + app_request(invalid, sizeof(invalid)); app_request(gi_request, sizeof(gi_request)); + } else if (mode == 1) { + event = gi_begin(); IEC104_Interrogation_Cancel(app.interrogation); + app_request(gi_request, sizeof(gi_request)); + } else if (mode == 2) { + event = gi_begin(); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } else { + event = gi_data(); + if (mode == 3) { + single_batch(&batch, ¶meters, bytes, 1); + congest(&last_request.connection); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + } else { CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); } + } + if (mode != 1 && mode != 3) { congest(&last_request.connection); } + gi_process(); CHECK(app.gi.waiting); + CHECK(mode == 1 ? !app.gi.timer_active : (app.gi.timer_active && app.gi.timeout_remaining_ms == 100)); + fake.now += 100; CHECK(IEC104_Interrogation_Process(app.interrogation)); + if (mode == 1) { + IEC104_InterrogationEvent_t old = gi_take(IEC104_INTERROGATION_FINISHED); + IEC104_TcpServerStatus_t status; + CHECK(old.reason == IEC104_INTERROGATION_FINISH_CANCELLED && old.request_id == event.request_id); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.pending_requests == 1 && app.gi.timeouts == 0 && app.gi.negative_submitted == 0); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.connected); + gi_empty(); + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(app.gi.timer_active && app.gi.timeout_remaining_ms == 100); + IEC104_Interrogation_Cancel(app.interrogation); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request_id == event.request_id); + } else { + check_timeout(IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT, mode >= 2, event.request_id, mode < 3); + } + app_close(); + } + custom_config = NULL; +} + +static void test_timeout_disabled_and_large(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t event; + IEC104_Interrogation_Config_Default(&config); + config.begin_timeout_ms = config.data_timeout_ms = config.total_timeout_ms = 0; + custom_config = &config; app_open(); event = gi_begin(); + fake.now += 10000; gi_process(); + CHECK(!app.gi.timer_active && app.gi.timeouts == 0 && Example_Wait_Time(&app, 50) == 50); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&event, 0, 0); + app_close(); + config.total_timeout_ms = UINT32_MAX; + app_open(); event = gi_begin(); + fake.now += (uint64_t)UINT32_MAX - 1U; + /* 只推进业务时钟,不运行链路定时器,用于独立验证32位最大配置与64位时钟差。 */ + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi) && app.gi.timeout_remaining_ms == 1); + ++fake.now; + CHECK(IEC104_Interrogation_Process(app.interrogation)); + check_timeout(IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT, 1, event.request_id, true); + app_close(); custom_config = NULL; +} + +/* 到期时先消费已经排队的传输通知,STOP/主站撤销优先,应用应答先得到WAIT再得到STALE。 */ +static void test_timeout_transport_priority(void) +{ + IEC104_InterrogationConfig_t config; + unsigned mode; + IEC104_Interrogation_Config_Default(&config); + config.begin_timeout_ms = config.data_timeout_ms = config.total_timeout_ms = 100; + custom_config = &config; + for (mode = 0; mode < 2; ++mode) { + IEC104_InterrogationEvent_t event, finish; + uint8_t request[sizeof(gi_request)]; + app_open(); event = gi_data(); fake.now += 100; + if (mode == 0) { push(stop_act, sizeof(stop_act)); } + else { + memcpy(request, gi_request, sizeof(request)); request[2] = 0x88; + queue_request(request, sizeof(request)); + } + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Interrogation_Process(app.interrogation)); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_WAIT); + app_poll(); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == (mode == 0 ? IEC104_INTERROGATION_FINISH_STOPPED : IEC104_INTERROGATION_FINISH_DEACTIVATED)); + gi_process(); gi_empty(); CHECK(app.gi.timeouts == 0); + app_close(); + } + custom_config = NULL; +} + +/* 五种确认前拒绝共用原任务槽;真实HIGH和任务池同时满时,旧REPLY保持顺序,恢复后可重新总召。 */ +static void test_preconfirm_failure_reply_order(void) +{ + static const IEC104_InterrogationFinishReason_t reasons[] = { + IEC104_INTERROGATION_FINISH_REJECTED, IEC104_INTERROGATION_FINISH_SOURCE_ERROR, + IEC104_INTERROGATION_FINISH_BEGIN_TIMEOUT, IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT, + IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT + }; + unsigned mode; + for (mode = 0; mode < 10; ++mode) { + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t begin, finish, next; + IEC104_TcpServerStatus_t service; + const uint8_t *frames[32]; + size_t lengths[32]; + uint8_t request[sizeof(gi_request)], expected[sizeof(gi_request)]; + unsigned i, scenario = mode % 5; + IEC104_Interrogation_Config_Default(&config); + config.begin_timeout_ms = scenario == 2 ? 50 : 0; + config.total_timeout_ms = 100; + custom_config = &config; app_open(); + memcpy(request, gi_request, sizeof(request)); request[2] = 6; request[3] = 0x71; request[9] = 36; + if (mode >= 5) { request[4] = request[5] = 0xff; } + app_request(request, sizeof(request)); gi_process(); begin = gi_take(IEC104_INTERROGATION_BEGIN); + congest(&last_request.connection); + if (scenario == 4) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &begin, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process_only(); + CHECK(app.gi.phase == IEC104_INTERROGATION_CONFIRM && app.gi.waiting && app.gi.submitted == 0); + } + for (i = 0; i < 3; ++i) { request_busy((uint8_t)(0x60U + i)); } + CHECK(app.gi.pending_requests == 4 && app.gi.gi_pending && app.gi.negative_submitted == 0); + if (scenario == 0) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &begin, false) == IEC104_INTERROGATION_REPLY_OK); + } else if (scenario == 1) { + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &begin) == IEC104_INTERROGATION_REPLY_OK); + } else { fake.now += scenario == 2 ? 50U : 100U; } + gi_process_only(); + CHECK(app.gi.pending_requests == 4 && !app.gi.gi_pending && app.gi.phase == IEC104_INTERROGATION_REPLY); + CHECK(app.gi.active && app.gi.waiting && !app.gi.timer_active && !app.gi.application_waiting); + CHECK(app.gi.rejected == 4 && app.gi.overloads == 0 && app.gi.dropped_requests == 0 && app.gi.cancelled == 0); + CHECK(app.gi.timeouts == (scenario >= 2 ? 1U : 0U) && app.gi.last_finish_reason == reasons[scenario]); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == reasons[scenario] && finish.instance_id == begin.instance_id && finish.request_id == begin.request_id); + CHECK(finish.request.common_address == 1 && finish.request.qualifier == 36 && finish.request.originator_address == 0x71 && !finish.request.is_test); + CHECK(finish.batch_index == 0 && finish.objects_submitted == 0); + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &begin) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &begin, true) == IEC104_INTERROGATION_REPLY_STALE); + fake.now += 101; gi_process_only(); gi_empty(); + CHECK(app.gi.timeouts == (scenario >= 2 ? 1U : 0U) && app.gi.pending_requests == 4); + fake.send_blocked = 0; gi_process(); app_poll(); + CHECK(collect_asdus(frames, lengths) == 4); + for (i = 0; i < 3; ++i) { + memcpy(expected, gi_request, sizeof(expected)); expected[2] = 0xc7; expected[3] = (uint8_t)(0x60U + i); + CHECK(lengths[i] == sizeof(expected) && memcmp(frames[i], expected, sizeof(expected)) == 0); + } + memcpy(expected, request, sizeof(expected)); expected[2] = 0x47; expected[4] = 1; expected[5] = 0; + CHECK(lengths[3] == sizeof(expected) && memcmp(frames[3], expected, sizeof(expected)) == 0); + CHECK(app.gi.pending_requests == 0 && app.gi.negative_submitted == 4 && app.gi.gi_submitted == 0); + app_request(request, sizeof(request)); gi_process(); next = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(next.request_id != begin.request_id && next.request.generation == begin.request.generation); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &next, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); next = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&next, 0, 0); + CHECK(collect_asdus(frames, lengths) == 6 && frames[4][2] == 7 && frames[5][2] == 10); + CHECK(app.gi.gi_submitted == 1 && app.gi.negative_submitted == 4); + app_close(); custom_config = NULL; + } +} + +/* BEGIN失败和超时不得跳过Slave中未读的STOP、撤销或断线,原终结原因和单次FINISHED保持不变。 */ +static void test_preconfirm_failure_transport_priority(void) +{ + static const IEC104_InterrogationFinishReason_t reasons[] = { + IEC104_INTERROGATION_FINISH_STOPPED, IEC104_INTERROGATION_FINISH_DEACTIVATED, + IEC104_INTERROGATION_FINISH_DISCONNECTED + }; + IEC104_InterrogationConfig_t config; + unsigned mode; + IEC104_Interrogation_Config_Default(&config); + config.begin_timeout_ms = config.total_timeout_ms = 50; + custom_config = &config; + for (mode = 0; mode < 3; ++mode) { + IEC104_InterrogationEvent_t begin, finish; + uint8_t request[sizeof(gi_request)]; + app_open(); begin = gi_begin(); fake.now += 50; + if (mode == 0) { push(stop_act, sizeof(stop_act)); } + else if (mode == 1) { + memcpy(request, gi_request, sizeof(request)); request[2] = 0x88; + queue_request(request, sizeof(request)); + } else { CHECK(IEC104_Slave_Close_Connection(app.slave, &last_request, NULL) == IEC104_TCP_OK); } + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + gi_process_only(); CHECK(app.gi.events_pending && app.gi.timeouts == 0); + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &begin) == IEC104_INTERROGATION_REPLY_WAIT); + app_poll(); + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &begin) == IEC104_INTERROGATION_REPLY_STALE); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == reasons[mode] && finish.request_id == begin.request_id); + gi_process(); gi_empty(); + CHECK(app.gi.timeouts == 0 && app.gi.negative_submitted == 0 && app.gi.rejected == 0); + app_close(); + } + custom_config = NULL; +} + +/* 肯定ACT_CON已被发送层接手但TCP尚未写出时,已经进入确认后阶段,失败不能改发否定ACT_CON。 */ +static void test_confirm_submission_boundary(void) +{ + static const IEC104_InterrogationFinishReason_t reasons[] = { + IEC104_INTERROGATION_FINISH_SOURCE_ERROR, IEC104_INTERROGATION_FINISH_DATA_TIMEOUT, + IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT + }; + unsigned mode; + for (mode = 0; mode < 3; ++mode) { + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t event, finish; + IEC104_TcpServerStatus_t service; + const uint8_t *frames[32]; + size_t lengths[32]; + IEC104_Interrogation_Config_Default(&config); + config.data_timeout_ms = mode == 1 ? 50 : 100; + config.total_timeout_ms = mode == 2 ? 50 : 200; + custom_config = &config; app_open(); event = gi_begin(); + fake.send_blocked = 1; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(app.gi.phase == IEC104_INTERROGATION_DATA && app.gi.submitted == 1); + CHECK(collect_asdus(frames, lengths) == 0); + if (mode == 0) { + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + } else { fake.now += 50; gi_process_only(); } + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && !service.connected); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == reasons[mode] && finish.request_id == event.request_id); + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + gi_process_only(); gi_empty(); + CHECK(app.gi.negative_submitted == 0 && app.gi.rejected == 0 && app.gi.pending_requests == 0); + CHECK(app.gi.timeouts == (mode == 0 ? 0U : 1U) && app.gi.gi_submitted == 0); + CHECK(collect_asdus(frames, lengths) == 0); + app_close(); custom_config = NULL; + } +} + +/* 直接检查传给业务层的输入事件:立即应答、排队、撤销和丢弃都不能改写原请求字节。 */ +static void request_unchanged(const uint8_t *request, size_t size) +{ + IEC104_SlaveEvent_t event, copy; + IEC104_TcpEventResult_t result; + unsigned received = 0; + queue_request(request, size); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + while ((result = IEC104_Slave_NextEvent(app.slave, &event)) == IEC104_TCP_EVENT_READY) { + CHECK(Example_Handle_Event(&app, &event)); + if (event.transport.type == IEC104_TCP_EVENT_I_FRAME) { + const uint8_t *data; + uint16_t length; + CHECK(IEC60870_Asdu_Get_Data(&event.asdu, &data, &length) == IEC60870_ASDU_OK); + CHECK(length == size && memcmp(data, request, size) == 0); + CHECK(IEC104_Slave_Copy_Event(©, &event) && IEC104_Slave_Copy_Event(&last_request, ©)); + ++received; + } + } + CHECK(result == IEC104_TCP_EVENT_EMPTY && received == 1); +} + +/* BEGIN/NEED_DATA已取出时,错误激活/撤销不取消、刷新期限或使原业务事件失效。 */ +static void test_malformed_preserves_active(void) +{ + unsigned stage, deactivation, shape, broadcast; + for (stage = 0; stage < 2; ++stage) { + for (deactivation = 0; deactivation < 2; ++deactivation) { + for (shape = 0; shape < 4; ++shape) { + for (broadcast = 0; broadcast < 2; ++broadcast) { + IEC104_InterrogationEvent_t event; + IEC104_InterrogationStatus_t before; + IEC104_TcpServerStatus_t service; + uint8_t request[14] = {0}; + const uint8_t *frames[32]; + size_t lengths[32], length = shape == 0 ? 11U : shape == 3 ? 14U : 10U; + app_open(); event = stage == 0 ? gi_begin() : gi_data(); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &before) && before.timer_active); + fake.now += 7U; + memcpy(request, gi_request, sizeof(gi_request)); + request[1] = shape == 1 ? 0x81 : shape == 2 ? 0 : shape == 3 ? 2 : 1; + request[2] = deactivation ? 0x88 : 0x86; + if (broadcast) { request[4] = request[5] = 0xff; } + request_unchanged(request, length); gi_process_only(); gi_empty(); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); + CHECK(app.gi.phase == before.phase && app.gi.pending_requests == 1 && app.gi.gi_pending); + CHECK(app.gi.cancelled == 0 && app.gi.deactivated == 0 && app.gi.rejected == 1); + CHECK(app.gi.timer_active && app.gi.timeout_remaining_ms + 7U == before.timeout_remaining_ms); + CHECK(app.gi.negative_submitted == (shape < 2 ? 1U : 0U)); + if (stage == 0) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + } + gi_done(&event, 0, 0); + CHECK(app.gi.gi_submitted == 1 && collect_asdus(frames, lengths) == (shape < 2 ? 3U : 2U)); + if (shape < 2) { + unsigned index = stage == 0 ? 0U : 1U; + CHECK(lengths[index] == 10 && frames[index][1] == 1); + CHECK(frames[index][2] == (deactivation ? 0xc9 : 0xc7) && frames[index][4] == 1); + } + app_close(); + } + } + } + } +} + +/* 畸形单对象否定沿原HIGH/FIFO背压路径,满槽只丢新请求,原BEGIN仍能完成。 */ +static void test_malformed_backpressure(void) +{ + IEC104_InterrogationEvent_t event; + IEC104_TcpServerStatus_t service; + uint8_t request[11] = {0}; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); event = gi_begin(); congest(&last_request.connection); + memcpy(request, gi_request, sizeof(gi_request)); + for (i = 0; i < 4; ++i) { + request[3] = (uint8_t)(0x60U + i); request[2] = i & 1U ? 0x88 : 0x86; + request_unchanged(request, sizeof(request)); + } + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); + CHECK(app.gi.pending_requests == 4 && app.gi.dropped_requests == 1 && app.gi.overloads == 1); + CHECK(app.gi.rejected == 4 && app.gi.negative_submitted == 0 && app.gi.cancelled == 0); + gi_empty(); fake.send_blocked = 0; + for (i = 0; i < 8; ++i) { gi_process(); } + CHECK(app.gi.pending_requests == 1 && app.gi.negative_submitted == 3 && app.gi.waits != 0); + CHECK(collect_asdus(frames, lengths) == 3); + for (i = 0; i < 3; ++i) { + CHECK(lengths[i] == 10 && frames[i][1] == 1 && frames[i][3] == 0x60U + i); + CHECK(frames[i][2] == (i & 1U ? 0xc9 : 0xc7)); + } + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&event, 0, 0); + CHECK(app.gi.gi_submitted == 1 && app.gi.cancelled == 0); + app_close(); +} + +/* 广播全站及全部16组,非默认本站地址;确认/数据/终止WAIT及批次CA校验。 */ +static void test_broadcast_interrogation(void) +{ + IEC104_InterrogationConfig_t config; + unsigned qoi; + IEC104_Interrogation_Config_Default(&config); config.common_address = 0x1234; + custom_config = &config; + for (qoi = 20; qoi <= 36; ++qoi) { + IEC104_InterrogationEvent_t event, finish; + IEC60870_Asdu_t batch; + IEC60870_AsduHeader_t header = {(uint8_t)qoi, 0, (uint8_t)(qoi & 1U), 0x67, UINT16_MAX}; + IEC60870_SinglePointInformation_t point; + const uint8_t *frames[32]; + size_t lengths[32]; + uint8_t request[sizeof(gi_request)], expected[sizeof(gi_request)], bytes[249]; + bool wait = qoi == 20 || qoi == 36; + app_open(); + memcpy(request, gi_request, sizeof(request)); request[2] = (uint8_t)(6U | ((qoi & 1U) << 7)); + request[3] = 0x67; request[4] = request[5] = 0xff; request[9] = (uint8_t)qoi; + request_unchanged(request, sizeof(request)); gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == 0x1234 && event.request.qualifier == qoi); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + if (wait) { + congest(&last_request.connection); gi_process_only(); + CHECK(app.gi.phase == IEC104_INTERROGATION_CONFIRM && app.gi.waiting); + fake.send_blocked = 0; + } + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(event.request.common_address == 0x1234); + CHECK(IEC60870_Asdu_Create(&batch, IEC104_Slave_Get_Application_Layer_Parameters(app.slave), + bytes, sizeof(bytes), &header, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_SinglePointInformation_Create(&point, 0x10203, 1, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&batch, &point.object) == IEC60870_ASDU_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_ERROR); + CHECK(IEC60870_Asdu_Set_CA(&batch, 1) == IEC60870_ASDU_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_ERROR); + CHECK(IEC60870_Asdu_Set_CA(&batch, 0x1234) == IEC60870_ASDU_OK); + if (wait) { + congest(&last_request.connection); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + gi_process_only(); + CHECK(app.gi.phase == IEC104_INTERROGATION_DATA && app.gi.waiting && app.gi.batch_index == 0); + fake.send_blocked = 0; gi_process(); + } + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + if (wait) { + congest(&last_request.connection); gi_process_only(); + CHECK(app.gi.phase == IEC104_INTERROGATION_TERMINATE && app.gi.waiting); + fake.send_blocked = 0; + } + gi_process(); finish = gi_take(IEC104_INTERROGATION_FINISHED); app_poll(); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_COMPLETED && finish.request.common_address == 0x1234); + CHECK(finish.batch_index == 1 && finish.objects_submitted == 1 && app.gi.gi_submitted == 1); + CHECK(collect_asdus(frames, lengths) == 3); + memcpy(expected, request, sizeof(expected)); expected[4] = 0x34; expected[5] = 0x12; + expected[2] = (uint8_t)(7U | (request[2] & 0x80U)); + CHECK(lengths[0] == sizeof(expected) && memcmp(frames[0], expected, sizeof(expected)) == 0); + expected[2] = (uint8_t)(10U | (request[2] & 0x80U)); + CHECK(lengths[2] == sizeof(expected) && memcmp(frames[2], expected, sizeof(expected)) == 0); + CHECK(lengths[1] == 10 && frames[1][0] == 1 && frames[1][1] == 1 && frames[1][2] == (qoi | (request[2] & 0x80U))); + CHECK(frames[1][3] == 0x67 && frames[1][4] == 0x34 && frames[1][5] == 0x12); + CHECK(memcmp(frames[1] + 6, (const uint8_t[]){3, 2, 1, 1}, 4) == 0); + gi_empty(); app_close(); + } + custom_config = NULL; +} + +/* 广播错误回复的立即/排队路径都改为本站CA;普通错误站地址保持原值。 */ +static void test_broadcast_error_replies(void) +{ + static const struct { unsigned offset; uint8_t value, cot; } cases[] = { + {9, 19, 7}, {9, 37, 7}, {6, 1, 47}, {2, 0x83, 45}, {2, 0xc6, 7}, + {2, 0x88, 9}, {4, 2, 46}, {4, 0, 46} + }; + IEC104_InterrogationConfig_t config; + unsigned wait, i; + IEC104_Interrogation_Config_Default(&config); config.common_address = 0x1234; + custom_config = &config; + for (wait = 0; wait < 2; ++wait) { + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + uint8_t request[sizeof(gi_request)], expected[sizeof(gi_request)]; + IEC104_TcpServerStatus_t service; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + if (wait) { /* 先取得有效的连接接口,取消尚未交付BEGIN的请求再填满HIGH。 */ + memcpy(request, gi_request, sizeof(request)); request[4] = 0x34; request[5] = 0x12; + app_request(request, sizeof(request)); IEC104_Interrogation_Cancel(app.interrogation); + congest(&last_request.connection); + } + memcpy(request, gi_request, sizeof(request)); request[4] = request[5] = 0xff; + request[cases[i].offset] = cases[i].value; + if (i >= 6) { request[5] = 0; } + request_unchanged(request, sizeof(request)); + if (wait) { gi_process_only(); CHECK(app.gi.pending_requests == 1 && app.gi.waiting); } + fake.send_blocked = 0; gi_process(); app_poll(); + memcpy(expected, request, sizeof(expected)); expected[2] = (uint8_t)(0xc0U | cases[i].cot); + if (i < 6) { expected[4] = 0x34; expected[5] = 0x12; } + CHECK(collect_asdus(frames, lengths) == 1 && lengths[0] == sizeof(expected)); + CHECK(memcmp(frames[0], expected, sizeof(expected)) == 0 && app.gi.negative_submitted == 1); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); + gi_empty(); app_close(); + } + } + custom_config = NULL; +} + +/* 广播仍受单总召与满槽规则约束,忙应答使用本站CA,不能修改当前交互或断链。 */ +static void test_broadcast_busy(void) +{ + unsigned broadcast; + for (broadcast = 0; broadcast < 2; ++broadcast) { + IEC104_InterrogationEvent_t begin; + IEC104_TcpServerStatus_t service; + const uint8_t *frames[32]; + size_t lengths[32]; + uint8_t request[sizeof(gi_request)]; + unsigned i; + app_open(); memcpy(request, gi_request, sizeof(request)); + if (broadcast) { request[4] = request[5] = 0xff; } + request_unchanged(request, sizeof(request)); gi_process(); begin = gi_take(IEC104_INTERROGATION_BEGIN); + request[4] = request[5] = 0xff; request[3] = 0x60; request[9] = 21; + request_unchanged(request, sizeof(request)); + gi_process_only(); /* 多站应答保存遍历任务,再由Process有界提交。 */ + CHECK(app.gi.gi_pending && app.gi.negative_submitted == 1 && app.gi.pending_requests == 1); + congest(&last_request.connection); + for (i = 0; i < 4; ++i) { request[3] = (uint8_t)(0x61U + i); request_unchanged(request, sizeof(request)); } + CHECK(app.gi.gi_pending && app.gi.pending_requests == 4 && app.gi.dropped_requests == 1 && app.gi.overloads == 1); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &begin, false) == IEC104_INTERROGATION_REPLY_OK); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_REJECTED); + fake.send_blocked = 0; app_finish(); gi_empty(); + CHECK(collect_asdus(frames, lengths) == 5); + for (i = 0; i < 5; ++i) { + CHECK(lengths[i] == 10 && frames[i][2] == 0xc7 && frames[i][4] == 1 && frames[i][5] == 0); + CHECK(frames[i][3] == (i < 4 ? 0x60U + i : 0x5aU)); + } + app_close(); + } +} + +/* 广播/定向激活与撤销的四种组合,覆盖BEGIN前、确认后以及满任务池撤销。 */ +static void test_broadcast_deactivation(void) +{ + unsigned phase, combination; + for (phase = 0; phase < 3; ++phase) { + for (combination = 0; combination < 4; ++combination) { + uint8_t activation[sizeof(gi_request)], deactivation[sizeof(gi_request)]; + IEC104_InterrogationEvent_t event = {0}, finish; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned count, i; + app_open(); memcpy(activation, gi_request, sizeof(activation)); activation[9] = 36; + if (combination & 1U) { activation[4] = activation[5] = 0xff; } + request_unchanged(activation, sizeof(activation)); + if (phase != 0) { + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == 1); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + } + if (phase == 2) { + congest(&last_request.connection); + for (i = 0; i < 3; ++i) { request_busy((uint8_t)(0x60U + i)); } + CHECK(app.gi.pending_requests == 4); + } + memcpy(deactivation, activation, sizeof(deactivation)); deactivation[2] = 0x88; + deactivation[4] = combination & 2U ? 0xff : 1; deactivation[5] = combination & 2U ? 0xff : 0; + request_unchanged(deactivation, sizeof(deactivation)); + if (phase != 0) { + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_DEACTIVATED && finish.request_id == event.request_id); + CHECK(finish.request.common_address == 1 && finish.request.qualifier == 36); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + } + gi_empty(); fake.send_blocked = 0; app_finish(); + count = collect_asdus(frames, lengths); + CHECK(count == (phase == 0 ? 1U : phase == 1 ? 2U : 5U)); + CHECK(frames[count - 1U][2] == 0x89 && frames[count - 1U][4] == 1 && frames[count - 1U][5] == 0); + CHECK(frames[count - 1U][9] == 36 && app.gi.deactivated == 1); + deactivation[4] = deactivation[5] = 0xff; + request_unchanged(deactivation, sizeof(deactivation)); app_finish(); + CHECK(collect_asdus(frames, lengths) == count + 1U && frames[count][2] == 0xc9); + CHECK(frames[count][4] == 1 && frames[count][5] == 0); + gi_empty(); app_close(); + } + } +} + +/* 总召绑定必须属于本Slave;配置错误、重复创建和失败销毁均不能破坏站点引用。 */ +static void test_station_binding_lifetime(void) +{ + IEC104_Station_t station = {0}; + IEC104_InterrogationConfig_t config; + IEC104_Slave_t *slave = IEC104_Slave_Create(); + IEC104_Interrogation_t *gi; + IEC104_InterrogationEvent_t event; + CHECK(slave != NULL); + IEC104_Interrogation_Config_Default(&config); + CHECK(config.station == NULL); + config.station = &station; + config.common_address = 0U; /* 绑定站点时不用兼容地址,避免两处配置不同CA。 */ + CHECK(IEC104_Interrogation_Create(slave, &config) == NULL); + CHECK(IEC104_Slave_Add_Station(slave, &station, 0x1234U, &config) == IEC104_STATION_OK); + config.max_objects_per_asdu = 0U; + CHECK(IEC104_Interrogation_Create(slave, &config) == NULL); + CHECK(IEC104_Slave_Remove_Station(slave, &station) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(slave, &station, 0x1234U, &config) == IEC104_STATION_OK); + config.max_objects_per_asdu = 4U; + config.use_station_registry = true; + CHECK(IEC104_Interrogation_Create(slave, &config) == NULL); /* 两种站点选择方式不能同时配置。 */ + config.use_station_registry = false; + gi = IEC104_Interrogation_Create(slave, &config); + CHECK(gi != NULL && IEC104_Interrogation_Create(slave, &config) == NULL); + CHECK(IEC104_Slave_Remove_Station(slave, &station) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&station) && IEC104_Slave_Get_Station(slave, 0x1234U) == NULL); + CHECK(IEC104_Slave_Destroy(slave, NULL) == IEC104_TCP_INVALID_STATE); + CHECK(IEC104_Interrogation_Destroy(gi)); + CHECK(IEC104_Station_Is_Reusable(&station)); +#if IEC104_TCP_SERVER_CAPACITY >= 2U + { + IEC104_Slave_t *other = IEC104_Slave_Create(); + CHECK(other != NULL); + CHECK(IEC104_Slave_Add_Station(other, &station, 0x1234U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Interrogation_Create(slave, &config) == NULL); + CHECK(IEC104_Slave_Remove_Station(other, &station) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Destroy(other, NULL) == IEC104_TCP_OK); + } +#endif + CHECK(IEC104_Slave_Destroy(slave, NULL) == IEC104_TCP_OK); + + app_open(); + event = gi_begin(); + CHECK(!IEC104_Interrogation_Destroy(app.interrogation)); + CHECK(IEC104_Slave_Acquire_Station(app.slave, &app.station) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + IEC104_Interrogation_Cancel(app.interrogation); + CHECK(!IEC104_Interrogation_Destroy(app.interrogation)); /* 未读FINISHED仍禁止销毁总召实例。 */ + CHECK(!IEC104_Station_Is_Reusable(&app.station)); /* 应用持有到清理完成。 */ + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request_id == event.request_id); + CHECK(IEC104_Slave_Release_Station(app.slave, &app.station) == IEC104_STATION_OK); + app_close(); /* 退出必须能归还唯一引用并成功删除节点,避免重复创建/销毁泄漏。 */ +} + +/* 定向和广播回复均使用绑定站点的实际CA;注册其他站点不会自动展开新的总召。 */ +static void test_station_binding_wire(void) +{ + IEC104_InterrogationConfig_t config; + unsigned broadcast; + IEC104_Interrogation_Config_Default(&config); + config.common_address = 0x1234U; + config.station = &app.station; + config.max_objects_per_asdu = 4U; + custom_config = &config; + for (broadcast = 0U; broadcast < 2U; ++broadcast) { + IEC104_Station_t other = {0}; + uint8_t request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &other, 7U, &other) == IEC104_STATION_OK); + memcpy(request, gi_request, sizeof(request)); + request[4] = broadcast ? 0xffU : 0x34U; + request[5] = broadcast ? 0xffU : 0x12U; + app_request(request, sizeof(request)); + app_finish(); + CHECK(collect_asdus(frames, lengths) == 6U); + CHECK(frames[0][2] == 0x87U && frames[5][2] == 0x8aU); + for (i = 0U; i < 6U; ++i) { CHECK(frames[i][4] == 0x34U && frames[i][5] == 0x12U); } + request[4] = 7U; + request[5] = 0U; + app_request(request, sizeof(request)); + app_finish(); + CHECK(collect_asdus(frames, lengths) == 7U); + CHECK(frames[6][2] == 0xeeU && frames[6][4] == 7U && frames[6][5] == 0U); + CHECK(IEC104_Slave_Remove_Station(app.slave, &other) == IEC104_STATION_OK); + app_close(); + } + custom_config = NULL; +} + +static void registry_request(uint16_t ca, uint8_t cot) +{ + uint8_t request[sizeof(gi_request)]; + memcpy(request, gi_request, sizeof(request)); + request[4] = (uint8_t)ca; + request[5] = (uint8_t)(ca >> 8); + request[2] = (uint8_t)(cot | 0x80U); + app_request(request, sizeof(request)); +} + +/* 同一IOA属于不同CA时按站点context取数;提交数据必须匹配当前站,而不是默认CA。 */ +static void test_registry_routing(void) +{ + IEC104_Station_t second = {0}; + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + int station_value = 0; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 0x1234U, &station_value) == IEC104_STATION_OK); + registry_request(0x1234U, 6U); gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == 0x1234U && event.request.station_id == IEC104_Station_Get_Id(&second)); + CHECK(IEC104_Station_Get_Context(IEC104_Slave_Get_Station_By_Id(app.slave, event.request.station_id)) == &station_value); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + single_batch(&batch, ¶meters, bytes, 1U); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_ERROR); + CHECK(IEC60870_Asdu_Set_CA(&batch, 0x1234U) == IEC60870_ASDU_OK); + bytes[9] = (uint8_t)station_value; + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&event, 1U, 1U); + CHECK(collect_asdus(frames, lengths) == 3U); + CHECK(frames[0][2] == 0x87U && frames[1][2] == 0x94U && frames[2][2] == 0x8aU); + CHECK(frames[1][4] == 0x34U && frames[1][5] == 0x12U && frames[1][9] == 0U); + CHECK(frames[1][6] == 0xe9U && frames[1][7] == 3U); /* IOA仍由应用指定。 */ + registry_request(99U, 6U); app_finish(); + CHECK(collect_asdus(frames, lengths) == 4U && frames[3][2] == 0xeeU && frames[3][4] == 99U); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + app_close(); +} + +/* 13站超过任务池容量;每站空数据也有独立确认/终止、事件身份和计时,不丢站不扩队列。 */ +static void test_registry_broadcast_many(void) +{ + IEC104_Station_t stations[12] = {{0}}; + IEC104_InterrogationEvent_t event, finish; + uint64_t ids[13]; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + for (i = 0U; i < 12U; ++i) { + CHECK(IEC104_Slave_Add_Station(app.slave, &stations[i], (uint16_t)(i + 2U), &stations[i]) == IEC104_STATION_OK); + } + registry_request(IEC104_CA_BROADCAST, 6U); + for (i = 0U; i < 13U; ++i) { + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == i + 1U && (i == 0U || event.request_id > ids[i - 1U])); + CHECK(event.batch_index == 0U && event.objects_submitted == 0U && app.gi.pending_requests == 1U); + CHECK(event.request.originator_address == 0x5aU && event.request.is_test && event.request.qualifier == 20U); + ids[i] = event.request_id; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } + for (i = 0U; i < 13U; ++i) { + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(event.request_id == ids[i] && event.request.common_address == i + 1U); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + } + gi_process(); gi_process(); /* 终止有步数预算,但不需要先消费FINISHED才继续发送。 */ + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(!app.gi.gi_pending && app.gi.phase == IEC104_INTERROGATION_FINISHING && app.gi.pending_events == 13U); + CHECK(!app.gi.need_process && !IEC104_Interrogation_Destroy(app.interrogation)); + for (i = 0U; i < 13U; ++i) { + finish = gi_take(IEC104_INTERROGATION_FINISHED); /* 无需Process也能读完超过FIFO容量的终结记录。 */ + CHECK(finish.request_id == ids[i] && finish.reason == IEC104_INTERROGATION_FINISH_COMPLETED); + CHECK(finish.batch_index == 0U && finish.objects_submitted == 0U); + } + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + app_poll(); + CHECK(collect_asdus(frames, lengths) == 26U && app.gi.gi_submitted == 13U); + CHECK(!app.gi.gi_pending && app.gi.pending_requests == 0U); + for (i = 0U; i < 26U; ++i) { + CHECK(lengths[i] == 10U && frames[i][4] == i % 13U + 1U && frames[i][5] == 0U); + CHECK(frames[i][2] == (i < 13U ? 0x87U : 0x8aU)); + } + for (i = 0U; i < 12U; ++i) { CHECK(IEC104_Slave_Remove_Station(app.slave, &stations[i]) == IEC104_STATION_OK); } + app_close(); +} + +/* 删除当前站并以同CA注册新context,旧总召仍读原站;示例仅在取数阶段额外持有引用。 */ +static void test_registry_delete_current(void) +{ + IEC104_Station_t replacement = {0}; + uint64_t old_id; + app_open(); old_id = IEC104_Station_Get_Id(&app.station); + registry_request(1U, 6U); + CHECK(Example_Step(&app)); + CHECK(app.gi_station == NULL); /* BEGIN只判断接受,不占用取数游标。 */ + CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&app.station)); + CHECK(IEC104_Slave_Get_Station(app.slave, 1U) == NULL); + CHECK(IEC104_Slave_Add_Station(app.slave, &replacement, 1U, &replacement) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Get_Station_By_Id(app.slave, old_id) == &app.station); + CHECK(IEC104_Station_Get_Id(&replacement) != old_id); + app_finish(); check_gi(0U, 0x5aU); + CHECK(IEC104_Station_Is_Reusable(&app.station) && app.gi_station == NULL); + CHECK(IEC104_Slave_Get_Station_By_Id(app.slave, old_id) == NULL); + CHECK(IEC104_Slave_Remove_Station(app.slave, &replacement) == IEC104_STATION_OK); + registry_request(1U, 6U); app_finish(); /* 删除后新请求必须收到未知CA。 */ + CHECK(app.gi.negative_submitted == 1U); + app_close(); +} + +/* 当前站完成,尚未选入BEGIN的已删除站跳过,替代/新增站下一轮才参加。 */ +static void test_registry_broadcast_mutation(void) +{ + IEC104_Station_t second = {0}, third = {0}, replacement = {0}, added = {0}; + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, NULL) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &replacement, 2U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &added, 4U, NULL) == IEC104_STATION_OK); + for (i = 0U; i < 2U; ++i) { + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == (i == 0U ? 1U : 3U)); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } + for (i = 0U; i < 2U; ++i) { + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(event.request.common_address == (i == 0U ? 1U : 3U)); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + } + gi_process(); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request.common_address == 1U); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request.common_address == 3U); + app_poll(); CHECK(collect_asdus(frames, lengths) == 4U); + CHECK(frames[0][4] == 1U && frames[1][4] == 3U && frames[2][4] == 1U && frames[3][4] == 3U); + CHECK(frames[0][2] == 0x87U && frames[1][2] == 0x87U && frames[2][2] == 0x8aU && frames[3][2] == 0x8aU); + CHECK(IEC104_Station_Is_Reusable(&app.station) && IEC104_Station_Is_Reusable(&second)); + CHECK(IEC104_Slave_Remove_Station(app.slave, &replacement) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &added) == IEC104_STATION_OK); + app_close(); +} + +/* 广播忙/错误应答按各站实际CA展开;轮次有界且余下工作可见,不越过更早应答。 */ +static void test_registry_reply_fanout(void) +{ + IEC104_Station_t stations[12] = {{0}}; + IEC104_InterrogationEvent_t begin; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + for (i = 0U; i < 12U; ++i) { + CHECK(IEC104_Slave_Add_Station(app.slave, &stations[i], (uint16_t)(i + 2U), NULL) == IEC104_STATION_OK); + } + begin = gi_begin(); + registry_request(IEC104_CA_BROADCAST, 6U); + gi_process_only(); + CHECK(app.gi.negative_submitted == 8U && app.gi.need_process && app.gi.gi_pending); + gi_process(); app_poll(); + CHECK(collect_asdus(frames, lengths) == 13U && app.gi.pending_requests == 1U); + for (i = 0U; i < 13U; ++i) { CHECK(frames[i][2] == 0xc7U && frames[i][4] == i + 1U); } + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &begin, false) == IEC104_INTERROGATION_REPLY_OK); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request_id == begin.request_id); + app_finish(); + for (i = 0U; i < 12U; ++i) { CHECK(IEC104_Slave_Remove_Station(app.slave, &stations[i]) == IEC104_STATION_OK); } + CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); app_finish(); + CHECK(collect_asdus(frames, lengths) == 15U); + CHECK(frames[14][2] == 0xeeU && frames[14][4] == 0xffU && frames[14][5] == 0xffU); + app_close(); +} + +/* 定向撤销已确认但尚未取数的站;广播撤销剩余站,各自有肯定COT9且不发送ACT_TERM。 */ +static void test_registry_deactivation(void) +{ + IEC104_Station_t second = {0}, third = {0}; + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, NULL) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); + for (i = 0U; i < 3U; ++i) { + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == i + 1U); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&second)); + registry_request(2U, 8U); /* 删除后仍能撤销原活动事务;CA1的NEED_DATA继续有效。 */ + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request.common_address == 2U); + gi_process(); + registry_request(IEC104_CA_BROADCAST, 8U); + for (i = 0U; i < 2U; ++i) { + IEC104_InterrogationEvent_t finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_DEACTIVATED); + CHECK(finish.request.common_address == (i == 0U ? 1U : 3U)); + } + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + app_finish(); + CHECK(collect_asdus(frames, lengths) == 6U && app.gi.deactivated == 2U && app.gi.gi_submitted == 0U); + for (i = 0U; i < 3U; ++i) { CHECK(frames[i][2] == 0x87U && frames[i][4] == i + 1U); } + for (i = 3U; i < 6U; ++i) { CHECK(frames[i][2] == 0x89U); } + CHECK(frames[3][4] == 2U && frames[4][4] == 1U && frames[5][4] == 3U); + CHECK(IEC104_Station_Is_Reusable(&second)); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + app_close(); +} + +/* 首站BEGIN拒绝/超时后继续下一站;STOP取消整轮,快照/节点引用均归还。 */ +static void test_registry_reject_timeout_stop(void) +{ + unsigned mode; + for (mode = 0U; mode < 4U; ++mode) { + IEC104_Station_t second = {0}; + IEC104_InterrogationEvent_t event; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + if (mode == 0U) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, false) == IEC104_INTERROGATION_REPLY_OK); + registry_request(1U, 8U); /* 已拒绝的当前站无活动事务,不能改成肯定撤销并丢失否定确认。 */ + CHECK(app.gi.deactivated == 0U); + } else if (mode == 1U) { fake.now += 15000U; } + else if (mode == 2U) { push(stop_act, sizeof(stop_act)); app_poll(); } + else { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == 2U); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + fake.now += 15000U; + } + if (mode == 3U) { gi_process_only(); } /* 单独触发业务期限,避免同点到期的链路t1先收尾。 */ + else { gi_process(); } + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == (mode == 0U ? IEC104_INTERROGATION_FINISH_REJECTED : + mode == 1U ? IEC104_INTERROGATION_FINISH_BEGIN_TIMEOUT : + mode == 2U ? IEC104_INTERROGATION_FINISH_STOPPED : IEC104_INTERROGATION_FINISH_DATA_TIMEOUT)); + if (mode == 3U) { CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_DATA_TIMEOUT); } + if (mode < 2U) { + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == 2U); + } + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + IEC104_Interrogation_Cancel(app.interrogation); + if (mode < 2U) { CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request_id == event.request_id); } + CHECK(IEC104_Station_Is_Reusable(&app.station) && IEC104_Station_Is_Reusable(&second)); + app_close(); + } +} + +/* 在广播的确认/数据/终止遇到发送WAIT,不能提前换CA;恢复后仍逐站有序完成。 */ +static void test_registry_wait_each_phase(void) +{ + unsigned phase; + for (phase = 0U; phase < 3U; ++phase) { + IEC104_Station_t second = {0}; + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, &app) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + if (phase == 0U) { + congest(&last_request.connection); gi_process_only(); + CHECK(app.gi.waiting && !app.gi.need_process && app.gi.phase == IEC104_INTERROGATION_CONFIRM); + fake.send_blocked = 0; + } + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == 2U); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + if (phase == 1U) { + single_batch(&batch, ¶meters, bytes, 1U); congest(&last_request.connection); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + gi_process_only(); CHECK(app.gi.data_send_waiting && app.gi.pending_requests == 1U); + fake.send_blocked = 0; gi_process(); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + } + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(event.request.common_address == 2U); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + if (phase == 2U) { + congest(&last_request.connection); gi_process_only(); + CHECK(app.gi.waiting && app.gi.phase == IEC104_INTERROGATION_TERMINATE && app.gi.gi_submitted == 0U); + fake.send_blocked = 0; + } + gi_process(); CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request.common_address == 1U); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request.common_address == 2U); + app_finish(); CHECK(app.gi.gi_submitted == 2U); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + app_close(); + } +} + +static void registry_accept(unsigned count) +{ + unsigned i; + registry_request(IEC104_CA_BROADCAST, 6U); + for (i = 0U; i < count; ++i) { + IEC104_InterrogationEvent_t event; + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == i + 1U); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } + gi_process(); +} + +/* 广播畸形撤销只能逐站否定,不能改动已确认各站的活动总召/终结记录。 */ +static void test_registry_malformed(void) +{ + IEC104_Station_t second = {0}; + IEC104_InterrogationEvent_t first, next, finish; + IEC104_TcpServerStatus_t service; + uint8_t request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, &app) == IEC104_STATION_OK); + registry_accept(2); first = gi_take(IEC104_INTERROGATION_NEED_DATA); + memcpy(request, gi_request, sizeof(request)); request[1] = 0x81; request[2] = 0x88; + request[4] = request[5] = 0xff; + request_unchanged(request, sizeof(request)); gi_process_only(); gi_empty(); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); + CHECK(app.gi.pending_requests == 1 && app.gi.gi_pending && app.gi.negative_submitted == 2); + CHECK(app.gi.cancelled == 0 && app.gi.deactivated == 0 && app.gi.rejected == 1); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &first) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); next = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(next.request.common_address == 2 && next.request_id != first.request_id); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &next) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == first.request_id && finish.reason == IEC104_INTERROGATION_FINISH_COMPLETED); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == next.request_id && finish.reason == IEC104_INTERROGATION_FINISH_COMPLETED); + gi_empty(); app_poll(); + CHECK(collect_asdus(frames, lengths) == 6 && app.gi.gi_submitted == 2); + for (i = 0; i < 6; ++i) { + CHECK(lengths[i] == 10 && frames[i][1] == 1 && frames[i][4] == i % 2U + 1U); + CHECK(frames[i][2] == (i < 2 ? 0x87 : i < 4 ? 0xc9 : 0x8a)); + } + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + app_close(); +} + +/* 示例只需一套取数游标;线上三段顺序包含非空、多批、混合类型数据。 */ +static void test_registry_example_order(void) +{ + IEC104_Station_t second = {0}, third = {0}; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, &app) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, &app) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); app_finish(); + CHECK(collect_asdus(frames, lengths) == 18U && app.gi.gi_submitted == 3U); + for (i = 0U; i < 18U; ++i) { + unsigned ca = i < 3U ? i + 1U : i < 15U ? (i - 3U) / 4U + 1U : i - 14U; + CHECK(frames[i][4] == ca && frames[i][5] == 0U); + CHECK(frames[i][2] == (i < 3U ? 0x87U : i < 15U ? 0x94U : 0x8aU)); + if (i >= 3U && i < 15U) { CHECK((frames[i][1] & 0x7fU) == 4U); } + } + CHECK(app.gi_station == NULL && app.session_request == 0U); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + app_close(); +} + +/* 广播容量不足必须在任何肯定确认之前整体拒绝;单站和定向总召不需要状态区。 */ +static void test_registry_capacity(void) +{ + size_t capacity; + for (capacity = 0U; capacity < 3U; ++capacity) { + IEC104_InterrogationConfig_t config; + IEC104_Station_t second = {0}, third = {0}; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + IEC104_Interrogation_Config_Default(&config); + config.use_station_registry = true; + config.broadcast_states = capacity != 0U ? broadcast_states : NULL; + config.broadcast_capacity = capacity; + custom_config = &config; app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, &app) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, &app) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); app_finish(); gi_empty(); + CHECK(collect_asdus(frames, lengths) == 3U && app.gi.gi_submitted == 0U && app.gi.overloads == 1U); + for (i = 0U; i < 3U; ++i) { CHECK(frames[i][2] == 0xc7U && frames[i][4] == i + 1U); } + registry_request(2U, 6U); app_finish(); CHECK(app.gi.gi_submitted == 1U); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); app_finish(); CHECK(app.gi.gi_submitted == 2U); + app_close(); custom_config = NULL; + } +} + +static void test_registry_workspace_contract(void) +{ + IEC104_InterrogationConfig_t config; + app_open(); + CHECK(IEC104_Interrogation_Destroy(app.interrogation)); + IEC104_Interrogation_Config_Default(&config); + CHECK(config.broadcast_states == NULL && config.broadcast_capacity == 0U); + config.broadcast_capacity = 1U; + CHECK(IEC104_Interrogation_Create(app.slave, &config) == NULL); + config.broadcast_states = broadcast_states; config.broadcast_capacity = 0U; + CHECK(IEC104_Interrogation_Create(app.slave, &config) == NULL); + config.broadcast_capacity = SIZE_MAX / sizeof(*broadcast_states) + 1U; + CHECK(IEC104_Interrogation_Create(app.slave, &config) == NULL); + config.broadcast_capacity = 8U; + app.interrogation = IEC104_Interrogation_Create(app.slave, &config); + CHECK(app.interrogation != NULL); +#if IEC104_TCP_SERVER_CAPACITY >= 2 + { + IEC104_Slave_t *other = IEC104_Slave_Create(); + IEC104_Interrogation_t *other_gi; + CHECK(other != NULL); + CHECK(IEC104_Interrogation_Create(other, &config) == NULL); /* 完全重叠。 */ + config.broadcast_states = broadcast_states + 4U; + CHECK(IEC104_Interrogation_Create(other, &config) == NULL); /* 部分重叠。 */ + config.broadcast_states = broadcast_states + 8U; + other_gi = IEC104_Interrogation_Create(other, &config); + CHECK(other_gi != NULL && IEC104_Interrogation_Destroy(other_gi)); + CHECK(IEC104_Slave_Destroy(other, NULL) == IEC104_TCP_OK); + } +#endif + app_close(); +} + +/* 已确认的未来站退役后,仍用原注册身份取值并发送终止,不转向同CA的新节点。 */ +static void test_registry_confirmed_deleted(void) +{ + IEC104_Station_t second = {0}, third = {0}, replacement = {0}; + IEC104_InterrogationEvent_t event; + uint64_t original; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, &app) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, &app) == IEC104_STATION_OK); + original = IEC104_Station_Get_Id(&second); + registry_accept(3U); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &replacement, 2U, &replacement) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&second)); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(event.request.common_address == 2U && event.request.station_id == original); + app.business_event = event; app.business_pending = true; + app_finish(); + CHECK(app.gi.gi_submitted == 3U && app.gi.cancelled == 0U && IEC104_Station_Is_Reusable(&second)); + CHECK(IEC104_Slave_Get_Station_By_Id(app.slave, original) == NULL); + CHECK(IEC104_Slave_Remove_Station(app.slave, &replacement) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + app_close(); +} + +/* 超过FIFO容量的BEGIN都必须有且仅有一次FINISHED;取消后无需Process即可逐个清理。 */ +static void test_registry_finish_delivery(void) +{ + static const IEC104_InterrogationFinishReason_t reasons[] = { + IEC104_INTERROGATION_FINISH_CANCELLED, IEC104_INTERROGATION_FINISH_STOPPED, + IEC104_INTERROGATION_FINISH_DISCONNECTED, IEC104_INTERROGATION_FINISH_SOURCE_ERROR + }; + unsigned mode; + for (mode = 0U; mode < 4U; ++mode) { + IEC104_Station_t stations[12] = {{0}}; + IEC104_InterrogationEvent_t event, finish; + uint64_t ids[13]; + unsigned i; + app_open(); + for (i = 0U; i < 12U; ++i) { + CHECK(IEC104_Slave_Add_Station(app.slave, &stations[i], (uint16_t)(i + 2U), &app) == IEC104_STATION_OK); + } + registry_request(IEC104_CA_BROADCAST, 6U); + for (i = 0U; i < 13U; ++i) { + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); ids[i] = event.request_id; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + if (mode == 0U) { IEC104_Interrogation_Cancel(app.interrogation); } + if (mode == 1U) { push(stop_act, sizeof(stop_act)); app_poll(); } + if (mode == 2U) { + CHECK(IEC104_Slave_Close_Connection(app.slave, &last_request, NULL) == IEC104_TCP_OK); + app_poll(); + } + if (mode == 3U) { CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); } + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + IEC104_Interrogation_Cancel(app.interrogation); /* 不覆盖已有原因。 */ + if (mode == 0U) { + registry_request(1U, 6U); /* 状态区尚未清理,新总召忙拒绝,不能覆盖旧FINISHED。 */ + CHECK(app.gi.negative_submitted == 1U); + } + CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + for (i = 0U; i < 12U; ++i) { + CHECK(IEC104_Slave_Remove_Station(app.slave, &stations[i]) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&stations[i])); + } + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.pending_events == 13U && !app.gi.gi_pending && !app.gi.need_process); + CHECK(!IEC104_Interrogation_Destroy(app.interrogation)); + for (i = 0U; i < 13U; ++i) { + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == ids[i] && finish.reason == reasons[mode]); + } + gi_empty(); + CHECK(IEC104_Station_Is_Reusable(&app.station)); + for (i = 0U; i < 12U; ++i) { CHECK(IEC104_Station_Is_Reusable(&stations[i])); } + app_close(); + } +} + +/* 当前取数站、未来已确认站、已取完数据等终止站,都支持按CA撤销而不损害其他站。 */ +static void test_registry_directed_deactivation(void) +{ + unsigned mode; + for (mode = 0U; mode < 3U; ++mode) { + IEC104_Station_t second = {0}, third = {0}; + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned count, i; + uint16_t target = mode == 1U ? 2U : 1U; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, &app) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, &app) == IEC104_STATION_OK); + registry_accept(3U); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + if (mode == 2U) { + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + } + registry_request(target, 8U); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request.common_address == target); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == + (mode == 0U ? IEC104_INTERROGATION_REPLY_STALE : IEC104_INTERROGATION_REPLY_OK)); + app_finish(); CHECK(app.gi.gi_submitted == 2U && app.gi.deactivated == 1U); + count = collect_asdus(frames, lengths); + for (i = 0U; i < count; ++i) { + CHECK(frames[i][2] != 0x8aU || frames[i][4] != target); + } + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + app_close(); + } +} + +/* 满槽广播撤销复用原任务,按先前应答顺序提交每站COT9;不额外占用事件预留或任务槽。 */ +static void test_registry_deactivation_full(void) +{ + enum { task_capacity = 4 }; /* 本测试构建采用模块默认任务容量。 */ + IEC104_Station_t second = {0}, third = {0}; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, NULL) == IEC104_STATION_OK); + registry_accept(3U); (void)gi_take(IEC104_INTERROGATION_NEED_DATA); + congest(&last_request.connection); + for (i = 0U; i < task_capacity - 1U; ++i) { registry_request(1U, 6U); } + CHECK(app.gi.pending_requests == task_capacity); + registry_request(IEC104_CA_BROADCAST, 8U); + CHECK(app.gi.deactivated == 1U && app.gi.pending_requests == task_capacity); + registry_request(IEC104_CA_BROADCAST, 8U); /* 不覆盖首条撤销;此时也没有错误应答空槽。 */ + CHECK(app.gi.deactivated == 1U && app.gi.dropped_requests == 1U); + for (i = 0U; i < 3U; ++i) { CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request.common_address == i + 1U); } + gi_process_only(); CHECK(app.gi.waiting && !app.gi.need_process); + fake.send_blocked = 0; app_finish(); + CHECK(collect_asdus(frames, lengths) == 6U + task_capacity - 1U); + for (i = 3U; i < 3U + task_capacity - 1U; ++i) { CHECK(frames[i][2] == 0xc7U); } + for (; i < 6U + task_capacity - 1U; ++i) { CHECK(frames[i][2] == 0x89U); } + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + app_close(); +} + +/* 另一个站的撤销等待发送时,当前BEGIN被拒绝的ACT_CON不能被撤销应答覆盖。 */ +static void test_registry_reject_during_deactivation(void) +{ + unsigned mode; + for (mode = 0U; mode < 2U; ++mode) { + IEC104_Station_t second = {0}, third = {0}; + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, &app) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, &app) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + if (mode == 0U) { registry_request(1U, 8U); } + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, false) == IEC104_INTERROGATION_REPLY_OK); + if (mode == 1U) { registry_request(IEC104_CA_BROADCAST, 8U); } + (void)gi_take(IEC104_INTERROGATION_FINISHED); (void)gi_take(IEC104_INTERROGATION_FINISHED); + app_finish(); + CHECK(collect_asdus(frames, lengths) == (mode == 0U ? 9U : 3U)); + CHECK(frames[0][2] == 0x87U && frames[0][4] == 1U); + CHECK(frames[1][2] == 0xc7U && frames[1][4] == 2U); + CHECK(frames[2][2] == 0x89U && frames[2][4] == 1U); + CHECK(app.gi.gi_submitted == (mode == 0U ? 1U : 0U)); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + app_close(); + } +} + +/* 显式启用总期限时,已确认但不在当前取数位置的站仍受期限约束,不能遗漏。 */ +static void test_registry_total_deadline(void) +{ + unsigned mode; + for (mode = 0U; mode < 2U; ++mode) { + IEC104_InterrogationConfig_t config; + IEC104_Station_t second = {0}; + IEC104_InterrogationEvent_t event; + IEC104_Interrogation_Config_Default(&config); + config.use_station_registry = true; + config.broadcast_states = broadcast_states; config.broadcast_capacity = 16U; + config.total_timeout_ms = 100U; + custom_config = &config; app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + fake.now += 50U; gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + if (mode == 1U) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + } + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.timer_active && app.gi.timeout_remaining_ms == 50U); + fake.now += 50U; gi_process_only(); + CHECK(!app.gi.active && app.gi.timeouts == 1U && app.gi.gi_submitted == 0U); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT); + gi_empty(); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + app_close(); custom_config = NULL; + } +} + +/* 当前站BEGIN超时不能误关尚未到总期限的已确认站;同时到期时总期限优先。 */ +static void test_registry_timeout_priority(void) +{ + unsigned mode; + for (mode = 0U; mode < 3U; ++mode) { + IEC104_InterrogationConfig_t config; + IEC104_Station_t second = {0}; + IEC104_InterrogationEvent_t event, finish; + IEC104_TcpServerStatus_t service; + uint64_t first_request_id; + IEC104_Interrogation_Config_Default(&config); + config.use_station_registry = true; + config.broadcast_states = broadcast_states; config.broadcast_capacity = 16U; + config.total_timeout_ms = mode == 0U ? 0U : 100U; + config.begin_timeout_ms = mode == 2U ? 50U : 25U; + custom_config = &config; app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + first_request_id = event.request_id; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + fake.now += 50U; gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == 2U); + fake.now += config.begin_timeout_ms - 1U; gi_process_only(); + CHECK(app.gi.active && app.gi.timeouts == 0U && app.gi.timer_active && app.gi.timeout_remaining_ms == 1U); + ++fake.now; + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.active && app.gi.timeouts == 0U && app.gi.timer_active && app.gi.timeout_remaining_ms == 0U); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi) && app.gi.timeouts == 1U); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK); + if (mode == 2U) { + CHECK(!app.gi.active && !service.connected && app.gi.gi_submitted == 0U); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT); + } else { + CHECK(app.gi.active && service.connected); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == IEC104_INTERROGATION_FINISH_BEGIN_TIMEOUT); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(event.request_id == first_request_id && event.request.common_address == 1U); + CHECK(app.gi.timer_active && app.gi.timeout_remaining_ms == (mode == 0U ? config.data_timeout_ms : 25U)); + gi_done(&event, 0U, 0U); + } + gi_empty(); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + app_close(); custom_config = NULL; + } +} + +/* 计数量复用可控传输夹具,但使用独立的引擎实例和广播状态区。 */ +static const uint8_t ci_request[] = {101, 1, 6, 0x5a, 1, 0, 0, 0, 0, 5}; + +static void ci_open(void) { counter_enabled = true; app_open(); } +static void ci_close(void) { app_close(); counter_enabled = false; counter_custom_config = NULL; } + +/* 先推进并读空Slave通知,再推进计数量;测试自行取出业务事件并决定应答时机。 */ +static void ci_process(void) +{ + app_poll(); + CHECK(IEC104_Interrogation_Process(app.counter)); + CHECK(IEC104_Interrogation_Get_Status(app.counter, &app.ci)); +} + +static IEC104_InterrogationEvent_t ci_take(IEC104_InterrogationEventType_t type) +{ + IEC104_InterrogationEvent_t event; + CHECK(IEC104_Interrogation_NextEvent(app.counter, &event) == IEC104_INTERROGATION_NEXT_READY); + CHECK(event.type == type); + return event; +} + +static void ci_empty(void) +{ + IEC104_InterrogationEvent_t event; + CHECK(IEC104_Interrogation_NextEvent(app.counter, &event) == IEC104_INTERROGATION_NEXT_EMPTY); +} + +static void ci_send(uint8_t qcc, uint16_t ca) +{ + uint8_t request[10]; + memcpy(request, ci_request, sizeof(request)); request[9] = qcc; + request[4] = (uint8_t)ca; request[5] = (uint8_t)(ca >> 8); + request_unchanged(request, sizeof(request)); +} + +static IEC104_InterrogationEvent_t ci_begin(uint8_t qcc) +{ + ci_send(qcc, 1); ci_process(); return ci_take(IEC104_INTERROGATION_BEGIN); +} + +/* 接受读取请求并取得首个NEED_DATA;冻结/复位没有数据阶段,不使用此辅助函数。 */ +static IEC104_InterrogationEvent_t ci_data(uint8_t qcc) +{ + IEC104_InterrogationEvent_t event = ci_begin(qcc); + CHECK(IEC104_Interrogation_Reply_Begin(app.counter, &event, true) == IEC104_INTERROGATION_REPLY_OK); + ci_process(); return ci_take(IEC104_INTERROGATION_NEED_DATA); +} + +static void ci_done(const IEC104_InterrogationEvent_t *event) +{ + CHECK(IEC104_Interrogation_Complete_Data(app.counter, event) == IEC104_INTERROGATION_REPLY_OK); + ci_process(); CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_COMPLETED); + ci_empty(); app_poll(); +} + +/* 独立拼接数据字节,覆盖SQ寻址和CP56时标,避免用待测对象编码器生成预期布局。 */ +static void ci_batch(IEC60870_Asdu_t *asdu, uint8_t bytes[249], const IEC104_InterrogationEvent_t *event, + uint8_t type, unsigned count, bool sq) +{ + unsigned i, width = type == 1 ? 1U : type == 15 ? 5U : type == 16 ? 8U : 12U; + size_t offset = 6U; + memset(bytes, 0, 249); + bytes[0] = type; bytes[1] = (uint8_t)(count | (sq ? 0x80U : 0U)); + bytes[2] = (uint8_t)(event->request.data_cause | (event->request.is_test ? 0x80U : 0U)); + bytes[3] = event->request.originator_address; + bytes[4] = (uint8_t)event->request.common_address; bytes[5] = (uint8_t)(event->request.common_address >> 8); + CHECK(6U + (sq ? 3U : 3U * count) + width * count <= 249U); + for (i = 0; i < count; ++i) { + if (!sq || i == 0) { + bytes[offset++] = (uint8_t)(3001U + i); bytes[offset++] = (uint8_t)((3001U + i) >> 8); bytes[offset++] = 0; + } + bytes[offset] = 1; + if (type == 37) { bytes[offset + 9] = 1; bytes[offset + 10] = 1; } + offset += width; + } + CHECK(IEC60870_Asdu_Create_From_Buffer(asdu, IEC104_Slave_Get_Application_Layer_Parameters(app.slave), + bytes, (uint16_t)offset) == IEC60870_ASDU_OK); +} + +/* 同一Slave允许两种召唤并存;同种实例重复创建或广播状态区重叠必须失败,销毁后可重建。 */ +static void test_counter_module_contracts(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_Interrogation_t *counter; + app_open(); IEC104_Interrogation_Config_Default(&config); + config.use_station_registry = true; config.broadcast_states = broadcast_states; config.broadcast_capacity = 16; + CHECK(IEC104_CounterInterrogation_Create(app.slave, &config) == NULL); + config.broadcast_states = counter_broadcast_states; + counter = IEC104_CounterInterrogation_Create(app.slave, &config); CHECK(counter != NULL); + CHECK(IEC104_CounterInterrogation_Create(app.slave, &config) == NULL); + CHECK(IEC104_Interrogation_Destroy(counter)); + counter = IEC104_CounterInterrogation_Create(app.slave, &config); CHECK(counter != NULL); + CHECK(IEC104_Interrogation_Destroy(counter)); app_close(); +} + +/* 校验错误原因优先级、畸形布局的规范化回显和无法可靠应答时的丢弃,输入原文保持不变。 */ +static void test_counter_validation(void) +{ + static const struct { unsigned offset; uint8_t value, cot; unsigned length; } cases[] = { + {2, 8, 45, 10}, {2, 3, 45, 10}, {4, 2, 46, 10}, {6, 1, 47, 10}, + {1, 0x81, 7, 10}, {9, 5, 7, 11}, {9, 0, 7, 10}, {9, 6, 7, 10}, + {9, 63, 7, 10}, {2, 0x46, 7, 10}, {1, 0, 0, 10}, {1, 2, 0, 14} + }; + unsigned i; + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + uint8_t request[14] = {0}, expected[10]; const uint8_t *out[32]; size_t lengths[32]; + ci_open(); memcpy(request, ci_request, sizeof(ci_request)); request[cases[i].offset] = cases[i].value; + request[2] |= 0x80; request_unchanged(request, cases[i].length); app_finish(); + CHECK(IEC104_Interrogation_Get_Status(app.counter, &app.ci)); + CHECK(app.ci.rejected == 1 && app.other_rejected == 0 && !app.ci.gi_pending); + CHECK(collect_asdus(out, lengths) == (cases[i].cot != 0)); + if (cases[i].cot != 0) { + memcpy(expected, request, sizeof(expected)); expected[1] = 1; expected[2] = (uint8_t)(0xc0 | cases[i].cot); + CHECK(lengths[0] == 10 && memcmp(out[0], expected, 10) == 0); + } + ci_close(); + } +} + +/* 检查249字节下的三种最大批次,以及类型、SQ、头部、地址宽度和配置数量限制。 + * 错误批次保留当前交互供修正;总召实例仍拒绝计数量数据。 */ +static void test_counter_batch_validation(void) +{ + static const struct { uint8_t type; bool sq; unsigned count; } valid[] = {{15, false, 30}, {15, true, 48}, {37, false, 16}}; + IEC104_InterrogationConfig_t wide_config; + unsigned i; + IEC104_Interrogation_Config_Default(&wide_config); counter_custom_config = &wide_config; + ci_open(); + for (i = 0; i < sizeof(valid) / sizeof(valid[0]); ++i) { + IEC104_InterrogationEvent_t event = ci_data(5); + IEC60870_Asdu_t asdu; uint8_t bytes[249]; + ci_batch(&asdu, bytes, &event, valid[i].type, valid[i].count, valid[i].sq); + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_OK); + ci_process(); event = ci_take(IEC104_INTERROGATION_NEED_DATA); ci_done(&event); + } + { + IEC104_InterrogationEvent_t event = ci_data(1); IEC60870_Asdu_t asdu; uint8_t bytes[249]; + static const unsigned offsets[] = {2, 3, 4, 2, 2}; + static const uint8_t values[] = {37, 0x44, 2, 0x66, 0xa6}; + for (i = 0; i < 3; ++i) { + ci_batch(&asdu, bytes, &event, i == 0 ? 1 : i == 1 ? 16 : 37, 1, i == 2); + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_ERROR); + } + for (i = 0; i < sizeof(offsets) / sizeof(offsets[0]); ++i) { + ci_batch(&asdu, bytes, &event, 15, 1, false); bytes[offsets[i]] = values[i]; + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_ERROR); + } + { + IEC60870_AppLayerParameters_t wrong = *IEC104_Slave_Get_Application_Layer_Parameters(app.slave); + ci_batch(&asdu, bytes, &event, 15, 1, false); wrong.sizeOfIOA = 2; asdu.parameters = &wrong; + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_ERROR); + ci_batch(&asdu, bytes, &event, 15, 1, false); bytes[4] = 0xff; bytes[5] = 0xff; + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_ERROR); + } + ci_done(&event); + event = gi_data(); ci_batch(&asdu, bytes, &event, 15, 1, false); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &asdu) == IEC104_INTERROGATION_REPLY_ERROR); + gi_done(&event, 0, 0); + } + ci_close(); + { + IEC104_InterrogationConfig_t config; IEC104_InterrogationEvent_t event; IEC60870_Asdu_t asdu; uint8_t bytes[249]; + IEC104_Interrogation_Config_Default(&config); config.max_objects_per_asdu = 4; + counter_custom_config = &config; ci_open(); event = ci_data(5); + ci_batch(&asdu, bytes, &event, 15, 5, false); + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_ERROR); + ci_done(&event); ci_close(); + } +} + +/* 计数量忙只拒绝本实例的新请求;k=1、总召每帧一点时,两种业务仍须交错推进。 */ +static void test_counter_busy_independent(void) +{ + IEC104_InterrogationEvent_t event; const uint8_t *frames[32]; size_t lengths[32]; + ci_open(); event = ci_begin(5); ci_send(1, 1); ci_process(); + CHECK(app.ci.rejected == 1 && app.ci.gi_pending); + app_request(gi_request, sizeof(gi_request)); app_finish(); + CHECK(app.gi.gi_submitted == 1); + CHECK(IEC104_Interrogation_Reply_Begin(app.counter, &event, true) == IEC104_INTERROGATION_REPLY_OK); + ci_process(); event = ci_take(IEC104_INTERROGATION_NEED_DATA); ci_done(&event); + CHECK(collect_asdus(frames, lengths) == 9); ci_close(); + { + IEC104_InterrogationConfig_t config; unsigned step, acked = 0, first_counter = 32, last_general = 0, i; + IEC104_Interrogation_Config_Default(&config); config.max_objects_per_asdu = 1; + custom_config = &config; custom_window = 1; ci_open(); + queue_request(gi_request, sizeof(gi_request)); queue_request(ci_request, sizeof(ci_request)); app_poll(); + for (step = 0; step < 128; ++step) { + unsigned sent; + CHECK(Example_Step(&app)); app_poll(); sent = collect_asdus(frames, lengths); + if (sent != acked) { push_ack(sent); acked = sent; app_poll(); } + if (app.gi.pending_requests == 0 && app.ci.pending_requests == 0) { break; } + } + CHECK(step < 128 && acked == 22 && app.counter_completed == 1 && app.gi.gi_submitted == 1); + for (i = 0; i < acked; ++i) { + if (frames[i][0] == 15 && first_counter == 32) { first_counter = i; } + if (frames[i][0] == 1 || frames[i][0] == 11) { last_general = i; } + } + CHECK(first_counter < last_general); ci_close(); custom_config = NULL; custom_window = 32; + } +} + +/* 总请求按确认、两批数据、终止逐字节核对IOA、初值和BCR状态,并检查整轮快照一致性。 */ +static void test_counter_general_read(void) +{ + static const int32_t expected[] = {120000, 345600, 7890, 0, 1, 65535, INT32_MAX, 42}; + const uint8_t *frames[32]; size_t lengths[32]; unsigned i, j; + ci_open(); ci_send(5, 1); app_finish(); + CHECK(collect_asdus(frames, lengths) == 4); + CHECK(frames[0][0] == 101 && frames[0][2] == 7 && lengths[0] == 10); + CHECK(frames[3][0] == 101 && frames[3][2] == 10 && lengths[3] == 10); + for (i = 1; i < 3; ++i) { + CHECK(frames[i][0] == 15 && frames[i][1] == 4 && frames[i][2] == 37 && frames[i][3] == 0x5a); + CHECK(lengths[i] == 38); + for (j = 0; j < 4; ++j) { + const uint8_t *p = frames[i] + 6 + j * 8; unsigned n = (i - 1) * 4 + j; + uint32_t value = (uint32_t)p[3] | (uint32_t)p[4] << 8 | (uint32_t)p[5] << 16 | (uint32_t)p[6] << 24; + CHECK(((unsigned)p[0] | (unsigned)p[1] << 8) == 3001 + n && p[2] == 0); + CHECK(value == (uint32_t)expected[n] && p[7] == 0); + } + } + CHECK(app.counter_completed == 1 && app.other_rejected == 0); ci_close(); + /* 首批发送受阻后改写整个冻结区,两批仍须使用已固定的值、序号和品质。 */ + ci_open(); ci_send(5, 1); ci_process(); CHECK(Example_Drain_Counter(&app)); ci_process(); + congest(&last_request.connection); CHECK(Example_Drain_Counter(&app)); + CHECK(app.counter_pending && app.counter_cursor == 0 && app.counter_request != 0); + for (i = 0; i < EXAMPLE_COUNTER_COUNT; ++i) { + app.counter_frozen[i].value = -123; app.counter_frozen[i].sequence_number = 7; + app.counter_frozen[i].has_carry = 1; app.counter_frozen[i].is_adjusted = 1; app.counter_frozen[i].is_invalid = 1; + } + fake.send_blocked = 0; app_finish(); CHECK(collect_asdus(frames, lengths) == 4); + CHECK(frames[1][9] == 0xc0 && frames[1][10] == 0xd4 && frames[1][11] == 1 && frames[1][13] == 0); + CHECK(frames[2][9] == 1 && frames[2][10] == 0 && frames[2][13] == 0); + CHECK(app.counter_completed == 1); ci_close(); +} + +/* 示例只配置第1、2组,分别使用COT38、39;协议合法但未配置的第3、4组由应用拒绝。 */ +static void test_counter_group_read(void) +{ + unsigned q; + for (q = 1; q <= 4; ++q) { + const uint8_t *frames[32]; size_t lengths[32]; + ci_open(); ci_send((uint8_t)q, 1); app_finish(); + CHECK(collect_asdus(frames, lengths) == (q <= 2 ? 3U : 1U)); + if (q <= 2) { CHECK(frames[1][2] == 37 + q && frames[1][1] == 4); } + else { CHECK(frames[0][2] == 0x47); } + ci_close(); + } +} + +/* 记录动作次数并模拟执行耗时,区分“请求尚未执行”和“已执行、确认仍在等待”。 */ +typedef struct { unsigned calls; uint64_t elapsed; bool success; } CounterActionProbe; +static bool counter_action(void *context, const IEC104_InterrogationRequest_t *request) +{ + CounterActionProbe *probe = context; + CHECK(request->freeze >= 1 && request->freeze <= 3 && !request->is_test); + ++probe->calls; fake.now += probe->elapsed; return probe->success; +} + +/* 三种动作只确认、不发终止;检查清零后再冻结的值、SQ回绕和分组隔离。 + * TEST及WAIT不调用动作,已消费事件重试为STALE,应用拒绝只执行一次。 */ +static void test_counter_freeze_modes(void) +{ + unsigned frz; + for (frz = 1; frz <= 3; ++frz) { + const uint8_t *frames[32]; size_t lengths[32]; + uint8_t test_request[10]; + ci_open(); memcpy(test_request, ci_request, 10); test_request[2] = 0x86; + test_request[9] = (uint8_t)((frz << 6) | 5); app_request(test_request, 10); app_finish(); + CHECK(app.counter_current[0] == 120000 && app.counter_frozen[0].value == 120000 && app.counter_frozen[0].sequence_number == 0); + CHECK(collect_asdus(frames, lengths) == 1 && frames[0][2] == 0xc7); + fake.sent_bytes = 0; + ci_send((uint8_t)((frz << 6) | 5), 1); app_finish(); + CHECK(collect_asdus(frames, lengths) == 1 && frames[0][2] == 7); + CHECK(app.counter_completed == 1); ci_send(5, 1); app_finish(); + CHECK(collect_asdus(frames, lengths) == 5 && frames[2][13] == (frz == 3 ? 0 : 1)); + if (frz == 2 || frz == 3) { + ci_send(0x45, 1); app_finish(); ci_send(5, 1); app_finish(); + CHECK(collect_asdus(frames, lengths) == 10); + CHECK(frames[7][9] == 0 && frames[7][10] == 0 && frames[7][11] == 0 && frames[7][12] == 0); + } + ci_close(); + } + { + unsigned i; + ci_open(); + for (i = 0; i < EXAMPLE_COUNTER_COUNT; ++i) { app.counter_frozen[i].sequence_number = 31; } + ci_send(0x41, 1); app_finish(); + for (i = 0; i < EXAMPLE_COUNTER_COUNT; ++i) { CHECK(app.counter_frozen[i].sequence_number == (i < 4 ? 0 : 31)); } + ci_close(); + } + { + IEC104_InterrogationEvent_t event; CounterActionProbe probe = {0, 0, true}; + ci_open(); event = ci_begin(0x85); + queue_request(gi_request, sizeof(gi_request)); CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + CHECK(IEC104_CounterInterrogation_Execute_Begin(app.counter, &event, counter_action, &probe) == IEC104_INTERROGATION_REPLY_WAIT); + CHECK(probe.calls == 0); app_poll(); + CHECK(IEC104_CounterInterrogation_Execute_Begin(app.counter, &event, counter_action, &probe) == IEC104_INTERROGATION_REPLY_OK); + CHECK(IEC104_CounterInterrogation_Execute_Begin(app.counter, &event, counter_action, &probe) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(probe.calls == 1); ci_process(); CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_COMPLETED); + ci_empty(); ci_close(); + } + { + uint8_t request[10]; IEC104_InterrogationEvent_t event; CounterActionProbe probe = {0, 0, true}; + ci_open(); memcpy(request, ci_request, 10); request[2] = 0x86; request[9] = 0xc5; + app_request(request, 10); ci_process(); event = ci_take(IEC104_INTERROGATION_BEGIN); + CHECK(IEC104_CounterInterrogation_Execute_Begin(app.counter, &event, counter_action, &probe) == IEC104_INTERROGATION_REPLY_OK); + CHECK(probe.calls == 0 && ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_REJECTED); + ci_process(); ci_close(); + } + { + IEC104_InterrogationEvent_t event; CounterActionProbe probe = {0, 0, false}; + ci_open(); event = ci_begin(0x45); + CHECK(IEC104_CounterInterrogation_Execute_Begin(app.counter, &event, counter_action, &probe) == IEC104_INTERROGATION_REPLY_OK); + CHECK(probe.calls == 1 && ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_REJECTED); + ci_process(); CHECK(app.ci.negative_submitted == 1); ci_close(); + } +} + +/* STOP和断线须同时终结两种召唤,旧取数事件失效;重新激活或连接后可再次读取。 */ +static void test_counter_stop_disconnect(void) +{ + unsigned mode; + for (mode = 0; mode < 2; ++mode) { + IEC104_InterrogationEvent_t ce, ge; + ci_open(); ce = ci_data(5); ge = gi_data(); + if (mode == 0) { push(stop_act, sizeof(stop_act)); app_poll(); } + else { CHECK(IEC104_Slave_Close_Connection(app.slave, &last_request, NULL) == IEC104_TCP_OK); app_poll(); } + CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == (mode == 0 ? IEC104_INTERROGATION_FINISH_STOPPED : IEC104_INTERROGATION_FINISH_DISCONNECTED)); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == (mode == 0 ? IEC104_INTERROGATION_FINISH_STOPPED : IEC104_INTERROGATION_FINISH_DISCONNECTED)); + CHECK(IEC104_Interrogation_Complete_Data(app.counter, &ce) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &ge) == IEC104_INTERROGATION_REPLY_STALE); + if (mode == 0) { push(start_act, sizeof(start_act)); app_poll(); } + else { app_connect(); } + ci_send(5, 1); app_finish(); CHECK(app.counter_completed == 1); ci_close(); + } +} + +/* 动作开始前超时不执行回调;读取超时关闭共享连接,并使另一实例收到断线终结通知。 */ +static void test_counter_timeouts(void) +{ + IEC104_InterrogationConfig_t config; IEC104_InterrogationEvent_t event; CounterActionProbe probe = {0, 0, true}; + IEC104_TcpServerStatus_t service; + IEC104_Interrogation_Config_Default(&config); config.begin_timeout_ms = 100; config.data_timeout_ms = 100; config.total_timeout_ms = 200; + counter_custom_config = &config; ci_open(); event = ci_begin(0x85); fake.now += 100; + CHECK(IEC104_CounterInterrogation_Execute_Begin(app.counter, &event, counter_action, &probe) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(probe.calls == 0 && ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_BEGIN_TIMEOUT); + ci_process(); CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); ci_close(); + counter_custom_config = &config; ci_open(); event = ci_data(5); (void)gi_begin(); fake.now += 100; ci_process(); + CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_DATA_TIMEOUT); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && !service.connected); + app_poll(); CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_DISCONNECTED); ci_close(); + /* 动作已执行,即使确认排队跨过业务期限,也不能改成否定确认或再次执行动作。 */ + counter_custom_config = &config; ci_open(); event = ci_begin(0x85); congest(&last_request.connection); probe.elapsed = 150; + CHECK(IEC104_CounterInterrogation_Execute_Begin(app.counter, &event, counter_action, &probe) == IEC104_INTERROGATION_REPLY_OK); + fake.now += 300; ci_process(); CHECK(probe.calls == 1 && app.ci.timeouts == 0 && app.ci.negative_submitted == 0); + fake.send_blocked = 0; ci_process(); ci_process(); + CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_COMPLETED); ci_close(); +} + +/* 数据WAIT不推进批次,恢复后使用重新组好的数据;终止等待也必须保留并继续提交。 */ +static void test_counter_send_wait(void) +{ + IEC104_InterrogationEvent_t event; IEC60870_Asdu_t asdu; uint8_t bytes[249]; unsigned i; + ci_open(); event = ci_data(5); congest(&last_request.connection); ci_batch(&asdu, bytes, &event, 15, 1, false); + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_WAIT); + CHECK(event.batch_index == 0); fake.send_blocked = 0; + for (i = 0; i < 4; ++i) { app_poll(); } + bytes[9] = 42; + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_OK); + ci_process(); event = ci_take(IEC104_INTERROGATION_NEED_DATA); CHECK(event.batch_index == 1); + congest(&last_request.connection); CHECK(IEC104_Interrogation_Complete_Data(app.counter, &event) == IEC104_INTERROGATION_REPLY_OK); + ci_process(); CHECK(app.ci.waiting); fake.send_blocked = 0; ci_process(); ci_process(); + CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_COMPLETED); ci_close(); +} + +/* 三站读取保持全部确认、逐站数据、全部终止;动作广播逐站确认,中间站拒绝不影响其他站。 + * 每站使用实际CA,动作只产生一次确认和一次终结通知,不额外发送ACT_TERM。 */ +static void test_counter_broadcast(void) +{ + unsigned mode; + for (mode = 0; mode < 3; ++mode) { + IEC104_Station_t stations[2] = {{0}}; unsigned i; const uint8_t *frames[32]; size_t lengths[32]; + CounterActionProbe probe = {0, 0, true}; + ci_open(); + for (i = 0; i < 2; ++i) { CHECK(IEC104_Slave_Add_Station(app.slave, &stations[i], (uint16_t)(i + 2), NULL) == IEC104_STATION_OK); } + ci_send(mode == 0 ? 5 : 0x45, IEC104_CA_BROADCAST); + for (i = 0; i < 3; ++i) { + IEC104_InterrogationEvent_t event; + ci_process(); event = ci_take(IEC104_INTERROGATION_BEGIN); CHECK(event.request.common_address == i + 1); + if (mode == 0) { CHECK(IEC104_Interrogation_Reply_Begin(app.counter, &event, true) == IEC104_INTERROGATION_REPLY_OK); } + else { + if (mode == 2 && i == 1) { + CHECK(IEC104_Interrogation_Reply_Begin(app.counter, &event, false) == IEC104_INTERROGATION_REPLY_OK); + CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_REJECTED); ci_process(); + } else { + CHECK(IEC104_CounterInterrogation_Execute_Begin(app.counter, &event, counter_action, &probe) == IEC104_INTERROGATION_REPLY_OK); + ci_process(); CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_COMPLETED); + } + } + } + if (mode == 0) { + for (i = 0; i < 3; ++i) { + IEC104_InterrogationEvent_t event; IEC60870_Asdu_t asdu; uint8_t bytes[249]; + ci_process(); event = ci_take(IEC104_INTERROGATION_NEED_DATA); CHECK(event.request.common_address == i + 1); + ci_batch(&asdu, bytes, &event, 15, 1, false); + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_OK); + ci_process(); event = ci_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(IEC104_Interrogation_Complete_Data(app.counter, &event) == IEC104_INTERROGATION_REPLY_OK); + } + ci_process(); ci_process(); + for (i = 0; i < 3; ++i) { CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_COMPLETED); } + } + app_poll(); CHECK(collect_asdus(frames, lengths) == (mode == 0 ? 9U : 3U)); + for (i = 0; i < (mode == 0 ? 9U : 3U); ++i) { + CHECK(frames[i][4] == i % 3 + 1 && frames[i][5] == 0); + CHECK(frames[i][2] == (mode == 2 && i == 1 ? 0x47 : i < 3 ? 7 : i < 6 ? 37 : 10)); + } + CHECK(probe.calls == (mode == 0 ? 0U : mode == 1 ? 3U : 2U)); ci_empty(); + for (i = 0; i < 2; ++i) { CHECK(IEC104_Slave_Remove_Station(app.slave, &stations[i]) == IEC104_STATION_OK); } + ci_close(); + } +} + +/* Type101由计数量实例接手;未知类型仍走兜底,即使计数量正在等待应用决定也能应答。 */ +static void test_dispatch_fallback(void) +{ + uint8_t request[10]; IEC104_InterrogationEvent_t event; + ci_open(); event = ci_begin(5); memcpy(request, ci_request, 10); request[0] = 200; + app_request(request, 10); + CHECK(app.other_rejected == 1 && app.other_submitted == 1 && app.ci.gi_pending); + app_finish(); + CHECK(IEC104_Interrogation_Reply_Begin(app.counter, &event, false) == IEC104_INTERROGATION_REPLY_OK); + CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_REJECTED); + ci_process(); ci_close(); +} + +static void check_other_reply(const uint8_t *actual, size_t actual_length, + const uint8_t *request, size_t length) +{ + size_t index; + CHECK(actual_length == length); + for (index = 0; index < length; ++index) { + CHECK(actual[index] == (index == 2 ? (uint8_t)((request[2] & 0x80U) | 0x40U | 44U) : request[index])); + } +} + +/* 同一Poll交付两条命令时当场提交;窗口/写阻塞由HIGH承担,重复命令也逐条应答。 */ +static void test_other_batch(void) +{ + unsigned order, blocked; + for (order = 0; order < 3; ++order) { + for (blocked = 0; blocked < 3; ++blocked) { + const uint8_t *requests[2] = { + order == 1 ? read_request : fallback_request, + order == 0 ? read_request : fallback_request + }; + size_t sizes[2] = { + order == 1 ? sizeof(read_request) : sizeof(fallback_request), + order == 0 ? sizeof(read_request) : sizeof(fallback_request) + }; + const uint8_t *frames[32]; + size_t lengths[32]; + IEC104_TcpServerStatus_t status; + custom_window = blocked == 1 ? 1U : 32U; + app_open(); + fake.send_blocked = blocked == 2; + queue_request(requests[0], sizes[0]); + queue_request(requests[1], sizes[1]); + app_poll(); + CHECK(app.other_rejected == 2 && app.other_submitted == 2); + CHECK(app.other_overloads == 0 && app.other_cancelled == 0); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.connected); + if (blocked == 1) { + CHECK(collect_asdus(frames, lengths) == 1 && status.high_queue.stored_asdu_count == 1); + push_ack(1); + } else if (blocked == 2) { + CHECK(collect_asdus(frames, lengths) == 0 && status.high_queue.stored_asdu_count == 1); + fake.send_blocked = 0; + } + app_finish(); + CHECK(collect_asdus(frames, lengths) == 2); + check_other_reply(frames[0], lengths[0], requests[0], sizes[0]); + check_other_reply(frames[1], lengths[1], requests[1], sizes[1]); + app_close(); + custom_window = 32; + } + } +} + +/* HIGH不足时只丢新兜底请求,恢复后没有旧请求重试,原连接继续接受新命令。 */ +static void test_other_overload(void) +{ + const uint8_t *frames[32]; + size_t lengths[32]; + uint8_t fresh[sizeof(read_request)]; + IEC104_TcpServerStatus_t before, after; + app_open(); + app_request(fallback_request, sizeof(fallback_request)); + CHECK(app.other_submitted == 1); + congest(&last_request.connection); + CHECK(IEC104_Slave_Get_Status(app.slave, &before, NULL) == IEC104_TCP_OK); + queue_request(read_request, sizeof(read_request)); + queue_request(read_request, sizeof(read_request)); + app_poll(); + CHECK(IEC104_Slave_Get_Status(app.slave, &after, NULL) == IEC104_TCP_OK && after.connected); + CHECK(after.high_queue.stored_asdu_count == before.high_queue.stored_asdu_count); + CHECK(app.other_rejected == 3 && app.other_submitted == 1 && app.other_overloads == 2); + CHECK(app.other_cancelled == 0 && app.gi.dropped_requests + app.other_overloads == 2); + fake.send_blocked = 0; + app_finish(); + CHECK(collect_asdus(frames, lengths) == 1); + memcpy(fresh, read_request, sizeof(fresh)); + fresh[3] = 0x63; + app_request(fresh, sizeof(fresh)); + CHECK(app.other_submitted == 2 && app.other_overloads == 2); + app_finish(); + CHECK(collect_asdus(frames, lengths) == 2); + check_other_reply(frames[0], lengths[0], fallback_request, sizeof(fallback_request)); + check_other_reply(frames[1], lengths[1], fresh, sizeof(fresh)); + CHECK(IEC104_Slave_Get_Status(app.slave, &after, NULL) == IEC104_TCP_OK && after.connected); + app_close(); +} + +/* 后续STOP或STOP/START已经受理时,当前请求取消;下一激活阶段只应答新请求。 */ +static void test_other_state_change(void) +{ + unsigned restarted; + for (restarted = 0; restarted < 2; ++restarted) { + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + queue_request(fallback_request, sizeof(fallback_request)); + push(stop_act, sizeof(stop_act)); + if (restarted) { push(start_act, sizeof(start_act)); } + app_poll(); + CHECK(app.other_rejected == 1 && app.other_submitted == 0 && app.other_cancelled == 1); + CHECK(app.other_overloads == 0 && collect_asdus(frames, lengths) == 0); + if (!restarted) { push(start_act, sizeof(start_act)); app_poll(); } + app_request(read_request, sizeof(read_request)); + CHECK(app.other_submitted == 1 && app.other_cancelled == 1); + app_finish(); + CHECK(collect_asdus(frames, lengths) == 1); + check_other_reply(frames[0], lengths[0], read_request, sizeof(read_request)); + app_close(); + } +} + +/* 单总召仍等待应用BEGIN时,其他命令即可提交,不必等总召结束。 */ +static void test_other_during_general(void) +{ + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + app_request(gi_request, sizeof(gi_request)); + CHECK(app.gi.gi_pending); + app_request(read_request, sizeof(read_request)); + CHECK(app.gi.gi_pending && app.other_submitted == 1); + CHECK(collect_asdus(frames, lengths) == 1); + check_other_reply(frames[0], lengths[0], read_request, sizeof(read_request)); + app_finish(); + check_gi(1, 0x5a); + app_close(); +} + +/* Clock actions are controlled; these tests never call the host Clock_Set backend. */ +static const uint8_t cs_request[] = {103, 1, 6, 0x71, 1, 0, 0, 0, 0, 0x39, 0x30, 34, 12, 28, 10, 26}; +typedef struct { + unsigned calls; + bool success, reentrant; + uint64_t elapsed; + IEC104_ClockSynchronizationRequest_t last; +} ClockActionProbe; +static ClockActionProbe cs_probe; + +static bool cs_action(void *context, const IEC104_ClockSynchronizationRequest_t *request) +{ + ClockActionProbe *probe = context; + IEC104_ClockSynchronizationStatus_t before, after; + CHECK(request != NULL && request->generation != 0U && request->receive_serial != 0U); + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &before) && before.confirm_pending); + ++probe->calls; probe->last = *request; + if (probe->reentrant) { + CHECK(!before.timer_active); + CHECK(!IEC104_ClockSynchronization_Process(app.clock)); + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, context) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_ERROR); + CHECK(IEC104_ClockSynchronization_Handle_Event(app.clock, &last_request) == + IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR); + CHECK(!IEC104_ClockSynchronization_Destroy(app.clock)); + IEC104_ClockSynchronization_Cancel(app.clock); + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &after)); + CHECK(after.pending_requests == before.pending_requests && after.confirm_pending); + } + fake.now += probe->elapsed; + return probe->success; +} + +static void cs_open(const IEC104_ClockSynchronizationConfig_t *config) +{ + memset(&cs_probe, 0, sizeof(cs_probe)); cs_probe.success = true; + clock_enabled = true; clock_custom_config = config; app_open(); + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs) && app.cs.active); +} + +static void cs_close(void) +{ + cs_test_now_step = 0U; app_close(); clock_enabled = false; clock_custom_config = NULL; +} + +static void cs_process(void) +{ + CHECK(IEC104_ClockSynchronization_Process(app.clock)); + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs)); +} + +static void cs_finish(void) +{ + unsigned i; + for (i = 0U; i < 16U; ++i) { app_poll(); cs_process(); } + CHECK(app.cs.pending_requests == 0U); +} + +static void cs_send(uint8_t oa, uint16_t ca) +{ + uint8_t request[sizeof(cs_request)]; + memcpy(request, cs_request, sizeof(request)); request[3] = oa; + request[4] = (uint8_t)ca; request[5] = (uint8_t)(ca >> 8); + app_request(request, sizeof(request)); +} + +static void cs_timed_event(uint8_t value) +{ + uint8_t bytes[] = {30, 1, 3, 0, 1, 0, 0xe9, 3, 0, 0, 0x39, 0x30, 34, 12, 28, 10, 26}; + IEC60870_Asdu_t asdu; + bytes[9] = value; + CHECK(IEC60870_Asdu_Create_From_Buffer(&asdu, + IEC104_Slave_Get_Application_Layer_Parameters(app.slave), bytes, sizeof(bytes)) == IEC60870_ASDU_OK); + CHECK(IEC104_Slave_Enqueue_Event(app.slave, &asdu) == IEC104_TCP_SUBMIT_OK); +} + +static void cs_execute(void) +{ + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_OK); +} + +static void cs_reply(const uint8_t *bytes, size_t length, const uint8_t *request, + uint8_t cot, uint16_t ca) +{ + size_t i; + CHECK(length == sizeof(cs_request)); + for (i = 0U; i < length; ++i) { + uint8_t expected = i == 1U ? 1U : i == 2U ? cot : i == 4U ? (uint8_t)ca : + i == 5U ? (uint8_t)(ca >> 8) : request[i]; + CHECK(bytes[i] == expected); + } +} + +static void test_clock_interfaces(void) +{ + IEC104_ClockSynchronizationConfig_t config; + IEC104_ClockSynchronizationStatus_t status; + IEC104_ClockSynchronization_Config_Default(&config); + CHECK(config.master_utc_offset_minutes == 480 && config.execute_timeout_ms == 15000U); + CHECK(IEC104_ClockSynchronization_Create(NULL, &config) == NULL); + CHECK(!IEC104_ClockSynchronization_Destroy(NULL)); + CHECK(!IEC104_ClockSynchronization_Process(NULL)); + CHECK(!IEC104_ClockSynchronization_Get_Status(NULL, &status)); + CHECK(IEC104_ClockSynchronization_Handle_Event(NULL, NULL) == IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR); + CHECK(IEC104_ClockSynchronization_Execute_Next(NULL, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_ERROR); + app_open(); config.master_utc_offset_minutes = 1440; + CHECK(IEC104_ClockSynchronization_Create(app.slave, &config) == NULL); + config.master_utc_offset_minutes = -1440; + CHECK(IEC104_ClockSynchronization_Create(app.slave, &config) == NULL); + app_close(); cs_open(NULL); IEC104_ClockSynchronization_Config_Default(&config); + CHECK(IEC104_ClockSynchronization_Create(app.slave, &config) == NULL); + CHECK(!IEC104_ClockSynchronization_Get_Status(app.clock, NULL)); + CHECK(IEC104_ClockSynchronization_Handle_Event(app.clock, NULL) == IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR); + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, NULL, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_ERROR); + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY); + cs_send(0x71, 1); CHECK(!IEC104_ClockSynchronization_Destroy(app.clock)); +#if IEC104_TCP_SERVER_CAPACITY > 1 + { + IEC104_Slave_t *other_slave = IEC104_Slave_Create(); + IEC104_ClockSynchronization_t *other_clock; + IEC104_SlaveConfig_t slave_config; + CHECK(other_slave != NULL); IEC104_Slave_Config_Default(&slave_config); + CHECK(IEC104_Slave_Init(other_slave, &slave_config, NULL) == IEC104_TCP_OK); + other_clock = IEC104_ClockSynchronization_Create(other_slave, &config); + CHECK(other_clock != NULL); + CHECK(IEC104_ClockSynchronization_Handle_Event(other_clock, &last_request) == + IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR); + CHECK(IEC104_ClockSynchronization_Destroy(other_clock)); + CHECK(IEC104_Slave_Close(other_slave, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Destroy(other_slave, NULL) == IEC104_TCP_OK); + } +#endif + CHECK(cs_probe.calls == 0U); IEC104_ClockSynchronization_Cancel(app.clock); cs_close(); +} + +static void test_clock_execute_and_echo(void) +{ + const uint8_t *frames[32]; size_t lengths[32]; + IEC104_SlaveEvent_t before; + cs_open(NULL); app_request(cs_request, sizeof(cs_request)); before = last_request; + cs_process(); CHECK(cs_probe.calls == 0U && app.cs.execute_pending); + fake.now += 1655U; cs_probe.elapsed = 400U; cs_execute(); + CHECK(cs_probe.calls == 1U && cs_probe.last.accepted_ms == 1000U && cs_probe.last.base_ms == 2655U); + CHECK(cs_probe.last.local_time.year == 2026U && cs_probe.last.local_time.month == 10U && + cs_probe.last.local_time.day == 28U && cs_probe.last.local_time.hour == 12U && + cs_probe.last.local_time.minute == 34U && cs_probe.last.local_time.second == 14U && + cs_probe.last.local_time.ms == 0U && cs_probe.last.station_id == IEC104_Station_Get_Id(&app.station)); + CHECK(memcmp(&before, &last_request, sizeof(before)) == 0); + cs_finish(); CHECK(app.cs.executed == 1U && app.cs.synchronized == 1U); + CHECK(collect_asdus(frames, lengths) == 1U); cs_reply(frames[0], lengths[0], cs_request, 7, 1); + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY); + app_request(cs_request, sizeof(cs_request)); fake.now += 100U; cs_execute(); cs_finish(); + CHECK(cs_probe.calls == 2U && cs_probe.last.accepted_ms == 3055U && cs_probe.last.base_ms == 3155U); + CHECK(cs_probe.last.local_time.second == 12U && cs_probe.last.local_time.ms == 445U); + CHECK(collect_asdus(frames, lengths) == 2U); cs_reply(frames[1], lengths[1], cs_request, 7U, 1U); + cs_close(); +} + +static void test_clock_timezone_rollover(void) +{ + IEC104_ClockSynchronizationConfig_t config; + uint8_t request[sizeof(cs_request)]; + IEC104_ClockSynchronization_Config_Default(&config); config.master_utc_offset_minutes = 0; + cs_open(&config); memcpy(request, cs_request, sizeof(request)); request[12] |= 0x80U; + app_request(request, sizeof(request)); cs_execute(); + CHECK(cs_probe.last.master_time.is_summer_time && cs_probe.last.local_time.hour == 19U); + cs_finish(); cs_close(); + cs_open(NULL); memcpy(request, cs_request, sizeof(request)); + request[9] = 0xfc; request[10] = 0xe9; request[11] = 59; request[12] = 23; + request[13] = 31; request[14] = 12; request[15] = 26; + app_request(request, sizeof(request)); fake.now += 200U; cs_execute(); + CHECK(cs_probe.last.local_time.year == 2027U && cs_probe.last.local_time.month == 1U && + cs_probe.last.local_time.day == 1U && cs_probe.last.local_time.hour == 0U && + cs_probe.last.local_time.minute == 0U && cs_probe.last.local_time.second == 0U && + cs_probe.last.local_time.ms == 100U); + cs_finish(); cs_close(); + cs_open(NULL); memcpy(request, cs_request, sizeof(request)); + request[9] = 0; request[10] = 0; request[11] = 0; request[12] = 0x80; + request[13] = 1; request[14] = 1; request[15] = 0; + app_request(request, sizeof(request)); cs_execute(); cs_finish(); + CHECK(cs_probe.last.local_time.year == 1999U && cs_probe.last.local_time.month == 12U && + cs_probe.last.local_time.day == 31U && cs_probe.last.local_time.hour == 23U); + { + const uint8_t *frames[32]; size_t lengths[32]; + CHECK(collect_asdus(frames, lengths) == 1U); cs_reply(frames[0], lengths[0], request, 7U, 1U); + } + cs_close(); +} + +static void test_clock_validation(void) +{ + static const struct { unsigned offset; uint8_t value, cot; } cases[] = { + {2, 5, 45}, {4, 2, 46}, {6, 1, 47}, {2, 0x86, 0xc7}, {2, 0x46, 0x47}, + {1, 0x81, 0x47}, {11, 0xa2, 0x47}, {14, 0, 0x47}, {11, 0x62, 0x47} + }; + unsigned i; + for (i = 0U; i < sizeof(cases) / sizeof(cases[0]); ++i) { + uint8_t request[sizeof(cs_request)]; const uint8_t *frames[32]; size_t lengths[32]; + cs_open(NULL); memcpy(request, cs_request, sizeof(request)); request[cases[i].offset] = cases[i].value; + if (i < 2U) { request[4] = 2U; request[6] = 1U; } + app_request(request, sizeof(request)); cs_finish(); + CHECK(cs_probe.calls == 0U && collect_asdus(frames, lengths) == 1U); + cs_reply(frames[0], lengths[0], request, + cases[i].cot < 64U ? (uint8_t)(cases[i].cot | 0x40U) : cases[i].cot, + request[4]); + cs_close(); + } + { + uint8_t request[26]; const uint8_t *frames[32]; size_t lengths[32]; + cs_open(NULL); memcpy(request, cs_request, sizeof(cs_request)); request[16] = 0x5a; + app_request(request, 17U); cs_finish(); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][2] == 0x47U); + memcpy(request + 16, cs_request + 6, 10); request[1] = 2; + app_request(request, sizeof(request)); cs_finish(); + CHECK(collect_asdus(frames, lengths) == 1U && cs_probe.calls == 0U); + memcpy(request, cs_request, sizeof(cs_request)); request[1] = 0U; + app_request(request, sizeof(cs_request)); cs_finish(); + request[1] = 1U; app_request(request, sizeof(cs_request) - 1U); cs_finish(); + CHECK(collect_asdus(frames, lengths) == 1U && cs_probe.calls == 0U); + cs_close(); + } +} + +static void test_clock_fifo_and_full(void) +{ + const uint8_t *frames[32]; size_t lengths[32]; unsigned i; + uint8_t request[sizeof(cs_request)]; + cs_open(NULL); cs_send(0x70, 1); + memcpy(request, cs_request, sizeof(request)); request[3] = 0x71; request[2] = 5; + app_request(request, sizeof(request)); cs_send(0x72, 1); cs_send(0x73, 1); cs_send(0x74, IEC104_CA_BROADCAST); + cs_process(); CHECK(collect_asdus(frames, lengths) == 0U && app.cs.pending_requests == 4U); + CHECK(app.cs.dropped_requests == 1U && app.cs.overloads == 1U && cs_probe.calls == 0U); + for (i = 0U; i < 3U; ++i) { cs_execute(); } + cs_finish(); CHECK(cs_probe.calls == 3U && collect_asdus(frames, lengths) == 4U); + for (i = 0U; i < 4U; ++i) { + CHECK(frames[i][3] == 0x70U + i && frames[i][2] == (i == 1U ? 0x6dU : 7U)); + } + cs_close(); + cs_open(NULL); queue_request(cs_request, sizeof(cs_request)); queue_request(cs_request, sizeof(cs_request)); + app_poll(); cs_execute(); cs_execute(); cs_finish(); + CHECK(cs_probe.calls == 2U && app.cs.submitted == 2U && collect_asdus(frames, lengths) == 2U); + cs_close(); +} + +static void test_clock_action_contract(void) +{ + const uint8_t *frames[32]; size_t lengths[32]; + IEC104_ClockSynchronizationConfig_t config; + IEC104_ClockSynchronization_Config_Default(&config); config.execute_timeout_ms = 10U; + cs_open(&config); cs_probe.reentrant = true; cs_probe.elapsed = 100U; + cs_send(0x71, 1); cs_execute(); cs_finish(); + CHECK(cs_probe.calls == 1U && app.cs.timeouts == 0U && app.cs.synchronized == 1U); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][2] == 7U); + cs_close(); + cs_open(NULL); cs_probe.success = false; cs_send(0x71, 1); cs_execute(); cs_finish(); + CHECK(cs_probe.calls == 1U && app.cs.executed == 1U && app.cs.synchronized == 0U); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][2] == 0x47U); + cs_close(); +} + +static void test_clock_deadlines(void) +{ + IEC104_ClockSynchronizationConfig_t config; + const uint8_t *frames[32]; size_t lengths[32]; unsigned via_execute; + IEC104_ClockSynchronization_Config_Default(&config); config.execute_timeout_ms = 10U; + for (via_execute = 0U; via_execute < 2U; ++via_execute) { + cs_open(&config); cs_send(0x71, 1); fake.now += 9U; + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs)); + CHECK(app.cs.timer_active && app.cs.timeout_remaining_ms == 1U); + ++fake.now; + if (via_execute) { + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY); + } else { cs_process(); } + cs_finish(); + CHECK(cs_probe.calls == 0U && app.cs.timeouts == 1U && !app.cs.timer_active); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][2] == 0x47U); + cs_close(); + } + cs_open(&config); cs_send(0x71, 1); fake.now = 1009U; cs_test_now_step = 2U; + (void)IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe); + cs_test_now_step = 0U; + CHECK(cs_probe.calls == 0U || cs_probe.last.base_ms < cs_probe.last.accepted_ms + 10U); + cs_finish(); cs_close(); + config.execute_timeout_ms = 0U; cs_open(&config); cs_send(0x71, 1); fake.now += 100U; + cs_execute(); cs_finish(); CHECK(app.cs.timeouts == 0U && cs_probe.calls == 1U); cs_close(); + config.execute_timeout_ms = 10U; cs_open(&config); cs_send(0x71, 1); fake.now = 999U; + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY); + fake.now = 1001U; cs_finish(); + CHECK(cs_probe.calls == 0U && app.cs.time_errors + app.cs.timeouts == 1U); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][2] == 0x47U); cs_close(); +} + +static void test_clock_time_errors(void) +{ + IEC104_ClockSynchronizationConfig_t config; + unsigned mode; + IEC104_ClockSynchronization_Config_Default(&config); config.execute_timeout_ms = 0U; + for (mode = 0U; mode < 3U; ++mode) { + uint8_t request[sizeof(cs_request)]; const uint8_t *frames[32]; size_t lengths[32]; + cs_open(&config); memcpy(request, cs_request, sizeof(request)); + if (mode == 2U) { + request[9] = 0xfc; request[10] = 0xe9; request[11] = 59; request[12] = 23; + request[13] = 31; request[14] = 12; request[15] = 99; + } + app_request(request, sizeof(request)); + fake.now = mode == 0U ? 999U : mode == 1U ? UINT64_MAX : 1200U; + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY); + fake.now = 1200U; cs_finish(); + CHECK(cs_probe.calls == 0U && app.cs.time_errors == 1U && app.cs.timeouts == 0U); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][2] == 0x47U); + cs_close(); + } + { + uint8_t request[sizeof(cs_request)]; const uint8_t *frames[32]; size_t lengths[32]; + config.master_utc_offset_minutes = 0; cs_open(&config); + memcpy(request, cs_request, sizeof(request)); + request[9] = 0x5f; request[10] = 0xea; request[11] = 59U; request[12] = 23U; + request[13] = 31U; request[14] = 12U; request[15] = 99U; + app_request(request, sizeof(request)); cs_finish(); + CHECK(app.cs.accepted == 0U && cs_probe.calls == 0U && app.cs.rejected == 1U); + CHECK(collect_asdus(frames, lengths) == 1U); cs_reply(frames[0], lengths[0], request, 0x47U, 1U); + cs_close(); + } +} + +static void test_clock_try_order_and_wait(void) +{ + const uint8_t *frames[32]; size_t lengths[32]; + IEC104_TcpServerStatus_t status; + custom_window = 1U; cs_open(NULL); cs_send(0x71, 1); + cs_timed_event(0); app_poll(); cs_timed_event(1); + cs_execute(); cs_process(); + CHECK(cs_probe.calls == 1U && app.cs.confirm_pending && app.cs.waits != 0U && !app.cs.timer_active); + CHECK(!app.cs.need_process && IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK); + CHECK(status.high_queue.stored_asdu_count == 0U && collect_asdus(frames, lengths) == 1U); + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY); + push_ack(1); app_poll(); cs_process(); + CHECK(collect_asdus(frames, lengths) == 2U && frames[0][0] == 30U && frames[1][0] == 30U && frames[1][9] == 1U); + CHECK(app.cs.confirm_pending && cs_probe.calls == 1U); + push_ack(2); cs_finish(); + CHECK(collect_asdus(frames, lengths) == 3U && frames[2][0] == 103U && frames[2][2] == 7U); + CHECK(cs_probe.calls == 1U && app.cs.submitted == 1U); + cs_close(); custom_window = 32U; + { + uint8_t request[sizeof(cs_request)]; IEC104_TcpServerStatus_t before; + cs_open(NULL); app_request(read_request, sizeof(read_request)); congest(&last_request.connection); + CHECK(IEC104_Slave_Get_Status(app.slave, &before, NULL) == IEC104_TCP_OK); + memcpy(request, cs_request, sizeof(request)); request[2] = 5U; + app_request(request, sizeof(request)); memset(request, 0xcc, sizeof(request)); cs_process(); + CHECK(app.cs.pending_requests == 1U && app.cs.waits != 0U && cs_probe.calls == 0U && !app.cs.need_process); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK); + CHECK(status.high_queue.stored_asdu_count == before.high_queue.stored_asdu_count); + fake.send_blocked = 0; cs_finish(); + CHECK(collect_asdus(frames, lengths) == 2U); cs_reply(frames[1], lengths[1], cs_request, 0x6dU, 1U); + cs_close(); + } +} + +static void test_clock_session_cleanup(void) +{ + unsigned mode; + cs_open(NULL); cs_send(0x71, 1); push(start_act, sizeof(start_act)); app_poll(); + cs_execute(); cs_finish(); CHECK(cs_probe.calls == 1U && app.cs.cancelled == 0U); cs_close(); + for (mode = 0U; mode < 3U; ++mode) { + const uint8_t *frames[32]; size_t lengths[32]; + cs_open(NULL); cs_send(0x71, 1); + if (mode != 0U) { congest(&last_request.connection); cs_execute(); cs_process(); } + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs) && app.cs.pending_requests == 1U); + if (mode == 2U) { + CHECK(IEC104_Slave_Close_Connection(app.slave, &last_request, NULL) == IEC104_TCP_OK); + app_poll(); fake.send_blocked = 0; app_connect(); + } else { + push(stop_act, sizeof(stop_act)); push(start_act, sizeof(start_act)); + app_poll(); fake.send_blocked = 0; + } + cs_process(); CHECK(app.cs.pending_requests == 0U && app.cs.cancelled == 1U); + CHECK(collect_asdus(frames, lengths) == 0U && cs_probe.calls == (mode == 0U ? 0U : 1U)); + cs_send(0x72, 1); cs_execute(); cs_finish(); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][3] == 0x72U); + CHECK(cs_probe.calls == (mode == 0U ? 1U : 2U)); + cs_close(); + } + { + const uint8_t *frames[32]; size_t lengths[32], written; + cs_open(NULL); cs_send(0x71, 1); written = fake.sent_bytes; fake.send_limit = 3U; cs_execute(); + CHECK(fake.sent_bytes > written && fake.sent_bytes < written + sizeof(cs_request) + 6U); + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs)); + CHECK(app.cs.pending_requests == 0U && app.cs.submitted == 1U); + push(stop_act, sizeof(stop_act)); app_poll(); fake.send_limit = 0U; cs_finish(); + CHECK(collect_asdus(frames, lengths) == 1U && cs_probe.calls == 1U && app.cs.cancelled == 0U); + cs_reply(frames[0], lengths[0], cs_request, 7U, 1U); cs_close(); + } +} + +static void test_clock_stop_priority(void) +{ + IEC104_ClockSynchronizationConfig_t config; + IEC104_TcpServerStatus_t service; + IEC104_ClockSynchronization_Config_Default(&config); config.execute_timeout_ms = 10U; + cs_open(&config); cs_send(0x71, 1); fake.now += 10U; + push(stop_act, sizeof(stop_act)); CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_WAIT); + cs_process(); CHECK(app.cs.events_pending && app.cs.timeouts == 0U && app.cs.cancelled == 0U); + app_poll(); cs_process(); CHECK(app.cs.cancelled == 1U && app.cs.timeouts == 0U && cs_probe.calls == 0U); + cs_close(); + cs_open(NULL); cs_send(0x71, 1); congest(&last_request.connection); cs_execute(); cs_process(); + retain_stop_for_submit(); fake.send_blocked = 0; cs_process(); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK); + CHECK(!service.connection.data_transfer_started && service.pending_event_count != 0U); + CHECK(app.cs.events_pending && !app.cs.need_process && app.cs.cancelled == 0U && app.cs.pending_requests == 1U); + app_poll(); cs_process(); CHECK(app.cs.cancelled == 1U && app.cs.pending_requests == 0U && cs_probe.calls == 1U); + cs_close(); +} + +static void test_clock_fatal_cleanup(void) +{ + cs_open(NULL); cs_send(0x71, 1); congest(&last_request.connection); cs_execute(); cs_process(); + sut->queue_initialized = 0; + CHECK(IEC104_ClockSynchronization_Process(app.clock)); + sut->queue_initialized = 1; + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs)); + CHECK(app.cs.pending_requests == 0U && app.cs.cancelled == 1U && cs_probe.calls == 1U); + fake.send_blocked = 0; app_poll(); cs_process(); + CHECK(cs_probe.calls == 1U && app.cs.submitted == 0U); + cs_close(); +} + +static void test_clock_directed_station(void) +{ + IEC104_Station_t replacement = {0}; uint64_t original; + const uint8_t *frames[32]; size_t lengths[32]; + cs_open(NULL); original = IEC104_Station_Get_Id(&app.station); cs_send(0x71, 1); + CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&app.station)); + CHECK(IEC104_Slave_Add_Station(app.slave, &replacement, 1U, &replacement) == IEC104_STATION_OK); + cs_execute(); CHECK(cs_probe.last.station_id == original); + cs_finish(); CHECK(IEC104_Station_Is_Reusable(&app.station)); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][4] == 1U); + CHECK(IEC104_Slave_Remove_Station(app.slave, &replacement) == IEC104_STATION_OK); + cs_close(); +} + +static void test_clock_broadcast(void) +{ + IEC104_Station_t second = {0}, third = {0}; + const uint8_t *frames[32]; size_t lengths[32]; unsigned i; + cs_open(NULL); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, NULL) == IEC104_STATION_OK); + cs_send(0x71, IEC104_CA_BROADCAST); cs_execute(); cs_finish(); + CHECK(cs_probe.calls == 1U && cs_probe.last.station_id == 0U && collect_asdus(frames, lengths) == 3U); + for (i = 0U; i < 3U; ++i) { CHECK(frames[i][4] == i + 1U && frames[i][2] == 7U); } + cs_probe.success = false; cs_send(0x72, IEC104_CA_BROADCAST); cs_execute(); cs_finish(); + CHECK(cs_probe.calls == 2U && collect_asdus(frames, lengths) == 6U); + for (i = 3U; i < 6U; ++i) { CHECK(frames[i][4] == i - 2U && frames[i][2] == 0x47U); } + { + uint8_t request[sizeof(cs_request)]; + memcpy(request, cs_request, sizeof(request)); request[4] = 0xff; request[5] = 0xff; request[2] = 5; + app_request(request, sizeof(request)); cs_finish(); + CHECK(cs_probe.calls == 2U && collect_asdus(frames, lengths) == 9U); + for (i = 6U; i < 9U; ++i) { CHECK(frames[i][4] == i - 5U && frames[i][2] == 0x6dU); } + } + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + cs_close(); + for (i = 0U; i < 2U; ++i) { + uint8_t request[sizeof(cs_request)]; + cs_open(NULL); CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + memcpy(request, cs_request, sizeof(request)); request[4] = 0xff; request[5] = 0xff; + request[2] = i == 0U ? 5U : 6U; app_request(request, sizeof(request)); cs_finish(); + CHECK(collect_asdus(frames, lengths) == 1U && cs_probe.calls == 0U); + cs_reply(frames[0], lengths[0], request, i == 0U ? 0x6dU : 0x6eU, IEC104_CA_BROADCAST); + cs_close(); + } +} + +static void test_clock_broadcast_mutation(void) +{ + IEC104_Station_t second = {0}, replacement = {0}, added = {0}; + const uint8_t *frames[32]; size_t lengths[32]; + cs_open(NULL); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + cs_send(0x71, IEC104_CA_BROADCAST); congest(&last_request.connection); cs_execute(); cs_process(); + CHECK(app.cs.confirm_pending && app.cs.waits != 0U); + CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &replacement, 1U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &added, 3U, NULL) == IEC104_STATION_OK); + fake.send_blocked = 0; cs_finish(); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][4] == 2U && cs_probe.calls == 1U); + CHECK(IEC104_Station_Is_Reusable(&app.station)); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &replacement) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &added) == IEC104_STATION_OK); + cs_close(); + { + unsigned mode; + for (mode = 0U; mode < 3U; ++mode) { + memset(&second, 0, sizeof(second)); cs_open(NULL); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + cs_send(0x71, IEC104_CA_BROADCAST); congest(&last_request.connection); cs_execute(); cs_process(); + if (mode != 2U) { CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); } + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&app.station) && !IEC104_Station_Is_Reusable(&second)); + if (mode == 0U) { push(stop_act, sizeof(stop_act)); app_poll(); } + else if (mode == 1U) { IEC104_ClockSynchronization_Cancel(app.clock); } + else { + sut->queue_initialized = 0; CHECK(IEC104_ClockSynchronization_Process(app.clock)); + sut->queue_initialized = 1; + } + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs)); + CHECK(app.cs.pending_requests == 0U && app.cs.cancelled == 1U && cs_probe.calls == 1U); + if (mode == 2U) { CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); } + CHECK(IEC104_Station_Is_Reusable(&app.station) && IEC104_Station_Is_Reusable(&second)); + fake.send_blocked = 0; cs_close(); + } + } +} + +static void test_clock_example_integration(void) +{ + const uint8_t *frames[32]; size_t lengths[32]; unsigned i, clocks = 0U; + uint8_t counter_request[sizeof(ci_request)]; + counter_enabled = true; cs_open(NULL); + memcpy(counter_request, ci_request, sizeof(counter_request)); counter_request[3] = 0x72U; + queue_request(gi_request, sizeof(gi_request)); queue_request(counter_request, sizeof(counter_request)); + queue_request(cs_request, sizeof(cs_request)); app_poll(); + CHECK(!app.clock_system && !app.clock_valid); app_finish(); + CHECK(app.clock_valid && app.clock_base_ms == 1000U && app.cs.executed == 1U && app.cs.synchronized == 1U); + CHECK(cs_probe.calls == 0U && app.other_rejected == 0U); + for (i = 0U; i < collect_asdus(frames, lengths); ++i) { + if (frames[i][0] == 103U) { ++clocks; cs_reply(frames[i], lengths[i], cs_request, 7U, 1U); } + } + CHECK(clocks == 1U && app.counter_completed == 1U); + check_gi(5U, 0x5aU); cs_close(); counter_enabled = false; +} + +int main(void) +{ + test_total_call(); + test_wait_each_phase(); + test_validation(); + test_busy(); + test_stop(); + test_disconnect(); + test_malformed(); + test_malformed_drop(); + test_receiver_truncated(); + test_malformed_preserves_active(); + test_malformed_backpressure(); + test_overload(); + test_event_budget_stop_start(); + test_non_gi_ignored(); + test_module_contracts(); + test_need_process_business(); + test_need_process_send_wait(); + test_need_process_reply_and_transport(); + test_gi_spontaneous_order(); + test_gi_accepted_order(); + test_gi_send_wait_timeout(); + test_gi_window_wait(); + test_gi_due_ack_wait(); + test_gi_submit_fatal(); + test_stop_in_process_after_state_backpressure(); + test_stop_during_submit(); + test_fatal_during_stop_submit(); + test_submit_error_without_stop(); + test_submit_with_new_record(); + test_event_empty_and_wait(); + test_event_copy_and_send_wait(); + test_event_batch_validation(); + test_event_mixed_types(); + test_event_capacity_batches(); + test_event_reject_and_fail(); + test_event_cancellation(); + test_event_queue_and_destroy(); + test_event_pending_stop_start(); + test_event_instance_reuse(); + test_slave_event_validation(); + test_reply_while_waiting(); + test_reply_event_backpressure(); + test_reply_slot_reuse(); + test_reply_non_head_events(); + test_reply_round_robin(); + test_reply_send_wait(); + test_reply_preserves_submitted_data(); + test_reply_cleanup(); + test_process_reply_only(); + test_immediate_reply_guard(); + test_overload_drop_preserves_active(); + test_qoi_sequential_retry(); + test_busy_all_phases(); + test_busy_cancel_match(); + test_busy_stop_cleanup(); + test_busy_preserves_deadline(); + test_next_request_deadline(); + test_begin_capacity_wait(); + test_cancel_before_begin(); + test_group_interrogation(); + test_example_groups(); + test_remote_deactivation_phases(); + test_remote_deactivation_validation(); + test_remote_deactivation_full_queue(); + test_begin_timeout(); + test_data_timeout(); + test_timeout_progress(); + test_total_timeout_waits(); + test_timeout_disabled_and_large(); + test_timeout_transport_priority(); + test_preconfirm_failure_reply_order(); + test_preconfirm_failure_transport_priority(); + test_confirm_submission_boundary(); + test_broadcast_interrogation(); + test_broadcast_error_replies(); + test_broadcast_busy(); + test_broadcast_deactivation(); + test_station_binding_lifetime(); + test_station_binding_wire(); + test_registry_routing(); + test_registry_broadcast_many(); + test_registry_delete_current(); + test_registry_broadcast_mutation(); + test_registry_reply_fanout(); + test_registry_deactivation(); + test_registry_reject_timeout_stop(); + test_registry_wait_each_phase(); + test_registry_malformed(); + test_registry_example_order(); + test_registry_capacity(); + test_registry_workspace_contract(); + test_registry_confirmed_deleted(); + test_registry_finish_delivery(); + test_registry_directed_deactivation(); + test_registry_deactivation_full(); + test_registry_reject_during_deactivation(); + test_registry_total_deadline(); + test_registry_timeout_priority(); + test_counter_module_contracts(); + test_counter_validation(); + test_counter_batch_validation(); + test_counter_busy_independent(); + test_counter_general_read(); + test_counter_group_read(); + test_counter_freeze_modes(); + test_counter_stop_disconnect(); + test_counter_timeouts(); + test_counter_send_wait(); + test_counter_broadcast(); + test_dispatch_fallback(); + test_other_batch(); + test_other_overload(); + test_other_state_change(); + test_other_during_general(); + test_clock_interfaces(); + test_clock_execute_and_echo(); + test_clock_timezone_rollover(); + test_clock_validation(); + test_clock_fifo_and_full(); + test_clock_action_contract(); + test_clock_deadlines(); + test_clock_time_errors(); + test_clock_try_order_and_wait(); + test_clock_session_cleanup(); + test_clock_stop_priority(); + test_clock_fatal_cleanup(); + test_clock_directed_station(); + test_clock_broadcast(); + test_clock_broadcast_mutation(); + test_clock_example_integration(); + puts("slave example + event interrogation tests: 130 groups passed (98 general + 12 counter + 4 fallback + 16 clock)"); + return 0; +} diff --git a/IEC60870-Slave/.build/command-v1/before/test/iec104_slave_test.c b/IEC60870-Slave/.build/command-v1/before/test/iec104_slave_test.c new file mode 100644 index 00000000..e68a47a0 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/test/iec104_slave_test.c @@ -0,0 +1,1317 @@ +/* 共用子站发送接口回归:复用可控Net后端,验证真实104提交路径。 + * 不另行链接TcpServer.c、Net后端或事件环测试翻译单元。 */ +#define main event_ring_suite_main +#include "iec104_tcp_event_ring_test.c" +#undef main +#include "IEC104_Slave.h" + +static const IEC60870_AppLayerParameters_t slave_parameters = IEC104_APP_LAYER_PARAMETERS_DEFAULT; +/* Type100、TEST=1、OA=0x5A、CA=0x1234、IOA=0、QOI=20。 */ +static const uint8_t interrogation[] = {100, 1, 0x86, 0x5A, 0x34, 0x12, 0, 0, 0, 20}; + +/** + * 功能:检查期望报文只改动COT首字节。 + * 输入参数:actual——实际ASDU;original——输入副本;length——长度;cot——期望COT字节。 + * 输出参数:无。 + * 返回值:无,断言失败时终止测试。 + */ +static void check_reply(const uint8_t *actual, const uint8_t *original, size_t length, uint8_t cot) +{ + size_t i; + for (i = 0; i < length; ++i) { CHECK(actual[i] == (i == 2U ? cot : original[i])); } +} + +/** + * 功能:验证12种字段宽度下的原文借用、原地修改和失败原子性。 + * 输入参数:无。 + * 输出参数:无。 + * 返回值:无。 + */ +static void test_mutable_views(void) +{ + int cot, ca, ioa; + for (cot = 1; cot <= 2; ++cot) { + for (ca = 1; ca <= 2; ++ca) { + for (ioa = 1; ioa <= 3; ++ioa) { + IEC60870_AppLayerParameters_t p = {1, 1, cot, 0, ca, ioa, 254}; + uint8_t bytes[16] = {100, 1, 0xC6}, original[16]; + uint16_t length = (uint16_t)(2 + cot + ca + ioa + 1), exported = 0; + const uint8_t *data = NULL; + IEC60870_Asdu_t object, snapshot; + IEC60870_InterrogationCommand_t command; + if (cot == 2) { bytes[3] = 0x5A; } + bytes[2 + cot] = 0x34; + if (ca == 2) { bytes[3 + cot] = 0x12; } + bytes[length - 1U] = 20; + memcpy(original, bytes, sizeof(bytes)); + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, &p, bytes, length) == IEC60870_ASDU_OK); + CHECK(object.buffer == bytes && object.capacity == length && object.is_from_buffer); + CHECK(memcmp(bytes, original, sizeof(bytes)) == 0); + CHECK(IEC60870_Asdu_Set_COT(&object, 7) == IEC60870_ASDU_OK); + check_reply(bytes, original, length, 0xC7); + CHECK(IEC60870_Asdu_Set_Negative(&object, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Get_Data(&object, &data, &exported) == IEC60870_ASDU_OK); + CHECK(data == bytes && exported == length); + check_reply(data, original, length, 0x87); + snapshot = object; + CHECK(IEC60870_Asdu_Set_COT(&object, -1) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Set_COT(&object, 64) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Set_Negative(&object, 2) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, &p, bytes, length - 1U) == IEC60870_ASDU_ERROR); + CHECK(memcmp(&object, &snapshot, sizeof(object)) == 0); + CHECK(IEC60870_InterrogationCommand_Create(&command, 0, 20) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&object, &command.object) == IEC60870_ASDU_ERROR); + CHECK(memcmp(&object, &snapshot, sizeof(object)) == 0); + check_reply(bytes, original, length, 0x87); + CHECK(memcmp(bytes + length, original + length, sizeof(bytes) - length) == 0); + } + } + } + { + /* 未知类型及零对象可原文回显,但不能作为已校验业务对象执行。 */ + IEC60870_AppLayerParameters_t p = {1, 1, 1, 0, 1, 1, 254}; + uint8_t bytes[254] = {200, 0, 0x86, 1}; + IEC60870_Asdu_t object, snapshot; + IEC60870_AsduInfo_t info; + const uint8_t *data = NULL; + uint16_t length = 0; + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, &p, bytes, 4) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Parse(&p, bytes, 4, &info) == IEC60870_ASDU_UNSUPPORTED); + CHECK(IEC60870_Asdu_Set_COT(&object, 44) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Get_Data(&object, &data, &length) == IEC60870_ASDU_OK && length == 4); + CHECK(data == bytes && bytes[2] == 0xAC); + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, &p, bytes, 254) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Get_Data(&object, &data, &length) == IEC60870_ASDU_OK && length == 254); + snapshot = object; + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, &p, bytes, 255) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, NULL, bytes, 4) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, &p, NULL, 4) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Create_From_Buffer(NULL, &p, bytes, 4) == IEC60870_ASDU_ERROR); + CHECK(memcmp(&object, &snapshot, sizeof(object)) == 0); + CHECK(IEC60870_Asdu_Set_COT(NULL, 7) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Set_Negative(NULL, 0) == IEC60870_ASDU_ERROR); + object.is_from_buffer = 2; + CHECK(IEC60870_Asdu_Set_COT(&object, 7) == IEC60870_ASDU_ERROR && bytes[2] == 0xAC); + } +} + +typedef struct { + IEC60870_MasterConnection_t connection; + IEC60870_AppLayerParameters_t parameters; + IEC60870_Asdu_t *seen; + unsigned calls; + bool negative; + bool result; +} TestConnection; + +/* 测试接入只记录分派参数和返回值,用来确认共用层不替代协议实现。 */ +static bool record_asdu(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *object) +{ + TestConnection *test = (TestConnection *)self->object; + CHECK(self == &test->connection); + test->seen = object; + test->calls += 1U; + return test->result; +} +static bool record_con(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *object, bool negative) +{ + TestConnection *test = (TestConnection *)self->object; + test->negative = negative; + test->calls += 10U; + return record_asdu(self, object); +} +static bool record_term(IEC60870_MasterConnection_t *self, IEC60870_Asdu_t *object) +{ + TestConnection *test = (TestConnection *)self->object; + test->calls += 100U; + return record_asdu(self, object); +} +static const IEC60870_AppLayerParameters_t *record_parameters(IEC60870_MasterConnection_t *self) +{ + return &((TestConnection *)self->object)->parameters; +} + +/** + * 功能:验证共用接口的回调分派、布尔返回和非104参数传递。 + * 输入参数:无。 + * 输出参数:无。 + * 返回值:无。 + */ +static void test_common_dispatch(void) +{ + TestConnection test = {0}; + IEC60870_MasterConnection_t empty = {0}; + IEC60870_Asdu_t object = {0}; + test.parameters = (IEC60870_AppLayerParameters_t){1, 1, 1, 0, 1, 2, 100}; + test.connection.object = &test; + test.connection.send_asdu = record_asdu; + test.connection.send_act_con = record_con; + test.connection.send_act_term = record_term; + test.connection.get_application_layer_parameters = record_parameters; + test.result = true; + CHECK(IEC60870_MasterConnection_Send_ASDU(&test.connection, &object)); + CHECK(IEC60870_MasterConnection_Send_ACT_CON(&test.connection, &object, true)); + CHECK(test.negative); + CHECK(IEC60870_MasterConnection_Send_ACT_TERM(&test.connection, &object)); + CHECK(test.calls == 113U && test.seen == &object); + CHECK(IEC60870_MasterConnection_Get_Application_Layer_Parameters(&test.connection) == &test.parameters); + test.result = false; + CHECK(!IEC60870_MasterConnection_Send_ACT_CON(&test.connection, &object, false)); + CHECK(!test.negative && test.calls == 124U); + CHECK(!IEC60870_MasterConnection_Send_ASDU(NULL, &object)); + CHECK(!IEC60870_MasterConnection_Send_ACT_CON(NULL, &object, false)); + CHECK(!IEC60870_MasterConnection_Send_ACT_TERM(NULL, &object)); + CHECK(!IEC60870_MasterConnection_Send_ASDU(&empty, &object)); + CHECK(!IEC60870_MasterConnection_Send_ACT_CON(&empty, &object, false)); + CHECK(!IEC60870_MasterConnection_Send_ACT_TERM(&empty, &object)); + CHECK(IEC60870_MasterConnection_Get_Application_Layer_Parameters(NULL) == NULL); + CHECK(IEC60870_MasterConnection_Get_Application_Layer_Parameters(&empty) == NULL); +} + +static IEC104_Slave_t *service; + +/* 仅测试为了注入底层故障取得池中对象;正常业务测试只调用Slave公开接口。 */ +static void locate_test_server(void) +{ + size_t i; + sut = NULL; + for (i = 0U; i < IEC104_TCP_SERVER_CAPACITY; ++i) { + if (server_pool[i].in_use != 0U) { sut = &server_pool[i].server; break; } + } + CHECK(sut != NULL); +} + +static void service_poll(void) { CHECK(IEC104_Slave_Poll(service, 0, NULL) == IEC104_TCP_OK); } + +static unsigned service_drain(void) +{ + IEC104_SlaveEvent_t event; + IEC104_TcpEventResult_t result; + unsigned count = 0; + while ((result = IEC104_Slave_NextEvent(service, &event)) == IEC104_TCP_EVENT_READY) { ++count; } + CHECK(result == IEC104_TCP_EVENT_EMPTY); + return count; +} + +static void service_open_with_window(uint16_t window) +{ + IEC104_SlaveConfig_t config; + memset(&fake, 0, sizeof(fake)); + fake.now = 1000; + service = IEC104_Slave_Create(); + CHECK(service != NULL); + locate_test_server(); + IEC104_Slave_Config_Default(&config); + config.protocol.link.k = window; + CHECK(IEC104_Slave_Init(service, &config, NULL) == IEC104_TCP_OK); +} + +static void service_open(void) { service_open_with_window(IEC104_TCP_RECORD_CAPACITY); } + +static void service_connect(void) +{ + fake.accept_pending = 1; + service_poll(); + CHECK(service_drain() == 1); + push(start_act, sizeof(start_act)); + service_poll(); + CHECK(service_drain() == 1); +} + +static void service_begin(void) { service_open(); service_connect(); } + +static void service_end(void) +{ + CHECK(IEC104_Slave_Close(service, NULL) == IEC104_TCP_OK); + service_drain(); + CHECK(IEC104_Slave_Destroy(service, NULL) == IEC104_TCP_OK); + service = NULL; +} + +static void receive_request(IEC104_SlaveEvent_t *event, uint16_t ns) +{ + uint8_t input[16]; + CHECK(IEC104_Build_Iframe(input, ns, 0, interrogation, sizeof(interrogation)) == 16); + push(input, sizeof(input)); + service_poll(); + CHECK(IEC104_Slave_NextEvent(service, event) == IEC104_TCP_EVENT_READY); + CHECK(event->transport.type == IEC104_TCP_EVENT_I_FRAME); + CHECK(event->asdu_result == IEC60870_ASDU_OK); +} + +/* 无需手动Init连接或解析ASDU,直接确认;检查完整报文及出队失败的输出保留。 */ +static void test_event_replies(void) +{ + IEC104_SlaveEvent_t event, snapshot; + const IEC60870_AppLayerParameters_t *parameters; + size_t offset; + unsigned i; + static const uint8_t expected_cot[] = {0x87, 0xC7, 0x8A}; + service_begin(); + receive_request(&event, 0); + CHECK(event.asdu.buffer == event.transport.data.i_frame.apdu + IEC104_APCI_LEN); + parameters = IEC60870_MasterConnection_Get_Application_Layer_Parameters(&event.connection); + CHECK(parameters != NULL && parameters->sizeOfCOT == 2 && parameters->sizeOfCA == 2 && + parameters->sizeOfIOA == 3 && parameters->maxSizeOfASDU == 249); + CHECK(parameters == IEC104_Slave_Get_Application_Layer_Parameters(service)); + offset = fake.sent_bytes; + CHECK(IEC60870_MasterConnection_Send_ACT_CON(&event.connection, &event.asdu, false)); + CHECK(IEC60870_MasterConnection_Send_ACT_CON(&event.connection, &event.asdu, true)); + CHECK(IEC60870_MasterConnection_Send_ACT_TERM(&event.connection, &event.asdu)); + CHECK(IEC104_Slave_Get_Last_Submit_Result(&event.connection) == IEC104_TCP_SUBMIT_OK); + CHECK(fake.sent_bytes == offset + 48U); + for (i = 0; i < 3; ++i) { + const uint8_t *frame = fake.output + offset + i * 16U; + CHECK(frame[0] == 0x68 && frame[1] == 14 && frame[2] == 2U * i && frame[3] == 0); + CHECK(frame[4] == 2 && frame[5] == 0); + check_reply(frame + 6, interrogation, 10, expected_cot[i]); + } + check_reply(event.asdu.buffer, interrogation, 10, 0x8A); + memcpy(&snapshot, &event, sizeof(snapshot)); + CHECK(IEC104_Slave_NextEvent(service, &event) == IEC104_TCP_EVENT_EMPTY); + CHECK(memcmp(&snapshot, &event, sizeof(event)) == 0); + CHECK(IEC104_Slave_NextEvent(NULL, &event) == IEC104_TCP_EVENT_ERROR); + CHECK(memcmp(&snapshot, &event, sizeof(event)) == 0); + CHECK(IEC104_Slave_NextEvent(service, NULL) == IEC104_TCP_EVENT_ERROR); + service_end(); +} + +/* 同一自动绑定的连接也可提交业务另行构造的ASDU。 */ +static void test_constructed_asdu(void) +{ + uint8_t buffer[249]; + IEC104_SlaveEvent_t event; + IEC60870_Asdu_t object; + IEC60870_AsduHeader_t header = {3, 0, 0, 0, 1}; + IEC60870_SinglePointInformation_t point; + const uint8_t *data = NULL; + uint16_t length = 0; + service_begin(); + receive_request(&event, 0); + CHECK(IEC60870_Asdu_Create(&object, &slave_parameters, buffer, sizeof(buffer), &header, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Set_COT(&object, 20) == IEC60870_ASDU_OK); + CHECK(!IEC60870_MasterConnection_Send_ASDU(&event.connection, &object)); + CHECK(IEC104_Slave_Get_Last_Submit_Result(&event.connection) == IEC104_TCP_SUBMIT_ERROR); + CHECK(IEC60870_SinglePointInformation_Create(&point, 1, 1, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&object, &point.object) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Get_Data(&object, &data, &length) == IEC60870_ASDU_OK && length == 10); + CHECK(IEC60870_MasterConnection_Send_ASDU(&event.connection, &object)); + CHECK(fake.sent_bytes == 22); + check_reply(fake.output + 12, asdu, 10, 20); + CHECK(memcmp(fake.output + 12, data, length) == 0); + service_end(); +} + +/* HIGH满后的WAIT不接手,复制事件保留已修改字段后可在容量恢复时重试。 */ +static void test_wait_and_retry(void) +{ + IEC104_SlaveEvent_t event, pending; + IEC104_TcpServerStatus_t before, after; + unsigned accepted; + service_begin(); + receive_request(&event, 0); + fake.send_blocked = 1; + for (accepted = 0; accepted < 200; ++accepted) { + if (!IEC60870_MasterConnection_Send_ASDU(&event.connection, &event.asdu)) { break; } + } + CHECK(accepted > 0 && accepted < 200); + CHECK(IEC104_Slave_Get_Last_Submit_Result(&event.connection) == IEC104_TCP_SUBMIT_WAIT); + CHECK(IEC104_Slave_Get_Status(service, &before, NULL) == IEC104_TCP_OK); + CHECK(!IEC60870_MasterConnection_Send_ACT_CON(&event.connection, &event.asdu, true)); + CHECK(IEC104_Slave_Get_Last_Submit_Result(&event.connection) == IEC104_TCP_SUBMIT_WAIT); + check_reply(event.asdu.buffer, interrogation, 10, 0xC7); + CHECK(IEC104_Slave_Get_Status(service, &after, NULL) == IEC104_TCP_OK); + CHECK(before.high_queue.pending_asdu_count == after.high_queue.pending_asdu_count); + CHECK(IEC104_Slave_Copy_Event(&pending, &event)); + memset(&event, 0, sizeof(event)); + fake.send_blocked = 0; + fake.now += 100; + service_poll(); + CHECK(IEC60870_MasterConnection_Send_ACT_CON(&pending.connection, &pending.asdu, true)); + CHECK(IEC104_Slave_Get_Last_Submit_Result(&pending.connection) == IEC104_TCP_SUBMIT_OK); + service_end(); +} + +/* 保留101布局/254长度/NULL拒绝,以及复制自引用接口后的明确失败。 */ +static void test_invalid_inputs(void) +{ + IEC104_SlaveEvent_t event, snapshot, destination, before; + IEC60870_AppLayerParameters_t p = {1, 1, 1, 0, 1, 1, 254}; + IEC60870_Asdu_t object; + uint8_t bytes[254] = {200, 0, 0x86, 1}; + size_t sent; + service_begin(); + receive_request(&event, 0); + snapshot = event; /* 故意模拟错误的按值复制。 */ + CHECK(IEC104_Slave_Get_Last_Submit_Result(NULL) == IEC104_TCP_SUBMIT_ERROR); + sent = fake.sent_bytes; + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, &p, bytes, 4) == IEC60870_ASDU_OK); + CHECK(!IEC60870_MasterConnection_Send_ACT_CON(&event.connection, &object, false)); + CHECK(bytes[2] == 0x87); + CHECK(IEC104_Slave_Get_Last_Submit_Result(&event.connection) == IEC104_TCP_SUBMIT_ERROR); + CHECK(IEC104_Slave_Enqueue_Event(service, &object) == IEC104_TCP_SUBMIT_ERROR); + p = slave_parameters; + p.maxSizeOfASDU = 254; + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, &p, bytes, 254) == IEC60870_ASDU_OK); + CHECK(!IEC60870_MasterConnection_Send_ACT_TERM(&event.connection, &object)); + CHECK(bytes[2] == 0x8A); + CHECK(!IEC60870_MasterConnection_Send_ASDU(&event.connection, NULL)); + CHECK(!IEC60870_MasterConnection_Send_ACT_CON(&event.connection, NULL, false)); + CHECK(!IEC60870_MasterConnection_Send_ACT_TERM(&event.connection, NULL)); + CHECK(IEC104_Slave_Enqueue_Event(service, NULL) == IEC104_TCP_SUBMIT_ERROR); + CHECK(fake.sent_bytes == sent); + CHECK(!IEC60870_MasterConnection_Send_ACT_TERM(&snapshot.connection, &object)); + CHECK(IEC60870_MasterConnection_Get_Application_Layer_Parameters(&snapshot.connection) == NULL); + CHECK(IEC104_Slave_Get_Last_Submit_Result(&snapshot.connection) == IEC104_TCP_SUBMIT_ERROR); + memset(&destination, 0x5A, sizeof(destination)); + memcpy(&before, &destination, sizeof(before)); + CHECK(!IEC104_Slave_Copy_Event(&destination, &snapshot)); + CHECK(!IEC104_Slave_Copy_Event(&destination, NULL)); + CHECK(!IEC104_Slave_Copy_Event(NULL, &event)); + CHECK(memcmp(&destination, &before, sizeof(destination)) == 0); + service_end(); +} + +/* 原事件可以立即复用;普通/不排队提交只依赖独立身份及本次ASDU,接手后不借用输入。 */ +static void test_identity_response_storage(void) +{ + IEC104_SlaveEvent_t event; + IEC104_SlaveEventIdentity_t identity; + IEC60870_Asdu_t object; + uint8_t bytes[sizeof(interrogation)]; + size_t sent; + service_begin(); receive_request(&event, 0); + CHECK(IEC104_Slave_Get_Event_Identity(service, &event, &identity)); + memcpy(bytes, interrogation, sizeof(bytes)); + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, &slave_parameters, bytes, sizeof(bytes)) == IEC60870_ASDU_OK); + memset(&event, 0xCC, sizeof(event)); + CHECK(IEC60870_Asdu_Set_COT(&object, 7) == IEC60870_ASDU_OK); + sent = fake.sent_bytes; + CHECK(IEC104_Slave_Send_Response(service, &identity, &object) == IEC104_TCP_SUBMIT_OK); + CHECK(fake.sent_bytes == sent + 16U); + check_reply(fake.output + sent + 6U, interrogation, 10, 0x87); + CHECK(IEC60870_Asdu_Set_COT(&object, 10) == IEC60870_ASDU_OK); + fake.send_blocked = 1; + sent = fake.sent_bytes; + CHECK(IEC104_Slave_Try_Send_Response(service, &identity, &object) == IEC104_TCP_SUBMIT_OK); + CHECK(fake.sent_bytes == sent); + memset(bytes, 0xCC, sizeof(bytes)); + fake.send_blocked = 0; service_poll(); + CHECK(fake.sent_bytes == sent + 16U); + check_reply(fake.output + sent + 6U, interrogation, 10, 0x8A); + CHECK(IEC104_Slave_Close_Connection_By_Identity(service, &identity, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Close_Connection_By_Identity(service, &identity, NULL) == IEC104_TCP_NOT_CONNECTED); + service_end(); +} + +/* 压缩请求不缩小HIGH容量;普通入口容量与原接口相同,Try遇待发NORMAL不入队/覆盖。 */ +static void test_identity_response_wait(void) +{ + IEC104_SlaveEvent_t event; + IEC104_SlaveEventIdentity_t identity; + IEC104_TcpServerStatus_t before, after; + unsigned accepted; + service_begin(); receive_request(&event, 0); + CHECK(IEC104_Slave_Get_Event_Identity(service, &event, &identity)); + fake.send_blocked = 1; + for (accepted = 0; accepted < 200; ++accepted) { + IEC104_TcpSubmitResult_t result = IEC104_Slave_Send_Response(service, &identity, &event.asdu); + if (result == IEC104_TCP_SUBMIT_WAIT) { break; } + CHECK(result == IEC104_TCP_SUBMIT_OK); + } + CHECK(accepted == 1U + IEC104_TCP_HIGH_CAPACITY / (IEC104_ASDU_HIGH_ENTRY_HEADER_LEN + sizeof(interrogation))); + CHECK(IEC104_Slave_Get_Status(service, &before, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Try_Send_Response(service, &identity, &event.asdu) == IEC104_TCP_SUBMIT_WAIT); + CHECK(IEC104_Slave_Get_Status(service, &after, NULL) == IEC104_TCP_OK); + CHECK(before.high_queue.used_bytes == after.high_queue.used_bytes && + before.connection.pending_send_records == after.connection.pending_send_records); + fake.send_blocked = 0; service_poll(); service_drain(); + CHECK(IEC104_Slave_Send_Response(service, &identity, &event.asdu) == IEC104_TCP_SUBMIT_OK); + service_end(); + + service_begin(); receive_request(&event, 0); + CHECK(IEC104_Slave_Get_Event_Identity(service, &event, &identity)); + CHECK(IEC104_Slave_Enqueue_Event(service, &event.asdu) == IEC104_TCP_SUBMIT_OK); + CHECK(IEC104_Slave_Get_Status(service, &before, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Try_Send_Response(service, &identity, &event.asdu) == IEC104_TCP_SUBMIT_WAIT); + CHECK(IEC104_Slave_Get_Status(service, &after, NULL) == IEC104_TCP_OK); + CHECK(after.high_queue.used_bytes == before.high_queue.used_bytes && + after.normal_queue.used_bytes == before.normal_queue.used_bytes && + after.normal_queue.pending_asdu_count == before.normal_queue.pending_asdu_count); + service_poll(); service_drain(); + CHECK(IEC104_Slave_Try_Send_Response(service, &identity, &event.asdu) == IEC104_TCP_SUBMIT_OK); + service_end(); +} + +/* 紧凑身份入口保留原实例/代次校验、参数拒绝和FATAL语义,不因省去事件而放宽准入。 */ +static void test_identity_response_validation(void) +{ + IEC104_SlaveEvent_t event; + IEC104_SlaveEventIdentity_t identity, invalid; + IEC60870_Asdu_t object; + IEC60870_AppLayerParameters_t parameters = {1, 1, 1, 0, 1, 1, 249}; + uint8_t bytes[] = {200, 0, 3, 1}; + size_t sent; + service_begin(); receive_request(&event, 0); + CHECK(IEC104_Slave_Get_Event_Identity(service, &event, &identity)); + sent = fake.sent_bytes; + CHECK(IEC104_Slave_Send_Response(NULL, &identity, &event.asdu) == IEC104_TCP_SUBMIT_ERROR); + CHECK(IEC104_Slave_Send_Response(service, NULL, &event.asdu) == IEC104_TCP_SUBMIT_ERROR); + CHECK(IEC104_Slave_Try_Send_Response(service, &identity, NULL) == IEC104_TCP_SUBMIT_ERROR); + invalid = identity; invalid.slave = NULL; + CHECK(IEC104_Slave_Send_Response(service, &invalid, &event.asdu) == IEC104_TCP_SUBMIT_ERROR); + invalid = identity; ++invalid.instance_id; + CHECK(IEC104_Slave_Try_Send_Response(service, &invalid, &event.asdu) == IEC104_TCP_SUBMIT_ERROR); + CHECK(IEC104_Slave_Close_Connection_By_Identity(service, &invalid, NULL) == IEC104_TCP_INVALID_ARGUMENT); + invalid = identity; invalid.generation = 0; + CHECK(IEC104_Slave_Send_Response(service, &invalid, &event.asdu) == IEC104_TCP_SUBMIT_ERROR); + CHECK(IEC104_Slave_Close_Connection_By_Identity(service, &invalid, NULL) == IEC104_TCP_INVALID_ARGUMENT); + CHECK(IEC104_Slave_Close_Connection_By_Identity(NULL, &identity, NULL) == IEC104_TCP_INVALID_HANDLE); + CHECK(IEC104_Slave_Close_Connection_By_Identity(service, NULL, NULL) == IEC104_TCP_INVALID_ARGUMENT); + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, ¶meters, bytes, sizeof(bytes)) == IEC60870_ASDU_OK); + CHECK(IEC104_Slave_Send_Response(service, &identity, &object) == IEC104_TCP_SUBMIT_ERROR); + CHECK(IEC104_Slave_Try_Send_Response(service, &identity, &object) == IEC104_TCP_SUBMIT_ERROR); + CHECK(fake.sent_bytes == sent); + sut->queue_initialized = 0; + CHECK(IEC104_Slave_Try_Send_Response(service, &identity, &event.asdu) == IEC104_TCP_SUBMIT_FATAL); + sut->queue_initialized = 1; + push(stop_act, sizeof(stop_act)); service_poll(); service_drain(); + CHECK(IEC104_Slave_Send_Response(service, &identity, &event.asdu) == IEC104_TCP_SUBMIT_ERROR); + CHECK(IEC104_Slave_Try_Send_Response(service, &identity, &event.asdu) == IEC104_TCP_SUBMIT_ERROR); + service_end(); +} + +/* 身份输出独立于事件存储;非法来源、自引用和元数据不得污染输出或复制目标。 */ +static void test_event_identity(void) +{ + IEC104_SlaveEvent_t event, copied, moved; + IEC104_SlaveEventIdentity_t identity, before, copy_identity; + size_t sent; + service_begin(); + receive_request(&event, 0); + sent = fake.sent_bytes; + CHECK(IEC104_Slave_Get_Event_Identity(service, &event, &identity)); + CHECK(identity.slave == service && identity.instance_id != 0U && + identity.generation == event.transport.generation); + before = identity; + CHECK(!IEC104_Slave_Get_Event_Identity(NULL, &event, &identity)); + CHECK(!IEC104_Slave_Get_Event_Identity(service, NULL, &identity)); + CHECK(!IEC104_Slave_Get_Event_Identity(service, &event, NULL)); + moved = event; /* 未修复自引用的按值副本必须拒绝。 */ + CHECK(!IEC104_Slave_Get_Event_Identity(service, &moved, &identity)); + CHECK(memcmp(&identity, &before, sizeof(identity)) == 0); + CHECK(IEC104_Slave_Copy_Event(&copied, &event)); + ++copied.transport.generation; /* 两份连接代次不一致,不能接受或继续传播。 */ + CHECK(!IEC104_Slave_Get_Event_Identity(service, &copied, &identity)); + CHECK(!IEC104_Slave_Copy_Event(&moved, &copied)); + CHECK(memcmp(&identity, &before, sizeof(identity)) == 0); + CHECK(IEC104_Slave_Copy_Event(&copied, &event)); + memset(&event, 0, sizeof(event)); + CHECK(IEC104_Slave_Get_Event_Identity(service, &copied, ©_identity)); + CHECK(copy_identity.slave == identity.slave && copy_identity.instance_id == identity.instance_id && + copy_identity.generation == identity.generation); + CHECK(IEC104_Slave_Check_Request(service, &identity) == IEC104_SLAVE_REQUEST_READY); +#if IEC104_TCP_SERVER_CAPACITY > 1 + { + IEC104_Slave_t *other = IEC104_Slave_Create(); + CHECK(other != NULL); + CHECK(!IEC104_Slave_Get_Event_Identity(other, &copied, &identity)); + CHECK(memcmp(&identity, &before, sizeof(identity)) == 0); + CHECK(IEC104_Slave_Send_Response(other, &identity, &copied.asdu) == IEC104_TCP_SUBMIT_ERROR); + CHECK(IEC104_Slave_Try_Send_Response(other, &identity, &copied.asdu) == IEC104_TCP_SUBMIT_ERROR); + CHECK(IEC104_Slave_Close_Connection_By_Identity(other, &identity, NULL) == IEC104_TCP_INVALID_ARGUMENT); + copy_identity.slave = other; /* 即使局部编号相同,其他服务身份也不能用于当前会话。 */ + CHECK(IEC104_Slave_Check_Request(service, ©_identity) == IEC104_SLAVE_REQUEST_STALE); + CHECK(IEC104_Slave_Destroy(other, NULL) == IEC104_TCP_OK); + } +#endif + CHECK(fake.sent_bytes == sent); + service_end(); +} + +/* 通知FIFO优先于当前传输状态:不能跨过尚未消费的STOP/START继续旧业务。 */ +static void test_request_state(void) +{ + IEC104_SlaveEvent_t event; + IEC104_SlaveEventIdentity_t identity = {0}; + size_t sent; + service_open(); + CHECK(IEC104_Slave_Check_Request(NULL, &identity) == IEC104_SLAVE_REQUEST_ERROR); + CHECK(IEC104_Slave_Check_Request(service, NULL) == IEC104_SLAVE_REQUEST_ERROR); + CHECK(IEC104_Slave_Check_Request(service, &identity) == IEC104_SLAVE_REQUEST_STALE); + fake.accept_pending = 1; + service_poll(); + CHECK(IEC104_Slave_Check_Request(service, &identity) == IEC104_SLAVE_REQUEST_WAIT_EVENTS); + CHECK(IEC104_Slave_NextEvent(service, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.transport.type == IEC104_TCP_EVENT_CONNECTED); + CHECK(IEC104_Slave_Get_Event_Identity(service, &event, &identity)); + CHECK(IEC104_Slave_Check_Request(service, &identity) == IEC104_SLAVE_REQUEST_STALE); + push(start_act, sizeof(start_act)); service_poll(); + CHECK(IEC104_Slave_Check_Request(service, &identity) == IEC104_SLAVE_REQUEST_WAIT_EVENTS); + CHECK(service_drain() == 1); + CHECK(IEC104_Slave_Check_Request(service, &identity) == IEC104_SLAVE_REQUEST_READY); + push(stop_act, sizeof(stop_act)); service_poll(); + CHECK(IEC104_Slave_Check_Request(service, &identity) == IEC104_SLAVE_REQUEST_WAIT_EVENTS); + CHECK(service_drain() == 1); + CHECK(IEC104_Slave_Check_Request(service, &identity) == IEC104_SLAVE_REQUEST_STALE); + push(start_act, sizeof(start_act)); service_poll(); + CHECK(service_drain() == 1); + push(stop_act, sizeof(stop_act)); service_poll(); + push(start_act, sizeof(start_act)); service_poll(); + CHECK(IEC104_Slave_Check_Request(service, &identity) == IEC104_SLAVE_REQUEST_WAIT_EVENTS); + CHECK(IEC104_Slave_NextEvent(service, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.transport.type == IEC104_TCP_EVENT_DEACTIVATED); + CHECK(IEC104_Slave_Check_Request(service, &identity) == IEC104_SLAVE_REQUEST_WAIT_EVENTS); + CHECK(IEC104_Slave_NextEvent(service, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.transport.type == IEC104_TCP_EVENT_ACTIVATED); + sent = fake.sent_bytes; + CHECK(IEC104_Slave_Check_Request(service, &identity) == IEC104_SLAVE_REQUEST_READY); + CHECK(IEC104_Slave_Check_Request(service, &identity) == IEC104_SLAVE_REQUEST_READY); + CHECK(fake.sent_bytes == sent && service_drain() == 0U); + CHECK(IEC104_Slave_Close_Connection(service, &event, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Check_Request(service, &identity) == IEC104_SLAVE_REQUEST_WAIT_EVENTS); + CHECK(service_drain() == 1); + CHECK(IEC104_Slave_Check_Request(service, &identity) == IEC104_SLAVE_REQUEST_STALE); + service_end(); +} + +/* 旧事件的发送和关闭操作均不能作用于新连接,业务无须提取或回传generation。 */ +static void test_stale_generation(void) +{ + IEC104_SlaveEvent_t old_event, current; + IEC104_SlaveEventIdentity_t old_identity, current_identity; + size_t sent; + service_begin(); + receive_request(&old_event, 0); + CHECK(IEC104_Slave_Get_Event_Identity(service, &old_event, &old_identity)); + CHECK(IEC104_Slave_Close_Connection(service, &old_event, NULL) == IEC104_TCP_OK); + service_drain(); + service_connect(); + receive_request(¤t, 0); + CHECK(old_event.transport.generation != current.transport.generation); + CHECK(IEC104_Slave_Get_Event_Identity(service, &old_event, ¤t_identity)); + CHECK(current_identity.generation == old_identity.generation); /* 历史事件仍可识别,但不能继续业务。 */ + CHECK(IEC104_Slave_Check_Request(service, &old_identity) == IEC104_SLAVE_REQUEST_STALE); + CHECK(IEC104_Slave_Get_Event_Identity(service, ¤t, ¤t_identity)); + CHECK(IEC104_Slave_Check_Request(service, ¤t_identity) == IEC104_SLAVE_REQUEST_READY); + sent = fake.sent_bytes; + CHECK(IEC104_Slave_Close_Connection(service, &old_event, NULL) == IEC104_TCP_STALE_GENERATION); + CHECK(IEC104_Slave_Close_Connection_By_Identity(service, &old_identity, NULL) == IEC104_TCP_STALE_GENERATION); + CHECK(IEC104_Slave_Send_Response(service, &old_identity, ¤t.asdu) == IEC104_TCP_SUBMIT_ERROR); + CHECK(IEC104_Slave_Try_Send_Response(service, &old_identity, ¤t.asdu) == IEC104_TCP_SUBMIT_ERROR); + CHECK(!IEC60870_MasterConnection_Send_ACT_CON(&old_event.connection, &old_event.asdu, false)); + CHECK(IEC104_Slave_Get_Last_Submit_Result(&old_event.connection) == IEC104_TCP_SUBMIT_ERROR); + CHECK(fake.sent_bytes == sent && old_event.asdu.buffer[2] == 0x87); + CHECK(IEC60870_MasterConnection_Send_ACT_CON(¤t.connection, ¤t.asdu, false)); + CHECK(fake.sent_bytes == sent + 16U); + service_end(); +} + +static void test_fatal_result(void) +{ + IEC104_SlaveEvent_t event; + service_begin(); + receive_request(&event, 0); + /* 故障注入只在测试中访问底层,不为业务增加内部句柄访问接口。 */ + sut->queue_initialized = 0; + CHECK(!IEC60870_MasterConnection_Send_ACT_CON(&event.connection, &event.asdu, false)); + CHECK(IEC104_Slave_Get_Last_Submit_Result(&event.connection) == IEC104_TCP_SUBMIT_FATAL); + CHECK(event.asdu.buffer[2] == 0x87); + sut->queue_initialized = 1; + service_end(); +} + +/* 源事件被后续请求覆盖后,任务副本仍持有自己的原文与应答接口。 */ +static void test_deferred_event_copy(void) +{ + IEC104_SlaveEvent_t source, pending; + uint64_t original_serial; + service_begin(); + receive_request(&source, 0); + original_serial = source.transport.data.i_frame.receive_serial; + CHECK(IEC60870_Asdu_Set_COT(&source.asdu, 7) == IEC60870_ASDU_OK); + CHECK(IEC104_Slave_Copy_Event(&pending, &source)); + CHECK(IEC104_Slave_Copy_Event(&pending, &pending)); + CHECK(pending.asdu.buffer != source.asdu.buffer); + CHECK(pending.connection.object == &pending); + receive_request(&source, 1); + CHECK(source.transport.data.i_frame.receive_serial != original_serial); + CHECK(pending.transport.data.i_frame.receive_serial == original_serial); + CHECK(source.asdu.buffer[2] == 0x86 && pending.asdu.buffer[2] == 0x87); + CHECK(IEC60870_MasterConnection_Send_ACT_TERM(&pending.connection, &pending.asdu)); + CHECK(source.asdu.buffer[2] == 0x86 && pending.asdu.buffer[2] == 0x8A); + service_end(); +} + +/* 服务重启、销毁和池槽复用后,仍存活的任务副本都不得向新实例发送。 */ +static void test_service_reuse(void) +{ + IEC104_SlaveEvent_t old_event, current; + IEC104_SlaveEventIdentity_t old_identity, identity; + IEC104_SlaveConfig_t config; + IEC104_Slave_t *old_service; + size_t sent; + service_begin(); + receive_request(&old_event, 0); + CHECK(IEC104_Slave_Get_Event_Identity(service, &old_event, &old_identity)); + CHECK(IEC104_Slave_Close(service, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Init(service, NULL, NULL) == IEC104_TCP_INVALID_ARGUMENT); + service_drain(); + IEC104_Slave_Config_Default(&config); + CHECK(IEC104_Slave_Init(service, &config, NULL) == IEC104_TCP_OK); + service_connect(); + receive_request(¤t, 0); + CHECK(IEC104_Slave_Check_Request(service, &old_identity) == IEC104_SLAVE_REQUEST_STALE); + sent = fake.sent_bytes; + CHECK(!IEC60870_MasterConnection_Send_ACT_CON(&old_event.connection, &old_event.asdu, false)); + CHECK(fake.sent_bytes == sent); + CHECK(IEC60870_MasterConnection_Send_ACT_CON(¤t.connection, ¤t.asdu, false)); + old_service = service; + service_end(); + CHECK(!IEC60870_MasterConnection_Send_ACT_TERM(&old_event.connection, &old_event.asdu)); + service_begin(); + CHECK(service == old_service); /* 默认先复用门面第一个槽。 */ + receive_request(¤t, 0); + CHECK(IEC104_Slave_Get_Event_Identity(service, ¤t, &identity)); + CHECK(old_identity.instance_id != identity.instance_id); + CHECK(!IEC104_Slave_Get_Event_Identity(service, &old_event, &identity)); + CHECK(IEC104_Slave_Check_Request(service, &old_identity) == IEC104_SLAVE_REQUEST_STALE); + sent = fake.sent_bytes; + CHECK(!IEC60870_MasterConnection_Send_ACT_CON(&old_event.connection, &old_event.asdu, false)); + CHECK(IEC104_Slave_Close_Connection(service, &old_event, NULL) == IEC104_TCP_INVALID_ARGUMENT); + CHECK(IEC104_Slave_Close_Connection_By_Identity(service, &old_identity, NULL) == IEC104_TCP_INVALID_ARGUMENT); + CHECK(IEC104_Slave_Send_Response(service, &old_identity, ¤t.asdu) == IEC104_TCP_SUBMIT_ERROR); + CHECK(IEC104_Slave_Try_Send_Response(service, &old_identity, ¤t.asdu) == IEC104_TCP_SUBMIT_ERROR); + CHECK(fake.sent_bytes == sent); + CHECK(IEC60870_MasterConnection_Send_ACT_CON(¤t.connection, ¤t.asdu, false)); + service_end(); +} + +/* 两层池共享容量;创建失败不泄漏,Close后的收尾通知不能被Destroy悄悄丢弃。 */ +static void test_lifecycle(void) +{ + IEC104_Slave_t *slots[IEC104_TCP_SERVER_CAPACITY]; + IEC104_TcpServer_t *tcp_slots[IEC104_TCP_SERVER_CAPACITY]; + IEC104_SlaveEvent_t event; + IEC104_TcpError_t error; + IEC104_SlaveConfig_t config; + IEC104_TcpServerStatus_t status; + size_t i; + for (i = 0; i < IEC104_TCP_SERVER_CAPACITY; ++i) { + slots[i] = IEC104_Slave_Create(); + CHECK(slots[i] != NULL); + } + CHECK(IEC104_Slave_Create() == NULL && IEC104_TcpServer_Create() == NULL); + for (i = 0; i < IEC104_TCP_SERVER_CAPACITY; ++i) { + CHECK(IEC104_Slave_Destroy(slots[i], NULL) == IEC104_TCP_OK); + } + for (i = 0; i < IEC104_TCP_SERVER_CAPACITY; ++i) { + tcp_slots[i] = IEC104_TcpServer_Create(); + CHECK(tcp_slots[i] != NULL); + } + CHECK(IEC104_Slave_Create() == NULL); + for (i = 0; i < IEC104_TCP_SERVER_CAPACITY; ++i) { + CHECK(IEC104_TcpServer_Destroy(tcp_slots[i], NULL) == IEC104_TCP_OK); + } + service = IEC104_Slave_Create(); + CHECK(service != NULL); + CHECK(IEC104_Slave_Get_Status(service, &status, NULL) == IEC104_TCP_OK && !status.listening); + IEC104_Slave_Config_Default(&config); + config.protocol.link.k = 0; + CHECK(IEC104_Slave_Init(service, &config, &error) == IEC104_TCP_INVALID_CONFIG); + CHECK(error.result == IEC104_TCP_INVALID_CONFIG && error.stage == IEC104_TCP_STAGE_VALIDATE); + CHECK(IEC104_Slave_Destroy(service, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Destroy(service, &error) == IEC104_TCP_INVALID_HANDLE); + CHECK(error.result == IEC104_TCP_INVALID_HANDLE); + service_begin(); + CHECK(IEC104_Slave_Destroy(service, NULL) == IEC104_TCP_INVALID_STATE); + CHECK(IEC104_Slave_Close(service, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Destroy(service, NULL) == IEC104_TCP_EVENTS_PENDING); + CHECK(IEC104_Slave_NextEvent(service, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.transport.type == IEC104_TCP_EVENT_DISCONNECTED); + CHECK(event.asdu_result == IEC60870_ASDU_UNSUPPORTED && event.connection.send_asdu == NULL); + CHECK(IEC104_Slave_Destroy(service, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Get_Application_Layer_Parameters(NULL) == NULL); +} + +/* 仅通过公开门面接入外部事件循环,取得请求、对端地址和只读状态。 */ +static void test_external_scheduler(void) +{ + IEC104_TcpWaitInfo_t wait; + IEC104_TcpReady_t ready; + IEC104_TcpPeerInfo_t peer; + IEC104_TcpServerStatus_t status; + IEC104_TcpError_t error; + IEC104_SlaveEvent_t event; + uint8_t input[16]; + uint32_t i; + service_begin(); + CHECK(IEC104_Slave_Get_Remote_Address(service, &peer, NULL) == IEC104_TCP_OK); + CHECK(strcmp(peer.address, "127.0.0.1") == 0 && peer.port == 12000); + CHECK(IEC104_Slave_Prepare(service, &wait, NULL) == IEC104_TCP_OK); + memset(&ready, 0, sizeof(ready)); + ready.service_token = wait.service_token; + for (i = 0; i < wait.socket_count; ++i) { + ready.sockets[i] = wait.sockets[i]; + ready.sockets[i].events = IEC104_TCP_IO_READ; + } + ready.socket_count = wait.socket_count; + CHECK(IEC104_Build_Iframe(input, 0, 0, interrogation, sizeof(interrogation)) == 16); + push(input, sizeof(input)); + CHECK(IEC104_Slave_Process(service, &ready, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_NextEvent(service, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.asdu_result == IEC60870_ASDU_OK); + CHECK(IEC60870_MasterConnection_Send_ACT_CON(&event.connection, &event.asdu, false)); + CHECK(IEC104_Slave_Get_Status(service, &status, NULL) == IEC104_TCP_OK); + CHECK(status.connected && status.connection.data_transfer_started); + CHECK(IEC104_Slave_Process(service, NULL, &error) == IEC104_TCP_INVALID_ARGUMENT); + CHECK(error.result == IEC104_TCP_INVALID_ARGUMENT); + CHECK(IEC104_Slave_Prepare(NULL, &wait, &error) == IEC104_TCP_INVALID_HANDLE); + CHECK(error.stage == IEC104_TCP_STAGE_VALIDATE && error.socket_error == 0); + CHECK(IEC104_Slave_Close_Connection(service, NULL, NULL) == IEC104_TCP_INVALID_ARGUMENT); + service_end(); +} + +/* 主动上报只需Slave和ASDU;离线入NORMAL后在连接激活时正常发送。 */ +static void test_offline_enqueue(void) +{ + IEC60870_Asdu_t object; + IEC104_TcpServerStatus_t status; + uint8_t bytes[sizeof(asdu)]; + service_open(); + memcpy(bytes, asdu, sizeof(bytes)); + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, + IEC104_Slave_Get_Application_Layer_Parameters(service), bytes, sizeof(bytes)) == IEC60870_ASDU_OK); + CHECK(IEC104_Slave_Enqueue_Event(service, &object) == IEC104_TCP_SUBMIT_OK); + CHECK(fake.sent_bytes == 0); + CHECK(IEC104_Slave_Get_Status(service, &status, NULL) == IEC104_TCP_OK); + CHECK(status.normal_queue.pending_asdu_count == 1); + service_connect(); + service_poll(); + CHECK(fake.sent_bytes == 22); + CHECK(memcmp(fake.output + 12, asdu, sizeof(asdu)) == 0); + CHECK(IEC104_Slave_Get_Status(service, &status, NULL) == IEC104_TCP_OK); + CHECK(status.normal_queue.pending_asdu_count == 0 && status.normal_queue.stored_asdu_count == 1); + service_end(); +} + +/* 多于原任务池容量的站点共享一个Slave;同IOA的数据源由CA定位到不同context。 */ +static void test_station_registry(void) +{ + IEC104_Station_t stations[13] = {0}, duplicate = {0}, boundary = {0}; + struct { uint32_t ioa; int value; } points[13]; + IEC104_Station_t *current = NULL; + unsigned i; + service = IEC104_Slave_Create(); + CHECK(service != NULL && IEC104_Slave_Get_Station_Count(service) == 0U); + for (i = 0U; i < 13U; ++i) { + points[i].ioa = 100U; + points[i].value = (int)i; + CHECK(IEC104_Slave_Add_Station(service, &stations[i], (uint16_t)(i + 1U), &points[i]) == IEC104_STATION_OK); + } + CHECK(IEC104_Slave_Get_Station_Count(service) == 13U); + for (i = 0U; i < 13U; ++i) { + current = IEC104_Slave_Next_Station(service, current); + CHECK(current == &stations[i]); + CHECK(IEC104_Slave_Get_Station(service, (uint16_t)(i + 1U)) == current); + CHECK(IEC104_Station_Get_Common_Address(current) == i + 1U); + CHECK(IEC104_Station_Get_Context(current) == &points[i]); + } + CHECK(IEC104_Slave_Next_Station(service, current) == NULL); + CHECK(IEC104_Slave_Next_Station(service, &duplicate) == NULL); + CHECK(IEC104_Slave_Add_Station(service, &duplicate, 7U, NULL) == IEC104_STATION_DUPLICATE_CA); + CHECK(IEC104_Station_Get_Common_Address(&duplicate) == 0U); + CHECK(IEC104_Slave_Add_Station(service, &stations[0], 100U, NULL) == IEC104_STATION_ALREADY_REGISTERED); + CHECK(IEC104_Station_Get_Context(&stations[0]) == &points[0]); + CHECK(IEC104_Slave_Add_Station(service, &boundary, 65534U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Get_Station(service, 65534U) == &boundary); + CHECK(IEC104_Slave_Get_Station(service, IEC104_CA_BROADCAST) == NULL); + CHECK(IEC104_Slave_Get_Station(service, 0U) == NULL); + CHECK(IEC104_Slave_Get_Station(service, 100U) == NULL); + CHECK(IEC104_Slave_Remove_Station(service, &stations[6]) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Next_Station(service, &stations[5]) == &stations[7]); + CHECK(IEC104_Slave_Add_Station(service, &duplicate, 7U, &points[6]) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Get_Station(service, 7U) == &duplicate); + CHECK(IEC104_Slave_Next_Station(service, &boundary) == &duplicate); + for (i = 0U; i < 13U; ++i) { + if (i != 6U) { CHECK(IEC104_Slave_Remove_Station(service, &stations[i]) == IEC104_STATION_OK); } + } + CHECK(IEC104_Slave_Remove_Station(service, &boundary) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(service, &duplicate) == IEC104_STATION_OK); + CHECK(IEC104_Station_Get_Common_Address(&duplicate) == 0U && IEC104_Station_Get_Context(&duplicate) == NULL); + CHECK(IEC104_Slave_Get_Station_Count(service) == 0U && IEC104_Slave_Next_Station(service, NULL) == NULL); + CHECK(IEC104_Slave_Destroy(service, NULL) == IEC104_TCP_OK); +} + +/* 引用覆盖延迟业务寿命;不占用其他站点,不允许零计数释放或计数回绕。 */ +static void test_station_references(void) +{ + IEC104_Station_t first = {0}, second = {0}; + service = IEC104_Slave_Create(); + CHECK(service != NULL); + CHECK(IEC104_Slave_Acquire_Station(service, &first) == IEC104_STATION_NOT_FOUND); + CHECK(IEC104_Slave_Release_Station(service, &first) == IEC104_STATION_NOT_FOUND); + CHECK(IEC104_Slave_Remove_Station(service, &first) == IEC104_STATION_NOT_FOUND); + CHECK(IEC104_Slave_Add_Station(service, &first, 1U, &first) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Release_Station(service, &first) == IEC104_STATION_INVALID_ARGUMENT); + CHECK(IEC104_Slave_Acquire_Station(service, &first) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Acquire_Station(service, &first) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(service, &first) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Registered(&first) && !IEC104_Station_Is_Reusable(&first)); + CHECK(IEC104_Slave_Get_Station(service, 1U) == NULL); + CHECK(IEC104_Slave_Get_Station_By_Id(service, IEC104_Station_Get_Id(&first)) == &first); + CHECK(IEC104_Station_Get_Context(&first) == &first); + CHECK(IEC104_Slave_Add_Station(service, &second, 2U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(service, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Release_Station(service, &first) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(service, &first) == IEC104_STATION_NOT_FOUND); + CHECK(!IEC104_Station_Is_Reusable(&first)); + CHECK(IEC104_Slave_Release_Station(service, &first) == IEC104_STATION_OK); + CHECK(IEC104_Station_Is_Reusable(&first)); + CHECK(IEC104_Slave_Add_Station(service, &first, 1U, &first) == IEC104_STATION_OK); + first._users = UINT32_MAX; /* 仅测试注入溢出边界,不需要执行数十亿次Acquire。 */ + CHECK(IEC104_Slave_Acquire_Station(service, &first) == IEC104_STATION_LIMIT); + CHECK(IEC104_Slave_Remove_Station(service, &first) == IEC104_STATION_OK); + first._users = 0U; + CHECK(IEC104_Slave_Acquire_Station(service, &first) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Release_Station(service, &first) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Release_Station(service, &first) == IEC104_STATION_NOT_FOUND); + CHECK(IEC104_Slave_Destroy(service, NULL) == IEC104_TCP_OK); +} + +/* 固定集合允许嵌套读者及站点引用,但在最后一个读者离开前不能增删或销毁。 */ +static void test_station_iteration_lock(void) +{ + IEC104_Station_t first = {0}, second = {0}; + service = IEC104_Slave_Create(); + CHECK(service != NULL); + CHECK(IEC104_Slave_Unlock_Stations(service) == IEC104_STATION_INVALID_ARGUMENT); + CHECK(IEC104_Slave_Lock_Stations(service) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Destroy(service, NULL) == IEC104_TCP_INVALID_STATE); + CHECK(IEC104_Slave_Add_Station(service, &first, 1U, NULL) == IEC104_STATION_BUSY); + CHECK(IEC104_Slave_Unlock_Stations(service) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(service, &first, 1U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Lock_Stations(service) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Lock_Stations(service) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Get_Station(service, 1U) == &first); + CHECK(IEC104_Slave_Next_Station(service, NULL) == &first); + CHECK(IEC104_Slave_Add_Station(service, &second, 2U, NULL) == IEC104_STATION_BUSY); + CHECK(IEC104_Slave_Remove_Station(service, &first) == IEC104_STATION_BUSY); + CHECK(IEC104_Slave_Acquire_Station(service, &first) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Release_Station(service, &first) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Unlock_Stations(service) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(service, &first) == IEC104_STATION_BUSY); + CHECK(IEC104_Slave_Unlock_Stations(service) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(service, &first) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Unlock_Stations(service) == IEC104_STATION_INVALID_ARGUMENT); + CHECK(IEC104_Slave_Destroy(service, NULL) == IEC104_TCP_OK); +} + +/* 站点属于服务,不属于某次TCP连接;关闭/重启保留,销毁前必须显式解除关联。 */ +static void test_station_lifecycle(void) +{ + IEC104_Station_t station = {0}; + IEC104_TcpError_t error; + IEC104_SlaveConfig_t config; + service_begin(); + CHECK(IEC104_Slave_Add_Station(service, &station, 0U, NULL) == IEC104_STATION_INVALID_ARGUMENT); + CHECK(IEC104_Slave_Add_Station(service, &station, IEC104_CA_BROADCAST, NULL) == IEC104_STATION_INVALID_ARGUMENT); + CHECK(IEC104_Slave_Add_Station(NULL, &station, 1U, NULL) == IEC104_STATION_INVALID_ARGUMENT); + CHECK(IEC104_Slave_Add_Station(service, NULL, 1U, NULL) == IEC104_STATION_INVALID_ARGUMENT); + CHECK(IEC104_Slave_Add_Station(service, &station, 1U, &config) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Close(service, NULL) == IEC104_TCP_OK); + service_drain(); + CHECK(IEC104_Slave_Get_Station(service, 1U) == &station); + CHECK(IEC104_Slave_Destroy(service, &error) == IEC104_TCP_INVALID_STATE); + CHECK(error.result == IEC104_TCP_INVALID_STATE && error.stage == IEC104_TCP_STAGE_VALIDATE); + IEC104_Slave_Config_Default(&config); + CHECK(IEC104_Slave_Init(service, &config, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Station_Get_Context(IEC104_Slave_Get_Station(service, 1U)) == &config); + CHECK(IEC104_Slave_Remove_Station(service, &station) == IEC104_STATION_OK); + service_end(); + CHECK(IEC104_Slave_Remove_Station(NULL, &station) == IEC104_STATION_INVALID_ARGUMENT); + CHECK(IEC104_Slave_Acquire_Station(NULL, &station) == IEC104_STATION_INVALID_ARGUMENT); + CHECK(IEC104_Slave_Release_Station(NULL, &station) == IEC104_STATION_INVALID_ARGUMENT); + CHECK(IEC104_Slave_Lock_Stations(NULL) == IEC104_STATION_INVALID_ARGUMENT); + CHECK(IEC104_Slave_Unlock_Stations(NULL) == IEC104_STATION_INVALID_ARGUMENT); + CHECK(IEC104_Slave_Get_Station(NULL, 1U) == NULL && IEC104_Slave_Get_Station_Count(NULL) == 0U); + CHECK(IEC104_Slave_Next_Station(NULL, NULL) == NULL); + CHECK(IEC104_Station_Get_Common_Address(NULL) == 0U && IEC104_Station_Get_Context(NULL) == NULL); +#if IEC104_TCP_SERVER_CAPACITY >= 2U + { + IEC104_Slave_t *other; + IEC104_Station_t foreign = {0}; + service = IEC104_Slave_Create(); + other = IEC104_Slave_Create(); + CHECK(service != NULL && other != NULL); + CHECK(IEC104_Slave_Add_Station(service, &station, 7U, &station) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(other, &station, 8U, NULL) == IEC104_STATION_ALREADY_REGISTERED); + CHECK(IEC104_Slave_Add_Station(other, &foreign, 7U, &foreign) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Get_Station(service, 7U) == &station && IEC104_Slave_Get_Station(other, 7U) == &foreign); + CHECK(IEC104_Slave_Acquire_Station(other, &station) == IEC104_STATION_NOT_FOUND); + CHECK(IEC104_Slave_Release_Station(other, &station) == IEC104_STATION_NOT_FOUND); + CHECK(IEC104_Slave_Remove_Station(other, &station) == IEC104_STATION_NOT_FOUND); + CHECK(IEC104_Slave_Next_Station(other, &station) == NULL); + CHECK(IEC104_Slave_Remove_Station(service, &station) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(other, &foreign) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Destroy(other, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Destroy(service, NULL) == IEC104_TCP_OK); + } +#endif +} + +/* 遍历不冻结增删;旧引用可用,新注册编号不会混入进行中的广播,最后一个快照归还才回收。 */ +static void test_station_snapshot(void) +{ + IEC104_Station_t first = {0}, second = {0}, replacement = {0}, added = {0}; + IEC104_StationSnapshot_t scan = {0}, nested = {0}, copy; + uint64_t old_id; + service = IEC104_Slave_Create(); + CHECK(service != NULL); + CHECK(IEC104_Slave_StationSnapshot_Begin(NULL, &scan) == IEC104_STATION_INVALID_ARGUMENT); + CHECK(IEC104_Slave_StationSnapshot_End(&scan) == IEC104_STATION_INVALID_ARGUMENT); + CHECK(IEC104_Slave_StationSnapshot_Next(&scan) == NULL); + CHECK(IEC104_Slave_Add_Station(service, &first, 1U, &first) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(service, &second, 2U, &second) == IEC104_STATION_OK); + old_id = IEC104_Station_Get_Id(&second); + CHECK(IEC104_Slave_StationSnapshot_Begin(service, &scan) == IEC104_STATION_OK); + CHECK(IEC104_Slave_StationSnapshot_Begin(service, &scan) == IEC104_STATION_INVALID_ARGUMENT); + copy = scan; + CHECK(IEC104_Slave_StationSnapshot_Next(©) == NULL); + CHECK(IEC104_Slave_StationSnapshot_End(©) == IEC104_STATION_INVALID_ARGUMENT); + CHECK(IEC104_Slave_StationSnapshot_Next(&scan) == &first); + CHECK(IEC104_Slave_Remove_Station(service, &second) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&second)); + CHECK(IEC104_Slave_Add_Station(service, &replacement, 2U, &replacement) == IEC104_STATION_OK); + CHECK(IEC104_Station_Get_Id(&replacement) > old_id); + CHECK(IEC104_Slave_Get_Station_By_Id(service, old_id) == &second); + CHECK(IEC104_Slave_Get_Station(service, 2U) == &replacement); + CHECK(IEC104_Slave_Add_Station(service, &added, 3U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Slave_StationSnapshot_Begin(service, &nested) == IEC104_STATION_OK); + CHECK(IEC104_Slave_StationSnapshot_Next(&scan) == NULL); + CHECK(IEC104_Slave_StationSnapshot_End(&scan) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&second)); + CHECK(IEC104_Slave_StationSnapshot_Next(&nested) == &first); + CHECK(IEC104_Slave_StationSnapshot_Next(&nested) == &replacement); + CHECK(IEC104_Slave_StationSnapshot_Next(&nested) == &added); + CHECK(IEC104_Slave_StationSnapshot_Next(&nested) == NULL); + CHECK(IEC104_Slave_StationSnapshot_End(&nested) == IEC104_STATION_OK); + CHECK(IEC104_Station_Is_Reusable(&second) && IEC104_Slave_Get_Station_By_Id(service, old_id) == NULL); + CHECK(IEC104_Slave_Remove_Station(service, &first) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(service, &replacement) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(service, &added) == IEC104_STATION_OK); + CHECK(IEC104_Slave_StationSnapshot_Begin(service, &scan) == IEC104_STATION_OK); + CHECK(IEC104_Slave_StationSnapshot_Next(&scan) == NULL); + CHECK(IEC104_Slave_Destroy(service, NULL) == IEC104_TCP_INVALID_STATE); + CHECK(IEC104_Slave_StationSnapshot_End(&scan) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Destroy(service, NULL) == IEC104_TCP_OK); +} + +/* Rejection helpers retain the real transport, queue and sequence handling. */ +static const uint8_t reject_clock[] = {103, 1, 6, 0x31, 1, 0, 0, 0, 0, 0, 0, 0, 12, 28, 10, 26}; +static const uint8_t reject_read[] = {102, 1, 5, 0x32, 1, 0, 0xe9, 3, 0}; + +static void reject_push(uint16_t ns, uint16_t nr, const uint8_t *bytes, size_t length) +{ + uint8_t frame[IEC104_MAX_APDU_LEN]; + int built = IEC104_Build_Iframe(frame, ns, nr, bytes, (uint16_t)length); + CHECK(built == (int)(length + IEC104_APCI_LEN)); + push(frame, (size_t)built); +} + +static void reject_take(IEC104_SlaveEvent_t *event) +{ + CHECK(IEC104_Slave_NextEvent(service, event) == IEC104_TCP_EVENT_READY); + CHECK(event->transport.type == IEC104_TCP_EVENT_I_FRAME && event->asdu_result == IEC60870_ASDU_OK); +} + +static unsigned reject_output(const uint8_t *frames[], size_t lengths[], unsigned capacity) +{ + size_t offset = 0U; + unsigned count = 0U; + while (offset < fake.sent_bytes) { + const uint8_t *frame = fake.output + offset; + size_t length = frame[1] + 2U; + CHECK(frame[0] == 0x68 && length >= IEC104_APCI_LEN && offset + length <= fake.sent_bytes); + if ((frame[2] & 1U) == 0U) { + CHECK(count < capacity); + frames[count] = frame + IEC104_APCI_LEN; + lengths[count++] = length - IEC104_APCI_LEN; + } + offset += length; + } + return count; +} + +static void reject_settle(void) +{ + unsigned i; + for (i = 0U; i < 8U; ++i) { service_poll(); service_drain(); } +} + +/* Every error cause preserves the request, including unknown object bytes and TEST. */ +static void test_reject_echo(void) +{ + unsigned cause; + for (cause = 44U; cause <= 47U; ++cause) { + IEC104_SlaveEvent_t event, before; + uint8_t request[IEC104_MAX_ASDU_LEN]; + const uint8_t *frames[2]; + size_t lengths[2]; + memset(request, 0x5a, sizeof(request)); + request[0] = 200U; request[1] = 0x82U; request[2] = 0x86U; + service_begin(); + reject_push(0, 0, request, sizeof(request)); service_poll(); reject_take(&event); + /* Seed OK so an implementation that resets last-submit to ERROR is caught. */ + CHECK(IEC60870_MasterConnection_Send_ASDU(&event.connection, &event.asdu)); + before = event; + CHECK(IEC104_Slave_Reject_Request(service, &event, (int)cause) == IEC104_TCP_SUBMIT_OK); + CHECK(memcmp(&event, &before, sizeof(event)) == 0); + CHECK(IEC104_Slave_Get_Last_Submit_Result(&event.connection) == IEC104_TCP_SUBMIT_OK); + CHECK(reject_output(frames, lengths, 2) == 2U && lengths[1] == sizeof(request)); + check_reply(frames[1], request, sizeof(request), (uint8_t)(0xc0U | cause)); + service_end(); + } +} + +static void test_reject_invalid(void) +{ + IEC104_SlaveEvent_t event, before, invalid; + static const int causes[] = {-1, 7, 43, 48, 256}; + size_t sent, i; + service_begin(); receive_request(&event, 0); before = event; sent = fake.sent_bytes; + CHECK(IEC104_Slave_Reject_Request(NULL, &event, 44) == IEC104_TCP_SUBMIT_ERROR); + CHECK(IEC104_Slave_Reject_Request(service, NULL, 44) == IEC104_TCP_SUBMIT_ERROR); + for (i = 0U; i < sizeof(causes) / sizeof(causes[0]); ++i) { + CHECK(IEC104_Slave_Reject_Request(service, &event, causes[i]) == IEC104_TCP_SUBMIT_ERROR); + } + invalid = event; /* Deliberately invalid self-references. */ + CHECK(IEC104_Slave_Reject_Request(service, &invalid, 44) == IEC104_TCP_SUBMIT_ERROR); + CHECK(IEC104_Slave_Copy_Event(&invalid, &event)); + invalid.asdu_result = IEC60870_ASDU_ERROR; + CHECK(IEC104_Slave_Reject_Request(service, &invalid, 44) == IEC104_TCP_SUBMIT_ERROR); + CHECK(IEC104_Slave_Copy_Event(&invalid, &event)); ++invalid._owner_serial; + CHECK(IEC104_Slave_Reject_Request(service, &invalid, 44) == IEC104_TCP_SUBMIT_ERROR); +#if IEC104_TCP_SERVER_CAPACITY > 1 + { + IEC104_Slave_t *other = IEC104_Slave_Create(); + CHECK(other != NULL); + CHECK(IEC104_Slave_Reject_Request(other, &event, 44) == IEC104_TCP_SUBMIT_ERROR); + CHECK(IEC104_Slave_Destroy(other, NULL) == IEC104_TCP_OK); + } +#endif + CHECK(fake.sent_bytes == sent && memcmp(&event, &before, sizeof(event)) == 0); + service_end(); +} + +/* Ordinary unread I events must not block an immediate rejection. */ +static void test_reject_batch(void) +{ + unsigned mode, i; + for (mode = 0U; mode < 3U; ++mode) { + const uint8_t *requests[] = {reject_clock, reject_read}; + size_t sizes[] = {sizeof(reject_clock), sizeof(reject_read)}; + const uint8_t *frames[2]; size_t lengths[2]; + IEC104_SlaveEvent_t event, before; + IEC104_TcpServerStatus_t status; + if (mode == 1U) { requests[0] = reject_read; sizes[0] = sizeof(reject_read); + requests[1] = reject_clock; sizes[1] = sizeof(reject_clock); } + if (mode == 2U) { requests[1] = reject_clock; sizes[1] = sizeof(reject_clock); } + service_begin(); + for (i = 0U; i < 2U; ++i) { reject_push((uint16_t)i, 0, requests[i], sizes[i]); } + service_poll(); + for (i = 0U; i < 2U; ++i) { + reject_take(&event); before = event; + CHECK(IEC104_Slave_Get_Status(service, &status, NULL) == IEC104_TCP_OK); + CHECK(status.pending_state_event_count == 0U && status.pending_event_count == 1U - i); + CHECK(IEC104_Slave_Reject_Request(service, &event, 44) == IEC104_TCP_SUBMIT_OK); + CHECK(memcmp(&event, &before, sizeof(event)) == 0); + } + reject_settle(); + CHECK(reject_output(frames, lengths, 2) == 2U); + for (i = 0U; i < 2U; ++i) { + CHECK(lengths[i] == sizes[i]); check_reply(frames[i], requests[i], sizes[i], 0x6c); + } + CHECK(IEC104_Slave_Get_Status(service, &status, NULL) == IEC104_TCP_OK && status.connected); + service_end(); + } +} + +/* k=1 queues the second response; reusing the source cannot change the saved bytes. */ +static void test_reject_window(void) +{ + IEC104_SlaveEvent_t event; + IEC104_TcpServerStatus_t status; + const uint8_t *frames[2]; size_t lengths[2]; + uint8_t unknown[IEC104_MAX_ASDU_LEN]; + memset(unknown, 0x5a, sizeof(unknown)); + unknown[0] = 200U; unknown[1] = 0U; unknown[2] = 0x86U; + service_open_with_window(1U); service_connect(); + reject_push(0, 0, reject_clock, sizeof(reject_clock)); + reject_push(1, 0, unknown, sizeof(unknown)); service_poll(); + reject_take(&event); CHECK(IEC104_Slave_Reject_Request(service, &event, 44) == IEC104_TCP_SUBMIT_OK); + reject_take(&event); CHECK(IEC104_Slave_Reject_Request(service, &event, 44) == IEC104_TCP_SUBMIT_OK); + memset(&event, 0xa5, sizeof(event)); + CHECK(IEC104_Slave_Get_Status(service, &status, NULL) == IEC104_TCP_OK); + CHECK(status.high_queue.pending_asdu_count == 1U && reject_output(frames, lengths, 2) == 1U); + push_ack(1); reject_settle(); + CHECK(reject_output(frames, lengths, 2) == 2U && lengths[1] == sizeof(unknown)); + check_reply(frames[1], unknown, sizeof(unknown), 0xec); + service_end(); +} + +/* Real HIGH exhaustion returns WAIT, retaining existing entries and the input event. */ +static void test_reject_full(void) +{ + IEC104_SlaveEvent_t event, before_event; + IEC104_TcpServerStatus_t before, after; + unsigned accepted; + uint8_t high_before[IEC104_TCP_HIGH_CAPACITY]; + service_begin(); receive_request(&event, 0); fake.send_blocked = 1; + for (accepted = 0U; accepted < 200U; ++accepted) { + if (!IEC60870_MasterConnection_Send_ASDU(&event.connection, &event.asdu)) { break; } + } + CHECK(accepted > 0U && accepted < 200U); + CHECK(IEC104_Slave_Get_Last_Submit_Result(&event.connection) == IEC104_TCP_SUBMIT_WAIT); + before_event = event; memcpy(high_before, sut->high_bytes, sizeof(high_before)); + CHECK(IEC104_Slave_Get_Status(service, &before, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Reject_Request(service, &event, 44) == IEC104_TCP_SUBMIT_WAIT); + CHECK(memcmp(&event, &before_event, sizeof(event)) == 0); + CHECK(memcmp(high_before, sut->high_bytes, sizeof(high_before)) == 0); + CHECK(IEC104_Slave_Get_Status(service, &after, NULL) == IEC104_TCP_OK && after.connected); + CHECK(before.high_queue.used_bytes == after.high_queue.used_bytes && + before.high_queue.pending_asdu_count == after.high_queue.pending_asdu_count); + fake.send_blocked = 0; reject_settle(); + service_end(); +} + +/* State counts exclude CONNECTED; unread STOP/START blocks an old immediate response. */ +static void test_reject_state_gate(void) +{ + unsigned restart; + for (restart = 0U; restart < 2U; ++restart) { + IEC104_SlaveEvent_t event, before; + IEC104_TcpServerStatus_t status; + const uint8_t *frames[1]; size_t lengths[1]; + service_open(); fake.accept_pending = 1; service_poll(); + CHECK(IEC104_Slave_Get_Status(service, &status, NULL) == IEC104_TCP_OK); + CHECK(status.pending_event_count == 1U && status.pending_state_event_count == 0U); + CHECK(IEC104_Slave_NextEvent(service, &event) == IEC104_TCP_EVENT_READY); + CHECK(IEC104_Slave_Reject_Request(service, &event, 44) == IEC104_TCP_SUBMIT_ERROR); + push(start_act, sizeof(start_act)); service_poll(); + CHECK(IEC104_Slave_Get_Status(service, &status, NULL) == IEC104_TCP_OK); + CHECK(status.pending_state_event_count == 1U); CHECK(service_drain() == 1U); + reject_push(0, 0, reject_clock, sizeof(reject_clock)); push(stop_act, sizeof(stop_act)); + if (restart) { push(start_act, sizeof(start_act)); } + service_poll(); reject_take(&event); before = event; + CHECK(IEC104_Slave_Get_Status(service, &status, NULL) == IEC104_TCP_OK); + CHECK(status.pending_state_event_count == 1U + restart); + CHECK(IEC104_Slave_Reject_Request(service, &event, 44) == IEC104_TCP_SUBMIT_ERROR); + CHECK(memcmp(&event, &before, sizeof(event)) == 0 && reject_output(frames, lengths, 1) == 0U); + CHECK(IEC104_Slave_NextEvent(service, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.transport.type == IEC104_TCP_EVENT_DEACTIVATED); + CHECK(IEC104_Slave_Get_Status(service, &status, NULL) == IEC104_TCP_OK); + CHECK(status.pending_state_event_count == restart); + service_drain(); + if (!restart) { push(start_act, sizeof(start_act)); service_poll(); service_drain(); } + reject_push(1, 0, reject_read, sizeof(reject_read)); service_poll(); reject_take(&event); + CHECK(IEC104_Slave_Reject_Request(service, &event, 44) == IEC104_TCP_SUBMIT_OK); + CHECK(reject_output(frames, lengths, 1) == 1U); + service_end(); + } +} + +/* STOP invalidates queued replies; a repeated START preserves them without a state event. */ +static void test_reject_queued_lifecycle(void) +{ + unsigned stop; + for (stop = 0U; stop < 2U; ++stop) { + IEC104_SlaveEvent_t event; + IEC104_TcpServerStatus_t status; + const uint8_t *frames[2]; size_t lengths[2]; + service_open_with_window(1U); service_connect(); + reject_push(0, 0, reject_clock, sizeof(reject_clock)); + reject_push(1, 0, reject_read, sizeof(reject_read)); service_poll(); + reject_take(&event); CHECK(IEC104_Slave_Reject_Request(service, &event, 44) == IEC104_TCP_SUBMIT_OK); + reject_take(&event); CHECK(IEC104_Slave_Reject_Request(service, &event, 44) == IEC104_TCP_SUBMIT_OK); + push(stop ? stop_act : start_act, sizeof(start_act)); service_poll(); + CHECK(IEC104_Slave_Get_Status(service, &status, NULL) == IEC104_TCP_OK); + CHECK(status.pending_state_event_count == stop && status.high_queue.pending_asdu_count == 1U - stop); + service_drain(); push_ack(1); reject_settle(); + CHECK(reject_output(frames, lengths, 2) == 2U - stop); + if (stop) { + push(start_act, sizeof(start_act)); service_poll(); service_drain(); + reject_push(2, 1, reject_read, sizeof(reject_read)); service_poll(); reject_take(&event); + CHECK(IEC104_Slave_Reject_Request(service, &event, 44) == IEC104_TCP_SUBMIT_OK); + } + CHECK(reject_output(frames, lengths, 2) == 2U); + check_reply(frames[1], reject_read, sizeof(reject_read), 0x6c); + service_end(); + } +} + +static void test_reject_stale(void) +{ + IEC104_SlaveEvent_t old_event, current; + size_t sent; + service_begin(); receive_request(&old_event, 0); + CHECK(IEC104_Slave_Close_Connection(service, &old_event, NULL) == IEC104_TCP_OK); + service_drain(); service_connect(); receive_request(¤t, 0); sent = fake.sent_bytes; + CHECK(IEC104_Slave_Reject_Request(service, &old_event, 44) == IEC104_TCP_SUBMIT_ERROR); + CHECK(fake.sent_bytes == sent); + CHECK(IEC104_Slave_Reject_Request(service, ¤t, 44) == IEC104_TCP_SUBMIT_OK); + service_end(); service_begin(); receive_request(¤t, 0); sent = fake.sent_bytes; + CHECK(IEC104_Slave_Reject_Request(service, &old_event, 44) == IEC104_TCP_SUBMIT_ERROR); + CHECK(fake.sent_bytes == sent); + service_end(); +} + +static void test_reject_fatal(void) +{ + IEC104_SlaveEvent_t event, before; + size_t sent; + service_begin(); receive_request(&event, 0); before = event; sent = fake.sent_bytes; + sut->queue_initialized = 0U; + CHECK(IEC104_Slave_Reject_Request(service, &event, 44) == IEC104_TCP_SUBMIT_FATAL); + CHECK(memcmp(&event, &before, sizeof(event)) == 0 && fake.sent_bytes == sent); + sut->queue_initialized = 1U; /* Restore the injected fault only for cleanup; do not retry. */ + service_end(); +} + +int main(void) +{ + test_mutable_views(); + test_common_dispatch(); + test_event_replies(); + test_constructed_asdu(); + test_wait_and_retry(); + test_invalid_inputs(); + test_identity_response_storage(); + test_identity_response_wait(); + test_identity_response_validation(); + test_event_identity(); + test_request_state(); + test_stale_generation(); + test_fatal_result(); + test_deferred_event_copy(); + test_service_reuse(); + test_lifecycle(); + test_external_scheduler(); + test_offline_enqueue(); + test_station_registry(); + test_station_references(); + test_station_iteration_lock(); + test_station_lifecycle(); + test_station_snapshot(); + test_reject_echo(); + test_reject_invalid(); + test_reject_batch(); + test_reject_window(); + test_reject_full(); + test_reject_state_gate(); + test_reject_queued_lifecycle(); + test_reject_stale(); + test_reject_fatal(); + puts("Slave service: 32 groups passed (replies, identities, stations, lifecycle, backpressure, rejection)"); + return 0; +} diff --git a/IEC60870-Slave/.build/command-v1/before/test/iec104_tcp_event_ring_test.c b/IEC60870-Slave/.build/command-v1/before/test/iec104_tcp_event_ring_test.c new file mode 100644 index 00000000..47b6716c --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/test/iec104_tcp_event_ring_test.c @@ -0,0 +1,865 @@ +/* 变长通知FIFO回归:可控Net后端,无真实等待。 + * 为验证跨尾部和损坏数据的事务性出队,直接包含实现;不要另行链接TcpServer.c或Net后端。 + * TCP_EVENT_RING_BENCHMARK只运行公共接收路径,可用-I指定修改前实现进行同机对照。 */ +#include +#include +#include +#include + +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "%s:%d: %s\n", __func__, __LINE__, #x); exit(1); } } while (0) + +static struct { + uint64_t now; + uint8_t input[65536]; + uint8_t output[65536]; + size_t length, offset, recv_limit, sent_bytes, send_limit; + unsigned accept_pending, recv_calls, send_calls, stop_confirmations; + unsigned snapshot_calls, send_blocked, recv_eof, recv_retry, pending_error; +} fake; +static IEC104_TcpServer_t *sut; +static const uint8_t start_act[] = {0x68, 4, 7, 0, 0, 0}; +#ifndef TCP_EVENT_RING_BENCHMARK +static const uint8_t stop_act[] = {0x68, 4, 0x13, 0, 0, 0}; +static const uint8_t asdu[] = {1, 1, 3, 0, 1, 0, 1, 0, 0, 1}; +#endif + +static IEC104_NetResult_t net_result(IEC104_NetResult_t result, int *error) +{ + if (error != NULL) { *error = 0; } + return result; +} +IEC104_NetResult_t IEC104_Net_Init(int *error) { return net_result(IEC104_NET_OK, error); } +void IEC104_Net_Cleanup(void) {} +int IEC104_Net_Invalid_Argument_Error(void) { return 22; } +IEC104_NetResult_t IEC104_Net_Listen(const char *address, uint16_t port, + IEC104_NetSocket_t *listener, uint16_t *bound_port, int *error) +{ + (void)address; + *listener = 1; + *bound_port = port; + return net_result(IEC104_NET_OK, error); +} +IEC104_NetResult_t IEC104_Net_Accept(IEC104_NetSocket_t listener, + IEC104_NetSocket_t *client, IEC104_NetPeer_t *peer, int *error) +{ + CHECK(listener == 1); + *client = IEC104_NET_INVALID_SOCKET; + if (!fake.accept_pending) { return net_result(IEC104_NET_WOULD_BLOCK, error); } + fake.accept_pending = 0; + *client = 2; + memset(peer, 0, sizeof(*peer)); + memcpy(peer->address, "127.0.0.1", sizeof("127.0.0.1")); + peer->port = 12000; + return net_result(IEC104_NET_OK, error); +} +IEC104_NetIoResult_t IEC104_Net_Recv(IEC104_NetSocket_t socket, void *data, size_t capacity) +{ + IEC104_NetIoResult_t result = {IEC104_NET_WOULD_BLOCK, 0, 0}; + size_t count = fake.length - fake.offset; + CHECK(socket == 2); + ++fake.recv_calls; + if (fake.recv_retry) { --fake.recv_retry; result.result = IEC104_NET_RETRY; return result; } + if (count == 0) { + if (fake.recv_eof) { result.result = IEC104_NET_EOF; } + return result; + } + if (count > capacity) { count = capacity; } + if (fake.recv_limit && count > fake.recv_limit) { count = fake.recv_limit; } + memcpy(data, fake.input + fake.offset, count); + fake.offset += count; + result.result = IEC104_NET_OK; + result.transferred = count; + return result; +} +IEC104_NetResult_t IEC104_Net_Pending_Input( + IEC104_NetSocket_t socket, uint32_t *available, int *error) +{ + CHECK(socket == 2 && available != NULL); + ++fake.snapshot_calls; + if (fake.pending_error) { return net_result(IEC104_NET_ERROR, error); } + *available = (uint32_t)(fake.length - fake.offset); + return net_result(IEC104_NET_OK, error); +} +IEC104_NetIoResult_t IEC104_Net_Send(IEC104_NetSocket_t socket, const void *data, size_t length) +{ + IEC104_NetIoResult_t result = {IEC104_NET_OK, length, 0}; + const uint8_t *bytes = (const uint8_t *)data; + CHECK(socket == 2 && data != NULL && length > 0); + ++fake.send_calls; + if (fake.send_blocked) { result.result = IEC104_NET_WOULD_BLOCK; result.transferred = 0; return result; } + if (fake.send_limit && length > fake.send_limit) { length = fake.send_limit; result.transferred = length; } +#ifndef TCP_EVENT_RING_BENCHMARK + CHECK(fake.sent_bytes + length <= sizeof(fake.output)); + memcpy(fake.output + fake.sent_bytes, data, length); +#endif + fake.sent_bytes += length; + if (length == IEC104_APCI_LEN && bytes[2] == 0x23) { ++fake.stop_confirmations; } + return result; +} +IEC104_NetResult_t IEC104_Net_Wait(IEC104_NetSocket_t listener, IEC104_NetSocket_t client, + uint32_t interest, uint32_t timeout, uint32_t *ready, int *error) +{ + (void)listener; (void)client; (void)timeout; + *ready = 0; + if (fake.accept_pending && (interest & IEC104_NET_EVENT_ACCEPT)) { *ready |= IEC104_NET_EVENT_ACCEPT; } + if (fake.offset < fake.length && (interest & IEC104_NET_EVENT_READ)) { *ready |= IEC104_NET_EVENT_READ; } + if (interest & IEC104_NET_EVENT_WRITE) { *ready |= IEC104_NET_EVENT_WRITE; } + return net_result(IEC104_NET_OK, error); +} +IEC104_NetResult_t IEC104_Net_Close(IEC104_NetSocket_t socket, int *error) +{ + (void)socket; + return net_result(IEC104_NET_OK, error); +} +uint64_t IEC104_Net_Now_Ms(void) { return fake.now; } + +static void push(const uint8_t *bytes, size_t length) +{ + if (fake.offset == fake.length) { fake.offset = fake.length = 0; } + CHECK(fake.length + length <= sizeof(fake.input)); + memcpy(fake.input + fake.length, bytes, length); + fake.length += length; +} +static void push_i(unsigned ns, unsigned nr, uint16_t length) +{ + uint8_t frame[IEC104_MAX_APDU_LEN]; + CHECK(length >= IEC104_APCI_LEN && length <= sizeof(frame)); + memset(frame, 0x5a, sizeof(frame)); + ns &= IEC104_SEQUENCE_MASK; + nr &= IEC104_SEQUENCE_MASK; + frame[0] = 0x68; frame[1] = (uint8_t)(length - 2U); + frame[2] = (uint8_t)(ns << 1); frame[3] = (uint8_t)(ns >> 7); + frame[4] = (uint8_t)(nr << 1); frame[5] = (uint8_t)(nr >> 7); + push(frame, length); +} +static void poll_once(void) { CHECK(IEC104_TcpServer_Poll(sut, 0, NULL) == IEC104_TCP_OK); } +static unsigned drain(void) +{ + unsigned count = 0; + IEC104_TcpEvent_t event; + IEC104_TcpEventResult_t result; + while ((result = IEC104_TcpServer_NextEvent(sut, &event)) == IEC104_TCP_EVENT_READY) { ++count; } + CHECK(result == IEC104_TCP_EVENT_EMPTY); + return count; +} +static void begin(void) +{ + IEC104_TcpServerConfig_t config; + memset(&fake, 0, sizeof(fake)); + fake.now = 1000; + fake.accept_pending = 1; + sut = IEC104_TcpServer_Create(); + CHECK(sut != NULL); + IEC104_TcpServer_Config_Default(&config); + config.protocol.link.k = IEC104_TCP_RECORD_CAPACITY; + CHECK(IEC104_TcpServer_Init(sut, &config, NULL) == IEC104_TCP_OK); + poll_once(); + CHECK(drain() == 1); + push(start_act, sizeof(start_act)); + poll_once(); + CHECK(drain() == 1); + CHECK(sut->connection.link_state.dt_state == IEC104_DT_STARTED); +} +static void end(void) +{ + CHECK(IEC104_TcpServer_Close(sut, NULL) == IEC104_TCP_OK); + drain(); + CHECK(IEC104_TcpServer_Destroy(sut, NULL) == IEC104_TCP_OK); +} + +#ifndef TCP_EVENT_RING_BENCHMARK +static void push_ack(unsigned nr) +{ + uint8_t frame[] = {0x68, 4, 1, 0, 0, 0}; + frame[4] = (uint8_t)(nr << 1); frame[5] = (uint8_t)(nr >> 7); + push(frame, sizeof(frame)); +} +static void send_one(void) +{ + CHECK(IEC104_TcpServer_Send_Response(sut, sut->generation, asdu, sizeof(asdu)) == IEC104_TCP_SUBMIT_OK); +} +static void fill_input(unsigned count, uint16_t length) +{ + unsigned index; + for (index = 0; index < count; ++index) { push_i(index, 0, length); } + for (index = 0; index < count + 16U && !sut->rx_event_pending && + (fake.offset < fake.length || sut->rx_begin != sut->rx_end); ++index) { poll_once(); } + CHECK(sut->client_socket != IEC104_NET_INVALID_SOCKET); +} +static void raw_begin(void) +{ + sut = IEC104_TcpServer_Create(); + CHECK(sut != NULL); + if (sut->generation == 0U) { sut->generation = 1; } +} +static void raw_end(void) +{ + drain(); + CHECK(IEC104_TcpServer_Destroy(sut, NULL) == IEC104_TCP_OK); +} +static IEC104_TcpIFrameEvent_t frame_payload(uint16_t length) +{ + IEC104_TcpIFrameEvent_t frame; + uint16_t index; + memset(&frame, 0, sizeof(frame)); + frame.receive_serial = 123; + frame.apdu_length = length; + frame.send_number = 17; + frame.receive_number = 3; + for (index = 0; index < length; ++index) { frame.apdu[index] = (uint8_t)(index * 7U); } + return frame; +} +static void append_frame(uint16_t length) +{ + IEC104_TcpIFrameEvent_t frame = frame_payload(length); + CHECK(Append_Event(sut, IEC104_TCP_EVENT_I_FRAME, &frame, (uint16_t)(TCP_EVENT_I_PREFIX + length))); +} +static void check_event(const IEC104_TcpEvent_t *actual, const IEC104_TcpEvent_t *expected) +{ + CHECK(actual->type == expected->type && actual->generation == expected->generation); + CHECK(memcmp(&actual->data, &expected->data, sizeof(actual->data)) == 0); +} + +/* 每个可能起点都覆盖短/长I帧、RECORD及所有连接通知,验证头/载荷跨尾、零填充和旧代次。 */ +static void test_wrap_roundtrip(void) +{ + uint32_t index; + unsigned kind; + raw_begin(); + for (index = 0; index < sizeof(sut->event_bytes); ++index) { + for (kind = 0; kind < 9; ++kind) { + IEC104_TcpEvent_t expected, actual; + uint16_t length; + memset(&expected, 0, sizeof(expected)); + expected.generation = sut->generation; + sut->event_head = index; + if (kind < 4) { + static const uint16_t lengths[] = {IEC104_APCI_LEN, 16, 22, IEC104_MAX_APDU_LEN}; + expected.type = IEC104_TCP_EVENT_I_FRAME; + expected.data.i_frame = frame_payload(lengths[kind]); + length = (uint16_t)(TCP_EVENT_I_PREFIX + lengths[kind]); + } else if (kind == 4) { + expected.type = IEC104_TCP_EVENT_RECORD; + expected.data.record.value.frame_serial = 91; + expected.data.record.value.apdu_length = IEC104_MAX_APDU_LEN; + expected.data.record.value.local_send_completed = 1; + expected.data.record.closing = 1; + length = sizeof(expected.data.record); + } else { + static const IEC104_TcpEventType_t types[] = {IEC104_TCP_EVENT_CONNECTED, + IEC104_TCP_EVENT_ACTIVATED, IEC104_TCP_EVENT_DEACTIVATED, IEC104_TCP_EVENT_DISCONNECTED}; + expected.type = types[kind - 5]; + expected.data.connection.peer.generation = expected.generation; + expected.data.connection.peer.port = 2404; + expected.data.connection.status_valid = 1; + length = sizeof(expected.data.connection); + } + CHECK(Append_Event(sut, expected.type, &expected.data, length)); + CHECK(sut->event_used == sizeof(TcpEventHeader_t) + length); + ++sut->generation; + memset(&actual, 0xa5, sizeof(actual)); + CHECK(IEC104_TcpServer_NextEvent(sut, &actual) == IEC104_TCP_EVENT_READY); + check_event(&actual, &expected); + CHECK(sut->event_used == 0 && sut->event_count == 0 && sut->state_event_count == 0); + CHECK(sut->event_head == (index + sizeof(TcpEventHeader_t) + length) % sizeof(sut->event_bytes)); + } + } + raw_end(); +} + +static void test_full_reserves(void) +{ + unsigned index; + IEC104_TcpConnectionEvent_t connection = {0}; + IEC104_TcpRecordEvent_t record = {0}; + IEC104_TcpEvent_t event; + IEC104_TcpServer_t saved; + raw_begin(); + sut->event_head = (uint32_t)sizeof(sut->event_bytes) - 1U; + for (index = 0; index < IEC104_TCP_EVENT_NORMAL_CAPACITY; ++index) { + CHECK(Ordinary_Event_Room(sut, TCP_EVENT_I_MAX_BYTES)); + append_frame(IEC104_MAX_APDU_LEN); + } + CHECK(!Ordinary_Event_Room(sut, TCP_EVENT_I_MIN_BYTES)); + for (index = 0; index < IEC104_TCP_EVENT_STATE_RESERVE; ++index) { + CHECK(State_Event_Room(sut) && Transfer_Event_Room(sut)); + CHECK(Append_Event(sut, IEC104_TCP_EVENT_ACTIVATED, &connection, sizeof(connection))); + } + CHECK(!State_Event_Room(sut) && !Transfer_Event_Room(sut)); + CHECK(!Append_Event(sut, IEC104_TCP_EVENT_DEACTIVATED, &connection, sizeof(connection))); + CHECK(sut->event_used == TCP_EVENT_CAPACITY_BYTES - TCP_EVENT_CLOSE_BYTES); + append_frame(IEC104_MAX_APDU_LEN); + record.closing = 1; + for (index = 0; index < IEC104_TCP_RECORD_CAPACITY; ++index) { + record.value.frame_serial = index + 1U; + CHECK(Append_Event(sut, IEC104_TCP_EVENT_RECORD, &record, sizeof(record))); + } + CHECK(Append_Event(sut, IEC104_TCP_EVENT_DISCONNECTED, &connection, sizeof(connection))); + CHECK(sut->event_used == sizeof(sut->event_bytes) && sut->event_count == IEC104_TCP_EVENT_CAPACITY); + saved = *sut; + CHECK(!Append_Event(sut, IEC104_TCP_EVENT_RECORD, &record, sizeof(record))); + CHECK(memcmp(&saved, sut, sizeof(saved)) == 0); + CHECK(!Can_Accept_Connection(sut)); + for (index = 0; index < IEC104_TCP_EVENT_NORMAL_CAPACITY; ++index) { + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.type == IEC104_TCP_EVENT_I_FRAME); + } + CHECK(!Can_Accept_Connection(sut)); /* 空间已足够,旧启停通知仍阻挡新连接。 */ + for (index = 0; index < IEC104_TCP_EVENT_STATE_RESERVE; ++index) { + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.type == IEC104_TCP_EVENT_ACTIVATED); + } + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.type == IEC104_TCP_EVENT_I_FRAME); + for (index = 0; index < IEC104_TCP_RECORD_CAPACITY; ++index) { + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.type == IEC104_TCP_EVENT_RECORD && event.data.record.closing); + CHECK(event.data.record.value.frame_serial == index + 1U); + } + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.type == IEC104_TCP_EVENT_DISCONNECTED); + CHECK(Can_Accept_Connection(sut)); + raw_end(); +} + +static void test_room_boundaries(void) +{ + unsigned index; + IEC104_TcpConnectionEvent_t connection = {0}; + IEC104_TcpServer_t saved; + raw_begin(); + for (index = 0; index + 1U < IEC104_TCP_EVENT_NORMAL_CAPACITY; ++index) { + append_frame(IEC104_MAX_APDU_LEN); + } + append_frame((uint16_t)(IEC104_MAX_APDU_LEN - TCP_EVENT_CONNECTION_BYTES)); + CHECK(Ordinary_Event_Room(sut, TCP_EVENT_CONNECTION_BYTES)); + CHECK(Can_Accept_Connection(sut)); + CHECK(!Ordinary_Event_Room(sut, TCP_EVENT_I_MAX_BYTES)); + append_frame(IEC104_APCI_LEN); + CHECK(!Can_Accept_Connection(sut)); + raw_end(); + + raw_begin(); + for (index = 0; index < IEC104_TCP_EVENT_NORMAL_CAPACITY; ++index) { + append_frame(IEC104_MAX_APDU_LEN); + } + /* 模拟旧会话收尾项占用普通区以外的空间;即使启停条数为0,也不能挤占新的收尾预留。 */ + while (sut->event_used + TCP_EVENT_CONNECTION_BYTES <= TCP_EVENT_CAPACITY_BYTES - TCP_EVENT_CLOSE_BYTES) { + CHECK(Append_Event(sut, IEC104_TCP_EVENT_DISCONNECTED, &connection, sizeof(connection))); + } + CHECK(sut->state_event_count == 0 && !State_Event_Room(sut) && !Transfer_Event_Room(sut)); + saved = *sut; + CHECK(!Append_Event(sut, IEC104_TCP_EVENT_ACTIVATED, &connection, sizeof(connection))); + CHECK(!Notify_Transfer_State(sut, IEC104_DT_STARTED)); + CHECK(memcmp(sut, &saved, sizeof(saved)) == 0); + raw_end(); +} + +static void expect_error_unchanged(void) +{ + IEC104_TcpServer_t saved = *sut; + IEC104_TcpEvent_t output, original; + memset(&output, 0xa5, sizeof(output)); + original = output; + CHECK(IEC104_TcpServer_NextEvent(sut, &output) == IEC104_TCP_EVENT_ERROR); + CHECK(memcmp(&output, &original, sizeof(output)) == 0); + CHECK(memcmp(sut, &saved, sizeof(saved)) == 0); +} +static void test_invalid_entries(void) +{ + IEC104_TcpServer_t saved; + TcpEventHeader_t header; + unsigned index; + raw_begin(); + sut->event_head = (uint32_t)sizeof(sut->event_bytes) - 3U; + append_frame(16); + saved = *sut; + for (index = 0; index < 14; ++index) { + *sut = saved; + Event_Ring_Read(sut, sut->event_head, &header, sizeof(header)); + switch (index) { + case 0: header.type = (IEC104_TcpEventType_t)99; break; + case 1: header.generation = 0; break; + case 2: header.payload_length = 0; break; + case 3: header.payload_length = UINT16_MAX; break; + case 4: header.type = IEC104_TCP_EVENT_RECORD; break; + case 5: header.type = IEC104_TCP_EVENT_CONNECTED; break; + case 6: --header.payload_length; break; + case 7: sut->event_head = sizeof(sut->event_bytes); break; + case 8: sut->event_used = sizeof(sut->event_bytes) + 1U; break; + case 9: sut->event_count = 0; break; + case 10: sut->event_count = 2; break; + case 11: sut->state_event_count = 1; break; + case 12: --sut->event_used; break; + case 13: ++sut->event_used; break; + } + if (index < 7) { Event_Ring_Write(sut, sut->event_head, &header, sizeof(header)); } + expect_error_unchanged(); + } + for (index = 0; index < 4; ++index) { + static const uint16_t bad_lengths[] = {0, IEC104_APCI_LEN - 1, 17, IEC104_MAX_APDU_LEN + 1}; + *sut = saved; + Event_Ring_Write(sut, Event_Ring_Advance(sut->event_head, + sizeof(header) + offsetof(IEC104_TcpIFrameEvent_t, apdu_length)), &bad_lengths[index], sizeof(uint16_t)); + expect_error_unchanged(); + } + *sut = saved; + raw_end(); + raw_begin(); + { + IEC104_TcpConnectionEvent_t connection = {0}; + CHECK(Append_Event(sut, IEC104_TCP_EVENT_ACTIVATED, &connection, sizeof(connection))); + saved = *sut; + sut->state_event_count = 0; + expect_error_unchanged(); + *sut = saved; + Event_Ring_Read(sut, sut->event_head, &header, sizeof(header)); + header.type = IEC104_TCP_EVENT_CONNECTED; + Event_Ring_Write(sut, sut->event_head, &header, sizeof(header)); + expect_error_unchanged(); + *sut = saved; + } + raw_end(); +} + +static void test_short_capacity(void) +{ + unsigned count = (unsigned)(TCP_EVENT_NORMAL_BYTES / (sizeof(TcpEventHeader_t) + TCP_EVENT_I_PREFIX + 16)); + unsigned index; + IEC104_TcpEvent_t event; + IEC104_TcpWaitInfo_t wait; + IEC104_TcpServerStatus_t status; + begin(); + fill_input(count + 1U, 16); + CHECK(sut->event_count == count && sut->rx_event_pending); + CHECK(IEC104_TcpServer_Prepare(sut, &wait, NULL) == IEC104_TCP_OK); + CHECK(IEC104_TcpServer_Get_Status(sut, &status, NULL) == IEC104_TCP_OK); + CHECK(wait.receive_paused && status.receive_paused && wait.pending_event_count == count); + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.type == IEC104_TCP_EVENT_I_FRAME && event.data.i_frame.send_number == 0); + poll_once(); + CHECK(!sut->rx_event_pending && sut->event_count == count); + for (index = 1; index <= count; ++index) { + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.type == IEC104_TCP_EVENT_I_FRAME && event.data.i_frame.send_number == index); + } + printf("Short APDU capacity: %u queued + one retained in Receiver\n", count); + end(); +} + +static void test_pending_frame_before_record(void) +{ + unsigned index; + IEC104_TcpEvent_t event; + begin(); + send_one(); + fill_input(IEC104_TCP_EVENT_NORMAL_CAPACITY - 1U, IEC104_MAX_APDU_LEN); + push_i(IEC104_TCP_EVENT_NORMAL_CAPACITY - 1U, 0, 16); + poll_once(); + CHECK(Ordinary_Event_Room(sut, TCP_EVENT_RECORD_BYTES)); + CHECK(!Ordinary_Event_Room(sut, TCP_EVENT_I_MAX_BYTES)); + push_i(IEC104_TCP_EVENT_NORMAL_CAPACITY, 1, IEC104_MAX_APDU_LEN); + poll_once(); + CHECK(sut->rx_event_pending && sut->connection.send_manager.record_count == 1); + CHECK(sut->event_count == IEC104_TCP_EVENT_NORMAL_CAPACITY); + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + poll_once(); + poll_once(); + CHECK(!sut->rx_event_pending && sut->connection.send_manager.record_count == 0); + for (index = 1; index <= IEC104_TCP_EVENT_NORMAL_CAPACITY; ++index) { + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.type == IEC104_TCP_EVENT_I_FRAME && event.data.i_frame.send_number == index); + } + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.type == IEC104_TCP_EVENT_RECORD && !event.data.record.closing); + CHECK(event.data.record.value.peer_acknowledged); + end(); +} + +static void test_delayed_receipt(int budget_limited) +{ + unsigned count = budget_limited ? TCP_EVENT_BUDGET : + (unsigned)(TCP_EVENT_NORMAL_BYTES / (sizeof(TcpEventHeader_t) + TCP_EVENT_I_PREFIX + 16)); + unsigned index, seen_state = 0, seen_record = 0; + IEC104_TcpEvent_t event; + begin(); + send_one(); + if (budget_limited) { + for (index = 0; index < count; ++index) { push_i(index, index + 1U == count ? 1U : 0U, 16); } + push(stop_act, sizeof(stop_act)); + poll_once(); + CHECK(sut->event_count == count && sut->connection.send_manager.record_count == 1); + } else { + fill_input(count, 16); + CHECK(!Ordinary_Event_Room(sut, TCP_EVENT_RECORD_BYTES)); + push_ack(1); + push(stop_act, sizeof(stop_act)); + } + poll_once(); + CHECK(sut->state_event_count == 1 && fake.stop_confirmations == 1); + if (!budget_limited) { + CHECK(sut->connection.send_manager.record_count == 1); + do { + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.type == IEC104_TCP_EVENT_I_FRAME); + } while (!Ordinary_Event_Room(sut, TCP_EVENT_RECORD_BYTES)); + poll_once(); + } + CHECK(sut->connection.send_manager.record_count == 0); + while (IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY) { + if (event.type == IEC104_TCP_EVENT_DEACTIVATED) { CHECK(!seen_record); ++seen_state; } + if (event.type == IEC104_TCP_EVENT_RECORD) { CHECK(seen_state == 1); ++seen_record; } + } + CHECK(seen_state == 1 && seen_record == 1); + end(); +} + +static void test_close_with_pending_frame_and_all_records(void) +{ + unsigned index; + uint64_t generation; + IEC104_TcpEvent_t event; + begin(); + generation = sut->generation; + for (index = 0; index < IEC104_TCP_RECORD_CAPACITY; ++index) { send_one(); } + CHECK(sut->connection.send_manager.record_count == IEC104_TCP_RECORD_CAPACITY); + fill_input(IEC104_TCP_EVENT_NORMAL_CAPACITY + 1U, IEC104_MAX_APDU_LEN); + CHECK(sut->rx_event_pending && sut->event_count == IEC104_TCP_EVENT_NORMAL_CAPACITY); + CHECK(IEC104_TcpServer_Close_Connection(sut, generation, NULL) == IEC104_TCP_OK); + CHECK(sut->event_count == IEC104_TCP_EVENT_NORMAL_CAPACITY + IEC104_TCP_EVENT_CLOSE_RESERVE); + for (index = 0; index <= IEC104_TCP_EVENT_NORMAL_CAPACITY; ++index) { + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.type == IEC104_TCP_EVENT_I_FRAME && event.generation == generation); + CHECK(event.data.i_frame.send_number == index && event.data.i_frame.apdu_length == IEC104_MAX_APDU_LEN); + } + for (index = 0; index < IEC104_TCP_RECORD_CAPACITY; ++index) { + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.type == IEC104_TCP_EVENT_RECORD && event.data.record.closing); + CHECK(event.data.record.value.local_send_completed && !event.data.record.value.peer_acknowledged); + } + fake.accept_pending = 1; + poll_once(); + CHECK(sut->generation > generation); + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.type == IEC104_TCP_EVENT_DISCONNECTED && event.generation == generation); + CHECK(event.data.connection.peer.generation == generation); + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + CHECK(event.type == IEC104_TCP_EVENT_CONNECTED && event.generation == sut->generation); + end(); +} + +/* 核对真正写出的完整U帧;调用次数包含WOULD_BLOCK/部分写,不能代替CON数量。 + * 调用方另行断言总字节数,既检查帧内容,也防止多回或少回CON。 */ +static void check_state_confirmation(size_t offset, int started) +{ + const uint8_t expected[] = {0x68, 4, started ? 0x0b : 0x23, 0, 0, 0}; + CHECK(offset <= fake.sent_bytes && sizeof(expected) <= fake.sent_bytes - offset); + CHECK(memcmp(fake.output + offset, expected, sizeof(expected)) == 0); +} + +/* 以下场景没有业务I帧或故障;逐字段检查事件产生时的快照,不能用当前连接状态替代。 + * 主动关闭的DISCONNECTED也保存Close前的ACTIVE阶段,并保留原连接的地址和代次。 */ +static void check_connection_event(const IEC104_TcpEvent_t *event, + IEC104_TcpEventType_t type, uint64_t generation, int started) +{ + CHECK(event->type == type && event->generation == generation); + CHECK(event->data.connection.peer.generation == generation); + CHECK(strcmp(event->data.connection.peer.address, "127.0.0.1") == 0); + CHECK(event->data.connection.peer.port == 12000); + CHECK(event->data.connection.status_valid == 1 && event->data.connection.socket_error == 0); + CHECK(event->data.connection.status.phase == IEC104_TCP_SESSION_ACTIVE); + CHECK(event->data.connection.status.fault == IEC104_TCP_FAULT_NONE); + CHECK(event->data.connection.status.pending_send_records == 0); + CHECK(event->data.connection.status.data_transfer_started == started); +} + +/* 配额只暂缓下一次状态变化;已受理ACT的CON仍发送,通知不能合并或覆盖。 */ +static void test_state_reserve_backpressure(void) +{ + unsigned index; + const int started_before_wait = IEC104_TCP_EVENT_STATE_RESERVE % 2U == 0U; + size_t confirmed_bytes; + uint64_t generation; + IEC104_TcpEvent_t event; + begin(); + generation = sut->generation; + for (index = 0; index < IEC104_TCP_EVENT_STATE_RESERVE; ++index) { + push(index % 2U == 0U ? stop_act : start_act, sizeof(start_act)); + poll_once(); + } + CHECK(sut->state_event_count == IEC104_TCP_EVENT_STATE_RESERVE); + /* 配额耗尽那一轮可能先让出;再推进一次,检查包括begin中START在内的全部CON。 + * 时钟固定且没有I帧,本用例的输出只能由这些6字节CON组成。 */ + poll_once(); + confirmed_bytes = (size_t)(IEC104_TCP_EVENT_STATE_RESERVE + 1U) * IEC104_APCI_LEN; + CHECK(fake.sent_bytes == confirmed_bytes); + check_state_confirmation(0, 1); + for (index = 0; index < IEC104_TCP_EVENT_STATE_RESERVE; ++index) { + check_state_confirmation((size_t)(index + 1U) * IEC104_APCI_LEN, index % 2U != 0U); + } + + /* 下一个ACT要求反向切换。多推进一轮仍不得提前改变状态、占槽或回CON。 */ + push(started_before_wait ? stop_act : start_act, sizeof(start_act)); + poll_once(); + poll_once(); + CHECK(sut->rx_blocked && !sut->rx_event_pending); + CHECK(sut->state_event_count == IEC104_TCP_EVENT_STATE_RESERVE); + CHECK(sut->event_count == IEC104_TCP_EVENT_STATE_RESERVE); + CHECK((sut->connection.link_state.dt_state == IEC104_DT_STARTED) == started_before_wait); + CHECK(fake.sent_bytes == confirmed_bytes); + + /* NextEvent只释放一个槽;下一轮Poll才重试原ACT并补入一条通知。 */ + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + check_connection_event(&event, IEC104_TCP_EVENT_DEACTIVATED, generation, 0); + CHECK(sut->state_event_count == IEC104_TCP_EVENT_STATE_RESERVE - 1U); + CHECK(fake.sent_bytes == confirmed_bytes); + poll_once(); + CHECK(!sut->rx_blocked && !sut->rx_event_pending); + CHECK(sut->state_event_count == IEC104_TCP_EVENT_STATE_RESERVE); + CHECK((sut->connection.link_state.dt_state == IEC104_DT_STARTED) == !started_before_wait); + poll_once(); + CHECK(fake.sent_bytes == confirmed_bytes + IEC104_APCI_LEN); + check_state_confirmation(confirmed_bytes, !started_before_wait); + + /* 旧通知仍逐条在前,刚受理的变化追加到末尾;每条快照保持当时的启停状态。 */ + for (index = 1; index <= IEC104_TCP_EVENT_STATE_RESERVE; ++index) { + int started = index % 2U != 0U; + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + check_connection_event(&event, started ? IEC104_TCP_EVENT_ACTIVATED : IEC104_TCP_EVENT_DEACTIVATED, + generation, started); + } + CHECK(sut->state_event_count == 0 && sut->event_count == 0); + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_EMPTY); + end(); +} + +/* 同状态ACT只确认、不通知;分别从STOPPED和STARTED开始,使空队列和满配额下 + * 两个方向都被覆盖,不依赖STATE_RESERVE的奇偶。STOP先完成上一轮CON,确保处于IDLE。 */ +static void test_repeated_state_act(void) +{ + unsigned initial_started, index; + IEC104_TcpEvent_t event; + for (initial_started = 0; initial_started <= 1U; ++initial_started) { + int started = (int)initial_started; + size_t confirmed_bytes; + uint64_t generation; + begin(); + generation = sut->generation; + if (!started) { + push(stop_act, sizeof(stop_act)); + poll_once(); + poll_once(); + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + check_connection_event(&event, IEC104_TCP_EVENT_DEACTIVATED, generation, 0); + } + CHECK(sut->state_event_count == 0 && sut->event_count == 0); + confirmed_bytes = (started ? 1U : 2U) * IEC104_APCI_LEN; + CHECK(fake.sent_bytes == confirmed_bytes); + + /* 没有未读启停通知时,重复START/STOP各追加一个CON,FIFO保持为空。 */ + push(started ? start_act : stop_act, sizeof(start_act)); + poll_once(); + CHECK(fake.sent_bytes == confirmed_bytes + IEC104_APCI_LEN); + check_state_confirmation(confirmed_bytes, started); + CHECK(!sut->rx_blocked && !sut->rx_event_pending); + CHECK((sut->connection.link_state.dt_state == IEC104_DT_STARTED) == started); + CHECK(sut->state_event_count == 0 && sut->event_count == 0); + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_EMPTY); + + /* 用真实状态变化填满配额;保留全部通知,再发送当前状态对应的重复ACT。 */ + confirmed_bytes = fake.sent_bytes; + for (index = 0; index < IEC104_TCP_EVENT_STATE_RESERVE; ++index) { + started = !started; + push(started ? start_act : stop_act, sizeof(start_act)); + poll_once(); + } + poll_once(); + CHECK(fake.sent_bytes == confirmed_bytes + (size_t)IEC104_TCP_EVENT_STATE_RESERVE * IEC104_APCI_LEN); + CHECK(sut->state_event_count == IEC104_TCP_EVENT_STATE_RESERVE); + confirmed_bytes = fake.sent_bytes; + push(started ? start_act : stop_act, sizeof(start_act)); + poll_once(); + CHECK(fake.sent_bytes == confirmed_bytes + IEC104_APCI_LEN); + check_state_confirmation(confirmed_bytes, started); + CHECK(!sut->rx_blocked && !sut->rx_event_pending); + CHECK((sut->connection.link_state.dt_state == IEC104_DT_STARTED) == started); + CHECK(sut->state_event_count == IEC104_TCP_EVENT_STATE_RESERVE); + CHECK(sut->event_count == IEC104_TCP_EVENT_STATE_RESERVE); + + /* 重复ACT未占槽,也没有替换先前保留的任一通知。 */ + started = (int)initial_started; + for (index = 0; index < IEC104_TCP_EVENT_STATE_RESERVE; ++index) { + started = !started; + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + check_connection_event(&event, started ? IEC104_TCP_EVENT_ACTIVATED : IEC104_TCP_EVENT_DEACTIVATED, + generation, started); + } + CHECK(sut->state_event_count == 0 && sut->event_count == 0); + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_EMPTY); + end(); + } +} + +/* 接入限制要求读完旧启停通知,不要求读完旧DISCONNECTED。 + * 启停与建连/断开共用FIFO才有以下跨代次顺序;改成同步回调时需重新定义跨通道时序。 */ +static void test_generation_state_order(void) +{ + unsigned index; + uint64_t old_generation, new_generation; + IEC104_TcpEvent_t event; + begin(); + old_generation = sut->generation; + for (index = 0; index < IEC104_TCP_EVENT_STATE_RESERVE; ++index) { + push(index % 2U == 0U ? stop_act : start_act, sizeof(start_act)); + poll_once(); + } + poll_once(); + CHECK(sut->state_event_count == IEC104_TCP_EVENT_STATE_RESERVE); + CHECK(IEC104_TcpServer_Close_Connection(sut, old_generation, NULL) == IEC104_TCP_OK); + CHECK(sut->event_count == IEC104_TCP_EVENT_STATE_RESERVE + 1U); + /* 普通区有足够空间,接入被阻止应仅因旧启停通知尚未消费。 */ + CHECK(Ordinary_Event_Room(sut, TCP_EVENT_CONNECTION_BYTES)); + fake.accept_pending = 1; + for (index = 0; index < IEC104_TCP_EVENT_STATE_RESERVE; ++index) { + int started = index % 2U != 0U; + poll_once(); + CHECK(fake.accept_pending == 1 && sut->client_socket == IEC104_NET_INVALID_SOCKET); + CHECK(sut->generation == old_generation); + CHECK(sut->state_event_count == IEC104_TCP_EVENT_STATE_RESERVE - index); + CHECK(sut->event_count == IEC104_TCP_EVENT_STATE_RESERVE - index + 1U); + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + check_connection_event(&event, started ? IEC104_TCP_EVENT_ACTIVATED : IEC104_TCP_EVENT_DEACTIVATED, + old_generation, started); + } + + /* 最后一条启停通知读走后,保留DISCONNECTED也允许新接入和START。 */ + CHECK(sut->state_event_count == 0 && sut->event_count == 1); + poll_once(); + CHECK(fake.accept_pending == 0 && sut->client_socket != IEC104_NET_INVALID_SOCKET); + new_generation = sut->generation; + CHECK(new_generation > old_generation && sut->event_count == 2); + CHECK(sut->connection.link_state.dt_state == IEC104_DT_STOPPED); + push(start_act, sizeof(start_act)); + poll_once(); + CHECK(sut->event_count == 3 && sut->state_event_count == 1); + CHECK(sut->connection.link_state.dt_state == IEC104_DT_STARTED); + + /* 当前连接已STARTED,但CONNECTED快照仍为初始STOPPED,旧事件代次也不改变。 */ + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + check_connection_event(&event, IEC104_TCP_EVENT_DISCONNECTED, old_generation, + IEC104_TCP_EVENT_STATE_RESERVE % 2U == 0U); + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + check_connection_event(&event, IEC104_TCP_EVENT_CONNECTED, new_generation, 0); + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY); + check_connection_event(&event, IEC104_TCP_EVENT_ACTIVATED, new_generation, 1); + CHECK(sut->state_event_count == 0 && sut->event_count == 0); + CHECK(IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_EMPTY); + end(); +} + +static void test_fragmented_receive_and_budget(void) +{ + unsigned index, frames = 0, calls; + const unsigned batch = IEC104_TCP_EVENT_NORMAL_CAPACITY < TCP_EVENT_BUDGET ? + IEC104_TCP_EVENT_NORMAL_CAPACITY : TCP_EVENT_BUDGET; + IEC104_TcpEvent_t event; + begin(); + calls = fake.recv_calls; + for (index = 0; index < batch; ++index) { push_i(index, 0, IEC104_MAX_APDU_LEN); } + poll_once(); + CHECK(fake.recv_calls == calls + 1U && sut->event_count == batch); + CHECK(drain() == batch); + fake.recv_limit = 17; + push_i(batch, 0, IEC104_MAX_APDU_LEN); + for (index = 0; index < IEC104_MAX_APDU_LEN && frames == 0; ++index) { + poll_once(); + while (IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY) { + CHECK(event.type == IEC104_TCP_EVENT_I_FRAME && event.data.i_frame.apdu_length == IEC104_MAX_APDU_LEN); + CHECK(event.data.i_frame.send_number == batch); + ++frames; + } + } + CHECK(frames == 1); + end(); +} + +int main(void) +{ + printf("Sizes: header=%zu normal=%zu state=%zu close=%zu ring=%zu rx=%u combined=%zu server=%zu\n", + sizeof(TcpEventHeader_t), TCP_EVENT_NORMAL_BYTES, TCP_EVENT_STATE_BYTES, TCP_EVENT_CLOSE_BYTES, + TCP_EVENT_CAPACITY_BYTES, (unsigned)IEC104_TCP_RX_CAPACITY, + TCP_EVENT_CAPACITY_BYTES + IEC104_TCP_RX_CAPACITY, sizeof(*sut)); + test_wrap_roundtrip(); + test_full_reserves(); + test_room_boundaries(); + test_invalid_entries(); + test_short_capacity(); + test_pending_frame_before_record(); + test_delayed_receipt(0); + test_delayed_receipt(1); + test_close_with_pending_frame_and_all_records(); + test_state_reserve_backpressure(); + test_repeated_state_act(); + test_generation_state_order(); + test_fragmented_receive_and_budget(); + puts("TCP event ring: 13 cases passed"); + return 0; +} +#else +int main(void) +{ + static const uint16_t lengths[] = {16, IEC104_MAX_APDU_LEN}; + unsigned kind; + printf("Server=%zu rx=%u\n", sizeof(*sut), (unsigned)IEC104_TCP_RX_CAPACITY); + for (kind = 0; kind < sizeof(lengths) / sizeof(lengths[0]); ++kind) { + unsigned batch, index, calls, frames = 0; + const unsigned batches = 200000; + clock_t started; + begin(); + calls = fake.recv_calls; + started = clock(); + for (batch = 0; batch < batches; ++batch) { + unsigned delivered = 0, attempts = 0; + for (index = 0; index < TCP_EVENT_BUDGET; ++index) { + push_i(batch * TCP_EVENT_BUDGET + index, 0, lengths[kind]); + } + do { + poll_once(); + if (sut->client_socket == IEC104_NET_INVALID_SOCKET) { + IEC104_TcpServerStatus_t status; + CHECK(IEC104_TcpServer_Get_Status(sut, &status, NULL) == IEC104_TCP_OK); + fprintf(stderr, "benchmark closed: batch=%u APDU=%u fault=%d\n", batch, lengths[kind], + status.connection.fault); + } + CHECK(sut->client_socket != IEC104_NET_INVALID_SOCKET); + delivered += drain(); + CHECK(++attempts <= TCP_IO_BUDGET); + } while (delivered < TCP_EVENT_BUDGET); + CHECK(delivered == TCP_EVENT_BUDGET); + frames += delivered; + /* 模拟主站等待S确认后再发下一批;给原有调度一次无新输入的发送机会。 */ + poll_once(); + CHECK(drain() == 0); + } + CHECK(frames == batches * TCP_EVENT_BUDGET); + CHECK(fake.recv_calls - calls == 2U * batches); + printf("APDU=%u frames=%u recv=%u us_per_frame=%.3f\n", lengths[kind], frames, + fake.recv_calls - calls, 1000000.0 * (double)(clock() - started) / CLOCKS_PER_SEC / frames); + end(); + } + { + unsigned index, calls, frames = 0; + begin(); + for (index = 0; index < 2U * TCP_EVENT_BUDGET; ++index) { push_i(index, 0, IEC104_MAX_APDU_LEN); } + calls = fake.recv_calls; + for (index = 0; index < TCP_IO_BUDGET && frames < 2U * TCP_EVENT_BUDGET; ++index) { + poll_once(); + frames += drain(); + } + CHECK(frames == 2U * TCP_EVENT_BUDGET); + printf("Burst: APDU=%u frames=%u recv=%u\n", IEC104_MAX_APDU_LEN, frames, fake.recv_calls - calls); + end(); + } + return 0; +} +#endif diff --git a/IEC60870-Slave/.build/command-v1/before/test/iec104_tcp_scheduler_test.c b/IEC60870-Slave/.build/command-v1/before/test/iec104_tcp_scheduler_test.c new file mode 100644 index 00000000..8ecf239e --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/test/iec104_tcp_scheduler_test.c @@ -0,0 +1,353 @@ +/* 调度回归复用可控Net后端;不另行链接TcpServer.c、Net或字节环测试的翻译单元。 */ +#define main event_ring_suite_main +#include "iec104_tcp_event_ring_test.c" +#undef main + +static IEC104_TcpServerStatus_t scheduler_status(void) +{ + IEC104_TcpServerStatus_t status; + CHECK(IEC104_TcpServer_Get_Status(sut, &status, NULL) == IEC104_TCP_OK); + CHECK(status.connection_status_valid); + return status; +} + +static void scheduler_begin(uint32_t t1, uint32_t local_timeout) +{ + IEC104_TcpServerConfig_t config; + memset(&fake, 0, sizeof(fake)); + fake.now = 1000; + fake.accept_pending = 1; + sut = IEC104_TcpServer_Create(); + CHECK(sut != NULL); + IEC104_TcpServer_Config_Default(&config); + config.protocol.link.t1_ms = t1; + config.protocol.link.t2_ms = 50; + config.protocol.local_send_timeout_ms = local_timeout; + config.protocol.stop_timeout_ms = 100; + CHECK(IEC104_TcpServer_Init(sut, &config, NULL) == IEC104_TCP_OK); + poll_once(); + CHECK(drain() == 1); + push(start_act, sizeof(start_act)); + poll_once(); + CHECK(drain() == 1 && fake.sent_bytes == sizeof(start_act)); +} + +static void scheduler_send_one(void) +{ + CHECK(IEC104_TcpServer_Enqueue_Event(sut, asdu, sizeof(asdu)) == IEC104_TCP_SUBMIT_OK); + poll_once(); + CHECK(fake.sent_bytes == sizeof(start_act) + IEC104_APCI_LEN + sizeof(asdu)); +} + +static void check_connected(void) +{ + IEC104_TcpServerStatus_t status = scheduler_status(); + CHECK(status.connected && status.connection.fault == IEC104_TCP_FAULT_NONE); +} + +static void check_fault(IEC104_TcpFault_t fault) +{ + IEC104_TcpServerStatus_t status = scheduler_status(); + CHECK(!status.connected && status.connection.fault == fault); + CHECK(!sut->rx_snapshot_active && sut->rx_snapshot_remaining == 0); + CHECK(!sut->connection.receive_batch_active && !sut->connection.receive_batch_pending); +} + +static void check_immediate_continuation(void) +{ + IEC104_TcpWaitInfo_t info; + CHECK(IEC104_TcpServer_Prepare(sut, &info, NULL) == IEC104_TCP_OK); + CHECK(info.need_process && info.timer_active && info.timeout_ms == 0); + CHECK(!sut->connection.receive_batch_active && sut->connection.receive_batch_pending); +} + +/* 每轮仍最多交付8个I事件;ACK可能在同一recv尾部或任意分段后的内核队列中。 */ +static void test_ack_boundary(unsigned preceding_i, size_t recv_limit, uint16_t apdu_length) +{ + unsigned index, frames = 0, records = 0; + IEC104_TcpEvent_t event; + scheduler_begin(100, 5000); + scheduler_send_one(); + for (index = 0; index < preceding_i; ++index) { push_i(index, 0, apdu_length); } + push_ack(1); + fake.recv_limit = recv_limit; + fake.now = 1100; + for (index = 0; index < 2000 && !records; ++index) { + unsigned round_frames = 0; + poll_once(); + check_connected(); + if (sut->rx_snapshot_active) { check_immediate_continuation(); } + while (IEC104_TcpServer_NextEvent(sut, &event) == IEC104_TCP_EVENT_READY) { + if (event.type == IEC104_TCP_EVENT_I_FRAME) { + CHECK(event.data.i_frame.send_number == frames); + CHECK(event.data.i_frame.apdu_length == apdu_length); + ++frames; + ++round_frames; + } else { + CHECK(event.type == IEC104_TCP_EVENT_RECORD && !event.data.record.closing); + CHECK(event.data.record.value.local_send_completed && event.data.record.value.peer_acknowledged); + ++records; + } + } + CHECK(round_frames <= TCP_EVENT_BUDGET); + } + CHECK(frames == preceding_i && records == 1); + CHECK(!sut->rx_snapshot_active && !sut->connection.receive_batch_pending); + CHECK(fake.offset == fake.length && sut->rx_end == 0); + end(); +} + +/* 快照之外的新流量不能延长旧ACK期限;覆盖用户态尾部与跨多次recv的内核快照。 */ +static void test_fixed_snapshot(size_t recv_limit, int control_only) +{ + unsigned index, rounds = 0, old_count = control_only ? 3U * TCP_IO_BUDGET : 33U; + unsigned next_sequence = old_count, snapshots; + scheduler_begin(100, 5000); + scheduler_send_one(); + snapshots = fake.snapshot_calls; + for (index = 0; index < old_count; ++index) { + if (control_only) { push_ack(0); } else { push_i(index, 0, 16); } + } + fake.recv_limit = recv_limit; + fake.now = 1100; + poll_once(); + check_connected(); + check_immediate_continuation(); + CHECK(fake.snapshot_calls == snapshots + 1); + snapshots = fake.snapshot_calls; + drain(); + while (sut->client_socket != IEC104_NET_INVALID_SOCKET && rounds++ < old_count + 10U) { + for (index = 0; index < 10; ++index) { + if (control_only) { push_ack(0); } else { push_i(next_sequence++, 0, 16); } + } + poll_once(); + drain(); + } + check_fault(IEC104_TCP_FAULT_I_ACK_TIMEOUT); + CHECK(fake.snapshot_calls == snapshots && fake.offset < fake.length); + end(); +} + +static void test_missing_ack(void) +{ + scheduler_begin(100, 5000); + scheduler_send_one(); + fake.now = 1100; + poll_once(); + check_fault(IEC104_TCP_FAULT_I_ACK_TIMEOUT); + end(); +} + +static void test_writable_stream(size_t send_limit) +{ + unsigned round, index; + size_t written; + scheduler_begin(1000, 100); + fake.send_limit = send_limit; + written = fake.sent_bytes; + for (round = 0; round < 8; ++round) { + fake.now = 1000 + round * 50U; + for (index = 0; index < TCP_EVENT_BUDGET; ++index) { + push_i(round * TCP_EVENT_BUDGET + index, 0, 16); + } + poll_once(); + check_connected(); + CHECK(drain() == TCP_EVENT_BUDGET); + CHECK(fake.sent_bytes > written); + written = fake.sent_bytes; + } + end(); +} + +static void test_business_output_after_yield(void) +{ + unsigned index; + size_t written; + scheduler_begin(1000, 100); + for (index = 0; index < TCP_EVENT_BUDGET; ++index) { push_i(index, 0, 16); } + CHECK(IEC104_TcpServer_Enqueue_Event(sut, asdu, sizeof(asdu)) == IEC104_TCP_SUBMIT_OK); + written = fake.sent_bytes; + poll_once(); + check_connected(); + CHECK(fake.sent_bytes >= written + IEC104_APCI_LEN + sizeof(asdu)); + CHECK(sut->connection.send_manager.record_count == 1); + end(); +} + +static void test_blocked_output(int recover) +{ + unsigned index, calls; + size_t written; + scheduler_begin(1000, 100); + written = fake.sent_bytes; + fake.send_blocked = 1; + for (index = 0; index < TCP_EVENT_BUDGET; ++index) { push_i(index, 0, 16); } + calls = fake.send_calls; + poll_once(); + check_connected(); + CHECK(fake.send_calls == calls + 1 && fake.sent_bytes == written); + drain(); + if (recover) { + fake.now = 1050; + fake.send_blocked = 0; + poll_once(); + check_connected(); + CHECK(fake.sent_bytes > written); + } else { + fake.now = 1100; + for (index = 0; index < 9; ++index) { push_i(TCP_EVENT_BUDGET + index, 0, 16); } + poll_once(); + check_fault(IEC104_TCP_FAULT_LOCAL_SEND_TIMEOUT); + } + end(); +} + +static void test_testfr_boundary(int provide_confirmation) +{ + static const uint8_t test_con[] = {0x68, 4, 0x83, 0, 0, 0}; + unsigned index; + scheduler_begin(100, 5000); + /* 等到t3,真实推进TESTFR_ACT发送并启动探测t1。 */ + fake.now += sut->config.protocol.link.t3_ms; + poll_once(); + CHECK(sut->connection.testfr_timer_active); + for (index = 0; index < 9; ++index) { push_i(index, 0, 16); } + if (provide_confirmation) { push(test_con, sizeof(test_con)); } + fake.now += 100; + poll_once(); + check_connected(); + check_immediate_continuation(); + drain(); + poll_once(); + if (provide_confirmation) { + check_connected(); + CHECK(!sut->connection.testfr_timer_active); + } else { + check_fault(IEC104_TCP_FAULT_TESTFR_TIMEOUT); + } + end(); +} + +/* 原有容量策略:真正空间不足仍暂停接收,不用预算续收标志掩盖严格期限。 */ +static void test_real_backpressure(int state_full) +{ + unsigned index; + unsigned count = (unsigned)(TCP_EVENT_NORMAL_BYTES / + (sizeof(TcpEventHeader_t) + TCP_EVENT_I_PREFIX + 16)); + scheduler_begin(100, 5000); + scheduler_send_one(); + if (state_full) { + for (index = 0; index < IEC104_TCP_EVENT_STATE_RESERVE; ++index) { + push(index % 2 == 0 ? stop_act : start_act, sizeof(start_act)); + poll_once(); + } + push(IEC104_TCP_EVENT_STATE_RESERVE % 2 == 0 ? stop_act : start_act, sizeof(start_act)); + } else { + for (index = 0; index < count; ++index) { push_i(index, 0, 16); poll_once(); } + push_i(count, 0, 16); + } + push_ack(1); + poll_once(); + check_connected(); + CHECK(state_full ? sut->rx_blocked : sut->rx_event_pending); + CHECK(!sut->connection.receive_batch_pending); + fake.now = 1100; + poll_once(); + check_fault(IEC104_TCP_FAULT_I_ACK_TIMEOUT); + end(); +} + +static void test_response_during_continuation(void) +{ + scheduler_begin(100, 5000); + scheduler_send_one(); + push_i(0, 0, 16); + push_ack(1); + fake.recv_limit = 16; + fake.now = 1100; + poll_once(); + check_immediate_continuation(); + CHECK(drain() == 1); + CHECK(IEC104_TcpServer_Send_Response(sut, sut->generation, asdu, sizeof(asdu)) == IEC104_TCP_SUBMIT_OK); + poll_once(); + check_connected(); + CHECK(drain() == 1); + push_ack(2); + fake.now = 1200; + poll_once(); + check_connected(); + CHECK(drain() == 1); + end(); +} + +static void test_invalid_ack(void) +{ + unsigned index; + scheduler_begin(100, 5000); + scheduler_send_one(); + for (index = 0; index < TCP_EVENT_BUDGET; ++index) { push_i(index, 0, 16); } + push_ack(2); + fake.now = 1100; + poll_once(); + check_immediate_continuation(); + drain(); + poll_once(); + check_fault(IEC104_TCP_FAULT_PROTOCOL); + end(); +} + +static void test_snapshot_failure(void) +{ + scheduler_begin(100, 5000); + push_i(0, 0, 16); + fake.pending_error = 1; + poll_once(); + check_fault(IEC104_TCP_FAULT_TRANSPORT); + end(); +} + +static void test_eof_response(void) +{ + scheduler_begin(100, 5000); + push_i(0, 0, 16); + fake.recv_eof = 1; + poll_once(); + check_connected(); + CHECK(!sut->rx_eof && drain() == 1); + CHECK(IEC104_TcpServer_Send_Response(sut, sut->generation, asdu, sizeof(asdu)) == IEC104_TCP_SUBMIT_OK); + CHECK(fake.sent_bytes == sizeof(start_act) + IEC104_APCI_LEN + sizeof(asdu)); + poll_once(); + CHECK(!scheduler_status().connected); + end(); +} + +int main(void) +{ + test_ack_boundary(7, 0, 16); + test_ack_boundary(8, 0, 16); + test_ack_boundary(9, 0, 16); + test_ack_boundary(33, 0, 16); + test_ack_boundary(1, 16, 16); + test_ack_boundary(1, 1, 16); + test_ack_boundary(16, 0, IEC104_MAX_APDU_LEN); + test_ack_boundary(33, 17, IEC104_MAX_APDU_LEN); + test_fixed_snapshot(0, 0); + test_fixed_snapshot(16, 0); + test_fixed_snapshot(0, 1); + test_missing_ack(); + test_writable_stream(0); + test_writable_stream(2); + test_business_output_after_yield(); + test_blocked_output(1); + test_blocked_output(0); + test_testfr_boundary(1); + test_testfr_boundary(0); + test_real_backpressure(0); + test_real_backpressure(1); + test_response_during_continuation(); + test_invalid_ack(); + test_snapshot_failure(); + test_eof_response(); + puts("TCP scheduler: 25 cases passed (including strict backpressure policy checks)"); + return 0; +} diff --git a/IEC60870-Slave/.build/command-v1/before/test/iec60870_asdu_extended_test.c b/IEC60870-Slave/.build/command-v1/before/test/iec60870_asdu_extended_test.c new file mode 100644 index 00000000..9a65c0a8 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/test/iec60870_asdu_extended_test.c @@ -0,0 +1,604 @@ +/* 新增37种类型的独立报文字节向量及边界测试。 + * 期望字节手工给出,不通过生产编码器计算;联合体仅用于测试存储。 */ +#include "IEC60870_Asdu.h" +#include +#include +#include + +#define CHECK(c) do { if (!(c)) { fprintf(stderr, "%s:%d: %s\n", __func__, __LINE__, #c); return 0; } } while (0) +#define COUNT(a) (sizeof(a) / sizeof((a)[0])) +#define SHORT_TIME_BYTES 0xD5, 0xDD, 0xA2 +#define LONG_TIME_BYTES 0xD5, 0xDD, 0xA2, 0x8C, 0x9D, 0x02, 0x18 + +static const IEC60870_CP24Time2a_t short_time = {56789, 34, 1}; +static const IEC60870_CP56Time2a_t long_time = {{2024,2,29,12,34,56,789,4},1,1,1}; +static const IEC60870_CP16Time2a_t elapsed = {0xBEEF}; +static const IEC60870_BinaryCounterReading_t counter = {INT32_MIN,21,1,1,1}; +static const uint8_t segment_data[] = {0xDE,0xAD,0xBE,0xEF}; +/* 公共测试显式提供布局,不引入104传输头文件。 */ +static const IEC60870_AppLayerParameters_t default_parameters = {1, 1, 2, 0, 2, 3, 249}; + +typedef union { + IEC60870_StepPositionInformation_t v5; + IEC60870_BitString32_t v7; + IEC60870_SinglePointWithCP24Time2a_t v2; + IEC60870_DoublePointWithCP24Time2a_t v4; + IEC60870_StepPositionWithCP24Time2a_t v6; + IEC60870_Bitstring32WithCP24Time2a_t v8; + IEC60870_MeasuredValueNormalizedWithCP24Time2a_t v10; + IEC60870_MeasuredValueScaledWithCP24Time2a_t v12; + IEC60870_MeasuredValueShortWithCP24Time2a_t v14; + IEC60870_IntegratedTotalsWithCP24Time2a_t v16; + IEC60870_StepPositionWithCP56Time2a_t v32; + IEC60870_Bitstring32WithCP56Time2a_t v33; + IEC60870_EventOfProtectionEquipment_t v17; + IEC60870_PackedStartEventsOfProtectionEquipment_t v18; + IEC60870_PackedOutputCircuitInfo_t v19; + IEC60870_EventOfProtectionEquipmentWithCP56Time2a_t v38; + IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_t v39; + IEC60870_PackedOutputCircuitInfoWithCP56Time2a_t v40; + IEC60870_PackedSinglePointWithSCD_t v20; + IEC60870_MeasuredValueNormalizedWithoutQuality_t v21; + IEC60870_Bitstring32Command_t v51; + IEC60870_Bitstring32CommandWithCP56Time2a_t v64; + IEC60870_EndOfInitialization_t v70; + IEC60870_TestCommand_t v104; + IEC60870_DelayAcquisitionCommand_t v106; + IEC60870_ParameterNormalizedValue_t v110; + IEC60870_ParameterScaledValue_t v111; + IEC60870_ParameterFloatValue_t v112; + IEC60870_ParameterActivation_t v113; + IEC60870_FileReady_t v120; + IEC60870_SectionReady_t v121; + IEC60870_FileCallOrSelect_t v122; + IEC60870_FileACK_t v124; + IEC60870_FileLastSegmentOrSection_t v123; + IEC60870_FileSegment_t v125; + IEC60870_FileDirectory_t v126; + IEC60870_QueryLog_t v127; +} Object; + +typedef struct { + uint8_t type, length, single, sequence; + uint8_t payload[16]; +} Vector; + +static const Vector vectors[] = { + {2, 4, 0, 1, {0xF1, SHORT_TIME_BYTES}}, + {4, 4, 0, 1, {0x93, SHORT_TIME_BYTES}}, + {5, 2, 0, 1, {0xFE, 0x81}}, + {6, 5, 0, 1, {0xFE, 0x81, SHORT_TIME_BYTES}}, + {7, 5, 0, 1, {0xEF, 0xCD, 0xAB, 0x89, 0x51}}, + {8, 8, 0, 1, {0xEF, 0xCD, 0xAB, 0x89, 0x51, SHORT_TIME_BYTES}}, + {10, 6, 0, 1, {0, 0x80, 0x81, SHORT_TIME_BYTES}}, + {12, 6, 0, 1, {0xFE, 0xFF, 0x10, SHORT_TIME_BYTES}}, + {14, 8, 0, 1, {0, 0, 0xC0, 0x3F, 0x41, SHORT_TIME_BYTES}}, + {16, 8, 0, 1, {0, 0, 0, 0x80, 0xF5, SHORT_TIME_BYTES}}, + {17, 6, 0, 1, {0x9A, 0xEF, 0xBE, SHORT_TIME_BYTES}}, + {18, 7, 0, 1, {0x21, 0x98, 0xEF, 0xBE, SHORT_TIME_BYTES}}, + {19, 7, 0, 1, {9, 0x98, 0xEF, 0xBE, SHORT_TIME_BYTES}}, + {20, 5, 0, 1, {0x34, 0x12, 0xCD, 0xAB, 0x81}}, + {21, 2, 0, 1, {0, 0x80}}, + {32, 9, 0, 1, {0xFE, 0x81, LONG_TIME_BYTES}}, + {33, 12, 0, 1, {0xEF, 0xCD, 0xAB, 0x89, 0x51, LONG_TIME_BYTES}}, + {38, 10, 0, 1, {0x9A, 0xEF, 0xBE, LONG_TIME_BYTES}}, + {39, 11, 0, 1, {0x21, 0x98, 0xEF, 0xBE, LONG_TIME_BYTES}}, + {40, 11, 0, 1, {9, 0x98, 0xEF, 0xBE, LONG_TIME_BYTES}}, + {51, 4, 1, 0, {0xEF, 0xCD, 0xAB, 0x89}}, + {64, 11, 1, 0, {0xEF, 0xCD, 0xAB, 0x89, LONG_TIME_BYTES}}, + {70, 1, 1, 0, {0x82}}, + {104, 2, 1, 0, {0xAA, 0x55}}, + {106, 2, 1, 0, {0xEF, 0xBE}}, + {110, 3, 0, 0, {0, 0x80, 0xC3}}, + {111, 3, 0, 0, {0xFE, 0xFF, 0xC3}}, + {112, 5, 0, 0, {0, 0, 0xC0, 0x3F, 0xC3}}, + {113, 1, 0, 0, {0x82}}, + {120, 6, 1, 0, {0x34, 0x12, 1, 0xCD, 0xAB, 0x80}}, + {121, 7, 1, 0, {0x34, 0x12, 0x56, 1, 0xCD, 0xAB, 0x80}}, + {122, 4, 1, 0, {0x34, 0x12, 0x56, 0x12}}, + {123, 5, 1, 0, {0x34, 0x12, 0x56, 1, 0xA5}}, + {124, 4, 1, 0, {0x34, 0x12, 0x56, 0x12}}, + {125, 8, 1, 0, {0x34, 0x12, 0x56, 4, 0xDE, 0xAD, 0xBE, 0xEF}}, + {126, 13, 0, 1, {0x34, 0x12, 1, 0xCD, 0xAB, 0xE3, LONG_TIME_BYTES}}, + {127, 16, 1, 0, {0x34, 0x12, LONG_TIME_BYTES, LONG_TIME_BYTES}}, +}; + +static IEC60870_AsduResult_t Create_Object(uint8_t type, uint32_t ioa, Object *out) +{ + switch (type) { + case 2: return IEC60870_SinglePointWithCP24Time2a_Create(&out->v2, ioa, 1, 0xF0, &short_time); + case 4: return IEC60870_DoublePointWithCP24Time2a_Create(&out->v4, ioa, 3, 0x90, &short_time); + case 5: return IEC60870_StepPositionInformation_Create(&out->v5, ioa, -2, 1, 0x81); + case 6: return IEC60870_StepPositionWithCP24Time2a_Create(&out->v6, ioa, -2, 1, 0x81, &short_time); + case 7: return IEC60870_BitString32_Create(&out->v7, ioa, UINT32_C(0x89ABCDEF), 0x51); + case 8: return IEC60870_Bitstring32WithCP24Time2a_Create(&out->v8, ioa, UINT32_C(0x89ABCDEF), 0x51, &short_time); + case 10: return IEC60870_MeasuredValueNormalizedWithCP24Time2a_Create(&out->v10, ioa, INT16_MIN, 0x81, &short_time); + case 12: return IEC60870_MeasuredValueScaledWithCP24Time2a_Create(&out->v12, ioa, -2, 0x10, &short_time); + case 14: return IEC60870_MeasuredValueShortWithCP24Time2a_Create(&out->v14, ioa, 1.5f, 0x41, &short_time); + case 16: return IEC60870_IntegratedTotalsWithCP24Time2a_Create(&out->v16, ioa, &counter, &short_time); + case 17: return IEC60870_EventOfProtectionEquipment_Create(&out->v17, ioa, 2, 0x98, &elapsed, &short_time); + case 18: return IEC60870_PackedStartEventsOfProtectionEquipment_Create(&out->v18, ioa, 0x21, 0x98, &elapsed, &short_time); + case 19: return IEC60870_PackedOutputCircuitInfo_Create(&out->v19, ioa, 9, 0x98, &elapsed, &short_time); + case 20: return IEC60870_PackedSinglePointWithSCD_Create(&out->v20, ioa, 0x1234, 0xABCD, 0x81); + case 21: return IEC60870_MeasuredValueNormalizedWithoutQuality_Create(&out->v21, ioa, INT16_MIN); + case 32: return IEC60870_StepPositionWithCP56Time2a_Create(&out->v32, ioa, -2, 1, 0x81, &long_time); + case 33: return IEC60870_Bitstring32WithCP56Time2a_Create(&out->v33, ioa, UINT32_C(0x89ABCDEF), 0x51, &long_time); + case 38: return IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Create(&out->v38, ioa, 2, 0x98, &elapsed, &long_time); + case 39: return IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Create(&out->v39, ioa, 0x21, 0x98, &elapsed, &long_time); + case 40: return IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Create(&out->v40, ioa, 9, 0x98, &elapsed, &long_time); + case 51: return IEC60870_Bitstring32Command_Create(&out->v51, ioa, UINT32_C(0x89ABCDEF)); + case 64: return IEC60870_Bitstring32CommandWithCP56Time2a_Create(&out->v64, ioa, UINT32_C(0x89ABCDEF), &long_time); + case 70: return IEC60870_EndOfInitialization_Create(&out->v70, ioa, 0x82); + case 104: return IEC60870_TestCommand_Create(&out->v104, ioa); + case 106: return IEC60870_DelayAcquisitionCommand_Create(&out->v106, ioa, &elapsed); + case 110: return IEC60870_ParameterNormalizedValue_Create(&out->v110, ioa, INT16_MIN, 0xC3); + case 111: return IEC60870_ParameterScaledValue_Create(&out->v111, ioa, -2, 0xC3); + case 112: return IEC60870_ParameterFloatValue_Create(&out->v112, ioa, 1.5f, 0xC3); + case 113: return IEC60870_ParameterActivation_Create(&out->v113, ioa, 0x82); + case 120: return IEC60870_FileReady_Create(&out->v120, ioa, 0x1234, UINT32_C(0xABCD01), 0x80); + case 121: return IEC60870_SectionReady_Create(&out->v121, ioa, 0x1234, 0x56, UINT32_C(0xABCD01), 0x80); + case 122: return IEC60870_FileCallOrSelect_Create(&out->v122, ioa, 0x1234, 0x56, 0x12); + case 123: return IEC60870_FileLastSegmentOrSection_Create(&out->v123, ioa, 0x1234, 0x56, 1, 0xA5); + case 124: return IEC60870_FileACK_Create(&out->v124, ioa, 0x1234, 0x56, 0x12); + case 125: return IEC60870_FileSegment_Create(&out->v125, ioa, 0x1234, 0x56, segment_data, sizeof(segment_data)); + case 126: return IEC60870_FileDirectory_Create(&out->v126, ioa, 0x1234, UINT32_C(0xABCD01), 0xE3, &long_time); + case 127: return IEC60870_QueryLog_Create(&out->v127, ioa, 0x1234, &long_time, &long_time); + default: return IEC60870_ASDU_UNSUPPORTED; + } +} + +static IEC60870_AsduResult_t Parse_Object(uint8_t type, uint32_t ioa, const uint8_t *payload, uint16_t length, Object *out) +{ + switch (type) { + case 5: return IEC60870_StepPositionInformation_Get_From_Buffer(&out->v5, ioa, payload, length); + case 7: return IEC60870_BitString32_Get_From_Buffer(&out->v7, ioa, payload, length); + case 2: return IEC60870_SinglePointWithCP24Time2a_Get_From_Buffer(&out->v2, ioa, payload, length); + case 4: return IEC60870_DoublePointWithCP24Time2a_Get_From_Buffer(&out->v4, ioa, payload, length); + case 6: return IEC60870_StepPositionWithCP24Time2a_Get_From_Buffer(&out->v6, ioa, payload, length); + case 8: return IEC60870_Bitstring32WithCP24Time2a_Get_From_Buffer(&out->v8, ioa, payload, length); + case 10: return IEC60870_MeasuredValueNormalizedWithCP24Time2a_Get_From_Buffer(&out->v10, ioa, payload, length); + case 12: return IEC60870_MeasuredValueScaledWithCP24Time2a_Get_From_Buffer(&out->v12, ioa, payload, length); + case 14: return IEC60870_MeasuredValueShortWithCP24Time2a_Get_From_Buffer(&out->v14, ioa, payload, length); + case 16: return IEC60870_IntegratedTotalsWithCP24Time2a_Get_From_Buffer(&out->v16, ioa, payload, length); + case 32: return IEC60870_StepPositionWithCP56Time2a_Get_From_Buffer(&out->v32, ioa, payload, length); + case 33: return IEC60870_Bitstring32WithCP56Time2a_Get_From_Buffer(&out->v33, ioa, payload, length); + case 17: return IEC60870_EventOfProtectionEquipment_Get_From_Buffer(&out->v17, ioa, payload, length); + case 18: return IEC60870_PackedStartEventsOfProtectionEquipment_Get_From_Buffer(&out->v18, ioa, payload, length); + case 19: return IEC60870_PackedOutputCircuitInfo_Get_From_Buffer(&out->v19, ioa, payload, length); + case 38: return IEC60870_EventOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer(&out->v38, ioa, payload, length); + case 39: return IEC60870_PackedStartEventsOfProtectionEquipmentWithCP56Time2a_Get_From_Buffer(&out->v39, ioa, payload, length); + case 40: return IEC60870_PackedOutputCircuitInfoWithCP56Time2a_Get_From_Buffer(&out->v40, ioa, payload, length); + case 20: return IEC60870_PackedSinglePointWithSCD_Get_From_Buffer(&out->v20, ioa, payload, length); + case 21: return IEC60870_MeasuredValueNormalizedWithoutQuality_Get_From_Buffer(&out->v21, ioa, payload, length); + case 51: return IEC60870_Bitstring32Command_Get_From_Buffer(&out->v51, ioa, payload, length); + case 64: return IEC60870_Bitstring32CommandWithCP56Time2a_Get_From_Buffer(&out->v64, ioa, payload, length); + case 70: return IEC60870_EndOfInitialization_Get_From_Buffer(&out->v70, ioa, payload, length); + case 104: return IEC60870_TestCommand_Get_From_Buffer(&out->v104, ioa, payload, length); + case 106: return IEC60870_DelayAcquisitionCommand_Get_From_Buffer(&out->v106, ioa, payload, length); + case 110: return IEC60870_ParameterNormalizedValue_Get_From_Buffer(&out->v110, ioa, payload, length); + case 111: return IEC60870_ParameterScaledValue_Get_From_Buffer(&out->v111, ioa, payload, length); + case 112: return IEC60870_ParameterFloatValue_Get_From_Buffer(&out->v112, ioa, payload, length); + case 113: return IEC60870_ParameterActivation_Get_From_Buffer(&out->v113, ioa, payload, length); + case 120: return IEC60870_FileReady_Get_From_Buffer(&out->v120, ioa, payload, length); + case 121: return IEC60870_SectionReady_Get_From_Buffer(&out->v121, ioa, payload, length); + case 122: return IEC60870_FileCallOrSelect_Get_From_Buffer(&out->v122, ioa, payload, length); + case 124: return IEC60870_FileACK_Get_From_Buffer(&out->v124, ioa, payload, length); + case 123: return IEC60870_FileLastSegmentOrSection_Get_From_Buffer(&out->v123, ioa, payload, length); + case 125: return IEC60870_FileSegment_Get_From_Buffer(&out->v125, ioa, payload, length); + case 126: return IEC60870_FileDirectory_Get_From_Buffer(&out->v126, ioa, payload, length); + case 127: return IEC60870_QueryLog_Get_From_Buffer(&out->v127, ioa, payload, length); + default: return IEC60870_ASDU_UNSUPPORTED; + } +} + +static IEC60870_InformationObject_t *Base(Object *object) +{ + return (IEC60870_InformationObject_t *)object; +} + +/* 独立按协议宽度拼接地址,未调用生产工具。 */ +static void Write_Address(uint8_t *out, uint32_t value, int width) +{ + int i; + for (i = 0; i < width; ++i) { out[i] = (uint8_t)(value >> (8 * i)); } +} + +static uint16_t Expected_Frame(const Vector *v, const IEC60870_AppLayerParameters_t *p, + int sq, int count, uint32_t first, uint8_t *out) +{ + uint16_t n = (uint16_t)(2 + p->sizeOfCOT + p->sizeOfCA); + int i; + out[0] = v->type; + out[1] = (uint8_t)((sq << 7) | count); + out[2] = 0xC3; + if (p->sizeOfCOT == 2) { out[3] = 0xA7; } + Write_Address(out + 2 + p->sizeOfCOT, p->sizeOfCA == 2 ? 0x1234U : 0x34U, p->sizeOfCA); + for (i = 0; i < count; ++i) { + if (i == 0 || !sq) { + Write_Address(out + n, first + (uint32_t)(sq ? i : i * 7), p->sizeOfIOA); + n = (uint16_t)(n + p->sizeOfIOA); + } + memcpy(out + n, v->payload, v->length); + n = (uint16_t)(n + v->length); + } + return n; +} + +static int Reject_Frame(const IEC60870_AppLayerParameters_t *p, const uint8_t *frame, uint16_t length) +{ + IEC60870_AsduInfo_t info; + uint8_t saved[sizeof(info)]; + memset(&info, 0xA5, sizeof(info)); + memcpy(saved, &info, sizeof(info)); + CHECK(IEC60870_Asdu_Parse(p, frame, length, &info) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &info, sizeof(info)) == 0); + return 1; +} + +static int test_all_layouts(void) +{ + int cot, ca, ioa, sq; + unsigned int cases = 0; + size_t k; + for (cot = 1; cot <= 2; ++cot) for (ca = 1; ca <= 2; ++ca) for (ioa = 1; ioa <= 3; ++ioa) { + IEC60870_AppLayerParameters_t p = default_parameters; + IEC60870_AsduHeader_t header = {3,1,1,0xA7,0}; + uint32_t first = ioa == 1 ? 0x40U : ioa == 2 ? 0x3440U : 0x123440U; + p.sizeOfCOT = cot; p.sizeOfCA = ca; p.sizeOfIOA = ioa; p.maxSizeOfASDU = 254; + header.common_address = ca == 1 ? 0x34 : 0x1234; + for (k = 0; k < COUNT(vectors); ++k) for (sq = 0; sq <= vectors[k].sequence; ++sq) { + const Vector *v = &vectors[k]; + IEC60870_Asdu_t asdu, saved_asdu; + IEC60870_AsduInfo_t info; + Object object, parsed, saved_object; + uint8_t bytes[260], expected[260], saved_bytes[260], encoded[262]; + const uint8_t *data; + uint16_t length, wire_length, object_length; + int count = v->single ? 1 : 2, i; + memset(bytes, 0xA5, sizeof(bytes)); + CHECK(IEC60870_Asdu_Create(&asdu, &p, bytes, sizeof(bytes), &header, sq) == IEC60870_ASDU_OK); + for (i = 0; i < count; ++i) { + uint32_t address = first + (uint32_t)(sq ? i : i * 7); + CHECK(Create_Object(v->type, address, &object) == IEC60870_ASDU_OK); + CHECK(IEC60870_InformationObject_Get_Type(Base(&object)) == v->type); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, Base(&object)) == IEC60870_ASDU_OK); + CHECK(IEC60870_InformationObject_Encode(Base(&object), &p, 0, encoded, sizeof(encoded), &object_length) == IEC60870_ASDU_OK); + CHECK(object_length == ioa + v->length); + CHECK(memcmp(encoded + ioa, v->payload, v->length) == 0); + } + CHECK(IEC60870_Asdu_Get_Data(&asdu, &data, &length) == IEC60870_ASDU_OK); + wire_length = Expected_Frame(v, &p, sq, count, first, expected); + CHECK(length == wire_length && memcmp(data, expected, length) == 0); + CHECK(bytes[length] == 0xA5); + CHECK(IEC60870_Asdu_Parse(&p, expected, length, &info) == IEC60870_ASDU_OK); + CHECK(info.object_count == count && info.is_sequence == sq && info.common_address == header.common_address); + for (i = 0; i < count; ++i) { + CHECK(IEC60870_Asdu_Get_Element(&info, (uint8_t)i, &parsed, sizeof(parsed)) == IEC60870_ASDU_OK); + CHECK(Base(&parsed)->ioa == first + (uint32_t)(sq ? i : i * 7)); + CHECK(IEC60870_InformationObject_Encode(Base(&parsed), &p, 0, encoded, sizeof(encoded), &object_length) == IEC60870_ASDU_OK); + CHECK(memcmp(encoded + ioa, v->payload, v->length) == 0); + } + memset(&parsed, 0xA5, sizeof(parsed)); memcpy(&saved_object, &parsed, sizeof(parsed)); + CHECK(IEC60870_Asdu_Get_Element(&info, 0, &parsed, 1) == IEC60870_ASDU_ERROR); + CHECK(memcmp(&parsed, &saved_object, sizeof(parsed)) == 0); + CHECK(Reject_Frame(&p, expected, (uint16_t)(length - 1))); + expected[length] = 0; + CHECK(Reject_Frame(&p, expected, (uint16_t)(length + 1))); + for (i = 0; i < v->length; ++i) { + CHECK(Parse_Object(v->type, first, v->payload, (uint16_t)i, &parsed) == IEC60870_ASDU_ERROR); + CHECK(memcmp(&parsed, &saved_object, sizeof(parsed)) == 0); + } + CHECK(Parse_Object(v->type, first, v->payload, v->length, &parsed) == IEC60870_ASDU_OK); + /* 原对象存储可复用,加入ASDU后保存的字节必须独立。 */ + memset(&object, 0, sizeof(object)); + CHECK(memcmp(data, expected, length) == 0); + saved_asdu = asdu; memcpy(saved_bytes, bytes, sizeof(bytes)); + CHECK(Create_Object(v->type, first, &object) == IEC60870_ASDU_OK); + if (v->single) { + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, Base(&object)) == IEC60870_ASDU_FULL); + } else if (sq) { + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, Base(&object)) == IEC60870_ASDU_ERROR); + } else { + asdu.capacity = asdu.length; saved_asdu = asdu; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, Base(&object)) == IEC60870_ASDU_FULL); + } + CHECK(memcmp(&asdu, &saved_asdu, sizeof(asdu)) == 0); + CHECK(memcmp(bytes, saved_bytes, sizeof(bytes)) == 0); + ++cases; + } + } + printf("Extended ASDU: %u independent wire layouts verified\n", cases); + return 1; +} + +static int test_sequence_and_counts(void) +{ + size_t k; + uint8_t frame[260], buffer[260], saved[260]; + IEC60870_AsduHeader_t header = {3,1,1,0xA7,0x1234}; + IEC60870_Asdu_t asdu, before; + Object object; + for (k = 0; k < COUNT(vectors); ++k) { + const Vector *v = &vectors[k]; + uint16_t n = Expected_Frame(v, &default_parameters, 0, 1, 0x40, frame); + frame[1] = 0; + CHECK(Reject_Frame(&default_parameters, frame, n)); + if (!v->sequence) { + n = Expected_Frame(v, &default_parameters, 1, 1, 0x40, frame); + CHECK(Reject_Frame(&default_parameters, frame, n)); + memset(buffer, 0xA5, sizeof(buffer)); + CHECK(IEC60870_Asdu_Create(&asdu, &default_parameters, buffer, sizeof(buffer), &header, 1) == IEC60870_ASDU_OK); + before = asdu; memcpy(saved, buffer, sizeof(buffer)); + CHECK(Create_Object(v->type, 0x40, &object) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, Base(&object)) == IEC60870_ASDU_ERROR); + CHECK(memcmp(&asdu, &before, sizeof(asdu)) == 0 && memcmp(buffer, saved, sizeof(buffer)) == 0); + } + if (v->single) { + n = Expected_Frame(v, &default_parameters, 0, 2, 0x40, frame); + CHECK(Reject_Frame(&default_parameters, frame, n)); + } + if (v->sequence) { + IEC60870_AppLayerParameters_t p = default_parameters; + p.sizeOfIOA = 1; + n = Expected_Frame(v, &p, 1, 2, 255, frame); + CHECK(Reject_Frame(&p, frame, n)); + } + } + return 1; +} + +static int test_invalid_fields(void) +{ + /* 每项给出独立的非法保留位/状态/时间修改,不依赖生产描述表。 */ + static const struct { uint8_t type, offset, mask; } faults[] = { + {2,0,2},{4,0,4},{5,1,2},{6,1,2},{7,4,2},{8,4,2}, + {10,2,2},{12,2,2},{14,4,2},{16,7,0x40}, + {17,0,4},{18,0,0x40},{19,0,0x10},{20,4,2}, + {32,1,2},{33,4,2},{38,0,4},{39,1,4},{40,0,0x10}, + {104,0,1},{120,5,1},{121,6,1},{125,3,1}, + {126,11,0x80},{127,14,0x80} + }; + size_t i, k; + for (i = 0; i < COUNT(faults); ++i) { + Object object, before; + uint8_t payload[17], frame[260]; + for (k = 0; k < COUNT(vectors) && vectors[k].type != faults[i].type; ++k) {} + CHECK(k < COUNT(vectors)); + memcpy(payload, vectors[k].payload, vectors[k].length); + payload[faults[i].offset] |= faults[i].mask; + memset(&object, 0xA5, sizeof(object)); memcpy(&before, &object, sizeof(object)); + CHECK(Parse_Object(vectors[k].type, 1, payload, vectors[k].length, &object) == IEC60870_ASDU_ERROR); + CHECK(memcmp(&object, &before, sizeof(object)) == 0); + { + uint16_t n = Expected_Frame(&vectors[k], &default_parameters, 0, 1, 1, frame); + memcpy(frame + 9, payload, vectors[k].length); + CHECK(Reject_Frame(&default_parameters, frame, n)); + } + } + /* 多对象中的第二个非法对象也必须导致整条ASDU解析失败。 */ + { + IEC60870_AsduInfo_t info; + uint8_t frame[] = {5,0x82,3,0,1,0,1,0,0,0xFE,0x81,0xFE,0x83}; + CHECK(Reject_Frame(&default_parameters, frame, sizeof(frame))); + frame[12] = 0x81; + CHECK(IEC60870_Asdu_Parse(&default_parameters, frame, sizeof(frame), &info) == IEC60870_ASDU_OK); + } + return 1; +} + +static int test_short_times(void) +{ + IEC60870_CP16Time2a_t cp16, parsed16 = {0}; + IEC60870_CP24Time2a_t cp24 = {59999,59,1}, parsed24, before24; + uint8_t bytes[4] = {0xA5,0xA5,0xA5,0xA5}, saved[4]; + uint32_t i; + for (i = 0; i <= 65535U; ++i) { + cp16.milliseconds = (uint16_t)i; + CHECK(IEC60870_Time_To_CP16(&cp16, bytes, 2)); + CHECK(bytes[0] == (uint8_t)i && bytes[1] == (uint8_t)(i >> 8)); + CHECK(IEC60870_Time_From_CP16(bytes, 2, &parsed16) && parsed16.milliseconds == i); + } + CHECK(IEC60870_Time_To_CP24(&cp24, bytes, 3)); + CHECK(bytes[0] == 0x5F && bytes[1] == 0xEA && bytes[2] == 0xBB && bytes[3] == 0xA5); + CHECK(IEC60870_Time_From_CP24(bytes, 3, &parsed24)); + CHECK(parsed24.milliseconds == 59999 && parsed24.minute == 59 && parsed24.is_invalid == 1); + before24 = parsed24; + memcpy(saved, bytes, sizeof(bytes)); + cp24.milliseconds = 60000; + CHECK(!IEC60870_Time_To_CP24(&cp24, bytes, sizeof(bytes))); + CHECK(memcmp(bytes, saved, sizeof(bytes)) == 0); + cp24.milliseconds = 0; cp24.minute = 60; + CHECK(!IEC60870_Time_To_CP24(&cp24, bytes, sizeof(bytes))); + cp24.minute = 0; cp24.is_invalid = 2; + CHECK(!IEC60870_Time_To_CP24(&cp24, bytes, sizeof(bytes))); + bytes[2] |= 0x40; + CHECK(!IEC60870_Time_From_CP24(bytes, 3, &parsed24)); + CHECK(memcmp(&parsed24, &before24, sizeof(parsed24)) == 0); + bytes[0] = 0x60; bytes[1] = 0xEA; bytes[2] = 0; + CHECK(!IEC60870_Time_From_CP24(bytes, 3, &parsed24)); + bytes[0] = 0; bytes[1] = 0; bytes[2] = 60; + CHECK(!IEC60870_Time_From_CP24(bytes, 3, &parsed24)); + CHECK(!IEC60870_Time_From_CP24(bytes, 2, &parsed24)); + CHECK(!IEC60870_Time_From_CP24(NULL, 3, &parsed24)); + CHECK(!IEC60870_Time_From_CP16(bytes, 1, &parsed16)); + CHECK(!IEC60870_Time_To_CP16(&cp16, bytes, 1)); + CHECK(!IEC60870_Time_To_CP24(NULL, bytes, 3)); + cp24.milliseconds = 0; cp24.minute = 0; cp24.is_invalid = 0; + CHECK(IEC60870_Time_To_CP24(&cp24, bytes, 3)); + CHECK(bytes[0] == 0 && bytes[1] == 0 && bytes[2] == 0); + return 1; +} + +static int test_constructor_atomicity(void) +{ + Object object, before; + size_t i; + uint8_t encoded[262], saved_bytes[262]; + uint16_t length = 0xA55A; + IEC60870_CP24Time2a_t bad_time = {60000,0,0}; + memset(&object, 0xA5, sizeof(object)); memcpy(&before, &object, sizeof(object)); + for (i = 0; i < COUNT(vectors); ++i) { + CHECK(Create_Object(vectors[i].type, UINT32_C(0x1000000), &object) == IEC60870_ASDU_ERROR); + CHECK(memcmp(&object, &before, sizeof(object)) == 0); + CHECK(Parse_Object(vectors[i].type, 1, NULL, vectors[i].length, &object) == IEC60870_ASDU_ERROR); + CHECK(memcmp(&object, &before, sizeof(object)) == 0); + } +#define REJECT_CREATE(call) do { CHECK((call) == IEC60870_ASDU_ERROR); CHECK(memcmp(&object, &before, sizeof(object)) == 0); } while (0) + REJECT_CREATE(IEC60870_StepPositionInformation_Create(&object.v5, 1, -65, 0, 0)); + REJECT_CREATE(IEC60870_StepPositionInformation_Create(&object.v5, 1, 64, 0, 0)); + REJECT_CREATE(IEC60870_StepPositionInformation_Create(&object.v5, 1, 0, 2, 0)); + REJECT_CREATE(IEC60870_BitString32_Create(&object.v7, 1, 0, 2)); + REJECT_CREATE(IEC60870_SinglePointWithCP24Time2a_Create(&object.v2, 1, 1, 0, &bad_time)); + REJECT_CREATE(IEC60870_SinglePointWithCP24Time2a_Create(&object.v2, 1, 1, 0, NULL)); + REJECT_CREATE(IEC60870_EventOfProtectionEquipment_Create(&object.v17, 1, 4, 0, &elapsed, &short_time)); + REJECT_CREATE(IEC60870_EventOfProtectionEquipment_Create(&object.v17, 1, 1, 4, &elapsed, &short_time)); + REJECT_CREATE(IEC60870_PackedStartEventsOfProtectionEquipment_Create(&object.v18, 1, 64, 0, &elapsed, &short_time)); + REJECT_CREATE(IEC60870_PackedOutputCircuitInfo_Create(&object.v19, 1, 16, 0, &elapsed, &short_time)); + REJECT_CREATE(IEC60870_EndOfInitialization_Create(&object.v70, 1, 256)); + REJECT_CREATE(IEC60870_DelayAcquisitionCommand_Create(&object.v106, 1, NULL)); + REJECT_CREATE(IEC60870_ParameterNormalizedValue_Create(&object.v110, 1, 0, -1)); + REJECT_CREATE(IEC60870_FileReady_Create(&object.v120, 1, 1, UINT32_C(0x1000000), 0)); + REJECT_CREATE(IEC60870_SectionReady_Create(&object.v121, 1, 1, 1, 0, 1)); + REJECT_CREATE(IEC60870_FileSegment_Create(&object.v125, 1, 1, 1, NULL, 1)); + REJECT_CREATE(IEC60870_FileSegment_Create(&object.v125, 1, 1, 1, segment_data, 256)); + REJECT_CREATE(IEC60870_FileDirectory_Create(&object.v126, 1, 1, 0, 0, NULL)); + REJECT_CREATE(IEC60870_QueryLog_Create(&object.v127, 1, 1, &long_time, NULL)); +#undef REJECT_CREATE + CHECK(Create_Object(5, 1, &object) == IEC60870_ASDU_OK); + object.v5.value = 100; + memset(encoded, 0xA5, sizeof(encoded)); memcpy(saved_bytes, encoded, sizeof(encoded)); + CHECK(IEC60870_InformationObject_Encode(Base(&object), &default_parameters, 0, encoded, sizeof(encoded), &length) == IEC60870_ASDU_ERROR); + CHECK(memcmp(encoded, saved_bytes, sizeof(encoded)) == 0 && length == 0xA55A); + return 1; +} + +static int test_file_segments(void) +{ + int cot, ca, ioa, max; + uint8_t input[255], bytes[270], before_bytes[270], encoded[262]; + IEC60870_AsduHeader_t header = {13,0,0,0,1}; + unsigned int i; + CHECK(IEC60870_FileSegment_Get_Max_Data_Size(&default_parameters) == 236); + CHECK(IEC60870_FileSegment_Get_Max_Data_Size(NULL) == -1); + for (i = 0; i < sizeof(input); ++i) { input[i] = (uint8_t)(i ^ 0x5A); } + for (cot = 1; cot <= 2; ++cot) for (ca = 1; ca <= 2; ++ca) for (ioa = 1; ioa <= 3; ++ioa) { + IEC60870_AppLayerParameters_t p = default_parameters; + IEC60870_FileSegment_t segment, parsed; + IEC60870_Asdu_t asdu, before; + IEC60870_AsduInfo_t info; + const uint8_t *wire; + uint16_t n, object_length; + int h = 2 + cot + ca; + p.sizeOfCOT = cot; p.sizeOfCA = ca; p.sizeOfIOA = ioa; p.maxSizeOfASDU = 254; + max = IEC60870_FileSegment_Get_Max_Data_Size(&p); + CHECK(max == 254 - h - ioa - 4); + CHECK(IEC60870_FileSegment_Create(&segment, 1, 0x1234, 7, input, (uint16_t)max) == IEC60870_ASDU_OK); + memset(bytes, 0xA5, sizeof(bytes)); + CHECK(IEC60870_Asdu_Create(&asdu, &p, bytes, sizeof(bytes), &header, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &segment.object) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Get_Data(&asdu, &wire, &n) == IEC60870_ASDU_OK && n == 254); + CHECK(wire[h+ioa] == 0x34 && wire[h+ioa+1] == 0x12 && wire[h+ioa+2] == 7 && wire[h+ioa+3] == max); + CHECK(memcmp(wire+h+ioa+4, input, (size_t)max) == 0 && bytes[254] == 0xA5); + CHECK(IEC60870_Asdu_Parse(&p, wire, n, &info) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Get_Element(&info, 0, &parsed, sizeof(parsed)) == IEC60870_ASDU_OK); + CHECK(parsed.length == max && memcmp(parsed.data, input, (size_t)max) == 0); + memset(bytes+h+ioa+4, 0, (size_t)max); + CHECK(memcmp(parsed.data, input, (size_t)max) == 0); /* 解析结果拥有独立数据。 */ + CHECK(IEC60870_FileSegment_Create(&segment, 1, 1, 1, input, (uint16_t)(max+1)) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Create(&asdu, &p, bytes, sizeof(bytes), &header, 0) == IEC60870_ASDU_OK); + before = asdu; memcpy(before_bytes, bytes, sizeof(bytes)); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &segment.object) == IEC60870_ASDU_FULL); + CHECK(memcmp(&asdu, &before, sizeof(asdu)) == 0 && memcmp(bytes, before_bytes, sizeof(bytes)) == 0); + CHECK(IEC60870_FileSegment_Create(&segment, 1, 1, 1, input, 255) == IEC60870_ASDU_OK); + CHECK(IEC60870_InformationObject_Encode(&segment.object, &p, 0, encoded, sizeof(encoded), &object_length) == IEC60870_ASDU_OK); + CHECK(object_length == ioa+259 && encoded[ioa+3] == 255); + CHECK(IEC60870_FileSegment_Get_From_Buffer(&parsed, 1, encoded+ioa, 259) == IEC60870_ASDU_OK); + CHECK(parsed.length == 255 && memcmp(parsed.data, input, sizeof(input)) == 0); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &segment.object) == IEC60870_ASDU_FULL); + CHECK(IEC60870_FileSegment_Create(&segment, 1, 1, 1, NULL, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &segment.object) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Get_Data(&asdu, &wire, &n) == IEC60870_ASDU_OK && n == h+ioa+4); + CHECK(IEC60870_Asdu_Parse(&p, wire, n, &info) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Get_Element(&info, 0, &parsed, sizeof(parsed)) == IEC60870_ASDU_OK && parsed.length == 0); + p.maxSizeOfASDU = h+ioa+4; + CHECK(IEC60870_FileSegment_Get_Max_Data_Size(&p) == 0); + p.maxSizeOfASDU--; + CHECK(IEC60870_FileSegment_Get_Max_Data_Size(&p) == -1); + } + /* 默认104配置:236字节恰好249;237字节必须返回FULL。 */ + { + IEC60870_FileSegment_t segment; + IEC60870_Asdu_t asdu; + const uint8_t *wire; + uint16_t n; + CHECK(IEC60870_Asdu_Create(&asdu, &default_parameters, bytes, sizeof(bytes), &header, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_FileSegment_Create(&segment, 1, 1, 1, input, 237) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &segment.object) == IEC60870_ASDU_FULL); + CHECK(IEC60870_FileSegment_Create(&segment, 1, 1, 1, input, 236) == IEC60870_ASDU_OK); + memset(input, 0, sizeof(input)); + CHECK(segment.data[0] == 0x5A); /* 构造结果也不借用源数据。 */ + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &segment.object) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Get_Data(&asdu, &wire, &n) == IEC60870_ASDU_OK && n == 249); + bytes[12]++; /* LOS称有237字节,但原文只有236字节。 */ + CHECK(Reject_Frame(&default_parameters, bytes, n)); + CHECK(IEC60870_Asdu_Get_Data(&asdu, &wire, &n) == IEC60870_ASDU_ERROR); + } + return 1; +} + +static int test_numeric_and_parameter_fields(void) +{ + IEC60870_StepPositionInformation_t step; + IEC60870_ParameterNormalizedValue_t parameter; + IEC60870_ParameterFloatValue_t floating; + IEC60870_PackedSinglePointWithSCD_t packed; + IEC60870_FileReady_t file; + IEC60870_Asdu_t asdu; + IEC60870_AsduInfo_t info; + IEC60870_AsduHeader_t header = {6,0,0,0,1}; + uint8_t bytes[254], payload[16], raw_float[] = {0x23,0x01,0xC0,0x7F,0xFF}; + uint16_t n; + const uint8_t *wire; + int v; + for (v = -64; v <= 63; ++v) { + CHECK(IEC60870_StepPositionInformation_Create(&step, 1, v, 0, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_InformationObject_Encode(&step.object, &default_parameters, 1, payload, sizeof(payload), &n) == IEC60870_ASDU_OK); + CHECK(n == 2 && payload[0] == ((unsigned int)v & 0x7FU) && payload[1] == 0); + CHECK(IEC60870_StepPositionInformation_Get_From_Buffer(&step, 1, payload, 2) == IEC60870_ASDU_OK && step.value == v); + } + CHECK(IEC60870_PackedSinglePointWithSCD_Get_From_Buffer(&packed, 1, (const uint8_t[]){0x34,0x12,0xCD,0xAB,0x81}, 5) == IEC60870_ASDU_OK); + CHECK(packed.status == 0x1234 && packed.change == 0xABCD && packed.quality == 0x81); + CHECK(IEC60870_FileReady_Create(&file, 1, 1, UINT32_C(0xFFFFFF), 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_InformationObject_Encode(&file.object, &default_parameters, 0, payload, sizeof(payload), &n) == IEC60870_ASDU_OK); + CHECK(payload[5] == 255 && payload[6] == 255 && payload[7] == 255); + CHECK(IEC60870_ParameterFloatValue_Get_From_Buffer(&floating, 1, raw_float, sizeof(raw_float)) == IEC60870_ASDU_OK); + CHECK(floating.qualifier == 255); + CHECK(IEC60870_InformationObject_Encode(&floating.object, &default_parameters, 0, payload, sizeof(payload), &n) == IEC60870_ASDU_OK); + CHECK(memcmp(payload+3, raw_float, sizeof(raw_float)) == 0); /* NaN位模式与QPM全部保留。 */ + CHECK(IEC60870_Asdu_Create(&asdu, &default_parameters, bytes, sizeof(bytes), &header, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_ParameterNormalizedValue_Create(¶meter, 1, INT16_MIN, 0xFF) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, ¶meter.object) == IEC60870_ASDU_OK); + CHECK(IEC60870_ParameterNormalizedValue_Create(¶meter, 999, INT16_MAX, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, ¶meter.object) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Get_Data(&asdu, &wire, &n) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Parse(&default_parameters, wire, n, &info) == IEC60870_ASDU_OK && info.object_count == 2); + CHECK(IEC60870_Asdu_Get_Element(&info, 1, ¶meter, sizeof(parameter)) == IEC60870_ASDU_OK); + CHECK(parameter.object.ioa == 999 && parameter.value == INT16_MAX && parameter.qualifier == 0); + return 1; +} + +int main(void) +{ + int passed = 0; +#define RUN(test) do { if (!(test)()) { return 1; } ++passed; } while (0) + RUN(test_all_layouts); + RUN(test_sequence_and_counts); + RUN(test_invalid_fields); + RUN(test_short_times); + RUN(test_constructor_atomicity); + RUN(test_file_segments); + RUN(test_numeric_and_parameter_fields); + printf("Extended ASDU: %d groups passed (37 additional types, short times, variable file segments)\n", passed); + return 0; +} + diff --git a/IEC60870-Slave/.build/command-v1/before/test/iec60870_asdu_test.c b/IEC60870-Slave/.build/command-v1/before/test/iec60870_asdu_test.c new file mode 100644 index 00000000..b3089e99 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/test/iec60870_asdu_test.c @@ -0,0 +1,1198 @@ +/* 独立字节向量、边界和失败原子性测试,不操作网络或系统时钟。 + * 这里只用联合体为多种具体类型复用测试存储;生产API均采用独立类型。 */ +#include "IEC60870_Asdu.h" +#include +#include +#include + +#define CHECK(condition) do { if (!(condition)) { \ + fprintf(stderr, "%s:%d: %s\n", __func__, __LINE__, #condition); return 0; \ +} } while (0) +#define COUNT(array) (sizeof(array) / sizeof((array)[0])) +#define TIME_BYTES 0xD5, 0xDD, 0xA2, 0x8C, 0x9D, 0x02, 0x18 + +/* 显式采用2/2/3、249布局作为一组测试参数;公共模块不依赖104默认宏。 */ +static const IEC60870_AppLayerParameters_t parameters104 = {1, 1, 2, 0, 2, 3, 249}; + +typedef struct { + uint8_t type, size, timed, command; + uint8_t payload[12]; +} Vector; + +/* 期望载荷沿用独立字节向量,不调用待测编码器计算。 */ +static const Vector vectors[] = { + {1, 1, 0, 0, {0xF1}}, + {3, 1, 0, 0, {0x93}}, + {9, 3, 0, 0, {0x00,0x80,0x81}}, + {11, 3, 0, 0, {0xFE,0xFF,0x10}}, + {13, 5, 0, 0, {0x00,0x00,0xC0,0x3F,0x41}}, + {15, 5, 0, 0, {0x00,0x00,0x00,0x80,0xF5}}, + {30, 8, 1, 0, {0xF1,TIME_BYTES}}, + {31, 8, 1, 0, {0x93,TIME_BYTES}}, + {34, 10, 1, 0, {0x00,0x80,0x81,TIME_BYTES}}, + {35, 10, 1, 0, {0xFE,0xFF,0x10,TIME_BYTES}}, + {36, 12, 1, 0, {0x00,0x00,0xC0,0x3F,0x41,TIME_BYTES}}, + {37, 12, 1, 0, {0x00,0x00,0x00,0x80,0xF5,TIME_BYTES}}, + {45, 1, 0, 1, {0x8D}}, + {46, 1, 0, 1, {0x86}}, + {47, 1, 0, 1, {0x09}}, + {48, 3, 0, 1, {0x00,0x80,0xC1}}, + {49, 3, 0, 1, {0xFE,0xFF,0x7F}}, + {50, 5, 0, 1, {0x00,0x00,0xC0,0x3F,0x80}}, + {58, 8, 1, 1, {0x8D,TIME_BYTES}}, + {59, 8, 1, 1, {0x86,TIME_BYTES}}, + {60, 8, 1, 1, {0x09,TIME_BYTES}}, + {61, 10, 1, 1, {0x00,0x80,0xC1,TIME_BYTES}}, + {62, 10, 1, 1, {0xFE,0xFF,0x7F,TIME_BYTES}}, + {63, 12, 1, 1, {0x00,0x00,0xC0,0x3F,0x80,TIME_BYTES}}, + {100, 1, 0, 1, {0x14}}, + {101, 1, 0, 1, {0xC5}}, + {102, 0, 0, 1, {0}}, + {103, 7, 1, 1, {TIME_BYTES}}, + {105, 1, 0, 1, {0x02}}, + {107, 9, 1, 1, {0x34,0x12,TIME_BYTES}}, +}; + +typedef union { + IEC60870_SinglePointInformation_t v1; + IEC60870_DoublePointInformation_t v3; + IEC60870_MeasuredValueNormalized_t v9; + IEC60870_MeasuredValueScaled_t v11; + IEC60870_MeasuredValueShort_t v13; + IEC60870_IntegratedTotals_t v15; + IEC60870_SinglePointWithCP56Time2a_t v30; + IEC60870_DoublePointWithCP56Time2a_t v31; + IEC60870_MeasuredValueNormalizedWithCP56Time2a_t v34; + IEC60870_MeasuredValueScaledWithCP56Time2a_t v35; + IEC60870_MeasuredValueShortWithCP56Time2a_t v36; + IEC60870_IntegratedTotalsWithCP56Time2a_t v37; + IEC60870_SingleCommand_t v45; + IEC60870_DoubleCommand_t v46; + IEC60870_StepCommand_t v47; + IEC60870_SetpointCommandNormalized_t v48; + IEC60870_SetpointCommandScaled_t v49; + IEC60870_SetpointCommandShort_t v50; + IEC60870_SingleCommandWithCP56Time2a_t v58; + IEC60870_DoubleCommandWithCP56Time2a_t v59; + IEC60870_StepCommandWithCP56Time2a_t v60; + IEC60870_SetpointCommandNormalizedWithCP56Time2a_t v61; + IEC60870_SetpointCommandScaledWithCP56Time2a_t v62; + IEC60870_SetpointCommandShortWithCP56Time2a_t v63; + IEC60870_InterrogationCommand_t v100; + IEC60870_CounterInterrogationCommand_t v101; + IEC60870_ReadCommand_t v102; + IEC60870_ClockSynchronizationCommand_t v103; + IEC60870_ResetProcessCommand_t v105; + IEC60870_TestCommandWithCP56Time2a_t v107; +} TestObject; + +static IEC60870_CP56Time2a_t Sample_Time(void) +{ + IEC60870_CP56Time2a_t time = {{2024, 2, 29, 12, 34, 56, 789, 4}, 1, 1, 1}; + return time; +} + +static int Check_Time(const IEC60870_CP56Time2a_t *time) +{ + CHECK(time->time.year == 2024 && time->time.month == 2 && time->time.day == 29); + CHECK(time->time.hour == 12 && time->time.minute == 34 && time->time.second == 56); + CHECK(time->time.ms == 789 && time->time.weekday == 4); + CHECK(time->is_invalid == 1 && time->is_summer_time == 1 && time->has_weekday == 1); + return 1; +} + +static IEC60870_AsduHeader_t Header(void) +{ + IEC60870_AsduHeader_t header = {0}; + header.cause_of_transmission = IEC60870_ASDU_COT_SPONTANEOUS; + header.is_test = 1; + header.originator_address = 0x21; + header.common_address = 0x4567; + return header; +} + +/* 为每个Type ID构造真实具体对象,测试应用调用方式及函数表绑定。 */ +static IEC60870_AsduResult_t Sample_Object(uint8_t type, uint32_t ioa, TestObject *object) +{ + IEC60870_CP56Time2a_t time = Sample_Time(); + const IEC60870_BinaryCounterReading_t counter = {INT32_MIN, 21, 1, 1, 1}; + switch (type) { + case 1: return IEC60870_SinglePointInformation_Create(&object->v1, ioa, 1, 0xF0); + case 3: return IEC60870_DoublePointInformation_Create(&object->v3, ioa, 3, 0x90); + case 9: return IEC60870_MeasuredValueNormalized_Create(&object->v9, ioa, INT16_MIN, 0x81); + case 11: return IEC60870_MeasuredValueScaled_Create(&object->v11, ioa, -2, 0x10); + case 13: return IEC60870_MeasuredValueShort_Create(&object->v13, ioa, 1.5f, 0x41); + case 15: return IEC60870_IntegratedTotals_Create(&object->v15, ioa, &counter); + case 30: return IEC60870_SinglePointWithCP56Time2a_Create(&object->v30, ioa, 1, 0xF0, &time); + case 31: return IEC60870_DoublePointWithCP56Time2a_Create(&object->v31, ioa, 3, 0x90, &time); + case 34: return IEC60870_MeasuredValueNormalizedWithCP56Time2a_Create(&object->v34, ioa, INT16_MIN, 0x81, &time); + case 35: return IEC60870_MeasuredValueScaledWithCP56Time2a_Create(&object->v35, ioa, -2, 0x10, &time); + case 36: return IEC60870_MeasuredValueShortWithCP56Time2a_Create(&object->v36, ioa, 1.5f, 0x41, &time); + case 37: return IEC60870_IntegratedTotalsWithCP56Time2a_Create(&object->v37, ioa, &counter, &time); + case 45: return IEC60870_SingleCommand_Create(&object->v45, ioa, 1, 1, 3); + case 46: return IEC60870_DoubleCommand_Create(&object->v46, ioa, 2, 1, 1); + case 47: return IEC60870_StepCommand_Create(&object->v47, ioa, 1, 0, 2); + case 48: return IEC60870_SetpointCommandNormalized_Create(&object->v48, ioa, INT16_MIN, 1, 65); + case 49: return IEC60870_SetpointCommandScaled_Create(&object->v49, ioa, -2, 0, 127); + case 50: return IEC60870_SetpointCommandShort_Create(&object->v50, ioa, 1.5f, 1, 0); + case 58: return IEC60870_SingleCommandWithCP56Time2a_Create(&object->v58, ioa, 1, 1, 3, &time); + case 59: return IEC60870_DoubleCommandWithCP56Time2a_Create(&object->v59, ioa, 2, 1, 1, &time); + case 60: return IEC60870_StepCommandWithCP56Time2a_Create(&object->v60, ioa, 1, 0, 2, &time); + case 61: return IEC60870_SetpointCommandNormalizedWithCP56Time2a_Create(&object->v61, ioa, INT16_MIN, 1, 65, &time); + case 62: return IEC60870_SetpointCommandScaledWithCP56Time2a_Create(&object->v62, ioa, -2, 0, 127, &time); + case 63: return IEC60870_SetpointCommandShortWithCP56Time2a_Create(&object->v63, ioa, 1.5f, 1, 0, &time); + case 100: return IEC60870_InterrogationCommand_Create(&object->v100, ioa, 20); + case 101: return IEC60870_CounterInterrogationCommand_Create(&object->v101, ioa, 5, 3); + case 102: return IEC60870_ReadCommand_Create(&object->v102, ioa); + case 103: return IEC60870_ClockSynchronizationCommand_Create(&object->v103, ioa, &time); + case 105: return IEC60870_ResetProcessCommand_Create(&object->v105, ioa, 2); + case 107: return IEC60870_TestCommandWithCP56Time2a_Create(&object->v107, ioa, 0x1234, &time); + default: return IEC60870_ASDU_UNSUPPORTED; + } +} + +static IEC60870_InformationObject_t *Object_Base(TestObject *object, uint8_t type) +{ + switch (type) { + case 1: return &object->v1.object; + case 3: return &object->v3.object; + case 9: return &object->v9.object; + case 11: return &object->v11.object; + case 13: return &object->v13.object; + case 15: return &object->v15.object; + case 30: return &object->v30.base.object; + case 31: return &object->v31.base.object; + case 34: return &object->v34.base.object; + case 35: return &object->v35.base.object; + case 36: return &object->v36.base.object; + case 37: return &object->v37.base.object; + case 45: return &object->v45.object; + case 46: return &object->v46.object; + case 47: return &object->v47.object; + case 48: return &object->v48.object; + case 49: return &object->v49.object; + case 50: return &object->v50.object; + case 58: return &object->v58.base.object; + case 59: return &object->v59.base.object; + case 60: return &object->v60.base.object; + case 61: return &object->v61.base.object; + case 62: return &object->v62.base.object; + case 63: return &object->v63.base.object; + case 100: return &object->v100.object; + case 101: return &object->v101.object; + case 102: return &object->v102.object; + case 103: return &object->v103.object; + case 105: return &object->v105.object; + case 107: return &object->v107.object; + default: return NULL; + } +} + +static size_t Object_Size(uint8_t type) +{ + switch (type) { + case 1: return sizeof(IEC60870_SinglePointInformation_t); + case 3: return sizeof(IEC60870_DoublePointInformation_t); + case 9: return sizeof(IEC60870_MeasuredValueNormalized_t); + case 11: return sizeof(IEC60870_MeasuredValueScaled_t); + case 13: return sizeof(IEC60870_MeasuredValueShort_t); + case 15: return sizeof(IEC60870_IntegratedTotals_t); + case 30: return sizeof(IEC60870_SinglePointWithCP56Time2a_t); + case 31: return sizeof(IEC60870_DoublePointWithCP56Time2a_t); + case 34: return sizeof(IEC60870_MeasuredValueNormalizedWithCP56Time2a_t); + case 35: return sizeof(IEC60870_MeasuredValueScaledWithCP56Time2a_t); + case 36: return sizeof(IEC60870_MeasuredValueShortWithCP56Time2a_t); + case 37: return sizeof(IEC60870_IntegratedTotalsWithCP56Time2a_t); + case 45: return sizeof(IEC60870_SingleCommand_t); + case 46: return sizeof(IEC60870_DoubleCommand_t); + case 47: return sizeof(IEC60870_StepCommand_t); + case 48: return sizeof(IEC60870_SetpointCommandNormalized_t); + case 49: return sizeof(IEC60870_SetpointCommandScaled_t); + case 50: return sizeof(IEC60870_SetpointCommandShort_t); + case 58: return sizeof(IEC60870_SingleCommandWithCP56Time2a_t); + case 59: return sizeof(IEC60870_DoubleCommandWithCP56Time2a_t); + case 60: return sizeof(IEC60870_StepCommandWithCP56Time2a_t); + case 61: return sizeof(IEC60870_SetpointCommandNormalizedWithCP56Time2a_t); + case 62: return sizeof(IEC60870_SetpointCommandScaledWithCP56Time2a_t); + case 63: return sizeof(IEC60870_SetpointCommandShortWithCP56Time2a_t); + case 100: return sizeof(IEC60870_InterrogationCommand_t); + case 101: return sizeof(IEC60870_CounterInterrogationCommand_t); + case 102: return sizeof(IEC60870_ReadCommand_t); + case 103: return sizeof(IEC60870_ClockSynchronizationCommand_t); + case 105: return sizeof(IEC60870_ResetProcessCommand_t); + case 107: return sizeof(IEC60870_TestCommandWithCP56Time2a_t); + default: return 0; + } +} + +/* 直接测试各类型的载荷解析入口,独立于ASDU布局。 */ +static IEC60870_AsduResult_t Parse_Object(uint8_t type, uint32_t ioa, + const uint8_t *payload, uint16_t length, TestObject *object) +{ + switch (type) { + case 1: return IEC60870_SinglePointInformation_Get_From_Buffer(object == NULL ? NULL : &object->v1, ioa, payload, length); + case 3: return IEC60870_DoublePointInformation_Get_From_Buffer(object == NULL ? NULL : &object->v3, ioa, payload, length); + case 9: return IEC60870_MeasuredValueNormalized_Get_From_Buffer(object == NULL ? NULL : &object->v9, ioa, payload, length); + case 11: return IEC60870_MeasuredValueScaled_Get_From_Buffer(object == NULL ? NULL : &object->v11, ioa, payload, length); + case 13: return IEC60870_MeasuredValueShort_Get_From_Buffer(object == NULL ? NULL : &object->v13, ioa, payload, length); + case 15: return IEC60870_IntegratedTotals_Get_From_Buffer(object == NULL ? NULL : &object->v15, ioa, payload, length); + case 30: return IEC60870_SinglePointWithCP56Time2a_Get_From_Buffer(object == NULL ? NULL : &object->v30, ioa, payload, length); + case 31: return IEC60870_DoublePointWithCP56Time2a_Get_From_Buffer(object == NULL ? NULL : &object->v31, ioa, payload, length); + case 34: return IEC60870_MeasuredValueNormalizedWithCP56Time2a_Get_From_Buffer(object == NULL ? NULL : &object->v34, ioa, payload, length); + case 35: return IEC60870_MeasuredValueScaledWithCP56Time2a_Get_From_Buffer(object == NULL ? NULL : &object->v35, ioa, payload, length); + case 36: return IEC60870_MeasuredValueShortWithCP56Time2a_Get_From_Buffer(object == NULL ? NULL : &object->v36, ioa, payload, length); + case 37: return IEC60870_IntegratedTotalsWithCP56Time2a_Get_From_Buffer(object == NULL ? NULL : &object->v37, ioa, payload, length); + case 45: return IEC60870_SingleCommand_Get_From_Buffer(object == NULL ? NULL : &object->v45, ioa, payload, length); + case 46: return IEC60870_DoubleCommand_Get_From_Buffer(object == NULL ? NULL : &object->v46, ioa, payload, length); + case 47: return IEC60870_StepCommand_Get_From_Buffer(object == NULL ? NULL : &object->v47, ioa, payload, length); + case 48: return IEC60870_SetpointCommandNormalized_Get_From_Buffer(object == NULL ? NULL : &object->v48, ioa, payload, length); + case 49: return IEC60870_SetpointCommandScaled_Get_From_Buffer(object == NULL ? NULL : &object->v49, ioa, payload, length); + case 50: return IEC60870_SetpointCommandShort_Get_From_Buffer(object == NULL ? NULL : &object->v50, ioa, payload, length); + case 58: return IEC60870_SingleCommandWithCP56Time2a_Get_From_Buffer(object == NULL ? NULL : &object->v58, ioa, payload, length); + case 59: return IEC60870_DoubleCommandWithCP56Time2a_Get_From_Buffer(object == NULL ? NULL : &object->v59, ioa, payload, length); + case 60: return IEC60870_StepCommandWithCP56Time2a_Get_From_Buffer(object == NULL ? NULL : &object->v60, ioa, payload, length); + case 61: return IEC60870_SetpointCommandNormalizedWithCP56Time2a_Get_From_Buffer(object == NULL ? NULL : &object->v61, ioa, payload, length); + case 62: return IEC60870_SetpointCommandScaledWithCP56Time2a_Get_From_Buffer(object == NULL ? NULL : &object->v62, ioa, payload, length); + case 63: return IEC60870_SetpointCommandShortWithCP56Time2a_Get_From_Buffer(object == NULL ? NULL : &object->v63, ioa, payload, length); + case 100: return IEC60870_InterrogationCommand_Get_From_Buffer(object == NULL ? NULL : &object->v100, ioa, payload, length); + case 101: return IEC60870_CounterInterrogationCommand_Get_From_Buffer(object == NULL ? NULL : &object->v101, ioa, payload, length); + case 102: return IEC60870_ReadCommand_Get_From_Buffer(object == NULL ? NULL : &object->v102, ioa, payload, length); + case 103: return IEC60870_ClockSynchronizationCommand_Get_From_Buffer(object == NULL ? NULL : &object->v103, ioa, payload, length); + case 105: return IEC60870_ResetProcessCommand_Get_From_Buffer(object == NULL ? NULL : &object->v105, ioa, payload, length); + case 107: return IEC60870_TestCommandWithCP56Time2a_Get_From_Buffer(object == NULL ? NULL : &object->v107, ioa, payload, length); + default: return IEC60870_ASDU_UNSUPPORTED; + } +} + +static int Check_Object(const Vector *v, const TestObject *object) +{ + switch (v->type) { + case 1: + CHECK(object->v1.value == 1); + CHECK(object->v1.quality == 0xF0); + break; + case 3: + CHECK(object->v3.value == 3); + CHECK(object->v3.quality == 0x90); + break; + case 9: + CHECK(object->v9.value == INT16_MIN); + CHECK(object->v9.quality == 0x81); + break; + case 11: + CHECK(object->v11.value == -2); + CHECK(object->v11.quality == 0x10); + break; + case 13: + CHECK(object->v13.value == 1.5f); + CHECK(object->v13.quality == 0x41); + break; + case 15: + CHECK(object->v15.value.value == INT32_MIN); + CHECK(object->v15.value.sequence_number == 21); + CHECK(object->v15.value.has_carry == 1); + CHECK(object->v15.value.is_adjusted == 1); + CHECK(object->v15.value.is_invalid == 1); + break; + case 30: + CHECK(object->v30.base.value == 1); + CHECK(object->v30.base.quality == 0xF0); + CHECK(Check_Time(&object->v30.time)); + break; + case 31: + CHECK(object->v31.base.value == 3); + CHECK(object->v31.base.quality == 0x90); + CHECK(Check_Time(&object->v31.time)); + break; + case 34: + CHECK(object->v34.base.value == INT16_MIN); + CHECK(object->v34.base.quality == 0x81); + CHECK(Check_Time(&object->v34.time)); + break; + case 35: + CHECK(object->v35.base.value == -2); + CHECK(object->v35.base.quality == 0x10); + CHECK(Check_Time(&object->v35.time)); + break; + case 36: + CHECK(object->v36.base.value == 1.5f); + CHECK(object->v36.base.quality == 0x41); + CHECK(Check_Time(&object->v36.time)); + break; + case 37: + CHECK(object->v37.base.value.value == INT32_MIN); + CHECK(object->v37.base.value.sequence_number == 21); + CHECK(object->v37.base.value.has_carry == 1); + CHECK(object->v37.base.value.is_adjusted == 1); + CHECK(object->v37.base.value.is_invalid == 1); + CHECK(Check_Time(&object->v37.time)); + break; + case 45: + CHECK(object->v45.state == 1); + CHECK(object->v45.is_select == 1); + CHECK(object->v45.qualifier == 3); + break; + case 46: + CHECK(object->v46.state == 2); + CHECK(object->v46.is_select == 1); + CHECK(object->v46.qualifier == 1); + break; + case 47: + CHECK(object->v47.state == 1); + CHECK(object->v47.is_select == 0); + CHECK(object->v47.qualifier == 2); + break; + case 48: + CHECK(object->v48.value == INT16_MIN); + CHECK(object->v48.is_select == 1); + CHECK(object->v48.qualifier == 65); + break; + case 49: + CHECK(object->v49.value == -2); + CHECK(object->v49.is_select == 0); + CHECK(object->v49.qualifier == 127); + break; + case 50: + CHECK(object->v50.value == 1.5f); + CHECK(object->v50.is_select == 1); + CHECK(object->v50.qualifier == 0); + break; + case 58: + CHECK(object->v58.base.state == 1); + CHECK(object->v58.base.is_select == 1); + CHECK(object->v58.base.qualifier == 3); + CHECK(Check_Time(&object->v58.time)); + break; + case 59: + CHECK(object->v59.base.state == 2); + CHECK(object->v59.base.is_select == 1); + CHECK(object->v59.base.qualifier == 1); + CHECK(Check_Time(&object->v59.time)); + break; + case 60: + CHECK(object->v60.base.state == 1); + CHECK(object->v60.base.is_select == 0); + CHECK(object->v60.base.qualifier == 2); + CHECK(Check_Time(&object->v60.time)); + break; + case 61: + CHECK(object->v61.base.value == INT16_MIN); + CHECK(object->v61.base.is_select == 1); + CHECK(object->v61.base.qualifier == 65); + CHECK(Check_Time(&object->v61.time)); + break; + case 62: + CHECK(object->v62.base.value == -2); + CHECK(object->v62.base.is_select == 0); + CHECK(object->v62.base.qualifier == 127); + CHECK(Check_Time(&object->v62.time)); + break; + case 63: + CHECK(object->v63.base.value == 1.5f); + CHECK(object->v63.base.is_select == 1); + CHECK(object->v63.base.qualifier == 0); + CHECK(Check_Time(&object->v63.time)); + break; + case 100: + CHECK(object->v100.qualifier == 20); + break; + case 101: + CHECK(object->v101.qualifier == 5); + CHECK(object->v101.freeze == 3); + break; + case 102: + + break; + case 103: + + CHECK(Check_Time(&object->v103.time)); + break; + case 105: + CHECK(object->v105.qualifier == 2); + break; + case 107: + CHECK(object->v107.sequence == 0x1234); + CHECK(Check_Time(&object->v107.time)); + break; + default: CHECK(0); + } + return 1; +} + +/* 独立拼接可变宽度头,避免把容器编码结果再次用作期望值。 */ +static uint16_t Wire_Header(const IEC60870_AppLayerParameters_t *p, + uint8_t type, uint8_t vsq, uint8_t cot, uint8_t *frame) +{ + uint16_t n = 0; + frame[n++] = type; frame[n++] = vsq; frame[n++] = cot; + if (p->sizeOfCOT == 2) { frame[n++] = 0x21; } + frame[n++] = 0x67; + if (p->sizeOfCA == 2) { frame[n++] = 0x45; } + return n; +} + +static int Reject_Frame(const IEC60870_AppLayerParameters_t *p, + const uint8_t *frame, uint16_t length, IEC60870_AsduResult_t expected) +{ + IEC60870_AsduInfo_t info; + unsigned char saved[sizeof(info)]; + memset(&info, 0xA5, sizeof(info)); + memcpy(saved, &info, sizeof(info)); + CHECK(IEC60870_Asdu_Parse(p, frame, length, &info) == expected); + CHECK(memcmp(saved, &info, sizeof(info)) == 0); + return 1; +} + +/* 12种配置 x(12监视类型 x 两种SQ + 18命令类型)=504种完整布局。 + * 对命令新增编码测试;监视使用两个对象,验证连续/独立地址和所有字段。 */ +static int test_wire_matrix(void) +{ + static const uint8_t addresses[2][3] = {{0x56,0x34,0x12}, {0x57,0x34,0x12}}; + IEC60870_AppLayerParameters_t p = parameters104; + size_t v; + for (p.sizeOfCOT = 1; p.sizeOfCOT <= 2; ++p.sizeOfCOT) { + for (p.sizeOfCA = 1; p.sizeOfCA <= 2; ++p.sizeOfCA) { + for (p.sizeOfIOA = 1; p.sizeOfIOA <= 3; ++p.sizeOfIOA) { + uint32_t base = p.sizeOfIOA == 1 ? 0x56U : p.sizeOfIOA == 2 ? 0x3456U : 0x123456U; + for (v = 0; v < COUNT(vectors); ++v) { + const Vector *vector = &vectors[v]; + int sq; + for (sq = 0; sq <= (vector->command ? 0 : 1); ++sq) { + uint8_t frame[64], expected[64], saved[64]; + IEC60870_Asdu_t asdu, saved_asdu; + IEC60870_AsduInfo_t info; + IEC60870_AsduHeader_t header = Header(); + TestObject input, output; + const uint8_t *data = NULL; + uint16_t length = 77, h, n; + int i, count = vector->command ? 1 : 2; + header.common_address = p.sizeOfCA == 1 ? 0x67 : 0x4567; + header.cause_of_transmission = vector->command ? 6 : 3; + header.is_negative_confirm = vector->command; + memset(frame, 0xA5, sizeof(frame)); + CHECK(IEC60870_Asdu_Create(&asdu, &p, frame + 1, 62, &header, sq) == IEC60870_ASDU_OK); + CHECK(asdu.type_id == 0 && frame[1] == 0 && asdu.object_count == 0); + CHECK(IEC60870_Asdu_Get_Data(&asdu, &data, &length) == IEC60870_ASDU_ERROR); + CHECK(data == NULL && length == 77); + h = Wire_Header(&p, vector->type, (uint8_t)(count | (sq << 7)), + vector->command ? 0xC6 : 0x83, expected); + n = h; + for (i = 0; i < count; ++i) { + CHECK(Sample_Object(vector->type, base + (uint32_t)i, &input) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, Object_Base(&input, vector->type)) == IEC60870_ASDU_OK); + if (!sq || i == 0) { + memcpy(expected + n, addresses[i], (size_t)p.sizeOfIOA); + n += (uint16_t)p.sizeOfIOA; + } + memcpy(expected + n, vector->payload, vector->size); n += vector->size; + } + CHECK(IEC60870_Asdu_Get_Data(&asdu, &data, &length) == IEC60870_ASDU_OK); + CHECK(length == n && data == frame + 1 && memcmp(data, expected, n) == 0); + CHECK(frame[0] == 0xA5 && frame[n + 1] == 0xA5 && asdu.parameters == &p); + CHECK(IEC60870_Asdu_Parse(&p, expected, n, &info) == IEC60870_ASDU_OK); + CHECK(info.type_id == vector->type && info.object_count == count && info.is_sequence == sq); + CHECK(info.parameters == &p && info.information_objects == expected + h); + CHECK(info.common_address == header.common_address && info.is_test == 1); + CHECK(info.originator_address == (p.sizeOfCOT == 2 ? 0x21 : 0)); + CHECK(info.cause_of_transmission == (vector->command ? 6 : 3)); + CHECK(info.is_negative_confirm == vector->command); + for (i = 0; i < count; ++i) { + unsigned char snapshot[sizeof(output)]; + memset(&output, 0xA5, sizeof(output)); memcpy(snapshot, &output, sizeof(output)); + CHECK(IEC60870_Asdu_Get_Element(&info, (uint8_t)i, &output, Object_Size(vector->type) - 1) == IEC60870_ASDU_ERROR); + CHECK(memcmp(snapshot, &output, sizeof(output)) == 0); + CHECK(IEC60870_Asdu_Get_Element(&info, (uint8_t)i, &output, Object_Size(vector->type)) == IEC60870_ASDU_OK); + CHECK(Object_Base(&output, vector->type)->ioa == base + (uint32_t)i); + CHECK(IEC60870_InformationObject_Get_Type(Object_Base(&output, vector->type)) == vector->type); + CHECK(Check_Object(vector, &output)); + CHECK(memcmp((unsigned char *)&output + Object_Size(vector->type), + snapshot + Object_Size(vector->type), sizeof(output) - Object_Size(vector->type)) == 0); + } + for (i = 0; i < n; ++i) { CHECK(Reject_Frame(&p, expected, (uint16_t)i, IEC60870_ASDU_ERROR)); } + expected[n] = 0; + CHECK(Reject_Frame(&p, expected, (uint16_t)(n + 1), IEC60870_ASDU_ERROR)); + /* 命令数量上限;第二个对象不改变容器或原文。 */ + if (vector->command) { + memcpy(saved, frame, sizeof(frame)); memcpy(&saved_asdu, &asdu, sizeof(asdu)); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, Object_Base(&input, vector->type)) == IEC60870_ASDU_FULL); + CHECK(memcmp(saved, frame, sizeof(frame)) == 0 && memcmp(&saved_asdu, &asdu, sizeof(asdu)) == 0); + CHECK(IEC60870_Asdu_Create(&asdu, &p, frame, sizeof(frame), &header, 1) == IEC60870_ASDU_OK); + memcpy(saved, frame, sizeof(frame)); memcpy(&saved_asdu, &asdu, sizeof(asdu)); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, Object_Base(&input, vector->type)) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, frame, sizeof(frame)) == 0 && memcmp(&saved_asdu, &asdu, sizeof(asdu)) == 0); + } + } + } + } + } + } + return 1; +} + +static int test_object_entrypoints(void) +{ + size_t v; + for (v = 0; v < COUNT(vectors); ++v) { + const Vector *vector = &vectors[v]; + TestObject input, output; + unsigned char snapshot[sizeof(output)]; + uint8_t encoded[20], saved[20], payload[13]; + uint16_t length = 99, n; + CHECK(Sample_Object(vector->type, 0x123456, &input) == IEC60870_ASDU_OK); + memset(encoded, 0xA5, sizeof(encoded)); + CHECK(IEC60870_InformationObject_Encode(Object_Base(&input, vector->type), ¶meters104, 0, + encoded, sizeof(encoded), &length) == IEC60870_ASDU_OK); + CHECK(length == vector->size + 3U && encoded[0] == 0x56 && encoded[1] == 0x34 && encoded[2] == 0x12); + CHECK(memcmp(encoded + 3, vector->payload, vector->size) == 0 && encoded[length] == 0xA5); + memcpy(saved, encoded, sizeof(saved)); length = 99; + CHECK(IEC60870_InformationObject_Encode(Object_Base(&input, vector->type), ¶meters104, 0, + encoded, (uint16_t)(vector->size + 2), &length) == IEC60870_ASDU_FULL); + CHECK(length == 99 && memcmp(encoded, saved, sizeof(saved)) == 0); + if (!vector->command) { + CHECK(IEC60870_InformationObject_Encode(Object_Base(&input, vector->type), ¶meters104, 1, + encoded, sizeof(encoded), &length) == IEC60870_ASDU_OK); + CHECK(length == vector->size && memcmp(encoded, vector->payload, length) == 0); + } else { + CHECK(IEC60870_InformationObject_Encode(Object_Base(&input, vector->type), ¶meters104, 1, + encoded, sizeof(encoded), &length) == IEC60870_ASDU_ERROR); + } + memcpy(payload, vector->payload, vector->size); + CHECK(Parse_Object(vector->type, 0x123456, payload, vector->size, &output) == IEC60870_ASDU_OK); + memcpy(payload + 1, vector->payload, vector->size); + CHECK(Parse_Object(vector->type, 0x123456, payload + 1, vector->size, &output) == IEC60870_ASDU_OK); + memset(payload, 0, sizeof(payload)); /* 按值对象不借用载荷。 */ + CHECK(Check_Object(vector, &output)); + CHECK(Object_Base(&output, vector->type)->ioa == 0x123456); + memset(&output, 0xA5, sizeof(output)); memcpy(snapshot, &output, sizeof(output)); + for (n = 0; n < vector->size; ++n) { + CHECK(Parse_Object(vector->type, 0, vector->payload, n, &output) == IEC60870_ASDU_ERROR); + CHECK(memcmp(snapshot, &output, sizeof(output)) == 0); + } + CHECK(Parse_Object(vector->type, 0, vector->payload, (uint16_t)(vector->size + 1), &output) == IEC60870_ASDU_ERROR); + CHECK(Parse_Object(vector->type, 0x1000000, vector->payload, vector->size, &output) == IEC60870_ASDU_ERROR); + CHECK(Parse_Object(vector->type, 0, vector->payload, vector->size, NULL) == IEC60870_ASDU_ERROR); + if (vector->size != 0U) { + CHECK(Parse_Object(vector->type, 0, NULL, vector->size, &output) == IEC60870_ASDU_ERROR); + } + CHECK(memcmp(snapshot, &output, sizeof(output)) == 0); + Object_Base(&input, vector->type)->virtual_function_table = NULL; + CHECK(IEC60870_InformationObject_Get_Type(Object_Base(&input, vector->type)) == 0); + CHECK(IEC60870_InformationObject_Encode(Object_Base(&input, vector->type), ¶meters104, 0, + encoded, sizeof(encoded), &length) == IEC60870_ASDU_ERROR); + } + { + IEC60870_ReadCommand_t read; + CHECK(IEC60870_ReadCommand_Get_From_Buffer(&read, 7, NULL, 0) == IEC60870_ASDU_OK && read.object.ioa == 7); + } + return 1; +} + +static int test_capacity_and_addresses(void) +{ + IEC60870_AppLayerParameters_t p = parameters104; + IEC60870_Asdu_t asdu, saved_asdu; + IEC60870_AsduInfo_t info; + IEC60870_AsduHeader_t header = Header(); + IEC60870_MeasuredValueNormalized_t value; + IEC60870_SinglePointInformation_t point, decoded; + uint8_t frame[300], before[300]; + const uint8_t *data = NULL; + uint16_t length = 0; + unsigned int i; + memset(frame, 0xA5, sizeof(frame)); + CHECK(IEC60870_MeasuredValueNormalized_Create(&value, 0, -1, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Create(&asdu, &p, frame, sizeof(frame), &header, 1) == IEC60870_ASDU_OK); + for (i = 0; i < 80; ++i) { + value.object.ioa = i; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &value.object) == IEC60870_ASDU_OK); + } + CHECK(IEC60870_Asdu_Get_Data(&asdu, &data, &length) == IEC60870_ASDU_OK && length == 249); + CHECK(asdu.capacity == 249 && frame[1] == 0xD0 && frame[249] == 0xA5); + memcpy(before, frame, sizeof(frame)); memcpy(&saved_asdu, &asdu, sizeof(asdu)); + value.object.ioa = 80; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &value.object) == IEC60870_ASDU_FULL); + CHECK(memcmp(before, frame, sizeof(frame)) == 0 && memcmp(&saved_asdu, &asdu, sizeof(asdu)) == 0); + /* FULL后开启下一条并重试原对象。 */ + CHECK(IEC60870_Asdu_Create(&asdu, &p, frame, sizeof(frame), &header, 1) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &value.object) == IEC60870_ASDU_OK && asdu.object_count == 1); + + p.sizeOfCOT = 1; p.sizeOfCA = 1; p.sizeOfIOA = 2; p.maxSizeOfASDU = 254; + header.common_address = 0x67; + CHECK(IEC60870_Asdu_Create(&asdu, &p, frame, sizeof(frame), &header, 0) == IEC60870_ASDU_OK); + for (i = 0; i < 50; ++i) { + value.object.ioa = i; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &value.object) == IEC60870_ASDU_OK); + } + CHECK(asdu.length == 254 && asdu.capacity == 254 && frame[254] == 0xA5); + memcpy(before, frame, sizeof(frame)); memcpy(&saved_asdu, &asdu, sizeof(asdu)); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &value.object) == IEC60870_ASDU_FULL); + CHECK(memcmp(before, frame, sizeof(frame)) == 0 && memcmp(&saved_asdu, &asdu, sizeof(asdu)) == 0); + CHECK(IEC60870_Asdu_Parse(&p, frame, 254, &info) == IEC60870_ASDU_OK); + CHECK(!IEC60870_Parse_ASDU_Header(¶meters104, frame, 254, &info)); + + /* 首对象装不下:类型和VSQ保持空状态;参数上限与实际容量都要生效。 */ + for (i = 0; i < 3; ++i) { + p.maxSizeOfASDU = i == 0 ? 4 : i == 1 ? 8 : 254; + CHECK(IEC60870_Asdu_Create(&asdu, &p, frame, i == 2 ? 8 : sizeof(frame), &header, 0) == IEC60870_ASDU_OK); + memcpy(before, frame, sizeof(frame)); memcpy(&saved_asdu, &asdu, sizeof(asdu)); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &value.object) == IEC60870_ASDU_FULL); + CHECK(asdu.type_id == 0 && asdu.object_count == 0 && asdu.length == 4); + CHECK(memcmp(before, frame, sizeof(frame)) == 0 && memcmp(&saved_asdu, &asdu, sizeof(asdu)) == 0); + } + p.maxSizeOfASDU = 9; + CHECK(IEC60870_Asdu_Create(&asdu, &p, frame, sizeof(frame), &header, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &value.object) == IEC60870_ASDU_OK && asdu.length == 9); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &value.object) == IEC60870_ASDU_FULL); + p.maxSizeOfASDU = 254; + + CHECK(IEC60870_SinglePointInformation_Create(&point, 0, 1, 0) == IEC60870_ASDU_OK); + for (p.sizeOfIOA = 1; p.sizeOfIOA <= 3; ++p.sizeOfIOA) { + uint32_t max_ioa = p.sizeOfIOA == 1 ? 0xFFU : p.sizeOfIOA == 2 ? 0xFFFFU : 0xFFFFFFU; + CHECK(IEC60870_Asdu_Create(&asdu, &p, frame, sizeof(frame), &header, 1) == IEC60870_ASDU_OK); + point.object.ioa = max_ioa - 1U; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_OK); + ++point.object.ioa; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Parse(&p, frame, asdu.length, &info) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Get_Element(&info, 1, &decoded, sizeof(decoded)) == IEC60870_ASDU_OK && decoded.object.ioa == max_ioa); + memcpy(before, frame, sizeof(frame)); memcpy(&saved_asdu, &asdu, sizeof(asdu)); + ++point.object.ioa; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_ERROR); + point.object.ioa = 0; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_ERROR); + CHECK(memcmp(before, frame, sizeof(frame)) == 0 && memcmp(&saved_asdu, &asdu, sizeof(asdu)) == 0); + frame[4] = 0xFF; /* SQ首地址改为上限,隐含的第二个地址溢出。 */ + CHECK(Reject_Frame(&p, frame, asdu.length, IEC60870_ASDU_ERROR)); + CHECK(IEC60870_Asdu_Create(&asdu, &p, frame, sizeof(frame), &header, 1) == IEC60870_ASDU_OK); + for (i = 0; i < 127; ++i) { + point.object.ioa = i; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_OK); + } + point.object.ioa = 127; + memcpy(before, frame, sizeof(frame)); memcpy(&saved_asdu, &asdu, sizeof(asdu)); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_FULL); + CHECK(frame[1] == 0xFF && memcmp(before, frame, sizeof(frame)) == 0); + CHECK(memcmp(&saved_asdu, &asdu, sizeof(asdu)) == 0); + } + /* SQ=0允许不连续的独立地址。 */ + p.sizeOfIOA = 3; + CHECK(IEC60870_Asdu_Create(&asdu, &p, frame, sizeof(frame), &header, 0) == IEC60870_ASDU_OK); + point.object.ioa = 7; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_OK); + point.object.ioa = 0x123456; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Parse(&p, frame, asdu.length, &info) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Get_Element(&info, 1, &decoded, sizeof(decoded)) == IEC60870_ASDU_OK && decoded.object.ioa == 0x123456); + return 1; +} + +static int test_malformed_payloads(void) +{ + size_t v; + for (v = 0; v < COUNT(vectors); ++v) { + const Vector *vector = &vectors[v]; + uint8_t frame[64]; + uint16_t n = Wire_Header(¶meters104, vector->type, 1, 3, frame); + uint16_t h = n; + TestObject output; + unsigned char saved[sizeof(output)]; + frame[n++] = 0; frame[n++] = 0; frame[n++] = 0; + memcpy(frame + n, vector->payload, vector->size); n += vector->size; + if (vector->command) { + frame[1] = 0x81; + CHECK(Reject_Frame(¶meters104, frame, n, IEC60870_ASDU_ERROR)); + frame[1] = 2; + memcpy(frame + n, frame + h, n - h); + CHECK(Reject_Frame(¶meters104, frame, (uint16_t)(n + n - h), IEC60870_ASDU_ERROR)); + frame[1] = 1; + } + if (vector->timed) { + frame[n - 5] |= 0x40; /* CP56分钟保留位。 */ + } else if (vector->type == 1 || vector->type == 45) { + frame[h + 3] |= 2; + } else if (vector->type == 3) { + frame[h + 3] |= 4; + } else if (vector->type == 9 || vector->type == 11 || vector->type == 13) { + frame[n - 1] |= 2; + } else if (vector->type == 46 || vector->type == 47) { + frame[h + 3] |= 3; /* 双命令/步调节状态3非法。 */ + } else { continue; } + CHECK(Reject_Frame(¶meters104, frame, n, IEC60870_ASDU_ERROR)); + memset(&output, 0xA5, sizeof(output)); memcpy(saved, &output, sizeof(output)); + CHECK(Parse_Object(vector->type, 0, frame + h + 3, vector->size, &output) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &output, sizeof(output)) == 0); + } + { + uint8_t frame[] = {1,0x82,3,0,1,0,0,0,0,1,2}; + CHECK(Reject_Frame(¶meters104, frame, sizeof(frame), IEC60870_ASDU_ERROR)); /* 后部对象错误 */ + frame[1] = 0; + CHECK(Reject_Frame(¶meters104, frame, 6, IEC60870_ASDU_ERROR)); + frame[0] = 250; + CHECK(Reject_Frame(¶meters104, frame, 6, IEC60870_ASDU_UNSUPPORTED)); + } + return 1; +} + +static int test_constructor_failures(void) +{ + TestObject object; + unsigned char saved[sizeof(object)]; + IEC60870_CP56Time2a_t time = Sample_Time(); + IEC60870_CP56Time2a_t bad_time = time; + IEC60870_BinaryCounterReading_t counter = {INT32_MIN, 21, 1, 1, 1}; + IEC60870_BinaryCounterReading_t bad_counter = counter; + bad_time.time.day = 30; /* 不存在的2024-02-30。 */ + bad_counter.sequence_number = 32; + memset(&object, 0xA5, sizeof(object)); memcpy(saved, &object, sizeof(object)); + CHECK(IEC60870_SinglePointInformation_Create(NULL, 0, 1, 0xF0) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SinglePointInformation_Create(&object.v1, 0x1000000, 1, 0xF0) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SinglePointInformation_Create(&object.v1, 0, 1, 2) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_DoublePointInformation_Create(NULL, 0, 3, 0x90) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_DoublePointInformation_Create(&object.v3, 0x1000000, 3, 0x90) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_DoublePointInformation_Create(&object.v3, 0, 3, 2) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_MeasuredValueNormalized_Create(NULL, 0, INT16_MIN, 0x81) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_MeasuredValueNormalized_Create(&object.v9, 0x1000000, INT16_MIN, 0x81) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_MeasuredValueNormalized_Create(&object.v9, 0, 0, 2) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_MeasuredValueScaled_Create(NULL, 0, -2, 0x10) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_MeasuredValueScaled_Create(&object.v11, 0x1000000, -2, 0x10) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_MeasuredValueScaled_Create(&object.v11, 0, 0, 2) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_MeasuredValueShort_Create(NULL, 0, 1.5f, 0x41) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_MeasuredValueShort_Create(&object.v13, 0x1000000, 1.5f, 0x41) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_MeasuredValueShort_Create(&object.v13, 0, 0, 2) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_IntegratedTotals_Create(NULL, 0, &counter) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_IntegratedTotals_Create(&object.v15, 0x1000000, &counter) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_IntegratedTotals_Create(&object.v15, 0, &bad_counter) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_SinglePointWithCP56Time2a_Create(NULL, 0, 1, 0xF0, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SinglePointWithCP56Time2a_Create(&object.v30, 0x1000000, 1, 0xF0, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SinglePointWithCP56Time2a_Create(&object.v30, 0, 1, 0xF0, &bad_time) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_DoublePointWithCP56Time2a_Create(NULL, 0, 3, 0x90, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_DoublePointWithCP56Time2a_Create(&object.v31, 0x1000000, 3, 0x90, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_DoublePointWithCP56Time2a_Create(&object.v31, 0, 3, 0x90, &bad_time) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_MeasuredValueNormalizedWithCP56Time2a_Create(NULL, 0, INT16_MIN, 0x81, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_MeasuredValueNormalizedWithCP56Time2a_Create(&object.v34, 0x1000000, INT16_MIN, 0x81, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_MeasuredValueNormalizedWithCP56Time2a_Create(&object.v34, 0, INT16_MIN, 0x81, &bad_time) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_MeasuredValueScaledWithCP56Time2a_Create(NULL, 0, -2, 0x10, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_MeasuredValueScaledWithCP56Time2a_Create(&object.v35, 0x1000000, -2, 0x10, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_MeasuredValueScaledWithCP56Time2a_Create(&object.v35, 0, -2, 0x10, &bad_time) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_MeasuredValueShortWithCP56Time2a_Create(NULL, 0, 1.5f, 0x41, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_MeasuredValueShortWithCP56Time2a_Create(&object.v36, 0x1000000, 1.5f, 0x41, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_MeasuredValueShortWithCP56Time2a_Create(&object.v36, 0, 1.5f, 0x41, &bad_time) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_IntegratedTotalsWithCP56Time2a_Create(NULL, 0, &counter, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_IntegratedTotalsWithCP56Time2a_Create(&object.v37, 0x1000000, &counter, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_IntegratedTotalsWithCP56Time2a_Create(&object.v37, 0, &counter, &bad_time) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_SingleCommand_Create(NULL, 0, 1, 1, 3) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SingleCommand_Create(&object.v45, 0x1000000, 1, 1, 3) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SingleCommand_Create(&object.v45, 0, 2, 1, 3) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_DoubleCommand_Create(NULL, 0, 2, 1, 1) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_DoubleCommand_Create(&object.v46, 0x1000000, 2, 1, 1) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_DoubleCommand_Create(&object.v46, 0, 0, 1, 1) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_StepCommand_Create(NULL, 0, 1, 0, 2) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_StepCommand_Create(&object.v47, 0x1000000, 1, 0, 2) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_StepCommand_Create(&object.v47, 0, 3, 0, 2) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_SetpointCommandNormalized_Create(NULL, 0, INT16_MIN, 1, 65) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SetpointCommandNormalized_Create(&object.v48, 0x1000000, INT16_MIN, 1, 65) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SetpointCommandNormalized_Create(&object.v48, 0, 0, 1, 128) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_SetpointCommandScaled_Create(NULL, 0, -2, 0, 127) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SetpointCommandScaled_Create(&object.v49, 0x1000000, -2, 0, 127) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SetpointCommandScaled_Create(&object.v49, 0, 0, 2, 0) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_SetpointCommandShort_Create(NULL, 0, 1.5f, 1, 0) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SetpointCommandShort_Create(&object.v50, 0x1000000, 1.5f, 1, 0) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SetpointCommandShort_Create(&object.v50, 0, 0, -1, 0) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_SingleCommandWithCP56Time2a_Create(NULL, 0, 1, 1, 3, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SingleCommandWithCP56Time2a_Create(&object.v58, 0x1000000, 1, 1, 3, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SingleCommandWithCP56Time2a_Create(&object.v58, 0, 1, 1, 3, &bad_time) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_DoubleCommandWithCP56Time2a_Create(NULL, 0, 2, 1, 1, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_DoubleCommandWithCP56Time2a_Create(&object.v59, 0x1000000, 2, 1, 1, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_DoubleCommandWithCP56Time2a_Create(&object.v59, 0, 2, 1, 1, &bad_time) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_StepCommandWithCP56Time2a_Create(NULL, 0, 1, 0, 2, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_StepCommandWithCP56Time2a_Create(&object.v60, 0x1000000, 1, 0, 2, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_StepCommandWithCP56Time2a_Create(&object.v60, 0, 1, 0, 2, &bad_time) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_SetpointCommandNormalizedWithCP56Time2a_Create(NULL, 0, INT16_MIN, 1, 65, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SetpointCommandNormalizedWithCP56Time2a_Create(&object.v61, 0x1000000, INT16_MIN, 1, 65, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SetpointCommandNormalizedWithCP56Time2a_Create(&object.v61, 0, INT16_MIN, 1, 65, &bad_time) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_SetpointCommandScaledWithCP56Time2a_Create(NULL, 0, -2, 0, 127, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SetpointCommandScaledWithCP56Time2a_Create(&object.v62, 0x1000000, -2, 0, 127, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SetpointCommandScaledWithCP56Time2a_Create(&object.v62, 0, -2, 0, 127, &bad_time) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_SetpointCommandShortWithCP56Time2a_Create(NULL, 0, 1.5f, 1, 0, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SetpointCommandShortWithCP56Time2a_Create(&object.v63, 0x1000000, 1.5f, 1, 0, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SetpointCommandShortWithCP56Time2a_Create(&object.v63, 0, 1.5f, 1, 0, &bad_time) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_InterrogationCommand_Create(NULL, 0, 20) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_InterrogationCommand_Create(&object.v100, 0x1000000, 20) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_InterrogationCommand_Create(&object.v100, 0, 256) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_CounterInterrogationCommand_Create(NULL, 0, 5, 3) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_CounterInterrogationCommand_Create(&object.v101, 0x1000000, 5, 3) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_CounterInterrogationCommand_Create(&object.v101, 0, 64, 0) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_ReadCommand_Create(NULL, 0) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_ReadCommand_Create(&object.v102, 0x1000000) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_ClockSynchronizationCommand_Create(NULL, 0, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_ClockSynchronizationCommand_Create(&object.v103, 0x1000000, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_ClockSynchronizationCommand_Create(&object.v103, 0, &bad_time) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_ResetProcessCommand_Create(NULL, 0, 2) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_ResetProcessCommand_Create(&object.v105, 0x1000000, 2) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_ResetProcessCommand_Create(&object.v105, 0, -1) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_TestCommandWithCP56Time2a_Create(NULL, 0, 0x1234, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_TestCommandWithCP56Time2a_Create(&object.v107, 0x1000000, 0x1234, &time) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_TestCommandWithCP56Time2a_Create(&object.v107, 0, 0, &bad_time) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + CHECK(IEC60870_SinglePointInformation_Create(&object.v1, 0, -1, 0) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_DoublePointInformation_Create(&object.v3, 0, 4, 0) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SingleCommand_Create(&object.v45, 0, 1, 0, 32) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_CounterInterrogationCommand_Create(&object.v101, 0, 1, 4) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_IntegratedTotals_Create(&object.v15, 0, NULL) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_SinglePointWithCP56Time2a_Create(&object.v30, 0, 1, 0, NULL) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_ClockSynchronizationCommand_Create(&object.v103, 0, NULL) == IEC60870_ASDU_ERROR); + CHECK(memcmp(saved, &object, sizeof(object)) == 0); + return 1; +} + +/* 编码时重新检查对象,不依赖Create曾经检查过字段。 */ +static int test_add_atomicity(void) +{ + uint8_t frame[64], before[64]; + IEC60870_Asdu_t asdu, saved; + IEC60870_AsduHeader_t header = Header(); + IEC60870_SinglePointInformation_t point; + IEC60870_DoublePointInformation_t double_point; + IEC60870_SinglePointWithCP56Time2a_t timed; + IEC60870_CP56Time2a_t time = Sample_Time(); + IEC60870_AsduInfo_t info; + unsigned char object_before[sizeof(point)]; + CHECK(IEC60870_SinglePointInformation_Create(&point, 5, 1, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_DoublePointInformation_Create(&double_point, 6, 2, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Create(&asdu, ¶meters104, frame, sizeof(frame), &header, 1) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_OK); + memcpy(before, frame, sizeof(frame)); memcpy(&saved, &asdu, sizeof(asdu)); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &double_point.object) == IEC60870_ASDU_ERROR); + point.object.ioa = 7; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_ERROR); + point.object.ioa = 6; point.quality = 2; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_ERROR); + point.quality = 0; point.value = 2; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_ERROR); + point.value = 1; point.object.virtual_function_table = double_point.object.virtual_function_table; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_ERROR); + point.object.type_id = 250; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_UNSUPPORTED); + CHECK(memcmp(before, frame, sizeof(frame)) == 0 && memcmp(&saved, &asdu, sizeof(asdu)) == 0); + CHECK(IEC60870_Asdu_Parse(¶meters104, frame, asdu.length, &info) == IEC60870_ASDU_OK); + memset(&point, 0xA5, sizeof(point)); memcpy(object_before, &point, sizeof(point)); + CHECK(IEC60870_Asdu_Get_Element(&info, 1, &point, sizeof(point)) == IEC60870_ASDU_ERROR); + frame[9] |= 2; /* 视图建立后修改原文,取对象仍验证选中载荷。 */ + CHECK(IEC60870_Asdu_Get_Element(&info, 0, &point, sizeof(point)) == IEC60870_ASDU_ERROR); + CHECK(memcmp(object_before, &point, sizeof(point)) == 0); + CHECK(IEC60870_SinglePointWithCP56Time2a_Create(&timed, 0, 1, 0, &time) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Create(&asdu, ¶meters104, frame, sizeof(frame), &header, 0) == IEC60870_ASDU_OK); + memcpy(before, frame, sizeof(frame)); memcpy(&saved, &asdu, sizeof(asdu)); + timed.time.time.day = 30; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &timed.base.object) == IEC60870_ASDU_ERROR); + CHECK(memcmp(before, frame, sizeof(frame)) == 0 && memcmp(&saved, &asdu, sizeof(asdu)) == 0); + CHECK(IEC60870_Asdu_Create(&asdu, ¶meters104, frame, 5, &header, 0) == IEC60870_ASDU_FULL); + header.is_test = 2; + CHECK(IEC60870_Asdu_Create(&asdu, ¶meters104, frame, sizeof(frame), &header, 0) == IEC60870_ASDU_ERROR); + header = Header(); header.cause_of_transmission = 64; + CHECK(IEC60870_Asdu_Create(&asdu, ¶meters104, frame, sizeof(frame), &header, 0) == IEC60870_ASDU_ERROR); + header = Header(); header.is_negative_confirm = 2; + CHECK(IEC60870_Asdu_Create(&asdu, ¶meters104, frame, sizeof(frame), &header, 0) == IEC60870_ASDU_ERROR); + header = Header(); + CHECK(IEC60870_Asdu_Create(&asdu, ¶meters104, frame, sizeof(frame), &header, 2) == IEC60870_ASDU_ERROR); + CHECK(memcmp(before, frame, sizeof(frame)) == 0 && memcmp(&saved, &asdu, sizeof(asdu)) == 0); + return 1; +} + +static int test_numeric_bits(void) +{ + static const uint32_t bits[] = {0,0x80000000U,0x00000001U,0x7F7FFFFFU,0x7F800000U,0xFF800000U,0x7FC12345U}; + static const uint8_t float_types[] = {13,36,50,63}; + static const uint8_t time_bytes[] = {TIME_BYTES}; + size_t i, t; + for (t = 0; t < COUNT(float_types); ++t) { + for (i = 0; i < COUNT(bits); ++i) { + uint8_t payload[12] = {0}, encoded[16]; + TestObject object; + uint16_t length, size = (float_types[t] == 36 || float_types[t] == 63) ? 12 : 5; + unsigned int b; + for (b = 0; b < 4; ++b) { payload[b] = (uint8_t)(bits[i] >> (b * 8)); } + if (size == 12) { memcpy(payload + 5, time_bytes, 7); } + CHECK(Parse_Object(float_types[t], 0, payload, size, &object) == IEC60870_ASDU_OK); + CHECK(IEC60870_InformationObject_Encode(Object_Base(&object, float_types[t]), ¶meters104, + 0, encoded, sizeof(encoded), &length) == IEC60870_ASDU_OK); + CHECK(length == size + 3U && memcmp(encoded + 3, payload, size) == 0); + } + } + { + static const int16_t values[] = {INT16_MIN,-1,0,INT16_MAX}; + static const uint8_t expected[][2] = {{0,0x80},{0xFF,0xFF},{0,0},{0xFF,0x7F}}; + IEC60870_MeasuredValueNormalized_t object, decoded; + uint8_t encoded[6]; + uint16_t length; + for (i = 0; i < COUNT(values); ++i) { + CHECK(IEC60870_MeasuredValueNormalized_Create(&object, 0, values[i], 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_InformationObject_Encode(&object.object, ¶meters104, 0, encoded, sizeof(encoded), &length) == IEC60870_ASDU_OK); + CHECK(length == 6 && memcmp(encoded + 3, expected[i], 2) == 0); + CHECK(IEC60870_MeasuredValueNormalized_Get_From_Buffer(&decoded, 0, encoded + 3, 3) == IEC60870_ASDU_OK && decoded.value == values[i]); + } + } + { + static const int32_t values[] = {INT32_MIN,-1,0,INT32_MAX}; + static const uint8_t expected[][4] = { + {0,0,0,0x80}, {0xFF,0xFF,0xFF,0xFF}, {0,0,0,0}, {0xFF,0xFF,0xFF,0x7F} + }; + IEC60870_IntegratedTotals_t object, decoded; + IEC60870_BinaryCounterReading_t counter = {0,31,1,1,1}; + uint8_t encoded[8]; + uint16_t length; + for (i = 0; i < COUNT(values); ++i) { + counter.value = values[i]; + CHECK(IEC60870_IntegratedTotals_Create(&object, 0, &counter) == IEC60870_ASDU_OK); + CHECK(IEC60870_InformationObject_Encode(&object.object, ¶meters104, 0, encoded, sizeof(encoded), &length) == IEC60870_ASDU_OK); + CHECK(length == 8 && memcmp(encoded + 3, expected[i], 4) == 0 && encoded[7] == 0xFF); + CHECK(IEC60870_IntegratedTotals_Get_From_Buffer(&decoded, 0, encoded + 3, 5) == IEC60870_ASDU_OK); + CHECK(decoded.value.value == values[i] && decoded.value.sequence_number == 31); + CHECK(decoded.value.has_carry == 1 && decoded.value.is_adjusted == 1 && decoded.value.is_invalid == 1); + } + } + return 1; +} + +static int test_parameters_and_nulls(void) +{ + static const IEC60870_AppLayerParameters_t invalid[] = { + {0,1,2,0,2,3,249}, {2,1,2,0,2,3,249}, {1,0,2,0,2,3,249}, + {1,2,2,0,2,3,249}, {1,1,0,0,2,3,249}, {1,1,3,0,2,3,249}, + {1,1,2,0,0,3,249}, {1,1,2,0,3,3,249}, {1,1,2,0,2,0,249}, + {1,1,2,0,2,4,249}, {1,1,2,-1,2,3,249}, {1,1,2,256,2,3,249}, + {1,1,2,0,2,3,5}, {1,1,2,0,2,3,255}, {1,1,2,0,2,3,INT_MAX}, + {1,1,INT_MIN,0,2,3,249}, {1,1,2,0,INT_MAX,3,249}, {1,1,2,0,2,-1,249} + }; + IEC60870_Asdu_t asdu, second, saved; + IEC60870_AsduInfo_t info; + IEC60870_AsduHeader_t header = Header(); + IEC60870_SinglePointInformation_t point; + IEC60870_AppLayerParameters_t p = {1,1,1,0,1,1,249}; + uint8_t frame[32], other[32], before[32]; + const uint8_t request[] = {1,1,3,0,1,0,1,0,0,1}; + const uint8_t *data = frame; + uint16_t length = 77; + size_t i; + CHECK(IEC60870_SinglePointInformation_Create(&point, 0, 1, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_AppLayerParameters_Valid(¶meters104)); + for (i = 0; i <= COUNT(invalid); ++i) { + const IEC60870_AppLayerParameters_t *bad = i == COUNT(invalid) ? NULL : &invalid[i]; + CHECK(!IEC60870_AppLayerParameters_Valid(bad)); + memset(frame, 0xA5, sizeof(frame)); memcpy(before, frame, sizeof(frame)); + memset(&asdu, 0xA5, sizeof(asdu)); memcpy(&saved, &asdu, sizeof(asdu)); + CHECK(IEC60870_Asdu_Create(&asdu, bad, frame, sizeof(frame), &header, 0) == IEC60870_ASDU_ERROR); + CHECK(memcmp(&saved, &asdu, sizeof(asdu)) == 0); + CHECK(Reject_Frame(bad, request, sizeof(request), IEC60870_ASDU_ERROR)); + CHECK(IEC60870_InformationObject_Encode(&point.object, bad, 0, frame, sizeof(frame), &length) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Build_Response(bad, request, sizeof(request), 7, 0, frame, sizeof(frame), &length) == IEC60870_ASDU_ERROR); + CHECK(length == 77 && memcmp(before, frame, sizeof(frame)) == 0); + } + header.common_address = 256; + CHECK(IEC60870_Asdu_Create(&asdu, &p, frame, sizeof(frame), &header, 0) == IEC60870_ASDU_ERROR); + CHECK(memcmp(&saved, &asdu, sizeof(asdu)) == 0 && memcmp(before, frame, sizeof(frame)) == 0); + header.common_address = 255; + CHECK(IEC60870_Asdu_Create(&asdu, &p, frame, 3, &header, 0) == IEC60870_ASDU_FULL); + CHECK(IEC60870_Asdu_Create(&asdu, &p, frame, sizeof(frame), &header, 0) == IEC60870_ASDU_OK); + CHECK(frame[3] == 255 && frame[4] == 0xA5); + CHECK(IEC60870_Asdu_Create(NULL, &p, frame, sizeof(frame), &header, 0) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Create(&asdu, &p, NULL, sizeof(frame), &header, 0) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Create(&asdu, &p, frame, sizeof(frame), NULL, 0) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Add_InformationObject(NULL, &point.object) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, NULL) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Get_Data(NULL, &data, &length) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Parse(&p, NULL, 8, &info) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Parse(¶meters104, request, sizeof(request), NULL) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Get_Element(NULL, 0, &point, sizeof(point)) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Parse(¶meters104, request, sizeof(request), &info) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Get_Element(&info, 0, NULL, sizeof(point)) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_InformationObject_Get_Type(NULL) == 0); + CHECK(IEC60870_InformationObject_Encode(NULL, &p, 0, frame, sizeof(frame), &length) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_InformationObject_Encode(&point.object, &p, 2, frame, sizeof(frame), &length) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_InformationObject_Encode(&point.object, &p, 0, NULL, sizeof(frame), &length) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_InformationObject_Encode(&point.object, &p, 0, frame, sizeof(frame), NULL) == IEC60870_ASDU_ERROR); + CHECK(length == 77 && data == frame); + point.object.ioa = 256; + CHECK(IEC60870_InformationObject_Encode(&point.object, &p, 0, frame, sizeof(frame), &length) == IEC60870_ASDU_ERROR); + point.object.ioa = 0; + CHECK(IEC60870_Asdu_Create(&asdu, ¶meters104, frame, sizeof(frame), &header, 1) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Create(&second, &p, other, sizeof(other), &header, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&second, &point.object) == IEC60870_ASDU_OK); + ++point.object.ioa; + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_OK); + point.object.ioa = 5; + CHECK(IEC60870_Asdu_Add_InformationObject(&second, &point.object) == IEC60870_ASDU_OK); + CHECK(asdu.length == 11 && second.length == 8 && frame[1] == 0x82 && other[1] == 2); + CHECK(IEC60870_Asdu_Get_Data(&asdu, NULL, &length) == IEC60870_ASDU_ERROR && length == 77); + CHECK(IEC60870_Asdu_Get_Data(&asdu, &data, NULL) == IEC60870_ASDU_ERROR && data == frame); + /* 容器/视图被篡改时,布局检查拒绝,不越界读载荷。 */ + ++asdu.length; + CHECK(IEC60870_Asdu_Get_Data(&asdu, &data, &length) == IEC60870_ASDU_ERROR); + info.parameters = NULL; + CHECK(IEC60870_Asdu_Get_Element(&info, 0, &point, sizeof(point)) == IEC60870_ASDU_ERROR); + return 1; +} + +static int test_responses(void) +{ + static const int causes[] = {7,9,10,44,45,46,47}; + IEC60870_AppLayerParameters_t p = parameters104; + size_t v, ci; + int pn; + for (p.sizeOfCOT = 1; p.sizeOfCOT <= 2; ++p.sizeOfCOT) { + for (p.sizeOfCA = 1; p.sizeOfCA <= 2; ++p.sizeOfCA) { + for (p.sizeOfIOA = 1; p.sizeOfIOA <= 3; ++p.sizeOfIOA) { + for (v = 0; v < COUNT(vectors); ++v) { + uint8_t frame[32], reply[32], expected[32]; + uint16_t n = Wire_Header(&p, vectors[v].type, 1, 0x86, frame), length; + int b; + for (b = 0; b < p.sizeOfIOA; ++b) { frame[n++] = (uint8_t)(0x56 - b); } + memcpy(frame + n, vectors[v].payload, vectors[v].size); n += vectors[v].size; + for (ci = 0; ci < COUNT(causes); ++ci) { + for (pn = 0; pn <= 1; ++pn) { + memcpy(expected, frame, n); expected[2] = (uint8_t)(0x80 | (pn << 6) | causes[ci]); + CHECK(IEC60870_Asdu_Build_Response(&p, frame, n, causes[ci], pn, reply, sizeof(reply), &length) == IEC60870_ASDU_OK); + CHECK(length == n && memcmp(expected, reply, n) == 0); + } + } + } + } + } + } + { + uint8_t frame[40], saved[40]; + static const uint8_t unknown[] = {250,0,0x83,0x67}; + const IEC60870_AppLayerParameters_t short_p = {1,1,1,0,1,1,249}; + IEC60870_AsduInfo_t info; + IEC60870_ReadCommand_t read; + uint16_t length = 77; + memcpy(frame, unknown, sizeof(unknown)); + CHECK(IEC60870_Asdu_Build_Response(&short_p, frame, 4, 44, 1, frame + 1, 39, &length) == IEC60870_ASDU_OK); + CHECK(length == 4 && frame[1] == 250 && frame[3] == 0xEC && frame[4] == 0x67); + CHECK(IEC60870_Asdu_Build_Response(&short_p, frame + 1, 4, 47, 0, frame, 40, &length) == IEC60870_ASDU_OK); + CHECK(frame[0] == 250 && frame[2] == 0xAF && frame[3] == 0x67); + CHECK(IEC60870_Asdu_Build_Response(&short_p, frame, 4, 10, 0, frame, 40, &length) == IEC60870_ASDU_OK && frame[2] == 0x8A); + memcpy(saved, frame, sizeof(frame)); length = 77; + CHECK(IEC60870_Asdu_Build_Response(&short_p, frame, 4, 7, 0, frame, 3, &length) == IEC60870_ASDU_FULL); + CHECK(IEC60870_Asdu_Build_Response(&short_p, frame, 4, 6, 0, frame, 40, &length) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Build_Response(&short_p, frame, 4, 7, 2, frame, 40, &length) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Build_Response(&short_p, frame, 3, 7, 0, frame, 40, &length) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Build_Response(&short_p, NULL, 4, 7, 0, frame, 40, &length) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Build_Response(&short_p, frame, 4, 7, 0, NULL, 40, &length) == IEC60870_ASDU_ERROR); + CHECK(IEC60870_Asdu_Build_Response(&short_p, frame, 4, 7, 0, frame, 40, NULL) == IEC60870_ASDU_ERROR); + CHECK(length == 77 && memcmp(saved, frame, sizeof(frame)) == 0); + memcpy(frame, unknown, sizeof(unknown)); frame[0] = 102; frame[1] = 1; frame[4] = 0x56; + CHECK(IEC60870_Asdu_Parse(&short_p, frame, 5, &info) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Get_Element(&info, 0, &read, sizeof(read)) == IEC60870_ASDU_OK && read.object.ioa == 0x56); + CHECK(IEC60870_Asdu_Build_Response(&short_p, frame, 5, 7, 0, frame, 40, &length) == IEC60870_ASDU_OK && length == 5); + } + return 1; +} + +/* CA宽度、COT决定的偏移、借用/构造对象及失败原子性使用独立字节期望。 */ +static int test_set_ca(void) +{ + static const uint16_t addresses[] = {0, 1, 255, 256, 0x1234, UINT16_MAX}; + IEC60870_AppLayerParameters_t p = parameters104; + unsigned form; + size_t i; + CHECK(IEC60870_Asdu_Set_CA(NULL, 1) == IEC60870_ASDU_ERROR); + for (p.sizeOfCOT = 1; p.sizeOfCOT <= 2; ++p.sizeOfCOT) { + for (p.sizeOfCA = 1; p.sizeOfCA <= 2; ++p.sizeOfCA) { + for (form = 0; form < 3; ++form) { + IEC60870_Asdu_t asdu, saved; + IEC60870_AsduHeader_t header = {6, 1, 1, 0x78, 0x34}; + IEC60870_SinglePointInformation_t point; + uint8_t bytes[32], expected[32]; + unsigned offset = 2U + (unsigned)p.sizeOfCOT; + memset(bytes, 0xa5, sizeof(bytes)); + CHECK(IEC60870_Asdu_Create(&asdu, &p, bytes, sizeof(bytes), &header, 0) == IEC60870_ASDU_OK); + if (form != 0) { + CHECK(IEC60870_SinglePointInformation_Create(&point, 0x1234, 1, 0x80) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_OK); + } + if (form == 2) { + CHECK(IEC60870_Asdu_Create_From_Buffer(&asdu, &p, bytes, asdu.length) == IEC60870_ASDU_OK); + } + saved = asdu; + for (i = 0; i < COUNT(addresses); ++i) { + memcpy(expected, bytes, sizeof(bytes)); + if (p.sizeOfCA == 1 && addresses[i] > 255) { + CHECK(IEC60870_Asdu_Set_CA(&asdu, addresses[i]) == IEC60870_ASDU_ERROR); + } else { + expected[offset] = (uint8_t)addresses[i]; + if (p.sizeOfCA == 2) { expected[offset + 1U] = (uint8_t)(addresses[i] >> 8); } + CHECK(IEC60870_Asdu_Set_CA(&asdu, addresses[i]) == IEC60870_ASDU_OK); + } + CHECK(memcmp(bytes, expected, sizeof(bytes)) == 0 && memcmp(&asdu, &saved, sizeof(asdu)) == 0); + } + memcpy(expected, bytes, sizeof(bytes)); + asdu.parameters = NULL; + CHECK(IEC60870_Asdu_Set_CA(&asdu, 1) == IEC60870_ASDU_ERROR); asdu = saved; + asdu.length = (uint16_t)(offset + (unsigned)p.sizeOfCA - 1U); + CHECK(IEC60870_Asdu_Set_CA(&asdu, 1) == IEC60870_ASDU_ERROR); asdu = saved; + asdu.buffer = NULL; + CHECK(IEC60870_Asdu_Set_CA(&asdu, 1) == IEC60870_ASDU_ERROR); asdu = saved; + asdu.type_id ^= 1U; + CHECK(IEC60870_Asdu_Set_CA(&asdu, 1) == IEC60870_ASDU_ERROR); asdu = saved; + { + int width = p.sizeOfCA; + p.sizeOfCA = 3; + CHECK(IEC60870_Asdu_Set_CA(&asdu, 1) == IEC60870_ASDU_ERROR); + p.sizeOfCA = width; + } + CHECK(memcmp(bytes, expected, sizeof(bytes)) == 0); + } + } + } + return 1; +} + +int main(void) +{ + int passed = 0; +#define RUN(test) do { if (!(test)()) { return 1; } ++passed; } while (0) + RUN(test_wire_matrix); + RUN(test_object_entrypoints); + RUN(test_capacity_and_addresses); + RUN(test_malformed_payloads); + RUN(test_constructor_failures); + RUN(test_add_atomicity); + RUN(test_numeric_bits); + RUN(test_parameters_and_nulls); + RUN(test_responses); + RUN(test_set_ca); +#undef RUN + printf("ASDU: %d groups passed (30 concrete types, 504 wire layouts, 12 parameter configurations)\n", passed); + return 0; +} diff --git a/IEC60870-Slave/.build/command-v1/before/test/slave_example_loopback_test.cjs b/IEC60870-Slave/.build/command-v1/before/test/slave_example_loopback_test.cjs new file mode 100644 index 00000000..8706fd56 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/before/test/slave_example_loopback_test.cjs @@ -0,0 +1,349 @@ +/* 独立主站字节测试:启动示例自己的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 '); + +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 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]; +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); + const count = qoi === 20 ? 6 : 4; + 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 digital = qoi === 20 ? f < 3 : qoi === 21; + const width = digital ? 4 : 6; + const first = (qoi === 20 && !digital ? f - 3 : f - 1) * 4; + assert.equal(asdu.length, 6 + width * 4); + assert.deepEqual([...asdu.subarray(0, 6)], [digital ? 1 : 11, 4, 0x80 | qoi, oa, 7, 0]); + for (let p = 0; p < 4; p++) { + const offset = 6 + p * width; + assert.equal(asdu.readUIntLE(offset, 3), (digital ? 1001 : 2001) + first + p); + if (digital) assert.equal(asdu[offset + 3], signals[first + p]); + else { + assert.equal(asdu.readInt16LE(offset + 3), measures[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')); + 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', '8000'], + { 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(); }, 15000); + 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); + 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 < 10; index++) { + const asdu = await master.readASDU(); + (asdu[0] === 101 || asdu[0] === 15 ? counterFrames : generalFrames).push(asdu); + } + assert.equal(generalFrames.length, 6); + 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=65 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/); + assert.equal(errors, ''); + console.log('Slave example loopback: passed (real TCP, batched 106+102 and 103+102, simulated clock/TEST/COT/broadcast, GI and counter groups/broadcast, freeze/reset/TEST, independent k=1 progress, busy/deactivation/errors, 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; }); diff --git a/IEC60870-Slave/.build/command-v1/command-stack.su b/IEC60870-Slave/.build/command-v1/command-stack.su new file mode 100644 index 00000000..6248904b --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/command-stack.su @@ -0,0 +1,26 @@ +104/business/IEC104_Command.c:108:13:Reap 48 static +104/business/IEC104_Command.c:260:13:Defer_For_Stop 144 static +104/business/IEC104_Command.c:177:17:Timer_Limit 8 static +104/business/IEC104_Command.c:277:17:Reply_Head.constprop 8 static +104/business/IEC104_Command.c:118:13:Finish.part.0 48 static +104/business/IEC104_Command.c:298:13:Complete_If_Sent 48 static +104/business/IEC104_Command.c:233:13:Expire 48 static +104/business/IEC104_Command.c:160:35:Check_Session 80 static +104/business/IEC104_Command.c:698:36:Check_Reply 64 static +104/business/IEC104_Command.c:310:12:Submit_Head 144 static +104/business/IEC104_Command.c:427:13:End_With_Reply 48 static +104/business/IEC104_Command.c:376:13:Short_Reply 80 static +104/business/IEC104_Command.c:533:6:IEC104_Command_Config_Default 8 static +104/business/IEC104_Command.c:545:19:IEC104_Command_Create 64 static +104/business/IEC104_Command.c:566:6:IEC104_Command_Destroy 64 static +104/business/IEC104_Command.c:573:6:IEC104_Command_Cancel 8 static +104/business/IEC104_Command.c:579:29:IEC104_Command_Handle_Event 208 static +104/business/IEC104_Command.c:618:6:IEC104_Command_Process 64 static +104/business/IEC104_Command.c:635:28:IEC104_Command_NextEvent 160 static +104/business/IEC104_Command.c:712:29:IEC104_Command_Accept 128 static +104/business/IEC104_Command.c:760:29:IEC104_Command_Reject 80 static +104/business/IEC104_Command.c:780:29:IEC104_Command_Report_Result 64 static +104/business/IEC104_Command.c:794:29:IEC104_Command_Submit_Return_Info 144 static +104/business/IEC104_Command.c:837:29:IEC104_Command_Complete 64 static +104/business/IEC104_Command.c:858:6:IEC104_Command_Get_Transaction 32 static +104/business/IEC104_Command.c:887:6:IEC104_Command_Get_Status 256 static diff --git a/IEC60870-Slave/.build/command-v1/example-after.su b/IEC60870-Slave/.build/command-v1/example-after.su new file mode 100644 index 00000000..7ef8f733 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/example-after.su @@ -0,0 +1,10 @@ +examples/iec104_slave_example.c:230:13:Example_Counter_Action 8 static +examples/iec104_slave_example.c:760:13:Example_Stop 8 static +examples/iec104_slave_example.c:770:13:Example_Number 80 static +examples/iec104_slave_example.c:405:13:Example_Clock_Action 48 static +examples/iec104_slave_example.c:454:37:Example_Command_Action 8 static +examples/iec104_slave_example.c:290:13:Example_Drain_Counter.part.0.constprop 144 static +examples/iec104_slave_example.c:693:13:Example_Step_Counter.constprop 48 static +examples/iec104_slave_example.c:498:13:Example_Drain_Command.part.0.constprop 240 static +examples/iec104_slave_example.c:160:13:Example_Drain_Business.constprop 160 static +examples/iec104_slave_example.c:815:5:main 848 static diff --git a/IEC60870-Slave/.build/command-v1/example-before.su b/IEC60870-Slave/.build/command-v1/example-before.su new file mode 100644 index 00000000..596e4103 --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/example-before.su @@ -0,0 +1,8 @@ +.build/command-v1/before/examples/iec104_slave_example.c:219:13:Example_Counter_Action 8 static +.build/command-v1/before/examples/iec104_slave_example.c:581:13:Example_Stop 8 static +.build/command-v1/before/examples/iec104_slave_example.c:591:13:Example_Number 80 static +.build/command-v1/before/examples/iec104_slave_example.c:389:13:Example_Clock_Action 48 static +.build/command-v1/before/examples/iec104_slave_example.c:279:13:Example_Drain_Counter.part.0.constprop 144 static +.build/command-v1/before/examples/iec104_slave_example.c:516:13:Example_Step_Counter.constprop 48 static +.build/command-v1/before/examples/iec104_slave_example.c:149:13:Example_Drain_Business.constprop 160 static +.build/command-v1/before/examples/iec104_slave_example.c:634:5:main 800 static diff --git a/IEC60870-Slave/.build/command-v1/header_check.cpp b/IEC60870-Slave/.build/command-v1/header_check.cpp new file mode 100644 index 00000000..1e1f48cc --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/header_check.cpp @@ -0,0 +1,7 @@ +#include "IEC104_Command.h" +static_assert(IEC104_COMMAND_TYPE_BIT(45) == 1, "type45"); +static_assert(IEC104_COMMAND_TYPE_BIT(46) == 2, "type46"); +static_assert(IEC104_COMMAND_TYPE_BIT(44) == 0, "invalid-low"); +static_assert(IEC104_COMMAND_TYPE_BIT(52) == 0, "invalid-gap"); +static_assert(IEC104_COMMAND_TYPE_BIT(64) == 8192, "future-layout"); +int main() { return 0; } diff --git a/IEC60870-Slave/.build/command-v1/size_probe.c b/IEC60870-Slave/.build/command-v1/size_probe.c new file mode 100644 index 00000000..0d61ba9e --- /dev/null +++ b/IEC60870-Slave/.build/command-v1/size_probe.c @@ -0,0 +1,11 @@ +#include +#include "../../104/business/IEC104_Command.c" + +int main(void) +{ + printf("pointer=%zu instance=%zu transaction=%zu reply=%zu pool=%zu request=%zu event=%zu status=%zu reply_capacity=%u\n", + sizeof(void *), sizeof(IEC104_Command_t), sizeof(CommandTransaction), sizeof(CommandReply), + sizeof(command_pool), sizeof(IEC104_CommandRequest_t), sizeof(IEC104_CommandEvent_t), + sizeof(IEC104_CommandStatus_t), (unsigned)IEC104_COMMAND_REPLY_CAPACITY); + return 0; +} diff --git a/IEC60870-Slave/.build/iec104_slave_example_test.before-lf.c b/IEC60870-Slave/.build/iec104_slave_example_test.before-lf.c new file mode 100644 index 00000000..9eb05ae0 --- /dev/null +++ b/IEC60870-Slave/.build/iec104_slave_example_test.before-lf.c @@ -0,0 +1,4976 @@ +/* 纯事件总召模块及示例接入回归:真实Slave/TCP提交路径 + 可控Net,无系统socket。 + * 仅链接scripts/build.ps1中的公共依赖,不再链接TcpServer.c或Net后端。 + * HIGH缩到251字节,通过应用应答和真实发送背压覆盖各业务阶段及自动推进的停止条件。 */ +#define main existing_event_ring_main +#define IEC104_Net_Now_Ms example_test_base_now +#include "iec104_tcp_event_ring_test.c" +#undef IEC104_Net_Now_Ms +#undef main +/* Advance only when armed: exercise two module time reads crossing a deadline. */ +static uint64_t cs_test_now_step; +uint64_t IEC104_Net_Now_Ms(void) +{ + uint64_t now = example_test_base_now(); + fake.now += cs_test_now_step; + return now; +} +#define IEC104_SLAVE_EXAMPLE_NO_MAIN +#include "../examples/iec104_slave_example.c" +#include "IEC104_ClockSynchronization.h" + +static ExampleApp app; +static IEC104_InterrogationStationState_t broadcast_states[16]; +static unsigned master_ns; +static IEC104_SlaveEvent_t last_request; +static const IEC104_InterrogationConfig_t *custom_config; +static const IEC104_InterrogationConfig_t *counter_custom_config; +static bool counter_enabled; +static bool clock_enabled; +static const IEC104_ClockSynchronizationConfig_t *clock_custom_config; +static IEC104_InterrogationStationState_t counter_broadcast_states[16]; +static uint16_t custom_window = 32; +static const uint8_t gi_request[] = {100, 1, 0x86, 0x5a, 1, 0, 0, 0, 0, 20}; +static const uint8_t fallback_request[] = {106, 1, 0x86, 0x61, 1, 0, 0, 0, 0, 0x39, 0x30}; +static const uint8_t read_request[] = {102, 1, 5, 0x62, 1, 0, 0xe9, 3, 0}; + +static void request_unchanged(const uint8_t *request, size_t size); + +static void app_poll(void) +{ + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + { + IEC104_SlaveEvent_t event; + IEC104_TcpEventResult_t result; + while ((result = IEC104_Slave_NextEvent(app.slave, &event)) == IEC104_TCP_EVENT_READY) { + if (event.transport.type == IEC104_TCP_EVENT_I_FRAME) { + CHECK(IEC104_Slave_Copy_Event(&last_request, &event)); + } + CHECK(Example_Handle_Event(&app, &event)); + } + CHECK(result == IEC104_TCP_EVENT_EMPTY); + } +} + +static void app_connect(void) +{ + fake.accept_pending = 1; + app_poll(); + CHECK(!app.gi.active); + push(start_act, sizeof(start_act)); + app_poll(); + CHECK(app.gi.active); + master_ns = 0; +} + +static void app_open(void) +{ + IEC104_SlaveConfig_t config; + IEC104_Slave_t *slave; + memset(&fake, 0, sizeof(fake)); + fake.now = 1000; + slave = IEC104_Slave_Create(); + CHECK(slave != NULL); + IEC104_Slave_Config_Default(&config); + config.protocol.link.k = custom_window; + CHECK(IEC104_Slave_Init(slave, &config, NULL) == IEC104_TCP_OK); + /* 可控后端仅用此指针调整窗口或注入底层故障,不替换发送路径。 */ + { + unsigned slot; + sut = NULL; + for (slot = 0; slot < IEC104_TCP_SERVER_CAPACITY; ++slot) { + if (server_pool[slot].in_use != 0U) { sut = &server_pool[slot].server; break; } + } + CHECK(sut != NULL); + } + CHECK(Example_Init(&app, slave, custom_config == NULL ? 1U : custom_config->common_address, + broadcast_states, sizeof(broadcast_states) / sizeof(broadcast_states[0]))); + if (custom_config != NULL) { + CHECK(IEC104_Interrogation_Destroy(app.interrogation)); + app.interrogation = IEC104_Interrogation_Create(slave, custom_config); + CHECK(app.interrogation != NULL); + } + if (counter_enabled) { + CHECK(Example_Init_Counters(&app, counter_broadcast_states, 16U)); + if (counter_custom_config != NULL) { + CHECK(IEC104_Interrogation_Destroy(app.counter)); + app.counter = IEC104_CounterInterrogation_Create(slave, counter_custom_config); + CHECK(app.counter != NULL); + } + } + if (clock_enabled) { CHECK(Example_Init_Clock(&app, clock_custom_config)); } + app_connect(); +} + +static void app_close(void) +{ + IEC104_Slave_t *slave = app.slave; + Example_Cancel(&app); + CHECK(Example_Drain_Business(&app)); + CHECK(Example_Drain_Counter(&app)); + CHECK(IEC104_Slave_Close(slave, NULL) == IEC104_TCP_OK); + CHECK(Example_Drain(&app)); + CHECK(IEC104_Interrogation_Destroy(app.interrogation)); + if (app.counter != NULL) { CHECK(IEC104_Interrogation_Destroy(app.counter)); } + if (app.clock != NULL) { CHECK(IEC104_ClockSynchronization_Destroy(app.clock)); } + if (IEC104_Station_Is_Registered(&app.station)) { + CHECK(IEC104_Slave_Remove_Station(slave, &app.station) == IEC104_STATION_OK); + } + CHECK(IEC104_Station_Is_Reusable(&app.station)); + CHECK(IEC104_Slave_Destroy(slave, NULL) == IEC104_TCP_OK); + memset(&app, 0, sizeof(app)); +} + +/* 独立拼接主站I帧,避免用待测ASDU构造器生成输入或期望结果。 */ +static void queue_request(const uint8_t *payload, size_t length) +{ + uint8_t frame[255] = {0x68}; + CHECK(length <= 249); + frame[1] = (uint8_t)(length + 4U); + frame[2] = (uint8_t)(master_ns << 1); + frame[3] = (uint8_t)(master_ns >> 7); + ++master_ns; + memcpy(frame + 6, payload, length); + push(frame, length + 6); +} + +static void app_request(const uint8_t *payload, size_t length) +{ + queue_request(payload, length); + app_poll(); +} + +static void app_finish(void) +{ + unsigned i; + for (i = 0; i < 128; ++i) { + app_poll(); + CHECK(Example_Step(&app)); + } + CHECK(app.gi.pending_requests == 0 && !app.gi.gi_pending); +} + +/* 跳过U/S帧和用于制造HIGH拥塞的Type200填充帧,保留真实业务输出顺序。 */ +static unsigned collect_asdus(const uint8_t **out, size_t *lengths) +{ + size_t offset = 0; + unsigned count = 0; + while (offset < fake.sent_bytes) { + const uint8_t *frame = fake.output + offset; + size_t length; + CHECK(fake.sent_bytes - offset >= 6 && frame[0] == 0x68); + length = (size_t)frame[1] + 2U; + CHECK(length >= 6 && offset + length <= fake.sent_bytes); + if ((frame[2] & 1U) == 0U && frame[6] != 200) { + CHECK(count < 32); + out[count] = frame + 6; + lengths[count++] = length - 6; + } + offset += length; + } + return count; +} + +static void check_gi(unsigned extra_reply, uint8_t oa) +{ + static const uint8_t values[] = {0, 1, 0, 1, 1, 0, 1, 0}; + static const int16_t measures[] = {2300, 2310, 2290, 5000, -125, 0, 1234, 42}; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned f, p, indexes[8], gi_count = 0U; + unsigned total = collect_asdus(frames, lengths); + CHECK(total == 8 + extra_reply); + for (f = 0U; f < total; ++f) { + if (frames[f][3] == oa && (frames[f][2] & 0x40U) == 0U) { + CHECK(gi_count < 8U); + indexes[gi_count++] = f; + } + } + CHECK(gi_count == 8U); /* 其他请求的否定应答可穿插,但本次总召内容及自身顺序不变。 */ + for (f = 0; f < 8; ++f) { + const uint8_t *a = frames[indexes[f]]; + CHECK(a[3] == oa && a[4] == 1 && a[5] == 0); + if (f == 0 || f == 7) { + CHECK(lengths[indexes[f]] == 10 && a[0] == 100 && a[1] == 1); + CHECK(a[2] == (f == 0 ? 0x87 : 0x8a)); + CHECK(a[6] == 0 && a[7] == 0 && a[8] == 0 && a[9] == 20); + } else { + unsigned is_signal = f < 5; + unsigned is_single = f < 3; + size_t width = is_signal ? 4U : 6U; + unsigned first = (is_single ? f - 1U : is_signal ? f - 3U : f - 5U) * 4U; + CHECK(a[0] == (is_single ? 1 : is_signal ? 3 : 11) && a[1] == 4 && a[2] == 0x94); + CHECK(lengths[indexes[f]] == 6 + width * 4); + for (p = 0; p < 4; ++p) { + const uint8_t *object = a + 6 + width * p; + unsigned ioa = object[0] | (unsigned)object[1] << 8 | (unsigned)object[2] << 16; + CHECK(ioa == (is_single ? 1001U : is_signal ? 1101U : 2001U) + first + p); + if (is_single) { CHECK(object[3] == values[first + p]); } + else if (is_signal) { CHECK(object[3] == 1U); } + else { + CHECK((int16_t)(object[3] | (uint16_t)object[4] << 8) == measures[first + p]); + CHECK(object[5] == 0); + } + } + } + } + CHECK(app.gi.gi_submitted == 1); +} + +static void test_total_call(void) +{ + app_open(); + fake.send_limit = 3; /* 每帧分多次write,仍只有一次提交且原字节顺序不变。 */ + app_request(gi_request, sizeof(gi_request)); + CHECK(app.gi.pending_requests == 1); + app_finish(); + check_gi(0, 0x5a); + app_close(); +} + +/* 把实际HIGH填满,并保留一个尚未写出的当前帧;失败必须是WAIT而不是测试桩伪造。 */ +static void congest(IEC60870_MasterConnection_t *connection) +{ + uint8_t bytes[249] = {200, 0, 3, 0, 1, 0}; + IEC60870_Asdu_t filler; + unsigned i; + CHECK(IEC60870_Asdu_Create_From_Buffer(&filler, + IEC104_Slave_Get_Application_Layer_Parameters(app.slave), bytes, sizeof(bytes)) == IEC60870_ASDU_OK); + fake.send_blocked = 1; + for (i = 0; i < 32; ++i) { + if (!IEC60870_MasterConnection_Send_ASDU(connection, &filler)) { + CHECK(IEC104_Slave_Get_Last_Submit_Result(connection) == IEC104_TCP_SUBMIT_WAIT); + return; + } + } + CHECK(0); +} + +/* 保留一个阻塞的当前帧,HIGH中只留下一个10字节应答及其2字节记录头的空间。 + * 通过真实容量限制在第一条REPLY后产生WAIT,不依赖业务层的处理步数参数。 */ +static void congest_except_one_reply(IEC60870_MasterConnection_t *connection) +{ + uint8_t bytes[249] = {200, 0, 3, 0, 1, 0}; + IEC60870_Asdu_t filler; + fake.send_blocked = 1; + CHECK(IEC60870_Asdu_Create_From_Buffer(&filler, + IEC104_Slave_Get_Application_Layer_Parameters(app.slave), bytes, sizeof(bytes)) == IEC60870_ASDU_OK); + CHECK(IEC60870_MasterConnection_Send_ASDU(connection, &filler)); + CHECK(IEC60870_Asdu_Create_From_Buffer(&filler, + IEC104_Slave_Get_Application_Layer_Parameters(app.slave), bytes, sizeof(bytes) - 12U) == IEC60870_ASDU_OK); + CHECK(IEC60870_MasterConnection_Send_ASDU(connection, &filler)); +} + +static void test_wait_each_phase(void) +{ + static const struct { IEC104_InterrogationPhase_t phase; size_t batch; } phases[] = { + {IEC104_INTERROGATION_CONFIRM, 0}, {IEC104_INTERROGATION_DATA, 0}, + {IEC104_INTERROGATION_DATA, 2}, {IEC104_INTERROGATION_DATA, 4}, {IEC104_INTERROGATION_TERMINATE, 6} + }; + unsigned i; + for (i = 0; i < sizeof(phases) / sizeof(phases[0]); ++i) { + size_t point_index; + unsigned steps; + app_open(); + app_request(gi_request, sizeof(gi_request)); + for (steps = 0; steps < 32; ++steps) { + if (app.gi.phase == phases[i].phase && app.gi.batch_index == phases[i].batch) { break; } + CHECK(IEC104_Interrogation_Process(app.interrogation)); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + if (app.gi.phase == phases[i].phase && app.gi.batch_index == phases[i].batch) { break; } + CHECK(Example_Drain_Business(&app)); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + } + CHECK(steps < 32); + congest(&last_request.connection); + point_index = app.gi.batch_index; + /* DATA阶段由应用应答当场尝试发送;WAIT不得推进应用或协议游标。 */ + CHECK(Example_Step(&app)); + if (!app.gi.waiting) { CHECK(Example_Step(&app)); } + CHECK(app.gi.waiting && app.gi.phase == phases[i].phase); + CHECK(Example_Step(&app) && app.gi.waiting && app.gi.batch_index == point_index); + fake.send_blocked = 0; + fake.send_limit = 3; + app_finish(); + CHECK(app.gi.waits >= 2); + check_gi(0, 0x5a); + app_close(); + } +} + +static void test_validation(void) +{ + static const struct { unsigned offset; uint8_t value, cot; } cases[] = { + {0, 200, 44}, {2, 0x83, 45}, {4, 2, 46}, {6, 1, 47}, {9, 19, 7}, {9, 37, 7}, {2, 0xc6, 7} + }; + unsigned i; + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + const uint8_t *out[32]; + size_t lengths[32], j; + uint8_t request[sizeof(gi_request)]; + app_open(); + memcpy(request, gi_request, sizeof(request)); + request[cases[i].offset] = cases[i].value; + app_request(request, sizeof(request)); + CHECK(!app.gi.gi_pending && app.gi.pending_requests == 0); + CHECK((i == 0 && app.other_submitted == 1) || (i != 0 && app.gi.negative_submitted == 1)); + app_finish(); + /* Type200既用作填充也用作未知类型请求;本案例直接检查唯一输出帧。 */ + if (i == 0) { out[0] = fake.output + 12; lengths[0] = fake.output[7] - 4U; } + else { CHECK(collect_asdus(out, lengths) == 1); } + CHECK(lengths[0] == sizeof(request)); + for (j = 0; j < sizeof(request); ++j) { + CHECK(out[0][j] == (j == 2 ? (uint8_t)(0xc0 | cases[i].cot) : request[j])); + } + CHECK((app.gi.rejected + app.other_rejected) == 1 && app.gi.gi_submitted == 0); + app_close(); + } +} + +static void test_busy(void) +{ + const uint8_t *out[32]; + size_t lengths[32]; + uint8_t second[sizeof(gi_request)]; + app_open(); + app_request(gi_request, sizeof(gi_request)); + memcpy(second, gi_request, sizeof(second)); + second[3] = 0x6b; + app_request(second, sizeof(second)); + CHECK(app.gi.pending_requests == 1 && app.gi.negative_submitted == 1); + app_finish(); + check_gi(1, 0x5a); + CHECK(collect_asdus(out, lengths) == 9); + CHECK(lengths[0] == 10 && out[0][2] == 0xc7 && out[0][3] == 0x6b); + app_close(); +} + +static void test_stop(void) +{ + uint8_t second[sizeof(gi_request)]; + app_open(); + app_request(gi_request, sizeof(gi_request)); + push(stop_act, sizeof(stop_act)); + app_poll(); + CHECK(app.gi.pending_requests == 0 && !app.gi.active && app.gi.cancelled == 1); + push(start_act, sizeof(start_act)); + app_poll(); + memcpy(second, gi_request, sizeof(second)); + second[3] = 0x6b; + app_request(second, sizeof(second)); + app_finish(); + check_gi(0, 0x6b); + app_close(); +} + +static void test_disconnect(void) +{ + IEC104_SlaveEvent_t old; + uint64_t generation; + uint8_t second[sizeof(gi_request)]; + app_open(); + app_request(gi_request, sizeof(gi_request)); + generation = app.gi.generation; + CHECK(IEC104_Slave_Copy_Event(&old, &last_request)); + CHECK(IEC104_Slave_Close_Connection(app.slave, &old, NULL) == IEC104_TCP_OK); + CHECK(Example_Drain(&app) && app.gi.pending_requests == 0 && !app.gi.active); + app_connect(); + CHECK(app.gi.generation != generation); + CHECK(!IEC60870_MasterConnection_Send_ACT_CON(&old.connection, &old.asdu, false)); + memcpy(second, gi_request, sizeof(second)); + second[3] = 0x6b; + app_request(second, sizeof(second)); + app_finish(); + check_gi(0, 0x6b); + app_close(); +} + +static void test_malformed(void) +{ + static const struct { size_t length; uint8_t vsq, cot, ca, ioa, qoi, reply; } cases[] = { + {10, 0x81, 0x86, 1, 0, 20, 7}, {11, 1, 0x86, 1, 0, 20, 7}, + {249, 0x81, 0x86, 1, 0, 36, 7}, {11, 1, 0x88, 1, 0, 20, 9}, + {10, 0x81, 0x88, 1, 0, 20, 9}, {11, 1, 6, 1, 0, 20, 7}, + {11, 1, 0x83, 2, 1, 19, 45}, {11, 1, 0x86, 2, 1, 19, 46}, + {11, 1, 0x88, 1, 1, 19, 47}, {11, 1, 0xc6, 1, 0, 19, 7} + }; + unsigned i; + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + IEC104_TcpServerStatus_t status; + const uint8_t *frames[32]; + size_t lengths[32]; + uint8_t request[249], expected[10]; + app_open(); + memset(request, 0xa5, sizeof(request)); memcpy(request, gi_request, sizeof(gi_request)); + request[1] = cases[i].vsq; request[2] = cases[i].cot; request[3] = 0x67; + request[4] = cases[i].ca; request[6] = cases[i].ioa; request[9] = cases[i].qoi; + request_unchanged(request, cases[i].length); app_finish(); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.connected); + CHECK(!app.gi.gi_pending && app.gi.gi_submitted == 0 && app.gi.rejected == 1 && app.gi.cancelled == 0); + CHECK(app.gi.negative_submitted == 1 && app.gi.dropped_requests == 0 && app.other_rejected == 0); + memcpy(expected, request, sizeof(expected)); expected[1] = 1; + expected[2] = (uint8_t)((request[2] & 0x80U) | 0x40U | cases[i].reply); + CHECK(collect_asdus(frames, lengths) == 1 && lengths[0] == sizeof(expected)); + CHECK(memcmp(frames[0], expected, sizeof(expected)) == 0); + app_request(gi_request, sizeof(gi_request)); app_finish(); check_gi(1, 0x5a); + app_close(); + } +} + +/* 头部/最低载荷合法但对象不唯一:总召接手后丢弃,不能猜测首对象或进入UNKNOWN_TYPE兜底。 */ +static void test_malformed_drop(void) +{ + static const struct { size_t length; uint8_t vsq; } cases[] = { + {6, 0}, {10, 0}, {6, 0x80}, {14, 2}, {11, 0x82} + }; + unsigned i; + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + uint8_t request[14] = {0}; + IEC104_TcpServerStatus_t status; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); memcpy(request, gi_request, sizeof(gi_request)); request[1] = cases[i].vsq; + request_unchanged(request, cases[i].length); app_finish(); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.connected); + CHECK(collect_asdus(frames, lengths) == 0 && app.gi.rejected == 1 && app.gi.cancelled == 0); + CHECK(app.gi.negative_submitted == 0 && app.gi.dropped_requests == 0 && app.other_rejected == 0); + app_request(gi_request, sizeof(gi_request)); app_finish(); check_gi(0, 0x5a); + app_close(); + } +} + +/* 真正截短的公共头/载荷仍由Receiver和连接层拒绝;不能放宽为业务层的畸形单对象回复。 */ +static void test_receiver_truncated(void) +{ + static const struct { size_t length; uint8_t type, vsq; } cases[] = { + {5, 100, 1}, {9, 100, 1}, {10, 100, 2}, {10, 100, 0x82}, {5, 101, 1} + }; + unsigned i; + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + uint8_t request[sizeof(gi_request)]; + IEC104_TcpServerStatus_t status; + app_open(); memcpy(request, gi_request, sizeof(request)); + request[0] = cases[i].type; request[1] = cases[i].vsq; + app_request(request, cases[i].length); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && !status.connected); + CHECK(app.gi.rejected == 0 && app.gi.submitted == 0 && app.gi.pending_requests == 0); + app_close(); + } +} + +static void test_overload(void) +{ + unsigned i; + IEC104_TcpServerStatus_t status; + uint8_t request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + memcpy(request, gi_request, sizeof(request)); + request[9] = 19; /* 仅有短应答也能占满池;新总召不得挤掉旧应答。 */ + for (i = 0; i < 4; ++i) { request[3] = (uint8_t)(0x60U + i); queue_request(request, sizeof(request)); } + app_poll(); + app_request(gi_request, sizeof(gi_request)); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.connected); + CHECK(app.gi.overloads == 1 && app.gi.pending_requests == 4 && !app.gi.gi_pending); + CHECK(app.gi.negative_submitted == 0 && app.gi.dropped_requests == 1 && app.gi.cancelled == 0); + app_finish(); + CHECK(collect_asdus(frames, lengths) == 4 && app.gi.negative_submitted == 4); + for (i = 0; i < 4; ++i) { CHECK(frames[i][2] == 0xc7 && frames[i][3] == 0x60U + i); } + app_close(); +} + +/* STOP/START排在一轮通知预算之后时,旧总召也不能在同一TCP重新激活后继续发送。 */ +static void test_event_budget_stop_start(void) +{ + IEC60870_Asdu_t filler; + IEC104_TcpServerStatus_t status; + uint8_t bytes[] = {200, 0, 3, 0, 1, 0}; + unsigned i; + app_open(); + app_request(gi_request, sizeof(gi_request)); + CHECK(IEC60870_Asdu_Create_From_Buffer(&filler, + IEC104_Slave_Get_Application_Layer_Parameters(app.slave), bytes, sizeof(bytes)) == IEC60870_ASDU_OK); + for (i = 0; i < 32; ++i) { + CHECK(IEC60870_MasterConnection_Send_ASDU(&last_request.connection, &filler)); + } + push_ack(32); + for (i = 0; i < 8; ++i) { CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); } + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK); + CHECK(status.pending_event_count == 32); + push(stop_act, sizeof(stop_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + push(start_act, sizeof(start_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + /* 直接验证库入口也会让出,不仅依靠示例自己的事件预算标志。 */ + CHECK(IEC104_Interrogation_Process(app.interrogation)); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.events_pending && app.gi.submitted == 0U && app.gi.pending_requests == 1U); + CHECK(Example_Drain(&app) && app.events_pending && app.gi.pending_requests == 1); + CHECK(Example_Step(&app) && (app.gi.submitted + app.other_submitted) == 0); + CHECK(Example_Drain(&app) && !app.events_pending && app.gi.active && app.gi.pending_requests == 0); + CHECK(Example_Step(&app) && (app.gi.submitted + app.other_submitted) == 0); + app_close(); +} + +static void test_non_gi_ignored(void) +{ + unsigned busy; + for (busy = 0; busy < 4; ++busy) { + IEC104_SlaveEvent_t event; + uint8_t input[] = {101, 1, 6, 0x5a, 1, 0, 0, 0, 0, 5}; + app_open(); + if (busy & 1U) { app_request(gi_request, sizeof(gi_request)); } + if (busy & 2U) { input[4] = input[5] = 0xff; } + queue_request(input, sizeof(input)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_NextEvent(app.slave, &event) == IEC104_TCP_EVENT_READY); + CHECK(IEC104_Interrogation_Handle_Event(app.interrogation, &event) == IEC104_INTERROGATION_EVENT_IGNORED); + CHECK(memcmp(event.transport.data.i_frame.apdu + 6, input, sizeof(input)) == 0); + CHECK(IEC104_Interrogation_Process(app.interrogation)); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi) && app.gi.pending_requests == (busy & 1U)); + CHECK(app.gi.rejected == 0 && app.gi.submitted == 0); /* 总召忙不能替总电度作答。 */ + app_close(); + } +} + +static void test_module_contracts(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationStatus_t status, before; + IEC104_Slave_t *slave = IEC104_Slave_Create(); + IEC104_Interrogation_t *gi; + CHECK(slave != NULL); + IEC104_Interrogation_Config_Default(NULL); + IEC104_Interrogation_Config_Default(&config); + CHECK(IEC104_Interrogation_Create(NULL, &config) == NULL); + CHECK(IEC104_Interrogation_Create(slave, NULL) == NULL); + config.common_address = 0U; + CHECK(IEC104_Interrogation_Create(slave, &config) == NULL); + config.common_address = IEC104_CA_BROADCAST; + CHECK(IEC104_Interrogation_Create(slave, &config) == NULL); + config.common_address = 1U; + config.max_objects_per_asdu = 0U; + CHECK(IEC104_Interrogation_Create(slave, &config) == NULL); + config.max_objects_per_asdu = 128U; + CHECK(IEC104_Interrogation_Create(slave, &config) == NULL); + config.max_objects_per_asdu = 60U; + gi = IEC104_Interrogation_Create(slave, &config); + CHECK(gi != NULL && IEC104_Interrogation_Create(slave, &config) == NULL); + CHECK(!IEC104_Interrogation_Process(NULL)); + CHECK(IEC104_Interrogation_Handle_Event(gi, NULL) == IEC104_INTERROGATION_EVENT_ERROR); + memset(&status, 0xa5, sizeof(status)); + memcpy(&before, &status, sizeof(status)); + CHECK(!IEC104_Interrogation_Get_Status(NULL, &status) && memcmp(&status, &before, sizeof(status)) == 0); + CHECK(IEC104_Interrogation_Get_Status(gi, &status) && status.phase == IEC104_INTERROGATION_IDLE); + IEC104_Interrogation_Cancel(gi); + CHECK(IEC104_Interrogation_Destroy(gi) && !IEC104_Interrogation_Destroy(gi)); + CHECK(!IEC104_Interrogation_Process(gi)); + CHECK(IEC104_Interrogation_Create(slave, &config) == gi); /* 销毁后可重新创建。 */ + CHECK(IEC104_Interrogation_Destroy(gi)); + CHECK(IEC104_Slave_Destroy(slave, NULL) == IEC104_TCP_OK); +} + +/* 以下直接驱动公开业务事件API,不经过示例的自动事件应答。 */ +static void gi_process(void) +{ + app_poll(); + CHECK(IEC104_Interrogation_Process(app.interrogation)); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); +} + +static IEC104_InterrogationEvent_t gi_take(IEC104_InterrogationEventType_t type) +{ + IEC104_InterrogationEvent_t event; + CHECK(IEC104_Interrogation_NextEvent(app.interrogation, &event) == IEC104_INTERROGATION_NEXT_READY); + CHECK(event.type == type && event.instance_id != 0U && event.request_id != 0U); + return event; +} + +static void gi_empty(void) +{ + IEC104_InterrogationEvent_t event, before; + memset(&event, 0xa5, sizeof(event)); + memcpy(&before, &event, sizeof(before)); + CHECK(IEC104_Interrogation_NextEvent(app.interrogation, &event) == IEC104_INTERROGATION_NEXT_EMPTY); + CHECK(memcmp(&event, &before, sizeof(event)) == 0); + CHECK(IEC104_Interrogation_NextEvent(NULL, &event) == IEC104_INTERROGATION_NEXT_ERROR); + CHECK(memcmp(&event, &before, sizeof(event)) == 0); +} + +static IEC104_InterrogationEvent_t gi_begin(void) +{ + app_request(gi_request, sizeof(gi_request)); + gi_process(); + return gi_take(IEC104_INTERROGATION_BEGIN); +} + +static IEC104_InterrogationEvent_t gi_data(void) +{ + IEC104_InterrogationEvent_t event = gi_begin(); + CHECK(event.request.common_address == 1 && event.request.originator_address == 0x5a && + event.request.qualifier == 20 && event.request.is_test && event.request.receive_serial != 0U); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_STALE); + gi_process(); + return gi_take(IEC104_INTERROGATION_NEED_DATA); +} + +static void gi_done(const IEC104_InterrogationEvent_t *event, size_t batches, size_t objects) +{ + IEC104_InterrogationEvent_t finish; + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, event) == IEC104_INTERROGATION_REPLY_OK); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, event) == IEC104_INTERROGATION_REPLY_STALE); + gi_process(); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_COMPLETED && + finish.request_id == event->request_id && finish.batch_index == batches && finish.objects_submitted == objects); + gi_empty(); + app_poll(); +} + +/* 以应用自己的参数/缓冲构造单点ASDU;Submit成功后这些存储可全部修改。 */ +static void single_batch(IEC60870_Asdu_t *asdu, IEC60870_AppLayerParameters_t *parameters, + uint8_t *bytes, unsigned count) +{ + IEC60870_AsduHeader_t header = {20, 0, 1, 0x5a, 1}; + unsigned i; + *parameters = *IEC104_Slave_Get_Application_Layer_Parameters(app.slave); + CHECK(IEC60870_Asdu_Create(asdu, parameters, bytes, 249, &header, 0) == IEC60870_ASDU_OK); + for (i = 0; i < count; ++i) { + IEC60870_SinglePointInformation_t point; + CHECK(IEC60870_SinglePointInformation_Create(&point, 1001 + i, 1, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(asdu, &point.object) == IEC60870_ASDU_OK); + } +} + +/* 仅推进当前业务任务,不隐式Poll或消费Slave通知,便于检查自动推进的停止条件。 */ +static void gi_process_only(void) +{ + CHECK(IEC104_Interrogation_Process(app.interrogation)); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); +} + +/* 同一IOA的突发原文采用独立报文字节,避免由总召构造逻辑生成预期结果。 */ +static void spontaneous_value(uint8_t value) +{ + uint8_t bytes[] = {1, 1, 3, 0, 1, 0, 0xe9, 3, 0, 0}; + IEC60870_Asdu_t asdu; + bytes[9] = value; + CHECK(IEC60870_Asdu_Create_From_Buffer(&asdu, + IEC104_Slave_Get_Application_Layer_Parameters(app.slave), bytes, sizeof(bytes)) == IEC60870_ASDU_OK); + CHECK(IEC104_Slave_Enqueue_Event(app.slave, &asdu) == IEC104_TCP_SUBMIT_OK); +} + +/* 旧突发不能晚于新总召;新突发先发后,WAIT的旧总召必须重新取值,且不阻挡突发。 */ +static void test_gi_spontaneous_order(void) +{ + unsigned mode; + for (mode = 0; mode < 3; ++mode) { + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + IEC104_TcpServerStatus_t before, after; + uint8_t bytes[249]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); event = gi_data(); + single_batch(&batch, ¶meters, bytes, 1); + bytes[9] = mode == 0 ? 1 : 0; + if (mode == 2) { congest(&last_request.connection); } + spontaneous_value(mode == 0 ? 0 : 1); + CHECK(IEC104_Slave_Get_Status(app.slave, &before, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + CHECK(IEC104_Slave_Get_Status(app.slave, &after, NULL) == IEC104_TCP_OK); + CHECK(after.high_queue.stored_asdu_count == before.high_queue.stored_asdu_count && + after.normal_queue.pending_asdu_count == before.normal_queue.pending_asdu_count && + after.connection.pending_send_records == before.connection.pending_send_records); + gi_process_only(); + CHECK(app.gi.data_send_waiting && app.gi.batch_index == 0 && !app.gi.need_process); + memset(bytes, 0xcc, sizeof(bytes)); /* WAIT没有借用或缓存旧批次。 */ + fake.send_blocked = 0; gi_process(); + CHECK(collect_asdus(frames, lengths) == 2 && frames[1][2] == 3 && frames[1][9] == (mode == 0 ? 0 : 1)); + single_batch(&batch, ¶meters, bytes, 1); /* 重新取到当前值1。 */ + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(&event, 1, 1); + CHECK(collect_asdus(frames, lengths) == 4 && frames[2][2] == 0x94 && frames[2][9] == 1); + CHECK(frames[3][2] == 0x8a); + app_close(); + } +} + +/* 总召已接手后,即使只写了部分APDU,后来的新突发也必须排在它后面。 */ +static void test_gi_accepted_order(void) +{ + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + const uint8_t *frames[32]; + size_t lengths[32], written; + app_open(); event = gi_data(); + single_batch(&batch, ¶meters, bytes, 1); bytes[9] = 0; + written = fake.sent_bytes; fake.send_limit = 3; + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + CHECK(fake.sent_bytes > written && fake.sent_bytes < written + 16); + memset(bytes, 0xcc, sizeof(bytes)); + spontaneous_value(1); + fake.send_limit = 0; gi_process(); + CHECK(collect_asdus(frames, lengths) == 3); + CHECK(frames[1][2] == 0x94 && frames[1][9] == 0 && frames[2][2] == 3 && frames[2][9] == 1); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&event, 1, 1); + app_close(); +} + +/* 首次有效数据及时交付后,发送WAIT不耗尽取数期限;下一批仍受新的取数期限保护。 */ +static void test_gi_send_wait_timeout(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t event, finish; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + IEC104_Interrogation_Config_Default(&config); config.data_timeout_ms = 20; + custom_config = &config; app_open(); event = gi_data(); + congest(&last_request.connection); + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC60870_Asdu_Set_CA(&batch, 2) == IEC60870_ASDU_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_ERROR); + fake.now += 19; gi_process_only(); + CHECK(app.gi.application_waiting && !app.gi.data_send_waiting && app.gi.timeout_remaining_ms == 1); + CHECK(IEC60870_Asdu_Set_CA(&batch, 1) == IEC60870_ASDU_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + fake.now += 100; gi_process_only(); + CHECK(app.gi.timeouts == 0 && !app.gi.timer_active && !app.gi.application_waiting && app.gi.data_send_waiting); + fake.send_blocked = 0; gi_process(); + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(app.gi.application_waiting && !app.gi.data_send_waiting && app.gi.timeout_remaining_ms == 20); + fake.now += 20; gi_process_only(); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_DATA_TIMEOUT && finish.batch_index == 1); + app_close(); custom_config = NULL; +} + +/* k=1窗口满时不转入HIGH;ACK释放窗口后先发NORMAL,下一次ACK后才接手总召。 */ +static void test_gi_window_wait(void) +{ + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + IEC104_TcpServerStatus_t status; + uint8_t bytes[249]; + const uint8_t *frames[32]; + size_t lengths[32]; + custom_window = 1; app_open(); event = gi_data(); + single_batch(&batch, ¶meters, bytes, 1); bytes[9] = 0; + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.high_queue.stored_asdu_count == 0); + spontaneous_value(1); push_ack(1); gi_process(); + CHECK(collect_asdus(frames, lengths) == 2 && frames[1][2] == 3); + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + push_ack(2); gi_process(); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + push_ack(3); gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(&event, 1, 1); + CHECK(collect_asdus(frames, lengths) == 4 && frames[2][2] == 0x94 && frames[2][9] == 1); + app_close(); custom_window = 32; +} + +/* 对端确认期限已到时,不排队入口先让正常接收处理已经到达的ACK。 */ +static void test_gi_due_ack_wait(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + IEC104_Interrogation_Config_Default(&config); config.data_timeout_ms = 0; + custom_config = &config; app_open(); event = gi_data(); + single_batch(&batch, ¶meters, bytes, 1); + fake.now += 15000; push_ack(1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + gi_process(); + CHECK(app.gi.active && app.gi.batch_index == 0); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&event, 1, 1); + app_close(); custom_config = NULL; +} + +/* 不排队入口的FATAL使交互失效,不能把可能已发送的批次当成WAIT重试。 */ +static void test_gi_submit_fatal(void) +{ + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + app_open(); event = gi_data(); single_batch(&batch, ¶meters, bytes, 1); + sut->queue_initialized = 0; + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_STALE); + sut->queue_initialized = 1; + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_SEND_ERROR); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_STALE); + app_close(); +} + +/* 真实U帧填满启停通知槽后,新的GI及STOP同批到达;读空FIFO不自动受理保留的STOP。 */ +static void test_stop_in_process_after_state_backpressure(void) +{ + IEC104_InterrogationEvent_t event, finish; + IEC104_ConnectionStatus_t connection; + IEC104_TcpServerStatus_t service; + unsigned i; + app_open(); + CHECK(IEC104_TCP_EVENT_STATE_RESERVE % 2U == 0U); + for (i = 0; i < IEC104_TCP_EVENT_STATE_RESERVE; ++i) { + push(i % 2U == 0U ? stop_act : start_act, sizeof(start_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + } + queue_request(gi_request, sizeof(gi_request)); push(stop_act, sizeof(stop_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK && sut->rx_blocked); + CHECK(Example_Drain(&app)); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.pending_event_count == 0); + CHECK(IEC104_Connection_Get_Status(&sut->connection, sut->generation, &connection) == IEC104_CONNECTION_OK); + CHECK(connection.has_unhandled_frame && app.gi.gi_pending && app.gi.active); + gi_process_only(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process_only(); gi_empty(); + CHECK(app.gi.events_pending && !app.gi.need_process && app.gi.gi_pending && app.gi.cancelled == 0); + CHECK(app.gi.submitted == 0 && app.gi.waits == 0); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); + CHECK(!service.connection.data_transfer_started && service.pending_event_count != 0); + CHECK(Example_Drain(&app)); finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == IEC104_INTERROGATION_FINISH_STOPPED); + gi_empty(); app_close(); +} + +static int hold_transfer_change(void *context) +{ + (void)context; + return 0; +} + +/* 注入一次通知容量背压以覆盖任意业务阶段;STOP仍经真实Receiver/Drive受理,不伪造发送结果。 */ +static void retain_stop_for_submit(void) +{ + IEC104_ConnectionTransferObserver_t observer = sut->connection.transfer_observer; + IEC104_TcpServerStatus_t service; + sut->connection.transfer_observer.can_accept = hold_transfer_change; + push(stop_act, sizeof(stop_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + sut->connection.transfer_observer = observer; + CHECK(sut->rx_blocked && !sut->connection.receive_batch_pending); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK); + CHECK(service.pending_event_count == 0 && service.connection.data_transfer_started); +} + +/* 准备待交给Handle_Event的真实忙请求,测试立即应答入口;不调用业务处理器提前提交。 */ +static void read_busy_request(void) +{ + uint8_t request[sizeof(gi_request)]; + memcpy(request, gi_request, sizeof(request)); request[3] = 0x61; + queue_request(request, sizeof(request)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_NextEvent(app.slave, &last_request) == IEC104_TCP_EVENT_READY); + CHECK(last_request.transport.type == IEC104_TCP_EVENT_I_FRAME); +} + +/* Process、立即应答、Submit_Batch均须先交付STOP;不停止取数计时、不重发、可在START后重新总召。 */ +static void test_stop_during_submit(void) +{ + unsigned mode; + for (mode = 0; mode < 3; ++mode) { + IEC104_InterrogationEvent_t event, finish; + IEC104_InterrogationStatus_t before; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + app_open(); event = mode == 0 ? gi_begin() : gi_data(); + if (mode == 0) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } else if (mode == 1) { read_busy_request(); } + else { single_batch(&batch, ¶meters, bytes, 1); } + retain_stop_for_submit(); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &before)); + if (mode == 0) { CHECK(IEC104_Interrogation_Process(app.interrogation)); } + else if (mode == 1) { + CHECK(IEC104_Interrogation_Handle_Event(app.interrogation, &last_request) == IEC104_INTERROGATION_EVENT_HANDLED); + } else { + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + } + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.events_pending && !app.gi.need_process && app.gi.gi_pending && app.gi.cancelled == before.cancelled); + CHECK(app.gi.submitted == before.submitted && app.gi.waits == before.waits && app.gi.phase == before.phase); + CHECK(app.gi.batch_index == before.batch_index && app.gi.objects_submitted == before.objects_submitted); + CHECK(app.gi.data_send_waiting == before.data_send_waiting && app.gi.timer_active == before.timer_active); + CHECK(!before.timer_active || app.gi.timeout_remaining_ms == before.timeout_remaining_ms); + gi_empty(); gi_process_only(); gi_empty(); /* 通知未读时重复推进也不能抢先定终结原因。 */ + CHECK(Example_Drain(&app)); finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == IEC104_INTERROGATION_FINISH_STOPPED); + if (mode == 2) { + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_STALE); + } + gi_empty(); push(start_act, sizeof(start_act)); app_poll(); + app_request(gi_request, sizeof(gi_request)); app_finish(); + CHECK(app.gi.gi_submitted == 1 && app.gi.negative_submitted == 0); + app_close(); + } +} + +/* 通知已成功交付后注入内部故障,真实FATAL即使伴随STOP也不能降为WAIT。 */ +static int notify_stop_then_fail(void *context, IEC104_DataTransferState_t state) +{ + CHECK(Notify_Transfer_State(context, state)); + return 0; +} + +static void test_fatal_during_stop_submit(void) +{ + unsigned mode; + for (mode = 0; mode < 3; ++mode) { + IEC104_InterrogationEvent_t event, finish; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + IEC104_TcpServerStatus_t service; + uint8_t bytes[249]; + app_open(); event = mode == 0 ? gi_begin() : gi_data(); + if (mode == 0) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } else if (mode == 1) { read_busy_request(); } + else { single_batch(&batch, ¶meters, bytes, 1); } + retain_stop_for_submit(); sut->connection.transfer_observer.notify = notify_stop_then_fail; + if (mode == 0) { CHECK(IEC104_Interrogation_Process(app.interrogation)); } + else if (mode == 1) { + CHECK(IEC104_Interrogation_Handle_Event(app.interrogation, &last_request) == IEC104_INTERROGATION_EVENT_ERROR); + } else { + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_STALE); + } + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK); + CHECK(!service.connected && service.pending_event_count != 0 && service.connection.fault == IEC104_TCP_FAULT_INTERNAL); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == IEC104_INTERROGATION_FINISH_SEND_ERROR); + CHECK(Example_Drain(&app)); gi_empty(); app_close(); + } +} + +/* EOF后的提交属于ERROR;连接仍存在且传输快照仍为STARTED,不能误认成STOP。 */ +static void test_submit_error_without_stop(void) +{ + unsigned mode; + for (mode = 0; mode < 3; ++mode) { + IEC104_InterrogationEvent_t event, finish; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + IEC104_TcpServerStatus_t service; + uint8_t bytes[249]; + app_open(); event = mode == 0 ? gi_begin() : gi_data(); + if (mode == 0) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } else if (mode == 1) { read_busy_request(); } + else { single_batch(&batch, ¶meters, bytes, 1); } + sut->rx_eof = 1; /* 保留连接以完成EOF收尾,但不再允许新应答。 */ + if (mode == 0) { CHECK(IEC104_Interrogation_Process(app.interrogation)); } + else if (mode == 1) { + CHECK(IEC104_Interrogation_Handle_Event(app.interrogation, &last_request) == IEC104_INTERROGATION_EVENT_ERROR); + } else { + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_STALE); + } + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK); + CHECK(service.connected && service.connection.data_transfer_started && service.pending_event_count == 0); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == IEC104_INTERROGATION_FINISH_SEND_ERROR); + gi_empty(); app_close(); + } +} + +/* OK及WAIT也能顺带交付RECORD;成功只提交一次,后续应答必须等新通知被消费。 */ +static void test_submit_with_new_record(void) +{ + static const uint8_t ack[] = {0x68, 4, 1, 0, 2, 0}; + unsigned wait, i; + for (wait = 0; wait < 2; ++wait) { + IEC104_InterrogationEvent_t event; + IEC104_TcpServerStatus_t service; + IEC104_SlaveEvent_t record; + uint8_t request[sizeof(gi_request)]; + uint32_t consumed; + app_open(); event = gi_data(); + memcpy(request, gi_request, sizeof(request)); + for (i = 0; i < 2; ++i) { request[3] = (uint8_t)(0x60U + i); queue_request(request, sizeof(request)); } + app_poll(); CHECK(app.gi.pending_requests == 3 && app.gi.negative_submitted == 0); + if (wait) { congest(&last_request.connection); } + /* ACK已由连接层处理,回执尚未转入Slave FIFO,由Send_Response内的Consume_Records交付。 */ + CHECK(IEC104_Connection_Feed(&sut->connection, sut->generation, ack, sizeof(ack), fake.now, + &consumed) == IEC104_CONNECTION_OK && consumed == sizeof(ack)); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.pending_event_count == 0); + gi_process_only(); + CHECK(app.gi.events_pending && app.gi.negative_submitted == (wait ? 0U : 1U)); + CHECK(app.gi.pending_requests == (wait ? 3U : 2U) && app.gi.gi_pending && app.gi.cancelled == 0); + gi_process_only(); /* 即使再次调用,未读回执也阻止第二个应答提交。 */ + CHECK(app.gi.negative_submitted == (wait ? 0U : 1U)); + CHECK(IEC104_Slave_NextEvent(app.slave, &record) == IEC104_TCP_EVENT_READY); + CHECK(record.transport.type == IEC104_TCP_EVENT_RECORD && Example_Handle_Event(&app, &record)); + gi_empty(); fake.send_blocked = 0; + for (i = 0; i < 8; ++i) { gi_process(); } + CHECK(app.gi.negative_submitted == 2 && app.gi.pending_requests == 1); + gi_done(&event, 0, 0); app_close(); + } +} + +static void queue_busy(uint8_t oa) +{ + uint8_t request[sizeof(gi_request)]; + memcpy(request, gi_request, sizeof(request)); + request[3] = oa; + queue_request(request, sizeof(request)); +} + +static void request_busy(uint8_t oa) +{ + queue_busy(oa); + app_poll(); +} + +/* 本地决定/数据一旦接手,无需新网络事件即可续跑;查询本身不提交帧或消费事件。 */ +static void test_need_process_business(void) +{ + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + app_open(); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi) && !app.gi.need_process); + app_request(gi_request, sizeof(gi_request)); + CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.need_process && app.gi.pending_events == 0); + gi_process_only(); + CHECK(!app.gi.need_process && app.gi.pending_events == 1 && app.gi.submitted == 0); + event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(Example_Wait_Time(&app, 50) == 50 && !app.gi.need_process && app.gi.application_waiting); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.need_process && app.gi.submitted == 0); + gi_process_only(); + CHECK(!app.gi.need_process && app.gi.submitted == 1); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(Example_Wait_Time(&app, 50) == 50 && !app.gi.need_process); + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.need_process && app.gi.batch_index == 1); + CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.submitted == 2 && app.gi.pending_events == 0); + gi_process_only(); + CHECK(!app.gi.need_process && app.gi.batch_index == 1 && app.gi.objects_submitted == 1); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.need_process && app.gi.submitted == 2); + gi_process_only(); + CHECK(!app.gi.need_process && app.gi.pending_requests == 0 && app.gi.pending_events == 1); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_COMPLETED); + CHECK(Example_Wait_Time(&app, 50) == 50 && !app.gi.need_process); + app_close(); +} + +/* CONFIRM/批次/终止/短应答的真实发送背压均停止本地续跑,网络推进后显式重试恢复。 */ +static void test_need_process_send_wait(void) +{ + unsigned mode, i; + for (mode = 0; mode < 4; ++mode) { + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + unsigned long waits; + app_open(); event = mode == 0 ? gi_begin() : gi_data(); + congest(&last_request.connection); + if (mode == 0) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } else if (mode == 1) { + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + } else if (mode == 2) { + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + } else { + request_busy(0x60); + CHECK(!app.gi.need_process && app.gi.waits == 1); /* 入口已尝试并得到WAIT。 */ + } + if (mode == 0 || mode == 2) { CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.need_process); } + gi_process_only(); + CHECK(app.gi.waiting && !app.gi.need_process && app.gi.batch_index == 0); + waits = app.gi.waits; + for (i = 0; i < 3; ++i) { + CHECK(Example_Wait_Time(&app, 50) == 50 && !app.gi.need_process && app.gi.waits == waits); + } + fake.send_blocked = 0; gi_process(); + CHECK(!app.gi.need_process && app.gi.waits == waits); + if (mode == 1) { + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); + } + if (mode == 2) { + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_COMPLETED); + } else { + if (mode < 2) { event = gi_take(IEC104_INTERROGATION_NEED_DATA); } + gi_done(&event, mode == 1 ? 1U : 0U, mode == 1 ? 1U : 0U); + } + app_close(); + } +} + +/* 总召等应用不遮蔽后面的短应答;未读STOP/START始终先于本地续跑。 */ +static void test_need_process_reply_and_transport(void) +{ + IEC104_InterrogationEvent_t event; + app_open(); event = gi_data(); + queue_busy(0x60); queue_busy(0x61); app_poll(); + CHECK(app.gi.pending_requests == 3 && app.gi.application_waiting && app.gi.waiting); + CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.need_process); + gi_process_only(); + CHECK(app.gi.pending_requests == 1 && app.gi.negative_submitted == 2 && !app.gi.need_process); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.need_process); + push(stop_act, sizeof(stop_act)); push(start_act, sizeof(start_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.events_pending && !app.gi.need_process && app.gi.pending_requests == 1); + gi_process_only(); + CHECK(app.gi.events_pending && !app.gi.need_process && app.gi.gi_submitted == 0); + app_poll(); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(!app.gi.events_pending && !app.gi.need_process && app.gi.pending_requests == 0); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_STOPPED); + gi_empty(); app_close(); +} + +/* BEGIN和NEED_DATA均可长期留在应用手中,忙拒绝/错误应答不依赖应用先完成总召。 */ +static void test_reply_while_waiting(void) +{ + unsigned mode; + for (mode = 0U; mode < 2U; ++mode) { + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + uint8_t invalid[sizeof(gi_request)]; + app_open(); + event = mode == 0U ? gi_begin() : gi_data(); + request_busy(0x61); + memcpy(invalid, gi_request, sizeof(invalid)); invalid[4] = 2; + app_request(invalid, sizeof(invalid)); + CHECK(collect_asdus(frames, lengths) == mode + 2U); /* 安全的短应答已在入口提交,不等待Process。 */ + gi_process_only(); + CHECK(collect_asdus(frames, lengths) == mode + 2U); + CHECK(frames[mode][2] == 0xc7 && frames[mode][3] == 0x61); + invalid[2] = 0xee; /* TEST + P/N + UNKNOWN_COMMON_ADDRESS。 */ + CHECK(lengths[mode + 1U] == sizeof(invalid) && memcmp(frames[mode + 1U], invalid, sizeof(invalid)) == 0); + CHECK(app.gi.gi_pending && app.gi.pending_requests == 1 && app.gi.waiting && app.gi.application_waiting); + CHECK(app.gi.phase == (mode == 0U ? IEC104_INTERROGATION_START : IEC104_INTERROGATION_DATA)); + gi_process_only(); + CHECK(collect_asdus(frames, lengths) == mode + 2U); /* 等待应用时立即返回,不重发。 */ + gi_empty(); /* 原工作事件不被重复交付或替换。 */ + if (mode == 0U) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + } + gi_done(&event, 0, 0); + app_close(); + } +} + +/* 旧FINISHED占满最小业务FIFO时,新总召不能生成BEGIN,单帧应答仍可发送。 */ +static void test_reply_event_backpressure(void) +{ + IEC104_InterrogationEvent_t old, event; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); old = gi_begin(); + IEC104_Interrogation_Cancel(app.interrogation); + app_request(gi_request, sizeof(gi_request)); request_busy(0x61); + gi_process(); + CHECK(collect_asdus(frames, lengths) == 1 && frames[0][2] == 0xc7 && frames[0][3] == 0x61); + CHECK(app.gi.pending_requests == 1 && app.gi.pending_events == 1 && app.gi.waiting && !app.gi.application_waiting); + event = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(event.request_id == old.request_id && event.reason == IEC104_INTERROGATION_FINISH_CANCELLED); + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request_id != old.request_id); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &old, true) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(&event, 0, 0); app_close(); +} + +/* 持续释放并复用非队首槽:总召未完成也能接收后续请求,应答自身仍保持FIFO。 */ +static void test_reply_slot_reuse(void) +{ + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); event = gi_data(); + for (i = 0U; i < 3U; ++i) { queue_busy((uint8_t)(0x60U + i)); } + app_poll(); /* 同批通知未读空时先保存,后到应答不能越过已排队应答。 */ + CHECK(app.gi.pending_requests == 4); + for (i = 0U; i < 6U; ++i) { + congest_except_one_reply(&last_request.connection); + gi_process_only(); + fake.send_blocked = 0; + app_poll(); + CHECK(collect_asdus(frames, lengths) == i + 2U); + CHECK(lengths[i + 1U] == 10 && frames[i + 1U][2] == 0xc7 && frames[i + 1U][3] == 0x60U + i); + CHECK(app.gi.phase == IEC104_INTERROGATION_DATA && app.gi.application_waiting && app.gi.overloads == 0); + if (i < 3U) { request_busy((uint8_t)(0x63U + i)); CHECK(app.gi.pending_requests == 4); } + } + CHECK(app.gi.pending_requests == 1 && app.gi.batch_index == 0); + gi_done(&event, 0, 0); app_close(); +} + +/* 总召在旧应答之后建立:BEGIN出队及应用应答必须定位该请求,不能误操作索引队首。 */ +static void test_reply_non_head_events(void) +{ + IEC104_InterrogationEvent_t event; + uint8_t invalid[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + memcpy(invalid, gi_request, sizeof(invalid)); invalid[9] = 19; invalid[3] = 0x61; + queue_request(invalid, sizeof(invalid)); invalid[3] = 0x62; queue_request(invalid, sizeof(invalid)); + queue_request(gi_request, sizeof(gi_request)); app_poll(); + congest_except_one_reply(&last_request.connection); + gi_process_only(); /* 第一条应答、非队首总召BEGIN,再因第二条应答的WAIT返回。 */ + CHECK(app.gi.phase == IEC104_INTERROGATION_START && app.gi.pending_requests == 2); + event = gi_take(IEC104_INTERROGATION_BEGIN); + request_busy(0x63); /* 复用刚释放的槽,不能改变工作事件所指的总召。 */ + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + fake.send_blocked = 0; + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(collect_asdus(frames, lengths) == 4); + CHECK(frames[0][3] == 0x61 && frames[1][3] == 0x62 && frames[2][2] == 0x87 && frames[3][3] == 0x63); + gi_done(&event, 0, 0); app_close(); +} + +/* 一次调用在两类就绪任务之间轮转,自动提交当前批次并索取下一批;后续应答仍可穿插。 */ +static void test_reply_round_robin(void) +{ + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); event = gi_data(); + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + queue_busy(0x60); queue_busy(0x61); queue_busy(0x62); app_poll(); + gi_process_only(); + CHECK(app.gi.batch_index == 1 && app.gi.pending_requests == 1 && app.gi.application_waiting); + CHECK(collect_asdus(frames, lengths) == 5); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(event.batch_index == 1); + request_busy(0x63); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process_only(); CHECK(app.gi.batch_index == 2 && app.gi.application_waiting); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(event.batch_index == 2 && app.gi.pending_requests == 1); + gi_done(&event, 2, 2); + CHECK(collect_asdus(frames, lengths) == 8); + CHECK(frames[0][2] == 0x87 && frames[1][0] == 1 && frames[6][0] == 1 && frames[7][2] == 0x8a); + CHECK(frames[2][3] == 0x60 && frames[3][3] == 0x61 && frames[4][3] == 0x62 && frames[5][3] == 0x63); + app_close(); +} + +/* HIGH满时保留最早应答;后来的应答不能越过它,恢复后每条只提交一次。 */ +static void test_reply_send_wait(void) +{ + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); event = gi_data(); + congest(&last_request.connection); + request_busy(0x60); gi_process_only(); + CHECK(app.gi.pending_requests == 2 && app.gi.waits == 2); /* 入口和Process各尝试一次,均未接手。 */ + request_busy(0x61); gi_process_only(); + CHECK(app.gi.pending_requests == 3 && app.gi.waits == 3); + CHECK(collect_asdus(frames, lengths) == 1); + fake.send_blocked = 0; gi_process(); + CHECK(app.gi.pending_requests == 1 && app.gi.application_waiting); + CHECK(collect_asdus(frames, lengths) == 3 && frames[1][3] == 0x60 && frames[2][3] == 0x61); + gi_process(); CHECK(collect_asdus(frames, lengths) == 3); + gi_done(&event, 0, 0); app_close(); +} + +/* 新应答只能越过尚未提交的总召工作,不能越过发送层已经接手的数据帧。 */ +static void test_reply_preserves_submitted_data(void) +{ + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); event = gi_data(); fake.send_blocked = 1; + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process_only(); CHECK(app.gi.batch_index == 1); + request_busy(0x60); gi_process_only(); + CHECK(collect_asdus(frames, lengths) == 1); + fake.send_blocked = 0; gi_process(); + CHECK(collect_asdus(frames, lengths) == 3 && frames[1][0] == 1 && frames[2][2] == 0xc7); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(&event, 1, 1); app_close(); +} + +/* 短应答已完成一部分后,STOP/断线/超时仍须清掉其余任务,FINISHED恰好一次。 */ +static void test_reply_cleanup(void) +{ + IEC104_InterrogationConfig_t config; + unsigned mode; + IEC104_Interrogation_Config_Default(&config); config.data_timeout_ms = 20; + custom_config = &config; + for (mode = 0U; mode < 3U; ++mode) { + IEC104_InterrogationEvent_t event, finish; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); event = gi_data(); queue_busy(0x60); queue_busy(0x61); app_poll(); + congest_except_one_reply(&last_request.connection); gi_process_only(); + fake.send_blocked = 0; app_poll(); + CHECK(app.gi.pending_requests == 2 && collect_asdus(frames, lengths) == 2); + fake.now += 20; + if (mode == 0U) { + push(stop_act, sizeof(stop_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + } else if (mode == 1U) { + CHECK(IEC104_Slave_Close_Connection(app.slave, &last_request, NULL) == IEC104_TCP_OK); + } + gi_process_only(); + if (mode < 2U) { CHECK(app.gi.events_pending); } + app_poll(); + CHECK(collect_asdus(frames, lengths) == 2 && app.gi.pending_requests == 0); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == (mode == 0U ? IEC104_INTERROGATION_FINISH_STOPPED : + (mode == 1U ? IEC104_INTERROGATION_FINISH_DISCONNECTED : IEC104_INTERROGATION_FINISH_DATA_TIMEOUT))); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + gi_process(); gi_empty(); app_close(); + } + custom_config = NULL; +} + +/* 仅有单帧任务时,一次调用提交整个固定池中的就绪应答并回到空闲;再次调用无副作用。 */ +static void test_process_reply_only(void) +{ + uint8_t invalid[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + memcpy(invalid, gi_request, sizeof(invalid)); invalid[9] = 19; + for (i = 0U; i < 4U; ++i) { + invalid[3] = (uint8_t)(0x60U + i); + queue_request(invalid, sizeof(invalid)); + } + app_poll(); + CHECK(app.gi.pending_requests == 4 && app.gi.submitted == 0); + gi_process_only(); + CHECK(app.gi.pending_requests == 0 && app.gi.phase == IEC104_INTERROGATION_IDLE && !app.gi.waiting); + CHECK(collect_asdus(frames, lengths) == 4); + for (i = 0U; i < 4U; ++i) { + CHECK(lengths[i] == 10 && frames[i][2] == 0xc7 && frames[i][3] == 0x60U + i); + } + gi_process_only(); gi_empty(); + CHECK(app.gi.submitted == 4 && collect_asdus(frames, lengths) == 4); + app_close(); +} + +/* 短应答入口不得越过尚未交付的STOP/START;满槽时也不能用最后激活状态发送旧应答。 */ +static void test_immediate_reply_guard(void) +{ + unsigned mode, i; + for (mode = 0; mode < 2; ++mode) { + IEC104_InterrogationEvent_t event = {0}; + uint8_t request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + memcpy(request, gi_request, sizeof(request)); + if (mode == 0) { event = gi_data(); queue_busy(0x61); } + else { + for (i = 0; i < 4; ++i) { request[9] = (uint8_t)(20U + i); queue_request(request, sizeof(request)); } + app_poll(); /* 一个总召及三个REPLY,未读通知/旧REPLY阻止提前发送。 */ + request[9] = 24; queue_request(request, sizeof(request)); + } + push(stop_act, sizeof(stop_act)); push(start_act, sizeof(start_act)); + for (i = 0; i < 3; ++i) { CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); } + app_poll(); + CHECK(app.gi.active && app.gi.pending_requests == 0 && app.gi.negative_submitted == 0); + CHECK(collect_asdus(frames, lengths) == (mode == 0 ? 1U : 0U)); + if (mode == 0) { + IEC104_InterrogationEvent_t finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == IEC104_INTERROGATION_FINISH_STOPPED); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + } else { CHECK(app.gi.overloads == 1 && app.gi.dropped_requests == 1); } + gi_empty(); + memcpy(request, gi_request, sizeof(request)); request[4] = 2; + app_request(request, sizeof(request)); + CHECK(app.gi.negative_submitted == 1); /* 新启用的会话可立即应答。 */ + CHECK(memcmp(last_request.transport.data.i_frame.apdu + 6, request, sizeof(request)) == 0); + app_close(); + } +} + +/* 旧REPLY不能被新拒绝越过;发送也满时只丢新请求,原NEED_DATA保持有效。 */ +static void test_overload_drop_preserves_active(void) +{ + unsigned mode, i; + for (mode = 0; mode < 2; ++mode) { + IEC104_InterrogationEvent_t event; + IEC104_TcpServerStatus_t status; + uint8_t request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); event = gi_data(); + if (mode == 0) { + for (i = 0; i < 4; ++i) { queue_busy((uint8_t)(0x60U + i)); } + app_poll(); + } else { + memcpy(request, gi_request, sizeof(request)); + congest(&last_request.connection); + for (i = 21; i <= 23; ++i) { request[9] = (uint8_t)i; app_request(request, sizeof(request)); } + request[9] = 24; app_request(request, sizeof(request)); + } + CHECK(app.gi.pending_requests == 4 && app.gi.overloads == 1 && app.gi.dropped_requests == 1); + CHECK(app.gi.cancelled == 0 && app.gi.negative_submitted == 0 && app.gi.application_waiting); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.connected); + gi_empty(); + fake.send_blocked = 0; gi_process(); + if (mode == 0) { + CHECK(collect_asdus(frames, lengths) == 4 && app.gi.negative_submitted == 3); + for (i = 1; i < 4; ++i) { CHECK(frames[i][2] == 0xc7 && frames[i][3] == 0x5fU + i); } + } else { + app_poll(); + CHECK(app.gi.pending_requests == 1 && app.gi.negative_submitted == 3); + CHECK(collect_asdus(frames, lengths) == 4); + for (i = 1; i < 4; ++i) { CHECK(frames[i][2] == 0xc7 && frames[i][9] == 20U + i); } + } + gi_done(&event, 0, 0); /* 过载没有使原应用事件失效。 */ + CHECK(app.gi.dropped_requests == 1 && app.gi.overloads == 1); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.connected); + app_close(); + } +} + +/* 忙时不同组也拒绝且不自动重放;主站在结束后重新请求,每组保留自己的OA/TEST。 */ +static void test_qoi_sequential_retry(void) +{ + static const uint8_t qualifiers[] = {22, 20, 21}; + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_SinglePointInformation_t point; + uint8_t request[sizeof(gi_request)], bytes[249]; + const uint8_t *frames[32]; + size_t lengths[32]; + uint64_t previous_id = 0; + unsigned i; + app_open(); + for (i = 0; i < 3; ++i) { + IEC60870_AsduHeader_t header = {0}; + memcpy(request, gi_request, sizeof(request)); + request[9] = qualifiers[i]; request[3] = (uint8_t)(0x70U + i); request[2] = i == 1 ? 0x86 : 6; + app_request(request, sizeof(request)); + if (i == 0) { + unsigned later; + for (later = 1; later < 3; ++later) { + request[9] = qualifiers[later]; request[3] = (uint8_t)(0x70U + later); + request[2] = later == 1 ? 0x86 : 6; + app_request(request, sizeof(request)); + } + CHECK(app.gi.negative_submitted == 2 && app.gi.pending_requests == 1); + } + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.qualifier == qualifiers[i] && event.request.originator_address == 0x70U + i); + CHECK(event.request.is_test == (i == 1) && event.request_id > previous_id); + previous_id = event.request_id; + gi_empty(); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + header.cause_of_transmission = qualifiers[i]; header.common_address = 1; + header.originator_address = event.request.originator_address; header.is_test = event.request.is_test; + CHECK(IEC60870_Asdu_Create(&batch, IEC104_Slave_Get_Application_Layer_Parameters(app.slave), + bytes, sizeof(bytes), &header, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_SinglePointInformation_Create(&point, 1001U + i, 1, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&batch, &point.object) == IEC60870_ASDU_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(&event, 1, 1); + gi_process(); gi_empty(); CHECK(!app.gi.gi_pending && app.gi.pending_requests == 0); + } + CHECK(collect_asdus(frames, lengths) == 11 && app.gi.gi_submitted == 3 && !app.gi.gi_pending); + CHECK(frames[0][2] == 0xc7 && frames[0][9] == 20 && frames[0][3] == 0x71); + CHECK(frames[1][2] == 0x47 && frames[1][9] == 21 && frames[1][3] == 0x72); + for (i = 0; i < 3; ++i) { + unsigned first = 2U + i * 3U, test = i == 1 ? 0x80U : 0U; + CHECK(frames[first][2] == (test | 7U) && frames[first][9] == qualifiers[i]); + CHECK(frames[first + 1][0] == 1 && frames[first + 1][2] == (test | qualifiers[i])); + CHECK(frames[first + 2][2] == (test | 10U) && frames[first + 2][9] == qualifiers[i]); + CHECK(frames[first][3] == 0x70U + i && frames[first + 1][3] == 0x70U + i && frames[first + 2][3] == 0x70U + i); + } + app_close(); +} + +/* BEGIN生成前、交付前后、确认/数据/终止发送等待期间,均拒绝相同及不同QOI。 */ +static void test_busy_all_phases(void) +{ + unsigned mode, qoi; + for (mode = 0; mode < 8; ++mode) { + IEC104_InterrogationEvent_t event = {0}, finish; + IEC104_InterrogationPhase_t phase; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t request[sizeof(gi_request)], bytes[249]; + app_open(); app_request(gi_request, sizeof(gi_request)); + if (mode > 0) { gi_process(); } + if (mode > 1) { event = gi_take(IEC104_INTERROGATION_BEGIN); } + if (mode > 2) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } + if (mode > 3) { gi_process(); } + if (mode > 4) { event = gi_take(IEC104_INTERROGATION_NEED_DATA); } + if (mode == 6) { + single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + } else if (mode == 7) { + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + } + if (mode == 3 || mode >= 6) { congest(&last_request.connection); gi_process_only(); } + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); phase = app.gi.phase; + memcpy(request, gi_request, sizeof(request)); request[3] = 0x71; request[2] = 6; + for (qoi = 20; qoi <= 21; ++qoi) { request[9] = (uint8_t)qoi; app_request(request, sizeof(request)); } + CHECK(app.gi.rejected == 2 && app.gi.gi_pending && app.gi.phase == phase); + CHECK(app.gi.pending_requests == ((mode == 3 || mode >= 6) ? 3U : 1U)); + IEC104_Interrogation_Cancel(app.interrogation); + if (mode > 1) { + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.request.qualifier == 20); + CHECK(finish.request.originator_address == 0x5a && finish.request.is_test); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_CANCELLED); + } + gi_empty(); app_close(); + } +} + +/* 被忙拒绝的QOI没有撤销对象;匹配原请求仍可立即撤销,之后主站可重新发起其他组。 */ +static void test_busy_cancel_match(void) +{ + IEC104_InterrogationEvent_t event, next, finish; + uint8_t request[sizeof(gi_request)]; + app_open(); event = gi_data(); memcpy(request, gi_request, sizeof(request)); + request[9] = 21; app_request(request, sizeof(request)); + request[2] = 0x88; app_request(request, sizeof(request)); + CHECK(app.gi.pending_requests == 1 && app.gi.application_waiting && app.gi.deactivated == 0); + CHECK(app.gi.negative_submitted == 2); gi_empty(); + request[9] = 20; app_request(request, sizeof(request)); + CHECK(!app.gi.gi_pending && app.gi.pending_requests == 0 && app.gi.deactivated == 1); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == IEC104_INTERROGATION_FINISH_DEACTIVATED); + gi_empty(); + app_request(request, sizeof(request)); CHECK(app.gi.negative_submitted == 3); + request[9] = 21; request[2] = 0x86; app_request(request, sizeof(request)); + gi_process(); next = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(next.request.qualifier == 21 && next.request_id != event.request_id); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &next, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); next = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&next, 0, 0); + CHECK(app.gi.gi_submitted == 1); app_close(); +} + +/* 清理当前总召和积压的忙应答,仅已交付BEGIN的原请求产生FINISHED。 */ +static void test_busy_stop_cleanup(void) +{ + unsigned mode, qoi; + for (mode = 0; mode < 3; ++mode) { + IEC104_InterrogationEvent_t event, finish; + uint8_t request[sizeof(gi_request)]; + app_open(); event = gi_data(); memcpy(request, gi_request, sizeof(request)); + for (qoi = 21; qoi <= 22; ++qoi) { request[9] = (uint8_t)qoi; queue_request(request, sizeof(request)); } + app_poll(); CHECK(app.gi.pending_requests == 3); + if (mode == 0) { push(stop_act, sizeof(stop_act)); app_poll(); } + else if (mode == 1) { + CHECK(IEC104_Slave_Close_Connection(app.slave, &last_request, NULL) == IEC104_TCP_OK); app_poll(); + } else { IEC104_Interrogation_Cancel(app.interrogation); } + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.pending_requests == 0 && !app.gi.gi_pending && app.gi.cancelled == 3); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == (mode == 0 ? IEC104_INTERROGATION_FINISH_STOPPED : + (mode == 1 ? IEC104_INTERROGATION_FINISH_DISCONNECTED : IEC104_INTERROGATION_FINISH_CANCELLED))); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + gi_empty(); app_close(); + } +} + +/* 后来的忙应答不会重置原总召的执行期限。 */ +static void test_busy_preserves_deadline(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t event, finish; + IEC104_TcpServerStatus_t status; + uint8_t request[sizeof(gi_request)]; + IEC104_Interrogation_Config_Default(&config); + config.data_timeout_ms = 500; config.total_timeout_ms = 100; + custom_config = &config; app_open(); event = gi_data(); fake.now += 99; + memcpy(request, gi_request, sizeof(request)); request[9] = 21; app_request(request, sizeof(request)); + request[9] = 22; app_request(request, sizeof(request)); + CHECK(app.gi.timeout_remaining_ms == 1 && app.gi.negative_submitted == 2 && app.gi.pending_requests == 1); + ++fake.now; gi_process_only(); + CHECK(app.gi.timeouts == 1 && app.gi.pending_requests == 0 && !app.gi.gi_pending); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT && finish.request_id == event.request_id); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && !status.connected); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + gi_empty(); app_close(); custom_config = NULL; +} + +/* 主站重新发起的下一个组获得完整执行期限,不继承此前被拒绝请求的等待时间。 */ +static void test_next_request_deadline(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t event; + uint8_t request[sizeof(gi_request)]; + IEC104_Interrogation_Config_Default(&config); + config.total_timeout_ms = 100; config.begin_timeout_ms = config.data_timeout_ms = 0; + custom_config = &config; app_open(); event = gi_data(); + memcpy(request, gi_request, sizeof(request)); request[9] = 21; app_request(request, sizeof(request)); + fake.now += 90; gi_done(&event, 0, 0); gi_process(); gi_empty(); + app_request(request, sizeof(request)); gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.qualifier == 21 && app.gi.timeout_remaining_ms == 100); + fake.now += 99; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&event, 0, 0); + CHECK(app.gi.timeouts == 0 && app.gi.gi_submitted == 2); + app_close(); custom_config = NULL; +} + +/* 历史FINISHED占据业务FIFO时,仅当前START等待空位;没有排队计时,但仍拒绝新总召。 */ +static void test_begin_capacity_wait(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t old, next, finish; + uint8_t request[sizeof(gi_request)]; + IEC104_Interrogation_Config_Default(&config); + config.begin_timeout_ms = 10; config.data_timeout_ms = 20; config.total_timeout_ms = 100; + custom_config = &config; app_open(); old = gi_begin(); IEC104_Interrogation_Cancel(app.interrogation); + memcpy(request, gi_request, sizeof(request)); request[9] = 21; app_request(request, sizeof(request)); + gi_process_only(); CHECK(app.gi.gi_pending && app.gi.waiting && !app.gi.timer_active && !app.gi.need_process); + request[9] = 22; app_request(request, sizeof(request)); + CHECK(app.gi.negative_submitted == 1 && app.gi.pending_requests == 1); + fake.now += 60001; gi_process_only(); + CHECK(app.gi.pending_requests == 1 && !app.gi.timer_active && app.gi.timeouts == 0); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == old.request_id && finish.reason == IEC104_INTERROGATION_FINISH_CANCELLED); + CHECK(Example_Wait_Time(&app, 50) == 0 && app.gi.need_process && app.gi.waiting && app.gi.pending_events == 0); + gi_process_only(); next = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(next.request.qualifier == 21 && next.request_id != old.request_id); + CHECK(app.gi.timer_active && app.gi.timeout_remaining_ms == 10); + IEC104_Interrogation_Cancel(app.interrogation); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request_id == next.request_id); + gi_empty(); app_close(); custom_config = NULL; +} + +/* BEGIN生成前也可匹配撤销;Slave中已到达的撤销先于业务推进,不交付多余BEGIN。 */ +static void test_cancel_before_begin(void) +{ + uint8_t request[sizeof(gi_request)]; + IEC104_InterrogationEvent_t event; + app_open(); app_request(gi_request, sizeof(gi_request)); + memcpy(request, gi_request, sizeof(request)); request[9] = 21; queue_request(request, sizeof(request)); + request[9] = 20; request[2] = 0x88; queue_request(request, sizeof(request)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + gi_process_only(); CHECK(app.gi.events_pending && app.gi.pending_events == 0); + app_poll(); gi_process(); gi_empty(); + CHECK(app.gi.deactivated == 1 && app.gi.negative_submitted == 1 && !app.gi.gi_pending); + CHECK(app.gi.pending_requests == 0 && app.gi.gi_submitted == 0); + request[9] = 21; request[2] = 0x86; app_request(request, sizeof(request)); + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); CHECK(event.request.qualifier == 21); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&event, 0, 0); + app_close(); +} + +static void test_event_empty_and_wait(void) +{ + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + event = gi_data(); + for (i = 0; i < 20; ++i) { + gi_process(); + CHECK(app.gi.application_waiting && app.gi.waiting && app.gi.batch_index == 0 && app.gi.submitted == 1); + gi_empty(); /* 已取出的NEED_DATA只交付一次,数据源未就绪不重复请求。 */ + } + gi_done(&event, 0, 0); + CHECK(collect_asdus(frames, lengths) == 2); + CHECK(frames[0][2] == 0x87 && frames[1][2] == 0x8a); + app_close(); +} + +static void test_event_copy_and_send_wait(void) +{ + IEC104_InterrogationEvent_t event, next; + IEC60870_Asdu_t asdu; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249], expected[10]; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + event = gi_data(); + single_batch(&asdu, ¶meters, bytes, 1); + memcpy(expected, bytes, sizeof(expected)); + congest(&last_request.connection); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &asdu) == IEC104_INTERROGATION_REPLY_WAIT); + for (i = 0; i < 3; ++i) { + gi_process(); + CHECK(app.gi.waiting && app.gi.data_send_waiting && !app.gi.application_waiting && app.gi.batch_index == 0); + gi_empty(); + } + fake.send_blocked = 0; gi_process(); + single_batch(&asdu, ¶meters, bytes, 1); + fake.send_blocked = 1; /* 成功接手但未写出时仍须复制原文,不能借用应用缓冲。 */ + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &asdu) == IEC104_INTERROGATION_REPLY_OK); + memset(bytes, 0xcc, sizeof(bytes)); + memset(¶meters, 0, sizeof(parameters)); + memset(&asdu, 0, sizeof(asdu)); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + fake.send_blocked = 0; + gi_process(); + next = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(next.batch_index == 1 && next.objects_submitted == 1); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + gi_done(&next, 1, 1); + CHECK(collect_asdus(frames, lengths) == 3 && lengths[1] == sizeof(expected)); + CHECK(memcmp(frames[1], expected, sizeof(expected)) == 0); + app_close(); +} + +static void test_event_batch_validation(void) +{ + unsigned mode; + for (mode = 0; mode < 12; ++mode) { + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t asdu; + IEC60870_AppLayerParameters_t parameters; + IEC104_TcpServerStatus_t status; + uint8_t bytes[249]; + app_open(); + event = gi_data(); + single_batch(&asdu, ¶meters, bytes, mode == 6 ? 5 : mode == 7 ? 0 : 1); + switch (mode) { + case 0: bytes[2] = 0x83; break; /* COT */ + case 1: bytes[2] = 0xd4; break; /* P/N */ + case 2: bytes[2] = 0x14; break; /* TEST */ + case 3: bytes[3] = 0; break; /* OA */ + case 4: bytes[4] = 2; break; /* CA */ + case 5: bytes[0] = 100; bytes[9] = 20; break; /* 命令类型不作为总召数据 */ + case 6: break; /* 超过配置的四点上限 */ + case 7: break; /* 空容器 */ + case 8: parameters.sizeOfIOA = 2; break; + case 9: bytes[1] = 2; break; /* 不完整载荷 */ + case 10: asdu.length = 250; break; + default: asdu.parameters = NULL; break; + } + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &asdu) == IEC104_INTERROGATION_REPLY_ERROR); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.connected); + single_batch(&asdu, ¶meters, bytes, 1); /* 同一事件修正后仍可提交。 */ + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &asdu) == IEC104_INTERROGATION_REPLY_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &asdu) == IEC104_INTERROGATION_REPLY_STALE); + gi_process(); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(&event, 1, 1); + app_close(); + } +} + +/* 不同类型、SQ及容量边界通过独立线缆字节断言,不依赖应用示例点表。 */ +static void test_event_mixed_types(void) +{ + static const uint8_t data[][14] = { + {3, 0x82, 0x94, 0x5a, 1, 0, 0x56, 0x34, 0x12, 0x81, 0x82}, + {13, 1, 0x94, 0x5a, 1, 0, 200, 0, 0, 0, 0, 0xc0, 0x3f, 0x10}, + {9, 1, 0x94, 0x5a, 1, 0, 0x2c, 1, 0, 0, 0xc0, 0} + }; + static const uint16_t sizes[] = {11, 14, 12}; + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + event = gi_data(); + for (i = 0; i < 3; ++i) { + IEC60870_Asdu_t asdu; + uint8_t bytes[14]; + memcpy(bytes, data[i], sizes[i]); + CHECK(IEC60870_Asdu_Create_From_Buffer(&asdu, + IEC104_Slave_Get_Application_Layer_Parameters(app.slave), bytes, sizes[i]) == IEC60870_ASDU_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &asdu) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); + } + gi_done(&event, 3, 4); + CHECK(collect_asdus(frames, lengths) == 5); + for (i = 0; i < 3; ++i) { + CHECK(lengths[i + 1] == sizes[i] && memcmp(frames[i + 1], data[i], sizes[i]) == 0); + } + app_close(); +} + +static void test_event_capacity_batches(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t event; + IEC60870_AppLayerParameters_t parameters; + IEC60870_Asdu_t asdu; + uint8_t bytes[249]; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned batch, index, counts[] = {60, 60, 2, 40, 40, 1}; + IEC104_Interrogation_Config_Default(&config); + custom_config = &config; + app_open(); + event = gi_data(); + parameters = *IEC104_Slave_Get_Application_Layer_Parameters(app.slave); + for (batch = 0; batch < 6; ++batch) { + IEC60870_AsduHeader_t header = {20, 0, 1, 0x5a, 1}; + CHECK(IEC60870_Asdu_Create(&asdu, ¶meters, bytes, sizeof(bytes), &header, 0) == IEC60870_ASDU_OK); + for (index = 0; index < counts[batch]; ++index) { + if (batch < 3) { + IEC60870_SinglePointInformation_t point; + CHECK(IEC60870_SinglePointInformation_Create(&point, 1001 + (batch * 60) + index, 1, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_OK); + } else { + IEC60870_MeasuredValueScaled_t point; + CHECK(IEC60870_MeasuredValueScaled_Create(&point, 2001 + ((batch - 3) * 40) + index, -125, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&asdu, &point.object) == IEC60870_ASDU_OK); + } + } + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &asdu) == IEC104_INTERROGATION_REPLY_OK); + /* 大批次可能填满实际HIGH,循环Poll后再请求下一批。 */ + do { gi_process(); } while (app.gi.batch_index == batch); + event = gi_take(IEC104_INTERROGATION_NEED_DATA); + } + gi_done(&event, 6, 203); + CHECK(collect_asdus(frames, lengths) == 8); + for (batch = 0; batch < 6; ++batch) { + CHECK(frames[batch + 1][1] == counts[batch] && lengths[batch + 1] == 6 + counts[batch] * (batch < 3 ? 4 : 6)); + } + app_close(); + custom_config = NULL; +} + +static void test_event_reject_and_fail(void) +{ + unsigned mode; + for (mode = 0; mode < 3; ++mode) { + IEC104_InterrogationEvent_t event, finish; + IEC104_TcpServerStatus_t service; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + event = mode == 2 ? gi_data() : gi_begin(); + if (mode == 0) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, false) == IEC104_INTERROGATION_REPLY_OK); + } else { + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + } + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == (mode == 0 ? IEC104_INTERROGATION_FINISH_REJECTED : IEC104_INTERROGATION_FINISH_SOURCE_ERROR)); + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + gi_process(); + app_poll(); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK); + if (mode < 2) { + CHECK(service.connected && collect_asdus(frames, lengths) == 1 && frames[0][2] == 0xc7); + } else { + CHECK(!service.connected && app.gi.gi_submitted == 0); + CHECK(collect_asdus(frames, lengths) == (mode == 2 ? 1U : 0U)); + } + gi_empty(); + app_close(); + } +} + +static void test_event_cancellation(void) +{ + static const IEC104_InterrogationFinishReason_t reasons[] = { + IEC104_INTERROGATION_FINISH_CANCELLED, IEC104_INTERROGATION_FINISH_STOPPED, + IEC104_INTERROGATION_FINISH_DISCONNECTED + }; + unsigned mode; + for (mode = 0; mode < sizeof(reasons) / sizeof(reasons[0]); ++mode) { + IEC104_InterrogationEvent_t event, finish; + app_open(); + event = gi_data(); + if (mode == 0) { IEC104_Interrogation_Cancel(app.interrogation); } + if (mode == 1) { push(stop_act, sizeof(stop_act)); app_poll(); } + if (mode == 2) { + CHECK(IEC104_Slave_Close_Connection(app.slave, &last_request, NULL) == IEC104_TCP_OK); + app_poll(); + } + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(!IEC104_Interrogation_Destroy(app.interrogation)); /* 终结事件尚未读取。 */ + IEC104_Interrogation_Cancel(app.interrogation); /* 重复取消不覆盖已排队的终结原因。 */ + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == reasons[mode] && finish.request_id == event.request_id); + gi_empty(); + app_close(); + } +} + +/* 最小两槽FIFO:旧FINISHED未读时暂停新BEGIN,为将来的终结保留容量。 */ +static void test_event_queue_and_destroy(void) +{ + IEC104_InterrogationEvent_t first, second, finish; + app_open(); + first = gi_begin(); + CHECK(!IEC104_Interrogation_Destroy(app.interrogation)); /* 正在等待应用决定。 */ + IEC104_Interrogation_Cancel(app.interrogation); + app_request(gi_request, sizeof(gi_request)); + gi_process(); + CHECK(app.gi.pending_events == 1 && app.gi.waiting && !app.gi.application_waiting); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == first.request_id); + gi_process(); + second = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(second.request_id != first.request_id); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &first, true) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &second, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); /* NEED_DATA已入队,取消必须移除它,只保留FINISHED。 */ + IEC104_Interrogation_Cancel(app.interrogation); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == second.request_id); + gi_empty(); + app_request(gi_request, sizeof(gi_request)); + gi_process(); /* BEGIN未取出便取消,不通知未建立资源的应用。 */ + IEC104_Interrogation_Cancel(app.interrogation); + gi_empty(); + app_close(); +} + +static void test_event_pending_stop_start(void) +{ + IEC104_InterrogationEvent_t event, finish, next, changed; + uint64_t generation; + app_open(); + event = gi_data(); + generation = event.request.generation; + push(stop_act, sizeof(stop_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + push(start_act, sizeof(start_act)); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_WAIT); + CHECK(IEC104_Interrogation_Process(app.interrogation)); + app_poll(); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_STOPPED); + next = gi_begin(); + CHECK(next.request.generation == generation && next.request_id != event.request_id); + changed = next; ++changed.instance_id; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &changed, true) == IEC104_INTERROGATION_REPLY_STALE); + changed = next; ++changed.batch_index; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &changed, true) == IEC104_INTERROGATION_REPLY_STALE); + changed = next; changed.type = IEC104_INTERROGATION_FINISHED; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &changed, true) == IEC104_INTERROGATION_REPLY_STALE); + app_close(); +} + +static void test_event_instance_reuse(void) +{ + IEC104_InterrogationEvent_t old, current; + IEC104_Interrogation_t *slot; + app_open(); + old = gi_begin(); + slot = app.interrogation; + app_close(); + app_open(); + current = gi_begin(); + CHECK(app.interrogation == slot && current.request_id == old.request_id && current.instance_id != old.instance_id); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &old, true) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &old) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, ¤t, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); + current = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(¤t, 0, 0); + app_close(); +} + +/* 总召入口通过Slave统一拒绝错误副本/身份,不能改变已交付BEGIN的任务。 */ +static void test_slave_event_validation(void) +{ + IEC104_InterrogationEvent_t begin, data; + IEC104_InterrogationStatus_t before, after; + IEC104_SlaveEvent_t invalid; + unsigned mode; + app_open(); + begin = gi_begin(); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &before)); + invalid = last_request; + CHECK(IEC104_Interrogation_Handle_Event(app.interrogation, &invalid) == IEC104_INTERROGATION_EVENT_ERROR); + CHECK(IEC104_Slave_Copy_Event(&invalid, &last_request)); + ++invalid.transport.generation; + CHECK(IEC104_Interrogation_Handle_Event(app.interrogation, &invalid) == IEC104_INTERROGATION_EVENT_ERROR); + /* 仅注入公共视图失败;正常线上截短报文在Receiver中被挡住,不会产生这种I帧事件。 */ + for (mode = 0; mode < 4; ++mode) { + CHECK(IEC104_Slave_Copy_Event(&invalid, &last_request)); + invalid.asdu_result = IEC60870_ASDU_ERROR; + invalid.transport.data.i_frame.apdu[6] = mode == 0 ? 101 : 100; + if (mode == 2) { invalid.transport.data.i_frame.apdu_length = 6; } + if (mode == 3) { invalid.transport.data.i_frame.apdu_length = 7; } + CHECK(IEC104_Interrogation_Handle_Event(app.interrogation, &invalid) == IEC104_INTERROGATION_EVENT_IGNORED); + CHECK(Example_Handle_Event(&app, &invalid) && app.other_submitted == 0 && app.other_rejected == 0); + } + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &after)); + CHECK(after.pending_requests == before.pending_requests && after.gi_pending && + after.phase == before.phase && after.cancelled == before.cancelled && after.rejected == before.rejected); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &begin, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); + data = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(&data, 0, 0); + app_close(); +} + +/* 所有16组都由应用通过同一事件API接受;用独立请求/输出字节核对QOI和数据COT。 */ +static void test_group_interrogation(void) +{ + unsigned qoi; + for (qoi = 20; qoi <= 36; ++qoi) { + uint8_t request[sizeof(gi_request)], bytes[249]; + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + memcpy(request, gi_request, sizeof(request)); request[9] = (uint8_t)qoi; + app_request(request, sizeof(request)); + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.qualifier == qoi); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + single_batch(&batch, ¶meters, bytes, 1); + if (qoi != 20) { + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_ERROR); + } + CHECK(IEC60870_Asdu_Set_COT(&batch, (uint8_t)qoi) == IEC60870_ASDU_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + gi_done(&event, 1, 1); + CHECK(collect_asdus(frames, lengths) == 3); + request[2] = 0x87; CHECK(lengths[0] == 10 && memcmp(frames[0], request, 10) == 0); + CHECK(lengths[1] == 10 && frames[1][0] == 1 && frames[1][2] == (0x80 | qoi)); + CHECK(frames[1][3] == 0x5a && frames[1][4] == 1 && frames[1][6] == 0xe9); + request[2] = 0x8a; CHECK(lengths[2] == 10 && memcmp(frames[2], request, 10) == 0); + app_close(); + } +} + +/* 点表属于应用:示例仅配置组1遥信和组2遥测,其余合法组交给应用否定确认。 */ +static void test_example_groups(void) +{ + static const uint8_t groups[] = {21, 22, 23, 36}; + unsigned i, f; + for (i = 0; i < sizeof(groups); ++i) { + uint8_t request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned count; + app_open(); + memcpy(request, gi_request, sizeof(request)); request[9] = groups[i]; + app_request(request, sizeof(request)); app_finish(); + count = collect_asdus(frames, lengths); + CHECK(count == (groups[i] == 21 ? 6U : groups[i] == 22 ? 4U : 1U)); + if (count == 1) { CHECK(frames[0][2] == 0xc7 && app.gi.gi_submitted == 0); } + else { + CHECK(frames[0][2] == 0x87 && frames[count - 1U][2] == 0x8a); + for (f = 1; f < count - 1U; ++f) { + const uint8_t *a = frames[f]; + unsigned is_double = groups[i] == 21 && f >= 3U; + unsigned first = groups[i] == 22 ? 2001U : is_double ? 1101U : 1001U; + unsigned batch = is_double ? f - 3U : f - 1U; + CHECK(a[0] == (groups[i] == 22 ? 11 : is_double ? 3 : 1) && a[1] == 4 && a[2] == (0x80 | groups[i])); + CHECK((unsigned)(a[6] | (unsigned)a[7] << 8 | (unsigned)a[8] << 16) == first + batch * 4U); + if (is_double) { + unsigned point; + CHECK(lengths[f] == 22U); + for (point = 0U; point < 4U; ++point) { CHECK(a[9U + point * 4U] == 1U); } + } + } + } + CHECK(frames[0][9] == groups[i] && app.session_request == 0); + app_close(); + } +} + +/* 站总召和遥信分组均包含双点实时值:首批双点遭发送WAIT时保持游标, + * 下一次尝试重新读取变化后的double_values;最终每个双点恰好交接一次。 */ +static void test_example_double_wait_values(void) +{ + unsigned qualifier; + for (qualifier = 20U; qualifier <= 21U; ++qualifier) { + uint8_t request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned step, point; + app_open(); + memcpy(request, gi_request, sizeof(request)); request[9] = (uint8_t)qualifier; + app_request(request, sizeof(request)); + for (step = 0U; step < 32U; ++step) { + CHECK(IEC104_Interrogation_Process(app.interrogation)); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + if (app.gi.phase == IEC104_INTERROGATION_DATA && app.gi.batch_index == 2U) { break; } + CHECK(Example_Drain_Business(&app)); + } + CHECK(step < 32U && app.gi_data.single_index == 8U && app.gi_data.double_index == 0U); + congest(&last_request.connection); + CHECK(Example_Step(&app) && app.business_pending && app.gi.waiting); + CHECK(app.gi_data.double_index == 0U && app.gi.batch_index == 2U && app.batch_bytes[9] == 1U); + app.double_values[0] = app.double_values[3] = app.double_values[7] = 2U; + CHECK(Example_Step(&app) && app.business_pending && app.gi.waiting); + CHECK(app.gi_data.double_index == 0U && app.gi.batch_index == 2U && app.batch_bytes[9] == 2U); + fake.send_blocked = 0; + app_finish(); + CHECK(collect_asdus(frames, lengths) == (qualifier == 20U ? 8U : 6U)); + for (point = 0U; point < 8U; ++point) { + unsigned batch = 3U + point / 4U; + const uint8_t *a = frames[batch]; + const uint8_t *object = a + 6U + (point % 4U) * 4U; + CHECK(lengths[batch] == 22U && a[0] == 3U && a[1] == 4U && a[2] == (0x80U | qualifier)); + CHECK((unsigned)(object[0] | (unsigned)object[1] << 8 | (unsigned)object[2] << 16) == 1101U + point); + CHECK(object[3] == (point == 0U || point == 3U || point == 7U ? 2U : 1U)); + } + CHECK(app.gi.gi_submitted == 1U && app.session_request == 0U && app.gi_station == NULL); + app_close(); + } +} + +/* 撤销覆盖所有阶段,包含未读事件、已取事件、HIGH满、已提交部分数据和待发终止。 + * 已提交帧保留顺序;尚未提交批次及ACT_TERM不能越过肯定撤销确认。 */ +static void test_remote_deactivation_phases(void) +{ + unsigned phase; + for (phase = 0; phase <= 8; ++phase) { + uint8_t request[sizeof(gi_request)], bytes[249]; + IEC104_InterrogationEvent_t event = {0}, finish; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned prior = phase >= 4 ? 1U : 0U; + app_open(); + app_request(gi_request, sizeof(gi_request)); + if (phase >= 1) { gi_process(); } + if (phase >= 2) { event = gi_take(IEC104_INTERROGATION_BEGIN); } + if (phase >= 3) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } + if (phase >= 4) { gi_process(); } + if (phase >= 5) { event = gi_take(IEC104_INTERROGATION_NEED_DATA); } + if (phase == 6 || phase == 8) { + single_batch(&batch, ¶meters, bytes, 1); + if (phase == 6) { congest(&last_request.connection); } + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == + (phase == 6 ? IEC104_INTERROGATION_REPLY_WAIT : IEC104_INTERROGATION_REPLY_OK)); + } + if (phase == 7) { + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + } + if (phase == 8) { + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); ++prior; + } + if (phase == 3 || phase == 6 || phase == 7) { + if (phase != 6) { congest(&last_request.connection); } + gi_process(); CHECK(app.gi.waiting); + } + memcpy(request, gi_request, sizeof(request)); request[2] = 0x88; + app_request(request, sizeof(request)); + CHECK(!app.gi.gi_pending && app.gi.active && app.gi.deactivated == 1 && + app.gi.pending_requests == (fake.send_blocked ? 1U : 0U)); + if (phase >= 2) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, NULL) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_DEACTIVATED && finish.request_id == event.request_id); + CHECK(finish.batch_index == (phase == 8 ? 1U : 0U)); + } + gi_empty(); + if (fake.send_blocked) { + gi_process(); CHECK(app.gi.waiting && app.gi.phase == IEC104_INTERROGATION_REPLY); + fake.send_blocked = 0; + } + app_finish(); + CHECK(collect_asdus(frames, lengths) == prior + 1 && app.gi.gi_submitted == 0); + request[2] = 0x89; + CHECK(lengths[prior] == 10 && memcmp(frames[prior], request, 10) == 0); + app_request(gi_request, sizeof(gi_request)); app_finish(); + CHECK(app.gi.gi_submitted == 1 && collect_asdus(frames, lengths) == prior + 9); + app_close(); + } +} + +static void test_remote_deactivation_validation(void) +{ + static const struct { unsigned offset; uint8_t value, cot; } cases[] = { + {9, 21, 9}, {3, 0x6b, 9}, {2, 8, 9}, {9, 19, 9}, {9, 37, 9}, + {2, 0xc8, 9}, {4, 2, 46}, {6, 1, 47}, {2, 0x83, 45} + }; + unsigned i; + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + IEC104_InterrogationEvent_t event; + uint8_t request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); event = gi_data(); + memcpy(request, gi_request, sizeof(request)); request[2] = 0x88; + request[cases[i].offset] = cases[i].value; + app_request(request, sizeof(request)); + CHECK(app.gi.gi_pending && app.gi.deactivated == 0); + gi_done(&event, 0, 0); /* 不匹配撤销不终结当前请求,原任务仍能正常完成。 */ + CHECK(collect_asdus(frames, lengths) == 3); + request[2] = (request[2] & 0x80) | 0x40 | cases[i].cot; + CHECK(lengths[1] == 10 && memcmp(frames[1], request, 10) == 0 && frames[2][2] == 0x8a); + memcpy(request, gi_request, sizeof(request)); request[2] = 0x88; + app_request(request, sizeof(request)); app_finish(); /* 已完成,无可撤销任务。 */ + CHECK(collect_asdus(frames, lengths) == 4 && frames[3][2] == 0xc9); + app_close(); + } +} + +/* 满队列中撤销非队首的分组总召,不能因缺少额外槽而断线;重复撤销回否定COT=9。 */ +static void test_remote_deactivation_full_queue(void) +{ + uint8_t invalid[sizeof(gi_request)], request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + memcpy(invalid, gi_request, sizeof(invalid)); invalid[9] = 19; + memcpy(request, gi_request, sizeof(request)); request[9] = 36; + queue_request(invalid, sizeof(invalid)); queue_request(request, sizeof(request)); + queue_request(invalid, sizeof(invalid)); queue_request(invalid, sizeof(invalid)); app_poll(); + CHECK(app.gi.pending_requests == 4 && app.gi.gi_pending); + request[2] = 0x88; app_request(request, sizeof(request)); + CHECK(app.gi.active && app.gi.deactivated == 1 && app.gi.overloads == 0 && app.gi.pending_requests == 4); + app_finish(); gi_empty(); + CHECK(collect_asdus(frames, lengths) == 4); + CHECK(frames[0][2] == 0xc7 && frames[1][2] == 0xc7 && frames[2][2] == 0xc7 && + frames[3][2] == 0x89 && frames[3][9] == 36); + app_request(request, sizeof(request)); app_finish(); + CHECK(collect_asdus(frames, lengths) == 5 && frames[4][2] == 0xc9); + app_close(); +} + +static void check_timeout(IEC104_InterrogationFinishReason_t reason, unsigned delivered, uint64_t id, + bool keep_connection) +{ + IEC104_TcpServerStatus_t service; + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.timeouts == 1 && app.gi.last_finish_reason == reason && !app.gi.gi_pending && !app.gi.timer_active); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected == keep_connection); + if (delivered) { + IEC104_InterrogationEvent_t finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == reason && finish.request_id == id); + } + gi_process_only(); gi_empty(); /* 重复Process不再产生第二次FINISHED;独立验证业务时钟,不推进链路期限。 */ + CHECK(app.gi.timeouts == 1 && app.gi.gi_submitted == 0); +} + +static void test_begin_timeout(void) +{ + IEC104_InterrogationConfig_t config; + unsigned mode; + IEC104_Interrogation_Config_Default(&config); + CHECK(config.begin_timeout_ms == 15000 && config.data_timeout_ms == 15000 && config.total_timeout_ms == 0); + config.begin_timeout_ms = 100; config.total_timeout_ms = 1000; + custom_config = &config; + for (mode = 0; mode < 5; ++mode) { + IEC104_InterrogationEvent_t event = {0}; + app_open(); app_request(gi_request, sizeof(gi_request)); gi_process(); + if (mode != 0) { event = gi_take(IEC104_INTERROGATION_BEGIN); } + fake.now += 99; gi_process(); + CHECK(app.gi.timer_active && app.gi.timeout_remaining_ms == 1 && app.gi.timeouts == 0); + CHECK(Example_Wait_Time(&app, 50) == (mode == 0 ? 0U : 1U)); + ++fake.now; + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi) && app.gi.timeout_remaining_ms == 0 && app.gi.timeouts == 0); + CHECK(app.gi.need_process && Example_Wait_Time(&app, 50) == 0); + if (mode == 1 || mode == 4) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, mode == 1) == IEC104_INTERROGATION_REPLY_STALE); + } else if (mode == 2) { + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + } else { CHECK(IEC104_Interrogation_Process(app.interrogation)); } + check_timeout(IEC104_INTERROGATION_FINISH_BEGIN_TIMEOUT, mode != 0, event.request_id, true); + { + const uint8_t *frames[32]; + size_t lengths[32]; + uint8_t expected[sizeof(gi_request)]; + memcpy(expected, gi_request, sizeof(expected)); expected[2] = 0xc7; + app_poll(); + CHECK(collect_asdus(frames, lengths) == 1 && lengths[0] == sizeof(expected)); + CHECK(memcmp(frames[0], expected, sizeof(expected)) == 0); + CHECK(app.gi.negative_submitted == 1 && app.gi.rejected == 1 && app.gi.cancelled == 0); + } + app_close(); + } + custom_config = NULL; +} + +static void test_data_timeout(void) +{ + IEC104_InterrogationConfig_t config; + unsigned mode; + IEC104_Interrogation_Config_Default(&config); + config.begin_timeout_ms = 50; config.data_timeout_ms = 100; config.total_timeout_ms = 1000; + custom_config = &config; + for (mode = 0; mode < 5; ++mode) { + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + app_open(); + if (mode == 4) { /* NEED_DATA已排队但应用从未取出。 */ + event = gi_begin(); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); + } else { event = gi_data(); } + single_batch(&batch, ¶meters, bytes, 1); + fake.now += 99; gi_process(); + CHECK(app.gi.timeout_remaining_ms == 1 && app.gi.timeouts == 0); + ++fake.now; + if (mode == 0) { CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_STALE); } + else if (mode == 1) { CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); } + else if (mode == 2) { CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); } + else { CHECK(IEC104_Interrogation_Process(app.interrogation)); } + check_timeout(IEC104_INTERROGATION_FINISH_DATA_TIMEOUT, 1, event.request_id, false); + app_close(); + } + custom_config = NULL; +} + +/* 新一批重启取数期限,但不会延长整个请求的总期限。 */ +static void test_timeout_progress(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + IEC104_Interrogation_Config_Default(&config); + config.begin_timeout_ms = config.data_timeout_ms = 100; config.total_timeout_ms = 250; + custom_config = &config; app_open(); event = gi_begin(); + fake.now += 99; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + fake.now += 99; single_batch(&batch, ¶meters, bytes, 1); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + fake.now += 49; gi_process(); + CHECK(app.gi.timeouts == 0 && app.gi.timeout_remaining_ms == 3 && app.gi.objects_submitted == 1); + fake.now += 3; + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + check_timeout(IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT, 1, event.request_id, false); + app_close(); custom_config = NULL; +} + +/* BEGIN前等业务FIFO空位不计期限;BEGIN生成后,确认/数据/终止的发送等待均受总期限约束。 */ +static void test_total_timeout_waits(void) +{ + IEC104_InterrogationConfig_t config; + unsigned mode; + IEC104_Interrogation_Config_Default(&config); + config.begin_timeout_ms = config.data_timeout_ms = 0; config.total_timeout_ms = 100; + custom_config = &config; + for (mode = 0; mode < 5; ++mode) { + IEC104_InterrogationEvent_t event = {0}; + uint8_t invalid[sizeof(gi_request)], bytes[249]; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + app_open(); + if (mode == 0) { + memcpy(invalid, gi_request, sizeof(invalid)); invalid[9] = 19; + app_request(invalid, sizeof(invalid)); app_request(gi_request, sizeof(gi_request)); + } else if (mode == 1) { + event = gi_begin(); IEC104_Interrogation_Cancel(app.interrogation); + app_request(gi_request, sizeof(gi_request)); + } else if (mode == 2) { + event = gi_begin(); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } else { + event = gi_data(); + if (mode == 3) { + single_batch(&batch, ¶meters, bytes, 1); + congest(&last_request.connection); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + } else { CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); } + } + if (mode != 1 && mode != 3) { congest(&last_request.connection); } + gi_process(); CHECK(app.gi.waiting); + CHECK(mode == 1 ? !app.gi.timer_active : (app.gi.timer_active && app.gi.timeout_remaining_ms == 100)); + fake.now += 100; CHECK(IEC104_Interrogation_Process(app.interrogation)); + if (mode == 1) { + IEC104_InterrogationEvent_t old = gi_take(IEC104_INTERROGATION_FINISHED); + IEC104_TcpServerStatus_t status; + CHECK(old.reason == IEC104_INTERROGATION_FINISH_CANCELLED && old.request_id == event.request_id); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.pending_requests == 1 && app.gi.timeouts == 0 && app.gi.negative_submitted == 0); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.connected); + gi_empty(); + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(app.gi.timer_active && app.gi.timeout_remaining_ms == 100); + IEC104_Interrogation_Cancel(app.interrogation); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request_id == event.request_id); + } else { + check_timeout(IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT, mode >= 2, event.request_id, mode < 3); + } + app_close(); + } + custom_config = NULL; +} + +static void test_timeout_disabled_and_large(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t event; + IEC104_Interrogation_Config_Default(&config); + config.begin_timeout_ms = config.data_timeout_ms = config.total_timeout_ms = 0; + custom_config = &config; app_open(); event = gi_begin(); + fake.now += 10000; gi_process(); + CHECK(!app.gi.timer_active && app.gi.timeouts == 0 && Example_Wait_Time(&app, 50) == 50); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&event, 0, 0); + app_close(); + config.total_timeout_ms = UINT32_MAX; + app_open(); event = gi_begin(); + fake.now += (uint64_t)UINT32_MAX - 1U; + /* 只推进业务时钟,不运行链路定时器,用于独立验证32位最大配置与64位时钟差。 */ + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi) && app.gi.timeout_remaining_ms == 1); + ++fake.now; + CHECK(IEC104_Interrogation_Process(app.interrogation)); + check_timeout(IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT, 1, event.request_id, true); + app_close(); custom_config = NULL; +} + +/* 到期时先消费已经排队的传输通知,STOP/主站撤销优先,应用应答先得到WAIT再得到STALE。 */ +static void test_timeout_transport_priority(void) +{ + IEC104_InterrogationConfig_t config; + unsigned mode; + IEC104_Interrogation_Config_Default(&config); + config.begin_timeout_ms = config.data_timeout_ms = config.total_timeout_ms = 100; + custom_config = &config; + for (mode = 0; mode < 2; ++mode) { + IEC104_InterrogationEvent_t event, finish; + uint8_t request[sizeof(gi_request)]; + app_open(); event = gi_data(); fake.now += 100; + if (mode == 0) { push(stop_act, sizeof(stop_act)); } + else { + memcpy(request, gi_request, sizeof(request)); request[2] = 0x88; + queue_request(request, sizeof(request)); + } + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Interrogation_Process(app.interrogation)); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_WAIT); + app_poll(); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == (mode == 0 ? IEC104_INTERROGATION_FINISH_STOPPED : IEC104_INTERROGATION_FINISH_DEACTIVATED)); + gi_process(); gi_empty(); CHECK(app.gi.timeouts == 0); + app_close(); + } + custom_config = NULL; +} + +/* 五种确认前拒绝共用原任务槽;真实HIGH和任务池同时满时,旧REPLY保持顺序,恢复后可重新总召。 */ +static void test_preconfirm_failure_reply_order(void) +{ + static const IEC104_InterrogationFinishReason_t reasons[] = { + IEC104_INTERROGATION_FINISH_REJECTED, IEC104_INTERROGATION_FINISH_SOURCE_ERROR, + IEC104_INTERROGATION_FINISH_BEGIN_TIMEOUT, IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT, + IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT + }; + unsigned mode; + for (mode = 0; mode < 10; ++mode) { + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t begin, finish, next; + IEC104_TcpServerStatus_t service; + const uint8_t *frames[32]; + size_t lengths[32]; + uint8_t request[sizeof(gi_request)], expected[sizeof(gi_request)]; + unsigned i, scenario = mode % 5; + IEC104_Interrogation_Config_Default(&config); + config.begin_timeout_ms = scenario == 2 ? 50 : 0; + config.total_timeout_ms = 100; + custom_config = &config; app_open(); + memcpy(request, gi_request, sizeof(request)); request[2] = 6; request[3] = 0x71; request[9] = 36; + if (mode >= 5) { request[4] = request[5] = 0xff; } + app_request(request, sizeof(request)); gi_process(); begin = gi_take(IEC104_INTERROGATION_BEGIN); + congest(&last_request.connection); + if (scenario == 4) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &begin, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process_only(); + CHECK(app.gi.phase == IEC104_INTERROGATION_CONFIRM && app.gi.waiting && app.gi.submitted == 0); + } + for (i = 0; i < 3; ++i) { request_busy((uint8_t)(0x60U + i)); } + CHECK(app.gi.pending_requests == 4 && app.gi.gi_pending && app.gi.negative_submitted == 0); + if (scenario == 0) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &begin, false) == IEC104_INTERROGATION_REPLY_OK); + } else if (scenario == 1) { + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &begin) == IEC104_INTERROGATION_REPLY_OK); + } else { fake.now += scenario == 2 ? 50U : 100U; } + gi_process_only(); + CHECK(app.gi.pending_requests == 4 && !app.gi.gi_pending && app.gi.phase == IEC104_INTERROGATION_REPLY); + CHECK(app.gi.active && app.gi.waiting && !app.gi.timer_active && !app.gi.application_waiting); + CHECK(app.gi.rejected == 4 && app.gi.overloads == 0 && app.gi.dropped_requests == 0 && app.gi.cancelled == 0); + CHECK(app.gi.timeouts == (scenario >= 2 ? 1U : 0U) && app.gi.last_finish_reason == reasons[scenario]); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == reasons[scenario] && finish.instance_id == begin.instance_id && finish.request_id == begin.request_id); + CHECK(finish.request.common_address == 1 && finish.request.qualifier == 36 && finish.request.originator_address == 0x71 && !finish.request.is_test); + CHECK(finish.batch_index == 0 && finish.objects_submitted == 0); + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &begin) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &begin, true) == IEC104_INTERROGATION_REPLY_STALE); + fake.now += 101; gi_process_only(); gi_empty(); + CHECK(app.gi.timeouts == (scenario >= 2 ? 1U : 0U) && app.gi.pending_requests == 4); + fake.send_blocked = 0; gi_process(); app_poll(); + CHECK(collect_asdus(frames, lengths) == 4); + for (i = 0; i < 3; ++i) { + memcpy(expected, gi_request, sizeof(expected)); expected[2] = 0xc7; expected[3] = (uint8_t)(0x60U + i); + CHECK(lengths[i] == sizeof(expected) && memcmp(frames[i], expected, sizeof(expected)) == 0); + } + memcpy(expected, request, sizeof(expected)); expected[2] = 0x47; expected[4] = 1; expected[5] = 0; + CHECK(lengths[3] == sizeof(expected) && memcmp(frames[3], expected, sizeof(expected)) == 0); + CHECK(app.gi.pending_requests == 0 && app.gi.negative_submitted == 4 && app.gi.gi_submitted == 0); + app_request(request, sizeof(request)); gi_process(); next = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(next.request_id != begin.request_id && next.request.generation == begin.request.generation); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &next, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); next = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&next, 0, 0); + CHECK(collect_asdus(frames, lengths) == 6 && frames[4][2] == 7 && frames[5][2] == 10); + CHECK(app.gi.gi_submitted == 1 && app.gi.negative_submitted == 4); + app_close(); custom_config = NULL; + } +} + +/* BEGIN失败和超时不得跳过Slave中未读的STOP、撤销或断线,原终结原因和单次FINISHED保持不变。 */ +static void test_preconfirm_failure_transport_priority(void) +{ + static const IEC104_InterrogationFinishReason_t reasons[] = { + IEC104_INTERROGATION_FINISH_STOPPED, IEC104_INTERROGATION_FINISH_DEACTIVATED, + IEC104_INTERROGATION_FINISH_DISCONNECTED + }; + IEC104_InterrogationConfig_t config; + unsigned mode; + IEC104_Interrogation_Config_Default(&config); + config.begin_timeout_ms = config.total_timeout_ms = 50; + custom_config = &config; + for (mode = 0; mode < 3; ++mode) { + IEC104_InterrogationEvent_t begin, finish; + uint8_t request[sizeof(gi_request)]; + app_open(); begin = gi_begin(); fake.now += 50; + if (mode == 0) { push(stop_act, sizeof(stop_act)); } + else if (mode == 1) { + memcpy(request, gi_request, sizeof(request)); request[2] = 0x88; + queue_request(request, sizeof(request)); + } else { CHECK(IEC104_Slave_Close_Connection(app.slave, &last_request, NULL) == IEC104_TCP_OK); } + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + gi_process_only(); CHECK(app.gi.events_pending && app.gi.timeouts == 0); + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &begin) == IEC104_INTERROGATION_REPLY_WAIT); + app_poll(); + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &begin) == IEC104_INTERROGATION_REPLY_STALE); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == reasons[mode] && finish.request_id == begin.request_id); + gi_process(); gi_empty(); + CHECK(app.gi.timeouts == 0 && app.gi.negative_submitted == 0 && app.gi.rejected == 0); + app_close(); + } + custom_config = NULL; +} + +/* 肯定ACT_CON已被发送层接手但TCP尚未写出时,已经进入确认后阶段,失败不能改发否定ACT_CON。 */ +static void test_confirm_submission_boundary(void) +{ + static const IEC104_InterrogationFinishReason_t reasons[] = { + IEC104_INTERROGATION_FINISH_SOURCE_ERROR, IEC104_INTERROGATION_FINISH_DATA_TIMEOUT, + IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT + }; + unsigned mode; + for (mode = 0; mode < 3; ++mode) { + IEC104_InterrogationConfig_t config; + IEC104_InterrogationEvent_t event, finish; + IEC104_TcpServerStatus_t service; + const uint8_t *frames[32]; + size_t lengths[32]; + IEC104_Interrogation_Config_Default(&config); + config.data_timeout_ms = mode == 1 ? 50 : 100; + config.total_timeout_ms = mode == 2 ? 50 : 200; + custom_config = &config; app_open(); event = gi_begin(); + fake.send_blocked = 1; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(app.gi.phase == IEC104_INTERROGATION_DATA && app.gi.submitted == 1); + CHECK(collect_asdus(frames, lengths) == 0); + if (mode == 0) { + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + } else { fake.now += 50; gi_process_only(); } + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && !service.connected); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == reasons[mode] && finish.request_id == event.request_id); + CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + gi_process_only(); gi_empty(); + CHECK(app.gi.negative_submitted == 0 && app.gi.rejected == 0 && app.gi.pending_requests == 0); + CHECK(app.gi.timeouts == (mode == 0 ? 0U : 1U) && app.gi.gi_submitted == 0); + CHECK(collect_asdus(frames, lengths) == 0); + app_close(); custom_config = NULL; + } +} + +/* 直接检查传给业务层的输入事件:立即应答、排队、撤销和丢弃都不能改写原请求字节。 */ +static void request_unchanged(const uint8_t *request, size_t size) +{ + IEC104_SlaveEvent_t event, copy; + IEC104_TcpEventResult_t result; + unsigned received = 0; + queue_request(request, size); + CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + while ((result = IEC104_Slave_NextEvent(app.slave, &event)) == IEC104_TCP_EVENT_READY) { + CHECK(Example_Handle_Event(&app, &event)); + if (event.transport.type == IEC104_TCP_EVENT_I_FRAME) { + const uint8_t *data; + uint16_t length; + CHECK(IEC60870_Asdu_Get_Data(&event.asdu, &data, &length) == IEC60870_ASDU_OK); + CHECK(length == size && memcmp(data, request, size) == 0); + CHECK(IEC104_Slave_Copy_Event(©, &event) && IEC104_Slave_Copy_Event(&last_request, ©)); + ++received; + } + } + CHECK(result == IEC104_TCP_EVENT_EMPTY && received == 1); +} + +/* BEGIN/NEED_DATA已取出时,错误激活/撤销不取消、刷新期限或使原业务事件失效。 */ +static void test_malformed_preserves_active(void) +{ + unsigned stage, deactivation, shape, broadcast; + for (stage = 0; stage < 2; ++stage) { + for (deactivation = 0; deactivation < 2; ++deactivation) { + for (shape = 0; shape < 4; ++shape) { + for (broadcast = 0; broadcast < 2; ++broadcast) { + IEC104_InterrogationEvent_t event; + IEC104_InterrogationStatus_t before; + IEC104_TcpServerStatus_t service; + uint8_t request[14] = {0}; + const uint8_t *frames[32]; + size_t lengths[32], length = shape == 0 ? 11U : shape == 3 ? 14U : 10U; + app_open(); event = stage == 0 ? gi_begin() : gi_data(); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &before) && before.timer_active); + fake.now += 7U; + memcpy(request, gi_request, sizeof(gi_request)); + request[1] = shape == 1 ? 0x81 : shape == 2 ? 0 : shape == 3 ? 2 : 1; + request[2] = deactivation ? 0x88 : 0x86; + if (broadcast) { request[4] = request[5] = 0xff; } + request_unchanged(request, length); gi_process_only(); gi_empty(); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); + CHECK(app.gi.phase == before.phase && app.gi.pending_requests == 1 && app.gi.gi_pending); + CHECK(app.gi.cancelled == 0 && app.gi.deactivated == 0 && app.gi.rejected == 1); + CHECK(app.gi.timer_active && app.gi.timeout_remaining_ms + 7U == before.timeout_remaining_ms); + CHECK(app.gi.negative_submitted == (shape < 2 ? 1U : 0U)); + if (stage == 0) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + } + gi_done(&event, 0, 0); + CHECK(app.gi.gi_submitted == 1 && collect_asdus(frames, lengths) == (shape < 2 ? 3U : 2U)); + if (shape < 2) { + unsigned index = stage == 0 ? 0U : 1U; + CHECK(lengths[index] == 10 && frames[index][1] == 1); + CHECK(frames[index][2] == (deactivation ? 0xc9 : 0xc7) && frames[index][4] == 1); + } + app_close(); + } + } + } + } +} + +/* 畸形单对象否定沿原HIGH/FIFO背压路径,满槽只丢新请求,原BEGIN仍能完成。 */ +static void test_malformed_backpressure(void) +{ + IEC104_InterrogationEvent_t event; + IEC104_TcpServerStatus_t service; + uint8_t request[11] = {0}; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); event = gi_begin(); congest(&last_request.connection); + memcpy(request, gi_request, sizeof(gi_request)); + for (i = 0; i < 4; ++i) { + request[3] = (uint8_t)(0x60U + i); request[2] = i & 1U ? 0x88 : 0x86; + request_unchanged(request, sizeof(request)); + } + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); + CHECK(app.gi.pending_requests == 4 && app.gi.dropped_requests == 1 && app.gi.overloads == 1); + CHECK(app.gi.rejected == 4 && app.gi.negative_submitted == 0 && app.gi.cancelled == 0); + gi_empty(); fake.send_blocked = 0; + for (i = 0; i < 8; ++i) { gi_process(); } + CHECK(app.gi.pending_requests == 1 && app.gi.negative_submitted == 3 && app.gi.waits != 0); + CHECK(collect_asdus(frames, lengths) == 3); + for (i = 0; i < 3; ++i) { + CHECK(lengths[i] == 10 && frames[i][1] == 1 && frames[i][3] == 0x60U + i); + CHECK(frames[i][2] == (i & 1U ? 0xc9 : 0xc7)); + } + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&event, 0, 0); + CHECK(app.gi.gi_submitted == 1 && app.gi.cancelled == 0); + app_close(); +} + +/* 广播全站及全部16组,非默认本站地址;确认/数据/终止WAIT及批次CA校验。 */ +static void test_broadcast_interrogation(void) +{ + IEC104_InterrogationConfig_t config; + unsigned qoi; + IEC104_Interrogation_Config_Default(&config); config.common_address = 0x1234; + custom_config = &config; + for (qoi = 20; qoi <= 36; ++qoi) { + IEC104_InterrogationEvent_t event, finish; + IEC60870_Asdu_t batch; + IEC60870_AsduHeader_t header = {(uint8_t)qoi, 0, (uint8_t)(qoi & 1U), 0x67, UINT16_MAX}; + IEC60870_SinglePointInformation_t point; + const uint8_t *frames[32]; + size_t lengths[32]; + uint8_t request[sizeof(gi_request)], expected[sizeof(gi_request)], bytes[249]; + bool wait = qoi == 20 || qoi == 36; + app_open(); + memcpy(request, gi_request, sizeof(request)); request[2] = (uint8_t)(6U | ((qoi & 1U) << 7)); + request[3] = 0x67; request[4] = request[5] = 0xff; request[9] = (uint8_t)qoi; + request_unchanged(request, sizeof(request)); gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == 0x1234 && event.request.qualifier == qoi); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + if (wait) { + congest(&last_request.connection); gi_process_only(); + CHECK(app.gi.phase == IEC104_INTERROGATION_CONFIRM && app.gi.waiting); + fake.send_blocked = 0; + } + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(event.request.common_address == 0x1234); + CHECK(IEC60870_Asdu_Create(&batch, IEC104_Slave_Get_Application_Layer_Parameters(app.slave), + bytes, sizeof(bytes), &header, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_SinglePointInformation_Create(&point, 0x10203, 1, 0) == IEC60870_ASDU_OK); + CHECK(IEC60870_Asdu_Add_InformationObject(&batch, &point.object) == IEC60870_ASDU_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_ERROR); + CHECK(IEC60870_Asdu_Set_CA(&batch, 1) == IEC60870_ASDU_OK); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_ERROR); + CHECK(IEC60870_Asdu_Set_CA(&batch, 0x1234) == IEC60870_ASDU_OK); + if (wait) { + congest(&last_request.connection); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + gi_process_only(); + CHECK(app.gi.phase == IEC104_INTERROGATION_DATA && app.gi.waiting && app.gi.batch_index == 0); + fake.send_blocked = 0; gi_process(); + } + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + if (wait) { + congest(&last_request.connection); gi_process_only(); + CHECK(app.gi.phase == IEC104_INTERROGATION_TERMINATE && app.gi.waiting); + fake.send_blocked = 0; + } + gi_process(); finish = gi_take(IEC104_INTERROGATION_FINISHED); app_poll(); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_COMPLETED && finish.request.common_address == 0x1234); + CHECK(finish.batch_index == 1 && finish.objects_submitted == 1 && app.gi.gi_submitted == 1); + CHECK(collect_asdus(frames, lengths) == 3); + memcpy(expected, request, sizeof(expected)); expected[4] = 0x34; expected[5] = 0x12; + expected[2] = (uint8_t)(7U | (request[2] & 0x80U)); + CHECK(lengths[0] == sizeof(expected) && memcmp(frames[0], expected, sizeof(expected)) == 0); + expected[2] = (uint8_t)(10U | (request[2] & 0x80U)); + CHECK(lengths[2] == sizeof(expected) && memcmp(frames[2], expected, sizeof(expected)) == 0); + CHECK(lengths[1] == 10 && frames[1][0] == 1 && frames[1][1] == 1 && frames[1][2] == (qoi | (request[2] & 0x80U))); + CHECK(frames[1][3] == 0x67 && frames[1][4] == 0x34 && frames[1][5] == 0x12); + CHECK(memcmp(frames[1] + 6, (const uint8_t[]){3, 2, 1, 1}, 4) == 0); + gi_empty(); app_close(); + } + custom_config = NULL; +} + +/* 广播错误回复的立即/排队路径都改为本站CA;普通错误站地址保持原值。 */ +static void test_broadcast_error_replies(void) +{ + static const struct { unsigned offset; uint8_t value, cot; } cases[] = { + {9, 19, 7}, {9, 37, 7}, {6, 1, 47}, {2, 0x83, 45}, {2, 0xc6, 7}, + {2, 0x88, 9}, {4, 2, 46}, {4, 0, 46} + }; + IEC104_InterrogationConfig_t config; + unsigned wait, i; + IEC104_Interrogation_Config_Default(&config); config.common_address = 0x1234; + custom_config = &config; + for (wait = 0; wait < 2; ++wait) { + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + uint8_t request[sizeof(gi_request)], expected[sizeof(gi_request)]; + IEC104_TcpServerStatus_t service; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + if (wait) { /* 先取得有效的连接接口,取消尚未交付BEGIN的请求再填满HIGH。 */ + memcpy(request, gi_request, sizeof(request)); request[4] = 0x34; request[5] = 0x12; + app_request(request, sizeof(request)); IEC104_Interrogation_Cancel(app.interrogation); + congest(&last_request.connection); + } + memcpy(request, gi_request, sizeof(request)); request[4] = request[5] = 0xff; + request[cases[i].offset] = cases[i].value; + if (i >= 6) { request[5] = 0; } + request_unchanged(request, sizeof(request)); + if (wait) { gi_process_only(); CHECK(app.gi.pending_requests == 1 && app.gi.waiting); } + fake.send_blocked = 0; gi_process(); app_poll(); + memcpy(expected, request, sizeof(expected)); expected[2] = (uint8_t)(0xc0U | cases[i].cot); + if (i < 6) { expected[4] = 0x34; expected[5] = 0x12; } + CHECK(collect_asdus(frames, lengths) == 1 && lengths[0] == sizeof(expected)); + CHECK(memcmp(frames[0], expected, sizeof(expected)) == 0 && app.gi.negative_submitted == 1); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); + gi_empty(); app_close(); + } + } + custom_config = NULL; +} + +/* 广播仍受单总召与满槽规则约束,忙应答使用本站CA,不能修改当前交互或断链。 */ +static void test_broadcast_busy(void) +{ + unsigned broadcast; + for (broadcast = 0; broadcast < 2; ++broadcast) { + IEC104_InterrogationEvent_t begin; + IEC104_TcpServerStatus_t service; + const uint8_t *frames[32]; + size_t lengths[32]; + uint8_t request[sizeof(gi_request)]; + unsigned i; + app_open(); memcpy(request, gi_request, sizeof(request)); + if (broadcast) { request[4] = request[5] = 0xff; } + request_unchanged(request, sizeof(request)); gi_process(); begin = gi_take(IEC104_INTERROGATION_BEGIN); + request[4] = request[5] = 0xff; request[3] = 0x60; request[9] = 21; + request_unchanged(request, sizeof(request)); + gi_process_only(); /* 多站应答保存遍历任务,再由Process有界提交。 */ + CHECK(app.gi.gi_pending && app.gi.negative_submitted == 1 && app.gi.pending_requests == 1); + congest(&last_request.connection); + for (i = 0; i < 4; ++i) { request[3] = (uint8_t)(0x61U + i); request_unchanged(request, sizeof(request)); } + CHECK(app.gi.gi_pending && app.gi.pending_requests == 4 && app.gi.dropped_requests == 1 && app.gi.overloads == 1); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &begin, false) == IEC104_INTERROGATION_REPLY_OK); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_REJECTED); + fake.send_blocked = 0; app_finish(); gi_empty(); + CHECK(collect_asdus(frames, lengths) == 5); + for (i = 0; i < 5; ++i) { + CHECK(lengths[i] == 10 && frames[i][2] == 0xc7 && frames[i][4] == 1 && frames[i][5] == 0); + CHECK(frames[i][3] == (i < 4 ? 0x60U + i : 0x5aU)); + } + app_close(); + } +} + +/* 广播/定向激活与撤销的四种组合,覆盖BEGIN前、确认后以及满任务池撤销。 */ +static void test_broadcast_deactivation(void) +{ + unsigned phase, combination; + for (phase = 0; phase < 3; ++phase) { + for (combination = 0; combination < 4; ++combination) { + uint8_t activation[sizeof(gi_request)], deactivation[sizeof(gi_request)]; + IEC104_InterrogationEvent_t event = {0}, finish; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned count, i; + app_open(); memcpy(activation, gi_request, sizeof(activation)); activation[9] = 36; + if (combination & 1U) { activation[4] = activation[5] = 0xff; } + request_unchanged(activation, sizeof(activation)); + if (phase != 0) { + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == 1); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + } + if (phase == 2) { + congest(&last_request.connection); + for (i = 0; i < 3; ++i) { request_busy((uint8_t)(0x60U + i)); } + CHECK(app.gi.pending_requests == 4); + } + memcpy(deactivation, activation, sizeof(deactivation)); deactivation[2] = 0x88; + deactivation[4] = combination & 2U ? 0xff : 1; deactivation[5] = combination & 2U ? 0xff : 0; + request_unchanged(deactivation, sizeof(deactivation)); + if (phase != 0) { + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_DEACTIVATED && finish.request_id == event.request_id); + CHECK(finish.request.common_address == 1 && finish.request.qualifier == 36); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + } + gi_empty(); fake.send_blocked = 0; app_finish(); + count = collect_asdus(frames, lengths); + CHECK(count == (phase == 0 ? 1U : phase == 1 ? 2U : 5U)); + CHECK(frames[count - 1U][2] == 0x89 && frames[count - 1U][4] == 1 && frames[count - 1U][5] == 0); + CHECK(frames[count - 1U][9] == 36 && app.gi.deactivated == 1); + deactivation[4] = deactivation[5] = 0xff; + request_unchanged(deactivation, sizeof(deactivation)); app_finish(); + CHECK(collect_asdus(frames, lengths) == count + 1U && frames[count][2] == 0xc9); + CHECK(frames[count][4] == 1 && frames[count][5] == 0); + gi_empty(); app_close(); + } + } +} + +/* 总召绑定必须属于本Slave;配置错误、重复创建和失败销毁均不能破坏站点引用。 */ +static void test_station_binding_lifetime(void) +{ + IEC104_Station_t station = {0}; + IEC104_InterrogationConfig_t config; + IEC104_Slave_t *slave = IEC104_Slave_Create(); + IEC104_Interrogation_t *gi; + IEC104_InterrogationEvent_t event; + CHECK(slave != NULL); + IEC104_Interrogation_Config_Default(&config); + CHECK(config.station == NULL); + config.station = &station; + config.common_address = 0U; /* 绑定站点时不用兼容地址,避免两处配置不同CA。 */ + CHECK(IEC104_Interrogation_Create(slave, &config) == NULL); + CHECK(IEC104_Slave_Add_Station(slave, &station, 0x1234U, &config) == IEC104_STATION_OK); + config.max_objects_per_asdu = 0U; + CHECK(IEC104_Interrogation_Create(slave, &config) == NULL); + CHECK(IEC104_Slave_Remove_Station(slave, &station) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(slave, &station, 0x1234U, &config) == IEC104_STATION_OK); + config.max_objects_per_asdu = 4U; + config.use_station_registry = true; + CHECK(IEC104_Interrogation_Create(slave, &config) == NULL); /* 两种站点选择方式不能同时配置。 */ + config.use_station_registry = false; + gi = IEC104_Interrogation_Create(slave, &config); + CHECK(gi != NULL && IEC104_Interrogation_Create(slave, &config) == NULL); + CHECK(IEC104_Slave_Remove_Station(slave, &station) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&station) && IEC104_Slave_Get_Station(slave, 0x1234U) == NULL); + CHECK(IEC104_Slave_Destroy(slave, NULL) == IEC104_TCP_INVALID_STATE); + CHECK(IEC104_Interrogation_Destroy(gi)); + CHECK(IEC104_Station_Is_Reusable(&station)); +#if IEC104_TCP_SERVER_CAPACITY >= 2U + { + IEC104_Slave_t *other = IEC104_Slave_Create(); + CHECK(other != NULL); + CHECK(IEC104_Slave_Add_Station(other, &station, 0x1234U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Interrogation_Create(slave, &config) == NULL); + CHECK(IEC104_Slave_Remove_Station(other, &station) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Destroy(other, NULL) == IEC104_TCP_OK); + } +#endif + CHECK(IEC104_Slave_Destroy(slave, NULL) == IEC104_TCP_OK); + + app_open(); + event = gi_begin(); + CHECK(!IEC104_Interrogation_Destroy(app.interrogation)); + CHECK(IEC104_Slave_Acquire_Station(app.slave, &app.station) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + IEC104_Interrogation_Cancel(app.interrogation); + CHECK(!IEC104_Interrogation_Destroy(app.interrogation)); /* 未读FINISHED仍禁止销毁总召实例。 */ + CHECK(!IEC104_Station_Is_Reusable(&app.station)); /* 应用持有到清理完成。 */ + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request_id == event.request_id); + CHECK(IEC104_Slave_Release_Station(app.slave, &app.station) == IEC104_STATION_OK); + app_close(); /* 退出必须能归还唯一引用并成功删除节点,避免重复创建/销毁泄漏。 */ +} + +/* 定向和广播回复均使用绑定站点的实际CA;注册其他站点不会自动展开新的总召。 */ +static void test_station_binding_wire(void) +{ + IEC104_InterrogationConfig_t config; + unsigned broadcast; + IEC104_Interrogation_Config_Default(&config); + config.common_address = 0x1234U; + config.station = &app.station; + config.max_objects_per_asdu = 4U; + custom_config = &config; + for (broadcast = 0U; broadcast < 2U; ++broadcast) { + IEC104_Station_t other = {0}; + uint8_t request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &other, 7U, &other) == IEC104_STATION_OK); + memcpy(request, gi_request, sizeof(request)); + request[4] = broadcast ? 0xffU : 0x34U; + request[5] = broadcast ? 0xffU : 0x12U; + app_request(request, sizeof(request)); + app_finish(); + CHECK(collect_asdus(frames, lengths) == 8U); + CHECK(frames[0][2] == 0x87U && frames[7][2] == 0x8aU); + for (i = 0U; i < 8U; ++i) { CHECK(frames[i][4] == 0x34U && frames[i][5] == 0x12U); } + request[4] = 7U; + request[5] = 0U; + app_request(request, sizeof(request)); + app_finish(); + CHECK(collect_asdus(frames, lengths) == 9U); + CHECK(frames[8][2] == 0xeeU && frames[8][4] == 7U && frames[8][5] == 0U); + CHECK(IEC104_Slave_Remove_Station(app.slave, &other) == IEC104_STATION_OK); + app_close(); + } + custom_config = NULL; +} + +static void registry_request(uint16_t ca, uint8_t cot) +{ + uint8_t request[sizeof(gi_request)]; + memcpy(request, gi_request, sizeof(request)); + request[4] = (uint8_t)ca; + request[5] = (uint8_t)(ca >> 8); + request[2] = (uint8_t)(cot | 0x80U); + app_request(request, sizeof(request)); +} + +/* 同一IOA属于不同CA时按站点context取数;提交数据必须匹配当前站,而不是默认CA。 */ +static void test_registry_routing(void) +{ + IEC104_Station_t second = {0}; + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + int station_value = 0; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 0x1234U, &station_value) == IEC104_STATION_OK); + registry_request(0x1234U, 6U); gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == 0x1234U && event.request.station_id == IEC104_Station_Get_Id(&second)); + CHECK(IEC104_Station_Get_Context(IEC104_Slave_Get_Station_By_Id(app.slave, event.request.station_id)) == &station_value); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + single_batch(&batch, ¶meters, bytes, 1U); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_ERROR); + CHECK(IEC60870_Asdu_Set_CA(&batch, 0x1234U) == IEC60870_ASDU_OK); + bytes[9] = (uint8_t)station_value; + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); gi_done(&event, 1U, 1U); + CHECK(collect_asdus(frames, lengths) == 3U); + CHECK(frames[0][2] == 0x87U && frames[1][2] == 0x94U && frames[2][2] == 0x8aU); + CHECK(frames[1][4] == 0x34U && frames[1][5] == 0x12U && frames[1][9] == 0U); + CHECK(frames[1][6] == 0xe9U && frames[1][7] == 3U); /* IOA仍由应用指定。 */ + registry_request(99U, 6U); app_finish(); + CHECK(collect_asdus(frames, lengths) == 4U && frames[3][2] == 0xeeU && frames[3][4] == 99U); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + app_close(); +} + +/* 13站超过任务池容量;每站空数据也有独立确认/终止、事件身份和计时,不丢站不扩队列。 */ +static void test_registry_broadcast_many(void) +{ + IEC104_Station_t stations[12] = {{0}}; + IEC104_InterrogationEvent_t event, finish; + uint64_t ids[13]; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + for (i = 0U; i < 12U; ++i) { + CHECK(IEC104_Slave_Add_Station(app.slave, &stations[i], (uint16_t)(i + 2U), &stations[i]) == IEC104_STATION_OK); + } + registry_request(IEC104_CA_BROADCAST, 6U); + for (i = 0U; i < 13U; ++i) { + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == i + 1U && (i == 0U || event.request_id > ids[i - 1U])); + CHECK(event.batch_index == 0U && event.objects_submitted == 0U && app.gi.pending_requests == 1U); + CHECK(event.request.originator_address == 0x5aU && event.request.is_test && event.request.qualifier == 20U); + ids[i] = event.request_id; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } + for (i = 0U; i < 13U; ++i) { + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(event.request_id == ids[i] && event.request.common_address == i + 1U); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + } + gi_process(); gi_process(); /* 终止有步数预算,但不需要先消费FINISHED才继续发送。 */ + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(!app.gi.gi_pending && app.gi.phase == IEC104_INTERROGATION_FINISHING && app.gi.pending_events == 13U); + CHECK(!app.gi.need_process && !IEC104_Interrogation_Destroy(app.interrogation)); + for (i = 0U; i < 13U; ++i) { + finish = gi_take(IEC104_INTERROGATION_FINISHED); /* 无需Process也能读完超过FIFO容量的终结记录。 */ + CHECK(finish.request_id == ids[i] && finish.reason == IEC104_INTERROGATION_FINISH_COMPLETED); + CHECK(finish.batch_index == 0U && finish.objects_submitted == 0U); + } + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + app_poll(); + CHECK(collect_asdus(frames, lengths) == 26U && app.gi.gi_submitted == 13U); + CHECK(!app.gi.gi_pending && app.gi.pending_requests == 0U); + for (i = 0U; i < 26U; ++i) { + CHECK(lengths[i] == 10U && frames[i][4] == i % 13U + 1U && frames[i][5] == 0U); + CHECK(frames[i][2] == (i < 13U ? 0x87U : 0x8aU)); + } + for (i = 0U; i < 12U; ++i) { CHECK(IEC104_Slave_Remove_Station(app.slave, &stations[i]) == IEC104_STATION_OK); } + app_close(); +} + +/* 删除当前站并以同CA注册新context,旧总召仍读原站;示例仅在取数阶段额外持有引用。 */ +static void test_registry_delete_current(void) +{ + IEC104_Station_t replacement = {0}; + uint64_t old_id; + app_open(); old_id = IEC104_Station_Get_Id(&app.station); + registry_request(1U, 6U); + CHECK(Example_Step(&app)); + CHECK(app.gi_station == NULL); /* BEGIN只判断接受,不占用取数游标。 */ + CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&app.station)); + CHECK(IEC104_Slave_Get_Station(app.slave, 1U) == NULL); + CHECK(IEC104_Slave_Add_Station(app.slave, &replacement, 1U, &replacement) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Get_Station_By_Id(app.slave, old_id) == &app.station); + CHECK(IEC104_Station_Get_Id(&replacement) != old_id); + app_finish(); check_gi(0U, 0x5aU); + CHECK(IEC104_Station_Is_Reusable(&app.station) && app.gi_station == NULL); + CHECK(IEC104_Slave_Get_Station_By_Id(app.slave, old_id) == NULL); + CHECK(IEC104_Slave_Remove_Station(app.slave, &replacement) == IEC104_STATION_OK); + registry_request(1U, 6U); app_finish(); /* 删除后新请求必须收到未知CA。 */ + CHECK(app.gi.negative_submitted == 1U); + app_close(); +} + +/* 当前站完成,尚未选入BEGIN的已删除站跳过,替代/新增站下一轮才参加。 */ +static void test_registry_broadcast_mutation(void) +{ + IEC104_Station_t second = {0}, third = {0}, replacement = {0}, added = {0}; + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, NULL) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &replacement, 2U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &added, 4U, NULL) == IEC104_STATION_OK); + for (i = 0U; i < 2U; ++i) { + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == (i == 0U ? 1U : 3U)); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } + for (i = 0U; i < 2U; ++i) { + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(event.request.common_address == (i == 0U ? 1U : 3U)); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + } + gi_process(); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request.common_address == 1U); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request.common_address == 3U); + app_poll(); CHECK(collect_asdus(frames, lengths) == 4U); + CHECK(frames[0][4] == 1U && frames[1][4] == 3U && frames[2][4] == 1U && frames[3][4] == 3U); + CHECK(frames[0][2] == 0x87U && frames[1][2] == 0x87U && frames[2][2] == 0x8aU && frames[3][2] == 0x8aU); + CHECK(IEC104_Station_Is_Reusable(&app.station) && IEC104_Station_Is_Reusable(&second)); + CHECK(IEC104_Slave_Remove_Station(app.slave, &replacement) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &added) == IEC104_STATION_OK); + app_close(); +} + +/* 广播忙/错误应答按各站实际CA展开;轮次有界且余下工作可见,不越过更早应答。 */ +static void test_registry_reply_fanout(void) +{ + IEC104_Station_t stations[12] = {{0}}; + IEC104_InterrogationEvent_t begin; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + for (i = 0U; i < 12U; ++i) { + CHECK(IEC104_Slave_Add_Station(app.slave, &stations[i], (uint16_t)(i + 2U), NULL) == IEC104_STATION_OK); + } + begin = gi_begin(); + registry_request(IEC104_CA_BROADCAST, 6U); + gi_process_only(); + CHECK(app.gi.negative_submitted == 8U && app.gi.need_process && app.gi.gi_pending); + gi_process(); app_poll(); + CHECK(collect_asdus(frames, lengths) == 13U && app.gi.pending_requests == 1U); + for (i = 0U; i < 13U; ++i) { CHECK(frames[i][2] == 0xc7U && frames[i][4] == i + 1U); } + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &begin, false) == IEC104_INTERROGATION_REPLY_OK); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request_id == begin.request_id); + app_finish(); + for (i = 0U; i < 12U; ++i) { CHECK(IEC104_Slave_Remove_Station(app.slave, &stations[i]) == IEC104_STATION_OK); } + CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); app_finish(); + CHECK(collect_asdus(frames, lengths) == 15U); + CHECK(frames[14][2] == 0xeeU && frames[14][4] == 0xffU && frames[14][5] == 0xffU); + app_close(); +} + +/* 定向撤销已确认但尚未取数的站;广播撤销剩余站,各自有肯定COT9且不发送ACT_TERM。 */ +static void test_registry_deactivation(void) +{ + IEC104_Station_t second = {0}, third = {0}; + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, NULL) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); + for (i = 0U; i < 3U; ++i) { + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == i + 1U); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&second)); + registry_request(2U, 8U); /* 删除后仍能撤销原活动事务;CA1的NEED_DATA继续有效。 */ + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request.common_address == 2U); + gi_process(); + registry_request(IEC104_CA_BROADCAST, 8U); + for (i = 0U; i < 2U; ++i) { + IEC104_InterrogationEvent_t finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.reason == IEC104_INTERROGATION_FINISH_DEACTIVATED); + CHECK(finish.request.common_address == (i == 0U ? 1U : 3U)); + } + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + app_finish(); + CHECK(collect_asdus(frames, lengths) == 6U && app.gi.deactivated == 2U && app.gi.gi_submitted == 0U); + for (i = 0U; i < 3U; ++i) { CHECK(frames[i][2] == 0x87U && frames[i][4] == i + 1U); } + for (i = 3U; i < 6U; ++i) { CHECK(frames[i][2] == 0x89U); } + CHECK(frames[3][4] == 2U && frames[4][4] == 1U && frames[5][4] == 3U); + CHECK(IEC104_Station_Is_Reusable(&second)); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + app_close(); +} + +/* 首站BEGIN拒绝/超时后继续下一站;STOP取消整轮,快照/节点引用均归还。 */ +static void test_registry_reject_timeout_stop(void) +{ + unsigned mode; + for (mode = 0U; mode < 4U; ++mode) { + IEC104_Station_t second = {0}; + IEC104_InterrogationEvent_t event; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + if (mode == 0U) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, false) == IEC104_INTERROGATION_REPLY_OK); + registry_request(1U, 8U); /* 已拒绝的当前站无活动事务,不能改成肯定撤销并丢失否定确认。 */ + CHECK(app.gi.deactivated == 0U); + } else if (mode == 1U) { fake.now += 15000U; } + else if (mode == 2U) { push(stop_act, sizeof(stop_act)); app_poll(); } + else { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == 2U); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + fake.now += 15000U; + } + if (mode == 3U) { gi_process_only(); } /* 单独触发业务期限,避免同点到期的链路t1先收尾。 */ + else { gi_process(); } + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == (mode == 0U ? IEC104_INTERROGATION_FINISH_REJECTED : + mode == 1U ? IEC104_INTERROGATION_FINISH_BEGIN_TIMEOUT : + mode == 2U ? IEC104_INTERROGATION_FINISH_STOPPED : IEC104_INTERROGATION_FINISH_DATA_TIMEOUT)); + if (mode == 3U) { CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_DATA_TIMEOUT); } + if (mode < 2U) { + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == 2U); + } + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + IEC104_Interrogation_Cancel(app.interrogation); + if (mode < 2U) { CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request_id == event.request_id); } + CHECK(IEC104_Station_Is_Reusable(&app.station) && IEC104_Station_Is_Reusable(&second)); + app_close(); + } +} + +/* 在广播的确认/数据/终止遇到发送WAIT,不能提前换CA;恢复后仍逐站有序完成。 */ +static void test_registry_wait_each_phase(void) +{ + unsigned phase; + for (phase = 0U; phase < 3U; ++phase) { + IEC104_Station_t second = {0}; + IEC104_InterrogationEvent_t event; + IEC60870_Asdu_t batch; + IEC60870_AppLayerParameters_t parameters; + uint8_t bytes[249]; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, &app) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + if (phase == 0U) { + congest(&last_request.connection); gi_process_only(); + CHECK(app.gi.waiting && !app.gi.need_process && app.gi.phase == IEC104_INTERROGATION_CONFIRM); + fake.send_blocked = 0; + } + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == 2U); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + if (phase == 1U) { + single_batch(&batch, ¶meters, bytes, 1U); congest(&last_request.connection); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_WAIT); + gi_process_only(); CHECK(app.gi.data_send_waiting && app.gi.pending_requests == 1U); + fake.send_blocked = 0; gi_process(); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &batch) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + } + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(event.request.common_address == 2U); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + if (phase == 2U) { + congest(&last_request.connection); gi_process_only(); + CHECK(app.gi.waiting && app.gi.phase == IEC104_INTERROGATION_TERMINATE && app.gi.gi_submitted == 0U); + fake.send_blocked = 0; + } + gi_process(); CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request.common_address == 1U); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request.common_address == 2U); + app_finish(); CHECK(app.gi.gi_submitted == 2U); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + app_close(); + } +} + +static void registry_accept(unsigned count) +{ + unsigned i; + registry_request(IEC104_CA_BROADCAST, 6U); + for (i = 0U; i < count; ++i) { + IEC104_InterrogationEvent_t event; + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == i + 1U); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } + gi_process(); +} + +/* 广播畸形撤销只能逐站否定,不能改动已确认各站的活动总召/终结记录。 */ +static void test_registry_malformed(void) +{ + IEC104_Station_t second = {0}; + IEC104_InterrogationEvent_t first, next, finish; + IEC104_TcpServerStatus_t service; + uint8_t request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, &app) == IEC104_STATION_OK); + registry_accept(2); first = gi_take(IEC104_INTERROGATION_NEED_DATA); + memcpy(request, gi_request, sizeof(request)); request[1] = 0x81; request[2] = 0x88; + request[4] = request[5] = 0xff; + request_unchanged(request, sizeof(request)); gi_process_only(); gi_empty(); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); + CHECK(app.gi.pending_requests == 1 && app.gi.gi_pending && app.gi.negative_submitted == 2); + CHECK(app.gi.cancelled == 0 && app.gi.deactivated == 0 && app.gi.rejected == 1); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &first) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); next = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(next.request.common_address == 2 && next.request_id != first.request_id); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &next) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == first.request_id && finish.reason == IEC104_INTERROGATION_FINISH_COMPLETED); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == next.request_id && finish.reason == IEC104_INTERROGATION_FINISH_COMPLETED); + gi_empty(); app_poll(); + CHECK(collect_asdus(frames, lengths) == 6 && app.gi.gi_submitted == 2); + for (i = 0; i < 6; ++i) { + CHECK(lengths[i] == 10 && frames[i][1] == 1 && frames[i][4] == i % 2U + 1U); + CHECK(frames[i][2] == (i < 2 ? 0x87 : i < 4 ? 0xc9 : 0x8a)); + } + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + app_close(); +} + +/* 示例只需一套取数游标;线上三段顺序包含非空、多批、混合类型数据。 */ +static void test_registry_example_order(void) +{ + IEC104_Station_t second = {0}, third = {0}; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, &app) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, &app) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); app_finish(); + CHECK(collect_asdus(frames, lengths) == 24U && app.gi.gi_submitted == 3U); + for (i = 0U; i < 24U; ++i) { + unsigned ca = i < 3U ? i + 1U : i < 21U ? (i - 3U) / 6U + 1U : i - 20U; + CHECK(frames[i][4] == ca && frames[i][5] == 0U); + CHECK(frames[i][2] == (i < 3U ? 0x87U : i < 21U ? 0x94U : 0x8aU)); + if (i >= 3U && i < 21U) { + unsigned batch = (i - 3U) % 6U; + CHECK((frames[i][1] & 0x7fU) == 4U); + CHECK(frames[i][0] == (batch < 2U ? 1U : batch < 4U ? 3U : 11U)); + } + } + CHECK(app.gi_station == NULL && app.session_request == 0U); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + app_close(); +} + +/* 广播容量不足必须在任何肯定确认之前整体拒绝;单站和定向总召不需要状态区。 */ +static void test_registry_capacity(void) +{ + size_t capacity; + for (capacity = 0U; capacity < 3U; ++capacity) { + IEC104_InterrogationConfig_t config; + IEC104_Station_t second = {0}, third = {0}; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + IEC104_Interrogation_Config_Default(&config); + config.use_station_registry = true; + config.broadcast_states = capacity != 0U ? broadcast_states : NULL; + config.broadcast_capacity = capacity; + custom_config = &config; app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, &app) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, &app) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); app_finish(); gi_empty(); + CHECK(collect_asdus(frames, lengths) == 3U && app.gi.gi_submitted == 0U && app.gi.overloads == 1U); + for (i = 0U; i < 3U; ++i) { CHECK(frames[i][2] == 0xc7U && frames[i][4] == i + 1U); } + registry_request(2U, 6U); app_finish(); CHECK(app.gi.gi_submitted == 1U); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); app_finish(); CHECK(app.gi.gi_submitted == 2U); + app_close(); custom_config = NULL; + } +} + +static void test_registry_workspace_contract(void) +{ + IEC104_InterrogationConfig_t config; + app_open(); + CHECK(IEC104_Interrogation_Destroy(app.interrogation)); + IEC104_Interrogation_Config_Default(&config); + CHECK(config.broadcast_states == NULL && config.broadcast_capacity == 0U); + config.broadcast_capacity = 1U; + CHECK(IEC104_Interrogation_Create(app.slave, &config) == NULL); + config.broadcast_states = broadcast_states; config.broadcast_capacity = 0U; + CHECK(IEC104_Interrogation_Create(app.slave, &config) == NULL); + config.broadcast_capacity = SIZE_MAX / sizeof(*broadcast_states) + 1U; + CHECK(IEC104_Interrogation_Create(app.slave, &config) == NULL); + config.broadcast_capacity = 8U; + app.interrogation = IEC104_Interrogation_Create(app.slave, &config); + CHECK(app.interrogation != NULL); +#if IEC104_TCP_SERVER_CAPACITY >= 2 + { + IEC104_Slave_t *other = IEC104_Slave_Create(); + IEC104_Interrogation_t *other_gi; + CHECK(other != NULL); + CHECK(IEC104_Interrogation_Create(other, &config) == NULL); /* 完全重叠。 */ + config.broadcast_states = broadcast_states + 4U; + CHECK(IEC104_Interrogation_Create(other, &config) == NULL); /* 部分重叠。 */ + config.broadcast_states = broadcast_states + 8U; + other_gi = IEC104_Interrogation_Create(other, &config); + CHECK(other_gi != NULL && IEC104_Interrogation_Destroy(other_gi)); + CHECK(IEC104_Slave_Destroy(other, NULL) == IEC104_TCP_OK); + } +#endif + app_close(); +} + +/* 已确认的未来站退役后,仍用原注册身份取值并发送终止,不转向同CA的新节点。 */ +static void test_registry_confirmed_deleted(void) +{ + IEC104_Station_t second = {0}, third = {0}, replacement = {0}; + IEC104_InterrogationEvent_t event; + uint64_t original; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, &app) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, &app) == IEC104_STATION_OK); + original = IEC104_Station_Get_Id(&second); + registry_accept(3U); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &replacement, 2U, &replacement) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&second)); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(event.request.common_address == 2U && event.request.station_id == original); + app.business_event = event; app.business_pending = true; + app_finish(); + CHECK(app.gi.gi_submitted == 3U && app.gi.cancelled == 0U && IEC104_Station_Is_Reusable(&second)); + CHECK(IEC104_Slave_Get_Station_By_Id(app.slave, original) == NULL); + CHECK(IEC104_Slave_Remove_Station(app.slave, &replacement) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + app_close(); +} + +/* 超过FIFO容量的BEGIN都必须有且仅有一次FINISHED;取消后无需Process即可逐个清理。 */ +static void test_registry_finish_delivery(void) +{ + static const IEC104_InterrogationFinishReason_t reasons[] = { + IEC104_INTERROGATION_FINISH_CANCELLED, IEC104_INTERROGATION_FINISH_STOPPED, + IEC104_INTERROGATION_FINISH_DISCONNECTED, IEC104_INTERROGATION_FINISH_SOURCE_ERROR + }; + unsigned mode; + for (mode = 0U; mode < 4U; ++mode) { + IEC104_Station_t stations[12] = {{0}}; + IEC104_InterrogationEvent_t event, finish; + uint64_t ids[13]; + unsigned i; + app_open(); + for (i = 0U; i < 12U; ++i) { + CHECK(IEC104_Slave_Add_Station(app.slave, &stations[i], (uint16_t)(i + 2U), &app) == IEC104_STATION_OK); + } + registry_request(IEC104_CA_BROADCAST, 6U); + for (i = 0U; i < 13U; ++i) { + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); ids[i] = event.request_id; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + } + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + if (mode == 0U) { IEC104_Interrogation_Cancel(app.interrogation); } + if (mode == 1U) { push(stop_act, sizeof(stop_act)); app_poll(); } + if (mode == 2U) { + CHECK(IEC104_Slave_Close_Connection(app.slave, &last_request, NULL) == IEC104_TCP_OK); + app_poll(); + } + if (mode == 3U) { CHECK(IEC104_Interrogation_Fail_Request(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); } + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_STALE); + IEC104_Interrogation_Cancel(app.interrogation); /* 不覆盖已有原因。 */ + if (mode == 0U) { + registry_request(1U, 6U); /* 状态区尚未清理,新总召忙拒绝,不能覆盖旧FINISHED。 */ + CHECK(app.gi.negative_submitted == 1U); + } + CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + for (i = 0U; i < 12U; ++i) { + CHECK(IEC104_Slave_Remove_Station(app.slave, &stations[i]) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&stations[i])); + } + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.pending_events == 13U && !app.gi.gi_pending && !app.gi.need_process); + CHECK(!IEC104_Interrogation_Destroy(app.interrogation)); + for (i = 0U; i < 13U; ++i) { + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == ids[i] && finish.reason == reasons[mode]); + } + gi_empty(); + CHECK(IEC104_Station_Is_Reusable(&app.station)); + for (i = 0U; i < 12U; ++i) { CHECK(IEC104_Station_Is_Reusable(&stations[i])); } + app_close(); + } +} + +/* 当前取数站、未来已确认站、已取完数据等终止站,都支持按CA撤销而不损害其他站。 */ +static void test_registry_directed_deactivation(void) +{ + unsigned mode; + for (mode = 0U; mode < 3U; ++mode) { + IEC104_Station_t second = {0}, third = {0}; + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned count, i; + uint16_t target = mode == 1U ? 2U : 1U; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, &app) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, &app) == IEC104_STATION_OK); + registry_accept(3U); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + if (mode == 2U) { + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + } + registry_request(target, 8U); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request.common_address == target); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == + (mode == 0U ? IEC104_INTERROGATION_REPLY_STALE : IEC104_INTERROGATION_REPLY_OK)); + app_finish(); CHECK(app.gi.gi_submitted == 2U && app.gi.deactivated == 1U); + count = collect_asdus(frames, lengths); + for (i = 0U; i < count; ++i) { + CHECK(frames[i][2] != 0x8aU || frames[i][4] != target); + } + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + app_close(); + } +} + +/* 满槽广播撤销复用原任务,按先前应答顺序提交每站COT9;不额外占用事件预留或任务槽。 */ +static void test_registry_deactivation_full(void) +{ + enum { task_capacity = 4 }; /* 本测试构建采用模块默认任务容量。 */ + IEC104_Station_t second = {0}, third = {0}; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned i; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, NULL) == IEC104_STATION_OK); + registry_accept(3U); (void)gi_take(IEC104_INTERROGATION_NEED_DATA); + congest(&last_request.connection); + for (i = 0U; i < task_capacity - 1U; ++i) { registry_request(1U, 6U); } + CHECK(app.gi.pending_requests == task_capacity); + registry_request(IEC104_CA_BROADCAST, 8U); + CHECK(app.gi.deactivated == 1U && app.gi.pending_requests == task_capacity); + registry_request(IEC104_CA_BROADCAST, 8U); /* 不覆盖首条撤销;此时也没有错误应答空槽。 */ + CHECK(app.gi.deactivated == 1U && app.gi.dropped_requests == 1U); + for (i = 0U; i < 3U; ++i) { CHECK(gi_take(IEC104_INTERROGATION_FINISHED).request.common_address == i + 1U); } + gi_process_only(); CHECK(app.gi.waiting && !app.gi.need_process); + fake.send_blocked = 0; app_finish(); + CHECK(collect_asdus(frames, lengths) == 6U + task_capacity - 1U); + for (i = 3U; i < 3U + task_capacity - 1U; ++i) { CHECK(frames[i][2] == 0xc7U); } + for (; i < 6U + task_capacity - 1U; ++i) { CHECK(frames[i][2] == 0x89U); } + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + app_close(); +} + +/* 另一个站的撤销等待发送时,当前BEGIN被拒绝的ACT_CON不能被撤销应答覆盖。 */ +static void test_registry_reject_during_deactivation(void) +{ + unsigned mode; + for (mode = 0U; mode < 2U; ++mode) { + IEC104_Station_t second = {0}, third = {0}; + IEC104_InterrogationEvent_t event; + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, &app) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, &app) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + if (mode == 0U) { registry_request(1U, 8U); } + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, false) == IEC104_INTERROGATION_REPLY_OK); + if (mode == 1U) { registry_request(IEC104_CA_BROADCAST, 8U); } + (void)gi_take(IEC104_INTERROGATION_FINISHED); (void)gi_take(IEC104_INTERROGATION_FINISHED); + app_finish(); + CHECK(collect_asdus(frames, lengths) == (mode == 0U ? 11U : 3U)); + CHECK(frames[0][2] == 0x87U && frames[0][4] == 1U); + CHECK(frames[1][2] == 0xc7U && frames[1][4] == 2U); + CHECK(frames[2][2] == 0x89U && frames[2][4] == 1U); + CHECK(app.gi.gi_submitted == (mode == 0U ? 1U : 0U)); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + app_close(); + } +} + +/* 显式启用总期限时,已确认但不在当前取数位置的站仍受期限约束,不能遗漏。 */ +static void test_registry_total_deadline(void) +{ + unsigned mode; + for (mode = 0U; mode < 2U; ++mode) { + IEC104_InterrogationConfig_t config; + IEC104_Station_t second = {0}; + IEC104_InterrogationEvent_t event; + IEC104_Interrogation_Config_Default(&config); + config.use_station_registry = true; + config.broadcast_states = broadcast_states; config.broadcast_capacity = 16U; + config.total_timeout_ms = 100U; + custom_config = &config; app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + fake.now += 50U; gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + if (mode == 1U) { + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &event) == IEC104_INTERROGATION_REPLY_OK); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + } + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.timer_active && app.gi.timeout_remaining_ms == 50U); + fake.now += 50U; gi_process_only(); + CHECK(!app.gi.active && app.gi.timeouts == 1U && app.gi.gi_submitted == 0U); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT); + gi_empty(); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + app_close(); custom_config = NULL; + } +} + +/* 当前站BEGIN超时不能误关尚未到总期限的已确认站;同时到期时总期限优先。 */ +static void test_registry_timeout_priority(void) +{ + unsigned mode; + for (mode = 0U; mode < 3U; ++mode) { + IEC104_InterrogationConfig_t config; + IEC104_Station_t second = {0}; + IEC104_InterrogationEvent_t event, finish; + IEC104_TcpServerStatus_t service; + uint64_t first_request_id; + IEC104_Interrogation_Config_Default(&config); + config.use_station_registry = true; + config.broadcast_states = broadcast_states; config.broadcast_capacity = 16U; + config.total_timeout_ms = mode == 0U ? 0U : 100U; + config.begin_timeout_ms = mode == 2U ? 50U : 25U; + custom_config = &config; app_open(); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + registry_request(IEC104_CA_BROADCAST, 6U); gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + first_request_id = event.request_id; + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_OK); + fake.now += 50U; gi_process(); event = gi_take(IEC104_INTERROGATION_BEGIN); + CHECK(event.request.common_address == 2U); + fake.now += config.begin_timeout_ms - 1U; gi_process_only(); + CHECK(app.gi.active && app.gi.timeouts == 0U && app.gi.timer_active && app.gi.timeout_remaining_ms == 1U); + ++fake.now; + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + CHECK(app.gi.active && app.gi.timeouts == 0U && app.gi.timer_active && app.gi.timeout_remaining_ms == 0U); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); + CHECK(IEC104_Interrogation_Reply_Begin(app.interrogation, &event, true) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi) && app.gi.timeouts == 1U); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK); + if (mode == 2U) { + CHECK(!app.gi.active && !service.connected && app.gi.gi_submitted == 0U); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT); + } else { + CHECK(app.gi.active && service.connected); + finish = gi_take(IEC104_INTERROGATION_FINISHED); + CHECK(finish.request_id == event.request_id && finish.reason == IEC104_INTERROGATION_FINISH_BEGIN_TIMEOUT); + gi_process(); event = gi_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(event.request_id == first_request_id && event.request.common_address == 1U); + CHECK(app.gi.timer_active && app.gi.timeout_remaining_ms == (mode == 0U ? config.data_timeout_ms : 25U)); + gi_done(&event, 0U, 0U); + } + gi_empty(); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + app_close(); custom_config = NULL; + } +} + +/* 计数量复用可控传输夹具,但使用独立的引擎实例和广播状态区。 */ +static const uint8_t ci_request[] = {101, 1, 6, 0x5a, 1, 0, 0, 0, 0, 5}; + +static void ci_open(void) { counter_enabled = true; app_open(); } +static void ci_close(void) { app_close(); counter_enabled = false; counter_custom_config = NULL; } + +/* 先推进并读空Slave通知,再推进计数量;测试自行取出业务事件并决定应答时机。 */ +static void ci_process(void) +{ + app_poll(); + CHECK(IEC104_Interrogation_Process(app.counter)); + CHECK(IEC104_Interrogation_Get_Status(app.counter, &app.ci)); +} + +static IEC104_InterrogationEvent_t ci_take(IEC104_InterrogationEventType_t type) +{ + IEC104_InterrogationEvent_t event; + CHECK(IEC104_Interrogation_NextEvent(app.counter, &event) == IEC104_INTERROGATION_NEXT_READY); + CHECK(event.type == type); + return event; +} + +static void ci_empty(void) +{ + IEC104_InterrogationEvent_t event; + CHECK(IEC104_Interrogation_NextEvent(app.counter, &event) == IEC104_INTERROGATION_NEXT_EMPTY); +} + +static void ci_send(uint8_t qcc, uint16_t ca) +{ + uint8_t request[10]; + memcpy(request, ci_request, sizeof(request)); request[9] = qcc; + request[4] = (uint8_t)ca; request[5] = (uint8_t)(ca >> 8); + request_unchanged(request, sizeof(request)); +} + +static IEC104_InterrogationEvent_t ci_begin(uint8_t qcc) +{ + ci_send(qcc, 1); ci_process(); return ci_take(IEC104_INTERROGATION_BEGIN); +} + +/* 接受读取请求并取得首个NEED_DATA;冻结/复位没有数据阶段,不使用此辅助函数。 */ +static IEC104_InterrogationEvent_t ci_data(uint8_t qcc) +{ + IEC104_InterrogationEvent_t event = ci_begin(qcc); + CHECK(IEC104_Interrogation_Reply_Begin(app.counter, &event, true) == IEC104_INTERROGATION_REPLY_OK); + ci_process(); return ci_take(IEC104_INTERROGATION_NEED_DATA); +} + +static void ci_done(const IEC104_InterrogationEvent_t *event) +{ + CHECK(IEC104_Interrogation_Complete_Data(app.counter, event) == IEC104_INTERROGATION_REPLY_OK); + ci_process(); CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_COMPLETED); + ci_empty(); app_poll(); +} + +/* 独立拼接数据字节,覆盖SQ寻址和CP56时标,避免用待测对象编码器生成预期布局。 */ +static void ci_batch(IEC60870_Asdu_t *asdu, uint8_t bytes[249], const IEC104_InterrogationEvent_t *event, + uint8_t type, unsigned count, bool sq) +{ + unsigned i, width = type == 1 ? 1U : type == 15 ? 5U : type == 16 ? 8U : 12U; + size_t offset = 6U; + memset(bytes, 0, 249); + bytes[0] = type; bytes[1] = (uint8_t)(count | (sq ? 0x80U : 0U)); + bytes[2] = (uint8_t)(event->request.data_cause | (event->request.is_test ? 0x80U : 0U)); + bytes[3] = event->request.originator_address; + bytes[4] = (uint8_t)event->request.common_address; bytes[5] = (uint8_t)(event->request.common_address >> 8); + CHECK(6U + (sq ? 3U : 3U * count) + width * count <= 249U); + for (i = 0; i < count; ++i) { + if (!sq || i == 0) { + bytes[offset++] = (uint8_t)(3001U + i); bytes[offset++] = (uint8_t)((3001U + i) >> 8); bytes[offset++] = 0; + } + bytes[offset] = 1; + if (type == 37) { bytes[offset + 9] = 1; bytes[offset + 10] = 1; } + offset += width; + } + CHECK(IEC60870_Asdu_Create_From_Buffer(asdu, IEC104_Slave_Get_Application_Layer_Parameters(app.slave), + bytes, (uint16_t)offset) == IEC60870_ASDU_OK); +} + +/* 同一Slave允许两种召唤并存;同种实例重复创建或广播状态区重叠必须失败,销毁后可重建。 */ +static void test_counter_module_contracts(void) +{ + IEC104_InterrogationConfig_t config; + IEC104_Interrogation_t *counter; + app_open(); IEC104_Interrogation_Config_Default(&config); + config.use_station_registry = true; config.broadcast_states = broadcast_states; config.broadcast_capacity = 16; + CHECK(IEC104_CounterInterrogation_Create(app.slave, &config) == NULL); + config.broadcast_states = counter_broadcast_states; + counter = IEC104_CounterInterrogation_Create(app.slave, &config); CHECK(counter != NULL); + CHECK(IEC104_CounterInterrogation_Create(app.slave, &config) == NULL); + CHECK(IEC104_Interrogation_Destroy(counter)); + counter = IEC104_CounterInterrogation_Create(app.slave, &config); CHECK(counter != NULL); + CHECK(IEC104_Interrogation_Destroy(counter)); app_close(); +} + +/* 校验错误原因优先级、畸形布局的规范化回显和无法可靠应答时的丢弃,输入原文保持不变。 */ +static void test_counter_validation(void) +{ + static const struct { unsigned offset; uint8_t value, cot; unsigned length; } cases[] = { + {2, 8, 45, 10}, {2, 3, 45, 10}, {4, 2, 46, 10}, {6, 1, 47, 10}, + {1, 0x81, 7, 10}, {9, 5, 7, 11}, {9, 0, 7, 10}, {9, 6, 7, 10}, + {9, 63, 7, 10}, {2, 0x46, 7, 10}, {1, 0, 0, 10}, {1, 2, 0, 14} + }; + unsigned i; + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + uint8_t request[14] = {0}, expected[10]; const uint8_t *out[32]; size_t lengths[32]; + ci_open(); memcpy(request, ci_request, sizeof(ci_request)); request[cases[i].offset] = cases[i].value; + request[2] |= 0x80; request_unchanged(request, cases[i].length); app_finish(); + CHECK(IEC104_Interrogation_Get_Status(app.counter, &app.ci)); + CHECK(app.ci.rejected == 1 && app.other_rejected == 0 && !app.ci.gi_pending); + CHECK(collect_asdus(out, lengths) == (cases[i].cot != 0)); + if (cases[i].cot != 0) { + memcpy(expected, request, sizeof(expected)); expected[1] = 1; expected[2] = (uint8_t)(0xc0 | cases[i].cot); + CHECK(lengths[0] == 10 && memcmp(out[0], expected, 10) == 0); + } + ci_close(); + } +} + +/* 检查249字节下的三种最大批次,以及类型、SQ、头部、地址宽度和配置数量限制。 + * 错误批次保留当前交互供修正;总召实例仍拒绝计数量数据。 */ +static void test_counter_batch_validation(void) +{ + static const struct { uint8_t type; bool sq; unsigned count; } valid[] = {{15, false, 30}, {15, true, 48}, {37, false, 16}}; + IEC104_InterrogationConfig_t wide_config; + unsigned i; + IEC104_Interrogation_Config_Default(&wide_config); counter_custom_config = &wide_config; + ci_open(); + for (i = 0; i < sizeof(valid) / sizeof(valid[0]); ++i) { + IEC104_InterrogationEvent_t event = ci_data(5); + IEC60870_Asdu_t asdu; uint8_t bytes[249]; + ci_batch(&asdu, bytes, &event, valid[i].type, valid[i].count, valid[i].sq); + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_OK); + ci_process(); event = ci_take(IEC104_INTERROGATION_NEED_DATA); ci_done(&event); + } + { + IEC104_InterrogationEvent_t event = ci_data(1); IEC60870_Asdu_t asdu; uint8_t bytes[249]; + static const unsigned offsets[] = {2, 3, 4, 2, 2}; + static const uint8_t values[] = {37, 0x44, 2, 0x66, 0xa6}; + for (i = 0; i < 3; ++i) { + ci_batch(&asdu, bytes, &event, i == 0 ? 1 : i == 1 ? 16 : 37, 1, i == 2); + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_ERROR); + } + for (i = 0; i < sizeof(offsets) / sizeof(offsets[0]); ++i) { + ci_batch(&asdu, bytes, &event, 15, 1, false); bytes[offsets[i]] = values[i]; + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_ERROR); + } + { + IEC60870_AppLayerParameters_t wrong = *IEC104_Slave_Get_Application_Layer_Parameters(app.slave); + ci_batch(&asdu, bytes, &event, 15, 1, false); wrong.sizeOfIOA = 2; asdu.parameters = &wrong; + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_ERROR); + ci_batch(&asdu, bytes, &event, 15, 1, false); bytes[4] = 0xff; bytes[5] = 0xff; + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_ERROR); + } + ci_done(&event); + event = gi_data(); ci_batch(&asdu, bytes, &event, 15, 1, false); + CHECK(IEC104_Interrogation_Submit_Batch(app.interrogation, &event, &asdu) == IEC104_INTERROGATION_REPLY_ERROR); + gi_done(&event, 0, 0); + } + ci_close(); + { + IEC104_InterrogationConfig_t config; IEC104_InterrogationEvent_t event; IEC60870_Asdu_t asdu; uint8_t bytes[249]; + IEC104_Interrogation_Config_Default(&config); config.max_objects_per_asdu = 4; + counter_custom_config = &config; ci_open(); event = ci_data(5); + ci_batch(&asdu, bytes, &event, 15, 5, false); + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_ERROR); + ci_done(&event); ci_close(); + } +} + +/* 计数量忙只拒绝本实例的新请求;k=1、总召每帧一点时,两种业务仍须交错推进。 */ +static void test_counter_busy_independent(void) +{ + IEC104_InterrogationEvent_t event; const uint8_t *frames[32]; size_t lengths[32]; + ci_open(); event = ci_begin(5); ci_send(1, 1); ci_process(); + CHECK(app.ci.rejected == 1 && app.ci.gi_pending); + app_request(gi_request, sizeof(gi_request)); app_finish(); + CHECK(app.gi.gi_submitted == 1); + CHECK(IEC104_Interrogation_Reply_Begin(app.counter, &event, true) == IEC104_INTERROGATION_REPLY_OK); + ci_process(); event = ci_take(IEC104_INTERROGATION_NEED_DATA); ci_done(&event); + CHECK(collect_asdus(frames, lengths) == 11); ci_close(); + { + IEC104_InterrogationConfig_t config; unsigned step, acked = 0, first_counter = 32, last_general = 0, i; + IEC104_Interrogation_Config_Default(&config); config.max_objects_per_asdu = 1; + custom_config = &config; custom_window = 1; ci_open(); + queue_request(gi_request, sizeof(gi_request)); queue_request(ci_request, sizeof(ci_request)); app_poll(); + for (step = 0; step < 128; ++step) { + unsigned sent; + CHECK(Example_Step(&app)); app_poll(); sent = collect_asdus(frames, lengths); + if (sent != acked) { push_ack(sent); acked = sent; app_poll(); } + if (app.gi.pending_requests == 0 && app.ci.pending_requests == 0) { break; } + } + CHECK(step < 128 && acked == 30 && app.counter_completed == 1 && app.gi.gi_submitted == 1); + for (i = 0; i < acked; ++i) { + if (frames[i][0] == 15 && first_counter == 32) { first_counter = i; } + if (frames[i][0] == 1 || frames[i][0] == 3 || frames[i][0] == 11) { last_general = i; } + } + CHECK(first_counter < last_general); ci_close(); custom_config = NULL; custom_window = 32; + } +} + +/* 总请求按确认、两批数据、终止逐字节核对IOA、初值和BCR状态,并检查整轮快照一致性。 */ +static void test_counter_general_read(void) +{ + static const int32_t expected[] = {120000, 345600, 7890, 0, 1, 65535, INT32_MAX, 42}; + const uint8_t *frames[32]; size_t lengths[32]; unsigned i, j; + ci_open(); ci_send(5, 1); app_finish(); + CHECK(collect_asdus(frames, lengths) == 4); + CHECK(frames[0][0] == 101 && frames[0][2] == 7 && lengths[0] == 10); + CHECK(frames[3][0] == 101 && frames[3][2] == 10 && lengths[3] == 10); + for (i = 1; i < 3; ++i) { + CHECK(frames[i][0] == 15 && frames[i][1] == 4 && frames[i][2] == 37 && frames[i][3] == 0x5a); + CHECK(lengths[i] == 38); + for (j = 0; j < 4; ++j) { + const uint8_t *p = frames[i] + 6 + j * 8; unsigned n = (i - 1) * 4 + j; + uint32_t value = (uint32_t)p[3] | (uint32_t)p[4] << 8 | (uint32_t)p[5] << 16 | (uint32_t)p[6] << 24; + CHECK(((unsigned)p[0] | (unsigned)p[1] << 8) == 3001 + n && p[2] == 0); + CHECK(value == (uint32_t)expected[n] && p[7] == 0); + } + } + CHECK(app.counter_completed == 1 && app.other_rejected == 0); ci_close(); + /* 首批发送受阻后改写整个冻结区,两批仍须使用已固定的值、序号和品质。 */ + ci_open(); ci_send(5, 1); ci_process(); CHECK(Example_Drain_Counter(&app)); ci_process(); + congest(&last_request.connection); CHECK(Example_Drain_Counter(&app)); + CHECK(app.counter_pending && app.counter_cursor == 0 && app.counter_request != 0); + for (i = 0; i < EXAMPLE_COUNTER_COUNT; ++i) { + app.counter_frozen[i].value = -123; app.counter_frozen[i].sequence_number = 7; + app.counter_frozen[i].has_carry = 1; app.counter_frozen[i].is_adjusted = 1; app.counter_frozen[i].is_invalid = 1; + } + fake.send_blocked = 0; app_finish(); CHECK(collect_asdus(frames, lengths) == 4); + CHECK(frames[1][9] == 0xc0 && frames[1][10] == 0xd4 && frames[1][11] == 1 && frames[1][13] == 0); + CHECK(frames[2][9] == 1 && frames[2][10] == 0 && frames[2][13] == 0); + CHECK(app.counter_completed == 1); ci_close(); +} + +/* 示例只配置第1、2组,分别使用COT38、39;协议合法但未配置的第3、4组由应用拒绝。 */ +static void test_counter_group_read(void) +{ + unsigned q; + for (q = 1; q <= 4; ++q) { + const uint8_t *frames[32]; size_t lengths[32]; + ci_open(); ci_send((uint8_t)q, 1); app_finish(); + CHECK(collect_asdus(frames, lengths) == (q <= 2 ? 3U : 1U)); + if (q <= 2) { CHECK(frames[1][2] == 37 + q && frames[1][1] == 4); } + else { CHECK(frames[0][2] == 0x47); } + ci_close(); + } +} + +/* 记录动作次数并模拟执行耗时,区分“请求尚未执行”和“已执行、确认仍在等待”。 */ +typedef struct { unsigned calls; uint64_t elapsed; bool success; } CounterActionProbe; +static bool counter_action(void *context, const IEC104_InterrogationRequest_t *request) +{ + CounterActionProbe *probe = context; + CHECK(request->freeze >= 1 && request->freeze <= 3 && !request->is_test); + ++probe->calls; fake.now += probe->elapsed; return probe->success; +} + +/* 三种动作只确认、不发终止;检查清零后再冻结的值、SQ回绕和分组隔离。 + * TEST及WAIT不调用动作,已消费事件重试为STALE,应用拒绝只执行一次。 */ +static void test_counter_freeze_modes(void) +{ + unsigned frz; + for (frz = 1; frz <= 3; ++frz) { + const uint8_t *frames[32]; size_t lengths[32]; + uint8_t test_request[10]; + ci_open(); memcpy(test_request, ci_request, 10); test_request[2] = 0x86; + test_request[9] = (uint8_t)((frz << 6) | 5); app_request(test_request, 10); app_finish(); + CHECK(app.counter_current[0] == 120000 && app.counter_frozen[0].value == 120000 && app.counter_frozen[0].sequence_number == 0); + CHECK(collect_asdus(frames, lengths) == 1 && frames[0][2] == 0xc7); + fake.sent_bytes = 0; + ci_send((uint8_t)((frz << 6) | 5), 1); app_finish(); + CHECK(collect_asdus(frames, lengths) == 1 && frames[0][2] == 7); + CHECK(app.counter_completed == 1); ci_send(5, 1); app_finish(); + CHECK(collect_asdus(frames, lengths) == 5 && frames[2][13] == (frz == 3 ? 0 : 1)); + if (frz == 2 || frz == 3) { + ci_send(0x45, 1); app_finish(); ci_send(5, 1); app_finish(); + CHECK(collect_asdus(frames, lengths) == 10); + CHECK(frames[7][9] == 0 && frames[7][10] == 0 && frames[7][11] == 0 && frames[7][12] == 0); + } + ci_close(); + } + { + unsigned i; + ci_open(); + for (i = 0; i < EXAMPLE_COUNTER_COUNT; ++i) { app.counter_frozen[i].sequence_number = 31; } + ci_send(0x41, 1); app_finish(); + for (i = 0; i < EXAMPLE_COUNTER_COUNT; ++i) { CHECK(app.counter_frozen[i].sequence_number == (i < 4 ? 0 : 31)); } + ci_close(); + } + { + IEC104_InterrogationEvent_t event; CounterActionProbe probe = {0, 0, true}; + ci_open(); event = ci_begin(0x85); + queue_request(gi_request, sizeof(gi_request)); CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + CHECK(IEC104_CounterInterrogation_Execute_Begin(app.counter, &event, counter_action, &probe) == IEC104_INTERROGATION_REPLY_WAIT); + CHECK(probe.calls == 0); app_poll(); + CHECK(IEC104_CounterInterrogation_Execute_Begin(app.counter, &event, counter_action, &probe) == IEC104_INTERROGATION_REPLY_OK); + CHECK(IEC104_CounterInterrogation_Execute_Begin(app.counter, &event, counter_action, &probe) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(probe.calls == 1); ci_process(); CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_COMPLETED); + ci_empty(); ci_close(); + } + { + uint8_t request[10]; IEC104_InterrogationEvent_t event; CounterActionProbe probe = {0, 0, true}; + ci_open(); memcpy(request, ci_request, 10); request[2] = 0x86; request[9] = 0xc5; + app_request(request, 10); ci_process(); event = ci_take(IEC104_INTERROGATION_BEGIN); + CHECK(IEC104_CounterInterrogation_Execute_Begin(app.counter, &event, counter_action, &probe) == IEC104_INTERROGATION_REPLY_OK); + CHECK(probe.calls == 0 && ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_REJECTED); + ci_process(); ci_close(); + } + { + IEC104_InterrogationEvent_t event; CounterActionProbe probe = {0, 0, false}; + ci_open(); event = ci_begin(0x45); + CHECK(IEC104_CounterInterrogation_Execute_Begin(app.counter, &event, counter_action, &probe) == IEC104_INTERROGATION_REPLY_OK); + CHECK(probe.calls == 1 && ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_REJECTED); + ci_process(); CHECK(app.ci.negative_submitted == 1); ci_close(); + } +} + +/* STOP和断线须同时终结两种召唤,旧取数事件失效;重新激活或连接后可再次读取。 */ +static void test_counter_stop_disconnect(void) +{ + unsigned mode; + for (mode = 0; mode < 2; ++mode) { + IEC104_InterrogationEvent_t ce, ge; + ci_open(); ce = ci_data(5); ge = gi_data(); + if (mode == 0) { push(stop_act, sizeof(stop_act)); app_poll(); } + else { CHECK(IEC104_Slave_Close_Connection(app.slave, &last_request, NULL) == IEC104_TCP_OK); app_poll(); } + CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == (mode == 0 ? IEC104_INTERROGATION_FINISH_STOPPED : IEC104_INTERROGATION_FINISH_DISCONNECTED)); + CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == (mode == 0 ? IEC104_INTERROGATION_FINISH_STOPPED : IEC104_INTERROGATION_FINISH_DISCONNECTED)); + CHECK(IEC104_Interrogation_Complete_Data(app.counter, &ce) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(IEC104_Interrogation_Complete_Data(app.interrogation, &ge) == IEC104_INTERROGATION_REPLY_STALE); + if (mode == 0) { push(start_act, sizeof(start_act)); app_poll(); } + else { app_connect(); } + ci_send(5, 1); app_finish(); CHECK(app.counter_completed == 1); ci_close(); + } +} + +/* 动作开始前超时不执行回调;读取超时关闭共享连接,并使另一实例收到断线终结通知。 */ +static void test_counter_timeouts(void) +{ + IEC104_InterrogationConfig_t config; IEC104_InterrogationEvent_t event; CounterActionProbe probe = {0, 0, true}; + IEC104_TcpServerStatus_t service; + IEC104_Interrogation_Config_Default(&config); config.begin_timeout_ms = 100; config.data_timeout_ms = 100; config.total_timeout_ms = 200; + counter_custom_config = &config; ci_open(); event = ci_begin(0x85); fake.now += 100; + CHECK(IEC104_CounterInterrogation_Execute_Begin(app.counter, &event, counter_action, &probe) == IEC104_INTERROGATION_REPLY_STALE); + CHECK(probe.calls == 0 && ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_BEGIN_TIMEOUT); + ci_process(); CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && service.connected); ci_close(); + counter_custom_config = &config; ci_open(); event = ci_data(5); (void)gi_begin(); fake.now += 100; ci_process(); + CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_DATA_TIMEOUT); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK && !service.connected); + app_poll(); CHECK(gi_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_DISCONNECTED); ci_close(); + /* 动作已执行,即使确认排队跨过业务期限,也不能改成否定确认或再次执行动作。 */ + counter_custom_config = &config; ci_open(); event = ci_begin(0x85); congest(&last_request.connection); probe.elapsed = 150; + CHECK(IEC104_CounterInterrogation_Execute_Begin(app.counter, &event, counter_action, &probe) == IEC104_INTERROGATION_REPLY_OK); + fake.now += 300; ci_process(); CHECK(probe.calls == 1 && app.ci.timeouts == 0 && app.ci.negative_submitted == 0); + fake.send_blocked = 0; ci_process(); ci_process(); + CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_COMPLETED); ci_close(); +} + +/* 数据WAIT不推进批次,恢复后使用重新组好的数据;终止等待也必须保留并继续提交。 */ +static void test_counter_send_wait(void) +{ + IEC104_InterrogationEvent_t event; IEC60870_Asdu_t asdu; uint8_t bytes[249]; unsigned i; + ci_open(); event = ci_data(5); congest(&last_request.connection); ci_batch(&asdu, bytes, &event, 15, 1, false); + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_WAIT); + CHECK(event.batch_index == 0); fake.send_blocked = 0; + for (i = 0; i < 4; ++i) { app_poll(); } + bytes[9] = 42; + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_OK); + ci_process(); event = ci_take(IEC104_INTERROGATION_NEED_DATA); CHECK(event.batch_index == 1); + congest(&last_request.connection); CHECK(IEC104_Interrogation_Complete_Data(app.counter, &event) == IEC104_INTERROGATION_REPLY_OK); + ci_process(); CHECK(app.ci.waiting); fake.send_blocked = 0; ci_process(); ci_process(); + CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_COMPLETED); ci_close(); +} + +/* 三站读取保持全部确认、逐站数据、全部终止;动作广播逐站确认,中间站拒绝不影响其他站。 + * 每站使用实际CA,动作只产生一次确认和一次终结通知,不额外发送ACT_TERM。 */ +static void test_counter_broadcast(void) +{ + unsigned mode; + for (mode = 0; mode < 3; ++mode) { + IEC104_Station_t stations[2] = {{0}}; unsigned i; const uint8_t *frames[32]; size_t lengths[32]; + CounterActionProbe probe = {0, 0, true}; + ci_open(); + for (i = 0; i < 2; ++i) { CHECK(IEC104_Slave_Add_Station(app.slave, &stations[i], (uint16_t)(i + 2), NULL) == IEC104_STATION_OK); } + ci_send(mode == 0 ? 5 : 0x45, IEC104_CA_BROADCAST); + for (i = 0; i < 3; ++i) { + IEC104_InterrogationEvent_t event; + ci_process(); event = ci_take(IEC104_INTERROGATION_BEGIN); CHECK(event.request.common_address == i + 1); + if (mode == 0) { CHECK(IEC104_Interrogation_Reply_Begin(app.counter, &event, true) == IEC104_INTERROGATION_REPLY_OK); } + else { + if (mode == 2 && i == 1) { + CHECK(IEC104_Interrogation_Reply_Begin(app.counter, &event, false) == IEC104_INTERROGATION_REPLY_OK); + CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_REJECTED); ci_process(); + } else { + CHECK(IEC104_CounterInterrogation_Execute_Begin(app.counter, &event, counter_action, &probe) == IEC104_INTERROGATION_REPLY_OK); + ci_process(); CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_COMPLETED); + } + } + } + if (mode == 0) { + for (i = 0; i < 3; ++i) { + IEC104_InterrogationEvent_t event; IEC60870_Asdu_t asdu; uint8_t bytes[249]; + ci_process(); event = ci_take(IEC104_INTERROGATION_NEED_DATA); CHECK(event.request.common_address == i + 1); + ci_batch(&asdu, bytes, &event, 15, 1, false); + CHECK(IEC104_Interrogation_Submit_Batch(app.counter, &event, &asdu) == IEC104_INTERROGATION_REPLY_OK); + ci_process(); event = ci_take(IEC104_INTERROGATION_NEED_DATA); + CHECK(IEC104_Interrogation_Complete_Data(app.counter, &event) == IEC104_INTERROGATION_REPLY_OK); + } + ci_process(); ci_process(); + for (i = 0; i < 3; ++i) { CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_COMPLETED); } + } + app_poll(); CHECK(collect_asdus(frames, lengths) == (mode == 0 ? 9U : 3U)); + for (i = 0; i < (mode == 0 ? 9U : 3U); ++i) { + CHECK(frames[i][4] == i % 3 + 1 && frames[i][5] == 0); + CHECK(frames[i][2] == (mode == 2 && i == 1 ? 0x47 : i < 3 ? 7 : i < 6 ? 37 : 10)); + } + CHECK(probe.calls == (mode == 0 ? 0U : mode == 1 ? 3U : 2U)); ci_empty(); + for (i = 0; i < 2; ++i) { CHECK(IEC104_Slave_Remove_Station(app.slave, &stations[i]) == IEC104_STATION_OK); } + ci_close(); + } +} + +/* Type101由计数量实例接手;未知类型仍走兜底,即使计数量正在等待应用决定也能应答。 */ +static void test_dispatch_fallback(void) +{ + uint8_t request[10]; IEC104_InterrogationEvent_t event; + ci_open(); event = ci_begin(5); memcpy(request, ci_request, 10); request[0] = 200; + app_request(request, 10); + CHECK(app.other_rejected == 1 && app.other_submitted == 1 && app.ci.gi_pending); + app_finish(); + CHECK(IEC104_Interrogation_Reply_Begin(app.counter, &event, false) == IEC104_INTERROGATION_REPLY_OK); + CHECK(ci_take(IEC104_INTERROGATION_FINISHED).reason == IEC104_INTERROGATION_FINISH_REJECTED); + ci_process(); ci_close(); +} + +static void check_other_reply(const uint8_t *actual, size_t actual_length, + const uint8_t *request, size_t length) +{ + size_t index; + CHECK(actual_length == length); + for (index = 0; index < length; ++index) { + CHECK(actual[index] == (index == 2 ? (uint8_t)((request[2] & 0x80U) | 0x40U | 44U) : request[index])); + } +} + +/* 同一Poll交付两条命令时当场提交;窗口/写阻塞由HIGH承担,重复命令也逐条应答。 */ +static void test_other_batch(void) +{ + unsigned order, blocked; + for (order = 0; order < 3; ++order) { + for (blocked = 0; blocked < 3; ++blocked) { + const uint8_t *requests[2] = { + order == 1 ? read_request : fallback_request, + order == 0 ? read_request : fallback_request + }; + size_t sizes[2] = { + order == 1 ? sizeof(read_request) : sizeof(fallback_request), + order == 0 ? sizeof(read_request) : sizeof(fallback_request) + }; + const uint8_t *frames[32]; + size_t lengths[32]; + IEC104_TcpServerStatus_t status; + custom_window = blocked == 1 ? 1U : 32U; + app_open(); + fake.send_blocked = blocked == 2; + queue_request(requests[0], sizes[0]); + queue_request(requests[1], sizes[1]); + app_poll(); + CHECK(app.other_rejected == 2 && app.other_submitted == 2); + CHECK(app.other_overloads == 0 && app.other_cancelled == 0); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK && status.connected); + if (blocked == 1) { + CHECK(collect_asdus(frames, lengths) == 1 && status.high_queue.stored_asdu_count == 1); + push_ack(1); + } else if (blocked == 2) { + CHECK(collect_asdus(frames, lengths) == 0 && status.high_queue.stored_asdu_count == 1); + fake.send_blocked = 0; + } + app_finish(); + CHECK(collect_asdus(frames, lengths) == 2); + check_other_reply(frames[0], lengths[0], requests[0], sizes[0]); + check_other_reply(frames[1], lengths[1], requests[1], sizes[1]); + app_close(); + custom_window = 32; + } + } +} + +/* HIGH不足时只丢新兜底请求,恢复后没有旧请求重试,原连接继续接受新命令。 */ +static void test_other_overload(void) +{ + const uint8_t *frames[32]; + size_t lengths[32]; + uint8_t fresh[sizeof(read_request)]; + IEC104_TcpServerStatus_t before, after; + app_open(); + app_request(fallback_request, sizeof(fallback_request)); + CHECK(app.other_submitted == 1); + congest(&last_request.connection); + CHECK(IEC104_Slave_Get_Status(app.slave, &before, NULL) == IEC104_TCP_OK); + queue_request(read_request, sizeof(read_request)); + queue_request(read_request, sizeof(read_request)); + app_poll(); + CHECK(IEC104_Slave_Get_Status(app.slave, &after, NULL) == IEC104_TCP_OK && after.connected); + CHECK(after.high_queue.stored_asdu_count == before.high_queue.stored_asdu_count); + CHECK(app.other_rejected == 3 && app.other_submitted == 1 && app.other_overloads == 2); + CHECK(app.other_cancelled == 0 && app.gi.dropped_requests + app.other_overloads == 2); + fake.send_blocked = 0; + app_finish(); + CHECK(collect_asdus(frames, lengths) == 1); + memcpy(fresh, read_request, sizeof(fresh)); + fresh[3] = 0x63; + app_request(fresh, sizeof(fresh)); + CHECK(app.other_submitted == 2 && app.other_overloads == 2); + app_finish(); + CHECK(collect_asdus(frames, lengths) == 2); + check_other_reply(frames[0], lengths[0], fallback_request, sizeof(fallback_request)); + check_other_reply(frames[1], lengths[1], fresh, sizeof(fresh)); + CHECK(IEC104_Slave_Get_Status(app.slave, &after, NULL) == IEC104_TCP_OK && after.connected); + app_close(); +} + +/* 后续STOP或STOP/START已经受理时,当前请求取消;下一激活阶段只应答新请求。 */ +static void test_other_state_change(void) +{ + unsigned restarted; + for (restarted = 0; restarted < 2; ++restarted) { + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + queue_request(fallback_request, sizeof(fallback_request)); + push(stop_act, sizeof(stop_act)); + if (restarted) { push(start_act, sizeof(start_act)); } + app_poll(); + CHECK(app.other_rejected == 1 && app.other_submitted == 0 && app.other_cancelled == 1); + CHECK(app.other_overloads == 0 && collect_asdus(frames, lengths) == 0); + if (!restarted) { push(start_act, sizeof(start_act)); app_poll(); } + app_request(read_request, sizeof(read_request)); + CHECK(app.other_submitted == 1 && app.other_cancelled == 1); + app_finish(); + CHECK(collect_asdus(frames, lengths) == 1); + check_other_reply(frames[0], lengths[0], read_request, sizeof(read_request)); + app_close(); + } +} + +/* 单总召仍等待应用BEGIN时,其他命令即可提交,不必等总召结束。 */ +static void test_other_during_general(void) +{ + const uint8_t *frames[32]; + size_t lengths[32]; + app_open(); + app_request(gi_request, sizeof(gi_request)); + CHECK(app.gi.gi_pending); + app_request(read_request, sizeof(read_request)); + CHECK(app.gi.gi_pending && app.other_submitted == 1); + CHECK(collect_asdus(frames, lengths) == 1); + check_other_reply(frames[0], lengths[0], read_request, sizeof(read_request)); + app_finish(); + check_gi(1, 0x5a); + app_close(); +} + +/* Clock actions are controlled; these tests never call the host Clock_Set backend. */ +static const uint8_t cs_request[] = {103, 1, 6, 0x71, 1, 0, 0, 0, 0, 0x39, 0x30, 34, 12, 28, 10, 26}; +typedef struct { + unsigned calls; + bool success, reentrant; + uint64_t elapsed; + IEC104_ClockSynchronizationRequest_t last; +} ClockActionProbe; +static ClockActionProbe cs_probe; + +static bool cs_action(void *context, const IEC104_ClockSynchronizationRequest_t *request) +{ + ClockActionProbe *probe = context; + IEC104_ClockSynchronizationStatus_t before, after; + CHECK(request != NULL && request->generation != 0U && request->receive_serial != 0U); + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &before) && before.confirm_pending); + ++probe->calls; probe->last = *request; + if (probe->reentrant) { + CHECK(!before.timer_active); + CHECK(!IEC104_ClockSynchronization_Process(app.clock)); + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, context) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_ERROR); + CHECK(IEC104_ClockSynchronization_Handle_Event(app.clock, &last_request) == + IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR); + CHECK(!IEC104_ClockSynchronization_Destroy(app.clock)); + IEC104_ClockSynchronization_Cancel(app.clock); + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &after)); + CHECK(after.pending_requests == before.pending_requests && after.confirm_pending); + } + fake.now += probe->elapsed; + return probe->success; +} + +static void cs_open(const IEC104_ClockSynchronizationConfig_t *config) +{ + memset(&cs_probe, 0, sizeof(cs_probe)); cs_probe.success = true; + clock_enabled = true; clock_custom_config = config; app_open(); + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs) && app.cs.active); +} + +static void cs_close(void) +{ + cs_test_now_step = 0U; app_close(); clock_enabled = false; clock_custom_config = NULL; +} + +static void cs_process(void) +{ + CHECK(IEC104_ClockSynchronization_Process(app.clock)); + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs)); +} + +static void cs_finish(void) +{ + unsigned i; + for (i = 0U; i < 16U; ++i) { app_poll(); cs_process(); } + CHECK(app.cs.pending_requests == 0U); +} + +static void cs_send(uint8_t oa, uint16_t ca) +{ + uint8_t request[sizeof(cs_request)]; + memcpy(request, cs_request, sizeof(request)); request[3] = oa; + request[4] = (uint8_t)ca; request[5] = (uint8_t)(ca >> 8); + app_request(request, sizeof(request)); +} + +static void cs_timed_event(uint8_t value) +{ + uint8_t bytes[] = {30, 1, 3, 0, 1, 0, 0xe9, 3, 0, 0, 0x39, 0x30, 34, 12, 28, 10, 26}; + IEC60870_Asdu_t asdu; + bytes[9] = value; + CHECK(IEC60870_Asdu_Create_From_Buffer(&asdu, + IEC104_Slave_Get_Application_Layer_Parameters(app.slave), bytes, sizeof(bytes)) == IEC60870_ASDU_OK); + CHECK(IEC104_Slave_Enqueue_Event(app.slave, &asdu) == IEC104_TCP_SUBMIT_OK); +} + +static void cs_execute(void) +{ + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_OK); +} + +static void cs_reply(const uint8_t *bytes, size_t length, const uint8_t *request, + uint8_t cot, uint16_t ca) +{ + size_t i; + CHECK(length == sizeof(cs_request)); + for (i = 0U; i < length; ++i) { + uint8_t expected = i == 1U ? 1U : i == 2U ? cot : i == 4U ? (uint8_t)ca : + i == 5U ? (uint8_t)(ca >> 8) : request[i]; + CHECK(bytes[i] == expected); + } +} + +static void test_clock_interfaces(void) +{ + IEC104_ClockSynchronizationConfig_t config; + IEC104_ClockSynchronizationStatus_t status; + IEC104_ClockSynchronization_Config_Default(&config); + CHECK(config.master_utc_offset_minutes == 480 && config.execute_timeout_ms == 15000U); + CHECK(IEC104_ClockSynchronization_Create(NULL, &config) == NULL); + CHECK(!IEC104_ClockSynchronization_Destroy(NULL)); + CHECK(!IEC104_ClockSynchronization_Process(NULL)); + CHECK(!IEC104_ClockSynchronization_Get_Status(NULL, &status)); + CHECK(IEC104_ClockSynchronization_Handle_Event(NULL, NULL) == IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR); + CHECK(IEC104_ClockSynchronization_Execute_Next(NULL, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_ERROR); + app_open(); config.master_utc_offset_minutes = 1440; + CHECK(IEC104_ClockSynchronization_Create(app.slave, &config) == NULL); + config.master_utc_offset_minutes = -1440; + CHECK(IEC104_ClockSynchronization_Create(app.slave, &config) == NULL); + app_close(); cs_open(NULL); IEC104_ClockSynchronization_Config_Default(&config); + CHECK(IEC104_ClockSynchronization_Create(app.slave, &config) == NULL); + CHECK(!IEC104_ClockSynchronization_Get_Status(app.clock, NULL)); + CHECK(IEC104_ClockSynchronization_Handle_Event(app.clock, NULL) == IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR); + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, NULL, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_ERROR); + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY); + cs_send(0x71, 1); CHECK(!IEC104_ClockSynchronization_Destroy(app.clock)); +#if IEC104_TCP_SERVER_CAPACITY > 1 + { + IEC104_Slave_t *other_slave = IEC104_Slave_Create(); + IEC104_ClockSynchronization_t *other_clock; + IEC104_SlaveConfig_t slave_config; + CHECK(other_slave != NULL); IEC104_Slave_Config_Default(&slave_config); + CHECK(IEC104_Slave_Init(other_slave, &slave_config, NULL) == IEC104_TCP_OK); + other_clock = IEC104_ClockSynchronization_Create(other_slave, &config); + CHECK(other_clock != NULL); + CHECK(IEC104_ClockSynchronization_Handle_Event(other_clock, &last_request) == + IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR); + CHECK(IEC104_ClockSynchronization_Destroy(other_clock)); + CHECK(IEC104_Slave_Close(other_slave, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Destroy(other_slave, NULL) == IEC104_TCP_OK); + } +#endif + CHECK(cs_probe.calls == 0U); IEC104_ClockSynchronization_Cancel(app.clock); cs_close(); +} + +static void test_clock_execute_and_echo(void) +{ + const uint8_t *frames[32]; size_t lengths[32]; + IEC104_SlaveEvent_t before; + cs_open(NULL); app_request(cs_request, sizeof(cs_request)); before = last_request; + cs_process(); CHECK(cs_probe.calls == 0U && app.cs.execute_pending); + fake.now += 1655U; cs_probe.elapsed = 400U; cs_execute(); + CHECK(cs_probe.calls == 1U && cs_probe.last.accepted_ms == 1000U && cs_probe.last.base_ms == 2655U); + CHECK(cs_probe.last.local_time.year == 2026U && cs_probe.last.local_time.month == 10U && + cs_probe.last.local_time.day == 28U && cs_probe.last.local_time.hour == 12U && + cs_probe.last.local_time.minute == 34U && cs_probe.last.local_time.second == 14U && + cs_probe.last.local_time.ms == 0U && cs_probe.last.station_id == IEC104_Station_Get_Id(&app.station)); + CHECK(memcmp(&before, &last_request, sizeof(before)) == 0); + cs_finish(); CHECK(app.cs.executed == 1U && app.cs.synchronized == 1U); + CHECK(collect_asdus(frames, lengths) == 1U); cs_reply(frames[0], lengths[0], cs_request, 7, 1); + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY); + app_request(cs_request, sizeof(cs_request)); fake.now += 100U; cs_execute(); cs_finish(); + CHECK(cs_probe.calls == 2U && cs_probe.last.accepted_ms == 3055U && cs_probe.last.base_ms == 3155U); + CHECK(cs_probe.last.local_time.second == 12U && cs_probe.last.local_time.ms == 445U); + CHECK(collect_asdus(frames, lengths) == 2U); cs_reply(frames[1], lengths[1], cs_request, 7U, 1U); + cs_close(); +} + +static void test_clock_timezone_rollover(void) +{ + IEC104_ClockSynchronizationConfig_t config; + uint8_t request[sizeof(cs_request)]; + IEC104_ClockSynchronization_Config_Default(&config); config.master_utc_offset_minutes = 0; + cs_open(&config); memcpy(request, cs_request, sizeof(request)); request[12] |= 0x80U; + app_request(request, sizeof(request)); cs_execute(); + CHECK(cs_probe.last.master_time.is_summer_time && cs_probe.last.local_time.hour == 19U); + cs_finish(); cs_close(); + cs_open(NULL); memcpy(request, cs_request, sizeof(request)); + request[9] = 0xfc; request[10] = 0xe9; request[11] = 59; request[12] = 23; + request[13] = 31; request[14] = 12; request[15] = 26; + app_request(request, sizeof(request)); fake.now += 200U; cs_execute(); + CHECK(cs_probe.last.local_time.year == 2027U && cs_probe.last.local_time.month == 1U && + cs_probe.last.local_time.day == 1U && cs_probe.last.local_time.hour == 0U && + cs_probe.last.local_time.minute == 0U && cs_probe.last.local_time.second == 0U && + cs_probe.last.local_time.ms == 100U); + cs_finish(); cs_close(); + cs_open(NULL); memcpy(request, cs_request, sizeof(request)); + request[9] = 0; request[10] = 0; request[11] = 0; request[12] = 0x80; + request[13] = 1; request[14] = 1; request[15] = 0; + app_request(request, sizeof(request)); cs_execute(); cs_finish(); + CHECK(cs_probe.last.local_time.year == 1999U && cs_probe.last.local_time.month == 12U && + cs_probe.last.local_time.day == 31U && cs_probe.last.local_time.hour == 23U); + { + const uint8_t *frames[32]; size_t lengths[32]; + CHECK(collect_asdus(frames, lengths) == 1U); cs_reply(frames[0], lengths[0], request, 7U, 1U); + } + cs_close(); +} + +static void test_clock_validation(void) +{ + static const struct { unsigned offset; uint8_t value, cot; } cases[] = { + {2, 5, 45}, {4, 2, 46}, {6, 1, 47}, {2, 0x86, 0xc7}, {2, 0x46, 0x47}, + {1, 0x81, 0x47}, {11, 0xa2, 0x47}, {14, 0, 0x47}, {11, 0x62, 0x47} + }; + unsigned i; + for (i = 0U; i < sizeof(cases) / sizeof(cases[0]); ++i) { + uint8_t request[sizeof(cs_request)]; const uint8_t *frames[32]; size_t lengths[32]; + cs_open(NULL); memcpy(request, cs_request, sizeof(request)); request[cases[i].offset] = cases[i].value; + if (i < 2U) { request[4] = 2U; request[6] = 1U; } + app_request(request, sizeof(request)); cs_finish(); + CHECK(cs_probe.calls == 0U && collect_asdus(frames, lengths) == 1U); + cs_reply(frames[0], lengths[0], request, + cases[i].cot < 64U ? (uint8_t)(cases[i].cot | 0x40U) : cases[i].cot, + request[4]); + cs_close(); + } + { + uint8_t request[26]; const uint8_t *frames[32]; size_t lengths[32]; + cs_open(NULL); memcpy(request, cs_request, sizeof(cs_request)); request[16] = 0x5a; + app_request(request, 17U); cs_finish(); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][2] == 0x47U); + memcpy(request + 16, cs_request + 6, 10); request[1] = 2; + app_request(request, sizeof(request)); cs_finish(); + CHECK(collect_asdus(frames, lengths) == 1U && cs_probe.calls == 0U); + memcpy(request, cs_request, sizeof(cs_request)); request[1] = 0U; + app_request(request, sizeof(cs_request)); cs_finish(); + request[1] = 1U; app_request(request, sizeof(cs_request) - 1U); cs_finish(); + CHECK(collect_asdus(frames, lengths) == 1U && cs_probe.calls == 0U); + cs_close(); + } +} + +static void test_clock_fifo_and_full(void) +{ + const uint8_t *frames[32]; size_t lengths[32]; unsigned i; + uint8_t request[sizeof(cs_request)]; + cs_open(NULL); cs_send(0x70, 1); + memcpy(request, cs_request, sizeof(request)); request[3] = 0x71; request[2] = 5; + app_request(request, sizeof(request)); cs_send(0x72, 1); cs_send(0x73, 1); cs_send(0x74, IEC104_CA_BROADCAST); + cs_process(); CHECK(collect_asdus(frames, lengths) == 0U && app.cs.pending_requests == 4U); + CHECK(app.cs.dropped_requests == 1U && app.cs.overloads == 1U && cs_probe.calls == 0U); + for (i = 0U; i < 3U; ++i) { cs_execute(); } + cs_finish(); CHECK(cs_probe.calls == 3U && collect_asdus(frames, lengths) == 4U); + for (i = 0U; i < 4U; ++i) { + CHECK(frames[i][3] == 0x70U + i && frames[i][2] == (i == 1U ? 0x6dU : 7U)); + } + cs_close(); + cs_open(NULL); queue_request(cs_request, sizeof(cs_request)); queue_request(cs_request, sizeof(cs_request)); + app_poll(); cs_execute(); cs_execute(); cs_finish(); + CHECK(cs_probe.calls == 2U && app.cs.submitted == 2U && collect_asdus(frames, lengths) == 2U); + cs_close(); +} + +static void test_clock_action_contract(void) +{ + const uint8_t *frames[32]; size_t lengths[32]; + IEC104_ClockSynchronizationConfig_t config; + IEC104_ClockSynchronization_Config_Default(&config); config.execute_timeout_ms = 10U; + cs_open(&config); cs_probe.reentrant = true; cs_probe.elapsed = 100U; + cs_send(0x71, 1); cs_execute(); cs_finish(); + CHECK(cs_probe.calls == 1U && app.cs.timeouts == 0U && app.cs.synchronized == 1U); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][2] == 7U); + cs_close(); + cs_open(NULL); cs_probe.success = false; cs_send(0x71, 1); cs_execute(); cs_finish(); + CHECK(cs_probe.calls == 1U && app.cs.executed == 1U && app.cs.synchronized == 0U); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][2] == 0x47U); + cs_close(); +} + +static void test_clock_deadlines(void) +{ + IEC104_ClockSynchronizationConfig_t config; + const uint8_t *frames[32]; size_t lengths[32]; unsigned via_execute; + IEC104_ClockSynchronization_Config_Default(&config); config.execute_timeout_ms = 10U; + for (via_execute = 0U; via_execute < 2U; ++via_execute) { + cs_open(&config); cs_send(0x71, 1); fake.now += 9U; + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs)); + CHECK(app.cs.timer_active && app.cs.timeout_remaining_ms == 1U); + ++fake.now; + if (via_execute) { + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY); + } else { cs_process(); } + cs_finish(); + CHECK(cs_probe.calls == 0U && app.cs.timeouts == 1U && !app.cs.timer_active); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][2] == 0x47U); + cs_close(); + } + cs_open(&config); cs_send(0x71, 1); fake.now = 1009U; cs_test_now_step = 2U; + (void)IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe); + cs_test_now_step = 0U; + CHECK(cs_probe.calls == 0U || cs_probe.last.base_ms < cs_probe.last.accepted_ms + 10U); + cs_finish(); cs_close(); + config.execute_timeout_ms = 0U; cs_open(&config); cs_send(0x71, 1); fake.now += 100U; + cs_execute(); cs_finish(); CHECK(app.cs.timeouts == 0U && cs_probe.calls == 1U); cs_close(); + config.execute_timeout_ms = 10U; cs_open(&config); cs_send(0x71, 1); fake.now = 999U; + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY); + fake.now = 1001U; cs_finish(); + CHECK(cs_probe.calls == 0U && app.cs.time_errors + app.cs.timeouts == 1U); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][2] == 0x47U); cs_close(); +} + +static void test_clock_time_errors(void) +{ + IEC104_ClockSynchronizationConfig_t config; + unsigned mode; + IEC104_ClockSynchronization_Config_Default(&config); config.execute_timeout_ms = 0U; + for (mode = 0U; mode < 3U; ++mode) { + uint8_t request[sizeof(cs_request)]; const uint8_t *frames[32]; size_t lengths[32]; + cs_open(&config); memcpy(request, cs_request, sizeof(request)); + if (mode == 2U) { + request[9] = 0xfc; request[10] = 0xe9; request[11] = 59; request[12] = 23; + request[13] = 31; request[14] = 12; request[15] = 99; + } + app_request(request, sizeof(request)); + fake.now = mode == 0U ? 999U : mode == 1U ? UINT64_MAX : 1200U; + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY); + fake.now = 1200U; cs_finish(); + CHECK(cs_probe.calls == 0U && app.cs.time_errors == 1U && app.cs.timeouts == 0U); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][2] == 0x47U); + cs_close(); + } + { + uint8_t request[sizeof(cs_request)]; const uint8_t *frames[32]; size_t lengths[32]; + config.master_utc_offset_minutes = 0; cs_open(&config); + memcpy(request, cs_request, sizeof(request)); + request[9] = 0x5f; request[10] = 0xea; request[11] = 59U; request[12] = 23U; + request[13] = 31U; request[14] = 12U; request[15] = 99U; + app_request(request, sizeof(request)); cs_finish(); + CHECK(app.cs.accepted == 0U && cs_probe.calls == 0U && app.cs.rejected == 1U); + CHECK(collect_asdus(frames, lengths) == 1U); cs_reply(frames[0], lengths[0], request, 0x47U, 1U); + cs_close(); + } +} + +static void test_clock_try_order_and_wait(void) +{ + const uint8_t *frames[32]; size_t lengths[32]; + IEC104_TcpServerStatus_t status; + custom_window = 1U; cs_open(NULL); cs_send(0x71, 1); + cs_timed_event(0); app_poll(); cs_timed_event(1); + cs_execute(); cs_process(); + CHECK(cs_probe.calls == 1U && app.cs.confirm_pending && app.cs.waits != 0U && !app.cs.timer_active); + CHECK(!app.cs.need_process && IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK); + CHECK(status.high_queue.stored_asdu_count == 0U && collect_asdus(frames, lengths) == 1U); + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_EMPTY); + push_ack(1); app_poll(); cs_process(); + CHECK(collect_asdus(frames, lengths) == 2U && frames[0][0] == 30U && frames[1][0] == 30U && frames[1][9] == 1U); + CHECK(app.cs.confirm_pending && cs_probe.calls == 1U); + push_ack(2); cs_finish(); + CHECK(collect_asdus(frames, lengths) == 3U && frames[2][0] == 103U && frames[2][2] == 7U); + CHECK(cs_probe.calls == 1U && app.cs.submitted == 1U); + cs_close(); custom_window = 32U; + { + uint8_t request[sizeof(cs_request)]; IEC104_TcpServerStatus_t before; + cs_open(NULL); app_request(read_request, sizeof(read_request)); congest(&last_request.connection); + CHECK(IEC104_Slave_Get_Status(app.slave, &before, NULL) == IEC104_TCP_OK); + memcpy(request, cs_request, sizeof(request)); request[2] = 5U; + app_request(request, sizeof(request)); memset(request, 0xcc, sizeof(request)); cs_process(); + CHECK(app.cs.pending_requests == 1U && app.cs.waits != 0U && cs_probe.calls == 0U && !app.cs.need_process); + CHECK(IEC104_Slave_Get_Status(app.slave, &status, NULL) == IEC104_TCP_OK); + CHECK(status.high_queue.stored_asdu_count == before.high_queue.stored_asdu_count); + fake.send_blocked = 0; cs_finish(); + CHECK(collect_asdus(frames, lengths) == 2U); cs_reply(frames[1], lengths[1], cs_request, 0x6dU, 1U); + cs_close(); + } +} + +static void test_clock_session_cleanup(void) +{ + unsigned mode; + cs_open(NULL); cs_send(0x71, 1); push(start_act, sizeof(start_act)); app_poll(); + cs_execute(); cs_finish(); CHECK(cs_probe.calls == 1U && app.cs.cancelled == 0U); cs_close(); + for (mode = 0U; mode < 3U; ++mode) { + const uint8_t *frames[32]; size_t lengths[32]; + cs_open(NULL); cs_send(0x71, 1); + if (mode != 0U) { congest(&last_request.connection); cs_execute(); cs_process(); } + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs) && app.cs.pending_requests == 1U); + if (mode == 2U) { + CHECK(IEC104_Slave_Close_Connection(app.slave, &last_request, NULL) == IEC104_TCP_OK); + app_poll(); fake.send_blocked = 0; app_connect(); + } else { + push(stop_act, sizeof(stop_act)); push(start_act, sizeof(start_act)); + app_poll(); fake.send_blocked = 0; + } + cs_process(); CHECK(app.cs.pending_requests == 0U && app.cs.cancelled == 1U); + CHECK(collect_asdus(frames, lengths) == 0U && cs_probe.calls == (mode == 0U ? 0U : 1U)); + cs_send(0x72, 1); cs_execute(); cs_finish(); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][3] == 0x72U); + CHECK(cs_probe.calls == (mode == 0U ? 1U : 2U)); + cs_close(); + } + { + const uint8_t *frames[32]; size_t lengths[32], written; + cs_open(NULL); cs_send(0x71, 1); written = fake.sent_bytes; fake.send_limit = 3U; cs_execute(); + CHECK(fake.sent_bytes > written && fake.sent_bytes < written + sizeof(cs_request) + 6U); + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs)); + CHECK(app.cs.pending_requests == 0U && app.cs.submitted == 1U); + push(stop_act, sizeof(stop_act)); app_poll(); fake.send_limit = 0U; cs_finish(); + CHECK(collect_asdus(frames, lengths) == 1U && cs_probe.calls == 1U && app.cs.cancelled == 0U); + cs_reply(frames[0], lengths[0], cs_request, 7U, 1U); cs_close(); + } +} + +static void test_clock_stop_priority(void) +{ + IEC104_ClockSynchronizationConfig_t config; + IEC104_TcpServerStatus_t service; + IEC104_ClockSynchronization_Config_Default(&config); config.execute_timeout_ms = 10U; + cs_open(&config); cs_send(0x71, 1); fake.now += 10U; + push(stop_act, sizeof(stop_act)); CHECK(IEC104_Slave_Poll(app.slave, 0, NULL) == IEC104_TCP_OK); + CHECK(IEC104_ClockSynchronization_Execute_Next(app.clock, cs_action, &cs_probe) == + IEC104_CLOCK_SYNCHRONIZATION_EXECUTE_WAIT); + cs_process(); CHECK(app.cs.events_pending && app.cs.timeouts == 0U && app.cs.cancelled == 0U); + app_poll(); cs_process(); CHECK(app.cs.cancelled == 1U && app.cs.timeouts == 0U && cs_probe.calls == 0U); + cs_close(); + cs_open(NULL); cs_send(0x71, 1); congest(&last_request.connection); cs_execute(); cs_process(); + retain_stop_for_submit(); fake.send_blocked = 0; cs_process(); + CHECK(IEC104_Slave_Get_Status(app.slave, &service, NULL) == IEC104_TCP_OK); + CHECK(!service.connection.data_transfer_started && service.pending_event_count != 0U); + CHECK(app.cs.events_pending && !app.cs.need_process && app.cs.cancelled == 0U && app.cs.pending_requests == 1U); + app_poll(); cs_process(); CHECK(app.cs.cancelled == 1U && app.cs.pending_requests == 0U && cs_probe.calls == 1U); + cs_close(); +} + +static void test_clock_fatal_cleanup(void) +{ + cs_open(NULL); cs_send(0x71, 1); congest(&last_request.connection); cs_execute(); cs_process(); + sut->queue_initialized = 0; + CHECK(IEC104_ClockSynchronization_Process(app.clock)); + sut->queue_initialized = 1; + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs)); + CHECK(app.cs.pending_requests == 0U && app.cs.cancelled == 1U && cs_probe.calls == 1U); + fake.send_blocked = 0; app_poll(); cs_process(); + CHECK(cs_probe.calls == 1U && app.cs.submitted == 0U); + cs_close(); +} + +static void test_clock_directed_station(void) +{ + IEC104_Station_t replacement = {0}; uint64_t original; + const uint8_t *frames[32]; size_t lengths[32]; + cs_open(NULL); original = IEC104_Station_Get_Id(&app.station); cs_send(0x71, 1); + CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&app.station)); + CHECK(IEC104_Slave_Add_Station(app.slave, &replacement, 1U, &replacement) == IEC104_STATION_OK); + cs_execute(); CHECK(cs_probe.last.station_id == original); + cs_finish(); CHECK(IEC104_Station_Is_Reusable(&app.station)); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][4] == 1U); + CHECK(IEC104_Slave_Remove_Station(app.slave, &replacement) == IEC104_STATION_OK); + cs_close(); +} + +static void test_clock_broadcast(void) +{ + IEC104_Station_t second = {0}, third = {0}; + const uint8_t *frames[32]; size_t lengths[32]; unsigned i; + cs_open(NULL); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, NULL) == IEC104_STATION_OK); + cs_send(0x71, IEC104_CA_BROADCAST); cs_execute(); cs_finish(); + CHECK(cs_probe.calls == 1U && cs_probe.last.station_id == 0U && collect_asdus(frames, lengths) == 3U); + for (i = 0U; i < 3U; ++i) { CHECK(frames[i][4] == i + 1U && frames[i][2] == 7U); } + cs_probe.success = false; cs_send(0x72, IEC104_CA_BROADCAST); cs_execute(); cs_finish(); + CHECK(cs_probe.calls == 2U && collect_asdus(frames, lengths) == 6U); + for (i = 3U; i < 6U; ++i) { CHECK(frames[i][4] == i - 2U && frames[i][2] == 0x47U); } + { + uint8_t request[sizeof(cs_request)]; + memcpy(request, cs_request, sizeof(request)); request[4] = 0xff; request[5] = 0xff; request[2] = 5; + app_request(request, sizeof(request)); cs_finish(); + CHECK(cs_probe.calls == 2U && collect_asdus(frames, lengths) == 9U); + for (i = 6U; i < 9U; ++i) { CHECK(frames[i][4] == i - 5U && frames[i][2] == 0x6dU); } + } + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &third) == IEC104_STATION_OK); + cs_close(); + for (i = 0U; i < 2U; ++i) { + uint8_t request[sizeof(cs_request)]; + cs_open(NULL); CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + memcpy(request, cs_request, sizeof(request)); request[4] = 0xff; request[5] = 0xff; + request[2] = i == 0U ? 5U : 6U; app_request(request, sizeof(request)); cs_finish(); + CHECK(collect_asdus(frames, lengths) == 1U && cs_probe.calls == 0U); + cs_reply(frames[0], lengths[0], request, i == 0U ? 0x6dU : 0x6eU, IEC104_CA_BROADCAST); + cs_close(); + } +} + +static void test_clock_broadcast_mutation(void) +{ + IEC104_Station_t second = {0}, replacement = {0}, added = {0}; + const uint8_t *frames[32]; size_t lengths[32]; + cs_open(NULL); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + cs_send(0x71, IEC104_CA_BROADCAST); congest(&last_request.connection); cs_execute(); cs_process(); + CHECK(app.cs.confirm_pending && app.cs.waits != 0U); + CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &replacement, 1U, NULL) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Add_Station(app.slave, &added, 3U, NULL) == IEC104_STATION_OK); + fake.send_blocked = 0; cs_finish(); + CHECK(collect_asdus(frames, lengths) == 1U && frames[0][4] == 2U && cs_probe.calls == 1U); + CHECK(IEC104_Station_Is_Reusable(&app.station)); + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &replacement) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(app.slave, &added) == IEC104_STATION_OK); + cs_close(); + { + unsigned mode; + for (mode = 0U; mode < 3U; ++mode) { + memset(&second, 0, sizeof(second)); cs_open(NULL); + CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, NULL) == IEC104_STATION_OK); + cs_send(0x71, IEC104_CA_BROADCAST); congest(&last_request.connection); cs_execute(); cs_process(); + if (mode != 2U) { CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); } + CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&app.station) && !IEC104_Station_Is_Reusable(&second)); + if (mode == 0U) { push(stop_act, sizeof(stop_act)); app_poll(); } + else if (mode == 1U) { IEC104_ClockSynchronization_Cancel(app.clock); } + else { + sut->queue_initialized = 0; CHECK(IEC104_ClockSynchronization_Process(app.clock)); + sut->queue_initialized = 1; + } + CHECK(IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs)); + CHECK(app.cs.pending_requests == 0U && app.cs.cancelled == 1U && cs_probe.calls == 1U); + if (mode == 2U) { CHECK(IEC104_Slave_Remove_Station(app.slave, &app.station) == IEC104_STATION_OK); } + CHECK(IEC104_Station_Is_Reusable(&app.station) && IEC104_Station_Is_Reusable(&second)); + fake.send_blocked = 0; cs_close(); + } + } +} + +static void test_clock_example_integration(void) +{ + const uint8_t *frames[32]; size_t lengths[32]; unsigned i, clocks = 0U; + uint8_t counter_request[sizeof(ci_request)]; + counter_enabled = true; cs_open(NULL); + memcpy(counter_request, ci_request, sizeof(counter_request)); counter_request[3] = 0x72U; + queue_request(gi_request, sizeof(gi_request)); queue_request(counter_request, sizeof(counter_request)); + queue_request(cs_request, sizeof(cs_request)); app_poll(); + CHECK(!app.clock_system && !app.clock_valid); app_finish(); + CHECK(app.clock_valid && app.clock_base_ms == 1000U && app.cs.executed == 1U && app.cs.synchronized == 1U); + CHECK(cs_probe.calls == 0U && app.other_rejected == 0U); + for (i = 0U; i < collect_asdus(frames, lengths); ++i) { + if (frames[i][0] == 103U) { ++clocks; cs_reply(frames[i], lengths[i], cs_request, 7U, 1U); } + } + CHECK(clocks == 1U && app.counter_completed == 1U); + check_gi(5U, 0x5aU); cs_close(); counter_enabled = false; +} + +int main(void) +{ + test_total_call(); + test_wait_each_phase(); + test_validation(); + test_busy(); + test_stop(); + test_disconnect(); + test_malformed(); + test_malformed_drop(); + test_receiver_truncated(); + test_malformed_preserves_active(); + test_malformed_backpressure(); + test_overload(); + test_event_budget_stop_start(); + test_non_gi_ignored(); + test_module_contracts(); + test_need_process_business(); + test_need_process_send_wait(); + test_need_process_reply_and_transport(); + test_gi_spontaneous_order(); + test_gi_accepted_order(); + test_gi_send_wait_timeout(); + test_gi_window_wait(); + test_gi_due_ack_wait(); + test_gi_submit_fatal(); + test_stop_in_process_after_state_backpressure(); + test_stop_during_submit(); + test_fatal_during_stop_submit(); + test_submit_error_without_stop(); + test_submit_with_new_record(); + test_event_empty_and_wait(); + test_event_copy_and_send_wait(); + test_event_batch_validation(); + test_event_mixed_types(); + test_event_capacity_batches(); + test_event_reject_and_fail(); + test_event_cancellation(); + test_event_queue_and_destroy(); + test_event_pending_stop_start(); + test_event_instance_reuse(); + test_slave_event_validation(); + test_reply_while_waiting(); + test_reply_event_backpressure(); + test_reply_slot_reuse(); + test_reply_non_head_events(); + test_reply_round_robin(); + test_reply_send_wait(); + test_reply_preserves_submitted_data(); + test_reply_cleanup(); + test_process_reply_only(); + test_immediate_reply_guard(); + test_overload_drop_preserves_active(); + test_qoi_sequential_retry(); + test_busy_all_phases(); + test_busy_cancel_match(); + test_busy_stop_cleanup(); + test_busy_preserves_deadline(); + test_next_request_deadline(); + test_begin_capacity_wait(); + test_cancel_before_begin(); + test_group_interrogation(); + test_example_groups(); + test_example_double_wait_values(); + test_remote_deactivation_phases(); + test_remote_deactivation_validation(); + test_remote_deactivation_full_queue(); + test_begin_timeout(); + test_data_timeout(); + test_timeout_progress(); + test_total_timeout_waits(); + test_timeout_disabled_and_large(); + test_timeout_transport_priority(); + test_preconfirm_failure_reply_order(); + test_preconfirm_failure_transport_priority(); + test_confirm_submission_boundary(); + test_broadcast_interrogation(); + test_broadcast_error_replies(); + test_broadcast_busy(); + test_broadcast_deactivation(); + test_station_binding_lifetime(); + test_station_binding_wire(); + test_registry_routing(); + test_registry_broadcast_many(); + test_registry_delete_current(); + test_registry_broadcast_mutation(); + test_registry_reply_fanout(); + test_registry_deactivation(); + test_registry_reject_timeout_stop(); + test_registry_wait_each_phase(); + test_registry_malformed(); + test_registry_example_order(); + test_registry_capacity(); + test_registry_workspace_contract(); + test_registry_confirmed_deleted(); + test_registry_finish_delivery(); + test_registry_directed_deactivation(); + test_registry_deactivation_full(); + test_registry_reject_during_deactivation(); + test_registry_total_deadline(); + test_registry_timeout_priority(); + test_counter_module_contracts(); + test_counter_validation(); + test_counter_batch_validation(); + test_counter_busy_independent(); + test_counter_general_read(); + test_counter_group_read(); + test_counter_freeze_modes(); + test_counter_stop_disconnect(); + test_counter_timeouts(); + test_counter_send_wait(); + test_counter_broadcast(); + test_dispatch_fallback(); + test_other_batch(); + test_other_overload(); + test_other_state_change(); + test_other_during_general(); + test_clock_interfaces(); + test_clock_execute_and_echo(); + test_clock_timezone_rollover(); + test_clock_validation(); + test_clock_fifo_and_full(); + test_clock_action_contract(); + test_clock_deadlines(); + test_clock_time_errors(); + test_clock_try_order_and_wait(); + test_clock_session_cleanup(); + test_clock_stop_priority(); + test_clock_fatal_cleanup(); + test_clock_directed_station(); + test_clock_broadcast(); + test_clock_broadcast_mutation(); + test_clock_example_integration(); + puts("slave example + event interrogation tests: 131 groups passed (99 general + 12 counter + 4 fallback + 16 clock)"); + return 0; +} diff --git a/IEC60870-Slave/.build/startup-idle-probe.cjs b/IEC60870-Slave/.build/startup-idle-probe.cjs new file mode 100644 index 00000000..e7738f73 --- /dev/null +++ b/IEC60870-Slave/.build/startup-idle-probe.cjs @@ -0,0 +1,67 @@ +'use strict'; +const assert = require('node:assert/strict'); +const net = require('node:net'); +const { spawn } = require('node:child_process'); +const { once } = require('node:events'); +const { setTimeout: delay } = require('node:timers/promises'); + +async function main() { + const child = spawn(process.argv[2], ['--port', '0', '--ca', '7', '--run-ms', '6000'], + { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }); + let output = '', errors = '', socket; + child.stdout.on('data', data => { output += data; }); + child.stderr.on('data', data => { errors += data; }); + const exit = once(child, 'exit'); + // Observe launch failure immediately even while awaiting the listening line. + exit.catch(() => {}); + try { + const port = await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('No listener: ' + 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])); } + }); + }); + socket = net.connect({ host: '127.0.0.1', port }); + let received = Buffer.alloc(0), networkError; + socket.on('data', data => { received = Buffer.concat([received, data]); }); + socket.on('error', error => { networkError = error; }); + await once(socket, 'connect'); + await delay(1000); + assert.ifError(networkError); + assert.equal(received.length, 0, 'Unexpected data after TCP connection'); + console.log('TCP connect, no command, 1000 ms: 0 received bytes'); + + socket.write(Buffer.from([0x68, 4, 7, 0, 0, 0])); + await delay(1500); + assert.ifError(networkError); + assert.equal(received.toString('hex'), '68040b000000'); + console.log('STARTDT only, 1500 ms: 68 04 0b 00 00 00; 0 I frames'); + + received = Buffer.alloc(0); + const request = Buffer.from([0x68, 14, 0, 0, 0, 0, 100, 1, 6, 0, 7, 0, 0, 0, 0, 20]); + socket.write(request); + await delay(1000); + assert.ifError(networkError); + const actual = []; + for (let offset = 0; offset < received.length;) { + assert.equal(received[offset], 0x68); + const end = offset + received[offset + 1] + 2; + assert(end <= received.length); + if ((received[offset + 2] & 1) === 0) actual.push([received[offset + 6], received[offset + 8] & 0x3f]); + offset = end; + } + assert.deepEqual(actual, [[100, 7], [1, 20], [1, 20], [3, 20], [3, 20], [11, 20], [11, 20], [100, 10]]); + console.log('After explicit Type 100 / COT 6 / QOI 20: ' + JSON.stringify(actual)); + socket.end(); + const [code] = await exit; + assert.equal(code, 0, errors + output); + assert.equal(errors, ''); + console.log(output.trim()); + } finally { + if (socket) socket.destroy(); + if (child.exitCode === null && child.pid) child.kill(); + } +} +main().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/IEC60870-Slave/104/business/IEC104_Command.c b/IEC60870-Slave/104/business/IEC104_Command.c new file mode 100644 index 00000000..a21f47bc --- /dev/null +++ b/IEC60870-Slave/104/business/IEC104_Command.c @@ -0,0 +1,1267 @@ +/* + * 模块职责:IEC104单点/双点遥控(45/46),处理校验、选择、执行、撤销、期限及协议应答。 + * 数据来源:Slave交付连接/启停通知和I帧;应用通过纯值业务事件显式决定、启动和结束操作。 + * 本模块不维护设备点表、不直接控制硬件、不创建线程,也不保存应用回调或申请堆内存。 + * + * 状态主线:SELECT_WAIT -> SELECTED -> EXECUTE_WAIT -> EXECUTING -> ENDED; + * 允许直接执行时从EXECUTE_WAIT开始,拒绝、撤销、超时及会话失效可提前进入ENDED。 + * Accept消费工作事件后最多调用一次动作;Report_Result停止设备执行期限;Complete关闭返回信息阶段。 + * ENDED只表示协议交互结束,不保证物理输出已停止,也不立即代表事务槽可以复用。 + * + * 存储与所有权:一个事务槽保存工作请求、业务事件和必要回复,独立短应答使用固定池。 + * 模块借用Slave;事务持有一个站点引用直到槽释放;独立短应答只保留会话身份和原文字节。 + * 应用若跨循环持有站点上下文或异步设备资源,须提前取得自己的引用,并自行维护设备忙状态。 + * FINISHED已消费(或无需交付)且事务自身回复全部交接/取消,才归还事务槽及模块站点引用。 + * + * 发送顺序:事务确认、终止、撤销/不匹配回复与独立短应答共用递增序号,只有最早者可提交。 + * 返回信息不在模块缓存,先等待模块FIFO清空,再通过原连接的Try接口固定帧序。 + * 发送OK只是发送层接手,不等于socket写完或主站ACK;WAIT未接手,只可重试发送,不能重做动作。 + * + * 执行约束:全部接口与Slave在同一事件循环串行调用;修改入口在动作回调期间拒绝重入。 + * Handle_Event按FIFO消费通知,不受后续未读通知的门禁阻挡;动作和发送先查会话,再判期限。 + * FINISHED可在离线时继续读取;取消不回滚设备,实例/事务/事件身份阻止迟到调用操作新事务。 + */ +#include "IEC104_Command.h" +#include "IEC104_TcpServer_Config.h" +#include "IEC104_Net.h" +#include + +#ifndef IEC104_COMMAND_CAPACITY +/* 实例数默认与TCP服务池一致;每个Slave至多绑定一个遥控实例。 */ +#define IEC104_COMMAND_CAPACITY IEC104_TCP_SERVER_CAPACITY +#endif +#ifndef IEC104_COMMAND_REPLY_CAPACITY +/* 独立短回复容量;事务自己的必要确认、终止及撤销回复不占用这些槽。 */ +#define IEC104_COMMAND_REPLY_CAPACITY 4U +#endif +_Static_assert(IEC104_COMMAND_CAPACITY > 0U, "Command needs instance storage"); +_Static_assert(IEC104_COMMAND_REPLY_CAPACITY > 0U && + IEC104_COMMAND_REPLY_CAPACITY < UINT32_MAX / 2U, "Command needs bounded reply storage"); + +/* 唯一事务槽。槽地址固定,当前工作事件、历史已交付事件身份和待发应答分别记账。 */ +typedef struct { + IEC104_SlaveEventIdentity_t identity; /* 原服务实例和连接代次;不能替换为重连后的当前身份。 */ + /* request_bytes:当前选择/执行请求,供动作、事件、ACT_CON及ACT_TERM使用。 + * reply_bytes:撤销或不匹配执行请求,仅供对应短回复回显,不覆盖工作请求。 */ + uint8_t request_bytes[10], reply_bytes[10]; + IEC104_Station_t *station; /* 本事务Acquire的原站点;删除注册后仍持有,Reap时Release。 */ + /* transaction_id:实例内单调事务号;receive_serial:当前工作请求的原I帧接收号。 + * accepted_ms:当前请求在Handle_Event受理的时刻;timer_start_ms:当前阶段的单调计时起点。 */ + uint64_t transaction_id, receive_serial, accepted_ms, timer_start_ms; + /* event_serial:当前工作/终结事件号;accepted_event_serial:已消费工作事件号,0表示尚未消费。 + * select_event_serial/execute_event_serial:本事务两个工作事件的历史身份,供只读查询验证。 */ + uint64_t event_serial, accepted_event_serial, select_event_serial, execute_event_serial; + /* confirm_serial/term_serial/reply_serial:待交确认、终止及撤销/不匹配回复的全局FIFO号,0表示无。 */ + uint64_t confirm_serial, term_serial, reply_serial; + /* phase:事务阶段;event_type:当前事件种类;finish_reason:首次终结原因,后续取消不得覆盖。 + * result:设备执行结果,0表示尚未报告;confirm_cot/reply_cot:两类待发回复各自的传送原因。 */ + uint8_t phase, event_type, finish_reason, result, confirm_cot, reply_cot; + /* occupied:槽被占用;event_taken:当前事件已交付;ever_work_delivered:曾交付过任一工作事件。 + * finish_pending:仍欠应用一个FINISHED;该义务不能因创建新的EXECUTE事件而丢失。 */ + bool occupied, event_taken, ever_work_delivered, finish_pending; + /* select_delivered/execute_delivered:对应历史工作事件确实交付过,避免伪造查询身份。 + * selected:当前执行经过了本事务的选择匹配,直接执行时为false。 */ + bool select_delivered, execute_delivered, selected; + /* confirm_negative:当前确认P/N;confirm_try:确认是否采用不排队Try路径。 + * send_waiting:事务回复上次提交遇到背压;return_waiting:应用返回信息上次提交遇到背压。 */ + bool confirm_negative, confirm_try, send_waiting, return_waiting; + /* confirm_submitted:当前工作请求确认已被发送层接手;completed:Complete已关闭返回阶段。 + * term_submitted:ACT_TERM已被发送层接手;select_effect/execute_effect:动作可能留有设备副作用。 */ + bool confirm_submitted, completed, term_submitted, select_effect, execute_effect; +} CommandTransaction; + +/* 与事务槽分离的拒绝回复,不保存站点指针,也不产生应用业务事件。 */ +typedef struct { + IEC104_SlaveEventIdentity_t identity; /* 请求原会话身份;断线/换代清理,不能跨连接重发。 */ + uint8_t bytes[10]; /* 单个完整命令原文,VSQ已规范为1;提交时仅改COT及P/N。 */ + uint64_t serial; /* 与事务内回复共用的生成序号,决定全局FIFO位置。 */ + uint8_t cot; /* 错误原因或否定ACT_CON/DEACT_CON的COT;独立回复P/N固定为1。 */ + bool occupied, send_waiting; /* 槽占用标志;上次发送WAIT标志,用于避免need_process空转。 */ +} CommandReply; + +struct IEC104_Command { + IEC104_Slave_t *slave; /* 借用的所属服务,寿命必须覆盖本实例。 */ + IEC104_SlaveEventIdentity_t session; /* 已按FIFO消费的连接/启停通知所确定的会话身份。 */ + IEC104_CommandConfig_t config; /* Create时按值复制的配置;不借用调用方配置指针。 */ + IEC104_CommandStatus_t status; /* 累计计数及模块活动状态;调度字段由Get_Status即时计算。 */ + CommandTransaction transaction; /* 固定地址的唯一事务;占用期间所有站点的新遥控均忙拒绝。 */ + CommandReply replies[IEC104_COMMAND_REPLY_CAPACITY]; /* 独立短回复池;满时丢新、不覆盖。 */ + /* serial:进程内唯一实例号;next_transaction/next_event/next_reply:实例内身份计数器。 + * last_generation:已接手CONNECTED的最高代次,断线后保留,用于拒绝旧连接通知。 */ + uint64_t serial, next_transaction, next_event, next_reply, last_generation; + bool in_use, in_action; /* 静态实例池占用标志;同步动作回调的修改入口重入保护。 */ +}; + +/* 静态池不迁移对象、不进行堆分配;实例编号跨Destroy保留递增,旧事件不能匹配复用的池槽。 */ +static IEC104_Command_t command_pool[IEC104_COMMAND_CAPACITY]; +static uint64_t next_instance; + +/* + * 功能:验证句柄是否指向本模块当前占用的静态池槽。 + * 参数:self为待检查地址,可以是NULL或不属于池的地址。 + * 返回:归属合法且in_use为true时返回true;否则false,不修改状态。 + * 约束:先比较地址再解引用,所有公开入口据此拒绝无效句柄。 + */ +static bool Owned(const IEC104_Command_t *self) +{ + unsigned i; + for (i = 0U; i < IEC104_COMMAND_CAPACITY; ++i) { + if (self == &command_pool[i]) { return self->in_use; } + } + return false; +} + +/* + * 功能:从固定104命令布局读取24位小端信息对象地址。 + * 参数:bytes指向至少10字节的45/46 ASDU;调用方已完成长度检查。 + * 返回:0..0xFFFFFF的IOA;不判断该地址是否存在或适用于遥控。 + */ +static uint32_t Read_IOA(const uint8_t *bytes) +{ + return (uint32_t)bytes[6] | ((uint32_t)bytes[7] << 8) | ((uint32_t)bytes[8] << 16); +} + +/* + * 功能:从固定104布局读取16位小端公共地址。 + * 参数:bytes为具有完整6字节公共头的ASDU,COT/CA宽度均为2。 + * 返回:原CA数值;不查询站点注册表,不修改原文。 + */ +static uint16_t Read_CA(const uint8_t *bytes) +{ + return (uint16_t)((uint16_t)bytes[4] | ((uint16_t)bytes[5] << 8)); +} + +/* + * 功能:从事务内部保留值重建交给应用的纯值请求。 + * 参数:task为占用且持有站点引用的事务;request为非NULL输出。 + * 输出:完整初始化request,含当前原文取值、站点注册号及受理时刻。 + * 返回:无;不改变事务、不分配内存。 + * 约束:动作不能读取应用回传event中的可修改request,必须以本函数的内部值为准。 + */ +static void Copy_Request(const CommandTransaction *task, IEC104_CommandRequest_t *request) +{ + memset(request, 0, sizeof(*request)); + request->generation = task->identity.generation; + request->receive_serial = task->receive_serial; + request->station_id = IEC104_Station_Get_Id(task->station); + request->common_address = Read_CA(task->request_bytes); + request->originator_address = task->request_bytes[3]; + request->type_id = task->request_bytes[0]; + request->ioa = Read_IOA(task->request_bytes); + request->is_select = (task->request_bytes[9] & 0x80U) != 0U; + request->selected = task->selected; + request->state = task->request_bytes[9] & (request->type_id == 45U ? 1U : 3U); + request->qualifier = (task->request_bytes[9] >> 2) & 31U; + request->accepted_ms = task->accepted_ms; +} + +/* + * 功能:统计模块仍持有、尚未被发送层接手的回复数。 + * 参数:self为有效实例。 + * 返回:事务内最多三种回复与全部已占用独立短回复的总数。 + * 约束:不包括已经进入HIGH/当前发送帧的回复,也不包括应用尚未交付的返回信息。 + */ +static unsigned Pending_Replies(const IEC104_Command_t *self) +{ + const CommandTransaction *task = &self->transaction; + unsigned i, count = (task->confirm_serial != 0U ? 1U : 0U) + + (task->term_serial != 0U ? 1U : 0U) + (task->reply_serial != 0U ? 1U : 0U); + for (i = 0U; i < IEC104_COMMAND_REPLY_CAPACITY; ++i) { + if (self->replies[i].occupied) { ++count; } + } + return count; +} + +/* + * 功能:在应用清理通知与本事务发送收尾均完成后释放事务槽。 + * 参数:self为有效实例;允许事务尚未结束或已经空闲,此时无操作。 + * 输出:满足条件时Release模块自己的站点引用,并清零事务全部状态。 + * 返回:无;不清理独立短应答、不关闭连接、不回滚设备动作。 + * 约束:只有ENDED且FINISHED已读取(或无需生成)、本事务回复已全部交接(或取消), + * 才能回收;两个条件分别记账。独立短应答不借用事务内存,不阻止本事务回收。 + */ +static void Reap(IEC104_Command_t *self) +{ + CommandTransaction *task = &self->transaction; + if (!task->occupied || task->phase != IEC104_COMMAND_PHASE_ENDED || task->finish_pending || + task->confirm_serial != 0U || task->term_serial != 0U || task->reply_serial != 0U) { return; } + (void)IEC104_Slave_Release_Station(self->slave, task->station); + memset(task, 0, sizeof(*task)); +} + +/* + * 功能:结束当前应用交互,冻结首次原因并按交付历史生成一次FINISHED。 + * 参数:self为有效实例;reason为本次终结原因。 + * 输出:活动事务进入ENDED;未读工作事件被替换或移除,累计相应终结统计。 + * 返回:无;不会自行丢弃待发回复,末尾仅尝试Reap。 + * 约束:是否欠FINISHED取决于ever_work_delivered,而非当前事件是否取走。 + * 已经ENDED时保持原原因和通知,不因后续STOP/Cancel重复交付。 + * 结束应用交互不等于释放槽,仍须由Reap同时核对FINISHED与本事务回复的收尾条件。 + */ +static void Finish(IEC104_Command_t *self, IEC104_CommandFinishReason_t reason) +{ + CommandTransaction *task = &self->transaction; + if (!task->occupied || task->phase == IEC104_COMMAND_PHASE_ENDED) { Reap(self); return; } + task->phase = IEC104_COMMAND_PHASE_ENDED; + task->finish_reason = (uint8_t)reason; + task->event_type = IEC104_COMMAND_FINISHED; + task->event_taken = false; + task->finish_pending = task->ever_work_delivered; + task->return_waiting = false; + /* 创建事务时为最多两个工作事件和一个FINISHED检查了序号余量。 */ + if (task->finish_pending) { task->event_serial = ++self->next_event; } + switch (reason) { + case IEC104_COMMAND_FINISH_COMPLETED: ++self->status.completed; break; + case IEC104_COMMAND_FINISH_EXECUTION_FAILED: ++self->status.execution_failed; break; + case IEC104_COMMAND_FINISH_ACTION_FAILED: ++self->status.action_failed; break; + case IEC104_COMMAND_FINISH_MISMATCH: ++self->status.mismatches; break; + case IEC104_COMMAND_FINISH_DEACTIVATED: ++self->status.deactivated; break; + case IEC104_COMMAND_FINISH_SELECT_TIMEOUT: ++self->status.select_timeouts; break; + case IEC104_COMMAND_FINISH_DECISION_TIMEOUT: ++self->status.decision_timeouts; break; + case IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT: ++self->status.execute_timeouts; break; + case IEC104_COMMAND_FINISH_CANCELLED: + case IEC104_COMMAND_FINISH_STOPPED: + case IEC104_COMMAND_FINISH_DISCONNECTED: + case IEC104_COMMAND_FINISH_CONNECTION_CHANGED: + case IEC104_COMMAND_FINISH_SEND_ERROR: + case IEC104_COMMAND_FINISH_SERVICE_ERROR: ++self->status.cancelled; break; + default: break; + } + Reap(self); +} + +/* + * 功能:取消模块仍持有的全部回复,并终结尚未结束的事务。 + * 参数:self为有效实例;reason交给Finish记录,仅首次终结生效。 + * 输出:事务回复序号及独立回复池清空;欠应用的FINISHED仍保留。 + * 返回:无;不修改发送层已经接手的帧,也不负责设置模块激活标志。 + */ +static void Cancel_All(IEC104_Command_t *self, IEC104_CommandFinishReason_t reason) +{ + CommandTransaction *task = &self->transaction; + task->confirm_serial = task->term_serial = task->reply_serial = 0U; + task->send_waiting = false; + memset(self->replies, 0, sizeof(self->replies)); + Finish(self, reason); +} + +/* + * 功能:动作或发送之前核对Slave会话,并按失效原因清理模块状态。 + * 参数:self为有效实例,其session来自已经消费的连接通知。 + * 返回:READY可推进;WAIT_EVENTS须先读空通知;STALE表示旧事务不能继续;ERROR表示查询失败。 + * 输出:刷新events_pending;失效/错误时取消未提交回复、终结事务并清active。 + * 约束:WAIT_EVENTS优先且不取消事务。Handle_Event消费通知时不能调用本门禁, + * 否则队列还有后续通知时,当前已经出队的通知会被阻挡或遗漏。 + */ +static IEC104_SlaveRequestState_t Check_Session(IEC104_Command_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) { + Cancel_All(self, IEC104_COMMAND_FINISH_SERVICE_ERROR); + self->status.active = false; + } else if (state != IEC104_SLAVE_REQUEST_WAIT_EVENTS && + (!self->status.active || state != IEC104_SLAVE_REQUEST_READY)) { + Cancel_All(self, IEC104_COMMAND_FINISH_CONNECTION_CHANGED); + self->status.active = false; + state = IEC104_SLAVE_REQUEST_STALE; + } + return state; +} + +/* + * 功能:根据当前事务阶段选择唯一生效的时长限制。 + * 参数:self为有效实例。 + * 返回:决定、选择或执行期限的毫秒值;无计时阶段、已报告结果、回调中或配置禁用时为0。 + * 约束:只读,不判定到期;0不屏蔽Clock_Reversed单独检查的单调时钟异常。 + */ +static uint32_t Timer_Limit(const IEC104_Command_t *self) +{ + const CommandTransaction *task = &self->transaction; + if (!task->occupied || self->in_action) { return 0U; } + switch (task->phase) { + case IEC104_COMMAND_PHASE_SELECT_WAIT: + case IEC104_COMMAND_PHASE_EXECUTE_WAIT: return self->config.decision_timeout_ms; + case IEC104_COMMAND_PHASE_SELECTED: return self->config.select_timeout_ms; + case IEC104_COMMAND_PHASE_EXECUTING: + return task->result == 0U ? self->config.execute_timeout_ms : 0U; + default: return 0U; + } +} + +/* + * 功能:计算当前阶段距离启用的期限还剩多少毫秒。 + * 参数:task提供timer_start_ms;limit为非零期限;now是本次统一采样的单调时间。 + * 返回:剩余毫秒;恰好到期、已经超期或时钟倒退均为0。 + * 约束:先比较64位差值再截窄,不使用可能溢出的起点加期限计算。 + */ +static uint32_t Remaining(const CommandTransaction *task, uint32_t limit, uint64_t now) +{ + if (now < task->timer_start_ms || now - task->timer_start_ms >= limit) { return 0U; } + return limit - (uint32_t)(now - task->timer_start_ms); +} + +/* + * 功能:检查仍等待决定、执行命令或设备结果的阶段是否遇到单调时间倒退。 + * 参数:self为有效实例;now为本次单调时钟采样。 + * 返回:等待阶段中now早于计时起点时为true;空闲、ENDED、已报告结果或回调中为false。 + * 约束:期限0仅关闭经过时长限制,不允许等待状态依赖倒退的时钟继续执行。 + */ +static bool Clock_Reversed(const IEC104_Command_t *self, uint64_t now) +{ + const CommandTransaction *task = &self->transaction; + return !self->in_action && task->occupied && now < task->timer_start_ms && + (task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT || task->phase == IEC104_COMMAND_PHASE_SELECTED || + task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT || + (task->phase == IEC104_COMMAND_PHASE_EXECUTING && task->result == 0U)); +} + +/* + * 功能:分配模块内所有回复共用的递增FIFO序号。 + * 参数:self为有效实例。 + * 返回:成功为非零新序号;耗尽返回0,并以SERVICE_ERROR取消事务及回复、清active。 + * 约束:0是未排队哨兵;计数不回绕,避免新回复与历史排序身份重合。 + */ +static uint64_t Reply_Serial(IEC104_Command_t *self) +{ + if (self->next_reply == UINT64_MAX) { + Cancel_All(self, IEC104_COMMAND_FINISH_SERVICE_ERROR); + self->status.active = false; + return 0U; + } + return ++self->next_reply; +} + +/* + * 功能:使用事务自己的存储生成一条确认,分配全局FIFO位置。 + * 参数:self持有当前事务;cause为确认/错误COT;negative为P/N;try_send选择不排队Try路径。 + * 输出:成功时写确认序号和策略,清confirm_submitted及send_waiting。 + * 返回:true已准备确认;false为序号耗尽,Reply_Serial已执行服务错误清理。 + * 约束:动作调用前必须准备必要确认资源,不能执行后再依赖独立回复池空位。 + */ +static bool Set_Confirm(IEC104_Command_t *self, uint8_t cause, bool negative, bool try_send) +{ + CommandTransaction *task = &self->transaction; + uint64_t serial = Reply_Serial(self); + if (serial == 0U) { return false; } + task->confirm_serial = serial; + task->confirm_cot = cause; + task->confirm_negative = negative; + task->confirm_try = try_send; + task->confirm_submitted = false; + task->send_waiting = false; + return true; +} + +/* + * 功能:按统一采样时刻执行当前阶段的到期转换,不调用应用动作。 + * 参数:self为有效实例;now同时用于回退检测和期限比较。 + * 输出:决定到期生成否定确认;选择到期丢弃本模块尚持有的选择确认; + * 执行到期保留原肯定确认但不发终止;对应事务进入ENDED并按需产生FINISHED。 + * 返回:无;未到期且时钟未回退时不改变状态,时钟回退单独计clock_errors。 + * 期限为0时仍执行回退检测,不将失效的单调时钟视为无限期有效。 + * 约束:动作/提交入口先通过会话门禁再调用;有效I帧按FIFO受理时也须在匹配前调用, + * 防止迟到执行先停掉选择计时、使已过期选择重新生效。 + */ +static void Expire(IEC104_Command_t *self, uint64_t now) +{ + CommandTransaction *task = &self->transaction; + uint32_t limit = Timer_Limit(self); + bool reversed = Clock_Reversed(self, now); + if (!reversed && (limit == 0U || Remaining(task, limit, now) != 0U)) { return; } + if (reversed) { ++self->status.clock_errors; } + switch (task->phase) { + case IEC104_COMMAND_PHASE_SELECT_WAIT: + case IEC104_COMMAND_PHASE_EXECUTE_WAIT: + if (Set_Confirm(self, IEC60870_ASDU_COT_ACTIVATION_CON, true, false)) { + Finish(self, IEC104_COMMAND_FINISH_DECISION_TIMEOUT); + } + break; + case IEC104_COMMAND_PHASE_SELECTED: + task->confirm_serial = 0U; /* 只能撤回模块尚持有的确认。 */ + Finish(self, IEC104_COMMAND_FINISH_SELECT_TIMEOUT); + break; + case IEC104_COMMAND_PHASE_EXECUTING: + /* 动作已启动,不能因结果超时把原肯定确认改成否定。 */ + Finish(self, IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT); + break; + default: break; + } +} + +/* + * 功能:识别发送入口处理新网络输入后遇到的正常STOP,保留回复等待通知收尾。 + * 参数:self为有效实例;identity是本次提交的原身份;submitted是发送入口返回值。 + * 返回:仅ERROR且同一连接健康、已停用、仍有未读通知时为true;其他错误及FATAL均false。 + * 输出:匹配正常STOP时置events_pending,不计发送背压次数,不直接改变事务阶段。 + * 约束:FATAL可能已经写出字节,绝不能借此入口解释成可重试WAIT。 + */ +static bool Defer_For_Stop(IEC104_Command_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; +} + +/* 队首种类:NONE无待发;CONFIRM/TERM/REPLY对应事务内三份排序记录;SHORT对应独立回复池。 + * 三种事务内回复和独立回复共享一个序号域,不另建可溢出的指针FIFO。 */ +enum { COMMAND_HEAD_NONE, COMMAND_HEAD_CONFIRM, COMMAND_HEAD_TERM, COMMAND_HEAD_REPLY, COMMAND_HEAD_SHORT }; +/* + * 功能:只读查找所有待提交回复中生成序号最小的一项。 + * 参数:self为有效实例;slot为非NULL输出,仅队首为SHORT时写入独立槽下标。 + * 返回:COMMAND_HEAD_*种类;没有待发回复时返回NONE。 + * 约束:不弹出、不预留发送资源;序号0表示无回复,不能参与排序。 + */ +static unsigned Reply_Head(const IEC104_Command_t *self, unsigned *slot) +{ + const CommandTransaction *task = &self->transaction; + uint64_t first = 0U; + unsigned head = COMMAND_HEAD_NONE, i; + if (task->confirm_serial != 0U) { first = task->confirm_serial; head = COMMAND_HEAD_CONFIRM; } + if (task->term_serial != 0U && (first == 0U || task->term_serial < first)) { + first = task->term_serial; head = COMMAND_HEAD_TERM; + } + if (task->reply_serial != 0U && (first == 0U || task->reply_serial < first)) { + first = task->reply_serial; head = COMMAND_HEAD_REPLY; + } + for (i = 0U; i < IEC104_COMMAND_REPLY_CAPACITY; ++i) { + if (self->replies[i].occupied && (first == 0U || self->replies[i].serial < first)) { + first = self->replies[i].serial; head = COMMAND_HEAD_SHORT; *slot = i; + } + } + return head; +} + +/* + * 功能:在Complete关闭返回阶段后,检查协议收尾是否已经全部交接。 + * 参数:self为有效实例。 + * 输出:EXECUTING且completed、确认已交接、无确认/终止待发时,按设备结果产生 + * FINISHED(COMPLETED/EXECUTION_FAILED);其余状态只尝试Reap。 + * 返回:无;既不发送报文也不回调应用。 + * 约束:无ACT_TERM策略仍须等待原确认交接;发送交接不表示主站ACK或设备成功。 + */ +static void Complete_If_Sent(IEC104_Command_t *self) +{ + CommandTransaction *task = &self->transaction; + if (task->occupied && task->phase == IEC104_COMMAND_PHASE_EXECUTING && task->completed && + task->confirm_submitted && task->confirm_serial == 0U && task->term_serial == 0U) { + Finish(self, task->result == IEC104_COMMAND_RESULT_SUCCEEDED ? + IEC104_COMMAND_FINISH_COMPLETED : IEC104_COMMAND_FINISH_EXECUTION_FAILED); + } + Reap(self); +} + +/* + * 功能:按全局FIFO向发送层交付至多一条回复。 + * 参数:self为有效实例;内部先核对会话,再读取当前队首。 + * 输出:OK后移除对应待交标记并计数,随后检查完成/释放;WAIT只保留原回复; + * 正常STOP先让出,其他发送错误终结事务并清回复池。 + * 返回:1表示有一条回复已交接;0表示无回复、等待或已处理发送失败; + * -1表示服务查询/组包错误,调用方应报告本轮推进失败。 + * 约束:局部10字节副本承载COT/P/N改写,原工作请求和撤销回显来源不会被修改。 + */ +static int Submit_Head(IEC104_Command_t *self) +{ + CommandTransaction *task = &self->transaction; + const IEC104_SlaveEventIdentity_t *identity; + const uint8_t *source; + uint8_t bytes[10], cause; + IEC60870_Asdu_t asdu; + IEC104_TcpSubmitResult_t submitted; + IEC104_SlaveRequestState_t state = Check_Session(self); + unsigned slot = 0U, head; + bool negative, try_send; + if (state == IEC104_SLAVE_REQUEST_ERROR) { return -1; } + if (state != IEC104_SLAVE_REQUEST_READY) { return 0; } + head = Reply_Head(self, &slot); + if (head == COMMAND_HEAD_NONE) { return 0; } + identity = &task->identity; + source = task->request_bytes; + cause = IEC60870_ASDU_COT_ACTIVATION_TERMINATION; + negative = false; + try_send = true; + /* 默认分支为ACT_TERM;即使配置允许失败后终止,P/N=0也只表示流程结束。 + * 普通确认可进入HIGH,执行后确认和终止必须走Try,防止越过既有过程信息。 */ + if (head == COMMAND_HEAD_CONFIRM) { + cause = task->confirm_cot; negative = task->confirm_negative; try_send = task->confirm_try; + } else if (head == COMMAND_HEAD_REPLY) { + source = task->reply_bytes; cause = task->reply_cot; + negative = cause != IEC60870_ASDU_COT_DEACTIVATION_CON; try_send = false; + } else if (head == COMMAND_HEAD_SHORT) { + identity = &self->replies[slot].identity; + source = self->replies[slot].bytes; cause = self->replies[slot].cot; + negative = true; try_send = false; + } + memcpy(bytes, source, sizeof(bytes)); + if (IEC60870_Asdu_Create_From_Buffer(&asdu, IEC104_Slave_Get_Application_Layer_Parameters(self->slave), + bytes, sizeof(bytes)) != IEC60870_ASDU_OK || + IEC60870_Asdu_Set_COT(&asdu, cause) != IEC60870_ASDU_OK || + IEC60870_Asdu_Set_Negative(&asdu, negative) != IEC60870_ASDU_OK) { + Cancel_All(self, IEC104_COMMAND_FINISH_SERVICE_ERROR); + return -1; + } + submitted = try_send ? IEC104_Slave_Try_Send_Response(self->slave, identity, &asdu) : + IEC104_Slave_Send_Response(self->slave, identity, &asdu); + if (submitted == IEC104_TCP_SUBMIT_OK) { + /* 发送层已复制/接手字节,立即撤销模块待发标记;不能等ACK再重复提交。 */ + ++self->status.submitted; + if (negative) { ++self->status.negative_submitted; } + if (head == COMMAND_HEAD_SHORT) { memset(&self->replies[slot], 0, sizeof(self->replies[slot])); } + else { + task->send_waiting = false; + if (head == COMMAND_HEAD_CONFIRM) { task->confirm_serial = 0U; task->confirm_submitted = true; } + else if (head == COMMAND_HEAD_TERM) { task->term_serial = 0U; task->term_submitted = true; } + else { task->reply_serial = 0U; } + } + Complete_If_Sent(self); + return 1; + } + if (submitted == IEC104_TCP_SUBMIT_WAIT) { + /* WAIT未接手,不改变FIFO号;仅标记等待,使状态查询不会要求无网络推进的忙轮询。 */ + if (head == COMMAND_HEAD_SHORT) { self->replies[slot].send_waiting = true; } + else { task->send_waiting = true; } + ++self->status.waits; + return 0; + } + if (Defer_For_Stop(self, identity, submitted)) { return 0; } + Cancel_All(self, IEC104_COMMAND_FINISH_SEND_ERROR); + self->status.active = false; + return 0; +} + +/* + * 功能:为校验失败、实例忙或不允许撤销的请求保存一条独立否定回复。 + * 参数:self为有效实例;identity为原会话身份;data至少包含完整10字节命令;cause为回复COT。 + * 输出:有空槽时复制原文、规范VSQ并分配FIFO号;池满计overloads/dropped_requests。 + * 返回:true表示已处理(含丢新或发送等待);false表示序号/服务/组包内部错误。 + * 约束:此前没有模块回复时才尝试当场提交;新短回复不能抢占事务必要确认存储, + * 也不能覆盖旧回复或因过载断开连接。 + */ +static bool Short_Reply(IEC104_Command_t *self, const IEC104_SlaveEventIdentity_t *identity, + const uint8_t *data, uint8_t cause) +{ + unsigned slot; + bool empty = Pending_Replies(self) == 0U; + uint64_t serial; + for (slot = 0U; slot < IEC104_COMMAND_REPLY_CAPACITY; ++slot) { + if (!self->replies[slot].occupied) { break; } + } + if (slot == IEC104_COMMAND_REPLY_CAPACITY) { + ++self->status.overloads; + ++self->status.dropped_requests; + return true; + } + serial = Reply_Serial(self); + if (serial == 0U) { return false; } + self->replies[slot].identity = *identity; + memcpy(self->replies[slot].bytes, data, sizeof(self->replies[slot].bytes)); + self->replies[slot].bytes[1] = 1U; + self->replies[slot].serial = serial; + self->replies[slot].cot = cause; + self->replies[slot].occupied = true; + self->replies[slot].send_waiting = false; + return !empty || Submit_Head(self) >= 0; +} + +/* + * 功能:在已占用的事务槽中建立SELECT或EXECUTE工作事件。 + * 参数:self已持有事务/站点;data为通过校验的命令原文;receive_serial为原I帧编号; + * now为受理时刻;selected说明本执行是否已经匹配本事务选择。 + * 输出:替换工作原文,生成新事件号,重置当前交付/消费和确认标志,开始决定期限并计accepted。 + * 返回:无;不调用应用、不提交确认。 + * 约束:初始事务或匹配执行才可调用;保留事务身份、选择副作用和ever_work_delivered, + * 因此SELECT已交付而EXECUTE尚未交付时,取消仍须通知应用清理选择资源。 + */ +static void Work_Event(IEC104_Command_t *self, const uint8_t *data, uint64_t receive_serial, + uint64_t now, bool selected) +{ + CommandTransaction *task = &self->transaction; + memcpy(task->request_bytes, data, sizeof(task->request_bytes)); + task->request_bytes[1] = 1U; + task->receive_serial = receive_serial; + task->accepted_ms = task->timer_start_ms = now; + task->event_type = (data[9] & 0x80U) != 0U ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE; + task->phase = task->event_type == IEC104_COMMAND_SELECT ? + IEC104_COMMAND_PHASE_SELECT_WAIT : IEC104_COMMAND_PHASE_EXECUTE_WAIT; + task->event_serial = ++self->next_event; + task->accepted_event_serial = 0U; + if (task->event_type == IEC104_COMMAND_SELECT) { task->select_event_serial = task->event_serial; } + else { task->execute_event_serial = task->event_serial; } + task->event_taken = false; + task->selected = selected; + task->confirm_submitted = false; + task->send_waiting = false; + ++self->status.accepted; +} + +/* + * 功能:使用事务自带回复存储接受撤销或处理同点执行不匹配,然后结束应用交互。 + * 参数:self持有待终结事务;data为本次撤销/不匹配请求;cause为9或7;reason为终结原因。 + * 输出:丢弃模块尚持有的旧确认/终止,保存本次请求回显及新的FIFO号,生成必要FINISHED。 + * 返回:true已处理(可仍等待发送);false为序号/服务/组包错误。 + * 约束:不依赖独立回复池空闲;原工作请求不覆盖,已提交给发送层的旧确认无法撤回。 + */ +static bool End_With_Reply(IEC104_Command_t *self, const uint8_t *data, uint8_t cause, + IEC104_CommandFinishReason_t reason) +{ + CommandTransaction *task = &self->transaction; + uint64_t serial = Reply_Serial(self); + if (serial == 0U) { return false; } + task->confirm_serial = task->term_serial = 0U; + memcpy(task->reply_bytes, data, sizeof(task->reply_bytes)); + task->reply_bytes[1] = 1U; + task->reply_cot = cause; + task->reply_serial = serial; + task->send_waiting = false; + Finish(self, reason); + return Submit_Head(self) >= 0; +} + +/* + * 功能:受理一个当前代次的I帧,完成遥控校验、撤销、选择执行匹配和新事务建立。 + * 参数:self为有效实例;event是Slave交付且归属已核验的事件;identity为其原会话身份。 + * 输出:有效命令生成工作事件或改变匹配事务;可回复错误生成否定;决定期间同点激活静默丢弃计数。 + * 返回:IGNORED为非本模块类型或当前不能接手;HANDLED包括保存、拒绝和丢弃; + * ERROR为内部序号、引用或发送准备错误,调用方不能继续按未知类型兜底。 + * 约束:先确认完整单对象,再按COT、CA、IOA、对象字段的优先级判错; + * 完整10字节可以规范回显,截短或多对象不能截取/补零伪造单命令。 + */ +static IEC104_CommandEventResult_t Request(IEC104_Command_t *self, const IEC104_SlaveEvent_t *event, + const IEC104_SlaveEventIdentity_t *identity) +{ + CommandTransaction *task = &self->transaction; + IEC60870_AsduInfo_t info; + IEC104_Station_t *station; + const uint8_t *data; + uint16_t length; + uint8_t cause = 0U, state; + uint32_t ioa; + uint64_t now; + bool same_point, is_select; + 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] != 45U && data[0] != 46U) || + (self->config.type_mask & IEC104_COMMAND_TYPE_BIT(data[0])) == 0U || + !IEC60870_Parse_ASDU_Header(event->asdu.parameters, data, length, &info)) { + return IEC104_COMMAND_EVENT_IGNORED; + } + now = IEC104_Net_Now_Ms(); + /* 已经按FIFO消费到当前I帧;先转换期限,随后匹配不得重新激活已过期选择。 */ + Expire(self, now); + if (info.object_count != 1U || info.information_objects_len < 4U) { + ++self->status.rejected; + return IEC104_COMMAND_EVENT_HANDLED; + } + ioa = Read_IOA(data); + station = IEC104_Slave_Get_Station(self->slave, info.common_address); + state = data[9] & 3U; + is_select = (data[9] & 0x80U) != 0U; + if (info.cause_of_transmission != IEC60870_ASDU_COT_ACTIVATION && + info.cause_of_transmission != IEC60870_ASDU_COT_DEACTIVATION) { + cause = IEC60870_ASDU_COT_UNKNOWN_CAUSE; + } else if (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 != 10U || info.is_negative_confirm || info.is_test || + (data[0] == 45U ? (data[9] & 2U) != 0U : (state == 0U || state == 3U))) { + cause = info.cause_of_transmission == IEC60870_ASDU_COT_DEACTIVATION ? + IEC60870_ASDU_COT_DEACTIVATION_CON : IEC60870_ASDU_COT_ACTIVATION_CON; + } + if (cause != 0U) { + ++self->status.rejected; + return Short_Reply(self, identity, data, cause) ? IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + same_point = task->occupied && task->identity.generation == identity->generation && + IEC104_Station_Get_Id(task->station) == IEC104_Station_Get_Id(station) && + Read_IOA(task->request_bytes) == ioa; + /* 站点注册编号参与匹配,同CA删除再注册不能接管旧选择;撤销不要求状态/QU相同。 */ + if (info.cause_of_transmission == IEC60870_ASDU_COT_DEACTIVATION) { + if (same_point && task->request_bytes[0] == data[0] && task->request_bytes[3] == data[3] && + (task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT || task->phase == IEC104_COMMAND_PHASE_SELECTED || + task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT)) { + return End_With_Reply(self, data, IEC60870_ASDU_COT_DEACTIVATION_CON, + IEC104_COMMAND_FINISH_DEACTIVATED) ? IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + ++self->status.rejected; + return Short_Reply(self, identity, data, IEC60870_ASDU_COT_DEACTIVATION_CON) ? + IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + if (same_point && (task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT || + task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT)) { + /* 原请求尚待应用决定,先回忙否定可能被主站误认为原请求的结果。 + * 只对通过上述校验的同会话/站点注册号/IOA激活静默丢弃;不要求命令字段相同。 + * 不占应答槽,不覆盖原请求/事件,不刷新决定期限;撤销已在前面独立处理。 */ + ++self->status.dropped_requests; + return IEC104_COMMAND_EVENT_HANDLED; + } + /* SELECTED阶段选择确认尚未交接时忙拒绝;交接后同点执行先匹配,其余新激活不影响选择。 */ + if (same_point && !is_select && task->phase == IEC104_COMMAND_PHASE_SELECTED && task->confirm_submitted) { + if (task->request_bytes[0] != data[0] || task->request_bytes[3] != data[3] || + (task->request_bytes[9] & 0x7FU) != (data[9] & 0x7FU)) { + ++self->status.rejected; + return End_With_Reply(self, data, IEC60870_ASDU_COT_ACTIVATION_CON, + IEC104_COMMAND_FINISH_MISMATCH) ? IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + Work_Event(self, data, event->transport.data.i_frame.receive_serial, now, true); + return IEC104_COMMAND_EVENT_HANDLED; + } + if (task->occupied || (!is_select && (self->config.select_required_mask & IEC104_COMMAND_TYPE_BIT(data[0])) != 0U)) { + /* ENDED但尚欠FINISHED/事务回复也算忙;其他点的新请求不能破坏当前选择。 */ + ++self->status.rejected; + if (task->occupied) { ++self->status.busy_rejected; } + return Short_Reply(self, identity, data, IEC60870_ASDU_COT_ACTIVATION_CON) ? + IEC104_COMMAND_EVENT_HANDLED : IEC104_COMMAND_EVENT_ERROR; + } + if (self->next_transaction == UINT64_MAX || self->next_event > UINT64_MAX - 3U) { + /* 每事务最多SELECT、EXECUTE、FINISHED三次生成,开始前保留足够事件身份余量。 */ + ++self->status.overloads; + ++self->status.dropped_requests; + return IEC104_COMMAND_EVENT_HANDLED; + } + if (IEC104_Slave_Acquire_Station(self->slave, station) != IEC104_STATION_OK) { return IEC104_COMMAND_EVENT_ERROR; } + /* Acquire成功后才占事务槽;独立短回复中的数值CA不借用这个引用。 */ + memset(task, 0, sizeof(*task)); + task->occupied = true; + task->identity = *identity; + task->station = station; + task->transaction_id = ++self->next_transaction; + Work_Event(self, data, event->transport.data.i_frame.receive_serial, now, false); + return IEC104_COMMAND_EVENT_HANDLED; +} + +/* + * 功能:填写第一版遥控默认配置,不创建任何运行对象。 + * 参数:config为输出结构;NULL时无操作。 + * 输出:启用45/46且默认均先选择,选择/决定/执行期限为30000/15000/60000毫秒, + * 成功发送ACT_TERM,失败默认不发送ACT_TERM。 + * 返回:无;调用方可在Create之前修改配置。 + */ +void IEC104_Command_Config_Default(IEC104_CommandConfig_t *config) +{ + if (config == NULL) { return; } + memset(config, 0, sizeof(*config)); + config->type_mask = IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(46U); + config->select_required_mask = config->type_mask; + config->select_timeout_ms = 30000U; + config->decision_timeout_ms = 15000U; + config->execute_timeout_ms = 60000U; + config->send_act_term = true; +} + +/* + * 功能:从静态池创建一个与Slave独占绑定的遥控实例。 + * 参数:slave为有效服务句柄,寿命覆盖本实例;config为非NULL且按值复制的配置。 + * 返回:成功为池内不透明句柄;类型位/期限配置非法、重复绑定、池满或实例号耗尽返回NULL。 + * 输出:成功时初始化空事务、空回复池和零统计,不建立网络监听,也不调用应用代码。 + * 约束:必须早于消费Slave通知创建;select_required_mask须为type_mask子集, + * 当前仅允许45/46位,后续修改调用方config不会影响实例。 + */ +IEC104_Command_t *IEC104_Command_Create(IEC104_Slave_t *slave, const IEC104_CommandConfig_t *config) +{ + const uint16_t supported = IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(46U); + IEC104_Command_t *free_slot = NULL; + unsigned i; + if (config == NULL || config->type_mask == 0U || (config->type_mask & (uint16_t)~supported) != 0U || + (config->select_required_mask & (uint16_t)~config->type_mask) != 0U || config->select_timeout_ms == 0U || + next_instance == UINT64_MAX || IEC104_Slave_Get_Application_Layer_Parameters(slave) == NULL) { return NULL; } + for (i = 0U; i < IEC104_COMMAND_CAPACITY; ++i) { + if (command_pool[i].in_use && command_pool[i].slave == slave) { return NULL; } + if (!command_pool[i].in_use && free_slot == NULL) { free_slot = &command_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_instance; + free_slot->in_use = true; + return free_slot; +} + +/* + * 功能:归还空闲实例的静态池槽。 + * 参数:self为待销毁句柄。 + * 返回:true成功;无效句柄、动作回调中、事务仍占用或独立回复未清空时false。 + * 输出:成功清零实例;不隐式Cancel、不读取FINISHED、不关闭Slave。 + * 约束:先显式取消并完成事件/应答清理;成功后不得继续使用旧句柄。 + */ +bool IEC104_Command_Destroy(IEC104_Command_t *self) +{ + if (!Owned(self) || self->in_action || self->transaction.occupied || Pending_Replies(self) != 0U) { return false; } + memset(self, 0, sizeof(*self)); + return true; +} + +/* + * 功能:本地取消事务和模块尚未交接的回复,保留必要的应用清理通知。 + * 参数:self为实例句柄;无效句柄或动作回调中调用时无操作。 + * 输出:活动事务以CANCELLED结束,所有独立回复丢弃;会话激活状态保持,后续仍可接新请求。 + * 返回:无;不撤回发送层已接手的字节,不回滚输出;已ENDED事务不改首次终结原因。 + */ +void IEC104_Command_Cancel(IEC104_Command_t *self) +{ + if (Owned(self) && !self->in_action) { Cancel_All(self, IEC104_COMMAND_FINISH_CANCELLED); } +} + +/* + * 功能:按Slave FIFO顺序分派当前通知或I帧,不调用应用动作。 + * 参数:self为实例;event须来自所属Slave的NextEvent或合法Copy_Event副本。 + * 返回:HANDLED包括会话通知和已识别遥控请求;IGNORED为旧代次/未接手事件; + * ERROR为归属、实例、参数或重入错误,不可继续生成未知类型兜底回复。 + * 输出:连接/启停通知更新会话并按原因取消旧事务;I帧交Request处理。 + * 约束:状态通知先处理,不能统一套Check_Request;后续未出队通知不应阻挡当前通知消费。 + */ +IEC104_CommandEventResult_t IEC104_Command_Handle_Event(IEC104_Command_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_COMMAND_EVENT_ERROR; + } + self->session.slave = identity.slave; + self->session.instance_id = identity.instance_id; + if (event->transport.type == IEC104_TCP_EVENT_CONNECTED) { + /* 最高代次跨断线保留,历史CONNECTED副本不能重新绑定已经退役的连接。 */ + if (identity.generation <= self->last_generation) { return IEC104_COMMAND_EVENT_IGNORED; } + Cancel_All(self, IEC104_COMMAND_FINISH_CONNECTION_CHANGED); + self->session.generation = self->last_generation = identity.generation; + self->status.active = false; + return IEC104_COMMAND_EVENT_HANDLED; + } + if (identity.generation != self->session.generation || identity.generation == 0U) { + return IEC104_COMMAND_EVENT_IGNORED; + } + switch (event->transport.type) { + case IEC104_TCP_EVENT_ACTIVATED: + self->status.active = true; + return IEC104_COMMAND_EVENT_HANDLED; + case IEC104_TCP_EVENT_DEACTIVATED: + Cancel_All(self, IEC104_COMMAND_FINISH_STOPPED); + self->status.active = false; + return IEC104_COMMAND_EVENT_HANDLED; + case IEC104_TCP_EVENT_DISCONNECTED: + Cancel_All(self, IEC104_COMMAND_FINISH_DISCONNECTED); + self->session.generation = 0U; + self->status.active = false; + return IEC104_COMMAND_EVENT_HANDLED; + case IEC104_TCP_EVENT_I_FRAME: return Request(self, event, &identity); + default: return IEC104_COMMAND_EVENT_IGNORED; + } +} + +/* + * 功能:有界推进会话检查、期限转换及全局FIFO队首发送。 + * 参数:self为实例句柄。 + * 返回:true表示本轮已处理,可仍等待发送/应用或刚刚取消;false为参数、重入、服务或组包错误。 + * 输出:每轮至多推进2*(1+独立回复容量)步;队首WAIT或有待消费通知时立即让出。 + * 约束:不调用应用;每轮网络推进并分派通知后仍须调用,不能只在need_process为true时调用。 + */ +bool IEC104_Command_Process(IEC104_Command_t *self) +{ + unsigned step; + if (!Owned(self) || self->in_action) { return false; } + for (step = 0U; step < 2U * (1U + IEC104_COMMAND_REPLY_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) { return true; } + Expire(self, IEC104_Net_Now_Ms()); + Complete_If_Sent(self); + submitted = Submit_Head(self); + if (submitted <= 0) { return submitted == 0; } + } + return true; +} + +/* + * 功能:交付当前最早且可读取的工作/终结事件。 + * 参数:self为实例;event为非NULL输出结构,只有READY时改写。 + * 返回:READY交付一个纯值事件;EMPTY为无事件或需先消费Slave通知;ERROR为参数/重入/服务错误。 + * 输出:工作事件交付记录当前和历史交付标志;FINISHED交付清通知义务并尝试释放事务槽。 + * 约束:已有FINISHED不受离线门禁阻挡;工作事件先检查会话与期限,过期事件不会交给动作。 + * 返回FINISHED时模块引用可能已释放,应用跨循环使用站点必须提前取得自己的引用。 + */ +IEC104_CommandNextResult_t IEC104_Command_NextEvent(IEC104_Command_t *self, IEC104_CommandEvent_t *event) +{ + CommandTransaction *task; + IEC104_CommandEvent_t result; + IEC104_SlaveRequestState_t state = IEC104_SLAVE_REQUEST_READY; + if (!Owned(self) || self->in_action || event == NULL) { return IEC104_COMMAND_NEXT_ERROR; } + task = &self->transaction; + /* 已存在的FINISHED不受离线或未读通知门禁阻挡;它只清理已结束的协议交互。 */ + if (!task->finish_pending) { + state = Check_Session(self); + if (state == IEC104_SLAVE_REQUEST_READY) { Expire(self, IEC104_Net_Now_Ms()); } + } + if (!task->finish_pending && state == IEC104_SLAVE_REQUEST_ERROR) { return IEC104_COMMAND_NEXT_ERROR; } + if (!task->occupied || (!task->finish_pending && + (state != IEC104_SLAVE_REQUEST_READY || task->event_taken || + (task->phase != IEC104_COMMAND_PHASE_SELECT_WAIT && task->phase != IEC104_COMMAND_PHASE_EXECUTE_WAIT)))) { + return IEC104_COMMAND_NEXT_EMPTY; + } + memset(&result, 0, sizeof(result)); + result.type = (IEC104_CommandEventType_t)task->event_type; + result.instance_id = self->serial; + result.transaction_id = task->transaction_id; + result.event_serial = task->event_serial; + Copy_Request(task, &result.request); + if (task->finish_pending) { + result.reason = (IEC104_CommandFinishReason_t)task->finish_reason; + result.select_effect = task->select_effect; + result.execute_effect = task->execute_effect; + task->finish_pending = false; + } else { + /* 分开记录当前事件和事务历史:生成后续EXECUTE不能抹掉已交付SELECT的清理义务。 */ + task->ever_work_delivered = true; + if (result.type == IEC104_COMMAND_SELECT) { task->select_delivered = true; } + else { task->execute_delivered = true; } + } + task->event_taken = true; + *event = result; + Reap(self); + return IEC104_COMMAND_NEXT_READY; +} + +/* + * 功能:检查事件是否为当前事务当前阶段已经交付的那一个事件。 + * 参数:self为有效实例;event可为NULL,其他情况下仅读取身份和类型。 + * 返回:实例号、事务号、当前事件号/种类和已交付标志全部匹配时true。 + * 约束:只读,不检查会话/期限;Slave身份不能代替这里的应用事务与工作事件身份。 + */ +static bool Current_Event(const IEC104_Command_t *self, const IEC104_CommandEvent_t *event) +{ + const CommandTransaction *task = &self->transaction; + return event != NULL && task->occupied && event->instance_id == self->serial && + event->transaction_id == task->transaction_id && event->event_serial == task->event_serial && + event->type == task->event_type && task->event_taken; +} + +/* + * 功能:核对应用事件是否允许用于本类应答接口。 + * 参数:self为有效实例;event为待应答事件;work=true要求已由NextEvent取走、 + * 尚未由Accept/Reject消费的SELECT/EXECUTE, + * work=false要求已Accept且动作启动成功、仍处于EXECUTING的原EXECUTE令牌。 + * 返回:身份、交付/消费状态及阶段相符时true,否则false;不改变状态。 + * 约束:执行令牌可供报告结果、逐帧返回信息和Complete使用,各接口再检查自己的一次性条件。 + */ +static bool Reply_Stage(const IEC104_Command_t *self, const IEC104_CommandEvent_t *event, bool work) +{ + const CommandTransaction *task = &self->transaction; + if (!Current_Event(self, event)) { return false; } + if (work) { + return task->accepted_event_serial == 0U && + ((event->type == IEC104_COMMAND_SELECT && task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT) || + (event->type == IEC104_COMMAND_EXECUTE && task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT)); + } + return event->type == IEC104_COMMAND_EXECUTE && task->phase == IEC104_COMMAND_PHASE_EXECUTING && + task->accepted_event_serial == event->event_serial && task->execute_effect; +} + +/* + * 功能:统一执行应用应答入口的参数、事件、会话及期限检查。 + * 参数:self/event为调用方传回的实例和纯值事件;work选择工作决定或已启动执行的校验模式。 + * 返回:OK可继续本次应答;WAIT须先消费Slave通知且本次未消费事件;STALE为身份/阶段失效; + * ERROR为参数、重入或服务查询错误。 + * 输出:仅会话失效或期限到期时进行统一收尾;成功检查本身不消费事件、不调用动作。 + * 约束:先查会话再取一次now检查期限,之后再次校验阶段,避免到期转换后仍使用旧事件。 + */ +static IEC104_CommandReplyResult_t Check_Reply(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, bool work) +{ + IEC104_SlaveRequestState_t state; + if (!Owned(self) || self->in_action || event == NULL) { return IEC104_COMMAND_REPLY_ERROR; } + if (!Reply_Stage(self, event, work)) { return IEC104_COMMAND_REPLY_STALE; } + state = Check_Session(self); + if (state == IEC104_SLAVE_REQUEST_ERROR) { return IEC104_COMMAND_REPLY_ERROR; } + if (state == IEC104_SLAVE_REQUEST_WAIT_EVENTS) { return IEC104_COMMAND_REPLY_WAIT; } + if (state != IEC104_SLAVE_REQUEST_READY) { return IEC104_COMMAND_REPLY_STALE; } + Expire(self, IEC104_Net_Now_Ms()); + return Reply_Stage(self, event, work) ? IEC104_COMMAND_REPLY_OK : IEC104_COMMAND_REPLY_STALE; +} + +/* + * 功能:接受已交付的SELECT/EXECUTE,在事件有效时最多调用一次同步应用动作。 + * 参数:self/event为实例及原工作事件;action/context仅在本次调用借用,不保存。 + * SELECT允许action=NULL,表示逻辑选择生效而无硬件副作用;EXECUTE必须提供action。 + * 返回:OK仅表示工作交互已消费,动作可能NOT_STARTED/FAILED,确认也可能仍待发送; + * WAIT未调用动作、未消费事件;STALE不得执行;ERROR为参数/重入/服务或资源错误。 + * 输出:准备事务自带确认资源后先消费事件,再调用动作;按结果进入选择保持、执行等待结果或ENDED。 + * 约束:回调期间只允许只读查询;返回后重新采样阶段计时起点,不把回调耗时追认为决定超时。 + */ +IEC104_CommandReplyResult_t IEC104_Command_Accept(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandAction_t action, void *context) +{ + CommandTransaction *task; + IEC104_CommandRequest_t request; + IEC104_CommandActionResult_t action_result; + IEC104_CommandReplyResult_t checked; + bool selecting; + if (event != NULL && event->type == IEC104_COMMAND_EXECUTE && action == NULL) { + return IEC104_COMMAND_REPLY_ERROR; + } + checked = Check_Reply(self, event, true); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + task = &self->transaction; + selecting = task->event_type == IEC104_COMMAND_SELECT; + /* 确认资源和序号在动作前准备。事务自己的回复不会被独立短回复洪泛挤占。 */ + if (!Set_Confirm(self, IEC60870_ASDU_COT_ACTIVATION_CON, false, !selecting)) { + return IEC104_COMMAND_REPLY_ERROR; + } + Copy_Request(task, &request); + /* 先记录已消费的事件身份,回调即使经应用代码再次进入也不能重做同一动作。 */ + task->accepted_event_serial = task->event_serial; + self->in_action = true; + action_result = action == NULL ? IEC104_COMMAND_ACTION_STARTED : action(context, &request); + self->in_action = false; + /* 非法动作返回值按可能有副作用的失败处理;已消费的动作绝不重试。 */ + if (action_result != IEC104_COMMAND_ACTION_STARTED && action_result != IEC104_COMMAND_ACTION_NOT_STARTED) { + action_result = IEC104_COMMAND_ACTION_FAILED; + } + if (selecting) { task->select_effect = action != NULL && action_result != IEC104_COMMAND_ACTION_NOT_STARTED; } + else { task->execute_effect = action_result != IEC104_COMMAND_ACTION_NOT_STARTED; } + if (action_result == IEC104_COMMAND_ACTION_STARTED) { + task->phase = selecting ? IEC104_COMMAND_PHASE_SELECTED : IEC104_COMMAND_PHASE_EXECUTING; + task->timer_start_ms = IEC104_Net_Now_Ms(); + if (selecting) { ++self->status.selected; } + else { ++self->status.executed; } + } else { + task->confirm_negative = true; + task->confirm_try = !selecting && action_result == IEC104_COMMAND_ACTION_FAILED; + if (action_result == IEC104_COMMAND_ACTION_NOT_STARTED) { + ++self->status.app_rejected; + Finish(self, IEC104_COMMAND_FINISH_REJECTED); + } else { Finish(self, IEC104_COMMAND_FINISH_ACTION_FAILED); } + } + /* 提交可能接收STOP或产生故障;OK仍只描述动作交互已消费,应用继续读取FINISHED。 */ + (void)Submit_Head(self); + return IEC104_COMMAND_REPLY_OK; +} + +/* + * 功能:拒绝已交付且尚未消费的选择/执行工作事件。 + * 参数:self/event为实例和原事件;cause只接受否定ACT_CON或未知IOA两种原因。 + * 返回:OK交互已消费、否定回复曾安排并尝试提交;WAIT未消费;STALE为旧事件;ERROR为非法参数或内部错误。 + * 输出:以当前工作请求回显7/47,计app_rejected并产生FINISHED(REJECTED)。 + * 约束:仅改变协议状态,不调用设备动作;回复使用事务自己的存储,不能因独立池满丢失。 + * OK不保证回复仍在队列或已经发出:本次发送若失败,待交回复可能已被取消,须继续读取FINISHED。 + */ +IEC104_CommandReplyResult_t IEC104_Command_Reject(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandRejectCause_t cause) +{ + IEC104_CommandReplyResult_t checked; + if (cause != IEC104_COMMAND_REJECT_NEGATIVE && cause != IEC104_COMMAND_REJECT_UNKNOWN_IOA) { + return IEC104_COMMAND_REPLY_ERROR; + } + checked = Check_Reply(self, event, true); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + if (!Set_Confirm(self, cause == IEC104_COMMAND_REJECT_UNKNOWN_IOA ? + IEC60870_ASDU_COT_UNKNOWN_IOA : IEC60870_ASDU_COT_ACTIVATION_CON, true, false)) { + return IEC104_COMMAND_REPLY_ERROR; + } + self->transaction.accepted_event_serial = self->transaction.event_serial; + ++self->status.app_rejected; + Finish(self, IEC104_COMMAND_FINISH_REJECTED); + (void)Submit_Head(self); + return IEC104_COMMAND_REPLY_OK; +} + +/* + * 功能:一次性记录已启动设备动作的最终成功/失败结果,停止执行期限。 + * 参数:self/event为实例及启动成功的原EXECUTE事件;result为SUCCEEDED或FAILED。 + * 返回:OK已记录;WAIT结果未记录、先消费通知;STALE表示事务或事件已失效; + * ERROR为结果值非法、重复报告、已经Complete或参数/服务错误。 + * 输出:写入事务result;不发送确认/终止、不关闭返回信息阶段、不调用应用。 + * 约束:允许早于ACT_CON交接,设备执行时长和发送拥塞等待必须分开记账。 + */ +IEC104_CommandReplyResult_t IEC104_Command_Report_Result(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result) +{ + IEC104_CommandReplyResult_t checked; + if (result != IEC104_COMMAND_RESULT_SUCCEEDED && result != IEC104_COMMAND_RESULT_FAILED) { + return IEC104_COMMAND_REPLY_ERROR; + } + checked = Check_Reply(self, event, false); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + if (self->transaction.result != 0U || self->transaction.completed) { return IEC104_COMMAND_REPLY_ERROR; } + self->transaction.result = (uint8_t)result; + return IEC104_COMMAND_REPLY_OK; +} + +/* + * 功能:沿原事务连接直接提交一帧命令返回信息,并保持模块与传输层既有帧序。 + * 参数:self/event为实例和启动成功的EXECUTE令牌;asdu为调用方持有的完整监视ASDU。 + * 返回:OK发送层已接手,不能重交;WAIT未接手且不缓存,应用保留或重建后重试; + * STALE为事务结束/发送故障已取消;ERROR为ASDU、阶段或参数错误。 + * 正常STOP且通知尚未读取的发送ERROR先返回WAIT,保留由会话通知收尾的机会。 + * 输出:完整验证104布局、对象、类型、COT、CA及标志;OK后累计发送和返回信息计数。 + * 约束:只能在肯定确认交接之后、Complete之前;模块内有任何更早回复时也必须WAIT。 + * 返回信息不进入NORMAL、没有跨重连缓存;WAIT不取得模块内持久FIFO位置。 + */ +IEC104_CommandReplyResult_t IEC104_Command_Submit_Return_Info(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, const IEC60870_Asdu_t *asdu) +{ + CommandTransaction *task; + IEC60870_AsduInfo_t info; + const uint8_t *data; + uint16_t length; + IEC104_TcpSubmitResult_t submitted; + IEC104_CommandReplyResult_t checked = Check_Reply(self, event, false); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + task = &self->transaction; + if (task->completed || asdu == NULL || !IEC60870_AppLayerParameters_Valid(asdu->parameters) || + asdu->parameters->sizeOfCOT != 2 || asdu->parameters->sizeOfCA != 2 || asdu->parameters->sizeOfIOA != 3 || + asdu->parameters->maxSizeOfASDU > IEC104_MAX_ASDU_LEN || + IEC60870_Asdu_Get_Data(asdu, &data, &length) != IEC60870_ASDU_OK || length > IEC104_MAX_ASDU_LEN || + IEC60870_Asdu_Parse(IEC104_Slave_Get_Application_Layer_Parameters(self->slave), data, length, &info) != + IEC60870_ASDU_OK || + !((info.type_id >= 1U && info.type_id <= 21U) || (info.type_id >= 30U && info.type_id <= 40U)) || + info.object_count == 0U || + (info.cause_of_transmission != IEC60870_ASDU_COT_RETURN_INFO_REMOTE && + info.cause_of_transmission != IEC60870_ASDU_COT_SPONTANEOUS) || + info.is_negative_confirm || info.is_test != ((task->request_bytes[2] >> 7) & 1U) || + info.common_address != Read_CA(task->request_bytes)) { return IEC104_COMMAND_REPLY_ERROR; } + /* 底层Try只知道传输队列;这里还必须检查模块内更早的独立/事务应答。 */ + if (!task->confirm_submitted || Pending_Replies(self) != 0U) { return IEC104_COMMAND_REPLY_WAIT; } + submitted = IEC104_Slave_Try_Send_Response(self->slave, &task->identity, asdu); + if (submitted == IEC104_TCP_SUBMIT_WAIT) { + task->return_waiting = true; + ++self->status.waits; + return IEC104_COMMAND_REPLY_WAIT; + } + if (submitted == IEC104_TCP_SUBMIT_OK) { + task->return_waiting = false; + ++self->status.submitted; + ++self->status.return_info_submitted; + return IEC104_COMMAND_REPLY_OK; + } + if (Defer_For_Stop(self, &task->identity, submitted)) { return IEC104_COMMAND_REPLY_WAIT; } + Cancel_All(self, IEC104_COMMAND_FINISH_SEND_ERROR); + self->status.active = false; + return IEC104_COMMAND_REPLY_STALE; +} + +/* + * 功能:声明本事务不再提交返回信息,并按配置启动协议终止收尾。 + * 参数:self/event为实例及启动成功的原EXECUTE事件;设备结果必须已经Report_Result。 + * 返回:OK已关闭返回阶段,后续ACT_TERM由Process推进;WAIT未消费,先处理通知; + * STALE为事务失效;ERROR为未报告结果、重复调用或参数/内部错误。 + * 输出:必要时分配终止FIFO序号,置completed;无终止且确认已交接时可立即产生FINISHED。 + * 约束:OK不保证ACT_TERM已经发送;不能因等待终止而再次Complete或再提交返回信息。 + */ +IEC104_CommandReplyResult_t IEC104_Command_Complete(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event) +{ + CommandTransaction *task; + IEC104_CommandReplyResult_t checked = Check_Reply(self, event, false); + if (checked != IEC104_COMMAND_REPLY_OK) { return checked; } + task = &self->transaction; + if (task->result == 0U || task->completed) { return IEC104_COMMAND_REPLY_ERROR; } + if (self->config.send_act_term && + (task->result == IEC104_COMMAND_RESULT_SUCCEEDED || self->config.act_term_on_failure)) { + uint64_t serial = Reply_Serial(self); + if (serial == 0U) { return IEC104_COMMAND_REPLY_ERROR; } + task->term_serial = serial; + } + task->completed = true; + task->return_waiting = false; + /* 无TERM且确认已交接可立即结束;否则Process只重试协议收尾,不要求应用重复Complete。 */ + Complete_If_Sent(self); + return IEC104_COMMAND_REPLY_OK; +} + +/* + * 功能:通过该事务任一已经交付的事件,只读查询仍保留的事务状态。 + * 参数:self为实例;event为本事务历史SELECT/EXECUTE或已取FINISHED;info为非NULL输出。 + * 返回:true成功;句柄、身份、交付状态非法或槽已回收时false,失败保持info原值。 + * 输出:当前阶段、是否尚未ENDED、确认/结果/Complete/终止状态;不推进会话或期限。 + * 约束:可在动作回调中查询;active只表示本地事务阶段,不保证当前连接仍允许操作。 + * 不保存历史事务,槽回收后旧事件不能再查询,即使其字节副本仍由应用持有。 + */ +bool IEC104_Command_Get_Transaction(const IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandTransactionInfo_t *info) +{ + const CommandTransaction *task; + IEC104_CommandTransactionInfo_t result; + bool delivered; + if (!Owned(self) || event == NULL || info == NULL) { return false; } + task = &self->transaction; + if (!task->occupied || event->instance_id != self->serial || event->transaction_id != task->transaction_id) { + return false; + } + delivered = (event->type == IEC104_COMMAND_SELECT && task->select_delivered && + event->event_serial == task->select_event_serial) || + (event->type == IEC104_COMMAND_EXECUTE && task->execute_delivered && + event->event_serial == task->execute_event_serial) || + (event->type == IEC104_COMMAND_FINISHED && task->event_type == IEC104_COMMAND_FINISHED && + task->event_taken && event->event_serial == task->event_serial); + if (!delivered) { return false; } + memset(&result, 0, sizeof(result)); + result.active = task->phase != IEC104_COMMAND_PHASE_ENDED; + result.phase = (IEC104_CommandPhase_t)task->phase; + result.confirm_submitted = task->confirm_submitted; + result.result_reported = task->result != 0U; + result.completed = task->completed; + result.term_submitted = task->term_submitted; + *info = result; + return true; +} + +/* + * 功能:生成实例累计统计和当前调度条件的只读快照。 + * 参数:self为实例;status为非NULL输出,可在动作回调内使用。 + * 返回:true成功;无效参数或Slave查询失败时false,失败不改输出。 + * 输出:在累计统计副本上即时计算会话通知、事务占用、待交回复/事件及期限剩余量。 + * 约束:不触发到期转换、不取消事务、不预留发送容量;发送WAIT不要求持续空转, + * 但每轮网络推进后仍须调用Process重试。已有FINISHED由NextEvent读取,不靠Process清理。 + */ +bool IEC104_Command_Get_Status(const IEC104_Command_t *self, IEC104_CommandStatus_t *status) +{ + const CommandTransaction *task; + IEC104_CommandStatus_t result; + IEC104_SlaveRequestState_t state; + uint32_t limit; + uint64_t now; + unsigned slot = 0U, head; + bool waiting; + if (!Owned(self) || status == NULL) { return false; } + state = IEC104_Slave_Check_Request(self->slave, &self->session); + if (state == IEC104_SLAVE_REQUEST_ERROR) { return false; } + task = &self->transaction; + result = self->status; + result.generation = self->session.generation; + result.events_pending = state == IEC104_SLAVE_REQUEST_WAIT_EVENTS; + result.busy = task->occupied; + result.pending_replies = Pending_Replies(self); + result.pending_events = task->occupied && (task->finish_pending || + (!task->event_taken && (task->phase == IEC104_COMMAND_PHASE_SELECT_WAIT || + task->phase == IEC104_COMMAND_PHASE_EXECUTE_WAIT))) ? 1U : 0U; + limit = Timer_Limit(self); + now = IEC104_Net_Now_Ms(); + result.timer_active = limit != 0U; + result.timeout_remaining_ms = limit != 0U ? Remaining(task, limit, now) : 0U; + result.need_process = false; + head = Reply_Head(self, &slot); + waiting = head == COMMAND_HEAD_SHORT ? self->replies[slot].send_waiting : task->send_waiting; + if (!self->in_action && !result.events_pending) { + /* 有通知时先交回Slave;离线仅有FINISHED时无需反复推进,让应用直接读清理事件。 */ + if (state != IEC104_SLAVE_REQUEST_READY || !result.active) { + result.need_process = result.pending_replies != 0U || + (task->occupied && task->phase != IEC104_COMMAND_PHASE_ENDED); + } else { + /* 到期/回退或尚未尝试的队首需要立即推进;已WAIT的队首等待网络进展再重试。 */ + result.need_process = Clock_Reversed(self, now) || + (result.timer_active && result.timeout_remaining_ms == 0U) || + (head != COMMAND_HEAD_NONE && !waiting); + } + } + *status = result; + return true; +} diff --git a/IEC60870-Slave/104/business/IEC104_Command.h b/IEC60870-Slave/104/business/IEC104_Command.h new file mode 100644 index 00000000..721cab45 --- /dev/null +++ b/IEC60870-Slave/104/business/IEC104_Command.h @@ -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 */ diff --git a/IEC60870-Slave/README.md b/IEC60870-Slave/README.md index 64c3a498..f931d8c9 100644 --- a/IEC60870-Slave/README.md +++ b/IEC60870-Slave/README.md @@ -1,6 +1,6 @@ # IEC60870-Slave -IEC 60870 子站协议库,当前提供 IEC104 子站、Windows TCP 后端、总召、计数量召唤和Type 103时钟同步。101/104 可复用的 ASDU、信息对象、时间工具及子站应答接口集中在公共目录。 +IEC 60870 子站协议库,当前提供 IEC104 子站、Windows TCP 后端、总召、计数量召唤、Type 103时钟同步和Type 45/46遥控。101/104 可复用的 ASDU、信息对象、时间工具及子站应答接口集中在公共目录。 ## 目录结构 @@ -15,7 +15,8 @@ IEC60870-Slave/ │ │ ├── IEC104_Interrogation_Internal.h # 引擎与种类规则的私有接口 │ │ ├── IEC104_GeneralInterrogation.c/h # 站总召、分组总召规则 │ │ ├── IEC104_CounterInterrogation.c/h # 计数量读取、冻结与复位 -│ │ └── IEC104_ClockSynchronization.c/h # 校时校验、显式动作与确认 +│ │ ├── IEC104_ClockSynchronization.c/h # 校时校验、显式动作与确认 +│ │ └── IEC104_Command.c/h # 单点/双点遥控、选择执行、撤销与返回信息 │ ├── IEC104_TcpServer.c/h # TCP 服务与事件调度 │ ├── IEC104_Connection.c/h # 连接协调 │ ├── IEC104_Link*.c/h # 链路、主站/子站启停 @@ -35,7 +36,7 @@ IEC60870-Slave/ ├── examples/ # 应用示例 │ └── iec104_slave_example.c # 模拟点表、业务分派与程序入口 ├── test/ # 测试源码与主机脚本 -│ ├── iec104_*_test.c # 104 传输、调度、子站与总召测试 +│ ├── iec104_*_test.c # 104 传输、调度、子站及业务测试 │ ├── iec60870_*_test.c # 公共 ASDU 与信息对象测试 │ └── slave_example_loopback_test.cjs # 真实 TCP 回环测试 ├── docs/ # 设计、接口、示例及测试说明 @@ -45,7 +46,7 @@ IEC60870-Slave/ │ ├── examples/ # 示例使用说明 │ └── test/ # 测试说明与验证记录 ├── scripts/ # 构建脚本 -│ └── build.ps1 # 构建示例,-Test 同时运行示例回归 +│ └── build.ps1 # 构建示例,-Test 运行业务回归、容量矩阵及回环 ├── .build/ # 本地构建及验证产物(运行脚本后生成) └── README.md # 项目总览 ``` @@ -63,8 +64,12 @@ IEC60870-Slave/ 脚本按自身位置定位项目,可从其他工作目录调用。构建产物统一写入项目根目录的 `.build/`。 -接入自己的工程时,将 `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`。接入约定见[计数量设计](docs/104/IEC104_CounterInterrogation_设计架构.md)和[时钟同步设计](docs/104/IEC104_ClockSynchronization_设计架构.md)。 +`-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的6001..6008控制Type 1遥信1001..1008;Type 46的6101..6108控制Type 3模拟双点位置1101..1108,双点初值均为1。单点和双点遥信均纳入站总召(QOI=20)及遥信分组(QOI=21),读取遥控更新后的当前值;遥测分组(QOI=22)仍只返回Type 11。示例只更新内存状态,实际输出由应用动作接入;遥调及带时标命令尚未实现。 + 详细资料见[文档索引](docs/README.md)、[模块说明与接入示例](docs/104/README.md)、[子站示例说明](docs/examples/IEC104子站示例.md)和[完整测试命令](docs/test/IEC104测试文档.md#2-编译与运行)。 diff --git a/IEC60870-Slave/common/IEC60870_Asdu.h b/IEC60870-Slave/common/IEC60870_Asdu.h index c93e2450..c811610b 100644 --- a/IEC60870-Slave/common/IEC60870_Asdu.h +++ b/IEC60870-Slave/common/IEC60870_Asdu.h @@ -30,6 +30,8 @@ typedef enum { IEC60870_ASDU_COT_DEACTIVATION = 8, /* 停止激活请求。 */ IEC60870_ASDU_COT_DEACTIVATION_CON = 9, /* 停止激活确认。 */ IEC60870_ASDU_COT_ACTIVATION_TERMINATION = 10, /* 激活终止。 */ + IEC60870_ASDU_COT_RETURN_INFO_REMOTE = 11, /* 远方命令引起的返回信息。 */ + IEC60870_ASDU_COT_RETURN_INFO_LOCAL = 12, /* 当地命令引起的返回信息。 */ IEC60870_ASDU_COT_FILE_TRANSFER = 13, /* 文件传输。 */ IEC60870_ASDU_COT_INTERROGATED_BY_STATION = 20, /* 响应站总召唤的数据。 */ IEC60870_ASDU_COT_COUNTER_INTERROGATION = 37, /* 响应电能脉冲总召唤的数据。 */ diff --git a/IEC60870-Slave/docs/104/IEC104_API接口汇总.md b/IEC60870-Slave/docs/104/IEC104_API接口汇总.md index b5c56930..a869d533 100644 --- a/IEC60870-Slave/docs/104/IEC104_API接口汇总.md +++ b/IEC60870-Slave/docs/104/IEC104_API接口汇总.md @@ -1,8 +1,8 @@ # IEC104 函数 API 接口汇总(按实现文件) -依据当前工作区的协议库源码及对应头文件整理,按实现所在的 `.c` 文件分节。范围不包含示范程序、测试程序及其业务辅助函数;2026-10-29补充独立Type 103时钟同步模块。 +依据当前工作区的协议库源码及对应头文件整理,按实现所在的 `.c` 文件分节。范围不包含示范程序、测试程序及其业务辅助函数;包括独立 Type 45/46 遥控模块的 14 个公开函数及其完整类型、返回值和生命周期约定。 -共覆盖 **24 个实现文件**,其中内部跨文件协作函数为Link 5个、信息对象层2个、召唤引擎2个。表中标有“内部协作”的函数不属于业务 API;文件内的 `static` 辅助函数、宏和类型定义不作为函数接口列出,各模块入口以对应小节和声明头文件为准。 +共覆盖 **25 个实现文件**,其中内部跨文件协作函数为Link 5个、信息对象层2个、召唤引擎2个。表中标有“内部协作”的函数不属于业务 API;文件内的 `static` 辅助函数、宏和类型定义不作为函数接口列出,各模块入口以对应小节和声明头文件为准。 模块接口并不都供业务直接调用:子站业务入口使用 `IEC104_Slave.h`,TcpServer 由 Slave 持有,Connection、SendManager 及其底层接口供相应实现层协作。运行顺序见 [完整调用流程](IEC104_完整调用流程.md)。各文件小节先说明使用约定,再列功能表和可展开的完整函数原型;原型中的参数名、类型和返回类型与声明头文件一致。 @@ -19,6 +19,7 @@ | 接入站总召/分组总召 | [Interrogation](#file-21) | Handle_Event接收激活/撤销及状态通知,Process推进并检查期限;NextEvent取业务事件,以Reply_Begin/Submit_Batch/Complete_Data显式应答 | | 接入计数量召唤 | [CounterInterrogation](#file-23) | 独立创建,复用驱动;读取用Reply_Begin和数据接口,冻结/复位用Execute_Begin | | 接入时钟同步 | [ClockSynchronization](#file-24) | Handle_Event校验保存,Process推进应答及期限,只有显式Execute_Next调用一次应用动作;确认回显原时标 | +| 接入单点/双点遥控 | [Command 配置](#command-api-config)、[函数契约](#command-api-functions)、[返回值](#command-api-results) | NextEvent取得SELECT/EXECUTE,Accept/Reject消费事件;执行后Report_Result、Submit_Return_Info、Complete,FINISHED清理资源 | | 解析主站请求 | [Asdu_Get_Data / Parse / Get_Element](#file-16) | 先检查 I_FRAME 及 `asdu_result`,再做精确解析与业务条件检查 | | 回复任意 ASDU | [MasterConnection_Send_ASDU](#file-18) | 使用 `&event.connection`;ASDU 可以是单独构造的对象 | | 回复激活确认、终止 | [MasterConnection_Send_ACT_CON / Send_ACT_TERM](#file-18) | 修改传入 ASDU 的 COT/P/N 后提交,执行时机由业务决定 | @@ -28,7 +29,7 @@ | 构造主动上报 | [具体信息对象 Create](#file-17) → [Asdu_Create / Add_InformationObject](#file-16) → [Slave_Enqueue_Event](#file-19) | 使用 Slave 提供的应用层参数;提交到 NORMAL,OK 后原文已复制 | | 查看当前主站和容量 | [Slave_Get_Remote_Address / Get_Status](#file-19) | 取得只读副本;状态查询不预留容量,不保证下一次提交成功 | | 断开原主站,继续监听 | [Slave_Close_Connection](#file-19) | 传原连接事件,不手工替换 generation | -| 停止并释放服务 | [Slave_Close → NextEvent 读空 → Destroy](#file-19) | Close 保留通知但清除 ASDU 缓存;Destroy 不隐式关闭或丢弃通知 | +| 停止并释放服务 | [Slave关闭](#file-19)、[遥控退出顺序](#command-api-lifecycle) | 先Cancel各业务并处理其清理事件,再Close、读空并分派通知;销毁业务、归还引用并移除站点后才Slave_Destroy,不能只清通信队列 | | 使用时标、读取实际时间 | [Time](#file-15)、[Clock](#file-14) | Time 负责转换;Clock 负责实际读写。命令校验与执行由业务负责 | “后续只调 Slave”指服务的创建、驱动、事件、状态和关闭统一从 `IEC104_Slave_*` 进入。ASDU 与信息对象仍使用共用 `IEC60870_*`;请求应答通过 Slave 事件提供的 `IEC60870_MasterConnection_t` 分派,无需业务再操作 TcpServer。 @@ -41,7 +42,9 @@ | --- | --- | | `IEC104_Slave_t` / `IEC104_Slave.h` | 不透明服务句柄,Create/Destroy 管理静态槽;内部持有私有 TcpServer 指针 | | `IEC104_Station_t` | 调用方拥有的固定地址节点,Add/Remove管理注册关系;CA、context及引用由Slave统一管理,字段仅库可改写 | -| `IEC104_Interrogation_t` / `IEC104_Interrogation.h` | 不透明总召业务句柄,借用Slave;管理请求副本、分批、撤销、期限和WAIT进度,不拥有网络服务 | +| `IEC104_Interrogation_t` / `IEC104_Interrogation.h` | 总召与计数量使用的共用不透明句柄,种类入口分别创建实例;管理请求副本、分批、撤销、期限和WAIT进度,借用Slave、不拥有网络服务 | +| `IEC104_ClockSynchronization_t` / `IEC104_ClockSynchronization.h` | 独立校时句柄,借用Slave;显式Execute_Next执行动作,Cancel清理任务,Destroy要求任务空且不在回调中,不隐式关闭Slave | +| `IEC104_Command_t` / `IEC104_Command.h` | 不透明遥控句柄,借用Slave;每Slave至多一个实例、每实例一个事务,工作事件及FINISHED均为纯值副本 | | `IEC104_SlaveEventIdentity_t` | Slave校验后交付的来源身份,包含服务句柄、实例编号和连接代次;可按值保存,不借用事件原文;字段只读 | | `IEC104_TcpServer_t` / `IEC104_TcpServer.h` | 不透明底层 TCP 服务,持有连接、队列、帧缓冲、记录和事件 FIFO;可独立接入,但应用须自行管理原请求身份 | | `IEC60870_MasterConnection_t` / `IEC60870_Slave.h` | 共用发送函数表及上下文;104 实现由 Slave 安装,不创建第二个服务、socket 或线程 | @@ -65,6 +68,7 @@ | `IEC104_TcpEventResult_t` | READY=1、EMPTY=0、ERROR=-1 | 仅 READY 提供新事件;EMPTY/ERROR 保持输出原值 | | `IEC104_TcpSubmitResult_t` | OK=1、WAIT=2、ERROR=-1、FATAL=-2 | OK 已接手;WAIT 未接手;ERROR 为身份/参数/阶段错误;FATAL 需按事实收尾 | | `IEC104_SlaveRequestState_t` | READY=2、WAIT_EVENTS=1、STALE=0、ERROR=-1 | READY仅说明会话可处理;WAIT_EVENTS先读空通知;STALE终结旧任务;ERROR处理参数或查询失败 | +| `IEC104_CommandReplyResult_t` | OK=1、WAIT=0、ERROR=-1、STALE=-2 | Accept的OK只表示消费事件;Submit_Return_Info的OK才表示帧序固定。WAIT保留输入,STALE禁止再执行旧命令 | | 共用发送的 `bool` | true 为已接手,false 为未成功 | 104 通过 `Slave_Get_Last_Submit_Result` 进一步分类,不能把 false 一律当作 WAIT | | `IEC60870_AsduResult_t` | OK=1、FULL=0、ERROR=-1、UNSUPPORTED=-2 | 单次 ASDU/对象操作只有 OK 修改输出;FULL 时处理容量或数量限制 | | 时间与时钟的 `int` | 1 成功,0 失败 | 编解码成功与时间品质有效分开判断 | @@ -83,7 +87,7 @@ 带 `IEC104_TcpError_t *error` 的接口允许 NULL;提供存储时完整写入本次 `result/stage/socket_error`,成功为 OK/NONE/0。它表示本次 API 调用结果;连接故障另见事件或状态里的 `fault`。例如客户端断开时 Poll 仍可返回 OK,再通过 DISCONNECTED 交付原因。诊断存储不得与其他输入/输出重叠。 -发送分三个阶段:**提交接手 → 本地整帧写出 → 对端链路 ACK**。OK 只承诺第一阶段;正常 RECORD 的两项完成标志为真,收尾 RECORD 则按实际事实填写。ACK 也不等于对端业务执行成功。`Get_Last_Submit_Result` 是最近一次提交的快照,不随后续 Poll 或 ACK 更新;FATAL 可能发生在部分乃至全部字节已经写出之后。 +发送分三个阶段:**提交接手 → 本地整帧写出 → 对端链路 ACK**。发送接口的 OK 只承诺第一阶段;正常 RECORD 的两项完成标志为真,收尾 RECORD 则按实际事实填写。ACK 也不等于对端业务执行成功。业务决定接口另有含义,例如 Command_Accept 的 OK 只表示已消费工作事件,Command_Complete 的 OK 只表示已关闭返回信息阶段,均不能推定确认或终止已交接。`Get_Last_Submit_Result` 是最近一次提交的快照,不随后续 Poll 或 ACK 更新;FATAL 可能发生在部分乃至全部字节已经写出之后。 @@ -94,10 +98,11 @@ | Slave / TcpServer 句柄 | 静态池持有,地址稳定;应用不 malloc/free、不复制,Destroy 成功后丢弃旧指针 | | 原始 `IEC104_TcpEvent_t`、状态及地址快照 | 独立值副本,可跨 Poll/Close 保存;只读 type 对应的 union 成员,状态字段须先检查有效标志 | | `IEC104_SlaveEvent_t` | APDU 是副本,但 asdu 与 connection 含自引用;用 Copy_Event 保存,禁止结构赋值/memcpy/搬移,地址必须稳定 | +| `IEC104_CommandRequest_t` / `IEC104_CommandEvent_t` | 纯值副本,可直接保存;应答原样回传事件身份,不能用修改request改变实际动作。动作中的request指针仅本次调用有效 | | `event.connection` | 使用 `&event.connection`;它绑定原 Slave 实例和原 generation。单独复制 connection 无法重建上下文 | | `IEC60870_Asdu_t` / `IEC60870_AsduInfo_t` | 借用参数和原文字节,使用期间保持有效;解析视图使用期间原文不可改写 | | 具体信息对象 | Create/Get_Element/Get_From_Buffer 使用调用方存储,保存数值及只读函数表,可复制完整对象,无需 Destroy | -| Add / 发送 / 入队 | Add 成功已编码复制信息对象;发送或入队 OK 已复制 ASDU,可复用本次输入。WAIT 须保存原 connection、ASDU 和业务进度 | +| Add / 发送 / 入队 | Add 成功已编码复制信息对象;发送或入队 OK 已接手 ASDU,可复用本次输入。WAIT须保留输入及原身份;Command返回信息使用纯值EXECUTE事件,不能跨重连继续提交 | | Slave 返回的应用层参数 | 静态只读常量,已取得的指针不借用服务存储;Get_Data 返回的报文指针则借用 ASDU 缓冲 | `Send_ACT_CON/TERM` 是组合操作:先改 COT/P/N,再提交传输。即使提交 WAIT/ERROR/FATAL,已改字段也不会回滚,不能套用“单次 ASDU setter 失败保持原值”的约定。事件被 NextEvent 的下一次 READY 替换后,其旧视图和应答上下文不能继续使用。 @@ -114,6 +119,8 @@ 编译期容量见 [IEC104_TcpServer_Config.h](../../104/IEC104_TcpServer_Config.h):服务槽 1、接收暂存 2048 字节、HIGH/NORMAL 各 1024/4096 字节、控制帧 8 槽、发送记录 32 槽、事件保证容量 68 条,其中启停保留 4 条、收尾保留 34 条、普通通知保证 30 条。实际通知使用变长字节环,短通知可能多于保证条数;这些容量不等于 k 窗口,队列字节数还包含内部条目头。Slave 和直接创建的 TcpServer 共享底层服务名额。 +Command 的实例池与独立应答池另由 `IEC104_COMMAND_CAPACITY`、`IEC104_COMMAND_REPLY_CAPACITY` 配置,分别默认跟随 TCP 服务容量和取 4;每实例仍只有一个遥控事务。宏须作用于 IEC104_Command.c 的编译,详见[遥控配置与容量](#command-api-config)。 + NORMAL 允许覆盖最旧原文;事件 FIFO 不覆盖。HIGH 清除、NORMAL 被覆盖、SendBuffer 已释放、发送记录已回收分别表示不同事实,不能据此推定对端已执行业务。 @@ -161,6 +168,7 @@ CONNECTED 先于同代次其他通知,DISCONNECTED 最后;实际启停变化 - [IEC104_GeneralInterrogation.c](#file-22):1 个函数(保留旧Create名称)。 - [IEC104_CounterInterrogation.c](#file-23):2 个函数。 - [IEC104_ClockSynchronization.c](#file-24):8 个函数。 +- [IEC104_Command.c](#file-25):14 个函数。 @@ -2014,7 +2022,7 @@ Init 失败不销毁对象;Create 后、Init 失败后和 Close 后仍可 Get_ | `IEC104_TCP_SUBMIT_ERROR` | 参数/事件/cause非法、原会话失效或存在未读启停通知;不按容量不足计数,继续处理通知及相应失效原因 | | `IEC104_TCP_SUBMIT_FATAL` | 发送故障,可能已经写出字节;不得重试,继续排空RECORD/DISCONNECTED等收尾通知 | -示例在总召、计数量及后续业务均未接手后显式调用此接口;Slave不会根据类型自动吞掉或拒绝NextEvent交付的请求。HIGH真正满时允许丢弃本条错误应答,因此不承诺所有负载下都有响应;已接手条目不因新兜底请求到达而被覆盖或主动断链。 +示例在总召、计数量、校时和遥控均未接手后显式调用此接口;Slave不会根据类型自动吞掉或拒绝NextEvent交付的请求。HIGH真正满时允许丢弃本条错误应答,因此不承诺所有负载下都有响应;已接手条目不因新兜底请求到达而被覆盖或主动断链。 @@ -2024,13 +2032,13 @@ Init 失败不销毁对象;Create 后、Init 失败后和 Close 后仍可 Get_ 每个`IEC104_Station_t`须零初始化,注册成功后直到`Is_Reusable`返回true均须保持节点及context的地址和寿命稳定,不得赋值搬移、清零或直接改字段。CA及context在注册时指定;context内容、IOA点表、权限和业务状态归应用管理。同一Slave内已注册CA唯一,不同Slave可以注册相同CA,但不能共用同一个节点。查找和注册为线性扫描,适合当前少量站点规模。 -`Get_Station`只查具体的已注册站点,0、65535及未知CA返回NULL。地址检查与业务分派分离的思路亦见[lib60870的CA许可检查](https://github.com/mz-automation/lib60870/blob/master/lib60870-C/src/iec60870/cs104/cs104_slave.c)。NextEvent不自动丢弃或应答报文:业务负责对象布局、COT、CA、权限及广播规则。总召开启`use_station_registry`后已接入按CA查找和广播遍历,其他业务可复用同一基础接口。 +`Get_Station`只查具体的已注册站点,0、65535及未知CA返回NULL。地址检查与业务分派分离的思路亦见[lib60870的CA许可检查](https://github.com/mz-automation/lib60870/blob/master/lib60870-C/src/iec60870/cs104/cs104_slave.c)。NextEvent不自动丢弃或应答报文:业务负责对象布局、COT、CA、权限及广播规则。总召开启`use_station_registry`后已接入按CA查找和广播遍历;Command始终使用共用站点表,拒绝广播CA,创建事务时Acquire原节点,选择匹配使用注册编号而非仅比较CA。 延迟业务保存节点前调用`Acquire_Station`,清理完成后调用`Release_Station`。Remove立即使节点退出CA查找、计数和普通遍历;已有引用仍可读取原CA/context,最后一个引用和快照归还后才清零节点。Remove返回OK不等于可释放存储,须检查`Is_Reusable`。同CA可立即注册另一个节点;`Get_Id/Get_Station_By_Id`以不复用的注册编号定位原节点,避免旧事务查到替代站点。引用仍不能代替原连接身份检查。 `StationSnapshot_Begin/Next/End`提供跨循环遍历:Begin记录注册编号上界,Next跳过已删除的未来站点,并排除后来新增或同CA重建的节点;不复制或冻结IOA和点值。快照须零初始化、地址稳定、配对End,Next返回NULL也不自动结束。任一快照活动时退役节点延迟回收,因此调用方不得提前释放这些节点。需要跨End保留Next返回值时另行Acquire。`Lock_Stations/Unlock_Stations`仍作为显式冻结集合的兼容接口,锁定期间Add/Remove返回BUSY;总召广播不使用集合锁,合法多目标请求在调用方状态区中持有各节点引用,错误回复遍历使用快照。所有操作在同一事件循环串行执行,不是线程锁。 -Close/Init不改变站点表。Slave_Destroy要求站点全部Remove、引用及快照归还、集合锁解除,否则返回INVALID_STATE。Remove不释放应用context,也不撤回已接手报文;已选中的总召可持原引用完成。若业务要求立即停止该事务,应先显式取消并处理FINISHED;不能靠删除再注册同一CA清空旧报文。 +Close/Init不改变站点表。Slave_Destroy要求站点全部Remove、引用及快照归还、集合锁解除,否则返回INVALID_STATE。Remove不释放应用context,也不撤回已接手报文;已选中的总召或已经创建的遥控事务可持原引用完成。若业务要求立即停止协议事务,应先显式取消并处理FINISHED;不能靠删除再注册同一CA清空旧报文。遥控FINISHED只表示协议结束,物理输出仍可能继续;应用应在首个工作事件时取得自己的引用并保留至输出真正结束,详见[遥控引用与退出](#command-api-lifecycle)。 `IEC104_StationResult_t`须显式比较:OK成功;INVALID_ARGUMENT参数/配对非法;DUPLICATE_CA已有注册CA;ALREADY_REGISTERED节点仍由某服务持有;NOT_FOUND节点不属于本服务或重复删除;BUSY显式集合锁阻止修改;LIMIT引用/快照/锁计数或注册编号耗尽。失败不改变注册关系;零计数Release/Unlock返回INVALID_ARGUMENT。 @@ -2070,12 +2078,12 @@ Windows x64 GCC测量(默认4任务、4业务事件):节点40字节、快 | `IEC104_Slave_Get_Event_Identity` | 校验事件并取得来源身份;允许同实例历史会话通知,失败保持输出。 | | `IEC104_Slave_Check_Request` | 统一检查未读通知、原服务实例、连接代次和激活状态,返回业务推进条件。 | | `IEC104_Slave_Get_Last_Submit_Result` | 按事件connection查询最近发送的OK/WAIT/ERROR/FATAL,仅WAIT可等待容量重试。 | -| `IEC104_Slave_Try_Send_ASDU` | 沿事件原connection尝试不排队提交ASDU,直接返回并记录OK/WAIT/ERROR/FATAL;WAIT不保存原文,应用重读后重试,普通共用发送接口不变。 | +| `IEC104_Slave_Try_Send_ASDU` | 沿事件原connection尝试不排队提交ASDU,直接返回并记录OK/WAIT/ERROR/FATAL;WAIT不保存原文,应用按业务语义保留或重构后重试(总召重新取当前值),普通共用发送接口不变。 | | `IEC104_Slave_Send_Response` | 使用保存的原事件身份提交独立ASDU;保留原实例/代次检查,直接发送或进入HIGH,返回提交结果,不要求保留完整事件。 | | `IEC104_Slave_Try_Send_Response` | 使用保存的原事件身份尝试不排队提交;WAIT不缓存数据,原队列、窗口和期限规则保持不变。 | | `IEC104_Slave_Reject_Request` | 同步拒绝当前事件,原文仅改COT=44..47/P/N=1,复用HIGH;直接返回提交结果,输入及最近提交结果不变。 | | `IEC104_Slave_Get_Application_Layer_Parameters` | 返回标准104静态只读参数,供主动上报构造ASDU。 | -| `IEC104_Slave_Enqueue_Event` | 校验ASDU对象并提交NORMAL,支持离线缓存和原有覆盖策略。 | +| `IEC104_Slave_Enqueue_Event` | 校验104布局和容器可读性后提交NORMAL,支持离线缓存和原有覆盖策略;完整对象及业务校验由调用方或业务模块承担。 | | `IEC104_Slave_Get_Remote_Address` | 取得当前主站地址副本,不借用内部连接对象。 | | `IEC104_Slave_Close_Connection` | 按原事件关闭指定连接,内部提取身份;旧代次不能关闭新连接。 | | `IEC104_Slave_Close_Connection_By_Identity` | 按保存的原事件身份关闭原连接;与完整事件入口同样检查服务实例/代次,保留监听和NORMAL。 | @@ -2483,3 +2491,363 @@ IEC104_ClockSynchronizationExecuteResult_t IEC104_ClockSynchronization_Execute_N bool IEC104_ClockSynchronization_Get_Status(const IEC104_ClockSynchronization_t *self, IEC104_ClockSynchronizationStatus_t *status); ``` + + + +## 25. IEC104_Command.c + +实现:[IEC104_Command.c](../../104/business/IEC104_Command.c);声明:[IEC104_Command.h](../../104/business/IEC104_Command.h)。本节列出全部 **14 个公开函数**,并说明其配置、值类型及调用约束。状态机设计见[遥控设计](IEC104_Command_设计架构.md),事件循环与应用处理顺序见[遥控调用流程](IEC104_完整调用流程.md#flow-command)。 + +模块只处理 Type 45(单点命令)和 Type 46(双点命令)的 COT=6 激活、COT=8 撤销。模块负责格式、寻址、选择/执行匹配、期限和协议收尾;应用负责 IOA 点表、远方/就地、闭锁、返校、输出及设备结果。未配置的类型返回 IGNORED,交后续处理器;不支持遥调、带时标命令、多个遥控并行或跨重连选择保持。决定窗口内的同点激活按[请求校验规则](#command-api-validation)静默丢弃,窗口外不按请求内容去重。 + +本节函数表省略公共前缀 `IEC104_Command_`;枚举表注明各自省略的前缀。不能把枚举的非零值统一视为成功。 + + + +### 25.1 配置、容量与所有权 + +`IEC104_CommandConfig_t` 由 Config_Default 填写,Create 按值复制;调用后可修改或释放原配置存储,不影响实例。Create 不接受 NULL 配置,也不自动初始化或开启 Slave。 + +| 字段 | 默认值 | 校验及语义 | +| --- | --- | --- | +| `type_mask` | `IEC104_COMMAND_TYPE_BIT(45U) \| IEC104_COMMAND_TYPE_BIT(46U)` | 非零,仅允许45/46两位;未选中的类型不接手 | +| `select_required_mask` | 与默认 `type_mask` 相同 | 必须为 `type_mask` 子集;某位为0表示该类型允许直接执行,也仍可选择后执行 | +| `select_timeout_ms` | 30000 | 必须非零;选择生效至匹配执行被Handle_Event受理,包含选择确认的发送等待 | +| `decision_timeout_ms` | 15000 | 工作事件生成至Accept/Reject消费;0禁用时长限制 | +| `execute_timeout_ms` | 60000 | 执行动作返回STARTED至Report_Result成功;0禁用时长限制 | +| `send_act_term` | true | 成功执行收尾时发送ACT_TERM;false时任何结果均不发 | +| `act_term_on_failure` | false | 仅 `send_act_term=true` 时有效:启动后Report_Result(FAILED)也发送ACT_TERM;不适用于动作返回FAILED或执行超时 | + +`IEC104_COMMAND_TYPE_BIT(type)` 的布局为45..51对应bit0..6、58..64对应bit7..13,范围外返回0,避免非法移位。第一版Create仍只接受45/46两位;宏参数会多次求值,只传常量或无副作用表达式。结构体扩展要求库和调用者一起重新编译,不承诺二进制布局兼容。 + +| 编译期设置 | 默认值 | 作用 | +| --- | --- | --- | +| `IEC104_COMMAND_CAPACITY` | `IEC104_TCP_SERVER_CAPACITY` | 全库静态Command实例数;每个Slave至多绑定一个实例,两个宏都不增加单实例并发事务数 | +| `IEC104_COMMAND_REPLY_CAPACITY` | 4 | 每实例独立短应答槽数,用于校验错误、实例忙和否定撤销;须大于0且小于 `UINT32_MAX / 2` | +| 唯一事务槽 | 固定1个 | 内含工作/终结事件、确认、撤销或失配应答、终止的存储及排序身份;必要事务应答不依赖独立池空位 | + +两个容量宏均须大于0,编译选项作用于 IEC104_Command.c。全部存储静态分配,无堆分配;模块借用Slave,Slave寿命覆盖Command。所有入口与Slave在同一事件循环串行执行,不保存动作函数或context指针。 + +从事务创建至槽释放,所有站点、所有IOA均视为遥控忙;FINISHED未读或本事务最后应答尚未交接也占槽。决定窗口内同点合法激活只丢弃,不新增忙否定应答。独立短应答可在 `busy=false` 时仍待发送,不阻止创建新事务,但仍参与全局FIFO,且Destroy要求它们全部清空。独立池满只计过载并丢弃新应答,不覆盖旧内容、不主动断链。事务释放后,主站重发的相同命令按新请求处理;直接执行可能再次启动输出。 + + + + +### 25.2 请求、事件与动作类型 + +`IEC104_CommandRequest_t` 为纯值快照,无借用指针,可跨循环按值保存。每次动作使用模块内部请求重新构造的快照;修改应用保存的event.request不能改变实际输出参数。 + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `generation` | uint64_t | 原连接代次,不能改成新代次重试 | +| `receive_serial` | uint64_t | 当前工作请求的原I帧接收编号 | +| `station_id` | uint64_t | 原站点注册身份;同CA删除再注册后编号不同 | +| `common_address` | uint16_t | 原CA,实际站地址1..65534 | +| `originator_address` | uint8_t | 原OA;参与选择/执行及撤销匹配 | +| `type_id` | uint8_t | 45或46 | +| `ioa` | uint32_t | 非零24位信息对象地址;点表存在性由应用检查 | +| `is_select` | bool | 请求S/E位,true为选择,false为执行 | +| `selected` | bool | EXECUTE是否匹配了本事务已有选择;直接执行为false | +| `state` | uint8_t | Type45为0/1,Type46为1/2,通常对应分/合;具体设备操作含义由点表定义 | +| `qualifier` | uint8_t | QU原始0..31;1短脉冲、2长脉冲、3持续输出,0及其他值由应用解释 | +| `accepted_ms` | uint64_t | 当前请求由Handle_Event受理的单调毫秒;包含Slave通知排队时间,不是网络收包时间 | + + + +`IEC104_CommandEvent_t` 同样是纯值副本,**NextEvent取走与Accept/Reject消费是两个步骤**。取走的工作事件仍可因撤销、期限或会话结束失效,不能脱离模块检查后直接启动设备动作。 + +| 字段 | 类型/用途 | 约定 | +| --- | --- | --- | +| `type` | IEC104_CommandEventType_t | 与三层编号共同验证身份;SELECT=1、EXECUTE=2、FINISHED=3 | +| `instance_id` | uint64_t | 模块实例身份,静态池槽复用也不会复用实例号 | +| `transaction_id` | uint64_t | 实例内事务身份 | +| `event_serial` | uint64_t | 当前事件身份;匹配执行和FINISHED各有新序号 | +| `request` | IEC104_CommandRequest_t | 最后被接纳的工作请求;撤销及不匹配请求只改变相应回显,不覆盖本字段 | +| `reason` | IEC104_CommandFinishReason_t | 仅FINISHED有效;工作事件为FINISH_NONE | +| `select_effect` / `execute_effect` | bool | 仅FINISHED有效,表示对应动作曾返回STARTED或FAILED,可能需要清理;SELECT的NULL动作例外,不置select_effect | + +事件类型省略 `IEC104_COMMAND_` 前缀:SELECT由应用Accept/Reject选择;EXECUTE由应用Accept/Reject执行,request.selected区分是否已选择;FINISHED只通知收尾,无需应答。应答时保留整个原事件,按实例号、事务号、事件号和type匹配,不以CA/IOA替代事件身份。 + + + +```c +typedef IEC104_CommandActionResult_t (*IEC104_CommandAction_t)( + void *context, const IEC104_CommandRequest_t *request); +``` + +Accept同步至多调用一次action,context原样转交、允许NULL,两者均不保存。request指针只在此次回调内有效,需异步使用时复制值。回调须短时返回,不推进Slave事件循环、不发布任何上报;本模块中仅允许Get_Status/Get_Transaction,修改入口拒绝重入,Cancel无效果、Destroy返回false。 + +| 动作返回值(省略 `IEC104_COMMAND_ACTION_`) | 数值 | 选择/执行效果 | +| --- | --- | --- | +| NOT_STARTED | 0 | 应用必须保证完全无副作用;否定确认,以REJECTED结束,不置对应effect | +| STARTED | 1 | 选择已生效,或执行已启动;选择进入SELECTED,执行进入EXECUTING,结果以后Report_Result | +| FAILED | 2 | 启动尝试失败但可能留下副作用;否定确认,以ACTION_FAILED结束,置对应effect | + +非法动作返回值按FAILED处理,模块不重试动作。SELECT可以action=NULL,逻辑选择视为STARTED但无物理副作用;EXECUTE必须提供action。Accept返回OK仍可能对应NOT_STARTED/FAILED,或确认提交时遇到故障;应用保存自己的动作结果,并通过Get_Transaction及后续FINISHED判断协议是否继续,不能把OK当作设备成功。 + +`IEC104_CommandResult_t` 用于已经STARTED的执行:`IEC104_COMMAND_RESULT_SUCCEEDED=1`、`IEC104_COMMAND_RESULT_FAILED=2`,表示设备最终结果,与动作是否成功启动不同。`IEC104_CommandRejectCause_t` 为 `IEC104_COMMAND_REJECT_NEGATIVE=1`(COT=7、P/N=1)或 `IEC104_COMMAND_REJECT_UNKNOWN_IOA=2`(COT=47、P/N=1);其他值返回ERROR。 + + + +### 25.3 事务阶段与结束原因 + +阶段枚举为 `IEC104_CommandPhase_t`,省略 `IEC104_COMMAND_PHASE_` 前缀: + +| 阶段 | 数值 | 当前等待及主要迁移 | +| --- | --- | --- | +| SELECT_WAIT | 1 | 等应用消费SELECT;STARTED后SELECTED,拒绝、撤销或超时后ENDED | +| SELECTED | 2 | 保持已生效选择;确认可能仍待交接,交接后且未到期的匹配执行才进入EXECUTE_WAIT | +| EXECUTE_WAIT | 3 | 等应用消费EXECUTE;已停止选择期限,STARTED后EXECUTING | +| EXECUTING | 4 | 等设备结果、返回信息、Complete及最终交接;Report_Result和Complete各自置标志,不另设阶段 | +| ENDED | 5 | 协议已结束,可能仍欠FINISHED或待交应答;达到两个回收条件后释放槽 | + +没有公开IDLE枚举;实例无事务由Status.busy=false表示,旧事件的Get_Transaction返回false。 + +`IEC104_CommandTransactionInfo_t` 是仍占槽事务的只读快照,Get_Transaction只接受该事务已经交付的SELECT、EXECUTE或FINISHED事件,包括已消费的旧SELECT。它不保留已回收事务的历史,也不做会话/期限转换。 + +| 字段 | 含义 | +| --- | --- | +| `active` | 阶段不是ENDED;不能据此保证当前连接允许操作 | +| `phase` | 上表中的当前阶段 | +| `confirm_submitted` | **当前工作请求**的确认已由发送层接手;SELECT匹配EXECUTE时会重新清零,不表示写完/ACK | +| `result_reported` | Report_Result成功,结果已冻结且执行期限停止 | +| `completed` | Complete成功,返回信息阶段已关闭,可能仍在等待最终交接 | +| `term_submitted` | ACT_TERM已交接;配置不发TERM时一直false,不能只等待此字段判断完成 | + + + +终结枚举为 `IEC104_CommandFinishReason_t`,下表省略 `IEC104_COMMAND_FINISH_` 前缀。首次原因冻结,后续STOP/Cancel/发送失败不会改写已ENDED事务的原因或重复产生FINISHED。 + +| 原因 | 触发及协议含义 | +| --- | --- | +| NONE=0 | 工作事件占位,不是正常FINISHED原因 | +| COMPLETED | 已记录Report_Result(SUCCEEDED)、Complete已调用,最后应答已交接;不表示主站ACK | +| EXECUTION_FAILED | STARTED后Report_Result报告失败,Complete后的协议收尾已完成 | +| REJECTED | 应用Reject或动作NOT_STARTED | +| ACTION_FAILED | 动作FAILED或非法动作返回值,可能有设备副作用 | +| MISMATCH | 选择确认交接后,同一点的执行在类型、OA、状态或QU上不匹配 | +| DEACTIVATED | COT=8成功撤销尚未启动执行的事务;与STOPDT不同 | +| SELECT_TIMEOUT | 已生效选择到期,释放选择,仅丢弃模块尚持有的选择确认 | +| DECISION_TIMEOUT | 未及时Accept/Reject,安排否定ACT_CON | +| EXECUTE_TIMEOUT | 未及时Report_Result;不发TERM,模块持有的肯定执行确认仍可继续提交 | +| CANCELLED | 本地Command_Cancel | +| STOPPED | 原连接STOPDT停用通知 | +| DISCONNECTED | 原连接断开通知 | +| CONNECTION_CHANGED | 新连接代次或原会话身份失效 | +| SEND_ERROR | 发送ERROR/FATAL,且不是等待分派正常STOP通知的例外 | +| SERVICE_ERROR | Slave查询、组包或内部身份/应答序号等资源不能继续使用 | + +以上结束原因均不证明物理输出已停止;副作用清理及独立站点引用见[生命周期](#command-api-lifecycle)。 + + + +### 25.4 全部函数的参数与前置条件 + +所有self必须为尚未Destroy的本模块句柄。除Config_Default和Create外,下表的接口都作用于现有实例;除两项只读查询外,不在动作回调中调用。完整声明见[函数签名](#command-api-signatures)。 + +| 函数 | 参数与前置条件 | 成功、失败和输出 | +| --- | --- | --- | +| Config_Default | config指向调用方可写配置 | 无返回;完整填写默认值,NULL无操作,不创建实例/启动期限 | +| Create | slave为有效服务,寿命覆盖实例;config非NULL、满足配置表;**在开始消费Slave通知前调用** | 成功返回静态句柄;非法服务/配置、同Slave重复创建、池满或实例号耗尽返回NULL。仅复制配置,不开启网络 | +| Destroy | 事务槽为空、无未读FINISHED、独立应答池为空,不在回调中 | true归还实例,旧指针失效;否则false且保留实例。不隐式Cancel、关闭Slave或释放应用引用 | +| Cancel | self有效;不在回调中 | 无返回;清全部模块待交应答,以CANCELLED结束活动事务,保留应交FINISHED,会话激活状态不变;无效句柄/回调中无操作 | +| Handle_Event | event为所属Slave按FIFO取出的通知,借用内容仍有效;所有连接/启停通知必须分派 | HANDLED含状态通知及已识别请求的受理、拒绝、丢弃;IGNORED继续分派;ERROR处理来源/参数/服务错误,不能再发44兜底。详见返回表 | +| Process | 每轮网络推进、Slave通知分派后调用 | true包含无事、WAIT、事务已终结或发送故障已收尾;false为句柄/重入/服务/组包错误。最多推进 `2*(1+应答槽容量)` 步,不调用应用动作 | +| NextEvent | event为非NULL输出 | READY写入一个纯值事件并记录已取走;EMPTY/ERROR保持输出。取走不消费工作决定;已有FINISHED离线也可取 | +| Accept | event为已取走、尚未消费的当前SELECT/EXECUTE;action仅SELECT可NULL;context按值传给回调 | 先校验会话/期限并准备确认,再消费事件,至多调用一次动作;OK不是动作或发送成功,精确返回见下表 | +| Reject | event同Accept;cause为两个RejectCause之一 | 不调用动作,消费事件并安排7/47,以REJECTED结束;FINISHED可早于否定交接 | +| Report_Result | event为Accept动作返回STARTED的原EXECUTE;result为SUCCEEDED/FAILED,尚未成功报告 | 一次记录并冻结结果、停止执行期限;可早于确认交接。不提交返回信息、不隐式Complete | +| Submit_Return_Info | event为上述EXECUTE;asdu符合返回信息校验,仍在EXECUTING且Complete未成功 | 一次提交一帧;只能在执行确认交接后接手,否则WAIT。不缓存ASDU、不占用持久FIFO位置 | +| Complete | event为上述EXECUTE;结果已成功报告,计划提交的返回信息已全部OK,可为零帧 | 一次关闭返回信息阶段并按策略安排TERM;可早于确认交接。后续由Process完成,不能因TERM仍待交而重复调用 | +| Get_Transaction | event可为尚占槽事务任一已交付事件;info非NULL | true写入快照,ENDED时active=false;无效身份/参数、已回收返回false且不改info。刚取出的FINISHED也可能已触发回收 | +| Get_Status | status非NULL,可在动作回调中调用 | true写入当前状态及计数;句柄/参数或Slave查询失败返回false且不改status。只计算期限剩余,不转换状态 | + +模块不会保存Handle_Event输入指针、Accept的action/context或Submit_Return_Info的ASDU指针。Command事件可按值复制,Slave事件若需延迟保存仍须使用Slave_Copy_Event。 + + + +### 25.5 返回值与重试规则 + +| 返回类型 | 枚举值 | 含义 | +| --- | --- | --- | +| IEC104_CommandEventResult_t | EVENT_ERROR=-1 / EVENT_IGNORED=0 / EVENT_HANDLED=1 | 前缀为IEC104_COMMAND_;HANDLED不等于已发应答,IGNORED的I帧才交后续业务及最终44兜底 | +| IEC104_CommandNextResult_t | NEXT_ERROR=-1 / NEXT_EMPTY=0 / NEXT_READY=1 | 仅READY改写输出;EMPTY也可能表示须优先读Slave通知 | +| IEC104_CommandReplyResult_t | REPLY_STALE=-2 / REPLY_ERROR=-1 / REPLY_WAIT=0 / REPLY_OK=1 | 前缀为IEC104_COMMAND_;按下表区分消费工作事件、记录结果、关闭阶段和实际接手ASDU | + +Handle_Event的IGNORED包括非本版/未配置类型、未激活I帧、ASDU视图不可用、RECORD及旧代次通知。当前CONNECTED/ACTIVATED/DEACTIVATED/DISCONNECTED返回HANDLED;参数、外来服务身份、禁止重入及内部处理错误返回ERROR。 + +下表均以合法self、有效枚举参数为基础;非法参数可先返回ERROR,不能假定所有失效事务调用都优先返回STALE。五个应答入口先验证事件交付/阶段,再检查Slave会话,READY后执行期限转换并再次验证阶段。 + +| 接口 | OK:本次已接手 | WAIT:本次未接手 | STALE:旧操作失效 | ERROR:检查参数或用法 | +| --- | --- | --- | --- | --- | +| Accept | 工作事件已消费;动作最多一次,NOT_STARTED/FAILED也返回OK | 有未读Slave通知;动作未调用、事件未消费 | 身份/类型/阶段不符、事件已消费、事务结束或到期;不得自行补执行 | 非法参数、EXECUTE未给动作、回调重入、服务/确认资源错误;动作未调用 | +| Reject | 工作事件已消费,应答曾安排;可能已交接,也可能被随后发送故障取消 | 有未读通知;事件未消费 | 与Accept相同 | 非法cause/参数、重入或内部错误 | +| Report_Result | 结果已记录,执行期限停止 | 有未读通知;结果未记录 | 事务已结束/到期,或不是已STARTED的原EXECUTE事件 | result非法、有效事务中重复报告/已Complete、参数/重入/服务错误 | +| Submit_Return_Info | ASDU已由发送层接手,帧序固定,不能重交 | 未读通知、确认未交接、模块尚有待交应答、网络背压或正常STOP通知待分派;没有缓存ASDU | 身份/阶段失效、事务结束,或发送ERROR/FATAL导致取消(正常STOP例外) | ASDU校验失败、Complete后仍待收尾、参数/重入/服务错误;本帧未接手 | +| Complete | 已关闭返回阶段,TERM按配置由Process提交;不必重调 | 有未读通知;尚未关闭阶段 | 身份/阶段失效或事务结束 | 结果尚未报告、仍待收尾时重复调用、参数/重入/服务或终止序号错误 | + +WAIT后保留本次事件、待报告结果或ASDU,先分派Slave通知、推进网络,再重试当前步骤;不能跳过仍WAIT的Report_Result或返回信息就调用Complete。Accept/Reject的OK后不得再次调用该工作决定,Complete的OK后不得再提交返回信息。STALE后停止提交原事务,仍应读取FINISHED清理资源。参数错误可在事务有效时修正;服务/资源错误可能已结束事务,应查询状态、继续分派通知和FINISHED,不一概重试。 + +有效事务中重复Report_Result/Complete返回ERROR;已ENDED或回收后旧EXECUTE返回STALE。Accept即使动作STARTED,随后尝试提交确认仍可能结束事务,OK不保证它仍处于EXECUTING。Get_Transaction可用于查看当前阶段,但只是只读快照,下一次修改入口仍会重新检查身份、通知及期限。 + + + +### 25.6 请求校验、匹配和撤销 + +45/46请求原文固定为10字节:6字节公共头、3字节IOA、1字节SCO/DCO。当前代次、已激活且可读取的配置内命令,在匹配前先做到期转换,然后按以下优先级处理;已识别请求的拒绝/丢弃均返回HANDLED。 + +| 顺序 | 条件 | 处理 | +| --- | --- | --- | +| 1 | 对象数不是1,或对象不足4字节(含IOA) | 计rejected,不应答,不截取多对象伪造单命令 | +| 2 | COT不是6或8 | COT=45、P/N=1 | +| 3 | CA未注册、0或65535 | COT=46、P/N=1 | +| 4 | IOA=0 | COT=47、P/N=1 | +| 5 | SQ=1、长度不等于10、P/N=1、TEST=1、SCO保留位为1或DCS为0/3 | 否定ACT_CON(7);撤销请求否定DEACT_CON(9) | +| 6 | 合法COT=8 | 按撤销规则匹配 | +| 7 | 合法COT=6,当前仍为SELECT_WAIT或EXECUTE_WAIT,同generation、station_id及IOA | 静默丢弃,仅增加dropped_requests,返回HANDLED | +| 8 | 其他合法COT=6 | 匹配选择、受理新事务或忙/无选择否定7 | + +回显保留原类型、TEST、OA、CA和对象字节,VSQ规范为1、长度取10字节,按应答替换COT/P/N;因对象截短或数量不符而丢弃时不回显。除IOA=0外,模块不检查业务点表;应用用Reject(UNKNOWN_IOA)表达点不存在。 + +决定窗口内的丢弃不比较类型、OA、S/E、状态或QU;同点合法激活即使这些字段不同也不替代原工作请求。type_mask及上表前置校验仍优先,COT=8不适用此规则。丢弃不刷新期限、不生成新事件或应答,不增加rejected、busy_rejected或overloads;应用继续处理原SELECT/EXECUTE事件。到期转换之后已非决定阶段时,不再按此规则丢弃。 + +SELECTED阶段的选择匹配要求原会话、原station_id、IOA、类型、OA一致,且SCO/DCO屏蔽S/E后的字节相同(状态和QU均比较)。此阶段选择肯定确认尚未交接时,任何执行均按忙拒绝并保留选择;交接后,同点执行失配终结为MISMATCH并回显此次执行。其他站点/IOA的新激活、SELECTED中的重复选择及EXECUTING/ENDED中的新激活仍否定7,原事务继续。 + +撤销要求原会话、station_id、IOA、类型、OA一致,不比较状态/QU。SELECT_WAIT、SELECTED、EXECUTE_WAIT可成功撤销,用事务自带槽回肯定9,因此独立池满也可撤销;尚未提交的旧确认被取消,新DEACT_CON取得新FIFO序号。已经交给发送层的确认无法撤回,DEACT_CON排在其后。EXECUTING、ENDED或无匹配事务时回否定9,原事务不受影响。本地Cancel不发送DEACT_CON,STOPDT则是连接停用而非COT=8撤销。 + + + +### 25.7 期限、会话与调度 + +全部期限使用 `IEC104_Net_Now_Ms` 的单调毫秒;本版无CP56及设备UTC参数。每阶段至多一个业务期限,已用时间大于或等于期限即到期。 + +| 期限 | 起点 | 停止点 | 到期处理 | +| --- | --- | --- | --- | +| decision_timeout_ms | SELECT/EXECUTE工作事件生成,包含未被取走时间 | Accept/Reject成功消费 | 否定7,终结DECISION_TIMEOUT;曾交付工作事件才产生FINISHED | +| select_timeout_ms | 选择动作返回后重新采样;NULL选择同样计时 | Handle_Event受理匹配执行 | 终结SELECT_TIMEOUT,丢弃模块尚持有的选择确认,不改发否定 | +| execute_timeout_ms | 执行动作返回STARTED后重新采样 | Report_Result成功 | 终结EXECUTE_TIMEOUT,不发TERM,原肯定确认仍可提交 | + +决定和执行期限为0只关闭时长限制;处于等待决定、选择保持或等待设备结果时,`now < timer_start_ms` 仍按该阶段到期并计clock_errors。回调耗时不追认为已经消费的决定超时,选择/执行从回调返回后重新计时;回调仍必须短时完成,以免阻塞Slave事件循环。 + +确认待发不阻止Report_Result,设备结果产生后应及时报告。Report_Result成功后无业务收尾期限,应用漏调Complete会持续占槽;选择期限则有意包含确认发送等待。已交接的迟到选择确认不能撤回,过期后到达的执行仍被拒绝。底层链路、发送和STOP期限始终独立生效。 + +Handle_Event按通知FIFO更新CONNECTED/ACTIVATED/DEACTIVATED/DISCONNECTED,不以“还有后续未读通知”为由阻挡当前通知;新CONNECTED结束旧事务并更新代次,ACTIVATED启用,DEACTIVATED结束为STOPPED,DISCONNECTED结束并清当前代次,旧代次和RECORD忽略。有效I帧匹配前检查期限。 + +Process、工作事件NextEvent及有效应用应答先用Slave_Check_Request检查原身份:WAIT_EVENTS时让出,先读空并分派Slave通知;STALE终结旧事务,服务查询错误终结为SERVICE_ERROR。READY后才检查期限和改变业务状态。已经生成的FINISHED可在离线或通知未读空时由NextEvent取出;Get_Status/Get_Transaction只读,不推进期限。 + +每轮网络推进并读空Slave通知后都调用Process、读取业务事件,再重试应用保留的结果/返回信息/Complete。`need_process=true` 要立即推进;false不表示无待重试任务。等待上限同时受Slave_Prepare、`timer_active/timeout_remaining_ms` 和应用异步状态约束,不能因发送WAIT紧循环空转,具体见[遥控调度流程](IEC104_完整调用流程.md#flow-command-schedule)。 + + + + +### 25.8 应答FIFO与返回信息 + +事务确认、撤销/失配回复、终止与独立短应答共用生成序号,Process只提交全局最早一项,队首WAIT就结束本轮。Handle_Event产生独立应答时,仅在模块原本无待提交应答时尝试当场交接;仍须通过会话检查。新回复不越过更早的模块回复,过载不覆盖旧内容。 + +| 内容 | 发送路径 | 交接语义 | +| --- | --- | --- | +| 45/46/47、校验/忙否定、选择阶段确认、DEACT_CON,以及执行因Reject/NOT_STARTED/决定超时产生的否定 | Slave_Send_Response | 可进入HIGH;OK后不再归模块持有,不能Cancel撤回 | +| 执行动作STARTED后的肯定确认、执行动作FAILED后的否定确认 | Slave_Try_Send_Response | 不入HIGH/NORMAL;OK时帧序已固定,不越过已排队的过程信息 | +| Submit_Return_Info提交的监视信息 | 先检查模块全部待交应答,再用原事务身份Try | WAIT不缓存、不占持久排序位置;后续重试须重新通过全部门禁 | +| ACT_TERM | Try | 排在执行确认和已提交返回信息后;Complete只安排,Process负责后续交接 | + +Try要求当前发送缓冲、HIGH/NORMAL未交接内容及窗口/记录资源均允许;持续过程信息积压可能推迟确认/返回信息/TERM,不承诺有界完成。`high_priority_burst_limit`不能改变本模块已固定的事务帧序。交接后仍可能未写完、更未ACK。 + +Submit_Return_Info只接受已STARTED的原EXECUTE事件,确认已交接、Complete尚未接手时可用;既可早于也可晚于Report_Result。一次调用只能提交一帧非空ASDU,事务允许调用零次或多次。 + +| ASDU检查 | 当前规则 | +| --- | --- | +| 参数与长度 | 参数有效,COT/CA/IOA宽度为2/2/3,配置maxSizeOfASDU及实际长度不超过249;原文可完整解析,无截短、错误对象或多余尾字节 | +| 类型与对象 | 监视方向Type1..21或30..40,至少一个对象;SQ=0/1均可,只要布局有效 | +| COT | 11(远方命令返回信息)或3(突发);不接受12 | +| P/N、TEST、CA | P/N=0,TEST与原命令一致(本版为0),CA必须等于命令CA | +| IOA与OA | IOA可不同于命令IOA,不做点表匹配;当前接口也不要求返回信息OA等于命令OA,由应用按接入约定填写 | + +远方及当地命令返回信息常量分别为 `IEC60870_ASDU_COT_RETURN_INFO_REMOTE=11`、`IEC60870_ASDU_COT_RETURN_INFO_LOCAL=12`;定义了12不表示本遥控返回信息接口接受12。 + +应用持有ASDU及其参数/缓冲:WAIT未接手,保留本帧,或从同一事件快照重建后重试;OK后发送层已接手,可释放或复用输入且不得重交。模块既不缓存WAIT的返回信息,也不把原连接数据放进可能覆盖并跨连接保留的NORMAL。断线后应用放弃原事务尚未提交部分;如设备仍产生需要报告的变化,另用Enqueue_Event/COT=3。同一变化不要同时经两个路径重复发布;动作FAILED时也只能用独立过程事件报告后续变化,不能继续调用原执行返回接口。 + +TERM策略在Report_Result之后由Complete决定:成功且send_act_term=true时发;失败还须act_term_on_failure=true;其他情况不发。失败TERM仍为原执行请求的COT=10、P/N=0,表示流程终止,不表示设备成功,这是本模块可配置约定。即使不发TERM,也须等执行确认交接才结束为COMPLETED/EXECUTION_FAILED。 + +发送返回ERROR且核实为原连接仍健康、已经STOP、对应通知尚未读取时,保留模块应答并让出,返回信息接口返回WAIT,待DEACTIVATED处理;其他ERROR及全部FATAL取消全部模块应答、结束活动事务为SEND_ERROR。FATAL可能已写出部分乃至全部字节,不能重试旧帧。Accept/Reject在动作/决定已经消费后仍返回OK,不以随后发送失败要求应用重做动作。 + + + +### 25.9 状态字段和累计计数 + +`IEC104_CommandStatus_t` 由Get_Status填写,计数自Create起累计;查询不发送、不处理到期。各计数可重叠,`unsigned long` 达上限自然回绕,适合诊断而非可靠业务账单。 + +| 字段 | 语义与调度用途 | +| --- | --- | +| `generation` | 已分派通知确定的当前连接代次,断线后0 | +| `active` | 模块已处理ACTIVATED且尚未停用,不代替实时会话门禁 | +| `events_pending` | Slave还有通知须优先读取;为true时need_process=false | +| `need_process` | 有可立即尝试的本地发送、到期或失效会话收尾;发送WAIT后不持续置true。离线只剩FINISHED时为false,应用直接NextEvent读取 | +| `timer_active` | 当前启用非零时长期限;false不关闭时钟回退检测 | +| `busy` | 唯一事务仍占槽,含未读FINISHED/待交事务回复;不包含仅有独立短应答的情况 | +| `timeout_remaining_ms` | 当前期限剩余毫秒,无期限/已到期/时钟倒退时为0;timer_active=true且为0时立即推进 | +| `pending_replies` | 模块持有的事务与独立应答总数;不含已接手的HIGH/在途帧,也不含应用保留的返回信息 | +| `pending_events` | 尚未取走的工作事件或FINISHED数,本版最多1;已取走尚未Accept的事件不在此数内 | +| `accepted` | 生成的工作事件数,选择后执行计两次,不是成功设备操作数 | +| `selected` / `executed` | 选择/执行动作返回STARTED次数;NULL选择也计selected | +| `completed` / `execution_failed` | 以COMPLETED/EXECUTION_FAILED完成协议收尾的事务数 | +| `action_failed` | 以ACTION_FAILED终结的事务数,含非法动作返回值 | +| `submitted` | 所有已交接帧总数,包含返回信息,不等于主站接收数 | +| `negative_submitted` | 已交接的P/N=1应答数 | +| `return_info_submitted` | Submit_Return_Info已交接的帧数 | +| `waits` | 底层发送明确返回WAIT的次数;通知门禁、模块FIFO等待及正常STOP让出不计入 | +| `rejected` | 接收校验、实例忙、失配及未选择等拒绝次数,包含无法可靠回显的畸形请求;不含决定窗口内同点合法激活的静默丢弃 | +| `app_rejected` | 应用Reject或动作NOT_STARTED次数,与接收侧rejected分开 | +| `busy_rejected` | 因事务占用而拒绝的新激活次数;其他点同样计入,决定窗口内同点合法激活的静默丢弃不计入 | +| `overloads` | 独立应答池满或新事务/事件身份序号耗尽的过载次数;决定窗口内同点合法激活的静默丢弃不计入 | +| `dropped_requests` | 过载丢新,以及SELECT_WAIT/EXECUTE_WAIT内同generation、station_id及IOA的合法激活静默丢弃次数;后者可单独增加,不通过覆盖旧事务腾空间 | +| `cancelled` | CANCELLED、STOPPED、DISCONNECTED、CONNECTION_CHANGED、SEND_ERROR、SERVICE_ERROR导致的事务结束总数;不证明设备无副作用 | +| `deactivated` / `mismatches` | DEACTIVATED成功撤销 / MISMATCH终结的事务数 | +| `select_timeouts` / `decision_timeouts` / `execute_timeouts` | 对应期限导致的结束数;时钟回退也走相应到期路径 | +| `clock_errors` | 等待阶段发现单调时钟倒退次数;必要确认资源失败时最终原因可能为SERVICE_ERROR | + + + +### 25.10 FINISHED、站点引用与退出 + +曾经由NextEvent取走任一工作事件的事务,恰好交付一次FINISHED;从未取走工作事件就结束的事务直接移除未读事件,不产生FINISHED。这是事务级配对:SELECT已取走、随后EXECUTE尚未取走就撤销/超时,仍欠一个FINISHED;不会因生成EXECUTE清除SELECT留下的清理义务。 + +COMPLETED/EXECUTION_FAILED在Complete后的最后应答交接后交付;不发TERM时指确认已交接。其他原因在事务终结时即可交付,否定或DEACT_CON可以仍待发送。FINISHED中的request、原因及effect保留首次终结快照,不被撤销原文或后续STOP覆盖。 + +ENDED事务只有同时满足两个条件才释放:FINISHED已读取或无需交付;所有**本事务**应答已交接或取消。模块随后Release站点并清空槽。独立短应答不借用事务槽,也不延迟该槽回收;它们仍影响FIFO及Destroy。NextEvent返回FINISHED时可能已经释放模块的最后引用,此时Get_Transaction即使使用刚返回的FINISHED也可返回false。 + +事务创建时按注册表Acquire站点,槽释放时Release;站点删除后新请求回46,原事务持原编号继续,同CA替代站点不会继承选择。应用要跨循环保留context或启动异步设备输出,应在**首个工作事件**时按station_id取得原节点并另行Acquire,不能等读到FINISHED后再申请引用。FINISHED后物理输出未结束时,由应用继续维持设备忙状态和该引用,直到确实空闲再Release。 + +正常生命周期为:创建Slave、注册站点并Create Command;开始按FIFO分派通知;持续Process/NextEvent和应用应答;退出时Command_Cancel、读完FINISHED并处理应用资源、Slave_Close、读空并分派Slave通知、Command_Destroy、Remove_Station、Slave_Destroy。存在仍在进行的设备输出时,应用保留自己的引用并延后实际存储回收/Slave_Destroy,不能为遵循顺序提前释放context。Cancel不关闭连接、不撤回已交接帧、不回滚输出,也不清除已经冻结的终结原因。应用侧具体清理步骤见[FINISHED与引用流程](IEC104_完整调用流程.md#flow-command-finished)。 + + + +### 25.11 完整函数签名 + +```c +#include "IEC104_Command.h" + +void IEC104_Command_Config_Default(IEC104_CommandConfig_t *config); +IEC104_Command_t *IEC104_Command_Create(IEC104_Slave_t *slave, + const IEC104_CommandConfig_t *config); +bool IEC104_Command_Destroy(IEC104_Command_t *self); +void IEC104_Command_Cancel(IEC104_Command_t *self); +IEC104_CommandEventResult_t IEC104_Command_Handle_Event(IEC104_Command_t *self, + const IEC104_SlaveEvent_t *event); +bool IEC104_Command_Process(IEC104_Command_t *self); +IEC104_CommandNextResult_t IEC104_Command_NextEvent(IEC104_Command_t *self, + IEC104_CommandEvent_t *event); +IEC104_CommandReplyResult_t IEC104_Command_Accept(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandAction_t action, void *context); +IEC104_CommandReplyResult_t IEC104_Command_Reject(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandRejectCause_t cause); +IEC104_CommandReplyResult_t IEC104_Command_Report_Result(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result); +IEC104_CommandReplyResult_t IEC104_Command_Submit_Return_Info(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, const IEC60870_Asdu_t *asdu); +IEC104_CommandReplyResult_t IEC104_Command_Complete(IEC104_Command_t *self, + const IEC104_CommandEvent_t *event); +bool IEC104_Command_Get_Transaction(const IEC104_Command_t *self, + const IEC104_CommandEvent_t *event, IEC104_CommandTransactionInfo_t *info); +bool IEC104_Command_Get_Status(const IEC104_Command_t *self, IEC104_CommandStatus_t *status); +``` diff --git a/IEC60870-Slave/docs/104/IEC104_Command_设计架构.md b/IEC60870-Slave/docs/104/IEC104_Command_设计架构.md new file mode 100644 index 00000000..20d4cda9 --- /dev/null +++ b/IEC60870-Slave/docs/104/IEC104_Command_设计架构.md @@ -0,0 +1,87 @@ +# IEC104 遥控模块设计 + +`104/business/IEC104_Command.c/h` 提供 Type 45 单命令和 Type 46 双命令业务。业务入口为 Slave,实例使用静态池,不动态分配内存、不保存应用回调;所有入口与所属 Slave 在同一事件循环中串行调用。一个 Slave 至多绑定一个 Command 实例,一个实例至多保留一个遥控事务。其他命令类型返回 IGNORED,交给后续业务或未知类型兜底。 + +## 1. 职责与默认配置 + +模块管理请求校验、选择与执行匹配、撤销、三类期限、应答排序、事务身份及 FINISHED。应用管理点表、权限、闭锁、返校和设备输出,显式报告设备结果并构造监视方向返回信息。协议结束不代表物理输出已经停止。 + +| 配置 | 默认值 | 含义 | +|---|---|---| +| type_mask | Type 45、46 | 其他类型位在第一版配置中非法 | +| select_required_mask | Type 45、46 | 清除对应位允许直接执行;必须是 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 时忽略 | + +TEST=1 不执行动作,返回否定确认。广播 CA=65535 不接受。失败终止可选策略发送原执行请求的 COT=10、P/N=0,表示执行过程终止,不表示设备执行成功;结果由应用监视信息和 FINISHED(EXECUTION_FAILED) 表达。该策略为本模块接入约定,默认不开启。 + +## 2. 请求校验和选择匹配 + +45/46 的104原文均为10字节:6字节公共头、3字节IOA、1字节SCO/DCO。唯一完整对象存在后,错误优先级为 COT、CA、IOA、布局/值、事务条件。COT非6/8回45,CA未注册或广播回46,IOA=0回47。对象数不是1或对象截短只计数丢弃;SQ、额外尾字节、P/N、TEST、SCO保留位和DCS非法值回否定7/9,回显规范为VSQ=1、10字节。已识别的拒绝和丢弃均返回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阶段的选择执行要求同会话、原站点注册编号、IOA、类型、OA,且SCO/DCO屏蔽S/E后相同。此阶段选择确认尚未交接时,执行按忙拒绝并保留选择;确认交接后同点不匹配执行终结为MISMATCH,否定回显本次执行。其他点的新激活只忙拒绝。匹配前检查选择期限,到期选择不能复活。 + +撤销按会话、站点编号、IOA、类型、OA匹配,不比较状态。SELECT_WAIT、SELECTED、EXECUTE_WAIT中的匹配撤销复用事务存储,肯定DEACT_CON回显撤销请求。执行已经启动、事务已结束或没有匹配时否定撤销。发送接口已经返回OK的选择确认不能撤回,随后DEACT_CON按顺序交接。 + +## 3. 应用交互与设备结果 + +事件为纯值副本,身份包含模块实例、事务、工作事件序号和类型。Accept/Reject只接受已经取出且未消费的工作事件。模块使用自己保存的请求调用动作;调用者修改事件副本不能改变执行参数。 + +| 入口 | 成功后的含义 | +|---|---| +| 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. 固定存储、验证与扩展 + +`IEC104_COMMAND_CAPACITY`默认跟随TCP服务容量;每实例一个事务槽,`IEC104_COMMAND_REPLY_CAPACITY`默认4。事务必要应答不依赖回复池剩余容量。池满只拒绝/丢新,不覆盖活动事务,不主动断开连接。Process每轮推进次数有界。实际sizeof及配置矩阵结果统一记录在[测试说明](../test/IEC104测试文档.md),不把宿主机结果当作其他目标的预算。 + +示例地址、动作与返回信息见[子站示例](../examples/IEC104子站示例.md)。Type45命令6001..6008对应Type1遥信1001..1008;Type46命令6101..6108对应Type3位置1101..1108,双点初值均为1。两类遥信均纳入示例站总召及遥信分组,逐批读取应用当前值,能反映遥控执行后的状态;点表及分组属于应用,不改变Command模块职责。 + +验证覆盖TCP容量1/2与回复容量1/4、期限边界、动作只调用一次、通知优先、FIFO、部分写入、结果先于确认、撤销及站点引用,并使用真实本机TCP核对45/46流程。决定窗口内的同点激活丢弃不依赖内容相同,也不提供窗口外去重;事务释放后,相同ASDU按新请求处理,允许直接执行时仍可能再次输出。 + +带时标58/59、遥调及并发不属于第一版。后续扩展需保持事件身份、引用与交接契约;结构体新增字段要求调用方一起重新编译,不承诺二进制布局兼容。 diff --git a/IEC60870-Slave/docs/104/IEC104_完整调用流程.md b/IEC60870-Slave/docs/104/IEC104_完整调用流程.md index dc3c6903..d628a543 100644 --- a/IEC60870-Slave/docs/104/IEC104_完整调用流程.md +++ b/IEC60870-Slave/docs/104/IEC104_完整调用流程.md @@ -1,14 +1,14 @@ # IEC104 完整调用流程 -本文依据 2026-10-29 当前工作区的协议库源码,说明从服务创建到关闭释放的调用顺序,与 [API 接口汇总](IEC104_API接口汇总.md) 配合阅读。范围仅为库本身:不包含示范程序、测试程序或应用业务实现。接口汇总按实现文件查函数,本文按运行过程追踪函数之间的关系。 +本文依据 2026-10-30 当前工作区的协议库源码,说明从服务创建到关闭释放的调用顺序,与 [API 接口汇总](IEC104_API接口汇总.md) 配合阅读。本文以库接口为主,引用实际示例说明应用接入,但不逐函数展开示例或测试实现。接口汇总按实现文件查函数,本文按运行过程追踪函数之间的关系。 主线为当前已实现的 **IPv4 TCP 子站**:应用通过 `IEC104_Slave_*` 接入,Slave内部持有并调用 `IEC104_TcpServer_*`,内部使用 Connection、SendManager、Link、Receiver、SendBuffer 和 AsduQueue。当前网络后端为 Windows;主站文件只提供 Link 层启停过程,未接入这一套 TCP 子站调度。 调用树中的 `[内部]` 表示对应 `.c` 内的静态辅助函数,不是应用可直接调用的 API。调用树表示主要步骤;具体的等待、失败和恢复分支在后文说明。 -可先读[应用主线](#flow-1)和[对象关系](#layers),需要业务时读[总召接入](#flow-interrogation)和[时钟同步接入](#flow-clock-synchronization),再查[事件与 ASDU](#flow-6)、[应答与上报](#flow-7)、[容量等待](#flow-12)、[断线重连](#flow-13)、[自有事件循环](#flow-14)和[关闭释放](#flow-15)。 +可先读[应用主线](#flow-1)和[对象关系](#layers),需要业务时读[总召接入](#flow-interrogation)、[计数量接入](#flow-counter-interrogation)、[时钟同步接入](#flow-clock-synchronization)和[遥控接入](#flow-command),再查[事件与 ASDU](#flow-6)、[应答与上报](#flow-7)、[容量等待](#flow-12)、[断线重连](#flow-13)、[自有事件循环](#flow-14)和[关闭释放](#flow-15)。 -业务调用主线为 `Slave_Create → Slave_Config_Default/Init → Slave_Prepare/Process 或 Poll → Slave_NextEvent → 共用发送接口 → Slave_Close → 读空事件 → Slave_Destroy`。NextEvent 直接交付 ASDU 及原连接接口;延迟处理可用Slave_Copy_Event,也可保存来源身份及必要请求数据,通过Slave身份入口应答。以下展开 Slave 调用到的 TCP 内部流程;其中 `server` 和应答所用的 `generation` 由 Slave 维护。 +业务调用主线为 `Slave_Create → 站点/业务模块绑定及Slave_Init → 网络推进与通知分派 → 业务Process/事件/显式应答 → 业务Cancel和清理 → Slave_Close及通知收尾 → 业务Destroy/站点移除 → Slave_Destroy`。Slave_NextEvent直接交付ASDU及原连接接口;独立业务模块自行保存原身份,并交付可按值保留的业务事件。以下展开应用与TCP内部流程;其中`server`和应答所用的`generation`由Slave维护。 @@ -18,43 +18,47 @@ IEC104_Slave_Create() → IEC104_Slave_Config_Default() → 按需要修改监听地址、端口和协议配置 + → 注册站点,创建所需的总召/计数量/校时/遥控实例 → IEC104_Slave_Init() → 循环: IEC104_Slave_Poll() IEC104_Slave_NextEvent(),逐条读到 EMPTY CONNECTED → 记录主站地址和连接代次;此时尚未启用数据传输 ACTIVATED → 处理数据传输启用通知 - I_FRAME → 检查 asdu_result,完整解析并校验业务条件 - 通过 &event.connection 调用共用 Send_ASDU / ACT_CON / ACT_TERM + I_FRAME → 检查 asdu_result,分派业务模块;全部未接手才兜底 + 自定义同步业务可通过 &event.connection 应答 RECORD → 处理发送事实回执 DEACTIVATED → 处理数据传输停用通知 DISCONNECTED → 结束该 generation 的连接业务 - 有主动上报时构造 ASDU,调用 IEC104_Slave_Enqueue_Event() + 推进业务Process,读取业务事件,执行应用决定/结果报告/返回信息/Complete + 有独立主动上报时调用 IEC104_Slave_Enqueue_Event() 对此前返回 WAIT 且仍有效的业务提交,择机重试 + → 分别Cancel业务模块,读完FINISHED并清理应用资源 → IEC104_Slave_Close() - → IEC104_Slave_NextEvent(),处理收尾事件直到 EMPTY + → IEC104_Slave_NextEvent(),分派收尾通知直到 EMPTY;再读完业务FINISHED + → 分别Destroy业务模块,归还引用并移除站点 → IEC104_Slave_Destroy() ``` -以上是调用顺序伪代码。事件不会每轮都出现,排列也不是固定脚本;实际以 `NextEvent` 返回的 FIFO 顺序为准。 +以上是调用顺序伪代码,省略各调用的错误处理,返回值见下表及各业务小节。业务实例也可在Slave_Init后创建,条件是尚未开始消费通知。事件不会每轮都出现,排列也不是固定脚本;实际以Slave_NextEvent返回的FIFO顺序为准,生命周期通知须分派给所有业务实例。 ```mermaid flowchart TD - A["Slave_Create / Config_Default / Init"] --> B["Slave_Poll:等待并推进一次"] + A["创建Slave / 注册站点 / 绑定业务 / Init"] --> B["Slave_Poll:等待并推进一次"] B --> C["NextEvent:逐条取事件"] C --> D{"事件类型"} - D -->|I_FRAME| E["验证 ASDU / 通过 event.connection 应答"] + D -->|I_FRAME| E["验证视图 / 按业务模块分派 / 未接手才兜底"] D -->|RECORD| F["读取本地完成和对端 ACK 事实"] - D -->|连接或启停| G["按 generation 处理状态变化"] + D -->|连接或启停| G["按 generation 分派给全部业务模块"] E --> C F --> C G --> C - C -->|EMPTY| H["提交主动上报 / 安排有效的 WAIT 重试"] + C -->|EMPTY| H["业务Process / 业务事件 / 显式应答及WAIT重试"] H --> I{"继续运行?"} I -->|是| B - I -->|否| J["Close"] - J --> K["NextEvent 读完收尾事件"] - K --> L["Destroy"] + I -->|否| J["业务Cancel / 读完FINISHED / Slave_Close"] + J --> K["分派收尾通知 / 清理业务资源"] + K --> L["业务Destroy / 移除站点 / Slave_Destroy"] ``` 使用这条主线时: @@ -69,12 +73,13 @@ flowchart TD | 接口组 | 成功或有结果 | 需要区别处理的结果 | | --- | --- | --- | -| Init、Poll、Prepare、Process、Close 等 | `IEC104_TCP_OK`,数值为 0 | 错误时保存该次 `IEC104_TcpError_t`;客户端断开本身可正常返回 OK,由事件说明 | -| NextEvent | `IEC104_TCP_EVENT_READY` | EMPTY 表示已读空;ERROR 不是有效事件 | +| Slave/TcpServer的Init、Poll、Prepare、Process、Close等 | `IEC104_TCP_OK`,数值为0 | 错误时保存该次`IEC104_TcpError_t`;客户端断开本身可正常返回OK,由事件说明 | +| Slave_NextEvent / TcpServer_NextEvent | `IEC104_TCP_EVENT_READY` | EMPTY表示通知已读空;ERROR不是有效事件 | | Slave_Enqueue_Event、底层 TcpServer_Send_Response | `IEC104_TCP_SUBMIT_OK` | WAIT 尚未接手;ERROR 为参数、代次或准入等错误;FATAL 须结合收尾事实处理 | | 共用连接的Send_ASDU、Send_ACT_CON、Send_ACT_TERM | `true` | 104侧用Get_Last_Submit_Result区分WAIT/ERROR/FATAL;false不等于可以直接重试 | +| Command_Accept / Reject / Report_Result / Submit_Return_Info / Complete | `IEC104_COMMAND_REPLY_OK`,数值为1 | WAIT=0保留当前步骤;STALE=-2停止旧交互;ERROR=-1检查参数/服务或用法,OK含义依接口而定 | -`SUBMIT_OK` 表示已经复制接手,应避免重复提交;`SUBMIT_WAIT` 表示未接手,由业务稍后重试。普通应答保留原文,总召数据使用不排队接口,WAIT后重读当前值再试。应答返回 FATAL 时可能已经写出部分甚至全部字节,不能直接按“完全未发送”处理。 +`SUBMIT_OK`表示发送层已经接手,应避免重复提交;`SUBMIT_WAIT`表示未接手,由业务稍后重试。普通应答保留原文,总召数据WAIT后重读当前值;遥控返回信息WAIT保留同一帧及EXECUTE事件,不能照搬总召的重采样策略。Command_Accept的OK只表示消费事件,不等于STARTED或确认已发送。应答返回FATAL时可能已经写出部分甚至全部字节,不能直接按“完全未发送”处理。 @@ -84,6 +89,8 @@ flowchart TD flowchart TD App["应用业务"] --> Slave["IEC104_Slave:生命周期、事件封装、连接身份绑定"] App --> Common["IEC60870_MasterConnection:共用发送接口"] + App --> Business["总召 / 计数量 / 校时 / 遥控:显式业务交互"] + Business -->|原请求身份应答| Slave Common -->|函数指针分派| Slave Slave -->|内部持有指针| TCP["IEC104_TcpServer:监听、socket、FIFO、调度"] TCP --> Conn["IEC104_Connection:接收、状态机、期限协调"] @@ -105,6 +112,8 @@ flowchart TD `Check_Request`返回`WAIT_EVENTS`时先逐条处理Slave通知,返回`STALE`时终结原业务,返回`ERROR`时处理查询或参数错误;只有`READY`才继续业务阶段。READY不保证发送容量,也不能替代业务撤销和期限判断。后续业务模块复用这两个接口,不读取事件的内部身份字段,也不自行组合TCP连接状态。 +此门禁用于推进原请求,不得挡住Handle_Event正在消费的生命周期通知。遥控模块先按事件来源分派CONNECTED/ACTIVATED/DEACTIVATED/DISCONNECTED,按FIFO处理完后再推进事务;NextEvent中的FINISHED即使离线也必须能够读取,才能完成资源清理。 + ### 1.2 配置入口与默认值 运行配置先调用 `IEC104_Slave_Config_Default(&config)`,再修改所需字段,最后交给 Init。Init 复制配置;修改原配置变量不会改变已运行的服务。 @@ -119,6 +128,8 @@ flowchart TD | `IEC104_APP_LAYER_PARAMETERS_DEFAULT` | 定义在 [IEC104_Types.h](../../104/IEC104_Types.h):Type ID 1、VSQ 1、COT 2、默认 OA 0、CA 2、IOA 3、ASDU 最大 249 字节 | | 静态存储容量 | 编译期由 [IEC104_TcpServer_Config.h](../../104/IEC104_TcpServer_Config.h) 配置,默认可创建 1 个服务。与上述运行参数分开 | +业务模块有各自的Config_Default/Create,不能把业务期限填进Slave的链路配置。遥控类型掩码、三个期限、ACT_TERM策略及编译期实例/应答池容量见[遥控创建与配置](#flow-command-create);这些期限须由应用与网络等待条件合并,Slave_Prepare不会自动读取业务模块状态。 + 104 专用的默认参数宏保留在 104 接入层。共用 `IEC60870_AppLayerParameters_t` 只描述布局,不替调用方选择传输配置。业务一般通过 `IEC104_Slave_Get_Application_Layer_Parameters` 或事件连接的对应 getter 取得标准 104 参数,无需自行维护宏副本。 @@ -156,7 +167,7 @@ Slave_Create → Interrogation_Destroy → Slave_Remove_Station → Slave_Destroy ``` -必须在开始消费Slave通知前创建总召模块,一个Slave只绑定一个实例。配置按值复制,Slave须存活到总召Destroy之后;Init失败也要释放已创建实例。所有接口串行调用;异步采样结果须回到所属事件循环再提交。模块不创建线程,也不会调用应用函数。 +必须在开始消费Slave通知前创建总召模块,同一Slave每种召唤各至多一个实例,Type100总召和Type101计数量可以独立并存。配置按值复制,Slave须存活到总召Destroy之后;Init失败也要释放已创建实例。所有接口串行调用;异步采样结果须回到所属事件循环再提交。模块不创建线程,也不会调用应用函数。 上面的顺序使用共用站点表,CA只在注册时指定。多目标广播先确认全部站,再逐站取数,最后终止全部站;各站BEGIN可先于首条FINISHED。应用可在首个NEED_DATA建立唯一取数游标,Complete_Data返回OK后复用;异常时按FINISHED清理。需保留context则按station_id查原节点并Acquire,使用结束后Release。删除立即退出路由,已有引用结束后才可释放存储;同CA重建不改变旧数据源。兼容应用可保留use_station_registry=false的单CA或固定station模式。详见[共用站点表](IEC104_API接口汇总.md#station-registry)。 @@ -244,17 +255,28 @@ COT/CA/IOA错误回45/46/47;激活的非法QOI、P/N、总召忙或应用拒 构建在Slave及依赖之外加入104/business/IEC104_Interrogation.c及104/business/IEC104_GeneralInterrogation.c,头文件搜索路径包含104/、104/business/和common/。静态容量宏为IEC104_INTERROGATION_CAPACITY(默认TCP实例容量的两倍)、IEC104_INTERROGATION_TASK_CAPACITY(默认4)、IEC104_INTERROGATION_EVENT_CAPACITY(默认4,最小2)。每实例4个任务槽共用于至多一个召唤及短应答,不是后续召唤队列。任务使用固定槽位和顺序索引,完成的非队首应答可立即释放槽位,其他任务的自引用不受影响。生成BEGIN时同时预留一个终结槽;未读业务事件挤满队列时暂停生成工作事件,已排队单帧应答仍可调度,取消仍可交付FINISHED,不使用malloc。 -同时接入计数量时另编译104/business/IEC104_CounterInterrogation.c,创建独立实例,配置独占广播状态区。连接及启停通知必须同时交付两边;I帧先总召、再计数量,未接手再交校时等后续模块或兜底。每轮读空Slave通知后推进两个召唤实例,轮换先后顺序,合并need_process、events_pending及最小剩余期限。FRZ=0走确认、数据、终止;FRZ=1..3通过Execute_Begin执行一次本地同步动作,确认后直接FINISHED。两个实例分别Cancel、读取FINISHED、Destroy。动作失败及快照约定见[计数量设计](IEC104_CounterInterrogation_设计架构.md)。 - 本接口替换旧begin/read_batch/finish及parameter配置:应用使用NextEvent和显式应答,遍历状态自行持有。Destroy前须Cancel并读完业务事件。Process只接收实例,旧调用删除第二个预算实参。当前只保留一个未终结总召,忙时否定后续激活,不自动执行此前被拒绝的请求。已删除queue_timeout_ms、QUEUED/QUEUE_TIMEOUT、queued_interrogations和queue_timeouts;调用方删除相关字段及分支,使用Config_Default初始化配置并重新编译。negative_submitted和dropped_requests继续记录否定提交与满槽丢弃;FINISH_OVERLOAD保留枚举值,满槽不以此终结已有任务。 + + +#### 1.3.1 同时接入计数量召唤 + +另编译`104/business/IEC104_CounterInterrogation.c`,使用CounterInterrogation_Create创建独立实例,配置独占广播状态区;返回的句柄沿用Interrogation_Process/NextEvent/Cancel/Destroy驱动。连接和启停通知交付两个召唤实例,I帧先总召、再计数量,未接手再交校时、遥控或兜底。每轮读空Slave通知后轮换推进两个召唤实例,合并各自的need_process、events_pending和最近期限。 + +| 操作 | BEGIN之后的应用流程 | 模块收尾 | +|---|---|---| +| FRZ=0读取 | Reply_Begin接受;NEED_DATA按固定冻结快照构造批次,Submit_Batch;读完Complete_Data | ACT_CON、数据、ACT_TERM,随后FINISHED | +| FRZ=1..3冻结/复位 | 用CounterInterrogation_Execute_Begin先校验有效BEGIN,再调用本次传入的同步动作一次;WAIT保留事件,STALE/ERROR不执行动作 | 动作结果决定肯定/否定ACT_CON,确认后FINISHED;不走NEED_DATA或ACT_TERM | + +两个实例分别Cancel、读取FINISHED和Destroy;不能把计数量BEGIN交给总召实例应答。冻结/复位动作返回false必须保证未改变计数器,这与遥控ACTION_FAILED可有副作用的契约不同。详细分组、数据COT和快照约定见[计数量设计](IEC104_CounterInterrogation_设计架构.md)及[计数量API](IEC104_API接口汇总.md#file-23)。 + ### 1.4 接入独立时钟同步模块 -编译`104/business/IEC104_ClockSynchronization.c`并包含对应头文件,在Slave创建并初始化后、消费任何Slave通知前创建校时实例。配置只含源标准时区和执行期限,默认480分钟及15000ms;模块使用独立静态池和每实例4个任务槽,不与召唤引擎共享任务。详情见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md)。 +编译`104/business/IEC104_ClockSynchronization.c`并包含对应头文件,在Slave_Create成功后、消费任何Slave通知前创建校时实例,可在Slave_Init前完成绑定,示例即采用此顺序。配置只含源标准时区和执行期限,默认480分钟及15000ms;模块使用独立静态池和每实例4个任务槽,不与召唤引擎共享任务。详情见[时钟同步设计](IEC104_ClockSynchronization_设计架构.md)。 -1. 将CONNECTED、ACTIVATED、DEACTIVATED及DISCONNECTED按FIFO交付总召、计数量、校时所有实例。I帧按总召→计数量→校时分派,均IGNORED才用Reject_Request兜底;HANDLED也可能表示拒绝或满槽丢弃。 +1. 将CONNECTED、ACTIVATED、DEACTIVATED及DISCONNECTED按FIFO交付总召、计数量、校时及遥控所有实例。I帧按总召→计数量→校时→遥控分派,均IGNORED才用Reject_Request兜底;HANDLED也可能表示拒绝或满槽丢弃。 2. `Handle_Event`校验Type 103的唯一对象、COT/CA/IOA、CP56/IV/TEST、源时区及本站日期范围。合法请求保存原文及accepted_ms,不设置时钟;广播在受理时建立目标快照。 3. 读空Slave通知后调用`Process`推进队首应答和未执行请求期限。Process不调用action,不越过等待执行的队首提交后续应答。 4. 应用明确准备执行时调用`Execute_Next(cs, action, context)`。入口检查原会话,使用同一单调采样检查期限及计算滞留补偿,向action交付对应base_ms的utc_ms和固定UTC+8 local_time。只调用最早EXECUTE一次;可以越过较早REPLY执行,但确认保持原FIFO位置。 @@ -266,6 +288,235 @@ Execute结果须按枚举分支:WAIT先处理Slave通知,EMPTY只表示没 时间只补偿Handle_Event受理后的本站滞留;base_ms不是设备生效时刻,不补网络、通知FIFO和驱动耗时。新请求不按内容去重,重发旧时标可能再次回拨。Try不隔离新增NORMAL,持续发布可能使确认延迟;confirm_pending是应用协调依据,不是传输层预留或线程锁。 + + +### 1.5 接入单点、双点遥控模块 + +`IEC104_Command`接手Type45单点命令、Type46双点命令,提供选择、执行、撤销、期限和返回信息顺序管理。每个Slave至多一个实例,每实例至多一个未释放事务;不同站点的遥控也互斥。点表、闭锁、远方/就地、真实输出及设备忙状态由应用提供。模块使用静态池,不分配内存、不保存回调,只在Accept中调用本次传入的动作。接口声明与全部返回值见[API第25节](IEC104_API接口汇总.md#file-25),协议校验及内部存储见[遥控设计](IEC104_Command_设计架构.md)。 + + + +#### 1.5.1 创建实例与配置 + +编译`104/business/IEC104_Command.c`及Slave依赖,头文件路径包含`104/`、`104/business/`和`common/`。Slave句柄创建成功后即可创建遥控实例,允许在Slave_Init之前或之后,但必须早于第一次消费Slave通知。否则模块会漏掉CONNECTED/ACTIVATED,不能靠最终连接状态补出历史启停顺序。站点由Slave统一注册,CA不放进Command配置;当前不接受广播CA=65535。 + +以下C函数只负责创建默认实例。调用方须检查返回NULL、清理已创建资源;成功后把句柄保存到所属事件循环,Slave寿命须覆盖Command_Destroy。 + +```c +#include "IEC104_Command.h" + +static IEC104_Command_t *Create_Command(IEC104_Slave_t *slave) +{ + IEC104_CommandConfig_t config; + IEC104_Command_Config_Default(&config); + return IEC104_Command_Create(slave, &config); +} +``` + +| 配置 | 默认行为 | 修改时的约束 | +|---|---|---| +| type_mask | 接手45、46 | 不得为0;本版其他类型位使Create失败,未启用类型返回IGNORED | +| select_required_mask | 45、46均须先选择 | 必须为type_mask子集;只清除允许直接执行类型的对应位 | +| decision_timeout_ms | 15000ms | 从工作事件生成开始计时;0只禁用时长限制 | +| select_timeout_ms | 30000ms | 不得为0;含选择确认等待发送的时间 | +| execute_timeout_ms | 60000ms | 从执行动作返回STARTED至Report_Result;0只禁用时长限制 | +| send_act_term | true | false时成功、失败都不发ACT_TERM,仍须Complete | +| act_term_on_failure | false | 仅send_act_term=true时生效;失败终止的P/N仍为0,只表示流程结束 | + +静态池容量由编译宏`IEC104_COMMAND_CAPACITY`控制,默认与TCP服务容量相同;每实例独立应答槽由`IEC104_COMMAND_REPLY_CAPACITY`控制,默认4。事务自带确认、终止及撤销/失配应答存储,独立槽处理忙拒绝和校验否定,不能用增加应答槽开启多个事务。重复绑定同一Slave、池满或配置非法时Create返回NULL。 + + + +#### 1.5.2 先分派Slave通知,再读取遥控事件 + +每轮先用`IEC104_Slave_NextEvent`按FIFO读到EMPTY。CONNECTED、ACTIVATED、DEACTIVATED、DISCONNECTED逐条交给所有已创建业务模块;不能因为前一个模块返回HANDLED就跳过其他模块的生命周期通知。I帧按总召、计数量、校时、遥控依次分派,只有全部IGNORED才调用44兜底。RECORD不驱动遥控工作状态。 + +| Command_Handle_Event结果 | 应用下一步 | +|---|---| +| EVENT_HANDLED | 此请求已由遥控接手;包括校验否定、忙拒绝、过载丢弃及决定窗口内同点激活的静默丢弃,不能再次兜底 | +| EVENT_IGNORED | I帧可继续交给后续业务;未启用类型、未激活、视图不可用或旧代次可能走此结果 | +| EVENT_ERROR | 记录来源、参数或服务错误并进入应用错误处理;不要把它当IGNORED继续执行命令 | + +Handle_Event消费当前通知时只校验来源,不能在外层用`Check_Request==WAIT_EVENTS`挡住它:后续通知尚未出队正是必须继续分派的原因。合法遥控I帧在匹配前先检查当前事务期限;应用动作及发送推进则由模块在操作前检查会话,遇未读通知先让出。一次通知消费预算用完但尚未EMPTY时,先安排续读,暂不Accept或重试返回信息。 + +读空后调用`IEC104_Command_Process`,再循环`IEC104_Command_NextEvent`。Process的true包含空闲、等待或发送故障已转换为事务终结,不能据此判断设备成功;false按服务/用法错误处理。NextEvent的READY才写出新事件;EMPTY停止本轮读取,可能仍有应答或应用重试;ERROR不使用旧输出。已有FINISHED即使离线、或又出现未读Slave通知也可交付,必须继续消费。 + +工作事件为纯值,按值保存`IEC104_CommandEvent_t`即可,不需要Slave_Copy_Event。事件中的instance_id、transaction_id、event_serial及type一起校验;应用不得改写request来改变执行内容。SELECT和匹配后的EXECUTE属于同一事务、不同工作事件;各工作事件只交付一次,WAIT不会生成替代事件。 + + + +#### 1.5.3 选择、执行匹配及应用决定 + +空闲收到S/E=1、COT=6,模块生成SELECT并进入SELECT_WAIT;S/E=0只有在该类型允许直接执行时才生成EXECUTE,否则否定ACT_CON。应用取到工作事件后检查点表和策略,调用Accept或Reject。IOA不存在用`IEC104_COMMAND_REJECT_UNKNOWN_IOA`回47;其他策略拒绝用`IEC104_COMMAND_REJECT_NEGATIVE`回否定7。 + +SELECT_WAIT、EXECUTE_WAIT期间,同会话generation、同station_id、同IOA的合法COT=6激活静默丢弃,返回HANDLED;不比较类型、OA、S/E、状态或QU,但type_mask及全部既有寻址、格式和值校验仍优先。无论应用是否已取走原工作事件,都继续处理该事件:丢弃不覆盖原请求、不刷新决定期限、不生成新事件或应答,只增加dropped_requests,不增加rejected、busy_rejected或overloads。期限到达后先终结原事务,再处理新请求;其他点仍忙拒绝,COT=8仍按撤销规则处理。 + +Accept先确认工作事件仍有效并准备确认存储,再消费事件、至多调用一次action。SELECT可传NULL,视为选择成功且select_effect=false;EXECUTE必须提供动作。action须短时返回,不推进Slave、不发布上报、不保留request指针;回调内只允许查询本实例Get_Status/Get_Transaction。异步设备工作由应用另存必要值和引用,设备结果回到同一事件循环提交。 + +| 动作结果 | 选择阶段 | 执行阶段 | +|---|---|---| +| NOT_STARTED | 否定确认,REJECTED;保证没有副作用 | 否定确认,REJECTED;不得后续调用Report_Result | +| STARTED | 肯定确认,SELECTED,开始选择期限 | 肯定确认,EXECUTING,开始执行期限 | +| FAILED或非法返回值 | 否定确认,ACTION_FAILED,置select_effect | 否定确认走Try,ACTION_FAILED,置execute_effect;可能已有变化 | + +`Accept==REPLY_OK`只表示事件已消费,以上三种结果都可返回OK。应用应检查自己保存的动作结果,或用Get_Transaction确认`active && phase==IEC104_COMMAND_PHASE_EXECUTING`,再安排执行结果流程;提交确认时可能已遇STOP或发送失败,所以STARTED也不保证返回后事务仍有效。选中后等待新的EXECUTE事件,不能再次Accept旧SELECT事件。 + +SELECTED阶段选择确认交给发送层后、选择未到期时,执行须匹配原会话、station_id、IOA、类型、OA以及屏蔽S/E后的命令字节(状态与QU)。此阶段选择确认尚未交接时收到执行,一律忙拒绝并保留选择;交接后同一点不匹配执行回否定7并以MISMATCH结束选择。其他点执行、SELECTED中的重复选择及EXECUTING/ENDED中的新激活仍忙拒绝,不破坏已有事务。 + +| Accept / Reject结果 | 是否可重试 | +|---|---| +| REPLY_OK | 工作事件已消费,不能重交;Reject也可能仍有待发送否定 | +| REPLY_WAIT | 未消费、未调用动作;保留同一事件,先读通知后再试 | +| REPLY_STALE | 已消费、身份/阶段失效或到期;停止旧操作,不能执行设备输出,继续处理FINISHED | +| REPLY_ERROR | 参数/原因/重入、服务或确认资源错误;检查错误并修复或取消,不能当作背压盲目重试 | + +决定窗口内丢弃的是同点合法激活,不要求内容完全重复,也不构成窗口外去重。已消费事件的重复Accept不会再调用动作;事务释放后,主站发来的相同命令仍按新请求处理,允许直接执行时可能再次输出。 + + + +#### 1.5.4 设备结果、返回信息与Complete + +执行STARTED后,应用持有原EXECUTE事件作为三个收尾接口的令牌。设备完成时优先调用Report_Result(SUCCEEDED或FAILED):它不要求肯定确认已经交接,OK立即停止执行期限。不要因确认还在WAIT就拖延报告设备结果;报告以后确认或返回信息可以继续等待发送层。结果只能记录一次,不可把SUCCEEDED改为FAILED。 + +```mermaid +sequenceDiagram + participant M as 主站 + participant C as Command + participant A as 应用事件循环 + participant D as 设备 + M->>C: 选择 S/E=1,COT=6 + C->>A: NextEvent:SELECT + A->>C: Accept(选择动作) + C-->>M: ACT_CON,S/E=1 + M->>C: 匹配执行 S/E=0,COT=6 + C->>A: NextEvent:EXECUTE + A->>C: Accept + C->>A: 本次执行动作 + A->>D: 启动输出 + A-->>C: STARTED + C-->>A: REPLY_OK + Note over C,A: 肯定执行确认可能尚在WAIT + D-->>A: 设备结果 + A->>C: Report_Result,OK停止执行期限 + C-->>M: 肯定ACT_CON + loop 零帧或多帧返回信息 + A->>C: Submit_Return_Info(WAIT保留原帧) + C-->>M: OK后按序发送COT=11或3 + end + A->>C: Complete + C-->>M: 按配置发送ACT_TERM + C->>A: NextEvent:FINISHED +``` + +图示选择了“设备结果早于确认交接”的合法次序,通常确认也可能先交接;网络箭头抽象表示发送层交接顺序,不代表本地整帧写完或主站已ACK,FINISHED也不等待这两个节点。真实驱动的启动调用发生在Accept动作中,Report_Result、返回信息及Complete都在动作返回后的事件循环执行。直接执行跳过前面的选择过程。 + +返回信息由应用构造,使用`IEC104_Command_Submit_Return_Info(cmd, &execute_event, &asdu)`,不得混用旧SELECT事件。必须为监视方向Type1..21或30..40、至少一个完整对象、COT=11或3、P/N=0,CA/TEST与命令一致;当前TEST命令被拒绝,正常返回TEST=0。IOA可不同于命令IOA,应用决定受控对象映射;104参数必须2/2/3且ASDU不超过249字节。 + +模块先等肯定执行确认交接,并让模块内所有已有应答先行,再用原请求身份走Try。WAIT不缓存ASDU、不为它保留FIFO位置,应用须保存同一帧的完整原文及EXECUTE事件;OK后帧序已固定,才可复用缓冲、推进到下一帧。返回信息不进入NORMAL,因此不会被覆盖或留给新连接,但持续HIGH/NORMAL积压仍可能推迟Try。 + +Report_Result之前或之后都可提交返回信息;设备结果已经可用时,应优先报告以停止执行计时。所有返回信息都OK后调用Complete,允许零帧。Complete的OK立即关闭返回信息阶段,Process按配置交接ACT_TERM;不能因等待终止而重交Complete或已成功的返回信息。不发ACT_TERM时仍须等待肯定确认交接才终结;失败配置发ACT_TERM时P/N=0只表示流程结束。 + +| 接口 | OK | WAIT | STALE | ERROR | +|---|---|---|---|---| +| Report_Result | 结果已记录、期限停止 | 有未读通知,结果未记录 | 事务到期/结束或事件不符,停止旧结果提交 | 参数/结果非法、有效事务已报告或已Complete、重入或服务错误 | +| Submit_Return_Info | 原ASDU已接手,不能重交 | 确认/FIFO/通知/发送条件未满足,保留原帧 | 事务/事件失效或发送失败终结,丢弃原连接重试 | ASDU/参数/服务错误,或Complete后尚待收尾;本次未接手 | +| Complete | 返回阶段关闭,后续由模块收尾 | 有未读通知,本次未接手 | 事务结束或事件不符 | 尚未报告结果、有效事务重复调用、参数/重入/服务或终止资源错误 | + +下面是应用持久化步骤的**伪代码**,其中“保存/等待设备/错误处理”为应用动作,不是库函数。不能在一次调用中遇WAIT后继续执行下一步: + +```text +保存原EXECUTE事件及应用站点引用 +等待设备结果时继续驱动Slave/Command,仍可提交已经产生的返回信息 +设备结果已知 → 步骤=REPORT +REPORT: IEC104_Command_Report_Result + OK → 步骤=RETURN;WAIT → 保存结果,返回事件循环 + STALE → 停止此事务重试,等待/处理FINISHED;ERROR → 应用错误处理 +RETURN: 尚有返回信息时 IEC104_Command_Submit_Return_Info + OK → 只推进这一帧;WAIT → 保留同一ASDU,返回事件循环 + STALE → 停止此事务重试,等待/处理FINISHED;ERROR → 修复本帧或应用错误处理 + 所有帧已OK或没有返回信息 → 步骤=COMPLETE +COMPLETE: IEC104_Command_Complete + OK → 步骤=WAIT_FINISHED;WAIT → 保留步骤,返回事件循环 + STALE → 停止此事务重试,等待/处理FINISHED;ERROR → 应用错误处理 +WAIT_FINISHED: 继续Process和NextEvent,不重复Complete,不再Submit_Return_Info +``` + +动作FAILED或事务STALE后若实际位置仍发生变化,不能再用旧事务的Submit_Return_Info;应用按需要用Enqueue_Event、COT=3上报。同一变位不要既经本接口又经NORMAL重复发布。 + + + +#### 1.5.5 三类期限与发送等待 + +所有业务期限取`IEC104_Net_Now_Ms`,不读设备墙钟。阶段切换后只保留一个计时起点,边界相等即到期;时钟倒退即使在决定/执行期限配置0时也走到期处理并增加clock_errors。Get_Status只算剩余时间,不执行转换。 + +| 期限 | 起点 → 正常终点 | 到期处理 | +|---|---|---| +| 决定 | SELECT/EXECUTE事件生成(含未被应用取走的时间)→ Accept/Reject消费 | 否定7,DECISION_TIMEOUT;事务从未交付任何工作事件时不产生FINISHED | +| 选择 | 选择动作返回后重新取时间 → Handle_Event受理匹配执行 | SELECT_TIMEOUT,丢弃模块尚持有的选择确认;已交发送层的迟到肯定确认无法撤回 | +| 执行 | 执行动作返回STARTED后重新取时间 → Report_Result | EXECUTE_TIMEOUT,不发ACT_TERM;尚未交接的肯定执行确认照常提交 | + +Handle_Event在有效遥控I帧的匹配前检查;Process、NextEvent及应用应答入口在允许推进时先检查会话,再检查期限。未读通知优先于这些推进,但不能挡住Handle_Event消费通知;已存在FINISHED直接交付。主站命令的“到达”指Handle_Event受理时刻,含Slave通知排队时间,不是网络成帧时间。 + +Accept/Reject后决定计时停止;Report_Result后执行计时停止,不因后续发送WAIT超时。选择期限有意包含选择确认的发送等待,限制返校最长保持时间。报告结果后漏调Complete会持续占槽,直到应用Cancel或会话结束;链路和本地发送期限仍独立生效,见[期限查询](#flow-deadlines)。 + + + +#### 1.5.6 撤销、STOP和发送故障 + +主站撤销使用COT=8,须匹配会话、station_id、IOA、类型和OA,不比较状态/QU;请求本身仍须先通过格式和对象值合法性校验。撤销选择不是STOPDT,不停用整个连接。 + +| 情况 | 应答和事务处理 | +|---|---| +| SELECT_WAIT、SELECTED、EXECUTE_WAIT匹配撤销 | 复用事务槽回肯定DEACT_CON(9),替换模块尚持有的确认,DEACTIVATED结束;是否FINISHED取决于是否曾交付工作事件 | +| EXECUTING、ENDED或不匹配撤销 | 独立否定9,原事务不受影响;不能借撤销回滚已启动设备 | +| 选择确认已由普通发送接口返回OK | 即使仍在HIGH也不能撤回,DEACT_CON排在它之后 | +| DEACTIVATED通知(STOPDT) | 丢弃模块尚持有应答,STOPPED结束;后续START不恢复旧选择或执行令牌 | +| DISCONNECTED / 新CONNECTED | 清理旧会话事务,原因DISCONNECTED / CONNECTION_CHANGED;旧返回信息不转到新连接 | +| Cancel | CANCELLED结束,清空模块持有应答,会话保持原激活状态;不发送主站撤销确认 | +| 发送ERROR,确认为原连接正常STOP且通知尚未分派 | 保留应答并让出,Submit_Return_Info返回WAIT;继续分派DEACTIVATED,由它终结为STOPPED | +| 其他发送ERROR或FATAL | SEND_ERROR结束,清空模块持有应答;Submit_Return_Info返回STALE,FATAL可能已写出字节,不能重发 | + +首次终结原因和FINISHED的工作请求不因后来的STOP/Cancel改变。FINISHED只提示应用结束协议交互,不隐式撤除返校、停止物理输出或回滚点值。设备仍忙时,应用必须拒绝后续不安全操作,即使模块事务槽已经释放。 + + + +#### 1.5.7 FINISHED配对、站点引用与退出 + +同一事务只要取走过SELECT或EXECUTE,就恰好产生一次FINISHED;从未交付工作事件便撤销/到期时直接移除事件。SELECT已读、匹配EXECUTE尚未读便结束仍需FINISHED,request是最后接纳的EXECUTE请求。撤销和失配请求只用于应答回显,不覆盖FINISHED的request;应用按instance_id与transaction_id清理资源,不根据新CA猜测归属。 + +COMPLETED/EXECUTION_FAILED在Complete后的最后应答交接后交付;其他原因可先于否定确认或DEACT_CON交接。模块释放槽需要同时满足“FINISHED已读或无需交付”和“事务应答全部交接/取消”。期间busy仍为true,所有站点的新遥控都被拒绝;独立短应答本身不占事务槽,但也须在Destroy前清空。 + +模块创建事务时Acquire站点、释放槽时Release;应用保留context或异步设备操作需要自己的引用。首个工作事件到来时先按request.station_id查原节点并Acquire,成功后才保存context与事务身份;后续匹配EXECUTE沿用同一引用。查找/Acquire失败走应用错误清理,不能仅按CA改绑到新站。站点删除后新请求回46,已有事务引用继续保护原节点。 + +`NextEvent(FINISHED)`可能在返回前释放模块引用,因此不能等读到FINISHED才申请应用引用。应用完成返校清理且物理输出确已结束后才Release;execute_effect=true遇超时、断线、发送错误时,设备可能仍在运行。Get_Transaction只查询当前仍占槽事务,刚读出的FINISHED若触发回收,查询返回false且不写输出,这是正常结果。 + +正常退出按以下顺序执行,完整服务关闭流程见[关闭释放](#flow-15): + +1. 停止产生新动作,分别Cancel遥控及其他业务模块。 +2. 读取全部业务FINISHED;停止原事务重试,清理应用资源。真实输出尚未结束时继续完成应用设备清理和引用归还。 +3. Slave_Close,读空保留的Slave通知,把非I帧生命周期通知交给模块,不再受理新请求或执行动作。 +4. 再读空可能产生的FINISHED,Command_Destroy;false时检查未读事件、事务或独立应答,不能丢弃句柄假定成功。 +5. 所有业务和应用引用归还后Remove_Station,最后Slave_Destroy。 + + + +#### 1.5.8 事件循环与挂起重试 + +示例[iec104_slave_example.c](../../examples/iec104_slave_example.c)由Example_Handle_Event分派通知,Example_Step先Command_Process,再Example_Drain_Command读取业务事件和重试应用步骤,随后推进召唤与校时。Example_Command_Action只改变模拟设备;Example_Drain_Command在动作外报告结果、构造并逐帧交付返回信息;FINISHED清标志和引用,保留已执行点值。 + +等待前重新Get_Status并合并应用状态: + +| 调度条件 | 下一步 | +|---|---| +| events_pending或本轮Slave通知未读空 | 立即续读并分派Slave通知,不执行旧工作事件 | +| pending_events非零 | 及时NextEvent,尤其读取可离线交付的FINISHED | +| need_process=true | 安排本地Command_Process;仍按通知优先,不把它理解为必能发送 | +| timer_active=true | 等待上限取timeout_remaining_ms与网络/应用上限的较小值,0即立即检查期限 | +| 应用有可立即决定的工作事件、刚完成设备结果或未尝试的收尾步骤 | 立即进行相应应用接口;Process不会替应用报告结果或再次提交ASDU | +| 应用步骤上次返回WAIT | 保留事件、结果/ASDU和步骤,允许网络等待;下一轮网络推进并读空通知后重试,不等待新的工作事件 | + +Get_Status不执行超时,need_process=false也不免除每轮网络推进后的Process及应用重试。模块最多推进`2 * (1 + IEC104_COMMAND_REPLY_CAPACITY)`步,队首WAIT结束本轮;不能用紧循环反复重试Try。通知、业务FINISHED、应用设备完成是不同来源,都须纳入唤醒和等待策略;接入自有事件循环见[第14节](#flow-14)。 + ## 2. 创建服务与接入主站 @@ -508,6 +759,8 @@ IEC60870_Asdu_Get_Data(&event.asdu, &bytes, &length) 解析视图 `info` 借用参数和原文字节,二者在取对象期间必须有效且不变。`Get_Element` 复制出具体对象,但输出结构须由 Type ID 选择;尺寸足够不能代替正确的 C 类型。若随后改写事件的 COT/P/N,不能继续把旧解析视图当作更新后的公共头。 +以上是自定义业务直接使用公共编解码器的流程。交给Command_Handle_Event的45/46由模块按协议错误优先级完成校验,应用不先按“完整解析失败就忽略”过滤它,也不另发ACT_CON;应用从SELECT/EXECUTE事件开始检查点表与策略,见[遥控通知分派](#flow-command-dispatch)。 + ### 6.3 延迟处理、事件复制与连接寿命 `IEC104_TcpEvent_t` 只是值与字节数组,可以按值复制;`IEC104_SlaveEvent_t` 含指向自身的 ASDU 视图和 connection 上下文,**不能用结构赋值、memcpy 或搬移数组元素来保存**。事件使用期间地址必须稳定。 @@ -526,7 +779,7 @@ Copy_Event 不发送报文,也不把历史请求重新绑定到当前主站。 READY 会替换传给 NextEvent 的整个输出;EMPTY/ERROR 保持输出原值。把事件地址交给待处理任务后,不应再用同一存储接收下一条事件。多个应答可以使用同一个原请求 connection,待发 ASDU 也可以单独构造,不必都复用 `event.asdu`。 -需要精简任务存储时,另一种方式是保存`Slave_Get_Event_Identity`返回的纯值身份,以及业务需要的请求字段/字节。随后先处理通知并检查原请求,再用`Slave_Send_Response / Slave_Try_Send_Response`提交独立ASDU;失败需关闭原连接时用`Slave_Close_Connection_By_Identity`。这些入口保留原服务实例和原连接代次检查。总召采用此方式;完整事件及Copy_Event接口继续保留,容量及排序不因内部存储精简改变。 +需要精简任务存储时,另一种方式是保存`Slave_Get_Event_Identity`返回的纯值身份,以及业务需要的请求字段/字节。随后先处理通知并检查原请求,再用`Slave_Send_Response / Slave_Try_Send_Response`提交独立ASDU;失败需关闭原连接时用`Slave_Close_Connection_By_Identity`。这些入口保留原服务实例和原连接代次检查。总召、校时及遥控内部采用此类存储;完整事件及Copy_Event接口继续保留。应用接入遥控时只保存NextEvent交付的纯值CommandEvent,不能为延迟操作另造连接身份或绕过事务接口。 @@ -538,6 +791,8 @@ READY 会替换传给 NextEvent 的整个输出;EMPTY/ERROR 保持输出原值 下图为普通事件应答路径。总召确认/终止经`IEC104_Slave_Send_Response`按保存的原身份进入同一普通HIGH路径;数据经`IEC104_Slave_Try_Send_Response → IEC104_TcpServer_Try_Send_Response → IEC104_Connection_Try_Submit_Response → IEC104_SendManager_Try_Submit_Response`:当前发送缓冲空闲、两级队列均无未提交条目且链路/窗口允许时才直接组帧;否则WAIT,不缓存HIGH。到期协议工作也先返回WAIT,由网络Process处理已到达的ACK。OK后沿同一发送缓冲、记录和非阻塞写入路径推进。 +遥控采用不同的业务提交规则:选择确认、DEACT_CON及无执行副作用的否定走普通路径;执行动作返回STARTED/FAILED后的确认、返回信息和ACT_TERM走Try。模块内待发应答全局FIFO,应用返回信息还须等待模块已有应答清空,不能直接调用共用Send_ACT_TERM绕过Complete。普通HIGH的OK仅表示交接,Try的OK已经确定帧序;具体应用步骤见[执行收尾](#flow-command-execution)。 + ```text 应用:IEC60870_MasterConnection_Send_ASDU / Send_ACT_CON / Send_ACT_TERM → 共用层检查连接及函数指针,分派给 Slave 的发送适配器 @@ -577,6 +832,8 @@ HIGH 是当前连接的应答队列。STOP 受理后尚留在 HIGH 的应答会 ### 7.2 主动上报:NORMAL +独立变位可以走本节NORMAL路径。遥控引起且属于当前有效事务的返回信息使用Submit_Return_Info,避免覆盖和跨连接恢复,且保证在肯定执行确认之后、ACT_TERM之前。动作FAILED或原事务已经STALE后仍需报告设备变化时,应用另按COT=3上报;同一变化不能在两条路径重复发布。 + ```text 应用:取得 Slave 的应用层参数 → 具体信息对象 Create() @@ -791,6 +1048,8 @@ I 帧受理后,Connection 从首个尚未确认 I 帧的成帧时刻记录 t2 对端主动发来的 TESTFR_ACT 走另一条应答路径:`IEC104_SendManager_Handle_Testfr_Act` 调用 `IEC104_Link_Handle_Testfr_Act`,生成 TESTFR_CON 并进入控制 FIFO。提前或没有匹配过程的 TESTFR_CON 不缓存为未来确认。 + + ### 11.3 期限查询与执行 ```text @@ -821,6 +1080,8 @@ TcpServer 查询等待条件 t2/t3 只有在控制容量和 Link 条件允许时才作为可调度动作,避免无容量时按已到期时间不断空转。协议时间使用单调毫秒时钟,Windows 后端通过 `IEC104_Net_Now_Ms` 提供;与业务的 CP56Time2a/RTC 时间分开。 +上表只列传输期限。Command的决定/选择/执行期限由业务入口检查,Slave不会代它唤醒;应用须读取Command_Get_Status并缩短本轮等待,见[遥控三类期限](#flow-command-timeouts)和[自有事件循环](#flow-14)。设备结果已知时及时Report_Result,不能把确认发送等待误算成设备执行耗时。 + ## 12. 容量不足时,流程在哪里等待 @@ -833,16 +1094,19 @@ t2/t3 只有在控制容量和 Link 条件允许时才作为可调度动作, | 普通通知空间不足,只有已确认记录 | 记录暂留 Manager;不会单凭这项积压阻挡前面的 S/U 处理或 CON 发送 | 有普通空间和交付预算后转为 RECORD | | 启停专用槽满,下一 ACT 会改变状态 | 该 ACT 留在 Receiver,尚未改变 Link 状态,也不为它生成 CON | NextEvent 消费启停通知后,由 Process/Poll 重试受理 | | 控制帧槽满 | 需要预留槽的 START/TESTFR 请求暂缓,已有控制发送保留 | 既有帧完成并释放控制槽后重试 | -| HIGH 空间不足 | 本次业务应答未接手,提交返回 WAIT | 应用保留原文,在连接和准入仍有效时重试 | +| HIGH 空间不足 | 普通应答未接手,提交返回WAIT;独立业务模块保留自己的应答 | 模块Process重试;自定义业务自行保留原文。同步44兜底采用丢新策略,不盲目保存旧事件 | | k 窗口用尽 | 已提交发送继续等待确认,不为新业务分配 N(S) | 收到合法累计 ACK | | 发送记录槽用尽 | 即使有些帧已 ACK,也不能再建新 I 帧记录 | 已确认记录成功复制到事件 FIFO 后释放槽 | | socket 暂不可写 | 当前 APDU 和偏移保留 | 可写或适用的重试调度后继续 | +| 遥控Try确认或ACT_TERM等待 | Command保留队首和全局FIFO序号,后面的忙拒绝也等待 | 网络推进、通知读空后仍调用Command_Process | +| 遥控返回信息WAIT | 应用保留原EXECUTE事件及同一ASDU,模块未缓存也未预留排序位置 | 继续驱动网络/模块,再重试当前应用步骤;设备结果不因此延迟报告 | +| 遥控FINISHED未读或最后事务应答未交接 | 唯一事务槽保持busy,新的遥控否定;独立应答池满则丢新计数 | 及时读取FINISHED并推进剩余应答,两条件满足后释放槽 | 其中,**启停槽满不再扣住已经生成的 CON**。限制发生在下一次改变状态的 ACT 受理之前;已有 CON 仍可续写。同状态重复 ACT 不需要新增启停通知槽,仍按自身控制发送条件处理。 不过,保留的 ACT 或 I 帧仍占据同一 TCP 字节流的前部,后续帧不能越过它。当前实现保留这个顺序约束,应用应及时消费通知。等待不直接等于断线,但不会延长原协议期限。 -`NextEvent` 本身只复制、移除最早事件并安排后续工作,不执行接收、发送或协议受理。因此“读走一条通知”和“被暂缓的 ACT 得到受理”之间,还需要一次 Process/Poll 推进。 +`Slave_NextEvent`本身只复制、移除最早通知并安排后续工作,不执行接收、发送或协议受理。因此“读走一条通知”和“被暂缓的ACT得到受理”之间,还需要一次Slave_Process/Poll推进。业务模块的NextEvent具有各自契约,例如Command_NextEvent还会校验会话、检查期限或释放已结束槽,不能把这项结论套到所有同名入口。 通知 FIFO 保证已经入队的通知逐条交付。启停变化不合并、不覆盖;但 ACK 回执若因普通容量或预算暂留内部,可能晚于随后入队的启停通知。不能把所有 RECORD 与启停事件理解为严格按网络发生时刻排序。 @@ -929,35 +1193,48 @@ DISCONNECTED(旧 generation) NORMAL 恢复依据是仍保留的原文,不会恢复已经覆盖的旧条目。未 ACK 不保证对端没有收到,因此跨连接恢复可能重复,业务应按自身协议处理重复。 +遥控选择、EXECUTE令牌和尚未提交的返回信息均不跨连接恢复。应用分派旧DISCONNECTED并处理FINISHED后,才能按新工作事件建立新事务;同CA重建站点也不能替换旧station_id。设备输出是否完成与网络重连独立,应用仍须维护忙状态和引用,见[异常终结](#flow-command-abort)。 + ## 14. 接入应用自己的事件循环 独立循环使用 `IEC104_Slave_Poll` 即可;已有 select、GUI 或其他统一事件循环时,使用 `IEC104_Slave_Prepare / Process` 替代 Poll。Slave 复用 TCP 层的调度值类型,不需要应用取得私有 TcpServer。 +下面是包含独立业务模块的循环伪代码。网络、业务、应用设备完成分别产生工作;四类业务的推进方式见[总召](#flow-interrogation)、[计数量](#flow-counter-interrogation)、[校时](#flow-clock-synchronization)及[遥控调度](#flow-command-schedule)。按各接口自己的结果类型判断:Slave_Process等比较IEC104_TCP_OK,业务Process/Get_Status的false进入错误处理,不能把失败当作本轮已读空或已提交。Get_Transaction的false也可能只是事务槽已正常回收,须按该查询的契约处理。 + ```text 1. IEC104_Slave_NextEvent() - 先处理已有业务通知,逐条读到 EMPTY。 + READY → 按FIFO分派;EMPTY → 进入第2步;ERROR → 保存错误并清理。 + 通知预算用完但未EMPTY → 立即续读,不执行旧业务动作。 -2. IEC104_Slave_Prepare(slave, &info, &error) - 取得 service_token、socket/token、关注位、定时条件。 +2. 推进已启用的业务模块并处理应用工作: + Command_Process → Command_NextEvent → 工作事件Accept/Reject、设备结果Report_Result、 + 挂起Submit_Return_Info/Complete重试及FINISHED清理。 + 两个Interrogation实例轮换Process/NextEvent/显式应答;校时Process/Execute_Next。 + 每个WAIT保留当前交互;若期间产生Slave通知,先回第1步。 -3. 判断是否能等待: - pending_event_count > 0 → 继续消费事件,再重新 Prepare。 - need_process != 0 → 使用当前 service_token 构造本地空批次, - 调用 Process;之后处理事件并重新 Prepare。 - 其余情况 → 更新完整 socket 注册集合及一次性定时器, - 与应用其他任务一起等待。 +3. IEC104_Slave_Prepare(slave, &info, &error),成功后查询各业务Get_Status。 + 取得service_token、socket/token、关注位、网络与业务的定时条件。 -4. 将本轮 socket 就绪和定时到期汇成一个 IEC104_TcpReady_t: +4. 合并本轮等待: + Slave有pending_event_count或业务events_pending → 回第1步。 + Slave need_process → 用当前service_token构造本地空批次,Process后回第1步。 + 业务有pending_events/need_process、校时execute_pending,或设备结果/未尝试的应用步骤就绪 + → 本地续跑第2步。 + 其余情况 → 更新完整socket注册集合;等待上限取网络、全部业务剩余期限、应用上限的最小值。 + 应用步骤上次WAIT不单独强制0等待;网络推进后重试,不忙轮询。 + +5. 将本轮 socket 就绪和网络定时到期汇成一个 IEC104_TcpReady_t: 同一 socket 的读写位合并。 token 使用实际注册时保存的值。 - timer_expired 只在定时器真正到期时置 1。 + timer_expired只在Slave_Prepare报告的网络期限真正到期时置1。 + 只有业务期限或设备完成唤醒时,不伪造socket就绪/网络超时,可使用本地空批次。 -5. IEC104_Slave_Process(slave, &ready, &error) +6. IEC104_Slave_Process(slave, &ready, &error) 不等待,有界推进当前输入、发送、期限和接入。 -6. 回到第 1 步。 +7. 回到第1步;业务等待条件需重新查询,不能仅重用上轮快照。 ``` 本地唤醒可使用以下字段组合,不伪造网络就绪: @@ -974,18 +1251,22 @@ ready.service_token = info.service_token; `timer_active == 1 && timeout_ms == 0` 表示应立即调度;`timer_active == 0` 表示没有定时器,两者不能混淆。`UINT32_MAX` 仍是有限毫秒数。超时从 Prepare 查询时刻计起,若中途处理其他任务耗时较长,应在真正等待前重新 Prepare。采用边沿或一次性通知的事件循环也必须落实 `need_process`,并按最新快照重新注册,避免预算让出后无人续跑。 +业务剩余时间用各自Get_Status重新采样,遥控对应`timeout_remaining_ms`;Slave_Prepare不会自动合并它们。遥控Report_Result后的应用收尾可能没有业务期限,必须靠设备完成或挂起步骤状态继续驱动,不能只等网络定时器。FINISHED可以离线读取,清理资源不以连接激活为前提。 + 旧服务 token 的整批通知被忽略;已关闭或替换 socket 的旧注册通知也被忽略。不要把旧通知的 token 换成新值,否则会破坏隔离。 ## 15. 整个服务关闭与对象释放 -接入总召模块时,先Interrogation_Cancel,再读取所有业务FINISHED并清理应用资源;最后在Slave_Destroy之前释放总召实例。总召的业务FIFO和Slave的通知FIFO须分别读空,详见[总召接入](#flow-interrogation)。 +关闭前先停止发起新业务,并分别取消所有已创建模块:总召/计数量用Interrogation_Cancel,校时用ClockSynchronization_Cancel,遥控用Command_Cancel。召唤和遥控仍可能欠应用FINISHED,须分别读取并清理引用;校时取消不回滚已经设置的时钟。遥控FINISHED只表示协议结束,物理输出仍忙时须继续应用设备清理,见[遥控引用与退出](#flow-command-finished)。 使用共用站点表时,还须归还业务引用、结束全部快照、解除集合锁,并Remove所有注册节点后才可Slave_Destroy。节点Is_Reusable为true后才可释放;Close/Init保留站点表,不能用网络关闭代替业务清理。 ```text 应用停止提交新业务 + → 各业务Cancel + → 总召/计数量/遥控各自NextEvent读完FINISHED,清理应用资源和已完成设备操作的引用 → IEC104_Slave_Close() → IEC104_TcpServer_Close() ├─ 当前客户端按 Drop_Client 流程收尾 ├─ 关闭监听 socket @@ -993,14 +1274,18 @@ ready.service_token = info.service_token; └─ IEC104_Net_Cleanup() → 应用撤销自有事件循环中的 socket 注册和定时器 → IEC104_Slave_NextEvent() - 逐条处理保留下来的事件,直到 EMPTY + 逐条读空保留通知,向业务模块分派非I帧生命周期通知;不再受理新请求或执行动作 + → 再读完业务FINISHED,完成必要设备清理和应用引用归还 + → Interrogation_Destroy(总召/计数量各自)、ClockSynchronization_Destroy、Command_Destroy + 每个成功后才丢弃句柄;失败须检查活动任务、未读事件或未清空应答 + → 结束站点快照/集合锁,归还引用,Remove全部站点 → IEC104_Slave_Destroy() ├─ IEC104_TcpServer_Destroy(),归还私有 TCP 槽 └─ 归还 Slave 槽,保留不回绕的实例编号历史 → 应用保存的句柄置 NULL ``` -Close 不等待业务发送或 ACK,保留未读事件,但不保留 NORMAL 缓存。Destroy 要求服务已经关闭或尚未 Init,且事件已读空;不会隐式丢弃事件或替调用者关闭运行中的服务。 +Slave_Close不等待业务发送或ACK,保留未读通知,但不保留NORMAL缓存。Slave_Destroy要求服务已经关闭或尚未Init、通知已读空且站点资源已清理;不会代替业务Cancel/Destroy。Command_Destroy另要求事务槽、FINISHED及独立应答槽均空,Cancel后仍须读完FINISHED;不能把网络已关闭当作所有对象已释放。 Close 后读空事件,也可以再次 Init 重开;新服务和连接的身份编号不复用旧编号。Init 失败时句柄仍归应用持有,可处理错误后重试或 Destroy。 @@ -1036,6 +1321,10 @@ Close 后读空事件,也可以再次 Init 重开;新服务和连接的身 ```text 子站入口与身份绑定:IEC104_Slave.c 站总召/分组总召、撤销、业务期限与事件:104/business/IEC104_Interrogation.c +Type100种类规则:104/business/IEC104_GeneralInterrogation.c +计数量读取/冻结/复位:104/business/IEC104_CounterInterrogation.c +Type103校时校验与显式动作:104/business/IEC104_ClockSynchronization.c +Type45/46选择执行、结果及返回信息:104/business/IEC104_Command.c 共用发送接口分派: IEC60870_Slave.c → IEC104_Slave.c 内的发送适配器 ASDU 与信息对象: IEC60870_Asdu.c → IEC60870_InformationObjects.c 监听和网络调度: IEC104_TcpServer.c @@ -1048,5 +1337,5 @@ ASDU 与信息对象: IEC60870_Asdu.c → IEC60870_InformationObjects.c 平台非阻塞 I/O: IEC104_Net_Windows.c ``` -库提供命令、校时、总召、参数和文件等信息对象的编解码,以及响应提交接口。独立召唤引擎已实现站/分组总召和计数量读取/冻结/复位,应用通过NextEvent与显式应答管理数据源。独立ClockSynchronization已实现Type 103校验、补偿、期限及原文确认,仅由Execute_Next调用应用动作。遥控、参数和文件业务尚未接入;业务级权限及硬件操作仍由应用提供。Time的编码转换、Clock的墙上时间与Net的协议单调时钟属于不同用途;默认示例校时只更新模拟值。 +库提供命令、校时、总召、参数和文件等信息对象的编解码,以及响应提交接口。召唤引擎已实现站/分组总召和计数量读取/冻结/复位;ClockSynchronization已实现Type103校验、补偿、期限及原文确认;Command已实现Type45/46选择执行、撤销、三类期限和按事务提交返回信息。遥调47..51、带时标命令58..64、参数和文件业务尚未接入,不能把对象编解码支持等同于完整业务支持。业务权限和硬件操作仍由应用提供;Time转换、Clock墙上时间及Net单调计时用途不同,默认示例只更新模拟时钟和模拟遥控点值。 diff --git a/IEC60870-Slave/docs/104/IEC104_设计架构.md b/IEC60870-Slave/docs/104/IEC104_设计架构.md index 4bdba469..febe56c1 100644 --- a/IEC60870-Slave/docs/104/IEC104_设计架构.md +++ b/IEC60870-Slave/docs/104/IEC104_设计架构.md @@ -6,7 +6,8 @@ > **同步日期**: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-21。`IEC60870_InformationObjects.c/h`提供12种监视、18种命令的独立对象编解码;`IEC60870_Asdu.c/h`统一维护公共头、VSQ/SQ、容量、对象分派及原文应答。编码经对象函数表,解码按Type ID分派;带时标类型复用基础值处理及公共CP56工具。该模块由业务显式调用,不改变Receiver准入或链路序号处理;当时实际总召、校时和遥控流程仍待业务接入。接口及示例见[README](README.md#asdu信息对象模块)。 +> **业务同步**:2026-10-30。总召、计数量、Type103校时及Type45/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副本。 @@ -469,7 +470,9 @@ Receiver已实现每连接成帧和完整APDU交付;独立发送队列、业 | `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) | 待实现业务模块 | 公共信息对象已提供编解码;尚未接入选择执行、设备动作和时标策略 | +| 实际遥信/遥测数据源、输出及设备设时 | 应用接入层 | 按业务模块请求读取点表或执行设备动作;示例提供模拟点表、遥控输出和模拟时钟,协议库不自动操作硬件 | ### 10.2 统一连接上下文 @@ -525,6 +528,7 @@ I帧完成接口要求快照的 `N(S)==sent_vs`,只推进本地完成边界和 | 1(已实现接收基础) | Receiver独立实例、完整帧视图、消费数和显式释放;Link由连接层共享 | 两个接收实例可交错输入;待处理帧不被后续Feed覆盖,旧代次调用被拒绝 | | 2(已实现) | 接入发送前准备、部分写入续传及独立确认记录,补齐实际I/O和收发串行调度 | 发出 `N(S)=0` 后收到 `N(R)=1` 能正确确认并释放原ASDU;发送失败不重复推进状态,旧确认不清除新增待确认数据 | | 3(已实现) | 连接生命周期、U 控制、定时器及窗口策略 | 受控传输与时间事件下可验证启动、停止、心跳、超时和窗口边界;序号错误停止当前批次并关闭连接 | -| 4(对象及三类业务已实现) | 公共对象编解码、总召/计数量、校时校验及显式执行;继续接入真实数据源和遥控 | 自动回归覆盖地址路由、召唤进度、校时原文确认及时间边界;模拟模式不设系统时间,硬件设时和实际主站联调须另行验收 | +| 4(对象及四类业务已实现) | 公共对象编解码、总召/计数量、校时及Type45/46遥控;继续接入真实数据源和设备输出 | 验证地址路由、召唤进度、校时原文确认、遥控选择执行/撤销/期限及返回信息顺序;模拟模式不设系统时间,硬件设时、实际输出和主站联调须另行验收 | +| 5(待实现) | 遥调47..51及带时标命令58..64 | 补充按类型匹配、时标解码及延迟策略,再扩展应用接口与回归 | --- diff --git a/IEC60870-Slave/docs/104/README.md b/IEC60870-Slave/docs/104/README.md index 264a676c..2bced35e 100644 --- a/IEC60870-Slave/docs/104/README.md +++ b/IEC60870-Slave/docs/104/README.md @@ -36,6 +36,8 @@ Slave提供[共用站点表](IEC104_API接口汇总.md#station-registry):应 | `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/46遥控、选择执行、撤销、三类期限及返回信息;见[遥控设计](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) | @@ -68,6 +70,7 @@ Slave提供[共用站点表](IEC104_API接口汇总.md#station-registry):应 | 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`,头文件搜索路径增加`104/business/`;每Slave一个实例、一个事务,Accept显式执行动作,应用报告结果和提交返回信息 | | 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`;具体芯片驱动暂不提供 | @@ -125,7 +128,7 @@ I/S的启停准入和停止阶段立即确认规则集中在 `IEC104_Link.c` 的 公共层仅需 `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、分组及遍历状态;其他命令的选择/执行、校时权限及业务任务由应用接入。 +业务包含`IEC104_Slave.h`,取得`Slave_NextEvent`交付的I帧后可直接使用ASDU视图并解析具体对象,或先构造ASDU再交给发送接口。ASDU编解码层不分配内存、不操作连接、不读取或设置系统时钟;共用连接接口可构造并提交确认/终止应答。站/分组总召可交给IEC104_Interrogation管理校验、分批、撤销、期限和重试,应用响应业务事件提交数据,自行维护IOA、分组及遍历状态;Type45/46的选择/执行由IEC104_Command管理,Type103由独立校时模块管理,权限策略及设备动作仍由应用接入。 | 层次 | 职责 | |---|---| @@ -321,6 +324,14 @@ BEGIN携带实际CA、station_id和QOI=20..36;应用检查数据源并决定 调用过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/46、COT=6激活与COT=8撤销;默认两种类型都须先选择。每个Slave只能绑定一个实例,同一实例从创建事务到读取FINISHED并交接完应答期间一直忙,所有站点的新遥控互斥。Type47..51及58..64未接手,继续交给后续处理器或44兜底。 + +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)。 + ## 启停主流程 | 角色与过程 | 调用顺序 | diff --git a/IEC60870-Slave/docs/README.md b/IEC60870-Slave/docs/README.md index a0f1092d..a799987b 100644 --- a/IEC60870-Slave/docs/README.md +++ b/IEC60870-Slave/docs/README.md @@ -5,7 +5,7 @@ ## 推荐阅读顺序 1. [模块说明与接入示例](104/README.md):公共 ASDU、子站统一入口及模块依赖。 -2. [子站示例说明](examples/IEC104子站示例.md):构建运行、模拟点表、总召、计数量与校时处理。 +2. [子站示例说明](examples/IEC104子站示例.md):构建运行、模拟点表、总召、计数量、校时与遥控处理。 3. [完整调用流程](104/IEC104_完整调用流程.md):从应用入口到协议收发的调用关系。 4. [API 接口汇总](104/IEC104_API接口汇总.md):按文件查阅接口。 5. [测试说明](test/IEC104测试文档.md):测试入口、构建命令、覆盖范围与验证记录。 @@ -23,8 +23,9 @@ | 104 | [总召设计](104/IEC104_Interrogation_设计架构.md) | 站总召、分组、撤销、超时与业务事件 | | 104 | [计数量设计](104/IEC104_CounterInterrogation_设计架构.md) | 读取、冻结、复位、动作执行及双实例接入 | | 104 | [时钟同步设计](104/IEC104_ClockSynchronization_设计架构.md) | Type 103、显式设时动作、滞留补偿、期限及广播确认 | +| 104 | [遥控设计](104/IEC104_Command_设计架构.md) | Type 45/46、选择执行、撤销、期限、返回信息及FINISHED清理 | | 104 | [完整调用流程](104/IEC104_完整调用流程.md) | 各层调用顺序与业务流程 | | 104 | [API 接口汇总](104/IEC104_API接口汇总.md) | 104 及其依赖的公共接口 | | 公共 | [时间工具设计](common/IEC60870_Time_设计架构.md) | CP16/CP24/CP56、日历转换与时钟接口 | -| 示例 | [子站示例说明](examples/IEC104子站示例.md) | 编译运行、命令参数、点表与总召接入 | +| 示例 | [子站示例说明](examples/IEC104子站示例.md) | 编译运行、命令参数、点表及四类业务接入 | | 测试 | [测试说明与验证记录](test/IEC104测试文档.md) | 公共模块、104 及真实 TCP 回环测试 | diff --git a/IEC60870-Slave/docs/examples/IEC104子站示例.md b/IEC60870-Slave/docs/examples/IEC104子站示例.md index 1f4768ff..aac804d2 100644 --- a/IEC60870-Slave/docs/examples/IEC104子站示例.md +++ b/IEC60870-Slave/docs/examples/IEC104子站示例.md @@ -2,7 +2,7 @@ [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个标度化遥测值;分组1(QOI=21)只返回遥信,分组2(QOI=22)只返回遥测。库支持全部16组,示例未配置的其余组由应用拒绝。通信通过 `IEC104_Slave`,总召通过 `IEC104_Interrogation` 接入,所有任务使用固定存储、在主循环线程串行推进。示例另含计数量召唤和Type 103校时;默认时钟为模拟值,实际设备数据源及遥控仍由应用接入。 +当前实现站总召(`C_IC_NA_1`,QOI=20),依次返回8个单点遥信、8个双点遥信和8个标度化遥测值;分组1(QOI=21)返回单点及双点遥信,分组2(QOI=22)只返回遥测。库支持全部16组,示例未配置的其余组由应用拒绝。通信通过 `IEC104_Slave`,总召通过 `IEC104_Interrogation` 接入,所有任务使用固定存储、在主循环线程串行推进。示例另含计数量召唤、Type 103校时及Type 45/46遥控;默认时钟为模拟值,遥控只更新内存点值,实际设备数据源和物理输出仍由应用接入。 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)。 @@ -24,11 +24,11 @@ config.broadcast_capacity = sizeof(gi_stations) / sizeof(gi_stations[0]); 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。 -主循环向总召、计数量、校时实例广播生命周期通知,交替推进召唤业务并显式执行校时;其他类型在三个模块均未接手后兜底。原Example_Init仍可用于仅总召测试,计数量及校时通过各自初始化辅助函数补充,两个召唤实例各需独立广播状态区,校时使用共用站点快照。协议及应用动作限制见[计数量设计](../104/IEC104_CounterInterrogation_设计架构.md)和[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)。 +主循环向总召、计数量、校时、遥控实例广播生命周期通知,读空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` 引入业务声明;构建脚本添加 `104/`、`104/business/` 和 `common/` 头文件搜索路径,编辑器也应配置这三个目录。构建可执行程序仍使用下面的脚本,它会链接所需库源文件、Clock适配及Windows网络库;只编译当前C文件会缺少库函数实现。编辑器的C语言标准应与构建保持一致,使用C11,并选择实际的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 @@ -53,6 +53,8 @@ FRZ=1冻结并将序号模32加一;FRZ=2另将当前值清零;FRZ=3仅清当 启动输出 `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`,分别表示已接受的选择、已启动的执行、成功完成事务、已交接应答和返回信息帧数;保留原有统计字段的含义。 + ### 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,不返回本站当前时间。 @@ -61,27 +63,46 @@ FRZ=1冻结并将序号模32加一;FRZ=2另将当前值清零;FRZ=3仅清当 确认发送WAIT时只重试原确认,不再次设时;STOP取消未交接确认而不撤销已完成的动作。同内容的新请求会再执行,旧时标重发可能回拨。真实数据发布方应在confirm_pending期间暂停新的带时标上报并继续驱动网络,避免持续新增NORMAL推迟确认。完整范围及期限见[时钟同步设计](../104/IEC104_ClockSynchronization_设计架构.md)。 +### Type 45/46遥控 + +`Example_Init_Command`使用默认配置:两种类型均须先选择,决定期限15秒、选择保持30秒、执行期限60秒;成功发送ACT_TERM,失败默认不发。整个实例只允许一个事务,所有站点的新遥控互斥;TEST请求否定,CA=65535回UNKNOWN_CA,其他遥控IOA由应用Reject(UNKNOWN_IOA)。 + +| 命令 | 受控点及返回信息 | 初值与总召 | +|---|---|---| +| Type45,IOA6001..6008 | Type1,IOA1001..1008,COT=11;状态0分/1合 | 初值见下表,执行后总召读取更新值 | +| Type46,IOA6101..6108 | Type3,IOA1101..1108,COT=11;状态1分/2合 | 初值全部为1;执行后站总召及遥信分组读取更新值 | + +表中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须与选择一致。主站收到选择的肯定ACT_CON后再执行;执行依次返回ACT_CON、位置返回信息、ACT_TERM。撤销使用COT=8,同类型/IOA/OA匹配,不要求状态相同。47..51及58..64仍由兜底回44。 + +`Example_Drain_Command`先读取SELECT/EXECUTE/FINISHED;首次保存事务context前Acquire应用站点引用。`Example_Command_Action`模拟返校或修改`ExampleApp.signal_values/double_values`并返回STARTED,不在动作中上报。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 | 类型 | 模拟值(按地址顺序) | +| 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位标度值,本示例没有定义物理单位和倍率。 -一次成功站总召依次提交6个ASDU: +一次成功站总召依次提交8个ASDU: 1. Type=100,COT=7,肯定激活确认。 2. 两帧Type=1、COT=20的单点数据,每帧4个对象。 -3. 两帧Type=11、COT=20的标度化遥测,每帧4个对象。 -4. Type=100,COT=10,激活终止。 +3. 两帧Type=3、COT=20的双点数据,每帧4个对象。 +4. 两帧Type=11、COT=20的标度化遥测,每帧4个对象。 +5. Type=100,COT=10,激活终止。 -分组总召各提交4个ASDU:ACT_CON、两批对应类型数据、ACT_TERM。分组1数据为Type=1/COT=21,分组2数据为Type=11/COT=22;确认和终止保留请求QOI。BEGIN校验分组,首个NEED_DATA按该组初始化应用游标,不把模拟点表写进库。 +分组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。未提交HIGH/NORMAL、当前帧或窗口背压时Try返回WAIT;校验错误仍走普通HIGH。总召数据不使用Enqueue_Event,只有本批接手后才推进游标及后续终止。 +全部应答使用原请求的连接身份。总召确认、终止及短应答走普通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。示例点数很少,总召可能在主站发撤销前已全部提交;观察中途撤销和各阶段拥塞可使用下面的可控网络测试。 @@ -105,7 +126,7 @@ FRZ=1冻结并将序号模32加一;FRZ=2另将当前值清零;FRZ=3仅清当 | 需要新增总召且4个任务槽已占满 | 尝试否定ACT_CON;无法安全提交/保存则丢新请求并计数,保留连接与原任务;匹配撤销复用原槽 | | 接受/取数/整体业务期限到点 | 肯定ACT_CON提交前回否定确认并保留连接,提交后关闭原连接;已取出BEGIN才交付对应TIMEOUT终结事件 | -Example_Handle_Event先过滤asdu_result失败的I帧视图,避免进入业务分派或未知类型兜底;正常线上基础头/载荷截短已由Receiver和连接层处理。当前I帧依次交给总召、计数量、校时及后续业务;都未接手时,在读取下一事件前同步调用`IEC104_Slave_Reject_Request`回UNKNOWN_TYPE。该接口借用当前事件生成原文错误应答,复用库的普通HIGH路径;示例不保存other_request,也没有other_pending、other_waiting或other_waits。未支持类型的连续及相同内容有效新请求逐条尝试应答,允许穿插正在进行的总召。103现在由校时模块接手,102仍回44;模块之间不承诺跨业务应答按接收顺序排列。 +Example_Handle_Event先过滤asdu_result失败的I帧视图,避免进入业务分派或未知类型兜底;正常线上基础头/载荷截短已由Receiver和连接层处理。当前I帧依次交给总召、计数量、校时及遥控;都未接手时,在读取下一事件前同步调用`IEC104_Slave_Reject_Request`回UNKNOWN_TYPE。该接口借用当前事件生成原文错误应答,复用库的普通HIGH路径;示例不保存other_request,也没有other_pending、other_waiting或other_waits。未支持类型的连续及相同内容有效新请求逐条尝试应答,允许穿插正在进行的总召。103由校时接手,45/46由遥控接手,102、47、58仍回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)。 @@ -113,7 +134,7 @@ OK表示已接手;WAIT只因HIGH容量不足,示例丢弃本条并增加over `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列表。示例常量表只用于展示业务接入,不是库规定的地址范围。逐批采样不保证整轮同一时刻,也不自动解决与突发上报的新旧排序;完整边界见[总召模块设计](../104/IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。 +本例BEGIN只判断接受,首个NEED_DATA初始化游标并读取这一批;接真实数据源时,在Example_Build_Batch的逐点组包处读取当前值和品质。IOA、类型、分组成员及遍历策略均由调用者决定,协议库不登记或冻结完整IOA列表。示例单点常量表保存地址、初值及品质,双点初值均为1;单点和双点实际值分别存放在ExampleApp.signal_values和ExampleApp.double_values中,站总召及遥信分组逐批读取这些可写数组的当前值,包括遥控执行后的更新;这些地址不是库规定的范围。逐批采样不保证整轮同一时刻,也不自动解决与突发上报的新旧排序;完整边界见[总召模块设计](../104/IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。 Example_Build_Batch使用局部游标遍历,仅在Submit_Batch返回OK后推进应用游标,此时发送层已复制原文并确定帧顺序,输入存储可复用。返回WAIT时保留同一业务事件及游标,先推进网络、读空Slave通知,再从同一位置重新取当前值和品质,不能重发旧构造结果。STALE表示原交互已失效(包括取消、撤销、超时及提交故障),丢弃旧应答并处理FINISHED。接实际数据源时,须将点值更新及突发发布与总召读值及提交串行化;排序保证及持续突发可能推迟总召的边界见[数据批次与发送路径](../104/IEC104_Interrogation_设计架构.md#6-数据批次与发送路径)。 @@ -125,18 +146,21 @@ Example_Init使用默认业务期限:BEGIN应答15秒、每批NEED_DATA至首 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。脚本依次运行32组Slave/站点测试(单服务及双服务配置)、130组总召/计数量/兜底/校时可控网络测试(单服务及双服务)、3组规则单测和真实本机TCP测试: +`-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槽,使用真实容量背压限制发送。 -- `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;连接保留。服务约8秒后自行关闭,使用默认模拟时钟。 +- `iec104_command_test.c`:独立可控Net/单调时钟测试,TCP服务容量1/2和独立应答槽容量1/4交叉构建,HIGH=251字节;覆盖选择执行、撤销、动作结果、期限、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选择执行、返回信息在确认和终止之间、撤销、广播46、未知IOA47、TEST否定、47/58回44,以及单点遥控后总召读取新值。服务约12秒后自行关闭,使用默认模拟时钟。 原114组基线由98组总召、12组计数量及4组兜底回归组成。兜底改用未支持的106/102,覆盖同批及反序、相同内容的有效新请求、k=1与写阻塞排队、HIGH满后丢新且不补发、STOP/STOPSTART取消,以及总召未结束时立即提交错误应答。启用校时的独立回归另外核对103业务。 diff --git a/IEC60870-Slave/docs/test/IEC104测试文档.md b/IEC60870-Slave/docs/test/IEC104测试文档.md index e5a2fb46..25bcec6e 100644 --- a/IEC60870-Slave/docs/test/IEC104测试文档.md +++ b/IEC60870-Slave/docs/test/IEC104测试文档.md @@ -25,7 +25,13 @@ ## 2. 编译与运行 -当前业务测试由98组总召、12组计数量、4组兜底及16组校时组成,共130组,单/双服务配置各跑一次。计数量覆盖独立实例、请求校验、Type15/37批次边界、忙隔离、冻结/复位、动作重复调用和WAIT/STALE/TEST、业务期限、STOP/断线、共享连接失败、发送背压及多站广播;兜底改用未支持的106/102,覆盖同批与反序/重复内容、窗口与写阻塞、HIGH满丢新及恢复、STOP/STOPSTART取消和活动总召中的应答。校时专项检查独立受理与执行、原文确认、补偿/期限、广播及生命周期。`iec104_interrogation_rules_test.c`另有3组规则测试,遍历限定词、数据COT和类型/SQ/FRZ矩阵并检查解析;没有网络交互。 +遥控专项入口为`test/iec104_command_test.c`,复用TCP事件环测试的可控Net后端,但独立驱动Slave和Command,不依赖示例的应用状态。`./scripts/build.ps1 -Test`同时构建和执行TCP容量1/2 × 遥控独立应答槽1/4的四种组合,每种27组。库源文件须包含`104/business/IEC104_Command.c`,测试本身包含可控TCP实现,不再额外链接Windows Net或第二份TcpServer。 + +遥控覆盖45/46选择和直接执行、匹配/忙拒绝/撤销、三类动作结果、成功失败及终止配置、零/多帧返回信息、完整ASDU校验、全局FIFO、回复池满、窗口/部分写入、结果先于确认、重复及迟到事件、FINISHED配对、三个期限和时钟倒退(含期限配置0)、STOP提交竞态/FATAL、NORMAL覆盖反例、站点删除重注册、引用和单/双服务生命周期。真实回环另验证45/46返回信息及终止、TEST、广播/未知IOA、47/58兜底和遥控后总召读到新值。 + +决定期间同点激活的4组专项回归覆盖45/46、SELECT_WAIT与直接/选择后EXECUTE_WAIT、事件已取/未取、同批输入及发送背压。检查相同或不同命令字段均只增加dropped_requests,原事件、动作参数、回显和决定期限不变,不产生抢先的忙否定;另验证到期FINISHED配对、格式校验/type_mask/撤销/其他点忙拒绝,以及同CA重注册、STOP和重连的身份边界。 + +当前业务测试由99组总召、12组计数量、4组兜底及16组校时组成,共131组,单/双服务配置各跑一次。总召在原98组基础上新增双点批次发送WAIT后读取最新值的回归,验证站总召和遥信分组的单点/双点帧序、游标只在提交OK后推进;真实TCP另核对双点遥控后总召读到更新值。计数量覆盖独立实例、请求校验、Type15/37批次边界、忙隔离、冻结/复位、动作重复调用和WAIT/STALE/TEST、业务期限、STOP/断线、共享连接失败、发送背压及多站广播;兜底改用未支持的106/102,覆盖同批与反序/重复内容、窗口与写阻塞、HIGH满丢新及恢复、STOP/STOPSTART取消和活动总召中的应答。校时专项检查独立受理与执行、原文确认、补偿/期限、广播及生命周期。`iec104_interrogation_rules_test.c`另有3组规则测试,遍历限定词、数据COT和类型/SQ/FRZ矩阵并检查解析;没有网络交互。 在项目根目录的PowerShell中执行,要求GCC可用。下述TCP事件环和调度测试已包含TcpServer实现且自行实现Net接口,构建时不要再次链接`IEC104_TcpServer.c`或`IEC104_Net_Windows.c`。 @@ -211,8 +217,35 @@ if ($LASTEXITCODE -ne 0) { throw 'Large configuration tests failed' } `-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 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的单函数测量,不是最坏调用链或其他目标工具链的内存预算。 + ## 5. 验证记录 +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目标构建或硬件联调。 diff --git a/IEC60870-Slave/examples/iec104_slave_example.c b/IEC60870-Slave/examples/iec104_slave_example.c index eb6b42bd..894d8f23 100644 --- a/IEC60870-Slave/examples/iec104_slave_example.c +++ b/IEC60870-Slave/examples/iec104_slave_example.c @@ -1,19 +1,22 @@ /* - * 104子站示例:总召、计数量召唤、时钟同步、模拟点表、其他命令兜底及main。 + * 104子站示例:总召、计数量召唤、时钟同步、45/46遥控、模拟点表及main。 * 两种召唤共用IEC104_Interrogation.c状态机,各自有独立实例和业务事件。 - * 每轮Slave_Poll -> NextEvent逐条交付 -> Interrogation_Process;所有调用串行。 - * 总召每批4点,模块自动推进至需要等待;8个遥信和8个遥测组成6个响应ASDU。 - * QOI=21只读遥信,22只读遥测,其余分组由应用拒绝;分组点表策略不写进协议库。 + * 每轮Slave_Poll -> NextEvent逐条交付 -> Command_Process及应用交互 -> 召唤/校时;所有调用串行。 + * 总召每批4点,模块自动推进至需要等待;8个单点遥信、8个双点遥信和8个遥测组成8个响应ASDU。 + * QOI=21读取单点及双点遥信,22只读遥测,其余分组由应用拒绝;分组点表策略不写进协议库。 * 同一连接至多一个未终结总召,忙时不区分QOI均否定;任务满时拒绝/计数丢弃新请求,保留当前连接。 * 总召模块处理WAIT、STOPDT及断线;应用读取业务事件,选择类型并显式交付ASDU。 * 计数量使用8个模拟计数器;读取快照,冻结/复位由经过事件校验的同步动作入口执行。 * 对时默认只更新模拟UTC基准;--clock system显式选择实际时钟,协议仍使用单调时钟。 + * 遥控默认先选择后执行,同一实例只占一个事务;设备结果、返回信息和Complete分步推进,WAIT保留当前步骤。 + * 模拟输出同步更新点值;FINISHED只清理返校/输出标志,不恢复已执行的点值,也不代表真实设备已经空闲。 * 不支持的其他命令当场提交COT=44否定应答,复用发送层HIGH;容量不足只丢本条并计数。 * 默认127.0.0.1:2404、CA=1;构建命令见同目录README.md。 */ #include "IEC104_Interrogation.h" #include "IEC104_CounterInterrogation.h" #include "IEC104_ClockSynchronization.h" +#include "IEC104_Command.h" #include "IEC60870_Clock.h" #include #include @@ -24,7 +27,7 @@ /* 模拟遥信点表:ioa为24位信息对象地址,value为0/1,quality为SIQ品质位(这里均为0)。 * 表项不是线上报文,后续由SinglePointInformation_Create及ASDU容器编码成Type=1。 - * 示例点表只读;接真实数据源时,在每次NEED_DATA组包处读取所选点的当前值和品质。 */ + * 此表提供地址、初值和品质;运行值保存在各ExampleApp中,遥控与总召共用同一数据源。 */ static const struct { uint32_t ioa; uint8_t value, quality; } example_signals[] = { {1001, 0, 0}, {1002, 1, 0}, {1003, 0, 0}, {1004, 1, 0}, {1005, 1, 0}, {1006, 0, 0}, {1007, 1, 0}, {1008, 0, 0} @@ -41,7 +44,7 @@ static const struct { uint32_t ioa; int16_t value; uint8_t quality; } example_me /* 当前取数站的应用游标;首个NEED_DATA才初始化,协议库不登记或冻结业务IOA列表。 */ -typedef struct { size_t single_index, scaled_index; } ExampleGiData; +typedef struct { size_t single_index, double_index, scaled_index; } ExampleGiData; typedef struct { IEC104_Slave_t *slave; /* main拥有;本上下文仅借用。 */ @@ -52,6 +55,20 @@ typedef struct { IEC104_InterrogationStatus_t ci; /* 计数量调度及统计快照,每次推进后重新查询。 */ IEC104_ClockSynchronization_t *clock; /* 独立时钟同步模块,共用Slave及站点表。 */ IEC104_ClockSynchronizationStatus_t cs; /* 对时执行、确认及期限的只读快照。 */ + IEC104_Command_t *command; /* CLI启用;仅总召的旧测试可以不创建遥控实例。 */ + IEC104_CommandStatus_t command_status; /* 推进后/等待前刷新;计时、通知和应答状态只供调度及统计。 */ + IEC104_CommandEvent_t command_event; /* 工作事件可跨WAIT保留,执行收尾继续使用原EXECUTE身份。 */ + IEC104_Station_t *command_station; /* 应用额外引用:首个工作事件先Acquire,再保存context;FINISHED后Release。 */ + uint64_t command_instance, command_transaction; /* 保留事务归属;SELECT/EXECUTE共用,FINISHED须先核对再清理。 */ + /* pending仅表示工作事件尚待Accept/Reject;waiting表示上次接口WAIT,避免把待重试状态当作可立即推进。 + * selected/output是模拟返校及输出的副作用标志,不代替协议阶段;真实设备忙状态应独立维护。 */ + bool command_pending, command_waiting, command_selected, command_output; + unsigned command_step; /* 0等待事件/终结,1报告设备结果,2返回信息,3关闭返回阶段。 */ + IEC60870_Asdu_t command_return; /* 借用下面的固定缓冲;WAIT未接手,必须连同事件保留到OK或STALE。 */ + uint8_t command_return_bytes[10]; /* 单个Type1/3对象,WAIT保持原文,不借用总召缓冲。 */ + /* 单点及双点运行值由遥控更新,总召与遥信分组均读取同一数组的当前值。 + * 单点命令6001..6008对应遥信1001..1008,双点命令6101..6108对应遥信1101..1108;FINISHED不重置运行值。 */ + uint8_t signal_values[EXAMPLE_SIGNAL_COUNT], double_values[EXAMPLE_SIGNAL_COUNT]; bool clock_system, clock_valid; /* 实际设时为显式选项;clock_valid只描述模拟基准。 */ uint64_t clock_utc_ms, clock_base_ms; /* 模拟UTC毫秒及其单调基准,不影响协议计时。 */ IEC104_InterrogationEvent_t counter_event; /* WAIT时保留的原业务事件,重试不能换成新身份。 */ @@ -105,7 +122,7 @@ static int Example_Build_Batch(ExampleApp *app, const IEC104_InterrogationReques IEC60870_SinglePointInformation_t point; IEC60870_AsduResult_t added; if (IEC60870_SinglePointInformation_Create(&point, example_signals[index].ioa, - example_signals[index].value, example_signals[index].quality) != IEC60870_ASDU_OK) { + app->signal_values[index], example_signals[index].quality) != IEC60870_ASDU_OK) { return -1; } added = IEC60870_Asdu_Add_InformationObject(asdu, &point.object); @@ -113,6 +130,18 @@ static int Example_Build_Batch(ExampleApp *app, const IEC104_InterrogationReques if (added != IEC60870_ASDU_OK) { return -1; } ++index; } + } else if (data->double_index < EXAMPLE_SIGNAL_COUNT) { + size_t index = data->double_index; + while (index < EXAMPLE_SIGNAL_COUNT && asdu->object_count < request->max_objects_per_asdu) { + IEC60870_DoublePointInformation_t point; + IEC60870_AsduResult_t added; + if (IEC60870_DoublePointInformation_Create(&point, (uint32_t)(1101U + index), + app->double_values[index], 0U) != IEC60870_ASDU_OK) { return -1; } + added = IEC60870_Asdu_Add_InformationObject(asdu, &point.object); + if (added == IEC60870_ASDU_FULL) { break; } + if (added != IEC60870_ASDU_OK) { return -1; } + ++index; + } } else if (data->scaled_index < EXAMPLE_MEASUREMENT_COUNT) { size_t index = data->scaled_index; while (index < EXAMPLE_MEASUREMENT_COUNT && asdu->object_count < request->max_objects_per_asdu) { @@ -161,7 +190,7 @@ static bool Example_Drain_Business(ExampleApp *app) switch (event->type) { case IEC104_INTERROGATION_BEGIN: { bool accept = event->request.qualifier <= 22U; - /* 示例仅定义组1=遥信、组2=遥测;拒绝未配置的组。协议库支持全部16组。 + /* 示例仅定义组1=单点及双点遥信、组2=遥测;拒绝未配置的组。协议库支持全部16组。 * 广播会先交付所有站的BEGIN;这里只作准入判断,不占用唯一取数游标。 */ reply = IEC104_Interrogation_Reply_Begin(app->interrogation, event, accept); break; @@ -178,7 +207,10 @@ static bool Example_Drain_Business(ExampleApp *app) } memset(&app->gi_data, 0, sizeof(app->gi_data)); if (event->request.qualifier == 21U) { app->gi_data.scaled_index = EXAMPLE_MEASUREMENT_COUNT; } - if (event->request.qualifier == 22U) { app->gi_data.single_index = EXAMPLE_SIGNAL_COUNT; } + if (event->request.qualifier == 22U) { + app->gi_data.single_index = EXAMPLE_SIGNAL_COUNT; + app->gi_data.double_index = EXAMPLE_SIGNAL_COUNT; + } app->session_instance = event->instance_id; app->session_request = event->request_id; } @@ -189,6 +221,8 @@ static bool Example_Drain_Business(ExampleApp *app) if (reply == IEC104_INTERROGATION_REPLY_OK && app->batch_result > 0) { if (app->gi_data.single_index < EXAMPLE_SIGNAL_COUNT) { app->gi_data.single_index += app->batch.object_count; + } else if (app->gi_data.double_index < EXAMPLE_SIGNAL_COUNT) { + app->gi_data.double_index += app->batch.object_count; } else { app->gi_data.scaled_index += app->batch.object_count; } } else if (reply == IEC104_INTERROGATION_REPLY_OK && app->batch_result == 0) { /* 其他站的数据随后才开始;无需等到所有站的ACT_TERM才复用游标。 */ @@ -348,8 +382,15 @@ static bool Example_Init(ExampleApp *app, IEC104_Slave_t *slave, uint16_t common IEC104_InterrogationStationState_t *broadcast_states, size_t broadcast_capacity) { IEC104_InterrogationConfig_t config; + size_t index; memset(app, 0, sizeof(*app)); app->slave = slave; + /* 每个应用实例独立保存受控运行值:单点沿用原总召初值,双点1表示分位。 + * 这里只在应用初始化时赋值,选择结束、撤销和FINISHED清理均不得恢复这些初值。 */ + for (index = 0U; index < EXAMPLE_SIGNAL_COUNT; ++index) { + app->signal_values[index] = example_signals[index].value; + app->double_values[index] = 1U; + } if (IEC104_Slave_Add_Station(slave, &app->station, common_address, app) != IEC104_STATION_OK) { return false; } IEC104_Interrogation_Config_Default(&config); config.use_station_registry = true; /* CA只在注册时指定;多站广播需提供每站状态区。 */ @@ -424,6 +465,199 @@ static bool Example_Drain_Clock(ExampleApp *app) return true; } +/** + * 功能:把示例支持的遥控命令地址映射为对应运行值数组的下标。 + * 输入参数:request——非NULL的已解码45/46工作请求,仅借用;本函数不检查会话或选择匹配。 + * 输出参数:index——非NULL,仅成功时写入0..EXAMPLE_SIGNAL_COUNT-1,失败保持原值。 + * 返回值:true为Type45的6001..6008或Type46的6101..6108;false为示例点表之外的类型/地址。 + * 说明:库负责协议格式和事务身份,应用负责IOA是否存在;范围外由调用者Reject(UNKNOWN_IOA)。 + * 单点命令6001..6008映射到遥信1001..1008;双点命令6101..6108映射到位置1101..1108。 + */ +static bool Example_Command_Point(const IEC104_CommandRequest_t *request, size_t *index) +{ + uint32_t first = request->type_id == 45U ? 6001U : 6101U; + if ((request->type_id != 45U && request->type_id != 46U) || request->ioa < first || + request->ioa - first >= EXAMPLE_SIGNAL_COUNT) { return false; } + *index = (size_t)(request->ioa - first); + return true; +} + +/** + * 功能:执行已通过Accept校验并消费的选择/执行动作,仅修改模拟设备状态。 + * 输入参数:context——当前ExampleApp;request——库仅在本次回调期间借出的工作请求,不得保存指针。 + * 输出参数:选择置返校标志;执行置输出标志并更新对应单点/双点运行值,供后续返回信息及总召读取。 + * 返回值:点表不匹配返回NOT_STARTED且没有副作用;模拟动作完成返回STARTED。 + * 说明:STARTED表示选择生效或输出启动,不代替Report_Result。真实异步驱动应在设备完成后报告结果。 + * 不在动作中发布任何上报、推进Slave或重入修改Command;库不保存本函数或context。 + */ +static IEC104_CommandActionResult_t Example_Command_Action(void *context, + const IEC104_CommandRequest_t *request) +{ + ExampleApp *app = (ExampleApp *)context; + size_t index; + if (!Example_Command_Point(request, &index)) { return IEC104_COMMAND_ACTION_NOT_STARTED; } + if (request->is_select) { app->command_selected = true; } + else { + app->command_output = true; + if (request->type_id == 45U) { app->signal_values[index] = request->state; } + else { app->double_values[index] = request->state; } + } + return IEC104_COMMAND_ACTION_STARTED; +} + +/** + * 功能:为已启动的当前EXECUTE构造一帧COT=11的模拟位置返回信息,此处只组包,不提交。 + * 输入参数:app——非NULL,command_event保留当前EXECUTE身份,运行值已由动作更新。 + * 输出参数:app->command_return及其固定10字节缓冲,CA/OA沿用工作请求,IOA映射到对应遥信。 + * 返回值:true为Type1或Type3单对象ASDU构造成功;false为点表或编解码失败,此时不可提交该ASDU。 + * 说明:命令模块已拒绝TEST请求,因此清零公共头得到TEST=0;对象品质为示例约定的0。 + * 局部信息对象由ASDU编码复制,离开函数不借用局部变量;ASDU仍借用app内固定字节缓冲。 + * 成功后冻结这一帧,Submit_Return_Info的WAIT不重新组包;只有OK才可推进,避免同一变位重复上报。 + */ +static bool Example_Build_Command_Return(ExampleApp *app) +{ + const IEC104_CommandRequest_t *request = &app->command_event.request; + IEC60870_AsduHeader_t header; + size_t index; + if (!Example_Command_Point(request, &index)) { return false; } + memset(&header, 0, sizeof(header)); + header.cause_of_transmission = IEC60870_ASDU_COT_RETURN_INFO_REMOTE; + header.common_address = request->common_address; + header.originator_address = request->originator_address; + if (IEC60870_Asdu_Create(&app->command_return, + IEC104_Slave_Get_Application_Layer_Parameters(app->slave), app->command_return_bytes, + sizeof(app->command_return_bytes), &header, 0) != IEC60870_ASDU_OK) { return false; } + if (request->type_id == 45U) { + IEC60870_SinglePointInformation_t point; + return IEC60870_SinglePointInformation_Create(&point, (uint32_t)(1001U + index), + app->signal_values[index], 0U) == IEC60870_ASDU_OK && + IEC60870_Asdu_Add_InformationObject(&app->command_return, &point.object) == IEC60870_ASDU_OK; + } else { + IEC60870_DoublePointInformation_t point; + return IEC60870_DoublePointInformation_Create(&point, (uint32_t)(1101U + index), + app->double_values[index], 0U) == IEC60870_ASDU_OK && + IEC60870_Asdu_Add_InformationObject(&app->command_return, &point.object) == IEC60870_ASDU_OK; + } +} + +/** + * 功能:消费遥控业务事件、处理应用决定,并推进结果报告/返回信息/Complete的持久化步骤。 + * 输入参数:app——非NULL,所属Slave通知须先按FIFO分派;command可为NULL以兼容仅总召测试。 + * 输出参数:更新应用持有事件、站点引用、模拟标志和重试步骤;不替代Command_Process提交协议应答。 + * 返回值:true为本轮处理完成或遇到正常WAIT/STALE;false为事件身份、引用或接口/组包错误。 + * 说明:先读取业务事件再重试旧交互,使STOP、断线、撤销产生的FINISHED能先清理过期步骤。 + * 一个事务可能交付SELECT后再交付EXECUTE,但只持有一个应用站点引用,最后恰好按FINISHED清理。 + * FINISHED不回滚设备值;本例动作同步完成,可直接清除模拟标志并Release。真实输出未结束时, + * 应用须另存设备操作及站点引用,保持设备忙状态,不能照搬本例的立即释放。 + */ +static bool Example_Drain_Command(ExampleApp *app) +{ + unsigned count; + if (app->command == NULL) { return true; } + for (count = 0U; count < EXAMPLE_EVENT_BUDGET; ++count) { + IEC104_CommandEvent_t event; + IEC104_CommandNextResult_t next = IEC104_Command_NextEvent(app->command, &event); + if (next == IEC104_COMMAND_NEXT_EMPTY) { break; } + if (next != IEC104_COMMAND_NEXT_READY) { return false; } + if (event.type == IEC104_COMMAND_FINISHED) { + /* 只清理当前应用持有的事务,不能让旧实例/旧事务的终结通知释放另一次输出资源。 + * 模块槽可能仍因最后应答WAIT而占用;应用读到FINISHED也不能据此强行发起新动作。 */ + if (event.instance_id != app->command_instance || + event.transaction_id != app->command_transaction) { return false; } + if (event.select_effect) { app->command_selected = false; } + if (event.execute_effect) { app->command_output = false; } + if (app->command_station != NULL && + IEC104_Slave_Release_Station(app->slave, app->command_station) != IEC104_STATION_OK) { return false; } + app->command_station = NULL; + app->command_instance = app->command_transaction = 0U; + app->command_pending = app->command_waiting = false; + app->command_step = 0U; + } else { + if (app->command_transaction == 0U) { + /* 先按注册编号找到原站并Acquire,再保存引用及事务身份;同CA重新注册不能替代原站。 + * 模块自身的引用可能在FINISHED出队时释放,不能借它维持应用context的生命周期。 */ + IEC104_Station_t *station = IEC104_Slave_Get_Station_By_Id(app->slave, event.request.station_id); + if (station == NULL || IEC104_Slave_Acquire_Station(app->slave, station) != IEC104_STATION_OK) { + return false; + } + app->command_station = station; + app->command_instance = event.instance_id; + app->command_transaction = event.transaction_id; + } else if (app->command_instance != event.instance_id || + app->command_transaction != event.transaction_id || app->command_pending) { return false; } + /* 按值保存,不持有NextEvent局部变量的地址;匹配执行覆盖工作事件,继续保留同一站点引用。 */ + app->command_event = event; + app->command_pending = true; + app->command_waiting = false; + } + } + if (app->command_pending) { + IEC104_CommandReplyResult_t reply; + size_t index; + /* context和点表属于应用策略;格式已合法但不属于本应用的点仍明确回UNKNOWN_IOA。 */ + if (IEC104_Station_Get_Context(app->command_station) != app || + !Example_Command_Point(&app->command_event.request, &index)) { + reply = IEC104_Command_Reject(app->command, &app->command_event, IEC104_COMMAND_REJECT_UNKNOWN_IOA); + } else { reply = IEC104_Command_Accept(app->command, &app->command_event, Example_Command_Action, app); } + app->command_waiting = reply == IEC104_COMMAND_REPLY_WAIT; + /* WAIT未消费工作事件、未调用动作;保留身份,下轮先处理Slave通知再重试。 + * STALE不再重交动作,只保留事务归属等待FINISHED;OK也仅表示事件被消费。 */ + if (app->command_waiting) { return true; } + if (reply != IEC104_COMMAND_REPLY_OK && reply != IEC104_COMMAND_REPLY_STALE) { return false; } + app->command_pending = false; + if (reply == IEC104_COMMAND_REPLY_OK && app->command_event.type == IEC104_COMMAND_EXECUTE) { + IEC104_CommandTransactionInfo_t info; + /* Reject和动作NOT_STARTED/FAILED也可能返回OK,必须确认仍为EXECUTING才能报告设备结果。 + * 返回信息此时可先构造,但Submit_Return_Info会等待肯定执行确认已经交接。 */ + if (IEC104_Command_Get_Transaction(app->command, &app->command_event, &info) && info.active && + info.phase == IEC104_COMMAND_PHASE_EXECUTING) { + if (!Example_Build_Command_Return(app)) { return false; } + app->command_step = 1U; + } + } + } + /* 模拟动作已经完成,先报告结果停止执行计时;确认是否已交接不影响本次报告。 + * 每轮至多推进三个步骤。每步仅OK后递增;返回信息OK已经固定帧序,不能因Complete等待而重发。 */ + for (count = 0U; count < 3U && app->command_step != 0U; ++count) { + IEC104_CommandReplyResult_t reply; + if (app->command_step == 1U) { + reply = IEC104_Command_Report_Result(app->command, &app->command_event, IEC104_COMMAND_RESULT_SUCCEEDED); + } else if (app->command_step == 2U) { + reply = IEC104_Command_Submit_Return_Info(app->command, &app->command_event, &app->command_return); + } else { reply = IEC104_Command_Complete(app->command, &app->command_event); } + app->command_waiting = reply == IEC104_COMMAND_REPLY_WAIT; + /* WAIT保持当前步骤及完整ASDU;STALE停止后续提交,引用与模拟标志交给FINISHED统一清理。 + * Complete的OK只关闭返回信息阶段,ACT_TERM交接及FINISHED仍由后续Process/NextEvent推进。 */ + if (app->command_waiting) { return true; } + if (reply == IEC104_COMMAND_REPLY_STALE) { app->command_step = 0U; return true; } + if (reply != IEC104_COMMAND_REPLY_OK) { return false; } + app->command_step = app->command_step == 3U ? 0U : app->command_step + 1U; + } + return true; +} + +#ifndef IEC104_SLAVE_EXAMPLE_NO_MAIN +/** + * 功能:为命令行示例创建默认遥控实例,与仅总召的初始化入口分开。 + * 输入参数:app——非NULL,Slave句柄和站点已创建,尚未开始消费Slave通知。 + * 输出参数:成功后保存app->command;失败不创建可用实例。 + * 返回值:true为创建成功;false为重复初始化、配置/归属错误或静态池没有空位。 + * 说明:默认45/46均须先选择,决定15秒、选择30秒、执行60秒;本函数不执行设备动作。 + * 每Slave只能绑定一个Command。main在Slave_Init前创建亦可,但必须早于首条生命周期通知。 + */ +static bool Example_Init_Command(ExampleApp *app) +{ + IEC104_CommandConfig_t config; + if (app->command != NULL) { return false; } + IEC104_Command_Config_Default(&config); + app->command = IEC104_Command_Create(app->slave, &config); + return app->command != NULL; +} +#endif + +/* 统一取消应用业务;app为有效应用上下文,未创建的可选模块直接跳过,无返回值。 + * 遥控Cancel丢弃模块持有应答但保留应交付FINISHED,不回滚已经执行的输出。 + * 本函数仅刷新状态,调用方还须Drain_Command读取终结通知、归还应用引用,再关闭Slave或Destroy。 */ static void Example_Cancel(ExampleApp *app) { IEC104_Interrogation_Cancel(app->interrogation); @@ -435,13 +669,30 @@ static void Example_Cancel(ExampleApp *app) IEC104_ClockSynchronization_Cancel(app->clock); (void)IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs); } + if (app->command != NULL) { + IEC104_Command_Cancel(app->command); + (void)IEC104_Command_Get_Status(app->command, &app->command_status); + } (void)IEC104_Interrogation_Get_Status(app->interrogation, &app->gi); } /* 每次等待前重新查询剩余期限,避免采样/业务处理耗时后继续使用旧的毫秒数。 + * 输入app为有效上下文,maximum_ms为调用方等待上限;返回0表示本地立即续跑,否则为缩短后的上限。 + * 查询失败也返回0,让随后推进路径处理错误;本函数只查状态,不消费事件或触发业务到期转换。 * 本地可推进时不休眠;否则本例网络等待最多50ms,并受最近业务期限限制。 */ static uint32_t Example_Wait_Time(ExampleApp *app, uint32_t maximum_ms) { + if (app->command != NULL) { + if (!IEC104_Command_Get_Status(app->command, &app->command_status)) { return 0U; } + /* 未读Slave通知、FINISHED等业务事件及模块本地进度都须立即处理。 + * 应用持有事件/收尾步骤不代表能立即发送:上次WAIT时允许Poll等待,避免确认拥塞造成空转。 */ + if (app->command_status.events_pending || app->command_status.pending_events != 0U || + app->command_status.need_process || + ((app->command_pending || app->command_step != 0U) && !app->command_waiting)) { return 0U; } + if (app->command_status.timer_active && app->command_status.timeout_remaining_ms < maximum_ms) { + maximum_ms = app->command_status.timeout_remaining_ms; + } + } if (!IEC104_Interrogation_Get_Status(app->interrogation, &app->gi)) { return 0U; } if (app->counter != NULL) { if (!IEC104_Interrogation_Get_Status(app->counter, &app->ci)) { return 0U; } @@ -463,7 +714,11 @@ static uint32_t Example_Wait_Time(ExampleApp *app, uint32_t maximum_ms) return maximum_ms; } -/* 状态事件交给所有实例;I帧依次由总召、计数量、时钟同步及未知类型兜底接手。 */ +/* 状态事件交给所有实例;I帧依次由总召、计数量、时钟同步、遥控及兜底接手。 + * 输入app为有效上下文,event是刚由Slave_NextEvent交付的借用事件,本函数不保存它的指针。 + * 返回true表示分派完成(包括模块拒绝/丢弃),false表示业务入口或状态查询失败,应统一收尾。 + * 生命周期通知不能因前一个模块HANDLED而短路,否则遥控可能漏读STOP/换代后继续执行旧请求。 + * Handle_Event负责消费这些通知,不能在它之前用Check_Request等待通知读空而阻断分派。 */ static bool Example_Handle_Event(ExampleApp *app, const IEC104_SlaveEvent_t *event) { IEC104_InterrogationEventResult_t result; @@ -485,7 +740,17 @@ static bool Example_Handle_Event(ExampleApp *app, const IEC104_SlaveEvent_t *eve IEC104_ClockSynchronization_Handle_Event(app->clock, event); if (clock_result == IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR || !IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs)) { return false; } - if (clock_result != IEC104_CLOCK_SYNCHRONIZATION_EVENT_IGNORED) { return true; } + if (clock_result != IEC104_CLOCK_SYNCHRONIZATION_EVENT_IGNORED && + event->transport.type == IEC104_TCP_EVENT_I_FRAME) { return true; } + } + if (app->command != NULL && (event->transport.type != IEC104_TCP_EVENT_I_FRAME || + result == IEC104_INTERROGATION_EVENT_IGNORED)) { + /* HANDLED也包括格式否定、实例忙和过载丢弃,不能再走44兜底;未启用类型仍可继续分派。 */ + IEC104_CommandEventResult_t command_result = IEC104_Command_Handle_Event(app->command, event); + if (command_result == IEC104_COMMAND_EVENT_ERROR || + !IEC104_Command_Get_Status(app->command, &app->command_status)) { return false; } + if (command_result != IEC104_COMMAND_EVENT_IGNORED && + event->transport.type == IEC104_TCP_EVENT_I_FRAME) { return true; } } if (result != IEC104_INTERROGATION_EVENT_IGNORED || event->transport.type != IEC104_TCP_EVENT_I_FRAME || !app->gi.active || @@ -525,10 +790,15 @@ static bool Example_Step_Clock(ExampleApp *app) Example_Drain_Clock(app) && IEC104_ClockSynchronization_Get_Status(app->clock, &app->cs)); } -/* 两类召唤轮流先执行,再推进对时;发送WAIT后下一轮继续,不只靠need_process重试。 */ +/* 先推进遥控,随后两类召唤轮换、对时;发送WAIT后下一轮继续,不只靠need_process重试。 + * app为有效上下文;返回true表示本轮成功或因未读通知暂缓,false交给主循环停止并清理。 + * Command_Process只推进协议应答,Drain_Command才执行应用决定及结果/返回信息步骤,最后刷新调度快照。 + * 通知预算尚未读空时不能调用Accept;否则可能在尚未分派的STOP或撤销之后错误启动设备。 */ static bool Example_Step(ExampleApp *app) { if (app->events_pending) { return true; } + if (app->command != NULL && (!IEC104_Command_Process(app->command) || !Example_Drain_Command(app) || + !IEC104_Command_Get_Status(app->command, &app->command_status))) { return false; } app->counter_first = !app->counter_first; if (app->counter_first) { if (!Example_Step_Counter(app) || !Example_Step_General(app)) { return false; } @@ -538,7 +808,8 @@ static bool Example_Step(ExampleApp *app) return Example_Step_Clock(app); } -/* 有界消费通知。预算用完先续读,不能跨过排在后面的STOP/START直接续发旧总召。 */ +/* 有界消费通知。预算用完先续读,不能跨过排在后面的STOP/START直接续发旧总召或执行旧遥控。 + * 只有读到EMPTY才清events_pending并允许Example_Step推进动作,避免用最终激活状态掩盖中间STOP。 */ static bool Example_Drain(ExampleApp *app) { unsigned count; @@ -613,13 +884,16 @@ static void Example_Usage(const char *program) printf("Usage: %s [--bind IPv4] [--port 0..65535] [--ca 1..65534]\n" " [--window 1..32767] [--run-ms milliseconds] [--clock simulated|system]\n" "Defaults: 127.0.0.1:2404, CA=1, k=12, run until Ctrl+C.\n" - "GI: QOI=20 (all), 21 (signals), 22 (scaled); IOA=0.\n" - "Signals 1001..1008, scaled values 2001..2008; COT=8 cancels matching GI.\n" + "GI: QOI=20 (all), 21 (single/double signals), 22 (scaled); IOA=0.\n" + "Single signals 1001..1008, double signals 1101..1108, scaled values 2001..2008.\n" + "COT=8 cancels matching GI.\n" "Counters 3001..3008: RQT=5 (all), 1/2 (groups); Type 15, four per batch.\n" "FRZ=0 reads frozen values; 1 freezes; 2 freezes/resets; 3 resets.\n" "Freeze/reset completes with ACT_CON only; TEST actions and COT=8 are rejected.\n" "Clock: simulated by default; system sets the actual clock and requires permission.\n" - "Clock requests use UTC+8 standard time; TEST is rejected without setting time.\n", program); + "Clock requests use UTC+8 standard time; TEST is rejected without setting time.\n" + "Commands: Type45 IOA6001..6008 -> signals1001..1008; Type46 IOA6101..6108 -> double1101..1108.\n" + "Commands require select then execute; COT8 cancels selection; TEST is rejected.\n", program); } /** @@ -695,6 +969,15 @@ int main(int argc, char **argv) (void)IEC104_Slave_Destroy(slave, NULL); return EXIT_FAILURE; } + /* 在网络监听及首条Slave通知之前绑定遥控实例;失败时尚无事务,按创建的逆序归还业务对象。 */ + if (!Example_Init_Command(&app)) { + (void)IEC104_ClockSynchronization_Destroy(app.clock); + (void)IEC104_Interrogation_Destroy(app.counter); + (void)IEC104_Interrogation_Destroy(app.interrogation); + (void)IEC104_Slave_Remove_Station(slave, &app.station); + (void)IEC104_Slave_Destroy(slave, NULL); + return EXIT_FAILURE; + } app.clock_system = clock_system; result = IEC104_Slave_Init(slave, &config, &error); if (result != IEC104_TCP_OK) { @@ -703,6 +986,7 @@ int main(int argc, char **argv) (void)IEC104_Interrogation_Destroy(app.interrogation); (void)IEC104_Interrogation_Destroy(app.counter); (void)IEC104_ClockSynchronization_Destroy(app.clock); + (void)IEC104_Command_Destroy(app.command); (void)IEC104_Slave_Remove_Station(slave, &app.station); (void)IEC104_Slave_Destroy(slave, NULL); return EXIT_FAILURE; @@ -739,34 +1023,50 @@ int main(int argc, char **argv) cleanup: /* 第4步:停止应用任务,再结束网络;取消只丢弃尚未提交完毕的任务,不重发已接手数据。 */ Example_Cancel(&app); - /* Cancel只将终结通知排队;释放总召实例前必须读完,并清理应用自己的遍历状态及资源。 */ - if (!Example_Drain_Business(&app) || !Example_Drain_Counter(&app)) { exit_code = EXIT_FAILURE; } + /* Cancel只将终结通知排队;释放业务实例前必须读完,并清理应用自己的遍历状态及资源。 + * 遥控FINISHED在离线时仍可读;这里只释放已同步完成的模拟输出,真实设备须自行等实际结束。 */ + if (!Example_Drain_Business(&app) || !Example_Drain_Counter(&app) || !Example_Drain_Command(&app)) { + exit_code = EXIT_FAILURE; + } if (IEC104_Slave_Close(slave, &error) != IEC104_TCP_OK) { exit_code = EXIT_FAILURE; } { IEC104_SlaveEvent_t event; IEC104_TcpEventResult_t next; - /* Close保留收尾通知。对时只消费非I帧状态,不再受理请求或执行动作。 */ + /* Close保留收尾通知。对时和遥控只消费非I帧状态,不再受理请求或执行动作。 + * 即使Cancel已经终结事务,也须交付断线通知让模块清除会话;首次终结原因不会被改写。 */ while ((next = IEC104_Slave_NextEvent(slave, &event)) == IEC104_TCP_EVENT_READY) { if (app.clock != NULL && event.transport.type != IEC104_TCP_EVENT_I_FRAME && IEC104_ClockSynchronization_Handle_Event(app.clock, &event) == IEC104_CLOCK_SYNCHRONIZATION_EVENT_ERROR) { exit_code = EXIT_FAILURE; } + if (app.command != NULL && event.transport.type != IEC104_TCP_EVENT_I_FRAME && + IEC104_Command_Handle_Event(app.command, &event) == IEC104_COMMAND_EVENT_ERROR) { + exit_code = EXIT_FAILURE; + } } if (next != IEC104_TCP_EVENT_EMPTY) { exit_code = EXIT_FAILURE; } } if (!IEC104_ClockSynchronization_Get_Status(app.clock, &app.cs)) { exit_code = EXIT_FAILURE; } - /* 只有关闭且FIFO读空后才能归还槽;清理失败也必须体现在程序退出码中。 */ + /* Close期间新产生的终结仍需读完;刷新统计后才能Destroy,不能用Destroy代替FINISHED清理。 */ + if (!Example_Drain_Command(&app) || !IEC104_Command_Get_Status(app.command, &app.command_status)) { + exit_code = EXIT_FAILURE; + } + /* 只有关闭且FIFO读空后才能归还槽;清理失败也必须体现在程序退出码中。 + * Command还要求事务/独立应答全部清空;应用站点引用已在FINISHED分支释放,再Remove_Station。 */ if (!IEC104_Interrogation_Destroy(app.interrogation)) { exit_code = EXIT_FAILURE; } if (!IEC104_Interrogation_Destroy(app.counter)) { exit_code = EXIT_FAILURE; } if (!IEC104_ClockSynchronization_Destroy(app.clock)) { exit_code = EXIT_FAILURE; } + if (!IEC104_Command_Destroy(app.command)) { exit_code = EXIT_FAILURE; } if (IEC104_Slave_Remove_Station(slave, &app.station) != IEC104_STATION_OK) { exit_code = EXIT_FAILURE; } if (IEC104_Slave_Destroy(slave, &error) != IEC104_TCP_OK) { exit_code = EXIT_FAILURE; } /* 打印应用提交统计;这些值不用于推断未确认帧是否已到达主站,也不作为自动重试依据。 */ - printf("STOP submitted=%lu gi_submitted=%lu waits=%lu rejected=%lu cancelled=%lu overloads=%lu deactivated=%lu timeouts=%lu negative_submitted=%lu dropped_requests=%lu counter_submitted=%lu counter_completed=%lu clock_executed=%lu clock_synchronized=%lu clock_submitted=%lu\n", + printf("STOP submitted=%lu gi_submitted=%lu waits=%lu rejected=%lu cancelled=%lu overloads=%lu deactivated=%lu timeouts=%lu negative_submitted=%lu dropped_requests=%lu counter_submitted=%lu counter_completed=%lu clock_executed=%lu clock_synchronized=%lu clock_submitted=%lu command_selected=%lu command_executed=%lu command_completed=%lu command_submitted=%lu command_returns=%lu\n", app.gi.submitted + app.other_submitted, app.gi.gi_submitted, app.gi.waits, app.gi.rejected + app.other_rejected, app.gi.cancelled + app.other_cancelled, app.gi.overloads + app.other_overloads, app.gi.deactivated, app.gi.timeouts, app.gi.negative_submitted, app.gi.dropped_requests + app.other_overloads, - app.ci.submitted, app.counter_completed, app.cs.executed, app.cs.synchronized, app.cs.submitted); + app.ci.submitted, app.counter_completed, app.cs.executed, app.cs.synchronized, app.cs.submitted, + app.command_status.selected, app.command_status.executed, app.command_status.completed, + app.command_status.submitted, app.command_status.return_info_submitted); return exit_code; } #endif diff --git a/IEC60870-Slave/scripts/build.ps1 b/IEC60870-Slave/scripts/build.ps1 index 3a1babdc..c06a8f9f 100644 --- a/IEC60870-Slave/scripts/build.ps1 +++ b/IEC60870-Slave/scripts/build.ps1 @@ -15,6 +15,7 @@ Get-Command $Compiler -ErrorAction Stop | Out-Null $libraryNames = @( '104/business/IEC104_Interrogation.c', '104/business/IEC104_GeneralInterrogation.c', '104/business/IEC104_CounterInterrogation.c', '104/business/IEC104_ClockSynchronization.c', + '104/business/IEC104_Command.c', '104/IEC104_Slave.c', 'common/IEC60870_Slave.c', 'common/IEC60870_Asdu.c', 'common/IEC60870_InformationObjects.c', 'common/IEC60870_Time.c', 'common/IEC60870_Clock.c', @@ -54,6 +55,18 @@ if ($Test) { & $testOutput if ($LASTEXITCODE -ne 0) { throw 'Slave example tests failed' } } + # Command transactions use one fixed slot and a bounded independent reply pool. + # The controlled backend includes TcpServer.c; do not link TCP/Windows Net again. + $commandTestSources = @((Join-Path $projectRoot 'test/iec104_command_test.c')) + $librarySources + foreach ($capacity in @(1, 2)) { + foreach ($replyCapacity in @(1, 4)) { + $commandOutput = Join-Path $exampleBuild "iec104_command_test_${capacity}_${replyCapacity}.exe" + & $Compiler @compileFlags "-DIEC104_TCP_SERVER_CAPACITY=$capacity" "-DIEC104_COMMAND_REPLY_CAPACITY=$replyCapacity" -DIEC104_TCP_HIGH_CAPACITY=251 @commandTestSources -o $commandOutput + if ($LASTEXITCODE -ne 0) { throw 'Command test build failed' } + & $commandOutput + if ($LASTEXITCODE -ne 0) { throw 'Command tests failed' } + } + } $rulesOutput = Join-Path $exampleBuild 'iec104_interrogation_rules_test.exe' $rulesSources = @( (Join-Path $projectRoot 'test/iec104_interrogation_rules_test.c'), diff --git a/IEC60870-Slave/test/iec104_command_test.c b/IEC60870-Slave/test/iec104_command_test.c new file mode 100644 index 00000000..b7001e63 --- /dev/null +++ b/IEC60870-Slave/test/iec104_command_test.c @@ -0,0 +1,1425 @@ +/* + * IEC104遥控业务回归:通过真实Slave、TCP及SendManager路径验证45/46事务。 + * 复用已有测试的可控Net、单调时钟和收发缓冲;重命名后的原测试入口不在本文件执行。 + * 输入及期望应答由字节手工构造,避免输入与被测编码器共享同一种错误。 + * 本文件已间接包含TcpServer.c,构建时不得再链接TcpServer.c或平台Net后端。 + * 同一组用例分别运行于TCP容量1/2、独立应答槽容量1/4,输出用例组数与公开结构尺寸。 + */ +#define main command_net_fixture_main +#include "iec104_tcp_event_ring_test.c" +#undef main +#include "IEC104_Command.h" + +#ifndef IEC104_COMMAND_REPLY_CAPACITY +#define IEC104_COMMAND_REPLY_CAPACITY 4U +#endif + +/* 每个用例独占一套服务、业务实例和站点;open/close负责隔离上一轮状态。 + * last_input保留修复自引用后的真实Slave事件,供构造发送拥塞或重入校验使用。 + * action_result控制模拟动作结果,action_calls和action_request核对至多一次执行及参数。 + * window只在窗口背压用例中缩小,close恢复默认值,防止影响下一组测试。 */ +static IEC104_Slave_t *command_slave; +static IEC104_Command_t *command; +static IEC104_Station_t command_station; +static IEC104_CommandStatus_t command_status; +static IEC104_SlaveEvent_t command_last_input; +static IEC104_CommandEventResult_t command_last_handled; +static unsigned command_master_ns, command_action_calls, command_checks; +static IEC104_CommandActionResult_t command_action_result; +static IEC104_CommandRequest_t command_action_request; +static uint16_t command_window = 32U; + +/* 消费全部Slave通知并按原FIFO交给Command,不在此消费业务事件或执行应用动作。 + * last_handled只记录最近一条I帧的分派结果,后续RECORD/启停通知不得覆盖它。 + * 用于验证“先读空通知,再推进动作”的接入约定。 */ +static void command_drain_transport(void) +{ + IEC104_SlaveEvent_t event; + IEC104_TcpEventResult_t result; + while ((result = IEC104_Slave_NextEvent(command_slave, &event)) == IEC104_TCP_EVENT_READY) { + IEC104_CommandEventResult_t handled; + if (event.transport.type == IEC104_TCP_EVENT_I_FRAME) { + CHECK(IEC104_Slave_Copy_Event(&command_last_input, &event)); + } + handled = IEC104_Command_Handle_Event(command, &event); + CHECK(handled != IEC104_COMMAND_EVENT_ERROR); + if (event.transport.type == IEC104_TCP_EVENT_I_FRAME) { command_last_handled = handled; } + } + CHECK(result == IEC104_TCP_EVENT_EMPTY); +} + +/* 推进一次真实传输层并读空通知;等待参数为0,可控后端不会真实阻塞。 */ +static void command_poll(void) +{ + CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK); + command_drain_transport(); +} + +/* 推进一步业务并刷新只读状态;等待或已正常终结也必须返回处理成功。 */ +static void command_process(void) +{ + CHECK(IEC104_Command_Process(command)); + CHECK(IEC104_Command_Get_Status(command, &command_status)); +} + +/* 有界交替驱动传输与业务,使已就绪应答交接、写出及通知消费能够完成。 + * 不自动Accept、不读取FINISHED,也不伪造主站ACK;仍然等待的事务由各用例显式处理。 */ +static void command_flush(void) +{ + unsigned index; + for (index = 0U; index < 16U; ++index) { + command_poll(); + command_process(); + } +} + +/* 经可控accept和真实STARTDT建立新会话,核对Command收到激活通知。 + * 新TCP会话的主站发送序号从0开始;事务及旧事件身份由业务模块自行隔离。 */ +static void command_connect(void) +{ + fake.accept_pending = 1U; + command_poll(); + push(start_act, sizeof(start_act)); + command_poll(); + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(command_status.active); + command_master_ns = 0U; +} + +/* 创建并激活测试服务,configuration为NULL时使用Command默认配置。 + * 链路和本地发送期限刻意放宽,使业务期限测试不会先触发无关的传输超时。 + * 查找内部TCP实例仅用于现有夹具的容量背压和故障注入,不替换公开收发入口。 */ +static void command_open(const IEC104_CommandConfig_t *configuration) +{ + IEC104_SlaveConfig_t server_configuration; + IEC104_CommandConfig_t defaults; + unsigned slot; + memset(&fake, 0, sizeof(fake)); + memset(&command_station, 0, sizeof(command_station)); + memset(&command_last_input, 0, sizeof(command_last_input)); + fake.now = 1000U; + command_action_calls = 0U; + command_action_result = IEC104_COMMAND_ACTION_STARTED; + command_slave = IEC104_Slave_Create(); + CHECK(command_slave != NULL); + IEC104_Slave_Config_Default(&server_configuration); + server_configuration.protocol.link.k = command_window; + server_configuration.protocol.link.t1_ms = 600000U; + server_configuration.protocol.link.t3_ms = 600000U; + server_configuration.protocol.local_send_timeout_ms = 600000U; + CHECK(IEC104_Slave_Init(command_slave, &server_configuration, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Add_Station(command_slave, &command_station, 1U, &command_action_calls) == IEC104_STATION_OK); + IEC104_Command_Config_Default(&defaults); + command = IEC104_Command_Create(command_slave, configuration == NULL ? &defaults : configuration); + CHECK(command != NULL); + sut = NULL; + for (slot = 0U; slot < IEC104_TCP_SERVER_CAPACITY; ++slot) { + if (server_pool[slot].in_use) { sut = &server_pool[slot].server; break; } + } + CHECK(sut != NULL); + command_connect(); +} + +/* 读取且核对恰好一个指定类型的业务事件,返回可跨循环保存的纯值副本。 + * 调用后事件才算已交付,后续是否欠应用FINISHED以此为界。 */ +static IEC104_CommandEvent_t command_take(IEC104_CommandEventType_t type) +{ + IEC104_CommandEvent_t event; + memset(&event, 0, sizeof(event)); + CHECK(IEC104_Command_NextEvent(command, &event) == IEC104_COMMAND_NEXT_READY); + CHECK(event.type == type); + return event; +} + +/* 核对业务队列确实为空,并验证EMPTY不改写调用方输出存储的接口契约。 */ +static void command_empty(void) +{ + IEC104_CommandEvent_t event, before; + memset(&event, 0x5a, sizeof(event)); + before = event; + CHECK(IEC104_Command_NextEvent(command, &event) == IEC104_COMMAND_NEXT_EMPTY); + CHECK(memcmp(&event, &before, sizeof(event)) == 0); +} + +/* 按Cancel、读完FINISHED、关闭服务、读空通知、Destroy、移除站点的顺序收尾。 + * 节点必须已可复用,才能销毁Slave;此处不替应用归还额外Acquire的引用。 */ +static void command_close(void) +{ + IEC104_CommandEvent_t event; + IEC104_CommandNextResult_t result; + IEC104_Command_Cancel(command); + while ((result = IEC104_Command_NextEvent(command, &event)) == IEC104_COMMAND_NEXT_READY) { + CHECK(event.type == IEC104_COMMAND_FINISHED); + } + CHECK(result == IEC104_COMMAND_NEXT_EMPTY); + CHECK(IEC104_Slave_Close(command_slave, NULL) == IEC104_TCP_OK); + command_drain_transport(); + CHECK(IEC104_Command_Destroy(command)); + if (IEC104_Station_Is_Registered(&command_station)) { + CHECK(IEC104_Slave_Remove_Station(command_slave, &command_station) == IEC104_STATION_OK); + } + CHECK(IEC104_Station_Is_Reusable(&command_station)); + CHECK(IEC104_Slave_Destroy(command_slave, NULL) == IEC104_TCP_OK); + command = NULL; + command_slave = NULL; + command_window = 32U; +} + +/* 将调用方提供的ASDU字节封装成主站I帧,维护主站N(S),不自动确认子站数据。 + * 只写入可控Net输入队列;何时接收、交付和推进业务由用例决定。 */ +static void command_queue(const uint8_t *payload, size_t length) +{ + uint8_t frame[255] = {0x68}; + CHECK(length <= 249U); + frame[1] = (uint8_t)(length + 4U); + frame[2] = (uint8_t)(command_master_ns << 1); + frame[3] = (uint8_t)(command_master_ns >> 7); + ++command_master_ns; + memcpy(frame + 6U, payload, length); + push(frame, length + 6U); +} + +/* 手工生成固定10字节的45/46请求或等长监视对象,用于输入和独立的期望结果。 + * cause包含完整COT字节(包括TEST/P/N),value保留原SCO/DCO或监视对象字节。 */ +static void command_bytes(uint8_t bytes[10], unsigned type, unsigned cause, + unsigned oa, unsigned ca, unsigned ioa, unsigned value) +{ + bytes[0] = (uint8_t)type; bytes[1] = 1U; bytes[2] = (uint8_t)cause; bytes[3] = (uint8_t)oa; + bytes[4] = (uint8_t)ca; bytes[5] = (uint8_t)(ca >> 8); + bytes[6] = (uint8_t)ioa; bytes[7] = (uint8_t)(ioa >> 8); bytes[8] = (uint8_t)(ioa >> 16); + bytes[9] = (uint8_t)value; +} + +/* 发送一条手工构造的请求并消费传输通知;不会自动读取或应答工作事件。 */ +static void command_send(unsigned type, unsigned cause, unsigned oa, unsigned ca, unsigned ioa, unsigned value) +{ + uint8_t bytes[10]; + command_bytes(bytes, type, cause, oa, ca, ioa, value); + command_queue(bytes, sizeof(bytes)); + command_poll(); +} + +/* 扫描已经写出的APDU,返回真实业务ASDU的地址及长度,保持线上原顺序。 + * 忽略S/U帧和制造拥塞的Type200填充帧;输入必须已写成完整帧。 + * 输出指针借用fake.output,各用例应在缓冲内容改变前完成断言。 */ +static unsigned command_collect(const uint8_t **frames, size_t *lengths) +{ + size_t offset = 0U; + unsigned count = 0U; + while (offset < fake.sent_bytes) { + const uint8_t *frame = fake.output + offset; + size_t length; + CHECK(fake.sent_bytes - offset >= 6U && frame[0] == 0x68U); + length = (size_t)frame[1] + 2U; + CHECK(length >= 6U && offset + length <= fake.sent_bytes); + if ((frame[2] & 1U) == 0U && frame[6] != 200U) { + CHECK(count < 64U); + frames[count] = frame + 6U; + lengths[count++] = length - 6U; + } + offset += length; + } + return count; +} + +/* 核对第index条业务输出的完整原文;同时检查长度、OA/CA/IOA、原命令值及COT。 + * 期望原文由手工字节构造,避免只检查状态计数而遗漏回显或帧序错误。 */ +static void command_reply_at(unsigned index, unsigned type, unsigned cause, + unsigned oa, unsigned ca, unsigned ioa, unsigned value) +{ + const uint8_t *frames[64]; + size_t lengths[64]; + uint8_t expected[10]; + CHECK(command_collect(frames, lengths) > index); + command_bytes(expected, type, cause, oa, ca, ioa, value); + CHECK(lengths[index] == sizeof(expected)); + CHECK(memcmp(frames[index], expected, sizeof(expected)) == 0); +} + +/* 返回已写出的业务I帧数量,过滤规则与command_reply_at一致。 */ +static unsigned command_frame_count(void) +{ + const uint8_t *frames[64]; size_t lengths[64]; + return command_collect(frames, lengths); +} + +/* 模拟短时应用动作:记录调用次数与模块实际交付的请求,返回用例预设结果。 + * 不发布数据、不推进网络,因此重复调用可直接识别为事务执行错误。 */ +static IEC104_CommandActionResult_t command_action(void *context, const IEC104_CommandRequest_t *request) +{ + CHECK(context == &command_action_calls); + ++command_action_calls; + command_action_request = *request; + return command_action_result; +} + +/* 受理并接受一次选择,推进其确认后返回原SELECT事件。 + * 固定使用CA=1、IOA=6001、OA=0x51,方便后续匹配与逐字段变异测试。 */ +static IEC104_CommandEvent_t command_select(unsigned type, unsigned value) +{ + IEC104_CommandEvent_t event; + command_send(type, 6U, 0x51U, 1U, 6001U, value | 0x80U); + event = command_take(IEC104_COMMAND_SELECT); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_flush(); + return event; +} + +/* 接受一次执行并返回其事件;刻意不等待确认交接,支持结果先于确认的测试。 */ +static IEC104_CommandEvent_t command_execute(unsigned type, unsigned value) +{ + IEC104_CommandEvent_t event; + command_send(type, 6U, 0x51U, 1U, 6001U, value); + event = command_take(IEC104_COMMAND_EXECUTE); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + return event; +} + +/* 构造CA=1、监视IOA=1001、COT=11的返回信息视图;监视IOA与遥控IOA不同。 + * object借用bytes,调用方在提交返回WAIT期间必须保留并维护该缓冲。 */ +static void command_return(IEC60870_Asdu_t *object, uint8_t bytes[10], unsigned type, unsigned value) +{ + command_bytes(bytes, type, 11U, 0x51U, 1U, 1001U, value); + CHECK(IEC60870_Asdu_Create_From_Buffer(object, + IEC104_Slave_Get_Application_Layer_Parameters(command_slave), bytes, 10U) == IEC60870_ASDU_OK); +} + +/* 报告给定设备结果、关闭返回信息阶段并读出FINISHED,核对成功/失败终结原因。 + * 只适用于需要的返回信息已经提交完毕、且无需额外ACK才能推进的用例。 */ +static IEC104_CommandEvent_t command_done(const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result) +{ + IEC104_CommandEvent_t finished; + command_flush(); + CHECK(IEC104_Command_Report_Result(command, event, result) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Complete(command, event) == IEC104_COMMAND_REPLY_OK); + command_flush(); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == (result == IEC104_COMMAND_RESULT_SUCCEEDED ? + IEC104_COMMAND_FINISH_COMPLETED : IEC104_COMMAND_FINISH_EXECUTION_FAILED)); + command_empty(); + return finished; +} + +/* 阻塞实际Net写入,再填满当前帧与HIGH容量,直到公开发送接口真实返回WAIT。 + * 业务层没有伪造的提交结果;解除send_blocked后由原发送管理器恢复推进。 */ +static void command_congest(void) +{ + uint8_t bytes[249] = {200, 0, 3, 0, 1, 0}; + IEC60870_Asdu_t filler; + unsigned index; + fake.send_blocked = 1U; + CHECK(IEC60870_Asdu_Create_From_Buffer(&filler, + IEC104_Slave_Get_Application_Layer_Parameters(command_slave), bytes, sizeof(bytes)) == IEC60870_ASDU_OK); + for (index = 0U; index < 32U; ++index) { + if (!IEC60870_MasterConnection_Send_ASDU(&command_last_input.connection, &filler)) { + CHECK(IEC104_Slave_Get_Last_Submit_Result(&command_last_input.connection) == IEC104_TCP_SUBMIT_WAIT); + return; + } + } + CHECK(0); +} + +/* 配置与实例契约:默认值、类型位边界、非法配置、重复绑定及NULL参数。 + * 先销毁合法空实例再逐项尝试非法配置,确保拒绝不是被已有实例掩盖。 */ +static void test_command_configuration(void) +{ + IEC104_CommandConfig_t config, invalid; + IEC104_CommandStatus_t status; + unsigned index; + IEC104_Command_Config_Default(&config); + CHECK(config.type_mask == 3U && config.select_required_mask == 3U); + CHECK(config.select_timeout_ms == 30000U && config.decision_timeout_ms == 15000U); + CHECK(config.execute_timeout_ms == 60000U && config.send_act_term && !config.act_term_on_failure); + CHECK(IEC104_COMMAND_TYPE_BIT(44U) == 0U && IEC104_COMMAND_TYPE_BIT(52U) == 0U); + CHECK(IEC104_COMMAND_TYPE_BIT(58U) == 128U && IEC104_COMMAND_TYPE_BIT(64U) == 8192U); + CHECK(IEC104_COMMAND_TYPE_BIT(65U) == 0U); + CHECK(IEC104_Command_Create(NULL, &config) == NULL); + CHECK(!IEC104_Command_Destroy(NULL)); + CHECK(!IEC104_Command_Get_Status(NULL, &status)); + command_open(NULL); + CHECK(IEC104_Command_Create(command_slave, &config) == NULL); + CHECK(IEC104_Command_Destroy(command)); + for (index = 0U; index < 5U; ++index) { + invalid = config; + switch (index) { + case 0U: invalid.type_mask = 0U; break; + case 1U: invalid.type_mask |= IEC104_COMMAND_TYPE_BIT(47U); break; + case 2U: invalid.select_timeout_ms = 0U; break; + case 3U: invalid.type_mask = 1U; invalid.select_required_mask = 2U; break; + default: invalid.select_required_mask = UINT16_MAX; break; + } + CHECK(IEC104_Command_Create(command_slave, &invalid) == NULL); + } + CHECK(IEC104_Command_Create(command_slave, NULL) == NULL); + command = IEC104_Command_Create(command_slave, &config); + CHECK(command != NULL); + command_close(); + ++command_checks; +} + +/* 接收校验顺序:先COT、再CA、再IOA、最后对象内容;TEST/P/N随否定回显保留。 + * 无唯一完整对象时静默丢弃,SQ和尾字节异常则规范成单对象10字节否定。 + * 另验证未支持或被type_mask禁用的类型返回IGNORED,不误交付工作事件。 */ +static void test_command_wire_validation(void) +{ + static const struct { unsigned type, cot, ca, ioa, value, cause; } cases[] = { + {45, 5, 65535, 0, 2, 45}, {45, 8, 65535, 0, 2, 46}, + {45, 6, 2, 0, 2, 46}, {45, 6, 1, 0, 2, 47}, + {45, 6, 1, 6001, 2, 7}, {46, 6, 1, 6001, 0, 7}, + {46, 6, 1, 6001, 3, 7}, {46, 8, 1, 6001, 3, 9}, + {45, 0x46, 1, 6001, 1, 7}, {45, 0x86, 1, 6001, 1, 0x87} + }; + unsigned index; + for (index = 0U; index < sizeof(cases) / sizeof(cases[0]); ++index) { + command_open(NULL); + command_send(cases[index].type, cases[index].cot, 0x61U, cases[index].ca, + cases[index].ioa, cases[index].value); + command_flush(); + CHECK(command_frame_count() == 1U); + command_reply_at(0U, cases[index].type, cases[index].cause | 0x40U, + 0x61U, cases[index].ca, cases[index].ioa, cases[index].value); + CHECK(command_status.rejected == 1U && command_action_calls == 0U); + command_empty(); + command_close(); + } + for (index = 0U; index < 5U; ++index) { + uint8_t bytes[14]; + size_t length = 10U; + command_open(NULL); + command_bytes(bytes, 45U, 6U, 0x62U, 1U, 6001U, 0x81U); + switch (index) { + case 0U: bytes[1] = 0U; break; + case 1U: bytes[1] = 2U; length = 14U; memset(bytes + 10U, 0, 4U); break; + case 2U: length = 9U; break; + case 3U: bytes[1] = 0x81U; break; + default: bytes[10] = 0x55U; length = 11U; break; + } + command_queue(bytes, length); command_poll(); command_flush(); + CHECK(command_frame_count() == (index < 3U ? 0U : 1U)); + if (index >= 3U) { command_reply_at(0U, 45U, 0x47U, 0x62U, 1U, 6001U, 0x81U); } + command_empty(); command_close(); + } + command_open(NULL); + command_send(47U, 6U, 0U, 1U, 6001U, 1U); + CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED); + { + uint8_t timed[17] = {0}; + command_bytes(timed, 58U, 6U, 0U, 1U, 6001U, 1U); + timed[14] = 1U; timed[15] = 1U; timed[16] = 26U; + command_queue(timed, sizeof(timed)); command_poll(); + } + CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED); + command_empty(); command_close(); + { + IEC104_CommandConfig_t config; + IEC104_Command_Config_Default(&config); + config.type_mask = config.select_required_mask = IEC104_COMMAND_TYPE_BIT(46U); + command_open(&config); + command_send(45U, 6U, 0U, 1U, 6001U, 0x81U); + CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED); + command_empty(); + command_send(46U, 6U, 0U, 1U, 6001U, 0x82U); + CHECK(command_take(IEC104_COMMAND_SELECT).request.type_id == 46U); + command_close(); + } + ++command_checks; +} + +/* 单/双命令的完整选择执行链:分别提交0、1、2帧返回信息,逐字节检查帧序。 + * 同时核对选择与执行属于同一事务但使用不同事件令牌、结果冻结、Complete一次性、 + * 旧SELECT只能查询事务不能报告执行结果,以及槽回收后旧令牌失效。 */ +static void test_command_complete_flows(void) +{ + unsigned type, count; + for (type = 45U; type <= 46U; ++type) { + for (count = 0U; count <= 2U; ++count) { + IEC104_CommandEvent_t selection, execution, finished; + IEC104_CommandTransactionInfo_t info; + IEC60870_Asdu_t data; + uint8_t bytes[10]; + unsigned index, value = type == 45U ? 1U : 2U; + command_open(NULL); + selection = command_select(type, value | 4U); + CHECK(selection.request.state == value && selection.request.qualifier == 1U); + CHECK(selection.request.is_select && !selection.request.selected); + execution = command_execute(type, value | 4U); + CHECK(execution.request.selected && !execution.request.is_select); + CHECK(execution.transaction_id == selection.transaction_id); + CHECK(execution.event_serial != selection.event_serial); + command_flush(); + CHECK(IEC104_Command_Get_Transaction(command, &selection, &info) && info.active); + CHECK(info.phase == IEC104_COMMAND_PHASE_EXECUTING && info.confirm_submitted); + CHECK(IEC104_Command_Report_Result(command, &selection, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + CHECK(IEC104_Command_Complete(command, &execution) == IEC104_COMMAND_REPLY_ERROR); + CHECK(IEC104_Command_Report_Result(command, &execution, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Report_Result(command, &execution, IEC104_COMMAND_RESULT_FAILED) == IEC104_COMMAND_REPLY_ERROR); + for (index = 0U; index < count; ++index) { + command_return(&data, bytes, type == 45U ? 1U : 3U, value); + CHECK(IEC104_Command_Submit_Return_Info(command, &execution, &data) == IEC104_COMMAND_REPLY_OK); + command_poll(); + } + CHECK(IEC104_Command_Complete(command, &execution) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Complete(command, &execution) == IEC104_COMMAND_REPLY_ERROR); + command_return(&data, bytes, 1U, 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &execution, &data) == IEC104_COMMAND_REPLY_ERROR); + command_flush(); + CHECK(command_frame_count() == count + 3U); + command_reply_at(0U, type, 7U, 0x51U, 1U, 6001U, value | 0x84U); + command_reply_at(1U, type, 7U, 0x51U, 1U, 6001U, value | 4U); + for (index = 0U; index < count; ++index) { + command_reply_at(index + 2U, type == 45U ? 1U : 3U, 11U, 0x51U, 1U, 1001U, value); + } + command_reply_at(count + 2U, type, 10U, 0x51U, 1U, 6001U, value | 4U); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_COMPLETED && finished.select_effect && finished.execute_effect); + CHECK(finished.request.receive_serial == execution.request.receive_serial); + CHECK(command_action_calls == 2U); + CHECK(!IEC104_Command_Get_Transaction(command, &execution, &info)); + CHECK(IEC104_Command_Complete(command, &execution) == IEC104_COMMAND_REPLY_STALE); + command_empty(); command_close(); + } + } + ++command_checks; +} + +/* 直接执行的结果/终止策略矩阵:设备成功或失败,组合两个ACT_TERM配置开关。 + * 明确失败终止的COT=10/P/N=0只表示流程结束,FINISHED仍保留EXECUTION_FAILED。 */ +static void test_command_result_policies(void) +{ + unsigned success, send_term, failure_term; + for (success = 0U; success < 2U; ++success) { + for (send_term = 0U; send_term < 2U; ++send_term) { + for (failure_term = 0U; failure_term < 2U; ++failure_term) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); + config.select_required_mask = 0U; + config.send_act_term = send_term != 0U; + config.act_term_on_failure = failure_term != 0U; + command_open(&config); + event = command_execute(45U, 1U); + CHECK(!event.request.selected); + finished = command_done(&event, success ? IEC104_COMMAND_RESULT_SUCCEEDED : IEC104_COMMAND_RESULT_FAILED); + CHECK(finished.execute_effect && !finished.select_effect); + CHECK(command_frame_count() == 1U + (send_term && (success || failure_term) ? 1U : 0U)); + command_reply_at(0U, 45U, 7U, 0x51U, 1U, 6001U, 1U); + if (send_term && (success || failure_term)) { command_reply_at(1U, 45U, 10U, 0x51U, 1U, 6001U, 1U); } + command_close(); + } + } + } + ++command_checks; +} + +/* 选择与执行各覆盖NOT_STARTED、STARTED、FAILED,检查确认、原因及副作用标志。 + * Accept的OK只代表事件消费;再次Accept不能重复动作。无返校动作的NULL选择 + * 可以生效,但不得错误标记为曾产生物理选择副作用。 */ +static void test_command_action_outcomes(void) +{ + unsigned selected, outcome; + for (selected = 0U; selected < 2U; ++selected) { + for (outcome = 0U; outcome < 3U; ++outcome) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); + command_send(45U, 6U, 0x51U, 1U, 6001U, selected ? 0x81U : 1U); + event = command_take(selected ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE); + command_action_result = (IEC104_CommandActionResult_t)outcome; + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + CHECK(command_action_calls == 1U); + command_flush(); + command_reply_at(0U, 45U, outcome == IEC104_COMMAND_ACTION_STARTED ? 7U : 0x47U, + 0x51U, 1U, 6001U, selected ? 0x81U : 1U); + if (outcome == IEC104_COMMAND_ACTION_STARTED) { + IEC104_Command_Cancel(command); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_CANCELLED); + } else { + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == (outcome == IEC104_COMMAND_ACTION_FAILED ? + IEC104_COMMAND_FINISH_ACTION_FAILED : IEC104_COMMAND_FINISH_REJECTED)); + } + CHECK(finished.select_effect == (selected && outcome != IEC104_COMMAND_ACTION_NOT_STARTED)); + CHECK(finished.execute_effect == (!selected && outcome != IEC104_COMMAND_ACTION_NOT_STARTED)); + command_close(); + } + } + { + IEC104_CommandEvent_t event, finished; + command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); + CHECK(IEC104_Command_Accept(command, &event, NULL, NULL) == IEC104_COMMAND_REPLY_OK); + command_flush(); IEC104_Command_Cancel(command); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(!finished.select_effect && command_action_calls == 0U); + command_close(); + } + ++command_checks; +} + +/* 事件身份和拒绝接口:分别篡改实例、事务、事件序号和类型,旧事件不得执行动作。 + * Reject只能消费一次;ENDED仍可查询,槽回收后查询失败且输出不变,新事务也不能 + * 重新接纳上一事务的令牌。 */ +static void test_command_rejection_and_stale(void) +{ + IEC104_CommandEvent_t event, stale, finished; + IEC104_CommandTransactionInfo_t info, before; + unsigned index; + command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); + for (index = 0U; index < 4U; ++index) { + stale = event; + switch (index) { + case 0U: ++stale.instance_id; break; + case 1U: ++stale.transaction_id; break; + case 2U: ++stale.event_serial; break; + default: stale.type = IEC104_COMMAND_EXECUTE; break; + } + CHECK(IEC104_Command_Accept(command, &stale, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + } + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + CHECK(IEC104_Command_Reject(command, &event, (IEC104_CommandRejectCause_t)0) == IEC104_COMMAND_REPLY_ERROR); + CHECK(IEC104_Command_Reject(command, &event, IEC104_COMMAND_REJECT_UNKNOWN_IOA) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Reject(command, &event, IEC104_COMMAND_REJECT_NEGATIVE) == IEC104_COMMAND_REPLY_STALE); + command_flush(); command_reply_at(0U, 45U, 0x6fU, 0x51U, 1U, 6001U, 0x81U); + CHECK(IEC104_Command_Get_Transaction(command, &event, &info) && !info.active); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_REJECTED && command_action_calls == 0U); + memset(&info, 0x5a, sizeof(info)); before = info; + CHECK(!IEC104_Command_Get_Transaction(command, &event, &info)); + CHECK(memcmp(&before, &info, sizeof(info)) == 0); + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + command_close(); + ++command_checks; +} + +/* 选择已确认后逐项改变执行类型、状态、QU、OA,必须取消原选择并回显新请求。 + * 其他点的新选择只得到忙否定,原选择仍可正常执行;默认禁止无选择直接执行。 */ +static void test_command_matching_and_busy(void) +{ + unsigned change; + for (change = 0U; change < 4U; ++change) { + IEC104_CommandEvent_t selected, finished; + unsigned type = change == 0U ? 46U : 45U; + unsigned value = change == 0U ? 2U : (change == 1U ? 0U : (change == 2U ? 5U : 1U)); + command_open(NULL); selected = command_select(45U, 1U); + command_send(type, 6U, change == 3U ? 0x52U : 0x51U, 1U, 6001U, value); + command_flush(); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_MISMATCH); + CHECK(finished.request.receive_serial == selected.request.receive_serial && finished.request.type_id == 45U); + command_reply_at(1U, type, 0x47U, change == 3U ? 0x52U : 0x51U, 1U, 6001U, value); + CHECK(command_action_calls == 1U); command_close(); + } + { + IEC104_CommandEvent_t event; + command_open(NULL); (void)command_select(45U, 1U); + command_send(45U, 6U, 0x51U, 1U, 6002U, 0x81U); command_flush(); + CHECK(command_status.busy && command_status.busy_rejected == 1U); + command_reply_at(1U, 45U, 0x47U, 0x51U, 1U, 6002U, 0x81U); + event = command_execute(45U, 1U); (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED); + command_close(); + } + command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); command_flush(); + CHECK(!command_status.busy); command_reply_at(0U, 45U, 0x47U, 0x51U, 1U, 6001U, 1U); + command_empty(); command_close(); + ++command_checks; +} + +/* 决定窗口内的同点激活静默丢弃:覆盖两种类型、选择/直接执行/选择后执行, + * 以及工作事件未取和已取两种情况。未取分支把原请求与重发放在同一批输入; + * 已取分支制造真实发送背压。变更类型、OA、S/E、状态、QU仍不能插入忙否定, + * 原工作身份、动作参数及决定期限保持不变,最终只出现原请求自己的确认。 */ +static void test_command_pending_same_point(void) +{ + unsigned type, phase, taken; + for (type = 45U; type <= 46U; ++type) { + for (phase = 0U; phase < 3U; ++phase) { + for (taken = 0U; taken < 2U; ++taken) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event; + IEC104_CommandTransactionInfo_t info; + uint8_t bytes[10]; + unsigned state = type == 45U ? 1U : 2U; + unsigned select_bit = phase == 0U ? 0x80U : 0U; + unsigned prior = phase == 2U ? 1U : 0U; + uint64_t started, original_receive; + IEC104_Command_Config_Default(&config); + config.decision_timeout_ms = 100U; + if (phase == 1U) { config.select_required_mask = 0U; } + command_open(&config); + if (phase == 2U) { (void)command_select(type, state); } + started = fake.now; + command_bytes(bytes, type, 6U, 0x51U, 1U, 6001U, state | select_bit); + command_queue(bytes, sizeof(bytes)); + if (taken != 0U) { + command_poll(); + event = command_take(phase == 0U ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE); + command_congest(); + } + command_queue(bytes, sizeof(bytes)); command_flush(); + original_receive = command_last_input.transport.data.i_frame.receive_serial - 1U; + fake.now += 5U; + command_send(type, 6U, 0x51U, 1U, 6001U, state | (select_bit ^ 0x80U)); + command_send(type, 6U, 0x51U, 1U, 6001U, (type == 45U ? 0U : 1U) | select_bit); + command_send(type, 6U, 0x51U, 1U, 6001U, state | select_bit | 4U); + command_send(type, 6U, 0x52U, 1U, 6001U, state | select_bit); + command_send(type == 45U ? 46U : 45U, 6U, 0x51U, 1U, 6001U, + (type == 45U ? 2U : 1U) | select_bit); + CHECK(command_last_handled == IEC104_COMMAND_EVENT_HANDLED); + command_flush(); + CHECK(command_status.accepted == prior + 1U && command_status.dropped_requests == 6U); + CHECK(command_status.rejected == 0U && command_status.busy_rejected == 0U && + command_status.overloads == 0U && command_status.negative_submitted == 0U); + CHECK(command_status.pending_replies == 0U && command_status.pending_events == (taken == 0U ? 1U : 0U)); + CHECK(command_status.timer_active && command_status.timeout_remaining_ms == 95U); + CHECK(command_frame_count() == prior && command_action_calls == prior); + if (taken == 0U) { event = command_take(phase == 0U ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE); } + CHECK(event.request.type_id == type && event.request.originator_address == 0x51U && + event.request.state == state && event.request.qualifier == 0U && + event.request.is_select == (phase == 0U) && event.request.selected == (phase == 2U)); + CHECK(event.request.accepted_ms == started && event.request.receive_serial == original_receive); + CHECK(IEC104_Command_Get_Transaction(command, &event, &info) && info.active && + info.phase == (phase == 0U ? IEC104_COMMAND_PHASE_SELECT_WAIT : IEC104_COMMAND_PHASE_EXECUTE_WAIT)); + command_empty(); + fake.send_blocked = 0U; command_flush(); + CHECK(command_frame_count() == prior); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_flush(); + CHECK(command_action_calls == prior + 1U && command_frame_count() == prior + 1U); + CHECK(command_action_request.receive_serial == original_receive && + command_action_request.state == state && command_action_request.qualifier == 0U); + command_reply_at(prior, type, 7U, 0x51U, 1U, 6001U, state | select_bit); + if (phase != 0U) { (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED); } + command_close(); + } + } + } + ++command_checks; +} + +/* 重发不能续期,也不能替换超时确认的回显来源。决定到期后只拒绝原请求; + * FINISHED仍按事务历史配对,选择曾交付时未读EXECUTE超时也须通知清理。 */ +static void test_command_pending_repeat_deadline(void) +{ + unsigned phase, taken; + for (phase = 0U; phase < 3U; ++phase) { + for (taken = 0U; taken < 2U; ++taken) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + unsigned prior = phase == 2U ? 1U : 0U; + unsigned value = phase == 0U ? 0x82U : 2U; + uint64_t original_receive; + IEC104_Command_Config_Default(&config); config.decision_timeout_ms = 10U; + if (phase == 1U) { config.select_required_mask = 0U; } + command_open(&config); + if (phase == 2U) { (void)command_select(46U, 2U); } + command_send(46U, 6U, 0x51U, 1U, 6001U, value); + original_receive = command_last_input.transport.data.i_frame.receive_serial; + if (taken != 0U) { event = command_take(phase == 0U ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE); } + fake.now += 9U; command_send(45U, 6U, 0x52U, 1U, 6001U, 0x85U); command_process(); + CHECK(command_status.dropped_requests == 1U && command_status.timeout_remaining_ms == 1U); + CHECK(command_frame_count() == prior); + ++fake.now; command_flush(); + CHECK(command_status.decision_timeouts == 1U && command_action_calls == prior); + CHECK(command_frame_count() == prior + 1U); + command_reply_at(prior, 46U, 0x47U, 0x51U, 1U, 6001U, value); + if (taken != 0U || phase == 2U) { + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_DECISION_TIMEOUT && + finished.request.receive_serial == original_receive && finished.request.type_id == 46U); + } + if (taken != 0U) { + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + } + command_empty(); command_close(); + } + } + ++command_checks; +} + +/* 静默策略只作用于通过校验的同点COT=6:禁用类型仍IGNORED,错误请求仍答错, + * 其他IOA/站点仍忙拒绝,合法撤销仍能终结原待决定事务且回显撤销请求。 */ +static void test_command_pending_repeat_boundaries(void) +{ + static const unsigned invalid[][3] = { + {3U, 0x81U, 0x6dU}, {0x86U, 0x81U, 0xc7U}, + {0x46U, 0x81U, 0x47U}, {6U, 0x83U, 0x47U} + }; + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Station_t other = {0}; + unsigned index; + IEC104_Command_Config_Default(&config); + config.type_mask = config.select_required_mask = IEC104_COMMAND_TYPE_BIT(45U); + command_open(&config); + CHECK(IEC104_Slave_Add_Station(command_slave, &other, 2U, &other) == IEC104_STATION_OK); + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); event = command_take(IEC104_COMMAND_SELECT); + command_send(45U, 6U, 0x52U, 1U, 6001U, 0U); + command_send(46U, 6U, 0x51U, 1U, 6001U, 0x82U); + CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED); + for (index = 0U; index < sizeof(invalid) / sizeof(invalid[0]); ++index) { + command_send(45U, invalid[index][0], 0x51U, 1U, 6001U, invalid[index][1]); command_flush(); + command_reply_at(index, 45U, invalid[index][2], 0x51U, 1U, 6001U, invalid[index][1]); + } + command_send(45U, 6U, 0x51U, 1U, 6002U, 0x81U); command_flush(); + command_reply_at(index++, 45U, 0x47U, 0x51U, 1U, 6002U, 0x81U); + command_send(45U, 6U, 0x51U, 2U, 6001U, 0x81U); command_flush(); + command_reply_at(index++, 45U, 0x47U, 0x51U, 2U, 6001U, 0x81U); + CHECK(command_status.dropped_requests == 1U && command_status.busy_rejected == 2U && + command_status.rejected == 6U && command_status.overloads == 0U && command_action_calls == 0U); + command_send(45U, 8U, 0x51U, 1U, 6001U, 0U); command_flush(); + command_reply_at(index++, 45U, 9U, 0x51U, 1U, 6001U, 0U); + CHECK(command_frame_count() == index && command_status.dropped_requests == 1U); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_DEACTIVATED && + finished.request.receive_serial == event.request.receive_serial); + CHECK(IEC104_Slave_Remove_Station(command_slave, &other) == IEC104_STATION_OK); + command_empty(); command_close(); ++command_checks; +} + +/* 同点依据原注册身份及会话;删除后未知CA仍回46,同CA替代节点仍属其他点并忙拒绝。 + * STOP/断线终结待决定事务后,相同原文可建立新事务;此前被丢弃的请求不得留下回复或动作。 */ +static void test_command_pending_repeat_lifetime(void) +{ + IEC104_CommandEvent_t event, next, finished; + IEC104_Station_t replacement = {0}; + unsigned mode; + command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); + CHECK(IEC104_Slave_Remove_Station(command_slave, &command_station) == IEC104_STATION_OK); + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); command_flush(); + command_reply_at(0U, 45U, 0x6eU, 0x51U, 1U, 6001U, 0x81U); + CHECK(IEC104_Slave_Add_Station(command_slave, &replacement, 1U, &replacement) == IEC104_STATION_OK); + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); command_flush(); + command_reply_at(1U, 45U, 0x47U, 0x51U, 1U, 6001U, 0x81U); + CHECK(command_status.dropped_requests == 0U && command_status.busy_rejected == 1U); + IEC104_Command_Cancel(command); finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_CANCELLED && + finished.request.station_id == event.request.station_id && IEC104_Station_Is_Reusable(&command_station)); + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); next = command_take(IEC104_COMMAND_SELECT); + CHECK(next.request.station_id != event.request.station_id && next.transaction_id != event.transaction_id); + CHECK(IEC104_Command_Accept(command, &next, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_flush(); command_reply_at(2U, 45U, 7U, 0x51U, 1U, 6001U, 0x81U); + CHECK(command_action_calls == 1U && command_frame_count() == 3U); + CHECK(IEC104_Slave_Remove_Station(command_slave, &replacement) == IEC104_STATION_OK); + command_close(); CHECK(IEC104_Station_Is_Reusable(&replacement)); + for (mode = 0U; mode < 2U; ++mode) { + IEC104_CommandConfig_t config; + IEC104_SlaveEvent_t old_input; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); command_send(46U, 6U, 0x51U, 1U, 6001U, 2U); + event = command_take(IEC104_COMMAND_EXECUTE); + CHECK(IEC104_Slave_Copy_Event(&old_input, &command_last_input)); + command_send(46U, 6U, 0x51U, 1U, 6001U, 2U); + if (mode == 0U) { push(stop_act, sizeof(stop_act)); } + else { fake.recv_eof = 1U; } + command_poll(); finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == (mode == 0U ? IEC104_COMMAND_FINISH_STOPPED : IEC104_COMMAND_FINISH_DISCONNECTED)); + CHECK(command_action_calls == 0U && command_frame_count() == 0U); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + if (mode == 0U) { push(start_act, sizeof(start_act)); command_poll(); } + else { + fake.recv_eof = 0U; command_connect(); + CHECK(IEC104_Command_Handle_Event(command, &old_input) == IEC104_COMMAND_EVENT_IGNORED); + } + command_send(46U, 6U, 0x51U, 1U, 6001U, 2U); next = command_take(IEC104_COMMAND_EXECUTE); + CHECK(next.transaction_id != event.transaction_id); + CHECK((next.request.generation == event.request.generation) == (mode == 0U)); + CHECK(IEC104_Command_Accept(command, &next, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + (void)command_done(&next, IEC104_COMMAND_RESULT_SUCCEEDED); + CHECK(command_action_calls == 1U && command_frame_count() == 2U && command_status.dropped_requests == 1U); + command_reply_at(0U, 46U, 7U, 0x51U, 1U, 6001U, 2U); + command_empty(); command_close(); + } + ++command_checks; +} + +/* FINISHED配对按事务历史判断:未读SELECT、未读直接EXECUTE不欠终结通知; + * SELECT曾交付后,即使匹配的EXECUTE尚未读,撤销也必须恰好交付一次FINISHED。 + * 撤销不要求命令值相同,DEACT_CON应回显撤销请求自身的值。 */ +static void test_command_finished_pairing(void) +{ + unsigned mode; + for (mode = 0U; mode < 4U; ++mode) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); + if (mode >= 2U) { + event = command_select(45U, 1U); + command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); + } else { + command_send(45U, 6U, 0x51U, 1U, 6001U, mode == 0U ? 0x81U : 1U); + } + if (mode == 3U) { event = command_take(IEC104_COMMAND_EXECUTE); } + command_send(45U, 8U, 0x51U, 1U, 6001U, 0U); command_flush(); + if (mode < 2U) { command_empty(); } + else { + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_DEACTIVATED); + CHECK(finished.transaction_id == event.transaction_id && finished.select_effect); + command_empty(); + } + command_reply_at(mode >= 2U ? 1U : 0U, 45U, 9U, 0x51U, 1U, 6001U, 0U); + command_close(); + } + ++command_checks; +} + +/* 选择确认未交接时的优先级:即使新执行值或类型不匹配,也只按实例忙拒绝。 + * 发送恢复后原选择仍有效,再发匹配执行应成功,不能提前走MISMATCH取消路径。 */ +static void test_command_unsubmitted_selection(void) +{ + IEC104_CommandEvent_t selected, executed; + IEC104_CommandTransactionInfo_t info; + command_open(NULL); + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + selected = command_take(IEC104_COMMAND_SELECT); command_congest(); + CHECK(IEC104_Command_Accept(command, &selected, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_process(); + CHECK(IEC104_Command_Get_Transaction(command, &selected, &info) && !info.confirm_submitted); + /* 选择确认尚在模块内等待;此时不匹配执行也不能使已生效选择作废。 */ + command_send(46U, 6U, 0x51U, 1U, 6001U, 2U); + CHECK(IEC104_Command_Get_Transaction(command, &selected, &info) && info.active); + CHECK(info.phase == IEC104_COMMAND_PHASE_SELECTED); + fake.send_blocked = 0U; command_flush(); + CHECK(command_status.mismatches == 0U && command_status.busy_rejected == 1U); + command_reply_at(0U, 45U, 7U, 0x51U, 1U, 6001U, 0x81U); + command_reply_at(1U, 46U, 0x47U, 0x51U, 1U, 6001U, 2U); + executed = command_execute(45U, 1U); + (void)command_done(&executed, IEC104_COMMAND_RESULT_SUCCEEDED); + CHECK(command_action_calls == 2U); + command_close(); ++command_checks; +} + +/* 应用结束与槽释放分离:先读取撤销FINISHED,但DEACT_CON仍被HIGH背压阻塞。 + * 事务必须继续占槽,Destroy失败,新选择忙拒绝;确认交接后才允许释放。 */ +static void test_command_deactivation_wait(void) +{ + IEC104_CommandEvent_t event, finished; + command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); command_congest(); + command_send(45U, 8U, 0x51U, 1U, 6001U, 0U); command_process(); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_DEACTIVATED); + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(command_status.busy && command_status.pending_replies != 0U); + CHECK(!IEC104_Command_Destroy(command)); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + command_send(45U, 6U, 0x52U, 1U, 6002U, 0x81U); + fake.send_blocked = 0U; command_flush(); + CHECK(!command_status.busy); + command_reply_at(0U, 45U, 9U, 0x51U, 1U, 6001U, 0U); + command_reply_at(1U, 45U, 0x47U, 0x52U, 1U, 6002U, 0x81U); + command_empty(); command_close(); + ++command_checks; +} + +/* 决定、选择、执行三种期限分别验证到期前、恰好到期和单调时间倒退。 + * 不先调用Process,而由Accept、Handle_Event、Report_Result或NextEvent触发检查, + * 防止过期状态被新的操作复活;关闭持续时长限制仍不能掩盖时钟倒退。 */ +static void test_command_deadlines(void) +{ + unsigned phase, boundary; + for (phase = 0U; phase < 3U; ++phase) { + for (boundary = 0U; boundary < 3U; ++boundary) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + uint64_t started; + IEC104_Command_Config_Default(&config); + config.decision_timeout_ms = config.select_timeout_ms = config.execute_timeout_ms = 10U; + command_open(&config); + if (phase == 0U) { + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); + } else { + event = command_select(45U, 1U); + if (phase == 2U) { event = command_execute(45U, 1U); command_flush(); } + } + started = fake.now; + if (phase == 1U) { + uint8_t bytes[10]; + command_bytes(bytes, 45U, 6U, 0x51U, 1U, 6001U, 1U); + command_queue(bytes, sizeof(bytes)); + CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK); + } + /* 传输接收时保持正常时间,再改变业务受理时刻,隔离链路计时的影响。 */ + fake.now = boundary == 2U ? started - 1U : started + 9U + boundary; + if (phase == 0U) { + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == + (boundary == 0U ? IEC104_COMMAND_REPLY_OK : IEC104_COMMAND_REPLY_STALE)); + } else if (phase == 1U) { + command_drain_transport(); + } else { + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == + (boundary == 0U ? IEC104_COMMAND_REPLY_OK : IEC104_COMMAND_REPLY_STALE)); + } + if (boundary != 0U) { + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == (phase == 0U ? IEC104_COMMAND_FINISH_DECISION_TIMEOUT : + (phase == 1U ? IEC104_COMMAND_FINISH_SELECT_TIMEOUT : IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT))); + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(command_status.clock_errors == (boundary == 2U ? 1U : 0U)); + } else if (phase == 1U) { + event = command_take(IEC104_COMMAND_EXECUTE); CHECK(event.request.selected); + } + fake.now = started + 10U; + command_close(); + } + } + /* 期限配置0仅关闭持续时长限制;单调时钟倒退仍须终结并累计clock_errors。 */ + for (phase = 0U; phase < 2U; ++phase) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); + config.select_required_mask = 0U; + config.decision_timeout_ms = config.execute_timeout_ms = 0U; + command_open(&config); + command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); + event = command_take(IEC104_COMMAND_EXECUTE); + if (phase != 0U) { + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_flush(); + } + --fake.now; + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(!command_status.timer_active && command_status.need_process); + if (phase == 0U) { + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + CHECK(command_action_calls == 0U); + } else { + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + } + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == (phase == 0U ? IEC104_COMMAND_FINISH_DECISION_TIMEOUT : IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT)); + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(command_status.clock_errors == 1U); + ++fake.now; + command_close(); + } + /* SELECT已经交付,即使后续EXECUTE尚未读取,决定超时仍欠应用一次FINISHED。 */ + { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event; + IEC104_Command_Config_Default(&config); config.decision_timeout_ms = 10U; + command_open(&config); (void)command_select(45U, 1U); + command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); fake.now += 10U; + event = command_take(IEC104_COMMAND_FINISHED); + CHECK(event.reason == IEC104_COMMAND_FINISH_DECISION_TIMEOUT); + command_empty(); command_close(); + } + ++command_checks; +} + +/* 设备结果与发送等待分离:ACT_CON被真实发送背压阻塞时先报告设备成功。 + * 随后跨过执行期限也不得超时或重复动作;解除背压后依次输出确认、返回信息、终止。 */ +static void test_command_result_before_confirm(void) +{ + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_CommandTransactionInfo_t info; + IEC60870_Asdu_t data; + uint8_t bytes[10]; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; config.execute_timeout_ms = 10U; + command_open(&config); command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); + event = command_take(IEC104_COMMAND_EXECUTE); command_congest(); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_process(); + CHECK(IEC104_Command_Get_Transaction(command, &event, &info) && !info.confirm_submitted); + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_OK); + command_return(&data, bytes, 1U, 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_WAIT); + fake.now += 100U; command_process(); + CHECK(command_status.execute_timeouts == 0U && command_action_calls == 1U); + fake.send_blocked = 0U; command_flush(); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Complete(command, &event) == IEC104_COMMAND_REPLY_OK); + command_flush(); finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_COMPLETED && command_frame_count() == 3U); + command_reply_at(0U, 45U, 7U, 0x51U, 1U, 6001U, 1U); + command_reply_at(1U, 1U, 11U, 0x51U, 1U, 1001U, 1U); + command_reply_at(2U, 45U, 10U, 0x51U, 1U, 6001U, 1U); + command_close(); ++command_checks; +} + +/* 全局FIFO与独立应答满池:连续忙请求填满固定槽,多出的新请求丢弃并计数。 + * 只排空传输缓冲而不推进Command时,Submit_Return_Info仍须等待模块内旧应答, + * 证明直接Try路径不能绕过业务FIFO;解除后按原请求OA顺序回包。 */ +static void test_command_fifo_and_overload(void) +{ + IEC104_CommandEvent_t event; + IEC60870_Asdu_t data; + uint8_t bytes[10]; + unsigned index; + command_open(NULL); (void)command_select(45U, 1U); event = command_execute(45U, 1U); command_flush(); + command_congest(); + for (index = 0U; index < IEC104_COMMAND_REPLY_CAPACITY + 2U; ++index) { + command_send(45U, 6U, 0x60U + index, 1U, 6002U, 0x81U); + } + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(command_status.pending_replies == IEC104_COMMAND_REPLY_CAPACITY); + CHECK(command_status.overloads == 2U && command_status.dropped_requests == 2U); + fake.send_blocked = 0U; + /* 只推进TCP,保留Command队首;底层空闲不能成为返回信息插队的理由。 */ + for (index = 0U; index < 16U; ++index) { command_poll(); } + command_return(&data, bytes, 1U, 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_WAIT); + command_flush(); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_OK); + command_poll(); (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED); + for (index = 0U; index < IEC104_COMMAND_REPLY_CAPACITY; ++index) { + command_reply_at(index + 2U, 45U, 0x47U, 0x60U + index, 1U, 6002U, 0x81U); + } + command_reply_at(IEC104_COMMAND_REPLY_CAPACITY + 2U, 1U, 11U, 0x51U, 1U, 1001U, 1U); + command_close(); ++command_checks; +} + +/* 返回信息必须完整验证类型、COT、P/N、TEST、CA、对象品质和精确长度。 + * 错误ASDU不得接手;合法的COT=3及与遥控点不同的监视IOA应可提交。 */ +static void test_command_return_validation(void) +{ + IEC104_CommandEvent_t event; + IEC60870_Asdu_t object; + uint8_t bytes[11]; + unsigned index; + command_open(NULL); (void)command_select(45U, 1U); event = command_execute(45U, 1U); command_flush(); + for (index = 0U; index < 7U; ++index) { + size_t length = 10U; + command_bytes(bytes, 1U, 11U, 0x51U, 1U, 1001U, 1U); + switch (index) { + case 0U: bytes[0] = 45U; break; + case 1U: bytes[2] = 5U; break; + case 2U: bytes[2] |= 0x40U; break; + case 3U: bytes[2] |= 0x80U; break; + case 4U: bytes[4] = 2U; break; + case 5U: bytes[9] = 2U; break; + default: bytes[10] = 1U; length = 11U; break; + } + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, + IEC104_Slave_Get_Application_Layer_Parameters(command_slave), bytes, length) == IEC60870_ASDU_OK); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &object) == IEC104_COMMAND_REPLY_ERROR); + } + command_bytes(bytes, 1U, 3U, 0x51U, 1U, 9876U, 0U); + CHECK(IEC60870_Asdu_Create_From_Buffer(&object, + IEC104_Slave_Get_Application_Layer_Parameters(command_slave), bytes, 10U) == IEC60870_ASDU_OK); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &object) == IEC104_COMMAND_REPLY_OK); + command_poll(); (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED); + command_reply_at(2U, 1U, 3U, 0x51U, 1U, 9876U, 0U); + command_close(); ++command_checks; +} + +/* STOP/START通知优先于同一时刻的决定超时:未读通知时Accept只返回WAIT且不动作。 + * 按FIFO消费后,旧事务原因保持STOPPED,重新激活也不能复活旧事件。 */ +static void test_command_stop_notifications(void) +{ + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); config.decision_timeout_ms = 10U; + command_open(&config); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); + fake.now += 10U; + push(stop_act, sizeof(stop_act)); push(start_act, sizeof(start_act)); + CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_WAIT); + command_process(); CHECK(command_status.events_pending && command_status.decision_timeouts == 0U); + command_drain_transport(); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_STOPPED && command_action_calls == 0U); + command_empty(); CHECK(IEC104_Command_Get_Status(command, &command_status) && command_status.active); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE); + command_close(); + ++command_checks; +} + +/* 执行中断线不回滚动作,但必须交付DISCONNECTED并拒绝迟到结果。 + * 重连后的同点新事务具有新连接代次与事务号,旧令牌不能作用于新输出。 */ +static void test_command_disconnect_and_reuse(void) +{ + IEC104_CommandEvent_t event, next, finished; + IEC104_CommandConfig_t config; + uint64_t generation; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); event = command_execute(45U, 1U); command_flush(); + generation = event.request.generation; + fake.recv_eof = 1U; command_poll(); + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_DISCONNECTED && finished.execute_effect); + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + command_empty(); fake.recv_eof = 0U; command_connect(); + next = command_execute(45U, 1U); + CHECK(next.request.generation != generation && next.transaction_id != event.transaction_id); + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + (void)command_done(&next, IEC104_COMMAND_RESULT_SUCCEEDED); command_close(); + ++command_checks; +} + +/* 原站点删除、同CA重新注册时,已受理事务仍持原节点,不能匹配替代站点的执行。 + * 应用在工作事件阶段额外Acquire,保证FINISHED出队并释放模块引用之后仍能清理; + * 最后一次应用Release才允许旧节点复用。 */ +static void test_command_station_lifetime(void) +{ + IEC104_CommandEvent_t event, finished; + IEC104_Station_t replacement = {0}; + uint64_t original; + command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); original = event.request.station_id; + CHECK(IEC104_Slave_Acquire_Station(command_slave, &command_station) == IEC104_STATION_OK); + CHECK(IEC104_Slave_Remove_Station(command_slave, &command_station) == IEC104_STATION_OK); + CHECK(!IEC104_Station_Is_Reusable(&command_station)); + CHECK(IEC104_Slave_Add_Station(command_slave, &replacement, 1U, &replacement) == IEC104_STATION_OK); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_flush(); CHECK(command_action_request.station_id == original); + command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); command_flush(); + CHECK(command_action_calls == 1U && command_status.busy); + command_reply_at(1U, 45U, 0x47U, 0x51U, 1U, 6001U, 1U); + IEC104_Command_Cancel(command); finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_CANCELLED && finished.request.station_id == original); + CHECK(IEC104_Slave_Get_Station_By_Id(command_slave, original) == &command_station); + CHECK(IEC104_Slave_Release_Station(command_slave, &command_station) == IEC104_STATION_OK); + CHECK(IEC104_Station_Is_Reusable(&command_station)); + CHECK(IEC104_Slave_Remove_Station(command_slave, &replacement) == IEC104_STATION_OK); + command_close(); ++command_checks; +} + +/* 供动作回调使用的已交付事件,专门验证重入时不能再次消费或销毁当前事务。 */ +static IEC104_CommandEvent_t command_reentry_event; +/* 回调中只读查询应成功,推进、消费、拒绝、销毁及重复动作应被拒绝。 + * Cancel为无效果操作;返回后原动作仍只执行一次,并可正常报告结果。 */ +static IEC104_CommandActionResult_t command_reentry_action(void *context, const IEC104_CommandRequest_t *request) +{ + IEC104_CommandTransactionInfo_t info; + IEC104_CommandEvent_t ignored; + IEC104_CommandStatus_t status; + CHECK(IEC104_Command_Get_Status(command, &status)); + CHECK(IEC104_Command_Get_Transaction(command, &command_reentry_event, &info)); + CHECK(!IEC104_Command_Destroy(command)); + CHECK(!IEC104_Command_Process(command)); + CHECK(IEC104_Command_NextEvent(command, &ignored) == IEC104_COMMAND_NEXT_ERROR); + CHECK(IEC104_Command_Accept(command, &command_reentry_event, command_action, context) == IEC104_COMMAND_REPLY_ERROR); + CHECK(IEC104_Command_Reject(command, &command_reentry_event, IEC104_COMMAND_REJECT_NEGATIVE) == IEC104_COMMAND_REPLY_ERROR); + CHECK(IEC104_Command_Handle_Event(command, &command_last_input) == IEC104_COMMAND_EVENT_ERROR); + IEC104_Command_Cancel(command); + return command_action(context, request); +} + +/* EXECUTE必须提供动作;回调重入受测入口不会改变当前事务或触发第二次动作。 */ +static void test_command_reentry(void) +{ + IEC104_CommandConfig_t config; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); + command_reentry_event = command_take(IEC104_COMMAND_EXECUTE); + CHECK(IEC104_Command_Accept(command, &command_reentry_event, NULL, NULL) == IEC104_COMMAND_REPLY_ERROR); + CHECK(IEC104_Command_Accept(command, &command_reentry_event, command_reentry_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + CHECK(command_action_calls == 1U); + (void)command_done(&command_reentry_event, IEC104_COMMAND_RESULT_SUCCEEDED); + command_close(); ++command_checks; +} + +/* 将k缩为1,逐帧给出真实主站ACK:选择确认、执行确认、返回信息各占一个窗口。 + * 未获得容量时Submit_Return_Info返回WAIT,Complete可关闭阶段但TERM仍等待窗口, + * 最后交接TERM后才交付完成通知,随后按统一生命周期释放资源。 */ +static void test_command_window_and_cancel(void) +{ + IEC104_CommandEvent_t event; + IEC60870_Asdu_t data; + uint8_t bytes[10]; + command_window = 1U; command_open(NULL); (void)command_select(45U, 1U); + event = command_execute(45U, 1U); command_process(); + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_OK); + command_return(&data, bytes, 1U, 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_WAIT); + push_ack(1U); command_poll(); command_flush(); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_WAIT); + push_ack(2U); command_poll(); command_flush(); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_OK); + CHECK(IEC104_Command_Complete(command, &event) == IEC104_COMMAND_REPLY_OK); + command_flush(); CHECK(command_frame_count() == 3U); + push_ack(3U); command_poll(); command_flush(); + CHECK(command_take(IEC104_COMMAND_FINISHED).reason == IEC104_COMMAND_FINISH_COMPLETED); + CHECK(command_frame_count() == 4U); command_close(); + ++command_checks; +} + +/* TCP容量至少为2时创建第二个独立Slave,验证实例池可分配且事件归属不串服务。 + * 外来Slave通知被拒绝,外来工作事件不能触发动作;容量1构建仍保留其余测试。 */ +static void test_command_two_services(void) +{ +#if IEC104_TCP_SERVER_CAPACITY >= 2 + IEC104_Slave_t *other; + IEC104_Command_t *other_command; + IEC104_CommandConfig_t config; + IEC104_SlaveConfig_t service_config; + IEC104_CommandEvent_t event; + command_open(NULL); + other = IEC104_Slave_Create(); CHECK(other != NULL); + IEC104_Slave_Config_Default(&service_config); + CHECK(IEC104_Slave_Init(other, &service_config, NULL) == IEC104_TCP_OK); + IEC104_Command_Config_Default(&config); + other_command = IEC104_Command_Create(other, &config); CHECK(other_command != NULL); + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + event = command_take(IEC104_COMMAND_SELECT); + CHECK(IEC104_Command_Handle_Event(other_command, &command_last_input) == IEC104_COMMAND_EVENT_ERROR); + CHECK(IEC104_Command_Accept(other_command, &event, command_action, &command_action_calls) != IEC104_COMMAND_REPLY_OK); + CHECK(command_action_calls == 0U); + CHECK(IEC104_Command_Destroy(other_command)); + CHECK(IEC104_Slave_Close(other, NULL) == IEC104_TCP_OK); + CHECK(IEC104_Slave_Destroy(other, NULL) == IEC104_TCP_OK); + command_close(); +#endif + ++command_checks; +} + +/* 对照NORMAL覆盖行为:先把一条返回信息入NORMAL,再以填充事件将其覆盖。 + * Command的返回接口在队列未排空时返回WAIT且不接手,排空后直接提交的返回帧 + * 仍能按顺序送出,证明命令返回路径没有依赖可覆盖缓存。 */ +static void test_command_normal_overwrite(void) +{ + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event; + IEC60870_Asdu_t object, filler; + uint8_t bytes[10], fill_bytes[249] = {200, 0, 3, 0, 1, 0}; + unsigned index; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); event = command_execute(45U, 1U); command_flush(); + command_return(&object, bytes, 1U, 0U); + CHECK(IEC104_Slave_Enqueue_Event(command_slave, &object) == IEC104_TCP_SUBMIT_OK); + CHECK(IEC60870_Asdu_Create_From_Buffer(&filler, + IEC104_Slave_Get_Application_Layer_Parameters(command_slave), fill_bytes, sizeof(fill_bytes)) == IEC60870_ASDU_OK); + for (index = 0U; index < IEC104_TCP_NORMAL_CAPACITY / sizeof(fill_bytes) + 4U; ++index) { + CHECK(IEC104_Slave_Enqueue_Event(command_slave, &filler) == IEC104_TCP_SUBMIT_OK); + } + command_return(&object, bytes, 1U, 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &object) == IEC104_COMMAND_REPLY_WAIT); + command_flush(); + /* NORMAL中的第一条返回信息(值0)已被覆盖;过滤填充帧后只有ACT_CON。 */ + CHECK(command_frame_count() == 1U); + CHECK(IEC104_Command_Submit_Return_Info(command, &event, &object) == IEC104_COMMAND_REPLY_OK); + command_poll(); (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED); + CHECK(command_frame_count() == 3U); + command_reply_at(1U, 1U, 11U, 0x51U, 1U, 1001U, 1U); + command_close(); ++command_checks; +} + +/* 临时拒绝启停通知准入,让真实STOP停留在传输层,供提交入口再次处理。 */ +static int command_hold_transfer(void *context) { (void)context; return 0; } + +/* 利用原传输观察者的背压入口保留STOP,随后恢复观察者。 + * 此时服务仍显示已启动且通知队列为空,下一次提交必须亲自处理STOP竞态。 */ +static void command_retain_stop(void) +{ + IEC104_ConnectionTransferObserver_t observer = sut->connection.transfer_observer; + IEC104_TcpServerStatus_t status; + sut->connection.transfer_observer.can_accept = command_hold_transfer; + push(stop_act, sizeof(stop_act)); + CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK); + sut->connection.transfer_observer = observer; + CHECK(sut->rx_blocked && !sut->connection.receive_batch_pending); + CHECK(IEC104_Slave_Get_Status(command_slave, &status, NULL) == IEC104_TCP_OK); + CHECK(status.pending_event_count == 0U && status.connection.data_transfer_started); +} + +/* 先真实交付启停通知,再报告观察者失败,构造“已有STOP通知但结果为FATAL”。 + * 用于区分可以让出的正常STOP与必须立即终结的传输故障。 */ +static int command_fail_after_stop(void *context, IEC104_DataTransferState_t state) +{ + CHECK(Notify_Transfer_State(context, state)); + return 0; +} + +/* 事务确认与独立忙否定同时挂起时,在提交入口内才受理STOP。 + * 正常STOP应等待通知再以STOPPED收尾;注入FATAL则立即记SEND_ERROR,不能降为WAIT。 + * 两种路径都清理独立应答、保留副作用事实,并拒绝迟到Report_Result。 */ +static void test_command_stop_during_submit(void) +{ + unsigned fatal; + for (fatal = 0U; fatal < 2U; ++fatal) { + IEC104_CommandConfig_t config; + IEC104_CommandEvent_t event, finished; + IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; + command_open(&config); command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); + event = command_take(IEC104_COMMAND_EXECUTE); command_congest(); + CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK); + command_process(); + command_send(45U, 6U, 0x62U, 1U, 6002U, 0x81U); + CHECK(IEC104_Command_Get_Status(command, &command_status) && command_status.pending_replies == 2U); + command_retain_stop(); fake.send_blocked = 0U; + if (fatal) { sut->connection.transfer_observer.notify = command_fail_after_stop; } + command_process(); + if (fatal) { + /* 即使已有STOP通知,FATAL仍必须保留发送错误语义,不能当成普通让出。 */ + finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_SEND_ERROR); + sut->connection.transfer_observer.notify = Notify_Transfer_State; + command_drain_transport(); + } else { + CHECK(command_status.events_pending && command_status.busy && !command_status.need_process); + command_drain_transport(); finished = command_take(IEC104_COMMAND_FINISHED); + CHECK(finished.reason == IEC104_COMMAND_FINISH_STOPPED); + } + CHECK(finished.execute_effect && command_action_calls == 1U); + CHECK(IEC104_Command_Get_Status(command, &command_status) && command_status.pending_replies == 0U); + CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE); + command_empty(); command_close(); + } + ++command_checks; +} + +/* 无活动事务但仍有独立错误应答时,实例也不能销毁。 + * Cancel必须清理该应答池;不会因不存在工作事件而凭空产生FINISHED。 */ +static void test_command_independent_reply_lifetime(void) +{ + command_open(NULL); + command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); + command_congest(); + /* 取消尚未交付的事务,再制造独立错误应答,隔离应答池自身的生命周期。 */ + IEC104_Command_Cancel(command); + command_send(45U, 6U, 0x61U, 2U, 6001U, 0x81U); + CHECK(IEC104_Command_Get_Status(command, &command_status)); + CHECK(!command_status.busy && command_status.pending_replies == 1U); + CHECK(!IEC104_Command_Destroy(command)); + IEC104_Command_Cancel(command); + CHECK(IEC104_Command_Get_Status(command, &command_status) && command_status.pending_replies == 0U); + command_empty(); command_close(); ++command_checks; +} + +/* 执行27组契约测试;各组内部的类型、动作结果、期限及策略组合分别断言。 + * 尺寸输出用于记录当前宿主ABI的公开结构布局,不当作MCU内存预算。 */ +int main(void) +{ + test_command_configuration(); + test_command_wire_validation(); + test_command_complete_flows(); + test_command_result_policies(); + test_command_action_outcomes(); + test_command_rejection_and_stale(); + test_command_matching_and_busy(); + test_command_pending_same_point(); + test_command_pending_repeat_deadline(); + test_command_pending_repeat_boundaries(); + test_command_pending_repeat_lifetime(); + test_command_finished_pairing(); + test_command_unsubmitted_selection(); + test_command_deactivation_wait(); + test_command_deadlines(); + test_command_result_before_confirm(); + test_command_fifo_and_overload(); + test_command_return_validation(); + test_command_stop_notifications(); + test_command_disconnect_and_reuse(); + test_command_station_lifetime(); + test_command_reentry(); + test_command_window_and_cancel(); + test_command_two_services(); + test_command_normal_overwrite(); + test_command_stop_during_submit(); + test_command_independent_reply_lifetime(); + printf("Command tests passed: groups=%u tcp=%u replies=%u request=%zu event=%zu status=%zu\n", + command_checks, (unsigned)IEC104_TCP_SERVER_CAPACITY, (unsigned)IEC104_COMMAND_REPLY_CAPACITY, + sizeof(IEC104_CommandRequest_t), sizeof(IEC104_CommandEvent_t), sizeof(IEC104_CommandStatus_t)); + return 0; +} diff --git a/IEC60870-Slave/test/iec104_slave_example_test.c b/IEC60870-Slave/test/iec104_slave_example_test.c index e870763a..9eb05ae0 100644 --- a/IEC60870-Slave/test/iec104_slave_example_test.c +++ b/IEC60870-Slave/test/iec104_slave_example_test.c @@ -176,34 +176,36 @@ static void check_gi(unsigned extra_reply, uint8_t oa) static const int16_t measures[] = {2300, 2310, 2290, 5000, -125, 0, 1234, 42}; const uint8_t *frames[32]; size_t lengths[32]; - unsigned f, p, indexes[6], gi_count = 0U; + unsigned f, p, indexes[8], gi_count = 0U; unsigned total = collect_asdus(frames, lengths); - CHECK(total == 6 + extra_reply); + CHECK(total == 8 + extra_reply); for (f = 0U; f < total; ++f) { if (frames[f][3] == oa && (frames[f][2] & 0x40U) == 0U) { - CHECK(gi_count < 6U); + CHECK(gi_count < 8U); indexes[gi_count++] = f; } } - CHECK(gi_count == 6U); /* 其他请求的否定应答可穿插,但本次总召内容及自身顺序不变。 */ - for (f = 0; f < 6; ++f) { + CHECK(gi_count == 8U); /* 其他请求的否定应答可穿插,但本次总召内容及自身顺序不变。 */ + for (f = 0; f < 8; ++f) { const uint8_t *a = frames[indexes[f]]; CHECK(a[3] == oa && a[4] == 1 && a[5] == 0); - if (f == 0 || f == 5) { + if (f == 0 || f == 7) { CHECK(lengths[indexes[f]] == 10 && a[0] == 100 && a[1] == 1); CHECK(a[2] == (f == 0 ? 0x87 : 0x8a)); CHECK(a[6] == 0 && a[7] == 0 && a[8] == 0 && a[9] == 20); } else { - unsigned is_signal = f < 3; + unsigned is_signal = f < 5; + unsigned is_single = f < 3; size_t width = is_signal ? 4U : 6U; - unsigned first = (is_signal ? f - 1U : f - 3U) * 4U; - CHECK(a[0] == (is_signal ? 1 : 11) && a[1] == 4 && a[2] == 0x94); + unsigned first = (is_single ? f - 1U : is_signal ? f - 3U : f - 5U) * 4U; + CHECK(a[0] == (is_single ? 1 : is_signal ? 3 : 11) && a[1] == 4 && a[2] == 0x94); CHECK(lengths[indexes[f]] == 6 + width * 4); for (p = 0; p < 4; ++p) { const uint8_t *object = a + 6 + width * p; unsigned ioa = object[0] | (unsigned)object[1] << 8 | (unsigned)object[2] << 16; - CHECK(ioa == (is_signal ? 1001U : 2001U) + first + p); - if (is_signal) { CHECK(object[3] == values[first + p]); } + CHECK(ioa == (is_single ? 1001U : is_signal ? 1101U : 2001U) + first + p); + if (is_single) { CHECK(object[3] == values[first + p]); } + else if (is_signal) { CHECK(object[3] == 1U); } else { CHECK((int16_t)(object[3] | (uint16_t)object[4] << 8) == measures[first + p]); CHECK(object[5] == 0); @@ -262,7 +264,7 @@ static void test_wait_each_phase(void) { static const struct { IEC104_InterrogationPhase_t phase; size_t batch; } phases[] = { {IEC104_INTERROGATION_CONFIRM, 0}, {IEC104_INTERROGATION_DATA, 0}, - {IEC104_INTERROGATION_DATA, 2}, {IEC104_INTERROGATION_TERMINATE, 4} + {IEC104_INTERROGATION_DATA, 2}, {IEC104_INTERROGATION_DATA, 4}, {IEC104_INTERROGATION_TERMINATE, 6} }; unsigned i; for (i = 0; i < sizeof(phases) / sizeof(phases[0]); ++i) { @@ -337,7 +339,7 @@ static void test_busy(void) CHECK(app.gi.pending_requests == 1 && app.gi.negative_submitted == 1); app_finish(); check_gi(1, 0x5a); - CHECK(collect_asdus(out, lengths) == 7); + CHECK(collect_asdus(out, lengths) == 9); CHECK(lengths[0] == 10 && out[0][2] == 0xc7 && out[0][3] == 0x6b); app_close(); } @@ -2083,15 +2085,22 @@ static void test_example_groups(void) memcpy(request, gi_request, sizeof(request)); request[9] = groups[i]; app_request(request, sizeof(request)); app_finish(); count = collect_asdus(frames, lengths); - CHECK(count == (groups[i] <= 22 ? 4U : 1U)); + CHECK(count == (groups[i] == 21 ? 6U : groups[i] == 22 ? 4U : 1U)); if (count == 1) { CHECK(frames[0][2] == 0xc7 && app.gi.gi_submitted == 0); } else { - CHECK(frames[0][2] == 0x87 && frames[3][2] == 0x8a); - for (f = 1; f <= 2; ++f) { + CHECK(frames[0][2] == 0x87 && frames[count - 1U][2] == 0x8a); + for (f = 1; f < count - 1U; ++f) { const uint8_t *a = frames[f]; - unsigned first = groups[i] == 21 ? 1001 : 2001; - CHECK(a[0] == (groups[i] == 21 ? 1 : 11) && a[1] == 4 && a[2] == (0x80 | groups[i])); - CHECK((unsigned)(a[6] | (unsigned)a[7] << 8 | (unsigned)a[8] << 16) == first + (f - 1) * 4); + unsigned is_double = groups[i] == 21 && f >= 3U; + unsigned first = groups[i] == 22 ? 2001U : is_double ? 1101U : 1001U; + unsigned batch = is_double ? f - 3U : f - 1U; + CHECK(a[0] == (groups[i] == 22 ? 11 : is_double ? 3 : 1) && a[1] == 4 && a[2] == (0x80 | groups[i])); + CHECK((unsigned)(a[6] | (unsigned)a[7] << 8 | (unsigned)a[8] << 16) == first + batch * 4U); + if (is_double) { + unsigned point; + CHECK(lengths[f] == 22U); + for (point = 0U; point < 4U; ++point) { CHECK(a[9U + point * 4U] == 1U); } + } } } CHECK(frames[0][9] == groups[i] && app.session_request == 0); @@ -2099,6 +2108,48 @@ static void test_example_groups(void) } } +/* 站总召和遥信分组均包含双点实时值:首批双点遭发送WAIT时保持游标, + * 下一次尝试重新读取变化后的double_values;最终每个双点恰好交接一次。 */ +static void test_example_double_wait_values(void) +{ + unsigned qualifier; + for (qualifier = 20U; qualifier <= 21U; ++qualifier) { + uint8_t request[sizeof(gi_request)]; + const uint8_t *frames[32]; + size_t lengths[32]; + unsigned step, point; + app_open(); + memcpy(request, gi_request, sizeof(request)); request[9] = (uint8_t)qualifier; + app_request(request, sizeof(request)); + for (step = 0U; step < 32U; ++step) { + CHECK(IEC104_Interrogation_Process(app.interrogation)); + CHECK(IEC104_Interrogation_Get_Status(app.interrogation, &app.gi)); + if (app.gi.phase == IEC104_INTERROGATION_DATA && app.gi.batch_index == 2U) { break; } + CHECK(Example_Drain_Business(&app)); + } + CHECK(step < 32U && app.gi_data.single_index == 8U && app.gi_data.double_index == 0U); + congest(&last_request.connection); + CHECK(Example_Step(&app) && app.business_pending && app.gi.waiting); + CHECK(app.gi_data.double_index == 0U && app.gi.batch_index == 2U && app.batch_bytes[9] == 1U); + app.double_values[0] = app.double_values[3] = app.double_values[7] = 2U; + CHECK(Example_Step(&app) && app.business_pending && app.gi.waiting); + CHECK(app.gi_data.double_index == 0U && app.gi.batch_index == 2U && app.batch_bytes[9] == 2U); + fake.send_blocked = 0; + app_finish(); + CHECK(collect_asdus(frames, lengths) == (qualifier == 20U ? 8U : 6U)); + for (point = 0U; point < 8U; ++point) { + unsigned batch = 3U + point / 4U; + const uint8_t *a = frames[batch]; + const uint8_t *object = a + 6U + (point % 4U) * 4U; + CHECK(lengths[batch] == 22U && a[0] == 3U && a[1] == 4U && a[2] == (0x80U | qualifier)); + CHECK((unsigned)(object[0] | (unsigned)object[1] << 8 | (unsigned)object[2] << 16) == 1101U + point); + CHECK(object[3] == (point == 0U || point == 3U || point == 7U ? 2U : 1U)); + } + CHECK(app.gi.gi_submitted == 1U && app.session_request == 0U && app.gi_station == NULL); + app_close(); + } +} + /* 撤销覆盖所有阶段,包含未读事件、已取事件、HIGH满、已提交部分数据和待发终止。 * 已提交帧保留顺序;尚未提交批次及ACT_TERM不能越过肯定撤销确认。 */ static void test_remote_deactivation_phases(void) @@ -2159,7 +2210,7 @@ static void test_remote_deactivation_phases(void) request[2] = 0x89; CHECK(lengths[prior] == 10 && memcmp(frames[prior], request, 10) == 0); app_request(gi_request, sizeof(gi_request)); app_finish(); - CHECK(app.gi.gi_submitted == 1 && collect_asdus(frames, lengths) == prior + 7); + CHECK(app.gi.gi_submitted == 1 && collect_asdus(frames, lengths) == prior + 9); app_close(); } } @@ -2945,15 +2996,15 @@ static void test_station_binding_wire(void) request[5] = broadcast ? 0xffU : 0x12U; app_request(request, sizeof(request)); app_finish(); - CHECK(collect_asdus(frames, lengths) == 6U); - CHECK(frames[0][2] == 0x87U && frames[5][2] == 0x8aU); - for (i = 0U; i < 6U; ++i) { CHECK(frames[i][4] == 0x34U && frames[i][5] == 0x12U); } + CHECK(collect_asdus(frames, lengths) == 8U); + CHECK(frames[0][2] == 0x87U && frames[7][2] == 0x8aU); + for (i = 0U; i < 8U; ++i) { CHECK(frames[i][4] == 0x34U && frames[i][5] == 0x12U); } request[4] = 7U; request[5] = 0U; app_request(request, sizeof(request)); app_finish(); - CHECK(collect_asdus(frames, lengths) == 7U); - CHECK(frames[6][2] == 0xeeU && frames[6][4] == 7U && frames[6][5] == 0U); + CHECK(collect_asdus(frames, lengths) == 9U); + CHECK(frames[8][2] == 0xeeU && frames[8][4] == 7U && frames[8][5] == 0U); CHECK(IEC104_Slave_Remove_Station(app.slave, &other) == IEC104_STATION_OK); app_close(); } @@ -3337,12 +3388,16 @@ static void test_registry_example_order(void) CHECK(IEC104_Slave_Add_Station(app.slave, &second, 2U, &app) == IEC104_STATION_OK); CHECK(IEC104_Slave_Add_Station(app.slave, &third, 3U, &app) == IEC104_STATION_OK); registry_request(IEC104_CA_BROADCAST, 6U); app_finish(); - CHECK(collect_asdus(frames, lengths) == 18U && app.gi.gi_submitted == 3U); - for (i = 0U; i < 18U; ++i) { - unsigned ca = i < 3U ? i + 1U : i < 15U ? (i - 3U) / 4U + 1U : i - 14U; + CHECK(collect_asdus(frames, lengths) == 24U && app.gi.gi_submitted == 3U); + for (i = 0U; i < 24U; ++i) { + unsigned ca = i < 3U ? i + 1U : i < 21U ? (i - 3U) / 6U + 1U : i - 20U; CHECK(frames[i][4] == ca && frames[i][5] == 0U); - CHECK(frames[i][2] == (i < 3U ? 0x87U : i < 15U ? 0x94U : 0x8aU)); - if (i >= 3U && i < 15U) { CHECK((frames[i][1] & 0x7fU) == 4U); } + CHECK(frames[i][2] == (i < 3U ? 0x87U : i < 21U ? 0x94U : 0x8aU)); + if (i >= 3U && i < 21U) { + unsigned batch = (i - 3U) % 6U; + CHECK((frames[i][1] & 0x7fU) == 4U); + CHECK(frames[i][0] == (batch < 2U ? 1U : batch < 4U ? 3U : 11U)); + } } CHECK(app.gi_station == NULL && app.session_request == 0U); CHECK(IEC104_Slave_Remove_Station(app.slave, &second) == IEC104_STATION_OK); @@ -3576,7 +3631,7 @@ static void test_registry_reject_during_deactivation(void) if (mode == 1U) { registry_request(IEC104_CA_BROADCAST, 8U); } (void)gi_take(IEC104_INTERROGATION_FINISHED); (void)gi_take(IEC104_INTERROGATION_FINISHED); app_finish(); - CHECK(collect_asdus(frames, lengths) == (mode == 0U ? 9U : 3U)); + CHECK(collect_asdus(frames, lengths) == (mode == 0U ? 11U : 3U)); CHECK(frames[0][2] == 0x87U && frames[0][4] == 1U); CHECK(frames[1][2] == 0xc7U && frames[1][4] == 2U); CHECK(frames[2][2] == 0x89U && frames[2][4] == 1U); @@ -3853,7 +3908,7 @@ static void test_counter_busy_independent(void) CHECK(app.gi.gi_submitted == 1); CHECK(IEC104_Interrogation_Reply_Begin(app.counter, &event, true) == IEC104_INTERROGATION_REPLY_OK); ci_process(); event = ci_take(IEC104_INTERROGATION_NEED_DATA); ci_done(&event); - CHECK(collect_asdus(frames, lengths) == 9); ci_close(); + CHECK(collect_asdus(frames, lengths) == 11); ci_close(); { IEC104_InterrogationConfig_t config; unsigned step, acked = 0, first_counter = 32, last_general = 0, i; IEC104_Interrogation_Config_Default(&config); config.max_objects_per_asdu = 1; @@ -3865,10 +3920,10 @@ static void test_counter_busy_independent(void) if (sent != acked) { push_ack(sent); acked = sent; app_poll(); } if (app.gi.pending_requests == 0 && app.ci.pending_requests == 0) { break; } } - CHECK(step < 128 && acked == 22 && app.counter_completed == 1 && app.gi.gi_submitted == 1); + CHECK(step < 128 && acked == 30 && app.counter_completed == 1 && app.gi.gi_submitted == 1); for (i = 0; i < acked; ++i) { if (frames[i][0] == 15 && first_counter == 32) { first_counter = i; } - if (frames[i][0] == 1 || frames[i][0] == 11) { last_general = i; } + if (frames[i][0] == 1 || frames[i][0] == 3 || frames[i][0] == 11) { last_general = i; } } CHECK(first_counter < last_general); ci_close(); custom_config = NULL; custom_window = 32; } @@ -4846,6 +4901,7 @@ int main(void) test_cancel_before_begin(); test_group_interrogation(); test_example_groups(); + test_example_double_wait_values(); test_remote_deactivation_phases(); test_remote_deactivation_validation(); test_remote_deactivation_full_queue(); @@ -4915,6 +4971,6 @@ int main(void) test_clock_broadcast(); test_clock_broadcast_mutation(); test_clock_example_integration(); - puts("slave example + event interrogation tests: 130 groups passed (98 general + 12 counter + 4 fallback + 16 clock)"); + puts("slave example + event interrogation tests: 131 groups passed (99 general + 12 counter + 4 fallback + 16 clock)"); return 0; } diff --git a/IEC60870-Slave/test/slave_example_loopback_test.cjs b/IEC60870-Slave/test/slave_example_loopback_test.cjs index 8706fd56..9d099655 100644 --- a/IEC60870-Slave/test/slave_example_loopback_test.cjs +++ b/IEC60870-Slave/test/slave_example_loopback_test.cjs @@ -104,8 +104,36 @@ class Master { } 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); +} 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]; @@ -155,7 +183,10 @@ async function totalCall(master, oa, fragmented = false, qoi = 20, ca = 7) { async function readTotalCall(master, oa, qoi = 20, busyRequests = []) { const original = request(oa, qoi); - const count = qoi === 20 ? 6 : 4; + // 站总召依次返回单点、双点、遥测;遥信分组含前两类,遥测分组只含最后一类。 + // 每类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; } @@ -175,17 +206,20 @@ async function readTotalCall(master, oa, qoi = 20, busyRequests = []) { expected[2] = f === 0 ? 0x87 : 0x8a; assert.deepEqual([...asdu], expected); } else { - const digital = qoi === 20 ? f < 3 : qoi === 21; + 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 = (qoi === 20 && !digital ? f - 3 : f - 1) * 4; + const first = ((f - 1) % 2) * 4; assert.equal(asdu.length, 6 + width * 4); - assert.deepEqual([...asdu.subarray(0, 6)], [digital ? 1 : 11, 4, 0x80 | qoi, oa, 7, 0]); + 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), (digital ? 1001 : 2001) + first + p); - if (digital) assert.equal(asdu[offset + 3], signals[first + p]); + 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), measures[first + p]); + assert.equal(asdu.readInt16LE(offset + 3), values[first + p]); assert.equal(asdu[offset + 5], 0); } } @@ -200,20 +234,22 @@ async function main() { 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')); 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', '8000'], + 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(); }, 15000); + 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); @@ -263,6 +299,28 @@ async function main() { 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; + // 撤销故意使用不同状态值:匹配只要求类型/站点/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;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); @@ -318,11 +376,11 @@ async function main() { // 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 < 10; index++) { + 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, 6); + 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); @@ -332,13 +390,15 @@ async function main() { await next.close(); const [code] = await exit; assert.equal(code, 0, `${output}\n${errors}`); - assert.match(output, /STOP submitted=65 gi_submitted=11 /); + 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/); + // 三次选择含一次成功撤销;只有前两次启动输出并产生各一帧返回信息。 + assert.match(output, /command_selected=3 command_executed=2 command_completed=2 command_submitted=\d+ command_returns=2/); assert.equal(errors, ''); - console.log('Slave example loopback: passed (real TCP, batched 106+102 and 103+102, simulated clock/TEST/COT/broadcast, GI and counter groups/broadcast, freeze/reset/TEST, independent k=1 progress, busy/deactivation/errors, STOP/START, reconnect, cleanup)'); + console.log('Slave example loopback: passed (real TCP, Type45/46 select/execute/return/termination, command cancel/TEST/address/unsupported types, changed single/double 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();