"""PID + DTC data model and registry, backed by a vehicle Profile. The actual PID numbers, scaling formulas, and DTC meanings live in JSON vehicle profiles under profiles/ (data, not code) so the app is vehicle- agnostic and others can contribute profiles. This module is the in-memory model + lookups; profile.py loads/saves the JSON. """ from dataclasses import dataclass, field from typing import Callable, Tuple @dataclass class Pid: key: str name: str mode: str = "22" # "01" | "22" | "atrv" | "derived" pid: str = "" # hex: "1446" (m22) or "0C" (m01) nbytes: int = 2 formula: str = "" # scaling expr in A/B/... (raw) or dep keys (derived) decode: Callable = None # built from formula by profile loader unit: str = "" group: str = "misc" # fuel | ficm | air | engine | driveline | power | misc vmin: float = 0.0 vmax: float = 100.0 confidence: str = "verified" # verified | doc | tentative round: int = None # display rounding (None=raw float, 0=int) deps: Tuple[str, ...] = () notes: str = "" # optional gauge warning zones (all None = neutral, no redline drawn). # high-side: value >= threshold -> warn/redline. low-side: value <= -> warn/redline. warn_hi: float = None redline_hi: float = None warn_lo: float = None redline_lo: float = None def zone(self, v): """Classify a value as 'crit' (red), 'warn' (amber), or 'ok' (green).""" if v is None: return "ok" if self.redline_hi is not None and v >= self.redline_hi: return "crit" if self.redline_lo is not None and v <= self.redline_lo: return "crit" if self.warn_hi is not None and v >= self.warn_hi: return "warn" if self.warn_lo is not None and v <= self.warn_lo: return "warn" return "ok" @dataclass class Dtc: code: str desc: str system: str = "powertrain" no_start: bool = False causes: str = "" class PidRegistry: """In-memory PID set + presets for the active vehicle profile.""" def __init__(self, profile): self.profile = profile self._by_key = {p.key: p for p in profile.pids} self.presets = dict(profile.presets) def get(self, key): return self._by_key.get(key) def all(self): return list(self._by_key.values()) def group(self, g): return [p for p in self._by_key.values() if p.group == g] def preset(self, name): return [self._by_key[k] for k in self.presets.get(name, []) if k in self._by_key] def preset_names(self): return list(self.presets.keys()) class DtcDatabase: def __init__(self, profile): self._db = {d.code: d for d in profile.dtcs} def get(self, code): return self._db.get(code) or Dtc(code, "(unknown - look up this code)") def all(self): return list(self._db.values())