Files
obdash/obdcore/registry.py
T
justin d893ff383a Gauge redline zones + C/F units toggle + cleaner dials
Gauges:
- Optional per-metric warning zones (warn_hi/redline_hi/warn_lo/redline_lo) in
  the profile schema; gauges draw colored redline/warn bands and color the
  needle + readout by zone. Default neutral when unset (no false redline).
- Removed the value progress-arc fill (it dominated the dial / looked wrong) ->
  clean tach face: bezel, ticks, numeric scale, needle, redline band, readout.
- Auto-derivation rejected: bad direction/threshold vary per metric, so zones
  are config (with a sensible neutral default).

Units:
- New Units menu: Temperature C / F. Converts gauges, graph, table, and PID
  browser (values, scale, zones, unit labels) at display time; data stays C.

Ford 6.0 profile: zones for ICP (red<500), FICM (red<40/amber40-48/green48+),
ECT/EOT (high redline), RPM (redline 3800), boost, battery; tightened FICM
(38-52) and battery (9-15) ranges so redline bands land sensibly.

Docs: profiles/PROFILE_SPEC.md -- canonical, AI-agent-ready profile spec
(schema, formula language, zones, confidence, rules); README points to it.

Validated headless: zones parse/classify, F conversion (112C->233.6F, zones
converted), gauges render; obdcore + diagnostics tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 15:52:49 -04:00

93 lines
2.9 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())
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())