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    ///
198    /// Populates the VehicleProfile with:
199    /// - Offline VIN decode (manufacturer, year, vehicle class)
200    /// - Matched vehicle spec (if any)
201    /// - Supported standard PIDs
202    pub async fn identify_vehicle(&mut self) -> Result<VehicleProfile, Obd2Error> {
203        let vin = self.read_vin().await?;
204        let supported = self.supported_pids().await.unwrap_or_default();
205
206        // Decode VIN offline — manufacturer, year, vehicle class
207        let decoded = crate::vehicle::vin::decode(&vin);
208
209        // Match spec by VIN
210        let spec = self.specs.match_vin(&vin).cloned();
211
212        let profile = VehicleProfile {
213            vin: vin.clone(),
214            decoded_vin: Some(decoded),
215            info: Some(VehicleInfo {
216                vin: vin.clone(),
217                calibration_ids: vec![],
218                cvns: vec![],
219                ecu_name: None,
220            }),
221            spec,
222            supported_pids: supported,
223        };
224
225        self.profile = Some(profile.clone());
226        Ok(profile)
227    }
228
229    // -- Enhanced PIDs (Mode 21/22) --
230
231    /// Read an enhanced PID from a specific module.
232    pub async fn read_enhanced(&mut self, did: u16, module: ModuleId) -> Result<Reading, Obd2Error> {
233        // Look up the service ID for this enhanced PID from the spec
234        let service_id = self.lookup_enhanced_service_id(did, &module);
235
236        let req = ServiceRequest::enhanced_read(
237            service_id,
238            did,
239            Target::Module(module.0.clone()),
240        );
241        let data = self.adapter.request(&req).await?;
242
243        // Return raw bytes as Value::Raw until we have formula evaluation
244        Ok(Reading {
245            value: Value::Raw(data.clone()),
246            unit: "",
247            timestamp: Instant::now(),
248            raw_bytes: data,
249            source: ReadingSource::Live,
250        })
251    }
252
253    /// Look up the service ID for an enhanced PID from the spec.
254    fn lookup_enhanced_service_id(&self, did: u16, module: &ModuleId) -> u8 {
255        enhanced::find_service_id_from_spec(self.spec(), did, module)
256    }
257
258    /// List enhanced PIDs available for a module (from matched spec).
259    pub fn module_pids(&self, module: ModuleId) -> Vec<&crate::protocol::enhanced::EnhancedPid> {
260        enhanced::list_module_pids(self.spec(), &module)
261    }
262
263    // -- Mode 05: O2 Sensor Monitoring (non-CAN) --
264
265    /// Read O2 sensor monitoring test results for a specific TID.
266    pub async fn read_o2_monitoring(
267        &mut self,
268        test_id: u8,
269    ) -> Result<Vec<crate::protocol::service::O2TestResult>, Obd2Error> {
270        modes::read_o2_monitoring(&mut self.adapter, test_id).await
271    }
272
273    /// Read all O2 sensor monitoring tests (TIDs 0x01-0x09).
274    pub async fn read_all_o2_monitoring(
275        &mut self,
276    ) -> Result<Vec<crate::protocol::service::O2TestResult>, Obd2Error> {
277        modes::read_all_o2_monitoring(&mut self.adapter).await
278    }
279
280    // -- J1939 Heavy-Duty Protocol --
281
282    /// Read a J1939 Parameter Group from a heavy-duty vehicle.
283    ///
284    /// Sends a CAN 29-bit request for the specified PGN and returns the raw
285    /// response bytes. Use the decoder functions in [`crate::protocol::j1939`]
286    /// to parse the response.
287    ///
288    /// Requires an ELM327/STN adapter on a J1939-capable vehicle (CAN 29-bit 250 kbps).
289    ///
290    /// # Example
291    ///
292    /// ```rust,no_run
293    /// # use obd2_core::protocol::j1939::{Pgn, decode_eec1};
294    /// # async fn example(session: &mut obd2_core::session::Session<obd2_core::adapter::mock::MockAdapter>) {
295    /// let data = session.read_j1939_pgn(Pgn::EEC1).await.unwrap();
296    /// if let Some(eec1) = decode_eec1(&data) {
297    ///     if let (Some(rpm), Some(torque)) = (eec1.engine_rpm, eec1.actual_torque_pct) {
298    ///         println!("RPM: {rpm:.0}, Torque: {torque:.0}%");
299    ///     }
300    /// }
301    /// # }
302    /// ```
303    pub async fn read_j1939_pgn(
304        &mut self,
305        pgn: crate::protocol::j1939::Pgn,
306    ) -> Result<Vec<u8>, Obd2Error> {
307        // J1939 request PGN via CAN 29-bit.
308        // ELM327/STN: use service 0x00 with the PGN encoded in the data bytes.
309        // The PGN is sent as a 3-byte request: [PGN_low, PGN_mid, PGN_high]
310        let pgn_bytes = [
311            (pgn.0 & 0xFF) as u8,
312            ((pgn.0 >> 8) & 0xFF) as u8,
313            ((pgn.0 >> 16) & 0xFF) as u8,
314        ];
315        // Use raw_request with a J1939-specific service marker (0xEA = Request PGN)
316        self.raw_request(0xEA, &pgn_bytes, Target::Broadcast).await
317    }
318
319    /// Read and decode J1939 active DTCs (DM1 — PGN 65226).
320    ///
321    /// Returns J1939-format DTCs (SPN + FMI), distinct from OBD-II P-codes.
322    pub async fn read_j1939_dtcs(&mut self) -> Result<Vec<crate::protocol::j1939::J1939Dtc>, Obd2Error> {
323        let data = self.read_j1939_pgn(crate::protocol::j1939::Pgn::DM1).await?;
324        Ok(crate::protocol::j1939::decode_dm1(&data))
325    }
326
327    // -- Thresholds --
328
329    /// Evaluate a standard PID reading against the matched spec's thresholds.
330    ///
331    /// Returns `None` if the value is in normal range, no threshold is defined
332    /// for this PID, or no spec is matched.
333    ///
334    /// # Example
335    ///
336    /// ```rust,no_run
337    /// # use obd2_core::protocol::pid::Pid;
338    /// # use obd2_core::vehicle::AlertLevel;
339    /// # async fn example(session: &obd2_core::session::Session<obd2_core::adapter::mock::MockAdapter>) {
340    /// if let Some(result) = session.evaluate_threshold(Pid::COOLANT_TEMP, 110.0) {
341    ///     match result.level {
342    ///         AlertLevel::Warning => eprintln!("Warning: {}", result.message),
343    ///         AlertLevel::Critical => eprintln!("CRITICAL: {}", result.message),
344    ///         AlertLevel::Normal => {}
345    ///     }
346    /// }
347    /// # }
348    /// ```
349    pub fn evaluate_threshold(&self, pid: Pid, value: f64) -> Option<crate::vehicle::ThresholdResult> {
350        threshold::evaluate_pid_threshold(self.spec(), pid, value)
351    }
352
353    /// Evaluate an enhanced PID (DID) reading against the matched spec's thresholds.
354    pub fn evaluate_enhanced_threshold(&self, did: u16, value: f64) -> Option<crate::vehicle::ThresholdResult> {
355        threshold::evaluate_enhanced_threshold(self.spec(), did, value)
356    }
357
358    // -- State Accessors --
359
360    /// Current vehicle profile (after identify_vehicle()).
361    pub fn vehicle(&self) -> Option<&VehicleProfile> {
362        self.profile.as_ref()
363    }
364
365    /// Matched spec (shorthand).
366    pub fn spec(&self) -> Option<&VehicleSpec> {
367        self.profile.as_ref().and_then(|p| p.spec.as_ref())
368    }
369
370    /// Adapter info.
371    pub fn adapter_info(&self) -> &crate::adapter::AdapterInfo {
372        self.adapter.info()
373    }
374
375    /// Battery voltage.
376    pub async fn battery_voltage(&mut self) -> Result<Option<f64>, Obd2Error> {
377        self.adapter.battery_voltage().await
378    }
379
380    /// Raw service request (escape hatch).
381    pub async fn raw_request(&mut self, service: u8, data: &[u8], target: Target) -> Result<Vec<u8>, Obd2Error> {
382        let req = ServiceRequest {
383            service_id: service,
384            data: data.to_vec(),
385            target,
386        };
387        self.adapter.request(&req).await
388    }
389}
390
391impl<A: Adapter> std::fmt::Debug for Session<A> {
392    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393        f.debug_struct("Session")
394            .field("profile", &self.profile)
395            .field("specs_loaded", &self.specs.specs().len())
396            .finish()
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use crate::adapter::mock::MockAdapter;
404
405    #[tokio::test]
406    async fn test_session_read_pid() {
407        let adapter = MockAdapter::new();
408        let mut session = Session::new(adapter);
409        let reading = session.read_pid(Pid::ENGINE_RPM).await.unwrap();
410        // MockAdapter returns [0x0A, 0xA0] for RPM = 680
411        assert_eq!(reading.value.as_f64().unwrap(), 680.0);
412        assert_eq!(reading.unit, "RPM");
413        assert_eq!(reading.source, ReadingSource::Live);
414    }
415
416    #[tokio::test]
417    async fn test_session_read_multiple_pids() {
418        let adapter = MockAdapter::new();
419        let mut session = Session::new(adapter);
420        let results = session.read_pids(&[Pid::ENGINE_RPM, Pid::COOLANT_TEMP, Pid::VEHICLE_SPEED]).await.unwrap();
421        assert_eq!(results.len(), 3);
422    }
423
424    #[tokio::test]
425    async fn test_session_supported_pids() {
426        let adapter = MockAdapter::new();
427        let mut session = Session::new(adapter);
428        let pids = session.supported_pids().await.unwrap();
429        assert!(pids.contains(&Pid::ENGINE_RPM));
430        assert!(pids.contains(&Pid::VEHICLE_SPEED));
431    }
432
433    #[tokio::test]
434    async fn test_session_supported_pids_cached() {
435        let adapter = MockAdapter::new();
436        let mut session = Session::new(adapter);
437        let pids1 = session.supported_pids().await.unwrap();
438        let pids2 = session.supported_pids().await.unwrap();
439        assert_eq!(pids1, pids2); // Second call uses cache
440    }
441
442    #[tokio::test]
443    async fn test_session_read_vin() {
444        let adapter = MockAdapter::with_vin("1GCHK23224F000001");
445        let mut session = Session::new(adapter);
446        let vin = session.read_vin().await.unwrap();
447        assert_eq!(vin, "1GCHK23224F000001");
448    }
449
450    #[tokio::test]
451    async fn test_session_identify_vehicle() {
452        let adapter = MockAdapter::with_vin("1GCHK23224F000001");
453        let mut session = Session::new(adapter);
454        let profile = session.identify_vehicle().await.unwrap();
455        assert_eq!(profile.vin, "1GCHK23224F000001");
456        // Should match the embedded Duramax spec
457        assert!(profile.spec.is_some(), "should match Duramax spec by VIN");
458        assert_eq!(profile.spec.as_ref().unwrap().identity.engine.code, "LLY");
459    }
460
461    #[tokio::test]
462    async fn test_session_identify_no_spec() {
463        let adapter = MockAdapter::with_vin("JH4KA7660PC000001"); // Acura, no spec
464        let mut session = Session::new(adapter);
465        let profile = session.identify_vehicle().await.unwrap();
466        assert!(profile.spec.is_none());
467    }
468
469    #[tokio::test]
470    async fn test_session_read_dtcs() {
471        let mut adapter = MockAdapter::new();
472        adapter.set_dtcs(vec![
473            Dtc::from_code("P0420"),
474            Dtc::from_code("P0171"),
475        ]);
476        let mut session = Session::new(adapter);
477        let dtcs = session.read_dtcs().await.unwrap();
478        assert_eq!(dtcs.len(), 2);
479        assert!(dtcs.iter().any(|d| d.code == "P0420"));
480        assert!(dtcs.iter().any(|d| d.code == "P0171"));
481    }
482
483    #[tokio::test]
484    async fn test_session_clear_dtcs() {
485        let mut adapter = MockAdapter::new();
486        adapter.set_dtcs(vec![Dtc::from_code("P0420")]);
487        let mut session = Session::new(adapter);
488
489        session.clear_dtcs().await.unwrap();
490        let dtcs = session.read_dtcs().await.unwrap();
491        assert!(dtcs.is_empty());
492    }
493
494    #[tokio::test]
495    async fn test_session_battery_voltage() {
496        let adapter = MockAdapter::new();
497        let mut session = Session::new(adapter);
498        let voltage = session.battery_voltage().await.unwrap();
499        assert_eq!(voltage, Some(14.4));
500    }
501
502    #[tokio::test]
503    async fn test_session_no_spec_still_reads_pids() {
504        let adapter = MockAdapter::with_vin("JH4KA7660PC000001");
505        let mut session = Session::new(adapter);
506        // Standard PIDs work without a spec
507        let reading = session.read_pid(Pid::ENGINE_RPM).await.unwrap();
508        assert!(reading.value.as_f64().is_ok());
509    }
510
511    #[tokio::test]
512    async fn test_session_raw_request() {
513        let adapter = MockAdapter::new();
514        let mut session = Session::new(adapter);
515        let data = session.raw_request(0x09, &[0x02], Target::Broadcast).await.unwrap();
516        assert!(!data.is_empty()); // VIN bytes
517    }
518}