Skip to main content

rtcom_core/
device.rs

1//! Asynchronous serial device abstraction and default `serialport` backend.
2//!
3//! The [`SerialDevice`] trait is the narrow runtime contract every backend —
4//! real hardware, in-memory mock, pseudo-terminal loopback — must satisfy.
5//! The rest of `rtcom-core` (event bus, session orchestrator, mappers) never
6//! references a concrete serial implementation, which keeps testing and
7//! future backends (e.g. a TCP passthrough, a simulated device) cheap.
8//!
9//! [`SerialPortDevice`] is the stock implementation, layered on top of
10//! [`tokio_serial`] so reads and writes are driven by the tokio reactor
11//! rather than a per-port blocking thread.
12
13use std::pin::Pin;
14use std::task::{Context, Poll};
15use std::thread;
16use std::time::Duration;
17
18use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
19use tokio_serial::{SerialPort, SerialPortBuilderExt, SerialStream};
20
21use crate::config::{DataBits, FlowControl, ModemStatus, Parity, SerialConfig, StopBits};
22use crate::error::Result;
23
24/// Runtime contract for every serial backend used by `rtcom-core`.
25///
26/// Implementors supply full-duplex async I/O (via [`AsyncRead`] +
27/// [`AsyncWrite`]) plus the control-plane operations needed for interactive
28/// sessions: baud / framing changes, DTR/RTS toggling, line-break injection,
29/// and a modem-status snapshot.
30///
31/// # Examples
32///
33/// ```no_run
34/// use rtcom_core::{SerialConfig, SerialDevice, SerialPortDevice};
35/// use tokio::io::AsyncWriteExt;
36///
37/// # async fn example() -> rtcom_core::Result<()> {
38/// let mut port = SerialPortDevice::open("/dev/ttyUSB0", SerialConfig::default())?;
39/// port.write_all(b"AT\r\n").await?;
40/// # Ok(()) }
41/// ```
42pub trait SerialDevice: AsyncRead + AsyncWrite + Send + Unpin {
43    /// Changes the baud rate at runtime.
44    ///
45    /// Successful calls also update the cached [`SerialConfig`] returned by
46    /// [`config`](Self::config).
47    ///
48    /// # Errors
49    ///
50    /// Returns an error if the underlying driver rejects the rate (e.g. the
51    /// hardware cannot produce the requested divisor).
52    fn set_baud_rate(&mut self, baud: u32) -> Result<()>;
53
54    /// Changes the data-bit width.
55    ///
56    /// # Errors
57    ///
58    /// Propagates backend errors if the new setting cannot be applied.
59    fn set_data_bits(&mut self, bits: DataBits) -> Result<()>;
60
61    /// Changes the stop-bit count.
62    ///
63    /// # Errors
64    ///
65    /// Propagates backend errors if the new setting cannot be applied.
66    fn set_stop_bits(&mut self, bits: StopBits) -> Result<()>;
67
68    /// Changes the parity mode.
69    ///
70    /// # Errors
71    ///
72    /// Propagates backend errors if the new setting cannot be applied. Some
73    /// platforms reject [`Parity::Mark`] / [`Parity::Space`].
74    fn set_parity(&mut self, parity: Parity) -> Result<()>;
75
76    /// Changes the flow-control mode.
77    ///
78    /// # Errors
79    ///
80    /// Propagates backend errors if the new setting cannot be applied.
81    fn set_flow_control(&mut self, flow: FlowControl) -> Result<()>;
82
83    /// Drives the DTR output line to `level` (`true` = asserted).
84    ///
85    /// # Errors
86    ///
87    /// Propagates backend errors if the line cannot be toggled.
88    fn set_dtr(&mut self, level: bool) -> Result<()>;
89
90    /// Drives the RTS output line to `level` (`true` = asserted).
91    ///
92    /// # Errors
93    ///
94    /// Propagates backend errors if the line cannot be toggled.
95    fn set_rts(&mut self, level: bool) -> Result<()>;
96
97    /// Asserts a line break for `duration`.
98    ///
99    /// The call blocks the current thread for the duration of the break. In
100    /// async contexts, schedule it via [`tokio::task::spawn_blocking`] if the
101    /// duration is long enough to matter.
102    ///
103    /// # Errors
104    ///
105    /// Propagates backend errors if the break cannot be asserted or cleared.
106    fn send_break(&mut self, duration: Duration) -> Result<()>;
107
108    /// Reads the current input-side modem-status lines.
109    ///
110    /// Takes `&mut self` because the underlying [`serialport`] API does: the
111    /// OS read may update internal driver state.
112    ///
113    /// # Errors
114    ///
115    /// Propagates backend errors if the modem status register cannot be read.
116    fn modem_status(&mut self) -> Result<ModemStatus>;
117
118    /// Returns the most recently applied [`SerialConfig`].
119    ///
120    /// This is always in sync with successful calls to the `set_*` methods;
121    /// it may diverge from the hardware if an external process reconfigures
122    /// the port behind our back.
123    fn config(&self) -> &SerialConfig;
124}
125
126/// Default [`SerialDevice`] backed by [`tokio_serial::SerialStream`].
127///
128/// Use [`SerialPortDevice::open`] to create one from a device path. On Unix,
129/// [`SerialPortDevice::pair`] creates a connected pseudo-terminal pair that is
130/// convenient for integration tests without real hardware.
131pub struct SerialPortDevice {
132    stream: SerialStream,
133    config: SerialConfig,
134}
135
136impl SerialPortDevice {
137    /// Opens the device at `path` with the supplied `config`.
138    ///
139    /// # Errors
140    ///
141    /// Returns [`Error::InvalidConfig`](crate::Error::InvalidConfig) if
142    /// `config` fails [`SerialConfig::validate`](SerialConfig::validate), and
143    /// [`Error::Backend`](crate::Error::Backend) if the port cannot be opened
144    /// or configured.
145    pub fn open(path: &str, config: SerialConfig) -> Result<Self> {
146        config.validate()?;
147        let stream = tokio_serial::new(path, config.baud_rate)
148            .data_bits(to_sp_data_bits(config.data_bits))
149            .stop_bits(to_sp_stop_bits(config.stop_bits))
150            .parity(to_sp_parity(config.parity))
151            .flow_control(to_sp_flow(config.flow_control))
152            .timeout(config.read_timeout)
153            .open_native_async()?;
154        Ok(Self { stream, config })
155    }
156
157    /// Creates a connected pseudo-terminal pair for testing. **Unix only.**
158    ///
159    /// Both ends are returned with [`SerialConfig::default`] cached; the
160    /// baud-rate setting does not meaningfully affect a PTY but round-trip
161    /// writes/reads work.
162    ///
163    /// # Errors
164    ///
165    /// Returns [`Error::Backend`](crate::Error::Backend) if the kernel cannot
166    /// allocate a PTY pair.
167    #[cfg(unix)]
168    pub fn pair() -> Result<(Self, Self)> {
169        let (a, b) = SerialStream::pair()?;
170        let config = SerialConfig::default();
171        Ok((Self { stream: a, config }, Self { stream: b, config }))
172    }
173}
174
175impl AsyncRead for SerialPortDevice {
176    fn poll_read(
177        mut self: Pin<&mut Self>,
178        cx: &mut Context<'_>,
179        buf: &mut ReadBuf<'_>,
180    ) -> Poll<std::io::Result<()>> {
181        Pin::new(&mut self.stream).poll_read(cx, buf)
182    }
183}
184
185impl AsyncWrite for SerialPortDevice {
186    fn poll_write(
187        mut self: Pin<&mut Self>,
188        cx: &mut Context<'_>,
189        buf: &[u8],
190    ) -> Poll<std::io::Result<usize>> {
191        Pin::new(&mut self.stream).poll_write(cx, buf)
192    }
193
194    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
195        Pin::new(&mut self.stream).poll_flush(cx)
196    }
197
198    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
199        Pin::new(&mut self.stream).poll_shutdown(cx)
200    }
201}
202
203impl SerialDevice for SerialPortDevice {
204    fn set_baud_rate(&mut self, baud: u32) -> Result<()> {
205        if baud == 0 {
206            return Err(crate::Error::InvalidConfig(
207                "baud_rate must be non-zero".into(),
208            ));
209        }
210        self.stream.set_baud_rate(baud)?;
211        self.config.baud_rate = baud;
212        Ok(())
213    }
214
215    fn set_data_bits(&mut self, bits: DataBits) -> Result<()> {
216        self.stream.set_data_bits(to_sp_data_bits(bits))?;
217        self.config.data_bits = bits;
218        Ok(())
219    }
220
221    fn set_stop_bits(&mut self, bits: StopBits) -> Result<()> {
222        self.stream.set_stop_bits(to_sp_stop_bits(bits))?;
223        self.config.stop_bits = bits;
224        Ok(())
225    }
226
227    fn set_parity(&mut self, parity: Parity) -> Result<()> {
228        self.stream.set_parity(to_sp_parity(parity))?;
229        self.config.parity = parity;
230        Ok(())
231    }
232
233    fn set_flow_control(&mut self, flow: FlowControl) -> Result<()> {
234        self.stream.set_flow_control(to_sp_flow(flow))?;
235        self.config.flow_control = flow;
236        Ok(())
237    }
238
239    fn set_dtr(&mut self, level: bool) -> Result<()> {
240        self.stream.write_data_terminal_ready(level)?;
241        Ok(())
242    }
243
244    fn set_rts(&mut self, level: bool) -> Result<()> {
245        self.stream.write_request_to_send(level)?;
246        Ok(())
247    }
248
249    fn send_break(&mut self, duration: Duration) -> Result<()> {
250        self.stream.set_break()?;
251        thread::sleep(duration);
252        self.stream.clear_break()?;
253        Ok(())
254    }
255
256    fn modem_status(&mut self) -> Result<ModemStatus> {
257        Ok(ModemStatus {
258            cts: self.stream.read_clear_to_send()?,
259            dsr: self.stream.read_data_set_ready()?,
260            ri: self.stream.read_ring_indicator()?,
261            cd: self.stream.read_carrier_detect()?,
262        })
263    }
264
265    fn config(&self) -> &SerialConfig {
266        &self.config
267    }
268}
269
270const fn to_sp_data_bits(b: DataBits) -> serialport::DataBits {
271    match b {
272        DataBits::Five => serialport::DataBits::Five,
273        DataBits::Six => serialport::DataBits::Six,
274        DataBits::Seven => serialport::DataBits::Seven,
275        DataBits::Eight => serialport::DataBits::Eight,
276    }
277}
278
279const fn to_sp_stop_bits(b: StopBits) -> serialport::StopBits {
280    match b {
281        StopBits::One => serialport::StopBits::One,
282        StopBits::Two => serialport::StopBits::Two,
283    }
284}
285
286// Mark/Space parity are not represented in the serialport crate; map them to
287// None and let a future backend-specific hook override if a driver supports
288// them natively.
289const fn to_sp_parity(p: Parity) -> serialport::Parity {
290    match p {
291        Parity::Even => serialport::Parity::Even,
292        Parity::Odd => serialport::Parity::Odd,
293        Parity::None | Parity::Mark | Parity::Space => serialport::Parity::None,
294    }
295}
296
297const fn to_sp_flow(f: FlowControl) -> serialport::FlowControl {
298    match f {
299        FlowControl::None => serialport::FlowControl::None,
300        FlowControl::Hardware => serialport::FlowControl::Hardware,
301        FlowControl::Software => serialport::FlowControl::Software,
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn data_bits_round_trip() {
311        assert_eq!(
312            to_sp_data_bits(DataBits::Eight),
313            serialport::DataBits::Eight
314        );
315        assert_eq!(to_sp_data_bits(DataBits::Five), serialport::DataBits::Five);
316    }
317
318    #[test]
319    fn stop_bits_round_trip() {
320        assert_eq!(to_sp_stop_bits(StopBits::One), serialport::StopBits::One);
321        assert_eq!(to_sp_stop_bits(StopBits::Two), serialport::StopBits::Two);
322    }
323
324    #[test]
325    fn parity_round_trip() {
326        assert_eq!(to_sp_parity(Parity::Even), serialport::Parity::Even);
327        assert_eq!(to_sp_parity(Parity::Odd), serialport::Parity::Odd);
328        assert_eq!(to_sp_parity(Parity::None), serialport::Parity::None);
329    }
330
331    #[test]
332    fn flow_round_trip() {
333        assert_eq!(to_sp_flow(FlowControl::None), serialport::FlowControl::None);
334        assert_eq!(
335            to_sp_flow(FlowControl::Hardware),
336            serialport::FlowControl::Hardware
337        );
338        assert_eq!(
339            to_sp_flow(FlowControl::Software),
340            serialport::FlowControl::Software
341        );
342    }
343
344    #[cfg(unix)]
345    #[tokio::test]
346    async fn pair_returns_default_config() {
347        let (a, b) = SerialPortDevice::pair().expect("pty pair");
348        assert_eq!(a.config(), &SerialConfig::default());
349        assert_eq!(b.config(), &SerialConfig::default());
350    }
351}