Skip to main content

serialport_stream/
lib.rs

1//! Async serial port I/O as [`Stream`], [`AsyncRead`], and [`AsyncWrite`].
2//!
3//! Async runtime agnostic: this crate implements [`futures`] traits only and does not depend on
4//! Tokio, async-std, or any other executor. Use it with any runtime that polls those futures
5//! (Tokio, async-std, [`futures_lite::future::block_on`], etc.).
6//!
7//! Configure and open ports with [`new`] → [`SerialPortStreamBuilder`] → [`.open()`](SerialPortStreamBuilder::open).
8//! Line settings, DTR, and buffer clearing are applied at open time.
9//!
10//! POSIX `termios` on Unix; Win32 COMM APIs on Windows. Configuration types ([`DataBits`],
11//! [`Parity`], [`StopBits`], [`FlowControl`], [`ClearBuffer`]) are defined in this crate.
12//!
13//! Optional [`tracing`] logs (EAGAIN/EINTR retries, receive-buffer diagnostics) are enabled with
14//! the `tracing` Cargo feature.
15//!
16//! The first read poll starts a background thread that appends incoming bytes to an in-memory FIFO
17//! shared by [`Stream`] and [`AsyncRead`]. There is no backpressure.
18//!
19//! [`Stream`] / [`TryStreamExt::try_next`] drains the full FIFO per item; [`AsyncRead`] reads
20//! partially and leaves the remainder cached. Use one read style per open port.
21//!
22//! [`AsyncWriteExt`], [`AsyncReadExt`], and [`TryStreamExt`] are re-exported from `futures`.
23//!
24//! ```no_run
25//! use serialport_stream::{new, AsyncWriteExt, TryStreamExt};
26//!
27//! # async fn example() -> std::io::Result<()> {
28//! let mut stream = new("/dev/ttyUSB0", 115200).open()?;
29//! stream.write_all(b"PING\r\n").await?;
30//! while let Some(chunk) = stream.try_next().await? {
31//!     println!("{chunk:?}");
32//! }
33//! # Ok(())
34//! # }
35//! ```
36//!
37
38use std::future::Future;
39use std::pin::Pin;
40use std::sync::{Arc, Mutex};
41use std::task::{Context, Poll};
42
43macro_rules! trace_info {
44    ($($tt:tt)*) => {
45        #[cfg(feature = "tracing")]
46        tracing::info!($($tt)*);
47    };
48}
49
50mod platform;
51mod types;
52
53pub mod line_settings;
54pub use types::{ClearBuffer, DataBits, FlowControl, Parity, StopBits};
55
56use crate::platform::PlatformStream;
57use futures::task::AtomicWaker;
58
59fn clone_io_error(err: &std::io::Error) -> std::io::Error {
60    match err.raw_os_error() {
61        Some(code) => std::io::Error::from_raw_os_error(code),
62        None => std::io::Error::new(err.kind(), err.to_string()),
63    }
64}
65
66pub use futures::io::{AsyncRead, AsyncReadExt};
67pub use futures::io::{AsyncWrite, AsyncWriteExt};
68pub use futures::stream::{Stream, TryStreamExt};
69
70#[derive(Debug)]
71pub(crate) struct EventsInnerRead {
72    pub(crate) in_buffer: Mutex<Vec<u8>>,
73    pub(crate) stream_error: Mutex<Option<std::io::Error>>,
74    pub(crate) waker: AtomicWaker,
75}
76
77impl EventsInnerRead {
78    pub(crate) fn new() -> Self {
79        Self {
80            in_buffer: Mutex::new(Vec::new()),
81            stream_error: Mutex::new(None),
82            waker: AtomicWaker::new(),
83        }
84    }
85}
86
87#[derive(Debug)]
88pub(crate) struct EventsInnerWrite {
89    pub(crate) write_error: Mutex<Option<std::io::Error>>,
90    pub(crate) waker: AtomicWaker,
91}
92
93impl EventsInnerWrite {
94    pub(crate) fn new() -> Self {
95        Self {
96            write_error: Mutex::new(None),
97            waker: AtomicWaker::new(),
98        }
99    }
100}
101
102/// Builder for serial port path, line settings, and one-shot open options.
103///
104/// Created with [`new()`], configured with chained methods, then finalized with
105/// [`open()`](SerialPortStreamBuilder::open).
106///
107/// # Example
108///
109/// ```no_run
110/// use serialport_stream::new;
111/// use serialport_stream::{ClearBuffer, DataBits, FlowControl, Parity, StopBits};
112///
113/// # fn example() -> std::io::Result<()> {
114/// let stream = new("/dev/ttyUSB0", 115200)
115///     .data_bits(DataBits::Eight)
116///     .parity(Parity::None)
117///     .stop_bits(StopBits::One)
118///     .flow_control(FlowControl::None)
119///     .dtr_on_open(true)
120///     .clear(ClearBuffer::All)
121///     .open()?;
122/// # Ok(())
123/// # }
124/// ```
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct SerialPortStreamBuilder {
127    pub(crate) path: String,
128    pub(crate) baud_rate: u32,
129    pub(crate) data_bits: DataBits,
130    pub(crate) flow_control: FlowControl,
131    pub(crate) parity: Parity,
132    pub(crate) stop_bits: StopBits,
133    pub(crate) dtr_on_open: bool,
134    pub(crate) clear_buffer: Option<ClearBuffer>,
135}
136
137impl SerialPortStreamBuilder {
138    /// Sets the path to the serial port device.
139    ///
140    /// # Examples
141    /// - Unix: `"/dev/ttyUSB0"`, `"/dev/ttyACM0"`
142    /// - Windows: `"COM3"`, `"COM10"`
143    #[allow(clippy::assigning_clones)]
144    #[must_use]
145    pub fn path<'a>(mut self, path: impl Into<std::borrow::Cow<'a, str>>) -> Self {
146        self.path = path.into().as_ref().to_owned();
147        self
148    }
149
150    /// Sets the baud rate (bits per second).
151    ///
152    /// Common values: 9600, 19200, 38400, 57600, 115200
153    #[must_use]
154    pub fn baud_rate(mut self, baud_rate: u32) -> Self {
155        self.baud_rate = baud_rate;
156        self
157    }
158
159    /// Sets the number of data bits per character.
160    ///
161    /// Default: `DataBits::Eight`
162    #[must_use]
163    pub fn data_bits(mut self, data_bits: DataBits) -> Self {
164        self.data_bits = data_bits;
165        self
166    }
167
168    /// Sets the flow control mode.
169    ///
170    /// Default: `FlowControl::None`
171    #[must_use]
172    pub fn flow_control(mut self, flow_control: FlowControl) -> Self {
173        self.flow_control = flow_control;
174        self
175    }
176
177    /// Sets the parity checking mode.
178    ///
179    /// Default: `Parity::None`
180    #[must_use]
181    pub fn parity(mut self, parity: Parity) -> Self {
182        self.parity = parity;
183        self
184    }
185
186    /// Sets the number of stop bits.
187    ///
188    /// Default: `StopBits::One`
189    #[must_use]
190    pub fn stop_bits(mut self, stop_bits: StopBits) -> Self {
191        self.stop_bits = stop_bits;
192        self
193    }
194
195    /// Sets the DTR (Data Terminal Ready) signal state applied when opening the port.
196    ///
197    /// Default: `false`
198    #[must_use]
199    pub fn dtr_on_open(mut self, state: bool) -> Self {
200        self.dtr_on_open = state;
201        self
202    }
203
204    /// Clears RX and/or TX driver buffers when the port is opened, before async I/O starts.
205    ///
206    /// See [`ClearBuffer`] (`Input`, `Output`, or `All`).
207    #[must_use]
208    pub fn clear(mut self, buffer: ClearBuffer) -> Self {
209        self.clear_buffer = Some(buffer);
210        self
211    }
212
213    /// Opens the serial port and returns a [`SerialPortStream`].
214    ///
215    /// Applies line settings, [`dtr_on_open`](Self::dtr_on_open), and optional
216    /// [`clear`](Self::clear) before any background read/write threads are started.
217    pub fn open(self) -> std::io::Result<SerialPortStream> {
218        let read_inner = Arc::new(EventsInnerRead::new());
219        let write_inner = Arc::new(EventsInnerWrite::new());
220        Ok(SerialPortStream {
221            platform: PlatformStream::new(self, read_inner.clone(), write_inner.clone())?,
222            read_inner,
223            write_inner,
224            flush_task: None,
225            write_in_flight: false,
226        })
227    }
228}
229
230/// Creates a [`SerialPortStreamBuilder`] with default line settings (8N1, no flow control).
231///
232/// # Examples
233///
234/// Unix device path:
235///
236/// ```no_run
237/// # use serialport_stream::new;
238/// # fn example() -> std::io::Result<()> {
239/// let _stream = new("/dev/ttyUSB0", 115200).open()?;
240/// # Ok(())
241/// # }
242/// ```
243///
244/// Windows COM port:
245///
246/// ```no_run
247/// # use serialport_stream::new;
248/// # fn example() -> std::io::Result<()> {
249/// let _stream = new("COM3", 9600).open()?;
250/// # Ok(())
251/// # }
252/// ```
253pub fn new<'a>(
254    path: impl Into<std::borrow::Cow<'a, str>>,
255    baud_rate: u32,
256) -> SerialPortStreamBuilder {
257    SerialPortStreamBuilder {
258        path: path.into().into_owned(),
259        baud_rate,
260        data_bits: DataBits::Eight,
261        flow_control: FlowControl::None,
262        parity: Parity::None,
263        stop_bits: StopBits::One,
264        dtr_on_open: false,
265        clear_buffer: None,
266    }
267}
268
269/// An opened serial port for async reads and writes.
270///
271/// - [`Stream`] / [`AsyncRead`]: shared in-memory receive FIFO (background read thread).
272/// - [`AsyncWrite`]: dedicated background write thread.
273///
274/// # Example
275///
276/// ```no_run
277/// use serialport_stream::new;
278/// use futures::io::AsyncWriteExt;
279/// use futures::stream::TryStreamExt;
280///
281/// # async fn example() -> std::io::Result<()> {
282/// let mut stream = new("COM3", 115200).open()?;
283/// stream.write_all(&[0x0a, 0xC0]).await?;
284/// if let Some(bytes) = stream.try_next().await? {
285///     println!("{bytes:?}");
286/// }
287/// # Ok(())
288/// # }
289/// ```
290pub struct SerialPortStream {
291    platform: PlatformStream,
292    read_inner: Arc<EventsInnerRead>,
293    write_inner: Arc<EventsInnerWrite>,
294    flush_task: Option<blocking::Task<std::io::Result<()>>>,
295    write_in_flight: bool,
296}
297
298impl std::fmt::Debug for SerialPortStream {
299    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300        f.debug_struct("SerialPortStream")
301            .field("platform", &self.platform)
302            .field("read_inner", &self.read_inner)
303            .field("write_inner", &self.write_inner)
304            .field("flush_task", &self.flush_task.as_ref().map(|_| "..."))
305            .field("write_in_flight", &self.write_in_flight)
306            .finish()
307    }
308}
309
310impl SerialPortStream {
311    fn poll_receiver_ready(&mut self, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
312        self.read_inner.waker.register(cx.waker());
313
314        if let Some(err) = self.read_inner.stream_error.lock().unwrap().as_ref() {
315            return Poll::Ready(Err(clone_io_error(err)));
316        }
317
318        if !self.platform.is_read_thread_started() {
319            self.platform.start_read_thread();
320            return Poll::Pending;
321        }
322
323        Poll::Ready(Ok(()))
324    }
325
326    fn poll_writer_ready(&mut self, cx: &mut Context<'_>) -> std::io::Result<()> {
327        self.write_inner.waker.register(cx.waker());
328
329        if let Some(err) = self.write_inner.write_error.lock().unwrap().as_ref() {
330            return Err(clone_io_error(err));
331        }
332
333        if !self.platform.is_write_thread_started() {
334            self.platform.start_write_thread();
335        }
336
337        Ok(())
338    }
339
340    /// Polls for the next received chunk, same as [`Stream::poll_next`].
341    ///
342    /// When ready, returns `Poll::Ready(Some(Ok(vec)))` with every byte currently buffered,
343    /// or `Poll::Pending` if the read thread has not yet delivered data.
344    pub fn try_poll_next(
345        &mut self,
346        cx: &mut Context<'_>,
347    ) -> Poll<Option<Result<Vec<u8>, std::io::Error>>> {
348        match self.poll_receiver_ready(cx) {
349            Poll::Pending => Poll::Pending,
350            Poll::Ready(Err(e)) => Poll::Ready(Some(Err(e))),
351            Poll::Ready(Ok(())) => {
352                let mut buffer = self.read_inner.in_buffer.lock().unwrap();
353                if !buffer.is_empty() {
354                    // Drain all available data
355                    let data = buffer.drain(..).collect();
356                    return Poll::Ready(Some(Ok(data)));
357                }
358
359                Poll::Pending
360            }
361        }
362    }
363
364    /// Sets the baud rate (bits per second) on an already-open port.
365    ///
366    /// Other line settings (data bits, parity, stop bits, flow control) remain unchanged.
367    pub fn set_baudrate(&mut self, baud_rate: u32) -> std::io::Result<()> {
368        self.platform.set_baud_rate(baud_rate)
369    }
370}
371
372unsafe impl Send for SerialPortStream {}
373
374unsafe impl Sync for SerialPortStream {}
375
376impl AsyncRead for SerialPortStream {
377    fn poll_read(
378        mut self: Pin<&mut Self>,
379        cx: &mut Context<'_>,
380        buf: &mut [u8],
381    ) -> Poll<std::io::Result<usize>> {
382        assert!(!buf.is_empty());
383        let this = self.as_mut().get_mut();
384        match this.poll_receiver_ready(cx) {
385            Poll::Pending => Poll::Pending,
386            Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
387            Poll::Ready(Ok(())) => {
388                let mut buffer = this.read_inner.in_buffer.lock().unwrap();
389                if buffer.is_empty() {
390                    return Poll::Pending;
391                }
392                let n = buf.len().min(buffer.len());
393                buf[..n].copy_from_slice(&buffer[..n]);
394                buffer.drain(..n);
395                let cached_bytes = buffer.len();
396                if cached_bytes > 0 {
397                    trace_info!(
398                        read_bytes = n,
399                        cached_bytes,
400                        "serialport-stream receive buffer after AsyncRead read"
401                    );
402                }
403                Poll::Ready(Ok(n))
404            }
405        }
406    }
407}
408
409impl Stream for SerialPortStream {
410    type Item = Result<Vec<u8>, std::io::Error>;
411
412    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
413        self.try_poll_next(cx)
414    }
415}
416
417impl AsyncWrite for SerialPortStream {
418    fn poll_write(
419        mut self: Pin<&mut Self>,
420        cx: &mut Context<'_>,
421        buf: &[u8],
422    ) -> Poll<std::io::Result<usize>> {
423        assert!(!buf.is_empty());
424        let this = self.as_mut().get_mut();
425        let result = match this.poll_writer_ready(cx) {
426            Err(e) => Poll::Ready(Err(e)),
427            Ok(()) => this.platform.poll_write(buf),
428        };
429        // A pending write has been started but not yet finished; `poll_flush` waits on this.
430        this.write_in_flight = result.is_pending();
431        result
432    }
433
434    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
435        let this = self.as_mut().get_mut();
436
437        this.write_inner.waker.register(cx.waker());
438
439        if let Some(err) = this.write_inner.write_error.lock().unwrap().as_ref() {
440            return Poll::Ready(Err(clone_io_error(err)));
441        }
442
443        if this.write_in_flight {
444            return Poll::Pending;
445        }
446
447        if this.flush_task.is_none() {
448            this.flush_task = Some(this.platform.flush_tx_unblocked());
449        }
450
451        let task = this.flush_task.as_mut().expect("flush task");
452        match Pin::new(task).poll(cx) {
453            Poll::Ready(result) => {
454                this.flush_task = None;
455                Poll::Ready(result)
456            }
457            Poll::Pending => Poll::Pending,
458        }
459    }
460
461    fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
462        self.poll_flush(cx)
463    }
464}