1use crate::protocol::dtc::{Dtc, universal_dtc_description};
17use crate::vehicle::{VehicleSpec, DiagnosticRule, RuleTrigger, KnownIssue};
18
19pub fn enrich_dtcs(dtcs: &mut [Dtc], spec: Option<&VehicleSpec>) {
26 for dtc in dtcs.iter_mut() {
27 if let Some(spec) = spec {
29 if let Some(lib) = &spec.dtc_library {
30 if let Some(entry) = lib.lookup(&dtc.code) {
31 dtc.description = Some(entry.meaning.clone());
32 dtc.severity = Some(entry.severity);
33 dtc.notes = entry.notes.clone();
34 continue;
35 }
36 }
37 }
38
39 if dtc.description.is_none() {
41 dtc.description = universal_dtc_description(&dtc.code).map(String::from);
42 }
43 }
44}
45
46pub fn active_rules<'a>(
52 dtcs: &[Dtc],
53 spec: Option<&'a VehicleSpec>,
54) -> Vec<&'a DiagnosticRule> {
55 let spec = match spec {
56 Some(s) => s,
57 None => return vec![],
58 };
59
60 spec.diagnostic_rules.iter().filter(|rule| {
61 match &rule.trigger {
62 RuleTrigger::DtcPresent(code) => {
63 dtcs.iter().any(|d| d.code == *code)
64 }
65 RuleTrigger::DtcRange(start, end) => {
66 dtcs.iter().any(|d| d.code >= *start && d.code <= *end)
67 }
68 }
69 }).collect()
70}
71
72pub fn matching_issues<'a>(
74 dtcs: &[Dtc],
75 spec: Option<&'a VehicleSpec>,
76) -> Vec<&'a KnownIssue> {
77 let spec = match spec {
78 Some(s) => s,
79 None => return vec![],
80 };
81
82 let dtc_codes: Vec<&str> = dtcs.iter().map(|d| d.code.as_str()).collect();
83
84 let mut matches: Vec<&KnownIssue> = spec.known_issues.iter().filter(|issue| {
85 issue.symptoms.iter().any(|symptom| dtc_codes.contains(&symptom.as_str()))
86 }).collect();
87
88 matches.sort_by_key(|i| i.rank);
90 matches
91}
92
93pub fn dedup_dtcs(dtcs: &mut Vec<Dtc>) {
95 let mut seen = std::collections::HashSet::new();
96 dtcs.retain(|dtc| seen.insert(dtc.code.clone()));
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::protocol::dtc::Severity;
103 use crate::vehicle::*;
104
105 fn make_spec_with_dtcs() -> VehicleSpec {
106 VehicleSpec {
107 spec_version: Some("1.0".into()),
108 identity: SpecIdentity {
109 name: "Test".into(),
110 model_years: (2004, 2005),
111 makes: vec!["Chevrolet".into()],
112 models: vec!["Silverado".into()],
113 engine: EngineSpec {
114 code: "LLY".into(),
115 displacement_l: 6.6,
116 cylinders: 8,
117 layout: "V8".into(),
118 aspiration: "Turbo".into(),
119 fuel_type: "Diesel".into(),
120 fuel_system: None,
121 compression_ratio: None,
122 max_power_kw: None,
123 max_torque_nm: None,
124 redline_rpm: 3250,
125 idle_rpm_warm: 680,
126 idle_rpm_cold: 780,
127 firing_order: None,
128 ecm_hardware: None,
129 },
130 transmission: None,
131 vin_match: None,
132 },
133 communication: CommunicationSpec {
134 buses: vec![],
135 elm327_protocol_code: None,
136 },
137 thresholds: None,
138 dtc_library: Some(DtcLibrary {
139 ecm: vec![
140 DtcEntry {
141 code: "P0087".into(),
142 meaning: "Fuel Rail Pressure Too Low".into(),
143 severity: Severity::Critical,
144 notes: Some("CP3 pump failure or fuel filter".into()),
145 related_pids: None,
146 category: None,
147 },
148 DtcEntry {
149 code: "P0234".into(),
150 meaning: "Turbo Overboost Condition".into(),
151 severity: Severity::Critical,
152 notes: Some("VGT vanes stuck closed".into()),
153 related_pids: None,
154 category: None,
155 },
156 ],
157 tcm: vec![],
158 bcm: vec![],
159 network: vec![],
160 }),
161 polling_groups: vec![],
162 diagnostic_rules: vec![
163 DiagnosticRule {
164 name: "P0700 TCM redirect".into(),
165 trigger: RuleTrigger::DtcPresent("P0700".into()),
166 action: RuleAction::QueryModule {
167 module: "tcm".into(),
168 service: 0x03,
169 },
170 description: "P0700 means query TCM directly for real DTCs".into(),
171 },
172 DiagnosticRule {
173 name: "FICM check".into(),
174 trigger: RuleTrigger::DtcRange("P0201".into(), "P0208".into()),
175 action: RuleAction::CheckFirst {
176 pid: 0x1100,
177 module: "ficm".into(),
178 reason: "Check FICM 48V before condemning injectors".into(),
179 },
180 description: "90% of injector circuit codes are FICM failures".into(),
181 },
182 ],
183 known_issues: vec![
184 KnownIssue {
185 rank: 1,
186 name: "turbo_vane_sticking".into(),
187 description: "VGT vanes stick from carbon buildup".into(),
188 symptoms: vec!["P0234".into(), "P0299".into()],
189 root_cause: "Carbon/soot in unison ring".into(),
190 quick_test: Some(QuickTest {
191 description: "Monitor VGT Position Error under load".into(),
192 pass_criteria: "Error < 10%".into(),
193 }),
194 fix: "Remove turbo, clean unison ring".into(),
195 },
196 KnownIssue {
197 rank: 2,
198 name: "ficm_failure".into(),
199 description: "FICM 48V capacitor bank degradation".into(),
200 symptoms: vec!["P0201".into(), "P0611".into(), "P2146".into()],
201 root_cause: "Internal capacitor failure".into(),
202 quick_test: None,
203 fix: "Replace or rebuild FICM".into(),
204 },
205 ],
206 enhanced_pids: vec![],
207 }
208 }
209
210 #[test]
211 fn test_enrich_from_spec() {
212 let spec = make_spec_with_dtcs();
213 let mut dtcs = vec![Dtc::from_code("P0087")];
214 enrich_dtcs(&mut dtcs, Some(&spec));
215 assert_eq!(dtcs[0].description.as_deref(), Some("Fuel Rail Pressure Too Low"));
216 assert_eq!(dtcs[0].severity, Some(Severity::Critical));
217 assert!(dtcs[0].notes.is_some());
218 }
219
220 #[test]
221 fn test_enrich_fallback_universal() {
222 let spec = make_spec_with_dtcs();
223 let mut dtcs = vec![Dtc::from_code("P0420")]; enrich_dtcs(&mut dtcs, Some(&spec));
225 assert!(dtcs[0].description.is_some());
226 assert!(dtcs[0].description.as_ref().unwrap().contains("Catalyst"));
227 }
228
229 #[test]
230 fn test_enrich_no_spec() {
231 let mut dtcs = vec![Dtc::from_code("P0420")];
232 enrich_dtcs(&mut dtcs, None);
233 assert!(dtcs[0].description.is_some()); }
235
236 #[test]
237 fn test_active_rules_p0700() {
238 let spec = make_spec_with_dtcs();
239 let dtcs = vec![Dtc::from_code("P0700")];
240 let rules = active_rules(&dtcs, Some(&spec));
241 assert_eq!(rules.len(), 1);
242 assert_eq!(rules[0].name, "P0700 TCM redirect");
243 }
244
245 #[test]
246 fn test_active_rules_injector_range() {
247 let spec = make_spec_with_dtcs();
248 let dtcs = vec![Dtc::from_code("P0204")]; let rules = active_rules(&dtcs, Some(&spec));
250 assert_eq!(rules.len(), 1);
251 assert_eq!(rules[0].name, "FICM check");
252 }
253
254 #[test]
255 fn test_active_rules_none() {
256 let spec = make_spec_with_dtcs();
257 let dtcs = vec![Dtc::from_code("P0420")]; let rules = active_rules(&dtcs, Some(&spec));
259 assert!(rules.is_empty());
260 }
261
262 #[test]
263 fn test_matching_issues_turbo() {
264 let spec = make_spec_with_dtcs();
265 let dtcs = vec![Dtc::from_code("P0234")];
266 let issues = matching_issues(&dtcs, Some(&spec));
267 assert_eq!(issues.len(), 1);
268 assert_eq!(issues[0].name, "turbo_vane_sticking");
269 }
270
271 #[test]
272 fn test_matching_issues_ficm() {
273 let spec = make_spec_with_dtcs();
274 let dtcs = vec![Dtc::from_code("P0201")];
275 let issues = matching_issues(&dtcs, Some(&spec));
276 assert_eq!(issues.len(), 1);
277 assert_eq!(issues[0].name, "ficm_failure");
278 }
279
280 #[test]
281 fn test_matching_issues_multiple() {
282 let spec = make_spec_with_dtcs();
283 let dtcs = vec![Dtc::from_code("P0234"), Dtc::from_code("P0201")];
284 let issues = matching_issues(&dtcs, Some(&spec));
285 assert_eq!(issues.len(), 2);
286 assert_eq!(issues[0].rank, 1); }
288
289 #[test]
290 fn test_matching_issues_no_match() {
291 let spec = make_spec_with_dtcs();
292 let dtcs = vec![Dtc::from_code("P0420")];
293 let issues = matching_issues(&dtcs, Some(&spec));
294 assert!(issues.is_empty());
295 }
296
297 #[test]
298 fn test_dedup_dtcs() {
299 let mut dtcs = vec![
300 Dtc::from_code("P0420"),
301 Dtc::from_code("P0171"),
302 Dtc::from_code("P0420"), ];
304 dedup_dtcs(&mut dtcs);
305 assert_eq!(dtcs.len(), 2);
306 }
307}