sys/
rpienv.rs

1//! Raspberry Pi 固有の環境情報。
2
3use 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    /// デフォルトカメラ
42    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    // e.g. "Raspberry Pi 5 Model B Rev 1.1"
59    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            // NotFound は Raspberry Pi ではない正常環境
69            // それ以外は panic
70            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
89/*
90Sample:
91
92Available cameras
93-----------------
940 : imx500 [4056x3040 10-bit RGGB] (/base/axi/pcie@1000120000/rp1/i2c@88000/imx500@1a)
95    Modes: 'SRGGB10_CSI2P' : 2028x1520 [30.02 fps - (0, 0)/4056x3040 crop]
96                             4056x3040 [10.00 fps - (0, 0)/4056x3040 crop]
97 */
98fn 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}