diff --git a/.github/workflows/modem__build-host-tests.yml b/.github/workflows/modem__build-host-tests.yml index db55b7edc..d28a30d11 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/.github/workflows/modem_sim__build.yml b/.github/workflows/modem_sim__build.yml new file mode 100644 index 000000000..4515c87a9 --- /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/.gitignore b/.gitignore index 9cf987f7e..4db695984 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,9 @@ docs/html # esp-idf managed components **/managed_components/** + +# modem simulator uses esp-at clone +common_components/modem_sim/modem_sim_esp32/ + +# repository release tools +release_notes.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c589b45b4..2eee6e394 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 000000000..5ac6e2da1 --- /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 000000000..2f39e9d31 --- /dev/null +++ b/common_components/modem_sim/install.sh @@ -0,0 +1,64 @@ +#!/bin/bash +set -e + +# 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 + +if [ -z "$IDF_PATH" ]; then + echo "Error: IDF_PATH environment variable is not set" + exit 1 +fi + +# Default ESP_AT_VERSION uses this specific commit from master to support new chips and features +ESP_AT_VERSION="aa9d7e0e9b741744f7bf5bec3bbf887cff033d5f" + +# Shallow clone of esp-at.git at $ESP_AT_VERSION +if [ ! -d "esp-at" ]; then + # cannot shallow clone from a specific commit, so we init, shallow fetch, and checkout + mkdir -p esp-at && cd esp-at && git init && git remote add origin https://github.com/espressif/esp-at.git + git fetch --depth 1 origin $ESP_AT_VERSION && git checkout $ESP_AT_VERSION +else + echo "esp-at directory already exists, skipping clone." + cd esp-at +fi + +# Add esp-idf directory which is a symlink to the $IDF_PATH +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 000000000..5fccd9e0d --- /dev/null +++ b/common_components/modem_sim/pppd_cmd/CMakeLists.txt @@ -0,0 +1,6 @@ +idf_component_register( + SRCS additional_commands.c + INCLUDE_DIRS include + REQUIRES at freertos nvs_flash) + +idf_component_set_property(${COMPONENT_NAME} WHOLE_ARCHIVE TRUE) diff --git a/common_components/modem_sim/pppd_cmd/additional_commands.c b/common_components/modem_sim/pppd_cmd/additional_commands.c new file mode 100644 index 000000000..e7cab25a5 --- /dev/null +++ b/common_components/modem_sim/pppd_cmd/additional_commands.c @@ -0,0 +1,411 @@ +/* + * 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" +#include "esp_http_server.h" +#include "esp_timer.h" + +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 httpd_handle_t http_server = 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[1500] = {0}; + int len = esp_at_port_read_data(buffer, sizeof(buffer) - 1); + + // Check for the escape sequence "+++" in the received data + const uint8_t escape_seq[] = "+++"; + uint8_t *escape_ptr = memmem(buffer, len, escape_seq, 3); + + if (escape_ptr != NULL) { + printf("Found +++ sequence, signal to the command processing thread\n"); + + int data_before_escape = escape_ptr - buffer; + if (data_before_escape > 0) { + esp_netif_receive(s_netif, buffer, data_before_escape, NULL); + } + + if (at_sync_sema) { + xSemaphoreGive(at_sync_sema); + } + return; + } + esp_netif_receive(s_netif, buffer, len, NULL); +} + +static esp_err_t transmit(void *h, void *buffer, size_t len) +{ + printf("transmit: %d bytes\n", len); + 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("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); + + while (xSemaphoreTake(at_sync_sema, pdMS_TO_TICKS(1000)) == pdFALSE) { + 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: 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 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}, +}; + +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 000000000..0aa0312eb --- /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 000000000..9c95f9c66 --- /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 diff --git a/components/esp_modem/src/esp_modem_dte.cpp b/components/esp_modem/src/esp_modem_dte.cpp index 3a9630c9c..2c5385180 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 000000000..1aacbc890 --- /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 000000000..1a308290e --- /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 000000000..e6a180d70 --- /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 000000000..9d9de29dd --- /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"); +} 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 000000000..5ab863acb --- /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