164 lines
5.8 KiB
Rust
164 lines
5.8 KiB
Rust
// 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
|
|
// a `Result<T, rn2903::Error>`.
|
|
#[macro_use]
|
|
extern crate quick_error;
|
|
use std::io;
|
|
|
|
quick_error! {
|
|
#[derive(Debug)]
|
|
pub enum Error {
|
|
/// The connection to the RN2903 was impossible for some reason. Perhaps an
|
|
/// invalid port was specified, or this program does not have permission to
|
|
/// access the specified port.
|
|
ConnectionFailed(err: serialport::Error) {
|
|
cause(err)
|
|
description(err.description())
|
|
from()
|
|
}
|
|
/// The device to which the serial link is connected does not appear to be
|
|
/// a RN2903, because it did not respond to `sys get ver` correctly.
|
|
WrongDevice(version: String) {
|
|
description("failed to verify connected module")
|
|
display("Could not verify version string. Expected a RN2903 firmware revision, got '{}'",
|
|
version)
|
|
}
|
|
/// The program has become disconnected from the RN2903 module due to an I/O
|
|
/// error. It is possible the device was physically disconnected, or that the
|
|
/// host operating system closed the serial port for some reason.
|
|
Disconnected(err: io::Error) {
|
|
cause(err)
|
|
description(err.description())
|
|
from()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Universal `Result` wrapper for the RN2903 interface.
|
|
type Result<T> = std::result::Result<T, Error>;
|
|
|
|
// 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
|
|
// communicate correctly with an RN2903.
|
|
|
|
use core::convert::AsRef;
|
|
use core::time::Duration;
|
|
use serialport::prelude::*;
|
|
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.
|
|
///
|
|
/// 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.
|
|
pub fn serial_config() -> SerialPortSettings {
|
|
SerialPortSettings {
|
|
baud_rate: 57600,
|
|
data_bits: DataBits::Eight,
|
|
flow_control: FlowControl::None,
|
|
parity: Parity::None,
|
|
stop_bits: StopBits::One,
|
|
timeout: Duration::new(1, 0),
|
|
}
|
|
}
|
|
|
|
// Once connected to a serial port, the library needs to verify that it is actually
|
|
// connected to a RN2903 and not some other serial device. To this end, the `Rn2903`
|
|
// 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 {
|
|
(&*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.
|
|
pub struct Rn2903 {
|
|
port: Box<dyn SerialPort>,
|
|
}
|
|
|
|
impl Rn2903 {
|
|
/// Open a new connection to a module at the given path or port name, with the
|
|
/// default settings.
|
|
pub fn new_at<S: AsRef<OsStr>>(port_name: S) -> Result<Self> {
|
|
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.
|
|
pub fn new(port: Box<dyn SerialPort>) -> Result<Self> {
|
|
let mut new = Self { port };
|
|
let version = new.system_version()?;
|
|
if &version[0..6] != "RN2903" {
|
|
Err(Error::WrongDevice(version))
|
|
} else {
|
|
Ok(new)
|
|
}
|
|
}
|
|
|
|
/// Query 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<String> {
|
|
let bytes = self.transact_command(b"sys get ver\x0D\x0A")?;
|
|
Ok(bytes_to_string(&bytes))
|
|
}
|
|
|
|
/// Write the specified command to the module and get a single line of 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<Vec<u8>> {
|
|
use std::io::IoSlice;
|
|
self.port.write_vectored(&[IoSlice::new(command), IoSlice::new(b"\x0D\x0A")])?;
|
|
self.port.flush()?;
|
|
self.read_line()
|
|
}
|
|
|
|
/// Read bytes from the device until a CRLF is encountered, then returns the bytes
|
|
/// read, not including the CRLF.
|
|
// This operation waits 12ms between each 32-byte read because the LoStick has
|
|
// the hiccups.
|
|
fn read_line(&mut self) -> Result<Vec<u8>> {
|
|
let mut vec = Vec::with_capacity(32);
|
|
loop {
|
|
let mut buf = [0; 32];
|
|
self.port.read(&mut buf)?;
|
|
vec.extend_from_slice(&buf);
|
|
|
|
// Check if crlf was added to the buffer.
|
|
let mut found_lf = false;
|
|
let mut found_crlf = false;
|
|
for byte in vec.iter().rev() {
|
|
if found_lf {
|
|
if *byte == b'\x0D' {
|
|
found_crlf = true;
|
|
break;
|
|
}
|
|
} else {
|
|
found_lf = *byte == b'\x0A';
|
|
}
|
|
}
|
|
if found_crlf {
|
|
break;
|
|
} else {
|
|
thread::sleep(Duration::from_millis(12));
|
|
}
|
|
}
|
|
|
|
// Remove zeroes and crlf
|
|
while (b"\x00\x0D\x0A").contains(&vec[vec.len() - 1]) {
|
|
vec.pop();
|
|
}
|
|
|
|
Ok(vec)
|
|
}
|
|
}
|