fix: make repeater stats battery display board-aware
Este commit está contenido en:
@@ -153,6 +153,7 @@ Notes:
|
||||
- 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`
|
||||
- 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>`
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
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:
|
||||
|
||||
1. summary/status
|
||||
|
||||
@@ -28,6 +28,15 @@
|
||||
|
||||
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 snapshot;
|
||||
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 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 bool archive_available = (_archive != nullptr) && _archive->isMounted();
|
||||
#ifdef WITH_MQTT_UPLINK
|
||||
@@ -2510,7 +2524,8 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) {
|
||||
"\"events\":%u,\"event_capacity\":%u},"
|
||||
"\"archive\":{\"logical\":\"%s\",\"available\":%s,\"path\":\"%s\",\"type\":\"%s\","
|
||||
"\"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},"
|
||||
"\"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,"
|
||||
@@ -2539,6 +2554,9 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) {
|
||||
static_cast<unsigned long long>(_archive != nullptr ? _archive->getUsedBytes() : 0),
|
||||
battery_mv,
|
||||
battery_pct,
|
||||
battery_display_pct,
|
||||
battery_min_mv,
|
||||
battery_max_mv,
|
||||
static_cast<unsigned long>(uptime_millis / 1000),
|
||||
_err_flags,
|
||||
static_cast<unsigned>(_mgr->getOutboundTotal()),
|
||||
|
||||
@@ -425,3 +425,24 @@ releases:
|
||||
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: []
|
||||
|
||||
- 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: []
|
||||
|
||||
@@ -45,6 +45,8 @@ class MainBoard {
|
||||
public:
|
||||
virtual uint16_t getBattMilliVolts() = 0;
|
||||
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 isVbusPresent() { return false; }
|
||||
virtual float getMCUTemperature() { return NAN; }
|
||||
|
||||
@@ -1457,13 +1457,20 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
};
|
||||
}
|
||||
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 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">
|
||||
<h3>Core</h3>
|
||||
<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("Errors", String(core.errors ?? 0), errorsPct, "sticky error flags", true)}
|
||||
</div>
|
||||
@@ -1798,6 +1805,10 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
return baseColor || "#2f8f4e";
|
||||
}
|
||||
function sparkValueRange(key, values) {
|
||||
if (key === "packets" || key === "gps_satellites") {
|
||||
const maxValue = Math.max(1, ...values);
|
||||
return { min:0, max:maxValue };
|
||||
}
|
||||
if (key === "signal") {
|
||||
return { min:(-125 * 4), max:(-30 * 4) };
|
||||
}
|
||||
@@ -1881,21 +1892,25 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
ctx.lineTo(plotRight, y);
|
||||
ctx.stroke();
|
||||
});
|
||||
const slotWidth = (plotRight - plotLeft) / Math.max(points.length, 1);
|
||||
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])
|
||||
}));
|
||||
const strokeColor = sparkStrokeColor(key, points);
|
||||
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 hoverColor = sparkHoverColor(key, strokeColor);
|
||||
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 barHeight = Math.max(1, plotBottom - top);
|
||||
const barHeight = Math.max(0, plotBottom - top);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,14 @@ uint16_t TBeam1WBoard::getBattMilliVolts() {
|
||||
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 {
|
||||
return "LilyGo T-Beam 1W";
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ public:
|
||||
void onBeforeTransmit() override;
|
||||
void onAfterTransmit() override;
|
||||
uint16_t getBattMilliVolts() override;
|
||||
uint16_t getBatteryMinMilliVolts() const override;
|
||||
uint16_t getBatteryMaxMilliVolts() const override;
|
||||
const char* getManufacturerName() const override;
|
||||
void powerOff() override;
|
||||
|
||||
|
||||
Referencia en una nueva incidencia
Block a user