1use 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
16const WIDTH: usize = 6;
18const HEIGHT: usize = 8;
19
20pub fn read(m: &CardModel, dev: &mut Link, index: u16, wait: u64) -> Result<Vec<u8>> {
25 dev.send(&flash_map(m).read_screen_record(index))?;
27 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
36const START_X: usize = 2;
40const START_Y: usize = 4;
41
42#[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#[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
69pub 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
130pub 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
145pub 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
155pub 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
170pub 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 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}