diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 7bc42c5701f594620be7cc89f2cb46fcf49c4206..78174779abb734dac7fcc526d9dbfd1fd193bd4c 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -9,6 +9,8 @@ and this project adheres to http://semver.org/[Semantic Versioning]. ==== Added * Added `examples/hardware_check.rs` for use in debugging library or driver issues when using physical serial ports. +* Added a new function in the trait `SerialPort` : `try_clone` which try to clone + the underlying structure of a serial port to allow full duplex read/write. === [2.1.0] - 2018-02-14 ==== Added diff --git a/examples/duplex.rs b/examples/duplex.rs new file mode 100644 index 0000000000000000000000000000000000000000..96ca5eaccb78f3c4d838342ea85f9f1e237f6130 --- /dev/null +++ b/examples/duplex.rs @@ -0,0 +1,49 @@ +//! Duplex example +//! +//! This example tests the ability to clone a serial port. It works by creating +//! a new file descriptor, and therefore a new `SerialPort` object that's safe +//! to send to a new thread. +//! +//! This example selects the first port on the system, clones the port into a child +//! thread that writes data to the port every second. While this is running the parent +//! thread continually reads from the port. +//! +//! To test this, have a physical or virtual loopback device connected as the +//! only port in the system. + +extern crate serialport; + +use serialport::{available_ports, open}; +use std::io; +use std::io::Write; +use std::{mem, thread}; +use std::time::Duration; + +fn main() { + + // Open the first serialport available. + let mut serialport = open(&available_ports().expect("No serial port")[0].port_name) + .expect("Failed to open serial port"); + + // Clone the port + let mut clone = serialport.try_clone().expect("Failed to clone"); + + // Send out 4 bytes every second + thread::spawn(move || { + loop { + clone.write(&[5, 6, 7, 8]).expect("Failed to write to serial port"); + thread::sleep(Duration::from_millis(1000)); + } + }); + + // Read the four bytes back from the cloned port + let mut buffer: [u8; 1] = unsafe { mem::uninitialized() }; + loop { + match serialport.read(&mut buffer) { + Ok(bytes) => if bytes == 1 { println!("Received: {:?}",buffer); }, + Err(ref e) if e.kind() == io::ErrorKind::TimedOut => (), + Err(e) => eprintln!("{:?}",e), + } + } +} + diff --git a/src/lib.rs b/src/lib.rs index 7a3afc3525266f588aaf94e4338551cb1a365262..fc9d3f8535835c43d641ef8ae0fc1c466a69e4f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -735,6 +735,22 @@ pub trait SerialPort: Send + io::Read + io::Write { /// * `NoDevice` if the device was disconnected. /// * `Io` for any other type of I/O error. fn read_carrier_detect(&mut self) -> ::Result; + + // Misc methods + + /// Attempts to clone the `SerialPort`. This allow you to write and read simultaneously from the + /// same serial connection. Please note that if you want a real asynchronous serial port you + /// should look at [mio-serial](https://crates.io/crates/mio-serial) or + /// [tokio-serial](https://crates.io/crates/tokio-serial). + /// + /// Also, you must be very carefull when changing the settings of a cloned `SerialPort` : since + /// the settings are cached on a per object basis, trying to modify them from two different + /// objects can cause some nasty behavior. + /// + /// # Errors + /// + /// This function returns an error if the serial port couldn't be cloned. + fn try_clone(&self) -> ::Result>; } #[derive(Debug,Clone,PartialEq,Eq)] diff --git a/src/posix/tty.rs b/src/posix/tty.rs index bd64cdcd5cd776db2135b4198d3f5c0a1e365337..799b627cd314bd87a341f0f758396b63db721b9a 100644 --- a/src/posix/tty.rs +++ b/src/posix/tty.rs @@ -29,7 +29,7 @@ use {Error, ErrorKind}; /// Convenience method for removing exclusive access from /// a fd and closing it. -fn close(fd: RawFd){ +fn close(fd: RawFd) { // remove exclusive access let _ = ioctl::tiocnxcl(fd); @@ -60,12 +60,14 @@ pub struct TTYPort { impl fmt::Debug for TTYPort { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - write!(f, - "TTYPort {{ fd: {}, timeout: {:?}, exclusive: {}, port_name: {:?} }}", - self.fd, - self.timeout, - self.exclusive, - self.port_name) + write!( + f, + "TTYPort {{ fd: {}, timeout: {:?}, exclusive: {}, port_name: {:?} }}", + self.fd, + self.timeout, + self.exclusive, + self.port_name + ) } } @@ -97,11 +99,10 @@ impl TTYPort { OFlag::O_RDWR | OFlag::O_NOCTTY | OFlag::O_NONBLOCK, nix::sys::stat::Mode::empty())?; - let mut termios = tcgetattr(fd) - .map_err(|e| { - close(fd); - e - })?; + let mut termios = tcgetattr(fd).map_err(|e| { + close(fd); + e + })?; // If any of these steps fail, then we should abort creation of the // TTYPort and ensure the file descriptor is closed. @@ -308,20 +309,20 @@ impl TTYPort { BaudRate::Baud2400 => B2400, BaudRate::Baud4800 => B4800, #[cfg(any(target_os = "freebsd", target_os = "dragonfly", target_os = "macos", - target_os = "netbsd", target_os = "openbsd"))] + target_os = "netbsd", target_os = "openbsd"))] BaudRate::Baud7200 => B7200, BaudRate::Baud9600 => B9600, #[cfg(any(target_os = "freebsd", target_os = "dragonfly", target_os = "macos", - target_os = "netbsd", target_os = "openbsd"))] + target_os = "netbsd", target_os = "openbsd"))] BaudRate::Baud14400 => B14400, BaudRate::Baud19200 => B19200, #[cfg(any(target_os = "freebsd", target_os = "dragonfly", target_os = "macos", - target_os = "netbsd", target_os = "openbsd"))] + target_os = "netbsd", target_os = "openbsd"))] BaudRate::Baud28800 => B28800, BaudRate::Baud38400 => B38400, BaudRate::Baud57600 => B57600, #[cfg(any(target_os = "freebsd", target_os = "dragonfly", target_os = "macos", - target_os = "netbsd", target_os = "openbsd"))] + target_os = "netbsd", target_os = "openbsd"))] BaudRate::Baud76800 => B76800, BaudRate::Baud115200 => B115200, BaudRate::Baud230400 => B230400, @@ -451,7 +452,8 @@ impl IntoRawFd for TTYPort { impl FromRawFd for TTYPort { unsafe fn from_raw_fd(fd: RawFd) -> Self { - let termios = nix::sys::termios::tcgetattr(fd).expect("Unable to retrieve termios settings."); + let termios = + nix::sys::termios::tcgetattr(fd).expect("Unable to retrieve termios settings."); // Try to set exclusive, as is the default setting. Catch errors.. this method MUST // return a TTYPort so we'll just indicate non-exclusive on an error here. @@ -499,7 +501,9 @@ impl io::Write for TTYPort { } fn flush(&mut self) -> io::Result<()> { - nix::sys::termios::tcdrain(self.fd).map_err(|_| io::Error::new(io::ErrorKind::Other, "flush failed")) + nix::sys::termios::tcdrain(self.fd).map_err(|_| { + io::Error::new(io::ErrorKind::Other, "flush failed") + }) } } @@ -513,8 +517,7 @@ impl SerialPort for TTYPort { SerialPortSettings { baud_rate: self.baud_rate().expect("Couldn't retrieve baud rate"), data_bits: self.data_bits().expect("Couldn't retrieve data bits"), - flow_control: self.flow_control() - .expect("Couldn't retrieve flow control"), + flow_control: self.flow_control().expect("Couldn't retrieve flow control"), parity: self.parity().expect("Couldn't retrieve parity"), stop_bits: self.stop_bits().expect("Couldn't retrieve stop bits"), timeout: self.timeout, @@ -706,6 +709,19 @@ impl SerialPort for TTYPort { fn read_carrier_detect(&mut self) -> ::Result { self.read_pin(ioctl::DATA_CARRIER_DETECT) } + + // FIXME : Remove the setting caching as it can cause problem if you change the setting + // through two different objects. + fn try_clone(&self) -> ::Result> { + let fd_cloned: i32 = fcntl(self.fd, nix::fcntl::F_DUPFD(self.fd))?; + Ok(Box::new(TTYPort { + fd: fd_cloned, + termios: self.termios.clone(), + exclusive: self.exclusive, + port_name: self.port_name.clone(), + timeout: self.timeout, + })) + } } /// Retrieves the udev property value named by `key`. If the value exists, then it will be @@ -742,12 +758,12 @@ fn port_type(d: &libudev::Device) -> ::Result { Some("usb") => { let serial_number = udev_property_as_string(d, "ID_SERIAL_SHORT"); Ok(SerialPortType::UsbPort(UsbPortInfo { - vid: udev_hex_property_as_u16(d, "ID_VENDOR_ID")?, - pid: udev_hex_property_as_u16(d, "ID_MODEL_ID")?, - serial_number: serial_number, - manufacturer: udev_property_as_string(d, "ID_VENDOR"), - product: udev_property_as_string(d, "ID_MODEL"), - })) + vid: udev_hex_property_as_u16(d, "ID_VENDOR_ID")?, + pid: udev_hex_property_as_u16(d, "ID_MODEL_ID")?, + serial_number: serial_number, + manufacturer: udev_property_as_string(d, "ID_VENDOR"), + product: udev_property_as_string(d, "ID_MODEL"), + })) } Some("pci") => Ok(SerialPortType::PciPort), _ => Ok(SerialPortType::Unknown), @@ -770,7 +786,8 @@ pub fn available_ports() -> ::Result> { if let Some(path) = devnode.to_str() { if let Some(driver) = p.driver() { if driver == "serial8250" && - TTYPort::open(devnode, &Default::default()).is_err() { + TTYPort::open(devnode, &Default::default()).is_err() + { continue; } } @@ -778,9 +795,9 @@ pub fn available_ports() -> ::Result> { // skipped instead of causing no ports to be returned. if let Ok(pt) = port_type(&d) { vec.push(SerialPortInfo { - port_name: String::from(path), - port_type: pt, - }); + port_name: String::from(path), + port_type: pt, + }); } } } @@ -791,9 +808,10 @@ pub fn available_ports() -> ::Result> { } #[cfg(target_os = "macos")] -fn get_parent_device_by_type(device: io_object_t, - parent_type: *const c_char) - -> Option { +fn get_parent_device_by_type( + device: io_object_t, + parent_type: *const c_char, +) -> Option { let parent_type = unsafe { CStr::from_ptr(parent_type) }; use mach::kern_return::KERN_SUCCESS; let mut device = device; @@ -806,8 +824,9 @@ fn get_parent_device_by_type(device: io_object_t, } let mut parent: io_registry_entry_t = unsafe { mem::uninitialized() }; if unsafe { - IORegistryEntryGetParentEntry(device, kIOServiceClass(), &mut parent) != KERN_SUCCESS - } { + IORegistryEntryGetParentEntry(device, kIOServiceClass(), &mut parent) != KERN_SUCCESS + } + { return None; } device = parent; @@ -817,15 +836,18 @@ fn get_parent_device_by_type(device: io_object_t, #[cfg(target_os = "macos")] #[allow(non_upper_case_globals)] /// Returns a specific property of the given device as an integer. -fn get_int_property(device_type: io_registry_entry_t, - property: &str, - cf_number_type: CFNumberType) - -> Option { +fn get_int_property( + device_type: io_registry_entry_t, + property: &str, + cf_number_type: CFNumberType, +) -> Option { unsafe { let prop_str = CString::new(property).unwrap(); - let key = CFStringCreateWithCString(kCFAllocatorDefault, - prop_str.as_ptr(), - kCFStringEncodingUTF8); + let key = CFStringCreateWithCString( + kCFAllocatorDefault, + prop_str.as_ptr(), + kCFStringEncodingUTF8, + ); let container = IORegistryEntryCreateCFProperty(device_type, key, kCFAllocatorDefault, 0); if container.is_null() { return None; @@ -856,9 +878,11 @@ fn get_int_property(device_type: io_registry_entry_t, fn get_string_property(device_type: io_registry_entry_t, property: &str) -> Option { unsafe { let prop_str = CString::new(property).unwrap(); - let key = CFStringCreateWithCString(kCFAllocatorDefault, - prop_str.as_ptr(), - kCFStringEncodingUTF8); + let key = CFStringCreateWithCString( + kCFAllocatorDefault, + prop_str.as_ptr(), + kCFStringEncodingUTF8, + ); let container = IORegistryEntryCreateCFProperty(device_type, key, kCFAllocatorDefault, 0); if container.is_null() { return None; @@ -884,22 +908,14 @@ fn port_type(service: io_object_t) -> SerialPortType { let bluetooth_device_class_name = b"IOBluetoothSerialClient\0".as_ptr() as *const c_char; if let Some(usb_device) = get_parent_device_by_type(service, kIOUSBDeviceClassName()) { SerialPortType::UsbPort(UsbPortInfo { - vid: get_int_property(usb_device, - "idVendor", - kCFNumberSInt16Type) - .unwrap_or_default() as - u16, - pid: get_int_property(usb_device, - "idProduct", - kCFNumberSInt16Type) - .unwrap_or_default() as - u16, - serial_number: get_string_property(usb_device, - "USB Serial Number"), - manufacturer: get_string_property(usb_device, - "USB Vendor Name"), - product: get_string_property(usb_device, "USB Product Name"), - }) + vid: get_int_property(usb_device, "idVendor", kCFNumberSInt16Type) + .unwrap_or_default() as u16, + pid: get_int_property(usb_device, "idProduct", kCFNumberSInt16Type) + .unwrap_or_default() as u16, + serial_number: get_string_property(usb_device, "USB Serial Number"), + manufacturer: get_string_property(usb_device, "USB Vendor Name"), + product: get_string_property(usb_device, "USB Product Name"), + }) } else if get_parent_device_by_type(service, bluetooth_device_class_name).is_some() { SerialPortType::BluetoothPort } else { @@ -922,23 +938,33 @@ pub fn available_ports() -> ::Result> { // Create a dictionary for specifying the search terms against the IOService let classes_to_match = IOServiceMatching(kIOSerialBSDServiceValue()); if classes_to_match.is_null() { - return Err(Error::new(ErrorKind::Unknown, - "IOServiceMatching returned a NULL dictionary.")); + return Err(Error::new( + ErrorKind::Unknown, + "IOServiceMatching returned a NULL dictionary.", + )); } // Populate the search dictionary with a single key/value pair indicating that we're // searching for serial devices matching the RS232 device type. - let key = CFStringCreateWithCString(kCFAllocatorDefault, - kIOSerialBSDTypeKey(), - kCFStringEncodingUTF8); + let key = CFStringCreateWithCString( + kCFAllocatorDefault, + kIOSerialBSDTypeKey(), + kCFStringEncodingUTF8, + ); if key.is_null() { - return Err(Error::new(ErrorKind::Unknown, "Failed to allocate key string.")); + return Err(Error::new( + ErrorKind::Unknown, + "Failed to allocate key string.", + )); } let value = CFStringCreateWithCString(kCFAllocatorDefault, kIOSerialBSDAllTypes(), kCFStringEncodingUTF8); if value.is_null() { - return Err(Error::new(ErrorKind::Unknown, "Failed to allocate value string.")); + return Err(Error::new( + ErrorKind::Unknown, + "Failed to allocate value string.", + )); } CFDictionarySetValue(classes_to_match, key as CFTypeRef, value as CFTypeRef); @@ -946,16 +972,24 @@ pub fn available_ports() -> ::Result> { let mut master_port: mach_port_t = MACH_PORT_NULL; let mut kern_result = IOMasterPort(MACH_PORT_NULL, &mut master_port); if kern_result != KERN_SUCCESS { - return Err(Error::new(ErrorKind::Unknown, format!("ERROR: {}", kern_result))); + return Err(Error::new( + ErrorKind::Unknown, + format!("ERROR: {}", kern_result), + )); } // Run the search. let mut matching_services: io_iterator_t = mem::uninitialized(); - kern_result = IOServiceGetMatchingServices(kIOMasterPortDefault, - classes_to_match, - &mut matching_services); + kern_result = IOServiceGetMatchingServices( + kIOMasterPortDefault, + classes_to_match, + &mut matching_services, + ); if kern_result != KERN_SUCCESS { - return Err(Error::new(ErrorKind::Unknown, format!("ERROR: {}", kern_result))); + return Err(Error::new( + ErrorKind::Unknown, + format!("ERROR: {}", kern_result), + )); } loop { @@ -969,33 +1003,42 @@ pub fn available_ports() -> ::Result> { // Fetch all properties of the current search result item. let mut props = mem::uninitialized(); - let result = IORegistryEntryCreateCFProperties(modem_service, - &mut props, - kCFAllocatorDefault, - 0); + let result = IORegistryEntryCreateCFProperties( + modem_service, + &mut props, + kCFAllocatorDefault, + 0, + ); if result == KERN_SUCCESS { // We only care about the IODialinDevice, which is the device path for this port. let key = CString::new("IODialinDevice").unwrap(); - let key_cfstring = CFStringCreateWithCString(kCFAllocatorDefault, - key.as_ptr(), - kCFStringEncodingUTF8); + let key_cfstring = CFStringCreateWithCString( + kCFAllocatorDefault, + key.as_ptr(), + kCFStringEncodingUTF8, + ); let value = CFDictionaryGetValue(props, key_cfstring as *const c_void); let type_id = CFGetTypeID(value); if type_id == CFStringGetTypeID() { let mut buf = Vec::with_capacity(256); - CFStringGetCString(value as CFStringRef, - buf.as_mut_ptr(), - 256, - kCFStringEncodingUTF8); + CFStringGetCString( + value as CFStringRef, + buf.as_mut_ptr(), + 256, + kCFStringEncodingUTF8, + ); let path = CStr::from_ptr(buf.as_ptr()).to_string_lossy(); vec.push(SerialPortInfo { - port_name: path.to_string(), - port_type: port_type(modem_service), - }); + port_name: path.to_string(), + port_type: port_type(modem_service), + }); } else { - return Err(Error::new(ErrorKind::Unknown, "Found invalid type for TypeID")); + return Err(Error::new( + ErrorKind::Unknown, + "Found invalid type for TypeID", + )); } } else { return Err(Error::new(ErrorKind::Unknown, format!("ERROR: {}", result))); @@ -1011,7 +1054,10 @@ pub fn available_ports() -> ::Result> { #[cfg(not(any(target_os = "linux", target_os = "macos")))] /// Enumerating serial ports on non-Linux POSIX platforms is not yet supported pub fn available_ports() -> ::Result> { - Err(Error::new(ErrorKind::Unknown, "Not implemented for this OS")) + Err(Error::new( + ErrorKind::Unknown, + "Not implemented for this OS", + )) } /// Returns a list of baud rates officially supported by this platform. It's likely more are @@ -1019,7 +1065,7 @@ pub fn available_ports() -> ::Result> { pub fn available_baud_rates() -> Vec { let mut vec = vec![50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800]; #[cfg(any(target_os = "freebsd", target_os = "dragonfly", target_os = "macos", - target_os = "netbsd", target_os = "openbsd"))] + target_os = "netbsd", target_os = "openbsd"))] vec.push(7200); vec.push(9600); #[cfg(any(target_os = "freebsd", target_os = "dragonfly", target_os = "macos", diff --git a/src/windows/com.rs b/src/windows/com.rs index 3205298f408eb10e8175ecc5f4335224d04abd6a..ec4bfc3e41bd0ac042b929e592889556ba869472 100644 --- a/src/windows/com.rs +++ b/src/windows/com.rs @@ -17,9 +17,10 @@ use winapi::um::commapi::*; use winapi::um::errhandlingapi::GetLastError; use winapi::um::fileapi::*; use winapi::um::handleapi::*; +use winapi::um::processthreadsapi::GetCurrentProcess; use winapi::um::setupapi::*; use winapi::um::winbase::*; -use winapi::um::winnt::{FILE_ATTRIBUTE_NORMAL, GENERIC_READ, GENERIC_WRITE, HANDLE, KEY_READ}; +use winapi::um::winnt::{DUPLICATE_SAME_ACCESS, FILE_ATTRIBUTE_NORMAL, GENERIC_READ, GENERIC_WRITE, HANDLE, KEY_READ}; use winapi::um::winreg::*; use {BaudRate, DataBits, FlowControl, Parity, SerialPort, SerialPortInfo, SerialPortSettings, @@ -428,6 +429,33 @@ impl SerialPort for COMPort { self.write_settings() } + + // FIXME : Remove the setting caching as changing the setting through multiple objects can + // cause some problems. + fn try_clone(&self) -> ::Result> { + let process_handle: HANDLE = unsafe {GetCurrentProcess()}; + let mut cloned_handle: HANDLE; + unsafe { + cloned_handle = mem::uninitialized(); + DuplicateHandle(process_handle, + self.handle, + process_handle, + &mut cloned_handle, + 0, + TRUE, + DUPLICATE_SAME_ACCESS); + if cloned_handle != INVALID_HANDLE_VALUE { + Ok(Box::new(COMPort { + handle: cloned_handle, + port_name: self.port_name.clone(), + inner: self.inner.clone(), + timeout: self.timeout, + })) + } else { + Err(super::error::last_os_error()) + } + } + } } // According to the MSDN docs, we should use SetupDiGetClassDevs, SetupDiEnumDeviceInfo diff --git a/tests/test_try_clone.rs b/tests/test_try_clone.rs new file mode 100644 index 0000000000000000000000000000000000000000..86e1c9eda58da6d0d4c7857316fa7725e47f5172 --- /dev/null +++ b/tests/test_try_clone.rs @@ -0,0 +1,61 @@ +#![cfg(unix)] +extern crate serialport; + +use std::io::{Read,Write,ErrorKind}; +use serialport::posix::TTYPort; +use serialport::SerialPort; + +#[test] +fn test_try_clone() { + use std::{mem, thread}; + + let (mut master, mut slave) = TTYPort::pair().expect("Unable to create ptty pair"); + + // Slave tty is just used to forward what the master tty sends back to it. + // So we can clone master tty and test that we can simultaneously read and write + // to it and check that the values are correct. + let loopback = thread::spawn(move || { + let mut buffer: [u8; 10] = unsafe { mem::uninitialized() }; + + // loops until the serial tty is closed + loop { + match slave.read(&mut buffer) { + Ok(bytes) => if bytes > 0 {slave.write(&buffer[0..bytes]).unwrap();} else { break; }, + Err(ref e) => {assert_eq!(e.kind(),ErrorKind::ConnectionAborted);}, + } + } + }); + + // Create clone in an inner scope to test that dropping a clone do not + // shuts off the serial device. + { + let mut clone = master.try_clone().expect("Failed to clone"); + let writes = thread::spawn(move || { + let bytes = [b'a', b'b', b'c', b'd', b'e', b'f']; + clone.write(&bytes).unwrap(); + }); + let mut buffer = [0; 6]; + master.read_exact(&mut buffer).unwrap(); + writes.join().unwrap(); + assert_eq!(buffer, [b'a', b'b', b'c', b'd', b'e', b'f']); + } + + // Second try to check that the serial device is not closed + { + let mut clone = master.try_clone().expect("Failed to clone"); + let writes = thread::spawn(move || { + let bytes = [b'g', b'h', b'i', b'j', b'k', b'l']; + clone.write(&bytes).unwrap(); + }); + let mut buffer = [0; 6]; + master.read_exact(&mut buffer).unwrap(); + writes.join().unwrap(); + assert_eq!(buffer, [b'g', b'h', b'i', b'j', b'k', b'l']); + } + + // Check that once all reference to the master tty are gone, the + // serial device is closed and the loopback thread should finish + mem::drop(master); + loopback.join().unwrap(); +} +