How to Add User-defined AT Commands

[中文]

This document provides guidance on how to add custom AT commands. It uses the example of at_custom_cmd to demonstrate each step with the sample code.

To define a basic, functional command, at least the following six steps are required:

After completing the above steps, please execute the AT+TEST command to get result.

To customize relatively complex commands, please refer to the sample codes below:

The source code for the AT command set is not open source and is presented in the form of library files , which is also the basis for parsing custom AT commands.

Customize AT Commands

Step 1: Define AT Commands

You can define AT commands in the files at_custom_cmd.c and at_custom_cmd.h, or create new source files and header files in the directories examples/at_custom_cmd/custom and examples/at_custom_cmd/include to define AT commands.

Before customizing AT commands, please first determine the name and type of the AT command.

Command naming rules:

  • Start a command with + character.

  • Alphabetic characters (A~Z, a~z), numeric characters (0~9), and some other characters (!, %, -, ., /, :, _) are supported. See AT Command Types for more information.

Command types:

Each AT command can have up to four types: Test Command, Query Command, Set Command, and Execute Command. See AT Command Types for more information.

Then, define desired type of command. Assuming that AT+TEST supports all the four types. Below is the code to define the name and types of the AT command, as well as sample code to define each type.

  • First, call esp_at_cmd_t to define the name and type(s) that your AT command supports. The sample code below defined the name +TEST (omitting AT) and all the four types.

    static const esp_at_cmd_t at_custom_cmd[] = {
        {"+TEST", at_test_cmd_test, at_query_cmd_test, at_setup_cmd_test, at_exe_cmd_test},
        /**
         * @brief You can define your own AT commands here.
         */
    };
    

    Note

    If you do not want to define a particular type, set it to NULL.

  • Test Command:

    static uint8_t at_test_cmd_test(uint8_t *cmd_name)
    {
        uint8_t buffer[64] = {0};
        snprintf((char *)buffer, 64, "test command: <AT%s=?> is executed\r\n", cmd_name);
        esp_at_port_write_data(buffer, strlen((char *)buffer));
    
        return ESP_AT_RESULT_CODE_OK;
    }
    
  • Query Command:

    static uint8_t at_query_cmd_test(uint8_t *cmd_name)
    {
        uint8_t buffer[64] = {0};
        snprintf((char *)buffer, 64, "query command: <AT%s?> is executed\r\n", cmd_name);
        esp_at_port_write_data(buffer, strlen((char *)buffer));
    
        return ESP_AT_RESULT_CODE_OK;
    }
    
  • Set Command:

    static uint8_t at_setup_cmd_test(uint8_t para_num)
    {
        uint8_t index = 0;
    
        // get first parameter, and parse it into a digit
        int32_t digit = 0;
        if (esp_at_get_para_as_digit(index++, &digit) != ESP_AT_PARA_PARSE_RET_OK) {
            return ESP_AT_RESULT_CODE_ERROR;
        }
    
        // get second parameter, and parse it into a string
        uint8_t *str = NULL;
        if (esp_at_get_para_as_str(index++, &str) != ESP_AT_PARA_PARSE_RET_OK) {
            return ESP_AT_RESULT_CODE_ERROR;
        }
    
        // allocate a buffer and construct the data, then send the data to mcu via interface (uart/spi/sdio/socket)
        uint8_t *buffer = (uint8_t *)malloc(512);
        if (!buffer) {
            return ESP_AT_RESULT_CODE_ERROR;
        }
        int len = snprintf((char *)buffer, 512, "setup command: <AT%s=%d,\"%s\"> is executed\r\n",
                           esp_at_get_current_cmd_name(), digit, str);
        esp_at_port_write_data(buffer, len);
    
        // remember to free the buffer
        free(buffer);
    
        return ESP_AT_RESULT_CODE_OK;
    }
    
  • Execute Command:

    static uint8_t at_exe_cmd_test(uint8_t *cmd_name)
    {
        uint8_t buffer[64] = {0};
        snprintf((char *)buffer, 64, "execute command: <AT%s> is executed\r\n", cmd_name);
        esp_at_port_write_data(buffer, strlen((char *)buffer));
    
        return ESP_AT_RESULT_CODE_OK;
    }
    

