Skip to content

Instantly share code, notes, and snippets.

@carstenandrich
Last active May 1, 2020 08:25
Show Gist options
  • Save carstenandrich/10b3962fa1abc9e50816b6460010900b to your computer and use it in GitHub Desktop.
Save carstenandrich/10b3962fa1abc9e50816b6460010900b to your computer and use it in GitHub Desktop.
Rust program using only winapi crate to illustrate the read/write timeout behavior of COM ports on Windows
// Copyright 2020 Carsten Andrich <base64decode("Y2Fyc3Rlblx4NDBhbmRyaWNoXHgyZW5hbWU=")>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// WARNING: THIS PROGRAM WILL WRITE A BUNCH OF NULL CHARACTERS (0x00) ON A
// WINDOWS COM PORT OF YOUR CHOICE. DO **NOT** USE IT ON ANY COM PORT
// UNLESS YOU'RE **ABSOLUTELY** SURE THAT THIS WILL NOT HAVE ANY
// UNDESIRED EFFECTS ON THAT PORT OR DEVICES ACCESSED VIA IT.
//
// This example illustrates the Windows COM port read/write timeout behavior
// by opening a COM port with 1 millisecond read/write timeout and reading/
// writing a couple of times.
//
// According to this document, read/write timeouts are not considered errors:
// https://docs.microsoft.com/en-us/windows/win32/devio/time-outs
// > It is not treated as an error when a time-out occurs during a read or
// > write operation (that is, the read or write function's return value
// > indicates success). The count of bytes actually read or written is
// > reported by ReadFile or WriteFile
//
// This is true for ReadFile() on COM ports, which return TRUE and sets
// lpNumberOfBytesRead to 0 if a timeout occurs. However, WriteFile() returns
// FALSE and GetLastError() indicates ERR_SEM_TIMEOUT (121) on timeout (tested
// on Windows 10 version 1809).
extern crate winapi;
use std::ffi::OsStr;
use std::io;
use std::mem;
use std::os::windows::ffi::OsStrExt;
use std::ptr;
use std::time::Duration;
use winapi::ctypes::c_void;
use winapi::shared::minwindef::{BOOL, DWORD, TRUE};
use winapi::um::commapi::{SetCommState, SetCommTimeouts};
use winapi::um::fileapi::{CreateFileW, OPEN_EXISTING, FlushFileBuffers, ReadFile, WriteFile};
use winapi::um::handleapi::INVALID_HANDLE_VALUE;
use winapi::um::winbase::{CBR_256000, COMMTIMEOUTS, DCB, NOPARITY, ONESTOPBIT};
use winapi::um::winnt::{FILE_ATTRIBUTE_NORMAL, GENERIC_READ, GENERIC_WRITE, HANDLE, MAXDWORD};
pub struct SerialPort {
handle: HANDLE
}
impl SerialPort {
pub fn open<T>(port_name: &T, timeout: Option<Duration>) -> io::Result<Self>
where T: AsRef<OsStr> + ?Sized {
// construct prefixed COM port name to support COMn with n > 9
let mut name = Vec::<u16>::new();
name.extend(OsStr::new("\\\\.\\").encode_wide());
name.extend(port_name.as_ref().encode_wide());
name.push(0);
// open COM port as raw HANDLE
let handle = unsafe {
CreateFileW(name.as_ptr(), GENERIC_READ | GENERIC_WRITE, 0,
ptr::null_mut(), OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0 as HANDLE)
};
if handle == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
// configure COM port for raw communication
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-dcb
let mut dcb: DCB = unsafe { mem::zeroed() };
dcb.DCBlength = mem::size_of::<DCB>() as u32;
dcb.set_fBinary(TRUE as u32);
dcb.BaudRate = CBR_256000;
dcb.ByteSize = 8;
dcb.StopBits = ONESTOPBIT;
dcb.Parity = NOPARITY;
if unsafe { SetCommState(handle, &mut dcb) } == 0 {
return Err(io::Error::last_os_error());
}
// populate COMMTIMEOUTS struct from Option<Duration>
// https://docs.microsoft.com/en-us/windows/win32/devio/time-outs
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-commtimeouts
let mut timeouts = if let Some(dur) = timeout {
// FIXME: Durations less than 1 ms will configure non-blocking read
// and blocking write without timeout
// FIXME: Long Durations will overflow MAXDWORD
let dur_ms = dur.as_secs() * 1000
+ dur.subsec_millis() as u64;
COMMTIMEOUTS {
// return immediately if bytes are available (like POSIX would)
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-commtimeouts#remarks
ReadIntervalTimeout: MAXDWORD,
ReadTotalTimeoutMultiplier: MAXDWORD,
ReadTotalTimeoutConstant: dur_ms as DWORD,
// MAXDWORD is *not* a reserved WriteTotalTimeoutMultiplier
// value, i.e., setting it incurs an very long write timeout
WriteTotalTimeoutMultiplier: 0,
WriteTotalTimeoutConstant: dur_ms as DWORD,
}
} else {
// blocking read/write without timeout
COMMTIMEOUTS {
ReadIntervalTimeout: 0,
ReadTotalTimeoutMultiplier: 0,
ReadTotalTimeoutConstant: 0,
WriteTotalTimeoutMultiplier: 0,
WriteTotalTimeoutConstant: 0,
}
};
// set timouts
if unsafe { SetCommTimeouts(handle, &mut timeouts) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(Self { handle })
}
}
impl io::Read for SerialPort {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let mut len: DWORD = 0;
let res: BOOL = unsafe {
ReadFile(self.handle, buf.as_mut_ptr() as *mut c_void,
buf.len() as DWORD, &mut len, ptr::null_mut())
};
match res {
0 => Err(io::Error::last_os_error()),
_ if len == 0 => Err(io::Error::new(io::ErrorKind::TimedOut,
"ReadFile() returned TRUE with 0 bytes read")),
_ => Ok(len as usize)
}
}
}
impl io::Write for SerialPort {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let mut len: DWORD = 0;
let res: BOOL = unsafe {
WriteFile(self.handle, buf.as_ptr() as *mut c_void,
buf.len() as DWORD, &mut len, ptr::null_mut())
};
match res {
0 => Err(io::Error::last_os_error()),
_ if len == 0 => Err(io::Error::new(io::ErrorKind::TimedOut,
"WriteFile() returned TRUE with 0 bytes written")),
_ => Ok(len as usize)
}
}
fn flush(&mut self) -> io::Result<()> {
match unsafe { FlushFileBuffers(self.handle) } {
0 => Err(io::Error::last_os_error()),
_ => Ok(()),
}
}
}
use std::env;
use std::io::{Read, Write};
use std::time::Instant;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: {} COMn", args[0]);
return;
}
// open port with smallest possible timeout duration to get timeouts below
let mut com = SerialPort::open(
&args[1], Some(Duration::from_millis(1))
).expect("Opening COM port failed");
println!("{:?}", Instant::now());
let mut buf = [0u8; 1024];
for _ in 0..50 {
let res = com.read(&mut buf);
println!("{:?} {:?}", Instant::now(), res);
}
println!("\n{:?}", Instant::now());
let buf = [0u8; 1024];
for _ in 0..20 {
let res = com.write(&buf);
println!("{:?} {:?}", Instant::now(), res);
}
}
@carstenandrich
Copy link
Author

