From cd3eb4a26401f43e7727c047f0467f3aa4194885 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Wed, 1 Apr 2026 16:29:42 +0200 Subject: [PATCH] feat(modem): Add end2end host tests with host modem_sim --- .../test/host_test_app/CMakeLists.txt | 17 ++ .../esp_modem/test/host_test_app/README.md | 25 ++ .../components/esp_netif_linux/CMakeLists.txt | 3 + .../esp_netif_linux/esp_netif_stub.c | 30 +++ .../esp_netif_linux/include/esp_netif.h | 69 ++++++ .../include/esp_netif_ip_addr.h | 157 +++++++++++++ .../esp_netif_linux/include/esp_netif_ppp.h | 10 + .../test/host_test_app/main/CMakeLists.txt | 15 ++ .../test/host_test_app/main/idf_component.yml | 5 + .../test/host_test_app/main/test_app.cpp | 171 ++++++++++++++ .../host_test_app/modem_sim/CMakeLists.txt | 8 + .../host_test_app/modem_sim/modem_sim.cpp | 215 ++++++++++++++++++ .../esp_modem/test/host_test_app/run_test.sh | 65 ++++++ .../test/host_test_app/sdkconfig.ci.linux | 5 + .../test/host_test_app/sdkconfig.defaults | 5 + 15 files changed, 800 insertions(+) create mode 100644 components/esp_modem/test/host_test_app/CMakeLists.txt create mode 100644 components/esp_modem/test/host_test_app/README.md create mode 100644 components/esp_modem/test/host_test_app/components/esp_netif_linux/CMakeLists.txt create mode 100644 components/esp_modem/test/host_test_app/components/esp_netif_linux/esp_netif_stub.c create mode 100644 components/esp_modem/test/host_test_app/components/esp_netif_linux/include/esp_netif.h create mode 100644 components/esp_modem/test/host_test_app/components/esp_netif_linux/include/esp_netif_ip_addr.h create mode 100644 components/esp_modem/test/host_test_app/components/esp_netif_linux/include/esp_netif_ppp.h create mode 100644 components/esp_modem/test/host_test_app/main/CMakeLists.txt create mode 100644 components/esp_modem/test/host_test_app/main/idf_component.yml create mode 100644 components/esp_modem/test/host_test_app/main/test_app.cpp create mode 100644 components/esp_modem/test/host_test_app/modem_sim/CMakeLists.txt create mode 100644 components/esp_modem/test/host_test_app/modem_sim/modem_sim.cpp create mode 100755 components/esp_modem/test/host_test_app/run_test.sh create mode 100644 components/esp_modem/test/host_test_app/sdkconfig.ci.linux create mode 100644 components/esp_modem/test/host_test_app/sdkconfig.defaults diff --git a/components/esp_modem/test/host_test_app/CMakeLists.txt b/components/esp_modem/test/host_test_app/CMakeLists.txt new file mode 100644 index 000000000..678ea8bf6 --- /dev/null +++ b/components/esp_modem/test/host_test_app/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.5) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +set(EXTRA_COMPONENT_DIRS + ../.. # esp_modem component + ../../port/linux # esp_event_mock, esp_system_protocols_linux + ../../../../common_components/linux_compat/esp_timer # esp_timer for linux + ../../../../common_components/linux_compat/freertos # freertos shim for linux +) + +set(COMPONENTS main) +project(host_test_app) + +idf_component_get_property(esp_modem esp_modem COMPONENT_LIB) +target_compile_definitions(${esp_modem} PRIVATE "-DCONFIG_COMPILER_CXX_EXCEPTIONS") +target_compile_definitions(${esp_modem} PRIVATE "-DCONFIG_IDF_TARGET_LINUX") diff --git a/components/esp_modem/test/host_test_app/README.md b/components/esp_modem/test/host_test_app/README.md new file mode 100644 index 000000000..2d28f959a --- /dev/null +++ b/components/esp_modem/test/host_test_app/README.md @@ -0,0 +1,25 @@ +# Host Test App + +End-to-end host tests for esp_modem using two Linux processes connected over a TCP socket: + +- **test_app** – IDF Linux application (Catch2) that exercises esp_modem AT commands via a VFS socket DTE. +- **modem_sim** – Standalone TCP server that emulates a SIM7600-like modem, responding to AT commands. + +A lightweight `esp_netif_linux` stub replaces the real component so lwIP is not required. + +## Build & Run + +```bash +# 1. Build the modem simulator (plain CMake, no IDF) +cmake -S modem_sim -B modem_sim/build && cmake --build modem_sim/build + +# 2. Build the test app (IDF, Linux target) +source $IDF_PATH/export.sh +idf.py --preview set-target linux +idf.py build + +# 3. Run both (simulator + tests) +./run_test.sh +``` + +`MODEM_SIM_PORT` (default `10000`) and `TEST_TIMEOUT` (default `30`s) can be overridden via environment variables. diff --git a/components/esp_modem/test/host_test_app/components/esp_netif_linux/CMakeLists.txt b/components/esp_modem/test/host_test_app/components/esp_netif_linux/CMakeLists.txt new file mode 100644 index 000000000..7b94862cb --- /dev/null +++ b/components/esp_modem/test/host_test_app/components/esp_netif_linux/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS esp_netif_stub.c + INCLUDE_DIRS include + REQUIRES esp_system_protocols_linux) diff --git a/components/esp_modem/test/host_test_app/components/esp_netif_linux/esp_netif_stub.c b/components/esp_modem/test/host_test_app/components/esp_netif_linux/esp_netif_stub.c new file mode 100644 index 000000000..b682f3d4f --- /dev/null +++ b/components/esp_modem/test/host_test_app/components/esp_netif_linux/esp_netif_stub.c @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include "esp_netif.h" +#include +#include + +esp_netif_t *esp_netif_new(const esp_netif_config_t *config) +{ + esp_netif_t *netif = (esp_netif_t *)calloc(1, sizeof(esp_netif_t)); + return netif; +} + +void esp_netif_destroy(esp_netif_t *esp_netif) +{ + free(esp_netif); +} + +int esp_netif_receive(esp_netif_t *netif, uint8_t *data, size_t len) +{ + return 0; +} + +esp_ip6_addr_type_t esp_netif_ip6_get_addr_type(esp_ip6_addr_t *ip6_addr) +{ + return ESP_IP6_ADDR_IS_UNKNOWN; +} diff --git a/components/esp_modem/test/host_test_app/components/esp_netif_linux/include/esp_netif.h b/components/esp_modem/test/host_test_app/components/esp_netif_linux/include/esp_netif.h new file mode 100644 index 000000000..25163a9f6 --- /dev/null +++ b/components/esp_modem/test/host_test_app/components/esp_netif_linux/include/esp_netif.h @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include "esp_netif_ip_addr.h" +#include "esp_err.h" +#include "esp_event_base.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct esp_netif_obj esp_netif_t; + +typedef struct esp_netif_driver_base_s { + esp_err_t (*post_attach)(esp_netif_t *netif, void *h); + esp_netif_t *netif; +} esp_netif_driver_base_t; + +struct esp_netif_driver_ifconfig { + void *handle; + esp_err_t (*transmit)(void *h, void *buffer, size_t len); + esp_err_t (*transmit_wrap)(void *h, void *buffer, size_t len, void *netstack_buffer); + void (*driver_free_rx_buffer)(void *h, void *buffer); +}; + +struct esp_netif_config { + const char *dev_name; /**< Name of the file device */ + const char *if_name; /**< Network interface name */ +}; + +struct esp_netif_obj { + uint8_t *in_buf; + uint8_t *out_buf; + int fd; + esp_err_t (*transmit)(void *h, void *buffer, size_t len); + void *ctx; +}; + +int esp_netif_receive(esp_netif_t *netif, uint8_t *data, size_t len); + +typedef struct esp_netif_config esp_netif_config_t; + +esp_netif_t *esp_netif_new(const esp_netif_config_t *esp_netif_config); + +void esp_netif_destroy(esp_netif_t *esp_netif); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_modem/test/host_test_app/components/esp_netif_linux/include/esp_netif_ip_addr.h b/components/esp_modem/test/host_test_app/components/esp_netif_linux/include/esp_netif_ip_addr.h new file mode 100644 index 000000000..e5a6f1fe7 --- /dev/null +++ b/components/esp_modem/test/host_test_app/components/esp_netif_linux/include/esp_netif_ip_addr.h @@ -0,0 +1,157 @@ +/* + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if BYTE_ORDER == BIG_ENDIAN +#define esp_netif_htonl(x) ((uint32_t)(x)) +#else +#define esp_netif_htonl(x) ((((x) & (uint32_t)0x000000ffUL) << 24) | \ + (((x) & (uint32_t)0x0000ff00UL) << 8) | \ + (((x) & (uint32_t)0x00ff0000UL) >> 8) | \ + (((x) & (uint32_t)0xff000000UL) >> 24)) +#endif + +#define esp_netif_ip4_makeu32(a,b,c,d) (((uint32_t)((a) & 0xff) << 24) | \ + ((uint32_t)((b) & 0xff) << 16) | \ + ((uint32_t)((c) & 0xff) << 8) | \ + (uint32_t)((d) & 0xff)) + +// Access address in 16-bit block +#define ESP_IP6_ADDR_BLOCK1(ip6addr) ((uint16_t)((esp_netif_htonl((ip6addr)->addr[0]) >> 16) & 0xffff)) +#define ESP_IP6_ADDR_BLOCK2(ip6addr) ((uint16_t)((esp_netif_htonl((ip6addr)->addr[0])) & 0xffff)) +#define ESP_IP6_ADDR_BLOCK3(ip6addr) ((uint16_t)((esp_netif_htonl((ip6addr)->addr[1]) >> 16) & 0xffff)) +#define ESP_IP6_ADDR_BLOCK4(ip6addr) ((uint16_t)((esp_netif_htonl((ip6addr)->addr[1])) & 0xffff)) +#define ESP_IP6_ADDR_BLOCK5(ip6addr) ((uint16_t)((esp_netif_htonl((ip6addr)->addr[2]) >> 16) & 0xffff)) +#define ESP_IP6_ADDR_BLOCK6(ip6addr) ((uint16_t)((esp_netif_htonl((ip6addr)->addr[2])) & 0xffff)) +#define ESP_IP6_ADDR_BLOCK7(ip6addr) ((uint16_t)((esp_netif_htonl((ip6addr)->addr[3]) >> 16) & 0xffff)) +#define ESP_IP6_ADDR_BLOCK8(ip6addr) ((uint16_t)((esp_netif_htonl((ip6addr)->addr[3])) & 0xffff)) + +#define IPSTR "%d.%d.%d.%d" +#define esp_ip4_addr_get_byte(ipaddr, idx) (((const uint8_t*)(&(ipaddr)->addr))[idx]) +#define esp_ip4_addr1(ipaddr) esp_ip4_addr_get_byte(ipaddr, 0) +#define esp_ip4_addr2(ipaddr) esp_ip4_addr_get_byte(ipaddr, 1) +#define esp_ip4_addr3(ipaddr) esp_ip4_addr_get_byte(ipaddr, 2) +#define esp_ip4_addr4(ipaddr) esp_ip4_addr_get_byte(ipaddr, 3) + + +#define esp_ip4_addr1_16(ipaddr) ((uint16_t)esp_ip4_addr1(ipaddr)) +#define esp_ip4_addr2_16(ipaddr) ((uint16_t)esp_ip4_addr2(ipaddr)) +#define esp_ip4_addr3_16(ipaddr) ((uint16_t)esp_ip4_addr3(ipaddr)) +#define esp_ip4_addr4_16(ipaddr) ((uint16_t)esp_ip4_addr4(ipaddr)) + +#define IP2STR(ipaddr) esp_ip4_addr1_16(ipaddr), \ + esp_ip4_addr2_16(ipaddr), \ + esp_ip4_addr3_16(ipaddr), \ + esp_ip4_addr4_16(ipaddr) + +#define IPV6STR "%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x" + +#define IPV62STR(ipaddr) ESP_IP6_ADDR_BLOCK1(&(ipaddr)), \ + ESP_IP6_ADDR_BLOCK2(&(ipaddr)), \ + ESP_IP6_ADDR_BLOCK3(&(ipaddr)), \ + ESP_IP6_ADDR_BLOCK4(&(ipaddr)), \ + ESP_IP6_ADDR_BLOCK5(&(ipaddr)), \ + ESP_IP6_ADDR_BLOCK6(&(ipaddr)), \ + ESP_IP6_ADDR_BLOCK7(&(ipaddr)), \ + ESP_IP6_ADDR_BLOCK8(&(ipaddr)) + +#define ESP_IPADDR_TYPE_V4 0U +#define ESP_IPADDR_TYPE_V6 6U +#define ESP_IPADDR_TYPE_ANY 46U + +#define ESP_IP4TOUINT32(a,b,c,d) (((uint32_t)((a) & 0xffU) << 24) | \ + ((uint32_t)((b) & 0xffU) << 16) | \ + ((uint32_t)((c) & 0xffU) << 8) | \ + (uint32_t)((d) & 0xffU)) + +#define ESP_IP4TOADDR(a,b,c,d) esp_netif_htonl(ESP_IP4TOUINT32(a, b, c, d)) + +// new definitions +#define ESP_IPADDR4_INIT(ipaddr, a,b,c,d) (ipaddr)->addr = ESP_IP4TOADDR(a,b,c,d) +#define ESP_IP6TOADDR(a, b, c, d) { { { { a, b, c, d } , 0 } }, ESP_IPADDR_TYPE_V6 } + +// TODO: use esp-netif instead of lwip API +#define ip_2_ip6(ipaddr) (&((ipaddr)->u_addr.ip6)) +#define ip_2_ip4(ipaddr) (&((ipaddr)->u_addr.ip4)) +#define IP_SET_TYPE_VAL(ipaddr, iptype) do { (ipaddr).type = (iptype); }while(0) +#define IP_GET_TYPE(ipaddr) ((ipaddr)->type) + +#define IP6_NO_ZONE 0 +#define ip6_addr_clear_zone(ip6addr) ((ip6addr)->zone = IP6_NO_ZONE) + +#define inet6_addr_from_ip6addr(target_in6addr, source_ip6addr) {(target_in6addr)->s6_addr32[0] = (source_ip6addr)->addr[0]; \ + (target_in6addr)->s6_addr32[1] = (source_ip6addr)->addr[1]; \ + (target_in6addr)->s6_addr32[2] = (source_ip6addr)->addr[2]; \ + (target_in6addr)->s6_addr32[3] = (source_ip6addr)->addr[3];} + +#define inet6_addr_to_ip6addr(target_ip6addr, source_in6addr) {(target_ip6addr)->addr[0] = (source_in6addr)->s6_addr32[0]; \ + (target_ip6addr)->addr[1] = (source_in6addr)->s6_addr32[1]; \ + (target_ip6addr)->addr[2] = (source_in6addr)->s6_addr32[2]; \ + (target_ip6addr)->addr[3] = (source_in6addr)->s6_addr32[3]; \ + ip6_addr_clear_zone(target_ip6addr);} + + +struct esp_ip6_addr { + uint32_t addr[4]; + uint8_t zone; +}; + + + +struct esp_ip4_addr { + uint32_t addr; +}; + +typedef struct esp_ip4_addr esp_ip4_addr_t; + +typedef struct esp_ip6_addr esp_ip6_addr_t; + +typedef struct _ip_addr { + union { + esp_ip6_addr_t ip6; + esp_ip4_addr_t ip4; + } u_addr; + uint8_t type; +} esp_ip_addr_t; + +typedef enum { + ESP_IP6_ADDR_IS_UNKNOWN, + ESP_IP6_ADDR_IS_GLOBAL, + ESP_IP6_ADDR_IS_LINK_LOCAL, + ESP_IP6_ADDR_IS_SITE_LOCAL, + ESP_IP6_ADDR_IS_UNIQUE_LOCAL, + ESP_IP6_ADDR_IS_IPV4_MAPPED_IPV6 +} esp_ip6_addr_type_t; + +typedef struct { + esp_ip4_addr_t ip; /**< Interface IPV4 address */ + esp_ip4_addr_t netmask; /**< Interface IPV4 netmask */ + esp_ip4_addr_t gw; /**< Interface IPV4 gateway address */ +} esp_netif_ip_info_t; + +typedef struct { + esp_ip6_addr_t ip; /**< Interface IPV6 address */ +} esp_netif_ip6_info_t; + +/** + * @brief Get the IPv6 address type + * + * @param[in] ip6_addr IPv6 type + * + * @return IPv6 type in form of enum esp_ip6_addr_type_t + */ +esp_ip6_addr_type_t esp_netif_ip6_get_addr_type(esp_ip6_addr_t *ip6_addr); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_modem/test/host_test_app/components/esp_netif_linux/include/esp_netif_ppp.h b/components/esp_modem/test/host_test_app/components/esp_netif_linux/include/esp_netif_ppp.h new file mode 100644 index 000000000..9ca947c23 --- /dev/null +++ b/components/esp_modem/test/host_test_app/components/esp_netif_linux/include/esp_netif_ppp.h @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +#pragma once + +#define NETIF_PP_PHASE_OFFSET 0x100 diff --git a/components/esp_modem/test/host_test_app/main/CMakeLists.txt b/components/esp_modem/test/host_test_app/main/CMakeLists.txt new file mode 100644 index 000000000..3f9f8309f --- /dev/null +++ b/components/esp_modem/test/host_test_app/main/CMakeLists.txt @@ -0,0 +1,15 @@ +idf_component_register(SRCS "test_app.cpp" + REQUIRES esp_modem WHOLE_ARCHIVE) + +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) +target_link_libraries(${COMPONENT_LIB} PRIVATE Threads::Threads) + +target_link_libraries(${COMPONENT_LIB} PRIVATE Catch2WithMain) + +set_target_properties(${COMPONENT_LIB} PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS ON +) +target_compile_definitions(${COMPONENT_LIB} PRIVATE "-DCONFIG_IDF_TARGET_LINUX") diff --git a/components/esp_modem/test/host_test_app/main/idf_component.yml b/components/esp_modem/test/host_test_app/main/idf_component.yml new file mode 100644 index 000000000..218c0392d --- /dev/null +++ b/components/esp_modem/test/host_test_app/main/idf_component.yml @@ -0,0 +1,5 @@ +dependencies: + espressif/catch2: + version: '*' + idf: + version: ">=5.0" diff --git a/components/esp_modem/test/host_test_app/main/test_app.cpp b/components/esp_modem/test/host_test_app/main/test_app.cpp new file mode 100644 index 000000000..89dae8015 --- /dev/null +++ b/components/esp_modem/test/host_test_app/main/test_app.cpp @@ -0,0 +1,171 @@ +/* + * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#define CATCH_CONFIG_MAIN +#include +#include +#include +#include +#include +#include "cxx_include/esp_modem_api.hpp" +#include "cxx_include/esp_modem_dte.hpp" +#include "esp_modem_config.h" +#include "esp_log.h" +#include "vfs_resource/vfs_create.hpp" + +using namespace esp_modem; + +[[maybe_unused]] constexpr auto TAG = "host_test_app"; + +static int get_port() +{ + const char *env = std::getenv("MODEM_SIM_PORT"); + return env ? std::atoi(env) : 10000; +} + +static std::shared_ptr create_test_dte() +{ + esp_modem_dte_config_t dte_config = { + .dte_buffer_size = 512, + .task_stack_size = 4096, + .task_priority = 5, + .vfs_config = {} + }; + + struct esp_modem_vfs_socket_creator socket_config = { + .host_name = "127.0.0.1", + .port = get_port() + }; + + bool ok = vfs_create_socket(&socket_config, &dte_config.vfs_config); + if (!ok) { + return nullptr; + } + return create_vfs_dte(&dte_config); +} + +TEST_CASE("Basic AT command via socket", "[esp_modem][at]") +{ + auto dte = create_test_dte(); + REQUIRE(dte != nullptr); + + esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG("internet"); + esp_netif_t netif{}; + auto dce = create_SIM7600_dce(&dce_config, dte, &netif); + REQUIRE(dce != nullptr); + + CHECK(dce->set_command_mode() == command_result::OK); +} + +TEST_CASE("Get signal quality", "[esp_modem][at]") +{ + auto dte = create_test_dte(); + REQUIRE(dte != nullptr); + + esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG("internet"); + esp_netif_t netif{}; + auto dce = create_SIM7600_dce(&dce_config, dte, &netif); + REQUIRE(dce != nullptr); + + int rssi, ber; + CHECK(dce->get_signal_quality(rssi, ber) == command_result::OK); + CHECK(rssi == 27); + CHECK(ber == 99); +} + +TEST_CASE("Get module name", "[esp_modem][at]") +{ + auto dte = create_test_dte(); + REQUIRE(dte != nullptr); + + esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG("internet"); + esp_netif_t netif{}; + auto dce = create_SIM7600_dce(&dce_config, dte, &netif); + REQUIRE(dce != nullptr); + + std::string model; + CHECK(dce->get_module_name(model) == command_result::OK); + CHECK(model == "SimModem-7600"); +} + +TEST_CASE("Get operator name", "[esp_modem][at]") +{ + auto dte = create_test_dte(); + REQUIRE(dte != nullptr); + + esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG("internet"); + esp_netif_t netif{}; + auto dce = create_SIM7600_dce(&dce_config, dte, &netif); + REQUIRE(dce != nullptr); + + std::string operator_name; + int act = -1; + CHECK(dce->get_operator_name(operator_name, act) == command_result::OK); + CHECK(operator_name == "TestOperator"); + CHECK(act == 7); +} + +TEST_CASE("Get IMSI", "[esp_modem][at]") +{ + auto dte = create_test_dte(); + REQUIRE(dte != nullptr); + + esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG("internet"); + esp_netif_t netif{}; + auto dce = create_SIM7600_dce(&dce_config, dte, &netif); + REQUIRE(dce != nullptr); + + std::string imsi; + CHECK(dce->get_imsi(imsi) == command_result::OK); + CHECK(imsi == "987654321098765"); +} + +TEST_CASE("Get IMEI", "[esp_modem][at]") +{ + auto dte = create_test_dte(); + REQUIRE(dte != nullptr); + + esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG("internet"); + esp_netif_t netif{}; + auto dce = create_SIM7600_dce(&dce_config, dte, &netif); + REQUIRE(dce != nullptr); + + std::string imei; + CHECK(dce->get_imei(imei) == command_result::OK); + CHECK(imei == "123456789012345"); +} + +TEST_CASE("Read PIN status", "[esp_modem][at]") +{ + auto dte = create_test_dte(); + REQUIRE(dte != nullptr); + + esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG("internet"); + esp_netif_t netif{}; + auto dce = create_SIM7600_dce(&dce_config, dte, &netif); + REQUIRE(dce != nullptr); + + bool pin_ok; + CHECK(dce->read_pin(pin_ok) == command_result::OK); + CHECK(pin_ok == true); +} + +#define CATCH_CONFIG_RUNNER +extern "C" int app_main(void) +{ + int argc = 5; + const char *argv[] = {"host_test_app", "-r", "junit", "-o", "junit.xml", nullptr}; + + int result = Catch::Session().run(argc, argv); + + if (result != 0) { + printf("Test failed with result %d.\n", result); + } else { + printf("All tests passed successfully.\n"); + } + + std::exit(result); +} diff --git a/components/esp_modem/test/host_test_app/modem_sim/CMakeLists.txt b/components/esp_modem/test/host_test_app/modem_sim/CMakeLists.txt new file mode 100644 index 000000000..17ecc86d0 --- /dev/null +++ b/components/esp_modem/test/host_test_app/modem_sim/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.10) +project(modem_sim CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(modem_sim modem_sim.cpp) +target_link_libraries(modem_sim PRIVATE pthread) diff --git a/components/esp_modem/test/host_test_app/modem_sim/modem_sim.cpp b/components/esp_modem/test/host_test_app/modem_sim/modem_sim.cpp new file mode 100644 index 000000000..bb614f65c --- /dev/null +++ b/components/esp_modem/test/host_test_app/modem_sim/modem_sim.cpp @@ -0,0 +1,215 @@ +/* + * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static volatile bool running = true; + +static void signal_handler(int sig) +{ + running = false; +} + +static std::string process_at_command(const std::string &command) +{ + if (command == "AT\r" || command == "AT") { + return "OK\r\n"; + } + if (command == "ATE0\r" || command == "ATE1\r") { + return "OK\r\n"; + } + if (command.find("AT+CSQ\r") != std::string::npos) { + return "+CSQ: 27,99\r\n\r\nOK\r\n"; + } + if (command.find("AT+CGMM\r") != std::string::npos) { + return "SimModem-7600\r\n\r\nOK\r\n"; + } + if (command.find("AT+CGSN\r") != std::string::npos) { + return "123456789012345\r\n\r\nOK\r\n"; + } + if (command.find("AT+CIMI\r") != std::string::npos) { + return "987654321098765\r\n\r\nOK\r\n"; + } + if (command.find("AT+COPS?\r") != std::string::npos) { + return "+COPS: 0,0,\"TestOperator\",7\r\n\r\nOK\r\n"; + } + if (command.find("AT+COPS=3,0\r") != std::string::npos) { + return "OK\r\n"; + } + if (command.find("AT+CBC\r") != std::string::npos) { + return "+CBC: 3800mV\r\n\r\nOK\r\n"; + } + if (command.find("AT+CPIN=") != std::string::npos) { + return "OK\r\n"; + } + if (command.find("AT+CPIN?\r") != std::string::npos) { + return "+CPIN: READY\r\n\r\nOK\r\n"; + } + if (command.find("AT+CFUN") != std::string::npos) { + return "OK\r\n"; + } + if (command.find("AT") != std::string::npos) { + return "OK\r\n"; + } + return "ERROR\r\n"; +} + +static void send_response(int fd, const std::string &cmd, const std::string &response) +{ + printf("modem_sim: rx [%zu]: ", cmd.size()); + for (auto c : cmd) { + printf(c >= 0x20 ? "%c" : "\\x%02x", (unsigned char)c); + } + printf("\n"); + printf("modem_sim: tx [%zu]: ", response.size()); + for (auto c : response) { + printf(c >= 0x20 ? "%c" : "\\x%02x", (unsigned char)c); + } + printf("\n"); + fflush(stdout); + + size_t total = 0; + while (total < response.size()) { + ssize_t sent = write(fd, response.c_str() + total, response.size() - total); + if (sent < 0) { + perror("modem_sim: write error"); + return; + } + total += sent; + } +} + +static void handle_client(int client_fd) +{ + char buf[1024]; + std::string pending; + + printf("modem_sim: client connected\n"); + fflush(stdout); + + struct pollfd pfd = { .fd = client_fd, .events = POLLIN, .revents = 0 }; + + while (running) { + int ret = poll(&pfd, 1, 500); + if (ret < 0) { + if (errno == EINTR) { + continue; + } + perror("modem_sim: poll"); + break; + } + if (ret == 0) { + // Check for "+++" pattern (no \r terminator) + if (pending.find("+++") != std::string::npos) { + std::string response = "NO CARRIER\r\n"; + send_response(client_fd, pending, response); + pending.clear(); + } + continue; + } + + ssize_t n = read(client_fd, buf, sizeof(buf)); + if (n <= 0) { + if (n == 0) { + printf("modem_sim: client disconnected\n"); + } else if (errno != EAGAIN && errno != EWOULDBLOCK) { + perror("modem_sim: read error"); + } + break; + } + + pending.append(buf, n); + + // Handle "+++" escape sequence (no \r) + size_t ppp_pos = pending.find("+++"); + if (ppp_pos != std::string::npos && pending.find('\r') == std::string::npos) { + std::string response = "NO CARRIER\r\n"; + send_response(client_fd, "+++", response); + pending.erase(0, ppp_pos + 3); + continue; + } + + size_t pos; + while ((pos = pending.find('\r')) != std::string::npos) { + std::string cmd = pending.substr(0, pos + 1); + pending.erase(0, pos + 1); + + std::string response = process_at_command(cmd); + send_response(client_fd, cmd, response); + } + } +} + +int main(int argc, char *argv[]) +{ + int port = 10000; + if (argc > 1) { + port = atoi(argv[1]); + } + + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + signal(SIGPIPE, SIG_IGN); + + int server_fd = socket(AF_INET, SOCK_STREAM, 0); + if (server_fd < 0) { + perror("modem_sim: socket"); + return 1; + } + + int opt = 1; + setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + struct sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(port); + + if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + perror("modem_sim: bind"); + close(server_fd); + return 1; + } + + if (listen(server_fd, 1) < 0) { + perror("modem_sim: listen"); + close(server_fd); + return 1; + } + + printf("modem_sim: listening on 127.0.0.1:%d\n", port); + fflush(stdout); + + while (running) { + struct sockaddr_in client_addr = {}; + socklen_t client_len = sizeof(client_addr); + int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); + if (client_fd < 0) { + if (errno == EINTR) { + continue; + } + perror("modem_sim: accept"); + break; + } + + handle_client(client_fd); + close(client_fd); + } + + close(server_fd); + printf("modem_sim: shutdown\n"); + return 0; +} diff --git a/components/esp_modem/test/host_test_app/run_test.sh b/components/esp_modem/test/host_test_app/run_test.sh new file mode 100755 index 000000000..a72bfc4b0 --- /dev/null +++ b/components/esp_modem/test/host_test_app/run_test.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# +# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +# +# Test harness: starts modem simulator, runs test app, collects results. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PORT="${MODEM_SIM_PORT:-10000}" +SIM_BINARY="${SCRIPT_DIR}/modem_sim/build/modem_sim" +TEST_BINARY="${SCRIPT_DIR}/build/host_test_app.elf" +TEST_TIMEOUT="${TEST_TIMEOUT:-30}" + +cleanup() { + if [ -n "$SIM_PID" ]; then + kill "$SIM_PID" 2>/dev/null + kill -9 "$SIM_PID" 2>/dev/null + wait "$SIM_PID" 2>/dev/null + fi +} +trap cleanup EXIT + +# --- Build modem_sim if needed --- +if [ ! -f "$SIM_BINARY" ]; then + echo "Building modem_sim..." + cmake -S "${SCRIPT_DIR}/modem_sim" -B "${SCRIPT_DIR}/modem_sim/build" + cmake --build "${SCRIPT_DIR}/modem_sim/build" +fi + +# --- Check test binary --- +if [ ! -f "$TEST_BINARY" ]; then + echo "Error: test binary not found at $TEST_BINARY" + echo "Build with: idf.py --preview set-target linux && idf.py build" + exit 1 +fi + +# --- Start modem simulator --- +echo "Starting modem_sim on port $PORT..." +"$SIM_BINARY" "$PORT" & +SIM_PID=$! + +sleep 0.5 +if ! kill -0 "$SIM_PID" 2>/dev/null; then + echo "Error: modem_sim failed to start" + exit 1 +fi + +# --- Run tests --- +echo "Running test app (timeout=${TEST_TIMEOUT}s)..." +export MODEM_SIM_PORT="$PORT" +timeout --signal=KILL "$TEST_TIMEOUT" "$TEST_BINARY" +TEST_RESULT=$? + +if [ $TEST_RESULT -eq 137 ]; then + echo "Error: test timed out after ${TEST_TIMEOUT}s" +fi + +echo "" +if [ $TEST_RESULT -eq 0 ]; then + echo "=== ALL TESTS PASSED ===" +else + echo "=== TESTS FAILED (exit code: $TEST_RESULT) ===" +fi + +exit $TEST_RESULT diff --git a/components/esp_modem/test/host_test_app/sdkconfig.ci.linux b/components/esp_modem/test/host_test_app/sdkconfig.ci.linux new file mode 100644 index 000000000..cd841b55e --- /dev/null +++ b/components/esp_modem/test/host_test_app/sdkconfig.ci.linux @@ -0,0 +1,5 @@ +CONFIG_IDF_TARGET="linux" +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_COMPILER_CXX_RTTI=y +CONFIG_COMPILER_CXX_EXCEPTIONS_EMG_POOL_SIZE=0 +CONFIG_COMPILER_STACK_CHECK_NONE=y diff --git a/components/esp_modem/test/host_test_app/sdkconfig.defaults b/components/esp_modem/test/host_test_app/sdkconfig.defaults new file mode 100644 index 000000000..cd841b55e --- /dev/null +++ b/components/esp_modem/test/host_test_app/sdkconfig.defaults @@ -0,0 +1,5 @@ +CONFIG_IDF_TARGET="linux" +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_COMPILER_CXX_RTTI=y +CONFIG_COMPILER_CXX_EXCEPTIONS_EMG_POOL_SIZE=0 +CONFIG_COMPILER_STACK_CHECK_NONE=y