Step 2: Register AT Command Functions

  • Please define the esp_at_custom_cmd_register function and call the API esp_at_custom_cmd_array_register() to register AT commands.

    Sample code:

    bool esp_at_custom_cmd_register(void)
    {
        return esp_at_custom_cmd_array_register(at_custom_cmd, sizeof(at_custom_cmd) / sizeof(esp_at_cmd_t));
    }
    
  • Then, call the API ESP_AT_CMD_SET_INIT_FN to initialize your implemented registration AT command function esp_at_custom_cmd_register.

    Sample code:

    ESP_AT_CMD_SET_INIT_FN(esp_at_custom_cmd_register, 1);
    

Note

To customize AT commands in the examples/at_custom_cmd/custom and examples/at_custom_cmd/include directories, please avoid naming the registered AT command function esp_at_custom_cmd_register, as this function is already defined and initialized in the at_custom_cmd example. Instead, name it something like esp_at_custom_cmd_register_foo, and use ESP_AT_CMD_SET_INIT_FN to initialize it.

Step 3: Add Component Dependencies

If you use components other than at, freertos, nvs_flash during Step 1: Define AT Commands, please add these component dependencies in the examples/at_custom_cmd/CMakeLists.txt file. Otherwise, you can skip this step. For example, if you additionally use the lwip component, the sample code is as follows:

set(require_components at freertos nvs_flash lwip)

Step 5: Set Component Environment Variables

This section introduces two methods for setting the at_custom_cmd component environment variables to ensure that the ESP-AT project can locate this component correctly during compilation. Choose the method that best suits your needs. If you customize AT commands or modify code in the original components under the esp-at/components directory, you do not need to perform this step. However, it is not recommended to customize AT commands in the original components under the esp-at/components directory, and this document does not explain this.

Method 1: Set the AT_CUSTOM_COMPONENTS environment variable directly in the command line (Suitable for local compilation).

  • Linux or macOS

export AT_CUSTOM_COMPONENTS=(path_of_at_custom_cmd)
  • Windows

set AT_CUSTOM_COMPONENTS=(path_of_at_custom_cmd)

Note

  • Please replace (path_of_at_custom_cmd) with the actual absolute path of the at_custom_cmd directory.

  • You can specify multiple components. For example:

    export AT_CUSTOM_COMPONENTS="~/prefix/my_path1 ~/prefix/my_path2"

Method 2: Add the code to set the AT_CUSTOM_COMPONENTS environment variable in the esp-at/build.py file’s setup_env_variables() function. (Suitable for local compilation and web compilation). The sample code is as follows:

# set AT_CUSTOM_COMPONENTS
at_custom_cmd_path=os.path.join(os.getcwd(), 'examples', 'at_custom_cmd')
os.environ['AT_CUSTOM_COMPONENTS']=at_custom_cmd_path

Step 6: Compile the AT Firmware

After completing these steps, choose either web compilation or local compilation of the AT firmware according to your needs, and flash it to your device.

Execute the AT+TEST Command to Get Result

If you have followed the steps correctly, below is the execution result of the AT+TEST command you defined.

Test Command:

AT+TEST=?

Response:

AT+TEST=?
test command: <AT+TEST=?> is executed

OK

Query Command:

AT+TEST?

Response:

AT+TEST?
query command: <AT+TEST?> is executed

OK

Set Command:

AT+TEST=1,"espressif"

Response:

AT+TEST=1,"espressif"
setup command: <AT+TEST=1,"espressif"> is executed

OK

Execute Command:

AT+TEST

Response:

AT+TEST
execute command: <AT+TEST> is executed

OK

Customize Complex AT Commands

The sample codes below are used to customize more complex commands, from which you can choose based on personal needs.

Define Return Values

