From 63fc4ea0e9541dc20673c4dff129eb743fe3bbdc Mon Sep 17 00:00:00 2001 From: Patrick Winnertz Date: Fri, 24 Apr 2026 20:40:12 +0200 Subject: [PATCH 1/3] First try to implement the watchdog + set commands - maybe switch to a compile flag --- examples/simple_repeater/main.cpp | 5 ++++ examples/simple_room_server/main.cpp | 5 ++++ examples/simple_sensor/main.cpp | 5 ++++ src/helpers/CommonCLI.cpp | 23 +++++++++++++-- src/helpers/CommonCLI.h | 1 + src/helpers/NRF52Board.cpp | 43 ++++++++++++++++++++++++++++ src/helpers/NRF52Board.h | 9 ++++++ 7 files changed, 89 insertions(+), 2 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index e37078ce5f..676d7ef02a 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -95,6 +95,10 @@ void setup() { the_mesh.begin(fs); +#if defined(NRF52_PLATFORM) + board.initWatchdog(the_mesh.getNodePrefs()->wdt_timeout_secs); +#endif + #ifdef DISPLAY_CLASS ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif @@ -106,6 +110,7 @@ void setup() { } void loop() { + board.feedWatchdog(); int len = strlen(command); while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 825fb007d5..d83d54bd6a 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -72,6 +72,10 @@ void setup() { the_mesh.begin(fs); +#if defined(NRF52_PLATFORM) + board.initWatchdog(the_mesh.getNodePrefs()->wdt_timeout_secs); +#endif + #ifdef DISPLAY_CLASS ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif @@ -83,6 +87,7 @@ void setup() { } void loop() { + board.feedWatchdog(); int len = strlen(command); while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 330adcc2e4..b76d94ecf5 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -106,6 +106,10 @@ void setup() { the_mesh.begin(fs); +#if defined(NRF52_PLATFORM) + board.initWatchdog(the_mesh.getNodePrefs()->wdt_timeout_secs); +#endif + #ifdef DISPLAY_CLASS ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif @@ -117,6 +121,7 @@ void setup() { } void loop() { + board.feedWatchdog(); int len = strlen(command); while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b71afc72e2..2b16921ce2 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -89,7 +89,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.read((uint8_t *)&_prefs->wdt_timeout_secs, sizeof(_prefs->wdt_timeout_secs)); // 291 + // next: 292 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -119,6 +120,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean + // wdt_timeout_secs: 0 is valid (disabled), no constrain needed — uint8_t can't exceed 255 file.close(); } @@ -180,7 +182,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.write((uint8_t *)&_prefs->wdt_timeout_secs, sizeof(_prefs->wdt_timeout_secs)); // 291 + // next: 292 file.close(); } @@ -656,6 +659,15 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep savePrefs(); strcpy(reply, "OK"); } + } else if (memcmp(config, "wdt ", 4) == 0) { + uint32_t val = _atoi(&config[4]); + if (val > 255) { + strcpy(reply, "Error, must be 0 (off) or 1-255 (seconds)"); + } else { + _prefs->wdt_timeout_secs = (uint8_t)val; + savePrefs(); + sprintf(reply, "OK - WDT %s (reboot to apply)", val == 0 ? "disabled" : "enabled"); + } } else if (memcmp(config, "tx ", 3) == 0) { _prefs->tx_power_dbm = atoi(&config[3]); savePrefs(); @@ -808,6 +820,13 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "> strict"); } + } else if (memcmp(config, "wdt", 3) == 0 && (config[3] == 0 || config[3] == ' ')) { +#if defined(NRF52_PLATFORM) + sprintf(reply, "> %u secs%s", _prefs->wdt_timeout_secs, + _prefs->wdt_timeout_secs == 0 ? " (disabled)" : " (active after reboot)"); +#else + strcpy(reply, "> not supported on this platform"); +#endif } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) { sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm); } else if (memcmp(config, "freq", 4) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index ffdc7c6536..535aacd291 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -61,6 +61,7 @@ struct NodePrefs { // persisted to file uint8_t rx_boosted_gain; // power settings uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; + uint8_t wdt_timeout_secs; // 0=disabled, 1-255=seconds (nRF52 only, applies on reboot) }; class CommonCLICallbacks { diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 2c8753d464..0ea004677d 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -20,6 +20,49 @@ static void disconnect_callback(uint16_t conn_handle, uint8_t reason) { void NRF52Board::begin() { startup_reason = BD_STARTUP_NORMAL; + // NOTE: WDT is NOT started here. It is started from the application's + // setup() AFTER loading prefs, by calling initWatchdog(prefs.wdt_timeout_secs). + // This allows the timeout to be configured via CLI (set wdt N). +} + +void NRF52Board::initWatchdog(uint8_t timeout_secs) { + // 0 = watchdog disabled + if (timeout_secs == 0) { + MESH_DEBUG_PRINTLN("WDT: Disabled by configuration (set wdt to enable)"); + return; + } + + // WDT can only be configured once after reset (before TASKS_START). + // After START, CRV and CONFIG are read-only and locked until next reset. + if (_wdt_running) return; + + // Convert seconds to 32.768 kHz ticks + uint32_t ticks = (uint32_t)timeout_secs * 32768UL; + + // Configure WDT behavior: + // Bit 0 (SLEEP): 1 = Run in SLEEP mode (keep counting during WFE/WFI) + // Bit 3 (HALT): 1 = Run in HALT (debug) mode + // This is the safest configuration — WDT counts in all power states. + // + // NOTE: SLEEP_Run means the WDT keeps counting during board.sleep() / WFE. + // This is safe because the Adafruit nRF52 BSP runs FreeRTOS with a ~1ms + // tick timer that wakes the CPU from WFE frequently, so loop() and + // feedWatchdog() run well within the timeout window. If a future change + // enables FreeRTOS tickless idle or any sleep >timeout without waking, + // the timeout must be increased or SLEEP_Pause used instead. + NRF_WDT->CONFIG = (WDT_CONFIG_SLEEP_Run << WDT_CONFIG_SLEEP_Pos) | + (WDT_CONFIG_HALT_Run << WDT_CONFIG_HALT_Pos); + + NRF_WDT->CRV = ticks; + NRF_WDT->RREN = WDT_RREN_RR0_Enabled << WDT_RREN_RR0_Pos; + NRF_WDT->TASKS_START = 1; + + _wdt_running = true; + + // Initial kick so the full timeout window is available + NRF_WDT->RR[0] = WDT_RR_RR_Reload; + + MESH_DEBUG_PRINTLN("WDT: Started with %u second timeout", (unsigned)timeout_secs); } #ifdef NRF52_POWER_MANAGEMENT diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 96f67dc950..92b5394a3e 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #if defined(NRF52_PLATFORM) @@ -48,6 +49,14 @@ class NRF52Board : public mesh::MainBoard { NRF52Board(char *otaname) : ota_name(otaname) {} virtual void begin(); virtual uint8_t getStartupReason() const override { return startup_reason; } + + // Hardware watchdog — call feedWatchdog() every loop iteration. + // If the MCU hangs (HardFault, deadlock, infinite loop), the WDT will + // force a reset. timeout_secs=0 means don't start the WDT. + void initWatchdog(uint8_t timeout_secs); + inline void feedWatchdog() { if (_wdt_running) NRF_WDT->RR[0] = WDT_RR_RR_Reload; } + bool _wdt_running = false; + virtual float getMCUTemperature() override; virtual void reboot() override { NVIC_SystemReset(); } virtual bool getBootloaderVersion(char* version, size_t max_len) override; From 1cbab4a4cb33ed2abe49f4e8938637da2b4447f4 Mon Sep 17 00:00:00 2001 From: Patrick Winnertz Date: Sat, 25 Apr 2026 22:38:10 +0200 Subject: [PATCH 2/3] Move away from #if defined but create stubs so that the implementation can be easier added for other platforms later --- examples/simple_repeater/main.cpp | 2 -- examples/simple_room_server/main.cpp | 2 -- examples/simple_sensor/main.cpp | 2 -- src/MeshCore.h | 4 ++++ src/helpers/CommonCLI.h | 2 +- src/helpers/NRF52Board.h | 7 ++----- 6 files changed, 7 insertions(+), 12 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 676d7ef02a..1276c42735 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -95,9 +95,7 @@ void setup() { the_mesh.begin(fs); -#if defined(NRF52_PLATFORM) board.initWatchdog(the_mesh.getNodePrefs()->wdt_timeout_secs); -#endif #ifdef DISPLAY_CLASS ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index d83d54bd6a..a7fe8f0506 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -72,9 +72,7 @@ void setup() { the_mesh.begin(fs); -#if defined(NRF52_PLATFORM) board.initWatchdog(the_mesh.getNodePrefs()->wdt_timeout_secs); -#endif #ifdef DISPLAY_CLASS ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index b76d94ecf5..aad4433464 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -106,9 +106,7 @@ void setup() { the_mesh.begin(fs); -#if defined(NRF52_PLATFORM) board.initWatchdog(the_mesh.getNodePrefs()->wdt_timeout_secs); -#endif #ifdef DISPLAY_CLASS ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); diff --git a/src/MeshCore.h b/src/MeshCore.h index 2db1d4c3ec..577436741f 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -59,6 +59,10 @@ class MainBoard { virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; } virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported + // Watchdog interface — override on platforms that support hardware WDT + virtual void initWatchdog(uint8_t timeout_secs) { } + virtual void feedWatchdog() { } + // Power management interface (boards with power management override these) virtual bool isExternalPowered() { return false; } virtual uint16_t getBootVoltage() { return 0; } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 535aacd291..8334ad5769 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -61,7 +61,7 @@ struct NodePrefs { // persisted to file uint8_t rx_boosted_gain; // power settings uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; - uint8_t wdt_timeout_secs; // 0=disabled, 1-255=seconds (nRF52 only, applies on reboot) + uint8_t wdt_timeout_secs; // 0=disabled, 1-255=seconds (applies on reboot) }; class CommonCLICallbacks { diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 92b5394a3e..f5e85da42e 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -50,11 +50,8 @@ class NRF52Board : public mesh::MainBoard { virtual void begin(); virtual uint8_t getStartupReason() const override { return startup_reason; } - // Hardware watchdog — call feedWatchdog() every loop iteration. - // If the MCU hangs (HardFault, deadlock, infinite loop), the WDT will - // force a reset. timeout_secs=0 means don't start the WDT. - void initWatchdog(uint8_t timeout_secs); - inline void feedWatchdog() { if (_wdt_running) NRF_WDT->RR[0] = WDT_RR_RR_Reload; } + void initWatchdog(uint8_t timeout_secs) override; + inline void feedWatchdog() override { if (_wdt_running) NRF_WDT->RR[0] = WDT_RR_RR_Reload; } bool _wdt_running = false; virtual float getMCUTemperature() override; From 9af06a27699c217fd8e2625554a4839598d35967 Mon Sep 17 00:00:00 2001 From: Patrick Winnertz Date: Sat, 25 Apr 2026 22:44:17 +0200 Subject: [PATCH 3/3] Rename cli command from wdt to watchdog - easier to remember Use #ifdef just for setting/getting - this can be removed if the watchdog is implemented on the other platforms. --- src/helpers/CommonCLI.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 2b16921ce2..4c909db022 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -659,14 +659,18 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep savePrefs(); strcpy(reply, "OK"); } - } else if (memcmp(config, "wdt ", 4) == 0) { - uint32_t val = _atoi(&config[4]); + } else if (memcmp(config, "watchdog ", 9) == 0) { + uint32_t val = _atoi(&config[9]); if (val > 255) { strcpy(reply, "Error, must be 0 (off) or 1-255 (seconds)"); } else { _prefs->wdt_timeout_secs = (uint8_t)val; savePrefs(); - sprintf(reply, "OK - WDT %s (reboot to apply)", val == 0 ? "disabled" : "enabled"); +#if defined(NRF52_PLATFORM) + sprintf(reply, "OK - Watchdog %s (reboot to apply)", val == 0 ? "disabled" : "enabled"); +#else + sprintf(reply, "OK - Watchdog currently not implemented on this platform") +#endif } } else if (memcmp(config, "tx ", 3) == 0) { _prefs->tx_power_dbm = atoi(&config[3]); @@ -820,7 +824,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "> strict"); } - } else if (memcmp(config, "wdt", 3) == 0 && (config[3] == 0 || config[3] == ' ')) { + } else if (memcmp(config, "watchdog", 8) == 0 && (config[8] == 0 || config[8] == ' ')) { #if defined(NRF52_PLATFORM) sprintf(reply, "> %u secs%s", _prefs->wdt_timeout_secs, _prefs->wdt_timeout_secs == 0 ? " (disabled)" : " (active after reboot)");