···22//!
33//! Based on GxEPD2_426_GDEQ0426T82.cpp by Jean-Marc Zingg
44//! <https://github.com/ZinggJM/GxEPD2>
55-use embedded_graphics_core::{
66- Pixel,
77- draw_target::DrawTarget,
88- geometry::{OriginDimensions, Size},
99- pixelcolor::BinaryColor,
1010-};
55+use embedded_graphics_core::geometry::{OriginDimensions, Size};
116use embedded_hal::digital::{InputPin, OutputPin};
127use embedded_hal::spi::SpiDevice;
138use esp_hal::delay::Delay;
1491010+use super::strip::{StripBuffer, STRIP_BUF_SIZE, STRIP_COUNT};
1111+1512// Display dimensions (physical)
1613pub const WIDTH: u16 = 800;
1714pub const HEIGHT: u16 = 480;
1818-pub const FRAMEBUFFER_SIZE: usize = (WIDTH as usize * HEIGHT as usize) / 8;
19152016// SPI frequency
2117pub const SPI_FREQ_MHZ: u32 = 20;
···3026/// Display rotation
3127#[derive(Clone, Copy, Debug, Default, PartialEq)]
3228pub enum Rotation {
3333- /// Landscape, 800x480, no rotation
3429 #[default]
3530 Deg0,
3636- /// Portrait, 480x800, rotated 90° clockwise
3731 Deg90,
3838- /// Landscape, 800x480, upside down
3932 Deg180,
4040- /// Portrait, 480x800, rotated 270° clockwise
4133 Deg270,
4234}
43354444-// SSD1677 Commands (matching GxEPD2 exactly)
3636+// SSD1677 Commands (matching GxEPD2
4537mod cmd {
4638 pub const DRIVER_OUTPUT_CONTROL: u8 = 0x01;
4739 pub const BOOSTER_SOFT_START: u8 = 0x0C;
···4941 pub const DATA_ENTRY_MODE: u8 = 0x11;
5042 pub const SW_RESET: u8 = 0x12;
5143 pub const TEMPERATURE_SENSOR: u8 = 0x18;
4444+ #[allow(dead_code)]
5245 pub const WRITE_TEMP_REGISTER: u8 = 0x1A;
5346 pub const MASTER_ACTIVATION: u8 = 0x20;
5447 pub const DISPLAY_UPDATE_CONTROL_1: u8 = 0x21;
···6255 pub const SET_RAM_Y_COUNTER: u8 = 0x4F;
6356}
64576565-/// Display driver for SSD1677-based e-paper (GDEQ0426T82)
5858+// Display driver for SSD1677-based e-paper (GDEQ0426T82)
5959+// No framebuffer — rendering is done through StripBuffer.
6060+// The display controller has its own 48KB RAM; we stream into it.
6661pub struct DisplayDriver<SPI, DC, RST, BUSY> {
6762 spi: SPI,
6863 dc: DC,
6964 rst: RST,
7065 busy: BUSY,
7171- framebuffer: [u8; FRAMEBUFFER_SIZE],
7266 rotation: Rotation,
7367 power_is_on: bool,
7468 init_done: bool,
7569 initial_refresh: bool,
7676- initial_write: bool,
7770}
78717972impl<SPI, DC, RST, BUSY, E> DisplayDriver<SPI, DC, RST, BUSY>
···8376 RST: OutputPin,
8477 BUSY: InputPin,
8578{
8686- /// Create a new display driver
7979+ // Create a new display driver
8780 pub fn new(spi: SPI, dc: DC, rst: RST, busy: BUSY) -> Self {
8881 Self {
8982 spi,
9083 dc,
9184 rst,
9285 busy,
9393- framebuffer: [0xFF; FRAMEBUFFER_SIZE], // White
9486 rotation: Rotation::Deg270,
9587 power_is_on: false,
9688 init_done: false,
9789 initial_refresh: true,
9898- initial_write: true,
9990 }
10091 }
10192102102- /// Set display rotation
10393 pub fn set_rotation(&mut self, rotation: Rotation) {
10494 self.rotation = rotation;
10595 }
10696107107- /// Get current rotation
10897 pub fn rotation(&self) -> Rotation {
10998 self.rotation
11099 }
111100112112- /// Get logical display size (accounts for rotation)
113101 pub fn size(&self) -> Size {
114102 match self.rotation {
115103 Rotation::Deg0 | Rotation::Deg180 => Size::new(WIDTH as u32, HEIGHT as u32),
···117105 }
118106 }
119107120120- /// Hardware reset
121108 pub fn reset(&mut self, delay: &mut Delay) {
122109 let _ = self.rst.set_high();
123110 delay.delay_millis(20);
···127114 delay.delay_millis(20);
128115 }
129116130130- /// Initialize the display
131117 pub fn init(&mut self, delay: &mut Delay) {
132118 self.reset(delay);
133119 self.init_display(delay);
134120 }
135121136136- /// Clear the entire screen to white
137122 pub fn clear(&mut self, delay: &mut Delay) {
138138- self.framebuffer.fill(0xFF);
139123 self.clear_screen(delay);
140124 }
141125142142- /// Fill framebuffer only (no display update)
143143- pub fn fill_white(&mut self) {
144144- self.framebuffer.fill(0xFF);
145145- }
146146-147147- /// Fill framebuffer with black (no display update)
148148- pub fn fill_black(&mut self) {
149149- self.framebuffer.fill(0x00);
150150- }
151151-152152- /// Set a pixel in the framebuffer using LOGICAL coordinates
153153- /// Coordinates are transformed based on rotation
154154- pub fn set_pixel(&mut self, x: u16, y: u16, black: bool) {
155155- // Get logical dimensions for bounds check
156156- let (log_w, log_h) = match self.rotation {
157157- Rotation::Deg0 | Rotation::Deg180 => (WIDTH, HEIGHT),
158158- Rotation::Deg90 | Rotation::Deg270 => (HEIGHT, WIDTH),
159159- };
160160-161161- if x >= log_w || y >= log_h {
162162- return;
163163- }
164164-165165- // Transform logical → physical coordinates
166166- let (px, py) = match self.rotation {
167167- Rotation::Deg0 => (x, y),
168168- Rotation::Deg90 => (WIDTH - 1 - y, x),
169169- Rotation::Deg180 => (WIDTH - 1 - x, HEIGHT - 1 - y),
170170- Rotation::Deg270 => (y, HEIGHT - 1 - x),
171171- };
172172-173173- let idx = (px as usize / 8) + (py as usize * (WIDTH as usize / 8));
174174- let bit = 7 - (px % 8);
175175- if black {
176176- self.framebuffer[idx] &= !(1 << bit);
177177- } else {
178178- self.framebuffer[idx] |= 1 << bit;
179179- }
180180- }
181181-182182- /// Fill framebuffer with color (true = black, false = white)
183183- pub fn fill(&mut self, black: bool) {
184184- self.framebuffer.fill(if black { 0x00 } else { 0xFF });
185185- }
186186-187187- /// Full screen refresh (use after initial setup or to clear ghosting)
188188- pub fn refresh_full(&mut self, delay: &mut Delay) {
126126+ pub fn render_full<F>(&mut self, strip: &mut StripBuffer, delay: &mut Delay, draw: F)
127127+ where
128128+ F: Fn(&mut StripBuffer),
129129+ {
189130 if !self.init_done {
190131 self.init_display(delay);
191132 }
192133193193- // Small delay to ensure display is ready
194134 delay.delay_millis(1);
195135196196- // Write to both buffers for full refresh
197197- self.set_partial_ram_area(0, 0, WIDTH, HEIGHT);
198198- self.write_full_buffer(cmd::WRITE_RAM_RED); // Previous
199199-200200- delay.delay_millis(1); // Yield between large transfers
136136+ // Write to both display RAM buffers via strips
137137+ for &ram_cmd in &[cmd::WRITE_RAM_RED, cmd::WRITE_RAM_BW] {
138138+ self.set_partial_ram_area(0, 0, WIDTH, HEIGHT);
139139+ self.send_command(ram_cmd);
140140+ delay.delay_millis(1);
201141202202- self.set_partial_ram_area(0, 0, WIDTH, HEIGHT);
203203- self.write_full_buffer(cmd::WRITE_RAM_BW); // Current
142142+ for i in 0..STRIP_COUNT {
143143+ strip.begin_strip(self.rotation, i);
144144+ draw(strip);
145145+ self.send_data(strip.data());
146146+ }
147147+ }
204148205149 self.update_full(delay);
206150 self.initial_refresh = false;
207207- self.initial_write = false;
208151 }
209152210210- /// Partial screen refresh
211211- /// Takes LOGICAL coordinates
212212- pub fn refresh_partial(&mut self, x: u16, y: u16, w: u16, h: u16, delay: &mut Delay) {
153153+ /// Render a partial region and do a partial refresh.
154154+ pub fn render_partial<F>(
155155+ &mut self,
156156+ strip: &mut StripBuffer,
157157+ x: u16,
158158+ y: u16,
159159+ w: u16,
160160+ h: u16,
161161+ delay: &mut Delay,
162162+ draw: F,
163163+ ) where
164164+ F: Fn(&mut StripBuffer),
165165+ {
213166 // Initial refresh must be full
214167 if self.initial_refresh {
215215- return self.refresh_full(delay);
168168+ return self.render_full(strip, delay, draw);
216169 }
217170218171 if !self.init_done {
···240193 return;
241194 }
242195243243- // Step 1: Write to current buffer (0x24) only
244244- self.write_image_partial_physical(cmd::WRITE_RAM_BW, px, py, pw, ph);
196196+ self.write_region_strips(strip, px, py, pw, ph, cmd::WRITE_RAM_BW, &draw);
245197246246- // Step 2: Partial refresh
247198 self.set_partial_ram_area(px, py, pw, ph);
248199 self.update_partial(delay);
249200250250- // Step 3: Sync buffers - write to BOTH previous (0x26) AND current (0x24)
251251- // This is writeImageAgain() in GxEPD2
252252- self.write_image_partial_physical(cmd::WRITE_RAM_RED, px, py, pw, ph);
253253- self.write_image_partial_physical(cmd::WRITE_RAM_BW, px, py, pw, ph);
201201+ self.write_region_strips(strip, px, py, pw, ph, cmd::WRITE_RAM_RED, &draw);
202202+ self.write_region_strips(strip, px, py, pw, ph, cmd::WRITE_RAM_BW, &draw);
254203255255- // Step 4: Power off display controller to save power.
256256- // E-paper retains image without power. Leaving it on draws ~15mA idle.
257204 self.power_off(delay);
258205 }
259206260260- /// Transform logical region to physical region based on rotation
261261- fn transform_region(&self, x: u16, y: u16, w: u16, h: u16) -> (u16, u16, u16, u16) {
262262- match self.rotation {
263263- Rotation::Deg0 => (x, y, w, h),
264264- Rotation::Deg90 => {
265265- // Logical (x,y,w,h) in 480x800 → Physical in 800x480
266266- // Logical top-left (x,y) → Physical (WIDTH-1-y, x)
267267- // But we need the physical top-left of the region
268268- (WIDTH - y - h, x, h, w)
269269- }
270270- Rotation::Deg180 => (WIDTH - x - w, HEIGHT - y - h, w, h),
271271- Rotation::Deg270 => (y, HEIGHT - x - w, h, w),
272272- }
273273- }
274274-275275- /// Refresh a rectangular window (convenience method)
276276- pub fn refresh_window(&mut self, x: u16, y: u16, w: u16, h: u16, delay: &mut Delay) {
277277- self.refresh_partial(x, y, w, h, delay);
207207+ // partial refresh with a region tuple
208208+ pub fn render_window<F>(
209209+ &mut self,
210210+ strip: &mut StripBuffer,
211211+ x: u16,
212212+ y: u16,
213213+ w: u16,
214214+ h: u16,
215215+ delay: &mut Delay,
216216+ draw: F,
217217+ ) where
218218+ F: Fn(&mut StripBuffer),
219219+ {
220220+ self.render_partial(strip, x, y, w, h, delay, draw);
278221 }
279222280280- /// Power off the display (reduces power consumption, prevents fading)
223223+ // Power off the display
281224 pub fn power_off(&mut self, delay: &mut Delay) {
282225 if self.power_is_on {
283226 self.send_command(cmd::DISPLAY_UPDATE_CONTROL_2);
···296239 self.init_done = false;
297240 }
298241299299- // ========== Private methods ==========
242242+ /// Write a physical region to display RAM using strip iteration.
243243+ /// Handles regions larger than the strip buffer by splitting into
244244+ /// multiple passes with as many rows as fit.
245245+ fn write_region_strips<F>(
246246+ &mut self,
247247+ strip: &mut StripBuffer,
248248+ px: u16,
249249+ py: u16,
250250+ pw: u16,
251251+ ph: u16,
252252+ ram_cmd: u8,
253253+ draw: &F,
254254+ ) where
255255+ F: Fn(&mut StripBuffer),
256256+ {
257257+ let max_rows = StripBuffer::max_rows_for_width(pw);
258258+259259+ self.set_partial_ram_area(px, py, pw, ph);
260260+ self.send_command(ram_cmd);
261261+262262+ let mut y = py;
263263+ while y < py + ph {
264264+ let rows = max_rows.min(py + ph - y);
265265+ strip.begin_window(self.rotation, px, y, pw, rows);
266266+ draw(strip);
267267+ self.send_data(strip.data());
268268+ y += rows;
269269+ }
270270+ }
300271301272 fn init_display(&mut self, delay: &mut Delay) {
302273 // Software reset
···329300 self.init_done = true;
330301 }
331302303303+ // Transform logical region to physical region based on rotation
304304+ fn transform_region(&self, x: u16, y: u16, w: u16, h: u16) -> (u16, u16, u16, u16) {
305305+ match self.rotation {
306306+ Rotation::Deg0 => (x, y, w, h),
307307+ Rotation::Deg90 => {
308308+ // Logical (x,y,w,h) in 480x800 → Physical in 800x480
309309+ // Logical top-left (x,y) → Physical (WIDTH-1-y, x)
310310+ // But we need the physical top-left of the region
311311+ (WIDTH - y - h, x, h, w)
312312+ }
313313+ Rotation::Deg180 => (WIDTH - x - w, HEIGHT - y - h, w, h),
314314+ Rotation::Deg270 => (y, HEIGHT - x - w, h, w),
315315+ }
316316+ }
317317+332318 fn set_partial_ram_area(&mut self, x: u16, y: u16, w: u16, h: u16) {
333319 // Gates are reversed on this display - flip Y
334320 let y_flipped = HEIGHT - y - h;
···372358 self.init_display(delay);
373359 }
374360375375- // Write white to both buffers
361361+ // write white to both buffers
376362 self.set_partial_ram_area(0, 0, WIDTH, HEIGHT);
377363 self.write_screen_buffer(cmd::WRITE_RAM_RED, 0xFF);
378364 self.set_partial_ram_area(0, 0, WIDTH, HEIGHT);
379365 self.write_screen_buffer(cmd::WRITE_RAM_BW, 0xFF);
380366381367 self.update_full(delay);
382382- self.initial_write = false;
383368 self.initial_refresh = false;
384369 }
385370···395380 }
396381 }
397382398398- fn write_full_buffer(&mut self, command: u8) {
399399- self.send_command(command);
400400- // Write in normal row order - gate reversal is handled by RAM address setup
401401- // (Y-decrease mode in _setPartialRamArea), NOT by reversing rows here
402402- let bytes_per_row = (WIDTH / 8) as usize;
403403-404404- // Use a temporary buffer to avoid borrow checker issues
405405- let mut row_buf = [0u8; 256];
406406-407407- for row in 0..HEIGHT as usize {
408408- let start = row * bytes_per_row;
409409- // Write row in chunks to avoid issues
410410- for chunk_start in (0..bytes_per_row).step_by(256) {
411411- let chunk_end = (chunk_start + 256).min(bytes_per_row);
412412- let chunk_len = chunk_end - chunk_start;
413413-414414- // Copy to temp buffer
415415- row_buf[..chunk_len]
416416- .copy_from_slice(&self.framebuffer[start + chunk_start..start + chunk_end]);
417417- self.send_data(&row_buf[..chunk_len]);
418418- }
419419- }
420420- }
421421-422422- fn write_image_partial_physical(&mut self, command: u8, x: u16, y: u16, w: u16, h: u16) {
423423- self.set_partial_ram_area(x, y, w, h);
424424- self.send_command(command);
425425-426426- let bytes_per_row = (WIDTH / 8) as usize;
427427- let window_bytes = (w / 8) as usize;
428428- let x_byte = (x / 8) as usize;
429429-430430- // Use a temporary buffer to avoid borrow checker issues
431431- // Max window width is 800/8 = 100 bytes per row
432432- let mut row_buf = [0u8; 100];
433433-434434- // Write rows in NORMAL order (row 0, 1, 2, ...)
435435- // Gate reversal is handled by the RAM address setup, not here!
436436- // x, y, w, h are PHYSICAL coordinates
437437- for row in 0..h as usize {
438438- let src_row = y as usize + row;
439439- let src_start = src_row * bytes_per_row + x_byte;
440440-441441- // Copy to temp buffer
442442- row_buf[..window_bytes]
443443- .copy_from_slice(&self.framebuffer[src_start..src_start + window_bytes]);
444444- self.send_data(&row_buf[..window_bytes]);
445445- }
446446- }
447447-448383 fn update_full(&mut self, delay: &mut Delay) {
449449- // Display Update Control 1: bypass RED as 0
450384 self.send_command(cmd::DISPLAY_UPDATE_CONTROL_1);
451385 self.send_data(&[0x40, 0x00]);
452386453453- // Use standard full refresh (0xF7) - more reliable than fast mode (0xD7)
454454- // Fast mode requires temperature register which may not work on all panels
455387 self.send_command(cmd::DISPLAY_UPDATE_CONTROL_2);
456388 self.send_data(&[0xF7]);
457389···462394 }
463395464396 fn update_partial(&mut self, delay: &mut Delay) {
465465- // Display Update Control 1: RED normal
466397 self.send_command(cmd::DISPLAY_UPDATE_CONTROL_1);
467398 self.send_data(&[0x00, 0x00]);
468399···499430}
500431501432// embedded-graphics integration
433433+// NOTE: size queriies only, drawing goes through StripBuffer
502434503435impl<SPI, DC, RST, BUSY, E> OriginDimensions for DisplayDriver<SPI, DC, RST, BUSY>
504436where
···514446 }
515447 }
516448}
517517-518518-impl<SPI, DC, RST, BUSY, E> DrawTarget for DisplayDriver<SPI, DC, RST, BUSY>
519519-where
520520- SPI: SpiDevice<Error = E>,
521521- DC: OutputPin,
522522- RST: OutputPin,
523523- BUSY: InputPin,
524524-{
525525- type Color = BinaryColor;
526526- type Error = core::convert::Infallible;
527527-528528- fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
529529- where
530530- I: IntoIterator<Item = Pixel<Self::Color>>,
531531- {
532532- let size = self.size();
533533- let log_w = size.width as i32;
534534- let log_h = size.height as i32;
535535-536536- for Pixel(coord, color) in pixels {
537537- // Bounds check against LOGICAL dimensions
538538- if coord.x >= 0 && coord.x < log_w && coord.y >= 0 && coord.y < log_h {
539539- self.set_pixel(
540540- coord.x as u16,
541541- coord.y as u16,
542542- color == BinaryColor::On, // On = black (foreground), Off = white (background)
543543- );
544544- }
545545- }
546546- Ok(())
547547- }
548548-}
+3-1
src/board/mod.rs
···77pub mod button;
88pub mod display;
99pub mod pins;
1010+pub mod strip;
10111112pub use button::{Button, ROW1_THRESHOLDS, ROW2_THRESHOLDS, decode_ladder};
1212-pub use display::{DisplayDriver, FRAMEBUFFER_SIZE, HEIGHT, SPI_FREQ_MHZ, WIDTH};
1313+pub use display::{DisplayDriver, HEIGHT, SPI_FREQ_MHZ, WIDTH};
1414+pub use strip::StripBuffer;
13151416use embedded_hal_bus::spi::ExclusiveDevice;
1517use esp_hal::{
+209
src/board/strip.rs
···11+// Strip-based rendering buffer
22+//
33+// Instead of holding a full 48KB framebuffer in SRAM, we render
44+// through a small strip buffer (~4KB) and stream each strip to
55+// the display controller via SPI.
66+// The display is divided into horizontal bands of physical rows.
77+// For each band:
88+// 1. Clear the strip buffer to white
99+// 2. Draw all widgets (DrawTarget clips to current band)
1010+// 3. SPI transfer the strip data to display controller RAM
1111+// 4. Reuse the buffer for the next band
1212+1313+use embedded_graphics_core::{
1414+ Pixel,
1515+ draw_target::DrawTarget,
1616+ geometry::{OriginDimensions, Size},
1717+ pixelcolor::BinaryColor,
1818+};
1919+2020+use super::display::{HEIGHT, Rotation, WIDTH};
2121+2222+// Physical rows per strip for full-page rendering.
2323+// 480 / 40 = 12 strips, each 100 bytes/row × 40 rows = 4000 bytes.
2424+pub const STRIP_ROWS: u16 = 40;
2525+pub const PHYS_BYTES_PER_ROW: usize = (WIDTH as usize) / 8; // 100
2626+2727+pub const STRIP_BUF_SIZE: usize = PHYS_BYTES_PER_ROW * STRIP_ROWS as usize; // 4000
2828+pub const STRIP_COUNT: u16 = HEIGHT / STRIP_ROWS; // 12
2929+3030+3131+// A small rendering buffer that covers a physical rectangle of the display.
3232+//
3333+// Operates in two modes:
3434+// - Full-width strips: For full-page rendering. Covers 800×40 physical
3535+// pixels (4000 bytes). Iterate 12 strips top-to-bottom.
3636+// - Windowed: For partial refresh of small UI regions. Covers an
3737+// arbitrary physical rectangle that fits within STRIP_BUF_SIZE bytes.
3838+pub struct StripBuffer {
3939+ buf: [u8; STRIP_BUF_SIZE],
4040+ rotation: Rotation,
4141+ // Physical window this strip covers
4242+ win_x: u16,
4343+ win_y: u16,
4444+ win_w: u16,
4545+ win_h: u16,
4646+ // Derived from win_w for indexing
4747+ row_bytes: u16,
4848+}
4949+5050+impl StripBuffer {
5151+ pub const fn new() -> Self {
5252+ Self {
5353+ buf: [0xFF; STRIP_BUF_SIZE], // White
5454+ rotation: Rotation::Deg270,
5555+ win_x: 0,
5656+ win_y: 0,
5757+ win_w: WIDTH,
5858+ win_h: STRIP_ROWS,
5959+ row_bytes: (WIDTH / 8),
6060+ }
6161+ }
6262+6363+ // Configure for full-width strip rendering at the given strip index.
6464+ // Clears the buffer to white.
6565+ //
6666+ // strip_idx: 0..STRIP_COUNT (physical row bands top-to-bottom)
6767+ pub fn begin_strip(&mut self, rotation: Rotation, strip_idx: u16) {
6868+ self.rotation = rotation;
6969+ self.win_x = 0;
7070+ self.win_y = strip_idx * STRIP_ROWS;
7171+ self.win_w = WIDTH;
7272+ self.win_h = STRIP_ROWS;
7373+ self.row_bytes = PHYS_BYTES_PER_ROW as u16;
7474+7575+ // Clear to white
7676+ self.buf[..STRIP_BUF_SIZE].fill(0xFF);
7777+ }
7878+7979+ // Configure for an arbitrary physical window (partial refresh mode).
8080+ // Region must be byte-aligned (x and w multiples of 8).
8181+ // NOTE: Panics if the window doesn't fit in STRIP_BUF_SIZE.
8282+ pub fn begin_window(&mut self, rotation: Rotation, x: u16, y: u16, w: u16, h: u16) {
8383+ let rb = (w / 8) as usize;
8484+ let total = rb * h as usize;
8585+ assert!(
8686+ total <= STRIP_BUF_SIZE,
8787+ "partial region {}×{} = {} bytes exceeds strip buffer ({})",
8888+ w,
8989+ h,
9090+ total,
9191+ STRIP_BUF_SIZE,
9292+ );
9393+9494+ self.rotation = rotation;
9595+ self.win_x = x;
9696+ self.win_y = y;
9797+ self.win_w = w;
9898+ self.win_h = h;
9999+ self.row_bytes = rb as u16;
100100+101101+ self.buf[..total].fill(0xFF);
102102+ }
103103+104104+ // Get the valid data bytes for SPI transfer.
105105+ // Only the bytes covering the current window are returned.
106106+ pub fn data(&self) -> &[u8] {
107107+ let total = self.row_bytes as usize * self.win_h as usize;
108108+ &self.buf[..total]
109109+ }
110110+111111+ // Current window's physical origin and size.
112112+ pub fn window(&self) -> (u16, u16, u16, u16) {
113113+ (self.win_x, self.win_y, self.win_w, self.win_h)
114114+ }
115115+116116+ pub const fn strip_count() -> u16 {
117117+ STRIP_COUNT
118118+ }
119119+120120+ // Max rows that fit in the buffer at a given window width.
121121+ pub fn max_rows_for_width(width: u16) -> u16 {
122122+ let rb = (width / 8) as usize;
123123+ if rb == 0 {
124124+ return 0;
125125+ }
126126+ (STRIP_BUF_SIZE / rb) as u16
127127+ }
128128+129129+ // Transform logical coordinates to physical based on rotation.
130130+ #[inline]
131131+ fn to_physical(&self, lx: u16, ly: u16) -> (u16, u16) {
132132+ match self.rotation {
133133+ Rotation::Deg0 => (lx, ly),
134134+ Rotation::Deg90 => (WIDTH - 1 - ly, lx),
135135+ Rotation::Deg180 => (WIDTH - 1 - lx, HEIGHT - 1 - ly),
136136+ Rotation::Deg270 => (ly, HEIGHT - 1 - lx),
137137+ }
138138+ }
139139+140140+ // set a pixel in the buffer using physical coordinates.
141141+ // silently clips if outside current window.
142142+ #[inline]
143143+ fn set_pixel_physical(&mut self, px: u16, py: u16, black: bool) {
144144+ // clip to window
145145+ if px < self.win_x || px >= self.win_x + self.win_w {
146146+ return;
147147+ }
148148+ if py < self.win_y || py >= self.win_y + self.win_h {
149149+ return;
150150+ }
151151+152152+ let local_x = (px - self.win_x) as usize;
153153+ let local_y = (py - self.win_y) as usize;
154154+ let idx = (local_x / 8) + (local_y * self.row_bytes as usize);
155155+ let bit = 7 - (local_x as u16 % 8);
156156+157157+ if black {
158158+ self.buf[idx] &= !(1 << bit);
159159+ } else {
160160+ self.buf[idx] |= 1 << bit;
161161+ }
162162+ }
163163+}
164164+165165+impl Default for StripBuffer {
166166+ fn default() -> Self {
167167+ Self::new()
168168+ }
169169+}
170170+171171+// embedded-graphics integration
172172+173173+impl OriginDimensions for StripBuffer {
174174+ // Report FULL logical display size.
175175+ // Widgets think they're drawing to the entire screen;
176176+ // the strip clips at the physical level.
177177+ fn size(&self) -> Size {
178178+ match self.rotation {
179179+ Rotation::Deg0 | Rotation::Deg180 => Size::new(WIDTH as u32, HEIGHT as u32),
180180+ Rotation::Deg90 | Rotation::Deg270 => Size::new(HEIGHT as u32, WIDTH as u32),
181181+ }
182182+ }
183183+}
184184+185185+impl DrawTarget for StripBuffer {
186186+ type Color = BinaryColor;
187187+ type Error = core::convert::Infallible;
188188+189189+ fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
190190+ where
191191+ I: IntoIterator<Item = Pixel<Self::Color>>,
192192+ {
193193+ let size = self.size();
194194+ let log_w = size.width as i32;
195195+ let log_h = size.height as i32;
196196+197197+ for Pixel(coord, color) in pixels {
198198+ // Bounds check against logical dimensions
199199+ if coord.x < 0 || coord.x >= log_w || coord.y < 0 || coord.y >= log_h {
200200+ continue;
201201+ }
202202+203203+ // Transform logical → physical, then clip to current strip
204204+ let (px, py) = self.to_physical(coord.x as u16, coord.y as u16);
205205+ self.set_pixel_physical(px, py, color == BinaryColor::On);
206206+ }
207207+ Ok(())
208208+ }
209209+}