| 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)); |
| } |
| } |