Expose RX power saving in WebConfig

This commit is contained in:
mikecarper
2026-08-15 09:07:41 -07:00
parent 772607eba1
commit 9161dfd7d5
16 changed files with 2113 additions and 1751 deletions
+7
View File
@@ -196,6 +196,13 @@ WiFi companions do not have the repeater/room-server admin CLI password model,
so their LAN WebConfig page is intentionally unauthenticated. Use them only on
a trusted LAN.
On radio chips that support receive duty cycling, the WebConfig **Advanced**
card also exposes RX power saving. Its master switch selects continuous receive
when off or RX/sleep duty cycling when on. Levels 1-10, automatic or explicit
16/32-symbol preambles, and manual receive/sleep windows are persisted across
reboots. This radio setting is separate from whole-device sleep; the WiFi
companion remains awake so its TCP service and configuration page stay reachable.
When `ENABLE_OTA` is included, a WiFi companion also listens on:
- TCP 5001 for the OTA folder seeder used by `motatool serve --tcp`;
+21
View File
@@ -323,6 +323,17 @@ void DataStore::loadPrefsInt(const char *filename, CompanionNodePrefs& _prefs, d
if (file.available() >= (int)sizeof(_prefs.radio_fem_txgain)) {
file.read((uint8_t *)&_prefs.radio_fem_txgain, sizeof(_prefs.radio_fem_txgain)); // 125
}
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 (file.available() >= (int)rxps_tail_size) {
file.read((uint8_t *)&_prefs.rx_powersaving_enabled,
sizeof(_prefs.rx_powersaving_enabled)); // 126
file.read((uint8_t *)&_prefs.rx_ps_rx_us, sizeof(_prefs.rx_ps_rx_us)); // 127
file.read((uint8_t *)&_prefs.rx_ps_sleep_us, sizeof(_prefs.rx_ps_sleep_us)); // 131
file.read((uint8_t *)&_prefs.rx_ps_level, sizeof(_prefs.rx_ps_level)); // 135
file.read((uint8_t *)&_prefs.rx_ps_preamble, sizeof(_prefs.rx_ps_preamble)); // 136
}
file.close();
}
@@ -374,6 +385,16 @@ bool DataStore::savePrefs(const CompanionNodePrefs& _prefs, double node_lat, dou
sizeof(_prefs.vibe_quiet)) == sizeof(_prefs.vibe_quiet); // 124
success = success && file.write((uint8_t *)&_prefs.radio_fem_txgain,
sizeof(_prefs.radio_fem_txgain)) == sizeof(_prefs.radio_fem_txgain); // 125
success = success && file.write((uint8_t *)&_prefs.rx_powersaving_enabled,
sizeof(_prefs.rx_powersaving_enabled)) == sizeof(_prefs.rx_powersaving_enabled); // 126
success = success && file.write((uint8_t *)&_prefs.rx_ps_rx_us,
sizeof(_prefs.rx_ps_rx_us)) == sizeof(_prefs.rx_ps_rx_us); // 127
success = success && file.write((uint8_t *)&_prefs.rx_ps_sleep_us,
sizeof(_prefs.rx_ps_sleep_us)) == sizeof(_prefs.rx_ps_sleep_us); // 131
success = success && file.write((uint8_t *)&_prefs.rx_ps_level,
sizeof(_prefs.rx_ps_level)) == sizeof(_prefs.rx_ps_level); // 135
success = success && file.write((uint8_t *)&_prefs.rx_ps_preamble,
sizeof(_prefs.rx_ps_preamble)) == sizeof(_prefs.rx_ps_preamble); // 136
#if defined(NRF52_PLATFORM)
success = file.commit(success);
+155 -31
View File
@@ -376,32 +376,6 @@ int MyMesh::getInterferenceThreshold() const {
return 0; // disabled for now, until currentRSSI() problem is resolved
}
#if RXPS_FIXED_ENABLED
static mesh::RadioParamApplyResult applyFixedRadioParams(float freq, float bw, uint8_t sf, uint8_t cr) {
uint32_t rx_us, sleep_us;
if (!calcRxPowerSavingLevel(RXPS_FIXED_LEVEL, sf, bw, RXPS_FIXED_PREAMBLE, &rx_us, &sleep_us)) {
POWERSAVING_DEBUG_PRINTLN("RX Power Saving fixed profile invalid");
return mesh::RadioParamApplyResult::FAILED;
}
uint32_t timings[2] = {rx_us, sleep_us};
const bool supports_rxps = radio_driver.supportsRxPowerSaving();
// Keep ordinary radio configuration working on companion targets whose
// radio does not implement RX duty cycling. This matches the former
// setParams()+setRxPowerSaving() behavior while retaining one atomic
// transition on radios that do support it.
mesh::RadioParamApplyResult result =
radio_driver.trySetParams(freq, bw, sf, cr, supports_rxps ? timings : NULL);
POWERSAVING_DEBUG_PRINTLN(
"RX Power Saving fixed level %d p%d: %s (%lu/%lu us)", RXPS_FIXED_LEVEL,
RXPS_FIXED_PREAMBLE,
result == mesh::RadioParamApplyResult::APPLIED
? (supports_rxps ? "Enabled" : "Unsupported")
: (result == mesh::RadioParamApplyResult::BUSY ? "Busy" : "Apply failed"),
(unsigned long)rx_us, (unsigned long)sleep_us);
return result;
}
#endif
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
if (_prefs.rx_delay_base <= 0.0f) return 0;
return (int)((powf(_prefs.rx_delay_base, 0.85f - score) - 1.0f) * air_time);
@@ -1376,6 +1350,14 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_prefs.rx_boosted_gain = mesh::radio::configuredRxBoostedGainDefault();
#endif
_prefs.radio_fem_rxgain = DEFAULT_FEM_RX_GAIN;
_prefs.rx_powersaving_enabled = RXPS_FIXED_ENABLED ? 1 : 0;
_prefs.rx_ps_level = RXPS_FIXED_LEVEL;
_prefs.rx_ps_preamble = RXPS_FIXED_PREAMBLE;
_prefs.rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US;
_prefs.rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;
recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, _prefs.sf, _prefs.bw,
_prefs.rx_ps_preamble, &_prefs.rx_ps_rx_us,
&_prefs.rx_ps_sleep_us);
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
memset(&_mqtt_prefs, 0, sizeof(_mqtt_prefs));
@@ -1454,6 +1436,15 @@ void MyMesh::begin(bool has_display) {
}
_prefs.radio_fem_rxgain = constrain(_prefs.radio_fem_rxgain, 0, 1);
_prefs.radio_fem_txgain = constrain(_prefs.radio_fem_txgain, 0, 1);
_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;
}
ensureRxPowerSavingDefaults(&_prefs.rx_ps_rx_us, &_prefs.rx_ps_sleep_us);
recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, _prefs.sf, _prefs.bw,
_prefs.rx_ps_preamble, &_prefs.rx_ps_rx_us,
&_prefs.rx_ps_sleep_us);
#ifdef BLE_PIN_CODE // 123456 by default
if (_prefs.ble_pin == 0) {
@@ -1545,11 +1536,17 @@ void MyMesh::begin(bool has_display) {
}
mesh::RadioParamApplyResult MyMesh::tryApplyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr) {
#if RXPS_FIXED_ENABLED
return applyFixedRadioParams(freq, bw, sf, cr);
#else
return radio_driver.trySetParams(freq, bw, sf, cr);
#endif
uint32_t rx_us = _prefs.rx_ps_rx_us;
uint32_t sleep_us = _prefs.rx_ps_sleep_us;
if (_prefs.rx_powersaving_enabled && _prefs.rx_ps_level != 0
&& !recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, sf, bw,
_prefs.rx_ps_preamble, &rx_us, &sleep_us)) {
return mesh::RadioParamApplyResult::FAILED;
}
uint32_t timings[2] = {rx_us, sleep_us};
const uint32_t* applied_timings = _prefs.rx_powersaving_enabled
&& radio_driver.supportsRxPowerSaving() ? timings : NULL;
return radio_driver.trySetParams(freq, bw, sf, cr, applied_timings);
}
bool MyMesh::applySavedRadioParams() {
@@ -1563,6 +1560,9 @@ void MyMesh::finishRadioParamApply(float freq, float bw, uint8_t sf, uint8_t cr,
_prefs.freq = freq;
_prefs.bw = bw;
_prefs.client_repeat = repeat;
recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, _prefs.sf, _prefs.bw,
_prefs.rx_ps_preamble, &_prefs.rx_ps_rx_us,
&_prefs.rx_ps_sleep_us);
savePrefs();
saved_radio_apply_pending = false;
@@ -1878,12 +1878,20 @@ void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
s.rx_delay = _prefs.rx_delay_base;
s.rx_gain = _prefs.rx_boosted_gain;
s.fem_rx_gain = board.isLoRaFemLnaEnabled();
s.rx_ps_enabled = _prefs.rx_powersaving_enabled;
s.rx_ps_level = _prefs.rx_ps_level;
s.rx_ps_preamble = _prefs.rx_ps_preamble;
s.rx_ps_rx_us = _prefs.rx_ps_rx_us;
s.rx_ps_sleep_us = _prefs.rx_ps_sleep_us;
s.repeat = _prefs.client_repeat != 0;
s.capabilities = WebConfigServer::CAP_LOCATION | WebConfigServer::CAP_AIRTIME
| WebConfigServer::CAP_RX_DELAY | WebConfigServer::CAP_RX_GAIN;
if (board.canControlLoRaFemLna()) {
s.capabilities |= WebConfigServer::CAP_FEM_RX_GAIN;
}
if (radio_driver.supportsRxPowerSaving()) {
s.capabilities |= WebConfigServer::CAP_RX_POWER_SAVING;
}
}
bool MyMesh::startWebConfig(bool force_ap, char* reply) {
@@ -2008,6 +2016,9 @@ void MyMesh::execCommand(char* cmd, char* reply) {
_prefs.bw = bw;
_prefs.sf = static_cast<uint8_t>(sf);
_prefs.cr = static_cast<uint8_t>(cr);
recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, _prefs.sf, _prefs.bw,
_prefs.rx_ps_preamble, &_prefs.rx_ps_rx_us,
&_prefs.rx_ps_sleep_us);
savePrefs();
strcpy(reply, "OK - reboot required");
}
@@ -2054,6 +2065,10 @@ void MyMesh::execCommand(char* cmd, char* reply) {
}
return;
}
if (strcmp(key, "radio.rxps") == 0) {
applyAndSaveRxPowerSaving(value, reply);
return;
}
if (strcmp(key, "radio.fem.rxgain") == 0) {
bool enabled;
if (!wcParseBool(value, enabled)) {
@@ -3492,6 +3507,115 @@ bool MyMesh::applyAndSaveFemTxGain(bool enabled) {
return true;
}
bool MyMesh::applyAndSaveRxPowerSaving(const char* value, char* reply) {
if (!radio_driver.supportsRxPowerSaving()) {
strcpy(reply, "Error: RX power saving unsupported");
return false;
}
uint8_t enabled = _prefs.rx_powersaving_enabled;
uint8_t level = _prefs.rx_ps_level;
uint8_t preamble = _prefs.rx_ps_preamble;
uint32_t rx_us = _prefs.rx_ps_rx_us;
uint32_t sleep_us = _prefs.rx_ps_sleep_us;
bool level_requested = false;
bool manual_requested = false;
unsigned long parsed_level = 0;
unsigned long parsed_preamble = 0;
unsigned long parsed_rx = 0;
unsigned long parsed_sleep = 0;
char extra = 0;
if (strcmp(value, "off") == 0) {
enabled = 0;
} else if (strcmp(value, "on") == 0 || strcmp(value, "conservative") == 0) {
enabled = 1;
level = RX_POWERSAVING_CONSERVATIVE_LEVEL;
preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
level_requested = true;
} else if (strcmp(value, "balanced") == 0) {
enabled = 1;
level = RX_POWERSAVING_BALANCED_LEVEL;
preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
level_requested = true;
} else if (sscanf(value, "level %lu preamble %lu %c",
&parsed_level, &parsed_preamble, &extra) == 2) {
if (parsed_level < 1 || parsed_level > 10
|| (parsed_preamble != 16 && parsed_preamble != 32)) {
strcpy(reply, "Error: level must be 1-10; preamble must be 16 or 32");
return false;
}
enabled = 1;
level = static_cast<uint8_t>(parsed_level);
preamble = static_cast<uint8_t>(parsed_preamble);
level_requested = true;
} else if (sscanf(value, "level %lu %c", &parsed_level, &extra) == 1
|| sscanf(value, "%lu %c", &parsed_level, &extra) == 1) {
if (parsed_level < 1 || parsed_level > 10) {
strcpy(reply, "Error: level must be 1-10");
return false;
}
enabled = 1;
level = static_cast<uint8_t>(parsed_level);
preamble = 0;
level_requested = true;
} else if (sscanf(value, "%lu %lu %c", &parsed_rx, &parsed_sleep, &extra) == 2) {
if (parsed_rx < RX_POWERSAVING_MIN_PERIOD_US
|| parsed_rx > RX_POWERSAVING_MAX_PERIOD_US
|| parsed_sleep < RX_POWERSAVING_MIN_PERIOD_US
|| parsed_sleep > RX_POWERSAVING_MAX_PERIOD_US) {
snprintf(reply, 160, "Error: RX/SLEEP must be %lu-%lu us",
(unsigned long)RX_POWERSAVING_MIN_PERIOD_US,
(unsigned long)RX_POWERSAVING_MAX_PERIOD_US);
return false;
}
enabled = 1;
rx_us = static_cast<uint32_t>(parsed_rx);
sleep_us = static_cast<uint32_t>(parsed_sleep);
level = 0;
preamble = 0;
manual_requested = true;
} else {
strcpy(reply, "Error: use off, level 1-10, or RX/SLEEP microseconds");
return false;
}
if (level_requested) {
if (level < 1 || level > 10
|| (preamble != 0 && preamble != 16 && preamble != 32)
|| !recalcRxPowerSavingFromLevel(level, _prefs.sf, _prefs.bw, preamble,
&rx_us, &sleep_us)) {
strcpy(reply, "Error: level must be 1-10; preamble must be auto, 16, or 32");
return false;
}
}
if ((manual_requested || enabled)
&& (!isValidRxPowerSavingPeriod(rx_us)
|| !isValidRxPowerSavingPeriod(sleep_us))) {
snprintf(reply, 160, "Error: RX/SLEEP must be %lu-%lu us",
(unsigned long)RX_POWERSAVING_MIN_PERIOD_US,
(unsigned long)RX_POWERSAVING_MAX_PERIOD_US);
return false;
}
if (!radio_driver.setRxPowerSaving(enabled != 0, rx_us, sleep_us)) {
strcpy(reply, "Error: radio busy; retry");
return false;
}
_prefs.rx_powersaving_enabled = enabled;
_prefs.rx_ps_rx_us = rx_us;
_prefs.rx_ps_sleep_us = sleep_us;
_prefs.rx_ps_level = level;
_prefs.rx_ps_preamble = preamble;
savePrefs();
snprintf(reply, 160, "OK - %s,%lu,%lu",
enabled ? "on" : "off", (unsigned long)rx_us,
(unsigned long)sleep_us);
return true;
}
#ifdef ENABLE_USB_INTERFACE
void MyMesh::enterTerminalMode() {
_terminal_mode = true;
+1
View File
@@ -278,6 +278,7 @@ private:
void checkSerialInterface();
bool applyAndSaveFemRxGain(bool enabled);
bool applyAndSaveFemTxGain(bool enabled);
bool applyAndSaveRxPowerSaving(const char* value, char* reply);
#ifdef ENABLE_USB_INTERFACE
ContactInfo* getTerminalRecipient();
void printTerminalPath(const ContactInfo& recipient);
+5
View File
@@ -39,6 +39,11 @@ struct CompanionNodePrefs { // persisted to file
uint8_t radio_fem_rxgain_override; // 1 once the user overrides the build default
uint8_t vibe_quiet; // haptic quiet mode; appended for prefs compatibility
uint8_t radio_fem_txgain; // LoRa FEM TX gain; appended for prefs compatibility
uint8_t rx_powersaving_enabled; // SX126x/LR11xx receive duty cycling
uint32_t rx_ps_rx_us; // receive window in microseconds
uint32_t rx_ps_sleep_us; // sleep window in microseconds
uint8_t rx_ps_level; // 0=manual timings, 1..10=level-derived
uint8_t rx_ps_preamble; // 0=auto from SF, otherwise 16 or 32
// Keep the upstream repeat API while retaining the existing binary prefs
// layout used by this branch.
+8
View File
@@ -9569,6 +9569,11 @@ void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
s.cad = _prefs.cad_enabled;
s.rx_gain = _prefs.rx_boosted_gain;
s.fem_rx_gain = board.isLoRaFemLnaEnabled();
s.rx_ps_enabled = _prefs.rx_powersaving_enabled;
s.rx_ps_level = _prefs.rx_ps_level;
s.rx_ps_preamble = _prefs.rx_ps_preamble;
s.rx_ps_rx_us = _prefs.rx_ps_rx_us;
s.rx_ps_sleep_us = _prefs.rx_ps_sleep_us;
s.repeat = !_prefs.disable_fwd;
s.advert_interval = _prefs.advert_interval * 2;
s.flood_advert_interval = _prefs.flood_advert_interval;
@@ -9584,6 +9589,9 @@ void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
if (board.canControlLoRaFemLna()) {
s.capabilities |= WebConfigServer::CAP_FEM_RX_GAIN;
}
if (radio_driver.supportsRxPowerSaving()) {
s.capabilities |= WebConfigServer::CAP_RX_POWER_SAVING;
}
}
bool MyMesh::startWebConfig(bool force_ap, char* reply) {
+8
View File
@@ -1563,6 +1563,11 @@ void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
s.cad = _prefs.cad_enabled;
s.rx_gain = _prefs.rx_boosted_gain;
s.fem_rx_gain = board.isLoRaFemLnaEnabled();
s.rx_ps_enabled = _prefs.rx_powersaving_enabled;
s.rx_ps_level = _prefs.rx_ps_level;
s.rx_ps_preamble = _prefs.rx_ps_preamble;
s.rx_ps_rx_us = _prefs.rx_ps_rx_us;
s.rx_ps_sleep_us = _prefs.rx_ps_sleep_us;
s.repeat = !_prefs.disable_fwd;
s.advert_interval = _prefs.advert_interval * 2;
s.flood_advert_interval = _prefs.flood_advert_interval;
@@ -1578,6 +1583,9 @@ void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
if (board.canControlLoRaFemLna()) {
s.capabilities |= WebConfigServer::CAP_FEM_RX_GAIN;
}
if (radio_driver.supportsRxPowerSaving()) {
s.capabilities |= WebConfigServer::CAP_RX_POWER_SAVING;
}
}
bool MyMesh::startWebConfig(bool force_ap, char* reply) {
+1
View File
@@ -105,6 +105,7 @@ ROUND_TRIPS = [
("set mqtt.neighbors on", "get mqtt.neighbors", "on"),
("set path.hash.mode 2", "get path.hash.mode", "2"),
("set mqtt.iata den", "get mqtt.iata", "DEN"),
("set radio.rxps 70000 60000", "get radio.rxps", "on,70000,60000"),
# Secret reads are masked back down for an HTTP caller, in CommonCLI's own
# words for a non-serial one (wcIsSecretReadCommand).
("set guest.password hunter2", "get guest.password", "******** (serial only)"),
+57
View File
@@ -30,6 +30,7 @@ Stdlib only; no pip install.
import argparse
import copy
import json
import math
import os
import re
import secrets
@@ -101,6 +102,8 @@ def default_config(setup_mode):
"radio": {
"freq": 910.525, "bw": 62.5, "sf": 7, "cr": 5, "tx": 22, "af": 1.0,
"rxdelay": 0.0, "txdelay": 0.5, "cad": False, "rxgain": True,
"rxps_enabled": True, "rxps_level": 5, "rxps_preamble": 16,
"rxps_rx_us": 20936, "rxps_sleep_us": 13425,
"repeat": True, "flood_max": 64, "flood_max_advert": 8,
"flood_max_unscoped": 8, "loop_detect": "moderate",
"name": "MockNode", "lat": 39.7392, "lon": -104.9903,
@@ -192,6 +195,9 @@ class State:
"role": "Repeater", "board": "Heltec V3 (mock)",
"uptime_s": int(time.time() - self.start),
"runtime_slots": 6, "max_slots": 6, "active_slots": self.active_slots,
# Every ordinary repeater feature except an external FEM, plus
# radio RX power saving (bit 12).
"capabilities": 0x17FF,
"max_cmds": CLI_MAX_CMDS,
}
@@ -238,6 +244,19 @@ def _hex64(v):
return len(v) == 64 and all(c in "0123456789abcdefABCDEF" for c in v)
def _rxps_level_timings(level, sf, bw, preamble):
"""Mirror calcRxPowerSavingLevel() for realistic portal round-trips."""
actual_preamble = preamble or (32 if sf <= 8 else 16)
symbol_us = 1000.0 * (1 << sf) / bw
amount = (level - 1) / 9.0
rx_start = 12.0 if actual_preamble == 16 else 16.0
sleep_start = 2.0 if actual_preamble == 16 else 15.0
rx_symbols = rx_start + amount * (8.0 - rx_start)
sleep_edge = actual_preamble + 4.25 - 8.0
sleep_symbols = sleep_start + amount * (sleep_edge - sleep_start)
return math.ceil(rx_symbols * symbol_us), int(sleep_symbols * symbol_us)
def apply_set(cfg, key, val):
"""Return (ok, reply) and mutate cfg. Mirrors the firmware's validation for
the fields where it matters (length, IATA, owner key, port, radio combo)."""
@@ -256,6 +275,39 @@ def apply_set(cfg, key, val):
if key == "radio.fem.rxgain":
return False, "Error: unsupported" # no FEM on the mock board, see GETTERS
if key == "radio.rxps":
radio = cfg["radio"]
if val == "off":
radio["rxps_enabled"] = False
return True, "OK - off,%d,%d" % (radio["rxps_rx_us"], radio["rxps_sleep_us"])
if val in ("on", "conservative", "balanced"):
level = 5 if val == "balanced" else 1
preamble = 16
else:
match = re.fullmatch(r"(?:level )?(\d+)(?: preamble (\d+))?", val)
if match:
level = int(match.group(1))
preamble = int(match.group(2) or 0)
if not 1 <= level <= 10 or preamble not in (0, 16, 32):
return False, "Error: level must be 1-10; preamble must be auto, 16, or 32"
else:
match = re.fullmatch(r"(\d+) (\d+)", val)
if not match:
return False, "Error: use off, level 1-10, or RX/SLEEP microseconds"
rx_us, sleep_us = map(int, match.groups())
if not (1000 <= rx_us <= 30000000 and 1000 <= sleep_us <= 30000000):
return False, "Error: RX/SLEEP must be 1000-30000000 us"
radio.update(rxps_enabled=True, rxps_level=0, rxps_preamble=0,
rxps_rx_us=rx_us, rxps_sleep_us=sleep_us)
return True, "OK - on,%d,%d" % (rx_us, sleep_us)
rx_us, sleep_us = _rxps_level_timings(
level, radio["sf"], radio["bw"], preamble)
radio.update(rxps_enabled=True, rxps_level=level, rxps_preamble=preamble,
rxps_rx_us=rx_us, rxps_sleep_us=sleep_us)
return True, "OK - on,%d,%d" % (rx_us, sleep_us)
if key == "dutycycle":
try:
dc = float(val)
@@ -514,6 +566,9 @@ GETTERS = {
# compiled out -- the command exists everywhere and the board answers for
# itself. The mock board is a Heltec V3, which has no FEM.
"radio.fem.rxgain": lambda c: None,
"radio.rxps": lambda c: "%s,%d,%d" % (
"on" if c["radio"]["rxps_enabled"] else "off",
c["radio"]["rxps_rx_us"], c["radio"]["rxps_sleep_us"]),
}
@@ -570,6 +625,8 @@ def cli_read_key(cfg, key):
return {
"name": r["name"], "lat": r["lat"], "lon": r["lon"],
"radio": "%.3f,%.2f,%d,%d" % (r["freq"], r["bw"], r["sf"], r["cr"]),
"radio.rxps": "%s,%d,%d" % (
"on" if r["rxps_enabled"] else "off", r["rxps_rx_us"], r["rxps_sleep_us"]),
"bw": r["bw"], "sf": r["sf"], "cr": r["cr"],
"mqtt.iata": cfg["mqtt"]["iata"], "mqtt.owner": cfg["mqtt"]["owner"],
}.get(key)
+1 -1
View File
@@ -19,7 +19,7 @@
static const char* const WC_ALLOWED_SET_KEYS[] = {
// NodePrefs (radio / node)
"name", "lat", "lon", "radio", "tx", "af", "rxdelay", "txdelay",
"cad", "radio.rxgain", "radio.fem.rxgain", "repeat",
"cad", "radio.rxgain", "radio.fem.rxgain", "radio.rxps", "repeat",
"advert.interval", "flood.advert.interval",
"flood.max", "flood.max.advert", "flood.max.unscoped", "loop.detect",
// MQTTPrefs (WiFi / MQTT / misc observer)
File diff suppressed because it is too large Load Diff
+5
View File
@@ -1433,6 +1433,11 @@ void WebConfigServer::handleConfigGet(AsyncWebServerRequest* req) {
radio["cad"] = (bool)node.cad;
radio["rxgain"] = (bool)node.rx_gain;
radio["fem_rxgain"] = (bool)node.fem_rx_gain;
radio["rxps_enabled"] = (bool)node.rx_ps_enabled;
radio["rxps_level"] = node.rx_ps_level;
radio["rxps_preamble"] = node.rx_ps_preamble;
radio["rxps_rx_us"] = node.rx_ps_rx_us;
radio["rxps_sleep_us"] = node.rx_ps_sleep_us;
radio["repeat"] = (bool)node.repeat;
radio["flood_max"] = node.flood_max;
radio["flood_max_advert"] = node.flood_max_advert;
+6
View File
@@ -58,6 +58,7 @@ public:
CAP_TX_DELAY = 1UL << 9,
CAP_WIFI_POWER_SAVE = 1UL << 10,
CAP_FEM_RX_GAIN = 1UL << 11,
CAP_RX_POWER_SAVING = 1UL << 12,
CAP_DELAYS = CAP_RX_DELAY | CAP_TX_DELAY,
};
@@ -80,6 +81,11 @@ public:
uint8_t cad;
uint8_t rx_gain;
uint8_t fem_rx_gain;
uint8_t rx_ps_enabled;
uint8_t rx_ps_level;
uint8_t rx_ps_preamble;
uint32_t rx_ps_rx_us;
uint32_t rx_ps_sleep_us;
uint8_t repeat;
uint16_t advert_interval;
uint8_t flood_advert_interval;
+2 -1
View File
@@ -13,7 +13,8 @@
#define RX_POWERSAVING_BALANCED_LEVEL 5
#define RX_POWERSAVING_PROFILE_PREAMBLE 16
// Fixed settings for companions. Build flags can still override these defaults.
// Initial settings for companions. Build flags can override the defaults;
// roles with runtime RXPS controls persist the operator's selection afterward.
#ifndef RXPS_FIXED_ENABLED
#define RXPS_FIXED_ENABLED 1
#endif
@@ -9,6 +9,7 @@
TEST(WebConfigKeys, AllowsKnownScalarKeys) {
EXPECT_TRUE(wcIsAllowedSetKey("name"));
EXPECT_TRUE(wcIsAllowedSetKey("radio"));
EXPECT_TRUE(wcIsAllowedSetKey("radio.rxps"));
EXPECT_TRUE(wcIsAllowedSetKey("repeat"));
EXPECT_TRUE(wcIsAllowedSetKey("wifi.ssid"));
EXPECT_TRUE(wcIsAllowedSetKey("mqtt.iata"));
+59 -1
View File
@@ -387,6 +387,22 @@ body.tab-cli{padding-bottom:0}
<span class="tgl"><input type="checkbox" data-k="radio.rxgain"><u></u></span></div>
<div class="sw" data-cap="2048"><span><b>FEM RX boost</b><i>External front-end-module LNA</i></span>
<span class="tgl"><input type="checkbox" data-k="radio.fem.rxgain"><u></u></span></div>
<div data-cap="4096" id="rxps-wrap">
<div class="sw"><span><b>RX power saving</b><i>Duty-cycle the LoRa receiver; off keeps continuous receive</i></span>
<span class="tgl"><input type="checkbox" data-rxps="enabled"><u></u></span></div>
<div class="row" id="rxps-options">
<div class="f"><label>RXPS level</label>
<select data-rxps="level"><option value="1">1 (conservative)</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5 (balanced)</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option><option value="manual">Manual timings</option></select>
<input type="hidden" data-k="radio.rxps"></div>
<div class="f" id="rxps-preamble-field"><label>Preamble</label>
<select data-rxps="preamble"><option value="0">Auto from spreading factor</option><option value="16">16 symbols</option><option value="32">32 symbols</option></select></div>
</div>
<div class="row hide" id="rxps-manual">
<div class="f"><label>Receive window (&micro;s)</label><input type="number" data-rxps="rx" min="1000" max="30000000" step="1"></div>
<div class="f"><label>Sleep window (&micro;s)</label><input type="number" data-rxps="sleep" min="1000" max="30000000" step="1"></div>
</div>
<div class="hint" id="rxps-hint"></div>
</div>
<div class="sw" data-cap="32"><span><b>Repeat</b><i>Forward mesh traffic. Off = listen-only (still observes and publishes).</i></span>
<span class="tgl"><input type="checkbox" data-k="repeat"><u></u></span></div>
<div class="row" style="margin-top:6px" data-cap="64">
@@ -721,6 +737,7 @@ function cfgVal(k){ // map a `set` key to its current value string, from st.cfg
case"rxdelay":return String(r.rxdelay);case"txdelay":return String(r.txdelay);
case"cad":return r.cad?"on":"off";case"radio.rxgain":return r.rxgain?"on":"off";
case"radio.fem.rxgain":return r.fem_rxgain?"on":"off";
case"radio.rxps":return rxpsCfgValue(r);
case"repeat":return r.repeat?"on":"off";
case"flood.max":return String(r.flood_max);
case"flood.max.advert":return String(r.flood_max_advert);
@@ -750,6 +767,43 @@ function setEl(el,v){
if(el.type==="checkbox")el.checked=(v==="on");
else el.value=v;
}
function rxpsCfgValue(r){
if(!r.rxps_enabled)return"off";
var level=Number(r.rxps_level)||0,preamble=Number(r.rxps_preamble)||0;
if(level>=1&&level<=10)return"level "+level+((preamble===16||preamble===32)?" preamble "+preamble:"");
return String(r.rxps_rx_us||65625)+" "+String(r.rxps_sleep_us||60000);
}
function loadRxps(r){
var enabled=$('[data-rxps="enabled"]'),level=$('[data-rxps="level"]');
if(!enabled||!level)return;
enabled.checked=!!r.rxps_enabled;
var n=Number(r.rxps_level)||0;level.value=n>=1&&n<=10?String(n):"manual";
$('[data-rxps="preamble"]').value=String(r.rxps_preamble===16||r.rxps_preamble===32?r.rxps_preamble:0);
$('[data-rxps="rx"]').value=String(r.rxps_rx_us||65625);
$('[data-rxps="sleep"]').value=String(r.rxps_sleep_us||60000);
refreshRxps(false);
}
function refreshRxps(mark){
var enabled=$('[data-rxps="enabled"]').checked,level=$('[data-rxps="level"]').value;
var manual=level==="manual",preamble=$('[data-rxps="preamble"]').value;
$("#rxps-manual").classList.toggle("hide",!manual);
$("#rxps-preamble-field").classList.toggle("hide",manual);
var value="off";
if(enabled){
value=manual
? $('[data-rxps="rx"]').value+" "+$('[data-rxps="sleep"]').value
: "level "+level+(preamble!=="0"?" preamble "+preamble:"");
}
var hidden=$('[data-k="radio.rxps"]');hidden.value=value;
var dirty=mark&&value!==st.orig["radio.rxps"];
$$('[data-rxps]').forEach(function(el){el.classList.toggle("dirty",dirty)});
if(mark)markDirty("radio.rxps",value,hidden);
$("#rxps-hint").textContent=!enabled
? "Off: the LoRa receiver listens continuously."
: manual
? "Custom receive/sleep windows are applied immediately and retained after reboot."
: "Level timing is recalculated for the selected SF and bandwidth when saved.";
}
function radioCombo(){
var p={};$$("[data-rg]").forEach(function(el){p[el.dataset.rg]=el.value});
return p.freq+","+p.bw+","+p.sf+","+p.cr;
@@ -765,6 +819,7 @@ function loadConfig(){
});
$$("[data-cfm]").forEach(function(el){el.value=""});
$$("[data-rg]").forEach(function(el){el.value=String(c.radio[el.dataset.rg]);el.classList.remove("dirty")});
loadRxps(c.radio);
st.orig.radio=radioCombo();afHint();
for(var i=1;i<=st.nslots;i++){refreshSlotFields($("#app-slots"),i);refreshSlotFields($("#wz-slots"),i)}
syncPacketFilters();
@@ -787,7 +842,9 @@ function afHint(){
}
document.addEventListener("input",function(ev){
var el=ev.target;
if(el.dataset.k){
if(el.dataset.rxps){
refreshRxps(true);
}else if(el.dataset.k){
// keep duplicate fields (wizard vs app share keys) in sync
$$('[data-k="'+el.dataset.k+'"]').forEach(function(o){if(o!==el)setEl(o,elVal(el))});
markDirty(el.dataset.k,elVal(el),el);
@@ -1519,6 +1576,7 @@ var CLI_KEYS=[
["cad","Listen before transmit",0,"on|off"],
["radio.rxgain","SX126x RX boosted gain",0,"on|off"],
["radio.fem.rxgain","Front-end module RX gain",0],
["radio.rxps","RX duty-cycle power saving",0,"off|level 1-10|level 1-10 preamble 16|32|rx_us sleep_us"],
["radio.watchdog","Restart the radio if silent this long {0-120 min}",0],
["int.thresh","Interference threshold",0],
["agc.reset.interval","AGC reset interval in seconds",0],