diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index e37078ce5f..1276c42735 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -95,6 +95,8 @@ void setup() { the_mesh.begin(fs); + board.initWatchdog(the_mesh.getNodePrefs()->wdt_timeout_secs); + #ifdef DISPLAY_CLASS ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif @@ -106,6 +108,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..a7fe8f0506 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -72,6 +72,8 @@ void setup() { the_mesh.begin(fs); + board.initWatchdog(the_mesh.getNodePrefs()->wdt_timeout_secs); + #ifdef DISPLAY_CLASS ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif @@ -83,6 +85,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..aad4433464 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -106,6 +106,8 @@ void setup() { the_mesh.begin(fs); + board.initWatchdog(the_mesh.getNodePrefs()->wdt_timeout_secs); + #ifdef DISPLAY_CLASS ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif @@ -117,6 +119,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/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.cpp b/src/helpers/CommonCLI.cpp index b71afc72e2..4c909db022 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,19 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep savePrefs(); strcpy(reply, "OK"); } + } 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(); +#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]); savePrefs(); @@ -808,6 +824,13 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "> strict"); } + } 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)"); +#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..8334ad5769 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 (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..f5e85da42e 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,11 @@ class NRF52Board : public mesh::MainBoard { NRF52Board(char *otaname) : ota_name(otaname) {} virtual void begin(); virtual uint8_t getStartupReason() const override { return startup_reason; } + + 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; virtual void reboot() override { NVIC_SystemReset(); } virtual bool getBootloaderVersion(char* version, size_t max_len) override;