From 8e52f10ffcacd91f147d095b1a53fa104a4da432 Mon Sep 17 00:00:00 2001 From: Leonora Tindall Date: Sun, 29 Dec 2019 11:58:05 -0800 Subject: [PATCH] Better docs and rename transact() --- CHANGELOG.md | 1 + src/lib.rs | 175 ++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 152 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87d44bb..0febaa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - GNU GPL v3 license - Cargo metadata +- Basic functionality ### Changed diff --git a/src/lib.rs b/src/lib.rs index 0f074b7..7633d54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,14 @@ +//! ## A Rusty interface for the RN2903 serial protocol +//! +//! The RN2903 is a LoRa and FSK transciever for the 915MHz ISM band, commonly used in USB +//! devices like the LoStik. +//! +//! This crate provides a safe, idiomatic interface using cross-platform native serial +//! functionality via `serialport`. This supports, for instance, a LoStik connected to a USB +//! TTY or virtual COM port, or a RN2903 connected via a TTL serial interface. +//! +//! See the [`Rn2903` struct](struct.Rn2903.html) for the bulk of the crate's functionality. + // One of the critical aspects of this library is error handling. Because it is intended // to communicate with an external device, any operation could discover a disconnection // from the RN2903 serial link, so everything which does such communication will return @@ -7,6 +18,7 @@ extern crate quick_error; use std::io; quick_error! { + /// The primary error type used for fallible operations on the RN2903. #[derive(Debug)] pub enum Error { /// The connection to the RN2903 was impossible for some reason. Perhaps an @@ -36,7 +48,7 @@ quick_error! { } /// Universal `Result` wrapper for the RN2903 interface. -type Result = std::result::Result; +pub type Result = std::result::Result; // It's first necessary to actually connect to the module. To this end, the library // exports all the configuration information needed to configure a serial port to @@ -49,12 +61,27 @@ use std::ffi::OsStr; use std::io::prelude::*; use std::thread; -/// Returns a `serialport::SerialPortSettings` corresponding to the default settings of -/// an RNB2903. Use this to configure your serial port. +/// Returns the `SerialPortSettings` corresponding to the default settings of +/// an RNB2903. /// /// Information obtained from Microchip document 40001811 revision B. Timeout is by /// default set to a very long time; this is sometimes modified on the `SerialPort` itself /// during certain operations. +/// +/// # Examples +/// +/// Opening a serial port with slightly modified settings. In this case, the baud rate +/// has been reduced. +/// +/// ```no_run +/// let settings = serialport::SerialPortSettings { +/// baud_rate: 9600, +/// ..rn2903::serial_config() +/// }; +/// +/// serialport::open_with_settings("/dev/ttyUSB0", &settings) +/// .expect("Could not open serial port. Error"); +/// ``` pub fn serial_config() -> SerialPortSettings { SerialPortSettings { baud_rate: 57600, @@ -62,7 +89,7 @@ pub fn serial_config() -> SerialPortSettings { flow_control: FlowControl::None, parity: Parity::None, stop_bits: StopBits::One, - timeout: Duration::new(1, 0), + timeout: Duration::new(65535, 0), } } @@ -71,30 +98,70 @@ pub fn serial_config() -> SerialPortSettings { // wrapper struct's `::new()` function checks the output of the `sys get ver` command, // which is well-specified. -// In order to turn the raw bytes into a String for display, this helper function comes -// in handy. -fn bytes_to_string(bytes: &[u8]) -> String { +/// Turn the raw bytes into a String for display. +pub fn bytes_to_string(bytes: &[u8]) -> String { (&*String::from_utf8_lossy(bytes)).into() } /// A handle to a serial link connected to a RN2903 module. /// -/// This library guarantees safety regardless of the state of the RN2903. +/// This library guarantees safety regardless of the state of the RN2903. Refer to the +/// documentation for sections and individual associated functions for specifics. +/// +/// # Examples +/// +/// Basic functionality can be obtained just by using `::new_at()` and `::transact()`. +/// For instance, blinking the LoStik's LED: +/// +/// ```no_run +/// # use rn2903::Rn2903; +/// # use std::time::Duration; +/// # use std::thread; +/// let mut txvr = Rn2903::new_at("/dev/ttyUSB0") +/// .expect("Could not open device. Error"); +/// loop { +/// txvr.transact(b"radio set pindig GPIO10 0").unwrap(); +/// thread::sleep(Duration::from_millis(1000)); +/// txvr.transact(b"radio set pindig GPIO10 1").unwrap(); +/// thread::sleep(Duration::from_millis(1000)); +/// } +/// ``` pub struct Rn2903 { port: Box, } +/// # Meta (type) Functions +/// +/// These functions deal with the type `Rn2903`, providing ways to create and manipulate +/// the structure itself. Aside from performing validation of the device on the other side +/// of the serial link, these functions do not communicate with the module. +/// +/// ## Creating an `Rn2903` +/// There are several ways to create a `Rn2903` wrapper for an RN2903 serial connection. +/// `::new_at()` is the recommended method, but `::new()` can be useful if the platform +/// does not support named serial ports, or some extra configuration is needed. impl Rn2903 { - /// Open a new connection to a module at the given path or port name, with the - /// default settings. + /// Opens a new connection to a module at the given path or port name, with the + /// default (and usually correct) settings from + /// [`serial_config`](fn.serial_config.html). + /// + /// # Example + /// + /// Connecting to a module accessible over the USB0 TTY. + /// ```no_run + /// # use rn2903::Rn2903; + /// let txvr = Rn2903::new_at("/dev/ttyUSB0") + /// .expect("Could not open device. Error"); + /// ``` pub fn new_at>(port_name: S) -> Result { let sp = serialport::open_with_settings(&port_name, &serial_config())?; Self::new(sp) } - /// Open a new connection to a module over the given serial connection. + /// Open a new connection to a module over the connection described by the given + /// `SerialPort` trait object. pub fn new(port: Box) -> Result { - let mut new = Self { port }; + let mut new = Self::new_unchecked(port); let version = new.system_version()?; if &version[0..6] != "RN2903" { Err(Error::WrongDevice(version)) @@ -103,30 +170,78 @@ impl Rn2903 { } } - /// Query the module for its firmware version information. + /// Open a new connection to a module over the connection described by the given + /// `SerialPort` trait object without performing a `sys get ver` check. /// - /// Returns a `String` like `RN2903 1.0.3 Aug 8 2017 15:11:09` - pub fn system_version(&mut self) -> Result { - let bytes = self.transact_command(b"sys get ver\x0D\x0A")?; - Ok(bytes_to_string(&bytes)) + /// The results of operations on a `Rn2903` struct that does _not_ represent an + /// actual connection to an RN2903 module are completely unpredictable, and may + /// result in lots of badness (though not memory unsafety). + pub fn new_unchecked(port: Box) -> Self { + Self { port } } + + /// Acquires temporary direct access to the captured `SerialPort` trait object. + /// + /// Use this access to, for example, reconfigure the connection on the fly, + /// or set flags that will be used by devices this crate is unaware of. + /// + /// # Example + /// + /// Raising and then lowering the RTS signal, for example to signal a bus observer + /// to switch on. + /// ```no_run + /// # use rn2903::Rn2903; + /// # use std::thread; + /// # use std::time::Duration; + /// # let mut txvr = Rn2903::new_at("/dev/ttyUSB0") + /// # .expect("Could not open device. Error"); + /// txvr.port().write_request_to_send(true) + /// .expect("Could not set RTS. Error"); + /// thread::sleep(Duration::from_millis(25)); + /// txvr.port().write_request_to_send(false) + /// .expect("Could not set RTS. Error"); + /// ``` + pub fn port(&mut self) -> &mut dyn SerialPort { + &mut *self.port + } +} - /// Write the specified command to the module and get a single line of response. +/// # Low-level Communications +impl Rn2903 { + /// Writes the specified command to the module and returns a single line in response. /// /// This function adds the CRLF to the given command and returns the response without /// the CRLF. - pub fn transact_command(&mut self, command: &[u8]) -> Result> { - use std::io::IoSlice; - self.port.write_vectored(&[IoSlice::new(command), IoSlice::new(b"\x0D\x0A")])?; - self.port.flush()?; + /// + /// This is the preferred low-level communication method, since the RN2903 is supposed + /// to respond with a single line to every command. + pub fn transact(&mut self, command: &[u8]) -> Result> { + self.send_line(command)?; self.read_line() } - /// Read bytes from the device until a CRLF is encountered, then returns the bytes + /// Writes the specified command to the module, adding a CRLF and flushing the buffer. + /// + /// Using [`::transact()`](#method.transact) is preferred. + pub fn send_line(&mut self, line: &[u8]) -> Result<()> { + use std::io::IoSlice; + let bytes: Vec = line.iter().chain(b"\x0D\x0A".iter()).cloned().collect(); + let mut cursor = 0; + while cursor < bytes.len() { + cursor += self.port.write(&bytes[cursor..])?; + } + self.port.flush()?; + thread::sleep(Duration::from_millis(500)); + Ok(()) + } + + /// Reads bytes from the device until a CRLF is encountered, then returns the bytes /// read, not including the CRLF. + /// + /// Using [`::transact()`](#method.transact) is preferred. // This operation waits 12ms between each 32-byte read because the LoStick has // the hiccups. - fn read_line(&mut self) -> Result> { + pub fn read_line(&mut self) -> Result> { let mut vec = Vec::with_capacity(32); loop { let mut buf = [0; 32]; @@ -160,4 +275,16 @@ impl Rn2903 { Ok(vec) } + +} + +/// # System API Functions +impl Rn2903 { + /// Queries the module for its firmware version information. + /// + /// Returns a `String` like `RN2903 1.0.3 Aug 8 2017 15:11:09` + pub fn system_version(&mut self) -> Result { + let bytes = self.transact(b"sys get ver")?; + Ok(bytes_to_string(&bytes)) + } }