ESP-AT 系列: 自定义 AT 命令

一. 简介

虽然 ESP-AT 内部已经集成了很多指令, 比如 Wi-Fi, BT, BLE, IP 等等, 但是同时也支持客户进行二次开发, 定义客户自己的命令.

本文主要介绍客户如何自定义 AT 命令.

1.1 ESP-AT 命令的四种格式

ESP-AT 命令包含 4 种命令格式:

  • Test Command
    • 示例: AT+<x>=?
    • 用途: 查询 Set Command 的各个参数以及参数的范围
  • Query Command
    • 示例: AT+<x>?
    • 用途: 查询命令, 可以返回当前参数的值, 也可以返回其他一些想要得到的信息
  • Set Command
    • 示例: AT+<x>=<…>
    • 用途: 设置命令, 向AT输入一些参数, 执行相应的操作
  • Execute Command
    • 示例: AT+<x>
    • 用途: 执行指令, 该指令不带参数

1.2 如何开始自定义一组 AT 命令

首先, 我们看一下 ESP-AT 命令结构体的定义:

typedef struct {
    char *at_cmdName;                               /*!< at command name */
    uint8_t (*at_testCmd)(uint8_t *cmd_name);       /*!< Test Command function pointer */
    uint8_t (*at_queryCmd)(uint8_t *cmd_name);      /*!< Query Command function pointer */
    uint8_t (*at_setupCmd)(uint8_t para_num);       /*!< Setup Command function pointer */
    uint8_t (*at_exeCmd)(uint8_t *cmd_name);        /*!< Execute Command function pointer */
} esp_at_cmd_struct;

这个结构体中包含 5 个元素, 第一个是个字符串指针, 是 AT 命令的名字, AT 命令的名字有一定的格式要求, 都是+开始. 后面跟着四个函数参数指针, 分别对应上面提到的四种命令.

现在我们首先举个简单的例子, 定义一个命令, 用来输出Hello word.

定义命令:

static esp_at_cmd_struct at_example_cmd[] = {
    {"+EXAMPLE", NULL, NULL, NULL, NULL},
};

这样, 这条命令就定义好了, 命令的名字叫 +EXAMPLE, 实际用起来的时候,用户输入的命令就是这个样子:

  • AT+EXAMPLE=?
  • AT+EXAMPLE?
  • AT+EXAMPLE=<param_1>,<param_2>,<param_3>…
  • AT+EXAMPLE

当然, 仅仅这样, 还是远远不够的, 要想真的用起来, 至少还少 2 个步骤, 首先就是注册这组命令, 其次就是添加命令的具体实现.

注册命令:

注册自定义命令数组需要用 API:

bool esp_at_custom_cmd_array_regist(const esp_at_cmd_struct *custom_at_cmd_array, uint32_t cmd_num);

注册客户自定义命令的代码需要加到 app_main() 里, 建议放在 app_main() 的最后 at_custom_init(); 之前, 参考代码如下:

bool esp_at_example_cmd_regist(void)
{
    return esp_at_custom_cmd_array_regist(at_example_cmd, sizeof(at_example_cmd) / sizeof(at_example_cmd[0]));
}

void app_main()
{
    ...
    
    if(esp_at_example_cmd_regist() == false) {
        printf("regist example cmd fail\r\n");
    }
    
    at_custom_init();
}

添加命令的具体实现:

刚次才我们定义的命令数组里, 四个回调函数都是 NULL, 其实还是什么都做不了的, 我们现在添加个实例函数, 来输出 Hello World.

因为不需要带参数, 我们就用执行命令来实现吧, 示例代码如下:

uint8_t at_exeCmdExample(uint8_t *cmd_name)
{
    esp_at_port_write_data("Hello World", strlen("Hello World"));
    return ESP_AT_RESULT_CODE_OK;
}

同时修改命令数组如下:

static esp_at_cmd_struct at_example_cmd[] = {
    {"+EXAMPLE", NULL, NULL, NULL, at_exeCmdExample},
};

