dcuddeback / serial-rs

Rust library for interacting with serial ports.
MIT License
208 stars 63 forks source link

API for allowing different timings for settings-changes to take effect #43

Open mbr opened 7 years ago

mbr commented 7 years ago

Currently, settings (on Linux) are changed using tcsetattr in write_settings, passing in TCSANOW for optional_actions. This causes the new settings to take effect immediately, here's the code in question:

    fn write_settings(&mut self, settings: &TTYSettings) -> ::Result<()> {
        use self::termios::{tcsetattr, tcflush};
        use self::termios::{TCSANOW, TCIOFLUSH};

        // write settings to TTY
        if let Err(err) = tcsetattr(self.fd, TCSANOW, &settings.termios) {
            return Err(super::error::from_io_error(err));
        }

        if let Err(err) = tcflush(self.fd, TCIOFLUSH) {
            return Err(super::error::from_io_error(err));
        }

        Ok(())
    }

WIth the proposed pull request #42 , this (on Linux) changes to:

    #[cfg(target_os = "linux")]
    fn write_settings(&mut self, settings: &TTYSettings) -> ::Result<()> {
        let err = unsafe { ioctl::ioctl(self.fd, TCSETS2, &settings.termios) };

        if err != 0 {
            return Err(super::error::from_raw_os_error(err));
        }
        Ok(())
    }

Instead of tcsetattr, the TCSETS2 ioctl is used.


The issue here is that TCSANOW is hardcoded, which prevents using some of the useful alternative options (from man tcsetattr(3)):

arg meaning
TCSANOW the change occurs immediately.
TCSADRAIN the change occurs after all output written to fd has been transmitted. This option should be used when changing parameters that affect output.
TCSAFLUSH the change occurs after all output written to the object referred by fd has been transmitted, and all input that has been received but not read will be discarded before the change is made.

This parameter cannot be specified with the TCSETS2, instead, different IOCTLS must be used. The old equivalents are documented in tty_ioctl(4):

       TCSETS    const struct termios *argp
              Equivalent to tcsetattr(fd, TCSANOW, argp).
              Set the current serial port settings.

       TCSETSW   const struct termios *argp
              Equivalent to tcsetattr(fd, TCSADRAIN, argp).
              Allow the output buffer to drain, and set the current serial
              port settings.

       TCSETSF   const struct termios *argp
              Equivalent to tcsetattr(fd, TCSAFLUSH, argp).
              Allow the output buffer to drain, discard pending input, and
              set the current serial port settings.

Note the missing 2. An equivalent TCSETS2, TCSETSW2 and TCSETSF2 exist (some information about these can be found here); they work pretty much the same, except that they require a struct termios2 * instead of a struct termios *.


A feature that is currently missing in serial-rs is the ability to specifiy when the settings changes should take effect. This API needs to be designed and added to the trait first, I'll happily implemented it on Linux, but I'd like to avoid introducing core API changes that may be rejected.

Currently, a workaround exists: Calling flush() on the SerialPort and only then writing new settings. This will cause an additional gap in transmisison for as long as it takes to setup and transfer these settings into the kernel.


A usecase for this is implementing DMX512 using an RS485 serial port. The DMX512 has a lenient but weird structure, it basically is just a regular UART serial port at 250,000 baud (hence the other pull request for non-standard bitrates).

However, to begin transmission of a frame a DMX master must send a long BREAK by pulling the line low for a fairly long amount of time, then releasing it. This can easily be emulated on any UART by setting the bitrate to 57,000 baud, writing a 0x00 byte (7 bit), which will result in a 138 us BREAK (the minimum is 92 us). Directly after (but not during) the port should be switched to 250,000 baud and transmission can begin. In Pseudo-code:

  1. Set to low bitrate/7 bit.
  2. Send BREAK
  3. Set to high bitrate/8 bit
  4. Send data
  5. Drain
  6. Repeat for next packet.

Step 3. could be improved by preparing the new settings in advanced through TCSETSW2, hopefully causing minimal delay when switching bitrates.

mbr commented 7 years ago

An alternative that is less flexible would be adding support for the tcsendbreak syscall, which achieves the above without funky bitrate changing tricks (although the required pause MARK-AFTER-BREAK must then be done "by hand"). Adding support for this feature would almost be equivalent.

Edit: Unfortunately, the tc_sendbreak is fairly platform dependant:

The effect of a nonzero duration with tcsendbreak() varies. SunOS specifies a break of duration * N seconds, where N is at least 0.25, and not more than 0.5. Linux, AIX, DU, Tru64 send a break of duration milliseconds. FreeBSD and NetBSD and HP-UX and MacOS ignore the value of duration. Under Solaris and UnixWare, tcsendbreak() with nonzero duration behaves like tcdrain().

A millisecond is a long time, about 10x as much as needed for the usecase described above.