use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum Probed<T> {
Present { value: T, evidence: Evidence },
Absent { reason: AbsenceReason },
#[default]
NotProbed,
}
impl<T> Probed<T> {
pub fn is_present(&self) -> bool {
matches!(self, Probed::Present { .. })
}
pub fn value(&self) -> Option<&T> {
match self {
Probed::Present { value, .. } => Some(value),
_ => None,
}
}
pub fn present(value: T, evidence: Evidence) -> Self {
Probed::Present { value, evidence }
}
pub fn absent(reason: AbsenceReason) -> Self {
Probed::Absent { reason }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Evidence {
SysfsPath(String),
DeviceTreeCompatible(String),
Ioctl { node: String, call: String },
TrialInit { node: String, ms: u32 },
ProcCpuinfo { midr: u32 },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "reason", rename_all = "snake_case")]
pub enum AbsenceReason {
NodeMissing,
FormatUnsupported,
TrialInitFailed { exit_after_ms: u32 },
TrialInitHung,
DeniedByQuirk { key: String },
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ProbePhase {
Setup,
BootPreArm,
Runtime,
}
impl ProbePhase {
pub fn allows_destructive(&self) -> bool {
!matches!(self, ProbePhase::Runtime)
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct Midr(pub u32);
impl Midr {
pub fn cortex_name(&self) -> Option<&'static str> {
let part = (self.0 >> 4) & 0xFFF;
Some(match part {
0xd0b => "Cortex-A76",
0xd05 => "Cortex-A55",
0xd03 => "Cortex-A53",
0xd08 => "Cortex-A72",
0xc07 => "Cortex-A7",
_ => return None,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CoreCluster {
pub cortex: String,
pub midr: Midr,
pub count: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SocCompatible(pub Vec<String>);
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct V4l2Node {
pub path: String,
pub is_m2m: bool,
pub output_fourccs: Vec<[u8; 4]>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct EncoderDevice {
pub node: String,
pub fourcc: [u8; 4],
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct UsbId {
pub vid: u16,
pub pid: u16,
}
pub fn is_rc_bridge_usb_id(vid: u16, pid: u16) -> bool {
matches!((vid, pid), (0x10C4, 0xEA60) | (0x1A86, 0x7523)) || vid == 0x303A
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SerialPort {
pub path: String,
pub usb: Option<UsbId>,
pub fc_score: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct HardwareCapabilities {
pub cpu: Probed<Vec<CoreCluster>>,
pub soc: Probed<SocCompatible>,
pub h264_encoder: Probed<EncoderDevice>,
pub usb: Probed<Vec<UsbId>>,
pub serial: Probed<Vec<SerialPort>>,
}
impl Default for HardwareCapabilities {
fn default() -> Self {
Self {
cpu: Probed::NotProbed,
soc: Probed::NotProbed,
h264_encoder: Probed::NotProbed,
usb: Probed::NotProbed,
serial: Probed::NotProbed,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn midr_maps_known_cortex_parts() {
assert_eq!(Midr(0x410f_d0b0 & 0xFFF0).cortex_name(), Some("Cortex-A76"));
assert_eq!(Midr(0xd0b << 4).cortex_name(), Some("Cortex-A76"));
assert_eq!(Midr(0xd05 << 4).cortex_name(), Some("Cortex-A55"));
assert_eq!(Midr(0xd03 << 4).cortex_name(), Some("Cortex-A53"));
assert_eq!(Midr(0xd08 << 4).cortex_name(), Some("Cortex-A72"));
assert_eq!(Midr(0xc07 << 4).cortex_name(), Some("Cortex-A7"));
assert_eq!(Midr(0xfff << 4).cortex_name(), None);
}
#[test]
fn rc_bridge_ids_match_full_pairs_and_espressif_vendor() {
assert!(is_rc_bridge_usb_id(0x10C4, 0xEA60)); assert!(is_rc_bridge_usb_id(0x1A86, 0x7523)); assert!(!is_rc_bridge_usb_id(0x10C4, 0x0001)); assert!(!is_rc_bridge_usb_id(0x1A86, 0x0001)); assert!(is_rc_bridge_usb_id(0x303A, 0x1001));
assert!(is_rc_bridge_usb_id(0x303A, 0x0009));
assert!(!is_rc_bridge_usb_id(0x0483, 0x5740)); assert!(!is_rc_bridge_usb_id(0x0403, 0x6001)); assert!(!is_rc_bridge_usb_id(0x1209, 0x5741)); }
#[test]
fn phase_gates_destructive_probes() {
assert!(ProbePhase::Setup.allows_destructive());
assert!(ProbePhase::BootPreArm.allows_destructive());
assert!(!ProbePhase::Runtime.allows_destructive());
}
#[test]
fn probed_present_carries_evidence() {
let p = Probed::present(
EncoderDevice {
node: "/dev/video11".into(),
fourcc: *b"H264",
},
Evidence::TrialInit {
node: "/dev/video11".into(),
ms: 42,
},
);
assert!(p.is_present());
assert_eq!(p.value().unwrap().node, "/dev/video11");
}
#[test]
fn probed_default_is_not_probed() {
let p: Probed<u32> = Probed::default();
assert!(matches!(p, Probed::NotProbed));
assert!(!p.is_present());
}
#[test]
fn snapshot_round_trips_json() {
let caps = HardwareCapabilities {
h264_encoder: Probed::absent(AbsenceReason::TrialInitFailed { exit_after_ms: 130 }),
cpu: Probed::present(
vec![CoreCluster {
cortex: "Cortex-A55".into(),
midr: Midr(0xd05 << 4),
count: 8,
}],
Evidence::ProcCpuinfo { midr: 0xd05 << 4 },
),
..Default::default()
};
let json = serde_json::to_string(&caps).unwrap();
let back: HardwareCapabilities = serde_json::from_str(&json).unwrap();
assert_eq!(caps, back);
assert!(matches!(
back.h264_encoder,
Probed::Absent {
reason: AbsenceReason::TrialInitFailed { exit_after_ms: 130 }
}
));
}
}