Skip to main content

ados_protocol/
vision_status.rs

1//! The vision engine's work sidecar: proof that the engine is consuming frames.
2//!
3//! `ados-vision` takes every frame over a Unix socket (the video pipeline's
4//! tap) and hands it on over shared memory and more Unix sockets. None of that
5//! moves the process's `/proc/<pid>/io` `rchar`/`wchar` counters, which follow
6//! only the `read(2)`/`write(2)` file path, so a fully fed engine reads as
7//! flat there. The engine instead publishes the cumulative count of frames it
8//! has consumed, plus how many of its inputs are delivering right now, and the
9//! supervisor judges it on that count.
10//!
11//! The count only means something while an input is delivering: an engine
12//! whose camera is absent, or whose tap has gone silent, is idle, not wedged.
13//! [`VisionStatus::work_counter`] therefore answers only while at least one
14//! input is live. A source that delivers nothing for a while is closed by the
15//! engine and stops counting as live, so a silent upstream never ages into a
16//! stall verdict; an engine wedged anywhere else keeps its inputs live with a
17//! frozen count, which is exactly a stall.
18//!
19//! Producer: `ados-vision`, rewritten about once a second under its socket
20//! directory. Consumer: the supervisor's work proof.
21
22use serde::{Deserialize, Serialize};
23
24/// The sidecar's file name under the run directory (`/run/ados` by default).
25pub const VISION_STATUS_FILE: &str = "vision-status.json";
26
27/// The sidecar schema version, stamped on write.
28pub const VISION_STATUS_VERSION: u16 = 1;
29
30/// The last failure of one camera's frame source (open, format negotiation or
31/// stream end), kept until the source next delivers a frame.
32#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
33pub struct SourceError {
34    pub camera: String,
35    pub error: String,
36}
37
38/// The engine's work state.
39#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
40pub struct VisionStatus {
41    #[serde(default)]
42    pub version: u16,
43    /// Frames consumed from every input since the engine started.
44    pub frames_consumed: u64,
45    /// Inputs that have delivered a frame since they were last (re)opened and
46    /// have not since failed or gone silent.
47    pub live_inputs: u32,
48    /// Captured buffers the driver flagged as corrupt and the engine dropped.
49    #[serde(default)]
50    pub error_frames: u64,
51    /// Cameras whose source is currently failing, with the reason.
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub source_errors: Vec<SourceError>,
54}
55
56impl VisionStatus {
57    /// The work counter to judge the engine on: the frames consumed, but only
58    /// while an input is delivering. `None` (no verdict) otherwise.
59    pub fn work_counter(&self) -> Option<u64> {
60        (self.live_inputs > 0).then_some(self.frames_consumed)
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn the_count_is_a_verdict_only_while_an_input_is_delivering() {
70        let fed = VisionStatus {
71            version: VISION_STATUS_VERSION,
72            frames_consumed: 42,
73            live_inputs: 1,
74            ..Default::default()
75        };
76        assert_eq!(fed.work_counter(), Some(42));
77        // No camera, or a silent tap: idle, never a stall.
78        let idle = VisionStatus {
79            live_inputs: 0,
80            ..fed.clone()
81        };
82        assert_eq!(idle.work_counter(), None);
83    }
84
85    #[test]
86    fn the_wire_shape_round_trips_and_tolerates_a_missing_version() {
87        let s = VisionStatus {
88            version: VISION_STATUS_VERSION,
89            frames_consumed: 7,
90            live_inputs: 2,
91            error_frames: 1,
92            source_errors: vec![SourceError {
93                camera: "cam_fl".into(),
94                error: "device busy".into(),
95            }],
96        };
97        let text = serde_json::to_string(&s).unwrap();
98        assert_eq!(serde_json::from_str::<VisionStatus>(&text).unwrap(), s);
99        let old: VisionStatus =
100            serde_json::from_str(r#"{"frames_consumed":3,"live_inputs":1}"#).unwrap();
101        assert_eq!(old.version, 0);
102        assert_eq!(old.work_counter(), Some(3));
103    }
104}