Parcourir la source

fix: make repeater stats battery display board-aware

Jared Dohrman il y a 3 mois
Parent
commit
b1a32fa56b

+ 1 - 0
docs/api.md

@@ -153,6 +153,7 @@ Notes:
 - this summary view is also what the repo's web panel requests first before loading trend series
 - this summary view is also what the repo's web panel requests first before loading trend series
 - if `web.stats` is disabled, the endpoint returns `503 Service Unavailable`
 - if `web.stats` is disabled, the endpoint returns `503 Service Unavailable`
 - supported boards may also include an optional `sensors` object in the summary payload for current GPS and environmental telemetry
 - supported boards may also include an optional `sensors` object in the summary payload for current GPS and environmental telemetry
+- the `core` object includes raw `battery_mv`, board-reported `battery_pct` when available, a UI-ready `battery_display_pct`, and board-specific `battery_min_mv` / `battery_max_mv` range hints used by `/stats` when the board does not expose its own battery percentage
 
 
 ### `GET /api/stats?series=<name>`
 ### `GET /api/stats?series=<name>`
 
 

+ 2 - 0
docs/web-panel.md

@@ -224,6 +224,8 @@ For boards that expose extra telemetry, the optional `Environment` summary card
 
 
 Metrics with no current value are hidden rather than showing placeholder rows, so the cards vary by board and by current sensor state.
 Metrics with no current value are hidden rather than showing placeholder rows, so the cards vary by board and by current sensor state.
 
 
+The `Core` battery meter prefers a board-reported battery percentage when the target exposes one. Otherwise it scales the displayed percentage from the board's configured battery voltage range rather than assuming a fixed single-cell `3000-4200 mV` pack.
+
 The trend graphs load sequentially rather than as one large payload:
 The trend graphs load sequentially rather than as one large payload:
 
 
 1. summary/status
 1. summary/status

+ 19 - 1
examples/simple_repeater/MyMesh.cpp

