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

+127 -5
View File
@@ -122,6 +122,8 @@ void Dispatcher::restoreOutboundTxOverrides() {
bool Dispatcher::startOutboundTransmit() {
if (outbound == NULL) return false;
if (!isPacketRadioCurrent(outbound)
|| _radio->prepareTransmitProfile(outbound->radio_profile) != RadioParamApplyResult::APPLIED) return false;
int len = 0;
uint8_t raw[MAX_TRANS_UNIT];
@@ -145,6 +147,7 @@ bool Dispatcher::startOutboundTransmit() {
uint32_t max_airtime = _radio->getEstAirtimeFor(len) * 3 / 2;
outbound_restore_cr = 0;
uint8_t default_cr = getDefaultTxCodingRate();
if (_radio->profiles()) default_cr = _radio->profiles()->params(outbound->radio_profile).cr;
if (outbound->tx_cr >= 4 && outbound->tx_cr <= 8
&& default_cr >= 4 && default_cr <= 8
&& outbound->tx_cr != default_cr) {
@@ -244,6 +247,7 @@ void Dispatcher::logPacketLine(const char* direction, const Packet* packet,
#endif
bool Dispatcher::getNextQueueWakeDelay(uint32_t& delay_millis) const {
if (isDualRadioActive()) { delay_millis = 0; return true; }
const uint32_t now = _ms->getMillis();
bool found = false;
uint32_t shortest_delay = 0;
@@ -342,10 +346,20 @@ void Dispatcher::loop() {
if (outbound) { // waiting for outbound send to complete, or for its one radio retry
if (outbound_radio_retry_pending) {
// The failed send has already returned the chip to RX. Drain a packet
// arriving during backoff before asking to retune; otherwise BUSY can
// keep the retry waiting forever on an unread RxDone interrupt.
if (isDualRadioActive()) checkRecv();
if (!millisHasNowPassed(outbound_radio_retry_at)) return;
const auto prepared = _radio->prepareTransmitProfile(outbound->radio_profile);
if (prepared == RadioParamApplyResult::BUSY) {
outbound_radio_retry_at = futureMillis(10);
return;
}
outbound_radio_retry_pending = false;
if (!allowPacketTransmit(outbound)) {
if (prepared == RadioParamApplyResult::FAILED || !isPacketRadioCurrent(outbound)
|| !allowPacketTransmit(outbound)) {
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): radio retry packet no longer allowed, type=%u",
getLogDateTime(), (uint32_t)outbound->getPayloadType());
failOutboundTransmit();
@@ -384,6 +398,7 @@ void Dispatcher::loop() {
restoreOutboundTxOverrides();
logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
onSendComplete(outbound);
if (auto* p = _radio->profiles()) ++p->tx_packets[outbound->radio_profile];
if (outbound->isRouteFlood()) {
n_sent_flood++;
} else {
@@ -523,6 +538,10 @@ bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) {
pkt->tx_cr = 0;
pkt->flood_retry_policy = FLOOD_RETRY_POLICY_DEFAULT;
pkt->radio_profile = pkt->radio_origin = _radio->receiveProfile();
pkt->radio_bound = false;
pkt->radio_local = false;
pkt->radio_generation = pkt->radio_origin_generation = _radio->receiveProfileGeneration();
pkt->header = raw[i++];
if (pkt->getPayloadVer() > PAYLOAD_VER_1) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported packet version", getLogDateTime());
@@ -597,6 +616,7 @@ void Dispatcher::checkRecv() {
} else {
if (tryParsePacket(pkt, raw, len)) {
last_meshcore_recv_millis = _ms->getMillis();
if (auto* p = _radio->profiles()) ++p->rx_packets[pkt->radio_profile];
pkt->_snr = snr * 4.0f;
pkt->_rssi = (int16_t)rssi;
score = _radio->packetScore(snr, len);
@@ -642,7 +662,23 @@ void Dispatcher::checkRecv() {
}
void Dispatcher::processRecvPacket(Packet* pkt) {
const auto* profiles = _radio->profiles();
if (profiles && (pkt->radio_profile > 1
|| (pkt->radio_profile == 1 && !profiles->enabled())
|| pkt->radio_generation != profiles->generation[pkt->radio_profile])) {
releasePacket(pkt);
return;
}
const uint8_t previous_profile = receive_context_profile;
const uint32_t previous_generation = receive_context_generation;
const bool previous_active = receive_context_active;
receive_context_profile = pkt->radio_profile;
receive_context_generation = pkt->radio_generation;
receive_context_active = true;
DispatcherAction action = onRecvPacket(pkt);
receive_context_profile = previous_profile;
receive_context_generation = previous_generation;
receive_context_active = previous_active;
if (action == ACTION_RELEASE) {
_mgr->free(pkt);
} else if (action == ACTION_MANUAL_HOLD) {
@@ -671,9 +707,19 @@ void Dispatcher::checkSend() {
return;
}
// Discard work bound to an expired/changed profile before it can be retuned
// onto a different channel. This also retires its retry ownership normally.
Packet* pending = _mgr->peekNextOutbound(now);
if (pending && !isPacketRadioCurrent(pending)) {
outbound = _mgr->getNextOutbound(now);
failOutboundTransmit();
return;
}
updateTxBudget();
uint32_t est_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT);
uint32_t est_airtime = pending && _radio->profiles()
? _radio->getProfileAirtime(pending->radio_profile, MAX_TRANS_UNIT)
: _radio->getEstAirtimeFor(MAX_TRANS_UNIT);
if (tx_budget_ms < est_airtime / MIN_TX_BUDGET_AIRTIME_DIV) {
float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor());
unsigned long needed = est_airtime / MIN_TX_BUDGET_AIRTIME_DIV - tx_budget_ms;
@@ -683,15 +729,29 @@ void Dispatcher::checkSend() {
if (!millisHasNowPassed(next_tx_time)) return;
Packet* pending = _mgr->peekNextOutbound(_ms->getMillis());
// Waiting for airtime credit must leave the receive scanner free to run.
// Only retune once this queue entry is actually eligible to transmit.
if (pending && _radio->profiles()) {
const auto prepared = _radio->prepareTransmitProfile(pending->radio_profile);
if (prepared == RadioParamApplyResult::BUSY) return;
if (prepared == RadioParamApplyResult::FAILED) {
outbound = _mgr->getNextOutbound(now);
failOutboundTransmit();
return;
}
}
bool channel_busy = pending != NULL && usePassiveChannelCheck(pending)
? _radio->isReceivingPassive(getRetryInterferenceMargin())
: _radio->isReceiving();
if (channel_busy) {
const uint32_t cad_now = _ms->getMillis();
const int ready_count = _mgr->getOutboundCount(cad_now);
uint32_t& profile_busy = profile_cad_busy[pending ? pending->radio_profile : 0];
cad_busy_start = profile_busy;
if (cad_busy_start == 0) {
cad_busy_start = cad_now; // record when CAD busy state started
profile_busy = cad_now;
}
const uint32_t max_busy_duration = scaleCADDelayForQueue(
@@ -705,11 +765,14 @@ void Dispatcher::checkSend() {
} else {
const uint32_t retry_delay = scaleCADDelayForQueue(
getCADFailRetryDelay(), ready_count, MIN_CAD_FAIL_RETRY_DELAY_MS);
next_tx_time = futureMillis(retry_delay);
if (!isDualRadioActive() || !pending || !_mgr->deferOutbound(pending, futureMillis(retry_delay))) {
next_tx_time = futureMillis(retry_delay);
}
return;
}
}
cad_busy_start = 0; // reset busy state
profile_cad_busy[pending ? pending->radio_profile : 0] = 0;
outbound = _mgr->getNextOutbound(_ms->getMillis());
if (outbound) {
@@ -750,6 +813,10 @@ Packet* Dispatcher::obtainNewPacket() {
pkt->_snr = 0;
pkt->tx_cr = 0;
pkt->flood_retry_policy = FLOOD_RETRY_POLICY_DEFAULT;
pkt->radio_profile = pkt->radio_origin = receive_context_active ? receive_context_profile : 0;
pkt->radio_generation = pkt->radio_origin_generation = receive_context_active ? receive_context_generation : 0;
pkt->radio_bound = false;
pkt->radio_local = !receive_context_active;
}
return pkt;
}
@@ -767,7 +834,62 @@ bool Dispatcher::queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t
MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d, payload_len=%d", getLogDateTime(), (uint32_t) packet->path_len, (uint32_t) packet->payload_len);
return false;
}
return _mgr->queueOutbound(packet, priority, futureMillis(delay_millis));
auto* profiles = _radio->profiles();
if (packet->radio_profile > 1) return false;
if (profiles == nullptr || packet->radio_bound) {
return _mgr->queueOutbound(packet, priority, futureMillis(delay_millis));
}
if (!packet->radio_local && packet->radio_generation
&& packet->radio_generation != profiles->generation[packet->radio_profile]) return false;
// Locally generated OTA traffic uses the temporary update profile. Replies
// created while processing RX inherit that request's profile instead.
// Keep that origin in RX-only mode too: isolation must drop a disallowed
// transmission instead of silently sending it on the normal channel.
if (packet->radio_local && packet->getPayloadType() == PAYLOAD_TYPE_OTA
&& profiles->secondary_temporary && !profiles->primary_temporary) {
packet->radio_profile = 1;
}
const uint8_t origin = packet->radio_profile;
const uint8_t mask = profiles->transmitMask(origin);
if (!mask) return false;
packet->radio_origin = origin;
packet->radio_origin_generation = profiles->generation[origin];
if (!(mask & (1U << origin))) {
onSendFail(packet); // retire any retry reserved on the RX-only profile
packet->radio_profile ^= 1;
}
packet->radio_generation = profiles->generation[packet->radio_profile];
packet->radio_bound = true;
if (!_mgr->queueOutbound(packet, priority, futureMillis(delay_millis))) return false;
if (packet->radio_profile != origin) onRadioProfileCopyQueued(packet, nullptr, priority);
const uint8_t other = packet->radio_profile ^ 1;
if (mask & (1U << other)) {
Packet* copy = obtainNewPacket();
if (copy) {
*copy = *packet;
copy->radio_profile = other;
copy->radio_generation = profiles->generation[other];
if (_mgr->queueOutbound(copy, priority, futureMillis(delay_millis))) {
onRadioProfileCopyQueued(copy, packet, priority);
onTracePacketQueuedForSend(copy);
} else {
onSendFail(copy);
releasePacket(copy);
}
}
}
return true;
}
bool Dispatcher::isPacketRadioCurrent(const Packet* packet) const {
const auto* p = _radio->profiles();
if (!p || !packet->radio_bound) return true;
const uint8_t target = packet->radio_profile;
const uint8_t origin = packet->radio_origin;
return target < 2 && origin < 2 && p->canTransmit(target)
&& packet->radio_generation == p->generation[target]
&& packet->radio_origin_generation == p->generation[origin]
&& (origin == target || p->canCross());
}
bool Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) {
+52
View File
@@ -4,6 +4,7 @@
#include <Identity.h>
#include <Packet.h>
#include <Utils.h>
#include <RadioProfiles.h>
#ifndef RADIO_LIVENESS_SOFT_ONLY
#include <helpers/RadioLivenessTracker.h>
#endif
@@ -31,6 +32,25 @@ public:
class Radio {
public:
virtual void begin() { }
virtual RadioProfiles* profiles() { return nullptr; }
virtual const RadioProfiles* profiles() const { return nullptr; }
virtual bool validateProfile(const RadioProfileParams&) const { return false; }
virtual uint8_t receiveProfile() const { return 0; }
virtual uint32_t receiveProfileGeneration() const {
return profiles() ? profiles()->generation[receiveProfile()] : 0;
}
virtual RadioParamApplyResult prepareTransmitProfile(uint8_t profile) {
return profile == 0 ? RadioParamApplyResult::APPLIED : RadioParamApplyResult::FAILED;
}
virtual uint16_t profilePreamble(uint8_t) const { return 0; }
virtual uint32_t getProfileAirtime(uint8_t, int len_bytes, uint8_t = 0) {
return getEstAirtimeFor(len_bytes);
}
virtual RadioParamApplyResult trySetPrimaryParams(const RadioProfileParams& p,
bool temporary, const uint32_t* timings = nullptr) {
(void)temporary;
return trySetParams(p.freq, p.bw, p.sf, p.cr, timings);
}
/**
* \brief polls for incoming raw packet.
@@ -193,6 +213,7 @@ public:
// freeing them silently. Dispatcher will run the normal send-failure
// lifecycle hook before returning each packet to the pool.
virtual Packet* getNextDroppedOutbound() { return NULL; }
virtual bool deferOutbound(Packet*, uint32_t) { return false; }
virtual int getOutboundCount(uint32_t now) const = 0;
virtual int getOutboundTotal() const = 0;
// Returns the earliest runnable time in the queue. A queue with any overdue
@@ -265,6 +286,10 @@ class Dispatcher {
unsigned long tx_budget_ms;
unsigned long last_budget_update;
unsigned long duty_cycle_window_ms;
uint8_t receive_context_profile = 0;
uint32_t receive_context_generation = 0;
bool receive_context_active = false;
uint32_t profile_cad_busy[2] = {};
void processRecvPacket(Packet* pkt);
void releaseDroppedOutbound();
@@ -275,6 +300,29 @@ class Dispatcher {
void updateTxBudget();
protected:
// Preserve RX affinity while an authenticated command is handled outside
// the receive call stack. Replies inherit this profile and session.
class ReceiveProfileScope {
Dispatcher& owner;
uint8_t old_profile;
uint32_t old_generation;
bool old_active;
public:
ReceiveProfileScope(Dispatcher& dispatcher, uint8_t profile, uint32_t generation)
: owner(dispatcher), old_profile(owner.receive_context_profile),
old_generation(owner.receive_context_generation), old_active(owner.receive_context_active) {
owner.receive_context_profile = profile;
owner.receive_context_generation = generation;
owner.receive_context_active = true;
}
~ReceiveProfileScope() {
owner.receive_context_profile = old_profile;
owner.receive_context_generation = old_generation;
owner.receive_context_active = old_active;
}
ReceiveProfileScope(const ReceiveProfileScope&) = delete;
ReceiveProfileScope& operator=(const ReceiveProfileScope&) = delete;
};
PacketManager* _mgr;
Radio* _radio;
MillisecondClock* _ms;
@@ -320,6 +368,7 @@ protected:
virtual void onTracePacketQueuedForSend(Packet* packet) { }
virtual void onSendComplete(Packet* packet) { }
virtual void onSendFail(Packet* packet) { }
virtual void onRadioProfileCopyQueued(Packet* packet, const Packet* original, uint8_t priority) { }
virtual const char* getLogDateTime() { return ""; }
virtual float getAirtimeBudgetFactor() const;
@@ -361,10 +410,13 @@ protected:
bool tryParsePacket(Packet* pkt, const uint8_t* raw, int len);
void setRadioAvailable(bool available);
bool isRadioAvailable() const { return radio_available; }
bool isPacketRadioCurrent(const Packet* packet) const;
public:
void begin();
void loop();
Radio* getProfileRadio() { return _radio; }
bool isDualRadioActive() const { return _radio->profiles() && _radio->profiles()->enabled(); }
Packet* obtainNewPacket();
void releasePacket(Packet* packet);
+127 -53
View File
@@ -138,6 +138,7 @@ void Mesh::configureDirectRetryPacket(Packet* retry, const Packet* original, uin
}
uint8_t default_cr = getDefaultTxCodingRate();
if (original && _radio->profiles()) default_cr = _radio->profiles()->params(original->radio_profile).cr;
if (default_cr < 4 || default_cr > 8) {
return;
}
@@ -151,7 +152,8 @@ void Mesh::configureFloodRetryPacket(Packet* retry, const Packet* original, uint
// Always start from the active radio setting, not the previous retry's
// override. Only the originating sender has no hops recorded yet; bridge
// path adjustments must not make a forwarded packet eligible for this.
const uint8_t default_cr = getDefaultTxCodingRate();
const uint8_t default_cr = original && _radio->profiles()
? _radio->profiles()->params(original->radio_profile).cr : getDefaultTxCodingRate();
retry->tx_cr = original != NULL && original->isRouteFlood()
&& original->getPathHashCount() == 0
? getDirectRetryCodingRateForAttempt(default_cr, retry_attempt)
@@ -189,7 +191,7 @@ static int queuedPacedOtaResponses(PacketManager* manager) {
// DATA/PROOF admission is credit-limited; false leaves the manager's response descriptor intact.
bool Mesh::otaSendAdapter(void* ctx, const uint8_t* msg, uint16_t len, bool /*flood*/) {
Mesh* m = (Mesh*)ctx;
if (!m->isTempRadioActive()) return false;
if (!m->isAnyTempRadioActive()) return false;
if (isPacedOtaResponse(msg, len)
&& (queuedPacedOtaResponses(m->_mgr) >= OTA_EGRESS_QUEUE_CREDIT
|| m->_mgr->getFreeCount() <= OTA_EGRESS_MIN_FREE)) {
@@ -213,7 +215,7 @@ void Mesh::begin() {
_next_recent_advert_echo = 0;
_next_direct_retry_timeout = 0;
_next_flood_retry_timeout = 0;
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
_direct_retries[i].packet = NULL;
_direct_retries[i].trigger_packet = NULL;
_direct_retries[i].retry_started_at = 0;
@@ -238,7 +240,7 @@ void Mesh::begin() {
_direct_retries[i].has_message_replacement_key = false;
_direct_retries[i].active = false;
}
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_FLOOD_RETRY_SLOTS; i++) {
_flood_retries[i].packet = NULL;
_flood_retries[i].trigger_packet = NULL;
_flood_retries[i].retry_started_at = 0;
@@ -282,6 +284,33 @@ void Mesh::begin() {
}
void Mesh::loop() {
const auto* p = _radio->profiles();
if (p && (_retry_radio_generations[0] != p->generation[0]
|| _retry_radio_generations[1] != p->generation[1] || _retry_cross_mode != p->cross)) {
// A changed session is not a failed radio link. Retire its ownership
// without recording a failed final echo, and promptly return queued storage.
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; ++i) {
if (!_direct_retries[i].active) continue;
const auto* packet = _direct_retries[i].queued ? _direct_retries[i].packet : _direct_retries[i].trigger_packet;
if (_direct_retries[i].waiting_final_echo || (packet && !isPacketRadioCurrent(packet))) retireDirectRetrySlot(i);
}
for (int i = 0; i < TOTAL_FLOOD_RETRY_SLOTS; ++i) {
if (!_flood_retries[i].active) continue;
const auto* packet = _flood_retries[i].queued ? _flood_retries[i].packet : _flood_retries[i].trigger_packet;
if ((_flood_retries[i].waiting_final_echo && _flood_retries[i].packet
&& !isPacketRadioCurrent(_flood_retries[i].packet))
|| (packet && !isPacketRadioCurrent(packet))) retireFloodRetrySlot(i);
}
for (int i = _mgr->getOutboundTotal() - 1; i >= 0; --i) {
if (isPacketRadioCurrent(_mgr->getOutboundByIdx(i))) continue;
auto* stale = _mgr->removeOutboundByIdx(i);
onSendFail(stale);
releasePacket(stale);
}
_retry_radio_generations[0] = p->generation[0];
_retry_radio_generations[1] = p->generation[1];
_retry_cross_mode = p->cross;
}
Dispatcher::loop();
serviceLoopMaintenance();
#if defined(ENABLE_OTA) && defined(ESP32_PLATFORM) && \
@@ -305,7 +334,7 @@ void __attribute__((noinline)) Mesh::serviceLoopMaintenance() {
if (auto* limiter = getFloodAdvertLimiter()) limiter->tick(_ms->getMillis());
if (_waiting_direct_retry_count != 0
&& millisHasNowPassed(_next_direct_retry_timeout)) {
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (!_direct_retries[i].active || !_direct_retries[i].waiting_final_echo) {
continue;
}
@@ -329,7 +358,7 @@ void __attribute__((noinline)) Mesh::serviceLoopMaintenance() {
if (_waiting_flood_retry_count != 0
&& millisHasNowPassed(_next_flood_retry_timeout)) {
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_FLOOD_RETRY_SLOTS; i++) {
if (!_flood_retries[i].active || !_flood_retries[i].waiting_final_echo) {
continue;
}
@@ -389,7 +418,7 @@ void __attribute__((noinline)) Mesh::serviceLoopMaintenance() {
}
}
#endif
const bool ota_active = isTempRadioActive();
const bool ota_active = isAnyTempRadioActive();
if (!ota_active) {
if (_ota_temp_was_active) {
ota::ota_ctx().manager.clearPendingEgress();
@@ -474,11 +503,11 @@ bool Mesh::allowPacketTransmit(const Packet* packet) const {
// This is an egress guard, separate from the receive-side TempRadio check below. A relay can queue an OTA
// packet just before its temporary window closes; never let that delayed packet leak onto the normal channel.
if (packet != NULL && packet->getPayloadType() == PAYLOAD_TYPE_OTA
&& !isTempRadioActive()) {
&& !isAnyTempRadioActive()) {
return false;
}
if (packet != NULL && _active_flood_retry_count != 0) {
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_FLOOD_RETRY_SLOTS; i++) {
if (!_flood_retries[i].active || !_flood_retries[i].queued
|| _flood_retries[i].packet != packet) {
continue;
@@ -494,7 +523,7 @@ bool Mesh::allowPacketForward(const mesh::Packet* packet) {
return false; // by default, Transport NOT enabled
}
uint32_t Mesh::getRetransmitDelay(const mesh::Packet* packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getRawLength()) * 52 / 50) / 2;
uint32_t t = (_radio->getProfileAirtime(packet->radio_profile, packet->getRawLength(), packet->tx_cr) * 52 / 50) / 2;
return _rng->nextInt(0, 5)*t;
}
@@ -577,7 +606,7 @@ uint32_t Mesh::getOtaRetransmitDelay(const mesh::Packet* packet) {
return getRetransmitDelay(packet);
}
decayOtaRelayBackoff();
uint32_t airtime = _radio->getEstAirtimeFor(packet->getRawLength());
uint32_t airtime = _radio->getProfileAirtime(packet->radio_profile, packet->getRawLength(), packet->tx_cr);
if (airtime == 0) return 0;
// Quarter-airtime units by pressure level: 0.25-0.5, 0.5-1.0, 0.75-2.0, 1.0-3.0.
// The maximum uses floor division, so level 3 can never exceed exactly three measured airtimes.
@@ -592,14 +621,14 @@ uint32_t Mesh::getOtaRetransmitDelay(const mesh::Packet* packet) {
int Mesh::calcRxDelayForPacket(const Packet* packet, float score, uint32_t air_time) {
if (packet != NULL && packet->getPayloadType() == PAYLOAD_TYPE_OTA
&& isTempRadioActive()) {
&& isAnyTempRadioActive()) {
return 0;
}
return Dispatcher::calcRxDelayForPacket(packet, score, air_time);
}
uint32_t Mesh::getCADFailRetryDelay() const {
if (!isTempRadioActive()) return _rng->nextInt(1, 4) * 120;
if (!isAnyTempRadioActive()) return _rng->nextInt(1, 4) * 120;
uint32_t airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT);
uint32_t retry = airtime / 4;
if (retry < 5) retry = 5;
@@ -634,7 +663,7 @@ uint32_t Mesh::getDirectRetryPacketAirtimeDelay(const Packet* packet) const {
return 0;
}
return _radio->getEstAirtimeFor(packet->getRawLength()) * (uint32_t)getDirectRetryPacketAirtimeFactor(packet);
return _radio->getProfileAirtime(packet->radio_profile, packet->getRawLength(), packet->tx_cr) * (uint32_t)getDirectRetryPacketAirtimeFactor(packet);
}
uint32_t Mesh::getDirectRetryEchoDelay(const Packet* packet) const {
return 200 + getDirectRetryPacketAirtimeDelay(packet);
@@ -734,8 +763,8 @@ uint32_t Mesh::getFloodRetryAttemptDelay(const Packet* packet, uint8_t attempt_i
return _radio->getEstAirtimeFor(MAX_TRANS_UNIT);
}
uint32_t max_packet_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT);
uint32_t packet_airtime = _radio->getEstAirtimeFor(packet->getRawLength());
uint32_t max_packet_airtime = _radio->getProfileAirtime(packet->radio_profile, MAX_TRANS_UNIT);
uint32_t packet_airtime = _radio->getProfileAirtime(packet->radio_profile, packet->getRawLength(), packet->tx_cr);
uint32_t jitter_percent = _rng->nextInt(0, 201);
uint32_t jitter = (packet_airtime * jitter_percent) / 100UL;
uint32_t delay = max_packet_airtime + (20UL * packet_airtime) + jitter;
@@ -754,6 +783,24 @@ void Mesh::onSendComplete(Packet* packet) {
armFloodRetryOnSendComplete(packet);
}
void Mesh::onRadioProfileCopyQueued(Packet* packet, const Packet* original, uint8_t priority) {
if (packet->isRouteFlood()) {
maybeScheduleFloodRetry(packet, priority);
replaceQueuedSelfAdvertRetries(packet);
} else {
bool final_hop = false;
if (original) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; ++i) {
if (_direct_retries[i].active && _direct_retries[i].trigger_packet == original) {
final_hop = _direct_retries[i].final_hop_retry;
break;
}
}
}
maybeScheduleDirectRetry(packet, priority, final_hop);
}
}
void Mesh::onTracePacketQueuedForSend(Packet* packet) {
replaceQueuedTraceRetries(packet);
}
@@ -1130,7 +1177,10 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
case PAYLOAD_TYPE_OTA: {
// OTA is invisible outside an actually-running temporary-radio window. In particular, do not add it
// to the seen table: a copy heard on the normal channel must not suppress one received after temp radio starts.
if (!isTempRadioActive()) break;
if (!isAnyTempRadioActive() || (_radio->profiles()
&& !(_radio->profiles()->primary_temporary && pkt->radio_profile == 0)
&& !(_radio->profiles()->secondary_temporary && pkt->radio_profile == 1)
&& _radio->profiles()->cross != RadioCrossMode::On)) break;
observeOtaRequestPressure(pkt);
uint8_t n = pkt->getPathHashCount(); // hops travelled to reach us (flood path-hash count)
#if defined(ENABLE_OTA)
@@ -1323,7 +1373,7 @@ void Mesh::clearDirectRetrySlot(int idx) {
}
void Mesh::retireDirectRetrySlot(int idx) {
if (idx < 0 || idx >= MAX_DIRECT_RETRY_SLOTS || !_direct_retries[idx].active) {
if (idx < 0 || idx >= TOTAL_DIRECT_RETRY_SLOTS || !_direct_retries[idx].active) {
return;
}
@@ -1346,7 +1396,7 @@ void Mesh::rebuildNextDirectRetryTimeout() {
bool found = false;
uint32_t shortest_delay = 0;
const uint32_t now = _ms->getMillis();
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (!_direct_retries[i].active || !_direct_retries[i].waiting_final_echo) continue;
int32_t signed_delay = (int32_t)(_direct_retries[i].retry_at - now);
uint32_t delay = signed_delay > 0 ? (uint32_t)signed_delay : 0;
@@ -1361,7 +1411,7 @@ void Mesh::rebuildNextDirectRetryTimeout() {
bool Mesh::usePassiveChannelCheck(const Packet* packet) const {
if (_active_direct_retry_count != 0) {
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (_direct_retries[i].active && _direct_retries[i].queued
&& _direct_retries[i].packet == packet) {
return true;
@@ -1375,7 +1425,7 @@ bool Mesh::usePassiveChannelCheck(const Packet* packet) const {
// has trigger_packet set but queued=false, so ordinary flood forwarding
// continues to use the normal CAD check.
if (_active_flood_retry_count != 0) {
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_FLOOD_RETRY_SLOTS; i++) {
if (_flood_retries[i].active && _flood_retries[i].queued
&& _flood_retries[i].packet == packet) {
return true;
@@ -1437,7 +1487,8 @@ void Mesh::replaceQueuedTraceRetries(const Packet* packet) {
int replacement_slot = -1;
bool found_prior = false;
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (i / MAX_DIRECT_RETRY_SLOTS != packet->radio_profile) continue;
if (!_direct_retries[i].active || _direct_retries[i].payload_type != PAYLOAD_TYPE_TRACE
|| memcmp(replacement_key, _direct_retries[i].trace_replacement_key, MAX_HASH_SIZE) != 0) {
continue;
@@ -1451,7 +1502,8 @@ void Mesh::replaceQueuedTraceRetries(const Packet* packet) {
if (!found_prior) return;
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (i / MAX_DIRECT_RETRY_SLOTS != packet->radio_profile) continue;
if (i == replacement_slot || !_direct_retries[i].active
|| _direct_retries[i].payload_type != PAYLOAD_TYPE_TRACE
|| memcmp(replacement_key, _direct_retries[i].trace_replacement_key, MAX_HASH_SIZE) != 0) {
@@ -1475,7 +1527,8 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) {
calculateDirectRetryKey(packet, recv_key);
bool cleared = false;
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (i / MAX_DIRECT_RETRY_SLOTS != packet->radio_profile) continue;
if (!_direct_retries[i].active || memcmp(recv_key, _direct_retries[i].retry_key, MAX_HASH_SIZE) != 0) {
continue;
}
@@ -1535,7 +1588,7 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) {
void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
if (_active_direct_retry_count == 0) return;
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (!_direct_retries[i].active) {
continue;
}
@@ -1657,7 +1710,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
void Mesh::clearPendingDirectRetryOnSendFail(const Packet* packet) {
if (_active_direct_retry_count == 0) return;
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (!_direct_retries[i].active) {
continue;
}
@@ -1825,7 +1878,8 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority, bool
calculateDirectRetryKey(packet, retry_key);
uint8_t trace_replacement_key[MAX_HASH_SIZE] = { 0 };
bool has_trace_replacement_key = calculateTraceReplacementKey(packet, trace_replacement_key);
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (i / MAX_DIRECT_RETRY_SLOTS != packet->radio_profile) continue;
if (_direct_retries[i].active
&& memcmp(retry_key, _direct_retries[i].retry_key, MAX_HASH_SIZE) == 0) {
return; // the normal direct send still happens, but only one retry sequence owns this logical packet
@@ -1833,7 +1887,8 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority, bool
}
int slot_idx = -1;
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (i / MAX_DIRECT_RETRY_SLOTS != packet->radio_profile) continue;
if (!_direct_retries[i].active) {
slot_idx = i;
break;
@@ -1841,7 +1896,8 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority, bool
}
if (slot_idx < 0) {
if (has_trace_replacement_key) {
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (i / MAX_DIRECT_RETRY_SLOTS != packet->radio_profile) continue;
if (_direct_retries[i].active && _direct_retries[i].payload_type == PAYLOAD_TYPE_TRACE
&& memcmp(trace_replacement_key, _direct_retries[i].trace_replacement_key,
MAX_HASH_SIZE) == 0) {
@@ -1893,7 +1949,7 @@ void Mesh::clearFloodRetrySlot(int idx) {
if (_flood_retries[idx].waiting_final_echo && _waiting_flood_retry_count > 0) {
_waiting_flood_retry_count--;
}
onFloodRetrySlotReleased(_flood_retries[idx].retry_key);
onFloodRetrySlotReleased(_flood_retries[idx].retry_key, idx / MAX_FLOOD_RETRY_SLOTS);
}
if (_flood_retries[idx].waiting_final_echo && _flood_retries[idx].packet != NULL) {
releasePacket(_flood_retries[idx].packet);
@@ -1919,7 +1975,7 @@ void Mesh::clearFloodRetrySlot(int idx) {
}
void Mesh::retireFloodRetrySlot(int idx) {
if (idx < 0 || idx >= MAX_FLOOD_RETRY_SLOTS || !_flood_retries[idx].active) {
if (idx < 0 || idx >= TOTAL_FLOOD_RETRY_SLOTS || !_flood_retries[idx].active) {
return;
}
@@ -1945,7 +2001,8 @@ void Mesh::replaceQueuedSelfAdvertRetries(const Packet* packet) {
int replacement_slot = -1;
bool found_prior = false;
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_retries[i].active
|| !_flood_retries[i].self_advert) {
continue;
@@ -1959,7 +2016,8 @@ void Mesh::replaceQueuedSelfAdvertRetries(const Packet* packet) {
if (!found_prior) 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 != packet->radio_profile) continue;
if (i == replacement_slot || !_flood_retries[i].active
|| !_flood_retries[i].self_advert) {
continue;
@@ -1979,7 +2037,7 @@ void Mesh::rebuildNextFloodRetryTimeout() {
bool found = false;
uint32_t shortest_delay = 0;
const uint32_t now = _ms->getMillis();
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_FLOOD_RETRY_SLOTS; i++) {
if (!_flood_retries[i].active || !_flood_retries[i].waiting_final_echo) continue;
int32_t signed_delay = (int32_t)(_flood_retries[i].retry_at - now);
uint32_t delay = signed_delay > 0 ? (uint32_t)signed_delay : 0;
@@ -1995,7 +2053,7 @@ void Mesh::rebuildNextFloodRetryTimeout() {
void Mesh::cancelAllDirectRetries() {
if (_active_direct_retry_count == 0) return;
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (!_direct_retries[i].active) continue;
retireDirectRetrySlot(i);
}
@@ -2004,7 +2062,7 @@ void Mesh::cancelAllDirectRetries() {
void Mesh::cancelAllFloodRetries() {
if (_active_flood_retry_count == 0) return;
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_FLOOD_RETRY_SLOTS; i++) {
if (!_flood_retries[i].active) continue;
retireFloodRetrySlot(i);
}
@@ -2018,7 +2076,7 @@ bool Mesh::cancelActiveRetries(const uint8_t retry_key[MAX_HASH_SIZE]) {
uint8_t key[MAX_HASH_SIZE];
memcpy(key, retry_key, sizeof(key)); // tolerate callers passing storage owned by a retry slot
bool cancelled = false;
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (!_direct_retries[i].active
|| memcmp(key, _direct_retries[i].retry_key, MAX_HASH_SIZE) != 0) {
continue;
@@ -2028,7 +2086,7 @@ bool Mesh::cancelActiveRetries(const uint8_t retry_key[MAX_HASH_SIZE]) {
cancelled = true;
}
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_FLOOD_RETRY_SLOTS; i++) {
if (!_flood_retries[i].active
|| memcmp(key, _flood_retries[i].retry_key, MAX_HASH_SIZE) != 0) {
continue;
@@ -2079,7 +2137,8 @@ void Mesh::replaceActiveMessageRetries(
int replacement_flood_slot = -1;
bool found_prior = false;
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (i / MAX_DIRECT_RETRY_SLOTS != replacement_packet->radio_profile) continue;
if (!_direct_retries[i].active) continue;
if (_direct_retries[i].trigger_packet == replacement_packet) {
replacement_direct_slot = i;
@@ -2092,7 +2151,8 @@ void Mesh::replaceActiveMessageRetries(
found_prior = true;
}
}
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 != replacement_packet->radio_profile) continue;
if (!_flood_retries[i].active) continue;
if (_flood_retries[i].trigger_packet == replacement_packet) {
replacement_flood_slot = i;
@@ -2107,7 +2167,8 @@ void Mesh::replaceActiveMessageRetries(
}
if (found_prior) {
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (i / MAX_DIRECT_RETRY_SLOTS != replacement_packet->radio_profile) continue;
if (i == replacement_direct_slot || !_direct_retries[i].active
|| !_direct_retries[i].has_message_replacement_key
|| _direct_retries[i].message_timestamp == message_timestamp
@@ -2117,7 +2178,8 @@ void Mesh::replaceActiveMessageRetries(
}
retireDirectRetrySlot(i);
}
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 != replacement_packet->radio_profile) continue;
if (i == replacement_flood_slot || !_flood_retries[i].active
|| !_flood_retries[i].has_message_replacement_key
|| _flood_retries[i].message_timestamp == message_timestamp
@@ -2143,7 +2205,8 @@ void Mesh::replaceActiveMessageRetries(
// new packet. If no slot exists, the role's ordinary retry policy declined
// this message and there is no retry state to tag.
if (replacement_direct_slot < 0 && replacement_flood_slot < 0) {
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (i / MAX_DIRECT_RETRY_SLOTS != replacement_packet->radio_profile) continue;
if (_direct_retries[i].active
&& _direct_retries[i].trigger_packet == replacement_packet) {
replacement_direct_slot = i;
@@ -2151,7 +2214,8 @@ void Mesh::replaceActiveMessageRetries(
}
}
if (replacement_direct_slot < 0) {
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 != replacement_packet->radio_profile) continue;
if (_flood_retries[i].active
&& _flood_retries[i].trigger_packet == replacement_packet) {
replacement_flood_slot = i;
@@ -2179,13 +2243,13 @@ bool Mesh::hasActiveRetries(const uint8_t retry_key[MAX_HASH_SIZE]) const {
return false;
}
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_DIRECT_RETRY_SLOTS; i++) {
if (_direct_retries[i].active
&& memcmp(retry_key, _direct_retries[i].retry_key, MAX_HASH_SIZE) == 0) {
return true;
}
}
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_FLOOD_RETRY_SLOTS; i++) {
if (_flood_retries[i].active
&& memcmp(retry_key, _flood_retries[i].retry_key, MAX_HASH_SIZE) == 0) {
return true;
@@ -2228,7 +2292,9 @@ void Mesh::watchForwardedAdvertEcho(const Packet* packet) {
int slot_idx = -1;
for (int i = 0; i < MAX_RECENT_ADVERT_ECHOS; i++) {
RecentAdvertEchoEntry& entry = _recent_advert_echoes[i];
if (entry.valid && memcmp(entry.packet_hash, packet_hash, MAX_HASH_SIZE) == 0) {
if (entry.valid && entry.radio_profile == packet->radio_profile
&& entry.radio_generation == packet->radio_generation
&& memcmp(entry.packet_hash, packet_hash, MAX_HASH_SIZE) == 0) {
if (entry.confirmed) {
return;
}
@@ -2254,6 +2320,8 @@ void Mesh::watchForwardedAdvertEcho(const Packet* packet) {
entry.advert_timestamp = advert_timestamp;
entry.watch_started_at = now_millis;
entry.progress_marker = packet->getPathHashCount();
entry.radio_profile = packet->radio_profile;
entry.radio_generation = packet->radio_generation;
entry.confirmed = false;
entry.valid = true;
}
@@ -2273,7 +2341,8 @@ void Mesh::observeForwardedAdvertEcho(const Packet* packet) {
uint32_t now_millis = _ms->getMillis();
for (int i = 0; i < MAX_RECENT_ADVERT_ECHOS; i++) {
RecentAdvertEchoEntry& entry = _recent_advert_echoes[i];
if (!entry.valid || entry.confirmed
if (!entry.valid || entry.confirmed || entry.radio_profile != packet->radio_profile
|| entry.radio_generation != packet->radio_generation
|| memcmp(entry.packet_hash, packet_hash, MAX_HASH_SIZE) != 0) {
continue;
}
@@ -2306,6 +2375,8 @@ bool Mesh::shouldSuppressEchoedAdvertForward(const Packet* packet) const {
for (int i = 0; i < MAX_RECENT_ADVERT_ECHOS; i++) {
const RecentAdvertEchoEntry& entry = _recent_advert_echoes[i];
if (entry.valid && entry.confirmed && entry.advert_timestamp == advert_timestamp
&& entry.radio_profile == packet->radio_profile
&& entry.radio_generation == packet->radio_generation
&& memcmp(entry.packet_hash, packet_hash, MAX_HASH_SIZE) == 0) {
return true;
}
@@ -2320,7 +2391,8 @@ bool Mesh::cancelFloodRetryOnEcho(const Packet* packet) {
packet->calculatePacketHash(recv_key);
bool cleared = false;
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_retries[i].active || memcmp(recv_key, _flood_retries[i].retry_key, MAX_HASH_SIZE) != 0) {
continue;
}
@@ -2346,7 +2418,7 @@ bool Mesh::cancelFloodRetryOnEcho(const Packet* packet) {
void Mesh::armFloodRetryOnSendComplete(const Packet* packet) {
if (_active_flood_retry_count == 0) return;
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_FLOOD_RETRY_SLOTS; i++) {
if (!_flood_retries[i].active) {
continue;
}
@@ -2449,7 +2521,7 @@ void Mesh::armFloodRetryOnSendComplete(const Packet* packet) {
void Mesh::clearPendingFloodRetryOnSendFail(const Packet* packet) {
if (_active_flood_retry_count == 0) return;
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
for (int i = 0; i < TOTAL_FLOOD_RETRY_SLOTS; i++) {
if (!_flood_retries[i].active) {
continue;
}
@@ -2484,7 +2556,8 @@ void Mesh::maybeScheduleFloodRetry(const Packet* packet, uint8_t priority) {
uint8_t retry_key[MAX_HASH_SIZE];
packet->calculatePacketHash(retry_key);
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_retries[i].active
&& memcmp(retry_key, _flood_retries[i].retry_key, MAX_HASH_SIZE) == 0) {
return; // the normal flood still sends, but only one retry sequence owns this logical packet
@@ -2492,7 +2565,8 @@ void Mesh::maybeScheduleFloodRetry(const Packet* packet, uint8_t priority) {
}
int slot_idx = -1;
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_retries[i].active) {
slot_idx = i;
break;
+19 -3
View File
@@ -42,6 +42,9 @@ namespace mesh {
#define MAX_FLOOD_RETRY_SLOTS 6
#endif
#define TOTAL_DIRECT_RETRY_SLOTS (2 * MAX_DIRECT_RETRY_SLOTS)
#define TOTAL_FLOOD_RETRY_SLOTS (2 * MAX_FLOOD_RETRY_SLOTS)
#ifndef MAX_RECENT_ADVERT_ECHOS
#define MAX_RECENT_ADVERT_ECHOS 8
#endif
@@ -126,6 +129,8 @@ class Mesh : public Dispatcher {
uint8_t progress_marker;
bool confirmed;
bool valid;
uint8_t radio_profile;
uint32_t radio_generation;
};
static const uint8_t OTA_REQUEST_TRACK_SLOTS = 4;
@@ -139,8 +144,8 @@ class Mesh : public Dispatcher {
RTCClock* _rtc;
RNG* _rng;
MeshTables* _tables;
DirectRetryEntry _direct_retries[MAX_DIRECT_RETRY_SLOTS];
FloodRetryEntry _flood_retries[MAX_FLOOD_RETRY_SLOTS];
DirectRetryEntry _direct_retries[TOTAL_DIRECT_RETRY_SLOTS];
FloodRetryEntry _flood_retries[TOTAL_FLOOD_RETRY_SLOTS];
RecentAdvertEchoEntry _recent_advert_echoes[MAX_RECENT_ADVERT_ECHOS];
OtaRequestTrackEntry _ota_request_track[OTA_REQUEST_TRACK_SLOTS] = {};
uint32_t _ota_relay_decay_at = 0;
@@ -152,6 +157,8 @@ class Mesh : public Dispatcher {
uint8_t _next_recent_advert_echo;
unsigned long _next_direct_retry_timeout;
unsigned long _next_flood_retry_timeout;
uint32_t _retry_radio_generations[2] = {};
RadioCrossMode _retry_cross_mode = RadioCrossMode::Auto;
void removePathPrefix(Packet* packet, uint8_t prefix_count);
void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis);
@@ -201,6 +208,7 @@ protected:
DispatcherAction onRecvPacket(Packet* pkt) override;
void onTracePacketQueuedForSend(Packet* packet) override;
void onSendComplete(Packet* packet) override;
void onRadioProfileCopyQueued(Packet* packet, const Packet* original, uint8_t priority) override;
void onSendFail(Packet* packet) override;
bool allowPacketTransmit(const Packet* packet) const override;
bool usePassiveChannelCheck(const Packet* packet) const override;
@@ -387,7 +395,7 @@ protected:
/**
* \brief Called exactly once whenever an active flood-retry slot is released.
*/
virtual void onFloodRetrySlotReleased(const uint8_t* retry_key) { }
virtual void onFloodRetrySlotReleased(const uint8_t* retry_key, uint8_t radio_profile) { }
/**
* \returns number of extra (Direct) ACK transmissions wanted.
@@ -568,6 +576,14 @@ public:
RNG* getRNG() const { return _rng; }
RTCClock* getRTCClock() const { return _rtc; }
bool isAnyTempRadioActive() const {
return isTempRadioActive() || (_radio->profiles() && _radio->profiles()->secondary_temporary);
}
bool isPacketOnTempRadio(const Packet* packet) const {
const auto* p = _radio->profiles();
if (!p || !packet) return isTempRadioActive();
return packet->radio_profile == 1 ? p->secondary_temporary : p->primary_temporary;
}
Packet* createAdvert(const LocalIdentity& id, const uint8_t* app_data=NULL, size_t app_data_len=0);
Packet* createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t len);
+6
View File
@@ -12,6 +12,9 @@ Packet::Packet() {
_snr = 0;
tx_cr = 0;
flood_retry_policy = FLOOD_RETRY_POLICY_DEFAULT;
radio_generation = radio_origin_generation = 0;
radio_profile = radio_origin = 0;
radio_bound = false; radio_local = true;
}
bool Packet::isValidPathLen(uint8_t path_len) {
@@ -74,6 +77,9 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) {
_snr = 0;
tx_cr = 0;
flood_retry_policy = FLOOD_RETRY_POLICY_DEFAULT;
radio_generation = radio_origin_generation = 0;
radio_profile = radio_origin = 0;
radio_bound = false; radio_local = true;
header = src[i++];
if (getPayloadVer() > PAYLOAD_VER_1) return false;
+6
View File
@@ -59,6 +59,12 @@ public:
int8_t _snr;
uint8_t tx_cr; // volatile local-only TX coding-rate override; not serialized
uint8_t flood_retry_policy; // volatile receive-policy result; not serialized
uint32_t radio_generation = 0;
uint32_t radio_origin_generation = 0;
uint8_t radio_profile = 0;
uint8_t radio_origin = 0;
bool radio_bound = false; // retries retain one profile and never fan out again
bool radio_local = true;
/**
* \brief calculate the hash of payload + type
+174
View File
@@ -0,0 +1,174 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <math.h>
namespace mesh {
enum class RadioProfileMode : uint8_t { Off = 0, Rx = 1, RxTx = 2 };
enum class RadioCrossMode : uint8_t { Auto = 0, On = 1, Off = 2 };
// These are modulation profiles of ONE transceiver, never additional radios.
struct RadioProfileParams {
float freq = 0;
float bw = 0;
uint16_t preamble = 0; // 0 = automatic, otherwise symbols
uint8_t sf = 0;
uint8_t cr = 0;
bool operator==(const RadioProfileParams& other) const {
return freq == other.freq && bw == other.bw && sf == other.sf
&& cr == other.cr && preamble == other.preamble;
}
bool operator!=(const RadioProfileParams& other) const { return !(*this == other); }
};
struct RadioProfileConfig {
RadioProfileParams params;
RadioProfileMode mode = RadioProfileMode::Off;
};
// Portable timing/routing model, also used by the native tests. Hardware access
// remains in RadioLibWrapper; timers and persisted settings remain in the CLI.
class RadioProfiles {
public:
// Normal RX uses a microsecond window, so fractional symbols are supported.
static constexpr double SlowListenSymbols = 4.8;
static constexpr uint8_t MinFastListenSymbols = 4;
static constexpr uint8_t AcquisitionSymbols = 8;
static constexpr uint16_t MaxPreamble = 65528;
// The mixed-profile bench tests measured ~1.2 ms/visit with fast RX. Normal
// drivers may wake a TCXO; production V4 measured up to 4.4 ms. Budget
// 6 ms per change plus 4 ms of loop jitter. Packet receptions can extend
// a visit; these budgets describe an otherwise idle scan.
static constexpr uint32_t SwitchBudgetUs = 6000;
static constexpr uint32_t LoopBudgetUs = 4000;
RadioProfileParams primary;
RadioProfileConfig secondary;
bool primary_temporary = false;
bool secondary_temporary = false;
RadioCrossMode cross = RadioCrossMode::Auto;
uint32_t generation[2] = {1, 1};
uint32_t switches = 0;
uint32_t rx_packets[2] = {};
uint32_t tx_packets[2] = {};
uint32_t switch_failures = 0;
uint32_t longest_switch_us = 0;
bool enabled() const { return secondary.mode != RadioProfileMode::Off; }
bool canTransmit(uint8_t profile) const {
return profile == 0 || (profile == 1 && secondary.mode == RadioProfileMode::RxTx);
}
bool canCross() const {
return cross == RadioCrossMode::On || (cross == RadioCrossMode::Auto
&& primary_temporary == secondary_temporary);
}
uint8_t transmitMask(uint8_t origin) const {
if (origin > 1) origin = 0;
uint8_t result = canTransmit(origin) ? (1U << origin) : 0;
if (enabled() && canCross() && canTransmit(origin ^ 1)) result |= 1U << (origin ^ 1);
return result;
}
const RadioProfileParams& params(uint8_t profile) const {
return profile == 1 ? secondary.params : primary;
}
static bool valid(const RadioProfileParams& p) {
if (!isfinite(p.freq) || !isfinite(p.bw) || p.freq < 150 || p.freq > 2500
|| p.sf < 5 || p.sf > 12 || p.cr < 5 || p.cr > 8
|| (p.preamble != 0 && (p.preamble < 8 || p.preamble > MaxPreamble))) return false;
const float bandwidths[] = {7.8f, 10.4f, 15.6f, 20.8f, 31.25f, 41.7f,
62.5f, 125, 250, 500, 1000, 812.5f, 1625};
for (float bw : bandwidths) if (fabsf(p.bw - bw) < 0.01f) return true;
return false;
}
static double symbolUs(const RadioProfileParams& p) {
return p.bw > 0 && p.sf <= 12 ? (double)(1U << p.sf) * 1000.0 / p.bw : 0;
}
static bool safePreamble(const RadioProfileParams& p, uint16_t symbols) {
const double symbol = symbolUs(p);
if (symbol <= 0) return false;
const int de = symbol >= 16000 ? 1 : 0;
const double payload = 8 + ceil((8.0 * 255 - 4 * p.sf
+ (p.sf <= 6 ? 20 : 28) + 16) / (4 * (p.sf - 2 * de))) * 8;
// RadioLib's microsecond airtime calculation multiplies by four before
// dividing. Reject settings that overflow that intermediate for a full
// packet, including a retry using coding rate 4/8.
return (symbols + (p.sf <= 6 ? 6.25 : 4.25) + payload) * symbol * 4 <= UINT32_MAX;
}
uint8_t slowerProfile() const {
return symbolUs(primary) >= symbolUs(secondary.params) ? 0 : 1;
}
static double roundPreamble(double symbols) {
return ceil((symbols < 32 ? 32 : symbols) / 8.0) * 8.0;
}
double automaticPreamble(uint8_t profile) const {
const double symbol = symbolUs(params(profile));
if (symbol <= 0) return 32;
// Allow two slow-channel visits per slow preamble: one short detection
// opportunity was insufficient in the production V4 test. The fast
// preamble covers the blind interval during the slow visit and two sets
// of acquisition symbols (the extra eight come from the hardware test).
const double overhead = 2.0 * SwitchBudgetUs + LoopBudgetUs;
if (profile == slowerProfile()) {
const double fast_visit = MinFastListenSymbols * symbolUs(params(profile ^ 1));
return roundPreamble(2.0 * (SlowListenSymbols * symbol + fast_visit + overhead) / symbol);
}
const double slow_visit = SlowListenSymbols * symbolUs(params(profile ^ 1));
double symbols = roundPreamble((slow_visit + overhead) / symbol + 2 * AcquisitionSymbols);
// Production V4 + XIAO testing lost SF8/500 packets at 72 and 80;
// 88 passed with the paired SF7/62.5 profile. Keep that measured floor
// even when the theoretical blind interval permits a shorter preamble.
const auto& other = params(profile ^ 1);
if (params(profile).sf == 8 && params(profile).bw == 500
&& other.sf == 7 && other.bw == 62.5f && symbols < 88) symbols = 88;
return symbols;
}
uint32_t listenUs(uint8_t profile, uint16_t slow_preamble = 0) const {
const uint8_t slow = slowerProfile();
const double minimum = symbolUs(params(profile))
* (profile == slow ? SlowListenSymbols : MinFastListenSymbols);
if (!enabled() || profile == slow) return (uint32_t)ceil(minimum);
if (!slow_preamble) slow_preamble = preamble(slow, 32);
const double available = (double(slow_preamble) / 2.0 - SlowListenSymbols)
* symbolUs(params(slow)) - 2.0 * SwitchBudgetUs - LoopBudgetUs;
return available > minimum ? (uint32_t)floor(available) : (uint32_t)ceil(minimum);
}
uint16_t preamble(uint8_t profile, uint16_t single_profile_default) const {
const auto& p = params(profile);
if (p.preamble) return p.preamble;
if (!enabled()) return single_profile_default;
double symbols = automaticPreamble(profile);
if (symbols < single_profile_default) symbols = single_profile_default;
if (symbols > MaxPreamble) symbols = MaxPreamble;
return (uint16_t)symbols;
}
bool automaticPreambleFits() const {
for (uint8_t i = 0; i < (enabled() ? 2 : 1); ++i) {
if (!valid(params(i)) || !safePreamble(params(i), preamble(i, 32))) return false;
if (!enabled()) continue;
if (!params(i).preamble && automaticPreamble(i) > MaxPreamble) return false;
}
if (enabled()) {
const uint8_t slow = slowerProfile();
const double available = (double(preamble(slow, 32)) / 2.0 - SlowListenSymbols)
* symbolUs(params(slow)) - 2.0 * SwitchBudgetUs - LoopBudgetUs;
if (available < MinFastListenSymbols * symbolUs(params(slow ^ 1))) return false;
}
return true;
}
void setPrimary(const RadioProfileParams& p, bool temporary) {
if (primary != p || primary_temporary != temporary) ++generation[0];
primary = p;
primary_temporary = temporary;
}
void setSecondary(const RadioProfileConfig& p, bool temporary) {
if (secondary.params != p.params || secondary.mode != p.mode
|| secondary_temporary != temporary) ++generation[1];
secondary = p;
secondary_temporary = temporary;
}
};
} // namespace mesh
+71 -15
View File
@@ -142,6 +142,7 @@ static bool isGpioConfig(const char* config) {
#endif
void CommonCLI::loop() {
_radio_profiles.loop();
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
_user_gpio.loop();
UserGpio::Completion completion;
@@ -732,6 +733,7 @@ static void formatSnrDbX4Short(char* dest, size_t dest_len, int16_t snr_x4) {
}
void CommonCLI::loadPrefs(FILESYSTEM* fs) {
_radio_profiles.begin(fs, _callbacks->getProfileRadio(), _rtc);
const bool display_settings_loaded = mesh::ui::loadDisplayPowerSettings(fs, false);
(void)display_settings_loaded;
bool is_fresh_install = false;
@@ -2462,6 +2464,20 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
mesh::cli::normalizeCommandVerb(command);
if (_radio_profiles.handle(command, reply)) return;
if (strncmp(command, "set tempradio ", 14) == 0) {
handleCommand(sender_timestamp, command + 4, reply);
return;
}
if (!strcmp(command, "get tempradio") || !strcmp(command, "tempradio")) {
const auto* radio = _callbacks->getProfileRadio();
if (!radio || !radio->profiles() || !radio->profiles()->primary_temporary) strcpy(reply, "> off");
else {
const auto& p = radio->profiles()->primary;
snprintf(reply, 160, "> %.3f,%.3f,%u,%u,%u", p.freq, p.bw, p.sf, p.cr, radio->profilePreamble(0));
}
return;
}
if (mesh::ui::handleDisplayPowerCommand(command, reply, 160)) return;
// Observer-only top-level commands (ota check/update, tls.bundletest, alert test)
@@ -2601,15 +2617,18 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
strcpy(reply, "Error: unsupported");
}
} else if (memcmp(command, "tempradio ", 10) == 0) {
strcpy(tmp, &command[10]);
uint16_t preamble = 0;
if (!mesh::RadioProfileCLI::parseSuffix(command + 10, 5, tmp, sizeof(tmp), preamble)) {
strcpy(reply, "Error, use: tempradio f,bw,sf,cr,minutes[,preamble]"); return;
}
const char *parts[5];
int num = mesh::Utils::parseTextParts(tmp, parts, 5);
float freq = 0.0f;
float bw = 0.0f;
uint8_t sf = num > 2 ? atoi(parts[2]) : 0;
uint8_t cr = num > 3 ? atoi(parts[3]) : 0;
uint32_t sf = 0, cr = 0;
uint32_t temp_timeout_mins = 0;
if (num == 5
&& parseUint32Strict(parts[2], sf) && parseUint32Strict(parts[3], cr)
&& parseUint32Strict(parts[4], temp_timeout_mins)
&& temp_timeout_mins > 0
&& (uint64_t)getRTCClock()->getCurrentTime() + 2
@@ -2618,8 +2637,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
&& mesh::cli::parseDecimalStrict(parts[1], bw)
&& freq >= 150.0f && freq <= 2500.0f
&& sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8
&& isValidLoRaBandwidth(bw)) {
_callbacks->applyTempRadioParams(freq, bw, sf, cr, temp_timeout_mins);
&& isValidLoRaBandwidth(bw)
&& _radio_profiles.acceptsPrimary(freq, bw, sf, cr, preamble)) {
_callbacks->applyTempRadioParams(freq, bw, sf, cr, temp_timeout_mins, preamble);
char duration[64];
const uint32_t seconds = (uint32_t)temp_timeout_mins * 60UL;
mesh::RepeaterRadioTiming::formatDuration(duration, sizeof(duration), seconds);
@@ -2659,7 +2679,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
sprintf(reply, "%s", _board->getManufacturerName());
#if defined(ENABLE_OTA)
} else if (memcmp(command, "ota", 3) == 0 && (command[3] == 0 || command[3] == ' ')) {
if (!_callbacks->isTempRadioActive() && otaCommandNeedsTempRadio(command)) {
if (!_callbacks->isTempRadioActive() && !_radio_profiles.secondaryTemporary()
&& otaCommandNeedsTempRadio(command)) {
strcpy(reply, "LoRa OTA needs temp radio on every node. Run: tempradio 909.950,250,5,5,120");
} else {
mesh::ota::handle_ota_command(command, reply, *_board);
@@ -3118,6 +3139,19 @@ bool CommonCLI::handleSdCardGetCmd(const char* config, char* reply) {
void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) {
const char* config = &command[4];
// Infrastructure roles dispatch here rather than through CommonRadioPrefs.
if (strncmp(config, "path.hash.mode", 14) == 0
&& (config[14] == 0 || config[14] == ' ' || config[14] == '\t')) {
uint32_t mode = 0;
if (mesh::cli::parseUnsignedIntegerStrict(config + 14, mode) && mode <= 2) {
_prefs->path_hash_mode = static_cast<uint8_t>(mode);
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0,1, or 2");
}
return;
}
#if ENV_INCLUDE_GPS == 1
if (strncmp(config, "gps ", 4) == 0) {
if (strcmp(config + 4, "on") != 0 && strcmp(config + 4, "off") != 0) {
@@ -3603,10 +3637,17 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
float bw = 0.0f;
uint8_t sf = 0;
uint8_t cr = 0;
if (mesh::cli::parseRadioTupleStrict(&config[6], freq, bw, sf, cr)
uint16_t preamble = 0;
char legacy[96];
if (mesh::RadioProfileCLI::parseSuffix(&config[6], 4, legacy, sizeof(legacy), preamble)
&& mesh::cli::parseRadioTupleStrict(legacy, freq, bw, sf, cr)
&& freq >= 150.0f && freq <= 2500.0f
&& sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8
&& isValidLoRaBandwidth(bw)) {
&& isValidLoRaBandwidth(bw)
&& _radio_profiles.acceptsPrimary(freq, bw, sf, cr, preamble)) {
if (!_radio_profiles.savePrimaryPreamble(preamble)) {
strcpy(reply, "Error: preamble could not be saved"); return;
}
_prefs->sf = sf;
_prefs->cr = cr;
_prefs->freq = freq;
@@ -3634,12 +3675,16 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
float freq, bw;
uint8_t sf, cr;
uint32_t start_time, end_time;
if (!parseScheduledRadioArgs(&config[8], false, freq, bw, sf, cr, start_time, end_time)) {
uint16_t preamble = 0;
char legacy[120];
if (!mesh::RadioProfileCLI::parseSuffix(&config[8], 5, legacy, sizeof(legacy), preamble)
|| !parseScheduledRadioArgs(legacy, false, freq, bw, sf, cr, start_time, end_time)) {
strcpy(reply, "Error, use: set radioat f,bw,sf,cr,start");
} else if (freq < 150.0f || freq > 2500.0f || sf < 5 || sf > 12 || cr < 5 || cr > 8 || !isValidLoRaBandwidth(bw)) {
} else if (freq < 150.0f || freq > 2500.0f || sf < 5 || sf > 12 || cr < 5 || cr > 8 || !isValidLoRaBandwidth(bw)
|| !_radio_profiles.acceptsPrimary(freq, bw, sf, cr, preamble)) {
strcpy(reply, "Error, invalid radio params");
} else {
_callbacks->addScheduledRadioParams(false, freq, bw, sf, cr, start_time, end_time, reply);
_callbacks->addScheduledRadioParams(false, freq, bw, sf, cr, start_time, end_time, reply, preamble);
if (strncmp(reply, "OK", 2) == 0) {
appendRxPowerSavingAdjustmentNote(reply, _prefs, sf, bw);
}
@@ -3648,12 +3693,16 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
float freq, bw;
uint8_t sf, cr;
uint32_t start_time, end_time;
if (!parseScheduledRadioArgs(&config[12], true, freq, bw, sf, cr, start_time, end_time)) {
uint16_t preamble = 0;
char legacy[120];
if (!mesh::RadioProfileCLI::parseSuffix(&config[12], 6, legacy, sizeof(legacy), preamble)
|| !parseScheduledRadioArgs(legacy, true, freq, bw, sf, cr, start_time, end_time)) {
strcpy(reply, "Error, use: set tempradioat f,bw,sf,cr,start,end");
} else if (freq < 150.0f || freq > 2500.0f || sf < 5 || sf > 12 || cr < 5 || cr > 8 || !isValidLoRaBandwidth(bw)) {
} else if (freq < 150.0f || freq > 2500.0f || sf < 5 || sf > 12 || cr < 5 || cr > 8 || !isValidLoRaBandwidth(bw)
|| !_radio_profiles.acceptsPrimary(freq, bw, sf, cr, preamble)) {
strcpy(reply, "Error, invalid radio params");
} else {
_callbacks->addScheduledRadioParams(true, freq, bw, sf, cr, start_time, end_time, reply);
_callbacks->addScheduledRadioParams(true, freq, bw, sf, cr, start_time, end_time, reply, preamble);
if (strncmp(reply, "OK", 2) == 0) {
appendRxPowerSavingAdjustmentNote(reply, _prefs, sf, bw);
}
@@ -4288,8 +4337,8 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
savePrefs();
sprintf(reply, "OK - reboot.interval set to %d", _prefs->reboot_interval);
}
#if defined(USE_LR2021)
} else if (strcmp(config, "extra.sf") == 0 || memcmp(config, "extra.sf ", 9) == 0) {
#if defined(USE_LR2021)
uint8_t sideDetSFs[mesh::lr2021::STORED_SIDE_DETECTOR_BYTES] = {};
uint8_t num = 0;
const char* value = config[8] == '\0' ? "" : &config[9];
@@ -4307,6 +4356,8 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "Invalid extra SF config");
}
}
#else
strcpy(reply, "Error: extra.sf requires an LR2021 radio");
#endif
} else {
sprintf(reply, "unknown config: %s", config);
@@ -4538,6 +4589,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(freq, StrHelper::ftoa(_prefs->freq));
strcpy(bw, StrHelper::ftoa3(_prefs->bw));
sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr);
_radio_profiles.appendSavedPreamble(reply, 160, _prefs->sf, _prefs->bw);
} else if (configKeyEquals(config, "rxdelay")) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base));
} else if (configKeyEquals(config, "txdelay")) {
@@ -4738,6 +4790,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
sprintf(reply, "> %d", (uint8_t)_prefs->reboot_interval);
}
} else if (strcmp(config, "extra.sf") == 0) {
#if defined(USE_LR2021)
char* tmp = reply;
for (int i = 0; i < 3 && _prefs->extra_sf[i] != 0; i++) {
tmp += sprintf(tmp, "%s%d", (i == 0) ? "" : ",", _prefs->extra_sf[i]);
@@ -4745,6 +4798,9 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
if (tmp == reply) {
strcpy(reply, "No extra SF configured");
}
#else
strcpy(reply, "Error: extra.sf requires an LR2021 radio");
#endif
} else {
mesh::cli::formatUnknownSetting(reply, 160, config);
}
+8 -2
View File
@@ -13,6 +13,7 @@
#include <helpers/CommonRadioPrefs.h>
#include <helpers/DynamicConfigSerializer.h>
#include <helpers/RepeaterRadioTiming.h>
#include <helpers/RadioProfileCLI.h>
#ifndef DEFAULT_CAD_ENABLED
#define DEFAULT_CAD_ENABLED 0
@@ -447,6 +448,7 @@ struct LegacyObserverTail {
class CommonCLICallbacks {
public:
virtual mesh::Radio* getProfileRadio() { return nullptr; }
virtual mesh::FloodAdvertLimiter* getFloodAdvertLimiter() { return nullptr; }
// Ordinary CommonCLI setters mutate NodePrefs and therefore save only the
// common image. Observer setters explicitly request Observer; only
@@ -511,7 +513,7 @@ public:
virtual mesh::LocalIdentity& getSelfId() = 0;
virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0;
virtual void clearStats() = 0;
virtual void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) = 0;
virtual void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble = 0) = 0;
virtual uint32_t getTempRadioDurationSeconds() const { return 0; }
virtual void appendTempRadioTimingNote(char* reply, size_t size, uint32_t seconds) const { }
// Cancel pending/active temporary-radio windows and restore the saved tuple
@@ -521,7 +523,8 @@ public:
virtual bool isTempRadioActive() const { return false; }
#endif
virtual void 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 = 0) {
(void)preamble;
(void)temporary;
(void)freq;
(void)bw;
@@ -744,6 +747,7 @@ class CommonCLI {
bool _observer_save_succeeded = false;
#endif
bool _com_prefs_needs_upgrade = false; // old-format legacy prefs detected; rewrite once after load
mesh::RadioProfileCLI _radio_profiles;
mesh::RTCClock* getRTCClock() { return _rtc; }
void savePrefs(
@@ -780,6 +784,8 @@ class CommonCLI {
bool handleObserverCommand(uint32_t sender_timestamp, char* command, char* reply);
public:
mesh::RadioProfileCLI& radioProfiles() { return _radio_profiles; }
const mesh::RadioProfileCLI& radioProfiles() const { return _radio_profiles; }
static bool calculateRxPowerSavingLevel(uint32_t level, uint8_t sf, float bw, uint32_t preamble,
uint32_t* rx_us, uint32_t* sleep_us);
static bool recalculateRxPowerSavingFromLevel(NodePrefs* prefs);
+8 -1
View File
@@ -18,6 +18,8 @@ struct DeferredCliCommand {
uint32_t sender_timestamp;
uint32_t request_id;
uint8_t path_hash_size;
uint8_t radio_profile = 0;
uint32_t radio_generation = 0;
uint8_t secret[PUB_KEY_SIZE];
char command[MAX_PACKET_PAYLOAD + 1];
@@ -31,7 +33,8 @@ struct DeferredCliCommand {
bool enqueue(int new_client_index, uint32_t new_sender_timestamp,
uint8_t new_path_hash_size, const uint8_t* new_secret,
const char* new_command, size_t command_len,
uint32_t new_request_id = 0) {
uint32_t new_request_id = 0, uint8_t new_radio_profile = 0,
uint32_t new_radio_generation = 0) {
if (pending || new_secret == NULL || new_command == NULL
|| command_len >= sizeof(command)) {
return false;
@@ -42,6 +45,8 @@ struct DeferredCliCommand {
request_id = new_request_id != 0
? new_request_id : new_sender_timestamp;
path_hash_size = new_path_hash_size;
radio_profile = new_radio_profile;
radio_generation = new_radio_generation;
memcpy(secret, new_secret, sizeof(secret));
memcpy(command, new_command, command_len);
command[command_len] = 0;
@@ -65,6 +70,8 @@ struct DeferredCliCommand {
sender_timestamp = 0;
request_id = 0;
path_hash_size = 1;
radio_profile = 0;
radio_generation = 0;
memset(secret, 0, sizeof(secret));
memset(command, 0, sizeof(command));
}
+421
View File
@@ -0,0 +1,421 @@
#include "RadioProfileCLI.h"
#include "RadioProfileCommandUtils.h"
#include "radiolib/RXPowerSaving.h"
#include <Arduino.h>
#include <stdio.h>
#include <string.h>
namespace mesh {
namespace {
constexpr size_t ImageSize = 24;
const char* const ImagePath = "/radio_profiles";
const char* const BackupPath = "/radio_profiles.bak";
const char* const TempPath = "/radio_profiles.tmp";
uint32_t checksum(const uint8_t* p, size_t n) {
uint32_t crc = 0xffffffffU;
while (n--) {
crc ^= *p++;
for (int i = 0; i < 8; ++i) crc = (crc >> 1) ^ ((crc & 1) ? 0xedb88320U : 0);
}
return crc;
}
const char* modeName(RadioProfileMode m) {
return m == RadioProfileMode::RxTx ? "rxtx" : m == RadioProfileMode::Rx ? "rx" : "off";
}
bool parseMode(const char* text, RadioProfileMode& mode) {
if (!strcmp(text, "rx")) mode = RadioProfileMode::Rx;
else if (!strcmp(text, "rxtx") || !strcmp(text, "rx&tx")) mode = RadioProfileMode::RxTx;
else return false;
return true;
}
unsigned split(char* s, char* parts[], unsigned maximum) {
unsigned count = 0;
do {
if (count == maximum || !*s) return 0;
parts[count++] = s;
char* comma = strchr(s, ',');
if (!comma) break;
*comma = 0;
s = comma + 1;
} while (true);
return count;
}
bool parsePreamble(const char* s, uint16_t& result) {
return cli::parseRadioPreamble(s, result);
}
}
bool RadioProfileCLI::readImage(const char* path, uint8_t* bytes, size_t size) {
#if defined(NRF52_PLATFORM)
File file(*fs_);
if (!file.open(path, FILE_O_READ)) return false;
#elif defined(STM32_PLATFORM)
File file = fs_->open(path, FILE_O_READ);
#else
File file = fs_->open(path, "r");
#endif
if (!file) return false;
bool ok = file.size() == size && file.read(bytes, size) == (int)size;
file.close();
if (!ok) return false;
uint32_t stored;
memcpy(&stored, bytes + size - 4, 4);
return bytes[0] == 'R' && bytes[1] == '2' && bytes[2] == 1
&& stored == checksum(bytes, size - 4);
}
bool RadioProfileCLI::writeImage(const char* path, const uint8_t* bytes, size_t size) {
if (fs_->exists(path)) fs_->remove(path);
#if defined(NRF52_PLATFORM)
File file(*fs_);
if (!file.open(path, FILE_O_WRITE)) return false;
#elif defined(STM32_PLATFORM)
File file = fs_->open(path, FILE_O_WRITE);
#else
File file = fs_->open(path, "w");
#endif
if (!file) return false;
bool ok = file.write(bytes, size) == size;
file.flush();
file.close();
uint8_t verify[ImageSize];
return ok && readImage(path, verify, size) && memcmp(bytes, verify, size) == 0;
}
bool RadioProfileCLI::save(const RadioProfileConfig& config, uint16_t preamble, RadioCrossMode cross) {
if (!fs_ || hold_) return false;
uint8_t image[ImageSize] = {'R', '2', 1, (uint8_t)config.mode};
memcpy(image + 4, &config.params.freq, 4);
memcpy(image + 8, &config.params.bw, 4);
image[12] = config.params.sf; image[13] = config.params.cr;
memcpy(image + 14, &config.params.preamble, 2);
memcpy(image + 16, &preamble, 2);
image[18] = (uint8_t)cross;
const uint32_t crc = checksum(image, ImageSize - 4);
memcpy(image + ImageSize - 4, &crc, 4);
if (!writeImage(TempPath, image, sizeof(image))) return false;
if (fs_->exists(ImagePath)) {
if (fs_->exists(BackupPath)) fs_->remove(BackupPath);
if (!fs_->rename(ImagePath, BackupPath)) return false;
}
if (!fs_->rename(TempPath, ImagePath)) {
if (fs_->exists(BackupPath)) fs_->rename(BackupPath, ImagePath);
return false;
}
if (fs_->exists(BackupPath)) fs_->remove(BackupPath);
return true;
}
void RadioProfileCLI::begin(FILESYSTEM* fs, Radio* radio, RTCClock* rtc) {
fs_ = fs; radio_ = radio; rtc_ = rtc; last_ms_ = millis();
if (!fs_ || !radio_ || !radio_->profiles()) return;
uint8_t bytes[ImageSize];
bool loaded = readImage(ImagePath, bytes, sizeof(bytes));
if (!loaded && readImage(BackupPath, bytes, sizeof(bytes))) {
if (fs_->exists(ImagePath)) fs_->remove(ImagePath);
loaded = fs_->rename(BackupPath, ImagePath);
}
if (!loaded) {
hold_ = fs_->exists(ImagePath) || fs_->exists(BackupPath);
return;
}
saved_.mode = (RadioProfileMode)bytes[3];
memcpy(&saved_.params.freq, bytes + 4, 4);
memcpy(&saved_.params.bw, bytes + 8, 4);
saved_.params.sf = bytes[12]; saved_.params.cr = bytes[13];
memcpy(&saved_.params.preamble, bytes + 14, 2);
memcpy(&primary_preamble_, bytes + 16, 2);
cross_ = (RadioCrossMode)bytes[18];
if ((uint8_t)saved_.mode > 2 || (uint8_t)cross_ > 2
|| (saved_.mode != RadioProfileMode::Off && !radio_->validateProfile(saved_.params))
|| (primary_preamble_ && (primary_preamble_ < 8 || primary_preamble_ > RadioProfiles::MaxPreamble))) {
hold_ = true; saved_ = {}; primary_preamble_ = 0; cross_ = RadioCrossMode::Auto;
return;
}
stagePrimary(primary_preamble_, false);
publish();
}
bool RadioProfileCLI::savePrimaryPreamble(uint16_t symbols) {
if (symbols && (symbols < 8 || symbols > RadioProfiles::MaxPreamble)) return false;
if (!radio_ || !radio_->profiles()) return symbols == 0;
if (symbols == primary_preamble_) return true;
if (!save(saved_, symbols, cross_)) return false;
primary_preamble_ = symbols;
return true;
}
bool RadioProfileCLI::acceptsPrimary(float freq, float bw, uint8_t sf, uint8_t cr, uint16_t preamble) const {
if (!radio_ || !radio_->profiles()) return preamble == 0;
auto preview = *radio_->profiles();
auto& p = preview.primary;
p.freq = freq; p.bw = bw; p.sf = sf; p.cr = cr; p.preamble = preamble;
return radio_->validateProfile(p) && preview.automaticPreambleFits();
}
void RadioProfileCLI::publish() {
if (!radio_ || !radio_->profiles()) return;
radio_->profiles()->cross = cross_;
if (publish_pending_ && (int32_t)(millis() - publish_after_ms_) < 0) return;
publish_pending_ = false;
radio_->profiles()->setSecondary(temp_active_ ? temporary_ : saved_, temp_active_);
}
void RadioProfileCLI::loop() {
if (!radio_ || !radio_->profiles()) return;
const uint32_t now = millis(), elapsed = now - last_ms_;
last_ms_ = now;
if (temp_pending_ && (int32_t)(now - temp_start_ms_) >= 0) {
temporary_ = pending_temporary_;
const uint32_t late = now - temp_start_ms_;
temp_remaining_ms_ = late < pending_duration_ms_ ? pending_duration_ms_ - late : 0;
temp_pending_ = false; temp_active_ = temp_remaining_ms_ != 0;
} else if (temp_active_) {
if (elapsed >= temp_remaining_ms_) { temp_remaining_ms_ = 0; temp_active_ = false; }
else temp_remaining_ms_ -= elapsed;
}
const uint32_t epoch = rtc_ ? rtc_->getCurrentTime() : 0;
for (auto& s : schedules_) {
if (!s.active) continue;
if (s.temporary) {
if (elapsed >= s.remaining_ms || epoch >= s.end) {
if (s.started) { temp_active_ = false; temp_remaining_ms_ = 0; }
s = {}; continue;
}
s.remaining_ms -= elapsed;
}
if (s.started || epoch < s.start) continue;
if (s.temporary) {
if (temp_active_ || temp_pending_) continue;
temporary_ = s.config;
temp_remaining_ms_ = s.remaining_ms;
const uint32_t epoch_left = (s.end - epoch) * 1000UL;
if (temp_remaining_ms_ > epoch_left) temp_remaining_ms_ = epoch_left;
temp_active_ = true; s.started = true;
} else if (save(s.config, primary_preamble_, cross_)) {
saved_ = s.config; s = {};
} else {
// Avoid a flash write on every main-loop iteration after a storage fault.
s.start = epoch <= UINT32_MAX - 60 ? epoch + 60 : UINT32_MAX;
}
}
publish();
}
bool RadioProfileCLI::parseSuffix(const char* input, unsigned fields, char* legacy,
size_t capacity, uint16_t& preamble) {
return cli::parseRadioPreambleSuffix(input, fields, legacy, capacity, preamble);
}
void RadioProfileCLI::appendPreamble(char* reply, size_t capacity, uint8_t profile) const {
if (!radio_ || !radio_->profiles()) return;
const size_t used = strlen(reply);
if (used < capacity) snprintf(reply + used, capacity - used, ",preamble=%u",
(unsigned)radio_->profilePreamble(profile));
}
void RadioProfileCLI::appendSavedPreamble(char* reply, size_t capacity, uint8_t sf, float bw) const {
if (!radio_ || !radio_->profiles()) return;
auto preview = *radio_->profiles();
preview.primary.sf = sf; preview.primary.bw = bw;
preview.primary.preamble = primary_preamble_;
const size_t used = strlen(reply);
if (used < capacity) snprintf(reply + used, capacity - used, ",preamble=%u%s",
preview.preamble(0, rxPowerSavingPreambleForParams(sf, bw)), primary_preamble_ ? "" : " (auto)");
}
void RadioProfileCLI::formatConfig(char* reply, size_t capacity, const RadioProfileConfig& config,
bool temporary, uint32_t remaining) const {
if (config.mode == RadioProfileMode::Off) { snprintf(reply, capacity, "> off"); return; }
RadioProfiles preview = *radio_->profiles();
preview.secondary = config;
uint16_t preamble = preview.preamble(1, 32);
snprintf(reply, capacity, "> %.3f,%.3f,%u,%u,%s,%u%s", config.params.freq, config.params.bw,
config.params.sf, config.params.cr, modeName(config.mode), preamble,
config.params.preamble ? "" : " (auto)");
if (temporary) {
uint32_t minutes = (remaining + 59999UL) / 60000UL;
const size_t used = strlen(reply);
if (used < capacity) snprintf(reply + used, capacity - used, "; %lud%luh%lum (%lu min) left",
(unsigned long)(minutes / 1440), (unsigned long)(minutes / 60 % 24),
(unsigned long)(minutes % 60), (unsigned long)minutes);
}
}
bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity) {
const char* text = command;
enum { Get, Set, Delete } verb = Get;
if (!strncmp(text, "get ", 4)) text += 4;
else if (!strncmp(text, "set ", 4)) { text += 4; verb = Set; }
else if (!strncmp(text, "del ", 4)) { text += 4; verb = Delete; }
char key[24];
size_t n = strcspn(text, " \t");
if (n >= sizeof(key)) return false;
memcpy(key, text, n); key[n] = 0;
const char* args = text + n;
while (*args == ' ' || *args == '\t') ++args;
const bool base = !strcmp(key, "radio2");
const bool temporary = !strcmp(key, "tempradio2");
const bool scheduled = !strcmp(key, "radioat2") || !strcmp(key, "tempradioat2");
const bool scheduled_temp = !strcmp(key, "tempradioat2");
const bool crossing = !strcmp(key, "radio2.cross");
const bool status = !strcmp(key, "radio2.status");
const bool scan = !strcmp(key, "radio2.scan");
if (!base && !temporary && !scheduled && !crossing && !status && !scan) return false;
if (!radio_ || !radio_->profiles()) { snprintf(reply, capacity, "Error: radio profiles unsupported"); return true; }
if (command == text && *args && verb == Get && (temporary || base)) verb = Set;
if (crossing) {
if (verb == Get && !*args) {
snprintf(reply, capacity, "> %s", cross_ == RadioCrossMode::Auto ? "auto" : cross_ == RadioCrossMode::On ? "on" : "off");
} else if (verb == Set && (!strcmp(args, "auto") || !strcmp(args, "on") || !strcmp(args, "off"))) {
auto mode = !strcmp(args, "auto") ? RadioCrossMode::Auto : !strcmp(args, "on") ? RadioCrossMode::On : RadioCrossMode::Off;
if (save(saved_, primary_preamble_, mode)) { cross_ = mode; publish(); snprintf(reply, capacity, "OK"); }
else snprintf(reply, capacity, "Error: settings could not be saved");
} else snprintf(reply, capacity, "Error: use get/set radio2.cross [auto|on|off]");
return true;
}
if (scan) {
if (verb != Get || *args) { snprintf(reply, capacity, "Error: radio2.scan is read-only"); return true; }
const auto& p = *radio_->profiles();
if (!p.enabled()) snprintf(reply, capacity, "> off");
else {
const uint8_t slow = p.slowerProfile();
const uint16_t preamble = radio_->profilePreamble(slow);
snprintf(reply, capacity, "> slow=%s; listen_us=%lu,%lu; preamble=%u,%u",
slow ? "radio2" : "radio", (unsigned long)p.listenUs(0, preamble),
(unsigned long)p.listenUs(1, preamble), radio_->profilePreamble(0), radio_->profilePreamble(1));
}
return true;
}
if (status) {
if (verb != Get || *args) { snprintf(reply, capacity, "Error: radio2.status is read-only"); return true; }
const auto& p = *radio_->profiles();
snprintf(reply, capacity, "> %s; RX=%lu,%lu TX=%lu,%lu switches=%lu errors=%lu max=%luus preamble=%u,%u",
modeName(p.secondary.mode), (unsigned long)p.rx_packets[0], (unsigned long)p.rx_packets[1],
(unsigned long)p.tx_packets[0], (unsigned long)p.tx_packets[1],
(unsigned long)p.switches, (unsigned long)p.switch_failures,
(unsigned long)p.longest_switch_us, radio_->profilePreamble(0), radio_->profilePreamble(1));
return true;
}
if ((base || temporary) && verb == Get && !*args) {
if (temporary && !temp_active_ && !temp_pending_) snprintf(reply, capacity, "> off");
else formatConfig(reply, capacity, temporary ? (temp_pending_ ? pending_temporary_ : temporary_) : saved_,
temporary, temp_pending_ ? pending_duration_ms_ : temp_remaining_ms_);
return true;
}
if ((base || temporary) && verb == Set && !strcmp(args, "off")) {
if (base && !save({}, primary_preamble_, cross_)) {
snprintf(reply, capacity, "Error: settings could not be saved"); return true;
}
if (base) saved_ = {};
temp_pending_ = temp_active_ = false; temp_remaining_ms_ = 0;
for (auto& s : schedules_) if (base || s.temporary) s = {};
publish_pending_ = true; publish_after_ms_ = millis() + 2000;
publish(); snprintf(reply, capacity, "OK - %s in 2s", saved_.mode != RadioProfileMode::Off ? "saved radio2 restored" : "single radio");
return true;
}
if (scheduled && (verb == Get || verb == Delete)) {
uint32_t index = 0;
if (*args && strcmp(args, "all") && (!cli::parseUnsignedIntegerStrict(args, index) || index < 1 || index > 4)) {
snprintf(reply, capacity, "Error: slot must be 1-4 or all"); return true;
}
const unsigned first = scheduled_temp ? 4 : 0;
if (verb == Delete) {
for (unsigned i = 0; i < 4; ++i) if (!index || index == i + 1) {
if (schedules_[first+i].started) {
temp_active_ = false; temp_remaining_ms_ = 0;
publish_pending_ = true; publish_after_ms_ = millis() + 2000;
}
schedules_[first+i] = {};
}
publish(); snprintf(reply, capacity, "OK"); return true;
}
if (!index) {
snprintf(reply, capacity, "> slots: ");
for (unsigned i = 0; i < 4; ++i) if (schedules_[first+i].active) {
const size_t used = strlen(reply);
if (used < capacity) snprintf(reply + used, capacity-used, "%u@%lu ", i+1, (unsigned long)schedules_[first+i].start);
}
return true;
}
const auto& s = schedules_[first+index-1];
if (!s.active) snprintf(reply, capacity, "> off");
else {
formatConfig(reply, capacity, s.config, s.temporary, s.remaining_ms);
const size_t used = strlen(reply);
if (used < capacity) snprintf(reply+used, capacity-used, "; @%lu-%lu", (unsigned long)s.start, (unsigned long)s.end);
}
return true;
}
if (verb != Set) { snprintf(reply, capacity, "Error: invalid radio2 command"); return true; }
char local[140];
if (strlen(args) >= sizeof(local)) { snprintf(reply, capacity, "Error: params too long"); return true; }
strcpy(local, args);
char* parts[8];
const unsigned count = split(local, parts, 8);
const unsigned expected = scheduled_temp ? 7 : scheduled || temporary ? 6 : 5;
RadioProfileConfig config;
uint32_t sf = 0, cr = 0, start = 0, end = 0, minutes = 0;
bool valid = (count == expected || count == expected + 1)
&& cli::parseDecimalStrict(parts[0], config.params.freq)
&& cli::parseDecimalStrict(parts[1], config.params.bw)
&& cli::parseUnsignedIntegerStrict(parts[2], sf) && sf <= 255
&& cli::parseUnsignedIntegerStrict(parts[3], cr) && cr <= 255
&& parseMode(parts[4], config.mode);
config.params.sf = sf; config.params.cr = cr;
if (valid && count > expected) valid = parsePreamble(parts[expected], config.params.preamble);
if (valid && temporary) valid = cli::parseUnsignedIntegerStrict(parts[5], minutes) && minutes >= 1 && minutes <= 10080;
if (valid && scheduled) valid = cli::parseUnsignedIntegerStrict(parts[5], start);
if (valid && scheduled_temp) valid = cli::parseUnsignedIntegerStrict(parts[6], end);
if (valid) valid = radio_->validateProfile(config.params);
RadioProfiles preview = *radio_->profiles(); preview.secondary = config;
if (valid) valid = preview.automaticPreambleFits();
if (!valid) { snprintf(reply, capacity, "Error: use %s f,bw,sf,cr,rx|rxtx%s[,preamble|auto]", key,
scheduled_temp ? ",start,end" : scheduled ? ",start" : temporary ? ",minutes" : ""); return true; }
if (scheduled) {
const uint32_t epoch = rtc_ ? rtc_->getCurrentTime() : 0;
if (!epoch || start <= epoch || (uint64_t)(start-epoch) * 1000 > 0x7fffffffULL
|| (scheduled_temp && (end <= start || (uint64_t)(end-epoch) * 1000 > 0x7fffffffULL))) {
snprintf(reply, capacity, "Error: future UTC start/end required, within 24 days"); return true;
}
if (scheduled_temp) {
const uint64_t pending_left = temp_pending_
? pending_duration_ms_ + (uint64_t)((int32_t)(temp_start_ms_ - millis()) > 0 ? temp_start_ms_ - millis() : 0)
: 0;
const uint64_t session_left = pending_left > temp_remaining_ms_ ? pending_left : temp_remaining_ms_;
if ((temp_active_ || temp_pending_) && (uint64_t)(start-epoch)*1000 < session_left) {
snprintf(reply, capacity, "Error: temporary schedule overlaps active session"); return true;
}
for (const auto& s : schedules_) if (s.active && s.temporary && start < s.end && end > s.start) {
snprintf(reply, capacity, "Error: temporary schedules overlap"); return true;
}
}
const unsigned first = scheduled_temp ? 4 : 0;
for (unsigned i = 0; i < 4; ++i) if (!schedules_[first+i].active) {
auto& s = schedules_[first+i]; s = {};
s.config = config; s.start = start; s.end = end; s.temporary = scheduled_temp;
s.remaining_ms = scheduled_temp ? (end-epoch)*1000UL : 0; s.active = true;
snprintf(reply, capacity, "OK - %s %u queued, preamble=%u", key, i+1, preview.preamble(1, 32)); return true;
}
snprintf(reply, capacity, "Error: all four schedule slots occupied"); return true;
}
if (temporary) {
for (const auto& s : schedules_) if (s.active && s.temporary) {
snprintf(reply, capacity, "Error: clear tempradioat2 schedules first"); return true;
}
pending_temporary_ = config; pending_duration_ms_ = minutes * 60000UL;
temp_start_ms_ = millis() + 2000; temp_pending_ = true;
// An existing session remains active until the new request can take over.
snprintf(reply, capacity, "OK - tempradio2 %lud%luh%lum (%lu min); preamble=%u",
(unsigned long)(minutes/1440), (unsigned long)(minutes/60%24),
(unsigned long)(minutes%60), (unsigned long)minutes, preview.preamble(1, 32));
} else if (save(config, primary_preamble_, cross_)) {
saved_ = config;
publish_pending_ = true; publish_after_ms_ = millis() + 2000;
publish();
snprintf(reply, capacity, "OK - radio2 %s; preamble=%u", modeName(config.mode), preview.preamble(1, 32));
} else snprintf(reply, capacity, "Error: settings could not be saved");
return true;
}
} // namespace mesh
+73
View File
@@ -0,0 +1,73 @@
#pragma once
#include <Dispatcher.h>
#include <helpers/IdentityStore.h>
#include <helpers/CLICommandUtils.h>
namespace mesh {
// Shared by infrastructure, Companion and standalone roles. Settings use a
// separate versioned image so existing preference offsets stay compatible.
class RadioProfileCLI {
public:
struct Schedule {
RadioProfileConfig config;
uint32_t start = 0, end = 0;
uint32_t remaining_ms = 0; // monotonic upper bound, including start delay
bool active = false, temporary = false, started = false;
};
private:
FILESYSTEM* fs_ = nullptr;
Radio* radio_ = nullptr;
RTCClock* rtc_ = nullptr;
RadioProfileConfig saved_;
RadioProfileConfig temporary_;
RadioProfileConfig pending_temporary_;
uint16_t primary_preamble_ = 0;
RadioCrossMode cross_ = RadioCrossMode::Auto;
Schedule schedules_[8];
uint32_t temp_remaining_ms_ = 0;
uint32_t temp_start_ms_ = 0;
uint32_t pending_duration_ms_ = 0;
uint32_t publish_after_ms_ = 0;
uint32_t last_ms_ = 0;
bool temp_pending_ = false, temp_active_ = false;
bool hold_ = false;
bool publish_pending_ = false;
bool save(const RadioProfileConfig& config, uint16_t preamble, RadioCrossMode cross);
bool readImage(const char* path, uint8_t* bytes, size_t size);
bool writeImage(const char* path, const uint8_t* bytes, size_t size);
void publish();
void formatConfig(char* reply, size_t capacity, const RadioProfileConfig& config,
bool temporary, uint32_t remaining_ms) const;
public:
void begin(FILESYSTEM* fs, Radio* radio, RTCClock* rtc);
void loop();
bool handle(const char* command, char* reply, size_t capacity = 160);
uint16_t primaryPreamble() const { return primary_preamble_; }
bool savePrimaryPreamble(uint16_t symbols);
bool acceptsPrimary(float freq, float bw, uint8_t sf, uint8_t cr, uint16_t preamble) const;
RadioParamApplyResult applyPrimary(float freq, float bw, uint8_t sf, uint8_t cr,
bool temporary, uint16_t preamble, const uint32_t* timings = nullptr) {
if (!radio_) return RadioParamApplyResult::FAILED;
RadioProfileParams p;
p.freq = freq; p.bw = bw; p.sf = sf; p.cr = cr; p.preamble = preamble;
return radio_->trySetPrimaryParams(p, temporary, timings);
}
void stagePrimary(uint16_t preamble, bool temporary) {
if (!radio_ || !radio_->profiles()) return;
auto p = radio_->profiles()->primary;
p.preamble = preamble;
radio_->profiles()->setPrimary(p, temporary);
}
bool secondaryTemporary() const { return temp_active_; }
uint32_t secondaryRemainingSeconds() const { return (temp_remaining_ms_ + 999) / 1000; }
void appendPreamble(char* reply, size_t capacity, uint8_t profile = 0) const;
void appendSavedPreamble(char* reply, size_t capacity, uint8_t sf, float bw) const;
// Strip exactly one optional trailing preamble before a legacy primary
// parser runs. Missing means auto. Reject extra fields and malformed input.
static bool parseSuffix(const char* input, unsigned fields, char* legacy,
size_t capacity, uint16_t& preamble);
};
} // namespace mesh
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <RadioProfiles.h>
#include <helpers/CLICommandUtils.h>
namespace mesh { namespace cli {
inline bool parseRadioPreamble(const char* text, uint16_t& result) {
uint32_t value;
if (!strcmp(text, "auto")) { result = 0; return true; }
if (!parseUnsignedIntegerStrict(text, value) || value > RadioProfiles::MaxPreamble
|| (value != 0 && value < 8)) return false;
result = (uint16_t)value;
return true;
}
inline bool parseRadioPreambleSuffix(const char* input, unsigned fields, char* legacy,
size_t capacity, uint16_t& preamble) {
preamble = 0;
if (!input || strlen(input) >= capacity) return false;
strcpy(legacy, input);
unsigned count = 1;
char* last = nullptr;
for (char* p = legacy; *p; ++p) if (*p == ',') { ++count; last = p; }
if (count == fields) return true;
if (count != fields + 1 || !last || !parseRadioPreamble(last + 1, preamble)) return false;
*last = 0;
return true;
}
} }
+13 -1
View File
@@ -89,6 +89,16 @@ mesh::Packet* PacketQueue::removeByIdx(int i) {
return item;
}
bool PacketQueue::reschedule(mesh::Packet* packet, uint32_t scheduled_for) {
for (int i = 0; i < _num; ++i) {
if (_table[i] != packet) continue;
_schedule_table[i] = scheduled_for;
rebuildNextTime();
return true;
}
return false;
}
bool PacketQueue::add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) {
if (_num == _size) {
return false;
@@ -120,7 +130,9 @@ void PacketQueue::applyBestFloodTransportScope(mesh::Packet* packet,
uint8_t best_preference = 0;
for (int i = 0; i < _num; i++) {
const mesh::Packet* candidate = _table[i];
if (candidate == NULL || candidate->getRouteType() != ROUTE_TYPE_TRANSPORT_FLOOD) continue;
if (candidate == NULL || candidate->getRouteType() != ROUTE_TYPE_TRANSPORT_FLOOD
|| candidate->radio_profile != packet->radio_profile
|| candidate->radio_generation != packet->radio_generation) continue;
uint8_t candidate_hash[MAX_HASH_SIZE];
candidate->calculatePacketHash(candidate_hash);
if (memcmp(packet_hash, candidate_hash, sizeof(packet_hash)) != 0) continue;
+4
View File
@@ -30,6 +30,7 @@ public:
bool getNextTime(uint32_t now, uint32_t& scheduled_for) const;
mesh::Packet* itemAt(int i) const { return _table[i]; }
mesh::Packet* removeByIdx(int i);
bool reschedule(mesh::Packet* packet, uint32_t scheduled_for);
};
class StaticPoolPacketManager : public mesh::PacketManager {
@@ -50,6 +51,9 @@ public:
bool queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override;
mesh::Packet* getNextOutbound(uint32_t now) override;
mesh::Packet* peekNextOutbound(uint32_t now) override;
bool deferOutbound(mesh::Packet* packet, uint32_t scheduled_for) override {
return send_queue.reschedule(packet, scheduled_for);
}
int getOutboundCount(uint32_t now) const override;
int getOutboundTotal() const override;
bool getNextOutboundTime(uint32_t now, uint32_t& scheduled_for) const override;
+5 -1
View File
@@ -56,6 +56,7 @@ public:
}
bool configSideDetectors(const uint8_t* sideDetSFs, uint8_t num, float bw) override {
if (_profiles.enabled()) return false;
const uint8_t primary_sf = getSpreadingFactor();
float active_bw = _params_valid ? _cur_bw : ((CustomLR2021 *)_radio)->getBandwidthKhz();
if (active_bw <= 0.0f) active_bw = bw;
@@ -104,6 +105,9 @@ protected:
}
bool applySideDetectorConfig(uint8_t primary_sf, float bw) {
if (_profiles.enabled()) {
return ((CustomLR2021 *)_radio)->setSideDetector(nullptr, 0) == RADIOLIB_ERR_NONE;
}
if (!mesh::lr2021::validateSideDetectorSFs(_sideDetSFs, _numSideDet, primary_sf, bw)) {
return false;
}
@@ -114,7 +118,7 @@ protected:
}
int16_t performChannelScan() override {
if (_numSideDet == 0) return RadioLibWrapper::performChannelScan();
if (_numSideDet == 0 || _profiles.enabled()) return RadioLibWrapper::performChannelScan();
CustomLR2021* radio = (CustomLR2021 *)_radio;
const uint8_t rx_primary_sf = getSpreadingFactor();
+183 -5
View File
@@ -44,6 +44,10 @@ void RadioLibWrapper::begin() {
const uint8_t initial_cr = 5;
#endif
cacheParams(LORA_FREQ, LORA_BW, LORA_SF, initial_cr);
_profiles.primary.freq = LORA_FREQ;
_profiles.primary.bw = LORA_BW;
_profiles.primary.sf = LORA_SF;
_profiles.primary.cr = initial_cr;
}
#endif
#ifdef LORA_TX_POWER
@@ -162,18 +166,31 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw,
const float previous_bw = _cur_bw;
const uint8_t previous_sf = _cur_sf;
const uint8_t previous_cr = _cur_cr;
const uint16_t previous_preamble = _physical_preamble;
mesh::RadioProfileParams requested = _profiles.primary;
requested.freq = freq; requested.bw = bw; requested.sf = sf; requested.cr = cr;
const auto previous_primary = _profiles.primary;
_profiles.primary = requested;
_physical_preamble = profilePreamble(0);
bool success = applyParams(freq, bw, sf, cr);
if (success) {
cacheParams(freq, bw, sf, cr);
_profiles.primary = previous_primary;
_profiles.setPrimary(requested, _profiles.primary_temporary);
_active_profile = 0;
_profile_generation = _profiles.generation[0];
if (rx_ps_timings != NULL) {
_rx_ps_enabled = true;
if (_profile_rxps_suspended) _profile_saved_rxps = true;
else _rx_ps_enabled = true;
_rx_ps_rx_us = rx_ps_timings[0];
_rx_ps_sleep_us = rx_ps_timings[1];
_rx_ps_continuous_fallback = rxPowerSavingUsesContinuousFallback(
_rx_ps_rx_us, _rx_ps_sleep_us);
}
} else {
_profiles.primary = previous_primary;
_physical_preamble = previous_preamble;
bool restored = had_previous_params
&& applyParams(previous_freq, previous_bw, previous_sf, previous_cr);
@@ -185,14 +202,158 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw,
}
endReconfigure(resume_rx);
_profile_visit_us = micros();
return success ? mesh::RadioParamApplyResult::APPLIED : mesh::RadioParamApplyResult::FAILED;
}
bool RadioLibWrapper::validateProfile(const mesh::RadioProfileParams& p) const {
if (!mesh::RadioProfiles::valid(p)) return false;
#if defined(USE_SX1276)
return p.freq >= 137 && p.freq <= 1020 && p.sf >= 7 && p.bw <= 500;
#elif defined(USE_SX1272)
return p.freq >= 860 && p.freq <= 1020 && p.sf >= 7 && p.bw >= 125 && p.bw <= 500;
#elif defined(USE_LLCC68)
return p.freq >= 150 && p.freq <= 960
&& ((p.bw == 125 && p.sf <= 9) || (p.bw == 250 && p.sf <= 10)
|| (p.bw == 500 && p.sf <= 11));
#elif defined(USE_LR2021)
return (p.freq <= 1090 || p.freq >= 1900) && p.bw >= 31.25 && p.bw <= 1000;
#elif defined(USE_LR1110)
return p.freq <= 960 && p.bw >= 62.5 && p.bw <= 500;
#elif defined(USE_SX1268)
return p.freq >= 410 && p.freq <= 810 && p.bw <= 500;
#else
return p.freq <= 960 && p.bw <= 500;
#endif
}
mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) {
if (profile > 1 || (profile == 1 && !_profiles.enabled())) return mesh::RadioParamApplyResult::FAILED;
const auto& p = _profiles.params(profile);
const uint16_t preamble = profilePreamble(profile);
if (!_profile_refresh_required && _active_profile == profile && _profile_generation == _profiles.generation[profile]
&& _physical_preamble == preamble) return mesh::RadioParamApplyResult::APPLIED;
if (!validateProfile(p)) return mesh::RadioParamApplyResult::FAILED;
const uint32_t started = micros();
const uint8_t resume = beginReconfigure();
if (resume > 1) return mesh::RadioParamApplyResult::BUSY;
const uint16_t old_preamble = _physical_preamble;
_physical_preamble = preamble;
const bool applied = applyParams(p.freq, p.bw, p.sf, p.cr);
if (applied) {
cacheParams(p.freq, p.bw, p.sf, p.cr);
_active_profile = profile;
_profile_generation = _profiles.generation[profile];
_profile_refresh_required = false;
++_profiles.switches;
} else {
++_profiles.switch_failures;
_physical_preamble = old_preamble;
if (!applyParams(_cur_freq, _cur_bw, _cur_sf, _cur_cr)) restoreAfterDeepInit();
}
endReconfigure(resume);
_profile_visit_us = micros();
const uint32_t elapsed = _profile_visit_us - started;
if (elapsed > _profiles.longest_switch_us) _profiles.longest_switch_us = elapsed;
return applied ? mesh::RadioParamApplyResult::APPLIED : mesh::RadioParamApplyResult::FAILED;
}
mesh::RadioParamApplyResult RadioLibWrapper::prepareTransmitProfile(uint8_t profile) {
if (!_profiles.canTransmit(profile)) return mesh::RadioParamApplyResult::FAILED;
if (_profiles.enabled() && (isChipBusy() || isPacketPendingOrReceiving())) {
return mesh::RadioParamApplyResult::BUSY;
}
return tuneProfile(profile);
}
void RadioLibWrapper::serviceProfileScan() {
if (!_params_valid || (_profile_retry_at && (int32_t)(millis() - _profile_retry_at) < 0)) return;
_profile_retry_at = 0;
if (_profiles.enabled() && !_profile_rxps_suspended) {
const uint8_t resume = beginReconfigure();
if (resume > 1) return;
_profile_saved_rxps = _rx_ps_enabled;
_rx_ps_enabled = false;
_profile_rxps_suspended = true;
_nf_calib_active = false;
_noise_floor_valid = false; // a single-channel floor cannot describe both channels
_profile_refresh_required = true; // refresh side detectors even if the tuple is unchanged
endReconfigure(resume);
}
uint8_t target = _active_profile;
const bool restart_scan = _profiles.enabled()
&& (_profile_scan_generation[0] != _profiles.generation[0]
|| _profile_scan_generation[1] != _profiles.generation[1]);
if (!_profiles.enabled()) target = 0;
else if (restart_scan) target = _profiles.slowerProfile();
else if ((uint32_t)(micros() - _profile_visit_us) >= _profiles.listenUs(
_active_profile, profilePreamble(_profiles.slowerProfile()))) {
target ^= 1;
}
const auto result = tuneProfile(target);
if (restart_scan && result == mesh::RadioParamApplyResult::APPLIED) {
_profile_scan_generation[0] = _profiles.generation[0];
_profile_scan_generation[1] = _profiles.generation[1];
_profile_visit_us = micros();
}
if (result == mesh::RadioParamApplyResult::FAILED) {
// Retry a rejected or failing profile at a bounded rate; retain primary RX.
_profile_retry_at = millis() + 1000;
tuneProfile(0);
}
if (!_profiles.enabled() && _profile_rxps_suspended && _active_profile == 0
&& result == mesh::RadioParamApplyResult::APPLIED) {
const uint8_t resume = beginReconfigure();
if (resume > 1) return;
_rx_ps_enabled = _profile_saved_rxps;
_profile_rxps_suspended = false;
_profile_refresh_required = true;
recalibrateNoiseFloor();
endReconfigure(resume);
}
}
uint32_t RadioLibWrapper::getProfileAirtime(uint8_t profile, int len, uint8_t cr) {
if (!_profiles.enabled() && profile == 0 && cr == 0) return getEstAirtimeFor(len);
const auto& p = _profiles.params(profile);
const double symbol = mesh::RadioProfiles::symbolUs(p);
if (symbol <= 0 || len < 0) return 0;
if (cr < 5 || cr > 8) cr = p.cr;
const int de = symbol >= 16000 ? 1 : 0;
const double coded = ceil((8.0 * len - 4 * p.sf + (p.sf <= 6 ? 20 : 28) + 16)
/ (4 * (p.sf - 2 * de)));
const double payload_symbols = 8 + (coded > 0 ? coded * cr : 0);
const double suffix = p.sf <= 6 ? 6.25 : 4.25;
return (uint32_t)ceil((profilePreamble(profile) + suffix + payload_symbols) * symbol / 1000.0);
}
bool RadioLibWrapper::setParams(float freq, float bw, uint8_t sf, uint8_t cr,
const uint32_t* rx_ps_timings) {
return trySetParams(freq, bw, sf, cr, rx_ps_timings) == mesh::RadioParamApplyResult::APPLIED;
}
mesh::RadioParamApplyResult RadioLibWrapper::trySetPrimaryParams(const mesh::RadioProfileParams& p,
bool temporary, const uint32_t* timings) {
auto preview = _profiles;
preview.primary = p;
if (!validateProfile(p) || !preview.automaticPreambleFits()) return mesh::RadioParamApplyResult::FAILED;
const auto previous = _profiles.primary;
const bool was_temp = _profiles.primary_temporary;
const uint32_t generation = _profiles.generation[0];
_profiles.primary.preamble = p.preamble;
_profiles.primary_temporary = temporary;
const auto result = trySetParams(p.freq, p.bw, p.sf, p.cr, timings);
if (result != mesh::RadioParamApplyResult::APPLIED) {
_profiles.primary = previous;
_profiles.primary_temporary = was_temp;
_profiles.generation[0] = generation;
} else {
_profiles.generation[0] = generation + (previous != p || was_temp != temporary);
_profile_generation = _profiles.generation[0];
}
return result;
}
bool RadioLibWrapper::setRxBoostedGainMode(bool enabled) {
uint8_t resume_rx = beginReconfigure();
if (resume_rx > 1) return false;
@@ -471,6 +632,13 @@ void RadioLibWrapper::checkReceiveMode(uint32_t now) {
}
void RadioLibWrapper::loop() {
serviceProfileScan();
// Calibration batches need one stable channel. Do not publish a noise floor
// assembled from different frequencies, or let a batch pin the scan on one.
if (_profiles.enabled()) {
checkReceiveMode(static_cast<uint32_t>(millis()));
return;
}
if (_rx_ps_enabled && !_rx_ps_continuous_fallback) {
rxPsWatchdogCheck();
}
@@ -552,6 +720,7 @@ void RadioLibWrapper::startRecv() {
int err = startReceiveMode();
if (err == RADIOLIB_ERR_NONE) {
// A very short frame may complete while startReceiveMode() returns.
_profile_visit_us = micros();
// Retain that RX interrupt instead of overwriting it with software state.
noInterrupts();
state = (state & STATE_INT_READY) | STATE_RX;
@@ -611,6 +780,12 @@ bool RadioLibWrapper::isInRecvMode() const {
// RX PowerSaving
bool RadioLibWrapper::setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) {
if (_profile_rxps_suspended) {
_profile_saved_rxps = enabled;
_rx_ps_rx_us = rx_us;
_rx_ps_sleep_us = sleep_us;
return !enabled || supportsRxPowerSaving();
}
if (enabled && !supportsRxPowerSaving()) {
return false;
}
@@ -791,7 +966,7 @@ bool RadioLibWrapper::isChannelActive() {
// int.thresh: RSSI-based interference detection (relative to noise floor).
// In RX duty-cycle mode only checked while the chip is in a listen window
// (during the sleep window the frontend is off and the read would stall).
if (_threshold != 0 && !(_rx_ps_armed && isChipBusy())
if (_threshold != 0 && !_profiles.enabled() && !(_rx_ps_armed && isChipBusy())
&& getCurrentRSSI() > _noise_floor + _threshold) return true;
// cad: hardware channel activity detection
@@ -823,7 +998,10 @@ bool RadioLibWrapper::isReceivingPassive(int interference_margin_db) {
// busy so the retry is deferred instead of transmitting blind; Dispatcher
// retains its bounded busy timeout as a last-resort escape.
if (isChipBusy()) return true;
if (isReceivingPacket()) return true;
if (isPacketPendingOrReceiving()) return true;
// Four-symbol visits are too short for the settled single-channel noise
// calibration. Keep retries in RX and use the preamble/header IRQs here.
if (_profiles.enabled()) return false;
unsigned long now = millis();
if ((!_noise_floor_valid && _nf_last_calib == 0)
@@ -866,13 +1044,13 @@ float RadioLibWrapper::packetScoreInt(float snr, int sf, int packet_len) {
return score < 1.0f ? score : 1.0f;
}
PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols) {
PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint16_t preambleSymbols) {
// based on RadioLib's calculateTimeOnAir()
uint32_t tsym_us = ((uint32_t)10000 << sf) / (bw * 10);
uint32_t sfCoeff1_x4 = (sf == 5 || sf == 6) ? 25 : 17; // 6.25 : 4.25, semtech magic numbers to account for sync word + sfd
// preamble + syncword + sfd + header
uint32_t preamble_us = (((preambleSymbols + 8) * 4 + sfCoeff1_x4) * tsym_us) / 4;
uint64_t preamble_us = (((uint64_t)(preambleSymbols + 8) * 4 + sfCoeff1_x4) * tsym_us) / 4;
// airtime for max packet at current radio settings
uint32_t total_us = _radio->getTimeOnAir(MAX_TRANS_UNIT);
+28 -3
View File
@@ -73,6 +73,18 @@ protected:
int8_t _cur_dbm;
bool _cur_rx_boosted_gain;
bool _params_valid, _dbm_valid, _rx_boosted_gain_valid;
mesh::RadioProfiles _profiles;
uint8_t _active_profile = 0;
uint16_t _physical_preamble = 0;
uint32_t _profile_generation = 0;
uint32_t _profile_scan_generation[2] = {};
uint32_t _profile_visit_us = 0;
uint32_t _profile_retry_at = 0;
bool _profile_rxps_suspended = false;
bool _profile_saved_rxps = false;
bool _profile_refresh_required = false;
mesh::RadioParamApplyResult tuneProfile(uint8_t profile);
void serviceProfileScan();
// On-demand noise-floor calibration while RX duty-cycle powersaving is
// armed. A duty-cycled receiver can't be sampled reliably, so a requested
@@ -169,6 +181,19 @@ public:
}
void begin() override;
mesh::RadioProfiles* profiles() override { return &_profiles; }
const mesh::RadioProfiles* profiles() const override { return &_profiles; }
bool validateProfile(const mesh::RadioProfileParams& params) const override;
uint8_t receiveProfile() const override { return _active_profile; }
uint32_t receiveProfileGeneration() const override { return _profile_generation; }
mesh::RadioParamApplyResult prepareTransmitProfile(uint8_t profile) override;
mesh::RadioParamApplyResult trySetPrimaryParams(const mesh::RadioProfileParams& params,
bool temporary, const uint32_t* timings = nullptr) override;
uint16_t profilePreamble(uint8_t profile) const override {
const auto& p = _profiles.params(profile);
return _profiles.preamble(profile, rxPowerSavingPreambleForParams(p.sf, p.bw));
}
uint32_t getProfileAirtime(uint8_t profile, int len_bytes, uint8_t cr = 0) override;
int recvRaw(uint8_t* bytes, int sz) override;
uint32_t getEstAirtimeFor(int len_bytes) override;
bool startSendRaw(const uint8_t* bytes, int len) override;
@@ -202,8 +227,8 @@ public:
virtual float getCurrentRSSI() =0;
virtual uint8_t getSpreadingFactor() const { return LORA_SF; }
static uint16_t preambleLengthForParams(uint8_t sf, float bw) {
return rxPowerSavingPreambleForParams(sf, bw);
uint16_t preambleLengthForParams(uint8_t sf, float bw) const {
return _physical_preamble ? _physical_preamble : rxPowerSavingPreambleForParams(sf, bw);
}
uint16_t currentPreambleLength() const {
return _params_valid
@@ -213,7 +238,7 @@ public:
bool updatePreamble(uint8_t sf, float bw) {
return _radio->setPreambleLength(preambleLengthForParams(sf, bw)) == RADIOLIB_ERR_NONE;
}
PacketMillis calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols);
PacketMillis calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint16_t preambleSymbols);
virtual int16_t performChannelScan();
int getNoiseFloor() const override { return _noise_floor; }