From 0994648ed17c037320fc8778bd42fd8086dd9eb5 Mon Sep 17 00:00:00 2001 From: Nicholas Orlowsky Date: Sun, 16 Mar 2025 15:12:29 -0400 Subject: [PATCH 1/6] api modifications --- Cargo.toml | 2 ++ src/commands/mod.rs | 2 ++ src/device/elm327.rs | 8 +------- src/device/mod.rs | 3 +++ src/error.rs | 2 +- src/interface.rs | 9 ++++++++- src/lib.rs | 2 +- 7 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ec43f5f..75dc960 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,5 @@ env_logger = "0.10" ftdi = "0.1.3" log = "0.4.8" thiserror = "1.0.15" +serialport="=4.6.1" +anyhow = "1.0.97" diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 1265eb9..8ce16d5 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -220,4 +220,6 @@ func! { /// Get service 1 PID support for $21 to $40 fn get_service_1_pid_support_2(0x01, 0x20) -> u32; + + fn get_fuel_level(0x01, 0x2F) -> u8; } diff --git a/src/device/elm327.rs b/src/device/elm327.rs index 83543a6..5482193 100644 --- a/src/device/elm327.rs +++ b/src/device/elm327.rs @@ -22,12 +22,6 @@ pub struct Elm327 { baud_rate: u32, } -impl Default for Elm327 { - fn default() -> Self { - Elm327::new().unwrap() - } -} - impl Obd2BaseDevice for Elm327 { fn reset(&mut self) -> Result<()> { self.flush_buffers()?; @@ -66,7 +60,7 @@ impl Obd2Reader for Elm327 { impl Elm327 { fn new() -> Result { - let mut ftdi_device = ftdi::find_by_vid_pid(0x0403, 0x6001) + let mut ftdi_device = ftdi::find_by_vid_pid(0x0404, 0x6001) .interface(ftdi::Interface::A) .open()?; diff --git a/src/device/mod.rs b/src/device/mod.rs index 1f0f561..9a8d934 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -3,6 +3,9 @@ mod elm327; pub use elm327::Elm327; +mod elm327_linux; +pub use elm327_linux::Elm327Linux; + type Result = std::result::Result; /// A lower-level API for using an OBD-II device diff --git a/src/error.rs b/src/error.rs index 6feac68..725427f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -17,7 +17,7 @@ pub enum Error { } #[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 5d49c0b..d9beb4c 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,14 @@ impl Obd2Device for Obd2 { } impl Obd2 { + pub fn new(dev: T) -> ::anyhow::Result { + let mut device = Obd2 { + device: dev + }; + + Ok(device) + } + fn command(&mut self, command: &[u8]) -> Result>> { let response = self .device diff --git a/src/lib.rs b/src/lib.rs index b0b6261..7fc066a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,7 @@ pub mod commands; pub mod device; -mod error; +pub mod error; pub use error::Error; use error::Result; From ab604f3c517247cf8c89a537fb3d4ddb8b1deae9 Mon Sep 17 00:00:00 2001 From: Nicholas Orlowsky Date: Tue, 18 Mar 2025 17:13:03 -0400 Subject: [PATCH 2/6] last before fix --- src/device/elm327_linux.rs | 240 +++++++++++++++++++++++++++++++++++++ src/interface.rs | 4 + 2 files changed, 244 insertions(+) create mode 100644 src/device/elm327_linux.rs diff --git a/src/device/elm327_linux.rs b/src/device/elm327_linux.rs new file mode 100644 index 0000000..5d40fad --- /dev/null +++ b/src/device/elm327_linux.rs @@ -0,0 +1,240 @@ +use log::{debug, info, trace}; +use std::{ + collections::VecDeque, + io::{Read, Write}, + thread, time, +}; +use std::time::Duration; + +use super::{Error, Obd2BaseDevice, Obd2Reader, Result}; + +/// An ELM327 OBD-II adapter +/// +/// Uses a /dev/ttyUSB* device or similar to communicate +pub struct Elm327Linux { + port: Box, + buffer: VecDeque, + baud_rate: u32, +} + +impl Obd2BaseDevice for Elm327Linux { + fn reset(&mut self) -> Result<()> { + self.flush_buffers()?; + self.reset_ic()?; + thread::sleep(time::Duration::from_millis(500)); + self.reset_protocol()?; + Ok(()) + } + + fn send_cmd(&mut self, data: &[u8]) -> Result<()> { + trace!("send_cmd: sending {:?}", std::str::from_utf8(data)); + self.send_serial_str( + data.into_iter() + .flat_map(|v| format!("{:02X}", v).chars().collect::>()) + .collect::() + .as_str(), + ) + } +} + +impl Obd2Reader for Elm327Linux { + fn get_line(&mut self) -> Result>> { + self.get_until(b'\n', false) + } + + /// Read data until the ELM327's prompt character is printed + /// + /// This will receive 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 receive 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) + } +} + +impl Elm327Linux { + pub fn new(serial_path: &str) -> ::anyhow::Result { + let port = serialport::new(serial_path, 38_400) + .timeout(Duration::from_millis(10)) + .parity(serialport::Parity::None) + .data_bits(serialport::DataBits::Eight) + .stop_bits(serialport::StopBits::One) + .path(serial_path) + .open()?; + + let mut device = Elm327Linux { + port, + buffer: VecDeque::new(), + baud_rate: 38_400, + }; + + 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 connect(&mut self) -> Result<()> { + thread::sleep(time::Duration::from_millis(500)); + self.serial_cmd(" ")?; + thread::sleep(time::Duration::from_millis(500)); + self.reset()?; + + Ok(()) + } + + fn reset_ic(&mut self) -> Result<()> { + info!("Performing IC reset"); + self.send_serial_str("ATZ")?; + self.get_response()?; + //debug!( + // "reset_ic: got response {:?}", + // self.get_response()? + // .as_ref() + // .map(|l| std::str::from_utf8(l.as_slice())) + //); + Ok(()) + } + + fn flush_buffers(&mut self) -> Result<()> { + self.buffer.flush(); + Ok(()) + } + + fn reset_protocol(&mut self) -> Result<()> { + info!("Performing protocol reset"); + self.serial_cmd("ATSP0")?; + //debug!( + // "reset_protocol: got response {:?}", + // self.serial_cmd("ATSP0")? + //); + self.cmd(&[0x01, 0x00])?; + //debug!( + // "reset_protocol: got OBD response {:?}", + // self.cmd(&[0x01, 0x00])? + //); + self.flush_buffers()?; + Ok(()) + } + + + fn get_until(&mut self, end_byte: u8, allow_empty: bool) -> Result>> { + const TIMEOUT: time::Duration = time::Duration::from_secs(5); + + trace!("get_until: getting until {}", end_byte); + + let mut buf = Vec::new(); + let start = time::Instant::now(); + while start.elapsed() < TIMEOUT { + trace!("Getting Byte"); + let Some(b) = self.get_byte()? else { continue }; + trace!("{}", b); + let b = match b { + b'\r' => Some(b'\n'), + b'\n' => None, // no push here + _ => Some(b), + }; + if let Some(b) = b { + buf.push(b); + if b == end_byte { + break; + } + } + } + + trace!( + "get_until: got {:?} ({:?})", + buf, + std::str::from_utf8(buf.as_slice()) + ); + + match buf.pop() { + Some(b) if b == end_byte => { + trace!("got it!"); + if allow_empty || !buf.is_empty() { + Ok(Some(buf)) + } else { + // empty line, try again + self.get_until(end_byte, allow_empty) + } + } // we got it + Some(f) => { + // incomplete line read + for b in buf.iter().rev() { + self.buffer.push_front(*b); + } + self.buffer.push_front(f); + Ok(None) + } + None => Ok(None), + } + } + + fn get_byte(&mut self) -> Result> { + match self.buffer.pop_front() { + Some(b'\0') => Ok(None), + Some(b) => Ok(Some(b)), + None => { + trace!("EmptyBuf"); + self.read_into_queue()?; + Ok(None) + } + } + } + + fn read_into_queue(&mut self) -> Result<()> { + let mut buf = [0u8; 16]; + loop { + let len_res = self.port.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; + } + } + Ok(()) + } + + fn serial_cmd(&mut self, cmd: &str) -> Result> { + self.send_serial_str(cmd)?; + self.get_response() + .map(|o| o.and_then(|resp| String::from_utf8(resp).ok())) + } + + /// Function for sending a raw string, without encoding into ASCII hex + fn send_serial_str(&mut self, data: &str) -> Result<()> { + trace!("send_serial_str: sending {:?}", data); + + let data = data.as_bytes(); + + self.port.write_all(data)?; + self.port.write_all(b"\r\n")?; + let line = self.get_line()?; + if line.as_ref().is_some_and(|v| v == data) { + Ok(()) + } else { + Err(Error::Communication(format!( + "send_serial_str: got {:?} instead of echoed command ({:?})", + line, data + ))) + } + } +} + diff --git a/src/interface.rs b/src/interface.rs index d9beb4c..e1ec870 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -48,6 +48,10 @@ impl Obd2 { Ok(device) } + pub fn reset(&mut self) -> Result<()> { + Ok(self.device.reset()?) + } + fn command(&mut self, command: &[u8]) -> Result>> { let response = self .device From 6effd4f1a8204ff9fcd89039b7e33674ed085be4 Mon Sep 17 00:00:00 2001 From: Nicholas Orlowsky Date: Tue, 18 Mar 2025 18:59:58 -0400 Subject: [PATCH 3/6] cleanup --- src/commands/mod.rs | 1 + src/device/elm327.rs | 146 +++++++++++++++++----- src/device/elm327_linux.rs | 240 ------------------------------------- src/device/mod.rs | 14 ++- src/interface.rs | 2 +- 5 files changed, 129 insertions(+), 274 deletions(-) delete mode 100644 src/device/elm327_linux.rs diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 8ce16d5..443d8f0 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -221,5 +221,6 @@ 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 5482193..9b8d9e9 100644 --- a/src/device/elm327.rs +++ b/src/device/elm327.rs @@ -4,9 +4,97 @@ use std::{ io::{Read, Write}, thread, time, }; +use std::time::Duration; use super::{Error, Obd2BaseDevice, Obd2Reader, Result}; +trait SerialDevice { + 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<()>; +} + +pub struct SerialPort { + device: Box +} + +impl SerialPort { + pub fn new(path: &str) -> Result { + let port = serialport::new(path, 38_400) + .timeout(Duration::from_millis(10)) + .parity(serialport::Parity::None) + .data_bits(serialport::DataBits::Eight) + .stop_bits(serialport::StopBits::One) + .path(path) + .open()?; + + let device = SerialPort { + device: port + }; + + Ok(device) + } +} + +impl SerialDevice 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)?) + } +} + +pub struct FTDIDevice { + device: ftdi::Device +} + +impl FTDIDevice { + pub fn new() -> Result { + let mut ftdi_device = ftdi::find_by_vid_pid(0x0404, 0x6001) + .interface(ftdi::Interface::A) + .open()?; + + ftdi_device.set_baud_rate(38400)?; + ftdi_device.configure(ftdi::Bits::Eight, ftdi::StopBits::One, ftdi::Parity::None)?; + ftdi_device.usb_reset()?; + + let device = FTDIDevice { + device: ftdi_device + }; + + Ok(device) + } +} + +impl SerialDevice 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()?) + } +} + /// An ELM327 OBD-II adapter /// /// It communicates with the computer over UART using an FTDI FT232R USB-to-UART converter. @@ -16,13 +104,13 @@ use super::{Error, Obd2BaseDevice, Obd2Reader, Result}; /// /// [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 Obd2BaseDevice for Elm327 { +impl Obd2BaseDevice for Elm327 { fn reset(&mut self) -> Result<()> { self.flush_buffers()?; self.reset_ic()?; @@ -42,7 +130,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) } @@ -58,22 +146,12 @@ impl Obd2Reader for Elm327 { } } -impl Elm327 { - fn new() -> Result { - let mut ftdi_device = ftdi::find_by_vid_pid(0x0404, 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 { + 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)?; @@ -92,7 +170,7 @@ impl Elm327 { } fn flush_buffers(&mut self) -> Result<()> { - self.device.usb_purge_buffers()?; + self.device.purge_buffers()?; Ok(()) } @@ -117,9 +195,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()? + response .as_ref() .map(|l| std::str::from_utf8(l.as_slice())) ); @@ -128,14 +207,18 @@ impl Elm327 { fn reset_protocol(&mut self) -> Result<()> { info!("Performing protocol reset"); + let elm_response = self.serial_cmd("ATSP0")?; debug!( "reset_protocol: got response {:?}", - self.serial_cmd("ATSP0")? + elm_response ); + + let obd_response = self.cmd(&[0x01, 0x00])?; debug!( "reset_protocol: got OBD response {:?}", - self.cmd(&[0x01, 0x00])? + obd_response ); + self.flush_buffers()?; Ok(()) } @@ -252,13 +335,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/elm327_linux.rs b/src/device/elm327_linux.rs deleted file mode 100644 index 5d40fad..0000000 --- a/src/device/elm327_linux.rs +++ /dev/null @@ -1,240 +0,0 @@ -use log::{debug, info, trace}; -use std::{ - collections::VecDeque, - io::{Read, Write}, - thread, time, -}; -use std::time::Duration; - -use super::{Error, Obd2BaseDevice, Obd2Reader, Result}; - -/// An ELM327 OBD-II adapter -/// -/// Uses a /dev/ttyUSB* device or similar to communicate -pub struct Elm327Linux { - port: Box, - buffer: VecDeque, - baud_rate: u32, -} - -impl Obd2BaseDevice for Elm327Linux { - fn reset(&mut self) -> Result<()> { - self.flush_buffers()?; - self.reset_ic()?; - thread::sleep(time::Duration::from_millis(500)); - self.reset_protocol()?; - Ok(()) - } - - fn send_cmd(&mut self, data: &[u8]) -> Result<()> { - trace!("send_cmd: sending {:?}", std::str::from_utf8(data)); - self.send_serial_str( - data.into_iter() - .flat_map(|v| format!("{:02X}", v).chars().collect::>()) - .collect::() - .as_str(), - ) - } -} - -impl Obd2Reader for Elm327Linux { - fn get_line(&mut self) -> Result>> { - self.get_until(b'\n', false) - } - - /// Read data until the ELM327's prompt character is printed - /// - /// This will receive 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 receive 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) - } -} - -impl Elm327Linux { - pub fn new(serial_path: &str) -> ::anyhow::Result { - let port = serialport::new(serial_path, 38_400) - .timeout(Duration::from_millis(10)) - .parity(serialport::Parity::None) - .data_bits(serialport::DataBits::Eight) - .stop_bits(serialport::StopBits::One) - .path(serial_path) - .open()?; - - let mut device = Elm327Linux { - port, - buffer: VecDeque::new(), - baud_rate: 38_400, - }; - - 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 connect(&mut self) -> Result<()> { - thread::sleep(time::Duration::from_millis(500)); - self.serial_cmd(" ")?; - thread::sleep(time::Duration::from_millis(500)); - self.reset()?; - - Ok(()) - } - - fn reset_ic(&mut self) -> Result<()> { - info!("Performing IC reset"); - self.send_serial_str("ATZ")?; - self.get_response()?; - //debug!( - // "reset_ic: got response {:?}", - // self.get_response()? - // .as_ref() - // .map(|l| std::str::from_utf8(l.as_slice())) - //); - Ok(()) - } - - fn flush_buffers(&mut self) -> Result<()> { - self.buffer.flush(); - Ok(()) - } - - fn reset_protocol(&mut self) -> Result<()> { - info!("Performing protocol reset"); - self.serial_cmd("ATSP0")?; - //debug!( - // "reset_protocol: got response {:?}", - // self.serial_cmd("ATSP0")? - //); - self.cmd(&[0x01, 0x00])?; - //debug!( - // "reset_protocol: got OBD response {:?}", - // self.cmd(&[0x01, 0x00])? - //); - self.flush_buffers()?; - Ok(()) - } - - - fn get_until(&mut self, end_byte: u8, allow_empty: bool) -> Result>> { - const TIMEOUT: time::Duration = time::Duration::from_secs(5); - - trace!("get_until: getting until {}", end_byte); - - let mut buf = Vec::new(); - let start = time::Instant::now(); - while start.elapsed() < TIMEOUT { - trace!("Getting Byte"); - let Some(b) = self.get_byte()? else { continue }; - trace!("{}", b); - let b = match b { - b'\r' => Some(b'\n'), - b'\n' => None, // no push here - _ => Some(b), - }; - if let Some(b) = b { - buf.push(b); - if b == end_byte { - break; - } - } - } - - trace!( - "get_until: got {:?} ({:?})", - buf, - std::str::from_utf8(buf.as_slice()) - ); - - match buf.pop() { - Some(b) if b == end_byte => { - trace!("got it!"); - if allow_empty || !buf.is_empty() { - Ok(Some(buf)) - } else { - // empty line, try again - self.get_until(end_byte, allow_empty) - } - } // we got it - Some(f) => { - // incomplete line read - for b in buf.iter().rev() { - self.buffer.push_front(*b); - } - self.buffer.push_front(f); - Ok(None) - } - None => Ok(None), - } - } - - fn get_byte(&mut self) -> Result> { - match self.buffer.pop_front() { - Some(b'\0') => Ok(None), - Some(b) => Ok(Some(b)), - None => { - trace!("EmptyBuf"); - self.read_into_queue()?; - Ok(None) - } - } - } - - fn read_into_queue(&mut self) -> Result<()> { - let mut buf = [0u8; 16]; - loop { - let len_res = self.port.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; - } - } - Ok(()) - } - - fn serial_cmd(&mut self, cmd: &str) -> Result> { - self.send_serial_str(cmd)?; - self.get_response() - .map(|o| o.and_then(|resp| String::from_utf8(resp).ok())) - } - - /// Function for sending a raw string, without encoding into ASCII hex - fn send_serial_str(&mut self, data: &str) -> Result<()> { - trace!("send_serial_str: sending {:?}", data); - - let data = data.as_bytes(); - - self.port.write_all(data)?; - self.port.write_all(b"\r\n")?; - let line = self.get_line()?; - if line.as_ref().is_some_and(|v| v == data) { - Ok(()) - } else { - Err(Error::Communication(format!( - "send_serial_str: got {:?} instead of echoed command ({:?})", - line, data - ))) - } - } -} - diff --git a/src/device/mod.rs b/src/device/mod.rs index 9a8d934..cff2463 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -1,10 +1,7 @@ //! Lower level OBD-II interfacing structures mod elm327; -pub use elm327::Elm327; - -mod elm327_linux; -pub use elm327_linux::Elm327Linux; +pub use elm327::{Elm327, SerialPort, FTDIDevice}; type Result = std::result::Result; @@ -56,6 +53,9 @@ pub enum Error { /// An error with the underlying [FTDI device](ftdi::Device) #[error("FTDI error: `{0:?}`")] Ftdi(ftdi::Error), + + #[error("Serialport error: `{0:?}`")] + Serialport(serialport::Error), /// An I/O error in a low-level [std::io] stream operation #[error("IO error: `{0:?}`")] @@ -72,6 +72,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/interface.rs b/src/interface.rs index e1ec870..cc083dd 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -41,7 +41,7 @@ impl Obd2Device for Obd2 { impl Obd2 { pub fn new(dev: T) -> ::anyhow::Result { - let mut device = Obd2 { + let device = Obd2 { device: dev }; From e941cf8bc810da8644ef752d8f1ce97b6c55d450 Mon Sep 17 00:00:00 2001 From: Nicholas Orlowsky Date: Tue, 18 Mar 2025 21:15:33 -0400 Subject: [PATCH 4/6] prefmt --- Cargo.toml | 1 - src/device/elm327.rs | 103 ++++--------------------------------------- src/device/mod.rs | 6 ++- src/error.rs | 8 +++- src/interface.rs | 4 +- 5 files changed, 22 insertions(+), 100 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 75dc960..b05ba2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,3 @@ ftdi = "0.1.3" log = "0.4.8" thiserror = "1.0.15" serialport="=4.6.1" -anyhow = "1.0.97" diff --git a/src/device/elm327.rs b/src/device/elm327.rs index 9b8d9e9..8dd3513 100644 --- a/src/device/elm327.rs +++ b/src/device/elm327.rs @@ -1,116 +1,27 @@ use log::{debug, info, trace}; use std::{ collections::VecDeque, - io::{Read, Write}, thread, time, }; -use std::time::Duration; -use super::{Error, Obd2BaseDevice, Obd2Reader, Result}; - -trait SerialDevice { - 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<()>; -} - -pub struct SerialPort { - device: Box -} - -impl SerialPort { - pub fn new(path: &str) -> Result { - let port = serialport::new(path, 38_400) - .timeout(Duration::from_millis(10)) - .parity(serialport::Parity::None) - .data_bits(serialport::DataBits::Eight) - .stop_bits(serialport::StopBits::One) - .path(path) - .open()?; - - let device = SerialPort { - device: port - }; - - Ok(device) - } -} - -impl SerialDevice 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)?) - } -} - -pub struct FTDIDevice { - device: ftdi::Device -} - -impl FTDIDevice { - pub fn new() -> Result { - let mut ftdi_device = ftdi::find_by_vid_pid(0x0404, 0x6001) - .interface(ftdi::Interface::A) - .open()?; - - ftdi_device.set_baud_rate(38400)?; - ftdi_device.configure(ftdi::Bits::Eight, ftdi::StopBits::One, ftdi::Parity::None)?; - ftdi_device.usb_reset()?; - - let device = FTDIDevice { - device: ftdi_device - }; - - Ok(device) - } -} - -impl SerialDevice 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()?) - } -} +use super::{Error, Obd2BaseDevice, Obd2Reader, Result, serial_comm::SerialComm}; /// 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 { +pub struct Elm327 { device: T, buffer: VecDeque, baud_rate: u32, } -impl Obd2BaseDevice for Elm327 { +impl Obd2BaseDevice for Elm327 { fn reset(&mut self) -> Result<()> { self.flush_buffers()?; self.reset_ic()?; @@ -130,7 +41,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) } @@ -146,7 +57,9 @@ impl Obd2Reader for Elm327 { } } -impl Elm327 { +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: serial_device, diff --git a/src/device/mod.rs b/src/device/mod.rs index cff2463..96fca1c 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -1,7 +1,10 @@ //! Lower level OBD-II interfacing structures mod elm327; -pub use elm327::{Elm327, SerialPort, FTDIDevice}; +pub use elm327::Elm327; + +mod serial_comm; +pub use serial_comm::{SerialPort, FTDIDevice}; type Result = std::result::Result; @@ -54,6 +57,7 @@ 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), diff --git a/src/error.rs b/src/error.rs index 725427f..3498595 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,6 +19,7 @@ pub enum Error { Other(String), } +/// An error with the ELM327 device #[derive(Debug)] pub struct DeviceError(pub crate::device::Error); @@ -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 cc083dd..ae728fa 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -40,7 +40,8 @@ impl Obd2Device for Obd2 { } impl Obd2 { - pub fn new(dev: T) -> ::anyhow::Result { + /// Creates a new instance of an Obd device + pub fn new(dev: T) -> Result { let device = Obd2 { device: dev }; @@ -48,6 +49,7 @@ impl Obd2 { Ok(device) } + /// Resets the device pub fn reset(&mut self) -> Result<()> { Ok(self.device.reset()?) } From 1b3f6a2f6b6a636a688f01e2bae467bdd54c4010 Mon Sep 17 00:00:00 2001 From: Nicholas Orlowsky Date: Tue, 18 Mar 2025 21:15:38 -0400 Subject: [PATCH 5/6] prefmt --- src/device/serial_comm.rs | 95 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/device/serial_comm.rs diff --git a/src/device/serial_comm.rs b/src/device/serial_comm.rs new file mode 100644 index 0000000..0ee1dd1 --- /dev/null +++ b/src/device/serial_comm.rs @@ -0,0 +1,95 @@ +use std::time::Duration; +use super::Result; +use std::io::{Read, Write}; + +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()?) + } +} + From 6426fc7afcbe81d1660cf44d36205fb9b0e0e293 Mon Sep 17 00:00:00 2001 From: Nicholas Orlowsky Date: Tue, 18 Mar 2025 21:33:05 -0400 Subject: [PATCH 6/6] update examples --- examples/basic/main.rs | 14 ++++++++++---- src/lib.rs | 10 ++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/examples/basic/main.rs b/examples/basic/main.rs index f396bb2..fb83144 100644 --- a/examples/basic/main.rs +++ b/examples/basic/main.rs @@ -2,15 +2,19 @@ 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 +51,6 @@ fn main() { device.get_throttle_position() ); } + + Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 7fc066a..10bc301 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)]