Fit STM32 release images and repair stale CI fixtures

Fix the STM32 flash overflows and fleet fixture compile failure from
https://github.com/mikecarper/MeshCore/actions/runs/40241742182.

Fold already-unsupported RX control paths only for the integrated STM32WL
wrapper. Preserve RXPS-off validation, persistence, pending application and
rollback. External radios retain their supported controls.

Use measured GCC 14 code sharing in the four small STM32 USB Companion
recipes. Keep firmware features, contact/channel capacities, filesystem
boundaries and flash/RAM admission checks unchanged.

Refresh downstream host fixtures for saved radio intent, deferred service
restarts, WebConfig ownership, RX recovery and color tint state. Retain or
strengthen behavior checks rather than skipping failing coverage.

Validation: canonical RAK3x72, Wio Mini, Wio Dev and Tiny Relay USB Companion
packages and Mini Repeater build passed at release-version length. All
post-failure host checks, compact arithmetic/float checks, build-profile and
real PlatformIO recipe/path/RAM-policy checks passed locally.
This commit is contained in:
mikecarper committed 2026-10-25 22:00:24 -07:00
1 parent ad34fd3a0d
commit b0c49a4cc3
24 files changed
+252 -35

No files matched your search

+36 -4
View File
@@ -253,8 +253,10 @@ mesh::RadioParamApplyResult CommonCLI::applyRxSetting(uint8_t setting) {
const mesh::Radio* radio = _callbacks->getProfileRadio();
if (mesh::morseTimingActive(radio) || (radio && radio->isCarrierWaveActive())) return Result::BUSY;
switch (setting) {
#if !defined(STM32_PLATFORM) || !defined(STM32WLxx) || !defined(USE_STM32WL)
case RxGain:
return _callbacks->trySetRxBoostedGain(_prefs->rx_boosted_gain != 0);
#endif
case RxPowerSaving:
return _callbacks->trySetRxPowerSaving(_prefs->rx_powersaving_enabled != 0,
_prefs->rx_ps_rx_us, _prefs->rx_ps_sleep_us);
@@ -265,9 +267,11 @@ mesh::RadioParamApplyResult CommonCLI::applyRxSetting(uint8_t setting) {
return _callbacks->tryConfigSideDetectors(_prefs->extra_sf, count, _prefs->bw);
}
#endif
#if !defined(STM32_PLATFORM) || !defined(STM32WLxx) || !defined(USE_STM32WL)
case RxRfRx:
return _callbacks->trySetRxPowerSavingRfRxDisabled(
(_rx_settings_pending & RxRfRxDisabled) != 0);
#endif
default: return Result::FAILED;
}
}
@@ -276,7 +280,14 @@ void CommonCLI::servicePendingRxSettings() {
if (!(_rx_settings_pending & RxPendingMask) || (_rx_settings_retry_at
&& static_cast<int32_t>(millis() - _rx_settings_retry_at) < 0)) return;
bool failed = false;
for (uint8_t setting = RxGain; setting <= RxRfRx; setting <<= 1) {
#if defined(STM32_PLATFORM) && defined(STM32WLxx) && defined(USE_STM32WL)
// The integrated driver supports only the RXPS-off transaction below.
// Gain, RF_RX control and side detectors cannot acquire pending targets.
constexpr uint8_t first = RxPowerSaving, last = RxPowerSaving;
#else
constexpr uint8_t first = RxGain, last = RxRfRx;
#endif
for (uint8_t setting = first; setting <= last; setting <<= 1) {
if (!(_rx_settings_pending & setting)) continue;
const auto result = applyRxSetting(setting);
if (result == mesh::RadioParamApplyResult::APPLIED) _rx_settings_pending &= ~setting;
@@ -4782,7 +4793,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
mesh::cli::formatError(reply, 160, "state must be on or off");
return;
}
if (!_callbacks->supportsRxBoostedGain()) {
#if defined(STM32_PLATFORM) && defined(STM32WLxx) && defined(USE_STM32WL)
// CustomSTM32WLxWrapper has no runtime gain/RXPS/RF_RX controls. Fold
// unreachable setters without changing their validation or error replies.
constexpr bool gain_supported = false;
#else
const bool gain_supported = _callbacks->supportsRxBoostedGain();
#endif
if (!gain_supported) {
mesh::cli::formatError(reply, 160, "unsupported");
return;
}
@@ -4865,7 +4883,12 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
return;
}
if (!_callbacks->supportsRxPowerSavingRfRxDisable()) {
#if defined(STM32_PLATFORM) && defined(STM32WLxx) && defined(USE_STM32WL)
constexpr bool rf_rx_control_supported = false;
#else
const bool rf_rx_control_supported = _callbacks->supportsRxPowerSavingRfRxDisable();
#endif
if (!rf_rx_control_supported) {
mesh::cli::formatError(reply, 160, "unsupported");
return;
}
@@ -4909,7 +4932,16 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
return;
}
using mesh::cli::RxPowerSavingArgumentMode;
if (!_callbacks->supportsRxPowerSaving() && parsed.mode != RxPowerSavingArgumentMode::Off) {
#if defined(STM32_PLATFORM) && defined(STM32WLxx) && defined(USE_STM32WL)
#ifdef WRAPPER_CLASS
static_assert(!WRAPPER_CLASS::SupportsRxPowerSaving,
"Revisit STM32WL CLI folding when the integrated driver gains RXPS");
#endif
constexpr bool powersaving_supported = false;
#else
const bool powersaving_supported = _callbacks->supportsRxPowerSaving();
#endif
if (!powersaving_supported && parsed.mode != RxPowerSavingArgumentMode::Off) {
strcpy(reply, "ERROR: RX powersaving unsupported");
return;
}
@@ -1,4 +1,8 @@
#pragma once
#ifndef USE_STM32WL
#define USE_STM32WL
#endif
#include "SX126xReceiveMode.h"
#include "CustomSTM32WLx.h"
+13 -1
View File
@@ -42,9 +42,10 @@ struct Board{
void setInhibitSleep(bool enabled){inhibit_sleep=enabled;}
}board;
struct Mesh{
bool setup=false,recovery=false,active=false;
bool setup=false,recovery=false,active=false,stopping=false;
bool isWebConfigSetupActive(){return setup;}
bool isWebConfigWiFiRecoveryActive(){return recovery;}
bool isWebConfigWiFiTransitionActive(){return setup||stopping;}
bool isWebConfigActiveOrStopping(){return active;}
bool startWebConfig(bool,char*){return false;}
void stopWebConfig(){setup=false;}
@@ -113,6 +114,17 @@ int main(int argc,char** argv){
sdk_mode=WIFI_STA;board.ota=false;serviceCompanionWiFiCredentialReload();
assert(!companion_wifi_credential_reload_pending&&companion_wifi_credential_reload_at==0);
assert(credential_loads==1&&mode_writes==1&&disconnects==1&&joins==1);
}else if(scenario=="transition_reload"){
#ifdef WITH_WEBCONFIG
sdk_mode=WIFI_STA;board.ota=false;the_mesh.stopping=true;
serviceCompanionWiFiCredentialReload();
assert(companion_wifi_credential_reload_pending&&companion_wifi_credential_reload_at==1);
assert(credential_loads==0&&mode_writes==0&&disconnects==0&&joins==0);
// The SDK AP may be gone while WebConfig still owns its teardown barrier.
the_mesh.stopping=false;serviceCompanionWiFiCredentialReload();
assert(!companion_wifi_credential_reload_pending&&companion_wifi_credential_reload_at==0);
assert(credential_loads==1&&mode_writes==1&&disconnects==1&&joins==1);
#endif
}else if(scenario=="setup_handoff"){
#ifdef WITH_WEBCONFIG
sdk_mode=WIFI_AP_STA;board.ota=false;the_mesh.setup=the_mesh.recovery=true;
+3 -1
View File
@@ -111,6 +111,7 @@ public:
void endCommand() {}
} _morse_reply;
bool _morse_reply_admission = false;
bool _primary_radio_reply_admission = false;
struct Prefs { uint8_t lost_reply = 0; } _prefs;
Clock clock, *_ms = &clock;
Manager manager, *_mgr = &manager;
@@ -462,8 +463,9 @@ static void localPolicyAndRemoteBoundary() {
auto& peer = m.contact(1); peer.flags = 0x10;
for (uint32_t timestamp : {uint32_t(0), uint32_t(1), uint32_t(0xffffffffU)}) {
reply[0] = 0;
m._primary_radio_reply_admission = true;
m.onCLICommandRecv(peer, &m.incoming, timestamp, "set lost.reply yes", reply);
assert(m._prefs.lost_reply == 0 && m.saves == 4);
assert(!m._primary_radio_reply_admission && m._prefs.lost_reply == 0 && m.saves == 4);
reply[0] = 0;
m.onCLICommandRecv(peer, &m.incoming, timestamp, "get lost.reply", reply);
assert(std::string(reply) == "Unknown command");
+7
View File
@@ -53,7 +53,13 @@ static void transmitted(SensorMesh& mesh) {
assert(mesh.pool.queued.size() == 1 && mesh.tracker_pending);
Packet* packet = mesh.pool.removeOutboundByIdx(0);
mesh.pool.in_flight = packet;
const auto tx_services = mesh._cli.tx_services, rx_services = mesh._cli.rx_services;
const auto tuning_services = mesh._cli.profiles.tuning_services;
const auto primary_services = mesh.primary_radio_services;
mesh.onSendComplete(packet);
assert(mesh._cli.tx_services == tx_services + 1 && mesh._cli.rx_services == rx_services + 1);
assert(mesh._cli.profiles.tuning_services == tuning_services + 1);
assert(mesh.primary_radio_services == primary_services + 1);
assert(mesh.tracker_sent && mesh.tracker_reply_until == uint32_t(mesh.now_ms + 20000));
// The dispatcher owns and frees the original packet after this callback.
mesh.pool.in_flight = nullptr; mesh.releasePacket(packet);
@@ -640,6 +646,7 @@ static void gpsAndMaintenance() {
{ SensorMesh mesh; configure(mesh); queueCheck(mesh); transmitted(mesh); assert(response(mesh, Safe));
bool* inhibitors[] = {&mesh.board.usb, &mesh.board.ota, &mesh.board.test, &mesh.temporary,
&mesh.pending_ota, &mesh.dual, &mesh.saved_radio_apply_pending, &mesh._cli.active,
&mesh._cli.pending_radio_settings,
&mesh.queue_wake, &mesh.retry_wake, &mesh::test_logging_watchdog};
for (bool* condition : inhibitors) {
*condition = true; assert(mesh.trackerNeedsRadio()); mesh.serviceTracker();
+12 -2
View File
@@ -169,7 +169,15 @@ public:
FakeTrigger* alert_tasks[4] = {&alert1, &alert2};
int num_alert_tasks = 2;
struct CLI {
void servicePendingTxPower() {}
unsigned tx_services = 0, rx_services = 0;
bool pending_radio_settings = false;
void servicePendingTxPower() { ++tx_services; }
void servicePendingRxSettings() { ++rx_services; }
struct Profiles {
unsigned tuning_services = 0;
void servicePendingTuning() { ++tuning_services; }
} profiles;
Profiles& radioProfiles() { return profiles; }
bool batteryAllowsForward(bool) const { return true; }
bool batteryAllowsTransmit(const mesh::Packet*) const { return true; }
struct NoMorseReply { void complete(const void*) {} void fail(const void*) {} } morse;
@@ -180,13 +188,15 @@ public:
SensorMesh* owner = nullptr;
void loop();
bool hasActiveUserGpioTimer() const { return active; }
bool hasPendingTxPower() const { return false; }
bool hasPendingRadioSettings() const { return pending_radio_settings; }
} _cli;
struct ClockSync { unsigned observed = 0, loops = 0;
void loop() { ++loops; }
void observeAcceptedFlood(const mesh::Packet*) { ++observed; } } _clock_sync;
uint32_t set_radio_at = 0, revert_radio_at = 0;
bool saved_radio_apply_pending = false, temporary = false, pending_ota = false, dual = false;
unsigned primary_radio_services = 0;
void servicePendingPrimaryRadioSettings(bool tx_gap) { assert(tx_gap); ++primary_radio_services; }
bool queue_wake = false, retry_wake = false, available = true;
bool allocation_failure = false, admission_failure = false;
unsigned created = 0, direct_sends = 0, flood_sends = 0, adverts = 0, zero_hops = 0;
+2 -1
View File
@@ -76,6 +76,7 @@ struct RadioLibWrapper {
PhysicalLayer* _radio;
mesh::MainBoard* _board;
bool _params_valid = true, _rx_ps_armed = true, _dbm_valid = false, _dbm_applied = false, _rx_boosted_gain_valid = false;
bool _rx_boosted_gain_applied = true;
bool _profile_repair_required = false;
int state = 1;
float _cur_freq = 906.875f, _cur_bw = 250;
@@ -142,7 +143,7 @@ static void failures_and_recovery() {
assert(wrapper.restoreAfterDeepInit());
assert(!wrapper._profile_repair_required);
assert(chip.sync == 0x2b && chip.crc && chip.explicit_header && !chip.iq);
assert(!wrapper._rx_ps_armed && wrapper.state == STATE_IDLE);
assert(!wrapper._rx_ps_armed && wrapper.state == STATE_IDLE && !wrapper._rx_boosted_gain_applied);
chip.fail_at = fail;
wrapper._profile_repair_required = true;
assert(!wrapper.restoreAfterDeepInit());
+3
View File
@@ -62,6 +62,9 @@ class CompanionAccessPointOwnershipTest(unittest.TestCase):
self.run_fixture("reload")
self.run_fixture("reload", webconfig=False)
def test_credential_reload_waits_for_webconfig_teardown_after_sdk_ap_is_gone(self):
self.run_fixture("transition_reload")
def test_starting_wifi_services_defers_without_consuming_requested_intent(self):
self.run_fixture("start_wifi")
self.run_fixture("start_wifi", webconfig=False)
+2
View File
@@ -27,6 +27,7 @@ HARNESS = r'''
#include <string>
#include <vector>
#include <helpers/FleetCommand.h>
#include <helpers/TempRadioReplyBarrier.h>
#include "regulator_cli_stub.h"
#include <helpers/CompanionFrameLimits.h>
#include <openssl/evp.h>
@@ -132,6 +133,7 @@ struct Clock { uint32_t now = mesh::FleetCommand::MinEpoch + 1000;
class MyMesh : public mesh::Mesh {
public:
struct NoMorseReply { void copy(const void*, const void*) {} } _morse_reply;
mesh::TempRadioReplyBarrier _primary_radio_reply;
mesh::LocalIdentity self_id;
Clock clock;
ChannelDetails channels[MAX_GROUP_CHANNELS];
+6 -6
View File
@@ -47,10 +47,10 @@ MultiSerialInterface interface_manager;
constexpr int EXPECTED_ACK_TABLE_SIZE = 6;
struct MyMesh {
bool streaming = false;
int stream_cancels = 0, pending_cancels = 0, radio_cancels = 0;
int stream_cancels = 0, pending_cancels = 0;
int trace_cancels = 0, signing_cancels = 0, expirations = 0;
mesh::CompanionDelayedReplies _delayed_replies;
BaseSerialInterface* command_radio_reply_route = nullptr;
bool saved_radio_apply_pending = false;
BaseSerialInterface* sign_data_reply_route = nullptr;
BaseSerialInterface* private_key_backup_route = nullptr;
unsigned long private_key_backup_deadline = 0;
@@ -64,7 +64,6 @@ struct MyMesh {
if (streaming) { streaming = false; interface_manager.unlockReplyRoute(); }
}
void clearPendingReqs() { ++pending_cancels; _delayed_replies.retireRequest(millis()); }
void cancelPendingRadioParamApply() { ++radio_cancels; command_radio_reply_route = nullptr; }
void clearBinaryTraceReply() { ++trace_cancels; _delayed_replies.retireBinaryTrace(millis()); }
void cancelSigningSession() { ++signing_cancels; sign_data_reply_route = nullptr; }
void expireExpectedAcks() { ++expirations; }
@@ -91,7 +90,7 @@ int main() {
assert(the_mesh._delayed_replies.reserveRequest(
mesh::CompanionDelayedReplies::Binary, peer, owner, false, millis()));
the_mesh._delayed_replies.armRequest(17, 1000, false, millis());
the_mesh.command_radio_reply_route = owner;
the_mesh.saved_radio_apply_pending = true;
assert(the_mesh._delayed_replies.reserveBinaryTrace(17, 23, owner, millis()));
the_mesh._delayed_replies.armBinaryTrace(1000, millis());
the_mesh.sign_data_reply_route = owner;
@@ -111,12 +110,13 @@ int main() {
assert(the_mesh.stream_cancels == int(cancelled));
assert(the_mesh.streaming == !cancelled);
assert(the_mesh.pending_cancels == int(cancelled));
assert(the_mesh.radio_cancels == int(cancelled));
assert(the_mesh.trace_cancels == int(cancelled));
assert(the_mesh.signing_cancels == int(cancelled));
assert(the_mesh._delayed_replies.request.route == (cancelled ? nullptr : owner));
assert(the_mesh._delayed_replies.hasRequest() == !cancelled);
assert(the_mesh.command_radio_reply_route == (cancelled ? nullptr : owner));
// The radio setting was already saved and acknowledged. Losing a client
// session must not cancel its automatic hardware application.
assert(the_mesh.saved_radio_apply_pending);
assert(the_mesh._delayed_replies.trace.route == (cancelled ? nullptr : owner));
assert(the_mesh._delayed_replies.hasBinaryTrace() == !cancelled);
assert(the_mesh.sign_data_reply_route == (cancelled ? nullptr : owner));
+4 -1
View File
@@ -159,7 +159,10 @@ int main() {
start = mesh.index("bool MyMesh::hasFiniteDelayedReplyForRoute(")
body = mesh[start : mesh.index("void MyMesh::servicePendingSerialReply()", start)]
self.assertIn("_delayed_replies.hasReplyForRoute(route, _ms->getMillis())", body)
self.assertIn("command_radio_reply_route == route", body)
# Radio settings acknowledge their saved value immediately; automatic
# background application must not prolong the USB reply lease.
self.assertNotIn("command_radio_reply_route", body)
self.assertNotIn("saved_radio_apply_pending", body)
delayed = source("src/helpers/CompanionDelayedReplies.cpp")
lease = delayed[delayed.index("bool CompanionDelayedReplies::hasReplyForRoute("):]
self.assertIn("{ &request, &trace }", lease)
+4 -4
View File
@@ -142,10 +142,9 @@ class EspNowLifecycleTests(unittest.TestCase):
mqtt_switch = method(common, 'if (strncmp(config, "mqtt.enabled ", 13) == 0)')
self.assertIn('enable ? _callbacks->setMqttBridgeState(true)', mqtt_switch)
self.assertIn(': _callbacks->requestMqttBridgeStop()', mqtt_switch)
# Reject an unsafe restart before changing saved intent. An accepted
# OFF command saves successfully before signalling cooperative stop.
self.assertLess(mqtt_switch.index('isMqttBridgeStopping()'),
mqtt_switch.index('_prefs->bridge_enabled ='))
# Save desired state even during teardown; the role queues startup
# until stopping finishes. OFF still saves before cooperative stop.
self.assertNotIn('isMqttBridgeStopping()', mqtt_switch)
self.assertLess(mqtt_switch.index('if (!trySavePrefs())'),
mqtt_switch.index('setMqttBridgeState(true)'))
self.assertLess(mqtt_switch.index('if (!trySavePrefs())'),
@@ -161,6 +160,7 @@ class EspNowLifecycleTests(unittest.TestCase):
espnow = method(header, 'bool setEspNowBridgeState(bool enable) override')
self.assertIn('if (espnow_bridge.isRunning()) espnow_bridge.end();', mqtt)
self.assertLess(mqtt.index('isStopping()'), mqtt.index('espnow_bridge.end()'))
self.assertIn('->isStartPending()', mqtt)
self.assertIn('->requestStop()', stop)
self.assertNotIn('->end()', stop)
self.assertIn('if (mqtt_bridge' if role == 'simple_repeater' else 'if (bridge', espnow)
+1
View File
@@ -198,6 +198,7 @@ struct Bus {
};
struct Panel {
uint16_t displayWidth=0,displayHeight=0,displayBufferSize=0,_buffheight=0,_RGB=0xffff;
uint16_t _lastRGB=0xffff;
OLEDDISPLAY_GEOMETRY geometry=GEOMETRY_RAWMODE;
unsigned logBufferSize=0,logBufferFilled=0,logBufferLine=0,logBufferMaxLines=0;
uint8_t* buffer=nullptr; uint8_t* buffer_back=nullptr; uint8_t* logBuffer=nullptr;
+7
View File
@@ -44,8 +44,10 @@ struct MyMesh {
const AbstractBridge* activeBridge() const { return bridge; }
struct { bool pending = false; } deferred_cli_command;
struct {
bool radio_settings_pending = false;
bool hasActiveUserGpioTimer() const { return false; }
bool hasPendingTxPower() const { return false; }
bool hasPendingRadioSettings() const { return radio_settings_pending; }
} _cli;
struct { int getNumClients() const { return 0; } } acl;
struct { bool battery_alert_enabled = false; } _prefs;
@@ -83,6 +85,11 @@ int main() {
try {
MyMesh node;
require(node.getPowerSaveSleepSeconds(30) == 30, "quiet node cannot sleep");
node._cli.radio_settings_pending = true;
require(node.hasPendingWork(), "pending radio settings lost their sleep blocker");
require(node.getPowerSaveSleepSeconds(30) == 0,
"device sleep can interrupt pending radio settings");
node._cli.radio_settings_pending = false;
MQTTBridge logger;
node.bridge = &logger;
require(node.getPowerSaveSleepSeconds(30) == 30, "stopped MQTT blocked sleep");
+5 -2
View File
@@ -325,6 +325,7 @@ struct RadioLibWrapper {
bool supported=true;
bool supportsMeshtext() const { return supported; }
bool _rx_ps_armed=true,_params_valid=true,_dbm_valid=false,_dbm_applied=false,_rx_boosted_gain_valid=false;
bool _rx_boosted_gain_applied=true,gain_ok=true;
int state=1;uint8_t _cur_sf=11,_cur_cr=5;float _cur_freq=906.875,_cur_bw=250;
int8_t _cur_dbm=22;bool _cur_rx_boosted_gain=false;
mesh::RadioProfileProtocol _physical_protocol=mesh::RadioProfileProtocol::Meshtext;
@@ -333,7 +334,7 @@ struct RadioLibWrapper {
bool applyParams(float,float,uint8_t,uint8_t) { return true; }
uint16_t currentPreambleLength() { return 16; }
int applyCachedTxPower(int8_t) { return 0; }
bool applyRxBoostedGainMode(bool) { return true; }
bool applyRxBoostedGainMode(bool) { return gain_ok; }
void recalibrateNoiseFloor() {}
@WIRE@
bool restoreAfterDeepInit();
@@ -353,8 +354,10 @@ int main() {
assert(!invalid.applyProtocol(static_cast<Protocol>(99)) && invalid.chip.step==0);
RadioLibWrapper w;assert(w.restoreAfterDeepInit());
assert(w.chip.sync==0x2b && w.chip.crc && w.chip.explicit_header && !w.chip.iq);
assert(!w._rx_ps_armed && w.state==STATE_IDLE);
assert(!w._rx_ps_armed && w.state==STATE_IDLE && !w._rx_boosted_gain_applied);
w._physical_protocol=Protocol::MeshCore;assert(w.restoreAfterDeepInit());assert(w.chip.sync==0x12);
w._rx_boosted_gain_valid=true;assert(w.restoreAfterDeepInit() && w._rx_boosted_gain_applied);
w.gain_ok=false;assert(!w.restoreAfterDeepInit() && !w._rx_boosted_gain_applied);
RadioLibWrapper disabled;disabled.supported=false;
assert(disabled.applyProtocol(Protocol::MeshCore));
assert(!disabled.applyProtocol(Protocol::Meshtext) && disabled.chip.step==0);
+3 -2
View File
@@ -182,9 +182,9 @@ struct MyMesh {
struct { bool busy_value = false; bool busy() const { return busy_value; } } _local_cli_output;
struct { bool pending = false; } deferred_cli_command;
struct {
bool gpio = false, tx_pending = false;
bool gpio = false, tx_pending = false, rx_pending = false;
bool hasActiveUserGpioTimer() const { return gpio; }
bool hasPendingTxPower() const { return tx_pending; }
bool hasPendingRadioSettings() const { return tx_pending || rx_pending; }
} _cli;
struct { bool watchdog = false, calibration = false;
bool isWatchdogObserving() const { return watchdog; }
@@ -245,6 +245,7 @@ int main() {
check(node._local_cli_output.busy_value, Reason::SerialIO);
check(node.deferred_cli_command.pending, Reason::Scheduled);
check(node._cli.tx_pending, Reason::Scheduled);
check(node._cli.rx_pending, Reason::Scheduled);
check(node.uart.running, Reason::Bridge);
node.active_rs232_bridge_uart = 1;
assert(node.pendingWorkReason() == mask(Reason::BridgeCleanup));
+99 -9
View File
@@ -10,13 +10,13 @@ from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
def preprocess(source):
def preprocess(source, defines=("USE_LR2021",)):
return subprocess.run([os.environ.get("CXX", "g++"), "-E", "-P", "-x", "c++",
"-DUSE_LR2021", "-"], input=source, text=True,
*("-D" + value for value in defines), "-"], input=source, text=True,
capture_output=True, check=True).stdout
def fixture():
def fixture(defines=("USE_LR2021",)):
source = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="ascii")
header = (ROOT / "src/helpers/CommonCLI.h").read_text(encoding="ascii")
methods = "\n".join(extract_braced(source, signature) for signature in (
@@ -24,18 +24,18 @@ def fixture():
"void CommonCLI::servicePendingRxSettings(", "bool CommonCLI::saveRxSetting(",
"void CommonCLI::restoreRxSetting(", "bool CommonCLI::trySavePrefs(",
"void CommonCLI::loop(", "bool CommonCLI::calculateRxPowerSavingLevel("))
setter = preprocess(extract_braced(source, "void CommonCLI::handleSetCmd("))
setter = preprocess(extract_braced(source, "void CommonCLI::handleSetCmd("), defines)
branches = [extract_braced(setter, signature) for signature in (
'if (memcmp(config, "radio.rxgain ", 13) == 0)',
'if (memcmp(config, "radio.rxps ", 11) == 0)',
'if (strncmp(config, "radio.rxps.rfrx_disabled ", 25) == 0)',
'if (strcmp(config, "extra.sf") == 0 || memcmp(config, "extra.sf ", 9) == 0)')]
values = {
"METHODS": preprocess(methods),
"SETTERS": preprocess(" else ".join(branches)),
"METHODS": preprocess(methods, defines),
"SETTERS": preprocess(" else ".join(branches), defines),
"ENUM": extract_braced(header, "enum RxSetting : uint8_t") + ";",
"PENDING": extract_braced(header, "bool hasPendingRadioSettings() const"),
"NOTE": preprocess(extract_braced(source, "static void appendRxPowerSavingAdjustmentNote(")),
"NOTE": preprocess(extract_braced(source, "static void appendRxPowerSavingAdjustmentNote("), defines),
}
result = HARNESS
for key, value in values.items():
@@ -313,10 +313,10 @@ int main() {
class InfrastructureRxSettingsTests(unittest.TestCase):
def test_actual_commands_and_pending_transactions(self):
def compile_run(self, code):
with tempfile.TemporaryDirectory(prefix="mesh-infra-rx-") as directory:
source, exe = Path(directory) / "test.cpp", Path(directory) / "test"
source.write_text(fixture(), encoding="ascii")
source.write_text(code, encoding="ascii")
built = subprocess.run([
os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
@@ -326,6 +326,96 @@ class InfrastructureRxSettingsTests(unittest.TestCase):
result = subprocess.run([str(exe)], text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_actual_commands_and_pending_transactions(self):
self.compile_run(fixture())
def test_integrated_stm32_preserves_unsupported_errors_and_off_transactions(self):
code = fixture(("STM32_PLATFORM", "STM32WLxx", "USE_STM32WL")).split("static bool ok(", 1)[0]
code += r'''
int main() {
char reply[160];
CommonCLI cli;
cli.callbacks.gain_supported = cli.callbacks.ps_supported = cli.callbacks.rf_supported = false;
for (const char* value : {"radio.rxgain on", "radio.rxgain off",
"radio.rxps.rfrx_disabled on", "radio.rxps.rfrx_disabled off"}) {
cli.command(value, reply); assert(!strcmp(reply, "Error: unsupported"));
}
for (const char* value : {"radio.rxgain banana", "radio.rxgain on ",
"radio.rxps.rfrx_disabled banana", "radio.rxps.rfrx_disabled off "}) {
cli.command(value, reply); assert(!strcmp(reply, "Error: state must be on or off"));
}
for (const char* value : {"radio.rxps on", "radio.rxps conservative", "radio.rxps balanced",
"radio.rxps level 8 preamble 32", "radio.rxps 2000 3000"}) {
cli.command(value, reply); assert(!strcmp(reply, "ERROR: RX powersaving unsupported"));
}
cli.command("radio.rxps banana", reply); assert(strstr(reply, "ERROR: use off|on|"));
assert(!cli.callbacks.gain_attempts && !cli.callbacks.rf_attempts
&& !cli.callbacks.ps_attempts && !cli.callbacks.saves && !cli.hasPendingRadioSettings());
// Off retains accepted timing/level intent, checked persistence and safe-gap
// application even though this integrated driver cannot enable duty cycling.
cli.prefs.rx_powersaving_enabled = 1; cli.prefs.rx_ps_rx_us = 2000;
cli.prefs.rx_ps_sleep_us = 3000; cli.prefs.rx_ps_level = 8; cli.prefs.rx_ps_preamble = 32;
cli.callbacks.hardware_ps = RxPowerSavingConfig(true, 2000, 3000, 8, 32);
cli.callbacks.transmitting = true;
cli.command("radio.rxps off", reply);
assert(!strcmp(reply, "OK - off,2000,3000; saved; pending"));
assert(!cli.prefs.rx_powersaving_enabled && cli.prefs.rx_ps_level == 8
&& cli.prefs.rx_ps_preamble == 32 && cli.callbacks.saves == 1);
assert(!cli.callbacks.persisted.rx_powersaving_enabled && cli.callbacks.hardware_ps.enabled);
assert(cli.hasPendingRadioSettings() && cli._rx_settings_pending == CommonCLI::RxPowerSaving);
cli.callbacks.transmitting = false; cli.servicePendingRxSettings();
assert(!cli.hasPendingRadioSettings() && !cli.callbacks.hardware_ps.enabled);
assert(cli.callbacks.ps_attempts == 2 && cli.callbacks.saves == 1);
assert(!cli.callbacks.gain_attempts && !cli.callbacks.rf_attempts && !cli.callbacks.sf_attempts);
// A failed save restores the previous accepted values and pending deadline.
cli.callbacks.save_ok = false; cli.callbacks.transmitting = true;
cli._rx_settings_pending = CommonCLI::RxPowerSaving; cli._rx_settings_retry_at = 1234;
cli.command("radio.rxps off", reply);
assert(!strcmp(reply, "Error: RX save unconfirmed; reboot may differ"));
assert(cli._rx_settings_pending == CommonCLI::RxPowerSaving && cli._rx_settings_retry_at == 1234);
assert(cli.prefs.rx_ps_rx_us == 2000 && cli.prefs.rx_ps_sleep_us == 3000
&& cli.prefs.rx_ps_level == 8 && cli.prefs.rx_ps_preamble == 32);
cli.callbacks.transmitting = false; now_ms = 1234; cli.servicePendingRxSettings();
assert(!cli.hasPendingRadioSettings());
cli.callbacks.save_ok = true;
cli.callbacks.ps_results = {Result::FAILED, Result::APPLIED};
const unsigned saves = cli.callbacks.saves;
cli.command("radio.rxps off", reply);
assert(!strcmp(reply, "Error: RX setting rejected") && cli.callbacks.saves == saves);
assert(!cli.hasPendingRadioSettings() && !cli.callbacks.hardware_ps.enabled);
// The existing Off path still normalizes absent timing values locally.
cli.prefs.rx_ps_rx_us = cli.prefs.rx_ps_sleep_us = 0;
cli.command("radio.rxps off", reply);
assert(!strncmp(reply, "OK - off,", 9) && cli.prefs.rx_ps_rx_us == RX_POWERSAVING_DEFAULT_RX_US
&& cli.prefs.rx_ps_sleep_us == RX_POWERSAVING_DEFAULT_SLEEP_US);
}
'''
self.compile_run(code)
def test_other_platforms_and_external_radio_keep_supported_runtime_controls(self):
for defines in (("STM32_PLATFORM",), ("STM32WLxx",),
("STM32_PLATFORM", "STM32WLxx")):
with self.subTest(defines=defines):
code = fixture(defines).split("static bool ok(", 1)[0]
code += r'''
int main() {
CommonCLI cli; char reply[160];
cli.command("radio.rxgain on", reply);
assert(!strcmp(reply, "OK") && cli.callbacks.hardware_gain && cli.callbacks.saves == 1);
cli.command("radio.rxps 2000 3000", reply);
assert(!strncmp(reply, "OK - on,", 8) && cli.callbacks.hardware_ps.enabled && cli.callbacks.saves == 2);
cli.command("radio.rxps.rfrx_disabled on", reply);
assert(strstr(reply, "OK - radio.rxps.rfrx_disabled on") && cli.callbacks.hardware_rf);
}
'''
self.compile_run(code)
def test_stm32_fold_matches_the_integrated_driver_capabilities(self):
driver = (ROOT / "src/helpers/radiolib/CustomSTM32WLxWrapper.h").read_text(encoding="ascii")
self.assertNotRegex(driver, r"\b(?:supportsRxBoostedGainMode|supportsRxPowerSavingRfRxDisable|supportsRxPowerSaving)\s*\(")
self.assertNotRegex(driver, r"\bSupportsRxPowerSaving\b")
self.assertIn("#define USE_STM32WL", driver)
def test_pending_radio_settings_keep_all_infrastructure_roles_awake(self):
for role, name in (("simple_repeater", "MyMesh"), ("simple_room_server", "MyMesh"),
("simple_sensor", "SensorMesh")):
+9 -1
View File
@@ -145,8 +145,9 @@ struct MyMesh : Callbacks {
}
struct { bool pending = false; } deferred_cli_command;
struct {
bool pending_radio_settings = false;
bool hasActiveUserGpioTimer() const { return false; }
bool hasPendingTxPower() const { return false; }
bool hasPendingRadioSettings() const { return pending_radio_settings; }
} _cli;
struct {
bool isWatchdogObserving() const { return false; }
@@ -188,6 +189,13 @@ int main() {
{
MyMesh node;
assert(node.getPowerSaveSleepSeconds(30) == 30);
// Deferred radio settings own an awake loop even without an active UART.
node._cli.pending_radio_settings = true;
assert(node.pendingWorkReason() == mesh::powerdiag::mask(mesh::powerdiag::Reason::Scheduled));
assert(node.hasPendingWork() && node.getPowerSaveSleepMilliseconds(30000) == 0);
assert(node.getPowerSaveSleepSeconds(30) == 0);
node._cli.pending_radio_settings = false;
assert(!node.hasPendingWork() && node.getPowerSaveSleepSeconds(30) == 30);
char status[161]; std::memset(status, '!', sizeof(status));
node.formatRs232Status(status); assert(status[160] == '!');
assert(std::strstr(status, "saved=off running=off"));
+1 -1
View File
@@ -191,7 +191,7 @@ int main() {
common = (ROOT / 'src/helpers/CommonCLI.cpp').read_text()
setter = body(common, 'if (memcmp(config, "radio.rxps ", 11) == 0)')
if mutate:
guard = body(setter, 'if (!_callbacks->supportsRxPowerSaving()')
guard = body(setter, 'if (!powersaving_supported && parsed.mode')
setter = setter.replace(guard, '', 1)
code = HARNESS.replace('@SETTER@', setter)
code = code.replace('@GETTER@', body(common, 'if (strcmp(config, "radio.rxps.config") == 0)'))
+22
View File
@@ -419,14 +419,36 @@ int main() {
subprocess.run([str(work/'test')], check=True)
def test_roles_wire_filter_at_every_bridge_start_and_cross_hook(self):
# An on request during asynchronous MQTT teardown only retains start
# intent on the existing object. Its packet filter survives the stop;
# the fresh-start branches below must still configure admission first.
mqtt_begin = method((ROOT/'src/helpers/bridges/MQTTBridge.cpp').read_text(),
'void MQTTBridge::begin(')
stop_guard = mqtt_begin.index('if (_initialized || _lifecycle.isStopInProgress())')
self.assertLess(mqtt_begin.index('_start_requested = true;'), stop_guard)
self.assertIn('return;', method(mqtt_begin[stop_guard:],
'if (_initialized || _lifecycle.isStopInProgress())'))
self.assertNotIn('setPacketFilter(', mqtt_begin)
self.assertNotIn('_packet_filter', mqtt_begin)
deferred_start = re.compile(
r'if \((?:enable && )?(?P<pointer>mqtt_bridge|bridge) && '
r'(?P=pointer)->isStopping\(\)\) \{\s*'
r'(?P=pointer)->begin\(\);\s*'
r'return (?P=pointer)->isStartPending\(\);\s*\}')
for role in ['simple_repeater', 'simple_room_server']:
header = (ROOT/f'examples/{role}/MyMesh.h').read_text()
source = (ROOT/f'examples/{role}/MyMesh.cpp').read_text()
retained_starts = 0
for text in [header, source]:
for match in deferred_start.finditer(text):
self.assertIn('configureBridgeFilter(' + match['pointer'] + ');', header, role)
retained_starts += 1
text = deferred_start.sub('', text)
lines = text.splitlines()
for index, line in enumerate(lines):
if 'bridge->begin();' in line:
self.assertIn('configureBridgeFilter(', lines[index-1], role)
self.assertEqual(retained_starts, 2, role)
cross = method(header, 'bool allowRadioProfileCross(')
self.assertIn('allowTransportPacket', cross)
admission = method(source, 'bool MyMesh::allowTransportPacket(')
+3
View File
@@ -79,6 +79,9 @@ build_flags = ${rak3x72.build_flags}
-fno-reorder-functions
-fno-inline-small-functions
-fno-tree-dominator-opts ; preserve the 32 KiB filesystem with the stability guards
-fno-caller-saves
-fno-guess-branch-probability
--param=max-crossjump-edges=1000 ; share repeated CLI tails without cutting features or storage
-fipa-pta
-fmerge-all-constants
-fno-ipa-cp-clone
+2
View File
@@ -86,6 +86,8 @@ build_flags = ${Tiny_Relay.build_flags}
-fno-reorder-blocks
-fno-reorder-functions
-fno-caller-saves
-fno-guess-branch-probability
--param=max-crossjump-edges=1000 ; share repeated CLI tails without cutting features or storage
-fipa-pta
-fmerge-all-constants
-fno-ipa-cp-clone
+2
View File
@@ -105,6 +105,8 @@ build_flags = ${lora_e5.build_flags}
-fno-reorder-blocks
-fno-reorder-functions
-fno-caller-saves
-fno-guess-branch-probability
--param=max-crossjump-edges=1000 ; share repeated CLI tails without cutting features or storage
-fipa-pta
-fmerge-all-constants
-fno-ipa-cp-clone
+2
View File
@@ -113,6 +113,8 @@ build_flags = ${lora_e5_mini.build_flags}
-fno-inline-small-functions
-fno-tree-dominator-opts ; save 160 release bytes while preserving the 32 KiB filesystem
-fno-caller-saves ; avoid spill-code growth and retain the 32 KiB filesystem
-fno-guess-branch-probability
--param=max-crossjump-edges=1000 ; share repeated CLI tails without cutting features or storage
-fipa-pta
-fmerge-all-constants
-fno-ipa-cp-clone