1use serde::Serialize;
4
5static RASPI_ENV: std::sync::OnceLock<RaspiEnv> = std::sync::OnceLock::new();
6
7#[derive(Debug)]
8pub enum RaspiEnv {
9 NotRasRi,
10 RasRi {
11 model: String,
12 cameras: Vec<CameraInfo>,
13 },
14}
15
16impl std::fmt::Display for RaspiEnv {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 match self {
19 RaspiEnv::NotRasRi => write!(f, "Not Raspberry Pi"),
20 RaspiEnv::RasRi { model, cameras } => {
21 writeln!(f, "Model: {model}")?;
22
23 writeln!(f, "Cameras:")?;
24 for (i, cam) in cameras.iter().enumerate() {
25 write!(
26 f,
27 "{i}: model={}, resolution={}x{}",
28 cam.model, cam.width, cam.height
29 )?;
30 if i < cameras.len() - 1 {
31 writeln!(f)?;
32 }
33 }
34 Ok(())
35 }
36 }
37 }
38}
39
40impl RaspiEnv {
41 pub fn default_camera(&self) -> Option<&CameraInfo> {
43 match self {
44 RaspiEnv::RasRi { cameras, .. } => cameras.first(),
45 _ => None,
46 }
47 }
48}
49
50#[derive(Debug, Serialize)]
51pub struct CameraInfo {
52 pub model: String,
53 pub width: u32,
54 pub height: u32,
55}
56
57fn get_env() -> RaspiEnv {
58 let model = std::fs::read_to_string("/proc/device-tree/model")
60 .map(|s| s.trim_end_matches('\0').to_string());
61
62 match model {
63 Ok(model) => {
64 let cameras = get_camera_env().unwrap();
65 RaspiEnv::RasRi { model, cameras }
66 }
67 Err(err) => {
68 if err.kind() == std::io::ErrorKind::NotFound {
71 RaspiEnv::NotRasRi
72 } else {
73 panic!("{err}");
74 }
75 }
76 }
77}
78
79fn get_camera_env() -> anyhow::Result<Vec<CameraInfo>> {
80 let output = std::process::Command::new("rpicam-hello")
81 .arg("--list-cameras")
82 .output()?;
83 anyhow::ensure!(output.status.success(), "rpicam-hello failed");
84 let stdout = String::from_utf8_lossy(&output.stdout);
85
86 parse_camera_list(&stdout)
87}
88
89fn parse_camera_list(stdout: &str) -> anyhow::Result<Vec<CameraInfo>> {
99 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
100 let re = RE.get_or_init(|| regex::Regex::new(r"\s*\d+\s*:(.*)\[(\d+)x(\d+).*\]").unwrap());
101
102 let mut res = Vec::new();
103 for line in stdout.lines() {
104 if let Some(caps) = re.captures(line) {
105 let model = caps[1].trim().to_string();
106 let width = caps[2].parse().unwrap();
107 let height = caps[3].parse().unwrap();
108 res.push(CameraInfo {
109 model,
110 width,
111 height,
112 });
113 }
114 }
115
116 Ok(res)
117}
118
119pub fn raspi_env() -> &'static RaspiEnv {
120 RASPI_ENV.get_or_init(get_env)
121}
122
123#[cfg(test)]
124mod test {
125 use super::*;
126
127 #[test]
128 fn raspi_env_not_panic() {
129 let _env = raspi_env();
130 }
131
132 #[test]
133 fn raspi_env_camera() {
134 let sample = r"Available cameras
135-----------------
1360 : imx500 [4056x3040 10-bit RGGB] (/base/axi/pcie@1000120000/rp1/i2c@88000/imx500@1a)
137 Modes: 'SRGGB10_CSI2P' : 2028x1520 [30.02 fps - (0, 0)/4056x3040 crop]
138 4056x3040 [10.00 fps - (0, 0)/4056x3040 crop]
139";
140 let cameras = parse_camera_list(sample).unwrap();
141 assert_eq!(cameras.len(), 1);
142 assert_eq!(cameras[0].model, "imx500");
143 assert_eq!(cameras[0].width, 4056);
144 assert_eq!(cameras[0].height, 3040);
145 }
146}