Make app vehicle-agnostic: JSON vehicle profiles + menu bar

Vehicle data is now DATA, not code. PIDs/scaling/DTCs/presets live in
profiles/*.json; the app loads them at runtime, so it works across vehicles
and others can contribute profiles (open source).

Core:
- obdcore/formula.py: safe AST evaluator for scaling formulas (A/B/... byte
  vars, Torque/FORScan convention). Only arithmetic/bitwise + min/max/abs/
  round/int/float; names/attrs/arbitrary calls rejected at load -> a community
  profile CANNOT execute code.
- obdcore/profile.py: load/save/list profiles; compiles each formula into a
  decode callable. registry.py now profile-backed (PidRegistry/DtcDatabase
  take a Profile); hardcoded Ford table removed.
- store.py: clear()/snapshot()/export_csv() for capture management.

Profiles:
- profiles/ford-6.0-powerstroke.json (27 PIDs, verified formulas, DTCs)
- profiles/generic-obd2.json (standard SAE Mode-01 base, any vehicle)
- profiles/README.md (schema + formula language + contributing)

GUI:
- Menu bar: File (new/record/export/replay capture, quit), Profile (switch/
  load/import/reload/edit-JSON/export, live profile list), View (Graph/Table
  views, gauges P2, toggle PID dock, normalize, light/dark theme), Help
  (about/confidence legend/profile info).
- PID browser + presets rebuild on profile switch; added Table view; raw-JSON
  profile editor dialog (validates schema+formulas before saving).

Tests: profiles load+compile, formula sandbox rejects hostile input, decoders
still match real truck bytes, crank/derived/dead-PID/replay -- all pass.

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 14:34:33 -04:00
parent 45691334e1
commit f3f0bf2a77
12 changed files with 966 additions and 295 deletions
+29
View File
@@ -87,6 +87,35 @@ class TimeSeriesStore:
with self._lock:
return list(self._ch.keys())
def clear(self):
"""Empty every channel's history + min/max (start a fresh capture)."""
with self._lock:
chans = list(self._ch.values())
for c in chans:
with c._lock:
c.buf.clear()
c.lo = c.hi = c.last_v = c.last_t = None
def snapshot(self):
"""Return {key: [(t, v), ...]} of all current channel history."""
with self._lock:
chans = dict(self._ch)
return {k: c.series() for k, c in chans.items()}
def export_csv(store, path):
"""Write a store's current buffers to a long-format CSV (t,key,value)."""
rows = []
for key, series in store.snapshot().items():
for t, v in series:
rows.append((t, key, v))
rows.sort(key=lambda r: r[0])
with open(path, "w") as f:
f.write("t,key,value\n")
for t, key, v in rows:
f.write(f"{t:.3f},{key},{'' if v is None else v}\n")
return path
class CsvRecorder:
"""Long-format session recorder: one row per sample (t,key,value).