Skip to main content

obd2_core/session/
threshold.rs

1//! Threshold evaluation for Session.
2
3use crate::protocol::pid::Pid;
4use crate::vehicle::{ThresholdResult, VehicleSpec};
5
6/// Evaluate a standard PID reading against the matched spec's thresholds.
7///
8/// Returns None if no threshold is defined for this PID, or if no spec is matched.
9pub fn evaluate_pid_threshold(
10    spec: Option<&VehicleSpec>,
11    pid: Pid,
12    value: f64,
13) -> Option<ThresholdResult> {
14    let spec = spec?;
15    let thresholds = spec.thresholds.as_ref()?;
16
17    // Map PID code to threshold name(s)
18    let names = pid_threshold_name(pid)?;
19
20    // Search engine thresholds first, then transmission
21    for named in thresholds.engine.iter().chain(thresholds.transmission.iter()) {
22        if names.iter().any(|n| named.name == *n) {
23            return named.threshold.evaluate(value, pid.name());
24        }
25    }
26    None
27}
28
29/// Evaluate an enhanced PID reading against the matched spec's thresholds.
30pub fn evaluate_enhanced_threshold(
31    spec: Option<&VehicleSpec>,
32    did: u16,
33    value: f64,
34) -> Option<ThresholdResult> {
35    let spec = spec?;
36    let thresholds = spec.thresholds.as_ref()?;
37
38    // Search by DID hex string as name
39    let did_name = format!("{:#06X}", did);
40
41    for named in thresholds.engine.iter().chain(thresholds.transmission.iter()) {
42        if named.name == did_name {
43            return named.threshold.evaluate(value, &did_name);
44        }
45    }
46    None
47}
48
49/// Map a standard PID to its threshold name in the spec.
50///
51/// Tries both short names (used by embedded specs) and long names for
52/// maximum compatibility with user-authored specs.
53fn pid_threshold_name(pid: Pid) -> Option<&'static [&'static str]> {
54    match pid.0 {
55        0x05 => Some(&["coolant_temp", "coolant_temp_c"]),
56        0x0C => Some(&["rpm"]),
57        0x5C => Some(&["oil_temp", "oil_temp_c"]),
58        0x0B => Some(&["intake_map_kpa"]),
59        0x10 => Some(&["maf_gs"]),
60        0x42 => Some(&["battery_voltage"]),
61        0x2F => Some(&["fuel_tank_pct"]),
62        0x5E => Some(&["fuel_rate_lh"]),
63        0x46 => Some(&["ambient_temp_c", "ambient_temp"]),
64        0x0F => Some(&["intake_air_temp_c", "intake_air_temp"]),
65        0x33 => Some(&["barometric_kpa"]),
66        _ => None,
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use crate::vehicle::{
74        CommunicationSpec, EngineSpec, NamedThreshold, SpecIdentity, Threshold, ThresholdSet,
75        VehicleSpec,
76    };
77
78    fn make_spec_with_thresholds() -> VehicleSpec {
79        VehicleSpec {
80            spec_version: Some("1.0".into()),
81            identity: SpecIdentity {
82                name: "Test".into(),
83                model_years: (2020, 2020),
84                makes: vec!["Test".into()],
85                models: vec!["Test".into()],
86                engine: EngineSpec {
87                    code: "TEST".into(),
88                    displacement_l: 2.0,
89                    cylinders: 4,
90                    layout: "I4".into(),
91                    aspiration: "NA".into(),
92                    fuel_type: "Gasoline".into(),
93                    fuel_system: None,
94                    compression_ratio: None,
95                    max_power_kw: None,
96                    max_torque_nm: None,
97                    redline_rpm: 6500,
98                    idle_rpm_warm: 700,
99                    idle_rpm_cold: 900,
100                    firing_order: None,
101                    ecm_hardware: None,
102                },
103                transmission: None,
104                vin_match: None,
105            },
106            communication: CommunicationSpec {
107                buses: vec![],
108                elm327_protocol_code: None,
109            },
110            thresholds: Some(ThresholdSet {
111                engine: vec![
112                    NamedThreshold {
113                        name: "coolant_temp_c".into(),
114                        threshold: Threshold {
115                            min: Some(0.0),
116                            max: Some(130.0),
117                            warning_low: None,
118                            warning_high: Some(105.0),
119                            critical_low: None,
120                            critical_high: Some(115.0),
121                            unit: "\u{00B0}C".into(),
122                        },
123                    },
124                    NamedThreshold {
125                        name: "rpm".into(),
126                        threshold: Threshold {
127                            min: Some(0.0),
128                            max: Some(7000.0),
129                            warning_low: None,
130                            warning_high: Some(6000.0),
131                            critical_low: None,
132                            critical_high: Some(6500.0),
133                            unit: "RPM".into(),
134                        },
135                    },
136                ],
137                transmission: vec![],
138            }),
139            dtc_library: None,
140            polling_groups: vec![],
141            diagnostic_rules: vec![],
142            known_issues: vec![],
143            enhanced_pids: vec![],
144        }
145    }
146
147    #[test]
148    fn test_evaluate_normal() {
149        let spec = make_spec_with_thresholds();
150        let result = evaluate_pid_threshold(Some(&spec), Pid::COOLANT_TEMP, 90.0);
151        assert!(result.is_none()); // normal range
152    }
153
154    #[test]
155    fn test_evaluate_warning() {
156        let spec = make_spec_with_thresholds();
157        let result = evaluate_pid_threshold(Some(&spec), Pid::COOLANT_TEMP, 110.0);
158        assert!(result.is_some());
159        assert_eq!(result.unwrap().level, crate::vehicle::AlertLevel::Warning);
160    }
161
162    #[test]
163    fn test_evaluate_critical() {
164        let spec = make_spec_with_thresholds();
165        let result = evaluate_pid_threshold(Some(&spec), Pid::COOLANT_TEMP, 118.0);
166        assert!(result.is_some());
167        assert_eq!(result.unwrap().level, crate::vehicle::AlertLevel::Critical);
168    }
169
170    #[test]
171    fn test_evaluate_no_spec() {
172        let result = evaluate_pid_threshold(None, Pid::COOLANT_TEMP, 110.0);
173        assert!(result.is_none());
174    }
175
176    #[test]
177    fn test_evaluate_no_threshold_for_pid() {
178        let spec = make_spec_with_thresholds();
179        let result = evaluate_pid_threshold(Some(&spec), Pid::VEHICLE_SPEED, 200.0);
180        assert!(result.is_none()); // no speed threshold defined
181    }
182
183    #[test]
184    fn test_evaluate_rpm_warning() {
185        let spec = make_spec_with_thresholds();
186        let result = evaluate_pid_threshold(Some(&spec), Pid::ENGINE_RPM, 6200.0);
187        assert!(result.is_some());
188        assert_eq!(result.unwrap().level, crate::vehicle::AlertLevel::Warning);
189    }
190}