Skip to main content

obd2_core/adapter/
mock.rs

1//! Mock adapter for testing.
2
3use std::collections::HashSet;
4use async_trait::async_trait;
5use crate::error::Obd2Error;
6use crate::protocol::pid::Pid;
7use crate::protocol::dtc::Dtc;
8use crate::protocol::service::ServiceRequest;
9use crate::vehicle::Protocol;
10use super::{Adapter, AdapterInfo, Chipset, Capabilities};
11
12/// A mock adapter that simulates a vehicle for testing.
13///
14/// Returns realistic values for standard PIDs and configurable DTCs.
15#[derive(Debug)]
16pub struct MockAdapter {
17    info: AdapterInfo,
18    vin: String,
19    dtcs: Vec<Dtc>,
20    initialized: bool,
21    supported: HashSet<Pid>,
22}
23
24impl MockAdapter {
25    /// Create with default settings (generic vehicle).
26    pub fn new() -> Self {
27        Self::with_vin("1GCHK23224F000001") // Duramax VIN by default
28    }
29
30    /// Create with a specific VIN.
31    pub fn with_vin(vin: &str) -> Self {
32        let mut supported = HashSet::new();
33        // Standard PIDs most vehicles support
34        for pid in &[
35            0x00u8, 0x01, 0x03, 0x04, 0x05, 0x06, 0x07, 0x0B, 0x0C, 0x0D,
36            0x0E, 0x0F, 0x10, 0x11, 0x1C, 0x1F, 0x20, 0x21, 0x2C, 0x2F,
37            0x31, 0x33, 0x40, 0x42, 0x43, 0x44, 0x46, 0x5C, 0x5E,
38            0x60, 0x61, 0x62, 0x63,
39        ] {
40            supported.insert(Pid(*pid));
41        }
42
43        Self {
44            info: AdapterInfo {
45                chipset: Chipset::Elm327Genuine,
46                firmware: "MockAdapter v1.0".to_string(),
47                protocol: Protocol::Can11Bit500,
48                capabilities: Capabilities {
49                    can_clear_dtcs: true,
50                    dual_can: false,
51                    enhanced_diag: true,
52                    battery_voltage: true,
53                    adaptive_timing: true,
54                },
55            },
56            vin: vin.to_string(),
57            dtcs: Vec::new(),
58            initialized: false,
59            supported,
60        }
61    }
62
63    /// Set the DTCs that will be returned by Mode 03 queries.
64    pub fn set_dtcs(&mut self, dtcs: Vec<Dtc>) {
65        self.dtcs = dtcs;
66    }
67
68    /// Generate a realistic mock response for a standard PID.
69    fn mock_pid_response(&self, pid: u8) -> Vec<u8> {
70        match pid {
71            0x04 => vec![0x40],                     // Engine load: 25%
72            0x05 => vec![0x5A],                     // Coolant temp: 50 deg C (90-40)
73            0x06 => vec![0x80],                     // Fuel trim: 0%
74            0x07 => vec![0x80],                     // Long fuel trim: 0%
75            0x0B => vec![0x65],                     // MAP: 101 kPa
76            0x0C => vec![0x0A, 0xA0],               // RPM: 680
77            0x0D => vec![0x00],                     // Speed: 0 km/h (idle)
78            0x0E => vec![0x8C],                     // Timing: 6 deg
79            0x0F => vec![0x41],                     // IAT: 25 deg C
80            0x10 => vec![0x00, 0xFA],               // MAF: 2.5 g/s
81            0x11 => vec![0x26],                     // Throttle: 15%
82            0x1C => vec![0x06],                     // OBD standard: EOBD
83            0x1F => vec![0x00, 0x3C],               // Runtime: 60s
84            0x2C => vec![0x1A],                     // Commanded EGR: 10%
85            0x2F => vec![0xB3],                     // Fuel tank: 70%
86            0x33 => vec![0x65],                     // Baro: 101 kPa
87            0x42 => vec![0x38, 0x5C],               // Voltage: 14.428V
88            0x46 => vec![0x41],                     // Ambient: 25 deg C
89            0x5C => vec![0x78],                     // Oil temp: 80 deg C
90            0x5E => vec![0x00, 0x64],               // Fuel rate: 5.0 L/h
91            0x61 => vec![0x8D],                     // Demanded torque: 16%
92            0x62 => vec![0x8D],                     // Actual torque: 16%
93            0x63 => vec![0x03, 0x7F],               // Reference torque: 895 Nm
94            // Bitmaps
95            0x00 => vec![0xBE, 0x3E, 0xB8, 0x11],  // Supported PIDs 01-20
96            0x01 => vec![0x00, 0x07, 0x65, 0x00],   // Monitor status
97            0x20 => vec![0x80, 0x12, 0xA0, 0x13],   // Supported PIDs 21-40
98            0x40 => vec![0xFA, 0xDC, 0x80, 0x00],   // Supported PIDs 41-60
99            0x60 => vec![0xE0, 0x00, 0x00, 0x00],   // Supported PIDs 61-80
100            _ => vec![0x00],                         // Unknown: return 0
101        }
102    }
103
104    /// Generate a mock J1939 PGN response.
105    fn mock_j1939_response(&self, pgn: u32) -> Vec<u8> {
106        match pgn {
107            // EEC1 (61444): RPM 680, torque 30%
108            61444 => {
109                let rpm_raw = (680.0_f64 / 0.125) as u16; // 5440
110                vec![
111                    0x00,                       // torque mode
112                    155,                        // demand torque: -125 + 155 = 30%
113                    155,                        // actual torque: -125 + 155 = 30%
114                    (rpm_raw & 0xFF) as u8,     // RPM low
115                    (rpm_raw >> 8) as u8,       // RPM high
116                    0xFF, 0xFF, 0xFF,           // reserved
117                ]
118            }
119            // CCVS (65265): 0 km/h, brake off, cruise off
120            65265 => vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
121            // ET1 (65262): coolant 50°C, fuel 20°C
122            65262 => vec![90, 60, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
123            // EFLP1 (65263): oil 400kPa, coolant 100kPa
124            65263 => vec![0xFF, 50, 0xFF, 100, 0xFF, 0xFF, 0xFF, 0xFF],
125            // LFE (65266): fuel rate 5.0 L/h
126            65266 => {
127                let rate_raw = (5.0_f64 / 0.05) as u16; // 100
128                vec![
129                    (rate_raw & 0xFF) as u8,
130                    (rate_raw >> 8) as u8,
131                    0x00, 0x02,     // fuel economy
132                    0xFF, 0xFF, 0xFF, 0xFF,
133                ]
134            }
135            // DM1 (65226): no active DTCs
136            65226 => vec![0x00, 0x00], // lamp status only, no DTCs
137            // Unknown PGN
138            _ => vec![0xFF; 8],
139        }
140    }
141}
142
143impl Default for MockAdapter {
144    fn default() -> Self { Self::new() }
145}
146
147#[async_trait]
148impl Adapter for MockAdapter {
149    async fn initialize(&mut self) -> Result<AdapterInfo, Obd2Error> {
150        self.initialized = true;
151        Ok(self.info.clone())
152    }
153
154    async fn request(&mut self, req: &ServiceRequest) -> Result<Vec<u8>, Obd2Error> {
155        match req.service_id {
156            // Mode 01: Current data
157            0x01 => {
158                if let Some(&pid_code) = req.data.first() {
159                    if self.supported.contains(&Pid(pid_code)) {
160                        Ok(self.mock_pid_response(pid_code))
161                    } else {
162                        Err(Obd2Error::NoData)
163                    }
164                } else {
165                    Err(Obd2Error::ParseError("no PID in request".into()))
166                }
167            }
168
169            // Mode 03: Read stored DTCs
170            0x03 => {
171                let mut result = Vec::new();
172                for dtc in &self.dtcs {
173                    // Encode DTC back to bytes (simplified)
174                    if dtc.code.len() == 5 {
175                        let prefix = match dtc.code.chars().next() {
176                            Some('P') => 0x00u8,
177                            Some('C') => 0x40,
178                            Some('B') => 0x80,
179                            Some('U') => 0xC0,
180                            _ => 0x00,
181                        };
182                        if let Ok(num) = u16::from_str_radix(&dtc.code[1..], 16) {
183                            let a = prefix | ((num >> 8) as u8 & 0x3F);
184                            let b = (num & 0xFF) as u8;
185                            result.push(a);
186                            result.push(b);
187                        }
188                    }
189                }
190                Ok(result)
191            }
192
193            // Mode 04: Clear DTCs
194            0x04 => {
195                self.dtcs.clear();
196                Ok(vec![])
197            }
198
199            // Mode 09: Vehicle info
200            0x09 => {
201                match req.data.first() {
202                    Some(0x02) => Ok(self.vin.as_bytes().to_vec()), // VIN
203                    _ => Ok(vec![]),
204                }
205            }
206
207            // Mode 05: O2 sensor monitoring
208            0x05 => {
209                match (req.data.first(), req.data.get(1)) {
210                    // Return mock O2 data for B1S1 and B1S2 (sensors 0x01, 0x02)
211                    (Some(_tid), Some(&sensor)) if sensor <= 0x02 => {
212                        // Return a realistic threshold voltage ~0.45V = 90 * 0.005
213                        Ok(vec![0x00, 0x5A])
214                    }
215                    // Other sensors not present
216                    _ => Err(Obd2Error::NoData),
217                }
218            }
219
220            // Mode 22: Enhanced read (return mock data)
221            0x21 | 0x22 => Ok(vec![0x80, 0x00]),
222
223            // Mode 10: Diagnostic session control
224            0x10 => Ok(vec![]),
225
226            // Mode 27: Security access (return mock seed)
227            0x27 => {
228                match req.data.first() {
229                    Some(0x01) => Ok(vec![0xAA, 0xBB, 0xCC, 0xDD]), // Mock seed
230                    Some(0x02) => Ok(vec![]),                         // Key accepted
231                    _ => Ok(vec![]),
232                }
233            }
234
235            // Mode 2F: Actuator control
236            0x2F => Ok(vec![]),
237
238            // Mode 3E: Tester present
239            0x3E => Ok(vec![]),
240
241            // J1939 Request PGN (0xEA)
242            0xEA => {
243                let pgn = match (req.data.first(), req.data.get(1), req.data.get(2)) {
244                    (Some(&lo), Some(&mid), Some(&hi)) => {
245                        (lo as u32) | ((mid as u32) << 8) | ((hi as u32) << 16)
246                    }
247                    _ => return Err(Obd2Error::NoData),
248                };
249                Ok(self.mock_j1939_response(pgn))
250            }
251
252            _ => Err(Obd2Error::NoData),
253        }
254    }
255
256    async fn supported_pids(&mut self) -> Result<HashSet<Pid>, Obd2Error> {
257        Ok(self.supported.clone())
258    }
259
260    async fn battery_voltage(&mut self) -> Result<Option<f64>, Obd2Error> {
261        Ok(Some(14.4))
262    }
263
264    fn info(&self) -> &AdapterInfo {
265        &self.info
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[tokio::test]
274    async fn test_mock_adapter_initialize() {
275        let mut adapter = MockAdapter::new();
276        let info = adapter.initialize().await.unwrap();
277        assert_eq!(info.chipset, Chipset::Elm327Genuine);
278    }
279
280    #[tokio::test]
281    async fn test_mock_adapter_supported_pids() {
282        let mut adapter = MockAdapter::new();
283        let pids = adapter.supported_pids().await.unwrap();
284        assert!(pids.contains(&Pid::ENGINE_RPM));
285        assert!(pids.contains(&Pid::VEHICLE_SPEED));
286        assert!(pids.contains(&Pid::COOLANT_TEMP));
287    }
288
289    #[tokio::test]
290    async fn test_mock_adapter_read_pid() {
291        let mut adapter = MockAdapter::new();
292        adapter.initialize().await.unwrap();
293        let req = ServiceRequest::read_pid(Pid::ENGINE_RPM);
294        let response = adapter.request(&req).await.unwrap();
295        assert_eq!(response.len(), 2); // RPM is 2 bytes
296    }
297
298    #[tokio::test]
299    async fn test_mock_adapter_read_vin() {
300        let mut adapter = MockAdapter::with_vin("1GCHK23224F000001");
301        let req = ServiceRequest::read_vin();
302        let response = adapter.request(&req).await.unwrap();
303        let vin = String::from_utf8_lossy(&response);
304        assert_eq!(vin.len(), 17);
305        assert_eq!(vin, "1GCHK23224F000001");
306    }
307
308    #[tokio::test]
309    async fn test_mock_adapter_read_dtcs() {
310        let mut adapter = MockAdapter::new();
311        adapter.set_dtcs(vec![
312            Dtc::from_code("P0420"),
313            Dtc::from_code("P0171"),
314        ]);
315        let req = ServiceRequest::read_dtcs();
316        let response = adapter.request(&req).await.unwrap();
317        assert_eq!(response.len(), 4); // 2 DTCs * 2 bytes each
318    }
319
320    #[tokio::test]
321    async fn test_mock_adapter_clear_dtcs() {
322        let mut adapter = MockAdapter::new();
323        adapter.set_dtcs(vec![Dtc::from_code("P0420")]);
324
325        // Clear
326        let req = ServiceRequest {
327            service_id: 0x04,
328            data: vec![],
329            target: crate::protocol::service::Target::Broadcast,
330        };
331        adapter.request(&req).await.unwrap();
332
333        // Verify cleared
334        let req = ServiceRequest::read_dtcs();
335        let response = adapter.request(&req).await.unwrap();
336        assert!(response.is_empty());
337    }
338
339    #[tokio::test]
340    async fn test_mock_adapter_unsupported_pid() {
341        let mut adapter = MockAdapter::new();
342        // PID 0xFF is not in supported set
343        let req = ServiceRequest::read_pid(Pid(0xFF));
344        let result = adapter.request(&req).await;
345        assert!(result.is_err());
346    }
347
348    #[tokio::test]
349    async fn test_mock_adapter_battery_voltage() {
350        let mut adapter = MockAdapter::new();
351        let voltage = adapter.battery_voltage().await.unwrap();
352        assert_eq!(voltage, Some(14.4));
353    }
354}