Skip to content

fix: full TCON re-init on first SEEED render after deep-sleep wake#2

Merged
davelee98 merged 2 commits into
reset-reason-loggingfrom
fix/seeed-deepsleep-wake-tcon-init
Jul 8, 2026
Merged

fix: full TCON re-init on first SEEED render after deep-sleep wake#2
davelee98 merged 2 commits into
reset-reason-loggingfrom
fix/seeed-deepsleep-wake-tcon-init

Conversation

@davelee98

Copy link
Copy Markdown
Owner

Problem

On the SEEED_GFX (ED103TC2 / IT8951 TCON) display path, deep-sleep wake is a full ESP32 reboot that power-cycles the panel/TCON rail (src/main.cpp "Panel rail is cut"; pwrmgm(true) = pwr_pin HIGH + delay(800); AXP2101 boards fully power-cycle the PMIC). The IT8951 therefore loses all register state.

The post-wake render seam seeed_gfx_direct_write_reset() only called g_seeed_epaper.wake(), which:

  1. no-ops after wake — g_seeed_epaper is a plain static, so its _sleep flag is reconstructed to false and wake() hits if (!_sleep) return;, and
  2. even if it ran, wake() only issues tconWake() — it never runs hostTconInit() (VCOM, panel dimensions, I80CPCR packed-pixel mode), which lives in init()/begin().

So the first image push after a deep-sleep wake lands on an un-initialized TCON → likely garbled/blank first refresh or a TCON-busy stall (15 s watchdog). The non-SEEED bb_epaper path is immune (it re-inits unconditionally every direct write).

Fix

Track whether the SEEED driver has been fully initialized since the current boot with a plain-RAM (deliberately not RTC_DATA_ATTR) file-static flag — false after every reset, including deep-sleep wake, which is exactly when a full begin() is required. On the first render each boot, run the full seeed_gfx_epaper_begin() (does hostTconInit() + correct gray-mode sprite alloc); keep the cheap wake() for subsequent same-session writes.

  • Cold boot: unchanged — boot-screen init sets the flag, later writes take wake().
  • Deep-sleep wake: first write runs full re-init (bug fixed), then wake().
  • Also restores the correct 4-gray sprite depth post-wake.

Single-file change: src/display_seeed_gfx.cpp (3 hunks).

Notes / testing

  • Based on reset-reason-logging (the reset-logging diagnostic branch); PR targets that branch.
  • Not yet hardware-verified. PlatformIO isn't available in the build environment, so no compile/on-device run was performed. Needs: (1) confirm it builds on an esp32-s3-* OPENDISPLAY_SEEED_GFX env, and (2) on an ED103TC2 unit (ideally 4-gray), reproduce the pre-fix garbled/stalled first post-wake refresh, then confirm it renders correctly after the fix and cold-boot/normal writes are unchanged.
  • Independent of the confirmed prvCopyDataToQueue BLE panic in minimalSetup() — that fires before any display code and affects bb_epaper units too.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wkc4weAfTh36iPGHawXAxi

davelee98 and others added 2 commits July 7, 2026 20:47
On the ED103TC2/IT8951 SEEED_GFX path, deep-sleep wake is a full reboot
that power-cycles the panel/TCON rail, but the post-wake direct-write seam
only called EPaper::wake() -- a no-op after reboot (g_seeed_epaper is plain
RAM, so _sleep resets to false) that also only issues tconWake() and never
runs hostTconInit() (VCOM / panel dimensions / I80CPCR packed-pixel mode).
The first post-wake image push therefore landed on an un-initialized TCON,
risking a garbled/blank refresh or a TCON-busy stall.

Track init state in a plain-RAM (NOT RTC_DATA_ATTR) file-static flag that is
false after every reset -- including deep-sleep wake, which is exactly when a
full begin() is required -- and run the full seeed_gfx_epaper_begin() on the
first render each boot, keeping the cheap wake() for subsequent same-session
writes. Also restores the correct gray-mode sprite on 4-gray panels post-wake.

The non-SEEED bb_epaper path re-inits unconditionally and is unaffected. This
is independent of the prvCopyDataToQueue BLE panic in minimalSetup().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wkc4weAfTh36iPGHawXAxi
An image push logged 6-8 lines per 0x71 data frame across the receive
callback, command/response queue drain, imageDataWritten, and sendResponse
-- ~1 MB of UART text for a 1.3 MB image. At 115200 baud writeSerial blocks
once the TX buffer fills, throttling the transfer itself.

Replace the per-frame spam with a compact stream log: the first frame in
full, a 5%-step percentage meter thereafter, and a completion summary with
the final frame, total chunk count, elapsed time, and KB/s throughput.

