1032 lines
56 KiB
C
1032 lines
56 KiB
C
/**
|
||
* 子站两级ASDU字节环:HIGH只保存队列待发条目;NORMAL另保留已登记发送交接、仍待ACK的前缀。
|
||
* 条目在逻辑上连续,头与原文均可跨数组尾部回绕,不为尾部空隙浪费容量。
|
||
* HIGH空间不足时拒绝;NORMAL覆盖最旧原文,包括已提交条目,但保留其待确认计数。
|
||
* SendBuffer接手I帧且Manager建立记录后,Mark_Submitted才完成队列记账:HIGH消费原文,NORMAL保留原文。
|
||
* Manager初始化成功、受理新STOP或从STOPPED受理START时清空HIGH;已启用时重复START保留HIGH。
|
||
* Rebind清空HIGH并恢复NORMAL现存原文。
|
||
* 本模块负责存储及条目身份;选择优先级、分配N(S)、确认合法性由Manager和Link处理。
|
||
* HIGH只存长度;NORMAL存编号、状态及长度。仅NORMAL入队分配entry_serial,与I帧的N(S)不同。
|
||
* 所有存储由调用方提供;同连接操作串行,输入/输出与内部存储不得重叠。
|
||
*/
|
||
#include "IEC104_AsduQueue.h"
|
||
|
||
#include <stddef.h>
|
||
#include <string.h>
|
||
|
||
/*
|
||
* 编号环为1..UINT64_MAX,跳过无效值0;活跃编号到next_entry_serial的前向距离至多半环。
|
||
* 队列活跃编号包括仍有原文的条目和NORMAL覆盖待ACK条目,不能按普通整数大小判断先后。
|
||
* HIGH不分配条目编号,也不占编号窗口;Manager使用独立frame_id和N(S)处理完成与ACK。
|
||
*/
|
||
#define ENTRY_SERIAL_WINDOW_MAX (UINT64_MAX / 2U)
|
||
|
||
/* 队列记账状态,对应公开快照is_submitted的0/1值;不是NORMAL条目头里的编码。
|
||
* PENDING表示队列尚未登记发送交接,不能据此断言SendBuffer是否已经接手。
|
||
* HIGH在Mark_Submitted中直接消费原文,因此环内没有RETAINED_UNACKED状态。 */
|
||
enum {
|
||
QUEUE_ENTRY_PENDING = 0,
|
||
QUEUE_ENTRY_RETAINED_UNACKED = 1
|
||
};
|
||
|
||
/* NORMAL条目头的实际存储编码:1为队列待发,2为已登记交接且保留原文等待ACK。 */
|
||
enum {
|
||
NORMAL_HEADER_PENDING = 1,
|
||
NORMAL_HEADER_RETAINED_UNACKED = 2
|
||
};
|
||
|
||
/* 编译期检查:NORMAL头的asdu_length仅8位,可表示0..255,当前ASDU长度上限249满足要求。
|
||
* 若以后将上限提高到255以上,此处会编译报错,提醒同步调整长度字段及该约束,避免长度被截断。
|
||
* 此检查不生成运行时代码,也不增加条目存储开销。 */
|
||
_Static_assert(IEC104_MAX_ASDU_LEN <= UINT8_MAX, "NORMAL ASDU length must fit in eight bits");
|
||
|
||
/**
|
||
* 功能:取得指定优先级每条记录的管理头长度,用于容量计算和定位ASDU起点。
|
||
* 输入参数:priority——调用方已验证为HIGH或NORMAL。
|
||
* 输出参数:无。
|
||
* 返回值:HIGH为sizeof(uint16_t),NORMAL为sizeof(IEC104_AsduNormalEntryHeader_t),单位为字节。
|
||
* 说明:长度不包含ASDU原文;NORMAL结果包含当前编译器的结构体填充,不是固定的协议长度。
|
||
*/
|
||
static uint32_t Entry_Header_Length(IEC104_AsduPriority_t priority)
|
||
{
|
||
return priority == IEC104_ASDU_PRIORITY_HIGH ?
|
||
IEC104_ASDU_HIGH_ENTRY_HEADER_LEN : IEC104_ASDU_NORMAL_ENTRY_HEADER_LEN;
|
||
}
|
||
|
||
/**
|
||
* 功能:计算编号环中的后继编号,跳过保留值0。
|
||
* 输入参数:serial——1..UINT64_MAX内的有效编号。
|
||
* 输出参数:无,不修改队列或连接代次。
|
||
* 返回值:普通编号加1;UINT64_MAX的后继为1。
|
||
* 说明:0留作无效编号;回绕只计算编号,不会清空队列或触发连接重绑。
|
||
*/
|
||
static uint64_t Entry_Serial_Next(uint64_t serial)
|
||
{
|
||
return serial == UINT64_MAX ? 1U : serial + 1U;
|
||
}
|
||
|
||
/**
|
||
* 功能:计算从from沿编号递增方向到to所需的步数,允许跨上限回绕。
|
||
* 输入参数:from/to——调用方保证均在1..UINT64_MAX内。
|
||
* 输出参数:无。
|
||
* 返回值:0..UINT64_MAX-1内的前向距离;编号相同时为0。
|
||
* 说明:有效编号共UINT64_MAX个,按此环长计算;例如UINT64_MAX到1的距离为1。
|
||
* 直接用uint64_t减法会按2^64取模,包含已跳过的0,不能用于本编号环。
|
||
*/
|
||
static uint64_t Entry_Serial_Distance(uint64_t from, uint64_t to)
|
||
{
|
||
return to >= from ? to - from : UINT64_MAX - from + to;
|
||
}
|
||
|
||
/**
|
||
* 功能:按半环规则判断older是否严格早于newer。
|
||
* 输入参数:older/newer——待比较的内部条目编号,0按无效编号处理。
|
||
* 输出参数:无。
|
||
* 返回值:两者非零且前向距离在1..ENTRY_SERIAL_WINDOW_MAX内时为1,否则0。
|
||
* 说明:先后关系仅适用于跨度受限的活跃窗口,不能判断任意历史编号的年代。
|
||
* UINT64_MAX早于1,但1不早于UINT64_MAX;相同编号不构成先后关系。
|
||
* 限制在半环内是为了避免回绕时混淆活跃编号的新旧关系;这里比较的是编号距离,不是条目数量。
|
||
*/
|
||
static int Entry_Serial_Before(uint64_t older, uint64_t newer)
|
||
{
|
||
uint64_t distance;
|
||
|
||
if (older == 0U || newer == 0U) {
|
||
return 0;
|
||
}
|
||
distance = Entry_Serial_Distance(older, newer);
|
||
return distance != 0U && distance <= ENTRY_SERIAL_WINDOW_MAX;
|
||
}
|
||
|
||
/**
|
||
* 功能:检查业务优先级是否为HIGH或NORMAL。
|
||
* 输入参数:priority——调用方传入的优先级。
|
||
* 输出参数:无。
|
||
* 返回值:有效返回1,否则0;COUNT只用于数组长度,不能作为业务优先级。
|
||
*/
|
||
static int Priority_Valid(IEC104_AsduPriority_t priority)
|
||
{
|
||
return priority == IEC104_ASDU_PRIORITY_HIGH || priority == IEC104_ASDU_PRIORITY_NORMAL;
|
||
}
|
||
|
||
/**
|
||
* 功能:沿字节环前进指定长度,跨数组尾部时从下标0继续。
|
||
* 输入参数:bucket——容量非零的桶;index——有效下标;length——0..byte_capacity。
|
||
* 输出参数:无。
|
||
* 返回值:前进后的有效数组下标,length等于总容量时回到原位置。
|
||
* 说明:先比较尾部剩余长度再计算,避免index+length在32位边界溢出。
|
||
* 例如容量100、index为90、length为20,结果为10;本函数不校验占用范围或移动实际字节。
|
||
*/
|
||
static uint32_t Ring_Advance(const IEC104_AsduPriorityQueue_t *bucket, uint32_t index, uint32_t length)
|
||
{
|
||
uint32_t tail_bytes = bucket->byte_capacity - index;
|
||
return length >= tail_bytes ? length - tail_bytes : index + length;
|
||
}
|
||
|
||
/**
|
||
* 功能:从环中读取一段逻辑连续字节,必要时分两次复制。
|
||
* 输入参数:bucket/index——已验证的字节环及起点;length——不超过总容量的读取长度。
|
||
* 输出参数:destination——至少length字节、与环不重叠的可写区。
|
||
* 返回值:无。
|
||
* 说明:调用方先验证逻辑占用范围;本函数只处理物理回绕,不检查条目身份。
|
||
* 例如容量100,从90读取20字节,先复制尾部10字节,再复制开头10字节。
|
||
* 通过memcpy读取到调用方对象,条目头跨尾部或环内地址未对齐时也无需强转结构体指针。
|
||
*/
|
||
static void Ring_Read(const IEC104_AsduPriorityQueue_t *bucket, uint32_t index,
|
||
void *destination, uint32_t length)
|
||
{
|
||
uint32_t first_length = bucket->byte_capacity - index;
|
||
if (first_length > length) {
|
||
first_length = length;
|
||
}
|
||
memcpy(destination, bucket->bytes + index, first_length);
|
||
if (length > first_length) {
|
||
memcpy((uint8_t *)destination + first_length, bucket->bytes, length - first_length);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 功能:将一段逻辑连续字节写入环,必要时分两次复制。
|
||
* 输入参数:bucket/index——已验证的目标环及起点;source——不与环重叠的输入;
|
||
* length——不超过总容量且已获得空间保障的长度。
|
||
* 输出参数:bucket->bytes——写入对应的尾部和起始部分,不改变任何游标。
|
||
* 返回值:无。
|
||
* 说明:Enqueue先完成全部容量检查,再写头和原文,最后统一发布控制字段。
|
||
* 本函数只复制字节,不决定哪些旧条目可覆盖,也不更新used_bytes或条目计数。
|
||
*/
|
||
static void Ring_Write(const IEC104_AsduPriorityQueue_t *bucket, uint32_t index,
|
||
const void *source, uint32_t length)
|
||
{
|
||
uint32_t first_length = bucket->byte_capacity - index;
|
||
if (first_length > length) {
|
||
first_length = length;
|
||
}
|
||
memcpy(bucket->bytes + index, source, first_length);
|
||
if (length > first_length) {
|
||
memcpy(bucket->bytes, (const uint8_t *)source + first_length, length - first_length);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 功能:检查单个优先级字节环的边界、占用量和待发游标是否自洽。
|
||
* 输入参数:bucket——非NULL控制结构;priority——已验证的优先级,用于确定本级头长。
|
||
* 输出参数:无,不读取条目头或修改字节。
|
||
* 返回值:控制字段自洽返回1,否则0。
|
||
* 说明:used_bytes只计条目头和ASDU,无回绕空隙;它区分head==tail时的满与空。
|
||
* head==tail且used_bytes为0表示空环,为byte_capacity表示满环。
|
||
* 检查为常数时间,不验证每条记录的头;完整条目布局由Bucket_Entries_Valid检查。
|
||
*/
|
||
static int Bucket_Valid(const IEC104_AsduPriorityQueue_t *bucket, IEC104_AsduPriority_t priority)
|
||
{
|
||
uint32_t unsubmitted_distance;
|
||
uint32_t minimum_size = Entry_Header_Length(priority) + IEC104_ASDU_HEADER_LEN;
|
||
|
||
if (bucket->bytes == NULL || bucket->byte_capacity < minimum_size ||
|
||
bucket->head_index >= bucket->byte_capacity || bucket->tail_index >= bucket->byte_capacity ||
|
||
bucket->unsubmitted_index >= bucket->byte_capacity || bucket->used_bytes > bucket->byte_capacity ||
|
||
bucket->unsubmitted_count > bucket->entry_count ||
|
||
bucket->entry_count > bucket->used_bytes / minimum_size) {
|
||
return 0;
|
||
}
|
||
if (bucket->entry_count == 0U) {
|
||
return bucket->used_bytes == 0U && bucket->head_index == 0U &&
|
||
bucket->tail_index == 0U && bucket->unsubmitted_index == 0U;
|
||
}
|
||
/* 条目紧接存放,从队首经过全部占用字节后必须正好到队尾。 */
|
||
if (Ring_Advance(bucket, bucket->head_index, bucket->used_bytes) != bucket->tail_index) {
|
||
return 0;
|
||
}
|
||
if (bucket->unsubmitted_count == 0U) {
|
||
return bucket->unsubmitted_index == bucket->tail_index;
|
||
}
|
||
if (bucket->unsubmitted_count == bucket->entry_count) {
|
||
return bucket->unsubmitted_index == bucket->head_index;
|
||
}
|
||
/* 同时存在保留待ACK前缀和待发后缀时,待发游标应严格位于占用区内部。 */
|
||
unsubmitted_distance = bucket->unsubmitted_index >= bucket->head_index ?
|
||
bucket->unsubmitted_index - bucket->head_index :
|
||
bucket->byte_capacity - bucket->head_index + bucket->unsubmitted_index;
|
||
return unsubmitted_distance != 0U && unsubmitted_distance < bucket->used_bytes;
|
||
}
|
||
|
||
/**
|
||
* 功能:检查业务队列的初始化标志、连接身份及两个字节环。
|
||
* 输入参数:queue——待检查对象,允许NULL。
|
||
* 输出参数:无。
|
||
* 返回值:整体控制状态合法返回1,否则0;不检查TCP状态或逐条遍历ASDU。
|
||
* 说明:NORMAL覆盖待确认计数与身份边界须一致;原文已覆盖不代表协议已确认。
|
||
* HIGH原文在登记交接时出队,因此其保留条目数必须等于待发条目数。
|
||
* 代次0表示离线缓存,HIGH、已提交记账及EOF发送限制必须为空;具体代次匹配由入口核对。
|
||
*/
|
||
static int Queue_State_Valid(const IEC104_AsduQueue_t *queue)
|
||
{
|
||
uint64_t overwritten_span;
|
||
|
||
if (queue == NULL || queue->is_initialized != 1U ||
|
||
queue->next_entry_serial == 0U ||
|
||
!Bucket_Valid(&queue->priority_queues[IEC104_ASDU_PRIORITY_HIGH], IEC104_ASDU_PRIORITY_HIGH) ||
|
||
!Bucket_Valid(&queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL], IEC104_ASDU_PRIORITY_NORMAL) ||
|
||
queue->priority_queues[IEC104_ASDU_PRIORITY_HIGH].entry_count !=
|
||
queue->priority_queues[IEC104_ASDU_PRIORITY_HIGH].unsubmitted_count) {
|
||
return 0;
|
||
}
|
||
if (queue->normal_send_limited > 1U) {
|
||
return 0;
|
||
}
|
||
if (queue->connection_generation == 0U &&
|
||
(queue->priority_queues[IEC104_ASDU_PRIORITY_HIGH].entry_count != 0U ||
|
||
queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL].entry_count !=
|
||
queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL].unsubmitted_count ||
|
||
queue->overwritten_unacked_count != 0U || queue->normal_send_limited != 0U)) {
|
||
return 0;
|
||
}
|
||
if (queue->overwritten_unacked_count == 0U) {
|
||
/* 覆盖前缀已清完时不保留旧边界,避免已确认的历史编号继续占住窗口。 */
|
||
return queue->overwritten_unacked_serial_end == 0U && queue->overwritten_unacked_serial_begin == 0U;
|
||
}
|
||
if (!Entry_Serial_Before(queue->overwritten_unacked_serial_begin, queue->next_entry_serial) ||
|
||
!Entry_Serial_Before(queue->overwritten_unacked_serial_end, queue->next_entry_serial)) {
|
||
return 0;
|
||
}
|
||
overwritten_span = Entry_Serial_Distance(queue->overwritten_unacked_serial_begin,
|
||
queue->overwritten_unacked_serial_end);
|
||
/* 区间含两端且止于next之前;丢弃未提交NORMAL可留下间隔,数量不超过span+1。 */
|
||
return overwritten_span < Entry_Serial_Distance(queue->overwritten_unacked_serial_begin, queue->next_entry_serial) &&
|
||
queue->overwritten_unacked_count <= overwritten_span + 1U;
|
||
}
|
||
|
||
/**
|
||
* 功能:读取条目并核对预期的队列旧状态,返回最多两段ASDU原文的借用指针。
|
||
* 输入参数:queue/priority——已通过容器和优先级检查的队列与桶;
|
||
* index——候选条目头下标;expected_queue_state——预期的QUEUE_ENTRY_*记账状态。
|
||
* 输出参数:info——非NULL,仅成功时写入身份、长度及两段只读借用指针。
|
||
* 返回值:条目头和占用范围通过检查返回1,否则0,失败不改info;不校验ASDU业务内容。
|
||
* 说明:先检查整个条目头属于占用区,再用Ring_Read处理回绕和未对齐数值。
|
||
* index由有效游标或条目边界推导;不扫描前序条目,也不移动原文。
|
||
* 该参数只校验读取时的状态,不设置新状态,也不检查SendBuffer或发送结果。
|
||
* NORMAL头编码映射为记账状态后比较;HIGH环内条目恒为PENDING且没有条目编号。
|
||
* NORMAL头内的1/2对应输出is_submitted的0/1,不能直接拿头内编码与预期记账状态比较。
|
||
* NORMAL编号须位于next之前的半环窗口内,并晚于已覆盖待ACK前缀。
|
||
* 输出指针指向队列存储,不复制ASDU;调用方只能在条目借用仍有效期间读取原文。
|
||
*/
|
||
static int Read_Entry_With_Expected_State(const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority,
|
||
uint32_t index, uint8_t expected_queue_state,
|
||
IEC104_AsduHeadInfo_t *info)
|
||
{
|
||
const IEC104_AsduPriorityQueue_t *bucket = &queue->priority_queues[priority];
|
||
IEC104_AsduHeadInfo_t prepared;
|
||
IEC104_AsduNormalEntryHeader_t header;
|
||
uint32_t header_length = Entry_Header_Length(priority);
|
||
uint32_t distance, asdu_index, first_length;
|
||
|
||
if (index >= bucket->byte_capacity) {
|
||
return 0;
|
||
}
|
||
distance = index >= bucket->head_index ? index - bucket->head_index :
|
||
bucket->byte_capacity - bucket->head_index + index;
|
||
if (distance >= bucket->used_bytes || bucket->used_bytes - distance < header_length) {
|
||
return 0;
|
||
}
|
||
if (priority == IEC104_ASDU_PRIORITY_HIGH) {
|
||
Ring_Read(bucket, index, &prepared.asdu_length, sizeof(prepared.asdu_length));
|
||
prepared.entry_id.entry_serial = 0U;
|
||
prepared.is_submitted = QUEUE_ENTRY_PENDING;
|
||
} else {
|
||
Ring_Read(bucket, index, &header, sizeof(header));
|
||
if ((header.entry_state != NORMAL_HEADER_PENDING && header.entry_state != NORMAL_HEADER_RETAINED_UNACKED) ||
|
||
!Entry_Serial_Before(header.entry_serial, queue->next_entry_serial) ||
|
||
(queue->overwritten_unacked_count != 0U &&
|
||
!Entry_Serial_Before(queue->overwritten_unacked_serial_end, header.entry_serial))) {
|
||
return 0;
|
||
}
|
||
prepared.asdu_length = (uint16_t)header.asdu_length;
|
||
prepared.entry_id.entry_serial = header.entry_serial;
|
||
prepared.is_submitted = header.entry_state == NORMAL_HEADER_RETAINED_UNACKED;
|
||
}
|
||
if (prepared.asdu_length < IEC104_ASDU_HEADER_LEN || prepared.asdu_length > IEC104_MAX_ASDU_LEN ||
|
||
(uint32_t)prepared.asdu_length + header_length > bucket->used_bytes - distance ||
|
||
prepared.is_submitted != expected_queue_state) {
|
||
return 0;
|
||
}
|
||
asdu_index = Ring_Advance(bucket, index, header_length);
|
||
first_length = bucket->byte_capacity - asdu_index;
|
||
if (first_length > prepared.asdu_length) {
|
||
first_length = prepared.asdu_length;
|
||
}
|
||
prepared.entry_id.connection_generation = queue->connection_generation;
|
||
prepared.priority = priority;
|
||
prepared.asdu_first = bucket->bytes + asdu_index;
|
||
prepared.asdu_first_length = (uint16_t)first_length;
|
||
prepared.asdu_second = first_length < prepared.asdu_length ? bucket->bytes : NULL;
|
||
*info = prepared;
|
||
return 1;
|
||
}
|
||
|
||
/**
|
||
* 功能:在入队写字节前,检查分配本次编号后能否继续维持半环窗口。
|
||
* 输入参数:queue——非NULL、控制字段已检查的副本,已完成本次NORMAL覆盖预处理。
|
||
* 输出参数:无,不修改副本或字节数组。
|
||
* 返回值:OK允许推进;FULL表示旧编号占住窗口;ERROR表示物理队首条目非法。
|
||
* 说明:next是NORMAL本次入队后的下一编号;NORMAL物理队首及覆盖区间下界均须早于它。
|
||
* NORMAL保持FIFO编号顺序,只需检查队首,开销与条目总数无关;HIGH不使用本检查。
|
||
* 已丢弃的未提交条目不再占窗口;已覆盖但待ACK的条目仍占窗口。
|
||
* FULL时须先处理旧条目或ACK再重试,不能越过窗口继续分配编号。
|
||
* 本函数只预检,不推进next_entry_serial;因此即使字节空间足够,也可能因编号窗口返回FULL。
|
||
*/
|
||
static IEC104_AsduQueueResult_t Check_Entry_Serial_Advance(const IEC104_AsduQueue_t *queue)
|
||
{
|
||
IEC104_AsduHeadInfo_t oldest;
|
||
const IEC104_AsduPriorityQueue_t *bucket = &queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL];
|
||
uint64_t next = Entry_Serial_Next(queue->next_entry_serial);
|
||
|
||
if (queue->overwritten_unacked_count != 0U &&
|
||
!Entry_Serial_Before(queue->overwritten_unacked_serial_begin, next)) {
|
||
return IEC104_ASDU_QUEUE_FULL;
|
||
}
|
||
if (bucket->entry_count != 0U) {
|
||
if (!Read_Entry_With_Expected_State(queue, IEC104_ASDU_PRIORITY_NORMAL, bucket->head_index,
|
||
bucket->entry_count > bucket->unsubmitted_count ?
|
||
QUEUE_ENTRY_RETAINED_UNACKED : QUEUE_ENTRY_PENDING, &oldest)) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
if (!Entry_Serial_Before(oldest.entry_id.entry_serial, next)) {
|
||
return IEC104_ASDU_QUEUE_FULL;
|
||
}
|
||
}
|
||
return IEC104_ASDU_QUEUE_OK;
|
||
}
|
||
|
||
/**
|
||
* 功能:计算当前条目之后的位置,完整条目按实际字节数跨尾部回绕。
|
||
* 输入参数:bucket——有效字节环;priority——已验证的优先级,用于确定条目头长;
|
||
* index/asdu_length——已验证条目头的位置和ASDU长度,头长加原文长度不超过环容量。
|
||
* 输出参数:无。
|
||
* 返回值:下一逻辑位置,可能是下一条目或队尾。
|
||
* 说明:条目之间没有填充;有后继条目时,由调用方另行验证其头和占用范围。
|
||
*/
|
||
static uint32_t Next_Entry_Index(const IEC104_AsduPriorityQueue_t *bucket, IEC104_AsduPriority_t priority,
|
||
uint32_t index, uint16_t asdu_length)
|
||
{
|
||
return Ring_Advance(bucket, index, Entry_Header_Length(priority) + (uint32_t)asdu_length);
|
||
}
|
||
|
||
/**
|
||
* 功能:将NORMAL条目头从待发状态改为保留原文等待ACK。
|
||
* 输入参数:bucket/index——有效NORMAL字节环和已验证的待发条目头位置。
|
||
* 输出参数:bucket->bytes中的entry_state改为NORMAL_HEADER_RETAINED_UNACKED,编号、长度及原文不变。
|
||
* 返回值:无;本函数不再检查身份和旧状态,调用方须先完成相关预检及发送交接。
|
||
* 说明:写入的是头内编码2;对应公开视图is_submitted为1,两者数值含义不同。
|
||
* 只更新条目头,待发计数和游标由Mark_Submitted随后更新;memcpy处理未对齐及回绕。
|
||
*/
|
||
static void Mark_Normal_Entry_Retained_Unacked(const IEC104_AsduPriorityQueue_t *bucket, uint32_t index)
|
||
{
|
||
IEC104_AsduNormalEntryHeader_t header;
|
||
|
||
Ring_Read(bucket, index, &header, sizeof(header));
|
||
header.entry_state = NORMAL_HEADER_RETAINED_UNACKED;
|
||
Ring_Write(bucket, index, &header, sizeof(header));
|
||
}
|
||
|
||
/**
|
||
* 功能:绑定调用方提供的两块字节数组,建立空的两级业务队列。
|
||
* 输入参数:high_bytes/normal_bytes——持续有效且互不重叠的数组;
|
||
* high_capacity/normal_capacity——字节容量,分别至少对应优先级的MIN_CAPACITY;
|
||
* connection_generation——0建立服务离线队列;非零值表示绑定到不复用的连接代次。
|
||
* 输出参数:queue——非NULL,仅成功时整体写入初始化结果。
|
||
* 返回值:OK表示成功,ERROR表示参数非法;失败不修改queue或字节数组。
|
||
* 说明:不分配内存,不清零原文字节;重新Init会丢弃旧条目引用,恢复业务应使用Rebind。
|
||
* 最小容量只保证最短ASDU;支持249字节ASDU时,至少配置本级HEADER_LEN+249字节。
|
||
* 数组在队列使用期间须持续有效;本函数只检查两起始地址不同,完整地址区间不重叠由调用方保证。
|
||
* 成功后NORMAL的next_entry_serial从1开始;所有条目计数、占用字节数和游标从0开始。
|
||
*/
|
||
IEC104_AsduQueueResult_t IEC104_AsduQueue_Init(
|
||
IEC104_AsduQueue_t *queue, uint8_t *high_bytes, uint32_t high_capacity,
|
||
uint8_t *normal_bytes, uint32_t normal_capacity, uint64_t connection_generation)
|
||
{
|
||
IEC104_AsduQueue_t prepared;
|
||
|
||
if (queue == NULL || high_bytes == NULL || normal_bytes == NULL || high_bytes == normal_bytes ||
|
||
high_capacity < IEC104_ASDU_HIGH_QUEUE_MIN_CAPACITY ||
|
||
normal_capacity < IEC104_ASDU_NORMAL_QUEUE_MIN_CAPACITY) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
memset(&prepared, 0, sizeof(prepared));
|
||
prepared.priority_queues[IEC104_ASDU_PRIORITY_HIGH].bytes = high_bytes;
|
||
prepared.priority_queues[IEC104_ASDU_PRIORITY_HIGH].byte_capacity = high_capacity;
|
||
prepared.priority_queues[IEC104_ASDU_PRIORITY_NORMAL].bytes = normal_bytes;
|
||
prepared.priority_queues[IEC104_ASDU_PRIORITY_NORMAL].byte_capacity = normal_capacity;
|
||
prepared.connection_generation = connection_generation;
|
||
prepared.next_entry_serial = 1U;
|
||
prepared.is_initialized = 1U;
|
||
*queue = prepared;
|
||
return IEC104_ASDU_QUEUE_OK;
|
||
}
|
||
|
||
/**
|
||
* 功能:读取连接代次及ASDU条目数,区分原文仍保留、原文仍保留且已提交待确认、原文已覆盖且待确认。
|
||
* 输入参数:queue——已初始化的业务队列。
|
||
* 输出参数:status——非NULL,仅OK时写入数值副本。
|
||
* 返回值:OK或ERROR;失败不修改输出。
|
||
* 说明:HIGH提交时出队,其retained_unacked_count恒为0,未确认HIGH数量由Manager发送记录维护。
|
||
* NORMAL的retained_unacked_count加overwritten_unacked_count得到NORMAL已提交待确认条目数。
|
||
* is_storage_empty只表示两个ASDU原文环为空,HIGH或NORMAL仍可能有对应I帧未ACK;查询不预留容量。
|
||
* 各count以ASDU条目为单位,不是字节数;只读取控制字段,不遍历或复制ASDU原文。
|
||
*/
|
||
IEC104_AsduQueueResult_t IEC104_AsduQueue_Get_Status(
|
||
const IEC104_AsduQueue_t *queue, IEC104_AsduQueueStatus_t *status)
|
||
{
|
||
IEC104_AsduQueueStatus_t prepared;
|
||
unsigned i;
|
||
|
||
if (!Queue_State_Valid(queue) || status == NULL) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
prepared.connection_generation = queue->connection_generation;
|
||
prepared.overwritten_unacked_count = queue->overwritten_unacked_count;
|
||
prepared.is_storage_empty = 1U;
|
||
for (i = 0U; i < IEC104_ASDU_PRIORITY_COUNT; ++i) {
|
||
const IEC104_AsduPriorityQueue_t *bucket = &queue->priority_queues[i];
|
||
prepared.entry_count[i] = bucket->entry_count;
|
||
prepared.retained_unacked_count[i] = bucket->entry_count - bucket->unsubmitted_count;
|
||
if (bucket->entry_count != 0U) {
|
||
prepared.is_storage_empty = 0U;
|
||
}
|
||
}
|
||
*status = prepared;
|
||
return IEC104_ASDU_QUEUE_OK;
|
||
}
|
||
|
||
/**
|
||
* 功能:核对身份及预期的队列旧状态后,释放指定优先级的物理队首。
|
||
* 输入参数:queue/priority/entry_id——目标队首;expected_queue_state——删除前预期的QUEUE_ENTRY_*状态。
|
||
* 输出参数:queue——成功时更新字节占用、条目数和相关游标,不清零原文字节。
|
||
* 返回值:OK或ERROR;先在副本中检查新状态,失败不修改原队列。
|
||
* 说明:expected_queue_state只是删除前的校验条件,不是出队原因,也不是要写入的新状态。
|
||
* 出队原因由调用方决定:HIGH发送交接完成、NORMAL有效ACK、主动取消或容量覆盖。
|
||
* 本函数不访问SendBuffer或验证ACK;调用方须先满足对应前提。只移除队首,不修改覆盖待确认元数据。
|
||
* HIGH只核对代次及0编号,调用方不得跨队首变化复用旧Peek结果。
|
||
* 多个调用路径共用此入口,因此自行复核队首;即使上层刚Peek过,也保留这里的身份和状态检查。
|
||
*/
|
||
static IEC104_AsduQueueResult_t Remove_Head_With_Expected_State(
|
||
IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority,
|
||
IEC104_AsduEntryId_t entry_id, uint8_t expected_queue_state)
|
||
{
|
||
IEC104_AsduHeadInfo_t info, next_info;
|
||
IEC104_AsduPriorityQueue_t prepared;
|
||
uint32_t next, size;
|
||
|
||
if (IEC104_AsduQueue_Peek_Head(queue, priority, &info) != IEC104_ASDU_QUEUE_OK ||
|
||
info.entry_id.connection_generation != entry_id.connection_generation ||
|
||
info.entry_id.entry_serial != entry_id.entry_serial || info.is_submitted != expected_queue_state) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
prepared = queue->priority_queues[priority];
|
||
size = Entry_Header_Length(priority) + (uint32_t)info.asdu_length;
|
||
next = Next_Entry_Index(&prepared, priority, prepared.head_index, info.asdu_length);
|
||
if (prepared.entry_count > 1U && !Read_Entry_With_Expected_State(queue, priority, next,
|
||
prepared.entry_count - prepared.unsubmitted_count > 1U ?
|
||
QUEUE_ENTRY_RETAINED_UNACKED : QUEUE_ENTRY_PENDING, &next_info)) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
prepared.used_bytes -= size;
|
||
prepared.head_index = next;
|
||
prepared.entry_count--;
|
||
/* 删除待发队首时同步推进待发区;删除待ACK前缀时,待发区的位置和数量保持不变。 */
|
||
if (expected_queue_state == QUEUE_ENTRY_PENDING) {
|
||
prepared.unsubmitted_count--;
|
||
prepared.unsubmitted_index = next;
|
||
}
|
||
/* 最后一条移除后满足空队列的归零约束,下次从下标0追加;不清零原文字节。 */
|
||
if (prepared.entry_count == 0U) {
|
||
prepared.head_index = 0U;
|
||
prepared.tail_index = 0U;
|
||
prepared.unsubmitted_index = 0U;
|
||
prepared.used_bytes = 0U;
|
||
}
|
||
if (!Bucket_Valid(&prepared, priority)) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
queue->priority_queues[priority] = prepared;
|
||
return IEC104_ASDU_QUEUE_OK;
|
||
}
|
||
|
||
/**
|
||
* 功能:复制一条ASDU到指定优先级的字节环队尾,建立尚未登记发送交接的待发条目。
|
||
* 输入参数:queue/connection_generation——有效队列及匹配代次;priority——HIGH或NORMAL;
|
||
* asdu/asdu_length——完整ASDU及6..249字节长度,输入区不得与队列及其存储重叠。
|
||
* 输出参数:queue及对应字节数组——仅OK时追加条目,NORMAL另分配条目编号;不存业务标识。
|
||
* 返回值:OK成功,NORMAL可能已覆盖旧条目且不另行通知;FULL表示HIGH空间不足或NORMAL编号窗口受阻;
|
||
* ERROR表示条目超过目标桶总容量,或参数、状态、身份/覆盖计数非法。
|
||
* 说明:FULL/ERROR不修改字节、控制字段或编号;生产者仅在OK后推进业务游标。
|
||
* FULL须先释放字节空间或处理占住编号窗口的旧条目/ACK;条目超过总容量不能等待ACK解决。
|
||
* NORMAL按最旧顺序覆盖,含已提交原文;对应I帧及确认记录不删除,仍须完成和等待ACK。
|
||
* 先在控制副本上验证所有必要删除,成功后才写新字节并发布状态,避免失败时丢弃旧业务。
|
||
* 本接口不分配N(S),不解释业务对象;未提交条目被覆盖时不会产生逐帧回执。
|
||
* OK后队列已有独立的ASDU副本,调用方可复用输入缓冲;条目占用为本级头长加实际ASDU长度。
|
||
*/
|
||
IEC104_AsduQueueResult_t IEC104_AsduQueue_Enqueue(
|
||
IEC104_AsduQueue_t *queue, uint64_t connection_generation, IEC104_AsduPriority_t priority,
|
||
const uint8_t *asdu, uint16_t asdu_length)
|
||
{
|
||
IEC104_AsduQueue_t prepared;
|
||
IEC104_AsduQueueResult_t result;
|
||
IEC104_AsduHeadInfo_t oldest;
|
||
IEC104_AsduNormalEntryHeader_t header;
|
||
IEC104_AsduPriorityQueue_t *bucket;
|
||
uint32_t size, index, available;
|
||
|
||
if (!Queue_State_Valid(queue) || connection_generation != queue->connection_generation ||
|
||
!Priority_Valid(priority) || (priority == IEC104_ASDU_PRIORITY_HIGH && connection_generation == 0U) ||
|
||
asdu == NULL || asdu_length < IEC104_ASDU_HEADER_LEN ||
|
||
asdu_length > IEC104_MAX_ASDU_LEN) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
bucket = &queue->priority_queues[priority];
|
||
size = Entry_Header_Length(priority) + (uint32_t)asdu_length;
|
||
/* 空队列也装不下属于配置与条目不匹配,不能报告为可等待空间恢复的FULL。 */
|
||
if (size > bucket->byte_capacity) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
available = bucket->byte_capacity - bucket->used_bytes;
|
||
if (size > available && priority == IEC104_ASDU_PRIORITY_HIGH) {
|
||
return IEC104_ASDU_QUEUE_FULL;
|
||
}
|
||
prepared = *queue;
|
||
bucket = &prepared.priority_queues[priority];
|
||
/* prepared只复制控制结构,bytes仍指向原数组;此处只模拟删除,不写共享字节。
|
||
* 任一旧头或后继非法时直接返回,原字节、游标及编号都保持原样。 */
|
||
while (size > bucket->byte_capacity - bucket->used_bytes) {
|
||
if (IEC104_AsduQueue_Peek_Head(&prepared, priority, &oldest) != IEC104_ASDU_QUEUE_OK ||
|
||
(oldest.is_submitted != 0U && prepared.overwritten_unacked_count == UINT32_MAX) ||
|
||
Remove_Head_With_Expected_State(&prepared, priority, oldest.entry_id,
|
||
oldest.is_submitted) != IEC104_ASDU_QUEUE_OK) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
if (oldest.is_submitted != 0U) {
|
||
if (prepared.overwritten_unacked_count == 0U) {
|
||
/* 首次覆盖已提交原文时建立区间;已有待ACK前缀时保留原下界。 */
|
||
prepared.overwritten_unacked_serial_begin = oldest.entry_id.entry_serial;
|
||
}
|
||
prepared.overwritten_unacked_count++;
|
||
prepared.overwritten_unacked_serial_end = oldest.entry_id.entry_serial;
|
||
}
|
||
}
|
||
/* 副本中的覆盖尚未发布;窗口检查失败时,原队列及原文字节仍保持调用前状态。 */
|
||
if (priority == IEC104_ASDU_PRIORITY_NORMAL) {
|
||
result = Check_Entry_Serial_Advance(&prepared);
|
||
if (result != IEC104_ASDU_QUEUE_OK) {
|
||
return result;
|
||
}
|
||
}
|
||
/* 总空闲已足够,直接分段写入;即使条目头本身跨尾部,也无需额外保留空间。 */
|
||
index = bucket->tail_index;
|
||
if (priority == IEC104_ASDU_PRIORITY_HIGH) {
|
||
Ring_Write(bucket, index, &asdu_length, sizeof(asdu_length));
|
||
} else {
|
||
memset(&header, 0, sizeof(header));
|
||
header.entry_serial = prepared.next_entry_serial;
|
||
header.entry_state = NORMAL_HEADER_PENDING;
|
||
/* 入口已检查长度为6..249,显式收窄为8位后写入条目头。 */
|
||
header.asdu_length = (uint8_t)asdu_length;
|
||
Ring_Write(bucket, index, &header, sizeof(header));
|
||
}
|
||
Ring_Write(bucket, Ring_Advance(bucket, index, Entry_Header_Length(priority)), asdu, asdu_length);
|
||
if (bucket->unsubmitted_count == 0U) {
|
||
bucket->unsubmitted_index = index;
|
||
}
|
||
bucket->tail_index = Ring_Advance(bucket, index, size);
|
||
bucket->used_bytes += size;
|
||
bucket->entry_count++;
|
||
bucket->unsubmitted_count++;
|
||
/* 只有NORMAL消耗编号;HIGH不被旧NORMAL条目占住的编号窗口阻塞。 */
|
||
if (priority == IEC104_ASDU_PRIORITY_NORMAL) {
|
||
prepared.next_entry_serial = Entry_Serial_Next(prepared.next_entry_serial);
|
||
}
|
||
*queue = prepared;
|
||
return IEC104_ASDU_QUEUE_OK;
|
||
}
|
||
|
||
/**
|
||
* 功能:查看指定优先级最旧的保留条目;NORMAL队首可能已登记交接并正在等待ACK。
|
||
* 输入参数:queue——有效队列;priority——HIGH或NORMAL。
|
||
* 输出参数:head_info——非NULL,仅OK时提供身份、原文借用及是否已提交。
|
||
* 返回值:OK有条目,EMPTY为空,ERROR表示参数、状态或条目非法。
|
||
* 说明:不移动任何游标;选择第一条未提交ASDU应调用Peek_Unsubmitted。
|
||
* HIGH队首也是待发区首条;NORMAL存在待ACK前缀时,两者可能不是同一条。
|
||
* 返回的是原文借用,不是副本;条目被覆盖或释放后不能继续使用返回的指针。
|
||
*/
|
||
IEC104_AsduQueueResult_t IEC104_AsduQueue_Peek_Head(
|
||
const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduHeadInfo_t *head_info)
|
||
{
|
||
const IEC104_AsduPriorityQueue_t *bucket;
|
||
|
||
if (!Queue_State_Valid(queue) || !Priority_Valid(priority) || head_info == NULL) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
bucket = &queue->priority_queues[priority];
|
||
if (bucket->entry_count == 0U) {
|
||
return IEC104_ASDU_QUEUE_EMPTY;
|
||
}
|
||
return Read_Entry_With_Expected_State(queue, priority, bucket->head_index,
|
||
bucket->entry_count > bucket->unsubmitted_count ?
|
||
QUEUE_ENTRY_RETAINED_UNACKED : QUEUE_ENTRY_PENDING, head_info) ?
|
||
IEC104_ASDU_QUEUE_OK : IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
|
||
/**
|
||
* 功能:只读查看指定优先级中尚未完成队列交接记账的第一条ASDU。
|
||
* 输入参数:queue——有效队列;priority——HIGH或NORMAL。
|
||
* 输出参数:head_info——调用方提供的非NULL指针;仅返回OK时写入,is_submitted为0。
|
||
* 返回值:OK表示找到当前允许交接的未提交条目;EMPTY表示没有可交接条目,不代表整个队列为空;
|
||
* ERROR表示参数、状态或条目非法。
|
||
* 说明:通过unsubmitted_index常数时间定位,不遍历NORMAL中已提交但未ACK的前缀;
|
||
* 不移除条目,不改变记账状态;Manager交接过程中的内部复核也使用本接口。
|
||
* 本队列不查询SendBuffer,调用方不能把is_submitted为0当作发送模块尚未接手的证明。
|
||
* NORMAL全部已提交或已因EOF停止选取时返回EMPTY,Peek_Head仍可查看实际保留原文。
|
||
* 离线查询可返回代次0的NORMAL原文视图,真正提交发送须先Rebind到非零代次。
|
||
*/
|
||
IEC104_AsduQueueResult_t IEC104_AsduQueue_Peek_Unsubmitted(
|
||
const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduHeadInfo_t *head_info)
|
||
{
|
||
const IEC104_AsduPriorityQueue_t *bucket;
|
||
|
||
if (!Queue_State_Valid(queue) || !Priority_Valid(priority) || head_info == NULL) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
bucket = &queue->priority_queues[priority];
|
||
if (bucket->unsubmitted_count == 0U ||
|
||
(priority == IEC104_ASDU_PRIORITY_NORMAL && queue->normal_send_limited != 0U)) {
|
||
return IEC104_ASDU_QUEUE_EMPTY;
|
||
}
|
||
return Read_Entry_With_Expected_State(queue, priority, bucket->unsubmitted_index,
|
||
QUEUE_ENTRY_PENDING, head_info) ?
|
||
IEC104_ASDU_QUEUE_OK : IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
|
||
/**
|
||
* 功能:把Peek或Get_Entry返回的最多两段ASDU复制到调用方连续缓冲。
|
||
* 输入参数:head_info——借用仍有效的条目描述;capacity——输出缓冲实际字节容量。
|
||
* 输出参数:destination——非NULL、与两段原文及描述不重叠的可写数组,仅OK时写入。
|
||
* 返回值:OK复制了asdu_length字节;ERROR表示描述或容量非法,此时不改输出。
|
||
* 说明:只检查分段描述,不重新查询队列或核对代次;调用方保证条目未覆盖、释放、Detach、Rebind或Abort。
|
||
* 连续ASDU也可使用本接口;正常发送仅在跨界时才需要额外拼接。
|
||
* 复制结果仅含ASDU,不添加APCI;复制成功也不会移除条目或登记发送交接。
|
||
*/
|
||
IEC104_AsduQueueResult_t IEC104_AsduQueue_Copy_Asdu(
|
||
const IEC104_AsduHeadInfo_t *head_info, uint8_t *destination, uint16_t capacity)
|
||
{
|
||
if (head_info == NULL || destination == NULL ||
|
||
head_info->asdu_length < IEC104_ASDU_HEADER_LEN || head_info->asdu_length > IEC104_MAX_ASDU_LEN ||
|
||
head_info->asdu_first == NULL || head_info->asdu_first_length == 0U ||
|
||
head_info->asdu_first_length > head_info->asdu_length || capacity < head_info->asdu_length ||
|
||
(head_info->asdu_first_length == head_info->asdu_length ?
|
||
head_info->asdu_second != NULL : head_info->asdu_second == NULL)) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
memcpy(destination, head_info->asdu_first, head_info->asdu_first_length);
|
||
if (head_info->asdu_second != NULL) {
|
||
memcpy(destination + head_info->asdu_first_length, head_info->asdu_second,
|
||
head_info->asdu_length - head_info->asdu_first_length);
|
||
}
|
||
return IEC104_ASDU_QUEUE_OK;
|
||
}
|
||
|
||
/**
|
||
* 功能:在发送模块接手后登记队列交接;HIGH消费当前原文,NORMAL保留原文并推进待发游标。
|
||
* 输入参数:queue/priority——目标队列与优先级;entry_id——NORMAL完整身份,HIGH为当前代次及0编号。
|
||
* 输出参数:queue——HIGH释放队首;NORMAL更新头内状态、unsubmitted_count和unsubmitted_index。
|
||
* 返回值:OK或ERROR;失败不修改队列。
|
||
* 说明:调用方须先把完整I帧交给SendBuffer并建立Manager记录,再串行调用本函数。
|
||
* 调用前队列仍记录PENDING;HIGH不在环内建立已交接状态,直接消费原文完成记账。
|
||
* 本函数不验证发送缓冲是否接手,也不构帧或执行发送,不能提前调用来丢弃待发数据。
|
||
* HIGH原文借用在成功出队后失效;SendBuffer报文及Manager记录继续有效,仍须完成发送和ACK。
|
||
* HIGH仍要求entry_serial等于0,用于拒绝非法标识;它不能区分同一代次中先后入队的两条HIGH。
|
||
*/
|
||
IEC104_AsduQueueResult_t IEC104_AsduQueue_Mark_Submitted(
|
||
IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduEntryId_t entry_id)
|
||
{
|
||
IEC104_AsduHeadInfo_t info, next_info;
|
||
IEC104_AsduPriorityQueue_t *bucket;
|
||
uint32_t next;
|
||
|
||
if (entry_id.connection_generation == 0U ||
|
||
IEC104_AsduQueue_Peek_Unsubmitted(queue, priority, &info) != IEC104_ASDU_QUEUE_OK ||
|
||
info.entry_id.connection_generation != entry_id.connection_generation ||
|
||
info.entry_id.entry_serial != entry_id.entry_serial) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
if (priority == IEC104_ASDU_PRIORITY_HIGH) {
|
||
/* 发送模块已接手;PENDING仅核对删除前的队列旧状态,随后消费原文完成交接记账。 */
|
||
return Remove_Head_With_Expected_State(queue, priority, entry_id, QUEUE_ENTRY_PENDING);
|
||
}
|
||
bucket = &queue->priority_queues[priority];
|
||
next = Next_Entry_Index(bucket, priority, bucket->unsubmitted_index, info.asdu_length);
|
||
if (bucket->unsubmitted_count > 1U &&
|
||
!Read_Entry_With_Expected_State(queue, priority, next, QUEUE_ENTRY_PENDING, &next_info)) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
/* 登记NORMAL交接:头编码改为保留待ACK,待发游标移到下一条;原文不出队。 */
|
||
Mark_Normal_Entry_Retained_Unacked(bucket, bucket->unsubmitted_index);
|
||
bucket->unsubmitted_count--;
|
||
bucket->unsubmitted_index = bucket->unsubmitted_count == 0U ? bucket->tail_index : next;
|
||
return IEC104_ASDU_QUEUE_OK;
|
||
}
|
||
|
||
/**
|
||
* 功能:完成NORMAL下一条有效ACK的存储清理,兼容原文已被覆盖;HIGH的ACK只更新Manager记录。
|
||
* 输入参数:queue——有效队列;priority——只允许NORMAL;entry_id——有效ACK对应Manager记录的真实身份。
|
||
* 输出参数:queue——仍有原文时释放匹配队首;已覆盖时更新待确认计数和区间边界,不移除当前队首。
|
||
* 返回值:OK或ERROR;ERROR表示代次、编号区间、覆盖计数或已提交队首身份不符合约束。
|
||
* 说明:仅供Manager在Link验证ACK后按发送顺序调用;本函数不验证协议N(R)。
|
||
* 覆盖后不另存完整身份列表,只有数量和循环编号区间;准确身份及顺序由Manager独立记录保证。
|
||
* 覆盖前缀清完才允许确认仍保留的队首,最后一个覆盖身份必须与边界相等。
|
||
* 只清理ASDU存储及覆盖计数,不自行推进协议确认边界或释放Manager回执记录。
|
||
*/
|
||
IEC104_AsduQueueResult_t IEC104_AsduQueue_Confirm_Next_Normal_Entry(
|
||
IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduEntryId_t entry_id)
|
||
{
|
||
uint64_t confirmed_offset, overwritten_span;
|
||
|
||
if (!Queue_State_Valid(queue) || priority != IEC104_ASDU_PRIORITY_NORMAL ||
|
||
entry_id.connection_generation != queue->connection_generation ||
|
||
!Entry_Serial_Before(entry_id.entry_serial, queue->next_entry_serial)) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
if (queue->overwritten_unacked_count != 0U) {
|
||
/* 以区间下界为起点比较循环距离,使跨UINT64_MAX的覆盖区间仍能按同一规则检查。 */
|
||
confirmed_offset = Entry_Serial_Distance(queue->overwritten_unacked_serial_begin, entry_id.entry_serial);
|
||
overwritten_span = Entry_Serial_Distance(queue->overwritten_unacked_serial_begin,
|
||
queue->overwritten_unacked_serial_end);
|
||
/*
|
||
* ACK须落在区间内,且只有最后一次ACK才可命中上界;后续编号位置须容得下剩余计数。
|
||
* 此处只检查区间和数量,具体ACK身份是否属于发送记录仍由Manager保证。
|
||
*/
|
||
if (confirmed_offset > overwritten_span ||
|
||
((queue->overwritten_unacked_count == 1U) !=
|
||
(entry_id.entry_serial == queue->overwritten_unacked_serial_end)) ||
|
||
queue->overwritten_unacked_count - 1U > overwritten_span - confirmed_offset) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
/* 原文可能已被新条目占用,绝不能把当前物理队首当成旧帧释放。 */
|
||
queue->overwritten_unacked_count--;
|
||
/* 排除已确认及更早的编号;下界可落在编号间隔中,不一定就是下一待ACK条目的编号。 */
|
||
queue->overwritten_unacked_serial_begin = Entry_Serial_Next(entry_id.entry_serial);
|
||
if (queue->overwritten_unacked_count == 0U) {
|
||
/* 最后一个覆盖条目已确认,清除两端边界,释放这段编号窗口。 */
|
||
queue->overwritten_unacked_serial_end = 0U;
|
||
queue->overwritten_unacked_serial_begin = 0U;
|
||
}
|
||
return IEC104_ASDU_QUEUE_OK;
|
||
}
|
||
return Remove_Head_With_Expected_State(queue, priority, entry_id, QUEUE_ENTRY_RETAINED_UNACKED);
|
||
}
|
||
|
||
/**
|
||
* 功能:在发送交接开始前,主动丢弃仍处于队列待发状态的物理队首ASDU。
|
||
* 输入参数:queue/priority——目标字节环;entry_id——NORMAL完整身份,HIGH为当前代次及0编号。
|
||
* 输出参数:queue——成功时释放队首并推进待发游标;不清零原文字节,被丢弃条目的借用失效。
|
||
* 返回值:OK或ERROR;队首为空、身份不符或已登记交接时ERROR且不修改队列。
|
||
* 说明:只删除物理队首,不能越过NORMAL的待ACK前缀去删除后面的待发条目。
|
||
* 本函数只检查队列记账状态,无法判断SendBuffer是否已经接手;调用方必须在发送交接前使用,
|
||
* 不得插入Manager的交接过程,也不能用它撤回已编号I帧。断线恢复使用Rebind。
|
||
*/
|
||
IEC104_AsduQueueResult_t IEC104_AsduQueue_Discard_Unsubmitted_Head(
|
||
IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, IEC104_AsduEntryId_t entry_id)
|
||
{
|
||
return Remove_Head_With_Expected_State(queue, priority, entry_id, QUEUE_ENTRY_PENDING);
|
||
}
|
||
|
||
/**
|
||
* 功能:按保留顺序查看指定偏移的原始ASDU,供检查和恢复使用。
|
||
* 输入参数:queue/priority——目标字节环;offset——从最旧保留条目起的条目序位,0为队首,单位不是字节。
|
||
* 输出参数:head_info——非NULL,仅OK时写入对应条目及其已提交状态。
|
||
* 返回值:OK找到条目,EMPTY表示偏移超出已有条目,ERROR表示参数或经过的条目非法。
|
||
* 说明:从队首读取offset+1条记录,逐条计算边界;正常发送选取使用Peek_Unsubmitted直接定位。
|
||
* 同时包含NORMAL的待ACK前缀与待发后缀;输出原文的借用有效期与Peek一致,不生成副本。
|
||
*/
|
||
IEC104_AsduQueueResult_t IEC104_AsduQueue_Get_Entry_By_Offset(
|
||
const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority,
|
||
uint32_t offset, IEC104_AsduHeadInfo_t *head_info)
|
||
{
|
||
const IEC104_AsduPriorityQueue_t *bucket;
|
||
IEC104_AsduHeadInfo_t info;
|
||
uint32_t i, index;
|
||
|
||
if (!Queue_State_Valid(queue) || !Priority_Valid(priority) || head_info == NULL) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
bucket = &queue->priority_queues[priority];
|
||
if (offset >= bucket->entry_count) {
|
||
return IEC104_ASDU_QUEUE_EMPTY;
|
||
}
|
||
index = bucket->head_index;
|
||
for (i = 0U; i <= offset; ++i) {
|
||
if (!Read_Entry_With_Expected_State(queue, priority, index,
|
||
i < bucket->entry_count - bucket->unsubmitted_count ?
|
||
QUEUE_ENTRY_RETAINED_UNACKED : QUEUE_ENTRY_PENDING, &info)) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
index = Next_Entry_Index(bucket, priority, index, info.asdu_length);
|
||
}
|
||
*head_info = info;
|
||
return IEC104_ASDU_QUEUE_OK;
|
||
}
|
||
|
||
/**
|
||
* 功能:查询指定优先级当前未占用的字节数。
|
||
* 输入参数:queue/priority——目标字节环。
|
||
* 输出参数:free_bytes——非NULL,仅OK时写入容量减used_bytes。
|
||
* 返回值:OK或ERROR,失败不改输出。
|
||
* 说明:空闲不少于本级HEADER_LEN+ASDU长度时有足够容量;NORMAL另检查编号窗口。
|
||
* 空闲不足时NORMAL仍可能通过覆盖旧原文入队;本查询不预留空间,也不能代替Enqueue的结果检查。
|
||
*/
|
||
IEC104_AsduQueueResult_t IEC104_AsduQueue_Get_Free_Bytes(
|
||
const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority, uint32_t *free_bytes)
|
||
{
|
||
if (!Queue_State_Valid(queue) || !Priority_Valid(priority) || free_bytes == NULL) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
*free_bytes = queue->priority_queues[priority].byte_capacity - queue->priority_queues[priority].used_bytes;
|
||
return IEC104_ASDU_QUEUE_OK;
|
||
}
|
||
|
||
/**
|
||
* 功能:逐条验证完整占用区及提交分界;NORMAL另验证条目身份顺序和头内状态。
|
||
* 输入参数:queue/priority——控制字段与优先级均已验证的队列和目标桶。
|
||
* 输出参数:无,不修改控制字段或原文字节。
|
||
* 返回值:全部条目恰好组成有效FIFO返回1,否则0。
|
||
* 说明:Rebind在更新控制字段前验证两个桶;remaining防止错误长度绕回已读区。
|
||
* Read_Entry_With_Expected_State先限制各编号到next的距离,再按循环先后验证相邻条目,允许UINT64_MAX接1。
|
||
* 耗时随条目数增长;只检查存储布局、身份及记账状态,不解析ASDU业务对象。
|
||
*/
|
||
static int Bucket_Entries_Valid(const IEC104_AsduQueue_t *queue, IEC104_AsduPriority_t priority)
|
||
{
|
||
const IEC104_AsduPriorityQueue_t *bucket = &queue->priority_queues[priority];
|
||
IEC104_AsduHeadInfo_t info;
|
||
uint32_t index = bucket->head_index;
|
||
uint32_t remaining = bucket->used_bytes;
|
||
uint32_t retained_unacked_count = bucket->entry_count - bucket->unsubmitted_count;
|
||
uint64_t previous_serial = 0U;
|
||
uint32_t i, size;
|
||
|
||
for (i = 0U; i < bucket->entry_count; ++i) {
|
||
if (remaining < Entry_Header_Length(priority) + IEC104_ASDU_HEADER_LEN ||
|
||
(i == retained_unacked_count && index != bucket->unsubmitted_index) ||
|
||
!Read_Entry_With_Expected_State(queue, priority, index,
|
||
i < retained_unacked_count ? QUEUE_ENTRY_RETAINED_UNACKED : QUEUE_ENTRY_PENDING, &info)) {
|
||
return 0;
|
||
}
|
||
size = Entry_Header_Length(priority) + (uint32_t)info.asdu_length;
|
||
if (size > remaining || (priority == IEC104_ASDU_PRIORITY_NORMAL && i != 0U &&
|
||
!Entry_Serial_Before(previous_serial, info.entry_id.entry_serial))) {
|
||
return 0;
|
||
}
|
||
remaining -= size;
|
||
previous_serial = info.entry_id.entry_serial;
|
||
index = Ring_Advance(bucket, index, size);
|
||
}
|
||
return remaining == 0U && index == bucket->tail_index;
|
||
}
|
||
|
||
/**
|
||
* 功能:将HIGH桶恢复为空环,供Reset_High和Rebind在预检成功后调用。
|
||
* 输入参数:queue——已验证的队列,调用方已满足清空当前HIGH业务的前提。
|
||
* 输出参数:HIGH的头尾下标、字节占用、条目数及待发游标归零,所有HIGH原文借用失效。
|
||
* 返回值:无;此内部函数不重复检查参数或连接代次。
|
||
* 说明:保留借用数组及容量,不清零原文字节;NORMAL、队列代次和编号进度均不在此处修改。
|
||
*/
|
||
static void Reset_High_Bucket(IEC104_AsduQueue_t *queue)
|
||
{
|
||
IEC104_AsduPriorityQueue_t *bucket = &queue->priority_queues[IEC104_ASDU_PRIORITY_HIGH];
|
||
|
||
bucket->head_index = 0U;
|
||
bucket->tail_index = 0U;
|
||
bucket->used_bytes = 0U;
|
||
bucket->entry_count = 0U;
|
||
bucket->unsubmitted_index = 0U;
|
||
bucket->unsubmitted_count = 0U;
|
||
}
|
||
|
||
/**
|
||
* 功能:清空当前连接的HIGH待发条目,供Manager初始化、受理新STOP及从STOPPED受理START时调用。
|
||
* 输入参数:queue——有效队列;connection_generation——匹配当前绑定的连接代次。
|
||
* 输出参数:仅HIGH桶的条目计数、字节占用及游标清零;所有HIGH原文借用失效。
|
||
* 返回值:OK或ERROR;非法状态或旧代次不修改队列。
|
||
* 说明:HIGH已提交原文已经出队,因此不影响当前I帧、发送记录、ACK义务和NORMAL数据。
|
||
* 不影响NORMAL的next_entry_serial;HIGH只有代次,没有可跨队首变化保存的条目身份。
|
||
* 这是同一代次内的清理,不执行重连,也不会把NORMAL待ACK条目重新变成待发。
|
||
*/
|
||
IEC104_AsduQueueResult_t IEC104_AsduQueue_Reset_High(
|
||
IEC104_AsduQueue_t *queue, uint64_t connection_generation)
|
||
{
|
||
if (!Queue_State_Valid(queue) || connection_generation != queue->connection_generation) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
Reset_High_Bucket(queue);
|
||
return IEC104_ASDU_QUEUE_OK;
|
||
}
|
||
|
||
/* EOF后不再可能收到新I帧的ACK,停止选取全部尚未交接的NORMAL,原文留给下一连接。
|
||
* 不改已提交条目、SendBuffer或HIGH;当前已编号帧仍由发送模块完成。
|
||
* 入队、覆盖、取消条目及重复调用都不解除限制,只有Detach/Rebind恢复选取。 */
|
||
IEC104_AsduQueueResult_t IEC104_AsduQueue_Limit_Normal_Send(
|
||
IEC104_AsduQueue_t *queue, uint64_t connection_generation)
|
||
{
|
||
if (!Queue_State_Valid(queue) || connection_generation == 0U ||
|
||
connection_generation != queue->connection_generation) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
queue->normal_send_limited = 1U;
|
||
return IEC104_ASDU_QUEUE_OK;
|
||
}
|
||
|
||
/* Detach/Rebind共用的绑定转换:入口已核对旧状态及目标代次,先验证全部条目再统一修改。
|
||
* 0表示解除绑定,非零表示新会话;均恢复现存NORMAL,清空HIGH和旧连接独有的记账。 */
|
||
static IEC104_AsduQueueResult_t Reset_Connection_Binding(
|
||
IEC104_AsduQueue_t *queue, uint64_t connection_generation)
|
||
{
|
||
unsigned i;
|
||
IEC104_AsduPriorityQueue_t *normal;
|
||
IEC104_AsduNormalEntryHeader_t header;
|
||
uint32_t index, submitted_count, entry;
|
||
|
||
for (i = 0U; i < IEC104_ASDU_PRIORITY_COUNT; ++i) {
|
||
if (!Bucket_Entries_Valid(queue, (IEC104_AsduPriority_t)i)) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
}
|
||
/* 全部预检通过后才写头。后续没有可失败分支,保持ERROR不改字节的契约。 */
|
||
normal = &queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL];
|
||
index = normal->head_index;
|
||
submitted_count = normal->entry_count - normal->unsubmitted_count;
|
||
for (entry = 0U; entry < submitted_count; ++entry) {
|
||
Ring_Read(normal, index, &header, sizeof(header));
|
||
header.entry_state = NORMAL_HEADER_PENDING;
|
||
Ring_Write(normal, index, &header, sizeof(header));
|
||
index = Next_Entry_Index(normal, IEC104_ASDU_PRIORITY_NORMAL, index, (uint16_t)header.asdu_length);
|
||
}
|
||
/* NORMAL全部保留条目恢复为待发;HIGH直接清空。 */
|
||
Reset_High_Bucket(queue);
|
||
queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL].unsubmitted_index =
|
||
queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL].head_index;
|
||
queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL].unsubmitted_count =
|
||
queue->priority_queues[IEC104_ASDU_PRIORITY_NORMAL].entry_count;
|
||
queue->connection_generation = connection_generation;
|
||
queue->overwritten_unacked_count = 0U;
|
||
queue->overwritten_unacked_serial_end = 0U;
|
||
queue->overwritten_unacked_serial_begin = 0U;
|
||
queue->normal_send_limited = 0U;
|
||
return IEC104_ASDU_QUEUE_OK;
|
||
}
|
||
|
||
/**
|
||
* 功能:结束旧会话绑定,立即恢复服务级离线缓存状态。
|
||
* 输入参数:queue——有效队列;connection_generation——要解除的当前非零代次。
|
||
* 返回值:OK保留现存NORMAL并切换到代次0;ERROR不修改队列。
|
||
* 说明:先结束旧I/O及Manager,处理已知ACK事实;随后允许新NORMAL以代次0入队。
|
||
* 清空HIGH、覆盖待ACK记账及EOF限制,不恢复已被容量覆盖的原文,也不复制或移动ASDU。
|
||
*/
|
||
IEC104_AsduQueueResult_t IEC104_AsduQueue_Detach(
|
||
IEC104_AsduQueue_t *queue, uint64_t connection_generation)
|
||
{
|
||
if (!Queue_State_Valid(queue) || connection_generation == 0U ||
|
||
connection_generation != queue->connection_generation) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
return Reset_Connection_Binding(queue, 0U);
|
||
}
|
||
|
||
/**
|
||
* 功能:把现存NORMAL绑定到新会话,全部恢复为待发,供新STARTDT完成后重新组帧。
|
||
* 输入参数:queue——有效队列,可以尚未绑定;connection_generation——未使用过的非零新代次。
|
||
* 返回值:OK或ERROR;先完整验证两个环,ERROR不修改控制字段或字节。
|
||
* 说明:跨连接不复用代次由调用方保证;旧I/O及Manager必须先结束,已知ACK事实先处理。
|
||
* 保留原文顺序、serial及next_entry_serial,旧身份和借用失效;清空HIGH、覆盖记账和EOF限制。
|
||
* 未ACK不等于对端未收到,原文重发可能重复;容量已覆盖的原文不能恢复。
|
||
*/
|
||
IEC104_AsduQueueResult_t IEC104_AsduQueue_Rebind(
|
||
IEC104_AsduQueue_t *queue, uint64_t connection_generation)
|
||
{
|
||
if (!Queue_State_Valid(queue) || connection_generation == 0U ||
|
||
connection_generation == queue->connection_generation) {
|
||
return IEC104_ASDU_QUEUE_ERROR;
|
||
}
|
||
return Reset_Connection_Binding(queue, connection_generation);
|
||
}
|
||
|
||
/**
|
||
* 功能:清除业务队列控制结构,明确丢弃所有保留条目的引用。
|
||
* 输入参数:queue——待终止对象,允许NULL。
|
||
* 输出参数:queue——非NULL时清零;调用方提供的字节数组不清零、不释放。
|
||
* 返回值:无。
|
||
* 说明:先完成业务回收并结束所有借用;Manager.Abort不会隐式调用此接口。
|
||
* 清零后队列不再处于已初始化状态,重新使用须Init;如需保留NORMAL原文恢复连接,
|
||
* 应结束旧Manager后使用Rebind,不应先调用本接口丢弃队列引用。
|
||
*/
|
||
void IEC104_AsduQueue_Abort(IEC104_AsduQueue_t *queue)
|
||
{
|
||
if (queue != NULL) {
|
||
memset(queue, 0, sizeof(*queue));
|
||
}
|
||
}
|