├── .cargo └── config.toml ├── .github └── workflows │ ├── build.yaml │ └── ci.yaml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE.txt ├── NOTES.md ├── README.md ├── TESTING.md ├── deny.toml ├── doc ├── dev_notes.md ├── index.md └── platforms.md ├── examples ├── clear_input_buffer.rs ├── clear_output_buffer.rs ├── duplex.rs ├── hardware_check.rs ├── list_ports.rs ├── loopback.rs ├── pseudo_terminal.rs ├── receive_data.rs └── transmit.rs ├── src ├── lib.rs ├── posix │ ├── enumerate.rs │ ├── error.rs │ ├── ioctl.rs │ ├── mod.rs │ ├── poll.rs │ ├── termios.rs │ └── tty.rs ├── tests │ ├── mod.rs │ └── timeout.rs └── windows │ ├── com.rs │ ├── dcb.rs │ ├── enumerate.rs │ ├── error.rs │ └── mod.rs └── tests ├── config.rs ├── test_baudrate.rs ├── test_serialport.rs ├── test_timeout.rs ├── test_try_clone.rs └── test_tty.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | # Some of our tests make use of actual serial ports. Run all tests sequentially 3 | # by default to avoid race conditions (see 4 | # https://github.com/rust-lang/cargo/issues/8430). 5 | RUST_TEST_THREADS = "1" 6 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | disable_extra_builds: 7 | type: boolean 8 | disable_tests: 9 | type: boolean 10 | extra_packages: 11 | type: string 12 | runs_on: 13 | default: ubuntu-latest 14 | type: string 15 | target: 16 | required: true 17 | type: string 18 | toolchain: 19 | default: stable 20 | type: string 21 | continue-on-error: 22 | default: false 23 | type: boolean 24 | 25 | env: 26 | # While we could define these on a per-job basis, there's no harm in simply 27 | # defining all environment variables for each job. This has the added benefit 28 | # of keeping them all together in one place. 29 | CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc 30 | CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-gnu-gcc 31 | CARGO_TARGET_ARMV5TE_UNKNOWN_LINUX_GNUEABI_LINKER: arm-linux-gnueabi-gcc 32 | CARGO_TARGET_ARMV5TE_UNKNOWN_LINUX_MUSLEABI_LINKER: arm-linux-gnueabi-gcc 33 | CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc 34 | CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER: arm-linux-gnueabihf-gcc 35 | CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc 36 | CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABI_LINKER: arm-linux-gnueabi-gcc 37 | CARGO_TARGET_ARM_UNKNOWN_LINUX_MUSLEABI_LINKER: arm-linux-gnueabi-gcc 38 | CARGO_TARGET_MIPS64EL_UNKNOWN_LINUX_GNUABI64_LINKER: mips64el-linux-gnuabi64-gcc 39 | CARGO_TARGET_MIPS64_UNKNOWN_LINUX_GNUABI64_LINKER: mips64-linux-gnuabi64-gcc 40 | CARGO_TARGET_MIPSEL_UNKNOWN_LINUX_GNU_LINKER: mipsel-linux-gnu-gcc 41 | CARGO_TARGET_MIPSEL_UNKNOWN_LINUX_MUSL_LINKER: mipsel-linux-gnu-gcc 42 | CARGO_TARGET_MIPS_UNKNOWN_LINUX_GNU_LINKER: mips-linux-gnu-gcc 43 | CARGO_TARGET_MIPS_UNKNOWN_LINUX_MUSL_LINKER: mips-linux-gnu-gcc 44 | CARGO_TARGET_POWERPC64LE_UNKNOWN_LINUX_GNU_LINKER: powerpc64le-linux-gnu-gcc 45 | CARGO_TARGET_POWERPC64_UNKNOWN_LINUX_GNU_LINKER: powerpc64-linux-gnu-gcc 46 | CARGO_TARGET_POWERPC_UNKNOWN_LINUX_GNU_LINKER: powerpc-linux-gnu-gcc 47 | CARGO_TARGET_S390X_UNKNOWN_LINUX_GNU_LINKER: s390x-linux-gnu-gcc 48 | # Pretty cargo output! 49 | CARGO_TERM_COLOR: always 50 | # Enable cross compilation for `pkg_config`. 51 | PKG_CONFIG_ALLOW_CROSS: 1 52 | # Deny warnings. 53 | RUSTFLAGS: -D warnings 54 | 55 | jobs: 56 | build: 57 | runs-on: ${{ inputs.runs_on }} 58 | continue-on-error: ${{ inputs.continue-on-error }} 59 | steps: 60 | - name: Build | install dependencies 61 | if: inputs.runs_on == 'ubuntu-latest' 62 | run: | 63 | sudo sed -i 's/azure.archive.ubuntu.com/archive.ubuntu.com/' /etc/apt/sources.list 64 | sudo apt-get -qq update 65 | sudo apt-get -qq -y install build-essential curl git pkg-config ${{ inputs.extra_packages }} 66 | 67 | - name: Build | add mingw32 to path 68 | if: inputs.runs_on == 'windows-2019' 69 | shell: bash 70 | run: | 71 | echo "C:\msys64\mingw32\bin" >> $GITHUB_PATH 72 | 73 | - name: Build | checkout 74 | uses: actions/checkout@v2 75 | 76 | - name: Build | install toolchain 77 | uses: dtolnay/rust-toolchain@stable 78 | with: 79 | target: ${{ inputs.target }} 80 | toolchain: ${{ inputs.toolchain }} 81 | 82 | - name: Build | rust-cache 83 | uses: Swatinem/rust-cache@v2 84 | 85 | - name: Build | build library (default features) 86 | run: cargo build --target=${{ inputs.target }} 87 | 88 | - name: Build | build library (no default features) 89 | run: cargo build --no-default-features --target=${{ inputs.target }} 90 | 91 | - name: Build | build library (all features) 92 | run: cargo build --all-features --target=${{ inputs.target }} 93 | 94 | - name: Build | build examples (default features) 95 | if: ${{ inputs.disable_extra_builds == false }} 96 | run: cargo build --examples --target=${{ inputs.target }} 97 | 98 | - name: Build | build examples (no default features) 99 | if: ${{ inputs.disable_extra_builds == false }} 100 | run: cargo build --no-default-features --examples --target=${{ inputs.target }} 101 | 102 | - name: Build | build examples (all features) 103 | if: ${{ inputs.disable_extra_builds == false }} 104 | run: cargo build --examples --all-features --target=${{ inputs.target }} 105 | 106 | - name: Build | build tests (default features) 107 | if: ${{ inputs.disable_extra_builds == false }} 108 | run: cargo build --tests --target=${{ inputs.target }} 109 | 110 | - name: Build | run tests (default features) 111 | if: ${{ inputs.disable_tests == false }} 112 | run: cargo test --no-fail-fast --target=${{ inputs.target }} 113 | 114 | - name: Build | build tests (no default features) 115 | if: ${{ inputs.disable_extra_builds == false }} 116 | run: cargo build --tests --no-default-features --target=${{ inputs.target }} 117 | 118 | - name: Build | run tests (no default features) 119 | if: ${{ inputs.disable_tests == false }} 120 | run: cargo test --no-default-features --no-fail-fast --target=${{ inputs.target }} 121 | 122 | - name: Build | build tests (selected features) 123 | if: ${{ inputs.disable_extra_builds == false }} 124 | run: cargo build --tests --features libudev,usbportinfo-interface --target=${{ inputs.target }} 125 | 126 | - name: Build | run tests (selected features) 127 | if: ${{ inputs.disable_tests == false }} 128 | run: cargo test --no-fail-fast --features libudev,usbportinfo-interface --target=${{ inputs.target }} 129 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | # Check for new issues from updated dependencies once a week (Friday noon). 9 | schedule: 10 | - cron: "0 12 * * 5" 11 | workflow_dispatch: 12 | 13 | jobs: 14 | # -------------------------------------------------------------------------- 15 | # LINT 16 | 17 | lint: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Lint | install dependencies 21 | run: | 22 | sudo sed -i 's/azure.archive.ubuntu.com/archive.ubuntu.com/' /etc/apt/sources.list 23 | sudo apt-get -qq update 24 | sudo apt install -qq -y libudev-dev 25 | 26 | - name: Lint | checkout 27 | uses: actions/checkout@v2 28 | 29 | - name: Lint | install toolchain 30 | uses: dtolnay/rust-toolchain@stable 31 | with: 32 | toolchain: stable 33 | components: clippy, rustfmt 34 | 35 | - name: Lint | rust-cache 36 | uses: Swatinem/rust-cache@v2 37 | 38 | - name: Lint | check formatting 39 | run: cargo fmt -- --check 40 | 41 | - name: Lint | clippy 42 | run: cargo clippy --all-targets --all-features 43 | 44 | # -------------------------------------------------------------------------- 45 | # MSRV 46 | # 47 | # Check at least once per platform. 48 | 49 | msrv-aarch64-apple-darwin: 50 | uses: ./.github/workflows/build.yaml 51 | with: 52 | disable_extra_builds: true 53 | disable_tests: true 54 | target: aarch64-apple-darwin 55 | toolchain: "1.59.0" 56 | 57 | msrv-arm-linux-androideabi: 58 | uses: ./.github/workflows/build.yaml 59 | with: 60 | disable_extra_builds: true 61 | disable_tests: true 62 | target: arm-linux-androideabi 63 | toolchain: "1.59.0" 64 | 65 | msrv-x86_64-unknown-freebsd: 66 | uses: ./.github/workflows/build.yaml 67 | with: 68 | disable_extra_builds: true 69 | disable_tests: true 70 | target: x86_64-unknown-freebsd 71 | toolchain: "1.59.0" 72 | 73 | msrv-x86_64-unknown-linux-gnu: 74 | uses: ./.github/workflows/build.yaml 75 | with: 76 | disable_extra_builds: true 77 | disable_tests: true 78 | extra_packages: libudev-dev 79 | target: x86_64-unknown-linux-gnu 80 | toolchain: "1.59.0" 81 | 82 | msrv-x86_64-unknown-linux-musl: 83 | uses: ./.github/workflows/build.yaml 84 | with: 85 | disable_extra_builds: true 86 | disable_tests: true 87 | extra_packages: gcc-aarch64-linux-gnu 88 | target: aarch64-unknown-linux-musl 89 | toolchain: "1.59.0" 90 | 91 | msrv-x86_64-pc-windows-msvc: 92 | uses: ./.github/workflows/build.yaml 93 | with: 94 | disable_extra_builds: true 95 | disable_tests: true 96 | runs_on: windows-2019 97 | target: x86_64-pc-windows-msvc 98 | toolchain: "1.59.0" 99 | 100 | msrv-x86_64-unknown-netbsd: 101 | uses: ./.github/workflows/build.yaml 102 | with: 103 | disable_extra_builds: true 104 | disable_tests: true 105 | target: x86_64-unknown-netbsd 106 | toolchain: "1.59.0" 107 | 108 | # -------------------------------------------------------------------------- 109 | # Semantic Versioning 110 | # 111 | # Check at least once per platform as we heavily depend on platform-specific 112 | # code. The feature groups are used for attempting to cover different 113 | # backends for a platform (like Linux with and without libudev). 114 | 115 | semver: 116 | runs-on: ubuntu-latest 117 | strategy: 118 | matrix: 119 | target: 120 | - aarch64-apple-darwin 121 | - arm-linux-androideabi 122 | - x86_64-pc-windows-msvc 123 | - x86_64-unknown-freebsd 124 | - x86_64-unknown-linux-gnu 125 | - x86_64-unknown-netbsd 126 | feature-group: 127 | - "only-explicit-features" 128 | - "all-features" 129 | steps: 130 | - run: | 131 | # TODO: Harmonize with build.yaml 132 | sudo sed -i 's/azure.archive.ubuntu.com/archive.ubuntu.com/' /etc/apt/sources.list 133 | sudo apt-get -qq update 134 | sudo apt-get -qq -y install build-essential curl git pkg-config libudev-dev 135 | - uses: actions/checkout@v2 136 | - uses: dtolnay/rust-toolchain@stable 137 | with: 138 | target: ${{ matrix.target }} 139 | - uses: Swatinem/rust-cache@v2 140 | - uses: obi1kenobi/cargo-semver-checks-action@v2 141 | with: 142 | rust-target: ${{ matrix.target }} 143 | feature-group: ${{ matrix.feature-group }} 144 | 145 | # -------------------------------------------------------------------------- 146 | # cargo-deny 147 | 148 | cargo-deny: 149 | runs-on: ubuntu-latest 150 | strategy: 151 | matrix: 152 | checks: 153 | - advisories 154 | - bans licenses sources 155 | 156 | # Prevent sudden announcement of a new advisory from failing ci: 157 | continue-on-error: ${{ matrix.checks == 'advisories' }} 158 | 159 | steps: 160 | - uses: actions/checkout@v3 161 | - uses: EmbarkStudios/cargo-deny-action@v1 162 | with: 163 | command: check ${{ matrix.checks }} 164 | 165 | # -------------------------------------------------------------------------- 166 | # BUILD 167 | 168 | aarch64-apple-darwin: 169 | uses: ./.github/workflows/build.yaml 170 | with: 171 | disable_tests: true 172 | runs_on: macos-latest 173 | target: aarch64-apple-darwin 174 | 175 | aarch64-apple-ios: 176 | uses: ./.github/workflows/build.yaml 177 | with: 178 | disable_tests: true 179 | runs_on: macos-latest 180 | target: aarch64-apple-ios 181 | 182 | aarch64-unknown-linux-gnu: 183 | uses: ./.github/workflows/build.yaml 184 | with: 185 | disable_extra_builds: true 186 | disable_tests: true 187 | extra_packages: libudev-dev gcc-aarch64-linux-gnu libc6-dev-arm64-cross 188 | target: aarch64-unknown-linux-gnu 189 | 190 | aarch64-unknown-linux-musl: 191 | uses: ./.github/workflows/build.yaml 192 | with: 193 | disable_tests: true 194 | extra_packages: gcc-aarch64-linux-gnu 195 | target: aarch64-unknown-linux-musl 196 | 197 | arm-linux-androideabi: 198 | uses: ./.github/workflows/build.yaml 199 | with: 200 | disable_extra_builds: true 201 | disable_tests: true 202 | target: arm-linux-androideabi 203 | 204 | armv7-linux-androideabi: 205 | uses: ./.github/workflows/build.yaml 206 | with: 207 | disable_extra_builds: true 208 | disable_tests: true 209 | target: armv7-linux-androideabi 210 | 211 | i686-pc-windows-gnu: 212 | uses: ./.github/workflows/build.yaml 213 | with: 214 | runs_on: windows-2019 215 | target: i686-pc-windows-gnu 216 | 217 | i686-pc-windows-msvc: 218 | uses: ./.github/workflows/build.yaml 219 | with: 220 | runs_on: windows-2019 221 | target: i686-pc-windows-msvc 222 | 223 | i686-unknown-linux-gnu: 224 | uses: ./.github/workflows/build.yaml 225 | with: 226 | disable_extra_builds: true 227 | disable_tests: true 228 | extra_packages: libudev-dev gcc-multilib 229 | target: i686-unknown-linux-gnu 230 | 231 | i686-unknown-linux-musl: 232 | uses: ./.github/workflows/build.yaml 233 | with: 234 | extra_packages: libudev-dev gcc-multilib 235 | target: i686-unknown-linux-musl 236 | 237 | x86_64-apple-darwin: 238 | uses: ./.github/workflows/build.yaml 239 | with: 240 | runs_on: macos-latest 241 | target: x86_64-apple-darwin 242 | 243 | x86_64-pc-windows-gnu: 244 | uses: ./.github/workflows/build.yaml 245 | with: 246 | runs_on: windows-2019 247 | target: x86_64-pc-windows-gnu 248 | 249 | x86_64-pc-windows-msvc: 250 | uses: ./.github/workflows/build.yaml 251 | with: 252 | runs_on: windows-2019 253 | target: x86_64-pc-windows-msvc 254 | 255 | x86_64-unknown-freebsd: 256 | uses: ./.github/workflows/build.yaml 257 | with: 258 | disable_extra_builds: true 259 | disable_tests: true 260 | target: x86_64-unknown-freebsd 261 | 262 | x86_64-unknown-linux-gnu: 263 | uses: ./.github/workflows/build.yaml 264 | with: 265 | extra_packages: libudev-dev 266 | target: x86_64-unknown-linux-gnu 267 | 268 | x86_64-unknown-linux-musl: 269 | uses: ./.github/workflows/build.yaml 270 | with: 271 | target: x86_64-unknown-linux-musl 272 | 273 | x86_64-unknown-netbsd: 274 | uses: ./.github/workflows/build.yaml 275 | with: 276 | disable_extra_builds: true 277 | disable_tests: true 278 | target: x86_64-unknown-netbsd 279 | 280 | # -------------------------------------------------------------------------- 281 | # NIGHTLY BUILD 282 | 283 | aarch64-apple-darwin-nightly: 284 | uses: ./.github/workflows/build.yaml 285 | with: 286 | continue-on-error: true 287 | disable_tests: true 288 | runs_on: macos-latest 289 | target: aarch64-apple-darwin 290 | toolchain: nightly 291 | 292 | x86_64-pc-windows-msvc-nightly: 293 | uses: ./.github/workflows/build.yaml 294 | with: 295 | continue-on-error: true 296 | runs_on: windows-2019 297 | target: x86_64-pc-windows-msvc 298 | toolchain: nightly 299 | 300 | x86_64-unknown-linux-gnu-nightly: 301 | uses: ./.github/workflows/build.yaml 302 | with: 303 | continue-on-error: true 304 | extra_packages: libudev-dev 305 | target: x86_64-unknown-linux-gnu 306 | toolchain: nightly 307 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | *.bk 4 | *~ 5 | .env 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](https://keepachangelog.com/) and this 6 | project adheres to [Semantic Versioning](https://semver.org/). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | ### Changed 12 | ### Fixed 13 | ### Removed 14 | 15 | 16 | ## [4.7.2] - 2025-05-16 17 | 18 | ### Fixed 19 | 20 | * Set data terminal ready (DTR) on best-effort for not failing in the situation 21 | where we can't detect whether to apply this setting reliably. 22 | [#268](https://github.com/serialport/serialport-rs/pull/268) 23 | 24 | 25 | ## [4.7.1] - 2025-03-25 26 | 27 | ### Fixed 28 | 29 | * Parsing serial numbers with underscore from Windows HWIDs 30 | [#253](https://github.com/serialport/serialport-rs/issues/253) 31 | * Enumerate Bluetooth serial devices (RFCOMM) on Linux too. 32 | [#246](https://github.com/serialport/serialport-rs/issues/246) 33 | 34 | 35 | ## [4.7.0] - 2025-01-13 36 | 37 | ### Changed 38 | 39 | * Enumerate ports from more subsystems on Linux without libudev. 40 | [#238](https://github.com/serialport/serialport-rs/pull/238) 41 | * Set data terminal ready (DTR) signal when opening a port by default and allow 42 | to customize this behavior through the builder. 43 | [#239](https://github.com/serialport/serialport-rs/pull/239) 44 | 45 | ### Fixed 46 | 47 | * Retry flushing data on `EINTR` up to the ports read/write timeout. 48 | [#225](https://github.com/serialport/serialport-rs/pull/225) 49 | 50 | 51 | ## [4.6.1] - 2024-12-01 52 | 53 | ### Fixed 54 | 55 | * Pin subdependency `libc` to maintain compatibility with MSRV 1.59.0. 56 | [#229](https://github.com/serialport/serialport-rs/pull/229) 57 | 58 | 59 | ## [4.6.0] - 2024-10-21 60 | 61 | ### Added 62 | 63 | * Add recommendation on how to interpret `UsbPortInfo::interface_number`. 64 | [#219](https://github.com/serialport/serialport-rs/pull/219) 65 | * Add support for retrieving USB port info on Linux without libudev. 66 | [#220](https://github.com/serialport/serialport-rs/pull/220) 67 | 68 | ### Changed 69 | 70 | * Switched from core-foundation-sys to core-foundation for more conveniently 71 | working with Core Foundation types for enumeration on macOS. 72 | [#218](https://github.com/serialport/serialport-rs/pull/218) 73 | * Refactored output from example `list_ports` (alignment and order) for easily 74 | comparing different runs. 75 | [#220](https://github.com/serialport/serialport-rs/pull/220) 76 | 77 | ### Fixed 78 | 79 | * Fix enumeration USB reported as PCI devices which do not have a (short) 80 | serial number. 81 | [#160](https://github.com/serialport/serialport-rs/pull/160) 82 | * Fix ignoring the status of several function calls into Core Foundation on mac 83 | OS. 84 | [#218](https://github.com/serialport/serialport-rs/pull/218) 85 | 86 | 87 | ## [4.5.1] - 2024-09-20 88 | 89 | ### Fixed 90 | 91 | * Fix ignoring errors from setting baud rate and ignoring unsupported baud 92 | rates. 93 | [#213](https://github.com/serialport/serialport-rs/pull/213) 94 | * Remove leftover debug output from `posix::poll::wait_fd`. 95 | [#216](https://github.com/serialport/serialport-rs/pull/216) 96 | 97 | 98 | ## [4.5.0] - 2024-08-05 99 | 100 | ### Added 101 | 102 | * Add `IntoRawHandle` implementation for `COMPort` 103 | [#199](https://github.com/serialport/serialport-rs/pull/199) 104 | * Add MODALIAS as additional source of information for USB devices on Linux 105 | [#170](https://github.com/serialport/serialport-rs/pull/170) 106 | 107 | ### Changed 108 | * Replace using regex crate for parsing device identification strings for 109 | `available_ports` on Windows. This is now done by some bespoke code to 110 | significantly reduce build times. 111 | [#201](https://github.com/serialport/serialport-rs/pull/201) 112 | * Switch from ANSI to Unicode/UTF-16 string API on Windows. 113 | [#89](https://github.com/serialport/serialport-rs/pull/89) 114 | ### Fixed 115 | * Fix looking up `UsbPortInfo::interface` on macOS. 116 | [#193](https://github.com/serialport/serialport-rs/pull/193) 117 | * Fix issues with very long timeout values like `Duration::MAX` by clamping to 118 | maximum supported value for underlying platform. 119 | [#207](https://github.com/serialport/serialport-rs/issues/207), 120 | [#208](https://github.com/serialport/serialport-rs/pull/208) 121 | 122 | 123 | ## [4.4.0] - 2024-06-26 124 | 125 | ### Added 126 | * Add conversions between `DataBits`, `StopBits` types and their numeric 127 | representations. 128 | * Add `FromStr` implementation for `FlowControl`. 129 | [#163](https://github.com/serialport/serialport-rs/pull/163) 130 | 131 | ### Changed 132 | * Several changes for CI hygiene. 133 | 134 | ### Fixed 135 | * Fix a bug where `available_ports()` returned disabled devices on Windows. 136 | [#144](https://github.com/serialport/serialport-rs/pull/144) 137 | * Fix a bug on Windows where the `WriteTotalTimeoutConstant` field hasn't been 138 | configured properly when the `set_timeout` method is called. 139 | [#124](https://github.com/serialport/serialport-rs/issues/124) 140 | * Fix a longstanding bug on Windows where timeouts of length zero 141 | (`Duration::ZERO`) actually resulted in waiting indefinitely. 142 | [#79](https://github.com/serialport/serialport-rs/pull/79) 143 | * Fix missing modem ports from `available_ports()` on Windows. 144 | [#81](https://github.com/serialport/serialport-rs/issues/81) 145 | [#84](https://github.com/serialport/serialport-rs/pull/84) 146 | * Fix MSRV incompatibility with sub-dependency of clap. 147 | [#186](https://github.com/serialport/serialport-rs/pull/186) 148 | 149 | 150 | ## [4.3.0] - 2023-12-11 151 | 152 | ### Changed 153 | 154 | * Raise MSRV from 1.56.1 to 1.59.0 and Rust edition from 2018 to 2021. 155 | [#137](https://github.com/serialport/serialport-rs/pull/137) 156 | * Update `bitflags` dependency to 2.4.0. 157 | [#127](https://github.com/serialport/serialport-rs/pull/127) 158 | * Open serial devices with `O_CLOEXEC` (Posix). This will close the device 159 | handles when starting a child process. In particular this means that a serial 160 | device can be reopened after making SW update of a Tauri application. 161 | [#130](https://github.com/serialport/serialport-rs/pull/130) 162 | * Prefer USB device manufacturer and model information from the actual USB 163 | device over the information from udev's database. 164 | [#137](https://github.com/serialport/serialport-rs/pull/137) 165 | 166 | ### Fixed 167 | * Fixes a bug on Windows where composite devices would show a incorrect serial 168 | number. 169 | [#141](https://github.com/serialport/serialport-rs/pull/141) 170 | * Fixes a bug on Linux without udev where `available_ports()` returned wrong 171 | device file paths. 172 | [#122](https://github.com/serialport/serialport-rs/pull/122) 173 | * Fixes a bug on Windows where some USB device serial numbers were truncated. 174 | [#131](https://github.com/serialport/serialport-rs/pull/131) 175 | * Switches to maintained sys crates for CoreFoundation and IOKit on macOS. 176 | [#112](https://github.com/serialport/serialport-rs/issues/112), 177 | [#136](https://github.com/serialport/serialport-rs/pull/136) 178 | 179 | ## [4.2.2] - 2023-08-03 180 | ### Fixed 181 | * Fixes a bug on the Raspberry Pi 4, which results in USB-devices being detected as PCI-devices. 182 | [#113](https://github.com/serialport/serialport-rs/pull/113) 183 | 184 | 185 | 186 | ## [4.2.1] - 2023-05-21 187 | ### Added 188 | * Add support for reporting the USB device interface (feature-gated by 189 | _usbserialinfo-interface_). 190 | [#47](https://github.com/serialport/serialport-rs/pull/47), 191 | [#101](https://github.com/serialport/serialport-rs/pull/101) 192 | * Add example for loopback testing with real hardware. 193 | [#69](https://github.com/serialport/serialport-rs/pull/69) 194 | * Implement `fmt::Debug` and `fmt::Display` for `SerialPort` and related enums. 195 | [#91](https://github.com/serialport/serialport-rs/pull/91) 196 | ### Changed 197 | * Migrated from unmaintainted dependency `mach` to `mach2`. 198 | * Update dependency `nix` from 0.24.1 to 0.26.0 and raise MSRV to 1.56.1. 199 | [#67](https://github.com/serialport/serialport-rs/pull/67), 200 | [#75](https://github.com/serialport/serialport-rs/pull/75), 201 | [#78](https://github.com/serialport/serialport-rs/pull/78) 202 | ### Fixed 203 | * Skip attempts to set baud rate 0 on macOS. 204 | [#58](https://github.com/serialport/serialport-rs/pull/58) 205 | * Fix getting actual result value from `tiocmget`. 206 | [#61](https://github.com/serialport/serialport-rs/pull/61/files) 207 | * Fix serial number retrieval procedure on macOS. 208 | [#65](https://github.com/serialport/serialport-rs/pull/65) 209 | * Fix port name retrieval procedure for Unicode names on Windows. 210 | [#63](https://github.com/serialport/serialport-rs/pull/63) 211 | * Fix compilation for OpenBSD due to missing use declaration. 212 | [#68](https://github.com/serialport/serialport-rs/pull/68) 213 | * A number of memory leaks have been addressed when using serialport-rs. 214 | [#98](https://github.com/serialport/serialport-rs/pull/98) 215 | 216 | ## [4.2.0] - 2022-06-02 217 | ### Added 218 | * Add `serde` support behind a feature flag. 219 | [#51](https://github.com/serialport/serialport-rs/pull/51) 220 | ### Changed 221 | * Request exclusive access when opening a POSIX serial port by default. 222 | [#44](https://github.com/serialport/serialport-rs/pull/44) 223 | * Updated `nix` dependency to 0.24.1 and limited features. 224 | [#46](https://github.com/serialport/serialport-rs/pull/46) 225 | * Derive the `Clone` trait for `Error`. 226 | [#53](https://github.com/serialport/serialport-rs/pull/53) 227 | * Enumerate callout devices in addition to dial-in devices on macOS. 228 | [#54](https://github.com/serialport/serialport-rs/pull/54) 229 | * Revert to edition 2018 to allow for use with older compiler versions. 230 | ### Fixed 231 | * Set port timeout to a non-zero value before performing loopback test. 232 | [#45](https://github.com/serialport/serialport-rs/pull/45) 233 | 234 | ## [4.1.0] - 2022-04-04 235 | ### Added 236 | * impl `SerialPort` for `&mut T`. This allows a `&mut T (where T: SerialPort)` 237 | to be used in a context where `impl SerialPort` is expected. 238 | [!114](https://gitlab.com/susurrus/serialport-rs/-/merge_requests/114) 239 | ### Changed 240 | * Updated `nix` dependency to 0.23.1. 241 | * Remove useless call to tcflush on open. 242 | [#40](https://github.com/serialport/serialport-rs/pull/40) 243 | ### Fixed 244 | * Improved support for recent versions of macOS. 245 | [!104](https://gitlab.com/susurrus/serialport-rs/-/merge_requests/104) 246 | * Fix filehandle leak in open() on Windows. 247 | [#36](https://github.com/serialport/serialport-rs/pull/36) 248 | * Make sure fd is properly closed if initialization fails. 249 | [#39](https://github.com/serialport/serialport-rs/pull/39) 250 | [#41](https://github.com/serialport/serialport-rs/pull/41) 251 | 252 | ## [4.0.1] - 2021-04-17 253 | ### Changed 254 | * Update maintenance status to looking for a new maintainer. 255 | ### Fixed 256 | * Properly initialize DCB structure on Windows. This fixes some non-functional 257 | devices. 258 | [!97](https://gitlab.com/susurrus/serialport-rs/-/merge_requests/97) 259 | 260 | ## [4.0.0] - 2020-12-17 261 | ### Added 262 | * Added `send_break()` to `TTYPort`. 263 | [!69](https://gitlab.com/susurrus/serialport-rs/merge_requests/69) 264 | * Enable `available_ports()` for Linux musl targets and those without the 265 | `libudev` feature enabled by scanning `/sys/` for ports. 266 | [!72](https://gitlab.com/susurrus/serialport-rs/merge_requests/72) 267 | * `ENOENT` and `EACCES` errors are now exposed as `NotFound` and 268 | `PermissionDenied` errors on Linux. 269 | [!80](https://gitlab.com/susurrus/serialport-rs/merge_requests/80) 270 | * `try_clone_native()` was added to `COMPort` and `TTYPort` to complement 271 | `SerialPort::try_clone()` but returning the concrete type instead. 272 | [!85](https://gitlab.com/susurrus/serialport-rs/merge_requests/85) 273 | * Added `set_break()` and `clear_break()` to `SerialPort`. 274 | [!70](https://gitlab.com/susurrus/serialport-rs/merge_requests/70) 275 | 276 | ### Changed 277 | * Minimum supported Rust version is now 1.36.0 to support the `mem::MaybeUninit` 278 | feature. 279 | * The platform-specific `TTYPort`/`BreakDuration` and `COMPort` are now at the 280 | root level rather than under the `posix` and `windows` submodules 281 | respectively. 282 | * Opening `SerialPort` s now uses the builder pattern through 283 | `serialport::new()`. See the README for concrete examples. 284 | [!73](https://gitlab.com/susurrus/serialport-rs/merge_requests/73) 285 | * `SerialPorts`s are no longer opened with a default timeout of 1ms. 286 | * Under linux, the `manufacturer` and `product` fields of `UsbPortInfo` now take 287 | their values from the `ID_VENDOR_FROM_DATABASE` and `ID_MODEL_FROM_DATABASE` 288 | udev properties respectively, instead of the `ID_VENDOR` and `ID_MODEL` 289 | properties that were used before. When the `_FROM_DATABASE` values are not 290 | available, it falls back to the old behavior. 291 | [!86](https://gitlab.com/susurrus/serialport-rs/merge_requests/86) 292 | * POSIX ports are no longer opened in exclusive mode. After opening they can be 293 | made exclusive via `TTYPort::set_exclusive()`. 294 | [!98](https://gitlab.com/susurrus/serialport-rs/merge_requests/98) 295 | 296 | ### Fixed 297 | * Raised the version specification for `bitflags` to 1.0.4. Previously it was 298 | set to 1.0.0, but this version of `bitflags` is actually incompatible with 299 | Rust 2018 style macro imports that `serialport-rs` requires. 300 | [!83](https://gitlab.com/susurrus/serialport-rs/merge_requests/83) 301 | 302 | ### Removed 303 | * Removed the `serialport::prelude` module. Types should be explicitly imported 304 | or can be glob-imported from the root like `use serialport::*`. 305 | [!82](https://gitlab.com/susurrus/serialport-rs/merge_requests/82) 306 | 307 | ## [3.3.0] - 2019-06-12 308 | ### Added 309 | * Added support for arbitrary baud rates on macOS and iOS. 310 | 311 | ### Changed 312 | * Minimum supported Rust version is now 1.31 to support using the 2018 edition 313 | of Rust. 314 | 315 | ### Fixed 316 | * Upgraded `sparc64-unknown-linux-gnu` to Tier 2 support. 317 | 318 | ## [3.2.0] - 2019-01-01 319 | ### Added 320 | * Port enumeration is now supported on FreeBSD. 321 | 322 | ### Changed 323 | * Minimum supported Rust version changed to 1.24.1. 324 | * Made `aarch64-unknown-linux-musl` a Tier-2 supported target. 325 | 326 | ### Fixed 327 | * Fixed software flow control for POSIX systems. 328 | [!54](https://gitlab.com/susurrus/serialport-rs/merge_requests/54) 329 | 330 | ### Removed 331 | * Removed support for `x86_64-unknown-linux-gnux32`. 332 | 333 | ## [3.1.0] - 2018-11-02 334 | ### Added 335 | * Added `bytes_to_read()`, `bytes_to_write()`, and `clear()` to `SerialPort`. 336 | Also added example scripts for using them. 337 | * Added Tier 2 support for: 338 | * `armv5te-unknown-linux-musleabi` 339 | * Added "libudev" feature to allow for disabling linking to `libudev` on Linux. 340 | 341 | ## [3.0.0] - 2018-07-14 342 | ### Added 343 | * Arbitrary baud rates are now supported on BSDs, Linux, and Windows. 344 | * Added Tier 1 support for `{i586|i686|x86_64}-unknown-linux-musl`. 345 | * Added Tier 2 support for: 346 | * `{arm|armv7}-linux-androideabi` 347 | * `i686-linux-android` 348 | * `{i686|x86_64}-unknown-freebsd` 349 | * `arm-unknown-linux-musleabi` 350 | * `armv7-unknown-linux-musleabihf` 351 | * `{mips64|mips64el}-unknown-linux-gnuabi64` 352 | * `armv5te-unknown-linux-gnueabi` 353 | * `{aarch64|mips|mipsel|powerpc64|powerpc64le|powerpc|s390x}-unknown-linux-gnu` 354 | * `{mips|mipsel}-unknown-linux-musl` 355 | * `x86_64-unknown-netbsd` 356 | * Added Tier 3 support for: 357 | * `{aarch64|x86_64}-linux-android` 358 | * `aarch64-unknown-linux-musl` 359 | * `sparc64-unknown-linux-gnu`, 360 | * `x86_64-unknown-linux-gnux32` 361 | 362 | ### Changed 363 | * Most port configuration methods now return a `Result<()>`. 364 | * Renamed `SerialPort::port_name()` to `name()`. 365 | 366 | ### Fixed 367 | * On Windows, the `port_name` field on `SerialPortInfo` included an extraneous 368 | trailing nul byte character. 369 | 370 | ### Removed 371 | * The `BaudRate` enum was removed in favor of a `u32`. 372 | 373 | ## [2.3.0] - 2018-03-13 374 | ### Added 375 | * Added `examples/hardware_check.rs` for use in debugging library or driver 376 | issues when using physical serial ports. 377 | * Added `SerialPort::try_clone` which allows for cloning a port for full-duplex 378 | reading and writing. 379 | 380 | ### Changed 381 | * Removed configuration caching for serial ports. The underlying implementations 382 | for all platforms cached a configuration struct so that modifying the port 383 | settings involved a single switch into kernel space. This has been removed so 384 | now two system calls are needed for every configuration change. This is 385 | probably a slight performance regression, but should allow the new 386 | `SerialPort::try_clone` interface to work as people expect. 387 | 388 | ### Fixed 389 | * `TTYPort::into_raw_fd` will now work as expected. It previously closed the 390 | port so the returned file descriptor would be invalid. 391 | * 921600 baud is now supported on NetBSD and FreeBSD. 392 | 393 | ## 2.2.0 - 2018-03-13 394 | Unreleased, happened due to a user error using `cargo-release`. 395 | 396 | ## [2.1.0] - 2018-02-14 397 | ### Added 398 | * `impl FromRawHandle` for `COMPort`. 399 | 400 | ### Changed 401 | * Specific IO-related errors are now returned instead of mapping every IO error 402 | to Unknown. This makes it possible to catch things like time-out errors. 403 | * Changed all baud rates to be reported as the discrete `BaudRate::Baud*` types 404 | rather than as the `BaudRate::BaudOther(*)` type. 405 | 406 | ### Fixed 407 | * Modem-type USB serial devices are now enumerated on macOS. This now allows 408 | connected Arduinos to be detected. 409 | * Compilation on FreeBSD and NetBSD was fixed by removing the 921600 baud rates. 410 | These will be re-added in a future release. 411 | 412 | ## [2.0.0] - 2017-12-18 413 | ### Added 414 | * USB device information is now returned in calls to `available_ports()`. 415 | * Serial port enumeration is now supported on Mac. 416 | * Serial port enumeration now attempts to return the interface used for the port 417 | (USB, PCI, Bluetooth, Unknown). 418 | * `BaudRate::standard_rates()` provides a vector of cross-platform baud rates. 419 | * `SerialPort` trait is now `Send`. 420 | 421 | ### Changed 422 | * Software license has changed from LGPLv3+ to MPL-2.0. This makes it possible 423 | to use this library in any Rust project if it's unmodified. 424 | * Mac is now a Tier 2 supported platform. 425 | * Removed `BaudRate::from_speed(usize)` and `BaudRate::speed -> usize` in favor 426 | of the `From` and `Into` traits. 427 | * Removed `available_baud_rates` in favor of `BaudRate::platform_rates()` as 428 | this has a more clear semantic meaning. The returned list of baud rates is now 429 | also correct for all supported platforms. 430 | * Removed `termios` dependency in favor of `nix`. This is a big step towards 431 | supporting additional platforms. 432 | 433 | ### Fixed 434 | * Stop bits are now specified properly (had been reversed). Thanks to 435 | @serviushack. (MR#9) 436 | * `TTYPort::pair()` is now thread-safe. 437 | * `TTYPort::open()` no longer leaks file descriptors if it errors. Thanks to 438 | @daniel. (MR#12) 439 | * Fixed compilation when targeting Android. 440 | 441 | ## [1.0.1] - 2017-02-20 442 | ### Fixed 443 | * `read()` now properly blocks for at least one character. 444 | * Compilation now works on Mac. 445 | 446 | ## [1.0.0] - 2017-02-13 447 | ### Changed 448 | * Various documentation/README updates. 449 | * Minor formatting fixes (from rustfmt). 450 | 451 | ### Fixed 452 | * Platform-specific examples are now only built on appropriate platforms. 453 | 454 | ## [0.9.0] - 2017-02-09 455 | ### Added 456 | * `impl Debug` for `COMPort`. 457 | * `exclusive()` and `set_exclusive()` for `TTYPort`. 458 | * `port_name()` for `SerialPort`. 459 | * `impl FromRawFd` and `impl IntoRawFd` for `TTYPort`. 460 | * `pair()` for `TTYPort`. 461 | 462 | ## [0.3.0] - 2017-01-28 463 | ### Added 464 | * `open_with_settings()` to support initializing the port with custom settings. 465 | * `SerialPortSettings` is now publically usable being exported in the prelude, 466 | having all public and commented fields, and a `Default` impl. 467 | 468 | ### Changed 469 | * `TTYPort/COMPort::open()` now take a `SerialPortSettings` argument and return 470 | concrete types. 471 | * `serialport::open()` now initializes the port to reasonable defaults. 472 | * Removed all instances of `try!()` for `?`. 473 | * `SerialPort::set_all()` now borrows `SerialPortSettings`. 474 | 475 | ## [0.2.4] - 2017-01-26 476 | ### Added 477 | * Report an Unimplemented error for unsupported unix targets. 478 | 479 | ### Changed 480 | * Minor changes suggested by Clippy. 481 | * Reworked Cargo.toml to more easily support additional targets. 482 | 483 | ### Fixed 484 | * AppVeyor badge should now be properly displayed. 485 | 486 | ## [0.2.3] - 2017-01-21 487 | ### Added 488 | * Specify AppVeyor build status badge for crates.io. 489 | 490 | ## [0.2.2] - 2017-01-21 491 | * No changes, purely a version increment to push new crate metadata to 492 | crates.io. 493 | 494 | ## [0.2.1] - 2017-01-21 495 | ### Added 496 | * Specify category for crates.io. 497 | 498 | ## [0.2.0] - 2017-01-07 499 | ### Added 500 | * Added a changelog. 501 | * Added a getter/setter pair for all settings at once. 502 | * An error is thrown if settings weren't correctly applied on POSIX. 503 | 504 | ## [0.1.1] - 2016-12-23 505 | ### Changed 506 | * Fixed compilation on x86_64-pc-windows-gnu target. 507 | * Added contributors to README. 508 | * Clarified license terms in the README. 509 | 510 | ## [0.1.0] - 2016-12-22 511 | ### Added 512 | * Initial release. 513 | 514 | 515 | [Unreleased]: https://github.com/serialport/serialport-rs/compare/v4.7.2...HEAD 516 | [4.7.2]: https://github.com/serialport/serialport-rs/compare/v4.7.1...v4.7.2 517 | [4.7.1]: https://github.com/serialport/serialport-rs/compare/v4.7.0...v4.7.1 518 | [4.7.0]: https://github.com/serialport/serialport-rs/compare/v4.6.1...v4.7.0 519 | [4.6.1]: https://github.com/serialport/serialport-rs/compare/v4.6.0...v4.6.1 520 | [4.6.0]: https://github.com/serialport/serialport-rs/compare/v4.5.1...v4.6.0 521 | [4.5.1]: https://github.com/serialport/serialport-rs/compare/v4.5.0...v4.5.1 522 | [4.5.0]: https://github.com/serialport/serialport-rs/compare/v4.4.0...v4.5.0 523 | [4.4.0]: https://github.com/serialport/serialport-rs/compare/v4.3.0...v4.4.0 524 | [4.3.0]: https://github.com/serialport/serialport-rs/compare/v4.2.2...v4.3.0 525 | [4.2.2]: https://github.com/serialport/serialport-rs/compare/v4.2.1...v4.2.2 526 | [4.2.1]: https://github.com/serialport/serialport-rs/compare/v4.2.0...v4.2.1 527 | [4.2.0]: https://github.com/serialport/serialport-rs/compare/v4.1.0...v4.2.0 528 | [4.1.0]: https://github.com/serialport/serialport-rs/compare/v4.0.1...v4.1.0 529 | [4.0.1]: https://github.com/serialport/serialport-rs/compare/v4.0.0...v4.0.1 530 | [4.0.0]: https://github.com/serialport/serialport-rs/compare/v3.3.0...v4.0.0 531 | [3.3.0]: https://github.com/serialport/serialport-rs/compare/v3.2.0...v3.3.0 532 | [3.2.0]: https://github.com/serialport/serialport-rs/compare/v3.1.0...v3.2.0 533 | [3.1.0]: https://github.com/serialport/serialport-rs/compare/v3.0.0...v3.1.0 534 | [3.0.0]: https://github.com/serialport/serialport-rs/compare/v2.3.0...v3.0.0 535 | [2.3.0]: https://github.com/serialport/serialport-rs/compare/v2.1.0...v2.3.0 536 | [2.1.0]: https://github.com/serialport/serialport-rs/compare/v2.0.0...v2.1.0 537 | [2.0.0]: https://github.com/serialport/serialport-rs/compare/v1.0.1...v2.0.0 538 | [1.0.1]: https://github.com/serialport/serialport-rs/compare/v1.0.0...v1.0.1 539 | [1.0.0]: https://github.com/serialport/serialport-rs/compare/v0.9.0...v1.0.0 540 | [0.9.0]: https://github.com/serialport/serialport-rs/compare/v0.3.0...v0.9.0 541 | [0.3.0]: https://github.com/serialport/serialport-rs/compare/v0.2.4...v0.3.0 542 | [0.2.4]: https://github.com/serialport/serialport-rs/compare/v0.2.3...v0.2.4 543 | [0.2.3]: https://github.com/serialport/serialport-rs/compare/v0.2.2...v0.2.3 544 | [0.2.2]: https://github.com/serialport/serialport-rs/compare/v0.2.1...v0.2.2 545 | [0.2.1]: https://github.com/serialport/serialport-rs/compare/v0.2.0...v0.2.1 546 | [0.2.0]: https://github.com/serialport/serialport-rs/compare/v0.1.1...v0.2.0 547 | [0.1.1]: https://github.com/serialport/serialport-rs/compare/v0.1.0...v0.1.1 548 | [0.1.0]: https://github.com/serialport/serialport-rs/releases/tag/v0.1.0 549 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "serialport" 3 | version = "4.7.3-alpha.0" 4 | authors = [ 5 | "Bryant Mairs ", 6 | "Jesse Braham ", 7 | ] 8 | edition = "2021" 9 | rust-version = "1.59.0" 10 | description = "A cross-platform low-level serial port library." 11 | documentation = "https://docs.rs/serialport" 12 | repository = "https://github.com/serialport/serialport-rs" 13 | license = "MPL-2.0" 14 | keywords = ["serial", "hardware", "system", "RS232"] 15 | categories = ["hardware-support"] 16 | 17 | [target."cfg(unix)".dependencies] 18 | bitflags = "2.4.0" 19 | nix = { version = "0.26", default-features = false, features = ["fs", "ioctl", "poll", "signal", "term"] } 20 | 21 | [target.'cfg(all(target_os = "linux", not(target_env = "musl")))'.dependencies] 22 | libudev = { version = "0.3.0", optional = true } 23 | unescaper = "0.1.3" 24 | 25 | [target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] 26 | core-foundation = "0.10.0" 27 | core-foundation-sys = "0.8.4" 28 | io-kit-sys = "0.4.0" 29 | mach2 = "0.4.1" 30 | 31 | [target."cfg(windows)".dependencies.winapi] 32 | version = "0.3.9" 33 | features = [ 34 | "cguid", "commapi", "errhandlingapi", "fileapi", "guiddef", "handleapi", "minwinbase", 35 | "minwindef", "ntdef", "setupapi", "winbase", "winerror", "winnt", 36 | ] 37 | 38 | [dependencies] 39 | cfg-if = "1.0.0" 40 | scopeguard = "1.1" 41 | serde = { version = "1.0", features = ["derive"], optional = true } 42 | 43 | [dev-dependencies] 44 | assert_hex = "0.4.1" 45 | clap = { version = "3.1.6", features = ["derive"] } 46 | envconfig = "0.10.0" 47 | # TODES Remove pinning this subdependency (of clap) when we are bumping our 48 | # MSRV (libc raised its MSRV with a patch release 0.2.167 from 1.19.0 to 49 | # 1.63.0). Trick the resolver into picking a compatible release of libc by 50 | # adding it as a direct dependency meanwhile. 51 | libc = ">=0.2.0, <=0.2.163" 52 | # TODO: Remove pinning this subdependency of clap when we are bumping our MSRV. 53 | # (There has been an incompatible change with the MSRV of os_str_bytes with 54 | # 6.6.0) Until then we are tricking the dependency resolver into using a 55 | # compatible version by adding it as a direct dependency here. 56 | os_str_bytes = ">=6.0, <6.6.0" 57 | quickcheck = "1.0.3" 58 | quickcheck_macros = "1.0.0" 59 | rstest = { version = "0.12.0", default-features = false } 60 | rstest_reuse = "0.6.0" 61 | rustversion = "1.0.16" 62 | 63 | [features] 64 | default = ["libudev"] 65 | hardware-tests = [] 66 | # TODO: Make the feature unconditionally available with the next major release 67 | # (5.0) and remove this feature gate. 68 | usbportinfo-interface = [] 69 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of 2 | the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 3 | 4 | -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | # macOS / iOS 2 | 3 | Macs use the usual `termios` TTY implementation that other POSIXes use, but support non-standard 4 | baud rates through the `iossiospeed` ioctl (as of Mac OS X 10.4). To support non-standard baud rates 5 | on Mac, there are three main approaches: 6 | 7 | 1. Always use `iossiospeed` 8 | 2. Use `iossiospeed` for non-standard bauds, but `termios` with standard bauds 9 | 3. Use `iossiospeed` always by default and fail-over to the termios approach 10 | 11 | ## Implementation notes 12 | 13 | This library uses the first approach. Given that macOS as far back as 10.4 supports it (2005), there 14 | seem to be no downsides. Internally, baud rates within the `termios` struct are kept at 9600 when 15 | that struct is read & written. This means that anytime the `termios` struct is written back (using 16 | `tcsetattr` a call to `iossiospeed` follows it. Additionally, the `termios` struct is not cached and 17 | instead retrieved on every settings adjustment. While this can increase the number of system calls 18 | when changing port settings, it removes the need to keep state consistent and instead the kernel's 19 | state can always be considered the canonical source. 20 | 21 | ## Platform notes 22 | 23 | `iossiospeed` has no official documentation that can be found by searching 24 | https://developer.apple.com. However 25 | [IOSerialTestLib.c](https://opensource.apple.com/source/IOSerialFamily/IOSerialFamily-93/tests/IOSerialTestLib.c.auto.html) 26 | can be found on Apple's open source code repository and has some example code for using this API. 27 | 28 | Experimentation has shown that there are a few key features to using `iossiospeed`: 29 | 30 | * `iossiospeed` should be called after setting the `termios` struct via `tcsetattr` as that resets 31 | the baud rate and you cannot put custom baud rates in the `termios` struct. 32 | * Calling `iossiospeed` will modify the `termios` struct in the kernel such that you can no longer 33 | round-trip the `termios` struct. The following code will fail: 34 | ```C 35 | struct termios t; 36 | tcgetattr(fd, &t); 37 | tcsetattr(fd, TCSANOW, &t) 38 | ``` 39 | 40 | ## Reference implementations 41 | 42 | * [picocom](https://github.com/npat-efault/picocom) follows the second approach. However they also 43 | cache the existing `termios` struct. 44 | 45 | # Additional References 46 | 47 | * [Understanding UNIX termios VMIN and VTIME](http://unixwiz.net/techtips/termios-vmin-vtime.html) 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![crates.io version badge](https://img.shields.io/crates/v/serialport.svg)](https://crates.io/crates/serialport) 2 | [![Documentation](https://docs.rs/serialport/badge.svg)](https://docs.rs/serialport) 3 | [![GitHub workflow status](https://img.shields.io/github/actions/workflow/status/serialport/serialport-rs/ci.yaml?branch=main&logo=github)](https://github.com/serialport/serialport-rs/actions) 4 | [![Minimum Stable Rust Version](https://img.shields.io/badge/Rust-1.59.0-blue?logo=rust)](https://blog.rust-lang.org/2022/02/24/Rust-1.59.0.html) 5 | 6 | # Introduction 7 | 8 | `serialport-rs` is a general-purpose cross-platform serial port library for Rust. It provides a 9 | blocking I/O interface and port enumeration on POSIX and Windows systems. 10 | 11 | For async I/O functionality, see the [mio-serial](https://github.com/berkowski/mio-serial) and 12 | [tokio-serial](https://github.com/berkowski/tokio-serial) crates. 13 | 14 | Join the discussion on Matrix! 15 | [#serialport-rs:matrix.org](https://matrix.to/#/#serialport-rs:matrix.org) 16 | 17 | **This project is looking for maintainers! Especially for Windows. If you are interested please let 18 | us know on Matrix, or by [creating a 19 | discussion](https://github.com/serialport/serialport-rs/discussions/new).** 20 | 21 | # Overview 22 | 23 | The library exposes cross-platform serial port functionality through the `SerialPort` trait. This 24 | library is structured to make this the simplest API to use to encourage cross-platform development 25 | by default. Working with the resultant `Box` type is therefore recommended. To 26 | expose additional platform-specific functionality use the platform-specific structs directly: 27 | `TTYPort` for POSIX systems and `COMPort` for Windows. 28 | 29 | Serial enumeration is provided on most platforms. The implementation on Linux using `glibc` relies 30 | on `libudev` (unless you disable the default `libudev` feature), an external dynamic library that 31 | will need to be available on the system the final binary is running on. Enumeration will still be 32 | available if this feature is disabled, but won't expose as much information and may return ports 33 | that don't exist physically. However this dependency can be removed by disabling the default 34 | `libudev` feature: 35 | 36 | ```shell 37 | $ cargo build --no-default-features 38 | ``` 39 | 40 | It should also be noted that on macOS, both the Callout (`/dev/cu.*`) and Dial-in ports 41 | (`/dev/tty.*`) ports are enumerated, resulting in two available ports per connected serial device. 42 | 43 | # Usage 44 | 45 | Listing available ports: 46 | 47 | ```rust 48 | let ports = serialport::available_ports().expect("No ports found!"); 49 | for p in ports { 50 | println!("{}", p.port_name); 51 | } 52 | 53 | ``` 54 | 55 | Opening and configuring a port: 56 | 57 | ```rust 58 | let port = serialport::new("/dev/ttyUSB0", 115_200) 59 | .timeout(Duration::from_millis(10)) 60 | .open().expect("Failed to open port"); 61 | ``` 62 | 63 | Writing to a port: 64 | 65 | ```rust 66 | let output = "This is a test. This is only a test.".as_bytes(); 67 | port.write(output).expect("Write failed!"); 68 | ``` 69 | 70 | Reading from a port (default is blocking with a 0ms timeout): 71 | 72 | ```rust 73 | let mut serial_buf: Vec = vec![0; 32]; 74 | port.read(serial_buf.as_mut_slice()).expect("Found no data!"); 75 | ``` 76 | 77 | Some platforms expose additional functionality, which is opened using the `open_native()` method: 78 | 79 | ```rust 80 | let port = serialport::new("/dev/ttyUSB0", 115_200) 81 | .open_native().expect("Failed to open port"); 82 | ``` 83 | 84 | Closing a port: 85 | 86 | `serialport-rs` uses the Resource Acquisition Is Initialization (RAII) paradigm and so closing a 87 | port is done when the `SerialPort` object is `Drop`ed either implicitly or explicitly using 88 | `std::mem::drop` (`std::mem::drop(port)`). 89 | 90 | # Examples 91 | 92 | There are several included examples, which help demonstrate the functionality of this library and 93 | can help debug software or hardware errors. 94 | 95 | - _clear_input_buffer_ - Demonstrates querying and clearing the driver input buffer. 96 | - _clear_output_buffer_ - Demonstrates querying and clearing the driver output buffer. 97 | - _duplex_ - Tests that a port can be successfully cloned. 98 | - _hardware_check_ - Checks port/driver functionality for a single port or a pair of ports connected 99 | to each other. 100 | - _list_ports_ - Lists available serial ports. 101 | - _pseudo_terminal_ - Unix only. Tests that a pseudo-terminal pair can be created. 102 | - _receive_data_ - Output data received on a port. 103 | - _transmit_ - Transmits data regularly on a port with various port configurations. Useful for 104 | debugging. 105 | 106 | # Dependencies 107 | 108 | Rust versions 1.59.0 and higher are supported by the library itself. There are 109 | examples requiring newer versions of Rust. 110 | 111 | For GNU/Linux `pkg-config` headers are required: 112 | 113 | - Ubuntu: `sudo apt install pkg-config` 114 | - Fedora: `sudo dnf install pkgconf-pkg-config` 115 | 116 | For other distros they may provide `pkg-config` through the `pkgconf` package instead. 117 | 118 | For GNU/Linux `libudev` headers are required as well (unless you disable the default `libudev` 119 | feature): 120 | 121 | - Ubuntu: `sudo apt install libudev-dev` 122 | - Fedora: `sudo dnf install systemd-devel` 123 | 124 | # Platform Support 125 | 126 | Builds and some tests (not requiring actual hardware) for major targets are run 127 | in CI. Failures of either block the inclusion of new code. This library should 128 | be compatible with additional targets not listed below, but no guarantees are 129 | made. Additional platforms may be added in the future if there is a need and/or 130 | demand. 131 | 132 | - Android 133 | - `arm-linux-androideabi` (no serial enumeration) 134 | - `armv7-linux-androideabi` (no serial enumeration) 135 | - FreeBSD 136 | - `x86_64-unknown-freebsd` 137 | - Linux 138 | - `aarch64-unknown-linux-gnu` 139 | - `aarch64-unknown-linux-musl` 140 | - `i686-unknown-linux-gnu` 141 | - `i686-unknown-linux-musl` 142 | - `x86_64-unknown-linux-gnu` 143 | - `x86_64-unknown-linux-musl` 144 | - macOS/iOS 145 | - `aarch64-apple-darwin` 146 | - `aarch64-apple-ios` 147 | - `x86_64-apple-darwin` 148 | - NetBSD 149 | - `x86_64-unknown-netbsd` (no serial enumeration) 150 | - Windows 151 | - `i686-pc-windows-gnu` 152 | - `i686-pc-windows-msvc` 153 | - `x86_64-pc-windows-gnu` 154 | - `x86_64-pc-windows-msvc` 155 | 156 | # Hardware Support 157 | 158 | This library has been developed to support all serial port devices across all supported platforms. 159 | To determine how well your platform is supported, please run the `hardware_check` example provided 160 | with this library. It will test the driver to confirm that all possible settings are supported for a 161 | port. Additionally, it will test that data transmission is correct for those settings if you have 162 | two ports physically configured to communicate. If you experience problems with your devices, please 163 | file a bug and identify the hardware, OS, and driver in use. 164 | 165 | Known issues: 166 | 167 | | Hardware | OS | Driver | Issues | 168 | | ------------- | ----- | ----------------------- | ---------------------------------------------------------------------------------- | 169 | | FTDI TTL-232R | Linux | ftdi_sio, Linux 4.14.11 | Hardware doesn't support 5 or 6 data bits, but the driver lies about supporting 5. | 170 | 171 | # Licensing 172 | 173 | Licensed under the [Mozilla Public License, version 2.0](https://www.mozilla.org/en-US/MPL/2.0/). 174 | 175 | # Contributing 176 | 177 | Please open an issue or pull request on GitHub to contribute. Code contributions submitted for 178 | inclusion in the work by you, as defined in the MPL2.0 license, shall be licensed as the above 179 | without any additional terms or conditions. 180 | 181 | # Acknowledgments 182 | 183 | This is the continuation of the development at . Thanks 184 | to susurrus and all other contributors to the original project on GitLab. 185 | 186 | Special thanks to dcuddeback, willem66745, and apoloval who wrote the original serial-rs library 187 | which this library heavily borrows from. 188 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | How to test `serialport-rs` for development. 2 | 3 | Without hardware: 4 | 5 | 1. Compilation 6 | 2. `cargo test` 7 | 8 | With a single unconnected device: 9 | 10 | `cargo run --example hardware_check ` 11 | 12 | And when wired in a physical loopback mode: 13 | 14 | `cargo run --example hardware_check --loopback` 15 | 16 | With two devices connected to each other: 17 | 18 | * `cargo run --example hardware_check --loopback-port ` 19 | * Also `cargo run --example heartbeat ` in one terminal and 20 | `cargo run --example receive_data ` in another 21 | * Running tests with test cases requiring hardware devices enabled: 22 | ``` 23 | $ export SERIALPORT_TEST_PORT_1=$(realpath /dev/ttyX) 24 | $ export SERIALPORT_TEST_PORT_2=$(realpath /dev/ttyY) 25 | $ cargo test --features hardware-tests 26 | ``` 27 | 28 | Can also verify trickier settings (like non-standard baud rates) using serial terminal programs 29 | like: 30 | 31 | * `screen` (POSIX) 32 | * [CoolTerm](http://freeware.the-meiers.org/) (macOS) 33 | * [RealTerm](https://sourceforge.net/projects/realterm/) (Windows) 34 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # Note that all fields that take a lint level have these possible values: 2 | # * deny - An error will be produced and the check will fail 3 | # * warn - A warning will be produced, but the check will not fail 4 | # * allow - No warning or error will be produced, though in some cases a note 5 | # will be 6 | 7 | [graph] 8 | # If 1 or more target triples (and optionally, target_features) are specified, 9 | # only the specified targets will be checked when running `cargo deny check`. 10 | # This means, if a particular package is only ever used as a target specific 11 | # dependency, such as, for example, the `nix` crate only being used via the 12 | # `target_family = "unix"` configuration, that only having windows targets in 13 | # this list would mean the nix crate, as well as any of its exclusive 14 | # dependencies not shared by any other crates, would be ignored, as the target 15 | # list here is effectively saying which targets you are building for. 16 | targets = [ 17 | { triple = "aarch64-apple-darwin" }, 18 | { triple = "aarch64-apple-ios" }, 19 | { triple = "aarch64-unknown-linux-gnu" }, 20 | { triple = "aarch64-unknown-linux-musl" }, 21 | { triple = "arm-linux-androideabi" }, 22 | { triple = "armv7-linux-androideabi" }, 23 | { triple = "i686-pc-windows-gnu" }, 24 | { triple = "i686-pc-windows-msvc" }, 25 | { triple = "i686-unknown-linux-gnu" }, 26 | { triple = "i686-unknown-linux-musl" }, 27 | { triple = "x86_64-apple-darwin" }, 28 | { triple = "x86_64-pc-windows-gnu" }, 29 | { triple = "x86_64-pc-windows-msvc" }, 30 | { triple = "x86_64-unknown-linux-gnu" }, 31 | { triple = "x86_64-unknown-linux-musl" }, 32 | { triple = "x86_64-unknown-netbsd" }, 33 | ] 34 | 35 | # This section is considered when running `cargo deny check advisories` 36 | # More documentation for the advisories section can be found here: 37 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 38 | [advisories] 39 | # Selects the default behavior for checking advisories. 40 | version = 2 41 | # The path where the advisory database is cloned/fetched into 42 | db-path = "~/.cargo/advisory-db" 43 | # The url(s) of the advisory databases to use 44 | db-urls = ["https://github.com/rustsec/advisory-db"] 45 | # The lint level for crates that have been yanked from their source registry 46 | yanked = "deny" 47 | # A list of advisory IDs to ignore. Note that ignored advisories will still 48 | # output a note when they are encountered. 49 | ignore = [ 50 | "RUSTSEC-2021-0145", # caused by unmaintained atty 51 | "RUSTSEC-2024-0370", # caused by unmaintained proc-macro-error used by some examples 52 | "RUSTSEC-2024-0375", # caused by umnaintained atty (again, with migration hint) 53 | ] 54 | # Threshold for security vulnerabilities, any vulnerability with a CVSS score 55 | # lower than the range specified will be ignored. Note that ignored advisories 56 | # will still output a note when they are encountered. 57 | # * None - CVSS Score 0.0 58 | # * Low - CVSS Score 0.1 - 3.9 59 | # * Medium - CVSS Score 4.0 - 6.9 60 | # * High - CVSS Score 7.0 - 8.9 61 | # * Critical - CVSS Score 9.0 - 10.0 62 | #severity-threshold = 63 | 64 | # If this is true, then cargo deny will use the git executable to fetch advisory database. 65 | # If this is false, then it uses a built-in git library. 66 | # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. 67 | # See Git Authentication for more information about setting up git authentication. 68 | #git-fetch-with-cli = true 69 | 70 | # This section is considered when running `cargo deny check licenses` 71 | # More documentation for the licenses section can be found here: 72 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 73 | [licenses] 74 | # Selects the default behavior for checking licenses. 75 | version = 2 76 | # List of explicitly allowed licenses 77 | # See https://spdx.org/licenses/ for list of possible licenses 78 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 79 | allow = [ 80 | "Apache-2.0", 81 | "BSD-2-Clause", 82 | "MIT", 83 | "MPL-2.0", 84 | "Unicode-3.0", 85 | "Unicode-DFS-2016", 86 | ] 87 | # The confidence threshold for detecting a license from license text. 88 | # The higher the value, the more closely the license text must be to the 89 | # canonical license text of a valid SPDX license file. 90 | # [possible values: any between 0.0 and 1.0]. 91 | confidence-threshold = 0.8 92 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 93 | # aren't accepted for every possible crate as with the normal allow list 94 | exceptions = [ 95 | # Each entry is the crate and version constraint, and its specific allow 96 | # list 97 | #{ allow = ["Zlib"], name = "adler32", version = "*" }, 98 | ] 99 | 100 | # Some crates don't have (easily) machine readable licensing information, 101 | # adding a clarification entry for it allows you to manually specify the 102 | # licensing information 103 | #[[licenses.clarify]] 104 | # The name of the crate the clarification applies to 105 | #name = "ring" 106 | # The optional version constraint for the crate 107 | #version = "*" 108 | # The SPDX expression for the license requirements of the crate 109 | #expression = "MIT AND ISC AND OpenSSL" 110 | # One or more files in the crate's source used as the "source of truth" for 111 | # the license expression. If the contents match, the clarification will be used 112 | # when running the license check, otherwise the clarification will be ignored 113 | # and the crate will be checked normally, which may produce warnings or errors 114 | # depending on the rest of your configuration 115 | #license-files = [ 116 | # Each entry is a crate relative path, and the (opaque) hash of its contents 117 | #{ path = "LICENSE", hash = 0xbd0eed23 } 118 | #] 119 | 120 | [licenses.private] 121 | # If true, ignores workspace crates that aren't published, or are only 122 | # published to private registries. 123 | # To see how to mark a crate as unpublished (to the official registry), 124 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. 125 | ignore = false 126 | # One or more private registries that you might publish crates to, if a crate 127 | # is only published to private registries, and ignore is true, the crate will 128 | # not have its license(s) checked 129 | registries = [ 130 | #"https://sekretz.com/registry 131 | ] 132 | 133 | # This section is considered when running `cargo deny check bans`. 134 | # More documentation about the 'bans' section can be found here: 135 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 136 | [bans] 137 | # Lint level for when multiple versions of the same crate are detected 138 | multiple-versions = "deny" 139 | # Lint level for when a crate version requirement is `*` 140 | wildcards = "deny" 141 | # The graph highlighting used when creating dotgraphs for crates 142 | # with multiple versions 143 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 144 | # * simplest-path - The path to the version with the fewest edges is highlighted 145 | # * all - Both lowest-version and simplest-path are used 146 | highlight = "all" 147 | # List of crates that are allowed. Use with care! 148 | allow = [ 149 | ] 150 | # List of crates to deny 151 | deny = [ 152 | ] 153 | # Certain crates/versions that will be skipped when doing duplicate detection. 154 | skip = [ 155 | ] 156 | # Similarly to `skip` allows you to skip certain crates during duplicate 157 | # detection. Unlike skip, it also includes the entire tree of transitive 158 | # dependencies starting at the specified crate, up to a certain depth, which is 159 | # by default infinite 160 | skip-tree = [ 161 | { name = "clap", version = "~3.2" }, # https://github.com/serialport/serialport-rs/pull/76 162 | ] 163 | 164 | # This section is considered when running `cargo deny check sources`. 165 | # More documentation about the 'sources' section can be found here: 166 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 167 | [sources] 168 | # Lint level for what to happen when a crate from a crate registry that is not 169 | # in the allow list is encountered 170 | unknown-registry = "deny" 171 | # Lint level for what to happen when a crate from a git repository that is not 172 | # in the allow list is encountered 173 | unknown-git = "deny" 174 | # List of URLs for allowed crate registries. Defaults to the crates.io index 175 | # if not specified. If it is specified but empty, no registries are allowed. 176 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 177 | # List of URLs for allowed Git repositories 178 | allow-git = [] 179 | 180 | [sources.allow-org] 181 | # 1 or more github.com organizations to allow git sources for 182 | #github = [""] 183 | # 1 or more gitlab.com organizations to allow git sources for 184 | #gitlab = [""] 185 | # 1 or more bitbucket.org organizations to allow git sources for 186 | #bitbucket = [""] 187 | -------------------------------------------------------------------------------- /doc/dev_notes.md: -------------------------------------------------------------------------------- 1 | # Developer Notes 2 | 3 | This is a collection of additional documentation about the design decisions made in the development of `serialport-rs`. -------------------------------------------------------------------------------- /doc/index.md: -------------------------------------------------------------------------------- 1 | # serialport-rs 2 | 3 | Serial ports are some of the oldest external interfaces exposed by system kernels and their interfaces are quite clumsy. Additionally there are plenty of caveats in each platform's interface that we attempt to hide through the safe cross-platform interface. Many details of these interfaces are not well documented and so the following resources are an attempt to remedy that situation. They also serve as helpful developer documentation for `serialport-rs`. 4 | 5 | ## Resources 6 | 7 | [Platform API overview](./platforms.md) 8 | 9 | [Developer notes](./dev_notes.md) 10 | 11 | ## References 12 | 13 | * 14 | * 15 | * 16 | * 17 | * 18 | * 19 | -------------------------------------------------------------------------------- /doc/platforms.md: -------------------------------------------------------------------------------- 1 | # Platform API Overview 2 | 3 | The three primary platforms have considerable differences in even their basic blocking APIs that are worth outlining in more detail. Many aspects of the cross-platform API came about because of these differences. 4 | 5 | ## Windows 6 | 7 | Surprisingly enough, Windows has the most sane out of all of the interfaces. There is a singular `DCB` struct that contains all configuration necessary for the port. Once a `DCB` struct is created and configured, it's quite easy to configure the port and call `SetCommState()`. Note that this struct supports arbitrary baud rates by default. 8 | 9 | The `DCB` struct for a given `HANDLE` can also be retrieved from the `GetCommState()` function. 10 | 11 | ## Linux & Android 12 | 13 | Linux and Android provide both the Termios and Termios2 APIs. The Termios API allows for all configuration necessary, but does not support arbitrary baud rates. In this API the speed members of the struct are not accessible and the `cfsetXspeed()` functions must be used to configure them. And the only appropriate values are the `B*` constants. 14 | 15 | The Termios2 API, on the other hand, supports arbitrary baud rates. Instead of using `cfsetXspeed` and the `B*` constants, you can modify the `c_ispeed` and `c_ospeed` fields of the `termios2` struct directly. 16 | 17 | ## The BSDs (DragonFlyBSD, FreeBSD, NetBSD, OpenBSD) 18 | 19 | The BSDs basically **only** have the Termios2 API, but they call it Termios. It supports arbitrary baud rates out of the gate as the `termios2.c_ispeed` and `termios2.c_ospeed` fields are directly settable to the desired baud rate. 20 | 21 | ### FreeBSD 22 | 23 | * https://docs.freebsd.org/en/books/handbook/serialcomms/#serial 24 | 25 | ### NetBSD 26 | 27 | * https://man.netbsd.org/tty.4 28 | * https://man.netbsd.org/com.4 29 | * https://www.netbsd.org/docs/Hardware/Misc/serial.html 30 | * https://www.netbsd.org/ports/hp300/faq.html 31 | 32 | ### OpenBSD 33 | 34 | * https://man.openbsd.org/tty.4 35 | 36 | ## macOS and iOS 37 | 38 | While macOS and iOS have the heritage of a BSD, their support is slightly different. In theory, they support arbitrary baud rates in their Termios API much like the BSDs, but in practice this doesn't work with many hardware devices, as it's dependent on driver support. Instead, Apple added the `IOSSIOSPEED` ioctl in Mac OS X 10.4, which can set the baud rate to an arbitrary value. As the oldest macOS version supported by Rust is 10.7, it's available on all Mac platforms. 39 | 40 | This API requires the port to be set into raw mode with `cfmakeraw`, and must be done after every call to `tcsetattr`, as that will reset the baud rate. Additionally, there is no way to retrieve the actual baud rate from the OS. This is therefore the clunkiest API of any platform. 41 | -------------------------------------------------------------------------------- /examples/clear_input_buffer.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Provides a way to test clearing and querying the size of the serial input buffer. 3 | // 4 | // USAGE: 5 | // 6 | // 1. Connect a serial device to your host computer. E.g. an Arduino loaded with the example sketch 7 | // given below 8 | // 2. Run this example 9 | // 3. Observe the output - it reports how many bytes have been received from the device and are 10 | // waiting to be read 11 | // 4. Press the Return key to make the example clear the input buffer. You should see the number of 12 | // bytes queued to read momentarily drop to 0 13 | // 5. Press Ctrl+D (Unix) or Ctrl+Z (Win) to quit 14 | // 15 | // The following Arduino firmware could be used to generate input: 16 | // 17 | // ``` 18 | // #include 19 | // 20 | // void setup() { 21 | // Serial.begin(9600); 22 | // while (!Serial); // wait for serial port to connect. Needed for native USB 23 | // } 24 | // 25 | // int iter = 0; 26 | // 27 | // void loop() { 28 | // Serial.print(iter); 29 | // if (++iter == 10) { 30 | // Serial.println(); 31 | // iter = 0; 32 | // } 33 | // delay(1000 / 20); 34 | // } 35 | // ``` 36 | 37 | use std::error::Error; 38 | use std::io::{self, Read}; 39 | use std::panic::panic_any; 40 | use std::sync::mpsc; 41 | use std::thread; 42 | use std::time::Duration; 43 | 44 | use clap::{Arg, Command}; 45 | 46 | use serialport::ClearBuffer; 47 | 48 | fn main() { 49 | let matches = Command::new("Serialport Example - Clear Input Buffer") 50 | .about("Reports how many bytes are waiting to be read and allows the user to clear the input buffer") 51 | .disable_version_flag(true) 52 | .arg(Arg::new("port") 53 | .help("The device path to a serial port") 54 | .use_value_delimiter(false) 55 | .required(true)) 56 | .arg(Arg::new("baud") 57 | .help("The baud rate to connect at") 58 | .use_value_delimiter(false) 59 | .required(true)) 60 | .get_matches(); 61 | 62 | let port_name = matches.value_of("port").unwrap(); 63 | let baud_rate = matches.value_of("baud").unwrap(); 64 | 65 | let exit_code = match run(port_name, baud_rate) { 66 | Ok(_) => 0, 67 | Err(e) => { 68 | println!("Error: {}", e); 69 | 1 70 | } 71 | }; 72 | 73 | std::process::exit(exit_code); 74 | } 75 | 76 | fn run(port_name: &str, baud_rate: &str) -> Result<(), Box> { 77 | let rate = baud_rate 78 | .parse::() 79 | .map_err(|_| format!("Invalid baud rate '{}' specified", baud_rate))?; 80 | 81 | let port = serialport::new(port_name, rate) 82 | .timeout(Duration::from_millis(10)) 83 | .open() 84 | .map_err(|ref e| format!("Port '{}' not available: {}", &port_name, e))?; 85 | 86 | let chan_clear_buf = input_service(); 87 | 88 | println!("Connected to {} at {} baud", &port_name, &baud_rate); 89 | println!("Ctrl+D (Unix) or Ctrl+Z (Win) to stop. Press Return to clear the buffer."); 90 | 91 | loop { 92 | println!( 93 | "Bytes available to read: {}", 94 | port.bytes_to_read().expect("Error calling bytes_to_read") 95 | ); 96 | 97 | match chan_clear_buf.try_recv() { 98 | Ok(_) => { 99 | println!("------------------------- Discarding buffer ------------------------- "); 100 | port.clear(ClearBuffer::Input) 101 | .expect("Failed to discard input buffer") 102 | } 103 | Err(mpsc::TryRecvError::Empty) => (), 104 | Err(mpsc::TryRecvError::Disconnected) => { 105 | println!("Stopping."); 106 | break; 107 | } 108 | } 109 | 110 | thread::sleep(Duration::from_millis(100)); 111 | } 112 | 113 | Ok(()) 114 | } 115 | 116 | fn input_service() -> mpsc::Receiver<()> { 117 | let (tx, rx) = mpsc::channel(); 118 | 119 | thread::spawn(move || { 120 | let mut buffer = [0; 32]; 121 | loop { 122 | // Block awaiting any user input 123 | match io::stdin().read(&mut buffer) { 124 | Ok(0) => { 125 | drop(tx); // EOF, drop the channel and stop the thread 126 | break; 127 | } 128 | Ok(_bytes_read) => tx.send(()).unwrap(), // Signal main to clear the buffer 129 | Err(e) => panic_any(e), 130 | } 131 | } 132 | }); 133 | 134 | rx 135 | } 136 | -------------------------------------------------------------------------------- /examples/clear_output_buffer.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Provides a way to test clearing and querying the size of the serial output buffer. 3 | // 4 | // USAGE: 5 | // 6 | // 1. Connect a serial device to your host computer. E.g. an Arduino could be used. It will be able 7 | // to receive data without any specific sketch loaded. 8 | // 2. Run this example 9 | // 3. Observe the output - it reports how many bytes are waiting to be sent to the connected device 10 | // 4. Press the Return key to make the example clear the output buffer. You should see the number 11 | // of bytes queued to send momentarily drop to 0 12 | // 5. Try passing different values for the buffer-size argument to see how that affects the speed 13 | // and saturation point of the output buffer 14 | // 6. Press Ctrl+D (Unix) or Ctrl+Z (Win) to quit 15 | // 16 | 17 | use std::error::Error; 18 | use std::io::{self, Read}; 19 | use std::panic::panic_any; 20 | use std::sync::mpsc; 21 | use std::thread; 22 | use std::time::Duration; 23 | 24 | use clap::{Arg, ArgMatches, Command}; 25 | 26 | use serialport::ClearBuffer; 27 | 28 | const DEFAULT_BLOCK_SIZE: &str = "128"; 29 | 30 | fn main() { 31 | let block_size_help = format!( 32 | "The size in bytes of the block of data to write to the port (default: {} bytes)", 33 | DEFAULT_BLOCK_SIZE 34 | ); 35 | 36 | let matches = Command::new("Serialport Example - Clear Output Buffer") 37 | .about("Reports how many bytes are waiting to be read and allows the user to clear the output buffer") 38 | .disable_version_flag(true) 39 | .arg(Arg::new("port") 40 | .help("The device path to a serial port") 41 | .use_value_delimiter(false) 42 | .required(true)) 43 | .arg(Arg::new("baud") 44 | .help("The baud rate to connect at") 45 | .use_value_delimiter(false) 46 | .required(true)) 47 | .arg(Arg::new("block-size") 48 | .help(Some(block_size_help.as_str())) 49 | .use_value_delimiter(false) 50 | .default_value(DEFAULT_BLOCK_SIZE)) 51 | .get_matches(); 52 | 53 | let port_name = matches.value_of("port").unwrap(); 54 | let baud_rate = matches.value_of("baud").unwrap(); 55 | let block_size = ArgMatches::value_of_t(&matches, "block-size").unwrap_or_else(|e| e.exit()); 56 | 57 | let exit_code = match run(port_name, baud_rate, block_size) { 58 | Ok(_) => 0, 59 | Err(e) => { 60 | println!("Error: {}", e); 61 | 1 62 | } 63 | }; 64 | 65 | std::process::exit(exit_code); 66 | } 67 | 68 | fn run(port_name: &str, baud_rate: &str, block_size: usize) -> Result<(), Box> { 69 | let rate = baud_rate 70 | .parse::() 71 | .map_err(|_| format!("Invalid baud rate '{}' specified", baud_rate))?; 72 | 73 | let mut port = serialport::new(port_name, rate) 74 | .timeout(Duration::from_millis(10)) 75 | .open() 76 | .map_err(|ref e| format!("Port '{}' not available: {}", &port_name, e))?; 77 | 78 | let chan_clear_buf = input_service(); 79 | 80 | println!("Connected to {} at {} baud", &port_name, &baud_rate); 81 | println!("Ctrl+D (Unix) or Ctrl+Z (Win) to stop. Press Return to clear the buffer."); 82 | 83 | let block = vec![0; block_size]; 84 | 85 | // This loop writes the block repeatedly, as fast as possible, to try to saturate the 86 | // output buffer. If you don't see much data queued to send, try changing the block size. 87 | loop { 88 | match port.write_all(&block) { 89 | Ok(_) => (), 90 | Err(ref e) if e.kind() == io::ErrorKind::TimedOut => (), 91 | Err(e) => panic!("Error while writing data to the port: {}", e), 92 | }; 93 | 94 | match chan_clear_buf.try_recv() { 95 | Ok(_) => { 96 | println!("------------------------- Discarding buffer ------------------------- "); 97 | port.clear(ClearBuffer::Output) 98 | .expect("Failed to discard output buffer") 99 | } 100 | Err(mpsc::TryRecvError::Empty) => (), 101 | Err(mpsc::TryRecvError::Disconnected) => { 102 | println!("Stopping."); 103 | break; 104 | } 105 | } 106 | 107 | println!( 108 | "Bytes queued to send: {}", 109 | port.bytes_to_write().expect("Error calling bytes_to_write") 110 | ); 111 | } 112 | 113 | Ok(()) 114 | } 115 | 116 | fn input_service() -> mpsc::Receiver<()> { 117 | let (tx, rx) = mpsc::channel(); 118 | 119 | thread::spawn(move || { 120 | let mut buffer = [0; 32]; 121 | loop { 122 | // Block awaiting any user input 123 | match io::stdin().read(&mut buffer) { 124 | Ok(0) => { 125 | drop(tx); // EOF, drop the channel and stop the thread 126 | break; 127 | } 128 | Ok(_bytes_read) => tx.send(()).unwrap(), // Signal main to clear the buffer 129 | Err(e) => panic_any(e), 130 | } 131 | } 132 | }); 133 | 134 | rx 135 | } 136 | -------------------------------------------------------------------------------- /examples/duplex.rs: -------------------------------------------------------------------------------- 1 | //! Duplex example 2 | //! 3 | //! This example tests the ability to clone a serial port. It works by creating 4 | //! a new file descriptor, and therefore a new `SerialPort` object that's safe 5 | //! to send to a new thread. 6 | //! 7 | //! This example selects the first port on the system, clones the port into a child 8 | //! thread that writes data to the port every second. While this is running the parent 9 | //! thread continually reads from the port. 10 | //! 11 | //! To test this, have a physical or virtual loopback device connected as the 12 | //! only port in the system. 13 | 14 | use std::io::Write; 15 | use std::time::Duration; 16 | use std::{io, thread}; 17 | 18 | fn main() { 19 | // Open the first serialport available. 20 | let port_name = &serialport::available_ports().expect("No serial port")[0].port_name; 21 | let mut port = serialport::new(port_name, 9600) 22 | .open() 23 | .expect("Failed to open serial port"); 24 | 25 | // Clone the port 26 | let mut clone = port.try_clone().expect("Failed to clone"); 27 | 28 | // Send out 4 bytes every second 29 | thread::spawn(move || loop { 30 | clone 31 | .write_all(&[5, 6, 7, 8]) 32 | .expect("Failed to write to serial port"); 33 | thread::sleep(Duration::from_millis(1000)); 34 | }); 35 | 36 | // Read the four bytes back from the cloned port 37 | let mut buffer: [u8; 1] = [0; 1]; 38 | loop { 39 | match port.read(&mut buffer) { 40 | Ok(bytes) => { 41 | if bytes == 1 { 42 | println!("Received: {:?}", buffer); 43 | } 44 | } 45 | Err(ref e) if e.kind() == io::ErrorKind::TimedOut => (), 46 | Err(e) => eprintln!("{:?}", e), 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/hardware_check.rs: -------------------------------------------------------------------------------- 1 | //! This example performs a test using real hardware ports. 2 | //! 3 | //! This tool serves as a debugging aid when encountering errors using `serialport-rs`. It should 4 | //! expose any kernel or driver bugs that your system may have by running physical ports through 5 | //! many configurations. 6 | //! 7 | //! There are 3 ways to run this example: 8 | //! 9 | //! 1) With a single port not connected to an external device: 10 | //! `cargo run --example hardware_check /dev/ttyUSB0` 11 | //! 12 | //! 2) With a single port physically connected in loopback mode (RX<->TX) 13 | //! `cargo run --example hardware_check /dev/ttyUSB0 --loopback` 14 | //! 15 | //! 3) With two ports physically connected to each other 16 | //! `cargo run --example hardware_check /dev/ttyUSB0 /dev/ttyUSB1` 17 | 18 | use std::io::Write; 19 | use std::str; 20 | use std::time::Duration; 21 | 22 | use assert_hex::assert_eq_hex; 23 | use clap::{Arg, Command}; 24 | 25 | use serialport::{ClearBuffer, DataBits, FlowControl, Parity, SerialPort, StopBits}; 26 | 27 | const TEST_MESSAGE: &[u8] = "Test Message".as_bytes(); 28 | 29 | fn main() { 30 | let matches = Command::new("Serialport Example - Hardware Check") 31 | .about("Test hardware capabilities of serial ports") 32 | .disable_version_flag(true) 33 | .arg(Arg::new("port") 34 | .help("The device path to a serial port") 35 | .use_value_delimiter(false) 36 | .required(true)) 37 | .arg(Arg::new("loopback") 38 | .help("Run extra tests if the port is configured for hardware loopback. Mutually exclusive with the --loopback-port option") 39 | .use_value_delimiter(false) 40 | .conflicts_with("loopback-port") 41 | .long("loopback")) 42 | .arg(Arg::new("loopback-port") 43 | .help("The device path of a second serial port that is connected to the first serial port. Mutually exclusive with the --loopback option.") 44 | .use_value_delimiter(false) 45 | .takes_value(true) 46 | .long("loopback-port")) 47 | .get_matches(); 48 | 49 | let port1_name = matches.value_of("port").unwrap(); 50 | let port2_name = matches.value_of("loopback-port").unwrap_or(""); 51 | let port1_loopback = matches.is_present("loopback"); 52 | 53 | // Loopback mode is only available when a single port is specified 54 | if port1_loopback && !port2_name.is_empty() { 55 | eprintln!("ERROR: loopback mode can only be enabled when a single port is specified."); 56 | ::std::process::exit(1); 57 | } 58 | 59 | // Run single-port tests on port1 60 | let mut port1 = match serialport::new(port1_name, 9600).open() { 61 | Err(e) => { 62 | eprintln!("Failed to open \"{}\". Error: {}", port1_name, e); 63 | ::std::process::exit(1); 64 | } 65 | Ok(p) => p, 66 | }; 67 | test_single_port(&mut *port1, port1_loopback); 68 | 69 | if !port2_name.is_empty() { 70 | // Run single-port tests on port2 71 | let mut port2 = match serialport::new(port2_name, 9600).open() { 72 | Err(e) => { 73 | eprintln!("Failed to open \"{}\". Error: {}", port2_name, e); 74 | ::std::process::exit(1); 75 | } 76 | Ok(p) => p, 77 | }; 78 | test_single_port(&mut *port2, false); 79 | 80 | // Test loopback pair 81 | test_dual_ports(&mut *port1, &mut *port2); 82 | } 83 | } 84 | 85 | macro_rules! baud_rate_check { 86 | ($port:ident, $baud:expr) => { 87 | let baud_rate = $baud; 88 | if let Err(e) = $port.set_baud_rate(baud_rate) { 89 | println!(" {:?}: FAILED ({})", baud_rate, e); 90 | } 91 | match $port.baud_rate() { 92 | Err(_) => println!(" {:?}: FAILED (error retrieving baud rate)", baud_rate), 93 | Ok(r) if r != baud_rate => println!( 94 | " {:?}: FAILED (baud rate {:?} does not match set baud rate {:?})", 95 | baud_rate, r, baud_rate 96 | ), 97 | Ok(_) => println!(" {:?}: success", baud_rate), 98 | } 99 | }; 100 | } 101 | 102 | macro_rules! data_bits_check { 103 | ($port:ident, $data_bits:path) => { 104 | let data_bits = $data_bits; 105 | if let Err(e) = $port.set_data_bits(data_bits) { 106 | println!(" {:?}: FAILED ({})", data_bits, e); 107 | } else { 108 | match $port.data_bits() { 109 | Err(_) => println!("FAILED to retrieve data bits"), 110 | Ok(r) if r != data_bits => println!( 111 | " {:?}: FAILED (data bits {:?} does not match set data bits {:?})", 112 | data_bits, r, data_bits 113 | ), 114 | Ok(_) => println!(" {:?}: success", data_bits), 115 | } 116 | } 117 | }; 118 | } 119 | 120 | macro_rules! flow_control_check { 121 | ($port:ident, $flow_control:path) => { 122 | let flow_control = $flow_control; 123 | if let Err(e) = $port.set_flow_control(flow_control) { 124 | println!(" {:?}: FAILED ({})", flow_control, e); 125 | } else { 126 | match $port.flow_control() { 127 | Err(_) => println!("FAILED to retrieve flow control"), 128 | Ok(r) if r != flow_control => println!( 129 | " {:?}: FAILED (flow control {:?} does not match set flow control {:?})", 130 | flow_control, r, flow_control 131 | ), 132 | Ok(_) => println!(" {:?}: success", flow_control), 133 | } 134 | } 135 | }; 136 | } 137 | 138 | macro_rules! parity_check { 139 | ($port:ident, $parity:path) => { 140 | let parity = $parity; 141 | if let Err(e) = $port.set_parity(parity) { 142 | println!(" {:?}: FAILED ({})", parity, e); 143 | } else { 144 | match $port.parity() { 145 | Err(_) => println!("FAILED to retrieve parity"), 146 | Ok(r) if r != parity => println!( 147 | " {:?}: FAILED (parity {:?} does not match set parity {:?})", 148 | parity, r, parity 149 | ), 150 | Ok(_) => println!(" {:?}: success", parity), 151 | } 152 | } 153 | }; 154 | } 155 | 156 | macro_rules! stop_bits_check { 157 | ($port:ident, $stop_bits:path) => { 158 | let stop_bits = $stop_bits; 159 | if let Err(e) = $port.set_stop_bits(stop_bits) { 160 | println!(" {:?}: FAILED ({})", stop_bits, e); 161 | } else { 162 | match $port.stop_bits() { 163 | Err(_) => println!("FAILED to retrieve stop bits"), 164 | Ok(r) if r != stop_bits => println!( 165 | "FAILED, stop bits {:?} does not match set stop bits {:?}", 166 | r, stop_bits 167 | ), 168 | Ok(_) => println!(" {:?}: success", stop_bits), 169 | } 170 | } 171 | }; 172 | } 173 | 174 | macro_rules! clear_check { 175 | ($port:ident, $buffer_direction:path) => { 176 | let buffer_direction = $buffer_direction; 177 | match $port.clear(buffer_direction) { 178 | Ok(_) => println!(" {:?}: success", buffer_direction), 179 | Err(ref e) => println!(" {:?}: FAILED ({})", buffer_direction, e), 180 | } 181 | }; 182 | } 183 | 184 | macro_rules! call_query_method_check { 185 | ($port:ident, $func:path) => { 186 | match $func($port) { 187 | Ok(_) => println!(" {}: success", stringify!($func)), 188 | Err(ref e) => println!(" {}: FAILED ({})", stringify!($func), e), 189 | } 190 | }; 191 | } 192 | 193 | fn test_single_port(port: &mut dyn serialport::SerialPort, loopback: bool) { 194 | println!("Testing '{}':", port.name().unwrap()); 195 | 196 | // Test setting standard baud rates 197 | println!("Testing baud rates..."); 198 | baud_rate_check!(port, 9600); 199 | baud_rate_check!(port, 38_400); 200 | baud_rate_check!(port, 115_200); 201 | 202 | // Test setting non-standard baud rates 203 | println!("Testing non-standard baud rates..."); 204 | baud_rate_check!(port, 10_000); 205 | baud_rate_check!(port, 600_000); 206 | baud_rate_check!(port, 1_800_000); 207 | 208 | // Test setting the data bits 209 | println!("Testing data bits..."); 210 | data_bits_check!(port, DataBits::Five); 211 | data_bits_check!(port, DataBits::Six); 212 | data_bits_check!(port, DataBits::Seven); 213 | data_bits_check!(port, DataBits::Eight); 214 | 215 | // Test setting flow control 216 | println!("Testing flow control..."); 217 | flow_control_check!(port, FlowControl::Software); 218 | flow_control_check!(port, FlowControl::Hardware); 219 | flow_control_check!(port, FlowControl::None); 220 | 221 | // Test setting parity 222 | println!("Testing parity..."); 223 | parity_check!(port, Parity::Odd); 224 | parity_check!(port, Parity::Even); 225 | parity_check!(port, Parity::None); 226 | 227 | // Test setting stop bits 228 | println!("Testing stop bits..."); 229 | stop_bits_check!(port, StopBits::Two); 230 | stop_bits_check!(port, StopBits::One); 231 | 232 | // Test bytes to read and write 233 | println!("Testing bytes to read and write..."); 234 | call_query_method_check!(port, SerialPort::bytes_to_write); 235 | call_query_method_check!(port, SerialPort::bytes_to_read); 236 | 237 | // Test clearing a buffer 238 | println!("Test clearing software buffers..."); 239 | clear_check!(port, ClearBuffer::Input); 240 | clear_check!(port, ClearBuffer::Output); 241 | clear_check!(port, ClearBuffer::All); 242 | 243 | // Test transmitting data 244 | print!("Testing data transmission..."); 245 | std::io::stdout().flush().unwrap(); 246 | // Make sure the port has sane defaults 247 | set_defaults(port); 248 | let msg = "Test Message"; 249 | port.write_all(msg.as_bytes()) 250 | .expect("Unable to write bytes."); 251 | println!("success"); 252 | 253 | if loopback { 254 | print!("Testing data reception..."); 255 | port.set_timeout(Duration::from_millis(250)).ok(); 256 | 257 | let mut buf = [0u8; 12]; 258 | if let Err(e) = port.read_exact(&mut buf) { 259 | println!("FAILED ({})", e); 260 | } else { 261 | assert_eq!( 262 | str::from_utf8(&buf).unwrap(), 263 | msg, 264 | "Received message does not match sent" 265 | ); 266 | println!("success"); 267 | } 268 | } 269 | } 270 | 271 | fn check_test_message(sender: &mut dyn SerialPort, receiver: &mut dyn SerialPort) { 272 | // Ignore any "residue" from previous tests. 273 | sender.clear(ClearBuffer::All).unwrap(); 274 | receiver.clear(ClearBuffer::All).unwrap(); 275 | 276 | let send_buf = TEST_MESSAGE; 277 | let mut recv_buf = [0u8; TEST_MESSAGE.len()]; 278 | 279 | sender.write_all(send_buf).unwrap(); 280 | sender.flush().unwrap(); 281 | 282 | match receiver.read_exact(&mut recv_buf) { 283 | Ok(()) => { 284 | assert_eq_hex!(recv_buf, send_buf, "Received message does not match sent",); 285 | println!(" success"); 286 | } 287 | Err(e) => println!("FAILED: {:?}", e), 288 | } 289 | } 290 | 291 | fn test_dual_ports(port1: &mut dyn serialport::SerialPort, port2: &mut dyn serialport::SerialPort) { 292 | println!( 293 | "Testing paired ports '{}' and '{}':", 294 | port1.name().unwrap(), 295 | port2.name().unwrap() 296 | ); 297 | 298 | // Make sure both ports are set to sane defaults 299 | set_defaults(port1); 300 | set_defaults(port2); 301 | 302 | // Test sending strings from port1 to port2 303 | println!( 304 | " Transmitting from {} to {}...", 305 | port1.name().unwrap(), 306 | port2.name().unwrap() 307 | ); 308 | 309 | let baud_rate = 2_000_000; 310 | println!(" At {},8,n,1,noflow...", baud_rate); 311 | std::io::stdout().flush().unwrap(); 312 | if port1.set_baud_rate(baud_rate).is_ok() && port2.set_baud_rate(baud_rate).is_ok() { 313 | check_test_message(port1, port2); 314 | check_test_message(port2, port1); 315 | } else { 316 | println!("FAILED (does this platform & port support arbitrary baud rates?)"); 317 | } 318 | 319 | let baud_rate = 115_200; 320 | println!(" At {},8,n,1,noflow...", baud_rate); 321 | std::io::stdout().flush().unwrap(); 322 | if port1.set_baud_rate(baud_rate).is_ok() && port2.set_baud_rate(baud_rate).is_ok() { 323 | check_test_message(port1, port2); 324 | check_test_message(port2, port1); 325 | } else { 326 | println!("FAILED"); 327 | } 328 | 329 | let baud_rate = 57_600; 330 | println!(" At {},8,n,1,noflow...", baud_rate); 331 | std::io::stdout().flush().unwrap(); 332 | if port1.set_baud_rate(baud_rate).is_ok() && port2.set_baud_rate(baud_rate).is_ok() { 333 | check_test_message(port1, port2); 334 | check_test_message(port2, port1); 335 | } else { 336 | println!("FAILED"); 337 | } 338 | 339 | let baud_rate = 10_000; 340 | println!(" At {},8,n,1,noflow...", baud_rate); 341 | std::io::stdout().flush().unwrap(); 342 | if port1.set_baud_rate(baud_rate).is_ok() && port2.set_baud_rate(baud_rate).is_ok() { 343 | check_test_message(port1, port2); 344 | check_test_message(port2, port1); 345 | } else { 346 | println!("FAILED (does this platform & port support arbitrary baud rates?)"); 347 | } 348 | let baud_rate = 9600; 349 | println!(" At {},8,n,1,noflow...", baud_rate); 350 | std::io::stdout().flush().unwrap(); 351 | if port1.set_baud_rate(baud_rate).is_ok() && port2.set_baud_rate(baud_rate).is_ok() { 352 | check_test_message(port1, port2); 353 | check_test_message(port2, port1); 354 | } else { 355 | println!("FAILED"); 356 | } 357 | 358 | // Test flow control 359 | port1.set_flow_control(FlowControl::Software).unwrap(); 360 | port2.set_flow_control(FlowControl::Software).unwrap(); 361 | println!(" At 9600,8,n,1,softflow..."); 362 | std::io::stdout().flush().unwrap(); 363 | check_test_message(port1, port2); 364 | check_test_message(port2, port1); 365 | 366 | port1.set_flow_control(FlowControl::Hardware).unwrap(); 367 | port2.set_flow_control(FlowControl::Hardware).unwrap(); 368 | println!(" At 9600,8,n,1,hardflow..."); 369 | std::io::stdout().flush().unwrap(); 370 | check_test_message(port1, port2); 371 | check_test_message(port2, port1); 372 | } 373 | 374 | fn set_defaults(port: &mut dyn serialport::SerialPort) { 375 | port.set_baud_rate(9600).unwrap(); 376 | port.set_data_bits(DataBits::Eight).unwrap(); 377 | port.set_flow_control(FlowControl::Software).unwrap(); 378 | port.set_parity(Parity::None).unwrap(); 379 | port.set_stop_bits(StopBits::One).unwrap(); 380 | // TODO: Clean up timeouts and use a less-arbitrary value here. The previous timeout of 0 made 381 | // test_dual_ports fail due to a timeout where having at least some some made them pass. 382 | port.set_timeout(Duration::from_millis(1000)).unwrap(); 383 | } 384 | -------------------------------------------------------------------------------- /examples/list_ports.rs: -------------------------------------------------------------------------------- 1 | use serialport::{available_ports, SerialPortType}; 2 | 3 | fn main() { 4 | match available_ports() { 5 | Ok(mut ports) => { 6 | // Let's output ports in a stable order to facilitate comparing the output from 7 | // different runs (on different platforms, with different features, ...). 8 | ports.sort_by_key(|i| i.port_name.clone()); 9 | 10 | match ports.len() { 11 | 0 => println!("No ports found."), 12 | 1 => println!("Found 1 port:"), 13 | n => println!("Found {} ports:", n), 14 | }; 15 | 16 | for p in ports { 17 | println!(" {}", p.port_name); 18 | match p.port_type { 19 | SerialPortType::UsbPort(info) => { 20 | println!(" Type: USB"); 21 | println!(" VID: {:04x}", info.vid); 22 | println!(" PID: {:04x}", info.pid); 23 | #[cfg(feature = "usbportinfo-interface")] 24 | println!( 25 | " Interface: {}", 26 | info.interface 27 | .as_ref() 28 | .map_or("".to_string(), |x| format!("{:02x}", *x)) 29 | ); 30 | println!( 31 | " Serial Number: {}", 32 | info.serial_number.as_ref().map_or("", String::as_str) 33 | ); 34 | println!( 35 | " Manufacturer: {}", 36 | info.manufacturer.as_ref().map_or("", String::as_str) 37 | ); 38 | println!( 39 | " Product: {}", 40 | info.product.as_ref().map_or("", String::as_str) 41 | ); 42 | } 43 | SerialPortType::BluetoothPort => { 44 | println!(" Type: Bluetooth"); 45 | } 46 | SerialPortType::PciPort => { 47 | println!(" Type: PCI"); 48 | } 49 | SerialPortType::Unknown => { 50 | println!(" Type: Unknown"); 51 | } 52 | } 53 | } 54 | } 55 | Err(e) => { 56 | eprintln!("{:?}", e); 57 | eprintln!("Error listing serial ports"); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/loopback.rs: -------------------------------------------------------------------------------- 1 | //! This example performs a loopback test using real hardware ports 2 | //! 3 | //! Additionally, some data will be collected and logged during the test to provide some 4 | //! rudimentary benchmarking information. When 'split-port' is specified, the serial port will 5 | //! be split into two channels that read/write "simultaneously" from multiple threads. 6 | //! 7 | //! You can also provide the length (in bytes) of data to test with, and the number of iterations to perform or 8 | //! a list of raw bytes to transmit. 9 | //! 10 | //! To run this example: 11 | //! 12 | //! 1) `cargo run --example loopback /dev/ttyUSB0` 13 | //! 14 | //! 2) `cargo run --example loopback /dev/ttyUSB0 --split-port` 15 | //! 16 | //! 3) `cargo run --example loopback /dev/ttyUSB0 -i 100 -l 32 -b 9600` 17 | //! 18 | //! 4) `cargo run --example loopback /dev/ttyUSB8 --bytes 222,173,190,239` 19 | 20 | use std::time::{Duration, Instant}; 21 | 22 | use clap::Parser; 23 | use serialport::SerialPort; 24 | 25 | /// Serialport Example - Loopback 26 | #[derive(Parser)] 27 | struct Args { 28 | /// The device path to a serialport 29 | port: String, 30 | 31 | /// The number of read/write iterations to perform 32 | #[clap(short, long, default_value = "100")] 33 | iterations: usize, 34 | 35 | /// The number of bytes written per transaction 36 | /// 37 | /// Ignored when bytes are passed directly from the command-line 38 | #[clap(short, long, default_value = "8")] 39 | length: usize, 40 | 41 | /// The baudrate to open the port with 42 | #[clap(short, long, default_value = "115200")] 43 | baudrate: u32, 44 | 45 | /// Bytes to write to the serial port 46 | /// 47 | /// When not specified, the bytes transmitted count up 48 | #[clap(long, use_value_delimiter = true)] 49 | bytes: Option>, 50 | 51 | /// Split the port to read/write from multiple threads 52 | #[clap(long)] 53 | split_port: bool, 54 | } 55 | 56 | fn main() { 57 | let args = Args::parse(); 58 | 59 | // Open the serial port 60 | let mut port = match serialport::new(&args.port, args.baudrate) 61 | .timeout(Duration::MAX) 62 | .open() 63 | { 64 | Err(e) => { 65 | eprintln!("Failed to open \"{}\". Error: {}", args.port, e); 66 | ::std::process::exit(1); 67 | } 68 | Ok(p) => p, 69 | }; 70 | 71 | // Setup stat-tracking 72 | let length = args.length; 73 | let data: Vec = args 74 | .bytes 75 | .unwrap_or_else(|| (0..length).map(|i| i as u8).collect()); 76 | 77 | let (mut read_stats, mut write_stats) = Stats::new(args.iterations, &data); 78 | 79 | // Run the tests 80 | if args.split_port { 81 | loopback_split(&mut port, &mut read_stats, &mut write_stats); 82 | } else { 83 | loopback_standard(&mut port, &mut read_stats, &mut write_stats); 84 | } 85 | 86 | // Print the results 87 | println!("Loopback {}:", args.port); 88 | println!(" data-length: {} bytes", read_stats.data.len()); 89 | println!(" iterations: {}", read_stats.iterations); 90 | println!(" read:"); 91 | println!(" total: {:.6}s", read_stats.total()); 92 | println!(" average: {:.6}s", read_stats.average()); 93 | println!(" max: {:.6}s", read_stats.max()); 94 | println!(" write:"); 95 | println!(" total: {:.6}s", write_stats.total()); 96 | println!(" average: {:.6}s", write_stats.average()); 97 | println!(" max: {:.6}s", write_stats.max()); 98 | println!(" total: {:.6}s", read_stats.total() + write_stats.total()); 99 | println!( 100 | " bytes/s: {:.6}", 101 | (read_stats.data.len() as f32) / (read_stats.average() + write_stats.average()) 102 | ) 103 | } 104 | 105 | /// Capture read/write times to calculate average durations 106 | #[derive(Clone)] 107 | struct Stats<'a> { 108 | pub data: &'a [u8], 109 | pub times: Vec, 110 | pub iterations: usize, 111 | now: Instant, 112 | } 113 | 114 | impl<'a> Stats<'a> { 115 | /// Create new read/write stats 116 | fn new(iterations: usize, data: &'a [u8]) -> (Self, Self) { 117 | ( 118 | Self { 119 | data, 120 | times: Vec::with_capacity(iterations), 121 | iterations, 122 | now: Instant::now(), 123 | }, 124 | Self { 125 | data, 126 | times: Vec::with_capacity(iterations), 127 | iterations, 128 | now: Instant::now(), 129 | }, 130 | ) 131 | } 132 | 133 | /// Start a duration timer 134 | fn start(&mut self) { 135 | self.now = Instant::now(); 136 | } 137 | 138 | /// Store a duration 139 | fn stop(&mut self) { 140 | self.times.push(self.now.elapsed()); 141 | } 142 | 143 | /// Provides the total time elapsed 144 | fn total(&self) -> f32 { 145 | self.times.iter().map(|d| d.as_secs_f32()).sum() 146 | } 147 | 148 | /// Provides average time per transaction 149 | fn average(&self) -> f32 { 150 | self.total() / (self.times.len() as f32) 151 | } 152 | 153 | /// Provides the maximum transaction time 154 | fn max(&self) -> f32 { 155 | self.times 156 | .iter() 157 | .max() 158 | .map(|d| d.as_secs_f32()) 159 | .unwrap_or(0.0) 160 | } 161 | } 162 | 163 | fn loopback_standard<'a>( 164 | port: &mut Box, 165 | read_stats: &mut Stats<'a>, 166 | write_stats: &mut Stats<'a>, 167 | ) { 168 | let mut buf = vec![0u8; read_stats.data.len()]; 169 | 170 | for _ in 0..read_stats.iterations { 171 | // Write data to the port 172 | write_stats.start(); 173 | port.write_all(write_stats.data) 174 | .expect("failed to write to serialport"); 175 | write_stats.stop(); 176 | 177 | // Read data back from the port 178 | read_stats.start(); 179 | port.read_exact(&mut buf) 180 | .expect("failed to read from serialport"); 181 | read_stats.stop(); 182 | 183 | // Crash on error 184 | for (i, x) in buf.iter().enumerate() { 185 | if read_stats.data[i] != *x { 186 | eprintln!( 187 | "Expected byte '{:02X}' but got '{:02X}'", 188 | read_stats.data[i], x 189 | ); 190 | ::std::process::exit(2); 191 | } 192 | } 193 | } 194 | } 195 | 196 | #[rustversion::before(1.63)] 197 | fn loopback_split<'a>( 198 | _port: &mut Box, 199 | _read_stats: &mut Stats<'a>, 200 | _write_stats: &mut Stats<'a>, 201 | ) { 202 | unimplemented!("requires Rust 1.63 or later"); 203 | } 204 | 205 | #[rustversion::since(1.63)] 206 | fn loopback_split<'a>( 207 | port: &mut Box, 208 | read_stats: &mut Stats<'a>, 209 | write_stats: &mut Stats<'a>, 210 | ) { 211 | let mut buf = vec![0u8; read_stats.data.len()]; 212 | let mut rport = match port.try_clone() { 213 | Ok(p) => p, 214 | Err(e) => { 215 | eprintln!("Failed to clone port: {}", e); 216 | ::std::process::exit(3); 217 | } 218 | }; 219 | 220 | // Manage threads for read/writing; port usage is not async, so threads can easily deadlock: 221 | // 222 | // 1. Read Thread: Park -> Read -> Unpark Write ──────┐ 223 | // └──────────────────────────────────┘ 224 | // 2. Write Thread: Write -> Unpark Read -> Park ──────┐ 225 | // └──────────────────────────────────┘ 226 | std::thread::scope(|scope| { 227 | // Get handle for writing thread 228 | let wr_thread = std::thread::current(); 229 | 230 | // Spawn a thread that reads data for n iterations 231 | let handle = scope.spawn(move || { 232 | for _ in 0..read_stats.iterations { 233 | // Wait for the write to complete 234 | std::thread::park(); 235 | 236 | read_stats.start(); 237 | rport 238 | .read_exact(&mut buf) 239 | .expect("failed to read from serialport"); 240 | read_stats.stop(); 241 | 242 | // Crash on error 243 | for (i, x) in buf.iter().enumerate() { 244 | if read_stats.data[i] != *x { 245 | eprintln!( 246 | "Expected byte '{:02X}' but got '{:02X}'", 247 | read_stats.data[i], x 248 | ); 249 | ::std::process::exit(2); 250 | } 251 | } 252 | 253 | // Allow the writing thread to start 254 | wr_thread.unpark(); 255 | } 256 | }); 257 | 258 | // Write data to the port for n iterations 259 | for _ in 0..write_stats.iterations { 260 | write_stats.start(); 261 | port.write_all(write_stats.data) 262 | .expect("failed to write to serialport"); 263 | write_stats.stop(); 264 | 265 | // Notify that the write completed 266 | handle.thread().unpark(); 267 | 268 | // Wait for read to complete 269 | std::thread::park(); 270 | } 271 | }); 272 | } 273 | -------------------------------------------------------------------------------- /examples/pseudo_terminal.rs: -------------------------------------------------------------------------------- 1 | //! Pseudo terminal example. 2 | 3 | #[cfg(unix)] 4 | fn main() { 5 | use std::io::{Read, Write}; 6 | use std::os::unix::prelude::*; 7 | use std::str; 8 | use std::thread; 9 | use std::time; 10 | 11 | use serialport::{SerialPort, TTYPort}; 12 | 13 | let (mut master, mut slave) = TTYPort::pair().expect("Unable to create pseudo-terminal pair"); 14 | 15 | // Master ptty has no associated path on the filesystem. 16 | println!( 17 | "Master ptty fd: {}, path: {:?}", 18 | master.as_raw_fd(), 19 | master.name() 20 | ); 21 | println!( 22 | "Slave ptty fd: {}, path: {:?}", 23 | slave.as_raw_fd(), 24 | slave.name() 25 | ); 26 | 27 | // Receive buffer. 28 | let mut buf = [0u8; 512]; 29 | 30 | println!("Sending 5 messages from master to slave."); 31 | 32 | // Send 5 messages. 33 | for x in 1..6 { 34 | let msg = format!("Message #{}", x); 35 | 36 | // Send the message on the master 37 | assert_eq!(master.write(msg.as_bytes()).unwrap(), msg.len()); 38 | 39 | // Receive on the slave 40 | let bytes_recvd = slave.read(&mut buf).unwrap(); 41 | assert_eq!(bytes_recvd, msg.len()); 42 | 43 | let msg_recvd = str::from_utf8(&buf[..bytes_recvd]).unwrap(); 44 | assert_eq!(msg_recvd, msg); 45 | 46 | println!("Slave Rx: {}", msg_recvd); 47 | thread::sleep(time::Duration::from_secs(1)); 48 | } 49 | } 50 | 51 | #[cfg(not(unix))] 52 | fn main() {} 53 | -------------------------------------------------------------------------------- /examples/receive_data.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | use std::time::Duration; 3 | 4 | use clap::{Arg, Command}; 5 | 6 | fn main() { 7 | let matches = Command::new("Serialport Example - Receive Data") 8 | .about("Reads data from a serial port and echoes it to stdout") 9 | .disable_version_flag(true) 10 | .arg( 11 | Arg::new("port") 12 | .help("The device path to a serial port") 13 | .use_value_delimiter(false) 14 | .required(true), 15 | ) 16 | .arg( 17 | Arg::new("baud") 18 | .help("The baud rate to connect at") 19 | .use_value_delimiter(false) 20 | .required(true) 21 | .validator(valid_baud), 22 | ) 23 | .get_matches(); 24 | 25 | let port_name = matches.value_of("port").unwrap(); 26 | let baud_rate = matches.value_of("baud").unwrap().parse::().unwrap(); 27 | 28 | let port = serialport::new(port_name, baud_rate) 29 | .timeout(Duration::from_millis(10)) 30 | .open(); 31 | 32 | match port { 33 | Ok(mut port) => { 34 | let mut serial_buf: Vec = vec![0; 1000]; 35 | println!("Receiving data on {} at {} baud:", &port_name, &baud_rate); 36 | loop { 37 | match port.read(serial_buf.as_mut_slice()) { 38 | Ok(t) => { 39 | io::stdout().write_all(&serial_buf[..t]).unwrap(); 40 | io::stdout().flush().unwrap(); 41 | } 42 | Err(ref e) if e.kind() == io::ErrorKind::TimedOut => (), 43 | Err(e) => eprintln!("{:?}", e), 44 | } 45 | } 46 | } 47 | Err(e) => { 48 | eprintln!("Failed to open \"{}\". Error: {}", port_name, e); 49 | ::std::process::exit(1); 50 | } 51 | } 52 | } 53 | 54 | fn valid_baud(val: &str) -> Result<(), String> { 55 | val.parse::() 56 | .map(|_| ()) 57 | .map_err(|_| format!("Invalid baud rate '{}' specified", val)) 58 | } 59 | -------------------------------------------------------------------------------- /examples/transmit.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | use std::time::Duration; 3 | 4 | use clap::{Arg, Command}; 5 | 6 | use serialport::{DataBits, StopBits}; 7 | 8 | fn main() { 9 | let matches = Command::new("Serialport Example - Heartbeat") 10 | .about("Write bytes to a serial port at 1Hz") 11 | .disable_version_flag(true) 12 | .arg( 13 | Arg::new("port") 14 | .help("The device path to a serial port") 15 | .required(true), 16 | ) 17 | .arg( 18 | Arg::new("baud") 19 | .help("The baud rate to connect at") 20 | .use_value_delimiter(false) 21 | .required(true) 22 | .validator(valid_baud), 23 | ) 24 | .arg( 25 | Arg::new("stop-bits") 26 | .long("stop-bits") 27 | .help("Number of stop bits to use") 28 | .takes_value(true) 29 | .possible_values(["1", "2"]) 30 | .default_value("1"), 31 | ) 32 | .arg( 33 | Arg::new("data-bits") 34 | .long("data-bits") 35 | .help("Number of data bits to use") 36 | .takes_value(true) 37 | .possible_values(["5", "6", "7", "8"]) 38 | .default_value("8"), 39 | ) 40 | .arg( 41 | Arg::new("rate") 42 | .long("rate") 43 | .help("Frequency (Hz) to repeat transmission of the pattern (0 indicates sending only once") 44 | .takes_value(true) 45 | .default_value("1"), 46 | ) 47 | .arg( 48 | Arg::new("string") 49 | .long("string") 50 | .help("String to transmit") 51 | .takes_value(true) 52 | .default_value("."), 53 | ) 54 | .get_matches(); 55 | 56 | let port_name = matches.value_of("port").unwrap(); 57 | let baud_rate = matches.value_of("baud").unwrap().parse::().unwrap(); 58 | let stop_bits = match matches.value_of("stop-bits") { 59 | Some("2") => StopBits::Two, 60 | _ => StopBits::One, 61 | }; 62 | let data_bits = match matches.value_of("data-bits") { 63 | Some("5") => DataBits::Five, 64 | Some("6") => DataBits::Six, 65 | Some("7") => DataBits::Seven, 66 | _ => DataBits::Eight, 67 | }; 68 | let rate = matches.value_of("rate").unwrap().parse::().unwrap(); 69 | let string = matches.value_of("string").unwrap(); 70 | 71 | let builder = serialport::new(port_name, baud_rate) 72 | .stop_bits(stop_bits) 73 | .data_bits(data_bits); 74 | println!("{:?}", &builder); 75 | let mut port = builder.open().unwrap_or_else(|e| { 76 | eprintln!("Failed to open \"{}\". Error: {}", port_name, e); 77 | ::std::process::exit(1); 78 | }); 79 | 80 | println!( 81 | "Writing '{}' to {} at {} baud at {}Hz", 82 | &string, &port_name, &baud_rate, &rate 83 | ); 84 | loop { 85 | match port.write(string.as_bytes()) { 86 | Ok(_) => { 87 | print!("{}", &string); 88 | std::io::stdout().flush().unwrap(); 89 | } 90 | Err(ref e) if e.kind() == io::ErrorKind::TimedOut => (), 91 | Err(e) => eprintln!("{:?}", e), 92 | } 93 | if rate == 0 { 94 | return; 95 | } 96 | std::thread::sleep(Duration::from_millis((1000.0 / (rate as f32)) as u64)); 97 | } 98 | } 99 | 100 | fn valid_baud(val: &str) -> std::result::Result<(), String> { 101 | val.parse::() 102 | .map(|_| ()) 103 | .map_err(|_| format!("Invalid baud rate '{}' specified", val)) 104 | } 105 | -------------------------------------------------------------------------------- /src/posix/error.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use crate::{Error, ErrorKind}; 4 | 5 | #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))] 6 | impl From for Error { 7 | fn from(e: libudev::Error) -> Error { 8 | use libudev::ErrorKind as K; 9 | let kind = match e.kind() { 10 | K::NoMem => ErrorKind::Unknown, 11 | K::InvalidInput => ErrorKind::InvalidInput, 12 | K::Io(a) => ErrorKind::Io(a), 13 | }; 14 | Error::new(kind, e.description()) 15 | } 16 | } 17 | 18 | impl From for Error { 19 | fn from(e: nix::Error) -> Error { 20 | use io::ErrorKind as IO; 21 | use nix::errno::Errno as E; 22 | use ErrorKind as K; 23 | let kind = match e { 24 | E::ETIMEDOUT => K::Io(IO::TimedOut), 25 | E::ECONNABORTED => K::Io(IO::ConnectionAborted), 26 | E::ECONNRESET => K::Io(IO::ConnectionReset), 27 | E::ECONNREFUSED => K::Io(IO::ConnectionRefused), 28 | E::ENOTCONN => K::Io(IO::NotConnected), 29 | E::EADDRINUSE => K::Io(IO::AddrInUse), 30 | E::EADDRNOTAVAIL => K::Io(IO::AddrNotAvailable), 31 | E::EAGAIN => K::Io(IO::WouldBlock), 32 | E::EINTR => K::Io(IO::Interrupted), 33 | E::EACCES => K::Io(IO::PermissionDenied), 34 | E::ENOENT => K::Io(IO::NotFound), 35 | _ => K::Unknown, 36 | }; 37 | Error::new(kind, e.desc()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/posix/ioctl.rs: -------------------------------------------------------------------------------- 1 | use std::os::unix::io::RawFd; 2 | 3 | use bitflags::bitflags; 4 | use nix::libc; 5 | 6 | use crate::Result; 7 | 8 | // These are wrapped in a module because they're `pub` by default 9 | mod raw { 10 | use nix::libc; 11 | use nix::{ioctl_none_bad, ioctl_read, ioctl_read_bad, ioctl_write_ptr, ioctl_write_ptr_bad}; 12 | 13 | ioctl_none_bad!(tiocexcl, libc::TIOCEXCL); 14 | ioctl_none_bad!(tiocnxcl, libc::TIOCNXCL); 15 | ioctl_read_bad!(tiocmget, libc::TIOCMGET, libc::c_int); 16 | ioctl_none_bad!(tiocsbrk, libc::TIOCSBRK); 17 | ioctl_none_bad!(tioccbrk, libc::TIOCCBRK); 18 | 19 | #[cfg(any(target_os = "android", target_os = "linux"))] 20 | ioctl_read_bad!(fionread, libc::FIONREAD, libc::c_int); 21 | 22 | // See: /usr/include/sys/filio.h 23 | #[cfg(any( 24 | target_os = "dragonfly", 25 | target_os = "freebsd", 26 | target_os = "ios", 27 | target_os = "macos", 28 | target_os = "netbsd", 29 | target_os = "openbsd" 30 | ))] 31 | ioctl_read!(fionread, b'f', 127, libc::c_int); 32 | 33 | #[cfg(any(target_os = "android", target_os = "linux"))] 34 | ioctl_read_bad!(tiocoutq, libc::TIOCOUTQ, libc::c_int); 35 | 36 | // See: /usr/include/sys/ttycom.h 37 | #[cfg(any( 38 | target_os = "dragonfly", 39 | target_os = "freebsd", 40 | target_os = "ios", 41 | target_os = "macos", 42 | target_os = "netbsd", 43 | target_os = "openbsd" 44 | ))] 45 | ioctl_read!(tiocoutq, b't', 115, libc::c_int); 46 | 47 | ioctl_write_ptr_bad!(tiocmbic, libc::TIOCMBIC, libc::c_int); 48 | ioctl_write_ptr_bad!(tiocmbis, libc::TIOCMBIS, libc::c_int); 49 | ioctl_read!( 50 | #[cfg(any( 51 | target_os = "android", 52 | all( 53 | target_os = "linux", 54 | not(any( 55 | target_env = "musl", 56 | target_arch = "powerpc", 57 | target_arch = "powerpc64" 58 | )) 59 | ) 60 | ))] 61 | tcgets2, 62 | b'T', 63 | 0x2A, 64 | libc::termios2 65 | ); 66 | ioctl_write_ptr!( 67 | #[cfg(any( 68 | target_os = "android", 69 | all( 70 | target_os = "linux", 71 | not(any( 72 | target_env = "musl", 73 | target_arch = "powerpc", 74 | target_arch = "powerpc64" 75 | )) 76 | ) 77 | ))] 78 | tcsets2, 79 | b'T', 80 | 0x2B, 81 | libc::termios2 82 | ); 83 | #[cfg(any(target_os = "ios", target_os = "macos"))] 84 | const IOSSIOSPEED: libc::c_ulong = 0x80045402; 85 | ioctl_write_ptr_bad!( 86 | #[cfg(any(target_os = "ios", target_os = "macos"))] 87 | iossiospeed, 88 | IOSSIOSPEED, 89 | libc::speed_t 90 | ); 91 | } 92 | 93 | bitflags! { 94 | /// Flags to indicate which wires in a serial connection to use 95 | pub struct SerialLines: libc::c_int { 96 | const DATA_SET_READY = libc::TIOCM_DSR; 97 | const DATA_TERMINAL_READY = libc::TIOCM_DTR; 98 | const REQUEST_TO_SEND = libc::TIOCM_RTS; 99 | const SECONDARY_TRANSMIT = libc::TIOCM_ST; 100 | const SECONDARY_RECEIVE = libc::TIOCM_SR; 101 | const CLEAR_TO_SEND = libc::TIOCM_CTS; 102 | const DATA_CARRIER_DETECT = libc::TIOCM_CAR; 103 | const RING = libc::TIOCM_RNG; 104 | } 105 | } 106 | 107 | pub fn tiocexcl(fd: RawFd) -> Result<()> { 108 | unsafe { raw::tiocexcl(fd) } 109 | .map(|_| ()) 110 | .map_err(|e| e.into()) 111 | } 112 | 113 | pub fn tiocnxcl(fd: RawFd) -> Result<()> { 114 | unsafe { raw::tiocnxcl(fd) } 115 | .map(|_| ()) 116 | .map_err(|e| e.into()) 117 | } 118 | 119 | pub fn tiocmget(fd: RawFd) -> Result { 120 | let mut status: libc::c_int = 0; 121 | unsafe { raw::tiocmget(fd, &mut status) } 122 | .map(|_| SerialLines::from_bits_truncate(status)) 123 | .map_err(|e| e.into()) 124 | } 125 | 126 | pub fn tiocsbrk(fd: RawFd) -> Result<()> { 127 | unsafe { raw::tiocsbrk(fd) } 128 | .map(|_| ()) 129 | .map_err(|e| e.into()) 130 | } 131 | 132 | pub fn tioccbrk(fd: RawFd) -> Result<()> { 133 | unsafe { raw::tioccbrk(fd) } 134 | .map(|_| ()) 135 | .map_err(|e| e.into()) 136 | } 137 | 138 | pub fn fionread(fd: RawFd) -> Result { 139 | let mut retval: libc::c_int = 0; 140 | unsafe { raw::fionread(fd, &mut retval) } 141 | .map(|_| retval as u32) 142 | .map_err(|e| e.into()) 143 | } 144 | 145 | pub fn tiocoutq(fd: RawFd) -> Result { 146 | let mut retval: libc::c_int = 0; 147 | unsafe { raw::tiocoutq(fd, &mut retval) } 148 | .map(|_| retval as u32) 149 | .map_err(|e| e.into()) 150 | } 151 | 152 | pub fn tiocmbic(fd: RawFd, status: SerialLines) -> Result<()> { 153 | let bits = status.bits() as libc::c_int; 154 | unsafe { raw::tiocmbic(fd, &bits) } 155 | .map(|_| ()) 156 | .map_err(|e| e.into()) 157 | } 158 | 159 | pub fn tiocmbis(fd: RawFd, status: SerialLines) -> Result<()> { 160 | let bits = status.bits() as libc::c_int; 161 | unsafe { raw::tiocmbis(fd, &bits) } 162 | .map(|_| ()) 163 | .map_err(|e| e.into()) 164 | } 165 | 166 | #[cfg(any( 167 | target_os = "android", 168 | all( 169 | target_os = "linux", 170 | not(any( 171 | target_env = "musl", 172 | target_arch = "powerpc", 173 | target_arch = "powerpc64" 174 | )) 175 | ) 176 | ))] 177 | pub fn tcgets2(fd: RawFd) -> Result { 178 | let mut options = std::mem::MaybeUninit::uninit(); 179 | match unsafe { raw::tcgets2(fd, options.as_mut_ptr()) } { 180 | Ok(_) => unsafe { Ok(options.assume_init()) }, 181 | Err(e) => Err(e.into()), 182 | } 183 | } 184 | 185 | #[cfg(any( 186 | target_os = "android", 187 | all( 188 | target_os = "linux", 189 | not(any( 190 | target_env = "musl", 191 | target_arch = "powerpc", 192 | target_arch = "powerpc64" 193 | )) 194 | ) 195 | ))] 196 | pub fn tcsets2(fd: RawFd, options: &libc::termios2) -> Result<()> { 197 | unsafe { raw::tcsets2(fd, options) } 198 | .map(|_| ()) 199 | .map_err(|e| e.into()) 200 | } 201 | 202 | #[cfg(any(target_os = "ios", target_os = "macos"))] 203 | pub fn iossiospeed(fd: RawFd, baud_rate: &libc::speed_t) -> Result<()> { 204 | unsafe { raw::iossiospeed(fd, baud_rate) } 205 | .map(|_| ()) 206 | .map_err(|e| e.into()) 207 | } 208 | -------------------------------------------------------------------------------- /src/posix/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::enumerate::*; 2 | pub use self::tty::*; 3 | 4 | mod enumerate; 5 | mod error; 6 | mod ioctl; 7 | mod poll; 8 | mod termios; 9 | mod tty; 10 | -------------------------------------------------------------------------------- /src/posix/poll.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types, dead_code)] 2 | 3 | use std::io; 4 | use std::os::unix::io::RawFd; 5 | use std::slice; 6 | use std::time::Duration; 7 | 8 | use nix::libc::c_int; 9 | use nix::poll::{PollFd, PollFlags}; 10 | #[cfg(target_os = "linux")] 11 | use nix::sys::signal::SigSet; 12 | #[cfg(any(target_os = "linux", test))] 13 | use nix::sys::time::TimeSpec; 14 | 15 | pub fn wait_read_fd(fd: RawFd, timeout: Duration) -> io::Result<()> { 16 | wait_fd(fd, PollFlags::POLLIN, timeout) 17 | } 18 | 19 | pub fn wait_write_fd(fd: RawFd, timeout: Duration) -> io::Result<()> { 20 | wait_fd(fd, PollFlags::POLLOUT, timeout) 21 | } 22 | 23 | fn wait_fd(fd: RawFd, events: PollFlags, timeout: Duration) -> io::Result<()> { 24 | use nix::errno::Errno::{EIO, EPIPE}; 25 | 26 | let mut fd = PollFd::new(fd, events); 27 | 28 | let wait = match poll_clamped(&mut fd, timeout) { 29 | Ok(r) => r, 30 | Err(e) => return Err(io::Error::from(crate::Error::from(e))), 31 | }; 32 | // All errors generated by poll or ppoll are already caught by the nix wrapper around libc, so 33 | // here we only need to check if there's at least 1 event 34 | if wait != 1 { 35 | return Err(io::Error::new( 36 | io::ErrorKind::TimedOut, 37 | "Operation timed out", 38 | )); 39 | } 40 | 41 | // Check the result of ppoll() by looking at the revents field 42 | match fd.revents() { 43 | Some(e) if e == events => return Ok(()), 44 | // If there was a hangout or invalid request 45 | Some(e) if e.contains(PollFlags::POLLHUP) || e.contains(PollFlags::POLLNVAL) => { 46 | return Err(io::Error::new(io::ErrorKind::BrokenPipe, EPIPE.desc())); 47 | } 48 | Some(_) | None => (), 49 | } 50 | 51 | Err(io::Error::new(io::ErrorKind::Other, EIO.desc())) 52 | } 53 | 54 | /// Poll with a duration clamped to the maximum value representable by the `TimeSpec` used by 55 | /// `ppoll`. 56 | #[cfg(target_os = "linux")] 57 | fn poll_clamped(fd: &mut PollFd, timeout: Duration) -> nix::Result { 58 | let spec = clamped_time_spec(timeout); 59 | nix::poll::ppoll(slice::from_mut(fd), Some(spec), Some(SigSet::empty())) 60 | } 61 | 62 | #[cfg(any(target_os = "linux", test))] 63 | // The type time_t is deprecaten on musl. The nix crate internally uses this type and makes an 64 | // exeption for the deprecation for musl. And so do we. 65 | // 66 | // See https://github.com/rust-lang/libc/issues/1848 which is referenced from every exemption used 67 | // in nix. 68 | #[cfg_attr(target_env = "musl", allow(deprecated))] 69 | fn clamped_time_spec(duration: Duration) -> TimeSpec { 70 | use nix::libc::c_long; 71 | use nix::sys::time::time_t; 72 | 73 | // We need to clamp manually as TimeSpec::from_duration translates durations with more than 74 | // i64::MAX seconds to negative timespans. This happens due to casting to i64 and is still the 75 | // case as of nix 0.29. 76 | let secs_limit = time_t::MAX as u64; 77 | let secs = duration.as_secs(); 78 | if secs <= secs_limit { 79 | TimeSpec::new(secs as time_t, duration.subsec_nanos() as c_long) 80 | } else { 81 | TimeSpec::new(time_t::MAX, 999_999_999) 82 | } 83 | } 84 | 85 | // Poll with a duration clamped to the maximum millisecond value representable by the `c_int` used 86 | // by `poll`. 87 | #[cfg(not(target_os = "linux"))] 88 | fn poll_clamped(fd: &mut PollFd, timeout: Duration) -> nix::Result { 89 | let millis = clamped_millis_c_int(timeout); 90 | nix::poll::poll(slice::from_mut(fd), millis) 91 | } 92 | 93 | #[cfg(any(not(target_os = "linux"), test))] 94 | fn clamped_millis_c_int(duration: Duration) -> c_int { 95 | let secs_limit = (c_int::MAX as u64) / 1000; 96 | let secs = duration.as_secs(); 97 | 98 | if secs <= secs_limit { 99 | secs as c_int * 1000 + duration.subsec_millis() as c_int 100 | } else { 101 | c_int::MAX 102 | } 103 | } 104 | 105 | #[cfg(test)] 106 | mod tests { 107 | use super::*; 108 | use crate::tests::timeout::MONOTONIC_DURATIONS; 109 | 110 | #[test] 111 | fn clamped_millis_c_int_is_monotonic() { 112 | let mut last = clamped_millis_c_int(Duration::ZERO); 113 | 114 | for (i, d) in MONOTONIC_DURATIONS.iter().enumerate() { 115 | let next = clamped_millis_c_int(*d); 116 | assert!( 117 | next >= last, 118 | "{next} >= {last} failed for {d:?} at index {i}" 119 | ); 120 | last = next; 121 | } 122 | } 123 | 124 | #[test] 125 | fn clamped_millis_c_int_zero_is_zero() { 126 | assert_eq!(0, clamped_millis_c_int(Duration::ZERO)); 127 | } 128 | 129 | #[test] 130 | fn clamped_time_spec_is_monotonic() { 131 | let mut last = clamped_time_spec(Duration::ZERO); 132 | 133 | for (i, d) in MONOTONIC_DURATIONS.iter().enumerate() { 134 | let next = clamped_time_spec(*d); 135 | assert!( 136 | next >= last, 137 | "{next} >= {last} failed for {d:?} at index {i}" 138 | ); 139 | last = next; 140 | } 141 | } 142 | 143 | #[test] 144 | fn clamped_time_spec_zero_is_zero() { 145 | let spec = clamped_time_spec(Duration::ZERO); 146 | assert_eq!(0, spec.tv_sec()); 147 | assert_eq!(0, spec.tv_nsec()); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/posix/termios.rs: -------------------------------------------------------------------------------- 1 | // A set of helper functions for working with the `termios` and `termios2` structs 2 | use cfg_if::cfg_if; 3 | 4 | use crate::{DataBits, FlowControl, Parity, Result, StopBits}; 5 | use nix::libc; 6 | 7 | use std::os::unix::prelude::*; 8 | 9 | cfg_if! { 10 | if #[cfg(any( 11 | target_os = "dragonfly", 12 | target_os = "freebsd", 13 | target_os = "ios", 14 | target_os = "macos", 15 | target_os = "netbsd", 16 | target_os = "openbsd", 17 | all( 18 | target_os = "linux", 19 | any( 20 | target_env = "musl", 21 | target_arch = "powerpc", 22 | target_arch = "powerpc64" 23 | ) 24 | ) 25 | ))] { 26 | pub(crate) type Termios = libc::termios; 27 | } else if #[cfg(any( 28 | target_os = "android", 29 | all( 30 | target_os = "linux", 31 | not(any( 32 | target_env = "musl", 33 | target_arch = "powerpc", 34 | target_arch = "powerpc64" 35 | )) 36 | ) 37 | ))] { 38 | pub(crate) type Termios = libc::termios2; 39 | } else { 40 | compile_error!("Unsupported platform. See crate documentation for supported platforms"); 41 | } 42 | } 43 | 44 | // The termios struct isn't used for storing the baud rate, but it can be affected by other 45 | // calls in this lib to the IOSSIOSPEED ioctl. So whenever we get this struct, make sure to 46 | // reset the input & output baud rates to a safe default. This is accounted for by the 47 | // corresponding set_termios that is mac-specific and always calls IOSSIOSPEED. 48 | #[cfg(any(target_os = "ios", target_os = "macos",))] 49 | pub(crate) fn get_termios(fd: RawFd) -> Result { 50 | use std::mem::MaybeUninit; 51 | 52 | let mut termios = MaybeUninit::uninit(); 53 | let res = unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) }; 54 | nix::errno::Errno::result(res)?; 55 | let mut termios = unsafe { termios.assume_init() }; 56 | termios.c_ispeed = self::libc::B9600; 57 | termios.c_ospeed = self::libc::B9600; 58 | Ok(termios) 59 | } 60 | 61 | #[cfg(any( 62 | target_os = "dragonfly", 63 | target_os = "freebsd", 64 | target_os = "netbsd", 65 | target_os = "openbsd", 66 | all( 67 | target_os = "linux", 68 | any( 69 | target_env = "musl", 70 | target_arch = "powerpc", 71 | target_arch = "powerpc64" 72 | ) 73 | ) 74 | ))] 75 | pub(crate) fn get_termios(fd: RawFd) -> Result { 76 | use std::mem::MaybeUninit; 77 | 78 | let mut termios = MaybeUninit::uninit(); 79 | let res = unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) }; 80 | nix::errno::Errno::result(res)?; 81 | unsafe { Ok(termios.assume_init()) } 82 | } 83 | 84 | #[cfg(any( 85 | target_os = "android", 86 | all( 87 | target_os = "linux", 88 | not(any( 89 | target_env = "musl", 90 | target_arch = "powerpc", 91 | target_arch = "powerpc64" 92 | )) 93 | ) 94 | ))] 95 | pub(crate) fn get_termios(fd: RawFd) -> Result { 96 | crate::posix::ioctl::tcgets2(fd) 97 | } 98 | 99 | #[cfg(any(target_os = "ios", target_os = "macos",))] 100 | pub(crate) fn set_termios(fd: RawFd, termios: &libc::termios, baud_rate: u32) -> Result<()> { 101 | let res = unsafe { libc::tcsetattr(fd, libc::TCSANOW, termios) }; 102 | nix::errno::Errno::result(res)?; 103 | 104 | // Note: attempting to set the baud rate on a pseudo terminal via this ioctl call will fail 105 | // with the `ENOTTY` error. 106 | if baud_rate > 0 { 107 | crate::posix::ioctl::iossiospeed(fd, &(baud_rate as libc::speed_t))?; 108 | } 109 | 110 | Ok(()) 111 | } 112 | 113 | #[cfg(any( 114 | target_os = "dragonfly", 115 | target_os = "freebsd", 116 | target_os = "netbsd", 117 | target_os = "openbsd", 118 | all( 119 | target_os = "linux", 120 | any( 121 | target_env = "musl", 122 | target_arch = "powerpc", 123 | target_arch = "powerpc64" 124 | ) 125 | ) 126 | ))] 127 | pub(crate) fn set_termios(fd: RawFd, termios: &libc::termios) -> Result<()> { 128 | let res = unsafe { libc::tcsetattr(fd, libc::TCSANOW, termios) }; 129 | nix::errno::Errno::result(res)?; 130 | Ok(()) 131 | } 132 | 133 | #[cfg(any( 134 | target_os = "android", 135 | all( 136 | target_os = "linux", 137 | not(any( 138 | target_env = "musl", 139 | target_arch = "powerpc", 140 | target_arch = "powerpc64" 141 | )) 142 | ) 143 | ))] 144 | pub(crate) fn set_termios(fd: RawFd, termios: &Termios) -> Result<()> { 145 | crate::posix::ioctl::tcsets2(fd, termios) 146 | } 147 | 148 | pub(crate) fn set_parity(termios: &mut Termios, parity: Parity) { 149 | match parity { 150 | Parity::None => { 151 | termios.c_cflag &= !(libc::PARENB | libc::PARODD); 152 | termios.c_iflag &= !libc::INPCK; 153 | termios.c_iflag |= libc::IGNPAR; 154 | } 155 | Parity::Odd => { 156 | termios.c_cflag |= libc::PARENB | libc::PARODD; 157 | termios.c_iflag |= libc::INPCK; 158 | termios.c_iflag &= !libc::IGNPAR; 159 | } 160 | Parity::Even => { 161 | termios.c_cflag &= !libc::PARODD; 162 | termios.c_cflag |= libc::PARENB; 163 | termios.c_iflag |= libc::INPCK; 164 | termios.c_iflag &= !libc::IGNPAR; 165 | } 166 | }; 167 | } 168 | 169 | pub(crate) fn set_flow_control(termios: &mut Termios, flow_control: FlowControl) { 170 | match flow_control { 171 | FlowControl::None => { 172 | termios.c_iflag &= !(libc::IXON | libc::IXOFF); 173 | termios.c_cflag &= !libc::CRTSCTS; 174 | } 175 | FlowControl::Software => { 176 | termios.c_iflag |= libc::IXON | libc::IXOFF; 177 | termios.c_cflag &= !libc::CRTSCTS; 178 | } 179 | FlowControl::Hardware => { 180 | termios.c_iflag &= !(libc::IXON | libc::IXOFF); 181 | termios.c_cflag |= libc::CRTSCTS; 182 | } 183 | }; 184 | } 185 | 186 | pub(crate) fn set_data_bits(termios: &mut Termios, data_bits: DataBits) { 187 | let size = match data_bits { 188 | DataBits::Five => libc::CS5, 189 | DataBits::Six => libc::CS6, 190 | DataBits::Seven => libc::CS7, 191 | DataBits::Eight => libc::CS8, 192 | }; 193 | 194 | termios.c_cflag &= !libc::CSIZE; 195 | termios.c_cflag |= size; 196 | } 197 | 198 | pub(crate) fn set_stop_bits(termios: &mut Termios, stop_bits: StopBits) { 199 | match stop_bits { 200 | StopBits::One => termios.c_cflag &= !libc::CSTOPB, 201 | StopBits::Two => termios.c_cflag |= libc::CSTOPB, 202 | }; 203 | } 204 | 205 | #[cfg(any( 206 | target_os = "android", 207 | all( 208 | target_os = "linux", 209 | not(any( 210 | target_env = "musl", 211 | target_arch = "powerpc", 212 | target_arch = "powerpc64" 213 | )) 214 | ) 215 | ))] 216 | pub(crate) fn set_baud_rate(termios: &mut Termios, baud_rate: u32) -> Result<()> { 217 | termios.c_cflag &= !nix::libc::CBAUD; 218 | termios.c_cflag |= nix::libc::BOTHER; 219 | termios.c_ispeed = baud_rate; 220 | termios.c_ospeed = baud_rate; 221 | Ok(()) 222 | } 223 | 224 | // BSDs use the baud rate as the constant value so there's no translation necessary 225 | #[cfg(any( 226 | target_os = "dragonfly", 227 | target_os = "freebsd", 228 | target_os = "netbsd", 229 | target_os = "openbsd" 230 | ))] 231 | pub(crate) fn set_baud_rate(termios: &mut Termios, baud_rate: u32) -> Result<()> { 232 | let res = unsafe { libc::cfsetspeed(termios, baud_rate.into()) }; 233 | nix::errno::Errno::result(res)?; 234 | Ok(()) 235 | } 236 | 237 | #[cfg(all( 238 | target_os = "linux", 239 | any( 240 | target_env = "musl", 241 | target_arch = "powerpc", 242 | target_arch = "powerpc64" 243 | ) 244 | ))] 245 | pub(crate) fn set_baud_rate(termios: &mut Termios, baud_rate: u32) -> Result<()> { 246 | use crate::{Error, ErrorKind}; 247 | 248 | use self::libc::{ 249 | B1000000, B1152000, B1500000, B2000000, B2500000, B3000000, B3500000, B4000000, B460800, 250 | B500000, B576000, B921600, 251 | }; 252 | use self::libc::{ 253 | B110, B115200, B1200, B134, B150, B1800, B19200, B200, B230400, B2400, B300, B38400, B4800, 254 | B50, B57600, B600, B75, B9600, 255 | }; 256 | 257 | let baud_rate = match baud_rate { 258 | 50 => B50, 259 | 75 => B75, 260 | 110 => B110, 261 | 134 => B134, 262 | 150 => B150, 263 | 200 => B200, 264 | 300 => B300, 265 | 600 => B600, 266 | 1200 => B1200, 267 | 1800 => B1800, 268 | 2400 => B2400, 269 | 4800 => B4800, 270 | 9600 => B9600, 271 | 19_200 => B19200, 272 | 38_400 => B38400, 273 | 57_600 => B57600, 274 | 115_200 => B115200, 275 | 230_400 => B230400, 276 | 460_800 => B460800, 277 | 500_000 => B500000, 278 | 576_000 => B576000, 279 | 921_600 => B921600, 280 | 1_000_000 => B1000000, 281 | 1_152_000 => B1152000, 282 | 1_500_000 => B1500000, 283 | 2_000_000 => B2000000, 284 | 2_500_000 => B2500000, 285 | 3_000_000 => B3000000, 286 | 3_500_000 => B3500000, 287 | 4_000_000 => B4000000, 288 | _ => return Err(Error::new(ErrorKind::InvalidInput, "Unsupported baud rate")), 289 | }; 290 | let res = unsafe { libc::cfsetspeed(termios, baud_rate) }; 291 | nix::errno::Errno::result(res)?; 292 | Ok(()) 293 | } 294 | -------------------------------------------------------------------------------- /src/posix/tty.rs: -------------------------------------------------------------------------------- 1 | use std::mem::MaybeUninit; 2 | use std::os::unix::prelude::*; 3 | use std::path::Path; 4 | use std::time::{Duration, Instant}; 5 | use std::{io, mem}; 6 | 7 | use nix::fcntl::{fcntl, OFlag}; 8 | use nix::{libc, unistd}; 9 | 10 | use crate::posix::ioctl::{self, SerialLines}; 11 | use crate::posix::termios; 12 | use crate::{ 13 | ClearBuffer, DataBits, Error, ErrorKind, FlowControl, Parity, Result, SerialPort, 14 | SerialPortBuilder, StopBits, 15 | }; 16 | 17 | /// Convenience method for removing exclusive access from 18 | /// a fd and closing it. 19 | fn close(fd: RawFd) { 20 | // remove exclusive access 21 | let _ = ioctl::tiocnxcl(fd); 22 | 23 | // On Linux and BSD, we don't need to worry about return 24 | // type as EBADF means the fd was never open or is already closed 25 | // 26 | // Linux and BSD guarantee that for any other error code the 27 | // fd is already closed, though MacOSX does not. 28 | // 29 | // close() also should never be retried, and the error code 30 | // in most cases in purely informative 31 | let _ = unistd::close(fd); 32 | } 33 | 34 | /// A serial port implementation for POSIX TTY ports 35 | /// 36 | /// The port will be closed when the value is dropped. This struct 37 | /// should not be instantiated directly by using `TTYPort::open()`. 38 | /// Instead, use the cross-platform `serialport::new()`. Example: 39 | /// 40 | /// ```no_run 41 | /// let mut port = serialport::new("/dev/ttyS0", 115200).open().expect("Unable to open"); 42 | /// # let _ = &mut port; 43 | /// ``` 44 | /// 45 | /// Note: on macOS, when connecting to a pseudo-terminal (`pty` opened via 46 | /// `posix_openpt`), the `baud_rate` should be set to 0; this will be used to 47 | /// explicitly _skip_ an attempt to set the baud rate of the file descriptor 48 | /// that would otherwise happen via an `ioctl` command. 49 | /// 50 | /// ``` 51 | /// use serialport::{TTYPort, SerialPort}; 52 | /// 53 | /// let (mut master, mut slave) = TTYPort::pair().expect("Unable to create ptty pair"); 54 | /// # let _ = &mut master; 55 | /// # let _ = &mut slave; 56 | /// // ... elsewhere 57 | /// let mut port = TTYPort::open(&serialport::new(slave.name().unwrap(), 0)).expect("Unable to open"); 58 | /// # let _ = &mut port; 59 | /// ``` 60 | #[derive(Debug)] 61 | pub struct TTYPort { 62 | fd: RawFd, 63 | timeout: Duration, 64 | exclusive: bool, 65 | port_name: Option, 66 | #[cfg(any(target_os = "ios", target_os = "macos"))] 67 | baud_rate: u32, 68 | } 69 | 70 | /// Specifies the duration of a transmission break 71 | #[derive(Clone, Copy, Debug)] 72 | pub enum BreakDuration { 73 | /// 0.25-0.5s 74 | Short, 75 | /// Specifies a break duration that is platform-dependent 76 | Arbitrary(std::num::NonZeroI32), 77 | } 78 | 79 | /// Wrapper for RawFd to assure that it's properly closed, 80 | /// even if the enclosing function exits early. 81 | /// 82 | /// This is similar to the (nightly-only) std::os::unix::io::OwnedFd. 83 | struct OwnedFd(RawFd); 84 | 85 | impl Drop for OwnedFd { 86 | fn drop(&mut self) { 87 | close(self.0); 88 | } 89 | } 90 | 91 | impl OwnedFd { 92 | fn into_raw(self) -> RawFd { 93 | let fd = self.0; 94 | mem::forget(self); 95 | fd 96 | } 97 | } 98 | 99 | impl TTYPort { 100 | /// Opens a TTY device as a serial port. 101 | /// 102 | /// `path` should be the path to a TTY device, e.g., `/dev/ttyS0`. 103 | /// 104 | /// Ports are opened in exclusive mode by default. If this is undesirable 105 | /// behavior, use `TTYPort::set_exclusive(false)`. 106 | /// 107 | /// If the port settings differ from the default settings, characters received 108 | /// before the new settings become active may be garbled. To remove those 109 | /// from the receive buffer, call `TTYPort::clear(ClearBuffer::Input)`. 110 | /// 111 | /// ## Errors 112 | /// 113 | /// * `NoDevice` if the device could not be opened. This could indicate that 114 | /// the device is already in use. 115 | /// * `InvalidInput` if `path` is not a valid device name. 116 | /// * `Io` for any other error while opening or initializing the device. 117 | pub fn open(builder: &SerialPortBuilder) -> Result { 118 | use nix::fcntl::FcntlArg::F_SETFL; 119 | use nix::libc::{cfmakeraw, tcgetattr, tcsetattr}; 120 | 121 | let path = Path::new(&builder.path); 122 | let fd = OwnedFd(nix::fcntl::open( 123 | path, 124 | OFlag::O_RDWR | OFlag::O_NOCTTY | OFlag::O_NONBLOCK | OFlag::O_CLOEXEC, 125 | nix::sys::stat::Mode::empty(), 126 | )?); 127 | 128 | // Try to claim exclusive access to the port. This is performed even 129 | // if the port will later be set as non-exclusive, in order to respect 130 | // other applications that may have an exclusive port lock. 131 | ioctl::tiocexcl(fd.0)?; 132 | 133 | let mut termios = MaybeUninit::uninit(); 134 | nix::errno::Errno::result(unsafe { tcgetattr(fd.0, termios.as_mut_ptr()) })?; 135 | let mut termios = unsafe { termios.assume_init() }; 136 | 137 | // setup TTY for binary serial port access 138 | // Enable reading from the port and ignore all modem control lines 139 | termios.c_cflag |= libc::CREAD | libc::CLOCAL; 140 | // Enable raw mode which disables any implicit processing of the input or output data streams 141 | // This also sets no timeout period and a read will block until at least one character is 142 | // available. 143 | unsafe { cfmakeraw(&mut termios) }; 144 | 145 | // write settings to TTY 146 | unsafe { tcsetattr(fd.0, libc::TCSANOW, &termios) }; 147 | 148 | // Read back settings from port and confirm they were applied correctly 149 | let mut actual_termios = MaybeUninit::uninit(); 150 | unsafe { tcgetattr(fd.0, actual_termios.as_mut_ptr()) }; 151 | let actual_termios = unsafe { actual_termios.assume_init() }; 152 | 153 | if actual_termios.c_iflag != termios.c_iflag 154 | || actual_termios.c_oflag != termios.c_oflag 155 | || actual_termios.c_lflag != termios.c_lflag 156 | || actual_termios.c_cflag != termios.c_cflag 157 | { 158 | return Err(Error::new( 159 | ErrorKind::Unknown, 160 | "Settings did not apply correctly", 161 | )); 162 | }; 163 | 164 | #[cfg(any(target_os = "ios", target_os = "macos"))] 165 | if builder.baud_rate > 0 { 166 | unsafe { libc::tcflush(fd.0, libc::TCIOFLUSH) }; 167 | } 168 | 169 | // clear O_NONBLOCK flag 170 | fcntl(fd.0, F_SETFL(nix::fcntl::OFlag::empty()))?; 171 | 172 | // Configure the low-level port settings 173 | let mut termios = termios::get_termios(fd.0)?; 174 | termios::set_parity(&mut termios, builder.parity); 175 | termios::set_flow_control(&mut termios, builder.flow_control); 176 | termios::set_data_bits(&mut termios, builder.data_bits); 177 | termios::set_stop_bits(&mut termios, builder.stop_bits); 178 | #[cfg(not(any(target_os = "ios", target_os = "macos")))] 179 | termios::set_baud_rate(&mut termios, builder.baud_rate)?; 180 | #[cfg(any(target_os = "ios", target_os = "macos"))] 181 | termios::set_termios(fd.0, &termios, builder.baud_rate)?; 182 | #[cfg(not(any(target_os = "ios", target_os = "macos")))] 183 | termios::set_termios(fd.0, &termios)?; 184 | 185 | // Return the final port object 186 | let mut port = TTYPort { 187 | fd: fd.into_raw(), 188 | timeout: builder.timeout, 189 | exclusive: true, 190 | port_name: Some(builder.path.clone()), 191 | #[cfg(any(target_os = "ios", target_os = "macos"))] 192 | baud_rate: builder.baud_rate, 193 | }; 194 | 195 | // Ignore setting DTR for pseudo terminals. This might be indicated by baud_rate == 0, but 196 | // as this is not always the case, just try on best-effort. 197 | if builder.baud_rate > 0 { 198 | if let Some(dtr) = builder.dtr_on_open { 199 | let _ = port.write_data_terminal_ready(dtr); 200 | } 201 | } 202 | 203 | Ok(port) 204 | } 205 | 206 | /// Returns the exclusivity of the port 207 | /// 208 | /// If a port is exclusive, then trying to open the same device path again 209 | /// will fail. 210 | pub fn exclusive(&self) -> bool { 211 | self.exclusive 212 | } 213 | 214 | /// Sets the exclusivity of the port 215 | /// 216 | /// If a port is exclusive, then trying to open the same device path again 217 | /// will fail. 218 | /// 219 | /// See the man pages for the tiocexcl and tiocnxcl ioctl's for more details. 220 | /// 221 | /// ## Errors 222 | /// 223 | /// * `Io` for any error while setting exclusivity for the port. 224 | pub fn set_exclusive(&mut self, exclusive: bool) -> Result<()> { 225 | let setting_result = if exclusive { 226 | ioctl::tiocexcl(self.fd) 227 | } else { 228 | ioctl::tiocnxcl(self.fd) 229 | }; 230 | 231 | setting_result?; 232 | self.exclusive = exclusive; 233 | Ok(()) 234 | } 235 | 236 | fn set_pin(&mut self, pin: ioctl::SerialLines, level: bool) -> Result<()> { 237 | if level { 238 | ioctl::tiocmbis(self.fd, pin) 239 | } else { 240 | ioctl::tiocmbic(self.fd, pin) 241 | } 242 | } 243 | 244 | fn read_pin(&mut self, pin: ioctl::SerialLines) -> Result { 245 | ioctl::tiocmget(self.fd).map(|pins| pins.contains(pin)) 246 | } 247 | 248 | /// Create a pair of pseudo serial terminals 249 | /// 250 | /// ## Returns 251 | /// Two connected `TTYPort` objects: `(master, slave)` 252 | /// 253 | /// ## Errors 254 | /// Attempting any IO or parameter settings on the slave tty after the master 255 | /// tty is closed will return errors. 256 | /// 257 | /// On some platforms manipulating the master port will fail and only 258 | /// modifying the slave port is possible. 259 | /// 260 | /// ## Examples 261 | /// 262 | /// ``` 263 | /// use serialport::TTYPort; 264 | /// 265 | /// let (mut master, mut slave) = TTYPort::pair().unwrap(); 266 | /// 267 | /// # let _ = &mut master; 268 | /// # let _ = &mut slave; 269 | /// ``` 270 | pub fn pair() -> Result<(Self, Self)> { 271 | // Open the next free pty. 272 | let next_pty_fd = nix::pty::posix_openpt(nix::fcntl::OFlag::O_RDWR)?; 273 | 274 | // Grant access to the associated slave pty 275 | nix::pty::grantpt(&next_pty_fd)?; 276 | 277 | // Unlock the slave pty 278 | nix::pty::unlockpt(&next_pty_fd)?; 279 | 280 | // Get the path of the attached slave ptty 281 | #[cfg(not(any( 282 | target_os = "linux", 283 | target_os = "android", 284 | target_os = "emscripten", 285 | target_os = "fuchsia" 286 | )))] 287 | let ptty_name = unsafe { nix::pty::ptsname(&next_pty_fd)? }; 288 | 289 | #[cfg(any( 290 | target_os = "linux", 291 | target_os = "android", 292 | target_os = "emscripten", 293 | target_os = "fuchsia" 294 | ))] 295 | let ptty_name = nix::pty::ptsname_r(&next_pty_fd)?; 296 | 297 | // Open the slave port 298 | #[cfg(any(target_os = "ios", target_os = "macos"))] 299 | let baud_rate = 9600; 300 | let fd = nix::fcntl::open( 301 | Path::new(&ptty_name), 302 | OFlag::O_RDWR | OFlag::O_NOCTTY | OFlag::O_NONBLOCK, 303 | nix::sys::stat::Mode::empty(), 304 | )?; 305 | 306 | // Set the port to a raw state. Using these ports will not work without this. 307 | let mut termios = MaybeUninit::uninit(); 308 | let res = unsafe { crate::posix::tty::libc::tcgetattr(fd, termios.as_mut_ptr()) }; 309 | if let Err(e) = nix::errno::Errno::result(res) { 310 | close(fd); 311 | return Err(e.into()); 312 | } 313 | let mut termios = unsafe { termios.assume_init() }; 314 | unsafe { crate::posix::tty::libc::cfmakeraw(&mut termios) }; 315 | unsafe { crate::posix::tty::libc::tcsetattr(fd, libc::TCSANOW, &termios) }; 316 | 317 | fcntl( 318 | fd, 319 | nix::fcntl::FcntlArg::F_SETFL(nix::fcntl::OFlag::empty()), 320 | )?; 321 | 322 | let slave_tty = TTYPort { 323 | fd, 324 | timeout: Duration::from_millis(100), 325 | exclusive: true, 326 | port_name: Some(ptty_name), 327 | #[cfg(any(target_os = "ios", target_os = "macos"))] 328 | baud_rate, 329 | }; 330 | 331 | // Manually construct the master port here because the 332 | // `tcgetattr()` doesn't work on Mac, Solaris, and maybe other 333 | // BSDs when used on the master port. 334 | let master_tty = TTYPort { 335 | fd: next_pty_fd.into_raw_fd(), 336 | timeout: Duration::from_millis(100), 337 | exclusive: true, 338 | port_name: None, 339 | #[cfg(any(target_os = "ios", target_os = "macos"))] 340 | baud_rate, 341 | }; 342 | 343 | Ok((master_tty, slave_tty)) 344 | } 345 | 346 | /// Sends 0-valued bits over the port for a set duration 347 | pub fn send_break(&self, duration: BreakDuration) -> Result<()> { 348 | match duration { 349 | BreakDuration::Short => nix::sys::termios::tcsendbreak(self.fd, 0), 350 | BreakDuration::Arbitrary(n) => nix::sys::termios::tcsendbreak(self.fd, n.get()), 351 | } 352 | .map_err(|e| e.into()) 353 | } 354 | 355 | /// Attempts to clone the `SerialPort`. This allow you to write and read simultaneously from the 356 | /// same serial connection. Please note that if you want a real asynchronous serial port you 357 | /// should look at [mio-serial](https://crates.io/crates/mio-serial) or 358 | /// [tokio-serial](https://crates.io/crates/tokio-serial). 359 | /// 360 | /// Also, you must be very careful when changing the settings of a cloned `SerialPort` : since 361 | /// the settings are cached on a per object basis, trying to modify them from two different 362 | /// objects can cause some nasty behavior. 363 | /// 364 | /// This is the same as `SerialPort::try_clone()` but returns the concrete type instead. 365 | /// 366 | /// # Errors 367 | /// 368 | /// This function returns an error if the serial port couldn't be cloned. 369 | pub fn try_clone_native(&self) -> Result { 370 | let fd_cloned: i32 = fcntl(self.fd, nix::fcntl::F_DUPFD_CLOEXEC(self.fd))?; 371 | Ok(TTYPort { 372 | fd: fd_cloned, 373 | exclusive: self.exclusive, 374 | port_name: self.port_name.clone(), 375 | timeout: self.timeout, 376 | #[cfg(any(target_os = "ios", target_os = "macos"))] 377 | baud_rate: self.baud_rate, 378 | }) 379 | } 380 | } 381 | 382 | impl Drop for TTYPort { 383 | fn drop(&mut self) { 384 | close(self.fd); 385 | } 386 | } 387 | 388 | impl AsRawFd for TTYPort { 389 | fn as_raw_fd(&self) -> RawFd { 390 | self.fd 391 | } 392 | } 393 | 394 | impl IntoRawFd for TTYPort { 395 | fn into_raw_fd(self) -> RawFd { 396 | // Pull just the file descriptor out. We also prevent the destructor 397 | // from being run by calling `mem::forget`. If we didn't do this, the 398 | // port would be closed, which would make `into_raw_fd` unusable. 399 | let TTYPort { fd, .. } = self; 400 | mem::forget(self); 401 | fd 402 | } 403 | } 404 | 405 | /// Get the baud speed for a port from its file descriptor 406 | #[cfg(any(target_os = "ios", target_os = "macos"))] 407 | fn get_termios_speed(fd: RawFd) -> u32 { 408 | let mut termios = MaybeUninit::uninit(); 409 | let res = unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) }; 410 | nix::errno::Errno::result(res).expect("Failed to get termios data"); 411 | let termios = unsafe { termios.assume_init() }; 412 | assert_eq!(termios.c_ospeed, termios.c_ispeed); 413 | termios.c_ospeed as u32 414 | } 415 | 416 | impl FromRawFd for TTYPort { 417 | unsafe fn from_raw_fd(fd: RawFd) -> Self { 418 | TTYPort { 419 | fd, 420 | timeout: Duration::from_millis(100), 421 | exclusive: ioctl::tiocexcl(fd).is_ok(), 422 | // It is not trivial to get the file path corresponding to a file descriptor. 423 | // We'll punt on it and set it to `None` here. 424 | port_name: None, 425 | // It's not guaranteed that the baud rate in the `termios` struct is correct, as 426 | // setting an arbitrary baud rate via the `iossiospeed` ioctl overrides that value, 427 | // but extract that value anyways as a best-guess of the actual baud rate. 428 | #[cfg(any(target_os = "ios", target_os = "macos"))] 429 | baud_rate: get_termios_speed(fd), 430 | } 431 | } 432 | } 433 | 434 | impl io::Read for TTYPort { 435 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 436 | if let Err(e) = super::poll::wait_read_fd(self.fd, self.timeout) { 437 | return Err(io::Error::from(Error::from(e))); 438 | } 439 | 440 | nix::unistd::read(self.fd, buf).map_err(|e| io::Error::from(Error::from(e))) 441 | } 442 | } 443 | 444 | impl io::Write for TTYPort { 445 | fn write(&mut self, buf: &[u8]) -> io::Result { 446 | if let Err(e) = super::poll::wait_write_fd(self.fd, self.timeout) { 447 | return Err(io::Error::from(Error::from(e))); 448 | } 449 | 450 | nix::unistd::write(self.fd, buf).map_err(|e| io::Error::from(Error::from(e))) 451 | } 452 | 453 | fn flush(&mut self) -> io::Result<()> { 454 | let timeout = Instant::now() + self.timeout; 455 | loop { 456 | return match nix::sys::termios::tcdrain(self.fd) { 457 | Ok(_) => Ok(()), 458 | Err(nix::errno::Errno::EINTR) => { 459 | // Retry flushing. But only up to the ports timeout for not retrying 460 | // indefinitely in case that it gets interrupted again. 461 | if Instant::now() < timeout { 462 | continue; 463 | } else { 464 | Err(io::Error::new( 465 | io::ErrorKind::TimedOut, 466 | "timeout for retrying flush reached", 467 | )) 468 | } 469 | } 470 | Err(_) => Err(io::Error::new(io::ErrorKind::Other, "flush failed")), 471 | }; 472 | } 473 | } 474 | } 475 | 476 | impl SerialPort for TTYPort { 477 | fn name(&self) -> Option { 478 | self.port_name.clone() 479 | } 480 | 481 | /// Returns the port's baud rate 482 | /// 483 | /// On some platforms this will be the actual device baud rate, which may differ from the 484 | /// desired baud rate. 485 | #[cfg(any( 486 | target_os = "android", 487 | all( 488 | target_os = "linux", 489 | not(any( 490 | target_env = "musl", 491 | target_arch = "powerpc", 492 | target_arch = "powerpc64" 493 | )) 494 | ) 495 | ))] 496 | fn baud_rate(&self) -> Result { 497 | let termios2 = ioctl::tcgets2(self.fd)?; 498 | 499 | assert!(termios2.c_ospeed == termios2.c_ispeed); 500 | 501 | Ok(termios2.c_ospeed) 502 | } 503 | 504 | /// Returns the port's baud rate 505 | /// 506 | /// On some platforms this will be the actual device baud rate, which may differ from the 507 | /// desired baud rate. 508 | #[cfg(any( 509 | target_os = "dragonfly", 510 | target_os = "freebsd", 511 | target_os = "netbsd", 512 | target_os = "openbsd" 513 | ))] 514 | fn baud_rate(&self) -> Result { 515 | let termios = termios::get_termios(self.fd)?; 516 | 517 | let ospeed = unsafe { libc::cfgetospeed(&termios) }; 518 | let ispeed = unsafe { libc::cfgetispeed(&termios) }; 519 | 520 | assert!(ospeed == ispeed); 521 | 522 | Ok(ospeed as u32) 523 | } 524 | 525 | /// Returns the port's baud rate 526 | /// 527 | /// On some platforms this will be the actual device baud rate, which may differ from the 528 | /// desired baud rate. 529 | #[cfg(any(target_os = "ios", target_os = "macos"))] 530 | fn baud_rate(&self) -> Result { 531 | Ok(self.baud_rate) 532 | } 533 | 534 | /// Returns the port's baud rate 535 | /// 536 | /// On some platforms this will be the actual device baud rate, which may differ from the 537 | /// desired baud rate. 538 | #[cfg(all( 539 | target_os = "linux", 540 | any( 541 | target_env = "musl", 542 | target_arch = "powerpc", 543 | target_arch = "powerpc64" 544 | ) 545 | ))] 546 | fn baud_rate(&self) -> Result { 547 | use self::libc::{ 548 | B1000000, B1152000, B1500000, B2000000, B2500000, B3000000, B3500000, B4000000, 549 | B460800, B500000, B576000, B921600, 550 | }; 551 | use self::libc::{ 552 | B110, B115200, B1200, B134, B150, B1800, B19200, B200, B230400, B2400, B300, B38400, 553 | B4800, B50, B57600, B600, B75, B9600, 554 | }; 555 | 556 | let termios = termios::get_termios(self.fd)?; 557 | let ospeed = unsafe { libc::cfgetospeed(&termios) }; 558 | let ispeed = unsafe { libc::cfgetispeed(&termios) }; 559 | 560 | assert!(ospeed == ispeed); 561 | 562 | let res: u32 = match ospeed { 563 | B50 => 50, 564 | B75 => 75, 565 | B110 => 110, 566 | B134 => 134, 567 | B150 => 150, 568 | B200 => 200, 569 | B300 => 300, 570 | B600 => 600, 571 | B1200 => 1200, 572 | B1800 => 1800, 573 | B2400 => 2400, 574 | B4800 => 4800, 575 | B9600 => 9600, 576 | B19200 => 19_200, 577 | B38400 => 38_400, 578 | B57600 => 57_600, 579 | B115200 => 115_200, 580 | B230400 => 230_400, 581 | B460800 => 460_800, 582 | B500000 => 500_000, 583 | B576000 => 576_000, 584 | B921600 => 921_600, 585 | B1000000 => 1_000_000, 586 | B1152000 => 1_152_000, 587 | B1500000 => 1_500_000, 588 | B2000000 => 2_000_000, 589 | B2500000 => 2_500_000, 590 | B3000000 => 3_000_000, 591 | B3500000 => 3_500_000, 592 | B4000000 => 4_000_000, 593 | _ => unreachable!(), 594 | }; 595 | 596 | Ok(res) 597 | } 598 | 599 | fn data_bits(&self) -> Result { 600 | let termios = termios::get_termios(self.fd)?; 601 | match termios.c_cflag & libc::CSIZE { 602 | libc::CS8 => Ok(DataBits::Eight), 603 | libc::CS7 => Ok(DataBits::Seven), 604 | libc::CS6 => Ok(DataBits::Six), 605 | libc::CS5 => Ok(DataBits::Five), 606 | _ => Err(Error::new( 607 | ErrorKind::Unknown, 608 | "Invalid data bits setting encountered", 609 | )), 610 | } 611 | } 612 | 613 | fn flow_control(&self) -> Result { 614 | let termios = termios::get_termios(self.fd)?; 615 | if termios.c_cflag & libc::CRTSCTS == libc::CRTSCTS { 616 | Ok(FlowControl::Hardware) 617 | } else if termios.c_iflag & (libc::IXON | libc::IXOFF) == (libc::IXON | libc::IXOFF) { 618 | Ok(FlowControl::Software) 619 | } else { 620 | Ok(FlowControl::None) 621 | } 622 | } 623 | 624 | fn parity(&self) -> Result { 625 | let termios = termios::get_termios(self.fd)?; 626 | if termios.c_cflag & libc::PARENB == libc::PARENB { 627 | if termios.c_cflag & libc::PARODD == libc::PARODD { 628 | Ok(Parity::Odd) 629 | } else { 630 | Ok(Parity::Even) 631 | } 632 | } else { 633 | Ok(Parity::None) 634 | } 635 | } 636 | 637 | fn stop_bits(&self) -> Result { 638 | let termios = termios::get_termios(self.fd)?; 639 | if termios.c_cflag & libc::CSTOPB == libc::CSTOPB { 640 | Ok(StopBits::Two) 641 | } else { 642 | Ok(StopBits::One) 643 | } 644 | } 645 | 646 | fn timeout(&self) -> Duration { 647 | self.timeout 648 | } 649 | 650 | #[cfg(any( 651 | target_os = "android", 652 | target_os = "dragonfly", 653 | target_os = "freebsd", 654 | target_os = "netbsd", 655 | target_os = "openbsd", 656 | target_os = "linux" 657 | ))] 658 | fn set_baud_rate(&mut self, baud_rate: u32) -> Result<()> { 659 | let mut termios = termios::get_termios(self.fd)?; 660 | termios::set_baud_rate(&mut termios, baud_rate)?; 661 | termios::set_termios(self.fd, &termios) 662 | } 663 | 664 | // Mac OS needs special logic for setting arbitrary baud rates. 665 | #[cfg(any(target_os = "ios", target_os = "macos"))] 666 | fn set_baud_rate(&mut self, baud_rate: u32) -> Result<()> { 667 | ioctl::iossiospeed(self.fd, &(baud_rate as libc::speed_t))?; 668 | self.baud_rate = baud_rate; 669 | Ok(()) 670 | } 671 | 672 | fn set_flow_control(&mut self, flow_control: FlowControl) -> Result<()> { 673 | let mut termios = termios::get_termios(self.fd)?; 674 | termios::set_flow_control(&mut termios, flow_control); 675 | #[cfg(any(target_os = "ios", target_os = "macos"))] 676 | return termios::set_termios(self.fd, &termios, self.baud_rate); 677 | #[cfg(not(any(target_os = "ios", target_os = "macos")))] 678 | return termios::set_termios(self.fd, &termios); 679 | } 680 | 681 | fn set_parity(&mut self, parity: Parity) -> Result<()> { 682 | let mut termios = termios::get_termios(self.fd)?; 683 | termios::set_parity(&mut termios, parity); 684 | #[cfg(any(target_os = "ios", target_os = "macos"))] 685 | return termios::set_termios(self.fd, &termios, self.baud_rate); 686 | #[cfg(not(any(target_os = "ios", target_os = "macos")))] 687 | return termios::set_termios(self.fd, &termios); 688 | } 689 | 690 | fn set_data_bits(&mut self, data_bits: DataBits) -> Result<()> { 691 | let mut termios = termios::get_termios(self.fd)?; 692 | termios::set_data_bits(&mut termios, data_bits); 693 | #[cfg(any(target_os = "ios", target_os = "macos"))] 694 | return termios::set_termios(self.fd, &termios, self.baud_rate); 695 | #[cfg(not(any(target_os = "ios", target_os = "macos")))] 696 | return termios::set_termios(self.fd, &termios); 697 | } 698 | 699 | fn set_stop_bits(&mut self, stop_bits: StopBits) -> Result<()> { 700 | let mut termios = termios::get_termios(self.fd)?; 701 | termios::set_stop_bits(&mut termios, stop_bits); 702 | #[cfg(any(target_os = "ios", target_os = "macos"))] 703 | return termios::set_termios(self.fd, &termios, self.baud_rate); 704 | #[cfg(not(any(target_os = "ios", target_os = "macos")))] 705 | return termios::set_termios(self.fd, &termios); 706 | } 707 | 708 | fn set_timeout(&mut self, timeout: Duration) -> Result<()> { 709 | self.timeout = timeout; 710 | Ok(()) 711 | } 712 | 713 | fn write_request_to_send(&mut self, level: bool) -> Result<()> { 714 | self.set_pin(SerialLines::REQUEST_TO_SEND, level) 715 | } 716 | 717 | fn write_data_terminal_ready(&mut self, level: bool) -> Result<()> { 718 | self.set_pin(SerialLines::DATA_TERMINAL_READY, level) 719 | } 720 | 721 | fn read_clear_to_send(&mut self) -> Result { 722 | self.read_pin(SerialLines::CLEAR_TO_SEND) 723 | } 724 | 725 | fn read_data_set_ready(&mut self) -> Result { 726 | self.read_pin(SerialLines::DATA_SET_READY) 727 | } 728 | 729 | fn read_ring_indicator(&mut self) -> Result { 730 | self.read_pin(SerialLines::RING) 731 | } 732 | 733 | fn read_carrier_detect(&mut self) -> Result { 734 | self.read_pin(SerialLines::DATA_CARRIER_DETECT) 735 | } 736 | 737 | fn bytes_to_read(&self) -> Result { 738 | ioctl::fionread(self.fd) 739 | } 740 | 741 | fn bytes_to_write(&self) -> Result { 742 | ioctl::tiocoutq(self.fd) 743 | } 744 | 745 | fn clear(&self, buffer_to_clear: ClearBuffer) -> Result<()> { 746 | let buffer_id = match buffer_to_clear { 747 | ClearBuffer::Input => libc::TCIFLUSH, 748 | ClearBuffer::Output => libc::TCOFLUSH, 749 | ClearBuffer::All => libc::TCIOFLUSH, 750 | }; 751 | 752 | let res = unsafe { nix::libc::tcflush(self.fd, buffer_id) }; 753 | 754 | nix::errno::Errno::result(res) 755 | .map(|_| ()) 756 | .map_err(|e| e.into()) 757 | } 758 | 759 | fn try_clone(&self) -> Result> { 760 | match self.try_clone_native() { 761 | Ok(p) => Ok(Box::new(p)), 762 | Err(e) => Err(e), 763 | } 764 | } 765 | 766 | fn set_break(&self) -> Result<()> { 767 | ioctl::tiocsbrk(self.fd) 768 | } 769 | 770 | fn clear_break(&self) -> Result<()> { 771 | ioctl::tioccbrk(self.fd) 772 | } 773 | } 774 | 775 | #[test] 776 | fn test_ttyport_into_raw_fd() { 777 | // `master` must be used here as Dropping it causes slave to be deleted by the OS. 778 | // TODO: Convert this to a statement-level attribute once 779 | // https://github.com/rust-lang/rust/issues/15701 is on stable. 780 | // FIXME: Create a mutex across all tests for using `TTYPort::pair()` as it's not threadsafe 781 | #![allow(unused_variables)] 782 | let (master, slave) = TTYPort::pair().expect("Unable to create ptty pair"); 783 | 784 | // First test with the master 785 | let master_fd = master.into_raw_fd(); 786 | let mut termios = MaybeUninit::uninit(); 787 | let res = unsafe { nix::libc::tcgetattr(master_fd, termios.as_mut_ptr()) }; 788 | if res != 0 { 789 | close(master_fd); 790 | panic!("tcgetattr on the master port failed"); 791 | } 792 | 793 | // And then the slave 794 | let slave_fd = slave.into_raw_fd(); 795 | let res = unsafe { nix::libc::tcgetattr(slave_fd, termios.as_mut_ptr()) }; 796 | if res != 0 { 797 | close(slave_fd); 798 | panic!("tcgetattr on the master port failed"); 799 | } 800 | close(master_fd); 801 | close(slave_fd); 802 | } 803 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use cfg_if::cfg_if; 2 | 3 | cfg_if! { 4 | if #[cfg(test)] { 5 | pub(crate) mod timeout; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/tests/timeout.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | /// A sequence of strongly monotonic inrceasing durations. Introduced for testing conversions from 4 | /// `Duration` to platform-specific types. 5 | pub(crate) const MONOTONIC_DURATIONS: [Duration; 17] = [ 6 | Duration::ZERO, 7 | Duration::from_nanos(1), 8 | Duration::from_millis(1), 9 | Duration::from_secs(1), 10 | Duration::from_secs(i16::MAX as u64 - 1), 11 | Duration::from_secs(i16::MAX as u64), 12 | Duration::from_secs(i16::MAX as u64 + 1), 13 | Duration::from_secs(i32::MAX as u64 - 1), 14 | Duration::from_secs(i32::MAX as u64), 15 | Duration::from_secs(i32::MAX as u64 + 1), 16 | Duration::from_secs(i64::MAX as u64 - 1), 17 | Duration::from_secs(i64::MAX as u64), 18 | Duration::from_secs(i64::MAX as u64 + 1), 19 | Duration::from_secs(u64::MAX - 1), 20 | Duration::from_secs(u64::MAX), 21 | Duration::new(u64::MAX, 1_000_000), 22 | Duration::MAX, 23 | ]; 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::*; 28 | 29 | #[test] 30 | fn basic_durations_properties() { 31 | assert_eq!(Duration::ZERO, *MONOTONIC_DURATIONS.first().unwrap()); 32 | assert_eq!(Duration::MAX, *MONOTONIC_DURATIONS.last().unwrap()); 33 | 34 | // Check that this array is monotonic. 35 | let mut last = MONOTONIC_DURATIONS[0]; 36 | for next in MONOTONIC_DURATIONS { 37 | assert!(last <= next); 38 | last = next; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/windows/com.rs: -------------------------------------------------------------------------------- 1 | use std::mem::MaybeUninit; 2 | use std::os::windows::prelude::*; 3 | use std::time::Duration; 4 | use std::{io, ptr}; 5 | 6 | use winapi::shared::minwindef::*; 7 | use winapi::um::commapi::*; 8 | use winapi::um::fileapi::*; 9 | use winapi::um::handleapi::*; 10 | use winapi::um::processthreadsapi::GetCurrentProcess; 11 | use winapi::um::winbase::*; 12 | use winapi::um::winnt::{ 13 | DUPLICATE_SAME_ACCESS, FILE_ATTRIBUTE_NORMAL, GENERIC_READ, GENERIC_WRITE, HANDLE, MAXDWORD, 14 | }; 15 | 16 | use crate::windows::dcb; 17 | use crate::{ 18 | ClearBuffer, DataBits, Error, ErrorKind, FlowControl, Parity, Result, SerialPort, 19 | SerialPortBuilder, StopBits, 20 | }; 21 | 22 | /// A serial port implementation for Windows COM ports 23 | /// 24 | /// The port will be closed when the value is dropped. However, this struct 25 | /// should not be instantiated directly by using `COMPort::open()`, instead use 26 | /// the cross-platform `serialport::open()` or 27 | /// `serialport::open_with_settings()`. 28 | #[derive(Debug)] 29 | pub struct COMPort { 30 | handle: HANDLE, 31 | timeout: Duration, 32 | port_name: Option, 33 | } 34 | 35 | unsafe impl Send for COMPort {} 36 | 37 | impl COMPort { 38 | /// Opens a COM port as a serial device. 39 | /// 40 | /// `port` should be the name of a COM port, e.g., `COM1`. 41 | /// 42 | /// If the COM port handle needs to be opened with special flags, use 43 | /// `from_raw_handle` method to create the `COMPort`. Note that you should 44 | /// set the different settings before using the serial port using `set_all`. 45 | /// 46 | /// ## Errors 47 | /// 48 | /// * `NoDevice` if the device could not be opened. This could indicate that 49 | /// the device is already in use. 50 | /// * `InvalidInput` if `port` is not a valid device name. 51 | /// * `Io` for any other I/O error while opening or initializing the device. 52 | pub fn open(builder: &SerialPortBuilder) -> Result { 53 | let mut name = Vec::::with_capacity(4 + builder.path.len() + 1); 54 | 55 | name.extend(r"\\.\".encode_utf16()); 56 | name.extend(builder.path.encode_utf16()); 57 | name.push(0); 58 | 59 | let handle = unsafe { 60 | CreateFileW( 61 | name.as_ptr(), 62 | GENERIC_READ | GENERIC_WRITE, 63 | 0, 64 | ptr::null_mut(), 65 | OPEN_EXISTING, 66 | FILE_ATTRIBUTE_NORMAL, 67 | 0 as HANDLE, 68 | ) 69 | }; 70 | 71 | if handle == INVALID_HANDLE_VALUE { 72 | return Err(super::error::last_os_error()); 73 | } 74 | 75 | // create the COMPort here so the handle is getting closed 76 | // if one of the calls to `get_dcb()` or `set_dcb()` fails 77 | let mut com = COMPort::open_from_raw_handle(handle as RawHandle); 78 | 79 | let mut dcb = dcb::get_dcb(handle)?; 80 | dcb::init(&mut dcb); 81 | dcb::set_baud_rate(&mut dcb, builder.baud_rate); 82 | dcb::set_data_bits(&mut dcb, builder.data_bits); 83 | dcb::set_parity(&mut dcb, builder.parity); 84 | dcb::set_stop_bits(&mut dcb, builder.stop_bits); 85 | dcb::set_flow_control(&mut dcb, builder.flow_control); 86 | dcb::set_dcb(handle, dcb)?; 87 | 88 | // Try to set DTR on best-effort. 89 | if let Some(dtr) = builder.dtr_on_open { 90 | let _ = com.write_data_terminal_ready(dtr); 91 | } 92 | 93 | com.set_timeout(builder.timeout)?; 94 | com.port_name = Some(builder.path.clone()); 95 | Ok(com) 96 | } 97 | 98 | /// Attempts to clone the `SerialPort`. This allow you to write and read simultaneously from the 99 | /// same serial connection. Please note that if you want a real asynchronous serial port you 100 | /// should look at [mio-serial](https://crates.io/crates/mio-serial) or 101 | /// [tokio-serial](https://crates.io/crates/tokio-serial). 102 | /// 103 | /// Also, you must be very careful when changing the settings of a cloned `SerialPort` : since 104 | /// the settings are cached on a per object basis, trying to modify them from two different 105 | /// objects can cause some nasty behavior. 106 | /// 107 | /// This is the same as `SerialPort::try_clone()` but returns the concrete type instead. 108 | /// 109 | /// # Errors 110 | /// 111 | /// This function returns an error if the serial port couldn't be cloned. 112 | pub fn try_clone_native(&self) -> Result { 113 | let process_handle: HANDLE = unsafe { GetCurrentProcess() }; 114 | let mut cloned_handle: HANDLE = INVALID_HANDLE_VALUE; 115 | unsafe { 116 | DuplicateHandle( 117 | process_handle, 118 | self.handle, 119 | process_handle, 120 | &mut cloned_handle, 121 | 0, 122 | TRUE, 123 | DUPLICATE_SAME_ACCESS, 124 | ); 125 | if cloned_handle != INVALID_HANDLE_VALUE { 126 | Ok(COMPort { 127 | handle: cloned_handle, 128 | port_name: self.port_name.clone(), 129 | timeout: self.timeout, 130 | }) 131 | } else { 132 | Err(super::error::last_os_error()) 133 | } 134 | } 135 | } 136 | 137 | fn escape_comm_function(&mut self, function: DWORD) -> Result<()> { 138 | match unsafe { EscapeCommFunction(self.handle, function) } { 139 | 0 => Err(super::error::last_os_error()), 140 | _ => Ok(()), 141 | } 142 | } 143 | 144 | fn read_pin(&mut self, pin: DWORD) -> Result { 145 | let mut status: DWORD = 0; 146 | 147 | match unsafe { GetCommModemStatus(self.handle, &mut status) } { 148 | 0 => Err(super::error::last_os_error()), 149 | _ => Ok(status & pin != 0), 150 | } 151 | } 152 | 153 | fn open_from_raw_handle(handle: RawHandle) -> Self { 154 | // It is not trivial to get the file path corresponding to a handle. 155 | // We'll punt and set it `None` here. 156 | COMPort { 157 | handle: handle as HANDLE, 158 | timeout: Duration::from_millis(100), 159 | port_name: None, 160 | } 161 | } 162 | 163 | fn timeout_constant(duration: Duration) -> DWORD { 164 | let milliseconds = duration.as_millis(); 165 | // In the way we are setting up COMMTIMEOUTS, a timeout_constant of MAXDWORD gets rejected. 166 | // Let's clamp the timeout constant for values of MAXDWORD and above. See remarks at 167 | // https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-commtimeouts. 168 | // 169 | // This effectively throws away accuracy for really long timeouts but at least preserves a 170 | // long-ish timeout. But just casting to DWORD would result in presumably unexpected short 171 | // and non-monotonic timeouts from cutting off the higher bits. 172 | u128::min(milliseconds, MAXDWORD as u128 - 1) as DWORD 173 | } 174 | } 175 | 176 | impl Drop for COMPort { 177 | fn drop(&mut self) { 178 | unsafe { 179 | CloseHandle(self.handle); 180 | } 181 | } 182 | } 183 | 184 | impl AsRawHandle for COMPort { 185 | fn as_raw_handle(&self) -> RawHandle { 186 | self.handle as RawHandle 187 | } 188 | } 189 | 190 | impl FromRawHandle for COMPort { 191 | unsafe fn from_raw_handle(handle: RawHandle) -> Self { 192 | COMPort::open_from_raw_handle(handle) 193 | } 194 | } 195 | 196 | impl IntoRawHandle for COMPort { 197 | fn into_raw_handle(self) -> RawHandle { 198 | let Self { handle, .. } = self; 199 | 200 | handle as RawHandle 201 | } 202 | } 203 | 204 | impl io::Read for COMPort { 205 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 206 | let mut len: DWORD = 0; 207 | 208 | match unsafe { 209 | ReadFile( 210 | self.handle, 211 | buf.as_mut_ptr() as LPVOID, 212 | buf.len() as DWORD, 213 | &mut len, 214 | ptr::null_mut(), 215 | ) 216 | } { 217 | 0 => Err(io::Error::last_os_error()), 218 | _ => { 219 | if len != 0 { 220 | Ok(len as usize) 221 | } else { 222 | Err(io::Error::new( 223 | io::ErrorKind::TimedOut, 224 | "Operation timed out", 225 | )) 226 | } 227 | } 228 | } 229 | } 230 | } 231 | 232 | impl io::Write for COMPort { 233 | fn write(&mut self, buf: &[u8]) -> io::Result { 234 | let mut len: DWORD = 0; 235 | 236 | match unsafe { 237 | WriteFile( 238 | self.handle, 239 | buf.as_ptr() as LPVOID, 240 | buf.len() as DWORD, 241 | &mut len, 242 | ptr::null_mut(), 243 | ) 244 | } { 245 | 0 => Err(io::Error::last_os_error()), 246 | _ => Ok(len as usize), 247 | } 248 | } 249 | 250 | fn flush(&mut self) -> io::Result<()> { 251 | match unsafe { FlushFileBuffers(self.handle) } { 252 | 0 => Err(io::Error::last_os_error()), 253 | _ => Ok(()), 254 | } 255 | } 256 | } 257 | 258 | impl SerialPort for COMPort { 259 | fn name(&self) -> Option { 260 | self.port_name.clone() 261 | } 262 | 263 | fn timeout(&self) -> Duration { 264 | self.timeout 265 | } 266 | 267 | fn set_timeout(&mut self, timeout: Duration) -> Result<()> { 268 | let timeout_constant = Self::timeout_constant(timeout); 269 | 270 | let mut timeouts = COMMTIMEOUTS { 271 | ReadIntervalTimeout: MAXDWORD, 272 | ReadTotalTimeoutMultiplier: MAXDWORD, 273 | ReadTotalTimeoutConstant: timeout_constant, 274 | WriteTotalTimeoutMultiplier: 0, 275 | WriteTotalTimeoutConstant: timeout_constant, 276 | }; 277 | 278 | if unsafe { SetCommTimeouts(self.handle, &mut timeouts) } == 0 { 279 | return Err(super::error::last_os_error()); 280 | } 281 | 282 | self.timeout = timeout; 283 | Ok(()) 284 | } 285 | 286 | fn write_request_to_send(&mut self, level: bool) -> Result<()> { 287 | if level { 288 | self.escape_comm_function(SETRTS) 289 | } else { 290 | self.escape_comm_function(CLRRTS) 291 | } 292 | } 293 | 294 | fn write_data_terminal_ready(&mut self, level: bool) -> Result<()> { 295 | if level { 296 | self.escape_comm_function(SETDTR) 297 | } else { 298 | self.escape_comm_function(CLRDTR) 299 | } 300 | } 301 | 302 | fn read_clear_to_send(&mut self) -> Result { 303 | self.read_pin(MS_CTS_ON) 304 | } 305 | 306 | fn read_data_set_ready(&mut self) -> Result { 307 | self.read_pin(MS_DSR_ON) 308 | } 309 | 310 | fn read_ring_indicator(&mut self) -> Result { 311 | self.read_pin(MS_RING_ON) 312 | } 313 | 314 | fn read_carrier_detect(&mut self) -> Result { 315 | self.read_pin(MS_RLSD_ON) 316 | } 317 | 318 | fn baud_rate(&self) -> Result { 319 | let dcb = dcb::get_dcb(self.handle)?; 320 | Ok(dcb.BaudRate as u32) 321 | } 322 | 323 | fn data_bits(&self) -> Result { 324 | let dcb = dcb::get_dcb(self.handle)?; 325 | match dcb.ByteSize { 326 | 5 => Ok(DataBits::Five), 327 | 6 => Ok(DataBits::Six), 328 | 7 => Ok(DataBits::Seven), 329 | 8 => Ok(DataBits::Eight), 330 | _ => Err(Error::new( 331 | ErrorKind::Unknown, 332 | "Invalid data bits setting encountered", 333 | )), 334 | } 335 | } 336 | 337 | fn parity(&self) -> Result { 338 | let dcb = dcb::get_dcb(self.handle)?; 339 | match dcb.Parity { 340 | ODDPARITY => Ok(Parity::Odd), 341 | EVENPARITY => Ok(Parity::Even), 342 | NOPARITY => Ok(Parity::None), 343 | _ => Err(Error::new( 344 | ErrorKind::Unknown, 345 | "Invalid parity bits setting encountered", 346 | )), 347 | } 348 | } 349 | 350 | fn stop_bits(&self) -> Result { 351 | let dcb = dcb::get_dcb(self.handle)?; 352 | match dcb.StopBits { 353 | TWOSTOPBITS => Ok(StopBits::Two), 354 | ONESTOPBIT => Ok(StopBits::One), 355 | _ => Err(Error::new( 356 | ErrorKind::Unknown, 357 | "Invalid stop bits setting encountered", 358 | )), 359 | } 360 | } 361 | 362 | fn flow_control(&self) -> Result { 363 | let dcb = dcb::get_dcb(self.handle)?; 364 | if dcb.fOutxCtsFlow() != 0 || dcb.fRtsControl() != 0 { 365 | Ok(FlowControl::Hardware) 366 | } else if dcb.fOutX() != 0 || dcb.fInX() != 0 { 367 | Ok(FlowControl::Software) 368 | } else { 369 | Ok(FlowControl::None) 370 | } 371 | } 372 | 373 | fn set_baud_rate(&mut self, baud_rate: u32) -> Result<()> { 374 | let mut dcb = dcb::get_dcb(self.handle)?; 375 | dcb::set_baud_rate(&mut dcb, baud_rate); 376 | dcb::set_dcb(self.handle, dcb) 377 | } 378 | 379 | fn set_data_bits(&mut self, data_bits: DataBits) -> Result<()> { 380 | let mut dcb = dcb::get_dcb(self.handle)?; 381 | dcb::set_data_bits(&mut dcb, data_bits); 382 | dcb::set_dcb(self.handle, dcb) 383 | } 384 | 385 | fn set_parity(&mut self, parity: Parity) -> Result<()> { 386 | let mut dcb = dcb::get_dcb(self.handle)?; 387 | dcb::set_parity(&mut dcb, parity); 388 | dcb::set_dcb(self.handle, dcb) 389 | } 390 | 391 | fn set_stop_bits(&mut self, stop_bits: StopBits) -> Result<()> { 392 | let mut dcb = dcb::get_dcb(self.handle)?; 393 | dcb::set_stop_bits(&mut dcb, stop_bits); 394 | dcb::set_dcb(self.handle, dcb) 395 | } 396 | 397 | fn set_flow_control(&mut self, flow_control: FlowControl) -> Result<()> { 398 | let mut dcb = dcb::get_dcb(self.handle)?; 399 | dcb::set_flow_control(&mut dcb, flow_control); 400 | dcb::set_dcb(self.handle, dcb) 401 | } 402 | 403 | fn bytes_to_read(&self) -> Result { 404 | let mut errors: DWORD = 0; 405 | let mut comstat = MaybeUninit::uninit(); 406 | 407 | if unsafe { ClearCommError(self.handle, &mut errors, comstat.as_mut_ptr()) != 0 } { 408 | unsafe { Ok(comstat.assume_init().cbInQue) } 409 | } else { 410 | Err(super::error::last_os_error()) 411 | } 412 | } 413 | 414 | fn bytes_to_write(&self) -> Result { 415 | let mut errors: DWORD = 0; 416 | let mut comstat = MaybeUninit::uninit(); 417 | 418 | if unsafe { ClearCommError(self.handle, &mut errors, comstat.as_mut_ptr()) != 0 } { 419 | unsafe { Ok(comstat.assume_init().cbOutQue) } 420 | } else { 421 | Err(super::error::last_os_error()) 422 | } 423 | } 424 | 425 | fn clear(&self, buffer_to_clear: ClearBuffer) -> Result<()> { 426 | let buffer_flags = match buffer_to_clear { 427 | ClearBuffer::Input => PURGE_RXABORT | PURGE_RXCLEAR, 428 | ClearBuffer::Output => PURGE_TXABORT | PURGE_TXCLEAR, 429 | ClearBuffer::All => PURGE_RXABORT | PURGE_RXCLEAR | PURGE_TXABORT | PURGE_TXCLEAR, 430 | }; 431 | 432 | if unsafe { PurgeComm(self.handle, buffer_flags) != 0 } { 433 | Ok(()) 434 | } else { 435 | Err(super::error::last_os_error()) 436 | } 437 | } 438 | 439 | fn try_clone(&self) -> Result> { 440 | match self.try_clone_native() { 441 | Ok(p) => Ok(Box::new(p)), 442 | Err(e) => Err(e), 443 | } 444 | } 445 | 446 | fn set_break(&self) -> Result<()> { 447 | if unsafe { SetCommBreak(self.handle) != 0 } { 448 | Ok(()) 449 | } else { 450 | Err(super::error::last_os_error()) 451 | } 452 | } 453 | 454 | fn clear_break(&self) -> Result<()> { 455 | if unsafe { ClearCommBreak(self.handle) != 0 } { 456 | Ok(()) 457 | } else { 458 | Err(super::error::last_os_error()) 459 | } 460 | } 461 | } 462 | 463 | #[cfg(test)] 464 | mod tests { 465 | use super::*; 466 | use crate::tests::timeout::MONOTONIC_DURATIONS; 467 | 468 | #[test] 469 | fn timeout_constant_is_monotonic() { 470 | let mut last = COMPort::timeout_constant(Duration::ZERO); 471 | 472 | for (i, d) in MONOTONIC_DURATIONS.iter().enumerate() { 473 | let next = COMPort::timeout_constant(*d); 474 | assert!( 475 | next >= last, 476 | "{next} >= {last} failed for {d:?} at index {i}" 477 | ); 478 | last = next; 479 | } 480 | } 481 | 482 | #[test] 483 | fn timeout_constant_zero_is_zero() { 484 | assert_eq!(0, COMPort::timeout_constant(Duration::ZERO)); 485 | } 486 | } 487 | -------------------------------------------------------------------------------- /src/windows/dcb.rs: -------------------------------------------------------------------------------- 1 | use std::mem::MaybeUninit; 2 | use winapi::shared::minwindef::*; 3 | use winapi::um::commapi::*; 4 | use winapi::um::winbase::*; 5 | use winapi::um::winnt::HANDLE; 6 | 7 | use crate::{DataBits, FlowControl, Parity, Result, StopBits}; 8 | 9 | pub(crate) fn get_dcb(handle: HANDLE) -> Result { 10 | let mut dcb: DCB = unsafe { MaybeUninit::zeroed().assume_init() }; 11 | dcb.DCBlength = std::mem::size_of::() as u32; 12 | 13 | if unsafe { GetCommState(handle, &mut dcb) } != 0 { 14 | Ok(dcb) 15 | } else { 16 | Err(super::error::last_os_error()) 17 | } 18 | } 19 | 20 | /// Initialize the DCB struct 21 | /// Set all values that won't be affected by `SerialPortBuilder` options. 22 | pub(crate) fn init(dcb: &mut DCB) { 23 | // dcb.DCBlength 24 | // dcb.BaudRate 25 | // dcb.BitFields 26 | // dcb.wReserved 27 | // dcb.XonLim 28 | // dcb.XoffLim 29 | // dcb.ByteSize 30 | // dcb.Parity 31 | // dcb.StopBits 32 | dcb.XonChar = 17; 33 | dcb.XoffChar = 19; 34 | dcb.ErrorChar = '\0' as winapi::ctypes::c_char; 35 | dcb.EofChar = 26; 36 | // dcb.EvtChar 37 | // always true for communications resources 38 | dcb.set_fBinary(TRUE as DWORD); 39 | // dcb.set_fParity() 40 | // dcb.set_fOutxCtsFlow() 41 | // serialport-rs doesn't support toggling DSR: so disable fOutxDsrFlow 42 | dcb.set_fOutxDsrFlow(FALSE as DWORD); 43 | dcb.set_fDtrControl(DTR_CONTROL_DISABLE); 44 | // disable because fOutxDsrFlow is disabled as well 45 | dcb.set_fDsrSensitivity(FALSE as DWORD); 46 | // dcb.set_fTXContinueOnXoff() 47 | // dcb.set_fOutX() 48 | // dcb.set_fInX() 49 | dcb.set_fErrorChar(FALSE as DWORD); 50 | // fNull: when set to TRUE null bytes are discarded when received. 51 | // null bytes won't be discarded by serialport-rs 52 | dcb.set_fNull(FALSE as DWORD); 53 | // dcb.set_fRtsControl() 54 | // serialport-rs does not handle the fAbortOnError behaviour, so we must make sure it's not enabled 55 | dcb.set_fAbortOnError(FALSE as DWORD); 56 | } 57 | 58 | pub(crate) fn set_dcb(handle: HANDLE, mut dcb: DCB) -> Result<()> { 59 | if unsafe { SetCommState(handle, &mut dcb as *mut _) != 0 } { 60 | Ok(()) 61 | } else { 62 | Err(super::error::last_os_error()) 63 | } 64 | } 65 | 66 | pub(crate) fn set_baud_rate(dcb: &mut DCB, baud_rate: u32) { 67 | dcb.BaudRate = baud_rate as DWORD; 68 | } 69 | 70 | pub(crate) fn set_data_bits(dcb: &mut DCB, data_bits: DataBits) { 71 | dcb.ByteSize = match data_bits { 72 | DataBits::Five => 5, 73 | DataBits::Six => 6, 74 | DataBits::Seven => 7, 75 | DataBits::Eight => 8, 76 | }; 77 | } 78 | 79 | pub(crate) fn set_parity(dcb: &mut DCB, parity: Parity) { 80 | dcb.Parity = match parity { 81 | Parity::None => NOPARITY, 82 | Parity::Odd => ODDPARITY, 83 | Parity::Even => EVENPARITY, 84 | }; 85 | 86 | dcb.set_fParity(if parity == Parity::None { FALSE } else { TRUE } as DWORD); 87 | } 88 | 89 | pub(crate) fn set_stop_bits(dcb: &mut DCB, stop_bits: StopBits) { 90 | dcb.StopBits = match stop_bits { 91 | StopBits::One => ONESTOPBIT, 92 | StopBits::Two => TWOSTOPBITS, 93 | }; 94 | } 95 | 96 | pub(crate) fn set_flow_control(dcb: &mut DCB, flow_control: FlowControl) { 97 | match flow_control { 98 | FlowControl::None => { 99 | dcb.set_fOutxCtsFlow(0); 100 | dcb.set_fRtsControl(0); 101 | dcb.set_fOutX(0); 102 | dcb.set_fInX(0); 103 | } 104 | FlowControl::Software => { 105 | dcb.set_fOutxCtsFlow(0); 106 | dcb.set_fRtsControl(0); 107 | dcb.set_fOutX(1); 108 | dcb.set_fInX(1); 109 | } 110 | FlowControl::Hardware => { 111 | dcb.set_fOutxCtsFlow(1); 112 | dcb.set_fRtsControl(1); 113 | dcb.set_fOutX(0); 114 | dcb.set_fInX(0); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/windows/error.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::ptr; 3 | 4 | use winapi::shared::minwindef::DWORD; 5 | use winapi::shared::winerror::*; 6 | use winapi::um::errhandlingapi::GetLastError; 7 | use winapi::um::winbase::{ 8 | FormatMessageW, FORMAT_MESSAGE_FROM_SYSTEM, FORMAT_MESSAGE_IGNORE_INSERTS, 9 | }; 10 | use winapi::um::winnt::{LANG_SYSTEM_DEFAULT, MAKELANGID, SUBLANG_SYS_DEFAULT, WCHAR}; 11 | 12 | use crate::{Error, ErrorKind}; 13 | 14 | pub fn last_os_error() -> Error { 15 | let errno = errno(); 16 | 17 | let kind = match errno { 18 | ERROR_FILE_NOT_FOUND | ERROR_PATH_NOT_FOUND | ERROR_ACCESS_DENIED => ErrorKind::NoDevice, 19 | _ => ErrorKind::Io(io::ErrorKind::Other), 20 | }; 21 | 22 | Error::new(kind, error_string(errno).trim()) 23 | } 24 | 25 | // the rest of this module is borrowed from libstd 26 | 27 | fn errno() -> u32 { 28 | unsafe { GetLastError() } 29 | } 30 | 31 | fn error_string(errnum: u32) -> String { 32 | #![allow(non_snake_case)] 33 | 34 | // This value is calculated from the macro 35 | // MAKELANGID(LANG_SYSTEM_DEFAULT, SUBLANG_SYS_DEFAULT) 36 | let langId = MAKELANGID(LANG_SYSTEM_DEFAULT, SUBLANG_SYS_DEFAULT) as DWORD; 37 | 38 | let mut buf = [0 as WCHAR; 2048]; 39 | 40 | unsafe { 41 | let res = FormatMessageW( 42 | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, 43 | ptr::null_mut(), 44 | errnum as DWORD, 45 | langId as DWORD, 46 | buf.as_mut_ptr(), 47 | buf.len() as DWORD, 48 | ptr::null_mut(), 49 | ); 50 | if res == 0 { 51 | // Sometimes FormatMessageW can fail e.g. system doesn't like langId, 52 | let fm_err = errno(); 53 | return format!( 54 | "OS Error {} (FormatMessageW() returned error {})", 55 | errnum, fm_err 56 | ); 57 | } 58 | 59 | let b = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); 60 | let msg = String::from_utf16(&buf[..b]); 61 | match msg { 62 | Ok(msg) => msg, 63 | Err(..) => format!( 64 | "OS Error {} (FormatMessageW() returned invalid UTF-16)", 65 | errnum 66 | ), 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/windows/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::com::*; 2 | pub use self::enumerate::*; 3 | 4 | mod com; 5 | mod dcb; 6 | mod enumerate; 7 | mod error; 8 | -------------------------------------------------------------------------------- /tests/config.rs: -------------------------------------------------------------------------------- 1 | // Configuration for integration tests. This crate is about interacting with real serial ports and 2 | // so some tests need actual hardware. 3 | 4 | use envconfig::Envconfig; 5 | use rstest::fixture; 6 | 7 | // Configuration for tests requiring acutual hardware. 8 | // 9 | // For conveniently pulling this configuration into a test case as a parameter, you might want to 10 | // use the test fixture [`hw_config`]. 11 | #[derive(Clone, Debug, Envconfig, Eq, PartialEq)] 12 | pub struct HardwareConfig { 13 | #[envconfig(from = "SERIALPORT_TEST_PORT_1")] 14 | pub port_1: String, 15 | #[envconfig(from = "SERIALPORT_TEST_PORT_2")] 16 | pub port_2: String, 17 | } 18 | 19 | // Test fixture for conveniently pulling the actual hardware configuration into test cases. 20 | // 21 | // See [`fixture`](rstest::fixture) for details. 22 | #[fixture] 23 | pub fn hw_config() -> HardwareConfig { 24 | HardwareConfig::init_from_env().unwrap() 25 | } 26 | -------------------------------------------------------------------------------- /tests/test_baudrate.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | 3 | use assert_hex::assert_eq_hex; 4 | use config::{hw_config, HardwareConfig}; 5 | use rstest::rstest; 6 | use rstest_reuse::{self, apply, template}; 7 | use serialport::{ClearBuffer, SerialPort}; 8 | use std::ops::Range; 9 | use std::time::Duration; 10 | 11 | const RESET_BAUD_RATE: u32 = 300; 12 | const TEST_MESSAGE: &[u8] = 13 | b"0123456789:;<=>?@abcdefghijklmnopqrstuvwxyz[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; 14 | const TEST_MESSAGE_TIMEOUT: Duration = Duration::from_millis(1000); 15 | 16 | /// Returs an acceptance interval for the actual baud rate returned from the device after setting 17 | /// the supplied value. For example, the CP2102 driver on Linux returns the baud rate actually 18 | /// configured a the device rather than the the value set. 19 | fn accepted_actual_baud_for(baud: u32) -> Range { 20 | let delta = baud / 200; 21 | baud.checked_sub(delta).unwrap()..baud.checked_add(delta).unwrap() 22 | } 23 | 24 | fn check_baud_rate(port: &dyn SerialPort, baud: u32) { 25 | let accepted = accepted_actual_baud_for(baud); 26 | let actual = port.baud_rate().unwrap(); 27 | assert!(accepted.contains(&actual)); 28 | } 29 | 30 | fn check_test_message(sender: &mut dyn SerialPort, receiver: &mut dyn SerialPort) { 31 | // Ignore any "residue" from previous tests. 32 | sender.clear(ClearBuffer::All).unwrap(); 33 | receiver.clear(ClearBuffer::All).unwrap(); 34 | 35 | let send_buf = TEST_MESSAGE; 36 | let mut recv_buf = [0u8; TEST_MESSAGE.len()]; 37 | 38 | sender.write_all(send_buf).unwrap(); 39 | sender.flush().unwrap(); 40 | 41 | receiver.read_exact(&mut recv_buf).unwrap(); 42 | assert_eq_hex!(recv_buf, send_buf, "Received message does not match sent"); 43 | } 44 | 45 | #[template] 46 | #[rstest] 47 | #[case(9600)] 48 | #[case(57600)] 49 | #[case(115200)] 50 | fn standard_baud_rates(#[case] baud: u32) {} 51 | 52 | #[template] 53 | #[rstest] 54 | #[case(1000)] 55 | #[case(42000)] 56 | #[case(100000)] 57 | fn non_standard_baud_rates(#[case] baud: u32) {} 58 | 59 | /// Test cases for setting the baud rate via [`SerialPortBuilder`]. 60 | mod builder { 61 | use super::*; 62 | 63 | #[apply(standard_baud_rates)] 64 | #[cfg_attr(not(feature = "hardware-tests"), ignore)] 65 | fn test_standard_baud_rate(hw_config: HardwareConfig, #[case] baud: u32) { 66 | let port = serialport::new(hw_config.port_1, RESET_BAUD_RATE) 67 | .baud_rate(baud) 68 | .open() 69 | .unwrap(); 70 | check_baud_rate(port.as_ref(), baud); 71 | } 72 | 73 | #[apply(non_standard_baud_rates)] 74 | #[cfg_attr( 75 | any( 76 | not(feature = "hardware-tests"), 77 | not(all(target_os = "linux", target_env = "musl")), 78 | ), 79 | ignore 80 | )] 81 | fn test_non_standard_baud_rate_fails_where_not_supported( 82 | hw_config: HardwareConfig, 83 | #[case] baud: u32, 84 | ) { 85 | let res = serialport::new(hw_config.port_1, RESET_BAUD_RATE) 86 | .baud_rate(baud) 87 | .open(); 88 | assert!(res.is_err()); 89 | } 90 | 91 | #[apply(non_standard_baud_rates)] 92 | #[cfg_attr( 93 | any( 94 | not(feature = "hardware-tests"), 95 | all(target_os = "linux", target_env = "musl"), 96 | ), 97 | ignore 98 | )] 99 | fn test_non_standard_baud_rate_succeeds_where_supported( 100 | hw_config: HardwareConfig, 101 | #[case] baud: u32, 102 | ) { 103 | let port = serialport::new(hw_config.port_1, RESET_BAUD_RATE) 104 | .baud_rate(baud) 105 | .open() 106 | .unwrap(); 107 | check_baud_rate(port.as_ref(), baud); 108 | } 109 | 110 | /// Transmits data back and forth as done by the hardware_check example. 111 | #[apply(standard_baud_rates)] 112 | #[cfg_attr(not(feature = "hardware-tests"), ignore)] 113 | fn test_transmit_at_standard_baud_rate(hw_config: HardwareConfig, #[case] baud: u32) { 114 | let mut port1 = serialport::new(hw_config.port_1, RESET_BAUD_RATE) 115 | .baud_rate(baud) 116 | .timeout(TEST_MESSAGE_TIMEOUT) 117 | .open() 118 | .unwrap(); 119 | let mut port2 = serialport::new(hw_config.port_2, RESET_BAUD_RATE) 120 | .baud_rate(baud) 121 | .timeout(TEST_MESSAGE_TIMEOUT) 122 | .open() 123 | .unwrap(); 124 | 125 | check_test_message(&mut *port1, &mut *port2); 126 | check_test_message(&mut *port2, &mut *port1); 127 | } 128 | } 129 | 130 | /// Test cases for setting the baud rate via [`serialport::new`]. 131 | mod new { 132 | use super::*; 133 | 134 | #[apply(standard_baud_rates)] 135 | #[cfg_attr(not(feature = "hardware-tests"), ignore)] 136 | fn test_standard_baud_rate(hw_config: HardwareConfig, #[case] baud: u32) { 137 | let port = serialport::new(hw_config.port_1, baud).open().unwrap(); 138 | check_baud_rate(port.as_ref(), baud); 139 | } 140 | 141 | #[apply(non_standard_baud_rates)] 142 | #[cfg_attr( 143 | any( 144 | not(feature = "hardware-tests"), 145 | not(all(target_os = "linux", target_env = "musl")), 146 | ), 147 | ignore 148 | )] 149 | fn test_non_standard_baud_rate_fails_where_not_supported( 150 | hw_config: HardwareConfig, 151 | #[case] baud: u32, 152 | ) { 153 | assert!(serialport::new(hw_config.port_1, baud).open().is_err()); 154 | } 155 | 156 | #[apply(non_standard_baud_rates)] 157 | #[cfg_attr( 158 | any( 159 | not(feature = "hardware-tests"), 160 | all(target_os = "linux", target_env = "musl"), 161 | ), 162 | ignore 163 | )] 164 | fn test_non_standard_baud_rate_succeeds_where_supported( 165 | hw_config: HardwareConfig, 166 | #[case] baud: u32, 167 | ) { 168 | let port = serialport::new(hw_config.port_1, baud).open().unwrap(); 169 | check_baud_rate(port.as_ref(), baud); 170 | } 171 | 172 | /// Transmits data back and forth as done by the hardware_check example. 173 | #[apply(standard_baud_rates)] 174 | #[cfg_attr(not(feature = "hardware-tests"), ignore)] 175 | fn test_transmit_at_standard_baud_rate(hw_config: HardwareConfig, #[case] baud: u32) { 176 | let mut port1 = serialport::new(hw_config.port_1, baud) 177 | .timeout(TEST_MESSAGE_TIMEOUT) 178 | .open() 179 | .unwrap(); 180 | let mut port2 = serialport::new(hw_config.port_2, baud) 181 | .timeout(TEST_MESSAGE_TIMEOUT) 182 | .open() 183 | .unwrap(); 184 | 185 | check_test_message(&mut *port1, &mut *port2); 186 | check_test_message(&mut *port2, &mut *port1); 187 | } 188 | } 189 | 190 | /// Test cases for setting the baud rate via [`SerialPort::set_baud_rate`]. 191 | mod set_baud { 192 | use super::*; 193 | 194 | #[apply(standard_baud_rates)] 195 | #[cfg_attr(not(feature = "hardware-tests"), ignore)] 196 | fn test_standard_baud_rate(hw_config: HardwareConfig, #[case] baud: u32) { 197 | let mut port = serialport::new(hw_config.port_1, RESET_BAUD_RATE) 198 | .open() 199 | .unwrap(); 200 | check_baud_rate(port.as_ref(), RESET_BAUD_RATE); 201 | 202 | port.set_baud_rate(baud).unwrap(); 203 | check_baud_rate(port.as_ref(), baud); 204 | } 205 | 206 | #[apply(non_standard_baud_rates)] 207 | #[cfg_attr( 208 | any( 209 | not(feature = "hardware-tests"), 210 | not(all(target_os = "linux", target_env = "musl")), 211 | ), 212 | ignore 213 | )] 214 | fn test_non_standard_baud_rate_fails_where_not_supported( 215 | hw_config: HardwareConfig, 216 | #[case] baud: u32, 217 | ) { 218 | let mut port = serialport::new(hw_config.port_1, RESET_BAUD_RATE) 219 | .open() 220 | .unwrap(); 221 | check_baud_rate(port.as_ref(), RESET_BAUD_RATE); 222 | 223 | assert!(port.set_baud_rate(baud).is_err()); 224 | check_baud_rate(port.as_ref(), RESET_BAUD_RATE); 225 | } 226 | 227 | #[apply(non_standard_baud_rates)] 228 | #[cfg_attr( 229 | any( 230 | not(feature = "hardware-tests"), 231 | all(target_os = "linux", target_env = "musl"), 232 | ), 233 | ignore 234 | )] 235 | fn test_non_standard_baud_rate_succeeds_where_supported( 236 | hw_config: HardwareConfig, 237 | #[case] baud: u32, 238 | ) { 239 | let mut port = serialport::new(hw_config.port_1, RESET_BAUD_RATE) 240 | .open() 241 | .unwrap(); 242 | check_baud_rate(port.as_ref(), RESET_BAUD_RATE); 243 | 244 | port.set_baud_rate(baud).unwrap(); 245 | check_baud_rate(port.as_ref(), baud); 246 | } 247 | 248 | /// Transmits data back and forth as done by the hardware_check example. 249 | #[apply(standard_baud_rates)] 250 | #[cfg_attr(not(feature = "hardware-tests"), ignore)] 251 | fn test_transmit_at_standard_baud_rate(hw_config: HardwareConfig, #[case] baud: u32) { 252 | let mut port1 = serialport::new(hw_config.port_1, RESET_BAUD_RATE) 253 | .timeout(TEST_MESSAGE_TIMEOUT) 254 | .open() 255 | .unwrap(); 256 | let mut port2 = serialport::new(hw_config.port_2, RESET_BAUD_RATE) 257 | .timeout(TEST_MESSAGE_TIMEOUT) 258 | .open() 259 | .unwrap(); 260 | 261 | port1.set_baud_rate(baud).unwrap(); 262 | port2.set_baud_rate(baud).unwrap(); 263 | 264 | check_test_message(&mut *port1, &mut *port2); 265 | check_test_message(&mut *port2, &mut *port1); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /tests/test_serialport.rs: -------------------------------------------------------------------------------- 1 | //! Tests for the `SerialPort` trait. 2 | mod config; 3 | 4 | use config::{hw_config, HardwareConfig}; 5 | use rstest::rstest; 6 | use serialport::*; 7 | use std::time::Duration; 8 | 9 | #[rstest] 10 | #[cfg_attr(not(feature = "hardware-tests"), ignore)] 11 | fn test_listing_ports() { 12 | let ports = serialport::available_ports().expect("No ports found!"); 13 | for p in ports { 14 | println!("{}", p.port_name); 15 | } 16 | } 17 | 18 | #[rstest] 19 | #[cfg_attr(not(feature = "hardware-tests"), ignore)] 20 | fn test_opening_found_ports(hw_config: HardwareConfig) { 21 | // There is no guarantee that we even might open the ports returned by `available_ports`. But 22 | // the ports we are using for testing shall be among them. 23 | let ports = serialport::available_ports().unwrap(); 24 | assert!(ports.iter().any(|info| info.port_name == hw_config.port_1)); 25 | assert!(ports.iter().any(|info| info.port_name == hw_config.port_2)); 26 | } 27 | 28 | #[rstest] 29 | #[cfg_attr(not(feature = "hardware-tests"), ignore)] 30 | fn test_opening_port(hw_config: HardwareConfig) { 31 | serialport::new(hw_config.port_1, 9600).open().unwrap(); 32 | } 33 | 34 | #[rstest] 35 | #[cfg_attr(not(feature = "hardware-tests"), ignore)] 36 | fn test_opening_native_port(hw_config: HardwareConfig) { 37 | serialport::new(hw_config.port_1, 9600) 38 | .open_native() 39 | .unwrap(); 40 | } 41 | 42 | #[rstest] 43 | #[cfg_attr(not(feature = "hardware-tests"), ignore)] 44 | fn test_configuring_ports(hw_config: HardwareConfig) { 45 | serialport::new(hw_config.port_1, 9600) 46 | .data_bits(DataBits::Five) 47 | .flow_control(FlowControl::None) 48 | .parity(Parity::None) 49 | .stop_bits(StopBits::One) 50 | .timeout(Duration::from_millis(1)) 51 | .open() 52 | .unwrap(); 53 | } 54 | 55 | #[rstest] 56 | #[cfg_attr(not(feature = "hardware-tests"), ignore)] 57 | fn test_duplicating_port_config(hw_config: HardwareConfig) { 58 | let port1_config = serialport::new(hw_config.port_1, 9600) 59 | .data_bits(DataBits::Five) 60 | .flow_control(FlowControl::None) 61 | .parity(Parity::None) 62 | .stop_bits(StopBits::One) 63 | .timeout(Duration::from_millis(1)); 64 | 65 | let port2_config = port1_config 66 | .clone() 67 | .path(hw_config.port_2) 68 | .baud_rate(115_200); 69 | 70 | let _port1 = port1_config.open().unwrap(); 71 | let _port1 = port2_config.open().unwrap(); 72 | } 73 | -------------------------------------------------------------------------------- /tests/test_timeout.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | 3 | use config::{hw_config, HardwareConfig}; 4 | use rstest::rstest; 5 | use serialport::*; 6 | use std::io::Read; 7 | use std::thread; 8 | use std::time::{Duration, Instant}; 9 | 10 | #[rstest] 11 | #[case(1, Vec::from(b"abcdef"))] 12 | #[case( 13 | 20, 14 | Vec::from(b"0123456789:;<=>?@abcdefghijklmnopqrstuvwxyz[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~") 15 | )] 16 | #[cfg_attr(not(feature = "hardware-tests"), ignore)] 17 | fn test_read_returns_available_data_before_timeout( 18 | hw_config: HardwareConfig, 19 | #[case] chunk_size: usize, 20 | #[case] message: Vec, 21 | ) { 22 | let send_period = Duration::from_millis(500); 23 | let receive_timeout = Duration::from_millis(3000); 24 | let marign = Duration::from_millis(100); 25 | 26 | let mut sender = serialport::new(hw_config.port_1, 115200).open().unwrap(); 27 | let mut receiver = serialport::new(hw_config.port_2, 115200) 28 | .timeout(receive_timeout) 29 | .open() 30 | .unwrap(); 31 | 32 | sender.clear(ClearBuffer::All).unwrap(); 33 | receiver.clear(ClearBuffer::All).unwrap(); 34 | 35 | let expected_message = message.clone(); 36 | let receiver_thread = thread::spawn(move || { 37 | let chunk_timeout = send_period + marign; 38 | assert!(receiver.timeout() > 3 * chunk_timeout); 39 | 40 | let mut received_message = Vec::with_capacity(expected_message.len()); 41 | 42 | loop { 43 | let chunk_start = Instant::now(); 44 | let expected_chunk_until = chunk_start + chunk_timeout; 45 | 46 | let mut buffer = [0u8; 1024]; 47 | assert!(buffer.len() > expected_message.len()); 48 | 49 | // Try to read more data than we are expecting and expect some data to be available 50 | // after the send period (plus some margin). 51 | match receiver.read(&mut buffer) { 52 | Ok(read) => { 53 | assert!(expected_chunk_until > Instant::now()); 54 | assert!(read > 0); 55 | println!( 56 | "receive: {} bytes after waiting {} ms", 57 | read, 58 | (Instant::now() - chunk_start).as_millis() 59 | ); 60 | received_message.extend_from_slice(&buffer[..read]); 61 | } 62 | e => panic!("unexpected error {:?}", e), 63 | } 64 | 65 | if received_message.len() >= expected_message.len() { 66 | break; 67 | } 68 | } 69 | 70 | assert_eq!(expected_message, received_message); 71 | }); 72 | 73 | let sender_thread = thread::spawn(move || { 74 | let mut next = Instant::now(); 75 | 76 | for chunk in message.chunks(chunk_size) { 77 | sender.write_all(chunk).unwrap(); 78 | sender.flush().unwrap(); 79 | 80 | println!("send: {} bytes", chunk.len()); 81 | 82 | next += send_period; 83 | thread::sleep(next - Instant::now()); 84 | } 85 | }); 86 | 87 | sender_thread.join().unwrap(); 88 | receiver_thread.join().unwrap(); 89 | } 90 | 91 | #[rstest] 92 | #[case(b"a")] 93 | #[case(b"0123456789")] 94 | #[case(b"0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~")] 95 | #[cfg_attr(not(feature = "hardware-tests"), ignore)] 96 | fn test_timeout_zero(hw_config: HardwareConfig, #[case] message: &[u8]) { 97 | let timeout = Duration::ZERO; 98 | let margin = Duration::from_millis(100); 99 | 100 | let mut sender = serialport::new(hw_config.port_1, 115200).open().unwrap(); 101 | let mut receiver = serialport::new(hw_config.port_2, 115200) 102 | .timeout(timeout) 103 | .open() 104 | .unwrap(); 105 | let mut buffer: [u8; 1024] = [0xff; 1024]; 106 | 107 | sender.clear(ClearBuffer::All).unwrap(); 108 | receiver.clear(ClearBuffer::All).unwrap(); 109 | 110 | sender.write_all(message).unwrap(); 111 | sender.flush().unwrap(); 112 | let flushed_at = Instant::now(); 113 | 114 | let expected_until = flushed_at + timeout + margin; 115 | let mut timeouts = 0usize; 116 | 117 | loop { 118 | match receiver.read(&mut buffer) { 119 | Ok(read) => { 120 | assert!(read > 0); 121 | println!( 122 | "read: {} bytes of {} after {} timeouts/{} ms", 123 | read, 124 | message.len(), 125 | timeouts, 126 | (Instant::now() - flushed_at).as_millis() 127 | ); 128 | assert_eq!(message[..read], buffer[..read]); 129 | break; 130 | } 131 | Err(e) => { 132 | assert_eq!(e.kind(), std::io::ErrorKind::TimedOut); 133 | timeouts += 1; 134 | } 135 | } 136 | 137 | assert!(expected_until > Instant::now()); 138 | } 139 | } 140 | 141 | #[rstest] 142 | #[case(Duration::from_millis(10))] 143 | #[case(Duration::from_millis(100))] 144 | #[case(Duration::from_millis(1000))] 145 | #[cfg_attr(not(feature = "hardware-tests"), ignore)] 146 | fn test_timeout_greater_zero(hw_config: HardwareConfig, #[case] timeout: Duration) { 147 | let margin = Duration::from_millis(100); 148 | 149 | let mut sender = serialport::new(hw_config.port_1, 115200).open().unwrap(); 150 | let mut receiver = serialport::new(hw_config.port_2, 115200) 151 | .timeout(timeout) 152 | .open() 153 | .unwrap(); 154 | 155 | let message = 156 | b"0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; 157 | let mut buffer: [u8; 1024] = [0xff; 1024]; 158 | 159 | sender.clear(ClearBuffer::All).unwrap(); 160 | receiver.clear(ClearBuffer::All).unwrap(); 161 | 162 | sender.write_all(message).unwrap(); 163 | sender.flush().unwrap(); 164 | 165 | let flushed_at = Instant::now(); 166 | 167 | let read = receiver.read(&mut buffer).unwrap(); 168 | let read_at = Instant::now(); 169 | 170 | println!( 171 | "read: {} bytes of {} after {} ms", 172 | read, 173 | message.len(), 174 | (Instant::now() - flushed_at).as_millis() 175 | ); 176 | 177 | assert!(read > 0); 178 | assert!(flushed_at + timeout + margin > read_at); 179 | assert_eq!(buffer[..read], message[..read]); 180 | } 181 | 182 | /// Checks that reading data with a timeout of `Duration::MAX` returns some data and no error. It 183 | /// does not check the actual timeout for obvious reason. 184 | #[rstest] 185 | #[cfg_attr(not(feature = "hardware-tests"), ignore)] 186 | fn test_timeout_max(hw_config: HardwareConfig) { 187 | let sleep = Duration::from_millis(3000); 188 | let margin = Duration::from_millis(500); 189 | let mut sender = serialport::new(hw_config.port_1, 115200).open().unwrap(); 190 | let mut receiver = serialport::new(hw_config.port_2, 115200) 191 | .timeout(Duration::MAX) 192 | .open() 193 | .unwrap(); 194 | 195 | let message = 196 | b"0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; 197 | let mut buffer: [u8; 1024] = [0xff; 1024]; 198 | 199 | sender.clear(ClearBuffer::All).unwrap(); 200 | receiver.clear(ClearBuffer::All).unwrap(); 201 | 202 | let started_at = Instant::now(); 203 | 204 | let sender_thread = thread::spawn(move || { 205 | thread::sleep(sleep); 206 | 207 | sender.write_all(message).unwrap(); 208 | sender.flush().unwrap(); 209 | }); 210 | 211 | let read = receiver.read(&mut buffer).unwrap(); 212 | let read_at = Instant::now(); 213 | 214 | println!( 215 | "read: {} bytes of {} after {} ms", 216 | read, 217 | message.len(), 218 | (Instant::now() - started_at).as_millis() 219 | ); 220 | 221 | assert!(read > 0); 222 | assert!(read_at > started_at + sleep); 223 | assert!(read_at < started_at + sleep + margin); 224 | assert_eq!(buffer[..read], message[..read]); 225 | 226 | sender_thread.join().unwrap(); 227 | } 228 | -------------------------------------------------------------------------------- /tests/test_try_clone.rs: -------------------------------------------------------------------------------- 1 | #![cfg(unix)] 2 | extern crate serialport; 3 | 4 | use serialport::{SerialPort, TTYPort}; 5 | use std::io::{Read, Write}; 6 | 7 | // Test that cloning a port works as expected 8 | #[test] 9 | fn test_try_clone() { 10 | let (master, mut slave) = TTYPort::pair().expect("Unable to create ptty pair"); 11 | 12 | // Create the clone in an inner scope to test that dropping a clone doesn't close the original 13 | // port 14 | { 15 | let mut clone = master.try_clone().expect("Failed to clone"); 16 | let bytes = [b'a', b'b', b'c', b'd', b'e', b'f']; 17 | clone.write_all(&bytes).unwrap(); 18 | let mut buffer = [0; 6]; 19 | slave.read_exact(&mut buffer).unwrap(); 20 | assert_eq!(buffer, [b'a', b'b', b'c', b'd', b'e', b'f']); 21 | } 22 | 23 | // Second try to check that the serial device is not closed 24 | { 25 | let mut clone = master.try_clone().expect("Failed to clone"); 26 | let bytes = [b'g', b'h', b'i', b'j', b'k', b'l']; 27 | clone.write_all(&bytes).unwrap(); 28 | let mut buffer = [0; 6]; 29 | slave.read_exact(&mut buffer).unwrap(); 30 | assert_eq!(buffer, [b'g', b'h', b'i', b'j', b'k', b'l']); 31 | } 32 | } 33 | 34 | // Test moving a cloned port into a thread 35 | #[test] 36 | fn test_try_clone_move() { 37 | use std::thread; 38 | 39 | let (master, mut slave) = TTYPort::pair().expect("Unable to create ptty pair"); 40 | 41 | let mut clone = master.try_clone().expect("Failed to clone the slave"); 42 | let loopback = thread::spawn(move || { 43 | let bytes = [b'a', b'b', b'c', b'd', b'e', b'f']; 44 | clone.write_all(&bytes).unwrap(); 45 | }); 46 | 47 | let mut buffer = [0; 6]; 48 | slave.read_exact(&mut buffer).unwrap(); 49 | assert_eq!(buffer, [b'a', b'b', b'c', b'd', b'e', b'f']); 50 | 51 | // The thread should have already ended, but we'll make sure here anyways. 52 | loopback.join().unwrap(); 53 | } 54 | -------------------------------------------------------------------------------- /tests/test_tty.rs: -------------------------------------------------------------------------------- 1 | //! Tests for the `posix::TTYPort` struct. 2 | #![cfg(unix)] 3 | 4 | extern crate serialport; 5 | 6 | use std::io::{Read, Write}; 7 | use std::os::unix::prelude::*; 8 | use std::str; 9 | use std::time::Duration; 10 | 11 | use serialport::{SerialPort, TTYPort}; 12 | 13 | #[test] 14 | fn test_ttyport_pair() { 15 | // FIXME: Create a mutex across all tests for using `TTYPort::pair()` as it's not threadsafe 16 | let (mut master, mut slave) = TTYPort::pair().expect("Unable to create ptty pair"); 17 | master 18 | .set_timeout(Duration::from_millis(10)) 19 | .expect("Unable to set timeout on the master"); 20 | slave 21 | .set_timeout(Duration::from_millis(10)) 22 | .expect("Unable to set timeout on the slave"); 23 | 24 | // Test file descriptors. 25 | assert!( 26 | master.as_raw_fd() > 0, 27 | "Invalid file descriptor on master ptty" 28 | ); 29 | assert!( 30 | slave.as_raw_fd() > 0, 31 | "Invalid file descriptor on slae ptty" 32 | ); 33 | assert_ne!( 34 | master.as_raw_fd(), 35 | slave.as_raw_fd(), 36 | "master and slave ptty's share the same file descriptor." 37 | ); 38 | 39 | let msg = "Test Message"; 40 | let mut buf = [0u8; 128]; 41 | 42 | // Write the string on the master 43 | let nbytes = master 44 | .write(msg.as_bytes()) 45 | .expect("Unable to write bytes."); 46 | assert_eq!( 47 | nbytes, 48 | msg.len(), 49 | "Write message length differs from sent message." 50 | ); 51 | 52 | // Read it on the slave 53 | let nbytes = slave.read(&mut buf).expect("Unable to read bytes."); 54 | assert_eq!( 55 | nbytes, 56 | msg.len(), 57 | "Read message length differs from sent message." 58 | ); 59 | 60 | assert_eq!( 61 | str::from_utf8(&buf[..nbytes]).unwrap(), 62 | msg, 63 | "Received message does not match sent" 64 | ); 65 | } 66 | 67 | #[test] 68 | fn test_ttyport_timeout() { 69 | let result = std::sync::Arc::new(std::sync::Mutex::new(None)); 70 | let result_thread = result.clone(); 71 | 72 | std::thread::spawn(move || { 73 | // FIXME: Create a mutex across all tests for using `TTYPort::pair()` as it's not threadsafe 74 | let (mut master, _slave) = TTYPort::pair().expect("Unable to create ptty pair"); 75 | master.set_timeout(Duration::new(1, 0)).unwrap(); 76 | 77 | let mut buffer = [0u8]; 78 | let read_res = master.read(&mut buffer); 79 | 80 | *result_thread.lock().unwrap() = Some(read_res); 81 | }); 82 | 83 | std::thread::sleep(std::time::Duration::new(2, 0)); 84 | 85 | let read_res = result.lock().unwrap(); 86 | match *read_res { 87 | Some(Ok(_)) => panic!("Received data without sending"), 88 | Some(Err(ref e)) => assert_eq!(e.kind(), std::io::ErrorKind::TimedOut), 89 | None => panic!("Read did not time out"), 90 | } 91 | } 92 | 93 | #[test] 94 | #[cfg(any(target_os = "ios", target_os = "macos"))] 95 | fn test_osx_pty_pair() { 96 | #![allow(unused_variables)] 97 | let (mut master, slave) = TTYPort::pair().expect("Unable to create ptty pair"); 98 | let (output_sink, output_stream) = std::sync::mpsc::sync_channel(1); 99 | let name = slave.name().unwrap(); 100 | 101 | master.write_all("12".as_bytes()).expect(""); 102 | 103 | let reader_thread = std::thread::spawn(move || { 104 | let mut port = TTYPort::open(&serialport::new(&name, 0)).expect("unable to open"); 105 | let mut buffer = [0u8; 2]; 106 | let amount = port.read_exact(&mut buffer); 107 | output_sink 108 | .send(String::from_utf8(buffer.to_vec()).expect("buffer not read as valid utf-8")) 109 | .expect("unable to send from thread"); 110 | }); 111 | 112 | reader_thread.join().expect("unable to join sink thread"); 113 | assert_eq!(output_stream.recv().unwrap(), "12"); 114 | } 115 | 116 | // On Mac this should work (in fact used to in b77768a) but now fails. It's not functionality that 117 | // should be required, and the ptys work otherwise. So going to just disable this test instead. 118 | #[test] 119 | #[cfg_attr(any(target_os = "ios", target_os = "macos"), ignore)] 120 | fn test_ttyport_set_standard_baud() { 121 | // `master` must be used here as Dropping it causes slave to be deleted by the OS. 122 | // TODO: Convert this to a statement-level attribute once 123 | // https://github.com/rust-lang/rust/issues/15701 is on stable. 124 | // FIXME: Create a mutex across all tests for using `TTYPort::pair()` as it's not threadsafe 125 | #![allow(unused_variables)] 126 | let (master, mut slave) = TTYPort::pair().expect("Unable to create ptty pair"); 127 | 128 | slave.set_baud_rate(9600).unwrap(); 129 | assert_eq!(slave.baud_rate().unwrap(), 9600); 130 | slave.set_baud_rate(57600).unwrap(); 131 | assert_eq!(slave.baud_rate().unwrap(), 57600); 132 | slave.set_baud_rate(115_200).unwrap(); 133 | assert_eq!(slave.baud_rate().unwrap(), 115_200); 134 | } 135 | 136 | // On mac this fails because you can't set nonstandard baud rates for these virtual ports 137 | #[test] 138 | #[cfg_attr( 139 | any( 140 | target_os = "ios", 141 | all(target_os = "linux", target_env = "musl"), 142 | target_os = "macos" 143 | ), 144 | ignore 145 | )] 146 | fn test_ttyport_set_nonstandard_baud() { 147 | // `master` must be used here as Dropping it causes slave to be deleted by the OS. 148 | // TODO: Convert this to a statement-level attribute once 149 | // https://github.com/rust-lang/rust/issues/15701 is on stable. 150 | // FIXME: Create a mutex across all tests for using `TTYPort::pair()` as it's not threadsafe 151 | #![allow(unused_variables)] 152 | let (master, mut slave) = TTYPort::pair().expect("Unable to create ptty pair"); 153 | 154 | slave.set_baud_rate(10000).unwrap(); 155 | assert_eq!(slave.baud_rate().unwrap(), 10000); 156 | slave.set_baud_rate(60000).unwrap(); 157 | assert_eq!(slave.baud_rate().unwrap(), 60000); 158 | slave.set_baud_rate(1_200_000).unwrap(); 159 | assert_eq!(slave.baud_rate().unwrap(), 1_200_000); 160 | } 161 | --------------------------------------------------------------------------------