Skip to main content

ados_protocol/
camera_control.rs

1//! The `camera.control.set` plugin method: set exposure, gain or the
2//! auto-exposure mode of a camera the vision engine owns, and report the
3//! values the driver reads back.
4//!
5//! The plugin host relays the request to the vision engine over
6//! `vision.sock` under the same method name. The engine applies it through
7//! the device handle it already streams from (the device is never opened a
8//! second time), reads every touched control back and answers with a
9//! [`CameraControlResult`]. `applied` is true only when every requested
10//! control was set and its read-back matches (exposure and gain within one
11//! driver step); otherwise `reason` carries one of the [`reason`] codes.
12//!
13//! Only cameras configured `capture: true` in `cameras[]` may be controlled;
14//! any other camera is refused with [`reason::NOT_CAPTURE_CAMERA`].
15//!
16//! `gain` is a multiplier (`1.0` is unity gain), on the request and on the
17//! read-back alike. The engine maps it to the driver's gain code linearly,
18//! taking the control's default code as 1×; a control whose default does not
19//! name a unit gain is refused with [`reason::GAIN_UNIT_UNKNOWN`] rather than
20//! guessed at.
21//!
22//! The types are the wire shape on both hops: the SDK encodes the request,
23//! the host and the engine decode it, and the reply travels back unchanged.
24
25use rmpv::Value;
26use serde::{Deserialize, Serialize};
27
28/// The plugin method, also the `vision.sock` method the host relays it as.
29pub const CAMERA_CONTROL_METHOD: &str = "camera.control.set";
30/// The capability the method is gated on.
31pub const CAMERA_CONTROL_CAPABILITY: &str = "camera.control.write";
32/// How long the engine waits, per request, for the camera's capture thread to
33/// apply it. A request not applied by then is answered
34/// [`reason::NOT_STREAMING`] and dropped, never applied later.
35pub const ENGINE_WAIT_MS: u64 = 1_500;
36
37/// Why a request was not applied.
38pub mod reason {
39    /// The capture camera is not a local V4L2 device (a simulated RTSP rig):
40    /// it has no controls this node can set.
41    pub const NOT_V4L2: &str = "not_v4l2";
42    /// The driver lacks a requested control, or reports it read-only.
43    pub const CONTROL_UNSUPPORTED: &str = "control_unsupported";
44    /// No camera with that id runs on this node.
45    pub const UNKNOWN_CAMERA: &str = "unknown_camera";
46    /// The camera is not configured `capture: true`, so a plugin may not
47    /// change its controls (the video pipeline's own camera, for one).
48    pub const NOT_CAPTURE_CAMERA: &str = "not_capture_camera";
49    /// The driver's gain control has no known unit gain, so a multiplier
50    /// cannot be mapped to it.
51    pub const GAIN_UNIT_UNKNOWN: &str = "gain_unit_unknown";
52    /// The camera's capture is not running, so there is no open device to
53    /// set the controls on.
54    pub const NOT_STREAMING: &str = "not_streaming";
55    /// A requested value lies outside the driver's range.
56    pub const OUT_OF_RANGE: &str = "out_of_range";
57    /// The driver refused to set a control.
58    pub const SET_FAILED: &str = "set_failed";
59    /// The driver accepted the values but reads back something else.
60    pub const READBACK_MISMATCH: &str = "readback_mismatch";
61    /// A manual exposure time was asked for together with auto exposure.
62    pub const CONFLICTING_REQUEST: &str = "conflicting_request";
63    /// A gain that is negative or not a number.
64    pub const INVALID_VALUE: &str = "invalid_value";
65    /// Nothing to set; the reply still carries the current read-back.
66    pub const EMPTY_REQUEST: &str = "empty_request";
67    /// The camera's control queue is full.
68    pub const BUSY: &str = "busy";
69}
70
71/// `camera.control.set` arguments. An absent field leaves that control as it
72/// is. `exposure_us` implies manual exposure; `gain` is a multiplier (1.0 is
73/// unity gain).
74#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
75pub struct CameraControlRequest {
76    pub camera_id: String,
77    #[serde(default)]
78    pub exposure_us: Option<u32>,
79    #[serde(default)]
80    pub gain: Option<f32>,
81    #[serde(default)]
82    pub auto_exposure: Option<bool>,
83}
84
85/// `camera.control.set` reply. `exposure_us` and `gain` (a multiplier) are the
86/// values the driver reads back (`None` when the camera has no such control,
87/// its gain unit is unknown, or it was not reached), not the requested ones.
88#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
89pub struct CameraControlResult {
90    pub applied: bool,
91    #[serde(default)]
92    pub exposure_us: Option<u32>,
93    #[serde(default)]
94    pub gain: Option<f32>,
95    #[serde(default)]
96    pub reason: Option<String>,
97}
98
99impl CameraControlResult {
100    /// Not applied, for `reason`, with nothing read back.
101    pub fn refused(reason: &str) -> Self {
102        Self {
103            applied: false,
104            exposure_us: None,
105            gain: None,
106            reason: Some(reason.to_string()),
107        }
108    }
109}
110
111/// Decode a msgpack map into `T`; `what` prefixes the error.
112fn decode<T: serde::de::DeserializeOwned>(value: &Value, what: &str) -> Result<T, String> {
113    rmpv::ext::from_value(value.clone()).map_err(|e| format!("{what}: {e}"))
114}
115
116fn map(entries: Vec<(&str, Option<Value>)>) -> Value {
117    Value::Map(
118        entries
119            .into_iter()
120            .filter_map(|(k, v)| v.map(|v| (Value::from(k), v)))
121            .collect(),
122    )
123}
124
125impl CameraControlRequest {
126    /// The request as a msgpack map; absent fields are omitted.
127    pub fn to_args(&self) -> Value {
128        map(vec![
129            ("camera_id", Some(Value::from(self.camera_id.as_str()))),
130            ("exposure_us", self.exposure_us.map(Value::from)),
131            ("gain", self.gain.map(Value::from)),
132            ("auto_exposure", self.auto_exposure.map(Value::from)),
133        ])
134    }
135
136    /// Decode request arguments.
137    pub fn from_args(args: &Value) -> Result<Self, String> {
138        decode(args, "camera.control.set arguments are invalid")
139    }
140}
141
142impl CameraControlResult {
143    /// The reply as a msgpack map; absent fields are omitted.
144    pub fn to_value(&self) -> Value {
145        map(vec![
146            ("applied", Some(Value::from(self.applied))),
147            ("exposure_us", self.exposure_us.map(Value::from)),
148            ("gain", self.gain.map(Value::from)),
149            ("reason", self.reason.as_deref().map(Value::from)),
150        ])
151    }
152
153    /// Decode a reply.
154    pub fn from_value(value: &Value) -> Result<Self, String> {
155        decode(value, "camera.control.set reply did not decode")
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn a_request_round_trips_and_omits_absent_fields() {
165        let req = CameraControlRequest {
166            camera_id: "cam_fl".into(),
167            exposure_us: Some(2_000),
168            gain: Some(4.0),
169            auto_exposure: None,
170        };
171        let args = req.to_args();
172        let Value::Map(entries) = &args else {
173            panic!("a map")
174        };
175        assert!(!entries
176            .iter()
177            .any(|(k, _)| k.as_str() == Some("auto_exposure")));
178        assert_eq!(CameraControlRequest::from_args(&args).unwrap(), req);
179    }
180
181    #[test]
182    fn a_request_without_optional_fields_decodes() {
183        let args = Value::Map(vec![(Value::from("camera_id"), Value::from("c"))]);
184        let req = CameraControlRequest::from_args(&args).unwrap();
185        assert_eq!(req.camera_id, "c");
186        assert_eq!(
187            (req.exposure_us, req.gain, req.auto_exposure),
188            (None, None, None)
189        );
190        // An integer gain, as a dynamically typed client may send, decodes.
191        let args = Value::Map(vec![
192            (Value::from("camera_id"), Value::from("c")),
193            (Value::from("gain"), Value::from(2)),
194        ]);
195        assert_eq!(
196            CameraControlRequest::from_args(&args).unwrap().gain,
197            Some(2.0)
198        );
199        assert!(CameraControlRequest::from_args(&Value::Map(vec![])).is_err());
200    }
201
202    #[test]
203    fn a_result_round_trips() {
204        let applied = CameraControlResult {
205            applied: true,
206            exposure_us: Some(2_000),
207            gain: Some(4.0),
208            reason: None,
209        };
210        assert_eq!(
211            CameraControlResult::from_value(&applied.to_value()).unwrap(),
212            applied
213        );
214        let refused = CameraControlResult::refused(reason::NOT_V4L2);
215        assert_eq!(
216            CameraControlResult::from_value(&refused.to_value()).unwrap(),
217            refused
218        );
219    }
220}