1pub mod firmware;
7
8use serde::Deserialize;
9use std::fmt;
10use std::ops::Range;
11use std::sync::OnceLock;
12
13mod embedded {
14 include!(concat!(env!("OUT_DIR"), "/cards.rs"));
15}
16
17#[derive(Debug, Clone, Deserialize)]
19#[serde(deny_unknown_fields)]
20pub struct CardModel {
21 pub name: String,
23 pub vendor: String,
24 pub family: String,
26 #[serde(default)]
31 pub id: Option<u8>,
32 pub status: Status,
33 #[serde(default)]
34 pub notes: String,
35 #[serde(default)]
37 pub image: Option<String>,
38 #[serde(default)]
40 pub image_source: Option<String>,
41 #[serde(default)]
43 pub datasheet: Option<String>,
44 #[serde(default)]
46 pub tested: Vec<Tested>,
47 pub limits: Limits,
48 pub memory: Memory,
49 pub firmware: Firmware,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
54#[serde(rename_all = "lowercase")]
55pub enum Status {
56 Tested,
58 Generates,
60 Unsupported,
61}
62
63impl fmt::Display for Status {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 f.write_str(match self {
66 Self::Tested => "tested",
67 Self::Generates => "generates",
68 Self::Unsupported => "unsupported",
69 })
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
75#[serde(deny_unknown_fields)]
76pub struct Tested {
77 pub panel: String,
79 pub firmware: String,
81}
82
83impl Tested {
84 #[must_use]
86 pub fn image(&self) -> Option<&'static firmware::Image> {
87 firmware::image(&self.firmware)
88 }
89
90 #[must_use]
92 pub fn version(&self) -> Option<Version> {
93 self.image().map(|i| i.version)
94 }
95}
96
97#[derive(Debug, Clone, Deserialize)]
99#[serde(deny_unknown_fields)]
100pub struct Limits {
101 pub max_width: u16,
102 pub max_height: u16,
103 pub hub_ports: u8,
104 pub chain: Option<u16>,
106}
107
108#[derive(Debug, Clone, Deserialize)]
110#[serde(deny_unknown_fields)]
111pub struct Memory {
112 pub block_bytes: u32,
113 pub primary_bank: u32,
114 pub bank_bytes: u32,
115 pub golden_bank: u32,
116 pub parameter_block: u8,
117 pub eeprom_mirror: u32,
118 #[serde(default)]
119 pub guarded: Vec<Guard>,
120 pub boot_image: BootImage,
121}
122
123impl Memory {
124 #[must_use]
126 pub fn primary_blocks(&self) -> Range<u8> {
127 let first = (self.primary_bank / self.block_bytes) as u8;
128 first..first + self.bank_blocks()
129 }
130
131 #[must_use]
133 pub fn bank_blocks(&self) -> u8 {
134 self.bank_bytes.div_ceil(self.block_bytes) as u8
135 }
136
137 #[must_use]
139 pub fn golden_block(&self) -> u8 {
140 (self.golden_bank / self.block_bytes) as u8
141 }
142
143 #[must_use]
146 pub fn guarded_blocks(&self, version: Version) -> &[u8] {
147 self.guarded
148 .iter()
149 .find(|g| g.covers(version))
150 .map_or(&[], |g| g.blocks.as_slice())
151 }
152
153 #[must_use]
156 pub fn config_page(&self) -> u16 {
157 (u16::from(self.parameter_block) << 8) | (self.boot_image.rcvbp / 0x100) as u16
158 }
159}
160
161#[derive(Debug, Clone, Deserialize)]
163#[serde(deny_unknown_fields)]
164pub struct Guard {
165 pub from: Version,
166 pub to: Option<Version>,
168 pub blocks: Vec<u8>,
169}
170
171impl Guard {
172 fn covers(&self, v: Version) -> bool {
173 self.from <= v && self.to.is_none_or(|to| v <= to)
174 }
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
179pub struct Version(pub u8, pub u8);
180
181impl fmt::Display for Version {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 write!(f, "{}.{}", self.0, self.1)
184 }
185}
186
187impl std::str::FromStr for Version {
188 type Err = String;
189
190 fn from_str(s: &str) -> Result<Self, String> {
191 let (a, b) = s
192 .split_once('.')
193 .ok_or_else(|| format!("version {s:?} is not major.minor"))?;
194 let n = |v: &str| v.parse::<u8>().map_err(|e| format!("version {s:?}: {e}"));
195 Ok(Self(n(a)?, n(b)?))
196 }
197}
198
199impl<'de> Deserialize<'de> for Version {
200 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
201 let s = String::deserialize(d)?;
202 s.parse().map_err(serde::de::Error::custom)
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
209#[serde(deny_unknown_fields)]
210pub struct BootImage {
211 pub basic_pack: usize,
212 pub data_swap: usize,
213 pub module_positions: usize,
214 pub chip_page: usize,
215 pub void_line: usize,
216 pub void_line_columns: usize,
217 pub anti_void: usize,
218 pub mapping: usize,
219 pub scan_table: usize,
220 pub rcvbp: usize,
221 pub map_entries: usize,
223 pub rcvbp_max: usize,
225}
226
227impl BootImage {
228 #[must_use]
230 pub const fn mapping_len(&self) -> usize {
231 self.map_entries * 3
232 }
233}
234
235#[derive(Debug, Clone, Deserialize)]
237#[serde(deny_unknown_fields)]
238pub struct Firmware {
239 pub image_pattern: String,
241 pub sdram_staging: bool,
243}
244
245impl Firmware {
246 #[must_use]
249 pub fn version_in_name(&self, path: &str) -> Option<Version> {
250 let name = std::path::Path::new(path).file_name()?.to_str()?;
251 let prefix = self.image_pattern.split("{version}").next()?;
252 let marker = prefix.rsplit('_').next().filter(|m| !m.is_empty())?;
253 let rest = &name[name.find(marker)? + marker.len()..];
254 let end = rest
255 .find(|c: char| !(c.is_ascii_digit() || c == '.'))
256 .unwrap_or(rest.len());
257 rest[..end].parse().ok()
258 }
259}
260
261fn parse_all() -> Vec<CardModel> {
262 let mut models: Vec<CardModel> = embedded::FILES
263 .iter()
264 .map(|(file, text)| {
265 toml::from_str(text).unwrap_or_else(|e| panic!("config/cards/{file}: {e}"))
266 })
267 .collect();
268 models.sort_by(|a, b| a.status.cmp(&b.status).then_with(|| a.name.cmp(&b.name)));
269 models
270}
271
272pub fn models() -> &'static [CardModel] {
274 static MODELS: OnceLock<Vec<CardModel>> = OnceLock::new();
275 MODELS.get_or_init(parse_all)
276}
277
278#[must_use]
281pub fn by_id(id: u8) -> Option<&'static CardModel> {
282 models().iter().find(|m| m.id == Some(id))
283}
284
285#[must_use]
287pub fn by_name(name: &str) -> Option<&'static CardModel> {
288 models().iter().find(|m| m.name.eq_ignore_ascii_case(name))
289}
290
291#[must_use]
294pub fn default_model() -> &'static CardModel {
295 &models()[0]
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn every_embedded_file_parses_and_ids_are_unique() {
304 let all = models();
305 assert!(!all.is_empty());
306 for (i, m) in all.iter().enumerate() {
307 assert!(
308 m.id.is_none() || all[..i].iter().all(|o| o.id != m.id),
309 "{}: id shared",
310 m.name
311 );
312 assert!(
313 all[..i]
314 .iter()
315 .all(|o| !o.name.eq_ignore_ascii_case(&m.name)),
316 "{}: name shared",
317 m.name
318 );
319 assert!(
320 m.memory
321 .primary_blocks()
322 .contains(&m.memory.parameter_block),
323 "{}: parameter block outside the bank",
324 m.name
325 );
326 assert!(
327 !m.memory.primary_blocks().contains(&m.memory.golden_block()),
328 "{}: golden bank inside the primary",
329 m.name
330 );
331 }
332 assert_eq!(all[0].status, Status::Tested);
333 for m in all {
334 assert_eq!(m.status == Status::Tested, !m.tested.is_empty(), "{}: status and tested disagree", m.name);
335 for t in &m.tested {
336 assert!(t.image().is_some(), "{}: tested firmware {} is not in config/firmware.toml", m.name, t.firmware);
337 }
338 }
339 }
340
341 #[test]
344 fn every_model_cites_a_source_and_only_tested_models_list_panels() {
345 for m in models() {
346 assert!(!m.notes.trim().is_empty(), "{}: no source note", m.name);
347 assert!(
348 m.notes.contains("http") || m.datasheet.is_some(),
349 "{}: the note names no source URL and there is no datasheet",
350 m.name
351 );
352 assert_eq!(
353 m.status == Status::Tested,
354 !m.tested.is_empty(),
355 "{}: only a tested model carries [[tested]]",
356 m.name
357 );
358 }
359 }
360
361 #[test]
364 fn an_unlisted_id_resolves_to_no_model() {
365 assert!(by_id(0x03).is_none());
366 for m in models() {
367 if let Some(id) = m.id {
368 assert!(std::ptr::eq(by_id(id).unwrap(), m), "{}: id lookup", m.name);
369 }
370 }
371 }
372
373 #[test]
374 fn the_e120_is_found_by_id_and_by_name() {
375 let m = by_id(0x64).expect("E120 by id");
376 assert_eq!(m.name, "E120");
377 assert_eq!(
378 m.tested,
379 [Tested {
380 panel: "config/panels/p25-128x64-sm16269s.toml".into(),
381 firmware: "E320_PWM_FPGA16.53_20231227_SM16386S_SM16269SH.hex".into()
382 }]
383 );
384 assert_eq!(m.tested[0].version(), Some(Version(16, 53)));
385 assert!(std::ptr::eq(by_name("e120").unwrap(), m));
386 assert!(std::ptr::eq(default_model(), m));
387 assert!(by_id(0x03).is_none());
388 assert!(by_name("e121").is_none());
389 }
390
391 #[test]
392 fn the_e120_map_reads_as_blocks() {
393 let m = by_name("E120").unwrap().memory.clone();
394 assert_eq!(m.primary_blocks(), 0x00..0x0b);
395 assert_eq!(m.bank_blocks(), 11);
396 assert_eq!(m.golden_block(), 0x20);
397 assert_eq!(m.config_page(), 0x0780);
398 assert_eq!(m.guarded_blocks(Version(16, 53)), &[0, 1, 2, 8]);
399 assert_eq!(m.guarded_blocks(Version(17, 0)), &[0, 1, 2, 8]);
400 assert!(m.guarded_blocks(Version(10, 81)).is_empty());
401 assert_eq!(m.boot_image.mapping_len(), 0x3000);
402 }
403
404 #[test]
405 fn versions_order_numerically_and_round_trip() {
406 assert!(Version(16, 53) < Version(17, 0));
407 assert!(Version(9, 53) < Version(16, 53));
408 assert_eq!("16.53".parse::<Version>(), Ok(Version(16, 53)));
409 assert_eq!(Version(16, 53).to_string(), "16.53");
410 assert!("16".parse::<Version>().is_err());
411 }
412
413 #[test]
414 fn the_version_is_read_from_the_image_name() {
415 let fw = &by_name("E120").unwrap().firmware;
416 assert_eq!(
417 fw.version_in_name(
418 "third-party/firmware/E320_PWM_FPGA16.53_20231227_SM16386S_SM16269SH.hex"
419 ),
420 Some(Version(16, 53))
421 );
422 assert_eq!(
423 fw.version_in_name("E320_PCB6.0_PWM_FPGA10.81_20230907.hex"),
424 Some(Version(10, 81))
425 );
426 assert_eq!(fw.version_in_name("image.hex"), None);
427 }
428}