Skip to main content

obd2_core/protocol/
pid.rs

1//! Standard OBD-II PID definitions.
2
3use super::enhanced::{Value, Bitfield};
4use crate::error::Obd2Error;
5
6/// The type of value a PID returns.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
8pub enum ValueType {
9    /// Numeric measurement (temperature, pressure, RPM, %)
10    Scalar,
11    /// Bitfield with named flags (readiness monitors, solenoid state)
12    Bitfield,
13    /// Enumerated state (gear position, key position)
14    State,
15}
16
17/// Standard OBD-II PID (Mode 01/02). Newtype over u8 for type safety.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct Pid(pub u8);
20
21impl Pid {
22    // Status & readiness
23    pub const SUPPORTED_PIDS_01_20: Pid = Pid(0x00);
24    pub const MONITOR_STATUS: Pid = Pid(0x01);
25    pub const FUEL_SYSTEM_STATUS: Pid = Pid(0x03);
26
27    // Engine performance
28    pub const ENGINE_LOAD: Pid = Pid(0x04);
29    pub const COOLANT_TEMP: Pid = Pid(0x05);
30    pub const SHORT_FUEL_TRIM_B1: Pid = Pid(0x06);
31    pub const LONG_FUEL_TRIM_B1: Pid = Pid(0x07);
32    pub const SHORT_FUEL_TRIM_B2: Pid = Pid(0x08);
33    pub const LONG_FUEL_TRIM_B2: Pid = Pid(0x09);
34    pub const FUEL_PRESSURE: Pid = Pid(0x0A);
35    pub const INTAKE_MAP: Pid = Pid(0x0B);
36    pub const ENGINE_RPM: Pid = Pid(0x0C);
37    pub const VEHICLE_SPEED: Pid = Pid(0x0D);
38    pub const TIMING_ADVANCE: Pid = Pid(0x0E);
39    pub const INTAKE_AIR_TEMP: Pid = Pid(0x0F);
40    pub const MAF: Pid = Pid(0x10);
41    pub const THROTTLE_POSITION: Pid = Pid(0x11);
42
43    // OBD standards
44    pub const OBD_STANDARD: Pid = Pid(0x1C);
45    pub const RUN_TIME: Pid = Pid(0x1F);
46
47    // Supported PIDs bitmaps
48    pub const SUPPORTED_PIDS_21_40: Pid = Pid(0x20);
49    pub const DISTANCE_WITH_MIL: Pid = Pid(0x21);
50    pub const FUEL_RAIL_GAUGE_PRESSURE: Pid = Pid(0x23);
51    pub const COMMANDED_EGR: Pid = Pid(0x2C);
52    pub const EGR_ERROR: Pid = Pid(0x2D);
53    pub const COMMANDED_EVAP_PURGE: Pid = Pid(0x2E);
54    pub const FUEL_TANK_LEVEL: Pid = Pid(0x2F);
55    pub const WARMUPS_SINCE_CLEAR: Pid = Pid(0x30);
56    pub const DISTANCE_SINCE_CLEAR: Pid = Pid(0x31);
57    pub const BAROMETRIC_PRESSURE: Pid = Pid(0x33);
58
59    // Catalysts
60    pub const CATALYST_TEMP_B1S1: Pid = Pid(0x3C);
61    pub const CATALYST_TEMP_B2S1: Pid = Pid(0x3D);
62    pub const CATALYST_TEMP_B1S2: Pid = Pid(0x3E);
63    pub const CATALYST_TEMP_B2S2: Pid = Pid(0x3F);
64
65    // Supported PIDs bitmap
66    pub const SUPPORTED_PIDS_41_60: Pid = Pid(0x40);
67    pub const CONTROL_MODULE_VOLTAGE: Pid = Pid(0x42);
68    pub const ABSOLUTE_LOAD: Pid = Pid(0x43);
69    pub const COMMANDED_EQUIV_RATIO: Pid = Pid(0x44);
70    pub const RELATIVE_THROTTLE_POS: Pid = Pid(0x45);
71    pub const AMBIENT_AIR_TEMP: Pid = Pid(0x46);
72    pub const ABS_THROTTLE_POS_B: Pid = Pid(0x47);
73    pub const ACCEL_PEDAL_POS_D: Pid = Pid(0x49);
74    pub const ACCEL_PEDAL_POS_E: Pid = Pid(0x4A);
75    pub const COMMANDED_THROTTLE_ACTUATOR: Pid = Pid(0x4C);
76
77    // Engine oil and fuel
78    pub const ENGINE_OIL_TEMP: Pid = Pid(0x5C);
79    pub const ENGINE_FUEL_RATE: Pid = Pid(0x5E);
80    pub const FUEL_RAIL_ABS_PRESSURE: Pid = Pid(0x59);
81
82    // Supported PIDs bitmap
83    pub const SUPPORTED_PIDS_61_80: Pid = Pid(0x60);
84    pub const DEMANDED_TORQUE: Pid = Pid(0x61);
85    pub const ACTUAL_TORQUE: Pid = Pid(0x62);
86    pub const REFERENCE_TORQUE: Pid = Pid(0x63);
87
88    /// Human-readable name for this PID.
89    pub fn name(&self) -> &'static str {
90        match self.0 {
91            0x00 => "Supported PIDs [01-20]",
92            0x01 => "Monitor Status",
93            0x03 => "Fuel System Status",
94            0x04 => "Engine Load",
95            0x05 => "Coolant Temperature",
96            0x06 => "Short Term Fuel Trim (Bank 1)",
97            0x07 => "Long Term Fuel Trim (Bank 1)",
98            0x08 => "Short Term Fuel Trim (Bank 2)",
99            0x09 => "Long Term Fuel Trim (Bank 2)",
100            0x0A => "Fuel Pressure",
101            0x0B => "Intake MAP",
102            0x0C => "Engine RPM",
103            0x0D => "Vehicle Speed",
104            0x0E => "Timing Advance",
105            0x0F => "Intake Air Temperature",
106            0x10 => "MAF Air Flow Rate",
107            0x11 => "Throttle Position",
108            0x1C => "OBD Standard",
109            0x1F => "Run Time Since Start",
110            0x20 => "Supported PIDs [21-40]",
111            0x21 => "Distance with MIL On",
112            0x23 => "Fuel Rail Gauge Pressure",
113            0x2C => "Commanded EGR",
114            0x2D => "EGR Error",
115            0x2E => "Commanded Evaporative Purge",
116            0x2F => "Fuel Tank Level",
117            0x30 => "Warm-ups Since Clear",
118            0x31 => "Distance Since DTC Clear",
119            0x33 => "Barometric Pressure",
120            0x3C => "Catalyst Temp B1S1",
121            0x3D => "Catalyst Temp B2S1",
122            0x3E => "Catalyst Temp B1S2",
123            0x3F => "Catalyst Temp B2S2",
124            0x40 => "Supported PIDs [41-60]",
125            0x42 => "Control Module Voltage",
126            0x43 => "Absolute Load",
127            0x44 => "Commanded Equivalence Ratio",
128            0x45 => "Relative Throttle Position",
129            0x46 => "Ambient Air Temperature",
130            0x47 => "Absolute Throttle Position B",
131            0x49 => "Accelerator Pedal Position D",
132            0x4A => "Accelerator Pedal Position E",
133            0x4C => "Commanded Throttle Actuator",
134            0x59 => "Fuel Rail Absolute Pressure",
135            0x5C => "Engine Oil Temperature",
136            0x5E => "Engine Fuel Rate",
137            0x60 => "Supported PIDs [61-80]",
138            0x61 => "Demanded Torque",
139            0x62 => "Actual Torque",
140            0x63 => "Reference Torque",
141            _ => "Unknown PID",
142        }
143    }
144
145    /// Measurement unit for this PID.
146    pub fn unit(&self) -> &'static str {
147        match self.0 {
148            0x00 | 0x01 | 0x03 | 0x20 | 0x40 | 0x60 => "bitfield",
149            0x04 | 0x06..=0x09 | 0x11 | 0x2C | 0x2D | 0x2E | 0x2F
150            | 0x43 | 0x45 | 0x47 | 0x49 | 0x4A | 0x4C | 0x61 | 0x62 => "%",
151            0x05 | 0x0F | 0x3C..=0x3F | 0x46 | 0x5C => "\u{00B0}C",
152            0x0A | 0x0B | 0x23 | 0x33 | 0x59 => "kPa",
153            0x0C => "RPM",
154            0x0D => "km/h",
155            0x0E => "\u{00B0}",
156            0x10 => "g/s",
157            0x1F => "s",
158            0x21 | 0x31 => "km",
159            0x30 => "count",
160            0x42 => "V",
161            0x44 => "\u{03BB}",
162            0x5E => "L/h",
163            0x63 => "Nm",
164            _ => "",
165        }
166    }
167
168    /// Number of response data bytes expected for this PID.
169    pub fn response_bytes(&self) -> u8 {
170        match self.0 {
171            0x00 | 0x01 | 0x20 | 0x40 | 0x60 => 4, // bitmaps
172            0x0C | 0x10 | 0x1F | 0x21 | 0x23 | 0x31 | 0x3C..=0x3F
173            | 0x42 | 0x43 | 0x44 | 0x59 | 0x5E | 0x63 => 2,
174            _ => 1, // most single-byte PIDs
175        }
176    }
177
178    /// The type of value this PID returns.
179    pub fn value_type(&self) -> ValueType {
180        match self.0 {
181            0x00 | 0x01 | 0x03 | 0x20 | 0x40 | 0x60 => ValueType::Bitfield,
182            0x1C => ValueType::State,
183            _ => ValueType::Scalar,
184        }
185    }
186
187    /// Returns a slice of all known standard PIDs.
188    pub fn all() -> &'static [Pid] {
189        &[
190            Self::SUPPORTED_PIDS_01_20, Self::MONITOR_STATUS, Self::FUEL_SYSTEM_STATUS,
191            Self::ENGINE_LOAD, Self::COOLANT_TEMP,
192            Self::SHORT_FUEL_TRIM_B1, Self::LONG_FUEL_TRIM_B1,
193            Self::SHORT_FUEL_TRIM_B2, Self::LONG_FUEL_TRIM_B2,
194            Self::FUEL_PRESSURE, Self::INTAKE_MAP, Self::ENGINE_RPM,
195            Self::VEHICLE_SPEED, Self::TIMING_ADVANCE, Self::INTAKE_AIR_TEMP,
196            Self::MAF, Self::THROTTLE_POSITION, Self::OBD_STANDARD, Self::RUN_TIME,
197            Self::SUPPORTED_PIDS_21_40, Self::DISTANCE_WITH_MIL,
198            Self::FUEL_RAIL_GAUGE_PRESSURE, Self::COMMANDED_EGR, Self::EGR_ERROR,
199            Self::COMMANDED_EVAP_PURGE, Self::FUEL_TANK_LEVEL, Self::WARMUPS_SINCE_CLEAR,
200            Self::DISTANCE_SINCE_CLEAR, Self::BAROMETRIC_PRESSURE,
201            Self::CATALYST_TEMP_B1S1, Self::CATALYST_TEMP_B2S1,
202            Self::CATALYST_TEMP_B1S2, Self::CATALYST_TEMP_B2S2,
203            Self::SUPPORTED_PIDS_41_60, Self::CONTROL_MODULE_VOLTAGE,
204            Self::ABSOLUTE_LOAD, Self::COMMANDED_EQUIV_RATIO,
205            Self::RELATIVE_THROTTLE_POS, Self::AMBIENT_AIR_TEMP,
206            Self::ABS_THROTTLE_POS_B, Self::ACCEL_PEDAL_POS_D,
207            Self::ACCEL_PEDAL_POS_E, Self::COMMANDED_THROTTLE_ACTUATOR,
208            Self::FUEL_RAIL_ABS_PRESSURE, Self::ENGINE_OIL_TEMP,
209            Self::ENGINE_FUEL_RATE, Self::SUPPORTED_PIDS_61_80,
210            Self::DEMANDED_TORQUE, Self::ACTUAL_TORQUE, Self::REFERENCE_TORQUE,
211        ]
212    }
213
214    /// Convert a raw byte code to a Pid.
215    pub fn from_code(code: u8) -> Pid {
216        Pid(code)
217    }
218
219    /// Parse raw response bytes into a decoded Value.
220    /// Bytes should NOT include the service ID or PID echo byte —
221    /// just the data bytes (A, B, C, D).
222    pub fn parse(&self, data: &[u8]) -> Result<Value, Obd2Error> {
223        // Check byte count
224        let expected = self.response_bytes() as usize;
225        if data.len() < expected {
226            return Err(Obd2Error::ParseError(format!(
227                "PID {:#04X} expects {} bytes, got {}", self.0, expected, data.len()
228            )));
229        }
230
231        let a = data[0] as f64;
232        let b = if data.len() > 1 { data[1] as f64 } else { 0.0 };
233        let _c = if data.len() > 2 { data[2] as f64 } else { 0.0 };
234        let _d = if data.len() > 3 { data[3] as f64 } else { 0.0 };
235
236        match self.0 {
237            // Bitmaps (4 bytes)
238            0x00 | 0x20 | 0x40 | 0x60 => {
239                let raw = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
240                Ok(Value::Bitfield(Bitfield { raw, flags: vec![] }))
241            }
242
243            // Monitor Status (special bitfield)
244            0x01 => {
245                let raw = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
246                let mil_on = (data[0] & 0x80) != 0;
247                let dtc_count = data[0] & 0x7F;
248                let flags = vec![
249                    ("MIL".into(), mil_on),
250                    (format!("{} DTCs", dtc_count), dtc_count > 0),
251                    ("Compression Ignition".into(), (data[1] & 0x08) != 0),
252                ];
253                Ok(Value::Bitfield(Bitfield { raw, flags }))
254            }
255
256            // Fuel system status
257            0x03 => {
258                let raw = u32::from(data[0]);
259                Ok(Value::Bitfield(Bitfield { raw, flags: vec![] }))
260            }
261
262            // Percentage: A * 100 / 255
263            0x04 | 0x11 | 0x2C | 0x2E | 0x2F | 0x45 | 0x47 | 0x49 | 0x4A | 0x4C => {
264                Ok(Value::Scalar(a * 100.0 / 255.0))
265            }
266
267            // Temperature: A - 40
268            0x05 | 0x0F | 0x46 | 0x5C => {
269                Ok(Value::Scalar(a - 40.0))
270            }
271
272            // Fuel trim: (A - 128) * 100 / 128
273            0x06 | 0x07 | 0x08 | 0x09 | 0x2D => {
274                Ok(Value::Scalar((a - 128.0) * 100.0 / 128.0))
275            }
276
277            // Fuel pressure: A * 3
278            0x0A => Ok(Value::Scalar(a * 3.0)),
279
280            // Intake MAP: A (direct kPa)
281            0x0B | 0x33 => Ok(Value::Scalar(a)),
282
283            // RPM: (256*A + B) / 4
284            0x0C => Ok(Value::Scalar((256.0 * a + b) / 4.0)),
285
286            // Speed: A (direct km/h)
287            0x0D => Ok(Value::Scalar(a)),
288
289            // Timing advance: A/2 - 64
290            0x0E => Ok(Value::Scalar(a / 2.0 - 64.0)),
291
292            // MAF: (256*A + B) / 100
293            0x10 => Ok(Value::Scalar((256.0 * a + b) / 100.0)),
294
295            // OBD Standard (state)
296            0x1C => Ok(Value::State(format!("Standard {}", data[0]))),
297
298            // Run time / distance: 256*A + B (seconds or km)
299            0x1F | 0x21 | 0x31 => Ok(Value::Scalar(256.0 * a + b)),
300
301            // Fuel rail gauge pressure: (256*A + B) * 10
302            0x23 => Ok(Value::Scalar((256.0 * a + b) * 10.0)),
303
304            // Warm-ups since clear: A (direct count)
305            0x30 => Ok(Value::Scalar(a)),
306
307            // Catalyst temps: (256*A + B) / 10 - 40
308            0x3C..=0x3F => {
309                Ok(Value::Scalar((256.0 * a + b) / 10.0 - 40.0))
310            }
311
312            // Control module voltage: (256*A + B) / 1000
313            0x42 => Ok(Value::Scalar((256.0 * a + b) / 1000.0)),
314
315            // Absolute load: (256*A + B) * 100 / 255
316            0x43 => Ok(Value::Scalar((256.0 * a + b) * 100.0 / 255.0)),
317
318            // Commanded equivalence ratio: (256*A + B) / 32768
319            0x44 => Ok(Value::Scalar((256.0 * a + b) / 32768.0)),
320
321            // Fuel rail absolute pressure: (256*A + B) * 10
322            0x59 => Ok(Value::Scalar((256.0 * a + b) * 10.0)),
323
324            // Engine fuel rate: (256*A + B) / 20
325            0x5E => Ok(Value::Scalar((256.0 * a + b) / 20.0)),
326
327            // Torque percentage: A - 125
328            0x61 | 0x62 => Ok(Value::Scalar(a - 125.0)),
329
330            // Reference torque: 256*A + B (Nm)
331            0x63 => Ok(Value::Scalar(256.0 * a + b)),
332
333            _ => Err(Obd2Error::ParseError(format!("no parse formula for PID {:#04X}", self.0))),
334        }
335    }
336}
337
338impl std::fmt::Display for Pid {
339    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340        write!(f, "{} ({:#04X})", self.name(), self.0)
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_pid_constants() {
350        assert_eq!(Pid::ENGINE_RPM.0, 0x0C);
351        assert_eq!(Pid::COOLANT_TEMP.0, 0x05);
352        assert_eq!(Pid::ENGINE_OIL_TEMP.0, 0x5C);
353    }
354
355    #[test]
356    fn test_pid_names() {
357        assert_eq!(Pid::ENGINE_RPM.name(), "Engine RPM");
358        assert_eq!(Pid::COOLANT_TEMP.name(), "Coolant Temperature");
359    }
360
361    #[test]
362    fn test_pid_units() {
363        assert_eq!(Pid::ENGINE_RPM.unit(), "RPM");
364        assert_eq!(Pid::COOLANT_TEMP.unit(), "\u{00B0}C");
365        assert_eq!(Pid::VEHICLE_SPEED.unit(), "km/h");
366    }
367
368    #[test]
369    fn test_pid_response_bytes() {
370        assert_eq!(Pid::ENGINE_RPM.response_bytes(), 2);
371        assert_eq!(Pid::COOLANT_TEMP.response_bytes(), 1);
372        assert_eq!(Pid::MONITOR_STATUS.response_bytes(), 4);
373    }
374
375    #[test]
376    fn test_pid_value_types() {
377        assert_eq!(Pid::ENGINE_RPM.value_type(), ValueType::Scalar);
378        assert_eq!(Pid::MONITOR_STATUS.value_type(), ValueType::Bitfield);
379    }
380
381    #[test]
382    fn test_all_pids_have_names() {
383        for pid in Pid::all() {
384            assert_ne!(pid.name(), "Unknown PID", "PID {:#04x} has no name", pid.0);
385        }
386    }
387
388    #[test]
389    fn test_pid_display() {
390        let s = format!("{}", Pid::ENGINE_RPM);
391        assert!(s.contains("Engine RPM"));
392        assert!(s.contains("0x0C"));
393    }
394
395    #[test]
396    fn test_parse_rpm() {
397        let data = [0x0C, 0x00]; // (0x0C * 256 + 0) / 4 = 768 RPM
398        let val = Pid::ENGINE_RPM.parse(&data).unwrap();
399        assert_eq!(val.as_f64().unwrap(), 768.0);
400    }
401
402    #[test]
403    fn test_parse_rpm_idle() {
404        let data = [0x0A, 0xA0]; // (10 * 256 + 160) / 4 = 680 RPM
405        let val = Pid::ENGINE_RPM.parse(&data).unwrap();
406        assert_eq!(val.as_f64().unwrap(), 680.0);
407    }
408
409    #[test]
410    fn test_parse_speed() {
411        let data = [0x3C]; // 60 km/h
412        let val = Pid::VEHICLE_SPEED.parse(&data).unwrap();
413        assert_eq!(val.as_f64().unwrap(), 60.0);
414    }
415
416    #[test]
417    fn test_parse_coolant_temp() {
418        let data = [0x7E]; // 126 - 40 = 86°C
419        let val = Pid::COOLANT_TEMP.parse(&data).unwrap();
420        assert_eq!(val.as_f64().unwrap(), 86.0);
421    }
422
423    #[test]
424    fn test_parse_coolant_temp_freezing() {
425        let data = [0x00]; // 0 - 40 = -40°C
426        let val = Pid::COOLANT_TEMP.parse(&data).unwrap();
427        assert_eq!(val.as_f64().unwrap(), -40.0);
428    }
429
430    #[test]
431    fn test_parse_fuel_trim_zero() {
432        let data = [0x80]; // (128 - 128) * 100 / 128 = 0%
433        let val = Pid::SHORT_FUEL_TRIM_B1.parse(&data).unwrap();
434        assert!((val.as_f64().unwrap()).abs() < 0.01);
435    }
436
437    #[test]
438    fn test_parse_fuel_trim_rich() {
439        let data = [0x90]; // (144 - 128) * 100 / 128 = 12.5%
440        let val = Pid::SHORT_FUEL_TRIM_B1.parse(&data).unwrap();
441        assert!((val.as_f64().unwrap() - 12.5).abs() < 0.01);
442    }
443
444    #[test]
445    fn test_parse_control_module_voltage() {
446        let data = [0x38, 0x5C]; // 14428 / 1000 = 14.428V
447        let val = Pid(0x42).parse(&data).unwrap();
448        assert!((val.as_f64().unwrap() - 14.428).abs() < 0.001);
449    }
450
451    #[test]
452    fn test_parse_maf() {
453        let data = [0x01, 0xF4]; // (256 + 244) / 100 = 5.00 g/s
454        let val = Pid::MAF.parse(&data).unwrap();
455        assert!((val.as_f64().unwrap() - 5.0).abs() < 0.01);
456    }
457
458    #[test]
459    fn test_parse_catalyst_temp() {
460        let data = [0x11, 0x0E]; // (17*256 + 14) / 10 - 40 = 4366/10 - 40 = 396.6°C
461        let val = Pid::CATALYST_TEMP_B1S1.parse(&data).unwrap();
462        assert!((val.as_f64().unwrap() - 396.6).abs() < 0.1);
463    }
464
465    #[test]
466    fn test_parse_fuel_rate() {
467        let data = [0x00, 0xC8]; // 200 / 20 = 10.0 L/h
468        let val = Pid::ENGINE_FUEL_RATE.parse(&data).unwrap();
469        assert!((val.as_f64().unwrap() - 10.0).abs() < 0.01);
470    }
471
472    #[test]
473    fn test_parse_monitor_status_bitfield() {
474        let data = [0x00, 0x07, 0x65, 0x00];
475        let val = Pid::MONITOR_STATUS.parse(&data).unwrap();
476        let bf = val.as_bitfield().unwrap();
477        assert!(!bf.flags.iter().find(|(n, _)| n == "MIL").unwrap().1); // MIL off
478    }
479
480    #[test]
481    fn test_parse_insufficient_bytes() {
482        let data = [0x0C]; // RPM needs 2 bytes
483        let result = Pid::ENGINE_RPM.parse(&data);
484        assert!(result.is_err());
485    }
486
487    #[test]
488    fn test_parse_timing_advance() {
489        let data = [0x8C]; // 140/2 - 64 = 6°
490        let val = Pid::TIMING_ADVANCE.parse(&data).unwrap();
491        assert!((val.as_f64().unwrap() - 6.0).abs() < 0.01);
492    }
493
494    #[test]
495    fn test_parse_torque_percent() {
496        let data = [0xAF]; // 175 - 125 = 50%
497        let val = Pid::ACTUAL_TORQUE.parse(&data).unwrap();
498        assert_eq!(val.as_f64().unwrap(), 50.0);
499    }
500
501    #[test]
502    fn test_parse_reference_torque() {
503        let data = [0x03, 0x7F]; // 256*3 + 127 = 895 Nm
504        let val = Pid::REFERENCE_TORQUE.parse(&data).unwrap();
505        assert_eq!(val.as_f64().unwrap(), 895.0);
506    }
507}