mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-09 15:45:34 +00:00
Expose RX power saving in WebConfig
This commit is contained in:
@@ -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`;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)"),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
+1776
-1717
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 (µs)</label><input type="number" data-rxps="rx" min="1000" max="30000000" step="1"></div>
|
||||
<div class="f"><label>Sleep window (µ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],
|
||||
|
||||
Reference in New Issue
Block a user