Skip to main content

ops/
display.rs

1//! Putting images on the wall: patterns, stills, and video. Every content
2//! command sends through `driver::Wall`, so the frame recipe lives once.
3
4use crate::util::open;
5use crate::{protocol, Ctx, Progress};
6use anyhow::{Context, Result};
7use wall::{Canvas, Frame};
8use sources::{Fit, FrameSource};
9use std::time::Duration;
10
11/// Load a wall layout, or build a single-panel one from the size flags.
12pub fn load_canvas(ctx: &Ctx, layout: Option<&str>) -> Result<Canvas> {
13    match layout {
14        Some(path) => {
15            let text = std::fs::read_to_string(path).with_context(|| format!("read {path}"))?;
16            serde_json::from_str(&text).with_context(|| format!("parse {path}"))
17        }
18        None => Ok(Canvas::single(u32::from(ctx.width), u32::from(ctx.height))),
19    }
20}
21
22/// An environment override for bench experiments, or the measured default.
23fn env_or<T: std::str::FromStr>(name: &str, default: T) -> T {
24    std::env::var(name)
25        .ok()
26        .and_then(|v| v.parse().ok())
27        .unwrap_or(default)
28}
29
30/// Driver settings from the CLI flags. `RXP_LATCHES`, `RXP_LATCH_GAP_US`
31/// and `RXP_ROW_GAP_US` override the measured timing for experiments.
32pub fn wall_settings(ctx: &Ctx) -> driver::Settings {
33    let t = driver::Timing::default();
34    let micros = |name, d: Duration| Duration::from_micros(env_or(name, d.as_micros() as u64));
35    driver::Settings {
36        brightness: ctx.brightness,
37        color_order: ctx.order,
38        announce_layout: false,
39        timing: driver::Timing {
40            latches: env_or("RXP_LATCHES", t.latches),
41            latch_gap: micros("RXP_LATCH_GAP_US", t.latch_gap),
42            row_gap: micros("RXP_ROW_GAP_US", t.row_gap),
43        },
44    }
45}
46
47/// Refresh period for held stills; `RXP_FRAME_MS` overrides it.
48fn frame_period() -> Duration {
49    Duration::from_millis(env_or("RXP_FRAME_MS", 33))
50}
51
52/// Set the panel's brightness.
53///
54/// The brightness frame, then the latches that commit it, in the order and
55/// with the gap a refresh uses: one latch with no gap leaves a held frame at
56/// its old brightness (docs/rendering.md).
57///
58/// # Errors
59/// Fails if the link cannot be opened.
60pub fn brightness(ctx: &Ctx, value: u8) -> Result<()> {
61    let timing = wall_settings(ctx).timing;
62    let mut dev = open(ctx)?;
63    dev.send(&protocol::brightness(value))?;
64    std::thread::sleep(timing.latch_gap);
65    for _ in 0..timing.latches {
66        dev.send(&protocol::sync(value))?;
67    }
68    Ok(())
69}
70
71
72/// Play a video source onto the wall in `layout`, or a single panel.
73pub fn play(
74    ctx: &Ctx,
75    input: &str,
76    fps: u32,
77    fit: &str,
78    looping: bool,
79    layout: Option<&str>,
80    p: &mut dyn Progress,
81) -> Result<()> {
82    let canvas = load_canvas(ctx, layout)?;
83    let fit: Fit = fit.parse()?;
84    play_on(ctx, canvas, input, fps, fit, looping, p)
85}
86
87/// Play a video source onto `canvas`. Reports `N frames, F fps` every 60
88/// frames as a transient line and once at the end; stops when cancelled.
89#[allow(clippy::too_many_arguments)]
90pub fn play_on(
91    ctx: &Ctx,
92    canvas: Canvas,
93    input: &str,
94    fps: u32,
95    fit: Fit,
96    looping: bool,
97    p: &mut dyn Progress,
98) -> Result<()> {
99    let mut source =
100        sources::VideoSource::open(input, canvas.width, canvas.height, fps, fit, looping)?;
101    let mut frame = Frame::black(canvas.width, canvas.height);
102    let mut wall = driver::Wall::open(&ctx.iface, canvas, wall_settings(ctx))?;
103    let mut pacer = driver::Pacer::new(fps);
104
105    while !p.cancelled() && source.next_frame(&mut frame)? {
106        wall.show(&frame)?;
107        pacer.wait();
108        if wall.frames_sent().is_multiple_of(60) {
109            p.transient(&format!(
110                "{} frames, {:.1} fps",
111                wall.frames_sent(),
112                pacer.achieved_fps()
113            ));
114        }
115    }
116    p.clear_transient();
117    p.out(&format!(
118        "{} frames, {:.1} fps",
119        wall.frames_sent(),
120        pacer.achieved_fps()
121    ));
122    Ok(())
123}
124
125/// Show one still: three refreshes, or refresh until cancelled when `hold`.
126pub fn show_frame(
127    ctx: &Ctx,
128    canvas: Canvas,
129    frame: &Frame,
130    hold: bool,
131    p: &mut dyn Progress,
132) -> Result<()> {
133    let mut wall = driver::Wall::open(&ctx.iface, canvas, wall_settings(ctx))?;
134    let period = frame_period();
135    if hold {
136        while !p.cancelled() {
137            wall.show(frame)?;
138            std::thread::sleep(period);
139        }
140        return Ok(());
141    }
142    // Three, so at least one lands after the card settles.
143    for _ in 0..3 {
144        wall.show(frame)?;
145        std::thread::sleep(period);
146    }
147    Ok(())
148}
149
150/// Draw a built-in pattern on the wall in `layout`, or a single panel.
151pub fn show_pattern(
152    ctx: &Ctx,
153    name: &str,
154    hold: bool,
155    layout: Option<&str>,
156    p: &mut dyn Progress,
157) -> Result<()> {
158    let canvas = load_canvas(ctx, layout)?;
159    show_pattern_on(ctx, canvas, name, hold, p)
160}
161
162/// Draw a built-in pattern on `canvas`.
163pub fn show_pattern_on(
164    ctx: &Ctx,
165    canvas: Canvas,
166    name: &str,
167    hold: bool,
168    p: &mut dyn Progress,
169) -> Result<()> {
170    let pattern: sources::Pattern = name.parse()?;
171    let frame = sources::pattern(pattern, canvas.width, canvas.height);
172    show_frame(ctx, canvas, &frame, hold, p)
173}
174
175/// Fill the panel with one colour.
176pub fn show_solid(ctx: &Ctx, rgb: [u8; 3], hold: bool, p: &mut dyn Progress) -> Result<()> {
177    let canvas = load_canvas(ctx, None)?;
178    show_solid_on(ctx, canvas, rgb, hold, p)
179}
180
181/// Fill `canvas` with one colour.
182pub fn show_solid_on(
183    ctx: &Ctx,
184    canvas: Canvas,
185    rgb: [u8; 3],
186    hold: bool,
187    p: &mut dyn Progress,
188) -> Result<()> {
189    let frame = Frame::from_rgb(
190        canvas.width,
191        canvas.height,
192        rgb.repeat((canvas.width * canvas.height) as usize),
193    )?;
194    show_frame(ctx, canvas, &frame, hold, p)
195}
196
197/// Display an image file, scaled to the panel.
198pub fn show_image(ctx: &Ctx, path: &str, hold: bool, p: &mut dyn Progress) -> Result<()> {
199    let canvas = load_canvas(ctx, None)?;
200    let img = image::open(path).with_context(|| format!("open image {path}"))?;
201    let frame = image_frame(&img, &canvas, Fit::Stretch)?;
202    show_frame(ctx, canvas, &frame, hold, p)
203}
204
205/// `img` as a canvas-sized frame: `Stretch` ignores the aspect ratio,
206/// `Contain` letterboxes in black, `Cover` crops. Lanczos3 throughout.
207///
208/// # Errors
209/// Fails if the resampled image does not match the canvas size.
210pub fn image_frame(img: &image::DynamicImage, canvas: &Canvas, fit: Fit) -> Result<Frame> {
211    use image::imageops::FilterType::Lanczos3;
212    let (w, h) = (canvas.width, canvas.height);
213    let rgb = match fit {
214        Fit::Stretch => img.resize_exact(w, h, Lanczos3).to_rgb8(),
215        Fit::Cover => img.resize_to_fill(w, h, Lanczos3).to_rgb8(),
216        Fit::Contain => {
217            let scaled = img.resize(w, h, Lanczos3).to_rgb8();
218            let mut out = image::RgbImage::new(w, h);
219            let x = i64::from((w - scaled.width()) / 2);
220            let y = i64::from((h - scaled.height()) / 2);
221            image::imageops::replace(&mut out, &scaled, x, y);
222            out
223        }
224    };
225    Ok(Frame::from_rgb(w, h, rgb.into_raw())?)
226}
227
228/// Send chosen pieces of a refresh with explicit pacing, so a current meter
229/// can attribute a change to one component instead of to a whole burst.
230/// The pixel content is a solid colour. Diagnosis only.
231pub fn probe(
232    ctx: &Ctx,
233    rows: u16,
234    row_gap_us: u64,
235    sync_after: bool,
236    repeat: u32,
237    rgb: [u8; 3],
238) -> Result<()> {
239    let mut dev = open(ctx)?;
240    let line = vec![rgb; ctx.width as usize];
241    for pass in 0..repeat {
242        for row in 0..rows {
243            dev.send(&protocol::pixel_row(row, 0, &line, ctx.order))?;
244            if row_gap_us > 0 {
245                std::thread::sleep(Duration::from_micros(row_gap_us));
246            }
247        }
248        if sync_after {
249            dev.send(&protocol::sync(ctx.brightness))?;
250        }
251        if repeat > 1 && pass + 1 < repeat {
252            std::thread::sleep(Duration::from_millis(33));
253        }
254    }
255    Ok(())
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    /// The frames a brightness change sends, in order.
263    fn frames(value: u8, latches: u32) -> Vec<Vec<u8>> {
264        let mut out = vec![protocol::brightness(value).to_vec()];
265        out.extend(std::iter::repeat_n(protocol::sync(value).to_vec(), latches as usize));
266        out
267    }
268
269    #[test]
270    fn brightness_sends_the_frame_then_the_measured_number_of_latches() {
271        let t = driver::Timing::default();
272        let f = frames(40, t.latches);
273        assert_eq!(f.len(), 1 + t.latches as usize);
274        assert_eq!(f[0][12..14], [0x0a, 40], "brightness frame type carries the value");
275        assert_eq!(f[0][14..17], [40, 40, 0xff], "brightness block");
276        assert_eq!(f[1][12..14], [0x01, 0x07], "latch frame type");
277        assert_eq!(f[1][35], 40, "master brightness");
278        assert_eq!(f[1][38..41], [40, 40, 40], "channel gains");
279        assert!(f[1..].iter().all(|x| *x == f[1]), "every latch identical");
280    }
281}