pub mod firmware;
use serde::Deserialize;
use std::fmt;
use std::ops::Range;
use std::sync::OnceLock;
mod embedded {
include!(concat!(env!("OUT_DIR"), "/cards.rs"));
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CardModel {
pub name: String,
pub vendor: String,
pub family: String,
#[serde(default)]
pub id: Option<u8>,
pub status: Status,
#[serde(default)]
pub notes: String,
#[serde(default)]
pub image: Option<String>,
#[serde(default)]
pub image_source: Option<String>,
#[serde(default)]
pub datasheet: Option<String>,
#[serde(default)]
pub tested: Vec<Tested>,
pub limits: Limits,
pub memory: Memory,
pub firmware: Firmware,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
Tested,
Generates,
Unsupported,
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Tested => "tested",
Self::Generates => "generates",
Self::Unsupported => "unsupported",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Tested {
pub panel: String,
pub firmware: String,
}
impl Tested {
#[must_use]
pub fn image(&self) -> Option<&'static firmware::Image> {
firmware::image(&self.firmware)
}
#[must_use]
pub fn version(&self) -> Option<Version> {
self.image().map(|i| i.version)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Limits {
pub max_width: u16,
pub max_height: u16,
pub hub_ports: u8,
pub chain: Option<u16>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Memory {
pub block_bytes: u32,
pub primary_bank: u32,
pub bank_bytes: u32,
pub golden_bank: u32,
pub parameter_block: u8,
pub eeprom_mirror: u32,
#[serde(default)]
pub guarded: Vec<Guard>,
pub boot_image: BootImage,
}
impl Memory {
#[must_use]
pub fn primary_blocks(&self) -> Range<u8> {
let first = (self.primary_bank / self.block_bytes) as u8;
first..first + self.bank_blocks()
}
#[must_use]
pub fn bank_blocks(&self) -> u8 {
self.bank_bytes.div_ceil(self.block_bytes) as u8
}
#[must_use]
pub fn golden_block(&self) -> u8 {
(self.golden_bank / self.block_bytes) as u8
}
#[must_use]
pub fn guarded_blocks(&self, version: Version) -> &[u8] {
self.guarded
.iter()
.find(|g| g.covers(version))
.map_or(&[], |g| g.blocks.as_slice())
}
#[must_use]
pub fn config_page(&self) -> u16 {
(u16::from(self.parameter_block) << 8) | (self.boot_image.rcvbp / 0x100) as u16
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Guard {
pub from: Version,
pub to: Option<Version>,
pub blocks: Vec<u8>,
}
impl Guard {
fn covers(&self, v: Version) -> bool {
self.from <= v && self.to.is_none_or(|to| v <= to)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Version(pub u8, pub u8);
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}", self.0, self.1)
}
}
impl std::str::FromStr for Version {
type Err = String;
fn from_str(s: &str) -> Result<Self, String> {
let (a, b) = s
.split_once('.')
.ok_or_else(|| format!("version {s:?} is not major.minor"))?;
let n = |v: &str| v.parse::<u8>().map_err(|e| format!("version {s:?}: {e}"));
Ok(Self(n(a)?, n(b)?))
}
}
impl<'de> Deserialize<'de> for Version {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BootImage {
pub basic_pack: usize,
pub data_swap: usize,
pub module_positions: usize,
pub chip_page: usize,
pub void_line: usize,
pub void_line_columns: usize,
pub anti_void: usize,
pub mapping: usize,
pub scan_table: usize,
pub rcvbp: usize,
pub map_entries: usize,
pub rcvbp_max: usize,
}
impl BootImage {
#[must_use]
pub const fn mapping_len(&self) -> usize {
self.map_entries * 3
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Firmware {
pub image_pattern: String,
pub sdram_staging: bool,
}
impl Firmware {
#[must_use]
pub fn version_in_name(&self, path: &str) -> Option<Version> {
let name = std::path::Path::new(path).file_name()?.to_str()?;
let prefix = self.image_pattern.split("{version}").next()?;
let marker = prefix.rsplit('_').next().filter(|m| !m.is_empty())?;
let rest = &name[name.find(marker)? + marker.len()..];
let end = rest
.find(|c: char| !(c.is_ascii_digit() || c == '.'))
.unwrap_or(rest.len());
rest[..end].parse().ok()
}
}
fn parse_all() -> Vec<CardModel> {
let mut models: Vec<CardModel> = embedded::FILES
.iter()
.map(|(file, text)| {
toml::from_str(text).unwrap_or_else(|e| panic!("config/cards/{file}: {e}"))
})
.collect();
models.sort_by(|a, b| a.status.cmp(&b.status).then_with(|| a.name.cmp(&b.name)));
models
}
pub fn models() -> &'static [CardModel] {
static MODELS: OnceLock<Vec<CardModel>> = OnceLock::new();
MODELS.get_or_init(parse_all)
}
#[must_use]
pub fn by_id(id: u8) -> Option<&'static CardModel> {
models().iter().find(|m| m.id == Some(id))
}
#[must_use]
pub fn by_name(name: &str) -> Option<&'static CardModel> {
models().iter().find(|m| m.name.eq_ignore_ascii_case(name))
}
#[must_use]
pub fn default_model() -> &'static CardModel {
&models()[0]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_embedded_file_parses_and_ids_are_unique() {
let all = models();
assert!(!all.is_empty());
for (i, m) in all.iter().enumerate() {
assert!(
m.id.is_none() || all[..i].iter().all(|o| o.id != m.id),
"{}: id shared",
m.name
);
assert!(
all[..i]
.iter()
.all(|o| !o.name.eq_ignore_ascii_case(&m.name)),
"{}: name shared",
m.name
);
assert!(
m.memory
.primary_blocks()
.contains(&m.memory.parameter_block),
"{}: parameter block outside the bank",
m.name
);
assert!(
!m.memory.primary_blocks().contains(&m.memory.golden_block()),
"{}: golden bank inside the primary",
m.name
);
}
assert_eq!(all[0].status, Status::Tested);
for m in all {
assert_eq!(m.status == Status::Tested, !m.tested.is_empty(), "{}: status and tested disagree", m.name);
for t in &m.tested {
assert!(t.image().is_some(), "{}: tested firmware {} is not in config/firmware.toml", m.name, t.firmware);
}
}
}
#[test]
fn every_model_cites_a_source_and_only_tested_models_list_panels() {
for m in models() {
assert!(!m.notes.trim().is_empty(), "{}: no source note", m.name);
assert!(
m.notes.contains("http") || m.datasheet.is_some(),
"{}: the note names no source URL and there is no datasheet",
m.name
);
assert_eq!(
m.status == Status::Tested,
!m.tested.is_empty(),
"{}: only a tested model carries [[tested]]",
m.name
);
}
}
#[test]
fn an_unlisted_id_resolves_to_no_model() {
assert!(by_id(0x03).is_none());
for m in models() {
if let Some(id) = m.id {
assert!(std::ptr::eq(by_id(id).unwrap(), m), "{}: id lookup", m.name);
}
}
}
#[test]
fn the_e120_is_found_by_id_and_by_name() {
let m = by_id(0x64).expect("E120 by id");
assert_eq!(m.name, "E120");
assert_eq!(
m.tested,
[Tested {
panel: "config/panels/p25-128x64-sm16269s.toml".into(),
firmware: "E320_PWM_FPGA16.53_20231227_SM16386S_SM16269SH.hex".into()
}]
);
assert_eq!(m.tested[0].version(), Some(Version(16, 53)));
assert!(std::ptr::eq(by_name("e120").unwrap(), m));
assert!(std::ptr::eq(default_model(), m));
assert!(by_id(0x03).is_none());
assert!(by_name("e121").is_none());
}
#[test]
fn the_e120_map_reads_as_blocks() {
let m = by_name("E120").unwrap().memory.clone();
assert_eq!(m.primary_blocks(), 0x00..0x0b);
assert_eq!(m.bank_blocks(), 11);
assert_eq!(m.golden_block(), 0x20);
assert_eq!(m.config_page(), 0x0780);
assert_eq!(m.guarded_blocks(Version(16, 53)), &[0, 1, 2, 8]);
assert_eq!(m.guarded_blocks(Version(17, 0)), &[0, 1, 2, 8]);
assert!(m.guarded_blocks(Version(10, 81)).is_empty());
assert_eq!(m.boot_image.mapping_len(), 0x3000);
}
#[test]
fn versions_order_numerically_and_round_trip() {
assert!(Version(16, 53) < Version(17, 0));
assert!(Version(9, 53) < Version(16, 53));
assert_eq!("16.53".parse::<Version>(), Ok(Version(16, 53)));
assert_eq!(Version(16, 53).to_string(), "16.53");
assert!("16".parse::<Version>().is_err());
}
#[test]
fn the_version_is_read_from_the_image_name() {
let fw = &by_name("E120").unwrap().firmware;
assert_eq!(
fw.version_in_name(
"third-party/firmware/E320_PWM_FPGA16.53_20231227_SM16386S_SM16269SH.hex"
),
Some(Version(16, 53))
);
assert_eq!(
fw.version_in_name("E320_PCB6.0_PWM_FPGA10.81_20230907.hex"),
Some(Version(10, 81))
);
assert_eq!(fw.version_in_name("image.hex"), None);
}
}