Compare commits

..
3 Commits
Author SHA1 Message Date
cpq fc71bc0588 Add skeleton for the radio mesh simulator 2026-11-01 11:05:59 +00:00
Sergey Lyubka 08305b9000 Merge pull request #3751 from cesanta/dhcp
Fix: DHCP client stuck in REQUESTING state forever if the REQUEST or ACK is lost
2026-10-30 12:43:26 +00:00
cpq 69e8c0a92f Fix: DHCP client stuck in REQUESTING state forever if the REQUEST or ACK is lost 2026-10-30 09:42:12 +00:00
21 changed files with 879 additions and 245 deletions

No files matched your search

-1
View File
@@ -637,7 +637,6 @@ jobs:
example:
- path: micropython/esp32
- path: esp32/uart-bridge
- path: esp32/minimal
name: ${{ matrix.example.path }}
env:
GO: 0
-1
View File
@@ -186,7 +186,6 @@ jobs:
matrix:
example:
- path: esp32/uart-bridge
- path: esp32/minimal
name: ${{ matrix.example.path }}
env:
GO: 0
+41 -50
View File
@@ -3626,13 +3626,12 @@ struct mg_fs mg_fs_posix = {p_stat, p_list, p_open, p_close, p_read,
#if MG_ENABLE_HEALTH
// The one health record. Lives in RAM that survives a warm reset, see
// MG_HEALTH_RAM and the .mg_health region in the linker script
struct mg_health mg_health_record MG_HEALTH_RAM;
// STM32 H7, H5, C5 support. Needs the _estack symbol from the linker script
#if defined(STM32H7_DEV_ID) || defined(STM32H5) || defined(STM32C5)
// STM32 H723 support. Needs the _estack symbol from the linker script
#if MG_ENABLE_HEALTH && (defined(STM32H7_DEV_ID) || defined(STM32H5) || defined(STM32C5))
// Fault handler body. Runs in exception context: no printf, no malloc, no
// blocking calls. Copies the unwind registers and raw stack into the health
// record, then resets.
@@ -3674,57 +3673,52 @@ void BusFault_Handler(void) __attribute__((alias("HardFault_Handler")));
void UsageFault_Handler(void) __attribute__((alias("HardFault_Handler")));
#endif
// Start a new boot: validate the record. Called by MG_HEALTH_INIT()
// Start a new boot: validate the record, count the boot, store the reset
// reason. The current boot moves to prev_* slots first, so the previous
// boot's uptime and reason stay readable. Called by MG_HEALTH_INIT()
void mg_health_init(void) {
char magic[] = MG_HEALTH_MAGIC;
// struct mg_health *h = mg_health_get();
if (!mg_health_valid()) { // First boot, or RAM lost its contents
memset(&mg_health_record, 0, sizeof(mg_health_record));
memcpy(mg_health_record.magic, magic, sizeof(mg_health_record.magic));
}
#if MG_ARCH == MG_ARCH_ESP32
#if CONFIG_IDF_TARGET_ARCH_XTENSA
mg_health_record.arch = (uint16_t) MG_HEALTH_ARCH_XTENSA;
#elif CONFIG_IDF_TARGET_ARCH_RISCV
mg_health_record.arch = (uint16_t) MG_HEALTH_ARCH_RISCV;
#endif
esp_reset_reason_t rr = esp_reset_reason();
mg_health_record.reset_reason = esp2mg(rr);
#if CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH == 0
MG_ERROR(("MG_HEALTH is enabled but CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=n"));
#else
#if 0 && MG_ARCH == MG_ARCH_ESP32 && CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH
{
// After a crash, copy the backtrace from the core dump in flash: the same
// addresses the panic handler prints as "Backtrace: PC:SP PC:SP ...".
// The core dump stays in flash, so check the reset reason, otherwise an
// old crash is reported on every boot
esp_core_dump_summary_t *s = calloc(1, sizeof(*s));
static esp_core_dump_summary_t s; // Big on RISC-V, keep it off the stack
esp_reset_reason_t rr = esp_reset_reason();
uint32_t *out = mg_health_record.data;
size_t n = 0;
size_t n = 0, max = MG_HEALTH_DATA_SIZE;
if (rr != ESP_RST_PANIC && rr != ESP_RST_INT_WDT && rr != ESP_RST_TASK_WDT)
return; // Not a crash that went through the panic handler
if (max == 0 || esp_core_dump_get_summary(&s) != ESP_OK) return;
mg_health_record.cpuid = 42; // ESP32
mg_health_record.reset_reason = MG_HEALTH_RESET_FAULT;
memset(out, 0, sizeof(mg_health_record.data));
if (s == NULL) return;
if ((rr != ESP_RST_PANIC && rr != ESP_RST_INT_WDT &&
rr != ESP_RST_TASK_WDT) // Not a crash caught by the panic handler
|| esp_core_dump_get_summary(s) != ESP_OK) {
free(s);
return;
}
out[n++] = s->exc_pc;
memcpy(&out[n], s->exc_task, 16), n += 4;
out[n++] = s.exc_pc;
#if CONFIG_IDF_TARGET_ARCH_XTENSA
out[n++] = s->ex_info.exc_a[0];
out[n++] = s->ex_info.exc_a[1];
size_t len = s->exc_bt_info.depth;
if (len > MG_HEALTH_DATA_SIZE - n) len = (MG_HEALTH_DATA_SIZE - n);
memcpy(&out[n], s->exc_bt_info.bt, 4 * len); // sizeof(uint32_t)
for (uint32_t i = 0; i < s.exc_bt_info.depth && n < max; i++) {
if (s.exc_bt_info.bt[i] != s.exc_pc) out[n++] = s.exc_bt_info.bt[i];
}
#elif CONFIG_IDF_TARGET_ARCH_RISCV
out[n++] = s->ex_info.exc_ra;
out[n++] = s->ex_info.exc_sp;
size_t len = s->exc_bt_info.dump_size / 4 ; // sizeof(uint32_t)
if (len > MG_HEALTH_DATA_SIZE - n) len = (MG_HEALTH_DATA_SIZE - n);
memcpy(&out[n], s->exc_bt_info.stackdump, 4 * len);
// No unwinder: take the return address, then words on the stack that
// point into executable memory (a heuristic, may include stale values)
if (n < max) out[n++] = s.ex_info.ra;
for (uint32_t i = 0; i + 4 <= s.exc_bt_info.dump_size && n < max; i += 4) {
uint32_t w;
memcpy(&w, &s.exc_bt_info.stackdump[i], sizeof(w)); // Byte array
if (esp_ptr_executable((void *) (uintptr_t) w)) out[n++] = w;
}
#endif
}
#endif
free(s);
#endif // CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH
#endif // MG_ARCH_ESP32
}
static size_t print_uint32(void (*fn)(char, void *), void *arg, va_list *ap) {
@@ -3738,17 +3732,11 @@ static size_t print_uint32(void (*fn)(char, void *), void *arg, va_list *ap) {
size_t mg_print_crash_record(void (*fn)(char, void *), void *arg, va_list *ap) {
size_t len = 0;
if (mg_health_valid() &&
(mg_health_record.reset_reason == (uint16_t) MG_HEALTH_RESET_FAULT
#if MG_ARCH == MG_ARCH_ESP32
|| mg_health_record.reset_reason == (uint16_t) MG_HEALTH_RESET_WATCHDOG
#endif
)) {
mg_health_record.reset_reason == MG_HEALTH_RESET_FAULT) {
size_t n = sizeof(mg_health_record.data) / sizeof(mg_health_record.data[0]);
len += mg_xprintf(fn, arg, "{%m:%hhu,%m:%hu,%m:%hu,%m:%u,%m:[%M]}", //
MG_ESC("version"), mg_health_record.magic[3], //
MG_ESC("arch"), mg_health_record.arch, //
MG_ESC("reason"), mg_health_record.reset_reason, //
MG_ESC("cpuid"), mg_health_record.cpuid, //
len += mg_xprintf(fn, arg, "{%m:%hhu,%m:%u,%m:[%M]}", //
MG_ESC("version"), mg_health_record.magic[3], //
MG_ESC("cpuid"), mg_health_record.cpuid, //
MG_ESC("data"), print_uint32, n, mg_health_record.data);
} else {
len += mg_xprintf(fn, arg, "{}");
@@ -3756,7 +3744,6 @@ size_t mg_print_crash_record(void (*fn)(char, void *), void *arg, va_list *ap) {
(void) ap;
return len;
}
#endif
#ifdef MG_ENABLE_LINES
#line 1 "src/http.c"
@@ -9412,6 +9399,7 @@ static void rx_dhcp_client(struct mg_tcpip_if *ifp, struct pkt *pkt) {
: pkt->dhcp->siaddr ? pkt->dhcp->siaddr
: pkt->ip->src);
ifp->state = MG_TCPIP_STATE_REQ; // REQUESTING state
ifp->lease_expire = ifp->now + 5000; // No ACK by then: start over
} else if (msgtype == 5) { // DHCPACK
if (ifp->state == MG_TCPIP_STATE_REQ && ip && gw && lease) { // got an IP
uint64_t rand;
@@ -10719,6 +10707,9 @@ static void mg_ip_poll(struct mg_tcpip_if *ifp, bool s1) {
if (ifp->enable_dhcp_client && s1) {
if (ifp->state == MG_TCPIP_STATE_UP) {
tx_dhcp_discover(ifp); // INIT (4.4.1)
} else if (ifp->state == MG_TCPIP_STATE_REQ &&
ifp->now >= ifp->lease_expire) {
ifp->state = MG_TCPIP_STATE_UP; // REQUEST or ACK lost: DISCOVER again
} else if (ifp->state == MG_TCPIP_STATE_READY &&
ifp->lease_expire > 0) { // BOUND / RENEWING / REBINDING
if (ifp->now >= ifp->lease_expire) {
+7 -69
View File
@@ -253,10 +253,8 @@ extern "C" {
#include <esp_system.h>
#include <esp_mac.h>
#include <esp_idf_version.h>
#if MG_ENABLE_HEALTH
#include <esp_memory_utils.h>
#include <esp_core_dump.h>
#endif
// #include <esp_memory_utils.h> // For health record coredumps
// #include <esp_core_dump.h> // For health record coredumps
#define MG_PATH_MAX 128
@@ -4375,8 +4373,6 @@ void mg_ota_poll(struct mg_mgr *);
#if MG_ENABLE_HEALTH
#ifndef MG_HEALTH_MAGIC
#define MG_HEALTH_MAGIC {'M', 'G', 'H', '3'} // '3' is the layout version
#endif
@@ -4385,15 +4381,13 @@ void mg_ota_poll(struct mg_mgr *);
#define MG_HEALTH_RAM __attribute__((section(".mg_health")))
#endif
#ifndef MG_HEALTH_DATA_SIZE
#if (defined(STM32H7_DEV_ID) || defined(STM32H5)) || defined(STM32C5)
#if !defined(MG_HEALTH_DATA_SIZE) && (defined(STM32H7_DEV_ID) || defined(STM32H5) || defined(STM32C5))
#define MG_HEALTH_DATA_SIZE 250
#elif MG_ARCH == MG_ARCH_ESP32
#define MG_HEALTH_DATA_SIZE 23
#else
#endif
#ifndef MG_HEALTH_DATA_SIZE
#define MG_HEALTH_DATA_SIZE 10
#endif
#endif // MG_HEALTH_DATA_SIZE
// Places the record in RAM that survives a reset. Define in mongoose_config.h
// to match the linker script, e.g. __attribute__((section(".mg_health")))
@@ -4401,13 +4395,6 @@ void mg_ota_poll(struct mg_mgr *);
#define MG_HEALTH_RAM
#endif
// CPU core architecture
enum mg_health_arch {
MG_HEALTH_ARCH_ARM = 0,
MG_HEALTH_ARCH_XTENSA,
MG_HEALTH_ARCH_RISCV
};
// Why the device restarted. Hardware usually reports several flags at once,
// so these are ordered by how much they tell you: the most specific cause a
// device reports wins
@@ -4424,8 +4411,7 @@ enum mg_health_reason {
struct mg_health {
char magic[4]; // MG_HEALTH_MAGIC when the record holds valid data
uint16_t reset_reason; // enum mg_health_reason that started this boot
uint16_t arch; // enum mg_health_arch, core architecture (ARM, etc)
uint32_t reset_reason; // enum mg_health_reason that started this boot
uint32_t cpuid; // ARM CPU ID
uint32_t data[MG_HEALTH_DATA_SIZE];
};
@@ -4458,57 +4444,9 @@ static inline const char *mg_health_reason_str(int reason) {
}
}
#if MG_ARCH == MG_ARCH_ESP32
#ifndef CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH
#define CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH 0
#endif
#ifndef ESP_PARTITION_SUBTYPE_DATA_COREDUMP
#define ESP_PARTITION_SUBTYPE_DATA_COREDUMP 0x03
#endif
// clang-format off
static inline uint16_t esp2mg(esp_reset_reason_t rr) {
switch(rr) {
case ESP_RST_POWERON: return (uint16_t) MG_HEALTH_RESET_POWER;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 2)
case ESP_RST_PWR_GLITCH:
#endif
case ESP_RST_BROWNOUT: return (uint16_t) MG_HEALTH_RESET_BROWNOUT;
case ESP_RST_EXT: return (uint16_t) MG_HEALTH_RESET_PIN;
case ESP_RST_SW: return (uint16_t) MG_HEALTH_RESET_SOFTWARE;
case ESP_RST_DEEPSLEEP: return (uint16_t) MG_HEALTH_RESET_LOWPOWER;
case ESP_RST_INT_WDT:
case ESP_RST_TASK_WDT:
case ESP_RST_WDT: return (uint16_t) MG_HEALTH_RESET_WATCHDOG;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 2)
case ESP_RST_CPU_LOCKUP:
#endif
case ESP_RST_PANIC: return (uint16_t) MG_HEALTH_RESET_FAULT;
default: break;
}
return (uint16_t) MG_HEALTH_RESET_UNKNOWN;
}
// clang-format on
#endif
#define MG_HEALTH_INIT() mg_health_init()
extern size_t mg_print_crash_record(mg_pfn_t, void *, va_list *);
extern void mg_health_init(void);
#else
#define MG_HEALTH_INIT()
static inline int mg_health_reason(void) {
return 0;
}
static inline const char *mg_health_reason_str(int reason) {
(void) reason;
return "unknown";
}
static inline size_t mg_print_crash_record(mg_pfn_t fn, void *arg,
va_list *ap) {
(void) ap;
return mg_xprintf(fn, arg, "{}");
}
#endif
// Cloud connector API to https://mdash.net - device management cloud
+2 -4
View File
@@ -22,10 +22,8 @@
#include <esp_system.h>
#include <esp_mac.h>
#include <esp_idf_version.h>
#if MG_ENABLE_HEALTH
#include <esp_memory_utils.h>
#include <esp_core_dump.h>
#endif
// #include <esp_memory_utils.h> // For health record coredumps
// #include <esp_core_dump.h> // For health record coredumps
#define MG_PATH_MAX 128
+37 -50
View File
@@ -1,13 +1,12 @@
#include "config.h"
#include "health.h"
#if MG_ENABLE_HEALTH
// The one health record. Lives in RAM that survives a warm reset, see
// MG_HEALTH_RAM and the .mg_health region in the linker script
struct mg_health mg_health_record MG_HEALTH_RAM;
// STM32 H7, H5, C5 support. Needs the _estack symbol from the linker script
#if defined(STM32H7_DEV_ID) || defined(STM32H5) || defined(STM32C5)
// STM32 H723 support. Needs the _estack symbol from the linker script
#if MG_ENABLE_HEALTH && (defined(STM32H7_DEV_ID) || defined(STM32H5) || defined(STM32C5))
// Fault handler body. Runs in exception context: no printf, no malloc, no
// blocking calls. Copies the unwind registers and raw stack into the health
// record, then resets.
@@ -49,57 +48,52 @@ void BusFault_Handler(void) __attribute__((alias("HardFault_Handler")));
void UsageFault_Handler(void) __attribute__((alias("HardFault_Handler")));
#endif
// Start a new boot: validate the record. Called by MG_HEALTH_INIT()
// Start a new boot: validate the record, count the boot, store the reset
// reason. The current boot moves to prev_* slots first, so the previous
// boot's uptime and reason stay readable. Called by MG_HEALTH_INIT()
void mg_health_init(void) {
char magic[] = MG_HEALTH_MAGIC;
// struct mg_health *h = mg_health_get();
if (!mg_health_valid()) { // First boot, or RAM lost its contents
memset(&mg_health_record, 0, sizeof(mg_health_record));
memcpy(mg_health_record.magic, magic, sizeof(mg_health_record.magic));
}
#if MG_ARCH == MG_ARCH_ESP32
#if CONFIG_IDF_TARGET_ARCH_XTENSA
mg_health_record.arch = (uint16_t) MG_HEALTH_ARCH_XTENSA;
#elif CONFIG_IDF_TARGET_ARCH_RISCV
mg_health_record.arch = (uint16_t) MG_HEALTH_ARCH_RISCV;
#endif
esp_reset_reason_t rr = esp_reset_reason();
mg_health_record.reset_reason = esp2mg(rr);
#if CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH == 0
MG_ERROR(("MG_HEALTH is enabled but CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=n"));
#else
#if 0 && MG_ARCH == MG_ARCH_ESP32 && CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH
{
// After a crash, copy the backtrace from the core dump in flash: the same
// addresses the panic handler prints as "Backtrace: PC:SP PC:SP ...".
// The core dump stays in flash, so check the reset reason, otherwise an
// old crash is reported on every boot
esp_core_dump_summary_t *s = calloc(1, sizeof(*s));
static esp_core_dump_summary_t s; // Big on RISC-V, keep it off the stack
esp_reset_reason_t rr = esp_reset_reason();
uint32_t *out = mg_health_record.data;
size_t n = 0;
size_t n = 0, max = MG_HEALTH_DATA_SIZE;
if (rr != ESP_RST_PANIC && rr != ESP_RST_INT_WDT && rr != ESP_RST_TASK_WDT)
return; // Not a crash that went through the panic handler
if (max == 0 || esp_core_dump_get_summary(&s) != ESP_OK) return;
mg_health_record.cpuid = 42; // ESP32
mg_health_record.reset_reason = MG_HEALTH_RESET_FAULT;
memset(out, 0, sizeof(mg_health_record.data));
if (s == NULL) return;
if ((rr != ESP_RST_PANIC && rr != ESP_RST_INT_WDT &&
rr != ESP_RST_TASK_WDT) // Not a crash caught by the panic handler
|| esp_core_dump_get_summary(s) != ESP_OK) {
free(s);
return;
}
out[n++] = s->exc_pc;
memcpy(&out[n], s->exc_task, 16), n += 4;
out[n++] = s.exc_pc;
#if CONFIG_IDF_TARGET_ARCH_XTENSA
out[n++] = s->ex_info.exc_a[0];
out[n++] = s->ex_info.exc_a[1];
size_t len = s->exc_bt_info.depth;
if (len > MG_HEALTH_DATA_SIZE - n) len = (MG_HEALTH_DATA_SIZE - n);
memcpy(&out[n], s->exc_bt_info.bt, 4 * len); // sizeof(uint32_t)
for (uint32_t i = 0; i < s.exc_bt_info.depth && n < max; i++) {
if (s.exc_bt_info.bt[i] != s.exc_pc) out[n++] = s.exc_bt_info.bt[i];
}
#elif CONFIG_IDF_TARGET_ARCH_RISCV
out[n++] = s->ex_info.exc_ra;
out[n++] = s->ex_info.exc_sp;
size_t len = s->exc_bt_info.dump_size / 4 ; // sizeof(uint32_t)
if (len > MG_HEALTH_DATA_SIZE - n) len = (MG_HEALTH_DATA_SIZE - n);
memcpy(&out[n], s->exc_bt_info.stackdump, 4 * len);
// No unwinder: take the return address, then words on the stack that
// point into executable memory (a heuristic, may include stale values)
if (n < max) out[n++] = s.ex_info.ra;
for (uint32_t i = 0; i + 4 <= s.exc_bt_info.dump_size && n < max; i += 4) {
uint32_t w;
memcpy(&w, &s.exc_bt_info.stackdump[i], sizeof(w)); // Byte array
if (esp_ptr_executable((void *) (uintptr_t) w)) out[n++] = w;
}
#endif
}
#endif
free(s);
#endif // CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH
#endif // MG_ARCH_ESP32
}
static size_t print_uint32(void (*fn)(char, void *), void *arg, va_list *ap) {
@@ -113,17 +107,11 @@ static size_t print_uint32(void (*fn)(char, void *), void *arg, va_list *ap) {
size_t mg_print_crash_record(void (*fn)(char, void *), void *arg, va_list *ap) {
size_t len = 0;
if (mg_health_valid() &&
(mg_health_record.reset_reason == (uint16_t) MG_HEALTH_RESET_FAULT
#if MG_ARCH == MG_ARCH_ESP32
|| mg_health_record.reset_reason == (uint16_t) MG_HEALTH_RESET_WATCHDOG
#endif
)) {
mg_health_record.reset_reason == MG_HEALTH_RESET_FAULT) {
size_t n = sizeof(mg_health_record.data) / sizeof(mg_health_record.data[0]);
len += mg_xprintf(fn, arg, "{%m:%hhu,%m:%hu,%m:%hu,%m:%u,%m:[%M]}", //
MG_ESC("version"), mg_health_record.magic[3], //
MG_ESC("arch"), mg_health_record.arch, //
MG_ESC("reason"), mg_health_record.reset_reason, //
MG_ESC("cpuid"), mg_health_record.cpuid, //
len += mg_xprintf(fn, arg, "{%m:%hhu,%m:%u,%m:[%M]}", //
MG_ESC("version"), mg_health_record.magic[3], //
MG_ESC("cpuid"), mg_health_record.cpuid, //
MG_ESC("data"), print_uint32, n, mg_health_record.data);
} else {
len += mg_xprintf(fn, arg, "{}");
@@ -131,4 +119,3 @@ size_t mg_print_crash_record(void (*fn)(char, void *), void *arg, va_list *ap) {
(void) ap;
return len;
}
#endif
+5 -65
View File
@@ -5,8 +5,6 @@
#include "arch.h"
#include "printf.h"
#if MG_ENABLE_HEALTH
#ifndef MG_HEALTH_MAGIC
#define MG_HEALTH_MAGIC {'M', 'G', 'H', '3'} // '3' is the layout version
#endif
@@ -15,15 +13,13 @@
#define MG_HEALTH_RAM __attribute__((section(".mg_health")))
#endif
#ifndef MG_HEALTH_DATA_SIZE
#if (defined(STM32H7_DEV_ID) || defined(STM32H5)) || defined(STM32C5)
#if !defined(MG_HEALTH_DATA_SIZE) && (defined(STM32H7_DEV_ID) || defined(STM32H5) || defined(STM32C5))
#define MG_HEALTH_DATA_SIZE 250
#elif MG_ARCH == MG_ARCH_ESP32
#define MG_HEALTH_DATA_SIZE 23
#else
#endif
#ifndef MG_HEALTH_DATA_SIZE
#define MG_HEALTH_DATA_SIZE 10
#endif
#endif // MG_HEALTH_DATA_SIZE
// Places the record in RAM that survives a reset. Define in mongoose_config.h
// to match the linker script, e.g. __attribute__((section(".mg_health")))
@@ -31,13 +27,6 @@
#define MG_HEALTH_RAM
#endif
// CPU core architecture
enum mg_health_arch {
MG_HEALTH_ARCH_ARM = 0,
MG_HEALTH_ARCH_XTENSA,
MG_HEALTH_ARCH_RISCV
};
// Why the device restarted. Hardware usually reports several flags at once,
// so these are ordered by how much they tell you: the most specific cause a
// device reports wins
@@ -54,8 +43,7 @@ enum mg_health_reason {
struct mg_health {
char magic[4]; // MG_HEALTH_MAGIC when the record holds valid data
uint16_t reset_reason; // enum mg_health_reason that started this boot
uint16_t arch; // enum mg_health_arch, core architecture (ARM, etc)
uint32_t reset_reason; // enum mg_health_reason that started this boot
uint32_t cpuid; // ARM CPU ID
uint32_t data[MG_HEALTH_DATA_SIZE];
};
@@ -88,54 +76,6 @@ static inline const char *mg_health_reason_str(int reason) {
}
}
#if MG_ARCH == MG_ARCH_ESP32
#ifndef CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH
#define CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH 0
#endif
#ifndef ESP_PARTITION_SUBTYPE_DATA_COREDUMP
#define ESP_PARTITION_SUBTYPE_DATA_COREDUMP 0x03
#endif
// clang-format off
static inline uint16_t esp2mg(esp_reset_reason_t rr) {
switch(rr) {
case ESP_RST_POWERON: return (uint16_t) MG_HEALTH_RESET_POWER;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 2)
case ESP_RST_PWR_GLITCH:
#endif
case ESP_RST_BROWNOUT: return (uint16_t) MG_HEALTH_RESET_BROWNOUT;
case ESP_RST_EXT: return (uint16_t) MG_HEALTH_RESET_PIN;
case ESP_RST_SW: return (uint16_t) MG_HEALTH_RESET_SOFTWARE;
case ESP_RST_DEEPSLEEP: return (uint16_t) MG_HEALTH_RESET_LOWPOWER;
case ESP_RST_INT_WDT:
case ESP_RST_TASK_WDT:
case ESP_RST_WDT: return (uint16_t) MG_HEALTH_RESET_WATCHDOG;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 2)
case ESP_RST_CPU_LOCKUP:
#endif
case ESP_RST_PANIC: return (uint16_t) MG_HEALTH_RESET_FAULT;
default: break;
}
return (uint16_t) MG_HEALTH_RESET_UNKNOWN;
}
// clang-format on
#endif
#define MG_HEALTH_INIT() mg_health_init()
extern size_t mg_print_crash_record(mg_pfn_t, void *, va_list *);
extern void mg_health_init(void);
#else
#define MG_HEALTH_INIT()
static inline int mg_health_reason(void) {
return 0;
}
static inline const char *mg_health_reason_str(int reason) {
(void) reason;
return "unknown";
}
static inline size_t mg_print_crash_record(mg_pfn_t fn, void *arg,
va_list *ap) {
(void) ap;
return mg_xprintf(fn, arg, "{}");
}
#endif
+4
View File
@@ -734,6 +734,7 @@ static void rx_dhcp_client(struct mg_tcpip_if *ifp, struct pkt *pkt) {
: pkt->dhcp->siaddr ? pkt->dhcp->siaddr
: pkt->ip->src);
ifp->state = MG_TCPIP_STATE_REQ; // REQUESTING state
ifp->lease_expire = ifp->now + 5000; // No ACK by then: start over
} else if (msgtype == 5) { // DHCPACK
if (ifp->state == MG_TCPIP_STATE_REQ && ip && gw && lease) { // got an IP
uint64_t rand;
@@ -2041,6 +2042,9 @@ static void mg_ip_poll(struct mg_tcpip_if *ifp, bool s1) {
if (ifp->enable_dhcp_client && s1) {
if (ifp->state == MG_TCPIP_STATE_UP) {
tx_dhcp_discover(ifp); // INIT (4.4.1)
} else if (ifp->state == MG_TCPIP_STATE_REQ &&
ifp->now >= ifp->lease_expire) {
ifp->state = MG_TCPIP_STATE_UP; // REQUEST or ACK lost: DISCOVER again
} else if (ifp->state == MG_TCPIP_STATE_READY &&
ifp->lease_expire > 0) { // BOUND / RENEWING / REBINDING
if (ifp->now >= ifp->lease_expire) {
+32
View File
@@ -0,0 +1,32 @@
# Mesh simulator. `make` builds and runs an example
ROOT = ../..
SRCS = simulator.c l2_mesh.c l2_802154.c star.c $(ROOT)/mongoose.c
HDRS = simulator.h l2_mesh.h l2_802154.h star.h $(ROOT)/mongoose.h
WARN = -pedantic -W -Wall -Werror -Wshadow -Wdouble-promotion -fno-common \
-Wconversion -Wundef -Wmissing-prototypes -Wstrict-prototypes
DEFS = -DMG_ENABLE_TCPIP=1 -DMG_ENABLE_TCPIP_DRIVER_INIT=0 -DMG_ENABLE_IPV6=0 \
-DMG_ENABLE_CUSTOM_MILLIS=1
CFLAGS ?= -O2 -g0 $(WARN)
X ?= 100
Y ?= 100
ROWS ?= 10
COLS ?= 10
# Nodes at every corner of a ROWS x COLS grid over X,Y, edges included
GRID = $(shell for r in $$(seq 0 $(ROWS)); do \
for c in $$(seq 0 $(COLS)); do \
echo -node $$((c * $(X) / $(COLS))),$$((r * $(Y) / $(ROWS))); \
done; done)
all: mesh_sim
./mesh_sim -area $(X),$(Y) \
-bridge $$(($(X) / 2)),$$(($(Y) / 2)) \
$(GRID)
mesh_sim: $(SRCS) $(HDRS) Makefile
$(CC) $(SRCS) $(CFLAGS) $(DEFS) -I. -I$(ROOT) -o $@
clean:
rm -rf mesh_sim mesh_sim.dSYM
.PHONY: all clean
+189
View File
@@ -0,0 +1,189 @@
# Ethernet over a radio mesh
## The idea
Small radio devices, e.g. IEEE 802.15.4, sub-GHz or LoRa boards, should run
Mongoose as if they were on an ordinary Ethernet LAN: get an IP address over
DHCP, answer HTTP, talk MQTT. No 6LoWPAN, no Thread, no special stack.
To do that, the radio interface looks like Ethernet to Mongoose
(`MG_TCPIP_L2_ETH`). Its driver splits each Ethernet frame into radio
frames, sends them over the air, and the receiver puts them back together.
One or more bridges connect the radio network to the wired LAN: a bridge has
an Ethernet port and a radio. A node in range of a bridge talks to it
directly; a node out of range reaches a bridge through other nodes, which
forward its frames. That makes the radio network a mesh. With several
bridges, each node uses one of them, e.g. the nearest; the mesh covers a
bigger area, and keeps working if a bridge goes down. A PC on the LAN sees
the radio nodes as regular hosts on the same LAN.
```
+---- bridge 1 ~~~ node ~~~ node
| ~
PC ---- LAN node
| ~
+---- bridge 2 ~~~ node ~~~ node ~~~ node
```
`~~~` and `~` are radio hops. Nodes far from a bridge send their frames
through nearer nodes. A node in the middle can reach either bridge.
## Two parts plug in
- `struct mg_radio`, a radio standard: how big a radio frame is, how
addresses are written into it. `mg_radio_802154` is the first one.
- `struct mg_mesh`, a mesh strategy: how nodes find a bridge, and where each
frame goes next. `mg_mesh_star` is the first and simplest one: every node
talks to a bridge directly, one hop.
Everything else, splitting and reassembling frames, sending again when a
frame is lost, is common code that works the same for every radio and every
mesh. The goal is to swap radios and mesh strategies without touching
anything else.
## Why a simulator
Real boards are slow to test with, and you can't put 500 of them on a desk.
The simulator runs a whole network in one process: every node is a real
Mongoose instance, only the radio and the air between nodes are emulated.
That lets us try mesh strategies with many nodes, and measure how long it
takes nodes to join, get an address, and answer a request.
## Virtual time
The simulator has its own clock, `simulator::now`, in microseconds, and
Mongoose reads it through `mg_millis()`. Nothing waits for real time: the
simulator moves the clock forward itself, 250 us per step, and polls every
station once per step. A minute of network traffic takes about a second to
simulate. `-t SEC` sets how long to run, 10 minutes by default.
The step is a trade-off. Each step polls every station's Mongoose, and even
an idle poll costs something, so a smaller step means a slower run. A
bigger step makes frames wait longer in queues, see below. Measured with
121 nodes and 10 simulated minutes:
| Step | Request time, mean | Run time |
|------|--------------------|----------|
| 1 ms | 6.1 ms | 0.9 s |
| 250 us | 1.5 ms | 3.0 s |
| 100 us | 0.6 ms | 7.2 s |
| 10 us | 0.06 ms | 72 s |
## The setup
```
DHCP server ---------+
10.0.0.1 |
02:01:00:00:00:00 +-- LAN ---- bridge ~~~ nodes
|
test station --------+
10.0.0.2
02:02:00:00:00:00
```
The LAN is assumed infinitely fast, see "How frames move on the LAN" below.
Two stations are on it:
- The DHCP server, 10.0.0.1, MAC 02:01:00:00:00:00, gives each node an
address made from its MAC: 02:00:00:X:Y:Z gets 10.X+1.Y.Z, so node
addresses never clash with the LAN stations' 10.0.0.x.
- The test station, 10.0.0.2, MAC 02:02:00:00:00:00, is a Mongoose
instance. It waits for the mesh to settle, 60 seconds by default
(`-w SEC`), then sends `GET /` to each node in turn, one at a time, at the
node's own IP address. A node with no address, or one that doesn't answer
within 10 seconds, counts as failed. At the end it prints how many
requests succeeded and how long they took.
Bridges are on the LAN too, and get every frame, as if in promiscuous mode:
a frame for one of their radio nodes isn't addressed to the bridge itself.
A bridge forwards every frame it gets from the LAN to the radio, and every
frame from the radio to the LAN, with no filtering yet.
Bridges and radio nodes get MACs 02:00:00:xx:xx:xx, numbered from 1 in
command-line order: 02:00:00:00:00:01, 02:00:00:00:00:02, and so on.
### How frames move on the LAN
Every LAN station, the DHCP server, the test station and each bridge, has a
LAN interface, `lif`: a `struct mg_tcpip_if` with a receive queue. All of
them use the same driver, `s_lan_driver`. There are no cables and no switch:
- **Sending (tx).** A station sends a frame with its driver's `tx`, which is
`lan_tx()`. It looks at the destination MAC and copies the frame into the
receive queues of the stations it is for:
- a broadcast or multicast goes to every station but the sender;
- a unicast goes to the station with that MAC;
- bridges get every frame, as a real bridge in promiscuous mode would:
a frame for a radio node is addressed to the node, not to its bridge.
That's all `tx` does. Nobody's code runs on the receiving side yet.
- **Receiving (rx).** Each station takes the frames from its own queue when
the main loop polls it:
- the test station runs Mongoose, which reads the queue by itself, as it
would with a real Ethernet driver that queues frames from an interrupt;
- the DHCP server reads it in `dhcp_poll()`, and replies by sending
through its own `lif`;
- bridges read it in `bridges_poll()`, and forward it to the radio.
So a frame waits in a queue until its receiver's next poll, at most one
step of virtual time, 250 us. Ethernet is hundreds of times faster than the radio, so that
doesn't matter for what we measure. And since sending never calls the
receiver's code, a reply is never sent from inside another station's send.
For example, a node asking for an address:
1. The node's DHCP DISCOVER crosses the radio to a bridge, which sends it
out of its `lif`. It is a broadcast, so `lan_tx()` puts it into the
queues of the DHCP server, the test station and the other bridges.
2. On its next poll, the DHCP server reads it and sends an OFFER, also a
broadcast, which lands in the test station's and the bridges' queues.
3. On their next poll, the bridges read the OFFER and send it over the
radio to the node. The test station's Mongoose drops it: the test station
has a fixed address and no DHCP client.
## Files
| File | What |
|------|------|
| `l2_mesh.h`, `l2_mesh.c` | `struct mg_radio`, `struct mg_mesh`, common code |
| `l2_802154.h`, `l2_802154.c` | `mg_radio_802154`, IEEE 802.15.4 |
| `star.h`, `star.c` | `mg_mesh_star`, the star mesh |
| `simulator.h`, `simulator.c` | The simulator |
| `Makefile` | Builds and runs the simulator |
## Where we are
It works end to end: nodes get addresses over DHCP, and the test station's
requests reach them through the bridges. But the radio is ideal for now:
- The radio driver works like the LAN's: it puts whole Ethernet frames
straight into receive queues, all of them for a broadcast, else the one
with the destination MAC, and bridges get every frame. No range, no
loss, no airtime.
- Bridges forward everything, with no filtering.
- So use one bridge for now. With two, a broadcast from the LAN goes to the
radio through both, each hears the other's copy and sends it back to the
LAN, and that never ends. Deciding who forwards what, without loops, is
the job of `mg_mesh`.
`mg_radio` and `mg_mesh` are still stubs. Next steps, one at a time: the
air (range, loss, airtime), fragmenting frames into radio frames with
`mg_radio`, choosing a bridge and forwarding through nodes with `mg_mesh`,
and filtering at the bridges.
## Running
```sh
make # 100x100 m, a bridge, 11x11 nodes
make X=200 Y=50 ROWS=2 COLS=8 # Another area and grid
```
`make` puts a bridge in the middle of the area, and a node at every corner
of a `ROWS` x `COLS` grid over it. The simulator itself takes:
```
./mesh_sim -area X,Y,Z -bridge X,Y,Z -node X,Y,Z ... -t SEC -w SEC
```
Positions are in metres; `X,Y` means `Z` is 0. Repeat `-bridge` and `-node`
for more.
+6
View File
@@ -0,0 +1,6 @@
// Copyright (c) 2026 Cesanta Software Limited
// All rights reserved
#include "l2_802154.h"
const struct mg_radio mg_radio_802154 = {"802.15.4"};
+10
View File
@@ -0,0 +1,10 @@
// Copyright (c) 2026 Cesanta Software Limited
// All rights reserved
//
// IEEE 802.15.4 radio
#pragma once
#include "l2_mesh.h"
extern const struct mg_radio mg_radio_802154;
+4
View File
@@ -0,0 +1,4 @@
// Copyright (c) 2026 Cesanta Software Limited
// All rights reserved
#include "l2_mesh.h"
+21
View File
@@ -0,0 +1,21 @@
// Copyright (c) 2026 Cesanta Software Limited
// All rights reserved
//
// Ethernet over radio, through a mesh. Mongoose sees an Ethernet interface,
// its driver splits Ethernet frames into radio frames. Two parts plug in:
// - struct mg_radio: a radio standard, e.g. mg_radio_802154
// - struct mg_mesh: how the mesh is built and routed, e.g. mg_mesh_star
#pragma once
#include "mongoose.h"
// A radio standard
struct mg_radio {
const char *name; // For logs
};
// A mesh strategy
struct mg_mesh {
const char *name; // For logs
};
+433
View File
@@ -0,0 +1,433 @@
// Copyright (c) 2026 Cesanta Software Limited
// All rights reserved
//
// Radio mesh simulator. Bridges and nodes are stations at positions in an
// area. Time is virtual: see mg_millis(). On the LAN: a DHCP server, and a
// test station that sends HTTP requests to the nodes
#include "simulator.h"
#define STEP_MICROSECONDS 250 // Virtual time per main loop step, see README.md
static struct simulator s_sim; // The simulator
// Mongoose's clock: virtual time. A minute of network traffic takes far less
// than a minute to simulate
uint64_t mg_millis(void) {
return s_sim.now / 1000;
}
// Parse "X,Y,Z", or "X,Y" with Z 0. Return false if it is not that
static bool parse_pos(const char *s, struct pos *p) {
int n = 0; // Characters parsed
p->x = p->y = p->z = 0;
return (sscanf(s, "%lf,%lf,%lf%n", &p->x, &p->y, &p->z, &n) == 3 ||
sscanf(s, "%lf,%lf%n", &p->x, &p->y, &n) == 2) &&
s[n] == 0;
}
// Is position p inside the area of sim
static bool inside(const struct simulator *sim, const struct pos *p) {
return p->x >= 0 && p->x <= sim->area.x && p->y >= 0 && p->y <= sim->area.y &&
p->z >= 0 && p->z <= sim->area.z;
}
static void usage(const struct simulator *sim, const char *prog) {
fprintf(stderr,
"Usage: %s OPTIONS\n"
"Positions: X,Y,Z in metres, or X,Y with Z 0\n"
" -area X,Y,Z - area, a box, default: %g,%g,%g\n"
" -bridge X,Y,Z - a bridge; repeat for more\n"
" -node X,Y,Z - a radio node; repeat for more\n"
" -t SEC - simulated time, seconds, default: %g\n"
" -w SEC - mesh settle time before requests, default: %g\n",
prog, sim->area.x, sim->area.y, sim->area.z,
(double) sim->duration / 1e6, (double) sim->tester.wait / 1e6);
exit(EXIT_FAILURE);
}
// The next radio MAC, to mac: 02:00:00:xx:xx:xx, numbered from 1
static void next_mac(struct simulator *sim, uint8_t *mac) {
uint32_t n = ++sim->last_mac;
mac[0] = 2, mac[1] = mac[2] = 0, mac[3] = (uint8_t) (n >> 16);
mac[4] = (uint8_t) (n >> 8), mac[5] = (uint8_t) n;
}
// Add a bridge at position s. Return false if s is not a position
static bool add_bridge(struct simulator *sim, const char *s) {
struct bridge *b = (struct bridge *) calloc(1, sizeof(*b));
if (b == NULL || !parse_pos(s, &b->pos)) return free(b), false;
next_mac(sim, b->rif.mac);
LIST_ADD_TAIL(struct bridge, &sim->bridges, b);
sim->num_bridges++;
return true;
}
// Add a radio node at position s. Return false if s is not a position
static bool add_node(struct simulator *sim, const char *s) {
struct radio_node *n = (struct radio_node *) calloc(1, sizeof(*n));
if (n == NULL || !parse_pos(s, &n->pos)) return free(n), false;
next_mac(sim, n->rif.mac);
LIST_ADD_TAIL(struct radio_node, &sim->nodes, n);
sim->num_nodes++;
return true;
}
// Exit if position x is outside the area of sim
static void check_inside(const struct simulator *sim, const struct pos *x) {
if (!inside(sim, x)) {
fprintf(stderr, "%g,%g,%g is outside the area\n", x->x, x->y, x->z);
exit(EXIT_FAILURE);
}
}
// Check that all bridges and nodes are inside the area of sim
static void check_area(const struct simulator *sim) {
struct bridge *b;
struct radio_node *n;
for (b = sim->bridges; b != NULL; b = b->next) check_inside(sim, &b->pos);
for (n = sim->nodes; n != NULL; n = n->next) check_inside(sim, &n->pos);
}
#define LAN_QUEUE_SIZE (64 * 1024) // Receive queue of a LAN interface
// Put frame buf, len bytes, into the receive queue of LAN interface to, if
// it is not the sender, from
static void lan_put(struct mg_tcpip_if *to, struct mg_tcpip_if *from,
const void *buf, size_t len) {
if (to != from) mg_tcpip_qwrite((void *) buf, len, to);
}
// LAN driver, all LAN interfaces share it. The LAN is infinitely fast: tx
// puts the frame straight into the receive queues of the interfaces it is
// for: all, if broadcast; else the one with the destination MAC. Bridges get
// every frame: some are for their radio nodes. Each station takes frames
// from its queue when polled, see dhcp_poll(), bridges_poll(); Mongoose does
// it for the test station
static size_t lan_tx(const void *buf, size_t len, struct mg_tcpip_if *ifp) {
const uint8_t *dst = (const uint8_t *) buf;
bool all = dst[0] & 1; // Broadcast, multicast
struct bridge *b;
if (all || memcmp(dst, s_sim.dhcp.lif.mac, 6) == 0) {
lan_put(&s_sim.dhcp.lif, ifp, buf, len);
}
if (all || memcmp(dst, s_sim.tester.lif.mac, 6) == 0) {
lan_put(&s_sim.tester.lif, ifp, buf, len);
}
for (b = s_sim.bridges; b != NULL; b = b->next) {
lan_put(&b->lif, ifp, buf, len);
}
return len;
}
static bool lan_init(struct mg_tcpip_if *ifp) {
(void) ifp;
return true;
}
static bool lan_link(struct mg_tcpip_if *ifp, bool s1) {
(void) ifp, (void) s1;
return true;
}
static struct mg_tcpip_driver s_lan_driver = {lan_init, lan_tx, NULL, lan_link};
// Internet checksum of len bytes
static uint16_t ip_csum(const uint8_t *buf, size_t len) {
uint32_t sum = 0;
size_t i;
for (i = 0; i + 1 < len; i += 2) sum += (uint32_t) (buf[i] << 8 | buf[i + 1]);
if (len & 1) sum += (uint32_t) (buf[len - 1] << 8);
while (sum >> 16) sum = (sum & 0xffff) + (sum >> 16);
return (uint16_t) ~sum;
}
// A node's address, from its MAC 02:00:00:X:Y:Z: 10.X+1.Y.Z, so it never
// clashes with the LAN stations' 10.0.0.x
static void node_ip(const uint8_t *mac, uint8_t *ip) {
ip[0] = 10, ip[1] = (uint8_t) (mac[3] + 1), ip[2] = mac[4], ip[3] = mac[5];
}
// DHCP server, a frame from its queue: a DISCOVER gets an OFFER, a REQUEST
// an ACK. Replies are broadcast. The address comes from the client's MAC
static void dhcp_rx(struct simulator *sim, const uint8_t *buf, size_t len) {
static const uint8_t opts[] = {53, 1, 0, // Message type, set below
1, 4, 255, 0, 0, 0, // Mask
3, 4, 10, 0, 0, 1, // Router
54, 4, 10, 0, 0, 1, // Server
51, 4, 0, 0, 14, 16, // Lease, 3600 s
255};
uint8_t r[14 + 20 + 8 + 240 + sizeof(opts)], type = 0;
const uint8_t *dh, *p, *end = buf + len;
size_t ihl, rlen = sizeof(r);
if ((buf[0] & 1) == 0) return; // Bcast
if (len < 34 || buf[12] != 8 || buf[13] != 0 || buf[23] != 17) return; // UDP
ihl = (size_t) (buf[14] & 15) * 4, dh = buf + 14 + ihl + 8; // DHCP
if (dh + 240 > end || buf[14 + ihl + 2] != 0 || buf[14 + ihl + 3] != 67) {
return;
}
for (p = dh + 240; p + 2 <= end && p[0] != 255; p += p[0] ? p[1] + 2 : 1) {
if (p[0] == 53 && p[1] == 1 && p + 3 <= end) type = p[2];
}
if (type != 1 && type != 3) return; // DISCOVER, REQUEST
memset(r, 0, sizeof(r));
memset(r, 0xff, 6), memcpy(r + 6, sim->dhcp.lif.mac, 6),
r[12] = 8; // Ethernet
r[14] = 0x45, r[16] = (uint8_t) ((rlen - 14) >> 8); // IPv4
r[17] = (uint8_t) (rlen - 14), r[22] = 64, r[23] = 17;
r[26] = 10, r[29] = 1, memset(r + 30, 0xff, 4); // 10.0.0.1 -> bcast
r[24] = (uint8_t) (ip_csum(r + 14, 20) >> 8);
r[25] = (uint8_t) ip_csum(r + 14, 20);
r[35] = 67, r[37] = 68; // UDP, csum 0: none
r[38] = (uint8_t) ((rlen - 34) >> 8), r[39] = (uint8_t) (rlen - 34);
r[42] = 2, r[43] = 1, r[44] = 6; // DHCP: reply, Ethernet
memcpy(r + 46, dh + 4, 4); // xid
memcpy(r + 52, dh + 10, 2); // flags
node_ip(dh + 28, r + 58); // yiaddr
r[62] = 10, r[65] = 1; // siaddr
memcpy(r + 70, dh + 28, 16); // chaddr
r[278] = 99, r[279] = 130, r[280] = 83, r[281] = 99; // Magic
memcpy(r + 282, opts, sizeof(opts));
r[284] = type == 1 ? 2 : 5; // OFFER, ACK
sim->dhcp.lif.driver->tx(r, rlen, &sim->dhcp.lif);
}
// DHCP server's work: take the frames from its queue
static void dhcp_poll(struct simulator *sim) {
char *buf;
size_t len;
while ((len = mg_queue_next(&sim->dhcp.lif.recv_queue, &buf)) > 0) {
dhcp_rx(sim, (const uint8_t *) buf, len);
mg_queue_del(&sim->dhcp.lif.recv_queue, len);
}
}
// The request on the way finished: ok, or failed
static void tester_done(struct lan_tester *t, uint64_t now, bool ok) {
double ms = (double) (now - t->start) / 1000;
if (ok) {
t->ok++, t->sum += ms;
if (t->ok == 1 || ms < t->min) t->min = ms;
if (ms > t->max) t->max = ms;
} else {
t->failed++;
}
if (t->c != NULL) t->c->is_closing = 1, t->c->fn_data = NULL;
t->c = NULL;
}
// HTTP client: GET /, the reply ends the request
static void tester_fn(struct mg_connection *c, int ev, void *ev_data) {
struct lan_tester *t = (struct lan_tester *) c->fn_data;
if (t == NULL) return; // Done with
if (ev == MG_EV_CONNECT) {
mg_printf(c, "GET / HTTP/1.0\r\n\r\n");
} else if (ev == MG_EV_HTTP_MSG) {
tester_done(t, s_sim.now, true);
} else if (ev == MG_EV_ERROR || ev == MG_EV_CLOSE) {
tester_done(t, s_sim.now, false);
}
(void) ev_data;
}
// Test station's work: Mongoose, then requests, one at a time, once the mesh
// had time to settle. A node without an IP address fails at once
static void tester_poll(struct simulator *sim) {
struct radio_node *n;
char url[64];
mg_mgr_poll(&sim->tester.mgr, 0);
if (sim->tester.c != NULL && sim->now - sim->tester.start >
(uint64_t) sim->request_timeout_ms * 1000) {
tester_done(&sim->tester, sim->now, false);
}
if (sim->now < sim->tester.wait || sim->tester.c != NULL ||
sim->tester.next == NULL)
return;
n = sim->tester.next;
sim->tester.next = n->next, sim->tester.start = sim->now;
if (n->rif.ip == 0) {
tester_done(&sim->tester, sim->now, false);
return;
}
mg_snprintf(url, sizeof(url), "http://%M/", mg_print_ip4, &n->rif.ip);
if ((sim->tester.c = mg_http_connect(&sim->tester.mgr, url, tester_fn,
&sim->tester)) == NULL) {
tester_done(&sim->tester, sim->now, false);
}
}
// Set up sim: defaults, LAN stations
static void init(struct simulator *sim) {
static const uint8_t dmac[6] = {2, 1, 0, 0, 0, 0}, // DHCP server MAC
tmac[6] = {2, 2, 0, 0, 0, 0}; // Test station MAC
memset(sim, 0, sizeof(*sim));
sim->area.x = sim->area.y = 100;
sim->duration = 600 * 1000000ULL, sim->tester.wait = 60 * 1000000ULL;
sim->request_timeout_ms = 10000;
memcpy(sim->dhcp.lif.mac, dmac, 6), sim->dhcp.lif.driver = &s_lan_driver;
mg_queue_init(&sim->dhcp.lif.recv_queue, (char *) calloc(1, LAN_QUEUE_SIZE),
LAN_QUEUE_SIZE);
memcpy(sim->tester.lif.mac, tmac, 6);
sim->tester.lif.ip = MG_IPV4(10, 0, 0, 2),
sim->tester.lif.mask = MG_IPV4(255, 0, 0, 0);
sim->tester.lif.driver = &s_lan_driver,
sim->tester.lif.enable_mac_check = true;
sim->tester.lif.recv_queue.size = 128 * 1024;
}
// Radio driver, all radio interfaces share it. An ideal radio for now:
// whole Ethernet frames, no range, no loss, no airtime. It works like the
// LAN, see lan_tx(): a frame goes into the receive queues of all other
// radio interfaces if broadcast, else of the one with the destination MAC.
// Bridges get every frame: some are for the LAN. Who forwards what, and
// loops with several bridges, are for mg_mesh
static size_t radio_tx(const void *buf, size_t len, struct mg_tcpip_if *ifp) {
const uint8_t *dst = (const uint8_t *) buf;
bool all = dst[0] & 1; // Broadcast, multicast
struct bridge *b;
struct radio_node *n;
for (b = s_sim.bridges; b != NULL; b = b->next) {
lan_put(&b->rif, ifp, buf, len);
}
for (n = s_sim.nodes; n != NULL; n = n->next) {
if (all || memcmp(dst, n->rif.mac, 6) == 0) {
lan_put(&n->rif, ifp, buf, len);
}
}
return len;
}
static bool radio_init(struct mg_tcpip_if *ifp) {
(void) ifp;
return true;
}
static bool radio_link(struct mg_tcpip_if *ifp, bool s1) {
(void) ifp, (void) s1;
return true;
}
static struct mg_tcpip_driver s_radio_driver = {radio_init, radio_tx, NULL,
radio_link};
// A node's HTTP server: the test station requests GET /
static void node_http_ev_handler(struct mg_connection *c, int ev,
void *ev_data) {
if (ev == MG_EV_HTTP_MSG) {
mg_http_reply(c, 200, "", "Hi from Mongoose, my MAC is %M\n", mg_print_mac,
c->fn_data);
}
(void) ev_data;
}
// Start each node's Mongoose: its radio interface, no IP address, so DHCP,
// and an HTTP server
static void nodes_init(struct simulator *sim) {
struct radio_node *n;
for (n = sim->nodes; n != NULL; n = n->next) {
n->rif.driver = &s_radio_driver;
n->rif.recv_queue.size = LAN_QUEUE_SIZE;
mg_mgr_init(&n->mgr);
mg_tcpip_init(&n->mgr, &n->rif);
mg_http_listen(&n->mgr, "http://0.0.0.0:80", node_http_ev_handler,
n->rif.mac);
}
}
// Start each bridge: its LAN and radio drivers. No Mongoose, no IP
static void bridges_init(struct simulator *sim) {
struct bridge *b;
for (b = sim->bridges; b != NULL; b = b->next) {
b->lif.driver = &s_lan_driver, b->rif.driver = &s_radio_driver;
mg_queue_init(&b->lif.recv_queue, (char *) calloc(1, LAN_QUEUE_SIZE),
LAN_QUEUE_SIZE);
mg_queue_init(&b->rif.recv_queue, (char *) calloc(1, LAN_QUEUE_SIZE),
LAN_QUEUE_SIZE);
b->lif.driver->init(&b->lif), b->rif.driver->init(&b->rif);
}
}
// Move the frames waiting in the receive queue of interface from out of
// interface to
static void bridge_forward(struct mg_tcpip_if *from, struct mg_tcpip_if *to) {
char *buf;
size_t len;
while ((len = mg_queue_next(&from->recv_queue, &buf)) > 0) {
// MG_DEBUG(("Bridging ETH frame, size %zu", len));
to->driver->tx(buf, len, to);
mg_queue_del(&from->recv_queue, len);
}
}
// Bridges' work: forward every frame, LAN to radio and radio to LAN. No
// filtering yet
static void bridges_poll(struct simulator *sim) {
struct bridge *b;
for (b = sim->bridges; b != NULL; b = b->next) {
bridge_forward(&b->lif, &b->rif);
bridge_forward(&b->rif, &b->lif);
}
}
// Poll each node's Mongoose
static void nodes_poll(struct simulator *sim) {
struct radio_node *n;
for (n = sim->nodes; n != NULL; n = n->next) mg_mgr_poll(&n->mgr, 0);
}
int main(int argc, char *argv[]) {
clock_t started; // CPU time, to show how fast the simulation runs
int i;
init(&s_sim);
for (i = 1; i < argc; i++) {
if (i + 1 >= argc) {
usage(&s_sim, argv[0]);
} else if (strcmp(argv[i], "-area") == 0) {
if (!parse_pos(argv[++i], &s_sim.area)) usage(&s_sim, argv[0]);
} else if (strcmp(argv[i], "-bridge") == 0) {
if (!add_bridge(&s_sim, argv[++i])) usage(&s_sim, argv[0]);
} else if (strcmp(argv[i], "-node") == 0) {
if (!add_node(&s_sim, argv[++i])) usage(&s_sim, argv[0]);
} else if (strcmp(argv[i], "-t") == 0) {
s_sim.duration = (uint64_t) (atof(argv[++i]) * 1e6);
} else if (strcmp(argv[i], "-w") == 0) {
s_sim.tester.wait = (uint64_t) (atof(argv[++i]) * 1e6);
} else {
usage(&s_sim, argv[0]);
}
}
printf("Area %gx%gx%g m, radio %s, mesh %s, bridges: %d, nodes: %d\n",
s_sim.area.x, s_sim.area.y, s_sim.area.z, mg_radio_802154.name,
mg_mesh_star.name, s_sim.num_bridges, s_sim.num_nodes);
check_area(&s_sim);
// mg_log_set(MG_LL_INFO);
mg_mgr_init(&s_sim.tester.mgr);
mg_tcpip_init(&s_sim.tester.mgr, &s_sim.tester.lif);
s_sim.tester.next = s_sim.nodes;
bridges_init(&s_sim);
nodes_init(&s_sim);
// Main loop: STEP_MICROSECONDS of virtual time per step
started = clock();
while (s_sim.now < s_sim.duration) {
dhcp_poll(&s_sim);
tester_poll(&s_sim);
bridges_poll(&s_sim);
nodes_poll(&s_sim);
s_sim.now += STEP_MICROSECONDS;
}
printf("Requests: %d ok, %d failed, of %d nodes", s_sim.tester.ok,
s_sim.tester.failed, s_sim.num_nodes);
if (s_sim.tester.ok > 0) {
printf("; ms: min %.1f, mean %.1f, max %.1f", s_sim.tester.min,
s_sim.tester.sum / s_sim.tester.ok, s_sim.tester.max);
}
printf("\nSimulated %g s in %.3f s\n", (double) s_sim.now / 1e6,
(double) (clock() - started) / CLOCKS_PER_SEC);
return 0;
}
+67
View File
@@ -0,0 +1,67 @@
// Copyright (c) 2026 Cesanta Software Limited
// All rights reserved
#pragma once
#define MG_ENABLE_TCPIP 1
#include "l2_802154.h"
#include "l2_mesh.h"
#include "mongoose.h"
#include "star.h"
// A position in metres
struct pos {
double x, y, z; // Coordinates
};
// A bridge: forwards frames between the LAN and the radio, no IP stack
struct bridge {
struct bridge *next; // Next in simulator::bridges
struct pos pos; // Where it is
struct mg_tcpip_if lif; // LAN side: LAN driver, gets every LAN frame
struct mg_tcpip_if rif; // Radio side: radio driver, gets every frame
};
// A radio node: Mongoose on a radio interface
struct radio_node {
struct radio_node *next; // Next in simulator::nodes
struct pos pos; // Where it is
struct mg_mgr mgr; // Mongoose event manager
struct mg_tcpip_if rif; // Radio interface. rif.ip from DHCP, 0: none yet
};
struct simulator;
// LAN station: a DHCP server, 10.0.0.1. Gives each client an address made
// from its MAC, see node_ip()
struct lan_dhcp_server {
struct mg_tcpip_if lif; // Its LAN interface, no Mongoose on it
};
// LAN station: the test station, 10.0.0.2, a Mongoose instance. Waits for
// the mesh to settle, then sends GET / to each node
struct lan_tester {
struct mg_mgr mgr; // Its Mongoose
struct mg_tcpip_if lif; // Its LAN interface
uint64_t wait; // Mesh settle time: start requests then, us
struct radio_node *next; // Node to request next, NULL: all done
struct mg_connection *c; // Request on the way, NULL: none
uint64_t start; // When it started, us
int ok, failed; // Requests done, by result
double min, max, sum; // Times of successful requests, ms
};
// Simulator state
struct simulator {
struct pos area; // A box: all positions inside
struct bridge *bridges; // Bridges, a list
struct radio_node *nodes; // Radio nodes, a list
int num_bridges, num_nodes; // How many
uint64_t now; // Virtual time, us since the start
uint64_t duration; // Run this long, us
uint32_t last_mac; // Low 3 bytes of the last MAC given
int request_timeout_ms; // A request that takes longer failed
struct lan_dhcp_server dhcp; // DHCP server on the LAN
struct lan_tester tester; // Test station on the LAN
};
+6
View File
@@ -0,0 +1,6 @@
// Copyright (c) 2026 Cesanta Software Limited
// All rights reserved
#include "star.h"
const struct mg_mesh mg_mesh_star = {"star"};
+10
View File
@@ -0,0 +1,10 @@
// Copyright (c) 2026 Cesanta Software Limited
// All rights reserved
//
// Star mesh: one hop, nodes talk to a bridge
#pragma once
#include "l2_mesh.h"
extern const struct mg_mesh mg_mesh_star;
+4
View File
@@ -80,6 +80,10 @@ static void test_statechange(void) {
onstatechange(&iface);
ASSERT(executed == true);
executed = false;
iface.enable_dhcp_client = true; // DHCP REQUEST unanswered: start over
iface.state = MG_TCPIP_STATE_REQ, iface.lease_expire = 0;
mg_ip_poll(&iface, true);
ASSERT(iface.state == MG_TCPIP_STATE_UP);
}
#if MG_ENABLE_IPV6
static void mif6_fn(struct mg_tcpip_if *ifp, int ev, void *ev_data) {
+1 -3
View File
@@ -70,10 +70,8 @@ static void http_ev_handler(struct mg_connection *c, int ev, void *ev_data) {
}
}
extern void wifi_init(const char *ssid, const char *pass);
void app_main(void) {
MG_HEALTH_INIT();
void wifi_init(const char *ssid, const char *pass);
wifi_init(WIFI_SSID, WIFI_PASS);
struct mg_mgr mgr;
@@ -1,3 +1 @@
#define MG_ARCH MG_ARCH_ESP32
#define MG_ENABLE_HEALTH 1