如果想同时打印这条命令的名字, 可以将 cmd_name 也打印出来, 例如:

esp_at_port_write_data("%s:", strlen((char *)cmd_name))
esp_at_port_write_data("Hello World\r\n",strlen("Hello World"));

此时在终端打印信息是这样的:

AT+EXAMPLE
+EXAMPLE:Hello Word

OK

如何添加多个命令

上面的例子只有一个命令, 如果需要多个命令, 可以依次添加, 例如:

static esp_at_cmd_struct at_example_cmd[] = {
    {"+EXAMPLE1", NULL, NULL, NULL, NULL},
    {"+EXAMPLE2", NULL, NULL, NULL, NULL},
    {"+EXAMPLE3", NULL, NULL, NULL, NULL},
    {"+EXAMPLE4", NULL, NULL, NULL, NULL},
    {"+EXAMPLE5", NULL, NULL, NULL, NULL},
};

二. ESP-AT 自定义命令进阶

2.1 命令实现中, 不同返回值的区别

在上面的 Hello Word 示例中, 我们在 at_exeCmdExample() 最后返回了 ESP_AT_RESULT_CODE_OK, 这个返回值的作用就是命令执行完毕之后, 输出字符 "OK".

ESP-AT 中, 返回值不止这一个, 而且每个效果都不同, 我们先看一下一共有哪些返回值

/**
 * @brief the result code of AT command processing
 *
 */
typedef enum {
    ESP_AT_RESULT_CODE_OK           = 0x00,       /*!< "OK" */
    ESP_AT_RESULT_CODE_ERROR        = 0x01,       /*!< "ERROR" */
    ESP_AT_RESULT_CODE_FAIL         = 0x02,       /*!< "ERROR" */
    ESP_AT_RESULT_CODE_SEND_OK      = 0x03,       /*!< "SEND OK" */
    ESP_AT_RESULT_CODE_SEND_FAIL    = 0x04,       /*!< "SEND FAIL" */
    ESP_AT_RESULT_CODE_IGNORE       = 0x05,       /*!< response nothing */
    ESP_AT_RESULT_CODE_PROCESS_DONE = 0x06,       /*!< response nothing */

    ESP_AT_RESULT_CODE_MAX
} esp_at_result_code_string_index;

从后面的注释能够看出, 不同的返回值可以输出 "OK", "ERROR", "SEND OK", "SEND FAIL", 或者处理结果的应答都没有.

前面 3 个应该很好理解, 如果顺利执行完毕, 那么就返回 ESP_AT_RESULT_CODE_OK, 最终在串口上面会输出 "OK", 如果返回的是 ESP_AT_RESULT_CODE_ERROR,ESP_AT_RESULT_CODE_FAIL, 那么最终会在串口上面输出 "ERROR"

ESP_AT_RESULT_CODE_SEND_OKESP_AT_RESULT_CODE_SEND_FAIL可以用在这样的一个场景, 比如基于TCP 连接发送一包数据, 这个时候发送失败了, 可以 return ESP_AT_RESULT_CODE_SEND_FAIL;, 如果发送成功了, 可以 return ESP_AT_RESULT_CODE_SEND_OK;

除了通过 return 返回值的方式, 向串口输出指令执行的结果, 我们还可以用另外一种方式来做:

/**
 * @brief response AT process result,
 *
 * @param result_code see esp_at_result_code_string_index
 *
 */
void esp_at_response_result(uint8_t result_code);

假如您现在向服务器发送了一串数据, 然后您想先打印 "SEND OK", 然后等待服务器回应, 最后再退出函数, 您可以这样做:

uint8_t at_exeCmdExample(uint8_t *cmd_name)
{
	 //send data to Server
	 Send...
	 
	 // 先打印 SEND OK
	 esp_at_response_result(ESP_AT_RESULT_CODE_OK);
	 
	 // 等待服务器回应, 具体的阻塞实现在下面会介绍
	 wait...
	 
	 //最后返回 OK 
	 //如果您在这里不想再输出 OK 了, 可以 return ESP_AT_RESULT_CODE_PROCESS_DONE;
    return ESP_AT_RESULT_CODE_OK;
}