New file-static logging state + helpers in display_service.cpp
(imageWriteLogReset/Start/Chunk/Progress/Finish) drive the meter; three
exported predicates (imageWriteLogQuietCmd/Ack/Frame) let communication.cpp,
main.cpp's queue drain, and the esp32_ble_callbacks receive path suppress
their per-frame lines mid-stream while keeping chunk 1, errors, and NACKs
visible. Applies to both direct-write and partial-write paths; the meter
tracks decompressed progress on compressed uploads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@davelee98
davelee98 marked this pull request as ready for review July 8, 2026 23:48
@davelee98
davelee98 merged commit 6a8b117 into reset-reason-logging Jul 8, 2026
davelee98 added a commit that referenced this pull request Jul 18, 2026
…dit #1-3,6)

The Bluedroid->NimBLE migration left heavyweight, state-mutating work running
inline inside the ESP32 server callbacks on the NimBLE host task, racing the
Arduino loop() task. Make the callbacks flag-only and service the flags from
loop(), mirroring the existing bleRestartAdvertisingPending pattern.

- onDisconnect: EPD session teardown (bbepSleep/delay/SPI.end + rail cut),
  partial + pipe cleanup, and resetPipeWriteState() ran on the host task while
  loop() streamed SPI via bbepWriteData and processed pipe frames. The only
  guard (epdRefreshInProgress) covered the refresh phase, not streaming.
  Now sets bleDisconnectCleanupPending; serviced by serviceBleDisconnectCleanup()
  in loop() (preserving the epdRefreshInProgress deferral), run before the
  advertising restart. (#1 critical, #2 high, OpenDisplay#6 medium)
- onConnect: updatemsdata() polled I2C and mutated the shared advertisement
  vector that loop() also drives on its 60s cadence -> heap corruption. Now
  sets msdUpdatePending; loop() calls updatemsdata(). (#3 high)
- updatemsdata(): drop the connected-branch that rebuilt *advertisementData
  but never pushed it via setAdvertisementData() -- dead work, no behavior
  change.

All ESP32-guarded; nRF/Bluefruit path untouched. Builds clean on
esp32-s3-N16R8, esp32-c3-N16, nrf52840custom.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
davelee98 added a commit that referenced this pull request Jul 19, 2026
…Display#107) (OpenDisplay#108)

* fix(ble): defer NimBLE onConnect/onDisconnect work to loop() (race audit #1-3,6)

The Bluedroid->NimBLE migration left heavyweight, state-mutating work running
inline inside the ESP32 server callbacks on the NimBLE host task, racing the
Arduino loop() task. Make the callbacks flag-only and service the flags from
loop(), mirroring the existing bleRestartAdvertisingPending pattern.

- onDisconnect: EPD session teardown (bbepSleep/delay/SPI.end + rail cut),
  partial + pipe cleanup, and resetPipeWriteState() ran on the host task while
  loop() streamed SPI via bbepWriteData and processed pipe frames. The only
  guard (epdRefreshInProgress) covered the refresh phase, not streaming.
  Now sets bleDisconnectCleanupPending; serviced by serviceBleDisconnectCleanup()
  in loop() (preserving the epdRefreshInProgress deferral), run before the
  advertising restart. (#1 critical, #2 high, OpenDisplay#6 medium)
- onConnect: updatemsdata() polled I2C and mutated the shared advertisement
  vector that loop() also drives on its 60s cadence -> heap corruption. Now
  sets msdUpdatePending; loop() calls updatemsdata(). (#3 high)
- updatemsdata(): drop the connected-branch that rebuilt *advertisementData
  but never pushed it via setAdvertisementData() -- dead work, no behavior
  change.

All ESP32-guarded; nRF/Bluefruit path untouched. Builds clean on
esp32-s3-N16R8, esp32-c3-N16, nrf52840custom.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Replace protocol magic numbers with canonical named constants

Follow-up to the header vendoring (3f09f6d): sweep the remaining
hand-coded protocol literals across the BLE command handlers and use
the constants from the vendored opendisplay_protocol.h. Value-for-value,
no wire-behavior change; all CI envs build clean.

- encryption.cpp: auth-status frames -> RESP_ACK/RESP_AUTHENTICATE +
  AUTH_STATUS_*; CCM envelope sizes -> BLE_CMD_HEADER_SIZE/
  ENCRYPTION_NONCE_SIZE/ENCRYPTION_TAG_SIZE (handshake nonces left as-is).
- communication.cpp: handler response codes -> RESP_* (incl.
  RESP_AUTH_REQUIRED); config limits -> CONFIG_CHUNK_SIZE*/
  MAX_CONFIG_CHUNKS/MAX_RESPONSE_DATA_SIZE; envelope sizes.
- device_control.cpp / buzzer_control.cpp: LED, deep-sleep, and buzzer
  ack/nack frames -> RESP_*.
- display_service.cpp: direct-write/refresh frames -> RESP_DIRECT_WRITE_*;
  pipe-start errors -> OD_ERR_PIPE_START_*; deleted the local ERR_*
  shadow copies of OD_ERR_PARTIAL_* in favor of the canonical names.

Pipe/partial-write opcode-echo bytes (0x76/0x80/0x81/0x82), the pipe
data-phase error codes, and the START response-flags bit stay raw with
TODO(protocol) markers: the canonical header defines no RESP_* mirror or
data-phase error namespace for them yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant