Skip to main content

obd2_core/protocol/
service.rs

1//! Diagnostic service definitions.
2
3/// Diagnostic session type (Mode 10).
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum DiagSession {
6    Default,
7    Programming,
8    Extended,
9}
10
11/// Actuator command for Mode 2F.
12#[derive(Debug, Clone)]
13pub enum ActuatorCommand {
14    ReturnToEcu,
15    Adjust(Vec<u8>),
16    Activate,
17}
18
19/// Readiness monitor status (decoded from Mode 01 PID 01).
20#[derive(Debug, Clone)]
21pub struct ReadinessStatus {
22    pub mil_on: bool,
23    pub dtc_count: u8,
24    pub compression_ignition: bool,
25    pub monitors: Vec<MonitorStatus>,
26}
27
28/// Status of a single readiness monitor.
29#[derive(Debug, Clone)]
30pub struct MonitorStatus {
31    pub name: String,
32    pub supported: bool,
33    pub complete: bool,
34}
35
36/// Mode 06 test result.
37#[derive(Debug, Clone)]
38pub struct TestResult {
39    pub test_id: u8,
40    pub name: String,
41    pub value: f64,
42    pub min: Option<f64>,
43    pub max: Option<f64>,
44    pub passed: bool,
45    pub unit: String,
46}
47
48/// Vehicle identification info (Mode 09).
49#[derive(Debug, Clone)]
50pub struct VehicleInfo {
51    pub vin: String,
52    pub calibration_ids: Vec<String>,
53    pub cvns: Vec<u32>,
54    pub ecu_name: Option<String>,
55}
56
57/// Extended DTC detail (Mode 19 sub-function 06).
58#[derive(Debug, Clone)]
59pub struct DtcDetail {
60    pub code: String,
61    pub occurrence_count: u16,
62    pub aging_counter: u16,
63}
64
65/// Mode 05: O2 sensor monitoring test result (non-CAN only).
66#[derive(Debug, Clone)]
67pub struct O2TestResult {
68    pub test_id: u8,
69    pub test_name: &'static str,
70    pub sensor: O2SensorLocation,
71    pub value: f64,
72    pub unit: &'static str,
73}
74
75/// O2 sensor location encoding for Mode 05.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum O2SensorLocation {
78    Bank1Sensor1,
79    Bank1Sensor2,
80    Bank2Sensor1,
81    Bank2Sensor2,
82    Bank3Sensor1,
83    Bank3Sensor2,
84    Bank4Sensor1,
85    Bank4Sensor2,
86}
87
88impl O2SensorLocation {
89    /// Decode from the Mode 05 sensor number byte.
90    pub fn from_byte(b: u8) -> Option<Self> {
91        match b {
92            0x01 => Some(Self::Bank1Sensor1),
93            0x02 => Some(Self::Bank1Sensor2),
94            0x03 => Some(Self::Bank2Sensor1),
95            0x04 => Some(Self::Bank2Sensor2),
96            0x05 => Some(Self::Bank3Sensor1),
97            0x06 => Some(Self::Bank3Sensor2),
98            0x07 => Some(Self::Bank4Sensor1),
99            0x08 => Some(Self::Bank4Sensor2),
100            _ => None,
101        }
102    }
103}
104
105impl std::fmt::Display for O2SensorLocation {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        match self {
108            Self::Bank1Sensor1 => write!(f, "B1S1"),
109            Self::Bank1Sensor2 => write!(f, "B1S2"),
110            Self::Bank2Sensor1 => write!(f, "B2S1"),
111            Self::Bank2Sensor2 => write!(f, "B2S2"),
112            Self::Bank3Sensor1 => write!(f, "B3S1"),
113            Self::Bank3Sensor2 => write!(f, "B3S2"),
114            Self::Bank4Sensor1 => write!(f, "B4S1"),
115            Self::Bank4Sensor2 => write!(f, "B4S2"),
116        }
117    }
118}
119
120/// Return the test name and unit for a Mode 05 TID.
121pub fn o2_test_info(tid: u8) -> (&'static str, &'static str, fn(u16) -> f64) {
122    match tid {
123        0x01 => ("Rich-to-Lean Threshold Voltage", "V", |v| v as f64 * 0.005),
124        0x02 => ("Lean-to-Rich Threshold Voltage", "V", |v| v as f64 * 0.005),
125        0x03 => ("Low Sensor Voltage for Switch Time", "V", |v| v as f64 * 0.005),
126        0x04 => ("High Sensor Voltage for Switch Time", "V", |v| v as f64 * 0.005),
127        0x05 => ("Rich-to-Lean Switch Time", "s", |v| v as f64 * 0.004),
128        0x06 => ("Lean-to-Rich Switch Time", "s", |v| v as f64 * 0.004),
129        0x07 => ("Minimum Sensor Voltage", "V", |v| v as f64 * 0.005),
130        0x08 => ("Maximum Sensor Voltage", "V", |v| v as f64 * 0.005),
131        0x09 => ("Time Between Transitions", "s", |v| v as f64 * 0.04),
132        _ => ("Unknown O2 Test", "", |v| v as f64),
133    }
134}
135
136/// A raw diagnostic service request.
137#[derive(Debug, Clone)]
138pub struct ServiceRequest {
139    pub service_id: u8,
140    pub data: Vec<u8>,
141    pub target: Target,
142}
143
144/// Request targeting.
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub enum Target {
147    Broadcast,
148    Module(String),
149}
150
151impl ServiceRequest {
152    /// Create a Mode 01 read PID request.
153    pub fn read_pid(pid: super::pid::Pid) -> Self {
154        Self {
155            service_id: 0x01,
156            data: vec![pid.0],
157            target: Target::Broadcast,
158        }
159    }
160
161    /// Create a Mode 09 read VIN request.
162    pub fn read_vin() -> Self {
163        Self {
164            service_id: 0x09,
165            data: vec![0x02],
166            target: Target::Broadcast,
167        }
168    }
169
170    /// Create a Mode 03 read stored DTCs request.
171    pub fn read_dtcs() -> Self {
172        Self {
173            service_id: 0x03,
174            data: vec![],
175            target: Target::Broadcast,
176        }
177    }
178
179    /// Create a Mode 22 enhanced PID read.
180    pub fn enhanced_read(service_id: u8, did: u16, target: Target) -> Self {
181        Self {
182            service_id,
183            data: vec![(did >> 8) as u8, (did & 0xFF) as u8],
184            target,
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use crate::protocol::pid::Pid;
193
194    #[test]
195    fn test_service_request_read_pid() {
196        let req = ServiceRequest::read_pid(Pid::ENGINE_RPM);
197        assert_eq!(req.service_id, 0x01);
198        assert_eq!(req.data, vec![0x0C]);
199        assert_eq!(req.target, Target::Broadcast);
200    }
201
202    #[test]
203    fn test_service_request_read_vin() {
204        let req = ServiceRequest::read_vin();
205        assert_eq!(req.service_id, 0x09);
206        assert_eq!(req.data, vec![0x02]);
207    }
208
209    #[test]
210    fn test_service_request_enhanced_read() {
211        let req = ServiceRequest::enhanced_read(0x22, 0x162F, Target::Module("ecm".into()));
212        assert_eq!(req.service_id, 0x22);
213        assert_eq!(req.data, vec![0x16, 0x2F]);
214    }
215
216    #[test]
217    fn test_service_request_read_dtcs() {
218        let req = ServiceRequest::read_dtcs();
219        assert_eq!(req.service_id, 0x03);
220        assert!(req.data.is_empty());
221    }
222
223    #[test]
224    fn test_o2_sensor_location_roundtrip() {
225        for b in 0x01..=0x08u8 {
226            assert!(O2SensorLocation::from_byte(b).is_some());
227        }
228    }
229
230    #[test]
231    fn test_o2_test_info_all_standard_tids() {
232        for tid in 0x01..=0x09u8 {
233            let (name, unit, _) = o2_test_info(tid);
234            assert!(!name.contains("Unknown"), "TID {:#04X} should be known", tid);
235            assert!(!unit.is_empty(), "TID {:#04X} should have a unit", tid);
236        }
237    }
238
239    #[test]
240    fn test_o2_test_info_unknown_tid() {
241        let (name, _, _) = o2_test_info(0xFF);
242        assert!(name.contains("Unknown"));
243    }
244}