pamoja_core/device.rs
1//! Device-model traits implemented by capability crates.
2//!
3//! These traits describe the roles a piece of hardware can play in an
4//! application: a connectable [`Device`], a [`Sensor`] that produces readings, an
5//! [`Actuator`] that accepts commands, and a [`Telemetry`] source that streams
6//! frames. A single type may implement more than one of them.
7//!
8//! The futures these traits return are `Send`, as a [`Transport`](crate::Transport)'s
9//! are, so a node built from any sensor and actuator can be driven from a task on a
10//! multi-threaded runtime and erased behind a trait object that is. An implementation
11//! written as `async fn` satisfies this as long as everything it holds across an await
12//! is `Send`; a driver generic over a bus says so with a `Send` bound on the bus.
13
14use core::future::Future;
15
16use crate::adapt::{Map, MapCommand};
17use crate::error::Result;
18
19/// A connectable physical or virtual device.
20///
21/// Implementors manage the lifecycle of an underlying resource such as a serial
22/// port, a network socket, or a vehicle link.
23pub trait Device {
24 /// Returns the stable identifier for this device.
25 ///
26 /// The identifier is expected to remain constant for the lifetime of the
27 /// device, for example a serial number, MAC address, or vehicle URI.
28 ///
29 /// # Returns
30 ///
31 /// A string slice borrowing the device's identifier.
32 fn id(&self) -> &str;
33
34 /// Opens the device and prepares it for use.
35 ///
36 /// # Returns
37 ///
38 /// `Ok(())` once the device is connected and ready.
39 ///
40 /// # Errors
41 ///
42 /// Returns [`Error::Io`](crate::Error::Io) if the underlying resource cannot
43 /// be opened, or [`Error::Transport`](crate::Error::Transport) if a link to
44 /// the device cannot be established.
45 fn connect(&mut self) -> impl Future<Output = Result<()>> + Send;
46
47 /// Releases the device and any resources it holds.
48 ///
49 /// # Returns
50 ///
51 /// `Ok(())` once the device has been disconnected and its resources freed.
52 ///
53 /// # Errors
54 ///
55 /// Returns [`Error::Io`](crate::Error::Io) if the underlying resource cannot
56 /// be released cleanly.
57 fn disconnect(&mut self) -> impl Future<Output = Result<()>> + Send;
58}
59
60/// A source of typed readings, such as a thermometer, GPS receiver, or lidar.
61pub trait Sensor {
62 /// The value produced by a single read, for example a temperature or a fix.
63 type Reading;
64
65 /// Takes a single reading from the sensor.
66 ///
67 /// # Returns
68 ///
69 /// The next [`Reading`](Self::Reading) sampled from the sensor.
70 ///
71 /// # Errors
72 ///
73 /// Returns [`Error::Io`](crate::Error::Io) if the sensor cannot be read, or
74 /// [`Error::Closed`](crate::Error::Closed) if the sensor has been
75 /// disconnected.
76 fn read(&mut self) -> impl Future<Output = Result<Self::Reading>> + Send;
77
78 /// Wraps this sensor so every reading passes through `select` first.
79 ///
80 /// This is how a driver that measures several channels feeds a consumer that
81 /// wants one number: `select` picks the channel, converts the unit, or combines
82 /// fields, and the result is a [`Sensor`] whose reading is whatever `select`
83 /// returns. See [`Map`] for an example.
84 ///
85 /// # Arguments
86 ///
87 /// * `select` - the function applied to each reading as it is taken.
88 ///
89 /// # Returns
90 ///
91 /// The adapted sensor.
92 fn map<F, T>(self, select: F) -> Map<Self, F>
93 where
94 Self: Sized,
95 F: FnMut(Self::Reading) -> T,
96 {
97 Map::new(self, select)
98 }
99}
100
101/// A sink that accepts typed commands, such as a motor or a valve.
102pub trait Actuator {
103 /// The command accepted by a single application, for example a setpoint.
104 type Command;
105
106 /// Applies a command to the actuator.
107 ///
108 /// # Arguments
109 ///
110 /// * `command` - the command to apply, consumed by the call.
111 ///
112 /// # Returns
113 ///
114 /// `Ok(())` once the command has been accepted by the actuator.
115 ///
116 /// # Errors
117 ///
118 /// Returns [`Error::Io`](crate::Error::Io) if the command cannot be
119 /// delivered, or [`Error::Closed`](crate::Error::Closed) if the actuator has
120 /// been disconnected.
121 fn apply(&mut self, command: Self::Command) -> impl Future<Output = Result<()>> + Send;
122
123 /// Wraps this actuator so every command passes through `convert` first.
124 ///
125 /// This is how a part that takes its own command shape is driven by a consumer
126 /// that has a simpler one: a profile's `bool` becomes a pulse width, a percentage
127 /// becomes a duty. The result is an [`Actuator`] whose command is whatever
128 /// `convert` accepts. See [`MapCommand`] for an example.
129 ///
130 /// # Arguments
131 ///
132 /// * `convert` - the function turning the caller's command into this part's.
133 ///
134 /// # Returns
135 ///
136 /// The adapted actuator.
137 fn map_command<F, C>(self, convert: F) -> MapCommand<Self, F, C>
138 where
139 Self: Sized,
140 F: FnMut(C) -> Self::Command,
141 {
142 MapCommand::new(self, convert)
143 }
144}
145
146/// A device that emits a continuous stream of telemetry frames.
147pub trait Telemetry {
148 /// A single telemetry frame, for example a status or position report.
149 type Frame;
150
151 /// Awaits the next telemetry frame.
152 ///
153 /// # Returns
154 ///
155 /// `Some(frame)` when a frame is available, or `None` once the telemetry
156 /// stream has ended.
157 ///
158 /// # Errors
159 ///
160 /// Returns [`Error::Transport`](crate::Error::Transport) if the telemetry
161 /// link fails while waiting.
162 fn next_frame(&mut self) -> impl Future<Output = Result<Option<Self::Frame>>> + Send;
163}