├── .github ├── NOTES.md ├── com0com.cer └── workflows │ └── github-ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── appveyor.yml ├── examples └── read_serialport.rs ├── src └── lib.rs └── tests ├── common.rs └── test_serialstream.rs /.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 | -------------------------------------------------------------------------------- /.github/com0com.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/berkowski/mio-serial/a71c0bd095df89b0ff7a2fd538faaa878ae69466/.github/com0com.cer -------------------------------------------------------------------------------- /.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.70.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.70.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.70.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | Cargo.lock 4 | 5 | *.bk 6 | -------------------------------------------------------------------------------- /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.0.3 and 5.0.4] 2023-01-12 9 | - update dependencies 10 | 11 | ## [5.0.2] 2022-03-04 12 | - merged [#28](https://github.com/berkowski/mio-serial/pull/28) 13 | - update dependencies 14 | 15 | ## [5.0.1] 2021-08-08 16 | Minor update to README.md 17 | 18 | ### Changed 19 | - Updated README.md example to use current release version of mio-serial 20 | 21 | ## [5.0.0] 2021-08-08 22 | 23 | Final release of version 5.0. No code changes since the previous 4.0.0-beta4. 24 | 25 | ### Added 26 | - [ColinFinck](https://github.com/ColinFinck) added as maintainer for `mio-serial` 27 | - [estokes](https://github.com/estokes) added as maintainer for `mio-serial` 28 | 29 | ## [4.0.0] SKIPPED 30 | 31 | A final release of version 4.0 is skipped due to version scheme (see [this comment](https://github.com/berkowski/tokio-serial/pull/42#issuecomment-881898536)). This entry is just here for some clarifcation. 32 | 33 | ## [4.0.0-beta4] 2021-07-23 34 | 35 | ### Added 36 | - Check in CI tests for building against the MSRV (currently `1.41.0`) 37 | 38 | ### Changed 39 | - Error returned from `SerialPort::try_clone` changed back to `std::io::ErrorKind::Other` 40 | (had switched to `std::io::ErrorKind::Unsupported`). `Unsupported` requires MSRV of `1.53` 41 | which is too high. (fix [#27](https://github.com/berkowski/mio-serial/issues/27)) 42 | 43 | ## [4.0.0-beta3] 2021-07-22 44 | 45 | ### Added 46 | - Some logging hooks for debugging 47 | 48 | ### Changed 49 | - Renamed `SerialPortBuilderExt::open_async` to `SerialPortBuilderExt::open_native_async` to reflect the original 50 | intention 51 | 52 | ## [4.0.0-beta2] 2021-07-16 53 | 54 | ### Added 55 | - AsRawHandle, FromRawHandle, and IntoRawHandle impls for SerialStream on Windows 56 | 57 | ### Fixed 58 | - Potential double Drop issue on Windows between NamedPipe and COMPort 59 | 60 | ## [4.0.0-beta1] 2021-07-13 61 | This is a major update crossing two API-breaking dependency version jumps in `mio` and 62 | `serialport-rs`. 63 | 64 | ### BREAKING CHANGES 65 | This release contains multiple API breaking changes with the move to [serialport-rs](https://gitlab.com/sussurrrus/serialport-rs) v4. 66 | Additional breaking changes were made to make the API more like mio/tokio where platform-specific 67 | implimentation details are provided with `#cfg[]` guards instead of discrete structures like in `serialport-rs` 68 | 69 | Specifically: 70 | 71 | * Removed platform-specific `mio_serial::windows::Serial` and `mio_serial::unix::Serial` 72 | * Added `mio_serial::SerialStream` with platform specific requirements at compile time with `#[cfg()]` 73 | * Removed `COMPort::from_path`, use `SerialStream::open` 74 | * Removed `TTYPort::from_path`, use `SerialStream::open` 75 | * Removed `TTYPort::from_serial`. Replaced with impl of `std::convert::TryFrom` 76 | * Removed `SerialPortSettings`, `serialport-rs` now uses the builder pattern 77 | 78 | ### Changed 79 | * Removed "libudev" from the default features. Still available for use when desired. 80 | * Bumped [nix](https://github.com/nix-rust/nix) to 0.22 81 | * Bumped [mio](https://github.com/tokio-rs/mio) to 0.7 82 | * Bumped [serialport-rs](https://gitlab.com/sussurrrus/serialport-rs) to 4.0.0 83 | * Changed CHANGELOG from asciidoc to markdown 84 | 85 | ### Added 86 | * `SerialStream` structure as the common entry point for serial port IO. 87 | * `SerialPortBuilderExt` extension trait to add `open_async` method 88 | to `serialport::SerialPortBuilder` much like the already existing `open` method. 89 | 90 | ### Other 91 | * Switched CI to appveyor for Windows, OSX, and Linux. It doesn't test as many targets, but some checks are better 92 | than none now that travis-ci is no longer an option. 93 | 94 | ## [3.3.1] 2020-03-15 95 | ### Added 96 | * @flosse added #derive Debug support for the Serial struct in [#20](https://github.com/berkowski/mio-serial/pull/20) 97 | * @vleesvlieg added automatic retrying for EINTR returns to file descriptors in [#21](https://github.com/berkowski/mio-serial/pull/21) 98 | 99 | ### Changed 100 | * Bumped [nix](https://github.com/nix-rust/nix) to 0.17 101 | 102 | ## [3.3.0] 2019-08-23 103 | * Bumped [serialport-rs](https://gitlab.com/susurrus/serialport-rs) to 3.3 104 | 105 | ## [3.2.14] 2019-06-01 106 | ### Changed 107 | * Bumped [nix](https://github.com/nix-rust/nix) to 0.14 to address [#17](https://github.com/berkowski/mio-serial/issues/17) 108 | 109 | ## [3.2] 2019-01-12 110 | ### Changed 111 | * Bumped [serialport-rs](https://gitlab.com/susurrus/serialport-rs) to 3.2 112 | 113 | ## [3.1.1] 2019-01-12 114 | ### Changed 115 | * Merged [#16](https://github/berkowski/mio-serial/pull/16) @yuja fixed feature flags 116 | 117 | ## [3.1] 2018-11-10 118 | ### Added 119 | * Added "libudev" feature. Enabled by default, can be disabled for targets without udev support. 120 | 121 | ### Changed 122 | * Bumped [serialport-rs](https://gitlab.com/susurrus/serialport-rs) to 3.1 123 | * Merged [#13](https://github.com/berkowski/mio-serial/pull/13) @dvtomas added some clarity to the example. 124 | 125 | ## [3.0.1] - 2018-11-06 126 | ### Changed 127 | * Restricted [serialport-rs](https://gitlab.com/susurrus/serialport-rs) to 3.0 128 | serialport-rs 3.1 contains API breaking changes. 129 | 130 | ## [3.0.0] - 2018-10-06 131 | ### Changed 132 | * Bumped [serialport-rs](https://gitlab.com/susurrus/serialport-rs) to 3.0 133 | serialport-rs 3.0 contains breaking changes. 134 | * Bumped [nix](https://github.com/nix-rust/nix) to 0.11 135 | * `mio-serial` version number will now track upstream serialport-rs. mio-serial 136 | is mostly feature complete at this point (at least for *nix) and this should 137 | help reduce confusion. 138 | 139 | ### Fixed 140 | * Merged [#10](https://github.com/berkowski/mio-serial/pull/10) (thanks @yuja!). Addresses some 141 | windows timeout settings. 142 | 143 | ## [0.8.0] - 2018-03-31 144 | ### Changed 145 | * Bumped [serialport-rs](https://gitlab.com/susurrus/serialport-rs) to 2.3 146 | 147 | ### Added 148 | * Merged[#5](https://github.com/berkowski/mio-serial/pull/5) @ndusart added `try_clone` implementations as requred 149 | by the serialport trait as of 2.3 150 | * Closed[#6](https://github.com/berkowski/mio-serial/pull/6) @snorp also drew attention to the `try_clone` addition 151 | 152 | ## [0.7.0] - 2018-02-25 153 | ### Changed 154 | * Bumped [serialport-rs](https://gitlab.com/susurrus/serialport-rs) to 2.1 155 | 156 | ### Added 157 | * Merged[#4](https://github.com/berkowski/mio-serial/pull/4) @ndusart added windows support! 158 | * Added appveyor config to support new windows impl. 159 | 160 | ## [0.6.0] - 2017-11-28 161 | ### Added 162 | * Closed [#3](https://github.com/berkowski/mio-serial/pull/3) Reexport serialport::Error for error handling without importing serialport crate. 163 | Thanks @Idanko 164 | 165 | ## [0.5.0] - 2017-04-15 166 | ### Added 167 | * Added [trust](https://github.com/japaric/trust) based ci 168 | 169 | ### Changed 170 | * Changed license back to MIT now that `serialport-rs` is MPL-2.0 171 | * Bumped `serialport-rs` dependency to 1.0 172 | 173 | ## [0.4.0] - 2017-02-13 174 | ### Changed 175 | * Changed to LGPL-3 for compliance with `serialport` dependency. 176 | 177 | ## [0.3.0] - 2017-02-13 [YANKED] 178 | ### Added 179 | * Bumped `serialport` dependency to 0.9 180 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mio-serial" 3 | version = "5.0.6" 4 | authors = ["Zac Berkowitz "] 5 | description = "A serial port implementation for mio" 6 | license = "MIT" 7 | homepage = "https://github.com/berkowski/mio-serial" 8 | repository = "https://github.com/berkowski/mio-serial" 9 | documentation = "https://docs.rs/mio-serial" 10 | readme = "README.md" 11 | keywords = ["rs232", "serial", "mio"] 12 | categories = ["asynchronous", "hardware-support"] 13 | edition = "2021" 14 | 15 | [package.metadata] 16 | msrv = "1.78.0" # Used by cargo-msrv 17 | 18 | [features] 19 | default = [] 20 | libudev = ["serialport/libudev"] 21 | serde = ["serialport/serde"] 22 | 23 | [dependencies.mio] 24 | version = "1" 25 | features = ["os-poll", "os-ext"] 26 | 27 | [dependencies.serialport] 28 | version = "4" 29 | default-features = false 30 | 31 | [dependencies.log] 32 | version = "0.4" 33 | 34 | [target.'cfg(unix)'.dependencies] 35 | nix = { version = "0.29", features = ["term"] } 36 | 37 | [target.'cfg(windows)'.dependencies] 38 | winapi = { version = "0.3", features = [ 39 | "commapi", 40 | "handleapi", 41 | "winbase", 42 | "std", 43 | ] } 44 | 45 | [dev-dependencies.env_logger] 46 | version = "0.11" 47 | 48 | [[example]] 49 | name = "read_serialport" 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Zac Berkowitz (zac.berkowitz@gmail.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /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/mio-serial.svg 6 | [crates-url]: https://crates.io/crates/mio-serial 7 | [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg 8 | [mit-url]: https://github.com/berkowski/mio-serial/blob/master/LICENSE 9 | [actions-badge]: https://github.com/berkowski/mio-serial/actions/workflows/github-ci.yml/badge.svg 10 | [actions-url]: https://github.com/berkowski/mio-serial/actions?query=workflow%3Agithub-ci+branch%3Amaster 11 | 12 | # mio-serial: A serial port IO library MIO. 13 | 14 | mio-serial provides a serial port implementation using [mio](https://github.com/carllerche/mio). 15 | 16 | ## Usage 17 | 18 | Add `mio-serial` to you `Cargo.toml`: 19 | 20 | ```toml 21 | [dependencies] 22 | mio-serial = "5.0.1" 23 | ``` 24 | 25 | Then add this to your crate root: 26 | 27 | ```rust 28 | extern crate mio_serial; 29 | ``` 30 | 31 | ## Features 32 | 33 | The "libudev" dependency of `serialport-rs` is enabled by default. For x86 linux systems this enables the `available_ports` function for port enumeration. 34 | Not all targets support udev, especially when cross-compiling. To disable this feature, compile with the `--no-default-features` option. For example: 35 | 36 | ``` 37 | cargo build --no-default-features 38 | ``` 39 | 40 | ### MSRV 41 | The Minimum Supported Rust Version is **1.78.0** as found using [cargo-msrv](https://crates.io/crates/cargo-msrv) 42 | 43 | ## Examples 44 | A few examples can be found [here](https://github.com/berkowski/mio-serial/tree/master/examples). 45 | 46 | ## Tests 47 | Useful tests for serial ports require... serial ports, and serial ports are not often provided by online CI providers. 48 | As so, automated build testing are really only check whether the code compiles, not whether it works. 49 | 50 | Integration tests are in the `tests/` directory and typically require two serial ports to run. 51 | The names of the serial ports can be configured at run time by setting the `TEST_PORT_NAMES` environment variable 52 | to a semi-colon delimited string with the two serial port names. The default values are: 53 | 54 | - For Unix: `TEST_PORT_NAMES=/dev/ttyUSB0;/dev/ttyUSB1` 55 | - For Windows: `TEST_PORT_NAMES=COM1;COM2` 56 | 57 | **IMPORTANT** To prevent multiple tests from talking to the same ports at the same time make sure to limit the number 58 | of test threads to 1 using: 59 | 60 | ```sh 61 | cargo test -j1 -- --test-threads=1 62 | ``` 63 | 64 | ## License 65 | This software is licensed under [MIT](https://opensource.org/licenses/MIT). 66 | 67 | This software builds upon the [MPL-2.0](https://opensource.org/licenses/MPL-2.0) licensed [serialport-rs](https://gitlab.com/susurrus/serialport-rs) and 68 | constitutes a "Larger Work" by that license. The source for [serialport-rs](https://gitlab.com/susurrus/serialport-rs) can be found at https://gitlab.com/susurrus/serialport-rs. 69 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: 2 | - Ubuntu 3 | - macOS 4 | - Visual Studio 2019 5 | 6 | ## Build Matrix ## 7 | 8 | # This configuration will setup a build for each channel & target combination (12 windows 9 | # combinations in all). 10 | # 11 | # There are 3 channels: stable, beta, and nightly. 12 | # 13 | # Alternatively, the full version may be specified for the channel to build using that specific 14 | # version (e.g. channel: 1.5.0) 15 | # 16 | # The values for target are the set of windows Rust build targets. Each value is of the form 17 | # 18 | # ARCH-pc-windows-TOOLCHAIN 19 | # 20 | # Where ARCH is the target architecture, either x86_64 or i686, and TOOLCHAIN is the linker 21 | # toolchain to use, either msvc or gnu. See https://www.rust-lang.org/downloads.html#win-foot for 22 | # a description of the toolchain differences. 23 | # See https://github.com/rust-lang-nursery/rustup.rs/#toolchain-specification for description of 24 | # toolchains and host triples. 25 | # 26 | # Comment out channel/target combos you do not wish to build in CI. 27 | # 28 | # You may use the `cargoflags` and `RUSTFLAGS` variables to set additional flags for cargo commands 29 | # and rustc, respectively. For instance, you can uncomment the cargoflags lines in the nightly 30 | # channels to enable unstable features when building for nightly. Or you could add additional 31 | # matrix entries to test different combinations of features. 32 | 33 | environment: 34 | APPVEYOR_YML_DISABLE_PS_LINUX: true 35 | matrix: 36 | - channel: stable 37 | - channel: beta 38 | - channel: nightly 39 | 40 | # OSX tests are allowed to fail until upstream serialport-rs crate fixes handling buadrate control for virtual serial 41 | # ports. See: 42 | # - https://gitlab.com/susurrus/serialport-rs/-/issues/105 43 | # - https://gitlab.com/susurrus/serialport-rs/-/merge_requests/101 44 | 45 | matrix: 46 | allow_failures: 47 | - image: macOS 48 | # This is the most important part of the Appveyor configuration. This installs the version of Rust 49 | # specified by the 'channel' and 'target' environment variables from the build matrix. This uses 50 | # rustup to install Rust. 51 | # 52 | # For simple configurations, instead of using the build matrix, you can simply set the 53 | # default-toolchain and default-host manually here. 54 | install: 55 | # Windows Install 56 | - cmd: cd .github 57 | - cmd: C:\"Program Files (x86)"\"Windows Kits"\10\bin\10.0.19041.0\x64\CertMgr.exe /add com0com.cer /s /r localMachine root 58 | - cmd: C:\"Program Files (x86)"\"Windows Kits"\10\bin\10.0.19041.0\x64\CertMgr.exe /add com0com.cer /s /r localMachine trustedpublisher 59 | - cmd: set CNC_INSTALL_CNCA0_CNCB0_PORTS=YES 60 | - cmd: .\setup_com0com_W7_x64_signed.exe /S 61 | - cmd: cd C:\"Program Files (x86)"\com0com 62 | - cmd: .\setupc.exe install PortName=COM10,EmuBR=yes PortName=COM11,EmuBR=yes 63 | - cmd: cd "%APPVEYOR_BUILD_FOLDER%" 64 | - cmd: appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe 65 | - cmd: rustup-init -yv --default-toolchain %channel% 66 | - cmd: set PATH=%PATH%;%USERPROFILE%\.cargo\bin 67 | # - cmd: choco install com0com 68 | # - cmd: cd "C:\Program Files (x86)\com0com\" 69 | # - cmd: set CNC_INSTALL_CNCA0_CNCB0_PORTS=YES 70 | # - cmd: dir 71 | # - cmd: .\setupc.exe --silent install PortName=COM10,EmuBR=yes PortName=COM11,EmuBR=yes 72 | # - cmd: cd "%APPVEYOR_BUILD_FOLDER%" 73 | # Linux install (socat already installed) 74 | - sh: echo Image is $APPVEYOR_BUILD_WORKER_IMAGE 75 | - sh: if [ "$APPVEYOR_BUILD_WORKER_IMAGE" = "macOS" ]; then HOMEBREW_NO_AUTO_UPDATE=1 brew install socat; fi 76 | # - sh: sudo apt-get update && sudo apt-get install socat 77 | - sh: curl --proto '=https' --tlsv1.2 -sSf -o rustup-init https://sh.rustup.rs 78 | - sh: chmod u+x rustup-init 79 | - sh: ./rustup-init -y --default-toolchain ${channel} 80 | - sh: source $HOME/.cargo/env 81 | #- sh: export PATH=${PATH};${HOME}/.cargo/bin 82 | - rustc -vV 83 | - cargo -vV 84 | - sh: socat -V 85 | 86 | ## Build Script ## 87 | 88 | # 'cargo test' takes care of building for us, so disable Appveyor's build stage. This prevents 89 | # the "directory does not contain a project or solution file" error. 90 | build: false 91 | 92 | # Don't run tests, they require actual serial ports and will fail on CI 93 | test_script: 94 | - cmd: set TEST_PORT_NAMES=COM10;COM11 95 | - cmd: set RUST_LOG=trace 96 | - sh: export TEST_PORT_NAMES='/tmp/ttyS0;/tmp/ttyS1' 97 | - sh: set RUST_LOG=trace 98 | - cargo test -j1 -- --test-threads=1 99 | -------------------------------------------------------------------------------- /examples/read_serialport.rs: -------------------------------------------------------------------------------- 1 | //! Simple example that echoes received serial traffic to stdout 2 | extern crate mio; 3 | extern crate mio_serial; 4 | 5 | use log::error; 6 | use mio::{Events, Interest, Poll, Token}; 7 | 8 | use std::env; 9 | use std::io; 10 | use std::io::Read; 11 | use std::str; 12 | 13 | use mio_serial::SerialPortBuilderExt; 14 | 15 | const SERIAL_TOKEN: Token = Token(0); 16 | 17 | #[cfg(unix)] 18 | const DEFAULT_TTY: &str = "/dev/ttyUSB0"; 19 | #[cfg(windows)] 20 | const DEFAULT_TTY: &str = "COM6"; 21 | 22 | const DEFAULT_BAUD: u32 = 9600; 23 | 24 | pub fn main() -> io::Result<()> { 25 | let mut args = env::args(); 26 | let path = args.nth(1).unwrap_or(DEFAULT_TTY.into()); 27 | // let baud = DEFAULT_BAUD; 28 | 29 | // Create a poll instance. 30 | let mut poll = Poll::new()?; 31 | // Create storage for events. Since we will only register a single serialport, a 32 | // capacity of 1 will do. 33 | let mut events = Events::with_capacity(1); 34 | 35 | // Create the serial port 36 | println!("Opening {path} at 9600,8N1"); 37 | let mut rx = mio_serial::new(path, DEFAULT_BAUD).open_native_async()?; 38 | 39 | // #[cfg(unix)] 40 | // let mut rx = mio_serial::TTYPort::open(&builder)?; 41 | // #[cfg(windows)] 42 | // let mut rx = mio_serial::COMPort::open(&builder)?; 43 | 44 | poll.registry() 45 | .register(&mut rx, SERIAL_TOKEN, Interest::READABLE) 46 | .unwrap(); 47 | 48 | let mut buf = [0u8; 1024]; 49 | 50 | loop { 51 | // Poll to check if we have events waiting for us. 52 | poll.poll(&mut events, None)?; 53 | 54 | // Process each event. 55 | for event in events.iter() { 56 | // Validate the token we registered our socket with, 57 | // in this example it will only ever be one but we 58 | // make sure it's valid none the less. 59 | if event.token() == SERIAL_TOKEN { 60 | loop { 61 | // In this loop we receive all packets queued for the socket. 62 | match rx.read(&mut buf) { 63 | Ok(count) => { 64 | println!("{:?}", String::from_utf8_lossy(&buf[..count])); 65 | } 66 | Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { 67 | break; 68 | } 69 | Err(e) => { 70 | println!("Quitting due to read error: {e}"); 71 | return Err(e); 72 | } 73 | } 74 | } 75 | } else { 76 | // This should never happen as we only registered our 77 | // `UdpSocket` using the `UDP_SOCKET` token, but if it ever 78 | // does we'll log it. 79 | error!("Got event for unexpected token: {:?}", event); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # mio-serial - Serial port I/O for mio 2 | //! 3 | //! This crate provides a serial port implementation compatable with mio. 4 | //! 5 | //! **Windows support is present but largely untested by the author** 6 | //! 7 | //! ## Links 8 | //! - repo: 9 | //! - docs: 10 | #![deny(missing_docs)] 11 | #![warn(rust_2018_idioms)] 12 | 13 | // Enums, Structs, and Traits from the serialport crate 14 | pub use serialport::{ 15 | // Enums 16 | ClearBuffer, 17 | DataBits, 18 | // Structs 19 | Error, 20 | ErrorKind, 21 | FlowControl, 22 | Parity, 23 | // Types 24 | Result, 25 | // Traits 26 | SerialPort, 27 | SerialPortBuilder, 28 | SerialPortInfo, 29 | SerialPortType, 30 | StopBits, 31 | UsbPortInfo, 32 | }; 33 | 34 | // Re-export port-enumerating utility function. 35 | pub use serialport::available_ports; 36 | 37 | // Re-export creation of SerialPortBuilder objects 38 | pub use serialport::new; 39 | 40 | use mio::{event::Source, Interest, Registry, Token}; 41 | use std::io::{Error as StdIoError, ErrorKind as StdIoErrorKind, Result as StdIoResult}; 42 | use std::time::Duration; 43 | 44 | #[cfg(unix)] 45 | mod os_prelude { 46 | pub use mio::unix::SourceFd; 47 | pub use nix::{self, libc}; 48 | pub use serialport::TTYPort as NativeBlockingSerialPort; 49 | pub use std::os::unix::prelude::*; 50 | } 51 | 52 | #[cfg(windows)] 53 | mod os_prelude { 54 | pub use mio::windows::NamedPipe; 55 | pub use serialport::COMPort as NativeBlockingSerialPort; 56 | pub use std::ffi::OsStr; 57 | pub use std::io; 58 | pub use std::mem; 59 | pub use std::os::windows::ffi::OsStrExt; 60 | pub use std::os::windows::io::{FromRawHandle, RawHandle}; 61 | pub use std::path::Path; 62 | pub use std::ptr; 63 | pub use winapi::um::commapi::SetCommTimeouts; 64 | pub use winapi::um::fileapi::*; 65 | pub use winapi::um::handleapi::INVALID_HANDLE_VALUE; 66 | pub use winapi::um::winbase::{COMMTIMEOUTS, FILE_FLAG_OVERLAPPED}; 67 | pub use winapi::um::winnt::{ 68 | FILE_ATTRIBUTE_NORMAL, GENERIC_READ, GENERIC_WRITE, HANDLE, 69 | }; 70 | } 71 | use os_prelude::*; 72 | 73 | /// A [`SerialStream`]. 74 | #[derive(Debug)] 75 | pub struct SerialStream { 76 | #[cfg(unix)] 77 | inner: serialport::TTYPort, 78 | #[cfg(windows)] 79 | inner: mem::ManuallyDrop, 80 | #[cfg(windows)] 81 | pipe: NamedPipe, 82 | } 83 | 84 | impl SerialStream { 85 | /// Open a nonblocking serial port from the provided builder 86 | /// 87 | /// ## Example 88 | /// 89 | /// ```no_run 90 | /// use mio_serial::{SerialPortBuilder, SerialStream}; 91 | /// use std::path::Path; 92 | /// 93 | /// let args = mio_serial::new("/dev/ttyUSB0", 9600); 94 | /// let serial = SerialStream::open(&args).unwrap(); 95 | /// ``` 96 | pub fn open(builder: &crate::SerialPortBuilder) -> crate::Result { 97 | log::debug!("opening serial port in synchronous blocking mode"); 98 | let port = NativeBlockingSerialPort::open(builder)?; 99 | Self::try_from(port) 100 | } 101 | 102 | /// Create a pair of pseudo serial terminals 103 | /// 104 | /// ## Returns 105 | /// Two connected `Serial` objects: `(master, slave)` 106 | /// 107 | /// ## Errors 108 | /// Attempting any IO or parameter settings on the slave tty after the master 109 | /// tty is closed will return errors. 110 | /// 111 | /// ## Examples 112 | /// 113 | /// ``` 114 | /// use mio_serial::SerialStream; 115 | /// 116 | /// let (master, slave) = SerialStream::pair().unwrap(); 117 | /// ``` 118 | #[cfg(unix)] 119 | pub fn pair() -> crate::Result<(Self, Self)> { 120 | let (master, slave) = NativeBlockingSerialPort::pair()?; 121 | 122 | let master = Self::try_from(master)?; 123 | let slave = Self::try_from(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.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.exclusive() 150 | } 151 | 152 | /// Attempts to clone the `SerialPort`. This allow you to write and read simultaneously from the 153 | /// same serial connection. 154 | /// 155 | /// Also, you must be very careful when changing the settings of a cloned `SerialPort` : since 156 | /// the settings are cached on a per object basis, trying to modify them from two different 157 | /// objects can cause some nasty behavior. 158 | /// 159 | /// This is the same as `SerialPort::try_clone()` but returns the concrete type instead. 160 | /// 161 | /// # Errors 162 | /// 163 | /// This function returns an error if the serial port couldn't be cloned. 164 | /// 165 | /// # DON'T USE THIS AS-IS 166 | /// 167 | /// This logic has never really completely worked. Cloned file descriptors in asynchronous 168 | /// code is a semantic minefield. Are you cloning the file descriptor? Are you cloning the 169 | /// event flags on the file descriptor? Both? It's a bit of a mess even within one OS, 170 | /// let alone across multiple OS's 171 | /// 172 | /// Maybe it can be done with more work, but until a clear use-case is required (or mio/tokio 173 | /// gets an equivalent of the unix `AsyncFd` for async file handles, see 174 | /// https://github.com/tokio-rs/tokio/issues/3781 and 175 | /// https://github.com/tokio-rs/tokio/pull/3760#issuecomment-839854617) I would rather not 176 | /// have any enabled code over a kind-of-works-maybe impl. So I'll leave this code here 177 | /// for now but hard-code it disabled. 178 | #[cfg(any())] 179 | pub fn try_clone_native(&self) -> Result { 180 | // This works so long as the underlying serialport-rs method doesn't do anything but 181 | // duplicate the low-level file descriptor. This is the case as of serialport-rs:4.0.1 182 | let cloned_native = self.inner.try_clone_native()?; 183 | #[cfg(unix)] 184 | { 185 | Ok(Self { 186 | inner: cloned_native, 187 | }) 188 | } 189 | #[cfg(windows)] 190 | { 191 | // Same procedure as used in serialport-rs for duplicating raw handles 192 | // https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-duplicatehandle 193 | // states that it can be used as well for pipes created with CreateNamedPipe as well 194 | let pipe_handle = self.pipe.as_raw_handle(); 195 | 196 | let process_handle: HANDLE = unsafe { GetCurrentProcess() }; 197 | let mut cloned_pipe_handle: HANDLE = INVALID_HANDLE_VALUE; 198 | unsafe { 199 | DuplicateHandle( 200 | process_handle, 201 | pipe_handle, 202 | process_handle, 203 | &mut cloned_pipe_handle, 204 | 0, 205 | TRUE, 206 | DUPLICATE_SAME_ACCESS, 207 | ); 208 | if cloned_pipe_handle != INVALID_HANDLE_VALUE { 209 | let cloned_pipe = unsafe { NamedPipe::from_raw_handle(cloned_pipe_handle) }; 210 | Ok(Self { 211 | inner: mem::ManuallyDrop::new(cloned_native), 212 | pipe: cloned_pipe, 213 | }) 214 | } else { 215 | Err(StdIoError::last_os_error().into()) 216 | } 217 | } 218 | } 219 | } 220 | } 221 | 222 | impl crate::SerialPort for SerialStream { 223 | /// Return the name associated with the serial port, if known. 224 | #[inline(always)] 225 | fn name(&self) -> Option { 226 | self.inner.name() 227 | } 228 | 229 | /// Returns the current baud rate. 230 | /// 231 | /// This function returns `None` if the baud rate could not be determined. This may occur if 232 | /// the hardware is in an uninitialized state. Setting a baud rate with `set_baud_rate()` 233 | /// should initialize the baud rate to a supported value. 234 | #[inline(always)] 235 | fn baud_rate(&self) -> crate::Result { 236 | self.inner.baud_rate() 237 | } 238 | 239 | /// Returns the character size. 240 | /// 241 | /// This function returns `None` if the character size could not be determined. This may occur 242 | /// if the hardware is in an uninitialized state or is using a non-standard character size. 243 | /// Setting a baud rate with `set_char_size()` should initialize the character size to a 244 | /// supported value. 245 | #[inline(always)] 246 | fn data_bits(&self) -> crate::Result { 247 | self.inner.data_bits() 248 | } 249 | 250 | /// Returns the flow control mode. 251 | /// 252 | /// This function returns `None` if the flow control mode could not be determined. This may 253 | /// occur if the hardware is in an uninitialized state or is using an unsupported flow control 254 | /// mode. Setting a flow control mode with `set_flow_control()` should initialize the flow 255 | /// control mode to a supported value. 256 | #[inline(always)] 257 | fn flow_control(&self) -> crate::Result { 258 | self.inner.flow_control() 259 | } 260 | 261 | /// Returns the parity-checking mode. 262 | /// 263 | /// This function returns `None` if the parity mode could not be determined. This may occur if 264 | /// the hardware is in an uninitialized state or is using a non-standard parity mode. Setting 265 | /// a parity mode with `set_parity()` should initialize the parity mode to a supported value. 266 | #[inline(always)] 267 | fn parity(&self) -> crate::Result { 268 | self.inner.parity() 269 | } 270 | 271 | /// Returns the number of stop bits. 272 | /// 273 | /// This function returns `None` if the number of stop bits could not be determined. This may 274 | /// occur if the hardware is in an uninitialized state or is using an unsupported stop bit 275 | /// configuration. Setting the number of stop bits with `set_stop-bits()` should initialize the 276 | /// stop bits to a supported value. 277 | #[inline(always)] 278 | fn stop_bits(&self) -> crate::Result { 279 | self.inner.stop_bits() 280 | } 281 | 282 | /// Returns the current timeout. This parameter is const and equal to zero and implemented due 283 | /// to required for trait completeness. 284 | #[inline(always)] 285 | fn timeout(&self) -> Duration { 286 | Duration::from_secs(0) 287 | } 288 | 289 | /// Sets the baud rate. 290 | /// 291 | /// ## Errors 292 | /// 293 | /// If the implementation does not support the requested baud rate, this function may return an 294 | /// `InvalidInput` error. Even if the baud rate is accepted by `set_baud_rate()`, it may not be 295 | /// supported by the underlying hardware. 296 | #[inline(always)] 297 | fn set_baud_rate(&mut self, baud_rate: u32) -> crate::Result<()> { 298 | self.inner.set_baud_rate(baud_rate) 299 | } 300 | 301 | /// Sets the character size. 302 | #[inline(always)] 303 | fn set_data_bits(&mut self, data_bits: crate::DataBits) -> crate::Result<()> { 304 | self.inner.set_data_bits(data_bits) 305 | } 306 | 307 | // Port settings setters 308 | 309 | /// Sets the flow control mode. 310 | #[inline(always)] 311 | fn set_flow_control(&mut self, flow_control: crate::FlowControl) -> crate::Result<()> { 312 | self.inner.set_flow_control(flow_control) 313 | } 314 | 315 | /// Sets the parity-checking mode. 316 | #[inline(always)] 317 | fn set_parity(&mut self, parity: crate::Parity) -> crate::Result<()> { 318 | self.inner.set_parity(parity) 319 | } 320 | 321 | /// Sets the number of stop bits. 322 | #[inline(always)] 323 | fn set_stop_bits(&mut self, stop_bits: crate::StopBits) -> crate::Result<()> { 324 | self.inner.set_stop_bits(stop_bits) 325 | } 326 | 327 | /// Sets the timeout for future I/O operations. This parameter is ignored but 328 | /// required for trait completeness. 329 | #[inline(always)] 330 | fn set_timeout(&mut self, _: Duration) -> crate::Result<()> { 331 | Ok(()) 332 | } 333 | 334 | /// Sets the state of the RTS (Request To Send) control signal. 335 | /// 336 | /// Setting a value of `true` asserts the RTS control signal. `false` clears the signal. 337 | /// 338 | /// ## Errors 339 | /// 340 | /// This function returns an error if the RTS control signal could not be set to the desired 341 | /// state on the underlying hardware: 342 | /// 343 | /// * `NoDevice` if the device was disconnected. 344 | /// * `Io` for any other type of I/O error. 345 | #[inline(always)] 346 | fn write_request_to_send(&mut self, level: bool) -> crate::Result<()> { 347 | self.inner.write_request_to_send(level) 348 | } 349 | 350 | /// Writes to the Data Terminal Ready pin 351 | /// 352 | /// Setting a value of `true` asserts the DTR control signal. `false` clears the signal. 353 | /// 354 | /// ## Errors 355 | /// 356 | /// This function returns an error if the DTR control signal could not be set to the desired 357 | /// state on the underlying hardware: 358 | /// 359 | /// * `NoDevice` if the device was disconnected. 360 | /// * `Io` for any other type of I/O error. 361 | #[inline(always)] 362 | fn write_data_terminal_ready(&mut self, level: bool) -> crate::Result<()> { 363 | self.inner.write_data_terminal_ready(level) 364 | } 365 | 366 | // Functions for setting non-data control signal pins 367 | 368 | /// Reads the state of the CTS (Clear To Send) control signal. 369 | /// 370 | /// This function returns a boolean that indicates whether the CTS control signal is asserted. 371 | /// 372 | /// ## Errors 373 | /// 374 | /// This function returns an error if the state of the CTS control signal could not be read 375 | /// from the underlying hardware: 376 | /// 377 | /// * `NoDevice` if the device was disconnected. 378 | /// * `Io` for any other type of I/O error. 379 | #[inline(always)] 380 | fn read_clear_to_send(&mut self) -> crate::Result { 381 | self.inner.read_clear_to_send() 382 | } 383 | 384 | /// Reads the state of the Data Set Ready control signal. 385 | /// 386 | /// This function returns a boolean that indicates whether the DSR control signal is asserted. 387 | /// 388 | /// ## Errors 389 | /// 390 | /// This function returns an error if the state of the DSR control signal could not be read 391 | /// from the underlying hardware: 392 | /// 393 | /// * `NoDevice` if the device was disconnected. 394 | /// * `Io` for any other type of I/O error. 395 | #[inline(always)] 396 | fn read_data_set_ready(&mut self) -> crate::Result { 397 | self.inner.read_data_set_ready() 398 | } 399 | 400 | // Functions for reading additional pins 401 | 402 | /// Reads the state of the Ring Indicator control signal. 403 | /// 404 | /// This function returns a boolean that indicates whether the RI control signal is asserted. 405 | /// 406 | /// ## Errors 407 | /// 408 | /// This function returns an error if the state of the RI control signal could not be read from 409 | /// the underlying hardware: 410 | /// 411 | /// * `NoDevice` if the device was disconnected. 412 | /// * `Io` for any other type of I/O error. 413 | #[inline(always)] 414 | fn read_ring_indicator(&mut self) -> crate::Result { 415 | self.inner.read_ring_indicator() 416 | } 417 | 418 | /// Reads the state of the Carrier Detect control signal. 419 | /// 420 | /// This function returns a boolean that indicates whether the CD control signal is asserted. 421 | /// 422 | /// ## Errors 423 | /// 424 | /// This function returns an error if the state of the CD control signal could not be read from 425 | /// the underlying hardware: 426 | /// 427 | /// * `NoDevice` if the device was disconnected. 428 | /// * `Io` for any other type of I/O error. 429 | #[inline(always)] 430 | fn read_carrier_detect(&mut self) -> crate::Result { 431 | self.inner.read_carrier_detect() 432 | } 433 | 434 | /// Gets the number of bytes available to be read from the input buffer. 435 | /// 436 | /// # Errors 437 | /// 438 | /// This function may return the following errors: 439 | /// 440 | /// * `NoDevice` if the device was disconnected. 441 | /// * `Io` for any other type of I/O error. 442 | #[inline(always)] 443 | fn bytes_to_read(&self) -> crate::Result { 444 | self.inner.bytes_to_read() 445 | } 446 | 447 | /// Get the number of bytes written to the output buffer, awaiting transmission. 448 | /// 449 | /// # Errors 450 | /// 451 | /// This function may return the following errors: 452 | /// 453 | /// * `NoDevice` if the device was disconnected. 454 | /// * `Io` for any other type of I/O error. 455 | #[inline(always)] 456 | fn bytes_to_write(&self) -> crate::Result { 457 | self.inner.bytes_to_write() 458 | } 459 | 460 | /// Discards all bytes from the serial driver's input buffer and/or output buffer. 461 | /// 462 | /// # Errors 463 | /// 464 | /// This function may return the following errors: 465 | /// 466 | /// * `NoDevice` if the device was disconnected. 467 | /// * `Io` for any other type of I/O error. 468 | #[inline(always)] 469 | fn clear(&self, buffer_to_clear: crate::ClearBuffer) -> crate::Result<()> { 470 | self.inner.clear(buffer_to_clear) 471 | } 472 | 473 | /// Attempts to clone the `SerialPort`. This allow you to write and read simultaneously from the 474 | /// same serial connection. Please note that if you want a real asynchronous serial port you 475 | /// should look at [mio-serial](https://crates.io/crates/mio-serial) or 476 | /// [tokio-serial](https://crates.io/crates/tokio-serial). 477 | /// 478 | /// Also, you must be very carefull when changing the settings of a cloned `SerialPort` : since 479 | /// the settings are cached on a per object basis, trying to modify them from two different 480 | /// objects can cause some nasty behavior. 481 | /// 482 | /// # Errors 483 | /// 484 | /// This function returns an error if the serial port couldn't be cloned. 485 | #[inline(always)] 486 | #[cfg(any())] 487 | fn try_clone(&self) -> crate::Result> { 488 | Ok(Box::new(self.try_clone_native()?)) 489 | } 490 | 491 | /// Cloning is not supported for [`SerialStream`] objects 492 | /// 493 | /// This logic has never really completely worked. Cloned file descriptors in asynchronous 494 | /// code is a semantic minefield. Are you cloning the file descriptor? Are you cloning the 495 | /// event flags on the file descriptor? Both? It's a bit of a mess even within one OS, 496 | /// let alone across multiple OS's 497 | /// 498 | /// Maybe it can be done with more work, but until a clear use-case is required (or mio/tokio 499 | /// gets an equivalent of the unix `AsyncFd` for async file handles, see 500 | /// and 501 | /// ) I would rather not 502 | /// have any code available over a kind-of-works-maybe impl. So I'll leave this code here 503 | /// for now but hard-code it disabled. 504 | fn try_clone(&self) -> crate::Result> { 505 | Err(crate::Error::new( 506 | crate::ErrorKind::Io(StdIoErrorKind::Other), 507 | "cloning SerialStream is not supported", 508 | )) 509 | } 510 | 511 | /// Start transmitting a break 512 | #[inline(always)] 513 | fn set_break(&self) -> crate::Result<()> { 514 | self.inner.set_break() 515 | } 516 | 517 | /// Stop transmitting a break 518 | #[inline(always)] 519 | fn clear_break(&self) -> crate::Result<()> { 520 | self.inner.clear_break() 521 | } 522 | } 523 | 524 | impl TryFrom for SerialStream { 525 | type Error = crate::Error; 526 | #[cfg(unix)] 527 | fn try_from(port: NativeBlockingSerialPort) -> std::result::Result { 528 | // Set the O_NONBLOCK flag. 529 | log::debug!( 530 | "setting O_NONBLOCK for {}", 531 | port.name().unwrap_or_else(|| String::from("")) 532 | ); 533 | let flags = unsafe { libc::fcntl(port.as_raw_fd(), libc::F_GETFL) }; 534 | if flags < 0 { 535 | return Err(StdIoError::last_os_error().into()); 536 | } 537 | 538 | match unsafe { libc::fcntl(port.as_raw_fd(), libc::F_SETFL, flags | libc::O_NONBLOCK) } { 539 | 0 => Ok(SerialStream { inner: port }), 540 | _ => Err(StdIoError::last_os_error().into()), 541 | } 542 | } 543 | #[cfg(windows)] 544 | fn try_from(port: NativeBlockingSerialPort) -> std::result::Result { 545 | log::debug!( 546 | "switching {} to asynchronous mode", 547 | port.name().unwrap_or_else(|| String::from("")) 548 | ); 549 | log::debug!("reading serial port settings"); 550 | let name = port 551 | .name() 552 | .ok_or_else(|| crate::Error::new(crate::ErrorKind::NoDevice, "Empty device name"))?; 553 | let baud = port.baud_rate()?; 554 | let parity = port.parity()?; 555 | let data_bits = port.data_bits()?; 556 | let stop_bits = port.stop_bits()?; 557 | let flow_control = port.flow_control()?; 558 | 559 | let mut path = Vec::::new(); 560 | path.extend(OsStr::new("\\\\.\\").encode_wide()); 561 | path.extend(Path::new(&name).as_os_str().encode_wide()); 562 | path.push(0); 563 | 564 | // Drop the port object, we'll reopen the file path as a raw handle 565 | log::debug!("closing synchronous port to re-open in FILE_FLAG_OVERLAPPED mode"); 566 | mem::drop(port); 567 | 568 | let handle = unsafe { 569 | CreateFileW( 570 | path.as_ptr(), 571 | GENERIC_READ | GENERIC_WRITE, 572 | 0, 573 | ptr::null_mut(), 574 | OPEN_EXISTING, 575 | FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, 576 | 0 as HANDLE, 577 | ) 578 | }; 579 | 580 | if handle == INVALID_HANDLE_VALUE { 581 | log::error!("unable to open new async file handle"); 582 | return Err(crate::Error::from(StdIoError::last_os_error())); 583 | } 584 | 585 | // Construct NamedPipe and COMPort from Handle 586 | // 587 | // We need both the NamedPipe for Read/Write and COMPort for serialport related 588 | // actions. Both are created using FromRawHandle which takes ownership of the 589 | // handle which may case a double-free as both objects attempt to close the handle. 590 | // 591 | // Looking through the source for both NamedPipe and COMPort, NamedPipe does some 592 | // cleanup in Drop while COMPort just closes the handle. 593 | // 594 | // We'll use a ManuallyDrop for COMPort and defer cleanup to the NamedPipe 595 | let pipe = unsafe { NamedPipe::from_raw_handle(handle) }; 596 | let mut com_port = 597 | mem::ManuallyDrop::new(unsafe { serialport::COMPort::from_raw_handle(handle) }); 598 | 599 | log::debug!("re-setting serial port parameters to original values from synchronous port"); 600 | com_port.set_baud_rate(baud)?; 601 | com_port.set_parity(parity)?; 602 | com_port.set_data_bits(data_bits)?; 603 | com_port.set_stop_bits(stop_bits)?; 604 | com_port.set_flow_control(flow_control)?; 605 | sys::override_comm_timeouts(handle)?; 606 | 607 | Ok(Self { 608 | inner: com_port, 609 | pipe, 610 | }) 611 | } 612 | } 613 | 614 | #[cfg(unix)] 615 | mod io { 616 | use super::{SerialStream, StdIoError, StdIoResult}; 617 | use nix::libc; 618 | use nix::sys::termios; 619 | use std::io::ErrorKind as StdIoErrorKind; 620 | use std::io::{Read, Write}; 621 | use std::os::unix::prelude::*; 622 | 623 | macro_rules! uninterruptibly { 624 | ($e:expr) => {{ 625 | loop { 626 | match $e { 627 | Err(ref error) if error.kind() == StdIoErrorKind::Interrupted => {} 628 | res => break res, 629 | } 630 | } 631 | }}; 632 | } 633 | 634 | impl Read for SerialStream { 635 | fn read(&mut self, bytes: &mut [u8]) -> StdIoResult { 636 | uninterruptibly!(match unsafe { 637 | libc::read( 638 | self.as_raw_fd(), 639 | bytes.as_ptr() as *mut libc::c_void, 640 | bytes.len() as libc::size_t, 641 | ) 642 | } { 643 | x if x >= 0 => Ok(x as usize), 644 | _ => Err(StdIoError::last_os_error()), 645 | }) 646 | } 647 | } 648 | 649 | impl Write for SerialStream { 650 | fn write(&mut self, bytes: &[u8]) -> StdIoResult { 651 | uninterruptibly!(match unsafe { 652 | libc::write( 653 | self.as_raw_fd(), 654 | bytes.as_ptr().cast::(), 655 | bytes.len() as libc::size_t, 656 | ) 657 | } { 658 | x if x >= 0 => Ok(x as usize), 659 | _ => Err(StdIoError::last_os_error()), 660 | }) 661 | } 662 | 663 | fn flush(&mut self) -> StdIoResult<()> { 664 | uninterruptibly!(termios::tcdrain(unsafe { 665 | BorrowedFd::borrow_raw(self.inner.as_raw_fd()) 666 | }) 667 | .map_err(StdIoError::from)) 668 | } 669 | } 670 | 671 | impl<'a> Read for &'a SerialStream { 672 | fn read(&mut self, bytes: &mut [u8]) -> StdIoResult { 673 | uninterruptibly!(match unsafe { 674 | libc::read( 675 | self.as_raw_fd(), 676 | bytes.as_ptr() as *mut libc::c_void, 677 | bytes.len() as libc::size_t, 678 | ) 679 | } { 680 | x if x >= 0 => Ok(x as usize), 681 | _ => Err(StdIoError::last_os_error()), 682 | }) 683 | } 684 | } 685 | 686 | impl<'a> Write for &'a SerialStream { 687 | fn write(&mut self, bytes: &[u8]) -> StdIoResult { 688 | uninterruptibly!(match unsafe { 689 | libc::write( 690 | self.as_raw_fd(), 691 | bytes.as_ptr() as *const libc::c_void, 692 | bytes.len() as libc::size_t, 693 | ) 694 | } { 695 | x if x >= 0 => Ok(x as usize), 696 | _ => Err(StdIoError::last_os_error()), 697 | }) 698 | } 699 | 700 | fn flush(&mut self) -> StdIoResult<()> { 701 | uninterruptibly!(termios::tcdrain(unsafe { 702 | BorrowedFd::borrow_raw(self.inner.as_raw_fd()) 703 | }) 704 | .map_err(StdIoError::from)) 705 | } 706 | } 707 | } 708 | 709 | #[cfg(windows)] 710 | mod io { 711 | use super::{NativeBlockingSerialPort, SerialStream, StdIoResult}; 712 | use crate::sys; 713 | use mio::windows::NamedPipe; 714 | use std::io::{Read, Write}; 715 | use std::mem; 716 | use std::os::windows::prelude::*; 717 | 718 | impl Read for SerialStream { 719 | fn read(&mut self, bytes: &mut [u8]) -> StdIoResult { 720 | self.pipe.read(bytes) 721 | } 722 | } 723 | 724 | impl Write for SerialStream { 725 | fn write(&mut self, bytes: &[u8]) -> StdIoResult { 726 | self.pipe.write(bytes) 727 | } 728 | 729 | fn flush(&mut self) -> StdIoResult<()> { 730 | self.pipe.flush() 731 | } 732 | } 733 | 734 | impl AsRawHandle for SerialStream { 735 | fn as_raw_handle(&self) -> RawHandle { 736 | self.pipe.as_raw_handle() 737 | } 738 | } 739 | 740 | impl IntoRawHandle for SerialStream { 741 | fn into_raw_handle(self) -> RawHandle { 742 | // Since NamedPipe doesn't impl IntoRawHandle we'll use AsRawHandle and bypass 743 | // NamedPipe's destructor to keep the handle in the current state 744 | let manual = mem::ManuallyDrop::new(self.pipe); 745 | manual.as_raw_handle() 746 | } 747 | } 748 | 749 | impl FromRawHandle for SerialStream { 750 | /// This method can potentially fail to override the communication timeout 751 | /// value set in `sys::override_comm_timeouts` without any indication to the user. 752 | unsafe fn from_raw_handle(handle: RawHandle) -> Self { 753 | let inner = mem::ManuallyDrop::new(NativeBlockingSerialPort::from_raw_handle(handle)); 754 | let pipe = NamedPipe::from_raw_handle(handle); 755 | sys::override_comm_timeouts(handle).ok(); 756 | 757 | Self { inner, pipe } 758 | } 759 | } 760 | } 761 | 762 | #[cfg(unix)] 763 | mod sys { 764 | use super::{NativeBlockingSerialPort, SerialStream}; 765 | use std::os::unix::prelude::*; 766 | 767 | impl AsRawFd for SerialStream { 768 | fn as_raw_fd(&self) -> RawFd { 769 | self.inner.as_raw_fd() 770 | } 771 | } 772 | 773 | impl IntoRawFd for SerialStream { 774 | fn into_raw_fd(self) -> RawFd { 775 | self.inner.into_raw_fd() 776 | } 777 | } 778 | 779 | impl FromRawFd for SerialStream { 780 | unsafe fn from_raw_fd(fd: RawFd) -> Self { 781 | let port = NativeBlockingSerialPort::from_raw_fd(fd); 782 | Self { inner: port } 783 | } 784 | } 785 | } 786 | 787 | #[cfg(windows)] 788 | mod sys { 789 | 790 | use super::os_prelude::*; 791 | use super::StdIoResult; 792 | /// Overrides timeout value set by serialport-rs so that the read end will 793 | /// never wake up with 0-byte payload. 794 | pub(crate) fn override_comm_timeouts(handle: RawHandle) -> StdIoResult<()> { 795 | let mut timeouts = COMMTIMEOUTS { 796 | // wait at most 1ms between two bytes (0 means no timeout) 797 | ReadIntervalTimeout: 1, 798 | // disable "total" timeout to wait at least 1 byte forever 799 | ReadTotalTimeoutMultiplier: 0, 800 | ReadTotalTimeoutConstant: 0, 801 | // write timeouts are just copied from serialport-rs 802 | WriteTotalTimeoutMultiplier: 0, 803 | WriteTotalTimeoutConstant: 0, 804 | }; 805 | 806 | let r = unsafe { SetCommTimeouts(handle, &mut timeouts) }; 807 | if r == 0 { 808 | return Err(io::Error::last_os_error()); 809 | } 810 | Ok(()) 811 | } 812 | } 813 | 814 | #[cfg(unix)] 815 | impl Source for SerialStream { 816 | #[inline(always)] 817 | fn register( 818 | &mut self, 819 | registry: &Registry, 820 | token: Token, 821 | interests: Interest, 822 | ) -> StdIoResult<()> { 823 | SourceFd(&self.as_raw_fd()).register(registry, token, interests) 824 | } 825 | 826 | #[inline(always)] 827 | fn reregister( 828 | &mut self, 829 | registry: &Registry, 830 | token: Token, 831 | interests: Interest, 832 | ) -> StdIoResult<()> { 833 | SourceFd(&self.as_raw_fd()).reregister(registry, token, interests) 834 | } 835 | 836 | #[inline(always)] 837 | fn deregister(&mut self, registry: &Registry) -> StdIoResult<()> { 838 | SourceFd(&self.as_raw_fd()).deregister(registry) 839 | } 840 | } 841 | 842 | #[cfg(windows)] 843 | impl Source for SerialStream { 844 | fn register( 845 | &mut self, 846 | registry: &Registry, 847 | token: Token, 848 | interest: Interest, 849 | ) -> StdIoResult<()> { 850 | self.pipe.register(registry, token, interest) 851 | } 852 | 853 | fn reregister( 854 | &mut self, 855 | registry: &Registry, 856 | token: Token, 857 | interest: Interest, 858 | ) -> StdIoResult<()> { 859 | self.pipe.reregister(registry, token, interest) 860 | } 861 | 862 | fn deregister(&mut self, registry: &Registry) -> StdIoResult<()> { 863 | self.pipe.deregister(registry) 864 | } 865 | } 866 | 867 | /// An extension trait for [`SerialPortBuilder`] 868 | /// 869 | /// This trait adds an additional method to [`SerialPortBuilder`]: 870 | /// 871 | /// - open_native_async 872 | /// 873 | /// These methods mirror the [`SerialPortBuilder::open_native`] methods 874 | pub trait SerialPortBuilderExt { 875 | /// Open a platform-specific interface to the port with the specified settings 876 | fn open_native_async(self) -> Result; 877 | } 878 | 879 | impl SerialPortBuilderExt for SerialPortBuilder { 880 | /// Open a platform-specific interface to the port with the specified settings 881 | fn open_native_async(self) -> Result { 882 | SerialStream::open(&self) 883 | } 884 | } 885 | -------------------------------------------------------------------------------- /tests/common.rs: -------------------------------------------------------------------------------- 1 | //! Common test code. Adapted from `mio/tests/util/mod.rs` 2 | #![allow(dead_code)] 3 | use mio::{event::Event, Events, Interest, Poll, Token}; 4 | use std::io::{Read, Write}; 5 | use std::ops::BitOr; 6 | use std::panic; 7 | use std::sync::Once; 8 | use std::time::Duration; 9 | 10 | use serialport::SerialPort; 11 | 12 | #[cfg(unix)] 13 | use std::process; 14 | #[cfg(unix)] 15 | use std::thread; 16 | 17 | static LOGGING_INIT: Once = Once::new(); 18 | 19 | /// Default serial port names used for testing 20 | #[cfg(unix)] 21 | const DEFAULT_TEST_PORT_NAMES: &str = "USB0;USB1"; 22 | 23 | /// Default serial port names used for testing 24 | #[cfg(windows)] 25 | const DEFAULT_TEST_PORT_NAMES: &str = "COM1;COM2"; 26 | 27 | #[derive(Debug)] 28 | pub struct Readiness(usize); 29 | 30 | const READABLE: usize = 0b0000_0001; 31 | const WRITABLE: usize = 0b0000_0010; 32 | const AIO: usize = 0b0000_0100; 33 | const LIO: usize = 0b0000_1000; 34 | const ERROR: usize = 0b0001_0000; 35 | const READ_CLOSED: usize = 0b0010_0000; 36 | const WRITE_CLOSED: usize = 0b0100_0000; 37 | const PRIORITY: usize = 0b1000_0000; 38 | 39 | impl Readiness { 40 | pub const READABLE: Readiness = Readiness(READABLE); 41 | pub const WRITABLE: Readiness = Readiness(WRITABLE); 42 | pub const AIO: Readiness = Readiness(AIO); 43 | pub const LIO: Readiness = Readiness(LIO); 44 | pub const ERROR: Readiness = Readiness(ERROR); 45 | pub const READ_CLOSED: Readiness = Readiness(READ_CLOSED); 46 | pub const WRITE_CLOSED: Readiness = Readiness(WRITE_CLOSED); 47 | pub const PRIORITY: Readiness = Readiness(PRIORITY); 48 | 49 | fn matches(&self, event: &Event) -> bool { 50 | // If we expect a readiness then also match on the event. 51 | // In maths terms that is p -> q, which is the same as !p || q. 52 | (!self.is(READABLE) || event.is_readable()) 53 | && (!self.is(WRITABLE) || event.is_writable()) 54 | && (!self.is(AIO) || event.is_aio()) 55 | && (!self.is(LIO) || event.is_lio()) 56 | && (!self.is(ERROR) || event.is_error()) 57 | && (!self.is(READ_CLOSED) || event.is_read_closed()) 58 | && (!self.is(WRITE_CLOSED) || event.is_write_closed()) 59 | && (!self.is(PRIORITY) || event.is_priority()) 60 | } 61 | 62 | /// Usage: `self.is(READABLE)`. 63 | fn is(&self, value: usize) -> bool { 64 | self.0 & value != 0 65 | } 66 | } 67 | 68 | impl BitOr for Readiness { 69 | type Output = Self; 70 | 71 | fn bitor(self, other: Self) -> Self { 72 | Readiness(self.0 | other.0) 73 | } 74 | } 75 | 76 | impl From for Readiness { 77 | fn from(interests: Interest) -> Readiness { 78 | let mut readiness = Readiness(0); 79 | if interests.is_readable() { 80 | readiness.0 |= READABLE; 81 | } 82 | if interests.is_writable() { 83 | readiness.0 |= WRITABLE; 84 | } 85 | if interests.is_aio() { 86 | readiness.0 |= AIO; 87 | } 88 | if interests.is_lio() { 89 | readiness.0 |= LIO; 90 | } 91 | readiness 92 | } 93 | } 94 | 95 | #[must_use] 96 | pub fn init_with_poll() -> (Poll, Events) { 97 | let poll = Poll::new().expect("unable to create poll object"); 98 | let events = Events::with_capacity(16); 99 | (poll, events) 100 | } 101 | 102 | /// An event that is expected to show up when `Poll` is polled, see 103 | /// `expect_events`. 104 | #[derive(Debug)] 105 | pub struct ExpectEvent { 106 | token: Token, 107 | readiness: Readiness, 108 | } 109 | 110 | impl ExpectEvent { 111 | pub fn new(token: Token, readiness: R) -> ExpectEvent 112 | where 113 | R: Into, 114 | { 115 | ExpectEvent { 116 | token, 117 | readiness: readiness.into(), 118 | } 119 | } 120 | 121 | fn matches(&self, event: &Event) -> bool { 122 | event.token() == self.token && self.readiness.matches(event) 123 | } 124 | } 125 | 126 | pub fn expect_events(poll: &mut Poll, events: &mut Events, mut expected: Vec) { 127 | // In a lot of calls we expect more then one event, but it could be that 128 | // poll returns the first event only in a single call. To be a bit more 129 | // lenient we'll poll a couple of times. 130 | for _ in 0..3 { 131 | poll.poll(events, Some(Duration::from_millis(500))) 132 | .expect("unable to poll"); 133 | 134 | for event in events.iter() { 135 | let index = expected.iter().position(|expected| expected.matches(event)); 136 | 137 | if let Some(index) = index { 138 | expected.swap_remove(index); 139 | } else { 140 | // Must accept sporadic events. 141 | // warn!("got unexpected event: {:?}", event); 142 | } 143 | } 144 | 145 | if expected.is_empty() { 146 | return; 147 | } 148 | } 149 | 150 | assert!( 151 | expected.is_empty(), 152 | "the following expected events were not found: {expected:?}", 153 | ); 154 | } 155 | 156 | pub fn assert_would_block(result: std::io::Result) { 157 | match result { 158 | Ok(_) => panic!("unexpected OK result, expected a `WouldBlock` error"), 159 | Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {} 160 | Err(e) => panic!("unexpected error result: {e}"), 161 | } 162 | } 163 | 164 | /// Ensure the entire buffer is written 165 | pub fn checked_write(port: &mut mio_serial::SerialStream, data: &[u8]) { 166 | let n = port.write(data).expect("unable to write to serial port"); 167 | assert_eq!(n, data.len(), "short write"); 168 | } 169 | 170 | /// Ensure the entire buffer is read 171 | pub fn checked_read(port: &mut mio_serial::SerialStream, data: &mut [u8], expected: &[u8]) { 172 | let n = port.read(data).expect("unable to read from serial port"); 173 | assert_eq!(n, expected.len(), "short read"); 174 | assert_eq!(&data[..n], expected); 175 | } 176 | 177 | pub struct Fixture { 178 | #[cfg(unix)] 179 | process: process::Child, 180 | pub port_a: &'static str, 181 | pub port_b: &'static str, 182 | } 183 | 184 | #[cfg(unix)] 185 | impl Drop for Fixture { 186 | fn drop(&mut self) { 187 | log::trace!("stopping socat process (id: {})...", self.process.id()); 188 | self.process.kill().ok(); 189 | thread::sleep(Duration::from_millis(1000)); 190 | log::trace!("removing link: {:?}", self.port_a); 191 | std::fs::remove_file(&self.port_a).ok(); 192 | log::trace!("removing link: {:?}", self.port_b); 193 | std::fs::remove_file(&self.port_b).ok(); 194 | thread::sleep(Duration::from_millis(1000)); 195 | } 196 | } 197 | 198 | impl Fixture { 199 | #[cfg(unix)] 200 | pub fn new(port_a: &'static str, port_b: &'static str) -> Self { 201 | use std::sync::atomic::{AtomicUsize, Ordering}; 202 | static N: AtomicUsize = AtomicUsize::new(0); 203 | LOGGING_INIT.call_once(|| env_logger::init()); 204 | let n = N.fetch_add(1, Ordering::Relaxed); 205 | let port_a = format!("{}{}", port_a, n).leak(); 206 | let port_b = format!("{}{}", port_b, n).leak(); 207 | let args = [ 208 | format!("PTY,link={}", port_a), 209 | format!("PTY,link={}", port_b), 210 | ]; 211 | log::trace!("starting process: socat {} {}", args[0], args[1]); 212 | 213 | let process = process::Command::new("socat") 214 | .args(&args) 215 | .spawn() 216 | .expect("unable to spawn socat process"); 217 | log::trace!(".... done! (pid: {:?})", process.id()); 218 | thread::sleep(Duration::from_millis(1000)); 219 | Self { 220 | process, 221 | port_a, 222 | port_b, 223 | } 224 | } 225 | 226 | #[cfg(not(unix))] 227 | pub fn new(port_a: &'static str, port_b: &'static str) -> Self { 228 | LOGGING_INIT.call_once(|| env_logger::init()); 229 | Self { port_a, port_b } 230 | } 231 | } 232 | 233 | pub fn setup_virtual_serial_ports() -> Fixture { 234 | let port_names: Vec<&str> = std::option_env!("TEST_PORT_NAMES") 235 | .unwrap_or(DEFAULT_TEST_PORT_NAMES) 236 | .split(';') 237 | .collect(); 238 | 239 | assert_eq!(port_names.len(), 2); 240 | Fixture::new(port_names[0], port_names[1]) 241 | } 242 | 243 | /// Assert serial port baud rate matches expected value. 244 | pub fn assert_baud_rate

