Fix #9: DTC/freeze-frame parsing (phantom codes, Mode 02, hex frame index)

- parse_dtcs CAN branch is now message-aware: each ECU reply '<svc> <count>
  <pairs>' has its header stripped per-message, instead of flattening all lines
  and stripping svc+count once. With multiple ECUs the old code ate the second
  header as a DTC pair -> phantom codes. Critically, it does NOT blind-scan for
  svc (0x43 is a legal DTC first byte: C03xx) — a numbered ISO-TP continuation
  is distinguished by its 'N:' frame-index prefix, not by value.
- _line_bytes strips hex frame indices A:-F: (ISO-TP index cycles 0-F), not just
  0-9, so consecutive frames past the 10th aren't dropped.
- read_freeze_frame sends the correct '020200' (svc 02, PID 02, frame 00) and
  skips SID+PID+frame (+3), fixing the off-by-one that mis-read the freeze DTC.
- tests/test_dtc_parse.py: single-frame, multi-ECU (no phantom), numbered
  multiframe with a real C03xx continuation, hex index, non-CAN legacy.

Closes #9

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
This commit is contained in:
2026-07-01 19:36:35 -04:00
parent 23c92018c1
commit fa7225d6dc
2 changed files with 89 additions and 10 deletions
+63
View File
@@ -0,0 +1,63 @@
"""DTC parsing correctness (CAN multi-ECU, ISO-TP frame indices, single-frame)."""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from obdcore.link import parse_dtcs, _line_bytes, decode_dtc
def test_single_frame_can():
# 43 02 <P0133> <P0420>
lines = ["43 02 01 33 04 20"]
assert parse_dtcs(lines, 0x43, True) == ["P0133", "P0420"]
print(" single-frame CAN: OK")
def test_multi_ecu_no_phantom_codes():
# Two ECUs each reply "43 <count> ...". The old flatten-and-strip-once code
# ate the second header (43 01) as a DTC pair -> phantom code. Each header
# must be stripped per-message.
lines = ["43 01 01 33", "43 01 04 20"]
got = parse_dtcs(lines, 0x43, True)
assert got == ["P0133", "P0420"], got
assert not any(d.startswith("C0301") or "4301" in d for d in got)
print(f" multi-ECU CAN, no phantom codes: {got}: OK")
def test_iso_tp_numbered_frames_hex_index():
# Multiframe with numbered continuations, including hex indices A:..F:.
# First frame "0:" starts with 43 08; continuations carry raw pairs. A
# continuation starting with 0x43 (a real C03xx code) must NOT be mistaken
# for a new header because it is a numbered continuation.
lines = [
"0: 43 08 01 33 04 20",
"1: 05 67 07 E4 43 45", # ...last pair 43 45 == C0345 (real code, starts 0x43)
"2: 09 96 00 00 00 00",
]
got = parse_dtcs(lines, 0x43, True)
assert got == ["P0133", "P0420", "P0567", "P07E4", "C0345", "P0996"], got
print(f" numbered multiframe + C03xx continuation not misread: {got}: OK")
def test_hex_frame_index_stripped():
assert _line_bytes("A: 11 22") == [0x11, 0x22] # hex index A: now stripped
assert _line_bytes("F:1122") == [0x11, 0x22]
print(" _line_bytes strips hex ISO-TP frame indices A:-F:: OK")
def test_legacy_non_can_still_works():
# 43 header per line (non-CAN)
lines = ["43 01 33 00 00", "43 04 20 00 00"]
got = parse_dtcs(lines, 0x43, False)
assert "P0133" in got and "P0420" in got, got
print(f" non-CAN legacy parse intact: {got}: OK")
if __name__ == "__main__":
test_single_frame_can()
test_multi_ecu_no_phantom_codes()
test_iso_tp_numbered_frames_hex_index()
test_hex_frame_index_stripped()
test_legacy_non_can_still_works()
print("\nALL DTC PARSE TESTS PASS")