Skip to main content

arora_hal/
lib.rs

1//! The Arora Hardware Abstraction Layer.
2//!
3//! [`Hal`] is the boundary to a real (or fake) device — the thing studio-bridge
4//! called a `Controller`. A HAL reads sensors and reported state, accepts
5//! actuator/state writes, and pushes a feed of changes the hardware makes.
6//!
7//! The Arora runtime mirrors a HAL against a
8//! [`DataStore`](arora_types::data::DataStore): HAL updates flow into the store,
9//! store writes flow to the HAL. The HAL trait depends only on `arora-types`, so
10//! any execution engine can drive it without pulling in the bridge.
11//!
12//! Pick an implementation per robot: [`FakeHal`] here (also the test double),
13//! and the real ones (ros2, restful, nao) in their own sibling crates.
14
15use std::sync::mpsc::Sender;
16use std::sync::{Arc, Mutex};
17
18use async_trait::async_trait;
19
20use arora_types::data::{Key, State, StateChange, Subscription};
21use arora_types::value::Value;
22
23/// What device a HAL drives.
24#[derive(Debug, Clone, Default, PartialEq, Eq)]
25pub struct HalDescription {
26    pub model_family: Option<String>,
27    pub hardware_version: Option<String>,
28    pub software_version: Option<String>,
29}
30
31/// Something went wrong talking to the hardware.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum HalError {
34    /// The hardware link is broken / unavailable.
35    Broken(String),
36    /// A key could not be resolved.
37    NoSuchKey(String),
38    /// Anything else, with a message.
39    Other(String),
40}
41
42impl std::fmt::Display for HalError {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            HalError::Broken(m) => write!(f, "hardware link broken: {m}"),
46            HalError::NoSuchKey(k) => write!(f, "no such key: {k}"),
47            HalError::Other(m) => write!(f, "{m}"),
48        }
49    }
50}
51
52impl std::error::Error for HalError {}
53
54pub type HalResult<T> = Result<T, HalError>;
55
56/// The Hardware Abstraction Layer: the boundary to a device.
57///
58/// Interior-mutable (`&self`) so the runtime can share one HAL across tasks,
59/// the same way a [`DataStore`](arora_types::data::DataStore) is shared.
60#[async_trait]
61pub trait Hal: Send + Sync {
62    /// Describe the device (model family, versions).
63    async fn describe(&self) -> HalDescription;
64
65    /// Read the current values for the given keys. Each entry is `None` if the
66    /// key is unset/absent (further nesting lives inside [`Value`]).
67    async fn read(&self, keys: &[Key]) -> HalResult<Vec<Option<Value>>>;
68
69    /// Read everything the HAL currently exposes.
70    async fn read_all(&self) -> HalResult<State>;
71
72    /// Apply actuator/state changes. Observers of [`updates`](Hal::updates) see
73    /// the resulting changes.
74    async fn write(&self, changes: StateChange) -> HalResult<()>;
75
76    /// A feed of changes the hardware reports (sensors, mirrored actuation, …).
77    fn updates(&self) -> Subscription;
78}
79
80/// Optional extension: HALs that can supply a 3D model (GLB) of the device.
81#[async_trait]
82pub trait HalAssets: Send + Sync {
83    async fn model_glb(&self) -> HalResult<Option<Vec<u8>>>;
84}
85
86#[derive(Default)]
87struct FakeInner {
88    description: HalDescription,
89    model_glb: Option<Vec<u8>>,
90    state: State,
91    subscribers: Vec<Sender<StateChange>>,
92}
93
94impl FakeInner {
95    fn notify(&mut self, change: &StateChange) {
96        if change.is_empty() {
97            return;
98        }
99        self.subscribers
100            .retain(|tx| tx.send(change.clone()).is_ok());
101    }
102}
103
104/// An in-memory fake [`Hal`] for tests and simulators.
105///
106/// Echoes writes back as state, and fakes joint actuation by mirroring any
107/// `*.target_position` write to the corresponding `*.position` (so a consumer
108/// that writes a target sees the measured position follow). Cheaply cloneable;
109/// clones share the same state. (Backed by [`State`](arora_types::data::State),
110/// the trivial owned state type.)
111#[derive(Clone, Default)]
112pub struct FakeHal {
113    inner: Arc<Mutex<FakeInner>>,
114}
115
116impl FakeHal {
117    pub fn new() -> Self {
118        Self::default()
119    }
120
121    pub fn with_description(description: HalDescription) -> Self {
122        Self {
123            inner: Arc::new(Mutex::new(FakeInner {
124                description,
125                ..Default::default()
126            })),
127        }
128    }
129
130    pub fn set_model_glb(&self, glb: Vec<u8>) {
131        self.inner.lock().unwrap().model_glb = Some(glb);
132    }
133}
134
135#[async_trait]
136impl Hal for FakeHal {
137    async fn describe(&self) -> HalDescription {
138        self.inner.lock().unwrap().description.clone()
139    }
140
141    async fn read(&self, keys: &[Key]) -> HalResult<Vec<Option<Value>>> {
142        let inner = self.inner.lock().unwrap();
143        Ok(keys
144            .iter()
145            .map(|k| inner.state.get(k).cloned().flatten())
146            .collect())
147    }
148
149    async fn read_all(&self) -> HalResult<State> {
150        Ok(self.inner.lock().unwrap().state.clone())
151    }
152
153    async fn write(&self, changes: StateChange) -> HalResult<()> {
154        if changes.is_empty() {
155            return Ok(());
156        }
157        let mut inner = self.inner.lock().unwrap();
158        inner.state.apply(changes.clone());
159        inner.notify(&changes);
160
161        // Fake joint actuation: mirror "*.target_position" to "*.position".
162        let mut mirrored = StateChange::new();
163        for (key, value) in &changes.set {
164            if key.get_component() == Some("target_position") {
165                mirrored
166                    .set
167                    .insert(key.clone().with_component("position"), value.clone());
168            }
169        }
170        if !mirrored.is_empty() {
171            inner.state.apply(mirrored.clone());
172            inner.notify(&mirrored);
173        }
174        Ok(())
175    }
176
177    fn updates(&self) -> Subscription {
178        let (tx, rx) = std::sync::mpsc::channel();
179        self.inner.lock().unwrap().subscribers.push(tx);
180        Subscription::new(rx)
181    }
182}
183
184#[async_trait]
185impl HalAssets for FakeHal {
186    async fn model_glb(&self) -> HalResult<Option<Vec<u8>>> {
187        Ok(self.inner.lock().unwrap().model_glb.clone())
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[tokio::test]
196    async fn fake_mirrors_target_position() {
197        let hal = FakeHal::new();
198        let sub = hal.updates();
199        hal.write(StateChange::set("joint1.target_position", Value::from(1.0)))
200            .await
201            .unwrap();
202        // measured position mirrors the target
203        assert_eq!(
204            hal.read(&[Key::from("joint1.position")]).await.unwrap(),
205            vec![Some(Value::from(1.0))]
206        );
207        // a subscriber saw the mirrored position change
208        let saw_position = sub
209            .try_iter()
210            .any(|c| c.contains(&Key::from("joint1.position")));
211        assert!(saw_position);
212    }
213
214    #[tokio::test]
215    async fn read_absent_is_none() {
216        let hal = FakeHal::new();
217        assert_eq!(hal.read(&[Key::from("nope")]).await.unwrap(), vec![None]);
218    }
219
220    #[tokio::test]
221    async fn describe_and_glb() {
222        let hal = FakeHal::with_description(HalDescription {
223            model_family: Some("test".into()),
224            ..Default::default()
225        });
226        assert_eq!(hal.describe().await.model_family.as_deref(), Some("test"));
227        hal.set_model_glb(vec![1, 2, 3]);
228        assert_eq!(hal.model_glb().await.unwrap(), Some(vec![1, 2, 3]));
229    }
230}