# Development Log ## 2026-04-25: Initial Setup ### Project: nrfwarch Smartwatch firmware for Seeed Xiao nRF54L15 Sense with GDEY0154D61LT 1.54" e-ink display (SSD1680 controller). ### Board setup - Ported SSD1680 e-ink driver from jcontrerasf/E-ink-display-Zephyr (nRF52840/PlatformIO) to Xiao nRF54L15 with modern Zephyr APIs - Created custom devicetree binding (`nrfwarch,epd-ctrl`) for EPD GPIO pins - VCOM command (0x2C, value 0xA5) added to init sequence - Partial update uses `0xCF` control byte (clock + analog + temperature, no LUT reload) ### Framebuffer renderer - 200x200 monochrome framebuffer, 5000 bytes - Y-flip in `wf_set_pixel` compensates for SSD1680 data entry mode 0x01 (Y decrement): `ram_row = (199 - y)` - `wf_draw_glyph` draws a glyph bitmap onto the framebuffer at (dst_x, dst_y). Font data uses bit=1 for foreground (draw black), bit=0 for background (skip). Background pixels are NOT written so the framebuffer underneath is preserved. - Stack overflow in `epd_clear` fixed by using static buffer instead of stack allocation ### Fonts - `tools/gen_fonts.py` generates `src/fonts.h` and `src/fonts.c` programmatically - Digits are 24x40px (120 bytes each), colon is 8x40px (40 bytes) - Digits are drawn as stroked outlines: horizontal/vertical lines + filled rectangles - Each digit is a seven-segment-style rendering with 2px stroke thickness and 3px margins - To regenerate: `python3 tools/gen_fonts.py` ### EPD wiring (Xiao nRF54L15 Sense) | Signal | Pin | GPIO | |--------|-----|------| | MOSI | D10 | SPI MOSI | | CLK | D8 | SPI SCK | | CS | D1 | xiao_d 1 | | DC | D3 | xiao_d 3 | | RST | D0 | xiao_d 0 | | BUSY | D2 | xiao_d 2 | ## 2026-04-25: Display Simulator ### What was done - `sim/` directory with display simulator for watchface development without hardware - `sim/src/epd_sim.c` implements the same API as `src/epd_drv.c`, dumps framebuffer as hex over stdout with `!F!`/`!E!` framing - `sim/view.py` reads hex frames from stdin, inverts polarity (framebuffer bit=1 is white, PBM bit=1 is black), writes PBM to `/tmp/nrfwarch.pbm` - feh auto-launches with `--auto-reload` for live display - Sim runs on Zephyr `native_sim` target (native Linux process, not QEMU). Time advances 1 min/sec. ### Sim architecture ``` Zephyr native_sim binary (zephyr.exe) -> watchface.c renders to framebuffer (same code as hardware) -> epd_sim.c dumps hex frames to stdout -> view.py reads stdin, converts to PBM -> feh displays the image with auto-reload ``` ### Frame protocol - `!F!` = frame start, then 5000 bytes as hex (32 bytes/line), `!E!` = frame end - view.py ignores everything outside markers (handles Zephyr log output mixed in) ## 2026-04-25: Flake, Tests, and Font Fixes ### Nix flake - Converted from shell.nix to flake.nix with three apps: - `nix run .#setup-sdk` - downloads Zephyr v4.0 + SDK 0.17.0, patches all ELF binaries for NixOS - `nix run .#test` - builds and runs ztest suite on QEMU x86 - `nix run .#sim` - builds and runs display simulator with feh viewer - Dev shell includes: cmake, ninja, west, qemu_full, feh, dtc, patchelf ### NixOS SDK patching - Zephyr SDK binaries are dynamically linked with `/lib64/ld-linux-x86-64.so.2` interpreter - Must patchelf every ELF binary (bin/, libexec/, lib/) to point at NixOS's ld-linux - The setup-sdk app handles this automatically ### Test suite (17 tests, all passing) - **Framebuffer**: clear sets all 0xFF, pixel set/get roundtrip, bounds checking, Y-flip at corners - **Fonts**: digit/colon array sizes match compile-time constants, all digits contain non-trivial data - **Rendering**: draw_time modifies buffer, different times produce different buffers, battery indicator renders, 0% vs 100% battery differs - **Glyph**: all-ones glyph sets pixel to black ### Bugs fixed this session - **Inverted fonts**: `wf_draw_glyph` had `black = !(val & bit)` which inverted foreground/background. Fixed to only draw when bit=1 (foreground), skip bit=0 (background). - **Leading bar artifact**: Background pixels were being explicitly written as white on every glyph byte, overwriting adjacent content. Fixed by skipping background pixels entirely (`continue` on bit=0). - **Font data mismatch**: Original hand-written font data had wrong byte counts (112 bytes declared as 64). Replaced with programmatic generator. - **epd_clear stack overflow**: 5000-byte array on stack blew past 4KB main stack. Changed to static buffer. - **Devicetree binding**: `compatible = "simple-bus"` rejected custom GPIO properties. Created proper `nrfwarch,epd-ctrl.yaml` binding. ### Next steps - BLE CTS client for time sync from phone - GRTC-based RTC for persistent timekeeping across system-off - Battery monitoring via ADC channel 7 with VBAT regulator - LSM6DSO IMU wrist-tilt wake detection - System-off power management with regulator control - Partial refresh for minute updates (avoid full refresh flicker)