Przeglądaj źródła

Merge pull request #45 from xJARiD/develop

fix: reduce repeater battery polling
xJARiD 3 miesięcy temu
rodzic
commit
874b51ac04

+ 1 - 2
docs/custom-cli.md

@@ -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 - 0
docs/web-panel.md

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

+ 1 - 3
examples/simple_room_server/MyMesh.cpp

@@ -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
examples/simple_sensor/SensorMesh.cpp

@@ -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 - 0
release-notes.yml

@@ -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: []

+ 0 - 3
src/MeshCore.h

@@ -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
src/helpers/CommonCLI.cpp

@@ -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
src/helpers/CommonCLI.h

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

+ 0 - 19
variants/heltec_v3/HeltecV3Board.h

@@ -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";
   }