Recover constrained firmware flash and update USB dispatch fixtures

This commit is contained in:
mikecarper committed 2026-10-28 02:51:56 -07:00
1 parent 7390e29086
commit 4261889095
15 files changed
+195 -102

No files matched your search

+100 -95
View File
@@ -6426,6 +6426,105 @@ bool MyMesh::handleDirectOtaCommand(const char* command, uint32_t timestamp,
}
#endif
__attribute__((noinline))
void MyMesh::handleSetChannelFrame(const uint8_t* cmd_frame) {
uint8_t channel_idx = cmd_frame[1];
ChannelDetails previous{};
ChannelDetails channel;
StrHelper::strncpy(channel.name, (char *)&cmd_frame[2], 32);
memset(channel.channel.secret, 0, sizeof(channel.channel.secret));
memcpy(channel.channel.secret, &cmd_frame[2 + 32], 16); // NOTE: only 128-bit supported
const auto alias = parseCompanionChannelAlias(channel.name);
if (alias.kind == CompanionChannelKind::Invalid) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
#if MESH_ENABLE_MESHTEXT_CHAT
// Legacy apps fetch only 16 bytes. A label-only edit must not silently
// truncate a provisioned MT AES-256 key to AES-128. An explicit 32-byte SET
// (zero upper half for AES-128) is the unambiguous way to replace that key.
if (alias.kind == CompanionChannelKind::Meshtext
&& getChannelForSave(channel_idx, previous) && isMeshtextChannel(previous)
&& !memcmp(channel.channel.secret, previous.channel.secret, 16)) {
memcpy(channel.channel.secret + 16, previous.channel.secret + 16, 16);
}
if (!validateMeshtextChannelChange(channel_idx, channel)) {
writeErrFrame(ERR_CODE_BAD_STATE); // Conflicting preset, manual RF, region or busy owner.
return;
}
#else
if (alias.kind == CompanionChannelKind::Meshtext && isAliasRadio2Defined()) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // Meshtext transport not implemented yet.
return;
}
#endif
const bool have_previous = getChannelForSave(channel_idx, previous);
ChannelDetails wire_channel = channel;
ChannelDetails previous_wire_channel = previous;
resolveChannelAlias(wire_channel);
if (have_previous) resolveChannelAlias(previous_wire_channel);
if (have_previous && channel.name[0] != 0 && previous.name[0] != 0
&& !memcmp(wire_channel.channel.secret, previous_wire_channel.channel.secret, sizeof(channel.channel.secret))) {
channel.channel.tx_radio = previous.channel.tx_radio;
}
const char* preset = companionChannelRadio2Preset(channel.name);
const char* previous_preset = companionChannelRadio2Preset(previous.name);
// A join or rename selects the saved preset, just like set radio2. App
// replays/key edits must not undo a later manual Off or custom RF setting.
const bool setup_preset = have_previous && preset
&& (!previous_preset || strcmp(preset, previous_preset));
if (setup_preset) {
#if !MESH_ENABLE_MESHTEXT_CHAT
// Even without the foreign-protocol backend, do not turn old fallback
// mt- channels into inactive protocol requests as a hidden side effect.
for (int i = 0; i < MAX_GROUP_CHANNELS; ++i) {
ChannelDetails other;
if (i != channel_idx && BaseChatMesh::getChannel(i, other)
&& parseCompanionChannelAlias(other.name).kind == CompanionChannelKind::Meshtext) {
writeErrFrame(ERR_CODE_BAD_STATE);
return;
}
}
#endif
if (!_radio_profiles.stageChannelPreset(preset)) {
writeErrFrame(ERR_CODE_BAD_STATE);
return;
}
}
if (!have_previous
|| !setChannel(channel_idx, channel)) {
if (setup_preset) _radio_profiles.finishChannelPreset(false);
writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx
} else if (saveChannels()) {
if (setup_preset && !_radio_profiles.finishChannelPreset(true)) {
// Never report a complete setup when the RF image did not commit.
// Restore the channel when storage permits; report failure even if
// an indeterminate channels write has quarantined further saves.
setChannel(channel_idx, previous);
saveChannels();
_radio_profiles.finishChannelPreset(false);
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
return;
}
#if MESH_ENABLE_MESHTEXT_CHAT
_mt_seen.clear();
_mt_profile_dirty = true;
refreshMeshtextProfile();
#endif
writeOKFrame();
// Local history seed only after the durable save and its acknowledgement.
seedEmergencyChannelNotice(channel_idx, previous);
} else {
// AtomicFileWriter preserves the last complete channels file. Keep the
// live table consistent with it when a full or failing filesystem cannot
// commit this update, and never acknowledge a change that will disappear
// after reboot.
setChannel(channel_idx, previous);
if (setup_preset) _radio_profiles.finishChannelPreset(false);
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
}
}
void MyMesh::handleCmdFrame(size_t len) {
if (len == 0) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
@@ -7453,101 +7552,7 @@ void MyMesh::handleCmdFrame(size_t len) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
#endif
} else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 16) {
uint8_t channel_idx = cmd_frame[1];
ChannelDetails previous{};
ChannelDetails channel;
StrHelper::strncpy(channel.name, (char *)&cmd_frame[2], 32);
memset(channel.channel.secret, 0, sizeof(channel.channel.secret));
memcpy(channel.channel.secret, &cmd_frame[2 + 32], 16); // NOTE: only 128-bit supported
const auto alias = parseCompanionChannelAlias(channel.name);
if (alias.kind == CompanionChannelKind::Invalid) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
#if MESH_ENABLE_MESHTEXT_CHAT
// Legacy apps fetch only 16 bytes. A label-only edit must not silently
// truncate a provisioned MT AES-256 key to AES-128. An explicit 32-byte SET
// (zero upper half for AES-128) is the unambiguous way to replace that key.
if (alias.kind == CompanionChannelKind::Meshtext
&& getChannelForSave(channel_idx, previous) && isMeshtextChannel(previous)
&& !memcmp(channel.channel.secret, previous.channel.secret, 16)) {
memcpy(channel.channel.secret + 16, previous.channel.secret + 16, 16);
}
if (!validateMeshtextChannelChange(channel_idx, channel)) {
writeErrFrame(ERR_CODE_BAD_STATE); // Conflicting preset, manual RF, region or busy owner.
return;
}
#else
if (alias.kind == CompanionChannelKind::Meshtext && isAliasRadio2Defined()) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // Meshtext transport not implemented yet.
return;
}
#endif
const bool have_previous = getChannelForSave(channel_idx, previous);
ChannelDetails wire_channel = channel;
ChannelDetails previous_wire_channel = previous;
resolveChannelAlias(wire_channel);
if (have_previous) resolveChannelAlias(previous_wire_channel);
if (have_previous && channel.name[0] != 0 && previous.name[0] != 0
&& !memcmp(wire_channel.channel.secret, previous_wire_channel.channel.secret, sizeof(channel.channel.secret))) {
channel.channel.tx_radio = previous.channel.tx_radio;
}
const char* preset = companionChannelRadio2Preset(channel.name);
const char* previous_preset = companionChannelRadio2Preset(previous.name);
// A join or rename selects the saved preset, just like set radio2. App
// replays/key edits must not undo a later manual Off or custom RF setting.
const bool setup_preset = have_previous && preset
&& (!previous_preset || strcmp(preset, previous_preset));
if (setup_preset) {
#if !MESH_ENABLE_MESHTEXT_CHAT
// Even without the foreign-protocol backend, do not turn old fallback
// mt- channels into inactive protocol requests as a hidden side effect.
for (int i = 0; i < MAX_GROUP_CHANNELS; ++i) {
ChannelDetails other;
if (i != channel_idx && BaseChatMesh::getChannel(i, other)
&& parseCompanionChannelAlias(other.name).kind == CompanionChannelKind::Meshtext) {
writeErrFrame(ERR_CODE_BAD_STATE);
return;
}
}
#endif
if (!_radio_profiles.stageChannelPreset(preset)) {
writeErrFrame(ERR_CODE_BAD_STATE);
return;
}
}
if (!have_previous
|| !setChannel(channel_idx, channel)) {
if (setup_preset) _radio_profiles.finishChannelPreset(false);
writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx
} else if (saveChannels()) {
if (setup_preset && !_radio_profiles.finishChannelPreset(true)) {
// Never report a complete setup when the RF image did not commit.
// Restore the channel when storage permits; report failure even if
// an indeterminate channels write has quarantined further saves.
setChannel(channel_idx, previous);
saveChannels();
_radio_profiles.finishChannelPreset(false);
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
return;
}
#if MESH_ENABLE_MESHTEXT_CHAT
_mt_seen.clear();
_mt_profile_dirty = true;
refreshMeshtextProfile();
#endif
writeOKFrame();
// Local history seed only after the durable save and its acknowledgement.
seedEmergencyChannelNotice(channel_idx, previous);
} else {
// AtomicFileWriter preserves the last complete channels file. Keep the
// live table consistent with it when a full or failing filesystem cannot
// commit this update, and never acknowledge a change that will disappear
// after reboot.
setChannel(channel_idx, previous);
if (setup_preset) _radio_profiles.finishChannelPreset(false);
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
}
handleSetChannelFrame(cmd_frame);
} else if (cmd_frame[0] == CMD_SIGN_START) {
BaseSerialInterface* signing_route = _serial->captureReplyRoute();
if (signing_route == NULL) {
+1
View File
@@ -332,6 +332,7 @@ public:
// constrained STM32 targets; merging their branches increases code size.
__attribute__((noinline))
#endif
void handleSetChannelFrame(const uint8_t* cmd_frame);
void handleCmdFrame(size_t len);
bool advert();
void enterCLIRescue();
+12
View File
@@ -6,6 +6,18 @@ Import("env")
# to the LTO driver automatically. Keep this list matched by the profile test.
# No compiler/ABI, flash boundary or capacity check is overridden here.
if env["PIOENV"] == "wio-e5-mini_repeater":
import importlib.util
from pathlib import Path
# Reuse the qualified RAK CLI's equivalent libc reply copies. Restrict
# this flag to CommonCLI.cpp; radio, framework and other objects retain
# the existing optimizer recipe.
source = Path(__file__).resolve().with_name("nrf52_flash_trim.py")
spec = importlib.util.spec_from_file_location("mesh_mini_cli_replies", source)
replies = importlib.util.module_from_spec(spec)
spec.loader.exec_module(replies)
env.AddBuildMiddleware(replies.compile_shared_cli_replies, "*CommonCLI.cpp")
env.Append(LINKFLAGS=[
"-fno-caller-saves",
"-fno-tree-dominator-opts",
+1
View File
@@ -206,6 +206,7 @@ public:
void writeErrFrame(uint8_t value) { error=value; }
void writeOKFrame() { ok=true; }
bool sendGroupMessage(uint32_t,mesh::GroupChannel&,const char*,const char*,int) { ++meshcore_sends; return true; }
void handleSetChannelFrame(const uint8_t*);
void command(uint8_t* cmd_frame,int len);
void commit(int index,const ChannelDetails& value) {
assert(setChannel(index,value)); _mt_profile_dirty=true; refreshMeshtextProfile();
+4 -3
View File
@@ -9,6 +9,7 @@
#include <helpers/CLICommandUtils.h>
#include <helpers/CompanionHardwareCommandCompat.h>
#include <helpers/radiolib/RXPowerSaving.h>
#include "usb_mode_console.h"
namespace mesh {enum class RadioParamApplyResult {APPLIED,BUSY,FAILED};}
struct Prefs {
@@ -40,7 +41,7 @@ struct Driver {
}
} radio_driver;
struct BaseSerialInterface {};
struct Serial {
struct SerialTransport {
BaseSerialInterface requester, other;
BaseSerialInterface* current = &requester;
BaseSerialInterface* reply_route = nullptr;
@@ -54,8 +55,8 @@ struct Serial {
};
struct MyMesh {
Prefs _prefs, durable;
Serial serial;
Serial* _serial = &serial;
SerialTransport serial;
SerialTransport* _serial = &serial;
bool _radio_available = true, storage_accepts = true, saved_radio_apply_pending = false;
bool _primary_radio_blocked=false;
unsigned saves = 0;
+7 -3
View File
@@ -9,6 +9,7 @@
#include <helpers/CLICommandUtils.h>
#include <helpers/CompanionFrameLimits.h>
#include <helpers/CompanionHardwareCommandCompat.h>
#include "../usb_mode_console.h"
struct Board {
#if defined(NRF52_PLATFORM)
@@ -31,7 +32,7 @@ struct Board {
#endif
};
struct BaseSerialInterface { unsigned id; };
struct Serial {
struct SerialTransport {
BaseSerialInterface usb{1}, ble{2};
BaseSerialInterface* current = &usb;
BaseSerialInterface* captured = nullptr;
@@ -62,8 +63,8 @@ struct Output {
};
struct MyMesh {
Board board;
Serial serial;
Serial* _serial = &serial;
SerialTransport serial;
SerialTransport* _serial = &serial;
Output output;
char reply_buf[160] = {};
uint8_t out_frame[MAX_FRAME_SIZE] = {};
@@ -142,6 +143,9 @@ static void dispatch(MyMesh& mesh, BaseSerialInterface* requester, bool nul) {
}
int main() {
MyMesh mesh;
char usb_reply[160] = {};
assert(mesh.handleCommand("AB|get usb.mode", 0, usb_reply));
assert(std::string(usb_reply) == std::string("AB|usb.mode ") + mesh::usbModeName<decltype(Serial)>());
bounds(mesh);
#if defined(NRF52_PLATFORM)
assert(mesh.board.reader_capacity == 128);
+23
View File
@@ -0,0 +1,23 @@
#pragma once
// Dispatch fixtures have no Arduino console, but must retain the real USB-mode
// handler. Model its backend types separately from each fixture's framed reply
// transport; no console reads, writes or USB peripherals are needed here.
struct HardwareSerial {};
struct HWCDC {};
struct USBCDC {};
struct Adafruit_USBD_CDC {};
#if (defined(ESP32) || defined(ESP32_PLATFORM)) \
&& defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
#if defined(ARDUINO_USB_MODE) && ARDUINO_USB_MODE == 1
inline HWCDC Serial;
#else
inline USBCDC Serial;
#endif
#elif defined(NRF52_PLATFORM) && defined(USE_TINYUSB)
inline Adafruit_USBD_CDC Serial;
#else
inline HardwareSerial Serial;
#endif
#include <helpers/UsbMode.h>
+3
View File
@@ -404,6 +404,7 @@ struct Serial {
'''
COMMAND_MEMBERS = r'''
void handleSetChannelFrame(const uint8_t*);
Serial serial; Serial* _serial = &serial;
struct ProfileSetup {
bool stage_ok = true, commit_ok = true, pending = false;
@@ -1027,6 +1028,7 @@ class CompanionChannelAliasTest(unittest.TestCase):
"if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 32)",
"if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 16)",
)
methods += "\n" + body(companion, "void MyMesh::handleSetChannelFrame(")
branches = " else ".join(body(companion, signature) for signature in command_signatures)
macros = ("CMD_GET_CHANNEL", "CMD_SET_CHANNEL", "RESP_CODE_CHANNEL_INFO",
"ERR_CODE_UNSUPPORTED_CMD", "ERR_CODE_NOT_FOUND", "ERR_CODE_FILE_IO_ERROR",
@@ -1166,6 +1168,7 @@ int main() {
"MyMesh::Frame& MyMesh::heldDMFrameAt(",
"void MyMesh::removeHeldOneKeyDM(",
)) + "\n#endif\n"
methods += "\n" + body(companion, "void MyMesh::handleSetChannelFrame(")
branches = " else ".join(body(companion, signature) for signature in (
"if (cmd_frame[0] == CMD_GET_CHANNEL && len >= 2)",
"if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 32)",
@@ -88,6 +88,7 @@ def publication_fixture(work, missing_latch, skip_noop_save=False):
methods = recovery + "\n" + route
methods += "\n" + method(store, "void DataStore::loadChannels(")
methods += "\n" + save + "\n" + method(store, "bool DataStore::hasIncompleteContactLoad(")
methods += "\n" + method(mesh, "void MyMesh::handleSetChannelFrame(")
binary_start = mesh.index(" } else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 16) {")
binary_end = mesh.index(" } else if (cmd_frame[0] == CMD_SIGN_START)", binary_start)
methods += ("\nvoid MyMesh::applyChannelFrame(const uint8_t* cmd_frame, size_t len) {\n"
@@ -129,6 +130,7 @@ struct StrHelper {
void writeOKFrame() { binary_ack = true; }
void seedEmergencyChannelNotice(uint8_t, const ChannelDetails&) {}
bool resolveChannelAlias(ChannelDetails&) const { return true; }
void handleSetChannelFrame(const uint8_t*);
void applyChannelFrame(const uint8_t*, size_t);
""", 1)
source = source.replace("@RECOVERY_METHOD@", method(header, " bool contactRecoveryPending() const"))
+6
View File
@@ -29,6 +29,7 @@ HARNESS = r'''
#include <helpers/FleetCommand.h>
#include <helpers/TempRadioReplyBarrier.h>
#include "regulator_cli_stub.h"
#include "usb_mode_console.h"
#include <helpers/CompanionFrameLimits.h>
#include <helpers/HostKissCodec.h>
#include <openssl/evp.h>
@@ -364,6 +365,10 @@ static void compactBroadcast(MyMesh& node, const char* command) {
int main(int argc, char** argv) {
assert(argc == 2);
MyMesh usb_probe;
char usb_reply[160] = {};
assert(usb_probe.handleCommand("AB|get usb.mode", 0, usb_reply));
assert(std::string(usb_reply) == "AB|usb.mode unknown" && usb_probe.queued == 0);
const std::string scenario = argv[1];
MyMesh node; char reply[160] = {};
if (scenario == "local_boundary") {
@@ -1513,6 +1518,7 @@ class CompanionFleetControlTests(unittest.TestCase):
#include <cstring>
#include <helpers/FleetCommand.h>
#include "regulator_cli_stub.h"
#include "usb_mode_console.h"
static_assert(MESH_ENABLE_FLEET_CONTROL == 0, "compact profile must disable fleet");
// No channels, clock, signing identity, codec definitions or sendGroupData.
class MyMesh {
+1 -1
View File
@@ -60,7 +60,7 @@ class CompanionMeshtextTest(unittest.TestCase):
"static bool isEmergencyNoticeChannelName(",
"void MyMesh::seedEmergencyChannelNotice(",
"void MyMesh::servicePendingChannelNotices(", "bool MyMesh::queueChannelText(",
"bool MyMesh::saveAdvertName(")
"bool MyMesh::saveAdvertName(", "void MyMesh::handleSetChannelFrame(")
methods = "\n".join(body(source, signature) for signature in signatures)
utils_source = (ROOT / "src/Utils.cpp").read_text(encoding="utf-8")
# CLI keys use the production strict hexadecimal parser as well.
@@ -22,6 +22,7 @@ HARNESS = r'''
#include <helpers/esp32/PowerTestControls.h>
#include "companion_fleet_dispatch.h"
#include "battery_calibration_cli_stub.h"
#include "usb_mode_console.h"
#include <sys/stat.h>
#include <limits>
#define constrain(value,low,high) ((value)<(low)?(low):((value)>(high)?(high):(value)))
+1
View File
@@ -392,6 +392,7 @@ int main() {
#include <cstring>
#include <string>
#include "battery_calibration_cli_stub.h"
#include "usb_mode_console.h"
struct BatteryBoard {} board;
struct Radio { bool active = false; };
namespace mesh {
+1
View File
@@ -282,6 +282,7 @@ int main() {
built = subprocess.run([
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", *sanitizers,
"-DCOMPANION_FEATURE_TEMP_RADIO=0", "-I", str(ROOT / "src"),
"-I", str(ROOT / "test/fixtures"),
"-isystem", str(ROOT / "test/mocks"), str(cpp),
str(ROOT / "src/helpers/radiolib/RXPowerSaving.cpp"), "-o", str(binary),
], capture_output=True, text=True, timeout=60)
+32
View File
@@ -39,6 +39,15 @@ def read_config(path):
class FakeEnvironment(dict):
def AddBuildMiddleware(self, callback, pattern):
self.middleware = getattr(self, "middleware", []) + [(callback, pattern)]
def subst(self, value):
return value.replace("$PROJECT_DIR", str(ROOT))
def Object(self, node, **flags):
return node, flags
def Append(self, **values):
for name, items in values.items():
self.setdefault(name, []).extend(items)
@@ -126,6 +135,29 @@ class MiniRepeaterProfileTest(unittest.TestCase):
expected = ["keep-existing-flag", *(LINK_SIZE_FLAGS if target == TARGET else ())]
self.assertEqual(environment["LINKFLAGS"], expected)
self.assertEqual(set(environment), {"PIOENV", "LINKFLAGS"})
self.assertEqual(len(getattr(environment, "middleware", [])),
int(target == TARGET))
def test_shared_reply_copy_policy_changes_only_the_exact_mini_cli_object(self):
class Node:
def __init__(self, path): self.path = path
def srcnode(self): return self
def get_abspath(self): return str(self.path)
environment = FakeEnvironment(PIOENV=TARGET, CCFLAGS=["-Oz", "-flto"])
runpy.run_path(str(ROOT / "scripts/stm32_mini_size.py"), init_globals={
"env": environment, "Import": lambda name: self.assertEqual(name, "env")})
callback, pattern = environment.middleware[0]
self.assertEqual(pattern, "*CommonCLI.cpp")
cli = Node(ROOT / "src/helpers/CommonCLI.cpp")
same, flags = callback(environment, cli)
self.assertIs(same, cli)
self.assertEqual(flags["CCFLAGS"], ["-Oz", "-flto", "-fno-builtin-strcpy"])
self.assertEqual(environment["CCFLAGS"], ["-Oz", "-flto"])
for path in ("test/fixtures/CommonCLI.cpp", "src/helpers/RadioProfileCLI.cpp",
"src/helpers/UsbLogging.cpp"):
other = Node(ROOT / path)
self.assertIs(callback(environment, other), other)
def test_capability_contract_declares_only_exact_mini_repeater_omissions(self):
command = r'''