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. The fake deals only in setpoints — the
173    /// `*.target_position` keys — and ignores every other key in the change,
174    /// the way hardware ignores what it has no actuator for. Each setpoint is
175    /// held and sensed back as the matching `*.position`, the fake's joint
176    /// actuation. Shared by [`Hal::write`] and [`Hal::try_send`].
177    fn apply_write(&self, changes: &StateChange) {
178        let mut setpoints = StateChange::new();
179        let mut sensed = StateChange::new();
180        for (key, value) in &changes.set {
181            if key.get_component() == Some("target_position") {
182                setpoints.set.insert(key.clone(), value.clone());
183                sensed
184                    .set
185                    .insert(key.clone().with_component("position"), value.clone());
186            }
187        }
188        for key in &changes.unset {
189            if key.get_component() == Some("target_position") {
190                setpoints.unset.insert(key.clone());
191                sensed.unset.insert(key.clone().with_component("position"));
192            }
193        }
194        if setpoints.is_empty() {
195            return;
196        }
197        let mut inner = self.inner.lock().unwrap();
198        inner.state.apply(setpoints);
199        inner.state.apply(sensed.clone());
200        inner.notify(&sensed);
201    }
202}
203
204#[async_trait]
205impl Hal for FakeHal {
206    async fn describe(&self) -> HalDescription {
207        self.inner.lock().unwrap().description.clone()
208    }
209
210    async fn read(&self, keys: &[Key]) -> HalResult<Vec<Option<Value>>> {
211        let inner = self.inner.lock().unwrap();
212        Ok(keys
213            .iter()
214            .map(|k| inner.state.get(k).cloned().flatten())
215            .collect())
216    }
217
218    async fn read_all(&self) -> HalResult<State> {
219        Ok(self.inner.lock().unwrap().state.clone())
220    }
221
222    async fn write(&self, changes: StateChange) -> HalResult<()> {
223        self.apply_write(&changes);
224        Ok(())
225    }
226
227    /// Synchronous, immediate apply — the fake never blocks, so it needs no task
228    /// of its own (unlike a real HTTP/DDS HAL).
229    fn try_send(&self, changes: &StateChange) {
230        self.apply_write(changes);
231    }
232
233    fn updates(&self) -> UpdatesStream {
234        let (tx, rx) = futures_channel::mpsc::unbounded();
235        self.inner.lock().unwrap().subscribers.push(tx);
236        Box::pin(rx)
237    }
238}
239
240#[async_trait]
241impl HalAssets for FakeHal {
242    async fn model_glb(&self) -> HalResult<Option<Vec<u8>>> {
243        Ok(self.inner.lock().unwrap().model_glb.clone())
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use futures::{FutureExt, StreamExt};
251
252    /// Drain everything the feed has already buffered, without blocking.
253    fn drain(feed: &mut UpdatesStream) -> Vec<StateChange> {
254        let mut out = Vec::new();
255        while let Some(Some(change)) = feed.next().now_or_never() {
256            out.push(change);
257        }
258        out
259    }
260
261    #[tokio::test]
262    async fn fake_mirrors_target_position() {
263        let hal = FakeHal::new();
264        let mut sub = hal.updates();
265        hal.write(StateChange::set("joint1.target_position", Value::from(1.0)))
266            .await
267            .unwrap();
268        // measured position mirrors the target
269        assert_eq!(
270            hal.read(&[Key::from("joint1.position")]).await.unwrap(),
271            vec![Some(Value::from(1.0))]
272        );
273        // a subscriber saw the mirrored position change
274        let saw_position = drain(&mut sub)
275            .iter()
276            .any(|c| c.contains(&Key::from("joint1.position")));
277        assert!(saw_position);
278    }
279
280    #[test]
281    fn try_send_applies_synchronously_and_mirrors() {
282        // The synchronous seam the runtime's step calls: no async, immediate.
283        let hal = FakeHal::new();
284        let mut sub = hal.updates();
285        hal.try_send(&StateChange::set(
286            "joint1.target_position",
287            Value::from(1.0),
288        ));
289        let saw_position = drain(&mut sub)
290            .iter()
291            .any(|c| c.contains(&Key::from("joint1.position")));
292        assert!(
293            saw_position,
294            "try_send should mirror target to measured position"
295        );
296    }
297
298    #[tokio::test]
299    async fn read_absent_is_none() {
300        let hal = FakeHal::new();
301        assert_eq!(hal.read(&[Key::from("nope")]).await.unwrap(), vec![None]);
302    }
303
304    #[tokio::test]
305    async fn describe_and_glb() {
306        let hal = FakeHal::with_description(HalDescription {
307            model_family: Some("test".into()),
308            ..Default::default()
309        });
310        assert_eq!(hal.describe().await.model_family.as_deref(), Some("test"));
311        hal.set_model_glb(vec![1, 2, 3]);
312        assert_eq!(hal.model_glb().await.unwrap(), Some(vec![1, 2, 3]));
313    }
314}