ESP-AT has defined return values in esp_at_rc_t. See AT Messages for more return values.

In addition to output return values through the return mode, you can also use API esp_at_write_result() to output the execution result of the command. ESP_AT_RESULT_CODE_SEND_OK and ESP_AT_RESULT_CODE_SEND_FAIL can be used with the API in code.

For example, when you send data to the server or MCU with the Execute Command of AT+TEST, you can use esp_at_write_result() to output the sending result, and the return mode to output the command execution result. Below is the sample code:

uint8_t at_exe_cmd_test(uint8_t *cmd_name)
{
    uint8_t buffer[64] = {0};

    snprintf((char *)buffer, 64, "this cmd is execute cmd: %s\r\n", cmd_name);

    esp_at_port_write_data(buffer, strlen((char *)buffer));

    // user-defined operation of sending data to server or MCU
    send_data_to_server();

    // output SEND OK
    esp_at_write_result(ESP_AT_RESULT_CODE_SEND_OK);

    return ESP_AT_RESULT_CODE_OK;
}

How it works out:

AT+TEST
this cmd is execute cmd: +TEST

SEND OK

OK

Access Command Parameters

ESP-AT provides two APIs to access command parameters:

See Set Command for an example.

Omit Command Parameters

This section describes how to provide optional command parameters:

Omit the First or Middle Parameter

Let’s say you want to make <param_2> and <param_3> of AT+TEST optional. <param_2> is a digital parameter, and <param_3> a string parameter.

AT+TEST=<param_1>[,<param_2>][,<param_3>],<param_4>

Below is the sample code to achieve it:

uint8_t at_setup_cmd_test(uint8_t para_num)
{
    int32_t para_int_1 = 0;
    int32_t para_int_2 = 0;
    uint8_t *para_str_3 = NULL;
    uint8_t *para_str_4 = NULL;
    uint8_t num_index = 0;
    uint8_t buffer[64] = {0};
    esp_at_para_parse_ret_t parse_result = ESP_AT_PARA_PARSE_RET_OK;

    snprintf((char *)buffer, 64, "this cmd is setup cmd and cmd num is: %u\r\n", para_num);
    esp_at_port_write_data(buffer, strlen((char *)buffer));

    parse_result = esp_at_get_para_as_digit(num_index++, &para_int_1);
    if (parse_result != ESP_AT_PARA_PARSE_RET_OK) {
        return ESP_AT_RESULT_CODE_ERROR;
    } else {
        memset(buffer, 0, 64);
        snprintf((char *)buffer, 64, "first parameter is: %d\r\n", para_int_1);
        esp_at_port_write_data(buffer, strlen((char *)buffer));
    }

    parse_result = esp_at_get_para_as_digit(num_index++, &para_int_2);
    if (parse_result != ESP_AT_PARA_PARSE_RET_OMITTED) {
        if (parse_result != ESP_AT_PARA_PARSE_RET_OK) {
            return ESP_AT_RESULT_CODE_ERROR;
        } else {
            // sample code
            // user needs to customize the operation
            memset(buffer, 0, 64);
            snprintf((char *)buffer, 64, "second parameter is: %d\r\n", para_int_2);
            esp_at_port_write_data(buffer, strlen((char *)buffer));
        }
    } else {
        // sample code
        // the second parameter is omitted
        // user needs to customize the operation
        memset(buffer, 0, 64);
        snprintf((char *)buffer, 64, "second parameter is omitted\r\n");
        esp_at_port_write_data(buffer, strlen((char *)buffer));
    }

    parse_result = esp_at_get_para_as_str(num_index++, &para_str_3);
    if (parse_result != ESP_AT_PARA_PARSE_RET_OMITTED) {
        if (parse_result != ESP_AT_PARA_PARSE_RET_OK) {
            return ESP_AT_RESULT_CODE_ERROR;
        } else {
            // sample code
            // user needs to customize the operation
            memset(buffer, 0, 64);
            snprintf((char *)buffer, 64, "third parameter is: %s\r\n", para_str_3);
            esp_at_port_write_data(buffer, strlen((char *)buffer));
        }
    } else {
        // sample code
        // the third parameter is omitted
        // user needs to customize the operation
        memset(buffer, 0, 64);
        snprintf((char *)buffer, 64, "third parameter is omitted\r\n");
        esp_at_port_write_data(buffer, strlen((char *)buffer));
    }

    parse_result = esp_at_get_para_as_str(num_index++, &para_str_4);
    if (parse_result != ESP_AT_PARA_PARSE_RET_OK) {
        return ESP_AT_RESULT_CODE_ERROR;
    } else {
        memset(buffer, 0, 64);
        snprintf((char *)buffer, 64, "fourth parameter is: %s\r\n", para_str_4);
        esp_at_port_write_data(buffer, strlen((char *)buffer));
    }

    return ESP_AT_RESULT_CODE_OK;
}

