添加遥控业务代码
This commit is contained in:
@@ -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; });
|
||||
Reference in New Issue
Block a user