Make uf2reset available across nRF52 CLI roles
This commit is contained in:
1 parent
e36b092aa3
commit
65a53285d9
14 files changed
+154
-34
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
Reference in new issue
Block a user