一. 簡介
雖然 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_OK
和ESP_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_IGNORE
和 ESP_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;
}