Merge pull request #45 from xJARiD/develop

fix: reduce repeater battery polling
This commit is contained in:
xJARiD
2026-04-22 10:03:12 +10:00
committed by GitHub
11 changed files with 133 additions and 82 deletions
+1 -2
View File
@@ -86,8 +86,7 @@ Legacy dotted aliases are also accepted:
### Board Battery Reporting
- `get battery.reporting`: shows whether board battery reporting is enabled. Support is board-dependent.
- `set battery.reporting on|off`: enables or disables battery voltage reporting on supported boards. This is currently useful for Heltec V3 boards where USB-only power can produce misleading battery readings. If your board needs this too, open an issue and support can be added board-by-board.
- On repeater MQTT builds, background battery sampling used for MQTT/status history is rate-limited to about once per minute. Explicit status and telemetry requests still refresh the reading immediately.
### T-Beam 1W Fan Control
+24
View File
@@ -165,6 +165,30 @@ Notes:
- the refresh buttons load the current value from the repeater
- the save buttons send the matching CLI command immediately
## Ghost Node Mode
Ghost Node Mode is a convenience control on `/app` for a repeater that should stay on Wi-Fi and MQTT, but should not actively behave like another nearby repeater.
Typical use case:
- an indoor or colocated MQTT observer where another repeater nearby is already doing the RF relay work
- a node you want feeding MQTT, web status, and troubleshooting data without also adding extra repeat traffic or adverts
When enabled, Ghost Node Mode:
- turns `repeat` off
- sets `advert.interval` to `0`
- sets `flood.advert.interval` to `0`
- leaves the local web panel and MQTT features running
When disabled, the panel restores the prior repeat and advert settings if it still knows them from the current browser session. If not, it falls back to:
- `repeat on`
- `advert.interval 60`
- `flood.advert.interval 12`
This mode is useful when you want the device to observe and publish, not to act as an additional RF repeater. It does not create a separate firmware role; it is just a grouped web-panel shortcut for those existing settings.
## MQTT Settings
This section includes:
+38 -47
View File
@@ -28,44 +28,14 @@
namespace {
struct WebSensorSnapshot {
bool has_supply_voltage = false;
float supply_voltage_v = NAN;
bool has_sensor_temp = false;
float sensor_temp_c = NAN;
bool has_mcu_temp = false;
float mcu_temp_c = NAN;
bool has_humidity = false;
float humidity_pct = NAN;
bool has_pressure = false;
float pressure_hpa = NAN;
bool has_pressure_altitude = false;
float pressure_altitude_m = NAN;
bool has_gps = false;
bool gps_enabled = false;
bool gps_fix = false;
bool has_gps_lat = false;
float gps_lat = NAN;
bool has_gps_lon = false;
float gps_lon = NAN;
bool has_gps_altitude = false;
float gps_altitude_m = NAN;
bool has_satellites = false;
long satellites = 0;
};
WebSensorSnapshot collectWebSensorSnapshot(mesh::MainBoard& board, SensorManager& sensors) {
WebSensorSnapshot collectWebSensorSnapshot(mesh::MainBoard& board, SensorManager& sensors, uint16_t battery_mv) {
WebSensorSnapshot snapshot;
snapshot.has_battery = true;
snapshot.battery_mv = battery_mv;
CayenneLPP sensor_telemetry(200);
sensor_telemetry.reset();
sensor_telemetry.addVoltage(TELEM_CHANNEL_SELF, static_cast<float>(board.getBattMilliVolts()) / 1000.0f);
sensor_telemetry.addVoltage(TELEM_CHANNEL_SELF, static_cast<float>(battery_mv) / 1000.0f);
sensors.querySensors(0xFF, sensor_telemetry);
const float board_temp_c = board.getMCUTemperature();
@@ -567,7 +537,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
if (payload[0] == REQ_TYPE_GET_STATUS) { // guests can also access this now
RepeaterStats stats;
stats.batt_milli_volts = board.getBattMilliVolts();
stats.batt_milli_volts = getBatteryMilliVolts(true);
stats.curr_tx_queue_len = _mgr->getOutboundTotal();
stats.noise_floor = (int16_t)_radio->getNoiseFloor();
stats.last_rssi = (int16_t)radio_driver.getLastRSSI();
@@ -593,7 +563,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)getBatteryMilliVolts(true) / 1000.0f);
// query other sensors -- target specific
if ((sender->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) {
@@ -1219,12 +1189,15 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_archive = nullptr;
uptime_millis = 0;
next_archive_neighbours_flush_ms = 0;
next_battery_sample_ms = 0;
next_history_sample_ms = 0;
next_local_advert = next_flood_advert = 0;
dirty_contacts_expiry = 0;
set_radio_at = revert_radio_at = 0;
_logging = false;
_archive_neighbours_dirty = false;
_battery_sample_valid = false;
_battery_mv_cache = 0;
region_load_active = false;
memset(&_stats_state, 0, sizeof(_stats_state));
@@ -1267,7 +1240,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
_prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier
_prefs.battery_reporting_enabled = 1;
_prefs.reserved_290 = 0;
_prefs.fan_mode = 0; // auto
_prefs.fan_timeout_secs = 30;
@@ -1374,7 +1347,6 @@ void MyMesh::begin(FILESYSTEM *fs, ArchiveStorage* archive) {
updateFloodAdvertTimer();
board.setAdcMultiplier(_prefs.adc_multiplier);
board.setBatteryReporting(_prefs.battery_reporting_enabled);
#if defined(TBEAM_1W)
auto& tbeam1w_board = static_cast<TBeam1WBoard&>(board);
tbeam1w_board.setFanPostTxHoldMs(static_cast<uint32_t>(_prefs.fan_timeout_secs) * 1000UL);
@@ -1399,6 +1371,18 @@ void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint3
}
}
uint16_t MyMesh::getBatteryMilliVolts(bool force_refresh) {
constexpr unsigned long kBatterySampleIntervalMs = 60000UL;
if (force_refresh || !_battery_sample_valid || next_battery_sample_ms == 0 || millisHasNowPassed(next_battery_sample_ms)) {
_battery_mv_cache = board.getBattMilliVolts();
_battery_sample_valid = true;
next_battery_sample_ms = millis() + kBatterySampleIntervalMs;
}
return _battery_mv_cache;
}
void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params
pending_freq = freq;
@@ -1534,7 +1518,13 @@ void MyMesh::removeNeighbor(const uint8_t *pubkey, int key_len) {
}
void MyMesh::formatStatsReply(char *reply, size_t reply_size) {
StatsFormatHelper::formatCoreStats(reply, reply_size, board, *_ms, _err_flags, _mgr);
snprintf(reply,
reply_size,
"{\"battery_mv\":%u,\"uptime_secs\":%u,\"errors\":%u,\"queue_len\":%u}",
getBatteryMilliVolts(true),
_ms->getMillis() / 1000,
_err_flags,
_mgr->getOutboundTotal());
}
void MyMesh::startRegionsLoad() {
@@ -1863,13 +1853,14 @@ void MyMesh::updateStatsHistory(unsigned long now_ms) {
#endif
if (next_history_sample_ms == 0 || millisHasNowPassed(next_history_sample_ms)) {
if (!live_stats_headroom_low) {
WebSensorSnapshot sensor_snapshot = collectWebSensorSnapshot(board, sensors);
const uint16_t battery_mv = getBatteryMilliVolts();
WebSensorSnapshot sensor_snapshot = collectWebSensorSnapshot(board, sensors, battery_mv);
HistorySample sample{};
sample.epoch_secs = getRTCClock()->getCurrentTime();
sample.uptime_secs = static_cast<uint32_t>(uptime_millis / 1000);
sample.packets_sent = radio_driver.getPacketsSent();
sample.packets_recv = radio_driver.getPacketsRecv();
sample.battery_mv = board.getBattMilliVolts();
sample.battery_mv = battery_mv;
sample.queue_len = static_cast<uint16_t>(_mgr->getOutboundTotal());
sample.error_flags = _err_flags;
sample.recv_errors = static_cast<uint16_t>(min<uint32_t>(radio_driver.getPacketsRecvErrors(), 0xFFFF));
@@ -2488,7 +2479,9 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) {
wifi_signal[sizeof(wifi_signal) - 1] = 0;
#endif
const uint16_t battery_mv = getBatteryMilliVolts(true);
const int battery_pct = board.getBatteryPercent();
const WebSensorSnapshot sensor_snapshot = collectWebSensorSnapshot(board, sensors, battery_mv);
const bool archive_available = (_archive != nullptr) && _archive->isMounted();
#ifdef WITH_MQTT_UPLINK
const bool mqtt_connected = mqtt.isAnyBrokerConnected();
@@ -2544,7 +2537,7 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) {
archive_type,
static_cast<unsigned long long>(_archive != nullptr ? _archive->getTotalBytes() : 0),
static_cast<unsigned long long>(_archive != nullptr ? _archive->getUsedBytes() : 0),
board.getBattMilliVolts(),
battery_mv,
battery_pct,
static_cast<unsigned long>(uptime_millis / 1000),
_err_flags,
@@ -2594,7 +2587,7 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) {
}
offset += snprintf(&reply[offset], reply_size - offset, ",");
if (!appendJsonSensors(reply, reply_size, offset)) {
if (!appendJsonSensors(reply, reply_size, offset, sensor_snapshot)) {
return false;
}
offset += snprintf(&reply[offset], reply_size - offset, ",");
@@ -2634,10 +2627,8 @@ bool MyMesh::formatWebStatsSeriesJson(const char* series, char* reply, size_t re
#endif
}
bool MyMesh::appendJsonSensors(char* reply, size_t reply_size, size_t& offset) const {
bool MyMesh::appendJsonSensors(char* reply, size_t reply_size, size_t& offset, const WebSensorSnapshot& snapshot) const {
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
WebSensorSnapshot snapshot = collectWebSensorSnapshot(board, sensors);
bool needs_comma = false;
const int open_written = snprintf(&reply[offset], reply_size - offset, "\"sensors\":{");
if (open_written < 0 || static_cast<size_t>(open_written) >= (reply_size - offset)) {
@@ -2776,7 +2767,7 @@ void MyMesh::loop() {
#endif
#ifdef WITH_MQTT_UPLINK
MQTTStatusSnapshot mqtt_status{};
mqtt_status.battery_mv = static_cast<int>(board.getBattMilliVolts());
mqtt_status.battery_mv = static_cast<int>(getBatteryMilliVolts());
mqtt_status.uptime_secs = static_cast<uint32_t>(uptime_millis / 1000);
mqtt_status.error_flags = _err_flags;
mqtt_status.queue_len = static_cast<uint16_t>(_mgr->getOutboundTotal());
+40 -1
View File
@@ -65,6 +65,41 @@ struct RepeaterStats {
uint32_t n_recv_errors;
};
struct WebSensorSnapshot {
bool has_battery = false;
uint16_t battery_mv = 0;
bool has_supply_voltage = false;
float supply_voltage_v = NAN;
bool has_sensor_temp = false;
float sensor_temp_c = NAN;
bool has_mcu_temp = false;
float mcu_temp_c = NAN;
bool has_humidity = false;
float humidity_pct = NAN;
bool has_pressure = false;
float pressure_hpa = NAN;
bool has_pressure_altitude = false;
float pressure_altitude_m = NAN;
bool has_gps = false;
bool gps_enabled = false;
bool gps_fix = false;
bool has_gps_lat = false;
float gps_lat = NAN;
bool has_gps_lon = false;
float gps_lon = NAN;
bool has_gps_altitude = false;
float gps_altitude_m = NAN;
bool has_satellites = false;
long satellites = 0;
};
#ifndef MAX_CLIENTS
#define MAX_CLIENTS 32
#endif
@@ -94,10 +129,13 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelComm
uint32_t last_millis;
uint64_t uptime_millis;
unsigned long next_archive_neighbours_flush_ms;
unsigned long next_battery_sample_ms;
unsigned long next_history_sample_ms;
unsigned long next_local_advert, next_flood_advert;
bool _logging;
bool _archive_neighbours_dirty;
bool _battery_sample_valid;
uint16_t _battery_mv_cache;
NodePrefs _prefs;
ClientACL acl;
CommonCLI _cli;
@@ -159,7 +197,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelComm
void recordStatsEvent(uint8_t type, int16_t value = 0);
bool appendJsonEvents(char* reply, size_t reply_size, size_t& offset) const;
bool appendJsonNeighbours(char* reply, size_t reply_size, size_t& offset) const;
bool appendJsonSensors(char* reply, size_t reply_size, size_t& offset) const;
bool appendJsonSensors(char* reply, size_t reply_size, size_t& offset, const WebSensorSnapshot& snapshot) const;
uint16_t getBatteryMilliVolts(bool force_refresh = false);
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
+1 -3
View File
@@ -652,8 +652,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.gps_enabled = 0;
_prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
_prefs.battery_reporting_enabled = 1;
_prefs.reserved_290 = 0;
next_post_idx = 0;
next_client_idx = 0;
next_push = 0;
@@ -699,7 +698,6 @@ void MyMesh::begin(FILESYSTEM *fs) {
updateFloodAdvertTimer();
board.setAdcMultiplier(_prefs.adc_multiplier);
board.setBatteryReporting(_prefs.battery_reporting_enabled);
#if ENV_INCLUDE_GPS == 1
applyGpsPrefs();
+1 -2
View File
@@ -731,7 +731,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
_prefs.gps_enabled = 0;
_prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
_prefs.battery_reporting_enabled = 1;
_prefs.reserved_290 = 0;
memset(default_scope.key, 0, sizeof(default_scope.key));
}
@@ -771,7 +771,6 @@ void SensorMesh::begin(FILESYSTEM* fs) {
updateFloodAdvertTimer();
board.setAdcMultiplier(_prefs.adc_multiplier);
board.setBatteryReporting(_prefs.battery_reporting_enabled);
#if ENV_INCLUDE_GPS == 1
applyGpsPrefs();
+24
View File
@@ -401,3 +401,27 @@ releases:
area: docs
text: "Updated the web panel, API, and custom CLI docs to cover the environment summary/trends, stats persistence behaviour, and existing T-Beam 1W fan controls."
breaking_changes: []
- track: repeater-mqtt
version: "1.3.9"
tag: "repeater-mqtt-eastmesh-v1.3.9"
date: "2026-04-22"
previous_version: "1.3.8"
summary: "Reduced unnecessary repeater battery polling by removing hot-loop MQTT sampling and reusing throttled battery reads across stats and web status paths."
changes:
- type: fixed
area: mqtt
text: "Stopped rebuilding the MQTT status battery reading from a fresh ADC sample on every repeater loop iteration and switched that path to a throttled cached value instead."
- type: changed
area: stats
text: "Reused a single battery sample across the web stats history and summary builders so one logical stats operation no longer performs multiple immediate battery reads."
- type: fixed
area: board-support
text: "Reduced Heltec V3 battery-sense activity while MQTT and web features are active, while still keeping explicit status and telemetry requests on a fresh reading path."
- type: changed
area: board-support
text: "Removed the obsolete battery reporting toggle path and its stale CLI documentation now that repeater battery sampling is handled correctly without a board-level reporting disable switch."
- type: docs
area: docs
text: "Updated the custom CLI docs to note that repeater MQTT background battery sampling is now rate-limited while explicit status and telemetry requests still refresh immediately."
breaking_changes: []
-3
View File
@@ -44,9 +44,6 @@ namespace mesh {
class MainBoard {
public:
virtual uint16_t getBattMilliVolts() = 0;
virtual bool supportsBatteryReporting() const { return false; }
virtual bool setBatteryReporting(bool enabled) { (void)enabled; return false; }
virtual bool isBatteryReportingEnabled() const { return true; }
virtual int getBatteryPercent() { return -1; }
virtual bool isCharging() { return false; }
virtual bool isVbusPresent() { return false; }
+3 -4
View File
@@ -87,8 +87,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
if (file.available() >= (int)sizeof(_prefs->battery_reporting_enabled)) {
file.read((uint8_t *)&_prefs->battery_reporting_enabled, sizeof(_prefs->battery_reporting_enabled)); // 290
if (file.available() >= (int)sizeof(_prefs->reserved_290)) {
file.read((uint8_t *)&_prefs->reserved_290, sizeof(_prefs->reserved_290)); // 290 reserved
}
if (file.available() >= (int)sizeof(_prefs->rx_boosted_gain)) {
file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 291
@@ -123,7 +123,6 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->bridge_channel = constrain(_prefs->bridge_channel, 0, 14);
_prefs->powersaving_enabled = constrain(_prefs->powersaving_enabled, 0, 1);
_prefs->battery_reporting_enabled = constrain(_prefs->battery_reporting_enabled, 0, 1);
_prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1);
_prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2);
@@ -192,7 +191,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
file.write((uint8_t *)&_prefs->battery_reporting_enabled, sizeof(_prefs->battery_reporting_enabled)); // 290
file.write((uint8_t *)&_prefs->reserved_290, sizeof(_prefs->reserved_290)); // 290 reserved
file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 291
file.write((uint8_t *)&_prefs->fan_mode, sizeof(_prefs->fan_mode)); // 292
file.write((uint8_t *)&_prefs->fan_timeout_secs, sizeof(_prefs->fan_timeout_secs)); // 293
+1 -1
View File
@@ -58,7 +58,7 @@ struct NodePrefs { // persisted to file
uint32_t discovery_mod_timestamp;
float adc_multiplier;
char owner_info[120];
uint8_t battery_reporting_enabled; // boolean
uint8_t reserved_290; // reserved to preserve on-disk prefs layout
uint8_t rx_boosted_gain; // power settings
uint8_t path_hash_mode; // which path mode to use when sending
uint8_t loop_detect;
-19
View File
@@ -22,13 +22,11 @@
class HeltecV3Board : public ESP32Board {
private:
bool adc_active_state;
bool battery_reporting_enabled;
public:
RefCountedDigitalPin periph_power;
HeltecV3Board() : adc_active_state(false),
battery_reporting_enabled(true),
periph_power(PIN_VEXT_EN) { }
void begin() {
@@ -83,10 +81,6 @@ public:
}
uint16_t getBattMilliVolts() override {
if (!battery_reporting_enabled) {
return 0;
}
analogReadResolution(10);
digitalWrite(PIN_ADC_CTRL, adc_active_state);
@@ -109,19 +103,6 @@ public:
return mv;
}
bool supportsBatteryReporting() const override {
return true;
}
bool setBatteryReporting(bool enabled) override {
battery_reporting_enabled = enabled;
return true;
}
bool isBatteryReportingEnabled() const override {
return battery_reporting_enabled;
}
const char* getManufacturerName() const override {
return "Heltec V3";
}