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//! # Synchronous I/O seam
13//!
14//! Consistent with the bridge, the runtime drives the HAL synchronously: the
15//! inbound sensor drain is [`updates`](Hal::updates) (a sync-pollable
16//! [`Subscription`] the step loop `try_recv`s), and the outbound actuator push
17//! is [`try_send`](Hal::try_send) — non-blocking, called directly from the
18//! synchronous step. Any real async work is the implementation's own
19//! responsibility (its own task/queue), the same way a bridge owns its socket.
20//!
21//! Pick an implementation per robot: [`FakeHal`] here (also the test double),
22//! and the real ones (ros2, restful, nao) in their own sibling crates.
23
24use std::sync::mpsc::Sender;
25use std::sync::{Arc, Mutex};
26
27use async_trait::async_trait;
28
29use arora_types::data::{Key, State, StateChange, Subscription};
30use arora_types::value::Value;
31
32/// What device a HAL drives.
33#[derive(Debug, Clone, Default, PartialEq, Eq)]
34pub struct HalDescription {
35    pub model_family: Option<String>,
36    pub hardware_version: Option<String>,
37    pub software_version: Option<String>,
38}
39
40/// Something went wrong talking to the hardware.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum HalError {
43    /// The hardware link is broken / unavailable.
44    Broken(String),
45    /// A key could not be resolved.
46    NoSuchKey(String),
47    /// Anything else, with a message.
48    Other(String),
49}
50
51impl std::fmt::Display for HalError {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        match self {
54            HalError::Broken(m) => write!(f, "hardware link broken: {m}"),
55            HalError::NoSuchKey(k) => write!(f, "no such key: {k}"),
56            HalError::Other(m) => write!(f, "{m}"),
57        }
58    }
59}
60
61impl std::error::Error for HalError {}
62
63pub type HalResult<T> = Result<T, HalError>;
64
65/// The Hardware Abstraction Layer: the boundary to a device.
66///
67/// Interior-mutable (`&self`) so the runtime can share one HAL across tasks,
68/// the same way a [`DataStore`](arora_types::data::DataStore) is shared.
69#[async_trait]
70pub trait Hal: Send + Sync {
71    /// Describe the device (model family, versions).
72    async fn describe(&self) -> HalDescription;
73
74    /// Read the current values for the given keys. Each entry is `None` if the
75    /// key is unset/absent (further nesting lives inside [`Value`]).
76    async fn read(&self, keys: &[Key]) -> HalResult<Vec<Option<Value>>>;
77
78    /// Read everything the HAL currently exposes.
79    async fn read_all(&self) -> HalResult<State>;
80
81    /// Apply actuator/state changes. Observers of [`updates`](Hal::updates) see
82    /// the resulting changes.
83    async fn write(&self, changes: StateChange) -> HalResult<()>;
84
85    /// Push actuator/state changes toward the hardware immediately, without
86    /// blocking — the outbound counterpart to the [`updates`](Hal::updates)
87    /// sensor drain, and the shape the synchronous runtime step calls directly
88    /// (mirroring `Bridge::try_send`).
89    ///
90    /// The default forwards to [`write`](Hal::write), which suits HALs whose
91    /// write does not truly block (in-memory fakes, cache-only writes). A HAL
92    /// that performs real async I/O (HTTP, DDS) should override this to enqueue
93    /// onto its own task so the caller's synchronous step loop never blocks on
94    /// the hardware.
95    fn try_send(&self, changes: &StateChange) {
96        #[cfg(not(target_arch = "wasm32"))]
97        {
98            let _ = futures::executor::block_on(self.write(changes.clone()));
99        }
100        // On wasm there are no threads to park on; a wasm HAL (an in-process
101        // fake) overrides this with a synchronous apply.
102        #[cfg(target_arch = "wasm32")]
103        {
104            let _ = changes;
105        }
106    }
107
108    /// A feed of changes the hardware reports (sensors, mirrored actuation, …).
109    fn updates(&self) -> Subscription;
110}
111
112/// Optional extension: HALs that can supply a 3D model (GLB) of the device.
113#[async_trait]
114pub trait HalAssets: Send + Sync {
115    async fn model_glb(&self) -> HalResult<Option<Vec<u8>>>;
116}
117
118#[derive(Default)]
119struct FakeInner {
120    description: HalDescription,
121    model_glb: Option<Vec<u8>>,
122    state: State,
123    subscribers: Vec<Sender<StateChange>>,
124}
125
126impl FakeInner {
127    fn notify(&mut self, change: &StateChange) {
128        if change.is_empty() {
129            return;
130        }
131        self.subscribers
132            .retain(|tx| tx.send(change.clone()).is_ok());
133    }
134}
135
136/// An in-memory fake [`Hal`] for tests and simulators.
137///
138/// Echoes writes back as state, and fakes joint actuation by mirroring any
139/// `*.target_position` write to the corresponding `*.position` (so a consumer
140/// that writes a target sees the measured position follow). Cheaply cloneable;
141/// clones share the same state. (Backed by [`State`](arora_types::data::State),
142/// the trivial owned state type.)
143#[derive(Clone, Default)]
144pub struct FakeHal {
145    inner: Arc<Mutex<FakeInner>>,
146}
147
148impl FakeHal {
149    pub fn new() -> Self {
150        Self::default()
151    }
152
153    pub fn with_description(description: HalDescription) -> Self {
154        Self {
155            inner: Arc::new(Mutex::new(FakeInner {
156                description,
157                ..Default::default()
158            })),
159        }
160    }
161
162    pub fn set_model_glb(&self, glb: Vec<u8>) {
163        self.inner.lock().unwrap().model_glb = Some(glb);
164    }
165
166    /// Apply a write synchronously: store it, echo it to subscribers, and fake
167    /// joint actuation by mirroring any `*.target_position` to `*.position`.
168    /// Shared by [`Hal::write`] and [`Hal::try_send`].
169    fn apply_write(&self, changes: &StateChange) {
170        if changes.is_empty() {
171            return;
172        }
173        let mut inner = self.inner.lock().unwrap();
174        inner.state.apply(changes.clone());
175        inner.notify(changes);
176
177        // Fake joint actuation: mirror "*.target_position" to "*.position".
178        let mut mirrored = StateChange::new();
179        for (key, value) in &changes.set {
180            if key.get_component() == Some("target_position") {
181                mirrored
182                    .set
183                    .insert(key.clone().with_component("position"), value.clone());
184            }
185        }
186        if !mirrored.is_empty() {
187            inner.state.apply(mirrored.clone());
188            inner.notify(&mirrored);
189        }
190    }
191}
192
193#[async_trait]
194impl Hal for FakeHal {
195    async fn describe(&self) -> HalDescription {
196        self.inner.lock().unwrap().description.clone()
197    }
198
199    async fn read(&self, keys: &[Key]) -> HalResult<Vec<Option<Value>>> {
200        let inner = self.inner.lock().unwrap();
201        Ok(keys
202            .iter()
203            .map(|k| inner.state.get(k).cloned().flatten())
204            .collect())
205    }
206
207    async fn read_all(&self) -> HalResult<State> {
208        Ok(self.inner.lock().unwrap().state.clone())
209    }
210
211    async fn write(&self, changes: StateChange) -> HalResult<()> {
212        self.apply_write(&changes);
213        Ok(())
214    }
215
216    /// Synchronous, immediate apply — the fake never blocks, so it needs no task
217    /// of its own (unlike a real HTTP/DDS HAL).
218    fn try_send(&self, changes: &StateChange) {
219        self.apply_write(changes);
220    }
221
222    fn updates(&self) -> Subscription {
223        let (tx, rx) = std::sync::mpsc::channel();
224        self.inner.lock().unwrap().subscribers.push(tx);
225        Subscription::new(rx)
226    }
227}
228
229#[async_trait]
230impl HalAssets for FakeHal {
231    async fn model_glb(&self) -> HalResult<Option<Vec<u8>>> {
232        Ok(self.inner.lock().unwrap().model_glb.clone())
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[tokio::test]
241    async fn fake_mirrors_target_position() {
242        let hal = FakeHal::new();
243        let sub = hal.updates();
244        hal.write(StateChange::set("joint1.target_position", Value::from(1.0)))
245            .await
246            .unwrap();
247        // measured position mirrors the target
248        assert_eq!(
249            hal.read(&[Key::from("joint1.position")]).await.unwrap(),
250            vec![Some(Value::from(1.0))]
251        );
252        // a subscriber saw the mirrored position change
253        let saw_position = sub
254            .try_iter()
255            .any(|c| c.contains(&Key::from("joint1.position")));
256        assert!(saw_position);
257    }
258
259    #[test]
260    fn try_send_applies_synchronously_and_mirrors() {
261        // The synchronous seam the runtime's step calls: no async, immediate.
262        let hal = FakeHal::new();
263        let sub = hal.updates();
264        hal.try_send(&StateChange::set(
265            "joint1.target_position",
266            Value::from(1.0),
267        ));
268        let saw_position = sub
269            .try_iter()
270            .any(|c| c.contains(&Key::from("joint1.position")));
271        assert!(
272            saw_position,
273            "try_send should mirror target to measured position"
274        );
275    }
276
277    #[tokio::test]
278    async fn read_absent_is_none() {
279        let hal = FakeHal::new();
280        assert_eq!(hal.read(&[Key::from("nope")]).await.unwrap(), vec![None]);
281    }
282
283    #[tokio::test]
284    async fn describe_and_glb() {
285        let hal = FakeHal::with_description(HalDescription {
286            model_family: Some("test".into()),
287            ..Default::default()
288        });
289        assert_eq!(hal.describe().await.model_family.as_deref(), Some("test"));
290        hal.set_model_glb(vec![1, 2, 3]);
291        assert_eq!(hal.model_glb().await.unwrap(), Some(vec![1, 2, 3]));
292    }
293}