diff --git a/build-iotthinks.sh b/build-iotthinks.sh index 5c432914..49382199 100644 --- a/build-iotthinks.sh +++ b/build-iotthinks.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # ./build-iotthinks.sh -export FIRMWARE_VERSION="PowerSaving17" +export FIRMWARE_VERSION="PowerSaving17.1" ############# Repeaters ############# # Commonly-used boards diff --git a/docs/cli_commands.md b/docs/cli_commands.md index b81b3323..99d577e8 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1094,6 +1094,18 @@ Companion firmware defaults this setting to `on`. Full Companion accepts the com --- +#### View or set the reboot interval (Repeater and room server) +**Usage:** +- `get reboot.interval` +- `set reboot.interval ` + +**Parameters:** +- `hours`: `0-255`; `0` disables scheduled reboots. + +**Default:** `0` (disabled) + +--- + #### Control an exposed GPIO **Availability:** ESP32 Repeater, Room Server, Bridge, and Sensor firmware. Companion firmware does not expose these commands. On nRF52, the commands are enabled only for Sensor builds on the Heltec T096, ProMicro, RAK3401, and RAK4631. GPIO expanders are not supported. @@ -1129,6 +1141,25 @@ The pin number is the Arduino pin number used by that target (the normal GPIO nu ### Routing +#### View or set the direct path override for the current remote client +**Usage:** +- `get outpath` +- `set outpath ` +- `set outpath direct` +- `set outpath clear` +- `set outpath flood` + +**Parameters:** +- `hopN_hex`: Hop hash with `2`, `4`, or `6` hexadecimal characters. Every hop must use the same width. + +**Notes:** +- These commands require remote client context and update the caller's ACL entry. +- `direct` selects a zero-hop route for a directly reachable caller. +- `clear` forgets the override so normal path discovery can repopulate it. +- `flood` forces replies to use flood packets until the client logs in again. + +--- + #### View or change this node's repeat flag **Usage:** - `get repeat` diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index bab0ef8f..34d1f3f6 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -114,6 +114,8 @@ class HomeScreen : public UIScreen { CompanionNodePrefs* _node_prefs; uint8_t _page; bool _shutdown_init; + uint32_t _uptime_last_millis; + uint64_t _uptime_millis; AdvertPath recent[UI_RECENT_LIST_SIZE]; @@ -138,6 +140,31 @@ class HomeScreen : public UIScreen { int iconY = 0; display.setColor(UIColor::title_txt); + // Track uptime across the 32-bit millis() rollover and show the two most + // useful units beside the battery icon. + uint32_t now = millis(); + _uptime_millis += (uint32_t)(now - _uptime_last_millis); + _uptime_last_millis = now; + + uint64_t uptime_minutes = _uptime_millis / 60000ULL; + unsigned long days = (unsigned long)(uptime_minutes / 1440ULL); + unsigned long hours = (unsigned long)((uptime_minutes % 1440ULL) / 60ULL); + unsigned long minutes = (unsigned long)(uptime_minutes % 60ULL); + char uptime[16]; + if (days > 0) { + snprintf(uptime, sizeof(uptime), "%lud %luh", days, hours); + } else if (hours > 0) { + snprintf(uptime, sizeof(uptime), "%luh %lum", hours, minutes); + } else { + snprintf(uptime, sizeof(uptime), "%lum", minutes); + } + + display.setTextSize(1); + int uptimeWidth = display.getTextWidth(uptime); + int spaceWidth = display.getTextWidth(" "); + display.setCursor(iconX - uptimeWidth - spaceWidth, iconY); + display.print(uptime); + // battery outline display.drawRect(iconX, iconY, iconWidth, iconHeight); @@ -187,7 +214,8 @@ class HomeScreen : public UIScreen { public: HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, CompanionNodePrefs* node_prefs) : _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), - _shutdown_init(false), sensors_lpp(200) { } + _shutdown_init(false), _uptime_last_millis(millis()), _uptime_millis(0), + sensors_lpp(200) { } void showFirstPage() { _page = HomePage::FIRST; } diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index a3ef2b45..9b48d4f8 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -329,6 +329,7 @@ protected: #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { + sensors.setPowerSavingEnabled(_prefs.powersaving_enabled != 0); sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0"); } #endif diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 658834d8..630b7e01 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -224,6 +224,7 @@ private: #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { + sensors.setPowerSavingEnabled(_prefs.powersaving_enabled != 0); sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0"); } #endif diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index c2d3e12f..ff5a5d5a 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -86,6 +86,11 @@ protected: } int startReceiveMode() override { + // Do not abort a frame that started while a calibration transition was + // being scheduled. recvRaw() will move it to continuous RX afterward. + if (_nf_calib_active && _rx_ps_armed && isPacketPendingOrReceiving()) { + return RADIOLIB_ERR_NONE; + } if (_rx_ps_armed) { // stop the previous duty-cycle sequence with an explicit standby before // reconfiguring the radio diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 8ce3a03d..b02490d9 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -99,6 +99,11 @@ public: protected: int startReceiveMode() override { + // Do not abort a frame that started while a calibration transition was + // being scheduled. recvRaw() will move it to continuous RX afterward. + if (_nf_calib_active && _rx_ps_armed && isPacketPendingOrReceiving()) { + return RADIOLIB_ERR_NONE; + } 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 diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index bff90b45..d520a68d 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -235,25 +235,16 @@ void RadioLibWrapper::doResetAGC() { void RadioLibWrapper::resetAGC() { // make sure we're not mid-receiving and mid-sending of packet! - if ((state & STATE_INT_READY) != 0 || isReceivingPacket() || (state == STATE_TX_WAIT)) return; + if (isPacketPendingOrReceiving() || (state == STATE_TX_WAIT)) return; doResetAGC(); state = STATE_IDLE; // trigger a startReceive() if (_rx_boosted_gain_valid) applyRxBoostedGainMode(_cur_rx_boosted_gain); - // Reset noise floor sampling so it reconverges from scratch. - // Without this, a stuck _noise_floor of -120 makes the sampling threshold - // too low (-106) to accept normal samples (~-105), self-reinforcing the - // stuck value even after the receiver has recovered. - _noise_floor = 0; - _noise_floor_valid = false; - _nf_calib_active = false; - _nf_refresh_requested = true; - _nf_last_calib = 0; - _nf_sample_from = 0; - _num_floor_samples = 0; - _floor_sample_sum = 0; - _nf_calib_deadline = 0; // starts after reset recovery reaches RX + // Preserve the last published value while a fresh post-reset batch is + // collected. Invalidating the sample gate lets the new floor converge even + // when the old value was stuck at its lower clamp. + recalibrateNoiseFloor(); } bool RadioLibWrapper::recoverRadio(bool hard) { @@ -371,6 +362,20 @@ void RadioLibWrapper::rxPsWatchdogCheck() { // sleep windows and settling right after each wake), so a requested refresh // drops receive mode to plain continuous RX, collects a fresh sample batch, // and re-arms the duty cycle. Requests are rate-limited and coalesced. +void RadioLibWrapper::requestRestartRecv() { + // An RX interrupt can arrive between the caller's idle check and this state + // transition. Preserve that flag so the completed packet is still consumed. + noInterrupts(); + if ((state & ~STATE_INT_READY) != STATE_TX_WAIT) { + state &= STATE_INT_READY; + } + interrupts(); +} + +bool RadioLibWrapper::isPacketPendingOrReceiving() { + return (state & STATE_INT_READY) != 0 || isReceivingPacket(); +} + void RadioLibWrapper::noiseFloorCalibCheck(unsigned long now) { if (_nf_calib_active) { if (!_rx_ps_enabled @@ -379,11 +384,15 @@ void RadioLibWrapper::noiseFloorCalibCheck(unsigned long now) { // 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_armed && !isPacketPendingOrReceiving()) { + // A packet can delay the transition from RXPS to continuous RX. Retry + // once the radio is idle without discarding a pending interrupt. + requestRestartRecv(); } } else if (_nf_refresh_requested && _rx_ps_enabled && _rx_ps_armed && state == STATE_RX && ((!_noise_floor_valid && _nf_last_calib == 0) || now - _nf_last_calib >= NF_CALIB_INTERVAL_MS) - && !isReceivingPacket()) { + && !isPacketPendingOrReceiving()) { // 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; @@ -391,8 +400,9 @@ void RadioLibWrapper::noiseFloorCalibCheck(unsigned long now) { _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 + if (!isPacketPendingOrReceiving()) { + requestRestartRecv(); // startReceiveMode() selects continuous RX + } } } @@ -404,8 +414,9 @@ void RadioLibWrapper::endNoiseFloorCalib(unsigned long 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; + if ((state & ~STATE_INT_READY) != STATE_TX_WAIT + && !isPacketPendingOrReceiving()) { + requestRestartRecv(); } } diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 90810456..f06be5ee 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -81,6 +81,8 @@ protected: void startRecv() override; void rxPsWatchdogCheck(); void requestNoiseFloorRefresh(); + void requestRestartRecv(); + bool isPacketPendingOrReceiving(); void noiseFloorCalibCheck(unsigned long now); void endNoiseFloorCalib(unsigned long now); void finishReceiveProcessing(); diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index f013e1ad..da1d39f0 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -426,16 +426,16 @@ bool ST7735Display::i2c_probe(TwoWire& wire, uint8_t addr) { #define PIN_TFT_LEDA_CTL_ACTIVE HIGH #endif -// Color scheme -ColorVal UIColor::window_bkg = ST77XX_WHITE; -ColorVal UIColor::title_bkg = ST77XX_BLUE; -ColorVal UIColor::title_txt = ST77XX_WHITE; -ColorVal UIColor::primary_txt = ST77XX_BLACK; -ColorVal UIColor::secondary_txt = (18 << 11) | (36 << 5) | 18; // mid-gray -ColorVal UIColor::warning_txt = ST77XX_ORANGE; -ColorVal UIColor::popup_bkg = ST77XX_CYAN; -ColorVal UIColor::popup_txt = ST77XX_BLACK; -ColorVal UIColor::corp_blue = 0x001A; +// Dark colors reduce panel current on the PowerSaving builds. +ColorVal UIColor::window_bkg = ST77XX_BLACK; +ColorVal UIColor::title_bkg = ST77XX_BLACK; +ColorVal UIColor::title_txt = ST77XX_GREEN; +ColorVal UIColor::primary_txt = ST77XX_GREEN; +ColorVal UIColor::secondary_txt = ST77XX_YELLOW; +ColorVal UIColor::warning_txt = ST77XX_RED; +ColorVal UIColor::popup_bkg = ST77XX_BLACK; +ColorVal UIColor::popup_txt = ST77XX_WHITE; +ColorVal UIColor::corp_blue = ST77XX_BLUE; bool ST7735Display::begin() { if (!sprite) {