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
+70
View File
@@ -0,0 +1,70 @@
"""Tests for the standard OBD services + trip/performance (no hardware)."""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from obdcore import obdservices as svc
from obdcore.trip import TripComputer, PerformanceMeter, KMH_PER_MPH
def test_decode_vin():
vin = "1FMZU73E12ZA12345"
data = [0x49, 0x02, 0x01] + [ord(c) for c in vin]
assert svc.decode_vin(data) == vin
assert svc.decode_vin([0x41, 0x00]) is None # not a mode-09 response
print(" VIN decode: OK")
def test_decode_readiness():
# A=MIL off/0 DTCs, B=3 continuous supported+ready, C=Cat/O2/O2htr supported,
# D=O2 sensor incomplete
r = svc.decode_readiness([0x00, 0x07, 0x61, 0x20])
assert r["mil"] is False and r["dtc_count"] == 0 and r["ignition"] == "spark"
by = {m["name"]: m["ready"] for m in r["monitors"]}
assert by["Misfire"] and by["Fuel System"] and by["Components"]
assert by["Catalyst"] is True and by["O2 Sensor"] is False
assert r["total"] == 6 and r["ready_count"] == 5
# diesel flag
rc = svc.decode_readiness([0x80, 0x0F, 0x00, 0x00])
assert rc["mil"] is True and rc["ignition"] == "compression"
print(f" readiness decode: {r['ready_count']}/{r['total']} ready, O2 not ready: OK")
def test_trip_computer():
tc = TripComputer()
t = 0.0
for _ in range(360): # 6 min at 1 Hz, 96.56 km/h (60 mph), 12 g/s
tc.update(t, 96.56, 12.0)
t += 1.0
s = tc.stats()
assert 5.5 < s["distance_mi"] < 6.5, s # 60mph * 0.1h = 6 mi
assert s["avg_mpg"] > 5 and s["avg_mpg"] < 60, s
inst = tc.instant_mpg(96.56, 12.0)
assert inst > 0
print(f" trip: {s['distance_mi']}mi, {s['avg_mpg']} avg mpg, "
f"{inst:.1f} inst: OK")
def test_performance_meter():
pm = PerformanceMeter()
t = 0.0
# parked
for _ in range(3):
pm.update(t, 0.0); t += 0.5
# accelerate 0 -> 70 mph over 7s (mph), then cruise to cover 1/4 mile
for i in range(1, 200):
t = 1.5 + i * 0.1
mph = min(70.0, (t - 1.5) * 10.0) # 10 mph/s
pm.update(t, mph * KMH_PER_MPH)
assert pm.best_0_60 is not None, "should have timed 0-60"
assert 5.0 < pm.best_0_60 < 8.0, pm.best_0_60 # ~6s at 10mph/s
assert pm.best_quarter is not None, "should have timed 1/4 mile"
print(f" performance: 0-60 {pm.best_0_60}s, 1/4mi {pm.best_quarter}s: OK")
if __name__ == "__main__":
for fn in [test_decode_vin, test_decode_readiness, test_trip_computer,
test_performance_meter]:
fn()
print("\nALL SERVICE TESTS PASS")