use std::pin::Pin;
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use futures_channel::mpsc::UnboundedSender;
use futures_core::Stream;
use arora_types::data::{Key, State, StateChange};
use arora_types::value::Value;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HalDescription {
pub model_family: Option<String>,
pub hardware_version: Option<String>,
pub software_version: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HalError {
Broken(String),
NoSuchKey(String),
Other(String),
}
impl std::fmt::Display for HalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HalError::Broken(m) => write!(f, "hardware link broken: {m}"),
HalError::NoSuchKey(k) => write!(f, "no such key: {k}"),
HalError::Other(m) => write!(f, "{m}"),
}
}
}
impl std::error::Error for HalError {}
pub type HalResult<T> = Result<T, HalError>;
#[async_trait]
pub trait Hal: Send + Sync {
async fn describe(&self) -> HalDescription;
async fn read(&self, keys: &[Key]) -> HalResult<Vec<Option<Value>>>;
async fn read_all(&self) -> HalResult<State>;
async fn write(&self, changes: StateChange) -> HalResult<()>;
fn try_send(&self, changes: &StateChange);
fn updates(&self) -> UpdatesStream;
}
pub type UpdatesStream = Pin<Box<dyn Stream<Item = StateChange> + Send>>;
#[async_trait]
pub trait HalAssets: Send + Sync {
async fn model_glb(&self) -> HalResult<Option<Vec<u8>>>;
}
#[derive(Default)]
struct FakeInner {
description: HalDescription,
model_glb: Option<Vec<u8>>,
state: State,
subscribers: Vec<UnboundedSender<StateChange>>,
}
impl FakeInner {
fn notify(&mut self, change: &StateChange) {
if change.is_empty() {
return;
}
self.subscribers
.retain(|tx| tx.unbounded_send(change.clone()).is_ok());
}
}
#[derive(Clone, Default)]
pub struct FakeHal {
inner: Arc<Mutex<FakeInner>>,
}
impl FakeHal {
pub fn new() -> Self {
Self::default()
}
pub fn with_description(description: HalDescription) -> Self {
Self {
inner: Arc::new(Mutex::new(FakeInner {
description,
..Default::default()
})),
}
}
pub fn set_model_glb(&self, glb: Vec<u8>) {
self.inner.lock().unwrap().model_glb = Some(glb);
}
fn apply_write(&self, changes: &StateChange) {
let mut setpoints = StateChange::new();
let mut sensed = StateChange::new();
for (key, value) in &changes.set {
if key.get_component() == Some("target_position") {
setpoints.set.insert(key.clone(), value.clone());
sensed
.set
.insert(key.clone().with_component("position"), value.clone());
}
}
for key in &changes.unset {
if key.get_component() == Some("target_position") {
setpoints.unset.insert(key.clone());
sensed.unset.insert(key.clone().with_component("position"));
}
}
if setpoints.is_empty() {
return;
}
let mut inner = self.inner.lock().unwrap();
inner.state.apply(setpoints);
inner.state.apply(sensed.clone());
inner.notify(&sensed);
}
}
#[async_trait]
impl Hal for FakeHal {
async fn describe(&self) -> HalDescription {
self.inner.lock().unwrap().description.clone()
}
async fn read(&self, keys: &[Key]) -> HalResult<Vec<Option<Value>>> {
let inner = self.inner.lock().unwrap();
Ok(keys
.iter()
.map(|k| inner.state.get(k).cloned().flatten())
.collect())
}
async fn read_all(&self) -> HalResult<State> {
Ok(self.inner.lock().unwrap().state.clone())
}
async fn write(&self, changes: StateChange) -> HalResult<()> {
self.apply_write(&changes);
Ok(())
}
fn try_send(&self, changes: &StateChange) {
self.apply_write(changes);
}
fn updates(&self) -> UpdatesStream {
let (tx, rx) = futures_channel::mpsc::unbounded();
self.inner.lock().unwrap().subscribers.push(tx);
Box::pin(rx)
}
}
#[async_trait]
impl HalAssets for FakeHal {
async fn model_glb(&self) -> HalResult<Option<Vec<u8>>> {
Ok(self.inner.lock().unwrap().model_glb.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures::{FutureExt, StreamExt};
fn drain(feed: &mut UpdatesStream) -> Vec<StateChange> {
let mut out = Vec::new();
while let Some(Some(change)) = feed.next().now_or_never() {
out.push(change);
}
out
}
#[tokio::test]
async fn fake_mirrors_target_position() {
let hal = FakeHal::new();
let mut sub = hal.updates();
hal.write(StateChange::set("joint1.target_position", Value::from(1.0)))
.await
.unwrap();
assert_eq!(
hal.read(&[Key::from("joint1.position")]).await.unwrap(),
vec![Some(Value::from(1.0))]
);
let saw_position = drain(&mut sub)
.iter()
.any(|c| c.contains(&Key::from("joint1.position")));
assert!(saw_position);
}
#[test]
fn try_send_applies_synchronously_and_mirrors() {
let hal = FakeHal::new();
let mut sub = hal.updates();
hal.try_send(&StateChange::set(
"joint1.target_position",
Value::from(1.0),
));
let saw_position = drain(&mut sub)
.iter()
.any(|c| c.contains(&Key::from("joint1.position")));
assert!(
saw_position,
"try_send should mirror target to measured position"
);
}
#[tokio::test]
async fn read_absent_is_none() {
let hal = FakeHal::new();
assert_eq!(hal.read(&[Key::from("nope")]).await.unwrap(), vec![None]);
}
#[tokio::test]
async fn describe_and_glb() {
let hal = FakeHal::with_description(HalDescription {
model_family: Some("test".into()),
..Default::default()
});
assert_eq!(hal.describe().await.model_family.as_deref(), Some("test"));
hal.set_model_glb(vec![1, 2, 3]);
assert_eq!(hal.model_glb().await.unwrap(), Some(vec![1, 2, 3]));
}
}