diff --git a/Cargo.toml b/Cargo.toml index 44d3ff8..286d7f8 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.2.0-pre1" +version = "0.2.0-pre2" edition = "2021" [dependencies] diff --git a/examples/basic/main.rs b/examples/basic/main.rs index 786abd5..9433b1f 100644 --- a/examples/basic/main.rs +++ b/examples/basic/main.rs @@ -1,4 +1,4 @@ -use obd2::Obd2Device; +use obd2::commands::Obd2DataRetrieval; use std::time; diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..473ccbc --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,155 @@ +//! High level OBD-II interface + +use std::fmt; + +use crate::{Error, Obd2Device, Result}; + +/// Trait for devices that can retrieve data over OBD-II +/// +/// Automatically impelemted for implementors of [Obd2Device]. +pub trait Obd2DataRetrieval: private::Sealed { + /// 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; + + /// Get DTC (diagnostic trouble code) metadata for each ECU + fn get_dtc_info(&mut self) -> Result>; + + /// Get DTCs for each ECU + fn get_dtcs(&mut self) -> Result>>; + + /// Get the RPM in increments of 0.25 + fn get_rpm(&mut self) -> Result; + + /// Get the speed in km/h + fn get_speed(&mut self) -> Result; +} + +impl Obd2DataRetrieval for T { + 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 + Ok(String::from_utf8(result)?) + } + + fn get_dtc_info(&mut self) -> Result> { + let result = self.obd_command(0x01, 0x01)?; + + result + .iter() + .map(|response| { + if response.len() == 4 { + Ok(DtcsInfo { + malfunction_indicator_light: (response[0] & 0x80) == 0x80, + dtc_count: response[0] & 0x7f, + common_test_availability: ((response[1] & 0xf0) >> 1) + | (response[1] & 0x07), + is_compression_engine: (response[1] & 0x08) == 0x08, + specific_test_availability: ((response[3] as u16) << 8) + | (response[2] as u16), + }) + } else { + Err(Error::Other(format!( + "get_dtc_info: expected length 4, got {}", + response.len() + ))) + } + }) + .collect() + } + + fn get_dtcs(&mut self) -> Result>> { + let result = self.obd_mode_command(0x03)?; + result + .iter() + .map(|response| match response.first() { + Some(0) => { + if response.len() % 2 == 1 { + let mut ret = Vec::new(); + for i in (1..response.len()).step_by(2) { + ret.push(match response[i] >> 6 { + 0 => Dtc::Powertrain(0), + 1 => Dtc::Chassis(0), + 2 => Dtc::Body(0), + 3 => Dtc::Network(0), + _ => unreachable!(), + }); + } + Ok(ret) + } else { + Err(Error::Other(format!( + "invalid response when getting DTCs {:?}", + response + ))) + } + } + Some(n) if *n <= 3 => todo!(), + Some(_) => Err(Error::Other(format!( + "invalid response {:?} when getting DTCs", + response + ))), + None => Err(Error::Other( + "no response bytes when getting DTCs".to_owned(), + )), + }) + .collect::>>>() + } + + fn get_rpm(&mut self) -> Result { + let result = self.obd_command_cnt_len::<1, 2>(0x01, 0x0C)?[0]; + Ok(f32::from(u16::from_be_bytes(result)) / 4.0) + } + + fn get_speed(&mut self) -> Result { + Ok(self.obd_command_cnt_len::<1, 1>(0x01, 0x0C)?[0][0]) + } +} + +/// DTC (diagnostic trouble code) metadata +#[derive(Debug)] +#[non_exhaustive] +pub struct DtcsInfo { + /// Whether the "check engine" light is illuminated + pub malfunction_indicator_light: bool, + + /// Number of DTCs for this ECU + pub dtc_count: u8, + + /// Bit field showing availability of seven common tests; the upper bit is currently unused. + pub common_test_availability: u8, + + /// Whether the engine is Diesel + pub is_compression_engine: bool, + + /// Bit field showing availability of sixteen engine-specific tests. What the tests are is + /// based on the value of `is_compression_engine`. + pub specific_test_availability: u16, +} + +/// An individual trouble code from an ECU +#[derive(Debug)] +pub enum Dtc { + Powertrain(u16), + Chassis(u16), + Body(u16), + Network(u16), +} + +impl fmt::Display for Dtc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (c, n) = match self { + Self::Powertrain(n) => ('P', n), + Self::Chassis(n) => ('C', n), + Self::Body(n) => ('B', n), + Self::Network(n) => ('U', n), + }; + f.write_fmt(format_args!("{}{:03X}", c, n)) + } +} + +mod private { + pub trait Sealed {} + impl Sealed for T {} +} diff --git a/src/device/mod.rs b/src/device/mod.rs index f07ca56..22d160f 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -1,3 +1,5 @@ +//! Lower level OBD-II interfacing structures + mod elm327; pub use elm327::Elm327; diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..3138de9 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,32 @@ +pub type Result = std::result::Result; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Device error: `{0:?}`")] + Device(DeviceError), + #[error("Other OBD2 error: `{0}`")] + Other(String), + #[error("Incorrect length (`{0}`): expected `{1}`, got `{2}`")] + IncorrectResponseLength(&'static str, usize, usize), +} + +#[derive(Debug)] +pub struct DeviceError(super::device::Error); + +impl From for Error { + fn from(e: super::device::Error) -> Self { + Error::Device(DeviceError(e)) + } +} + +impl From for Error { + fn from(e: std::num::ParseIntError) -> Self { + Error::Other(format!("invalid data recieved: {:?}", e)) + } +} + +impl From for Error { + fn from(e: std::string::FromUtf8Error) -> Self { + Error::Other(format!("invalid string recieved: {:?}", e)) + } +} diff --git a/src/interface.rs b/src/interface.rs index 7e88ed9..a74944f 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -2,6 +2,10 @@ use log::{debug, trace}; use super::{device::Obd2BaseDevice, Error, Obd2Device, Result}; +/// An OBD-II interface +/// +/// Wraps an implementor of [Obd2BaseDevice] to allow for higher-level usage of the OBD-II +/// interface. #[derive(Default)] pub struct Obd2 { device: T, diff --git a/src/lib.rs b/src/lib.rs index 219b9b5..11b59ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,28 @@ +//! Crate for communicating with OBD-II (on-board diagnostics) interfaces on cars +//! +//! # Usage +//! ``` +//! use obd2::{commands::Obd2DataRetrieval, device::Elm327, Obd2}; +//! +//! fn main() -> Result<(), obd2::Error> { +//! let mut device = Obd2::::default(); +//! println!("VIN: {}", device.get_vin()?); +//! Ok(()) +//! } +//! ``` + #![forbid(unsafe_code)] +pub mod commands; + pub mod device; +mod error; +pub use error::Error; +use error::Result; + mod interface; pub use interface::Obd2; mod obd2_device; -use obd2_device::Result; -pub use obd2_device::{Error, Obd2Device}; +pub use obd2_device::Obd2Device; diff --git a/src/obd2_device.rs b/src/obd2_device.rs index d53c56e..219855e 100644 --- a/src/obd2_device.rs +++ b/src/obd2_device.rs @@ -1,6 +1,4 @@ -use core::fmt; - -pub type Result = std::result::Result; +use crate::{Error, Result}; /// A higher-level API for using an OBD-II device pub trait Obd2Device { @@ -58,151 +56,4 @@ pub trait Obd2Device { .try_into() .map_err(|_| Error::IncorrectResponseLength("count", RESPONSE_COUNT, count)) } - - /// 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 - Ok(String::from_utf8(result)?) - } - - /// Get DTC (diagnostic trouble code) metadata for each ECU - fn get_dtc_info(&mut self) -> Result> { - let result = self.obd_command(0x01, 0x01)?; - - result - .iter() - .map(|response| { - if response.len() == 4 { - Ok(DtcsInfo { - malfunction_indicator_light: (response[0] & 0x80) == 0x80, - dtc_count: response[0] & 0x7f, - common_test_availability: ((response[1] & 0xf0) >> 1) - | (response[1] & 0x07), - is_compression_engine: (response[1] & 0x08) == 0x08, - specific_test_availability: ((response[3] as u16) << 8) - | (response[2] as u16), - }) - } else { - Err(Error::Other(format!( - "get_dtc_info: expected length 4, got {}", - response.len() - ))) - } - }) - .collect() - } - - /// Get DTCs for each ECU - fn get_dtcs(&mut self) -> Result>> { - let result = self.obd_mode_command(0x03)?; - result - .iter() - .map(|response| match response.first() { - Some(0) => { - if response.len() % 2 == 1 { - let mut ret = Vec::new(); - for i in (1..response.len()).step_by(2) { - ret.push(match response[i] >> 6 { - 0 => Dtc::Powertrain(0), - 1 => Dtc::Chassis(0), - 2 => Dtc::Body(0), - 3 => Dtc::Network(0), - _ => unreachable!(), - }); - } - Ok(ret) - } else { - Err(Error::Other(format!( - "invalid response when getting DTCs {:?}", - response - ))) - } - } - Some(n) if *n <= 3 => todo!(), - Some(_) => Err(Error::Other(format!( - "invalid response {:?} when getting DTCs", - response - ))), - None => Err(Error::Other( - "no response bytes when getting DTCs".to_owned(), - )), - }) - .collect::>>>() - } - - /// Get the RPM in increments of 0.25 - fn get_rpm(&mut self) -> Result { - let result = self.obd_command_cnt_len::<1, 2>(0x01, 0x0C)?[0]; - Ok(f32::from(u16::from_be_bytes(result)) / 4.0) - } - - /// Get the speed in km/h - fn get_speed(&mut self) -> Result { - Ok(self.obd_command_cnt_len::<1, 1>(0x01, 0x0C)?[0][0]) - } -} - -#[allow(dead_code)] -#[derive(Debug)] -pub struct DtcsInfo { - malfunction_indicator_light: bool, - dtc_count: u8, - common_test_availability: u8, - is_compression_engine: bool, - specific_test_availability: u16, -} - -#[derive(Debug)] -pub enum Dtc { - Powertrain(u16), - Chassis(u16), - Body(u16), - Network(u16), -} - -impl fmt::Display for Dtc { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let (c, n) = match self { - Self::Powertrain(n) => ('P', n), - Self::Chassis(n) => ('C', n), - Self::Body(n) => ('B', n), - Self::Network(n) => ('U', n), - }; - f.write_fmt(format_args!("{}{:03X}", c, n)) - } -} - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("Device error: `{0:?}`")] - Device(DeviceError), - #[error("Other OBD2 error: `{0}`")] - Other(String), - #[error("Incorrect length (`{0}`): expected `{1}`, got `{2}`")] - IncorrectResponseLength(&'static str, usize, usize), -} - -#[derive(Debug)] -pub struct DeviceError(super::device::Error); - -impl From for Error { - fn from(e: super::device::Error) -> Self { - Error::Device(DeviceError(e)) - } -} - -impl From for Error { - fn from(e: std::num::ParseIntError) -> Self { - Error::Other(format!("invalid data recieved: {:?}", e)) - } -} - -impl From for Error { - fn from(e: std::string::FromUtf8Error) -> Self { - Error::Other(format!("invalid string recieved: {:?}", e)) - } }