Files
STM32F4-Base/IEC60870-Slave/104/IEC104_AsduQueue.c
T
STF 4c55544f05
CI / L0 静态门禁 (pre-commit) (push) Failing after 45s
CI / L0.5 主机侧单测 (MSVC) (push) Failing after 42s
通信协议代码
2026-10-26 08:31:48 -04:00

1032 lines
56 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 子站两级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));
}
}