feat(modem): Add end2end host tests with host modem_sim

This commit is contained in:
David Cermak
2026-04-01 16:29:42 +02:00
parent dbccace49c
commit cd3eb4a264
15 changed files with 800 additions and 0 deletions
@@ -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")
@@ -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.
@@ -0,0 +1,3 @@
idf_component_register(SRCS esp_netif_stub.c
INCLUDE_DIRS include
REQUIRES esp_system_protocols_linux)
@@ -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 <stdlib.h>
#include <string.h>
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;
}
@@ -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 <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#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
@@ -0,0 +1,157 @@
/*
* SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <endian.h>
#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
@@ -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
@@ -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")
@@ -0,0 +1,5 @@
dependencies:
espressif/catch2:
version: '*'
idf:
version: ">=5.0"
@@ -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 <memory>
#include <cstdlib>
#include <unistd.h>
#include <catch2/catch_test_macros.hpp>
#include <catch2/catch_session.hpp>
#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<DTE> 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);
}
@@ -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)
@@ -0,0 +1,215 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
#include <poll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <errno.h>
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;
}
+65
View File
@@ -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
@@ -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
@@ -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