feat: add SSD1680 (Waveshare 2.13" V4) and UC8253 (Waveshare 3.52") drivers#255
Open
cogwheel886 wants to merge 27 commits intorust-embedded-community:mainfrom
Open
feat: add SSD1680 (Waveshare 2.13" V4) and UC8253 (Waveshare 3.52") drivers#255cogwheel886 wants to merge 27 commits intorust-embedded-community:mainfrom
cogwheel886 wants to merge 27 commits intorust-embedded-community:mainfrom
Conversation
added 6 commits
April 11, 2026 01:53
- 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
cogwheel886
pushed a commit
to cogwheel886/epd-waveshare
that referenced
this pull request
Apr 12, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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