Suspend the RAK3401 amplifier during emergency sleep

This commit is contained in:
mikecarper committed 2026-10-28 02:12:04 -07:00
1 parent 0c7dec9f19
commit f4f9453248
7 files changed
+125 -1

No files matched your search

+2
View File
@@ -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
+7
View File
@@ -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
+3
View File
@@ -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.
+2
View File
@@ -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);
+87 -1
View File
@@ -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 <string>
#include <vector>
using namespace mesh::wireless;
namespace power = mesh::power;
static std::vector<std::string> 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<std::string>{"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<std::string>{"cpu:10000"}));
cli.budget = 0; events.clear();
radio.failWake = true;
assert(!runtime.service(mesh, radio, board, cli, false));
std::vector<std::string> expected = {starting_level ? "fem:on" : "fem:off"};
if (starting_level) expected.push_back("backoff:1");
const std::vector<std::string> 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 <stdint.h>\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() {"):]
+17
View File
@@ -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
+7
View File
@@ -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 {