fix(modem): Run test in CI, add batch mode

This commit is contained in:
David Cermak
2026-04-01 16:48:38 +02:00
parent cd3eb4a264
commit 890088762f
10 changed files with 84 additions and 253 deletions
@@ -110,3 +110,29 @@ jobs:
./scripts/generate.sh
git diff --name-only
git diff --quiet
build_host_test_app:
if: contains(github.event.pull_request.labels.*.name, 'modem') || github.event_name == 'push'
name: Host test app
strategy:
matrix:
idf_ver: ["release-v5.5", "release-v6.0", "latest"]
runs-on: ubuntu-latest
container: espressif/idf:${{ matrix.idf_ver }}
steps:
- name: Checkout esp-protocols
uses: actions/checkout@v4
with:
path: protocols
- name: Build host test app with IDF-${{ matrix.idf_ver }}
shell: bash
run: |
cd $GITHUB_WORKSPACE/protocols/components/esp_modem/test/host_test_app
. ${IDF_PATH}/export.sh
idf.py --preview set-target linux
idf.py build
echo "Running test with batch size 0 and delay 1ms"
MODEM_SIM_PORT=10000 MODEM_SIM_BATCH_SIZE=0 MODEM_SIM_BATCH_DELAY_MS=1 TEST_TIMEOUT=30 ./run_test.sh
echo "Running test with batch size 1 and delay 5ms"
MODEM_SIM_PORT=10000 MODEM_SIM_BATCH_SIZE=1 MODEM_SIM_BATCH_DELAY_MS=5 TEST_TIMEOUT=30 ./run_test.sh
@@ -1,6 +1,14 @@
set(LWIP_DIR "$ENV{LWIP_PATH}")
set(LWIP_CONTRIB_DIR "$ENV{LWIP_CONTRIB_PATH}")
if(NOT LWIP_DIR OR NOT EXISTS "${LWIP_DIR}/src/Filelists.cmake")
# Stub-only build (no lwIP) -- enough for AT command testing without networking
idf_component_register(SRCS esp_netif_stub.c
INCLUDE_DIRS include
REQUIRES esp_system_protocols_linux)
return()
endif()
set(lwipcontribportunix_SRCS ${LWIP_CONTRIB_DIR}/ports/unix/port/sys_arch.c)
if(NOT CMAKE_BUILD_EARLY_EXPANSION)
@@ -5,7 +5,7 @@ End-to-end host tests for esp_modem using two Linux processes connected over a T
- **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.
When `LWIP_PATH` is not set, `esp_netif_linux` builds automatically as a stub (no lwIP download needed).
## Build & Run
@@ -1,3 +0,0 @@
idf_component_register(SRCS esp_netif_stub.c
INCLUDE_DIRS include
REQUIRES esp_system_protocols_linux)
@@ -1,69 +0,0 @@
/*
* 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
@@ -1,157 +0,0 @@
/*
* 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
@@ -1,10 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#define NETIF_PP_PHASE_OFFSET 0x100
@@ -17,6 +17,8 @@
#include <errno.h>
static volatile bool running = true;
static int batch_size = 0; // 0 = send whole response at once
static int batch_delay_ms = 1; // delay between batches (ms)
static void signal_handler(int sig)
{
@@ -67,28 +69,41 @@ static std::string process_at_command(const std::string &command)
return "ERROR\r\n";
}
static void print_escaped(const char *prefix, const std::string &s)
{
printf("%s[%zu]: ", prefix, s.size());
for (auto c : s) {
printf(c >= 0x20 ? "%c" : "\\x%02x", (unsigned char)c);
}
printf("\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);
print_escaped("modem_sim: rx ", cmd);
if (batch_size > 0) {
printf("modem_sim: tx [%zu] in batches of %d, delay %dms\n",
response.size(), batch_size, batch_delay_ms);
} else {
print_escaped("modem_sim: tx ", response);
}
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);
size_t chunk = response.size() - total;
if (batch_size > 0 && chunk > (size_t)batch_size) {
chunk = batch_size;
}
ssize_t sent = write(fd, response.c_str() + total, chunk);
if (sent < 0) {
perror("modem_sim: write error");
return;
}
total += sent;
if (batch_size > 0 && total < response.size() && batch_delay_ms > 0) {
usleep(batch_delay_ms * 1000);
}
}
}
@@ -153,12 +168,30 @@ static void handle_client(int client_fd)
}
}
static void usage(const char *prog)
{
printf("Usage: %s [port] [batch_size] [batch_delay_ms]\n"
" port TCP listen port (default: 10000)\n"
" batch_size reply chunk size in bytes, 0=whole (default: 0)\n"
" batch_delay_ms delay between chunks in ms (default: 1)\n", prog);
}
int main(int argc, char *argv[])
{
int port = 10000;
if (argc > 1) {
if (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) {
usage(argv[0]);
return 0;
}
port = atoi(argv[1]);
}
if (argc > 2) {
batch_size = atoi(argv[2]);
}
if (argc > 3) {
batch_delay_ms = atoi(argv[3]);
}
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
@@ -190,7 +223,8 @@ int main(int argc, char *argv[])
return 1;
}
printf("modem_sim: listening on 127.0.0.1:%d\n", port);
printf("modem_sim: listening on 127.0.0.1:%d (batch_size=%d, batch_delay=%dms)\n",
port, batch_size, batch_delay_ms);
fflush(stdout);
while (running) {
@@ -7,6 +7,8 @@
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PORT="${MODEM_SIM_PORT:-10000}"
BATCH_SIZE="${MODEM_SIM_BATCH_SIZE:-0}" # 0 = send whole response at once
BATCH_DELAY="${MODEM_SIM_BATCH_DELAY_MS:-1}" # delay between chunks (ms)
SIM_BINARY="${SCRIPT_DIR}/modem_sim/build/modem_sim"
TEST_BINARY="${SCRIPT_DIR}/build/host_test_app.elf"
TEST_TIMEOUT="${TEST_TIMEOUT:-30}"
@@ -35,8 +37,8 @@ if [ ! -f "$TEST_BINARY" ]; then
fi
# --- Start modem simulator ---
echo "Starting modem_sim on port $PORT..."
"$SIM_BINARY" "$PORT" &
echo "Starting modem_sim on port $PORT (batch_size=$BATCH_SIZE, batch_delay=${BATCH_DELAY}ms)..."
"$SIM_BINARY" "$PORT" "$BATCH_SIZE" "$BATCH_DELAY" &
SIM_PID=$!
sleep 0.5