1pub mod chips;
6
7pub use chips::{ChipLibrary, ScanPatch};
8
9pub mod embedded {
15 use anyhow::Context as _;
16
17 include!(concat!(env!("OUT_DIR"), "/libraries.rs"));
18
19 #[must_use]
21 pub fn chip(path: &str) -> Option<&'static str> {
22 CHIPS.iter().find(|(p, _)| *p == path).map(|(_, t)| *t)
23 }
24
25 #[must_use]
27 pub fn panel(path: &str) -> Option<&'static str> {
28 PANELS.iter().find(|(p, _)| *p == path).map(|(_, t)| *t)
29 }
30
31 #[must_use]
34 pub fn is_mined(path: &str) -> bool {
35 path.contains("/mined/")
36 }
37
38 pub fn specs() -> anyhow::Result<Vec<(&'static str, crate::PanelSpec)>> {
45 PANELS
46 .iter()
47 .map(|&(path, text)| {
48 let spec = crate::PanelSpec::parse(text)
49 .with_context(|| format!("parse {path}"))?;
50 Ok((path, spec))
51 })
52 .collect()
53 }
54
55 #[must_use]
59 pub fn chip_by_family(family_id: u16) -> Option<(&'static str, &'static str)> {
60 let named: Vec<String> = specs()
61 .unwrap_or_default()
62 .into_iter()
63 .map(|(_, spec)| spec.chip.library)
64 .collect();
65 let has_id = |&&(_, text): &&(&str, &str)| {
66 crate::ChipLibrary::parse(text).is_ok_and(|c| c.family_id == family_id)
67 };
68 CHIPS
69 .iter()
70 .filter(|(path, _)| named.iter().any(|n| n == path))
71 .chain(CHIPS.iter())
72 .find(has_id)
73 .copied()
74 }
75
76 #[cfg(test)]
77 mod tests {
78 use super::*;
79
80 #[test]
81 fn every_embedded_spec_parses_and_carries_meta() {
82 let specs = specs().unwrap();
83 assert_eq!(specs.len(), PANELS.len());
84 for (path, text) in PANELS {
85 let table: toml::Table = text.parse().unwrap();
86 assert!(table.contains_key("meta"), "{path}: no [meta] table");
87 }
88 let (path, bench) = &specs[0];
89 assert_eq!(*path, "config/panels/p25-128x64-sm16269s.toml");
90 assert_eq!(bench.meta.status, crate::Status::Tested);
91 assert_eq!(bench.meta.origin, crate::Origin::Bench);
92 assert_eq!(bench.meta.pitch_mm, Some(2.5));
93 for (path, spec) in &specs[1..] {
94 assert!(is_mined(path));
95 assert_eq!(spec.meta.status, crate::Status::Generates, "{path}");
96 assert_eq!(spec.meta.origin, crate::Origin::Mined, "{path}");
97 assert!(spec.meta.sources > 0, "{path}");
98 assert!(!spec.meta.examples.is_empty(), "{path}");
99 }
100 }
101
102 #[test]
103 fn a_chip_id_finds_the_library_the_shipped_specs_use() {
104 let (path, text) = chip_by_family(0x14C).unwrap();
106 assert_eq!(path, "config/chips/sm16269s-factory.toml");
107 assert_eq!(crate::ChipLibrary::parse(text).unwrap().family_id, 0x14C);
108 assert_eq!(chip_by_family(0x85).unwrap().0, "config/chips/mined/icn2053.toml");
109 assert!(chip_by_family(0xFFFF).is_none());
110 }
111
112 #[test]
113 fn the_bench_files_are_embedded_before_the_mined_ones() {
114 assert!(chip("config/chips/sm16269s-factory.toml").is_some());
115 assert!(chip("config/chips/mined/icn2053.toml").is_some());
116 assert!(chip("config/chips/x.toml").is_none());
117 assert_eq!(PANELS[0].0, "config/panels/p25-128x64-sm16269s.toml");
118 assert!(panel(PANELS[0].0).is_some());
119 let sorted = |xs: &[(&str, &str)]| {
120 let plain: Vec<&str> = xs.iter().map(|(p, _)| *p).filter(|p| !is_mined(p)).collect();
121 let mined: Vec<&str> = xs.iter().map(|(p, _)| *p).filter(|p| is_mined(p)).collect();
122 xs.iter().take(plain.len()).all(|(p, _)| !is_mined(p))
123 && plain.windows(2).all(|w| w[0] < w[1])
124 && mined.windows(2).all(|w| w[0] < w[1])
125 };
126 assert!(sorted(CHIPS) && sorted(PANELS));
127 for (p, text) in PANELS {
128 assert!(crate::PanelSpec::parse(text).is_ok(), "{p}");
129 }
130 for (p, text) in CHIPS {
131 assert!(crate::ChipLibrary::parse(text).is_ok(), "{p}");
132 }
133 }
134 }
135}
136
137use anyhow::{bail, Context, Result};
138use serde::{Deserialize, Serialize, Serializer};
139use std::collections::BTreeMap;
140use std::path::Path;
141
142pub const RECORD01_LEN: usize = 764;
144
145pub type Loader<'a> = &'a dyn Fn(&str) -> Result<String>;
148
149pub fn read_library(path: &str) -> Result<String> {
155 std::fs::read_to_string(path).with_context(|| format!("read {path}"))
156}
157
158#[derive(Debug, Clone, Deserialize, Serialize)]
159#[serde(deny_unknown_fields)]
160pub struct PanelSpec {
161 pub name: String,
163 #[serde(default)]
165 pub meta: Meta,
166 pub module: Module,
167 pub screen: Screen,
168 pub chip: Chip,
169 #[serde(default)]
170 pub color: Color,
171 #[serde(default)]
172 pub current: Current,
173 #[serde(default)]
174 pub timing: Timing,
175 #[serde(default)]
176 pub mapping: Mapping,
177 #[serde(default)]
178 pub boot: Boot,
179 #[serde(
182 default,
183 deserialize_with = "chips::record01_offsets",
184 serialize_with = "chips::hex_offsets",
185 skip_serializing_if = "BTreeMap::is_empty"
186 )]
187 pub record01_overrides: BTreeMap<usize, u8>,
188}
189
190struct Short(f32);
193
194impl Serialize for Short {
195 fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
196 let text = self.0.to_string();
197 s.serialize_f64(text.parse().unwrap_or_else(|_| f64::from(self.0)))
198 }
199}
200
201#[allow(clippy::trivially_copy_pass_by_ref)]
203fn short<S: Serializer>(v: &f32, s: S) -> std::result::Result<S::Ok, S::Error> {
204 Short(*v).serialize(s)
205}
206
207fn shorts<S: Serializer>(v: &[f32], s: S) -> std::result::Result<S::Ok, S::Error> {
208 s.collect_seq(v.iter().map(|&x| Short(x)))
209}
210
211#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
214#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
215#[serde(default, deny_unknown_fields)]
216pub struct Meta {
217 #[serde(skip_serializing_if = "Option::is_none")]
219 #[cfg_attr(feature = "ts", ts(optional))]
220 pub pitch_mm: Option<f32>,
221 pub status: Status,
222 pub origin: Origin,
223 pub sources: u32,
225 #[serde(skip_serializing_if = "Option::is_none")]
228 #[cfg_attr(feature = "ts", ts(optional))]
229 pub agreement: Option<f32>,
230 pub examples: Vec<String>,
232 pub vendors: Vec<String>,
235 #[serde(skip_serializing_if = "Option::is_none")]
236 #[cfg_attr(feature = "ts", ts(optional))]
237 pub notes: Option<String>,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
240 #[cfg_attr(feature = "ts", ts(optional))]
241 pub maker: Option<String>,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
244 #[cfg_attr(feature = "ts", ts(optional))]
245 pub product: Option<String>,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
248 #[cfg_attr(feature = "ts", ts(optional))]
249 pub url: Option<String>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
252 #[cfg_attr(feature = "ts", ts(optional))]
253 pub datasheet: Option<String>,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
256 #[cfg_attr(feature = "ts", ts(optional))]
257 pub image: Option<String>,
258 #[serde(default, skip_serializing_if = "Option::is_none")]
259 #[cfg_attr(feature = "ts", ts(optional))]
260 pub image_source: Option<String>,
261}
262
263#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
265#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
266#[serde(rename_all = "lowercase")]
267pub enum Status {
268 Tested,
270 #[default]
272 Generates,
273}
274
275#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
277#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
278#[serde(rename_all = "lowercase")]
279pub enum Origin {
280 Bench,
282 #[default]
284 Mined,
285}
286
287#[derive(Debug, Clone, Deserialize, Serialize)]
288#[serde(deny_unknown_fields)]
289pub struct Module {
290 pub width: u16,
292 pub height: u16,
294 pub scan: u8,
296 #[serde(skip_serializing_if = "Option::is_none")]
299 pub serial_clock: Option<u16>,
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub gray_bits: Option<u8>,
303 #[serde(default)]
305 pub line_dir: u8,
306 #[serde(default = "default_data_groups")]
308 pub data_groups: u8,
309}
310
311#[derive(Debug, Clone, Deserialize, Serialize)]
312#[serde(deny_unknown_fields)]
313pub struct Screen {
314 pub width: u16,
316 pub height: u16,
317}
318
319#[derive(Debug, Clone, Deserialize, Serialize)]
320#[serde(deny_unknown_fields)]
321pub struct Chip {
322 pub library: String,
324}
325
326#[derive(Debug, Clone, Deserialize, Serialize)]
327#[serde(deny_unknown_fields)]
328pub struct Color {
329 pub swap: u8,
331 pub source: [u8; 3],
333}
334
335impl Default for Color {
336 fn default() -> Self {
337 Self {
338 swap: 3,
339 source: [2, 1, 0],
340 }
341 }
342}
343
344#[derive(Debug, Clone, Deserialize, Serialize)]
345#[serde(deny_unknown_fields)]
346pub struct Current {
347 pub gains: [u8; 4],
349 #[serde(serialize_with = "shorts")]
351 pub percent: [f32; 3],
352}
353
354impl Default for Current {
355 fn default() -> Self {
356 Self {
357 gains: [43; 4],
358 percent: [0.1; 3],
359 }
360 }
361}
362
363#[derive(Debug, Clone, Deserialize, Serialize)]
364#[serde(deny_unknown_fields)]
365pub struct Timing {
366 #[serde(serialize_with = "short")]
367 pub gamma: f32,
368 #[serde(serialize_with = "short")]
369 pub refresh_hz: f32,
370 pub gclock: u8,
372 #[serde(serialize_with = "short")]
374 pub min_oe: f32,
375 pub luminance_level: u16,
377 pub oe_8ns: bool,
379}
380
381impl Default for Timing {
382 fn default() -> Self {
383 Self {
384 gamma: 2.8,
385 refresh_hz: 60.0,
386 gclock: 0x14,
387 min_oe: 1e-4,
388 luminance_level: 188,
389 oe_8ns: true,
390 }
391 }
392}
393
394#[derive(Debug, Clone, Deserialize, Serialize)]
397#[serde(deny_unknown_fields)]
398pub struct Mapping {
399 pub reversed_groups: bool,
402 pub reversed_lines: bool,
404 #[serde(default, skip_serializing_if = "Option::is_none")]
408 pub block: Option<u16>,
409 #[serde(default = "default_true")]
413 pub gate_phantom_positions: bool,
414}
415
416const fn default_true() -> bool {
417 true
418}
419
420impl Default for Mapping {
421 fn default() -> Self {
422 Self {
423 reversed_groups: true,
424 reversed_lines: false,
425 block: None,
426 gate_phantom_positions: true,
427 }
428 }
429}
430
431#[derive(Debug, Clone, Default, Deserialize, Serialize)]
432#[serde(deny_unknown_fields)]
433pub struct Boot {
434 pub arm_at_boot: bool,
437}
438
439const fn default_data_groups() -> u8 {
440 1
441}
442
443impl PanelSpec {
444 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
449 let path = path.as_ref();
450 let text =
451 std::fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
452 Self::parse(&text).with_context(|| format!("parse {}", path.display()))
453 }
454
455 pub fn parse(text: &str) -> Result<Self> {
460 Ok(toml::from_str(text)?)
461 }
462
463 pub fn to_toml(&self) -> Result<String> {
469 toml::to_string(self).context("write spec")
470 }
471
472 pub fn chip_library(&self, load: Loader) -> Result<ChipLibrary> {
478 let path = &self.chip.library;
479 let text = load(path)?;
480 ChipLibrary::parse(&text).with_context(|| format!("parse {path}"))
481 }
482
483 pub fn validate(&self) -> Result<()> {
488 if !self.module.height.is_multiple_of(2) {
489 bail!("module height must be even (the record stores height/2)");
490 }
491 if self.module.width > 255 || self.module.height / 2 > 255 {
492 bail!("module dimensions exceed the record's byte fields");
493 }
494 if !self.screen.width.is_multiple_of(self.module.width)
495 || !self.screen.height.is_multiple_of(self.module.height)
496 {
497 bail!("screen size must be a whole number of modules");
498 }
499 if self.module.scan == 0 || u16::from(self.module.scan) > self.module.height {
500 bail!("scan denominator must be 1..=module height");
501 }
502 if !(self.module.height / 2).is_multiple_of(u16::from(self.module.scan)) {
503 bail!("stored module height (height/2) must be a whole number of scan groups");
504 }
505 Ok(())
506 }
507
508 #[must_use]
510 pub fn serial_clock(&self, chip: &ChipLibrary) -> u16 {
511 self.module.serial_clock.unwrap_or(chip.serial_clock)
512 }
513
514 pub fn gray_bits(&self, chip: &ChipLibrary) -> Result<u8> {
520 match self.module.gray_bits {
521 Some(g) => Ok(g),
522 None => chip.gray_bits(),
523 }
524 }
525
526 #[must_use]
529 pub fn screen_extent_in_line_dir(&self) -> u16 {
530 if self.module.line_dir >= 2 {
531 self.screen.height
532 } else {
533 self.screen.width
534 }
535 }
536
537 #[must_use]
540 pub fn module_input_count(&self) -> u8 {
541 let unit = 16u16;
542 let dim = if self.module.line_dir >= 2 {
543 self.module.width
544 } else {
545 self.module.height / 2
546 };
547 (unit / dim.max(1)).max(1) as u8
548 }
549
550 #[must_use]
553 pub fn modules_in_line_dir(&self) -> u16 {
554 if self.module.line_dir >= 2 {
555 self.screen.height.div_ceil(self.module.height)
556 } else {
557 self.screen.width.div_ceil(self.module.width)
558 }
559 }
560
561 #[must_use]
563 pub fn one_scan_len(&self) -> u16 {
564 let v = u32::from(self.module.width) * u32::from(self.module.height / 2)
565 / u32::from(self.module.scan);
566 v.max(1) as u16
567 }
568
569 #[must_use]
572 pub fn card_scan_len(&self) -> u16 {
573 self.one_scan_len() * self.modules_in_line_dir()
574 }
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580
581 fn spec() -> PanelSpec {
582 PanelSpec::parse(
583 r#"
584 name = "t"
585 [module]
586 width = 128
587 height = 64
588 scan = 16
589 [screen]
590 width = 256
591 height = 64
592 [chip]
593 library = "x.toml"
594 "#,
595 )
596 .unwrap()
597 }
598
599 #[test]
600 fn geometry_helpers_follow_the_vendor_formulas() {
601 let s = spec();
602 assert!(s.validate().is_ok());
603 assert_eq!(s.modules_in_line_dir(), 2);
604 assert_eq!(s.one_scan_len(), 256);
605 assert_eq!(s.card_scan_len(), 512);
606 assert_eq!(s.screen_extent_in_line_dir(), 256);
607 assert_eq!(s.module_input_count(), 1);
608 }
609
610 #[test]
611 fn a_scan_that_does_not_divide_the_module_is_refused() {
612 let mut s = spec();
613 s.module.scan = 12;
614 assert!(s.validate().is_err());
615 }
616
617 #[test]
618 fn unknown_fields_are_refused() {
619 assert!(PanelSpec::parse("name = \"t\"\nextra = 1\n").is_err());
620 }
621
622 #[test]
623 fn a_spec_written_as_toml_reads_back_to_the_same_values() {
624 let text = std::fs::read_to_string(concat!(
625 env!("CARGO_MANIFEST_DIR"),
626 "/config/panels/p25-128x64-sm16269s.toml"
627 ))
628 .unwrap();
629 let spec = PanelSpec::parse(&text).unwrap();
630 let out = spec.to_toml().unwrap();
631 assert!(out.starts_with("name = \"p25-128x64-sm16269s\"\n\n[meta]\n"), "{out}");
632 assert!(out.contains("\n[record01_overrides]\n0x02F = 1\n"), "{out}");
633 assert!(out.contains("gamma = 2.8\n") && out.contains("min_oe = 0.0001\n"), "{out}");
634 let back = PanelSpec::parse(&out).unwrap();
635 assert_eq!(back.to_toml().unwrap(), out);
636 assert_eq!(back.record01_overrides, spec.record01_overrides);
637 assert_eq!(back.timing.min_oe.to_bits(), spec.timing.min_oe.to_bits());
638 assert_eq!(back.module.serial_clock, Some(8));
639
640 let mut bare = spec;
641 bare.record01_overrides.clear();
642 bare.mapping.block = None;
643 let out = bare.to_toml().unwrap();
644 assert!(!out.contains("record01_overrides") && !out.contains("block"), "{out}");
645 }
646}