@@ -28,6 +28,15 @@
 
 
 namespace {
 namespace {
 
 
+int clampBatteryPercentFromRange(uint16_t battery_mv, uint16_t min_mv, uint16_t max_mv) {
+  if (max_mv <= min_mv) {
+    return 0;
+  }
+  const long scaled = (static_cast<long>(battery_mv) - static_cast<long>(min_mv)) * 100L /
+                      static_cast<long>(max_mv - min_mv);
+  return std::max(0L, std::min(100L, scaled));
+}
+
 WebSensorSnapshot collectWebSensorSnapshot(mesh::MainBoard& board, SensorManager& sensors, uint16_t battery_mv) {
 WebSensorSnapshot collectWebSensorSnapshot(mesh::MainBoard& board, SensorManager& sensors, uint16_t battery_mv) {
   WebSensorSnapshot snapshot;
   WebSensorSnapshot snapshot;
   snapshot.has_battery = true;
   snapshot.has_battery = true;
@@ -2481,6 +2490,11 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) {
 
 
   const uint16_t battery_mv = getBatteryMilliVolts(true);
   const uint16_t battery_mv = getBatteryMilliVolts(true);
   const int battery_pct = board.getBatteryPercent();
   const int battery_pct = board.getBatteryPercent();
+  const uint16_t battery_min_mv = board.getBatteryMinMilliVolts();
+  const uint16_t battery_max_mv = board.getBatteryMaxMilliVolts();
+  const int battery_display_pct =
+      (battery_pct >= 0) ? std::max(0, std::min(100, battery_pct))
+                         : clampBatteryPercentFromRange(battery_mv, battery_min_mv, battery_max_mv);
   const WebSensorSnapshot sensor_snapshot = collectWebSensorSnapshot(board, sensors, battery_mv);
   const WebSensorSnapshot sensor_snapshot = collectWebSensorSnapshot(board, sensors, battery_mv);
   const bool archive_available = (_archive != nullptr) && _archive->isMounted();
   const bool archive_available = (_archive != nullptr) && _archive->isMounted();
 #ifdef WITH_MQTT_UPLINK
 #ifdef WITH_MQTT_UPLINK
@@ -2510,7 +2524,8 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) {
                      "\"events\":%u,\"event_capacity\":%u},"
                      "\"events\":%u,\"event_capacity\":%u},"
                      "\"archive\":{\"logical\":\"%s\",\"available\":%s,\"path\":\"%s\",\"type\":\"%s\","
                      "\"archive\":{\"logical\":\"%s\",\"available\":%s,\"path\":\"%s\",\"type\":\"%s\","
                      "\"total_bytes\":%llu,\"used_bytes\":%llu},"
                      "\"total_bytes\":%llu,\"used_bytes\":%llu},"
-                     "\"core\":{\"battery_mv\":%u,\"battery_pct\":%d,\"uptime_secs\":%lu,\"errors\":%u,\"queue_len\":%u,"
+                     "\"core\":{\"battery_mv\":%u,\"battery_pct\":%d,\"battery_display_pct\":%d,\"battery_min_mv\":%u,\"battery_max_mv\":%u,"
+                     "\"uptime_secs\":%lu,\"errors\":%u,\"queue_len\":%u,"
                      "\"external_power\":%s,\"charging\":%s,\"vbus\":%s},"
                      "\"external_power\":%s,\"charging\":%s,\"vbus\":%s},"
                      "\"radio\":{\"noise_floor\":%d,\"last_rssi\":%.2f,\"last_snr\":%.2f,\"tx_air_secs\":%lu,\"rx_air_secs\":%lu},"
                      "\"radio\":{\"noise_floor\":%d,\"last_rssi\":%.2f,\"last_snr\":%.2f,\"tx_air_secs\":%lu,\"rx_air_secs\":%lu},"
                      "\"packets\":{\"recv\":%u,\"sent\":%u,\"flood_tx\":%u,\"direct_tx\":%u,\"flood_rx\":%u,\"direct_rx\":%u,"
                      "\"packets\":{\"recv\":%u,\"sent\":%u,\"flood_tx\":%u,\"direct_tx\":%u,\"flood_rx\":%u,\"direct_rx\":%u,"
@@ -2539,6 +2554,9 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) {
                      static_cast<unsigned long long>(_archive != nullptr ? _archive->getUsedBytes() : 0),
                      static_cast<unsigned long long>(_archive != nullptr ? _archive->getUsedBytes() : 0),
                      battery_mv,
                      battery_mv,
                      battery_pct,
                      battery_pct,
+                     battery_display_pct,
+                     battery_min_mv,
+                     battery_max_mv,
                      static_cast<unsigned long>(uptime_millis / 1000),
                      static_cast<unsigned long>(uptime_millis / 1000),
                      _err_flags,
                      _err_flags,
                      static_cast<unsigned>(_mgr->getOutboundTotal()),
                      static_cast<unsigned>(_mgr->getOutboundTotal()),

+ 21 - 0
release-notes.yml

@@ -425,3 +425,24 @@ releases:
         area: 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."
         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: []
     breaking_changes: []
+
+  - track: repeater-mqtt
+    version: "1.3.10"
+    tag: "repeater-mqtt-eastmesh-v1.3.10"
+    date: "2026-04-22"
+    previous_version: "1.3.9"
+    summary: "Made `/stats` battery display board-aware and corrected count-trend rendering for restored packet activity and GPS satellites."
+    changes:
+      - type: changed
+        area: stats
+        text: "Updated the `/stats` Core battery meter to prefer a board-reported battery percentage when available and otherwise scale from a board-specific battery voltage range instead of a fixed single-cell assumption."
+      - type: added
+        area: board-support
+        text: "Added default board battery range hints to the board abstraction and set the T-Beam 1W repeater battery profile to a 2S `6000-8400 mV` range for the web stats path."
+      - type: fixed
+        area: web-panel
+        text: "Corrected `/stats` packet activity and GPS satellites bar charts so restored zero values no longer draw phantom bars and the final bar uses the same spacing as the rest of the series."
+      - type: docs
+        area: docs
+        text: "Updated the web panel and API docs to describe the board-aware battery percentage display fields used by the repeater stats UI."
+    breaking_changes: []

+ 2 - 0
src/MeshCore.h

@@ -45,6 +45,8 @@ class MainBoard {
 public:
 public:
   virtual uint16_t getBattMilliVolts() = 0;
   virtual uint16_t getBattMilliVolts() = 0;
   virtual int getBatteryPercent() { return -1; }
   virtual int getBatteryPercent() { return -1; }
+  virtual uint16_t getBatteryMinMilliVolts() const { return 3000; }
+  virtual uint16_t getBatteryMaxMilliVolts() const { return 4200; }
   virtual bool isCharging() { return false; }
   virtual bool isCharging() { return false; }
   virtual bool isVbusPresent() { return false; }
   virtual bool isVbusPresent() { return false; }
   virtual float getMCUTemperature() { return NAN; }
   virtual float getMCUTemperature() { return NAN; }

+ 22 - 7
src/helpers/web/WebPanelServer.cpp

@@ -1457,13 +1457,20 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
       };
       };
     }
     }
     function renderCoreCard(core) {
     function renderCoreCard(core) {
-      const batteryPct = pctRange(core.battery_mv, 3000, 4200);
+      const batteryMin = Number.isFinite(core.battery_min_mv) ? core.battery_min_mv : 3000;
+      const batteryMax = Number.isFinite(core.battery_max_mv) ? core.battery_max_mv : 4200;
+      const batteryPct = Number.isFinite(core.battery_display_pct)
+          ? clamp(core.battery_display_pct, 0, 100)
+          : pctRange(core.battery_mv, batteryMin, batteryMax);
       const queuePct = pctRange(core.queue_len, 0, 12);
       const queuePct = pctRange(core.queue_len, 0, 12);
       const errorsPct = core.errors > 0 ? 100 : 0;
       const errorsPct = core.errors > 0 ? 100 : 0;
+      const batteryDetail = batteryMin < batteryMax
+          ? (core.battery_mv || 0) + " mV (" + batteryMin + "-" + batteryMax + " mV)"
+          : (core.battery_mv || 0) + " mV";
       return `<section class="hud-card">
       return `<section class="hud-card">
         <h3>Core</h3>
         <h3>Core</h3>
         <div class="core-grid">
         <div class="core-grid">
-          ${renderMeter("Battery", Math.round(batteryPct) + "%", batteryPct, (core.battery_mv || 0) + " mV", false)}
+          ${renderMeter("Battery", Math.round(batteryPct) + "%", batteryPct, batteryDetail, false)}
           ${renderMeter("Queue", String(core.queue_len ?? 0), queuePct, "outbound packets", true)}
           ${renderMeter("Queue", String(core.queue_len ?? 0), queuePct, "outbound packets", true)}
           ${renderMeter("Errors", String(core.errors ?? 0), errorsPct, "sticky error flags", true)}
           ${renderMeter("Errors", String(core.errors ?? 0), errorsPct, "sticky error flags", true)}
         </div>
         </div>
@@ -1798,6 +1805,10 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
       return baseColor || "#2f8f4e";
       return baseColor || "#2f8f4e";
     }
     }
     function sparkValueRange(key, values) {
     function sparkValueRange(key, values) {
+      if (key === "packets" || key === "gps_satellites") {
+        const maxValue = Math.max(1, ...values);
+        return { min:0, max:maxValue };
+      }
       if (key === "signal") {
       if (key === "signal") {
         return { min:(-125 * 4), max:(-30 * 4) };
         return { min:(-125 * 4), max:(-30 * 4) };
       }
       }
@@ -1881,21 +1892,25 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
         ctx.lineTo(plotRight, y);
         ctx.lineTo(plotRight, y);
         ctx.stroke();
         ctx.stroke();
       });
       });
