Skip to main content

ops/
config.rs

1//! Inspecting, composing and comparing `.rcvbp` configuration files.
2
3use crate::rcvbp;
4use crate::util::{hex, hexdump, warn};
5use crate::{Loader, Progress};
6use anyhow::{Context, Result};
7use panelspec::PanelSpec;
8use receivers::CardModel;
9use std::fmt::Write as _;
10
11/// Parse a comma-separated list of hex record types.
12fn parse_types(s: &str) -> Result<Vec<u16>> {
13    s.split(',')
14        .map(str::trim)
15        .filter(|t| !t.is_empty())
16        .map(|t| {
17            u16::from_str_radix(t.trim_start_matches("0x"), 16)
18                .with_context(|| format!("bad record type {t:?}"))
19        })
20        .collect()
21}
22
23pub fn config_build(
24    base: &str,
25    copy_from: Option<&str>,
26    copy: &str,
27    remove: &str,
28    out: &str,
29    p: &mut dyn Progress,
30) -> Result<()> {
31    let mut cfg = rcvbp::Rcvbp::load(base)?;
32
33    let to_copy = parse_types(copy)?;
34    if !to_copy.is_empty() {
35        let src_path = copy_from.context("--copy needs --copy-from")?;
36        let src = rcvbp::Rcvbp::load(src_path)?;
37        for t in to_copy {
38            let rec = src
39                .find(t)
40                .with_context(|| format!("{src_path} has no record 0x{t:04x}"))?;
41            cfg.upsert(t, rec.payload.clone());
42        }
43    }
44
45    for t in parse_types(remove)? {
46        if !cfg.remove(t) {
47            warn(p, format!("{base} has no record 0x{t:04x} to remove"));
48        }
49    }
50
51    cfg.save(out)?;
52
53    // Read it straight back so a broken file never reaches the card.
54    let back = rcvbp::Rcvbp::load(out)?;
55    anyhow::ensure!(
56        back.records.len() == cfg.records.len(),
57        "{out}: wrote {} records but read back {}",
58        cfg.records.len(),
59        back.records.len()
60    );
61    p.out(out);
62    Ok(())
63}
64
65pub fn config_diff(a: &str, b: &str, p: &mut dyn Progress) -> Result<()> {
66    let fa = rcvbp::Rcvbp::load(a)?;
67    let fb = rcvbp::Rcvbp::load(b)?;
68    p.out(&format!("{a}: {} records", fa.records.len()));
69    p.out(&format!("{b}: {} records", fb.records.len()));
70
71    let types_a: Vec<u16> = fa.records.iter().map(rcvbp::Record::type_u16).collect();
72    let types_b: Vec<u16> = fb.records.iter().map(rcvbp::Record::type_u16).collect();
73    let only_a: Vec<String> = types_a
74        .iter()
75        .filter(|t| !types_b.contains(t))
76        .map(|t| format!("0x{t:04x}"))
77        .collect();
78    let only_b: Vec<String> = types_b
79        .iter()
80        .filter(|t| !types_a.contains(t))
81        .map(|t| format!("0x{t:04x}"))
82        .collect();
83    if !only_a.is_empty() {
84        p.out(&format!("only in {a}: {}", only_a.join(", ")));
85    }
86    if !only_b.is_empty() {
87        p.out(&format!("only in {b}: {}", only_b.join(", ")));
88    }
89
90    for t in &types_a {
91        let (Some(ra), Some(rb)) = (fa.find(*t), fb.find(*t)) else {
92            continue;
93        };
94        if ra.payload == rb.payload {
95            continue;
96        }
97        let diffs: Vec<usize> = ra
98            .payload
99            .iter()
100            .zip(&rb.payload)
101            .enumerate()
102            .filter(|(_, (x, y))| x != y)
103            .map(|(i, _)| i)
104            .collect();
105        p.out(&format!(
106            "record 0x{t:04x}: {} vs {} bytes, {} differ",
107            ra.payload.len(),
108            rb.payload.len(),
109            diffs.len()
110        ));
111        for i in diffs.iter().take(16) {
112            p.out(&format!(
113                "    +0x{i:03x}: {:3} (0x{:02x})  vs  {:3} (0x{:02x})",
114                ra.payload[*i], ra.payload[*i], rb.payload[*i], rb.payload[*i]
115            ));
116        }
117        if diffs.len() > 16 {
118            p.out(&format!("    ... and {} more", diffs.len() - 16));
119        }
120    }
121    Ok(())
122}
123
124pub fn rcvbp_info(path: &str, dump: bool, p: &mut dyn Progress) -> Result<()> {
125    let f = rcvbp::Rcvbp::load(path)?;
126    p.out(&format!(
127        "{path}\n  version {}, {} bytes decompressed, {} records",
128        f.version,
129        f.to_blob()?.len(),
130        f.records.len()
131    ));
132    if let Some((w, _)) = f.geometry() {
133        p.out(&format!("  cabinet width: {w}"));
134    }
135    if let Some(scan) = f.scan() {
136        p.out(&format!("  scan: 1/{scan}"));
137    }
138    if let Some((w, scan)) = f.main_geometry() {
139        p.out(&format!("  main param block: width {w}, scan 1/{scan}"));
140    }
141    p.out(&format!(
142        "\n{:>8} {:>7} {:>7} {:>8}  description",
143        "offset", "type", "bytes", "nonzero"
144    ));
145    for r in &f.records {
146        let nz = r.payload.iter().filter(|&&b| b != 0).count();
147        p.out(&format!(
148            "0x{:06x}  0x{:04x} {:7} {:8}  {}",
149            r.offset,
150            r.type_u16(),
151            r.payload.len(),
152            nz,
153            r.describe()
154        ));
155    }
156    if dump {
157        for r in &f.records {
158            if r.is_empty_table() {
159                continue;
160            }
161            p.out(&format!(
162                "\n=== record 0x{:04x} ({} bytes)",
163                r.type_u16(),
164                r.payload.len()
165            ));
166            hexdump(p, &r.payload[..r.payload.len().min(512)]);
167        }
168    }
169    Ok(())
170}
171
172/// `rxp config formats`: the codec registry as a table.
173pub fn list_formats(p: &mut dyn Progress) {
174    p.out(&format!("{:<8} {:<12} {:<10} {:<9} import", "format", "vendor", "extension", "generate"));
175    let yes_no = |b: bool| if b { "yes" } else { "no" };
176    for f in rcvbp::formats() {
177        p.out(&format!(
178            "{:<8} {:<12} .{:<9} {:<9} {}",
179            f.name,
180            f.vendor,
181            f.extension,
182            yes_no(f.generate),
183            yes_no(f.import)
184        ));
185    }
186}
187
188/// `rxp config import`: the spec that regenerates `path`, written to `out`.
189///
190/// The spec is named after the file. `format` names a codec; without it the
191/// codec is the one whose signature the file starts with. Chip libraries are
192/// chosen by chip id from the embedded set, as the site does. Every field
193/// the file did not determine is warned by name.
194pub fn import_config(path: &str, out: &str, format: Option<&str>, p: &mut dyn Progress) -> Result<()> {
195    let bytes = std::fs::read(path).with_context(|| format!("read {path}"))?;
196    let codec = match format {
197        Some(name) => rcvbp::codec(name)?,
198        None => rcvbp::detect(&bytes).with_context(|| path.to_owned())?,
199    };
200    let chips = |id: u16| panelspec::embedded::chip_by_family(id).map(|(p, t)| (p.to_owned(), t.to_owned()));
201    let (mut spec, unresolved) = codec.import(&bytes, &chips).with_context(|| path.to_owned())?;
202    spec.name = std::path::Path::new(path)
203        .file_stem()
204        .map_or_else(|| spec.name.clone(), |s| s.to_string_lossy().into_owned());
205    std::fs::write(out, spec.to_toml()?).with_context(|| format!("write {out}"))?;
206    for u in &unresolved {
207        warn(p, format!("{path}: not recovered: {u}"));
208    }
209    p.out(out);
210    Ok(())
211}
212
213/// Everything `rxp config gen` produces for a spec.
214pub struct GenOutputs {
215    /// `spec.name`, the stem of the output files.
216    pub name: String,
217    /// The format the file is in.
218    pub format: rcvbp::Format,
219    /// The configuration file bytes (`.rcvbp` for the Colorlight codec).
220    pub rcvbp: Vec<u8>,
221    /// The 256-byte basic-pack body.
222    pub basic_pack: Vec<u8>,
223    /// The 64 KB block-7 boot image; `None` when it could not be built, with
224    /// the reason as the last note.
225    pub block7: Option<Vec<u8>>,
226    /// One line per byte range placed in the `.rcvbp` and the pack.
227    pub sources: Vec<String>,
228    /// The image builder's notes and the pages it wrote.
229    pub notes: Vec<String>,
230    /// The `<name>-sources.txt` text.
231    pub report: String,
232    /// Files written, in the order `rxp config gen` prints them; empty
233    /// when nothing was written.
234    pub paths: Vec<String>,
235}
236
237/// Generate a spec's configuration in memory.
238///
239/// The `.rcvbp`, the basic pack, the boot image laid out for `card` and the
240/// sources report. `label` names the spec in the report; `format` names a
241/// codec in `rcvbp::formats()`; `load` resolves `[chip].library`.
242///
243/// # Errors
244/// Fails on an unknown format, an invalid spec or chip library. A boot-image
245/// build failure is not an error here: `block7` is `None` and the reason is
246/// the last note.
247pub fn generate(
248    card: &CardModel,
249    spec: &PanelSpec,
250    label: &str,
251    format: &str,
252    load: Loader,
253) -> Result<GenOutputs> {
254    // One codec is registered; the lookup is what refuses an unknown name.
255    // The pack and the boot image are the E320 line's, built beside the file.
256    let format = rcvbp::codec(format)?.format();
257    let g = rcvbp::spec::generate(spec, &spec.chip_library(load)?)?;
258    let rcvbp = g.rcvbp.to_file_bytes()?;
259
260    let mut notes = Vec::new();
261    let block7 = match rcvbp::image::compile(&card.memory.boot_image, spec, &g) {
262        Ok(b) => {
263            notes.extend(b.notes);
264            notes.push(format!(
265                "pages written: {}: {}",
266                b.changed_pages.len(),
267                hex(&b.changed_pages, " ")
268            ));
269            Some(b.image)
270        }
271        Err(e) => {
272            notes.push(format!("{e:#}"));
273            None
274        }
275    };
276
277    let mut report = String::new();
278    let _ = writeln!(report, "spec: {label}\n\n# record and pack sources");
279    for line in &g.sources {
280        report.push_str(line);
281        report.push('\n');
282    }
283    report.push_str("\n# compiled image\n");
284    for n in &notes {
285        report.push_str(n);
286        report.push('\n');
287    }
288
289    Ok(GenOutputs {
290        name: spec.name.clone(),
291        format,
292        rcvbp,
293        basic_pack: g.basic_pack.to_vec(),
294        block7,
295        sources: g.sources,
296        notes,
297        report,
298        paths: Vec::new(),
299    })
300}
301
302/// Generate a panel's configuration from a TOML spec into `out_dir`: the
303/// `.rcvbp`, the basic-pack body, the compiled block-7 boot image, and a file
304/// listing where every placed byte came from.
305pub fn gen_config(
306    card: &CardModel,
307    spec_path: &str,
308    out_dir: &str,
309    format: &str,
310    load: Loader,
311    p: &mut dyn Progress,
312) -> Result<GenOutputs> {
313    let spec = PanelSpec::load(spec_path)?;
314    let mut g = generate(card, &spec, spec_path, format, load)?;
315
316    std::fs::create_dir_all(out_dir).with_context(|| format!("create {out_dir}"))?;
317    let stem = format!("{out_dir}/{}", g.name);
318    let rcvbp_path = format!("{stem}.{}", g.format.extension);
319    std::fs::write(&rcvbp_path, &g.rcvbp).with_context(|| format!("write {rcvbp_path}"))?;
320    let pack_path = format!("{stem}-basic-pack.bin");
321    std::fs::write(&pack_path, &g.basic_pack).with_context(|| format!("write {pack_path}"))?;
322
323    let Some(img) = &g.block7 else {
324        anyhow::bail!("{}", g.notes.last().map_or("", String::as_str));
325    };
326    let img_path = format!("{stem}-block7.bin");
327    std::fs::write(&img_path, img).with_context(|| format!("write {img_path}"))?;
328    let report_path = format!("{stem}-sources.txt");
329    std::fs::write(&report_path, &g.report).with_context(|| format!("write {report_path}"))?;
330
331    p.out(&format!(
332        "{rcvbp_path}\n{pack_path}\n{img_path}\n{report_path}"
333    ));
334    g.paths = vec![rcvbp_path, pack_path, img_path, report_path];
335    Ok(g)
336}