1use 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum HalError {
52 Broken(String),
54 NoSuchKey(String),
56 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#[async_trait]
80pub trait Hal: Send + Sync {
81 async fn describe(&self) -> HalDescription;
83
84 async fn read(&self, keys: &[Key]) -> HalResult<Vec<Option<Value>>>;
87
88 async fn read_all(&self) -> HalResult<State>;
90
91 async fn write(&self, changes: StateChange) -> HalResult<()>;
94
95 fn try_send(&self, changes: &StateChange);
106
107 fn updates(&self) -> UpdatesStream;
112}
113
114pub type UpdatesStream = Pin<Box<dyn Stream<Item = StateChange> + Send>>;
117
118#[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#[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 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 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 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 assert_eq!(
270 hal.read(&[Key::from("joint1.position")]).await.unwrap(),
271 vec![Some(Value::from(1.0))]
272 );
273 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 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}