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

+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;
}
};