1use std::collections::BTreeMap;
29use std::sync::atomic::{compiler_fence, fence, Ordering};
30
31use serde::{Deserialize, Serialize};
32use thiserror::Error;
33
34mod seqlock;
35use seqlock::{load_seq_volatile, store_seq_volatile};
36
37pub const VISION_FRAME_TOPIC: &str = "vision.frame";
40
41pub const MAX_SLOT_COUNT: u32 = u16::MAX as u32;
48
49pub const VISION_DETECTION_TOPIC: &str = "vision.detection";
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "lowercase")]
56pub enum FrameFormat {
57 Rgb24,
59 Nv12,
61 Yuv420p,
63 Gray8,
65 Gray16,
67 BayerRggb8,
69 BayerRggb16,
71}
72
73impl FrameFormat {
74 pub fn frame_bytes(self, width: u32, height: u32) -> usize {
79 let px = width as usize * height as usize;
80 match self {
81 FrameFormat::Rgb24 => px * 3,
82 FrameFormat::Nv12 | FrameFormat::Yuv420p => px + px / 2,
84 FrameFormat::Gray8 | FrameFormat::BayerRggb8 => px,
85 FrameFormat::Gray16 | FrameFormat::BayerRggb16 => px * 2,
86 }
87 }
88
89 pub fn ffmpeg_pix_fmt(self) -> &'static str {
91 match self {
92 FrameFormat::Rgb24 => "rgb24",
93 FrameFormat::Nv12 => "nv12",
94 FrameFormat::Yuv420p => "yuv420p",
95 FrameFormat::Gray8 => "gray",
96 FrameFormat::Gray16 => "gray16le",
97 FrameFormat::BayerRggb8 => "bayer_rggb8",
98 FrameFormat::BayerRggb16 => "bayer_rggb16le",
99 }
100 }
101
102 pub fn is_bayer(self) -> bool {
104 matches!(self, FrameFormat::BayerRggb8 | FrameFormat::BayerRggb16)
105 }
106}
107
108pub const FRAMEBUS_DESCRIPTOR_VERSION: u16 = 2;
114
115pub const TIMEBASE_HOST: u8 = 0;
117pub const TIMEBASE_SENSOR: u8 = 1;
119pub const TIMEBASE_SIM: u8 = 2;
121
122mod clock;
123pub use clock::mono_ns;
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129pub struct FrameDescriptor {
130 #[serde(rename = "v")]
135 pub v: u16,
136 pub camera_id: String,
139 pub frame_id: u64,
141 pub ts_ns: u64,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub ts_sensor_ns: Option<u64>,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub exposure_ns: Option<u32>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub trigger_seq: Option<u64>,
152 pub timebase: u8,
154 pub width: u32,
155 pub height: u32,
156 pub format: FrameFormat,
157 pub shm_name: String,
159 pub slot: u32,
161 pub seq: u64,
165 pub byte_len: u32,
167}
168
169impl FrameDescriptor {
170 pub fn to_msgpack(&self) -> Result<Vec<u8>, rmp_serde::encode::Error> {
172 rmp_serde::to_vec_named(self)
173 }
174
175 pub fn from_msgpack(bytes: &[u8]) -> Result<Self, DescriptorError> {
179 let desc: FrameDescriptor = rmp_serde::from_slice(bytes)?;
180 if desc.v != FRAMEBUS_DESCRIPTOR_VERSION {
181 return Err(DescriptorError::Version {
182 got: desc.v,
183 ours: FRAMEBUS_DESCRIPTOR_VERSION,
184 });
185 }
186 Ok(desc)
187 }
188}
189
190#[derive(Debug, Error)]
192pub enum DescriptorError {
193 #[error("msgpack decode error: {0}")]
194 Decode(#[from] rmp_serde::decode::Error),
195 #[error("unsupported vision.frame descriptor version {got} (this build speaks {ours})")]
196 Version { got: u16, ours: u16 },
197}
198
199#[derive(Debug, Error, PartialEq, Eq)]
200pub enum RingError {
201 #[error("slot {slot} out of range (ring has {slot_count} slots)")]
202 SlotOutOfRange { slot: u32, slot_count: u32 },
203 #[error("payload of {len} bytes exceeds slot capacity {cap}")]
204 PayloadTooLarge { len: usize, cap: usize },
205 #[error("shared region is {got} bytes, layout needs {need}")]
206 RegionTooSmall { got: usize, need: usize },
207 #[error("slot_count {slot_count} exceeds the header maximum {max}")]
208 SlotCountTooLarge { slot_count: u32, max: u32 },
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub struct RingLayout {
230 pub slot_count: u32,
231 pub slot_bytes: u32,
233}
234
235impl RingLayout {
236 pub const MAGIC: u32 = 0x4144_5631;
238 pub const VERSION: u16 = 1;
239 pub const HEADER_LEN: usize = 16;
240 pub const SLOT_HEADER_LEN: usize = 16;
242 pub const SLOT_TRAILER_LEN: usize = 8;
244
245 pub fn for_frame(slot_count: u32, width: u32, height: u32, format: FrameFormat) -> Self {
247 RingLayout {
248 slot_count,
249 slot_bytes: format.frame_bytes(width, height) as u32,
250 }
251 }
252
253 pub fn slot_stride(&self) -> usize {
254 Self::SLOT_HEADER_LEN + self.slot_bytes as usize + Self::SLOT_TRAILER_LEN
255 }
256
257 pub fn total_len(&self) -> usize {
259 Self::HEADER_LEN + self.slot_count as usize * self.slot_stride()
260 }
261
262 fn slot_offset(&self, slot: u32) -> usize {
263 Self::HEADER_LEN + slot as usize * self.slot_stride()
264 }
265
266 pub fn validate(&self) -> Result<(), RingError> {
273 if self.slot_count > MAX_SLOT_COUNT {
274 return Err(RingError::SlotCountTooLarge {
275 slot_count: self.slot_count,
276 max: MAX_SLOT_COUNT,
277 });
278 }
279 Ok(())
280 }
281
282 pub fn write_header(&self, region: &mut [u8]) -> Result<(), RingError> {
287 self.validate()?;
288 self.check_region(region.len())?;
289 region[0..4].copy_from_slice(&Self::MAGIC.to_le_bytes());
290 region[4..6].copy_from_slice(&Self::VERSION.to_le_bytes());
291 region[6..8].copy_from_slice(&(self.slot_count as u16).to_le_bytes());
292 region[8..12].copy_from_slice(&self.slot_bytes.to_le_bytes());
293 region[12..16].copy_from_slice(&0u32.to_le_bytes());
294 Ok(())
295 }
296
297 pub fn read_header(region: &[u8]) -> Option<RingLayout> {
299 if region.len() < Self::HEADER_LEN {
300 return None;
301 }
302 let magic = u32::from_le_bytes(region[0..4].try_into().unwrap());
303 let version = u16::from_le_bytes(region[4..6].try_into().unwrap());
304 if magic != Self::MAGIC || version != Self::VERSION {
305 return None;
306 }
307 let slot_count = u16::from_le_bytes(region[6..8].try_into().unwrap()) as u32;
308 let slot_bytes = u32::from_le_bytes(region[8..12].try_into().unwrap());
309 Some(RingLayout {
310 slot_count,
311 slot_bytes,
312 })
313 }
314
315 fn check_region(&self, got: usize) -> Result<(), RingError> {
316 let need = self.total_len();
317 if got < need {
318 return Err(RingError::RegionTooSmall { got, need });
319 }
320 Ok(())
321 }
322}
323
324pub fn write_slot(
329 region: &mut [u8],
330 layout: &RingLayout,
331 slot: u32,
332 seq: u64,
333 data: &[u8],
334) -> Result<(), RingError> {
335 if slot >= layout.slot_count {
336 return Err(RingError::SlotOutOfRange {
337 slot,
338 slot_count: layout.slot_count,
339 });
340 }
341 let cap = layout.slot_bytes as usize;
342 if data.len() > cap {
343 return Err(RingError::PayloadTooLarge {
344 len: data.len(),
345 cap,
346 });
347 }
348 layout.check_region(region.len())?;
349
350 let base = layout.slot_offset(slot);
351 let data_off = base + RingLayout::SLOT_HEADER_LEN;
352 let trailer_off = data_off + cap;
353
354 store_seq_volatile(region, base, seq);
365 region[base + 8..base + 12].copy_from_slice(&(data.len() as u32).to_le_bytes());
366 region[base + 12..base + 16].copy_from_slice(&0u32.to_le_bytes());
367 fence(Ordering::Release);
368 region[data_off..data_off + data.len()].copy_from_slice(data);
369 fence(Ordering::Release);
370 store_seq_volatile(region, trailer_off, seq);
371 Ok(())
372}
373
374pub fn read_slot_into(
381 region: &[u8],
382 layout: &RingLayout,
383 slot: u32,
384 expected_seq: u64,
385 out: &mut Vec<u8>,
386) -> Result<Option<usize>, RingError> {
387 if slot >= layout.slot_count {
388 return Err(RingError::SlotOutOfRange {
389 slot,
390 slot_count: layout.slot_count,
391 });
392 }
393 layout.check_region(region.len())?;
394
395 let cap = layout.slot_bytes as usize;
396 let base = layout.slot_offset(slot);
397 let data_off = base + RingLayout::SLOT_HEADER_LEN;
398 let trailer_off = data_off + cap;
399
400 let seq_end = load_seq_volatile(region, trailer_off);
408 if seq_end != expected_seq {
409 return Ok(None);
410 }
411 let byte_len = u32::from_le_bytes(region[base + 8..base + 12].try_into().unwrap()) as usize;
412 if byte_len > cap {
413 return Ok(None);
414 }
415 fence(Ordering::Acquire);
416 compiler_fence(Ordering::Acquire);
419 out.clear();
420 out.extend_from_slice(®ion[data_off..data_off + byte_len]);
421 fence(Ordering::Acquire);
422 let seq_begin = load_seq_volatile(region, base);
423 let seq_end2 = load_seq_volatile(region, trailer_off);
424 if seq_begin != expected_seq || seq_end2 != expected_seq {
425 return Ok(None);
426 }
427 Ok(Some(byte_len))
428}
429
430pub mod methods;
444
445#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
447#[serde(rename_all = "lowercase")]
448pub enum ModelKind {
449 Detection,
450 Segmentation,
451 Classification,
452 Tracking,
453 Pose,
456 Depth,
459 Reid,
462}
463
464#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
469pub struct ModelInfo {
470 pub id: String,
471 pub kind: ModelKind,
472 pub execution: ModelExecution,
473 pub backend_loaded: bool,
476 pub output_classes: Vec<String>,
477 #[serde(default)]
482 pub fps: f32,
483 #[serde(default, rename = "latencyMs")]
485 pub latency_ms: f32,
486 #[serde(default, rename = "isInferenceCapable")]
491 pub is_inference_capable: bool,
492}
493
494#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
496#[serde(rename_all = "snake_case")]
497pub enum ModelExecution {
498 EngineRun,
501 PluginSide,
503}
504
505#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
514#[serde(rename_all = "lowercase")]
515pub enum DetectionHead {
516 #[default]
517 Yolo8,
518 Yolo5,
519}
520
521#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
523pub struct ModelMetadata {
524 pub id: String,
526 pub kind: ModelKind,
527 pub execution: ModelExecution,
528 pub input_width: u32,
530 pub input_height: u32,
531 pub input_format: FrameFormat,
532 #[serde(default)]
534 pub output_classes: Vec<String>,
535 #[serde(default)]
537 pub model_path: Option<String>,
538 #[serde(default)]
542 pub head: DetectionHead,
543}
544
545impl ModelMetadata {
546 pub fn to_msgpack(&self) -> Result<Vec<u8>, rmp_serde::encode::Error> {
547 rmp_serde::to_vec_named(self)
548 }
549 pub fn from_msgpack(bytes: &[u8]) -> Result<Self, rmp_serde::decode::Error> {
550 rmp_serde::from_slice(bytes)
551 }
552}
553
554#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
556pub struct BoundingBox {
557 pub x: f32,
558 pub y: f32,
559 pub width: f32,
560 pub height: f32,
561}
562
563#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
566pub struct Keypoint {
567 pub x: f32,
568 pub y: f32,
569 pub confidence: f32,
571}
572
573#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
582#[serde(rename_all = "lowercase")]
583pub enum LockState {
584 Locked,
585 Uncertain,
586 Lost,
587}
588
589#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
597pub struct Detection {
598 #[serde(default, skip_serializing_if = "Option::is_none")]
603 pub bbox: Option<BoundingBox>,
604 pub class_label: String,
605 pub confidence: f32,
606 #[serde(default)]
608 pub track_id: Option<u64>,
609 #[serde(default)]
614 pub assoc_confidence: Option<f32>,
615 #[serde(default)]
618 pub lock_state: Option<LockState>,
619 #[serde(default, skip_serializing_if = "Option::is_none")]
627 pub attributes: Option<BTreeMap<String, rmpv::Value>>,
628 #[serde(default, skip_serializing_if = "Option::is_none")]
632 pub mask: Option<Vec<[f32; 2]>>,
633 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub keypoints: Option<Vec<Keypoint>>,
637 #[serde(default, skip_serializing_if = "Option::is_none")]
640 pub depth: Option<f32>,
641 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub world_pos: Option<[f64; 3]>,
647}
648
649pub const VISION_DETECTION_VERSION: u16 = 3;
655
656#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
659pub struct DetectionBatch {
660 #[serde(rename = "v")]
665 pub v: u16,
666 pub model_id: String,
667 pub camera_id: String,
668 pub frame_id: u64,
669 pub ts_ms: i64,
672 #[serde(default)]
677 pub frame_width: u32,
678 #[serde(default)]
679 pub frame_height: u32,
680 pub detections: Vec<Detection>,
681}
682
683impl DetectionBatch {
684 pub fn to_msgpack(&self) -> Result<Vec<u8>, rmp_serde::encode::Error> {
685 rmp_serde::to_vec_named(self)
686 }
687
688 pub fn from_msgpack(bytes: &[u8]) -> Result<Self, DetectionBatchError> {
692 let batch: DetectionBatch = rmp_serde::from_slice(bytes)?;
693 if batch.v != VISION_DETECTION_VERSION {
694 return Err(DetectionBatchError::Version {
695 got: batch.v,
696 ours: VISION_DETECTION_VERSION,
697 });
698 }
699 Ok(batch)
700 }
701}
702
703#[derive(Debug, Error)]
705pub enum DetectionBatchError {
706 #[error("msgpack decode error: {0}")]
707 Decode(#[from] rmp_serde::decode::Error),
708 #[error("unsupported vision.detection version {got} (this build speaks {ours})")]
709 Version { got: u16, ours: u16 },
710}
711
712#[cfg(test)]
713mod contract_tests {
714 use super::*;
715
716 #[test]
717 fn model_metadata_round_trips() {
718 let m = ModelMetadata {
719 id: "com.example.weeds".into(),
720 kind: ModelKind::Detection,
721 execution: ModelExecution::EngineRun,
722 input_width: 640,
723 input_height: 480,
724 input_format: FrameFormat::Rgb24,
725 output_classes: vec!["weed".into(), "crop".into()],
726 model_path: Some("/opt/ados/models/vision/weeds.onnx".into()),
727 head: DetectionHead::Yolo8,
728 };
729 let bytes = m.to_msgpack().unwrap();
730 assert_eq!(ModelMetadata::from_msgpack(&bytes).unwrap(), m);
731 }
732
733 #[test]
734 fn model_metadata_without_head_defaults_to_yolo8() {
735 let legacy = ModelMetadataLegacy {
738 id: "com.example.old".into(),
739 kind: ModelKind::Detection,
740 execution: ModelExecution::EngineRun,
741 input_width: 640,
742 input_height: 640,
743 input_format: FrameFormat::Rgb24,
744 output_classes: vec!["uav".into()],
745 model_path: None,
746 };
747 let bytes = rmp_serde::to_vec_named(&legacy).unwrap();
748 let decoded = ModelMetadata::from_msgpack(&bytes).unwrap();
749 assert_eq!(decoded.head, DetectionHead::Yolo8);
750 assert_eq!(decoded.id, "com.example.old");
751 }
752
753 #[derive(Serialize)]
755 struct ModelMetadataLegacy {
756 id: String,
757 kind: ModelKind,
758 execution: ModelExecution,
759 input_width: u32,
760 input_height: u32,
761 input_format: FrameFormat,
762 output_classes: Vec<String>,
763 model_path: Option<String>,
764 }
765
766 #[test]
767 fn detection_head_serializes_lowercase() {
768 assert_eq!(
769 serde_json::to_string(&DetectionHead::Yolo8).unwrap(),
770 "\"yolo8\""
771 );
772 assert_eq!(
773 serde_json::to_string(&DetectionHead::Yolo5).unwrap(),
774 "\"yolo5\""
775 );
776 }
777
778 #[test]
779 fn detection_batch_round_trips() {
780 let b = DetectionBatch {
781 v: VISION_DETECTION_VERSION,
782 model_id: "com.example.weeds".into(),
783 camera_id: "uvc-0".into(),
784 frame_id: 7,
785 ts_ms: 1_700_000_000_000,
786 frame_width: 640,
787 frame_height: 480,
788 detections: vec![Detection {
789 bbox: Some(BoundingBox {
790 x: 12.0,
791 y: 20.0,
792 width: 64.0,
793 height: 32.0,
794 }),
795 class_label: "weed".into(),
796 confidence: 0.87,
797 track_id: None,
798 assoc_confidence: None,
799 lock_state: None,
800 attributes: None,
801 mask: None,
802 keypoints: None,
803 depth: None,
804 world_pos: None,
805 }],
806 };
807 let bytes = b.to_msgpack().unwrap();
808 assert_eq!(DetectionBatch::from_msgpack(&bytes).unwrap(), b);
809 }
810
811 #[test]
812 fn detection_version_matches_registry() {
813 assert_eq!(
814 VISION_DETECTION_VERSION,
815 crate::contracts::contract_version("vision.detection").unwrap()
816 );
817 }
818
819 #[test]
820 fn detection_rejects_a_future_version() {
821 let batch = DetectionBatch {
824 v: VISION_DETECTION_VERSION + 1,
825 model_id: "m".into(),
826 camera_id: "c".into(),
827 frame_id: 1,
828 ts_ms: 0,
829 frame_width: 640,
830 frame_height: 480,
831 detections: vec![],
832 };
833 let bytes = batch.to_msgpack().unwrap();
834 assert!(matches!(
835 DetectionBatch::from_msgpack(&bytes),
836 Err(DetectionBatchError::Version { .. })
837 ));
838 }
839
840 #[test]
841 fn detection_missing_version_fails_decode() {
842 #[derive(Serialize)]
845 struct NoVersionBatch {
846 model_id: String,
847 camera_id: String,
848 frame_id: u64,
849 ts_ms: i64,
850 detections: Vec<Detection>,
851 }
852 let old = NoVersionBatch {
853 model_id: "m".into(),
854 camera_id: "c".into(),
855 frame_id: 1,
856 ts_ms: 0,
857 detections: vec![],
858 };
859 let bytes = rmp_serde::to_vec_named(&old).unwrap();
860 assert!(matches!(
861 DetectionBatch::from_msgpack(&bytes),
862 Err(DetectionBatchError::Decode(_))
863 ));
864 }
865
866 #[test]
867 fn detection_lock_fields_round_trip_present() {
868 let d = Detection {
869 bbox: Some(BoundingBox {
870 x: 1.0,
871 y: 2.0,
872 width: 10.0,
873 height: 20.0,
874 }),
875 class_label: "target".into(),
876 confidence: 0.91,
877 track_id: Some(42),
878 assoc_confidence: Some(0.73),
879 lock_state: Some(LockState::Uncertain),
880 attributes: None,
881 mask: None,
882 keypoints: None,
883 depth: None,
884 world_pos: None,
885 };
886 let bytes = rmp_serde::to_vec_named(&d).unwrap();
887 let back: Detection = rmp_serde::from_slice(&bytes).unwrap();
888 assert_eq!(back, d);
889 }
890
891 #[test]
892 fn detection_lock_state_serializes_lowercase() {
893 let s = rmp_serde::to_vec_named(&LockState::Locked).unwrap();
894 assert_eq!(rmp_serde::from_slice::<String>(&s).unwrap(), "locked");
895 let s = rmp_serde::to_vec_named(&LockState::Lost).unwrap();
896 assert_eq!(rmp_serde::from_slice::<String>(&s).unwrap(), "lost");
897 }
898
899 #[test]
900 fn detection_attributes_round_trip() {
901 let mut attrs = BTreeMap::new();
903 attrs.insert("depth_m".to_string(), rmpv::Value::from(4.2_f64));
904 attrs.insert("source".to_string(), rmpv::Value::from("plugin"));
905 let d = Detection {
906 bbox: Some(BoundingBox {
907 x: 1.0,
908 y: 2.0,
909 width: 3.0,
910 height: 4.0,
911 }),
912 class_label: "target".into(),
913 confidence: 0.9,
914 track_id: Some(7),
915 assoc_confidence: None,
916 lock_state: Some(LockState::Locked),
917 attributes: Some(attrs.clone()),
918 mask: None,
919 keypoints: None,
920 depth: None,
921 world_pos: None,
922 };
923 let bytes = rmp_serde::to_vec_named(&d).unwrap();
924 let back: Detection = rmp_serde::from_slice(&bytes).unwrap();
925 assert_eq!(back, d);
926 assert_eq!(back.attributes, Some(attrs));
927 }
928
929 #[test]
930 fn detection_without_attributes_defaults_none() {
931 #[derive(Serialize)]
935 struct NoAttrsDetection {
936 bbox: BoundingBox,
937 class_label: String,
938 confidence: f32,
939 track_id: Option<u64>,
940 assoc_confidence: Option<f32>,
941 lock_state: Option<LockState>,
942 }
943 let old = NoAttrsDetection {
944 bbox: BoundingBox {
945 x: 0.0,
946 y: 0.0,
947 width: 5.0,
948 height: 5.0,
949 },
950 class_label: "target".into(),
951 confidence: 0.5,
952 track_id: None,
953 assoc_confidence: None,
954 lock_state: None,
955 };
956 let bytes = rmp_serde::to_vec_named(&old).unwrap();
957 let back: Detection = rmp_serde::from_slice(&bytes).unwrap();
958 assert_eq!(back.attributes, None);
959 }
960
961 #[test]
962 fn detection_new_fields_default_when_absent() {
963 #[derive(Serialize)]
966 struct OldDetection {
967 bbox: BoundingBox,
968 class_label: String,
969 confidence: f32,
970 track_id: Option<u64>,
971 }
972 let old = OldDetection {
973 bbox: BoundingBox {
974 x: 0.0,
975 y: 0.0,
976 width: 5.0,
977 height: 5.0,
978 },
979 class_label: "weed".into(),
980 confidence: 0.5,
981 track_id: Some(7),
982 };
983 let bytes = rmp_serde::to_vec_named(&old).unwrap();
984 let back: Detection = rmp_serde::from_slice(&bytes).unwrap();
985 assert_eq!(back.track_id, Some(7));
986 assert_eq!(back.assoc_confidence, None);
987 assert_eq!(back.lock_state, None);
988 }
989
990 #[test]
991 fn detection_new_fields_skipped_for_old_readers() {
992 #[derive(Deserialize)]
995 #[allow(dead_code)]
996 struct OldReader {
997 bbox: BoundingBox,
998 class_label: String,
999 confidence: f32,
1000 #[serde(default)]
1001 track_id: Option<u64>,
1002 }
1003 let d = Detection {
1004 bbox: Some(BoundingBox {
1005 x: 1.0,
1006 y: 1.0,
1007 width: 2.0,
1008 height: 2.0,
1009 }),
1010 class_label: "target".into(),
1011 confidence: 0.8,
1012 track_id: Some(3),
1013 assoc_confidence: Some(0.4),
1014 lock_state: Some(LockState::Lost),
1015 attributes: None,
1016 mask: None,
1017 keypoints: None,
1018 depth: None,
1019 world_pos: None,
1020 };
1021 let bytes = rmp_serde::to_vec_named(&d).unwrap();
1022 let back: OldReader = rmp_serde::from_slice(&bytes).unwrap();
1023 assert_eq!(back.track_id, Some(3));
1024 assert_eq!(back.class_label, "target");
1025 }
1026
1027 #[test]
1028 fn enums_serialize_to_expected_strings() {
1029 let k = rmp_serde::to_vec_named(&ModelKind::Tracking).unwrap();
1030 assert_eq!(rmp_serde::from_slice::<String>(&k).unwrap(), "tracking");
1031 let e = rmp_serde::to_vec_named(&ModelExecution::PluginSide).unwrap();
1032 assert_eq!(rmp_serde::from_slice::<String>(&e).unwrap(), "plugin_side");
1033 }
1034
1035 #[test]
1036 fn model_kind_new_variants_serialize_lowercase() {
1037 for (kind, want) in [
1038 (ModelKind::Pose, "pose"),
1039 (ModelKind::Depth, "depth"),
1040 (ModelKind::Reid, "reid"),
1041 ] {
1042 let bytes = rmp_serde::to_vec_named(&kind).unwrap();
1043 assert_eq!(rmp_serde::from_slice::<String>(&bytes).unwrap(), want);
1044 }
1045 }
1046
1047 #[test]
1048 fn percept_typed_fields_round_trip() {
1049 let d = Detection {
1052 bbox: Some(BoundingBox {
1053 x: 10.0,
1054 y: 12.0,
1055 width: 30.0,
1056 height: 40.0,
1057 }),
1058 class_label: "person".into(),
1059 confidence: 0.88,
1060 track_id: Some(9),
1061 assoc_confidence: Some(0.7),
1062 lock_state: Some(LockState::Locked),
1063 attributes: None,
1064 mask: Some(vec![[10.0, 12.0], [40.0, 12.0], [40.0, 52.0], [10.0, 52.0]]),
1065 keypoints: Some(vec![
1066 Keypoint {
1067 x: 25.0,
1068 y: 18.0,
1069 confidence: 0.9,
1070 },
1071 Keypoint {
1072 x: 25.0,
1073 y: 40.0,
1074 confidence: 0.6,
1075 },
1076 ]),
1077 depth: Some(4.5),
1078 world_pos: Some([12.5, -3.0, 1.2]),
1079 };
1080 let bytes = rmp_serde::to_vec_named(&d).unwrap();
1081 let back: Detection = rmp_serde::from_slice(&bytes).unwrap();
1082 assert_eq!(back, d);
1083 assert_eq!(back.mask.as_ref().unwrap().len(), 4);
1084 assert_eq!(back.keypoints.as_ref().unwrap().len(), 2);
1085 assert_eq!(back.depth, Some(4.5));
1086 assert_eq!(back.world_pos, Some([12.5, -3.0, 1.2]));
1087 }
1088
1089 #[test]
1090 fn boxless_percept_round_trips() {
1091 let d = Detection {
1094 bbox: None,
1095 class_label: "region".into(),
1096 confidence: 0.5,
1097 track_id: None,
1098 assoc_confidence: None,
1099 lock_state: None,
1100 attributes: None,
1101 mask: Some(vec![[0.0, 0.0], [100.0, 0.0], [50.0, 100.0]]),
1102 keypoints: None,
1103 depth: Some(2.0),
1104 world_pos: None,
1105 };
1106 let bytes = rmp_serde::to_vec_named(&d).unwrap();
1107 let back: Detection = rmp_serde::from_slice(&bytes).unwrap();
1108 assert_eq!(back, d);
1109 assert_eq!(back.bbox, None);
1110 }
1111
1112 #[test]
1113 fn detection_predating_percept_fields_defaults_none() {
1114 #[derive(Serialize)]
1118 struct PrePerceptDetection {
1119 bbox: BoundingBox,
1120 class_label: String,
1121 confidence: f32,
1122 track_id: Option<u64>,
1123 assoc_confidence: Option<f32>,
1124 lock_state: Option<LockState>,
1125 }
1126 let old = PrePerceptDetection {
1127 bbox: BoundingBox {
1128 x: 0.0,
1129 y: 0.0,
1130 width: 5.0,
1131 height: 5.0,
1132 },
1133 class_label: "target".into(),
1134 confidence: 0.6,
1135 track_id: Some(1),
1136 assoc_confidence: None,
1137 lock_state: Some(LockState::Locked),
1138 };
1139 let bytes = rmp_serde::to_vec_named(&old).unwrap();
1140 let back: Detection = rmp_serde::from_slice(&bytes).unwrap();
1141 assert_eq!(
1142 back.bbox,
1143 Some(BoundingBox {
1144 x: 0.0,
1145 y: 0.0,
1146 width: 5.0,
1147 height: 5.0,
1148 })
1149 );
1150 assert_eq!(back.mask, None);
1151 assert_eq!(back.keypoints, None);
1152 assert_eq!(back.depth, None);
1153 assert_eq!(back.world_pos, None);
1154 }
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159 use super::*;
1160
1161 #[test]
1162 fn mono_ns_is_monotonic() {
1163 let a = mono_ns();
1164 let b = mono_ns();
1165 assert!(a > 0 && b >= a);
1166 }
1167
1168 #[test]
1169 fn frame_bytes_matches_format() {
1170 assert_eq!(FrameFormat::Rgb24.frame_bytes(640, 480), 640 * 480 * 3);
1171 assert_eq!(FrameFormat::Nv12.frame_bytes(640, 480), 640 * 480 * 3 / 2);
1172 assert_eq!(
1173 FrameFormat::Yuv420p.frame_bytes(1280, 720),
1174 1280 * 720 * 3 / 2
1175 );
1176 assert_eq!(FrameFormat::Gray8.frame_bytes(640, 480), 640 * 480);
1177 assert_eq!(FrameFormat::Gray16.frame_bytes(640, 480), 640 * 480 * 2);
1178 assert_eq!(FrameFormat::BayerRggb8.frame_bytes(641, 481), 641 * 481);
1179 assert_eq!(
1180 FrameFormat::BayerRggb16.frame_bytes(640, 480),
1181 640 * 480 * 2
1182 );
1183 }
1184
1185 #[test]
1186 fn descriptor_round_trips_through_msgpack() {
1187 let base = FrameDescriptor {
1188 v: FRAMEBUS_DESCRIPTOR_VERSION,
1189 camera_id: "uvc-0".into(),
1190 frame_id: 42,
1191 ts_ns: 1_700_000_000_000_000_000,
1192 ts_sensor_ns: None,
1193 exposure_ns: None,
1194 trigger_seq: None,
1195 timebase: TIMEBASE_HOST,
1196 width: 640,
1197 height: 480,
1198 format: FrameFormat::Rgb24,
1199 shm_name: "ados-vision-uvc-0".into(),
1200 slot: 3,
1201 seq: 1234,
1202 byte_len: (640 * 480 * 3) as u32,
1203 };
1204 let bytes = base.to_msgpack().unwrap();
1205 assert_eq!(FrameDescriptor::from_msgpack(&bytes).unwrap(), base);
1206 for format in [
1207 FrameFormat::Gray8,
1208 FrameFormat::Gray16,
1209 FrameFormat::BayerRggb8,
1210 FrameFormat::BayerRggb16,
1211 ] {
1212 let d = FrameDescriptor {
1213 ts_sensor_ns: Some(123_456_789),
1214 exposure_ns: Some(4_000_000),
1215 trigger_seq: Some(77),
1216 timebase: TIMEBASE_SENSOR,
1217 format,
1218 byte_len: format.frame_bytes(640, 480) as u32,
1219 ..base.clone()
1220 };
1221 let bytes = d.to_msgpack().unwrap();
1222 assert_eq!(FrameDescriptor::from_msgpack(&bytes).unwrap(), d);
1223 }
1224 }
1225
1226 #[test]
1227 fn descriptor_version_matches_registry() {
1228 assert_eq!(
1229 FRAMEBUS_DESCRIPTOR_VERSION,
1230 crate::contracts::contract_version("framebus.descriptor").unwrap()
1231 );
1232 }
1233
1234 #[test]
1235 fn ring_version_matches_registry() {
1236 assert_eq!(
1237 RingLayout::VERSION,
1238 crate::contracts::contract_version("framebus.ring").unwrap()
1239 );
1240 }
1241
1242 #[test]
1243 fn descriptor_rejects_a_future_version() {
1244 let d = FrameDescriptor {
1247 v: FRAMEBUS_DESCRIPTOR_VERSION + 1,
1248 camera_id: "uvc-0".into(),
1249 frame_id: 1,
1250 ts_ns: 0,
1251 ts_sensor_ns: None,
1252 exposure_ns: None,
1253 trigger_seq: None,
1254 timebase: TIMEBASE_HOST,
1255 width: 8,
1256 height: 8,
1257 format: FrameFormat::Rgb24,
1258 shm_name: "ados-vision-uvc-0".into(),
1259 slot: 0,
1260 seq: 1,
1261 byte_len: 192,
1262 };
1263 let bytes = d.to_msgpack().unwrap();
1264 assert!(matches!(
1265 FrameDescriptor::from_msgpack(&bytes),
1266 Err(DescriptorError::Version { .. })
1267 ));
1268 }
1269
1270 #[test]
1271 fn descriptor_v1_with_ts_ms_is_rejected() {
1272 #[derive(Serialize)]
1274 struct V1Descriptor {
1275 v: u16,
1276 camera_id: String,
1277 frame_id: u64,
1278 ts_ms: i64,
1279 width: u32,
1280 height: u32,
1281 format: FrameFormat,
1282 shm_name: String,
1283 slot: u32,
1284 seq: u64,
1285 byte_len: u32,
1286 }
1287 let old = V1Descriptor {
1288 v: 1,
1289 camera_id: "uvc-0".into(),
1290 frame_id: 1,
1291 ts_ms: 0,
1292 width: 8,
1293 height: 8,
1294 format: FrameFormat::Rgb24,
1295 shm_name: "ados-vision-uvc-0".into(),
1296 slot: 0,
1297 seq: 1,
1298 byte_len: 192,
1299 };
1300 let bytes = rmp_serde::to_vec_named(&old).unwrap();
1301 assert!(matches!(
1302 FrameDescriptor::from_msgpack(&bytes),
1303 Err(DescriptorError::Decode(_) | DescriptorError::Version { .. })
1304 ));
1305 }
1306
1307 #[test]
1308 fn format_serializes_lowercase() {
1309 let bytes = rmp_serde::to_vec_named(&FrameFormat::Yuv420p).unwrap();
1311 let back: String = rmp_serde::from_slice(&bytes).unwrap();
1312 assert_eq!(back, "yuv420p");
1313 for (f, name) in [
1314 (FrameFormat::Gray8, "gray8"),
1315 (FrameFormat::Gray16, "gray16"),
1316 (FrameFormat::BayerRggb8, "bayerrggb8"),
1317 (FrameFormat::BayerRggb16, "bayerrggb16"),
1318 ] {
1319 let bytes = rmp_serde::to_vec_named(&f).unwrap();
1320 let back: String = rmp_serde::from_slice(&bytes).unwrap();
1321 assert_eq!(back, name);
1322 }
1323 }
1324
1325 #[test]
1326 fn header_round_trips() {
1327 let layout = RingLayout::for_frame(4, 64, 48, FrameFormat::Rgb24);
1328 let mut region = vec![0u8; layout.total_len()];
1329 layout.write_header(&mut region).unwrap();
1330 let read = RingLayout::read_header(®ion).unwrap();
1331 assert_eq!(read, layout);
1332 }
1333
1334 #[test]
1335 fn write_then_read_returns_the_frame() {
1336 let layout = RingLayout::for_frame(4, 8, 8, FrameFormat::Rgb24); let mut region = vec![0u8; layout.total_len()];
1338 layout.write_header(&mut region).unwrap();
1339
1340 let frame: Vec<u8> = (0..layout.slot_bytes as u8).collect();
1341 let seq = 9u64;
1342 let slot = (seq % layout.slot_count as u64) as u32;
1343 write_slot(&mut region, &layout, slot, seq, &frame).unwrap();
1344
1345 let mut out = Vec::new();
1346 let got = read_slot_into(®ion, &layout, slot, seq, &mut out).unwrap();
1347 assert_eq!(got, Some(frame.len()));
1348 assert_eq!(out, frame);
1349 }
1350
1351 #[test]
1352 fn read_slot_into_reuses_capacity() {
1353 let layout = RingLayout::for_frame(2, 8, 8, FrameFormat::Rgb24);
1354 let mut region = vec![0u8; layout.total_len()];
1355 layout.write_header(&mut region).unwrap();
1356 let mut out = Vec::with_capacity(layout.slot_bytes as usize);
1357 out.extend_from_slice(&[0xAA; 5]);
1358 let (ptr, cap) = (out.as_ptr(), out.capacity());
1359 for seq in 1..=4u64 {
1360 let frame = vec![seq as u8; layout.slot_bytes as usize];
1361 let slot = (seq % 2) as u32;
1362 write_slot(&mut region, &layout, slot, seq, &frame).unwrap();
1363 let n = read_slot_into(®ion, &layout, slot, seq, &mut out).unwrap();
1364 assert_eq!(n, Some(frame.len()));
1365 assert_eq!(out, frame);
1366 assert_eq!(out.as_ptr(), ptr);
1367 assert_eq!(out.capacity(), cap);
1368 }
1369 }
1370
1371 #[test]
1372 fn stale_seq_reads_none() {
1373 let layout = RingLayout::for_frame(2, 4, 4, FrameFormat::Rgb24);
1374 let mut region = vec![0u8; layout.total_len()];
1375 layout.write_header(&mut region).unwrap();
1376 write_slot(&mut region, &layout, 0, 10, &[1, 2, 3]).unwrap();
1377 let mut out = Vec::new();
1379 assert_eq!(
1380 read_slot_into(®ion, &layout, 0, 11, &mut out).unwrap(),
1381 None
1382 );
1383 }
1384
1385 #[test]
1386 fn recycled_slot_is_detected_as_torn() {
1387 let layout = RingLayout::for_frame(1, 4, 4, FrameFormat::Rgb24);
1388 let mut region = vec![0u8; layout.total_len()];
1389 layout.write_header(&mut region).unwrap();
1390 write_slot(&mut region, &layout, 0, 100, &[7, 7, 7, 7]).unwrap();
1391 write_slot(&mut region, &layout, 0, 101, &[8, 8, 8, 8]).unwrap();
1393 let mut out = Vec::new();
1395 assert_eq!(
1396 read_slot_into(®ion, &layout, 0, 100, &mut out).unwrap(),
1397 None
1398 );
1399 assert_eq!(
1400 read_slot_into(®ion, &layout, 0, 101, &mut out).unwrap(),
1401 Some(4)
1402 );
1403 assert_eq!(out, vec![8; 4]);
1404 }
1405
1406 #[test]
1407 fn oversized_payload_rejected() {
1408 let layout = RingLayout::for_frame(2, 2, 2, FrameFormat::Rgb24); let mut region = vec![0u8; layout.total_len()];
1410 layout.write_header(&mut region).unwrap();
1411 let too_big = vec![0u8; layout.slot_bytes as usize + 1];
1412 assert!(matches!(
1413 write_slot(&mut region, &layout, 0, 1, &too_big),
1414 Err(RingError::PayloadTooLarge { .. })
1415 ));
1416 }
1417
1418 #[test]
1419 fn slot_out_of_range_rejected() {
1420 let layout = RingLayout::for_frame(2, 2, 2, FrameFormat::Rgb24);
1421 let mut region = vec![0u8; layout.total_len()];
1422 layout.write_header(&mut region).unwrap();
1423 assert!(matches!(
1424 write_slot(&mut region, &layout, 2, 1, &[0]),
1425 Err(RingError::SlotOutOfRange { .. })
1426 ));
1427 }
1428
1429 #[test]
1430 fn small_region_rejected() {
1431 let layout = RingLayout::for_frame(4, 64, 64, FrameFormat::Rgb24);
1432 let mut tiny = vec![0u8; 8];
1433 assert!(matches!(
1434 layout.write_header(&mut tiny),
1435 Err(RingError::RegionTooSmall { .. })
1436 ));
1437 }
1438
1439 #[test]
1440 fn slot_count_at_the_header_maximum_round_trips() {
1441 let layout = RingLayout {
1444 slot_count: MAX_SLOT_COUNT,
1445 slot_bytes: 4,
1446 };
1447 assert!(layout.validate().is_ok());
1448 let mut header_region = vec![0u8; layout.total_len()];
1451 layout.write_header(&mut header_region).unwrap();
1452 let read = RingLayout::read_header(&header_region).unwrap();
1453 assert_eq!(read.slot_count, MAX_SLOT_COUNT);
1454 }
1455
1456 #[test]
1457 fn slot_count_above_the_header_maximum_is_rejected() {
1458 let layout = RingLayout {
1461 slot_count: MAX_SLOT_COUNT + 1,
1462 slot_bytes: 4,
1463 };
1464 assert!(matches!(
1465 layout.validate(),
1466 Err(RingError::SlotCountTooLarge { .. })
1467 ));
1468 let mut region = vec![0u8; 64];
1469 assert!(matches!(
1470 layout.write_header(&mut region),
1471 Err(RingError::SlotCountTooLarge { .. })
1472 ));
1473 }
1474
1475 #[test]
1476 fn concurrent_writer_and_reader_never_yield_a_torn_frame() {
1477 use std::sync::atomic::{AtomicBool, AtomicU64, Ordering as O};
1484 use std::sync::Arc;
1485
1486 const SLOTS: u32 = 3;
1487 const FRAME: usize = 256;
1488 let layout = RingLayout {
1489 slot_count: SLOTS,
1490 slot_bytes: FRAME as u32,
1491 };
1492 let total = layout.total_len();
1493
1494 struct Shared(*mut u8, usize);
1499 unsafe impl Send for Shared {}
1500 unsafe impl Sync for Shared {}
1501 let mut backing = vec![0u8; total];
1502 layout.write_header(&mut backing).unwrap();
1503 let ptr = backing.as_mut_ptr();
1504 let shared = Arc::new(Shared(ptr, total));
1505
1506 let latest = Arc::new(AtomicU64::new(0));
1508 let stop = Arc::new(AtomicBool::new(false));
1509
1510 let w_shared = shared.clone();
1511 let w_latest = latest.clone();
1512 let w_stop = stop.clone();
1513 let writer = std::thread::spawn(move || {
1514 let region = unsafe { std::slice::from_raw_parts_mut(w_shared.0, w_shared.1) };
1518 let mut seq = 1u64;
1519 while !w_stop.load(O::Relaxed) {
1520 let marker = (seq & 0xFF) as u8;
1521 let frame = vec![marker; FRAME];
1522 let slot = (seq % SLOTS as u64) as u32;
1523 write_slot(region, &layout, slot, seq, &frame).unwrap();
1524 w_latest.store(seq, O::Release);
1525 seq += 1;
1526 }
1527 });
1528
1529 let r_shared = shared.clone();
1530 let r_latest = latest.clone();
1531 let mut reads = 0u64;
1532 let mut iters = 0u64;
1538 let mut data = Vec::new();
1539 while reads < 10_000 && iters < 50_000_000 {
1540 iters += 1;
1541 let seq = r_latest.load(O::Acquire);
1542 if seq == 0 {
1543 std::hint::spin_loop();
1544 continue;
1545 }
1546 let slot = (seq % SLOTS as u64) as u32;
1547 let region = unsafe { std::slice::from_raw_parts(r_shared.0, r_shared.1) };
1550 if read_slot_into(region, &layout, slot, seq, &mut data)
1551 .unwrap()
1552 .is_some()
1553 {
1554 reads += 1;
1555 let marker = (seq & 0xFF) as u8;
1556 assert!(
1557 data.iter().all(|&b| b == marker),
1558 "torn read at seq {seq}: expected all {marker:#x}"
1559 );
1560 }
1561 }
1562 stop.store(true, O::Relaxed);
1563 writer.join().unwrap();
1564 assert!(reads > 0, "reader observed no committed frames");
1566 }
1567}