1use 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum HalError {
43 Broken(String),
45 NoSuchKey(String),
47 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#[async_trait]
70pub trait Hal: Send + Sync {
71 async fn describe(&self) -> HalDescription;
73
74 async fn read(&self, keys: &[Key]) -> HalResult<Vec<Option<Value>>>;
77
78 async fn read_all(&self) -> HalResult<State>;
80
81 async fn write(&self, changes: StateChange) -> HalResult<()>;
84
85 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 #[cfg(target_arch = "wasm32")]
103 {
104 let _ = changes;
105 }
106 }
107
108 fn updates(&self) -> Subscription;
110}
111
112#[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#[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 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 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 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 assert_eq!(
249 hal.read(&[Key::from("joint1.position")]).await.unwrap(),
250 vec![Some(Value::from(1.0))]
251 );
252 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 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}