diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index c2fce59c5..b065613fa 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -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(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; } diff --git a/src/helpers/radiolib/CustomSTM32WLxWrapper.h b/src/helpers/radiolib/CustomSTM32WLxWrapper.h index 91c048fe9..641eaf093 100644 --- a/src/helpers/radiolib/CustomSTM32WLxWrapper.h +++ b/src/helpers/radiolib/CustomSTM32WLxWrapper.h @@ -1,4 +1,8 @@ #pragma once + +#ifndef USE_STM32WL +#define USE_STM32WL +#endif #include "SX126xReceiveMode.h" #include "CustomSTM32WLx.h" diff --git a/test/fixtures/companion_ap_ownership.cpp b/test/fixtures/companion_ap_ownership.cpp index 20d757aee..e05b125a3 100644 --- a/test/fixtures/companion_ap_ownership.cpp +++ b/test/fixtures/companion_ap_ownership.cpp @@ -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; diff --git a/test/fixtures/companion_lost_reply.cpp b/test/fixtures/companion_lost_reply.cpp index 74421fc5c..47bcca407 100644 --- a/test/fixtures/companion_lost_reply.cpp +++ b/test/fixtures/companion_lost_reply.cpp @@ -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"); diff --git a/test/fixtures/sensor_tracker_client.cpp b/test/fixtures/sensor_tracker_client.cpp index 47fc03e1a..48e454486 100644 --- a/test/fixtures/sensor_tracker_client.cpp +++ b/test/fixtures/sensor_tracker_client.cpp @@ -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(); diff --git a/test/fixtures/sensor_tracker_client.h b/test/fixtures/sensor_tracker_client.h index 28d159d98..525b93286 100644 --- a/test/fixtures/sensor_tracker_client.h +++ b/test/fixtures/sensor_tracker_client.h @@ -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; diff --git a/test/test_beacon_legacy_lora_protocol.py b/test/test_beacon_legacy_lora_protocol.py index 7045a3d01..7327b538b 100644 --- a/test/test_beacon_legacy_lora_protocol.py +++ b/test/test_beacon_legacy_lora_protocol.py @@ -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()); diff --git a/test/test_companion_ap_ownership.py b/test/test_companion_ap_ownership.py index 4fa789dc6..c82c86add 100644 --- a/test/test_companion_ap_ownership.py +++ b/test/test_companion_ap_ownership.py @@ -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) diff --git a/test/test_companion_fleet_control.py b/test/test_companion_fleet_control.py index cac51540d..e6644acb2 100644 --- a/test/test_companion_fleet_control.py +++ b/test/test_companion_fleet_control.py @@ -27,6 +27,7 @@ HARNESS = r''' #include #include #include +#include #include "regulator_cli_stub.h" #include #include @@ -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]; diff --git a/test/test_companion_wifi_session.py b/test/test_companion_wifi_session.py index 729e8042d..d2634af50 100644 --- a/test/test_companion_wifi_session.py +++ b/test/test_companion_wifi_session.py @@ -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)); diff --git a/test/test_esp32_usb_serial_hygiene.py b/test/test_esp32_usb_serial_hygiene.py index 0a24ca7e5..b510f6e4a 100644 --- a/test/test_esp32_usb_serial_hygiene.py +++ b/test/test_esp32_usb_serial_hygiene.py @@ -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) diff --git a/test/test_espnow_radio_lifecycle.py b/test/test_espnow_radio_lifecycle.py index 663cd8078..1d3e8d89b 100644 --- a/test/test_espnow_radio_lifecycle.py +++ b/test/test_espnow_radio_lifecycle.py @@ -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) diff --git a/test/test_firmware_ram.py b/test/test_firmware_ram.py index 899f8efd3..d66956638 100644 --- a/test/test_firmware_ram.py +++ b/test/test_firmware_ram.py @@ -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; diff --git a/test/test_logging_sleep_contract.py b/test/test_logging_sleep_contract.py index aa74b005f..2a4a79d80 100644 --- a/test/test_logging_sleep_contract.py +++ b/test/test_logging_sleep_contract.py @@ -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"); diff --git a/test/test_meshtext_transport.py b/test/test_meshtext_transport.py index 625e535eb..b074e7427 100644 --- a/test/test_meshtext_transport.py +++ b/test/test_meshtext_transport.py @@ -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(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); diff --git a/test/test_power_saving_role_diagnostics.py b/test/test_power_saving_role_diagnostics.py index d6956b4e8..12285fa6c 100644 --- a/test/test_power_saving_role_diagnostics.py +++ b/test/test_power_saving_role_diagnostics.py @@ -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)); diff --git a/test/test_radio_rx_settings_transaction.py b/test/test_radio_rx_settings_transaction.py index f003ba5b5..67414d71d 100644 --- a/test/test_radio_rx_settings_transaction.py +++ b/test/test_radio_rx_settings_transaction.py @@ -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")): diff --git a/test/test_rs232_heartbeat_owner.py b/test/test_rs232_heartbeat_owner.py index 23cc2135f..8160d141d 100644 --- a/test/test_rs232_heartbeat_owner.py +++ b/test/test_rs232_heartbeat_owner.py @@ -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")); diff --git a/test/test_rxps_hardware_capability.py b/test/test_rxps_hardware_capability.py index ac446d9b7..e3ce86cec 100644 --- a/test/test_rxps_hardware_capability.py +++ b/test/test_rxps_hardware_capability.py @@ -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)')) diff --git a/test/test_transport_filter_wiring.py b/test/test_transport_filter_wiring.py index 7b56bc22d..477c43c51 100644 --- a/test/test_transport_filter_wiring.py +++ b/test/test_transport_filter_wiring.py @@ -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 && )?(?Pmqtt_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(') diff --git a/variants/rak3x72/platformio.ini b/variants/rak3x72/platformio.ini index 2b47492c9..e7302adc9 100644 --- a/variants/rak3x72/platformio.ini +++ b/variants/rak3x72/platformio.ini @@ -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 diff --git a/variants/tiny_relay/platformio.ini b/variants/tiny_relay/platformio.ini index 3e87ac693..f13376dae 100644 --- a/variants/tiny_relay/platformio.ini +++ b/variants/tiny_relay/platformio.ini @@ -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 diff --git a/variants/wio-e5-dev/platformio.ini b/variants/wio-e5-dev/platformio.ini index 378d25258..7f1d7cbed 100644 --- a/variants/wio-e5-dev/platformio.ini +++ b/variants/wio-e5-dev/platformio.ini @@ -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 diff --git a/variants/wio-e5-mini/platformio.ini b/variants/wio-e5-mini/platformio.ini index 40c0b4fb6..b3052c347 100644 --- a/variants/wio-e5-mini/platformio.ini +++ b/variants/wio-e5-mini/platformio.ini @@ -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