mirror of
https://github.com/cesanta/mongoose.git
synced 2026-11-01 13:10:43 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6df410570 |
No files matched your search
@@ -637,6 +637,7 @@ jobs:
|
|||||||
example:
|
example:
|
||||||
- path: micropython/esp32
|
- path: micropython/esp32
|
||||||
- path: esp32/uart-bridge
|
- path: esp32/uart-bridge
|
||||||
|
- path: esp32/minimal
|
||||||
name: ${{ matrix.example.path }}
|
name: ${{ matrix.example.path }}
|
||||||
env:
|
env:
|
||||||
GO: 0
|
GO: 0
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
example:
|
example:
|
||||||
- path: esp32/uart-bridge
|
- path: esp32/uart-bridge
|
||||||
|
- path: esp32/minimal
|
||||||
name: ${{ matrix.example.path }}
|
name: ${{ matrix.example.path }}
|
||||||
env:
|
env:
|
||||||
GO: 0
|
GO: 0
|
||||||
|
|||||||
+50
-41
@@ -3626,12 +3626,13 @@ 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
|
// 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
|
// MG_HEALTH_RAM and the .mg_health region in the linker script
|
||||||
struct mg_health mg_health_record MG_HEALTH_RAM;
|
struct mg_health mg_health_record MG_HEALTH_RAM;
|
||||||
|
|
||||||
// STM32 H723 support. Needs the _estack symbol from the linker script
|
// STM32 H7, H5, C5 support. Needs the _estack symbol from the linker script
|
||||||
#if MG_ENABLE_HEALTH && (defined(STM32H7_DEV_ID) || defined(STM32H5) || defined(STM32C5))
|
#if defined(STM32H7_DEV_ID) || defined(STM32H5) || defined(STM32C5)
|
||||||
// Fault handler body. Runs in exception context: no printf, no malloc, no
|
// Fault handler body. Runs in exception context: no printf, no malloc, no
|
||||||
// blocking calls. Copies the unwind registers and raw stack into the health
|
// blocking calls. Copies the unwind registers and raw stack into the health
|
||||||
// record, then resets.
|
// record, then resets.
|
||||||
@@ -3673,52 +3674,57 @@ void BusFault_Handler(void) __attribute__((alias("HardFault_Handler")));
|
|||||||
void UsageFault_Handler(void) __attribute__((alias("HardFault_Handler")));
|
void UsageFault_Handler(void) __attribute__((alias("HardFault_Handler")));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Start a new boot: validate the record, count the boot, store the reset
|
// Start a new boot: validate the record. Called by MG_HEALTH_INIT()
|
||||||
// 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) {
|
void mg_health_init(void) {
|
||||||
char magic[] = MG_HEALTH_MAGIC;
|
char magic[] = MG_HEALTH_MAGIC;
|
||||||
// struct mg_health *h = mg_health_get();
|
|
||||||
if (!mg_health_valid()) { // First boot, or RAM lost its contents
|
if (!mg_health_valid()) { // First boot, or RAM lost its contents
|
||||||
memset(&mg_health_record, 0, sizeof(mg_health_record));
|
memset(&mg_health_record, 0, sizeof(mg_health_record));
|
||||||
memcpy(mg_health_record.magic, magic, sizeof(mg_health_record.magic));
|
memcpy(mg_health_record.magic, magic, sizeof(mg_health_record.magic));
|
||||||
}
|
}
|
||||||
|
#if MG_ARCH == MG_ARCH_ESP32
|
||||||
#if 0 && MG_ARCH == MG_ARCH_ESP32 && CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH
|
#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
|
||||||
// After a crash, copy the backtrace from the core dump in flash: the same
|
// 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 ...".
|
// addresses the panic handler prints as "Backtrace: PC:SP PC:SP ...".
|
||||||
// The core dump stays in flash, so check the reset reason, otherwise an
|
// The core dump stays in flash, so check the reset reason, otherwise an
|
||||||
// old crash is reported on every boot
|
// old crash is reported on every boot
|
||||||
static esp_core_dump_summary_t s; // Big on RISC-V, keep it off the stack
|
esp_core_dump_summary_t *s = calloc(1, sizeof(*s));
|
||||||
esp_reset_reason_t rr = esp_reset_reason();
|
|
||||||
uint32_t *out = mg_health_record.data;
|
uint32_t *out = mg_health_record.data;
|
||||||
size_t n = 0, max = MG_HEALTH_DATA_SIZE;
|
size_t n = 0;
|
||||||
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));
|
memset(out, 0, sizeof(mg_health_record.data));
|
||||||
out[n++] = s.exc_pc;
|
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;
|
||||||
#if CONFIG_IDF_TARGET_ARCH_XTENSA
|
#if CONFIG_IDF_TARGET_ARCH_XTENSA
|
||||||
for (uint32_t i = 0; i < s.exc_bt_info.depth && n < max; i++) {
|
out[n++] = s->ex_info.exc_a[0];
|
||||||
if (s.exc_bt_info.bt[i] != s.exc_pc) out[n++] = s.exc_bt_info.bt[i];
|
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)
|
||||||
#elif CONFIG_IDF_TARGET_ARCH_RISCV
|
#elif CONFIG_IDF_TARGET_ARCH_RISCV
|
||||||
// No unwinder: take the return address, then words on the stack that
|
out[n++] = s->ex_info.exc_ra;
|
||||||
// point into executable memory (a heuristic, may include stale values)
|
out[n++] = s->ex_info.exc_sp;
|
||||||
if (n < max) out[n++] = s.ex_info.ra;
|
size_t len = s->exc_bt_info.dump_size / 4 ; // sizeof(uint32_t)
|
||||||
for (uint32_t i = 0; i + 4 <= s.exc_bt_info.dump_size && n < max; i += 4) {
|
if (len > MG_HEALTH_DATA_SIZE - n) len = (MG_HEALTH_DATA_SIZE - n);
|
||||||
uint32_t w;
|
memcpy(&out[n], s->exc_bt_info.stackdump, 4 * len);
|
||||||
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
|
#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) {
|
static size_t print_uint32(void (*fn)(char, void *), void *arg, va_list *ap) {
|
||||||
@@ -3732,11 +3738,17 @@ 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 mg_print_crash_record(void (*fn)(char, void *), void *arg, va_list *ap) {
|
||||||
size_t len = 0;
|
size_t len = 0;
|
||||||
if (mg_health_valid() &&
|
if (mg_health_valid() &&
|
||||||
mg_health_record.reset_reason == MG_HEALTH_RESET_FAULT) {
|
(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
|
||||||
|
)) {
|
||||||
size_t n = sizeof(mg_health_record.data) / sizeof(mg_health_record.data[0]);
|
size_t n = sizeof(mg_health_record.data) / sizeof(mg_health_record.data[0]);
|
||||||
len += mg_xprintf(fn, arg, "{%m:%hhu,%m:%u,%m:[%M]}", //
|
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("version"), mg_health_record.magic[3], //
|
||||||
MG_ESC("cpuid"), mg_health_record.cpuid, //
|
MG_ESC("arch"), mg_health_record.arch, //
|
||||||
|
MG_ESC("reason"), mg_health_record.reset_reason, //
|
||||||
|
MG_ESC("cpuid"), mg_health_record.cpuid, //
|
||||||
MG_ESC("data"), print_uint32, n, mg_health_record.data);
|
MG_ESC("data"), print_uint32, n, mg_health_record.data);
|
||||||
} else {
|
} else {
|
||||||
len += mg_xprintf(fn, arg, "{}");
|
len += mg_xprintf(fn, arg, "{}");
|
||||||
@@ -3744,6 +3756,7 @@ size_t mg_print_crash_record(void (*fn)(char, void *), void *arg, va_list *ap) {
|
|||||||
(void) ap;
|
(void) ap;
|
||||||
return len;
|
return len;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef MG_ENABLE_LINES
|
#ifdef MG_ENABLE_LINES
|
||||||
#line 1 "src/http.c"
|
#line 1 "src/http.c"
|
||||||
@@ -9399,7 +9412,6 @@ static void rx_dhcp_client(struct mg_tcpip_if *ifp, struct pkt *pkt) {
|
|||||||
: pkt->dhcp->siaddr ? pkt->dhcp->siaddr
|
: pkt->dhcp->siaddr ? pkt->dhcp->siaddr
|
||||||
: pkt->ip->src);
|
: pkt->ip->src);
|
||||||
ifp->state = MG_TCPIP_STATE_REQ; // REQUESTING state
|
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
|
} else if (msgtype == 5) { // DHCPACK
|
||||||
if (ifp->state == MG_TCPIP_STATE_REQ && ip && gw && lease) { // got an IP
|
if (ifp->state == MG_TCPIP_STATE_REQ && ip && gw && lease) { // got an IP
|
||||||
uint64_t rand;
|
uint64_t rand;
|
||||||
@@ -10707,9 +10719,6 @@ static void mg_ip_poll(struct mg_tcpip_if *ifp, bool s1) {
|
|||||||
if (ifp->enable_dhcp_client && s1) {
|
if (ifp->enable_dhcp_client && s1) {
|
||||||
if (ifp->state == MG_TCPIP_STATE_UP) {
|
if (ifp->state == MG_TCPIP_STATE_UP) {
|
||||||
tx_dhcp_discover(ifp); // INIT (4.4.1)
|
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 &&
|
} else if (ifp->state == MG_TCPIP_STATE_READY &&
|
||||||
ifp->lease_expire > 0) { // BOUND / RENEWING / REBINDING
|
ifp->lease_expire > 0) { // BOUND / RENEWING / REBINDING
|
||||||
if (ifp->now >= ifp->lease_expire) {
|
if (ifp->now >= ifp->lease_expire) {
|
||||||
|
|||||||
+69
-7
@@ -253,8 +253,10 @@ extern "C" {
|
|||||||
#include <esp_system.h>
|
#include <esp_system.h>
|
||||||
#include <esp_mac.h>
|
#include <esp_mac.h>
|
||||||
#include <esp_idf_version.h>
|
#include <esp_idf_version.h>
|
||||||
// #include <esp_memory_utils.h> // For health record coredumps
|
#if MG_ENABLE_HEALTH
|
||||||
// #include <esp_core_dump.h> // For health record coredumps
|
#include <esp_memory_utils.h>
|
||||||
|
#include <esp_core_dump.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#define MG_PATH_MAX 128
|
#define MG_PATH_MAX 128
|
||||||
|
|
||||||
@@ -4373,6 +4375,8 @@ void mg_ota_poll(struct mg_mgr *);
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#if MG_ENABLE_HEALTH
|
||||||
|
|
||||||
#ifndef MG_HEALTH_MAGIC
|
#ifndef MG_HEALTH_MAGIC
|
||||||
#define MG_HEALTH_MAGIC {'M', 'G', 'H', '3'} // '3' is the layout version
|
#define MG_HEALTH_MAGIC {'M', 'G', 'H', '3'} // '3' is the layout version
|
||||||
#endif
|
#endif
|
||||||
@@ -4381,13 +4385,15 @@ void mg_ota_poll(struct mg_mgr *);
|
|||||||
#define MG_HEALTH_RAM __attribute__((section(".mg_health")))
|
#define MG_HEALTH_RAM __attribute__((section(".mg_health")))
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if !defined(MG_HEALTH_DATA_SIZE) && (defined(STM32H7_DEV_ID) || defined(STM32H5) || defined(STM32C5))
|
|
||||||
#define MG_HEALTH_DATA_SIZE 250
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef MG_HEALTH_DATA_SIZE
|
#ifndef MG_HEALTH_DATA_SIZE
|
||||||
|
#if (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
|
||||||
#define MG_HEALTH_DATA_SIZE 10
|
#define MG_HEALTH_DATA_SIZE 10
|
||||||
#endif
|
#endif
|
||||||
|
#endif // MG_HEALTH_DATA_SIZE
|
||||||
|
|
||||||
// Places the record in RAM that survives a reset. Define in mongoose_config.h
|
// 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")))
|
// to match the linker script, e.g. __attribute__((section(".mg_health")))
|
||||||
@@ -4395,6 +4401,13 @@ void mg_ota_poll(struct mg_mgr *);
|
|||||||
#define MG_HEALTH_RAM
|
#define MG_HEALTH_RAM
|
||||||
#endif
|
#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,
|
// 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
|
// so these are ordered by how much they tell you: the most specific cause a
|
||||||
// device reports wins
|
// device reports wins
|
||||||
@@ -4411,7 +4424,8 @@ enum mg_health_reason {
|
|||||||
|
|
||||||
struct mg_health {
|
struct mg_health {
|
||||||
char magic[4]; // MG_HEALTH_MAGIC when the record holds valid data
|
char magic[4]; // MG_HEALTH_MAGIC when the record holds valid data
|
||||||
uint32_t reset_reason; // enum mg_health_reason that started this boot
|
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 cpuid; // ARM CPU ID
|
uint32_t cpuid; // ARM CPU ID
|
||||||
uint32_t data[MG_HEALTH_DATA_SIZE];
|
uint32_t data[MG_HEALTH_DATA_SIZE];
|
||||||
};
|
};
|
||||||
@@ -4444,9 +4458,57 @@ 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()
|
#define MG_HEALTH_INIT() mg_health_init()
|
||||||
extern size_t mg_print_crash_record(mg_pfn_t, void *, va_list *);
|
extern size_t mg_print_crash_record(mg_pfn_t, void *, va_list *);
|
||||||
extern void mg_health_init(void);
|
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
|
// Cloud connector API to https://mdash.net - device management cloud
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -22,8 +22,10 @@
|
|||||||
#include <esp_system.h>
|
#include <esp_system.h>
|
||||||
#include <esp_mac.h>
|
#include <esp_mac.h>
|
||||||
#include <esp_idf_version.h>
|
#include <esp_idf_version.h>
|
||||||
// #include <esp_memory_utils.h> // For health record coredumps
|
#if MG_ENABLE_HEALTH
|
||||||
// #include <esp_core_dump.h> // For health record coredumps
|
#include <esp_memory_utils.h>
|
||||||
|
#include <esp_core_dump.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#define MG_PATH_MAX 128
|
#define MG_PATH_MAX 128
|
||||||
|
|
||||||
|
|||||||
+50
-37
@@ -1,12 +1,13 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "health.h"
|
#include "health.h"
|
||||||
|
|
||||||
|
#if MG_ENABLE_HEALTH
|
||||||
// The one health record. Lives in RAM that survives a warm reset, see
|
// 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
|
// MG_HEALTH_RAM and the .mg_health region in the linker script
|
||||||
struct mg_health mg_health_record MG_HEALTH_RAM;
|
struct mg_health mg_health_record MG_HEALTH_RAM;
|
||||||
|
|
||||||
// STM32 H723 support. Needs the _estack symbol from the linker script
|
// STM32 H7, H5, C5 support. Needs the _estack symbol from the linker script
|
||||||
#if MG_ENABLE_HEALTH && (defined(STM32H7_DEV_ID) || defined(STM32H5) || defined(STM32C5))
|
#if defined(STM32H7_DEV_ID) || defined(STM32H5) || defined(STM32C5)
|
||||||
// Fault handler body. Runs in exception context: no printf, no malloc, no
|
// Fault handler body. Runs in exception context: no printf, no malloc, no
|
||||||
// blocking calls. Copies the unwind registers and raw stack into the health
|
// blocking calls. Copies the unwind registers and raw stack into the health
|
||||||
// record, then resets.
|
// record, then resets.
|
||||||
@@ -48,52 +49,57 @@ void BusFault_Handler(void) __attribute__((alias("HardFault_Handler")));
|
|||||||
void UsageFault_Handler(void) __attribute__((alias("HardFault_Handler")));
|
void UsageFault_Handler(void) __attribute__((alias("HardFault_Handler")));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Start a new boot: validate the record, count the boot, store the reset
|
// Start a new boot: validate the record. Called by MG_HEALTH_INIT()
|
||||||
// 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) {
|
void mg_health_init(void) {
|
||||||
char magic[] = MG_HEALTH_MAGIC;
|
char magic[] = MG_HEALTH_MAGIC;
|
||||||
// struct mg_health *h = mg_health_get();
|
|
||||||
if (!mg_health_valid()) { // First boot, or RAM lost its contents
|
if (!mg_health_valid()) { // First boot, or RAM lost its contents
|
||||||
memset(&mg_health_record, 0, sizeof(mg_health_record));
|
memset(&mg_health_record, 0, sizeof(mg_health_record));
|
||||||
memcpy(mg_health_record.magic, magic, sizeof(mg_health_record.magic));
|
memcpy(mg_health_record.magic, magic, sizeof(mg_health_record.magic));
|
||||||
}
|
}
|
||||||
|
#if MG_ARCH == MG_ARCH_ESP32
|
||||||
#if 0 && MG_ARCH == MG_ARCH_ESP32 && CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH
|
#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
|
||||||
// After a crash, copy the backtrace from the core dump in flash: the same
|
// 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 ...".
|
// addresses the panic handler prints as "Backtrace: PC:SP PC:SP ...".
|
||||||
// The core dump stays in flash, so check the reset reason, otherwise an
|
// The core dump stays in flash, so check the reset reason, otherwise an
|
||||||
// old crash is reported on every boot
|
// old crash is reported on every boot
|
||||||
static esp_core_dump_summary_t s; // Big on RISC-V, keep it off the stack
|
esp_core_dump_summary_t *s = calloc(1, sizeof(*s));
|
||||||
esp_reset_reason_t rr = esp_reset_reason();
|
|
||||||
uint32_t *out = mg_health_record.data;
|
uint32_t *out = mg_health_record.data;
|
||||||
size_t n = 0, max = MG_HEALTH_DATA_SIZE;
|
size_t n = 0;
|
||||||
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));
|
memset(out, 0, sizeof(mg_health_record.data));
|
||||||
out[n++] = s.exc_pc;
|
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;
|
||||||
#if CONFIG_IDF_TARGET_ARCH_XTENSA
|
#if CONFIG_IDF_TARGET_ARCH_XTENSA
|
||||||
for (uint32_t i = 0; i < s.exc_bt_info.depth && n < max; i++) {
|
out[n++] = s->ex_info.exc_a[0];
|
||||||
if (s.exc_bt_info.bt[i] != s.exc_pc) out[n++] = s.exc_bt_info.bt[i];
|
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)
|
||||||
#elif CONFIG_IDF_TARGET_ARCH_RISCV
|
#elif CONFIG_IDF_TARGET_ARCH_RISCV
|
||||||
// No unwinder: take the return address, then words on the stack that
|
out[n++] = s->ex_info.exc_ra;
|
||||||
// point into executable memory (a heuristic, may include stale values)
|
out[n++] = s->ex_info.exc_sp;
|
||||||
if (n < max) out[n++] = s.ex_info.ra;
|
size_t len = s->exc_bt_info.dump_size / 4 ; // sizeof(uint32_t)
|
||||||
for (uint32_t i = 0; i + 4 <= s.exc_bt_info.dump_size && n < max; i += 4) {
|
if (len > MG_HEALTH_DATA_SIZE - n) len = (MG_HEALTH_DATA_SIZE - n);
|
||||||
uint32_t w;
|
memcpy(&out[n], s->exc_bt_info.stackdump, 4 * len);
|
||||||
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
|
#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) {
|
static size_t print_uint32(void (*fn)(char, void *), void *arg, va_list *ap) {
|
||||||
@@ -107,11 +113,17 @@ 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 mg_print_crash_record(void (*fn)(char, void *), void *arg, va_list *ap) {
|
||||||
size_t len = 0;
|
size_t len = 0;
|
||||||
if (mg_health_valid() &&
|
if (mg_health_valid() &&
|
||||||
mg_health_record.reset_reason == MG_HEALTH_RESET_FAULT) {
|
(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
|
||||||
|
)) {
|
||||||
size_t n = sizeof(mg_health_record.data) / sizeof(mg_health_record.data[0]);
|
size_t n = sizeof(mg_health_record.data) / sizeof(mg_health_record.data[0]);
|
||||||
len += mg_xprintf(fn, arg, "{%m:%hhu,%m:%u,%m:[%M]}", //
|
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("version"), mg_health_record.magic[3], //
|
||||||
MG_ESC("cpuid"), mg_health_record.cpuid, //
|
MG_ESC("arch"), mg_health_record.arch, //
|
||||||
|
MG_ESC("reason"), mg_health_record.reset_reason, //
|
||||||
|
MG_ESC("cpuid"), mg_health_record.cpuid, //
|
||||||
MG_ESC("data"), print_uint32, n, mg_health_record.data);
|
MG_ESC("data"), print_uint32, n, mg_health_record.data);
|
||||||
} else {
|
} else {
|
||||||
len += mg_xprintf(fn, arg, "{}");
|
len += mg_xprintf(fn, arg, "{}");
|
||||||
@@ -119,3 +131,4 @@ size_t mg_print_crash_record(void (*fn)(char, void *), void *arg, va_list *ap) {
|
|||||||
(void) ap;
|
(void) ap;
|
||||||
return len;
|
return len;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
+65
-5
@@ -5,6 +5,8 @@
|
|||||||
#include "arch.h"
|
#include "arch.h"
|
||||||
#include "printf.h"
|
#include "printf.h"
|
||||||
|
|
||||||
|
#if MG_ENABLE_HEALTH
|
||||||
|
|
||||||
#ifndef MG_HEALTH_MAGIC
|
#ifndef MG_HEALTH_MAGIC
|
||||||
#define MG_HEALTH_MAGIC {'M', 'G', 'H', '3'} // '3' is the layout version
|
#define MG_HEALTH_MAGIC {'M', 'G', 'H', '3'} // '3' is the layout version
|
||||||
#endif
|
#endif
|
||||||
@@ -13,13 +15,15 @@
|
|||||||
#define MG_HEALTH_RAM __attribute__((section(".mg_health")))
|
#define MG_HEALTH_RAM __attribute__((section(".mg_health")))
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if !defined(MG_HEALTH_DATA_SIZE) && (defined(STM32H7_DEV_ID) || defined(STM32H5) || defined(STM32C5))
|
|
||||||
#define MG_HEALTH_DATA_SIZE 250
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef MG_HEALTH_DATA_SIZE
|
#ifndef MG_HEALTH_DATA_SIZE
|
||||||
|
#if (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
|
||||||
#define MG_HEALTH_DATA_SIZE 10
|
#define MG_HEALTH_DATA_SIZE 10
|
||||||
#endif
|
#endif
|
||||||
|
#endif // MG_HEALTH_DATA_SIZE
|
||||||
|
|
||||||
// Places the record in RAM that survives a reset. Define in mongoose_config.h
|
// 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")))
|
// to match the linker script, e.g. __attribute__((section(".mg_health")))
|
||||||
@@ -27,6 +31,13 @@
|
|||||||
#define MG_HEALTH_RAM
|
#define MG_HEALTH_RAM
|
||||||
#endif
|
#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,
|
// 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
|
// so these are ordered by how much they tell you: the most specific cause a
|
||||||
// device reports wins
|
// device reports wins
|
||||||
@@ -43,7 +54,8 @@ enum mg_health_reason {
|
|||||||
|
|
||||||
struct mg_health {
|
struct mg_health {
|
||||||
char magic[4]; // MG_HEALTH_MAGIC when the record holds valid data
|
char magic[4]; // MG_HEALTH_MAGIC when the record holds valid data
|
||||||
uint32_t reset_reason; // enum mg_health_reason that started this boot
|
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 cpuid; // ARM CPU ID
|
uint32_t cpuid; // ARM CPU ID
|
||||||
uint32_t data[MG_HEALTH_DATA_SIZE];
|
uint32_t data[MG_HEALTH_DATA_SIZE];
|
||||||
};
|
};
|
||||||
@@ -76,6 +88,54 @@ 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()
|
#define MG_HEALTH_INIT() mg_health_init()
|
||||||
extern size_t mg_print_crash_record(mg_pfn_t, void *, va_list *);
|
extern size_t mg_print_crash_record(mg_pfn_t, void *, va_list *);
|
||||||
extern void mg_health_init(void);
|
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
|
||||||
@@ -734,7 +734,6 @@ static void rx_dhcp_client(struct mg_tcpip_if *ifp, struct pkt *pkt) {
|
|||||||
: pkt->dhcp->siaddr ? pkt->dhcp->siaddr
|
: pkt->dhcp->siaddr ? pkt->dhcp->siaddr
|
||||||
: pkt->ip->src);
|
: pkt->ip->src);
|
||||||
ifp->state = MG_TCPIP_STATE_REQ; // REQUESTING state
|
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
|
} else if (msgtype == 5) { // DHCPACK
|
||||||
if (ifp->state == MG_TCPIP_STATE_REQ && ip && gw && lease) { // got an IP
|
if (ifp->state == MG_TCPIP_STATE_REQ && ip && gw && lease) { // got an IP
|
||||||
uint64_t rand;
|
uint64_t rand;
|
||||||
@@ -2042,9 +2041,6 @@ static void mg_ip_poll(struct mg_tcpip_if *ifp, bool s1) {
|
|||||||
if (ifp->enable_dhcp_client && s1) {
|
if (ifp->enable_dhcp_client && s1) {
|
||||||
if (ifp->state == MG_TCPIP_STATE_UP) {
|
if (ifp->state == MG_TCPIP_STATE_UP) {
|
||||||
tx_dhcp_discover(ifp); // INIT (4.4.1)
|
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 &&
|
} else if (ifp->state == MG_TCPIP_STATE_READY &&
|
||||||
ifp->lease_expire > 0) { // BOUND / RENEWING / REBINDING
|
ifp->lease_expire > 0) { // BOUND / RENEWING / REBINDING
|
||||||
if (ifp->now >= ifp->lease_expire) {
|
if (ifp->now >= ifp->lease_expire) {
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Copyright (c) 2026 Cesanta Software Limited
|
|
||||||
// All rights reserved
|
|
||||||
|
|
||||||
#include "l2_802154.h"
|
|
||||||
|
|
||||||
const struct mg_radio mg_radio_802154 = {"802.15.4"};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
// 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;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
// Copyright (c) 2026 Cesanta Software Limited
|
|
||||||
// All rights reserved
|
|
||||||
|
|
||||||
#include "l2_mesh.h"
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// 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
|
|
||||||
};
|
|
||||||
@@ -1,433 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
// 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
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Copyright (c) 2026 Cesanta Software Limited
|
|
||||||
// All rights reserved
|
|
||||||
|
|
||||||
#include "star.h"
|
|
||||||
|
|
||||||
const struct mg_mesh mg_mesh_star = {"star"};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
// 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;
|
|
||||||
@@ -80,10 +80,6 @@ static void test_statechange(void) {
|
|||||||
onstatechange(&iface);
|
onstatechange(&iface);
|
||||||
ASSERT(executed == true);
|
ASSERT(executed == true);
|
||||||
executed = false;
|
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
|
#if MG_ENABLE_IPV6
|
||||||
static void mif6_fn(struct mg_tcpip_if *ifp, int ev, void *ev_data) {
|
static void mif6_fn(struct mg_tcpip_if *ifp, int ev, void *ev_data) {
|
||||||
|
|||||||
@@ -70,8 +70,10 @@ 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) {
|
void app_main(void) {
|
||||||
void wifi_init(const char *ssid, const char *pass);
|
MG_HEALTH_INIT();
|
||||||
wifi_init(WIFI_SSID, WIFI_PASS);
|
wifi_init(WIFI_SSID, WIFI_PASS);
|
||||||
|
|
||||||
struct mg_mgr mgr;
|
struct mg_mgr mgr;
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
#define MG_ARCH MG_ARCH_ESP32
|
#define MG_ARCH MG_ARCH_ESP32
|
||||||
|
|
||||||
|
#define MG_ENABLE_HEALTH 1
|
||||||
Reference in new issue
Block a user