From cb27a4061ed699e7fc0756430f4120a552cd4120 Mon Sep 17 00:00:00 2001 From: Jonas Niesner Date: Wed, 26 Aug 2026 14:49:49 +0200 Subject: [PATCH 1/4] 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/4] 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/4] 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/4] 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).