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