1use rmpv::Value;
32use thiserror::Error;
33
34use crate::framebus::{BoundingBox, Detection, DetectionBatch, FrameDescriptor, ModelMetadata};
35
36pub const VISION_READERS_GROUP: &str = "ados-vision-readers";
40
41pub const OWNER_FIELD: &str = "owner";
43
44pub const UNREGISTER_OWNER: &str = "vision.unregister_owner";
48
49pub fn with_owner(args: &Value, owner: &str) -> Value {
53 let mut entries: Vec<(Value, Value)> = match args {
54 Value::Map(entries) => entries
55 .iter()
56 .filter(|(k, _)| k.as_str() != Some(OWNER_FIELD))
57 .cloned()
58 .collect(),
59 _ => Vec::new(),
60 };
61 entries.push((Value::from(OWNER_FIELD), Value::from(owner)));
62 Value::Map(entries)
63}
64
65pub fn owner_of(args: &Value) -> Result<&str, VisionArgsError> {
67 field(args, OWNER_FIELD)?
68 .as_str()
69 .filter(|s| !s.is_empty())
70 .ok_or_else(|| decode_err(OWNER_FIELD, "not a non-empty string"))
71}
72
73pub fn unregister_owner_args(owner: &str) -> Value {
75 map(vec![(OWNER_FIELD, Value::from(owner))])
76}
77
78pub fn owned_model_id(owner: &str, id: &str) -> String {
83 match id
84 .strip_prefix(owner)
85 .and_then(|rest| rest.strip_prefix('/'))
86 {
87 Some(_) => id.to_string(),
88 None => format!("{owner}/{id}"),
89 }
90}
91
92#[derive(Debug, Error)]
94pub enum VisionArgsError {
95 #[error("vision args are not a map")]
96 NotAMap,
97 #[error("missing `{0}`")]
98 Missing(&'static str),
99 #[error("`{field}` does not decode: {reason}")]
100 Decode { field: &'static str, reason: String },
101 #[error("`{field}` does not encode: {reason}")]
102 Encode { field: &'static str, reason: String },
103}
104
105pub fn publish_detection_args(batch: &DetectionBatch) -> Result<Value, VisionArgsError> {
107 Ok(map(vec![("batch", blob("batch", batch.to_msgpack())?)]))
108}
109
110pub fn decode_publish_detection(args: &Value) -> Result<DetectionBatch, VisionArgsError> {
113 DetectionBatch::from_msgpack(binary(args, "batch")?).map_err(|e| decode_err("batch", e))
114}
115
116pub fn register_model_args(model: &ModelMetadata) -> Result<Value, VisionArgsError> {
118 Ok(map(vec![("model", blob("model", model.to_msgpack())?)]))
119}
120
121pub fn decode_register_model(args: &Value) -> Result<ModelMetadata, VisionArgsError> {
123 ModelMetadata::from_msgpack(binary(args, "model")?).map_err(|e| decode_err("model", e))
124}
125
126#[derive(Debug, Clone, PartialEq)]
129pub struct InferRequest {
130 pub model_id: String,
131 pub frame: FrameDescriptor,
132}
133
134pub fn infer_args(model_id: &str, frame: &FrameDescriptor) -> Result<Value, VisionArgsError> {
137 Ok(map(vec![
138 ("model_id", Value::from(model_id)),
139 ("descriptor", blob("descriptor", frame.to_msgpack())?),
140 ]))
141}
142
143pub fn decode_infer(args: &Value) -> Result<InferRequest, VisionArgsError> {
146 let model_id = field(args, "model_id")?
147 .as_str()
148 .ok_or_else(|| decode_err("model_id", "not a string"))?
149 .to_string();
150 let frame = FrameDescriptor::from_msgpack(binary(args, "descriptor")?)
151 .map_err(|e| decode_err("descriptor", e))?;
152 Ok(InferRequest { model_id, frame })
153}
154
155pub fn infer_reply(batch: &DetectionBatch) -> Result<Value, VisionArgsError> {
157 publish_detection_args(batch)
158}
159
160pub fn decode_infer_reply(args: &Value) -> Result<DetectionBatch, VisionArgsError> {
162 decode_publish_detection(args)
163}
164
165#[derive(Debug, Clone, PartialEq)]
168pub struct DesignateTrack {
169 pub camera_id: String,
170 pub target: Detection,
171}
172
173pub fn designate_track_args(camera_id: &str, target: &Detection) -> Result<Value, VisionArgsError> {
176 let bbox = target.bbox.ok_or(VisionArgsError::Missing("bbox"))?;
177 Ok(map(vec![
178 ("camera_id", Value::from(camera_id)),
179 (
180 "bbox",
181 map(vec![
182 ("x", Value::from(f64::from(bbox.x))),
183 ("y", Value::from(f64::from(bbox.y))),
184 ("width", Value::from(f64::from(bbox.width))),
185 ("height", Value::from(f64::from(bbox.height))),
186 ]),
187 ),
188 ("class_label", Value::from(target.class_label.as_str())),
189 ("confidence", Value::from(f64::from(target.confidence))),
190 ]))
191}
192
193pub fn decode_designate_track(args: &Value) -> Result<DesignateTrack, VisionArgsError> {
200 let camera_id = field(args, "camera_id")?
201 .as_str()
202 .ok_or_else(|| decode_err("camera_id", "not a string"))?
203 .to_string();
204 let bbox = field(args, "bbox")?;
205 if !bbox.is_map() {
206 return Err(decode_err("bbox", "not a map"));
207 }
208 let side = |k: &'static str| -> Result<f32, VisionArgsError> {
209 let v = field(bbox, k)
210 .ok()
211 .and_then(number)
212 .ok_or_else(|| decode_err("bbox", format!("`{k}` is missing or not a number")))?;
213 if v.is_finite() {
214 Ok(v)
215 } else {
216 Err(decode_err("bbox", format!("`{k}` is not finite")))
217 }
218 };
219 let (x, y, width, height) = (side("x")?, side("y")?, side("width")?, side("height")?);
220 if x < 0.0 || y < 0.0 {
221 return Err(decode_err("bbox", "x and y must be non-negative"));
222 }
223 if width <= 0.0 || height <= 0.0 {
224 return Err(decode_err("bbox", "width and height must be positive"));
225 }
226 let confidence = field(args, "confidence")
227 .ok()
228 .and_then(number)
229 .filter(|c| c.is_finite())
230 .unwrap_or(1.0)
231 .clamp(0.0, 1.0);
232 let target = Detection {
233 bbox: Some(BoundingBox {
234 x,
235 y,
236 width,
237 height,
238 }),
239 class_label: field(args, "class_label")
240 .ok()
241 .and_then(Value::as_str)
242 .unwrap_or_default()
243 .to_string(),
244 confidence,
245 track_id: None,
246 assoc_confidence: None,
247 lock_state: None,
248 attributes: None,
249 mask: None,
250 keypoints: None,
251 depth: None,
252 world_pos: None,
253 };
254 Ok(DesignateTrack { camera_id, target })
255}
256
257fn map(pairs: Vec<(&str, Value)>) -> Value {
258 Value::Map(
259 pairs
260 .into_iter()
261 .map(|(k, v)| (Value::from(k), v))
262 .collect(),
263 )
264}
265
266fn blob(
267 field: &'static str,
268 encoded: Result<Vec<u8>, rmp_serde::encode::Error>,
269) -> Result<Value, VisionArgsError> {
270 encoded
271 .map(Value::Binary)
272 .map_err(|e| VisionArgsError::Encode {
273 field,
274 reason: e.to_string(),
275 })
276}
277
278fn field<'a>(args: &'a Value, key: &'static str) -> Result<&'a Value, VisionArgsError> {
279 let Value::Map(entries) = args else {
280 return Err(VisionArgsError::NotAMap);
281 };
282 entries
283 .iter()
284 .find(|(k, _)| k.as_str() == Some(key))
285 .map(|(_, v)| v)
286 .ok_or(VisionArgsError::Missing(key))
287}
288
289fn binary<'a>(args: &'a Value, key: &'static str) -> Result<&'a [u8], VisionArgsError> {
290 match field(args, key)? {
291 Value::Binary(bytes) => Ok(bytes),
292 _ => Err(decode_err(key, "not binary")),
293 }
294}
295
296fn decode_err(field: &'static str, reason: impl ToString) -> VisionArgsError {
297 VisionArgsError::Decode {
298 field,
299 reason: reason.to_string(),
300 }
301}
302
303fn number(v: &Value) -> Option<f32> {
305 v.as_f64()
306 .or_else(|| v.as_i64().map(|i| i as f64))
307 .or_else(|| v.as_u64().map(|u| u as f64))
308 .map(|f| f as f32)
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use crate::framebus::VISION_DETECTION_VERSION;
315
316 #[test]
317 fn a_batch_of_an_unknown_version_is_refused_at_decode() {
318 let batch = DetectionBatch {
319 v: VISION_DETECTION_VERSION + 1,
320 model_id: "m".into(),
321 camera_id: "c".into(),
322 frame_id: 1,
323 ts_ms: 0,
324 frame_width: 0,
325 frame_height: 0,
326 detections: vec![],
327 };
328 let args = publish_detection_args(&batch).unwrap();
329 assert!(matches!(
330 decode_publish_detection(&args),
331 Err(VisionArgsError::Decode { field: "batch", .. })
332 ));
333 }
334
335 #[test]
336 fn designate_reads_integer_boxes_and_defaults_label_and_confidence() {
337 let args = map(vec![
340 ("camera_id", Value::from("cam-0")),
341 (
342 "bbox",
343 map(vec![
344 ("x", Value::from(10)),
345 ("y", Value::from(20u64)),
346 ("width", Value::from(30.5)),
347 ("height", Value::from(40)),
348 ]),
349 ),
350 ]);
351 let d = decode_designate_track(&args).unwrap();
352 assert_eq!(d.camera_id, "cam-0");
353 assert_eq!(
354 d.target.bbox,
355 Some(BoundingBox {
356 x: 10.0,
357 y: 20.0,
358 width: 30.5,
359 height: 40.0
360 })
361 );
362 assert_eq!(d.target.class_label, "");
363 assert_eq!(d.target.confidence, 1.0);
364 }
365
366 #[test]
367 fn designate_without_a_box_is_refused_on_both_ends() {
368 let args = map(vec![("camera_id", Value::from("cam-0"))]);
369 assert!(matches!(
370 decode_designate_track(&args),
371 Err(VisionArgsError::Missing("bbox"))
372 ));
373 let boxless = Detection {
374 bbox: None,
375 class_label: "person".into(),
376 confidence: 0.9,
377 track_id: None,
378 assoc_confidence: None,
379 lock_state: None,
380 attributes: None,
381 mask: None,
382 keypoints: None,
383 depth: None,
384 world_pos: None,
385 };
386 assert!(matches!(
387 designate_track_args("cam-0", &boxless),
388 Err(VisionArgsError::Missing("bbox"))
389 ));
390 }
391
392 #[test]
393 fn designate_refuses_non_finite_and_empty_boxes_and_clamps_confidence() {
394 let with = |x: f64, w: f64, conf: f64| {
395 map(vec![
396 ("camera_id", Value::from("cam-0")),
397 (
398 "bbox",
399 map(vec![
400 ("x", Value::from(x)),
401 ("y", Value::from(1.0)),
402 ("width", Value::from(w)),
403 ("height", Value::from(5.0)),
404 ]),
405 ),
406 ("confidence", Value::from(conf)),
407 ])
408 };
409 for bad in [
410 with(f64::NAN, 5.0, 0.5),
411 with(f64::INFINITY, 5.0, 0.5),
412 with(-1.0, 5.0, 0.5),
413 with(1.0, 0.0, 0.5),
414 with(1.0, -3.0, 0.5),
415 ] {
416 assert!(matches!(
417 decode_designate_track(&bad),
418 Err(VisionArgsError::Decode { field: "bbox", .. })
419 ));
420 }
421 let d = decode_designate_track(&with(1.0, 5.0, 7.0)).unwrap();
422 assert_eq!(d.target.confidence, 1.0);
423 let d = decode_designate_track(&with(1.0, 5.0, -2.0)).unwrap();
424 assert_eq!(d.target.confidence, 0.0);
425 }
426
427 #[test]
428 fn the_stamped_owner_replaces_whatever_the_plugin_sent() {
429 let spoofed = map(vec![
430 ("model_id", Value::from("det")),
431 (OWNER_FIELD, Value::from("com.example.victim")),
432 ]);
433 let stamped = with_owner(&spoofed, "com.example.caller");
434 assert_eq!(owner_of(&stamped).unwrap(), "com.example.caller");
435 let Value::Map(entries) = &stamped else {
436 panic!("not a map");
437 };
438 assert_eq!(
439 entries
440 .iter()
441 .filter(|(k, _)| k.as_str() == Some(OWNER_FIELD))
442 .count(),
443 1
444 );
445 assert_eq!(field(&stamped, "model_id").unwrap().as_str(), Some("det"));
446 assert!(owner_of(&map(vec![(OWNER_FIELD, Value::from(""))])).is_err());
447 }
448}