+      const slotWidth = (plotRight - plotLeft) / Math.max(points.length, 1);
       const coords = points.map((point, index) => ({
       const coords = points.map((point, index) => ({
-        x: (index / Math.max(1, points.length - 1)) * (plotRight - plotLeft) + plotLeft,
+        x: (key === "packets" || key === "gps_satellites")
+            ? (plotLeft + (slotWidth * index) + (slotWidth / 2))
+            : ((index / Math.max(1, points.length - 1)) * (plotRight - plotLeft) + plotLeft),
         y: scaleY(point[1])
         y: scaleY(point[1])
       }));
       }));
       const strokeColor = sparkStrokeColor(key, points);
       const strokeColor = sparkStrokeColor(key, points);
       if (key === "packets" || key === "gps_satellites") {
       if (key === "packets" || key === "gps_satellites") {
-        const slotWidth = (plotRight - plotLeft) / Math.max(points.length, 1);
         const barWidth = Math.max(3, Math.min(18, slotWidth * 0.68));
         const barWidth = Math.max(3, Math.min(18, slotWidth * 0.68));
         const hoverColor = sparkHoverColor(key, strokeColor);
         const hoverColor = sparkHoverColor(key, strokeColor);
         coords.forEach((point, index) => {
         coords.forEach((point, index) => {
-          const left = clamp(point.x - (barWidth / 2), plotLeft, plotRight - barWidth);
+          const left = plotLeft + (slotWidth * index) + ((slotWidth - barWidth) / 2);
           const top = point.y;
           const top = point.y;
-          const barHeight = Math.max(1, plotBottom - top);
+          const barHeight = Math.max(0, plotBottom - top);
           ctx.fillStyle = Number.isInteger(hoverIndex) && hoverIndex === index ? hoverColor : strokeColor;
           ctx.fillStyle = Number.isInteger(hoverIndex) && hoverIndex === index ? hoverColor : strokeColor;
-          ctx.fillRect(left, top, barWidth, barHeight);
+          if (barHeight > 0) {
+            ctx.fillRect(left, top, barWidth, barHeight);
+          }
         });
         });
         return;
         return;
       }
       }

