1use crate::capture::{describe, discover_all, discover_one};
11use crate::flash::{flash_firmware, read_primary_bank, restore_flash};
12use crate::model::{bank_bytes, for_card};
13use crate::util::{hex, open};
14use crate::{check, config, protocol, restore, screen, upgrade, Ctx, Loader, Progress};
15use anyhow::{bail, Context, Result};
16use colorlight::{eeprom, BROADCAST};
17use receivers::Version;
18use std::time::{Duration, Instant};
19
20fn version_of(info: &protocol::DiscoveryInfo) -> Version {
21 Version(info.ver_major, info.ver_minor)
22}
23
24fn wait_for_version(ctx: &Ctx, want: Version, timeout: Duration) -> Result<protocol::DiscoveryInfo> {
25 let deadline = Instant::now() + timeout;
26 loop {
27 if let Some(info) = discover_one(ctx, 2)? {
28 if version_of(&info) == want {
29 return Ok(info);
30 }
31 }
32 if Instant::now() > deadline {
33 bail!("the card did not come back reporting firmware {want}");
34 }
35 std::thread::sleep(Duration::from_secs(1));
36 }
37}
38
39fn install_firmware(
43 ctx: &Ctx,
44 image: &str,
45 backup: &str,
46 guarded: &[u8],
47 wait: u64,
48 p: &mut dyn Progress,
49) -> Result<bool> {
50 let m = ctx.model()?;
51 let img = crate::firmware::load(image, p)?.bytes;
52 let want = &img[..bank_bytes(m).min(img.len())];
53
54 let current = read_primary_bank(m, &mut open(ctx)?, 0, wait, p)?;
55 let differing = |bank_bytes: &[u8]| -> Vec<u8> {
56 m.memory
57 .primary_blocks()
58 .filter(|&b| b != m.memory.parameter_block)
59 .filter(|&b| {
60 let s = usize::from(b) * 0x10000;
61 bank_bytes.get(s..s + 0x10000) != want.get(s..s + 0x10000)
62 })
63 .collect()
64 };
65 let before = differing(¤t);
66 if before.is_empty() {
67 p.err(&format!("firmware: bank already holds {image}"));
68 return Ok(false);
69 }
70 p.err(&format!("firmware: blocks {} differ", hex(&before, ",")));
71
72 p.err("firmware: sdram self-program");
74 upgrade::install(
75 ctx,
76 image,
77 true,
78 colorlight::upgrade::Partition::Primary,
79 120,
80 3000,
81 wait,
82 p,
83 )?;
84
85 let after_sdram = read_primary_bank(m, &mut open(ctx)?, 0, wait, p)?;
87 for &b in differing(&after_sdram).iter().filter(|b| !guarded.contains(b)) {
88 p.err(&format!("firmware: host write 0x{b:02x}"));
89 flash_firmware(ctx, image, backup, true, b..b + 1, 0, wait, p)?;
90 }
91 let final_bank = read_primary_bank(m, &mut open(ctx)?, 0, wait, p)?;
92 let left = differing(&final_bank);
93 if !left.is_empty() {
94 bail!(
95 "firmware: blocks {} still differ from {image} after both write paths",
96 hex(&left, ",")
97 );
98 }
99 p.err("firmware: bank verified");
100 Ok(true)
101}
102
103#[derive(Clone, Debug)]
105pub struct Args<'a> {
106 pub spec_path: &'a str,
108 pub firmware: Option<&'a str>,
112 pub position: (u16, u16),
114 pub index: Option<u16>,
118 pub snapshot_dir: Option<&'a str>,
121 pub commit: bool,
123 pub wait: u64,
125}
126
127#[allow(clippy::too_many_lines)]
133pub fn provision(ctx: &Ctx, a: &Args, load: Loader, p: &mut dyn Progress) -> Result<()> {
134 let Args {
135 spec_path,
136 firmware,
137 position,
138 index,
139 snapshot_dir,
140 commit,
141 wait,
142 } = *a;
143 let spec = panelspec::PanelSpec::load(spec_path)?;
144 let (w, h) = (spec.module.width, spec.module.height);
145 let cards = discover_all(ctx, wait, |i| p.err(&describe(i)))?;
146 let Some(info) = cards.first() else {
147 bail!("no response on {} within {wait}s", ctx.iface);
148 };
149 if cards.len() > 1 && index.is_none() {
151 bail!("{} cards answered discovery; pass --index", cards.len());
152 }
153 let rcv = index.unwrap_or(BROADCAST);
154 let m = match ctx.model {
157 Some(named) => {
158 if let Some(known) = receivers::by_id(info.card_id) {
159 anyhow::ensure!(
160 std::ptr::eq(known, named),
161 "the card answers as {} (id 0x{:02x}), not {}",
162 known.name,
163 info.card_id,
164 named.name
165 );
166 }
167 named
168 }
169 None => for_card(info)?,
170 };
171 let ctx = &Ctx { model: Some(m), ..ctx.clone() };
172 let running = version_of(info);
173 p.err(&format!(
174 "card: {} (id 0x{:02x}), firmware {running}, reports {}x{}",
175 m.name, info.card_id, info.cols, info.rows
176 ));
177 p.err(&format!(
178 "plan: spec {spec_path} ({w}x{h}), cabinet at {},{}",
179 position.0, position.1
180 ));
181 p.err(&format!("plan: {}", eeprom_target(index)));
182 let auto = firmware == Some(crate::firmware::AUTO);
185 let firmware = if auto {
186 let ranked = crate::firmware::select(&spec, m);
187 let chosen = crate::firmware::chosen(&ranked, &crate::firmware::chip_name(&spec))?;
188 let (image, why) = (chosen.image, chosen.why());
189 p.err(&format!("firmware: auto -> {} ({why})", image.name));
190 Some(image.name.as_str())
191 } else {
192 firmware
193 };
194 let want_version = match firmware {
195 Some(fw) => {
196 let r = crate::firmware::resolve(fw)?;
197 let want = match r.image {
198 Some(i) => i.version,
199 None => m
200 .firmware
201 .version_in_name(fw)
202 .with_context(|| format!("no version in the firmware file name {fw} ({})", m.firmware.image_pattern))?,
203 };
204 p.err(&format!(
205 "plan: firmware {} ({}), card to report {want} afterwards",
206 r.path.display(),
207 if r.image.is_some() { "in config/firmware.toml" } else { "not in config/firmware.toml" }
208 ));
209 Some(want)
210 }
211 None => None,
212 };
213 if !commit {
214 p.out("dry run: nothing written (add --commit)");
215 return Ok(());
216 }
217
218 check(p)?;
220 let snap = snapshot_dir.map_or_else(
221 || format!("build/snapshot-{}", unix_seconds()),
222 ToString::to_string,
223 );
224 p.err(&format!("[1/5] snapshot: {snap}"));
225 restore::snapshot(ctx, &snap, 0, wait, p)?;
226 let backup = format!("{snap}/primary-region.bin");
227
228 check(p)?;
230 if let (Some(fw), Some(want)) = (firmware, want_version) {
231 p.err(&format!("[2/5] firmware: {fw}"));
232 if auto && running == want {
233 p.err(&format!("firmware: the card already reports {want}; not installed"));
234 } else {
235 let guarded = m.memory.guarded_blocks(running);
237 if !guarded.is_empty() {
238 p.err(&format!("firmware: {running} guards blocks {} from host writes", hex(guarded, ",")));
239 }
240 if install_firmware(ctx, fw, &backup, guarded, wait, p)? {
241 p.err(&format!("firmware: power-cycle the card now; waiting for {want}"));
242 let info = wait_for_version(ctx, want, Duration::from_mins(10))?;
243 p.err(&format!("firmware: card back on {}", version_of(&info)));
244 std::thread::sleep(Duration::from_secs(12));
247 }
248 }
249 } else {
250 p.err("[2/5] firmware: skipped (no --firmware)");
251 }
252
253 check(p)?;
255 p.err("[3/5] eeprom: reading records");
256 let before = {
257 let mut dev = open(ctx)?;
258 screen::read(m, &mut dev, 0, wait)?
259 };
260 let erased = screen::looks_erased(&before);
261 if erased {
262 p.err("eeprom: record reads as erased; only the control area will be written");
263 }
264
265 check(p)?;
267 p.err(&format!("[4/5] config: {spec_path}"));
268 let out = format!("{snap}/config");
269 config::gen_config(m, spec_path, &out, "rcvbp", load, p)?;
270 let img = format!("{out}/{}-block7.bin", spec.name);
271 restore_flash(ctx, &img, true, 0, p)?;
272
273 check(p)?;
275 p.err("[5/5] eeprom: writing records");
276 let mut dev = open(ctx)?;
277 let ca = eeprom::control_area(position.0, position.1, w, h);
278 let kept = if erased { &[][..] } else { &before[..] };
279 for f in eeprom_writes(rcv, &ca, kept) {
280 dev.send(&f)?;
281 std::thread::sleep(Duration::from_millis(500));
284 }
285 dev.send(&eeprom::save_to(rcv))?;
286 std::thread::sleep(Duration::from_millis(500));
287 dev.send(&eeprom::reload_to(rcv))?;
288 std::thread::sleep(Duration::from_secs(1));
289
290 let after = screen::read(m, &mut dev, 0, wait)?;
292 match eeprom::parse_control_area(&after[2..]) {
293 Some((x0, y0, x1, y1))
294 if (x0, y0, x1, y1) == (position.0, position.1, position.0 + w, position.1 + h) =>
295 {
296 p.err(&format!(
297 "eeprom: control area verified {x0},{y0}-{x1},{y1}"
298 ));
299 }
300 other => bail!("eeprom: control area reads back as {other:?}"),
301 }
302 drop(dev);
303 match discover_one(ctx, wait)? {
304 Some(i) if (i.cols, i.rows) == (w, h) => {
305 p.err(&format!(
306 "discovery: {}x{} on firmware {}.{}",
307 i.cols, i.rows, i.ver_major, i.ver_minor
308 ));
309 }
310 Some(i) => p.err(&format!(
311 "discovery: {}x{} (expected {w}x{h}); usually corrects after the power-cycle",
312 i.cols, i.rows
313 )),
314 None => bail!("the card stopped answering discovery"),
315 }
316
317 p.err("power-cycle the card to apply");
318 Ok(())
319}
320
321fn eeprom_target(index: Option<u16>) -> String {
323 index.map_or_else(
324 || "eeprom: broadcast (every card on the chain)".to_string(),
325 |i| format!("eeprom: card index {i}"),
326 )
327}
328
329fn eeprom_writes(rcv: u16, ca: &[u8; 42], kept: &[u8]) -> Vec<Vec<u8>> {
333 eeprom::RECORDS
334 .iter()
335 .filter_map(|r| {
336 let (a, n) = (usize::from(r.addr), usize::from(r.len));
337 let data: &[u8] = if r.addr == 0x002 { ca } else { kept.get(a..a + n)? };
338 Some(eeprom::write_to(rcv, r.addr, data))
339 })
340 .collect()
341}
342
343fn unix_seconds() -> u64 {
344 std::time::SystemTime::now()
345 .duration_since(std::time::UNIX_EPOCH)
346 .map_or(0, |d| d.as_secs())
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 fn read_back() -> Vec<u8> {
354 (0..=255u8).collect()
355 }
356
357 #[test]
358 fn single_card_default_broadcasts_byte_for_byte() {
359 let ca = eeprom::control_area(0, 0, 128, 64);
360 let before = read_back();
361 let frames = eeprom_writes(BROADCAST, &ca, &before);
362 assert_eq!(frames.len(), eeprom::RECORDS.len());
363 for (f, r) in frames.iter().zip(eeprom::RECORDS) {
364 let (a, n) = (usize::from(r.addr), usize::from(r.len));
365 let data: &[u8] = if r.addr == 0x002 { &ca } else { &before[a..a + n] };
366 assert_eq!(*f, eeprom::write(r.addr, data), "{}", r.name);
367 assert_eq!(&f[15..17], &[0xff, 0xff], "{}", r.name);
368 }
369 assert_eq!(eeprom::save_to(BROADCAST), eeprom::save());
370 assert_eq!(eeprom::reload_to(BROADCAST), eeprom::reload());
371 }
372
373 #[test]
374 fn an_index_addresses_every_frame() {
375 let ca = eeprom::control_area(128, 0, 128, 64);
376 for f in eeprom_writes(2, &ca, &read_back()) {
377 assert_eq!(&f[15..18], &[0x00, 0x02, 0x85]);
378 }
379 assert_eq!(&eeprom::save_to(2)[15..18], &[0x00, 0x02, 0x87]);
380 assert_eq!(&eeprom::reload_to(2)[15..18], &[0x00, 0x02, 0x77]);
381 }
382
383 #[test]
384 fn an_erased_record_set_writes_only_the_control_area() {
385 let ca = eeprom::control_area(0, 0, 128, 64);
386 let frames = eeprom_writes(BROADCAST, &ca, &[]);
387 assert_eq!(frames.len(), 1);
388 assert_eq!(&frames[0][18..22], &[0, 0, 0, 2]);
389 assert_eq!(&frames[0][26..34], &ca[..8]);
390 }
391
392 #[test]
393 fn plan_line_names_the_target() {
394 assert_eq!(eeprom_target(None), "eeprom: broadcast (every card on the chain)");
395 assert_eq!(eeprom_target(Some(3)), "eeprom: card index 3");
396 }
397}