Note

If the string parameter input is "", it is not omitted.

Omit the Last Parameter

Let’s say you want to make the string parameter <param_3> of AT+TEST optional, which is also the last parameter.

AT+TEST=<param_1>,<param_2>[,<param_3>]

There are two cases of omission:

  • AT+TEST=<param_1>,<param_2>

  • AT+TEST=<param_1>,<param_2>,

Below is the sample code to achieve it:

uint8_t at_setup_cmd_test(uint8_t para_num)
{
    int32_t para_int_1 = 0;
    uint8_t *para_str_2 = NULL;
    uint8_t *para_str_3 = NULL;
    uint8_t num_index = 0;
    uint8_t buffer[64] = {0};
    esp_at_para_parse_ret_t parse_result = ESP_AT_PARA_PARSE_RET_OK;

    snprintf((char *)buffer, 64, "this cmd is setup cmd and cmd num is: %u\r\n", para_num);
    esp_at_port_write_data(buffer, strlen((char *)buffer));

    parse_result = esp_at_get_para_as_digit(num_index++, &para_int_1);
    if (parse_result != ESP_AT_PARA_PARSE_RET_OK) {
        return ESP_AT_RESULT_CODE_ERROR;
    } else {
        memset(buffer, 0, 64);
        snprintf((char *)buffer, 64, "first parameter is: %d\r\n", para_int_1);
        esp_at_port_write_data(buffer, strlen((char *)buffer));
    }

    parse_result = esp_at_get_para_as_str(num_index++, &para_str_2);
    if (parse_result != ESP_AT_PARA_PARSE_RET_OK) {
        return ESP_AT_RESULT_CODE_ERROR;
    } else {
        memset(buffer, 0, 64);
        snprintf((char *)buffer, 64, "second parameter is: %s\r\n", para_str_2);
        esp_at_port_write_data(buffer, strlen((char *)buffer));
    }

    if (num_index == para_num) {
        memset(buffer, 0, 64);
        snprintf((char *)buffer, 64, "third parameter is omitted\r\n");
        esp_at_port_write_data(buffer, strlen((char *)buffer));
    } else {
        parse_result = esp_at_get_para_as_str(num_index++, &para_str_3);
        if (parse_result != ESP_AT_PARA_PARSE_RET_OMITTED) {
            if (parse_result != ESP_AT_PARA_PARSE_RET_OK) {
                return ESP_AT_RESULT_CODE_ERROR;
            } else {
                // sample code
                // user needs to customize the operation
                memset(buffer, 0, 64);
                snprintf((char *)buffer, 64, "third parameter is: %s\r\n", para_str_3);
                esp_at_port_write_data(buffer, strlen((char *)buffer));
            }
        } else {
            // sample code
            // the third parameter is omitted
            // user needs to customize the operation
            memset(buffer, 0, 64);
            snprintf((char *)buffer, 64, "third parameter is omitted\r\n");
            esp_at_port_write_data(buffer, strlen((char *)buffer));
        }
    }

    return ESP_AT_RESULT_CODE_OK;
}

Note

If the string parameter input is "", it is not omitted.

Block Command Execution

