Merge pull request #55 from xJARiD/develop
fix: gps, truncated LWT, improve no-psram stats, web stability, and absent sensor probes
This commit is contained in:
10 files changed
+212
-47
No files matched your search
+15
@@ -102,6 +102,21 @@ Example:
|
||||
curl -sk -X POST https://<repeater-ip>/login --data '<admin-password>'
|
||||
```
|
||||
|
||||
### `GET /api/session`
|
||||
|
||||
Check whether a stored session token is still valid.
|
||||
|
||||
Headers:
|
||||
|
||||
```text
|
||||
X-Auth-Token: <token>
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
- JSON `{"authenticated":true}` on success
|
||||
- `401 Unauthorized` if the token is missing or stale after lock, restart, or reflash
|
||||
|
||||
### `POST /api/command`
|
||||
|
||||
Run a repeater CLI command remotely.
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ Legacy dotted aliases are also accepted:
|
||||
|
||||
- `get web`
|
||||
- `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.
|
||||
- `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-active 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.
|
||||
- `set web.stats on|off`
|
||||
|
||||
+7
-5
@@ -72,6 +72,8 @@ Example:
|
||||
- the connection is HTTPS, but the certificate is self-signed
|
||||
- browsers will warn the first time you connect
|
||||
- the panel exposes the repeater CLI after login
|
||||
- the browser keeps the session token while changing between `/app` and `/stats`; the page switches views in place until logout, idle lock, or server restart
|
||||
- after a device restart or reflash, the app checks the stored token before loading settings and returns to login if the session is stale
|
||||
|
||||
This is intended for local admin use on a trusted network, not for open internet exposure.
|
||||
|
||||
@@ -229,13 +231,13 @@ The `/stats` page currently shows:
|
||||
|
||||
- `Services`: MQTT, web, archive, neighbour count, and, when mounted, card and archive capacity
|
||||
- optional full-width `Environment` summary card on boards that report GPS or environmental telemetry
|
||||
- `Trends`: battery, heap free, packet activity, signal, noise floor, and, when GPS is enabled, satellites
|
||||
- `Trends`: battery, heap free, packet activity, signal, noise floor, and, when GPS is active, satellites
|
||||
- `Neighbours`: current neighbour table with ID, SNR, heard age, and advert age
|
||||
- `Events`: current boot/session events
|
||||
|
||||
Planned MQTT JWT rotations are shown as `mqtt_token_refreshed` events instead of a short `mqtt_disconnected` / `mqtt_connected` pair.
|
||||
|
||||
For boards that expose extra telemetry, the optional `Environment` summary card can show current values such as GPS fix state, latitude, longitude, GPS altitude, voltage, sensor temperature, humidity, barometer, pressure-derived altitude, and MCU temperature.
|
||||
For boards that expose extra telemetry, the optional `Environment` summary card can show current values such as GPS active/fix state, latitude, longitude, GPS altitude, voltage, sensor temperature, humidity, barometer, pressure-derived altitude, and MCU temperature.
|
||||
|
||||
Metrics with no current value are hidden rather than showing placeholder rows, so the cards vary by board and by current sensor state.
|
||||
|
||||
@@ -248,7 +250,7 @@ The trend graphs load sequentially rather than as one large payload:
|
||||
3. memory
|
||||
4. packet activity
|
||||
5. signal
|
||||
6. satellites when GPS is enabled
|
||||
6. satellites when GPS is active
|
||||
|
||||
This keeps browser-side and device-side memory use lower than the previous in-page stats view.
|
||||
|
||||
@@ -267,13 +269,13 @@ Current in-memory history caps are:
|
||||
| `4 MB` to less than `8 MB` PSRAM | `480` | `192` | About `8` hours |
|
||||
| `8 MB` PSRAM or more | `720` | `288` | About `12` hours |
|
||||
|
||||
On boards with roughly `2 MB` PSRAM or more, stats history starts capturing from boot when `web.stats` is enabled, even if `/stats` has not been opened yet.
|
||||
When `web.stats` is enabled, stats history starts capturing from boot or from the time it is enabled, even if `/stats` has not been opened yet. No-PSRAM boards use the smaller live-only buffer shown above.
|
||||
|
||||
Archive-backed restore requires `web.stats` enabled plus a mounted SD card on boards that support the EastMesh archive path.
|
||||
|
||||
The main purpose of the SD card is to let the repeater retain and restore stats history for `/stats`. The archive keeps fast `.latest` snapshot files for quick restore and UTC-dated daily `.log` files for longer-term history. As a secondary option, those files can also be removed and inspected on a computer for deeper manual review.
|
||||
|
||||
On no-PSRAM boards, `/stats` can still show recent graphs while the stats view is active, but the history is smaller and does not provide the same archive-backed behaviour as PSRAM-capable boards.
|
||||
On no-PSRAM boards, `/stats` can still show recent graphs, but the history is smaller and does not provide the same archive-backed behaviour as PSRAM-capable boards.
|
||||
|
||||
Useful CLI commands:
|
||||
|
||||
|
||||
@@ -55,10 +55,13 @@ WebSensorSnapshot collectWebSensorSnapshot(mesh::MainBoard& board, SensorManager
|
||||
LocationProvider* location = sensors.getLocationProvider();
|
||||
if (location != nullptr) {
|
||||
snapshot.has_gps = true;
|
||||
snapshot.gps_enabled = location->isEnabled();
|
||||
snapshot.gps_fix = location->isValid();
|
||||
const bool provider_enabled = location->isEnabled();
|
||||
const char* gps_setting = sensors.getSettingByKey("gps");
|
||||
const bool setting_active = (gps_setting == nullptr) || (strcmp(gps_setting, "1") == 0);
|
||||
snapshot.gps_enabled = provider_enabled && setting_active;
|
||||
snapshot.gps_fix = snapshot.gps_enabled && location->isValid();
|
||||
const long satellites = location->satellitesCount();
|
||||
if (snapshot.gps_enabled || satellites > 0) {
|
||||
if (snapshot.gps_enabled) {
|
||||
snapshot.has_satellites = true;
|
||||
snapshot.satellites = max<long>(satellites, 0);
|
||||
}
|
||||
|
||||
@@ -527,3 +527,33 @@ releases:
|
||||
area: readme
|
||||
text: "Updated the README to describe EastMesh as an Eastern Australia community mesh network, introduce CoreScope telemetry, and warn that MQTT repeater builds are intended for Eastern Australia repeaters only."
|
||||
breaking_changes: []
|
||||
|
||||
- track: repeater-mqtt
|
||||
version: "1.3.14"
|
||||
tag: "repeater-mqtt-eastmesh-v1.3.14"
|
||||
date: "2026-04-30"
|
||||
previous_version: "1.3.13"
|
||||
summary: "Fixes `/stats` GPS visibility and no-PSRAM trend capture, improves web-panel session recovery and App/Stats navigation on constrained boards, quiets absent I2C sensor probes, and prevents truncated MQTT offline status payloads."
|
||||
changes:
|
||||
- type: fixed
|
||||
area: stats
|
||||
text: "Updated `/stats` GPS gauges and satellite trends to respect the active GPS setting so boards that report GPS hardware as on but deactivated after `gps off` no longer keep satellite metrics visible."
|
||||
- type: fixed
|
||||
area: stats
|
||||
text: "Allowed no-PSRAM boards to collect the small live `/stats` history buffer whenever `web.stats` is enabled, so Heltec v3 targets can show recent trend samples without archive storage."
|
||||
- type: fixed
|
||||
area: web-panel
|
||||
text: "Improved App/Stats navigation and session recovery by preserving the login token across panel views, validating stale browser tokens before loading settings, and clearing expired sessions quickly after a reflash."
|
||||
- type: fixed
|
||||
area: web-panel
|
||||
text: "Delayed the red MQTT IATA warning until the panel has loaded the configured value, avoiding a misleading unset banner while `/app` is still fetching settings."
|
||||
- type: fixed
|
||||
area: sensors
|
||||
text: "Probed optional I2C environmental sensor addresses before driver initialization so boards without those sensors no longer emit repeated Wire read errors during boot."
|
||||
- type: fixed
|
||||
area: mqtt
|
||||
text: "Increased the MQTT offline last-will payload buffer so the retained offline status JSON is not truncated when release metadata and identity fields are near their configured limits."
|
||||
- type: docs
|
||||
area: web-panel
|
||||
text: "Documented no-PSRAM `/stats` history behavior, stale browser-token recovery, and the lightweight `/api/session` endpoint used by the web panel."
|
||||
breaking_changes: []
|
||||
@@ -537,7 +537,7 @@ bool StatsHistory::supportsPersistence() const {
|
||||
}
|
||||
|
||||
bool StatsHistory::shouldAutoActivateFromBoot() const {
|
||||
return _enabled && !_live_only && hasBootAutoCapturePsram();
|
||||
return _enabled && (_live_only || hasBootAutoCapturePsram());
|
||||
}
|
||||
|
||||
bool StatsHistory::isBootAutoCaptureExpected() const {
|
||||
@@ -556,7 +556,7 @@ bool StatsHistory::isAccessActive(uint32_t now_ms) const {
|
||||
if (!_live_only) {
|
||||
return true;
|
||||
}
|
||||
return _last_access_ms != 0 && (now_ms - _last_access_ms) < kLiveOnlyIdleTimeoutMs;
|
||||
return _activated || (_last_access_ms != 0 && (now_ms - _last_access_ms) < kLiveOnlyIdleTimeoutMs);
|
||||
}
|
||||
|
||||
void StatsHistory::noteAccess(uint32_t now_ms) {
|
||||
|
||||
@@ -94,7 +94,7 @@ private:
|
||||
char* token;
|
||||
char client_id[48];
|
||||
char status_topic[128];
|
||||
char offline_payload[256];
|
||||
char offline_payload[512];
|
||||
};
|
||||
#endif
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "EnvironmentSensorManager.h"
|
||||
|
||||
#include <Wire.h>
|
||||
|
||||
#if ENV_PIN_SDA && ENV_PIN_SCL
|
||||
#define TELEM_WIRE &Wire1 // Use Wire1 as the I2C bus for Environment Sensors
|
||||
#else
|
||||
@@ -161,6 +163,18 @@ public:
|
||||
static RAK12500LocationProvider RAK12500_provider;
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
bool i2cDevicePresent(TwoWire* wire, uint8_t address) {
|
||||
if (wire == nullptr) {
|
||||
return false;
|
||||
}
|
||||
wire->beginTransmission(address);
|
||||
return wire->endTransmission() == 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool EnvironmentSensorManager::begin() {
|
||||
#if ENV_INCLUDE_GPS
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
@@ -182,7 +196,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_AHTX0
|
||||
if (AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) {
|
||||
if (i2cDevicePresent(TELEM_WIRE, TELEM_AHTX_ADDRESS) && AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) {
|
||||
MESH_DEBUG_PRINTLN("Found AHT10/AHT20 at address: %02X", TELEM_AHTX_ADDRESS);
|
||||
AHTX0_initialized = true;
|
||||
} else {
|
||||
@@ -192,7 +206,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_BME680
|
||||
if (BME680.begin(TELEM_BME680_ADDRESS)) {
|
||||
if (i2cDevicePresent(TELEM_WIRE, TELEM_BME680_ADDRESS) && BME680.begin(TELEM_BME680_ADDRESS)) {
|
||||
MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESS);
|
||||
BME680_initialized = true;
|
||||
} else {
|
||||
@@ -202,7 +216,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_BME280
|
||||
if (BME280.begin(TELEM_BME280_ADDRESS, TELEM_WIRE)) {
|
||||
if (i2cDevicePresent(TELEM_WIRE, TELEM_BME280_ADDRESS) && BME280.begin(TELEM_BME280_ADDRESS, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESS);
|
||||
MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID());
|
||||
// Reduce self-heating: single-shot conversions, light oversampling, long standby.
|
||||
@@ -220,7 +234,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_BMP280
|
||||
if (BMP280.begin(TELEM_BMP280_ADDRESS)) {
|
||||
if (i2cDevicePresent(TELEM_WIRE, TELEM_BMP280_ADDRESS) && BMP280.begin(TELEM_BMP280_ADDRESS)) {
|
||||
MESH_DEBUG_PRINTLN("Found BMP280 at address: %02X", TELEM_BMP280_ADDRESS);
|
||||
MESH_DEBUG_PRINTLN("BMP sensor ID: %02X", BMP280.sensorID());
|
||||
BMP280_initialized = true;
|
||||
@@ -231,7 +245,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_SHTC3
|
||||
if (SHTC3.begin(TELEM_WIRE)) {
|
||||
if (i2cDevicePresent(TELEM_WIRE, 0x70) && SHTC3.begin(TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found sensor: SHTC3");
|
||||
SHTC3_initialized = true;
|
||||
} else {
|
||||
@@ -242,10 +256,14 @@ bool EnvironmentSensorManager::begin() {
|
||||
|
||||
|
||||
#if ENV_INCLUDE_SHT4X
|
||||
SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS);
|
||||
uint32_t serialNumber = 0;
|
||||
int16_t sht4x_error;
|
||||
sht4x_error = SHT4X.serialNumber(serialNumber);
|
||||
if (i2cDevicePresent(TELEM_WIRE, TELEM_SHT4X_ADDRESS)) {
|
||||
SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS);
|
||||
sht4x_error = SHT4X.serialNumber(serialNumber);
|
||||
} else {
|
||||
sht4x_error = -1;
|
||||
}
|
||||
if (sht4x_error == 0) {
|
||||
MESH_DEBUG_PRINTLN("Found SHT4X at address: %02X", TELEM_SHT4X_ADDRESS);
|
||||
SHT4X_initialized = true;
|
||||
@@ -256,7 +274,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_LPS22HB
|
||||
if (LPS22HB.begin()) {
|
||||
if (i2cDevicePresent(TELEM_WIRE, 0x5C) && LPS22HB.begin()) {
|
||||
MESH_DEBUG_PRINTLN("Found sensor: LPS22HB");
|
||||
LPS22HB_initialized = true;
|
||||
} else {
|
||||
@@ -266,7 +284,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA3221
|
||||
if (INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) {
|
||||
if (i2cDevicePresent(TELEM_WIRE, TELEM_INA3221_ADDRESS) && INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS);
|
||||
MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID());
|
||||
|
||||
@@ -281,7 +299,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA219
|
||||
if (INA219.begin(TELEM_WIRE)) {
|
||||
if (i2cDevicePresent(TELEM_WIRE, TELEM_INA219_ADDRESS) && INA219.begin(TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS);
|
||||
INA219_initialized = true;
|
||||
} else {
|
||||
@@ -291,7 +309,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA260
|
||||
if (INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) {
|
||||
if (i2cDevicePresent(TELEM_WIRE, TELEM_INA260_ADDRESS) && INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found INA260 at address: %02X", TELEM_INA260_ADDRESS);
|
||||
INA260_initialized = true;
|
||||
} else {
|
||||
@@ -301,7 +319,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA226
|
||||
if (INA226.begin()) {
|
||||
if (i2cDevicePresent(TELEM_WIRE, TELEM_INA226_ADDRESS) && INA226.begin()) {
|
||||
MESH_DEBUG_PRINTLN("Found INA226 at address: %02X", TELEM_INA226_ADDRESS);
|
||||
INA226.setMaxCurrentShunt(TELEM_INA226_MAX_AMP, TELEM_INA226_SHUNT_VALUE);
|
||||
INA226_initialized = true;
|
||||
@@ -312,7 +330,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_MLX90614
|
||||
if (MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) {
|
||||
if (i2cDevicePresent(TELEM_WIRE, TELEM_MLX90614_ADDRESS) && MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found MLX90614 at address: %02X", TELEM_MLX90614_ADDRESS);
|
||||
MLX90614_initialized = true;
|
||||
} else {
|
||||
@@ -322,7 +340,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_VL53L0X
|
||||
if (VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) {
|
||||
if (i2cDevicePresent(TELEM_WIRE, TELEM_VL53L0X_ADDRESS) && VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found VL53L0X at address: %02X", TELEM_VL53L0X_ADDRESS);
|
||||
VL53L0X_initialized = true;
|
||||
} else {
|
||||
@@ -334,7 +352,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#if ENV_INCLUDE_BMP085
|
||||
// First argument is MODE (aka oversampling)
|
||||
// choose ULTRALOWPOWER
|
||||
if (BMP085.begin(0, TELEM_WIRE)) {
|
||||
if (i2cDevicePresent(TELEM_WIRE, 0x77) && BMP085.begin(0, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found sensor BMP085");
|
||||
BMP085_initialized = true;
|
||||
} else {
|
||||
@@ -345,7 +363,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
|
||||
#if ENV_INCLUDE_RAK12035
|
||||
RAK12035.setup(*TELEM_WIRE);
|
||||
if (RAK12035.begin(TELEM_RAK12035_ADDRESS)) {
|
||||
if (i2cDevicePresent(TELEM_WIRE, TELEM_RAK12035_ADDRESS) && RAK12035.begin(TELEM_RAK12035_ADDRESS)) {
|
||||
MESH_DEBUG_PRINTLN("Found sensor RAK12035 at address: %02X", TELEM_RAK12035_ADDRESS);
|
||||
RAK12035_initialized = true;
|
||||
} else {
|
||||
|
||||
@@ -255,7 +255,12 @@ const char kWebPanelLoginHtml[] PROGMEM = R"HTML(
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
const TOKEN_KEY = "repeater-token";
|
||||
const statusEl = document.getElementById("status");
|
||||
function storeToken(value) {
|
||||
try { sessionStorage.setItem(TOKEN_KEY, value); } catch (_) {}
|
||||
try { localStorage.setItem(TOKEN_KEY, value); } catch (_) {}
|
||||
}
|
||||
function getPreferredPage() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const next = params.get("next");
|
||||
@@ -269,7 +274,7 @@ const char kWebPanelLoginHtml[] PROGMEM = R"HTML(
|
||||
statusEl.textContent = text || "Access denied";
|
||||
return;
|
||||
}
|
||||
sessionStorage.setItem("repeater-token", text.trim());
|
||||
storeToken(text.trim());
|
||||
window.location.replace(getPreferredPage());
|
||||
}
|
||||
document.getElementById("loginBtn").onclick = () => login();
|
||||
@@ -1021,7 +1026,8 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
</main>
|
||||
<script>
|
||||
const RADIO_PRESETS_URL = "https://api.meshcore.nz/api/v1/config";
|
||||
const isStatsPage = window.location.pathname === "/stats";
|
||||
let isStatsPage = window.location.pathname === "/stats";
|
||||
let pageLoadGeneration = 0;
|
||||
const PANEL_TITLE_KEY = "repeater-panel-title";
|
||||
const REGION_STATE_CODES = {
|
||||
"ACT": "act",
|
||||
@@ -1031,10 +1037,27 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
"Tasmania": "tas",
|
||||
"Victoria": "vic"
|
||||
};
|
||||
let token = sessionStorage.getItem("repeater-token") || "";
|
||||
const TOKEN_KEY = "repeater-token";
|
||||
function readStoredToken() {
|
||||
try {
|
||||
const sessionToken = sessionStorage.getItem(TOKEN_KEY);
|
||||
if (sessionToken) return sessionToken;
|
||||
} catch (_) {}
|
||||
try {
|
||||
return localStorage.getItem(TOKEN_KEY) || "";
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
function clearStoredToken() {
|
||||
try { sessionStorage.removeItem(TOKEN_KEY); } catch (_) {}
|
||||
try { localStorage.removeItem(TOKEN_KEY); } catch (_) {}
|
||||
}
|
||||
let token = readStoredToken();
|
||||
let commandQueue = Promise.resolve();
|
||||
let radioPresetEntries = [];
|
||||
let currentRadioConfig = null;
|
||||
let mqttIataLoaded = false;
|
||||
const statusEl = document.getElementById("status");
|
||||
const replyEl = document.getElementById("reply");
|
||||
const themeToggleEl = document.getElementById("themeToggle");
|
||||
@@ -1058,7 +1081,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
}
|
||||
function redirectToLogin() {
|
||||
const next = isStatsPage ? "/stats" : "/app";
|
||||
sessionStorage.removeItem("repeater-token");
|
||||
clearStoredToken();
|
||||
token = "";
|
||||
window.location.replace("/?next=" + encodeURIComponent(next));
|
||||
}
|
||||
@@ -1084,12 +1107,12 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
if (appBtn) {
|
||||
appBtn.classList.toggle("active", !isStatsPage);
|
||||
appBtn.title = "Open app page";
|
||||
appBtn.onclick = () => window.location.assign("/app");
|
||||
appBtn.onclick = () => navigateToPage("/app");
|
||||
}
|
||||
if (statsBtn) {
|
||||
statsBtn.classList.toggle("active", isStatsPage);
|
||||
statsBtn.title = "Open stats page";
|
||||
statsBtn.onclick = () => window.location.assign("/stats");
|
||||
statsBtn.onclick = () => navigateToPage("/stats");
|
||||
}
|
||||
if (advertBtn) advertBtn.style.display = isStatsPage ? "none" : "";
|
||||
if (otaBtn) otaBtn.style.display = isStatsPage ? "none" : "";
|
||||
@@ -2031,6 +2054,12 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
else ctx.lineTo(point.x, point.y);
|
||||
});
|
||||
ctx.stroke();
|
||||
if (coords.length === 1) {
|
||||
ctx.fillStyle = strokeColor;
|
||||
ctx.beginPath();
|
||||
ctx.arc(coords[0].x, coords[0].y, 3.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
if (Number.isInteger(hoverIndex) && hoverIndex >= 0 && hoverIndex < coords.length) {
|
||||
const hover = coords[hoverIndex];
|
||||
ctx.fillStyle = strokeColor;
|
||||
@@ -2168,7 +2197,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
const input = document.getElementById("mqttIata");
|
||||
const banner = document.getElementById("mqttIataBanner");
|
||||
const inlineWarning = document.getElementById("mqttBrokerWarning");
|
||||
const showWarning = !!(input && isUnsetMqttIata(input.value));
|
||||
const showWarning = !!(mqttIataLoaded && input && isUnsetMqttIata(input.value));
|
||||
if (banner) {
|
||||
banner.classList.toggle("visible", showWarning && !isStatsPage);
|
||||
}
|
||||
@@ -2216,6 +2245,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
const result = await runCommand(prefix + value);
|
||||
if (!result.ok) return;
|
||||
if (inputId === "mqttIata") {
|
||||
mqttIataLoaded = true;
|
||||
refreshMqttIataWarning();
|
||||
}
|
||||
if (inputId === "nodeName") {
|
||||
@@ -2233,6 +2263,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
}
|
||||
document.getElementById(inputId).value = value;
|
||||
if (inputId === "mqttIata") {
|
||||
mqttIataLoaded = true;
|
||||
refreshMqttIataWarning();
|
||||
}
|
||||
if (inputId === "nodeName") {
|
||||
@@ -2294,6 +2325,18 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
}
|
||||
return JSON.parse(text);
|
||||
});
|
||||
}
|
||||
async function validateSession() {
|
||||
if (!token) {
|
||||
redirectToLogin();
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await fetchJson("/api/session");
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function getLetsmeshMode() {
|
||||
const eu = document.getElementById("mqttLetsmeshEu");
|
||||
@@ -2436,13 +2479,37 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
setRadioPresetStatus(error && error.message ? error.message : "Unable to load community presets.", true);
|
||||
}
|
||||
}
|
||||
async function loadStatsPage() {
|
||||
function nextPageLoadGeneration() {
|
||||
pageLoadGeneration++;
|
||||
return pageLoadGeneration;
|
||||
}
|
||||
function isCurrentPageLoad(generation) {
|
||||
return generation === pageLoadGeneration;
|
||||
}
|
||||
function navigateToPage(path, options = {}) {
|
||||
if (path !== "/app" && path !== "/stats") return;
|
||||
const replace = options.replace === true;
|
||||
if (window.location.pathname !== path) {
|
||||
if (replace) window.history.replaceState({ page:path }, "", path);
|
||||
else window.history.pushState({ page:path }, "", path);
|
||||
}
|
||||
isStatsPage = path === "/stats";
|
||||
syncNavButton();
|
||||
refreshMqttIataWarning();
|
||||
const generation = nextPageLoadGeneration();
|
||||
initApp(generation);
|
||||
}
|
||||
window.addEventListener("popstate", () => {
|
||||
navigateToPage(window.location.pathname === "/stats" ? "/stats" : "/app", { replace:true });
|
||||
});
|
||||
async function loadStatsPage(generation = pageLoadGeneration) {
|
||||
const summaryEl = document.getElementById("statsSummary");
|
||||
if (!summaryEl) return;
|
||||
summaryEl.innerHTML = '<div class="stats-empty">Loading summary...</div>';
|
||||
let summaryPayload = null;
|
||||
try {
|
||||
summaryPayload = await fetchJson("/api/stats?view=summary");
|
||||
if (!isCurrentPageLoad(generation)) return;
|
||||
if (!summaryPayload) throw new Error("no summary payload");
|
||||
renderStatsSummary(summaryPayload);
|
||||
initTrendCards(getTrendSeriesOrder(summaryPayload));
|
||||
@@ -2453,8 +2520,10 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
|
||||
const seriesOrder = getTrendSeriesOrder(summaryPayload);
|
||||
for (const key of seriesOrder) {
|
||||
if (!isCurrentPageLoad(generation)) return;
|
||||
try {
|
||||
const payload = await fetchJson("/api/stats?series=" + encodeURIComponent(key));
|
||||
if (!isCurrentPageLoad(generation)) return;
|
||||
renderTrendResult(key, payload);
|
||||
} catch (error) {
|
||||
renderTrendError(key, error && error.message ? error.message : "Unavailable");
|
||||
@@ -2463,9 +2532,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
}
|
||||
}
|
||||
document.getElementById("runBtn").onclick = () => runCommand(document.getElementById("command").value);
|
||||
const openStats = () => window.location.assign("/stats");
|
||||
const openApp = () => window.location.assign("/app");
|
||||
document.getElementById("openStatsPanelBtn").onclick = () => openStats();
|
||||
document.getElementById("openStatsPanelBtn").onclick = () => navigateToPage("/stats");
|
||||
document.getElementById("command").addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
@@ -2615,26 +2682,33 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
}
|
||||
};
|
||||
document.getElementById("refreshPageBtn").onclick = async () => {
|
||||
const generation = nextPageLoadGeneration();
|
||||
if (isStatsPage) {
|
||||
await loadStatsPage();
|
||||
await loadStatsPage(generation);
|
||||
} else {
|
||||
await initApp();
|
||||
await initApp(generation);
|
||||
}
|
||||
};
|
||||
document.getElementById("logoutBtn").onclick = () => {
|
||||
redirectToLogin();
|
||||
};
|
||||
async function initApp() {
|
||||
async function initApp(generation = nextPageLoadGeneration()) {
|
||||
if (!token) {
|
||||
redirectToLogin();
|
||||
return;
|
||||
}
|
||||
mqttIataLoaded = false;
|
||||
refreshMqttIataWarning();
|
||||
if (!await validateSession()) {
|
||||
return;
|
||||
}
|
||||
showAuthedUi(true);
|
||||
if (isStatsPage) {
|
||||
try {
|
||||
await loadStatsPage();
|
||||
statusEl.textContent = "Ready";
|
||||
await loadStatsPage(generation);
|
||||
if (isCurrentPageLoad(generation)) statusEl.textContent = "Ready";
|
||||
} catch (error) {
|
||||
if (!isCurrentPageLoad(generation)) return;
|
||||
statusEl.textContent = error && error.message ? error.message : "Unable to load stats.";
|
||||
}
|
||||
return;
|
||||
@@ -2647,6 +2721,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
() => loadField("clock", "clockUtc", null, quiet),
|
||||
() => loadField("get public.key", "publicKey", "uppercase", quiet)
|
||||
]);
|
||||
if (!isCurrentPageLoad(generation)) return;
|
||||
await loadSection("Loading repeater settings...", [
|
||||
() => loadField("get name", "nodeName", null, quiet),
|
||||
() => loadField("get lat", "nodeLat", null, quiet),
|
||||
@@ -2654,17 +2729,20 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
() => loadField("get prv.key", "privateKey", "uppercase", quiet),
|
||||
() => loadField("get owner.info", "ownerInfo", "multiline", quiet)
|
||||
]);
|
||||
if (!isCurrentPageLoad(generation)) return;
|
||||
await loadSection("Loading radio settings...", [
|
||||
() => loadRadioConfig(quiet),
|
||||
() => loadField("get path.hash.mode", "pathHashMode", null, quiet),
|
||||
() => loadRegionSelection(quiet)
|
||||
]);
|
||||
if (!isCurrentPageLoad(generation)) return;
|
||||
await loadSection("Loading advertising...", [
|
||||
() => loadField("get advert.interval", "advertInterval", null, quiet),
|
||||
() => loadField("get flood.advert.interval", "floodInterval", null, quiet),
|
||||
() => loadField("get flood.max", "floodMax", null, quiet),
|
||||
() => loadGhostNodeModeState(quiet)
|
||||
]);
|
||||
if (!isCurrentPageLoad(generation)) return;
|
||||
await loadSection("Loading MQTT settings...", [
|
||||
() => loadField("get mqtt.iata", "mqttIata", null, quiet),
|
||||
() => loadField("get mqtt.owner", "mqttOwner", null, quiet),
|
||||
@@ -2673,18 +2751,19 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
() => loadBrokerState("get mqtt.letsmesh-eu", "mqttLetsmeshEu", quiet),
|
||||
() => loadBrokerState("get mqtt.letsmesh-us", "mqttLetsmeshUs", quiet)
|
||||
]);
|
||||
if (!isCurrentPageLoad(generation)) return;
|
||||
statusEl.textContent = "Ready";
|
||||
} catch (error) {
|
||||
if (!token) {
|
||||
if (!token || !isCurrentPageLoad(generation)) {
|
||||
return;
|
||||
}
|
||||
statusEl.textContent = error && error.message ? error.message : "Unable to load repeater settings.";
|
||||
}
|
||||
loadRadioPresets();
|
||||
if (isCurrentPageLoad(generation)) loadRadioPresets();
|
||||
}
|
||||
refreshEastmeshModeUi();
|
||||
refreshLetsmeshModeUi();
|
||||
initApp();
|
||||
navigateToPage(isStatsPage ? "/stats" : "/app", { replace:true });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2737,12 +2816,14 @@ bool WebPanelServer::start() {
|
||||
httpd_uri_t app_uri = {.uri = "/app", .method = HTTP_GET, .handler = &WebPanelServer::handleApp, .user_ctx = &_route_context};
|
||||
httpd_uri_t stats_page_uri = {.uri = "/stats", .method = HTTP_GET, .handler = &WebPanelServer::handleStatsPage, .user_ctx = &_route_context};
|
||||
httpd_uri_t login_uri = {.uri = "/login", .method = HTTP_POST, .handler = &WebPanelServer::handleLogin, .user_ctx = &_route_context};
|
||||
httpd_uri_t session_uri = {.uri = "/api/session", .method = HTTP_GET, .handler = &WebPanelServer::handleSession, .user_ctx = &_route_context};
|
||||
httpd_uri_t command_uri = {.uri = "/api/command", .method = HTTP_POST, .handler = &WebPanelServer::handleCommand, .user_ctx = &_route_context};
|
||||
httpd_uri_t stats_uri = {.uri = "/api/stats", .method = HTTP_GET, .handler = &WebPanelServer::handleStats, .user_ctx = &_route_context};
|
||||
httpd_register_uri_handler(_server, &index_uri);
|
||||
httpd_register_uri_handler(_server, &app_uri);
|
||||
httpd_register_uri_handler(_server, &stats_page_uri);
|
||||
httpd_register_uri_handler(_server, &login_uri);
|
||||
httpd_register_uri_handler(_server, &session_uri);
|
||||
httpd_register_uri_handler(_server, &command_uri);
|
||||
httpd_register_uri_handler(_server, &stats_uri);
|
||||
|
||||
@@ -2894,6 +2975,21 @@ esp_err_t WebPanelServer::handleLogin(httpd_req_t* req) {
|
||||
return httpd_resp_sendstr(req, ctx->self->_token);
|
||||
}
|
||||
|
||||
esp_err_t WebPanelServer::handleSession(httpd_req_t* req) {
|
||||
auto* ctx = static_cast<RouteContext*>(req->user_ctx);
|
||||
if (ctx == nullptr || ctx->self == nullptr) {
|
||||
return httpd_resp_send_500(req);
|
||||
}
|
||||
if (!ctx->self->isAuthorized(req)) {
|
||||
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
|
||||
}
|
||||
|
||||
ctx->self->noteActivity();
|
||||
httpd_resp_set_type(req, "application/json; charset=utf-8");
|
||||
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
|
||||
return httpd_resp_sendstr(req, "{\"authenticated\":true}");
|
||||
}
|
||||
|
||||
esp_err_t WebPanelServer::handleCommand(httpd_req_t* req) {
|
||||
auto* ctx = static_cast<RouteContext*>(req->user_ctx);
|
||||
if (ctx == nullptr || ctx->self == nullptr || ctx->self->_runner == nullptr) {
|
||||
|
||||
@@ -65,6 +65,7 @@ private:
|
||||
static esp_err_t handleApp(httpd_req_t* req);
|
||||
static esp_err_t handleStatsPage(httpd_req_t* req);
|
||||
static esp_err_t handleLogin(httpd_req_t* req);
|
||||
static esp_err_t handleSession(httpd_req_t* req);
|
||||
static esp_err_t handleCommand(httpd_req_t* req);
|
||||
static esp_err_t handleStats(httpd_req_t* req);
|
||||
|
||||
|
||||
Reference in new issue
Block a user