ESP_AT_RESULT_CODE_IGNOREESP_AT_RESULT_CODE_PROCESS_DONE 的区别就是:

ESP_AT_RESULT_CODE_IGNORE 不输出命令执行结果, 也不会有状态的切换, 仍处于处理当前命令的状态. 这个时候输入下一条命令, 会返回 busy.

ESP_AT_RESULT_CODE_PROCESS_DONE 不输出命令执行结果, 但会将当前的状态切换到空闲状态, 可以处理下一条命令

2.2 如何获取命令的参数

上一节提到的设置命令, 该命令是需要带一些参数的, 参数可能不止一个, 类型也不尽相同, 有的是整形, 有的是字符串, 该怎么在回调函数中处理这些参数呢?我们还是举一个例子, 假如我们现在要去连接一个 TCP Server, 那么它的参数至少有两个, IP 地址和端口号, 我们约定他的命令是这个样子的:

AT+TCP=<IP>,<port>

其中, IP 地址是字符串, port 是数字.

我们可以定义命令数组如下:

static esp_at_cmd_struct at_example_cmd[] = {
    {"+TCP", NULL, NULL, at_setupCmdTcp, NULL},
};

设置命令的具体实现如下:

uint8_t at_setupCmdTcp(uint8_t para_num)
{
    int32_t cnt = 0, value = 0;
    uint8_t* s = NULL;
    
    // 首先获取ip地址, 这是一个字符串, 如果失败, 会返回 ERROR
    if (esp_at_get_para_as_str(cnt++, &s) != ESP_AT_PARA_PARSE_RESULT_OK) {
        return ESP_AT_RESULT_CODE_ERROR;
    }
    
    // 然后获取端口号, 同理, 如果失败, 也会返回错误
    if (esp_at_get_para_as_digit(cnt++, &value) != ESP_AT_PARA_PARSE_RESULT_OK) {
        return ESP_AT_RESULT_CODE_ERROR;
    }
    
    // 最后再检查下参数个数, para_num 是用户输入的这条命令的参数个数, 如果不等于 2, 也可以报错
    if (para_num != cnt) {
        return ESP_AT_RESULT_CODE_ERROR;
    }
    
    // 下面就可以加入用户自己的处理代码了
    // TODO: 
    
    return ESP_AT_RESULT_CODE_OK;
}

这里需要关注这样的几点:

  • param_num 是用户实际输入的参数个数, 每个参数之间是以,隔开
  • esp_at_para_parse_result_type esp_at_get_para_as_digit(int32_t para_index, int32_t *value);用于获取整形数据
  • esp_at_para_parse_result_type esp_at_get_para_as_str(int32_t para_index, uint8_t **result);用于获取字符串参数

2.3 如何处理可选参数

有的时候, 可能有些参数是可选, 也就是可变参数.

这个时候涉及到两种情况, 一种省略的是第一个或者中间的参数, 另一种是省略最后的部分, 省略第一个参数和省略中间参数的处理方式相同.

2.3.1 省略的是中间的参数

这种情况, 命令的定义一般是这样的:

AT+TESTCMD=<parm_1>[,<param_2>],<param_3>

约定中间的参数 param_2 可以省略, 它的类型是整形, 另外两个参数 param_1 是整形, param_3 是字符串.

示例代码如下:

static esp_at_cmd_struct at_example_cmd[] = {
    {"+TESTCMD", NULL, NULL, at_setupCmdTestCmd, NULL},
};