Sometimes you want to block the execution of one command to wait for another execution result, and the system may return different values according to the result.

Generally, this kind of command needs to synchronize the results of other tasks.

semaphore is recommended to handle synchronization.

The sample code is as follows:

xSemaphoreHandle at_operation_sema = NULL;

uint8_t at_exe_cmd_test(uint8_t *cmd_name)
{
    uint8_t buffer[64] = {0};

    snprintf((char *)buffer, 64, "this cmd is execute cmd: %s\r\n", cmd_name);

    esp_at_port_write_data(buffer, strlen((char *)buffer));

    // sample code
    // users do not have to create semaphores here
    at_operation_sema = xSemaphoreCreateBinary();
    assert(at_operation_sema != NULL);

    // block command execution
    // wait for another execution result
    // other tasks can call xSemaphoreGive to release the semaphore
    xSemaphoreTake(at_operation_sema, portMAX_DELAY);

    return ESP_AT_RESULT_CODE_OK;
}

Access Input Data from AT Command Port

ESP-AT reads input data from the AT command port in two ways. Use esp_at_port_read_bytes() for one transfer of a known length. Use esp_at_port_read_data() when the length is unknown, or when the payload is long and arrives continuously, such as audio. The second way keeps the port owned for the whole transfer, so the read ownership is not taken and released on every chunk.

  • esp_at_port_read_bytes() reads an exact length in one call. It takes the port from the AT command parser, so the following bytes are received as raw data. It writes the prompt selected by prompt, then returns the port after the length is reached or the call fails, and hands any leftover bytes back to the AT command parser. The caller does not call esp_at_port_enter_specific() or esp_at_port_exit_specific(). On success the return value equals the requested length. On failure the return value is a negative error code, and the buffer contents are undefined. Call it from an AT command handler. Do not call it again before the previous call returns. Each call takes and releases the port.

  • esp_at_port_read_data() reads only the bytes already available. The return value may be less than the requested length. 0 means no new data. It does not take or release the port. Call it only after esp_at_port_enter_specific() and before esp_at_port_exit_specific(). It writes no prompt, has no timeout, and does not treat +++ as special. The caller hands leftover bytes back to the AT command parser with esp_at_port_recv_data_notify(). Input of unspecified length and a long continuous payload both call this function repeatedly after one enter_specific, then call exit_specific when the transfer is finished.

prompt tells the host MCU that AT is ready to receive data. The MCU sends the payload after it receives the prompt. Bytes sent earlier can be parsed as an AT command.

  • ESP_AT_PORT_PROMPT_NONE: write nothing. Use it when the handler has already signaled the MCU.

  • ESP_AT_PORT_PROMPT_SIMPLE: write >.

  • ESP_AT_PORT_PROMPT_OK: write OK, then >. This OK is separate from the result code returned by the command handler. Return ESP_AT_RESULT_CODE_OK when the command should produce another OK.

Input Data of Specified Length

Assume that the command gives the data length in <param_1>:

AT+TEST=<param_1>

The sample below is the esp_at_port_read_bytes() usage. The handler reads <param_1> bytes. The host MCU sends those bytes after it receives >. portMAX_DELAY waits until the length is reached or the read fails. A lone +++ aborts the read when at least 3 bytes are still expected. Bytes beyond the specified length are handed back to the AT command parser.

When <param_1> is large, or the data arrives continuously (for example, audio), use esp_at_port_read_data() as shown in the next section.

