Skip to main content

obd2_core/session/
mod.rs

1//! Session orchestrator -- the primary entry point for consumers.
2
3pub mod diag_session;
4pub mod diagnostics;
5pub mod enhanced;
6pub mod modes;
7pub mod poller;
8pub mod threshold;
9
10use std::collections::HashSet;
11use crate::adapter::Adapter;
12use crate::error::Obd2Error;
13use crate::protocol::pid::Pid;
14use crate::protocol::dtc::{Dtc, DtcStatus};
15use crate::protocol::enhanced::{Value, Reading, ReadingSource};
16use crate::protocol::service::{ServiceRequest, Target, VehicleInfo};
17use crate::vehicle::{VehicleSpec, VehicleProfile, SpecRegistry, ModuleId};
18use std::time::Instant;
19
20/// The primary entry point for all OBD-II operations.
21///
22/// A Session wraps an Adapter and provides high-level methods for
23/// reading PIDs, DTCs, identifying vehicles, and more.
24///
25/// # Example
26///
27/// ```rust,no_run
28/// use obd2_core::adapter::mock::MockAdapter;
29/// use obd2_core::session::Session;
30/// use obd2_core::protocol::pid::Pid;
31///
32/// # async fn example() -> Result<(), obd2_core::error::Obd2Error> {
33/// let adapter = MockAdapter::new();
34/// let mut session = Session::new(adapter);
35/// let profile = session.identify_vehicle().await?;
36/// let rpm = session.read_pid(Pid::ENGINE_RPM).await?;
37/// # Ok(())
38/// # }
39/// ```
40pub struct Session<A: Adapter> {
41    adapter: A,
42    specs: SpecRegistry,
43    profile: Option<VehicleProfile>,
44    supported_pids_cache: Option<HashSet<Pid>>,
45}
46
47impl<A: Adapter> Session<A> {
48    /// Create a new Session with default embedded specs.
49    pub fn new(adapter: A) -> Self {
50        Self {
51            adapter,
52            specs: SpecRegistry::with_defaults(),
53            profile: None,
54            supported_pids_cache: None,
55        }
56    }
57
58    // -- Spec Management --
59
60    /// Load a vehicle spec from a YAML file.
61    pub fn load_spec(&mut self, path: &std::path::Path) -> Result<(), Obd2Error> {
62        self.specs.load_file(path)
63    }
64
65    /// Load all specs from a directory.
66    pub fn load_spec_dir(&mut self, dir: &std::path::Path) -> Result<usize, Obd2Error> {
67        self.specs.load_directory(dir)
68    }
69
70    /// Access the spec registry.
71    pub fn specs(&self) -> &SpecRegistry {
72        &self.specs
73    }
74
75    // -- Mode 01: Current Data --
76
77    /// Read a single standard PID.
78    pub async fn read_pid(&mut self, pid: Pid) -> Result<Reading, Obd2Error> {
79        let req = ServiceRequest::read_pid(pid);
80        let data = self.adapter.request(&req).await?;
81        let value = pid.parse(&data)?;
82        Ok(Reading {
83            value,
84            unit: pid.unit(),
85            timestamp: Instant::now(),
86            raw_bytes: data,
87            source: ReadingSource::Live,
88        })
89    }
90
91    /// Read multiple standard PIDs in sequence.
92    pub async fn read_pids(&mut self, pids: &[Pid]) -> Result<Vec<(Pid, Reading)>, Obd2Error> {
93        let mut results = Vec::with_capacity(pids.len());
94        for &pid in pids {
95            match self.read_pid(pid).await {
96                Ok(reading) => results.push((pid, reading)),
97                Err(Obd2Error::NoData) => continue, // skip unsupported
98                Err(e) => return Err(e),
99            }
100        }
101        Ok(results)
102    }
103
104    /// Query which standard PIDs this vehicle supports.
105    pub async fn supported_pids(&mut self) -> Result<HashSet<Pid>, Obd2Error> {
106        if let Some(cached) = &self.supported_pids_cache {
107            return Ok(cached.clone());
108        }
109        let pids = self.adapter.supported_pids().await?;
110        self.supported_pids_cache = Some(pids.clone());
111        Ok(pids)
112    }
113
114    // -- Mode 03/07/0A: DTCs --
115
116    /// Read stored (confirmed) DTCs via broadcast.
117    pub async fn read_dtcs(&mut self) -> Result<Vec<Dtc>, Obd2Error> {
118        let req = ServiceRequest::read_dtcs();
119        let data = self.adapter.request(&req).await?;
120        Ok(Self::decode_dtc_response(&data, DtcStatus::Stored))
121    }
122
123    /// Read pending DTCs (Mode 07).
124    pub async fn read_pending_dtcs(&mut self) -> Result<Vec<Dtc>, Obd2Error> {
125        let req = ServiceRequest {
126            service_id: 0x07,
127            data: vec![],
128            target: Target::Broadcast,
129        };
130        let data = self.adapter.request(&req).await?;
131        Ok(Self::decode_dtc_response(&data, DtcStatus::Pending))
132    }
133
134    /// Read permanent DTCs (Mode 0A).
135    pub async fn read_permanent_dtcs(&mut self) -> Result<Vec<Dtc>, Obd2Error> {
136        let req = ServiceRequest {
137            service_id: 0x0A,
138            data: vec![],
139            target: Target::Broadcast,
140        };
141        let data = self.adapter.request(&req).await?;
142        Ok(Self::decode_dtc_response(&data, DtcStatus::Permanent))
143    }
144
145    /// Decode DTC bytes from Mode 03/07/0A response.
146    fn decode_dtc_response(data: &[u8], status: DtcStatus) -> Vec<Dtc> {
147        let mut dtcs = Vec::new();
148        let mut i = 0;
149        while i + 1 < data.len() {
150            // Skip 00 00 padding
151            if data[i] == 0 && data[i + 1] == 0 {
152                i += 2;
153                continue;
154            }
155            let mut dtc = Dtc::from_bytes(data[i], data[i + 1]);
156            dtc.status = status;
157            dtcs.push(dtc);
158            i += 2;
159        }
160        dtcs
161    }
162
163    // -- Mode 04: Clear DTCs --
164
165    /// Clear all DTCs and reset monitors (broadcast).
166    pub async fn clear_dtcs(&mut self) -> Result<(), Obd2Error> {
167        tracing::warn!("Clearing all DTCs -- readiness monitors will be reset");
168        let req = ServiceRequest {
169            service_id: 0x04,
170            data: vec![],
171            target: Target::Broadcast,
172        };
173        self.adapter.request(&req).await?;
174        Ok(())
175    }
176
177    // -- Mode 09: Vehicle Information --
178
179    /// Read VIN (17 characters).
180    pub async fn read_vin(&mut self) -> Result<String, Obd2Error> {
181        let req = ServiceRequest::read_vin();
182        let data = self.adapter.request(&req).await?;
183        // Filter printable ASCII, take first 17 chars
184        let vin: String = data.iter()
185            .filter(|&&b| (0x20..=0x7E).contains(&b))
186            .map(|&b| b as char)
187            .take(17)
188            .collect();
189        if vin.len() == 17 {
190            Ok(vin)
191        } else {
192            Err(Obd2Error::ParseError(format!("VIN too short: {} chars", vin.len())))
193        }
194    }
195
196    /// Identify vehicle: read VIN, decode offline, match spec.
197    pub async fn identify_vehicle(&mut self) -> Result<VehicleProfile, Obd2Error> {
198        let vin = self.read_vin().await?;
199        let supported = self.supported_pids().await.unwrap_or_default();
200
201        // Decode VIN offline (for side effects / logging; result unused for now)
202        let _decoded = crate::vehicle::vin::decode(&vin);
203
204        // Match spec by VIN
205        let spec = self.specs.match_vin(&vin).cloned();
206
207        let profile = VehicleProfile {
208            vin: vin.clone(),
209            info: Some(VehicleInfo {
210                vin: vin.clone(),
211                calibration_ids: vec![],
212                cvns: vec![],
213                ecu_name: None,
214            }),
215            spec,
216            supported_pids: supported,
217        };
218
219        self.profile = Some(profile.clone());
220        Ok(profile)
221    }
222
223    // -- Enhanced PIDs (Mode 21/22) --
224
225    /// Read an enhanced PID from a specific module.
226    pub async fn read_enhanced(&mut self, did: u16, module: ModuleId) -> Result<Reading, Obd2Error> {
227        // Look up the service ID for this enhanced PID from the spec
228        let service_id = self.lookup_enhanced_service_id(did, &module);
229
230        let req = ServiceRequest::enhanced_read(
231            service_id,
232            did,
233            Target::Module(module.0.clone()),
234        );
235        let data = self.adapter.request(&req).await?;
236
237        // Return raw bytes as Value::Raw until we have formula evaluation
238        Ok(Reading {
239            value: Value::Raw(data.clone()),
240            unit: "",
241            timestamp: Instant::now(),
242            raw_bytes: data,
243            source: ReadingSource::Live,
244        })
245    }
246
247    /// Look up the service ID for an enhanced PID from the spec.
248    fn lookup_enhanced_service_id(&self, did: u16, module: &ModuleId) -> u8 {
249        enhanced::find_service_id_from_spec(self.spec(), did, module)
250    }
251
252    /// List enhanced PIDs available for a module (from matched spec).
253    pub fn module_pids(&self, module: ModuleId) -> Vec<&crate::protocol::enhanced::EnhancedPid> {
254        enhanced::list_module_pids(self.spec(), &module)
255    }
256
257    // -- Mode 05: O2 Sensor Monitoring (non-CAN) --
258
259    /// Read O2 sensor monitoring test results for a specific TID.
260    pub async fn read_o2_monitoring(
261        &mut self,
262        test_id: u8,
263    ) -> Result<Vec<crate::protocol::service::O2TestResult>, Obd2Error> {
264        modes::read_o2_monitoring(&mut self.adapter, test_id).await
265    }
266
267    /// Read all O2 sensor monitoring tests (TIDs 0x01-0x09).
268    pub async fn read_all_o2_monitoring(
269        &mut self,
270    ) -> Result<Vec<crate::protocol::service::O2TestResult>, Obd2Error> {
271        modes::read_all_o2_monitoring(&mut self.adapter).await
272    }
273
274    // -- State Accessors --
275
276    /// Current vehicle profile (after identify_vehicle()).
277    pub fn vehicle(&self) -> Option<&VehicleProfile> {
278        self.profile.as_ref()
279    }
280
281    /// Matched spec (shorthand).
282    pub fn spec(&self) -> Option<&VehicleSpec> {
283        self.profile.as_ref().and_then(|p| p.spec.as_ref())
284    }
285
286    /// Adapter info.
287    pub fn adapter_info(&self) -> &crate::adapter::AdapterInfo {
288        self.adapter.info()
289    }
290
291    /// Battery voltage.
292    pub async fn battery_voltage(&mut self) -> Result<Option<f64>, Obd2Error> {
293        self.adapter.battery_voltage().await
294    }
295
296    /// Raw service request (escape hatch).
297    pub async fn raw_request(&mut self, service: u8, data: &[u8], target: Target) -> Result<Vec<u8>, Obd2Error> {
298        let req = ServiceRequest {
299            service_id: service,
300            data: data.to_vec(),
301            target,
302        };
303        self.adapter.request(&req).await
304    }
305}
306
307impl<A: Adapter> std::fmt::Debug for Session<A> {
308    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309        f.debug_struct("Session")
310            .field("profile", &self.profile)
311            .field("specs_loaded", &self.specs.specs().len())
312            .finish()
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use crate::adapter::mock::MockAdapter;
320
321    #[tokio::test]
322    async fn test_session_read_pid() {
323        let adapter = MockAdapter::new();
324        let mut session = Session::new(adapter);
325        let reading = session.read_pid(Pid::ENGINE_RPM).await.unwrap();
326        // MockAdapter returns [0x0A, 0xA0] for RPM = 680
327        assert_eq!(reading.value.as_f64().unwrap(), 680.0);
328        assert_eq!(reading.unit, "RPM");
329        assert_eq!(reading.source, ReadingSource::Live);
330    }
331
332    #[tokio::test]
333    async fn test_session_read_multiple_pids() {
334        let adapter = MockAdapter::new();
335        let mut session = Session::new(adapter);
336        let results = session.read_pids(&[Pid::ENGINE_RPM, Pid::COOLANT_TEMP, Pid::VEHICLE_SPEED]).await.unwrap();
337        assert_eq!(results.len(), 3);
338    }
339
340    #[tokio::test]
341    async fn test_session_supported_pids() {
342        let adapter = MockAdapter::new();
343        let mut session = Session::new(adapter);
344        let pids = session.supported_pids().await.unwrap();
345        assert!(pids.contains(&Pid::ENGINE_RPM));
346        assert!(pids.contains(&Pid::VEHICLE_SPEED));
347    }
348
349    #[tokio::test]
350    async fn test_session_supported_pids_cached() {
351        let adapter = MockAdapter::new();
352        let mut session = Session::new(adapter);
353        let pids1 = session.supported_pids().await.unwrap();
354        let pids2 = session.supported_pids().await.unwrap();
355        assert_eq!(pids1, pids2); // Second call uses cache
356    }
357
358    #[tokio::test]
359    async fn test_session_read_vin() {
360        let adapter = MockAdapter::with_vin("1GCHK23224F000001");
361        let mut session = Session::new(adapter);
362        let vin = session.read_vin().await.unwrap();
363        assert_eq!(vin, "1GCHK23224F000001");
364    }
365
366    #[tokio::test]
367    async fn test_session_identify_vehicle() {
368        let adapter = MockAdapter::with_vin("1GCHK23224F000001");
369        let mut session = Session::new(adapter);
370        let profile = session.identify_vehicle().await.unwrap();
371        assert_eq!(profile.vin, "1GCHK23224F000001");
372        // Should match the embedded Duramax spec
373        assert!(profile.spec.is_some(), "should match Duramax spec by VIN");
374        assert_eq!(profile.spec.as_ref().unwrap().identity.engine.code, "LLY");
375    }
376
377    #[tokio::test]
378    async fn test_session_identify_no_spec() {
379        let adapter = MockAdapter::with_vin("JH4KA7660PC000001"); // Acura, no spec
380        let mut session = Session::new(adapter);
381        let profile = session.identify_vehicle().await.unwrap();
382        assert!(profile.spec.is_none());
383    }
384
385    #[tokio::test]
386    async fn test_session_read_dtcs() {
387        let mut adapter = MockAdapter::new();
388        adapter.set_dtcs(vec![
389            Dtc::from_code("P0420"),
390            Dtc::from_code("P0171"),
391        ]);
392        let mut session = Session::new(adapter);
393        let dtcs = session.read_dtcs().await.unwrap();
394        assert_eq!(dtcs.len(), 2);
395        assert!(dtcs.iter().any(|d| d.code == "P0420"));
396        assert!(dtcs.iter().any(|d| d.code == "P0171"));
397    }
398
399    #[tokio::test]
400    async fn test_session_clear_dtcs() {
401        let mut adapter = MockAdapter::new();
402        adapter.set_dtcs(vec![Dtc::from_code("P0420")]);
403        let mut session = Session::new(adapter);
404
405        session.clear_dtcs().await.unwrap();
406        let dtcs = session.read_dtcs().await.unwrap();
407        assert!(dtcs.is_empty());
408    }
409
410    #[tokio::test]
411    async fn test_session_battery_voltage() {
412        let adapter = MockAdapter::new();
413        let mut session = Session::new(adapter);
414        let voltage = session.battery_voltage().await.unwrap();
415        assert_eq!(voltage, Some(14.4));
416    }
417
418    #[tokio::test]
419    async fn test_session_no_spec_still_reads_pids() {
420        let adapter = MockAdapter::with_vin("JH4KA7660PC000001");
421        let mut session = Session::new(adapter);
422        // Standard PIDs work without a spec
423        let reading = session.read_pid(Pid::ENGINE_RPM).await.unwrap();
424        assert!(reading.value.as_f64().is_ok());
425    }
426
427    #[tokio::test]
428    async fn test_session_raw_request() {
429        let adapter = MockAdapter::new();
430        let mut session = Session::new(adapter);
431        let data = session.raw_request(0x09, &[0x02], Target::Broadcast).await.unwrap();
432        assert!(!data.is_empty()); // VIN bytes
433    }
434}