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.
This commit is contained in:
Robert Sammelson 2023-05-21 01:38:30 -04:00
parent 5b20ac0ab9
commit 13b9041511
No known key found for this signature in database
GPG key ID: 92F1F04EDB06B9E9
5 changed files with 344 additions and 84 deletions

View file

@ -7,6 +7,13 @@ fn main() {
let mut device: obd2::Obd2<obd2::device::Elm327> = 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()
);
}
}

View file

@ -1,8 +1,14 @@
use crate::{Error, Obd2Device, Result};
use super::{Dtc, DtcsInfo};
use super::{Dtc, DtcsInfo, OxygenSensorData};
pub(crate) trait GetObd2Values<T>
pub(super) fn get_vin<T: Obd2Device>(device: &mut T) -> Result<String> {
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<T>
where
Self: Sized,
{
@ -19,79 +25,120 @@ impl<T: Obd2Device> GetObd2Values<T> for u8 {
}
}
impl<T: Obd2Device, const N: usize> GetObd2Values<T> for [u8; N] {
fn get_obd2_val(device: &mut T, service: u8, pid: u8) -> Result<Vec<Self>> {
device.obd_command_len::<N>(service, pid)
}
}
impl<T: Obd2Device> GetObd2Values<T> for u16 {
fn get_obd2_val(device: &mut T, service: u8, pid: u8) -> Result<Vec<Self>> {
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<T: Obd2Device>(device: &mut T) -> Result<String> {
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<T: Obd2Device> GetObd2Values<T> for u32 {
fn get_obd2_val(device: &mut T, service: u8, pid: u8) -> Result<Vec<Self>> {
Ok(<[u8; 4]>::get_obd2_val(device, service, pid)?
.into_iter()
.map(Self::from_be_bytes)
.collect())
}
}
pub(super) fn get_dtc_info<T: Obd2Device>(device: &mut T) -> Result<Vec<DtcsInfo>> {
let result = device.obd_command(0x01, 0x01)?;
impl<T: Obd2Device> GetObd2Values<T> for DtcsInfo {
fn get_obd2_val(device: &mut T, service: u8, pid: u8) -> Result<Vec<Self>> {
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<T: Obd2Device>(device: &mut T) -> Result<Vec<Vec<Dtc>>> {
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::<Result<Vec<Vec<Dtc>>>>()
})
.collect()
}
}
impl<T: Obd2Device> GetObd2Values<T> for Dtc {
fn get_obd2_val(device: &mut T, service: u8, pid: u8) -> Result<Vec<Self>> {
let resp = u16::get_obd2_val(device, service, pid)?;
Ok(resp.into_iter().map(|v| v.into()).collect())
}
}
impl<T: Obd2Device> GetObd2Values<T> for OxygenSensorData {
fn get_obd2_val(device: &mut T, service: u8, pid: u8) -> Result<Vec<Self>> {
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<T>
where
Self: Sized,
{
fn get_obd2_val_mode(device: &mut T, service: u8) -> Result<Vec<Self>>;
}
impl<T: Obd2Device> GetObd2ValuesMode<T> for Vec<Dtc> {
fn get_obd2_val_mode(device: &mut T, service: u8) -> Result<Vec<Self>> {
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::<Result<Vec<Vec<Dtc>>>>()
}
}

View file

@ -1,4 +1,11 @@
macro_rules! trait_func {
{
$(#[$attr:meta])*
fn $name:ident($service:expr) -> $retrieve_type:ty;
} => {
$(#[$attr])*
fn $name(&mut self) -> Result<Vec<$retrieve_type>>;
};
{
$(#[$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<Vec<$retrieve_type>> {
<$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;
}
)*
}

View file

@ -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<String> {
implementation::get_vin(self)
}
/// Get DTCs for each ECU
fn get_dtcs(self, 0x03) -> Result<Vec<Vec<Dtc>>> {
implementation::get_dtcs(self)
}
/// Get DTC (diagnostic trouble code) metadata for each ECU
fn get_dtc_info(self, 0x01, 0x01) -> Result<Vec<DtcsInfo>> {
implementation::get_dtc_info(self)
}
}
/// Get DTCs for each ECU
fn get_dtcs(0x03) -> Vec<Dtc>;
/// 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<u8>(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<u8>(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<u8>(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<u8>(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<u8>(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<u16>(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<u16>(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<u8>(0x01, 0x0E, |v: f32| (v - 128.0) / 2.0) -> f32;
/// Get intake manifold air temperature in ºC
fn get_intake_air_temperature<u8>(0x01, 0x0F, |v: i16| v - 40) -> i16;
/// Get air flow rate in g/s
fn get_air_flow_rate<u16>(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: <https://en.wikipedia.org/wiki/Secondary_air_injection>
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;
}

View file

@ -34,6 +34,19 @@ pub enum Dtc {
Network(u16),
}
impl From<u16> 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<T: crate::Obd2Device> Sealed for T {}