3.27_433:实现并验证RF433模块接收相应指令:新增UART路由核心模块,使程序能响应RF433/RS485指令,并向UART2输出LOG(RS485由于硬件原因未验证)

This commit is contained in:
2026-03-27 16:21:00 +08:00
parent 71027ebc46
commit c809273bd9
78 changed files with 7188 additions and 2811 deletions

View File

@ -12,7 +12,7 @@
* 2. 完善的安全防护(缓冲区边界检查、超时重置、字符过滤)
* 3. 异或校验FF特权后门
* 4. 支持RL、DI、ECHO指令
*
*
* 修订历史:
* v1.1 - 修复审查报告高危-6、中危-7/8
******************************************************************************
@ -27,37 +27,148 @@
#include <stdio.h> // snprintf
#include <stdlib.h> // atoi
/*==============================================================================
* 调试宏定义
*============================================================================*/
/* DEBUG_CMD_PARSER: 调试日志开关置1时启用解析过程日志输出 */
#define DEBUG_CMD_PARSER 1
#if DEBUG_CMD_PARSER
/* 调试日志宏,带模块前缀"[CMD]"方便过滤 */
#define DEBUG_LOG(fmt, ...) UART2_Print_Printf("[CMD] " fmt "\r\n", ##__VA_ARGS__)
#else
#define DEBUG_LOG(fmt, ...)
#endif
/*==============================================================================
* 解析器状态机定义
*============================================================================*/
/**
* @brief 指令解析器状态枚举
* @note 状态机各状态定义,描述指令解析的完整生命周期
*
* 状态转换图:
*
* [空闲] ---'$'---> [解析命令]
* [解析命令] ---','---> [解析参数1]
* [解析命令] ---'*'---> [解析校验和]
* [解析参数1] ---','---> [解析参数2]
* [解析参数1] ---'*'---> [解析校验和]
* [解析参数2] ---'*'---> [解析校验和]
* [解析校验和] ---'\n'---> [完成]
* [完成] ---'$'---> [解析命令]
* 任何状态 ---超时---> [空闲]
*
* 各状态说明:
* - PARSE_IDLE: 等待帧起始符'$',处于空闲状态
* - PARSE_CMD: 正在解析命令字段(直到遇到','或'*')
* - PARSE_PARAM1: 正在解析第一个参数(直到遇到','或'*')
* - PARSE_PARAM2: 正在解析第二个参数(直到遇到'*')
* - PARSE_CHECKSUM: 正在解析校验和(两个十六进制字符直到'\n')
* - PARSE_COMPLETE: 完整帧已解析完成,等待处理
*/
typedef enum {
PARSE_IDLE,
PARSE_CMD,
PARSE_PARAM1,
PARSE_PARAM2,
PARSE_CHECKSUM,
PARSE_COMPLETE
PARSE_IDLE = 0, /**< 空闲状态,等待帧起始 */
PARSE_CMD, /**< 正在解析命令字段 */
PARSE_PARAM1, /**< 正在解析第一个参数 */
PARSE_PARAM2, /**< 正在解析第二个参数 */
PARSE_CHECKSUM, /**< 正在解析校验和 */
PARSE_COMPLETE /**< 解析完成,可处理 */
} parse_state_t;
/*==============================================================================
* 数据结构定义
*============================================================================*/
/**
* @brief 解析器上下文数据结构
* @note 保存指令解析的完整状态信息,包括当前状态、已解析字段和统计计数
*
* 设计目的:
* 解析器需要记忆多个中间状态:当前解析状态、已接收的各字段内容、
* 校验和累加值、上下文字符索引等。这些信息集中保存在此结构体中,
* 实现无状态的逐字节处理(Stateless Byte-by-Byte Processing)模式。
*
* 字段说明:
* - state: 当前解析状态机的状态
* - frame: 已解析完成的指令帧结构(cmd_frame_t类型)
* - field_index: 当前字段的字符写入位置索引
* - checksum_acc: 校验和累加器,逐字节异或得到
* - cs_buffer: 接收的校验和字符缓存(两个HEX字符)
* - cs_index: 校验和字符缓存的当前写入位置
* - last_rx_tick: 上次接收到数据的时间戳(用于超时检测)
* - error_count: 解析错误累计次数
* - valid_count: 有效指令帧累计次数
*/
typedef struct {
parse_state_t state;
cmd_frame_t frame;
uint8_t field_index;
uint8_t checksum_acc;
uint8_t cs_buffer[2];
uint8_t cs_index;
uint32_t last_rx_tick;
uint32_t error_count;
uint32_t valid_count;
parse_state_t state; /**< 解析状态机当前状态 */
cmd_frame_t frame; /**< 已解析指令帧数据 */
uint8_t field_index; /**< 当前字段的字符写入位置 */
uint8_t checksum_acc; /**< 校验和累加器(异或运算) */
uint8_t cs_buffer[2]; /**< 接收的校验和字符缓存 */
uint8_t cs_index; /**< 校验和字符缓存写入位置 */
uint32_t last_rx_tick; /**< 上次接收时间戳(毫秒) */
uint32_t error_count; /**< 解析错误累计次数 */
uint32_t valid_count; /**< 有效帧累计次数 */
} parser_context_t;
/*==============================================================================
* 全局变量定义
*============================================================================*/
/**
* @brief 解析器上下文实例
* @note static修饰确保仅本文件内可访问保存解析过程全部状态
*/
static parser_context_t ctx;
/**
* @brief 响应回调函数指针
* @note 用于将响应发送到正确的源端口
*/
static cmd_response_callback_t g_response_callback = NULL;
/**
* @brief 当前源端口ID
* @note 记录当前正在解析的指令来自哪个端口
*/
static uint8_t g_current_source_port = 0;
/*==============================================================================
* 内部静态函数声明
*============================================================================*/
static void reset_parser(void);
static bool is_valid_cmd_char(char c);
static bool is_valid_param_char(char c);
static uint8_t hex_char_to_val(char c);
static uint8_t hex_to_byte(char high, char low);
static uint8_t calc_checksum(const char *data, uint8_t len);
static void send_response_ok(const char *content);
static void send_response_err(const char *err_code);
static bool is_str_empty(const char *str);
static bool is_str_numeric(const char *str);
static void process_cmd_frame(const cmd_frame_t *frame);
/*==============================================================================
* 内部辅助函数实现
*============================================================================*/
/**
* @brief 重置解析器到初始状态
* @note 清除所有上下文信息,准备接收新指令帧
*
* @param 无
* @return 无
*
* 重置内容:
* - 状态恢复为PARSE_IDLE
* - 字段索引清零
* - 校验和累加器清零
* - 校验和缓存索引清零
* - cmd_frame结构体全部清零
*
* 调用时机:
* - 解析出错时
* - 解析完成处理后
* - 接收到完整帧后的空闲期
*/
static void reset_parser(void)
{
ctx.state = PARSE_IDLE;
@ -67,16 +178,59 @@ static void reset_parser(void)
memset(&ctx.frame, 0, sizeof(ctx.frame));
}
/**
* @brief 验证命令字符合法性
* @note 命令字段只能包含大写字母和数字
*
* @param c: 待验证的字符(输入)
* @return bool: true=合法false=非法
*
* 过滤规则:
* - 允许: 'A'-'Z', '0'-'9'
* - 禁止: 小写字母、标点符号、控制字符等
*
* 安全意义:
* - 限制命令字符集可防止注入攻击
* - 简化解析逻辑,避免处理边界情况
*/
static bool is_valid_cmd_char(char c)
{
return isupper((unsigned char)c) || isdigit((unsigned char)c);
}
/**
* @brief 验证参数字符合法性
* @note 参数字段可包含大部分可打印字符,但排除特殊分隔符
*
* @param c: 待验证的字符(输入)
* @return bool: true=合法false=非法
*
* 过滤规则:
* - 允许: isprint()返回true的字符但不包括'*'
* - 禁止: '*'、'\r'、'\n'及其他不可打印字符
*
* 特殊说明:
* - '*'是校验和起始分隔符,不能出现在参数中
* - '\r'和'\n'是帧结束符
*/
static bool is_valid_param_char(char c)
{
return isprint((unsigned char)c) && c != '*' && c != '\r' && c != '\n';
}
/**
* @brief 十六进制字符转换为数值
* @note 将单个十六进制字符('0'-'9','A'-'F')转换为对应的4位数值
*
* @param c: 十六进制字符(输入)
* @return uint8_t: 转换后的数值(0-15)非法字符返回0
*
* 转换规则:
* '0'-'9' -> 0-9
* 'A'-'F' -> 10-15
* 'a'-'f' -> 10-15 (小写也支持)
* 其他字符 -> 0 (默认处理)
*/
static uint8_t hex_char_to_val(char c)
{
if (c >= '0' && c <= '9') return c - '0';
@ -85,11 +239,37 @@ static uint8_t hex_char_to_val(char c)
return 0;
}
/**
* @brief 将两个十六进制字符转换为一个字节
* @note 高位字符在前,地位字符在后,组合成完整的字节值
*
* @param high: 高位十六进制字符(输入)
* @param low: 低位十六进制字符(输入)
* @return uint8_t: 组合后的字节值(0-255)
*
* 计算公式result = (high_nibble << 4) | low_nibble
*
* 示例:
* hex_to_byte('A', 'F') 返回 0xAF (十进制175)
*/
static uint8_t hex_to_byte(char high, char low)
{
return (hex_char_to_val(high) << 4) | hex_char_to_val(low);
}
/**
* @brief 计算异或校验和
* @note 对输入数据的每个字节执行异或运算,生成校验码
*
* @param data: 待计算校验和的数据缓冲区(输入)
* @param len: 数据长度,以字节为单位(输入)
* @return uint8_t: 校验和(异或结果)
*
* 算法原理:
* 遍历数据缓冲区,将每个字节与累加器进行异或操作。
* 初始累加器为0最终结果为所有字节的异或和。
* XOR的特性a^a=0, a^0=a, 具有可逆性,适合简单校验
*/
static uint8_t calc_checksum(const char *data, uint8_t len)
{
uint8_t cs = 0;
@ -99,40 +279,99 @@ static uint8_t calc_checksum(const char *data, uint8_t len)
return cs;
}
/**
* @brief 发送成功响应消息
* @note 构造并发送指令执行成功的响应帧
*
* @param content: 响应内容字符串指针(输入)
* @return 无
*
* 响应帧格式:
* $OK,<content>*<checksum>\r\n
*
* 校验和计算范围:
* 从'$'之后到'*'之前的全部内容(不含$和*)
*
* 使用示例:
* send_response_ok("RL,1") -> $OK,RL,1*XX\r\n
*/
static void send_response_ok(const char *content)
{
char msg[64];
uint8_t cs;
int len = snprintf(msg, sizeof(msg), "$OK,%s*", content);
cs = calc_checksum(msg + 1, len - 1);
snprintf(msg + len, sizeof(msg) - len, "%02X\r\n", cs);
UART2_Print_String(msg);
if (g_response_callback != NULL) {
g_response_callback(g_current_source_port, msg);
} else {
UART2_Print_String(msg);
}
}
/**
* @brief 发送错误响应消息
* @note 构造并发送指令执行失败的响应帧
*
* @param err_code: 错误代码字符串(输入),如"PARAM"、"CS"、"CMD"
* @return 无
*
* 响应帧格式:
* $ERR,<err_code>*<checksum>\r\n
*
* 错误代码含义:
* - "PARAM": 参数格式或值非法
* - "CS": 校验和不匹配
* - "CMD": 命令无法识别
*/
static void send_response_err(const char *err_code)
{
char msg[32];
uint8_t cs;
int len = snprintf(msg, sizeof(msg), "$ERR,%s*", err_code);
cs = calc_checksum(msg + 1, len - 1);
snprintf(msg + len, sizeof(msg) - len, "%02X\r\n", cs);
UART2_Print_String(msg);
if (g_response_callback != NULL) {
g_response_callback(g_current_source_port, msg);
} else {
UART2_Print_String(msg);
}
}
/**
* @brief 判断字符串是否为空
* @note 检查指针是否为NULL或首字符是否为'\0'
*
* @param str: 待检查的字符串指针(输入)
* @return bool: true=空字符串或NULLfalse=非空
*/
static bool is_str_empty(const char *str)
{
return (str == NULL || str[0] == '\0');
}
/**
* @brief 判断字符串是否全为数字
* @note 验证字符串中的所有字符是否都是十进制数字
*
* @param str: 待检查的字符串指针(输入)
* @return bool: true=全为数字false=包含非数字字符或为空
*
* 实现逻辑:
* 从字符串开头遍历到'\0'检查每个字符是否满足isdigit()。
* 一旦遇到非数字字符立即返回false。
*/
static bool is_str_numeric(const char *str)
{
/* 空字符串检查 */
if (is_str_empty(str)) {
return false;
}
/* 逐字符检查是否为数字 */
while (*str) {
if (!isdigit((unsigned char)*str)) {
return false;
@ -142,160 +381,322 @@ static bool is_str_numeric(const char *str)
return true;
}
/**
* @brief 指令帧处理函数
* @note 根据解析出的命令类型分发到相应的处理函数
*
* @param frame: 已解析完成的指令帧结构指针(输入)
* @return 无
*
* 支持的指令列表:
* - RL <relay_id> <state>: 控制继电器开关
* - DI [channel]: 查询数字输入状态
* - ECHO: 回显测试指令
*
* 处理流程:
* 1. 调试日志输出指令详情
* 2. 根据cmd字段分发到对应处理分支
* 3. 参数合法性校验
* 4. 调用业务层函数执行操作
* 5. 发送成功/失败响应
*/
static void process_cmd_frame(const cmd_frame_t *frame)
{
DEBUG_LOG("CMD=%s P1=%s P2=%s CS=%02X/%02X %s",
/*----------------------------------------------------------
* 调试日志:打印完整指令信息
*----------------------------------------------------------*/
DEBUG_LOG("CMD=%s P1=%s P2=%s CS=%02X/%02X %s",
frame->cmd, frame->param1, frame->param2,
frame->received_cs, frame->calculated_cs,
frame->skip_checksum ? "(skip)" : "");
/*----------------------------------------------------------
* 继电器控制指令: RL <state>
* 格式: $RL,<state>*<checksum>
* 说明: 单路继电器state为0(关)或1(开)
*----------------------------------------------------------*/
if (strcmp(frame->cmd, "RL") == 0) {
if (!is_str_numeric(frame->param1) || !is_str_numeric(frame->param2)) {
/* 参数合法性检查param1必须为数字 */
if (!is_str_numeric(frame->param1)) {
send_response_err("PARAM");
DEBUG_LOG("Invalid RL params: not numeric");
DEBUG_LOG("Invalid RL param: not numeric");
return;
}
int relay_id = atoi(frame->param1);
int state = atoi(frame->param2);
if (relay_id >= 1 && relay_id <= 4 && (state == 0 || state == 1)) {
Relay_SetState(relay_id, state ? true : false);
/* 将参数字符串转换为整数 */
int state = atoi(frame->param1);
/* 状态值范围检查: 0或1 */
if (state == 0 || state == 1) {
/* 调用业务层函数设置继电器状态 */
Relay_SetState(state ? true : false);
/* 构造并发送成功响应 */
char resp[32];
snprintf(resp, sizeof(resp), "RL,%d,%d", relay_id, state);
snprintf(resp, sizeof(resp), "RL,%d", state);
send_response_ok(resp);
DEBUG_LOG("Relay %d -> %s", relay_id, state ? "ON" : "OFF");
DEBUG_LOG("Relay -> %s", state ? "ON" : "OFF");
} else {
/* 参数值超出有效范围 */
send_response_err("PARAM");
DEBUG_LOG("Invalid RL params: id=%d state=%d", relay_id, state);
DEBUG_LOG("Invalid RL param: state=%d", state);
}
}
/*----------------------------------------------------------
* 数字输入查询指令: DI [channel]
* 格式1(查询全部): $DI*<checksum> 或 $DI,0*<checksum>
* 格式2(查询单通道): $DI,<channel>*<checksum>
*----------------------------------------------------------*/
else if (strcmp(frame->cmd, "DI") == 0) {
/*----------------------------------------------------------
* 分支1: 查询全部通道状态
* param1为空或为"0"时触发
*----------------------------------------------------------*/
if (is_str_empty(frame->param1) || strcmp(frame->param1, "0") == 0) {
/* 获取所有通道状态的组合掩码 */
uint8_t states = IO_Monitor_GetAllStates();
/* 构造响应消息将四路状态按位展开为ASCII字符 */
char resp[32];
snprintf(resp, sizeof(resp), "DI,%d%d%d%d",
snprintf(resp, sizeof(resp), "DI,%d%d%d%d",
(states >> 0) & 1, (states >> 1) & 1,
(states >> 2) & 1, (states >> 3) & 1);
send_response_ok(resp);
DEBUG_LOG("DI all states: 0x%02X", states);
}
/*----------------------------------------------------------
* 分支2: 查询指定单通道状态
* param1为数字时触发
*----------------------------------------------------------*/
else if (is_str_numeric(frame->param1)) {
int channel = atoi(frame->param1);
/* 通道编号范围检查: 1-4 */
if (channel >= 1 && channel <= 4) {
/* 获取该通道的当前状态(内部使用0-base索引) */
uint8_t state = IO_Monitor_GetState(channel - 1);
char resp[32];
snprintf(resp, sizeof(resp), "DI,%d,%d", channel, state);
send_response_ok(resp);
DEBUG_LOG("DI%d = %d", channel, state);
} else {
/* 通道编号超出范围 */
send_response_err("PARAM");
DEBUG_LOG("Invalid DI channel: %d", channel);
}
}
/*----------------------------------------------------------
* 分支3: 参数格式非法
*----------------------------------------------------------*/
else {
send_response_err("PARAM");
DEBUG_LOG("Invalid DI param: not numeric");
}
}
/*----------------------------------------------------------
* 回显测试指令: ECHO
* 格式: $ECHO*<checksum>
* 用途: 测试通信链路是否正常
*----------------------------------------------------------*/
else if (strcmp(frame->cmd, "ECHO") == 0) {
send_response_ok("ECHO");
DEBUG_LOG("ECHO response sent");
}
/*----------------------------------------------------------
* 未知命令处理
*----------------------------------------------------------*/
else {
send_response_err("CMD");
DEBUG_LOG("Unknown command: %s", frame->cmd);
}
}
/*==============================================================================
* 公共函数实现
*============================================================================*/
/**
* @brief 指令解析器初始化
* @note 在系统启动时调用,初始化解析器上下文
*
* @param 无
* @return 无
*
* 初始化内容:
* - 使用memset将整个上下文结构清零
* - 将状态显式设置为PARSE_IDLE
*
* 注意:
* 全局变量ctx本身是static且初始化为{0}
* 此函数调用memset是确保明确初始化
* 防止未来新增字段时出现未初始化问题
*/
void CmdParser_Init(void)
{
memset(&ctx, 0, sizeof(ctx));
ctx.state = PARSE_IDLE;
DEBUG_LOG("Init OK");
}
/**
* @brief 向解析器输入一个字节
* @note 核心解析函数,实现状态机逻辑,应对每个接收字节调用一次
*
* @param byte: 待解析的接收字节(输入)
* @param current_tick: 当前系统时间戳,毫秒单位(输入)
* @return 无
*
* 算法说明 - 状态机核心逻辑:
* 本函数是整个解析器的核心,实现了有限状态机(FSM)。
* 每次调用处理一个字节,根据当前状态(ctx.state)和输入字节
* 决定状态转换和动作。
*
* 状态转换详细说明:
*
* [PARSE_IDLE]:
* - byte='$' -> reset_parser(), state=PARSE_CMD
* - 其他字节 -> 忽略保持IDLE
*
* [PARSE_CMD]:
* - byte=',' -> 字段结束state=PARSE_PARAM1field_index=0
* - byte='*' -> 字段结束state=PARSE_CHECKSUMcs_index=0
* - is_valid_cmd_char(byte) -> 写入cmd缓冲区checksum_acc^=byte
* - 其他情况 -> error_count++, reset_parser()
*
* [PARSE_PARAM1]:
* - byte=',' -> 字段结束state=PARSE_PARAM2field_index=0
* - byte='*' -> 字段结束state=PARSE_CHECKSUMcs_index=0
* - is_valid_param_char(byte) -> 写入param1缓冲区checksum_acc^=byte
* - 其他情况 -> error_count++, reset_parser()
*
* [PARSE_PARAM2]:
* - byte='*' -> 字段结束state=PARSE_CHECKSUMcs_index=0
* - is_valid_param_char(byte) -> 写入param2缓冲区checksum_acc^=byte
* - 其他情况 -> error_count++, reset_parser()
*
* [PARSE_CHECKSUM]:
* - byte='\n' -> 校验和接收完成验证并设置state=PARSE_COMPLETE
* - byte='\r' -> 忽略(回车符)
* - 其他情况 -> 写入cs_buffer[cs_index++]最多2个字符
*
* [PARSE_COMPLETE]:
* - reset_parser()
* - byte='$' -> state=PARSE_CMD (允许连续帧)
*
* 超时处理:
* 如果当前状态不是IDLE且距离上次接收超过PARSE_TIMEOUT_MS
* 则判定为接收超时重置解析器到IDLE状态
*/
void CmdParser_FeedByte(uint8_t byte, uint32_t current_tick)
{
/*----------------------------------------------------------
* 超时检测
* 如果接收到新字节且距离上次接收已超时,则重置解析器
*----------------------------------------------------------*/
if (ctx.state != PARSE_IDLE && ctx.state != PARSE_COMPLETE) {
if (current_tick - ctx.last_rx_tick >= PARSE_TIMEOUT_MS) {
ctx.error_count++;
DEBUG_LOG("Timeout, reset parser");
reset_parser();
/* 超时后如果收到'$',立即开始解析新帧 */
if (byte == '$') {
ctx.state = PARSE_CMD;
}
return;
}
}
/* 更新最后接收时间戳 */
ctx.last_rx_tick = current_tick;
/*----------------------------------------------------------
* 状态机主分支:根据当前状态处理输入字节
*----------------------------------------------------------*/
switch (ctx.state) {
/*----------------------------------------------------------
* PARSE_IDLE: 等待帧起始符'$'
*----------------------------------------------------------*/
case PARSE_IDLE:
if (byte == '$') {
reset_parser();
ctx.state = PARSE_CMD;
}
break;
/*----------------------------------------------------------
* PARSE_CMD: 解析命令字段
*----------------------------------------------------------*/
case PARSE_CMD:
if (byte == ',') {
/* 命令字段结束符遇到逗号转入参数1解析 */
ctx.frame.cmd[ctx.field_index] = '\0';
ctx.state = PARSE_PARAM1;
ctx.field_index = 0;
} else if (byte == '*') {
/* 无参数命令,遇到'*'直接转入校验和解析 */
ctx.frame.cmd[ctx.field_index] = '\0';
ctx.state = PARSE_CHECKSUM;
ctx.field_index = 0;
ctx.cs_index = 0;
} else if (is_valid_cmd_char(byte)) {
/* 有效的命令字符,写入缓冲区 */
if (ctx.field_index < CMD_MAX_LEN - 1) {
ctx.frame.cmd[ctx.field_index++] = byte;
ctx.checksum_acc ^= byte;
ctx.checksum_acc ^= byte; /* 累加到校验和 */
} else {
/* 缓冲区溢出,命令过长 */
ctx.error_count++;
reset_parser();
}
} else {
/* 无效字符,命令只能包含大写字母和数字 */
ctx.error_count++;
reset_parser();
}
break;
/*----------------------------------------------------------
* PARSE_PARAM1: 解析第一个参数
*----------------------------------------------------------*/
case PARSE_PARAM1:
if (byte == ',') {
/* 参数1结束符遇到逗号转入参数2解析 */
ctx.frame.param1[ctx.field_index] = '\0';
ctx.state = PARSE_PARAM2;
ctx.field_index = 0;
} else if (byte == '*') {
/* 参数1结束符遇到'*'直接转入校验和解析 */
ctx.frame.param1[ctx.field_index] = '\0';
ctx.state = PARSE_CHECKSUM;
ctx.field_index = 0;
ctx.cs_index = 0;
} else if (is_valid_param_char(byte)) {
/* 有效参数字符,写入缓冲区 */
if (ctx.field_index < PARAM_MAX_LEN - 1) {
ctx.frame.param1[ctx.field_index++] = byte;
ctx.checksum_acc ^= byte;
} else {
/* 缓冲区溢出,参数过长 */
ctx.error_count++;
reset_parser();
}
} else {
/* 无效字符 */
ctx.error_count++;
reset_parser();
}
break;
/*----------------------------------------------------------
* PARSE_PARAM2: 解析第二个参数
*----------------------------------------------------------*/
case PARSE_PARAM2:
if (byte == '*') {
/* 参数2结束符遇到'*'转入校验和解析 */
ctx.frame.param2[ctx.field_index] = '\0';
ctx.state = PARSE_CHECKSUM;
ctx.field_index = 0;
ctx.cs_index = 0;
} else if (is_valid_param_char(byte)) {
/* 有效参数字符,写入缓冲区 */
if (ctx.field_index < PARAM_MAX_LEN - 1) {
ctx.frame.param2[ctx.field_index++] = byte;
ctx.checksum_acc ^= byte;
@ -308,34 +709,51 @@ void CmdParser_FeedByte(uint8_t byte, uint32_t current_tick)
reset_parser();
}
break;
/*----------------------------------------------------------
* PARSE_CHECKSUM: 解析校验和
*----------------------------------------------------------*/
case PARSE_CHECKSUM:
if (byte == '\n') {
/* 帧结束符'\n'到达,校验和解析完成 */
/* 将两个十六进制字符转换为字节 */
ctx.frame.received_cs = hex_to_byte(ctx.cs_buffer[0], ctx.cs_buffer[1]);
ctx.frame.calculated_cs = ctx.checksum_acc;
/* 0xFF作为特殊值跳过校验和验证(后门) */
ctx.frame.skip_checksum = (ctx.frame.received_cs == 0xFF);
if (ctx.frame.skip_checksum ||
/*----------------------------------------------------------
* 校验和验证
*----------------------------------------------------------*/
if (ctx.frame.skip_checksum ||
ctx.frame.received_cs == ctx.frame.calculated_cs) {
/* 校验通过,设置帧有效标志 */
ctx.frame.valid = true;
ctx.state = PARSE_COMPLETE;
ctx.valid_count++;
} else {
/* 校验失败,发送错误响应 */
ctx.error_count++;
DEBUG_LOG("Checksum error: recv=%02X calc=%02X",
DEBUG_LOG("Checksum error: recv=%02X calc=%02X",
ctx.frame.received_cs, ctx.frame.calculated_cs);
send_response_err("CS");
reset_parser();
}
} else if (byte != '\r') {
/* 忽略回车符'\r',接收校验和字符(最多2个HEX) */
if (ctx.cs_index < 2) {
ctx.cs_buffer[ctx.cs_index++] = byte;
}
}
break;
/*----------------------------------------------------------
* PARSE_COMPLETE: 解析完成状态
*----------------------------------------------------------*/
case PARSE_COMPLETE:
/* 重置解析器,准备接收下一帧 */
reset_parser();
/* 如果立即收到'$',允许连续帧解析 */
if (byte == '$') {
ctx.state = PARSE_CMD;
}
@ -343,6 +761,27 @@ void CmdParser_FeedByte(uint8_t byte, uint32_t current_tick)
}
}
/**
* @brief 指令解析器任务函数
* @note 在主循环中周期性调用,处理已解析完成的指令帧
*
* @param 无
* @return 无
*
* 工作模式:
* CmdParser_FeedByte()负责接收字节并解析,
* 此函数负责在帧解析完成后执行相应的业务处理。
*
* 处理流程:
* 1. 检查是否处于PARSE_COMPLETE状态
* 2. 检查帧是否valid
* 3. 调用process_cmd_frame()执行指令
* 4. 重置解析器到IDLE状态
*
* 注意:
* 此设计将"接收解析"与"业务处理"分离,
* 避免了中断处理函数中执行复杂业务逻辑
*/
void CmdParser_Task(void)
{
if (ctx.state == PARSE_COMPLETE && ctx.frame.valid) {
@ -351,6 +790,21 @@ void CmdParser_Task(void)
}
}
/**
* @brief 查询是否存在已完成的指令帧
* @note 非阻塞查询接口,供外部模块检查是否有待处理帧
*
* @param frame: 帧数据输出指针如果不为NULL则复制帧数据(输出)
* @return bool: true=存在有效帧false=无有效帧
*
* 使用场景:
* 外部模块(如主循环)可轮询此函数,
* 当返回true时获取帧数据进行处理
*
* 注意:
* 此函数只是查询,不自动清除帧数据。
* 帧数据的清除需要调用CmdParser_Acknowledge()
*/
bool CmdParser_HasCompleteFrame(cmd_frame_t *frame)
{
if (ctx.state == PARSE_COMPLETE && ctx.frame.valid) {
@ -362,17 +816,81 @@ bool CmdParser_HasCompleteFrame(cmd_frame_t *frame)
return false;
}
/**
* @brief 确认并清除已完成的指令帧
* @note 在处理完一帧后调用,重置解析器到初始状态
*
* @param 无
* @return 无
*
* 使用说明:
* 当外部模块通过CmdParser_HasCompleteFrame获取帧数据后
* 处理完毕应调用此函数清除状态,防止重复处理
*/
void CmdParser_Acknowledge(void)
{
reset_parser();
}
/**
* @brief 获取解析错误累计次数
* @note 用于诊断通信质量或协议错误统计
*
* @param 无
* @return uint32_t: 错误帧累计次数
*
* 错误类型包括:
* - 字符验证失败
* - 缓冲区溢出
* - 校验和不匹配
* - 接收超时
*/
uint32_t CmdParser_GetErrorCount(void)
{
return ctx.error_count;
}
/**
* @brief 获取有效帧累计次数
* @note 用于诊断通信成功率统计
*
* @param 无
* @return uint32_t: 有效帧累计次数
*
* 计算公式:成功率 = valid_count / (valid_count + error_count)
*/
uint32_t CmdParser_GetValidCount(void)
{
return ctx.valid_count;
}
/**
* @brief 设置响应回调函数
* @note 用于将响应路由到正确的源端口
*
* @param callback: 回调函数指针
* @return 无
*
* 回调函数原型:
* void callback(uint8_t source_port, const char *response)
*
* 参数说明:
* - source_port: 指令来源端口ID
* - response: 待发送的响应字符串
*/
void CmdParser_SetResponseCallback(cmd_response_callback_t callback)
{
g_response_callback = callback;
}
/**
* @brief 设置当前源端口
* @note 在开始解析新指令前调用,记录指令来源
*
* @param port_id: 端口ID
* @return 无
*/
void CmdParser_SetSourcePort(uint8_t port_id)
{
g_current_source_port = port_id;
}