From 1b4ccd0dde167285e193e7d48eed44f0705e356d Mon Sep 17 00:00:00 2001 From: Robert Sammelson Date: Mon, 15 May 2023 00:56:36 -0400 Subject: [PATCH] Documentation and organization improvements --- Cargo.toml | 2 +- src/accessors.rs | 34 ++++++++++++--- src/device/elm327.rs | 99 +++++++++++++++++++++++++++++--------------- src/device/mod.rs | 39 ++++++++++++++--- src/interface.rs | 6 +-- 5 files changed, 131 insertions(+), 49 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d275fc2..2224cae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "obd2" description = "Utility for reading data from a vehicle over OBD-II" license = "MIT OR Apache-2.0" repository = "https://github.com/rsammelson/obd2" -version = "0.1.0" +version = "0.2.0-pre1" edition = "2021" [dependencies] diff --git a/src/accessors.rs b/src/accessors.rs index b5da403..d53c56e 100644 --- a/src/accessors.rs +++ b/src/accessors.rs @@ -2,14 +2,30 @@ use core::fmt; pub type Result = std::result::Result; +/// A higher-level API for using an OBD-II device pub trait Obd2Device { - /// Send an OBD command with mode and PID, and get a list of responses (one for each ECU that - /// responds) + /// Send an OBD-II command with mode and PID and get responses + /// + /// The responses are a list with one element for each ECU that responds. The data is decoded + /// into the ODB-II bytes from the vehicle and the first two bytes of the + /// response---representing the mode and PID the vehicle received---are validated and removed. fn obd_command(&mut self, mode: u8, pid: u8) -> Result>>; - /// Like [obd_command](Self::obd_command), but for commands that do not require a PID + /// Send an OBD-II command with only mode and get responses + /// + /// The responses are a list with one element for each ECU that responds. The data is decoded + /// into the ODB-II bytes from the vehicle and the first byte of the response---representing + /// the mode the vehicle recieved---is validated and removed. fn obd_mode_command(&mut self, mode: u8) -> Result>>; + /// Send command and get list of OBD-II responses as an array + /// + /// Like [obd_command](Self::obd_command), but each ECU's response (after removing the first + /// two bytes) is converted to an array of the specified length. If any response is the wrong + /// length, and error is returned. + /// + /// This function can be used when the response length is known, so that it is easier to index + /// into the response without causing a panic and without dealing with Options. fn obd_command_len( &mut self, mode: u8, @@ -25,6 +41,12 @@ pub trait Obd2Device { .collect() } + /// Send command and get array of OBD-II responses with each as an array + /// + /// Like [obd_command_len](Self::obd_command_len), but also convert the list of ECU responses + /// to an array. This can be used when the number of ECUs that should respond is known in + /// advance. Most commonly, this will be when the count of ECUs is one, for values where only a + /// single ECU should respond like the speed of the vehicle. fn obd_command_cnt_len( &mut self, mode: u8, @@ -37,8 +59,10 @@ pub trait Obd2Device { .map_err(|_| Error::IncorrectResponseLength("count", RESPONSE_COUNT, count)) } - /// Retreive the VIN (vehicle identification number), this should match the one printed on the - /// vehicle + /// Retreive the VIN (vehicle identification number) + /// + /// This should match the number printed on the vehicle, and is a good command for checking + /// that the OBD-II interface is working correctly. fn get_vin(&mut self) -> Result { let mut result = self.obd_command(0x09, 0x02)?.pop().unwrap(); result.remove(0); // do not know what this byte is diff --git a/src/device/elm327.rs b/src/device/elm327.rs index ad55635..5be3d9f 100644 --- a/src/device/elm327.rs +++ b/src/device/elm327.rs @@ -7,6 +7,12 @@ use std::{ use super::{Error, Obd2BaseDevice, Obd2Reader, Result}; +/// An ELM327 OBD-II adapter +/// +/// It communicates with the computer over UART using an FTDI FT232R USB-to-UART converter. +/// Commands to the device itself are indicated by sending "AT" followed by the command, while +/// plain strings of hex data indicate OBD-II requests to be sent to the vehicle. The responses of +/// the vehicle are echoed back as hex characters. Capitalization and spaces are always ignored. pub struct Elm327 { device: ftdi::Device, buffer: VecDeque, @@ -28,23 +34,16 @@ impl Obd2BaseDevice for Elm327 { Ok(()) } - fn flush(&mut self) -> Result<()> { - thread::sleep(time::Duration::from_millis(500)); - self.read_into_queue()?; - self.buffer.clear(); - thread::sleep(time::Duration::from_millis(500)); - Ok(()) - } - - fn send_serial_cmd(&mut self, data: &str) -> Result<()> { - self.device.write_all(data.as_bytes())?; + fn send_cmd(&mut self, data: &[u8]) -> Result<()> { + self.device.write_all(data)?; self.device.write_all(b"\r\n")?; + // thread::sleep(time::Duration::from_millis(200)); let line = self.get_line()?; - if line.as_ref().is_some_and(|v| v == data.as_bytes()) { + if line.as_ref().is_some_and(|v| v == data) { Ok(()) } else { Err(Error::Communication(format!( - "send_serial_cmd: got {:?} instead of echoed command ({})", + "send_serial_cmd: got {:?} instead of echoed command ({:?})", line, data ))) } @@ -56,7 +55,13 @@ impl Obd2Reader for Elm327 { self.get_until(b'\n', false) } - fn get_until_prompt(&mut self) -> Result>> { + /// Read data until the ELM327's prompt character is printed + /// + /// This will recieve the entire OBD-II response. The prompt signifies that the ELM327 is ready + /// for another command. If this is not called after each OBD-II command is sent, the prompt + /// character will come out of the recieve queue later and because it is not valid hex this + /// could cause problems. If a timeout occurs, `Ok(None)` will be returned. + fn get_response(&mut self) -> Result>> { self.get_until(b'>', true) } } @@ -85,6 +90,20 @@ impl Elm327 { Ok(device) } + /// Flush the device's buffer + pub fn flush(&mut self) -> Result<()> { + thread::sleep(time::Duration::from_millis(500)); + self.read_into_queue()?; + self.buffer.clear(); + thread::sleep(time::Duration::from_millis(500)); + Ok(()) + } + + fn flush_buffers(&mut self) -> Result<()> { + self.device.usb_purge_buffers()?; + Ok(()) + } + fn connect(&mut self, check_baud_rate: bool) -> Result<()> { self.flush_buffers()?; thread::sleep(time::Duration::from_millis(500)); @@ -108,7 +127,7 @@ impl Elm327 { self.send_serial_cmd("ATZ")?; debug!( "reset_ic: got response {:?}", - self.get_until_prompt()? + self.get_response()? .as_ref() .map(|l| std::str::from_utf8(l.as_slice())) ); @@ -117,8 +136,14 @@ impl Elm327 { fn reset_protocol(&mut self) -> Result<()> { info!("Performing protocol reset"); - debug!("reset_protocol: got response {:?}", self.cmd("ATSP0")?); - debug!("reset_protocol: got OBD response {:?}", self.cmd("0100")?); + debug!( + "reset_protocol: got response {:?}", + self.serial_cmd("ATSP0")? + ); + debug!( + "reset_protocol: got OBD response {:?}", + self.cmd(&[0x01, 0x00])? + ); self.flush_buffers()?; Ok(()) } @@ -148,7 +173,7 @@ impl Elm327 { // our TX is bad self.device.set_baud_rate(self.baud_rate)?; debug!("Baud rate bad - device did not receive response"); - self.get_until_prompt()?; + self.get_response()?; } } else { // reset baud rate and keep looking @@ -160,11 +185,11 @@ impl Elm327 { .as_ref() .map(|r| String::from_utf8_lossy(r)) ); - self.get_until_prompt()?; + self.get_response()?; } } else { debug!("Baud rate bad - did not ok initially"); - self.get_until_prompt()?; + self.get_response()?; } thread::sleep(time::Duration::from_millis(200)); @@ -222,25 +247,16 @@ impl Elm327 { } fn get_byte(&mut self) -> Result> { - self.read_into_queue()?; - loop { - let b = self.buffer.pop_front(); - if b != Some(b'\0') { - return Ok(b); + match self.buffer.pop_front() { + Some(b'\0') => Ok(None), + Some(b) => Ok(Some(b)), + None => { + self.read_into_queue()?; + Ok(None) } } } - fn flush_buffers(&mut self) -> Result<()> { - self.device.usb_purge_buffers()?; - Ok(()) - } - - fn send_serial_str(&mut self, data: &str) -> Result<()> { - self.device.write_all(data.as_bytes())?; - Ok(()) - } - fn read_into_queue(&mut self) -> Result<()> { let mut buf = [0u8; 16]; loop { @@ -258,4 +274,19 @@ impl Elm327 { } Ok(()) } + + fn send_serial_cmd(&mut self, data: &str) -> Result<()> { + self.send_cmd(data.as_bytes()) + } + + fn serial_cmd(&mut self, cmd: &str) -> Result> { + self.send_serial_cmd(cmd)?; + self.get_response() + .map(|o| o.and_then(|resp| String::from_utf8(resp).ok())) + } + + fn send_serial_str(&mut self, data: &str) -> Result<()> { + self.device.write_all(data.as_bytes())?; + Ok(()) + } } diff --git a/src/device/mod.rs b/src/device/mod.rs index f54d36d..f07ca56 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -3,22 +3,49 @@ pub use elm327::Elm327; type Result = std::result::Result; +/// A lower-level API for using an OBD-II device pub trait Obd2BaseDevice: Obd2Reader { + /// Reset the device and the OBD-II interface + /// + /// First the device is reset, if it is stateful. Then the OBD-II interface is reinitialized, + /// which resets the selected protocol on the device and rechecks the vehicle manufacturer if + /// needed. fn reset(&mut self) -> Result<()>; - fn flush(&mut self) -> Result<()>; - fn send_serial_cmd(&mut self, data: &str) -> Result<()>; - fn cmd(&mut self, cmd: &str) -> Result> { - self.send_serial_cmd(cmd)?; - self.get_until_prompt() + + /// Send an OBD-II command + fn send_cmd(&mut self, data: &[u8]) -> Result<()>; + + /// Send an OBD-II command and get the reply + /// + /// The reply is decoded into a String of mostly hex data. Depending on the format of the + /// response, some other characters may be included like line numbers for multiline responses + /// (of the format "0: AB CD ..."). + fn cmd(&mut self, cmd: &[u8]) -> Result> { + self.send_cmd(cmd)?; + self.get_response() .map(|o| o.and_then(|resp| String::from_utf8(resp).ok())) } } +/// An API for reading OBD-II response data pub trait Obd2Reader { + /// Try to get a single line of data from the device + /// + /// The trailing newline is not included. This function will never return an empty line, it + /// will retry until a line with data is found. If no data is available after a reasonable + /// timeout, `Ok(None)` will be returned. fn get_line(&mut self) -> Result>>; - fn get_until_prompt(&mut self) -> Result>>; + + /// Get an entire OBD-II response + /// + /// Empty vectors are allowed to be returned. This function should always be called after a + /// command is sent, possibly after calling [get_line](Self::get_line) to read the first lines, + /// so that any metadata sent by the device after the response from the vehicle can be dealt + /// with. + fn get_response(&mut self) -> Result>>; } +/// Error type for low-level ODB-II communication issues #[derive(thiserror::Error, Debug)] pub enum Error { #[error("FTDI error: `{0:?}`")] diff --git a/src/interface.rs b/src/interface.rs index 6b1c2ed..7e88ed9 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -9,7 +9,7 @@ pub struct Obd2 { impl Obd2Device for Obd2 { fn obd_command(&mut self, mode: u8, pid: u8) -> Result>> { - let result = self.command(&format!("{:02x}{:02x}", mode, pid))?; + let result = self.command(&[mode, pid])?; for response in result.iter() { if response.first() != Some(&(0x40 | mode)) { @@ -24,7 +24,7 @@ impl Obd2Device for Obd2 { } fn obd_mode_command(&mut self, mode: u8) -> Result>> { - let result = self.command(&format!("{:02x}", mode))?; + let result = self.command(std::slice::from_ref(&mode))?; for response in result.iter() { if response.first() != Some(&(0x40 | mode)) { @@ -37,7 +37,7 @@ impl Obd2Device for Obd2 { } impl Obd2 { - fn command(&mut self, command: &str) -> Result>> { + fn command(&mut self, command: &[u8]) -> Result>> { let response = self .device .cmd(command)?