添加遥控业务代码
CI / L0 静态门禁 (pre-commit) (push) Failing after 2m51s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 18s

This commit is contained in:
STF
2026-10-30 20:03:19 -07:00
parent 7596f3371a
commit cd42b4d61d
126 changed files with 70586 additions and 149 deletions
@@ -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 <string.h>
#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;
}
@@ -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 */
@@ -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`);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 <windows.h>
#include <errno.h>
#include <signal.h>
/* 信号处理器和主循环之间只交换停止标志;静态初始化为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
@@ -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 <example.exe>');
class Master {
constructor(socket) {
this.socket = socket;
this.buffer = Buffer.alloc(0);
this.frames = [];
this.waiters = [];
this.ns = this.nr = 0;
socket.on('data', data => {
this.buffer = Buffer.concat([this.buffer, data]);
try {
while (this.buffer.length >= 2) {
assert.equal(this.buffer[0], 0x68);
assert(this.buffer[1] >= 4 && this.buffer[1] <= 253);
const length = this.buffer[1] + 2;
if (this.buffer.length < length) break;
const frame = this.buffer.subarray(0, length);
this.buffer = this.buffer.subarray(length);
const waiter = this.waiters.shift();
if (waiter) { clearTimeout(waiter.timer); waiter.resolve(frame); }
else this.frames.push(frame);
}
} catch (error) { this.fail(error); socket.destroy(); }
});
socket.on('error', error => this.fail(error));
socket.on('close', () => this.fail(new Error('Connection closed')));
}
fail(error) {
this.error = error;
for (const waiter of this.waiters.splice(0)) {
clearTimeout(waiter.timer);
waiter.reject(error);
}
}
next() {
if (this.frames.length) return Promise.resolve(this.frames.shift());
if (this.error) return Promise.reject(this.error);
return new Promise((resolve, reject) => {
const waiter = { resolve, reject };
waiter.timer = setTimeout(() => {
this.waiters.splice(this.waiters.indexOf(waiter), 1);
reject(new Error('Timed out waiting for an APDU'));
}, 1500);
this.waiters.push(waiter);
});
}
async control(code, expected) {
this.socket.write(Buffer.from([0x68, 4, code, 0, 0, 0]));
for (let i = 0; i < 8; i++) {
const frame = await this.next();
if (frame[2] === 1) continue; // 主站I帧的S确认允许穿插。
assert.deepEqual([...frame], [0x68, 4, expected, 0, 0, 0]);
return;
}
throw new Error('Missing U-frame confirmation');
}
frame(payload) {
const frame = Buffer.alloc(payload.length + 6);
frame[0] = 0x68;
frame[1] = payload.length + 4;
frame.writeUInt16LE(this.ns++ << 1, 2);
frame.writeUInt16LE(this.nr << 1, 4);
Buffer.from(payload).copy(frame, 6);
return frame;
}
async send(payload, fragmented = false) {
const frame = this.frame(payload);
if (fragmented) {
this.socket.write(frame.subarray(0, 3));
await delay(10);
this.socket.write(frame.subarray(3));
} else this.socket.write(frame);
}
async readASDU() {
for (let i = 0; i < 8; i++) {
const frame = await this.next();
if (frame[2] === 1) continue;
assert.equal(frame[2] & 1, 0);
assert.equal(frame.readUInt16LE(2) >> 1, this.nr++);
assert.equal(frame.readUInt16LE(4) >> 1, this.ns);
const ack = Buffer.from([0x68, 4, 1, 0, 0, 0]);
ack.writeUInt16LE(this.nr << 1, 4);
this.socket.write(ack); // k=1,每帧ACK后服务才可继续发下一帧。
return frame.subarray(6);
}
throw new Error('Missing I frame');
}
async close() {
const closed = once(this.socket, 'close');
this.socket.end();
await closed;
}
}
const signals = [0, 1, 0, 1, 1, 0, 1, 0];
const 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; });