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:
+32
-4
@@ -11,7 +11,9 @@ import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from obdcore import PidRegistry, TimeSeriesStore, PollScheduler, CsvRecorder, replay_csv
|
||||
from obdcore import (PidRegistry, TimeSeriesStore, PollScheduler, CsvRecorder,
|
||||
replay_csv, load_default, load_profile, default_profile_path,
|
||||
list_profiles, compile_formula, FormulaError)
|
||||
from obdcore.mock import MockLink
|
||||
|
||||
|
||||
@@ -28,7 +30,7 @@ class FakeClock:
|
||||
|
||||
def _setup(specs):
|
||||
clk = FakeClock()
|
||||
reg = PidRegistry()
|
||||
reg = PidRegistry(load_default())
|
||||
store = TimeSeriesStore()
|
||||
link = MockLink(clock=clk)
|
||||
sch = PollScheduler(link, reg, store, clock=clk)
|
||||
@@ -36,8 +38,33 @@ def _setup(specs):
|
||||
return clk, reg, store, sch
|
||||
|
||||
|
||||
def test_profiles_load_and_validate():
|
||||
profs = list_profiles()
|
||||
assert any("ford-6.0" in p for p, _ in profs), "ford profile should be listed"
|
||||
for path, meta in profs:
|
||||
prof = load_profile(path) # compiles every formula -> raises if bad
|
||||
assert prof.meta.get("name")
|
||||
assert all(p.decode or p.mode == "atrv" for p in prof.pids)
|
||||
print(f" {len(profs)} profiles load + compile clean: OK")
|
||||
|
||||
|
||||
def test_formula_is_sandboxed():
|
||||
# legit
|
||||
fn = compile_formula("(A*256+B)*0.57", "ABCDEFGH")
|
||||
assert abs(fn({"A": 0, "B": 22}) - 12.54) < 0.01
|
||||
# hostile / disallowed -> rejected at compile
|
||||
for bad in ("__import__('os').system('x')", "open('/etc/passwd')",
|
||||
"A.__class__", "Z+1", "A if B else C"):
|
||||
try:
|
||||
compile_formula(bad, "ABC")
|
||||
raise AssertionError(f"should have rejected: {bad}")
|
||||
except FormulaError:
|
||||
pass
|
||||
print(" formula evaluator rejects code/unknowns: OK")
|
||||
|
||||
|
||||
def test_registry_decoders_match_truck_bytes():
|
||||
reg = PidRegistry()
|
||||
reg = PidRegistry(load_default())
|
||||
cases = {
|
||||
"ICP": ([0x00, 0x16], 12.5), "EBP": ([0x01, 0x8F], 14.46),
|
||||
"MAP": ([0x01, 0x89], 14.25), "BARO": ([0x01, 0x88], 14.21),
|
||||
@@ -110,7 +137,8 @@ def test_record_replay_roundtrip(tmp_path=None):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for fn in [test_registry_decoders_match_truck_bytes, test_crank_ramp_and_peak,
|
||||
for fn in [test_profiles_load_and_validate, test_formula_is_sandboxed,
|
||||
test_registry_decoders_match_truck_bytes, test_crank_ramp_and_peak,
|
||||
test_derived_boost_channel, test_dead_pid_parks_and_revives,
|
||||
test_record_replay_roundtrip]:
|
||||
fn()
|
||||
|
||||
Reference in New Issue
Block a user