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