From 1e99974b831f1f4a337127e6fe0489aa9fe3adf5 Mon Sep 17 00:00:00 2001 From: cpq Date: Thu, 20 Aug 2026 09:45:35 +0100 Subject: [PATCH] health record support --- mongoose.c | 187 ++++++++++++++++++ mongoose.h | 146 ++++++++++++++ .../downloads/integrate_mongoose_cubemx.sh | 3 +- src/health.c | 5 + src/health.h | 121 ++++++++++++ src/mdash.c | 169 ++++++++++++++++ src/mdash.h | 24 +++ src/net.c | 5 +- src/net.h | 1 + src/net_builtin.c | 1 + src/sock.c | 3 +- test/Makefile | 2 +- .../nucleo-h723zg/cubemx/Core/Src/main.c | 2 +- .../stm32/nucleo-h723zg/minimal/Makefile | 14 +- tutorials/stm32/nucleo-h723zg/minimal/link.ld | 4 + tutorials/stm32/nucleo-h723zg/minimal/main.c | 68 ++++++- .../minimal/{mongoose => }/mongoose_config.h | 10 + 17 files changed, 748 insertions(+), 17 deletions(-) create mode 100644 src/health.c create mode 100644 src/health.h create mode 100644 src/mdash.c create mode 100644 src/mdash.h rename tutorials/stm32/nucleo-h723zg/minimal/{mongoose => }/mongoose_config.h (91%) diff --git a/mongoose.c b/mongoose.c index d1007958..a22898a3 100644 --- a/mongoose.c +++ b/mongoose.c @@ -3597,6 +3597,15 @@ static bool p_mkdir(const char *path) { struct mg_fs mg_fs_posix = {p_stat, p_list, p_open, p_close, p_read, p_write, p_seek, p_rename, p_remove, p_mkdir}; +#ifdef MG_ENABLE_LINES +#line 1 "src/health.c" +#endif + + +// 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; + #ifdef MG_ENABLE_LINES #line 1 "src/http.c" #endif @@ -7044,6 +7053,179 @@ void mg_md5_final(mg_md5_ctx *ctx, unsigned char digest[16]) { } #endif +#ifdef MG_ENABLE_LINES +#line 1 "src/mdash.c" +#endif + + +#if MG_ENABLE_MDASH + + + + + + + + + + + + +static const char *mg_mdash_ca_pem = + "-----BEGIN CERTIFICATE-----\n" + "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\n" + "-----END CERTIFICATE-----\n"; + +static struct mg_connection *mg_mdash_conn = NULL; +// static struct mg_str mg_mdash_key; +// static void mg_connection *mg_mdash_conn = NULL; + +static void mg_mdash_fn(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_CONNECT) { + // c->is_hexdumping = true; + if (mg_url_is_ssl(MG_MDASH_URL)) { + struct mg_str host = mg_url_host(MG_MDASH_URL); + struct mg_tls_opts opts = {.ca = mg_str(mg_mdash_ca_pem), .name = host}; + mg_tls_init(c, &opts); + } + } + if (ev == MG_EV_WS_OPEN) { + MG_INFO(("Connected to %s", MG_MDASH_URL)); + // s_mDashState = MDASH_STATE_CLOUD_CONNECTED; + // mDashTriggerEvent(s_mDashState, NULL); + } else if (ev == MG_EV_WS_MSG) { + struct mg_iobuf io = {0, 0, 0, 512}; + struct mg_ws_message *wm = ev_data; + struct mg_str s = wm->data; + while (s.len > 0 && isspace((uint8_t) s.buf[s.len - 1])) s.len--; + MG_INFO(("RPC: %.*s", (int) s.len, s.buf)); + struct mg_rpc_req r = {.head = &c->mgr->rpcs, + .rpc = NULL, + .pfn = mg_pfn_iobuf, + .pfn_data = &io, + .req_data = NULL, + .frame = s}; + mg_rpc_process(&r); + if (io.buf != NULL) { + MG_INFO((" %s", io.buf)); + mg_ws_send(c, (const char *) io.buf, io.len, WEBSOCKET_OP_TEXT); + } else { + MG_DEBUG(("%s", "RPC unhandled")); + } + mg_iobuf_free(&io); + } else if (ev == MG_EV_CLOSE) { + mg_mdash_conn = NULL; + MG_INFO(("disconnected, ram %lu", 0)); + } + (void) c; +} + +static void mg_mdash_rpc_get_info(struct mg_rpc_req *r) { + mg_rpc_ok(r, "{%m:%m,%m:%llu,%m:%m,%m:\"mws.%d\"}", MG_ESC("fw_version"), + MG_ESC(MG_MDASH_FIRMWARE_VERSION), MG_ESC("uptime"), + (uint64_t) (mg_millis() / 1000), MG_ESC("reboot_reason"), + MG_ESC(mg_health_reason_str(mg_health_reason())), MG_ESC("arch"), + MG_ARCH); +} + +static void mg_mdash_rpc_ota_begin(struct mg_rpc_req *r) { + size_t total = (size_t) mg_json_get_long(r->frame, "$.params.size", -1); + if (mg_ota_begin(total)) { + mg_rpc_ok(r, "true"); + } else { + mg_rpc_err(r, 500, "%m", MG_ESC("failed")); + } +} + +static void mg_mdash_rpc_ota_write(struct mg_rpc_req *r) { + int len = 0, ofs = 0; + if ((ofs = mg_json_get(r->frame, "$.params.data", &len)) <= 0 || + r->frame.buf[ofs] != '"') { + mg_rpc_err(r, 500, "%m", MG_ESC("data required")); + } else { + struct mg_str s = mg_str_n(&r->frame.buf[ofs] + 1, (size_t) (len - 2)); + size_t n = mg_base64_decode(s.buf, s.len, s.buf, s.len); // Decode in-place + if (n == 0) { + mg_rpc_ok(r, "%m", MG_ESC("finished")); + mg_ota_end(); + } else if (mg_ota_write(s.buf, n)) { + mg_rpc_ok(r, "{%m:%d}", MG_ESC("queued"), n); + } else { + mg_rpc_err(r, 500, "%m", MG_ESC("write failed")); + mg_ota_end(); + } + // mDashNotify("State.Set", "{%m:{%m:%d}}", MG_ESC("ota"), + // MG_ESC("percent"), (int) (s_ota_written * 100 / s_ota_size)); + } +} + +static void mg_mdash_rpc_ota_end(struct mg_rpc_req *r) { + if (mg_ota_end()) { + mg_rpc_ok(r, "true"); + } else { + mg_rpc_err(r, 500, "%m", MG_ESC("failed")); + } + // mDashNotify("State.Set", "{%m:{%m:null}}", MG_ESC("ota"), + // MG_ESC("percent")); +} + +void mg_mdash_init(struct mg_mgr *mgr) { + MG_INFO(("Initialising mdash agent, key [%s]", MG_MDASH_KEY)); + mg_rpc_add(&mgr->rpcs, mg_str("RPC.List"), mg_rpc_list, NULL); + mg_rpc_add(&mgr->rpcs, mg_str("Sys.GetInfo"), mg_mdash_rpc_get_info, NULL); + mg_rpc_add(&mgr->rpcs, mg_str("OTA.Begin"), mg_mdash_rpc_ota_begin, NULL); + mg_rpc_add(&mgr->rpcs, mg_str("OTA.Write"), mg_mdash_rpc_ota_write, NULL); + mg_rpc_add(&mgr->rpcs, mg_str("OTA.End"), mg_mdash_rpc_ota_end, NULL); +} + +void mg_mdash_poll(struct mg_mgr *mgr) { + static uint64_t timer = 0; + if (mg_mdash_conn == NULL && mg_timer_expired(&timer, 1000, mg_millis())) { + MG_INFO(("Connecting to %s", MG_MDASH_URL)); + mg_mdash_conn = mg_ws_connect(mgr, MG_MDASH_URL, mg_mdash_fn, NULL, + "Origin: %s\r\n" + "Authorization: Bearer %s\r\n", + MG_MDASH_URL, MG_MDASH_KEY); + } +} + +#else +void mg_mdash_init(struct mg_mgr *mgr) { + (void) mgr; +} +void mg_mdash_poll(struct mg_mgr *mgr) { + (void) mgr; +} +#endif + #ifdef MG_ENABLE_LINES #line 1 "src/modbus.c" #endif @@ -8241,6 +8423,9 @@ void mg_mgr_init(struct mg_mgr *mgr) { : MG_TLS == MG_TLS_BUILTIN ? "builtin" : MG_TLS == MG_TLS_WOLFSSL ? "WolfSSL" : "custom")); +#if MG_ENABLE_MDASH + mg_mdash_init(mgr); +#endif } #if MG_ENABLE_TCPIP @@ -10661,6 +10846,7 @@ void mg_mgr_poll(struct mg_mgr *mgr, int ms) { uint64_t now = mg_millis(); mg_timer_poll(&mgr->timers, now); mg_ota_poll(mgr); + mg_mdash_poll(mgr); if (mgr->ifp == NULL || mgr->ifp->driver == NULL) return; mg_tcpip_poll(mgr->ifp, now); for (c = mgr->conns; c != NULL; c = tmp) { @@ -15151,6 +15337,7 @@ void mg_mgr_poll(struct mg_mgr *mgr, int ms) { now = mg_millis(); mg_timer_poll(&mgr->timers, now); mg_ota_poll(mgr); + mg_mdash_poll(mgr); for (c = mgr->conns; c != NULL; c = tmp) { long flush = 0; diff --git a/mongoose.h b/mongoose.h index 8b844644..bce2e8a0 100644 --- a/mongoose.h +++ b/mongoose.h @@ -2443,6 +2443,7 @@ struct mg_mgr { struct mg_tcpip_if *ifp; // Builtin TCP/IP stack: network interface pointer size_t extraconnsize; // Builtin TCP/IP stack: extra bytes allocated per connection union mg_pipe pipe; // Socketpair write-end / queue, used by mg_wakeup() + struct mg_rpc *rpcs; // Linked list of registered RPC handlers #if MG_ENABLE_FREERTOS_TCP SocketSet_t ss; // FreeRTOS-TCP socket set #endif @@ -4919,6 +4920,151 @@ void mg_ota_device_id(char *buf, size_t len); // mg_mgr_poll() when MG_OTA_URL is set in mongoose_config.h. Do not call // directly. void mg_ota_poll(struct mg_mgr *); +// Device health monitoring +// +// Keeps a small record in a RAM region that survives a warm reset, so a device +// can tell why it restarted. The record holds a boot counter, the uptime this +// boot has reached, and the reset reason reported by the hardware. A crash +// loop, a hang loop and a marginal power supply each leave a different mark. +// +// Put MG_HEALTH_INIT() in main(), right after clock and memory init: +// +// ```c +// int main(void) { +// hal_clock_init(); +// MG_HEALTH_INIT(); +// ... +// } +// ``` +// +// The record must live in RAM that startup code neither copies nor zeroes. +// See tutorials/stm32/nucleo-h723zg/minimal/link.ld for the .mg_health region, +// and tutorials/stm32/nucleo-h723zg/minimal/mongoose_config.h for the +// reset-reason hooks. +// +// On Cortex-M, a fault handler can record the crash: it sets reset_reason to +// MG_HEALTH_RESET_FAULT and walks the stack into backtrace[]. See +// tutorials/stm32/nucleo-h723zg/minimal/main.c for a sample HardFault_Handler. + + + + + +#ifndef MG_HEALTH_MAGIC +#define MG_HEALTH_MAGIC {'M', 'G', 'H', '3'} // '3' is the layout version +#endif + +// 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"))) +#ifndef MG_HEALTH_RAM +#define MG_HEALTH_RAM +#endif + +// 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 +enum mg_health_reason { + MG_HEALTH_RESET_UNKNOWN = 0, // Nothing reported, or no masks configured + MG_HEALTH_RESET_POWER, // Power-on reset, RAM contents are lost + MG_HEALTH_RESET_BROWNOUT, // Supply dipped below the brown-out level + MG_HEALTH_RESET_PIN, // External reset pin, e.g. the reset button + MG_HEALTH_RESET_SOFTWARE, // Firmware asked for it, e.g. NVIC_SystemReset + MG_HEALTH_RESET_LOWPOWER, // Illegal or failed low-power transition + MG_HEALTH_RESET_WATCHDOG, // Watchdog fired: the application stopped + MG_HEALTH_RESET_FAULT // Fault handler ran, see regs[] and saved_stack +}; + +#define MG_HEALTH_BACKTRACE 20 // Return addresses in mg_health::backtrace + +struct mg_health { + char magic[4]; // MG_HEALTH_MAGIC when the record holds valid data + uint32_t reset_reason; // enum mg_health_reason that started this boot + uint32_t backtrace[MG_HEALTH_BACKTRACE]; // Crash backtrace, frame 0 first +}; + +extern struct mg_health mg_health_record; // Defined in health.c + +// Return true if the record holds valid data, i.e. it survived the reset and +// was written by this firmware layout +static inline bool mg_health_valid(void) { + static const char magic[] = MG_HEALTH_MAGIC; + return memcmp(mg_health_record.magic, magic, + sizeof(mg_health_record.magic)) == 0; +} + +static inline int mg_health_reason(void) { + return mg_health_valid() ? (int) mg_health_record.reset_reason + : MG_HEALTH_RESET_UNKNOWN; +} + +static inline const char *mg_health_reason_str(int reason) { + switch (reason) { + case MG_HEALTH_RESET_POWER: return "power"; + case MG_HEALTH_RESET_BROWNOUT: return "brownout"; + case MG_HEALTH_RESET_PIN: return "pin"; + case MG_HEALTH_RESET_SOFTWARE: return "software"; + case MG_HEALTH_RESET_LOWPOWER: return "lowpower"; + case MG_HEALTH_RESET_WATCHDOG: return "watchdog"; + case MG_HEALTH_RESET_FAULT: return "fault"; + default: return "unknown"; + } +} + +// 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() +static inline 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)); + } + // h->prev_uptime = h->uptime; + // h->prev_reason = h->reason; + // h->counter++; + // h->uptime = 0; + // h->reason = mg_health_decode_reason(); + // MG_HEALTH_RESET_CLEAR(); +} + +// // Record that the main loop is still alive. Called from mg_mgr_poll(), so a +// // hung application leaves uptime frozen at the hang +// extern uint64_t mg_health_next_ms; // Defined in health.c + +// static inline void mg_health_uptime(uint64_t now_ms) { +// struct mg_health *h = mg_health_get(); +// if (now_ms >= mg_health_next_ms && mg_health_valid()) { +// mg_health_next_ms = now_ms + 1000; +// h->uptime++; +// } +// } + +#define MG_HEALTH_INIT() mg_health_init() +// Cloud connector API to https://mdash.net - device management cloud + + + + + +#ifndef MG_MDASH_URL +#define MG_MDASH_URL "wss://mdash.net/api/v2/rpc" +#endif + +#ifndef MG_MDASH_KEY +#define MG_MDASH_KEY "" +#endif + +#ifndef MG_MDASH_FIRMWARE_VERSION +#define MG_MDASH_FIRMWARE_VERSION "1.0.0" +#endif + +#ifndef MG_ENABLE_MDASH +#define MG_ENABLE_MDASH 0 +#endif + +void mg_mdash_init(struct mg_mgr *); +void mg_mdash_poll(struct mg_mgr *); diff --git a/resources/downloads/integrate_mongoose_cubemx.sh b/resources/downloads/integrate_mongoose_cubemx.sh index 9f0c921b..29d6e9da 100755 --- a/resources/downloads/integrate_mongoose_cubemx.sh +++ b/resources/downloads/integrate_mongoose_cubemx.sh @@ -39,8 +39,7 @@ patch_linker_script() { HUART=`perl -nle 'print \$1 if /^UART_HandleTypeDef (.+);/' $MAIN_C` grep -q mongoose.h $MAIN_C || perl -i -ne 'print; print "#include \"mongoose.h\"\n" if /BEGIN Includes/' $MAIN_C grep -q '^static void log_fn' $MAIN_C || perl -i -ne "print; print \"static void log_fn(char ch, void *param) {\n HAL_UART_Transmit(param, (unsigned char *) &ch, 1, HAL_MAX_DELAY);\n}\n\" if /USER CODE BEGIN 0/" $MAIN_C -grep -q 'mg_mgr_init' $MAIN_C || perl -i -ne 'print; print " struct mg_mgr mgr;\n mg_mgr_init(&mgr);\n mg_log_set_fn(log_fn, &'"${HUART}"');\n\n" if /USER CODE BEGIN WHILE/' $MAIN_C -grep -q 'mg_mgr_init' $MAIN_C && grep -q 'mg_log_set_fn' $MAIN_C || perl -i -ne 'print; print " mg_log_set_fn(log_fn, &'"${HUART}"');\n" if /mg_mgr_init/' $MAIN_C +grep -q 'mg_mgr_init' $MAIN_C || perl -i -ne 'print; print " struct mg_mgr mgr;\n mg_log_set_fn(log_fn, &'"${HUART}"');\n mg_mgr_init(&mgr);\n mg_log_set_fn(log_fn, &'"${HUART}"');\n\n" if /USER CODE BEGIN WHILE/' $MAIN_C grep -q 'mg_mgr_poll' $MAIN_C || perl -i -ne 'print " mg_mgr_poll(&mgr, 0);\n" if /USER CODE END WHILE/; print;' $MAIN_C patch_linker_script $DIR/STM32H723XG_FLASH.ld patch_linker_script $DIR/STM32H743XX_FLASH.ld diff --git a/src/health.c b/src/health.c new file mode 100644 index 00000000..bc82c602 --- /dev/null +++ b/src/health.c @@ -0,0 +1,5 @@ +#include "health.h" + +// 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; diff --git a/src/health.h b/src/health.h new file mode 100644 index 00000000..3fc1ed92 --- /dev/null +++ b/src/health.h @@ -0,0 +1,121 @@ +// Device health monitoring +// +// Keeps a small record in a RAM region that survives a warm reset, so a device +// can tell why it restarted. The record holds a boot counter, the uptime this +// boot has reached, and the reset reason reported by the hardware. A crash +// loop, a hang loop and a marginal power supply each leave a different mark. +// +// Put MG_HEALTH_INIT() in main(), right after clock and memory init: +// +// ```c +// int main(void) { +// hal_clock_init(); +// MG_HEALTH_INIT(); +// ... +// } +// ``` +// +// The record must live in RAM that startup code neither copies nor zeroes. +// See tutorials/stm32/nucleo-h723zg/minimal/link.ld for the .mg_health region, +// and tutorials/stm32/nucleo-h723zg/minimal/mongoose_config.h for the +// reset-reason hooks. +// +// On Cortex-M, a fault handler can record the crash: it sets reset_reason to +// MG_HEALTH_RESET_FAULT and walks the stack into backtrace[]. See +// tutorials/stm32/nucleo-h723zg/minimal/main.c for a sample HardFault_Handler. + +#pragma once + +#include "arch.h" + +#ifndef MG_HEALTH_MAGIC +#define MG_HEALTH_MAGIC {'M', 'G', 'H', '3'} // '3' is the layout version +#endif + +// 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"))) +#ifndef MG_HEALTH_RAM +#define MG_HEALTH_RAM +#endif + +// 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 +enum mg_health_reason { + MG_HEALTH_RESET_UNKNOWN = 0, // Nothing reported, or no masks configured + MG_HEALTH_RESET_POWER, // Power-on reset, RAM contents are lost + MG_HEALTH_RESET_BROWNOUT, // Supply dipped below the brown-out level + MG_HEALTH_RESET_PIN, // External reset pin, e.g. the reset button + MG_HEALTH_RESET_SOFTWARE, // Firmware asked for it, e.g. NVIC_SystemReset + MG_HEALTH_RESET_LOWPOWER, // Illegal or failed low-power transition + MG_HEALTH_RESET_WATCHDOG, // Watchdog fired: the application stopped + MG_HEALTH_RESET_FAULT // Fault handler ran, see regs[] and saved_stack +}; + +#define MG_HEALTH_BACKTRACE 20 // Return addresses in mg_health::backtrace + +struct mg_health { + char magic[4]; // MG_HEALTH_MAGIC when the record holds valid data + uint32_t reset_reason; // enum mg_health_reason that started this boot + uint32_t backtrace[MG_HEALTH_BACKTRACE]; // Crash backtrace, frame 0 first +}; + +extern struct mg_health mg_health_record; // Defined in health.c + +// Return true if the record holds valid data, i.e. it survived the reset and +// was written by this firmware layout +static inline bool mg_health_valid(void) { + static const char magic[] = MG_HEALTH_MAGIC; + return memcmp(mg_health_record.magic, magic, + sizeof(mg_health_record.magic)) == 0; +} + +static inline int mg_health_reason(void) { + return mg_health_valid() ? (int) mg_health_record.reset_reason + : MG_HEALTH_RESET_UNKNOWN; +} + +static inline const char *mg_health_reason_str(int reason) { + switch (reason) { + case MG_HEALTH_RESET_POWER: return "power"; + case MG_HEALTH_RESET_BROWNOUT: return "brownout"; + case MG_HEALTH_RESET_PIN: return "pin"; + case MG_HEALTH_RESET_SOFTWARE: return "software"; + case MG_HEALTH_RESET_LOWPOWER: return "lowpower"; + case MG_HEALTH_RESET_WATCHDOG: return "watchdog"; + case MG_HEALTH_RESET_FAULT: return "fault"; + default: return "unknown"; + } +} + +// 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() +static inline 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)); + } + // h->prev_uptime = h->uptime; + // h->prev_reason = h->reason; + // h->counter++; + // h->uptime = 0; + // h->reason = mg_health_decode_reason(); + // MG_HEALTH_RESET_CLEAR(); +} + +// // Record that the main loop is still alive. Called from mg_mgr_poll(), so a +// // hung application leaves uptime frozen at the hang +// extern uint64_t mg_health_next_ms; // Defined in health.c + +// static inline void mg_health_uptime(uint64_t now_ms) { +// struct mg_health *h = mg_health_get(); +// if (now_ms >= mg_health_next_ms && mg_health_valid()) { +// mg_health_next_ms = now_ms + 1000; +// h->uptime++; +// } +// } + +#define MG_HEALTH_INIT() mg_health_init() diff --git a/src/mdash.c b/src/mdash.c new file mode 100644 index 00000000..9ec5a4a3 --- /dev/null +++ b/src/mdash.c @@ -0,0 +1,169 @@ +#include "mdash.h" + +#if MG_ENABLE_MDASH + +#include "base64.h" +#include "health.h" +#include "log.h" +#include "ota.h" +#include "printf.h" +#include "rpc.h" +#include "sntp.h" +#include "tls.h" +#include "url.h" +#include "ws.h" + +static const char *mg_mdash_ca_pem = + "-----BEGIN CERTIFICATE-----\n" + "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\n" + "-----END CERTIFICATE-----\n"; + +static struct mg_connection *mg_mdash_conn = NULL; +// static struct mg_str mg_mdash_key; +// static void mg_connection *mg_mdash_conn = NULL; + +static void mg_mdash_fn(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_CONNECT) { + // c->is_hexdumping = true; + if (mg_url_is_ssl(MG_MDASH_URL)) { + struct mg_str host = mg_url_host(MG_MDASH_URL); + struct mg_tls_opts opts = {.ca = mg_str(mg_mdash_ca_pem), .name = host}; + mg_tls_init(c, &opts); + } + } + if (ev == MG_EV_WS_OPEN) { + MG_INFO(("Connected to %s", MG_MDASH_URL)); + // s_mDashState = MDASH_STATE_CLOUD_CONNECTED; + // mDashTriggerEvent(s_mDashState, NULL); + } else if (ev == MG_EV_WS_MSG) { + struct mg_iobuf io = {0, 0, 0, 512}; + struct mg_ws_message *wm = ev_data; + struct mg_str s = wm->data; + while (s.len > 0 && isspace((uint8_t) s.buf[s.len - 1])) s.len--; + MG_INFO(("RPC: %.*s", (int) s.len, s.buf)); + struct mg_rpc_req r = {.head = &c->mgr->rpcs, + .rpc = NULL, + .pfn = mg_pfn_iobuf, + .pfn_data = &io, + .req_data = NULL, + .frame = s}; + mg_rpc_process(&r); + if (io.buf != NULL) { + MG_INFO((" %s", io.buf)); + mg_ws_send(c, (const char *) io.buf, io.len, WEBSOCKET_OP_TEXT); + } else { + MG_DEBUG(("%s", "RPC unhandled")); + } + mg_iobuf_free(&io); + } else if (ev == MG_EV_CLOSE) { + mg_mdash_conn = NULL; + MG_INFO(("disconnected, ram %lu", 0)); + } + (void) c; +} + +static void mg_mdash_rpc_get_info(struct mg_rpc_req *r) { + mg_rpc_ok(r, "{%m:%m,%m:%llu,%m:%m,%m:\"mws.%d\"}", MG_ESC("fw_version"), + MG_ESC(MG_MDASH_FIRMWARE_VERSION), MG_ESC("uptime"), + (uint64_t) (mg_millis() / 1000), MG_ESC("reboot_reason"), + MG_ESC(mg_health_reason_str(mg_health_reason())), MG_ESC("arch"), + MG_ARCH); +} + +static void mg_mdash_rpc_ota_begin(struct mg_rpc_req *r) { + size_t total = (size_t) mg_json_get_long(r->frame, "$.params.size", -1); + if (mg_ota_begin(total)) { + mg_rpc_ok(r, "true"); + } else { + mg_rpc_err(r, 500, "%m", MG_ESC("failed")); + } +} + +static void mg_mdash_rpc_ota_write(struct mg_rpc_req *r) { + int len = 0, ofs = 0; + if ((ofs = mg_json_get(r->frame, "$.params.data", &len)) <= 0 || + r->frame.buf[ofs] != '"') { + mg_rpc_err(r, 500, "%m", MG_ESC("data required")); + } else { + struct mg_str s = mg_str_n(&r->frame.buf[ofs] + 1, (size_t) (len - 2)); + size_t n = mg_base64_decode(s.buf, s.len, s.buf, s.len); // Decode in-place + if (n == 0) { + mg_rpc_ok(r, "%m", MG_ESC("finished")); + mg_ota_end(); + } else if (mg_ota_write(s.buf, n)) { + mg_rpc_ok(r, "{%m:%d}", MG_ESC("queued"), n); + } else { + mg_rpc_err(r, 500, "%m", MG_ESC("write failed")); + mg_ota_end(); + } + // mDashNotify("State.Set", "{%m:{%m:%d}}", MG_ESC("ota"), + // MG_ESC("percent"), (int) (s_ota_written * 100 / s_ota_size)); + } +} + +static void mg_mdash_rpc_ota_end(struct mg_rpc_req *r) { + if (mg_ota_end()) { + mg_rpc_ok(r, "true"); + } else { + mg_rpc_err(r, 500, "%m", MG_ESC("failed")); + } + // mDashNotify("State.Set", "{%m:{%m:null}}", MG_ESC("ota"), + // MG_ESC("percent")); +} + +void mg_mdash_init(struct mg_mgr *mgr) { + MG_INFO(("Initialising mdash agent, key [%s]", MG_MDASH_KEY)); + mg_rpc_add(&mgr->rpcs, mg_str("RPC.List"), mg_rpc_list, NULL); + mg_rpc_add(&mgr->rpcs, mg_str("Sys.GetInfo"), mg_mdash_rpc_get_info, NULL); + mg_rpc_add(&mgr->rpcs, mg_str("OTA.Begin"), mg_mdash_rpc_ota_begin, NULL); + mg_rpc_add(&mgr->rpcs, mg_str("OTA.Write"), mg_mdash_rpc_ota_write, NULL); + mg_rpc_add(&mgr->rpcs, mg_str("OTA.End"), mg_mdash_rpc_ota_end, NULL); +} + +void mg_mdash_poll(struct mg_mgr *mgr) { + static uint64_t timer = 0; + if (mg_mdash_conn == NULL && mg_timer_expired(&timer, 1000, mg_millis())) { + MG_INFO(("Connecting to %s", MG_MDASH_URL)); + mg_mdash_conn = mg_ws_connect(mgr, MG_MDASH_URL, mg_mdash_fn, NULL, + "Origin: %s\r\n" + "Authorization: Bearer %s\r\n", + MG_MDASH_URL, MG_MDASH_KEY); + } +} + +#else +void mg_mdash_init(struct mg_mgr *mgr) { + (void) mgr; +} +void mg_mdash_poll(struct mg_mgr *mgr) { + (void) mgr; +} +#endif diff --git a/src/mdash.h b/src/mdash.h new file mode 100644 index 00000000..fe0d4459 --- /dev/null +++ b/src/mdash.h @@ -0,0 +1,24 @@ +// Cloud connector API to https://mdash.net - device management cloud + +#pragma once + +#include "net.h" + +#ifndef MG_MDASH_URL +#define MG_MDASH_URL "wss://mdash.net/api/v2/rpc" +#endif + +#ifndef MG_MDASH_KEY +#define MG_MDASH_KEY "" +#endif + +#ifndef MG_MDASH_FIRMWARE_VERSION +#define MG_MDASH_FIRMWARE_VERSION "1.0.0" +#endif + +#ifndef MG_ENABLE_MDASH +#define MG_ENABLE_MDASH 0 +#endif + +void mg_mdash_init(struct mg_mgr *); +void mg_mdash_poll(struct mg_mgr *); diff --git a/src/net.c b/src/net.c index d4c3efb2..e8d18907 100644 --- a/src/net.c +++ b/src/net.c @@ -2,7 +2,7 @@ #include "dns.h" #include "fmt.h" #include "log.h" -#include "ota.h" +#include "mdash.h" #include "printf.h" #include "profile.h" #include "timer.h" @@ -350,6 +350,9 @@ void mg_mgr_init(struct mg_mgr *mgr) { : MG_TLS == MG_TLS_BUILTIN ? "builtin" : MG_TLS == MG_TLS_WOLFSSL ? "WolfSSL" : "custom")); +#if MG_ENABLE_MDASH + mg_mdash_init(mgr); +#endif } #if MG_ENABLE_TCPIP diff --git a/src/net.h b/src/net.h index 10356cb5..5ba7a71b 100644 --- a/src/net.h +++ b/src/net.h @@ -50,6 +50,7 @@ struct mg_mgr { struct mg_tcpip_if *ifp; // Builtin TCP/IP stack: network interface pointer size_t extraconnsize; // Builtin TCP/IP stack: extra bytes allocated per connection union mg_pipe pipe; // Socketpair write-end / queue, used by mg_wakeup() + struct mg_rpc *rpcs; // Linked list of registered RPC handlers #if MG_ENABLE_FREERTOS_TCP SocketSet_t ss; // FreeRTOS-TCP socket set #endif diff --git a/src/net_builtin.c b/src/net_builtin.c index 98989e09..5b730320 100644 --- a/src/net_builtin.c +++ b/src/net_builtin.c @@ -2403,6 +2403,7 @@ void mg_mgr_poll(struct mg_mgr *mgr, int ms) { uint64_t now = mg_millis(); mg_timer_poll(&mgr->timers, now); mg_ota_poll(mgr); + mg_mdash_poll(mgr); if (mgr->ifp == NULL || mgr->ifp->driver == NULL) return; mg_tcpip_poll(mgr->ifp, now); for (c = mgr->conns; c != NULL; c = tmp) { diff --git a/src/sock.c b/src/sock.c index 7cca2975..28c3cc6c 100644 --- a/src/sock.c +++ b/src/sock.c @@ -1,6 +1,6 @@ -#include "dns.h" #include "event.h" #include "log.h" +#include "mdash.h" #include "net.h" #include "printf.h" #include "str.h" @@ -768,6 +768,7 @@ void mg_mgr_poll(struct mg_mgr *mgr, int ms) { now = mg_millis(); mg_timer_poll(&mgr->timers, now); mg_ota_poll(mgr); + mg_mdash_poll(mgr); for (c = mgr->conns; c != NULL; c = tmp) { long flush = 0; diff --git a/test/Makefile b/test/Makefile index 1d4a307b..94800a37 100644 --- a/test/Makefile +++ b/test/Makefile @@ -106,7 +106,7 @@ mongoose.c: Makefile $(wildcard ../src/*.c) $(wildcard ../src/drivers/*.c) cd .. && (export LC_ALL=C ; cat src/license.h; echo; echo '#include "mongoose.h"' ; (for F in src/*.c src/drivers/*.c ; do echo; echo '#ifdef MG_ENABLE_LINES'; echo "#line 1 \"$$F\""; echo '#endif'; cat $$F | sed -e 's,#include ".*,,'; done))> $@ mongoose.h: $(HDRS) Makefile - cd .. && (export LC_ALL=C ; cat src/license.h; echo; echo '#ifndef MONGOOSE_H'; echo '#define MONGOOSE_H'; echo; cat src/version.h ; echo; echo '#ifdef __cplusplus'; echo 'extern "C" {'; echo '#endif'; cat src/arch.h src/arch_*.h src/os_*.h src/net_ft.h src/net_lwip.h src/net_rl.h src/config.h src/profile.h src/str.h src/queue.h src/fmt.h src/printf.h src/log.h src/timer.h src/fs.h src/util.h src/url.h src/iobuf.h src/base64.h src/md5.h src/sha1.h src/sha256.h src/event.h src/net.h src/http.h src/ssi.h src/tls.h src/tls_x25519.h src/tls_aes128.h src/tls_uecc.h src/tls_chacha20.h src/tls_rsa.h src/tls_mbed.h src/tls_openssl.h src/ws.h src/sntp.h src/mqtt.h src/dns.h src/modbus.h src/json.h src/jwt.h src/rpc.h src/dash.h src/ota.h src/flash.h src/wifi.h src/l2.h src/net_builtin.h src/bsd.h src/drivers/*.h | sed -e '/keep/! s,#include ".*,,' -e 's,^#pragma once,,'; echo; echo '#ifdef __cplusplus'; echo '}'; echo '#endif'; echo '#endif // MONGOOSE_H')> $@ + cd .. && (export LC_ALL=C ; cat src/license.h; echo; echo '#ifndef MONGOOSE_H'; echo '#define MONGOOSE_H'; echo; cat src/version.h ; echo; echo '#ifdef __cplusplus'; echo 'extern "C" {'; echo '#endif'; cat src/arch.h src/arch_*.h src/os_*.h src/net_ft.h src/net_lwip.h src/net_rl.h src/config.h src/profile.h src/str.h src/queue.h src/fmt.h src/printf.h src/log.h src/timer.h src/fs.h src/util.h src/url.h src/iobuf.h src/base64.h src/md5.h src/sha1.h src/sha256.h src/event.h src/net.h src/http.h src/ssi.h src/tls.h src/tls_x25519.h src/tls_aes128.h src/tls_uecc.h src/tls_chacha20.h src/tls_rsa.h src/tls_mbed.h src/tls_openssl.h src/ws.h src/sntp.h src/mqtt.h src/dns.h src/modbus.h src/json.h src/jwt.h src/rpc.h src/dash.h src/ota.h src/health.h src/mdash.h src/flash.h src/wifi.h src/l2.h src/net_builtin.h src/bsd.h src/drivers/*.h | sed -e '/keep/! s,#include ".*,,' -e 's,^#pragma once,,'; echo; echo '#ifdef __cplusplus'; echo '}'; echo '#endif'; echo '#endif // MONGOOSE_H')> $@ # Check that all external (exported) symbols have "mg_" prefix mg_prefix: mongoose.c mongoose.h diff --git a/tutorials/stm32/nucleo-h723zg/cubemx/Core/Src/main.c b/tutorials/stm32/nucleo-h723zg/cubemx/Core/Src/main.c index 977c2ec8..0b3ecfe6 100644 --- a/tutorials/stm32/nucleo-h723zg/cubemx/Core/Src/main.c +++ b/tutorials/stm32/nucleo-h723zg/cubemx/Core/Src/main.c @@ -133,7 +133,7 @@ int main(void) /* Initialize all configured peripherals */ MX_GPIO_Init(); - (void) MX_ETH_Init; // Don't use that stuff, Mongoose handle Ethernet + MX_ETH_Init(); MX_RNG_Init(); MX_USART3_UART_Init(); /* USER CODE BEGIN 2 */ diff --git a/tutorials/stm32/nucleo-h723zg/minimal/Makefile b/tutorials/stm32/nucleo-h723zg/minimal/Makefile index 9cf5eadf..876fc6aa 100644 --- a/tutorials/stm32/nucleo-h723zg/minimal/Makefile +++ b/tutorials/stm32/nucleo-h723zg/minimal/Makefile @@ -1,24 +1,22 @@ # Copyright (c) 2026 Cesanta Software Limited # Environment setup: https://mongoose.ws/docs/getting-started/build-environment/ +MGDIR = ../../../.. + CFLAGS = -W -Wall -Wextra -Wundef -Wshadow -Wdouble-promotion CFLAGS += -Wformat-truncation -fno-common -Wconversion -Wno-sign-conversion CFLAGS += -g3 -Os -ffunction-sections -fdata-sections -CFLAGS += -I. -Icmsis_core/CMSIS/Core/Include -Icmsis_h7/Include -Imongoose +CFLAGS += -I. -I$(MGDIR) -Icmsis_core/CMSIS/Core/Include -Icmsis_h7/Include CFLAGS += -mcpu=cortex-m7 -mthumb -mfloat-abi=hard -mfpu=fpv5-d16 $(CFLAGS_EXTRA) LDFLAGS ?= -Tlink.ld -nostdlib -nostartfiles --specs nosys.specs -lc -lgcc -Wl,--gc-sections -Wl,-Map=$@.map SOURCES = main.c hal.c SOURCES += cmsis_h7/Source/Templates/gcc/startup_stm32h723xx.s -SOURCES += mongoose/mongoose.c - +SOURCES += $(MGDIR)/mongoose.c all build example: firmware.bin -mongoose/mongoose.c mongoose/mongoose.h: - cp ../../../../mongoose.[ch] mongoose/ - -firmware.elf: cmsis_core cmsis_h7 hal.h link.ld Makefile $(SOURCES) mongoose/mongoose.h +firmware.elf: cmsis_core cmsis_h7 hal.h link.ld Makefile $(SOURCES) $(MGDIR)/mongoose.h mongoose_config.h arm-none-eabi-gcc $(SOURCES) $(CFLAGS) $(CFLAGS_EXTRA) $(LDFLAGS) -o $@ firmware.bin: firmware.elf @@ -36,7 +34,7 @@ cmsis_h7: git clone -q -c advice.detachedHead=false --depth 1 -b v1.10.6 https://github.com/STMicroelectronics/cmsis_device_h7 $@ clean: - rm -rf firmware.* cmsis_* mongoose/mongoose.* + rm -rf firmware.* cmsis_* # Automated remote test. Requires env variable VCON_API_KEY set. See https://vcon.io/automated-firmware-tests/ diff --git a/tutorials/stm32/nucleo-h723zg/minimal/link.ld b/tutorials/stm32/nucleo-h723zg/minimal/link.ld index a62749dc..cacf44e7 100644 --- a/tutorials/stm32/nucleo-h723zg/minimal/link.ld +++ b/tutorials/stm32/nucleo-h723zg/minimal/link.ld @@ -23,6 +23,10 @@ SECTIONS { .eth_ram : { *(.eth_ram .eth_ram*) } > ram_d1 AT > flash + /* Health record. NOLOAD and in unused DTCM, so startup code neither copies + nor zeroes it and the contents survive a warm reset */ + .mg_health (NOLOAD) : { KEEP(*(.mg_health .mg_health*)) } > dtcmram + . = ALIGN(8); _end = .; } diff --git a/tutorials/stm32/nucleo-h723zg/minimal/main.c b/tutorials/stm32/nucleo-h723zg/minimal/main.c index 60ca9730..118ab0eb 100644 --- a/tutorials/stm32/nucleo-h723zg/minimal/main.c +++ b/tutorials/stm32/nucleo-h723zg/minimal/main.c @@ -24,10 +24,54 @@ static void log_fn(char ch, void *param) { static void blink_task(void) { static uint64_t blink_timer = 0; if (hal_timer_expired(&blink_timer, 500, hal_get_tick())) { - hal_gpio_toggle(LED1); + hal_gpio_toggle(LED2); } } +// Fault handler body. Runs in exception context: no printf, no malloc, no +// blocking calls. Records the crash reason and a backtrace into the health +// record, then resets. +// "used" keeps the linker from garbage-collecting this section: the only +// reference is the "b fault_c" branch in the naked handler below +__attribute__((used, noinline)) static void fault_c(uint32_t *sp) { + extern uint8_t _estack; // End of the main RAM region, defined in link.ld + size_t n = 0; + mg_health_record.reset_reason = MG_HEALTH_RESET_FAULT; + // Frame 0 is the faulting PC, frame 1 the caller's LR. Deeper frames come + // from a heuristic stack walk: BL pushes an odd return address that lives + // in flash. A stack word that merely looks like an address shows up as a + // bogus frame when symbolised, which is easy to filter by eye + mg_health_record.backtrace[n++] = sp[6] & ~1U; // Stacked PC + mg_health_record.backtrace[n++] = sp[5] & ~1U; // Stacked LR + for (uint32_t *p = sp + 8; + n < MG_HEALTH_BACKTRACE && + (uintptr_t) p < (uintptr_t) sp + 4096U && // Bound the scan + (uintptr_t) p < (uintptr_t) &_estack; // Stay in RAM + p++) { + uint32_t v = *p; + if ((v & 1U) && v >= 0x08000000U && v < 0x08000000U + 1024U * 1024U) { + mg_health_record.backtrace[n++] = v & ~1U; + } + } + NVIC_SystemReset(); +} + +// Common fault entry. EXC_RETURN bit 2 tells which stack was in use: +// 0 = MSP, 1 = PSP. Load the faulting SP into r0 and hand it to fault_c() +__attribute__((naked)) void HardFault_Handler(void) { + __asm volatile( + "tst lr, #4\n\t" // Test EXC_RETURN bit 2 + "ite eq\n\t" // If zero, use MSP; else PSP + "mrseq r0, msp\n\t" + "mrsne r0, psp\n\t" + "b fault_c\n\t"); +} + +// Route the other fault types through the same entry point +void MemManage_Handler(void) __attribute__((alias("HardFault_Handler"))); +void BusFault_Handler(void) __attribute__((alias("HardFault_Handler"))); +void UsageFault_Handler(void) __attribute__((alias("HardFault_Handler"))); + uint64_t mg_millis(void) { return hal_get_tick(); } @@ -70,6 +114,15 @@ static void http_ev_handler(struct mg_connection *c, int ev, void *ev_data) { int main(void) { hal_clock_init(); + + // Enable MemManage, BusFault and UsageFault so faults are precise and + // routed to their own handlers instead of escalating to HardFault + SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk | SCB_SHCSR_BUSFAULTENA_Msk | + SCB_SHCSR_USGFAULTENA_Msk; + + MG_OTA_BOOT_CHECK(); // Must be called after clock init + MG_HEALTH_INIT(); // Must be called after clock init + hal_uart_init(UART_DEBUG, UART_DEBUG_TX_PIN, UART_DEBUG_RX_PIN, 115200); mg_log_set_fn(log_fn, UART_DEBUG); hal_rng_init(); @@ -79,8 +132,17 @@ int main(void) { hal_gpio_output(LED3); MG_INFO(("Initialised. CPU clock: %lu MHz", SystemCoreClock / 1000000)); - - MG_OTA_BOOT_CHECK(); // Must be called after clock init + // Report the previous boot's crash backtrace, if any + if (mg_health_reason() == MG_HEALTH_RESET_FAULT) { + MG_INFO(("Previous boot faulted. Backtrace:")); + for (int i = 0; i < MG_HEALTH_BACKTRACE; i++) { + if (mg_health_record.backtrace[i] == 0) break; + MG_INFO((" #%d 0x%08lx", i, + (unsigned long) mg_health_record.backtrace[i])); + } + // * (volatile uint32_t *) 0 = 123; // Deliberate fault: write to unmapped memory + } + // for (;;) (void) 0; struct mg_mgr mgr; mg_mgr_init(&mgr); diff --git a/tutorials/stm32/nucleo-h723zg/minimal/mongoose/mongoose_config.h b/tutorials/stm32/nucleo-h723zg/minimal/mongoose_config.h similarity index 91% rename from tutorials/stm32/nucleo-h723zg/minimal/mongoose/mongoose_config.h rename to tutorials/stm32/nucleo-h723zg/minimal/mongoose_config.h index 1af698dd..cdfe6378 100644 --- a/tutorials/stm32/nucleo-h723zg/minimal/mongoose/mongoose_config.h +++ b/tutorials/stm32/nucleo-h723zg/minimal/mongoose_config.h @@ -44,6 +44,16 @@ #define MG_OTA_STATE_GET() (RTC->BKP0R) #define MG_OTA_STATE_SET(v) (RTC->BKP0R = (uint32_t) (v)) +// Health record lives in the .mg_health region, see link.ld +#define MG_HEALTH_RAM __attribute__((section(".mg_health"))) + +// mdash device token +#define MG_ENABLE_MDASH 1 + +#ifndef MG_MDASH_KEY +#define MG_MDASH_KEY "" // Set this +#endif + // Construct MAC address from the MCU unique ID #define MGUID ((uint32_t *) UID_BASE) #define MG_SET_MAC_ADDRESS(mac) \