use std::path::Path;
use std::sync::{Arc, Mutex};
use ados_protocol::framebus::{
self, DetectionBatch, FrameDescriptor, FrameFormat, RingLayout, FRAMEBUS_DESCRIPTOR_VERSION,
};
use crate::vision::{Frame, FrameCallback};
const DEFAULT_SLOT_COUNT: u32 = 8;
pub type CapturedDetection = DetectionBatch;
pub struct FakeVisionEngine {
camera_id: String,
shm_name: String,
format: FrameFormat,
layout: RingLayout,
region: Vec<u8>,
next_seq: u64,
pending: Vec<Vec<u8>>,
callback: Option<FrameCallback>,
captured: Arc<Mutex<Vec<CapturedDetection>>>,
}
impl FakeVisionEngine {
pub fn new(camera_id: &str, width: u32, height: u32, format: FrameFormat) -> Self {
Self::with_slot_count(camera_id, width, height, format, DEFAULT_SLOT_COUNT)
}
pub fn with_slot_count(
camera_id: &str,
width: u32,
height: u32,
format: FrameFormat,
slot_count: u32,
) -> Self {
let layout = RingLayout::for_frame(slot_count, width, height, format);
let mut region = vec![0u8; layout.total_len()];
layout
.write_header(&mut region)
.expect("freshly sized region holds its own header");
Self {
camera_id: camera_id.to_string(),
shm_name: format!("ados-vision-{camera_id}"),
format,
layout,
region,
next_seq: 0,
pending: Vec::new(),
callback: None,
captured: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn frame_bytes(&self) -> usize {
self.layout.slot_bytes as usize
}
pub fn on_frame<F>(&mut self, callback: F)
where
F: Fn(Frame) + Send + Sync + 'static,
{
self.callback = Some(Arc::new(callback));
}
pub fn on_frame_arc(&mut self, callback: FrameCallback) {
self.callback = Some(callback);
}
pub fn push_frame(&mut self, pixels: Vec<u8>) {
self.pending.push(pixels);
}
pub fn push_solid(&mut self, value: u8) {
self.pending.push(vec![value; self.frame_bytes()]);
}
pub fn push_dir(&mut self, dir: impl AsRef<Path>) -> std::io::Result<usize> {
let mut paths: Vec<_> = std::fs::read_dir(dir)?
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| {
matches!(
p.extension().and_then(|s| s.to_str()),
Some("bin") | Some("raw")
)
})
.collect();
paths.sort();
let mut n = 0;
for p in paths {
let bytes = std::fs::read(&p)?;
self.pending.push(bytes);
n += 1;
}
Ok(n)
}
pub fn deliver_one(&mut self) -> bool {
if self.pending.is_empty() {
return false;
}
let pixels = self.pending.remove(0);
let seq = self.next_seq + 1;
self.next_seq = seq;
let slot = (seq % self.layout.slot_count as u64) as u32;
framebus::write_slot(&mut self.region, &self.layout, slot, seq, &pixels)
.expect("synthetic frame fits a full-size slot");
let descriptor = FrameDescriptor {
v: FRAMEBUS_DESCRIPTOR_VERSION,
camera_id: self.camera_id.clone(),
frame_id: seq,
ts_ns: 1_700_000_000_000 + seq,
ts_sensor_ns: None,
exposure_ns: None,
trigger_seq: None,
timebase: ados_protocol::framebus::TIMEBASE_HOST,
width: 0, height: 0,
format: self.layout_format(),
shm_name: self.shm_name.clone(),
slot,
seq,
byte_len: pixels.len() as u32,
};
if let Some(frame) = self.resolve(descriptor) {
if let Some(cb) = &self.callback {
cb(frame);
}
}
true
}
pub fn deliver_all(&mut self) -> usize {
let mut n = 0;
while self.deliver_one() {
n += 1;
}
n
}
pub fn detection_sink(&self) -> Arc<Mutex<Vec<CapturedDetection>>> {
self.captured.clone()
}
pub fn capture(&self, batch: DetectionBatch) {
self.captured.lock().expect("capture lock").push(batch);
}
pub fn captured_detections(&self) -> Vec<CapturedDetection> {
self.captured.lock().expect("capture lock").clone()
}
pub fn clear_captured(&self) {
self.captured.lock().expect("capture lock").clear();
}
fn resolve(&self, descriptor: FrameDescriptor) -> Option<Frame> {
let mut pixels = Vec::with_capacity(descriptor.byte_len as usize);
framebus::read_slot_into(
&self.region,
&self.layout,
descriptor.slot,
descriptor.seq,
&mut pixels,
)
.ok()
.flatten()?;
Some(Frame { descriptor, pixels })
}
fn layout_format(&self) -> FrameFormat {
self.format
}
}
#[cfg(test)]
mod tests {
use super::*;
use ados_protocol::framebus::{BoundingBox, Detection};
#[test]
fn delivers_a_solid_frame_to_the_callback() {
let mut engine = FakeVisionEngine::new("uvc-0", 4, 4, FrameFormat::Rgb24);
let seen: Arc<Mutex<Vec<Frame>>> = Arc::new(Mutex::new(Vec::new()));
let s = seen.clone();
engine.on_frame(move |frame| s.lock().unwrap().push(frame));
engine.push_solid(0x42);
assert_eq!(engine.deliver_all(), 1);
let got = seen.lock().unwrap();
assert_eq!(got.len(), 1);
assert_eq!(got[0].descriptor.camera_id, "uvc-0");
assert_eq!(got[0].descriptor.frame_id, 1);
assert_eq!(got[0].pixels.len(), engine.frame_bytes());
assert!(got[0].pixels.iter().all(|&b| b == 0x42));
}
#[test]
fn delivers_frames_in_order() {
let mut engine = FakeVisionEngine::new("uvc-0", 2, 2, FrameFormat::Rgb24);
let ids: Arc<Mutex<Vec<u64>>> = Arc::new(Mutex::new(Vec::new()));
let i = ids.clone();
engine.on_frame(move |frame| i.lock().unwrap().push(frame.descriptor.frame_id));
engine.push_solid(1);
engine.push_solid(2);
engine.push_solid(3);
assert_eq!(engine.deliver_all(), 3);
assert_eq!(*ids.lock().unwrap(), vec![1, 2, 3]);
}
#[test]
fn captures_published_detections() {
let engine = FakeVisionEngine::new("uvc-0", 2, 2, FrameFormat::Rgb24);
let batch = DetectionBatch {
v: ados_protocol::framebus::VISION_DETECTION_VERSION,
model_id: "com.example.weeds".into(),
camera_id: "uvc-0".into(),
frame_id: 1,
ts_ms: 1,
frame_width: 640,
frame_height: 480,
detections: vec![Detection {
bbox: Some(BoundingBox {
x: 0.0,
y: 0.0,
width: 1.0,
height: 1.0,
}),
class_label: "weed".into(),
confidence: 0.5,
track_id: None,
assoc_confidence: None,
lock_state: None,
attributes: None,
mask: None,
keypoints: None,
depth: None,
world_pos: None,
}],
};
engine.capture(batch.clone());
let got = engine.captured_detections();
assert_eq!(got, vec![batch]);
engine.clear_captured();
assert!(engine.captured_detections().is_empty());
}
#[test]
fn push_dir_reads_raw_frames_in_name_order() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("b.bin"), [2u8; 12]).unwrap();
std::fs::write(dir.path().join("a.raw"), [1u8; 12]).unwrap();
std::fs::write(dir.path().join("skip.txt"), b"not a frame").unwrap();
let mut engine = FakeVisionEngine::new("uvc-0", 2, 2, FrameFormat::Rgb24); let n = engine.push_dir(dir.path()).unwrap();
assert_eq!(n, 2);
let first_byte: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
let f = first_byte.clone();
engine.on_frame(move |frame| f.lock().unwrap().push(frame.pixels[0]));
engine.deliver_all();
assert_eq!(*first_byte.lock().unwrap(), vec![1, 2]);
}
}