mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 13:22:22 +00:00
Add saved regulator modes and battery-gated crash recovery
This commit is contained in:
1 parent
515bb57cdf
commit
c4a1d485ff
70 files changed
+5335
-2472
No files matched your search
@@ -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
@@ -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"
|
||||
@@ -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 :
|
||||
{
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 :
|
||||
{
|
||||
|
||||
@@ -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 :
|
||||
{
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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 :
|
||||
{
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -748,6 +748,7 @@ void setup() {
|
||||
#endif
|
||||
|
||||
board.begin();
|
||||
board.applyMcuRegulator(mesh::power::RegulatorMode::Default);
|
||||
|
||||
#ifdef HAS_EXTERNAL_WATCHDOG
|
||||
external_watchdog.begin();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]);
|
||||
'''
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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>(®ulator_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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
#include "RegulatorRecovery.h"
|
||||
|
||||
namespace mesh { namespace power {
|
||||
volatile RecoveryRecord& platformRecoveryRecord();
|
||||
ResetKind platformResetKind();
|
||||
bool platformRecoverySupported();
|
||||
}}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
+2370
-2319
File diff suppressed because it is too large.
Load diff
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) { }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
Vendored
+5
@@ -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
@@ -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");
|
||||
}
|
||||
Vendored
+1
@@ -1,3 +1,4 @@
|
||||
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
|
||||
|
||||
#include <helpers/ota/MotaContainer.h>
|
||||
#include <helpers/ota/OtaByteIO.h>
|
||||
|
||||
Vendored
+1
@@ -1,3 +1,4 @@
|
||||
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
+268
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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("),
|
||||
|
||||
@@ -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(",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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
@@ -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],
|
||||
|
||||
Reference in new issue
Block a user