mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 05:21:16 +00:00
Merge RX duty-cycle power saving into keymindCascade
This commit is contained in:
commit
0ede4e2066
18 files changed
+865
-52
No files matched your search
@@ -3,6 +3,16 @@
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <Mesh.h>
|
||||
|
||||
#ifndef RXPS_FIXED_ENABLED
|
||||
#define RXPS_FIXED_ENABLED 1
|
||||
#endif
|
||||
#ifndef RXPS_FIXED_LEVEL
|
||||
#define RXPS_FIXED_LEVEL 2
|
||||
#endif
|
||||
#ifndef RXPS_FIXED_PREAMBLE
|
||||
#define RXPS_FIXED_PREAMBLE 16
|
||||
#endif
|
||||
|
||||
#define CMD_APP_START 1
|
||||
#define CMD_SEND_TXT_MSG 2
|
||||
#define CMD_SEND_CHANNEL_TXT_MSG 3
|
||||
@@ -301,6 +311,49 @@ int MyMesh::getInterferenceThreshold() const {
|
||||
return 0; // disabled for now, until currentRSSI() problem is resolved
|
||||
}
|
||||
|
||||
#if RXPS_FIXED_ENABLED
|
||||
static uint32_t ceilPositiveFloat(float value) {
|
||||
uint32_t rounded = (uint32_t)value;
|
||||
return value > (float)rounded ? rounded + 1 : rounded;
|
||||
}
|
||||
|
||||
static bool calcFixedRxPowerSaving(uint8_t sf, float bw, uint32_t* rx_us, uint32_t* sleep_us) {
|
||||
if (RXPS_FIXED_LEVEL < 1 || RXPS_FIXED_LEVEL > 10 || sf < 5 || sf > 12 ||
|
||||
bw <= 0.0f || (RXPS_FIXED_PREAMBLE != 16 && RXPS_FIXED_PREAMBLE != 32)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const float symbol_us = (1000.0f * (float)(1UL << sf)) / bw;
|
||||
const float amount = (float)(RXPS_FIXED_LEVEL - 1) / 9.0f;
|
||||
const float rx_start_symbols = RXPS_FIXED_PREAMBLE == 16 ? 12.0f : 16.0f;
|
||||
const float sleep_start_symbols = RXPS_FIXED_PREAMBLE == 16 ? 2.0f : 15.0f;
|
||||
const float rx_edge_symbols = 8.0f;
|
||||
const float sleep_edge_symbols = (float)RXPS_FIXED_PREAMBLE + 4.25f - 8.0f;
|
||||
|
||||
const float rx_symbols = rx_start_symbols + amount * (rx_edge_symbols - rx_start_symbols);
|
||||
const float sleep_symbols = sleep_start_symbols + amount * (sleep_edge_symbols - sleep_start_symbols);
|
||||
|
||||
*rx_us = ceilPositiveFloat(rx_symbols * symbol_us);
|
||||
*sleep_us = (uint32_t)(sleep_symbols * symbol_us);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void applyFixedRxPowerSaving(uint8_t sf, float bw) {
|
||||
uint32_t rx_us, sleep_us;
|
||||
if (!calcFixedRxPowerSaving(sf, bw, &rx_us, &sleep_us)) {
|
||||
MESH_DEBUG_PRINTLN("RX Power Saving fixed profile invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok = radio_driver.setRxPowerSaving(true, rx_us, sleep_us);
|
||||
MESH_DEBUG_PRINTLN("RX Power Saving fixed level %d p%d: %s (%lu/%lu us)",
|
||||
RXPS_FIXED_LEVEL,
|
||||
RXPS_FIXED_PREAMBLE,
|
||||
ok ? "Enabled" : "Unsupported",
|
||||
(unsigned long)rx_us,
|
||||
(unsigned long)sleep_us);
|
||||
}
|
||||
#endif
|
||||
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
|
||||
if (_prefs.rx_delay_base <= 0.0f) return 0;
|
||||
return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
|
||||
@@ -1079,6 +1132,9 @@ void MyMesh::begin(bool has_display) {
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
|
||||
#if RXPS_FIXED_ENABLED
|
||||
applyFixedRxPowerSaving(_prefs.sf, _prefs.bw);
|
||||
#endif
|
||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
||||
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
||||
// NOTE: no FEM LNA wiring here — companion has its own NodePrefs without
|
||||
@@ -1510,6 +1566,9 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
savePrefs();
|
||||
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
#if RXPS_FIXED_ENABLED
|
||||
applyFixedRxPowerSaving(_prefs.sf, _prefs.bw);
|
||||
#endif
|
||||
MESH_DEBUG_PRINTLN("OK: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf,
|
||||
(uint32_t)cr);
|
||||
|
||||
|
||||
@@ -1145,14 +1145,8 @@ static uint8_t getRetryLogCodingRate(const mesh::Packet* packet, uint8_t default
|
||||
}
|
||||
|
||||
static uint16_t getRetryLogPreambleLength(const mesh::Packet* packet, uint16_t default_preamble_len) {
|
||||
if (packet == NULL || default_preamble_len <= 16 || !(packet->tx_cr == 4 || packet->tx_cr == 5)) {
|
||||
return default_preamble_len;
|
||||
}
|
||||
|
||||
bool has_direct_path = packet->getPathHashCount() > 0
|
||||
|| (packet->getPayloadType() == PAYLOAD_TYPE_TRACE && packet->payload_len > 9);
|
||||
if (packet->isRouteDirect() && has_direct_path) {
|
||||
return 16;
|
||||
if (packet != NULL && packet->tx_cr >= 4 && packet->tx_cr <= 8) {
|
||||
return 32;
|
||||
}
|
||||
return default_preamble_len;
|
||||
}
|
||||
@@ -2294,6 +2288,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
#endif
|
||||
// Observer defaults (radio_watchdog, alert.*, snmp.*) moved to applyMQTTDefaults()
|
||||
// in MQTTDefaults.h — they live in /mqtt_prefs now, not NodePrefs.
|
||||
_prefs.rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US;
|
||||
_prefs.rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;
|
||||
|
||||
// bridge defaults
|
||||
_prefs.bridge_enabled = 1; // enabled
|
||||
@@ -2418,6 +2414,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
||||
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); // LoRa FEM LNA (FEM boards only)
|
||||
setRxPowerSaving(_prefs.rx_powersaving_enabled, _prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us);
|
||||
|
||||
updateAdvertTimer();
|
||||
updateFloodAdvertTimer();
|
||||
@@ -3097,6 +3094,21 @@ void MyMesh::setTxPower(int8_t power_dbm) {
|
||||
radio_driver.setTxPower(power_dbm);
|
||||
}
|
||||
|
||||
bool MyMesh::setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) {
|
||||
bool ok = radio_driver.setRxPowerSaving(enable, rx_us, sleep_us);
|
||||
MESH_DEBUG_PRINTLN("RX Power Saving: %s (%lu/%lu us)%s",
|
||||
enable ? "Enabled" : "Disabled",
|
||||
(unsigned long)rx_us,
|
||||
(unsigned long)sleep_us,
|
||||
ok ? "" : " unsupported");
|
||||
return ok;
|
||||
}
|
||||
|
||||
void MyMesh::getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) {
|
||||
*soft = radio_driver.getRxPsWatchdogSoftCount();
|
||||
*hard = radio_driver.getRxPsWatchdogHardCount();
|
||||
}
|
||||
|
||||
bool MyMesh::setRxBoostedGain(bool enable) {
|
||||
return radio_driver.setRxBoostedGainMode(enable);
|
||||
}
|
||||
@@ -3959,6 +3971,8 @@ bool MyMesh::hasPendingWork() const {
|
||||
const BridgeBase* active_bridge = activeBridge();
|
||||
if (active_bridge && active_bridge->isRunning()) return true;
|
||||
#endif
|
||||
if (radio_driver.isWatchdogObserving()) return true; // keep MCU awake for one radio duty cycle
|
||||
if (radio_driver.isCalibratingNoiseFloor()) return true; // keep MCU awake for the noise-floor window
|
||||
if (_mgr->getOutboundTotal() > 0) return true;
|
||||
if (isMillisTimerDue(next_flood_advert) || isMillisTimerDue(next_local_advert)) return true;
|
||||
if (isMillisTimerDue(dirty_contacts_expiry)) return true;
|
||||
|
||||
@@ -387,6 +387,8 @@ public:
|
||||
|
||||
void dumpLogFile() override;
|
||||
void setTxPower(int8_t power_dbm) override;
|
||||
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
|
||||
void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override;
|
||||
void formatNeighborsReply(char *reply) override;
|
||||
void removeNeighbor(const uint8_t* pubkey, int key_len) override;
|
||||
void formatStatsReply(char *reply) override;
|
||||
|
||||
@@ -675,6 +675,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
_prefs.interference_threshold = 0; // disabled
|
||||
_prefs.radio_fem_rxgain = 1; // LoRa FEM RX gain on by default (FEM boards)
|
||||
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
|
||||
_prefs.rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US;
|
||||
_prefs.rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;
|
||||
#ifdef ROOM_PASSWORD
|
||||
StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password));
|
||||
#endif
|
||||
@@ -738,6 +740,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); // LoRa FEM LNA (FEM boards only)
|
||||
setRxPowerSaving(_prefs.rx_powersaving_enabled, _prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us);
|
||||
|
||||
updateAdvertTimer();
|
||||
updateFloodAdvertTimer();
|
||||
@@ -899,6 +902,15 @@ void MyMesh::setTxPower(int8_t power_dbm) {
|
||||
radio_driver.setTxPower(power_dbm);
|
||||
}
|
||||
|
||||
bool MyMesh::setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) {
|
||||
return radio_driver.setRxPowerSaving(enable, rx_us, sleep_us);
|
||||
}
|
||||
void MyMesh::getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) {
|
||||
*soft = radio_driver.getRxPsWatchdogSoftCount();
|
||||
*hard = radio_driver.getRxPsWatchdogHardCount();
|
||||
}
|
||||
|
||||
|
||||
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
IdentityStore store(*_fs, "");
|
||||
@@ -1171,6 +1183,8 @@ bool MyMesh::hasPendingWork() const {
|
||||
#if defined(WITH_BRIDGE)
|
||||
if (bridge && bridge->isRunning()) return true; // bridge needs WiFi radio, can't sleep
|
||||
#endif
|
||||
if (radio_driver.isWatchdogObserving()) return true; // keep MCU awake for one radio duty cycle
|
||||
if (radio_driver.isCalibratingNoiseFloor()) return true; // keep MCU awake for the noise-floor window
|
||||
if (_mgr->getOutboundTotal() > 0) return true;
|
||||
if (acl.getNumClients() > 0 && isMillisTimerDue(next_push)) return true;
|
||||
if (isMillisTimerDue(next_flood_advert) || isMillisTimerDue(next_local_advert)) return true;
|
||||
|
||||
@@ -230,6 +230,8 @@ public:
|
||||
|
||||
void dumpLogFile() override;
|
||||
void setTxPower(int8_t power_dbm) override;
|
||||
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
|
||||
void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override;
|
||||
|
||||
void formatNeighborsReply(char *reply) override {
|
||||
strcpy(reply, "not supported");
|
||||
|
||||
@@ -769,6 +769,8 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
|
||||
_prefs.interference_threshold = 0; // disabled
|
||||
_prefs.radio_fem_rxgain = 1; // LoRa FEM RX gain on by default (FEM boards)
|
||||
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
|
||||
_prefs.rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US;
|
||||
_prefs.rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;
|
||||
|
||||
// GPS defaults
|
||||
_prefs.gps_enabled = 0;
|
||||
@@ -812,6 +814,7 @@ void SensorMesh::begin(FILESYSTEM* fs) {
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); // LoRa FEM LNA (FEM boards only)
|
||||
setRxPowerSaving(_prefs.rx_powersaving_enabled, _prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us);
|
||||
|
||||
updateAdvertTimer();
|
||||
updateFloodAdvertTimer();
|
||||
@@ -891,6 +894,15 @@ void SensorMesh::setTxPower(int8_t power_dbm) {
|
||||
radio_driver.setTxPower(power_dbm);
|
||||
}
|
||||
|
||||
bool SensorMesh::setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) {
|
||||
return radio_driver.setRxPowerSaving(enable, rx_us, sleep_us);
|
||||
}
|
||||
void SensorMesh::getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) {
|
||||
*soft = radio_driver.getRxPsWatchdogSoftCount();
|
||||
*hard = radio_driver.getRxPsWatchdogHardCount();
|
||||
}
|
||||
|
||||
|
||||
void SensorMesh::formatStatsReply(char *reply) {
|
||||
StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr);
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ public:
|
||||
void eraseLogFile() override { }
|
||||
void dumpLogFile() override { }
|
||||
void setTxPower(int8_t power_dbm) override;
|
||||
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
|
||||
void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override;
|
||||
void formatNeighborsReply(char *reply) override {
|
||||
strcpy(reply, "not supported");
|
||||
}
|
||||
|
||||
+3
-9
@@ -381,25 +381,19 @@ void Dispatcher::checkSend() {
|
||||
outbound_restore_cr = 0;
|
||||
outbound_restore_preamble_len = 0;
|
||||
uint8_t default_cr = getDefaultTxCodingRate();
|
||||
bool using_low_retry_cr = false;
|
||||
bool is_retry = outbound->tx_cr >= 4 && outbound->tx_cr <= 8;
|
||||
if (outbound->tx_cr >= 4 && outbound->tx_cr <= 8 && default_cr >= 4 && default_cr <= 8
|
||||
&& outbound->tx_cr != default_cr) {
|
||||
if (_radio->setCodingRate(outbound->tx_cr)) {
|
||||
outbound_restore_cr = default_cr;
|
||||
using_low_retry_cr = outbound->tx_cr == 4 || outbound->tx_cr == 5;
|
||||
max_airtime = _radio->getEstAirtimeFor(len)*3/2;
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): WARN: failed to set packet CR%d", getLogDateTime(), (uint32_t)outbound->tx_cr);
|
||||
}
|
||||
} else if (outbound->tx_cr == 4 || outbound->tx_cr == 5) {
|
||||
using_low_retry_cr = outbound->tx_cr == default_cr;
|
||||
}
|
||||
bool has_direct_path = outbound->getPathHashCount() > 0
|
||||
|| (outbound->getPayloadType() == PAYLOAD_TYPE_TRACE && outbound->payload_len > 9);
|
||||
if (outbound->isRouteDirect() && has_direct_path
|
||||
&& using_low_retry_cr) {
|
||||
if (is_retry) {
|
||||
uint16_t default_preamble_len = _radio->getDefaultPreambleLength();
|
||||
if (default_preamble_len > 16 && _radio->setPreambleLength(16)) {
|
||||
if (default_preamble_len != 32 && _radio->setPreambleLength(32)) {
|
||||
outbound_restore_preamble_len = default_preamble_len;
|
||||
max_airtime = _radio->getEstAirtimeFor(len)*3/2;
|
||||
}
|
||||
|
||||
@@ -86,6 +86,9 @@ public:
|
||||
|
||||
virtual bool isInRecvMode() const = 0;
|
||||
|
||||
virtual bool supportsRxPowerSaving() const { return false; }
|
||||
virtual bool setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) { return !enabled; }
|
||||
|
||||
/**
|
||||
* \returns true if the radio is currently mid-receive of a packet.
|
||||
*/
|
||||
|
||||
@@ -1364,6 +1364,7 @@ void Mesh::armFloodRetryOnSendComplete(const Packet* packet) {
|
||||
}
|
||||
|
||||
*retry = *packet;
|
||||
retry->tx_cr = getDefaultTxCodingRate();
|
||||
uint32_t retry_delay = getFloodRetryAttemptDelay(packet, _flood_retries[i].retry_attempts_sent);
|
||||
if (queueOutboundPacket(retry, _flood_retries[i].priority, retry_delay)) {
|
||||
_flood_retries[i].packet = retry;
|
||||
@@ -1394,6 +1395,7 @@ void Mesh::armFloodRetryOnSendComplete(const Packet* packet) {
|
||||
}
|
||||
|
||||
*retry = *packet;
|
||||
retry->tx_cr = getDefaultTxCodingRate();
|
||||
if (queueOutboundPacket(retry, _flood_retries[i].priority, _flood_retries[i].retry_delay)) {
|
||||
unsigned long now = _ms->getMillis();
|
||||
_flood_retries[i].packet = retry;
|
||||
|
||||
+250
-4
@@ -313,6 +313,80 @@ static const char* retryPresetName(uint8_t preset) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool isValidRxPowerSavingPeriod(uint32_t us) {
|
||||
return us >= RX_POWERSAVING_MIN_PERIOD_US && us <= RX_POWERSAVING_MAX_PERIOD_US;
|
||||
}
|
||||
|
||||
// MeshCore preamble convention used for the RX powersaving timing calculation.
|
||||
// Must stay in sync with RadioLibWrapper::preambleLengthForSF() (the value the
|
||||
// radio actually transmits); kept local here because CommonCLI is radio-agnostic.
|
||||
static uint16_t rxPowerSavingPreambleForSF(uint8_t sf) {
|
||||
return sf <= 8 ? 32 : 16;
|
||||
}
|
||||
|
||||
static bool isNumeric(const char* sp) {
|
||||
if (!sp || !*sp) return false;
|
||||
while (*sp) {
|
||||
if (*sp < '0' || *sp > '9') return false;
|
||||
sp++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint32_t ceilPositiveFloat(float value) {
|
||||
uint32_t rounded = (uint32_t)value;
|
||||
return value > (float)rounded ? rounded + 1 : rounded;
|
||||
}
|
||||
|
||||
static bool calcRxPowerSavingLevel(uint32_t level, uint8_t sf, float bw, uint32_t preamble,
|
||||
uint32_t* rx_us, uint32_t* sleep_us) {
|
||||
if (level < 1 || level > 10 || sf < 5 || sf > 12 || bw <= 0.0f || (preamble != 16 && preamble != 32)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const float symbol_us = (1000.0f * (float)(1UL << sf)) / bw;
|
||||
const float amount = (float)(level - 1) / 9.0f;
|
||||
const float rx_start_symbols = preamble == 16 ? 12.0f : 16.0f;
|
||||
const float sleep_start_symbols = preamble == 16 ? 2.0f : 15.0f;
|
||||
const float rx_edge_symbols = 8.0f;
|
||||
const float sleep_edge_symbols = (float)preamble + 4.25f - 8.0f;
|
||||
|
||||
const float rx_symbols = rx_start_symbols + amount * (rx_edge_symbols - rx_start_symbols);
|
||||
const float sleep_symbols = sleep_start_symbols + amount * (sleep_edge_symbols - sleep_start_symbols);
|
||||
|
||||
*rx_us = ceilPositiveFloat(rx_symbols * symbol_us);
|
||||
*sleep_us = (uint32_t)(sleep_symbols * symbol_us);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ensureRxPowerSavingDefaults(NodePrefs* prefs) {
|
||||
if (!isValidRxPowerSavingPeriod(prefs->rx_ps_rx_us)) {
|
||||
prefs->rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US;
|
||||
}
|
||||
if (!isValidRxPowerSavingPeriod(prefs->rx_ps_sleep_us)) {
|
||||
prefs->rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;
|
||||
}
|
||||
}
|
||||
|
||||
// Recomputes rx_ps_rx_us/rx_ps_sleep_us from the stored level and the current
|
||||
// radio SF/BW. No-op (returns false) for manual timings (rx_ps_level == 0).
|
||||
// Lets level-based RX powersaving auto-retune when SF/BW change.
|
||||
static bool recalcRxPowerSavingFromLevel(NodePrefs* prefs) {
|
||||
if (prefs->rx_ps_level < 1 || prefs->rx_ps_level > 10) return false; // manual: nothing to recompute
|
||||
uint32_t preamble = prefs->rx_ps_preamble ? prefs->rx_ps_preamble
|
||||
: rxPowerSavingPreambleForSF(prefs->sf);
|
||||
uint32_t rx_us, sleep_us;
|
||||
if (!calcRxPowerSavingLevel(prefs->rx_ps_level, prefs->sf, prefs->bw, preamble, &rx_us, &sleep_us)) {
|
||||
return false;
|
||||
}
|
||||
if (!isValidRxPowerSavingPeriod(rx_us) || !isValidRxPowerSavingPeriod(sleep_us)) {
|
||||
return false;
|
||||
}
|
||||
prefs->rx_ps_rx_us = rx_us;
|
||||
prefs->rx_ps_sleep_us = sleep_us;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void markDirectRetryPrefsValid(NodePrefs* prefs) {
|
||||
prefs->direct_retry_prefs_magic[0] = DIRECT_RETRY_PREFS_MAGIC_0;
|
||||
prefs->direct_retry_prefs_magic[1] = DIRECT_RETRY_PREFS_MAGIC_1;
|
||||
@@ -763,6 +837,11 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
// (upstream defaults: FEM RX gain on, CAD off) — overwritten below if present.
|
||||
_prefs->radio_fem_rxgain = 1;
|
||||
_prefs->cad_enabled = 0;
|
||||
_prefs->rx_powersaving_enabled = 0;
|
||||
_prefs->rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US;
|
||||
_prefs->rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;
|
||||
_prefs->rx_ps_level = 0;
|
||||
_prefs->rx_ps_preamble = 0;
|
||||
#if defined(ENABLE_OTA)
|
||||
// OTA settings were appended after Keymind's retry/flood tail. Initialize them
|
||||
// before reading so older and legacy preference files remain conservative.
|
||||
@@ -987,6 +1066,36 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
}
|
||||
// next: 811
|
||||
#endif
|
||||
// RXPS is stored at a fixed offset after the reserved OTA tail. This keeps
|
||||
// both old OTA and non-OTA /com_prefs files unambiguous.
|
||||
bool has_rxps_tail = file.available() >= 11;
|
||||
#if !defined(ENABLE_OTA)
|
||||
const size_t ota_tail_size = 136;
|
||||
const size_t padded_rxps_tail_size = sizeof(_prefs->rx_powersaving_enabled)
|
||||
+ sizeof(_prefs->rx_ps_rx_us) + sizeof(_prefs->rx_ps_sleep_us)
|
||||
+ sizeof(_prefs->rx_ps_level) + sizeof(_prefs->rx_ps_preamble);
|
||||
if (file.available() >= (int)(ota_tail_size + padded_rxps_tail_size)) {
|
||||
size_t remaining = ota_tail_size;
|
||||
while (remaining > 0) {
|
||||
size_t n = remaining > sizeof(pad) ? sizeof(pad) : remaining;
|
||||
file.read(pad, n);
|
||||
remaining -= n;
|
||||
}
|
||||
has_rxps_tail = true;
|
||||
} else {
|
||||
has_rxps_tail = false;
|
||||
}
|
||||
#endif
|
||||
const size_t rxps_tail_size = sizeof(_prefs->rx_powersaving_enabled)
|
||||
+ sizeof(_prefs->rx_ps_rx_us) + sizeof(_prefs->rx_ps_sleep_us)
|
||||
+ sizeof(_prefs->rx_ps_level) + sizeof(_prefs->rx_ps_preamble);
|
||||
if (has_rxps_tail && file.available() >= (int)rxps_tail_size) {
|
||||
file.read((uint8_t *)&_prefs->rx_powersaving_enabled, sizeof(_prefs->rx_powersaving_enabled));
|
||||
file.read((uint8_t *)&_prefs->rx_ps_rx_us, sizeof(_prefs->rx_ps_rx_us));
|
||||
file.read((uint8_t *)&_prefs->rx_ps_sleep_us, sizeof(_prefs->rx_ps_sleep_us));
|
||||
file.read((uint8_t *)&_prefs->rx_ps_level, sizeof(_prefs->rx_ps_level));
|
||||
file.read((uint8_t *)&_prefs->rx_ps_preamble, sizeof(_prefs->rx_ps_preamble));
|
||||
}
|
||||
}
|
||||
|
||||
// sanitise bad pref values
|
||||
@@ -1071,6 +1180,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
if (_prefs->ota_max_hops > 8) _prefs->ota_max_hops = 3; // 0=direct only; cap absurd reach
|
||||
if (_prefs->ota_signer_count > 4) _prefs->ota_signer_count = 0; // corrupt count -> drop keys
|
||||
#endif
|
||||
_prefs->rx_powersaving_enabled = constrain(_prefs->rx_powersaving_enabled, 0, 1);
|
||||
_prefs->rx_ps_level = constrain(_prefs->rx_ps_level, 0, 10);
|
||||
if (_prefs->rx_ps_preamble != 16 && _prefs->rx_ps_preamble != 32) {
|
||||
_prefs->rx_ps_preamble = 0; // 0 = auto (derive from SF)
|
||||
}
|
||||
ensureRxPowerSavingDefaults(_prefs);
|
||||
recalcRxPowerSavingFromLevel(_prefs); // retune level-based timings to the loaded SF/BW
|
||||
|
||||
file.close();
|
||||
}
|
||||
@@ -1132,9 +1248,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
|
||||
file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
|
||||
file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
|
||||
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
|
||||
// MQTT/observer settings are stored in /mqtt_prefs, not here. No zero-gap is
|
||||
// written anymore — /com_prefs holds the upstream fields plus the keymind
|
||||
// retry tail below (loadPrefsInt reads the same sequence; keep in sync).
|
||||
// MQTT/observer settings are stored in /mqtt_prefs, not here. /com_prefs
|
||||
// holds the upstream fields plus the keymind retry tail below.
|
||||
file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
|
||||
file.write((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 291
|
||||
file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292
|
||||
@@ -1178,7 +1293,28 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
|
||||
file.write((uint8_t *)&_prefs->ota_advert_interval, sizeof(_prefs->ota_advert_interval)); // 808
|
||||
file.write((uint8_t *)&_prefs->ota_max_hops, sizeof(_prefs->ota_max_hops)); // 810
|
||||
// next: 811
|
||||
#else
|
||||
// Reserve the OTA tail so RXPS has the same offset in every build. Write
|
||||
// OTA's normal defaults so a later OTA-enabled firmware reads sane values.
|
||||
file.write(pad, 3); // autofetch, autoinstall, signer_count
|
||||
for (size_t remaining = 128; remaining > 0; ) {
|
||||
size_t n = remaining > sizeof(pad) ? sizeof(pad) : remaining;
|
||||
file.write(pad, n);
|
||||
remaining -= n;
|
||||
}
|
||||
const uint16_t ota_checkpoint_blocks = 4;
|
||||
const uint16_t ota_advert_interval = 0;
|
||||
const uint8_t ota_max_hops = 3;
|
||||
file.write((uint8_t *)&ota_checkpoint_blocks, sizeof(ota_checkpoint_blocks));
|
||||
file.write((uint8_t *)&ota_advert_interval, sizeof(ota_advert_interval));
|
||||
file.write((uint8_t *)&ota_max_hops, sizeof(ota_max_hops));
|
||||
#endif
|
||||
file.write((uint8_t *)&_prefs->rx_powersaving_enabled, sizeof(_prefs->rx_powersaving_enabled)); // 811
|
||||
file.write((uint8_t *)&_prefs->rx_ps_rx_us, sizeof(_prefs->rx_ps_rx_us)); // 812
|
||||
file.write((uint8_t *)&_prefs->rx_ps_sleep_us, sizeof(_prefs->rx_ps_sleep_us)); // 816
|
||||
file.write((uint8_t *)&_prefs->rx_ps_level, sizeof(_prefs->rx_ps_level)); // 820
|
||||
file.write((uint8_t *)&_prefs->rx_ps_preamble, sizeof(_prefs->rx_ps_preamble)); // 821
|
||||
// next: 822
|
||||
|
||||
file.close();
|
||||
}
|
||||
@@ -1992,6 +2128,105 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else {
|
||||
strcpy(reply, "Error: state must be on or off");
|
||||
}
|
||||
} else if (memcmp(config, "radio.rxps ", 11) == 0) {
|
||||
const char* value = &config[11];
|
||||
uint8_t enable = _prefs->rx_powersaving_enabled;
|
||||
uint32_t rx_us = _prefs->rx_ps_rx_us;
|
||||
uint32_t sleep_us = _prefs->rx_ps_sleep_us;
|
||||
uint32_t level = 0;
|
||||
uint32_t preamble = rxPowerSavingPreambleForSF(_prefs->sf);
|
||||
bool level_requested = false;
|
||||
bool preamble_overridden = false;
|
||||
|
||||
ensureRxPowerSavingDefaults(_prefs);
|
||||
rx_us = _prefs->rx_ps_rx_us;
|
||||
sleep_us = _prefs->rx_ps_sleep_us;
|
||||
|
||||
if (strcmp(value, "off") == 0) {
|
||||
enable = 0;
|
||||
} else if (strcmp(value, "on") == 0 || strcmp(value, "conservative") == 0) {
|
||||
enable = 1;
|
||||
level = RX_POWERSAVING_CONSERVATIVE_LEVEL;
|
||||
preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
|
||||
level_requested = true;
|
||||
preamble_overridden = true;
|
||||
} else if (strcmp(value, "balanced") == 0) {
|
||||
enable = 1;
|
||||
level = RX_POWERSAVING_BALANCED_LEVEL;
|
||||
preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
|
||||
level_requested = true;
|
||||
preamble_overridden = true;
|
||||
} else {
|
||||
StrHelper::strncpy(tmp, value, sizeof(tmp));
|
||||
const char *parts[4];
|
||||
int num = mesh::Utils::parseTextParts(tmp, parts, 4, ' ');
|
||||
if (num == 1 && isNumeric(parts[0])) {
|
||||
level = _atoi(parts[0]);
|
||||
level_requested = true;
|
||||
enable = 1;
|
||||
} else if (num == 2 && strcmp(parts[0], "level") == 0 && isNumeric(parts[1])) {
|
||||
level = _atoi(parts[1]);
|
||||
level_requested = true;
|
||||
enable = 1;
|
||||
} else if (num == 4 && strcmp(parts[0], "level") == 0 && isNumeric(parts[1]) &&
|
||||
strcmp(parts[2], "preamble") == 0 && isNumeric(parts[3])) {
|
||||
level = _atoi(parts[1]);
|
||||
preamble = _atoi(parts[3]);
|
||||
level_requested = true;
|
||||
preamble_overridden = true;
|
||||
enable = 1;
|
||||
} else if (num == 2 && isNumeric(parts[0]) && isNumeric(parts[1])) {
|
||||
rx_us = _atoi(parts[0]);
|
||||
sleep_us = _atoi(parts[1]);
|
||||
enable = 1;
|
||||
} else {
|
||||
strcpy(reply, "ERROR: use off|on|conservative|balanced|level <1-10>|<rx_us> <sleep_us>");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (level_requested && !calcRxPowerSavingLevel(level, _prefs->sf, _prefs->bw, preamble, &rx_us, &sleep_us)) {
|
||||
strcpy(reply, "ERROR: level range is 1-10; preamble is 16 or 32");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidRxPowerSavingPeriod(rx_us) || !isValidRxPowerSavingPeriod(sleep_us)) {
|
||||
sprintf(reply, "ERROR: range is %lu-%lu us",
|
||||
(unsigned long)RX_POWERSAVING_MIN_PERIOD_US,
|
||||
(unsigned long)RX_POWERSAVING_MAX_PERIOD_US);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_callbacks->setRxPowerSaving(enable, rx_us, sleep_us)) {
|
||||
strcpy(reply, "ERROR: RX powersaving unsupported");
|
||||
return;
|
||||
}
|
||||
|
||||
_prefs->rx_powersaving_enabled = enable;
|
||||
_prefs->rx_ps_rx_us = rx_us;
|
||||
_prefs->rx_ps_sleep_us = sleep_us;
|
||||
if (level_requested) {
|
||||
// Remember the intent so the timings can auto-retune when SF/BW change.
|
||||
_prefs->rx_ps_level = level;
|
||||
_prefs->rx_ps_preamble = preamble_overridden ? preamble : 0; // 0 = auto (derive from SF)
|
||||
} else if (strcmp(value, "off") != 0) {
|
||||
// manual <rx_us> <sleep_us> timings are fixed, not level-derived
|
||||
// (the named profiles set level_requested and are handled above)
|
||||
_prefs->rx_ps_level = 0;
|
||||
_prefs->rx_ps_preamble = 0;
|
||||
}
|
||||
savePrefs();
|
||||
if (level_requested) {
|
||||
sprintf(reply, "OK - level %lu,%s,%lu,%lu,preamble=%lu",
|
||||
(unsigned long)level,
|
||||
enable ? "on" : "off",
|
||||
(unsigned long)rx_us,
|
||||
(unsigned long)sleep_us,
|
||||
(unsigned long)preamble);
|
||||
} else {
|
||||
sprintf(reply, "OK - %s,%lu,%lu", enable ? "on" : "off",
|
||||
(unsigned long)rx_us, (unsigned long)sleep_us);
|
||||
}
|
||||
} else if (memcmp(config, "radio ", 6) == 0) {
|
||||
strcpy(tmp, &config[6]);
|
||||
const char *parts[4];
|
||||
@@ -2005,8 +2240,11 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
_prefs->cr = cr;
|
||||
_prefs->freq = freq;
|
||||
_prefs->bw = bw;
|
||||
// Retune level-based RX powersaving to the new SF/BW. Persist only; the
|
||||
// radio itself is "reboot to apply", and begin() re-arms the timings then.
|
||||
bool rxps_retuned = recalcRxPowerSavingFromLevel(_prefs);
|
||||
_callbacks->savePrefs();
|
||||
strcpy(reply, "OK - reboot to apply");
|
||||
strcpy(reply, rxps_retuned ? "OK - reboot to apply (rxps retuned)" : "OK - reboot to apply");
|
||||
} else {
|
||||
strcpy(reply, "Error, invalid radio params");
|
||||
}
|
||||
@@ -2573,6 +2811,14 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
_callbacks->formatScheduledRadioParams(true, skipSpacesConst(&config[11]), reply);
|
||||
} else if (memcmp(config, "radioat", 7) == 0 && (config[7] == 0 || config[7] == ' ')) {
|
||||
_callbacks->formatScheduledRadioParams(false, skipSpacesConst(&config[7]), reply);
|
||||
} else if (memcmp(config, "radio.rxps", 10) == 0) {
|
||||
ensureRxPowerSavingDefaults(_prefs);
|
||||
sprintf(reply, "> %s,%lu,%lu", _prefs->rx_powersaving_enabled ? "on" : "off",
|
||||
(unsigned long)_prefs->rx_ps_rx_us, (unsigned long)_prefs->rx_ps_sleep_us);
|
||||
} else if (memcmp(config, "rxps.wd", 7) == 0) {
|
||||
uint32_t wd_soft, wd_hard;
|
||||
_callbacks->getRxPsWatchdogCounts(&wd_soft, &wd_hard);
|
||||
sprintf(reply, "> soft=%lu,hard=%lu", (unsigned long)wd_soft, (unsigned long)wd_hard);
|
||||
} else if (memcmp(config, "radio", 5) == 0) {
|
||||
char freq[16], bw[16];
|
||||
strcpy(freq, StrHelper::ftoa(_prefs->freq));
|
||||
|
||||
@@ -102,6 +102,18 @@
|
||||
#define TELEMETRY_ACCESS_ALL 0
|
||||
#define TELEMETRY_ACCESS_ACL 1
|
||||
|
||||
#define RX_POWERSAVING_DEFAULT_RX_US 65625UL
|
||||
#define RX_POWERSAVING_DEFAULT_SLEEP_US 60000UL
|
||||
#define RX_POWERSAVING_MIN_PERIOD_US 1000UL
|
||||
#define RX_POWERSAVING_MAX_PERIOD_US 30000000UL
|
||||
|
||||
// The named profiles are level presets pinned to a 16-symbol preamble: most
|
||||
// deployed senders still transmit 16-symbol preambles regardless of the newer
|
||||
// SF-based rule (32 for SF <= 8). Revisit once the field has largely migrated.
|
||||
#define RX_POWERSAVING_CONSERVATIVE_LEVEL 1UL
|
||||
#define RX_POWERSAVING_BALANCED_LEVEL 5UL
|
||||
#define RX_POWERSAVING_PROFILE_PREAMBLE 16UL
|
||||
|
||||
struct NodePrefs { // persisted to file
|
||||
float airtime_factor;
|
||||
char node_name[32];
|
||||
@@ -196,6 +208,12 @@ struct NodePrefs { // persisted to file
|
||||
uint16_t ota_advert_interval; // OTA beacon re-advertise cadence (mins); 0=off. Default 1440 (24h, tunable)
|
||||
uint8_t ota_max_hops; // OTA flood reach in hops; 0=direct only. Default 3 (runtime-tunable)
|
||||
#endif
|
||||
|
||||
uint8_t rx_powersaving_enabled; // boolean
|
||||
uint32_t rx_ps_rx_us;
|
||||
uint32_t rx_ps_sleep_us;
|
||||
uint8_t rx_ps_level; // 0 = manual/explicit us timings; 1..10 = level-derived (auto-retunes on SF/BW change)
|
||||
uint8_t rx_ps_preamble; // 0 = auto (derive from SF); else 16 or 32 = explicit override for level calc
|
||||
};
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
@@ -541,6 +559,14 @@ public:
|
||||
virtual bool resolveAlertScope(TransportKey& /*dest*/) {
|
||||
return false; // no op by default
|
||||
}
|
||||
|
||||
virtual bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) {
|
||||
return !enable;
|
||||
};
|
||||
|
||||
virtual void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) {
|
||||
*soft = 0; *hard = 0;
|
||||
};
|
||||
};
|
||||
|
||||
class CommonCLI {
|
||||
|
||||
@@ -24,6 +24,45 @@ class CustomLR1110 : public LR1110 {
|
||||
|
||||
float getFreqMHz() const { return freqMHz; }
|
||||
|
||||
int16_t startReceiveDutyCycle(uint32_t rxPeriod, uint32_t sleepPeriod,
|
||||
RadioLibIrqFlags_t irqFlags = RADIOLIB_IRQ_RX_DEFAULT_FLAGS,
|
||||
RadioLibIrqFlags_t irqMask = RADIOLIB_IRQ_RX_DEFAULT_MASK) {
|
||||
// RadioLib's LR11x0 duty-cycle path stages RX but does not call
|
||||
// launchMode(), where the software RF switch is normally set to RX.
|
||||
// RadioLib 7.6 does not expose LR11x0::tcxoDelay. MeshCore initializes
|
||||
// these radios with RadioLib's 5000 us default TCXO delay.
|
||||
const uint32_t transitionTime = 5000 + 1000;
|
||||
if (sleepPeriod <= transitionTime) {
|
||||
return RADIOLIB_ERR_INVALID_SLEEP_PERIOD;
|
||||
}
|
||||
sleepPeriod -= transitionTime;
|
||||
|
||||
uint32_t rxPeriodRaw = (rxPeriod * 32768UL) / 1000000UL;
|
||||
uint32_t sleepPeriodRaw = (sleepPeriod * 32768UL) / 1000000UL;
|
||||
|
||||
if ((rxPeriodRaw & 0xFF000000) || (rxPeriodRaw == 0)) {
|
||||
return RADIOLIB_ERR_INVALID_RX_PERIOD;
|
||||
}
|
||||
|
||||
if ((sleepPeriodRaw & 0xFF000000) || (sleepPeriodRaw == 0)) {
|
||||
return RADIOLIB_ERR_INVALID_SLEEP_PERIOD;
|
||||
}
|
||||
|
||||
RadioModeConfig_t cfg = {
|
||||
.receive = {
|
||||
.timeout = RADIOLIB_LR11X0_RX_TIMEOUT_INF,
|
||||
.irqFlags = irqFlags,
|
||||
.irqMask = irqMask,
|
||||
.len = 0,
|
||||
}
|
||||
};
|
||||
int16_t state = this->stageMode(RADIOLIB_RADIO_MODE_RX, &cfg);
|
||||
RADIOLIB_ASSERT(state);
|
||||
|
||||
this->mod->setRfSwitchState(Module::MODE_RX);
|
||||
return this->setRxDutyCycle(rxPeriodRaw, sleepPeriodRaw, RADIOLIB_LR11X0_RX_DUTY_CYCLE_MODE_RX);
|
||||
}
|
||||
|
||||
int16_t setRxBoostedGainMode(bool en) {
|
||||
_rx_boosted = en;
|
||||
return LR1110::setRxBoostedGainMode(en);
|
||||
@@ -31,11 +70,20 @@ class CustomLR1110 : public LR1110 {
|
||||
|
||||
bool getRxBoostedGainMode() const { return _rx_boosted; }
|
||||
|
||||
// BUSY high means the chip is asleep (RX duty-cycle sleep window) or mid
|
||||
// command; any SPI access would stall until the chip's next listen window.
|
||||
bool isChipBusy() {
|
||||
uint32_t busy = this->mod->getGpio();
|
||||
return busy != RADIOLIB_NC && this->mod->hal->digitalRead(busy);
|
||||
}
|
||||
|
||||
bool isReceiving() {
|
||||
if (isChipBusy()) return false; // asleep, cannot be mid-receive
|
||||
|
||||
uint16_t irq = getIrqStatus();
|
||||
bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED));
|
||||
return detected;
|
||||
}
|
||||
|
||||
uint8_t getSpreadingFactor() const { return spreadingFactor; }
|
||||
};
|
||||
};
|
||||
@@ -28,6 +28,9 @@ public:
|
||||
bool isReceivingPacket() override {
|
||||
return ((CustomLR1110 *)_radio)->isReceiving();
|
||||
}
|
||||
bool isChipBusy() override {
|
||||
return ((CustomLR1110 *)_radio)->isChipBusy();
|
||||
}
|
||||
float getCurrentRSSI() override {
|
||||
float rssi = -110;
|
||||
((CustomLR1110 *)_radio)->getRssiInst(&rssi);
|
||||
@@ -44,6 +47,41 @@ public:
|
||||
_radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts
|
||||
}
|
||||
|
||||
bool supportsRxPowerSaving() const override { return true; }
|
||||
|
||||
protected:
|
||||
int startReceiveMode() override {
|
||||
if (_rx_ps_armed) {
|
||||
// stop the previous duty-cycle sequence with an explicit standby before
|
||||
// reconfiguring the radio
|
||||
stopReceiveDutyCycle();
|
||||
}
|
||||
if (!_rx_ps_enabled || _nf_calib_active) {
|
||||
// plain continuous RX: powersaving off, or a periodic noise-floor
|
||||
// calibration window is in progress
|
||||
return _radio->startReceive();
|
||||
}
|
||||
|
||||
const RadioLibIrqFlags_t irqFlags = RADIOLIB_IRQ_RX_DEFAULT_FLAGS;
|
||||
// route RX timeout (false preamble detect) and error IRQs to the IRQ pin
|
||||
// as well, so the wrapper re-arms RX instead of waiting forever
|
||||
const RadioLibIrqFlags_t irqMask =
|
||||
(1UL << RADIOLIB_IRQ_RX_DONE) |
|
||||
(1UL << RADIOLIB_IRQ_TIMEOUT) |
|
||||
(1UL << RADIOLIB_IRQ_CRC_ERR) |
|
||||
(1UL << RADIOLIB_IRQ_HEADER_ERR);
|
||||
|
||||
int err = ((CustomLR1110 *)_radio)->startReceiveDutyCycle(_rx_ps_rx_us, _rx_ps_sleep_us, irqFlags, irqMask);
|
||||
if (err == RADIOLIB_ERR_NONE) {
|
||||
_rx_ps_armed = true;
|
||||
return err;
|
||||
}
|
||||
|
||||
MESH_DEBUG_PRINTLN("CustomLR1110Wrapper: error: startReceiveDutyCycle(%d), falling back to continuous RX", err);
|
||||
return _radio->startReceive();
|
||||
}
|
||||
|
||||
public:
|
||||
float getLastRSSI() const override { return ((CustomLR1110 *)_radio)->getRSSI(); }
|
||||
float getLastSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); }
|
||||
|
||||
|
||||
@@ -98,12 +98,39 @@ class CustomSX1262 : public SX1262 {
|
||||
return true; // success
|
||||
}
|
||||
|
||||
// BUSY high means the chip is asleep (RX duty-cycle sleep window) or mid
|
||||
// command; any SPI access would stall until the chip's next listen window.
|
||||
bool isChipBusy() {
|
||||
uint32_t busy = this->mod->getGpio();
|
||||
return busy != RADIOLIB_NC && this->mod->hal->digitalRead(busy);
|
||||
}
|
||||
|
||||
bool isReceiving() {
|
||||
if (isChipBusy()) return false; // asleep, cannot be mid-receive
|
||||
|
||||
uint16_t irq = getIrqFlags();
|
||||
bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED);
|
||||
return detected;
|
||||
}
|
||||
|
||||
// Port of Semtech's sx126x_stop_rtc() (same registers as RadioLib's
|
||||
// fixImplicitTimeout / datasheet errata 15.3): after duty-cycle RX ends via
|
||||
// RxDone or SetStandby, the internal RTC keeps running and its pending
|
||||
// event can silently knock a subsequently started RX back to standby with
|
||||
// no IRQ, leaving the node deaf. Must be called before re-arming RX.
|
||||
int16_t stopRTC() {
|
||||
uint8_t rtcStop = 0x00;
|
||||
int16_t state = writeRegister(RADIOLIB_SX126X_REG_RTC_CTRL, &rtcStop, 1);
|
||||
RADIOLIB_ASSERT(state);
|
||||
|
||||
uint8_t rtcEvent = 0;
|
||||
state = readRegister(RADIOLIB_SX126X_REG_EVENT_MASK, &rtcEvent, 1);
|
||||
RADIOLIB_ASSERT(state);
|
||||
|
||||
rtcEvent |= 0x02; // clear the RX timeout event
|
||||
return writeRegister(RADIOLIB_SX126X_REG_EVENT_MASK, &rtcEvent, 1);
|
||||
}
|
||||
|
||||
bool getRxBoostedGainMode() {
|
||||
uint8_t rxGain = 0;
|
||||
readRegister(RADIOLIB_SX126X_REG_RX_GAIN, &rxGain, 1);
|
||||
|
||||
@@ -13,6 +13,7 @@ public:
|
||||
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
|
||||
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
|
||||
cacheParams(freq, bw, sf, cr);
|
||||
((CustomSX1262 *)_radio)->setFrequency(freq);
|
||||
((CustomSX1262 *)_radio)->setSpreadingFactor(sf);
|
||||
((CustomSX1262 *)_radio)->setBandwidth(bw);
|
||||
@@ -24,9 +25,12 @@ public:
|
||||
return ((CustomSX1262 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
bool isReceivingPacket() override {
|
||||
bool isReceivingPacket() override {
|
||||
return ((CustomSX1262 *)_radio)->isReceiving();
|
||||
}
|
||||
bool isChipBusy() override {
|
||||
return ((CustomSX1262 *)_radio)->isChipBusy();
|
||||
}
|
||||
float getCurrentRSSI() override {
|
||||
return ((CustomSX1262 *)_radio)->getRSSI(false);
|
||||
}
|
||||
@@ -42,6 +46,52 @@ public:
|
||||
((CustomSX1262 *)_radio)->sleep(false);
|
||||
}
|
||||
|
||||
bool supportsRxPowerSaving() const override { return true; }
|
||||
|
||||
protected:
|
||||
int startReceiveMode() override {
|
||||
if (_rx_ps_armed) {
|
||||
// leaving duty-cycle mode (after RxDone or a reconfig): stop the
|
||||
// sequencer and the still-running RTC, or its pending event can
|
||||
// silently abort the RX we are about to start
|
||||
stopReceiveDutyCycle();
|
||||
}
|
||||
if (!_rx_ps_enabled || _nf_calib_active) {
|
||||
// plain continuous RX: powersaving off, or a periodic noise-floor
|
||||
// calibration window is in progress
|
||||
return _radio->startReceive();
|
||||
}
|
||||
|
||||
const RadioLibIrqFlags_t irqFlags = RADIOLIB_IRQ_RX_DEFAULT_FLAGS;
|
||||
const RadioLibIrqFlags_t irqMask =
|
||||
(1UL << RADIOLIB_IRQ_RX_DONE) |
|
||||
(1UL << RADIOLIB_IRQ_TIMEOUT) |
|
||||
(1UL << RADIOLIB_IRQ_CRC_ERR) |
|
||||
(1UL << RADIOLIB_IRQ_HEADER_ERR);
|
||||
|
||||
int err = ((CustomSX1262 *)_radio)->startReceiveDutyCycle(_rx_ps_rx_us, _rx_ps_sleep_us, irqFlags, irqMask);
|
||||
if (err == RADIOLIB_ERR_NONE) {
|
||||
_rx_ps_armed = true;
|
||||
return err;
|
||||
}
|
||||
|
||||
MESH_DEBUG_PRINTLN("CustomSX1262Wrapper: error: startReceiveDutyCycle(%d), falling back to continuous RX", err);
|
||||
return _radio->startReceive();
|
||||
}
|
||||
|
||||
void stopReceiveDutyCycle() override {
|
||||
_radio->standby(); // also wakes the chip if it is in the sleep window
|
||||
((CustomSX1262 *)_radio)->stopRTC();
|
||||
_rx_ps_armed = false;
|
||||
}
|
||||
|
||||
bool radioDeepInit() override {
|
||||
// std_init() re-runs RadioLib begin(), which starts with a hardware reset
|
||||
// via NRST - the only way out of a hard-locked chip (BUSY stuck high).
|
||||
return ((CustomSX1262 *)_radio)->std_init();
|
||||
}
|
||||
|
||||
public:
|
||||
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
|
||||
|
||||
bool setRxBoostedGainMode(bool en) override {
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
#define NUM_NOISE_FLOOR_SAMPLES 64
|
||||
#define SAMPLING_THRESHOLD 14
|
||||
|
||||
// periodic noise-floor calibration windows (RX duty-cycle powersaving only)
|
||||
#define NF_CALIB_INTERVAL_MS 60000UL // at least once a minute
|
||||
#define NF_CALIB_TIMEOUT_MS 5000UL // give up on the batch (busy channel)
|
||||
#define NF_CALIB_SETTLE_MS 20UL // frontend/AGC settle after RX entry
|
||||
|
||||
static volatile uint8_t state = STATE_IDLE;
|
||||
|
||||
// this function is called when a complete packet
|
||||
@@ -48,6 +53,8 @@ uint32_t RadioLibWrapper::getRngSeed() {
|
||||
}
|
||||
|
||||
void RadioLibWrapper::setTxPower(int8_t dbm) {
|
||||
_cur_dbm = dbm;
|
||||
_dbm_valid = true;
|
||||
_radio->setOutputPower(dbm);
|
||||
}
|
||||
|
||||
@@ -84,9 +91,132 @@ void RadioLibWrapper::resetAGC() {
|
||||
_floor_sample_sum = 0;
|
||||
}
|
||||
|
||||
void RadioLibWrapper::rxPsWatchdogCheck() {
|
||||
// don't interfere mid-transmit or with a completed-but-unread packet
|
||||
// (a pending DIO1 event is itself proof the radio is alive; recvRaw() will
|
||||
// re-arm and re-base the watchdog)
|
||||
if ((state & STATE_INT_READY) != 0 || (state & ~STATE_INT_READY) == STATE_TX_WAIT) {
|
||||
_wd_observe_until = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned long now = millis();
|
||||
bool tripped = false;
|
||||
|
||||
if (_rx_ps_armed && state == STATE_RX && _wd_stuck_thresh > 0) {
|
||||
bool busy = isChipBusy();
|
||||
if (busy != _wd_last_busy) {
|
||||
// the sleep/listen wave is present -> radio healthy
|
||||
_wd_last_busy = busy;
|
||||
_wd_last_transition = now;
|
||||
_wd_stage = 0;
|
||||
_wd_strikes = 0;
|
||||
_wd_observe_until = 0;
|
||||
} else if (_wd_observe_until != 0) {
|
||||
// active observation window in progress (MCU kept awake via
|
||||
// isWatchdogObserving()); a healthy chip must toggle BUSY within it
|
||||
if ((long)(now - _wd_observe_until) >= 0) {
|
||||
_wd_observe_until = 0;
|
||||
if (!busy && isReceivingPacket()) {
|
||||
// BUSY held low by an ongoing reception (extended RX) - alive
|
||||
_wd_last_transition = now;
|
||||
_wd_strikes = 0;
|
||||
} else if (++_wd_strikes >= 2) {
|
||||
_wd_strikes = 0;
|
||||
tripped = true;
|
||||
} else {
|
||||
_wd_last_transition = now; // full threshold before the next window
|
||||
}
|
||||
}
|
||||
} else if (now - _wd_last_transition > _wd_stuck_thresh) {
|
||||
// no proof of life for too long: actively watch one full cycle
|
||||
_wd_observe_until = now + _wd_observe_ms;
|
||||
if (_wd_observe_until == 0) _wd_observe_until = 1; // 0 means "off"
|
||||
}
|
||||
} else {
|
||||
_wd_observe_until = 0;
|
||||
}
|
||||
if (_startrx_fails >= 3) tripped = true; // can't even re-arm receive mode
|
||||
|
||||
if (!tripped) return;
|
||||
|
||||
_wd_last_transition = now; // grace period before the next escalation
|
||||
_startrx_fails = 0;
|
||||
_wd_observe_until = 0;
|
||||
|
||||
if (_wd_stage == 0) {
|
||||
_wd_stage = 1;
|
||||
n_wd_soft++;
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: watchdog: RX duty-cycle stuck, soft re-arm");
|
||||
state = STATE_IDLE; // next recvRaw() re-arms receive mode
|
||||
} else {
|
||||
_wd_stage = 2;
|
||||
n_wd_hard++;
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: watchdog: still stuck, hard radio reset");
|
||||
if (radioDeepInit()) {
|
||||
_rx_ps_armed = false; // chip is factory-fresh after NRST
|
||||
_radio->setPacketReceivedAction(setFlag);
|
||||
if (_params_valid) setParams(_cur_freq, _cur_bw, _cur_sf, _cur_cr);
|
||||
if (_dbm_valid) _radio->setOutputPower(_cur_dbm);
|
||||
}
|
||||
state = STATE_IDLE; // re-arm (rx powersaving settings are kept in members)
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic noise-floor calibration, active only with RX duty-cycle powersaving:
|
||||
// a duty-cycled receiver can't be sampled reliably (the frontend is off in the
|
||||
// sleep windows and settling right after each wake), so at least once a minute
|
||||
// the receive mode is dropped to plain continuous RX, a fresh sample batch is
|
||||
// collected exactly like the non-powersaving path does, and the duty cycle is
|
||||
// re-armed. The published average stays in _noise_floor as usual.
|
||||
void RadioLibWrapper::noiseFloorCalibCheck() {
|
||||
unsigned long now = millis();
|
||||
if (_nf_calib_active) {
|
||||
if (!_rx_ps_enabled || (long)(now - _nf_calib_deadline) >= 0) {
|
||||
// powersaving turned off mid-window, or the batch couldn't complete
|
||||
// (busy channel / stuck filter) - keep the previous floor
|
||||
endNoiseFloorCalib(now);
|
||||
}
|
||||
} else if (_rx_ps_enabled && _rx_ps_armed && state == STATE_RX
|
||||
&& now - _nf_last_calib >= NF_CALIB_INTERVAL_MS
|
||||
&& !isReceivingPacket()) {
|
||||
// never interrupt an ongoing reception to calibrate (a TX in flight is
|
||||
// already excluded by state == STATE_RX); retries next loop iteration
|
||||
_nf_calib_active = true;
|
||||
_nf_calib_deadline = now + NF_CALIB_TIMEOUT_MS;
|
||||
_nf_sample_from = now + NF_CALIB_SETTLE_MS;
|
||||
_num_floor_samples = 0; // start a fresh batch for this window
|
||||
_floor_sample_sum = 0;
|
||||
state = STATE_IDLE; // recvRaw() re-arms; startReceiveMode() sees the
|
||||
// active flag and starts continuous RX, not duty-cycle
|
||||
}
|
||||
}
|
||||
|
||||
void RadioLibWrapper::endNoiseFloorCalib(unsigned long now) {
|
||||
_nf_calib_active = false;
|
||||
_nf_last_calib = now;
|
||||
// force a receive re-arm back into duty-cycle mode, but don't clobber a
|
||||
// completed-but-unread packet or an in-flight TX (recvRaw()/onSendFinished()
|
||||
// will re-arm right after those anyway; same guard style as setRxPowerSaving)
|
||||
if ((state & STATE_INT_READY) == 0 && (state & ~STATE_INT_READY) != STATE_TX_WAIT) {
|
||||
state = STATE_IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
void RadioLibWrapper::loop() {
|
||||
if (_rx_ps_enabled) {
|
||||
rxPsWatchdogCheck();
|
||||
}
|
||||
noiseFloorCalibCheck();
|
||||
|
||||
if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) {
|
||||
if (!isReceivingPacket()) {
|
||||
// Noise floor is only sampled outside RX duty-cycle mode: continuously in
|
||||
// plain RX (powersaving off), or inside the periodic calibration window
|
||||
// (powersaving on), skipping the first moments after RX entry there while
|
||||
// the frontend/AGC settles (unsettled GetRssiInst reads ~-127 dBm garbage).
|
||||
if (!_rx_ps_armed
|
||||
&& !(_nf_calib_active && (long)(millis() - _nf_sample_from) < 0)
|
||||
&& !isReceivingPacket()) {
|
||||
int rssi = getCurrentRSSI();
|
||||
if (rssi < _noise_floor + SAMPLING_THRESHOLD) { // only consider samples below current floor + sampling THRESHOLD
|
||||
_num_floor_samples++;
|
||||
@@ -101,50 +231,110 @@ void RadioLibWrapper::loop() {
|
||||
_floor_sample_sum = 0;
|
||||
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor);
|
||||
|
||||
if (_nf_calib_active) {
|
||||
endNoiseFloorCalib(millis()); // fresh floor published - back to duty cycle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RadioLibWrapper::startRecv() {
|
||||
int err = _radio->startReceive();
|
||||
int err = startReceiveMode();
|
||||
if (err == RADIOLIB_ERR_NONE) {
|
||||
state = STATE_RX;
|
||||
_startrx_fails = 0;
|
||||
if (_rx_ps_armed) {
|
||||
// (re)base the duty-cycle watchdog on the freshly armed cycle
|
||||
_wd_last_busy = isChipBusy();
|
||||
_wd_last_transition = millis();
|
||||
// Longest legitimate silence on the BUSY pin: one full cycle, plus the
|
||||
// extended RX after a (possibly false) preamble detect (2*rx + sleep),
|
||||
// plus a worst-case packet airtime, plus margin for TCXO/transitions.
|
||||
// Floored at 60s so a light-sleeping MCU (ESP32 wakes every ~30s) opens
|
||||
// an observation window every couple of wakeups instead of on each one.
|
||||
uint32_t rx_ms = _rx_ps_rx_us / 1000, sleep_ms = _rx_ps_sleep_us / 1000;
|
||||
_wd_stuck_thresh = (rx_ms + sleep_ms) + 2 * (2 * rx_ms + sleep_ms)
|
||||
+ getEstAirtimeFor(MAX_TRANS_UNIT) + 1000;
|
||||
if (_wd_stuck_thresh < 60000) _wd_stuck_thresh = 60000;
|
||||
// active observation window must cover one full duty cycle
|
||||
_wd_observe_ms = rx_ms + sleep_ms + 50;
|
||||
if (_wd_observe_ms > 1500) _wd_observe_ms = 1500;
|
||||
}
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive(%d)", err);
|
||||
if (_startrx_fails < 255) _startrx_fails++;
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceiveMode(%d)", err);
|
||||
}
|
||||
}
|
||||
|
||||
int RadioLibWrapper::startReceiveMode() {
|
||||
return _radio->startReceive();
|
||||
}
|
||||
|
||||
void RadioLibWrapper::stopReceiveDutyCycle() {
|
||||
// The duty-cycle sequencer only stops on RxDone or an explicit standby;
|
||||
// issuing other mode commands while it runs leads to undefined behaviour.
|
||||
_radio->standby();
|
||||
_rx_ps_armed = false;
|
||||
}
|
||||
|
||||
bool RadioLibWrapper::isPacketReady() {
|
||||
if (!_rx_ps_armed) return true; // continuous RX: DIO1 only fires for RxDone/TxDone here
|
||||
|
||||
// In duty-cycle RX the DIO1 interrupt also fires for RX timeout (false
|
||||
// preamble detect) and header errors. GetRxBufferStatus still reports the
|
||||
// *previous* packet's length then, so reading the buffer would re-deliver
|
||||
// stale bytes as a ghost packet. Only read when the radio reports RxDone.
|
||||
// (checkIrq errors are treated as ready, falling back to old behaviour.)
|
||||
return _radio->checkIrq(RADIOLIB_IRQ_RX_DONE) != 0;
|
||||
}
|
||||
|
||||
bool RadioLibWrapper::isInRecvMode() const {
|
||||
return (state & ~STATE_INT_READY) == STATE_RX;
|
||||
}
|
||||
|
||||
bool RadioLibWrapper::setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) {
|
||||
if (enabled && !supportsRxPowerSaving()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_rx_ps_enabled = enabled;
|
||||
_rx_ps_rx_us = rx_us;
|
||||
_rx_ps_sleep_us = sleep_us;
|
||||
// Force the next recvRaw() to arm the requested RX mode, but don't clobber a
|
||||
// completed-but-unread packet (STATE_INT_READY): recvRaw() will consume it and
|
||||
// then re-arm with the new mode. Also leave an in-flight TX alone. (Same
|
||||
// non-atomic guard style as resetAGC().)
|
||||
if ((state & STATE_INT_READY) == 0 && (state & ~STATE_INT_READY) != STATE_TX_WAIT) {
|
||||
state = STATE_IDLE;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
|
||||
int len = 0;
|
||||
if (state & STATE_INT_READY) {
|
||||
last_radio_interrupt_millis = millis(); // ISR fired → radio hardware is alive
|
||||
len = _radio->getPacketLength();
|
||||
if (len > 0) {
|
||||
if (len > sz) { len = sz; }
|
||||
int err = _radio->readData(bytes, len);
|
||||
if (err != RADIOLIB_ERR_NONE) {
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: readData(%d)", err);
|
||||
len = 0;
|
||||
n_recv_errors++;
|
||||
} else {
|
||||
// Serial.print(" readData() -> "); Serial.println(len);
|
||||
n_recv++;
|
||||
last_recv_millis = millis();
|
||||
if (isPacketReady()) {
|
||||
len = _radio->getPacketLength();
|
||||
if (len > 0) {
|
||||
if (len > sz) { len = sz; }
|
||||
int err = _radio->readData(bytes, len);
|
||||
if (err != RADIOLIB_ERR_NONE) {
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: readData(%d)", err);
|
||||
len = 0;
|
||||
n_recv_errors++;
|
||||
} else {
|
||||
// Serial.print(" readData() -> "); Serial.println(len);
|
||||
n_recv++;
|
||||
last_recv_millis = millis();
|
||||
}
|
||||
}
|
||||
}
|
||||
state = STATE_IDLE; // need another startReceive()
|
||||
}
|
||||
|
||||
if (state != STATE_RX) {
|
||||
int err = _radio->startReceive();
|
||||
if (err == RADIOLIB_ERR_NONE) {
|
||||
state = STATE_RX;
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive(%d)", err);
|
||||
}
|
||||
startRecv();
|
||||
}
|
||||
return len;
|
||||
}
|
||||
@@ -154,6 +344,11 @@ uint32_t RadioLibWrapper::getEstAirtimeFor(int len_bytes) {
|
||||
}
|
||||
|
||||
bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) {
|
||||
if (_rx_ps_armed) {
|
||||
// stop the duty-cycle sequencer before SetTx, otherwise its next RTC
|
||||
// event can fire mid-transmission and abort the TX
|
||||
stopReceiveDutyCycle();
|
||||
}
|
||||
_board->onBeforeTransmit();
|
||||
int err = _radio->startTransmit((uint8_t *) bytes, len);
|
||||
if (err == RADIOLIB_ERR_NONE) {
|
||||
@@ -186,8 +381,11 @@ int16_t RadioLibWrapper::performChannelScan() {
|
||||
}
|
||||
|
||||
bool RadioLibWrapper::isChannelActive() {
|
||||
// int.thresh: RSSI-based interference detection (relative to noise floor)
|
||||
if (_threshold != 0 && getCurrentRSSI() > _noise_floor + _threshold) return true;
|
||||
// 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())
|
||||
&& getCurrentRSSI() > _noise_floor + _threshold) return true;
|
||||
|
||||
// cad: hardware channel activity detection
|
||||
if (_cad_enabled) {
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
#include <Mesh.h>
|
||||
#include <RadioLib.h>
|
||||
|
||||
// Fallback RX powersaving timings, only used until setRxPowerSaving() is called
|
||||
// (begin() always applies the persisted values). The authoritative defaults live
|
||||
// in CommonCLI.h as RX_POWERSAVING_DEFAULT_RX_US / _SLEEP_US and are delivered
|
||||
// via NodePrefs; keep these mirrored.
|
||||
#define RX_PS_FALLBACK_RX_US 65625UL
|
||||
#define RX_PS_FALLBACK_SLEEP_US 60000UL
|
||||
|
||||
class RadioLibWrapper : public mesh::Radio {
|
||||
protected:
|
||||
PhysicalLayer* _radio;
|
||||
@@ -15,20 +22,79 @@ protected:
|
||||
unsigned long last_recv_millis;
|
||||
unsigned long last_radio_interrupt_millis; // updated on any ISR event, even CRC errors
|
||||
uint8_t _preamble_sf;
|
||||
bool _rx_ps_enabled;
|
||||
bool _rx_ps_armed; // radio is currently in RX duty-cycle mode
|
||||
uint32_t _rx_ps_rx_us;
|
||||
uint32_t _rx_ps_sleep_us;
|
||||
|
||||
// RX duty-cycle watchdog: a healthy duty cycle shows a square wave on the
|
||||
// BUSY pin (high in the sleep window / TCXO warmup, low while listening).
|
||||
// If the wave stops, the chip fell out of the cycle without an IRQ.
|
||||
// Passive sampling only works while the main loop spins; on MCUs that light
|
||||
// sleep between wakeups the watchdog instead opens an "active observation"
|
||||
// window (isWatchdogObserving() keeps the MCU awake) spanning one full radio
|
||||
// cycle - a healthy chip must toggle BUSY within it.
|
||||
bool _wd_last_busy;
|
||||
uint8_t _wd_stage; // 0 = healthy, 1 = soft re-arm done, 2 = hard reset done
|
||||
uint8_t _wd_strikes; // consecutive failed observation windows
|
||||
uint8_t _startrx_fails; // consecutive startReceiveMode() failures
|
||||
unsigned long _wd_last_transition; // millis of last BUSY level change (proof of life)
|
||||
unsigned long _wd_stuck_thresh; // ms without proof of life before observing
|
||||
unsigned long _wd_observe_until; // 0 = not observing, else millis deadline
|
||||
uint32_t _wd_observe_ms; // observation window: one full cycle + margin
|
||||
uint32_t n_wd_soft, n_wd_hard;
|
||||
|
||||
// last applied radio settings, reapplied after a hard radio reset
|
||||
float _cur_freq, _cur_bw;
|
||||
uint8_t _cur_sf, _cur_cr;
|
||||
int8_t _cur_dbm;
|
||||
bool _params_valid, _dbm_valid;
|
||||
|
||||
// Periodic noise-floor calibration (only while RX duty-cycle powersaving is
|
||||
// armed): a duty-cycled receiver can't be sampled reliably, so at least once
|
||||
// a minute the wrapper drops to plain continuous RX, collects a fresh sample
|
||||
// batch exactly like the non-powersaving path does, publishes the average
|
||||
// into _noise_floor and re-arms the duty cycle.
|
||||
bool _nf_calib_active;
|
||||
unsigned long _nf_last_calib; // millis of last completed/attempted window
|
||||
unsigned long _nf_calib_deadline; // abort window if the batch can't complete
|
||||
unsigned long _nf_sample_from; // no samples before this (RX entry settle)
|
||||
|
||||
void idle() override;
|
||||
void startRecv() override;
|
||||
void rxPsWatchdogCheck();
|
||||
void noiseFloorCalibCheck();
|
||||
void endNoiseFloorCalib(unsigned long now);
|
||||
void cacheParams(float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
_cur_freq = freq; _cur_bw = bw; _cur_sf = sf; _cur_cr = cr; _params_valid = true;
|
||||
}
|
||||
virtual int startReceiveMode();
|
||||
virtual void stopReceiveDutyCycle();
|
||||
virtual bool isPacketReady();
|
||||
// true while the chip cannot service SPI (RX duty-cycle sleep window, or
|
||||
// briefly while processing a command); radios expose this via the BUSY pin
|
||||
virtual bool isChipBusy() { return false; }
|
||||
// full radio recovery: hardware reset (NRST) + re-init to boot defaults;
|
||||
// returns false if unsupported. Caller reapplies cached runtime params.
|
||||
virtual bool radioDeepInit() { return false; }
|
||||
float packetScoreInt(float snr, int sf, int packet_len);
|
||||
virtual bool isReceivingPacket() =0;
|
||||
virtual void doResetAGC();
|
||||
|
||||
public:
|
||||
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _preamble_sf(0) {
|
||||
n_recv = n_sent = n_recv_errors = 0;
|
||||
last_recv_millis = 0;
|
||||
last_radio_interrupt_millis = 0;
|
||||
_cad_enabled = false;
|
||||
}
|
||||
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board)
|
||||
: _radio(&radio), _board(&board), _preamble_sf(0), _rx_ps_enabled(false), _rx_ps_armed(false),
|
||||
_rx_ps_rx_us(RX_PS_FALLBACK_RX_US), _rx_ps_sleep_us(RX_PS_FALLBACK_SLEEP_US),
|
||||
_wd_last_busy(false), _wd_stage(0), _wd_strikes(0), _startrx_fails(0), _wd_last_transition(0),
|
||||
_wd_stuck_thresh(0), _wd_observe_until(0), _wd_observe_ms(0),
|
||||
_params_valid(false), _dbm_valid(false),
|
||||
_nf_calib_active(false), _nf_last_calib(0), _nf_calib_deadline(0), _nf_sample_from(0)
|
||||
{
|
||||
n_recv = n_sent = n_recv_errors = n_wd_soft = n_wd_hard = 0;
|
||||
last_recv_millis = 0;
|
||||
last_radio_interrupt_millis = 0;
|
||||
_cad_enabled = false;
|
||||
}
|
||||
|
||||
void begin() override;
|
||||
virtual void powerOff() { _radio->sleep(); }
|
||||
@@ -38,6 +104,7 @@ public:
|
||||
bool isSendComplete() override;
|
||||
void onSendFinished() override;
|
||||
bool isInRecvMode() const override;
|
||||
bool setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) override;
|
||||
bool isChannelActive();
|
||||
|
||||
bool isReceiving() override {
|
||||
@@ -68,6 +135,15 @@ public:
|
||||
uint32_t getPacketsRecv() const { return n_recv; }
|
||||
uint32_t getPacketsRecvErrors() const override { return n_recv_errors; }
|
||||
uint32_t getPacketsSent() const { return n_sent; }
|
||||
uint32_t getRxPsWatchdogSoftCount() const { return n_wd_soft; }
|
||||
uint32_t getRxPsWatchdogHardCount() const { return n_wd_hard; }
|
||||
// true while the watchdog is actively watching for BUSY transitions; used by
|
||||
// the app's hasPendingWork() to keep the MCU out of light sleep for the window
|
||||
bool isWatchdogObserving() const { return _wd_observe_until != 0; }
|
||||
// true while a periodic noise-floor calibration window is in progress; the
|
||||
// app's hasPendingWork() must keep the MCU awake so the sample batch and the
|
||||
// return to duty-cycle complete promptly
|
||||
bool isCalibratingNoiseFloor() const { return _nf_calib_active; }
|
||||
void resetStats() { n_recv = n_sent = n_recv_errors = 0; }
|
||||
|
||||
uint8_t getRadioState() const override;
|
||||
|
||||
Reference in new issue
Block a user