use std::time::{Duration, Instant};
use windows_sys::Win32::Devices::Communication::{
COMMTIMEOUTS, ClearCommBreak, DCB, EVENPARITY, GetCommState, NOPARITY, ODDPARITY, ONESTOPBIT,
PURGE_RXCLEAR, PurgeComm, SetCommBreak, SetCommState, SetCommTimeouts, TWOSTOPBITS,
};
use windows_sys::Win32::Foundation::{
CloseHandle, GENERIC_READ, GENERIC_WRITE, HANDLE, INVALID_HANDLE_VALUE,
};
use windows_sys::Win32::Storage::FileSystem::{
CreateFileW, FlushFileBuffers, OPEN_EXISTING, ReadFile, WriteFile,
};
use crate::error::{AsynError, AsynResult, AsynStatus};
use crate::interpose::{EomReason, OctetNext, OctetReadResult};
use crate::port::{PortDriver, PortDriverBase, PortFlags};
use crate::trace::TraceMask;
use crate::user::AsynUser;
use crate::{asyn_trace, asyn_trace_io};
use super::serial_config::{
DataBits, FlowControl, Parity, SerialConfig, StopBits, parse_bool_option,
};
mod dcb_bits {
pub const F_BINARY: u32 = 1 << 0;
pub const F_OUTX_CTS_FLOW: u32 = 1 << 2;
pub const F_OUTX_DSR_FLOW: u32 = 1 << 3;
pub const F_DTR_CONTROL_SHIFT: u32 = 4;
pub const F_DTR_CONTROL_MASK: u32 = 0b11 << 4;
pub const F_DSR_SENSITIVITY: u32 = 1 << 6;
pub const F_OUT_X: u32 = 1 << 8;
pub const F_IN_X: u32 = 1 << 9;
pub const F_RTS_CONTROL_SHIFT: u32 = 12;
pub const F_RTS_CONTROL_MASK: u32 = 0b11 << 12;
pub const DTR_CONTROL_ENABLE: u32 = 1;
pub const DTR_CONTROL_HANDSHAKE: u32 = 2;
pub const RTS_CONTROL_ENABLE: u32 = 1;
pub const RTS_CONTROL_HANDSHAKE: u32 = 2;
pub fn set_flag(bf: &mut u32, mask: u32, on: bool) {
if on {
*bf |= mask;
} else {
*bf &= !mask;
}
}
pub fn get_flag(bf: u32, mask: u32) -> bool {
bf & mask != 0
}
pub fn set_field(bf: &mut u32, shift: u32, mask: u32, val: u32) {
*bf = (*bf & !mask) | ((val << shift) & mask);
}
}
fn device_to_wide(device: &str) -> Vec<u16> {
const PREFIX: &[u8] = br"\\.\";
let already_prefixed = device.len() >= PREFIX.len()
&& device.as_bytes()[..PREFIX.len()].eq_ignore_ascii_case(PREFIX);
let full = if already_prefixed {
device.to_string()
} else {
format!(r"\\.\{device}")
};
full.encode_utf16().chain(std::iter::once(0)).collect()
}
fn apply_config_to_dcb(config: &SerialConfig, dcb: &mut DCB) {
use dcb_bits::*;
dcb.BaudRate = config.baud;
dcb.ByteSize = match config.data_bits {
DataBits::Five => 5,
DataBits::Six => 6,
DataBits::Seven => 7,
DataBits::Eight => 8,
};
dcb.Parity = match config.parity {
Parity::None => NOPARITY,
Parity::Even => EVENPARITY,
Parity::Odd => ODDPARITY,
};
dcb.StopBits = match config.stop_bits {
StopBits::One => ONESTOPBIT,
StopBits::Two => TWOSTOPBITS,
};
set_flag(&mut dcb._bitfield, F_BINARY, true);
dcb.XonChar = 0x11;
dcb.XoffChar = 0x13;
let bf = &mut dcb._bitfield;
match config.flow_control {
FlowControl::None => {
set_flag(bf, F_OUTX_CTS_FLOW, false);
set_field(
bf,
F_RTS_CONTROL_SHIFT,
F_RTS_CONTROL_MASK,
RTS_CONTROL_ENABLE,
);
set_flag(bf, F_OUT_X, false);
set_flag(bf, F_IN_X, false);
}
FlowControl::Hardware => {
set_flag(bf, F_OUTX_CTS_FLOW, true);
set_field(
bf,
F_RTS_CONTROL_SHIFT,
F_RTS_CONTROL_MASK,
RTS_CONTROL_HANDSHAKE,
);
set_flag(bf, F_OUT_X, false);
set_flag(bf, F_IN_X, false);
}
FlowControl::Software => {
set_flag(bf, F_OUTX_CTS_FLOW, false);
set_field(
bf,
F_RTS_CONTROL_SHIFT,
F_RTS_CONTROL_MASK,
RTS_CONTROL_ENABLE,
);
set_flag(bf, F_OUT_X, true);
set_flag(bf, F_IN_X, true);
}
}
}
fn io_err() -> AsynError {
AsynError::Io(std::io::Error::last_os_error())
}
fn is_fatal_transport_error(e: &AsynError) -> bool {
matches!(
e,
AsynError::Status {
status: AsynStatus::Disconnected,
..
} | AsynError::Io(_)
)
}
struct SerialIoStateWin32 {
handle_val: Option<isize>,
n_read: u64,
n_written: u64,
last_timeout: Option<Duration>,
}
impl SerialIoStateWin32 {
fn new() -> Self {
Self {
handle_val: None,
n_read: 0,
n_written: 0,
last_timeout: None,
}
}
fn handle(&self) -> Option<HANDLE> {
self.handle_val.map(|v| v as HANDLE)
}
fn handle_or_err(&self) -> AsynResult<HANDLE> {
self.handle().ok_or_else(|| AsynError::Status {
status: AsynStatus::Disconnected,
message: "serial port not open".into(),
})
}
fn apply_timeouts(&mut self, handle: HANDLE, timeout: Duration) -> AsynResult<()> {
if self.last_timeout == Some(timeout) {
return Ok(());
}
let mut ct: COMMTIMEOUTS = unsafe { std::mem::zeroed() };
if timeout.is_zero() {
ct.ReadIntervalTimeout = u32::MAX;
ct.ReadTotalTimeoutMultiplier = 0;
ct.ReadTotalTimeoutConstant = 0;
ct.WriteTotalTimeoutMultiplier = 0;
ct.WriteTotalTimeoutConstant = 1;
} else {
let ms = timeout.as_millis().min(u32::MAX as u128 - 1) as u32;
ct.ReadIntervalTimeout = ms;
ct.ReadTotalTimeoutMultiplier = 1;
ct.ReadTotalTimeoutConstant = ms;
ct.WriteTotalTimeoutMultiplier = 1;
ct.WriteTotalTimeoutConstant = ms;
}
if unsafe { SetCommTimeouts(handle, &ct) } == 0 {
return Err(io_err());
}
self.last_timeout = Some(timeout);
Ok(())
}
}
impl OctetNext for SerialIoStateWin32 {
fn read(&mut self, user: &AsynUser, buf: &mut [u8]) -> AsynResult<OctetReadResult> {
let handle = self.handle_or_err()?;
if buf.is_empty() {
return Err(AsynError::Status {
status: AsynStatus::Error,
message: "maxchars 0 Why <=0?".into(),
});
}
self.apply_timeouts(handle, user.timeout)?;
let want = buf.len().min(u32::MAX as usize) as u32;
let mut n_read: u32 = 0;
let ok = unsafe {
ReadFile(
handle,
buf.as_mut_ptr(),
want,
&mut n_read,
std::ptr::null_mut(),
)
};
if ok == 0 {
return Err(io_err());
}
if n_read == 0 {
return Err(AsynError::Status {
status: AsynStatus::Timeout,
message: "serial read timeout".into(),
});
}
self.n_read += n_read as u64;
Ok(OctetReadResult {
nbytes_transferred: n_read as usize,
eom_reason: if n_read as usize >= buf.len() {
EomReason::CNT
} else {
EomReason::empty()
},
})
}
fn write(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
let handle = self.handle_or_err()?;
if data.is_empty() {
return Ok(0);
}
self.apply_timeouts(handle, user.timeout)?;
let deadline = Instant::now() + user.timeout;
let mut total = 0usize;
while total < data.len() {
let chunk = &data[total..];
let want = chunk.len().min(u32::MAX as usize) as u32;
let mut n_written: u32 = 0;
let ok = unsafe {
WriteFile(
handle,
chunk.as_ptr(),
want,
&mut n_written,
std::ptr::null_mut(),
)
};
if ok == 0 {
return Err(io_err());
}
total += n_written as usize;
self.n_written += n_written as u64;
if total >= data.len() {
break;
}
if n_written == 0 || Instant::now() >= deadline {
return Err(AsynError::Status {
status: AsynStatus::Timeout,
message: "serial write timeout".into(),
});
}
}
Ok(total)
}
fn flush(&mut self, _user: &mut AsynUser) -> AsynResult<()> {
if let Some(handle) = self.handle() {
if unsafe { PurgeComm(handle, PURGE_RXCLEAR) } == 0 {
return Err(io_err());
}
}
Ok(())
}
}
pub struct DrvAsynSerialPort {
base: PortDriverBase,
config: SerialConfig,
io: SerialIoStateWin32,
}
impl DrvAsynSerialPort {
fn drop_connection(&mut self) {
if let Some(v) = self.io.handle_val.take() {
unsafe { CloseHandle(v as HANDLE) };
}
self.io.last_timeout = None;
self.base.set_connected(false);
}
pub fn new(port_name: &str, config_str: &str) -> AsynResult<Self> {
let config = SerialConfig::parse(config_str)?;
let mut base = PortDriverBase::new(
port_name,
1,
PortFlags {
multi_device: false,
can_block: true,
destructible: true,
},
);
base.connected = false;
base.auto_connect = true;
Ok(Self {
base,
config,
io: SerialIoStateWin32::new(),
})
}
pub fn configure(
port_name: &str,
config_str: &str,
no_auto_connect: bool,
no_process_eos: bool,
) -> AsynResult<Self> {
let mut driver = Self::new(port_name, config_str)?;
if no_auto_connect {
driver.base.auto_connect = false;
}
if !no_process_eos {
driver.push_interpose(Box::new(crate::interpose::eos::EosInterpose::default()));
}
Ok(driver)
}
pub fn push_interpose(&mut self, layer: Box<dyn crate::interpose::OctetInterpose>) {
self.base.push_octet_interpose(layer);
}
pub fn send_break(&self, duration_tenths: i32) -> AsynResult<()> {
let handle = self.io.handle_or_err()?;
if unsafe { SetCommBreak(handle) } == 0 {
return Err(io_err());
}
let ms = if duration_tenths <= 0 {
250
} else {
(duration_tenths as u64) * 100
};
std::thread::sleep(Duration::from_millis(ms));
if unsafe { ClearCommBreak(handle) } == 0 {
return Err(io_err());
}
Ok(())
}
pub fn drain_output(&self) -> AsynResult<()> {
let handle = self.io.handle_or_err()?;
if unsafe { FlushFileBuffers(handle) } == 0 {
return Err(io_err());
}
Ok(())
}
fn modify_dcb<F: FnOnce(&mut DCB)>(&self, f: F) -> AsynResult<()> {
let handle = self.io.handle_or_err()?;
let mut dcb: DCB = unsafe { std::mem::zeroed() };
dcb.DCBlength = std::mem::size_of::<DCB>() as u32;
if unsafe { GetCommState(handle, &mut dcb) } == 0 {
return Err(io_err());
}
f(&mut dcb);
if unsafe { SetCommState(handle, &dcb) } == 0 {
return Err(io_err());
}
Ok(())
}
}
impl PortDriver for DrvAsynSerialPort {
fn base(&self) -> &PortDriverBase {
&self.base
}
fn base_mut(&mut self) -> &mut PortDriverBase {
&mut self.base
}
fn connect(&mut self, _user: &AsynUser) -> AsynResult<()> {
if self.io.handle_val.is_some() {
return Err(AsynError::Status {
status: AsynStatus::Error,
message: format!("{}: Link already open!", self.base.port_name),
});
}
let wide = device_to_wide(&self.config.device);
let handle = unsafe {
CreateFileW(
wide.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
0, std::ptr::null(),
OPEN_EXISTING,
0, std::ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE {
return Err(io_err());
}
self.io.handle_val = Some(handle as isize);
self.io.last_timeout = None;
let setup = (|| -> AsynResult<()> {
unsafe { ClearCommBreak(handle) };
if unsafe { FlushFileBuffers(handle) } == 0 {
return Err(io_err());
}
let mut dcb: DCB = unsafe { std::mem::zeroed() };
dcb.DCBlength = std::mem::size_of::<DCB>() as u32;
if unsafe { GetCommState(handle, &mut dcb) } == 0 {
return Err(io_err());
}
apply_config_to_dcb(&self.config, &mut dcb);
if unsafe { SetCommState(handle, &dcb) } == 0 {
return Err(io_err());
}
unsafe { PurgeComm(handle, PURGE_RXCLEAR) };
Ok(())
})();
if let Err(e) = setup {
unsafe { CloseHandle(handle) };
self.io.handle_val = None;
return Err(e);
}
self.base.set_connected(true);
asyn_trace!(
Some(self.base.trace),
&self.base.port_name,
TraceMask::FLOW,
"connected to {} at {} baud",
self.config.device,
self.config.baud
);
Ok(())
}
fn disconnect(&mut self, _user: &AsynUser) -> AsynResult<()> {
asyn_trace!(
Some(self.base.trace),
&self.base.port_name,
TraceMask::FLOW,
"disconnect"
);
if let Some(v) = self.io.handle_val.take() {
unsafe { CloseHandle(v as HANDLE) };
}
self.io.last_timeout = None;
self.base.set_connected(false);
Ok(())
}
fn report(&self, level: i32) {
eprintln!(
"Serial line {}: {}",
self.config.device,
if self.base.connected {
"Connected"
} else {
"Disconnected"
}
);
if level >= 1 {
eprintln!(
" commHandle: {:?}",
self.io.handle().unwrap_or(std::ptr::null_mut())
);
eprintln!(" Characters written: {}", self.io.n_written);
eprintln!(" Characters read: {}", self.io.n_read);
self.base.report_params(level.saturating_sub(1));
}
}
fn read_octet(&mut self, user: &AsynUser, buf: &mut [u8]) -> AsynResult<usize> {
self.base.check_ready()?;
let result = match self
.base
.interpose_octet
.dispatch_read(user, buf, &mut self.io)
{
Ok(r) => r,
Err(e) => {
if is_fatal_transport_error(&e) && self.base.connected {
asyn_trace!(
Some(self.base.trace),
&self.base.port_name,
TraceMask::FLOW,
"read error, disconnecting: {e}"
);
self.drop_connection();
}
return Err(e);
}
};
asyn_trace_io!(
Some(self.base.trace),
&self.base.port_name,
TraceMask::IO_DRIVER,
&buf[..result.nbytes_transferred],
"read"
);
Ok(result.nbytes_transferred)
}
fn write_octet(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
self.base.check_ready()?;
asyn_trace_io!(
Some(self.base.trace),
&self.base.port_name,
TraceMask::IO_DRIVER,
data,
"write"
);
match self
.base
.interpose_octet
.dispatch_write(user, data, &mut self.io)
{
Ok(n) => Ok(n),
Err(e) => {
if is_fatal_transport_error(&e) && self.base.connected {
asyn_trace!(
Some(self.base.trace),
&self.base.port_name,
TraceMask::FLOW,
"write error, disconnecting: {e}"
);
self.drop_connection();
}
Err(e)
}
}
}
fn io_flush(&mut self, user: &mut AsynUser) -> AsynResult<()> {
self.base.interpose_octet.dispatch_flush(user, &mut self.io)
}
fn set_option(&mut self, key: &str, value: &str) -> AsynResult<()> {
use dcb_bits::*;
let key = key.trim().to_ascii_lowercase();
let value = value.trim();
match key.as_str() {
"baud" => {
let baud: u32 = value.parse().map_err(|_| AsynError::Status {
status: AsynStatus::Error,
message: format!("invalid baud rate: '{value}'"),
})?;
if self.io.handle_val.is_some() {
self.modify_dcb(|dcb| dcb.BaudRate = baud)?;
}
self.config.baud = baud;
}
"bits" => {
let bits = match value {
"5" => DataBits::Five,
"6" => DataBits::Six,
"7" => DataBits::Seven,
"8" => DataBits::Eight,
_ => {
return Err(AsynError::Status {
status: AsynStatus::Error,
message: format!("invalid data bits: '{value}' (expected 5/6/7/8)"),
});
}
};
let byte_size = match bits {
DataBits::Five => 5,
DataBits::Six => 6,
DataBits::Seven => 7,
DataBits::Eight => 8,
};
if self.io.handle_val.is_some() {
self.modify_dcb(|dcb| dcb.ByteSize = byte_size)?;
}
self.config.data_bits = bits;
}
"parity" => {
let val_lower = value.to_ascii_lowercase();
let parity = match val_lower.as_str() {
"none" => Parity::None,
"even" => Parity::Even,
"odd" => Parity::Odd,
_ => {
return Err(AsynError::Status {
status: AsynStatus::Error,
message: format!(
"invalid parity: '{value}' (expected none/odd/even; mark/space not supported)"
),
});
}
};
let dcb_parity = match parity {
Parity::None => NOPARITY,
Parity::Even => EVENPARITY,
Parity::Odd => ODDPARITY,
};
if self.io.handle_val.is_some() {
self.modify_dcb(|dcb| dcb.Parity = dcb_parity)?;
}
self.config.parity = parity;
}
"stop" => {
let stop = match value {
"1" => StopBits::One,
"2" => StopBits::Two,
_ => {
return Err(AsynError::Status {
status: AsynStatus::Error,
message: format!("invalid stop bits: '{value}' (expected 1/2)"),
});
}
};
let dcb_stop = match stop {
StopBits::One => ONESTOPBIT,
StopBits::Two => TWOSTOPBITS,
};
if self.io.handle_val.is_some() {
self.modify_dcb(|dcb| dcb.StopBits = dcb_stop)?;
}
self.config.stop_bits = stop;
}
"clocal" => {
let enabled = parse_bool_option(value)?;
if self.io.handle_val.is_some() {
self.modify_dcb(|dcb| {
let bf = &mut dcb._bitfield;
if enabled {
set_flag(bf, F_OUTX_DSR_FLOW, false);
set_flag(bf, F_DSR_SENSITIVITY, false);
set_field(
bf,
F_DTR_CONTROL_SHIFT,
F_DTR_CONTROL_MASK,
DTR_CONTROL_ENABLE,
);
} else {
set_flag(bf, F_OUTX_DSR_FLOW, true);
set_flag(bf, F_DSR_SENSITIVITY, true);
set_field(
bf,
F_DTR_CONTROL_SHIFT,
F_DTR_CONTROL_MASK,
DTR_CONTROL_HANDSHAKE,
);
}
})?;
}
}
"crtscts" => {
let enabled = parse_bool_option(value)?;
if self.io.handle_val.is_some() {
self.modify_dcb(|dcb| {
let bf = &mut dcb._bitfield;
set_flag(bf, F_OUTX_CTS_FLOW, enabled);
set_field(
bf,
F_RTS_CONTROL_SHIFT,
F_RTS_CONTROL_MASK,
if enabled {
RTS_CONTROL_HANDSHAKE
} else {
RTS_CONTROL_ENABLE
},
);
})?;
}
if enabled {
self.config.flow_control = FlowControl::Hardware;
} else if self.config.flow_control == FlowControl::Hardware {
self.config.flow_control = FlowControl::None;
}
}
"ixon" => {
let enabled = parse_bool_option(value)?;
if self.io.handle_val.is_some() {
self.modify_dcb(|dcb| set_flag(&mut dcb._bitfield, F_OUT_X, enabled))?;
}
}
"ixoff" => {
let enabled = parse_bool_option(value)?;
if self.io.handle_val.is_some() {
self.modify_dcb(|dcb| set_flag(&mut dcb._bitfield, F_IN_X, enabled))?;
}
}
"ixany" => {
return Err(AsynError::Status {
status: AsynStatus::Error,
message: "Option ixany not supported on Windows".into(),
});
}
"break" => {
if value != "off" {
let break_ms = if value.is_empty() || value == "on" {
250
} else {
value.parse::<u64>().map_err(|_| AsynError::Status {
status: AsynStatus::Error,
message: format!("invalid break duration: '{value}'"),
})?
};
let handle = self.io.handle_or_err()?;
if unsafe { FlushFileBuffers(handle) } == 0 {
return Err(io_err());
}
if unsafe { SetCommBreak(handle) } == 0 {
return Err(io_err());
}
std::thread::sleep(Duration::from_millis(break_ms));
if unsafe { ClearCommBreak(handle) } == 0 {
return Err(io_err());
}
}
}
other => {
if !other.is_empty() {
return Err(AsynError::OptionNotFound(other.to_string()));
}
if self.io.handle_val.is_some() {
let config = self.config.clone();
self.modify_dcb(|dcb| apply_config_to_dcb(&config, dcb))?;
}
}
}
Ok(())
}
fn get_option(&self, key: &str) -> AsynResult<String> {
use dcb_bits::*;
let live_dcb = || -> AsynResult<DCB> {
let handle = self.io.handle_or_err()?;
let mut dcb: DCB = unsafe { std::mem::zeroed() };
dcb.DCBlength = std::mem::size_of::<DCB>() as u32;
if unsafe { GetCommState(handle, &mut dcb) } == 0 {
return Err(io_err());
}
Ok(dcb)
};
match key {
"baud" => Ok(self.config.baud.to_string()),
"bits" => Ok(match self.config.data_bits {
DataBits::Five => "5",
DataBits::Six => "6",
DataBits::Seven => "7",
DataBits::Eight => "8",
}
.to_string()),
"parity" => Ok(match self.config.parity {
Parity::None => "none",
Parity::Even => "even",
Parity::Odd => "odd",
}
.to_string()),
"stop" => Ok(match self.config.stop_bits {
StopBits::One => "1",
StopBits::Two => "2",
}
.to_string()),
"clocal" => {
if self.io.handle_val.is_some() {
let dcb = live_dcb()?;
Ok(if get_flag(dcb._bitfield, F_OUTX_DSR_FLOW) {
"N"
} else {
"Y"
}
.to_string())
} else {
Ok("N".to_string())
}
}
"crtscts" => {
if self.io.handle_val.is_some() {
let dcb = live_dcb()?;
Ok(if get_flag(dcb._bitfield, F_OUTX_CTS_FLOW) {
"Y"
} else {
"N"
}
.to_string())
} else {
Ok(match self.config.flow_control {
FlowControl::Hardware => "Y",
_ => "N",
}
.to_string())
}
}
"ixon" => {
if self.io.handle_val.is_some() {
let dcb = live_dcb()?;
Ok(if get_flag(dcb._bitfield, F_OUT_X) {
"Y"
} else {
"N"
}
.to_string())
} else {
Ok("N".to_string())
}
}
"ixoff" => {
if self.io.handle_val.is_some() {
let dcb = live_dcb()?;
Ok(if get_flag(dcb._bitfield, F_IN_X) {
"Y"
} else {
"N"
}
.to_string())
} else {
Ok("N".to_string())
}
}
"ixany" => Ok("N".to_string()),
"break" => Ok("off".to_string()),
_ => self
.base
.options
.get(key)
.cloned()
.ok_or_else(|| AsynError::OptionNotFound(key.to_string())),
}
}
}
impl Drop for DrvAsynSerialPort {
fn drop(&mut self) {
let user = AsynUser::default();
if self.base.connected {
let _ = self.disconnect(&user);
}
}
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use super::*;
#[test]
fn parse_extracts_device() {
let cfg = SerialConfig::parse(r"\\.\COM3").unwrap();
assert_eq!(cfg.device, r"\\.\COM3");
assert_eq!(cfg.baud, 9600);
}
#[test]
fn parse_rejects_empty() {
assert!(SerialConfig::parse("").is_err());
assert!(SerialConfig::parse(" ").is_err());
}
#[test]
fn device_to_wide_prepends_prefix_once() {
let w = device_to_wide("COM3");
let s = String::from_utf16(&w[..w.len() - 1]).unwrap();
assert_eq!(s, r"\\.\COM3");
assert_eq!(*w.last().unwrap(), 0, "must be NUL-terminated");
let w2 = device_to_wide(r"\\.\COM10");
let s2 = String::from_utf16(&w2[..w2.len() - 1]).unwrap();
assert_eq!(s2, r"\\.\COM10");
}
#[test]
fn apply_config_sets_dcb_fields() {
let mut cfg = SerialConfig::parse("COM1").unwrap();
cfg.baud = 115200;
cfg.data_bits = DataBits::Seven;
cfg.parity = Parity::Even;
cfg.stop_bits = StopBits::Two;
cfg.flow_control = FlowControl::Hardware;
let mut dcb: DCB = unsafe { std::mem::zeroed() };
apply_config_to_dcb(&cfg, &mut dcb);
assert_eq!(dcb.BaudRate, 115200);
assert_eq!(dcb.ByteSize, 7);
assert_eq!(dcb.Parity, EVENPARITY);
assert_eq!(dcb.StopBits, TWOSTOPBITS);
assert!(dcb_bits::get_flag(dcb._bitfield, dcb_bits::F_BINARY));
assert!(dcb_bits::get_flag(dcb._bitfield, dcb_bits::F_OUTX_CTS_FLOW));
let rts = (dcb._bitfield & dcb_bits::F_RTS_CONTROL_MASK) >> dcb_bits::F_RTS_CONTROL_SHIFT;
assert_eq!(rts, dcb_bits::RTS_CONTROL_HANDSHAKE);
assert_eq!(dcb.XonChar, 0x11);
assert_eq!(dcb.XoffChar, 0x13);
}
#[test]
fn configure_installs_eos_unless_suppressed() {
let default_port = DrvAsynSerialPort::configure("s_eos", "COM1", false, false).unwrap();
assert_eq!(
default_port.base().interpose_octet.len(),
1,
"default serial port must auto-install the EOS interpose"
);
let suppressed = DrvAsynSerialPort::configure("s_eos_off", "COM1", true, true).unwrap();
assert_eq!(
suppressed.base().interpose_octet.len(),
0,
"noProcessEos must suppress the EOS interpose"
);
}
#[test]
fn new_is_parse_only_no_eos() {
let drv = DrvAsynSerialPort::new("s1", "COM1").unwrap();
assert_eq!(drv.base().interpose_octet.len(), 0);
}
}