diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index c7cab5e5a..1b2db2a34 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -233,6 +233,9 @@ jobs: python3 -B test/test_management_feature_gate.py -v python3 -B test/test_carrier_wave.py -v python3 -B test/test_radio_profile_cli.py -v + python3 -B test/test_radio_switch_tuning_cli.py -v + python3 -B test/test_radio_switch_tuning_transaction.py -v + python3 -B test/test_radio_switch_driver_controls.py -v python3 -B test/test_ed25519_compact_scalar.py -v python3 -B test/test_ed25519_compact_field.py -v python3 -B test/test_ed25519_compact.py -v diff --git a/docs/cli_commands.md b/docs/cli_commands.md index e120f2ea5..1cb7df9fc 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1653,6 +1653,31 @@ capacity. Remote use follows the existing administrator-command permissions. ### Radio +#### Tune and save radio switching acceleration + +Use local USB or BLE to change these settings on supported SX1262 and LR1110 +radios. Supported acceleration features default to on, including on boards +awaiting hardware qualification. `get radio.fast.caps` lists the options the +current radio and SPI bus actually support. + +| Command | Purpose | +| --- | --- | +| `get radio.fast` | Read feature selections, actual SPI clock and any pending saved policy | +| `get radio.fast.caps` | List supported feature names and SPI MHz values | +| `get radio.fast.` | Read an individual feature | +| `set radio.fast. on\|off` | Save one feature selection: `warm`, `fs`, `packet`, `iq`, `modem`, `batch`, or `buffer` | +| `get radio.fast.spi` | Read the selected MHz and actual clock in Hz | +| `set radio.fast.spi 0\|2\|4\|8\|16` | Save a supported SPI MHz choice; `0` restores the board default | +| `set radio.fast off` | Disable supported acceleration features and use 2 MHz where available | +| `set radio.fast on` | Enable supported features and restore the board default SPI clock | + +The change applies safely without reboot and survives firmware updates. A busy +radio or failed apply/save leaves the previous policy in use. Remote LoRa +mutations and unsupported choices are rejected. Both radio profiles share the +settings. Changing them restarts the switching timing test; inspect +`get radio2.timing` after it completes. See [saved switching controls](radio_profiles.md#saved-switching-controls) +for what each feature does, hardware limits, and default/fallback behavior. + #### Select MCU and LoRa regulator modes ```text diff --git a/docs/radio_profiles.md b/docs/radio_profiles.md index 148edc646..5867f3722 100644 --- a/docs/radio_profiles.md +++ b/docs/radio_profiles.md @@ -1196,6 +1196,68 @@ This ends temporary second-profile activity, cancels its temporary schedules and restores saved `radio2`. If the saved second profile is off, this returns to one profile. Packets/retries bound to an expired or changed session are discarded. +## Saved switching controls + +Supported SX1262 and LR1110 radios expose separate saved controls for their +switching optimizations. Available acceleration features start enabled, +including on supported boards that have not had a hardware timing test. The +board's SPI default remains its selected default clock. Availability depends on +the radio, board bus, and compiled feature support; inspect the device first: + +```text +get radio.fast +get radio.fast.caps +``` + +`get radio.fast` reports the selected SPI MHz, its actual clock in Hz, and each +feature as `on`, `off`, or `na` when unsupported. `get radio.fast.caps` lists +supported feature names and accepted SPI values (`spi=na` without clock +control). Settings affect the one +physical radio and both profiles. They are shared by the same CLI roles as +`radio2`, including Full Companion, and survive reboot and firmware updates. + +| Control | Values | Effect when enabled | +| --- | --- | --- | +| `radio.fast.warm` | `on`, `off` | Retain owned continuous-RX state between profile changes | +| `radio.fast.fs` | `on`, `off` | Use the synthesizer/FS shortcut for an eligible retune | +| `radio.fast.packet` | `on`, `off` | Reuse acknowledged unchanged receive-packet parameters | +| `radio.fast.iq` | `on`, `off` | Reuse the unchanged SX1262 IQ workaround state | +| `radio.fast.modem` | `on`, `off` | Reuse confirmed unchanged modem state/queries | +| `radio.fast.batch` | `on`, `off` | Apply SF, bandwidth and coding rate together | +| `radio.fast.buffer` | `on`, `off` | Transfer SPI command bytes in a buffer where supported | +| `radio.fast.spi` | `0`, `2`, `4`, `8`, `16` where supported | Select the radio SPI clock in MHz; `0` restores the board default | + +For example, compare buffered transfers or a lower clock independently: + +```text +set radio.fast.buffer off +set radio.fast.spi 4 +get radio2.timing +set radio.fast.spi 0 +set radio.fast.buffer on +``` + +`0` preserves board-specific defaults, including clocks that are not whole MHz. +An unsupported feature or clock is rejected. A feature saved as `on` only runs +when its normal ownership and safety checks allow it; disabling `warm` also +prevents its dependent RX shortcuts from being used. Buffered transfer support +is separate from clock support. Some nRF52 buses support an 8 MHz byte path +while reporting `buffer=na`. + +For a broad comparison, `set radio.fast off` disables all supported acceleration +features and selects 2 MHz where available. `set radio.fast on` enables all +supported features and restores the board's default clock. These commands do +not change the chosen SPI controller or the pins and do not adjust CPU clock, +RX power saving, channel settings, or whether `radio2` is enabled. + +Changes require local USB or BLE access. A busy radio rejects a change without +saving it. An accepted change restarts receive safely and verifies it before +saving; a receive or storage failure restores the prior policy. Switching +measurements restart after a policy change, so check `get radio2.timing` again +after its timing test completes. Packet and error history remains available. +If a saved policy cannot be applied during boot, its application retries at +most once per second; the aggregate getter reports `pending saved policy`. + ## Power saving and diagnostics Dual mode suspends RX power saving and MCU idle sleep so receive visits can run @@ -1237,8 +1299,8 @@ calculation remain synchronized; frequency, preamble, packet guards, and RX restart are unchanged. A later [HIL-only screening](test-results/radio_profile_switch_validation.md#expanded-screening-redundant-rx-setup-and-spi-v5) tested redundant standby/IRQ/buffer/packet setup and SPI transfer changes. Its fastest candidate measured 0.535 ms mean on XIAO but 8.203 ms on the -Indicator, whose radio GPIOs go through an I2C expander. The validated production -implementation now opts XIAO S3 WIO and Indicator LoRa into buffered 8 MHz SPI +Indicator, whose radio GPIOs go through an I2C expander. That production stage +opted XIAO S3 WIO and Indicator LoRa into buffered 8 MHz SPI and a guarded fast-RX state machine. The `heltec_v4_kiss_modem` and `Station_G3_ESP32_kiss_modem` targets use the same guarded path only for KISS dual-profile scanning; their normal board roles keep their established @@ -1251,7 +1313,8 @@ retune. Preamble/packet parameters (including the IQ workaround), stale-IRQ clearing, RX commands and BUSY waits remain. TX, CAD, sleep, reset, RXPS and failed commands revoke reuse; failed RX startup rolls back the profile. Sleep entry uses standby and wake restores the existing TCXO voltage/delay. -Other variants do not opt in to faster SPI or the fast-RX state machine. +At that stage other variants retained their earlier SPI and RX policies. The +current saved controls and supported defaults are described above. Within a valid owned fast-RX retune, identical acknowledged SF/BW/CR/LDRO settings now reuse the previous modulation command: frequency-only hops omit `SetModulationParams` (0x8B). A changed effective LDRO value also forces a write, diff --git a/examples/companion_radio/CompanionHelp.inc b/examples/companion_radio/CompanionHelp.inc index e706dbf23..f891f291c 100644 --- a/examples/companion_radio/CompanionHelp.inc +++ b/examples/companion_radio/CompanionHelp.inc @@ -224,7 +224,11 @@ MESH_COMPANION_HELP_BEGIN " set espnow.channel <1-13>\r\n" MESH_COMPANION_HELP_END #endif -// Keep native USB producer writes within 256 bytes without changing help text. +// Keep native USB producer writes within 256 bytes. +MESH_COMPANION_HELP_BEGIN + " get radio.fast / get radio.fast.caps (supported saved controls)\r\n" + " set radio.fast / set radio.fast.spi <0|2|4|8|16>\r\n" +MESH_COMPANION_HELP_END MESH_COMPANION_HELP_BEGIN " get radio.rxps.config\r\n" " get/set radio.rxps \r\n" diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 72edc46a1..85c2ad208 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -3654,6 +3654,7 @@ void setup() { if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) { MESH_DEBUG_PRINTLN("Regulator selection could not be applied"); } + mesh::RadioProfileCLI::configureBootTuning(regulator_prefs_fs, radio_driver); #ifdef DISPLAY_CLASS diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 87aba7036..e2f88efcf 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -221,6 +221,7 @@ void setup() { if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) { MESH_DEBUG_PRINTLN("Regulator selection could not be applied"); } + mesh::RadioProfileCLI::configureBootTuning(regulator_prefs_fs, radio_driver); mesh::hilStartupTrace("fs_ready"); #ifdef DISPLAY_CLASS mesh::hilStartupTrace("display_begin"); diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 83a5ce226..b4809e5af 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -155,6 +155,7 @@ void setup() { if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) { MESH_DEBUG_PRINTLN("Regulator selection could not be applied"); } + mesh::RadioProfileCLI::configureBootTuning(regulator_prefs_fs, radio_driver); #ifdef DISPLAY_CLASS mesh::ui::loadDisplayPowerSettings(fs, false); #if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index 848137466..4740a238d 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -765,22 +765,10 @@ void setup() { external_watchdog.begin(); #endif - int radioinit_attempts = 0; - while (!radio_init()) { - ++radioinit_attempts; - MESH_DEBUG_PRINTLN("Radio init failed! (attempt %d)", radioinit_attempts); - if (radioinit_attempts >= 3) { - MESH_DEBUG_PRINTLN("Radio init failed 3x - rebooting"); - board.reboot(); - } - delay(500); - } - - fast_rng.begin(radio_driver.getRngSeed()); - -#if defined(NRF52_PLATFORM) - FILESYSTEM* primary_fs = &InternalFS; + FILESYSTEM* primary_fs; bool volatile_primary_fs = false; +#if defined(NRF52_PLATFORM) + primary_fs = &InternalFS; mesh::storage::RamFallbackFileSystem* ram_primary_fs = nullptr; const auto primary_fs_boot = mesh::storage::beginInternalPrimaryFilesystemSafely(InternalFS); @@ -794,16 +782,31 @@ void setup() { primary_fs = &ram_primary_fs->filesystem(); volatile_primary_fs = true; } - the_mesh.begin(*primary_fs, volatile_primary_fs); #elif defined(RP2040_PLATFORM) LittleFS.begin(); - the_mesh.begin(LittleFS); + primary_fs = &LittleFS; #elif defined(ESP32) SPIFFS.begin(true); - the_mesh.begin(SPIFFS); + primary_fs = &SPIFFS; #else #error "need to define filesystem" #endif + mesh::RadioProfileCLI::configureBootTuning( + volatile_primary_fs ? nullptr : primary_fs, radio_driver); + + int radioinit_attempts = 0; + while (!radio_init()) { + ++radioinit_attempts; + MESH_DEBUG_PRINTLN("Radio init failed! (attempt %d)", radioinit_attempts); + if (radioinit_attempts >= 3) { + MESH_DEBUG_PRINTLN("Radio init failed 3x - rebooting"); + board.reboot(); + } + delay(500); + } + + fast_rng.begin(radio_driver.getRngSeed()); + the_mesh.begin(*primary_fs, volatile_primary_fs); radio_driver.setParams(the_mesh.getFreqPref(), LORA_BW, LORA_SF, LORA_CR); radio_driver.setTxPower(the_mesh.getTxPowerPref()); diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 5a68639f9..257c6d974 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -176,6 +176,7 @@ void setup() { if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) { MESH_DEBUG_PRINTLN("Regulator selection could not be applied"); } + mesh::RadioProfileCLI::configureBootTuning(regulator_prefs_fs, radio_driver); #ifdef DISPLAY_CLASS mesh::ui::loadDisplayPowerSettings(fs, false); #if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT diff --git a/src/Dispatcher.h b/src/Dispatcher.h index c715fcd06..b81cc2352 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -98,6 +99,16 @@ public: virtual void begin() { } virtual RadioProfiles* profiles() { return nullptr; } virtual const RadioProfiles* profiles() const { return nullptr; } + virtual RadioSwitchCapabilities switchTuningCapabilities() const { return {}; } + virtual RadioSwitchTuning switchTuning() const { return {}; } + virtual uint32_t switchSpiHz() const { return 0; } + virtual bool configureBootSwitchTuning(const RadioSwitchTuning&) { return false; } + // Commit only after admission and verified RX restoration. The callback + // must not access the radio; a failed save restores the previous policy. + virtual RadioParamApplyResult trySetSwitchTuning(const RadioSwitchTuning&, + bool (*)(void*, const RadioSwitchTuning&) = nullptr, void* = nullptr) { + return RadioParamApplyResult::FAILED; + } virtual bool validateProfile(const RadioProfileParams&) const { return false; } virtual bool supportsMeshtext() const { return false; } virtual bool supportsEthernetTunnel() const { return false; } diff --git a/src/helpers/RadioProfileCLI.cpp b/src/helpers/RadioProfileCLI.cpp index bd29676f9..87adbbc2e 100644 --- a/src/helpers/RadioProfileCLI.cpp +++ b/src/helpers/RadioProfileCLI.cpp @@ -36,6 +36,9 @@ const char* const TempPath = "/radio_profiles.tmp"; // Never considered a boot-time committed image. Losing power before the // acknowledgement leaves the previous saved radio untouched. const char* const ReplyPath = "/radio_profiles.reply"; +const char* const FastPath = "/radio_fast"; +const char* const FastBackup = "/radio_fast.bak"; +const char* const FastTemp = "/radio_fast.tmp"; uint32_t checksum(const uint8_t* p, size_t n) { return storage::updateCRC32(0xffffffffU, p, n); } @@ -198,21 +201,176 @@ bool RadioProfileCLI::prepareSavedImage(const char* path, const RadioProfileConf } bool RadioProfileCLI::commitSavedImage(const char* path) { - if (!fs_ || hold_) return false; - if (fs_->exists(ImagePath)) { - if (fs_->exists(BackupPath) && !fs_->remove(BackupPath)) return false; - if (!fs_->rename(ImagePath, BackupPath)) return false; + return commitImage(path, ImagePath, BackupPath, hold_); +} + +bool RadioProfileCLI::commitImage(const char* path, const char* active, + const char* backup, bool& hold, bool preserve_backup) { + if (!fs_ || hold) return false; + if (fs_->exists(active)) { + if (preserve_backup) { + if (!fs_->remove(active)) return false; + } else { + if (fs_->exists(backup) && !fs_->remove(backup)) return false; + if (!fs_->rename(active, backup)) return false; + } } - if (!fs_->rename(path, ImagePath)) { - if (fs_->exists(BackupPath) && !fs_->rename(BackupPath, ImagePath)) hold_ = true; + if (!fs_->rename(path, active)) { + if (fs_->exists(backup) && !fs_->rename(backup, active)) hold = true; return false; } - if (fs_->exists(BackupPath)) fs_->remove(BackupPath); + if (fs_->exists(backup)) fs_->remove(backup); + return true; +} + +bool RadioProfileCLI::validFastImage(const uint8_t* bytes, RadioSwitchTuning& value) const { + if (bytes[2] != 1) return false; + for (size_t n = 5; n < ImageSize - 4; ++n) if (bytes[n]) return false; + value.flags = bytes[3]; value.spi_mhz = bytes[4]; + return radio_ && validRadioSwitchTuning(value, radio_->switchTuningCapabilities()); +} + +void RadioProfileCLI::configureBootTuning(FILESYSTEM* fs, Radio& radio) { + RadioProfileCLI loader; + loader.fs_ = fs; loader.radio_ = &radio; + loader.loadFastSettings(true); +} + +__attribute__((noinline)) void RadioProfileCLI::loadFastSettings(bool boot) { + fast_pending_ = fast_hold_ = false; + if (!fs_ || !radio_) return; + uint8_t bytes[ImageSize]; + const auto primary = readImage(FastPath, bytes, sizeof(bytes)); + bool loaded = primary == ImageReadResult::Valid && validFastImage(bytes, fast_saved_); + if (!loaded) { + const auto backup = readImage(FastBackup, bytes, sizeof(bytes)); + loaded = backup == ImageReadResult::Valid && validFastImage(bytes, fast_saved_); + fast_hold_ = primary == ImageReadResult::Unreadable || backup == ImageReadResult::Unreadable; + if (loaded && !fast_hold_) { + // Publish the known-good backup before accepting further writes. Never + // move a corrupt/unsupported primary over this only valid record. + fast_hold_ = (fs_->exists(FastPath) && !fs_->remove(FastPath)) + || !fs_->rename(FastBackup, FastPath); + } + } + if (loaded) { + fast_pending_ = boot ? !radio_->configureBootSwitchTuning(fast_saved_) + : radio_->trySetSwitchTuning(fast_saved_) != RadioParamApplyResult::APPLIED; + fast_retry_ms_ = millis() + 1000; + } +} + +bool RadioProfileCLI::saveFastSettings(const RadioSwitchTuning& value) { + if (!fs_ || fast_hold_) return false; + // A read error must not be interpreted as a missing/corrupt saved policy. + uint8_t previous[ImageSize]; + RadioSwitchTuning decoded; + const auto read = readImage(FastPath, previous, sizeof(previous)); + if (read == ImageReadResult::Unreadable) return false; + const bool preserve = read != ImageReadResult::Valid || !validFastImage(previous, decoded); + uint8_t image[ImageSize] = {'R', '2', 1, value.flags, value.spi_mhz}; + const uint32_t crc = checksum(image, ImageSize - 4); + memcpy(image + ImageSize - 4, &crc, 4); + if (!writeImage(FastTemp, image, sizeof(image)) + || !commitImage(FastTemp, FastPath, FastBackup, fast_hold_, preserve)) return false; + fast_saved_ = value; + fast_pending_ = false; + return true; +} + +bool RadioProfileCLI::commitFastSettings(void* context, const RadioSwitchTuning& value) { + return static_cast(context)->saveFastSettings(value); +} + +__attribute__((noinline)) bool RadioProfileCLI::handleFastSettings(const char* key, const char* args, int verb, + bool remote, char* reply, size_t capacity) { + if (strcmp(key, "radio.fast") && strncmp(key, "radio.fast.", 11)) return false; + if (!radio_) { cli::formatError(reply, capacity, "radio tuning unsupported"); return true; } + const auto caps = radio_->switchTuningCapabilities(); + const auto current = radio_->switchTuning(); + const char* suffix = key + (key[10] ? 11 : 10); + static const char names[] = "warm\0fs\0packet\0iq\0modem\0batch\0buffer"; + const char* name = names; + unsigned feature = 0; + while (feature < 7 && strcmp(suffix, name)) { name += strlen(name) + 1; ++feature; } + const bool master = !*suffix, spi = !strcmp(suffix, "spi"); + const bool capabilities = !strcmp(suffix, "caps"); + if (!master && !spi && !capabilities && feature == 7) { + cli::formatError(reply, capacity, "unknown radio.fast control"); return true; + } + const uint8_t bit = feature < 7 ? 1U << feature : 0; + if ((spi && !caps.spi_clocks) || (bit && !(caps.flags & bit)) + || (master && !caps.flags && !caps.spi_clocks)) { + cli::formatError(reply, capacity, "radio tuning unsupported"); return true; + } + if (verb == 0 && !*args) { + if (capabilities) { + snprintf(reply, capacity, "> features:"); + name = names; + for (unsigned n = 0; n < 7; ++n) { + if (caps.flags & (1U << n)) appendReply(reply, capacity, " %s", name); + name += strlen(name) + 1; + } + appendReply(reply, capacity, "; spi=%s", caps.spi_clocks ? "0" : "na"); + for (unsigned n = 1; n <= 4; ++n) + if (caps.spi_clocks & (1U << n)) appendReply(reply, capacity, ",%u", 1U << n); + } else if (master) { + if (caps.spi_clocks) { + snprintf(reply, capacity, "> spi=%u (%luHz)", current.spi_mhz, + (unsigned long)radio_->switchSpiHz()); + } else snprintf(reply, capacity, "> spi=na"); + if (fast_pending_) appendReply(reply, capacity, " pending saved policy"); + name = names; + for (unsigned n = 0; n < 7; ++n) { + appendReply(reply, capacity, " %s=%s", name, + !(caps.flags & (1U << n)) ? "na" : current.flags & (1U << n) ? "on" : "off"); + name += strlen(name) + 1; + } + } else if (spi) snprintf(reply, capacity, "> %u (%luHz; 0=board default)", + current.spi_mhz, (unsigned long)radio_->switchSpiHz()); + else snprintf(reply, capacity, "> %s", current.flags & bit ? "on" : "off"); + return true; + } + if (capabilities) { + cli::formatError(reply, capacity, "radio.fast.caps is read-only"); return true; + } + if (verb != 1 || !*args) { + cli::formatError(reply, capacity, "use get/set radio.fast[.control] value"); return true; + } + if (remote || remote_command_) { + cli::formatError(reply, capacity, "radio.fast changes require local USB or BLE"); return true; + } + if (hasReplyMutation() || fast_hold_) { + cli::formatError(reply, capacity, "radio settings held; retry after recovery"); return true; + } + RadioSwitchTuning next = current; + if (spi) { + // Keep parsing strict without five separate whole-string comparisons. + if (!args[1] && strchr("0248", args[0])) next.spi_mhz = args[0] - '0'; + else if (!strcmp(args, "16")) next.spi_mhz = 16; + else { cli::formatError(reply, capacity, "SPI MHz: 0,2,4,8,16 where supported"); return true; } + } else { + const bool on = !strcmp(args, "on"); + if (!on && strcmp(args, "off")) { cli::formatError(reply, capacity, "use on or off"); return true; } + if (master) { + next.flags = on ? caps.flags : 0; + next.spi_mhz = !on && (caps.spi_clocks & 2) ? 2 : 0; + } else if (on) next.flags |= bit; + else next.flags &= ~bit; + } + if (!validRadioSwitchTuning(next, caps)) { + cli::formatError(reply, capacity, "radio tuning unsupported"); return true; + } + const auto result = radio_->trySetSwitchTuning(next, commitFastSettings, this); + if (result == RadioParamApplyResult::APPLIED) snprintf(reply, capacity, "OK - %s=%s (saved)", key, args); + else cli::formatError(reply, capacity, result == RadioParamApplyResult::BUSY + ? "radio busy; retry" : "radio tuning could not be applied/saved"); return true; } void RadioProfileCLI::begin(FILESYSTEM* fs, Radio* radio, RTCClock* rtc, bool infrastructure_replies) { fs_ = fs; radio_ = radio; rtc_ = rtc; last_ms_ = millis(); + loadFastSettings(); hold_ = false; cross_ = RadioCrossMode::Off; recoverable_corrupt_store_ = false; @@ -575,8 +733,15 @@ bool RadioProfileCLI::applyReplyMutation() { } void RadioProfileCLI::loop() { + const uint32_t now = millis(); + if (fast_pending_ && radio_ && (int32_t)(now - fast_retry_ms_) >= 0) { + // A saved setting may fail RX verification as well as be temporarily busy. + // Avoid repeatedly applying and rolling it back at the main-loop rate. + fast_pending_ = radio_->trySetSwitchTuning(fast_saved_) != RadioParamApplyResult::APPLIED; + fast_retry_ms_ = now + 1000; + } if (!radio_ || !radio_->profiles()) return; - const uint32_t now = millis(), elapsed = now - last_ms_; + const uint32_t elapsed = now - last_ms_; last_ms_ = now; remote_commit_retry_ms_ = elapsed >= remote_commit_retry_ms_ ? 0 : remote_commit_retry_ms_ - elapsed; if (remote_delivered_ && !remote_commit_retry_ms_ && !applyReplyMutation()) { @@ -784,6 +949,7 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity, memcpy(key, text, n); key[n] = 0; const char* args = text + n; while (*args == ' ' || *args == '\t') ++args; + if (handleFastSettings(key, args, verb, remote_origin, reply, capacity)) return true; enum { Base, Temporary, Scheduled, ScheduledTemporary, Crossing, Replies, Status, Scan, Timing, TimingAlias, KeyCount }; static const char keys[] = "radio2\0tempradio2\0radioat2\0tempradioat2\0radio2.cross\0" diff --git a/src/helpers/RadioProfileCLI.h b/src/helpers/RadioProfileCLI.h index 8da7ec9ef..f84bbba83 100644 --- a/src/helpers/RadioProfileCLI.h +++ b/src/helpers/RadioProfileCLI.h @@ -56,6 +56,9 @@ class RadioProfileCLI { // readable backup. Read/I/O failures and an intact backup remain read-only. bool recoverable_corrupt_store_ = false; bool publish_pending_ = false; + RadioSwitchTuning fast_saved_; + uint32_t fast_retry_ms_ = 0; + bool fast_pending_ = false, fast_hold_ = false; enum class ImageReadResult : uint8_t { Missing, Valid, Invalid, Unreadable }; enum class RemoteMutation : uint8_t { None, Saved, Temporary, Off, TempOff, DeleteTemp @@ -83,6 +86,14 @@ class RadioProfileCLI { bool prepareSavedImage(const char* path, const RadioProfileConfig& config, uint16_t preamble, RadioCrossMode cross, int8_t manual_secondary = -1); bool commitSavedImage(const char* path); + bool commitImage(const char* staged, const char* active, const char* backup, + bool& hold, bool preserve_backup = false); + bool validFastImage(const uint8_t* bytes, RadioSwitchTuning& value) const; + void loadFastSettings(bool boot = false); + bool saveFastSettings(const RadioSwitchTuning& value); + static bool commitFastSettings(void* context, const RadioSwitchTuning& value); + bool handleFastSettings(const char* key, const char* args, int verb, + bool remote, char* reply, size_t capacity); bool discardCorruptSavedImagesForWrite(); bool applyReplyMutation(); ImageReadResult readImage(const char* path, uint8_t* bytes, size_t size); @@ -96,6 +107,8 @@ class RadioProfileCLI { const Schedule* schedule = nullptr) const; void appendSecondaryWarnings(char* reply, size_t capacity, const RadioProfiles& preview) const; public: + // Call after the settings filesystem mounts and before radio_init(). + static void configureBootTuning(FILESYSTEM* fs, Radio& radio); void begin(FILESYSTEM* fs, Radio* radio, RTCClock* rtc, bool infrastructure_replies = false); void loop(); bool handle(const char* command, char* reply, size_t capacity = 160, bool remote_origin = false); diff --git a/src/helpers/RadioSwitchTuning.h b/src/helpers/RadioSwitchTuning.h new file mode 100644 index 000000000..566c2aace --- /dev/null +++ b/src/helpers/RadioSwitchTuning.h @@ -0,0 +1,36 @@ +#pragma once +#include + +namespace mesh { + +// Saved policy for one physical radio, shared by both RF profiles. SPI MHz 0 +// retains the board's default clock (which need not be an integer MHz). +struct RadioSwitchTuning { + enum Feature : uint8_t { + Warm = 1, Fs = 2, Packet = 4, Iq = 8, Modem = 16, + Batch = 32, Buffer = 64 + }; + uint8_t flags = 0; + uint8_t spi_mhz = 0; + bool operator==(const RadioSwitchTuning& other) const { + return flags == other.flags && spi_mhz == other.spi_mhz; + } + bool operator!=(const RadioSwitchTuning& other) const { return !(*this == other); } +}; + +struct RadioSwitchCapabilities { + uint8_t flags = 0; + // Bit N permits 2^N MHz; bit 0 (1 MHz) is deliberately unused. + uint8_t spi_clocks = 0; +}; + +inline bool validRadioSwitchTuning(const RadioSwitchTuning& value, + const RadioSwitchCapabilities& caps) { + if (value.flags & ~caps.flags) return false; + if (!value.spi_mhz) return true; + for (uint8_t n = 1; n <= 4; ++n) + if (value.spi_mhz == (1U << n)) return caps.spi_clocks & (1U << n); + return false; +} + +} // namespace mesh diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index d13b1ed1c..fe9219759 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -6,6 +6,7 @@ #include "RXPowerSaving.h" #include "LR1110RxRecovery.h" #include "LR1110ProfileSwitchState.h" +#include #include "LR11x0MorseRfSwitch.h" #include "../MorseRx.h" @@ -29,6 +30,7 @@ class CustomLR1110 : public LR1110 { bool _headerSeen = false; bool _rx_boosted = false; LR1110ProfileSwitchState _profileSwitch{MC_LR1110_FAST_PROFILE_SWITCH != 0}; + uint8_t _profileSwitchFeatures = profileSwitchCapabilities(); bool _profileStandbyWarm = false; bool _profilePacketDirty = false; uint32_t _profileFastResumes = 0; @@ -74,11 +76,23 @@ class CustomLR1110 : public LR1110 { return _regulator.finishInitialization(status); } - void setProfileSwitchOptimization(bool enabled) { - _profileSwitch.enabled = enabled; + uint8_t profileSwitchCapabilities() const { + return MC_LR1110_FAST_PROFILE_SWITCH + ? mesh::RadioSwitchTuning::Warm | mesh::RadioSwitchTuning::Fs + | mesh::RadioSwitchTuning::Packet | mesh::RadioSwitchTuning::Modem + | mesh::RadioSwitchTuning::Batch + : 0; + } + uint8_t getProfileSwitchFeatures() const { return _profileSwitchFeatures; } + void setProfileSwitchFeatures(uint8_t features) { + _profileSwitchFeatures = features & profileSwitchCapabilities(); + _profileSwitch.enabled = (_profileSwitchFeatures & mesh::RadioSwitchTuning::Warm) != 0; _profileSwitch.invalidate(); _profilePacketDirty = false; } + void setProfileSwitchOptimization(bool enabled) { + setProfileSwitchFeatures(enabled ? profileSwitchCapabilities() : 0); + } void setProfileStandbyWarm(bool enabled) { _profileStandbyWarm = enabled; if (!enabled) _profileSwitch.invalidate(); @@ -119,8 +133,8 @@ class CustomLR1110 : public LR1110 { const bool warm = _profileSwitch.active && _profileSwitch.rxValid; if (!warm) _profileSwitch.invalidate(); // RadioLib 7.7.1 defines STANDBY_XOSC as 0, the same as STANDBY_RC. - // The LR1110 command's XOSC selector is 1 (Semtech UM ยง2.1.1). - bool useFs = warm; + // The LR1110 command's XOSC selector is 1 (Semtech UM section 2.1.1). + bool useFs = warm && (_profileSwitchFeatures & mesh::RadioSwitchTuning::Fs); #ifdef MC_LR1110_HIL useFs = useFs && hilUseFsOnRetune; #endif @@ -227,7 +241,8 @@ class CustomLR1110 : public LR1110 { else if (fabsf(bw - 250.0f) < 0.01f) bwCode = RADIOLIB_LR11X0_LORA_BW_250_0; else if (fabsf(bw - 500.0f) < 0.01f) bwCode = RADIOLIB_LR11X0_LORA_BW_500_0; else return RADIOLIB_ERR_INVALID_BANDWIDTH; - if (!_profileSwitch.canResumeFast()) { + if (!(_profileSwitchFeatures & mesh::RadioSwitchTuning::Modem) + || !_profileSwitch.canResumeFast()) { uint8_t type = RADIOLIB_LR11X0_PACKET_TYPE_NONE; const int16_t rc = getPacketType(&type); if (rc != RADIOLIB_ERR_NONE) return rc; @@ -237,7 +252,8 @@ class CustomLR1110 : public LR1110 { ? (float(uint32_t(1) << sf) / bw >= 16.0f ? RADIOLIB_LR11X0_LORA_LDRO_ENABLED : RADIOLIB_LR11X0_LORA_LDRO_DISABLED) : ldrOptimize; - const bool unchanged = _profileSwitch.matchesModulation(sf, bwCode, cr - 4, effectiveLdro); + const bool unchanged = (_profileSwitchFeatures & mesh::RadioSwitchTuning::Batch) + && _profileSwitch.matchesModulation(sf, bwCode, cr - 4, effectiveLdro); const auto oldSf = spreadingFactor, oldBw = bandwidth, oldCr = codingRate, oldLdro = ldrOptimize; const auto oldBwKhz = bandwidthKhz; spreadingFactor = sf; @@ -467,7 +483,8 @@ class CustomLR1110 : public LR1110 { fastClearCycles += DWT->CYCCNT - segment; segment = DWT->CYCCNT; #endif - if (rc == RADIOLIB_ERR_NONE && _profilePacketDirty) { + if (rc == RADIOLIB_ERR_NONE && (_profilePacketDirty + || !(_profileSwitchFeatures & mesh::RadioSwitchTuning::Packet))) { rc = setPacketParamsLoRa(preambleLengthLoRa, headerType, RADIOLIB_LR11X0_MAX_PACKET_LENGTH, crcTypeLoRa, invertIQEnabled); } diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 74dad28aa..c77481206 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -35,6 +35,15 @@ public: void setDeepInitCallback(DeepInitCallback callback) { _deep_init = callback; } protected: + uint8_t getDriverSwitchFeatures() const override { + return static_cast(_radio)->getProfileSwitchFeatures(); + } + uint8_t driverSwitchCapabilities() const override { + return static_cast(_radio)->profileSwitchCapabilities(); + } + void setDriverSwitchFeatures(uint8_t features) override { + static_cast(_radio)->setProfileSwitchFeatures(features); + } #if MESH_ENABLE_MESHTEXT_WIRE bool applyProtocol(mesh::RadioProfileProtocol protocol) override { if (protocol != mesh::RadioProfileProtocol::MeshCore @@ -97,19 +106,20 @@ public: } protected: + bool applyProfileModulation(float bw, uint8_t sf, uint8_t cr) { + auto* radio = static_cast(_radio); + if (getDriverSwitchFeatures() & mesh::RadioSwitchTuning::Batch) + return radio->setLoRaModulationParams(bw, sf, cr) == RADIOLIB_ERR_NONE; + return radio->setSpreadingFactor(sf) == RADIOLIB_ERR_NONE + && radio->setBandwidth(bw) == RADIOLIB_ERR_NONE + && radio->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool applyParams(float freq, float bw, uint8_t sf, uint8_t cr) override { if (static_cast(_radio)->profileSwitchFailed()) return false; -#if MC_LR1110_FAST_PROFILE_SWITCH bool success = ((CustomLR1110 *)_radio)->setFrequency(freq) == RADIOLIB_ERR_NONE - && ((CustomLR1110 *)_radio)->setLoRaModulationParams(bw, sf, cr) == RADIOLIB_ERR_NONE + && applyProfileModulation(bw, sf, cr) && updatePreamble(sf, bw); -#else - bool success = ((CustomLR1110 *)_radio)->setFrequency(freq) == RADIOLIB_ERR_NONE - && ((CustomLR1110 *)_radio)->setSpreadingFactor(sf) == RADIOLIB_ERR_NONE - && ((CustomLR1110 *)_radio)->setBandwidth(bw) == RADIOLIB_ERR_NONE - && ((CustomLR1110 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE - && updatePreamble(sf, bw); -#endif if (!success) return false; PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForParams(sf, bw)); diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 9be20e480..cceaeadea 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -6,6 +6,7 @@ #include "MeshCore.h" #include "RXPowerSaving.h" #include "SX1262ProfileSwitchState.h" +#include #include "SX126xMorseRssi.h" #include "ESP32RadioTrace.h" #if defined(STATION_G3_ESP32) @@ -67,6 +68,7 @@ class CustomSX1262 : public SX1262 { bool _rx_ps_rf_rx_disabled = false; uint32_t _rxDutyCycleTransitionUs = 1000; SX1262ProfileSwitchState _profileSwitch{MC_SX1262_FAST_PROFILE_SWITCH != 0}; + uint8_t _profileSwitchFeatures = defaultProfileSwitchFeatures(); uint32_t _profileFastResumes = 0; bool _coldStandby = true; bool _tcxoWakePending = false; @@ -113,10 +115,53 @@ class CustomSX1262 : public SX1262 { return status == RADIOLIB_ERR_NONE ? raw * -0.5f : NAN; } - void setProfileSwitchOptimization(bool enabled) { - _profileSwitch.enabled = enabled; + uint8_t defaultProfileSwitchFeatures() const { + return mesh::RadioSwitchTuning::Batch + | (MC_SX1262_FAST_PROFILE_SWITCH ? mesh::RadioSwitchTuning::Warm : 0) + | (MC_SX1262_FAST_FS ? mesh::RadioSwitchTuning::Fs : 0) + | (MC_SX1262_FAST_PACKET_CACHE ? mesh::RadioSwitchTuning::Packet : 0) + | (MC_SX1262_FAST_IQ_CACHE ? mesh::RadioSwitchTuning::Iq : 0) + | (MC_SX1262_FAST_MODEM_CACHE ? mesh::RadioSwitchTuning::Modem : 0); + } + uint8_t profileSwitchCapabilities() const { + uint8_t features = defaultProfileSwitchFeatures(); +#ifdef MC_SX1262_HIL + // Lab images retain their explicit per-shortcut experiments even when a + // production macro defaults off. Ordinary builds never gain these caps. + if (MC_SX1262_FAST_PROFILE_SWITCH) + features |= mesh::RadioSwitchTuning::Fs | mesh::RadioSwitchTuning::Packet + | mesh::RadioSwitchTuning::Iq | mesh::RadioSwitchTuning::Modem; +#endif + return features; + } + uint8_t getProfileSwitchFeatures() const { + uint8_t features = _profileSwitchFeatures; +#ifdef MC_SX1262_HIL + // Legacy lab commands directly select these booleans. Reflect them in + // the same getter used by the production feature gates and new CLI. + features &= ~(mesh::RadioSwitchTuning::Fs | mesh::RadioSwitchTuning::Packet + | mesh::RadioSwitchTuning::Iq | mesh::RadioSwitchTuning::Modem); + if (hilUseFsOnRetune) features |= mesh::RadioSwitchTuning::Fs; + if (hilSkipUnchangedPacket) features |= mesh::RadioSwitchTuning::Packet; + if (hilSkipUnchangedIq) features |= mesh::RadioSwitchTuning::Iq; + if (hilSkipConfirmedModemQuery) features |= mesh::RadioSwitchTuning::Modem; +#endif + return features & profileSwitchCapabilities(); + } + void setProfileSwitchFeatures(uint8_t features) { + _profileSwitchFeatures = features & profileSwitchCapabilities(); +#ifdef MC_SX1262_HIL + hilUseFsOnRetune = (_profileSwitchFeatures & mesh::RadioSwitchTuning::Fs) != 0; + hilSkipUnchangedPacket = (_profileSwitchFeatures & mesh::RadioSwitchTuning::Packet) != 0; + hilSkipUnchangedIq = (_profileSwitchFeatures & mesh::RadioSwitchTuning::Iq) != 0; + hilSkipConfirmedModemQuery = (_profileSwitchFeatures & mesh::RadioSwitchTuning::Modem) != 0; +#endif + _profileSwitch.enabled = (_profileSwitchFeatures & mesh::RadioSwitchTuning::Warm) != 0; _profileSwitch.invalidate(); } + void setProfileSwitchOptimization(bool enabled) { + setProfileSwitchFeatures(enabled ? profileSwitchCapabilities() : 0); + } void beginProfileSwitch(bool continuousRx) { _profileSwitch.begin(continuousRx, standbyXOSC); } void endProfileSwitch(bool success) { _profileSwitch.end(success); } bool profileSwitchFailed() const { return _profileSwitch.failed(); } @@ -160,9 +205,9 @@ class CustomSX1262 : public SX1262 { const bool alreadyAwake = !_coldStandby && _profileSwitch.active && _profileSwitch.rxValid; if (!alreadyAwake) _profileSwitch.invalidate(); int16_t rc; - bool useFs = MC_SX1262_FAST_FS != 0; + bool useFs = (getProfileSwitchFeatures() & mesh::RadioSwitchTuning::Fs) != 0; #ifdef MC_SX1262_HIL - useFs = hilUseFsOnRetune; + useFs = useFs && hilUseFsOnRetune; #endif if (alreadyAwake && useFs) { getMod()->setRfSwitchState(Module::MODE_IDLE); @@ -327,9 +372,9 @@ class CustomSX1262 : public SX1262 { } bool queryModem = true; #if MC_SX1262_FAST_MODEM_CACHE || defined(MC_SX1262_HIL) - bool reuseModem = MC_SX1262_FAST_MODEM_CACHE != 0; + bool reuseModem = (getProfileSwitchFeatures() & mesh::RadioSwitchTuning::Modem) != 0; #ifdef MC_SX1262_HIL - reuseModem = hilSkipConfirmedModemQuery; + reuseModem = reuseModem && hilSkipConfirmedModemQuery; #endif // rxValid is established only after full RX setup confirms LoRa, or by // resuming that same owned context. A successful owned standby, warm @@ -353,7 +398,8 @@ class CustomSX1262 : public SX1262 { const uint8_t effectiveLdro = ldroAuto ? ((float(uint32_t(1) << sf) / bw) >= 16.0f ? 1 : 0) : ldrOptimize; - const bool unchanged = _profileSwitch.matchesModulation( + const bool unchanged = (_profileSwitchFeatures & mesh::RadioSwitchTuning::Batch) + && _profileSwitch.matchesModulation( sf, codes[index], cr - 4, effectiveLdro); const auto oldSf = spreadingFactor; const auto oldBw = bandwidth; @@ -744,17 +790,17 @@ class CustomSX1262 : public SX1262 { // Existing continuous RX already established buffer bases and the // MeshCore IRQ map (including preamble detection). No TX/CAD/sleep/ // reset/staging may intervene without revoking that context. - bool skipPacket = MC_SX1262_FAST_PACKET_CACHE != 0; + bool skipPacket = (getProfileSwitchFeatures() & mesh::RadioSwitchTuning::Packet) != 0; #ifdef MC_SX1262_HIL - skipPacket = hilSkipUnchangedPacket; + skipPacket = skipPacket && hilSkipUnchangedPacket; #endif skipPacket = skipPacket && _profileSwitch.matchesPacket( preambleLengthLoRa, crcTypeLoRa, implicitLen, headerType, invertIQEnabled); #if MC_SX1262_FAST_IQ_CACHE || defined(MC_SX1262_HIL) - bool reuseIq = MC_SX1262_FAST_IQ_CACHE != 0; + bool reuseIq = (getProfileSwitchFeatures() & mesh::RadioSwitchTuning::Iq) != 0; #ifdef MC_SX1262_HIL - reuseIq = hilSkipUnchangedIq; + reuseIq = reuseIq && hilSkipUnchangedIq; #endif // Capture this capability BEFORE revoking rxValid for the command // sequence. Packet/IQ setters, protocol changes, sleep, reset, TX/CAD, diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 9dba1d1b1..94b6f82a1 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -17,6 +17,15 @@ class CustomSX1262Wrapper : public RadioLibWrapper { bool _saved_standby_xosc = false; protected: + uint8_t getDriverSwitchFeatures() const override { + return static_cast(_radio)->getProfileSwitchFeatures(); + } + uint8_t driverSwitchCapabilities() const override { + return static_cast(_radio)->profileSwitchCapabilities(); + } + void setDriverSwitchFeatures(uint8_t features) override { + static_cast(_radio)->setProfileSwitchFeatures(features); + } int8_t readReceiveMode() override { return sx126xReceiveMode((SX126x*)_radio); } void beginProfileRetune(bool continuousRx) override { static_cast(_radio)->beginProfileSwitch(continuousRx); @@ -125,13 +134,22 @@ protected: #endif } + bool applyProfileModulation(float bw, uint8_t sf, uint8_t cr) { + auto* radio = static_cast(_radio); + if (getDriverSwitchFeatures() & mesh::RadioSwitchTuning::Batch) + return radio->setLoRaModulationParams(bw, sf, cr) == RADIOLIB_ERR_NONE; + return radio->setSpreadingFactor(sf) == RADIOLIB_ERR_NONE + && radio->setBandwidth(bw) == RADIOLIB_ERR_NONE + && radio->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool applyParams(float freq, float bw, uint8_t sf, uint8_t cr) override { if (static_cast(_radio)->profileSwitchFailed()) return false; #if MESH_HIL_PROFILE_TIMING bool success = ((CustomSX1262 *)_radio)->setFrequency(freq) == RADIOLIB_ERR_NONE; mesh::profiletiming::mark(mesh::profiletiming::Frequency, mesh::profiletiming::readClock()); if (success) { - success = ((CustomSX1262 *)_radio)->setLoRaModulationParams(bw, sf, cr) == RADIOLIB_ERR_NONE; + success = applyProfileModulation(bw, sf, cr); mesh::profiletiming::mark(mesh::profiletiming::Modulation, mesh::profiletiming::readClock()); } if (success) { @@ -140,7 +158,7 @@ protected: } #else bool success = ((CustomSX1262 *)_radio)->setFrequency(freq) == RADIOLIB_ERR_NONE - && ((CustomSX1262 *)_radio)->setLoRaModulationParams(bw, sf, cr) == RADIOLIB_ERR_NONE + && applyProfileModulation(bw, sf, cr) && updatePreamble(sf, bw); #endif if (!success) return false; diff --git a/src/helpers/radiolib/ESP32BufferedRadioHal.h b/src/helpers/radiolib/ESP32BufferedRadioHal.h index a68b2bfab..6ee29cdf3 100644 --- a/src/helpers/radiolib/ESP32BufferedRadioHal.h +++ b/src/helpers/radiolib/ESP32BufferedRadioHal.h @@ -1,5 +1,5 @@ #pragma once -#include +#include "TunableRadioHal.h" #ifndef MC_SX1262_SPI_HZ #define MC_SX1262_SPI_HZ 8000000 @@ -8,11 +8,13 @@ // Opted-in ESP32 boards only. Keep RadioLib's transaction/CS, BUSY polling and // status parsing unchanged; send each full-duplex buffer in one SPI operation. // SX1262 boards retain their configured default; LR1110 boards pass 16 MHz. -class ESP32BufferedRadioHal : public ArduinoHal { +class ESP32BufferedRadioHal : public TunableRadioHal { public: explicit ESP32BufferedRadioHal(SPIClass& bus, uint32_t hz = MC_SX1262_SPI_HZ) - : ArduinoHal(bus, SPISettings(hz, MSBFIRST, SPI_MODE0)) {} + : TunableRadioHal(bus, hz, false, true, + hz >= 16000000 ? 16000000 : 8000000) {} void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override { - if (len) spi->transferBytes(out, in, len); + if (!bufferedTransfersEnabled()) ArduinoHal::spiTransfer(out, len, in); + else if (len) spi->transferBytes(out, in, len); } }; diff --git a/src/helpers/radiolib/Nrf52BufferedRadioHal.h b/src/helpers/radiolib/Nrf52BufferedRadioHal.h index 45742a1a5..07027e4e2 100644 --- a/src/helpers/radiolib/Nrf52BufferedRadioHal.h +++ b/src/helpers/radiolib/Nrf52BufferedRadioHal.h @@ -1,6 +1,6 @@ #pragma once -#include +#include "TunableRadioHal.h" #include "NrfRadioProfileExperimentConfig.h" #ifndef MC_NRF_RADIO_PROFILE_PRIORITY @@ -26,13 +26,14 @@ // nRF52840 SPIClass::transfer(uint8_t) starts one SPIM transaction per byte. // RadioLib sends whole command buffers; keep its CS, BUSY, and status handling // but transfer each buffer in one SPIM transaction. LR1110 supports 16 MHz SPI. -class Nrf52BufferedRadioHal : public ArduinoHal { +class Nrf52BufferedRadioHal : public TunableRadioHal { #if MC_NRF_RADIO_BUSY_SPIN_POLLS > 0 uint8_t _busy_spin_polls_remaining = 0; #endif public: explicit Nrf52BufferedRadioHal(SPIClass& bus, uint32_t hz = 16000000) - : ArduinoHal(bus, SPISettings(hz, MSBFIRST, SPI_MODE0)) {} + : TunableRadioHal(bus, hz, false, true, + hz >= 16000000 ? 16000000 : 8000000) {} #if MC_NRF_RADIO_PROFILE_PRIORITY || MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD void spiBeginTransaction() override { @@ -50,7 +51,8 @@ public: void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override { if (!len) return; - spi->transfer(out, in, len); + if (!bufferedTransfersEnabled()) ArduinoHal::spiTransfer(out, len, in); + else spi->transfer(out, in, len); #if MC_NRF_RADIO_BUSY_SPIN_POLLS > 0 _busy_spin_polls_remaining = MC_NRF_RADIO_BUSY_SPIN_POLLS; #endif diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 0366ca305..f04d2441e 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -6,6 +6,10 @@ #include "ProfileSwitchTiming.h" #include "NrfRadioPriorityLease.h" #include "NrfRadioUsbIrqGuard.h" +#if defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM) || defined(RP2040_PLATFORM) +#include "TunableRadioHal.h" +#define MESH_TUNABLE_RADIO_HAL 1 +#endif #if MESH_HIL_PROFILE_TIMING namespace mesh { namespace profiletiming { extern "C" { @@ -443,6 +447,69 @@ void RadioLibWrapper::endReconfigure(bool resume_rx) { if (resume_rx) startRecv(); } +mesh::RadioSwitchCapabilities RadioLibWrapper::switchTuningCapabilities() const { + mesh::RadioSwitchCapabilities caps; +#if MESH_TUNABLE_RADIO_HAL + auto* hal = TunableRadioHal::find(_radio->getMod()->hal); + if (hal) caps = hal->capabilities(); +#endif + caps.flags |= driverSwitchCapabilities(); + return caps; +} + +mesh::RadioSwitchTuning RadioLibWrapper::switchTuning() const { + mesh::RadioSwitchTuning value; +#if MESH_TUNABLE_RADIO_HAL + auto* hal = TunableRadioHal::find(_radio->getMod()->hal); + if (hal) value = hal->getTuning(); +#endif + value.flags |= getDriverSwitchFeatures(); + return value; +} + +uint32_t RadioLibWrapper::switchSpiHz() const { +#if MESH_TUNABLE_RADIO_HAL + auto* hal = TunableRadioHal::find(_radio->getMod()->hal); + if (hal) return hal->effectiveHz(); +#endif + return 0; +} + +mesh::RadioParamApplyResult RadioLibWrapper::trySetSwitchTuning( + const mesh::RadioSwitchTuning& value, + bool (*persist)(void*, const mesh::RadioSwitchTuning&), void* context) { + using Result = mesh::RadioParamApplyResult; + if (!mesh::validRadioSwitchTuning(value, switchTuningCapabilities())) return Result::FAILED; + const auto previous = switchTuning(); + if (value == previous) return !persist || persist(context, value) ? Result::APPLIED : Result::FAILED; + const uint8_t resume_rx = beginReconfigure(); + if (resume_rx > 1) return resume_rx == 2 ? Result::BUSY : Result::FAILED; + + // These setters change policy/SPISettings only, never an SPI peripheral or + // pin mapping. Clearing owned caches forces the next retune to prove RX. + configureBootSwitchTuning(value); + _profiles.resetSwitchTest(); + endReconfigure(resume_rx); + const bool resumed = !resume_rx || isInRecvMode(); + if (resumed && (!persist || persist(context, value))) return Result::APPLIED; + + // There is no SPI in these setters, including after an RX completion was + // queued during a filesystem save. Never rearm or clear that unread FIFO. + configureBootSwitchTuning(previous); + if (!resumed) endReconfigure(resume_rx); + return Result::FAILED; +} + +bool RadioLibWrapper::configureBootSwitchTuning(const mesh::RadioSwitchTuning& value) { + if (!mesh::validRadioSwitchTuning(value, switchTuningCapabilities())) return false; +#if MESH_TUNABLE_RADIO_HAL + auto* hal = TunableRadioHal::find(_radio->getMod()->hal); + if (hal && !hal->setTuning(value)) return false; +#endif + setDriverSwitchFeatures(value.flags); + return true; +} + bool RadioLibWrapper::restoreAfterDeepInit() { #if MESH_ENABLE_TX_BEACONS if (_beacon_owned) cacheBeaconRestoreParams(); diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index b597d86c4..482395c13 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -328,6 +328,9 @@ protected: virtual bool radioDeepInit() { return false; } virtual bool supportsRadioDeepInit() const { return false; } virtual bool applyParams(float freq, float bw, uint8_t sf, uint8_t cr) = 0; + virtual uint8_t getDriverSwitchFeatures() const { return 0; } + virtual uint8_t driverSwitchCapabilities() const { return 0; } + virtual void setDriverSwitchFeatures(uint8_t) {} virtual int16_t applyCachedTxPower(int8_t dbm) { return _radio->setOutputPower(dbm); } @@ -483,6 +486,13 @@ public: #endif mesh::RadioProfiles* profiles() override { return &_profiles; } const mesh::RadioProfiles* profiles() const override { return &_profiles; } + mesh::RadioSwitchCapabilities switchTuningCapabilities() const override; + mesh::RadioSwitchTuning switchTuning() const override; + uint32_t switchSpiHz() const override; + bool configureBootSwitchTuning(const mesh::RadioSwitchTuning& tuning) override; + mesh::RadioParamApplyResult trySetSwitchTuning(const mesh::RadioSwitchTuning& tuning, + bool (*persist)(void*, const mesh::RadioSwitchTuning&) = nullptr, + void* context = nullptr) override; bool validateProfile(const mesh::RadioProfileParams& params) const override; uint8_t receiveProfile() const override { return _active_profile; } uint32_t receiveProfileGeneration() const override { return _profile_generation; } diff --git a/src/helpers/radiolib/SX126xBufferedRadioHal.h b/src/helpers/radiolib/SX126xBufferedRadioHal.h index e1e2bd570..ebbedb717 100644 --- a/src/helpers/radiolib/SX126xBufferedRadioHal.h +++ b/src/helpers/radiolib/SX126xBufferedRadioHal.h @@ -11,15 +11,16 @@ #include "ESP32BufferedRadioHal.h" using SX126xBufferedRadioHalBase = ESP32BufferedRadioHal; #elif defined(RP2040_PLATFORM) - #include + #include "TunableRadioHal.h" // Earle Philhower's HardwareSPI bulk API retains full-duplex blocking // completion while filling/draining the controller FIFO in one call. - class SX126xBufferedRadioHalBase : public ArduinoHal { + class SX126xBufferedRadioHalBase : public TunableRadioHal { public: explicit SX126xBufferedRadioHalBase(SPIClass& bus, uint32_t hz) - : ArduinoHal(bus, SPISettings(hz, MSBFIRST, SPI_MODE0)) {} + : TunableRadioHal(bus, hz, false, true) {} void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override { - if (len) spi->transfer(out, in, len); + if (!bufferedTransfersEnabled()) ArduinoHal::spiTransfer(out, len, in); + else if (len) spi->transfer(out, in, len); } }; #else diff --git a/src/helpers/radiolib/TunableRadioHal.h b/src/helpers/radiolib/TunableRadioHal.h new file mode 100644 index 000000000..865218ac5 --- /dev/null +++ b/src/helpers/radiolib/TunableRadioHal.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include "../RadioSwitchTuning.h" + +// A radio keeps the board-selected SPI controller and its original lifetime +// owner. Runtime tuning only changes transaction settings and transfer style; +// it must never remap an nRF controller or restart a shared storage/display bus. +class TunableRadioHal : public ArduinoHal { + TunableRadioHal* _next; + const uint32_t _defaultHz; + const uint8_t _spiClocks; + const bool _canBuffer; + uint8_t _spiMHz = 0; + bool _bufferEnabled; + + static TunableRadioHal*& registry() { + static TunableRadioHal* head = nullptr; + return head; + } + + protected: + bool bufferedTransfersEnabled() const { return _bufferEnabled; } + + public: + explicit TunableRadioHal(SPIClass& bus, uint32_t hz = 8000000, + bool initializeBus = false, bool canBuffer = false, + uint32_t maxHz = 8000000) + : ArduinoHal(bus, SPISettings(hz, MSBFIRST, SPI_MODE0)), + _next(registry()), _defaultHz(hz), + _spiClocks(2 | 4 | 8 | (maxHz >= 16000000 ? 16 : 0)), + _canBuffer(canBuffer), _bufferEnabled(canBuffer) { + initInterface = initializeBus; + registry() = this; + } + + ~TunableRadioHal() override { + TunableRadioHal** entry = ®istry(); + while (*entry && *entry != this) entry = &(*entry)->_next; + if (*entry) *entry = _next; + } + + TunableRadioHal(const TunableRadioHal&) = delete; + TunableRadioHal& operator=(const TunableRadioHal&) = delete; + + // Pointer identity avoids RTTI and safely rejects unrelated board HALs. + static TunableRadioHal* find(RadioLibHal* hal) { + for (TunableRadioHal* entry = registry(); entry; entry = entry->_next) + if (static_cast(entry) == hal) return entry; + return nullptr; + } + + mesh::RadioSwitchTuning getTuning() const { + mesh::RadioSwitchTuning result; + result.flags = _bufferEnabled ? mesh::RadioSwitchTuning::Buffer : 0; + result.spi_mhz = _spiMHz; + return result; + } + + mesh::RadioSwitchCapabilities capabilities() const { + mesh::RadioSwitchCapabilities result; + result.flags = _canBuffer ? mesh::RadioSwitchTuning::Buffer : 0; + result.spi_clocks = _spiClocks; + return result; + } + + bool setTuning(const mesh::RadioSwitchTuning& tuning) { + mesh::RadioSwitchTuning value = tuning; + value.flags &= mesh::RadioSwitchTuning::Buffer; + if (!mesh::validRadioSwitchTuning(value, capabilities())) return false; + _spiMHz = value.spi_mhz; + _bufferEnabled = value.flags & mesh::RadioSwitchTuning::Buffer; + spiSettings = SPISettings(effectiveHz(), MSBFIRST, SPI_MODE0); + return true; + } + + uint32_t defaultHz() const { return _defaultHz; } + uint32_t effectiveHz() const { + return _spiMHz ? static_cast(_spiMHz) * 1000000UL : _defaultHz; + } +}; diff --git a/test/fixtures/radio_profiles/switch_tuning_test.cpp b/test/fixtures/radio_profiles/switch_tuning_test.cpp new file mode 100644 index 000000000..7fa7c4a3f --- /dev/null +++ b/test/fixtures/radio_profiles/switch_tuning_test.cpp @@ -0,0 +1,400 @@ +// Exercise the production shared parser and persistence with deterministic +// radio admission and storage failures. The physical driver has its own tests. +#define main existing_cli_main +#include "cli_test.cpp" +#undef main + +struct TuningRadio : Radio { + mesh::RadioSwitchCapabilities caps; + mesh::RadioSwitchTuning tuning; + mesh::RadioParamApplyResult admission = mesh::RadioParamApplyResult::APPLIED; + uint32_t default_spi_hz = 8888888; + unsigned boot_configurations = 0, attempts = 0, changes = 0, persists = 0; + TuningRadio() { + caps.flags = 127; + caps.spi_clocks = 0x1e; + tuning.flags = caps.flags; + } + mesh::RadioSwitchCapabilities switchTuningCapabilities() const override { return caps; } + mesh::RadioSwitchTuning switchTuning() const override { return tuning; } + uint32_t switchSpiHz() const override { + return tuning.spi_mhz ? uint32_t(tuning.spi_mhz) * 1000000UL : default_spi_hz; + } + bool configureBootSwitchTuning(const mesh::RadioSwitchTuning& value) override { + ++boot_configurations; + if (!mesh::validRadioSwitchTuning(value, caps)) return false; + tuning = value; + return true; + } + mesh::RadioParamApplyResult trySetSwitchTuning(const mesh::RadioSwitchTuning& value, + bool (*persist)(void*, const mesh::RadioSwitchTuning&) = nullptr, + void* context = nullptr) override { + ++attempts; + if (!mesh::validRadioSwitchTuning(value, caps)) return mesh::RadioParamApplyResult::FAILED; + if (admission != mesh::RadioParamApplyResult::APPLIED) return admission; + const auto previous = tuning; + tuning = value; + if (persist) { + ++persists; + if (!persist(context, value)) { + tuning = previous; + return mesh::RadioParamApplyResult::FAILED; + } + } + if (value != previous) { + ++changes; + p.resetSwitchTest(); + } + return mesh::RadioParamApplyResult::APPLIED; + } +}; + +struct TuningFixture { + MemoryFS fs; + Clock clock; + TuningRadio radio; + mesh::RadioProfileCLI cli; + char reply[160]; + TuningFixture() { g_mock_millis = 0; cli.begin(&fs, &radio, &clock); } + const char* cmd(const char* text, bool success = true, bool remote = false) { + memset(reply, 0x55, sizeof(reply)); + assert(cli.handle(text, reply, sizeof(reply), remote)); + assert(memchr(reply, 0, sizeof(reply))); + if (!strncmp(text, "set ", 4)) { + if ((!strncmp(reply, "OK", 2)) != success) { + fprintf(stderr, "%s -> %s\n", text, reply); + assert(false); + } + } + return reply; + } + void advance(uint32_t milliseconds) { g_mock_millis += milliseconds; cli.loop(); } +}; + +static void crc(std::vector& image) { + assert(image.size() == 24); + uint32_t value = 0xffffffffU; + for (size_t i = 0; i < image.size() - 4; ++i) { + value ^= image[i]; + for (unsigned bit = 0; bit < 8; ++bit) + value = (value >> 1) ^ ((value & 1) ? 0xedb88320U : 0); + } + memcpy(image.data() + image.size() - 4, &value, 4); +} + +static void controlsAndPersistence() { + TuningFixture f; + assert(f.radio.tuning.flags == 127 && !f.radio.tuning.spi_mhz); + assert(f.fs.files.empty()); + const char* features[] = {"warm", "fs", "packet", "iq", "modem", "batch", "buffer"}; + for (unsigned i = 0; i < 7; ++i) { + char command[48]; + snprintf(command, sizeof(command), "get radio.fast.%s", features[i]); + assert(!strcmp(f.cmd(command), "> on")); + snprintf(command, sizeof(command), "set radio.fast.%s off", features[i]); + f.cmd(command); + assert(!(f.radio.tuning.flags & (1U << i))); + const auto expected = f.radio.tuning; + TuningRadio reboot; + mesh::RadioProfileCLI restored; + restored.begin(&f.fs, &reboot, &f.clock); + assert(reboot.tuning == expected); + // An absent radio2 record must not prevent independent fast-policy restore. + assert(!f.fs.files.count("/radio_profiles")); + snprintf(command, sizeof(command), "set radio.fast.%s on", features[i]); + f.cmd(command); + assert(f.radio.tuning.flags & (1U << i)); + } + for (unsigned mhz : {2, 4, 8, 16, 0}) { + char command[48]; + snprintf(command, sizeof(command), "set radio.fast.spi %u", mhz); + f.cmd(command); + assert(f.radio.tuning.spi_mhz == mhz); + TuningRadio reboot; + mesh::RadioProfileCLI restored; + restored.begin(&f.fs, &reboot, &f.clock); + assert(reboot.tuning == f.radio.tuning); + assert(reboot.switchSpiHz() == (mhz ? mhz * 1000000UL : 8888888)); + } + assert(strstr(f.cmd("get radio.fast"), "8888888")); + assert(!strcmp(f.cmd("get radio.fast.caps"), + "> features: warm fs packet iq modem batch buffer; spi=0,2,4,8,16")); + f.cmd("set radio.fast off"); + assert(!f.radio.tuning.flags && f.radio.tuning.spi_mhz == 2); + f.cmd("set radio.fast on"); + assert(f.radio.tuning.flags == f.radio.caps.flags && !f.radio.tuning.spi_mhz); + for (size_t size = 1; size <= sizeof(f.reply); ++size) { + char buffer[161]; + memset(buffer, 0x55, sizeof(buffer)); + for (const char* command : {"get radio.fast", "get radio.fast.caps"}) { + memset(buffer, 0x55, sizeof(buffer)); + assert(f.cli.handle(command, buffer, size)); + assert(memchr(buffer, 0, size) && buffer[size] == 0x55); + } + } + puts("Fast tuning: independent features, board clocks, saved restore and bounded output passed"); +} + +static void strictInputsAndUnsupportedFeatures() { + TuningFixture f; + const auto original = f.radio.tuning; + for (const char* command : { + "set radio.fast", "set radio.fast maybe", "set radio.fast on extra", + "set radio.fast.fs", "set radio.fast.fs ON", "set radio.fast.fs on extra", + "set radio.fast.spi", "set radio.fast.spi -2", "set radio.fast.spi +8", + "set radio.fast.spi 8mhz", "set radio.fast.spi 3", "set radio.fast.spi 32", + "set radio.fast.spi 256", "set radio.fast.spi 8 16", "set radio.fast.caps on"}) { + f.cmd(command, false); + assert(f.radio.tuning == original && f.fs.files.empty()); + } + for (const char* command : {"get radio.fast on", "get radio.fast.fs extra", + "get radio.fast.spi extra", "get radio.fast.caps extra"}) { + assert(strstr(f.cmd(command), "Error")); + assert(f.radio.tuning == original && f.fs.files.empty()); + } + f.radio.caps.flags &= ~mesh::RadioSwitchTuning::Buffer; + f.radio.tuning.flags &= f.radio.caps.flags; + f.radio.caps.spi_clocks &= ~(1U << 4); + const auto limited = f.radio.tuning; + f.cmd("set radio.fast.buffer on", false); + f.cmd("set radio.fast.spi 16", false); + assert(f.radio.tuning == limited && f.fs.files.empty()); + assert(strstr(f.cmd("get radio.fast.buffer"), "unsupported")); + assert(!strstr(f.cmd("get radio.fast.caps"), "buffer")); + assert(strstr(f.reply, "; spi=0,2,4,8") && !strstr(f.reply, ",16")); + f.cmd("set radio.fast off"); + f.cmd("set radio.fast on"); + assert(f.radio.tuning.flags == f.radio.caps.flags); + f.radio.caps.spi_clocks = 0; + assert(strstr(f.cmd("get radio.fast.caps"), "; spi=na")); + assert(strstr(f.cmd("get radio.fast"), "> spi=na")); + f.cmd("set radio.fast.spi 0", false); + const auto no_clock_control = f.radio.tuning; + f.cmd("set radio.fast off"); + assert(!f.radio.tuning.flags && !f.radio.tuning.spi_mhz); + f.cmd("set radio.fast on"); + assert(f.radio.tuning == no_clock_control); + puts("Fast tuning: strict parsing and unsupported controls passed"); +} + +static void localOnlyAndBusyTransactions() { + TuningFixture f; + f.cmd("set radio.fast.fs off"); + const auto original = f.radio.tuning; + const auto saved = f.fs.files["/radio_fast"]; + const unsigned persists = f.radio.persists; + for (const auto result : {mesh::RadioParamApplyResult::BUSY, + mesh::RadioParamApplyResult::FAILED}) { + f.radio.admission = result; + f.cmd("set radio.fast.fs on", false); + assert(f.radio.tuning == original && f.radio.persists == persists); + assert(f.fs.files["/radio_fast"] == saved); + } + f.radio.admission = mesh::RadioParamApplyResult::APPLIED; + assert(strstr(f.cmd("set radio.fast.fs on", false, true), "local")); + f.cli.beginReplyCommand(); + assert(strstr(f.cmd("set radio.fast.fs on", false), "local")); + assert(strstr(f.cmd("set radio.fast.fs on", false, true), "local")); + f.cli.endReplyCommand(); + assert(!f.cli.hasReplyMutation()); + assert(f.radio.tuning == original && f.fs.files["/radio_fast"] == saved); + assert(!strcmp(f.cmd("get radio.fast.fs", true, true), "> off")); + puts("Fast tuning: busy admission and local-only changes passed"); +} + +static void storageFailureKeepsCommittedPolicy() { + for (unsigned failure = 0; failure < 6; ++failure) { + TuningFixture f; + f.cmd("set radio.fast.fs off"); + const auto original = f.radio.tuning; + const auto saved = f.fs.files["/radio_fast"]; + if (failure == 0) f.fs.fail_write = true; + else if (failure == 1) f.fs.fail_write_after = 7; + else if (failure == 2) f.fs.fail_read_after = 7; + else if (failure == 3) f.fs.fail_rename = 1; + else if (failure == 4) f.fs.fail_rename = 2; + else { + f.fs.files["/radio_fast.tmp"] = {0}; + f.fs.fail_remove = true; + } + f.cmd("set radio.fast.fs on", false); + assert(f.radio.tuning == original); + f.fs.fail_write = f.fs.fail_remove = false; + f.fs.fail_write_after = f.fs.fail_read_after = -1; + f.fs.fail_rename = 0; + TuningRadio reboot; + mesh::RadioProfileCLI restored; + restored.begin(&f.fs, &reboot, &f.clock); + assert(reboot.tuning == original); + assert(f.fs.files["/radio_fast"] == saved); + } + puts("Fast tuning: short writes, readback and atomic rename failures passed"); +} + +static void bootRecoveryAndDeferredAdmission() { + for (unsigned damaged_primary = 0; damaged_primary < 4; ++damaged_primary) { + TuningFixture f; + f.cmd("set radio.fast.modem off"); + const auto expected = f.radio.tuning; + const auto saved = f.fs.files["/radio_fast"]; + assert(f.fs.rename("/radio_fast", "/radio_fast.bak")); + if (damaged_primary == 1) f.fs.files["/radio_fast"] = {0}; + else if (damaged_primary >= 2) { + auto& invalid = f.fs.files["/radio_fast"] = saved; + invalid[damaged_primary == 2 ? 3 : 4] = damaged_primary == 2 ? 128 : 3; + crc(invalid); + } + TuningRadio reboot; + mesh::RadioProfileCLI restored; + restored.begin(&f.fs, &reboot, &f.clock); + assert(reboot.tuning == expected); + assert(f.fs.files["/radio_fast"] == saved); + } + { + TuningFixture f; + f.cmd("set radio.fast.packet off"); + const auto expected = f.radio.tuning; + const auto saved = f.fs.files; + TuningRadio reboot; + reboot.admission = mesh::RadioParamApplyResult::BUSY; + mesh::RadioProfileCLI restored; + restored.begin(&f.fs, &reboot, &f.clock); + assert(reboot.tuning != expected && f.fs.files == saved); + restored.loop(); + assert(reboot.tuning != expected); + reboot.admission = mesh::RadioParamApplyResult::APPLIED; + g_mock_millis += 1000; + restored.loop(); + assert(reboot.tuning == expected && f.fs.files == saved); + } + { + TuningFixture f; + f.cmd("set radio.fast.iq off"); + const auto expected = f.radio.tuning; + f.fs.files["/radio_profiles"] = {0}; + TuningRadio reboot; + mesh::RadioProfileCLI restored; + restored.begin(&f.fs, &reboot, &f.clock); + assert(reboot.tuning == expected); // A corrupt radio2 image is isolated. + } + puts("Fast tuning: interrupted commit, deferred boot admission and store isolation passed"); +} + +static void malformedAndUnreadableImagesStayProtected() { + for (unsigned corruption = 0; corruption < 5; ++corruption) { + TuningFixture f; + f.cmd("set radio.fast.fs off"); + auto& image = f.fs.files["/radio_fast"]; + if (corruption == 0) image[0] ^= 1; + else if (corruption == 1) { image[3] = 128; crc(image); } + else if (corruption == 2) { image[4] = 3; crc(image); } + else if (corruption == 3) { image[5] = 1; crc(image); } + else { image[2] = 2; crc(image); } + const auto damaged = image; + TuningRadio reboot; + mesh::RadioProfileCLI restored; + restored.begin(&f.fs, &reboot, &f.clock); + assert(reboot.tuning.flags == 127 && !reboot.tuning.spi_mhz); + assert(f.fs.files["/radio_fast"] == damaged); + } + { + TuningFixture f; + f.cmd("set radio.fast.fs off"); + const auto saved = f.fs.files["/radio_fast"]; + f.fs.fail_read_open = true; + TuningRadio reboot; + mesh::RadioProfileCLI restored; + restored.begin(&f.fs, &reboot, &f.clock); + f.fs.fail_read_open = false; + assert(restored.handle("set radio.fast.fs off", f.reply, sizeof(f.reply))); + assert(strstr(f.reply, "Error")); + assert(f.fs.files["/radio_fast"] == saved); + } + puts("Fast tuning: corrupt, unsupported, reserved and unreadable images protected"); +} + +static void bootRetriesAreBoundedAcrossRollover() { + for (bool rollover : {false, true}) { + for (const auto admission : {mesh::RadioParamApplyResult::BUSY, + mesh::RadioParamApplyResult::FAILED}) { + TuningFixture f; + f.cmd("set radio.fast.fs off"); + const auto expected = f.radio.tuning; + const auto saved = f.fs.files; + TuningRadio reboot; + reboot.admission = admission; + mesh::RadioProfileCLI restored; + g_mock_millis = rollover ? UINT32_MAX - 500 : 100; + restored.begin(&f.fs, &reboot, &f.clock); + assert(reboot.attempts == 1); + // Hundreds of main-loop visits must not keep trying a rejected clock or + // RX policy. Millisecond rollover does not make its deadline due early. + for (unsigned i = 0; i < 999; ++i) { + ++g_mock_millis; + restored.loop(); + assert(reboot.attempts == 1); + } + ++g_mock_millis; + restored.loop(); + assert(reboot.attempts == 2 && reboot.tuning != expected); + restored.loop(); + assert(reboot.attempts == 2); + reboot.admission = mesh::RadioParamApplyResult::APPLIED; + g_mock_millis += 1000; + restored.loop(); + assert(reboot.attempts == 3 && reboot.tuning == expected); + assert(f.fs.files == saved && !reboot.persists); + } + } + puts("Fast tuning: failed/busy boot retries bounded across timer rollover"); +} + +static void savedSlowClockPrecedesRadioInitialization() { + for (bool backup : {false, true}) { + TuningFixture f; + f.cmd("set radio.fast off"); + const auto expected = f.radio.tuning; + assert(!expected.flags && expected.spi_mhz == 2); + if (backup) { + assert(f.fs.rename("/radio_fast", "/radio_fast.bak")); + f.fs.files["/radio_fast"] = {0}; + } + TuningRadio reboot; + reboot.admission = mesh::RadioParamApplyResult::FAILED; + assert(reboot.switchSpiHz() == 8888888); + mesh::RadioProfileCLI::configureBootTuning(&f.fs, reboot); + // This is the policy used by the chip's first initialization transaction. + // No runtime admission, RX restart, or persistence callback is permitted + // before that initialization; a runtime failure cannot veto boot policy. + assert(reboot.tuning == expected && reboot.switchSpiHz() == 2000000); + assert(reboot.boot_configurations == 1 && !reboot.attempts); + assert(!reboot.changes && !reboot.persists); + reboot.admission = mesh::RadioParamApplyResult::APPLIED; + mesh::RadioProfileCLI restored; + restored.begin(&f.fs, &reboot, &f.clock); + assert(reboot.tuning == expected); + } + for (unsigned unavailable = 0; unavailable < 4; ++unavailable) { + TuningFixture f; + if (unavailable != 0) f.cmd("set radio.fast off"); + if (unavailable == 1) f.fs.files["/radio_fast"][0] ^= 1; + if (unavailable == 2) f.fs.fail_read_open = true; + TuningRadio reboot; + const auto defaults = reboot.tuning; + mesh::RadioProfileCLI::configureBootTuning(unavailable == 3 ? nullptr : &f.fs, reboot); + assert(reboot.tuning == defaults && reboot.switchSpiHz() == 8888888); + assert(!reboot.boot_configurations && !reboot.attempts && !reboot.persists); + } + puts("Fast tuning: saved fallback clock configured before first radio initialization"); +} + +int main() { + controlsAndPersistence(); + strictInputsAndUnsupportedFeatures(); + localOnlyAndBusyTransactions(); + storageFailureKeepsCommittedPolicy(); + bootRecoveryAndDeferredAdmission(); + malformedAndUnreadableImagesStayProtected(); + bootRetriesAreBoundedAcrossRollover(); + savedSlowClockPrecedesRadioInitialization(); +} diff --git a/test/test_companion_help_writer.py b/test/test_companion_help_writer.py index adbeb88f3..321c2b360 100644 --- a/test/test_companion_help_writer.py +++ b/test/test_companion_help_writer.py @@ -35,6 +35,9 @@ PROFILES = { # suffix. Non-RAK call hashes bind print/println order as well as all text bytes. GOLDEN = {'rak_full': {'text': '9a68ce5a10905a201c3bddeb19f4819ce03c6dcaff248cff9c140803b70b14a7', 'calls': '56ed09dee7d53881b2c9fcf3f4ec9b6223e1562ce63e110a065e46706702b53b'}, 'nrf_full': {'text': 'b114ac12ae2ea01b7c47609b72f7eafb616c1dbeb7fb796a95b1377465fd9105', 'calls': '4973a7f21887d07b129cce4ae4cc8145f3f35e664bae09be658957210440d52e'}, 'nrf_minimal': {'text': '15fa8f290cd8c75cfde2fd2ac1cb5b5151a6755c68448fb2793dde3f27669033', 'calls': '6063fd1a699f9c0b588252bd8ce5922e0ff31cec6bbd24522789284ab2a37644'}, 'esp_full': {'text': '1e3b6781c81b5603f7092108e5e309543c60cef8ff4d7cb41a891f67396f716f', 'calls': 'b6335df2bd05b9cab44a7933469b76c5b2611c4b81057436a959feaf3bfbaf60'}} +FAST_HELP = (b' get radio.fast / get radio.fast.caps (supported saved controls)\r\n' + b' set radio.fast / set radio.fast.spi <0|2|4|8|16>\r\n') + HARNESS = r''' #include "CompanionHelpWriter.h" #include @@ -198,9 +201,14 @@ class CompanionHelpWriterTests(unittest.TestCase): self.assertEqual(result.returncode,0,result.stdout+result.stderr) result = subprocess.run([str(work/'test')],capture_output=True) self.assertEqual(result.returncode,0,result.stderr.decode(errors='replace')) - self.assertEqual(hashlib.sha256(result.stdout).hexdigest(),GOLDEN[name]['text']) + self.assertEqual(result.stdout.count(FAST_HELP), 1) + previous_help = result.stdout.replace(FAST_HELP, b'') + self.assertEqual(hashlib.sha256(previous_help).hexdigest(),GOLDEN[name]['text']) if name != 'rak_full': - self.assertEqual(hashlib.sha256(result.stderr).hexdigest(),GOLDEN[name]['calls']) + fast_call = b'P' + len(FAST_HELP).to_bytes(4, 'little') + FAST_HELP + self.assertEqual(result.stderr.count(fast_call), 1) + previous_calls = result.stderr.replace(fast_call, b'') + self.assertEqual(hashlib.sha256(previous_calls).hexdigest(),GOLDEN[name]['calls']) else: image = (work / 'test').read_bytes() self.assertNotIn(b'Commands:\r\n stats-core / stats-radio / stats-radio-diag',image) @@ -228,7 +236,8 @@ class CompanionHelpWriterTests(unittest.TestCase): self.assertEqual(result.returncode,0,result.stdout+result.stderr) result=subprocess.run([str(work/'test')],capture_output=True) self.assertEqual(result.returncode,0,result.stderr.decode(errors='replace')) - self.assertEqual(hashlib.sha256(result.stdout).hexdigest(),GOLDEN['rak_full']['text']) + self.assertEqual(result.stdout.count(FAST_HELP), 1) + self.assertEqual(hashlib.sha256(result.stdout.replace(FAST_HELP, b'')).hexdigest(),GOLDEN['rak_full']['text']) self.assertNotIn(b'Commands:\r\n stats-core / stats-radio / stats-radio-diag',(work/'test').read_bytes()) if __name__ == '__main__': diff --git a/test/test_esp32_buffered_radio_targets.py b/test/test_esp32_buffered_radio_targets.py index 383566130..108f657b3 100644 --- a/test/test_esp32_buffered_radio_targets.py +++ b/test/test_esp32_buffered_radio_targets.py @@ -88,7 +88,7 @@ struct Radio { clock = 8888888 if target == "lilygo_tdeck" else 8000000 buffered = local or target not in LEGACY_FALLBACKS expected = (4000000 if local else 2000000) if target == "heltec_rc32" else ( - clock if buffered else 2000000) + clock) selected = "spi" if local else "::SPI" name = f"target_{index}_{int(local)}" options = f"#define P_LORA_SCLK 5\n" if local else "" diff --git a/test/test_lr1110_packet_setter_cache.py b/test/test_lr1110_packet_setter_cache.py index 8183a0c28..cdb5e10ac 100644 --- a/test/test_lr1110_packet_setter_cache.py +++ b/test/test_lr1110_packet_setter_cache.py @@ -37,6 +37,7 @@ class LR1110PacketSetterCacheTests(unittest.TestCase): state = (ROOT / "src/helpers/radiolib/LR1110ProfileSwitchState.h").read_text() harness = r""" #include +#include #include #include #include @@ -106,6 +107,7 @@ struct LR1110 { struct Radio : LR1110 { uint32_t _activityAt=0;bool _headerSeen=false; LR1110ProfileSwitchState _profileSwitch{true}; + uint8_t _profileSwitchFeatures=55; bool _profilePacketDirty = true; uint32_t _profileFastResumes = 0; Radio() { ownership = &_profileSwitch; } @@ -182,7 +184,7 @@ int main(int argc, char** argv) { flags = [] if os.name == "nt" else ["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"] built = subprocess.run([compiler, "-std=gnu++11", "-Wall", "-Wextra", *flags, - str(cpp), "-o", str(cls.binary)], + "-I", str(ROOT / "src"), str(cpp), "-o", str(cls.binary)], capture_output=True, text=True, timeout=60) if built.returncode: raise AssertionError(built.stdout + built.stderr) diff --git a/test/test_lr1110_profile_switch.py b/test/test_lr1110_profile_switch.py index 3f403bab4..09d4d86ff 100644 --- a/test/test_lr1110_profile_switch.py +++ b/test/test_lr1110_profile_switch.py @@ -20,7 +20,7 @@ class LR1110ProfileSwitchTests(unittest.TestCase): "thinknode_m3": "Nrf52BufferedRadioHal", # Fast RX ownership is independent of the SPI transport. Keep M4 # on its prior bus until its SPIM3/BLE transfer path is qualified. - "thinknode_m4": None, + "thinknode_m4": "TunableRadioHal", "minewsemi_me25ls01": "Nrf52BufferedRadioHal", "thinknode_m7": "ESP32BufferedRadioHal", "thinknode_m9": "ESP32BufferedRadioHal", @@ -33,8 +33,9 @@ class LR1110ProfileSwitchTests(unittest.TestCase): self.assertIn("-D RADIO_CLASS=CustomLR1110", config) self.assertIn("-D WRAPPER_CLASS=CustomLR1110Wrapper", config) if hal is not None: - self.assertIn(f"{hal} radioHal(", target_source) - self.assertIn("new Module(&radioHal,", target_source) + name = "radio_hal" if target == "thinknode_m4" else "radioHal" + self.assertIn(f"{hal} {name}(", target_source) + self.assertIn(f"new Module(&{name},", target_source) else: self.assertNotIn("BufferedRadioHal", target_source) self.assertIn( @@ -43,9 +44,11 @@ class LR1110ProfileSwitchTests(unittest.TestCase): esp32_hal = (ROOT / "src/helpers/radiolib/ESP32BufferedRadioHal.h").read_text() nrf52_hal = (ROOT / "src/helpers/radiolib/Nrf52BufferedRadioHal.h").read_text() - self.assertIn("SPISettings(hz, MSBFIRST, SPI_MODE0)", esp32_hal) + tunable_hal = (ROOT / "src/helpers/radiolib/TunableRadioHal.h").read_text() + self.assertIn("SPISettings(hz, MSBFIRST, SPI_MODE0)", tunable_hal) + self.assertIn("TunableRadioHal(bus, hz, false, true,", esp32_hal) self.assertIn("spi->transferBytes(out, in, len)", esp32_hal) - self.assertIn("SPISettings(hz, MSBFIRST, SPI_MODE0)", nrf52_hal) + self.assertIn("TunableRadioHal(bus, hz, false, true,", nrf52_hal) self.assertIn("spi->transfer(out, in, len)", nrf52_hal) for target in ("thinknode_m7", "thinknode_m9"): source = (ROOT / "variants" / target / "target.cpp").read_text() @@ -62,9 +65,11 @@ class LR1110ProfileSwitchTests(unittest.TestCase): implementation = method(source, "int16_t snapshotIrqAfterRxExit(") harness = r''' #include +#include #include #include #include +#define MC_LR1110_FAST_PROFILE_SWITCH 1 #define RADIOLIB_ERR_NONE 0 #define RADIOLIB_MODULE_SPI_WIDTH_STATUS 2 struct Module { @@ -96,7 +101,8 @@ int main() { def test_fast_rx_skips_only_unchanged_packet_tuple_and_fails_closed(self): source = (ROOT / "src/helpers/radiolib/CustomLR1110.h").read_text() signatures = ( - "void setProfileSwitchOptimization(", "void setProfileStandbyWarm(", + "uint8_t profileSwitchCapabilities(", "uint8_t getProfileSwitchFeatures(", + "void setProfileSwitchFeatures(", "void setProfileSwitchOptimization(", "void setProfileStandbyWarm(", "void beginProfileSwitch(", "void endProfileSwitch(", "bool profileSwitchFailed(", "int16_t standby()", "int16_t standby(uint8_t", "int16_t sleep()", "int16_t reset()", "int16_t stageMode(", @@ -109,10 +115,12 @@ int main() { ) harness = r''' #include +#include #include #include #include @STATE@ +#define MC_LR1110_FAST_PROFILE_SWITCH 1 #define RADIOLIB_ERR_NONE 0 #define RADIOLIB_LR11X0_STANDBY_XOSC 0 #define RADIOLIB_LR11X0_RX_TIMEOUT_INF 0xffffff @@ -161,6 +169,7 @@ struct LR1110 { struct Radio:LR1110 { uint32_t _activityAt=0;bool _headerSeen=false; LR1110ProfileSwitchState _profileSwitch{true}; + uint8_t _profileSwitchFeatures=55; bool _profileStandbyWarm=false,_profilePacketDirty=false; uint32_t _profileFastResumes=0; uint32_t pendingIrq=0; @@ -216,7 +225,29 @@ int main() { prime(r);r.stageMode(2,nullptr);assert(!r._profileSwitch.rxValid); prime(r);r.sleep();assert(!r._profileSwitch.rxValid); prime(r);r.reset();assert(!r._profileSwitch.rxValid); + // FS and packet reuse remain independently controllable without disabling + // warm receive ownership; unsupported SX1262 IQ/transport bits stay absent. + for(unsigned requested=0;requested<256;++requested) { + Radio x;x.setProfileSwitchFeatures(uint8_t(requested)); + const uint8_t features=requested & x.profileSwitchCapabilities(); + assert(x.getProfileSwitchFeatures()==features && x.trace.empty()); + assert(!(features & (mesh::RadioSwitchTuning::Iq | mesh::RadioSwitchTuning::Buffer))); + assert(x._profileSwitch.enabled==bool(features & mesh::RadioSwitchTuning::Warm)); + assert(!x._profileSwitch.rxValid && !x._profileSwitch.modulationValid); + assert(x.startReceive()==0);x.setProfileStandbyWarm(true);x.trace.clear(); + x.beginProfileSwitch(true);assert(x.standby()==0); + const bool warm=features & mesh::RadioSwitchTuning::Warm; + const bool fs=features & mesh::RadioSwitchTuning::Fs; + assert(x.trace==(warm ? (fs ? "IF":"s") : "S"));x.trace.clear(); + assert(x.startReceive()==0); + const bool packet=features & mesh::RadioSwitchTuning::Packet; + assert(x.trace==(warm ? (packet ? "QWR":"QPWR") : "N")); + x.endProfileSwitch(true);x.trace.clear();x._profilePacketDirty=true; + x.setProfileSwitchFeatures(features); + assert(x.trace.empty() && !x._profileSwitch.rxValid && !x._profilePacketDirty); + } } + ''' self.compile_run(harness.replace("@STATE@", state).replace("@METHODS@", methods)) @@ -226,6 +257,7 @@ int main() { ) harness = r''' #include +#include #include @STATE@ int main() { @@ -279,10 +311,12 @@ int main() { ) harness = r''' #include +#include #include #include #include @STATE@ +#define MC_LR1110_FAST_PROFILE_SWITCH 1 #define RADIOLIB_ERR_NONE 0 #define RADIOLIB_ERR_INVALID_SPREADING_FACTOR -1 #define RADIOLIB_ERR_INVALID_CODING_RATE -2 @@ -298,6 +332,7 @@ int main() { #define RADIOLIB_LR11X0_LORA_LDRO_DISABLED 0 struct Radio { LR1110ProfileSwitchState _profileSwitch{true}; + uint8_t _profileSwitchFeatures=55; uint8_t spreadingFactor=7, bandwidth=4, codingRate=1, ldrOptimize=0; float bandwidthKhz=125; bool ldroAuto=true; @@ -350,6 +385,15 @@ int main() { assert(r.spreadingFactor==oldSf && r.bandwidth==oldBw && r.codingRate==oldCr); assert(r.bandwidthKhz==oldKhz && r.ldrOptimize==oldLdro); assert(r._profileSwitch.failed() && !r._profileSwitch.rxValid); + for(unsigned features=0;features<64;++features) { + Radio x;x._profileSwitchFeatures=features; + assert(x.setLoRaModulationParams(125,7,5)==0 && x.writes==1 && x.queries==1); + x._profileSwitch.rxResult(0,true);x._profileSwitch.begin(true,true); + x._profileSwitch.standbyResult(0); + assert(x.setLoRaModulationParams(125,7,5)==0); + assert(x.queries==((features & mesh::RadioSwitchTuning::Modem) ? 1U:2U)); + assert(x.writes==((features & mesh::RadioSwitchTuning::Batch) ? 1U:2U)); + } } ''' self.compile_run(harness.replace("@STATE@", state).replace("@PRODUCTION@", production)) diff --git a/test/test_nrf52_hil_spim1.py b/test/test_nrf52_hil_spim1.py index 1627b53fd..0a8694166 100644 --- a/test/test_nrf52_hil_spim1.py +++ b/test/test_nrf52_hil_spim1.py @@ -185,7 +185,7 @@ int main() { assert(SPI1._spim.drv_inst_idx==unsigned(MC_NRF52_HIL_SPIM1_RADIO)); assert(c_indices()==(MC_NRF52_HIL_SPIM1_RADIO?0x3210u:0x2100u)); assert(radio.module->hal->spi==&SPI && !radio.module->hal->initInterface); - assert(radio.module->hal->spiSettings.hz==(MC_NRF52_HIL_SPIM1_RADIO?8000000:2000000)); + assert(radio.module->hal->spiSettings.hz==8000000); assert(radio_init() && SPI.begins==1 && SPI1.begins==0); uint8_t tx[]={0x86,1,2,3},rx[4]={};radio.module->hal->spiTransfer(tx,4,rx); assert(SPI.bursts==unsigned(MC_NRF52_HIL_SPIM1_RADIO)); diff --git a/test/test_nrf_radio_yield_budget.py b/test/test_nrf_radio_yield_budget.py index 2ba8c1413..9ee607fcc 100644 --- a/test/test_nrf_radio_yield_budget.py +++ b/test/test_nrf_radio_yield_budget.py @@ -23,7 +23,7 @@ int main() { ArduinoHal& radio=concrete; constexpr unsigned budget=MC_NRF_RADIO_BUSY_SPIN_POLLS; #if MC_NRF_RADIO_BUSY_SPIN_POLLS == 0 - static_assert(sizeof(Nrf52BufferedRadioHal)==sizeof(ArduinoHal),"default RAM growth"); + static_assert(sizeof(Nrf52BufferedRadioHal)==sizeof(TunableRadioHal),"default RAM growth"); #endif // The first pre-transfer wait delegates immediately, even in opt-in builds. for(unsigned i=0;i<1000;++i) radio.yield(); diff --git a/test/test_profile_sx1262_iq_cache.py b/test/test_profile_sx1262_iq_cache.py index b39007f11..d78ad6e0d 100644 --- a/test/test_profile_sx1262_iq_cache.py +++ b/test/test_profile_sx1262_iq_cache.py @@ -52,7 +52,7 @@ class ProfileIqCacheTest(unittest.TestCase): sanitizers = [] if os.name == "nt" else [ "-fsanitize=address,undefined", "-fno-omit-frame-pointer", "-no-pie"] built = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17", - "-Wall", "-Wextra", *sanitizers, str(cpp), "-o", str(binary)], + "-Wall", "-Wextra", *sanitizers, "-I", str(ROOT / "src"), str(cpp), "-o", str(binary)], capture_output=True, text=True) self.assertEqual(built.returncode, 0, built.stderr) ran = subprocess.run([str(binary)], capture_output=True, text=True) @@ -61,7 +61,8 @@ class ProfileIqCacheTest(unittest.TestCase): def test_owned_iq_ack_wire_payload_setters_lifecycle_and_errors(self): source = (ROOT / "src/helpers/radiolib/CustomSX1262.h").read_text() state = (ROOT / "src/helpers/radiolib/SX1262ProfileSwitchState.h").read_text().replace("#pragma once", "") - signatures = ("void setProfileSwitchOptimization(", "void beginProfileSwitch(", + signatures = ("uint8_t defaultProfileSwitchFeatures(", "uint8_t profileSwitchCapabilities(", "uint8_t getProfileSwitchFeatures(", + "void setProfileSwitchFeatures(", "void setProfileSwitchOptimization(", "void beginProfileSwitch(", "void endProfileSwitch(", "int16_t reset(bool", "int16_t sleep(bool", "int16_t stageMode(", "int16_t setPreambleLength(", "int16_t setCRC(", "int16_t implicitHeader(", "int16_t explicitHeader()", "int16_t invertIQ(", @@ -71,11 +72,15 @@ class ProfileIqCacheTest(unittest.TestCase): methods = "\n".join(method(source, signature).replace(" override", "") for signature in signatures) fixture = r''' #include +#include #include #include #include #include @DEFINES@ +#define MC_SX1262_FAST_PROFILE_SWITCH 1 +#define MC_SX1262_FAST_FS 1 +#define MC_SX1262_FAST_MODEM_CACHE 0 #define MC_SX1262_FAST_PACKET_CACHE 0 #define RADIOLIB_ERR_NONE 0 #define RADIOLIB_SX126X_STANDBY_RC 0 @@ -153,10 +158,12 @@ struct SX1262 { struct Radio : SX1262 { uint32_t _activityAt=0;bool _headerSeen=false; SX1262ProfileSwitchState _profileSwitch{true}; + uint8_t _profileSwitchFeatures=defaultProfileSwitchFeatures(); bool _coldStandby=true,_tcxoWakePending=false,_rx_ps_rf_rx_disabled=false; uint32_t _profileFastResumes=0; #ifdef MC_SX1262_HIL bool hilSkipUnchangedPacket=false,hilSkipUnchangedIq=true; + bool hilUseFsOnRetune=false,hilSkipConfirmedModemQuery=false; uint32_t hilPacketSkips=0,hilIqWorkaroundSkips=0; #endif @METHODS@ @@ -197,6 +204,11 @@ int main() { #endif assert(r.startReceive()==0 && r.trace=="CIiPWR"); } + // A runtime IQ opt-out keeps owned RX but forces its checked workaround. + { Radio r;prime(r);r.setProfileSwitchFeatures( + r.getProfileSwitchFeatures() & ~mesh::RadioSwitchTuning::Iq); + r.trace.clear();prime(r);arm(r); + assert(r.startReceive()==0 && r.trace=="CIiPWR"); } // Owned preamble staging is the intended fast case, unlike an ordinary // preamble command. Every ordinary packet/direct-register path revokes IQ. { Radio r;prime(r);arm(r);assert(r.setPreambleLength(64)==0 && r.trace.empty()); diff --git a/test/test_radio_rx_activity_epoch.py b/test/test_radio_rx_activity_epoch.py index b526ad7f1..2860a395a 100644 --- a/test/test_radio_rx_activity_epoch.py +++ b/test/test_radio_rx_activity_epoch.py @@ -24,6 +24,7 @@ def build_source(family='CustomSX1262',remove_reset=False,include_duty=False): source=source.replace('#include ', '''#include #include #include +#include #include #include #include @@ -59,11 +60,14 @@ namespace mesh { enum class RadioParamApplyResult { APPLIED, BUSY, FAILED }; } int64_t esp_timer_get_time(){return int64_t(now_ms)*1000;} uint32_t profileTimestamp(bool){return now_ms*1000;} uint32_t profileElapsedUs(uint32_t a,uint32_t b,bool){return b-a;}''') + source=source.replace('struct Board {', 'struct Board { bool prepareRadioProfileCpuClock(bool, bool& changed) {changed=false;return true;}') source=source.replace('struct Module {', '''struct Module { enum {MODE_RX=1};void setRfSwitchState(int){}''') source=source.replace('struct Chip : LR2021 {', '''using SX1262=LR2021;using SX1268=LR2021;using LLCC68=LR2021;using STM32WLx=LR2021;using LR1110=LR2021;using LR1121=LR2021; struct Chip : LR2021 { SX1262ProfileSwitchState _profileSwitch{true};bool _coldStandby=true; + uint8_t _profileSwitchFeatures=63; + uint8_t getProfileSwitchFeatures() const { return _profileSwitchFeatures; } uint32_t _profileFastResumes=0; bool _profilePacketDirty=false;Module* mod=&module; int16_t snapshotIrqAfterRxExit(uint32_t* pending){*pending=irq;return snapshot_failure;} diff --git a/test/test_radio_switch_driver_controls.py b/test/test_radio_switch_driver_controls.py new file mode 100644 index 000000000..b346baebd --- /dev/null +++ b/test/test_radio_switch_driver_controls.py @@ -0,0 +1,143 @@ +"""Execute production switch-feature masks and conservative modulation paths.""" +from pathlib import Path +import unittest + +from test_radio_receive_contract import method +import test_sx1262_batched_modulation as compiler + + +ROOT = Path(__file__).resolve().parents[1] + + +class RadioSwitchDriverControlsTests(unittest.TestCase): + compile_run = compiler.BatchedModulationTests.compile_run + + def test_sx1262_compile_opt_outs_cannot_be_enabled_at_runtime(self): + source = (ROOT / "src/helpers/radiolib/CustomSX1262.h").read_text() + definitions = source[source.index("#ifndef MC_SX1262_FAST_PROFILE_SWITCH"): + source.index("#ifndef SX126X_TX_BUSY_TIMEOUT_MS")] + methods = "\n".join(method(source, signature) for signature in ( + "uint8_t defaultProfileSwitchFeatures(", "uint8_t profileSwitchCapabilities(", + "uint8_t getProfileSwitchFeatures(", "void setProfileSwitchFeatures(")) + state = (ROOT / "src/helpers/radiolib/SX1262ProfileSwitchState.h").read_text() + harness = r''' +#include +#include +@OVERRIDES@ +@DEFINITIONS@ +@STATE@ +struct Radio { + SX1262ProfileSwitchState _profileSwitch{true}; + uint8_t _profileSwitchFeatures=defaultProfileSwitchFeatures(); + @METHODS@ +}; +int main() { + Radio r; + assert(r.profileSwitchCapabilities()==@CAPS@); + assert(r.getProfileSwitchFeatures()==@CAPS@); + for(unsigned bits=0;bits<256;++bits) { + r._profileSwitch.rxResult(0,true); + r._profileSwitch.begin(true,true);r._profileSwitch.standbyResult(0); + r._profileSwitch.packetResult(0,32,1,255,0,0); + r._profileSwitch.modulationResult(0,7,3,1,0); + r.setProfileSwitchFeatures(uint8_t(bits)); + assert(r.getProfileSwitchFeatures()==(bits & @CAPS@)); + assert(r._profileSwitch.enabled==bool(bits & @CAPS@ & mesh::RadioSwitchTuning::Warm)); + assert(!r._profileSwitch.rxValid && !r._profileSwitch.active + && !r._profileSwitch.modulationValid && !r._profileSwitch.packetValid); + } +} +''' + policies = ( + ("", 63), + ("#define MC_SX1262_FAST_PROFILE_SWITCH 0", 32), + ("#define MC_SX1262_FAST_FS 0", 37), + ("#define MC_SX1262_FAST_PACKET_CACHE 0", 59), + # The I/O-expander board retains its proven IQ workaround cache. + ("#define MC_SX1262_FAST_FS 0\n#define MC_SX1262_FAST_PACKET_CACHE 0\n" + "#define MC_SX1262_FAST_IQ_CACHE 1", 41), + ) + for overrides, caps in policies: + with self.subTest(overrides=overrides): + self.compile_run(harness.replace("@OVERRIDES@", overrides) + .replace("@DEFINITIONS@", definitions) + .replace("@STATE@", state) + .replace("@METHODS@", methods) + .replace("@CAPS@", str(caps))) + + def test_lr1110_compile_opt_out_keeps_all_shortcuts_unavailable(self): + source = (ROOT / "src/helpers/radiolib/CustomLR1110.h").read_text() + methods = "\n".join(method(source, signature) for signature in ( + "uint8_t profileSwitchCapabilities(", "uint8_t getProfileSwitchFeatures(", + "void setProfileSwitchFeatures(")) + state = (ROOT / "src/helpers/radiolib/LR1110ProfileSwitchState.h").read_text() + harness = r''' +#include +#include +#define MC_LR1110_FAST_PROFILE_SWITCH @ENABLED@ +@STATE@ +struct Radio { + LR1110ProfileSwitchState _profileSwitch{true}; + uint8_t _profileSwitchFeatures=profileSwitchCapabilities(); + bool _profilePacketDirty=true; + @METHODS@ +}; +int main() { + Radio r; + assert(r.profileSwitchCapabilities()==@CAPS@); + for(unsigned bits=0;bits<256;++bits) { + r._profilePacketDirty=true;r.setProfileSwitchFeatures(uint8_t(bits)); + assert(r.getProfileSwitchFeatures()==(bits & @CAPS@)); + assert(r._profileSwitch.enabled==bool(bits & @CAPS@ & mesh::RadioSwitchTuning::Warm)); + assert(!r._profileSwitch.rxValid && !r._profileSwitch.modulationValid + && !r._profilePacketDirty); + } +} +''' + for enabled in (0, 1): + with self.subTest(enabled=enabled): + self.compile_run(harness.replace("@STATE@", state) + .replace("@METHODS@", methods) + .replace("@ENABLED@", str(enabled)) + .replace("@CAPS@", "55" if enabled else "0")) + + def test_both_wrappers_execute_normal_setters_when_batch_is_disabled(self): + harness = r''' +#include +#include +#include +#define RADIOLIB_ERR_NONE 0 +struct @CHIP@ { + std::string trace;char failure=0; + int call(char op) {trace+=op;return failure==op?-7:0;} + int setLoRaModulationParams(float bw,uint8_t sf,uint8_t cr) { + assert(bw==62.5f && sf==7 && cr==5);return call('M'); + } + int setSpreadingFactor(uint8_t sf) {assert(sf==7);return call('S');} + int setBandwidth(float bw) {assert(bw==62.5f);return call('B');} + int setCodingRate(uint8_t cr) {assert(cr==5);return call('C');} +}; +struct Wrapper { + @CHIP@ radio;@CHIP@* _radio=&radio;uint8_t features=0; + uint8_t getDriverSwitchFeatures() const {return features;} + @METHOD@ +}; +int main() { + for(unsigned flags=0;flags<128;++flags) for(char failure:std::string("0MSBC")) { + Wrapper w;w.features=flags;w.radio.failure=failure; + const bool batch=flags & mesh::RadioSwitchTuning::Batch; + assert(w.applyProfileModulation(62.5f,7,5)==(batch ? failure!='M' + : failure!='S' && failure!='B' && failure!='C')); + assert(w.radio.trace==(batch ? "M":failure=='S'?"S":failure=='B'?"SB":"SBC")); + } +} +''' + for family in ("SX1262", "LR1110"): + with self.subTest(family=family): + source = (ROOT / f"src/helpers/radiolib/Custom{family}Wrapper.h").read_text() + self.compile_run(harness.replace("@CHIP@", f"Custom{family}") + .replace("@METHOD@", method(source, "bool applyProfileModulation("))) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_radio_switch_tuning_cli.py b/test/test_radio_switch_tuning_cli.py new file mode 100644 index 000000000..84ba606a7 --- /dev/null +++ b/test/test_radio_switch_tuning_cli.py @@ -0,0 +1,59 @@ +"""Exercise saved fast-switch controls through the production shared CLI.""" +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import unittest +from cpp_source import body + +ROOT = Path(__file__).resolve().parents[1] + + +class RadioSwitchTuningCLITest(unittest.TestCase): + def test_five_cli_roles_restore_tuning_before_first_radio_initialization(self): + for role in ('companion_radio', 'simple_repeater', 'simple_room_server', + 'simple_sensor', 'simple_secure_chat'): + with self.subTest(role=role): + source = (ROOT / f'examples/{role}/main.cpp').read_text() + setup = body(source, '\nvoid setup(') + setup = re.sub(r'//[^\n]*|/\*.*?\*/', '', setup, flags=re.S) + hook = 'mesh::RadioProfileCLI::configureBootTuning(' + self.assertEqual(setup.count(hook), 1) + if role != 'simple_secure_chat': + self.assertLess(setup.index('beginRegulatorSettings('), setup.index(hook)) + self.assertRegex(setup, r'configureBootTuning\(regulator_prefs_fs,\s*radio_driver\)') + else: + self.assertRegex(setup, r'configureBootTuning\(\s*volatile_primary_fs\s*\?\s*nullptr\s*:\s*primary_fs,\s*radio_driver\)') + for mount in ('beginInternalPrimaryFilesystemSafely(', + 'LittleFS.begin()', 'SPIFFS.begin(true)'): + self.assertEqual(setup.count(mount), 1) + self.assertLess(setup.index(mount), setup.index(hook)) + self.assertEqual(setup.count('the_mesh.begin('), 1) + self.assertLess(setup.index('fast_rng.begin('), setup.index('the_mesh.begin(')) + radio_init = re.search(r'\bradio_init\s*\(', setup) + self.assertIsNotNone(radio_init) + self.assertLess(setup.index(hook), radio_init.start()) + if role == 'simple_secure_chat': + self.assertLess(radio_init.start(), setup.index('fast_rng.begin(')) + + def test_saved_controls_and_failure_transactions(self): + compiler = shutil.which('g++') or shutil.which('clang++') + self.assertIsNotNone(compiler) + with tempfile.TemporaryDirectory() as work: + for fleet_enabled in (False, True): + with self.subTest(fleet=fleet_enabled): + executable = Path(work) / f'switch-{int(fleet_enabled)}' + subprocess.run([ + compiler, '-std=c++17', '-Wall', '-Wextra', + f'-DMESH_ENABLE_FLEET_CONTROL={int(fleet_enabled)}', + '-I', str(ROOT / 'test/fixtures/radio_profiles/mocks'), + '-I', str(ROOT / 'test/mocks'), '-I', str(ROOT / 'src'), + str(ROOT / 'src/helpers/RadioProfileCLI.cpp'), + str(ROOT / 'test/fixtures/radio_profiles/switch_tuning_test.cpp'), + '-o', str(executable)], check=True, capture_output=True, text=True) + subprocess.run([str(executable)], check=True) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_radio_switch_tuning_transaction.py b/test/test_radio_switch_tuning_transaction.py new file mode 100644 index 000000000..f99bf7672 --- /dev/null +++ b/test/test_radio_switch_tuning_transaction.py @@ -0,0 +1,91 @@ +"""Run the actual tuning transaction with radio ownership and RX/save faults.""" +from pathlib import Path +import unittest +from test_radio_receive_contract import method +from test_radio_interrupt_recovery import compile_run + +ROOT = Path(__file__).resolve().parents[1] + + +class RadioSwitchTransactionTest(unittest.TestCase): + def test_admission_restore_and_commit_order(self): + source = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.cpp').read_text() + transaction = method(source, 'mesh::RadioParamApplyResult RadioLibWrapper::trySetSwitchTuning(') + compile_run(r''' +#include +#include +#include +namespace mesh { enum class RadioParamApplyResult { APPLIED, BUSY, FAILED }; } +using Result=mesh::RadioParamApplyResult; +struct RadioLibWrapper { + mesh::RadioSwitchTuning policy; + struct { unsigned resets=0; void resetSwitchTest(){++resets;} } _profiles; + uint8_t admission=1; + bool receiving=true, fail_rx=false, inject_packet=false, pending_packet=false; + unsigned admissions=0, resumes=0, mutations=0; + mesh::RadioSwitchCapabilities switchTuningCapabilities()const { + mesh::RadioSwitchCapabilities caps; caps.flags=63; return caps; + } + mesh::RadioSwitchTuning switchTuning()const{return policy;} + uint8_t beginReconfigure(){++admissions;return pending_packet?2:admission;} + void setDriverSwitchFeatures(uint8_t value){++mutations;policy.flags=value;} + bool configureBootSwitchTuning(const mesh::RadioSwitchTuning& value){ + setDriverSwitchFeatures(value.flags);return true; + } + void endReconfigure(bool resume) { + if(resume){++resumes;receiving=!fail_rx;if(inject_packet)pending_packet=true;} + } + bool isInRecvMode()const{return receiving;} + Result trySetSwitchTuning(const mesh::RadioSwitchTuning&, + bool(*)(void*,const mesh::RadioSwitchTuning&)=nullptr,void*=nullptr); +}; +struct Commit { + RadioLibWrapper* radio; + bool fail=false; + unsigned calls=0; + static bool save(void* context,const mesh::RadioSwitchTuning& value){ + auto& self=*static_cast(context); + ++self.calls; + assert(self.radio->receiving && self.radio->policy==value); + return !self.fail; + } +}; +'''.replace('void*=nullptr', 'void* = nullptr') + transaction + r''' +int main(){ + mesh::RadioSwitchTuning requested;requested.flags=63; + for(uint8_t rejected:{uint8_t(2),uint8_t(3)}){ + RadioLibWrapper radio;radio.admission=rejected;Commit store{&radio}; + auto result=radio.trySetSwitchTuning(requested,Commit::save,&store); + assert(result==(rejected==2?Result::BUSY:Result::FAILED)); + assert(!radio.policy.flags && !radio.mutations && !store.calls && !radio._profiles.resets); + } + { + RadioLibWrapper radio;radio.pending_packet=true;Commit store{&radio}; + assert(radio.trySetSwitchTuning(requested,Commit::save,&store)==Result::BUSY); + assert(radio.pending_packet && !radio.mutations && !radio.resumes && !store.calls); + } + { + RadioLibWrapper radio;Commit store{&radio}; + auto invalid=requested;invalid.flags|=64; + assert(radio.trySetSwitchTuning(invalid,Commit::save,&store)==Result::FAILED); + assert(!radio.admissions && !radio.mutations && !store.calls); + assert(radio.trySetSwitchTuning(requested,Commit::save,&store)==Result::APPLIED); + assert(radio.policy==requested && radio._profiles.resets==1 && store.calls==1); + } + { + RadioLibWrapper radio;radio.fail_rx=true;Commit store{&radio}; + assert(radio.trySetSwitchTuning(requested,Commit::save,&store)==Result::FAILED); + assert(!radio.policy.flags && !store.calls && radio.mutations==2); + } + { + RadioLibWrapper radio;radio.inject_packet=true;Commit store{&radio};store.fail=true; + assert(radio.trySetSwitchTuning(requested,Commit::save,&store)==Result::FAILED); + assert(!radio.policy.flags && store.calls==1 && radio.pending_packet); + assert(radio.receiving && radio.resumes==1 && radio.mutations==2); + } +} +''') + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_rak3401_buffered_radio_hal.py b/test/test_rak3401_buffered_radio_hal.py index 618d87c89..5996c0b19 100644 --- a/test/test_rak3401_buffered_radio_hal.py +++ b/test/test_rak3401_buffered_radio_hal.py @@ -42,7 +42,9 @@ struct SPIClass { } uint8_t transfer(uint8_t value) { ++bytes; return value ^ 0xa5; } }; -struct ArduinoHal { +struct RadioLibHal { virtual ~RadioLibHal()=default; }; +struct ArduinoHal : RadioLibHal { + bool initInterface = false; SPIClass* spi; SPISettings spiSettings; explicit ArduinoHal(SPIClass& bus, SPISettings settings) diff --git a/test/test_sx1262_batched_modulation.py b/test/test_sx1262_batched_modulation.py index e59f7e853..461afdda4 100644 --- a/test/test_sx1262_batched_modulation.py +++ b/test/test_sx1262_batched_modulation.py @@ -16,7 +16,7 @@ class BatchedModulationTests(unittest.TestCase): cpp, exe = Path(folder) / "test.cpp", Path(folder) / "test.exe" cpp.write_text(source) built = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17", - "-Wall", "-Wextra", str(cpp), "-o", str(exe)], + "-Wall", "-Wextra", "-I", str(ROOT / "src"), str(cpp), "-o", str(exe)], capture_output=True, text=True) self.assertEqual(built.returncode, 0, built.stderr) ran = subprocess.run([str(exe)], capture_output=True, text=True) @@ -27,6 +27,7 @@ class BatchedModulationTests(unittest.TestCase): "int16_t setLoRaModulationParams(") harness = r''' #include +#include #include #include #include @@ -51,6 +52,7 @@ using std::isfinite; #define RADIOLIB_SX126X_LORA_BW_500_0 0x06 struct Radio { SX1262ProfileSwitchState _profileSwitch{true}; + uint8_t _profileSwitchFeatures=63; uint8_t spreadingFactor=9, bandwidth=4, codingRate=3, ldrOptimize=0; float bandwidthKhz=125; bool ldroAuto=true; @@ -181,6 +183,7 @@ int main() { state = (ROOT / "src/helpers/radiolib/SX1262ProfileSwitchState.h").read_text().replace("#pragma once", "") harness = r''' #include +#include #include @STATE@ struct SX1262 { @@ -193,6 +196,7 @@ struct SX1262 { }; struct Radio: SX1262 { SX1262ProfileSwitchState _profileSwitch{true}; + uint8_t _profileSwitchFeatures=63; @METHODS@ }; int main() { @@ -218,13 +222,18 @@ int main() { def test_wrapper_batches_once_and_preserves_failure_short_circuit(self): source = (ROOT / "src/helpers/radiolib/CustomSX1262Wrapper.h").read_text() - production = method(source, "bool applyParams(").replace(" override", "") + production = "\n".join(method(source, signature).replace(" override", "") + for signature in ("bool applyProfileModulation(", "bool applyParams(")) harness = r''' #include +#include #include #define RADIOLIB_ERR_NONE 0 struct CustomSX1262 { - unsigned freq=0,mod=0,limits=0;int failure=0; + unsigned freq=0,mod=0,limits=0,ordinary=0;int failure=0; + int setSpreadingFactor(uint8_t sf) { assert(sf==8);++ordinary;return failure==2?-1:0; } + int setBandwidth(float bw) { assert(bw==500);++ordinary;return 0; } + int setCodingRate(uint8_t cr) { assert(cr==5);++ordinary;return 0; } bool profileSwitchFailed() const { return failure==4; } int setFrequency(float f) { assert(f==910.5f);++freq;return failure==1?-1:0; } int setLoRaModulationParams(float bw,uint8_t sf,uint8_t cr) { @@ -236,6 +245,8 @@ struct CustomSX1262 { struct PacketMillis { uint32_t preambleMillis,payloadMillis; }; struct Wrapper { CustomSX1262 chip;CustomSX1262* _radio=&chip;unsigned preamble=0,calcs=0; + uint8_t features=mesh::RadioSwitchTuning::Batch; + uint8_t getDriverSwitchFeatures() const { return features; } bool updatePreamble(uint8_t sf,float bw) { assert(sf==8 && bw==500);++preamble;return chip.failure!=3; } @@ -256,6 +267,10 @@ int main() { assert(w.chip.mod==(failure==1?0u:1u)); assert(w.preamble==((failure==1 || failure==2)?0u:1u)); assert(w.calcs==(failure?0u:1u) && w.chip.limits==(failure?0u:2u)); + Wrapper slow;slow.features=0;slow.chip.failure=failure; + assert(slow.applyParams(910.5f,500,8,5)==(failure==0)); + assert(slow.chip.mod==0 && slow.chip.ordinary==(failure==1?0u:failure==2?1u:3u)); + assert(slow.preamble==((failure==1 || failure==2)?0u:1u)); } } ''' diff --git a/test/test_sx1262_modem_cache.py b/test/test_sx1262_modem_cache.py index 90250f504..916401141 100644 --- a/test/test_sx1262_modem_cache.py +++ b/test/test_sx1262_modem_cache.py @@ -15,17 +15,23 @@ class ModemCacheTests(unittest.TestCase): def test_confirmed_modem_requires_owned_warm_rx_and_survives_no_failure(self): source = (ROOT / "src/helpers/radiolib/CustomSX1262.h").read_text() state = (ROOT / "src/helpers/radiolib/SX1262ProfileSwitchState.h").read_text().replace("#pragma once", "") - signatures = ("void invalidateMorseModemCache(", "void setProfileSwitchOptimization(", + signatures = ("uint8_t defaultProfileSwitchFeatures(", "uint8_t profileSwitchCapabilities(", "uint8_t getProfileSwitchFeatures(", + "void setProfileSwitchFeatures(", "void invalidateMorseModemCache(", "void setProfileSwitchOptimization(", "int16_t begin(const ConfigLoRa_t&", "int16_t beginFSK(const ConfigFSK_t&", "int16_t beginLRFHSS(const ConfigLRFHSS_t&", "int16_t setModem(", "int16_t setLoRaModulationParams(") production = "\n".join(method(source, signature).replace(" override", "") for signature in signatures) fixture = r''' #include +#include #include #include #include @DEFINES@ +#define MC_SX1262_FAST_PROFILE_SWITCH 1 +#define MC_SX1262_FAST_FS 1 +#define MC_SX1262_FAST_PACKET_CACHE 1 +#define MC_SX1262_FAST_IQ_CACHE 1 @STATE@ using std::isfinite; #define RADIOLIB_ERR_NONE 0 @@ -67,6 +73,7 @@ struct SX1262 { }; struct Radio : SX1262 { SX1262ProfileSwitchState _profileSwitch{true}; + uint8_t _profileSwitchFeatures=defaultProfileSwitchFeatures(); bool _coldStandby = false, _tcxoWakePending = true; uint8_t spreadingFactor = 9, bandwidth = 4, codingRate = 3, ldrOptimize = 0; float bandwidthKhz = 125; @@ -76,6 +83,7 @@ struct Radio : SX1262 { int16_t writeResult = 0; #ifdef MC_SX1262_HIL bool hilSkipConfirmedModemQuery = false; + bool hilUseFsOnRetune=false,hilSkipUnchangedPacket=false,hilSkipUnchangedIq=false; uint32_t hilModemQuerySkips = 0; #endif using SX1262::beginFSK; @@ -91,19 +99,11 @@ struct Radio : SX1262 { @PRODUCTION@ }; bool skipsModem(const Radio& r) { -#ifdef MC_SX1262_HIL - return r.hilSkipConfirmedModemQuery; -#else - (void)r; - return MC_SX1262_FAST_MODEM_CACHE != 0; -#endif + return (r.getProfileSwitchFeatures() & mesh::RadioSwitchTuning::Modem) != 0; } void selectCache(Radio& r, bool enabled) { -#ifdef MC_SX1262_HIL - r.hilSkipConfirmedModemQuery = enabled; -#else - (void)r; (void)enabled; -#endif + const auto flags=r.getProfileSwitchFeatures() & ~mesh::RadioSwitchTuning::Modem; + r.setProfileSwitchFeatures(flags | (enabled ? mesh::RadioSwitchTuning::Modem : 0)); } void prime(Radio& r) { // Models the full RX path's successful live LoRa confirmation. The ordinary @@ -126,7 +126,7 @@ int main() { assert(r.setLoRaModulationParams(500, 8, 5) == 0 && r.writes == 2); assert(r.queries == (skipsModem(r) ? 1U : 3U)); #ifdef MC_SX1262_HIL - assert(r.hilModemQuerySkips == (requested ? 2U : 0U)); + assert(r.hilModemQuerySkips == (skipsModem(r) ? 2U : 0U)); #endif // Every ownership gate independently restores the live query. for (int gate = 0; gate < 9; ++gate) { diff --git a/test/test_sx1262_profile_switch.py b/test/test_sx1262_profile_switch.py index 0dd385f11..fdcfd7181 100644 --- a/test/test_sx1262_profile_switch.py +++ b/test/test_sx1262_profile_switch.py @@ -37,6 +37,7 @@ class FastProfileSwitchTests(unittest.TestCase): state = (ROOT / "src/helpers/radiolib/SX1262ProfileSwitchState.h").read_text() harness = r''' #include +#include #include #include @STATE@ @@ -66,7 +67,8 @@ int main() { def test_owned_rx_window_commands_errors_and_lifecycle_invalidation(self): src = (ROOT / "src/helpers/radiolib/CustomSX1262.h").read_text() state = (ROOT / "src/helpers/radiolib/SX1262ProfileSwitchState.h").read_text().replace("#pragma once", "") - signatures = ["void setProfileSwitchOptimization(", "void beginProfileSwitch(", + signatures = ["uint8_t defaultProfileSwitchFeatures(", "uint8_t profileSwitchCapabilities(", "uint8_t getProfileSwitchFeatures(", + "void setProfileSwitchFeatures(", "void setProfileSwitchOptimization(", "void beginProfileSwitch(", "void endProfileSwitch(", "bool profileSwitchFailed(", "int16_t restoreTcxoAfterSleep(", "int16_t standby()", "int16_t standby(uint8_t", "int16_t sleep()", "int16_t sleep(bool", "int16_t reset(bool", "int16_t stageMode(", @@ -77,10 +79,14 @@ int main() { methods = "\n".join(method(src, signature).replace(" override", "") for signature in signatures) harness = r''' #include +#include #include #include #include @STATE@ +#define MC_SX1262_FAST_PROFILE_SWITCH 1 +#define MC_SX1262_FAST_IQ_CACHE 0 +#define MC_SX1262_FAST_MODEM_CACHE 0 #define MC_SX1262_FAST_FS @FS@ #define MC_SX1262_FAST_PACKET_CACHE @PACKET@ #define RADIOLIB_ERR_NONE 0 @@ -142,7 +148,8 @@ struct SX1262 { }; struct Radio: SX1262 { uint32_t _activityAt=0;bool _headerSeen=false; - SX1262ProfileSwitchState _profileSwitch{true};bool _rx_ps_rf_rx_disabled=false;uint32_t _profileFastResumes=0; + SX1262ProfileSwitchState _profileSwitch{true}; + uint8_t _profileSwitchFeatures=defaultProfileSwitchFeatures();bool _rx_ps_rf_rx_disabled=false;uint32_t _profileFastResumes=0; bool _coldStandby=true; bool _tcxoWakePending=false; @METHODS@ @@ -232,6 +239,27 @@ int main() { fault.errors=32;fault.trace.clear();assert(fault.standby()==0 && fault.trace=="UEV"); assert(!fault._tcxoWakePending); Radio wrong;wrong.modem=0;assert(wrong.startReceive()==0 && !wrong._profileSwitch.rxValid); + // Every supported feature is independently selectable. Unsupported request + // bits are masked before ownership changes; setters issue no radio command. + for(unsigned requested=0;requested<256;++requested) { + Radio x;x.setProfileSwitchFeatures(uint8_t(requested)); + const uint8_t features=requested & x.profileSwitchCapabilities(); + assert(x.getProfileSwitchFeatures()==features && x.trace.empty()); + assert(x._profileSwitch.enabled==bool(features & mesh::RadioSwitchTuning::Warm)); + assert(!x._profileSwitch.rxValid && !x._profileSwitch.packetValid + && !x._profileSwitch.modulationValid); + assert(x.startReceive()==0);x.trace.clear();x.beginProfileSwitch(true); + assert(x.standby()==0); + const bool warm=features & mesh::RadioSwitchTuning::Warm; + const bool fs=features & mesh::RadioSwitchTuning::Fs; + assert(x.trace==(warm ? (fs ? "If":"s") : "S"));x.trace.clear(); + assert(x.startReceive()==0); + const bool packet=features & mesh::RadioSwitchTuning::Packet; + assert(x.trace==(warm ? (packet ? "CWR":"CPWR") : "GSF")); + x.endProfileSwitch(true);x.trace.clear(); + x.setProfileSwitchFeatures(features); + assert(x.trace.empty() && !x._profileSwitch.rxValid && !x._profileSwitch.packetValid); + } Radio off;off.setProfileSwitchOptimization(false);assert(off.startReceive()==0); assert(off.trace=="GUF" && !off._profileSwitch.rxValid); } diff --git a/test/test_sx1262_protocol_batching.py b/test/test_sx1262_protocol_batching.py index 35ca4d32a..c68504daa 100644 --- a/test/test_sx1262_protocol_batching.py +++ b/test/test_sx1262_protocol_batching.py @@ -19,6 +19,7 @@ class ProtocolBatchingTests(unittest.TestCase): "int16_t setProfileProtocol(", "int16_t startReceive()")) harness = r''' #include +#include #include #include #include @@ -86,6 +87,8 @@ struct SX1262 { struct Radio: SX1262 { uint32_t _activityAt=0;bool _headerSeen=false; SX1262ProfileSwitchState _profileSwitch{true}; + uint8_t _profileSwitchFeatures=63; + uint8_t getProfileSwitchFeatures() const { return _profileSwitchFeatures; } bool _coldStandby=false;uint32_t _profileFastResumes=0; @METHODS@ }; diff --git a/test/test_sx126x_buffered_radio_hal.py b/test/test_sx126x_buffered_radio_hal.py index f0af406db..0a36239bd 100644 --- a/test/test_sx126x_buffered_radio_hal.py +++ b/test/test_sx126x_buffered_radio_hal.py @@ -41,7 +41,8 @@ struct SPIClass { #endif uint8_t transfer(uint8_t value) { ++bytes; return value^0xa5; } }; -struct ArduinoHal { +struct RadioLibHal { virtual ~RadioLibHal()=default; }; +struct ArduinoHal : RadioLibHal { SPIClass* spi; SPISettings spiSettings; bool initInterface=false; explicit ArduinoHal(SPIClass& bus,SPISettings settings) : spi(&bus),spiSettings(settings) {} @@ -92,6 +93,63 @@ int main() { hal.spiTransfer(nullptr,0,nullptr); assert(explicitBus.bursts==before && explicitBus.bytes==0); assert(unused.bursts==0 && fallbackBus.bursts==0); + + using Tuning=mesh::RadioSwitchTuning; + assert(TunableRadioHal::find(&hal)==&external); + assert(external.defaultHz()==MC_SX1262_SPI_HZ); + assert(external.effectiveHz()==MC_SX1262_SPI_HZ); + assert(external.getTuning().spi_mhz==0); + assert(external.getTuning().flags==Tuning::Buffer); + assert(external.capabilities().flags==Tuning::Buffer); + assert(external.capabilities().spi_clocks==(2|4|8)); + // Disable bulk transfers without changing the controller, bus lifetime or + // packet response. Driver-owned feature bits are handled by the wrapper. + Tuning tuning; + tuning.flags=Tuning::Warm; + tuning.spi_mhz=2; + assert(external.setTuning(tuning)); + assert(external.getTuning().flags==0 && external.effectiveHz()==2000000); + assert(external.spiSettings.hz==2000000 && hal.spi==&explicitBus); + const unsigned bytes=explicitBus.bytes; + rx.fill(0x77); + hal.spiTransfer(tx.data(),7,rx.data()); + assert(explicitBus.bursts==before && explicitBus.bytes==bytes+7); + for (unsigned i=0;i<7;++i) assert(rx[i]==uint8_t(tx[i]^0xa5)); + for (unsigned i=7;iowned && radio.mod->hal->spi==&SPI); - assert(radio.mod->hal->spiSettings.hz==2000000); + assert(!radio.mod->owned && radio.mod->hal==&radio_hal && radio.mod->hal->spi==&SPI); + assert(radio.mod->hal->spiSettings.hz==8000000); + assert(radio_hal.capabilities().flags==0 && radio_hal.capabilities().spi_clocks==(2|4|8)); assert(radio.mod->hal->initInterface=={str(owns).lower()}); radio.mod->hal->init();radio.mod->hal->term(); assert(SPI.begins=={int(owns)} && SPI.ends=={int(owns)}); @@ -163,15 +167,17 @@ void check() {{ fixture+="\nint main() {"+"".join(f"{name}::check();" for name,_ in cases)+"}\n" self.compile_run("NRF52_PLATFORM",fixture) - def test_muzi_uno_and_lr1121_duo_keep_prior_constructor(self): + def test_muzi_uno_and_lr1121_duo_keep_bus_and_gain_tunable_byte_clock(self): text=(ROOT/"variants/muzi_base/target.cpp").read_text() - constructor=re.search(r"^RADIO_CLASS radio = new Module\(P_LORA_NSS[^;]+;",text,re.MULTILINE) + constructor=re.search(r"^static TunableRadioHal radio_hal\([^;]+;\n" + r"RADIO_CLASS radio = new Module\(&radio_hal,[^;]+;",text,re.MULTILINE) self.assertIsNotNone(constructor) self.assertNotIn("BufferedRadioHal",text) fixture=PREFIX+"\nSPIClass SPI;\n"+constructor.group(0)+r''' int main() { assert(radio.mod->hal->spi==&SPI); - assert(radio.mod->owned && radio.mod->hal->spiSettings.hz==2000000); + assert(!radio.mod->owned && radio.mod->hal->spiSettings.hz==8000000); + assert(TunableRadioHal::find(radio.mod->hal)==&radio_hal); } ''' for definitions in ((),("USE_SX1262",)): @@ -180,7 +186,8 @@ int main() { def test_lr1110_m4_keeps_manual_spi_start_and_prior_2mhz_byte_path(self): text=(ROOT/"variants/thinknode_m4/target.cpp").read_text() - construction=re.search(r"^RADIO_CLASS radio = new Module\(P_LORA_NSS[^;]+;",text,re.MULTILINE) + construction=re.search(r"^static TunableRadioHal radio_hal\([^;]+;\n" + r"RADIO_CLASS radio = new Module\(&radio_hal,[^;]+;",text,re.MULTILINE) self.assertIsNotNone(construction) self.assertNotIn("BufferedRadioHal",text) self.assertIn("SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);",text) @@ -188,7 +195,7 @@ int main() { self.assertIn("int status = radio.begin(",text) fixture=PREFIX+"\nSPIClass SPI;\n"+construction.group(0)+r''' int main() { - assert(radio.mod->owned && radio.mod->hal->spi==&SPI); + assert(!radio.mod->owned && radio.mod->hal==&radio_hal && radio.mod->hal->spi==&SPI); assert(radio.mod->hal->spiSettings.hz==2000000 && !radio.mod->hal->initInterface); radio.mod->hal->init();radio.mod->hal->term(); assert(SPI.begins==0 && SPI.ends==0); diff --git a/test/test_xiao_nrf52_buffered_spi.py b/test/test_xiao_nrf52_buffered_spi.py index 36a5e6462..5c822bb63 100644 --- a/test/test_xiao_nrf52_buffered_spi.py +++ b/test/test_xiao_nrf52_buffered_spi.py @@ -73,6 +73,7 @@ class XiaoNrf52BufferedSpiTests(unittest.TestCase): #include #include #include "XiaoRadioSpiConfig.h" +#include #define SPI_INTERFACES_COUNT 2 #define PIN_SPI_MISO 9 #define PIN_SPI_MOSI 10 @@ -118,7 +119,7 @@ int main() { assert(radio.module->hal->spi==&SPI); assert(SPI.device->number==(MC_XIAO_NRF52_HIL_FAST_SPI?2:3)); assert(SPI1.device->number==(MC_XIAO_NRF52_HIL_FAST_SPI?3:2)); - assert(radio.module->hal->spiSettings.hz==(MC_XIAO_NRF52_HIL_FAST_SPI?8000000:2000000)); + assert(radio.module->hal->spiSettings.hz==8000000); assert(!radio.module->hal->initInterface && SPI.begins==0 && SPI1.begins==0); radio.module->hal->init();radio.module->hal->term(); assert(SPI.begins==0 && SPI.ends==0); // The board still owns bus lifetime. @@ -187,7 +188,7 @@ int main() { self.assertIn(diagnostic, result.stderr) def test_default_candidate_has_no_code_or_sdk_dependency_and_recipes_do_not_enable_it(self): - result = subprocess.run([os.environ.get("CXX", "g++"), "-E", "-P", "-x", "c++", str(CONFIG)], + result = subprocess.run([os.environ.get("CXX", "g++"), "-E", "-P", "-x", "c++", "-I", str(ROOT / "src"), str(CONFIG)], capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stderr) self.assertFalse(result.stdout.strip()) diff --git a/variants/gat562_30s_mesh_kit/target.cpp b/variants/gat562_30s_mesh_kit/target.cpp index 8f84a7a19..7510d8fe4 100644 --- a/variants/gat562_30s_mesh_kit/target.cpp +++ b/variants/gat562_30s_mesh_kit/target.cpp @@ -1,5 +1,6 @@ #include #include "target.h" +#include #include GAT56230SMeshKitBoard board; @@ -18,7 +19,8 @@ GAT56230SMeshKitBoard board; #endif -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/gat562_mesh_evb_pro/target.cpp b/variants/gat562_mesh_evb_pro/target.cpp index 8dc203de9..262f468e5 100644 --- a/variants/gat562_mesh_evb_pro/target.cpp +++ b/variants/gat562_mesh_evb_pro/target.cpp @@ -1,5 +1,6 @@ #include #include "target.h" +#include #include GAT562EVBProBoard board; @@ -17,7 +18,8 @@ GAT562EVBProBoard board; #endif -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/gat562_mesh_tracker_pro/target.cpp b/variants/gat562_mesh_tracker_pro/target.cpp index d16b45381..0b3b00a08 100644 --- a/variants/gat562_mesh_tracker_pro/target.cpp +++ b/variants/gat562_mesh_tracker_pro/target.cpp @@ -1,5 +1,6 @@ #include #include "target.h" +#include #include GAT562MeshTrackerProBoard board; @@ -18,7 +19,8 @@ GAT562MeshTrackerProBoard board; #endif -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/gat562_mesh_watch13/target.cpp b/variants/gat562_mesh_watch13/target.cpp index 95bcf6dc2..fbde2b35e 100644 --- a/variants/gat562_mesh_watch13/target.cpp +++ b/variants/gat562_mesh_watch13/target.cpp @@ -1,5 +1,6 @@ #include #include "target.h" +#include #include GAT56MeshWatch13Board board; @@ -15,7 +16,8 @@ GAT56MeshWatch13Board board; #endif -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/heltec_mesh_solar/target.cpp b/variants/heltec_mesh_solar/target.cpp index dd95882ba..a6a9d99ff 100644 --- a/variants/heltec_mesh_solar/target.cpp +++ b/variants/heltec_mesh_solar/target.cpp @@ -1,11 +1,13 @@ #include #include "target.h" +#include #include #include MeshSolarBoard board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/heltec_t096/target.cpp b/variants/heltec_t096/target.cpp index 0ff5b7dba..97ae842a1 100644 --- a/variants/heltec_t096/target.cpp +++ b/variants/heltec_t096/target.cpp @@ -1,4 +1,5 @@ #include "target.h" +#include #include #include @@ -10,9 +11,11 @@ T096Board board; #if defined(P_LORA_SCLK) -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); #else -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +static TunableRadioHal radio_hal(SPI, 8000000, true); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); #endif WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/heltec_t1/target.cpp b/variants/heltec_t1/target.cpp index af771582d..2165fe50f 100644 --- a/variants/heltec_t1/target.cpp +++ b/variants/heltec_t1/target.cpp @@ -1,4 +1,5 @@ #include "target.h" +#include #include #include @@ -10,9 +11,11 @@ T1Board board; #if defined(P_LORA_SCLK) -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); #else -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +static TunableRadioHal radio_hal(SPI, 8000000, true); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); #endif WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/heltec_t114/target.cpp b/variants/heltec_t114/target.cpp index 90a902f80..868e46b1f 100644 --- a/variants/heltec_t114/target.cpp +++ b/variants/heltec_t114/target.cpp @@ -1,4 +1,5 @@ #include "target.h" +#include #include #include @@ -10,9 +11,11 @@ T114Board board; #if defined(P_LORA_SCLK) -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); #else -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +static TunableRadioHal radio_hal(SPI, 8000000, true); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); #endif WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/heltec_tower_v2/target.cpp b/variants/heltec_tower_v2/target.cpp index a3838e4b8..652b85706 100644 --- a/variants/heltec_tower_v2/target.cpp +++ b/variants/heltec_tower_v2/target.cpp @@ -1,4 +1,5 @@ #include "target.h" +#include #include #include @@ -14,7 +15,8 @@ HeltecTowerV2Board board; static Nrf52BufferedRadioHal radioHal(SPI, 8000000); RADIO_CLASS radio = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); #else -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); #endif WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/ikoka_handheld_nrf/target.cpp b/variants/ikoka_handheld_nrf/target.cpp index 6c53c5e32..47a11d6bb 100644 --- a/variants/ikoka_handheld_nrf/target.cpp +++ b/variants/ikoka_handheld_nrf/target.cpp @@ -1,10 +1,12 @@ #include #include "target.h" +#include #include IkokaNrf52Board board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/ikoka_nano_nrf/target.cpp b/variants/ikoka_nano_nrf/target.cpp index baf02c3d1..4f3fc577c 100644 --- a/variants/ikoka_nano_nrf/target.cpp +++ b/variants/ikoka_nano_nrf/target.cpp @@ -1,5 +1,6 @@ #include #include "target.h" +#include #include IkokaNanoNRFBoard board; @@ -9,7 +10,8 @@ IkokaNanoNRFBoard board; // MomentaryButton user_btn(PIN_USER_BTN, 1000, true); #endif -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/ikoka_stick_nrf/target.cpp b/variants/ikoka_stick_nrf/target.cpp index bb140505a..aa3414f77 100644 --- a/variants/ikoka_stick_nrf/target.cpp +++ b/variants/ikoka_stick_nrf/target.cpp @@ -1,5 +1,6 @@ #include #include "target.h" +#include #include IkokaStickNRFBoard board; @@ -9,7 +10,8 @@ IkokaStickNRFBoard board; MomentaryButton user_btn(PIN_USER_BTN, 1000, true); #endif -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/keepteen_lt1/target.cpp b/variants/keepteen_lt1/target.cpp index 72c627b41..2d5b7c446 100644 --- a/variants/keepteen_lt1/target.cpp +++ b/variants/keepteen_lt1/target.cpp @@ -1,10 +1,12 @@ #include #include "target.h" +#include #include KeepteenLT1Board board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/lilygo_t_impulse_plus/target.cpp b/variants/lilygo_t_impulse_plus/target.cpp index 87b3dbfed..e65b52a9c 100644 --- a/variants/lilygo_t_impulse_plus/target.cpp +++ b/variants/lilygo_t_impulse_plus/target.cpp @@ -1,11 +1,13 @@ #include #include "target.h" +#include #include #include TImpulsePlusBoard board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/lilygo_tdeck/target.cpp b/variants/lilygo_tdeck/target.cpp index 513f89ba0..195481b66 100644 --- a/variants/lilygo_tdeck/target.cpp +++ b/variants/lilygo_tdeck/target.cpp @@ -1,5 +1,6 @@ #include #include "target.h" +#include #include TDeckBoard board; @@ -9,7 +10,8 @@ TDeckBoard board; static ESP32BufferedRadioHal radio_hal(spi); RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); #else - RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); + static TunableRadioHal radio_hal(SPI, MC_SX1262_SPI_HZ, true); + RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); #endif WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/lilygo_techo/target.cpp b/variants/lilygo_techo/target.cpp index 278fc76da..ce2484566 100644 --- a/variants/lilygo_techo/target.cpp +++ b/variants/lilygo_techo/target.cpp @@ -1,11 +1,13 @@ #include #include "target.h" +#include #include #include TechoBoard board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/lilygo_techo_card/target.cpp b/variants/lilygo_techo_card/target.cpp index 8bbe03633..216954fbd 100644 --- a/variants/lilygo_techo_card/target.cpp +++ b/variants/lilygo_techo_card/target.cpp @@ -1,11 +1,13 @@ #include #include "target.h" +#include #include #include TechoCardBoard board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/lilygo_techo_lite/target.cpp b/variants/lilygo_techo_lite/target.cpp index 95a62fcd2..8f7885d9d 100644 --- a/variants/lilygo_techo_lite/target.cpp +++ b/variants/lilygo_techo_lite/target.cpp @@ -1,11 +1,13 @@ #include #include "target.h" +#include #include #include TechoBoard board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/mesh_pocket/target.cpp b/variants/mesh_pocket/target.cpp index ee8f944de..3702f2852 100644 --- a/variants/mesh_pocket/target.cpp +++ b/variants/mesh_pocket/target.cpp @@ -1,11 +1,13 @@ #include #include "target.h" +#include #include #include HeltecMeshPocket board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/meshtiny/target.cpp b/variants/meshtiny/target.cpp index 50d678266..3c5c2a7e3 100644 --- a/variants/meshtiny/target.cpp +++ b/variants/meshtiny/target.cpp @@ -1,11 +1,13 @@ #include "target.h" +#include #include #include MeshtinyBoard board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/muzi_base/target.cpp b/variants/muzi_base/target.cpp index 3d89a7c27..538c2c8ea 100644 --- a/variants/muzi_base/target.cpp +++ b/variants/muzi_base/target.cpp @@ -1,11 +1,13 @@ #include #include #include "target.h" +#include #include "variant.h" MuziBaseBoard board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/muziworks_r1_neo/target.cpp b/variants/muziworks_r1_neo/target.cpp index b0840a34e..ea1f254d5 100644 --- a/variants/muziworks_r1_neo/target.cpp +++ b/variants/muziworks_r1_neo/target.cpp @@ -1,10 +1,12 @@ #include #include "target.h" +#include #include R1NeoBoard board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); #ifdef DISPLAY_CLASS diff --git a/variants/nano_g2_ultra/target.cpp b/variants/nano_g2_ultra/target.cpp index 04b373341..5d074d433 100644 --- a/variants/nano_g2_ultra/target.cpp +++ b/variants/nano_g2_ultra/target.cpp @@ -1,4 +1,5 @@ #include "target.h" +#include #include #include @@ -6,7 +7,8 @@ NanoG2Ultra board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/promicro/target.cpp b/variants/promicro/target.cpp index 065d628f1..cfe676872 100644 --- a/variants/promicro/target.cpp +++ b/variants/promicro/target.cpp @@ -1,10 +1,12 @@ #include #include "target.h" +#include #include PromicroBoard board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/rak4631/target.cpp b/variants/rak4631/target.cpp index a41ba7207..e9b0b983a 100644 --- a/variants/rak4631/target.cpp +++ b/variants/rak4631/target.cpp @@ -1,5 +1,6 @@ #include #include "target.h" +#include #include RAK4631Board board; @@ -17,7 +18,8 @@ RAK4631Board board; #endif #endif -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/rak_wismesh_tag/target.cpp b/variants/rak_wismesh_tag/target.cpp index 2fe81c371..32cf06c74 100644 --- a/variants/rak_wismesh_tag/target.cpp +++ b/variants/rak_wismesh_tag/target.cpp @@ -1,5 +1,6 @@ #include #include "target.h" +#include #include #include #if MC_NRF52_HIL_SPIM1_RADIO @@ -21,7 +22,8 @@ RAKWismeshTagBoard board; static Nrf52BufferedRadioHal radioHal(SPI, 8000000); RADIO_CLASS radio = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); #else -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); #endif WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/sensecap_solar/target.cpp b/variants/sensecap_solar/target.cpp index 022e8a9b4..fd58b2bfd 100644 --- a/variants/sensecap_solar/target.cpp +++ b/variants/sensecap_solar/target.cpp @@ -1,5 +1,6 @@ #include #include "target.h" +#include #include #if ENV_INCLUDE_GPS @@ -8,7 +9,8 @@ SenseCapSolarBoard board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/thinknode_m1/target.cpp b/variants/thinknode_m1/target.cpp index 2cfab4bae..da93d8f91 100644 --- a/variants/thinknode_m1/target.cpp +++ b/variants/thinknode_m1/target.cpp @@ -1,11 +1,13 @@ #include #include "target.h" +#include #include #include ThinkNodeM1Board board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/thinknode_m4/target.cpp b/variants/thinknode_m4/target.cpp index 7da3f400d..6e0bc9802 100644 --- a/variants/thinknode_m4/target.cpp +++ b/variants/thinknode_m4/target.cpp @@ -1,10 +1,12 @@ #include #include "target.h" +#include #include ThinkNodeM4Board board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI, 2000000); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/thinknode_m6/target.cpp b/variants/thinknode_m6/target.cpp index de167194e..a5f0a5ce7 100644 --- a/variants/thinknode_m6/target.cpp +++ b/variants/thinknode_m6/target.cpp @@ -1,11 +1,13 @@ #include #include "target.h" +#include #include #include ThinkNodeM6Board board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/thinknode_m8/target.cpp b/variants/thinknode_m8/target.cpp index ac4ac100e..4961d06e8 100644 --- a/variants/thinknode_m8/target.cpp +++ b/variants/thinknode_m8/target.cpp @@ -1,12 +1,14 @@ #include #include "target.h" +#include #include #include #include ThinkNodeM8Board board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/wio-tracker-l1-1w/target.cpp b/variants/wio-tracker-l1-1w/target.cpp index 7a573258e..04709260c 100644 --- a/variants/wio-tracker-l1-1w/target.cpp +++ b/variants/wio-tracker-l1-1w/target.cpp @@ -1,11 +1,13 @@ #include #include "target.h" +#include #include #include WioTrackerL1Board board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/wio-tracker-l1/target.cpp b/variants/wio-tracker-l1/target.cpp index f82feff9d..5bfc5693c 100644 --- a/variants/wio-tracker-l1/target.cpp +++ b/variants/wio-tracker-l1/target.cpp @@ -1,5 +1,6 @@ #include #include "target.h" +#include #include #ifdef ENV_INCLUDE_GPS @@ -8,7 +9,8 @@ WioTrackerL1Board board; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/xiao_nrf52/target.cpp b/variants/xiao_nrf52/target.cpp index b32663fef..3d26dccef 100644 --- a/variants/xiao_nrf52/target.cpp +++ b/variants/xiao_nrf52/target.cpp @@ -1,5 +1,6 @@ #include #include "target.h" +#include #include "XiaoRadioSpiConfig.h" #include #ifdef USE_CC310_HW_CRYPTO @@ -21,7 +22,8 @@ XiaoNrf52Board board; static Nrf52BufferedRadioHal radioHal(SPI, 8000000); RADIO_CLASS radio = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); #else -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +static TunableRadioHal radio_hal(SPI); +RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); #endif WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/xiao_s3_wio/target.cpp b/variants/xiao_s3_wio/target.cpp index cb59840eb..d121a00a0 100644 --- a/variants/xiao_s3_wio/target.cpp +++ b/variants/xiao_s3_wio/target.cpp @@ -1,5 +1,6 @@ #include #include "target.h" +#include #include XiaoS3WIOBoard board; @@ -9,7 +10,8 @@ XiaoS3WIOBoard board; static ESP32BufferedRadioHal radio_hal(spi); RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); #else - RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); + static TunableRadioHal radio_hal(SPI, MC_SX1262_SPI_HZ, true); + RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); #endif WRAPPER_CLASS radio_driver(radio, board);