关键词:命令行、命令解析、零拷贝、函数指针、匹配分发
我们知道,Windows系统上有cmd命令窗口,Linux系统有shell命令系统,国产的RTOS操作系统RT_Thread也具有一个叫做Finsh的命令执行组件,它们都可以通过命令行输入的方式来使系统执行指定的动作。我们在开发一个嵌入式系统时,也常常需要给系统发送一些指令来查询设备的状态、获取设备的信息或者做些设置或执行的动作来和系统进行交互,比如研发过程中要发送一些调试指令;设备在工厂生产测试时生产工具要下发生产测试指令;还有在设备投入运营后维护工具或诊断工具下发的分析指令,所以一个嵌入式系统具备能够与外部进行交互的机制尤为重要。
前面几章我们已经能接收到一个完整的串口数据包了,可以进入数据包处理流程了,本章就来通过处理串口数据,来实现一个简单、可靠、实用的命令行处理功能组件,为系统提供交互能力,可以实现外部命令行接收,命令解析、命令执行和响应。通过本文的介绍读者将了解类似命令系统的实现原理和涉及的技术要点,稍作修改就可以运用到自己的项目当中,可以应用到开发调试、工厂生产测试,维护测试等实际的场景中。
要处理命令行式的命令,我们罗列下具体的需求:
1)通过串口实现命令行的输入,命令行以回车换行结尾
2)命令格式:命令名称 参数0 参数1 ...参数n\r\n
3)设备通过对命令行的解析并执行相应的动作,然后输出执行结果
实现思路:
1)选择一个串口作为命令交互通信口
2)该串口使用空闲中断+DMA方式进行命令行数据的接收
3)实现一个命令行解析器,对命令行字符串进行解析,提取出命令名称和参数
4)实现一个命令映射表,维护每个命令和对应的处理函数以及描述信息
5)命令行解析后对命令映射表进行遍历,匹配对应的命令,匹配成功后,调用相应的处理函数进行处理。
程序流程如下:
具体实现过程:
一、串口接收数据、同步数据到处理任务。该部分内容在上一章中已经详细介绍,请根据需要参阅, 此处主要介绍数据的处理部分。
二、实现命令行解析器,提取命令名称和参数
命令行格式:命令名称 参数0 参数1 ...参数n\r\n
先来确定下解析后存放命令名称和参数的数据结构如下:
#define MAX_ARGC 16 // 最大参数数量
typedef struct
{
int argc; //参数个数 包括命令名
char *argv[MAX_ARGC];//参数
}cmd_arg_t;
cmd_arg_t cmd_arg; // 存放解析数据的全局变量
我们把命令名称和参数统一放入到argv数组中,即argv[0]就是命令名称。
命令解析器
我们按照以空格为分隔符进行字符串分解和参数提取,此处采用直接在原始数据的上进行判断空格,然后替换成‘\0’作为参数结束符,并将参数指针存入数据结构,以零拷贝的方式获取到了数据,不仅效率高还节省空间。解析器代码如下:
static int cmd_line_parser(const char *input,cmd_arg_t *cmd_arg)
{
int argc = 0;
const char *p = input;
// 跳过开头的空格
while (*p && is_space(*p)) p++;
while (*p && argc < MAX_ARGC)
{
// 记录当前token起始位置
cmd_arg->argv[argc++] = (char *)p;
// 找到当前token结束位置
while (*p && !is_space(*p)) p++;
// 如果已到字符串末尾则退出
//if (!*p) break;
if('\r'==*p)
{
*((char *)p) = '\0';
break;
}
// 将空格替换为字符串结束符
*((char *)p) = '\0';
p++;
// 跳过连续空格
while (*p && isspace(*p)) p++;
}
cmd_arg->argc=argc;
return argc;
}
测试输入命令行:cmd_name para0 para1 para2 para3 para4 para5 para6 para7 para8\r\n
解析结果:
[14:57:37.707] cmd_name para0 para1 para2 para3 para4 para5 para6 para7 para8
[14:57:37.726] ------------------Cmd Parse----------------
[14:57:37.726] Cmd Argc:10
[14:57:37.726] Cmd Argv[0]:cmd_name
[14:57:37.726] Cmd Argv[1]:para0
[14:57:37.726] Cmd Argv[2]:para1
[14:57:37.726] Cmd Argv[3]:para2
[14:57:37.736] Cmd Argv[4]:para3
[14:57:37.736] Cmd Argv[5]:para4
[14:57:37.736] Cmd Argv[6]:para5
[14:57:37.736] Cmd Argv[7]:para6
[14:57:37.736] Cmd Argv[8]:para7
[14:57:37.740] Cmd Argv[9]:para8
三、通过面向对象的思想,构建命令数据结构
数据结构的主要成员有:1)命令名称 2)命令对应的执行函数指针 3)命令的描述信息,具体如下:
/*-定义函数指针类型 -*/
typedef int (*cmd_func_def)(int argc,char *argv[]);
/*-定义命令结构体 -*/
typedef struct
{
const char * cmd_name; //命令
cmd_func_def cmd_func; //命令处理函数
const char * cmd_desc; //命令描述信息
}cmd_t;
四、实现命令处理函数并构建命令表
static int cmd_version_handle(int argc,char *argv[]);
static int cmd_reboot_handle(int argc,char *argv[]);
static int cmd_led_handle(int argc,char *argv[]);
static int cmd_gps_handle(int argc,char *argv[]);
static int cmd_screen_handle(int argc,char *argv[]);
static int cmd_battery_handle(int argc,char *argv[]);
static int cmd_charger_handle(int argc,char *argv[]);
static int cmd_sleep_handle(int argc,char *argv[]);
static int cmd_factory_handle(int argc,char *argv[]);
static cmd_t cmd_func_table[]=
{
{"version" ,cmd_version_handle ,"show version info\r\n",},
{"reboot" ,cmd_reboot_handle ,"reboot the system\r\n"},
{"led" ,cmd_led_handle ,"set led example:led on/led off\r\n"},
{"gps" ,cmd_gps_handle ,"gps open example:gps on/gps off\r\n"},
{"screen" ,cmd_screen_handle ,"test screen\r\n"},
{"battery" ,cmd_battery_handle ,"get battery voltage\r\n"},
{"charger" ,cmd_charger_handle ,"enable charger example:charger on/charger off\r\n"},
{"sleep" ,cmd_sleep_handle ,"enter sleep\r\n example:sleep\r\n"},
{"factory" ,cmd_factory_handle ,"factory test example:factory speaker\r\n"},
};
static int cmd_version_handle(int argc,char *argv[])
{
printf("%s\r\n",get_mcu_ver());
}
static int cmd_reboot_handle(int argc,char *argv[])
{
bsp_soft_reset();
}
static int cmd_led_handle(int argc,char *argv[])
{
if(strstr(argv[1],"on"))
{
led_turn_on();
}
else if(strstr(argv[1],"off"))
{
led_turn_off();
}
}
static int cmd_gps_handle(int argc,char *argv[])
{
if(strstr(argv[1],"on"))
{
gps_turn_on();
}
else
{
gps_turn_off();
}
}
static int cmd_screen_handle(int argc,char *argv[])
{
screen_test();
}
static int cmd_battery_handle(int argc,char *argv[])
{
printf("voltage:%smV\r\n",GetIbatVol());
}
static int cmd_charger_handle(int argc,char *argv[])
{
if(strstr(argv[1],"on"))
{
charger_turn_on();
}
else
{
charger_turn_off();
}
}
static int cmd_sleep_handle(int argc,char *argv[])
{
enter_sleep();
}
static int cmd_factory_handle(int argc,char *argv[])
{
if(strstr(argv[1],"speaker"))
{
speaker_test();
}
else if(strstr(argv[1],"gps"))
{
gps_test();
}
}
五、构建命令分发器
命令解析成功后,通过命令分发器来匹配命令执行函数,并且将命令参数个数和命令参数数组作为实参传入函数,需要注意的是命令参数的第0个参数是命令名称,第1个参数才是命令参数的开始。命令分发器通过遍历命令列表,将命令行解析出的命令名称和命令列表中的命令名称对比匹配,匹配成功后调用响应的处理函数进行命令处理,代码见下:
static int cmd_func_distributer(cmd_arg_t *cmd)
{
uint32_t i=0;
uint32_t nCmdNum=sizeof(cmd_func_table)/sizeof(cmd_func_table[0]);
if(NULL==cmd)
{
printf("Err:cmd NULL\r\n");
return -1;
}
/* 遍历命令表 */
for(i=0;i<nCmdNum;i++)
{
if(0==strncasecmp(cmd->argv[0],cmd_func_table[i].name,(strlen(cmd->argv[0])<strlen(cmd_func_table[i].name))?strlen(cmd->argv[0]):strlen(cmd_func_table[i].name)))
{
return cmd_func_table[i].func(cmd->argc,cmd->argv);
}
}
return 0;
}
以上是我们介绍的各个功能元素,形成一个命令行处理函数如下:
int cmd_line_process(char *cmd_line)
{
int argc;
memset(&cmd_arg,0,sizeof(cmd_arg_t));
if(!strstr(cmd_line,"\r\n"))
{
printf("not end with \\r\\n \r\n");
return -1;
}
/*- 解析命令行:提取命令名称和参数、参数个数-*/
argc=cmd_line_parser(cmd_line,&cmd_arg);
printf("------------------Cmd Parse----------------\r\n");
printf("Cmd Argc:%d\r\n",cmd_arg.argc);
for(int i=0;i<cmd_arg.argc;i++)
{
printf("Cmd Argv[%d]:%s\r\n",i,cmd_arg.argv[i]);
}
if(argc)
{
/*- 命令分发执行-*/
cmd_func_distributer(&cmd_arg);
return 0;
}
else
{
return -1;
}
}
完成代码后,进行实测,输入version、led on、led off、factory speaker测试如下,
实际项目中需要将调试log关闭,测试如下:
以上就是通过串口输入命令行实现命令执行的整个实现过程,这种方式有比较好的优点就是通过“零拷贝”的方式进行参数的提取,不用另外再单独开辟内存空间去存储参数数据,整个过程也不需要来回拷贝数据,只是存储参数的指针数据,非常实用简便。有需要的同学可以参数上述代码稍作修改,移植到自己的实际项目中进行使用,比如可以根据不同的命令格式调整解析器,以便解析更复杂的命令行。
感觉各位同学的阅读、批评,指正。