Merge pull request #46 from xJARiD/develop

feat: add mqtt client version to repeater app and cli
This commit is contained in:
xJARiD
2026-04-22 13:37:55 +10:00
committed by GitHub
12 fájl változott, egészen pontosan 142 új sor hozzáadva és 32 régi sor törölve
+1
Fájl megtekintése
@@ -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>`
+3 -2
Fájl megtekintése
@@ -12,6 +12,7 @@ These commands are available on `*_repeater_mqtt` firmware targets.
- `get mqtt.status`: shows WiFi, NTP, IATA, endpoint status, status publishing state, and TX state.
- `get mqtt.statuscfg`: shows whether periodic status messages are enabled as a simple `on` or `off` value. Most users can just use `get mqtt.status`.
- `get mqtt.client_version`: shows the MQTT `client_version` string published by the repeater.
- `get mqtt.iata`: shows the IATA/location code used in MQTT topics.
- `set mqtt.iata <code>`: sets the IATA/location code, for example `MEL`.
- `set mqtt.iata UNSET`: marks MQTT IATA as not configured yet. While it is `UNSET`, enabled MQTT brokers stay disconnected until a real code is saved.
@@ -68,7 +69,7 @@ Legacy dotted aliases are also accepted:
### Web Panel Controls
- `get web`
- `get web.status`: shows whether the local HTTPS panel is available. After `start ota`, this reports `web:suspended ota` until the repeater reboots.
- `get web.status`: shows whether the local HTTPS panel is available.
- `get web.stats.status`: shows whether the dedicated stats page and history subsystem are enabled, whether recent history is active, whether PSRAM-backed history is available, and whether the SD-backed archive is mounted. When enabled, the history capture now covers supported environment telemetry too, not just the original battery/radio series. GPS-enabled boards also record per-minute satellites samples for the `/stats` history view.
- `set web on|off`
- `set.web on|off`: enables or disables the local HTTPS panel.
@@ -122,7 +123,7 @@ Notes:
- the panel still uses the repeater admin password for access
- commands run with the same care as if you typed them into the repeater CLI directly
- this is intended for local admin use on a trusted network
- `start ota` suspends the local repeater web panel until reboot so the OTA HTTP listener can take over port `80`
- `start ota` releases the local HTTP redirect listener on port `80` so the OTA HTTP listener can take over without stopping the rest of the repeater services
## Companion WiFi Rescue Commands
+13 -2
Fájl megtekintése
@@ -130,7 +130,7 @@ This section runs common read-only commands for:
- Wi-Fi
- MQTT
These are useful for quick checks without typing into the CLI field.
These are useful for quick checks without typing into the CLI field. The MQTT quick actions include `mqtt.status`, `mqtt.client_version`, `mqtt.iata`, `mqtt.owner`, and `mqtt.email`.
## Run CLI Command
@@ -149,6 +149,7 @@ This makes it easy to see exactly what the panel sent to the repeater.
This section includes:
- Device Name
- Clock UTC
- Latitude
- Longitude
- Guest Password
@@ -165,6 +166,14 @@ Notes:
- the refresh buttons load the current value from the repeater
- the save buttons send the matching CLI command immediately
## Info
This section shows:
- `Version`: firmware version with build date
- `Client Version`: MQTT client version string
- `Public Key`
## 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.
@@ -224,6 +233,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
@@ -301,7 +312,7 @@ On mobile:
1. Press `Start OTA`.
2. Confirm the action.
3. The local repeater web panel is suspended until reboot so OTA can take over HTTP on port `80`.
3. The local HTTP redirect listener on port `80` is released so OTA can take over that port.
4. Continue with your normal OTA workflow.
### Use Historical Stats
+21 -4
Fájl megtekintése
@@ -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;
@@ -2047,9 +2056,6 @@ void MyMesh::clearStats() {
}
void MyMesh::prepareForOTAStart() {
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
web.suspendForOTA();
#endif
}
void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) {
@@ -2279,6 +2285,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
sprintf(reply, "> %s", mqtt.isStatusEnabled() ? "on" : "off");
} else if (strcmp(command, "get mqtt.status") == 0) {
mqtt.formatStatusReply(reply, 160);
} else if (strcmp(command, "get mqtt.client_version") == 0) {
sprintf(reply, "> %s", mqtt.getClientVersion());
} else if (memcmp(command, "get mqtt.iata", 13) == 0) {
sprintf(reply, "> %s", mqtt.getIata());
} else if (memcmp(command, "get mqtt.owner", 14) == 0) {
@@ -2481,6 +2489,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 +2523,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 +2553,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()),
+30
Fájl megtekintése
@@ -425,3 +425,33 @@ 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: added
area: mqtt
text: "Added a `get mqtt.client_version` repeater CLI command and exposed the same value in the `/app` quick MQTT command buttons."
- type: changed
area: web-panel
text: "Reworked the `/app` Info panel to show `Version` and `Client Version`, and moved `Clock UTC` into Repeater Settings beside Device Name."
- type: fixed
area: web-panel
text: "Narrowed `start ota` handling so the repeater only releases the local HTTP redirect listener on port `80`, allowing ElegantOTA to start without unnecessarily stopping the rest of the repeater services."
- type: docs
area: docs
text: "Updated the web panel, API, and custom CLI docs to describe the board-aware battery display, the new MQTT client-version command, and the revised `/app` layout."
breaking_changes: []
+2
Fájl megtekintése
@@ -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; }
+11 -2
Fájl megtekintése
@@ -12,7 +12,11 @@
bool ESP32Board::startOTAUpdate(const char* id, char reply[]) {
inhibit_sleep = true; // prevent sleep during OTA
WiFi.softAP("MeshCore-OTA", NULL);
WiFi.mode(WIFI_AP_STA);
if (!WiFi.softAP("MeshCore-OTA", NULL)) {
strcpy(reply, "Error - OTA AP start failed");
return false;
}
sprintf(reply, "Started: http://%s/update", WiFi.softAPIP().toString().c_str());
MESH_DEBUG_PRINTLN("startOTAUpdate: %s", reply);
@@ -22,7 +26,12 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[]) {
static char home_buf[90];
sprintf(home_buf, "<H2>Hi! I am a MeshCore Repeater. ID: %s</H2>", id);
AsyncWebServer* server = new AsyncWebServer(80);
static AsyncWebServer* server = nullptr;
if (server != nullptr) {
delete server;
server = nullptr;
}
server = new AsyncWebServer(80);
server->on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/html", home_buf);
@@ -116,6 +116,10 @@ MQTTUplink::MQTTUplink(mesh::RTCClock& rtc, mesh::LocalIdentity& identity)
MQTT_LOG("uplink init");
}
const char* MQTTUplink::getClientVersion() const {
return CLIENT_VERSION;
}
bool MQTTUplink::savePrefs() {
return MQTTPrefsStore::save(_fs, _prefs);
}
+1
Fájl megtekintése
@@ -55,6 +55,7 @@ public:
bool isTxEnabled() const { return _prefs.tx_enabled != 0; }
bool setIata(const char* iata);
const char* getIata() const { return _prefs.iata; }
const char* getClientVersion() const;
void setNodeNameSource(const char* node_name) { _node_name = node_name; }
bool setOwnerPublicKey(const char* owner_public_key);
const char* getOwnerPublicKey() const { return _prefs.owner_public_key; }
+46 -22
Fájl megtekintése
@@ -576,6 +576,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
<span class="label">MQTT</span>
<div class="quick">
<button data-cmd="get mqtt.status">mqtt.status</button>
<button data-cmd="get mqtt.client_version">mqtt.client_version</button>
<button data-cmd="get mqtt.iata">mqtt.iata</button>
<button data-cmd="get mqtt.owner">mqtt.owner</button>
<button data-cmd="get mqtt.email">mqtt.email</button>
@@ -599,18 +600,15 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
<div class="stack">
<div class="row">
<div class="field-card">
<label class="label" for="roleValue">Role</label>
<label class="label" for="versionValue">Version</label>
<div class="fieldline">
<input id="roleValue" readonly disabled>
<span class="placeholder-slot" aria-hidden="true"></span>
<input id="versionValue" readonly disabled>
</div>
</div>
<div class="field-card">
<label class="label" for="clockUtc">Clock UTC</label>
<div class="inline-actions">
<input id="clockUtc" readonly disabled>
<button class="iconbtn" data-load-cmd="clock" data-load-input="clockUtc" title="Refresh clock UTC">&#8635;</button>
<button id="syncClockBtn" class="savebtn">Sync</button>
<label class="label" for="clientVersionValue">Client Version</label>
<div class="fieldline">
<input id="clientVersionValue" readonly disabled>
</div>
</div>
</div>
@@ -629,12 +627,22 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
<div class="stack">
<div class="section-group">
<h3>Repeater Settings</h3>
<div class="field-card">
<label class="label" for="nodeName">Device Name</label>
<div class="inline-actions">
<input id="nodeName" placeholder="MeshCore-HOWL">
<button class="iconbtn" data-load-cmd="get name" data-load-input="nodeName" title="Refresh device name">&#8635;</button>
<button class="savebtn" data-prefix="set name " data-input="nodeName">Save</button>
<div class="row">
<div class="field-card">
<label class="label" for="nodeName">Device Name</label>
<div class="inline-actions">
<input id="nodeName" placeholder="MeshCore-HOWL">
<button class="iconbtn" data-load-cmd="get name" data-load-input="nodeName" title="Refresh device name">&#8635;</button>
<button class="savebtn" data-prefix="set name " data-input="nodeName">Save</button>
</div>
</div>
<div class="field-card">
<label class="label" for="clockUtc">Clock UTC</label>
<div class="inline-actions">
<input id="clockUtc" readonly disabled>
<button class="iconbtn" data-load-cmd="clock" data-load-input="clockUtc" title="Refresh clock UTC">&#8635;</button>
<button id="syncClockBtn" class="savebtn">Sync</button>
</div>
</div>
</div>
<div class="row">
@@ -1457,13 +1465,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 +1813,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 +1900,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;
}
@@ -2516,7 +2539,8 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
const quiet = { recordHistory:false, updateInput:false };
try {
await loadSection("Loading info...", [
() => loadField("get role", "roleValue", null, quiet),
() => loadField("ver", "versionValue", null, quiet),
() => loadField("get mqtt.client_version", "clientVersionValue", null, quiet),
() => loadField("clock", "clockUtc", null, quiet),
() => loadField("get public.key", "publicKey", "uppercase", quiet)
]);
@@ -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;