Design: CIPHER-LINE Modes, Boot Splash, and the oled/ Library
chosen by Josh from mocked options). Implementation queued; see §10.
Task: GWP-727.
Related: ADR-0015 (CIPHER-LINE
surface + FFI), ADR-0017 (coprocessor),
coprocessor-protocol.md,
2026-07-02-oled-8080-bench-bringup.md (deleted 2026-07-29; recover from git history) (GWP-619),
Canonical Hardware Specification (authoritative values).
Mocks: design/mockups/cipher-line/: pixel-exact,
rendered with the runtime’s kn86_font at true 256×64 geometry by a pure KEC Lisp
generator (gen-mocks.lsp, GIF89a/LZW encoder included, writing each .gif
directly via binary-safe blob writes; run with kec run …/gen-mocks.lsp).
1. What triggered this
Section titled “1. What triggered this”Josh reviewed the bench OLED (2026-07-18 photo) and called out four gaps:
- No start-up image or animation.
- The running layout on the bench glass has overlapping elements and odd spacing.
- No display modes (first wanted: a full-screen HH:MM:SS clock that updates).
- No Lisp library for managing the aux display.
Diagnosis of (2): the overlap is an artifact of the GWP-619 scratch bench shell,
whose TEXT / BIG / RECT / BARS primitives draw independently with no notion of
fields or ownership, so bars land on top of labels and text crosses the column rule.
The scratch shell is bench-only and is not in the tree. The durable fix is the layout
model in this design: on the product path a single owner composes the panel, and row
content is placed through field helpers that clear their span before drawing. When the
product wire path replaces the scratch shell, the failure mode is gone structurally.
Current runtime state (for orientation): the panel is written from C only:
cipher.c (voice rows), oled_row4.c (Row 4 claim stack), nemacs.c, and peers,
over oled.c’s four 32-char logical rows. Carts reach it through the ADR-0015
cipher-* / aux-* NoshAPI primitives. There is no Lisp surface, no pixel-level
render tier, and no full-screen concept.
2. Decisions (made 2026-07-18)
Section titled “2. Decisions (made 2026-07-18)”Three options were mocked for each open call; Josh chose:
| Question | Choice |
|---|---|
| Boot splash | Option A: wordmark type-on (splash_a_typeon.gif). Runs entirely over row-text primitives, so it ships on the current wire commands; options B (phosphor sweep) and C (POST bar) not chosen, mocks retained for reference. |
| Device render path for big glyphs / full-screen modes | Frame blit. The Pi renders into the existing 2 KB oled.c framebuffer and ships whole frames over UART (~20 ms per frame at 1 Mbps, comfortable for 10–20 fps cadences). The Pico blits. Row commands stay as low-bandwidth fast paths. |
| Mode entry | Lisp call first. (oled/mode :clock) from the REPL or any system surface now; a SYS-layer key chord is a small follow-up once modes exist. |
3. Mode model
Section titled “3. Mode model”The panel has exactly one owner at a time:
- Default owner: ROWS. The ADR-0015 four-row layout, written by the existing C writers. Unchanged behavior when no mode is active.
- Full-screen modes claim the whole panel. While a mode is active, a C-side gate
in
oled.csuspends row-oriented writes; the writers keep updating their own state (cipher scrollback, Row 4 claim stack) so nothing is lost. On mode exit the panel redraws the rows from current state.
The mode stack lives in Lisp (oled/, §5); the gate lives in C (oled.c,
§4). This mirrors the split used elsewhere in the runtime: C owns the buffer and the
invariant, Lisp owns the policy.
The splash is itself a mode (:boot), pushed at nOSh start and popped when its
sequence completes. The clock is the second mode. Future modes (VU meter, link
status, timers) are additional oled/define-mode registrations with no new C work.
4. C changes (runtime/src/oled.c)
Section titled “4. C changes (runtime/src/oled.c)”A small pixel tier beside the existing row tier, same static-buffer discipline:
oled_big_text(oled, x, y, scale, text):kn86_fontglyphs at an integer scale. At scale 4 a glyph is 32 px, soHH:MM:SS(8 glyphs) spans the full 256 px width.oled_fill_rect(oled, x, y, w, h, on): set or clear a pixel rectangle (rules, bars, cursor blocks).- Mode gate:
oled_mode_claim(owner)/oled_mode_release()plus a check in the row-write entry points. Row writes while claimed are dropped at the panel (writer state is unaffected); release triggers a full row redraw from current state.
5. The oled/ library (runtime/system-image/lib/oled/oled.lsp)
Section titled “5. The oled/ library (runtime/system-image/lib/oled/oled.lsp)”System-tier Fe module. Public surface (namespaced oled/…, KEC Core vocabulary):
- Modes:
(oled/define-mode name plist)with:enter/:render/:tick/:exitcallbacks and a:tick-hzrate;(oled/mode name)pushes,(oled/pop)restores the previous owner. Ticks ride the GWP-644 idle-timer library (run-with-timer), armed lazily and guarded like every other animated surface. - Fields (the layout discipline):
(oled/field row col width text)clears its span then draws, truncating at the field edge;(oled/right row text)right-aligns. The Row 1 status strip is recomposed from fixed fields (battery / timer / mode chip / TERM hint, per the ADR-0015 row table);status_mode.gifis the target render. Fields make the bench-glass overlap unrepresentable: content cannot escape its span. - Built-in modes:
:boot(§6) and:clock(§7).
New system-tier bridge primitives back the library: oled-set-row,
oled-big-text, oled-fill-rect, oled-clear, oled-mode-claim,
oled-mode-release. These bind only in the System KEC context, exactly like the
render/* tier; loading them from a cart context raises unbound-symbol. The
cart-facing NoshAPI surface (ADR-0005/0015 cipher-* / aux-*) is unchanged.
Carts still cannot address the panel directly, so no ADR amendment is required.
6. Boot splash (chosen: type-on)
Section titled “6. Boot splash (chosen: type-on)”Storyboard, per splash_a_typeon.gif (all row/fill primitives, works on today’s
wire commands):
| Beat | Content | Timing |
|---|---|---|
| 1 | KINOSHITA types on glyph-by-glyph at 2× with a trailing block cursor, centered upper panel | 90 ms/glyph |
| 2 | KN-86 // CIPHER-LINE ONLINE types on at 1× below, 6 glyphs/frame | ~70 ms/frame |
| 3 | A 2 px rule sweeps left to right under the subline | ~240 ms |
| 4 | Hold | ~1 s |
| 5 | Cut to the running four-row layout | — |
Runs at nOSh host init on both targets (emulator and device) since it sits above the
backend seam. A Pico-resident static splash for the power-on window (the panel is
otherwise dark until the Pi boots and nOSh connects) is product-firmware scope and is
deferred with the rest of pico2-firmware/ per the GWP-152 gating rule; the design
intent is recorded here so the firmware task inherits it.
7. Clock mode
Section titled “7. Clock mode”(oled/mode :clock): 24-hour HH:MM:SS at scale 4 (full 256 px width, 32 px tall,
vertically centered), redrawn on a 1 Hz tick. Mock: clock_mode.gif. Time source is
CLOCK_REALTIME, which on the device is RTC-backed per the offline-first timekeeping
direction; the emulator uses the host clock. (oled/pop) returns to the rows.
8. Wire protocol (device path)
Section titled “8. Wire protocol (device path)”One new command, frame blit: the full 2048-byte 1-bpp framebuffer, with the
transaction/ACK boundary the GWP-619 bench note already recommends for smooth
animation. Spec lands in coprocessor-protocol.md; the runtime dispatches it through
the existing coproc vtable (in-process no-op path for the emulator). The Pico-side
implementation belongs to the existing Pico 2 firmware Phase 3 (SSD1322 driver) task.
Existing OLED_SET_ROW / SCROLL / FILL / CLEAR commands remain the
low-bandwidth path for ordinary row updates. This design does not touch the
SPI-vs-8080 panel-bus question; ADR-0015 §F8 owns that reconciliation, and the wire
protocol is independent of the panel bus.
9. Testing
Section titled “9. Testing”- Headless ctest: pixel tier (big-text geometry, fill-rect bounds), mode gate (no bleed-through into or out of a claimed mode), System-context-only binding of the bridge primitives.
- Every implementation story ends with a kn86rec real-screen recording (splash play, clock ticking, status strip fields).