Skip to main content

ops/
provision.rs

1//! Bring a receiver card to a working state in one command: snapshot,
2//! firmware, configuration, cabinet identity, verification.
3//!
4//! Firmware takes both write paths because 16.53 guards blocks 0-2 and 8 from
5//! the host path and its SDRAM self-program writes only those. The EEPROM
6//! records are read before block 7 is written, because that write wipes their
7//! mirror and the card then reports a healthy size while dropping every pixel
8//! (docs/provisioning.md, docs/receiver-identity.md).
9
10use 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
39/// Install `image` into the primary bank through both write paths and
40/// verify the whole bank. Returns true when a power-cycle is needed.
41/// `guarded` are the blocks the running firmware keeps from the host path.
42fn 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(&current);
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    // The card programs the guarded sectors itself from SDRAM.
73    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    // The rest goes in through the host path, block by block.
86    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/// What `rxp provision` takes.
104#[derive(Clone, Debug)]
105pub struct Args<'a> {
106    /// Panel spec file.
107    pub spec_path: &'a str,
108    /// Vendor firmware image to install: a `config/firmware.toml` name, a
109    /// path, or `auto` for the image the ranking picks for the spec
110    /// (`crate::firmware::pick`); skipped when absent.
111    pub firmware: Option<&'a str>,
112    /// Cabinet position in the whole screen, in pixels.
113    pub position: (u16, u16),
114    /// The card's position in the Ethernet chain, the receiver index the
115    /// EEPROM frames carry; absent, they broadcast, and a chain of more than
116    /// one card is refused.
117    pub index: Option<u16>,
118    /// Directory for the pre-provisioning snapshot; `build/snapshot-<time>`
119    /// when absent.
120    pub snapshot_dir: Option<&'a str>,
121    /// Write it; without this only the plan is printed.
122    pub commit: bool,
123    /// Seconds to wait for each reply.
124    pub wait: u64,
125}
126
127/// Provision a card: snapshot, firmware, configuration, EEPROM, verify.
128/// Cancellation is honoured between the five steps.
129///
130/// # Errors
131/// Fails at the first step whose result cannot be verified.
132#[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    // A broadcast EEPROM write gives every card on the chain the same window.
150    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    // The discovered id byte picks the model; `--card` stands in for an id
155    // no file carries, but a known id that disagrees with it is refused.
156    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    // `--firmware auto` ranks config/firmware.toml for this spec and this
183    // card and installs the one image the ranking decided (docs/cards.md).
184    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    // 1. Snapshot: the only copy of what this card held.
219    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    // 2. Firmware.
229    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            // The host page writes run under the firmware the card is on now.
236            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                // The card answers discovery before it has finished loading its
245                // parameters; flash writes sent before then are unreliable.
246                std::thread::sleep(Duration::from_secs(12));
247            }
248        }
249    } else {
250        p.err("[2/5] firmware: skipped (no --firmware)");
251    }
252
253    // 3. Read the EEPROM records before block 7 wipes their mirror.
254    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    // 4. Configuration image.
266    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    // 5. EEPROM: every record back, control area set for this cabinet.
274    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        // An EEPROM write takes the card milliseconds; back-to-back records
282        // are dropped.
283        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    // Verify.
291    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
321/// The plan line for the EEPROM step's addressing.
322fn 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
329/// The record writes of step 5 to receiver `rcv`, in `RECORDS` order: the
330/// control area `ca` at 0x002, every other record from `kept` (the read-back
331/// set; empty when it read as erased, then only the control area goes).
332fn 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}