Add saved regulator modes and battery-gated crash recovery

This commit is contained in:
mikecarper committed 2026-10-18 21:10:58 -07:00
1 parent 515bb57cdf
commit c4a1d485ff
70 files changed
+5335 -2472

No files matched your search

+5
View File
@@ -413,6 +413,10 @@ jobs:
python3 -B test/test_power_saving_deadlines.py -v
python3 -B test/test_t096_regulator.py -v
python3 -B test/test_nrf52_board_regulators.py -v
python3 -B test/test_nrf52_regulator.py -v
python3 -B test/test_radio_regulator_modes.py -v
python3 -B test/test_regulator_recovery.py -v
python3 -B test/test_regulator_settings.py -v
python3 -B test/test_power_saving_diagnostics.py -v
python3 -B test/test_power_saving_role_diagnostics.py -v
python3 -B test/test_companion_power_diagnostics.py -v
@@ -976,6 +980,7 @@ jobs:
python3 -B test/test_bluetooth_mac_contract.py
python3 -B test/test_companion_settings_persistence_contract.py
python3 -B test/test_webconfig_ui.py
python3 -B test/test_webconfig_regulators.py -v
python3 -B test/test_webconfig_gzip.py
python3 -B test/test_webconfig_secret_masking.py
python3 -B test/test_webconfig_station_ownership.py -v
+67 -49
View File
@@ -1,49 +1,67 @@
/* Linker script to configure memory regions. */
SEARCH_DIR(.)
GROUP(-lgcc -lc -lnosys)
MEMORY
{
FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xED000 - 0x26000
/* To keep data in RAM across resets */
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8
/* SRAM required by Softdevice depend on
* - Attribute Table Size (Number of Services and Characteristics)
* - Vendor UUID count
* - Max ATT MTU
* - Concurrent connection peripheral + central + secure links
* - Event Len, HVN queue, Write CMD queue
*/
RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8
}
SECTIONS
{
. = ALIGN(4);
.persistent (NOLOAD) :
{
KEEP(*(.persistent_magic))
KEEP(*(.persistent_data))
. = ALIGN(4);
} > PERSISTENT_RAM
. = ALIGN(4);
.svc_data :
{
PROVIDE(__start_svc_data = .);
KEEP(*(.svc_data))
PROVIDE(__stop_svc_data = .);
} > RAM
.fs_data :
{
PROVIDE(__start_fs_data = .);
KEEP(*(.fs_data))
PROVIDE(__stop_fs_data = .);
} > RAM
}
INCLUDE "nrf52_common.ld"
/* Linker script to configure memory regions. */
SEARCH_DIR(.)
GROUP(-lgcc -lc -lnosys)
MEMORY
{
FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xED000 - 0x26000
/* To keep data in RAM across resets */
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8
/* Separate reset-retained state; clock and OTA handoff ABIs stay fixed. */
REGULATOR_RECOVERY_RAM (rwx) : ORIGIN = 0x20006050, LENGTH = 64
/* SRAM required by Softdevice depend on
* - Attribute Table Size (Number of Services and Characteristics)
* - Vendor UUID count
* - Max ATT MTU
* - Concurrent connection peripheral + central + secure links
* - Event Len, HVN queue, Write CMD queue
*/
RAM (rwx) : ORIGIN = 0x20006090, LENGTH = 0x20040000 - 0x20006090
}
SECTIONS
{
. = ALIGN(4);
.persistent (NOLOAD) :
{
KEEP(*(.persistent_magic))
KEEP(*(.persistent_data))
. = ALIGN(4);
} > PERSISTENT_RAM
.regulator_recovery (NOLOAD) :
{
__regulator_recovery_start__ = .;
KEEP(*(.regulator_recovery))
. = ORIGIN(REGULATOR_RECOVERY_RAM) + LENGTH(REGULATOR_RECOVERY_RAM);
__regulator_recovery_end__ = .;
} > REGULATOR_RECOVERY_RAM
ASSERT(__regulator_recovery_start__ == 0x20006050,
"regulator recovery start must remain fixed")
ASSERT(__regulator_recovery_end__ == 0x20006090,
"regulator recovery reservation must remain exactly 64 bytes")
ASSERT(ORIGIN(RAM) == __regulator_recovery_end__,
"application RAM must start after regulator recovery")
. = ALIGN(4);
.svc_data :
{
PROVIDE(__start_svc_data = .);
KEEP(*(.svc_data))
PROVIDE(__stop_svc_data = .);
} > RAM
.fs_data :
{
PROVIDE(__start_fs_data = .);
KEEP(*(.fs_data))
PROVIDE(__stop_fs_data = .);
} > RAM
}
INCLUDE "nrf52_common.ld"
+19 -1
View File
@@ -10,6 +10,9 @@ MEMORY
/* To keep data in RAM across resets */
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8
/* Separate reset-retained state; clock and OTA handoff ABIs stay fixed. */
REGULATOR_RECOVERY_RAM (rwx) : ORIGIN = 0x20006050, LENGTH = 64
/* SRAM required by Softdevice depend on
* - Attribute Table Size (Number of Services and Characteristics)
* - Vendor UUID count
@@ -17,7 +20,7 @@ MEMORY
* - Concurrent connection peripheral + central + secure links
* - Event Len, HVN queue, Write CMD queue
*/
RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8
RAM (rwx) : ORIGIN = 0x20006090, LENGTH = 0x20040000 - 0x20006090
}
SECTIONS
@@ -30,6 +33,21 @@ SECTIONS
. = ALIGN(4);
} > PERSISTENT_RAM
.regulator_recovery (NOLOAD) :
{
__regulator_recovery_start__ = .;
KEEP(*(.regulator_recovery))
. = ORIGIN(REGULATOR_RECOVERY_RAM) + LENGTH(REGULATOR_RECOVERY_RAM);
__regulator_recovery_end__ = .;
} > REGULATOR_RECOVERY_RAM
ASSERT(__regulator_recovery_start__ == 0x20006050,
"regulator recovery start must remain fixed")
ASSERT(__regulator_recovery_end__ == 0x20006090,
"regulator recovery reservation must remain exactly 64 bytes")
ASSERT(ORIGIN(RAM) == __regulator_recovery_end__,
"application RAM must start after regulator recovery")
. = ALIGN(4);
.svc_data :
{
+21 -3
View File
@@ -10,8 +10,11 @@ MEMORY
/* 8-byte retained clock state followed by the 72-byte MOTAHYB1 record. */
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 80
/* Separate reset-retained state; clock and OTA handoff ABIs stay fixed. */
REGULATOR_RECOVERY_RAM (rwx) : ORIGIN = 0x20006050, LENGTH = 64
/* Normal application data, heap, and stack end below the mOTA arena. */
RAM (rwx) : ORIGIN = 0x20006000 + 80, LENGTH = 0x20030000 - 0x20006000 - 80
RAM (rwx) : ORIGIN = 0x20006090, LENGTH = 0x20030000 - 0x20006090
/* Fixed staging arena retained across the software-reset bootloader handoff. */
MOTA_RAM (rwx) : ORIGIN = 0x20030000, LENGTH = 0x10000
@@ -27,6 +30,21 @@ SECTIONS
. = ALIGN(4);
} > PERSISTENT_RAM
.regulator_recovery (NOLOAD) :
{
__regulator_recovery_start__ = .;
KEEP(*(.regulator_recovery))
. = ORIGIN(REGULATOR_RECOVERY_RAM) + LENGTH(REGULATOR_RECOVERY_RAM);
__regulator_recovery_end__ = .;
} > REGULATOR_RECOVERY_RAM
ASSERT(__regulator_recovery_start__ == 0x20006050,
"regulator recovery start must remain fixed")
ASSERT(__regulator_recovery_end__ == 0x20006090,
"regulator recovery reservation must remain exactly 64 bytes")
ASSERT(ORIGIN(RAM) == __regulator_recovery_end__,
"application RAM must start after regulator recovery")
/* Export the direct-access handoff ABI even though no input section owns it. */
__mota_hybrid_handoff_start__ = ADDR(.persistent) + SIZEOF(.persistent);
__mota_hybrid_handoff_end__ = ORIGIN(PERSISTENT_RAM) + LENGTH(PERSISTENT_RAM);
@@ -43,8 +61,8 @@ SECTIONS
"mOTA hybrid handoff end must remain fixed")
ASSERT(__mota_hybrid_handoff_end__ - __mota_hybrid_handoff_start__ == 72,
"mOTA hybrid handoff must remain exactly 72 bytes")
ASSERT(ORIGIN(RAM) == __mota_hybrid_handoff_end__,
"normal RAM must start immediately after the mOTA handoff")
ASSERT(ORIGIN(REGULATOR_RECOVERY_RAM) == __mota_hybrid_handoff_end__,
"regulator recovery must follow the mOTA handoff")
ASSERT(ORIGIN(RAM) >= __mota_sd_auth_end__,
"application RAM must not overlap SD OTA authorization")
+19 -1
View File
@@ -10,8 +10,11 @@ MEMORY
/* 8-byte retained clock state followed by the 72-byte MOTASDA2 authorization record. */
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 80
/* Separate reset-retained state; clock and OTA handoff ABIs stay fixed. */
REGULATOR_RECOVERY_RAM (rwx) : ORIGIN = 0x20006050, LENGTH = 64
/* SRAM required by SoftDevice depends on configured BLE resources. */
RAM (rwx) : ORIGIN = 0x20006000 + 80, LENGTH = 0x20040000 - 0x20006000 - 80
RAM (rwx) : ORIGIN = 0x20006090, LENGTH = 0x20040000 - 0x20006090
}
SECTIONS
@@ -24,6 +27,21 @@ SECTIONS
. = ALIGN(4);
} > PERSISTENT_RAM
.regulator_recovery (NOLOAD) :
{
__regulator_recovery_start__ = .;
KEEP(*(.regulator_recovery))
. = ORIGIN(REGULATOR_RECOVERY_RAM) + LENGTH(REGULATOR_RECOVERY_RAM);
__regulator_recovery_end__ = .;
} > REGULATOR_RECOVERY_RAM
ASSERT(__regulator_recovery_start__ == 0x20006050,
"regulator recovery start must remain fixed")
ASSERT(__regulator_recovery_end__ == 0x20006090,
"regulator recovery reservation must remain exactly 64 bytes")
ASSERT(ORIGIN(RAM) == __regulator_recovery_end__,
"application RAM must start after regulator recovery")
__mota_sd_auth_start__ = ADDR(.persistent) + SIZEOF(.persistent);
__mota_sd_auth_end__ = ORIGIN(PERSISTENT_RAM) + LENGTH(PERSISTENT_RAM);
+19 -1
View File
@@ -10,6 +10,9 @@ MEMORY
/* To keep data in RAM across resets */
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8
/* Separate reset-retained state; clock and OTA handoff ABIs stay fixed. */
REGULATOR_RECOVERY_RAM (rwx) : ORIGIN = 0x20006050, LENGTH = 64
/* SRAM required by Softdevice depend on
* - Attribute Table Size (Number of Services and Characteristics)
* - Vendor UUID count
@@ -17,7 +20,7 @@ MEMORY
* - Concurrent connection peripheral + central + secure links
* - Event Len, HVN queue, Write CMD queue
*/
RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8
RAM (rwx) : ORIGIN = 0x20006090, LENGTH = 0x20040000 - 0x20006090
}
SECTIONS
@@ -30,6 +33,21 @@ SECTIONS
. = ALIGN(4);
} > PERSISTENT_RAM
.regulator_recovery (NOLOAD) :
{
__regulator_recovery_start__ = .;
KEEP(*(.regulator_recovery))
. = ORIGIN(REGULATOR_RECOVERY_RAM) + LENGTH(REGULATOR_RECOVERY_RAM);
__regulator_recovery_end__ = .;
} > REGULATOR_RECOVERY_RAM
ASSERT(__regulator_recovery_start__ == 0x20006050,
"regulator recovery start must remain fixed")
ASSERT(__regulator_recovery_end__ == 0x20006090,
"regulator recovery reservation must remain exactly 64 bytes")
ASSERT(ORIGIN(RAM) == __regulator_recovery_end__,
"application RAM must start after regulator recovery")
. = ALIGN(4);
.svc_data :
{
+19 -1
View File
@@ -10,6 +10,9 @@ MEMORY
/* To keep data in RAM across resets */
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8
/* Separate reset-retained state; clock and OTA handoff ABIs stay fixed. */
REGULATOR_RECOVERY_RAM (rwx) : ORIGIN = 0x20006050, LENGTH = 64
/* SRAM required by Softdevice depend on
* - Attribute Table Size (Number of Services and Characteristics)
* - Vendor UUID count
@@ -17,7 +20,7 @@ MEMORY
* - Concurrent connection peripheral + central + secure links
* - Event Len, HVN queue, Write CMD queue
*/
RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8
RAM (rwx) : ORIGIN = 0x20006090, LENGTH = 0x20040000 - 0x20006090
}
SECTIONS
@@ -30,6 +33,21 @@ SECTIONS
. = ALIGN(4);
} > PERSISTENT_RAM
.regulator_recovery (NOLOAD) :
{
__regulator_recovery_start__ = .;
KEEP(*(.regulator_recovery))
. = ORIGIN(REGULATOR_RECOVERY_RAM) + LENGTH(REGULATOR_RECOVERY_RAM);
__regulator_recovery_end__ = .;
} > REGULATOR_RECOVERY_RAM
ASSERT(__regulator_recovery_start__ == 0x20006050,
"regulator recovery start must remain fixed")
ASSERT(__regulator_recovery_end__ == 0x20006090,
"regulator recovery reservation must remain exactly 64 bytes")
ASSERT(ORIGIN(RAM) == __regulator_recovery_end__,
"application RAM must start after regulator recovery")
. = ALIGN(4);
.svc_data :
{
+21 -3
View File
@@ -10,8 +10,11 @@ MEMORY
/* 8-byte retained clock state followed by the 72-byte MOTAHYB1 record. */
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 80
/* Separate reset-retained state; clock and OTA handoff ABIs stay fixed. */
REGULATOR_RECOVERY_RAM (rwx) : ORIGIN = 0x20006050, LENGTH = 64
/* Normal application data, heap, and stack end below the mOTA arena. */
RAM (rwx) : ORIGIN = 0x20006000 + 80, LENGTH = 0x20030000 - 0x20006000 - 80
RAM (rwx) : ORIGIN = 0x20006090, LENGTH = 0x20030000 - 0x20006090
/* Fixed staging arena retained across the software-reset bootloader handoff. */
MOTA_RAM (rwx) : ORIGIN = 0x20030000, LENGTH = 0x10000
@@ -27,6 +30,21 @@ SECTIONS
. = ALIGN(4);
} > PERSISTENT_RAM
.regulator_recovery (NOLOAD) :
{
__regulator_recovery_start__ = .;
KEEP(*(.regulator_recovery))
. = ORIGIN(REGULATOR_RECOVERY_RAM) + LENGTH(REGULATOR_RECOVERY_RAM);
__regulator_recovery_end__ = .;
} > REGULATOR_RECOVERY_RAM
ASSERT(__regulator_recovery_start__ == 0x20006050,
"regulator recovery start must remain fixed")
ASSERT(__regulator_recovery_end__ == 0x20006090,
"regulator recovery reservation must remain exactly 64 bytes")
ASSERT(ORIGIN(RAM) == __regulator_recovery_end__,
"application RAM must start after regulator recovery")
/* Export the direct-access handoff ABI even though no input section owns it. */
__mota_hybrid_handoff_start__ = ADDR(.persistent) + SIZEOF(.persistent);
__mota_hybrid_handoff_end__ = ORIGIN(PERSISTENT_RAM) + LENGTH(PERSISTENT_RAM);
@@ -43,8 +61,8 @@ SECTIONS
"mOTA hybrid handoff end must remain fixed")
ASSERT(__mota_hybrid_handoff_end__ - __mota_hybrid_handoff_start__ == 72,
"mOTA hybrid handoff must remain exactly 72 bytes")
ASSERT(ORIGIN(RAM) == __mota_hybrid_handoff_end__,
"normal RAM must start immediately after the mOTA handoff")
ASSERT(ORIGIN(REGULATOR_RECOVERY_RAM) == __mota_hybrid_handoff_end__,
"regulator recovery must follow the mOTA handoff")
ASSERT(ORIGIN(RAM) >= __mota_sd_auth_end__,
"application RAM must not overlap SD OTA authorization")
+19 -1
View File
@@ -14,6 +14,9 @@ MEMORY
/* To keep data in RAM across resets */
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8
/* Separate reset-retained state; clock and OTA handoff ABIs stay fixed. */
REGULATOR_RECOVERY_RAM (rwx) : ORIGIN = 0x20006050, LENGTH = 64
/* SRAM required by SoftDevice depend on
* - Attribute Table Size (Number of Services and Characteristics)
* - Vendor UUID count
@@ -21,7 +24,7 @@ MEMORY
* - Concurrent connection peripheral + central + secure links
* - Event Len, HVN queue, Write CMD queue
*/
RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8
RAM (rwx) : ORIGIN = 0x20006090, LENGTH = 0x20040000 - 0x20006090
}
SECTIONS
@@ -34,6 +37,21 @@ SECTIONS
. = ALIGN(4);
} > PERSISTENT_RAM
.regulator_recovery (NOLOAD) :
{
__regulator_recovery_start__ = .;
KEEP(*(.regulator_recovery))
. = ORIGIN(REGULATOR_RECOVERY_RAM) + LENGTH(REGULATOR_RECOVERY_RAM);
__regulator_recovery_end__ = .;
} > REGULATOR_RECOVERY_RAM
ASSERT(__regulator_recovery_start__ == 0x20006050,
"regulator recovery start must remain fixed")
ASSERT(__regulator_recovery_end__ == 0x20006090,
"regulator recovery reservation must remain exactly 64 bytes")
ASSERT(ORIGIN(RAM) == __regulator_recovery_end__,
"application RAM must start after regulator recovery")
. = ALIGN(4);
.svc_data :
{
+39
View File
@@ -1537,6 +1537,45 @@ capacity. Remote use follows the existing administrator-command permissions.
### Radio
#### Select MCU and LoRa regulator modes
```text
get mcu.regulator
set mcu.regulator auto|dcdc|ldo
get radio.regulator
set radio.regulator auto|dcdc|ldo
reboot
```
These are independent controls. `auto` follows the board's existing default;
`dcdc` requests its switching regulator and `ldo` requests its linear regulator.
Settings are saved for the next reboot. The getter reports the saved choice,
active mode, automatic fallback, crash streak and whether recovery is available.
Setting a mode again, including the same value, clears that regulator's learned
fallback and crash streak. The other regulator keeps its settings.
Choices depend on the board's verified circuit. MCU selection is available on
nRF52 boards that already initialize their supported REG1 DC-DC regulator; it
does not control REG0, an external PMIC, or ESP32/RP2040 processor regulators.
Radio selection covers supported SX1262, LR1110/LR1121 and Meshnology W12 LR2021
implementations. A board with only verified LDO support rejects `dcdc`.
Unverified LR2021 switching-regulator circuits retain their existing mode.
On platforms with retained crash recovery, three consecutive qualifying crashes
while a domain is using DC-DC select LDO for that domain on the next boot.
Each crash requires a valid last battery sample of at least 50%, no more than
60 seconds old at the last heartbeat. Low/unknown battery readings, stale
samples, ordinary reboots, sleep wakes and unknown reset causes break the
streak. Five minutes of healthy operation also clears the streak; a later crash
starts a new one. LDO fallback stays selected until a mode is explicitly set
again; recovery never switches back to DC-DC automatically.
ESP32 WebConfig shows **Power regulators** under **Radio → Advanced** when
supported, with separate processor/radio choices and active/fallback status.
Only supported options are offered. **Clear fallback on restart** reapplies the
current choice through the normal save flow. Saving either control restarts the
node after the save succeeds. See [regulator recovery and hardware checks](power_saving_diagnostics.md#user-selectable-regulators-and-crash-recovery).
#### View or change this node's radio parameters
**Usage:**
- `get radio`
+68 -11
View File
@@ -137,12 +137,68 @@ measurements on the physical core/module combination.
## Board regulator initialization
RAK4631 Companion uses the same nRF52840 REG1 DC/DC initialization as the other
RAK4631 roles. The former Companion-only force-LDO flag has been removed. This
does not change REG0, the SX1262 regulator, storage layout, or boot-voltage
protection. The shared initializer uses the SoftDevice API when BLE's stack is
already active and the hardware register otherwise; Companion starts the board
before it starts Bluetooth.
### User-selectable regulators and crash recovery
`mcu.regulator` and `radio.regulator` independently select `auto`, `dcdc` or
`ldo` where the board supports those modes. `auto` preserves its existing board
default. The choice is saved and takes effect after `reboot`; the two getters
show requested/active mode, fallback, crash count and recovery availability.
An explicit setter clears the corresponding fallback and streak, even when the
same value is saved again. See the [CLI commands](cli_commands.md#select-mcu-and-lora-regulator-modes).
Processor selection applies to the existing, circuit-supported nRF52 REG1
DC-DC path. It does not select REG0, change an external PMIC, or expose an
ESP32/RP2040 CPU regulator control. Radio controls apply to the supported
SX1262, LR1110/LR1121 and Meshnology W12 LR2021 implementations. DC-DC is offered
only when that board's regulator circuit is supported. LDO-only boards reject
`dcdc`; unverified LR2021 switching circuits keep their existing mode.
Automatic recovery uses reset-retained RAM on supported platforms. A domain
must have been armed in DC-DC mode before the crash, with a valid last battery
sample of at least **50%**, no more than **60 seconds** old at its last
heartbeat. The **third consecutive qualifying crash** selects LDO before that
domain starts again and saves the fallback. MCU and radio streaks are separate;
if both were using DC-DC during the same qualifying failures, both can fall
back. This is a recovery action, not proof that a regulator caused the crashes.
Low/unknown battery, stale samples, cold starts, intentional reboots, sleep
wakes and unclassified resets break a streak. Corrupt retained state cannot
add a crash. **Five healthy minutes** clear the running streak; a later crash
starts a new one. The saved LDO fallback survives normal resets and power
cycles. It stays in LDO until a user explicitly selects a mode again, with no
automatic return to DC-DC. Crash bookkeeping stays in retained RAM rather than
writing flash at every heartbeat.
Battery eligibility follows the board's battery measurement and configured
percentage curve. USB supply voltage alone does not establish that a physical
battery is at 50%; validate battery-only starts and the board's absent-battery
reading when qualifying recovery. Boards without a trustworthy battery reading
should report it invalid. A getter reporting recovery unavailable still permits
supported manual mode selection.
ESP32 WebConfig exposes supported controls under **Radio → Advanced → Power
regulators**, showing active mode and fallback status. Unsupported domains and
DC-DC options are hidden. Saving reboots only after the authenticated save
completes successfully. **Clear fallback on restart** lets a user retry the
current saved mode without first choosing a different one.
For hardware qualification, compare both modes with battery-only cold starts,
normal reboots, sleep/wake, BLE/WiFi and radio TX/RX under load. Confirm that
three injected qualifying watchdog/fault resets select LDO, that fewer than
three and low/unknown-battery resets do not, and that an explicit setter restores
the requested choice. Also check current consumption and voltage during TX;
native recovery tests and successful firmware builds do not establish these
physical results.
### Existing board defaults
RAK4631 Companion shares the nRF52840 REG1 regulator path with the other
RAK4631 roles. `auto` preserves its DC-DC default; the former Companion-only
force-LDO flag has been removed. Saved mode selection is deferred until board
ADC and primary filesystem initialization are complete, before display, radio
and wireless startup. This retains boot-voltage protection and keeps REG0 and
storage layout unchanged. The shared initializer uses the SoftDevice API when
BLE's stack is already active and the hardware register otherwise.
[RAK documents DC/DC support](https://docs.rakwireless.com/product-categories/wisduo/rak4630-module/quickstart/)
for the module. The related upstream [battery-startup report #1878](https://github.com/meshcore-dev/MeshCore/issues/1878)
@@ -152,8 +208,8 @@ DC/DC failure. [PR #1914](https://github.com/meshcore-dev/MeshCore/pull/1914)
also changes boot-voltage/wakeup handling and is not proof that every failure
in that thread has the same cause.
Host startup tests cover both SoftDevice states and regulator selection before
the boot-voltage check. Firmware compilation is not hardware qualification:
Host startup tests cover both SoftDevice states and deferred regulator selection
with boot-voltage protection preserved. Firmware compilation is not hardware qualification:
compare battery-only cold starts, low-voltage recovery, BLE operation and
settings writes/reboots on the actual board/baseboard revisions before a fleet
rollout. USB power can mask the reported startup symptoms. A current meter is
@@ -176,9 +232,10 @@ external LDO, PMIC output, or radio regulator.
| Minew ME25LS01 | [Manufacturer datasheet](https://store.minewsemi.com/wp-content/uploads/2024/03/ME25LS01_LR1110_Datasheet_K_EN.pdf), electrical schematic page 9: MCU DCC through ML2/ML3 to DEC4_6 and MC15/MC16. |
The startup tests execute each real `begin()` with SoftDevice off/on and power
management compiled out/in. They require DC/DC before peripheral initialization
and before any enabled boot-voltage check, while retaining one common board
initialization. The previous bypass fails these tests.
management compiled out/in. Normal startup defers the selected mode until the
saved settings and recovery state can be read, while retaining one common board
initialization. The board class still defines the circuit-supported default;
the settings layer applies it before display/radio startup.
ThinkNode M3/M4/M6 and generic ProMicro remain unchanged pending exact circuit
or physical-board confirmation; their DC/DC inheritance alone is not evidence.
+4
View File
@@ -1,3 +1,4 @@
#include <helpers/RegulatorSettings.h>
#include <helpers/ui/DisplayPowerSettings.h>
#include <helpers/BatteryChargeCLI.h>
#include <helpers/MorseCLI.h>
@@ -8222,6 +8223,7 @@ bool MyMesh::handleExtraFsHilCommand(const char* command, char* reply,
if (strcmp(action, "unsafe-reset CONFIRM") == 0) {
// Intentionally bypass all persistence hooks so HIL can prove that a
// mutation was already committed before an event-driven sleep.
meshRegulatorCleanReset(); // Retained marker only; no persistence flush.
NVIC_SystemReset();
while (true) {}
}
@@ -10720,6 +10722,8 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
while (*command == ' ' || *command == '\t') command++;
}
if (mesh::power::handleRegulatorCommand(command, reply, reply_capacity)) return true;
#if COMPANION_FEATURE_READER
if (sender_timestamp == 0 && mesh::morseTimingActive(_radio)
&& isCompanionReaderRequest(command)) {
+12
View File
@@ -1,3 +1,4 @@
#include <helpers/RegulatorSettings.h>
#include <Arduino.h> // needed for PlatformIO
#include <helpers/ui/StartupScreen.h>
#include <helpers/ui/DisplayPowerSettings.h>
@@ -3600,6 +3601,16 @@ void setup() {
#endif
FILESYSTEM* regulator_prefs_fs = store.getPrimaryFS();
#if defined(NRF52_PLATFORM)
if (!mesh::storage::internalPrimaryFilesystemReady(primary_fs_boot)
&& !store.isVolatilePrimaryFS()) regulator_prefs_fs = nullptr;
#endif
if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) {
MESH_DEBUG_PRINTLN("Regulator selection could not be applied");
}
#ifdef DISPLAY_CLASS
// Load only the display policy before radio and secondary-storage startup.
FILESYSTEM* display_prefs_fs = store.getPrimaryFS();
@@ -4205,6 +4216,7 @@ void loop() {
if (!morse_timing) companionPairingUiHilProbe();
#endif
rtc_clock.tick();
mesh::power::serviceRegulatorSettings();
board.loop();
if (mesh::serviceUsbLoggingWatchdog(usbLoggingRecoverySafe)) board.reboot();
#ifdef TBEAM_1W
+1
View File
@@ -180,6 +180,7 @@ void setup() {
#endif
KISS_BOOT_STAGE("board.before");
board.begin();
board.applyMcuRegulator(mesh::power::RegulatorMode::Default);
KISS_BOOT_STAGE("board.after");
#if defined(ESP32)
+1
View File
@@ -92,6 +92,7 @@ bool loadPrimaryRadio(mesh::RadioProfileParams& profile) {
void startLoRaReceiver() {
board.begin();
board.applyMcuRegulator(mesh::power::RegulatorMode::Default);
if (!radio_init()) {
Serial.println("Partition Expander: radio init failed; Wi-Fi recovery remains available");
return;
+5
View File
@@ -1,3 +1,4 @@
#include <helpers/RegulatorSettings.h>
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include <helpers/IdentityGeneration.h>
@@ -203,6 +204,9 @@ void setup() {
#else
#error "need to define filesystem"
#endif
if (!mesh::power::beginRegulatorSettings(board, radio_driver, fs)) {
MESH_DEBUG_PRINTLN("Regulator selection could not be applied");
}
mesh::hilStartupTrace("fs_ready");
#ifdef DISPLAY_CLASS
mesh::hilStartupTrace("display_begin");
@@ -538,6 +542,7 @@ static void __attribute__((noinline)) serviceCommandInterfaces() {
command[0] = 0; // reset command buffer
}
mesh::power::serviceRegulatorSettings();
board.loop(); // let the board feed its watchdog, run periodic housekeeping
#ifdef ETHERNET_ENABLED
+5
View File
@@ -1,3 +1,4 @@
#include <helpers/RegulatorSettings.h>
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include <helpers/IdentityGeneration.h>
@@ -137,6 +138,9 @@ void setup() {
#else
#error "need to define filesystem"
#endif
if (!mesh::power::beginRegulatorSettings(board, radio_driver, fs)) {
MESH_DEBUG_PRINTLN("Regulator selection could not be applied");
}
#ifdef DISPLAY_CLASS
mesh::ui::loadDisplayPowerSettings(fs, false);
mesh::ui::StartupScreen startup_screen;
@@ -390,6 +394,7 @@ void loop() {
if (display_ready) ui_task.loop();
#endif
rtc_clock.tick();
mesh::power::serviceRegulatorSettings();
board.loop();
if (mesh::serviceUsbLoggingWatchdog(usbLoggingRecoverySafe)) board.reboot();
#if MESH_USB_CONSOLE_COOPERATIVE
+1
View File
@@ -748,6 +748,7 @@ void setup() {
#endif
board.begin();
board.applyMcuRegulator(mesh::power::RegulatorMode::Default);
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.begin();
+5
View File
@@ -1,3 +1,4 @@
#include <helpers/RegulatorSettings.h>
#include "SensorMesh.h"
#include <math.h>
#include <helpers/IdentityGeneration.h>
@@ -162,6 +163,9 @@ void setup() {
#else
#error "need to define filesystem"
#endif
if (!mesh::power::beginRegulatorSettings(board, radio_driver, fs)) {
MESH_DEBUG_PRINTLN("Regulator selection could not be applied");
}
#ifdef DISPLAY_CLASS
mesh::ui::loadDisplayPowerSettings(fs, false);
mesh::ui::StartupScreen startup_screen;
@@ -351,6 +355,7 @@ void loop() {
command[0] = 0; // reset command buffer
}
mesh::power::serviceRegulatorSettings();
board.loop(); // let the board feed its watchdog, run periodic housekeeping
the_mesh.loop();
+5 -1
View File
@@ -12,7 +12,8 @@ from pathlib import Path
OLD_HELPER = 'extern "C" void bootloader_util_app_start(uint32_t start_addr);\n'
HELPER_NAME = "mesh_nrf52_dfu_jump"
HELPER = r'''// Keep all stack operations inside this assembly-only transition. In particular,
CLEAN_RESET_DECLARATION = 'extern "C" void meshRegulatorCleanReset() __attribute__((weak));\n'
HELPER = CLEAN_RESET_DECLARATION + r'''// Keep all stack operations inside this assembly-only transition. In particular,
// the callback must return neither through its PSP frame nor through a C helper
// after CONTROL selects MSP. Preserve handler-mode entry supported by BLEDfu.
extern "C" __attribute__((naked, noinline, noreturn, used))
@@ -80,6 +81,9 @@ OLD_JUMP = ''' __set_CONTROL(0); // switch to MSP, required if using FreeRT
'''
JUMP = ''' // The vector-table SVC above must complete before the helper masks
// interrupts. B1 and the retained peer record preserve legacy DFU entry.
// Only a committed handoff is intentional: rejected/aborted DFU remains
// covered by crash recovery while the application continues running.
if (meshRegulatorCleanReset) meshRegulatorCleanReset();
mesh_nrf52_dfu_jump(NRF_UICR->NRFFW[0]);
'''
+7
View File
@@ -10,6 +10,7 @@
#include <helpers/MorseRx.h>
#include <helpers/AGCResetScheduler.h>
#include <helpers/BeaconConfig.h>
#include <helpers/RegulatorMode.h>
#ifndef RADIO_LIVENESS_SOFT_ONLY
#include <helpers/RadioLivenessTracker.h>
#endif
@@ -36,6 +37,12 @@ public:
*/
class Radio {
public:
virtual uint8_t regulatorModes() const { return 0; }
virtual power::RegulatorMode defaultRegulatorMode() const { return power::RegulatorMode::Default; }
// Cache a selection before the first hardware initialization. Save later
// choices for the next reboot; never change a live RX/TX regulator here.
virtual bool configureBootRegulator(power::RegulatorMode) { return false; }
virtual power::RegulatorMode activeRegulatorMode() const { return power::RegulatorMode::Default; }
static constexpr uint32_t CarrierWaveDefaultMillis = 10000;
static constexpr uint32_t CarrierWaveMaxMillis = 60000;
virtual bool supportsCarrierWave() const { return false; }
+9
View File
@@ -5,6 +5,7 @@
#include <math.h>
#include "helpers/UsbLogging.h"
#include "helpers/BatteryChargeSupport.h"
#include "helpers/RegulatorMode.h"
#include "helpers/PowerSavingDiagnostics.h"
#if defined(ARDUINO) && (defined(RP2040_PLATFORM) || defined(STM32_PLATFORM))
#include <Arduino.h>
@@ -145,6 +146,14 @@ public:
virtual void onBeforeTransmit() { }
virtual void onAfterTransmit() { }
virtual void reboot() = 0;
virtual uint8_t mcuRegulatorModes() const { return 0; }
virtual power::RegulatorMode defaultMcuRegulatorMode() const {
return power::RegulatorMode::Default;
}
virtual bool applyMcuRegulator(power::RegulatorMode) { return false; }
virtual power::RegulatorMode activeMcuRegulatorMode() const {
return power::RegulatorMode::Default;
}
// Reboot into a UF2-capable bootloader when the platform supports the
// retained reset request. Returns false only when unsupported or rejected;
// a successful implementation resets and does not return.
+2
View File
@@ -1,3 +1,4 @@
#include <helpers/RegulatorSettings.h>
#include <Arduino.h>
#include "CarrierTools.h"
#include "PacketFileLoggingConfig.h"
@@ -2978,6 +2979,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
#if MESH_BATTERY_CHARGE_CONTROL
if (mesh::power::handleBatteryChargeCommand(*_board, command, reply, 160)) return;
#endif
if (mesh::power::handleRegulatorCommand(command, reply, 157)) return;
if (mesh::handleUsbLoggingWatchdogCommand(command, reply, 160)) return;
if (mesh::powerdiag::handleCommand(command, reply, 157, millis())) return;
if (handleManagementCommand(command, reply)) return;
+127 -10
View File
@@ -9,6 +9,7 @@
#include <target.h>
#include "UserGpioPinPolicy.h"
#include "PowerSavingDiagnostics.h"
#include "Nrf52RegulatorPlatform.h"
#include <bluefruit.h>
#include "ble_gap.h"
@@ -29,6 +30,87 @@ static uint16_t ota_conn_handle = BLE_CONN_HANDLE_INVALID;
static bool ota_active = false;
static bool ota_ble_started = false;
// Intentional reboot paths share the recovery policy's clean-reset marker.
// Keep this optional for standalone board/library builds without the policy.
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
namespace {
volatile mesh::power::RecoveryRecord regulator_recovery_record
__attribute__((section(".regulator_recovery"), used, aligned(4)));
static_assert(sizeof(mesh::power::RecoveryRecord) <= 64,
"nRF52 regulator recovery exceeds retained allocation");
uint32_t regulator_reset_reason = 0;
uint8_t regulator_shutdown_reason = 0;
}
extern "C" {
extern uint8_t __regulator_recovery_start__[] __attribute__((weak));
extern uint8_t __regulator_recovery_end__[] __attribute__((weak));
}
// Capture before the core's SystemInit constructor applies reset errata and
// before power management clears GPREGRET2. This also runs on boards without
// NRF52_POWER_MANAGEMENT; the recovery policy never writes either GPREGRET.
static void __attribute__((constructor(101))) nrf52_regulator_reset_capture() {
regulator_reset_reason = NRF_POWER->RESETREAS;
regulator_shutdown_reason = NRF_POWER->GPREGRET2;
}
namespace mesh { namespace power {
volatile RecoveryRecord& platformRecoveryRecord() {
return regulator_recovery_record;
}
bool platformRecoverySupported() {
return reinterpret_cast<uintptr_t>(__regulator_recovery_start__) == 0x20006050UL
&& reinterpret_cast<uintptr_t>(__regulator_recovery_end__) == 0x20006090UL
&& reinterpret_cast<uintptr_t>(&regulator_recovery_record) == 0x20006050UL;
}
ResetKind platformResetKind() {
// RESETREAS is sticky. Boards without power management otherwise retain
// wake bits forever and could hide a later real watchdog failure.
static bool acknowledged = false;
if (!acknowledged) {
uint8_t sd_enabled = 0;
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS)
return ResetKind::Unknown;
if (sd_enabled) {
if (sd_power_reset_reason_clr(0xFFFFFFFFUL) != NRF_SUCCESS)
return ResetKind::Unknown;
} else {
NRF_POWER->RESETREAS = 0xFFFFFFFFUL;
}
acknowledged = true;
}
// A deliberate SYSTEMOFF, reset-pin press, or debugger event never counts
// against DCDC, even if another latched reason bit is also present.
if (regulator_shutdown_reason == 0x4C // low voltage
|| regulator_shutdown_reason == 0x55 // user shutdown
|| regulator_shutdown_reason == 0x42) // boot protection
return ResetKind::Intentional;
const uint32_t reason = regulator_reset_reason;
if (reason & POWER_RESETREAS_RESETPIN_Msk) return ResetKind::Intentional;
#ifdef POWER_RESETREAS_DIF_Msk
if (reason & POWER_RESETREAS_DIF_Msk) return ResetKind::Intentional;
#endif
uint32_t wakes = 0;
#ifdef POWER_RESETREAS_OFF_Msk
wakes |= POWER_RESETREAS_OFF_Msk;
#endif
#ifdef POWER_RESETREAS_LPCOMP_Msk
wakes |= POWER_RESETREAS_LPCOMP_Msk;
#endif
#ifdef POWER_RESETREAS_VBUS_Msk
wakes |= POWER_RESETREAS_VBUS_Msk;
#endif
if (reason & wakes) return ResetKind::Wake;
if (reason & (POWER_RESETREAS_DOG_Msk | POWER_RESETREAS_LOCKUP_Msk
| POWER_RESETREAS_SREQ_Msk)) return ResetKind::Crash;
return reason == 0 ? ResetKind::Cold : ResetKind::Unknown;
}
}} // namespace mesh::power
// A normal internal-flash operation completes in milliseconds. One minute is
// deliberately generous for other legitimate application work while still
// recovering an indefinitely blocked SoftDevice flash wait without requiring
@@ -113,7 +195,7 @@ bool NRF52Board::rebootToUf2Bootloader() {
NRF_POWER->GPREGRET = DFU_MAGIC_UF2_RESET;
}
NVIC_SystemReset();
reboot();
return true;
}
@@ -140,6 +222,46 @@ void NRF52Board::begin() {
#endif
}
void NRF52Board::reboot() {
if (meshRegulatorCleanReset) meshRegulatorCleanReset();
NVIC_SystemReset();
}
uint8_t NRF52Board::mcuRegulatorModes() const {
return cpu_dcdc_supported
? mesh::power::REGULATOR_DCDC | mesh::power::REGULATOR_LDO : 0;
}
mesh::power::RegulatorMode NRF52Board::defaultMcuRegulatorMode() const {
return cpu_dcdc_supported ? mesh::power::RegulatorMode::Dcdc
: mesh::power::RegulatorMode::Default;
}
mesh::power::RegulatorMode NRF52Board::activeMcuRegulatorMode() const {
return cpu_dcdc_supported ? cpu_regulator_mode
: mesh::power::RegulatorMode::Default;
}
bool NRF52Board::applyMcuRegulator(mesh::power::RegulatorMode mode) {
using mesh::power::RegulatorMode;
if (!cpu_dcdc_supported) return false;
if (mode == RegulatorMode::Default) mode = defaultMcuRegulatorMode();
if (mode != RegulatorMode::Dcdc && mode != RegulatorMode::Ldo) return false;
uint8_t sd_enabled = 0;
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS) return false;
const bool dcdc = mode == RegulatorMode::Dcdc;
if (sd_enabled) {
if (sd_power_dcdc_mode_set(dcdc ? NRF_POWER_DCDC_ENABLE
: NRF_POWER_DCDC_DISABLE) != NRF_SUCCESS)
return false;
} else {
NRF_POWER->DCDCEN = dcdc ? 1 : 0;
}
cpu_regulator_mode = mode;
return true;
}
void NRF52Board::loop() {
#ifdef NRF52_POWER_MANAGEMENT
if (!mesh::power::voltagePolicySupported()) return;
@@ -454,15 +576,10 @@ void NRF52Board::armVbusWake() {
void NRF52BoardDCDC::begin() {
NRF52Board::begin();
// Enable DC/DC converter for improved power efficiency
uint8_t sd_enabled = 0;
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
if (sd_enabled) {
sd_power_dcdc_mode_set(NRF_POWER_DCDC_ENABLE);
} else {
NRF_POWER->DCDCEN = 1;
}
// Only boards which explicitly call this method approve the external LC
// circuitry. The common startup hook applies saved choices after the
// board's battery ADC and filesystem are ready; do not enable DCDC here.
cpu_dcdc_supported = true;
}
bool NRF52Board::isExternalPowered() {
+8 -1
View File
@@ -3,6 +3,7 @@
#include <Arduino.h>
#include <MeshCore.h>
#include <helpers/KeyValueStore.h>
#include <helpers/RegulatorMode.h>
#if defined(NRF52_PLATFORM)
@@ -35,6 +36,8 @@ class NRF52Board : public mesh::MainBoard {
protected:
uint8_t startup_reason;
char *ota_name;
bool cpu_dcdc_supported = false;
mesh::power::RegulatorMode cpu_regulator_mode = mesh::power::RegulatorMode::Ldo;
#ifdef NRF52_POWER_MANAGEMENT
void pwrmgtInit() { initPowerMgr(); }
@@ -65,7 +68,11 @@ public:
void serviceWatchdog() override;
virtual uint8_t getStartupReason() const override { return startup_reason; }
virtual float getMCUTemperature() override;
virtual void reboot() override { NVIC_SystemReset(); }
void reboot() override;
uint8_t mcuRegulatorModes() const override;
mesh::power::RegulatorMode defaultMcuRegulatorMode() const override;
bool applyMcuRegulator(mesh::power::RegulatorMode mode) override;
mesh::power::RegulatorMode activeMcuRegulatorMode() const override;
bool rebootToUf2Bootloader() override;
virtual void shutdownPeripherals();
virtual void powerOff() override;
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#if defined(NRF52_PLATFORM)
#include "RegulatorRecovery.h"
namespace mesh { namespace power {
volatile RecoveryRecord& platformRecoveryRecord();
ResetKind platformResetKind();
// False for an unsupported linker layout, so an unreserved orphan section
// cannot accidentally enable automatic recovery.
bool platformRecoverySupported();
}} // namespace mesh::power
#endif
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <stdint.h>
namespace mesh { namespace power {
enum class RegulatorMode : uint8_t { Default = 0, Dcdc = 1, Ldo = 2 };
constexpr uint8_t REGULATOR_DCDC = 1;
constexpr uint8_t REGULATOR_LDO = 2;
inline bool regulatorModeSupported(uint8_t modes, RegulatorMode mode) {
return modes != 0 && (mode == RegulatorMode::Default
|| (mode == RegulatorMode::Dcdc && (modes & REGULATOR_DCDC))
|| (mode == RegulatorMode::Ldo && (modes & REGULATOR_LDO)));
}
}} // namespace mesh::power
+58
View File
@@ -0,0 +1,58 @@
#include "RegulatorPlatform.h"
#if defined(ESP32)
#include <esp_system.h>
#include <esp_attr.h>
#include <soc/soc_caps.h>
#if SOC_RTC_SLOW_MEM_SUPPORTED || SOC_RTC_FAST_MEM_SUPPORTED
#define MESH_REGULATOR_RTC 1
static RTC_NOINIT_ATTR volatile mesh::power::RecoveryRecord regulator_record;
#else
static volatile mesh::power::RecoveryRecord regulator_record;
#endif
namespace mesh { namespace power {
volatile RecoveryRecord& platformRecoveryRecord() { return regulator_record; }
bool platformRecoverySupported() {
#ifdef MESH_REGULATOR_RTC
return true;
#else
return false;
#endif
}
ResetKind platformResetKind() {
switch (esp_reset_reason()) {
case ESP_RST_PANIC:
case ESP_RST_INT_WDT:
case ESP_RST_TASK_WDT:
case ESP_RST_WDT: return ResetKind::Crash;
case ESP_RST_DEEPSLEEP: return ResetKind::Wake;
case ESP_RST_SW: return ResetKind::Intentional;
case ESP_RST_POWERON:
case ESP_RST_EXT: return ResetKind::Cold;
// A supply failure is not evidence that the DC/DC regulator is faulty.
default: return ResetKind::Unknown;
}
}
}}
#elif defined(RP2040_PLATFORM)
#include <hardware/watchdog.h>
// Arduino-Pico's linker explicitly keeps this outside BSS and above the
// boot-ROM's scratch area. No watchdog scratch registers are commandeered.
static volatile mesh::power::RecoveryRecord regulator_record
__attribute__((section(".uninitialized_data.regulator_recovery"), used));
namespace mesh { namespace power {
volatile RecoveryRecord& platformRecoveryRecord() { return regulator_record; }
bool platformRecoverySupported() { return true; }
ResetKind platformResetKind() {
if (watchdog_enable_caused_reboot()) return ResetKind::Crash;
return watchdog_caused_reboot() ? ResetKind::Intentional : ResetKind::Cold;
}
}}
#elif !defined(NRF52_PLATFORM)
namespace mesh { namespace power {
static volatile RecoveryRecord regulator_record;
volatile RecoveryRecord& platformRecoveryRecord() { return regulator_record; }
bool platformRecoverySupported() { return false; }
ResetKind platformResetKind() { return ResetKind::Unknown; }
}}
#endif
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include "RegulatorRecovery.h"
namespace mesh { namespace power {
volatile RecoveryRecord& platformRecoveryRecord();
ResetKind platformResetKind();
bool platformRecoverySupported();
}}
+206
View File
@@ -0,0 +1,206 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
namespace mesh { namespace power {
constexpr uint8_t RECOVERY_MCU = 1;
constexpr uint8_t RECOVERY_RADIO = 2;
constexpr uint8_t RECOVERY_DOMAINS = RECOVERY_MCU | RECOVERY_RADIO;
constexpr uint8_t REGULATOR_CRASH_THRESHOLD = 3;
constexpr uint32_t REGULATOR_HEALTHY_MS = 300000;
constexpr uint32_t REGULATOR_BATTERY_FRESH_MS = 60000;
enum class ResetKind : uint8_t { Cold, Intentional, Wake, Crash, Unknown };
// Keep this POD in reset-retained RAM, never in flash. The checksum is committed
// last; an interrupted write or an uninitialised retained region cannot add a
// crash to the next boot. Only the sticky fallback mask needs flash storage.
struct RecoveryRecord {
uint32_t magic;
uint16_t version;
uint16_t bytes;
uint32_t startedAt;
uint32_t heartbeatAt;
uint32_t batteryAt;
uint8_t activeDcdcMask;
uint8_t fallbackMask;
uint8_t mcuCrashes;
uint8_t radioCrashes;
uint8_t batteryPercent;
uint8_t flags;
uint16_t reserved;
uint32_t crc;
};
static_assert(sizeof(RecoveryRecord) <= 64, "regulator recovery retention budget");
static_assert(sizeof(RecoveryRecord) == 32, "regulator recovery record layout");
static_assert(offsetof(RecoveryRecord, crc) == 28, "regulator recovery CRC layout");
class RecoveryPolicy {
public:
explicit RecoveryPolicy(volatile RecoveryRecord& retained) : retained_(retained), state_{} {}
// Read the previous boot exactly once, before arm(). A cold/intentional/wake
// or unclassified reset breaks a streak, but never undoes a saved fallback.
void begin(ResetKind reset, uint8_t savedFallbackMask) {
RecoveryRecord previous{};
read(previous);
const bool valid = isValid(previous);
state_ = RecoveryRecord{};
state_.magic = kMagic;
state_.version = kVersion;
state_.bytes = sizeof(RecoveryRecord);
state_.fallbackMask = savedFallbackMask & RECOVERY_DOMAINS;
if (valid) {
state_.fallbackMask |= previous.fallbackMask;
if (reset == ResetKind::Crash && eligible(previous)) {
state_.mcuCrashes = nextCount(previous, RECOVERY_MCU, previous.mcuCrashes);
state_.radioCrashes = nextCount(previous, RECOVERY_RADIO, previous.radioCrashes);
if (state_.mcuCrashes >= REGULATOR_CRASH_THRESHOLD)
state_.fallbackMask |= RECOVERY_MCU;
if (state_.radioCrashes >= REGULATOR_CRASH_THRESHOLD)
state_.fallbackMask |= RECOVERY_RADIO;
}
}
// Leave the new boot disarmed until its actual selected modes and measured
// battery are known. A crash before that point cannot extend the streak.
commit();
}
// Call before enabling either DC-DC regulator / starting the radio. Each bit
// describes an actually selected DC-DC domain, not mere board capability.
void arm(uint8_t activeDcdcMask, uint32_t now, uint8_t percent, bool valid) {
state_.activeDcdcMask = activeDcdcMask & RECOVERY_DOMAINS & ~state_.fallbackMask;
state_.startedAt = state_.heartbeatAt = now;
state_.flags = kArmed;
setBattery(now, percent, valid);
commit();
}
// Heartbeats must not refresh the battery timestamp: a stale/unknown reading
// must never turn a USB-only device or a discharged battery into an event.
void heartbeat(uint32_t now) {
state_.heartbeatAt = now;
if ((state_.flags & kArmed) && !(state_.flags & kHealthy) &&
elapsed(now, state_.startedAt) >= REGULATOR_HEALTHY_MS) {
state_.mcuCrashes = state_.radioCrashes = 0;
state_.flags |= kHealthy;
}
commit();
}
void updateBattery(uint32_t now, uint8_t percent, bool valid) {
setBattery(now, percent, valid);
commit();
}
void updateActiveDomains(uint8_t activeDcdcMask) {
state_.activeDcdcMask = activeDcdcMask & RECOVERY_DOMAINS & ~state_.fallbackMask;
commit();
}
// Convenience for callers that really take a fresh battery measurement each
// tick. Use heartbeat() separately when measurements are less frequent.
void tick(uint32_t now, uint8_t percent, bool valid) {
setBattery(now, percent, valid);
heartbeat(now);
}
void markCleanReset() {
state_.flags = (state_.flags & ~kArmed) | kClean;
state_.mcuCrashes = state_.radioCrashes = 0;
commit();
}
// A deliberate user mode change resets only that domain's learned fallback.
void clearDomain(uint8_t domains) {
domains &= RECOVERY_DOMAINS;
state_.fallbackMask &= ~domains;
if (domains & RECOVERY_MCU) state_.mcuCrashes = 0;
if (domains & RECOVERY_RADIO) state_.radioCrashes = 0;
commit();
}
uint8_t fallbackMask() const { return state_.fallbackMask; }
uint8_t crashCount(uint8_t domain) const {
return domain == RECOVERY_MCU ? state_.mcuCrashes :
domain == RECOVERY_RADIO ? state_.radioCrashes : 0;
}
private:
static constexpr uint32_t kMagic = 0x52504752; // RGPR
static constexpr uint16_t kVersion = 1;
static constexpr uint8_t kArmed = 1;
static constexpr uint8_t kClean = 2;
static constexpr uint8_t kBatteryValid = 4;
static constexpr uint8_t kHealthy = 8;
static constexpr uint8_t kFlags = kArmed | kClean | kBatteryValid | kHealthy;
volatile RecoveryRecord& retained_;
RecoveryRecord state_;
static uint32_t elapsed(uint32_t now, uint32_t then) { return now - then; }
static uint32_t checksum(const RecoveryRecord& record) {
uint32_t crc = 0xffffffff;
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(&record);
for (size_t i = 0; i < offsetof(RecoveryRecord, crc); ++i) {
crc ^= bytes[i];
for (uint8_t bit = 0; bit < 8; ++bit)
crc = (crc >> 1) ^ (0xedb88320u & (0u - (crc & 1u)));
}
return ~crc;
}
static bool isValid(const RecoveryRecord& record) {
return record.magic == kMagic && record.version == kVersion &&
record.bytes == sizeof(RecoveryRecord) && record.reserved == 0 &&
!(record.activeDcdcMask & ~RECOVERY_DOMAINS) &&
!(record.fallbackMask & ~RECOVERY_DOMAINS) &&
!(record.flags & ~kFlags) && record.batteryPercent <= 100 &&
record.mcuCrashes <= REGULATOR_CRASH_THRESHOLD &&
record.radioCrashes <= REGULATOR_CRASH_THRESHOLD &&
record.crc == checksum(record);
}
static bool eligible(const RecoveryRecord& record) {
return (record.flags & (kArmed | kClean | kBatteryValid)) == (kArmed | kBatteryValid) &&
record.batteryPercent >= 50 &&
elapsed(record.heartbeatAt, record.batteryAt) <= REGULATOR_BATTERY_FRESH_MS;
}
static uint8_t nextCount(const RecoveryRecord& previous, uint8_t domain, uint8_t count) {
if (!(previous.activeDcdcMask & domain) || (previous.fallbackMask & domain)) return 0;
return count < REGULATOR_CRASH_THRESHOLD ? count + 1 : REGULATOR_CRASH_THRESHOLD;
}
void setBattery(uint32_t now, uint8_t percent, bool valid) {
state_.batteryAt = now;
state_.batteryPercent = percent <= 100 ? percent : 0;
if (valid && percent <= 100) state_.flags |= kBatteryValid;
else state_.flags &= ~kBatteryValid;
}
void read(RecoveryRecord& result) const {
const volatile uint8_t* source = reinterpret_cast<const volatile uint8_t*>(&retained_);
uint8_t* target = reinterpret_cast<uint8_t*>(&result);
for (size_t i = 0; i < sizeof(result); ++i) target[i] = source[i];
}
void commit() {
state_.crc = checksum(state_);
// Invalidate before changing any payload, then publish its CRC last. No
// flash writes, heap allocation, interrupt dependencies, or platform APIs.
retained_.magic = 0;
retained_.crc = 0;
volatile uint8_t* target = reinterpret_cast<volatile uint8_t*>(&retained_);
const uint8_t* source = reinterpret_cast<const uint8_t*>(&state_);
for (size_t i = sizeof(state_.magic); i < offsetof(RecoveryRecord, crc); ++i)
target[i] = source[i];
retained_.magic = state_.magic;
retained_.crc = state_.crc;
}
};
}} // namespace mesh::power
+264
View File
@@ -0,0 +1,264 @@
#include "RegulatorSettings.h"
#include "RegulatorPlatform.h"
#include "NRF52VoltageRules.h"
#include "NRF52VoltagePolicy.h"
#include "PersistentStoreFormat.h"
#include <Arduino.h>
#include <Dispatcher.h>
#include <stdio.h>
#include <string.h>
namespace mesh { namespace power {
namespace {
MainBoard* main_board = nullptr;
Radio* main_radio = nullptr;
FILESYSTEM* settings_fs = nullptr;
RegulatorMode requested[2] = {RegulatorMode::Default, RegulatorMode::Default};
uint8_t saved_fallback = 0;
bool initialized = false;
bool fallback_saved = true;
uint32_t last_service = 0, last_battery = 0, last_save = 0;
RecoveryPolicy& recovery() {
static RecoveryPolicy policy(platformRecoveryRecord());
return policy;
}
const char* const path = "/regulator_v1";
const char* const temporary = "/regulator_v1.tmp";
const char* const backup = "/regulator_v1.bak";
void encode(RegulatorMode mcu, RegulatorMode radio, uint8_t fallback, uint8_t (&data)[12]) {
memcpy(data, "REG1", 4);
data[4] = uint8_t(mcu); data[5] = uint8_t(radio);
data[6] = fallback; data[7] = 0;
storage::writeLE32(data + 8, storage::updateCRC32(0xffffffffUL, data, 8));
}
bool readFile(const char* name, uint8_t (&data)[12]) {
if (!settings_fs) return false;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
File file(*settings_fs);
if (!file.open(name, FILE_O_READ)) return false;
#else
File file = settings_fs->open(name, "r");
if (!file) return false;
#endif
const bool complete = file.size() == sizeof(data)
&& file.read(data, sizeof(data)) == sizeof(data);
file.close();
return complete && memcmp(data, "REG1", 4) == 0
&& data[4] <= 2 && data[5] <= 2 && data[6] <= RECOVERY_DOMAINS
&& data[7] == 0 && storage::readLE32(data + 8)
== storage::updateCRC32(0xffffffffUL, data, 8);
}
bool save(RegulatorMode mcu, RegulatorMode radio, uint8_t fallback) {
if (!settings_fs) return false;
settings_fs->remove(temporary);
if (settings_fs->exists(temporary)) return false;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
File file(*settings_fs);
if (!file.open(temporary, FILE_O_WRITE)) return false;
#elif defined(RP2040_PLATFORM)
File file = settings_fs->open(temporary, "w");
if (!file) return false;
#else
File file = settings_fs->open(temporary, "w", true);
if (!file) return false;
#endif
uint8_t data[12], checked[12];
encode(mcu, radio, fallback, data);
const bool written = file.write(data, sizeof(data)) == sizeof(data);
file.close();
if (!written || !readFile(temporary, checked)
|| memcmp(data, checked, sizeof(data)) != 0) return false;
if (settings_fs->exists(backup) && !settings_fs->remove(backup)) return false;
if (settings_fs->exists(path) && !settings_fs->rename(path, backup)) return false;
if (!settings_fs->rename(temporary, path)) {
if (settings_fs->exists(backup)) settings_fs->rename(backup, path);
return false;
}
settings_fs->remove(backup);
saved_fallback = fallback;
return true;
}
uint8_t readBatteryPercent(bool& valid) {
uint16_t samples[3];
for (unsigned i = 0; i < 3; ++i) {
samples[i] = main_board->getBattMilliVolts();
if (i != 2) delay(5);
}
if (samples[0] > samples[1]) { auto v = samples[0]; samples[0] = samples[1]; samples[1] = v; }
if (samples[1] > samples[2]) { auto v = samples[1]; samples[1] = samples[2]; samples[2] = v; }
if (samples[0] > samples[1]) samples[1] = samples[0];
const uint16_t mv = samples[1];
valid = mv >= 2000 && mv <= 5500;
if (!valid) return 0;
#if defined(NRF52_PLATFORM) && defined(NRF52_POWER_MANAGEMENT)
// Board ADC methods already return calibrated millivolts.
return configuredBatteryPercent(mv);
#else
return batteryPercent(mv, 3000, 4200);
#endif
}
RegulatorMode effective(bool mcu) {
RegulatorMode mode = requested[mcu ? 0 : 1];
if (mode == RegulatorMode::Default) mode = mcu
? main_board->defaultMcuRegulatorMode() : main_radio->defaultRegulatorMode();
const uint8_t bit = mcu ? RECOVERY_MCU : RECOVERY_RADIO;
if (mode == RegulatorMode::Dcdc && (recovery().fallbackMask() & bit)) mode = RegulatorMode::Ldo;
return mode;
}
}
const char* regulatorModeName(RegulatorMode mode) {
return mode == RegulatorMode::Dcdc ? "dcdc" : mode == RegulatorMode::Ldo ? "ldo" : "auto";
}
uint8_t configuredRegulatorModes(bool mcu) {
if (!main_board || !main_radio) return 0;
return mcu ? main_board->mcuRegulatorModes() : main_radio->regulatorModes();
}
RegulatorMode requestedRegulatorMode(bool mcu) { return requested[mcu ? 0 : 1]; }
RegulatorMode activeRegulatorMode(bool mcu) {
if (!main_board || !main_radio) return RegulatorMode::Default;
return mcu ? main_board->activeMcuRegulatorMode() : main_radio->activeRegulatorMode();
}
const char* activeRegulatorModeName(bool mcu) {
const auto mode = activeRegulatorMode(mcu);
return mode == RegulatorMode::Default ? "unknown" : regulatorModeName(mode);
}
bool regulatorFallbackActive(bool mcu) {
return initialized && (recovery().fallbackMask() & (mcu ? RECOVERY_MCU : RECOVERY_RADIO));
}
bool regulatorRecoveryAvailable() { return initialized && platformRecoverySupported(); }
bool beginRegulatorSettings(MainBoard& board, Radio& radio, FILESYSTEM* fs) {
if (initialized) return true;
main_board = &board; main_radio = &radio; settings_fs = fs;
if (!board.mcuRegulatorModes() && !radio.regulatorModes()) return true;
uint8_t data[12];
bool loaded = readFile(path, data);
if (!loaded && readFile(backup, data)) {
loaded = true;
settings_fs->remove(path);
settings_fs->rename(backup, path);
}
if (loaded) {
requested[0] = RegulatorMode(data[4]); requested[1] = RegulatorMode(data[5]);
saved_fallback = data[6];
}
recovery().begin(platformRecoverySupported() ? platformResetKind() : ResetKind::Unknown,
saved_fallback);
initialized = true;
uint8_t active_dcdc = 0;
bool valid = false;
const uint8_t percent = readBatteryPercent(valid);
const uint32_t now = millis();
const auto cpu_mode = effective(true), radio_mode = effective(false);
if (configuredRegulatorModes(true) && cpu_mode == RegulatorMode::Dcdc) active_dcdc |= RECOVERY_MCU;
if (configuredRegulatorModes(false) && radio_mode == RegulatorMode::Dcdc) active_dcdc |= RECOVERY_RADIO;
recovery().arm(active_dcdc, now, percent, valid);
fallback_saved = recovery().fallbackMask() == saved_fallback
|| save(requested[0], requested[1], recovery().fallbackMask());
bool okay = true;
if (configuredRegulatorModes(true)) {
if (!board.applyMcuRegulator(cpu_mode)) {
active_dcdc &= ~RECOVERY_MCU;
recovery().arm(active_dcdc, now, percent, valid);
okay = false;
}
}
if (configuredRegulatorModes(false) && !radio.configureBootRegulator(radio_mode)) {
active_dcdc &= ~RECOVERY_RADIO;
recovery().updateActiveDomains(active_dcdc);
okay = false;
}
last_service = last_battery = last_save = now;
return okay;
}
void serviceRegulatorSettings() {
if (!initialized) return;
const uint32_t now = millis();
if (uint32_t(now - last_service) < 1000) return;
last_service = now;
const uint8_t active_dcdc =
(activeRegulatorMode(true) == RegulatorMode::Dcdc ? RECOVERY_MCU : 0)
| (activeRegulatorMode(false) == RegulatorMode::Dcdc ? RECOVERY_RADIO : 0);
recovery().updateActiveDomains(active_dcdc);
if (uint32_t(now - last_battery) >= 30000) {
bool valid = false;
const uint8_t percent = readBatteryPercent(valid);
last_battery = millis();
recovery().updateBattery(last_battery, percent, valid);
}
recovery().heartbeat(millis());
if (!fallback_saved && uint32_t(now - last_save) >= 30000) {
last_save = now;
fallback_saved = save(requested[0], requested[1], recovery().fallbackMask());
}
}
bool handleRegulatorCommand(const char* command, char* reply, size_t capacity) {
const bool get = strncmp(command, "get ", 4) == 0;
const bool set = strncmp(command, "set ", 4) == 0;
if (!get && !set) return false;
const char* key = command + 4;
bool mcu;
size_t length;
if (strncmp(key, "mcu.regulator", 13) == 0) { mcu = true; length = 13; }
else if (strncmp(key, "radio.regulator", 15) == 0) { mcu = false; length = 15; }
else return false;
if (key[length] != 0 && key[length] != ' ') return false;
if (!initialized || !configuredRegulatorModes(mcu)) {
snprintf(reply, capacity, "Error: %s regulator selection unsupported", mcu ? "MCU" : "radio");
return true;
}
const char* value = key + length;
if (get) {
if (*value) snprintf(reply, capacity, "Error: unexpected argument");
else snprintf(reply, capacity, "> %s; active %s; fallback %s; crashes %u; recovery %s%s",
regulatorModeName(requestedRegulatorMode(mcu)), activeRegulatorModeName(mcu),
regulatorFallbackActive(mcu) ? "ldo" : "off",
unsigned(recovery().crashCount(mcu ? RECOVERY_MCU : RECOVERY_RADIO)),
regulatorRecoveryAvailable() ? "on" : "unsupported",
fallback_saved ? "" : "; fallback not saved");
return true;
}
while (*value == ' ') ++value;
RegulatorMode mode;
if (strcmp(value, "auto") == 0) mode = RegulatorMode::Default;
else if (strcmp(value, "dcdc") == 0) mode = RegulatorMode::Dcdc;
else if (strcmp(value, "ldo") == 0) mode = RegulatorMode::Ldo;
else { snprintf(reply, capacity, "Error: use set %s.regulator auto|dcdc|ldo", mcu ? "mcu" : "radio"); return true; }
if (!regulatorModeSupported(configuredRegulatorModes(mcu), mode)) {
snprintf(reply, capacity, "Error: regulator mode unsupported by this hardware");
return true;
}
const uint8_t domain = mcu ? RECOVERY_MCU : RECOVERY_RADIO;
const uint8_t fallback = recovery().fallbackMask() & ~domain;
const auto cpu = mcu ? mode : requested[0], rf = mcu ? requested[1] : mode;
if (!save(cpu, rf, fallback)) {
snprintf(reply, capacity, "Error: regulator setting save failed");
return true;
}
requested[mcu ? 0 : 1] = mode;
recovery().clearDomain(domain);
fallback_saved = true;
snprintf(reply, capacity, "OK - %s regulator %s saved; reboot required", mcu ? "MCU" : "radio", regulatorModeName(mode));
return true;
}
void markRegulatorCleanReset() {
if (initialized) recovery().markCleanReset();
else {
RecoveryPolicy policy(platformRecoveryRecord());
policy.begin(ResetKind::Intentional, 0);
policy.markCleanReset();
}
}
}} // namespace mesh::power
extern "C" void meshRegulatorCleanReset() {
// Intentional resets can happen before the settings/filesystem is ready.
mesh::power::markRegulatorCleanReset();
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <helpers/IdentityStore.h>
#include "RegulatorMode.h"
namespace mesh {
class MainBoard;
class Radio;
namespace power {
// Called once after the primary filesystem and board ADC are ready, before
// display/radio/wireless initialization. Choices never alter an active radio.
bool beginRegulatorSettings(MainBoard& board, Radio& radio, FILESYSTEM* fs);
void serviceRegulatorSettings();
bool handleRegulatorCommand(const char* command, char* reply, size_t capacity);
const char* regulatorModeName(RegulatorMode mode);
uint8_t configuredRegulatorModes(bool mcu);
RegulatorMode requestedRegulatorMode(bool mcu);
RegulatorMode activeRegulatorMode(bool mcu);
const char* activeRegulatorModeName(bool mcu);
bool regulatorFallbackActive(bool mcu);
bool regulatorRecoveryAvailable();
}}
extern "C" void meshRegulatorCleanReset();
+4
View File
@@ -35,6 +35,9 @@
#if defined(NRF52_PLATFORM) && defined(USE_TINYUSB)
#include <Adafruit_TinyUSB.h>
#endif
#if defined(NRF52_PLATFORM)
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
#endif
#if MESH_ESP32_HWCDC_SESSION_GUARD
#include "UsbAsciiBinarySwitch.h"
#endif
@@ -1907,6 +1910,7 @@ extern "C" void meshTinyUsbCdcLineStateChanged(uint8_t instance, bool dtr,
cdc_line_coding_t coding;
tud_cdc_get_line_coding(&coding);
if (coding.bit_rate == 1200) {
if (meshRegulatorCleanReset) meshRegulatorCleanReset();
TinyUSB_Port_EnterDFU();
}
} else if (instance == 0) {
+4 -1
View File
@@ -21,6 +21,7 @@ static const char* const WC_ALLOWED_SET_KEYS[] = {
"name", "bluetooth.name", "bluetooth.mac", "bluetooth.stealth", "lost.reply",
"lat", "lon", "radio", "tx", "af", "rxdelay", "txdelay",
"cad", "radio.rxgain", "radio.fem.rxgain", "radio.fem.txgain", "radio.rxps", "powersaving",
"mcu.regulator", "radio.regulator",
"repeat",
"display.mode", "display.timeout", "display.usb.mode", "display.usb.timeout",
"display.inbox", "display.rotation",
@@ -70,7 +71,9 @@ static inline bool wcSetKeyRequiresReboot(const char* key) {
return key != NULL
&& (wcIsEspNowChannelKey(key)
|| strcmp(key, "bluetooth.mac") == 0
|| strcmp(key, "bluetooth.stealth") == 0);
|| strcmp(key, "bluetooth.stealth") == 0
|| strcmp(key, "mcu.regulator") == 0
|| strcmp(key, "radio.regulator") == 0);
}
// The admin password maps to the top-level `password` command, not a setter, so
File diff suppressed because it is too large. Load diff
+15 -1
View File
@@ -13,6 +13,7 @@ static_assert(sizeof(WEBCONFIG_AP_PREFIX) <= 28,
#include <ArduinoJson.h>
#include <helpers/ui/DisplayPowerSettings.h>
#include <helpers/ui/DisplayBuildFlags.h>
#include <helpers/RegulatorSettings.h>
#include <Preferences.h>
#include <esp_wifi.h>
#include <esp_arduino_version.h>
@@ -2331,11 +2332,23 @@ void WebConfigServer::handleLogout(AsyncWebServerRequest* req) {
req->send(res);
}
static void webConfigRegulatorsSnapshot(JsonObject regulators) {
regulators["recovery"] = mesh::power::regulatorRecoveryAvailable();
for (uint8_t domain = 0; domain < 2; ++domain) {
const bool mcu = domain == 0;
JsonObject entry = regulators.createNestedObject(mcu ? "mcu" : "radio");
entry["modes"] = mesh::power::configuredRegulatorModes(mcu);
entry["requested"] = mesh::power::regulatorModeName(mesh::power::requestedRegulatorMode(mcu));
entry["active"] = mesh::power::activeRegulatorModeName(mcu);
entry["fallback"] = mesh::power::regulatorFallbackActive(mcu);
}
}
void WebConfigServer::handleConfigGet(AsyncWebServerRequest* req) {
if (_mode == MODE_OFF) { req->send(503); return; }
if (!checkAuth(req)) { req->send(401, "application/json", "{\"error\":\"auth\"}"); return; }
DynamicJsonDocument doc(6144);
DynamicJsonDocument doc(6656);
{
WCLock lock(_mux);
NodeSnapshot node = {};
@@ -2400,6 +2413,7 @@ void WebConfigServer::handleConfigGet(AsyncWebServerRequest* req) {
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
wifi["espnow_channel"] = mesh::wifi::loadConfiguredEspNowChannel();
#endif
webConfigRegulatorsSnapshot(doc.createNestedObject("regulators"));
#ifdef WITH_MQTT_BRIDGE
MQTTPrefs* obs = static_cast<MQTTPrefs*>(_mqtt_prefs);
+3
View File
@@ -18,6 +18,7 @@
#include "OtaSelf.h" // SelfFwInfo / ota_self_firmware (running-image base_hash gate)
#endif
#elif defined(NRF52_PLATFORM)
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
#include "OtaVerify.h"
#include "OtaSelf.h"
#include "OtaFlashLayout_nrf52.h"
@@ -498,6 +499,7 @@ static bool ota_nrf52_clear_reset_reasons() {
}
void ota_reboot_to_apply() { // public: set the apply magic + reset (does not return)
if (meshRegulatorCleanReset) meshRegulatorCleanReset();
uint8_t stage_handoff = GPREGRET2_OTA_STAGE_LEGACY;
#if defined(OTA_RAK_AUTO_STORE) || defined(OTA_TOWER_AUTO_STORE)
if (g_nrf52_apply_store) {
@@ -546,6 +548,7 @@ void ota_reboot_to_apply() { // public: set the apply magic +
}
void ota_reboot_to_bootloader_update() {
if (meshRegulatorCleanReset) meshRegulatorCleanReset();
uint8_t source = GPREGRET2_OTA_STAGE_QSPI;
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
source = GPREGRET2_OTA_STAGE_EXPANDED;
+18 -5
View File
@@ -1,6 +1,7 @@
#pragma once
#include <RadioLib.h>
#include "RadioRegulatorState.h"
#include "MeshCore.h"
#include "RXPowerSaving.h"
#include "LR1110RxRecovery.h"
@@ -19,6 +20,9 @@
#endif
class CustomLR1110 : public LR1110 {
// Existing LR11xx recipes already require the fitted DC/DC network.
mesh::power::RadioRegulatorState _regulator{mesh::power::REGULATOR_DCDC | mesh::power::REGULATOR_LDO,
mesh::power::RegulatorMode::Dcdc};
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
@@ -33,6 +37,11 @@ class CustomLR1110 : public LR1110 {
public:
CustomLR1110(Module *mod) : LR1110(mod) { }
uint8_t regulatorModes() const { return _regulator.modes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const { return _regulator.defaultMode(); }
bool configureBootRegulator(mesh::power::RegulatorMode mode) { return _regulator.configureBoot(mode); }
mesh::power::RegulatorMode activeRegulatorMode() const { return _regulator.activeMode(); }
void setRfSwitchTable(const uint32_t (&pins)[Module::RFSWITCH_MAX_PINS],
const Module::RfSwitchMode_t table[]) {
_morse_rf_switch.capture(this->mod, pins, table);
@@ -40,12 +49,14 @@ class CustomLR1110 : public LR1110 {
}
int16_t beginMorseReceive(float frequency_mhz) {
const auto regulator = _regulator.beginInitialization();
_profileSwitch.invalidate();
_profilePacketDirty = false;
int16_t status = LR1110::beginGFSK(frequency_mhz,
mesh::MorseRx::ReceiverBitRateKbps, 0.6f, 4.8f, 2, 16, tcxoVoltage);
if (status == RADIOLIB_ERR_NONE) status = applyMeshCoreTcxoDelay();
if (status == RADIOLIB_ERR_NONE) status = setRegulatorDCDC();
if (status == RADIOLIB_ERR_NONE) status = regulator == mesh::power::RegulatorMode::Ldo
? setRegulatorLDO() : setRegulatorDCDC();
if (status == RADIOLIB_ERR_NONE) status = _morse_rf_switch.restore(
[this](uint8_t enable, const uint8_t* modes) {
return setDioAsRfSwitch(enable, modes[0], modes[1], modes[2],
@@ -60,7 +71,7 @@ class CustomLR1110 : public LR1110 {
}
// Infinite continuous RX samples envelope power independently of the
// FSK packet detector; no direct-data pin or TX/PA activation is used.
return status;
return _regulator.finishInitialization(status);
}
void setProfileSwitchOptimization(bool enabled) {
@@ -233,14 +244,16 @@ class CustomLR1110 : public LR1110 {
int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7,
uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10,
uint16_t preambleLength = 8, float tcxoVoltage = 1.6) {
const auto regulator = _regulator.beginInitialization();
_profileSwitch.invalidate();
_profilePacketDirty = false;
int16_t state = LR1110::begin(freq, bw, sf, cr, syncWord, power, preambleLength,
tcxoVoltage);
if (state == RADIOLIB_ERR_NONE) state = applyMeshCoreTcxoDelay();
// RadioLib begin() defaults to LDO; use the LR1110 DC/DC regulator.
if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC();
return state;
// Native begin() resets to LDO; restore the boot choice on every reset.
if (state == RADIOLIB_ERR_NONE) state = regulator == mesh::power::RegulatorMode::Ldo
? setRegulatorLDO() : setRegulatorDCDC();
return _regulator.finishInitialization(state);
}
int16_t applyMeshCoreTcxoDelay() {
@@ -13,6 +13,16 @@ class CustomLR1110Wrapper : public RadioLibWrapper {
DeepInitCallback _deep_init;
public:
uint8_t regulatorModes() const override { return static_cast<CustomLR1110*>(_radio)->regulatorModes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const override {
return static_cast<CustomLR1110*>(_radio)->defaultRegulatorMode();
}
bool configureBootRegulator(mesh::power::RegulatorMode mode) override {
return static_cast<CustomLR1110*>(_radio)->configureBootRegulator(mode);
}
mesh::power::RegulatorMode activeRegulatorMode() const override {
return static_cast<CustomLR1110*>(_radio)->activeRegulatorMode();
}
~CustomLR1110Wrapper() { unregisterInterruptAction(); }
CustomLR1110Wrapper(CustomLR1110& radio, mesh::MainBoard& board)
: RadioLibWrapper(radio, board), _deep_init(NULL) { }
+18 -5
View File
@@ -1,11 +1,15 @@
#pragma once
#include <RadioLib.h>
#include "RadioRegulatorState.h"
#include "MeshCore.h"
#include "LR11x0MorseRfSwitch.h"
#include "../MorseRx.h"
class CustomLR1121 : public LR1121 {
// Existing LR11xx recipes already require the fitted DC/DC network.
mesh::power::RadioRegulatorState _regulator{mesh::power::REGULATOR_DCDC | mesh::power::REGULATOR_LDO,
mesh::power::RegulatorMode::Dcdc};
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
@@ -18,6 +22,11 @@ class CustomLR1121 : public LR1121 {
public:
CustomLR1121(Module *mod) : LR1121(mod) { }
uint8_t regulatorModes() const { return _regulator.modes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const { return _regulator.defaultMode(); }
bool configureBootRegulator(mesh::power::RegulatorMode mode) { return _regulator.configureBoot(mode); }
mesh::power::RegulatorMode activeRegulatorMode() const { return _regulator.activeMode(); }
int16_t setProfileProtocol(uint8_t syncWord) {
int16_t status = LR1121::setSyncWord(syncWord);
if (status == RADIOLIB_ERR_NONE) status = LR1121::setCRC(2);
@@ -33,9 +42,11 @@ class CustomLR1121 : public LR1121 {
}
int16_t beginMorseReceive(float frequency_mhz) {
const auto regulator = _regulator.beginInitialization();
int16_t status = LR1121::beginGFSK(frequency_mhz,
mesh::MorseRx::ReceiverBitRateKbps, 0.6f, 4.8f, 2, 16, tcxoVoltage);
if (status == RADIOLIB_ERR_NONE) status = setRegulatorDCDC();
if (status == RADIOLIB_ERR_NONE) status = regulator == mesh::power::RegulatorMode::Ldo
? setRegulatorLDO() : setRegulatorDCDC();
if (status == RADIOLIB_ERR_NONE) status = _morse_rf_switch.restore(
[this](uint8_t enable, const uint8_t* modes) {
return setDioAsRfSwitch(enable, modes[0], modes[1], modes[2],
@@ -46,17 +57,19 @@ class CustomLR1121 : public LR1121 {
syncWordLength = preambleDetLength = 0;
status = LR1121::setCRC(0);
}
return status;
return _regulator.finishInitialization(status);
}
int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7,
uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10,
uint16_t preambleLength = 8, float tcxoVoltage = 1.6) {
const auto regulator = _regulator.beginInitialization();
int16_t state = LR1121::begin(freq, bw, sf, cr, syncWord, power, preambleLength,
tcxoVoltage);
// RadioLib begin() defaults to LDO; use the LR1121 DC/DC regulator.
if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC();
return state;
// Native begin() resets to LDO; restore the boot choice on every reset.
if (state == RADIOLIB_ERR_NONE) state = regulator == mesh::power::RegulatorMode::Ldo
? setRegulatorLDO() : setRegulatorDCDC();
return _regulator.finishInitialization(state);
}
size_t getPacketLength(bool update) override {
@@ -8,6 +8,16 @@ class CustomLR1121Wrapper : public RadioLibWrapper {
using DeepInitCallback = bool (*)();
DeepInitCallback _deep_init = nullptr;
public:
uint8_t regulatorModes() const override { return static_cast<CustomLR1121*>(_radio)->regulatorModes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const override {
return static_cast<CustomLR1121*>(_radio)->defaultRegulatorMode();
}
bool configureBootRegulator(mesh::power::RegulatorMode mode) override {
return static_cast<CustomLR1121*>(_radio)->configureBootRegulator(mode);
}
mesh::power::RegulatorMode activeRegulatorMode() const override {
return static_cast<CustomLR1121*>(_radio)->activeRegulatorMode();
}
~CustomLR1121Wrapper() { unregisterInterruptAction(); }
CustomLR1121Wrapper(CustomLR1121& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool supportsMorseTx() const override { return true; }
+26 -8
View File
@@ -1,6 +1,7 @@
#pragma once
#include <RadioLib.h>
#include "RadioRegulatorState.h"
#include "MeshCore.h"
#include "LR2021Band.h"
#include "../MorseRx.h"
@@ -21,6 +22,13 @@
#endif
class CustomLR2021 : public LR2021 {
#if LR2021_USE_DCDC
mesh::power::RadioRegulatorState _regulator{mesh::power::REGULATOR_DCDC | mesh::power::REGULATOR_LDO,
mesh::power::RegulatorMode::Dcdc};
#else
// An LDO-only layout must not enable SIMO without its external inductor.
mesh::power::RadioRegulatorState _regulator{mesh::power::REGULATOR_LDO, mesh::power::RegulatorMode::Ldo};
#endif
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
@@ -130,6 +138,11 @@ class CustomLR2021 : public LR2021 {
public:
CustomLR2021(Module *mod) : LR2021(mod) { irqDioNum = LR2021_IRQ_DIO; }
uint8_t regulatorModes() const { return _regulator.modes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const { return _regulator.defaultMode(); }
bool configureBootRegulator(mesh::power::RegulatorMode mode) { return _regulator.configureBoot(mode); }
mesh::power::RegulatorMode activeRegulatorMode() const { return _regulator.activeMode(); }
#if LR2021_TX_PREARM_MS > 0
using LR2021::standby;
int16_t standby() override {
@@ -164,13 +177,16 @@ class CustomLR2021 : public LR2021 {
}
int16_t beginMorseReceive(float frequency_mhz) {
const auto regulator = _regulator.beginInitialization();
const uint8_t gain = mesh::lr2021::isHighBand(frequency_mhz) ? gainModeHf : gainModeLf;
int16_t status = LR2021::beginGFSK(frequency_mhz,
mesh::MorseRx::ReceiverBitRateKbps, 0.6f, 4.8f, 2, 16, tcxoVoltage);
// Native modem initialization resets the regulator to LDO. Verified
// SIMO boards must restore DC/DC before admitting RX after any reset.
// Native initialization resets to LDO. SIMO is allowed only on verified
// layouts; an explicit LDO choice on those boards is checked as well.
if (status == RADIOLIB_ERR_NONE && regulator == mesh::power::RegulatorMode::Dcdc)
status = setRegulatorDCDC();
#if LR2021_USE_DCDC
if (status == RADIOLIB_ERR_NONE) status = setRegulatorDCDC();
else if (status == RADIOLIB_ERR_NONE) status = setRegulatorLDO();
#endif
if (status == RADIOLIB_ERR_NONE) status = restoreRfSwitchTable();
if (status == RADIOLIB_ERR_NONE) status = LR2021::setRxBoostedGainMode(gain);
@@ -183,10 +199,11 @@ class CustomLR2021 : public LR2021 {
status = LR2021::setCRC(0);
}
// launchMode() retains this chip's band-aware RX RF switch selection.
return status;
return _regulator.finishInitialization(status);
}
bool std_init(SPIClass* spi = NULL) {
const auto regulator = _regulator.beginInitialization();
#ifdef LR2021_TCXO_VOLTAGE
const float tcxo = LR2021_TCXO_VOLTAGE;
#else
@@ -220,15 +237,16 @@ class CustomLR2021 : public LR2021 {
if (status != RADIOLIB_ERR_NONE) {
mesh::usbLoggingPort().print("ERROR: radio init failed: ");
mesh::usbLoggingPort().println(status);
_regulator.finishInitialization(status);
return false; // fail
}
// Also reached when restoring LoRa after Morse RX or hard recovery.
if (regulator == mesh::power::RegulatorMode::Dcdc) status = setRegulatorDCDC();
#if LR2021_USE_DCDC
// Also reached by radioDeepInit() when restoring LoRa after Morse RX.
// Check this before later setters can overwrite a regulator error.
status = setRegulatorDCDC();
if (status != RADIOLIB_ERR_NONE) return false;
else status = setRegulatorLDO();
#endif
if (_regulator.finishInitialization(status) != RADIOLIB_ERR_NONE) return false;
status = setCRC(2);
if (status != RADIOLIB_ERR_NONE) return false;
@@ -16,6 +16,16 @@
class CustomLR2021Wrapper : public RadioLibWrapper {
public:
uint8_t regulatorModes() const override { return static_cast<CustomLR2021*>(_radio)->regulatorModes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const override {
return static_cast<CustomLR2021*>(_radio)->defaultRegulatorMode();
}
bool configureBootRegulator(mesh::power::RegulatorMode mode) override {
return static_cast<CustomLR2021*>(_radio)->configureBootRegulator(mode);
}
mesh::power::RegulatorMode activeRegulatorMode() const override {
return static_cast<CustomLR2021*>(_radio)->activeRegulatorMode();
}
~CustomLR2021Wrapper() { unregisterInterruptAction(); }
CustomLR2021Wrapper(CustomLR2021& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool supportsMorseTx() const override { return true; }
+30 -3
View File
@@ -1,6 +1,7 @@
#pragma once
#include <RadioLib.h>
#include "RadioRegulatorState.h"
#include <math.h>
#include "MeshCore.h"
#include "RXPowerSaving.h"
@@ -39,6 +40,12 @@
#endif
class CustomSX1262 : public SX1262 {
#if defined(SX126X_USE_REGULATOR_LDO) && SX126X_USE_REGULATOR_LDO
mesh::power::RadioRegulatorState _regulator{mesh::power::REGULATOR_LDO, mesh::power::RegulatorMode::Ldo};
#else
mesh::power::RadioRegulatorState _regulator{mesh::power::REGULATOR_DCDC | mesh::power::REGULATOR_LDO,
mesh::power::RegulatorMode::Dcdc};
#endif
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
@@ -52,6 +59,11 @@ class CustomSX1262 : public SX1262 {
public:
CustomSX1262(Module *mod) : SX1262(mod) { }
uint8_t regulatorModes() const { return _regulator.modes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const { return _regulator.defaultMode(); }
bool configureBootRegulator(mesh::power::RegulatorMode mode) { return _regulator.configureBoot(mode); }
mesh::power::RegulatorMode activeRegulatorMode() const { return _regulator.activeMode(); }
void invalidateMorseModemCache() {
_profileSwitch.invalidate();
_coldStandby = true;
@@ -296,6 +308,10 @@ class CustomSX1262 : public SX1262 {
uint8_t syncWord = RADIOLIB_SX126X_SYNC_WORD_PRIVATE, int8_t power = 10,
uint16_t preambleLength = 8, float tcxoVoltage = 1.6,
bool useRegulatorLDO = false) {
useRegulatorLDO = _regulator.beginInitialization(useRegulatorLDO
? mesh::power::RegulatorMode::Ldo : mesh::power::RegulatorMode::Dcdc)
== mesh::power::RegulatorMode::Ldo;
MESH_DEBUG_PRINTLN("SX1262 regulator requested: %s", useRegulatorLDO ? "LDO" : "DC-DC");
_profileSwitch.invalidate();
// A held dual-profile oscillator must not be selected during reset or
// setup, before DIO3/TCXO configuration has been restored. Restore the
@@ -309,7 +325,20 @@ class CustomSX1262 : public SX1262 {
tcxoVoltage, useRegulatorLDO);
if (state == RADIOLIB_ERR_NONE) state = applyMeshCoreTcxoDelay();
standbyXOSC = savedStandbyXosc;
return state;
return _regulator.finishInitialization(state);
}
// The GFSK reset used by Morse must retain the same boot choice and must
// invalidate reported active state when native initialization fails.
int16_t beginFSK(float freq = 434.0, float br = 4.8, float freqDev = 5.0,
float rxBw = 156.2, int8_t power = 10, uint16_t preambleLength = 16,
float tcxoVoltage = 1.6, bool useRegulatorLDO = false) override {
useRegulatorLDO = _regulator.beginInitialization(useRegulatorLDO
? mesh::power::RegulatorMode::Ldo : mesh::power::RegulatorMode::Dcdc)
== mesh::power::RegulatorMode::Ldo;
const int16_t status = SX1262::beginFSK(freq, br, freqDev, rxBw, power,
preambleLength, tcxoVoltage, useRegulatorLDO);
return _regulator.finishInitialization(status);
}
// Read tcxoVoltage back from the chip object rather than from the argument:
@@ -379,8 +408,6 @@ class CustomSX1262 : public SX1262 {
constexpr bool useRegulatorLDO = false;
#endif
MESH_DEBUG_PRINTLN("SX1262 regulator requested: %s", useRegulatorLDO ? "LDO" : "DC-DC");
#if defined(STATION_G3_ESP32)
// Reinitialization/watchdog recovery must keep the boot-selected slot;
// the build's historical slot macros are not the live SPI routing.
@@ -37,6 +37,16 @@ protected:
}
public:
uint8_t regulatorModes() const override { return static_cast<CustomSX1262*>(_radio)->regulatorModes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const override {
return static_cast<CustomSX1262*>(_radio)->defaultRegulatorMode();
}
bool configureBootRegulator(mesh::power::RegulatorMode mode) override {
return static_cast<CustomSX1262*>(_radio)->configureBootRegulator(mode);
}
mesh::power::RegulatorMode activeRegulatorMode() const override {
return static_cast<CustomSX1262*>(_radio)->activeRegulatorMode();
}
~CustomSX1262Wrapper() { unregisterInterruptAction(); }
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool supportsCarrierWave() const override { return true; }
@@ -0,0 +1,46 @@
#pragma once
#include <helpers/RegulatorMode.h>
namespace mesh { namespace power {
// A boot selection never writes the radio. Once initialization starts, freeze
// it so recovery and temporary modem changes cannot apply a pending CLI edit.
class RadioRegulatorState {
const uint8_t _modes;
const RegulatorMode _default;
RegulatorMode _requested = RegulatorMode::Default;
RegulatorMode _effective = RegulatorMode::Default;
RegulatorMode _active = RegulatorMode::Default;
bool _started = false;
public:
RadioRegulatorState(uint8_t modes, RegulatorMode default_mode)
: _modes(modes), _default(default_mode) { }
uint8_t modes() const { return _modes; }
RegulatorMode defaultMode() const { return _default; }
RegulatorMode activeMode() const { return _active; }
bool configureBoot(RegulatorMode mode) {
if (_started || !regulatorModeSupported(_modes, mode)) return false;
_requested = mode;
return true;
}
RegulatorMode beginInitialization(RegulatorMode legacy_default = RegulatorMode::Default) {
_started = true;
_active = RegulatorMode::Default; // no successful application in this init yet
const RegulatorMode fallback = regulatorModeSupported(_modes, legacy_default)
&& legacy_default != RegulatorMode::Default ? legacy_default : _default;
_effective = _requested == RegulatorMode::Default ? fallback : _requested;
return _effective;
}
int16_t finishInitialization(int16_t status) {
_active = status == 0 ? _effective : RegulatorMode::Default;
return status;
}
};
}} // namespace mesh::power
+5
View File
@@ -5,6 +5,7 @@
#include <cstring>
#include <vector>
#include <helpers/radiolib/LR2021Band.h>
#include <helpers/radiolib/RadioRegulatorState.h>
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_ERR_UNKNOWN -16
#define RADIOLIB_NC 0xffffffffU
@@ -74,6 +75,7 @@ struct Native {
}
int16_t applyMeshCoreTcxoDelay() { return call(2); }
int16_t setRegulatorDCDC() { return call(3); }
int16_t setRegulatorLDO() { return call(3); }
int16_t setDioAsRfSwitch(uint8_t mask,uint8_t a,uint8_t b,uint8_t c,uint8_t d,uint8_t e,uint8_t f,uint8_t g) {
enable=mask; const uint8_t values[]={a,b,c,d,e,f,g}; memcpy(modes,values,sizeof(modes));
return call(4);
@@ -112,17 +114,20 @@ using LR1110=Native; using LR1121=Native;
struct LR2021 : Native { enum { MODE_RX=3,MODE_RX_HF=6,MODE_END_OF_TABLE=0 }; };
using SX1276=Native;
struct LR1110Probe : LR1110 {
@LR1110_REGULATOR@
struct State { unsigned invalidations=0; void invalidate() { ++invalidations; } } _profileSwitch;
bool _profilePacketDirty=true,_rx_boosted=true;
LR11x0MorseRfSwitch _morse_rf_switch;
@LR1110@
};
struct LR1121Probe : LR1121 {
@LR1121_REGULATOR@
bool _rx_boosted=true;
LR11x0MorseRfSwitch _morse_rf_switch;
@LR1121@
};
struct LR2021Probe : LR2021 {
@LR2021_REGULATOR@
uint8_t gainModeLf=1,gainModeHf=3;
uint32_t _rf_switch_pins[Module::RFSWITCH_MAX_PINS]={0x80,0x81,RADIOLIB_NC};
const Module::RfSwitchMode_t* _rf_switch_table=nullptr;
+14 -3
View File
@@ -14,6 +14,7 @@ MockNrfUsbd mock_usbd;
static bool mounted = true, dtr = true, dfu = false;
static bool attached = true, armed_in = false;
static unsigned detaches = 0, attaches = 0;
static unsigned clean_reboots = 0;
static uint32_t baud = 115200;
static std::string fifo, host;
static std::function<void()> during_write;
@@ -47,7 +48,11 @@ uint32_t tud_cdc_n_write(uint8_t n, const void* data, uint32_t size) {
uint32_t tud_cdc_n_write_flush(uint8_t n) { assert(n == 0); return 0; }
uint32_t tud_cdc_n_write_clear(uint8_t n) { assert(n == 0); fifo.clear(); return 0; }
void tud_cdc_get_line_coding(cdc_line_coding_t* coding) { coding->bit_rate = baud; }
void TinyUSB_Port_EnterDFU() { dfu = true; }
extern "C" void meshRegulatorCleanReset() { ++clean_reboots; }
void TinyUSB_Port_EnterDFU() {
assert(clean_reboots == 1); // Mark the actual reset before entering the core.
dfu = true;
}
static void service() {
mesh::serviceUsbLoggingPort();
@@ -169,8 +174,14 @@ int main() {
drain();
assert(host.empty()); // old armed packet cannot leak into a new handle
baud = 1200; dtr = false;
assert(clean_reboots == 0); // Normal host closes/reconnects remain armed.
baud = 1200;
tud_cdc_line_coding_cb(0, nullptr);
tud_cdc_line_state_cb(0, true, false);
tud_cdc_line_state_cb(1, false, false);
assert(clean_reboots == 0 && !dfu); // Neither coding nor CDC1 requests DFU.
dtr = false;
tud_cdc_line_state_cb(0, false, false);
assert(dfu); // Existing bootloader entry is preserved.
assert(dfu && clean_reboots == 1); // Existing bootloader entry is preserved.
puts("native nRF52 console transport passed");
}
+1
View File
@@ -1,3 +1,4 @@
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
#include <helpers/ota/MotaContainer.h>
#include <helpers/ota/OtaByteIO.h>
+1
View File
@@ -1,3 +1,4 @@
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
#include <algorithm>
#include <cstdint>
#include <cstdio>
+268
View File
@@ -0,0 +1,268 @@
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
#include <iostream>
#include <helpers/RegulatorMode.h>
#include <helpers/RegulatorRecovery.h>
#include <helpers/IdentityStore.h>
#include <Dispatcher.h>
uint32_t mock_now=0;
#if defined(ESP32)
#include <esp_system.h>
int mock_reset_reason=ESP_RST_POWERON;
#elif defined(RP2040_PLATFORM)
bool mock_watchdog_enabled=false,mock_watchdog_any=false;
#else
namespace mesh { namespace power {
volatile RecoveryRecord test_retained;
ResetKind test_reset=ResetKind::Cold;
volatile RecoveryRecord& platformRecoveryRecord() { return test_retained; }
ResetKind platformResetKind() { return test_reset; }
bool platformRecoverySupported() { return true; }
unsigned configured_percent_calls=0;
uint8_t configuredBatteryPercent(uint16_t mv) {
++configured_percent_calls;
return mv<=3000 ? 0 : mv>=4200 ? 100 : uint8_t((mv-3000)*100/1200);
}
}}
#endif
#if !defined(NRF52_PLATFORM)
#include <helpers/RegulatorPlatform.cpp>
#endif
#include <helpers/RegulatorSettings.cpp>
using namespace mesh::power;
static void retainedClear() {
volatile uint8_t* p=reinterpret_cast<volatile uint8_t*>(&platformRecoveryRecord());
for(size_t i=0;i<sizeof(RecoveryRecord);++i)p[i]=0;
}
static void resetKind(ResetKind kind) {
#if defined(ESP32)
mock_reset_reason=kind==ResetKind::Crash ? ESP_RST_PANIC : kind==ResetKind::Intentional
? ESP_RST_SW : kind==ResetKind::Wake ? ESP_RST_DEEPSLEEP : kind==ResetKind::Cold
? ESP_RST_POWERON : ESP_RST_BROWNOUT;
#elif defined(RP2040_PLATFORM)
mock_watchdog_enabled=kind==ResetKind::Crash;
mock_watchdog_any=kind==ResetKind::Crash || kind==ResetKind::Intentional;
#else
test_reset=kind;
#endif
}
static void resetGlobals(bool cold=false) {
mesh::power::main_board=nullptr;mesh::power::main_radio=nullptr;
mesh::power::settings_fs=nullptr;
mesh::power::requested[0]=mesh::power::requested[1]=RegulatorMode::Default;
mesh::power::saved_fallback=0;mesh::power::initialized=false;
mesh::power::fallback_saved=true;
mesh::power::last_service=mesh::power::last_battery=mesh::power::last_save=0;
mock_now=0;
if(cold)retainedClear();
}
static void fresh(mesh::MainBoard& board,mesh::Radio& radio,MockFS& fs,
ResetKind kind=ResetKind::Cold,bool cold=false) {
resetGlobals(cold);resetKind(kind);board.resetHardware();radio.resetHardware();
assert(beginRegulatorSettings(board,radio,&fs));
assert(radio.active==RegulatorMode::Default); // selecting touches no active radio
radio.initialize();
}
static std::string command(const char* text) {
char reply[160];memset(reply,0xa5,sizeof(reply));
assert(handleRegulatorCommand(text,reply,sizeof(reply)));
assert(memchr(reply,0,sizeof(reply)));
return reply;
}
static void advance(uint32_t ms) { mock_now+=ms;serviceRegulatorSettings(); }
static void counts(unsigned cpu,unsigned rf) {
assert(recovery().crashCount(RECOVERY_MCU)==cpu);
assert(recovery().crashCount(RECOVERY_RADIO)==rf);
}
int main(int argc,char** argv) {
assert(argc==2);const std::string test=argv[1];
mesh::MainBoard board;mesh::Radio radio;MockFS fs;
if(test=="select") {
fresh(board,radio,fs,ResetKind::Cold,true);
assert(board.active==RegulatorMode::Dcdc && radio.active==RegulatorMode::Dcdc);
const unsigned applies=board.applies,configs=radio.configures,inits=radio.inits;
assert(command("set mcu.regulator ldo").find("reboot required")!=std::string::npos);
assert(command("set radio.regulator ldo").find("reboot required")!=std::string::npos);
assert(requestedRegulatorMode(true)==RegulatorMode::Ldo);
assert(requestedRegulatorMode(false)==RegulatorMode::Ldo);
assert(board.active==RegulatorMode::Dcdc && radio.active==RegulatorMode::Dcdc);
assert(board.applies==applies && radio.configures==configs && radio.inits==inits);
advance(1000);
assert(board.active==RegulatorMode::Dcdc && radio.active==RegulatorMode::Dcdc);
fresh(board,radio,fs,ResetKind::Cold,true);
assert(board.active==RegulatorMode::Ldo && radio.active==RegulatorMode::Ldo);
} else if(test=="fallback") {
fresh(board,radio,fs,ResetKind::Cold,true);
for(unsigned n=1;n<=3;++n) {
advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(n,n);
assert(regulatorFallbackActive(true)==(n==3));
assert(regulatorFallbackActive(false)==(n==3));
}
assert(board.active==RegulatorMode::Ldo && radio.active==RegulatorMode::Ldo);
assert(fs.files.count("/regulator_v1"));
fresh(board,radio,fs,ResetKind::Cold,true);counts(0,0);
assert(board.active==RegulatorMode::Ldo && radio.active==RegulatorMode::Ldo);
assert(command("get radio.regulator").find("fallback ldo")!=std::string::npos);
} else if(test=="domains") {
fresh(board,radio,fs,ResetKind::Cold,true);
command("set mcu.regulator ldo");fresh(board,radio,fs,ResetKind::Intentional);
for(unsigned n=1;n<=3;++n) { advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(0,n); }
assert(!regulatorFallbackActive(true) && regulatorFallbackActive(false));
const auto saved=fs.files["/regulator_v1"];
command("set mcu.regulator dcdc");
assert(regulatorFallbackActive(false) && !regulatorFallbackActive(true));
assert(requestedRegulatorMode(false)==RegulatorMode::Default);
fresh(board,radio,fs,ResetKind::Intentional);
assert(board.active==RegulatorMode::Dcdc && radio.active==RegulatorMode::Ldo);
command("set radio.regulator auto");
assert(!regulatorFallbackActive(false));
fresh(board,radio,fs,ResetKind::Intentional);
assert(radio.active==RegulatorMode::Dcdc);
} else if(test=="battery") {
for(unsigned mv:{3588u,0u,1999u,5501u}) {
fs.files.clear();board.mv=mv;
fresh(board,radio,fs,ResetKind::Cold,true);
for(unsigned n=0;n<4;++n) {advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(0,0);}
assert(!regulatorFallbackActive(true) && !regulatorFallbackActive(false));
}
board.mv=3600;fs.files.clear();fresh(board,radio,fs,ResetKind::Cold,true);
// Heartbeat alone cannot make a 60-second-old voltage sample fresh.
recovery().heartbeat(mock_now+REGULATOR_BATTERY_FRESH_MS+1);
fresh(board,radio,fs,ResetKind::Crash);counts(0,0);
// A median rejects one high outlier rather than guessing from the maximum.
board.samples={3588,5000,3588};fresh(board,radio,fs,ResetKind::Cold,true);
advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(0,0);
} else if(test=="clean") {
fresh(board,radio,fs,ResetKind::Cold,true);
for(unsigned n=1;n<=2;++n) {advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(n,n);}
meshRegulatorCleanReset();fresh(board,radio,fs,ResetKind::Crash);counts(0,0);
// A clean-reset marker cannot discard a learned fallback that has not yet
// reached flash because a write failed.
for(unsigned n=1;n<=2;++n) {advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(n,n);}
fs.fail_write=true;advance(1000);fresh(board,radio,fs,ResetKind::Crash);
assert(regulatorFallbackActive(true) && regulatorFallbackActive(false));
meshRegulatorCleanReset();
resetGlobals();meshRegulatorCleanReset(); // early reboot before filesystem setup
fs.fail_write=false;fresh(board,radio,fs,ResetKind::Crash);
assert(regulatorFallbackActive(true) && regulatorFallbackActive(false));
} else if(test=="atomic") {
for(unsigned failure=0;failure<5;++failure) {
fs=MockFS();fresh(board,radio,fs,ResetKind::Cold,true);
command("set mcu.regulator ldo");const auto old=fs.files["/regulator_v1"];
if(failure==0)fs.fail_write=true;
if(failure==1)fs.fail_open=true;
if(failure==2)fs.fail_rename_from="/regulator_v1";
if(failure==3)fs.fail_rename_from="/regulator_v1.tmp";
if(failure==4) {fs.fail_rename_from="/regulator_v1.tmp";fs.fail_restore=true;}
assert(command("set radio.regulator ldo").find("save failed")!=std::string::npos);
assert(requestedRegulatorMode(false)==RegulatorMode::Default);
fs.fail_write=fs.fail_open=fs.fail_restore=false;fs.fail_rename_from.clear();
fresh(board,radio,fs,ResetKind::Cold,true);
assert(requestedRegulatorMode(true)==RegulatorMode::Ldo);
assert(requestedRegulatorMode(false)==RegulatorMode::Default);
assert(fs.files["/regulator_v1"]==old);
}
} else if(test=="corruption") {
fresh(board,radio,fs,ResetKind::Cold,true);command("set mcu.regulator ldo");
const auto good=fs.files["/regulator_v1"];
for(size_t byte=0;byte<good.size();++byte) {
fs.files["/regulator_v1"]=good;fs.files["/regulator_v1"][byte]^=1;
fresh(board,radio,fs,ResetKind::Cold,true);
assert(requestedRegulatorMode(true)==RegulatorMode::Default);
assert(board.active==RegulatorMode::Dcdc);
}
fs.files["/regulator_v1"]=good;fs.files["/regulator_v1"].push_back(0);
fresh(board,radio,fs,ResetKind::Cold,true);assert(requestedRegulatorMode(true)==RegulatorMode::Default);
fs.files["/regulator_v1"]=good;fs.files["/regulator_v1"].pop_back();
fs.files["/regulator_v1.bak"]=good;
fresh(board,radio,fs,ResetKind::Cold,true);assert(board.active==RegulatorMode::Ldo);
} else if(test=="unsupported") {
board.modes=radio.modes=REGULATOR_LDO;board.default_mode=radio.default_mode=RegulatorMode::Ldo;
fresh(board,radio,fs,ResetKind::Cold,true);
assert(command("set mcu.regulator dcdc").find("unsupported")!=std::string::npos);
assert(command("set radio.regulator dcdc").find("unsupported")!=std::string::npos);
assert(board.active==RegulatorMode::Ldo && radio.active==RegulatorMode::Ldo);
assert(!board.dcdc_applied && !radio.dcdc_applied);
// A previously saved choice from a DC/DC-capable recipe cannot activate
// absent circuitry if that file is encountered by an LDO-only recipe.
board.modes=radio.modes=3;
command("set mcu.regulator dcdc");command("set radio.regulator dcdc");
board.modes=radio.modes=REGULATOR_LDO;
board.dcdc_applied=radio.dcdc_applied=false;
resetGlobals(true);resetKind(ResetKind::Cold);board.resetHardware();radio.resetHardware();
(void)beginRegulatorSettings(board,radio,&fs);
assert(!board.dcdc_applied && !radio.dcdc_applied);
assert(board.active!=RegulatorMode::Dcdc && radio.active!=RegulatorMode::Dcdc);
board.modes=radio.modes=0;
assert(command("get mcu.regulator").find("unsupported")!=std::string::npos);
assert(command("get radio.regulator").find("unsupported")!=std::string::npos);
} else if(test=="bounds") {
fresh(board,radio,fs,ResetKind::Cold,true);
for(const char* text:{"get mcu.regulatorx","set radio.regulatorx ldo","get radio.regulator.extra","get mcu.regulato","set radio.regulator\tldo","other"}) {
char reply[4]={'x','x','x','x'};assert(!handleRegulatorCommand(text,reply,sizeof(reply)));
assert(reply[0]=='x' && reply[3]=='x');
}
for(const char* text:{"get mcu.regulator arg","set radio.regulator", "set radio.regulator dcdc extra","set mcu.regulator LDO"})
assert(command(text).find("Error:")==0);
for(size_t size:{0u,1u,2u,8u,32u}) {
char guarded[40];memset(guarded,0x5a,sizeof(guarded));
assert(handleRegulatorCommand("get radio.regulator",guarded+3,size));
for(size_t i=0;i<3;++i)assert(guarded[i]==0x5a);
for(size_t i=size+3;i<sizeof(guarded);++i)assert(guarded[i]==0x5a);
if(size)assert(memchr(guarded+3,0,size));
}
} else if(test=="no_store") {
resetGlobals(true);resetKind(ResetKind::Cold);
assert(beginRegulatorSettings(board,radio,nullptr));radio.initialize();
assert(command("set radio.regulator ldo").find("save failed")!=std::string::npos);
assert(requestedRegulatorMode(false)==RegulatorMode::Default);
assert(radio.active==RegulatorMode::Dcdc);
} else if(test=="failed_radio") {
fresh(board,radio,fs,ResetKind::Cold,true);
for(unsigned n=1;n<=3;++n) {
radio.active=RegulatorMode::Default; // radio initialization never succeeded
advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(n,0);
}
assert(regulatorFallbackActive(true) && !regulatorFallbackActive(false));
} else if(test=="platform_unsupported") {
assert(!platformRecoverySupported());
fresh(board,radio,fs,ResetKind::Cold,true);
for(unsigned n=0;n<4;++n){advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(0,0);}
assert(!regulatorRecoveryAvailable());
assert(!regulatorFallbackActive(true) && !regulatorFallbackActive(false));
} else if(test=="platform") {
#if defined(ESP32)
for(int reason:{ESP_RST_PANIC,ESP_RST_INT_WDT,ESP_RST_TASK_WDT,ESP_RST_WDT}) {
mock_reset_reason=reason;assert(platformResetKind()==ResetKind::Crash);
}
mock_reset_reason=ESP_RST_SW;assert(platformResetKind()==ResetKind::Intentional);
mock_reset_reason=ESP_RST_DEEPSLEEP;assert(platformResetKind()==ResetKind::Wake);
for(int reason:{ESP_RST_POWERON,ESP_RST_EXT}) {mock_reset_reason=reason;assert(platformResetKind()==ResetKind::Cold);}
mock_reset_reason=ESP_RST_BROWNOUT;assert(platformResetKind()==ResetKind::Unknown);
#elif defined(RP2040_PLATFORM)
mock_watchdog_any=mock_watchdog_enabled=false;assert(platformResetKind()==ResetKind::Cold);
mock_watchdog_any=true;assert(platformResetKind()==ResetKind::Intentional);
mock_watchdog_enabled=true;assert(platformResetKind()==ResetKind::Crash);
#endif
assert(&platformRecoveryRecord()==&platformRecoveryRecord());
} else if(test=="healthy") {
fresh(board,radio,fs,ResetKind::Cold,true);advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(1,1);
for(unsigned n=0;n<301;++n)advance(1000);
fresh(board,radio,fs,ResetKind::Crash);counts(1,1);
} else if(test=="retry") {
fresh(board,radio,fs,ResetKind::Cold,true);
for(unsigned n=1;n<=2;++n){advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(n,n);}
fs.fail_write=true;advance(1000);fresh(board,radio,fs,ResetKind::Crash);
assert(command("get radio.regulator").find("fallback not saved")!=std::string::npos);
fs.fail_write=false;advance(30000);
assert(command("get radio.regulator").find("fallback not saved")==std::string::npos);
fresh(board,radio,fs,ResetKind::Cold,true);
assert(board.active==RegulatorMode::Ldo && radio.active==RegulatorMode::Ldo);
} else assert(false);
}
+12
View File
@@ -0,0 +1,12 @@
"""Extract radio regulator declarations for native production-method harnesses."""
from test_radio_receive_contract import method
def regulator_members(source):
start = source.index('{', source.index('class Custom')) + 1
end = source.index(' uint32_t _preambleMillis', start)
fields = source[start:end]
methods = '\n'.join(method(source, signature) for signature in (
'uint8_t regulatorModes(', 'mesh::power::RegulatorMode defaultRegulatorMode(',
'bool configureBootRegulator(', 'mesh::power::RegulatorMode activeRegulatorMode('))
return fields + '\n public:\n' + methods
+28
View File
@@ -10,6 +10,7 @@ import shutil
import subprocess
import tempfile
import unittest
from radio_regulator_harness import regulator_members
ROOT = Path(__file__).resolve().parents[1]
@@ -32,6 +33,7 @@ HARNESS = r'''
#include <vector>
#include <algorithm>
#include <helpers/radiolib/LR2021Band.h>
#include <helpers/radiolib/RadioRegulatorState.h>
@POLICY@
constexpr bool ExpectDcdc = @EXPECT_DCDC@;
static_assert((LR2021_USE_DCDC != 0)==ExpectDcdc,"unexpected board regulator default");
@@ -103,6 +105,9 @@ class LR2021 {
int16_t setRegulatorDCDC() {
++regulator_calls;const int16_t result=step(2);if(!result)regulator_on=true;return result;
}
int16_t setRegulatorLDO() {
++regulator_calls;const int16_t result=step(2);if(!result)regulator_on=false;return result;
}
int16_t setCRC(uint8_t value) { assert(value==0 || value==2);return step(3); }
int16_t explicitHeader() { return step(4); }
int16_t setRxBoostedGainMode(uint8_t value) { gain=value;return step(5); }
@@ -117,6 +122,7 @@ class LR2021 {
int16_t setSyncWord(uint8_t*,size_t length) { assert(length==0);return step(8); }
};
class CustomLR2021:public LR2021 {
@REGULATOR@
uint32_t _rf_switch_pins[Module::RFSWITCH_MAX_PINS]={};
const Module::RfSwitchMode_t* _rf_switch_table=nullptr;
@ROUTING@
@@ -154,6 +160,27 @@ static void routed(const CustomLR2021& radio) {
assert(radio.pulls[0]==1 && radio.pulls[1]==0);
}
int main() {
using mesh::power::RegulatorMode;
Module selection_module;
CustomLR2021 selected(&selection_module);configure(selected);
assert(selected.activeRegulatorMode()==RegulatorMode::Default && selected.calls.empty());
assert(!selected.configureBootRegulator(static_cast<RegulatorMode>(3)));
assert(selected.configureBootRegulator(RegulatorMode::Dcdc)==ExpectDcdc);
assert(selected.configureBootRegulator(RegulatorMode::Ldo));
assert(selected.calls.empty()); // selecting never touches SPI
assert(selected.std_init() && !selected.regulator_on);
assert(selected.activeRegulatorMode()==RegulatorMode::Ldo);
assert(!selected.configureBootRegulator(RegulatorMode::Default));
assert(selected.beginMorseReceive(2440.0f)==0 && !selected.regulator_on);
assert(selected.activeRegulatorMode()==RegulatorMode::Ldo);
assert(selected.std_init() && !selected.regulator_on); // recovery stays LDO
selected.fail=ExpectDcdc?2:1;
assert(!selected.std_init());
assert(selected.activeRegulatorMode()==RegulatorMode::Default);
assert(!selected.configureBootRegulator(RegulatorMode::Dcdc));
selected.fail=0;
assert(selected.std_init() && !selected.regulator_on);
assert(selected.activeRegulatorMode()==RegulatorMode::Ldo);
// Only verified/opted-in layouts reapply DC/DC after a native reset.
Module module;CustomLR2021 radio(&module);configure(radio);SPIClass spi;
assert(radio.std_init(&spi) && spi.started && radio.regulator_on==ExpectDcdc);
@@ -225,6 +252,7 @@ class LR2021PowerTest(unittest.TestCase):
self.assertIsNotNone(policy, "LR2021 board policy must remain explicit and default-safe")
for marker, body in {
"@POLICY@": policy.group(0),
"@REGULATOR@": regulator_members(driver),
"@EXPECT_DCDC@": "true" if "-DLR2021_USE_DCDC=1" in flags else "false",
"@ROUTING@": method(driver, "int16_t restoreRfSwitchTable("),
"@TABLE@": method(driver, "void setRfSwitchTable("),
+5 -1
View File
@@ -11,6 +11,7 @@ import shutil
import subprocess
import tempfile
import unittest
from radio_regulator_harness import regulator_members
ROOT = Path(__file__).resolve().parents[1]
@@ -34,6 +35,7 @@ HARNESS = r'''
#include <string>
#include <vector>
#include <helpers/radiolib/LR2021Band.h>
#include <helpers/radiolib/RadioRegulatorState.h>
@POLICY@
#define LR2021_TX_BUSY_TIMEOUT_MS 4UL
#define LR2021_USE_DCDC 0
@@ -195,6 +197,7 @@ public:
int16_t begin(float,float,uint8_t,uint8_t,uint8_t,int8_t,uint16_t,float) {
std::fill(masks,masks+7,0);applyMode(MODE_STBY);return 0;
}
int16_t setRegulatorDCDC() { assert(false); return -1; }
int16_t setCRC(uint8_t) { return 0; }
int16_t explicitHeader() { return 0; }
int16_t clearRxFifo() { return 0; }
@@ -203,6 +206,7 @@ public:
}
};
class CustomLR2021:public LR2021 {
@REGULATOR@
public:
using LR2021::LR2021;
uint32_t _rf_switch_pins[Module::RFSWITCH_MAX_PINS]={};
@@ -369,7 +373,7 @@ class LR2021TxPrearmTest(unittest.TestCase):
cls.binaries = {}
driver = (ROOT / "src/helpers/radiolib/CustomLR2021.h").read_text()
board = (ROOT / "variants/meshnology_w12/target.cpp").read_text()
source = HARNESS
source = HARNESS.replace("@REGULATOR@", regulator_members(driver))
for marker, signature in {
"@MASK@": "int16_t setTxStandbyMask(",
"@PREPARE@": "int16_t prepareTxRfSwitch(",
+3
View File
@@ -6,6 +6,7 @@ from test_morse_tx import compile_run, portable_radio_header
from test_carrier_wave import production_harness
from test_radio_receive_contract import method
from morse_rx_floor_step_experiment import legacy_estimator
from radio_regulator_harness import regulator_members
ROOT = Path(__file__).resolve().parents[1]
@@ -302,6 +303,8 @@ int main() {
for chip in ("LR1110", "LR1121", "LR2021", "SX1276"):
source = (ROOT / f"src/helpers/radiolib/Custom{chip}.h").read_text(encoding="utf-8")
code = code.replace(f"@{chip}@", method(source, "int16_t beginMorseReceive("))
if chip != "SX1276":
code = code.replace(f"@{chip}_REGULATOR@", regulator_members(source))
if chip == "LR2021":
code = code.replace("@LR2021_RESTORE@", method(source, "int16_t restoreRfSwitchTable("))
sx1262 = (ROOT / "src/helpers/radiolib/CustomSX1262.h").read_text(encoding="utf-8")
+1 -1
View File
@@ -15,7 +15,7 @@ SANITIZERS = ["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno
def portable_radio_header():
"""Use the exact Radio interface without unrelated packet/crypto mocks."""
dispatcher = (ROOT / "src/Dispatcher.h").read_text(encoding="utf-8")
return ("#pragma once\n#include <RadioProfiles.h>\n#include <helpers/MorseTx.h>\n#include <helpers/MorseRx.h>\nnamespace mesh {\n"
return ("#pragma once\n#include <RadioProfiles.h>\n#include <helpers/MorseTx.h>\n#include <helpers/MorseRx.h>\n#include <helpers/RegulatorMode.h>\nnamespace mesh {\n"
+ method(dispatcher, "enum class RadioParamApplyResult") + ";\n"
+ method(dispatcher, "class Radio {") + ";\n}\n")
+63
View File
@@ -77,8 +77,71 @@ class DfuSourcePatchTest(unittest.TestCase):
self.assertLess(fixed.index("SCB_ICSR_PENDSVCLR_Msk"),
fixed.index("sd_softdevice_vector_table_base_set"))
self.assertLess(fixed.index("sd_softdevice_vector_table_base_set"),
fixed.index("if (meshRegulatorCleanReset)"))
self.assertLess(fixed.index("if (meshRegulatorCleanReset)"),
fixed.index("mesh_nrf52_dfu_jump(NRF_UICR"))
def test_only_a_successful_dfu_handoff_marks_clean_with_optional_policy(self):
compiler = shutil.which("c++") or shutil.which("g++")
if not compiler:
self.skipTest("host C++ compiler required for DFU clean-reset regression")
code = r'''
#include <cassert>
#include <cstdint>
@DECLARATION@
static unsigned clean_marks, jumps, vector_calls;
static bool reject_vector;
struct Uicr { uint32_t NRFFW[1] = {0xF4000}; } uicr;
#define NRF_UICR (&uicr)
#define VERIFY_STATUS(expression, result) do { if ((expression) != 0) return result; } while (0)
uint32_t sd_softdevice_vector_table_base_set(uint32_t address) {
assert(address == 0xF4000);
++vector_calls;
return reject_vector ? 1 : 0;
}
void mesh_nrf52_dfu_jump(uint32_t address) {
assert(address == 0xF4000);
assert(clean_marks == EXPECTED_MARKS);
++jumps;
}
void attemptDfu(bool requested) {
if (!requested) return; // Advertising or an aborted update is not a reboot.
VERIFY_STATUS(sd_softdevice_vector_table_base_set(NRF_UICR->NRFFW[0]), );
@JUMP@
}
#if WITH_POLICY
extern "C" void meshRegulatorCleanReset() {
assert(jumps == 0 && vector_calls == 2 && !reject_vector);
++clean_marks;
}
#endif
int main() {
attemptDfu(false);
assert(clean_marks == 0 && vector_calls == 0 && jumps == 0);
reject_vector = true;
attemptDfu(true);
assert(clean_marks == 0 && vector_calls == 1 && jumps == 0);
reject_vector = false;
attemptDfu(true);
assert(clean_marks == EXPECTED_MARKS && vector_calls == 2 && jumps == 1);
}
'''.replace("@DECLARATION@", patch.CLEAN_RESET_DECLARATION).replace("@JUMP@", patch.JUMP)
with tempfile.TemporaryDirectory(prefix="meshcore-dfu-clean-reset-") as directory:
cpp = Path(directory) / "fixture.cpp"
cpp.write_text(code)
for policy in (False, True):
with self.subTest(policy=policy):
binary = Path(directory) / f"test-{int(policy)}"
built = subprocess.run([
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
f"-DWITH_POLICY={int(policy)}", f"-DEXPECTED_MARKS={int(policy)}",
str(cpp), "-o", str(binary),
], capture_output=True, text=True, timeout=30)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
tested = subprocess.run([str(binary)], capture_output=True,
text=True, timeout=10)
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
def test_unknown_or_partially_patched_framework_fails_closed(self):
for original in ("unrecognized framework", self.fixture().replace(
patch.OLD_JUMP, patch.JUMP), self.fixture().replace(
+22 -19
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Run real board startup methods against counted nRF52 regulator SDK calls."""
"""Board startup approves DC/DC hardware and defers activation to saved policy."""
import configparser
import os
from pathlib import Path
@@ -58,25 +58,28 @@ void sd_power_dcdc_mode_set(int mode) {
assert(base_calls == 1 && softdevice && mode == NRF_POWER_DCDC_ENABLE);
++sdk_calls;
}
void requireDcdc() {
void requireDeferredDcdc() {
assert(base_calls == 1);
assert(softdevice ? sdk_calls == 1 : power.DCDCEN == 1);
assert(sdk_calls == 0 && power.DCDCEN == 0);
}
void pinMode(int, int) { requireDcdc(); }
void digitalWrite(int, int) { requireDcdc(); }
void pinMode(int, int) { requireDeferredDcdc(); }
void digitalWrite(int, int) { requireDeferredDcdc(); }
void delay(unsigned) {}
void meshSolarStart() { requireDcdc(); }
void meshSolarStart() { requireDeferredDcdc(); }
struct Bus {
void setPins(int, int) { requireDcdc(); }
void begin() { requireDcdc(); }
void setPins(int, int) { requireDeferredDcdc(); }
void begin() { requireDeferredDcdc(); }
} Wire;
struct Uart { void begin(unsigned) { requireDcdc(); } } Serial;
struct Uart { void begin(unsigned) { requireDeferredDcdc(); } } Serial;
struct Peripheral {
void begin() { requireDcdc(); }
void init() { requireDcdc(); }
void begin() { requireDeferredDcdc(); }
void init() { requireDeferredDcdc(); }
};
struct Config {} power_config;
struct NRF52Board { void begin() { ++base_calls; } };
struct NRF52Board {
bool cpu_dcdc_supported = false;
void begin() { ++base_calls; }
};
struct NRF52BoardDCDC : NRF52Board { void begin(); };
struct @BOARD@ : NRF52BoardDCDC {
Peripheral periph_power, loRaFEMControl;
@@ -84,7 +87,7 @@ struct @BOARD@ : NRF52BoardDCDC {
void begin();
void checkBootVoltage(const Config*) {
++voltage_checks;
requireDcdc();
requireDeferredDcdc();
}
};
@METHODS@
@@ -94,8 +97,8 @@ int main() {
@BOARD@ board;
board.begin();
assert(base_calls == 1 && voltage_checks == @VOLTAGE_CHECKS@);
assert(sdk_calls == softdevice);
assert(power.DCDCEN == (softdevice ? 0U : 1U));
assert(board.cpu_dcdc_supported);
assert(sdk_calls == 0 && power.DCDCEN == 0);
}
}
'''
@@ -122,14 +125,14 @@ class Nrf52BoardRegulatorTests(unittest.TestCase):
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_rak4631_startup_uses_dcdc_before_voltage_check(self):
# Cover boot before BLE starts and an already-active SoftDevice. Also
# prove an obsolete downstream flag cannot silently force LDO again.
def test_rak4631_defers_dcdc_until_saved_policy_is_loaded(self):
# Board startup must not force either saved mode before policy loads,
# regardless of SoftDevice state or obsolete downstream flags.
for flags in ((), ('-DRAK4631_COMPANION_FORCE_LDO=1',)):
with self.subTest(flags=flags):
self.run_startup('variants/rak4631/RAK4631Board.cpp', 'RAK4631Board', flags)
def test_verified_boards_initialize_dcdc_before_peripherals(self):
def test_verified_boards_defer_dcdc_until_peripherals_and_policy_are_ready(self):
for filename, board, has_voltage_check in VERIFIED_BOARDS:
for power_management in (False, True):
with self.subTest(board=board, power_management=power_management):
+43 -11
View File
@@ -307,9 +307,10 @@ class Nrf52InternalBootloaderSelectorTest(unittest.TestCase):
class Nrf52HybridLinkerContractTest(unittest.TestCase):
def test_real_linker_markers_do_not_confuse_late_data_with_reservation(self):
compiler, linker, nm = (shutil.which(name) for name in ("cc", "ld", "nm"))
if not all((compiler, linker, nm)):
self.skipTest("native cc, ld and nm are required for linker fixtures")
compiler, linker, nm, readelf = (
shutil.which(name) for name in ("cc", "ld", "nm", "readelf"))
if not all((compiler, linker, nm, readelf)):
self.skipTest("native cc, ld, nm and readelf are required for linker fixtures")
# A large framework registration prefix moves .data beyond the auth
# ABI even in an unreserved map. Only the explicit region markers can
# distinguish it from a genuinely reserved receiver layout.
@@ -318,6 +319,8 @@ class Nrf52HybridLinkerContractTest(unittest.TestCase):
.long 0
.section .persistent_data,"aw"
.long 0
.section .regulator_recovery,"aw"
.space 64
.section .svc_data,"aw"
.space 128
.section .fs_data,"aw"
@@ -329,14 +332,27 @@ class Nrf52HybridLinkerContractTest(unittest.TestCase):
SECTIONS {
.data : { __data_start__ = .; *(.data) } > RAM
}
__HeapBase = __data_start__ + SIZEOF(.data);
__HeapLimit = ORIGIN(RAM) + LENGTH(RAM);
"""
with tempfile.TemporaryDirectory(prefix="meshcore-sd-auth-linker-") as directory:
work = Path(directory)
(work / "fixture.S").write_text(assembly, encoding="ascii")
(work / "nrf52_common.ld").write_text(common, encoding="ascii")
(work / "boards").mkdir()
shutil.copyfile(ROOT / "boards/nrf52_heap_reserve.ld",
work / "boards/nrf52_heap_reserve.ld")
subprocess.run([compiler, "-c", str(work / "fixture.S"), "-o", str(work / "fixture.o")],
check=True, capture_output=True, text=True, timeout=30)
for suffix in ("v6", "v7", "v6_sd_ota", "v6_mota64", "v7_mota64"):
(work / "overflow.S").write_text(
assembly.replace('.section .regulator_recovery,"aw"\n.space 64',
'.section .regulator_recovery,"aw"\n.space 65'),
encoding="ascii")
subprocess.run([compiler, "-c", str(work / "overflow.S"),
"-o", str(work / "overflow.o")],
check=True, capture_output=True, text=True, timeout=30)
for suffix in ("v6", "v7", "v6_extrafs", "v7_extrafs",
"v7_xiao_bootloader_ota", "v6_sd_ota", "v6_mota64", "v7_mota64"):
with self.subTest(suffix=suffix):
source = (ROOT / "boards" / f"nrf52840_s140_{suffix}.ld").read_text()
# Native tools suffice: omit only the ARM C library group;
@@ -349,13 +365,25 @@ SECTIONS {
output = subprocess.check_output([nm, str(work / "fixture.elf")], text=True)
symbols = {name: int(value, 16) for value, _kind, name in
(line.split() for line in output.splitlines())}
self.assertGreaterEqual(symbols["__data_start__"], 0x20006050)
if suffix in ("v6", "v7"):
self.assertGreaterEqual(symbols["__data_start__"], 0x20006090)
self.assertEqual(symbols["__regulator_recovery_start__"], 0x20006050)
self.assertEqual(symbols["__regulator_recovery_end__"], 0x20006090)
section_info = subprocess.check_output(
[readelf, "-SW", str(work / "fixture.elf")],
text=True)
self.assertRegex(section_info,
r"\.regulator_recovery\s+NOBITS\s+0*20006050\s+\S+\s+0*40\s")
if suffix in ("v6", "v7", "v6_extrafs", "v7_extrafs", "v7_xiao_bootloader_ota"):
self.assertNotIn("__mota_sd_auth_start__", symbols)
self.assertNotIn("__mota_sd_auth_end__", symbols)
else:
self.assertEqual(symbols["__mota_sd_auth_start__"], 0x20006008)
self.assertEqual(symbols["__mota_sd_auth_end__"], 0x20006050)
overflow = subprocess.run(
[linker, "-T", "fixture.ld", "overflow.o", "-o", "overflow.elf"],
cwd=work, text=True, capture_output=True, timeout=30)
self.assertNotEqual(overflow.returncode, 0,
"65-byte recovery record must fail the production link")
def test_sd_authorization_has_explicit_reserved_boundaries(self):
for suffix in ("v6_sd_ota", "v6_mota64", "v7_mota64"):
@@ -363,13 +391,14 @@ SECTIONS {
path = ROOT / "boards" / f"nrf52840_s140_{suffix}.ld"
regions = memory_regions(path)
source = path.read_text(encoding="utf-8")
self.assertEqual(regions["RAM"][0], 0x20006050)
self.assertEqual(regions["RAM"][0], 0x20006090)
self.assertEqual(regions["REGULATOR_RECOVERY_RAM"], (0x20006050, 64))
self.assertEqual(regions["PERSISTENT_RAM"], (0x20006000, 80))
self.assertIn("__mota_sd_auth_start__ = ", source)
self.assertIn("__mota_sd_auth_end__ = ", source)
self.assertIn("ASSERT(ORIGIN(RAM) >= __mota_sd_auth_end__,", source)
if suffix == "v6_sd_ota":
self.assertEqual(regions["RAM"][1], 0x20040000 - 0x20006050)
self.assertEqual(regions["RAM"][1], 0x20040000 - 0x20006090)
self.assertIn("ASSERT(__mota_sd_auth_start__ == 0x20006008,", source)
self.assertIn("ASSERT(__mota_sd_auth_end__ == 0x20006050,", source)
else:
@@ -398,13 +427,15 @@ SECTIONS {
self.assertEqual(hybrid["FLASH"], (flash_start, 0xED000 - flash_start))
self.assertEqual(hybrid["PERSISTENT_RAM"], (0x20006000, 8 + auth_len))
self.assertEqual(auth_start, 0x20006000 + 8)
self.assertEqual(hybrid["RAM"][0], auth_start + auth_len)
self.assertEqual(hybrid["REGULATOR_RECOVERY_RAM"], (auth_start + auth_len, 64))
self.assertEqual(normal["REGULATOR_RECOVERY_RAM"], hybrid["REGULATOR_RECOVERY_RAM"])
self.assertEqual(hybrid["RAM"][0], auth_start + auth_len + 64)
self.assertEqual(hybrid["RAM"][0] + hybrid["RAM"][1], arena_start)
self.assertEqual(hybrid["MOTA_RAM"], (arena_start, arena_len))
self.assertEqual(arena_start + arena_len, 0x20040000)
self.assertEqual(
normal["RAM"][1] - hybrid["RAM"][1],
arena_len + auth_len,
arena_len,
)
def test_linkers_export_and_assert_every_fixed_handoff_boundary(self):
@@ -416,7 +447,8 @@ SECTIONS {
"ASSERT(__mota_hybrid_handoff_start__ == 0x20006008,",
"ASSERT(__mota_hybrid_handoff_end__ == 0x20006050,",
"ASSERT(__mota_hybrid_handoff_end__ - __mota_hybrid_handoff_start__ == 72,",
"ASSERT(ORIGIN(RAM) == __mota_hybrid_handoff_end__,",
"ASSERT(ORIGIN(REGULATOR_RECOVERY_RAM) == __mota_hybrid_handoff_end__,",
"ASSERT(ORIGIN(RAM) == __regulator_recovery_end__,",
"ASSERT(ORIGIN(RAM) + LENGTH(RAM) == ORIGIN(MOTA_RAM),",
"ASSERT(ORIGIN(MOTA_RAM) + LENGTH(MOTA_RAM) == 0x20040000,",
".mota_ram (NOLOAD)",
+243
View File
@@ -0,0 +1,243 @@
#!/usr/bin/env python3
"""Execute the production nRF52 CPU regulator and reset classification paths."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
BOARD = ROOT / "src/helpers/NRF52Board.cpp"
def run_native(source):
with tempfile.TemporaryDirectory(prefix="meshcore-nrf52-regulator-") as directory:
cpp, binary = Path(directory) / "test.cpp", Path(directory) / "test"
cpp.write_text(source, encoding="ascii")
compiled = subprocess.run(
[os.environ.get("CXX", "c++"), "-std=c++17", "-Wall", "-Wextra",
"-Werror", "-I", str(ROOT / "src"), str(cpp), "-o", str(binary)],
capture_output=True, text=True, timeout=60)
if compiled.returncode:
raise AssertionError(compiled.stdout + compiled.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
if result.returncode:
raise AssertionError(result.stdout + result.stderr)
class Nrf52RegulatorTests(unittest.TestCase):
def test_saved_cpu_mode_uses_checked_sdk_and_rejects_unsupported_boards(self):
source = BOARD.read_text()
methods = "\n".join(body(source, signature) for signature in (
"uint8_t NRF52Board::mcuRegulatorModes() const",
"mesh::power::RegulatorMode NRF52Board::defaultMcuRegulatorMode() const",
"mesh::power::RegulatorMode NRF52Board::activeMcuRegulatorMode() const",
"bool NRF52Board::applyMcuRegulator(mesh::power::RegulatorMode mode)",
))
run_native(r'''
#include <cassert>
#include <cstdint>
#include "helpers/RegulatorMode.h"
using mesh::power::RegulatorMode;
constexpr uint32_t NRF_SUCCESS = 0;
constexpr uint8_t NRF_POWER_DCDC_DISABLE = 0, NRF_POWER_DCDC_ENABLE = 1;
static uint32_t query_result, setter_result;
static uint8_t softdevice, requested;
static unsigned setter_calls;
struct Power { unsigned DCDCEN; } power;
#define NRF_POWER (&power)
namespace mesh_nrf52 {
uint32_t softdeviceIsEnabled(uint8_t& enabled) {
enabled = softdevice;
return query_result;
}
}
uint32_t sd_power_dcdc_mode_set(uint8_t mode) {
assert(softdevice);
requested = mode;
++setter_calls;
return setter_result;
}
struct NRF52Board {
bool cpu_dcdc_supported = false;
RegulatorMode cpu_regulator_mode = RegulatorMode::Ldo;
uint8_t mcuRegulatorModes() const;
RegulatorMode defaultMcuRegulatorMode() const;
RegulatorMode activeMcuRegulatorMode() const;
bool applyMcuRegulator(RegulatorMode);
};
@METHODS@
int main() {
NRF52Board board;
power.DCDCEN = 77;
assert(board.mcuRegulatorModes() == 0);
assert(board.defaultMcuRegulatorMode() == RegulatorMode::Default);
assert(!board.applyMcuRegulator(RegulatorMode::Dcdc));
assert(!board.applyMcuRegulator(RegulatorMode::Ldo));
assert(!board.applyMcuRegulator(RegulatorMode::Default));
assert(power.DCDCEN == 77 && setter_calls == 0);
board.cpu_dcdc_supported = true;
assert(board.mcuRegulatorModes() == 3);
assert(board.defaultMcuRegulatorMode() == RegulatorMode::Dcdc);
assert(!board.applyMcuRegulator(static_cast<RegulatorMode>(255)));
for (softdevice = 0; softdevice < 2; ++softdevice) {
power.DCDCEN = 77;
query_result = 5;
assert(!board.applyMcuRegulator(RegulatorMode::Dcdc));
assert(power.DCDCEN == 77 && setter_calls == 0);
query_result = 0;
setter_result = 0;
assert(board.applyMcuRegulator(RegulatorMode::Default));
assert(board.activeMcuRegulatorMode() == RegulatorMode::Dcdc);
assert(softdevice ? requested == 1 : power.DCDCEN == 1);
assert(board.applyMcuRegulator(RegulatorMode::Ldo));
assert(board.activeMcuRegulatorMode() == RegulatorMode::Ldo);
assert(softdevice ? requested == 0 : power.DCDCEN == 0);
if (softdevice) {
setter_result = 4;
assert(!board.applyMcuRegulator(RegulatorMode::Dcdc));
assert(board.activeMcuRegulatorMode() == RegulatorMode::Ldo);
assert(power.DCDCEN == 77);
}
setter_calls = 0;
}
}
'''.replace("@METHODS@", methods))
def test_reset_classifier_excludes_wakes_and_intentional_resets(self):
method = body(BOARD.read_text(), "ResetKind platformResetKind()")
run_native(r'''
#include <cassert>
#include <cstdint>
#include "helpers/RegulatorRecovery.h"
using mesh::power::ResetKind;
constexpr uint32_t NRF_SUCCESS = 0;
#define POWER_RESETREAS_RESETPIN_Msk 1U
#define POWER_RESETREAS_DOG_Msk 2U
#define POWER_RESETREAS_SREQ_Msk 4U
#define POWER_RESETREAS_LOCKUP_Msk 8U
#define POWER_RESETREAS_OFF_Msk 16U
#define POWER_RESETREAS_LPCOMP_Msk 32U
#define POWER_RESETREAS_VBUS_Msk 64U
#define POWER_RESETREAS_DIF_Msk 128U
static uint32_t regulator_reset_reason;
static uint8_t regulator_shutdown_reason;
static uint32_t query_result = 3, clear_result = 7;
static unsigned query_calls, clear_calls;
struct Power { uint32_t RESETREAS = 0; } power;
#define NRF_POWER (&power)
namespace mesh_nrf52 {
uint32_t softdeviceIsEnabled(uint8_t& enabled) {
++query_calls;
enabled = 1;
return query_result;
}
}
uint32_t sd_power_reset_reason_clr(uint32_t bits) {
assert(bits == 0xFFFFFFFFUL);
++clear_calls;
return clear_result;
}
@METHOD@
int main() {
assert(platformResetKind() == ResetKind::Unknown);
assert(query_calls == 1 && clear_calls == 0 && power.RESETREAS == 0);
query_result = 0;
assert(platformResetKind() == ResetKind::Unknown);
assert(clear_calls == 1 && power.RESETREAS == 0);
clear_result = 0;
regulator_reset_reason = 0;
assert(platformResetKind() == ResetKind::Cold);
assert(clear_calls == 2);
for (uint32_t reason : {2U, 4U, 8U, 2U | 4U}) {
regulator_reset_reason = reason;
assert(platformResetKind() == ResetKind::Crash);
}
for (uint32_t reason : {16U, 32U, 64U, 16U | 2U}) {
regulator_reset_reason = reason;
assert(platformResetKind() == ResetKind::Wake);
}
for (uint32_t reason : {1U, 128U, 1U | 2U}) {
regulator_reset_reason = reason;
assert(platformResetKind() == ResetKind::Intentional);
}
regulator_reset_reason = 8U;
for (uint8_t shutdown : {0x4C, 0x55, 0x42}) {
regulator_shutdown_reason = shutdown;
assert(platformResetKind() == ResetKind::Intentional);
}
regulator_shutdown_reason = 0;
regulator_reset_reason = 0x80000000UL;
assert(platformResetKind() == ResetKind::Unknown);
assert(clear_calls == 2);
}
'''.replace("@METHOD@", method).replace("#include <cstdint>",
"#include <cstdint>\n#include <initializer_list>"))
def test_reset_acknowledgement_without_softdevice_uses_direct_register(self):
method = body(BOARD.read_text(), "ResetKind platformResetKind()")
run_native(r'''
#include <cassert>
#include <cstdint>
#include "helpers/RegulatorRecovery.h"
using mesh::power::ResetKind;
constexpr uint32_t NRF_SUCCESS = 0;
#define POWER_RESETREAS_RESETPIN_Msk 1U
#define POWER_RESETREAS_DOG_Msk 2U
#define POWER_RESETREAS_SREQ_Msk 4U
#define POWER_RESETREAS_LOCKUP_Msk 8U
static uint32_t regulator_reset_reason;
static uint8_t regulator_shutdown_reason;
static unsigned query_calls, clear_calls;
struct Power { uint32_t RESETREAS = 0; } power;
#define NRF_POWER (&power)
namespace mesh_nrf52 {
uint32_t softdeviceIsEnabled(uint8_t& enabled) {
++query_calls;
enabled = 0;
return 0;
}
}
uint32_t sd_power_reset_reason_clr(uint32_t) {
++clear_calls;
return 9;
}
@METHOD@
int main() {
assert(platformResetKind() == ResetKind::Cold);
assert(query_calls == 1 && clear_calls == 0);
assert(power.RESETREAS == 0xFFFFFFFFUL);
regulator_reset_reason = POWER_RESETREAS_DOG_Msk;
assert(platformResetKind() == ResetKind::Crash);
assert(query_calls == 1 && clear_calls == 0);
}
'''.replace("@METHOD@", method))
def test_clean_reboot_marker_is_optional_and_precedes_reset(self):
method = body(BOARD.read_text(), "void NRF52Board::reboot()")
run_native(r'''
#include <cassert>
static unsigned resets, clean_marks;
static void (*meshRegulatorCleanReset)();
void markClean() { assert(resets == 1); ++clean_marks; }
void NVIC_SystemReset() {
assert(resets == 0 || clean_marks == 1);
++resets;
}
struct NRF52Board { void reboot(); };
@METHOD@
int main() {
NRF52Board board;
board.reboot();
assert(resets == 1 && clean_marks == 0);
meshRegulatorCleanReset = markClean;
board.reboot();
assert(resets == 2 && clean_marks == 1);
}
'''.replace("@METHOD@", method))
if __name__ == "__main__":
unittest.main()
+7 -3
View File
@@ -5,6 +5,7 @@ import subprocess
import tempfile
import unittest
from test_radio_receive_contract import method
from radio_regulator_harness import regulator_members
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
@@ -447,7 +448,7 @@ class ProfileScanTest(unittest.TestCase):
(Path(folder)/'Arduino.h').write_text('#pragma once\n#include <cstdint>\n#include <cmath>\n')
cpp.write_text(HARNESS.replace('@METHODS@',methods))
result=subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-Wall','-Wextra',
'-I',folder,'-I',str(ROOT/'src'),str(cpp),'-o',str(exe)],capture_output=True,text=True)
'-I',folder,'-I',str(ROOT/'src'),'-I',str(ROOT/'src'),str(cpp),'-o',str(exe)],capture_output=True,text=True)
self.assertEqual(result.returncode,0,result.stderr)
result=subprocess.run([str(exe)],capture_output=True,text=True)
self.assertEqual(result.returncode,0,result.stderr)
@@ -458,6 +459,8 @@ class ProfileScanTest(unittest.TestCase):
harness=r'''
#include <cassert>
#include <cstdint>
#include <helpers/radiolib/RadioRegulatorState.h>
#define MESH_DEBUG_PRINTLN(...) ((void)0)
#define RADIOLIB_SX126X_SYNC_WORD_PRIVATE 0x12
#define RADIOLIB_ERR_NONE 0
struct SX1262 {
@@ -470,6 +473,7 @@ struct SX1262 {
}
};
struct CustomSX1262: SX1262 {
@REGULATOR@
bool _coldStandby=false;
bool _tcxoWakePending=true;
struct { bool invalidated=false; void invalidate() { invalidated=true; } } _profileSwitch;
@@ -496,9 +500,9 @@ int main() {
'''
with tempfile.TemporaryDirectory() as folder:
cpp=Path(folder)/'test.cpp'; exe=Path(folder)/'test.exe'
cpp.write_text('#include <initializer_list>\n'+harness.replace('@BEGIN@',begin))
cpp.write_text('#include <initializer_list>\n'+harness.replace('@BEGIN@',begin).replace('@REGULATOR@',regulator_members(source)))
result=subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-Wall','-Wextra',
str(cpp),'-o',str(exe)],capture_output=True,text=True)
'-I',str(ROOT/'src'),str(cpp),'-o',str(exe)],capture_output=True,text=True)
self.assertEqual(result.returncode,0,result.stderr)
result=subprocess.run([str(exe)],capture_output=True,text=True)
self.assertEqual(result.returncode,0,result.stderr)
+183
View File
@@ -0,0 +1,183 @@
"""Execute boot selection, regulator errors, Morse resets and radio recovery."""
from pathlib import Path
import unittest
from test_morse_tx import compile_run
from test_radio_receive_contract import method
from radio_regulator_harness import regulator_members
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <cstddef>
#include <initializer_list>
#include <helpers/radiolib/RadioRegulatorState.h>
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_ERR_UNKNOWN -16
#define RADIOLIB_SX126X_SYNC_WORD_PRIVATE 0x12
#define RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE 0x12
#define RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE 0x12
#define LORA_FREQ 909.5f
#define LORA_BW 500.0f
#define LORA_SF 5
#define LORA_CR 5
#define LORA_TX_POWER 2
#define MESH_DEBUG_PRINTLN(...) ((void)0)
struct SPIClass {};
struct Module {};
namespace mesh {
struct Logger { void print(const char*) {} void println(int) {} };
inline Logger& usbLoggingPort() { static Logger logger;return logger; }
struct MorseRx { static constexpr float ReceiverBitRateKbps=4.8f; };
namespace lr2021 { inline bool isHighBand(float f) { return f>1900; } }
}
struct Native {
int fail=0,spi=0,regulator_writes=0,modem_resets=0;
bool useRegulatorLDO=true,standbyXOSC=true;
float tcxoVoltage=1.6f;
uint8_t syncWordLength=4,preambleDetLength=4,gainModeLf=1,gainModeHf=3;
int step(int phase) { ++spi;return fail==phase ? -100-phase : 0; }
int nativeReset() { ++modem_resets;useRegulatorLDO=true;return step(1); }
int16_t begin(float,float,uint8_t,uint8_t,uint8_t,int8_t,uint16_t,float) {
return nativeReset();
}
int16_t begin(float,float,uint8_t,uint8_t,uint8_t,int8_t,uint16_t,float,bool ldo) {
assert(!standbyXOSC);
int result=nativeReset();
if(!result) { ++regulator_writes;result=step(2);if(!result)useRegulatorLDO=ldo; }
return result;
}
virtual int16_t beginFSK(float,float,float,float,int8_t,uint16_t,float,bool ldo) {
int result=nativeReset();
if(!result) { ++regulator_writes;result=step(2);if(!result)useRegulatorLDO=ldo; }
return result;
}
int16_t beginGFSK(float,float,float,float,int8_t,uint16_t,float) { return nativeReset(); }
int16_t setRegulatorDCDC() {
++regulator_writes;int result=step(2);if(!result)useRegulatorLDO=false;return result;
}
int16_t setRegulatorLDO() {
++regulator_writes;int result=step(2);if(!result)useRegulatorLDO=true;return result;
}
int16_t applyMeshCoreTcxoDelay() { return step(3); }
int16_t setCRC(int) { return step(4); }
int16_t setRxBoostedGainMode(int) { return step(5); }
int16_t setSyncWord(uint8_t*,size_t) { return step(6); }
int16_t explicitHeader() { return step(7); }
int16_t setDioAsRfSwitch(uint8_t,uint8_t,uint8_t,uint8_t,uint8_t,uint8_t,uint8_t,uint8_t) { return step(8); }
struct Profile { void invalidate() {} };
struct RfSwitch {
template<class Callback> int restore(Callback callback) {
const uint8_t modes[]={0,0,0,0,0,0,0};return callback(0,modes);
}
};
};
using @CHIP@=Native;
class Custom:public @CHIP@ {
@POLICY@
bool _coldStandby=false,_tcxoWakePending=false,_profilePacketDirty=true,_rx_boosted=false;
Profile _profileSwitch;
RfSwitch _morse_rf_switch;
public:
int restoreRfSwitchTable() { return 0; }
@INIT@
@MORSE@
int init() { return @INIT_CALL@; }
int morse() { return @MORSE_CALL@; }
};
@WRAPPER@
int main() {
using mesh::power::RegulatorMode;
constexpr bool supported_dcdc=@DCDC@;
for (auto requested:{RegulatorMode::Default,RegulatorMode::Ldo,RegulatorMode::Dcdc}) {
Custom radio;Wrapper wrapper{&radio};
assert(wrapper.activeRegulatorMode()==RegulatorMode::Default && radio.spi==0);
assert(!wrapper.configureBootRegulator(static_cast<RegulatorMode>(3)));
assert(wrapper.regulatorModes()==radio.regulatorModes());
const bool supported=requested!=RegulatorMode::Dcdc || supported_dcdc;
assert(wrapper.configureBootRegulator(requested)==supported);
assert(radio.spi==0); // boot configuration cannot send commands mid-RX/TX
if(!supported) continue;
const auto expected=requested==RegulatorMode::Default ? wrapper.defaultRegulatorMode() : requested;
assert(radio.init()==0 && wrapper.activeRegulatorMode()==expected);
assert(radio.useRegulatorLDO==(expected==RegulatorMode::Ldo));
assert(!wrapper.configureBootRegulator(RegulatorMode::Default));
// Both transitions reset the native modem; the saved choice survives each.
assert(radio.morse()==0 && wrapper.activeRegulatorMode()==expected);
assert(radio.useRegulatorLDO==(expected==RegulatorMode::Ldo));
assert(radio.init()==0 && wrapper.activeRegulatorMode()==expected);
assert(radio.useRegulatorLDO==(expected==RegulatorMode::Ldo));
for(int fault:{1,2}) {
if(fault==2 && @SKIP_LDO_WRITE@) continue;
radio.fail=fault;
assert(radio.morse()!=0);
assert(wrapper.activeRegulatorMode()==RegulatorMode::Default);
assert(!wrapper.configureBootRegulator(RegulatorMode::Ldo));
radio.fail=0;
assert(radio.init()==0 && wrapper.activeRegulatorMode()==expected);
assert(radio.useRegulatorLDO==(expected==RegulatorMode::Ldo));
radio.fail=fault;
assert(radio.init()!=0 && wrapper.activeRegulatorMode()==RegulatorMode::Default);
radio.fail=0;
assert(radio.init()==0 && wrapper.activeRegulatorMode()==expected);
}
}
// A failed first init must lock the boot policy as firmly as a successful one.
Custom failed;
assert(failed.configureBootRegulator(RegulatorMode::Ldo));failed.fail=1;
assert(failed.init()!=0 && !failed.configureBootRegulator(RegulatorMode::Default));
}
'''
class RadioRegulatorTests(unittest.TestCase):
def exercise(self, chip, flags=(), dcdc=True):
source = (ROOT / f'src/helpers/radiolib/Custom{chip}.h').read_text()
if chip == 'LR2021':
initialize = method(source, 'bool std_init(')
init_call = 'std_init() ? 0 : -1'
else:
initialize = method(source, 'int16_t begin(')
init_call = 'begin()'
if chip == 'SX1262':
morse = method(source, 'int16_t beginFSK(')
morse_call = 'beginFSK()'
else:
morse = method(source, 'int16_t beginMorseReceive(')
morse_call = 'beginMorseReceive(902.1f)'
wrapper_source = (ROOT / f'src/helpers/radiolib/Custom{chip}Wrapper.h').read_text()
delegates = '\n'.join(method(wrapper_source, signature).replace(' override', '')
for signature in ('uint8_t regulatorModes(',
'mesh::power::RegulatorMode defaultRegulatorMode(',
'bool configureBootRegulator(', 'mesh::power::RegulatorMode activeRegulatorMode('))
wrapper = f'using Custom{chip}=Custom;\nstruct Wrapper {{ Custom* _radio;\n{delegates}\n}};'
code = HARNESS.replace('@WRAPPER@', wrapper)
for marker, value in {'@CHIP@': chip, '@POLICY@': regulator_members(source),
'@INIT@': initialize, '@MORSE@': morse, '@INIT_CALL@': init_call,
'@MORSE_CALL@': morse_call, '@DCDC@': str(dcdc).lower(),
'@SKIP_LDO_WRITE@': str(chip == 'LR2021' and not dcdc).lower()}.items():
code = code.replace(marker, value)
compile_run(self, code, flags=('-Wno-unused-parameter', *flags))
def test_sx1262_boot_selection_morse_failures_and_recovery(self):
self.exercise('SX1262')
def test_sx1262_forced_ldo_recipe_cannot_enable_missing_dcdc_network(self):
self.exercise('SX1262', ('-DSX126X_USE_REGULATOR_LDO=1',), dcdc=False)
def test_lr1110_boot_selection_morse_failures_and_recovery(self):
self.exercise('LR1110')
def test_lr1121_boot_selection_morse_failures_and_recovery(self):
self.exercise('LR1121')
def test_lr2021_verified_layout_boot_selection_morse_failures_and_recovery(self):
self.exercise('LR2021', ('-DLR2021_USE_DCDC=1',))
def test_lr2021_unverified_layout_cannot_enable_simo(self):
self.exercise('LR2021', ('-DLR2021_USE_DCDC=0',), dcdc=False)
if __name__ == '__main__':
unittest.main()
+5
View File
@@ -42,6 +42,7 @@ class RakSdReceiverTest(unittest.TestCase):
"void ota_reboot_to_apply() { // public:",
))
run_cpp(r'''
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
#include <cassert>
#include <cstring>
#include "src/helpers/ota/OtaBlInfo.h"
@@ -127,6 +128,7 @@ int main() {
end = cli.index("#elif defined(OTA_QSPI_STORE)", error)
gate = cli[cli.index("\n", start) + 1:end]
run_cpp(r'''
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
#include <cassert>
#include <cstdio>
#include <cstring>
@@ -169,6 +171,7 @@ int main() {
self.assertNotIn("ota_acquire_context", body)
self.assertNotIn("ota_ctx()", body)
run_cpp(r'''
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
#include <cassert>
#include <cstdarg>
#include <cstdio>
@@ -216,6 +219,7 @@ int main() {
# Full Companion uses the separate stable SD archive, not an adaptive
# receiver. Compile the same production status block in both roles.
run_cpp(r'''
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
#include <cassert>
#include <cstdarg>
#include <cstdio>
@@ -304,6 +308,7 @@ int main() {
def test_exact_supplement_preserves_legacy_application_and_update_views(self):
run_cpp(r'''
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
#include <cassert>
#include <cstring>
#include <initializer_list>
+269
View File
@@ -0,0 +1,269 @@
"""Exercise the production retained-RAM regulator recovery policy on the host."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
from test_serial_wifi_sessions import SANITIZER_FLAGS
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <helpers/RegulatorRecovery.h>
#include <cassert>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <type_traits>
using namespace mesh::power;
static_assert(std::is_trivial<RecoveryRecord>::value, "retention must be POD");
static_assert(std::is_standard_layout<RecoveryRecord>::value, "retention layout");
void initial(RecoveryRecord& r, uint8_t domains = RECOVERY_DOMAINS,
uint8_t percent = 50, bool valid = true, uint32_t now = 0) {
RecoveryPolicy p(r);
p.begin(ResetKind::Cold, 0);
p.arm(domains, now, percent, valid);
}
void counted(RecoveryRecord& r, unsigned crashes, uint8_t domains = RECOVERY_DOMAINS) {
initial(r, domains);
for (unsigned i = 0; i < crashes; ++i) {
RecoveryPolicy boot(r);
boot.begin(ResetKind::Crash, 0);
boot.arm(domains, 0, 50, true);
}
}
void assertCounts(const RecoveryPolicy& p, unsigned mcu, unsigned radio, unsigned fallback = 0) {
assert(p.crashCount(RECOVERY_MCU) == mcu);
assert(p.crashCount(RECOVERY_RADIO) == radio);
assert(p.fallbackMask() == fallback);
}
int main(int argc, char** argv) {
assert(argc == 2);
const int scenario = std::atoi(argv[1]);
RecoveryRecord r{};
if (scenario == 0) {
initial(r);
for (unsigned n = 1; n <= 3; ++n) {
RecoveryPolicy boot(r); boot.begin(ResetKind::Crash, 0);
assertCounts(boot, n, n, n == 3 ? RECOVERY_DOMAINS : 0);
boot.arm(RECOVERY_DOMAINS, 10, 50, true);
}
// A learned LDO fallback never flips back to DC-DC after more resets.
for (unsigned n = 0; n < 4; ++n) {
RecoveryPolicy boot(r); boot.begin(ResetKind::Crash, 0);
assertCounts(boot, 0, 0, RECOVERY_DOMAINS);
boot.arm(RECOVERY_DOMAINS, 10, 100, true);
}
} else if (scenario == 1) {
// Battery threshold is inclusive at 50%; a low sample breaks a streak.
counted(r, 2);
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); p.clearDomain(RECOVERY_DOMAINS);
p.arm(RECOVERY_DOMAINS, 0, 49, true); }
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0);
p.arm(RECOVERY_DOMAINS, 0, 50, true); }
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 1, 1); }
for (unsigned percent : {0u, 49u, 101u, 255u}) {
initial(r, RECOVERY_DOMAINS, percent, true);
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0);
}
} else if (scenario == 2) {
// USB voltage, a missing battery, or an unknown percentage is not evidence.
for (unsigned percent : {0u, 50u, 100u, 255u}) {
initial(r, RECOVERY_DOMAINS, percent, false);
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0);
}
initial(r);
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 1, 1);
p.arm(RECOVERY_DOMAINS, 0, 90, true); p.updateBattery(100, 90, false);
p.heartbeat(200); }
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0); }
} else if (scenario == 3) {
for (uint32_t age : {59999u, 60000u, 60001u, 299999u}) {
initial(r, RECOVERY_DOMAINS, 50, true, 1000);
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); p.arm(RECOVERY_DOMAINS, 1000, 50, true);
p.heartbeat(1000 + age); }
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0);
assertCounts(p, age <= 60000 ? 2 : 0, age <= 60000 ? 2 : 0);
}
// A fresh new sample, rather than an old value re-used by a heartbeat,
// restores eligibility even when battery measurements are infrequent.
initial(r);
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); p.arm(RECOVERY_DOMAINS, 0, 50, true);
p.heartbeat(70000); p.updateBattery(70000, 51, true); p.heartbeat(70001); }
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 2, 2); }
} else if (scenario == 4) {
for (ResetKind reset : {ResetKind::Cold, ResetKind::Intentional, ResetKind::Wake,
ResetKind::Unknown}) {
counted(r, 2);
RecoveryPolicy p(r); p.begin(reset, 0); assertCounts(p, 0, 0);
p.arm(RECOVERY_DOMAINS, 0, 50, true);
RecoveryPolicy next(r); next.begin(ResetKind::Crash, 0); assertCounts(next, 1, 1);
}
} else if (scenario == 5) {
counted(r, 2);
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); p.clearDomain(RECOVERY_DOMAINS);
p.arm(RECOVERY_DOMAINS, 0, 90, true); p.markCleanReset(); }
// A clean marker beats even a hardware reset cause labelled as a crash.
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0);
} else if (scenario == 6) {
counted(r, 2);
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 3, 3, RECOVERY_DOMAINS); }
// Sticky fallback survives power loss, sleep, software reset, unknown cause.
for (ResetKind reset : {ResetKind::Cold, ResetKind::Intentional, ResetKind::Wake,
ResetKind::Unknown, ResetKind::Crash}) {
RecoveryPolicy p(r); p.begin(reset, 0); assertCounts(p, 0, 0, RECOVERY_DOMAINS);
}
// With retention lost, the flash-persisted fallback still applies.
std::memset(&r, 0xff, sizeof(r));
RecoveryPolicy p(r); p.begin(ResetKind::Cold, RECOVERY_RADIO | 0x80);
assertCounts(p, 0, 0, RECOVERY_RADIO);
} else if (scenario == 7) {
// Every single corrupted bit of a valid 32-byte record is fail-closed.
initial(r);
const RecoveryRecord valid = r;
for (size_t byte = 0; byte < sizeof(r); ++byte) {
for (unsigned bit = 0; bit < 8; ++bit) {
r = valid;
reinterpret_cast<unsigned char*>(&r)[byte] ^= 1u << bit;
RecoveryPolicy p(r); p.begin(ResetKind::Crash, RECOVERY_RADIO);
assertCounts(p, 0, 0, RECOVERY_RADIO);
}
}
// Uninitialised memory and an interrupted commit cannot create a streak.
for (unsigned fill : {0u, 0x55u, 0xaau, 0xffu}) {
std::memset(&r, fill, sizeof(r));
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0);
}
r = valid; r.magic = 0;
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0);
} else if (scenario == 8) {
counted(r, 1);
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 2, 2);
p.arm(RECOVERY_DOMAINS, 123, 90, true);
p.tick(123 + 299999, 90, true); assertCounts(p, 2, 2);
p.tick(123 + 300000, 90, true); assertCounts(p, 0, 0);
p.tick(123 + 301000, 90, true); assertCounts(p, 0, 0); }
// Healthy operation breaks the streak; a later crash starts a new one.
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 1, 1);
} else if (scenario == 9) {
// Both the stability window and sample age work across millis() rollover.
const uint32_t start = 0xffff0000u;
initial(r, RECOVERY_DOMAINS, 90, true, start);
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); p.arm(RECOVERY_DOMAINS, start, 90, true);
p.tick(start + 299999u, 90, true); assertCounts(p, 1, 1);
p.tick(start + 300000u, 90, true); assertCounts(p, 0, 0); }
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 1, 1); }
for (uint32_t age : {60000u, 60001u}) {
initial(r, RECOVERY_DOMAINS, 90, true, 0xfffffff0u);
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0);
p.arm(RECOVERY_DOMAINS, 0xfffffff0u, 90, true); p.heartbeat(0xfffffff0u + age); }
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0);
assertCounts(p, age <= 60000 ? 2 : 0, age <= 60000 ? 2 : 0);
}
} else if (scenario == 10) {
// A radio DC-DC streak does not accuse a processor already using LDO.
counted(r, 2, RECOVERY_RADIO);
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 3, RECOVERY_RADIO);
p.arm(RECOVERY_MCU, 0, 90, true); }
for (unsigned n = 1; n <= 3; ++n) {
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0);
assertCounts(p, n, 0, RECOVERY_RADIO | (n == 3 ? RECOVERY_MCU : 0));
if (n < 3) p.arm(RECOVERY_MCU, 0, 90, true);
}
} else if (scenario == 11) {
// Disabling one domain breaks only its consecutive-crash streak.
counted(r, 1);
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 2, 2);
p.arm(RECOVERY_RADIO, 0, 90, true); }
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 3, RECOVERY_RADIO);
assert(p.crashCount(RECOVERY_DOMAINS) == 0 && p.crashCount(0x80) == 0);
} else if (scenario == 12) {
counted(r, 2);
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 3, 3, RECOVERY_DOMAINS);
p.clearDomain(RECOVERY_RADIO | 0x80); assertCounts(p, 3, 0, RECOVERY_MCU);
p.arm(RECOVERY_RADIO, 0, 90, true);
RecoveryPolicy next(r); next.begin(ResetKind::Crash, RECOVERY_MCU);
assertCounts(next, 0, 1, RECOVERY_MCU);
next.clearDomain(RECOVERY_MCU); assertCounts(next, 0, 1);
next.clearDomain(0); assertCounts(next, 0, 1);
} else if (scenario == 13) {
initial(r);
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 1, 1); }
// Crash before the next arm() cannot blame a regulator not yet enabled.
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0);
p.arm(0, 0, 90, true); }
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0); }
} else if (scenario == 14) {
// Independently persisted and retained fallbacks are merged, never toggled.
counted(r, 2, RECOVERY_MCU);
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, RECOVERY_RADIO);
assertCounts(p, 3, 0, RECOVERY_DOMAINS); }
RecoveryPolicy p(r); p.begin(ResetKind::Cold, RECOVERY_MCU);
assertCounts(p, 0, 0, RECOVERY_DOMAINS);
} else if (scenario == 15) {
initial(r);
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); p.arm(RECOVERY_DOMAINS, 100, 90, true);
p.updateBattery(101, 90, true); }
// A sample apparently newer than the last heartbeat is conservative until
// that heartbeat catches up; subtraction cannot turn it into a fresh age.
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0); }
} else assert(false);
std::cout << "PASS: regulator recovery " << scenario << '\n';
}
'''
class RegulatorRecoveryTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if compiler is None:
raise unittest.SkipTest("a host C++11 compiler is required")
cls.folder = tempfile.TemporaryDirectory(prefix="regulator-recovery-")
cls.addClassCleanup(cls.folder.cleanup)
work = Path(cls.folder.name)
source = work / "test.cpp"
source.write_text(HARNESS, encoding="utf-8")
cls.binary = work / "test.exe"
built = subprocess.run(
[compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror", *SANITIZER_FLAGS,
"-I" + str(ROOT / "src"), str(source), "-o", str(cls.binary)],
capture_output=True, text=True, timeout=60,
)
if built.returncode:
raise AssertionError(built.stdout + built.stderr)
def test_retained_state_machine(self):
scenarios = (
"third crash latches fallback and never reverses it",
"50 percent threshold and invalid percentage",
"unknown battery and USB-only readings do not count",
"fresh sample boundary and heartbeat timestamp separation",
"cold intentional wake and unknown causes break streak",
"clean reset marker defeats misleading reset reason",
"sticky fallback survives normal resets and retention loss",
"every record bit corruption fails closed",
"five minute stability clears the streak",
"stability and freshness survive timer rollover",
"radio and MCU learn fallback independently",
"inactive regulator does not accrue crashes",
"manual domain change clears only its fallback and count",
"a boot is disarmed until the actual modes are selected",
"saved and retained fallback masks merge",
"future sample is ineligible until heartbeat catches up",
)
for number, scenario in enumerate(scenarios):
with self.subTest(scenario=scenario):
checked = subprocess.run([str(self.binary), str(number)],
capture_output=True, text=True, timeout=10)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
self.assertIn(f"PASS: regulator recovery {number}", checked.stdout)
if __name__ == "__main__":
unittest.main()
+237
View File
@@ -0,0 +1,237 @@
"""Execute the real regulator settings and retained-platform implementations."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
from test_radio_receive_contract import method
from test_serial_wifi_sessions import SANITIZER_FLAGS
ROOT = Path(__file__).resolve().parents[1]
FILESYSTEM = r'''
#pragma once
#include <map>
#include <string>
#include <vector>
#include <cstdint>
#include <cstring>
#include <algorithm>
struct MockFS;
struct File {
MockFS* fs=nullptr;std::string name;size_t cursor=0;bool valid=false;
File()=default;explicit File(MockFS& fs):fs(&fs) {}
File(MockFS* fs,const std::string& name,bool valid):fs(fs),name(name),valid(valid) {}
bool open(const char*,int);
explicit operator bool() const { return valid; }
size_t size() const;
size_t read(uint8_t*,size_t);
size_t write(const uint8_t*,size_t);
void close() {}
};
struct MockFS {
std::map<std::string,std::vector<uint8_t>> files;
bool fail_open=false,fail_write=false,fail_restore=false;
std::string fail_rename_from;
unsigned writes=0,renames=0;
bool exists(const char* name) const { return files.count(name); }
bool remove(const char* name) { return files.erase(name)!=0; }
bool rename(const char* from,const char* to) {
++renames;
if(fail_rename_from==from || (fail_restore && std::string(from)=="/regulator_v1.bak"))return false;
if(!files.count(from)||files.count(to))return false;
files[to]=files[from];files.erase(from);return true;
}
File open(const char* name,const char* mode,bool=false) {
if(fail_open)return File();
if(*mode=='w'){ files[name].clear();return File(this,name,true); }
return File(this,name,exists(name));
}
};
inline bool File::open(const char* key,int flags) {
File f=fs->open(key,flags?"w":"r");*this=f;return valid;
}
inline size_t File::size() const { return valid ? fs->files[name].size() : 0; }
inline size_t File::read(uint8_t* bytes,size_t length) {
if(!valid)return 0;
const auto& data=fs->files[name];length=std::min(length,data.size()-cursor);
memcpy(bytes,data.data()+cursor,length);cursor+=length;return length;
}
inline size_t File::write(const uint8_t* bytes,size_t length) {
if(!valid)return 0;
++fs->writes;if(fs->fail_write)length/=2;
auto& data=fs->files[name];data.insert(data.end(),bytes,bytes+length);return length;
}
#define FILESYSTEM MockFS
#define FILE_O_READ 0
#define FILE_O_WRITE 1
'''
DISPATCHER = r'''
#pragma once
#include <cstdint>
#include <vector>
#include <helpers/RegulatorMode.h>
namespace mesh {
struct MainBoard {
uint8_t modes=3;power::RegulatorMode default_mode=power::RegulatorMode::Dcdc;
power::RegulatorMode active=power::RegulatorMode::Default;
unsigned applies=0;bool dcdc_applied=false;uint16_t mv=3600;
std::vector<uint16_t> samples;unsigned sample_index=0;
uint8_t mcuRegulatorModes() const { return modes; }
power::RegulatorMode defaultMcuRegulatorMode() const { return default_mode; }
power::RegulatorMode activeMcuRegulatorMode() const { return active; }
bool applyMcuRegulator(power::RegulatorMode mode) {
++applies;if(!power::regulatorModeSupported(modes,mode))return false;
active=mode==power::RegulatorMode::Default ? default_mode : mode;
dcdc_applied|=active==power::RegulatorMode::Dcdc;return true;
}
uint16_t getBattMilliVolts() {
return samples.empty()?mv:samples[sample_index++%samples.size()];
}
void resetHardware() { active=power::RegulatorMode::Default;sample_index=0; }
};
struct Radio {
uint8_t modes=3;power::RegulatorMode default_mode=power::RegulatorMode::Dcdc;
power::RegulatorMode active=power::RegulatorMode::Default,boot=power::RegulatorMode::Default;
unsigned configures=0,inits=0;bool locked=false,dcdc_applied=false;
uint8_t regulatorModes() const { return modes; }
power::RegulatorMode defaultRegulatorMode() const { return default_mode; }
power::RegulatorMode activeRegulatorMode() const { return active; }
bool configureBootRegulator(power::RegulatorMode mode) {
++configures;if(locked||!power::regulatorModeSupported(modes,mode))return false;
boot=mode;return true;
}
void initialize() {
++inits;locked=true;active=boot==power::RegulatorMode::Default?default_mode:boot;
dcdc_applied|=active==power::RegulatorMode::Dcdc;
}
void resetHardware() { active=boot=power::RegulatorMode::Default;locked=false; }
};
}
'''
ESP_SYSTEM = r'''
#pragma once
extern int mock_reset_reason;
enum { ESP_RST_UNKNOWN=0,ESP_RST_POWERON,ESP_RST_EXT,ESP_RST_SW,ESP_RST_PANIC,
ESP_RST_INT_WDT,ESP_RST_TASK_WDT,ESP_RST_WDT,ESP_RST_DEEPSLEEP,ESP_RST_BROWNOUT };
inline int esp_reset_reason() { return mock_reset_reason; }
'''
class RegulatorSettingsTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = os.environ.get('CXX') or shutil.which('g++') or shutil.which('clang++')
if not compiler:
raise unittest.SkipTest('native C++ compiler unavailable')
cls.temp = tempfile.TemporaryDirectory(prefix='meshcore-regulator-settings-')
cls.addClassCleanup(cls.temp.cleanup)
folder = Path(cls.temp.name)
headers = {
'helpers/IdentityStore.h': FILESYSTEM,
'Dispatcher.h': DISPATCHER,
'Arduino.h': '#pragma once\n#include <cstdint>\nextern uint32_t mock_now;\ninline uint32_t millis(){return mock_now;}\ninline void delay(unsigned ms){mock_now+=ms;}\n',
'esp_system.h': ESP_SYSTEM,
'esp_attr.h': '#pragma once\n#define RTC_NOINIT_ATTR\n',
'soc/soc_caps.h': '#pragma once\n#ifndef SOC_RTC_SLOW_MEM_SUPPORTED\n#define SOC_RTC_SLOW_MEM_SUPPORTED 1\n#endif\n#define SOC_RTC_FAST_MEM_SUPPORTED 0\n',
'hardware/watchdog.h': '#pragma once\nextern bool mock_watchdog_enabled,mock_watchdog_any;\ninline bool watchdog_enable_caused_reboot(){return mock_watchdog_enabled;}\ninline bool watchdog_caused_reboot(){return mock_watchdog_any;}\n',
}
for name, content in headers.items():
path = folder / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
cls.binaries = {}
for platform, flags in {'esp32': ['-DESP32=1'],
'rp2040': ['-DRP2040_PLATFORM=1'],
'nrf52': ['-DNRF52_PLATFORM=1','-DNRF52_POWER_MANAGEMENT=1'],
'no_retention': ['-DESP32=1','-DSOC_RTC_SLOW_MEM_SUPPORTED=0']}.items():
binary = folder / platform
result = subprocess.run([compiler, '-std=c++17','-Wall','-Wextra','-Werror',
*SANITIZER_FLAGS,*flags,'-I',str(folder),'-I',str(ROOT/'src'),
str(ROOT/'test/fixtures/regulator_settings_native.cpp'),'-o',str(binary)],
text=True,capture_output=True,timeout=45)
if result.returncode:
raise AssertionError(result.stdout+result.stderr)
cls.binaries[platform] = binary
def scenario(self, scenario):
for platform, binary in self.binaries.items():
if platform == 'no_retention':
continue
with self.subTest(platform=platform, scenario=scenario):
result = subprocess.run([str(binary),scenario],text=True,capture_output=True,timeout=10)
self.assertEqual(result.returncode,0,result.stdout+result.stderr)
def test_selections_save_both_domains_without_altering_active_hardware(self):
self.scenario('select')
def test_three_crashes_at_fifty_percent_latch_both_domains_across_cold_boot(self):
self.scenario('fallback')
def test_domain_choices_and_fallbacks_are_independent(self):
self.scenario('domains')
def test_below_fifty_missing_stale_and_outlier_battery_readings_do_not_count(self):
self.scenario('battery')
def test_intentional_reset_breaks_streak_and_preserves_unsaved_fallback(self):
self.scenario('clean')
def test_short_writes_open_and_rename_failures_recover_last_committed_settings(self):
self.scenario('atomic')
def test_crc_length_validation_and_backup_recovery(self):
self.scenario('corruption')
def test_unsupported_dcdc_is_rejected_without_touching_either_regulator(self):
self.scenario('unsupported')
def test_command_prefix_boundaries_syntax_and_bounded_reply(self):
self.scenario('bounds')
def test_actual_esp_and_pico_reset_classification(self):
self.scenario('platform')
def test_five_healthy_minutes_break_old_crash_streak(self):
self.scenario('healthy')
def test_failed_fallback_save_retries_and_then_survives_power_loss(self):
self.scenario('retry')
def test_missing_primary_store_rejects_persistence_without_live_mode_changes(self):
self.scenario('no_store')
def test_failed_radio_initialization_is_not_counted_as_running_dcdc(self):
self.scenario('failed_radio')
def test_missing_rtc_retention_disables_automatic_recovery(self):
result = subprocess.run([str(self.binaries['no_retention']),'platform_unsupported'],
text=True,capture_output=True,timeout=10)
self.assertEqual(result.returncode,0,result.stdout+result.stderr)
def test_all_four_roles_initialize_once_before_first_radio_and_service_loop(self):
for role in ('companion_radio','simple_repeater','simple_room_server','simple_sensor'):
with self.subTest(role=role):
source = (ROOT/f'examples/{role}/main.cpp').read_text()
setup = method(source,'\nvoid setup(')
loop = method(source,'\nvoid loop(')
hook = 'mesh::power::beginRegulatorSettings(board, radio_driver, '
self.assertEqual(setup.count(hook),1)
self.assertLess(setup.index('board.begin()'),setup.index(hook))
self.assertLess(setup.index(hook),setup.index('radio_init()'))
if role == 'simple_repeater':
self.assertEqual(loop.count('serviceCommandInterfaces()'),1)
loop = method(source,'serviceCommandInterfaces() {')
self.assertEqual(loop.count('mesh::power::serviceRegulatorSettings()'),1)
self.assertLess(loop.index('mesh::power::serviceRegulatorSettings()'),loop.index('board.loop()'))
cli = (ROOT/'src/helpers/CommonCLI.cpp').read_text()
self.assertIn('handleRegulatorCommand(command, reply, 157)',cli)
other = (ROOT/'examples/kiss_modem/main.cpp').read_text()
self.assertNotIn('beginRegulatorSettings(',other)
if __name__ == '__main__':
unittest.main()
+9 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Execute T096 startup and the real DC/DC initializer with counted SDK I/O."""
"""T096 board startup must defer DC/DC until saved regulator policy is loaded."""
from pathlib import Path
import os
import subprocess
@@ -12,7 +12,7 @@ ROOT = Path(__file__).resolve().parents[1]
class T096RegulatorTests(unittest.TestCase):
def test_startup_selects_dcdc_before_voltage_check_with_or_without_softdevice(self):
def test_startup_defers_dcdc_until_after_battery_hardware_is_ready(self):
methods = body((ROOT / 'src/helpers/NRF52Board.cpp').read_text(),
'void NRF52BoardDCDC::begin()')
methods += body((ROOT / 'variants/heltec_t096/T096Board.cpp').read_text(),
@@ -42,14 +42,17 @@ void delay(unsigned) {}
struct Bus { void setPins(int, int) {} void begin() {} } Wire;
struct Peripheral { void begin() {} void init() {} };
struct Config {} power_config;
struct NRF52Board { void begin() { ++base_calls; } };
struct NRF52Board {
bool cpu_dcdc_supported = false;
void begin() { ++base_calls; }
};
struct NRF52BoardDCDC : NRF52Board { void begin(); };
struct T096Board : NRF52BoardDCDC {
Peripheral periph_power, loRaFEMControl;
void begin();
void checkBootVoltage(const Config*) {
++voltage_checks;
assert(softdevice ? sdk_calls == 1 : power.DCDCEN == 1);
assert(sdk_calls == 0 && power.DCDCEN == 0);
}
};
@METHODS@
@@ -59,8 +62,8 @@ int main() {
T096Board board;
board.begin();
assert(base_calls == 1 && voltage_checks == 1);
assert(sdk_calls == softdevice);
assert(power.DCDCEN == (softdevice ? 0U : 1U));
assert(board.cpu_dcdc_supported);
assert(sdk_calls == 0 && power.DCDCEN == 0);
}
}
'''
+1
View File
@@ -222,6 +222,7 @@ int main() {
'''
HANDOFF_SETUP = r'''
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
#include <cassert>
#include <cstdint>
#define OTA_TOWER_AUTO_STORE 1
+189
View File
@@ -0,0 +1,189 @@
"""Exercise regulator JSON, authenticated batch reboot and shipped browser UI."""
import json
from pathlib import Path
import unittest
from test_replay_reset_integration import extract_braced
import test_webconfig_lost_reply as batch_fixture
import test_webconfig_recovery_auth as auth_fixture
import test_webconfig_ui_runtime as browser_fixture
ROOT = Path(__file__).resolve().parents[1]
class WebConfigRegulatorTest(unittest.TestCase):
compile_run = auth_fixture.WebConfigRecoveryAuthTest.compile_run
setup_values = browser_fixture.WebConfigUiRuntimeTest.setup_values
run_page = browser_fixture.WebConfigUiRuntimeTest.run_page
def test_actual_snapshot_and_authenticated_reboot_batch(self):
# Reuse the actual HTTP/config parser fixture, rather than a duplicate
# batch model. The unrelated Lost-reply test main is replaced below.
source = batch_fixture.LostReplyWebConfigTest.fixture(self)
source = source[:source.index("String writeBody(")]
server = (ROOT / "src/helpers/esp32/WebConfigServer.cpp").read_text()
snapshot = extract_braced(server, "static void webConfigRegulatorsSnapshot(")
self.compile_run(source + r'''
#include <helpers/RegulatorMode.h>
namespace mesh { namespace power {
uint8_t modes[2] = {0, 3};
RegulatorMode requested[2] = {RegulatorMode::Default, RegulatorMode::Dcdc};
RegulatorMode active[2] = {RegulatorMode::Default, RegulatorMode::Ldo};
bool fallback[2] = {false, true}, recovery = true;
const char* regulatorModeName(RegulatorMode mode) {
return mode == RegulatorMode::Dcdc ? "dcdc" : mode == RegulatorMode::Ldo ? "ldo" : "auto";
}
uint8_t configuredRegulatorModes(bool mcu) { return modes[mcu ? 0 : 1]; }
RegulatorMode requestedRegulatorMode(bool mcu) { return requested[mcu ? 0 : 1]; }
RegulatorMode activeRegulatorMode(bool mcu) { return active[mcu ? 0 : 1]; }
const char* activeRegulatorModeName(bool mcu) {
auto mode = activeRegulatorMode(mcu);
return mode == RegulatorMode::Default ? "unknown" : regulatorModeName(mode);
}
bool regulatorFallbackActive(bool mcu) { return fallback[mcu ? 0 : 1]; }
bool regulatorRecoveryAvailable() { return recovery; }
}}
''' + snapshot + r'''
int main() {
using namespace mesh::power;
JsonDocument doc;
webConfigRegulatorsSnapshot(doc.to<JsonObject>());
assert(doc["recovery"].as<bool>());
assert(doc["mcu"]["modes"].as<int>() == 0);
assert(strcmp(doc["mcu"]["active"].as<const char*>(), "unknown") == 0);
assert(doc["radio"]["modes"].as<int>() == 3);
assert(strcmp(doc["radio"]["requested"].as<const char*>(), "dcdc") == 0);
assert(strcmp(doc["radio"]["active"].as<const char*>(), "ldo") == 0);
assert(doc["radio"]["fallback"].as<bool>());
modes[0] = 3; modes[1] = 2; recovery = false;
requested[0] = active[0] = RegulatorMode::Dcdc;
webConfigRegulatorsSnapshot(doc.to<JsonObject>());
assert(!doc["recovery"].as<bool>() && doc["mcu"]["modes"].as<int>() == 3);
assert(doc["radio"]["modes"].as<int>() == 2);
assert(strcmp(doc["mcu"]["active"].as<const char*>(), "dcdc") == 0);
assert(mesh::esp32::managementPassword::publish("password"));
for (const char* key : {"mcu.regulator", "radio.regulator"}) {
assert(wcIsAllowedSetKey(key) && wcSetKeyRequiresReboot(key));
WebConfigServer server;
String body = String("{\"reqid\":\"0123456789abcdef\",\"reboot\":false,\"set\":{\"") + key + "\":\"ldo\"}}";
AsyncWebServerRequest unauthenticated(body); unauthenticated.header.cookies.clear();
server.handleConfigPost(&unauthenticated);
assert(unauthenticated.status == 401 && server._batch_count == 0);
AsyncWebServerRequest save(body); server.handleConfigPost(&save);
assert(save.status == 202 && server._batch_count == 1 && server._batch_reboot);
assert(String(server._batch[0].cmd) == String("set ") + key + " ldo");
assert(String(server._batch[0].key) == key);
AsyncWebServerRequest replay(body); server.handleConfigPost(&replay);
assert(replay.status == 202 && server._batch_count == 1 && server._batch_reboot);
}
for (const char* key : {"mcu.regulator.extra", "radio.regulator.extra", "mcu.regulator\nreboot",
"MCU.regulator", "radio.regulator ", "regulator"}) {
assert(!wcIsAllowedSetKey(key) && !wcSetKeyRequiresReboot(key));
}
assert(!wcSetKeyRequiresReboot(nullptr));
}
''')
def test_ldo_only_radio_hides_dc_dc_and_unsupported_processor(self):
status, config = self.setup_values()
status["mode"] = "lan"
config["regulators"] = {
"recovery": False,
"mcu": {"modes": 0},
"radio": {"modes": 2, "requested": "ldo", "active": "ldo", "fallback": False},
}
prelude = r'''<script>
(function(){
var status=%s,config=%s;
window.fetch=function(path){var value=path==="/api/status"?status:config;
return Promise.resolve({ok:true,status:200,json:function(){return Promise.resolve(value)}})};
window.addEventListener("load",function(){setTimeout(function(){
var body=document.body,radio=document.querySelector('[data-k="radio.regulator"]');
var dcdc=radio.querySelector('[value="dcdc"]');
body.setAttribute("data-test-regulator-visible",!document.getElementById("regulator-settings").classList.contains("hide"));
body.setAttribute("data-test-mcu-hidden",document.getElementById("mcu-regulator-setting").classList.contains("hide"));
body.setAttribute("data-test-mcu-disabled",document.querySelector('[data-k="mcu.regulator"]').disabled);
body.setAttribute("data-test-dcdc-unavailable",dcdc.hidden&&dcdc.disabled);
body.setAttribute("data-test-radio-value",radio.value);
body.setAttribute("data-test-radio-status",document.getElementById("radio-regulator-status").textContent);
body.setAttribute("data-test-recovery-unavailable",document.getElementById("regulator-recovery-status").textContent.indexOf("unavailable")>=0);
},400)});
})();</script>''' % (json.dumps(status), json.dumps(config))
dom = self.run_page(prelude)
for check in ("regulator-visible", "mcu-hidden", "mcu-disabled", "dcdc-unavailable", "recovery-unavailable"):
self.assertIn(f'data-test-{check}="true"', dom)
self.assertIn('data-test-radio-value="ldo"', dom)
self.assertIn('data-test-radio-status="Active: LDO. Automatic fallback: off."', dom)
def test_clear_fallback_and_mode_change_use_existing_save_and_reboot_flow(self):
status, config = self.setup_values()
status["mode"] = "lan"
config["regulators"] = {
"recovery": True,
"mcu": {"modes": 3, "requested": "dcdc", "active": "dcdc", "fallback": False},
"radio": {"modes": 3, "requested": "auto", "active": "ldo", "fallback": True},
}
prelude = r'''<script>
(function(){
var status=%s,config=%s,submitted=null,posts=0;
function response(value,code){return {ok:true,status:code||200,json:function(){return Promise.resolve(value)}}}
window.fetch=function(path,options){
if(path==="/api/status")return Promise.resolve(response(status));
if(path==="/api/config"&&options&&options.method==="POST"){
posts++;submitted=JSON.parse(options.body);return Promise.resolve(response({state:"pending"},202));
}
if(path.indexOf("/api/config/result")===0)return Promise.resolve(response({state:"done",reqid:submitted.reqid,all_ok:true,reboot:true,
results:[{key:"mcu.regulator",reply:"OK - MCU regulator ldo saved; reboot required"},{key:"radio.regulator",reply:"OK - radio regulator auto saved; reboot required"}]}));
return Promise.resolve(response(config));
};
window.addEventListener("load",function(){
setTimeout(function(){
var body=document.body,mcu=document.querySelector('[data-k="mcu.regulator"]');
body.setAttribute("data-test-mcu-loaded",mcu.value);
body.setAttribute("data-test-fallback-status",document.getElementById("radio-regulator-status").textContent);
body.setAttribute("data-test-clear-visible",!document.getElementById("radio-regulator-clear").classList.contains("hide"));
document.getElementById("radio-regulator-clear").click();
body.setAttribute("data-test-unchanged-mode-resubmitted",st.dirty["radio.regulator"]);
body.setAttribute("data-test-posts-before-save",posts);
mcu.value="ldo";mcu.dispatchEvent(new Event("input",{bubbles:true}));appSave();
},400);
setTimeout(function(){
var body=document.body;
body.setAttribute("data-test-posts",posts);
body.setAttribute("data-test-post-mcu",submitted&&submitted.set["mcu.regulator"]);
body.setAttribute("data-test-post-radio",submitted&&submitted.set["radio.regulator"]);
body.setAttribute("data-test-post-reboot",submitted&&submitted.reboot);
body.setAttribute("data-test-reboot-visible",document.getElementById("reboot-ov").classList.contains("show"));
body.setAttribute("data-test-regulator-reboot-message",document.getElementById("reboot-msg").textContent.indexOf("power regulator mode")>=0);
},900);
});
})();</script>''' % (json.dumps(status), json.dumps(config))
dom = self.run_page(prelude)
for key, value in (("mcu-loaded", "dcdc"), ("clear-visible", "true"),
("unchanged-mode-resubmitted", "auto"), ("posts-before-save", "0"),
("posts", "1"), ("post-mcu", "ldo"), ("post-radio", "auto"),
("post-reboot", "true"), ("reboot-visible", "true"),
("regulator-reboot-message", "true")):
self.assertIn(f'data-test-{key}="{value}"', dom)
self.assertIn('data-test-fallback-status="Active: LDO. Automatic fallback: on (LDO)."', dom)
def test_older_firmware_without_regulator_metadata_hides_the_card(self):
status, config = self.setup_values()
status["mode"] = "lan"
config.pop("regulators", None)
prelude = r'''<script>
(function(){
var status=%s,config=%s;
window.fetch=function(path){return Promise.resolve({ok:true,status:200,json:function(){return Promise.resolve(path==="/api/status"?status:config)}})};
window.addEventListener("load",function(){setTimeout(function(){
document.body.setAttribute("data-test-regulator-hidden",document.getElementById("regulator-settings").classList.contains("hide"));
document.body.setAttribute("data-test-radio-disabled",document.querySelector('[data-k="radio.regulator"]').disabled);
},400)});
})();</script>''' % (json.dumps(status), json.dumps(config))
dom = self.run_page(prelude)
self.assertIn('data-test-regulator-hidden="true"', dom)
self.assertIn('data-test-radio-disabled="true"', dom)
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -32,7 +32,7 @@ public:
const char *getManufacturerName() const override { return "Meshtiny"; }
void reboot() override { NVIC_SystemReset(); }
void reboot() override { NRF52Board::reboot(); }
void shutdownPeripherals() override {
NRF52Board::shutdownPeripherals();
+53 -2
View File
@@ -429,6 +429,19 @@ body.tab-cli{padding-bottom:0}
<span class="tgl"><input type="checkbox" data-k="radio.fem.rxgain"><u></u></span></div>
<div class="sw" data-cap="8192"><span><b>Device power saving</b><i>Reduce device and GPS idle power</i></span>
<span class="tgl"><input type="checkbox" data-k="powersaving"><u></u></span></div>
<div class="hide" id="regulator-settings">
<h2>Power regulators</h2>
<div class="note">Auto follows the board default. Changes are saved and restart the node. Selecting a mode clears that regulator's automatic fallback.</div>
<div class="f hide" id="mcu-regulator-setting"><label>Processor regulator</label>
<select data-k="mcu.regulator" disabled><option value="auto">Auto (board default)</option><option value="dcdc">DC-DC</option><option value="ldo">LDO</option></select>
<div class="hint" id="mcu-regulator-status"></div>
<button class="btn sec hide" type="button" id="mcu-regulator-clear" onclick="resetRegulatorFallback('mcu')">Clear fallback on restart</button></div>
<div class="f hide" id="radio-regulator-setting"><label>LoRa radio regulator</label>
<select data-k="radio.regulator" disabled><option value="auto">Auto (board default)</option><option value="dcdc">DC-DC</option><option value="ldo">LDO</option></select>
<div class="hint" id="radio-regulator-status"></div>
<button class="btn sec hide" type="button" id="radio-regulator-clear" onclick="resetRegulatorFallback('radio')">Clear fallback on restart</button></div>
<div class="hint" id="regulator-recovery-status"></div>
</div>
<div data-cap="4096" id="rxps-wrap">
<div class="sw"><span><b>RX power saving</b><i>Duty-cycle the LoRa receiver; off keeps continuous receive</i></span>
<span class="tgl"><input type="checkbox" data-rxps="enabled"><u></u></span></div>
@@ -910,6 +923,8 @@ function cfgVal(k){ // map a `set` key to its current value string, from st.cfg
case"radio.fem.rxgain":return r.fem_rxgain?"on":"off";
case"radio.rxps":return rxpsCfgValue(r);
case"powersaving":return r.powersaving?"on":"off";
case"mcu.regulator":return ((c.regulators||{}).mcu||{}).requested||"auto";
case"radio.regulator":return ((c.regulators||{}).radio||{}).requested||"auto";
case"display.mode":return (st.cfg.display||{}).mode||"button";
case"display.rotation":return String((st.cfg.display||{}).rotation||0);
case"display.timeout":return String((st.cfg.display||{}).timeout||15);
@@ -1044,6 +1059,33 @@ function radioCombo(){
var p={};$$("[data-rg]").forEach(function(el){p[el.dataset.rg]=el.value});
return p.freq+","+p.bw+","+p.sf+","+p.cr;
}
function syncRegulators(c){
var regulators=c.regulators||{},supported=false;
["mcu","radio"].forEach(function(domain){
var entry=regulators[domain]||{},modes=Number(entry.modes)||0;
var select=$('[data-k="'+domain+'.regulator"]');
$("#"+domain+"-regulator-setting").classList.toggle("hide",!modes);
select.disabled=!modes;supported=supported||!!modes;
Array.prototype.forEach.call(select.options,function(option){
var available=option.value==="auto"||(option.value==="dcdc"&&(modes&1))||(option.value==="ldo"&&(modes&2));
option.hidden=!available;option.disabled=!available;
});
var active=entry.active==="dcdc"?"DC-DC":entry.active==="ldo"?"LDO":"unknown";
$("#"+domain+"-regulator-status").textContent="Active: "+active+". Automatic fallback: "+(entry.fallback?"on (LDO)":"off")+".";
$("#"+domain+"-regulator-clear").classList.toggle("hide",!modes||!entry.fallback);
});
$("#regulator-settings").classList.toggle("hide",!supported);
$("#regulator-recovery-status").textContent=regulators.recovery
?"Recovery switches a DC-DC regulator to LDO after three consecutive crashes with a fresh battery reading of at least 50%. It keeps LDO until you select a mode again."
:"Automatic crash recovery is unavailable on this platform.";
}
function resetRegulatorFallback(domain){
var key=domain+".regulator",select=$('[data-k="'+key+'"]');
if(!select||select.disabled)return;
// Re-send even an unchanged selection so a learned fallback can be cleared
// through the normal authenticated save/result/reboot flow.
st.dirty[key]=select.value;select.classList.add("dirty");updateSaveBar();
}
function loadConfig(loadCapture){
var epoch=++st.configEpoch;
if(loadCapture)loadCapture.loadEpoch=epoch;
@@ -1053,6 +1095,7 @@ function loadConfig(loadCapture){
if(epoch!==st.configEpoch)return false;
st.cfg=c;st.orig={};st.dirty={};st.skipRadio=false;st.skipMqtt=false;
st.wifiPasswordAutoCleared=false;
syncRegulators(c);
if(st.hasMqtt){buildSlots("#app-slots");buildSlots("#wz-slots")}
$$("[data-k]").forEach(function(el){
var k=el.dataset.k,v=cfgVal(k);
@@ -1327,7 +1370,7 @@ function saveChanges(extra,cb){
// Applying new station credentials necessarily drops this page. Reboot
// after a successful batch so the bridge joins with the complete SSID and
// password together instead of reconnecting between the two writes.
if((extra&&extra.reboot)||("wifi.ssid" in setmap)||("wifi.pwd" in setmap)||("espnow.channel" in setmap)||("bluetooth.mac" in setmap)||("bluetooth.stealth" in setmap))body.reboot=true;
if((extra&&extra.reboot)||("wifi.ssid" in setmap)||("wifi.pwd" in setmap)||("espnow.channel" in setmap)||("bluetooth.mac" in setmap)||("bluetooth.stealth" in setmap)||("mcu.regulator" in setmap)||("radio.regulator" in setmap))body.reboot=true;
if(Object.keys(setmap).length===0&&!body.reboot){toast("Nothing to save");return}
var reqid=mkReqId();body.reqid=reqid;
var auth={password:setmap.password||"",attempted:false,view:$("#v-wizard").classList.contains("hide")?"#v-app":"#v-wizard"};
@@ -1365,7 +1408,7 @@ function saveChanges(extra,cb){
});
}
function appSave(){
var rebootChanged=("wifi.ssid" in st.dirty)||("wifi.pwd" in st.dirty)||("espnow.channel" in st.dirty)||("bluetooth.mac" in st.dirty)||("bluetooth.stealth" in st.dirty);
var rebootChanged=("wifi.ssid" in st.dirty)||("wifi.pwd" in st.dirty)||("espnow.channel" in st.dirty)||("bluetooth.mac" in st.dirty)||("bluetooth.stealth" in st.dirty)||("mcu.regulator" in st.dirty)||("radio.regulator" in st.dirty);
var pwdErr=adminPwdError(rebootChanged);
if(pwdErr){
$('#tabs button[data-t="radio"]').click();
@@ -1423,12 +1466,15 @@ function pollResult(cb,errs,idles,acked,reqid,submitted,auth){
st.dirty={};
var bluetoothAddressChanged=submitted&&("bluetooth.mac" in submitted);
var bluetoothStealthChanged=submitted&&("bluetooth.stealth" in submitted);
var regulatorChanged=submitted&&(("mcu.regulator" in submitted)||("radio.regulator" in submitted));
showReboot(r.wifi_ip
? "The node joined the selected WiFi. This page and address stay visible while it reboots. Reconnect your phone or laptop to that network, then open the address."
: bluetoothAddressChanged
? "The node is restarting with its new Bluetooth address. Forget its old Bluetooth entry, scan again, and pair with the new identity."
: bluetoothStealthChanged
? "The node is restarting with the saved stealth flag. Its address policy is unchanged. When enabling stealth, pair once; afterward reconnect using the saved device."
: regulatorChanged
? "The node is restarting to apply the saved power regulator mode. This page will try to reconnect automatically."
: "The node is restarting with the saved WiFi settings. Reconnect to that network and open the node at its router-assigned address.",
false,bluetoothAddressChanged?"Bluetooth address saved [OK] - rebooting":bluetoothStealthChanged?"Bluetooth stealth saved [OK] - rebooting":"Settings saved [OK] - rebooting",r.wifi_ip,rebootSeconds(r));
reauthAfterReceipt();
@@ -1451,6 +1497,7 @@ function pollResult(cb,errs,idles,acked,reqid,submitted,auth){
function loadConfigSoft(){ // re-sync origin values without clobbering result chips
api("/api/config").then(function(c){
st.cfg=c;
syncRegulators(c);
$$("[data-k]").forEach(function(el){
var k=el.dataset.k,stored=cfgVal(k);
st.orig[k]=stored;
@@ -2069,6 +2116,8 @@ var CLI_KEYS=[
["webui","WiFi settings website",0,"on|off"],
["usb.logging","USB logging",0,"on|off"],
["powersaving","Device power saving",0,"on|off"],
["mcu.regulator","Processor regulator (supported boards; reboot required)",0,"auto|dcdc|ldo"],
["radio.regulator","LoRa regulator (supported boards; reboot required)",0,"auto|dcdc|ldo"],
["gps","GPS power",0,"on|off"],
["logging.output","USB and MQTT output selection",0,"off|usb|wifi|both"],
["mqtt.enabled","MQTT master switch (keeps broker settings)",0,"on|off"],
@@ -2229,6 +2278,8 @@ var CLI_COMPANION_KEYS=[
["radio.fem.txgain","Front-end module TX gain",0,"on|off"],
["radio.rxps","Configured RX power saving",0,"on|off"],
["powersaving","Device power saving",0,"on|off"],
["mcu.regulator","Processor regulator (supported boards; reboot required)",0,"auto|dcdc|ldo"],
["radio.regulator","LoRa regulator (supported boards; reboot required)",0,"auto|dcdc|ldo"],
["usb.logging","USB packet logging (logging-enabled builds)",0,"on|off"],
["display.rotation","Display rotation (0 restores the board default)",0,"0|90|180|270"],
["bluetooth.name","Bluetooth name",0],