(port: &P, expected: u32) 245 | where 246 | P: SerialPort, 247 | { 248 | let actual = port.baud_rate().expect("unable to get baud rate"); 249 | 250 | assert_eq!(actual, expected, "baud rate not equal"); 251 | } 252 | -------------------------------------------------------------------------------- /tests/test_serialstream.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use mio::{Interest, Token}; 3 | use mio_serial::SerialPortBuilderExt; 4 | use std::io::{Read, Write}; 5 | 6 | const TOKEN1: Token = Token(0); 7 | const TOKEN2: Token = Token(1); 8 | 9 | #[test] 10 | fn test_builder_open_async() { 11 | let fixture = common::setup_virtual_serial_ports(); 12 | let baud_rate = 9600; 13 | let builder = mio_serial::new(fixture.port_a, baud_rate); 14 | 15 | let stream = builder 16 | .open_native_async() 17 | .expect("unable to open serial port"); 18 | 19 | common::assert_baud_rate(&stream, baud_rate); 20 | } 21 | 22 | #[test] 23 | fn test_native_from_blocking() { 24 | use std::convert::TryFrom; 25 | let baud_rate = 9600; 26 | 27 | let fixture = common::setup_virtual_serial_ports(); 28 | let port = fixture.port_a; 29 | let native_blocking = mio_serial::new(port, baud_rate) 30 | .open_native() 31 | .unwrap_or_else(|e| panic!("unable to open serial port {port}: {e}")); 32 | 33 | let stream = mio_serial::SerialStream::try_from(native_blocking) 34 | .expect("unable to convert from blocking serial port object"); 35 | 36 | common::assert_baud_rate(&stream, baud_rate); 37 | } 38 | 39 | #[test] 40 | fn test_stream_open() { 41 | let baud_rate = 9600; 42 | let fixture = common::setup_virtual_serial_ports(); 43 | let port = fixture.port_a; 44 | let builder = mio_serial::new(port, baud_rate); 45 | let stream = mio_serial::SerialStream::open(&builder).expect("unable to open serial port"); 46 | 47 | common::assert_baud_rate(&stream, baud_rate); 48 | } 49 | 50 | /// Port enumeration doesn't seem to work on virtual serial ports created by com0com during 51 | /// the CI process 52 | #[test] 53 | #[ignore = "Port enumeration test does not seem to work with com0com virtual ports"] 54 | fn test_port_enumeration() { 55 | let fixture = common::setup_virtual_serial_ports(); 56 | let ports = mio_serial::available_ports().expect("unable to enumerate serial ports"); 57 | for name in [fixture.port_a, fixture.port_b] { 58 | ports 59 | .iter() 60 | .find(|&info| info.port_name == *name) 61 | .unwrap_or_else(|| { 62 | panic!("unable to find serial port named {name} in enumerated ports") 63 | }); 64 | } 65 | } 66 | 67 | #[test] 68 | fn test_read_write_pair() { 69 | const DATA1: &[u8] = b"Here is an example string"; 70 | const DATA2: &[u8] = b"And here is a reply to the example string"; 71 | const DEFAULT_BUF_SIZE: usize = 64; 72 | 73 | let baud_rate = 38400; 74 | 75 | let fixture = common::setup_virtual_serial_ports(); 76 | let (port_a, port_b) = (fixture.port_a, fixture.port_b); 77 | let (mut poll, mut events) = common::init_with_poll(); 78 | 79 | let mut port_1 = mio_serial::new(port_a, baud_rate) 80 | .open_native_async() 81 | .unwrap_or_else(|e| panic!("unable to open serial port {port_a}: {e}")); 82 | let mut port_2 = mio_serial::new(port_b, baud_rate) 83 | .open_native_async() 84 | .unwrap_or_else(|e| panic!("unable to open serial port {port_b}: {e}")); 85 | 86 | // register both serial ports for read and write events 87 | poll.registry() 88 | .register(&mut port_1, TOKEN1, Interest::WRITABLE | Interest::READABLE) 89 | .unwrap_or_else(|e| { 90 | panic!("unable to register port {port_a} as readable and writable: {e}") 91 | }); 92 | poll.registry() 93 | .register(&mut port_2, TOKEN2, Interest::READABLE | Interest::WRITABLE) 94 | .unwrap_or_else(|e| { 95 | panic!("unable to register port {port_b} as readable and writable: {e}") 96 | }); 97 | 98 | let mut buf = [0u8; DEFAULT_BUF_SIZE]; 99 | 100 | // port1 should immediately be writable 101 | common::expect_events( 102 | &mut poll, 103 | &mut events, 104 | vec![common::ExpectEvent::new(TOKEN1, Interest::WRITABLE)], 105 | ); 106 | 107 | // port2 should be blocking 108 | common::assert_would_block(port_2.read(&mut buf)); 109 | 110 | // write data on port 1 111 | common::checked_write(&mut port_1, DATA1); 112 | port_1 113 | .flush() 114 | .unwrap_or_else(|e| panic!("unable to flush serial port {port_a}: {e}")); 115 | 116 | // port 2 should now be readable 117 | common::expect_events( 118 | &mut poll, 119 | &mut events, 120 | vec![common::ExpectEvent::new(TOKEN2, Interest::READABLE)], 121 | ); 122 | 123 | // read data on port 2 124 | common::checked_read(&mut port_2, &mut buf, DATA1); 125 | 126 | // port 2 should then return to blocking 127 | common::assert_would_block(port_2.read(&mut buf)); 128 | 129 | // port 1 should be blocking on read for the reply 130 | common::assert_would_block(port_1.read(&mut buf)); 131 | 132 | // send data back on port 2 133 | common::checked_write(&mut port_2, DATA2); 134 | port_2 135 | .flush() 136 | .unwrap_or_else(|e| panic!("unable to flush serial port {port_b}: {e}")); 137 | 138 | // port 1 should now be readable 139 | common::expect_events( 140 | &mut poll, 141 | &mut events, 142 | vec![common::ExpectEvent::new(TOKEN1, Interest::READABLE)], 143 | ); 144 | // and be able to read the full data 145 | common::checked_read(&mut port_1, &mut buf, DATA2); 146 | // .. before blocking again. 147 | common::assert_would_block(port_1.read(&mut buf)); 148 | } 149 | 150 | // Same as test_send_recv but use a cloned receiver 151 | #[cfg(any())] 152 | #[test] 153 | fn test_try_clone_native() { 154 | const DATA1: &[u8] = b"Here is an example string"; 155 | const DEFAULT_BUF_SIZE: usize = 64; 156 | 157 | let baud_rate = 9600; 158 | let fixture = common::setup_virtual_serial_ports(); 159 | let (mut poll, mut events) = common::init_with_poll(); 160 | 161 | let builder_a = mio_serial::new(fixture.port_a, baud_rate); 162 | let builder_b = mio_serial::new(fixture.port_b, baud_rate); 163 | 164 | let mut sender = 165 | mio_serial::SerialStream::open(&builder_a).expect("unable to open serial port"); 166 | let mut receiver = 167 | mio_serial::SerialStream::open(&builder_b).expect("unable to open serial port"); 168 | 169 | // register the two ports 170 | poll.registry() 171 | .register(&mut sender, TOKEN1, Interest::WRITABLE | Interest::READABLE) 172 | .expect("unable to register port as readable and writable"); 173 | poll.registry() 174 | .register( 175 | &mut receiver, 176 | TOKEN2, 177 | Interest::READABLE | Interest::WRITABLE, 178 | ) 179 | .expect("unable to register port as readable and writable"); 180 | 181 | // then clone one of them 182 | let mut cloned_receiver = receiver 183 | .try_clone_native() 184 | .expect("unable to clone serial port"); 185 | 186 | // and drop the original 187 | std::mem::drop(receiver); 188 | 189 | let mut buf = [0u8; DEFAULT_BUF_SIZE]; 190 | 191 | common::expect_events( 192 | &mut poll, 193 | &mut events, 194 | vec![common::ExpectEvent::new(TOKEN1, Interest::WRITABLE)], 195 | ); 196 | 197 | common::assert_would_block(cloned_receiver.read(&mut buf).into()); 198 | 199 | // write data on port 1 200 | common::checked_write(&mut sender, DATA1); 201 | sender.flush().expect("unable to flush serial port"); 202 | 203 | // port 2 should now be readable 204 | common::expect_events( 205 | &mut poll, 206 | &mut events, 207 | vec![common::ExpectEvent::new(TOKEN2, Interest::READABLE)], 208 | ); 209 | 210 | // read data on port 2 211 | common::checked_read(&mut cloned_receiver, &mut buf, DATA1); 212 | 213 | // port 2 should then return to blocking 214 | common::assert_would_block(cloned_receiver.read(&mut buf)); 215 | 216 | // port 1 should be blocking on read for the reply 217 | common::assert_would_block(sender.read(&mut buf)); 218 | 219 | // write data on port 1 220 | common::checked_write(&mut sender, DATA1); 221 | sender.flush().expect("unable to flush serial port"); 222 | 223 | // port 2 should now be readable 224 | common::expect_events( 225 | &mut poll, 226 | &mut events, 227 | vec![common::ExpectEvent::new(TOKEN2, Interest::READABLE)], 228 | ); 229 | 230 | // read data on port 2 231 | common::checked_read(&mut cloned_receiver, &mut buf, DATA1); 232 | 233 | // port 2 should then return to blocking 234 | common::assert_would_block(cloned_receiver.read(&mut buf)); 235 | 236 | // port 1 should be blocking on read for the reply 237 | common::assert_would_block(sender.read(&mut buf)); 238 | } 239 | --------------------------------------------------------------------------------