flowrs-engine/crates/flowrs-appshell/assets/robotics_link.rs
flowRS fd958c0547 flowRS engine, cut from flowRS 3451b065
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 19:04:39 -05:00

162 lines
5.4 KiB
Rust

// The items of an exported robotics project's `src/link.rs` (flowrs-appshell's `robotics`):
// the generator writes the module's header, these, and the project's `EDGES` table after them.
// Its tests run in every export (`cargo test`), over the export's own table:
// tests/robotics_interop.rs runs them on the build host.
use flowrs_device::HardwareError;
use flowrs_device::link::LinkError;
use std::collections::BTreeMap;
use std::time::Duration;
/// A value's type on an edge: the link channel's own.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Ty {
Bool,
F32,
U16,
U32,
Bytes,
}
/// One edge between the domains.
#[derive(Clone, Copy, Debug)]
pub struct Edge {
/// Its link channel.
pub id: u8,
/// Its name: the dora output and input, the Copper message's module.
pub name: &'static str,
pub ty: Ty,
/// Whether the MCU sends it.
pub from_mcu: bool,
/// Its interface's FIH01 hash (units, axes, clock, bounds and defaults included).
pub fih01: &'static str,
/// The unit its interface declares.
pub unit: Option<&'static str>,
/// The edge, in words.
pub words: &'static str,
}
/// The calls a generated pass makes on its link.
pub trait Link {
fn recv<T: serde::de::DeserializeOwned>(
&mut self,
channel: u8,
timeout: Duration,
) -> Result<T, LinkError<HardwareError>>;
fn recv_bytes(
&mut self,
channel: u8,
timeout: Duration,
) -> Result<Vec<u8>, LinkError<HardwareError>>;
fn recv_fragments(
&mut self,
first: u8,
count: u8,
timeout: Duration,
) -> Result<Vec<u8>, LinkError<HardwareError>> {
let _ = count;
self.recv_bytes(first, timeout)
}
fn send<T: serde::Serialize + ?Sized>(
&mut self,
channel: u8,
value: &T,
) -> Result<(), LinkError<HardwareError>>;
fn send_bytes(&mut self, channel: u8, bytes: &[u8]) -> Result<(), LinkError<HardwareError>>;
fn send_fragments(
&mut self,
first: u8,
count: u8,
bytes: &[u8],
) -> Result<(), LinkError<HardwareError>> {
let _ = count;
self.send_bytes(first, bytes)
}
}
/// What one pass takes and gives: the values the runtime put in before it ran, and the ones
/// it sent.
#[derive(Debug, Default)]
pub struct Mailbox {
pub inbox: BTreeMap<u8, Vec<u8>>,
pub outbox: BTreeMap<u8, Vec<u8>>,
}
impl Link for Mailbox {
fn recv<T: serde::de::DeserializeOwned>(
&mut self,
channel: u8,
_timeout: Duration,
) -> Result<T, LinkError<HardwareError>> {
let bytes = self.inbox.remove(&channel).ok_or(LinkError::Timeout)?;
postcard::from_bytes(&bytes).map_err(|_| LinkError::Decode)
}
fn recv_bytes(
&mut self,
channel: u8,
_timeout: Duration,
) -> Result<Vec<u8>, LinkError<HardwareError>> {
self.inbox.remove(&channel).ok_or(LinkError::Timeout)
}
fn send<T: serde::Serialize + ?Sized>(
&mut self,
channel: u8,
value: &T,
) -> Result<(), LinkError<HardwareError>> {
let bytes = postcard::to_allocvec(value).map_err(|_| LinkError::Encode)?;
self.outbox.insert(channel, bytes);
Ok(())
}
fn send_bytes(&mut self, channel: u8, bytes: &[u8]) -> Result<(), LinkError<HardwareError>> {
self.outbox.insert(channel, bytes.to_vec());
Ok(())
}
}
/// The edge on channel `id`.
pub fn edge(id: u8) -> Option<&'static Edge> {
EDGES.iter().find(|e| e.id == id)
}
/// A sender's digest is this side's, or the value is refused, naming the edge and both.
pub fn check_digest(e: &Edge, got: Option<&str>) -> Result<(), LinkError<HardwareError>> {
match got {
Some(d) if d == e.fih01 => Ok(()),
other => Err(LinkError::Serial(HardwareError::CommunicationError(format!(
"edge {}: the sender's interface digest is {}, this side's {}: refused",
e.name,
other.unwrap_or("none"),
e.fih01
)))),
}
}
#[cfg(test)]
mod tests {
use super::*;
/// A value whose interface digest is not its edge's own is refused, naming the edge and
/// both digests; the edge's own is taken. A pass's values go through as the link's postcard
/// bytes, typed both ways.
#[test]
fn the_link_refuses_another_interface_and_carries_typed_values() {
let e = &EDGES[0];
assert!(check_digest(e, Some(e.fih01)).is_ok());
let other = "FIH01_0000000000000000000000000000000000000000000000000000000000000000";
let why = format!("{:?}", check_digest(e, Some(other)).unwrap_err());
assert!(why.contains(e.name) && why.contains(other) && why.contains(e.fih01), "{why}");
assert!(check_digest(e, None).is_err());
assert!(edge(e.id).is_some() && edge(255).is_none());
let mut m = Mailbox::default();
m.inbox.insert(e.id, postcard::to_allocvec(&7u16).unwrap());
let v: u16 = m.recv(e.id, Duration::ZERO).unwrap();
assert_eq!(v, 7);
assert!(m.recv::<u16>(e.id, Duration::ZERO).is_err(), "a value is taken once");
m.send(e.id, &true).unwrap();
assert!(postcard::from_bytes::<bool>(&m.outbox[&e.id]).unwrap());
m.send_bytes(e.id, &[1, 2, 3]).unwrap();
assert_eq!(m.outbox[&e.id], vec![1, 2, 3]);
m.inbox.insert(e.id, vec![9, 8]);
assert_eq!(m.recv_fragments(e.id, 2, Duration::ZERO).unwrap(), vec![9, 8]);
}
}