feat: add SSD1680 (Waveshare 2.13" V4) and UC8253 (Waveshare 3.52") drivers#255
feat: add SSD1680 (Waveshare 2.13" V4) and UC8253 (Waveshare 3.52") drivers#255cogwheel886 wants to merge 27 commits intorust-embedded-community:mainfrom
Conversation
- 240x360 resolution, UC8179 controller - Hardware-verified LUT waveform tables (GC and DU) - Alternating R22/R23 waveform tables in display_frame() - IS_BUSY_LOW: true, verified on Raspberry Pi 5 with lgpio - Includes Display3in52 type alias for graphics feature - 56 unit tests passing (54 original + 2 new)
- Add epd2in13_v4 module with full SSD1680 driver - Supports full refresh, fast refresh, and partial refresh - Optional PWR pin support (GPIO18 required on V4 HAT) - Tested on Raspberry Pi Zero 2W with physical hardware - Fixes: remove spurious wait_until_idle in set_ram_counter - Fixes: init_fast 0x80 sent as command not data - Add epd2in13_v4_raw example for low-level hardware validation - Closes rust-embedded-community#207
epd2in13_v4 (new): - Full SSD1680 driver with optional PWR pin (GPIO18) support - Full, fast, and partial refresh modes - Correct sleep() — does not drive PWR low (use power_off() instead) - Tested on Raspberry Pi Zero 2W with physical hardware - Status display example with sysinfo, PiSugar socket, systemd timer epd3in52 (bug fixes found during review): - Fix clear_frame() sending 8x too many bytes (WIDTH*HEIGHT → buffer_len) - Fix refresh command: 0x12 → 0x17 + 0xA5 matching Python reference - Rename EPD3in52 → Epd3in52 (naming convention) interface: - Add soft_reset() — RST LOW → delay → RST HIGH, no trailing 200ms - Used by epd2in13_v4 display_partial() for correct partial refresh Closes rust-embedded-community#207
Run 1: full refresh (establishes clean display state) Run 2: display_part_base_image (writes both RAM banks, one full refresh) Run 3+: display_partial only (true partial waveform, no color inversion) State tracked via /tmp files cleared on reboot. Eliminates 4-5 cycle full waveform on every status update. Confirmed working on Pi Zero 2W with Waveshare 2.13 V4 hardware.
…eboots State files in /tmp were cleared on reboot causing full refresh cycle to repeat unnecessarily. /var/lib/epd-status persists across reboots so partial refresh is maintained permanently after initial 3-fire sequence. - STATE_FILE: /var/lib/epd-status/initialized - BASE_FILE: /var/lib/epd-status/base_set - create_dir_all() ensures directory exists when run manually - StateDirectory=epd-status in service file for systemd management - install script creates directory with correct permissions
Convert [], [], [], [] from intra-doc links to plain backtick references — these are trait methods and rustdoc cannot resolve them without full qualified paths. Fixes CI doc build failure on PR rust-embedded-community#255.
- Landscape orientation (Rotate90), power port on top - Stats from /proc and /sys: hostname, IP, temp, RAM, uptime - Single display_frame() call matches Python lut_GC()+refresh() sequence - lut_flag inversion bug documented: do not call display_frame() twice per cycle - Verified on hardware: colors correct, orientation correct
- epd2in13_v4_status: remove sysinfo crate, read from /proc/stat, /proc/uptime, /proc/meminfo, /sys/class/thermal, /etc/hostname, df - epd3in52_ruby_status: already uses /proc+/sys (no change) - Remove sysinfo = 0.33 from dev-dependencies in Cargo.toml - Binary size: epd2in13_v4_status 1.1MB -> 772K (30% reduction) - Output format unchanged, partial refresh logic unchanged
- Add refresh_lut field to Epd3in52 struct, default RefreshLut::Full - set_lut() now stores requested variant instead of no-op - display_frame() selects GC or DU LUT tables via match - GC path byte-for-byte identical to previous implementation - DU path documented with Waveshare warning: not recommended - Remove dead_code allows from DU constants (now referenced) - Add lut_selection_default_is_full test Note: DU is full-screen fast refresh with shorter waveform — not true partial update. UC8253 has no hardware windowing. Use GC for normal operation.
Convert [], [], [], [] from intra-doc links to plain backtick references — these are trait methods and rustdoc cannot resolve them without full qualified paths. Fixes CI doc build failure on PR rust-embedded-community#255.
- Remove three horizontal divider lines, tighten spacing to y+=12 - Add refresh_mode field to StatusData, read from state files - Display as RFR: full/base/partial on screen after voltage line - Add refresh mode to stdout summary line - Hardware verified: full->base->partial sequence confirmed on Waveshare 2.13" EPD V4
- Both examples read rotation= from /etc/epd-waveshare.conf (0/90/180/270) - epd2in13_v4_status: detect rotation change, force full refresh cycle, write ROTATION_FILE to persist last-used rotation - All hardcoded widths replaced with logical_w from rotation - stdout includes rotation degrees - Hardware verified: all rotations work on Waveshare 3.52" EPD, 0/90/180 on Waveshare 2.13" EPD V4 - Note: Rotate270 renders but produces no visible change on SSD1680 hardware
- Both examples read color_invert=true/false from conf - EpdConfig struct consolidates rotation + color_invert parsing - stdout includes Inv:true/false in summary line - Hardware verified: Waveshare 3.52" EPD (UC8253) and Waveshare 2.13" EPD V4 (SSD1680) - Known: color_invert change on Waveshare 2.13" EPD V4 requires base->partial cycle to self-correct (no forced full refresh on inversion change — backlog)
…vice Usage: bash tools/test_rotation.sh <hostname> Reads current rotation before starting, restores on completion. 20s settle time between rotations to avoid timing issues on SSD1680.
- Add epd.sleep() before exit in epd2in13_v4_status (SSD1680 left
awake between timer runs without this)
- Replace unwrap_or_default() on SystemTime with "CLK? unsynced"
sentinel visible on display when clock not yet synced after boot
- Move state file removal to after successful display update — SPI
init failure no longer clears state unnecessarily
- Replace multi-line config-missing block with single eprintln! to
reduce systemd journal noise on timer-driven nodes
- Use rsplit_once(':') in parse_pisugar_float for robustness
- Standardise IP discovery UDP connect to port 53 in both files
- Use .get(..16).unwrap_or() instead of direct slice in 3in52
- Replace const TEST_PATTERN: bool = false with EPD_TEST_PATTERN
env var — no recompile needed to toggle
- Remove tautological assert_eq!(EXPECTED_BUF_LEN, 10800) in 3in52
- Register epd3in52_ruby_status in Cargo.toml [[example]]
- Align epd.sleep() position with 3in52 pattern
- H3: saturating_sub for dt - di in read_cpu_percent (both examples)
- M1: add .map_err() error context at SPI, GPIO, EPD init boundaries
Tested on hardware: Raspberry Pi Zero 2WH (aarch64, Raspberry Pi OS),
Waveshare 2.13" EPD V4 (SSD1680). Stripped binary: 581 KB.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- H2: correct epd3in52 reset pulse to match Waveshare Python reference driver: 30μs/10μs → 200ms/10ms (UC8253 RST timing) Hardware verified: Waveshare 3.52" EPD on Raspberry Pi 5 (aarch64) - E1: replace todo!() in epd3in52::update_partial_frame with documented fallback to update_frame — UC8253 does not support true partial frame updates; todo!() panics at runtime for any caller - E2: expand epd3in52 module doc to upstream parity — UC8253 name, product page link, Python driver link, lut_flag caveat, example block - E3: add epd3in52 = [] feature stub to Cargo.toml [features] - E4: add #[repr(u8)] to epd2in13_v4 Command enum for consistency with epd3in52 and explicit hardware register layout - E6: add SSD1680 reset timing citation comments in epd2in13_v4/mod.rs - E7: annotate DisplayUpdateControl2 0xF7/0xC7/0xFF bytes in v4; annotate UC8253 init register sequence in 3in52 - E8: expand Epd3in52 struct doc to match Epd2in13 density - E9: align module-doc section style between both drivers - M2: document data_x_times per-byte SPI write limitation - H1/M3/M4: deferred — BUSY timeout and GPIO error propagation require library API changes with dedicated hardware testing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove deployment-specific hostname from example filename. Update Cargo.toml [[example]] name to match.
66bcc62 to
9e7935a
Compare
Demonstrates SSD1680 partial refresh using an animated Ferris sprite walking left/right across the bottom of the panel. Ferris is 110x73 pixels on a 250x122 display — large enough to be clearly visible. Sequence: 1. Full refresh — clear panel white 2. display_part_base_image — write base to both SSD1680 RAM banks 3. Walk loop — 3 back-and-forth cycles via display_partial (no flash) 4. reinit() — required before full refresh after partial mode 5. Final full refresh — clear panel white 6. deep sleep Key implementation notes: - 500ms settle delays after full refreshes before partial mode begins - Uses gpio_cdev (not sysfs_gpio — deprecated on RPi OS Bookworm) - Ferris asset: examples/assets/ferris_110x73.raw (1-bit, 1022 bytes) generated from rustacean.net/assets/rustacean-flat-noshadow.png Tested on hardware: Raspberry Pi Zero 2WH (aarch64, Raspberry Pi OS), Waveshare 2.13" EPD V4 (SSD1680). Clean walk, no ghosting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace deployment-specific references with generic placeholders: - Remove hostname from module doc target line - Remove hostname from build/deploy instructions - Remove hostname from stdout banner - Remove hostname from GPIO comment - Update example name references epd3in52_ruby_status → epd3in52_status Tested on hardware: Raspberry Pi 5 (aarch64), Waveshare 3.52" EPD (UC8253).
Demonstrates UC8253 DU (quick) refresh mode via an 8-frame deterministic cross-dissolve from the OpenClaw lobster mascot to Ferris the Rustacean. Layout: 'Carcinisation' caption persists above the sprite throughout all phases. 'Rustacean' appears below on the final frame only. Sprite centered vertically between the two captions. Carcinisation: the convergent evolutionary process by which non-crab crustaceans independently evolve into crab-like forms. Applied here to the software ecosystem. Animation sequence: 1. Full refresh — lobster + 'Carcinisation' (2s pause) 2. 8-frame DU dissolve — deterministic pixel cross-dissolve, lobster to Ferris, 'Carcinisation' present throughout 3. Full refresh — Ferris + 'Carcinisation' + 'Rustacean' Assets: - examples/assets/lobster_96x96.raw (1-bit, 1152 bytes) sourced from Noto Color Emoji lobster glyph - examples/assets/ferris_96x96.raw (1-bit, 1152 bytes) sourced from rustacean.net/assets/rustacean-flat-happy.png Tested on hardware: Raspberry Pi 5 (aarch64), Waveshare 3.52" EPD (UC8253). DU refresh artifacts are characteristic of the waveform mode and expected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- epd-status-boot.service: clears display state files on every boot so the full->base->partial refresh cycle runs correctly after power cycle (display RAM is cleared but state files survive reboot) - journald-volatile.conf: volatile RAM-only journal with 10MB cap for SD card wear protection on Pi Zero 2W nodes - install_epd_status.sh: updated to install all five components - README.md: documents boot reset behavior and journald config Deployed and verified on Pi Zero 2WH (aarch64, Raspberry Pi OS). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- epd3in52-status.service: oneshot service for Waveshare 3.52" EPD - epd3in52-status.timer: 1h refresh interval (vs 5min for Pi Zero) UC8253 uses full GC refresh only — hourly is appropriate for a development machine display, reduces visual interruption Deployed and verified on Raspberry Pi 5 (aarch64, Raspberry Pi OS).
Runs the carcinisation demo at 03:00 daily on Pi 5 nodes. Provides thorough panel exercise (multiple full GC cycles) to prevent UC8253 ghosting from prolonged static display.
test_rotation.sh is deployment-specific tooling, not part of the driver contribution. Removing from branch before PR review.
|
Are you able to test it on a different hardware than raspberry pi? I can't seem to replicate the example with esp32c3 and the 3.52" screen. |
The driver itself (src/epd3in52/) is no_std compatible and should For ESP32C3 you would need to wire up the driver directly using These are all embedded-hal 1.0 traits that esp-hal implements. The |
Summary
Adds two new e-paper display drivers:
Consolidates and supersedes #254 which had the 3.52" driver in an earlier state without hardware-verified fixes.
Hardware tested
Both running Raspberry Pi OS (Bookworm, 64-bit).
Examples
All examples use gpio_cdev rather than the deprecated sysfs_gpio, required for Raspberry Pi OS Bookworm and Pi 5 compatibility.
Implementation notes
Checklist