897 lines
31 KiB
C
897 lines
31 KiB
C
/**
|
||
******************************************************************************
|
||
* @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=空字符串或NULL,false=非空
|
||
*/
|
||
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_PARAM1,field_index=0
|
||
* - byte='*' -> 字段结束,state=PARSE_CHECKSUM,cs_index=0
|
||
* - is_valid_cmd_char(byte) -> 写入cmd缓冲区,checksum_acc^=byte
|
||
* - 其他情况 -> error_count++, reset_parser()
|
||
*
|
||
* [PARSE_PARAM1]:
|
||
* - byte=',' -> 字段结束,state=PARSE_PARAM2,field_index=0
|
||
* - byte='*' -> 字段结束,state=PARSE_CHECKSUM,cs_index=0
|
||
* - is_valid_param_char(byte) -> 写入param1缓冲区,checksum_acc^=byte
|
||
* - 其他情况 -> error_count++, reset_parser()
|
||
*
|
||
* [PARSE_PARAM2]:
|
||
* - byte='*' -> 字段结束,state=PARSE_CHECKSUM,cs_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;
|
||
}
|