Running this on Windows 10 version 1809 with an Usbser.sys COM port (u-blox NEO-M8P GNSS receiver connected via USB) generates the following example output:

Instant { t: 0.0347632s }
Instant { t: 0.044776s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.0604238s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.0760185s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.0895689s } Ok(1)
Instant { t: 0.0896457s } Ok(23)
Instant { t: 0.0916059s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.106179s } Ok(1)
Instant { t: 0.1062408s } Ok(99)
Instant { t: 0.1064384s } Ok(1)
Instant { t: 0.106468s } Ok(39)
Instant { t: 0.1064956s } Ok(1)
Instant { t: 0.1065179s } Ok(35)
Instant { t: 0.1066652s } Ok(1)
Instant { t: 0.1067202s } Ok(71)
Instant { t: 0.1069506s } Ok(1)
Instant { t: 0.1069782s } Ok(91)
Instant { t: 0.1071387s } Ok(1)
Instant { t: 0.1071733s } Ok(11)
Instant { t: 0.1230177s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.1386637s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.1541398s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.1703842s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.1859203s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.2165789s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.2321606s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.2483029s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.2633712s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.2792839s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.2899231s } Ok(1)
Instant { t: 0.2900331s } Ok(23)
Instant { t: 0.2950898s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.30618s } Ok(1)
Instant { t: 0.3062906s } Ok(99)
Instant { t: 0.3063794s } Ok(1)
Instant { t: 0.3064067s } Ok(39)
Instant { t: 0.3065552s } Ok(1)
Instant { t: 0.306596s } Ok(79)
Instant { t: 0.3067099s } Ok(1)
Instant { t: 0.3067325s } Ok(27)
Instant { t: 0.3069587s } Ok(1)
Instant { t: 0.3069812s } Ok(91)
Instant { t: 0.3071594s } Ok(1)
Instant { t: 0.3071817s } Ok(11)
Instant { t: 0.3106016s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.326651s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.3573372s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.3727903s } Err(Custom { kind: TimedOut, error: "ReadFile() returned TRUE with 0 bytes read" })
Instant { t: 0.377438s } Ok(1)
Instant { t: 0.3774859s } Ok(46)
Instant { t: 0.377513s } Ok(42)

Instant { t: 0.3775238s }
Instant { t: 0.3793014s } Ok(1024)
Instant { t: 0.381436s } Ok(1024)
Instant { t: 0.383133s } Ok(1024)
Instant { t: 0.3847953s } Ok(1024)
Instant { t: 0.3885499s } Err(Os { code: 121, kind: Other, message: "The semaphore timeout period has expired." })
Instant { t: 0.4040566s } Err(Os { code: 121, kind: Other, message: "The semaphore timeout period has expired." })
Instant { t: 0.419729s } Err(Os { code: 121, kind: Other, message: "The semaphore timeout period has expired." })
Instant { t: 0.4358117s } Err(Os { code: 121, kind: Other, message: "The semaphore timeout period has expired." })
Instant { t: 0.4515629s } Err(Os { code: 121, kind: Other, message: "The semaphore timeout period has expired." })
Instant { t: 0.4586972s } Ok(1024)
Instant { t: 0.4606905s } Ok(1024)
Instant { t: 0.4622999s } Ok(1024)
Instant { t: 0.4667266s } Err(Os { code: 121, kind: Other, message: "The semaphore timeout period has expired." })
Instant { t: 0.4828019s } Err(Os { code: 121, kind: Other, message: "The semaphore timeout period has expired." })
Instant { t: 0.5139918s } Err(Os { code: 121, kind: Other, message: "The semaphore timeout period has expired." })
Instant { t: 0.5291662s } Err(Os { code: 121, kind: Other, message: "The semaphore timeout period has expired." })
Instant { t: 0.5452019s } Err(Os { code: 121, kind: Other, message: "The semaphore timeout period has expired." })
Instant { t: 0.5605556s } Err(Os { code: 121, kind: Other, message: "The semaphore timeout period has expired." })
Instant { t: 0.5683431s } Ok(1024)
Instant { t: 0.570108s } Ok(1024)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment