From 13b90415117757c1afdf40a86b8cf6e27ab9d132 Mon Sep 17 00:00:00 2001 From: Robert Sammelson Date: Sun, 21 May 2023 01:38:30 -0400 Subject: [PATCH] Add the remaining service $00 PIDs up to $20 Also restructure the code to implement get_dtcs and get_dtc_info by implementing GetObd2Values on the data types they return. This brings them in line with every other getter function besides get_vin. --- examples/basic/main.rs | 19 +++- src/commands/implementation.rs | 173 +++++++++++++++++++----------- src/commands/macros.rs | 23 +++- src/commands/mod.rs | 188 ++++++++++++++++++++++++++++++--- src/commands/types.rs | 25 +++++ 5 files changed, 344 insertions(+), 84 deletions(-) diff --git a/examples/basic/main.rs b/examples/basic/main.rs index 291eb54..f396bb2 100644 --- a/examples/basic/main.rs +++ b/examples/basic/main.rs @@ -7,6 +7,13 @@ fn main() { let mut device: obd2::Obd2 = obd2::Obd2::default(); println!("VIN: {:?}", device.get_vin()); + for s in device.get_service_1_pid_support_1().unwrap().iter() { + println!("PID support ($01-$20): {:08X}", s); + } + for s in device.get_service_1_pid_support_2().unwrap().iter() { + println!("PID support ($21-$40): {:08X}", s); + } + println!("DTC Info: {:#?}", device.get_dtc_info()); let dtcs = device.get_dtcs(); @@ -28,6 +35,16 @@ fn main() { device.get_engine_coolant_temperature() ); println!("RPM: {:?}", device.get_rpm()); - println!("Speed: {:?}", device.get_speed()); + println!("Speed (km/h): {:?}", device.get_speed()); + println!("Timing Advance (º): {:?}", device.get_timing_advance()); + println!( + "Intake air temp (ºC): {:?}", + device.get_intake_air_temperature() + ); + println!("Air flow rate (g/s): {:?}", device.get_air_flow_rate()); + println!( + "Throttle position (%): {:?}", + device.get_throttle_position() + ); } } diff --git a/src/commands/implementation.rs b/src/commands/implementation.rs index 795bd77..1c240d5 100644 --- a/src/commands/implementation.rs +++ b/src/commands/implementation.rs @@ -1,8 +1,14 @@ use crate::{Error, Obd2Device, Result}; -use super::{Dtc, DtcsInfo}; +use super::{Dtc, DtcsInfo, OxygenSensorData}; -pub(crate) trait GetObd2Values +pub(super) fn get_vin(device: &mut T) -> Result { + let mut result = device.obd_command(0x09, 0x02)?.pop().unwrap(); + result.remove(0); // do not know what this byte is + Ok(String::from_utf8(result)?) +} + +pub(super) trait GetObd2Values where Self: Sized, { @@ -19,79 +25,120 @@ impl GetObd2Values for u8 { } } +impl GetObd2Values for [u8; N] { + fn get_obd2_val(device: &mut T, service: u8, pid: u8) -> Result> { + device.obd_command_len::(service, pid) + } +} + impl GetObd2Values for u16 { fn get_obd2_val(device: &mut T, service: u8, pid: u8) -> Result> { - Ok(device - .obd_command_len::<2>(service, pid)? + Ok(<[u8; 2]>::get_obd2_val(device, service, pid)? .into_iter() - .map(u16::from_be_bytes) + .map(Self::from_be_bytes) .collect()) } } -pub(super) fn get_vin(device: &mut T) -> Result { - let mut result = device.obd_command(0x09, 0x02)?.pop().unwrap(); - result.remove(0); // do not know what this byte is - Ok(String::from_utf8(result)?) +impl GetObd2Values for u32 { + fn get_obd2_val(device: &mut T, service: u8, pid: u8) -> Result> { + Ok(<[u8; 4]>::get_obd2_val(device, service, pid)? + .into_iter() + .map(Self::from_be_bytes) + .collect()) + } } -pub(super) fn get_dtc_info(device: &mut T) -> Result> { - let result = device.obd_command(0x01, 0x01)?; +impl GetObd2Values for DtcsInfo { + fn get_obd2_val(device: &mut T, service: u8, pid: u8) -> Result> { + let result = device.obd_command(service, pid)?; - 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() -} - -pub(super) fn get_dtcs(device: &mut T) -> Result>> { - let result = device.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) + 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!( - "invalid response when getting DTCs {:?}", - response + "get_dtc_info: expected length 4, got {}", + response.len() ))) } - } - 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::>>>() + }) + .collect() + } +} + +impl GetObd2Values for Dtc { + fn get_obd2_val(device: &mut T, service: u8, pid: u8) -> Result> { + let resp = u16::get_obd2_val(device, service, pid)?; + Ok(resp.into_iter().map(|v| v.into()).collect()) + } +} + +impl GetObd2Values for OxygenSensorData { + fn get_obd2_val(device: &mut T, service: u8, pid: u8) -> Result> { + let resp = <[u8; 2]>::get_obd2_val(device, service, pid)?; + Ok(resp + .into_iter() + .map(|v| OxygenSensorData { + voltage: f32::from(v[0]) * 0.005, + shrft: f32::from(i16::from(v[1]) - 128) / 128., + }) + .collect()) + } +} + +pub(crate) trait GetObd2ValuesMode +where + Self: Sized, +{ + fn get_obd2_val_mode(device: &mut T, service: u8) -> Result>; +} + +impl GetObd2ValuesMode for Vec { + fn get_obd2_val_mode(device: &mut T, service: u8) -> Result> { + let result = device.obd_mode_command(service)?; + 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::>>>() + } } diff --git a/src/commands/macros.rs b/src/commands/macros.rs index c0ccaa4..7b85455 100644 --- a/src/commands/macros.rs +++ b/src/commands/macros.rs @@ -1,4 +1,11 @@ macro_rules! trait_func { + { + $(#[$attr:meta])* + fn $name:ident($service:expr) -> $retrieve_type:ty; + } => { + $(#[$attr])* + fn $name(&mut self) -> Result>; + }; { $(#[$attr:meta])* fn $name:ident($service:expr, $pid:expr) -> $retrieve_type:ty; @@ -23,6 +30,14 @@ macro_rules! trait_func { } macro_rules! impl_func { + { + $(#[$attr:meta])* + fn $name:ident($service:expr) -> $retrieve_type:ty; + } => { + fn $name(&mut self) -> Result> { + <$retrieve_type>::get_obd2_val_mode(self, $service) + } + }; { $(#[$attr:meta])* fn $name:ident($service:expr, $pid:expr) -> $retrieve_type:ty; @@ -69,7 +84,7 @@ macro_rules! func { $( $(#[$attr_inner:meta])* - fn $name:ident$(<$retrieve_type:ty>)?($service:expr, $pid:expr$(, $map:expr)?) -> $output:ty; + fn $name:ident$(<$retrieve_type:ty>)?($service:expr$(, $pid:expr$(, $map:expr)?)?) -> $output:ty; )* } => { $(#[$attr])* @@ -85,10 +100,10 @@ macro_rules! func { /// #[doc=concat!( "Details: service ", $service, - ", PID ", $pid, + $(", PID ", $pid,)? ", read type: `", decode_type!($output $(, $retrieve_type)?), "`" )] - fn $name$(<$retrieve_type>)?($service, $pid$(, $map)?) -> $output; + fn $name$(<$retrieve_type>)?($service$(, $pid$(, $map)?)?) -> $output; } )* } @@ -102,7 +117,7 @@ macro_rules! func { $( impl_func! { $(#[$attr_inner])* - fn $name$(<$retrieve_type>)?($service, $pid$(, $map)?) -> $output; + fn $name$(<$retrieve_type>)?($service$(, $pid$(, $map)?)?) -> $output; } )* } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 20ae0dd..b152b6f 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -5,21 +5,22 @@ //! Wikipedia](https://en.wikipedia.org/wiki/OBD-II_PIDs). This module mostly uses service 1. mod implementation; -use implementation::GetObd2Values; +use implementation::{GetObd2Values, GetObd2ValuesMode}; #[macro_use] mod macros; mod types; use types::private; -pub use types::{Dtc, DtcsInfo}; +pub use types::{Dtc, DtcsInfo, OxygenSensorData}; use crate::{Obd2Device, Result}; func! { /// Trait for devices that can retrieve data over OBD-II /// - /// Automatically implemented for implementors of [Obd2Device](crate::Obd2Device). + /// Automatically implemented for implementors of [odb2::Obd2Device](crate::Obd2Device), and + /// currently cannot be otherwise implemented. trait Obd2DataRetrieval; { @@ -30,25 +31,61 @@ func! { fn get_vin(self, 0x09, 0x02) -> Result { implementation::get_vin(self) } - - /// Get DTCs for each ECU - fn get_dtcs(self, 0x03) -> Result>> { - implementation::get_dtcs(self) - } - - /// Get DTC (diagnostic trouble code) metadata for each ECU - fn get_dtc_info(self, 0x01, 0x01) -> Result> { - implementation::get_dtc_info(self) - } - } + /// Get DTCs for each ECU + fn get_dtcs(0x03) -> Vec; + + /// Get service 1 PID support for $01 to $20 + fn get_service_1_pid_support_1(0x01, 0x00) -> u32; + + /// Get DTC (diagnostic trouble code) metadata for each ECU + fn get_dtc_info(0x01, 0x01) -> DtcsInfo; + + /// Get DTC that caused the current freeze frame + fn get_freeze_frame_dtc(0x01, 0x02) -> Dtc; + + /// Get fuel system status (system A and B) + /// + /// The first value describes the first fuel system. If there is a secondary fuel system, the + /// second value can be nonzero. Each value can represent one bank using the bottom four bits, + /// or two banks using either the bottom five bits or the whole byte, depending on the + /// configuration of the banks. + /// + /// Valid values for single bank systems: + /// - `0`: engine off (including temporarily for vehicles that turn of the engine at idle) + /// - `1`: open loop — conditions to go closed loop not yet met + /// - `2`: closed loop + /// - `4`: open loop — due to current conditions + /// - `8`: open loop — due to fault + fn get_fuel_system_status(0x01, 0x03) -> [u8; 2]; + /// Get the calculated engine load (out of 255) fn get_engine_load(0x01, 0x04) -> u8; /// Get the temperature of the engine's coolant in ºC fn get_engine_coolant_temperature(0x01, 0x05, |v: i16| v - 40) -> i16; + /// Get the short term fuel trim for bank 1 + /// + /// This is for vehicles with closed loop air/fuel ratio control. It ranges from about -1 to 1, + /// where negative percentages mean the mix is being made more lean. If the fuel system is in + /// open-loop control, this will read 0. + fn get_short_term_fuel_trim_1(0x01, 0x06, |v: f32| (v / 128.) - 1.) -> f32; + + /// Get the long term fuel trim for bank 1 + /// + /// This is for vehicles with closed loop air/fuel ratio control. It ranges from about -1 to 1, + /// where negative percentages mean the mix is being made more lean. This long term trim value + /// represents a value saved between shutdowns of the engine. In open-loop control, if this + /// value is not used it will read 0. + fn get_long_term_fuel_trim_1(0x01, 0x07, |v: f32| (v / 128.) - 1.) -> f32; + + /// Like [get_short_term_fuel_trim_1](Self::get_short_term_fuel_trim_1) but for bank 2 + fn get_short_term_fuel_trim_2(0x01, 0x08, |v: f32| (v / 128.) - 1.) -> f32; + /// Like [get_long_term_fuel_trim_1](Self::get_long_term_fuel_trim_1) but for bank 2 + fn get_long_term_fuel_trim_2(0x01, 0x09, |v: f32| (v / 128.) - 1.) -> f32; + /// Get the fuel pressure in kPa /// /// This measurement is gauge pressure (measured relative to the atmosphere) @@ -59,9 +96,128 @@ func! { /// This measurement is absolute pressure. fn get_engine_manifold_pressure(0x01, 0x0B, |v: f32| v) -> f32; - /// Get the RPM in increments of 0.25 + /// Get the RPM of the engine in increments of 0.25 fn get_rpm(0x01, 0x0C, |v: f32| v / 4.0) -> f32; - /// Get the speed in km/h + /// Get the speed of the vehicle in km/h fn get_speed(0x01, 0x0D) -> u8; + + /// Get the timing advance in degrees BTDC + /// + /// Higher numbers mean the ignition happens earlier; that is, longer before the piston reaches + /// the top of the cylinder. + fn get_timing_advance(0x01, 0x0E, |v: f32| (v - 128.0) / 2.0) -> f32; + + /// Get intake manifold air temperature in ºC + fn get_intake_air_temperature(0x01, 0x0F, |v: i16| v - 40) -> i16; + + /// Get air flow rate in g/s + fn get_air_flow_rate(0x01, 0x10, |v: f32| v * 0.01) -> f32; // TODO: scaling + + /// Get absolute throttle position (out of 255) + /// + /// This is the raw sensor value, so idle throttle will probably be more than 0 and open + /// throttle will probably be less than 255. + fn get_throttle_position(0x01, 0x11) -> u8; + + /// Get commanded secondary air status (bitfield) + /// + /// This describes where the secondary air system has been commanded to inject air. The valid + /// values are: + /// - `1`: Upstream of the first catalytic converter inlet + /// - `2`: Downstream of the first catalytic converter inlet + /// - `4`: Off (or atmosphere) + /// - `8`: On for diagnostics + /// + /// This system exists to reduce emissions. By injecting air in front of the catalytic + /// converter, extra fuel in the exhaust combusts, heating the catalytic converter. Once the + /// catalytic converter is up to temperature, air is injected into the catalytic converter to + /// help it catalyze unburned fuel. + /// + /// See: + fn get_commanded_secondary_air_status(0x01, 0x12) -> u8; + + /// Get location of oxygen sensors + /// + /// This version (cf. [get_oxygen_sensors_4_bank](Self::get_oxygen_sensors_4_bank)) is + /// recommended for two bank systems. A vehicle must not support both variants. + /// + /// The each nibble represents the sensors of one bank, the less significant nibble is bank 1. + /// The bits of the nibble represent each of the four possible sensors, with sensor 1 in the + /// least significant bit. + fn get_oxygen_sensors_2_bank(0x01, 0x13) -> u8; + + /// Get oxygen sensor 1 voltage and associated air/fuel short term trim + /// + /// This is bank 1, sensor 1. + fn get_oxygen_sensor_1(0x01, 0x14) -> OxygenSensorData; + + /// Get oxygen sensor 2 voltage and associated air/fuel short term trim + /// + /// This is for bank 1, sensor 2. + fn get_oxygen_sensor_2(0x01, 0x15) -> OxygenSensorData; + + /// Get oxygen sensor 3 voltage and associated air/fuel short term trim + /// + /// If using two banks, this is for bank 1, sensor 3. If using four banks, this is for bank 2 + /// sensor 1. + fn get_oxygen_sensor_3(0x01, 0x16) -> OxygenSensorData; + + /// Get oxygen sensor 4 voltage and associated air/fuel short term trim + /// + /// If using two banks, this is for bank 1, sensor 4. If using four banks, this is for bank 2 + /// sensor 2. + fn get_oxygen_sensor_4(0x01, 0x17) -> OxygenSensorData; + + /// Get oxygen sensor 5 voltage and associated air/fuel short term trim + /// + /// If using two banks, this is for bank 2, sensor 1. If using four banks, this is for bank 3 + /// sensor 1. + fn get_oxygen_sensor_5(0x01, 0x18) -> OxygenSensorData; + + /// Get oxygen sensor 6 voltage and associated air/fuel short term trim + /// + /// If using two banks, this is for bank 2, sensor 2. If using four banks, this is for bank 3 + /// sensor 2. + fn get_oxygen_sensor_6(0x01, 0x19) -> OxygenSensorData; + + /// Get oxygen sensor 7 voltage and associated air/fuel short term trim + /// + /// If using two banks, this is for bank 2, sensor 3. If using four banks, this is for bank 4 + /// sensor 1. + fn get_oxygen_sensor_7(0x01, 0x1A) -> OxygenSensorData; + + /// Get oxygen sensor 8 voltage and associated air/fuel short term trim + /// + /// If using two banks, this is for bank 2, sensor 4. If using four banks, this is for bank 4 + /// sensor 2. + fn get_oxygen_sensor_8(0x01, 0x1B) -> OxygenSensorData; + + /// Get which OBD standard this vehicle is designed to support + fn get_obd_requirements(0x01, 0x1C) -> u8; + + /// Get location of oxygen sensors + /// + /// This version (cf. [get_oxygen_sensors_2_bank](Self::get_oxygen_sensors_2_bank)) is + /// recommended for four bank systems. A vehicle must not support both variants. + /// + /// The each pair of bits represents the sensors of one bank, the least significant pair is + /// bank 1. The bits of the pair represent each of the two possible sensors, with sensor 1 in + /// the less significant bit. + fn get_oxygen_sensors_4_bank(0x01, 0x1D) -> u8; + + /// Get auxiliary input status + /// + /// The least significant bit indicates whether [power + /// take-off](https://en.wikipedia.org/wiki/Power_Take_Off) is active. + fn get_auxiliary_input_status(0x01, 0x1E) -> u8; + + /// Get the amount of time since the engine was started in seconds + /// + /// This should saturate—not roll over—after the engine has been running for [u16::MAX] seconds + /// (≈18.2 hours). + fn get_run_time(0x01, 0x1F) -> u16; + + /// Get service 1 PID support for $21 to $40 + fn get_service_1_pid_support_2(0x01, 0x20) -> u32; } diff --git a/src/commands/types.rs b/src/commands/types.rs index 3ca634a..8ee784d 100644 --- a/src/commands/types.rs +++ b/src/commands/types.rs @@ -34,6 +34,19 @@ pub enum Dtc { Network(u16), } +impl From for Dtc { + fn from(val: u16) -> Self { + let n = val & 0x3f; + match val >> 14 { + 0 => Dtc::Powertrain(n), + 1 => Dtc::Chassis(n), + 2 => Dtc::Body(n), + 3 => Dtc::Network(n), + _ => unreachable!(), + } + } +} + impl fmt::Display for Dtc { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let (c, n) = match self { @@ -46,6 +59,18 @@ impl fmt::Display for Dtc { } } +/// Data retreived when reading an oxygen sensor +pub struct OxygenSensorData { + /// The current voltage reading (V) + pub voltage: f32, + + /// The current associated short term fuel trim (%) + /// + /// The range of this value is approximately -1 to 1. This will be `127./128.` if not + /// applicable for the sensor. + pub shrft: f32, +} + pub(super) mod private { pub trait Sealed {} impl Sealed for T {}