uint8_t at_setupCmdTestCmd(uint8_t para_num)
{
    int32_t cnt = 0, value = 0;
    uint8_t* s = NULL;
    esp_at_para_parse_result_type parse_result = ESP_AT_PARA_PARSE_RESULT_FAIL;
    
    // 首先获取第一个参数 param_1,他的类型是整形
    if (esp_at_get_para_as_digit(cnt++, &value) != ESP_AT_PARA_PARSE_RESULT_OK) {
        return ESP_AT_RESULT_CODE_ERROR;
    }
    // 这里需要注意, 需要把 value 的值赋值给另外一个变量, 因为下面的代码会把 value 的值重置成另一个参数的值, 或者下面再获取整形参数值的时候, 用另外定义的变量
    // param_1 = value;
    
    // 现在处理第二个可选参数, 尝试下是否能获取到
    parse_result = esp_at_get_para_as_digit(cnt++, &value);
    if (parse_result != ESP_AT_PARA_PARSE_RESULT_OMITTED) {
        // 能走到这里, 说明这个可选参数没有被省略
        // 进一步判断返回值是不是OK
        // 需要注意, 例子这里举得是整形参数, 如果是字符串的话
        // 客户输入 "" 这样的空串也是会返回OK的, 只是字符串指针是 NULL
        if (parse_result != ESP_AT_PARA_PARSE_RESULT_OK) {
            return ESP_AT_RESULT_CODE_ERROR;
        }
        // param_2 = value;
    } else {
        // 走到这里, 说明用户没有输入第二个参数
        // 是不是用默认值, 还是做别的处理, 取决于客户自己的逻辑
    }
    
    // 现在获取最后一个参数 param_3
    if (esp_at_get_para_as_str(cnt++, &s) != ESP_AT_PARA_PARSE_RESULT_OK) {
        return ESP_AT_RESULT_CODE_ERROR;
    }
    // param_3 = s;
    
    
    // 最后再检查下参数个数, para_num 是用户输入的这条命令的参数个数, 如果不等于3, 报错
    if (para_num != cnt) {
        return ESP_AT_RESULT_CODE_ERROR;
    }
    
    // 下面就可以加入用户自己的处理代码了
    // TODO: 
    
    return ESP_AT_RESULT_CODE_OK;
}

2.3.2 省略的是最后的参数

这种情况, 命令的定义一般是这样的:

AT+TESTCMD=<parm_1>,<param_2>[,<param_3>]

约定最后的的参数 param_3 可以省略, 它的类型是整形, 另外两个参数 param_1 是整形, param_2 是字符串.

省略的形式有两种, 例如:

AT+TESTCMD=123,"abc"
AT+TESTCMD=123,"abc",

示例代码如下:

static esp_at_cmd_struct at_example_cmd[] = {
    {"+TESTCMD", NULL, NULL, at_setupCmdTestCmd, NULL},
};

uint8_t at_setupCmdTestCmd(uint8_t para_num)
{
    int32_t cnt = 0, value = 0;
    uint8_t* s = NULL;
    esp_at_para_parse_result_type parse_result = ESP_AT_PARA_PARSE_RESULT_FAIL;
    
    // 首先获取第一个参数 param_1, 他的类型是整形
    if (esp_at_get_para_as_digit(cnt++, &value) != ESP_AT_PARA_PARSE_RESULT_OK) {
        return ESP_AT_RESULT_CODE_ERROR;
    }
    // 这里需要注意, 需要把 value 的值赋值给另外一个变量, 因为下面的代码会把 value 的值重置成另一个参数的值, 或者下面再获取整形参数值的时候, 用另外定义的变量
    //param_1 = value;
    
    // 现在处理第二个参数
    if (esp_at_get_para_as_str(cnt++, &s) != ESP_AT_PARA_PARSE_RESULT_OK){
        return ESP_AT_RESULT_CODE_ERROR;
       
    }
    // param_2 = s;
    
    if (para_num != cnt) {
        // 走到这里说明第三个参数可能存在, 尝试获取它, 看看是不是真的输入了
        parse_result = esp_at_get_para_as_digit(cnt++, &value);
        if (parse_result != ESP_AT_PARA_PARSE_RESULT_OMITTED) {
            if (parse_result != ESP_AT_PARA_PARSE_RESULT_OK) {
                // 第三个参数格式错误
                return ESP_AT_RESULT_CODE_ERROR;
            }
            // 获取到了第三个参数
            // param_3 = value;
        } else {
            // 说明参数还是被省略
            // 用户自己处理这种情况
        }
    }
    
    // 最后再检查下参数个数, para_num 是用户输入的这条命令的参数个数, 如果不等于 3, 报错
    if (para_num != cnt) {
        return ESP_AT_RESULT_CODE_ERROR;
    }
    
    // 下面就可以加入用户自己的处理代码了
    // TODO: 
    
    return ESP_AT_RESULT_CODE_OK;
}

2.4 如何在命令中加入定时器进入阻塞状态

某些应用场景, 需要将命令处理过程阻塞在哪里, 然后等待结果返回, 再将命令退出, 这种情况, 我们一般可以通过信号量来处理.

这次我们用查询指令来举例, 比如想去云端查询某个状态.

示例代码如下:

static esp_at_cmd_struct at_example_cmd[] = {
    {"+TESTCMD", NULL, at_queryCmdTestCmd, NULL, NULL},
};

static xSemaphoreHandle at_operation_sema = NULL;

uint8_t at_queryCmdTestCmd(uint8_t *cmd_name)
{
    ...
    ...
    
    assert(!at_operation_sema);
    at_operation_sema = xSemaphoreCreateBinary();

    //用户代码处理, 比如向另外一个 task 发送一个 queue, 让这个 task 做一些耗时比较久的事情, 然后 AT 这里等待结果

    xSemaphoreTake(at_operation_sema, portMAX_DELAY);

    vSemaphoreDelete(at_operation_sema);
    at_operation_sema = NULL;
    
    return ESP_AT_RESULT_CODE_OK;
}

另一个 task 处理完可以这样处理: 

void user_task(void)
{
    ...
    if (at_operation_sema) {
        xSemaphoreGive(at_operation_sema);
    }
    ...
}

2.5 如何从 AT 命令 port 中截取数据

一般有两个应用场景, 第一个是截取指定长度的数据, 另一个是数据长度不指定, 类似于透传

这里需要注意这两个 API:

/**
 * @brief Set AT core as specific status, it will call callback if receiving data.
 * @param callback
 *
 */
void esp_at_port_enter_specific(esp_at_port_specific_callback_t callback);

/**
 * @brief Exit AT core as specific status.
 * @param NONE
 *
 */
void esp_at_port_exit_specific(void);

一个用于设置回调函数, 一个用于删除回调函数, 在本小节里的这种应用场景中, 他的工作原理是这样的:

  • 首先设置回调函数
  • 如果 AT Port 收到数据, 会回调这个函数
  • 我们在回调函数里 Give 信号量
  • AT 命令处理代码一直在等这个信号量
  • Take 到这个信号量之后, 就可以获取到 AT port 的数据了
  • 退出的时候, 删除回调函数, 删除信号量

2.5.1 数据长度不指定

如果用户需要进入输入模式, 直接获取串口的数据, 比如进入偷传状态, 我们可以这样做:

定义命令:

static esp_at_cmd_struct at_example_cmd[] = {
    {"+TESTCMD", NULL, NULL, NULL, at_exeCmdTestCmd},
};

具体实现如下:

static xSemaphoreHandle at_sync_sema   = NULL;

static void at_wait_data_callback(void)
{
    xSemaphoreGive(at_sync_sema);
}

#define BUFFER_LEN 2048 

