Suspend the RAK3401 amplifier during emergency sleep
This commit is contained in:
1 parent
0c7dec9f19
commit
f4f9453248
7 files changed
+125
-1
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {"):]
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user