Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serial enumeration for Linux #14

Closed
wants to merge 4 commits into from
Closed

Serial enumeration for Linux #14

wants to merge 4 commits into from

Conversation

Susurrus
Copy link

@Susurrus Susurrus commented Apr 7, 2016

Start of work for #13. This doesn't actually work yet, but I wanted feedback on the API that I'm adding to serial-rs before I go too far down this road. I'm not familiar with Rust enough to know what's "rusty", so I'm basically just copying from the existing API in this lib.

Note that SerialPortInfo will likely change to include more data (see http://code.qt.io/cgit/qt/qtserialport.git/tree/src/serialport/qserialportinfo.h#n62), but for the basic implementation only the port name is required, so I'm planning on leaving it at that for the first implementation.

Biggest bike-shedding issue is what to name scan_ports(), Qt uses AvailablePorts() (which I like)_ while Python uses list_ports.comports() (which I don't). I think available_ports() would be good, and I'll change it in the next revision if there's no complaint.

@dcuddeback
Copy link
Owner

@Susurrus Thanks for looking at this. I've done some work to prepare for this exact feature, which you might find helpful:

On Linux, serial ports can be listed with libudev. I wrote Rust bindings for libudev (https://github.com/dcuddeback/libudev-rs), which can enumerate existing ports or monitor for hotplug events. There's an example of listing devices here: https://github.com/dcuddeback/libudev-rs/blob/master/examples/list_devices.rs.

On OS X, serial ports can be listed with IOKit. I wrote *-sys crates for IOKit and CoreFoundation (https://github.com/dcuddeback/iokit-sys and https://github.com/dcuddeback/core-foundation-sys), but no safe wrappers yet. The IOKit-sys project has an example of listing serial ports: https://github.com/dcuddeback/iokit-sys/blob/master/examples/list_serial_ports.rs.

For Windows, I haven't implemented anything yet. I've been looking at this project as a reference: https://github.com/wjwwood/serial/blob/master/src/impl/list_ports/list_ports_win.cc. That project also has example of using libudev and IOKit.

For other Unix operating systems (this crate supports FreeBSD and OpenBSD, and will support more in the future), I'm not sure what the best approach is going to be. Maybe fallback to scanning /dev? My main concern with scanning devfs is that I'd like to support an event-based API in the future, and polling devfs would be inefficient. That doesn't have to be solved now, though.

As for naming, I personally like list_ports() for a freestanding function that returns a list of serial ports.

@dcuddeback dcuddeback mentioned this pull request Apr 8, 2016
@Susurrus
Copy link
Author

Susurrus commented Apr 8, 2016

So I've got enumeration working, but it pulls all 99 TTY devices on my system. I've plugged in an FTDI cable and here's what it says (using your example list_devices()) code.


initialized: true
     devnum: Some(48128)
    syspath: "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-9/1-9:1.0/ttyUSB0/tty/ttyUSB0"
    devpath: "/devices/pci0000:00/0000:00:14.0/usb1/1-9/1-9:1.0/ttyUSB0/tty/ttyUSB0"
  subsystem: "tty"
    sysname: "ttyUSB0"
     sysnum: Some(0)
    devtype: None
     driver: None
    devnode: Some("/dev/ttyUSB0")
  [properties]
    - "DEVLINKS" "/dev/serial/by-path/pci-0000:00:14.0-usb-0:9:1.0-port0 /dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A600ARDW-if00-port0"
    - "DEVNAME" "/dev/ttyUSB0"
    - "DEVPATH" "/devices/pci0000:00/0000:00:14.0/usb1/1-9/1-9:1.0/ttyUSB0/tty/ttyUSB0"
    - "ID_BUS" "usb"
    - "ID_MM_CANDIDATE" "1"
    - "ID_MODEL" "FT232R_USB_UART"
    - "ID_MODEL_ENC" "FT232R\\x20USB\\x20UART"
    - "ID_MODEL_FROM_DATABASE" "FT232 USB-Serial (UART) IC"
    - "ID_MODEL_ID" "6001"
    - "ID_PATH" "pci-0000:00:14.0-usb-0:9:1.0"
    - "ID_PATH_TAG" "pci-0000_00_14_0-usb-0_9_1_0"
    - "ID_PCI_CLASS_FROM_DATABASE" "Serial bus controller"
    - "ID_PCI_INTERFACE_FROM_DATABASE" "XHCI"
    - "ID_PCI_SUBCLASS_FROM_DATABASE" "USB controller"
    - "ID_REVISION" "0600"
    - "ID_SERIAL" "FTDI_FT232R_USB_UART_A600ARDW"
    - "ID_SERIAL_SHORT" "A600ARDW"
    - "ID_TYPE" "generic"
    - "ID_USB_DRIVER" "ftdi_sio"
    - "ID_USB_INTERFACES" ":ffffff:"
    - "ID_USB_INTERFACE_NUM" "00"
    - "ID_VENDOR" "FTDI"
    - "ID_VENDOR_ENC" "FTDI"
    - "ID_VENDOR_FROM_DATABASE" "Future Technology Devices International, Ltd"
    - "ID_VENDOR_ID" "0403"
    - "MAJOR" "188"
    - "MINOR" "0"
    - "SUBSYSTEM" "tty"
    - "TAGS" ":systemd:"
    - "USEC_INITIALIZED" "1574624740"
    - "hotplugscript" "/etc/.mplab_ide/mchplinusbdevice"
    - "seghotplugscript" "/etc/.mplab_ide/mchpsegusbdevice"
  [attributes]
    - "dev" Some("188:0")
    - "device" None
    - "subsystem" Some("tty")
    - "uevent" Some("MAJOR=188\nMINOR=0\nDEVNAME=ttyUSB0")

Now for the regular TTY devices they look less exciting:

initialized: true
     devnum: Some(1088)
    syspath: "/sys/devices/platform/serial8250/tty/ttyS0"
    devpath: "/devices/platform/serial8250/tty/ttyS0"
  subsystem: "tty"
    sysname: "ttyS0"
     sysnum: Some(0)
    devtype: None
     driver: None
    devnode: Some("/dev/ttyS0")
  [properties]
    - "DEVNAME" "/dev/ttyS0"
    - "DEVPATH" "/devices/platform/serial8250/tty/ttyS0"
    - "ID_MM_CANDIDATE" "1"
    - "MAJOR" "4"
    - "MINOR" "64"
    - "SUBSYSTEM" "tty"
    - "TAGS" ":systemd:"
    - "USEC_INITIALIZED" "4472846"
    - "hotplugscript" "/etc/.mplab_ide/mchplinusbdevice"
    - "seghotplugscript" "/etc/.mplab_ide/mchpsegusbdevice"
  [attributes]
    - "close_delay" None
    - "closing_wait" None
    - "custom_divisor" None
    - "dev" Some("4:64")
    - "device" None
    - "flags" None
    - "io_type" None
    - "iomem_base" None
    - "iomem_reg_shift" None
    - "irq" None
    - "line" None
    - "port" None
    - "subsystem" Some("tty")
    - "type" None
    - "uartclk" None
    - "uevent" Some("MAJOR=4\nMINOR=64\nDEVNAME=ttyS0")
    - "xmit_fifo_size" None

And then there are a few like:

initialized: true
     devnum: Some(1035)
    syspath: "/sys/devices/virtual/tty/tty11"
    devpath: "/devices/virtual/tty/tty11"
  subsystem: "tty"
    sysname: "tty11"
     sysnum: Some(11)
    devtype: None
     driver: None
    devnode: Some("/dev/tty11")
  [properties]
    - "DEVNAME" "/dev/tty11"
    - "DEVPATH" "/devices/virtual/tty/tty11"
    - "ID_MM_CANDIDATE" "1"
    - "MAJOR" "4"
    - "MINOR" "11"
    - "SUBSYSTEM" "tty"
    - "USEC_INITIALIZED" "4524629"
    - "hotplugscript" "/etc/.mplab_ide/mchplinusbdevice"
    - "seghotplugscript" "/etc/.mplab_ide/mchpsegusbdevice"
  [attributes]
    - "dev" Some("4:11")
    - "subsystem" Some("tty")
    - "uevent" Some("MAJOR=4\nMINOR=11\nDEVNAME=tty11")

I'm not certain what the best way to distinguish between these devices is, but if you look at libserialport, then you can see that they first check to see if the device has a parent, discarding them if they don't. Additionally if it's a serial8250 device, then they go through and try to open all of them ignoring them if they can't. I think this is basically our reference implementation to implement. So....how do I get the parent of a device from that device through libudev-rs as there doesn't seem to be a way to do that although libudev itself appears to offer such an API.

@Susurrus Susurrus changed the title RFC: API specification for serial enumeration [DO NOT MERGE] Serial enumeration Apr 10, 2016
@Susurrus
Copy link
Author

I've now added both of those checks that libserialport does (which depend on libudev-rs#5) and this code works. It shows a single FTDI device when I have it connected and none when I don't. I don't have any other devices besides this to test with, so some additional testing is warranted (ideally machines with real serial ports), but I think this is good to merge once libudev gets updated.

@Susurrus
Copy link
Author

And if someone could comment on my use of unwrap(), I'd appreciate it. I don't think they should be used in non-example code as far as I understand, but the code gets tedious quite quickly without them. Any guidance on refining those lines would be appreciated.

@dcuddeback
Copy link
Owner

@Susurrus Thanks for taking the time to write this. I'm not crazy about the idea of opening random devices on the user's computer. I think that goes against expectations of a function that lists devices. Opening a serial device often causes it to toggle it's RTS and DTR lines, which are sometimes hooked up to external equipment. The parent check alone without attempting to open the device returns both of my FTDI devices and about 20 /dev/ttyS* devices. That seems good enough to me and should match other libraries that only check the dev path, such as https://github.com/wjwwood/serial/blob/master/src/impl/list_ports/list_ports_linux.cc#L303.

As for unwrap(), you're right that it should be avoided outside of example code. It's much less tedious if you use something like try!() to wrap each call that can fail. serial::Error shouldn't publicly implement From<libudev::Error>, so I would write an internal macro called try_udev!().

@@ -20,7 +20,7 @@ pub use FlowControl::*;
/// use serial::prelude::*;
/// ```
pub mod prelude {
pub use ::{SerialPort,SerialPortSettings};
pub use ::{SerialPort,SerialPortSettings,SerialPortInfo};
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SerialPortInfo doesn't need to be part of the prelude.

@Susurrus
Copy link
Author

Still not really sure how to deal with all of the unwrap(). I think the right way is to create a helper function that returns a Result and use try!() macros within it for all the main work and then catch that for returning the final values for list_ports().

@dcuddeback
Copy link
Owner

@Susurrus Here's the source code for try!(): https://github.com/rust-lang/rust/blob/c66d2380a810c9a2b3dbb4f93a830b101ee49cc2/src/libcore/macros.rs#L194 All you need to do is replace From with a method to convert libudev errors to serial errors.

@Susurrus
Copy link
Author

Do you mean via specialization or by making a custom try!() macro that calls the correct one?

@Susurrus
Copy link
Author

I think I understand what you mean. Unfortunately using try!() requires that the function return a Result. This means either changing the prototype of list_ports() or writing a helper function. Which do you recommend?

@Susurrus
Copy link
Author

Alright, I've updated my commit. I got some help on r/rust that pointed out a few better ways to do this.

@Susurrus
Copy link
Author

Sorry for the noise, here's the latest. I accidentally included some debugging test code in there (wasn't suitable for a real test).

Also I've bumped the libudev requirements for this to 0.1.3, which is when Device::parent() will first be usable. So this is now properly blocking on dcuddeback/libudev-rs#7.

@dcuddeback
Copy link
Owner

@Susurrus When I suggested write a macro like try!(), I meant that list_ports() should propagate the error by returning a Result. Sorry if that wasn't clear.

I was also thinking that it might make sense to return an iterator instead of a Vec to avoid extra memory allocation. It depends on the details of how serial ports are enumerated for each OS, which I haven't looked into. You can leave it as Result<Vec> for now.

The CI build is failing. The .travis.yml file needs to be updated to install libudev on Linux.

Ok(d) => {
for device in d {
if let Some(_) = device.parent() {
if let Some(path) = device.syspath().to_str() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like devnode (/dev/ttyUSB0) would be more useful to the user than syspath (/sys/devices/pci0000:00/0000:00:14.0/usb1/1-9/1-9:1.0/ttyUSB0/tty/ttyUSB0).

@Susurrus
Copy link
Author

Alright, I've struggled a bit with error types. Finally hit that "fighting the compiler" problem that Rust has for newbies. I wasn't sure a) what error type to use (I think I just use Serial's Error type) and then b) expose the libudev errors through that error type (which I believe is the same). I'm still very new at this and I should probably read up on importing types, but I wanted to get the rest finished for now. I'll take another crack at this tomorrow probably, but all the rest of the changes you mentioned are now there.

Looks like there are still Travis CI errors, but they seem to be regarding libudev itself, so I don't know how to resolve them.

@Susurrus
Copy link
Author

Ha! I made a little progress here. I have pretty much all the scaffolding done, but I'm having an issue with an error:

Compiling serial v0.3.3 (file:///home/susurrus/Projects/serial-rs)
src/posix/tty.rs:662:34: 662:68 error: type ascription is experimental (see issue #23416)
src/posix/tty.rs:662             Err(e) => return Err(super::error:from_libudev_error(e)),
                                                      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/posix/tty.rs:669:34: 669:68 error: type ascription is experimental (see issue #23416)
src/posix/tty.rs:669             Err(e) => return Err(super::error:from_libudev_error(e)),
                                                      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
error: aborting due to 2 previous errors
Could not compile `serial`.

I'm unclear why exactly I'm having this error. It's supposed to be referring to how the type inference in the compiler can't determine an approriate type for this, but I don't understand a) why that is or b) how to fix it. I feel like this is so close, too!

@Susurrus
Copy link
Author

@dcuddeback Any feedback on this? I've also gotten the Travis CI process fixed and working so now this does work on Linux and doesn't break OS X. So it's not the prettiest, but it's sound.

As to your previous suggestion of using Error types, here is a patch that implements it.

diff --git a/src/lib.rs b/src/lib.rs
index d171386..681365f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -49,7 +49,10 @@ pub enum ErrorKind {
     /// A parameter was incorrect.
     InvalidInput,

-    /// An I/O error occured.
+    /// An unknown error occurred.
+    Unknown,
+
+    /// An I/O error occurred.
     ///
     /// The type of I/O error is determined by the inner `io::ErrorKind`.
     Io(io::ErrorKind)
@@ -866,7 +869,7 @@ mod tests {
 }


-/// A device-indepenent implementation of serial port information.
+/// A device-independent implementation of serial port information.
 #[derive(Debug,Clone,PartialEq,Eq)]
 pub struct PortInfo {
     /// Port name
@@ -874,6 +877,6 @@ pub struct PortInfo {
 }

 #[cfg(target_os = "linux")]
-pub fn list_ports() -> Vec<PortInfo> {
+pub fn list_ports() -> Result<Vec<PortInfo>> {
     posix::list_ports()
 }
diff --git a/src/posix/error.rs b/src/posix/error.rs
index b200e26..4e1bf31 100644
--- a/src/posix/error.rs
+++ b/src/posix/error.rs
@@ -1,4 +1,6 @@
 extern crate libc;
+#[cfg(target_os = "linux")]
+extern crate libudev;

 use std::error::Error;
 use std::ffi::CStr;
@@ -37,6 +39,16 @@ pub fn from_io_error(io_error: io::Error) -> ::Error {
     }
 }

+#[cfg(target_os = "linux")]
+pub fn from_libudev_error(libudev_error: libudev::Error) -> ::Error {
+    let description = libudev_error.description().to_string();
+    match libudev_error.kind() {
+        NoMem => ::Error::new(::ErrorKind::Unknown, description),
+        InvalidInput => ::Error::new(::ErrorKind::InvalidInput, description),
+        libudev::ErrorKind::Io(a) => ::Error::new(::ErrorKind::Io(a), description)
+    }
+}
+
 // the rest of this module is borrowed from libstd

 const TMPBUF_SZ: usize = 128;
diff --git a/src/posix/tty.rs b/src/posix/tty.rs
index 5922690..b7cf378 100644
--- a/src/posix/tty.rs
+++ b/src/posix/tty.rs
@@ -13,7 +13,7 @@ use std::os::unix::prelude::*;

 use self::libc::{c_int,c_void,size_t};

-use ::{SerialDevice,SerialPortSettings,PortInfo};
+use ::{SerialDevice,SerialPortSettings,PortInfo,ErrorKind,Error};


 #[cfg(target_os = "linux")]
@@ -535,7 +535,7 @@ impl SerialPortSettings for TTYSettings {
 mod tests {
     use std::mem;

-    use super::TTYSettings;
+    use super::{list_ports, TTYSettings};
     use ::prelude::*;

     fn default_settings() -> TTYSettings {
@@ -645,20 +645,28 @@ mod tests {
         settings.set_flow_control(::FlowNone);
         assert_eq!(settings.flow_control(), Some(::FlowNone));
     }
+
+    #[test]
+    fn list_ports_example() {
+        for p in list_ports() {
+            println!("Port: {}", p.port_name);
+        }
+    }
 }

 #[cfg(target_os = "linux")]
-pub fn list_ports() -> Vec<PortInfo> {
+pub fn list_ports() -> ::Result<Vec<PortInfo>> {
     let mut vec = Vec::new();
     if let Ok(context) = libudev::Context::new() {
         let mut enumerator = match libudev::Enumerator::new(&context) {
-            Err(_) => return vec,
+            Err(e) => return Err(super::error:from_libudev_error(e)),
             Ok(k) => k
         };
         if enumerator.match_subsystem("tty").is_err() {
-            return vec;
+            return Err(Error::new(ErrorKind::Unknown, "An unknown error occurred.".to_string()))
         }
         match enumerator.scan_devices() {
+            Err(e) => return Err(super::error:from_libudev_error(e)),
             Ok(d) => {
                 for device in d {
                     if device.parent().is_some() {
@@ -669,9 +677,8 @@ pub fn list_ports() -> Vec<PortInfo> {
                         }
                     }
                 }
-            },
-            Err(_) => return vec
+            }
         }
     }
-    vec
+    Ok(vec)
 }

Unfortunately it fails with:

$ cargo build
   Compiling serial v0.3.3 (file:///home/susurrus/Projects/serial-rs)
src/posix/tty.rs:662:34: 662:68 error: type ascription is experimental (see issue #23416)
src/posix/tty.rs:662             Err(e) => return Err(super::error:from_libudev_error(e)),
                                                      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/posix/tty.rs:669:34: 669:68 error: type ascription is experimental (see issue #23416)
src/posix/tty.rs:669             Err(e) => return Err(super::error:from_libudev_error(e)),
                                                      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
error: aborting due to 2 previous errors
Could not compile `serial`.

To learn more, run the command again with --verbose.

And right now I don't really know how to fix that.

@Susurrus
Copy link
Author

Nevermind, someone in #rust on IRC helped me figure out I had a syntax error above. I've now gotten everything fixed and I'm using the try!() macro. I think it looks pretty clean now. Also got some other code of mine using it to locate any serial ports and it works, so I think this is ready to ship!

@Susurrus
Copy link
Author

Let me also say that I really think that we should check all the ttyS* ports by opening them. I end up with 32 devices in my list that are fake and 1 real one. Considering both Qt and libserialport have this functionality I would consider it safe, even though you don't like it. While I understand not liking it, I also think that if someone has a real serialport and they have something connected to it that blows up when the port is turned on, they have issues with their hardware that existed before they ran our software.

@Susurrus Susurrus changed the title Serial enumeration Serial enumeration for Linux Apr 24, 2016
@Susurrus
Copy link
Author

Added an additional commit to remove serial8250 virtual ports in case we can get that merged.

@Susurrus
Copy link
Author

Susurrus commented May 8, 2016

@dcuddeback Just pinging you on this. What do you think about merging this now?

@dcuddeback
Copy link
Owner

@Susurrus I'd like to have implementations ready for Windows, OS X, BSD, and other Unixes before merging this. If I were to give you some feedback on this PR now, I think I'd be repeating things from earlier in this thread. I think it's a little premature for that, since adding implementations for the other operating systems may change how this is implemented anyway.

I also have a branch in progress that restructures this library quite heavily. I'd recommend putting this on the back-burner until that is done to avoid unnecessary merge conflicts.

@dcuddeback dcuddeback mentioned this pull request May 14, 2016
@mairsbw
Copy link

mairsbw commented May 14, 2016

I've added Windows now, and also added an unimplemented error for unsupported OSes.

@Susurrus
Copy link
Author

Note that adding the winapi crate pushes the minimum Rust requirement to 1.4.

@dcuddeback
Copy link
Owner

Closing due to inactivity. I'll probably work on this feature myself.

@dcuddeback dcuddeback closed this Jun 14, 2016
@mairsbw
Copy link

mairsbw commented Jun 14, 2016

What?! Why? This is fully implemented and working, why did you close this? I'm actively using this code on Linux and I've tested the Windows version as well. There's no activity for a wile because I've been waiting for you to review the code, provide feedback, and merge this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants