Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c1260e2
feat: add support for Waveshare 3.52" EPD (EPD3IN52)
Apr 11, 2026
9b66389
style: apply cargo fmt
Apr 11, 2026
811b95a
feat: add support for Waveshare 2.13" EPD V4 (SSD1680)
Apr 11, 2026
9f332b4
feat: add epd2in13_v4 (SSD1680), fix epd3in52 bugs, add soft_reset
Apr 11, 2026
e1835c5
fix: implement three-state partial refresh for epd2in13_v4_status
Apr 11, 2026
d0fd652
fix: move state files to /var/lib/epd-status for persistence across r…
Apr 11, 2026
9b200ff
feat: add epd3in52_status example (UC8253, Waveshare 3.52")
Apr 12, 2026
4065c40
refactor: replace sysinfo with /proc+/sys reads in both examples
Apr 12, 2026
f440a33
feat: implement RefreshLut::Quick (DU) for epd3in52
Apr 12, 2026
d428a96
fix: resolve broken intra-doc links in epd2in13_v4/mod.rs
Apr 12, 2026
669f98c
fix: remove dividers, add refresh mode display to epd2in13_v4_status
Apr 12, 2026
f3a1904
fix: remove duplicate refresh mode in stdout summary line
Apr 12, 2026
72d1992
feat: runtime rotation config from /etc/epd-waveshare.conf
Apr 12, 2026
d64b992
feat: add color_invert config key to /etc/epd-waveshare.conf
Apr 12, 2026
5c3519a
tools: add test_rotation.sh — cycle all four rotations on a target de…
Apr 12, 2026
b39c2df
fix: epd status examples — sleep, clock skew, state ordering, polish
Apr 13, 2026
a4c59b3
fix: driver correctness and documentation — reset timing, todo, idioms
Apr 13, 2026
9e7935a
refactor: rename epd3in52_ruby_status → epd3in52_status
Apr 13, 2026
614eee2
feat: add epd2in13_v4 Ferris walking demo (partial refresh)
Apr 13, 2026
3413015
fix: remove hostname references from epd3in52_status example
Apr 13, 2026
934ce7b
feat: add epd3in52 carcinisation demo (lobster→Ferris dissolve)
Apr 13, 2026
e287ac9
feat: add boot state reset and journald volatile config
Apr 13, 2026
2092676
chore: gitignore docs/ — internal process documentation
Apr 13, 2026
89616bb
feat: add epd3in52 status service and timer for Pi 5
Apr 14, 2026
529aff0
feat: add epd3in52 daily panel exercise service and timer
Apr 14, 2026
ba9901d
chore: remove deployment tool from upstream contribution
Apr 14, 2026
a301138
chore: gitignore .claude/ — contains local SSH allowlist with hostnames
Apr 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ Cargo.lock

# intellij/clion
.idea/

# Internal process documentation — never upstream
docs/
.claude/
18 changes: 18 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@ required-features = ["linux-dev"]
name = "epd4in2_variable_size"
required-features = ["linux-dev"]

[[example]]
name = "epd2in13_v4"
required-features = ["linux-dev"]

[[example]]
name = "epd2in13_v4_raw"
required-features = ["linux-dev"]

[[example]]
name = "epd2in13_v4_status"
required-features = ["linux-dev"]

[[example]]
name = "epd3in52_status"
required-features = ["linux-dev"]

[[example]]
name = "epd4in2"
required-features = ["linux-dev"]
Expand All @@ -54,6 +70,8 @@ default = ["graphics", "linux-dev", "epd2in13_v3"]
graphics = ["embedded-graphics-core"]
epd2in13_v2 = []
epd2in13_v3 = []
epd2in13_v4 = []
epd3in52 = []
linux-dev = []

# Offers an alternative fast full lut for type_a displays, but the refreshed screen isnt as clean looking
Expand Down
Binary file added examples/assets/ferris_110x73.raw
Binary file not shown.
Binary file added examples/assets/ferris_96x96.raw
Binary file not shown.
Binary file added examples/assets/lobster_96x96.raw
Binary file not shown.
212 changes: 212 additions & 0 deletions examples/epd2in13_v4.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
//! Ferris walking demo for the Waveshare 2.13" E-Paper HAT V4 (SSD1680).
//!
//! Demonstrates partial refresh by scuttling Ferris across the screen
//! three times. The sequence is:
//!
//! 1. Full refresh — clear the panel white.
//! 2. [`Epd2in13::display_part_base_image`] — establish the base image
//! in both SSD1680 RAM banks. This is required before any partial
//! refresh will produce correct output.
//! 3. Partial refresh loop — on each step, draw a white rectangle over
//! Ferris' previous position, draw him at the new position, then
//! call [`Epd2in13::display_partial`] which runs the soft reset +
//! partial waveform sequence.
//! 4. Final full refresh — clear the panel white.
//! 5. [`Epd2in13::sleep`] — enter deep sleep.
//!
//! # GPIO backend
//!
//! Uses `gpio_cdev` rather than `sysfs_gpio`: sysfs is deprecated on
//! Raspberry Pi OS Bookworm and the Pi 5 requires `gpio_cdev` due to
//! BCM offset changes on `gpiochip0`. The rest of the upstream examples
//! still use `sysfs_gpio`; this example targets newer RPi OS releases.
//!
//! # Wiring (Waveshare 2.13" V4 HAT, BCM numbering)
//!
//! | Signal | BCM |
//! |--------|-----|
//! | PWR | 18 |
//! | RST | 17 |
//! | DC | 25 |
//! | BUSY | 24 |
//! | SPI | CE0 on /dev/spidev0.0, 4 MHz, mode 0 |

use embedded_graphics::{
image::{Image, ImageRaw},
pixelcolor::BinaryColor,
prelude::*,
primitives::{PrimitiveStyle, Rectangle},
};
use embedded_hal::delay::DelayNs;
use epd_waveshare::{
epd2in13_v4::{Display2in13, Epd2in13},
graphics::DisplayRotation,
prelude::*,
};
use linux_embedded_hal::{
gpio_cdev::{Chip, LineRequestFlags},
spidev::{self, SpidevOptions},
CdevPin, Delay, SpidevDevice,
};

const FERRIS_W: u32 = 110;
const FERRIS_H: u32 = 73;
const FERRIS_BYTES: &[u8] = include_bytes!("./assets/ferris_110x73.raw");

/// Pixels advanced per animation frame.
const STEP: i32 = 5;
/// Horizontal walking room: 250 - 110 = 140.
const X_MAX: i32 = 250 - FERRIS_W as i32;
/// Y position: 3 px from the bottom of the 122 px landscape canvas.
const FERRIS_Y: i32 = 122 - FERRIS_H as i32 - 3;
/// Delay between partial-refresh steps.
const STEP_DELAY_MS: u32 = 150;
/// Number of full back-and-forth walk cycles.
const WALK_CYCLES: u32 = 3;

fn main() -> Result<(), Box<dyn std::error::Error>> {
// --- SPI setup ---------------------------------------------------------
let mut spi =
SpidevDevice::open("/dev/spidev0.0").map_err(|e| format!("open /dev/spidev0.0: {e}"))?;
let options = SpidevOptions::new()
.bits_per_word(8)
.max_speed_hz(4_000_000)
.mode(spidev::SpiModeFlags::SPI_MODE_0)
.build();
spi.configure(&options)
.map_err(|e| format!("configure /dev/spidev0.0: {e}"))?;

// --- GPIO setup (gpio_cdev) --------------------------------------------
let mut chip = Chip::new("/dev/gpiochip0").map_err(|e| format!("open /dev/gpiochip0: {e}"))?;
let busy = CdevPin::new(
chip.get_line(24)
.map_err(|e| format!("claim GPIO24 (BUSY): {e}"))?
.request(LineRequestFlags::INPUT, 0, "epd-busy")
.map_err(|e| format!("request GPIO24 (BUSY) as input: {e}"))?,
)?;
let dc = CdevPin::new(
chip.get_line(25)
.map_err(|e| format!("claim GPIO25 (DC): {e}"))?
.request(LineRequestFlags::OUTPUT, 0, "epd-dc")
.map_err(|e| format!("request GPIO25 (DC) as output: {e}"))?,
)?;
let rst = CdevPin::new(
chip.get_line(17)
.map_err(|e| format!("claim GPIO17 (RST): {e}"))?
.request(LineRequestFlags::OUTPUT, 1, "epd-rst")
.map_err(|e| format!("request GPIO17 (RST) as output: {e}"))?,
)?;
let pwr = CdevPin::new(
chip.get_line(18)
.map_err(|e| format!("claim GPIO18 (PWR): {e}"))?
.request(LineRequestFlags::OUTPUT, 0, "epd-pwr")
.map_err(|e| format!("request GPIO18 (PWR) as output: {e}"))?,
)?;

let mut delay = Delay;

// --- EPD init ----------------------------------------------------------
let mut epd = Epd2in13::new_with_pwr(&mut spi, busy, dc, rst, &mut delay, None, pwr)
.map_err(|e| format!("EPD init (SSD1680): {e}"))?;

let mut display = Display2in13::default();
display.set_rotation(DisplayRotation::Rotate90);

// 1. Full refresh — start from a clean white panel.
println!("Clearing panel (full refresh)...");
display.clear(Color::White).ok();
epd.update_frame(&mut spi, display.buffer(), &mut delay)?;
epd.display_frame(&mut spi, &mut delay)?;
// SSD1680: let the panel settle before the next RAM write. The busy
// pin drops before the full waveform has fully completed internally.
delay.delay_ms(500);

// 2. Establish the partial-refresh base image in both RAM banks.
// SSD1680 requires both the "old" and "new" RAM banks to hold the
// starting image before partial updates will waveform correctly.
println!("Establishing partial-refresh base image...");
display.clear(Color::White).ok();
draw_ferris(&mut display, 0)?;
epd.display_part_base_image(&mut spi, display.buffer(), &mut delay)?;
// display_part_base_image runs a full refresh internally; settle
// before the first partial step.
delay.delay_ms(500);

// 3. Walk loop — Ferris scuttles back and forth via partial refresh.
println!("Walking Ferris ({} cycles)...", WALK_CYCLES);
let mut prev_x: i32 = 0;
for _ in 0..WALK_CYCLES {
// Left → right
let mut x = 0;
while x <= X_MAX {
partial_step(&mut epd, &mut spi, &mut display, &mut delay, prev_x, x)?;
prev_x = x;
x += STEP;
delay.delay_ms(STEP_DELAY_MS);
}
// Right → left
let mut x = X_MAX;
while x >= 0 {
partial_step(&mut epd, &mut spi, &mut display, &mut delay, prev_x, x)?;
prev_x = x;
x -= STEP;
delay.delay_ms(STEP_DELAY_MS);
}
}

// 4. Final full refresh — leave the panel clean.
// SSD1680 requires a re-init after display_partial before it will
// accept full-refresh commands again; skipping this leaves the
// final clear silently dropped.
println!("Clearing panel (final full refresh)...");
epd.reinit(&mut spi, &mut delay)?;
display.clear(Color::White).ok();
epd.update_frame(&mut spi, display.buffer(), &mut delay)?;
epd.display_frame(&mut spi, &mut delay)?;

// 5. Deep sleep.
println!("Sleeping...");
epd.sleep(&mut spi, &mut delay)?;

Ok(())
}

/// Erase Ferris at `prev_x`, draw him at `new_x`, push via partial refresh.
fn partial_step<SPI, BUSY, DC, RST, DELAY, PWR>(
epd: &mut Epd2in13<SPI, BUSY, DC, RST, DELAY, PWR>,
spi: &mut SPI,
display: &mut Display2in13,
delay: &mut DELAY,
prev_x: i32,
new_x: i32,
) -> Result<(), SPI::Error>
where
SPI: embedded_hal::spi::SpiDevice,
BUSY: embedded_hal::digital::InputPin,
DC: embedded_hal::digital::OutputPin,
RST: embedded_hal::digital::OutputPin,
DELAY: DelayNs,
PWR: embedded_hal::digital::OutputPin,
{
// Erase the previous sprite footprint.
Rectangle::new(Point::new(prev_x, FERRIS_Y), Size::new(FERRIS_W, FERRIS_H))
.into_styled(PrimitiveStyle::with_fill(Color::White))
.draw(display)
.ok();
// Redraw at the new position (ok to ignore: Display2in13's error is Infallible).
draw_ferris(display, new_x).ok();
epd.display_partial(spi, display.buffer(), delay)
}

/// Blit the Ferris sprite at `(x, FERRIS_Y)`.
fn draw_ferris(display: &mut Display2in13, x: i32) -> Result<(), core::convert::Infallible> {
// The raw asset is 1-bit packed (MSB first). Per src/color.rs the on-wire
// encoding is 0 = White, 1 = Black, so we load it as BinaryColor (Off/On)
// and convert at draw time via the Display2in13's DrawTarget impl.
let raw: ImageRaw<BinaryColor> = ImageRaw::new(FERRIS_BYTES, FERRIS_W);
let image = Image::new(&raw, Point::new(x, FERRIS_Y));
image
.draw(&mut display.color_converted::<BinaryColor>())
.ok();
Ok(())
}
Loading
Loading