4a4daf3fa0
Compiled by the generic-dtc-db workflow (P0/U0/C0/B0 standard codes, system tags + no-start flags). Lives in profiles/_data/generic-dtcs.json (bundled with profiles/, not listed as a vehicle profile). DtcDatabase.get now falls back: profile code -> generic code -> unknown, so any standard code resolves to a description while vehicle profiles still override (e.g. P0148 keeps the 6.0 text). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
116 lines
3.7 KiB
Python
116 lines
3.7 KiB
Python
"""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())
|
|
|
|
|
|
_GENERIC = None
|
|
|
|
|
|
def _generic_dtcs():
|
|
"""Lazy-load the bundled generic SAE DTC database (code -> Dtc)."""
|
|
global _GENERIC
|
|
if _GENERIC is None:
|
|
_GENERIC = {}
|
|
try:
|
|
import json
|
|
import os
|
|
from .profile import profiles_dir
|
|
path = os.path.join(profiles_dir(), "_data", "generic-dtcs.json")
|
|
for d in json.load(open(path)).get("dtcs", []):
|
|
_GENERIC[d["code"]] = Dtc(code=d["code"], desc=d.get("desc", ""),
|
|
system=d.get("system", "powertrain"),
|
|
no_start=d.get("no_start", False))
|
|
except Exception:
|
|
pass
|
|
return _GENERIC
|
|
|
|
|
|
class DtcDatabase:
|
|
def __init__(self, profile):
|
|
self._db = {d.code: d for d in profile.dtcs} # profile codes take priority
|
|
|
|
def get(self, code):
|
|
return (self._db.get(code) or _generic_dtcs().get(code)
|
|
or Dtc(code, "(unknown - look up this code)"))
|
|
|
|
def all(self):
|
|
return list(self._db.values())
|