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) {
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 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 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 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 assert_eq!(
265 hal.read(&[Key::from("joint1.position")]).await.unwrap(),
266 vec![Some(Value::from(1.0))]
267 );
268 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 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}