Skip to main content

ops/
screen.rs

1//! The screen-size record: a 256-byte EEPROM-backed record at a linear flash
2//! address that the page-addressed frames cannot reach.
3//!
4//! A block erase clears it and a firmware write never restores it, so it is
5//! read and set by value. The other `card` commands, one frame each, are
6//! here too.
7
8use crate::model::flash_map;
9use crate::util::{await_reply, open};
10use crate::{protocol, Ctx, Progress};
11use anyhow::{Context, Result};
12use rawlink::Link;
13use receivers::CardModel;
14use std::time::Duration;
15
16/// Byte offsets of the geometry fields within the record.
17const WIDTH: usize = 6;
18const HEIGHT: usize = 8;
19
20/// Read the record off the card.
21///
22/// # Errors
23/// Fails if the card does not answer.
24pub fn read(m: &CardModel, dev: &mut Link, index: u16, wait: u64) -> Result<Vec<u8>> {
25    // The card answers the unrestricted linear read at the EEPROM mirror.
26    dev.send(&flash_map(m).read_screen_record(index))?;
27    // Linear reads answer with a different type than the page-addressed
28    // reads, so take any reply long enough to hold a record.
29    await_reply(dev, Duration::from_secs(wait), |f| {
30        f.get(15..15 + protocol::SCREEN_RECORD_LEN)
31            .map(<[u8]>::to_vec)
32    })?
33    .with_context(|| format!("no screen-size record from the card within {wait}s"))
34}
35
36/// Offsets of the receiver's control area within the record: the rectangle
37/// `(startX, startY) -> (endX, endY)` the card windows incoming pixels
38/// against. `endX`/`endY` are the geometry fields above.
39const START_X: usize = 2;
40const START_Y: usize = 4;
41
42/// True when the record has been erased rather than programmed.
43///
44/// The write path sends all 256 bytes, i.e. every EEPROM record
45/// (docs/eeprom-map.md), so an erased read must never be written back: it
46/// would persist `0xFF` across the control area and the card would drop
47/// every pixel (docs/receiver-identity.md).
48#[must_use]
49pub fn looks_erased(record: &[u8]) -> bool {
50    let empty_window =
51        |o: usize| matches!((record.get(o), record.get(o + 1)), (Some(0xFF), Some(0xFF)));
52    empty_window(START_X)
53        || empty_window(START_Y)
54        || record.iter().fold(0, |n, &b| n + usize::from(b == 0xFF)) > record.len() / 2
55}
56
57/// Geometry encoded in a record.
58#[must_use]
59pub fn geometry(record: &[u8]) -> Option<(u16, u16)> {
60    let be16 = |o| {
61        record
62            .get(o..o + 2)
63            .and_then(|s| s.try_into().ok())
64            .map(u16::from_be_bytes)
65    };
66    Some((be16(WIDTH)?, be16(HEIGHT)?))
67}
68
69/// Show the record, and optionally set the geometry it carries. Returns the
70/// geometry the card holds when the command finishes.
71///
72/// # Errors
73/// Fails if the card does not answer or the write is refused.
74pub fn screen_size(
75    ctx: &Ctx,
76    set: Option<(u16, u16)>,
77    commit: bool,
78    index: u16,
79    wait: u64,
80    p: &mut dyn Progress,
81) -> Result<(u16, u16)> {
82    let m = ctx.model()?;
83    let mut dev = open(ctx)?;
84    let record = read(m, &mut dev, index, wait)?;
85    let (w, h) = geometry(&record).context("the record is too short to hold a geometry")?;
86
87    let Some((nw, nh)) = set else {
88        p.out(&format!("{w}x{h}"));
89        return Ok((w, h));
90    };
91    if (nw, nh) == (w, h) {
92        p.out(&format!("{w}x{h}"));
93        return Ok((w, h));
94    }
95    if looks_erased(&record) {
96        let sx = u16::from_be_bytes([record[START_X], record[START_X + 1]]);
97        let sy = u16::from_be_bytes([record[START_Y], record[START_Y + 1]]);
98        anyhow::bail!(
99            "EEPROM record reads as erased (control area starts at {sx},{sy}); \
100             writing it back would persist 0xFF across every record in it \
101             (docs/eeprom-map.md); restore it first: \
102             python3 scripts/eeprom-restore.py --commit"
103        );
104    }
105    if !commit {
106        p.out(&format!("{w}x{h} -> {nw}x{nh} (dry run; add --commit)"));
107        return Ok((w, h));
108    }
109
110    let mut updated = record;
111    updated[WIDTH..WIDTH + 2].copy_from_slice(&nw.to_be_bytes());
112    updated[HEIGHT..HEIGHT + 2].copy_from_slice(&nh.to_be_bytes());
113    let map = flash_map(m);
114    dev.send(&map.write_screen_record(index, map.screen_record_addr, &updated)?)?;
115    std::thread::sleep(Duration::from_millis(200));
116
117    let after = read(m, &mut dev, index, wait)?;
118    let got = match geometry(&after) {
119        Some((aw, ah)) if (aw, ah) == (nw, nh) => {
120            p.out(&format!("{aw}x{ah}"));
121            (aw, ah)
122        }
123        Some((aw, ah)) => anyhow::bail!("wrote {nw}x{nh} but the card reads back {aw}x{ah}"),
124        None => anyhow::bail!("the card returned an unreadable record"),
125    };
126    p.err("power-cycle the card to apply");
127    Ok(got)
128}
129
130/// Ask the card to reload its parameters from flash; `full` sends the
131/// vendor's post-save frame instead of the bare reload.
132///
133/// # Errors
134/// Fails if the link cannot be opened.
135pub fn reload(ctx: &Ctx, index: u16, full: bool) -> Result<()> {
136    let mut dev = open(ctx)?;
137    if full {
138        dev.send(&protocol::reload_params_full(index))?;
139    } else {
140        dev.send(&protocol::reload_params(index))?;
141    }
142    Ok(())
143}
144
145/// Select the card's built-in test pattern; 0 is off.
146///
147/// # Errors
148/// Fails if the link cannot be opened.
149pub fn test_mode(ctx: &Ctx, index: u16, pattern: u8) -> Result<()> {
150    let mut dev = open(ctx)?;
151    dev.send(&protocol::test_mode(index, pattern))?;
152    Ok(())
153}
154
155/// Step through the card's test patterns, pausing on each, then switch off.
156///
157/// # Errors
158/// Fails if the link cannot be opened.
159pub fn test_sweep(ctx: &Ctx, count: u8, secs: u64, index: u16, p: &mut dyn Progress) -> Result<()> {
160    let mut dev = open(ctx)?;
161    for pattern in 0..count {
162        p.out(&format!("pattern {pattern}"));
163        dev.send(&protocol::test_mode(index, pattern))?;
164        std::thread::sleep(Duration::from_secs(secs));
165    }
166    dev.send(&protocol::test_mode(index, 0))?;
167    Ok(())
168}
169
170/// Tell the card its own size and the size of the whole screen.
171///
172/// # Errors
173/// Fails if the link cannot be opened.
174pub fn set_layout(ctx: &Ctx, index: u16, panel_width: u16, panel_height: u16) -> Result<()> {
175    let mut dev = open(ctx)?;
176    dev.send(&protocol::set_layout(
177        index,
178        panel_width,
179        panel_height,
180        0,
181        0,
182        panel_width,
183        panel_height,
184    ))?;
185    Ok(())
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn geometry_is_read_big_endian_from_the_documented_offsets() {
194        let mut r = vec![0u8; protocol::SCREEN_RECORD_LEN];
195        r[WIDTH] = 0x00;
196        r[WIDTH + 1] = 0x80;
197        r[HEIGHT] = 0x00;
198        r[HEIGHT + 1] = 0x40;
199        assert_eq!(geometry(&r), Some((128, 64)));
200    }
201
202    #[test]
203    fn a_short_record_has_no_geometry() {
204        assert_eq!(geometry(&[0u8; 4]), None);
205    }
206
207    #[test]
208    fn an_erased_record_is_recognised_before_it_can_be_written_back() {
209        // The exact shape the card was left in: geometry restored, control
210        // area still erased. `discover` reports a healthy 128x64 in this
211        // state, so the geometry fields alone cannot be the check.
212        let mut r = vec![0u8; protocol::SCREEN_RECORD_LEN];
213        r[START_X..START_X + 4].copy_from_slice(&[0xFF; 4]);
214        r[WIDTH..WIDTH + 2].copy_from_slice(&128u16.to_be_bytes());
215        r[HEIGHT..HEIGHT + 2].copy_from_slice(&64u16.to_be_bytes());
216        assert_eq!(geometry(&r), Some((128, 64)), "geometry still reads fine");
217        assert!(looks_erased(&r), "but the record must not be written back");
218    }
219
220    #[test]
221    fn a_wholly_erased_record_is_recognised() {
222        assert!(looks_erased(&[0xFFu8; protocol::SCREEN_RECORD_LEN]));
223    }
224
225    #[test]
226    fn the_factory_record_is_accepted() {
227        let mut r = vec![0u8; protocol::SCREEN_RECORD_LEN];
228        r[WIDTH..WIDTH + 2].copy_from_slice(&128u16.to_be_bytes());
229        r[HEIGHT..HEIGHT + 2].copy_from_slice(&64u16.to_be_bytes());
230        assert!(!looks_erased(&r));
231    }
232}