1use crate::capture::discover_one;
6use crate::{protocol, Ctx, Progress};
7use anyhow::{bail, Context, Result};
8use panelspec::{embedded, ChipLibrary, PanelSpec};
9use receivers::{by_id, by_name, models, CardModel, Status};
10use std::collections::BTreeMap;
11use std::fmt::Write as _;
12
13pub fn for_card(info: &protocol::DiscoveryInfo) -> Result<&'static CardModel> {
18 by_id(info.card_id).with_context(|| {
19 format!(
20 "card id 0x{:02x} matches no model in config/cards (pass --card NAME to override)",
21 info.card_id
22 )
23 })
24}
25
26pub fn named(name: &str) -> Result<&'static CardModel> {
31 by_name(name).with_context(|| {
32 let known: Vec<&str> = models().iter().map(|m| m.name.as_str()).collect();
33 format!("unknown card model {name:?}; known: {}", known.join(", "))
34 })
35}
36
37pub fn resolve(ctx: &Ctx, wait: u64) -> Result<&'static CardModel> {
42 if let Some(m) = ctx.model {
43 return Ok(m);
44 }
45 let Some(info) = discover_one(ctx, wait)? else {
46 bail!("no response on {} within {wait}s", ctx.iface);
47 };
48 for_card(&info)
49}
50
51#[must_use]
53pub fn flash_map(m: &CardModel) -> protocol::FlashMap {
54 protocol::FlashMap {
55 param_block: m.memory.parameter_block,
56 firmware_blocks: m.memory.primary_blocks(),
57 golden_block: m.memory.golden_block(),
58 screen_record_addr: m.memory.eeprom_mirror,
59 }
60}
61
62#[must_use]
64pub fn bank_bytes(m: &CardModel) -> usize {
65 m.memory.bank_bytes as usize
66}
67
68pub fn list(p: &mut dyn Progress) {
70 for m in models() {
71 let id = m.id.map_or_else(|| "? ".to_owned(), |b| format!("0x{b:02x}"));
74 p.out(&format!(
77 "{:<8} id={id} {:<11} {}",
78 m.name,
79 m.status.to_string(),
80 m.vendor
81 ));
82 }
83}
84
85const fn symbol(s: Status) -> &'static str {
87 match s {
88 Status::Tested => "✅",
89 Status::Generates => "⚠️",
90 Status::Unsupported => "❌",
91 }
92}
93
94const OTHER_COLUMNS: [(&str, Status); 2] = [
98 ("Colorlight 5A-75B · 5A-75E", Status::Generates),
99 ("Linsn · Novastar · Huidu", Status::Unsupported),
100];
101
102const UNSUPPORTED_ROWS: [&str; 2] = [
105 "SM16369S · ICND2263 (register record not decoded)",
106 "snake-wired outdoor modules (1/2, 1/4, 1/5, 1/10 scan)",
107];
108
109fn chip_name(lib: &ChipLibrary) -> String {
112 lib.name.split(" (").next().unwrap_or(&lib.name).to_owned()
113}
114
115fn family(chip: &str) -> String {
118 let letters: String = chip.chars().take_while(char::is_ascii_alphabetic).collect();
119 if letters == "ICND" { "ICN".to_owned() } else { letters }
120}
121
122fn embedded_chip(path: &str) -> Result<ChipLibrary> {
123 let text = embedded::chip(path).with_context(|| format!("{path}: not embedded"))?;
124 ChipLibrary::parse(text).with_context(|| format!("parse {path}"))
125}
126
127pub fn matrix_markdown() -> Result<String> {
134 let mut s = String::from("✅ driven on the bench · ⚠️ configuration generates, never driven · ❌ not supported
135
136");
137 let _ = write!(s, "| panel (driver chip) |");
138 for m in models() {
139 let _ = write!(s, " {} {} |", m.vendor, m.name);
140 }
141 for (name, _) in OTHER_COLUMNS {
142 let _ = write!(s, " {name} |");
143 }
144 s.push_str("
145|---|");
146 for _ in 0..models().len() + OTHER_COLUMNS.len() {
147 s.push_str(":---:|");
148 }
149 s.push('\n');
150 let row = |s: &mut String, label: &str, cell: &dyn Fn(&CardModel) -> Status, other: &dyn Fn(Status) -> Status| {
151 let _ = write!(s, "| {label} |");
152 for m in models() {
153 let _ = write!(s, " {} |", symbol(cell(m)));
154 }
155 for (_, status) in OTHER_COLUMNS {
156 let _ = write!(s, " {} |", symbol(other(status)));
157 }
158 s.push('\n');
159 };
160
161 let mut driven: Vec<&str> = Vec::new();
162 for t in models().iter().flat_map(|m| &m.tested) {
163 if !driven.contains(&t.panel.as_str()) {
164 driven.push(&t.panel);
165 }
166 }
167 for path in driven {
168 let text = embedded::panel(path).with_context(|| format!("{path}: not embedded"))?;
169 let spec = PanelSpec::parse(text).with_context(|| format!("parse {path}"))?;
170 let chip = chip_name(&embedded_chip(&spec.chip.library)?);
171 let label = format!(
172 "{}x{} 1/{}, {chip} (`{path}`)",
173 spec.module.width, spec.module.height, spec.module.scan
174 );
175 let cell = |m: &CardModel| {
176 if m.tested.iter().any(|t| t.panel == path) {
177 Status::Tested
178 } else {
179 m.status.max(Status::Generates)
180 }
181 };
182 row(&mut s, &label, &cell, &|o| o);
183 }
184
185 let mut families: BTreeMap<String, Vec<String>> = BTreeMap::new();
187 let mut classes = 0usize;
188 for (path, text) in embedded::PANELS {
189 let spec = PanelSpec::parse(text).with_context(|| format!("parse {path}"))?;
190 if spec.meta.status == panelspec::Status::Verified {
191 continue;
192 }
193 classes += 1;
194 let chip = chip_name(&embedded_chip(&spec.chip.library)?);
195 let chips = families.entry(family(&chip)).or_default();
196 if !chips.contains(&chip) {
197 chips.push(chip);
198 }
199 }
200 let generates = |m: &CardModel| m.status.max(Status::Generates);
201 for chips in families.values_mut() {
202 chips.sort();
203 row(&mut s, &chips.join(" · "), &generates, &|o| o);
204 }
205 for label in UNSUPPORTED_ROWS {
206 row(&mut s, label, &|_| Status::Unsupported, &|_| Status::Unsupported);
207 }
208 let _ = write!(
209 s,
210 "
211The ⚠️ chip rows are the {classes} derived module classes in `config/panels/`, grouped by driver-chip family."
212 );
213 Ok(s)
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
222 fn the_readme_matrix_is_generated() {
223 let readme = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../../README.md")).unwrap();
224 let (_, rest) = readme.split_once("<!-- tested -->").expect("<!-- tested --> marker");
225 let (table, _) = rest.split_once("<!-- /tested -->").expect("<!-- /tested --> marker");
226 assert_eq!(table.trim(), matrix_markdown().unwrap().trim(), "regenerate with: rxp card models --markdown");
227 }
228
229 #[test]
230 fn the_matrix_has_one_column_per_model_and_the_tested_panel_first() {
231 let s = matrix_markdown().unwrap();
232 let lines: Vec<&str> = s.lines().collect();
233 assert_eq!(lines[2], "| panel (driver chip) | Colorlight E120 | Colorlight E320 | Colorlight E80 | Colorlight 5A-75B · 5A-75E | Linsn · Novastar · Huidu |");
234 assert_eq!(lines[3], "|---|:---:|:---:|:---:|:---:|:---:|");
235 assert_eq!(lines[4], "| 128x64 1/16, SM16269S (`config/panels/p25-128x64-sm16269s.toml`) | ✅ | ⚠️ | ⚠️ | ⚠️ | ❌ |");
237 assert!(lines[5].starts_with("| DP5525 | ⚠️ |"), "{}", lines[5]);
238 assert!(s.contains("| ICN2038S · ICN2053 · ICN2055 · ICN2065 · ICND2163 | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ❌ |"), "{s}");
239 assert_eq!(family("ICND2163"), "ICN");
240 assert_eq!(family("SM16380"), "SM");
241 }
242
243 #[test]
244 fn the_e120_model_gives_the_pinned_allowlists() {
245 assert_eq!(flash_map(named("e120").unwrap()), protocol::E120);
246 assert_eq!(bank_bytes(named("E120").unwrap()), 11 * 0x10000);
247 }
248
249 #[test]
250 fn an_unknown_name_lists_the_models() {
251 let e = named("x").unwrap_err().to_string();
252 assert!(e.contains("E120"), "{e}");
253 }
254}