diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index b2f4a2a7..2c81407c 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1216,6 +1216,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _archive_neighbours_dirty = false; _battery_sample_valid = false; _battery_mv_cache = 0; + _web_sensor_snapshot_valid = false; region_load_active = false; memset(&_stats_state, 0, sizeof(_stats_state)); @@ -1332,9 +1333,11 @@ void MyMesh::begin(FILESYSTEM *fs, ArchiveStorage* archive) { web.setCommandRunner(this); web.setNetworkStateProvider(&network); web.begin(_fs); +#if defined(TBEAM_SUPREME_SX1262) if (web.isWebEnabled()) { network.loop(true); } +#endif _stats_history.begin(web.isWebStatsEnabled(), _archive); if (web.isWebStatsEnabled() && !_stats_history.isLiveOnly() && _archive != nullptr && _archive->isMounted()) { restoreArchiveNeighbours(); @@ -1356,7 +1359,7 @@ void MyMesh::begin(FILESYSTEM *fs, ArchiveStorage* archive) { mqtt.setNetworkStateProvider(&network); #endif mqtt.begin(_fs); -#if defined(ESP_PLATFORM) +#if defined(ESP_PLATFORM) && defined(TBEAM_SUPREME_SX1262) if (mqtt.isActive()) { network.loop(true); } @@ -1951,6 +1954,8 @@ void MyMesh::updateStatsHistory(unsigned long now_ms) { if (!live_stats_headroom_low) { const uint16_t battery_mv = getBatteryMilliVolts(); WebSensorSnapshot sensor_snapshot = collectWebSensorSnapshot(board, sensors, battery_mv); + _web_sensor_snapshot = sensor_snapshot; + _web_sensor_snapshot_valid = true; HistorySample sample{}; sample.epoch_secs = getRTCClock()->getCurrentTime(); sample.uptime_secs = static_cast(uptime_millis / 1000); @@ -2648,7 +2653,15 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) { 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); + WebSensorSnapshot sensor_snapshot; + if (_web_sensor_snapshot_valid) { + sensor_snapshot = _web_sensor_snapshot; + sensor_snapshot.has_battery = true; + sensor_snapshot.battery_mv = battery_mv; + } else { + sensor_snapshot.has_battery = true; + sensor_snapshot.battery_mv = battery_mv; + } const bool archive_available = (_archive != nullptr) && _archive->isMounted(); #ifdef WITH_MQTT_UPLINK const bool mqtt_connected = mqtt.isAnyBrokerConnected(); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 2842ff5c..192b937b 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -137,6 +137,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelComm bool _archive_neighbours_dirty; bool _battery_sample_valid; uint16_t _battery_mv_cache; + bool _web_sensor_snapshot_valid; + WebSensorSnapshot _web_sensor_snapshot; NodePrefs _prefs; ClientACL acl; CommonCLI _cli; diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 35b7d8ac..dce7e5fb 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -35,7 +35,9 @@ void setup() { board.begin(); +#if defined(TBEAM_SUPREME_SX1262) sensors.begin(); +#endif archive.begin(); @@ -93,6 +95,10 @@ void setup() { command[0] = 0; +#if !defined(TBEAM_SUPREME_SX1262) + sensors.begin(); +#endif + the_mesh.begin(fs, &archive); #ifdef DISPLAY_CLASS diff --git a/release-notes.yml b/release-notes.yml index d548cdea..c7fe8fe1 100644 --- a/release-notes.yml +++ b/release-notes.yml @@ -157,14 +157,23 @@ releases: tag: "observer-eastmesh-v2026.6.2" date: "2026-06-07" previous_version: "2026.6.1" - summary: "Improves Observer boot reliability, T-Beam S3 Supreme telemetry, Wi-Fi reconnect speed, and web-panel archive maintenance." + summary: "Improves Observer boot reliability, T-Beam S3 Supreme telemetry, Wi-Fi/OTA persistence, and web-panel archive maintenance." changes: - type: fixed area: board - text: "Fixed LilyGo T-Beam S3 Supreme BME280 detection and telemetry reads by using the board-detected address and a stop-based I2C read path." + text: "Fixed LilyGo T-Beam S3 Supreme BME280 detection and live telemetry by using the board-detected address, stop-based I2C reads, and board-specific I2C bus recovery before samples." + - type: fixed + area: web + text: "Changed live `/stats` sensor values to use the main-loop sensor snapshot instead of reading I2C sensors from the web request handler." - type: fixed area: wifi - text: "Improved boot-time Wi-Fi startup by starting Wi-Fi earlier and remembering the last connected AP channel, with automatic fallback to full scan if that channel is stale." + text: "Improved boot-time Wi-Fi startup by starting Wi-Fi earlier on T-Beam S3 Supreme, remembering the last connected AP channel, and falling back to full scan if that channel is stale." + - type: fixed + area: wifi + text: "Added NVS-backed Wi-Fi preference recovery so SSID and password survive OTA updates even if the filesystem preference file is lost." + - type: fixed + area: wifi + text: "Hardened Wi-Fi retry handling so scan-mode connects are not interrupted early and failed attempts reset the ESP32 station state before retrying." - type: fixed area: cli text: "Fixed `set wifi.powersaving none` returning an error when Wi-Fi power saving was already disabled." @@ -176,7 +185,7 @@ releases: text: "Added a web-panel and CLI SD purge action for clearing archive contents from supported SD-card targets." - type: fixed area: web - text: "Hardened web-panel OTA startup when the OTA listener port is still being released." + text: "Hardened OTA startup by binding before reporting success and falling back to alternate ports when port 80 is still being released." breaking_changes: [] - track: observer-eastmesh-bridge-espnow @@ -248,14 +257,23 @@ releases: tag: "observer-eastmesh-bridge-espnow-v2026.6.2" date: "2026-06-07" previous_version: "2026.6.1" - summary: "Applies the Observer boot, Wi-Fi, telemetry, archive, and web-panel maintenance fixes to ESP-NOW bridge builds." + summary: "Applies the Observer boot, Wi-Fi/OTA persistence, telemetry, archive, and web-panel maintenance fixes to ESP-NOW bridge builds." changes: - type: fixed area: board - text: "Fixed LilyGo T-Beam S3 Supreme BME280 detection and telemetry reads by using the board-detected address and a stop-based I2C read path." + text: "Fixed LilyGo T-Beam S3 Supreme BME280 detection and live telemetry by using the board-detected address, stop-based I2C reads, and board-specific I2C bus recovery before samples." + - type: fixed + area: web + text: "Changed live `/stats` sensor values to use the main-loop sensor snapshot instead of reading I2C sensors from the web request handler." - type: fixed area: wifi - text: "Improved boot-time Wi-Fi startup by starting Wi-Fi earlier and remembering the last connected AP channel, with automatic fallback to full scan if that channel is stale." + text: "Improved boot-time Wi-Fi startup by starting Wi-Fi earlier on T-Beam S3 Supreme, remembering the last connected AP channel, and falling back to full scan if that channel is stale." + - type: fixed + area: wifi + text: "Added NVS-backed Wi-Fi preference recovery so SSID and password survive OTA updates even if the filesystem preference file is lost." + - type: fixed + area: wifi + text: "Hardened Wi-Fi retry handling so scan-mode connects are not interrupted early and failed attempts reset the ESP32 station state before retrying." - type: fixed area: cli text: "Fixed `set wifi.powersaving none` returning an error when Wi-Fi power saving was already disabled." @@ -267,5 +285,5 @@ releases: text: "Added a web-panel and CLI SD purge action for clearing archive contents from supported SD-card targets." - type: fixed area: web - text: "Hardened web-panel OTA startup when the OTA listener port is still being released." + text: "Hardened OTA startup by binding before reporting success and falling back to alternate ports when port 80 is still being released." breaking_changes: [] diff --git a/src/helpers/ESP32Board.cpp b/src/helpers/ESP32Board.cpp index 4212786b..097dec3c 100644 --- a/src/helpers/ESP32Board.cpp +++ b/src/helpers/ESP32Board.cpp @@ -14,18 +14,19 @@ namespace { AsyncWebServer* ota_server = nullptr; char ota_id_buf[60]; char ota_home_buf[90]; -volatile bool ota_start_pending = false; -constexpr uint8_t kOtaStartAttempts = 60; +uint16_t ota_server_port = 0; +constexpr uint8_t kOtaPort80Attempts = 8; constexpr uint32_t kOtaStartRetryDelayMs = 500; -bool startOTAServerNow() { +bool startOTAServerNow(uint16_t port) { if (ota_server != nullptr) { ota_server->end(); delete ota_server; ota_server = nullptr; + ota_server_port = 0; } - ota_server = new AsyncWebServer(80); + ota_server = new AsyncWebServer(port); ota_server->on("/", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(200, "text/html", ota_home_buf); @@ -41,26 +42,35 @@ bool startOTAServerNow() { ota_server->end(); delete ota_server; ota_server = nullptr; + ota_server_port = 0; return false; } + ota_server_port = port; return true; } -void startOTAServerTask(void*) { - vTaskDelay(pdMS_TO_TICKS(1500)); - for (uint8_t attempt = 1; attempt <= kOtaStartAttempts; ++attempt) { - if (startOTAServerNow()) { +bool startOTAServerWithFallback() { + for (uint8_t attempt = 1; attempt <= kOtaPort80Attempts; ++attempt) { + if (startOTAServerNow(80)) { MESH_DEBUG_PRINTLN("OTA server listening on port 80"); - ota_start_pending = false; - vTaskDelete(nullptr); - return; + return true; } - MESH_DEBUG_PRINTLN("OTA server port 80 busy, retry %u/%u", attempt, kOtaStartAttempts); - vTaskDelay(pdMS_TO_TICKS(kOtaStartRetryDelayMs)); + MESH_DEBUG_PRINTLN("OTA server port 80 busy, retry %u/%u", attempt, kOtaPort80Attempts); + delay(kOtaStartRetryDelayMs); } - MESH_DEBUG_PRINTLN("OTA server failed to bind port 80"); - ota_start_pending = false; - vTaskDelete(nullptr); + + static const uint16_t fallback_ports[] = {8080, 8081}; + for (uint8_t i = 0; i < sizeof(fallback_ports) / sizeof(fallback_ports[0]); ++i) { + const uint16_t port = fallback_ports[i]; + if (startOTAServerNow(port)) { + MESH_DEBUG_PRINTLN("OTA server listening on fallback port %u", port); + return true; + } + MESH_DEBUG_PRINTLN("OTA server fallback port %u busy", port); + } + + MESH_DEBUG_PRINTLN("OTA server failed to bind any port"); + return false; } } @@ -79,24 +89,20 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[]) { ota_ip = WiFi.softAPIP(); } - sprintf(reply, "Started: http://%s/update", ota_ip.toString().c_str()); - MESH_DEBUG_PRINTLN("startOTAUpdate: %s", reply); - snprintf(ota_id_buf, sizeof(ota_id_buf), "%s (%s)", id, getManufacturerName()); snprintf(ota_home_buf, sizeof(ota_home_buf), "

Hi! I am a MeshCore Repeater. ID: %s

", id); - if (!ota_start_pending) { - ota_start_pending = true; - if (xTaskCreate(startOTAServerTask, "ota-start", 4096, nullptr, 1, nullptr) != pdPASS) { - const bool started = startOTAServerNow(); - ota_start_pending = false; - if (!started) { - strcpy(reply, "Error - OTA listener start failed"); - return false; - } - } + if (!startOTAServerWithFallback()) { + strcpy(reply, "Error - OTA listener start failed"); + return false; } + if (ota_server_port == 80) { + sprintf(reply, "Started: http://%s/update", ota_ip.toString().c_str()); + } else { + sprintf(reply, "Started: http://%s:%u/update", ota_ip.toString().c_str(), ota_server_port); + } + MESH_DEBUG_PRINTLN("startOTAUpdate: %s", reply); return true; } diff --git a/src/helpers/NetworkPrefs.cpp b/src/helpers/NetworkPrefs.cpp index 009eb14c..7d76c670 100644 --- a/src/helpers/NetworkPrefs.cpp +++ b/src/helpers/NetworkPrefs.cpp @@ -3,6 +3,10 @@ #include #include +#if defined(ESP_PLATFORM) + #include +#endif + namespace { struct LegacyWebPrefsV1 { @@ -47,6 +51,41 @@ bool loadLegacyWebWifiPrefs(FILESYSTEM* fs, NetworkPrefs& prefs) { return true; } +#if defined(ESP_PLATFORM) +bool loadNvsNetworkPrefs(NetworkPrefs& prefs) { + Preferences nvs; + if (!nvs.begin("eastmesh-net", true)) { + return false; + } + NetworkPrefs stored{}; + const size_t read = nvs.getBytes("prefs", &stored, sizeof(stored)); + nvs.end(); + if (read != sizeof(stored) || stored.magic != NetworkPrefsStore::magicValue()) { + return false; + } + prefs = stored; + return true; +} + +bool saveNvsNetworkPrefs(const NetworkPrefs& prefs) { + Preferences nvs; + if (!nvs.begin("eastmesh-net", false)) { + return false; + } + const size_t written = nvs.putBytes("prefs", &prefs, sizeof(prefs)); + nvs.end(); + return written == sizeof(prefs); +} +#else +bool loadNvsNetworkPrefs(NetworkPrefs&) { + return false; +} + +bool saveNvsNetworkPrefs(const NetworkPrefs&) { + return true; +} +#endif + } // namespace void NetworkPrefsStore::setDefaults(NetworkPrefs& prefs) { @@ -62,10 +101,15 @@ bool NetworkPrefsStore::load(FILESYSTEM* fs, NetworkPrefs& prefs, const char* legacy_wifi_pwd) { setDefaults(prefs); if (fs == nullptr) { + loadNvsNetworkPrefs(prefs); return false; } if (!fs->exists(kFilename)) { + if (loadNvsNetworkPrefs(prefs)) { + save(fs, prefs); + return true; + } prefs.wifi_powersave = legacy_wifi_powersave <= 2 ? legacy_wifi_powersave : 0; if (legacy_wifi_ssid != nullptr) { StrHelper::strncpy(prefs.wifi_ssid, legacy_wifi_ssid, sizeof(prefs.wifi_ssid)); @@ -95,6 +139,10 @@ bool NetworkPrefsStore::load(FILESYSTEM* fs, NetworkPrefs& prefs, if (!ok || persisted.magic != kMagic) { fs->remove(kFilename); + if (loadNvsNetworkPrefs(prefs)) { + save(fs, prefs); + return true; + } save(fs, prefs); return false; } @@ -106,15 +154,32 @@ bool NetworkPrefsStore::load(FILESYSTEM* fs, NetworkPrefs& prefs, if (prefs.wifi_channel < 1 || prefs.wifi_channel > 14) { prefs.wifi_channel = 0; } + bool repaired = false; + if (prefs.wifi_ssid[0] == 0 && legacy_wifi_ssid != nullptr && legacy_wifi_ssid[0] != 0) { + StrHelper::strncpy(prefs.wifi_ssid, legacy_wifi_ssid, sizeof(prefs.wifi_ssid)); + prefs.wifi_channel = 0; + repaired = true; + } + if (prefs.wifi_pwd[0] == 0 && legacy_wifi_pwd != nullptr && legacy_wifi_pwd[0] != 0) { + StrHelper::strncpy(prefs.wifi_pwd, legacy_wifi_pwd, sizeof(prefs.wifi_pwd)); + prefs.wifi_channel = 0; + repaired = true; + } + if (repaired) { + save(fs, prefs); + } else { + saveNvsNetworkPrefs(prefs); + } return true; } bool NetworkPrefsStore::save(FILESYSTEM* fs, const NetworkPrefs& prefs) { + const bool nvs_ok = saveNvsNetworkPrefs(prefs); if (fs == nullptr) { - return false; + return nvs_ok; } if (fs->exists(kFilename) && !fs->remove(kFilename)) { - return false; + return nvs_ok; } #if defined(RP2040_PLATFORM) File file = fs->open(kFilename, "w"); @@ -122,9 +187,9 @@ bool NetworkPrefsStore::save(FILESYSTEM* fs, const NetworkPrefs& prefs) { File file = fs->open(kFilename, "w", true); #endif if (!file) { - return false; + return nvs_ok; } bool ok = file.write(reinterpret_cast(&prefs), sizeof(prefs)) == sizeof(prefs); file.close(); - return ok; + return ok && nvs_ok; } diff --git a/src/helpers/NetworkPrefs.h b/src/helpers/NetworkPrefs.h index 3f041392..9eb9096b 100644 --- a/src/helpers/NetworkPrefs.h +++ b/src/helpers/NetworkPrefs.h @@ -20,6 +20,7 @@ public: const char* legacy_wifi_ssid = nullptr, const char* legacy_wifi_pwd = nullptr); static bool save(FILESYSTEM* fs, const NetworkPrefs& prefs); + static constexpr uint32_t magicValue() { return kMagic; } private: static constexpr uint32_t kMagic = 0x4E455450; diff --git a/src/helpers/NetworkService.cpp b/src/helpers/NetworkService.cpp index 65ed6103..ae72c030 100644 --- a/src/helpers/NetworkService.cpp +++ b/src/helpers/NetworkService.cpp @@ -327,9 +327,21 @@ void NetworkService::ensureWifi(bool network_required) { WiFi.disconnect(false, false); _last_wifi_attempt = 0; } - if (_last_wifi_attempt != 0 && status == WL_IDLE_STATUS && now_ms - _last_wifi_attempt < kWifiConnectTimeoutMillis) { + if (_last_wifi_attempt != 0 && now_ms - _last_wifi_attempt < kWifiConnectTimeoutMillis) { return; } + if (_last_wifi_attempt != 0) { + Serial.printf("[BOOT] wifi timeout t=%lu code=%d retry\n", + static_cast(millis()), + static_cast(status)); + WiFi.disconnect(false, false); + WiFi.mode(WIFI_OFF); + delay(100); + _wifi_started = false; + _sntp_started = false; + _have_time_sync = false; + _last_wifi_status = -1; + } if (now_ms - _last_wifi_attempt < kWifiRetryMillis) { return; } diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index a2ec73f8..69d897fe 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -274,6 +274,73 @@ static bool writeI2CRegister8(TwoWire* wire, uint8_t addr, uint8_t reg, uint8_t return wire->endTransmission(true) == 0; } +#if defined(TBEAM_SUPREME_SX1262) && ENV_INCLUDE_BME280 +static void recoverBME280I2CBus(TwoWire* wire) { + if (wire == nullptr) { + return; + } +#if defined(ESP32) && defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) + if (wire == &Wire) { + Wire.end(); + delay(5); + Wire.begin(PIN_BOARD_SDA, PIN_BOARD_SCL); + Wire.setClock(100000); + delay(5); + } +#endif +} + +static bool readI2CRegister8WithRetry(TwoWire* wire, uint8_t addr, uint8_t reg, uint8_t* value) { + for (uint8_t attempt = 0; attempt < 20; attempt++) { + if (readI2CRegister8(wire, addr, reg, value)) { + return true; + } + if (attempt == 4) { + recoverBME280I2CBus(wire); + } + delay(5); + } + MESH_DEBUG_PRINTLN("BME280 read reg 0x%02X failed", reg); + return false; +} + +static bool readI2CBufferWithRetry(TwoWire* wire, uint8_t addr, uint8_t reg, uint8_t* buffer, size_t len) { + for (uint8_t attempt = 0; attempt < 20; attempt++) { + if (readI2CBuffer(wire, addr, reg, buffer, len)) { + return true; + } + if (attempt == 4) { + recoverBME280I2CBus(wire); + } + delay(5); + } + MESH_DEBUG_PRINTLN("BME280 read buffer reg 0x%02X len=%u failed", reg, (unsigned)len); + return false; +} + +static bool writeI2CRegister8WithRetry(TwoWire* wire, uint8_t addr, uint8_t reg, uint8_t value) { + if (wire == nullptr) { + return false; + } + uint8_t last_error = 0xFF; + for (uint8_t attempt = 0; attempt < 20; attempt++) { + wire->beginTransmission(addr); + wire->write(reg); + wire->write(value); + last_error = wire->endTransmission(true); + if (last_error == 0) { + return true; + } + if (attempt == 4) { + recoverBME280I2CBus(wire); + } + delay(5); + } + MESH_DEBUG_PRINTLN("BME280 write reg 0x%02X failed err=%u", reg, last_error); + return false; +} +#endif + uint8_t boardDetectedI2CSensorAddress(const char* name) __attribute__((weak)); uint8_t boardDetectedI2CSensorAddress(const char* name) { (void)name; @@ -363,8 +430,8 @@ static int16_t s16le(const uint8_t* p) { static bool read_bme280_calibration(TwoWire* wire, uint8_t addr) { uint8_t calib1[26] = {}; uint8_t calib2[7] = {}; - if (!readI2CBuffer(wire, addr, 0x88, calib1, sizeof(calib1)) || - !readI2CBuffer(wire, addr, 0xE1, calib2, sizeof(calib2))) { + if (!readI2CBufferWithRetry(wire, addr, 0x88, calib1, sizeof(calib1)) || + !readI2CBufferWithRetry(wire, addr, 0xE1, calib2, sizeof(calib2))) { return false; } @@ -430,8 +497,13 @@ static float compensate_bme280_humidity(int32_t adc_H) { static uint8_t init_bme280(TwoWire* wire, uint8_t addr) { uint8_t chip_id = 0; - const bool chip_id_ok = readI2CRegister8(wire, addr, 0xD0, &chip_id); - if (!chip_id_ok || chip_id != 0x60 || !read_bme280_calibration(wire, addr)) { + const bool chip_id_ok = readI2CRegister8WithRetry(wire, addr, 0xD0, &chip_id); + if (!chip_id_ok || chip_id != 0x60) { + MESH_DEBUG_PRINTLN("BME280 init failed chip ID 0x%02X", chip_id); + return 0; + } + if (!read_bme280_calibration(wire, addr)) { + MESH_DEBUG_PRINTLN("BME280 init failed calibration read"); return 0; } BME280Wire = wire; @@ -445,21 +517,57 @@ static void query_bme280(uint8_t ch, uint8_t, CayenneLPP& lpp) { if (BME280Wire == nullptr || BME280Address == 0) { return; } - if (!writeI2CRegister8(BME280Wire, BME280Address, 0xF2, 0x01) || - !writeI2CRegister8(BME280Wire, BME280Address, 0xF4, 0x25)) { // temp/pressure x1, forced mode + recoverBME280I2CBus(BME280Wire); + if (!writeI2CRegister8WithRetry(BME280Wire, BME280Address, 0xF2, 0x01) || + !writeI2CRegister8WithRetry(BME280Wire, BME280Address, 0xF4, 0x25)) { // temp/pressure x1, forced mode + MESH_DEBUG_PRINTLN("BME280 query failed to start forced measurement"); return; } - delay(10); + + uint8_t status = 0; + bool measurement_done = false; + for (uint8_t attempt = 0; attempt < 20; attempt++) { + if (!readI2CRegister8WithRetry(BME280Wire, BME280Address, 0xF3, &status)) { + MESH_DEBUG_PRINTLN("BME280 query failed status read"); + return; + } + if ((status & 0x08) == 0) { + measurement_done = true; + break; + } + delay(2); + } + if (!measurement_done) { + MESH_DEBUG_PRINTLN("BME280 query timed out waiting for measurement"); + return; + } + uint8_t data[8] = {}; - if (!readI2CBuffer(BME280Wire, BME280Address, 0xF7, data, sizeof(data))) { + if (!readI2CBufferWithRetry(BME280Wire, BME280Address, 0xF7, data, sizeof(data))) { + MESH_DEBUG_PRINTLN("BME280 query failed data read"); return; } const int32_t adc_P = ((int32_t)data[0] << 12) | ((int32_t)data[1] << 4) | (data[2] >> 4); const int32_t adc_T = ((int32_t)data[3] << 12) | ((int32_t)data[4] << 4) | (data[5] >> 4); const int32_t adc_H = ((int32_t)data[6] << 8) | data[7]; + if (adc_P == 0 || adc_T == 0) { + MESH_DEBUG_PRINTLN("BME280 query returned invalid raw sample p=%ld t=%ld h=%ld", + (long)adc_P, + (long)adc_T, + (long)adc_H); + return; + } const float temperature = compensate_bme280_temperature(adc_T); const float pressure_pa = compensate_bme280_pressure(adc_P); const float humidity = compensate_bme280_humidity(adc_H); + if (pressure_pa <= 0.0f) { + MESH_DEBUG_PRINTLN("BME280 query returned invalid pressure %.2f", pressure_pa); + return; + } + MESH_DEBUG_PRINTLN("BME280 sample temp=%.2f humidity=%.2f pressure=%.2f", + temperature, + humidity, + pressure_pa / 100.0f); lpp.addTemperature(ch, temperature); lpp.addRelativeHumidity(ch, humidity); lpp.addBarometricPressure(ch, pressure_pa / 100.0f); @@ -865,6 +973,7 @@ bool EnvironmentSensorManager::begin() { } } + MESH_DEBUG_PRINTLN("Environment sensors active: %d", _active_sensor_count); return true; } diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp index f383bb00..2331c0f3 100644 --- a/src/helpers/ui/SH1106Display.cpp +++ b/src/helpers/ui/SH1106Display.cpp @@ -11,7 +11,12 @@ bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr) bool SH1106Display::begin() { - return display.begin(DISPLAY_ADDRESS, true) && i2c_probe(Wire, DISPLAY_ADDRESS); + const bool started = display.begin(DISPLAY_ADDRESS, true); +#if defined(ESP32) && defined(TBEAM_SUPREME_SX1262) && defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) + Wire.begin(PIN_BOARD_SDA, PIN_BOARD_SCL); + Wire.setClock(100000); +#endif + return started && i2c_probe(Wire, DISPLAY_ADDRESS); } void SH1106Display::turnOn() diff --git a/src/helpers/web/WebPanelServer.cpp b/src/helpers/web/WebPanelServer.cpp index 1cf652cc..b41d22a3 100644 --- a/src/helpers/web/WebPanelServer.cpp +++ b/src/helpers/web/WebPanelServer.cpp @@ -2497,9 +2497,30 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( const sensors = summaryPayload && summaryPayload.sensors ? summaryPayload.sensors : null; const gpsEnabled = !!(sensors && sensors.gps_enabled === true); const order = ["battery", "memory", "signal", "noise_floor", "packets"]; + if (sensors && Number.isFinite(sensors.supply_voltage_v)) { + order.push("voltage"); + } + if (sensors && Number.isFinite(sensors.sensor_temp_c)) { + order.push("sensor_temp"); + } + if (sensors && Number.isFinite(sensors.humidity_pct)) { + order.push("humidity"); + } + if (sensors && Number.isFinite(sensors.pressure_hpa)) { + order.push("pressure"); + } + if (sensors && Number.isFinite(sensors.pressure_altitude_m)) { + order.push("pressure_altitude"); + } + if (sensors && Number.isFinite(sensors.mcu_temp_c)) { + order.push("mcu_temp"); + } if (gpsEnabled) { order.push("gps_satellites"); } + if (sensors && Number.isFinite(sensors.gps_altitude_m)) { + order.push("gps_altitude"); + } return order; } function initTrendCards(seriesOrder) { @@ -2706,10 +2727,19 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( return false; } try { - await fetchJson("/api/session"); + const res = await fetch("/api/session", { headers:{ "X-Auth-Token": token } }); + if (res.status === 401) { + redirectToLogin(); + return false; + } + if (!res.ok) { + statusEl.textContent = "Web panel reconnecting..."; + return null; + } return true; - } catch (_) { - return false; + } catch (err) { + statusEl.textContent = "Web panel reconnecting..."; + return null; } } function getLetsmeshMode() { @@ -3171,10 +3201,14 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( } mqttIataLoaded = false; refreshMqttIataWarning(); - if (!await validateSession()) { + const sessionOk = await validateSession(); + if (sessionOk === false) { return; } showAuthedUi(true); + if (sessionOk === null) { + return; + } if (isStatsPage) { try { await loadStatsPage(generation);