mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-11-04 10:57:27 +00:00
Fix the STM32 flash overflows and fleet fixture compile failure from https://github.com/mikecarper/MeshCore/actions/runs/40241742182. Fold already-unsupported RX control paths only for the integrated STM32WL wrapper. Preserve RXPS-off validation, persistence, pending application and rollback. External radios retain their supported controls. Use measured GCC 14 code sharing in the four small STM32 USB Companion recipes. Keep firmware features, contact/channel capacities, filesystem boundaries and flash/RAM admission checks unchanged. Refresh downstream host fixtures for saved radio intent, deferred service restarts, WebConfig ownership, RX recovery and color tint state. Retain or strengthen behavior checks rather than skipping failing coverage. Validation: canonical RAK3x72, Wio Mini, Wio Dev and Tiny Relay USB Companion packages and Mini Repeater build passed at release-version length. All post-failure host checks, compact arithmetic/float checks, build-profile and real PlatformIO recipe/path/RAM-policy checks passed locally.
487 lines
21 KiB
Python
487 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
"""Execute real sleep guards and nRF52 wait calls with diagnostics on and off."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_replay_reset_integration import extract_braced
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
class PowerSavingRoleDiagnosticsTests(unittest.TestCase):
|
|
def execute(self, code, defines=()):
|
|
with tempfile.TemporaryDirectory(prefix="meshcore-power-role-") as directory:
|
|
source = Path(directory) / "test.cpp"
|
|
binary = Path(directory) / "test"
|
|
source.write_text(code, encoding="ascii")
|
|
result = subprocess.run([
|
|
os.environ.get("CXX", "c++"), "-std=c++17", "-Wall", "-Wextra",
|
|
"-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
|
"-fno-pie", "-no-pie", "-I", str(ROOT / "src"),
|
|
*["-D" + value for value in defines], str(source), "-o", str(binary),
|
|
], capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=20)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
|
|
def test_actual_radio_default_and_public_dispatcher_deadline_forwarder(self):
|
|
header = (ROOT / 'src/Dispatcher.h').read_text(encoding='ascii')
|
|
source = (ROOT / 'src/Dispatcher.cpp').read_text(encoding='ascii')
|
|
conservative = extract_braced(header, 'virtual bool getNoiseFloorWakeDelay(')
|
|
forwarder = extract_braced(source, 'bool Dispatcher::getNextNoiseFloorWakeDelay(')
|
|
self.execute(r'''
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
struct Radio {
|
|
bool calibrating = false;
|
|
bool isCalibratingNoiseFloor() const { return calibrating; }
|
|
@DEFAULT@
|
|
};
|
|
struct SpacedRadio : Radio {
|
|
mutable unsigned reads = 0;
|
|
uint32_t delay = 50;
|
|
bool getNoiseFloorWakeDelay(uint32_t now, uint32_t& out) const override {
|
|
assert(now == 123); ++reads; out = delay; return true;
|
|
}
|
|
};
|
|
struct Clock {
|
|
unsigned reads = 0;
|
|
uint32_t getMillis() { ++reads; return 123; }
|
|
};
|
|
struct Dispatcher {
|
|
bool radio_available = true;
|
|
Radio* _radio;
|
|
Clock* _ms;
|
|
bool getNextNoiseFloorWakeDelay(uint32_t&) const;
|
|
};
|
|
@FORWARDER@
|
|
int main() {
|
|
Radio legacy;
|
|
Clock clock;
|
|
Dispatcher node{true, &legacy, &clock};
|
|
uint32_t delay = 71;
|
|
assert(!node.getNextNoiseFloorWakeDelay(delay) && delay == 71);
|
|
legacy.calibrating = true;
|
|
assert(node.getNextNoiseFloorWakeDelay(delay) && delay == 0);
|
|
SpacedRadio spaced;
|
|
node._radio = &spaced;
|
|
assert(node.getNextNoiseFloorWakeDelay(delay) && delay == 50 && spaced.reads == 1);
|
|
node.radio_available = false;
|
|
const auto before = clock.reads;
|
|
assert(!node.getNextNoiseFloorWakeDelay(delay));
|
|
assert(clock.reads == before && spaced.reads == 1);
|
|
}
|
|
'''.replace('@DEFAULT@', conservative).replace('@FORWARDER@', forwarder))
|
|
|
|
def test_actual_role_guards_preserve_short_circuit_and_ignore_charger_only(self):
|
|
for role in ("simple_repeater", "simple_room_server", "simple_sensor"):
|
|
with self.subTest(role=role):
|
|
source = (ROOT / f"examples/{role}/main.cpp").read_text(encoding="ascii")
|
|
start = source.index(" const auto sleep_guard =")
|
|
end = source.index(" if (can_power_save)", start)
|
|
guard = source[start:end]
|
|
sensor = role == "simple_sensor"
|
|
code = r'''
|
|
#include <cassert>
|
|
#include <helpers/PowerSavingDiagnostics.h>
|
|
static uint32_t millis() { return 100; }
|
|
static bool morse_timing = false, logging_armed = false;
|
|
struct Prefs { bool powersaving_enabled = true; };
|
|
struct Mesh {
|
|
Prefs prefs; bool tracker = false;
|
|
Prefs* getNodePrefs() { return &prefs; }
|
|
bool isTrackerModeEnabled() const { return tracker; }
|
|
} the_mesh;
|
|
struct Board {
|
|
bool usb = false, charger = true;
|
|
unsigned usb_reads = 0, charger_reads = 0;
|
|
bool isUsbDataConnected() { ++usb_reads; return usb; }
|
|
bool isExternalPowered() { ++charger_reads; return charger; }
|
|
} board;
|
|
namespace mesh {
|
|
bool isUsbLoggingWatchdogArmed() { return logging_armed; }
|
|
namespace wireless {
|
|
struct Control { bool busy = false; unsigned reads = 0;
|
|
bool pending() { ++reads; return busy; }
|
|
};
|
|
Control& control() { static Control value; return value; }
|
|
}}
|
|
static bool probe() {
|
|
@GUARD@
|
|
return can_power_save;
|
|
}
|
|
int main() {
|
|
using namespace mesh::powerdiag;
|
|
for (unsigned flags = 0; flags != 64; ++flags) {
|
|
the_mesh.prefs.powersaving_enabled = !(flags & 1);
|
|
the_mesh.tracker = flags & 2;
|
|
morse_timing = flags & 4;
|
|
board.usb = flags & 8;
|
|
logging_armed = flags & 16;
|
|
mesh::wireless::control().busy = flags & 32;
|
|
const bool policy = the_mesh.prefs.powersaving_enabled || (@SENSOR@ && the_mesh.tracker);
|
|
const bool expected = policy && !morse_timing && !board.usb && !logging_armed
|
|
&& (@SENSOR@ || !mesh::wireless::control().busy);
|
|
unsigned off_usb = 0, off_wireless = 0;
|
|
for (bool diagnostics : {false, true}) {
|
|
setEnabled(diagnostics, millis()); reset(millis());
|
|
board.usb_reads = board.charger_reads = mesh::wireless::control().reads = 0;
|
|
assert(probe() == expected);
|
|
assert(board.charger_reads == 0); // USB power alone is not a data session.
|
|
if (!diagnostics) {
|
|
off_usb = board.usb_reads;
|
|
off_wireless = mesh::wireless::control().reads;
|
|
} else {
|
|
assert(board.usb_reads == off_usb);
|
|
assert(mesh::wireless::control().reads == off_wireless);
|
|
if (!expected) {
|
|
const Reason reason = !policy ? Reason::PolicyOff
|
|
: morse_timing ? Reason::Morse : board.usb ? Reason::UsbSession
|
|
: logging_armed ? Reason::UsbRecovery : Reason::Wireless;
|
|
assert(snapshot(millis()).current == mask(reason));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
'''.replace("@GUARD@", guard).replace("@SENSOR@", "true" if sensor else "false")
|
|
code = code.replace("#include <cassert>", "#include <cassert>\n#include <initializer_list>")
|
|
self.execute(code)
|
|
|
|
def test_repeater_actual_pending_reasons_keep_suspended_rs232_sleepable(self):
|
|
source = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text(encoding="ascii")
|
|
methods = "\n".join(extract_braced(source, signature) for signature in (
|
|
"bool MyMesh::hasPendingWork() const",
|
|
"mesh::powerdiag::Mask MyMesh::pendingWorkReason() const",
|
|
"uint32_t MyMesh::getPowerSaveSleepMilliseconds(uint32_t max_ms) const",
|
|
"uint32_t MyMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const",
|
|
))
|
|
code = r'''
|
|
#include <cassert>
|
|
#include <initializer_list>
|
|
#include <helpers/PowerSavingDiagnostics.h>
|
|
#include <helpers/PowerManagementUtils.h>
|
|
static uint32_t millis() { return 100; }
|
|
#define WITH_BRIDGE 1
|
|
#define WITH_RS232_BRIDGE 1
|
|
#define ETHERNET_ENABLED 1
|
|
struct AbstractBridge {
|
|
bool running = false; uint32_t wake_ms = 60000;
|
|
bool isRunning() const { return running; }
|
|
uint32_t powerWakeDelayMs(uint32_t) const { return wake_ms; }
|
|
};
|
|
struct MyMesh {
|
|
bool dual = false, profile_idle = false, ota = false, queue = false, retry = false, scheduled = false;
|
|
bool bridge_enabled = true;
|
|
bool queue_delay_set = false, retry_delay_set = false;
|
|
uint32_t queue_delay = 0, retry_delay = 0, calibration_delay = 0;
|
|
struct { bool busy_value = false; bool busy() const { return busy_value; } } _local_cli_output;
|
|
struct { bool pending = false; } deferred_cli_command;
|
|
struct {
|
|
bool gpio = false, tx_pending = false, rx_pending = false;
|
|
bool hasActiveUserGpioTimer() const { return gpio; }
|
|
bool hasPendingRadioSettings() const { return tx_pending || rx_pending; }
|
|
} _cli;
|
|
struct { bool watchdog = false, calibration = false;
|
|
bool isWatchdogObserving() const { return watchdog; }
|
|
bool isCalibratingNoiseFloor() const { return calibration; }
|
|
} radio_driver;
|
|
bool pending_self_advert = false;
|
|
struct { bool battery_alert_enabled = false; } _prefs;
|
|
unsigned long next_flood_advert = 0, next_local_advert = 0, dirty_contacts_expiry = 0;
|
|
unsigned long next_recent_repeater_sweep = 0, next_battery_alert_check = 0;
|
|
AbstractBridge uart; AbstractBridge* bridge = &uart;
|
|
uint8_t active_rs232_bridge_uart = 0;
|
|
const AbstractBridge* activeBridge() const { return bridge; }
|
|
bool rs232BridgeEnabled() const { return bridge_enabled; }
|
|
bool isDualRadioActive() const { return dual; }
|
|
bool canIdleDualRadio() const { return !dual || profile_idle; }
|
|
bool hasPendingOtaApply() const { return ota; }
|
|
bool hasQueuedWorkDue() const { return queue; }
|
|
bool hasRetryWorkDue() const { return retry; }
|
|
bool hasScheduledRadioWorkDue() const { return scheduled; }
|
|
bool isMillisTimerDue(unsigned long value) const { return value && value <= millis(); }
|
|
bool getNextQueueWakeDelay(uint32_t& value) const { value = queue_delay; return queue_delay_set; }
|
|
bool getNextRetryWakeDelay(uint32_t& value) const { value = retry_delay; return retry_delay_set; }
|
|
bool getNextNoiseFloorWakeDelay(uint32_t& value) const {
|
|
value = calibration_delay; return radio_driver.calibration;
|
|
}
|
|
uint32_t limitSleepToMillisTimer(unsigned long value, uint32_t seconds) const {
|
|
return value && value <= millis() ? 0 : seconds;
|
|
}
|
|
uint32_t limitSleepToScheduledRadioWork(uint32_t value) const { return value; }
|
|
bool hasPendingWork() const;
|
|
mesh::powerdiag::Mask pendingWorkReason() const;
|
|
uint32_t getPowerSaveSleepMilliseconds(uint32_t max_ms) const;
|
|
uint32_t getPowerSaveSleepSeconds(uint32_t max_secs) const;
|
|
};
|
|
@METHODS@
|
|
int main() {
|
|
using namespace mesh::powerdiag;
|
|
for (bool diagnostics : {false, true}) {
|
|
setEnabled(diagnostics, millis()); reset(millis());
|
|
MyMesh node;
|
|
assert(node.bridge_enabled && !node.uart.running);
|
|
assert(!node.hasPendingWork()); // Configured but suspended is not active UART ownership.
|
|
assert(node.getPowerSaveSleepSeconds(30) == 30);
|
|
auto check = [&](bool& flag, Reason why) {
|
|
flag = true;
|
|
assert(node.hasPendingWork());
|
|
assert(node.pendingWorkReason() == mask(why));
|
|
assert(node.getPowerSaveSleepSeconds(30) == 0);
|
|
if (diagnostics) assert(snapshot(millis()).current == mask(why));
|
|
flag = false;
|
|
};
|
|
check(node.dual, Reason::DualRadio); check(node.ota, Reason::Ota);
|
|
node.dual = true; node.profile_idle = true;
|
|
assert(!node.hasPendingWork());
|
|
check(node.ota, Reason::Ota); // An idle scanner does not bypass other role guards.
|
|
check(node.queue, Reason::Queue); check(node.retry, Reason::Retry);
|
|
node.dual = node.profile_idle = false;
|
|
check(node._local_cli_output.busy_value, Reason::SerialIO);
|
|
check(node.deferred_cli_command.pending, Reason::Scheduled);
|
|
check(node._cli.tx_pending, Reason::Scheduled);
|
|
check(node._cli.rx_pending, Reason::Scheduled);
|
|
check(node.uart.running, Reason::Bridge);
|
|
node.active_rs232_bridge_uart = 1;
|
|
assert(node.pendingWorkReason() == mask(Reason::BridgeCleanup));
|
|
node.active_rs232_bridge_uart = 0;
|
|
node.uart.wake_ms = 0;
|
|
assert(node.pendingWorkReason() == mask(Reason::BridgeWake));
|
|
node.uart.wake_ms = 500;
|
|
assert(!node.hasPendingWork());
|
|
reset(millis());
|
|
assert(node.getPowerSaveSleepMilliseconds(30000) == 500);
|
|
if (diagnostics) assert(snapshot(millis()).current == 0);
|
|
node.uart.wake_ms = 60000;
|
|
check(node.radio_driver.watchdog, Reason::RadioWatchdog);
|
|
check(node.radio_driver.calibration, Reason::Calibration);
|
|
node.radio_driver.calibration = true; node.calibration_delay = 50;
|
|
assert(!node.hasPendingWork() && node.getPowerSaveSleepSeconds(30) == 30);
|
|
node.calibration_delay = 0;
|
|
assert(node.pendingWorkReason() == mask(Reason::Calibration));
|
|
node.radio_driver.calibration = false;
|
|
check(node.queue, Reason::Queue); check(node.retry, Reason::Retry);
|
|
check(node.scheduled, Reason::Scheduled);
|
|
node.queue_delay_set = true; node.queue_delay = 500;
|
|
reset(millis());
|
|
assert(!node.hasPendingWork() && node.getPowerSaveSleepMilliseconds(30000) == 500);
|
|
if (diagnostics) assert(snapshot(millis()).current == 0);
|
|
node.queue_delay = 0;
|
|
assert(node.getPowerSaveSleepMilliseconds(30000) == 0);
|
|
if (diagnostics) assert(snapshot(millis()).current == mask(Reason::Queue));
|
|
node.queue_delay_set = false; node.retry_delay_set = true; node.retry_delay = 500;
|
|
reset(millis());
|
|
assert(node.getPowerSaveSleepMilliseconds(30000) == 500);
|
|
if (diagnostics) assert(snapshot(millis()).current == 0);
|
|
node.retry_delay = 0;
|
|
assert(node.getPowerSaveSleepMilliseconds(30000) == 0);
|
|
if (diagnostics) assert(snapshot(millis()).current == mask(Reason::Retry));
|
|
node.retry_delay_set = false; node.dirty_contacts_expiry = millis();
|
|
assert(node.pendingWorkReason() == mask(Reason::Scheduled));
|
|
}
|
|
}
|
|
'''.replace("@METHODS@", methods)
|
|
self.execute(code)
|
|
|
|
def test_nrf52_real_wait_sequence_veto_and_sdk_errors_are_observational(self):
|
|
source = (ROOT / "src/helpers/NRF52Board.cpp").read_text(encoding="ascii")
|
|
method = extract_braced(source, "void NRF52Board::sleep(uint32_t secs)")
|
|
code = r'''
|
|
#include <cassert>
|
|
#include <cstring>
|
|
#include <helpers/PowerSavingDiagnostics.h>
|
|
#include <helpers/nrf52/SleepCycleState.h>
|
|
static uint32_t now_us = 1000, wait_duration = 547, sdk_error = 0;
|
|
static bool softdevice = true;
|
|
static char sequence[32] = {}; static unsigned steps = 0, yields = 0;
|
|
static uint32_t micros() { return now_us; }
|
|
static uint32_t millis() { return now_us / 1000; }
|
|
static void delay(unsigned value) { yields += value; }
|
|
constexpr uint32_t NRF_SUCCESS = 0;
|
|
namespace mesh_nrf52 {
|
|
void softdeviceIsEnabled(uint8_t& value) { value = softdevice; }
|
|
}
|
|
static uint32_t sd_app_evt_wait() { sequence[steps++] = 'A'; now_us += wait_duration; return sdk_error; }
|
|
static void __WFE() { sequence[steps++] = 'W'; now_us += 20; }
|
|
static void __SEV() { sequence[steps++] = 'S'; }
|
|
struct NRF52Board {
|
|
bool test = false;
|
|
bool isRadioTestActive() const { return test; }
|
|
void sleep(uint32_t secs);
|
|
};
|
|
@METHOD@
|
|
static void prepare() {
|
|
auto& state = mesh_nrf52::sleepCycleState();
|
|
const uint32_t token = state.begin();
|
|
assert(state.publish(token, softdevice, 0));
|
|
}
|
|
int main() {
|
|
using namespace mesh::powerdiag;
|
|
NRF52Board board;
|
|
prepare();
|
|
board.sleep(0);
|
|
assert(steps == 1 && sequence[0] == 'A');
|
|
assert(snapshot(millis()).calls == 0);
|
|
setEnabled(true, millis()); reset(millis());
|
|
prepare();
|
|
board.sleep(0);
|
|
auto value = snapshot(millis());
|
|
assert(value.calls == 1 && value.returns == 1 && value.last_call_us == 547);
|
|
assert(value.errors == 0 && value.board_vetoes == 0);
|
|
board.test = true;
|
|
prepare();
|
|
board.sleep(0);
|
|
value = snapshot(millis());
|
|
assert(steps == 2 && yields == 1);
|
|
assert(value.calls == 1 && value.board_vetoes == 1 && value.detail == 2);
|
|
assert(value.current & mask(Reason::Board));
|
|
board.test = false; sdk_error = 17;
|
|
prepare();
|
|
board.sleep(0);
|
|
value = snapshot(millis());
|
|
assert(value.errors == 1 && value.detail == 17 && value.calls == 2 && value.returns == 2);
|
|
softdevice = false; steps = 0; memset(sequence, 0, sizeof(sequence));
|
|
prepare();
|
|
board.sleep(0);
|
|
assert(!strcmp(sequence, "WSW")); // Do not erase a radio GPIO edge with SEV/WFE/WFE.
|
|
value = snapshot(millis());
|
|
assert(value.calls == 3 && value.returns == 3 && value.last_call_us == 40);
|
|
assert(value.short_returns == 1);
|
|
}
|
|
'''.replace("@METHOD@", method)
|
|
self.execute(code)
|
|
|
|
def test_esp32_real_veto_and_sdk_failure_timing_does_not_invent_blocked_residency(self):
|
|
source = (ROOT / "src/helpers/ESP32Board.h").read_text(encoding="ascii")
|
|
method = "\n".join(extract_braced(source, signature) for signature in (
|
|
"void sleep(uint32_t secs) override",
|
|
"void sleepMilliseconds(uint32_t millisecs) override",
|
|
"void sleepMicroseconds(uint64_t wake_us, bool require_timer = false)",
|
|
"void sleepUntilMicroseconds(uint32_t deadline_us)",
|
|
"void sleepMicrosecondsImpl(uint64_t wake_us, bool require_timer,",
|
|
))
|
|
code = r'''
|
|
#include <cassert>
|
|
#include <helpers/PowerSavingDiagnostics.h>
|
|
#include <helpers/esp32/PowerTestControls.h>
|
|
#define ARDUINO_USB_CDC_ON_BOOT 1
|
|
#define MESH_USB_LOGGING_AVAILABLE 1
|
|
using esp_err_t = int;
|
|
using gpio_num_t = int;
|
|
constexpr int ESP_OK = 0, HIGH = 1, LOW = 0;
|
|
constexpr int USER_BTN_PRESSED = LOW;
|
|
constexpr int GPIO_INTR_HIGH_LEVEL = 1, GPIO_INTR_LOW_LEVEL = 2;
|
|
constexpr int GPIO_INTR_POSEDGE = 3, GPIO_INTR_DISABLE = 0;
|
|
static uint32_t now_us = 1000, sleep_duration = 7000;
|
|
static int timer_error = 0, gpio_error = 0, pin_error = 0, sleep_error = 0;
|
|
static unsigned sdk_calls = 0, yields = 0, critical_depth = 0, irq_reads = 0;
|
|
static bool radio_pending = false;
|
|
static uint32_t millis() { return now_us / 1000; }
|
|
static uint32_t micros() { return now_us; }
|
|
static void delay(unsigned ms) { yields += ms; }
|
|
static uint64_t timer_us = 0;
|
|
static esp_err_t esp_sleep_enable_timer_wakeup(uint64_t value) { timer_us = value; return timer_error; }
|
|
static esp_err_t esp_sleep_enable_gpio_wakeup() { return gpio_error; }
|
|
static esp_err_t gpio_wakeup_enable(gpio_num_t, int) { return pin_error; }
|
|
static void gpio_wakeup_disable(gpio_num_t) {}
|
|
static void gpio_set_intr_type(gpio_num_t, int) {}
|
|
static int gpio_get_level(gpio_num_t) { ++irq_reads; return radio_pending ? HIGH : LOW; }
|
|
static esp_err_t esp_light_sleep_start() { ++sdk_calls; now_us += sleep_duration; return sleep_error; }
|
|
#define portENTER_CRITICAL(mux) (++critical_depth)
|
|
#define portEXIT_CRITICAL(mux) (--critical_depth)
|
|
namespace mesh {
|
|
static bool logging = false;
|
|
static bool isUsbLoggingEnabled() { return logging; }
|
|
namespace esp32 {
|
|
static powerdiag::Reason manualLightSleepBlocker() { return powerdiag::Reason::Count; }
|
|
}
|
|
}
|
|
struct MainBoard {
|
|
virtual void sleep(uint32_t) = 0;
|
|
virtual void sleepMilliseconds(uint32_t) = 0;
|
|
};
|
|
struct ESP32Board : MainBoard {
|
|
bool inhibit_sleep = false, usb = false, test = false;
|
|
uint32_t uart_quiet_restore_at = 0;
|
|
uint32_t irq = 14;
|
|
bool isUsbSleepHeld() { return usb; }
|
|
bool isRadioTestActive() const { return test; }
|
|
uint32_t getIRQGpio() const { return irq; }
|
|
@METHOD@
|
|
};
|
|
int main() {
|
|
using namespace mesh::powerdiag;
|
|
ESP32Board board;
|
|
board.sleep(30);
|
|
assert(sdk_calls == 1 && snapshot(millis()).calls == 0);
|
|
assert(timer_us == 30000000ULL);
|
|
board.sleepMilliseconds(50);
|
|
assert(sdk_calls == 2 && timer_us == 50000ULL);
|
|
board.sleepMilliseconds(0);
|
|
assert(sdk_calls == 2 && timer_us == 50000ULL);
|
|
setEnabled(true, millis()); reset(millis()); decision(0, millis());
|
|
timer_error = 29;
|
|
board.sleep(30); // Keep the legacy seconds API's timer-error policy.
|
|
auto value = snapshot(millis());
|
|
assert(sdk_calls == 3 && value.calls == 1 && value.returns == 1 && value.errors == 1);
|
|
assert(value.detail == 29); // First setup failure, without changing later SDK calls.
|
|
assert(value.last_call_us == 7000 && value.eligible_ms == 7);
|
|
assert(value.blocked_ms[static_cast<size_t>(Reason::Board)] == 0);
|
|
assert(critical_depth == 0);
|
|
reset(millis()); decision(0, millis());
|
|
const auto before_failed_slice = sdk_calls;
|
|
board.sleepMilliseconds(50);
|
|
value = snapshot(millis());
|
|
assert(sdk_calls == before_failed_slice && value.calls == 0);
|
|
assert(value.errors == 1 && value.detail == 29 && critical_depth == 0);
|
|
const int gpio_errors[] = {31, 33};
|
|
for (int error : gpio_errors) {
|
|
reset(millis()); decision(0, millis());
|
|
timer_error = 0;
|
|
gpio_error = error == 31 ? error : 0;
|
|
pin_error = error == 33 ? error : 0;
|
|
board.sleep(30);
|
|
value = snapshot(millis());
|
|
assert(sdk_calls == before_failed_slice && value.calls == 0 && value.returns == 0);
|
|
assert(value.errors == 1 && value.detail == error && critical_depth == 0);
|
|
assert(value.eligible_ms == 0 && value.call_us == 0);
|
|
}
|
|
reset(millis()); decision(0, millis());
|
|
timer_error = gpio_error = pin_error = 0; sleep_error = 41;
|
|
board.sleep(30);
|
|
value = snapshot(millis());
|
|
assert(value.calls == 1 && value.returns == 1 && value.errors == 1 && value.detail == 41);
|
|
assert(value.eligible_ms == 7 && value.union_blocked_ms == 0);
|
|
sleep_error = 0;
|
|
auto veto = [&](Reason why, int detail) {
|
|
reset(millis()); decision(0, millis());
|
|
const unsigned before = sdk_calls;
|
|
board.sleep(30);
|
|
const auto got = snapshot(millis());
|
|
assert(sdk_calls == before && got.calls == 0 && got.board_vetoes == 1);
|
|
assert(got.current == mask(why) && got.detail == detail && critical_depth == 0);
|
|
};
|
|
board.inhibit_sleep = true; veto(Reason::Board, 1); board.inhibit_sleep = false;
|
|
board.test = true; veto(Reason::Board, 2); board.test = false;
|
|
board.usb = true; veto(Reason::UsbSession, 1); board.usb = false;
|
|
mesh::logging = true; veto(Reason::UsbLogging, 0); mesh::logging = false;
|
|
radio_pending = true; veto(Reason::Mesh, 14); radio_pending = false;
|
|
reset(millis()); board.irq = static_cast<uint32_t>(-1);
|
|
const unsigned before = sdk_calls;
|
|
board.sleep(0);
|
|
value = snapshot(millis());
|
|
assert(sdk_calls == before && value.board_vetoes == 1 && value.current == mask(Reason::NoWake));
|
|
assert(yields == 5 && irq_reads > 0);
|
|
}
|
|
'''.replace("@METHOD@", method)
|
|
self.execute(code)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|