Files
433_STM32/Core/Src/cmd_parser.c

897 lines
31 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.

/**
******************************************************************************
* @file cmd_parser.c
* @brief ASCII指令解析模块实现
* @author Application Layer
* @version 1.1
******************************************************************************
* @attention
* 本模块实现ASCII文本指令的解析和处理
* 关键特性:
* 1. 状态机解析,健壮可靠
* 2. 完善的安全防护(缓冲区边界检查、超时重置、字符过滤)
* 3. 异或校验FF特权后门
* 4. 支持RL、DI、ECHO指令
*
* 修订历史:
* v1.1 - 修复审查报告高危-6、中危-7/8
******************************************************************************
*/
#include "cmd_parser.h"
#include "uart2_print.h"
#include "io_monitor.h"
#include "relay_control.h"
#include <string.h>
#include <ctype.h>
#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 = 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; /**< 有效帧累计次数 */
} 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;
ctx.field_index = 0;
ctx.checksum_acc = 0;
ctx.cs_index = 0;
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';
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
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;
for (uint8_t i = 0; i < len; i++) {
cs ^= (uint8_t)data[i];
}
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);
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);
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;
}
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",
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) {
/* 参数合法性检查param1必须为数字 */
if (!is_str_numeric(frame->param1)) {
send_response_err("PARAM");
DEBUG_LOG("Invalid RL param: not numeric");
return;
}
/* 将参数字符串转换为整数 */
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", state);
send_response_ok(resp);
DEBUG_LOG("Relay -> %s", state ? "ON" : "OFF");
} else {
/* 参数值超出有效范围 */
send_response_err("PARAM");
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",
(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; /* 累加到校验和 */
} 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;
} else {
ctx.error_count++;
reset_parser();
}
} else {
ctx.error_count++;
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 ||
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",
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;
}
break;
}
}
/**
* @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) {
process_cmd_frame(&ctx.frame);
reset_parser();
}
}
/**
* @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) {
if (frame) {
memcpy(frame, &ctx.frame, sizeof(cmd_frame_t));
}
return true;
}
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;
}