1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct Pgn(pub u32);
28
29impl Pgn {
30 pub const EEC1: Pgn = Pgn(61444);
35
36 pub const ET1: Pgn = Pgn(65262);
39
40 pub const EFLP1: Pgn = Pgn(65263);
43
44 pub const LFE: Pgn = Pgn(65266);
47
48 pub const CCVS: Pgn = Pgn(65265);
53
54 pub const DM1: Pgn = Pgn(65226);
59
60 pub const DM2: Pgn = Pgn(65227);
63
64 pub fn name(&self) -> &'static str {
66 match self.0 {
67 61444 => "EEC1 (Electronic Engine Controller 1)",
68 65262 => "ET1 (Engine Temperature 1)",
69 65263 => "EFLP1 (Engine Fluid Level/Pressure 1)",
70 65265 => "CCVS (Cruise Control/Vehicle Speed)",
71 65266 => "LFE (Fuel Economy - Liquid)",
72 65226 => "DM1 (Active DTCs)",
73 65227 => "DM2 (Previously Active DTCs)",
74 _ => "Unknown PGN",
75 }
76 }
77}
78
79impl std::fmt::Display for Pgn {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 write!(f, "PGN {} ({})", self.0, self.name())
82 }
83}
84
85#[derive(Debug, Clone)]
91pub struct Eec1 {
92 pub engine_rpm: Option<f64>,
94 pub driver_demand_torque_pct: Option<f64>,
96 pub actual_torque_pct: Option<f64>,
98 pub torque_mode: u8,
100}
101
102#[derive(Debug, Clone)]
106pub struct Ccvs {
107 pub vehicle_speed: Option<f64>,
109 pub brake_switch: Option<bool>,
111 pub cruise_active: Option<bool>,
113}
114
115#[derive(Debug, Clone)]
119pub struct Et1 {
120 pub coolant_temp: Option<f64>,
122 pub fuel_temp: Option<f64>,
124 pub oil_temp: Option<f64>,
126}
127
128#[derive(Debug, Clone)]
132pub struct Eflp1 {
133 pub oil_pressure: Option<f64>,
135 pub coolant_pressure: Option<f64>,
137}
138
139#[derive(Debug, Clone)]
143pub struct Lfe {
144 pub fuel_rate: Option<f64>,
146 pub instantaneous_fuel_economy: Option<f64>,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct J1939Dtc {
157 pub spn: u32,
159 pub fmi: u8,
161 pub occurrence_count: u8,
163 pub conversion_method: u8,
165}
166
167impl J1939Dtc {
168 pub fn from_bytes(data: &[u8]) -> Option<Self> {
177 if data.len() < 4 {
178 return None;
179 }
180 let spn_low = u16::from_le_bytes([data[0], data[1]]) as u32;
181 let spn_high = ((data[2] >> 5) & 0x07) as u32;
182 let spn = spn_low | (spn_high << 16);
183 let fmi = data[2] & 0x1F;
184 let conversion_method = (data[3] >> 7) & 0x01;
185 let occurrence_count = data[3] & 0x7F;
186
187 Some(Self {
188 spn,
189 fmi,
190 occurrence_count,
191 conversion_method,
192 })
193 }
194
195 pub fn fmi_description(&self) -> &'static str {
197 match self.fmi {
198 0 => "Data Valid But Above Normal Operational Range - Most Severe",
199 1 => "Data Valid But Below Normal Operational Range - Most Severe",
200 2 => "Data Erratic, Intermittent Or Incorrect",
201 3 => "Voltage Above Normal, Or Shorted To High Source",
202 4 => "Voltage Below Normal, Or Shorted To Low Source",
203 5 => "Current Below Normal Or Open Circuit",
204 6 => "Current Above Normal Or Grounded Circuit",
205 7 => "Mechanical System Not Responding Or Out Of Adjustment",
206 8 => "Abnormal Frequency Or Pulse Width Or Period",
207 9 => "Abnormal Update Rate",
208 10 => "Abnormal Rate Of Change",
209 11 => "Root Cause Not Known",
210 12 => "Bad Intelligent Device Or Component",
211 13 => "Out Of Calibration",
212 14 => "Special Instructions",
213 15 => "Data Valid But Above Normal Operating Range - Least Severe",
214 16 => "Data Valid But Above Normal Operating Range - Moderately Severe",
215 17 => "Data Valid But Below Normal Operating Range - Least Severe",
216 18 => "Data Valid But Below Normal Operating Range - Moderately Severe",
217 19 => "Received Network Data In Error",
218 20 => "Data Drifted High",
219 21 => "Data Drifted Low",
220 31 => "Condition Exists",
221 _ => "Reserved",
222 }
223 }
224}
225
226impl std::fmt::Display for J1939Dtc {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 write!(f, "SPN {} FMI {} ({})", self.spn, self.fmi, self.fmi_description())
229 }
230}
231
232const NA_BYTE: u8 = 0xFF;
236const NA_WORD: u16 = 0xFFFF;
237
238fn byte_available(b: u8) -> Option<u8> {
240 if b == NA_BYTE { None } else { Some(b) }
241}
242
243fn word_available(w: u16) -> Option<u16> {
245 if w == NA_WORD { None } else { Some(w) }
246}
247
248pub fn decode_eec1(data: &[u8]) -> Option<Eec1> {
252 if data.len() < 8 {
253 return None;
254 }
255 let torque_mode = data[0] & 0x0F;
257
258 let driver_demand_torque_pct = byte_available(data[1]).map(|b| b as f64 - 125.0);
260
261 let actual_torque_pct = byte_available(data[2]).map(|b| b as f64 - 125.0);
263
264 let rpm_raw = u16::from_le_bytes([data[3], data[4]]);
266 let engine_rpm = word_available(rpm_raw).map(|w| w as f64 * 0.125);
267
268 Some(Eec1 {
269 engine_rpm,
270 driver_demand_torque_pct,
271 actual_torque_pct,
272 torque_mode,
273 })
274}
275
276pub fn decode_ccvs(data: &[u8]) -> Option<Ccvs> {
280 if data.len() < 8 {
281 return None;
282 }
283 let speed_raw = u16::from_le_bytes([data[1], data[2]]);
285 let vehicle_speed = word_available(speed_raw).map(|w| w as f64 / 256.0);
286
287 let brake_bits = (data[3] >> 2) & 0x03;
289 let brake_switch = if brake_bits == 0x03 { None } else { Some(brake_bits == 1) };
290
291 let cruise_bits = data[0] & 0x03;
293 let cruise_active = if cruise_bits == 0x03 { None } else { Some(cruise_bits == 1) };
294
295 Some(Ccvs {
296 vehicle_speed,
297 brake_switch,
298 cruise_active,
299 })
300}
301
302pub fn decode_et1(data: &[u8]) -> Option<Et1> {
306 if data.len() < 4 {
307 return None;
308 }
309 let coolant_temp = byte_available(data[0]).map(|b| b as f64 - 40.0);
311
312 let fuel_temp = byte_available(data[1]).map(|b| b as f64 - 40.0);
314
315 let oil_raw = u16::from_le_bytes([data[2], data[3]]);
317 let oil_temp = word_available(oil_raw).map(|w| w as f64 * 0.03125 - 273.0);
318
319 Some(Et1 {
320 coolant_temp,
321 fuel_temp,
322 oil_temp,
323 })
324}
325
326pub fn decode_eflp1(data: &[u8]) -> Option<Eflp1> {
330 if data.len() < 4 {
331 return None;
332 }
333 let coolant_pressure = byte_available(data[1]).map(|b| b as f64 * 2.0);
335
336 let oil_pressure = byte_available(data[3]).map(|b| b as f64 * 4.0);
338
339 Some(Eflp1 {
340 oil_pressure,
341 coolant_pressure,
342 })
343}
344
345pub fn decode_lfe(data: &[u8]) -> Option<Lfe> {
349 if data.len() < 4 {
350 return None;
351 }
352 let rate_raw = u16::from_le_bytes([data[0], data[1]]);
354 let fuel_rate = word_available(rate_raw).map(|w| w as f64 * 0.05);
355
356 let econ_raw = u16::from_le_bytes([data[2], data[3]]);
358 let instantaneous_fuel_economy = word_available(econ_raw).map(|w| w as f64 / 512.0);
359
360 Some(Lfe {
361 fuel_rate,
362 instantaneous_fuel_economy,
363 })
364}
365
366pub fn decode_dm1(data: &[u8]) -> Vec<J1939Dtc> {
370 if data.len() < 6 {
371 return vec![];
372 }
373 let dtc_data = &data[2..];
375 dtc_data
376 .chunks(4)
377 .filter_map(J1939Dtc::from_bytes)
378 .collect()
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn test_pgn_constants() {
387 assert_eq!(Pgn::EEC1.0, 61444);
388 assert_eq!(Pgn::CCVS.0, 65265);
389 assert_eq!(Pgn::ET1.0, 65262);
390 assert_eq!(Pgn::DM1.0, 65226);
391 }
392
393 #[test]
394 fn test_pgn_name() {
395 assert!(Pgn::EEC1.name().contains("Electronic Engine"));
396 assert!(Pgn::CCVS.name().contains("Vehicle Speed"));
397 assert_eq!(Pgn(99999).name(), "Unknown PGN");
398 }
399
400 #[test]
401 fn test_pgn_display() {
402 let s = format!("{}", Pgn::EEC1);
403 assert!(s.contains("61444"));
404 assert!(s.contains("EEC1"));
405 }
406
407 #[test]
408 fn test_decode_eec1() {
409 let data = [0x00, 155, 155, 0x40, 0x15, 0xFF, 0xFF, 0xFF];
411 let eec1 = decode_eec1(&data).unwrap();
412 assert!((eec1.engine_rpm.unwrap() - 680.0).abs() < 0.2);
413 assert!((eec1.driver_demand_torque_pct.unwrap() - 30.0).abs() < 0.1);
414 assert!((eec1.actual_torque_pct.unwrap() - 30.0).abs() < 0.1);
415 }
416
417 #[test]
418 fn test_decode_eec1_not_available() {
419 let data = [0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
421 let eec1 = decode_eec1(&data).unwrap();
422 assert!(eec1.engine_rpm.is_none());
423 assert!(eec1.driver_demand_torque_pct.is_none());
424 assert!(eec1.actual_torque_pct.is_none());
425 }
426
427 #[test]
428 fn test_decode_eec1_too_short() {
429 assert!(decode_eec1(&[0x00, 0x01]).is_none());
430 }
431
432 #[test]
433 fn test_decode_ccvs() {
434 let data = [0x00, 0x00, 0x1A, 0x00, 0x00, 0x00, 0x00, 0x00];
436 let ccvs = decode_ccvs(&data).unwrap();
437 assert!((ccvs.vehicle_speed.unwrap() - 26.0).abs() < 0.1);
438 assert_eq!(ccvs.brake_switch, Some(false));
439 assert_eq!(ccvs.cruise_active, Some(false));
440 }
441
442 #[test]
443 fn test_decode_ccvs_not_available() {
444 let data = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
445 let ccvs = decode_ccvs(&data).unwrap();
446 assert!(ccvs.vehicle_speed.is_none());
447 assert!(ccvs.brake_switch.is_none());
448 assert!(ccvs.cruise_active.is_none());
449 }
450
451 #[test]
452 fn test_decode_et1() {
453 let data = [90, 60, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF];
455 let et1 = decode_et1(&data).unwrap();
456 assert!((et1.coolant_temp.unwrap() - 50.0).abs() < 0.1);
457 assert!((et1.fuel_temp.unwrap() - 20.0).abs() < 0.1);
458 }
459
460 #[test]
461 fn test_decode_et1_not_available() {
462 let data = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
463 let et1 = decode_et1(&data).unwrap();
464 assert!(et1.coolant_temp.is_none());
465 assert!(et1.fuel_temp.is_none());
466 assert!(et1.oil_temp.is_none());
467 }
468
469 #[test]
470 fn test_decode_eflp1() {
471 let data = [0xFF, 50, 0xFF, 100, 0xFF, 0xFF, 0xFF, 0xFF];
473 let eflp1 = decode_eflp1(&data).unwrap();
474 assert!((eflp1.coolant_pressure.unwrap() - 100.0).abs() < 0.1);
475 assert!((eflp1.oil_pressure.unwrap() - 400.0).abs() < 0.1);
476 }
477
478 #[test]
479 fn test_decode_eflp1_not_available() {
480 let data = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
481 let eflp1 = decode_eflp1(&data).unwrap();
482 assert!(eflp1.oil_pressure.is_none());
483 assert!(eflp1.coolant_pressure.is_none());
484 }
485
486 #[test]
487 fn test_decode_lfe() {
488 let data = [100, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF];
490 let lfe = decode_lfe(&data).unwrap();
491 assert!((lfe.fuel_rate.unwrap() - 5.0).abs() < 0.1);
492 }
493
494 #[test]
495 fn test_decode_lfe_not_available() {
496 let data = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
497 let lfe = decode_lfe(&data).unwrap();
498 assert!(lfe.fuel_rate.is_none());
499 assert!(lfe.instantaneous_fuel_economy.is_none());
500 }
501
502 #[test]
503 fn test_j1939_dtc_from_bytes() {
504 let data = [0xBE, 0x00, 0x02, 0x01]; let dtc = J1939Dtc::from_bytes(&data).unwrap();
507 assert_eq!(dtc.spn, 190);
508 assert_eq!(dtc.fmi, 2);
509 assert_eq!(dtc.occurrence_count, 1);
510 }
511
512 #[test]
513 fn test_j1939_dtc_from_bytes_too_short() {
514 assert!(J1939Dtc::from_bytes(&[0x00, 0x01]).is_none());
515 }
516
517 #[test]
518 fn test_j1939_dtc_display() {
519 let dtc = J1939Dtc {
520 spn: 190,
521 fmi: 2,
522 occurrence_count: 1,
523 conversion_method: 0,
524 };
525 let s = format!("{}", dtc);
526 assert!(s.contains("SPN 190"));
527 assert!(s.contains("FMI 2"));
528 assert!(s.contains("Erratic"));
529 }
530
531 #[test]
532 fn test_j1939_dtc_fmi_descriptions() {
533 let dtc = J1939Dtc { spn: 0, fmi: 0, occurrence_count: 0, conversion_method: 0 };
534 assert!(dtc.fmi_description().contains("Above Normal"));
535 let dtc = J1939Dtc { spn: 0, fmi: 11, occurrence_count: 0, conversion_method: 0 };
536 assert!(dtc.fmi_description().contains("Root Cause Not Known"));
537 }
538
539 #[test]
540 fn test_decode_dm1() {
541 let data = [0x00, 0x00, 0xBE, 0x00, 0x02, 0x01];
543 let dtcs = decode_dm1(&data);
544 assert_eq!(dtcs.len(), 1);
545 assert_eq!(dtcs[0].spn, 190);
546 assert_eq!(dtcs[0].fmi, 2);
547 }
548
549 #[test]
550 fn test_decode_dm1_empty() {
551 assert!(decode_dm1(&[0x00, 0x00]).is_empty());
552 }
553
554 #[test]
555 fn test_decode_dm1_multiple_dtcs() {
556 let data = [
557 0x00, 0x00, 0xBE, 0x00, 0x02, 0x01, 0x64, 0x00, 0x03, 0x02, ];
561 let dtcs = decode_dm1(&data);
562 assert_eq!(dtcs.len(), 2);
563 assert_eq!(dtcs[0].spn, 190);
564 assert_eq!(dtcs[1].spn, 100);
565 }
566}