├── .gitignore ├── .github ├── com0com.cer ├── NOTES.md └── workflows │ └── github-ci.yml ├── LICENSE ├── examples ├── serial_println.rs └── filter_play_sound.rs ├── README.md ├── Cargo.toml ├── tests └── test_serialstream.rs ├── CHANGELOG.md └── src ├── frame.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .idea 4 | 5 | *.bk 6 | -------------------------------------------------------------------------------- /.github/com0com.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/berkowski/tokio-serial/HEAD/.github/com0com.cer -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Zac Berkowitz and other tokio-serial contributors. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /examples/serial_println.rs: -------------------------------------------------------------------------------- 1 | #![warn(rust_2018_idioms)] 2 | 3 | use futures::stream::StreamExt; 4 | use std::{env, io, str}; 5 | use tokio_util::codec::{Decoder, Encoder}; 6 | 7 | use bytes::BytesMut; 8 | use tokio_serial::SerialPortBuilderExt; 9 | 10 | #[cfg(unix)] 11 | const DEFAULT_TTY: &str = "/dev/ttyUSB0"; 12 | #[cfg(windows)] 13 | const DEFAULT_TTY: &str = "COM1"; 14 | 15 | struct LineCodec; 16 | 17 | impl Decoder for LineCodec { 18 | type Item = String; 19 | type Error = io::Error; 20 | 21 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 22 | let newline = src.as_ref().iter().position(|b| *b == b'\n'); 23 | if let Some(n) = newline { 24 | let line = src.split_to(n + 1); 25 | return match str::from_utf8(line.as_ref()) { 26 | Ok(s) => Ok(Some(s.to_string())), 27 | Err(_) => Err(io::Error::new(io::ErrorKind::Other, "Invalid String")), 28 | }; 29 | } 30 | Ok(None) 31 | } 32 | } 33 | 34 | impl Encoder for LineCodec { 35 | type Error = io::Error; 36 | 37 | fn encode(&mut self, _item: String, _dst: &mut BytesMut) -> Result<(), Self::Error> { 38 | Ok(()) 39 | } 40 | } 41 | 42 | #[tokio::main] 43 | async fn main() -> tokio_serial::Result<()> { 44 | let mut args = env::args(); 45 | let tty_path = args.nth(1).unwrap_or_else(|| DEFAULT_TTY.into()); 46 | 47 | let mut port = tokio_serial::new(tty_path, 9600).open_native_async()?; 48 | 49 | #[cfg(unix)] 50 | port.set_exclusive(false) 51 | .expect("Unable to set serial port exclusive to false"); 52 | 53 | let mut reader = LineCodec.framed(port); 54 | 55 | while let Some(line_result) = reader.next().await { 56 | let line = line_result.expect("Failed to read line"); 57 | println!("{}", line); 58 | } 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crates.io][crates-badge]][crates-url] 2 | [![MIT licensed][mit-badge]][mit-url] 3 | [![Build Status][actions-badge]][actions-url] 4 | 5 | [crates-badge]: https://img.shields.io/crates/v/tokio-serial.svg 6 | [crates-url]: https://crates.io/crates/tokio-serial 7 | [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg 8 | [mit-url]: https://github.com/berkowski/tokio-serial/blob/master/LICENSE 9 | [actions-badge]: https://github.com/berkowski/tokio-serial/actions/workflows/github-ci.yml/badge.svg 10 | [actions-url]: https://github.com/berkowski/tokio-serial/actions?query=workflow%3Agithub-ci+branch%3Amaster 11 | # tokio-serial 12 | 13 | An implementation of serialport I/O for Tokio, an async framework for rust. 14 | 15 | ## MSRV 16 | The Minimum Supported Rust Version is **1.46.0** as found using [cargo-msrv](https://crates.io/crates/cargo-msrv) 17 | 18 | ## Usage 19 | 20 | Add `tokio-serial` to you `Cargo.toml`: 21 | 22 | ```toml 23 | [dependencies] 24 | tokio-serial = "5.4.1" 25 | ``` 26 | 27 | ## Tests 28 | Useful tests for serial ports require... serial ports, and serial ports are not often provided by online CI providers. 29 | As so, automated build testing are really only check whether the code compiles, not whether it works. 30 | 31 | Integration tests are in the `tests/` directory and typically require two serial ports to run. 32 | The names of the serial ports can be configured at run time by setting the `TEST_PORT_NAMES` environment variable 33 | to a semi-colon delimited string with the two serial port names. The default values are: 34 | 35 | - For Unix: `TEST_PORT_NAMES=/dev/ttyUSB0;/dev/ttyUSB1` 36 | - For Windows: `TEST_PORT_NAMES=COM1;COM2` 37 | 38 | **IMPORTANT** To prevent multiple tests from talking to the same ports at the same time make sure to limit the number 39 | of test threads to 1 using: 40 | 41 | ```sh 42 | cargo test -j1 -- --test-threads=1 43 | ``` 44 | ## Resources 45 | 46 | [tokio.rs](https://tokio.rs) 47 | [serialport-rs](https://gitlab.com/susurrus/serialport-rs) 48 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tokio-serial" 3 | version = "5.4.5" 4 | authors = ["Zac Berkowitz "] 5 | description = "A serial port implementation for tokio" 6 | license = "MIT" 7 | homepage = "https://github.com/berkowski/tokio-serial" 8 | repository = "https://github.com/berkowski/tokio-serial" 9 | documentation = "http://docs.rs/tokio-serial" 10 | readme = "README.md" 11 | keywords = ["rs232", "serial", "tokio"] 12 | categories = ["asynchronous", "hardware-support"] 13 | edition = "2018" 14 | 15 | [package.metadata] 16 | msrv = "1.46.0" 17 | 18 | [package.metadata.docs.rs] 19 | features = ["codec"] 20 | 21 | [features] 22 | default = [] 23 | libudev = ["mio-serial/libudev"] 24 | rt = ["tokio/rt-multi-thread"] 25 | codec = ["tokio-util/codec", "bytes"] 26 | 27 | [dependencies.futures] 28 | version = "0.3" 29 | 30 | [dependencies.tokio] 31 | version = "^1.8" 32 | default-features = false 33 | features = ["net"] 34 | 35 | [dependencies.tokio-util] 36 | version = "0.7.12" 37 | default-features = false 38 | optional = true 39 | 40 | [dev-dependencies.tokio-util] 41 | version = "0.7.12" 42 | default-features = false 43 | features = ["codec"] 44 | 45 | [dependencies.mio-serial] 46 | version = "5.0.3" 47 | default-features = false 48 | 49 | [dependencies.bytes] 50 | version = "1" 51 | default-features = false 52 | optional = true 53 | 54 | [dev-dependencies.bytes] 55 | version = "1" 56 | 57 | [dev-dependencies.cpal] 58 | version = "0.15.3" 59 | 60 | [dependencies.log] 61 | version = "0.4" 62 | 63 | [dependencies.cfg-if] 64 | version = "1" 65 | 66 | [dependencies.serialport] 67 | version = "4" 68 | default-features = false 69 | 70 | [dev-dependencies] 71 | anyhow = "1.0.91" 72 | 73 | [dev-dependencies.tokio] 74 | version = "^1.8" 75 | features = [ 76 | "macros", 77 | "rt", 78 | "process", 79 | "time", 80 | "fs", 81 | "io-util", 82 | "rt-multi-thread", 83 | ] 84 | default-features = false 85 | 86 | [dev-dependencies.env_logger] 87 | version = "0.10.0" 88 | 89 | [[example]] 90 | name = "serial_println" 91 | path = "examples/serial_println.rs" 92 | required-features = ["rt", "codec"] 93 | -------------------------------------------------------------------------------- /.github/NOTES.md: -------------------------------------------------------------------------------- 1 | # Appveyor support files 2 | 3 | From [Apollon77/SupportingFiles/README_SERIAL_TESTING.md](https://github.com/Apollon77/SupportingFiles/blob/master/README_SERIAL_TESTING.md), 4 | preserved locally for use. Original text below: 5 | 6 | # Serial Port testing 7 | 8 | ## Serial testing on Appveyor for Windows 9 | Because of the fact that Appveyor is container based they do not provide any serial ports by default. 10 | 11 | One way to still do Serial testing on Appveyor is to use a "Virtual Serialport Driver"/"Null Modem Simulator" like com0com (http://com0com.sourceforge.net/). In here are all files needed for this. 12 | 13 | Additionally you can also use com2tcp (also from http://com0com.sourceforge.net/) 14 | 15 | ### com0com Installer 16 | Because com0com is a driver it needs to be installed and also the certificate needs to be allowed. 17 | 18 | You need the following in your appveyor.yml: 19 | 20 | ``` 21 | install: 22 | - ps: Start-FileDownload https://github.com/Apollon77/SupportingFiles/raw/master/appveyor/serial/com0com.cer 23 | - ps: C:\"Program Files"\"Microsoft SDKs"\Windows\v7.1\Bin\CertMgr.exe /add com0com.cer /s /r localMachine root 24 | - ps: C:\"Program Files"\"Microsoft SDKs"\Windows\v7.1\Bin\CertMgr.exe /add com0com.cer /s /r localMachine trustedpublisher 25 | - ps: Start-FileDownload https://github.com/Apollon77/SupportingFiles/raw/master/appveyor/serial/setup_com0com_W7_x64_signed.exe 26 | - ps: $env:CNC_INSTALL_CNCA0_CNCB0_PORTS="YES" 27 | - ps: .\setup_com0com_W7_x64_signed.exe /S 28 | - ps: sleep 60 29 | ``` 30 | 31 | After that you will have a virtual serial port pair with the names CNCA0 and CNCB0 that are connected to each other that you can use for testing. Make sure to use "\\.\CNCA0" and "\\.\CNCB0" to connect to them. 32 | 33 | ### com2tcp 34 | To be able to create a Serialport-to-TCP tunnel you can use com2tcp, this can simplify testing too. 35 | 36 | To get the program use the following in your appveyor.yml: 37 | 38 | ``` 39 | install: 40 | - ps: Start-FileDownload https://github.com/Apollon77/SupportingFiles/raw/master/appveyor/serial/com2tcp.exe 41 | ``` 42 | 43 | After that the com2tcp.exe is normally located in your %APPVEYOR_BUILD_FOLDER& which is the normal project clone folder. 44 | Call it (in Background) using e.g.: 45 | 46 | ``` 47 | com2tcp.exe --ignore-dsr --baud 9600 --parity e \\.\CNCA0 127.0.0.1 15001 48 | ``` 49 | to connect the CNCA0 port to a TCP server on localhost on port 15001 50 | 51 | ### Credits 52 | The final solution on how to use com0com on Appveyor was found by https://github.com/hybridgroup/rubyserial. I copied some file to my space to make sure they are available as I need them. 53 | 54 | ## Serial testing on Travis-CI for Linux and macOS 55 | To simplify it you use socat here which is a standard tool. 56 | To have it available for both Linux and macOS on Travis-CI add the following to your .travis.yml: 57 | 58 | ``` 59 | before_install: 60 | - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi' 61 | - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install socat; fi' 62 | addons: 63 | apt: 64 | packages: 65 | - socat 66 | ``` 67 | 68 | After this use socat to create a virtual serial port connected to a TCP server like: 69 | 70 | ``` 71 | socat -Dxs pty,link=/tmp/virtualcom0,ispeed=9600,ospeed=9600,raw tcp:127.0.0.1:15001 72 | ``` 73 | 74 | ... or comparable to provide a virtual serialport pair. 75 | -------------------------------------------------------------------------------- /tests/test_serialstream.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use tokio::{ 3 | io::{AsyncReadExt, AsyncWriteExt}, 4 | process, time, 5 | }; 6 | use tokio_serial::SerialPortBuilderExt; 7 | 8 | #[cfg(unix)] 9 | const DEFAULT_TEST_PORT_NAMES: &str = concat!( 10 | env!("CARGO_TARGET_TMPDIR"), 11 | "/ttyUSB0;", 12 | env!("CARGO_TARGET_TMPDIR"), 13 | "/ttyUSB1" 14 | ); 15 | #[cfg(not(unix))] 16 | const DEFAULT_TEST_PORT_NAMES: &str = "COM10;COM11"; 17 | 18 | struct Fixture { 19 | #[cfg(unix)] 20 | process: process::Child, 21 | pub port_a: &'static str, 22 | pub port_b: &'static str, 23 | } 24 | 25 | #[cfg(unix)] 26 | impl Drop for Fixture { 27 | fn drop(&mut self) { 28 | if let Some(id) = self.process.id() { 29 | log::trace!("stopping socat process (id: {})...", id); 30 | 31 | self.process.start_kill().ok(); 32 | std::thread::sleep(Duration::from_millis(1000)); 33 | log::trace!("removing link: {}", self.port_a); 34 | std::fs::remove_file(self.port_a).ok(); 35 | log::trace!("removing link: {}", self.port_b); 36 | std::fs::remove_file(self.port_b).ok(); 37 | } 38 | } 39 | } 40 | 41 | impl Fixture { 42 | #[cfg(unix)] 43 | pub async fn new(port_a: &'static str, port_b: &'static str) -> Self { 44 | use std::sync::atomic::{AtomicUsize, Ordering}; 45 | static N: AtomicUsize = AtomicUsize::new(0); 46 | let n = N.fetch_add(1, Ordering::Relaxed); 47 | let port_a = format!("{}{}", port_a, n).leak(); 48 | let port_b = format!("{}{}", port_b, n).leak(); 49 | let args = [ 50 | format!("PTY,link={}", port_a), 51 | format!("PTY,link={}", port_b), 52 | ]; 53 | log::trace!("starting process: socat {} {}", args[0], args[1]); 54 | let process = process::Command::new("socat") 55 | .args(&args) 56 | .spawn() 57 | .expect("unable to spawn socat process"); 58 | log::trace!(".... done! (pid: {:?})", process.id().unwrap()); 59 | time::sleep(Duration::from_millis(1000)).await; 60 | Self { 61 | process, 62 | port_a, 63 | port_b, 64 | } 65 | } 66 | 67 | #[cfg(not(unix))] 68 | pub async fn new(port_a: &'static str, port_b: &'static str) -> Self { 69 | Self { port_a, port_b } 70 | } 71 | } 72 | 73 | async fn setup_virtual_serial_ports() -> Fixture { 74 | let port_names: Vec<&str> = std::option_env!("TEST_PORT_NAMES") 75 | .unwrap_or(DEFAULT_TEST_PORT_NAMES) 76 | .split(';') 77 | .collect(); 78 | 79 | assert_eq!(port_names.len(), 2); 80 | Fixture::new(port_names[0], port_names[1]).await 81 | } 82 | 83 | #[tokio::test] 84 | async fn send_recv() { 85 | env_logger::init(); 86 | 87 | let fixture = setup_virtual_serial_ports().await; 88 | 89 | let mut sender = tokio_serial::new(fixture.port_a, 9600) 90 | .open_native_async() 91 | .expect("unable to open serial port"); 92 | let mut receiver = tokio_serial::new(fixture.port_b, 9600) 93 | .open_native_async() 94 | .expect("unable to open serial port"); 95 | 96 | log::trace!("sending test message"); 97 | let message = b"This is a test message"; 98 | sender 99 | .write_all(message) 100 | .await 101 | .expect("unable to write test message"); 102 | 103 | log::trace!("receiving test message"); 104 | let mut buf = [0u8; 32]; 105 | let n = receiver 106 | .read_exact(&mut buf[..message.len()]) 107 | .await 108 | .expect("unable to read test message"); 109 | 110 | log::trace!("checking test message"); 111 | assert_eq!(&buf[..n], message); 112 | } 113 | -------------------------------------------------------------------------------- /.github/workflows/github-ci.yml: -------------------------------------------------------------------------------- 1 | name: github-ci 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '0 8 15 * *' 7 | jobs: 8 | cargo-test-linux: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | rust: 13 | - stable 14 | - beta 15 | - 1.46.0 16 | # - nightly 17 | env: 18 | TEST_PORT_A: /tmp/ttyS10 19 | TEST_PORT_B: /tmp/ttyS11 20 | RUST_LOG: trace 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: actions-rs/toolchain@v1 24 | with: 25 | profile: minimal 26 | toolchain: ${{ matrix.rust }} 27 | override: true 28 | components: rustfmt, clippy 29 | - uses: Swatinem/rust-cache@v1 30 | - name: install socat 31 | run: | 32 | sudo apt-get update 33 | sudo apt-get install socat -y 34 | socat -V 35 | - name: cargo test 36 | run: cargo test -j1 -- --test-threads=1 37 | env: 38 | TEST_PORT_NAMES: ${{ env.TEST_PORT_A }};${{ env.TEST_PORT_B }} 39 | cargo-test-macOS: 40 | runs-on: macos-latest 41 | strategy: 42 | matrix: 43 | rust: 44 | - stable 45 | - beta 46 | - 1.46.0 47 | # - nightly 48 | env: 49 | TEST_PORT_A: /tmp/ttyS10 50 | TEST_PORT_B: /tmp/ttyS11 51 | RUST_LOG: trace 52 | steps: 53 | - uses: actions/checkout@v2 54 | - uses: actions-rs/toolchain@v1 55 | with: 56 | profile: minimal 57 | toolchain: ${{ matrix.rust }} 58 | override: true 59 | components: rustfmt, clippy 60 | - uses: Swatinem/rust-cache@v1 61 | - name: install socat 62 | run: | 63 | brew install socat 64 | socat -V 65 | env: 66 | HOMEBREW_NO_AUTO_UPDATE: 1 67 | # Github Actions don't support 'allow-failures': https://github.com/actions/toolkit/issues/399 68 | # Until it does then we'll just have to test building on OSX in the meantime 69 | # - name: cargo test 70 | # run: cargo test -j1 -- --test-threads=1 71 | # env: 72 | # TEST_PORT_NAMES: ${{ env.TEST_PORT_A }};${{ env.TEST_PORT_B }} 73 | - name: cargo build 74 | uses: actions-rs/cargo@v1 75 | with: 76 | command: build 77 | cargo-test-windows: 78 | runs-on: windows-latest 79 | strategy: 80 | matrix: 81 | rust: 82 | - stable 83 | - beta 84 | - 1.46.0 85 | # - nightly 86 | env: 87 | TEST_PORT_A: COM10 88 | TEST_PORT_B: COM11 89 | RUST_LOG: trace 90 | steps: 91 | - uses: actions/checkout@v2 92 | - uses: actions-rs/toolchain@v1 93 | with: 94 | profile: minimal 95 | toolchain: ${{ matrix.rust }} 96 | override: true 97 | components: rustfmt, clippy 98 | - uses: Swatinem/rust-cache@v1 99 | - uses: ilammy/msvc-dev-cmd@v1 100 | - name: install com0com 101 | run: | 102 | CertMgr.exe /add com0com.cer /s /r localMachine root 103 | CertMgr.exe /add com0com.cer /s /r localMachine trustedpublisher 104 | .\setup_com0com_W7_x64_signed.exe /S 105 | working-directory: .github 106 | - name: setup com0com 107 | run: .\setupc.exe install PortName=${{ env.TEST_PORT_A }},EmuBR=yes PortName=${{ env.TEST_PORT_B }},EmuBR=yes 108 | working-directory: C:\Program Files (x86)\com0com 109 | - name: cargo test 110 | run: cargo test -j1 -- --test-threads=1 111 | env: 112 | TEST_PORT_NAMES: ${{ env.TEST_PORT_A }};${{ env.TEST_PORT_B }} 113 | cargo-fmt: 114 | runs-on: ${{ matrix.os }} 115 | strategy: 116 | matrix: 117 | rust: 118 | - stable 119 | os: 120 | - ubuntu-latest 121 | - windows-latest 122 | steps: 123 | - uses: actions/checkout@v2 124 | - uses: actions-rs/toolchain@v1 125 | with: 126 | profile: minimal 127 | toolchain: ${{ matrix.rust }} 128 | override: true 129 | components: rustfmt, clippy 130 | - uses: Swatinem/rust-cache@v1 131 | - name: check format 132 | uses: actions-rs/cargo@v1 133 | with: 134 | command: fmt 135 | args: -- --check 136 | cargo-clippy: 137 | runs-on: ${{ matrix.os }} 138 | strategy: 139 | matrix: 140 | rust: 141 | - stable 142 | os: 143 | - ubuntu-latest 144 | - windows-latest 145 | steps: 146 | - uses: actions/checkout@v2 147 | - uses: actions-rs/toolchain@v1 148 | with: 149 | profile: minimal 150 | toolchain: ${{ matrix.rust }} 151 | override: true 152 | components: rustfmt, clippy 153 | - uses: Swatinem/rust-cache@v1 154 | - name: cargo clippy 155 | uses: actions-rs/cargo@v1 156 | with: 157 | command: clippy 158 | args: -- -D warnings 159 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [5.4.2] 2022-03-04 9 | - merge [#48](https://github.com/berkowski/tokio-serial/pull/48) 10 | 11 | ## [5.4.1] 2021-08-08 12 | Minor update to README.md 13 | 14 | ### Changed 15 | - version of tokio-serial indicated in README.md example. 16 | 17 | ## [5.4.0] 2021-08-08 18 | Final release of version 5.4. No code changes besides bumping `mio-serial` to 5.0 since the 19 | previous 5.4.0-beta4. 20 | 21 | ### Added 22 | - [ColinFinck](https://github.com/ColinFinck) added as maintainer for `tokio-serial` 23 | - [estokes](https://github.com/estokes) added as maintainer for `tokio-serial` 24 | 25 | ### Changed 26 | - Bumped [mio-serial](https://github.com/berkowski/mio-serial) to 5.0.0 27 | 28 | ## [5.4.0-beta4] 2021-07-23 29 | 30 | ### Changed 31 | - [#46](https://github.com/berkowski/tokio-serial/pull/46) [@ColinFinck](https://github.com/ColinFinck) fixed some docs. 32 | 33 | ## [5.4.0-beta3] 2021-07-23 34 | 35 | ### Added 36 | - Check in CI tests for building against the MSRV (currently `1.43.0`) 37 | 38 | ### Changed 39 | - Bumped [mio-serial](https://github.com/berkowski/mio-serial) to 4.0.0-beta4 40 | - [#45](https://github.com/berkowski/tokio-serial/pull/45) [@ColinFinck](https://github.com/ColinFinck) fixed the example. 41 | 42 | ## [5.4.0-beta2] 2021-07-22 43 | 44 | ### Changed 45 | - Renamed `SerialPortBuilderExt::open_async` to `SerialPortBuilderExt::open_native_async` to reflect the original 46 | intention 47 | - [@drbrain](https://github.com/drbrain) fixed formatting issues with this changelog in [#43](https://github.com/berkowski/tokio-serial/pull/43) 48 | - Bumped [mio-serial](https://github.com/berkowski/mio-serial) to 4.0.0-beta3 49 | 50 | ## [5.4.0-beta1] 2021-07-16 51 | 52 | Major release drawing in updates to `tokio` and `mio-serial` (and the upstream `serialport-rs`) 53 | 54 | ### BREAKING CHANGES 55 | 56 | - Following `mio-serial`, added `tokio_serial::SerialStream` with platform specific requirements at compile time with `#[cfg()]` 57 | - Removed `SerialPortSettings`, `serialport-rs` now uses the builder pattern 58 | 59 | ### Added 60 | - `SerialPortBuilderExt` extension trait for `serialport::SerialPortBuilder` to open a serial port in async mode 61 | - Re-exports of both `mio_serial::new` and `mio_serial::available_ports` 62 | 63 | ### Changed 64 | - Bumped [tokio](https://github.com/tokio-rs/tokio) to 1.0 65 | - Bumped [mio-serial](https://github.com/berkowski/mio-serial) to 4.0.0-beta2 66 | 67 | ### Contributions 68 | - [#35](https://github.com/berkowski/tokio-serial/pull/35) by [georgmu](https://github.com/georgmu) found an early bug in the AsyncRead trait impl 69 | - [#39](https://github.com/berkowski/tokio-serial/pull/39) and [#41](https://github.com/berkowski/tokio-serial/pull/41) by [ColinFinck](https://github.com/ColinFinck) took it upon himself to push windows support for Tokio 1.X 70 | and did the vast majority of the initial work and paved the way 71 | 72 | ## [4.3.3] 2019-11-24 73 | ### Changed 74 | * @Darksonn bumped tokio dependencies to version 0.2.0 and cleaned up some dependencies in PR [#24](https://github.com/berkowski/tokio-serial/pull/24) 75 | 76 | ## [4.3.3-alpha.6] 2019-11-15 77 | ### Changed 78 | * @Darksonn bumped tokio dependencies to version 0.2.0-alpha.6 in PR [#23](https://github.com/berkowski/tokio-serial/pull/23) 79 | * Updated README.md to include latest tokio-serial version numbers for both tokio 0.1 and 0.2-alpha based libraries. 80 | 81 | ## [4.3.3-alpha.2] 2019-08-26 82 | ### Changed 83 | * Bumped to tokio dependencies to version 0.2 84 | Majority of work done by @12101111 and @will-w in PR's [#19](https://github.com/berkowski/tokio-serial/pull/19) 85 | and [#21](https://github.com/berkowski/tokio-serial/pull/21) respectively 86 | * @D1plo1d bumped the tokio dependency to 0.2.0-alpha.2 in [#22](https://github.com/berkowski/tokio-serial/pull/21) 87 | 88 | 89 | 90 | ## [3.3.0] 2019-08-23 91 | * Bumped [mio-serial](https://gitlab.com/berkowski/mio-serial) to 3.3.0 92 | * Switched to "2018" edition 93 | 94 | ## [3.2.14] 2019-06-01 95 | ### Changed 96 | * Bumped [mio-serial](https://gitlab.com/berkowski/mio-serial) to 3.2.14 (tracking mio version 0.14) 97 | 98 | ### changed 99 | * Merged [#17](https://github.com/berkowski/tokio-serial/pull/17) @nanpuyue updated the printline example. 100 | 101 | ## [3.2] 2019-01-12 102 | ### Changed 103 | * Bumped [serialport-rs](https://gitlab.com/susurrus/serialport-rs) to 3.2 104 | 105 | ## [3.1.1] 2019-01-12 106 | ### Changed 107 | * Merged [#16](https://github.com/berkowski/tokio-serial/pull/16) @yuja fixed feature flags 108 | 109 | ## [3.1.0] - 2018-11-10 110 | ### changed 111 | * Bumped `mio-serial` dependency to 3.1 112 | 113 | ## [3.0.0] - 2018-10-06 114 | ### changed 115 | * Bumped `mio-serial` dependency to 3.0 116 | 117 | ## [0.8.0] - 2018-04-27 118 | ### changed 119 | * Migrated to tokio 0.1 with [#9](https://github.com/berkowski/tokio-serial/pull/9) and 120 | [#10](https://github.com/berkowski/tokio-serial/pull/10) Thanks, [lnicola](https://github.com/lnicola)! 121 | * Bumped `mio-serial` dependency to 0.8 122 | 123 | ## [0.7.0] - UNRELEASED 124 | ### added 125 | * Windows support (through mio-serial 0.7) 126 | * Appveyor testing support 127 | 128 | ### changed 129 | * Bumped `mio-serial` dependency to 0.7 130 | 131 | 132 | ## [0.6.0] - 2017-11-28 133 | ### added 134 | * Re-exporting `mio_serial::Error` (itself a re-export of `serialport::Error`) 135 | 136 | ### changed 137 | * Bumped `mio-serial` dependency to 0.6 138 | 139 | ## [0.5.0] - 2017-05-18 140 | ### added 141 | * Added `trust` CI 142 | * [#1](https://github.com/berkowski/tokio-serial/pull/1) provided `AsyncRead` and 143 | `AsyncWrite` impls. Thanks [lexxvir](https://github.com/lexxvir)! 144 | 145 | ### changed 146 | * Bumped `mio-serial` dependency to 0.5 Future releases will 147 | track `mio-serial` versions. 148 | -------------------------------------------------------------------------------- /src/frame.rs: -------------------------------------------------------------------------------- 1 | //! A unified [`Stream`] and [`Sink`] interface to an underlying `SerialStream`, using 2 | //! the `Encoder` and `Decoder` traits to encode and decode frames. 3 | use super::SerialStream; 4 | 5 | use tokio_util::codec::{Decoder, Encoder}; 6 | 7 | use futures::{Sink, Stream}; 8 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 9 | 10 | use bytes::{BufMut, BytesMut}; 11 | use futures::ready; 12 | use std::pin::Pin; 13 | use std::task::{Context, Poll}; 14 | use std::{io, mem::MaybeUninit}; 15 | 16 | /// A unified [`Stream`] and [`Sink`] interface to an underlying `SerialStream`, using 17 | /// the `Encoder` and `Decoder` traits to encode and decode frames. 18 | /// 19 | /// Raw serial ports work with bytes, but higher-level code usually wants to 20 | /// batch these into meaningful chunks, called "frames". This method layers 21 | /// framing on top of this socket by using the `Encoder` and `Decoder` traits to 22 | /// handle encoding and decoding of messages frames. Note that the incoming and 23 | /// outgoing frame types may be distinct. 24 | /// 25 | /// This function returns a *single* object that is both [`Stream`] and [`Sink`]; 26 | /// grouping this into a single object is often useful for layering things which 27 | /// require both read and write access to the underlying object. 28 | /// 29 | /// If you want to work more directly with the streams and sink, consider 30 | /// calling [`split`] on the `SerialFramed` returned by this method, which will break 31 | /// them into separate objects, allowing them to interact more easily. 32 | /// 33 | /// [`Stream`]: futures_core::Stream 34 | /// [`Sink`]: futures_sink::Sink 35 | /// [`split`]: https://docs.rs/futures/0.3/futures/stream/trait.StreamExt.html#method.split 36 | #[must_use = "sinks do nothing unless polled"] 37 | #[derive(Debug)] 38 | pub struct SerialFramed { 39 | port: SerialStream, 40 | codec: C, 41 | rd: BytesMut, 42 | wr: BytesMut, 43 | flushed: bool, 44 | is_readable: bool, 45 | } 46 | 47 | const INITIAL_RD_CAPACITY: usize = 64 * 1024; 48 | const INITIAL_WR_CAPACITY: usize = 8 * 1024; 49 | 50 | impl Stream for SerialFramed { 51 | type Item = Result; 52 | 53 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 54 | let pin = self.get_mut(); 55 | 56 | pin.rd.reserve(INITIAL_RD_CAPACITY); 57 | 58 | loop { 59 | // Are there still bytes left in the read buffer to decode? 60 | if pin.is_readable { 61 | if let Some(frame) = pin.codec.decode_eof(&mut pin.rd)? { 62 | return Poll::Ready(Some(Ok(frame))); 63 | } 64 | 65 | // if this line has been reached then decode has returned `None`. 66 | pin.is_readable = false; 67 | pin.rd.clear(); 68 | } 69 | 70 | // We're out of data. Try and fetch more data to decode 71 | unsafe { 72 | // Convert `&mut [MaybeUnit]` to `&mut [u8]` because we will be 73 | // writing to it via `poll_recv_from` and therefore initializing the memory. 74 | let buf = &mut *(pin.rd.chunk_mut() as *mut _ as *mut [MaybeUninit]); 75 | let mut read = ReadBuf::uninit(buf); 76 | let ptr = read.filled().as_ptr(); 77 | ready!(Pin::new(&mut pin.port).poll_read(cx, &mut read))?; 78 | 79 | assert_eq!(ptr, read.filled().as_ptr()); 80 | pin.rd.advance_mut(read.filled().len()); 81 | }; 82 | 83 | pin.is_readable = true; 84 | } 85 | } 86 | } 87 | 88 | impl + Unpin> Sink for SerialFramed { 89 | type Error = C::Error; 90 | 91 | fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 92 | if !self.flushed { 93 | match self.poll_flush(cx)? { 94 | Poll::Ready(()) => {} 95 | Poll::Pending => return Poll::Pending, 96 | } 97 | } 98 | 99 | Poll::Ready(Ok(())) 100 | } 101 | 102 | fn start_send(self: Pin<&mut Self>, item: I) -> Result<(), Self::Error> { 103 | let pin = self.get_mut(); 104 | 105 | pin.codec.encode(item, &mut pin.wr)?; 106 | pin.flushed = false; 107 | 108 | Ok(()) 109 | } 110 | 111 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 112 | if self.flushed { 113 | return Poll::Ready(Ok(())); 114 | } 115 | 116 | let Self { 117 | ref mut port, 118 | ref mut wr, 119 | .. 120 | } = *self; 121 | 122 | let pinned = Pin::new(port); 123 | let n = ready!(pinned.poll_write(cx, &wr))?; 124 | 125 | let wrote_all = n == self.wr.len(); 126 | self.wr.clear(); 127 | self.flushed = true; 128 | 129 | let res = if wrote_all { 130 | Ok(()) 131 | } else { 132 | Err(io::Error::new( 133 | io::ErrorKind::Other, 134 | "failed to write entire datagram to socket", 135 | ) 136 | .into()) 137 | }; 138 | 139 | Poll::Ready(res) 140 | } 141 | 142 | fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 143 | ready!(self.poll_flush(cx))?; 144 | Poll::Ready(Ok(())) 145 | } 146 | } 147 | 148 | impl SerialFramed { 149 | /// Create a new `SerialFramed` backed by the given socket and codec. 150 | /// 151 | /// See struct level documentation for more details. 152 | #[allow(dead_code)] 153 | pub fn new(port: SerialStream, codec: C) -> SerialFramed { 154 | Self { 155 | port, 156 | codec, 157 | rd: BytesMut::with_capacity(INITIAL_RD_CAPACITY), 158 | wr: BytesMut::with_capacity(INITIAL_WR_CAPACITY), 159 | flushed: true, 160 | is_readable: false, 161 | } 162 | } 163 | 164 | /// Returns a reference to the underlying I/O stream wrapped by `Framed`. 165 | /// 166 | /// # Note 167 | /// 168 | /// Care should be taken to not tamper with the underlying stream of data 169 | /// coming in as it may corrupt the stream of frames otherwise being worked 170 | /// with. 171 | #[allow(dead_code)] 172 | pub fn get_ref(&self) -> &SerialStream { 173 | &self.port 174 | } 175 | 176 | /// Returns a mutable reference to the underlying I/O stream wrapped by 177 | /// `Framed`. 178 | /// 179 | /// # Note 180 | /// 181 | /// Care should be taken to not tamper with the underlying stream of data 182 | /// coming in as it may corrupt the stream of frames otherwise being worked 183 | /// with. 184 | #[allow(dead_code)] 185 | pub fn get_mut(&mut self) -> &mut SerialStream { 186 | &mut self.port 187 | } 188 | 189 | /// Consumes the `Framed`, returning its underlying I/O stream. 190 | #[allow(dead_code)] 191 | pub fn into_inner(self) -> SerialStream { 192 | self.port 193 | } 194 | 195 | /// Returns a reference to the underlying codec wrapped by 196 | /// `Framed`. 197 | /// 198 | /// Note that care should be taken to not tamper with the underlying codec 199 | /// as it may corrupt the stream of frames otherwise being worked with. 200 | #[allow(dead_code)] 201 | pub fn codec(&self) -> &C { 202 | &self.codec 203 | } 204 | 205 | /// Returns a mutable reference to the underlying codec wrapped by 206 | /// `SerialFramed`. 207 | /// 208 | /// Note that care should be taken to not tamper with the underlying codec 209 | /// as it may corrupt the stream of frames otherwise being worked with. 210 | #[allow(dead_code)] 211 | pub fn codec_mut(&mut self) -> &mut C { 212 | &mut self.codec 213 | } 214 | 215 | /// Returns a reference to the read buffer. 216 | #[allow(dead_code)] 217 | pub fn read_buffer(&self) -> &BytesMut { 218 | &self.rd 219 | } 220 | 221 | /// Returns a mutable reference to the read buffer. 222 | #[allow(dead_code)] 223 | pub fn read_buffer_mut(&mut self) -> &mut BytesMut { 224 | &mut self.rd 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /examples/filter_play_sound.rs: -------------------------------------------------------------------------------- 1 | /// filter terms from the serial port and make a sound when found. 2 | /// 3 | /// dave horner 10/24 4 | /// 5 | /// Default settings for Nordic Thingy53, nrf5340dk, and other nordic devices (baud/com). 6 | use bytes::BytesMut; 7 | use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; 8 | use futures::stream::StreamExt; 9 | use std::sync::Mutex; 10 | use std::sync::Arc; 11 | use std::{env, io, str}; 12 | use tokio::time::Duration; 13 | use tokio_serial::SerialPortBuilderExt; 14 | use tokio_util::codec::{Decoder, Encoder}; 15 | extern crate anyhow; 16 | 17 | #[cfg(unix)] 18 | const DEFAULT_TTY: &str = "/dev/ttyACM1"; 19 | #[cfg(windows)] 20 | const DEFAULT_TTY: &str = "COM8"; 21 | 22 | // Create the table of findable strings and their sound parameters 23 | fn create_find_text_map() -> HashMap<&'static str, SoundParams> { 24 | let mut map = HashMap::new(); 25 | map.insert("Using Zephyr OS", SoundParams { 26 | waveform: Waveform::Sine, 27 | frequency: 500.0, 28 | duration: 150, 29 | }); 30 | map.insert("Error", SoundParams { 31 | waveform: Waveform::Square, 32 | frequency: 800.0, 33 | duration: 150, 34 | }); 35 | map.insert("Warning", SoundParams { 36 | waveform: Waveform::Triangle, 37 | frequency: 300.0, 38 | duration: 150, 39 | }); 40 | map.insert("DK handling", SoundParams { 41 | waveform: Waveform::Triangle, 42 | frequency: 600.0, 43 | duration: 150, 44 | }); 45 | map 46 | } 47 | 48 | #[tokio::main] 49 | async fn main() -> tokio_serial::Result<()> { 50 | let mut args = env::args(); 51 | let tty_path = args.nth(1).unwrap_or_else(|| DEFAULT_TTY.into()); 52 | 53 | 54 | #[cfg(unix)] 55 | let mut port = tokio_serial::new(tty_path, 115200).open_native_async()?; // Mutable on Unix 56 | #[cfg(windows)] 57 | let port = tokio_serial::new(tty_path, 115200).open_native_async()?; // Immutable on Windows 58 | #[cfg(unix)] 59 | port.set_exclusive(false) 60 | .expect("Unable to set serial port exclusive to false"); 61 | let mut reader = LineCodec.framed(port); 62 | 63 | let find_text_map = create_find_text_map(); 64 | while let Some(line_result) = reader.next().await { 65 | let line = line_result.expect("Failed to read line"); 66 | print!("{}", line); 67 | 68 | for (phrase, params) in &find_text_map { 69 | if line.contains(phrase) { 70 | let params_clone = params.clone(); 71 | tokio::spawn(async move { 72 | let _ = play_sound(params_clone).await; 73 | }); 74 | break; 75 | } 76 | } 77 | } 78 | Ok(()) 79 | } 80 | 81 | 82 | /////////////////////////////////// 83 | /// Codec 84 | /// /////////////////////////////// 85 | 86 | struct LineCodec; 87 | 88 | impl Decoder for LineCodec { 89 | type Item = String; 90 | type Error = io::Error; 91 | 92 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 93 | let newline = src.as_ref().iter().position(|b| *b == b'\n'); 94 | if let Some(n) = newline { 95 | let line = src.split_to(n + 1); 96 | return match str::from_utf8(line.as_ref()) { 97 | Ok(s) => Ok(Some(s.to_string())), 98 | Err(_) => Err(io::Error::new(io::ErrorKind::Other, "Invalid String")), 99 | }; 100 | } 101 | Ok(None) 102 | } 103 | } 104 | 105 | impl Encoder for LineCodec { 106 | type Error = io::Error; 107 | 108 | fn encode(&mut self, _item: String, _dst: &mut BytesMut) -> Result<(), Self::Error> { 109 | Ok(()) 110 | } 111 | } 112 | 113 | 114 | /////////////////////////////////// 115 | /// All this code to make noise. 116 | /// /////////////////////////////// 117 | use std::error::Error; 118 | use std::f32::consts::PI; 119 | use std::thread; 120 | use std::collections::HashMap; 121 | 122 | #[derive(Clone)] 123 | struct SoundParams { 124 | waveform: Waveform, 125 | frequency: f32, 126 | duration: u64, 127 | } 128 | 129 | async fn play_sound(params: SoundParams) -> Result<(), Box> { 130 | let oscillator = Arc::new(Mutex::new(Oscillator::new(44100.0, params.frequency, params.waveform))); 131 | let oscillator_clone = Arc::clone(&oscillator); 132 | 133 | let play_handle = thread::spawn(move || { 134 | let stream = start_audio_stream_arc(oscillator_clone).expect("Failed to start audio stream"); 135 | stream.play().expect("Failed to play audio stream"); 136 | std::thread::sleep(Duration::from_millis(params.duration)); 137 | }); 138 | 139 | play_handle.join().expect("Play thread panicked"); 140 | Ok(()) 141 | } 142 | 143 | #[derive(Clone, Copy)] 144 | pub enum Waveform { 145 | Sine, 146 | Square, 147 | Saw, 148 | Triangle, 149 | } 150 | 151 | pub struct Oscillator { 152 | pub sample_rate: f32, 153 | pub waveform: Waveform, 154 | pub current_sample_index: f32, 155 | pub frequency_hz: f32, 156 | } 157 | 158 | impl Oscillator { 159 | pub fn new(sample_rate: f32, frequency_hz: f32, waveform: Waveform) -> Self { 160 | Self { 161 | sample_rate, 162 | waveform, 163 | current_sample_index: 0.0, 164 | frequency_hz, 165 | } 166 | } 167 | 168 | pub fn set_waveform(&mut self, waveform: Waveform) { 169 | self.waveform = waveform; 170 | } 171 | 172 | pub fn tick(&mut self) -> f32 { 173 | match self.waveform { 174 | Waveform::Sine => self.sine_wave(), 175 | Waveform::Square => self.square_wave(), 176 | Waveform::Saw => self.saw_wave(), 177 | Waveform::Triangle => self.triangle_wave(), 178 | } 179 | } 180 | 181 | fn advance_sample(&mut self) { 182 | self.current_sample_index = (self.current_sample_index + 1.0) % self.sample_rate; 183 | } 184 | 185 | fn calculate_sine_output(&self) -> f32 { 186 | (self.current_sample_index * self.frequency_hz * 2.0 * PI / self.sample_rate).sin() 187 | } 188 | 189 | fn sine_wave(&mut self) -> f32 { 190 | self.advance_sample(); 191 | self.calculate_sine_output() 192 | } 193 | 194 | fn square_wave(&mut self) -> f32 { 195 | self.generative_waveform(2, 1.0) 196 | } 197 | 198 | fn saw_wave(&mut self) -> f32 { 199 | self.generative_waveform(1, 1.0) 200 | } 201 | 202 | fn triangle_wave(&mut self) -> f32 { 203 | self.generative_waveform(2, 2.0) 204 | } 205 | 206 | fn generative_waveform(&mut self, harmonic_step: i32, gain_factor: f32) -> f32 { 207 | self.advance_sample(); 208 | let mut output = 0.0; 209 | let mut harmonic = 1; 210 | while self.frequency_hz * harmonic as f32 <= self.sample_rate / 2.0 { 211 | let gain = 1.0 / (harmonic as f32).powf(gain_factor); 212 | output += gain * self.calculate_sine_output(); 213 | harmonic += harmonic_step; 214 | } 215 | output 216 | } 217 | } 218 | 219 | use cpal::{Sample, SampleFormat, SizedSample}; 220 | 221 | pub fn start_audio_stream(waveform: Waveform, frequency: f32) -> anyhow::Result { 222 | let (_host, device, config) = host_device_setup()?; 223 | match config.sample_format() { 224 | SampleFormat::F32 => create_stream::(&device, &config.into(), waveform, frequency), 225 | _ => Err(anyhow::Error::msg("Unsupported sample format")), 226 | } 227 | } 228 | 229 | pub fn start_audio_stream_arc(oscillator: Arc>) -> anyhow::Result { 230 | let (_host, device, config) = host_device_setup()?; 231 | match config.sample_format() { 232 | SampleFormat::F32 => create_stream_arc::(&device, &config.into(), oscillator), 233 | _ => Err(anyhow::Error::msg("Unsupported sample format")), 234 | } 235 | } 236 | 237 | fn host_device_setup( 238 | ) -> Result<(cpal::Host, cpal::Device, cpal::SupportedStreamConfig), anyhow::Error> { 239 | let host = cpal::default_host(); 240 | let device = host 241 | .default_output_device() 242 | .ok_or_else(|| anyhow::Error::msg("No output device available"))?; 243 | let config = device.default_output_config()?; 244 | Ok((host, device, config)) 245 | } 246 | 247 | pub fn create_stream_arc( 248 | device: &cpal::Device, 249 | config: &cpal::StreamConfig, 250 | oscillator: Arc>, 251 | ) -> anyhow::Result 252 | where 253 | T: Sample + SizedSample + cpal::FromSample, 254 | { 255 | let num_channels = config.channels as usize; 256 | 257 | let stream = device.build_output_stream( 258 | config, 259 | move |output: &mut [T], _| { 260 | let mut osc = oscillator.lock().unwrap(); 261 | for frame in output.chunks_mut(num_channels) { 262 | let sample_value: T = T::from_sample(osc.tick()); 263 | for sample in frame.iter_mut() { 264 | *sample = sample_value; 265 | } 266 | } 267 | }, 268 | |err| eprintln!("Error: {}", err), 269 | None, 270 | )?; 271 | 272 | Ok(stream) 273 | } 274 | 275 | fn create_stream( 276 | device: &cpal::Device, 277 | config: &cpal::StreamConfig, 278 | waveform: Waveform, 279 | frequency: f32, 280 | ) -> anyhow::Result 281 | where 282 | T: Sample + SizedSample + cpal::FromSample, 283 | { 284 | let mut oscillator = Oscillator::new(config.sample_rate.0 as f32, frequency, waveform); 285 | let num_channels = config.channels as usize; 286 | 287 | let stream = device.build_output_stream( 288 | config, 289 | move |output: &mut [T], _| { 290 | for frame in output.chunks_mut(num_channels) { 291 | let sample_value: T = T::from_sample(oscillator.tick()); 292 | for sample in frame.iter_mut() { 293 | *sample = sample_value; 294 | } 295 | } 296 | }, 297 | |err| eprintln!("Error: {}", err), 298 | None, 299 | )?; 300 | 301 | Ok(stream) 302 | } 303 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Bindings for serial port I/O and futures 2 | //! 3 | //! This crate provides bindings between `mio_serial`, a mio crate for 4 | //! serial port I/O, and `futures`. The API is very similar to the 5 | //! bindings in `mio_serial` 6 | //! 7 | #![deny(missing_docs)] 8 | #![warn(rust_2018_idioms)] 9 | 10 | // Re-export serialport types and traits from mio_serial 11 | pub use mio_serial::{ 12 | available_ports, new, ClearBuffer, DataBits, Error, ErrorKind, FlowControl, Parity, SerialPort, 13 | SerialPortBuilder, SerialPortInfo, SerialPortType, StopBits, UsbPortInfo, 14 | }; 15 | 16 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 17 | 18 | #[cfg(unix)] 19 | use std::convert::TryFrom; 20 | use std::io::{Read, Result as IoResult, Write}; 21 | use std::pin::Pin; 22 | use std::task::{Context, Poll}; 23 | use std::time::Duration; 24 | 25 | #[cfg(feature = "codec")] 26 | pub mod frame; 27 | 28 | #[cfg(unix)] 29 | mod os_prelude { 30 | pub use futures::ready; 31 | pub use tokio::io::unix::AsyncFd; 32 | } 33 | 34 | #[cfg(windows)] 35 | mod os_prelude { 36 | pub use std::mem; 37 | pub use std::ops::{Deref, DerefMut}; 38 | pub use std::os::windows::prelude::*; 39 | pub use tokio::net::windows::named_pipe; 40 | } 41 | 42 | use crate::os_prelude::*; 43 | 44 | /// A type for results generated by interacting with serial ports. 45 | pub type Result = mio_serial::Result; 46 | 47 | /// Async serial port I/O 48 | /// 49 | /// Reading and writing to a `SerialStream` is usually done using the 50 | /// convenience methods found on the [`tokio::io::AsyncReadExt`] and [`tokio::io::AsyncWriteExt`] 51 | /// traits. 52 | /// 53 | /// [`AsyncReadExt`]: trait@tokio::io::AsyncReadExt 54 | /// [`AsyncWriteExt`]: trait@tokio::io::AsyncWriteExt 55 | /// 56 | #[derive(Debug)] 57 | pub struct SerialStream { 58 | #[cfg(unix)] 59 | inner: AsyncFd, 60 | // Named pipes and COM ports are actually two entirely different things that hardly have anything in common. 61 | // The only thing they share is the opaque `HANDLE` type that can be fed into `CreateFileW`, `ReadFile`, `WriteFile`, etc. 62 | // 63 | // Both `mio` and `tokio` don't yet have any code to work on arbitrary HANDLEs. 64 | // But they have code for dealing with named pipes, and we (ab)use that here to work on COM ports. 65 | #[cfg(windows)] 66 | inner: named_pipe::NamedPipeClient, 67 | // The com port is kept around for serialport related methods 68 | #[cfg(windows)] 69 | com: mem::ManuallyDrop, 70 | } 71 | 72 | impl SerialStream { 73 | /// Open serial port from a provided path, using the default reactor. 74 | pub fn open(builder: &crate::SerialPortBuilder) -> crate::Result { 75 | let port = mio_serial::SerialStream::open(builder)?; 76 | 77 | #[cfg(unix)] 78 | { 79 | Ok(Self { 80 | inner: AsyncFd::new(port)?, 81 | }) 82 | } 83 | 84 | #[cfg(windows)] 85 | { 86 | let handle = port.as_raw_handle(); 87 | // Keep the com port around to use for serialport related things 88 | let com = mem::ManuallyDrop::new(port); 89 | Ok(Self { 90 | inner: unsafe { named_pipe::NamedPipeClient::from_raw_handle(handle)? }, 91 | com, 92 | }) 93 | } 94 | } 95 | 96 | /// Create a pair of pseudo serial terminals using the default reactor 97 | /// 98 | /// ## Returns 99 | /// Two connected, unnamed `Serial` objects. 100 | /// 101 | /// ## Errors 102 | /// Attempting any IO or parameter settings on the slave tty after the master 103 | /// tty is closed will return errors. 104 | /// 105 | /// ## Examples 106 | /// 107 | /// ```no_run 108 | /// use tokio_serial::SerialStream; 109 | /// 110 | /// #[tokio::main] 111 | /// async fn main() { 112 | /// let (master, slave) = SerialStream::pair().unwrap(); 113 | /// } 114 | /// ``` 115 | #[cfg(unix)] 116 | pub fn pair() -> crate::Result<(Self, Self)> { 117 | let (master, slave) = mio_serial::SerialStream::pair()?; 118 | 119 | let master = SerialStream { 120 | inner: AsyncFd::new(master)?, 121 | }; 122 | let slave = SerialStream { 123 | inner: AsyncFd::new(slave)?, 124 | }; 125 | Ok((master, slave)) 126 | } 127 | 128 | /// Sets the exclusivity of the port 129 | /// 130 | /// If a port is exclusive, then trying to open the same device path again 131 | /// will fail. 132 | /// 133 | /// See the man pages for the tiocexcl and tiocnxcl ioctl's for more details. 134 | /// 135 | /// ## Errors 136 | /// 137 | /// * `Io` for any error while setting exclusivity for the port. 138 | #[cfg(unix)] 139 | pub fn set_exclusive(&mut self, exclusive: bool) -> crate::Result<()> { 140 | self.inner.get_mut().set_exclusive(exclusive) 141 | } 142 | 143 | /// Returns the exclusivity of the port 144 | /// 145 | /// If a port is exclusive, then trying to open the same device path again 146 | /// will fail. 147 | #[cfg(unix)] 148 | pub fn exclusive(&self) -> bool { 149 | self.inner.get_ref().exclusive() 150 | } 151 | 152 | /// Borrow a reference to the underlying mio-serial::SerialStream object. 153 | #[inline(always)] 154 | fn borrow(&self) -> &mio_serial::SerialStream { 155 | #[cfg(unix)] 156 | { 157 | self.inner.get_ref() 158 | } 159 | #[cfg(windows)] 160 | { 161 | self.com.deref() 162 | } 163 | } 164 | 165 | /// Borrow a mutable reference to the underlying mio-serial::SerialStream object. 166 | #[inline(always)] 167 | fn borrow_mut(&mut self) -> &mut mio_serial::SerialStream { 168 | #[cfg(unix)] 169 | { 170 | self.inner.get_mut() 171 | } 172 | #[cfg(windows)] 173 | { 174 | self.com.deref_mut() 175 | } 176 | } 177 | /// Try to read bytes on the serial port. On success returns the number of bytes read. 178 | /// 179 | /// The function must be called with valid byte array `buf` of sufficient 180 | /// size to hold the message bytes. If a message is too long to fit in the 181 | /// supplied buffer, excess bytes may be discarded. 182 | /// 183 | /// When there is no pending data, `Err(io::ErrorKind::WouldBlock)` is 184 | /// returned. This function is usually paired with `readable()`. 185 | pub fn try_read(&mut self, buf: &mut [u8]) -> IoResult { 186 | #[cfg(unix)] 187 | { 188 | self.inner.get_mut().read(buf) 189 | } 190 | #[cfg(windows)] 191 | { 192 | self.inner.try_read(buf) 193 | } 194 | } 195 | 196 | /// Wait for the port to become readable. 197 | /// 198 | /// This function is usually paired with `try_read()`. 199 | /// 200 | /// The function may complete without the socket being readable. This is a 201 | /// false-positive and attempting a `try_read()` will return with 202 | /// `io::ErrorKind::WouldBlock`. 203 | pub async fn readable(&self) -> IoResult<()> { 204 | let _ = self.inner.readable().await?; 205 | Ok(()) 206 | } 207 | 208 | /// Try to write bytes on the serial port. On success returns the number of bytes written. 209 | /// 210 | /// When the write would block, `Err(io::ErrorKind::WouldBlock)` is 211 | /// returned. This function is usually paired with `writable()`. 212 | pub fn try_write(&mut self, buf: &[u8]) -> IoResult { 213 | #[cfg(unix)] 214 | { 215 | self.inner.get_mut().write(buf) 216 | } 217 | #[cfg(windows)] 218 | { 219 | self.inner.try_write(buf) 220 | } 221 | } 222 | 223 | /// Wait for the port to become writable. 224 | /// 225 | /// This function is usually paired with `try_write()`. 226 | /// 227 | /// The function may complete without the socket being readable. This is a 228 | /// false-positive and attempting a `try_write()` will return with 229 | /// `io::ErrorKind::WouldBlock`. 230 | pub async fn writable(&self) -> IoResult<()> { 231 | let _ = self.inner.writable().await?; 232 | Ok(()) 233 | } 234 | } 235 | 236 | #[cfg(unix)] 237 | impl AsyncRead for SerialStream { 238 | /// Attempts to ready bytes on the serial port. 239 | /// 240 | /// Note that on multiple calls to a `poll_*` method in the read direction, only the 241 | /// `Waker` from the `Context` passed to the most recent call will be scheduled to 242 | /// receive a wakeup. 243 | /// 244 | /// # Return value 245 | /// 246 | /// The function returns: 247 | /// 248 | /// * `Poll::Pending` if the socket is not ready to read 249 | /// * `Poll::Ready(Ok(()))` reads data `ReadBuf` if the socket is ready 250 | /// * `Poll::Ready(Err(e))` if an error is encountered. 251 | /// 252 | /// # Errors 253 | /// 254 | /// This function may encounter any standard I/O error except `WouldBlock`. 255 | fn poll_read( 256 | self: Pin<&mut Self>, 257 | cx: &mut Context<'_>, 258 | buf: &mut ReadBuf<'_>, 259 | ) -> Poll> { 260 | loop { 261 | let mut guard = ready!(self.inner.poll_read_ready(cx))?; 262 | 263 | match guard.try_io(|inner| inner.get_ref().read(buf.initialize_unfilled())) { 264 | Ok(Ok(bytes_read)) => { 265 | buf.advance(bytes_read); 266 | return Poll::Ready(Ok(())); 267 | } 268 | Ok(Err(err)) => { 269 | return Poll::Ready(Err(err)); 270 | } 271 | Err(_would_block) => continue, 272 | } 273 | } 274 | } 275 | } 276 | 277 | #[cfg(unix)] 278 | impl AsyncWrite for SerialStream { 279 | /// Attempts to send data on the serial port 280 | /// 281 | /// Note that on multiple calls to a `poll_*` method in the send direction, 282 | /// only the `Waker` from the `Context` passed to the most recent call will 283 | /// be scheduled to receive a wakeup. 284 | /// 285 | /// # Return value 286 | /// 287 | /// The function returns: 288 | /// 289 | /// * `Poll::Pending` if the socket is not available to write 290 | /// * `Poll::Ready(Ok(n))` `n` is the number of bytes sent 291 | /// * `Poll::Ready(Err(e))` if an error is encountered. 292 | /// 293 | /// # Errors 294 | /// 295 | /// This function may encounter any standard I/O error except `WouldBlock`. 296 | fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { 297 | loop { 298 | let mut guard = ready!(self.inner.poll_write_ready(cx))?; 299 | 300 | match guard.try_io(|inner| inner.get_ref().write(buf)) { 301 | Ok(result) => return Poll::Ready(result), 302 | Err(_would_block) => continue, 303 | } 304 | } 305 | } 306 | 307 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 308 | loop { 309 | let mut guard = ready!(self.inner.poll_write_ready(cx))?; 310 | match guard.try_io(|inner| inner.get_ref().flush()) { 311 | Ok(_) => return Poll::Ready(Ok(())), 312 | Err(_would_block) => continue, 313 | } 314 | } 315 | } 316 | 317 | fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 318 | let _ = self.poll_flush(cx)?; 319 | Ok(()).into() 320 | } 321 | } 322 | 323 | #[cfg(windows)] 324 | impl AsyncRead for SerialStream { 325 | fn poll_read( 326 | self: Pin<&mut Self>, 327 | cx: &mut Context<'_>, 328 | buf: &mut ReadBuf<'_>, 329 | ) -> Poll> { 330 | let mut self_ = self; 331 | Pin::new(&mut self_.inner).poll_read(cx, buf) 332 | } 333 | } 334 | 335 | #[cfg(windows)] 336 | impl AsyncWrite for SerialStream { 337 | fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { 338 | let mut self_ = self; 339 | Pin::new(&mut self_.inner).poll_write(cx, buf) 340 | } 341 | 342 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 343 | let mut self_ = self; 344 | Pin::new(&mut self_.inner).poll_flush(cx) 345 | } 346 | 347 | fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 348 | let mut self_ = self; 349 | Pin::new(&mut self_.inner).poll_shutdown(cx) 350 | } 351 | } 352 | 353 | impl crate::SerialPort for SerialStream { 354 | #[inline(always)] 355 | fn name(&self) -> Option { 356 | self.borrow().name() 357 | } 358 | 359 | #[inline(always)] 360 | fn baud_rate(&self) -> crate::Result { 361 | self.borrow().baud_rate() 362 | } 363 | 364 | #[inline(always)] 365 | fn data_bits(&self) -> crate::Result { 366 | self.borrow().data_bits() 367 | } 368 | 369 | #[inline(always)] 370 | fn flow_control(&self) -> crate::Result { 371 | self.borrow().flow_control() 372 | } 373 | 374 | #[inline(always)] 375 | fn parity(&self) -> crate::Result { 376 | self.borrow().parity() 377 | } 378 | 379 | #[inline(always)] 380 | fn stop_bits(&self) -> crate::Result { 381 | self.borrow().stop_bits() 382 | } 383 | 384 | #[inline(always)] 385 | fn timeout(&self) -> Duration { 386 | Duration::from_secs(0) 387 | } 388 | 389 | #[inline(always)] 390 | fn set_baud_rate(&mut self, baud_rate: u32) -> crate::Result<()> { 391 | self.borrow_mut().set_baud_rate(baud_rate) 392 | } 393 | 394 | #[inline(always)] 395 | fn set_data_bits(&mut self, data_bits: crate::DataBits) -> crate::Result<()> { 396 | self.borrow_mut().set_data_bits(data_bits) 397 | } 398 | 399 | #[inline(always)] 400 | fn set_flow_control(&mut self, flow_control: crate::FlowControl) -> crate::Result<()> { 401 | self.borrow_mut().set_flow_control(flow_control) 402 | } 403 | 404 | #[inline(always)] 405 | fn set_parity(&mut self, parity: crate::Parity) -> crate::Result<()> { 406 | self.borrow_mut().set_parity(parity) 407 | } 408 | 409 | #[inline(always)] 410 | fn set_stop_bits(&mut self, stop_bits: crate::StopBits) -> crate::Result<()> { 411 | self.borrow_mut().set_stop_bits(stop_bits) 412 | } 413 | 414 | #[inline(always)] 415 | fn set_timeout(&mut self, _: Duration) -> crate::Result<()> { 416 | Ok(()) 417 | } 418 | 419 | #[inline(always)] 420 | fn write_request_to_send(&mut self, level: bool) -> crate::Result<()> { 421 | self.borrow_mut().write_request_to_send(level) 422 | } 423 | 424 | #[inline(always)] 425 | fn write_data_terminal_ready(&mut self, level: bool) -> crate::Result<()> { 426 | self.borrow_mut().write_data_terminal_ready(level) 427 | } 428 | 429 | #[inline(always)] 430 | fn read_clear_to_send(&mut self) -> crate::Result { 431 | self.borrow_mut().read_clear_to_send() 432 | } 433 | 434 | #[inline(always)] 435 | fn read_data_set_ready(&mut self) -> crate::Result { 436 | self.borrow_mut().read_data_set_ready() 437 | } 438 | 439 | #[inline(always)] 440 | fn read_ring_indicator(&mut self) -> crate::Result { 441 | self.borrow_mut().read_ring_indicator() 442 | } 443 | 444 | #[inline(always)] 445 | fn read_carrier_detect(&mut self) -> crate::Result { 446 | self.borrow_mut().read_carrier_detect() 447 | } 448 | 449 | #[inline(always)] 450 | fn bytes_to_read(&self) -> crate::Result { 451 | self.borrow().bytes_to_read() 452 | } 453 | 454 | #[inline(always)] 455 | fn bytes_to_write(&self) -> crate::Result { 456 | self.borrow().bytes_to_write() 457 | } 458 | 459 | #[inline(always)] 460 | fn clear(&self, buffer_to_clear: crate::ClearBuffer) -> crate::Result<()> { 461 | self.borrow().clear(buffer_to_clear) 462 | } 463 | 464 | /// Cloning SerialStream is not supported. 465 | /// 466 | /// # Errors 467 | /// Always returns `ErrorKind::Other` with a message. 468 | #[inline(always)] 469 | fn try_clone(&self) -> crate::Result> { 470 | Err(crate::Error::new( 471 | crate::ErrorKind::Io(std::io::ErrorKind::Other), 472 | "Cannot clone Tokio handles", 473 | )) 474 | } 475 | 476 | #[inline(always)] 477 | fn set_break(&self) -> crate::Result<()> { 478 | self.borrow().set_break() 479 | } 480 | 481 | #[inline(always)] 482 | fn clear_break(&self) -> crate::Result<()> { 483 | self.borrow().clear_break() 484 | } 485 | } 486 | 487 | impl Read for SerialStream { 488 | fn read(&mut self, buf: &mut [u8]) -> IoResult { 489 | self.try_read(buf) 490 | } 491 | } 492 | 493 | impl Write for SerialStream { 494 | fn write(&mut self, buf: &[u8]) -> IoResult { 495 | self.try_write(buf) 496 | } 497 | 498 | fn flush(&mut self) -> IoResult<()> { 499 | self.borrow_mut().flush() 500 | } 501 | } 502 | 503 | #[cfg(unix)] 504 | impl TryFrom for SerialStream { 505 | type Error = Error; 506 | 507 | fn try_from(value: serialport::TTYPort) -> std::result::Result { 508 | let port = mio_serial::SerialStream::try_from(value)?; 509 | Ok(Self { 510 | inner: AsyncFd::new(port)?, 511 | }) 512 | } 513 | } 514 | 515 | #[cfg(unix)] 516 | mod sys { 517 | use super::SerialStream; 518 | use std::os::unix::io::{AsRawFd, RawFd}; 519 | impl AsRawFd for SerialStream { 520 | fn as_raw_fd(&self) -> RawFd { 521 | self.inner.as_raw_fd() 522 | } 523 | } 524 | } 525 | 526 | #[cfg(windows)] 527 | mod io { 528 | use super::SerialStream; 529 | use std::os::windows::io::{AsRawHandle, RawHandle}; 530 | impl AsRawHandle for SerialStream { 531 | fn as_raw_handle(&self) -> RawHandle { 532 | self.inner.as_raw_handle() 533 | } 534 | } 535 | } 536 | 537 | /// An extension trait for serialport::SerialPortBuilder 538 | /// 539 | /// This trait adds one method to SerialPortBuilder: 540 | /// 541 | /// - open_native_async 542 | /// 543 | /// This method mirrors the `open_native` method of SerialPortBuilder 544 | pub trait SerialPortBuilderExt { 545 | /// Open a platform-specific interface to the port with the specified settings 546 | fn open_native_async(self) -> Result; 547 | } 548 | 549 | impl SerialPortBuilderExt for SerialPortBuilder { 550 | /// Open a platform-specific interface to the port with the specified settings 551 | fn open_native_async(self) -> Result { 552 | SerialStream::open(&self) 553 | } 554 | } 555 | --------------------------------------------------------------------------------