From 3c1624a0938b92700c1ab21678cd3d418e173767 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 26 Jun 2025 17:09:21 +0200 Subject: [PATCH 1/6] feat(modem_sim): Modem simulator based on esp-at --- .github/workflows/modem_sim__build.yml | 28 +++ .pre-commit-config.yaml | 4 +- common_components/modem_sim/export.sh | 11 + common_components/modem_sim/install.sh | 59 +++++ .../modem_sim/pppd_cmd/CMakeLists.txt | 14 ++ .../modem_sim/pppd_cmd/custom/at_custom_cmd.c | 223 ++++++++++++++++++ .../pppd_cmd/include/at_custom_cmd.h | 12 + .../modem_sim/sdkconfig.defaults | 77 ++++++ 8 files changed, 426 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/modem_sim__build.yml create mode 100755 common_components/modem_sim/export.sh create mode 100755 common_components/modem_sim/install.sh create mode 100644 common_components/modem_sim/pppd_cmd/CMakeLists.txt create mode 100644 common_components/modem_sim/pppd_cmd/custom/at_custom_cmd.c create mode 100644 common_components/modem_sim/pppd_cmd/include/at_custom_cmd.h create mode 100644 common_components/modem_sim/sdkconfig.defaults diff --git a/.github/workflows/modem_sim__build.yml b/.github/workflows/modem_sim__build.yml new file mode 100644 index 0000000000..4515c87a90 --- /dev/null +++ b/.github/workflows/modem_sim__build.yml @@ -0,0 +1,28 @@ +name: "modem_sim: build-tests" + +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened, labeled] + +jobs: + build_modem_sim: + if: contains(github.event.pull_request.labels.*.name, 'modem_sim') || github.event_name == 'push' + name: Build + strategy: + matrix: + idf_ver: ["release-v5.4"] + runs-on: ubuntu-22.04 + container: espressif/idf:${{ matrix.idf_ver }} + steps: + - name: Checkout esp-protocols + uses: actions/checkout@v3 + - name: Build ESP-AT with IDF-${{ matrix.idf_ver }} + shell: bash + run: | + cd common_components/modem_sim + ./install.sh + source export.sh + idf.py build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c589b45b4e..2eee6e3944 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,8 +61,8 @@ repos: - repo: local hooks: - id: commit message scopes - name: "commit message must be scoped with: mdns, dns, modem, websocket, asio, mqtt_cxx, console, common, eppp, tls_cxx, mosq, sockutls, lws" - entry: '\A(?!(feat|fix|ci|bump|test|docs|chore)\((mdns|dns|modem|common|console|websocket|asio|mqtt_cxx|examples|eppp|tls_cxx|mosq|sockutls|lws)\)\:)' + name: "commit message must be scoped with: mdns, dns, modem, websocket, asio, mqtt_cxx, console, common, eppp, tls_cxx, mosq, sockutls, lws, modem_sim" + entry: '\A(?!(feat|fix|ci|bump|test|docs|chore)\((mdns|dns|modem|common|console|websocket|asio|mqtt_cxx|examples|eppp|tls_cxx|mosq|sockutls|lws|modem_sim)\)\:)' language: pygrep args: [--multiline] stages: [commit-msg] diff --git a/common_components/modem_sim/export.sh b/common_components/modem_sim/export.sh new file mode 100755 index 0000000000..5ac6e2da19 --- /dev/null +++ b/common_components/modem_sim/export.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +source $IDF_PATH/export.sh + +export AT_CUSTOM_COMPONENTS="`pwd`/pppd_cmd" + +cd modem_sim_esp32/esp-at + +python -m pip install -r requirements.txt + +python build.py reconfigure diff --git a/common_components/modem_sim/install.sh b/common_components/modem_sim/install.sh new file mode 100755 index 0000000000..a1ca7ed1d3 --- /dev/null +++ b/common_components/modem_sim/install.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +# Create directory "modem_sim_esp32", go inside it +# Usage: ./install.sh [platform] [module] + +SCRIPT_DIR=$(pwd) +mkdir -p modem_sim_esp32 +cd modem_sim_esp32 + +# Shallow clone https://github.com/espressif/esp-at.git +if [ ! -d "esp-at" ]; then + git clone --depth 1 https://github.com/espressif/esp-at.git +else + echo "esp-at directory already exists, skipping clone." +fi + +cd esp-at + +# Add esp-idf directory which is a symlink to the $IDF_PATH +if [ -z "$IDF_PATH" ]; then + echo "Error: IDF_PATH environment variable is not set" + exit 1 +fi + +if [ ! -L "esp-idf" ]; then + ln -sf "$IDF_PATH" esp-idf +else + echo "esp-idf symlink already exists, skipping." +fi + +# Create "build" directory +mkdir -p build + +# Default values for platform and module +platform="PLATFORM_ESP32" +module="WROOM-32" + +# Override defaults if parameters are provided +if [ ! -z "$1" ]; then + platform="$1" +fi +if [ ! -z "$2" ]; then + module="$2" +fi + +# Create file "build/module_info.json" with content +cat > build/module_info.json << EOF +{ + "platform": "$platform", + "module": "$module", + "description": "4MB, Wi-Fi + BLE, OTA, TX:17 RX:16", + "silence": 0 +} +EOF + +cp "$SCRIPT_DIR/sdkconfig.defaults" "module_config/module_esp32_default/sdkconfig.defaults" + +echo "Installation completed successfully!" +echo "Created modem_sim_esp32 directory with esp-at repository and configuration" diff --git a/common_components/modem_sim/pppd_cmd/CMakeLists.txt b/common_components/modem_sim/pppd_cmd/CMakeLists.txt new file mode 100644 index 0000000000..28aba2a308 --- /dev/null +++ b/common_components/modem_sim/pppd_cmd/CMakeLists.txt @@ -0,0 +1,14 @@ + +file(GLOB_RECURSE srcs *.c) + +set(includes "include") + +# Add more required components you need here, separated by spaces +set(require_components at freertos nvs_flash) + +idf_component_register( + SRCS ${srcs} + INCLUDE_DIRS ${includes} + REQUIRES ${require_components}) + +idf_component_set_property(${COMPONENT_NAME} WHOLE_ARCHIVE TRUE) diff --git a/common_components/modem_sim/pppd_cmd/custom/at_custom_cmd.c b/common_components/modem_sim/pppd_cmd/custom/at_custom_cmd.c new file mode 100644 index 0000000000..68d5059a84 --- /dev/null +++ b/common_components/modem_sim/pppd_cmd/custom/at_custom_cmd.c @@ -0,0 +1,223 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include "esp_at.h" +#include "driver/gpio.h" +#include "driver/uart.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/semphr.h" +#include "freertos/queue.h" +#include "esp_netif.h" +#include "esp_netif_ppp.h" +#include "esp_check.h" + +extern uint8_t g_at_cmd_port; + +static uint8_t at_test_cmd_test(uint8_t *cmd_name) +{ + uint8_t buffer[64] = {0}; + snprintf((char *)buffer, 64, "test command: is executed\r\n", cmd_name); + esp_at_port_write_data(buffer, strlen((char *)buffer)); + + return ESP_AT_RESULT_CODE_OK; +} + +static uint8_t at_query_cmd_test(uint8_t *cmd_name) +{ + uint8_t buffer[64] = {0}; + snprintf((char *)buffer, 64, "query command: is executed\r\n", cmd_name); + esp_at_port_write_data(buffer, strlen((char *)buffer)); + + return ESP_AT_RESULT_CODE_OK; +} + +static uint8_t at_setup_cmd_test(uint8_t para_num) +{ + uint8_t index = 0; + printf("setup command: is executed\r\n", esp_at_get_current_cmd_name(), para_num); + + // 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_RESULT_OK) { + return ESP_AT_RESULT_CODE_ERROR; + } + printf("digit: %d\r\n", digit); + + // 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_RESULT_OK) { + return ESP_AT_RESULT_CODE_ERROR; + } + printf("string: %s\r\n", str); + + // 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: 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; +} + +#define TAG "at_custom_cmd" +static esp_netif_t *s_netif = NULL; + +static void on_ppp_event(void *arg, esp_event_base_t base, int32_t event_id, void *data) +{ + esp_netif_t **netif = data; + if (base == NETIF_PPP_STATUS && event_id == NETIF_PPP_ERRORUSER) { + printf("Disconnected!"); + } +} + +static void on_ip_event(void *arg, esp_event_base_t base, int32_t event_id, void *data) +{ + ip_event_got_ip_t *event = (ip_event_got_ip_t *)data; + esp_netif_t *netif = event->esp_netif; + if (event_id == IP_EVENT_PPP_GOT_IP) { + printf("Got IPv4 event: Interface \"%s(%s)\" address: " IPSTR, esp_netif_get_desc(netif), + esp_netif_get_ifkey(netif), IP2STR(&event->ip_info.ip)); + ESP_ERROR_CHECK(esp_netif_napt_enable(s_netif)); + + } else if (event_id == IP_EVENT_PPP_LOST_IP) { + ESP_LOGI(TAG, "Disconnected"); + } +} + +static SemaphoreHandle_t at_sync_sema = NULL; +static void wait_data_callback(void) +{ + static uint8_t buffer[1024] = {0}; + // xSemaphoreGive(at_sync_sema); + int len = esp_at_port_read_data(buffer, sizeof(buffer) - 1); + ESP_LOG_BUFFER_HEXDUMP("ppp_uart_recv", buffer, len, ESP_LOG_VERBOSE); + esp_netif_receive(s_netif, buffer, len, NULL); +} + +static esp_err_t transmit(void *h, void *buffer, size_t len) +{ + // struct eppp_handle *handle = h; + printf("transmit: %d bytes\n", len); + // ESP_LOG_BUFFER_HEXDUMP("ppp_uart_send", buffer, len, ESP_LOG_INFO); + esp_at_port_write_data(buffer, len); + return ESP_OK; +} + +static uint8_t at_exe_cmd_test(uint8_t *cmd_name) +{ + uint8_t buffer[64] = {0}; + snprintf((char *)buffer, 64, "execute command: is executed\r\n", cmd_name); + esp_at_port_write_data(buffer, strlen((char *)buffer)); + printf("YYYEEES Command executed successfully\r\n", cmd_name); + if (!at_sync_sema) { + at_sync_sema = xSemaphoreCreateBinary(); + assert(at_sync_sema != NULL); + esp_netif_driver_ifconfig_t driver_cfg = { + .handle = (void *)1, + .transmit = transmit, + }; + const esp_netif_driver_ifconfig_t *ppp_driver_cfg = &driver_cfg; + + esp_netif_inherent_config_t base_netif_cfg = ESP_NETIF_INHERENT_DEFAULT_PPP(); + esp_netif_config_t netif_ppp_config = { .base = &base_netif_cfg, + .driver = ppp_driver_cfg, + .stack = ESP_NETIF_NETSTACK_DEFAULT_PPP + }; + + s_netif = esp_netif_new(&netif_ppp_config); + esp_netif_ppp_config_t netif_params; + ESP_ERROR_CHECK(esp_netif_ppp_get_params(s_netif, &netif_params)); + netif_params.ppp_our_ip4_addr.addr = ESP_IP4TOADDR(192, 168, 11, 1); + netif_params.ppp_their_ip4_addr.addr = ESP_IP4TOADDR(192, 168, 11, 2); + netif_params.ppp_error_event_enabled = true; + ESP_ERROR_CHECK(esp_netif_ppp_set_params(s_netif, &netif_params)); + if (esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, on_ip_event, NULL) != ESP_OK) { + printf("Failed to register IP event handler"); + } + if (esp_event_handler_register(NETIF_PPP_STATUS, ESP_EVENT_ANY_ID, on_ppp_event, NULL) != ESP_OK) { + printf("Failed to register NETIF_PPP_STATUS event handler"); + } + + + } + esp_at_port_write_data((uint8_t *)"CONNECT\r\n", strlen("CONNECT\r\n")); + + // set the callback function which will be called by AT port after receiving the input data + esp_at_port_enter_specific(wait_data_callback); + esp_netif_action_start(s_netif, 0, 0, 0); + esp_netif_action_connected(s_netif, 0, 0, 0); + + // receive input data + // while(xSemaphoreTake(at_sync_sema, portMAX_DELAY)) { + // int len = esp_at_port_read_data(buffer, sizeof(buffer) - 1); + // if (len > 0) { + // buffer[len] = '\0'; // null-terminate the string + // printf("Received data: %s\n", buffer); + // } else { + // printf("No data received or error occurred.\n"); + // continue; + // } + // } + + // uart_write_bytes(g_at_cmd_port, "CONNECT\r\n", strlen("CONNECT\r\n")); + while (1) { + vTaskDelay(pdMS_TO_TICKS(1000)); + printf("-"); + } + return ESP_AT_RESULT_CODE_OK; +} + +static uint8_t at_test_cereg(uint8_t *cmd_name) +{ + printf("%s: AT command is executed\r\n", __func__, cmd_name); + return ESP_AT_RESULT_CODE_OK; +} + +static uint8_t at_query_cereg(uint8_t *cmd_name) +{ + printf("%s: AT command is executed\r\n", __func__, cmd_name); + // static uint8_t buffer[] = "+CEREG: 0,1,2,3,4,5\r\n"; + static uint8_t buffer[] = "+CEREG: 7,8\r\n"; + esp_at_port_write_data(buffer, sizeof(buffer)); + return ESP_AT_RESULT_CODE_OK; +} + +static uint8_t at_setup_cereg(uint8_t num) +{ + printf("%s: AT command is executed\r\n", __func__, num); + return ESP_AT_RESULT_CODE_OK; +} + +static uint8_t at_exe_cereg(uint8_t *cmd_name) +{ + printf("%s: AT command is executed\r\n", __func__, cmd_name); + return ESP_AT_RESULT_CODE_OK; +} + + +static const esp_at_cmd_struct at_custom_cmd[] = { + {"+PPPD", at_test_cmd_test, at_query_cmd_test, at_setup_cmd_test, at_exe_cmd_test}, + {"+CEREG", at_test_cereg, at_query_cereg, at_setup_cereg, at_exe_cereg}, + /** + * @brief You can define your own AT commands here. + */ +}; + +bool esp_at_custom_cmd_register(void) +{ + return esp_at_custom_cmd_array_regist(at_custom_cmd, sizeof(at_custom_cmd) / sizeof(esp_at_cmd_struct)); +} + +ESP_AT_CMD_SET_INIT_FN(esp_at_custom_cmd_register, 1); diff --git a/common_components/modem_sim/pppd_cmd/include/at_custom_cmd.h b/common_components/modem_sim/pppd_cmd/include/at_custom_cmd.h new file mode 100644 index 0000000000..0aa0312eba --- /dev/null +++ b/common_components/modem_sim/pppd_cmd/include/at_custom_cmd.h @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once +#include "esp_at_core.h" +#include "esp_at.h" + +/** + * @brief You can include more header files here for your own AT commands. + */ diff --git a/common_components/modem_sim/sdkconfig.defaults b/common_components/modem_sim/sdkconfig.defaults new file mode 100644 index 0000000000..9c95f9c660 --- /dev/null +++ b/common_components/modem_sim/sdkconfig.defaults @@ -0,0 +1,77 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) 5.4.1 Project Minimal Configuration +# +CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y +CONFIG_APP_PROJECT_VER_FROM_CONFIG=y +CONFIG_APP_PROJECT_VER="v4.1.0.0-dev" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="module_config/module_esp32_default/partitions_at.csv" +CONFIG_PARTITION_TABLE_MD5=n +CONFIG_AT_CUSTOMIZED_PARTITION_TABLE_FILE="module_config/module_esp32_default/at_customize.csv" +CONFIG_BT_ENABLED=y +CONFIG_BT_BTU_TASK_STACK_SIZE=5120 +CONFIG_BT_BLE_BLUFI_ENABLE=y +CONFIG_BT_STACK_NO_LOG=y +CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY=y +CONFIG_BTDM_CTRL_MODE_BTDM=y +CONFIG_BTDM_CTRL_LPCLK_SEL_EXT_32K_XTAL=y +CONFIG_BTDM_SCAN_DUPL_CACHE_SIZE=200 +CONFIG_ESP_TLS_PSK_VERIFICATION=y +CONFIG_ESP_TLS_INSECURE=y +CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY=y +CONFIG_ESP_ERR_TO_NAME_LOOKUP=n +CONFIG_GPIO_ESP32_SUPPORT_SWITCH_SLP_PULL=y +CONFIG_ETH_DMA_RX_BUFFER_NUM=3 +CONFIG_ETH_DMA_TX_BUFFER_NUM=3 +CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 +CONFIG_HTTPD_MAX_URI_LEN=1024 +CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y +CONFIG_RTC_CLK_SRC_EXT_CRYS=y +CONFIG_RTC_EXT_CRYST_ADDIT_CURRENT=y +CONFIG_RTC_CLK_CAL_CYCLES=1024 +CONFIG_PM_ENABLE=y +CONFIG_PM_SLP_DISABLE_GPIO=y +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80=y +CONFIG_ESP_TASK_WDT_PANIC=y +CONFIG_ESP_TASK_WDT_TIMEOUT_S=60 +CONFIG_ESP_DEBUG_OCDAWARE=n +CONFIG_ESP_WIFI_IRAM_OPT=n +CONFIG_ESP_WIFI_RX_IRAM_OPT=n +CONFIG_ESP_WIFI_SLP_IRAM_OPT=y +CONFIG_ESP_WIFI_SLP_BEACON_LOST_OPT=y +CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM=0 +CONFIG_FATFS_LFN_HEAP=y +CONFIG_FREERTOS_UNICORE=y +CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_USE_TICKLESS_IDLE=y +CONFIG_FREERTOS_CHECK_MUTEX_GIVEN_BY_OWNER=n +CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH=y +CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH=y +CONFIG_LOG_DEFAULT_LEVEL_ERROR=y +CONFIG_LWIP_MAX_SOCKETS=16 +CONFIG_LWIP_SO_LINGER=y +CONFIG_LWIP_SO_RCVBUF=y +CONFIG_LWIP_IP4_REASSEMBLY=y +CONFIG_LWIP_IP6_REASSEMBLY=y +CONFIG_LWIP_IPV6_AUTOCONFIG=y +CONFIG_LWIP_TCP_MAXRTX=6 +CONFIG_LWIP_TCP_SYNMAXRTX=3 +CONFIG_LWIP_PPP_SUPPORT=y +CONFIG_LWIP_PPP_SERVER_SUPPORT=y +CONFIG_LWIP_SNTP_MAX_SERVERS=3 +CONFIG_LWIP_SNTP_STARTUP_DELAY=n +CONFIG_MBEDTLS_DYNAMIC_BUFFER=y +CONFIG_MBEDTLS_DYNAMIC_FREE_CONFIG_DATA=y +CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE=n +CONFIG_MBEDTLS_HAVE_TIME_DATE=y +CONFIG_MBEDTLS_DHM_C=y +CONFIG_NEWLIB_NANO_FORMAT=y +CONFIG_VFS_SUPPORT_TERMIOS=n +CONFIG_WL_SECTOR_SIZE_512=y +CONFIG_AT_PROCESS_TASK_STACK_SIZE=6144 +CONFIG_AT_MQTT_COMMAND_SUPPORT=y +CONFIG_AT_HTTP_COMMAND_SUPPORT=y +CONFIG_AT_BLE_COMMAND_SUPPORT=n +CONFIG_AT_BLE_HID_COMMAND_SUPPORT=n +CONFIG_AT_BLUFI_COMMAND_SUPPORT=n From f6abbf47f9a19366417e690ea163eb29b103dba6 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Wed, 2 Jul 2025 17:51:37 +0200 Subject: [PATCH 2/6] fix(modem): Fix URC handling, add esp-at based test This fixes urc regression cause by 6eceb28f7d694eceba89e68b29ea240bdc00fbef. when consuming the buffer --- .github/workflows/modem__build-host-tests.yml | 2 +- .../modem_sim/pppd_cmd/custom/at_custom_cmd.c | 195 ++++++++++++++++++ components/esp_modem/src/esp_modem_dte.cpp | 15 +- .../esp_modem/test/target_urc/CMakeLists.txt | 7 + .../test/target_urc/main/CMakeLists.txt | 2 + .../test/target_urc/main/idf_component.yml | 7 + .../test/target_urc/main/urc_test.cpp | 158 ++++++++++++++ 7 files changed, 376 insertions(+), 10 deletions(-) create mode 100644 components/esp_modem/test/target_urc/CMakeLists.txt create mode 100644 components/esp_modem/test/target_urc/main/CMakeLists.txt create mode 100644 components/esp_modem/test/target_urc/main/idf_component.yml create mode 100644 components/esp_modem/test/target_urc/main/urc_test.cpp diff --git a/.github/workflows/modem__build-host-tests.yml b/.github/workflows/modem__build-host-tests.yml index db55b7edcb..d28a30d116 100644 --- a/.github/workflows/modem__build-host-tests.yml +++ b/.github/workflows/modem__build-host-tests.yml @@ -45,7 +45,7 @@ jobs: strategy: matrix: idf_ver: ["release-v5.0", "release-v5.1", "release-v5.2", "release-v5.3", "release-v5.4", "latest"] - test: ["target", "target_ota", "target_iperf"] + test: ["target", "target_ota", "target_iperf", "target_urc"] runs-on: ubuntu-22.04 container: espressif/idf:${{ matrix.idf_ver }} diff --git a/common_components/modem_sim/pppd_cmd/custom/at_custom_cmd.c b/common_components/modem_sim/pppd_cmd/custom/at_custom_cmd.c index 68d5059a84..e668036e2a 100644 --- a/common_components/modem_sim/pppd_cmd/custom/at_custom_cmd.c +++ b/common_components/modem_sim/pppd_cmd/custom/at_custom_cmd.c @@ -16,6 +16,8 @@ #include "esp_netif.h" #include "esp_netif_ppp.h" #include "esp_check.h" +#include "esp_http_server.h" +#include "esp_timer.h" extern uint8_t g_at_cmd_port; @@ -73,6 +75,7 @@ static uint8_t at_setup_cmd_test(uint8_t para_num) #define TAG "at_custom_cmd" static esp_netif_t *s_netif = NULL; +static httpd_handle_t http_server = NULL; static void on_ppp_event(void *arg, esp_event_base_t base, int32_t event_id, void *data) { @@ -206,10 +209,202 @@ static uint8_t at_exe_cereg(uint8_t *cmd_name) return ESP_AT_RESULT_CODE_OK; } +/* HTTP Server handlers */ +static esp_err_t hello_get_handler(httpd_req_t *req) +{ + const char* resp_str = "Hello from ESP-AT HTTP Server!"; + httpd_resp_send(req, resp_str, HTTPD_RESP_USE_STRLEN); + return ESP_OK; +} + +static esp_err_t root_get_handler(httpd_req_t *req) +{ + const char* resp_str = "ESP-AT HTTP Server is running"; + httpd_resp_send(req, resp_str, HTTPD_RESP_USE_STRLEN); + return ESP_OK; +} + +static esp_err_t test_get_handler(httpd_req_t *req) +{ + const char* resp_str = "{\"status\":\"success\",\"message\":\"Test endpoint working\",\"timestamp\":12345}"; + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, resp_str, HTTPD_RESP_USE_STRLEN); + return ESP_OK; +} + +static esp_err_t async_get_handler(httpd_req_t *req) +{ + printf("Starting async chunked response handler\r\n"); + + // Set content type for plain text response + httpd_resp_set_type(req, "text/plain"); + + // Static counter to track requests + static uint8_t req_count = 0; + req_count++; + + // Send initial response with request count + char buffer[256]; + snprintf(buffer, sizeof(buffer), "=== Async Response #%d ===\r\n", req_count); + httpd_resp_sendstr_chunk(req, buffer); + + // Long message broken into chunks + const char* chunks[] = { + "This is a simulated slow server response.\r\n", + "Chunk 1: The ESP-AT HTTP server is demonstrating...\r\n", + "Chunk 2: ...asynchronous chunked transfer encoding...\r\n", + "Chunk 3: ...with artificial delays between chunks...\r\n", + "Chunk 4: ...to simulate real-world network conditions.\r\n", + "Chunk 5: Processing data... please wait...\r\n", + "Chunk 6: Still processing... almost done...\r\n", + "Chunk 7: Final chunk - transfer complete!\r\n", + "=== END OF RESPONSE ===\r\n" + }; + + int num_chunks = sizeof(chunks) / sizeof(chunks[0]); + + // Send each chunk with delays + for (int i = 0; i < num_chunks; i++) { + // Add a delay to simulate slow processing + vTaskDelay(pdMS_TO_TICKS(1500)); // 1.5 second delay between chunks + + // Add chunk number and timestamp + snprintf(buffer, sizeof(buffer), "[%d/%d] [%d ms] %s", + i + 1, num_chunks, (int)(esp_timer_get_time() / 1000), chunks[i]); + + printf("Sending chunk %d: %s", i + 1, chunks[i]); + httpd_resp_sendstr_chunk(req, buffer); + } + + // Add final summary + vTaskDelay(pdMS_TO_TICKS(500)); + snprintf(buffer, sizeof(buffer), "\r\nTransfer completed in %d chunks with delays.\r\n", num_chunks); + httpd_resp_sendstr_chunk(req, buffer); + + // Send NULL to signal end of chunked transfer + httpd_resp_sendstr_chunk(req, NULL); + + printf("Async chunked response completed\r\n"); + return ESP_OK; +} + +static const httpd_uri_t hello = { + .uri = "/hello", + .method = HTTP_GET, + .handler = hello_get_handler, + .user_ctx = NULL +}; + +static const httpd_uri_t root = { + .uri = "/", + .method = HTTP_GET, + .handler = root_get_handler, + .user_ctx = NULL +}; + +static const httpd_uri_t test = { + .uri = "/test", + .method = HTTP_GET, + .handler = test_get_handler, + .user_ctx = NULL +}; + +static const httpd_uri_t async_uri = { + .uri = "/async", + .method = HTTP_GET, + .handler = async_get_handler, + .user_ctx = NULL +}; + +static esp_err_t start_http_server(void) +{ + if (http_server != NULL) { + printf("HTTP server already running\r\n"); + return ESP_OK; + } + + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = 8080; + config.lru_purge_enable = true; + + printf("Starting HTTP server on port: %d\r\n", config.server_port); + if (httpd_start(&http_server, &config) == ESP_OK) { + printf("Registering URI handlers\r\n"); + httpd_register_uri_handler(http_server, &hello); + httpd_register_uri_handler(http_server, &root); + httpd_register_uri_handler(http_server, &test); + httpd_register_uri_handler(http_server, &async_uri); + return ESP_OK; + } + + printf("Error starting HTTP server!\r\n"); + return ESP_FAIL; +} + +static esp_err_t stop_http_server(void) +{ + if (http_server != NULL) { + httpd_stop(http_server); + http_server = NULL; + printf("HTTP server stopped\r\n"); + return ESP_OK; + } + return ESP_OK; +} + +/* HTTP Server AT Commands */ +static uint8_t at_test_httpd(uint8_t *cmd_name) +{ + uint8_t buffer[64] = {0}; + snprintf((char *)buffer, 64, "AT%s=<0/1> - Start/Stop HTTP server\r\n", cmd_name); + esp_at_port_write_data(buffer, strlen((char *)buffer)); + return ESP_AT_RESULT_CODE_OK; +} + +static uint8_t at_query_httpd(uint8_t *cmd_name) +{ + uint8_t buffer[64] = {0}; + snprintf((char *)buffer, 64, "+HTTPD:%d\r\n", http_server != NULL ? 1 : 0); + esp_at_port_write_data(buffer, strlen((char *)buffer)); + return ESP_AT_RESULT_CODE_OK; +} + +static uint8_t at_setup_httpd(uint8_t para_num) +{ + int32_t action = 0; + if (esp_at_get_para_as_digit(0, &action) != ESP_AT_PARA_PARSE_RESULT_OK) { + return ESP_AT_RESULT_CODE_ERROR; + } + + if (action == 1) { + if (start_http_server() == ESP_OK) { + printf("HTTP server started successfully\r\n"); + return ESP_AT_RESULT_CODE_OK; + } + } else if (action == 0) { + if (stop_http_server() == ESP_OK) { + return ESP_AT_RESULT_CODE_OK; + } + } + + return ESP_AT_RESULT_CODE_ERROR; +} + +static uint8_t at_exe_httpd(uint8_t *cmd_name) +{ + // Default action: start server + if (start_http_server() == ESP_OK) { + printf("HTTP server started via execute command\r\n"); + return ESP_AT_RESULT_CODE_OK; + } + return ESP_AT_RESULT_CODE_ERROR; +} + static const esp_at_cmd_struct at_custom_cmd[] = { {"+PPPD", at_test_cmd_test, at_query_cmd_test, at_setup_cmd_test, at_exe_cmd_test}, {"+CEREG", at_test_cereg, at_query_cereg, at_setup_cereg, at_exe_cereg}, + {"+HTTPD", at_test_httpd, at_query_httpd, at_setup_httpd, at_exe_httpd}, /** * @brief You can define your own AT commands here. */ diff --git a/components/esp_modem/src/esp_modem_dte.cpp b/components/esp_modem/src/esp_modem_dte.cpp index 3a9630c9c2..2c5385180c 100644 --- a/components/esp_modem/src/esp_modem_dte.cpp +++ b/components/esp_modem/src/esp_modem_dte.cpp @@ -367,24 +367,21 @@ void DTE::on_read(got_line_cb on_read_cb) bool DTE::command_cb::process_line(uint8_t *data, size_t consumed, size_t len) { + // returning true indicates that the processing finished and lower layers can destroy the accumulated buffer #ifdef CONFIG_ESP_MODEM_URC_HANDLER - command_result commandResult = command_result::FAIL; + bool consume_buffer = false; if (urc_handler) { - commandResult = urc_handler(data, consumed + len); + consume_buffer = urc_handler(data, consumed + len) != command_result::TIMEOUT; } - if (result != command_result::TIMEOUT && got_line == nullptr) { - return false; // this line has been processed already (got OK or FAIL previously) + if (result != command_result::TIMEOUT || got_line == nullptr) { + return consume_buffer; // this line has been processed already (got OK or FAIL previously) } #endif if (memchr(data + consumed, separator, len)) { - result = got_line(data + consumed, consumed + len); + result = got_line(data, consumed + len); if (result == command_result::OK || result == command_result::FAIL) { signal.set(GOT_LINE); -#ifdef CONFIG_ESP_MODEM_URC_HANDLER - return commandResult == command_result::OK; -#else return true; -#endif } } return false; diff --git a/components/esp_modem/test/target_urc/CMakeLists.txt b/components/esp_modem/test/target_urc/CMakeLists.txt new file mode 100644 index 0000000000..1aacbc890b --- /dev/null +++ b/components/esp_modem/test/target_urc/CMakeLists.txt @@ -0,0 +1,7 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +set(COMPONENTS main) +project(urc_test) diff --git a/components/esp_modem/test/target_urc/main/CMakeLists.txt b/components/esp_modem/test/target_urc/main/CMakeLists.txt new file mode 100644 index 0000000000..1a308290e5 --- /dev/null +++ b/components/esp_modem/test/target_urc/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "urc_test.cpp" + INCLUDE_DIRS ".") diff --git a/components/esp_modem/test/target_urc/main/idf_component.yml b/components/esp_modem/test/target_urc/main/idf_component.yml new file mode 100644 index 0000000000..e6a180d70b --- /dev/null +++ b/components/esp_modem/test/target_urc/main/idf_component.yml @@ -0,0 +1,7 @@ +## IDF Component Manager Manifest File +dependencies: + ## Required IDF version + idf: ">=4.1.0" + espressif/esp_modem: + version: "^1.0.0" + override_path: "../../../" diff --git a/components/esp_modem/test/target_urc/main/urc_test.cpp b/components/esp_modem/test/target_urc/main/urc_test.cpp new file mode 100644 index 0000000000..9d9de29dd5 --- /dev/null +++ b/components/esp_modem/test/target_urc/main/urc_test.cpp @@ -0,0 +1,158 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_netif.h" +#include "cxx_include/esp_modem_dte.hpp" +#include "esp_modem_config.h" +#include "cxx_include/esp_modem_api.hpp" +#include "cxx_include/esp_modem_dce_factory.hpp" +#include "cxx_include/esp_modem_command_library_utils.hpp" +#include "esp_log.h" +#include "sdkconfig.h" + +static const char *TAG = "urc_test"; +static EventGroupHandle_t s_event_group = nullptr; + +class ESP_AT_Module: public ::esp_modem::ModuleIf { +public: + explicit ESP_AT_Module(std::shared_ptr<::esp_modem::DTE> dte, const esp_modem_dce_config *config): + dte(std::move(dte)) {} + + bool setup_data_mode() override + { + // not using network here + return true; + } + + bool set_mode(::esp_modem::modem_mode mode) override + { + // we never allow mode change + return false; + } + +protected: + std::shared_ptr<::esp_modem::DTE> dte; +}; + +class DCE : public esp_modem::DCE_T { + using DCE_T::DCE_T; +public: + + + bool init() + { + for (int i = 0; i < 5; ++i) { + if (sync() == esp_modem::command_result::OK) { + ESP_LOGI(TAG, "Modem in sync"); + return true; + } + vTaskDelay(pdMS_TO_TICKS(500 * (i + 1))); + } + ESP_LOGE(TAG, "Failed to sync with esp-at"); + return false; + } + + esp_modem::command_result sync() + { + auto ret = esp_modem::dce_commands::generic_command_common(dte.get(), "AT\r\n"); + ESP_LOGI(TAG, "Syncing with esp-at...(%d)", static_cast(ret)); + return ret; + } + + bool http_get(const std::string &url) + { + std::string command = "AT+HTTPCGET=\"" + url + "\"\r\n"; + set_urc(handle_urc); + auto ret = dte->write(esp_modem::DTE_Command(command)); + ESP_LOGI(TAG, "HTTP GET...(%d)", static_cast(ret)); + return ret > 0; + } + + bool start_http_server() const + { + auto ret = esp_modem::dce_commands::generic_command_common(dte.get(), "AT+HTTPD\r\n"); + ESP_LOGI(TAG, "Start HTTP server...(%d)", static_cast(ret)); + return ret == esp_modem::command_result::OK; + } + + static constexpr int transfer_completed = 1; +private: + static esp_modem::command_result handle_urc(uint8_t *data, size_t len) + { + static int start_chunk = 0; + static int end_chunk = 0; + std::string_view chunk((const char*)data + start_chunk, len - start_chunk); + int newline = chunk.find('\n'); + if (newline == std::string_view::npos) { + end_chunk = len; // careful, this grows buffer usage + printf("."); + return esp_modem::command_result::TIMEOUT; + } + printf("%.*s\n", newline, (char*)data + start_chunk); + start_chunk = end_chunk; + // check for the last one + constexpr char last_chunk[] = "Transfer completed"; + if (memmem(data, len, last_chunk, sizeof(last_chunk) - 1) != nullptr) { + xEventGroupSetBits(s_event_group, transfer_completed); + } + return esp_modem::command_result::OK; + } +}; + +class Factory: public ::esp_modem::dce_factory::Factory { +public: + static std::unique_ptr create(const esp_modem::dce_config *config, std::shared_ptr dte, esp_netif_t *netif) + { + return build_generic_DCE>(config, std::move(dte), netif); + } +}; + +std::unique_ptr create(std::shared_ptr dte) +{ + esp_netif_config_t netif_ppp_config = ESP_NETIF_DEFAULT_PPP(); + static esp_netif_t *netif = esp_netif_new(&netif_ppp_config); + assert(netif); + + esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG("APN"); // dummy config (not used with esp-at) + return Factory::create(&dce_config, std::move(dte), netif); +} + +extern "C" void app_main(void) +{ + /* Init and register system/core components */ + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + s_event_group = xEventGroupCreate(); + + esp_modem_dte_config_t dte_config = ESP_MODEM_DTE_DEFAULT_CONFIG(); + dte_config.dte_buffer_size = 1024; + dte_config.uart_config.tx_io_num = 18; + dte_config.uart_config.rx_io_num = 17; + auto uart_dte = esp_modem::create_uart_dte(&dte_config); + if (uart_dte == nullptr) { + ESP_LOGE(TAG, "Failed to create UART DTE"); + return; + } + auto dce = create(std::move(uart_dte)); + if (!dce->init()) { + ESP_LOGE(TAG, "Failed to setup network"); + return; + } + + dce->start_http_server(); + + dce->http_get("http://127.0.0.1:8080/async"); + + EventBits_t bits = xEventGroupWaitBits(s_event_group, 1, pdTRUE, pdFALSE, pdMS_TO_TICKS(15000)); + if (bits & DCE::transfer_completed) { + ESP_LOGI(TAG, "Request finished!"); + } + dce->sync(); + vEventGroupDelete(s_event_group); + ESP_LOGI(TAG, "Done"); +} From 62ff2918e07510f4391a4a800f6eb1f84777fa72 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Tue, 1 Jul 2025 14:04:54 +0200 Subject: [PATCH 3/6] fix(modem): Fix to use compatible iterator types for std::search in new gcc GCC 15 enforces stricter type requirements for std::search, requiring both ranges to use the same iterator type or using raw pointers, but with a predicate function. This resolves a build error with GCC 15 while maintaining compatibility with older GCC versions. --- .../main/sock_commands_bg96.cpp | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/components/esp_modem/examples/modem_tcp_client/main/sock_commands_bg96.cpp b/components/esp_modem/examples/modem_tcp_client/main/sock_commands_bg96.cpp index 59e3f39877..04c7179e96 100644 --- a/components/esp_modem/examples/modem_tcp_client/main/sock_commands_bg96.cpp +++ b/components/esp_modem/examples/modem_tcp_client/main/sock_commands_bg96.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -18,14 +18,14 @@ using namespace esp_modem; command_result net_open(CommandableIf *t) { - ESP_LOGV(TAG, "%s", __func__ ); + ESP_LOGV(TAG, "%s", __func__); std::string out; auto ret = dce_commands::generic_get_string(t, "AT+QISTATE?\r", out, 1000); if (ret != command_result::OK) { return ret; } if (out.find("+QISTATE: 0") != std::string::npos) { - ESP_LOGV(TAG, "%s", out.data() ); + ESP_LOGV(TAG, "%s", out.data()); ESP_LOGD(TAG, "Already there"); return command_result::FAIL; } else if (out.empty()) { @@ -36,7 +36,7 @@ command_result net_open(CommandableIf *t) command_result net_close(CommandableIf *t) { - ESP_LOGV(TAG, "%s", __func__ ); + ESP_LOGV(TAG, "%s", __func__); dce_commands::generic_command(t, "AT+QICLOSE=0\r", "OK", "ERROR", 10000); esp_modem::Task::Delay(1000); return dce_commands::generic_command(t, "AT+QIDEACT=1\r", "OK", "ERROR", 40000); @@ -44,11 +44,11 @@ command_result net_close(CommandableIf *t) command_result tcp_open(CommandableIf *t, const std::string &host, int port, int timeout) { - ESP_LOGV(TAG, "%s", __func__ ); + ESP_LOGV(TAG, "%s", __func__); std::string ip_open = R"(AT+QIOPEN=1,0,"TCP",")" + host + "\"," + std::to_string(port) + "\r"; auto ret = dce_commands::generic_command(t, ip_open, "+QIOPEN: 0,0", "ERROR", timeout); if (ret != command_result::OK) { - ESP_LOGE(TAG, "%s Failed", __func__ ); + ESP_LOGE(TAG, "%s Failed", __func__); return ret; } return command_result::OK; @@ -56,27 +56,27 @@ command_result tcp_open(CommandableIf *t, const std::string &host, int port, int command_result tcp_close(CommandableIf *t) { - ESP_LOGV(TAG, "%s", __func__ ); + ESP_LOGV(TAG, "%s", __func__); return dce_commands::generic_command(t, "AT+QICLOSE=0\r", "OK", "ERROR", 10000); } command_result tcp_send(CommandableIf *t, uint8_t *data, size_t len) { - ESP_LOGV(TAG, "%s", __func__ ); + ESP_LOGV(TAG, "%s", __func__); assert(0); // Remove when fix done return command_result::FAIL; } command_result tcp_recv(CommandableIf *t, uint8_t *data, size_t len, size_t &out_len) { - ESP_LOGV(TAG, "%s", __func__ ); + ESP_LOGV(TAG, "%s", __func__); assert(0); // Remove when fix done return command_result::FAIL; } command_result get_ip(CommandableIf *t, std::string &ip) { - ESP_LOGV(TAG, "%s", __func__ ); + ESP_LOGV(TAG, "%s", __func__); std::string out; auto ret = dce_commands::generic_get_string(t, "AT+QIACT?\r", out, 5000); if (ret != command_result::OK) { @@ -130,7 +130,12 @@ Responder::ret Responder::recv(uint8_t *data, size_t len) auto *recv_data = (char *)data; if (data_to_recv == 0) { const std::string_view head = "+QIRD: "; - auto head_pos = std::search(recv_data, recv_data + len, head.begin(), head.end()); + // GCC 15 and later require the 5-argument version of std::search when using raw pointers, + // as the 4-argument overload is only available for ForwardIterators (not raw pointers). + auto head_pos = std::search(recv_data, recv_data + len, head.data(), head.data() + head.size(), [](char a, char b) { + return a == b; + } + ); if (head_pos == recv_data + len) { return ret::FAIL; } From 6b64e02b6074b2156dfab6f1223d7b39b01c6f95 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 4 Jul 2025 11:40:37 +0200 Subject: [PATCH 4/6] fix(modem): Fixup CI --- components/esp_modem/test/target_urc/sdkconfig.defaults | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 components/esp_modem/test/target_urc/sdkconfig.defaults diff --git a/components/esp_modem/test/target_urc/sdkconfig.defaults b/components/esp_modem/test/target_urc/sdkconfig.defaults new file mode 100644 index 0000000000..5ab863acb3 --- /dev/null +++ b/components/esp_modem/test/target_urc/sdkconfig.defaults @@ -0,0 +1,3 @@ +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_LWIP_PPP_SUPPORT=y +CONFIG_ESP_MODEM_URC_HANDLER=y From 65bcc0909bb93f091122a3c70e61bc5e0102c484 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Mon, 7 Jul 2025 10:18:30 +0200 Subject: [PATCH 5/6] fix(modem): Use another public broker --- .../esp_modem/examples/pppos_client/main/Kconfig.projbuild | 2 +- .../examples/simple_cmux_client/sdkconfig.ci.sim800_cmux | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/esp_modem/examples/pppos_client/main/Kconfig.projbuild b/components/esp_modem/examples/pppos_client/main/Kconfig.projbuild index 2bb3e4c551..836d94c039 100644 --- a/components/esp_modem/examples/pppos_client/main/Kconfig.projbuild +++ b/components/esp_modem/examples/pppos_client/main/Kconfig.projbuild @@ -185,7 +185,7 @@ menu "Example Configuration" config EXAMPLE_MQTT_BROKER_URI string "MQTT Broker URL" - default "mqtt://mqtt.eclipseprojects.io" + default "mqtt://test.mosquitto.org" help URL of the mqtt broker which this example connects to. diff --git a/components/esp_modem/examples/simple_cmux_client/sdkconfig.ci.sim800_cmux b/components/esp_modem/examples/simple_cmux_client/sdkconfig.ci.sim800_cmux index 6a22a89f12..7154d2fc39 100644 --- a/components/esp_modem/examples/simple_cmux_client/sdkconfig.ci.sim800_cmux +++ b/components/esp_modem/examples/simple_cmux_client/sdkconfig.ci.sim800_cmux @@ -16,4 +16,4 @@ CONFIG_COMPILER_CXX_EXCEPTIONS=y CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 CONFIG_EXAMPLE_CLOSE_CMUX_AT_END=y CONFIG_EXAMPLE_MQTT_TEST_TOPIC="/ci/esp-modem/pppos-client" -CONFIG_BROKER_URI="mqtt://mqtt.eclipseprojects.io" +CONFIG_BROKER_URI="mqtt://test.mosquitto.org" From f025e58dd77b007c426ceac83d1a75ea1635c370 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Mon, 7 Jul 2025 17:12:29 +0200 Subject: [PATCH 6/6] fix(modem): Add support for ESP-AT based tcp-client example --- .../modem_tcp_client/main/CMakeLists.txt | 2 + .../modem_tcp_client/main/Kconfig.projbuild | 18 + .../main/sock_commands_espat.cpp | 339 ++++++++++++++++++ 3 files changed, 359 insertions(+) create mode 100644 components/esp_modem/examples/modem_tcp_client/main/sock_commands_espat.cpp diff --git a/components/esp_modem/examples/modem_tcp_client/main/CMakeLists.txt b/components/esp_modem/examples/modem_tcp_client/main/CMakeLists.txt index 3788c86081..974b597541 100644 --- a/components/esp_modem/examples/modem_tcp_client/main/CMakeLists.txt +++ b/components/esp_modem/examples/modem_tcp_client/main/CMakeLists.txt @@ -2,6 +2,8 @@ if (CONFIG_EXAMPLE_MODEM_DEVICE_BG96) set(device_srcs sock_commands_bg96.cpp) elseif(CONFIG_EXAMPLE_MODEM_DEVICE_SIM7600) set(device_srcs sock_commands_sim7600.cpp) +elseif(CONFIG_EXAMPLE_MODEM_DEVICE_ESPAT) + set(device_srcs sock_commands_espat.cpp) endif() if(CONFIG_ESP_MODEM_ENABLE_DEVELOPMENT_MODE) diff --git a/components/esp_modem/examples/modem_tcp_client/main/Kconfig.projbuild b/components/esp_modem/examples/modem_tcp_client/main/Kconfig.projbuild index 988cade41a..5911579f5c 100644 --- a/components/esp_modem/examples/modem_tcp_client/main/Kconfig.projbuild +++ b/components/esp_modem/examples/modem_tcp_client/main/Kconfig.projbuild @@ -18,8 +18,26 @@ menu "Example Configuration" bool "SIM7600" help SIM7600 is Multi-Band LTE-TDD/LTE-FDD/HSPA+ and GSM/GPRS/EDGE module + config EXAMPLE_MODEM_DEVICE_ESPAT + bool "ESP-AT" + help + ESP-AT firmware for ESP32 modules with WiFi connectivity endchoice + if EXAMPLE_MODEM_DEVICE_ESPAT + config EXAMPLE_WIFI_SSID + string "WiFi SSID" + default "myssid" + help + SSID (network name) to connect to. + + config EXAMPLE_WIFI_PASSWORD + string "WiFi Password" + default "mypassword" + help + WiFi password (WPA or WPA2). + endif + config EXAMPLE_MODEM_APN string "Set MODEM APN" default "internet" diff --git a/components/esp_modem/examples/modem_tcp_client/main/sock_commands_espat.cpp b/components/esp_modem/examples/modem_tcp_client/main/sock_commands_espat.cpp new file mode 100644 index 0000000000..5573e45e9b --- /dev/null +++ b/components/esp_modem/examples/modem_tcp_client/main/sock_commands_espat.cpp @@ -0,0 +1,339 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "sock_commands.hpp" +#include "cxx_include/esp_modem_command_library_utils.hpp" +#include "sock_dce.hpp" + +static const char *TAG = "sock_commands_espat"; + +namespace sock_commands { + +using namespace esp_modem; + +command_result net_open(CommandableIf *t) +{ + ESP_LOGV(TAG, "%s", __func__); + + // Set WiFi mode to station + auto ret = dce_commands::generic_command(t, "AT+CWMODE=1\r", "OK", "ERROR", 5000); + if (ret != command_result::OK) { + ESP_LOGE(TAG, "Failed to set WiFi mode"); + return ret; + } + + // Connect to WiFi network + std::string wifi_cmd = "AT+CWJAP=\"" CONFIG_EXAMPLE_WIFI_SSID "\",\"" CONFIG_EXAMPLE_WIFI_PASSWORD "\"\r"; + ret = dce_commands::generic_command(t, wifi_cmd, "OK", "ERROR", 15000); + if (ret != command_result::OK) { + ESP_LOGE(TAG, "Failed to connect to WiFi"); + return ret; + } + + ESP_LOGI(TAG, "WiFi connected successfully"); + return command_result::OK; +} + +command_result net_close(CommandableIf *t) +{ + ESP_LOGV(TAG, "%s", __func__); + // Disconnect from WiFi + auto ret = dce_commands::generic_command(t, "AT+CWQAP\r", "OK", "ERROR", 5000); + if (ret != command_result::OK) { + ESP_LOGW(TAG, "Failed to disconnect WiFi (may already be disconnected)"); + } + return command_result::OK; +} + +command_result tcp_open(CommandableIf *t, const std::string &host, int port, int timeout) +{ + ESP_LOGV(TAG, "%s", __func__); + + // Set single connection mode (just in case) + auto ret = dce_commands::generic_command(t, "AT+CIPMUX=0\r", "OK", "ERROR", 1000); + if (ret != command_result::OK) { + ESP_LOGW(TAG, "Failed to set single connection mode"); + } + + // Establish TCP connection + std::string tcp_cmd = "AT+CIPSTART=\"TCP\",\"" + host + "\"," + std::to_string(port) + "\r"; + ret = dce_commands::generic_command(t, tcp_cmd, "CONNECT", "ERROR", timeout); + if (ret != command_result::OK) { + ESP_LOGE(TAG, "Failed to establish TCP connection to %s:%d", host.c_str(), port); + return ret; + } + + ESP_LOGI(TAG, "TCP connection established to %s:%d", host.c_str(), port); + return command_result::OK; +} + +command_result tcp_close(CommandableIf *t) +{ + ESP_LOGV(TAG, "%s", __func__); + return dce_commands::generic_command(t, "AT+CIPCLOSE\r", "CLOSED", "ERROR", 5000); +} + +command_result tcp_send(CommandableIf *t, uint8_t *data, size_t len) +{ + ESP_LOGV(TAG, "%s", __func__); + // This function is not used in the current implementation + // Data sending is handled by the DCE responder + return command_result::FAIL; +} + +command_result tcp_recv(CommandableIf *t, uint8_t *data, size_t len, size_t &out_len) +{ + ESP_LOGV(TAG, "%s", __func__); + // This function is not used in the current implementation + // Data receiving is handled by the DCE responder + return command_result::FAIL; +} + +command_result get_ip(CommandableIf *t, std::string &ip) +{ + ESP_LOGV(TAG, "%s", __func__); + std::string out; + auto ret = dce_commands::generic_get_string(t, "AT+CIFSR\r", out, 5000); + if (ret != command_result::OK) { + return ret; + } + + // Parse station IP from response + // Expected format: +CIFSR:STAIP,"192.168.1.100" + auto pos = out.find("+CIFSR:STAIP,\""); + if (pos != std::string::npos) { + pos += 14; // Move past "+CIFSR:STAIP,\"" + auto end_pos = out.find("\"", pos); + if (end_pos != std::string::npos) { + ip = out.substr(pos, end_pos - pos); + ESP_LOGI(TAG, "Got IP address: %s", ip.c_str()); + return command_result::OK; + } + } + + ESP_LOGE(TAG, "Failed to parse IP address from response"); + return command_result::FAIL; +} + +command_result set_rx_mode(CommandableIf *t, int mode) +{ + ESP_LOGV(TAG, "%s", __func__); + // Set passive receive mode (1) for better control + // Active mode (0) would send +IPD automatically + std::string cmd = "AT+CIPRECVTYPE=" + std::to_string(mode) + "\r"; + return dce_commands::generic_command(t, cmd, "OK", "ERROR", 1000); +} + +} // sock_commands + +namespace sock_dce { + +void Responder::start_sending(size_t len) +{ + data_to_send = len; + send_stat = 0; + send_cmd("AT+CIPSEND=" + std::to_string(len) + "\r"); +} + +void Responder::start_receiving(size_t len) +{ + send_cmd("AT+CIPRECVDATA=" + std::to_string(len) + "\r"); +} + +bool Responder::start_connecting(std::string host, int port) +{ + std::string cmd = "AT+CIPSTART=\"TCP\",\"" + host + "\"," + std::to_string(port) + "\r"; + send_cmd(cmd); + return true; +} + +Responder::ret Responder::recv(uint8_t *data, size_t len) +{ + const int MIN_MESSAGE = 6; + size_t actual_len = 0; + auto *recv_data = (char *)data; + + if (data_to_recv == 0) { + const std::string_view head = "+CIPRECVDATA:"; + + // Find the response header + auto head_pos = std::search(recv_data, recv_data + len, head.data(), head.data() + head.size(), [](char a, char b) { + return a == b; + }); + + if (head_pos == recv_data + len) { + return ret::FAIL; + } + + // Find the end of the length field + auto next_comma = (char *)memchr(head_pos + head.size(), ',', MIN_MESSAGE); + if (next_comma == nullptr) { + return ret::FAIL; + } + + // Parse the actual length + if (std::from_chars(head_pos + head.size(), next_comma, actual_len).ec == std::errc::invalid_argument) { + ESP_LOGE(TAG, "Cannot convert length"); + return ret::FAIL; + } + + ESP_LOGD(TAG, "Received: actual len=%zu", actual_len); + if (actual_len == 0) { + ESP_LOGD(TAG, "No data received"); + return ret::FAIL; + } + + if (actual_len > buffer_size) { + ESP_LOGE(TAG, "Data too large: %zu > %zu", actual_len, buffer_size); + return ret::FAIL; + } + + // Move to the actual data after the comma + recv_data = next_comma + 1; + auto first_data_len = len - (recv_data - (char *)data); + + if (actual_len > first_data_len) { + on_read(recv_data, first_data_len); + data_to_recv = actual_len - first_data_len; + return ret::NEED_MORE_DATA; + } + on_read(recv_data, actual_len); + + } else if (data_to_recv > len) { // Continue receiving + on_read(recv_data, len); + data_to_recv -= len; + return ret::NEED_MORE_DATA; + + } else if (data_to_recv <= len) { // Last chunk + on_read(recv_data, data_to_recv); + actual_len = data_to_recv; + } + + // Look for "OK" marker after the data + char *ok_pos = nullptr; + if (actual_len + 1 + 2 /* OK */ <= len) { + ok_pos = (char *)memchr(recv_data + actual_len + 1, 'O', MIN_MESSAGE); + if (ok_pos == nullptr || ok_pos[1] != 'K') { + data_to_recv = 0; + return ret::FAIL; + } + } + + // Reset and prepare for next receive + data_to_recv = 0; + return ret::OK; +} + +Responder::ret Responder::send(uint8_t *data, size_t len) +{ + if (send_stat < 3) { + // Look for the '>' prompt + if (memchr(data, '>', len) == NULL) { + if (send_stat++ < 2) { + return ret::NEED_MORE_DATA; + } + ESP_LOGE(TAG, "Missed '>' prompt"); + return ret::FAIL; + } + + // Send the actual data + auto written = dte->write(&buffer[0], data_to_send); + if (written != data_to_send) { + ESP_LOGE(TAG, "Failed to write data: %d/%zu", written, data_to_send); + return ret::FAIL; + } + data_to_send = 0; + send_stat = 3; + } + return ret::IN_PROGRESS; +} + +Responder::ret Responder::send(std::string_view response) +{ + if (send_stat == 3) { + if (response.find("SEND OK") != std::string::npos) { + send_stat = 0; + return ret::OK; + } else if (response.find("SEND FAIL") != std::string::npos) { + ESP_LOGE(TAG, "Send failed"); + return ret::FAIL; + } else if (response.find("ERROR") != std::string::npos) { + ESP_LOGE(TAG, "Send error"); + return ret::FAIL; + } + } + return ret::IN_PROGRESS; +} + +Responder::ret Responder::connect(std::string_view response) +{ + if (response.find("CONNECT") != std::string::npos) { + ESP_LOGI(TAG, "TCP connected!"); + return ret::OK; + } + if (response.find("ERROR") != std::string::npos) { + ESP_LOGE(TAG, "Failed to connect"); + return ret::FAIL; + } + return ret::IN_PROGRESS; +} + +Responder::ret Responder::check_async_replies(status state, std::string_view &response) +{ + ESP_LOGD(TAG, "Response: %.*s", static_cast(response.size()), response.data()); + + // Handle WiFi status messages + if (response.find("WIFI CONNECTED") != std::string::npos) { + ESP_LOGI(TAG, "WiFi connected"); + } else if (response.find("WIFI DISCONNECTED") != std::string::npos) { + ESP_LOGW(TAG, "WiFi disconnected"); + } + + // Handle TCP status messages + if (response.find("CONNECT") != std::string::npos && state == status::CONNECTING) { + return connect(response); + } else if (response.find("CLOSED") != std::string::npos) { + ESP_LOGW(TAG, "TCP connection closed"); + return ret::FAIL; + } + + // Handle data notifications in active mode (if we switch to it later) + if (response.find("+IPD,") != std::string::npos) { + uint64_t data_ready = 1; + write(data_ready_fd, &data_ready, sizeof(data_ready)); + ESP_LOGD(TAG, "Data available notification"); + } + + if (state == status::SENDING) { + return send(response); + } else if (state == status::CONNECTING) { + return connect(response); + } + + return ret::IN_PROGRESS; +} + +Responder::ret Responder::process_data(status state, uint8_t *data, size_t len) +{ + if (state == status::SENDING) { + return send(data, len); + } + if (state == status::RECEIVING) { + return recv(data, len); + } + return ret::IN_PROGRESS; +} + +status Responder::pending() +{ + // For ESP-AT, we don't need a pending check like BG96 + // Just return current status + return status::SENDING; +} + +} // sock_dce