diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 567edd71c..3412c1528 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -3134,6 +3134,8 @@ five seconds for its reply. `power.awake` keeps the node up for the next given number of minutes (0 cancels; maximum 10080), even outside the hourly window. That override is held in RAM and ends on reboot; it never disables hard low-voltage shutdown. Use it before maintenance that needs continuous uptime. +LoRa commands must arrive during an awake window. Local USB commands can wait +up to one ten-second maintenance interval while the node is sleeping. `get power.emergency` reports the mode, battery estimate, desired `window`, actual radio ownership, seconds until the next awake window, and remaining diff --git a/src/Dispatcher.h b/src/Dispatcher.h index a145719c3..1c1784089 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -20,6 +20,10 @@ namespace mesh { +#if MESH_ENABLE_EMERGENCY_POWER +namespace power { class EmergencyPowerRuntime; } +#endif + enum class RadioParamApplyResult : uint8_t { APPLIED, BUSY, @@ -421,6 +425,9 @@ typedef uint32_t DispatcherAction; * and scheduling of outbound Packets. */ class Dispatcher { +#if MESH_ENABLE_EMERGENCY_POWER + friend class power::EmergencyPowerRuntime; +#endif friend class FleetChannel; // bounded deferred receiver and exact reply retirement enum class OutboundCancellation : uint8_t { None, Cancelled, Delivered }; Packet* outbound; // current outbound packet diff --git a/src/MeshCore.h b/src/MeshCore.h index 423d370b4..532b47cf7 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -211,6 +211,9 @@ public: (void)millisecs; return false; } + // Quiesce only board-specific RF controls after owned warm radio sleep; + // restore their prior state before the caller wakes the radio. + virtual void setEmergencyRadioSleeping(bool sleeping) { (void)sleeping; } #endif // Called by example setup() functions to signal that boot is complete. // Boards may override to stop a boot-indicator LED sequence or similar. diff --git a/src/helpers/EmergencyPowerRuntime.h b/src/helpers/EmergencyPowerRuntime.h index 3a79508af..cbf853889 100644 --- a/src/helpers/EmergencyPowerRuntime.h +++ b/src/helpers/EmergencyPowerRuntime.h @@ -22,6 +22,7 @@ public: if (budget == 0 || active_operation) { if (radio_asleep_) { if (radio_was_available_) { + board.setEmergencyRadioSleeping(false); if (!radio.setTrackerSleep(false)) radio.recoverRadio(true); mesh.setRadioAvailable(true); } @@ -40,6 +41,7 @@ public: if (radio_was_available_) { if (!radio.setTrackerSleep(true)) return false; mesh.setRadioAvailable(false); + board.setEmergencyRadioSleeping(true); } radio_asleep_ = true; cli.setEmergencyRadioSleeping(true); diff --git a/test/test_emergency_power_runtime.py b/test/test_emergency_power_runtime.py index 81fe9f27b..131d882ae 100644 --- a/test/test_emergency_power_runtime.py +++ b/test/test_emergency_power_runtime.py @@ -1,10 +1,13 @@ """Run actual emergency RF/wireless ownership and restoration with fake hardware.""" from pathlib import Path import os +import re import subprocess import tempfile import unittest +from test_replay_reset_integration import extract_braced + ROOT = Path(__file__).resolve().parents[1] HARNESS = r''' @@ -15,6 +18,7 @@ HARNESS = r''' #include #include using namespace mesh::wireless; +namespace power = mesh::power; static std::vector events; void delay(uint32_t milliseconds) { events.push_back("backoff:" + std::to_string(milliseconds)); @@ -40,6 +44,8 @@ struct Mesh { bool available = true, inFlight = false; unsigned queuedFuturePackets = 20; bool hasOutbound() const { return inFlight; } +protected: + @RUNTIME_FRIEND@ bool isRadioAvailable() const { return available; } void setRadioAvailable(bool value) { events.push_back(value ? "mesh:on" : "mesh:off"); available = value; @@ -65,11 +71,30 @@ struct Board { bool supportsEmergencySleep() const { return supported; } bool isOTAUpdateRunning() const { return ota; } bool isRadioTestActive() const { return radioTest; } + virtual void setEmergencyRadioSleeping(bool) {} bool emergencySleepMilliseconds(uint32_t budget) { events.push_back("cpu:" + std::to_string(budget)); ++sleeps; lastBudget = budget; return sleepOk; } }; +static constexpr int HIGH = 1, LOW = 0; +static constexpr unsigned SX126X_POWER_EN = @FEM_PIN@, PIN_3V3_EN = @RAIL_PIN@; +static int levels[64] = {}; +static int digitalRead(unsigned pin) { + assert(pin == SX126X_POWER_EN); + events.push_back("fem:read"); + return levels[pin]; +} +static void digitalWrite(unsigned pin, int value) { + assert(pin == SX126X_POWER_EN); // Shared IO2/storage rail must never change. + levels[pin] = value; + events.push_back(value ? "fem:on" : "fem:off"); +} +struct RAK3401Board : Board { + @RAK_FIELDS@ + void setEmergencyRadioSleeping(bool sleeping) override; +}; +@RAK_FEM_METHOD@ struct CLI { uint32_t budget = 2700000; bool radioSleeping = false; @@ -273,6 +298,49 @@ int main(int argc, char** argv) { events.clear(); f.cli.budget = 0; assert(!f.service() && f.mesh.available && !f.cli.radioSleeping); assert(f.wireless.active == All); + } else if (scenario == 10) { + for (int starting_level : {LOW, HIGH}) { + Wireless wireless; control().begin(wireless); + Mesh mesh; Radio radio; RAK3401Board board; CLI cli; + mesh::power::EmergencyPowerRuntime runtime; + levels[SX126X_POWER_EN] = starting_level; levels[PIN_3V3_EN] = HIGH; + events.clear(); + // Failed warm sleep never captures/disables the FEM. + radio.failSleep = true; + assert(!runtime.service(mesh, radio, board, cli, false)); + assert(levels[SX126X_POWER_EN] == starting_level); + assert(events.back() == "radio:sleep"); + radio.failSleep = false; events.clear(); + assert(runtime.service(mesh, radio, board, cli, false)); + assert((events == std::vector{"wifi:off", "espnow:off", "ble:off", + "radio:sleep", "mesh:off", "fem:read", "fem:off", "status:sleep", "cpu:10000"})); + assert(levels[SX126X_POWER_EN] == LOW && levels[PIN_3V3_EN] == HIGH); + events.clear(); + // Maintenance and duplicate suspension must not overwrite the snapshot. + board.setEmergencyRadioSleeping(true); + assert(runtime.service(mesh, radio, board, cli, false)); + assert((events == std::vector{"cpu:10000"})); + cli.budget = 0; events.clear(); + radio.failWake = true; + assert(!runtime.service(mesh, radio, board, cli, false)); + std::vector expected = {starting_level ? "fem:on" : "fem:off"}; + if (starting_level) expected.push_back("backoff:1"); + const std::vector after = {"radio:wake", "radio:recover", "mesh:on", + "status:awake", "espnow:on", "wifi:on", "ble:on"}; + expected.insert(expected.end(), after.begin(), after.end()); + assert(events == expected); + assert(levels[SX126X_POWER_EN] == starting_level && levels[PIN_3V3_EN] == HIGH); + events.clear(); board.setEmergencyRadioSleeping(false); assert(events.empty()); + } + Wireless wireless; control().begin(wireless); + Mesh mesh; mesh.available = false; // Tracker owns RF; its FEM is untouched. + Radio radio; RAK3401Board board; CLI cli; + mesh::power::EmergencyPowerRuntime runtime; + levels[SX126X_POWER_EN] = HIGH; events.clear(); + assert(runtime.service(mesh, radio, board, cli, false)); + cli.budget = 0; assert(!runtime.service(mesh, radio, board, cli, false)); + for (const auto& event : events) assert(event.find("fem:") == std::string::npos); + assert(levels[SX126X_POWER_EN] == HIGH); } else assert(false); } ''' @@ -286,7 +354,22 @@ class EmergencyPowerRuntimeTests(unittest.TestCase): source, cls.binary = work / "main.cpp", work / "check" (work / "Arduino.h").write_text( "#pragma once\n#include \nvoid delay(uint32_t);\n", encoding="ascii") - source.write_text(HARNESS, encoding="ascii") + board_header = (ROOT / "variants/rak3401/RAK3401Board.h").read_text() + board_source = (ROOT / "variants/rak3401/RAK3401Board.cpp").read_text() + dispatcher = (ROOT / "src/Dispatcher.h").read_text() + variant = (ROOT / "variants/rak3401/variant.h").read_text() + fields = re.search(r"class RAK3401Board[^\n]*\{\n#if MESH_ENABLE_EMERGENCY_POWER\n(.*?)#endif", + board_header, re.S).group(1) + code = HARNESS.replace("@RAK_FIELDS@", fields) + code = code.replace("@RAK_FEM_METHOD@", extract_braced( + board_source, "void RAK3401Board::setEmergencyRadioSleeping(")) + code = code.replace("@RUNTIME_FRIEND@", re.search( + r"friend class power::EmergencyPowerRuntime;", dispatcher).group(0)) + for marker, name, body in (("FEM_PIN", "SX126X_POWER_EN", variant), + ("RAIL_PIN", "PIN_3V3_EN", board_header)): + pin = re.search(rf"#define\s+{name}\s+\(?\s*(\d+)", body).group(1) + code = code.replace("@" + marker + "@", pin) + source.write_text(code, encoding="ascii") result = subprocess.run([ os.environ.get("CXX", "g++"), "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-Werror", "-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-omit-frame-pointer", @@ -336,6 +419,9 @@ class EmergencyPowerRuntimeTests(unittest.TestCase): def test_cpu_sleep_refusal_yields_without_resuming_normal_rf_work(self): self.scenario(9) + def test_rak3401_actual_fem_pin_restores_before_radio_wake_without_cutting_shared_rail(self): + self.scenario(10) + def test_room_maintenance_runs_before_emergency_early_return(self): source = (ROOT / "examples/simple_room_server/main.cpp").read_text() loop = source[source.index("void loop() {"):] diff --git a/variants/rak3401/RAK3401Board.cpp b/variants/rak3401/RAK3401Board.cpp index 434aaf559..a8d8c255f 100644 --- a/variants/rak3401/RAK3401Board.cpp +++ b/variants/rak3401/RAK3401Board.cpp @@ -153,3 +153,20 @@ void RAK3401Board::enableRadioFrontend() { digitalWrite(SX126X_POWER_EN, HIGH); delay(1); } + +#if MESH_ENABLE_EMERGENCY_POWER +void RAK3401Board::setEmergencyRadioSleeping(bool sleeping) { + if (sleeping) { + if (emergency_fem_suspended) return; + emergency_fem_was_enabled = digitalRead(SX126X_POWER_EN) == HIGH; + // IO3 controls only the SKY66122 CSD+CPS. Keep the shared IO2 rail live + // for the radio core, storage and any fitted sensors. + digitalWrite(SX126X_POWER_EN, LOW); + emergency_fem_suspended = true; + } else if (emergency_fem_suspended) { + digitalWrite(SX126X_POWER_EN, emergency_fem_was_enabled ? HIGH : LOW); + if (emergency_fem_was_enabled) delay(1); + emergency_fem_suspended = false; + } +} +#endif diff --git a/variants/rak3401/RAK3401Board.h b/variants/rak3401/RAK3401Board.h index 74764501a..592118664 100644 --- a/variants/rak3401/RAK3401Board.h +++ b/variants/rak3401/RAK3401Board.h @@ -12,6 +12,10 @@ #define WB_IO2 PIN_3V3_EN class RAK3401Board : public NRF52BoardDCDC { +#if MESH_ENABLE_EMERGENCY_POWER + bool emergency_fem_suspended = false; + bool emergency_fem_was_enabled = false; +#endif protected: #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; @@ -22,6 +26,9 @@ public: void shutdownPeripherals() override; bool recoverRadio(); void enableRadioFrontend(); +#if MESH_ENABLE_EMERGENCY_POWER + void setEmergencyRadioSleeping(bool sleeping) override; +#endif bool supportsRadioHardReset() const override { return true; } bool resetRadio() override { return recoverRadio(); } bool finishRadioHardReset() override {