From cb27a4061ed699e7fc0756430f4120a552cd4120 Mon Sep 17 00:00:00 2001 From: Jonas Niesner Date: Wed, 26 Aug 2026 14:49:49 +0200 Subject: [PATCH 1/5] fix 4.2 graying out aft r powerdown --- platformio.ini | 6 ++++-- src/display_service.cpp | 21 ++++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/platformio.ini b/platformio.ini index effae4e..36b3bf8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -53,10 +53,12 @@ default_envs = [env] lib_deps = ; Pinned: bb_epaper has no releases, so an unpinned URL means CI and local - ; builds get whatever HEAD was at install time. 5dccfbb is "Added support for + ; builds get whatever HEAD was at install time. Full SHA required: PlatformIO + ; shallow-fetches the #ref (`git fetch --depth=1 origin `), and GitHub + ; does not resolve abbreviated SHAs as refs. 5dccfbbf… is "Added support for ; the Seeed reTerminal E1004 and its 13.3 Spectra6 1200x1600 panel" -- the ; commit that landed E1004 upstream, replacing the limengdu fork pin. - https://github.com/bitbank2/bb_epaper.git#5dccfbb + https://github.com/bitbank2/bb_epaper.git#5dccfbbf553a9b0fe2547cbc4e60138e1ff2fb43 h2zero/NimBLE-Arduino@^2.5.0 extra_scripts = pre:scripts/factory_config_gen.py diff --git a/src/display_service.cpp b/src/display_service.cpp index 0ed04bd..c5fa58c 100644 --- a/src/display_service.cpp +++ b/src/display_service.cpp @@ -337,6 +337,24 @@ static void pwrmgmLockGive(void) { __atomic_store_n(&pwrmgmLock, 0, __ATOMIC_RELEASE); } +// After controller power-off / deep-sleep, wait before cutting VDD so board boost +// caps and VCOM can bleed. 50 ms was too short on EP42B (panel_ic 2 / SSD16xx): +// refresh looked correct, then the image darkened in over the following minutes. +static const uint16_t EPD_POST_SLEEP_BLEED_MS = 200; + +// SSD16xx bbepSleep() only sends deep-sleep; it does not run the analog/HV +// shutdown. Force EOPT discharge frames + the GoodDisplay/GxEPD2 power-off +// sequence (enable clock → disable analog → disable OSC) while SPI is still up. +static void epdSsd16xxPowerOffDischarge(void) { + if (bbep.chip_type != BBEP_CHIP_SSD16xx) return; + if (!bbep.is_awake) return; + bbepCMD2(&bbep, 0x3F, 0x22); // EOPT: TFT discharge frames + sequenced VCOM/HV + bbepCMD2(&bbep, SSD1608_DISP_CTRL2, 0x83); + bbepWriteCmd(&bbep, SSD1608_MASTER_ACTIVATE); + odWatchdogFeed(); + bbepWaitBusy(&bbep); +} + // Lock-held core (callers must hold pwrmgmLock). Split out so Release/Tick can // power off without re-taking the non-recursive lock. static void epdSessionForceOffLocked(void) { @@ -355,8 +373,9 @@ static void epdSessionForceOffLocked(void) { #endif } else { odWatchdogFeed(); // reload before entering bb_epaper (may block ~240 s) + epdSsd16xxPowerOffDischarge(); bbepSleep(&bbep, 1); - delay(50); + delay(EPD_POST_SLEEP_BLEED_MS); } pwrmgm(false); // -> PWR_OFF, clears deadline epdPlanesPrepared = false; From 87afe71490ec86ff06ae8cecb5864efad82737c4 Mon Sep 17 00:00:00 2001 From: Jonas Niesner Date: Wed, 2 Sep 2026 17:35:55 +0200 Subject: [PATCH 2/5] fix: button feedbck, more reliable buttons after deep sleep and ned advertising data features --- include/opendisplay_structs.h | 16 ++++- src/ble_transport_esp32.cpp | 51 ++++++++++++++- src/buzzer_control.cpp | 27 ++++++++ src/buzzer_control.h | 1 + src/device_control.cpp | 120 ++++++++++++++++++++++++++++------ src/device_control.h | 4 ++ src/display_service.cpp | 7 +- src/main.cpp | 7 +- src/wake_button.cpp | 37 +++++++++++ src/wake_button.h | 5 ++ 10 files changed, 248 insertions(+), 27 deletions(-) diff --git a/include/opendisplay_structs.h b/include/opendisplay_structs.h index 7cc80c0..5c519b7 100644 --- a/include/opendisplay_structs.h +++ b/include/opendisplay_structs.h @@ -67,6 +67,13 @@ * new version heading on each bump -- see AGENT INSTRUCTIONS below) * -------------------------------------------------------------------------- * Unreleased (since 2.0) + * - LedFlags bit4: OD_LED_FLAG_BUTTON_PRESS (short LED flash on physical + * button press / button-wake synthetic click; default off). + * - BuzzerFlags bit1: OD_BUZZER_FLAG_BUTTON_PRESS (short chirp on physical + * button press / button-wake synthetic click; default off). + * - MsdStatusBits bit3: OD_MSD_STATUS_ENCRYPTION_ENABLED (1 = app-layer + * encryption active). Firmware sets it when encryption_enabled and master + * key are non-zero; dynamic[11] is all 0xFF when no display is configured. * - Doc-only: fixed two comment shapes the codegen parser mis-read and added * the CODEGEN AUTHORING RULES banner section to prevent recurrence. Split the * combined BusFlags/PinBitmap @bits comment into one comment per group; folded @@ -735,11 +742,12 @@ enum LedType { OD_LED_TYPE_FOUR_SEPARATE = 3 /**< @doc "four separate LEDs" */ }; -/* LedConfig.led_flags @bits LedFlags (bits 4-7 reserved). */ +/* LedConfig.led_flags @bits LedFlags (bits 5-7 reserved). */ #define OD_LED_FLAG_LED1_INVERT (1u << 0) /* @doc "invert LED channel 1 polarity" */ #define OD_LED_FLAG_LED2_INVERT (1u << 1) /* @doc "invert LED channel 2 polarity" */ #define OD_LED_FLAG_LED3_INVERT (1u << 2) /* @doc "invert LED channel 3 polarity" */ #define OD_LED_FLAG_LED4_INVERT (1u << 3) /* @doc "invert LED channel 4 polarity" */ +#define OD_LED_FLAG_BUTTON_PRESS (1u << 4) /* @doc "short flash on physical button press (incl. button-wake synthetic click); default off" */ /** @struct LedConfig @packet 0x21 @repeatable max=4 * @doc "LED channel pins + invert flags. Up to 4 instances. 22 bytes. NOTE: the @@ -963,8 +971,9 @@ OD_STATIC_ASSERT(sizeof(struct TouchController) == 32, "TouchController wire siz * 0x29 buzzer (config.yaml packet name: passive_buzzer) * ----------------------------------------------------------------------- */ -/* BuzzerConfig.flags @bits BuzzerFlags (bits 1-7 reserved). */ +/* BuzzerConfig.flags @bits BuzzerFlags (bits 2-7 reserved). */ #define OD_BUZZER_FLAG_ENABLE_ACTIVE_HIGH (1u << 0) /* @doc "enable pin is active-high when set; otherwise active-low" */ +#define OD_BUZZER_FLAG_BUTTON_PRESS (1u << 1) /* @doc "short chirp on physical button press (incl. button-wake synthetic click); default off" */ /** @struct BuzzerConfig @packet 0x29 @repeatable max=4 * @doc "Buzzer (passive piezo, PWM-driven). Up to 4 instances. 32 bytes. The tone @@ -1225,7 +1234,8 @@ OD_STATIC_ASSERT(sizeof(struct AuthProof) == 32, "AuthProof wire size"); #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_CONNECTION_REQUESTED (1u << 2) /* @doc "device is requesting a connection" */ -#define OD_MSD_STATUS_RESERVED_3 (1u << 3) /* @reserved @doc "reserved; must be 0 (placeholder name for a future status flag; sits between the flags and the bits 4-7 counter)" */ +#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 OD_MSD_STATUS_ENCRYPTION_ENABLED /* legacy doc name */ #define OD_MSD_STATUS_MAIN_LOOP_COUNTER_SHIFT 4u /* @doc "bits 4-7: free-running main-loop nibble counter (liveness)" */ #define OD_MSD_STATUS_MAIN_LOOP_COUNTER_MASK 0xF0u /* @doc "mask for the bits 4-7 main-loop counter nibble" */ diff --git a/src/ble_transport_esp32.cpp b/src/ble_transport_esp32.cpp index 2df8d80..1cbc43a 100644 --- a/src/ble_transport_esp32.cpp +++ b/src/ble_transport_esp32.cpp @@ -44,6 +44,30 @@ static volatile uint16_t s_disconnectReason = 0; static volatile uint32_t s_connectedWord = 0; // identity of the last connect static volatile uint32_t s_disconnectedWord = 0; // identity of the last disconnect +static uint32_t s_advBoostUntil = 0; +static constexpr uint32_t ESP_ADV_BOOST_MS = 3000u; +static constexpr uint16_t ESP_ADV_INTERVAL_MIN = 0x0100u; // 160 ms +static constexpr uint16_t ESP_ADV_INTERVAL_MAX = 0x0640u; // 1000 ms +static constexpr uint16_t ESP_ADV_BOOST_MIN = 0x0020u; // 20 ms +static constexpr uint16_t ESP_ADV_BOOST_MAX = 0x0030u; // 30 ms + +static void applyAdvInterval(BLEAdvertising* pAdvertising, bool fast) { + if (pAdvertising == nullptr) { + return; + } + if (fast) { + pAdvertising->setMinInterval(ESP_ADV_BOOST_MIN); + pAdvertising->setMaxInterval(ESP_ADV_BOOST_MAX); + } else { + pAdvertising->setMinInterval(ESP_ADV_INTERVAL_MIN); + pAdvertising->setMaxInterval(ESP_ADV_INTERVAL_MAX); + } +} + +static bool advBoostActive(uint32_t now) { + return s_advBoostUntil != 0u && now < s_advBoostUntil; +} + // --- the instance table (CONNECTION_POLICY R3 requirement 5) ----------------- // Sized by the connection cap. CONFIG_BT_NIMBLE_MAX_CONNECTIONS is 3 in the // precompiled sdkconfig.h for S3/C3/C6 and absent for classic ESP32 (NimBLE's own @@ -381,6 +405,7 @@ void BleTransport::startAdvertising() { // name). Scan response is off by default in NimBLE 2.x, so no // enableScanResponse() needed. pAdvertising->setAdvertisementData(s_advertisementData); + applyAdvInterval(pAdvertising, advBoostActive(millis())); s_server->getAdvertising()->start(); od_log_info("=== BLE advertising started successfully ==="); } @@ -550,6 +575,7 @@ void BleTransport::setManufacturerData(const uint8_t* msd, uint8_t len) { // enableScanResponse()/setPreferredParams() reset NimBLE's custom-data flag // and would make start() drop this manufacturer-data payload. pAdvertising->setAdvertisementData(fresh); + applyAdvInterval(pAdvertising, advBoostActive(millis())); delay(50); pAdvertising->start(); } @@ -584,11 +610,32 @@ void BleTransport::requestFastLink() { } void BleTransport::boostAdvertising() { - // No-op: the temporary fast-advertising interval is nRF-only today. + s_advBoostUntil = millis() + ESP_ADV_BOOST_MS; } void BleTransport::tick() { - // No-op: nothing periodic to restore, since boostAdvertising() is a no-op. + static bool was_boosted = false; + const uint32_t now = millis(); + const bool boosting = advBoostActive(now); + if (boosting) { + was_boosted = true; + return; + } + if (!was_boosted || s_server == nullptr || connectedCount() > 0) { + was_boosted = false; + s_advBoostUntil = 0; + return; + } + was_boosted = false; + s_advBoostUntil = 0; + BLEAdvertising* pAdvertising = s_server->getAdvertising(); + if (pAdvertising == nullptr) { + return; + } + pAdvertising->stop(); + applyAdvInterval(pAdvertising, false); + delay(50); + pAdvertising->start(); } bool BleTransport::eventPending() const { diff --git a/src/buzzer_control.cpp b/src/buzzer_control.cpp index 7e0849c..8ac8379 100644 --- a/src/buzzer_control.cpp +++ b/src/buzzer_control.cpp @@ -385,3 +385,30 @@ void passiveBuzzerPowerOffAlert(void) { buzzer_set_enable(b, false); buzzer_drive_off(b); } + +void passiveBuzzerButtonPressAlert(void) { + buzzer_stop_internal(); + const BuzzerConfig* b = nullptr; + for (uint8_t i = 0; i < globalConfig.passive_buzzer_count; i++) { + const BuzzerConfig* cand = &globalConfig.passive_buzzers[i]; + const uint8_t pin = cand->drive_pin; + if (pin == 0 || pin == 0xFF) { + continue; + } + if ((cand->flags & OD_BUZZER_FLAG_BUTTON_PRESS) == 0u) { + continue; + } + b = cand; + break; + } + if (!b) { + return; + } + const uint32_t centihz = buzzer_index_to_centihz(nA5); + buzzer_set_enable(b, true); + buzzer_hw_tone_start(b->drive_pin, centihz, b->duty_percent); + delay(60); + buzzer_hw_tone_stop(b->drive_pin); + buzzer_set_enable(b, false); + buzzer_drive_off(b); +} diff --git a/src/buzzer_control.h b/src/buzzer_control.h index 1bb4fdd..eb8d406 100644 --- a/src/buzzer_control.h +++ b/src/buzzer_control.h @@ -94,6 +94,7 @@ enum BuzzerNote : uint8_t { void initPassiveBuzzers(void); void handleBuzzerActivate(uint8_t* data, uint16_t len); void passiveBuzzerPowerOffAlert(void); +void passiveBuzzerButtonPressAlert(void); void buzzerService(void); // non-blocking playback tick, called from loop() /** * Silence the buzzer immediately. DEEP SLEEP ONLY -- not a session-teardown API. diff --git a/src/device_control.cpp b/src/device_control.cpp index 356a3db..17258ec 100644 --- a/src/device_control.cpp +++ b/src/device_control.cpp @@ -3,10 +3,13 @@ #include "touch_input.h" #include "power_latch.h" #include "buzzer_control.h" +#include "display_service.h" +#include "ble_transport.h" #include "od_log.h" #include #ifdef TARGET_ESP32 +#include "wake_button.h" void enterDeepSleep(bool force = false, uint16_t overrideSleepSeconds = 0); #endif @@ -235,6 +238,10 @@ static void pollAdcButtons() { ((l->press_count & 0x0F) << 3) | ((state & 0x01) << 7)); if (l->byte_index < 11) dynamicreturndata[l->byte_index] = data; + if (state != 0u) { + ble.boostAdvertising(); + buttonPressFeedback(); + } updatemsdata(); od_log_debug("ADC btn pin %u adc=%d idx=%d id=%u cnt=%u state=%u", l->pin, adc, btn, l->last_button_id, l->press_count, state); @@ -330,9 +337,10 @@ static void led_all_off(struct LedConfig* led) { if (led == NULL) { return; } - bool invertRed = (led->led_flags & 0x01) != 0; - bool invertGreen = (led->led_flags & 0x02) != 0; - bool invertBlue = (led->led_flags & 0x04) != 0; + bool invertRed = (led->led_flags & OD_LED_FLAG_LED1_INVERT) != 0; + bool invertGreen = (led->led_flags & OD_LED_FLAG_LED2_INVERT) != 0; + bool invertBlue = (led->led_flags & OD_LED_FLAG_LED3_INVERT) != 0; + bool invert4 = (led->led_flags & OD_LED_FLAG_LED4_INVERT) != 0; if (led->led_1_r != 0xFF) { digitalWrite(led->led_1_r, invertRed ? HIGH : LOW); } @@ -342,6 +350,9 @@ static void led_all_off(struct LedConfig* led) { if (led->led_3_b != 0xFF) { digitalWrite(led->led_3_b, invertBlue ? HIGH : LOW); } + if (led->led_4 != 0xFF) { + digitalWrite(led->led_4, invert4 ? HIGH : LOW); + } } static void led_stop_internal(bool clear_mode) { @@ -596,6 +607,88 @@ void handleLedStop(uint8_t* data, uint16_t len) { sendResponse(successResponse, sizeof(successResponse)); } +static void led_channels_on(struct LedConfig* led) { + if (led == NULL) { + return; + } + const bool invert1 = (led->led_flags & OD_LED_FLAG_LED1_INVERT) != 0; + const bool invert2 = (led->led_flags & OD_LED_FLAG_LED2_INVERT) != 0; + const bool invert3 = (led->led_flags & OD_LED_FLAG_LED3_INVERT) != 0; + const bool invert4 = (led->led_flags & OD_LED_FLAG_LED4_INVERT) != 0; + if (led->led_1_r != 0xFF) { + digitalWrite(led->led_1_r, invert1 ? LOW : HIGH); + } + if (led->led_2_g != 0xFF) { + digitalWrite(led->led_2_g, invert2 ? LOW : HIGH); + } + if (led->led_3_b != 0xFF) { + digitalWrite(led->led_3_b, invert3 ? LOW : HIGH); + } + if (led->led_4 != 0xFF) { + digitalWrite(led->led_4, invert4 ? LOW : HIGH); + } +} + +static void ledButtonPressAlert(void) { + if (s_led.active) { + return; + } + struct LedConfig* led = nullptr; + for (uint8_t i = 0; i < globalConfig.led_count; i++) { + struct LedConfig* cand = &globalConfig.leds[i]; + if ((cand->led_flags & OD_LED_FLAG_BUTTON_PRESS) == 0u) { + continue; + } + if (cand->led_1_r == 0xFF && cand->led_2_g == 0xFF && + cand->led_3_b == 0xFF && cand->led_4 == 0xFF) { + continue; + } + led = cand; + break; + } + if (!led) { + return; + } + led_channels_on(led); + delay(60); + led_all_off(led); +} + +void buttonPressFeedback(void) { + ledButtonPressAlert(); + passiveBuzzerButtonPressAlert(); +} + +static void publishButtonMsd(ButtonState* btn, uint8_t pressed) { + if (btn == nullptr || btn->byte_index >= 11) { + return; + } + btn->current_state = pressed ? 1u : 0u; + const uint8_t buttonData = (uint8_t)((btn->button_id & 0x07u) | + ((btn->press_count & 0x0Fu) << 3) | + ((btn->current_state & 0x01u) << 7)); + dynamicreturndata[btn->byte_index] = buttonData; + ble.boostAdvertising(); + updatemsdata(); +} + +#if defined(TARGET_ESP32) +void buttonWakeDeliverSyntheticClick(void) { + const int idx = wakeButtonFindIndex(); + if (idx < 0) { + return; + } + ButtonState* btn = &buttonStates[idx]; + btn->press_count = (uint8_t)((btn->press_count + 1u) & 0x0Fu); + od_log_info("Button wake: synthetic click id=%u pin=%u", btn->button_id, btn->pin); + publishButtonMsd(btn, 1u); + buttonPressFeedback(); + delay(80); + publishButtonMsd(btn, 0u); + ble.tick(); +} +#endif + void processButtonEvents() { powerButtonPoll(); pollConfiguredPowerOffButtons(); // no-op unless the board declares a latch @@ -615,24 +708,13 @@ void processButtonEvents() { uint8_t logicalState = logicalPressed ? 1 : 0; btn->current_state = logicalState; od_log_debug("Button: %u, Press count: %u, Current state: %u", btn->button_id, btn->press_count, btn->current_state); - uint8_t buttonData = (btn->button_id & 0x07) | - ((btn->press_count & 0x0F) << 3) | - ((btn->current_state & 0x01) << 7); - if (btn->byte_index < 11) { - dynamicreturndata[btn->byte_index] = buttonData; + if (logicalState != 0u) { + buttonPressFeedback(); } + publishButtonMsd(btn, logicalState); } - // ORDER IS LOAD-BEARING: boost first, publish second. updatemsdata() ends in - // setManufacturerData(), which calls applyAdvInterval() and then restarts - // advertising -- so the interval is chosen DURING the publish. Boosting - // afterwards set the deadline too late to affect the packet it exists for: - // the press went out at the 160 ms slow interval (~1 advertisement in a - // typical 230 ms press window, which a passive scanner routinely misses) - // while the release 230 ms later got the 20 ms boosted interval, because by - // then s_advBoostUntil was set. Net effect: a host saw "not pressed" - // reliably and "pressed" almost never. - ble.boostAdvertising(); // no-op where the stack has no fast-adv window - updatemsdata(); + // boostAdvertising() runs inside publishButtonMsd(); order is load-bearing + // for nRF (interval chosen during setManufacturerData restart). } } diff --git a/src/device_control.h b/src/device_control.h index 7bcd660..44f500d 100644 --- a/src/device_control.h +++ b/src/device_control.h @@ -5,6 +5,10 @@ void reboot(); void processButtonEvents(); +void buttonPressFeedback(void); +#if defined(TARGET_ESP32) +void buttonWakeDeliverSyntheticClick(void); +#endif void flashLed(uint8_t color, uint8_t brightness); void processLedFlash(); void initButtons(); diff --git a/src/display_service.cpp b/src/display_service.cpp index c5fa58c..415befb 100644 --- a/src/display_service.cpp +++ b/src/display_service.cpp @@ -1859,13 +1859,18 @@ void updatemsdata(){ uint8_t statusByte = (((batteryVoltage10mv >> 8) & 0x01) ? OD_MSD_STATUS_BATTERY_VOLTAGE_BIT8 : 0) | (rebootFlag ? 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); // Build the 16-byte advertisement via the canonical wire struct (all little-endian), // then copy into the global msd_payload[16] that the BLE adv APIs below consume. struct MsdAdvertisement m; memset(&m, 0, sizeof m); m.company_id = 0x2446; - memcpy(m.dynamic, dynamicreturndata, sizeof m.dynamic); + if (globalConfig.display_count == 0) { + memset(m.dynamic, 0xFF, sizeof m.dynamic); + } else { + memcpy(m.dynamic, dynamicreturndata, sizeof m.dynamic); + } m.chip_temperature = temperatureByte; m.battery_voltage_low = batteryVoltageLowByte; m.status = statusByte; diff --git a/src/main.cpp b/src/main.cpp index 77d0031..8f121e4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -272,6 +272,9 @@ void setup() { updatemsdata(); if (is_deep_sleep_wake) { od_log_info("[wake] >> initButtons"); od_log_flush(); } initButtons(); + if (woke_by_button) { + buttonWakeDeliverSyntheticClick(); + } if (is_deep_sleep_wake) { od_log_info("[wake] >> initTouchInput"); od_log_flush(); } initTouchInput(); #ifdef TARGET_ESP32 @@ -1054,7 +1057,7 @@ void loop() { } else { platformIdle(); } - ble.tick(); // no-op on ESP32 + ble.tick(); // restores ESP32 fast-adv window after button events processButtonEvents(); processTouchInput(); buzzerService(); @@ -1073,7 +1076,7 @@ void idleDelay(uint32_t delayMs) { // which is also what makes WDT CONFIG.SLEEP=1 safe: the CPU sleeps inside // delay() below, and the watchdog keeps counting through it. odWatchdogFeed(); - ble.tick(); // no-op on ESP32 + ble.tick(); processButtonEvents(); processTouchInput(); processLedFlash(); diff --git a/src/wake_button.cpp b/src/wake_button.cpp index 595606f..faa31c8 100644 --- a/src/wake_button.cpp +++ b/src/wake_button.cpp @@ -248,6 +248,43 @@ bool detectButtonWake(int wakeupCause) { } } +int wakeButtonFindIndex(void) { + uint8_t pin = 0xFF; + const esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); + +#if SOC_PM_SUPPORT_EXT0_WAKEUP + if (cause == ESP_SLEEP_WAKEUP_EXT0) { + pin = s_ext0WakePin; + } +#endif +#if SOC_PM_SUPPORT_EXT1_WAKEUP + if (cause == ESP_SLEEP_WAKEUP_EXT1) { + const uint64_t mask = esp_sleep_get_ext1_wakeup_status(); + if (mask != 0u) { + pin = (uint8_t)__builtin_ctzll(mask); + } + } +#endif +#if SOC_GPIO_SUPPORT_DEEPSLEEP_WAKEUP + if (cause == ESP_SLEEP_WAKEUP_GPIO) { + const uint64_t mask = esp_sleep_get_gpio_wakeup_status(); + if (mask != 0u) { + pin = (uint8_t)__builtin_ctzll(mask); + } + } +#endif + if (pin == 0xFF) { + return -1; + } + for (uint8_t i = 0; i < buttonStateCount; i++) { + if (buttonStates[i].initialized && buttonStates[i].pin == pin) { + return (int)i; + } + } + od_log_warn("Button wake: pin %u not in configured buttons", pin); + return -1; +} + #else // not ESP32 void armButtonWakeSources() {} diff --git a/src/wake_button.h b/src/wake_button.h index e7a852f..d6735ed 100644 --- a/src/wake_button.h +++ b/src/wake_button.h @@ -20,3 +20,8 @@ void armButtonWakeSources(); // the button causes (EXT0/EXT1/GPIO). Takes esp_sleep_wakeup_cause_t as int so // this header also compiles on targets without esp_sleep.h. bool detectButtonWake(int wakeupCause); + +#if defined(TARGET_ESP32) +// Map the last button-wake cause to a configured buttonStates[] index, or -1. +int wakeButtonFindIndex(void); +#endif From e4c4f37c85cd60f0e4f830355c632601fff4ef5a Mon Sep 17 00:00:00 2001 From: Jonas Niesner Date: Wed, 2 Sep 2026 17:44:23 +0200 Subject: [PATCH 3/5] fix nrf build --- src/main.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 8f121e4..ec2f546 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -133,7 +133,9 @@ void setup() { } // Set only by the ESP32 wake-cause check below; NRF has no deep-sleep wake path. bool is_deep_sleep_wake = false; +#ifdef TARGET_ESP32 bool woke_by_button = false; +#endif // Decode why we booted, on BOTH targets. On nRF this also reads the retained // breadcrumb, so a watchdog reset can name the panel phase that wedged. Must // run after od_log_init() (above) or the line is emitted into a dark port, and @@ -272,9 +274,11 @@ void setup() { updatemsdata(); if (is_deep_sleep_wake) { od_log_info("[wake] >> initButtons"); od_log_flush(); } initButtons(); +#if defined(TARGET_ESP32) if (woke_by_button) { buttonWakeDeliverSyntheticClick(); } +#endif if (is_deep_sleep_wake) { od_log_info("[wake] >> initTouchInput"); od_log_flush(); } initTouchInput(); #ifdef TARGET_ESP32 From ce56d0e7972cb7231ad7f244f01ded283b309432 Mon Sep 17 00:00:00 2001 From: Jonas Niesner Date: Wed, 2 Sep 2026 18:01:23 +0200 Subject: [PATCH 4/5] fix button processing order --- src/device_control.cpp | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/device_control.cpp b/src/device_control.cpp index 17258ec..dd7bb19 100644 --- a/src/device_control.cpp +++ b/src/device_control.cpp @@ -240,9 +240,11 @@ static void pollAdcButtons() { if (l->byte_index < 11) dynamicreturndata[l->byte_index] = data; if (state != 0u) { ble.boostAdvertising(); - buttonPressFeedback(); } updatemsdata(); + if (state != 0u) { + buttonPressFeedback(); + } od_log_debug("ADC btn pin %u adc=%d idx=%d id=%u cnt=%u state=%u", l->pin, adc, btn, l->last_button_id, l->press_count, state); } @@ -681,10 +683,19 @@ void buttonWakeDeliverSyntheticClick(void) { ButtonState* btn = &buttonStates[idx]; btn->press_count = (uint8_t)((btn->press_count + 1u) & 0x0Fu); od_log_info("Button wake: synthetic click id=%u pin=%u", btn->button_id, btn->pin); + // Publish pressed MSD first so scanners see it before blocking feedback delays. publishButtonMsd(btn, 1u); buttonPressFeedback(); - delay(80); - publishButtonMsd(btn, 0u); + + // If the finger is still down, leave current_state=1 so the real release edge + // can fire the ISR. Only synthesize the up if the pin already released during + // boot (otherwise hosts never see a lift after a held wake press). + const bool pinState = digitalRead(btn->pin); + const bool stillHeld = btn->inverted ? !pinState : pinState; + if (!stillHeld) { + delay(80); + publishButtonMsd(btn, 0u); + } ble.tick(); } #endif @@ -708,10 +719,12 @@ void processButtonEvents() { uint8_t logicalState = logicalPressed ? 1 : 0; btn->current_state = logicalState; od_log_debug("Button: %u, Press count: %u, Current state: %u", btn->button_id, btn->press_count, btn->current_state); + // Publish before feedback: LED/buzzer use blocking delays and would + // otherwise postpone the pressed advertisement. + publishButtonMsd(btn, logicalState); if (logicalState != 0u) { buttonPressFeedback(); } - publishButtonMsd(btn, logicalState); } // boostAdvertising() runs inside publishButtonMsd(); order is load-bearing // for nRF (interval chosen during setManufacturerData restart). From 3c6e89faae0c2a61d0c18002838452ff9710f5b4 Mon Sep 17 00:00:00 2001 From: Jonas Niesner Date: Mon, 14 Sep 2026 22:27:30 +0200 Subject: [PATCH 5/5] fix: better partial for 4.26 BW --- include/opendisplay_structs.h | 8 ++++- src/ble_transport.h | 7 ++-- src/ble_transport_esp32.cpp | 13 ++++++- src/ble_transport_nrf.cpp | 64 +++++++++++++++++++++++++---------- src/communication.h | 5 ++- src/display_service.cpp | 55 ++++++++++++++++++++++++++++-- src/main.cpp | 11 ++---- 7 files changed, 125 insertions(+), 38 deletions(-) diff --git a/include/opendisplay_structs.h b/include/opendisplay_structs.h index 5c519b7..725ec3c 100644 --- a/include/opendisplay_structs.h +++ b/include/opendisplay_structs.h @@ -69,6 +69,10 @@ * Unreleased (since 2.0) * - LedFlags bit4: OD_LED_FLAG_BUTTON_PRESS (short LED flash on physical * button press / button-wake synthetic click; default off). + * - LedFlags bit5: OD_LED_FLAG_NFC (short white LED flash on NFC field + * detect rising edge; default off). + * - LedFlags bit6: OD_LED_FLAG_NFC_FANCY (multi-colour LED blink on NFC + * field detect; default off; takes precedence over bit5 when both set). * - BuzzerFlags bit1: OD_BUZZER_FLAG_BUTTON_PRESS (short chirp on physical * button press / button-wake synthetic click; default off). * - MsdStatusBits bit3: OD_MSD_STATUS_ENCRYPTION_ENABLED (1 = app-layer @@ -742,12 +746,14 @@ enum LedType { OD_LED_TYPE_FOUR_SEPARATE = 3 /**< @doc "four separate LEDs" */ }; -/* LedConfig.led_flags @bits LedFlags (bits 5-7 reserved). */ +/* LedConfig.led_flags @bits LedFlags (bit 7 reserved). */ #define OD_LED_FLAG_LED1_INVERT (1u << 0) /* @doc "invert LED channel 1 polarity" */ #define OD_LED_FLAG_LED2_INVERT (1u << 1) /* @doc "invert LED channel 2 polarity" */ #define OD_LED_FLAG_LED3_INVERT (1u << 2) /* @doc "invert LED channel 3 polarity" */ #define OD_LED_FLAG_LED4_INVERT (1u << 3) /* @doc "invert LED channel 4 polarity" */ #define OD_LED_FLAG_BUTTON_PRESS (1u << 4) /* @doc "short flash on physical button press (incl. button-wake synthetic click); default off" */ +#define OD_LED_FLAG_NFC (1u << 5) /* @doc "short white flash on NFC field-detect rising edge; default off" */ +#define OD_LED_FLAG_NFC_FANCY (1u << 6) /* @doc "multi-colour blink on NFC field-detect rising edge; default off; overrides OD_LED_FLAG_NFC when both set" */ /** @struct LedConfig @packet 0x21 @repeatable max=4 * @doc "LED channel pins + invert flags. Up to 4 instances. 22 bytes. NOTE: the diff --git a/src/ble_transport.h b/src/ble_transport.h index 6db368c..1da900e 100644 --- a/src/ble_transport.h +++ b/src/ble_transport.h @@ -124,10 +124,9 @@ class BleTransport { // Each implementation keeps its own restart semantics (see the .cpp). void setManufacturerData(const uint8_t* msd, uint8_t len); - // True where the stack re-arms advertising by itself after a disconnect - // (nRF: Bluefruit.Advertising.restartOnDisconnect(true)). Where it is false - // the application must schedule the restart itself. A genuine capability - // difference, stated as a query so callers need no target #ifdef. + // True where the stack re-arms advertising by itself after a disconnect. + // nRF and ESP32 both return false: the app owns slow re-arm so a disconnect + // cannot inherit a SoftDevice "fast" advertising phase. bool restartsAdvertisingOnDisconnect() const; // --- link policy (no-op where the stack does not support it) --- diff --git a/src/ble_transport_esp32.cpp b/src/ble_transport_esp32.cpp index 1cbc43a..2b3c204 100644 --- a/src/ble_transport_esp32.cpp +++ b/src/ble_transport_esp32.cpp @@ -413,8 +413,15 @@ void BleTransport::startAdvertising() { void BleTransport::restartAdvertising() { // Unconditional by contract: the caller owns the "still connected / mid-EPD // refresh / stack not up" deferral policy (see serviceBleAdvertisingRestart - // in main.cpp). The delay mirrors the historical sequence. + // in main.cpp). Force slow intervals so a disconnect restart never inherits + // a prior button-boost burst. delay(100); + s_advBoostUntil = 0; + BLEAdvertising* pAdvertising = (s_server != nullptr) ? s_server->getAdvertising() + : BLEDevice::getAdvertising(); + if (pAdvertising != nullptr) { + applyAdvInterval(pAdvertising, false); + } BLEDevice::startAdvertising(); od_log_info("BLE advertising restarted"); } @@ -587,6 +594,10 @@ void BleTransport::setManufacturerData(const uint8_t* msd, uint8_t len) { // Called from loop() when the connect event is consumed, not from the connect // callback -- these are host-stack calls, which the callback contract excludes. void BleTransport::requestFastLink() { + // Connected: drop any button boost so a later advertising restart does not + // inherit 20–30 ms intervals from a still-open boost window. + s_advBoostUntil = 0; + // Tune the OWNER's link. This used to read the single s_connHandle scalar, // which the newest connect overwrote -- so with a contender attached, link // tuning targeted the wrong link (one of the shared-scalar defects R3 names). diff --git a/src/ble_transport_nrf.cpp b/src/ble_transport_nrf.cpp index 7927900..d2fbdb5 100644 --- a/src/ble_transport_nrf.cpp +++ b/src/ble_transport_nrf.cpp @@ -59,22 +59,25 @@ static volatile bool s_instanceSubscribed = false; static volatile uint32_t s_instanceDecidedWord = 0; // --- advertising interval policy -------------------------------------------- +// Bluefruit always restarts advertising after disconnect in its "fast" phase +// (setFastTimeout). Use the slow interval for both phases so a disconnect does +// not burn a multi-second high-rate burst. Button wakes still call +// boostAdvertising() for a short 20–30 ms window. static uint32_t s_advBoostUntil = 0; -static constexpr uint16_t NRF_ADV_INTERVAL_MIN = 256; // 160 ms static constexpr uint16_t NRF_ADV_INTERVAL_MAX = 1600; // 1000 ms static constexpr uint16_t NRF_ADV_BOOST_MIN = 32; // 20 ms static constexpr uint16_t NRF_ADV_BOOST_MAX = 48; // 30 ms static constexpr uint32_t NRF_ADV_BOOST_MS = 3000; static void applyAdvInterval() { - if (!globalConfig.loaded) { - Bluefruit.Advertising.setInterval(NRF_ADV_BOOST_MIN, NRF_ADV_BOOST_MAX); - } else if (s_advBoostUntil != 0 && millis() < s_advBoostUntil) { + if (!globalConfig.loaded || (s_advBoostUntil != 0 && millis() < s_advBoostUntil)) { Bluefruit.Advertising.setInterval(NRF_ADV_BOOST_MIN, NRF_ADV_BOOST_MAX); } else { s_advBoostUntil = 0; - Bluefruit.Advertising.setInterval(NRF_ADV_INTERVAL_MIN, NRF_ADV_INTERVAL_MAX); + // Identical fast/slow intervals: restartOnDisconnect starts in "fast" + // mode but at the slow rate — no post-disconnect advertising burst. + Bluefruit.Advertising.setInterval(NRF_ADV_INTERVAL_MAX, NRF_ADV_INTERVAL_MAX); } } @@ -249,6 +252,9 @@ bool BleTransport::begin(const char* deviceName) { od_log_info("BLE callbacks registered"); Bluefruit.setName(deviceName); od_log_info("Device name set to: %s", deviceName); + // Preferred supervision timeout 1 s (was Bluefruit default 2 s). Central may + // still negotiate longer; shortens unclean link-loss radio hang when accepted. + Bluefruit.Periph.setConnSupervisionTimeoutMS(1000); od_log_info("Configuring power management..."); sd_power_mode_set(NRF_POWER_MODE_LOWPWR); sd_power_dcdc_mode_set(NRF_POWER_DCDC_ENABLE); @@ -263,20 +269,29 @@ void BleTransport::startAdvertising() { Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); Bluefruit.Advertising.addName(); // Deliberately kept inside this sequence rather than hoisted to the caller: - // updatemsdata() lands in setManufacturerData() below, which itself sets - // setFastTimeout(1). Calling it after the setFastTimeout(10) line would leave - // the fast-advertising window at 1 s instead of 10 s -- a real behaviour - // change. Phase 1 keeps the historical order byte-for-byte. + // updatemsdata() lands in setManufacturerData() below. updatemsdata(); - Bluefruit.Advertising.restartOnDisconnect(true); + // App-owned restart after disconnect (see restartAdvertising): SoftDevice + // auto-restart always began a timed "fast" phase and fought our slow policy. + Bluefruit.Advertising.restartOnDisconnect(false); + s_advBoostUntil = 0; applyAdvInterval(); - Bluefruit.Advertising.setFastTimeout(10); + // duration 0 + equal slow/slow intervals → continuous slow advertising, no + // timed burst phase after start or (via restartAdvertising) after disconnect. + Bluefruit.Advertising.setFastTimeout(0); od_log_info("Starting BLE advertising..."); Bluefruit.Advertising.start(0); } void BleTransport::restartAdvertising() { - Bluefruit.Advertising.stop(); + // Slow-only re-arm after disconnect. Do not call updatemsdata() here — that + // stop/start + possible MSD rebuild is what reintroduced radio bursts. + s_advBoostUntil = 0; + applyAdvInterval(); + Bluefruit.Advertising.setFastTimeout(0); + if (Bluefruit.Advertising.isRunning()) { + Bluefruit.Advertising.stop(); + } Bluefruit.Advertising.start(0); } @@ -368,7 +383,16 @@ void BleTransport::setManufacturerData(const uint8_t* msd, uint8_t len) { Bluefruit.Advertising.addName(); Bluefruit.Advertising.addData(BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA, msd, len); applyAdvInterval(); - Bluefruit.Advertising.setFastTimeout(1); + // Boost window: short timed phase at boost intervals. Otherwise continuous + // slow (fast timeout 0 with equal slow/slow intervals). + if (s_advBoostUntil != 0 && millis() < s_advBoostUntil) { + Bluefruit.Advertising.setFastTimeout((uint16_t)((NRF_ADV_BOOST_MS + 999) / 1000)); + } else { + Bluefruit.Advertising.setFastTimeout(0); + } + // Only touch the radio while disconnected. Rebuilding MSD while connected + // used to stop/start advertising and could leave a burst after the link dropped. + if (Bluefruit.connected()) return; Bluefruit.Advertising.stop(); Bluefruit.Advertising.start(0); } @@ -384,6 +408,11 @@ void BleTransport::setManufacturerData(const uint8_t* msd, uint8_t len) { // nothing -- the central's own request arrives later than this either way, which // is why the diagnostics below log twice. void BleTransport::requestFastLink() { + // Connected: drop any button boost so the next restartOnDisconnect does not + // inherit 20–30 ms intervals from a still-open boost window. + s_advBoostUntil = 0; + Bluefruit.Advertising.setInterval(NRF_ADV_INTERVAL_MAX, NRF_ADV_INTERVAL_MAX); + BLEConnection* conn = Bluefruit.Connection(s_connHandle); if (conn == nullptr) return; @@ -427,7 +456,8 @@ void BleTransport::tick() { } was_boosted = false; s_advBoostUntil = 0; - Bluefruit.Advertising.setInterval(NRF_ADV_INTERVAL_MIN, NRF_ADV_INTERVAL_MAX); + Bluefruit.Advertising.setInterval(NRF_ADV_INTERVAL_MAX, NRF_ADV_INTERVAL_MAX); + Bluefruit.Advertising.setFastTimeout(0); Bluefruit.Advertising.stop(); Bluefruit.Advertising.start(0); } @@ -476,10 +506,8 @@ bool BleTransport::takeDisconnectedEvent(uint16_t* reason, uint32_t* instanceWor } bool BleTransport::restartsAdvertisingOnDisconnect() const { - // Bluefruit.Advertising.restartOnDisconnect(true) in startAdvertising(): the - // SoftDevice re-arms the radio itself, so the application must not also - // schedule a restart or the two fight over the advertising state. - return true; + // restartOnDisconnect(false): loop() owns slow re-arm via restartAdvertising(). + return false; } const char* BleTransport::addressString() { diff --git a/src/communication.h b/src/communication.h index eac34b6..9cc17b6 100644 --- a/src/communication.h +++ b/src/communication.h @@ -76,9 +76,8 @@ void resetAuthAbuseCounter(void); /// transport that OWNS the transfer is confirmed gone. void requestTransferSessionCleanup(void); -/// Re-arm BLE advertising when it is safe to. A no-op on targets whose stack -/// re-arms itself (see BleTransport::restartsAdvertisingOnDisconnect), so -/// callers need no target guard. +/// Re-arm BLE advertising when it is safe to. Schedules application-owned slow +/// re-arm on both targets (see BleTransport::restartsAdvertisingOnDisconnect). void requestAdvertisingRestart(void); #endif diff --git a/src/display_service.cpp b/src/display_service.cpp index 415befb..3fdf233 100644 --- a/src/display_service.cpp +++ b/src/display_service.cpp @@ -342,12 +342,17 @@ static void pwrmgmLockGive(void) { // refresh looked correct, then the image darkened in over the following minutes. static const uint16_t EPD_POST_SLEEP_BLEED_MS = 200; +static bool panel_is_ep426(void) { + return bbep.type == EP426_800x480 || bbep.type == EP426_800x480_4GRAY; +} + // SSD16xx bbepSleep() only sends deep-sleep; it does not run the analog/HV -// shutdown. Force EOPT discharge frames + the GoodDisplay/GxEPD2 power-off -// sequence (enable clock → disable analog → disable OSC) while SPI is still up. +// shutdown. The EOPT + 0x83 discharge ("gray fix") is required on EP42B 400x300 +// but burns ~1 s @ high current on EP426 800x480 — skip the whole sequence there. static void epdSsd16xxPowerOffDischarge(void) { if (bbep.chip_type != BBEP_CHIP_SSD16xx) return; if (!bbep.is_awake) return; + if (panel_is_ep426()) return; bbepCMD2(&bbep, 0x3F, 0x22); // EOPT: TFT discharge frames + sequenced VCOM/HV bbepCMD2(&bbep, SSD1608_DISP_CTRL2, 0x83); bbepWriteCmd(&bbep, SSD1608_MASTER_ACTIVATE); @@ -375,7 +380,7 @@ static void epdSessionForceOffLocked(void) { odWatchdogFeed(); // reload before entering bb_epaper (may block ~240 s) epdSsd16xxPowerOffDischarge(); bbepSleep(&bbep, 1); - delay(EPD_POST_SLEEP_BLEED_MS); + if (!panel_is_ep426()) delay(EPD_POST_SLEEP_BLEED_MS); } pwrmgm(false); // -> PWR_OFF, clears deadline epdPlanesPrepared = false; @@ -3375,9 +3380,53 @@ static bool partial_write_stream_bytes(uint8_t* data, uint32_t len) { return true; } +// EP426 (SSD1677) OTP Mode-2 under-drives W→B. Register LUT follows EP426B's +// VS row layout (white on row1 / black on row2). Single ~50-frame phase — +// dual 0x32/0x32 looked settled after the first HV spike, so the second +// phase was mostly wasted energy. Activate with 0xDC (register LUT, no OTP). +static const uint8_t epd426_partial_lut[105] = { + // VS: same channel order as epd426b_init_part (not WW/BW/WB labels) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // TP/RP groups 0..9 — one ~50-frame pulse (was 0x32,0x32) + 0x32, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x22, 0x22, 0x22, 0x22, 0x22, +}; + +static void epd426_load_custom_partial_lut(void) { + uint8_t ctrl1[2] = {0x00, 0x00}; + uint8_t src[3] = {0x41, 0xa8, 0x32}; + bbepWriteCmd(&bbep, SSD1608_WRITE_LUT); + bbepWriteData(&bbep, (uint8_t *)epd426_partial_lut, (int)sizeof(epd426_partial_lut)); + bbepCMD2(&bbep, SSD1608_GATE_VOLTAGE, 0x17); + bbepWriteCmd(&bbep, SSD1608_SOURCE_VOLTAGE); + bbepWriteData(&bbep, src, 3); + bbepCMD2(&bbep, SSD1608_WRITE_VCOM, 0x40); // EP426B partial VCOM (0x30 drifts white→gray) + bbepWriteCmd(&bbep, SSD1608_DISP_CTRL1); + bbepWriteData(&bbep, ctrl1, 2); +} + static bool partial_trigger_refresh(int refreshMode) { if (refreshMode < 0 || refreshMode > 3) refreshMode = REFRESH_PARTIAL; if (panel_skips_reinit_on_partial_refresh(&bbep)) { + if (refreshMode == REFRESH_PARTIAL && bbep.type == EP426_800x480) { + epd426_load_custom_partial_lut(); + bbepCMD2(&bbep, SSD1608_DISP_CTRL2, 0xdc); + bbepWriteCmd(&bbep, SSD1608_MASTER_ACTIVATE); + return waitforrefresh(60); + } if (panel_uses_ep397_y_decrement(&bbep)) { static const uint8_t u8CMDz3[4] = {0xf7, 0xd7, 0xff, 0}; bbepCMD2(&bbep, SSD1608_DISP_CTRL2, u8CMDz3[refreshMode]); diff --git a/src/main.cpp b/src/main.cpp index ec2f546..1f1b322 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -530,13 +530,8 @@ static void serviceBleDisconnectCleanup() { // "not yet" paths so a later pass retries. static void serviceBleAdvertisingRestart() { if (!s_advertisingRestartPending) return; - // Capability gate, and the reason this helper is safe for ANY caller to - // raise the flag: where the stack re-arms advertising itself (nRF's - // restartOnDisconnect(true)), driving our own stop()/start() would fight it. - // Refusing here rather than at each raise site means a new raiser -- the - // post-refresh hook in display_service.cpp, or a future portable - // requestAdvertisingRestart() -- cannot reintroduce that conflict by - // forgetting a target guard. + // Capability gate: where the stack re-arms advertising itself, driving our + // own stop()/start() would fight it. if (ble.restartsAdvertisingOnDisconnect()) { s_advertisingRestartPending = false; return; @@ -548,8 +543,8 @@ static void serviceBleAdvertisingRestart() { } if (epdRefreshInProgress) return; // never mid-refresh s_advertisingRestartPending = false; + // Slow re-arm only — no updatemsdata() (that stop/starts advertising again). ble.restartAdvertising(); - updatemsdata(); } // Translate the transport's consume-once connect/disconnect events into the