1037 lines
57 KiB
C
1037 lines
57 KiB
C
/*
|
||
* 事件式总召状态机。Slave负责通信,应用通过BEGIN/NEED_DATA/FINISHED交互。
|
||
* 请求任务固定地址保存含自引用的SlaveEvent;业务事件为纯值,可以移动和复制。
|
||
* 每个交付BEGIN预留一个FINISHED槽,断线/STOP/取消/撤销/超时仍能通知应用清理资源。
|
||
* 所有应答沿保存请求的connection走HIGH,避免跨连接重放和确认/数据/终止乱序。
|
||
*
|
||
* 正常阶段:START生成BEGIN并等待应用接受 -> CONFIRM提交激活确认 -> DATA反复索取并提交批次
|
||
* -> TERMINATE提交激活终止 -> 释放任务并交付FINISHED。
|
||
* REPLY表示只需提交一条确认或错误应答的任务,提交后直接释放,不再索取数据。
|
||
* Handle_Event更新会话和接收请求,条件允许时立即提交短应答;Process推进保存的任务。
|
||
* NextEvent把业务事件交给应用;一个实例至多一个未终结总召,忙时否定所有新激活。
|
||
* Reply_Begin/Submit_Batch/Complete_Data只接收应用决定,实际发帧由后续Process推进。
|
||
* 事件来源和会话可用性统一由Slave接口校验,本模块只维护总召自己的阶段和业务期限。
|
||
* 单帧应答与总召分开选择,双方可推进时轮转;总召等待应用不阻塞单帧应答。
|
||
* Type100解析后确定应答CA,建立内部请求副本时写入;发送及重试不再转换地址。
|
||
*/
|
||
#include "IEC104_Interrogation.h"
|
||
#include "IEC104_TcpServer_Config.h"
|
||
#include "IEC104_Net.h"
|
||
#include <string.h>
|
||
|
||
/* 三类存储均为静态有界容量:实例池、每实例协议任务池、每实例业务事件FIFO。
|
||
* 业务事件至少需要两个位置,才能同时容纳BEGIN及为该请求预留的FINISHED。 */
|
||
#ifndef IEC104_INTERROGATION_CAPACITY
|
||
#define IEC104_INTERROGATION_CAPACITY IEC104_TCP_SERVER_CAPACITY
|
||
#endif
|
||
#ifndef IEC104_INTERROGATION_TASK_CAPACITY
|
||
#define IEC104_INTERROGATION_TASK_CAPACITY 4U
|
||
#endif
|
||
#ifndef IEC104_INTERROGATION_EVENT_CAPACITY
|
||
#define IEC104_INTERROGATION_EVENT_CAPACITY 4U
|
||
#endif
|
||
_Static_assert(IEC104_INTERROGATION_CAPACITY > 0U, "Interrogation needs instance storage");
|
||
_Static_assert(IEC104_INTERROGATION_TASK_CAPACITY > 0U, "Interrogation needs request storage");
|
||
_Static_assert(IEC104_INTERROGATION_EVENT_CAPACITY >= 2U, "BEGIN needs a reserved FINISHED slot");
|
||
|
||
/* 一项协议任务独占请求副本和当前数据批次。request与outgoing含有指向本项存储的指针,
|
||
* 因此仅移动顺序索引,不能用结构赋值或memmove整理任务数组。 */
|
||
typedef struct {
|
||
IEC104_SlaveEvent_t request; /* 独立请求副本及原连接,复制时已设置应答CA;不移动任务地址。 */
|
||
IEC60870_Asdu_t outgoing; /* 仅借用本任务bytes,不借用应用的批次存储。 */
|
||
uint8_t bytes[IEC104_MAX_ASDU_LEN]; /* 当前批次的独立原文,发送等待期间保持有效。 */
|
||
IEC104_InterrogationRequest_t info; /* 唯一业务元数据;供事件交付及批次校验,终结时仍描述原激活。 */
|
||
uint64_t request_id; /* 激活任务编号;未建立总召事务的单帧应答为0。 */
|
||
uint64_t started_ms, application_since_ms; /* 单调毫秒:BEGIN生成、当前工作事件入队。 */
|
||
size_t batch_index, objects_submitted, prepared_count; /* 已提交批次数、对象总数、当前缓存批次对象数。 */
|
||
IEC104_InterrogationPhase_t phase; /* 决定下一步等待应用、提交哪种ASDU或释放任务。 */
|
||
uint8_t reply_cause; /* 非零为单帧确认/错误应答,肯定撤销确认也使用此路径。 */
|
||
bool reply_negative; /* REPLY阶段发送的P/N标志;肯定撤销确认为false。 */
|
||
bool prepared; /* DATA阶段已有完整批次;为true时不再生成新的NEED_DATA。 */
|
||
bool event_outstanding, event_delivered; /* 当前交互待应答、该工作事件已被应用取出。 */
|
||
bool begin_delivered, finish_reserved; /* 应用已取过BEGIN、为本任务保留了一个终结事件位置。 */
|
||
bool occupied; /* 固定任务槽正在使用;任务可非队首完成,释放后立即复用此槽。 */
|
||
} InterrogationTask;
|
||
|
||
struct IEC104_Interrogation {
|
||
IEC104_Slave_t *slave; /* NULL为空闲槽;有效期间Slave必须存活。 */
|
||
IEC104_InterrogationConfig_t config; /* Create复制的配置,不借用调用方配置对象。 */
|
||
IEC104_InterrogationStatus_t status; /* 会话状态和累计统计;即时进度由Get_Status补齐。 */
|
||
IEC104_SlaveEventIdentity_t session; /* Slave校验后交付的服务身份和已处理的连接代次。 */
|
||
uint64_t instance_id, next_request_id;
|
||
/* instance_id区分总召静态槽的历次使用;next_request_id在本实例内递增,
|
||
* 两者共同标识一次总召,独立于Slave的服务实例和连接身份。 */
|
||
unsigned count, event_count, reserved_finishes;
|
||
/* count描述有效任务数;event_count是实际业务事件数;reserved_finishes仅为容量预留。
|
||
* 始终保证event_count + reserved_finishes不超过业务事件容量。 */
|
||
unsigned order[IEC104_INTERROGATION_TASK_CAPACITY]; /* 有效任务槽下标,保持应答形成的先后顺序。 */
|
||
bool prefer_interrogation; /* 单帧提交后给可推进总召一次机会,跨Process返回保留。 */
|
||
InterrogationTask tasks[IEC104_INTERROGATION_TASK_CAPACITY]; /* 固定地址的协议任务池。 */
|
||
IEC104_InterrogationEvent_t events[IEC104_INTERROGATION_EVENT_CAPACITY]; /* 可移动的纯值事件FIFO。 */
|
||
};
|
||
static IEC104_Interrogation_t interrogation_pool[IEC104_INTERROGATION_CAPACITY];
|
||
/* 不随槽释放清零,避免旧事件误命中新实例;达到上限拒绝创建,不回绕。 */
|
||
static uint64_t instance_serial;
|
||
|
||
/* 检查句柄是否指向仍被占用的实例槽;先比较地址再解引用,NULL和池外指针返回false。
|
||
* 这里只验证实例占用,Slave的寿命仍由调用方保证。 */
|
||
static bool Owned(const IEC104_Interrogation_t *self)
|
||
{
|
||
size_t i;
|
||
for (i = 0U; i < IEC104_INTERROGATION_CAPACITY; ++i) {
|
||
if (self == &interrogation_pool[i]) { return self->slave != NULL; }
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/* 取得固定任务槽并追加顺序索引;调用方完成请求复制,失败时须立即Remove_Task。
|
||
* 不移动已有任务,确保它们的SlaveEvent和ASDU视图继续指向原存储。 */
|
||
static InterrogationTask *Allocate_Task(IEC104_Interrogation_t *self)
|
||
{
|
||
unsigned slot;
|
||
if (self->count == IEC104_INTERROGATION_TASK_CAPACITY) { return NULL; }
|
||
for (slot = 0U; slot < IEC104_INTERROGATION_TASK_CAPACITY; ++slot) {
|
||
InterrogationTask *task = &self->tasks[slot];
|
||
if (!task->occupied) {
|
||
memset(task, 0, sizeof(*task));
|
||
task->occupied = true;
|
||
self->order[self->count++] = slot;
|
||
return task;
|
||
}
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
/* 释放一个已终结或复制失败的任务;仅压紧索引,非队首完成也立即归还容量。
|
||
* 有应用交互的任务须先Finish,避免丢失预留的FINISHED。 */
|
||
static void Remove_Task(IEC104_Interrogation_t *self, InterrogationTask *task)
|
||
{
|
||
unsigned i;
|
||
for (i = 0U; i < self->count; ++i) {
|
||
if (&self->tasks[self->order[i]] == task) {
|
||
memset(task, 0, sizeof(*task));
|
||
memmove(self->order + i, self->order + i + 1U,
|
||
(self->count - i - 1U) * sizeof(self->order[0]));
|
||
self->order[--self->count] = 0U;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 原总召转为拒绝/撤销确认时,复用原槽并按应答形成时刻放到顺序末尾。
|
||
* 满池也能转换;不让这条新应答越过已经等待的其他单帧应答。 */
|
||
static void Move_Reply_To_Tail(IEC104_Interrogation_t *self, const InterrogationTask *task)
|
||
{
|
||
unsigned i;
|
||
for (i = 0U; i < self->count; ++i) {
|
||
unsigned slot = self->order[i];
|
||
if (&self->tasks[slot] == task) {
|
||
memmove(self->order + i, self->order + i + 1U,
|
||
(self->count - i - 1U) * sizeof(self->order[0]));
|
||
self->order[self->count - 1U] = slot;
|
||
self->prefer_interrogation = false;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 返回唯一未终结总召的固定槽下标;request_id为0查当前总召,非0核对指定请求。
|
||
* 无匹配返回TASK_CAPACITY;REPLY即使保留原request_id,也已退出应用交互。 */
|
||
static unsigned Find_Interrogation(const IEC104_Interrogation_t *self, uint64_t request_id)
|
||
{
|
||
unsigned i;
|
||
for (i = 0U; i < self->count; ++i) {
|
||
unsigned slot = self->order[i];
|
||
const InterrogationTask *task = &self->tasks[slot];
|
||
if (task->request_id != 0U && task->phase != IEC104_INTERROGATION_REPLY &&
|
||
(request_id == 0U || task->request_id == request_id)) { return slot; }
|
||
}
|
||
return IEC104_INTERROGATION_TASK_CAPACITY;
|
||
}
|
||
|
||
/* 最早形成的单帧应答;立即提交也必须遵守此顺序,不能越过旧REPLY。 */
|
||
static unsigned Find_Reply(const IEC104_Interrogation_t *self)
|
||
{
|
||
unsigned i;
|
||
for (i = 0U; i < self->count; ++i) {
|
||
unsigned slot = self->order[i];
|
||
if (self->tasks[slot].phase == IEC104_INTERROGATION_REPLY) { return slot; }
|
||
}
|
||
return IEC104_INTERROGATION_TASK_CAPACITY;
|
||
}
|
||
|
||
/* 业务推进无需等待应用或事件空位时为true;发送容量仍由实际提交结果决定。 */
|
||
static bool Interrogation_Can_Progress(const IEC104_Interrogation_t *self, const InterrogationTask *task)
|
||
{
|
||
if (task->phase == IEC104_INTERROGATION_START ||
|
||
(task->phase == IEC104_INTERROGATION_DATA && !task->prepared)) {
|
||
unsigned needed = task->phase == IEC104_INTERROGATION_DATA ? 1U : 2U;
|
||
return !task->event_outstanding &&
|
||
self->event_count + self->reserved_finishes + needed <= IEC104_INTERROGATION_EVENT_CAPACITY;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/* 单帧保持FIFO;双方可推进时先单帧再总召,轮转位置跨调用保存。
|
||
* 总召等应用或业务FIFO空位时只选单帧;均不能推进时返回NULL,让Process立即返回。 */
|
||
static InterrogationTask *Select_Task(IEC104_Interrogation_t *self)
|
||
{
|
||
unsigned slot = Find_Interrogation(self, 0U), reply_slot = Find_Reply(self);
|
||
InterrogationTask *gi = slot != IEC104_INTERROGATION_TASK_CAPACITY ? &self->tasks[slot] : NULL;
|
||
InterrogationTask *reply = reply_slot != IEC104_INTERROGATION_TASK_CAPACITY ? &self->tasks[reply_slot] : NULL;
|
||
if (gi != NULL && !Interrogation_Can_Progress(self, gi)) { gi = NULL; }
|
||
if (reply != NULL && (!self->prefer_interrogation || gi == NULL)) { return reply; }
|
||
return gi != NULL ? gi : reply;
|
||
}
|
||
|
||
/* 将任务身份、请求元数据和当前进度复制成一条业务事件;本函数不执行应用代码。
|
||
* type决定应用需要接受、交付数据还是清理;reason仅在FINISHED时有业务意义。
|
||
* 调用方须已检查容量,或已将本任务的终结预留释放为可写位置。 */
|
||
static void Push_Event(IEC104_Interrogation_t *self, const InterrogationTask *task,
|
||
IEC104_InterrogationEventType_t type, IEC104_InterrogationFinishReason_t reason)
|
||
{
|
||
IEC104_InterrogationEvent_t *event = &self->events[self->event_count++];
|
||
memset(event, 0, sizeof(*event));
|
||
event->type = type;
|
||
event->instance_id = self->instance_id;
|
||
event->request_id = task->request_id;
|
||
event->request = task->info;
|
||
event->batch_index = task->batch_index;
|
||
event->objects_submitted = task->objects_submitted;
|
||
event->reason = reason;
|
||
}
|
||
|
||
/* 结束一个总召的应用交互,reason保存为该请求的终结原因;不自行释放协议任务。
|
||
* 移除本请求尚未取出的BEGIN/NEED_DATA,保留其他事件和所有历史FINISHED。
|
||
* BEGIN已取出时,用预留位置交付一次FINISHED;BEGIN未取出时只撤销事件和预留,
|
||
* 因为应用尚未获得请求,无需通知它清理资源。finish_reserved同时防止重复终结。 */
|
||
static void Finish(IEC104_Interrogation_t *self, InterrogationTask *task,
|
||
IEC104_InterrogationFinishReason_t reason)
|
||
{
|
||
unsigned read, write = 0U;
|
||
if (task->request_id != 0U && task->phase != IEC104_INTERROGATION_REPLY) {
|
||
self->status.last_finish_reason = reason;
|
||
}
|
||
if (!task->finish_reserved) { return; }
|
||
for (read = 0U; read < self->event_count; ++read) {
|
||
const IEC104_InterrogationEvent_t *event = &self->events[read];
|
||
if (event->request_id == task->request_id && event->type != IEC104_INTERROGATION_FINISHED) { continue; }
|
||
self->events[write++] = *event;
|
||
}
|
||
self->event_count = write;
|
||
--self->reserved_finishes;
|
||
task->finish_reserved = false;
|
||
task->event_outstanding = task->event_delivered = false;
|
||
if (task->begin_delivered) {
|
||
Push_Event(self, task, IEC104_INTERROGATION_FINISHED, reason);
|
||
task->begin_delivered = false;
|
||
}
|
||
}
|
||
|
||
/* 肯定ACT_CON尚未被发送层接手时,结束本次业务并复用原槽保存否定确认。
|
||
* 应用主动拒绝、BEGIN失败以及START/CONFIRM超时共用此路径,保留各自终结原因。
|
||
* Finish清除未读工作事件并兑现终结预留;不取消其他任务、不额外占槽、不关闭连接。
|
||
* 新形成的否定确认排在已有REPLY之后;后续Process提交,WAIT时保留重试。 */
|
||
static void Reject_Request(IEC104_Interrogation_t *self, InterrogationTask *task,
|
||
IEC104_InterrogationFinishReason_t reason)
|
||
{
|
||
++self->status.rejected;
|
||
Finish(self, task, reason);
|
||
task->reply_cause = IEC60870_ASDU_COT_ACTIVATION_CON;
|
||
task->reply_negative = true;
|
||
task->phase = IEC104_INTERROGATION_REPLY;
|
||
self->status.waiting = false;
|
||
Move_Reply_To_Tail(self, task);
|
||
}
|
||
|
||
/* 按reason结束本实例全部任务,再清空任务池和总召占用状态;不关闭Slave或连接。
|
||
* Slave已复制接手的帧不撤销,终结业务事件保留到应用读取。
|
||
* cancelled按被移除的协议任务数累计,包含排队的单帧应答,并非仅统计已启动总召。 */
|
||
static void Cancel(IEC104_Interrogation_t *self, IEC104_InterrogationFinishReason_t reason)
|
||
{
|
||
unsigned i;
|
||
for (i = 0U; i < self->count; ++i) {
|
||
Finish(self, &self->tasks[self->order[i]], reason);
|
||
}
|
||
self->status.cancelled += self->count;
|
||
memset(self->tasks, 0, sizeof(self->tasks));
|
||
memset(self->order, 0, sizeof(self->order));
|
||
self->count = 0U;
|
||
self->prefer_interrogation = false;
|
||
self->status.waiting = false;
|
||
}
|
||
|
||
/* 严重请求错误或肯定确认提交后的执行失败,关闭event所属连接并按reason取消全部任务。
|
||
* event可能位于任务中,必须先关闭原连接再清任务;旧代次不会关闭新连接。
|
||
* true包括成功关闭、原连接已断开和代次已过期;false表示底层关闭失败,任务仍已取消。 */
|
||
static bool Reject_Connection(IEC104_Interrogation_t *self, const IEC104_SlaveEvent_t *event,
|
||
IEC104_InterrogationFinishReason_t reason)
|
||
{
|
||
IEC104_TcpResult_t result = IEC104_Slave_Close_Connection(self->slave, event, NULL);
|
||
Cancel(self, reason);
|
||
self->status.active = false;
|
||
return result == IEC104_TCP_OK || result == IEC104_TCP_NOT_CONNECTED ||
|
||
result == IEC104_TCP_STALE_GENERATION;
|
||
}
|
||
|
||
/* 按肯定确认的提交边界处理业务失败:START/CONFIRM仍可否定激活并保留连接;
|
||
* CONFIRM只有在ACT_CON被发送层接手后才转为DATA,即使仅在HIGH中排队也已越过边界。
|
||
* DATA/TERMINATE失败沿用关闭原连接的策略,不补发成功ACT_TERM。
|
||
* 调用方已校验会话与任务;true表示失败已处理,并不表示否定确认已经提交。 */
|
||
static bool Fail_Interrogation(IEC104_Interrogation_t *self, InterrogationTask *task,
|
||
IEC104_InterrogationFinishReason_t reason)
|
||
{
|
||
if (task->phase == IEC104_INTERROGATION_START || task->phase == IEC104_INTERROGATION_CONFIRM) {
|
||
Reject_Request(self, task, reason);
|
||
return true;
|
||
}
|
||
return Reject_Connection(self, &task->request, reason);
|
||
}
|
||
|
||
/* 立即提交短应答的结果。DEFER表示尚未接手,调用方保存原请求,或在满槽时丢弃并计数。
|
||
* 只使用现有HIGH接口,不越过Slave通知或旧REPLY,不改变总召数据的发送顺序。 */
|
||
typedef enum { REPLY_DEFER, REPLY_SENT, REPLY_FAILED } ImmediateReplyResult;
|
||
|
||
/* 本实例只服务一个实际站地址;广播CA映射到本站,其他地址保持原值。
|
||
* Request解析Type100后计算一次,地址校验、应答副本和撤销匹配使用同一结果。
|
||
* 错误站地址必须保留原值,不能因构造应答而把未知站误认为本站。 */
|
||
static uint16_t Resolve_Common_Address(const IEC104_Interrogation_t *self, uint16_t address)
|
||
{
|
||
return address == IEC104_CA_BROADCAST ? self->config.common_address : address;
|
||
}
|
||
|
||
/* 为立即应答、新任务或匹配撤销建立内部副本,不修改调用方的源事件。
|
||
* source是已完整校验布局的Type100;response_ca由Request根据原CA确定,可保留错误CA。
|
||
* Copy_Event先重建自引用并保留原服务/连接代次,再由Asdu层按字段宽度写入应答CA。
|
||
* 每份副本只在此准备地址,后续COT/P/N修改及发送WAIT重试均沿用该CA,无需额外缓存头部。
|
||
* 返回false表示复制或设置字段失败,调用方不得提交该副本。 */
|
||
static bool Copy_Response_Request(IEC104_SlaveEvent_t *destination,
|
||
const IEC104_SlaveEvent_t *source, uint16_t response_ca)
|
||
{
|
||
return IEC104_Slave_Copy_Event(destination, source) &&
|
||
IEC60870_Asdu_Set_CA(&destination->asdu, response_ca) == IEC60870_ASDU_OK;
|
||
}
|
||
|
||
static ImmediateReplyResult Try_Immediate_Reply(IEC104_Interrogation_t *self,
|
||
const IEC104_SlaveEvent_t *event, uint16_t response_ca,
|
||
uint8_t cause, bool negative, const InterrogationTask *queued)
|
||
{
|
||
IEC104_SlaveEvent_t copy;
|
||
IEC104_SlaveRequestState_t state;
|
||
unsigned first = Find_Reply(self);
|
||
if (first != IEC104_INTERROGATION_TASK_CAPACITY && &self->tasks[first] != queued) { return REPLY_DEFER; }
|
||
state = IEC104_Slave_Check_Request(self->slave, &self->session);
|
||
self->status.events_pending = state == IEC104_SLAVE_REQUEST_WAIT_EVENTS;
|
||
if (self->status.events_pending) { return REPLY_DEFER; }
|
||
if (state == IEC104_SLAVE_REQUEST_ERROR || !self->status.active || state != IEC104_SLAVE_REQUEST_READY) {
|
||
Cancel(self, state == IEC104_SLAVE_REQUEST_ERROR ? IEC104_INTERROGATION_FINISH_SERVICE_ERROR :
|
||
IEC104_INTERROGATION_FINISH_CONNECTION_CHANGED);
|
||
self->status.active = false;
|
||
return REPLY_FAILED;
|
||
}
|
||
/* 调用方event为只读且可能指向固定任务槽;副本单独重建自引用并准备应答CA。
|
||
* OK后发送层已复制;WAIT后本地副本可释放,保存任务时继续使用已确定的response_ca。 */
|
||
if (!Copy_Response_Request(©, event, response_ca) ||
|
||
IEC60870_Asdu_Set_COT(©.asdu, cause) != IEC60870_ASDU_OK ||
|
||
IEC60870_Asdu_Set_Negative(©.asdu, negative) != IEC60870_ASDU_OK) { return REPLY_FAILED; }
|
||
if (IEC60870_MasterConnection_Send_ASDU(©.connection, ©.asdu)) {
|
||
++self->status.submitted;
|
||
if (negative) { ++self->status.negative_submitted; }
|
||
self->prefer_interrogation = true;
|
||
return REPLY_SENT;
|
||
}
|
||
if (IEC104_Slave_Get_Last_Submit_Result(©.connection) == IEC104_TCP_SUBMIT_WAIT) {
|
||
++self->status.waits;
|
||
self->status.waiting = true;
|
||
return REPLY_DEFER;
|
||
}
|
||
Cancel(self, IEC104_INTERROGATION_FINISH_SEND_ERROR);
|
||
self->status.active = false;
|
||
return REPLY_FAILED;
|
||
}
|
||
|
||
/* 将一项期限并入当前最近期限:now/since为单调毫秒,duration为允许时长。
|
||
* remaining/reason是累积输入输出;reason为NONE表示此前还没有有效候选。
|
||
* 用经过时间比较,避免绝对期限相加溢出;0时长禁用,到期剩余值为0。
|
||
* 相同剩余时间保留先检查的原因,因此Task_Timeout先检查总期限以确定并发超时的归因。 */
|
||
static void Limit_Timeout(uint64_t now, uint64_t since, uint32_t duration,
|
||
IEC104_InterrogationFinishReason_t candidate, uint32_t *remaining,
|
||
IEC104_InterrogationFinishReason_t *reason)
|
||
{
|
||
uint64_t elapsed;
|
||
uint32_t left;
|
||
if (duration == 0U) { return; }
|
||
elapsed = now - since;
|
||
left = elapsed >= duration ? 0U : duration - (uint32_t)elapsed;
|
||
if (*reason == IEC104_INTERROGATION_FINISH_NONE || left < *remaining) {
|
||
*remaining = left;
|
||
*reason = candidate;
|
||
}
|
||
}
|
||
|
||
/* 只读计算task在now时刻最近的期限,写remaining并返回原因;NONE表示没有启用的期限。
|
||
* 只有尚未终结的激活业务受定时器约束,单帧应答不参加业务超时计算。
|
||
* START尚未生成BEGIN时不计业务期限;生成后启用执行总期限,覆盖取数及发送等待。
|
||
* BEGIN/NEED_DATA期限从工作事件入队起计算,包含应用尚未取出事件的时间。
|
||
* 本函数不触发超时动作,供Expire执行处理和Get_Status计算唤醒时间共用。 */
|
||
static IEC104_InterrogationFinishReason_t Task_Timeout(const IEC104_Interrogation_t *self,
|
||
const InterrogationTask *task, uint64_t now, uint32_t *remaining)
|
||
{
|
||
IEC104_InterrogationFinishReason_t reason = IEC104_INTERROGATION_FINISH_NONE;
|
||
*remaining = 0U;
|
||
if (task->request_id == 0U || task->phase == IEC104_INTERROGATION_REPLY) { return reason; }
|
||
if (task->phase == IEC104_INTERROGATION_START && !task->event_outstanding) { return reason; }
|
||
Limit_Timeout(now, task->started_ms, self->config.total_timeout_ms,
|
||
IEC104_INTERROGATION_FINISH_TOTAL_TIMEOUT, remaining, &reason);
|
||
if (task->event_outstanding) {
|
||
bool begin = task->phase == IEC104_INTERROGATION_START;
|
||
Limit_Timeout(now, task->application_since_ms,
|
||
begin ? self->config.begin_timeout_ms : self->config.data_timeout_ms,
|
||
begin ? IEC104_INTERROGATION_FINISH_BEGIN_TIMEOUT : IEC104_INTERROGATION_FINISH_DATA_TIMEOUT,
|
||
remaining, &reason);
|
||
}
|
||
return reason;
|
||
}
|
||
|
||
/* 只检查唯一未终结总召;确认前超时转为否定REPLY,确认后超时关闭原连接并取消。
|
||
* 已交付BEGIN的FINISHED必有预留槽,迟到应答随即失效;REPLY不再计算业务期限。
|
||
* 调用方须先确认Slave通知已读空,使先到的STOP/断线/撤销先于超时处理。
|
||
* 返回0为未超时,1为已处理超时,-1为任务已取消但底层关闭失败。 */
|
||
static int Expire(IEC104_Interrogation_t *self)
|
||
{
|
||
unsigned slot = Find_Interrogation(self, 0U);
|
||
InterrogationTask *task;
|
||
uint32_t remaining;
|
||
IEC104_InterrogationFinishReason_t reason;
|
||
if (slot == IEC104_INTERROGATION_TASK_CAPACITY) { return 0; }
|
||
task = &self->tasks[slot];
|
||
reason = Task_Timeout(self, task, IEC104_Net_Now_Ms(), &remaining);
|
||
if (reason != IEC104_INTERROGATION_FINISH_NONE && remaining == 0U) {
|
||
++self->status.timeouts;
|
||
return Fail_Interrogation(self, task, reason) ? 1 : -1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* 查找唯一未终结总召;它可能等待BEGIN或排在错误应答后面,不能只看任务队首。
|
||
* event/info/qualifier来自已校验的撤销请求,response_ca已解析为实际站CA;
|
||
* 原连接代次、实际站CA、OA、TEST和QOI须全部相同。
|
||
* 广播与定向地址解析到同一本站,允许广播激活后定向撤销,也允许广播撤销本站任务。
|
||
* 返回内部任务指针供Request原位改为撤销确认;NULL表示没有匹配的未终结激活任务。 */
|
||
static InterrogationTask *Find_Activation(IEC104_Interrogation_t *self,
|
||
const IEC104_SlaveEvent_t *event, const IEC60870_AsduInfo_t *info,
|
||
uint16_t response_ca, uint8_t qualifier)
|
||
{
|
||
unsigned i;
|
||
for (i = 0U; i < self->count; ++i) {
|
||
InterrogationTask *task = &self->tasks[self->order[i]];
|
||
if (task->request_id != 0U && task->phase != IEC104_INTERROGATION_REPLY &&
|
||
task->info.generation == event->transport.generation &&
|
||
task->info.common_address == response_ca &&
|
||
task->info.originator_address == info->originator_address &&
|
||
task->info.is_test == (info->is_test != 0U) && task->info.qualifier == qualifier) { return task; }
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
/* 解析一个I帧通知中的总召请求,完成布局、地址、QOI和事务状态校验。
|
||
* 未启用时或有效的其他Type返回IGNORED;接手、拒绝或满槽丢弃请求均返回HANDLED。
|
||
* 无法解析的报文关闭原连接;容量耗尽只拒绝/丢弃新请求,不取消已有任务。
|
||
* 单帧应答先尝试安全提交,未接手才保存;匹配的撤销复用原任务槽。
|
||
* 只有空闲时才建立START任务;此时已占用总召资格,Process有事件空位后生成BEGIN。 */
|
||
static IEC104_InterrogationEventResult_t Request(IEC104_Interrogation_t *self,
|
||
const IEC104_SlaveEvent_t *event)
|
||
{
|
||
IEC60870_AsduInfo_t info;
|
||
IEC60870_InterrogationCommand_t command;
|
||
const uint8_t *data;
|
||
uint16_t length;
|
||
uint16_t response_ca;
|
||
uint8_t reply_cause = 0U;
|
||
bool deactivation;
|
||
bool overload_counted = false;
|
||
InterrogationTask *task;
|
||
if (!self->status.active) { return IEC104_INTERROGATION_EVENT_IGNORED; }
|
||
if (event->asdu_result != IEC60870_ASDU_OK ||
|
||
IEC60870_Asdu_Get_Data(&event->asdu, &data, &length) != IEC60870_ASDU_OK ||
|
||
!IEC60870_Parse_ASDU_Header(event->asdu.parameters, data, length, &info)) {
|
||
++self->status.rejected;
|
||
return Reject_Connection(self, event, IEC104_INTERROGATION_FINISH_INVALID_REQUEST) ? IEC104_INTERROGATION_EVENT_HANDLED :
|
||
IEC104_INTERROGATION_EVENT_ERROR;
|
||
}
|
||
if (info.type_id != IEC60870_ASDU_C_IC_NA_1) { return IEC104_INTERROGATION_EVENT_IGNORED; }
|
||
if (IEC60870_Asdu_Parse(event->asdu.parameters, data, length, &info) != IEC60870_ASDU_OK ||
|
||
IEC60870_Asdu_Get_Element(&info, 0U, &command, sizeof(command)) != IEC60870_ASDU_OK) {
|
||
++self->status.rejected;
|
||
return Reject_Connection(self, event, IEC104_INTERROGATION_FINISH_INVALID_REQUEST) ? IEC104_INTERROGATION_EVENT_HANDLED :
|
||
IEC104_INTERROGATION_EVENT_ERROR;
|
||
}
|
||
/* 只决定应答地址,不改变原请求;后续仍按COT优先级决定是否接受及如何否定。 */
|
||
response_ca = Resolve_Common_Address(self, info.common_address);
|
||
deactivation = info.cause_of_transmission == IEC60870_ASDU_COT_DEACTIVATION;
|
||
/* 布局先完整校验;错误原因优先级COT -> CA -> IOA -> QOI/P/N/事务状态。 */
|
||
if (info.cause_of_transmission != IEC60870_ASDU_COT_ACTIVATION && !deactivation) {
|
||
reply_cause = IEC60870_ASDU_COT_UNKNOWN_CAUSE;
|
||
} else if (response_ca != self->config.common_address) {
|
||
reply_cause = IEC60870_ASDU_COT_UNKNOWN_COMMON_ADDRESS;
|
||
} else if (command.object.ioa != 0U) {
|
||
reply_cause = IEC60870_ASDU_COT_UNKNOWN_IOA;
|
||
} else if (command.qualifier < 20U || command.qualifier > 36U || info.is_negative_confirm != 0U) {
|
||
reply_cause = deactivation ? IEC60870_ASDU_COT_DEACTIVATION_CON : IEC60870_ASDU_COT_ACTIVATION_CON;
|
||
} else if (deactivation) {
|
||
task = Find_Activation(self, event, &info, response_ca, command.qualifier);
|
||
if (task != NULL) {
|
||
ImmediateReplyResult sent;
|
||
/* 原位替换为撤销确认,不移动自引用任务,也不要求额外任务槽。
|
||
* 先复制撤销请求并设置应答CA,Finish仍使用原激活的info/id/进度通知应用清理。
|
||
* 已提交HIGH的帧按原顺序在确认前发出;本地未提交批次就此丢弃。 */
|
||
if (!Copy_Response_Request(&task->request, event, response_ca)) { return IEC104_INTERROGATION_EVENT_ERROR; }
|
||
Finish(self, task, IEC104_INTERROGATION_FINISH_DEACTIVATED);
|
||
task->phase = IEC104_INTERROGATION_REPLY;
|
||
task->reply_cause = IEC60870_ASDU_COT_DEACTIVATION_CON;
|
||
task->reply_negative = false;
|
||
task->prepared = false;
|
||
task->request_id = 0U;
|
||
Move_Reply_To_Tail(self, task);
|
||
self->status.waiting = false;
|
||
++self->status.deactivated;
|
||
++self->status.cancelled;
|
||
sent = Try_Immediate_Reply(self, &task->request, response_ca, task->reply_cause, false, task);
|
||
if (sent == REPLY_SENT) { Remove_Task(self, task); }
|
||
if (sent == REPLY_FAILED) { return IEC104_INTERROGATION_EVENT_ERROR; }
|
||
return IEC104_INTERROGATION_EVENT_HANDLED;
|
||
}
|
||
reply_cause = IEC60870_ASDU_COT_DEACTIVATION_CON;
|
||
} else if (Find_Interrogation(self, 0U) != IEC104_INTERROGATION_TASK_CAPACITY) {
|
||
/* 从保存激活到终结全程占用总召资格;相同/不同QOI都否定,不替换原请求。
|
||
* 此判断仅作用于Type100激活,其他业务及匹配的COT=8撤销不受忙状态拦截。 */
|
||
reply_cause = IEC60870_ASDU_COT_ACTIVATION_CON;
|
||
}
|
||
/* 合法的新总召没有空槽时改为否定ACT_CON,仍可尝试提交,不影响当前任务。 */
|
||
if (reply_cause == 0U && self->count == IEC104_INTERROGATION_TASK_CAPACITY) {
|
||
++self->status.overloads;
|
||
overload_counted = true;
|
||
reply_cause = IEC60870_ASDU_COT_ACTIVATION_CON;
|
||
}
|
||
if (reply_cause != 0U) {
|
||
ImmediateReplyResult sent;
|
||
++self->status.rejected;
|
||
sent = Try_Immediate_Reply(self, event, response_ca, reply_cause, true, NULL);
|
||
if (sent == REPLY_SENT) { return IEC104_INTERROGATION_EVENT_HANDLED; }
|
||
if (sent == REPLY_FAILED) { return IEC104_INTERROGATION_EVENT_ERROR; }
|
||
if (self->count == IEC104_INTERROGATION_TASK_CAPACITY) {
|
||
if (!overload_counted) { ++self->status.overloads; }
|
||
++self->status.dropped_requests;
|
||
return IEC104_INTERROGATION_EVENT_HANDLED; /* 无法保存/提交的新请求已丢弃;不能重投或断链。 */
|
||
}
|
||
}
|
||
if (reply_cause == 0U && self->next_request_id == UINT64_MAX) {
|
||
return Reject_Connection(self, event, IEC104_INTERROGATION_FINISH_SERVICE_ERROR) ?
|
||
IEC104_INTERROGATION_EVENT_HANDLED : IEC104_INTERROGATION_EVENT_ERROR;
|
||
}
|
||
task = Allocate_Task(self);
|
||
if (task == NULL) {
|
||
(void)Reject_Connection(self, event, IEC104_INTERROGATION_FINISH_SERVICE_ERROR);
|
||
return IEC104_INTERROGATION_EVENT_ERROR;
|
||
}
|
||
if (!Copy_Response_Request(&task->request, event, response_ca)) {
|
||
Remove_Task(self, task);
|
||
return IEC104_INTERROGATION_EVENT_ERROR;
|
||
}
|
||
task->reply_cause = reply_cause;
|
||
task->reply_negative = reply_cause != 0U;
|
||
task->phase = reply_cause != 0U ? IEC104_INTERROGATION_REPLY : IEC104_INTERROGATION_START;
|
||
task->info.generation = event->transport.generation;
|
||
task->info.receive_serial = event->transport.data.i_frame.receive_serial;
|
||
task->info.common_address = response_ca; /* 合法总召均为本站CA;错误应答保留原错误CA且不交付BEGIN。 */
|
||
task->info.originator_address = info.originator_address;
|
||
task->info.qualifier = command.qualifier;
|
||
task->info.is_test = info.is_test != 0U;
|
||
task->info.max_objects_per_asdu = self->config.max_objects_per_asdu;
|
||
if (reply_cause == 0U) {
|
||
task->request_id = ++self->next_request_id;
|
||
}
|
||
return IEC104_INTERROGATION_EVENT_HANDLED;
|
||
}
|
||
|
||
/* 检查应用提交的数据Type是否属于本模块支持的无时标监视信息,返回true才可作为总召批次。
|
||
* 累计量不在此范围,留给总电度业务;各类型的对象编码、SQ和长度由Asdu层统一校验。 */
|
||
static bool Data_Type_Allowed(uint8_t type_id)
|
||
{
|
||
switch (type_id) {
|
||
case IEC60870_ASDU_M_SP_NA_1:
|
||
case IEC60870_ASDU_M_DP_NA_1:
|
||
case IEC60870_ASDU_M_ST_NA_1:
|
||
case IEC60870_ASDU_M_BO_NA_1:
|
||
case IEC60870_ASDU_M_ME_NA_1:
|
||
case IEC60870_ASDU_M_ME_NB_1:
|
||
case IEC60870_ASDU_M_ME_NC_1:
|
||
case IEC60870_ASDU_M_PS_NA_1:
|
||
case IEC60870_ASDU_M_ME_ND_1: return true;
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
/* 为总召任务生成一次BEGIN或NEED_DATA,并标记等待应用应答;task须处于START或未备好批次的DATA。
|
||
* event_outstanding为true时复用现有交互,不重复入队或重置等待起点。
|
||
* BEGIN需要自身和未来FINISHED两个位置;NEED_DATA不能占用终结预留。
|
||
* BEGIN前等FIFO空位不计业务期限,但已占用总召资格;BEGIN生成后开始执行整体期限。
|
||
* NEED_DATA等FIFO空位仍受整体期限约束;每次工作事件入队才启动本次应用应答期限。 */
|
||
static void Request_Application(IEC104_Interrogation_t *self, InterrogationTask *task)
|
||
{
|
||
self->status.waiting = true;
|
||
if (task->event_outstanding) { return; }
|
||
if (task->phase == IEC104_INTERROGATION_START) {
|
||
if (self->event_count + self->reserved_finishes + 2U > IEC104_INTERROGATION_EVENT_CAPACITY) { return; }
|
||
task->finish_reserved = true;
|
||
++self->reserved_finishes;
|
||
Push_Event(self, task, IEC104_INTERROGATION_BEGIN, IEC104_INTERROGATION_FINISH_NONE);
|
||
} else {
|
||
if (self->event_count + self->reserved_finishes >= IEC104_INTERROGATION_EVENT_CAPACITY) { return; }
|
||
Push_Event(self, task, IEC104_INTERROGATION_NEED_DATA, IEC104_INTERROGATION_FINISH_NONE);
|
||
}
|
||
task->event_outstanding = true;
|
||
task->event_delivered = false;
|
||
task->application_since_ms = IEC104_Net_Now_Ms();
|
||
if (task->phase == IEC104_INTERROGATION_START) { task->started_ms = task->application_since_ms; }
|
||
}
|
||
|
||
/* 应用应答的共同入口检查;expected指定应答BEGIN还是NEED_DATA,不在这里消费交互。
|
||
* 核对实例、请求、批次、类型、阶段和已取出标记,旧事件或重复应答返回STALE。
|
||
* Slave仍有通知时返回WAIT,调用方先按FIFO处理它们,防止跳过STOP/撤销/断线。
|
||
* 当前会话失效时取消;业务超时按确认边界终结,两者均返回STALE;参数/状态查询失败返回ERROR。
|
||
* OK只表示本次可以继续校验和应用应答;实际请求元数据始终取内部副本,不信任回传字段。 */
|
||
static IEC104_InterrogationReplyResult_t Check_Reply(IEC104_Interrogation_t *self,
|
||
const IEC104_InterrogationEvent_t *event, IEC104_InterrogationEventType_t expected,
|
||
InterrogationTask **matched_task)
|
||
{
|
||
InterrogationTask *task;
|
||
IEC104_SlaveRequestState_t state;
|
||
unsigned slot;
|
||
if (!Owned(self) || event == NULL) { return IEC104_INTERROGATION_REPLY_ERROR; }
|
||
slot = Find_Interrogation(self, event->request_id);
|
||
if (slot == IEC104_INTERROGATION_TASK_CAPACITY) { return IEC104_INTERROGATION_REPLY_STALE; }
|
||
task = &self->tasks[slot];
|
||
if (event->instance_id != self->instance_id || event->request_id != task->request_id ||
|
||
event->type != expected || event->batch_index != task->batch_index ||
|
||
!task->event_outstanding || !task->event_delivered ||
|
||
(expected == IEC104_INTERROGATION_BEGIN && task->phase != IEC104_INTERROGATION_START) ||
|
||
(expected == IEC104_INTERROGATION_NEED_DATA && task->phase != IEC104_INTERROGATION_DATA)) {
|
||
return IEC104_INTERROGATION_REPLY_STALE;
|
||
}
|
||
state = IEC104_Slave_Check_Request(self->slave, &self->session);
|
||
if (state == IEC104_SLAVE_REQUEST_ERROR) {
|
||
return IEC104_INTERROGATION_REPLY_ERROR;
|
||
}
|
||
self->status.events_pending = state == IEC104_SLAVE_REQUEST_WAIT_EVENTS;
|
||
if (self->status.events_pending) { return IEC104_INTERROGATION_REPLY_WAIT; }
|
||
if (!self->status.active || state != IEC104_SLAVE_REQUEST_READY) {
|
||
Cancel(self, IEC104_INTERROGATION_FINISH_CONNECTION_CHANGED);
|
||
self->status.active = false;
|
||
return IEC104_INTERROGATION_REPLY_STALE;
|
||
}
|
||
if (Expire(self) != 0) { return IEC104_INTERROGATION_REPLY_STALE; }
|
||
*matched_task = task; /* 仅OK交付内部任务地址,调用方不再假定它位于队首。 */
|
||
return IEC104_INTERROGATION_REPLY_OK;
|
||
}
|
||
|
||
/* 标记当前工作事件已经应答,解除应用等待;只在应答内容校验并接手后调用。
|
||
* 关闭event_outstanding也停止本次BEGIN/NEED_DATA期限,后续事件会设置新的计时起点。 */
|
||
static void Consume_Reply(IEC104_Interrogation_t *self, InterrogationTask *task)
|
||
{
|
||
task->event_outstanding = task->event_delivered = false;
|
||
self->status.waiting = false;
|
||
}
|
||
|
||
/* Process确认发送层已接手ASDU后推进所选任务;发送WAIT/失败时不得调用。
|
||
* CONFIRM转入DATA;DATA累加已提交进度并清prepared,允许下一次索取数据;
|
||
* TERMINATE生成FINISHED(COMPLETED),REPLY直接结束,两者均释放对应任务槽。
|
||
* 释放时只移动顺序索引,保持其他任务和ASDU的自引用;此处不等待主站ACK。 */
|
||
static void Advance(IEC104_Interrogation_t *self, InterrogationTask *task)
|
||
{
|
||
if (task->phase == IEC104_INTERROGATION_CONFIRM) {
|
||
task->phase = IEC104_INTERROGATION_DATA;
|
||
} else if (task->phase == IEC104_INTERROGATION_DATA) {
|
||
++task->batch_index;
|
||
task->objects_submitted += task->prepared_count;
|
||
task->prepared = false;
|
||
} else {
|
||
if (task->phase == IEC104_INTERROGATION_TERMINATE) {
|
||
++self->status.gi_submitted;
|
||
Finish(self, task, IEC104_INTERROGATION_FINISH_COMPLETED);
|
||
}
|
||
Remove_Task(self, task);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 功能:初始化调用方提供的配置,设置站地址、每批对象数以及应用应答和执行期限。
|
||
* 参数:config为可写配置对象;NULL时不执行任何操作。
|
||
* 说明:先清零再设置默认值,不创建实例或占用资源;应用可在Create前修改配置。
|
||
*/
|
||
void IEC104_Interrogation_Config_Default(IEC104_InterrogationConfig_t *config)
|
||
{
|
||
if (config == NULL) { return; }
|
||
memset(config, 0, sizeof(*config));
|
||
config->common_address = 1U;
|
||
config->max_objects_per_asdu = 127U;
|
||
config->begin_timeout_ms = 15000U;
|
||
config->data_timeout_ms = 15000U;
|
||
config->total_timeout_ms = 60000U;
|
||
}
|
||
|
||
/**
|
||
* 功能:从静态池创建总召实例,复制配置并借用一个有效Slave。
|
||
* 参数:slave须在本实例整个寿命内保持有效;config可在成功返回后复用。
|
||
* 返回:新实例句柄;配置非法、Slave无效、重复绑定、池满或编号耗尽时返回NULL。
|
||
* 说明:扫描完整静态池拒绝重复绑定,失败不占槽;实例编号在整个进程内不重复。
|
||
* 创建时尚无活动会话,后续依靠Handle_Event接收CONNECTED/ACTIVATED建立状态。
|
||
*/
|
||
IEC104_Interrogation_t *IEC104_Interrogation_Create(IEC104_Slave_t *slave,
|
||
const IEC104_InterrogationConfig_t *config)
|
||
{
|
||
size_t i;
|
||
IEC104_Interrogation_t *free_slot = NULL;
|
||
if (config == NULL || config->common_address == 0U || config->common_address == IEC104_CA_BROADCAST ||
|
||
config->max_objects_per_asdu == 0U || config->max_objects_per_asdu > 127U ||
|
||
IEC104_Slave_Get_Application_Layer_Parameters(slave) == NULL || instance_serial == UINT64_MAX) { return NULL; }
|
||
for (i = 0U; i < IEC104_INTERROGATION_CAPACITY; ++i) {
|
||
if (interrogation_pool[i].slave == slave) { return NULL; }
|
||
if (free_slot == NULL && interrogation_pool[i].slave == NULL) { free_slot = &interrogation_pool[i]; }
|
||
}
|
||
if (free_slot != NULL) {
|
||
memset(free_slot, 0, sizeof(*free_slot));
|
||
free_slot->slave = slave;
|
||
free_slot->config = *config;
|
||
free_slot->instance_id = ++instance_serial;
|
||
}
|
||
return free_slot;
|
||
}
|
||
|
||
/**
|
||
* 功能:释放已经没有协议任务和业务事件的实例槽,不销毁借用的Slave。
|
||
* 参数:self为Create返回的有效实例。
|
||
* 返回:true已释放,句柄失效;false为非法句柄或仍有任务/事件,原状态不变。
|
||
* 说明:不会隐式Cancel或丢弃通知;调用方须先取消任务、读完FINISHED并清理应用资源。
|
||
*/
|
||
bool IEC104_Interrogation_Destroy(IEC104_Interrogation_t *self)
|
||
{
|
||
if (!Owned(self) || self->count != 0U || self->event_count != 0U) { return false; }
|
||
memset(self, 0, sizeof(*self));
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 功能:以CANCELLED原因放弃本实例尚未提交的工作,保留终结事件供应用清理。
|
||
* 参数:self为总召实例;非法句柄无操作。
|
||
* 说明:不关闭连接、不发送确认或终止,也不撤回发送层已经接手的ASDU。
|
||
* 该入口是应用本地取消;主站撤销请求由Handle_Event执行匹配和确认流程。
|
||
*/
|
||
void IEC104_Interrogation_Cancel(IEC104_Interrogation_t *self)
|
||
{
|
||
if (Owned(self)) { Cancel(self, IEC104_INTERROGATION_FINISH_CANCELLED); }
|
||
}
|
||
|
||
/**
|
||
* 功能:处理应用转交的一个Slave历史通知,更新会话状态或保存总召请求。
|
||
* 参数:self为有效实例;event须为同一Slave按FIFO取出的原事件或Copy_Event副本。
|
||
* 返回:HANDLED已处理请求(含拒绝/满槽丢弃)或会话变化;IGNORED可交给其他业务;ERROR为参数/处理失败。
|
||
* 说明:校验事件所属服务及代次,不自行读取Slave FIFO,不保存调用方事件指针。
|
||
* 不调用应用代码;短应答在Slave通知已读空、会话有效且没有旧REPLY时尝试立即提交。
|
||
* 满槽只拒绝/丢弃新请求;取消时保留FINISHED,严重请求错误会关闭原连接。
|
||
*/
|
||
IEC104_InterrogationEventResult_t IEC104_Interrogation_Handle_Event(
|
||
IEC104_Interrogation_t *self, const IEC104_SlaveEvent_t *event)
|
||
{
|
||
IEC104_SlaveEventIdentity_t identity;
|
||
/* Slave负责来源和视图校验;本模块额外保持首次绑定的服务实例,不接管复用后的另一服务。 */
|
||
if (!Owned(self) || !IEC104_Slave_Get_Event_Identity(self->slave, event, &identity) ||
|
||
(self->session.instance_id != 0U && self->session.instance_id != identity.instance_id)) {
|
||
return IEC104_INTERROGATION_EVENT_ERROR;
|
||
}
|
||
self->session.slave = identity.slave;
|
||
self->session.instance_id = identity.instance_id;
|
||
if (event->transport.type == IEC104_TCP_EVENT_CONNECTED) {
|
||
/* TCP连通仅建立新代次;必须再收到ACTIVATED,才能接受本代次总召。 */
|
||
Cancel(self, IEC104_INTERROGATION_FINISH_CONNECTION_CHANGED);
|
||
self->session.generation = identity.generation;
|
||
self->status.active = false;
|
||
return IEC104_INTERROGATION_EVENT_HANDLED;
|
||
}
|
||
if (identity.generation != self->session.generation) {
|
||
/* 旧连接遗留的断开、回执或I帧不能改变新连接的业务状态。 */
|
||
return IEC104_INTERROGATION_EVENT_IGNORED;
|
||
}
|
||
switch (event->transport.type) {
|
||
case IEC104_TCP_EVENT_ACTIVATED:
|
||
self->status.active = true;
|
||
return IEC104_INTERROGATION_EVENT_HANDLED;
|
||
case IEC104_TCP_EVENT_DEACTIVATED:
|
||
case IEC104_TCP_EVENT_DISCONNECTED:
|
||
Cancel(self, event->transport.type == IEC104_TCP_EVENT_DEACTIVATED ?
|
||
IEC104_INTERROGATION_FINISH_STOPPED : IEC104_INTERROGATION_FINISH_DISCONNECTED);
|
||
self->status.active = false;
|
||
if (event->transport.type == IEC104_TCP_EVENT_DISCONNECTED) { self->session.generation = 0U; }
|
||
return IEC104_INTERROGATION_EVENT_HANDLED;
|
||
case IEC104_TCP_EVENT_I_FRAME: return Request(self, event);
|
||
default: return IEC104_INTERROGATION_EVENT_IGNORED; /* ACK回执不触发业务重发。 */
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 功能:自动推进当前可处理的单帧应答和总召任务,直到空闲或需要等待。
|
||
* 参数:self为有效实例;调用方不需要提供处理步数。
|
||
* 返回:true为本轮正常结束,包括等待、取消或成功处理超时;false为参数、状态查询、组包或关闭失败。
|
||
* 说明:Slave通知未读空时仅设置events_pending,应用先转交网络通知再继续调用。
|
||
* START有事件空位时生成BEGIN并开始执行计时;START及尚无批次的DATA等待应用处理。
|
||
* 双方可推进时轮转;总召等应用或业务FIFO空位时仍处理单帧应答。
|
||
* 每步重查Slave通知,发送过程中新交付的通知也须先处理;只有接手ASDU后才Advance。
|
||
* 发送WAIT保留任务并返回,下轮优先检查单帧应答。
|
||
* 本函数不接收新请求、不执行应用代码;每条REPLY提交后即释放,总召至多一个,
|
||
* 当前帧提交后结束或等待应用。任务池有界,本轮推进会自然结束。
|
||
* 调用方持续驱动本函数,并依据Get_Status给出的业务期限安排下次唤醒。
|
||
*/
|
||
bool IEC104_Interrogation_Process(IEC104_Interrogation_t *self)
|
||
{
|
||
IEC104_SlaveRequestState_t state;
|
||
if (!Owned(self)) { return false; }
|
||
for (;;) {
|
||
InterrogationTask *task;
|
||
IEC60870_MasterConnection_t *connection;
|
||
int expired;
|
||
bool accepted;
|
||
state = IEC104_Slave_Check_Request(self->slave, &self->session);
|
||
if (state == IEC104_SLAVE_REQUEST_ERROR) {
|
||
Cancel(self, IEC104_INTERROGATION_FINISH_SERVICE_ERROR);
|
||
self->status.active = false;
|
||
return false;
|
||
}
|
||
self->status.events_pending = state == IEC104_SLAVE_REQUEST_WAIT_EVENTS;
|
||
if (self->status.events_pending) { return true; }
|
||
self->status.waiting = false;
|
||
if (self->count == 0U) { return true; }
|
||
if (!self->status.active || state != IEC104_SLAVE_REQUEST_READY) {
|
||
Cancel(self, IEC104_INTERROGATION_FINISH_CONNECTION_CHANGED);
|
||
self->status.active = false;
|
||
return true;
|
||
}
|
||
expired = Expire(self);
|
||
if (expired != 0) {
|
||
if (expired < 0 || !self->status.active) { return expired > 0; }
|
||
continue; /* 确认前超时仅转为REPLY;重查通知后,本轮即可尝试提交否定确认。 */
|
||
}
|
||
task = Select_Task(self);
|
||
if (task == NULL) { self->status.waiting = true; return true; }
|
||
connection = &task->request.connection;
|
||
if (task->phase == IEC104_INTERROGATION_START ||
|
||
(task->phase == IEC104_INTERROGATION_DATA && !task->prepared)) {
|
||
Request_Application(self, task);
|
||
self->prefer_interrogation = false;
|
||
continue; /* 应用稍后取事件;继续处理当前已有的单帧应答。 */
|
||
}
|
||
/* 请求副本创建时已准备应答CA;此处只设置本阶段COT/P/N,WAIT后继续使用同一副本。 */
|
||
if (task->phase == IEC104_INTERROGATION_CONFIRM) {
|
||
accepted = IEC60870_MasterConnection_Send_ACT_CON(connection, &task->request.asdu, false);
|
||
} else if (task->phase == IEC104_INTERROGATION_TERMINATE) {
|
||
accepted = IEC60870_MasterConnection_Send_ACT_TERM(connection, &task->request.asdu);
|
||
} else if (task->phase == IEC104_INTERROGATION_REPLY) {
|
||
if (IEC60870_Asdu_Set_COT(&task->request.asdu, task->reply_cause) != IEC60870_ASDU_OK ||
|
||
IEC60870_Asdu_Set_Negative(&task->request.asdu, task->reply_negative) != IEC60870_ASDU_OK) {
|
||
(void)Reject_Connection(self, &task->request, IEC104_INTERROGATION_FINISH_SERVICE_ERROR);
|
||
return false;
|
||
}
|
||
accepted = IEC60870_MasterConnection_Send_ASDU(connection, &task->request.asdu);
|
||
} else {
|
||
accepted = IEC60870_MasterConnection_Send_ASDU(connection, &task->outgoing);
|
||
}
|
||
if (!accepted) {
|
||
IEC104_TcpSubmitResult_t result = IEC104_Slave_Get_Last_Submit_Result(connection);
|
||
if (result == IEC104_TCP_SUBMIT_WAIT) {
|
||
/* 未接手时保留phase、prepared和原文,下次重试同一帧,不重复向应用取数。 */
|
||
++self->status.waits;
|
||
self->status.waiting = true;
|
||
self->prefer_interrogation = false;
|
||
} else {
|
||
Cancel(self, IEC104_INTERROGATION_FINISH_SEND_ERROR);
|
||
self->status.active = false;
|
||
}
|
||
return true;
|
||
}
|
||
self->prefer_interrogation = task->phase == IEC104_INTERROGATION_REPLY;
|
||
++self->status.submitted;
|
||
if (task->phase == IEC104_INTERROGATION_REPLY && task->reply_negative) { ++self->status.negative_submitted; }
|
||
Advance(self, task);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 功能:将最早一条业务事件复制到调用方并从FIFO移除,不执行网络操作。
|
||
* 参数:self为有效实例;event为非NULL输出对象,接收可独立保存的纯值副本。
|
||
* 返回:READY写出事件;EMPTY表示暂时无事件;ERROR表示参数非法,后两者不改输出。
|
||
* 说明:工作事件出队后才允许应答;BEGIN出队同时标记应用已持有本请求,后续须配对FINISHED。
|
||
* 业务事件不含自引用指针,因此可以memmove压紧FIFO;协议任务数组不能如此移动。
|
||
*/
|
||
IEC104_InterrogationNextResult_t IEC104_Interrogation_NextEvent(
|
||
IEC104_Interrogation_t *self, IEC104_InterrogationEvent_t *event)
|
||
{
|
||
InterrogationTask *task = NULL;
|
||
if (!Owned(self) || event == NULL) { return IEC104_INTERROGATION_NEXT_ERROR; }
|
||
if (self->event_count == 0U) { return IEC104_INTERROGATION_NEXT_EMPTY; }
|
||
if (self->events[0].type != IEC104_INTERROGATION_FINISHED) {
|
||
unsigned slot = Find_Interrogation(self, self->events[0].request_id);
|
||
if (slot == IEC104_INTERROGATION_TASK_CAPACITY) { return IEC104_INTERROGATION_NEXT_ERROR; }
|
||
task = &self->tasks[slot];
|
||
}
|
||
*event = self->events[0];
|
||
--self->event_count;
|
||
memmove(self->events, self->events + 1, self->event_count * sizeof(self->events[0]));
|
||
if (task != NULL) {
|
||
task->event_delivered = true;
|
||
if (event->type == IEC104_INTERROGATION_BEGIN) { task->begin_delivered = true; }
|
||
}
|
||
return IEC104_INTERROGATION_NEXT_READY;
|
||
}
|
||
|
||
/**
|
||
* 功能:接收应用对已取出BEGIN事件的接受或拒绝决定。
|
||
* 参数:event为本实例当前BEGIN的原样副本;accept为true接受、false拒绝。
|
||
* 返回:沿用Check_Reply的WAIT/STALE/ERROR;OK表示决定已消费,不可用同一事件重复应答。
|
||
* 说明:接受后转为CONFIRM,等待Process提交肯定ACT_CON;拒绝先生成FINISHED(REJECTED),
|
||
* 再将任务转为否定ACT_CON应答。资源由应用读取FINISHED后释放,本函数不执行清理回调。
|
||
*/
|
||
IEC104_InterrogationReplyResult_t IEC104_Interrogation_Reply_Begin(
|
||
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event, bool accept)
|
||
{
|
||
InterrogationTask *task;
|
||
IEC104_InterrogationReplyResult_t result = Check_Reply(self, event, IEC104_INTERROGATION_BEGIN, &task);
|
||
if (result != IEC104_INTERROGATION_REPLY_OK) { return result; }
|
||
Consume_Reply(self, task);
|
||
if (accept) { task->phase = IEC104_INTERROGATION_CONFIRM; }
|
||
else { Reject_Request(self, task, IEC104_INTERROGATION_FINISH_REJECTED); }
|
||
return IEC104_INTERROGATION_REPLY_OK;
|
||
}
|
||
|
||
/**
|
||
* 功能:接收一次NEED_DATA对应的非空数据批次,校验后复制到任务自有缓冲。
|
||
* 参数:event为当前已取出的NEED_DATA;asdu为应用构造的完整监视信息ASDU。
|
||
* 返回:OK已接手批次;WAIT/STALE沿用交互校验结果;ERROR为参数、布局、类型或元数据不符。
|
||
* 说明:按原请求验证COT、OA、TEST,按配置验证本站CA和对象数,并检查批次/累计计数溢出。
|
||
* 全部校验通过才消费工作事件;非法批次可修正后用同一事件再交付,空数据应调用Complete_Data。
|
||
* 成功后不借用应用缓冲或参数,实际提交及进度计数由后续Process完成。
|
||
*/
|
||
IEC104_InterrogationReplyResult_t IEC104_Interrogation_Submit_Batch(
|
||
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event,
|
||
const IEC60870_Asdu_t *asdu)
|
||
{
|
||
InterrogationTask *task;
|
||
IEC60870_AsduInfo_t info;
|
||
const uint8_t *data;
|
||
uint16_t length;
|
||
const IEC60870_AppLayerParameters_t *parameters;
|
||
IEC104_InterrogationReplyResult_t result = Check_Reply(self, event, IEC104_INTERROGATION_NEED_DATA, &task);
|
||
if (result != IEC104_INTERROGATION_REPLY_OK) { return result; }
|
||
parameters = IEC104_Slave_Get_Application_Layer_Parameters(self->slave);
|
||
if (asdu == NULL || asdu->parameters == NULL ||
|
||
asdu->parameters->sizeOfCOT != 2 || asdu->parameters->sizeOfCA != 2 ||
|
||
asdu->parameters->sizeOfIOA != 3 ||
|
||
IEC60870_Asdu_Get_Data(asdu, &data, &length) != IEC60870_ASDU_OK ||
|
||
length > sizeof(task->bytes) ||
|
||
IEC60870_Asdu_Parse(parameters, data, length, &info) != IEC60870_ASDU_OK ||
|
||
!Data_Type_Allowed(info.type_id) || info.object_count == 0U ||
|
||
info.object_count > self->config.max_objects_per_asdu ||
|
||
info.cause_of_transmission != task->info.qualifier ||
|
||
info.common_address != self->config.common_address ||
|
||
info.originator_address != task->info.originator_address ||
|
||
info.is_test != (uint8_t)task->info.is_test || info.is_negative_confirm != 0U ||
|
||
task->batch_index == SIZE_MAX || task->objects_submitted > SIZE_MAX - info.object_count) {
|
||
return IEC104_INTERROGATION_REPLY_ERROR;
|
||
}
|
||
/* 原文复制到任务内,并以Slave的稳定参数重建视图,避免引用应用临时对象。 */
|
||
memcpy(task->bytes, data, length);
|
||
if (IEC60870_Asdu_Create_From_Buffer(&task->outgoing, parameters, task->bytes, length) != IEC60870_ASDU_OK) {
|
||
return IEC104_INTERROGATION_REPLY_ERROR;
|
||
}
|
||
task->prepared_count = info.object_count;
|
||
task->prepared = true;
|
||
Consume_Reply(self, task);
|
||
return IEC104_INTERROGATION_REPLY_OK;
|
||
}
|
||
|
||
/**
|
||
* 功能:应答当前NEED_DATA,声明本次总召已没有更多数据。
|
||
* 参数:event须为当前已取出的NEED_DATA,不能使用BEGIN或已经交付过批次的旧事件。
|
||
* 返回:OK已消费交互并转为TERMINATE;其他结果沿用Check_Reply,不进入正常终止流程。
|
||
* 说明:此处不发送ACT_TERM;后续Process提交成功才产生FINISHED(COMPLETED),发送等待时保留任务。
|
||
*/
|
||
IEC104_InterrogationReplyResult_t IEC104_Interrogation_Complete_Data(
|
||
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event)
|
||
{
|
||
InterrogationTask *task;
|
||
IEC104_InterrogationReplyResult_t result = Check_Reply(self, event, IEC104_INTERROGATION_NEED_DATA, &task);
|
||
if (result != IEC104_INTERROGATION_REPLY_OK) { return result; }
|
||
Consume_Reply(self, task);
|
||
task->phase = IEC104_INTERROGATION_TERMINATE;
|
||
return IEC104_INTERROGATION_REPLY_OK;
|
||
}
|
||
|
||
/**
|
||
* 功能:应用在建立快照或取得数据失败时,按肯定确认的提交边界终结本次总召。
|
||
* 参数:event为当前已取出且尚未应答的BEGIN或NEED_DATA。
|
||
* 返回:WAIT要求先处理Slave通知;STALE表示交互已失效;ERROR为检查或关闭失败。
|
||
* 通过检查后,BEGIN失败转为否定REPLY并保留连接;NEED_DATA失败关闭原连接。
|
||
* OK表示失败已处理,否定确认由Process提交;若关闭失败返回ERROR,任务仍已取消,不能重试。
|
||
* 说明:两种路径均保留SOURCE_ERROR并交付FINISHED供应用清理,不发送成功ACT_TERM。
|
||
*/
|
||
IEC104_InterrogationReplyResult_t IEC104_Interrogation_Fail_Request(
|
||
IEC104_Interrogation_t *self, const IEC104_InterrogationEvent_t *event)
|
||
{
|
||
IEC104_InterrogationReplyResult_t result;
|
||
InterrogationTask *task;
|
||
if (event == NULL) { return IEC104_INTERROGATION_REPLY_ERROR; }
|
||
if (event->type != IEC104_INTERROGATION_BEGIN && event->type != IEC104_INTERROGATION_NEED_DATA) {
|
||
return Owned(self) ? IEC104_INTERROGATION_REPLY_STALE : IEC104_INTERROGATION_REPLY_ERROR;
|
||
}
|
||
result = Check_Reply(self, event, event->type, &task);
|
||
if (result != IEC104_INTERROGATION_REPLY_OK) { return result; }
|
||
return Fail_Interrogation(self, task, IEC104_INTERROGATION_FINISH_SOURCE_ERROR) ?
|
||
IEC104_INTERROGATION_REPLY_OK : IEC104_INTERROGATION_REPLY_ERROR;
|
||
}
|
||
|
||
/**
|
||
* 功能:组合会话状态、累计统计、活动总召进度及最近业务期限,生成独立状态副本。
|
||
* 参数:self为有效实例;status为非NULL输出对象。
|
||
* 返回:true已写出快照;false为非法句柄/NULL,输出保持不变。
|
||
* 说明:phase和批次进度优先描述唯一总召;仅有应答时REPLY,无任务时IDLE。
|
||
* 尚未生成BEGIN不计业务期限;timer_active为false时剩余值无意义。
|
||
* 只读取当前单调时钟,不触发超时、发送或事件入队;到期后仍由Process或应答入口处理。
|
||
*/
|
||
bool IEC104_Interrogation_Get_Status(const IEC104_Interrogation_t *self,
|
||
IEC104_InterrogationStatus_t *status)
|
||
{
|
||
const InterrogationTask *task;
|
||
unsigned slot;
|
||
uint32_t remaining;
|
||
if (!Owned(self) || status == NULL) { return false; }
|
||
slot = Find_Interrogation(self, 0U);
|
||
task = slot != IEC104_INTERROGATION_TASK_CAPACITY ? &self->tasks[slot] :
|
||
(self->count != 0U ? &self->tasks[self->order[0]] : NULL);
|
||
*status = self->status;
|
||
status->gi_pending = slot != IEC104_INTERROGATION_TASK_CAPACITY;
|
||
status->generation = self->session.generation;
|
||
status->pending_requests = self->count;
|
||
status->pending_events = self->event_count;
|
||
status->phase = task != NULL ? task->phase : IEC104_INTERROGATION_IDLE;
|
||
status->batch_index = task != NULL ? task->batch_index : 0U;
|
||
status->objects_submitted = task != NULL ? task->objects_submitted : 0U;
|
||
status->application_waiting = task != NULL && task->event_outstanding;
|
||
if (task != NULL && task->phase != IEC104_INTERROGATION_REPLY &&
|
||
!Interrogation_Can_Progress(self, task)) { status->waiting = true; }
|
||
status->timer_active = false;
|
||
status->timeout_remaining_ms = 0U;
|
||
if (task != NULL && Task_Timeout(self, task, IEC104_Net_Now_Ms(), &remaining) != IEC104_INTERROGATION_FINISH_NONE) {
|
||
status->timer_active = true;
|
||
status->timeout_remaining_ms = remaining;
|
||
}
|
||
return true;
|
||
}
|