Section 1 backend: VIN/Mode-09, readiness monitors, freeze-frame, trip/perf

obdcore additions (all standard SAE J1979, vehicle-agnostic, hardware-free
tested):
- obdservices.py: decode_vin (Mode 09), decode_readiness (Mode 01 PID 01 I-M
  monitors + MIL + DTC count, spark/diesel monitor sets), freeze-frame PID set.
- link.py: ElmLink.read_vehicle_info (VIN/cal/ECU), read_readiness, read_freeze_frame.
- trip.py: TripComputer (MAF-based MPG + trip totals) and PerformanceMeter
  (0-60 / 1/4-mile with launch detection).
- mock.py: speed/MAF/readiness + service stubs for GUI mock mode.
- tests/test_services.py: VIN, readiness bit decode, trip math, 0-60/quarter.

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-06-30 19:37:48 -04:00
parent 310d5a3497
commit 6c1ee0c81d
5 changed files with 341 additions and 2 deletions
+35
View File
@@ -172,6 +172,41 @@ class ElmLink:
data = self._bytes(lines)
return 0x44 in data or ("OK" in "".join(lines).upper())
# -- standard OBD services (Mode 09 / 01-01 / 02) --
def read_vehicle_info(self, timeout=2.0):
"""Mode 09: VIN + calibration IDs + ECU name. Returns a dict."""
from . import obdservices as svc
vin = svc.decode_vin(self._bytes(self.cmd("0902", timeout=timeout)))
cal = svc.decode_ascii_block(self._bytes(self.cmd("0904", timeout=timeout)), 0x04)
ecu = svc.decode_ascii_block(self._bytes(self.cmd("090A", timeout=timeout)), 0x0A)
return {"vin": vin, "calibration": cal, "ecu_name": ecu}
def read_readiness(self, timeout=1.0):
"""Mode 01 PID 01: MIL, DTC count, and I-M readiness monitors."""
from . import obdservices as svc
data = self.read_m01("01", 4, timeout=timeout)
return svc.decode_readiness(data) if data else None
def read_freeze_frame(self, timeout=0.6):
"""Mode 02: the DTC that set the freeze frame + the standard PID snapshot."""
from . import obdservices as svc
out = {"dtc": None, "values": []}
d = self._bytes(self.cmd("0202", timeout=timeout))
if 0x42 in d:
r = d[d.index(0x42) + 2:] # after '42 02'
if len(r) >= 2 and (r[0] or r[1]):
out["dtc"] = decode_dtc(r[0], r[1])
for name, pid, nbytes, dec, unit in svc.FREEZE_PIDS:
dd = self._bytes(self.cmd(f"02{pid}00", timeout=timeout))
if 0x42 in dd:
payload = dd[dd.index(0x42) + 3:dd.index(0x42) + 3 + nbytes]
if len(payload) == nbytes:
try:
out["values"].append((name, dec(payload), unit))
except Exception:
pass
return out
def close(self):
try:
self.ser.close()