From 85706edcf85e1e55cda8c266c5e35905d07db237 Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Tue, 8 Sep 2026 10:21:45 +0200 Subject: [PATCH] fix(ble): raise rebootFlag after a live config write A BLE config write (CMD_CONFIG_WRITE) reloads the saved config but never touched rebootFlag, so only the client that just wrote it saw the change. Anyone else like Home Assistant didn't get the signal to reconnect and re-read the config. --- include/opendisplay_structs.h | 2 +- src/communication.cpp | 10 ++++++++++ src/communication.h | 7 +++++++ src/device_control.cpp | 1 - src/display_service.cpp | 3 +-- src/main.cpp | 15 +++++++++++++++ src/main.h | 5 +++-- tools/od-device-cli.py | 2 +- 8 files changed, 38 insertions(+), 7 deletions(-) diff --git a/include/opendisplay_structs.h b/include/opendisplay_structs.h index f8b0cb2..4df1582 100644 --- a/include/opendisplay_structs.h +++ b/include/opendisplay_structs.h @@ -1246,7 +1246,7 @@ OD_STATIC_ASSERT(sizeof(struct AuthProof) == 32, "AuthProof wire size"); /* MsdAdvertisement.status @bits MsdStatusBits. bit0 is the 9th (MSB) bit of the * 10-bit battery voltage; bits 4-7 are a free-running main-loop nibble counter. */ #define OD_MSD_STATUS_BATTERY_VOLTAGE_BIT8 (1u << 0) /* @doc "high bit of the 10-bit battery voltage (units of 10 mV); combine with battery_voltage_low" */ -#define OD_MSD_STATUS_REBOOT_FLAG (1u << 1) /* @doc "device rebooted since last read" */ +#define OD_MSD_STATUS_REBOOT_FLAG (1u << 1) /* @doc "device state changed since last read (reboot or live config write)" */ #define OD_MSD_STATUS_CONNECTION_REQUESTED (1u << 2) /* @doc "device is requesting a connection" */ #define OD_MSD_STATUS_ENCRYPTION_ENABLED (1u << 3) /* @doc "application-layer encryption active (encryption_enabled and non-zero master key)" */ #define OD_MSD_STATUS_RESERVED_3 (1u << 3) /* @deprecated @doc "legacy name for OD_MSD_STATUS_ENCRYPTION_ENABLED (same bit)" */ diff --git a/src/communication.cpp b/src/communication.cpp index c6e4a8b..522e3a2 100644 --- a/src/communication.cpp +++ b/src/communication.cpp @@ -595,6 +595,12 @@ void handleWriteConfig(uint8_t* data, uint16_t len) { reloadConfigAfterSave(); } sendResponse(ok ? responseOk : responseErr, 4); + if (ok) { + // Runs after the ACK: unlike the reload above, this only affects what + // OTHER scanners see, and updatemsdata() can block tens of ms on a + // sensor-cache miss -- no reason to make the writer's own ACK wait on it. + notifyConfigChangedOverBle(); + } } void handleClearConfig(void) { @@ -644,6 +650,10 @@ void handleWriteConfigChunk(uint8_t* data, uint16_t len) { reloadConfigAfterSave(); } sendResponse(saved ? ok : err, 4); + if (saved) { + // See handleWriteConfig(): deferred past the ACK on purpose. + notifyConfigChangedOverBle(); + } resetChunkedWriteState(); } else { uint8_t ackResponse[] = {RESP_ACK, RESP_CONFIG_CHUNK, 0x00, 0x00}; diff --git a/src/communication.h b/src/communication.h index 9cc17b6..dba946b 100644 --- a/src/communication.h +++ b/src/communication.h @@ -9,6 +9,7 @@ uint16_t calculateCRC16CCITT(uint8_t* data, uint32_t len); uint8_t getFirmwareMajor(); uint8_t getFirmwareMinor(); uint8_t getFirmwarePatch(); +uint8_t getRebootFlag(); // 1 after reboot or a live config write, cleared to 0 on BLE connection const char* getFirmwareShaString(); void handleFirmwareVersion(); void handleReadMSD(); @@ -80,4 +81,10 @@ void requestTransferSessionCleanup(void); /// re-arm on both targets (see BleTransport::restartsAdvertisingOnDisconnect). void requestAdvertisingRestart(void); +/// Set rebootFlag and refresh the advertised MSD payload immediately after a +/// config write. Unlike the two requests above this is not deferred: it is +/// safe to run synchronously from the command handler, the same way +/// updatemsdata() already runs synchronously from button/sensor call sites. +void notifyConfigChangedOverBle(void); + #endif diff --git a/src/device_control.cpp b/src/device_control.cpp index dd7bb19..f5867a8 100644 --- a/src/device_control.cpp +++ b/src/device_control.cpp @@ -29,7 +29,6 @@ extern "C" void bootloader_util_app_start(uint32_t start_addr); #include "ble_transport.h" -extern uint8_t rebootFlag; extern struct GlobalConfig globalConfig; extern uint8_t activeLedInstance; extern bool ledFlashActive; diff --git a/src/display_service.cpp b/src/display_service.cpp index 592e5ec..29d885c 100644 --- a/src/display_service.cpp +++ b/src/display_service.cpp @@ -65,7 +65,6 @@ extern BBEPDISP bbep; extern struct GlobalConfig globalConfig; extern uint8_t msd_payload[16]; extern uint8_t dynamicreturndata[11]; -extern uint8_t rebootFlag; extern uint8_t activeLedInstance; extern bool connectionRequested; extern uint8_t mloopcounter; @@ -1868,7 +1867,7 @@ void updatemsdata(){ uint8_t temperatureByte = (uint8_t)tempEncoded; uint8_t batteryVoltageLowByte = (uint8_t)(batteryVoltage10mv & 0xFF); uint8_t statusByte = (((batteryVoltage10mv >> 8) & 0x01) ? OD_MSD_STATUS_BATTERY_VOLTAGE_BIT8 : 0) | - (rebootFlag ? OD_MSD_STATUS_REBOOT_FLAG : 0) | + (getRebootFlag() ? OD_MSD_STATUS_REBOOT_FLAG : 0) | (connectionRequested ? OD_MSD_STATUS_CONNECTION_REQUESTED : 0) | (isEncryptionEnabled() ? OD_MSD_STATUS_ENCRYPTION_ENABLED : 0) | (((uint8_t)(mloopcounter << OD_MSD_STATUS_MAIN_LOOP_COUNTER_SHIFT)) & OD_MSD_STATUS_MAIN_LOOP_COUNTER_MASK); diff --git a/src/main.cpp b/src/main.cpp index 1f1b322..d1b6194 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -321,6 +321,10 @@ uint32_t getDeepSleepCount() { #endif } +uint8_t getRebootFlag() { + return rebootFlag; +} + // Deferred work, serviced by loop(). File-static on purpose: these encode // application policy, so nothing outside this file reads them, and the two that // other translation units need to RAISE do so through the request functions @@ -360,6 +364,17 @@ void requestAdvertisingRestart(void) { s_advertisingRestartPending = true; } +void notifyConfigChangedOverBle(void) { + // Mirrors the boot-time meaning of rebootFlag: a scanning hub sees it set in + // the advertised MSD payload and knows to reconnect and re-read config + // instead of trusting a cached copy (e.g. stale color_scheme). A config + // write changes the same on-device state a reboot would, so it must raise + // the same flag -- otherwise a live BLE write is invisible to anyone who + // isn't the client that just wrote it. + rebootFlag = 1; + updatemsdata(); +} + #ifdef TARGET_ESP32 // Minimum awake window (first boot / button wake). A floor layered UNDER the // quiet-window logic, not a replacement: sleep requires both the existing diff --git a/src/main.h b/src/main.h index 430b3c1..78da264 100644 --- a/src/main.h +++ b/src/main.h @@ -109,9 +109,9 @@ uint8_t mloopcounter = 0; #ifdef TARGET_ESP32 // Persists across deep sleep so a wake is not mistaken for a reboot. Re-armed // on the boot-screen path in setup(), which is the only path a real reset takes. -RTC_DATA_ATTR uint8_t rebootFlag = 1; // Set to 1 after reboot, cleared to 0 after BLE connection +RTC_DATA_ATTR uint8_t rebootFlag = 1; // Set to 1 on reboot or config write, cleared to 0 on BLE connection #else -uint8_t rebootFlag = 1; // Set to 1 after reboot, cleared to 0 after BLE connection +uint8_t rebootFlag = 1; // Set to 1 on reboot or config write, cleared to 0 on BLE connection #endif uint8_t connectionRequested = 0; // Reserved for future features (connection requested flag) uint8_t dynamicreturndata[11] = {0}; // Dynamic return data blocks (bytes 2-12 in advertising payload) @@ -248,6 +248,7 @@ uint8_t getFirmwareMajor(); uint8_t getFirmwareMinor(); uint8_t getFirmwarePatch(); uint32_t getDeepSleepCount(); // RTC-persisted wake cycle count on ESP32; always 0 on nRF52840 +uint8_t getRebootFlag(); // 1 after reboot or a live config write, cleared to 0 on BLE connection float readBatteryVoltage(); // Returns battery voltage in volts, or -1.0 if not configured float readChipTemperature(); // Returns chip temperature in degrees Celsius int getplane(); diff --git a/tools/od-device-cli.py b/tools/od-device-cli.py index 27e5107..11fbfa5 100755 --- a/tools/od-device-cli.py +++ b/tools/od-device-cli.py @@ -1051,7 +1051,7 @@ def cmd_read_msd(args: argparse.Namespace) -> int: voltage = info["battery_voltage_v"] print(f"Battery voltage: {f'{voltage:.2f} V' if voltage is not None else 'unknown (raw=0 - unconfigured or not yet sampled)'}") print(f"Chip temperature: {info['temperature_c']:.1f} C") - print(f"Reboot flag: {info['reboot_flag']}") + print(f"State changed (reboot/config): {info['reboot_flag']}") print(f"Connection requested: {info['connection_requested']}") print(f"Loop counter: {info['loop_counter']}") print(f"Dynamic data (bytes 2-12): {info['dynamic_data_hex']}")