mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-09-14 15:16:40 +00:00
feat(power): RX duty-cycle watchdog, noise-floor recal, GPS duty-cycling
Three power-saving additions, prompted by comparing this fork's existing RX duty-cycle support against IoTThinks/EasySkyMesh: - RX duty-cycle watchdog: the SX126x's hardware RX<->sleep sequencer runs with no MCU polling, so a desync (a known failure mode) previously had nothing watching for it. A new watchdog samples the BUSY pin every tick; no transition for too long triggers a soft re-arm, then a full chip reset (with cached radio params reapplied, since std_init() resets to compiled firmware defaults) if that doesn't clear it. Soft/hard recovery counts surface on Tools > Diagnostics > Live as "RXPS wd s/h". - Noise-floor recalibration during power-save: sampling was previously skipped entirely while duty-cycling, freezing int.thresh interference detection at whatever the floor was when power-save turned on. Now borrows a brief continuous-RX window once a minute to take a fresh reading before re-arming duty-cycle. - GPS duty-cycling (Settings > System > "GPS pwr"): cycles GPS off between fixes instead of running it continuously. Each wake waits for a fix (capped at 60s) before sleeping again for the configured interval. Repurposes the long-dead NodePrefs::gps_interval byte rather than adding a new persisted field. A "is anything live using GPS right now" hold in UITask keeps GPS continuously on whenever trail recording, live-share, an armed Locator, or the Compass/Nearby-navigate view actually need a live fix, so none of those features degrade. Locator crossing-state is reset on each wake so a still-settling first fix can't read as a false geofence crossing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -264,6 +264,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
file.read((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet));
|
||||
file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled));
|
||||
file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval));
|
||||
if (_prefs.gps_interval > 86400) _prefs.gps_interval = 0; // now a duty-cycle sleep window (secs); 0 = disabled
|
||||
file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config));
|
||||
file.read((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops));
|
||||
file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain));
|
||||
|
||||
@@ -308,11 +308,12 @@ public:
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
void applyGpsPrefs() {
|
||||
sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0");
|
||||
if (_prefs.gps_interval > 0) {
|
||||
char interval_str[12]; // Max: 24 hours = 86400 seconds (5 digits + null)
|
||||
sprintf(interval_str, "%u", _prefs.gps_interval);
|
||||
sensors.setSettingValue("gps_interval", interval_str);
|
||||
}
|
||||
// gps_interval doubles as the GPS duty-cycle sleep window in seconds
|
||||
// (0 = disabled, GPS stays continuous) -- see EnvironmentSensorManager::
|
||||
// gpsDutyCycleLoop(). Max: 24 hours = 86400 seconds (5 digits + null).
|
||||
char interval_str[12];
|
||||
sprintf(interval_str, "%u", _prefs.gps_interval);
|
||||
sensors.setSettingValue("gps_interval", interval_str);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ struct NodePrefs { // persisted to file
|
||||
uint8_t buzzer_quiet;
|
||||
uint8_t buzzer_volume; // 0=min..4=max, default 4
|
||||
uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled)
|
||||
uint32_t gps_interval; // GPS read interval in seconds
|
||||
uint32_t gps_interval; // GPS duty-cycle sleep window in seconds (0 = disabled, GPS stays continuous)
|
||||
uint8_t autoadd_config; // bitmask for auto-add contacts config
|
||||
uint8_t rx_boosted_gain; // SX126x RX boosted gain mode (0=power saving, 1=boosted)
|
||||
uint8_t client_repeat;
|
||||
|
||||
@@ -143,6 +143,13 @@ class DiagnosticsScreen : public UIScreen {
|
||||
if (len > 0 && buf[len - 1] == ' ') buf[len - 1] = '\0';
|
||||
}
|
||||
addRow("Errors", buf);
|
||||
|
||||
// RX duty-cycle watchdog recovery counts since boot/reset (soft re-arm /
|
||||
// hard chip reset). Always 0/0 on radios or profiles that never arm
|
||||
// duty-cycle power-save (repeaters force it off — see applyPowerSave()).
|
||||
snprintf(buf, sizeof(buf), "%lu/%lu", (unsigned long)radio_driver.getRxPsWatchdogSoftCount(),
|
||||
(unsigned long)radio_driver.getRxPsWatchdogHardCount());
|
||||
addRow("RXPS wd s/h", buf);
|
||||
}
|
||||
|
||||
void buildSystemLines() {
|
||||
@@ -257,7 +264,8 @@ public:
|
||||
if (_reset_menu.active) {
|
||||
auto res = _reset_menu.handleInput(c); // Back/Cancel dismisses; the only item is "Reset counters"
|
||||
if (res == PopupMenu::SELECTED) {
|
||||
the_mesh.resetStats(); // zeroes Dispatcher per-type counters + Mesh forward count + err flags
|
||||
the_mesh.resetStats(); // zeroes Dispatcher per-type counters + Mesh forward count + err flags
|
||||
radio_driver.resetStats(); // zeroes the radio's own counters, incl. RXPS watchdog soft/hard counts
|
||||
_task->showAlert("Counters reset", 800);
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -734,6 +734,11 @@ public:
|
||||
_sort_label[0] = '\0';
|
||||
}
|
||||
|
||||
// Whether the full-screen navigate-to-node view is up -- used by UITask's
|
||||
// GPS duty-cycle "is anything live using GPS right now" check, since this
|
||||
// view needs an unbroken stream of fixes for bearing/ETA, not a stale one.
|
||||
bool isNavigating() const { return _nav; }
|
||||
|
||||
void onShow() override {
|
||||
_sel = _scroll = 0;
|
||||
_detail = false;
|
||||
|
||||
@@ -66,6 +66,9 @@ class SettingsScreen : public UIScreen {
|
||||
DEVICE_NAME,
|
||||
TIMEZONE,
|
||||
LOW_BAT,
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
GPS_DUTY_CYCLE,
|
||||
#endif
|
||||
UNITS,
|
||||
REBOOT,
|
||||
// Keyboard section
|
||||
@@ -106,12 +109,24 @@ class SettingsScreen : public UIScreen {
|
||||
static const char* AUTO_OFF_LABELS[5];
|
||||
static const int AUTO_OFF_COUNT = 5;
|
||||
#endif
|
||||
// GPS update interval tables are no longer surfaced in Settings — the sensor
|
||||
// manager defaults to 1 s when nothing else sets it. Pref byte _prefs.gps_interval
|
||||
// is retained for backwards compatibility.
|
||||
static const uint16_t LOW_BAT_OPTS[7];
|
||||
static const char* LOW_BAT_LABELS[7];
|
||||
static const int LOW_BAT_COUNT = 7;
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
// GPS duty-cycle sleep window: how long GPS naps between fix acquisitions.
|
||||
// "off" (0) keeps it continuously on, today's behaviour. Backed by
|
||||
// NodePrefs::gps_interval, seconds.
|
||||
static const uint32_t GPS_DUTY_OPTS[6];
|
||||
static const char* GPS_DUTY_LABELS[6];
|
||||
static const int GPS_DUTY_COUNT = 6;
|
||||
int gpsDutyIndex() {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
if (!p) return 0;
|
||||
for (int i = 0; i < GPS_DUTY_COUNT; i++)
|
||||
if (GPS_DUTY_OPTS[i] == p->gps_interval) return i;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
static const char* BATT_DISPLAY_LABELS[3];
|
||||
static const int BATT_DISPLAY_COUNT = 3;
|
||||
static const char* SOUND_LABELS[4];
|
||||
@@ -562,6 +577,12 @@ class SettingsScreen : public UIScreen {
|
||||
display.print("LowBat");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(LOW_BAT_LABELS[lowBatIndex()]);
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
} else if (item == GPS_DUTY_CYCLE) {
|
||||
display.print("GPS pwr");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(GPS_DUTY_LABELS[gpsDutyIndex()]);
|
||||
#endif
|
||||
} else if (item == UNITS) {
|
||||
display.print("Units");
|
||||
display.setCursor(valCol(display), y);
|
||||
@@ -923,6 +944,17 @@ public:
|
||||
if (left) idx = (idx + LOW_BAT_COUNT - 1) % LOW_BAT_COUNT;
|
||||
if (left || right) { p->low_batt_mv = LOW_BAT_OPTS[idx]; _dirty = true; return true; }
|
||||
}
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
if (_selected == GPS_DUTY_CYCLE && p && (left || right)) {
|
||||
int idx = gpsDutyIndex();
|
||||
if (right) idx = (idx + 1) % GPS_DUTY_COUNT;
|
||||
if (left) idx = (idx + GPS_DUTY_COUNT - 1) % GPS_DUTY_COUNT;
|
||||
p->gps_interval = GPS_DUTY_OPTS[idx];
|
||||
_task->applyGpsInterval();
|
||||
_dirty = true;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (_selected == UNITS && p && (left || right || enter)) {
|
||||
p->units_imperial ^= 1;
|
||||
_dirty = true;
|
||||
@@ -1053,6 +1085,10 @@ const char* SettingsScreen::AUTO_OFF_LABELS[5] = { "5s", "15s", "30s", "60s"
|
||||
#endif
|
||||
const uint16_t SettingsScreen::LOW_BAT_OPTS[7] = { 0, 3000, 3100, 3200, 3300, 3400, 3500 };
|
||||
const char* SettingsScreen::LOW_BAT_LABELS[7] = { "off", "3.0V", "3.1V", "3.2V", "3.3V", "3.4V", "3.5V" };
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
const uint32_t SettingsScreen::GPS_DUTY_OPTS[6] = { 0, 60, 300, 900, 1800, 3600 };
|
||||
const char* SettingsScreen::GPS_DUTY_LABELS[6] = { "off", "1 min", "5 min", "15 min", "30 min", "1 h" };
|
||||
#endif
|
||||
const char* SettingsScreen::BATT_DISPLAY_LABELS[3] = { "icon", "%", "V" };
|
||||
const char* SettingsScreen::SOUND_LABELS[4] = { "built-in", "M1", "M2", "None" };
|
||||
const char* SettingsScreen::AD_SCOPE_LABELS[2] = { "All", "Zero-hop" };
|
||||
|
||||
@@ -497,6 +497,10 @@ class HomeScreen : public UIScreen {
|
||||
// keep running with Bluetooth off, so their cue must not vanish with it.
|
||||
LocationProvider* loc = _sensors ? _sensors->getLocationProvider() : nullptr;
|
||||
bool gps_on = loc && _node_prefs && _node_prefs->gps_enabled;
|
||||
// Blinks while GPS is napping between duty-cycle wakes -- same convention
|
||||
// the background-mode icons below already use for "running, but not busy
|
||||
// right this instant".
|
||||
bool gps_napping = _sensors && _sensors->isGpsDutySleeping();
|
||||
bool mute_on = false;
|
||||
#ifdef PIN_BUZZER
|
||||
mute_on = _task->isBuzzerQuiet();
|
||||
@@ -504,7 +508,7 @@ class HomeScreen : public UIScreen {
|
||||
struct Sicon { bool active; const MiniIcon* icon; bool boxed; bool blink; };
|
||||
const Sicon icons[] = {
|
||||
{ _task->isSerialEnabled(), &ICON_BLUETOOTH, _task->isSerialEnabled() && _task->isBLEConnected(), false },
|
||||
{ gps_on, &ICON_GPS, gps_on && loc->isValid(), false },
|
||||
{ gps_on, &ICON_GPS, gps_on && loc->isValid(), gps_napping },
|
||||
{ _node_prefs && _node_prefs->alarm_on, &ICON_ALARM, true, false },
|
||||
{ mute_on, &ICON_MUTE, true, false },
|
||||
{ _node_prefs && _node_prefs->advert_auto_interval_sec > 0, &ICON_ADVERT, true, true },
|
||||
@@ -2543,6 +2547,28 @@ void UITask::loop() {
|
||||
next_batt_chck = millis() + 8000;
|
||||
}
|
||||
|
||||
// GPS duty-cycle hold — tells the sensor manager whether *anything* needs
|
||||
// an unbroken stream of fixes right now, so it knows it's safe to let GPS
|
||||
// nap between reads (EnvironmentSensorManager::gpsDutyCycleLoop()).
|
||||
// Deliberately excludes the COG sampler just below: that one runs
|
||||
// unconditionally every ~1s specifically so a heading is ready whenever a
|
||||
// screen opens, and including it here would keep GPS permanently awake and
|
||||
// defeat duty-cycling entirely — it just goes stale during a sleep window
|
||||
// and catches up whenever GPS is awake for any other reason.
|
||||
if (_sensors) {
|
||||
bool gps_needed_live =
|
||||
(_trail.isActive() && !_trail.isPaused())
|
||||
|| (_node_prefs && _node_prefs->loc_share_enabled)
|
||||
|| (_node_prefs && _node_prefs->locator_enabled && _node_prefs->locator_has_target)
|
||||
|| curr == compass_screen
|
||||
|| (curr == nearby_screen && ((NearbyScreen*)nearby_screen)->isNavigating());
|
||||
_sensors->setGpsKeepAwake(gps_needed_live);
|
||||
// A fresh wake (either a duty-cycle wake, or GPS forced continuously back
|
||||
// on) may deliver a still-settling first fix — re-seed the locator's
|
||||
// crossing state so that doesn't read as a spurious geofence crossing.
|
||||
if (_sensors->consumeGpsWakeEvent()) resetLocator();
|
||||
}
|
||||
|
||||
// GPS trail sampling — runs in the background while the trail is
|
||||
// active, independent of which screen is shown. Skips silently if no GPS
|
||||
// fix; min-delta gate inside addPoint() avoids near-stationary spam.
|
||||
@@ -3334,6 +3360,15 @@ void UITask::applyApc() {
|
||||
the_mesh.applyApc(); // (re)initialise Adaptive Power Control from prefs
|
||||
}
|
||||
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
void UITask::applyGpsInterval() {
|
||||
if (_node_prefs == NULL || _sensors == NULL) return;
|
||||
char buf[12];
|
||||
sprintf(buf, "%u", _node_prefs->gps_interval);
|
||||
_sensors->setSettingValue("gps_interval", buf);
|
||||
}
|
||||
#endif
|
||||
|
||||
void UITask::applyRadioParams() {
|
||||
if (_node_prefs == NULL) return;
|
||||
the_mesh.applyRepeaterRadio(); // companion params, or the repeater profile if relaying with one set
|
||||
|
||||
@@ -455,6 +455,9 @@ public:
|
||||
void applyPowerSave(); // hardware duty-cycle RX on/off from prefs
|
||||
void applyApc(); // Adaptive Power Control on/off from prefs
|
||||
void applyRadioParams(); // freq/bw/sf/cr from prefs (radio preset change)
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
void applyGpsInterval(); // GPS duty-cycle sleep window from prefs
|
||||
#endif
|
||||
// Save-on-exit helper for the screen `_dirty` pattern: persists NodePrefs once
|
||||
// only if `dirty`, then clears the flag. Standardises the screens' exit paths
|
||||
// (some used to leave the flag set, relying on onShow() to reset it) and keeps
|
||||
|
||||
@@ -23,6 +23,17 @@ public:
|
||||
virtual const char* getSettingValue(int i) const { return NULL; }
|
||||
virtual bool setSettingValue(const char* name, const char* value) { return false; }
|
||||
virtual LocationProvider* getLocationProvider() { return NULL; }
|
||||
// GPS duty-cycle hold: force GPS continuously on regardless of any sleep
|
||||
// schedule, because something needs an unbroken stream of fixes right now.
|
||||
virtual void setGpsKeepAwake(bool on) { }
|
||||
// One-shot: true the first call after GPS transitions sleep->awake, so a
|
||||
// caller can re-seed state that a stale reading could otherwise corrupt
|
||||
// (e.g. a geofence's crossing state). False the rest of the time.
|
||||
virtual bool consumeGpsWakeEvent() { return false; }
|
||||
// True while GPS is in the sleep phase of its duty cycle (physically off,
|
||||
// waiting for the next scheduled wake) -- purely a UI cue, e.g. to blink
|
||||
// the GPS status icon.
|
||||
virtual bool isGpsDutySleeping() const { return false; }
|
||||
virtual int getAvailableLPPTypes(uint8_t* types, int max_count) const { return 0; }
|
||||
|
||||
// Helper functions to manage setting by keys (useful in many places ...)
|
||||
|
||||
@@ -9,10 +9,20 @@
|
||||
#endif
|
||||
|
||||
class CustomSX1262Wrapper : public RadioLibWrapper {
|
||||
// Cached runtime radio params, only used to recover from a hard reset (see
|
||||
// radioHardReset()): std_init() re-applies the compiled LORA_FREQ/BW/SF/CR
|
||||
// firmware defaults, not whatever the user has actually configured, so
|
||||
// these are needed to restore real state afterwards.
|
||||
float _wd_freq = 0, _wd_bw = 0;
|
||||
uint8_t _wd_sf = 0, _wd_cr = 0;
|
||||
bool _wd_params_valid = false;
|
||||
bool _wd_rx_boosted_gain = false;
|
||||
|
||||
public:
|
||||
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
|
||||
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
|
||||
_wd_freq = freq; _wd_bw = bw; _wd_sf = sf; _wd_cr = cr; _wd_params_valid = true;
|
||||
((CustomSX1262 *)_radio)->setFrequency(freq);
|
||||
((CustomSX1262 *)_radio)->setSpreadingFactor(sf);
|
||||
((CustomSX1262 *)_radio)->setBandwidth(bw);
|
||||
@@ -55,9 +65,42 @@ public:
|
||||
}
|
||||
|
||||
void setRxBoostedGainMode(bool en) override {
|
||||
_wd_rx_boosted_gain = en;
|
||||
((CustomSX1262 *)_radio)->setRxBoostedGainMode(en);
|
||||
}
|
||||
bool getRxBoostedGainMode() const override {
|
||||
return ((CustomSX1262 *)_radio)->getRxBoostedGainMode();
|
||||
}
|
||||
|
||||
bool supportsRxPsWatchdog() const override { return true; }
|
||||
|
||||
// BUSY is high whenever the chip can't service SPI -- including the sleep
|
||||
// window of an armed RX duty-cycle. Same access pattern already used by
|
||||
// sx126xResetAGC() in SX126xReset.h.
|
||||
bool isChipBusy() override {
|
||||
SX126x* radio = (SX126x *)_radio;
|
||||
return radio->mod->hal->digitalRead(radio->mod->getGpio());
|
||||
}
|
||||
|
||||
// Full chip reset + re-init after a stuck RX duty-cycle that a soft re-arm
|
||||
// didn't clear. std_init() re-applies compiled firmware defaults, not the
|
||||
// user's runtime settings, so reapply the cached params and re-attach the
|
||||
// packet-received action once it returns.
|
||||
bool radioHardReset() override {
|
||||
if (!((CustomSX1262 *)_radio)->std_init(&SPI)) return false;
|
||||
reattachRecvAction();
|
||||
if (_wd_params_valid) {
|
||||
((CustomSX1262 *)_radio)->setFrequency(_wd_freq);
|
||||
((CustomSX1262 *)_radio)->setSpreadingFactor(_wd_sf);
|
||||
((CustomSX1262 *)_radio)->setBandwidth(_wd_bw);
|
||||
((CustomSX1262 *)_radio)->setCodingRate(_wd_cr);
|
||||
updatePreamble(_wd_sf);
|
||||
}
|
||||
_radio->setOutputPower(getTxPower());
|
||||
// Unconditional: std_init() may have just turned boosted gain back ON via
|
||||
// the board's SX126X_RX_BOOSTED_GAIN compile default, so the OFF case
|
||||
// needs reapplying just as much as ON.
|
||||
((CustomSX1262 *)_radio)->setRxBoostedGainMode(_wd_rx_boosted_gain);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
#define NUM_NOISE_FLOOR_SAMPLES 64
|
||||
#define SAMPLING_THRESHOLD 14
|
||||
|
||||
#define RXPS_WATCHDOG_THRESHOLD_MS 60000UL // no BUSY transition for this long => stuck
|
||||
#define NF_CALIB_INTERVAL_MS 60000UL // recalibrate at least once a minute
|
||||
#define NF_CALIB_TIMEOUT_MS 5000UL // give up on the window (busy channel)
|
||||
|
||||
static volatile uint8_t state = STATE_IDLE;
|
||||
|
||||
// this function is called when a complete packet
|
||||
@@ -84,17 +88,94 @@ void RadioLibWrapper::resetAGC() {
|
||||
_floor_sample_sum = 0;
|
||||
}
|
||||
|
||||
// Detects a stuck RX duty-cycle sequencer: a healthy chip shows the hardware
|
||||
// BUSY pin toggling as it moves through its own RX<->sleep cycle. If that
|
||||
// stops for too long, first try a cheap soft re-arm; if it's still stuck next
|
||||
// time, escalate to a full chip reset (radioHardReset()).
|
||||
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, and recvRaw()
|
||||
// will consume it and re-arm (re-basing the watchdog) on its own.
|
||||
if ((state & STATE_INT_READY) != 0 || (state & ~STATE_INT_READY) == STATE_TX_WAIT) return;
|
||||
|
||||
uint32_t now = millis();
|
||||
bool busy = isChipBusy();
|
||||
if (busy != _wd_last_busy) {
|
||||
_wd_last_busy = busy;
|
||||
_wd_last_transition_ms = now;
|
||||
_wd_stage = 0; // proof of life -- any prior stall is over
|
||||
return;
|
||||
}
|
||||
|
||||
if (now - _wd_last_transition_ms <= RXPS_WATCHDOG_THRESHOLD_MS) return;
|
||||
|
||||
_wd_last_transition_ms = now; // grace period before the next escalation
|
||||
|
||||
if (_wd_stage == 0) {
|
||||
_wd_stage = 1;
|
||||
_wd_soft_count++;
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: RX duty-cycle watchdog: stuck, soft re-arm");
|
||||
state = STATE_IDLE; // next recvRaw()/loop() re-arms via armRecv()
|
||||
} else {
|
||||
_wd_hard_count++;
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: RX duty-cycle watchdog: still stuck, hard reset");
|
||||
radioHardReset();
|
||||
_wd_stage = 0; // chip is freshly (re)initialized either way -- observe again from scratch
|
||||
state = STATE_IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
void RadioLibWrapper::reattachRecvAction() {
|
||||
_radio->setPacketReceivedAction(setFlag);
|
||||
}
|
||||
|
||||
// Checks whether a periodic noise-floor recalibration window is due while RX
|
||||
// duty-cycle power-save is active (see the field comments in the header).
|
||||
// Only starts one when it's safe to interrupt: not mid-transmit, no unread
|
||||
// packet waiting, and no reception currently in progress.
|
||||
void RadioLibWrapper::noiseFloorCalibCheck() {
|
||||
if ((state & STATE_INT_READY) != 0 || (state & ~STATE_INT_READY) == STATE_TX_WAIT) return;
|
||||
if (isReceivingPacket()) return;
|
||||
|
||||
uint32_t now = millis();
|
||||
if (_nf_last_calib_ms != 0 && now - _nf_last_calib_ms < NF_CALIB_INTERVAL_MS) return;
|
||||
|
||||
_nf_calib_active = true;
|
||||
_nf_calib_deadline_ms = now + NF_CALIB_TIMEOUT_MS;
|
||||
_num_floor_samples = 0; // start a fresh batch for this window
|
||||
_floor_sample_sum = 0;
|
||||
state = STATE_IDLE; // next loop() re-arms into continuous RX (see armRecv())
|
||||
}
|
||||
|
||||
void RadioLibWrapper::loop() {
|
||||
// Power-save toggled vs the currently-armed RX mode: re-arm into the other mode
|
||||
// once the radio is idle (don't interrupt an in-flight TX or an unread RX-done).
|
||||
if (_power_save != _ps_active) {
|
||||
// Power-save vs the currently-armed RX mode (toggled by the user, or by a
|
||||
// noise-floor recalibration window borrowing continuous RX for a moment):
|
||||
// re-arm into the wanted mode once the radio is idle (don't interrupt an
|
||||
// in-flight TX or an unread RX-done).
|
||||
bool want_duty_cycle = _power_save && !_nf_calib_active;
|
||||
if (want_duty_cycle != _ps_active) {
|
||||
if (state != STATE_TX_WAIT && !(state & STATE_INT_READY) && !isReceivingPacket()) armRecv();
|
||||
return;
|
||||
}
|
||||
// In power-save the SX126x hardware duty-cycles RX on its own — nothing to poll
|
||||
// here, and noise-floor sampling (used only by the disabled interference check)
|
||||
// would read a chip that is asleep most of the time.
|
||||
if (_power_save) return;
|
||||
|
||||
if (want_duty_cycle) {
|
||||
// Steady-state duty-cycle: the chip cycles RX<->sleep on its own, nothing
|
||||
// to poll here. Just watch for a stuck sequencer and check whether a
|
||||
// recalibration window is due (noiseFloorCalibCheck() flips
|
||||
// _nf_calib_active, which the branch above then re-arms out of).
|
||||
if (supportsRxPsWatchdog()) rxPsWatchdogCheck();
|
||||
noiseFloorCalibCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_nf_calib_active && !isReceivingPacket() && (int32_t)(millis() - _nf_calib_deadline_ms) >= 0) {
|
||||
// Batch couldn't complete in time (busy channel) -- give up, keep the
|
||||
// previous floor, and let the branch above re-arm the duty-cycle.
|
||||
_nf_calib_active = false;
|
||||
_nf_last_calib_ms = millis();
|
||||
state = STATE_IDLE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) {
|
||||
if (!isReceivingPacket()) {
|
||||
@@ -112,6 +193,11 @@ void RadioLibWrapper::loop() {
|
||||
_floor_sample_sum = 0;
|
||||
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor);
|
||||
|
||||
if (_nf_calib_active) {
|
||||
_nf_calib_active = false; // fresh floor published -- back to duty-cycle
|
||||
_nf_last_calib_ms = millis();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,12 +205,14 @@ void RadioLibWrapper::startRecv() {
|
||||
armRecv();
|
||||
}
|
||||
|
||||
// Arm the receiver. In power-save mode this starts the SX126x hardware RX
|
||||
// duty-cycle (the chip cycles RX↔sleep and latches a preamble on its own);
|
||||
// otherwise a continuous RX. Falls back to continuous RX if the modem doesn't
|
||||
// support duty-cycle (base startPowerSaveRecv() returns UNSUPPORTED).
|
||||
// Arm the receiver. In power-save mode (and outside a noise-floor
|
||||
// recalibration window, which needs a moment of plain continuous RX -- see
|
||||
// noiseFloorCalibCheck()) this starts the SX126x hardware RX duty-cycle (the
|
||||
// chip cycles RX↔sleep and latches a preamble on its own); otherwise a
|
||||
// continuous RX. Falls back to continuous RX if the modem doesn't support
|
||||
// duty-cycle (base startPowerSaveRecv() returns UNSUPPORTED).
|
||||
void RadioLibWrapper::armRecv() {
|
||||
if (_power_save) {
|
||||
if (_power_save && !_nf_calib_active) {
|
||||
int16_t e = startPowerSaveRecv();
|
||||
if (e == RADIOLIB_ERR_NONE) { state = STATE_RX; _ps_active = true; return; }
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: RX duty-cycle unsupported (%d) — power-save off", (int)e);
|
||||
|
||||
@@ -13,6 +13,19 @@ protected:
|
||||
int32_t _floor_sample_sum;
|
||||
uint8_t _preamble_sf;
|
||||
|
||||
// Periodic noise-floor recalibration while RX duty-cycle power-save is
|
||||
// active: the frontend is off for most of a duty cycle, so samples taken
|
||||
// there aren't meaningful and loop() skips them entirely (see loop()) —
|
||||
// meaning _noise_floor would otherwise freeze at whatever it was when
|
||||
// power-save turned on, silently breaking int.thresh interference
|
||||
// detection. Instead, drop to plain continuous RX for one window every
|
||||
// NF_CALIB_INTERVAL_MS, run the normal sampling loop, then re-arm the
|
||||
// duty-cycle once a fresh average is published.
|
||||
bool _nf_calib_active = false;
|
||||
uint32_t _nf_last_calib_ms = 0;
|
||||
uint32_t _nf_calib_deadline_ms = 0; // abort the window if it can't complete (busy channel)
|
||||
void noiseFloorCalibCheck();
|
||||
|
||||
void idle();
|
||||
void startRecv();
|
||||
float packetScoreInt(float snr, int sf, int packet_len);
|
||||
@@ -32,6 +45,34 @@ protected:
|
||||
// back to continuous RX; SX126x overrides with startReceiveDutyCycleAuto().
|
||||
virtual int16_t startPowerSaveRecv() { return RADIOLIB_ERR_UNSUPPORTED; }
|
||||
|
||||
// RX duty-cycle watchdog: the chip's own sequencer cycles RX<->sleep with no
|
||||
// MCU polling, so if it desyncs (a known SX126x failure mode) nothing else
|
||||
// would notice. Healthy operation shows up as the hardware BUSY pin
|
||||
// toggling as the chip moves through its cycle; if that stops for too long,
|
||||
// first try a cheap soft re-arm, then a full chip reset.
|
||||
bool _wd_last_busy = false;
|
||||
uint32_t _wd_last_transition_ms = 0;
|
||||
uint8_t _wd_stage = 0; // 0 = healthy / not yet tried, 1 = soft re-arm already attempted this stall
|
||||
uint32_t _wd_soft_count = 0, _wd_hard_count = 0;
|
||||
void rxPsWatchdogCheck();
|
||||
// Re-attach the packet-received/duty-cycle-done interrupt action. Exposed so
|
||||
// radioHardReset() overrides (a different translation unit) can redo this
|
||||
// binding after a fresh begin(), without duplicating the static ISR here.
|
||||
void reattachRecvAction();
|
||||
|
||||
// Overridden by radios that support the watchdog (SX126x only today, since
|
||||
// it's the only one with a working startPowerSaveRecv()). Default false so
|
||||
// the watchdog never runs where isChipBusy()/radioHardReset() aren't real.
|
||||
virtual bool supportsRxPsWatchdog() const { return false; }
|
||||
// True while the chip can't service SPI (duty-cycle sleep window, or
|
||||
// briefly mid-command) — radios expose this via the hardware BUSY pin.
|
||||
virtual bool isChipBusy() { return false; }
|
||||
// Full chip reset + re-init after a stuck duty-cycle a soft re-arm didn't
|
||||
// clear. Returns false if unsupported (base default: no-op). Implementations
|
||||
// must reapply any runtime radio state a fresh init would reset to compiled
|
||||
// firmware defaults (frequency/bandwidth/SF/CR/TX power/preamble/gain).
|
||||
virtual bool radioHardReset() { return false; }
|
||||
|
||||
public:
|
||||
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _preamble_sf(0) { n_recv = n_sent = 0; }
|
||||
|
||||
@@ -87,7 +128,9 @@ public:
|
||||
uint32_t getPacketsRecv() const { return n_recv; }
|
||||
uint32_t getPacketsRecvErrors() const { return n_recv_errors; }
|
||||
uint32_t getPacketsSent() const { return n_sent; }
|
||||
void resetStats() { n_recv = n_sent = n_recv_errors = 0; }
|
||||
uint32_t getRxPsWatchdogSoftCount() const { return _wd_soft_count; }
|
||||
uint32_t getRxPsWatchdogHardCount() const { return _wd_hard_count; }
|
||||
void resetStats() { n_recv = n_sent = n_recv_errors = 0; _wd_soft_count = _wd_hard_count = 0; }
|
||||
|
||||
virtual float getLastRSSI() const override;
|
||||
virtual float getLastSNR() const override;
|
||||
|
||||
@@ -703,7 +703,7 @@ const char* EnvironmentSensorManager::getSettingValue(int i) const {
|
||||
int settings = 0;
|
||||
#if ENV_INCLUDE_GPS
|
||||
if (gps_detected && i == settings++) {
|
||||
return gps_active ? "1" : "0";
|
||||
return gps_master_enabled ? "1" : "0";
|
||||
}
|
||||
#endif
|
||||
return NULL;
|
||||
@@ -712,16 +712,19 @@ const char* EnvironmentSensorManager::getSettingValue(int i) const {
|
||||
bool EnvironmentSensorManager::setSettingValue(const char* name, const char* value) {
|
||||
#if ENV_INCLUDE_GPS
|
||||
if (gps_detected && strcmp(name, "gps") == 0) {
|
||||
if (strcmp(value, "0") == 0) {
|
||||
stop_gps();
|
||||
} else {
|
||||
gps_master_enabled = strcmp(value, "0") != 0;
|
||||
if (gps_master_enabled) {
|
||||
start_gps();
|
||||
} else {
|
||||
stop_gps();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (strcmp(name, "gps_interval") == 0) {
|
||||
uint32_t interval_seconds = atoi(value);
|
||||
gps_update_interval_sec = interval_seconds > 0 ? interval_seconds : 1;
|
||||
// Now the GPS duty-cycle sleep window in seconds (0 = disabled) rather
|
||||
// than a read/snapshot cadence -- same setting key, so the companion
|
||||
// app / CLI (MyMesh.cpp's "gps_interval" frame handler) needs no change.
|
||||
gps_duty_sleep_sec = (uint32_t)atoi(value);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -761,6 +764,7 @@ void EnvironmentSensorManager::initBasicGPS() {
|
||||
MESH_DEBUG_PRINTLN("GPS detected");
|
||||
#ifdef PERSISTANT_GPS
|
||||
gps_active = true;
|
||||
gps_master_enabled = true;
|
||||
return;
|
||||
#endif
|
||||
} else {
|
||||
@@ -768,6 +772,7 @@ void EnvironmentSensorManager::initBasicGPS() {
|
||||
}
|
||||
_location->stop();
|
||||
gps_active = false; //Set GPS visibility off until setting is changed
|
||||
gps_master_enabled = false;
|
||||
}
|
||||
|
||||
// gps code for rak might be moved to MicroNMEALoactionProvider
|
||||
@@ -793,6 +798,7 @@ void EnvironmentSensorManager::rakGPSInit(){
|
||||
else{
|
||||
MESH_DEBUG_PRINTLN("No GPS found");
|
||||
gps_active = false;
|
||||
gps_master_enabled = false;
|
||||
gps_detected = false;
|
||||
Serial1.end();
|
||||
return;
|
||||
@@ -802,6 +808,12 @@ void EnvironmentSensorManager::rakGPSInit(){
|
||||
//Now that GPS is found and set up, set to sleep for initial state
|
||||
stop_gps();
|
||||
#endif
|
||||
// Mirror whatever gps_active ended up as above into the master on/off
|
||||
// intent -- true if FORCE_GPS_ALIVE kept it running, false if stop_gps()
|
||||
// just parked it. Without this the duty-cycle scheduler (which only obeys
|
||||
// gps_master_enabled) would immediately turn GPS back on next tick and
|
||||
// defeat the initial-sleep intent of the stop_gps() call above.
|
||||
gps_master_enabled = gps_active;
|
||||
}
|
||||
|
||||
bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
|
||||
@@ -852,6 +864,56 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
|
||||
}
|
||||
#endif
|
||||
|
||||
// Max time to wait for a fix per wake before giving up and going back to
|
||||
// sleep anyway -- an uncapped wait would defeat duty-cycling entirely
|
||||
// indoors / under canopy / in a canyon, where a fix may never come.
|
||||
#define GPS_DUTY_ACQUIRE_TIMEOUT_MS 60000UL
|
||||
|
||||
// Cycles GPS power between a sleep phase and a wake-and-acquire phase when
|
||||
// duty-cycling is enabled (gps_duty_sleep_sec > 0) and nothing is holding it
|
||||
// continuously awake. Skipped entirely -- and GPS forced back on if it
|
||||
// happened to be asleep -- while the master toggle is off, duty-cycling is
|
||||
// disabled, or a live consumer is holding _gps_keep_awake, so all three of
|
||||
// those states behave exactly like today's always-on GPS.
|
||||
void EnvironmentSensorManager::gpsDutyCycleLoop() {
|
||||
if (!gps_detected || !gps_master_enabled) return;
|
||||
|
||||
if (gps_duty_sleep_sec == 0 || _gps_keep_awake) {
|
||||
if (!gps_active) { start_gps(); _gps_just_woke = true; }
|
||||
_gps_duty_phase_until = 0; // no scheduler-owned phase while bypassed --
|
||||
// re-armed fresh below whenever this resumes
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t now = millis();
|
||||
if (!gps_active) {
|
||||
// sleeping -- wake up once the sleep window has elapsed
|
||||
if (_gps_duty_phase_until == 0 || (int32_t)(now - _gps_duty_phase_until) >= 0) {
|
||||
start_gps();
|
||||
_gps_just_woke = true;
|
||||
_gps_duty_phase_until = now + GPS_DUTY_ACQUIRE_TIMEOUT_MS;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// GPS is on, but not because we just armed an acquire phase for it --
|
||||
// either boot-time start via applyGpsPrefs(), or duty-cycling just resumed
|
||||
// after a keep-awake hold. Start a fresh acquire window instead of treating
|
||||
// the unset timer as already-expired, which would stop_gps() before a cold
|
||||
// fix ever had a chance.
|
||||
if (_gps_duty_phase_until == 0) {
|
||||
_gps_duty_phase_until = now + GPS_DUTY_ACQUIRE_TIMEOUT_MS;
|
||||
return;
|
||||
}
|
||||
|
||||
// awake -- back to sleep once we have a fix, or once we've waited long
|
||||
// enough that one isn't coming this cycle
|
||||
if (_location->isValid() || (int32_t)(now - _gps_duty_phase_until) >= 0) {
|
||||
stop_gps();
|
||||
_gps_duty_phase_until = now + gps_duty_sleep_sec * 1000UL;
|
||||
}
|
||||
}
|
||||
|
||||
void EnvironmentSensorManager::start_gps() {
|
||||
gps_active = true;
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
@@ -888,6 +950,8 @@ void EnvironmentSensorManager::stop_gps() {
|
||||
void EnvironmentSensorManager::loop() {
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
gpsDutyCycleLoop();
|
||||
|
||||
static long next_gps_update = 0;
|
||||
if (gps_active) {
|
||||
_location->loop();
|
||||
|
||||
@@ -20,11 +20,23 @@ protected:
|
||||
uint8_t next_available_channel = TELEM_CHANNEL_SELF + 1;
|
||||
|
||||
bool gps_detected = false;
|
||||
bool gps_active = false;
|
||||
bool gps_active = false; // physically powered on right now
|
||||
uint32_t gps_update_interval_sec = 1;
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
LocationProvider* _location;
|
||||
bool gps_master_enabled = false; // user's on/off intent (Settings/bot/CLI) -- distinct
|
||||
// from gps_active, which duty-cycling now flips on its own
|
||||
// Duty-cycling: while enabled, GPS sleeps for gps_duty_sleep_sec between
|
||||
// acquisitions instead of running continuously. 0 = disabled (today's
|
||||
// always-on behaviour). Skipped entirely while _gps_keep_awake is held by
|
||||
// a live consumer (background trail/live-share/locator, or a screen that
|
||||
// needs a fresh reading right now) -- see setGpsKeepAwake().
|
||||
uint32_t gps_duty_sleep_sec = 0;
|
||||
bool _gps_keep_awake = false;
|
||||
uint32_t _gps_duty_phase_until = 0;
|
||||
bool _gps_just_woke = false; // one-shot; consumed by UITask to reset locator state
|
||||
void gpsDutyCycleLoop();
|
||||
void start_gps();
|
||||
void stop_gps();
|
||||
void initBasicGPS();
|
||||
@@ -38,6 +50,11 @@ public:
|
||||
#if ENV_INCLUDE_GPS
|
||||
EnvironmentSensorManager(LocationProvider &location): _location(&location){};
|
||||
LocationProvider* getLocationProvider() { return _location; }
|
||||
void setGpsKeepAwake(bool on) override { _gps_keep_awake = on; }
|
||||
bool consumeGpsWakeEvent() override { bool w = _gps_just_woke; _gps_just_woke = false; return w; }
|
||||
bool isGpsDutySleeping() const override {
|
||||
return gps_master_enabled && gps_duty_sleep_sec > 0 && !_gps_keep_awake && !gps_active;
|
||||
}
|
||||
#else
|
||||
EnvironmentSensorManager(){};
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user