Summary
A protocol-correct 0x76 partial upload on an EN05 (EP426_800x480 MONO, panel_ic 0x0027, fw 2.0) renders the rect correctly but erases everything outside the rect to white. Every protocol step ACKs (0x76 → 0x71 → 0x72 → 0x73), displayed_etag updates, and the client reports success — the failure is only visible on the physical panel.
Reproduction (fw 2.0, EN05)
- Full upload: frame with an 8 px border + black square left → renders correctly.
- Partial upload via 0x76: square moves right (rect
(120,140,568,201), 8-aligned, old‖new planes, END with refresh mode 2).
- Result: square correctly appears at the new position (lighter partial-waveform gray) — but the border and all other content outside the rect is gone (white).
With a small rect (16×7 px changed digit) the effect is "screen wiped except an unreadable sliver". Before/after photos in the comment below.
Root cause
partial_prepare_panel_ram() (display_service.cpp, unchanged since the original partial implementation in #21 / fw 1.8) runs unconditionally at every 0x76 START:
bbepInitIO(...); bbepWakeUp(&bbep);
bbepSendCMDSequence(&bbep, initSeq);
bbepFill(&bbep, BBEP_WHITE, PLANE_1); // full panel
bbepFill(&bbep, BBEP_WHITE, PLANE_0); // full panel
Both planes are white-filled across the entire panel; only the rect window then receives real old/new data. The design implicitly assumes the panel's partial waveform treats old==new (white/white) pixels as no-drive, leaving the physical pixels untouched. On EP426's OTP partial waveform (partial_trigger_refresh → DISP_CTRL2 = 0xFF, mode 2) that assumption does not hold: white/white pixels are actively driven white, erasing the previously displayed content. Presumably validated on EP397, whose waveform behaves differently — per-panel LUT behavior is why this never shows up in protocol-level testing.
Not client-specific
- py-opendisplay (current main) and the web client (
Web/httpdocs/js/ble-common.js @ fb847ae) both compute minimal 8-aligned diff rects and both send refresh mode 2 at END — both are affected identically.
- Client wire format cross-checked against firmware (planes, rect encoding, mode byte): correct.
Possible directions
- Preserve panel RAM instead of refilling — skip the reinit + white-fill when controller RAM still holds the previous frame (the
panel_skips_reinit_on_partial_refresh() special-casing of EP397/EP426 suggests this intent already exists; the blocker is the post-refresh deep-sleep losing RAM).
- Gate partial per panel: NACK 0x76 with
ERR_PARTIAL_UNSUPPORTED on panels whose waveform drives equal-RAM pixels (EP426), until 1 works.
- Protocol extension: client sends the full old frame as plane data (restores correctness everywhere, but defeats the transfer savings that motivate partial updates).
Environment
- EN05 rev 1, nRF52840, EP426_800x480 MONO, panel_ic 0x0027, encryption enabled
- fw 2.0 (
974e51b1), also expected on 1.8–1.83 (identical partial_prepare_panel_ram)
- Client: py-opendisplay current main; visual verification via webcam captures after each protocol step
🤖 Investigated and written with Claude Fable (Claude Code)
Summary
A protocol-correct 0x76 partial upload on an EN05 (EP426_800x480 MONO, panel_ic 0x0027, fw 2.0) renders the rect correctly but erases everything outside the rect to white. Every protocol step ACKs (0x76 → 0x71 → 0x72 → 0x73),
displayed_etagupdates, and the client reports success — the failure is only visible on the physical panel.Reproduction (fw 2.0, EN05)
(120,140,568,201), 8-aligned, old‖new planes, END with refresh mode 2).With a small rect (16×7 px changed digit) the effect is "screen wiped except an unreadable sliver". Before/after photos in the comment below.
Root cause
partial_prepare_panel_ram()(display_service.cpp, unchanged since the original partial implementation in #21 / fw 1.8) runs unconditionally at every 0x76 START:Both planes are white-filled across the entire panel; only the rect window then receives real old/new data. The design implicitly assumes the panel's partial waveform treats
old==new(white/white) pixels as no-drive, leaving the physical pixels untouched. On EP426's OTP partial waveform (partial_trigger_refresh→DISP_CTRL2 = 0xFF, mode 2) that assumption does not hold: white/white pixels are actively driven white, erasing the previously displayed content. Presumably validated on EP397, whose waveform behaves differently — per-panel LUT behavior is why this never shows up in protocol-level testing.Not client-specific
Web/httpdocs/js/ble-common.js@fb847ae) both compute minimal 8-aligned diff rects and both send refresh mode2at END — both are affected identically.Possible directions
panel_skips_reinit_on_partial_refresh()special-casing of EP397/EP426 suggests this intent already exists; the blocker is the post-refresh deep-sleep losing RAM).ERR_PARTIAL_UNSUPPORTEDon panels whose waveform drives equal-RAM pixels (EP426), until 1 works.Environment
974e51b1), also expected on 1.8–1.83 (identicalpartial_prepare_panel_ram)🤖 Investigated and written with Claude Fable (Claude Code)