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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章