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 pub fn specs() -> anyhow::Result<Vec<(&'static str, crate::PanelSpec)>> {
38 PANELS
39 .iter()
40 .map(|&(path, text)| {
41 let spec = crate::PanelSpec::parse(text)
42 .with_context(|| format!("parse {path}"))?;
43 Ok((path, spec))
44 })
45 .collect()
46 }
47
48 #[must_use]
52 pub fn chip_by_family(family_id: u16) -> Option<(&'static str, &'static str)> {
53 let named: Vec<String> = specs()
54 .unwrap_or_default()
55 .into_iter()
56 .map(|(_, spec)| spec.chip.library)
57 .collect();
58 let has_id = |&&(_, text): &&(&str, &str)| {
59 crate::ChipLibrary::parse(text).is_ok_and(|c| c.family_id == family_id)
60 };
61 CHIPS
62 .iter()
63 .filter(|(path, _)| named.iter().any(|n| n == path))
64 .chain(CHIPS.iter())
65 .find(has_id)
66 .copied()
67 }
68
69 #[cfg(test)]
70 mod tests {
71 use super::*;
72
73 #[test]
74 fn every_embedded_spec_parses_and_carries_meta() {
75 let specs = specs().unwrap();
76 assert_eq!(specs.len(), PANELS.len());
77 for (path, text) in PANELS {
78 let table: toml::Table = text.parse().unwrap();
79 assert!(table.contains_key("meta"), "{path}: no [meta] table");
80 }
81 let (path, bench) = &specs[0];
82 assert_eq!(*path, "config/panels/p25-128x64-sm16269s.toml");
83 assert_eq!(bench.meta.status, crate::Status::Verified);
84 assert_eq!(bench.meta.pitch_mm, Some(2.5));
85 for (path, spec) in &specs[1..] {
86 assert_eq!(spec.meta.status, crate::Status::Derived, "{path}");
87 assert!(spec.meta.sources > 0, "{path}");
88 assert!(!spec.meta.examples.is_empty(), "{path}");
89 }
90 }
91
92 #[test]
93 fn a_chip_id_finds_the_library_the_shipped_specs_use() {
94 let (path, text) = chip_by_family(0x14C).unwrap();
96 assert_eq!(path, "config/chips/sm16269s.toml");
97 assert_eq!(crate::ChipLibrary::parse(text).unwrap().family_id, 0x14C);
98 assert_eq!(chip_by_family(0x85).unwrap().0, "config/chips/icn2053.toml");
99 assert!(chip_by_family(0xFFFF).is_none());
100 }
101
102 #[test]
103 fn the_verified_files_are_embedded_before_the_derived_ones() {
104 assert!(chip("config/chips/sm16269s.toml").is_some());
105 assert!(chip("config/chips/icn2053.toml").is_some());
106 assert!(chip("config/chips/x.toml").is_none());
107 assert_eq!(PANELS[0].0, "config/panels/p25-128x64-sm16269s.toml");
108 assert_eq!(CHIPS[0].0, "config/chips/sm16269s.toml");
109 assert!(panel(PANELS[0].0).is_some());
110 let verified = |text: &str| text.lines().any(|l| l.trim() == r#"status = "verified""#);
111 let sorted = |xs: &[(&str, &str)]| {
112 let plain: Vec<&str> = xs.iter().filter(|(_, t)| verified(t)).map(|(p, _)| *p).collect();
113 let rest: Vec<&str> = xs.iter().filter(|(_, t)| !verified(t)).map(|(p, _)| *p).collect();
114 xs.iter().take(plain.len()).all(|(_, t)| verified(t))
115 && plain.windows(2).all(|w| w[0] < w[1])
116 && rest.windows(2).all(|w| w[0] < w[1])
117 };
118 assert!(sorted(CHIPS) && sorted(PANELS));
119 for (p, text) in PANELS {
120 assert!(crate::PanelSpec::parse(text).is_ok(), "{p}");
121 }
122 for (p, text) in CHIPS {
123 assert!(crate::ChipLibrary::parse(text).is_ok(), "{p}");
124 }
125 }
126 }
127}
128
129use anyhow::{bail, Context, Result};
130use serde::{Deserialize, Serialize, Serializer};
131use std::collections::BTreeMap;
132use std::path::Path;
133
134pub const RECORD01_LEN: usize = 764;
136
137pub type Loader<'a> = &'a dyn Fn(&str) -> Result<String>;
140
141pub fn read_library(path: &str) -> Result<String> {
147 std::fs::read_to_string(path).with_context(|| format!("read {path}"))
148}
149
150#[derive(Debug, Clone, Deserialize, Serialize)]
151#[serde(deny_unknown_fields)]
152pub struct PanelSpec {
153 pub name: String,
155 #[serde(default)]
157 pub meta: Meta,
158 pub module: Module,
159 pub screen: Screen,
160 pub chip: Chip,
161 #[serde(default)]
162 pub color: Color,
163 #[serde(default)]
164 pub current: Current,
165 #[serde(default)]
166 pub timing: Timing,
167 #[serde(default)]
168 pub mapping: Mapping,
169 #[serde(default)]
170 pub boot: Boot,
171 #[serde(
174 default,
175 deserialize_with = "chips::record01_offsets",
176 serialize_with = "chips::hex_offsets",
177 skip_serializing_if = "BTreeMap::is_empty"
178 )]
179 pub record01_overrides: BTreeMap<usize, u8>,
180}
181
182struct Short(f32);
185
186impl Serialize for Short {
187 fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
188 let text = self.0.to_string();
189 s.serialize_f64(text.parse().unwrap_or_else(|_| f64::from(self.0)))
190 }
191}
192
193#[allow(clippy::trivially_copy_pass_by_ref)]
195fn short<S: Serializer>(v: &f32, s: S) -> std::result::Result<S::Ok, S::Error> {
196 Short(*v).serialize(s)
197}
198
199fn shorts<S: Serializer>(v: &[f32], s: S) -> std::result::Result<S::Ok, S::Error> {
200 s.collect_seq(v.iter().map(|&x| Short(x)))
201}
202
203#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
207#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
208#[serde(default, deny_unknown_fields)]
209pub struct Meta {
210 #[serde(skip_serializing_if = "Option::is_none")]
212 #[cfg_attr(feature = "ts", ts(optional))]
213 pub pitch_mm: Option<f32>,
214 pub status: Status,
215 pub sources: u32,
217 #[serde(skip_serializing_if = "Option::is_none")]
220 #[cfg_attr(feature = "ts", ts(optional))]
221 pub agreement: Option<f32>,
222 pub examples: Vec<String>,
224 pub vendors: Vec<String>,
227 #[serde(skip_serializing_if = "Option::is_none")]
228 #[cfg_attr(feature = "ts", ts(optional))]
229 pub notes: Option<String>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
232 #[cfg_attr(feature = "ts", ts(optional))]
233 pub maker: Option<String>,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
236 #[cfg_attr(feature = "ts", ts(optional))]
237 pub product: Option<String>,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
240 #[cfg_attr(feature = "ts", ts(optional))]
241 pub url: Option<String>,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
244 #[cfg_attr(feature = "ts", ts(optional))]
245 pub datasheet: Option<String>,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
248 #[cfg_attr(feature = "ts", ts(optional))]
249 pub image: Option<String>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 #[cfg_attr(feature = "ts", ts(optional))]
252 pub image_source: Option<String>,
253}
254
255#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
257#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
258#[serde(rename_all = "lowercase")]
259pub enum Status {
260 Verified,
262 #[default]
265 Derived,
266 Stub,
268}
269
270#[derive(Debug, Clone, Deserialize, Serialize)]
271#[serde(deny_unknown_fields)]
272pub struct Module {
273 pub width: u16,
275 pub height: u16,
277 pub scan: u8,
279 #[serde(skip_serializing_if = "Option::is_none")]
282 pub serial_clock: Option<u16>,
283 #[serde(skip_serializing_if = "Option::is_none")]
285 pub gray_bits: Option<u8>,
286 #[serde(default)]
288 pub line_dir: u8,
289 #[serde(default = "default_data_groups")]
291 pub data_groups: u8,
292}
293
294#[derive(Debug, Clone, Deserialize, Serialize)]
295#[serde(deny_unknown_fields)]
296pub struct Screen {
297 pub width: u16,
299 pub height: u16,
300}
301
302#[derive(Debug, Clone, Deserialize, Serialize)]
303#[serde(deny_unknown_fields)]
304pub struct Chip {
305 pub library: String,
307}
308
309#[derive(Debug, Clone, Deserialize, Serialize)]
310#[serde(deny_unknown_fields)]
311pub struct Color {
312 pub swap: u8,
314 pub source: [u8; 3],
316}
317
318impl Default for Color {
319 fn default() -> Self {
320 Self {
321 swap: 3,
322 source: [2, 1, 0],
323 }
324 }
325}
326
327#[derive(Debug, Clone, Deserialize, Serialize)]
328#[serde(deny_unknown_fields)]
329pub struct Current {
330 pub gains: [u8; 4],
332 #[serde(serialize_with = "shorts")]
334 pub percent: [f32; 3],
335}
336
337impl Default for Current {
338 fn default() -> Self {
339 Self {
340 gains: [43; 4],
341 percent: [0.1; 3],
342 }
343 }
344}
345
346#[derive(Debug, Clone, Deserialize, Serialize)]
347#[serde(deny_unknown_fields)]
348pub struct Timing {
349 #[serde(serialize_with = "short")]
350 pub gamma: f32,
351 #[serde(serialize_with = "short")]
352 pub refresh_hz: f32,
353 pub gclock: u8,
355 #[serde(serialize_with = "short")]
357 pub min_oe: f32,
358 pub luminance_level: u16,
360 pub oe_8ns: bool,
362}
363
364impl Default for Timing {
365 fn default() -> Self {
366 Self {
367 gamma: 2.8,
368 refresh_hz: 60.0,
369 gclock: 0x14,
370 min_oe: 1e-4,
371 luminance_level: 188,
372 oe_8ns: true,
373 }
374 }
375}
376
377#[derive(Debug, Clone, Deserialize, Serialize)]
380#[serde(deny_unknown_fields)]
381pub struct Mapping {
382 pub reversed_groups: bool,
385 pub reversed_lines: bool,
387 #[serde(default, skip_serializing_if = "Option::is_none")]
391 pub block: Option<u16>,
392 #[serde(default = "default_true")]
396 pub gate_phantom_positions: bool,
397}
398
399const fn default_true() -> bool {
400 true
401}
402
403impl Default for Mapping {
404 fn default() -> Self {
405 Self {
406 reversed_groups: true,
407 reversed_lines: false,
408 block: None,
409 gate_phantom_positions: true,
410 }
411 }
412}
413
414#[derive(Debug, Clone, Default, Deserialize, Serialize)]
415#[serde(deny_unknown_fields)]
416pub struct Boot {
417 pub arm_at_boot: bool,
420}
421
422const fn default_data_groups() -> u8 {
423 1
424}
425
426impl PanelSpec {
427 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
432 let path = path.as_ref();
433 let text =
434 std::fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
435 Self::parse(&text).with_context(|| format!("parse {}", path.display()))
436 }
437
438 pub fn parse(text: &str) -> Result<Self> {
443 Ok(toml::from_str(text)?)
444 }
445
446 pub fn to_toml(&self) -> Result<String> {
452 toml::to_string(self).context("write spec")
453 }
454
455 pub fn chip_library(&self, load: Loader) -> Result<ChipLibrary> {
461 let path = &self.chip.library;
462 let text = load(path)?;
463 ChipLibrary::parse(&text).with_context(|| format!("parse {path}"))
464 }
465
466 pub fn validate(&self) -> Result<()> {
471 if !self.module.height.is_multiple_of(2) {
472 bail!("module height must be even (the record stores height/2)");
473 }
474 if self.module.width > 255 || self.module.height / 2 > 255 {
475 bail!("module dimensions exceed the record's byte fields");
476 }
477 if !self.screen.width.is_multiple_of(self.module.width)
478 || !self.screen.height.is_multiple_of(self.module.height)
479 {
480 bail!("screen size must be a whole number of modules");
481 }
482 if self.module.scan == 0 || u16::from(self.module.scan) > self.module.height {
483 bail!("scan denominator must be 1..=module height");
484 }
485 if !(self.module.height / 2).is_multiple_of(u16::from(self.module.scan)) {
486 bail!("stored module height (height/2) must be a whole number of scan groups");
487 }
488 Ok(())
489 }
490
491 #[must_use]
493 pub fn serial_clock(&self, chip: &ChipLibrary) -> u16 {
494 self.module.serial_clock.unwrap_or(chip.serial_clock)
495 }
496
497 pub fn gray_bits(&self, chip: &ChipLibrary) -> Result<u8> {
503 match self.module.gray_bits {
504 Some(g) => Ok(g),
505 None => chip.gray_bits(),
506 }
507 }
508
509 #[must_use]
512 pub fn screen_extent_in_line_dir(&self) -> u16 {
513 if self.module.line_dir >= 2 {
514 self.screen.height
515 } else {
516 self.screen.width
517 }
518 }
519
520 #[must_use]
523 pub fn module_input_count(&self) -> u8 {
524 let unit = 16u16;
525 let dim = if self.module.line_dir >= 2 {
526 self.module.width
527 } else {
528 self.module.height / 2
529 };
530 (unit / dim.max(1)).max(1) as u8
531 }
532
533 #[must_use]
536 pub fn modules_in_line_dir(&self) -> u16 {
537 if self.module.line_dir >= 2 {
538 self.screen.height.div_ceil(self.module.height)
539 } else {
540 self.screen.width.div_ceil(self.module.width)
541 }
542 }
543
544 #[must_use]
546 pub fn one_scan_len(&self) -> u16 {
547 let v = u32::from(self.module.width) * u32::from(self.module.height / 2)
548 / u32::from(self.module.scan);
549 v.max(1) as u16
550 }
551
552 #[must_use]
555 pub fn card_scan_len(&self) -> u16 {
556 self.one_scan_len() * self.modules_in_line_dir()
557 }
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563
564 fn spec() -> PanelSpec {
565 PanelSpec::parse(
566 r#"
567 name = "t"
568 [module]
569 width = 128
570 height = 64
571 scan = 16
572 [screen]
573 width = 256
574 height = 64
575 [chip]
576 library = "x.toml"
577 "#,
578 )
579 .unwrap()
580 }
581
582 #[test]
583 fn geometry_helpers_follow_the_vendor_formulas() {
584 let s = spec();
585 assert!(s.validate().is_ok());
586 assert_eq!(s.modules_in_line_dir(), 2);
587 assert_eq!(s.one_scan_len(), 256);
588 assert_eq!(s.card_scan_len(), 512);
589 assert_eq!(s.screen_extent_in_line_dir(), 256);
590 assert_eq!(s.module_input_count(), 1);
591 }
592
593 #[test]
594 fn a_scan_that_does_not_divide_the_module_is_refused() {
595 let mut s = spec();
596 s.module.scan = 12;
597 assert!(s.validate().is_err());
598 }
599
600 #[test]
601 fn unknown_fields_are_refused() {
602 assert!(PanelSpec::parse("name = \"t\"\nextra = 1\n").is_err());
603 }
604
605 #[test]
606 fn a_spec_written_as_toml_reads_back_to_the_same_values() {
607 let text = std::fs::read_to_string(concat!(
608 env!("CARGO_MANIFEST_DIR"),
609 "/config/panels/p25-128x64-sm16269s.toml"
610 ))
611 .unwrap();
612 let spec = PanelSpec::parse(&text).unwrap();
613 let out = spec.to_toml().unwrap();
614 assert!(out.starts_with("name = \"p25-128x64-sm16269s\"\n\n[meta]\n"), "{out}");
615 assert!(out.contains("\n[record01_overrides]\n0x02F = 1\n"), "{out}");
616 assert!(out.contains("gamma = 2.8\n") && out.contains("min_oe = 0.0001\n"), "{out}");
617 let back = PanelSpec::parse(&out).unwrap();
618 assert_eq!(back.to_toml().unwrap(), out);
619 assert_eq!(back.record01_overrides, spec.record01_overrides);
620 assert_eq!(back.timing.min_oe.to_bits(), spec.timing.min_oe.to_bits());
621 assert_eq!(back.module.serial_clock, Some(8));
622
623 let mut bare = spec;
624 bare.record01_overrides.clear();
625 bare.mapping.block = None;
626 let out = bare.to_toml().unwrap();
627 assert!(!out.contains("record01_overrides") && !out.contains("block"), "{out}");
628 }
629}