Forráskód Böngészése

fix: reduce repeater battery polling

Jared Dohrman 3 hónapja
szülő
commit
274cb8b8c2

+ 1 - 0
docs/custom-cli.md

@@ -88,6 +88,7 @@ Legacy dotted aliases are also accepted:
 
 - `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
 

+ 44 - 45
examples/simple_repeater/MyMesh.cpp

@@ -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));
 
@@ -1399,6 +1372,25 @@ void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint3
   }
 }
 
+uint16_t MyMesh::getBatteryMilliVolts(bool force_refresh) {
+  constexpr unsigned long kBatterySampleIntervalMs = 60000UL;
+
+  if (board.supportsBatteryReporting() && !board.isBatteryReportingEnabled()) {
+    _battery_sample_valid = true;
+    _battery_mv_cache = 0;
+    next_battery_sample_ms = 0;
+    return 0;
+  }
+
+  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 +1526,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 +1861,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 +2487,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 +2545,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 +2595,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 +2635,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 +2775,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
examples/simple_repeater/MyMesh.h

@@ -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);

+ 21 - 0
release-notes.yml

@@ -401,3 +401,24 @@ 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: 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: []