diff --git a/docs/api.md b/docs/api.md index f9313785..81811b0f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -102,6 +102,21 @@ Example: curl -sk -X POST https:///login --data '' ``` +### `GET /api/session` + +Check whether a stored session token is still valid. + +Headers: + +```text +X-Auth-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. diff --git a/docs/web-panel.md b/docs/web-panel.md index 3d983831..dea1ac2d 100644 --- a/docs/web-panel.md +++ b/docs/web-panel.md @@ -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. @@ -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: diff --git a/src/helpers/StatsHistory.cpp b/src/helpers/StatsHistory.cpp index 538d2dbe..a39a8bc4 100644 --- a/src/helpers/StatsHistory.cpp +++ b/src/helpers/StatsHistory.cpp @@ -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) { diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 19472406..1b580a9c 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -161,6 +161,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 +194,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 +204,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 +214,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 +232,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 +243,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 +254,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 +272,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 +282,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 +297,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 +307,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 +317,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 +328,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 +338,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 +350,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 +361,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 { diff --git a/src/helpers/web/WebPanelServer.cpp b/src/helpers/web/WebPanelServer.cpp index 39e74c5e..21881eec 100644 --- a/src/helpers/web/WebPanelServer.cpp +++ b/src/helpers/web/WebPanelServer.cpp @@ -255,7 +255,12 @@ const char kWebPanelLoginHtml[] PROGMEM = R"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(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(req->user_ctx); if (ctx == nullptr || ctx->self == nullptr || ctx->self->_runner == nullptr) { diff --git a/src/helpers/web/WebPanelServer.h b/src/helpers/web/WebPanelServer.h index 7c76ed08..e1d0ce58 100644 --- a/src/helpers/web/WebPanelServer.h +++ b/src/helpers/web/WebPanelServer.h @@ -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);