Add two-profile LoRa operation and radio CLI regression coverage

Implement radio2, tempradio2, radioat2 and tempradioat2 across Mesh roles,
with RX-only/RX+TX operation, optional preambles and persistent cross-TX
policy. Keep temporary OTA traffic on its profile by default and maintain
independent retry ownership and backoff for each profile.

Use slow-first receive scanning with 4.8-symbol visits and automatic
preambles rounded up in steps of eight. Preserve pending RX, restore power
saving on exit, and discard work bound to changed or expired profiles.

Restore the infrastructure path.hash.mode setter and report unsupported
extra.sf settings consistently. Add CLI, scheduling, scan and retry tests,
setup documentation, and the V4/XIAO hardware validation results.

Validation: 1,422 native tests, eight KISS tests, 63 final focused profile
tests, 18 checks from the staged source, sanitizer-enabled OTA transfers,
and builds for V4 Mesh roles, Full XIAO Companion and nRF52 T1000-E.
Hardware checks cover reception, cross-TX policy, expiry, reboot and OTA
discovery while receiving main-channel adverts.
This commit is contained in:
mikecarper committed 2026-09-13 08:25:00 -07:00
1 parent a25adbde64
commit 088c88507a
46 files changed
+5187 -143

No files matched your search

+55 -9
View File
@@ -1692,6 +1692,7 @@ void MyMesh::begin(bool has_display, bool radio_available) {
initializeContactStorage();
initializeOfflineQueue();
BaseChatMesh::begin();
_radio_profiles.begin(_store->getPrimaryFS(), _radio, getRTCClock());
const bool identity_loaded = _store->loadMainIdentity(self_id);
const bool is_new_install = !identity_loaded
@@ -2024,7 +2025,7 @@ void MyMesh::activateRadio() {
MESH_DEBUG_PRINTLN("Radio recovery completed; mesh transport is active");
}
mesh::RadioParamApplyResult MyMesh::tryApplyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr) {
mesh::RadioParamApplyResult MyMesh::tryApplyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, bool temporary, uint16_t preamble) {
if (!_radio_available) return mesh::RadioParamApplyResult::FAILED;
uint32_t rx_us = _prefs.rx_ps_rx_us;
@@ -2037,7 +2038,8 @@ mesh::RadioParamApplyResult MyMesh::tryApplyRadioParams(float freq, float bw, ui
uint32_t timings[2] = {rx_us, sleep_us};
const uint32_t* applied_timings = _prefs.rx_powersaving_enabled
&& radio_driver.supportsRxPowerSaving() ? timings : NULL;
return radio_driver.trySetParams(freq, bw, sf, cr, applied_timings);
return _radio_profiles.applyPrimary(freq, bw, sf, cr, temporary,
temporary ? preamble : _radio_profiles.primaryPreamble(), applied_timings);
}
bool MyMesh::applySavedRadioParams() {
@@ -3051,12 +3053,20 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
snprintf(reply, reply_size, "TempRadio pending: %.3f,%.2f,%u,%u",
_temp_radio_freq, _temp_radio_bw,
(unsigned)_temp_radio_sf, (unsigned)_temp_radio_cr);
mesh::RadioProfiles preview;
if (_radio->profiles()) preview = *_radio->profiles();
preview.primary.freq = _temp_radio_freq; preview.primary.bw = _temp_radio_bw;
preview.primary.sf = _temp_radio_sf; preview.primary.cr = _temp_radio_cr;
preview.primary.preamble = _temp_radio_preamble;
const size_t used = strlen(reply);
if (used < reply_size) snprintf(reply + used, reply_size - used, ",preamble=%u", preview.preamble(0, rxPowerSavingPreambleForParams(_temp_radio_sf, _temp_radio_bw)));
} else if (isTempRadioActive()) {
uint32_t seconds = (_temp_radio_revert_at - _ms->getMillis()) / 1000UL;
snprintf(reply, reply_size, "TempRadio active: %.3f,%.2f,%u,%u %lus left",
_temp_radio_freq, _temp_radio_bw,
(unsigned)_temp_radio_sf, (unsigned)_temp_radio_cr,
(unsigned long)seconds);
_radio_profiles.appendPreamble(reply, reply_size);
} else {
snprintf(reply, reply_size, "TempRadio inactive");
}
@@ -3064,21 +3074,27 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
}
if (strncmp(command, "tempradio ", 10) == 0) {
char legacy[120];
uint16_t preamble = 0;
if (!mesh::RadioProfileCLI::parseSuffix(command + 10, 5, legacy, sizeof(legacy), preamble)) {
snprintf(reply, reply_size, "Error: invalid preamble"); return true;
}
float freq = 0.0f, bw = 0.0f;
uint8_t sf = 0, cr = 0;
uint32_t timeout_mins = 0;
if (!mesh::cli::parseTemporaryRadioTupleStrict(
command + 10, freq, bw, sf, cr, timeout_mins)
legacy, freq, bw, sf, cr, timeout_mins)
|| !isfinite(freq) || !isfinite(bw)
|| freq < 150.0f || freq > 2500.0f
|| !isFullCompanionBandwidth(bw)
|| sf < 5 || sf > 12 || cr < 5 || cr > 8
|| timeout_mins == 0 || timeout_mins > 10080UL) {
|| timeout_mins == 0 || timeout_mins > 10080UL
|| !_radio_profiles.acceptsPrimary(freq, bw, sf, cr, preamble)) {
snprintf(reply, reply_size,
"ERR usage: tempradio freq,bw,sf,cr,minutes (minutes 1-10080)");
return true;
}
scheduleTempRadio(freq, bw, sf, cr, timeout_mins, reply, reply_size);
if (scheduleTempRadio(freq, bw, sf, cr, timeout_mins, reply, reply_size)) _temp_radio_preamble = preamble;
return true;
}
@@ -3136,7 +3152,7 @@ void MyMesh::serviceTempRadio() {
|| hasOutbound() || !retry_ready) return;
mesh::RadioParamApplyResult result = tryApplyRadioParams(
_temp_radio_freq, _temp_radio_bw, _temp_radio_sf, _temp_radio_cr);
_temp_radio_freq, _temp_radio_bw, _temp_radio_sf, _temp_radio_cr, true, _temp_radio_preamble);
if (result == mesh::RadioParamApplyResult::APPLIED) {
_temp_radio_set_at = 0;
_temp_radio_retry_at = 0;
@@ -8185,6 +8201,28 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
while (*command == ' ' || *command == '\t') command++;
}
if (_radio_profiles.handle(command, reply, reply_capacity)) return true;
if (!strncmp(command, "set tempradio ", 14)) command += 4;
if (!strcmp(command, "get tempradio")) command += 4;
char profile_command[140];
const uint16_t previous_primary_preamble = _radio_profiles.primaryPreamble();
if (!strncmp(command, "set radio ", 10)) {
char legacy[120];
uint16_t preamble = 0;
float f, bw; uint8_t sf, cr;
if (!mesh::RadioProfileCLI::parseSuffix(command + 10, 4, legacy, sizeof(legacy), preamble)
|| !mesh::cli::parseRadioTupleStrict(legacy, f, bw, sf, cr)) {
snprintf(reply, reply_capacity, "Error: invalid radio params"); return true;
}
if (!_radio_profiles.acceptsPrimary(f, bw, sf, cr, preamble)) {
snprintf(reply, reply_capacity, "Error: radio params unsupported"); return true;
}
if (!_radio_profiles.savePrimaryPreamble(preamble)) {
snprintf(reply, reply_capacity, "Error: preamble could not be saved"); return true;
}
snprintf(profile_command, sizeof(profile_command), "set radio %s", legacy);
command = profile_command;
}
if (sender_timestamp == 0 && handleDirectCommand(command, reply, reply_capacity)) return true;
#if COMPANION_FEATURE_USB_MOTA_SOURCE
@@ -8283,9 +8321,15 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
_prefs.rx_ps_rx_us = previous_rx_us;
_prefs.rx_ps_sleep_us = previous_sleep_us;
_prefs.clearDirty();
strcpy(reply, "Error: setting changed but save failed");
if (strncmp(command, "set radio ", 10) == 0
&& !_radio_profiles.savePrimaryPreamble(previous_primary_preamble)) {
strcpy(reply, "Error: radio save failed; preamble rollback failed");
} else {
strcpy(reply, "Error: setting changed but save failed");
}
}
}
if (!strcmp(command, "get radio")) _radio_profiles.appendSavedPreamble(reply, reply_capacity, _prefs.sf, _prefs.bw);
return true;
}
@@ -8610,6 +8654,7 @@ void MyMesh::checkSerialInterface() {
}
void MyMesh::loop() {
_radio_profiles.loop();
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
if (_mqtt_bridge) _mqtt_bridge->servicePendingClockCorrection();
#endif
@@ -8628,9 +8673,9 @@ void MyMesh::loop() {
serviceTempRadio();
#endif
#if defined(OTA_SHARED_COMPANION_QUEUE)
mesh::ota::ota_release_context_if_idle(isTempRadioActive() || _temp_radio_set_at != 0);
mesh::ota::ota_release_context_if_idle(isAnyTempRadioActive() || _temp_radio_set_at != 0);
#elif defined(ENABLE_OTA) && defined(OTA_HEAP_CONTEXT)
mesh::ota::ota_service_temp_radio_context(isTempRadioActive()
mesh::ota::ota_service_temp_radio_context(isAnyTempRadioActive()
#if COMPANION_FEATURE_TEMP_RADIO
|| _temp_radio_set_at != 0
#endif
@@ -8721,6 +8766,7 @@ bool MyMesh::advert() {
// To check if there is pending work
bool MyMesh::hasPendingWork() const {
if (isDualRadioActive()) return true;
if (_radio_available
&& (radio_driver.isWatchdogObserving()
|| radio_driver.isCalibratingNoiseFloor())) return true;
+4 -1
View File
@@ -25,6 +25,7 @@
#endif
#include "DataStore.h"
#include <helpers/RadioProfileCLI.h>
#include "NodePrefs.h"
#if defined(ESP32_PLATFORM) && defined(WIFI_SSID) && !defined(WEBCONFIG_DISABLED)
@@ -438,7 +439,7 @@ private:
bool isValidClientRepeatFreq(uint32_t f) const;
bool hasLocationTelemetryRecipient();
void updateGpsTelemetryPolicy();
mesh::RadioParamApplyResult tryApplyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr);
mesh::RadioParamApplyResult tryApplyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, bool temporary = false, uint16_t preamble = 0);
bool applySavedRadioParams();
void configureRadioFromPrefs();
void finishRadioParamApply(float freq, float bw, uint8_t sf, uint8_t cr,
@@ -480,6 +481,8 @@ private:
#endif
DataStore* _store;
mesh::RadioProfileCLI _radio_profiles;
uint16_t _temp_radio_preamble = 0;
CompanionNodePrefs _prefs;
#ifdef COMPANION_MESH_CLOCK_SYNC
ArduinoMillis _clock_sync_millis;
+38 -17
View File
@@ -1279,7 +1279,7 @@ bool MyMesh::evaluateScopeRewriteTiming(const mesh::Packet* packet,
bool MyMesh::shouldBypassRxDelay(const mesh::Packet* packet) {
if (packet != NULL && packet->getPayloadType() == PAYLOAD_TYPE_OTA
&& isTempRadioActive()) return true;
&& isPacketOnTempRadio(packet)) return true;
bool fast_track = false;
return evaluateScopeRewriteTiming(packet, fast_track) && fast_track;
}
@@ -1287,7 +1287,7 @@ bool MyMesh::shouldBypassRxDelay(const mesh::Packet* packet) {
int MyMesh::calcRxDelayForPacket(const mesh::Packet* packet, float score,
uint32_t air_time) {
if (packet != NULL && packet->getPayloadType() == PAYLOAD_TYPE_OTA
&& isTempRadioActive()) return 0;
&& isPacketOnTempRadio(packet)) return 0;
bool fast_track = false;
if (!evaluateScopeRewriteTiming(packet, fast_track)) {
return calcRxDelay(score, air_time);
@@ -1917,7 +1917,8 @@ MyMesh::FloodRetryBridgeState* MyMesh::floodRetryBridgeStateFor(const mesh::Pack
uint8_t key[MAX_HASH_SIZE];
packet->calculatePacketHash(key);
FloodRetryBridgeState* free_slot = NULL;
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_FLOOD_RETRY_SLOTS; i++) {
if (i / MAX_FLOOD_RETRY_SLOTS != packet->radio_profile) continue;
if (flood_retry_bridge_states[i].active
&& memcmp(flood_retry_bridge_states[i].key, key, MAX_HASH_SIZE) == 0) {
return &flood_retry_bridge_states[i];
@@ -1980,11 +1981,12 @@ bool MyMesh::prepareFloodRetry(const mesh::Packet* packet) const {
return floodRetryBridgeStateFor(packet, true) != NULL;
}
void MyMesh::clearFloodRetryBridgeStateByKey(const uint8_t* retry_key) {
void MyMesh::clearFloodRetryBridgeStateByKey(const uint8_t* retry_key, uint8_t radio_profile) {
if (retry_key == NULL) {
return;
}
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_FLOOD_RETRY_SLOTS; i++) {
if (i / MAX_FLOOD_RETRY_SLOTS != radio_profile) continue;
if (flood_retry_bridge_states[i].active
&& memcmp(flood_retry_bridge_states[i].key, retry_key, MAX_HASH_SIZE) == 0) {
flood_retry_bridge_states[i].active = false;
@@ -2234,8 +2236,8 @@ void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, ui
}
void MyMesh::onFloodRetrySlotReleased(const uint8_t* retry_key) {
clearFloodRetryBridgeStateByKey(retry_key);
void MyMesh::onFloodRetrySlotReleased(const uint8_t* retry_key, uint8_t radio_profile) {
clearFloodRetryBridgeStateByKey(retry_key, radio_profile);
}
bool MyMesh::hasFloodRetryTargetPrefix(const mesh::Packet* packet) const {
@@ -2869,7 +2871,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
} else if (!deferred_cli_command.enqueue(i, sender_timestamp,
packet->getPathHashSize(), secret,
command, command_len,
request_id)) {
request_id, packet->radio_profile,
packet->radio_generation)) {
const char* error = deferred_cli_command.pending
? "Err - another remote command is still running"
: "Err - remote command is too long";
@@ -3004,6 +3007,9 @@ bool MyMesh::completeHostCliRequest(const char* service_reply) {
reply, mesh::HostCliBridge::REMOTE_REPLY_MAX + 1U,
deferred_cli_command.command, service_reply);
ReceiveProfileScope radio_scope(*this, deferred_cli_command.radio_profile,
deferred_cli_command.radio_generation);
const uint32_t command_fingerprint =
mesh::RemoteCliReplyCache::fingerprint(
deferred_cli_command.command,
@@ -3063,6 +3069,8 @@ bool MyMesh::handleHostCliSerialReply(const char* command, char* reply) {
void __attribute__((noinline)) MyMesh::processDeferredCliCommand() {
if (!deferred_cli_command.pending) return;
ReceiveProfileScope radio_scope(*this, deferred_cli_command.radio_profile,
deferred_cli_command.radio_generation);
const int client_index = deferred_cli_command.client_index;
if (client_index < 0 || client_index >= acl.getNumClients()) {
@@ -4027,7 +4035,7 @@ void MyMesh::setTempRadioTiming(uint32_t duration_seconds) {
updateFloodAdvertTimer();
}
bool MyMesh::applyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr) {
bool MyMesh::applyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, uint16_t preamble, bool temporary) {
uint32_t rx_us = _prefs.rx_ps_rx_us;
uint32_t sleep_us = _prefs.rx_ps_sleep_us;
if (_prefs.rx_powersaving_enabled && _prefs.rx_ps_level != 0) {
@@ -4039,8 +4047,8 @@ bool MyMesh::applyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr) {
uint32_t timings[2] = {rx_us, sleep_us};
const uint32_t* applied_timings = _prefs.rx_powersaving_enabled
&& radio_driver.supportsRxPowerSaving() ? timings : NULL;
if (!radio_driver.setParams(freq, bw, sf, cr,
applied_timings)) {
if (_cli.radioProfiles().applyPrimary(freq, bw, sf, cr, temporary, preamble,
applied_timings) != mesh::RadioParamApplyResult::APPLIED) {
MESH_DEBUG_PRINTLN("Radio schedule: radio busy or parameter apply failed");
return false;
}
@@ -4068,7 +4076,7 @@ bool MyMesh::applySavedRadioParams() {
}
#endif
if (!applyRadioParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr)) return false;
if (!applyRadioParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr, _cli.radioProfiles().primaryPreamble())) return false;
#if defined(USE_LR2021)
return radio_driver.configSideDetectors(_prefs.extra_sf, extra_sf_count, _prefs.bw);
@@ -4292,7 +4300,13 @@ void MyMesh::formatRadioParamTuple(char* dest, size_t dest_len, const ScheduledR
char bw[16];
formatFixed3(freq, sizeof(freq), setting.freq);
StrHelper::strncpy(bw, StrHelper::ftoa3(setting.bw), sizeof(bw));
snprintf(dest, dest_len, "%s,%s,%u,%u", freq, bw, (uint32_t)setting.sf, (uint32_t)setting.cr);
mesh::RadioProfiles preview;
if (_radio->profiles()) preview = *_radio->profiles();
preview.primary.freq = setting.freq; preview.primary.bw = setting.bw;
preview.primary.sf = setting.sf; preview.primary.cr = setting.cr;
preview.primary.preamble = setting.preamble;
snprintf(dest, dest_len, "%s,%s,%u,%u,%u", freq, bw, (uint32_t)setting.sf, (uint32_t)setting.cr,
preview.preamble(0, rxPowerSavingPreambleForParams(setting.sf, setting.bw)));
}
void MyMesh::formatScheduledRadioSetting(char* reply, int setting_idx, int display_idx) const {
@@ -4330,7 +4344,7 @@ void MyMesh::formatScheduledRadioSetting(char* reply, int setting_idx, int displ
}
void MyMesh::addScheduledRadioParams(bool temporary, float freq, float bw, uint8_t sf, uint8_t cr,
uint32_t start_time, uint32_t end_time, char* reply) {
uint32_t start_time, uint32_t end_time, char* reply, uint16_t preamble) {
uint32_t now = getRTCClock()->getCurrentTime();
if (!isValidScheduledRadioParams(freq, bw, sf, cr)) {
strcpy(reply, "Error, invalid radio params");
@@ -4370,6 +4384,7 @@ void MyMesh::addScheduledRadioParams(bool temporary, float freq, float bw, uint8
scheduled_radio_settings[slot].bw = bw;
scheduled_radio_settings[slot].sf = sf;
scheduled_radio_settings[slot].cr = cr;
scheduled_radio_settings[slot].preamble = preamble;
scheduled_radio_settings[slot].start_time = start_time;
scheduled_radio_settings[slot].end_time = temporary ? end_time : 0;
const uint64_t current_uptime_millis =
@@ -4593,6 +4608,10 @@ void MyMesh::processScheduledRadioSettings() {
}
ScheduledRadioSetting& setting = scheduled_radio_settings[due_idx];
if (!_cli.radioProfiles().savePrimaryPreamble(setting.preamble)) {
scheduled_radio_retry_at = futureMillis(60000);
break;
}
_prefs.freq = setting.freq;
_prefs.bw = setting.bw;
_prefs.sf = setting.sf;
@@ -4633,7 +4652,7 @@ void MyMesh::processScheduledRadioSettings() {
temp_radio_handoff_pending = false;
queueSavedRadioApply();
}
} else if (applyRadioParams(setting.freq, setting.bw, setting.sf, setting.cr)) {
} else if (applyRadioParams(setting.freq, setting.bw, setting.sf, setting.cr, setting.preamble, true)) {
setting.started = true;
temp_radio_applied = true;
temp_radio_handoff_pending = false;
@@ -4757,7 +4776,7 @@ uint32_t MyMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const {
return sleep_secs;
}
void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble) {
// A newer TempRadio command supersedes the reply barrier belonging to the
// old schedule. processDeferredCliCommand() arms the new exact reply after
// it has been composed and successfully queued.
@@ -4798,6 +4817,7 @@ void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr,
scheduled_radio_settings[slot].bw = bw;
scheduled_radio_settings[slot].sf = sf;
scheduled_radio_settings[slot].cr = cr;
scheduled_radio_settings[slot].preamble = preamble;
scheduled_radio_settings[slot].start_time = start_time;
scheduled_radio_settings[slot].end_time = start_time + ((uint32_t)timeout_mins * 60);
const uint64_t current_uptime_millis =
@@ -12056,7 +12076,7 @@ void MyMesh::loop() {
processDeferredCliCommand();
servicePostMeshLoop();
#if defined(ENABLE_OTA) && OTA_DYNAMIC_CONTEXT
mesh::ota::ota_service_temp_radio_context(isTempRadioActive());
mesh::ota::ota_service_temp_radio_context(isAnyTempRadioActive());
#endif
}
@@ -13034,6 +13054,7 @@ bool MyMesh::startNeighborDiscover(char* reply) {
// To check if there is pending work
bool MyMesh::hasPendingWork() const {
if (isDualRadioActive()) return true;
if (hasPendingOtaApply()) return true;
#if defined(WITH_WEBCONFIG) || defined(ETHERNET_ENABLED)
if (_local_cli_output.busy()) return true;
+8 -6
View File
@@ -286,6 +286,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
#endif
{
struct ScheduledRadioSetting {
uint16_t preamble = 0;
bool active;
bool temporary;
bool started;
@@ -465,7 +466,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
uint32_t epoch;
uint32_t received_millis;
};
mutable FloodRetryBridgeState flood_retry_bridge_states[MAX_FLOOD_RETRY_SLOTS];
mutable FloodRetryBridgeState flood_retry_bridge_states[TOTAL_FLOOD_RETRY_SLOTS];
FloodRetryBridgeReachability flood_retry_bridge_reachability[FLOOD_RETRY_BRIDGE_BUCKETS + 1];
static_assert(sizeof(FloodPacketFilterEntry) <= (MESH_ENABLE_FLOOD_RULE_ENGINE ? 200 : 40),
"Update the flood-table runtime RAM budget in check_firmware_ram.py");
@@ -677,7 +678,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
uint8_t progress_marker) const;
bool floodRetryBridgeEligible(const mesh::Packet* packet) const;
FloodRetryBridgeState* floodRetryBridgeStateFor(const mesh::Packet* packet, bool create) const;
void clearFloodRetryBridgeStateByKey(const uint8_t* retry_key);
void clearFloodRetryBridgeStateByKey(const uint8_t* retry_key, uint8_t radio_profile);
void refreshFloodRetryReachability(const mesh::Packet* packet);
void formatFloodRetryPath(char* dest, size_t dest_len, const mesh::Packet* packet) const;
bool handleClientPathCommand(ClientInfo* sender, char* command, char* reply);
@@ -732,7 +733,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
File openAppend(const char* fname);
bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]);
bool applyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr);
bool applyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, uint16_t preamble = 0, bool temporary = false);
bool applySavedRadioParams();
void queueSavedRadioApply();
void refreshScheduledRadioState();
@@ -906,7 +907,7 @@ protected:
bool allowFloodRetry(const mesh::Packet* packet) const override;
bool prepareFloodRetry(const mesh::Packet* packet) const override;
void onFloodRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override;
void onFloodRetrySlotReleased(const uint8_t* retry_key) override;
void onFloodRetrySlotReleased(const uint8_t* retry_key, uint8_t radio_profile) override;
bool hasFloodRetryTargetPrefix(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxPathLength(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxAttempts(const mesh::Packet* packet) const override;
@@ -988,7 +989,8 @@ public:
bool sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
// CommonCLICallbacks
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
mesh::Radio* getProfileRadio() override { return _radio; }
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble = 0) override;
bool scheduleNormalRadio() override;
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
uint32_t getUserGpioRequestSource() const override {
@@ -1010,7 +1012,7 @@ public:
#endif
bool resolveAlertScope(TransportKey& dest) override;
void addScheduledRadioParams(bool temporary, float freq, float bw, uint8_t sf, uint8_t cr,
uint32_t start_time, uint32_t end_time, char* reply) override;
uint32_t start_time, uint32_t end_time, char* reply, uint16_t preamble = 0) override;
void formatScheduledRadioParams(bool temporary, const char* selector, char* reply) override;
void deleteScheduledRadioParams(bool temporary, const char* selector, char* reply) override;
bool formatFileSystem() override;
+10 -7
View File
@@ -438,7 +438,7 @@ bool MyMesh::isLooped(const mesh::Packet* packet,
int MyMesh::calcRxDelayForPacket(const mesh::Packet* packet, float score,
uint32_t air_time) {
if (packet != NULL && packet->getPayloadType() == PAYLOAD_TYPE_OTA
&& isTempRadioActive()) return 0;
&& isPacketOnTempRadio(packet)) return 0;
bool fast_track = false;
if (!evaluateFloodRuleTiming(packet, fast_track)) {
return calcRxDelay(score, air_time);
@@ -1458,8 +1458,9 @@ bool MyMesh::applySavedRadioParams() {
uint32_t timings[2] = {_prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us};
const uint32_t* applied_timings = _prefs.rx_powersaving_enabled
&& radio_driver.supportsRxPowerSaving() ? timings : NULL;
if (!radio_driver.setParams(
_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr, applied_timings)) {
if (_cli.radioProfiles().applyPrimary(
_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr, false,
_cli.radioProfiles().primaryPreamble(), applied_timings) != mesh::RadioParamApplyResult::APPLIED) {
return false;
}
active_cr = _prefs.cr;
@@ -1517,7 +1518,8 @@ void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, ui
}
}
void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble) {
pending_preamble = preamble;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params
@@ -2585,8 +2587,8 @@ void MyMesh::loop() {
uint32_t timings[2] = {rx_us, sleep_us};
const uint32_t* applied_timings = _prefs.rx_powersaving_enabled
&& radio_driver.supportsRxPowerSaving() ? timings : NULL;
if (timing_ok && radio_driver.setParams(
pending_freq, pending_bw, pending_sf, pending_cr, applied_timings)) {
if (timing_ok && _cli.radioProfiles().applyPrimary(
pending_freq, pending_bw, pending_sf, pending_cr, true, pending_preamble, applied_timings) == mesh::RadioParamApplyResult::APPLIED) {
set_radio_at = 0;
active_cr = pending_cr;
temp_radio_applied = true;
@@ -2783,7 +2785,7 @@ void MyMesh::loop() {
}
#endif
#if defined(ENABLE_OTA) && OTA_DYNAMIC_CONTEXT
mesh::ota::ota_service_temp_radio_context(isTempRadioActive());
mesh::ota::ota_service_temp_radio_context(isAnyTempRadioActive());
#endif
}
@@ -2838,6 +2840,7 @@ uint32_t MyMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const {
// To check if there is pending work
bool MyMesh::hasPendingWork() const {
if (isDualRadioActive()) return true;
if (hasPendingOtaApply()) return true;
#if defined(WITH_WEBCONFIG) || defined(ETHERNET_ENABLED)
if (_local_cli_output.busy()) return true;
+3 -1
View File
@@ -159,6 +159,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks,
#endif
{
FILESYSTEM* _fs;
uint16_t pending_preamble = 0;
mesh::StaticFloodAdvertLimiter<> flood_advert_limiter;
mesh::FloodAdvertLimiter* getFloodAdvertLimiter() override { return &flood_advert_limiter; }
#if defined(WITH_WEBCONFIG) || defined(ETHERNET_ENABLED)
@@ -443,7 +444,8 @@ public:
uint32_t delay_millis, uint8_t path_hash_size);
// CommonCLICallbacks
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
mesh::Radio* getProfileRadio() override { return _radio; }
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble = 0) override;
bool scheduleNormalRadio() override;
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
uint32_t getUserGpioRequestSource() const override {
+8
View File
@@ -1,3 +1,4 @@
#include <helpers/RadioProfileCLI.h>
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#if MESH_PACKET_LOGGING
@@ -84,6 +85,7 @@ struct NodePrefs { // persisted to file
class MyMesh : public BaseChatMesh, ContactVisitor {
FILESYSTEM* _fs;
mesh::RadioProfileCLI _radio_profiles;
NodePrefs _prefs;
uint32_t expected_ack_crc;
ChannelDetails* _public;
@@ -318,6 +320,7 @@ public:
_fs = &fs;
BaseChatMesh::begin();
_radio_profiles.begin(_fs, _radio, getRTCClock());
#if defined(NRF52_PLATFORM)
IdentityStore store(fs, "");
@@ -421,6 +424,10 @@ public:
}
void handleCommand(const char* command) {
char profile_reply[160];
if (_radio_profiles.handle(command, profile_reply, sizeof(profile_reply))) {
Serial.println(profile_reply); return;
}
while (*command == ' ') command++; // skip leading spaces
if (memcmp(command, "send ", 5) == 0) {
@@ -631,6 +638,7 @@ public:
}
void loop() {
_radio_profiles.loop();
BaseChatMesh::loop();
int len = strlen(command);
+9 -6
View File
@@ -1067,8 +1067,9 @@ bool SensorMesh::applySavedRadioParams() {
uint32_t timings[2] = {_prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us};
const uint32_t* applied_timings = _prefs.rx_powersaving_enabled
&& radio_driver.supportsRxPowerSaving() ? timings : NULL;
if (!radio_driver.setParams(
_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr, applied_timings)) {
if (_cli.radioProfiles().applyPrimary(
_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr, false,
_cli.radioProfiles().primaryPreamble(), applied_timings) != mesh::RadioParamApplyResult::APPLIED) {
return false;
}
active_cr = _prefs.cr;
@@ -1101,7 +1102,8 @@ void SensorMesh::saveIdentity(const mesh::LocalIdentity& new_id) {
store.save("_main", new_id);
}
void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble) {
pending_preamble = preamble;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params
@@ -1264,8 +1266,8 @@ void SensorMesh::loop() {
uint32_t timings[2] = {rx_us, sleep_us};
const uint32_t* applied_timings = _prefs.rx_powersaving_enabled
&& radio_driver.supportsRxPowerSaving() ? timings : NULL;
if (timing_ok && radio_driver.setParams(
pending_freq, pending_bw, pending_sf, pending_cr, applied_timings)) {
if (timing_ok && _cli.radioProfiles().applyPrimary(
pending_freq, pending_bw, pending_sf, pending_cr, true, pending_preamble, applied_timings) == mesh::RadioParamApplyResult::APPLIED) {
set_radio_at = 0;
active_cr = pending_cr;
temp_radio_applied = true;
@@ -1362,7 +1364,7 @@ void SensorMesh::loop() {
}
}
#if defined(ENABLE_OTA) && OTA_DYNAMIC_CONTEXT
mesh::ota::ota_service_temp_radio_context(isTempRadioActive());
mesh::ota::ota_service_temp_radio_context(isAnyTempRadioActive());
#endif
}
@@ -1382,6 +1384,7 @@ uint32_t SensorMesh::limitSleepToMillisTimer(unsigned long timestamp,
}
bool SensorMesh::hasPendingWork() const {
if (isDualRadioActive()) return true;
if (hasPendingOtaApply()) return true;
if (_cli.hasActiveUserGpioTimer()) return true;
if (radio_driver.isWatchdogObserving()) return true;
+3 -1
View File
@@ -94,7 +94,8 @@ public:
mesh::LocalIdentity& getSelfId() override { return self_id; }
void saveIdentity(const mesh::LocalIdentity& new_id) override;
void clearStats() override { }
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
mesh::Radio* getProfileRadio() override { return _radio; }
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble = 0) override;
bool scheduleNormalRadio() override;
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
uint32_t getUserGpioRequestSource() const override {
@@ -187,6 +188,7 @@ protected:
void sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size=1);
private:
FILESYSTEM* _fs;
uint16_t pending_preamble = 0;
unsigned long next_local_advert, next_flood_advert;
NodePrefs _prefs;
ClientACL acl;