uint8_t at_exeCmdExample(uint8_t *cmd_name)
{
    int32_t temp_len = 0;
    uint8_t test_buf[BUFFER_LEN] = {0};
    ...
    ...
    
    vSemaphoreCreateBinary(at_sync_sema);
    xSemaphoreTake(at_sync_sema, portMAX_DELAY);
    
    //打印输入提示符 '>'
    esp_at_port_write_data((uint8_t*)">", at_strlen(">"));

    esp_at_port_enter_specific(at_wait_data_callback);

    while (xSemaphoreTake(at_sync_sema, portMAX_DELAY)) {
        memset(test_buf, 0x0, BUFFER_LEN);
        //读取数据到buffer
        temp_len = esp_at_port_read_data(test_buf, BUFFER_LEN);
        //下面这段处理逻辑是判读是否推出透传, 这里示例代码的判断条件是 "+++" 三个字节的字符
        if ((temp_len == 3) && (memcmp((char *) test_buf, "+++", strlen("+++"))==0)) {
            esp_at_port_exit_specific();
            temp_len = esp_at_port_get_data_length();

            if (temp_len > 0) {
                esp_at_port_recv_data_notify(temp_len, portMAX_DELAY);
            }

            break;
        } else if (temp_len > 0 ){
            // 这里就把 RAW DATA 交由用户处理了
            customer_do_something(test_buf, temp_len);
        }
    }

    vSemaphoreDelete(at_sync_sema);
    at_sync_sema = NULL;
    // 这里就退出透传了, 同时还会输出OK字符, 如果您不想把OK在这里输出, 而是放在 ‘>’ 之前
    // 您首先需要在打印输入提示符 '>' 之前, 调用
    // esp_at_response_result(ESP_AT_RESULT_CODE_OK);
    // 然后在最后这里 return ESP_AT_RESULT_CODE_IGNORE;
    return ESP_AT_RESULT_CODE_OK;
}

2.5.2 指定数据长度

如果数据长度是指定, 和上面略微有些差异, 大体思路相同, 我们可以这么做:

定义命令:

static esp_at_cmd_struct at_example_cmd[] = {
    {"+TESTCMD", NULL, NULL, at_setupCmdTestCmd, NULL},
};

命令带一个整形参数.

具体实现如下:

static xSemaphoreHandle at_sync_sema   = NULL;

static void at_wait_data_callback(void)
{
    xSemaphoreGive(at_sync_sema);
}

uint8_t at_setupCmdTestCmd(uint8_t para_num)
{
    int32_t cnt = 0 , value = 0, len = 0, temp_len = 0;
    uint8_t *test_buf = NULL;
    
    // 获取数据长度
    if (esp_at_get_para_as_digit(cnt++, &value) != ESP_AT_PARA_PARSE_RESULT_OK) {
        return ESP_AT_RESULT_CODE_ERROR;
    }
    len = value;
    
    // 检查参数个数
    if (para_num != cnt) {
        return ESP_AT_RESULT_CODE_ERROR;
    }
    
    test_buf = (uint8_t *)malloc(len * sizeof(uint8_t));
    
    if (test_buf == NULL) {
        printf("malloc fail\n");
        return ESP_AT_RESULT_CODE_ERROR;
    }
    
    vSemaphoreCreateBinary(at_sync_sema);
    xSemaphoreTake(at_sync_sema, portMAX_DELAY);
    
    //打印输入提示符 '>'
    esp_at_port_write_data((uint8_t*)">", at_strlen(">"));

    esp_at_port_enter_specific(at_wait_data_callback);
    
    temp_len = 0;

    // 开始截取指定长度的数据, 
    while (xSemaphoreTake(at_sync_sema, portMAX_DELAY)) {
        temp_len += esp_at_port_read_data(test_buf + temp_len, len - temp_len);

        if (temp_len == len) {
            // 走到这里, 说明已经接收到了想要的长度的数据, 但是要要注意, 如果时间输入长度超过指定想要的长度, 也会进到这里
            esp_at_port_exit_specific();
            
            // 这里就是在获取看看还有多少数据没有读取, 如果是0, 那就是正好数据长度就是指定的
            temp_len = esp_at_port_get_data_length();

            if (temp_len > 0) {
                //如果实际输入的长度超过想要的长度, 会走到这里, 在这个例子中, 是直接把多余的数据打印出来, 您的应用中怎么处理取决于您
                esp_at_port_recv_data_notify(temp_len, portMAX_DELAY);
            }

            break;
        }
    }

    vSemaphoreDelete(at_sync_sema);
    at_sync_sema = NULL;
    
    free(test_buf);
    
    ...
    
    return ESP_AT_RESULT_CODE_OK;
}
发布了57 篇原创文章 · 获赞 76 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/espressif/article/details/102777084