pub mod pose;
use std::collections::HashMap;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use ados_protocol::framebus::{
self, Detection, DetectionBatch, FrameDescriptor, ModelMetadata, RingLayout,
VISION_DETECTION_VERSION,
};
use ados_protocol::vision_rpc;
use rmpv::Value;
use crate::client::{ClientError, OffloadAdvertisement, PluginIpcClient};
pub use pose::{Odometry, Pose, POSE_COVARIANCE_LEN, VIO_COMPONENT_ID};
#[derive(Debug, Clone, PartialEq)]
pub struct Frame {
pub descriptor: FrameDescriptor,
pub pixels: Vec<u8>,
}
pub type FrameCallback = Arc<dyn Fn(Frame) + Send + Sync>;
pub type DetectionCallback = Arc<dyn Fn(DetectionBatch) + Send + Sync>;
#[derive(Debug, Clone, PartialEq)]
pub struct ModelResolution {
pub state: String,
pub model_id: String,
pub runtime: Option<String>,
pub path: Option<String>,
pub reason: Option<String>,
}
fn mpv_get<'a>(v: &'a Value, key: &str) -> Option<&'a Value> {
v.as_map()?
.iter()
.find(|(k, _)| k.as_str() == Some(key))
.map(|(_, val)| val)
}
fn parse_model_resolutions(reply: &Value) -> Vec<ModelResolution> {
let Some(models) = mpv_get(reply, "models").and_then(Value::as_array) else {
return Vec::new();
};
models
.iter()
.filter_map(|m| {
Some(ModelResolution {
state: mpv_get(m, "state")?.as_str()?.to_string(),
model_id: mpv_get(m, "model_id")?.as_str()?.to_string(),
runtime: mpv_get(m, "runtime")
.and_then(Value::as_str)
.map(str::to_string),
path: mpv_get(m, "path")
.and_then(Value::as_str)
.map(str::to_string),
reason: mpv_get(m, "reason")
.and_then(Value::as_str)
.map(str::to_string),
})
})
.collect()
}
#[derive(Clone)]
pub struct VisionClient {
ipc: Arc<PluginIpcClient>,
rings: Arc<Mutex<RingCache>>,
}
impl VisionClient {
pub(crate) fn new(ipc: Arc<PluginIpcClient>) -> Self {
Self {
ipc,
rings: Arc::new(Mutex::new(RingCache::new(PathBuf::from(DEFAULT_SHM_DIR)))),
}
}
pub fn with_shm_dir(self, dir: impl Into<PathBuf>) -> Self {
Self {
ipc: self.ipc,
rings: Arc::new(Mutex::new(RingCache::new(dir.into()))),
}
}
pub async fn subscribe_frames(
&self,
camera_id: Option<&str>,
callback: FrameCallback,
) -> Result<(), ClientError> {
let want_camera = camera_id.map(str::to_string);
let rings = self.rings.clone();
let filter = want_camera.clone();
let on_deliver = move |args: Value| {
let Some(descriptor) = decode_descriptor(&args) else {
return;
};
if let Some(want) = &filter {
if &descriptor.camera_id != want {
return;
}
}
if let Some(frame) = resolve_frame(&rings, descriptor) {
callback(frame);
}
};
self.ipc
.register_vision_callback(camera_id, Arc::new(on_deliver));
self.ipc.vision_subscribe_frames(camera_id).await?;
Ok(())
}
pub async fn register_model(&self, model: &ModelMetadata) -> Result<Value, ClientError> {
self.ipc.vision_register_model(model).await
}
pub async fn resolved_model(&self) -> Result<Vec<ModelResolution>, ClientError> {
let reply = self.ipc.vision_read_model().await?;
Ok(parse_model_resolutions(&reply))
}
pub async fn infer(
&self,
model_id: &str,
frame: &Frame,
) -> Result<Vec<Detection>, ClientError> {
let reply = self.ipc.vision_infer(model_id, &frame.descriptor).await?;
vision_rpc::decode_infer_reply(&reply)
.map(|batch| batch.detections)
.map_err(|e| ClientError::Rpc(format!("infer reply decode failed: {e}")))
}
pub async fn publish_detection(&self, batch: &DetectionBatch) -> Result<Value, ClientError> {
self.ipc.vision_publish_detection(batch).await
}
pub async fn publish_one(
&self,
model_id: &str,
frame: &Frame,
detection: Detection,
) -> Result<Value, ClientError> {
let batch = DetectionBatch {
v: VISION_DETECTION_VERSION,
model_id: model_id.to_string(),
camera_id: frame.descriptor.camera_id.clone(),
frame_id: frame.descriptor.frame_id,
ts_ms: (frame.descriptor.ts_ns / 1_000_000) as i64,
frame_width: frame.descriptor.width,
frame_height: frame.descriptor.height,
detections: vec![detection],
};
self.publish_detection(&batch).await
}
pub async fn advertise_offload(
&self,
advert: &OffloadAdvertisement,
) -> Result<Value, ClientError> {
self.ipc.offload_advertise(advert).await
}
pub async fn register_vio_component(&self) -> Result<Value, ClientError> {
self.ipc
.mavlink_register_component(VIO_COMPONENT_ID, "vio")
.await
}
pub async fn inject_pose(&self, pose: &Pose) -> Result<Value, ClientError> {
let frame = pose::frame_for(&pose.to_vision_position_estimate())
.map_err(|e| ClientError::Rpc(format!("vision pose encode failed: {e}")))?;
self.ipc.mavlink_send(&frame, Some(VIO_COMPONENT_ID)).await
}
pub async fn inject_odometry(&self, odometry: &Odometry) -> Result<Value, ClientError> {
let frame = pose::frame_for(&odometry.to_odometry())
.map_err(|e| ClientError::Rpc(format!("vision odometry encode failed: {e}")))?;
self.ipc.mavlink_send(&frame, Some(VIO_COMPONENT_ID)).await
}
pub async fn subscribe_detections(
&self,
camera_id: Option<&str>,
callback: DetectionCallback,
) -> Result<(), ClientError> {
let filter = camera_id.map(str::to_string);
let on_deliver = move |args: Value| {
let Some(Value::Binary(blob)) = map_get(&args, "batch") else {
return;
};
let Ok(batch) = DetectionBatch::from_msgpack(&blob) else {
return;
};
if let Some(want) = &filter {
if &batch.camera_id != want {
return;
}
}
callback(batch);
};
self.ipc
.register_detection_callback(camera_id, Arc::new(on_deliver));
self.ipc.vision_subscribe_detections(camera_id).await?;
Ok(())
}
pub async fn designate_track(
&self,
camera_id: &str,
detection: &Detection,
) -> Result<Value, ClientError> {
self.ipc.vision_designate_track(camera_id, detection).await
}
}
const DEFAULT_SHM_DIR: &str = "/dev/shm";
struct RingCache {
dir: PathBuf,
rings: HashMap<String, MappedRing>,
}
impl RingCache {
fn new(dir: PathBuf) -> Self {
Self {
dir,
rings: HashMap::new(),
}
}
}
struct MappedRing {
map: memmap2::Mmap,
layout: RingLayout,
file_id: (u64, u64),
last_seq: u64,
}
impl MappedRing {
fn read(&mut self, descriptor: &FrameDescriptor) -> Option<Vec<u8>> {
let mut pixels = Vec::with_capacity(descriptor.byte_len as usize);
framebus::read_slot_into(
&self.map,
&self.layout,
descriptor.slot,
descriptor.seq,
&mut pixels,
)
.ok()
.flatten()?;
self.last_seq = self.last_seq.max(descriptor.seq);
Some(pixels)
}
fn is_named_by(&self, path: &Path) -> bool {
std::fs::metadata(path).is_ok_and(|m| (m.dev(), m.ino()) == self.file_id)
}
}
fn resolve_frame(cache: &Mutex<RingCache>, descriptor: FrameDescriptor) -> Option<Frame> {
let mut guard = cache.lock().expect("ring cache lock");
let cache = &mut *guard;
let path = cache.dir.join(&descriptor.shm_name);
if let Some(ring) = cache.rings.get_mut(&descriptor.shm_name) {
let regressed = descriptor.seq <= ring.last_seq;
if !regressed {
if let Some(pixels) = ring.read(&descriptor) {
return Some(Frame { descriptor, pixels });
}
}
if ring.is_named_by(&path) {
let pixels = if regressed {
ring.read(&descriptor)
} else {
None
}?;
return Some(Frame { descriptor, pixels });
}
cache.rings.remove(&descriptor.shm_name);
}
let mut ring = map_ring(&path)?;
let pixels = ring.read(&descriptor);
cache.rings.insert(descriptor.shm_name.clone(), ring);
Some(Frame {
descriptor,
pixels: pixels?,
})
}
fn map_ring(path: &Path) -> Option<MappedRing> {
let file = std::fs::File::open(path).ok()?;
let meta = file.metadata().ok()?;
let map = unsafe { memmap2::Mmap::map(&file) }.ok()?;
let layout = RingLayout::read_header(&map)?;
Some(MappedRing {
map,
layout,
file_id: (meta.dev(), meta.ino()),
last_seq: 0,
})
}
fn decode_descriptor(payload: &Value) -> Option<FrameDescriptor> {
if let Some(Value::Binary(blob)) = map_get(payload, "descriptor") {
return FrameDescriptor::from_msgpack(&blob).ok();
}
let bytes = rmp_serde::to_vec_named(payload).ok()?;
FrameDescriptor::from_msgpack(&bytes).ok()
}
fn map_get(args: &Value, key: &str) -> Option<Value> {
match args {
Value::Map(entries) => entries
.iter()
.find(|(k, _)| k.as_str() == Some(key))
.map(|(_, v)| v.clone()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use ados_protocol::framebus::FrameFormat;
fn descriptor(shm_name: &str, slot: u32, seq: u64, byte_len: u32) -> FrameDescriptor {
FrameDescriptor {
v: ados_protocol::framebus::FRAMEBUS_DESCRIPTOR_VERSION,
camera_id: "uvc-0".into(),
frame_id: seq,
ts_ns: 1_700_000_000_000,
ts_sensor_ns: None,
exposure_ns: None,
trigger_seq: None,
timebase: ados_protocol::framebus::TIMEBASE_HOST,
width: 4,
height: 4,
format: FrameFormat::Rgb24,
shm_name: shm_name.into(),
slot,
seq,
byte_len,
}
}
#[test]
fn descriptor_decodes_from_a_named_map_payload() {
let d = descriptor("ados-vision-uvc-0", 1, 7, 48);
let bytes = d.to_msgpack().unwrap();
let payload: Value = rmp_serde::from_slice(&bytes).unwrap();
let back = decode_descriptor(&payload).unwrap();
assert_eq!(back, d);
}
#[test]
fn descriptor_decodes_from_a_blob_payload() {
let d = descriptor("ados-vision-uvc-0", 2, 11, 48);
let blob = d.to_msgpack().unwrap();
let payload = Value::Map(vec![(Value::from("descriptor"), Value::Binary(blob))]);
let back = decode_descriptor(&payload).unwrap();
assert_eq!(back, d);
}
#[test]
fn resolve_reads_a_frame_written_to_a_real_ring() {
let layout = RingLayout::for_frame(4, 4, 4, FrameFormat::Rgb24); let mut region = vec![0u8; layout.total_len()];
layout.write_header(&mut region).unwrap();
let pixels: Vec<u8> = (0..layout.slot_bytes as u8).collect();
let seq = 5u64;
let slot = (seq % layout.slot_count as u64) as u32;
framebus::write_slot(&mut region, &layout, slot, seq, &pixels).unwrap();
let read_layout = RingLayout::read_header(®ion).unwrap();
assert_eq!(read_layout, layout);
let mut out = Vec::new();
let got = framebus::read_slot_into(®ion, &read_layout, slot, seq, &mut out).unwrap();
assert_eq!(got, Some(pixels.len()));
assert_eq!(out, pixels);
assert_eq!(
framebus::read_slot_into(®ion, &read_layout, slot, seq + 1, &mut out).unwrap(),
None
);
}
}