| use cfg_if::cfg_if; | |
| cfg_if! { | |
| if #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))]{ | |
| use std::ffi::OsStr; | |
| } | |
| } | |
| cfg_if! { | |
| if #[cfg(any(target_os = "ios", target_os = "macos"))] { | |
| use core_foundation::base::CFType; | |
| use core_foundation::base::TCFType; | |
| use core_foundation::dictionary::CFDictionary; | |
| use core_foundation::dictionary::CFMutableDictionary; | |
| use core_foundation::number::CFNumber; | |
| use core_foundation::string::CFString; | |
| use core_foundation_sys::base::{kCFAllocatorDefault, CFRetain}; | |
| use io_kit_sys::*; | |
| use io_kit_sys::keys::*; | |
| use io_kit_sys::serial::keys::*; | |
| use io_kit_sys::types::*; | |
| use io_kit_sys::usb::lib::*; | |
| use nix::libc::{c_char, c_void}; | |
| use std::ffi::CStr; | |
| use std::mem::MaybeUninit; | |
| } | |
| } | |
| #[cfg(any( | |
| target_os = "freebsd", | |
| target_os = "ios", | |
| target_os = "linux", | |
| target_os = "macos" | |
| ))] | |
| use crate::SerialPortType; | |
| #[cfg(any(target_os = "ios", target_os = "linux", target_os = "macos"))] | |
| use crate::UsbPortInfo; | |
| #[cfg(any( | |
| target_os = "android", | |
| target_os = "ios", | |
| all(target_os = "linux", not(target_env = "musl"), feature = "libudev"), | |
| target_os = "macos", | |
| target_os = "netbsd", | |
| target_os = "openbsd", | |
| ))] | |
| use crate::{Error, ErrorKind}; | |
| use crate::{Result, SerialPortInfo}; | |
| /// Retrieves the udev property value named by `key`. If the value exists, then it will be | |
| /// converted to a String, otherwise None will be returned. | |
| #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))] | |
| fn udev_property_as_string(d: &libudev::Device, key: &str) -> Option<String> { | |
| d.property_value(key) | |
| .and_then(OsStr::to_str) | |
| .map(|s| s.to_string()) | |
| } | |
| /// Retrieves the udev property value named by `key`. This function assumes that the retrieved | |
| /// string is comprised of hex digits and the integer value of this will be returned as a u16. | |
| /// If the property value doesn't exist or doesn't contain valid hex digits, then an error | |
| /// will be returned. | |
| /// This function uses a built-in type's `from_str_radix` to implementation to perform the | |
| /// actual conversion. | |
| #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))] | |
| fn udev_hex_property_as_int<T>( | |
| d: &libudev::Device, | |
| key: &str, | |
| from_str_radix: &dyn Fn(&str, u32) -> std::result::Result<T, std::num::ParseIntError>, | |
| ) -> Result<T> { | |
| if let Some(hex_str) = d.property_value(key).and_then(OsStr::to_str) { | |
| if let Ok(num) = from_str_radix(hex_str, 16) { | |
| Ok(num) | |
| } else { | |
| Err(Error::new(ErrorKind::Unknown, "value not hex string")) | |
| } | |
| } else { | |
| Err(Error::new(ErrorKind::Unknown, "key not found")) | |
| } | |
| } | |
| /// Looks up a property which is provided in two "flavors": Where special charaters and whitespaces | |
| /// are encoded/escaped and where they are replaced (with underscores). This is for example done | |
| /// by udev for manufacturer and model information. | |
| /// | |
| /// See | |
| /// https://github.com/systemd/systemd/blob/38c258398427d1f497268e615906759025e51ea6/src/udev/udev-builtin-usb_id.c#L432 | |
| /// for details. | |
| #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))] | |
| fn udev_property_encoded_or_replaced_as_string( | |
| d: &libudev::Device, | |
| encoded_key: &str, | |
| replaced_key: &str, | |
| ) -> Option<String> { | |
| udev_property_as_string(d, encoded_key) | |
| .and_then(|s| unescaper::unescape(&s).ok()) | |
| .or_else(|| udev_property_as_string(d, replaced_key)) | |
| .map(udev_restore_spaces) | |
| } | |
| /// Converts the underscores from `udev_replace_whitespace` back to spaces quick and dirtily. We | |
| /// are ignoring the different types of whitespaces and the substitutions from `udev_replace_chars` | |
| /// deliberately for keeping a low profile. | |
| /// | |
| /// See | |
| /// https://github.com/systemd/systemd/blob/38c258398427d1f497268e615906759025e51ea6/src/shared/udev-util.c#L281 | |
| /// for more details. | |
| #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))] | |
| fn udev_restore_spaces(source: String) -> String { | |
| source.replace('_', " ") | |
| } | |
| #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))] | |
| fn port_type(d: &libudev::Device) -> Result<SerialPortType> { | |
| match d.property_value("ID_BUS").and_then(OsStr::to_str) { | |
| Some("usb") => { | |
| let serial_number = udev_property_as_string(d, "ID_SERIAL_SHORT"); | |
| // For devices on the USB, udev also provides manufacturer and product information from | |
| // its hardware dataase. Use this as a fallback if this information is not provided | |
| // from the device itself. | |
| let manufacturer = | |
| udev_property_encoded_or_replaced_as_string(d, "ID_VENDOR_ENC", "ID_VENDOR") | |
| .or_else(|| udev_property_as_string(d, "ID_VENDOR_FROM_DATABASE")); | |
| let product = | |
| udev_property_encoded_or_replaced_as_string(d, "ID_MODEL_ENC", "ID_MODEL") | |
| .or_else(|| udev_property_as_string(d, "ID_MODEL_FROM_DATABASE")); | |
| Ok(SerialPortType::UsbPort(UsbPortInfo { | |
| vid: udev_hex_property_as_int(d, "ID_VENDOR_ID", &u16::from_str_radix)?, | |
| pid: udev_hex_property_as_int(d, "ID_MODEL_ID", &u16::from_str_radix)?, | |
| serial_number, | |
| manufacturer, | |
| product, | |
| #[cfg(feature = "usbportinfo-interface")] | |
| interface: udev_hex_property_as_int(d, "ID_USB_INTERFACE_NUM", &u8::from_str_radix) | |
| .ok(), | |
| })) | |
| } | |
| Some("pci") => { | |
| let usb_properties = vec![ | |
| d.property_value("ID_USB_VENDOR_ID"), | |
| d.property_value("ID_USB_MODEL_ID"), | |
| ] | |
| .into_iter() | |
| .collect::<Option<Vec<_>>>(); | |
| if usb_properties.is_some() { | |
| // For USB devices reported at a PCI bus, there is apparently no fallback | |
| // information from udevs hardware database provided. | |
| let manufacturer = udev_property_encoded_or_replaced_as_string( | |
| d, | |
| "ID_USB_VENDOR_ENC", | |
| "ID_USB_VENDOR", | |
| ); | |
| let product = udev_property_encoded_or_replaced_as_string( | |
| d, | |
| "ID_USB_MODEL_ENC", | |
| "ID_USB_MODEL", | |
| ); | |
| Ok(SerialPortType::UsbPort(UsbPortInfo { | |
| vid: udev_hex_property_as_int(d, "ID_USB_VENDOR_ID", &u16::from_str_radix)?, | |
| pid: udev_hex_property_as_int(d, "ID_USB_MODEL_ID", &u16::from_str_radix)?, | |
| serial_number: udev_property_as_string(d, "ID_USB_SERIAL_SHORT"), | |
| manufacturer, | |
| product, | |
| #[cfg(feature = "usbportinfo-interface")] | |
| interface: udev_hex_property_as_int( | |
| d, | |
| "ID_USB_INTERFACE_NUM", | |
| &u8::from_str_radix, | |
| ) | |
| .ok(), | |
| })) | |
| } else { | |
| Ok(SerialPortType::PciPort) | |
| } | |
| } | |
| None => find_usb_interface_from_parents(d.parent()) | |
| .and_then(get_modalias_from_device) | |
| .as_deref() | |
| .and_then(parse_modalias) | |
| .map_or(Ok(SerialPortType::Unknown), |port_info| { | |
| Ok(SerialPortType::UsbPort(port_info)) | |
| }), | |
| _ => Ok(SerialPortType::Unknown), | |
| } | |
| } | |
| #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))] | |
| fn find_usb_interface_from_parents(parent: Option<libudev::Device>) -> Option<libudev::Device> { | |
| let mut p = parent?; | |
| // limit the query depth | |
| for _ in 1..4 { | |
| match p.devtype() { | |
| None => match p.parent() { | |
| None => break, | |
| Some(x) => p = x, | |
| }, | |
| Some(s) => { | |
| if s.to_str()? == "usb_interface" { | |
| break; | |
| } else { | |
| match p.parent() { | |
| None => break, | |
| Some(x) => p = x, | |
| } | |
| } | |
| } | |
| } | |
| } | |
| Some(p) | |
| } | |
| #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))] | |
| fn get_modalias_from_device(d: libudev::Device) -> Option<String> { | |
| Some( | |
| d.property_value("MODALIAS") | |
| .and_then(OsStr::to_str)? | |
| .to_owned(), | |
| ) | |
| } | |
| // MODALIAS = usb:v303Ap1001d0101dcEFdsc02dp01ic02isc02ip00in00 | |
| // v 303A (device vendor) | |
| // p 1001 (device product) | |
| // d 0101 (bcddevice) | |
| // dc EF (device class) | |
| // dsc 02 (device subclass) | |
| // dp 01 (device protocol) | |
| // ic 02 (interface class) | |
| // isc 02 (interface subclass) | |
| // ip 00 (interface protocol) | |
| // in 00 (interface number) | |
| #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))] | |
| fn parse_modalias(moda: &str) -> Option<UsbPortInfo> { | |
| // Find the start of the string, will start with "usb:" | |
| let mod_start = moda.find("usb:v")?; | |
| // Tail to update while searching. | |
| let mut mod_tail = moda.get(mod_start + 5..)?; | |
| // The next four characters should be hex values of the vendor. | |
| let vid = mod_tail.get(..4)?; | |
| mod_tail = mod_tail.get(4..)?; | |
| // The next portion we care about is the device product ID. | |
| let pid_start = mod_tail.find('p')?; | |
| let pid = mod_tail.get(pid_start + 1..pid_start + 5)?; | |
| Some(UsbPortInfo { | |
| vid: u16::from_str_radix(vid, 16).ok()?, | |
| pid: u16::from_str_radix(pid, 16).ok()?, | |
| serial_number: None, | |
| manufacturer: None, | |
| product: None, | |
| // Only attempt to find the interface if the feature is enabled. | |
| #[cfg(feature = "usbportinfo-interface")] | |
| interface: mod_tail.get(pid_start + 4..).and_then(|mod_tail| { | |
| mod_tail.find("in").and_then(|i_start| { | |
| mod_tail | |
| .get(i_start + 2..i_start + 4) | |
| .and_then(|interface| u8::from_str_radix(interface, 16).ok()) | |
| }) | |
| }), | |
| }) | |
| } | |
| #[cfg(any(target_os = "ios", target_os = "macos"))] | |
| fn get_parent_device_by_type( | |
| device: io_object_t, | |
| parent_type: *const c_char, | |
| ) -> Option<io_registry_entry_t> { | |
| let parent_type = unsafe { CStr::from_ptr(parent_type) }; | |
| use mach2::kern_return::KERN_SUCCESS; | |
| let mut device = device; | |
| loop { | |
| let mut class_name = MaybeUninit::<[c_char; 128]>::uninit(); | |
| unsafe { IOObjectGetClass(device, class_name.as_mut_ptr() as *mut c_char) }; | |
| let class_name = unsafe { class_name.assume_init() }; | |
| let name = unsafe { CStr::from_ptr(&class_name[0]) }; | |
| if name == parent_type { | |
| return Some(device); | |
| } | |
| let mut parent = MaybeUninit::uninit(); | |
| if unsafe { | |
| IORegistryEntryGetParentEntry(device, kIOServiceClass, parent.as_mut_ptr()) | |
| != KERN_SUCCESS | |
| } { | |
| return None; | |
| } | |
| device = unsafe { parent.assume_init() }; | |
| } | |
| } | |
| #[cfg(any(target_os = "ios", 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) -> Result<u32> { | |
| let cf_property = CFString::new(property); | |
| let cf_type_ref = unsafe { | |
| IORegistryEntryCreateCFProperty( | |
| device_type, | |
| cf_property.as_concrete_TypeRef(), | |
| kCFAllocatorDefault, | |
| 0, | |
| ) | |
| }; | |
| if cf_type_ref.is_null() { | |
| return Err(Error::new(ErrorKind::Unknown, "Failed to get property")); | |
| } | |
| let cf_type = unsafe { CFType::wrap_under_create_rule(cf_type_ref) }; | |
| cf_type | |
| .downcast::<CFNumber>() | |
| .and_then(|n| n.to_i64()) | |
| .map(|n| n as u32) | |
| .ok_or(Error::new( | |
| ErrorKind::Unknown, | |
| "Failed to get numerical value", | |
| )) | |
| } | |
| #[cfg(any(target_os = "ios", target_os = "macos"))] | |
| /// Returns a specific property of the given device as a string. | |
| fn get_string_property(device_type: io_registry_entry_t, property: &str) -> Result<String> { | |
| let cf_property = CFString::new(property); | |
| let cf_type_ref = unsafe { | |
| IORegistryEntryCreateCFProperty( | |
| device_type, | |
| cf_property.as_concrete_TypeRef(), | |
| kCFAllocatorDefault, | |
| 0, | |
| ) | |
| }; | |
| if cf_type_ref.is_null() { | |
| return Err(Error::new(ErrorKind::Unknown, "Failed to get property")); | |
| } | |
| let cf_type = unsafe { CFType::wrap_under_create_rule(cf_type_ref) }; | |
| cf_type | |
| .downcast::<CFString>() | |
| .map(|s| s.to_string()) | |
| .ok_or(Error::new(ErrorKind::Unknown, "Failed to get string value")) | |
| } | |
| #[cfg(any(target_os = "ios", target_os = "macos"))] | |
| /// Determine the serial port type based on the service object (like that returned by | |
| /// `IOIteratorNext`). Specific properties are extracted for USB devices. | |
| fn port_type(service: io_object_t) -> SerialPortType { | |
| let bluetooth_device_class_name = b"IOBluetoothSerialClient\0".as_ptr() as *const c_char; | |
| let usb_device_class_name = b"IOUSBHostInterface\0".as_ptr() as *const c_char; | |
| let legacy_usb_device_class_name = kIOUSBDeviceClassName; | |
| let maybe_usb_device = get_parent_device_by_type(service, usb_device_class_name) | |
| .or_else(|| get_parent_device_by_type(service, legacy_usb_device_class_name)); | |
| if let Some(usb_device) = maybe_usb_device { | |
| SerialPortType::UsbPort(UsbPortInfo { | |
| vid: get_int_property(usb_device, "idVendor").unwrap_or_default() as u16, | |
| pid: get_int_property(usb_device, "idProduct").unwrap_or_default() as u16, | |
| serial_number: get_string_property(usb_device, "USB Serial Number").ok(), | |
| manufacturer: get_string_property(usb_device, "USB Vendor Name").ok(), | |
| product: get_string_property(usb_device, "USB Product Name").ok(), | |
| // Apple developer documentation indicates `bInterfaceNumber` is the supported key for | |
| // looking up the composite usb interface id. `idVendor` and `idProduct` are included in the same tables, so | |
| // we will lookup the interface number using the same method. See: | |
| // | |
| // https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_driverkit_transport_usb | |
| // https://developer.apple.com/library/archive/documentation/DeviceDrivers/Conceptual/USBBook/USBOverview/USBOverview.html#//apple_ref/doc/uid/TP40002644-BBCEACAJ | |
| #[cfg(feature = "usbportinfo-interface")] | |
| interface: get_int_property(usb_device, "bInterfaceNumber") | |
| .map(|x| x as u8) | |
| .ok(), | |
| }) | |
| } else if get_parent_device_by_type(service, bluetooth_device_class_name).is_some() { | |
| SerialPortType::BluetoothPort | |
| } else { | |
| SerialPortType::PciPort | |
| } | |
| } | |
| cfg_if! { | |
| if #[cfg(any(target_os = "ios", target_os = "macos"))] { | |
| /// Scans the system for serial ports and returns a list of them. | |
| /// The `SerialPortInfo` struct contains the name of the port which can be used for opening it. | |
| pub fn available_ports() -> Result<Vec<SerialPortInfo>> { | |
| use mach2::kern_return::KERN_SUCCESS; | |
| use mach2::port::{mach_port_t, MACH_PORT_NULL}; | |
| let mut vec = Vec::new(); | |
| unsafe { | |
| // 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.", | |
| )); | |
| } | |
| let mut classes_to_match = CFMutableDictionary::wrap_under_create_rule(classes_to_match); | |
| // Populate the search dictionary with a single key/value pair indicating that we're | |
| // searching for serial devices matching the RS232 device type. | |
| let search_key = CStr::from_ptr(kIOSerialBSDTypeKey); | |
| let search_key = CFString::from_static_string(search_key.to_str().map_err(|_| Error::new(ErrorKind::Unknown, "Failed to convert search key string"))?); | |
| let search_value = CStr::from_ptr(kIOSerialBSDAllTypes); | |
| let search_value = CFString::from_static_string(search_value.to_str().map_err(|_| Error::new(ErrorKind::Unknown, "Failed to convert search key string"))?); | |
| classes_to_match.set(search_key, search_value); | |
| // Get an interface to IOKit | |
| 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), | |
| )); | |
| } | |
| // Run the search. IOServiceGetMatchingServices consumes one reference count of | |
| // classes_to_match, so explicitly retain. | |
| // | |
| // TODO: We could also just mem::forget classes_to_match like in | |
| // TCFType::into_CFType. Is there a special reason that there is no | |
| // TCFType::into_concrete_TypeRef()? | |
| CFRetain(classes_to_match.as_CFTypeRef()); | |
| let mut matching_services = MaybeUninit::uninit(); | |
| kern_result = IOServiceGetMatchingServices( | |
| kIOMasterPortDefault, | |
| classes_to_match.as_concrete_TypeRef(), | |
| matching_services.as_mut_ptr(), | |
| ); | |
| if kern_result != KERN_SUCCESS { | |
| return Err(Error::new( | |
| ErrorKind::Unknown, | |
| format!("ERROR: {}", kern_result), | |
| )); | |
| } | |
| let matching_services = matching_services.assume_init(); | |
| let _matching_services_guard = scopeguard::guard((), |_| { | |
| IOObjectRelease(matching_services); | |
| }); | |
| loop { | |
| // Grab the next result. | |
| let modem_service = IOIteratorNext(matching_services); | |
| // Break out if we've reached the end of the iterator | |
| if modem_service == MACH_PORT_NULL { | |
| break; | |
| } | |
| let _modem_service_guard = scopeguard::guard((), |_| { | |
| IOObjectRelease(modem_service); | |
| }); | |
| // Fetch all properties of the current search result item. | |
| let mut props = MaybeUninit::uninit(); | |
| let result = IORegistryEntryCreateCFProperties( | |
| modem_service, | |
| props.as_mut_ptr(), | |
| kCFAllocatorDefault, | |
| 0, | |
| ); | |
| if result == KERN_SUCCESS { | |
| // A successful call to IORegistryEntryCreateCFProperties indicates that a | |
| // properties dict has been allocated and we as the caller are in charge of | |
| // releasing it. | |
| let props = props.assume_init(); | |
| let props: CFDictionary<CFString, *const c_void> = CFDictionary::wrap_under_create_rule(props); | |
| for key in ["IOCalloutDevice", "IODialinDevice"].iter() { | |
| let cf_key = CFString::new(key); | |
| if let Some(cf_ref) = props.find(cf_key) { | |
| let cf_type = CFType::wrap_under_get_rule(*cf_ref); | |
| match cf_type | |
| .downcast::<CFString>() | |
| .map(|s| s.to_string()) | |
| { | |
| Some(path) => { | |
| vec.push(SerialPortInfo { | |
| port_name: path, | |
| port_type: port_type(modem_service), | |
| }); | |
| } | |
| None => return Err(Error::new(ErrorKind::Unknown, format!("Failed to get string value for {}", key))), | |
| } | |
| } else { | |
| return Err(Error::new(ErrorKind::Unknown, format!("Key {} missing in dict", key))); | |
| } | |
| } | |
| } else { | |
| return Err(Error::new(ErrorKind::Unknown, format!("ERROR: {}", result))); | |
| } | |
| } | |
| } | |
| Ok(vec) | |
| } | |
| } else if #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))] { | |
| /// Scans the system for serial ports and returns a list of them. | |
| /// The `SerialPortInfo` struct contains the name of the port | |
| /// which can be used for opening it. | |
| pub fn available_ports() -> Result<Vec<SerialPortInfo>> { | |
| let mut vec = Vec::new(); | |
| if let Ok(context) = libudev::Context::new() { | |
| let mut enumerator = libudev::Enumerator::new(&context)?; | |
| enumerator.match_subsystem("tty")?; | |
| let devices = enumerator.scan_devices()?; | |
| for d in devices { | |
| if let Some(p) = d.parent() { | |
| if let Some(devnode) = d.devnode() { | |
| if let Some(path) = devnode.to_str() { | |
| if let Some(driver) = p.driver() { | |
| if driver == "serial8250" && crate::new(path, 9600).open().is_err() { | |
| continue; | |
| } | |
| } | |
| // Stop bubbling up port_type errors here so problematic ports are just | |
| // 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, | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| Ok(vec) | |
| } | |
| } else if #[cfg(target_os = "linux")] { | |
| use std::fs::File; | |
| use std::io::Read; | |
| use std::path::Path; | |
| fn read_file_to_trimmed_string(dir: &Path, file: &str) -> Option<String> { | |
| let path = dir.join(file); | |
| let mut s = String::new(); | |
| File::open(path).ok()?.read_to_string(&mut s).ok()?; | |
| Some(s.trim().to_owned()) | |
| } | |
| fn read_file_to_u16(dir: &Path, file: &str) -> Option<u16> { | |
| u16::from_str_radix(&read_file_to_trimmed_string(dir, file)?, 16).ok() | |
| } | |
| #[cfg(feature = "usbportinfo-interface")] | |
| fn read_file_to_u8(dir: &Path, file: &str) -> Option<u8> { | |
| u8::from_str_radix(&read_file_to_trimmed_string(dir, file)?, 16).ok() | |
| } | |
| fn read_port_type(path: &Path) -> Option<SerialPortType> { | |
| let path = path | |
| .canonicalize() | |
| .ok()?; | |
| let subsystem = path.join("subsystem").canonicalize().ok()?; | |
| let subsystem = subsystem.file_name()?.to_string_lossy(); | |
| match subsystem.as_ref() { | |
| // Broadcom SoC UARTs (of Raspberry Pi devices). | |
| "amba" => Some(SerialPortType::Unknown), | |
| "pci" => Some(SerialPortType::PciPort), | |
| "pnp" => Some(SerialPortType::Unknown), | |
| "usb" => usb_port_type(&path), | |
| "usb-serial" => usb_port_type(path.parent()?), | |
| _ => None, | |
| } | |
| } | |
| fn usb_port_type(interface_path: &Path) -> Option<SerialPortType> { | |
| let info = read_usb_port_info(interface_path)?; | |
| Some(SerialPortType::UsbPort(info)) | |
| } | |
| fn read_usb_port_info(interface_path: &Path) -> Option<UsbPortInfo> { | |
| let device_path = interface_path.parent()?; | |
| let vid = read_file_to_u16(&device_path, "idVendor")?; | |
| let pid = read_file_to_u16(&device_path, "idProduct")?; | |
| #[cfg(feature = "usbportinfo-interface")] | |
| let interface = read_file_to_u8(&interface_path, &"bInterfaceNumber"); | |
| let serial_number = read_file_to_trimmed_string(&device_path, &"serial"); | |
| let product = read_file_to_trimmed_string(&device_path, &"product"); | |
| let manufacturer = read_file_to_trimmed_string(&device_path, &"manufacturer"); | |
| Some(UsbPortInfo { | |
| vid, | |
| pid, | |
| serial_number, | |
| manufacturer, | |
| product, | |
| #[cfg(feature = "usbportinfo-interface")] | |
| interface, | |
| }) | |
| } | |
| /// Scans `/sys/class/tty` for serial devices (on Linux systems without libudev). | |
| pub fn available_ports() -> Result<Vec<SerialPortInfo>> { | |
| let mut vec = Vec::new(); | |
| let sys_path = Path::new("/sys/class/tty/"); | |
| let dev_path = Path::new("/dev"); | |
| for path in sys_path.read_dir().expect("/sys/class/tty/ doesn't exist on this system") { | |
| let raw_path = path?.path().clone(); | |
| let mut path = raw_path.clone(); | |
| path.push("device"); | |
| if !path.is_dir() { | |
| continue; | |
| } | |
| // Determine port type and proceed, if it's a known. | |
| // | |
| // TODO: Switch to a likely more readable let-else statement when our MSRV supports | |
| // it. | |
| let port_type = read_port_type(&path); | |
| let port_type = if let Some(port_type) = port_type { | |
| port_type | |
| } else { | |
| continue; | |
| }; | |
| // Generate the device file path `/dev/DEVICE` from the TTY class path | |
| // `/sys/class/tty/DEVICE` and emit a serial device if this path exists. There are | |
| // no further checks (yet) due to `Path::is_file` reports only regular files. | |
| // | |
| // See https://github.com/serialport/serialport-rs/issues/66 for details. | |
| if let Some(file_name) = raw_path.file_name() { | |
| let device_file = dev_path.join(file_name); | |
| if !device_file.exists() { | |
| continue; | |
| } | |
| vec.push(SerialPortInfo { | |
| port_name: device_file.to_string_lossy().to_string(), | |
| port_type, | |
| }); | |
| } | |
| } | |
| Ok(vec) | |
| } | |
| } else if #[cfg(target_os = "freebsd")] { | |
| use std::path::Path; | |
| /// Scans the system for serial ports and returns a list of them. | |
| /// The `SerialPortInfo` struct contains the name of the port | |
| /// which can be used for opening it. | |
| pub fn available_ports() -> Result<Vec<SerialPortInfo>> { | |
| let mut vec = Vec::new(); | |
| let dev_path = Path::new("/dev/"); | |
| for path in dev_path.read_dir()? { | |
| let path = path?; | |
| let filename = path.file_name(); | |
| let filename_string = filename.to_string_lossy(); | |
| if filename_string.starts_with("cuaU") || filename_string.starts_with("cuau") || filename_string.starts_with("cuad") { | |
| if !filename_string.ends_with(".init") && !filename_string.ends_with(".lock") { | |
| vec.push(SerialPortInfo { | |
| port_name: path.path().to_string_lossy().to_string(), | |
| port_type: SerialPortType::Unknown, | |
| }); | |
| } | |
| } | |
| } | |
| Ok(vec) | |
| } | |
| } else { | |
| /// Enumerating serial ports on this platform is not supported | |
| pub fn available_ports() -> Result<Vec<SerialPortInfo>> { | |
| Err(Error::new( | |
| ErrorKind::Unknown, | |
| "Not implemented for this OS", | |
| )) | |
| } | |
| } | |
| } | |
| #[cfg(all( | |
| test, | |
| target_os = "linux", | |
| not(target_env = "musl"), | |
| feature = "libudev" | |
| ))] | |
| mod tests { | |
| use super::*; | |
| use quickcheck_macros::quickcheck; | |
| #[quickcheck] | |
| fn quickcheck_parse_modalias_does_not_panic_from_random_data(modalias: String) -> bool { | |
| let _ = parse_modalias(&modalias); | |
| true | |
| } | |
| #[test] | |
| fn parse_modalias_canonical() { | |
| const MODALIAS: &str = "usb:v303Ap1001d0101dcEFdsc02dp01ic02isc02ip00in0C"; | |
| let port_info = parse_modalias(MODALIAS).expect("parse failed"); | |
| assert_eq!(port_info.vid, 0x303A, "vendor parse invalid"); | |
| assert_eq!(port_info.pid, 0x1001, "product parse invalid"); | |
| #[cfg(feature = "usbportinfo-interface")] | |
| assert_eq!(port_info.interface, Some(0x0C), "interface parse invalid"); | |
| } | |
| #[test] | |
| fn parse_modalias_corner_cases() { | |
| assert!(parse_modalias("").is_none()); | |
| assert!(parse_modalias("usb").is_none()); | |
| assert!(parse_modalias("usb:").is_none()); | |
| assert!(parse_modalias("usb:vdcdc").is_none()); | |
| assert!(parse_modalias("usb:pdcdc").is_none()); | |
| // Just vendor and product IDs. | |
| let info = parse_modalias("usb:vdcdcpabcd").unwrap(); | |
| assert_eq!(info.vid, 0xdcdc); | |
| assert_eq!(info.pid, 0xabcd); | |
| #[cfg(feature = "usbportinfo-interface")] | |
| assert!(info.interface.is_none()); | |
| // Vendor and product ID plus an interface number. | |
| let info = parse_modalias("usb:v1234p5678indc").unwrap(); | |
| assert_eq!(info.vid, 0x1234); | |
| assert_eq!(info.pid, 0x5678); | |
| #[cfg(feature = "usbportinfo-interface")] | |
| assert_eq!(info.interface, Some(0xdc)); | |
| } | |
| } |