Make uf2reset available across nRF52 CLI roles

This commit is contained in:
mikecarper committed 2026-09-04 02:01:40 -07:00
1 parent e36b092aa3
commit 65a53285d9
14 files changed
+154 -34

No files matched your search

+3
View File
@@ -27,6 +27,9 @@ jobs:
- name: Run Unit Tests
run: pio test -e native -e native_kiss_modem -vv
- name: Verify nRF52 UF2-reset CLI coverage
run: python3 -B test/test_nrf52_uf2reset_cli.py
- name: Upload Test Results
# Upload test results even if the test step failed.
if: always()
+1
View File
@@ -68,6 +68,7 @@ over the normal binary USB, BLE, or TCP connection:
| [`get/set radio.fem.rxgain`](cli_commands.md#view-or-change-the-lora-fem-receive-path-gain-state-on-supported-boards) | Companion on a board with controllable LoRa FEM LNA |
| [`get/set wifi.powersave`](cli_commands.md#browser-configuration-portal-esp32-repeater-and-room-server) | ESP32 WiFi Companion; active transport constraints still apply |
| [`get/set bluetooth.name`](cli_commands.md#view-or-change-the-independent-bluetooth-name-companion) | Companion firmware |
| [`uf2reset`](cli_commands.md#enter-the-uf2-bootloader-nrf52-only) | Every nRF52 Companion text terminal and local command `0x42`; local only |
Deprecated binary aliases remain receive-only for older clients; new clients
should use command `0x42`. See [Companion radio binary protocol](companion_protocol.md).
+2
View File
@@ -63,6 +63,8 @@ arguments such as node names, passwords, and keys is left unchanged.
**Serial Only:** Yes
**Note:** Reboots directly into the UF2 bootloader on supported nRF52 boards.
This includes the Repeater, Room Server, Sensor, Companion, and Terminal Chat
local serial command surfaces. It is never accepted as a remote mesh command.
---
+35 -5
View File
@@ -5,15 +5,15 @@ when its log identifies the physical device, firmware artifact, artifact hash,
command result, and cold/warm boot outcome. Do not infer success from a tool's
exit code when the tool has a documented false-success mode.
## Current marathon ledger (through 2026-09-01)
## Current marathon ledger (through 2026-09-04)
| Hardware | Stable identity | Current state | Next blocking check |
| --- | --- | --- | --- |
| Seeed XIAO nRF52840 | `B35E71C1C3726CE7` | A stalled Companion transport survived the stop token and exact USB reset, then recovered after a 36-second exact application DFU. Current Full Companion USB, bonded BLE, clock sync, terminal/Binary switching, serial-mOTA folder attach, and reversible dual-CDC logging all pass | Prove current-build cold persistence; separately reproduce the stall before using a power cut to test whether power alone clears it; then transfer an actual image through BLE-controlled LoRa OTA |
| Seeed Tracker T1000-E | `34A9141999729D5D` | A temporary Full Companion and one of two same-visible-version OTAFIX candidates are running; the exact candidate bytes are not yet proven. The bonded warm baseline passed at 3.34 kB/s. Two lab-only pre-START 15 ms/latency-0 DFUs with `0/0` no-preference event-length hints passed at 6.23 and 6.52 kB/s; a 10 ms event-length control timed out before START | Prove installed bootloader bytes by readback, exercise a physical cold 20-to-244-byte readiness transition, restore the protected Repeater identity, cold boot, then force LR1110 reset |
| Seeed XIAO nRF52840 | `B35E71C1C3726CE7` | OTAFIX candidate `0x02040405` passed exact serial install and corrected Legacy DIS model gates; the patched Bluetooth repeater-updater application was restored over BLE and returned as exact `2886:8044` USB | Use the updater for an identity-gated Bluetooth transfer to another repeater, then prove current-build cold persistence |
| Seeed Tracker T1000-E | `34A9141999729D5D` | OTAFIX candidate `0x02040405` and the `uf2reset`-fixed Full Companion are installed. The real text command returned exact `2886:0057` boot USB, the same fixed app was restored, and refreshed bonded BLE services pass as `MeshCore-09848C15` | Restore the protected Repeater identity when Companion qualification is complete; cold boot, then force LR1110 reset |
| RAK3401 | `0B81C9C68D8D01B4`; FICR `8D8D01B4 0B81C9C6` | OTAFIX test version `0x02040403` passes bidirectional application UF2, exact SWD application/bootloader readback, unchanged UICR, and a 74-block direct V4 LoRa apply from exact `3caf9dcf` to current HEAD `9fd580c8`. Post-apply USB and LoRa identity/hash checks pass and the temporary 0 dBm bench setting is restored to 22 dBm | Add exact RF packet counters, then repeat the supported-bandwidth and controlled/passive/mixed routed-hop matrix |
| Heltec T096 | `651F8E496197F882` | OTAFIX HIL candidate `0x02040401`, serial DFU, signed LoRa bootloader OTA, warm-watchdog recovery, physical power-removal cold persistence, bonded Legacy BLE application DFU, serial mOTA ownership, button/display wake, and Full Companion dual-CDC reconnect checks pass on `keymindCascade` `fcd1f8cc`. The final T096-only no-footer Full Companion image is installed | Perform a privileged or physical host-driven USB bus reset without resetting the MCU; complete the deferred multi-click/long-press physical matrix |
| Heltec MeshTower V2 with SD | `9352162A72082314` | Exact SD LoRa-OTA Repeater `e26d48e4` is running after identity-gated BLE recovery. Card format/cooldown/forced-format/raw-erase/remount pass; live bootloader contract reports ABI 3, FULL+INPLACE, and SD apply | Signed interrupted-download resume, corruption/signature rejection, then full/delta apply |
| Heltec T096 | `651F8E496197F882` | OTAFIX candidate `0x02040405` and the `uf2reset`-fixed no-footer Full Companion are installed. The real text command returned exact boot USB, and final USB terminal/Binary mode, dual CDC, version, help, and BLE identity pass | Perform a privileged or physical host-driven USB bus reset without resetting the MCU; complete the deferred multi-click/long-press physical matrix |
| Heltec MeshTower V2 with SD | `9352162A72082314` | OTAFIX candidate `0x02040405` passed serial combined install, the historical cached DIS handles, BLE application restore, and exact application USB return. Card format/cooldown/forced-format/raw-erase/remount and the live SD apply contract passed earlier | Signed interrupted-download resume, corruption/signature rejection, then full/delta apply |
| Heltec V4 | USB MAC `44:1B:F6:6A:E8:44`; BLE `44:1B:F6:6A:E8:45` | USB/BLE/Wi-Fi and all three TCP services pass on hardware. The final fresh-NTP-gated Full Companion image builds with 4.31 MB app space free | Flash the exact final NTP build, prove NTP-before-TLS success/failure ordering, then OTA seeding |
| SenseCAP Indicator LoRa | CH340 plus USB MAC `D8:3B:DA:75:23:AC`; BLE `D8:3B:DA:75:23:AD` | The current dark-layout application is SHA-256 `81bebdc07b6a8349c1c975cb5a0e30c5f6019d35bcac641e5f3d4df951f7406b`; identity-gated flash, USB ASCII/Binary switching, runtime logging, and separate configured-Wi-Fi, BLE, and TCP checks pass. Fresh-NTP HTTPS recovery, strict Range resume, STAGEV2 install, and RP USB readback passed on earlier exact artifacts; the final coordinator source still needs its blocked-UDP/123 hardware gate | Physical display-wrap and four-mode render/heap checks, exact-final blocked-NTP recovery gate, LoRa/TempRadio, and non-empty OTA seeding |
@@ -644,6 +644,26 @@ network, and reboot record in that manifest.
## Seeed Tracker T1000-E
- [x] The cross-role `uf2reset` repair was hardware-qualified with Full
Companion ZIP SHA-256
`16e470c0d66bec14e1e9f2d4ebe04d83ffb30efa606465f885cbe9fc3d004a14`.
It was built from `f2c15225` with GCC 14.2.1, the USA Cascadia/Cascade
profile, and embedded version
`v1.17.1-uf2reset-f2c15225-f2c15225`. Serial DFU took 32.196 seconds and
stable application USB returned in 35.888 seconds. Terminal entry,
version, help, the exact no-argument command, application disconnect, and
stable `2886:0057` boot USB all passed in one 8.898-second script. The same
image was restored in 32.204 seconds and stable `239a:8029` USB returned
in 36.313 seconds; a final terminal check returned Binary mode cleanly.
- [x] The fixed application advertises live as `MeshCore-09848C15` at
`D1:A0:86:CD:BB:C6`. The obsolete Pi bond for the earlier temporary
identity was removed, and the exact address was paired again with the
qualification PIN. Paired, bonded, and trusted state, an encrypted BLEDfu
revision read, and the Nordic UART service all passed. The pre-run old
application enumerated but produced neither a terminal response nor a
live advertisement, so this run does not claim continuity of that
temporary identity or of application-owned settings across a combined
SoftDevice/bootloader recovery install.
- [x] Stable application identity `34A9141999729D5D` is recorded and is not
confused with the RAK3401 even though both currently enumerate with USB
product ID `239a:8029`. Application is `239a:8029`; the exercised
@@ -946,6 +966,16 @@ network, and reboot record in that manifest.
## Heltec T096
- [x] The cross-role `uf2reset` repair was hardware-qualified on the exact
no-footer Full Companion image. ZIP SHA-256 is
`0585bee1c0b278a68788f78af384bcbdb5633d57f9ea301fc14f1c80c4275489`;
embedded version is `v1.17.1-uf2reset-f2c15225-f2c15225`. Terminal entry
and help passed, the exact command disconnected application USB in
1.031 seconds, and stable boot USB returned in 2.026 seconds. The final
Full image installed over serial in 38.900 seconds and stable application
USB returned in 41.672 seconds. Final primary terminal/Binary switching,
dedicated logging interface, version, help entry, and live
`MeshCore-8E229D0A` advertisement at `CF:A8:18:36:DE:8B` all passed.
- [x] Stable USB serial `651F8E496197F882` distinguishes the application
(`239A:8029`) from the OTAFIX serial-DFU interface. Every flash followed
that identity across the host-assigned `COM9 -> COM6 -> COM9` transition;
+10
View File
@@ -7214,6 +7214,9 @@ void MyMesh::handleTerminalCommand(char* command) {
#endif
terminalOutput().print(" reboot\r\n");
terminalOutput().print(" ver\r\n");
#if defined(NRF52_PLATFORM)
terminalOutput().println(" uf2reset");
#endif
if (_terminal_mode) {
terminalOutput().print(" +++MESHCORE-TERM-STOP\r\n");
} else {
@@ -7281,6 +7284,13 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
}
#endif
if (sender_timestamp == 0 && mesh::cli::isUf2ResetCommand(command)) {
if (!board.rebootToUf2Bootloader()) {
snprintf(reply, reply_capacity, "ERR: unsupported");
}
return true;
}
if (handleLocalControlCommand(command, reply, reply_capacity)) return true;
if (strncmp(command, "set tx ", 7) == 0) {
+8
View File
@@ -13,6 +13,7 @@
#endif
#include <helpers/ArduinoHelpers.h>
#include <helpers/CLICommandUtils.h>
#include <helpers/IdentityGeneration.h>
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/SimpleMeshTables.h>
@@ -595,6 +596,10 @@ public:
} else {
Serial.printf(" ERROR: unknown config: %s\n", config);
}
} else if (mesh::cli::isUf2ResetCommand(command)) {
if (!board.rebootToUf2Bootloader()) {
Serial.println(" ERROR: UF2 bootloader is unavailable");
}
} else if (memcmp(command, "ver", 3) == 0) {
Serial.println(FIRMWARE_VER_TEXT);
} else if (memcmp(command, "help", 4) == 0) {
@@ -617,6 +622,9 @@ public:
Serial.println(" advert");
Serial.println(" reset path");
Serial.println(" public <text>");
#if defined(NRF52_PLATFORM)
Serial.println(" uf2reset");
#endif
} else {
Serial.print(" ERROR: unknown command: "); Serial.println(command);
}
+4
View File
@@ -137,6 +137,10 @@ public:
virtual void onBeforeTransmit() { }
virtual void onAfterTransmit() { }
virtual void reboot() = 0;
// 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.
virtual bool rebootToUf2Bootloader() { return false; }
virtual void powerOff() { /* no op */ }
// Reload an already-running system watchdog without enabling one. Long,
// internally bounded operations can use this while retaining their own
+4
View File
@@ -517,6 +517,10 @@ inline NoArgCommandMatch matchNoArgCommand(const char* command,
: NoArgCommandMatch::HasArguments;
}
inline bool isUf2ResetCommand(const char* command) {
return matchNoArgCommand(command, "uf2reset") == NoArgCommandMatch::Exact;
}
inline void formatUnknownSetting(char* reply, size_t capacity,
const char* setting) {
if (reply == nullptr || capacity == 0) return;
+5 -29
View File
@@ -18,29 +18,6 @@
#include <new>
#include <stddef.h>
#if defined(NRF52_PLATFORM)
#include <nrf.h>
#include <nrf_soc.h>
#ifndef DFU_MAGIC_UF2_RESET
#define DFU_MAGIC_UF2_RESET 0x57
#endif
static void resetToUf2Bootloader() {
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
sd_power_gpregret_clr(0, 0xFF);
sd_power_gpregret_set(0, DFU_MAGIC_UF2_RESET);
} else {
NRF_POWER->GPREGRET = DFU_MAGIC_UF2_RESET;
}
NVIC_SystemReset();
}
#endif
#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)
#if defined(ENABLE_OTA)
@@ -2486,12 +2463,11 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
_board->powerOff(); // doesn't return
} else if (memcmp(command, "reboot", 6) == 0) {
_board->reboot(); // doesn't return
} else if (sender_timestamp == 0 && memcmp(command, "uf2reset", 8) == 0 && (command[8] == 0 || command[8] == ' ')) {
#if defined(NRF52_PLATFORM)
resetToUf2Bootloader(); // doesn't return
#else
strcpy(reply, "ERR: unsupported");
#endif
} else if (sender_timestamp == 0
&& mesh::cli::isUf2ResetCommand(command)) {
if (!_board->rebootToUf2Bootloader()) {
strcpy(reply, "ERR: unsupported");
}
} else if (memcmp(command, "clkreboot", 9) == 0) {
// Reset clock
getRTCClock()->setCurrentTime(1715770351); // 15 May 2024, 8:50pm
+21
View File
@@ -33,6 +33,10 @@ static bool ota_ble_started = false;
#define NRF52_WATCHDOG_TIMEOUT_SECONDS 60UL
#endif
#ifndef DFU_MAGIC_UF2_RESET
#define DFU_MAGIC_UF2_RESET 0x57
#endif
#if NRF52_WATCHDOG_TIMEOUT_SECONDS > 131071UL
#error "NRF52_WATCHDOG_TIMEOUT_SECONDS exceeds the nRF52 WDT counter range"
#endif
@@ -96,6 +100,23 @@ bool NRF52Board::isUserGpioAvailable(uint8_t pin) const {
#endif
}
bool NRF52Board::rebootToUf2Bootloader() {
uint8_t sd_enabled = 0;
if (sd_softdevice_is_enabled(&sd_enabled) != NRF_SUCCESS) return false;
if (sd_enabled) {
if (sd_power_gpregret_clr(0, 0xFF) != NRF_SUCCESS
|| sd_power_gpregret_set(0, DFU_MAGIC_UF2_RESET) != NRF_SUCCESS) {
return false;
}
} else {
NRF_POWER->GPREGRET = DFU_MAGIC_UF2_RESET;
}
NVIC_SystemReset();
return true;
}
static void connect_callback(uint16_t conn_handle) {
ota_conn_handle = conn_handle;
MESH_DEBUG_PRINTLN("BLE client connected");
+1
View File
@@ -63,6 +63,7 @@ public:
virtual uint8_t getStartupReason() const override { return startup_reason; }
virtual float getMCUTemperature() override;
virtual void reboot() override { NVIC_SystemReset(); }
bool rebootToUf2Bootloader() override;
virtual void shutdownPeripherals();
virtual void powerOff() override;
virtual bool getBootloaderVersion(char* version, size_t max_len) override;
+1
View File
@@ -20,6 +20,7 @@ python3 test/test_companion_transport_selector.py # Indicator transport-selecto
python3 test/test_color_theme.py # shared color-display dark-palette contract
python3 test/test_indicator_font_recovery.py # Indicator TLS/SD font recovery contract
python3 test/test_companion_terminal_profile.py # Companion CLI capability gates
python3 test/test_nrf52_uf2reset_cli.py # all nRF52 text-CLI reset dispatchers
python3 test/test_client_login_profile_contract.py # ACL login ordering/role contract
python3 test/test_client_acl_spiffs.py # Actual ACL: first login, replay/reboot, failed storage
python3 test/test_replay_reset_command.py # Strict full keys and one-use recovery confirmations
@@ -719,6 +719,15 @@ TEST(CLICommandUtils, MatchesDiscoverNeighborsWithoutPrefixCollisions) {
"discover.neighbors.extra", "discover.neighbors"));
}
TEST(CLICommandUtils, MatchesOnlyACompleteUf2ResetCommand) {
EXPECT_TRUE(mesh::cli::isUf2ResetCommand("uf2reset"));
EXPECT_TRUE(mesh::cli::isUf2ResetCommand("uf2reset "));
EXPECT_FALSE(mesh::cli::isUf2ResetCommand("uf2reset now"));
EXPECT_FALSE(mesh::cli::isUf2ResetCommand("uf2reset.extra"));
EXPECT_FALSE(mesh::cli::isUf2ResetCommand("uf2"));
EXPECT_FALSE(mesh::cli::isUf2ResetCommand(nullptr));
}
TEST(CLICommandUtils, ParsesRecentRepeaterListAndPageQueries) {
using mesh::cli::RecentRepeaterGetMatch;
mesh::cli::RecentRepeaterGetQuery query;
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""Pin local UF2 reset support across every nRF52 text CLI family."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
main_board = (ROOT / "src/MeshCore.h").read_text()
nrf_board_h = (ROOT / "src/helpers/NRF52Board.h").read_text()
nrf_board_cpp = (ROOT / "src/helpers/NRF52Board.cpp").read_text()
common = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
chat = (ROOT / "examples/simple_secure_chat/main.cpp").read_text()
# One board capability owns the retained-register transaction. This reaches
# every NRF52Board subclass, including variants which override handleCommand().
assert "virtual bool rebootToUf2Bootloader() { return false; }" in main_board
assert "bool rebootToUf2Bootloader() override;" in nrf_board_h
assert "bool NRF52Board::rebootToUf2Bootloader()" in nrf_board_cpp
assert "sd_softdevice_is_enabled(&sd_enabled) != NRF_SUCCESS" in nrf_board_cpp
clear = nrf_board_cpp.index("sd_power_gpregret_clr(0, 0xFF)")
set_magic = nrf_board_cpp.index(
"sd_power_gpregret_set(0, DFU_MAGIC_UF2_RESET)"
)
reset = nrf_board_cpp.index("NVIC_SystemReset();", set_magic)
assert clear < set_magic < reset
assert "NRF_POWER->GPREGRET = DFU_MAGIC_UF2_RESET" in nrf_board_cpp
# CommonCLI covers repeater, room-server, and sensor roles. Companion and
# Terminal Chat have independent parsers, so each must delegate explicitly.
assert "mesh::cli::isUf2ResetCommand(command)" in common
assert "_board->rebootToUf2Bootloader()" in common
assert "sender_timestamp == 0" in common
assert "sender_timestamp == 0 && mesh::cli::isUf2ResetCommand(command)" in companion
assert "board.rebootToUf2Bootloader()" in companion
assert 'terminalOutput().println(" uf2reset");' in companion
assert "mesh::cli::isUf2ResetCommand(command)" in chat
assert "board.rebootToUf2Bootloader()" in chat
assert 'Serial.println(" uf2reset");' in chat
# Platform register manipulation must not be copied into role-specific CLI
# files; otherwise one role can drift again.
for role_source in (common, companion, chat):
assert "DFU_MAGIC_UF2_RESET" not in role_source
assert "sd_power_gpregret_set" not in role_source
print("test_nrf52_uf2reset_cli: PASS")