Add saved controls for radio switching acceleration

This commit is contained in:
mikecarper committed 2026-10-23 10:41:26 -07:00
1 parent 5a4e81c4d5
commit 61d024bc72
78 files changed
+1696 -160

No files matched your search

+3
View File
@@ -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
+25
View File
@@ -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.<feature>` | Read an individual feature |
| `set radio.fast.<feature> 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
+66 -3
View File
@@ -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,
+5 -1
View File
@@ -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 <on|off> / 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 <off|on|level 1-10 [preamble 16|32]|rx_us sleep_us>\r\n"
+1
View File
@@ -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
+1
View File
@@ -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");
+1
View File
@@ -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
+21 -18
View File
@@ -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());
+1
View File
@@ -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
+11
View File
@@ -5,6 +5,7 @@
#include <Packet.h>
#include <Utils.h>
#include <RadioProfiles.h>
#include <helpers/RadioSwitchTuning.h>
#include <helpers/MeshtextConfig.h>
#include <helpers/MorseTx.h>
#include <helpers/MorseRx.h>
@@ -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; }
+174 -8
View File
@@ -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<RadioProfileCLI*>(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"
+13
View File
@@ -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);
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <stdint.h>
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
+24 -7
View File
@@ -6,6 +6,7 @@
#include "RXPowerSaving.h"
#include "LR1110RxRecovery.h"
#include "LR1110ProfileSwitchState.h"
#include <helpers/RadioSwitchTuning.h>
#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);
}
+19 -9
View File
@@ -35,6 +35,15 @@ public:
void setDeepInitCallback(DeepInitCallback callback) { _deep_init = callback; }
protected:
uint8_t getDriverSwitchFeatures() const override {
return static_cast<const CustomLR1110*>(_radio)->getProfileSwitchFeatures();
}
uint8_t driverSwitchCapabilities() const override {
return static_cast<const CustomLR1110*>(_radio)->profileSwitchCapabilities();
}
void setDriverSwitchFeatures(uint8_t features) override {
static_cast<CustomLR1110*>(_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<CustomLR1110*>(_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<CustomLR1110*>(_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));
+57 -11
View File
@@ -6,6 +6,7 @@
#include "MeshCore.h"
#include "RXPowerSaving.h"
#include "SX1262ProfileSwitchState.h"
#include <helpers/RadioSwitchTuning.h>
#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,
+20 -2
View File
@@ -17,6 +17,15 @@ class CustomSX1262Wrapper : public RadioLibWrapper {
bool _saved_standby_xosc = false;
protected:
uint8_t getDriverSwitchFeatures() const override {
return static_cast<const CustomSX1262*>(_radio)->getProfileSwitchFeatures();
}
uint8_t driverSwitchCapabilities() const override {
return static_cast<const CustomSX1262*>(_radio)->profileSwitchCapabilities();
}
void setDriverSwitchFeatures(uint8_t features) override {
static_cast<CustomSX1262*>(_radio)->setProfileSwitchFeatures(features);
}
int8_t readReceiveMode() override { return sx126xReceiveMode((SX126x*)_radio); }
void beginProfileRetune(bool continuousRx) override {
static_cast<CustomSX1262*>(_radio)->beginProfileSwitch(continuousRx);
@@ -125,13 +134,22 @@ protected:
#endif
}
bool applyProfileModulation(float bw, uint8_t sf, uint8_t cr) {
auto* radio = static_cast<CustomSX1262*>(_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<CustomSX1262*>(_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;
+6 -4
View File
@@ -1,5 +1,5 @@
#pragma once
#include <hal/Arduino/ArduinoHal.h>
#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);
}
};
+6 -4
View File
@@ -1,6 +1,6 @@
#pragma once
#include <hal/Arduino/ArduinoHal.h>
#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
+67
View File
@@ -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();
+10
View File
@@ -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; }
@@ -11,15 +11,16 @@
#include "ESP32BufferedRadioHal.h"
using SX126xBufferedRadioHalBase = ESP32BufferedRadioHal;
#elif defined(RP2040_PLATFORM)
#include <hal/Arduino/ArduinoHal.h>
#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
+81
View File
@@ -0,0 +1,81 @@
#pragma once
#include <hal/Arduino/ArduinoHal.h>
#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 = &registry();
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<RadioLibHal*>(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<uint32_t>(_spiMHz) * 1000000UL : _defaultHz;
}
};
+400
View File
@@ -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<uint8_t>& 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();
}
+12 -3
View File
@@ -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 <on|off> / set radio.fast.spi <0|2|4|8|16>\r\n')
HARNESS = r'''
#include "CompanionHelpWriter.h"
#include <algorithm>
@@ -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__':
+1 -1
View File
@@ -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 ""
+3 -1
View File
@@ -37,6 +37,7 @@ class LR1110PacketSetterCacheTests(unittest.TestCase):
state = (ROOT / "src/helpers/radiolib/LR1110ProfileSwitchState.h").read_text()
harness = r"""
#include <cassert>
#include <helpers/RadioSwitchTuning.h>
#include <cstddef>
#include <cstdint>
#include <string>
@@ -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)
+50 -6
View File
@@ -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 <cassert>
#include <helpers/RadioSwitchTuning.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
#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 <cassert>
#include <helpers/RadioSwitchTuning.h>
#include <cstdint>
#include <cstddef>
#include <string>
@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 <cassert>
#include <helpers/RadioSwitchTuning.h>
#include <cstdint>
@STATE@
int main() {
@@ -279,10 +311,12 @@ int main() {
)
harness = r'''
#include <cassert>
#include <helpers/RadioSwitchTuning.h>
#include <cmath>
#include <cstdint>
#include <initializer_list>
@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))
+1 -1
View File
@@ -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));
+1 -1
View File
@@ -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();
+14 -2
View File
@@ -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 <cassert>
#include <helpers/RadioSwitchTuning.h>
#include <cstdint>
#include <cstddef>
#include <string>
#include <vector>
@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());
+4
View File
@@ -24,6 +24,7 @@ def build_source(family='CustomSX1262',remove_reset=False,include_duty=False):
source=source.replace('#include <cassert>', '''#include <cassert>
#include <cstdio>
#include <RadioProfiles.h>
#include <helpers/RadioSwitchTuning.h>
#include <helpers/radiolib/NoiseFloorEstimator.h>
#include <helpers/radiolib/SX1262ProfileSwitchState.h>
#include <helpers/radiolib/LR1110ProfileSwitchState.h>
@@ -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;}
+143
View File
@@ -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 <cassert>
#include <helpers/RadioSwitchTuning.h>
@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 <cassert>
#include <helpers/RadioSwitchTuning.h>
#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 <cassert>
#include <string>
#include <helpers/RadioSwitchTuning.h>
#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()
+59
View File
@@ -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()
@@ -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 <cassert>
#include <initializer_list>
#include <helpers/RadioSwitchTuning.h>
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<Commit*>(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()
+3 -1
View File
@@ -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)
+18 -3
View File
@@ -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 <cassert>
#include <helpers/RadioSwitchTuning.h>
#include <cmath>
#include <cstdint>
#include <cstddef>
@@ -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 <cassert>
#include <helpers/RadioSwitchTuning.h>
#include <cstdint>
@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 <cassert>
#include <helpers/RadioSwitchTuning.h>
#include <cstdint>
#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));
}
}
'''
+13 -13
View File
@@ -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 <cassert>
#include <helpers/RadioSwitchTuning.h>
#include <cmath>
#include <cstdint>
#include <cstddef>
@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) {
+30 -2
View File
@@ -37,6 +37,7 @@ class FastProfileSwitchTests(unittest.TestCase):
state = (ROOT / "src/helpers/radiolib/SX1262ProfileSwitchState.h").read_text()
harness = r'''
#include <cassert>
#include <helpers/RadioSwitchTuning.h>
#include <cstddef>
#include <cstdint>
@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 <cassert>
#include <helpers/RadioSwitchTuning.h>
#include <cstdint>
#include <cstddef>
#include <string>
@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);
}
+3
View File
@@ -19,6 +19,7 @@ class ProtocolBatchingTests(unittest.TestCase):
"int16_t setProfileProtocol(", "int16_t startReceive()"))
harness = r'''
#include <cassert>
#include <helpers/RadioSwitchTuning.h>
#include <cstdint>
#include <cstddef>
#include <string>
@@ -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@
};
+64 -3
View File
@@ -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;i<rx.size();++i) assert(rx[i]==0x77);
for (uint8_t clock : {uint8_t(4),uint8_t(8)}) {
tuning.spi_mhz=clock;
assert(external.setTuning(tuning));
assert(external.effectiveHz()==unsigned(clock)*1000000);
assert(external.spiSettings.hz==external.effectiveHz());
}
const Tuning previous=external.getTuning();
for (uint8_t clock : {uint8_t(1),uint8_t(3),uint8_t(16),uint8_t(255)}) {
tuning.spi_mhz=clock;
assert(!external.setTuning(tuning) && external.getTuning()==previous);
}
tuning.spi_mhz=0;
tuning.flags=Tuning::Buffer;
assert(external.setTuning(tuning));
assert(external.effectiveHz()==MC_SX1262_SPI_HZ);
assert(external.spiSettings.hz==MC_SX1262_SPI_HZ);
hal.spiTransfer(tx.data(),7,rx.data());
assert(explicitBus.bursts==before+1 && explicitBus.bytes==bytes+7);
assert(explicitBus.begins==1 && explicitBus.ends==1);
// The plain nRF SPIM3 byte route exposes clock selection but never admits
// the buffered path that needs a separately qualified controller mapping.
{
TunableRadioHal byteHal(unused);
assert(TunableRadioHal::find(&byteHal)==&byteHal);
assert(byteHal.capabilities().flags==0 && byteHal.defaultHz()==8000000);
assert(!byteHal.setTuning(tuning));
tuning.flags=0;tuning.spi_mhz=2;
assert(byteHal.setTuning(tuning) && byteHal.spiSettings.hz==2000000);
}
ArduinoHal foreign(unused,SPISettings(2000000,MSBFIRST,SPI_MODE0));
assert(TunableRadioHal::find(&foreign)==nullptr);
assert(TunableRadioHal::find(nullptr)==nullptr);
assert(TunableRadioHal::find(&external)==&external);
}
'''
@@ -101,12 +159,15 @@ class SX126xBufferedRadioHalTests(unittest.TestCase):
compiler=shutil.which(os.environ.get("CXX","g++"))
self.assertIsNotNone(compiler)
headers={name:(RADIO/name).read_text(encoding="ascii") for name in (
"SX126xBufferedRadioHal.h","Nrf52BufferedRadioHal.h","ESP32BufferedRadioHal.h")}
"SX126xBufferedRadioHal.h","Nrf52BufferedRadioHal.h","ESP32BufferedRadioHal.h",
"TunableRadioHal.h")}
with tempfile.TemporaryDirectory(prefix="sx126x-buffered-radio-") as folder:
base=Path(folder)
(base/"hal/Arduino").mkdir(parents=True)
(base/"helpers/radiolib").mkdir(parents=True)
(base/"hal/Arduino/ArduinoHal.h").write_text(MOCK,encoding="ascii")
(base/"helpers/RadioSwitchTuning.h").write_text(
(ROOT/"src/helpers/RadioSwitchTuning.h").read_text(),encoding="ascii")
cpp,executable=base/"test.cpp",base/"test"
cpp.write_text(FIXTURE,encoding="ascii")
for arch,header in (("NRF52_PLATFORM","Nrf52BufferedRadioHal.h"),
@@ -125,7 +186,7 @@ class SX126xBufferedRadioHalTests(unittest.TestCase):
implementation if file==header else content,encoding="ascii")
built=subprocess.run([compiler,"-std=c++17","-Wall","-Wextra",
"-Werror",f"-D{arch}",f"-DMC_SX1262_SPI_HZ={clock}",
"-I",str(base),str(cpp),"-o",str(executable)],
"-I",str(base),"-I",str(ROOT/"src"),"-I",str(RADIO),str(cpp),"-o",str(executable)],
capture_output=True,text=True,timeout=60)
self.assertEqual(built.returncode,0,built.stdout+built.stderr)
ran=subprocess.run([str(executable)],capture_output=True,
+19 -12
View File
@@ -1,4 +1,4 @@
"""Compile actual conservative nRF52 and buffered RP2040 radio constructors."""
"""Compile actual tunable nRF52 and buffered RP2040 radio constructors."""
from pathlib import Path
import os
import re
@@ -118,7 +118,7 @@ void check() {{
fixture+="\nint main() {"+"".join(f"{name}::check();" for name,_ in cases)+"}\n"
self.compile_run(arch,fixture)
def test_all_new_nrf52_spim3_defaults_keep_prior_2mhz_byte_path(self):
def test_all_nrf52_spim3_defaults_use_tunable_8mhz_byte_path(self):
cases=[]
sources=target_sources({"nrf52_base","rak4631_hw"})
self.assertEqual(len(sources),32)
@@ -134,12 +134,15 @@ void check() {{
self.assertIn("#if MC_NRF52_HIL_SPIM1_RADIO",text)
else:
self.assertNotIn("BufferedRadioHal",text,str(source))
constructions=re.findall(r"^RADIO_CLASS radio = new Module\(P_LORA_NSS[^;]+;",text,re.MULTILINE)
constructions=re.findall(r"^static TunableRadioHal radio_hal\(([^;]+)\);\n"
r"(RADIO_CLASS radio = new Module\(&radio_hal,[^;]+;)",
text,re.MULTILINE)
fallback=source.parent.name in {"heltec_t096","heltec_t1","heltec_t114"}
self.assertEqual(len(constructions),2 if fallback else 1,str(source))
for ordinal,construction in enumerate(constructions):
for ordinal,(arguments,module) in enumerate(constructions):
owns=fallback and ordinal==1
self.assertEqual(construction.endswith(", SPI);"),not owns,str(source))
self.assertEqual(arguments, "SPI, 8000000, true" if owns else "SPI",str(source))
construction=f"static TunableRadioHal radio_hal({arguments});\n{module}"
name=f"case_{len(cases)}"
cases.append((name,f'''
namespace {name} {{
@@ -147,8 +150,9 @@ SPIClass SPI;
struct BindDefaultBus {{ BindDefaultBus() {{testDefaultBus=&SPI;}} }} bindDefaultBus;
{construction}
void check() {{
assert(radio.mod->owned && 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);
+3 -2
View File
@@ -73,6 +73,7 @@ class XiaoNrf52BufferedSpiTests(unittest.TestCase):
#include <cstdint>
#include <hal/Arduino/ArduinoHal.h>
#include "XiaoRadioSpiConfig.h"
#include <helpers/radiolib/TunableRadioHal.h>
#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())
+3 -1
View File
@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
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);
+3 -1
View File
@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
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);
+3 -1
View File
@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
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);
+3 -1
View File
@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
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);
+3 -1
View File
@@ -1,11 +1,13 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
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);
+5 -2
View File
@@ -1,4 +1,5 @@
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <Arduino.h>
#include <helpers/ArduinoHelpers.h>
@@ -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);
+5 -2
View File
@@ -1,4 +1,5 @@
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <Arduino.h>
#include <helpers/ArduinoHelpers.h>
@@ -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);
+5 -2
View File
@@ -1,4 +1,5 @@
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <Arduino.h>
#include <helpers/ArduinoHelpers.h>
@@ -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);
+3 -1
View File
@@ -1,4 +1,5 @@
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <Arduino.h>
#include <helpers/ArduinoHelpers.h>
@@ -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);
+3 -1
View File
@@ -1,10 +1,12 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
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);
+3 -1
View File
@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
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);
+3 -1
View File
@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
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);
+3 -1
View File
@@ -1,10 +1,12 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
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);
+3 -1
View File
@@ -1,11 +1,13 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
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);
+3 -1
View File
@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/radiolib/ESP32BufferedRadioHal.h>
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);
+3 -1
View File
@@ -1,11 +1,13 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
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);
+3 -1
View File
@@ -1,11 +1,13 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
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);
+3 -1
View File
@@ -1,11 +1,13 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
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);
+3 -1
View File
@@ -1,11 +1,13 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
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);
+3 -1
View File
@@ -1,11 +1,13 @@
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <Arduino.h>
#include <helpers/ArduinoHelpers.h>
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);
+3 -1
View File
@@ -1,11 +1,13 @@
#include <Arduino.h>
#include <nrf_gpio.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#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);
+3 -1
View File
@@ -1,10 +1,12 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
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
+3 -1
View File
@@ -1,4 +1,5 @@
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <Arduino.h>
#include <helpers/ArduinoHelpers.h>
@@ -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);
+3 -1
View File
@@ -1,10 +1,12 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
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);
+3 -1
View File
@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
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);
+3 -1
View File
@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/nrf52/Nrf52HilSpim1Owner.h>
#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);
+3 -1
View File
@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
#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);
+3 -1
View File
@@ -1,11 +1,13 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
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);
+3 -1
View File
@@ -1,10 +1,12 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
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);
+3 -1
View File
@@ -1,11 +1,13 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
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);
+3 -1
View File
@@ -1,12 +1,14 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
#include <Wire.h>
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);
+3 -1
View File
@@ -1,11 +1,13 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
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);
+3 -1
View File
@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/ArduinoHelpers.h>
#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);
+3 -1
View File
@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include "XiaoRadioSpiConfig.h"
#include <helpers/ArduinoHelpers.h>
#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);
+3 -1
View File
@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/TunableRadioHal.h>
#include <helpers/radiolib/ESP32BufferedRadioHal.h>
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);