Files
STM32F4-Base/IEC60870-Slave/104/IEC104_Receiver.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

300 lines
15 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.
/**
* 每连接独立的IEC-104流式接收器。
* 每个实例只保留一帧;完整帧经APCI及ASDU最低长度检查后暂停输入,显式释放后再处理剩余字节。
* Codec负责报文格式,Link/SendManager负责序号、ACK和启停;本模块不改变链路状态。
*/
#include "IEC104_Receiver.h"
#include "IEC60870_Codec.h"
#include <stddef.h>
#include <string.h>
/**
* 功能:丢弃当前候选帧,将接收器恢复到等待帧头的阶段。
* 输入参数:receiver——非NULL接收器,调用方已核对初始化状态和连接代次。
* 输出参数:receiver——接收长度、期望长度及完整帧标志清零。
* 返回值:无。
* 说明:保留连接代次、初始化标志和缓冲字节;旧字节不再属于有效报文。
* 如此前已交付完整帧视图,该视图随本次清除失效。
*/
static void Clear_Progress(IEC104_Receiver_t *receiver)
{
receiver->received_length = 0U;
receiver->expected_length = 0U;
receiver->is_frame_ready = 0U;
}
/**
* 功能:检查接收器是否已初始化,以及本次操作是否属于当前连接代次。
* 输入参数:receiver——待检查对象,允许NULL;connection_generation——本次操作所属代次。
* 输出参数:无。
* 返回值:对象已初始化且非零代次相同返回1,否则0。
* 说明:不检查组帧进度,因此Reset可以在进度字段异常时仍清除当前报文。
* 代次由连接层分配;本模块只比较代次,不负责生成或递增。
*/
static int Session_Valid(const IEC104_Receiver_t *receiver, uint64_t connection_generation)
{
return receiver != NULL && receiver->is_initialized == 1U &&
connection_generation != 0U && receiver->connection_generation == connection_generation;
}
/**
* 功能:检查连接身份、接收阶段、帧头和长度字段是否自洽。
* 输入参数:receiver——待检查对象,允许NULL;connection_generation——本次操作所属代次。
* 输出参数:无。
* 返回值:状态及已知边界合法返回1,否则0。
* 说明:未收到长度域时expected_length必须为0;收到后必须与长度域一致。
* 只有收齐报文时才能置完整帧标志。本函数不解析APCI,也不检查链路序号。
*/
static int Receiver_Valid(const IEC104_Receiver_t *receiver, uint64_t connection_generation)
{
if (!Session_Valid(receiver, connection_generation) || receiver->is_frame_ready > 1U) {
return 0;
}
/* 尚未收到帧头:没有候选帧,也不能存在期望长度或完整帧标志。 */
if (receiver->received_length == 0U) {
return receiver->expected_length == 0U && receiver->is_frame_ready == 0U;
}
if (receiver->buffer[0] != IEC104_START_BYTE) {
return 0;
}
/* 仅收到0x68:长度域尚未知,不能用缓冲中遗留的第二字节判断报文长度。 */
if (receiver->received_length == 1U) {
return receiver->expected_length == 0U && receiver->is_frame_ready == 0U;
}
/* 长度域不包含起始字节和长度字节,因此APDU总长为长度域加2。 */
if (receiver->buffer[1] < IEC104_MIN_LENGTH_FIELD || receiver->buffer[1] > IEC104_MAX_LENGTH_FIELD ||
receiver->expected_length != (uint16_t)(2U + receiver->buffer[1]) ||
receiver->expected_length > sizeof(receiver->buffer) ||
receiver->received_length > receiver->expected_length) {
return 0;
}
/* 完整帧等待释放;未就绪的候选帧则必须仍缺少至少一个字节。 */
return receiver->is_frame_ready != 0U ?
receiver->received_length == receiver->expected_length :
receiver->received_length < receiver->expected_length;
}
/* 成帧及借用共用格式检查;I帧在交给Link/Manager前检查已知对象的最低载荷长度。
* APCI解析也供发送缓冲使用,ASDU接收检查在此单独组合,不改变发送构帧路径。 */
static int Parse_Received_Frame(const IEC104_Receiver_t *receiver, IEC104_ApciInfo_t *apci)
{
/* 本接收器属于104传输,显式使用104标准应用层布局。 */
static const IEC60870_AppLayerParameters_t parameters = IEC104_APP_LAYER_PARAMETERS_DEFAULT;
IEC60870_AsduInfo_t asdu_info;
if (!IEC104_Parse_APCI(receiver->buffer, receiver->received_length, apci)) {
return 0;
}
return apci->frame_type != IEC104_FRAME_TYPE_I ||
IEC60870_Parse_ASDU_Header(&parameters, apci->asdu, apci->asdu_len, &asdu_info);
}
/**
* 功能:初始化独立接收器,并绑定一个非零连接代次。
* 输入参数:receiver——调用方提供的存储;connection_generation——连接层分配的代次。
* 输出参数:receiver——成功时清零全部字段和缓冲,再记录代次及初始化标志。
* 返回值:OK表示成功;ERROR表示指针为空或代次为0,对象保持不变。
* 说明:首次使用及新连接换代都调用本函数;重新初始化会使已有帧视图失效。
* 调用前须结束旧连接及其I/O,不得与Feed或视图读取并发执行。
*/
IEC104_ReceiverResult_t IEC104_Receiver_Init(
IEC104_Receiver_t *receiver, uint64_t connection_generation)
{
if (receiver == NULL || connection_generation == 0U) {
return IEC104_RECEIVER_ERROR;
}
memset(receiver, 0, sizeof(*receiver));
receiver->connection_generation = connection_generation;
receiver->is_initialized = 1U;
return IEC104_RECEIVER_OK;
}
/**
* 功能:读取接收进度,避免上层直接依赖接收器内部的成帧标志和长度字段。
* 输入参数:receiver/connection_generation——当前对象和代次;status——不重叠的输出存储。
* 返回值:OK写数值快照;ERROR参数、身份或进度非法且不改输出。查询不消费字节或释放原文。
*/
IEC104_ReceiverResult_t IEC104_Receiver_Get_Status(
const IEC104_Receiver_t *receiver, uint64_t connection_generation, IEC104_ReceiverStatus_t *status)
{
IEC104_ReceiverStatus_t prepared;
if (status == NULL || !Receiver_Valid(receiver, connection_generation)) {
return IEC104_RECEIVER_ERROR;
}
prepared.received_length = receiver->received_length;
prepared.expected_length = receiver->expected_length;
prepared.has_complete_frame = receiver->is_frame_ready;
*status = prepared;
return IEC104_RECEIVER_OK;
}
/**
* 功能:在连接收尾时清除接收缓冲及绑定,所有旧视图随即失效。
* 输入参数:receiver——待清理对象,允许NULL;调用方必须已经结束旧I/O及字节借用。
* 返回值:无。不调用Link或socket;之后重新使用须Init,不能直接Feed或Reset。
*/
void IEC104_Receiver_Abort(IEC104_Receiver_t *receiver)
{
if (receiver != NULL) {
memset(receiver, 0, sizeof(*receiver));
}
}
/**
* 功能:将按序到达的字节片段拼成一帧完整APDU,检查APCI及ASDU最低长度。
* 输入参数:receiver——已初始化接收器;connection_generation——本次输入所属代次;
* data——输入片段首地址,length为0时允许NULL;length——输入字节数。
* 输出参数:consumed——非NULL,写入本次实际消费数,格式错误时包括触发错误的字节;
* receiver——保存半帧,或保留一帧已通过格式及最低长度检查的完整报文。
* 返回值:FRAME_READY表示已有完整帧;IN_PROGRESS表示输入用完但尚无完整帧;
* FRAME_ERROR表示候选帧格式非法且已丢弃;ERROR表示参数、代次或内部状态非法。
* 说明:每次最多完成一帧;未消费的尾部由调用方保留,当前帧释放后再按原顺序递入。
* ERROR时接收器不变,有效consumed输出为0;输入、输出及接收器存储不得重叠。
* 这里只检查报文格式,不判断ASDU业务合法性、链路序号及启停状态。
*/
IEC104_ReceiverResult_t IEC104_Receiver_Feed(
IEC104_Receiver_t *receiver, uint64_t connection_generation,
const uint8_t *data, uint32_t length, uint32_t *consumed)
{
IEC104_ApciInfo_t apci;
if (consumed == NULL) {
return IEC104_RECEIVER_ERROR;
}
*consumed = 0U;
if (!Receiver_Valid(receiver, connection_generation) || (length != 0U && data == NULL)) {
return IEC104_RECEIVER_ERROR;
}
/* 上一帧尚未释放时暂停消费,保证调用方正在读取的帧内容不会被覆盖。 */
if (receiver->is_frame_ready != 0U) {
return IEC104_RECEIVER_FRAME_READY;
}
while (*consumed < length) {
uint32_t copy_length;
/* 阶段一:帧边界的首字节必须为0x68;不跳过垃圾重新同步。 */
if (receiver->received_length == 0U) {
uint8_t byte = data[(*consumed)++];
if (byte != IEC104_START_BYTE) {
Clear_Progress(receiver);
return IEC104_RECEIVER_FRAME_ERROR;
}
receiver->buffer[0] = byte;
receiver->received_length = 1U;
continue;
}
/* 阶段二:读取长度域。允许4..253,对应包含起始字节和长度字节的总长6..255。 */
if (receiver->received_length == 1U) {
uint8_t byte = data[(*consumed)++];
if (byte < IEC104_MIN_LENGTH_FIELD || byte > IEC104_MAX_LENGTH_FIELD) {
/* 非法长度字节已消费;立即返回,让调用方决定如何处理尚未消费的尾部。 */
Clear_Progress(receiver);
return IEC104_RECEIVER_FRAME_ERROR;
}
receiver->buffer[1] = byte;
receiver->received_length = 2U;
receiver->expected_length = (uint16_t)(2U + byte);
continue;
}
/* 阶段三:按长度收齐控制域及可选ASDU;报文体中的0x68不再当作帧头。
* 复制量取“当前帧缺少的字节数”和“本次剩余输入”中的较小值,避免吞掉下一帧。 */
copy_length = (uint32_t)(receiver->expected_length - receiver->received_length);
if (copy_length > length - *consumed) {
copy_length = length - *consumed;
}
memcpy(receiver->buffer + receiver->received_length, data + *consumed, copy_length);
receiver->received_length = (uint16_t)(receiver->received_length + copy_length);
*consumed += copy_length;
if (receiver->received_length == receiver->expected_length) {
/* 长度收齐后仍须检查控制域、ASDU公共头和已知对象的最低载荷长度。 */
if (!Parse_Received_Frame(receiver, &apci)) {
/* 整个非法候选帧已消费,只清除组帧进度,不继续搜索其内部字节。 */
Clear_Progress(receiver);
return IEC104_RECEIVER_FRAME_ERROR;
}
/* 完整帧留在内部缓冲,直到调用方Peek、处理并显式Release。 */
receiver->is_frame_ready = 1U;
return IEC104_RECEIVER_FRAME_READY;
}
}
/* 包括length为0且无完整帧的情况;保留当前进度,等待后续片段。 */
return IEC104_RECEIVER_IN_PROGRESS;
}
/**
* 功能:取得当前完整帧的只读借用视图,不消费或释放报文。
* 输入参数:receiver——已初始化接收器;connection_generation——本次读取所属代次。
* 输出参数:frame——非NULL且不与receiver重叠,仅成功时写入原文地址、长度和APCI结果。
* 返回值:FRAME_READY表示视图有效;EMPTY表示尚无完整帧;ERROR表示参数或状态非法。
* 说明:允许重复读取,但同一帧被业务受理后不能重复分派;本函数不修改接收器。
* 视图引用内部缓冲,成功Release、Reset或重新Init后失效;需要长期保存时须复制。
*/
IEC104_ReceiverResult_t IEC104_Receiver_Peek_Frame(
const IEC104_Receiver_t *receiver, uint64_t connection_generation,
IEC104_ReceivedFrame_t *frame)
{
IEC104_ReceivedFrame_t prepared;
if (frame == NULL || !Receiver_Valid(receiver, connection_generation)) {
return IEC104_RECEIVER_ERROR;
}
if (receiver->is_frame_ready == 0U) {
return IEC104_RECEIVER_EMPTY;
}
memset(&prepared, 0, sizeof(prepared));
/* 重新解析到调用方视图,避免在Receiver对象中缓存指向自身缓冲的指针。 */
if (!Parse_Received_Frame(receiver, &prepared.apci)) {
return IEC104_RECEIVER_ERROR;
}
prepared.connection_generation = receiver->connection_generation;
prepared.apdu = receiver->buffer;
prepared.apdu_length = receiver->received_length;
/* 临时结果全部准备成功后再写输出,避免失败时向调用方交付不完整的视图。 */
*frame = prepared;
return IEC104_RECEIVER_FRAME_READY;
}
/**
* 功能:释放已处理的完整帧,允许接收器继续消费后续输入。
* 输入参数:receiver——已初始化接收器;connection_generation——本次释放所属代次。
* 输出参数:receiver——成功时清除组帧进度;缓冲字节、连接代次及初始化标志保留。
* 返回值:OK表示已释放;EMPTY表示没有完整帧,已有半帧继续保留;ERROR表示状态非法。
* 说明:释放后旧视图立即失效;此处只复用接收空间,不发送ACK、不推进链路状态。
* 业务暂未受理当前帧时不能调用本函数,应保留当前帧等待后续处理。
*/
IEC104_ReceiverResult_t IEC104_Receiver_Release_Frame(
IEC104_Receiver_t *receiver, uint64_t connection_generation)
{
if (!Receiver_Valid(receiver, connection_generation)) {
return IEC104_RECEIVER_ERROR;
}
if (receiver->is_frame_ready == 0U) {
return IEC104_RECEIVER_EMPTY;
}
Clear_Progress(receiver);
return IEC104_RECEIVER_OK;
}
/**
* 功能:显式丢弃当前半帧或完整帧,重新等待帧头。
* 输入参数:receiver——已初始化接收器;connection_generation——本次复位所属代次。
* 输出参数:receiver——成功时清除组帧进度,保留代次、初始化标志和缓冲字节。
* 返回值:OK表示复位完成;ERROR表示对象未初始化或代次非法,对象保持不变。
* 说明:与Release不同,无需已经收齐一帧,也允许清除异常进度;已有视图随之失效。
* 只复位接收器,不复位Link;新连接换代须调用Init,不能用Reset替代。
*/
IEC104_ReceiverResult_t IEC104_Receiver_Reset(
IEC104_Receiver_t *receiver, uint64_t connection_generation)
{
/* 此处仅核对连接身份,避免进度字段异常时反而无法执行清除。 */
if (!Session_Valid(receiver, connection_generation)) {
return IEC104_RECEIVER_ERROR;
}
Clear_Progress(receiver);
return IEC104_RECEIVER_OK;
}