From 642d2255ef87d1aa06ada352106ebb139f57ad96 Mon Sep 17 00:00:00 2001 From: Nicholas Orlowsky Date: Tue, 18 Mar 2025 22:03:13 -0400 Subject: [PATCH 1/4] - allow communication to elm327 device via a serialport - allow creation of elm327 device without requiring unwrap() - added some extra documentation - add fuel_level obd-ii command - expose reset() functionality of device --- Cargo.toml | 1 + README.md | 11 ++++- examples/basic/main.rs | 11 +++-- src/commands/mod.rs | 3 ++ src/device/elm327.rs | 84 ++++++++++++++-------------------- src/device/mod.rs | 13 ++++++ src/device/serial_comm.rs | 94 +++++++++++++++++++++++++++++++++++++++ src/error.rs | 10 +++-- src/interface.rs | 13 +++++- src/lib.rs | 12 +++-- 10 files changed, 189 insertions(+), 63 deletions(-) create mode 100644 src/device/serial_comm.rs diff --git a/Cargo.toml b/Cargo.toml index ec43f5f..b05ba2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,4 @@ env_logger = "0.10" ftdi = "0.1.3" log = "0.4.8" thiserror = "1.0.15" +serialport="=4.6.1" diff --git a/README.md b/README.md index 9992303..412613f 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,20 @@ vehicle. ## Usage ```rs -use obd2::{commands::Obd2DataRetrieval, device::Elm327, Obd2}; +use obd2::{commands::Obd2DataRetrieval, device::{Elm327, FTDIDevice}, Obd2}; fn main() -> Result<(), obd2::Error> { - let mut device = Obd2::::default(); + let mut device = Obd2::>::new(Elm327::new(FTDIDevice::new()?)?)?; println!("VIN: {}", device.get_vin()?); Ok(()) } ``` +alternatively, you could use a serial port provided by your operating system such as +/dev/ttyUSB0 on unix-like systems + +```rs +let mut device = Obd2::>::new(Elm327::new(SerialPort::new("/dev/ttyUSB0")?)?)?; +``` + See the docs for more: https://docs.rs/obd2/ diff --git a/examples/basic/main.rs b/examples/basic/main.rs index f396bb2..7ecb3ad 100644 --- a/examples/basic/main.rs +++ b/examples/basic/main.rs @@ -2,15 +2,16 @@ use obd2::commands::Obd2DataRetrieval; use std::time; -fn main() { +fn main() -> Result<(), obd2::Error> { env_logger::init(); - let mut device: obd2::Obd2 = obd2::Obd2::default(); + let mut device: obd2::Obd2> = + obd2::Obd2::new(obd2::device::Elm327::new(obd2::device::FTDIDevice::new()?)?)?; println!("VIN: {:?}", device.get_vin()); - for s in device.get_service_1_pid_support_1().unwrap().iter() { + for s in device.get_service_1_pid_support_1()?.iter() { println!("PID support ($01-$20): {:08X}", s); } - for s in device.get_service_1_pid_support_2().unwrap().iter() { + for s in device.get_service_1_pid_support_2()?.iter() { println!("PID support ($21-$40): {:08X}", s); } @@ -47,4 +48,6 @@ fn main() { device.get_throttle_position() ); } + + Ok(()) } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 1265eb9..6d60b1a 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -220,4 +220,7 @@ func! { /// Get service 1 PID support for $21 to $40 fn get_service_1_pid_support_2(0x01, 0x20) -> u32; + + // Get the fuel level (out of 255) + fn get_fuel_level(0x01, 0x2F) -> u8; } diff --git a/src/device/elm327.rs b/src/device/elm327.rs index 83543a6..735aab4 100644 --- a/src/device/elm327.rs +++ b/src/device/elm327.rs @@ -1,34 +1,24 @@ use log::{debug, info, trace}; -use std::{ - collections::VecDeque, - io::{Read, Write}, - thread, time, -}; +use std::{collections::VecDeque, thread, time}; -use super::{Error, Obd2BaseDevice, Obd2Reader, Result}; +use super::{serial_comm::SerialComm, Error, Obd2BaseDevice, Obd2Reader, Result}; /// An ELM327 OBD-II adapter /// -/// It communicates with the computer over UART using an FTDI FT232R USB-to-UART converter. +/// It communicates with the computer or 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. /// /// [Datasheet for v1.4b](https://github.com/rsammelson/obd2/blob/master/docs/ELM327DSH.pdf), and /// the [source](https://www.elmelectronics.com/products/dsheets/). -pub struct Elm327 { - device: ftdi::Device, +pub struct Elm327 { + device: T, buffer: VecDeque, baud_rate: u32, } -impl Default for Elm327 { - fn default() -> Self { - Elm327::new().unwrap() - } -} - -impl Obd2BaseDevice for Elm327 { +impl Obd2BaseDevice for Elm327 { fn reset(&mut self) -> Result<()> { self.flush_buffers()?; self.reset_ic()?; @@ -48,7 +38,7 @@ impl Obd2BaseDevice for Elm327 { } } -impl Obd2Reader for Elm327 { +impl Obd2Reader for Elm327 { fn get_line(&mut self) -> Result>> { self.get_until(b'\n', false) } @@ -64,22 +54,14 @@ impl Obd2Reader for Elm327 { } } -impl Elm327 { - fn new() -> Result { - let mut ftdi_device = ftdi::find_by_vid_pid(0x0403, 0x6001) - .interface(ftdi::Interface::A) - .open()?; - - ftdi_device.set_baud_rate(38400)?; - ftdi_device.configure(ftdi::Bits::Eight, ftdi::StopBits::One, ftdi::Parity::None)?; - // device.set_latency_timer(2).unwrap(); - - ftdi_device.usb_reset()?; - +impl Elm327 { + /// Creates a new Elm327 adapter with the given + /// unserlying Serial Communication device + pub fn new(serial_device: T) -> Result { let mut device = Elm327 { - device: ftdi_device, + device: serial_device, buffer: VecDeque::new(), - baud_rate: 38400, + baud_rate: 38_400, }; device.connect(false)?; @@ -98,7 +80,7 @@ impl Elm327 { } fn flush_buffers(&mut self) -> Result<()> { - self.device.usb_purge_buffers()?; + self.device.purge_buffers()?; Ok(()) } @@ -123,25 +105,22 @@ impl Elm327 { fn reset_ic(&mut self) -> Result<()> { info!("Performing IC reset"); self.send_serial_str("ATZ")?; + let response = self.get_response()?; debug!( "reset_ic: got response {:?}", - self.get_response()? - .as_ref() - .map(|l| std::str::from_utf8(l.as_slice())) + response.as_ref().map(|l| std::str::from_utf8(l.as_slice())) ); Ok(()) } fn reset_protocol(&mut self) -> Result<()> { info!("Performing protocol reset"); - debug!( - "reset_protocol: got response {:?}", - self.serial_cmd("ATSP0")? - ); - debug!( - "reset_protocol: got OBD response {:?}", - self.cmd(&[0x01, 0x00])? - ); + let elm_response = self.serial_cmd("ATSP0")?; + debug!("reset_protocol: got response {:?}", elm_response); + + let obd_response = self.cmd(&[0x01, 0x00])?; + debug!("reset_protocol: got OBD response {:?}", obd_response); + self.flush_buffers()?; Ok(()) } @@ -258,13 +237,18 @@ impl Elm327 { fn read_into_queue(&mut self) -> Result<()> { let mut buf = [0u8; 16]; loop { - let len = self.device.read(&mut buf)?; - if len > 0 { - self.buffer.extend(&buf[0..len]); - trace!( - "read_into_queue: values {:?}", - std::str::from_utf8(&buf[0..len]) - ); + let len_res = self.device.read(&mut buf); + if let Ok(len) = len_res { + if len > 0 { + self.buffer.extend(&buf[0..len]); + trace!( + "read_into_queue: values {:?}", + std::str::from_utf8(&buf[0..len]) + ); + } else { + trace!("read_into_queue: no values left to read"); + break; + } } else { trace!("read_into_queue: no values left to read"); break; diff --git a/src/device/mod.rs b/src/device/mod.rs index 1f0f561..31f0293 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -3,6 +3,9 @@ mod elm327; pub use elm327::Elm327; +mod serial_comm; +pub use serial_comm::{FTDIDevice, SerialPort}; + type Result = std::result::Result; /// A lower-level API for using an OBD-II device @@ -54,6 +57,10 @@ pub enum Error { #[error("FTDI error: `{0:?}`")] Ftdi(ftdi::Error), + /// An error with the underlying [serialport device](serialport::SerialPort) + #[error("Serialport error: `{0:?}`")] + Serialport(serialport::Error), + /// An I/O error in a low-level [std::io] stream operation #[error("IO error: `{0:?}`")] IO(std::io::Error), @@ -69,6 +76,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: serialport::Error) -> Self { + Error::Serialport(e) + } +} + impl From for Error { fn from(e: std::io::Error) -> Self { Error::IO(e) diff --git a/src/device/serial_comm.rs b/src/device/serial_comm.rs new file mode 100644 index 0000000..0ab6467 --- /dev/null +++ b/src/device/serial_comm.rs @@ -0,0 +1,94 @@ +use super::Result; +use std::io::{Read, Write}; +use std::time::Duration; + +const DEFAULT_BAUD_RATE: u32 = 38_400; + +/// An API to communicate with a serial device +pub trait SerialComm { + fn write_all(&mut self, data: &[u8]) -> Result<()>; + fn read(&mut self, data: &mut [u8]) -> Result; + fn set_baud_rate(&mut self, baud_rate: u32) -> Result<()>; + fn purge_buffers(&mut self) -> Result<()>; +} + +/// Communicate with a serial device using the +/// serialport library +/// +/// /dev/tty* or similar on unix-like systems +/// COM devices on Windows systems +pub struct SerialPort { + device: Box, +} + +impl SerialPort { + /// Creates a new instance of a SerialPort + pub fn new(path: &str) -> Result { + let device = serialport::new(path, DEFAULT_BAUD_RATE) + .timeout(Duration::from_millis(10)) + .parity(serialport::Parity::None) + .data_bits(serialport::DataBits::Eight) + .stop_bits(serialport::StopBits::One) + .path(path) + .open()?; + + Ok(Self { device }) + } +} + +impl SerialComm for SerialPort { + fn write_all(&mut self, data: &[u8]) -> Result<()> { + Ok(self.device.write_all(data)?) + } + + fn read(&mut self, data: &mut [u8]) -> Result { + Ok(self.device.read(data)?) + } + + fn set_baud_rate(&mut self, baud_rate: u32) -> Result<()> { + Ok(self.device.set_baud_rate(baud_rate)?) + } + + fn purge_buffers(&mut self) -> Result<()> { + Ok(self.device.clear(serialport::ClearBuffer::All)?) + } +} + +/// Communicate with a USB to Serial FTDI device +/// with the FTDI library +pub struct FTDIDevice { + device: ftdi::Device, +} + +impl FTDIDevice { + /// Creates a new instance of an FTDIDevice + pub fn new() -> Result { + let mut device = ftdi::find_by_vid_pid(0x0404, 0x6001) + .interface(ftdi::Interface::A) + .open()?; + + device.set_baud_rate(DEFAULT_BAUD_RATE)?; + device.configure(ftdi::Bits::Eight, ftdi::StopBits::One, ftdi::Parity::None)?; + device.usb_reset()?; + + Ok(Self { device }) + } +} + +impl SerialComm for FTDIDevice { + fn write_all(&mut self, data: &[u8]) -> Result<()> { + Ok(self.device.write_all(data)?) + } + + fn read(&mut self, data: &mut [u8]) -> Result { + Ok(self.device.read(data)?) + } + + fn set_baud_rate(&mut self, baud_rate: u32) -> Result<()> { + Ok(self.device.set_baud_rate(baud_rate)?) + } + + fn purge_buffers(&mut self) -> Result<()> { + Ok(self.device.usb_purge_buffers()?) + } +} diff --git a/src/error.rs b/src/error.rs index 6feac68..f6afd75 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,6 @@ +//! Error types for OBD-II related errors + +/// Result type defaulted with this library's error type pub type Result = std::result::Result; /// An error with OBD-II communication @@ -16,8 +19,9 @@ pub enum Error { Other(String), } +/// An error with the ELM327 device #[derive(Debug)] -pub struct DeviceError(crate::device::Error); +pub struct DeviceError(pub crate::device::Error); impl From for Error { fn from(e: super::device::Error) -> Self { @@ -27,12 +31,12 @@ impl From for Error { impl From for Error { fn from(e: std::num::ParseIntError) -> Self { - Error::Other(format!("invalid data recieved: {:?}", e)) + Error::Other(format!("invalid data received: {:?}", e)) } } impl From for Error { fn from(e: std::string::FromUtf8Error) -> Self { - Error::Other(format!("invalid string recieved: {:?}", e)) + Error::Other(format!("invalid string received: {:?}", e)) } } diff --git a/src/interface.rs b/src/interface.rs index 5d49c0b..724ae15 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -6,7 +6,6 @@ use super::{device::Obd2BaseDevice, Error, Obd2Device, Result}; /// /// Wraps an implementer of [Obd2BaseDevice] to allow for higher-level usage of the OBD-II /// interface. -#[derive(Default)] pub struct Obd2 { device: T, } @@ -41,6 +40,18 @@ impl Obd2Device for Obd2 { } impl Obd2 { + /// Creates a new instance of an Obd device + pub fn new(dev: T) -> Result { + let device = Obd2 { device: dev }; + + Ok(device) + } + + /// Resets the device + pub fn reset(&mut self) -> Result<()> { + Ok(self.device.reset()?) + } + fn command(&mut self, command: &[u8]) -> Result>> { let response = self .device diff --git a/src/lib.rs b/src/lib.rs index b0b6261..2e04c5b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,14 +6,20 @@ //! //! # Usage //! ``` -//! use obd2::{commands::Obd2DataRetrieval, device::Elm327, Obd2}; +//! use obd2::{commands::Obd2DataRetrieval, device::{Elm327, FTDIDevice}, Obd2}; //! //! fn main() -> Result<(), obd2::Error> { -//! let mut device = Obd2::::default(); +//! let mut device = Obd2::>::new(Elm327::new(FTDIDevice::new()?)?)?; //! println!("VIN: {}", device.get_vin()?); //! Ok(()) //! } //! ``` +//! +//! alternatively, you could use a serial port provided by your operating system such as +//! /dev/ttyUSB0 on unix-like systems +//! ``` +//! let mut device = Obd2::>::new(Elm327::new(SerialPort::new("/dev/ttyUSB0")?)?)?; +//! ``` #![forbid(unsafe_code)] #![warn(missing_docs)] @@ -22,7 +28,7 @@ pub mod commands; pub mod device; -mod error; +pub mod error; pub use error::Error; use error::Result; From 11713808774871e049300f648e8d0c40c19010b9 Mon Sep 17 00:00:00 2001 From: Robert Sammelson Date: Thu, 20 Mar 2025 02:17:56 -0400 Subject: [PATCH 2/4] Fix lint issues and add comments Lint issues: - spelling problems - iter instead of into_iter --- src/commands/types.rs | 2 +- src/device/elm327.rs | 13 ++++++++++++- src/error.rs | 4 ++-- src/interface.rs | 5 ++++- src/lib.rs | 2 +- src/obd2_device.rs | 2 +- 6 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/commands/types.rs b/src/commands/types.rs index 5c10d8b..75f1f21 100644 --- a/src/commands/types.rs +++ b/src/commands/types.rs @@ -42,7 +42,7 @@ impl From for Dtc { 1 => Dtc::Chassis(n), 2 => Dtc::Body(n), 3 => Dtc::Network(n), - _ => unreachable!(), + _ => unreachable!(), // can't happen, only two bits } } } diff --git a/src/device/elm327.rs b/src/device/elm327.rs index 83543a6..59e3c17 100644 --- a/src/device/elm327.rs +++ b/src/device/elm327.rs @@ -23,6 +23,10 @@ pub struct Elm327 { } impl Default for Elm327 { + /// Create a Elm327 device + /// + /// # Panics + /// If the device cannot be initialized. Use [Self::new] for a panic-free API. fn default() -> Self { Elm327::new().unwrap() } @@ -40,7 +44,7 @@ impl Obd2BaseDevice for Elm327 { fn send_cmd(&mut self, data: &[u8]) -> Result<()> { trace!("send_cmd: sending {:?}", std::str::from_utf8(data)); self.send_serial_str( - data.into_iter() + data.iter() .flat_map(|v| format!("{:02X}", v).chars().collect::>()) .collect::() .as_str(), @@ -134,15 +138,22 @@ impl Elm327 { fn reset_protocol(&mut self) -> Result<()> { info!("Performing protocol reset"); + + // set to use automatic protocol selection debug!( "reset_protocol: got response {:?}", self.serial_cmd("ATSP0")? ); + + // perform the search debug!( "reset_protocol: got OBD response {:?}", self.cmd(&[0x01, 0x00])? ); + + // get rid of extra data hanging around in the buffer self.flush_buffers()?; + Ok(()) } diff --git a/src/error.rs b/src/error.rs index 6feac68..f209597 100644 --- a/src/error.rs +++ b/src/error.rs @@ -27,12 +27,12 @@ impl From for Error { impl From for Error { fn from(e: std::num::ParseIntError) -> Self { - Error::Other(format!("invalid data recieved: {:?}", e)) + Error::Other(format!("invalid data received: {:?}", e)) } } impl From for Error { fn from(e: std::string::FromUtf8Error) -> Self { - Error::Other(format!("invalid string recieved: {:?}", e)) + Error::Other(format!("invalid string received: {:?}", e)) } } diff --git a/src/interface.rs b/src/interface.rs index 5d49c0b..88328eb 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -17,9 +17,11 @@ impl Obd2Device for Obd2 { for response in result.iter() { if response.first() != Some(&(0x40 | mode)) { + // mismatch of mode in response todo!() } if response.get(1) != Some(&pid) { + // mismatch of PID in response todo!() } } @@ -109,7 +111,8 @@ impl Obd2 { .filter_map(|l| l.split_once(':')) .flat_map(|(idx, data)| { if u8::from_str_radix(idx, 16) != Ok(n_idx) { - todo!() + // got an invalid hex code or values were not already in the correct order + todo!("Line index: {}, should be {:X}", idx, n_idx) } n_idx = (n_idx + 1) % 0x10; data.split_whitespace().map(|s| s.to_owned()) diff --git a/src/lib.rs b/src/lib.rs index b0b6261..65a7a17 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ //! ``` #![forbid(unsafe_code)] -#![warn(missing_docs)] +#![warn(missing_docs, clippy::panic)] pub mod commands; diff --git a/src/obd2_device.rs b/src/obd2_device.rs index 219855e..b7268fd 100644 --- a/src/obd2_device.rs +++ b/src/obd2_device.rs @@ -13,7 +13,7 @@ pub trait Obd2Device { /// /// 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. + /// the mode the vehicle received---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 From 11e87eafaf2e499e4ef48d4e4dcda34fa34bb8ce Mon Sep 17 00:00:00 2001 From: Nicholas Orlowsky Date: Tue, 18 Mar 2025 22:03:13 -0400 Subject: [PATCH 3/4] Various changes - allow communication to elm327 device via a serialport - allow creation of elm327 device without requiring unwrap() - added some extra documentation - add fuel_level obd-ii command - expose reset() functionality of device --- Cargo.toml | 1 + README.md | 11 ++++- examples/basic/main.rs | 11 +++-- src/commands/mod.rs | 3 ++ src/device/elm327.rs | 88 ++++++++++++++---------------------- src/device/mod.rs | 13 ++++++ src/device/serial_comm.rs | 94 +++++++++++++++++++++++++++++++++++++++ src/error.rs | 6 ++- src/interface.rs | 13 +++++- src/lib.rs | 12 +++-- 10 files changed, 186 insertions(+), 66 deletions(-) create mode 100644 src/device/serial_comm.rs diff --git a/Cargo.toml b/Cargo.toml index ec43f5f..b05ba2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,4 @@ env_logger = "0.10" ftdi = "0.1.3" log = "0.4.8" thiserror = "1.0.15" +serialport="=4.6.1" diff --git a/README.md b/README.md index 9992303..412613f 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,20 @@ vehicle. ## Usage ```rs -use obd2::{commands::Obd2DataRetrieval, device::Elm327, Obd2}; +use obd2::{commands::Obd2DataRetrieval, device::{Elm327, FTDIDevice}, Obd2}; fn main() -> Result<(), obd2::Error> { - let mut device = Obd2::::default(); + let mut device = Obd2::>::new(Elm327::new(FTDIDevice::new()?)?)?; println!("VIN: {}", device.get_vin()?); Ok(()) } ``` +alternatively, you could use a serial port provided by your operating system such as +/dev/ttyUSB0 on unix-like systems + +```rs +let mut device = Obd2::>::new(Elm327::new(SerialPort::new("/dev/ttyUSB0")?)?)?; +``` + See the docs for more: https://docs.rs/obd2/ diff --git a/examples/basic/main.rs b/examples/basic/main.rs index f396bb2..7ecb3ad 100644 --- a/examples/basic/main.rs +++ b/examples/basic/main.rs @@ -2,15 +2,16 @@ use obd2::commands::Obd2DataRetrieval; use std::time; -fn main() { +fn main() -> Result<(), obd2::Error> { env_logger::init(); - let mut device: obd2::Obd2 = obd2::Obd2::default(); + let mut device: obd2::Obd2> = + obd2::Obd2::new(obd2::device::Elm327::new(obd2::device::FTDIDevice::new()?)?)?; println!("VIN: {:?}", device.get_vin()); - for s in device.get_service_1_pid_support_1().unwrap().iter() { + for s in device.get_service_1_pid_support_1()?.iter() { println!("PID support ($01-$20): {:08X}", s); } - for s in device.get_service_1_pid_support_2().unwrap().iter() { + for s in device.get_service_1_pid_support_2()?.iter() { println!("PID support ($21-$40): {:08X}", s); } @@ -47,4 +48,6 @@ fn main() { device.get_throttle_position() ); } + + Ok(()) } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 1265eb9..6d60b1a 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -220,4 +220,7 @@ func! { /// Get service 1 PID support for $21 to $40 fn get_service_1_pid_support_2(0x01, 0x20) -> u32; + + // Get the fuel level (out of 255) + fn get_fuel_level(0x01, 0x2F) -> u8; } diff --git a/src/device/elm327.rs b/src/device/elm327.rs index 59e3c17..2352fe3 100644 --- a/src/device/elm327.rs +++ b/src/device/elm327.rs @@ -1,38 +1,24 @@ use log::{debug, info, trace}; -use std::{ - collections::VecDeque, - io::{Read, Write}, - thread, time, -}; +use std::{collections::VecDeque, thread, time}; -use super::{Error, Obd2BaseDevice, Obd2Reader, Result}; +use super::{serial_comm::SerialComm, Error, Obd2BaseDevice, Obd2Reader, Result}; /// An ELM327 OBD-II adapter /// -/// It communicates with the computer over UART using an FTDI FT232R USB-to-UART converter. +/// It communicates with the computer or 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. /// /// [Datasheet for v1.4b](https://github.com/rsammelson/obd2/blob/master/docs/ELM327DSH.pdf), and /// the [source](https://www.elmelectronics.com/products/dsheets/). -pub struct Elm327 { - device: ftdi::Device, +pub struct Elm327 { + device: T, buffer: VecDeque, baud_rate: u32, } -impl Default for Elm327 { - /// Create a Elm327 device - /// - /// # Panics - /// If the device cannot be initialized. Use [Self::new] for a panic-free API. - fn default() -> Self { - Elm327::new().unwrap() - } -} - -impl Obd2BaseDevice for Elm327 { +impl Obd2BaseDevice for Elm327 { fn reset(&mut self) -> Result<()> { self.flush_buffers()?; self.reset_ic()?; @@ -52,7 +38,7 @@ impl Obd2BaseDevice for Elm327 { } } -impl Obd2Reader for Elm327 { +impl Obd2Reader for Elm327 { fn get_line(&mut self) -> Result>> { self.get_until(b'\n', false) } @@ -68,22 +54,14 @@ impl Obd2Reader for Elm327 { } } -impl Elm327 { - fn new() -> Result { - let mut ftdi_device = ftdi::find_by_vid_pid(0x0403, 0x6001) - .interface(ftdi::Interface::A) - .open()?; - - ftdi_device.set_baud_rate(38400)?; - ftdi_device.configure(ftdi::Bits::Eight, ftdi::StopBits::One, ftdi::Parity::None)?; - // device.set_latency_timer(2).unwrap(); - - ftdi_device.usb_reset()?; - +impl Elm327 { + /// Creates a new Elm327 adapter with the given + /// unserlying Serial Communication device + pub fn new(serial_device: T) -> Result { let mut device = Elm327 { - device: ftdi_device, + device: serial_device, buffer: VecDeque::new(), - baud_rate: 38400, + baud_rate: 38_400, }; device.connect(false)?; @@ -102,7 +80,7 @@ impl Elm327 { } fn flush_buffers(&mut self) -> Result<()> { - self.device.usb_purge_buffers()?; + self.device.purge_buffers()?; Ok(()) } @@ -127,11 +105,10 @@ impl Elm327 { fn reset_ic(&mut self) -> Result<()> { info!("Performing IC reset"); self.send_serial_str("ATZ")?; + let response = self.get_response()?; debug!( "reset_ic: got response {:?}", - self.get_response()? - .as_ref() - .map(|l| std::str::from_utf8(l.as_slice())) + response.as_ref().map(|l| std::str::from_utf8(l.as_slice())) ); Ok(()) } @@ -140,16 +117,12 @@ impl Elm327 { info!("Performing protocol reset"); // set to use automatic protocol selection - debug!( - "reset_protocol: got response {:?}", - self.serial_cmd("ATSP0")? - ); + let elm_response = self.serial_cmd("ATSP0")?; + debug!("reset_protocol: got response {:?}", elm_response); - // perform the search - debug!( - "reset_protocol: got OBD response {:?}", - self.cmd(&[0x01, 0x00])? - ); + // perform the search for ECUs + let obd_response = self.cmd(&[0x01, 0x00])?; + debug!("reset_protocol: got OBD response {:?}", obd_response); // get rid of extra data hanging around in the buffer self.flush_buffers()?; @@ -269,13 +242,18 @@ impl Elm327 { fn read_into_queue(&mut self) -> Result<()> { let mut buf = [0u8; 16]; loop { - let len = self.device.read(&mut buf)?; - if len > 0 { - self.buffer.extend(&buf[0..len]); - trace!( - "read_into_queue: values {:?}", - std::str::from_utf8(&buf[0..len]) - ); + let len_res = self.device.read(&mut buf); + if let Ok(len) = len_res { + if len > 0 { + self.buffer.extend(&buf[0..len]); + trace!( + "read_into_queue: values {:?}", + std::str::from_utf8(&buf[0..len]) + ); + } else { + trace!("read_into_queue: no values left to read"); + break; + } } else { trace!("read_into_queue: no values left to read"); break; diff --git a/src/device/mod.rs b/src/device/mod.rs index 1f0f561..31f0293 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -3,6 +3,9 @@ mod elm327; pub use elm327::Elm327; +mod serial_comm; +pub use serial_comm::{FTDIDevice, SerialPort}; + type Result = std::result::Result; /// A lower-level API for using an OBD-II device @@ -54,6 +57,10 @@ pub enum Error { #[error("FTDI error: `{0:?}`")] Ftdi(ftdi::Error), + /// An error with the underlying [serialport device](serialport::SerialPort) + #[error("Serialport error: `{0:?}`")] + Serialport(serialport::Error), + /// An I/O error in a low-level [std::io] stream operation #[error("IO error: `{0:?}`")] IO(std::io::Error), @@ -69,6 +76,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: serialport::Error) -> Self { + Error::Serialport(e) + } +} + impl From for Error { fn from(e: std::io::Error) -> Self { Error::IO(e) diff --git a/src/device/serial_comm.rs b/src/device/serial_comm.rs new file mode 100644 index 0000000..0ab6467 --- /dev/null +++ b/src/device/serial_comm.rs @@ -0,0 +1,94 @@ +use super::Result; +use std::io::{Read, Write}; +use std::time::Duration; + +const DEFAULT_BAUD_RATE: u32 = 38_400; + +/// An API to communicate with a serial device +pub trait SerialComm { + fn write_all(&mut self, data: &[u8]) -> Result<()>; + fn read(&mut self, data: &mut [u8]) -> Result; + fn set_baud_rate(&mut self, baud_rate: u32) -> Result<()>; + fn purge_buffers(&mut self) -> Result<()>; +} + +/// Communicate with a serial device using the +/// serialport library +/// +/// /dev/tty* or similar on unix-like systems +/// COM devices on Windows systems +pub struct SerialPort { + device: Box, +} + +impl SerialPort { + /// Creates a new instance of a SerialPort + pub fn new(path: &str) -> Result { + let device = serialport::new(path, DEFAULT_BAUD_RATE) + .timeout(Duration::from_millis(10)) + .parity(serialport::Parity::None) + .data_bits(serialport::DataBits::Eight) + .stop_bits(serialport::StopBits::One) + .path(path) + .open()?; + + Ok(Self { device }) + } +} + +impl SerialComm for SerialPort { + fn write_all(&mut self, data: &[u8]) -> Result<()> { + Ok(self.device.write_all(data)?) + } + + fn read(&mut self, data: &mut [u8]) -> Result { + Ok(self.device.read(data)?) + } + + fn set_baud_rate(&mut self, baud_rate: u32) -> Result<()> { + Ok(self.device.set_baud_rate(baud_rate)?) + } + + fn purge_buffers(&mut self) -> Result<()> { + Ok(self.device.clear(serialport::ClearBuffer::All)?) + } +} + +/// Communicate with a USB to Serial FTDI device +/// with the FTDI library +pub struct FTDIDevice { + device: ftdi::Device, +} + +impl FTDIDevice { + /// Creates a new instance of an FTDIDevice + pub fn new() -> Result { + let mut device = ftdi::find_by_vid_pid(0x0404, 0x6001) + .interface(ftdi::Interface::A) + .open()?; + + device.set_baud_rate(DEFAULT_BAUD_RATE)?; + device.configure(ftdi::Bits::Eight, ftdi::StopBits::One, ftdi::Parity::None)?; + device.usb_reset()?; + + Ok(Self { device }) + } +} + +impl SerialComm for FTDIDevice { + fn write_all(&mut self, data: &[u8]) -> Result<()> { + Ok(self.device.write_all(data)?) + } + + fn read(&mut self, data: &mut [u8]) -> Result { + Ok(self.device.read(data)?) + } + + fn set_baud_rate(&mut self, baud_rate: u32) -> Result<()> { + Ok(self.device.set_baud_rate(baud_rate)?) + } + + fn purge_buffers(&mut self) -> Result<()> { + Ok(self.device.usb_purge_buffers()?) + } +} diff --git a/src/error.rs b/src/error.rs index f209597..f6afd75 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,6 @@ +//! Error types for OBD-II related errors + +/// Result type defaulted with this library's error type pub type Result = std::result::Result; /// An error with OBD-II communication @@ -16,8 +19,9 @@ pub enum Error { Other(String), } +/// An error with the ELM327 device #[derive(Debug)] -pub struct DeviceError(crate::device::Error); +pub struct DeviceError(pub crate::device::Error); impl From for Error { fn from(e: super::device::Error) -> Self { diff --git a/src/interface.rs b/src/interface.rs index 88328eb..a73b7df 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -6,7 +6,6 @@ use super::{device::Obd2BaseDevice, Error, Obd2Device, Result}; /// /// Wraps an implementer of [Obd2BaseDevice] to allow for higher-level usage of the OBD-II /// interface. -#[derive(Default)] pub struct Obd2 { device: T, } @@ -43,6 +42,18 @@ impl Obd2Device for Obd2 { } impl Obd2 { + /// Creates a new instance of an Obd device + pub fn new(dev: T) -> Result { + let device = Obd2 { device: dev }; + + Ok(device) + } + + /// Resets the device + pub fn reset(&mut self) -> Result<()> { + Ok(self.device.reset()?) + } + fn command(&mut self, command: &[u8]) -> Result>> { let response = self .device diff --git a/src/lib.rs b/src/lib.rs index 65a7a17..7b9805d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,14 +6,20 @@ //! //! # Usage //! ``` -//! use obd2::{commands::Obd2DataRetrieval, device::Elm327, Obd2}; +//! use obd2::{commands::Obd2DataRetrieval, device::{Elm327, FTDIDevice}, Obd2}; //! //! fn main() -> Result<(), obd2::Error> { -//! let mut device = Obd2::::default(); +//! let mut device = Obd2::>::new(Elm327::new(FTDIDevice::new()?)?)?; //! println!("VIN: {}", device.get_vin()?); //! Ok(()) //! } //! ``` +//! +//! alternatively, you could use a serial port provided by your operating system such as +//! /dev/ttyUSB0 on unix-like systems +//! ``` +//! let mut device = Obd2::>::new(Elm327::new(SerialPort::new("/dev/ttyUSB0")?)?)?; +//! ``` #![forbid(unsafe_code)] #![warn(missing_docs, clippy::panic)] @@ -22,7 +28,7 @@ pub mod commands; pub mod device; -mod error; +pub mod error; pub use error::Error; use error::Result; From 1769d2a84b2e795a7349d379797311ebc2a307ec Mon Sep 17 00:00:00 2001 From: Nicholas Orlowsky Date: Thu, 20 Mar 2025 13:56:26 -0400 Subject: [PATCH 4/4] undid changes + put ftdi & serial behind features - serial_comm and ftdi_comm features added - received/recieved typos reverted for inclusion in another PR - fuel_level change reverted for inclusion in another PR - DeviceError's 0 field made private again --- Cargo.toml | 8 +++- src/commands/mod.rs | 3 -- src/device/ftdi_comm.rs | 42 +++++++++++++++++ src/device/mod.rs | 15 ++++++- src/device/serial_comm.rs | 85 +---------------------------------- src/device/serialport_comm.rs | 46 +++++++++++++++++++ src/error.rs | 6 +-- 7 files changed, 112 insertions(+), 93 deletions(-) create mode 100644 src/device/ftdi_comm.rs create mode 100644 src/device/serialport_comm.rs diff --git a/Cargo.toml b/Cargo.toml index b05ba2e..bfa0625 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,11 @@ edition = "2021" [dependencies] env_logger = "0.10" -ftdi = "0.1.3" log = "0.4.8" thiserror = "1.0.15" -serialport="=4.6.1" +ftdi = { version = "0.1.3", optional = true } +serialport= { version = "=4.6.1", optional = true } + +[features] +ftdi_comm = [ "dep:ftdi" ] +serialport_comm = [ "dep:serialport" ] diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 6d60b1a..1265eb9 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -220,7 +220,4 @@ func! { /// Get service 1 PID support for $21 to $40 fn get_service_1_pid_support_2(0x01, 0x20) -> u32; - - // Get the fuel level (out of 255) - fn get_fuel_level(0x01, 0x2F) -> u8; } diff --git a/src/device/ftdi_comm.rs b/src/device/ftdi_comm.rs new file mode 100644 index 0000000..b65afdb --- /dev/null +++ b/src/device/ftdi_comm.rs @@ -0,0 +1,42 @@ +use super::serial_comm::{SerialComm, DEFAULT_BAUD_RATE}; +use super::Result; +use std::io::{Read, Write}; + +/// Communicate with a USB to Serial FTDI device +/// with the FTDI library +pub struct FTDIDevice { + device: ftdi::Device, +} + +impl FTDIDevice { + /// Creates a new instance of an FTDIDevice + pub fn new() -> Result { + let mut device = ftdi::find_by_vid_pid(0x0404, 0x6001) + .interface(ftdi::Interface::A) + .open()?; + + device.set_baud_rate(DEFAULT_BAUD_RATE)?; + device.configure(ftdi::Bits::Eight, ftdi::StopBits::One, ftdi::Parity::None)?; + device.usb_reset()?; + + Ok(Self { device }) + } +} + +impl SerialComm for FTDIDevice { + fn write_all(&mut self, data: &[u8]) -> Result<()> { + Ok(self.device.write_all(data)?) + } + + fn read(&mut self, data: &mut [u8]) -> Result { + Ok(self.device.read(data)?) + } + + fn set_baud_rate(&mut self, baud_rate: u32) -> Result<()> { + Ok(self.device.set_baud_rate(baud_rate)?) + } + + fn purge_buffers(&mut self) -> Result<()> { + Ok(self.device.usb_purge_buffers()?) + } +} diff --git a/src/device/mod.rs b/src/device/mod.rs index 31f0293..878d2aa 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -4,7 +4,16 @@ mod elm327; pub use elm327::Elm327; mod serial_comm; -pub use serial_comm::{FTDIDevice, SerialPort}; + +#[cfg(feature = "ftdi_comm")] +mod ftdi_comm; +#[cfg(feature = "ftdi_comm")] +pub use ftdi_comm::FTDIDevice; + +#[cfg(feature = "serialport_comm")] +mod serialport_comm; +#[cfg(feature = "serialport_comm")] +pub use serialport_comm::SerialPort; type Result = std::result::Result; @@ -54,10 +63,12 @@ pub trait Obd2Reader { #[derive(thiserror::Error, Debug)] pub enum Error { /// An error with the underlying [FTDI device](ftdi::Device) + #[cfg(feature = "ftdi_comm")] #[error("FTDI error: `{0:?}`")] Ftdi(ftdi::Error), /// An error with the underlying [serialport device](serialport::SerialPort) + #[cfg(feature = "serialport_comm")] #[error("Serialport error: `{0:?}`")] Serialport(serialport::Error), @@ -70,12 +81,14 @@ pub enum Error { Communication(String), } +#[cfg(feature = "ftdi_comm")] impl From for Error { fn from(e: ftdi::Error) -> Self { Error::Ftdi(e) } } +#[cfg(feature = "serialport_comm")] impl From for Error { fn from(e: serialport::Error) -> Self { Error::Serialport(e) diff --git a/src/device/serial_comm.rs b/src/device/serial_comm.rs index 0ab6467..8c31b50 100644 --- a/src/device/serial_comm.rs +++ b/src/device/serial_comm.rs @@ -1,8 +1,6 @@ use super::Result; -use std::io::{Read, Write}; -use std::time::Duration; -const DEFAULT_BAUD_RATE: u32 = 38_400; +pub const DEFAULT_BAUD_RATE: u32 = 38_400; /// An API to communicate with a serial device pub trait SerialComm { @@ -11,84 +9,3 @@ pub trait SerialComm { fn set_baud_rate(&mut self, baud_rate: u32) -> Result<()>; fn purge_buffers(&mut self) -> Result<()>; } - -/// Communicate with a serial device using the -/// serialport library -/// -/// /dev/tty* or similar on unix-like systems -/// COM devices on Windows systems -pub struct SerialPort { - device: Box, -} - -impl SerialPort { - /// Creates a new instance of a SerialPort - pub fn new(path: &str) -> Result { - let device = serialport::new(path, DEFAULT_BAUD_RATE) - .timeout(Duration::from_millis(10)) - .parity(serialport::Parity::None) - .data_bits(serialport::DataBits::Eight) - .stop_bits(serialport::StopBits::One) - .path(path) - .open()?; - - Ok(Self { device }) - } -} - -impl SerialComm for SerialPort { - fn write_all(&mut self, data: &[u8]) -> Result<()> { - Ok(self.device.write_all(data)?) - } - - fn read(&mut self, data: &mut [u8]) -> Result { - Ok(self.device.read(data)?) - } - - fn set_baud_rate(&mut self, baud_rate: u32) -> Result<()> { - Ok(self.device.set_baud_rate(baud_rate)?) - } - - fn purge_buffers(&mut self) -> Result<()> { - Ok(self.device.clear(serialport::ClearBuffer::All)?) - } -} - -/// Communicate with a USB to Serial FTDI device -/// with the FTDI library -pub struct FTDIDevice { - device: ftdi::Device, -} - -impl FTDIDevice { - /// Creates a new instance of an FTDIDevice - pub fn new() -> Result { - let mut device = ftdi::find_by_vid_pid(0x0404, 0x6001) - .interface(ftdi::Interface::A) - .open()?; - - device.set_baud_rate(DEFAULT_BAUD_RATE)?; - device.configure(ftdi::Bits::Eight, ftdi::StopBits::One, ftdi::Parity::None)?; - device.usb_reset()?; - - Ok(Self { device }) - } -} - -impl SerialComm for FTDIDevice { - fn write_all(&mut self, data: &[u8]) -> Result<()> { - Ok(self.device.write_all(data)?) - } - - fn read(&mut self, data: &mut [u8]) -> Result { - Ok(self.device.read(data)?) - } - - fn set_baud_rate(&mut self, baud_rate: u32) -> Result<()> { - Ok(self.device.set_baud_rate(baud_rate)?) - } - - fn purge_buffers(&mut self) -> Result<()> { - Ok(self.device.usb_purge_buffers()?) - } -} diff --git a/src/device/serialport_comm.rs b/src/device/serialport_comm.rs new file mode 100644 index 0000000..1ff12fb --- /dev/null +++ b/src/device/serialport_comm.rs @@ -0,0 +1,46 @@ +use super::serial_comm::{SerialComm, DEFAULT_BAUD_RATE}; +use super::Result; +use std::io::{Read, Write}; +use std::time::Duration; + +/// Communicate with a serial device using the +/// serialport library +/// +/// /dev/tty* or similar on unix-like systems +/// COM devices on Windows systems +pub struct SerialPort { + device: Box, +} + +impl SerialPort { + /// Creates a new instance of a SerialPort + pub fn new(path: &str) -> Result { + let device = serialport::new(path, DEFAULT_BAUD_RATE) + .timeout(Duration::from_millis(10)) + .parity(serialport::Parity::None) + .data_bits(serialport::DataBits::Eight) + .stop_bits(serialport::StopBits::One) + .path(path) + .open()?; + + Ok(Self { device }) + } +} + +impl SerialComm for SerialPort { + fn write_all(&mut self, data: &[u8]) -> Result<()> { + Ok(self.device.write_all(data)?) + } + + fn read(&mut self, data: &mut [u8]) -> Result { + Ok(self.device.read(data)?) + } + + fn set_baud_rate(&mut self, baud_rate: u32) -> Result<()> { + Ok(self.device.set_baud_rate(baud_rate)?) + } + + fn purge_buffers(&mut self) -> Result<()> { + Ok(self.device.clear(serialport::ClearBuffer::All)?) + } +} diff --git a/src/error.rs b/src/error.rs index f6afd75..7368b97 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,7 +21,7 @@ pub enum Error { /// An error with the ELM327 device #[derive(Debug)] -pub struct DeviceError(pub crate::device::Error); +pub struct DeviceError(crate::device::Error); impl From for Error { fn from(e: super::device::Error) -> Self { @@ -31,12 +31,12 @@ impl From for Error { impl From for Error { fn from(e: std::num::ParseIntError) -> Self { - Error::Other(format!("invalid data received: {:?}", e)) + Error::Other(format!("invalid data recieved: {:?}", e)) } } impl From for Error { fn from(e: std::string::FromUtf8Error) -> Self { - Error::Other(format!("invalid string received: {:?}", e)) + Error::Other(format!("invalid string recieved: {:?}", e)) } }