uint8_t at_setup_cmd_test(uint8_t para_num)
{
    int32_t specified_len = 0;
    uint8_t *buf = NULL;
    uint8_t buffer[64] = {0};

    if (esp_at_get_para_as_digit(0, &specified_len) != ESP_AT_PARA_PARSE_RET_OK) {
        return ESP_AT_RESULT_CODE_ERROR;
    }
    if (specified_len <= 0) {
        return ESP_AT_RESULT_CODE_ERROR;
    }

    buf = (uint8_t *)malloc(specified_len);
    if (buf == NULL) {
        snprintf((char *)buffer, 64, "malloc failed\r\n");
        esp_at_port_write_data(buffer, strlen((char *)buffer));
        return ESP_AT_RESULT_CODE_ERROR;
    }

    // write the ">" prompt
    int32_t received_len = esp_at_port_read_bytes(buf, specified_len,
                                                  ESP_AT_PORT_PROMPT_SIMPLE,
                                                  portMAX_DELAY);
    if (received_len != specified_len) {
        free(buf);
        return ESP_AT_RESULT_CODE_ERROR;
    }

    snprintf((char *)buffer, 64, "\r\nreceived data is: ");
    esp_at_port_write_data(buffer, strlen((char *)buffer));
    esp_at_port_write_data(buf, specified_len);

    free(buf);
    return ESP_AT_RESULT_CODE_OK;
}

After AT+TEST=5, the input 12345 produces the following ESP-AT output.

AT+TEST=5
>12345
received data is: 12345
OK

Input Data of Unspecified Length

The sample below is the esp_at_port_read_data() usage. Input of unspecified length and a long continuous payload (for example, audio) both use it: call esp_at_port_enter_specific() once, read repeatedly, then call esp_at_port_exit_specific() when the transfer is finished. The port stays owned for the whole transfer. The handler writes the prompt itself. The MCU sends data after it receives the prompt.

AT+TEST

Assuming that ESP-AT ends the execution of the command and returns the execution result, the sample code is as follows:

#define BUFFER_LEN (2048)
static xSemaphoreHandle at_sync_sema = NULL;

void wait_data_callback(void)
{
    xSemaphoreGive(at_sync_sema);
}

uint8_t at_exe_cmd_test(uint8_t *cmd_name)
{
    int32_t received_len = 0;
    int32_t remain_len = 0;
    uint8_t *buf = NULL;
    uint8_t buffer[64] = {0};


    buf = (uint8_t *)malloc(BUFFER_LEN);
    if (buf == NULL) {
        memset(buffer, 0, 64);
        snprintf((char *)buffer, 64, "malloc failed\r\n");
        esp_at_port_write_data(buffer, strlen((char *)buffer));
    }

    // sample code
    // users do not have to create semaphores here
    if (!at_sync_sema) {
        at_sync_sema = xSemaphoreCreateBinary();
        assert(at_sync_sema != NULL);
    }

    // output input prompt ">"
    esp_at_port_write_data((uint8_t *)">", strlen(">"));

    // set the callback function which will be called by AT port after receiving the input data
    esp_at_port_enter_specific(wait_data_callback);

    // receive input data
    while(xSemaphoreTake(at_sync_sema, portMAX_DELAY)) {
        memset(buf, 0, BUFFER_LEN);

        received_len = esp_at_port_read_data(buf, BUFFER_LEN);
        // check whether to exit the mode
        // the exit condition is the "+++" string received
        if ((received_len == 3) && (strncmp((const char *)buf, "+++", 3)) == 0) {
            esp_at_port_exit_specific();

            // sample code
            // if the remaining data length > 0, it means that there is still data left in the buffer to be processed
            // users can customize the operation to process the remaining data
            // here is just a simple print out of the remaining data
            remain_len = esp_at_port_get_data_length();
            if (remain_len > 0) {
                esp_at_port_recv_data_notify(remain_len, portMAX_DELAY);
            }

            break;
        } else if (received_len > 0) {
            // sample code
            // users can customize the operation to process the received data
            // here is just a simple print received data
            memset(buffer, 0, 64);
            snprintf((char *)buffer, 64, "\r\nreceived data is: ");
            esp_at_port_write_data(buffer, strlen((char *)buffer));

            esp_at_port_write_data(buf, strlen((char *)buf));
        }
    }

    free(buf);

    return ESP_AT_RESULT_CODE_OK;
}

So, if the first input data is 1234567890, and the second input data is +++, the ESP-AT output is as follows:

AT+TEST
>
received data is: 1234567890
OK