+ 8 - 0
variants/lilygo_tbeam_1w/TBeam1WBoard.cpp

@@ -66,6 +66,14 @@ uint16_t TBeam1WBoard::getBattMilliVolts() {
   return static_cast<uint16_t>((raw * 3300 * ADC_MULTIPLIER) / 4095);
   return static_cast<uint16_t>((raw * 3300 * ADC_MULTIPLIER) / 4095);
 }
 }
 
 
+uint16_t TBeam1WBoard::getBatteryMinMilliVolts() const {
+  return 6000;
+}
+
+uint16_t TBeam1WBoard::getBatteryMaxMilliVolts() const {
+  return 8400;
+}
+
 const char* TBeam1WBoard::getManufacturerName() const {
 const char* TBeam1WBoard::getManufacturerName() const {
   return "LilyGo T-Beam 1W";
   return "LilyGo T-Beam 1W";
 }
 }

+ 2 - 0
variants/lilygo_tbeam_1w/TBeam1WBoard.h

@@ -50,6 +50,8 @@ public:
   void onBeforeTransmit() override;
   void onBeforeTransmit() override;
   void onAfterTransmit() override;
   void onAfterTransmit() override;
   uint16_t getBattMilliVolts() override;
   uint16_t getBattMilliVolts() override;
+  uint16_t getBatteryMinMilliVolts() const override;
+  uint16_t getBatteryMaxMilliVolts() const override;
   const char* getManufacturerName() const override;
   const char* getManufacturerName() const override;
   void powerOff() override;
   void powerOff() override;