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
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 180 KiB |
+60
-10
@@ -42,6 +42,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
self.curves = {}
|
||||
self._color_i = 0
|
||||
self._theme = "dark"
|
||||
self._temp_f = False # display temperatures in F instead of C
|
||||
|
||||
self._build_menubar()
|
||||
self._build_connection_bar()
|
||||
@@ -97,6 +98,15 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
viewm.addSeparator()
|
||||
self.theme_act = self._act(viewm, "Light Theme", self._toggle_theme, checkable=True)
|
||||
|
||||
unitsm = mb.addMenu("&Units")
|
||||
self._temp_grp = QtGui.QActionGroup(self); self._temp_grp.setExclusive(True)
|
||||
self.temp_c_act = self._act(unitsm, "Temperature: °C", lambda: self._set_temp(False),
|
||||
checkable=True)
|
||||
self.temp_f_act = self._act(unitsm, "Temperature: °F", lambda: self._set_temp(True),
|
||||
checkable=True)
|
||||
self._temp_grp.addAction(self.temp_c_act); self._temp_grp.addAction(self.temp_f_act)
|
||||
self.temp_c_act.setChecked(True)
|
||||
|
||||
helpm = mb.addMenu("&Help")
|
||||
self._act(helpm, "About OBDash", self._about)
|
||||
self._act(helpm, "PID Confidence Legend", self._legend)
|
||||
@@ -525,7 +535,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
p = self.ctl.reg.get(key)
|
||||
color = CURVE_COLORS[self._color_i % len(CURVE_COLORS)]; self._color_i += 1
|
||||
self.curves[key] = color # curves maps key -> color
|
||||
self._graph().add_curve(key, f"{p.name} ({p.unit})", p.unit, color)
|
||||
self._graph().add_curve(key, f"{p.name} ({self._dunit(p)})", self._dunit(p), color)
|
||||
self._refresh_gauges()
|
||||
|
||||
def _remove_curve(self, key):
|
||||
@@ -543,15 +553,51 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
active = self._graph()
|
||||
for key, color in self.curves.items():
|
||||
p = self.ctl.reg.get(key)
|
||||
active.add_curve(key, f"{p.name} ({p.unit})", p.unit, color)
|
||||
active.add_curve(key, f"{p.name} ({self._dunit(p)})", self._dunit(p), color)
|
||||
self._apply_theme()
|
||||
self._redraw_curves(static=not self.ctl.connected)
|
||||
|
||||
# ---------- display units (C / F) ----------
|
||||
def _is_temp(self, p):
|
||||
return p.unit == "C"
|
||||
|
||||
def _dval(self, p, v):
|
||||
if v is None:
|
||||
return None
|
||||
return v * 9 / 5 + 32 if (self._temp_f and self._is_temp(p)) else v
|
||||
|
||||
def _dunit(self, p):
|
||||
return "F" if (self._temp_f and self._is_temp(p)) else p.unit
|
||||
|
||||
def _dzones(self, p):
|
||||
z = {"warn_hi": p.warn_hi, "redline_hi": p.redline_hi,
|
||||
"warn_lo": p.warn_lo, "redline_lo": p.redline_lo}
|
||||
if self._temp_f and self._is_temp(p):
|
||||
z = {k: (None if x is None else x * 9 / 5 + 32) for k, x in z.items()}
|
||||
return z
|
||||
|
||||
def _set_temp(self, to_f):
|
||||
if to_f == self._temp_f:
|
||||
return
|
||||
self._temp_f = to_f
|
||||
# graph: relabel + rescale by rebuilding curves on the active widget
|
||||
self.multi.clear(); self.single.clear()
|
||||
active = self._graph()
|
||||
for key, color in self.curves.items():
|
||||
p = self.ctl.reg.get(key)
|
||||
active.add_curve(key, f"{p.name} ({self._dunit(p)})", self._dunit(p), color)
|
||||
for key, r in getattr(self, "_table_row", {}).items():
|
||||
self.table.item(r, 2).setText(self._dunit(self.ctl.reg.get(key)))
|
||||
self._refresh_gauges()
|
||||
if self.ctl.connected:
|
||||
self._tick()
|
||||
|
||||
def _refresh_gauges(self):
|
||||
specs = []
|
||||
for key, color in self.curves.items():
|
||||
p = self.ctl.reg.get(key)
|
||||
specs.append((key, p.name, p.unit, p.vmin, p.vmax, color))
|
||||
specs.append((key, p.name, self._dunit(p), self._dval(p, p.vmin),
|
||||
self._dval(p, p.vmax), color, self._dzones(p)))
|
||||
self.gauges.rebuild(specs)
|
||||
|
||||
# ---------- view ----------
|
||||
@@ -584,7 +630,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
self._table_row[p.key] = r
|
||||
self.table.setItem(r, 0, QtWidgets.QTableWidgetItem(p.name))
|
||||
self.table.setItem(r, 1, QtWidgets.QTableWidgetItem("--"))
|
||||
self.table.setItem(r, 2, QtWidgets.QTableWidgetItem(p.unit))
|
||||
self.table.setItem(r, 2, QtWidgets.QTableWidgetItem(self._dunit(p)))
|
||||
self.table.setItem(r, 3, QtWidgets.QTableWidgetItem("--"))
|
||||
self.table.setItem(r, 4, QtWidgets.QTableWidgetItem("--"))
|
||||
self.table.setItem(r, 5, QtWidgets.QTableWidgetItem(p.confidence))
|
||||
@@ -674,7 +720,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
if normalize and p.vmax != p.vmin:
|
||||
ys.append((v - p.vmin) / (p.vmax - p.vmin) * 100.0)
|
||||
else:
|
||||
ys.append(v)
|
||||
ys.append(self._dval(p, v))
|
||||
active.set_data(key, xs, ys)
|
||||
if normalize:
|
||||
self.single.set_y_label("% of range")
|
||||
@@ -686,19 +732,23 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
for key, it in self._items.items():
|
||||
v = self.ctl.store.latest(key)
|
||||
p = self.ctl.reg.get(key)
|
||||
txt = "--" if v is None else f"{v:g} {p.unit}".strip()
|
||||
dv = self._dval(p, v)
|
||||
txt = "--" if dv is None else f"{dv:g} {self._dunit(p)}".strip()
|
||||
it.setText(1, txt)
|
||||
if key in self._table_row:
|
||||
r = self._table_row[key]
|
||||
lo, hi = self.ctl.store.minmax(key)
|
||||
self.table.item(r, 1).setText("--" if v is None else f"{v:g}")
|
||||
self.table.item(r, 3).setText("--" if lo is None else f"{lo:g}")
|
||||
self.table.item(r, 4).setText("--" if hi is None else f"{hi:g}")
|
||||
dlo, dhi = self._dval(p, lo), self._dval(p, hi)
|
||||
self.table.item(r, 1).setText("--" if dv is None else f"{dv:g}")
|
||||
self.table.item(r, 3).setText("--" if dlo is None else f"{dlo:g}")
|
||||
self.table.item(r, 4).setText("--" if dhi is None else f"{dhi:g}")
|
||||
self.tree.blockSignals(False)
|
||||
if self.stack.currentIndex() == 2: # gauge view
|
||||
for key in self.curves:
|
||||
p = self.ctl.reg.get(key)
|
||||
lo, hi = self.ctl.store.minmax(key)
|
||||
self.gauges.set_value(key, self.ctl.store.latest(key), peak=hi)
|
||||
self.gauges.set_value(key, self._dval(p, self.ctl.store.latest(key)),
|
||||
peak=self._dval(p, hi))
|
||||
else:
|
||||
self._redraw_curves()
|
||||
|
||||
|
||||
+49
-14
@@ -203,13 +203,20 @@ class ArcGauge(QtWidgets.QWidget):
|
||||
"""A 270-degree arc gauge for a single signal: filled arc to the current
|
||||
value, peak tick, big numeric readout, name + unit."""
|
||||
|
||||
def __init__(self, name, unit, vmin, vmax, accent="#3cb44b"):
|
||||
_AMBER = "#e6a23c"
|
||||
_RED = "#e6194B"
|
||||
_NEEDLE = {"ok": "#e8e8e8", "warn": _AMBER, "crit": _RED}
|
||||
|
||||
def __init__(self, name, unit, vmin, vmax, accent="#3cb44b", zones=None):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.unit = unit
|
||||
self.vmin = float(vmin)
|
||||
self.vmax = float(vmax) if vmax != vmin else float(vmin) + 1.0
|
||||
self.accent = QtGui.QColor(accent)
|
||||
z = zones or {}
|
||||
self.warn_hi = z.get("warn_hi"); self.redline_hi = z.get("redline_hi")
|
||||
self.warn_lo = z.get("warn_lo"); self.redline_lo = z.get("redline_lo")
|
||||
self.value = None
|
||||
self.peak = None
|
||||
self.setMinimumSize(172, 176)
|
||||
@@ -222,6 +229,19 @@ class ArcGauge(QtWidgets.QWidget):
|
||||
def _frac(self, v):
|
||||
return max(0.0, min(1.0, (v - self.vmin) / (self.vmax - self.vmin)))
|
||||
|
||||
def _zone(self, v):
|
||||
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"
|
||||
|
||||
# gauge geometry: a 270-degree dial, gap at the bottom (like a tach)
|
||||
_START = 225.0 # value=min angle (math degrees, 0=3 o'clock, CCW+)
|
||||
_SWEEP = 270.0 # clockwise as value rises
|
||||
@@ -248,14 +268,26 @@ class ArcGauge(QtWidgets.QWidget):
|
||||
p.setBrush(QtGui.QColor("#0c0c0c"))
|
||||
p.drawEllipse(QtCore.QPointF(cx, cy), R, R)
|
||||
|
||||
# value progress arc just inside the rim
|
||||
rimrect = QtCore.QRectF(cx - R + 7, cy - R + 7, 2 * (R - 7), 2 * (R - 7))
|
||||
if self.value is not None:
|
||||
pen = QtGui.QPen(QtGui.QColor(self.accent), 4)
|
||||
# warning / redline zone bands on the outer ring (like a tach redline)
|
||||
ringrect = QtCore.QRectF(cx - R + 3, cy - R + 3, 2 * (R - 3), 2 * (R - 3))
|
||||
|
||||
def band(fa, fb, color):
|
||||
sa = self._ang(fa); span = self._ang(fb) - self._ang(fa)
|
||||
pen = QtGui.QPen(QtGui.QColor(color), 4)
|
||||
pen.setCapStyle(QtCore.Qt.FlatCap)
|
||||
p.setPen(pen)
|
||||
p.drawArc(rimrect, int(self._START * 16),
|
||||
int(-self._SWEEP * self._frac(self.value) * 16))
|
||||
p.drawArc(ringrect, int(sa * 16), int(span * 16))
|
||||
|
||||
if self.redline_hi is not None:
|
||||
band(self._frac(self.redline_hi), 1.0, self._RED)
|
||||
if self.warn_hi is not None:
|
||||
top = self._frac(self.redline_hi) if self.redline_hi is not None else 1.0
|
||||
band(self._frac(self.warn_hi), top, self._AMBER)
|
||||
if self.redline_lo is not None:
|
||||
band(0.0, self._frac(self.redline_lo), self._RED)
|
||||
if self.warn_lo is not None:
|
||||
bot = self._frac(self.redline_lo) if self.redline_lo is not None else 0.0
|
||||
band(bot, self._frac(self.warn_lo), self._AMBER)
|
||||
|
||||
# tick marks + numeric scale
|
||||
majors = 6
|
||||
@@ -282,7 +314,8 @@ class ArcGauge(QtWidgets.QWidget):
|
||||
p.setPen(QtGui.QPen(QtGui.QColor("#e6c84b"), 2))
|
||||
p.drawLine(pt(a, R - 3), pt(a, R - 14))
|
||||
|
||||
# needle
|
||||
zcol = QtGui.QColor(self._NEEDLE[self._zone(self.value)])
|
||||
# needle (colored by zone: white=ok, amber=warn, red=redline)
|
||||
if self.value is not None:
|
||||
a = self._ang(self._frac(self.value))
|
||||
tip = pt(a, R - 16)
|
||||
@@ -293,14 +326,14 @@ class ArcGauge(QtWidgets.QWidget):
|
||||
path.moveTo(b1); path.lineTo(tip); path.lineTo(b2); path.lineTo(tail)
|
||||
path.closeSubpath()
|
||||
p.setPen(QtCore.Qt.NoPen)
|
||||
p.setBrush(QtGui.QColor("#e6194B"))
|
||||
p.setBrush(zcol)
|
||||
p.drawPath(path)
|
||||
# hub
|
||||
p.setBrush(QtGui.QColor("#999")); p.setPen(QtCore.Qt.NoPen)
|
||||
p.drawEllipse(QtCore.QPointF(cx, cy), 5, 5)
|
||||
|
||||
# digital readout (lower face)
|
||||
p.setPen(QtGui.QColor("#fff"))
|
||||
# digital readout (lower face) -- colored by zone
|
||||
p.setPen(zcol)
|
||||
fnt.setPointSize(13); fnt.setBold(True); p.setFont(fnt)
|
||||
val = "--" if self.value is None else f"{self.value:g}"
|
||||
p.drawText(QtCore.QRectF(cx - 55, cy + R * 0.30, 110, 22),
|
||||
@@ -328,15 +361,17 @@ class GaugeGrid(QtWidgets.QScrollArea):
|
||||
self._cols = 4
|
||||
|
||||
def rebuild(self, specs):
|
||||
"""specs: list of (key, name, unit, vmin, vmax, accent)."""
|
||||
"""specs: list of (key, name, unit, vmin, vmax, accent, zones)."""
|
||||
while self._grid.count():
|
||||
item = self._grid.takeAt(0)
|
||||
wdg = item.widget()
|
||||
if wdg:
|
||||
wdg.deleteLater()
|
||||
self._gauges = {}
|
||||
for i, (key, name, unit, vmin, vmax, accent) in enumerate(specs):
|
||||
g = ArcGauge(name, unit, vmin, vmax, accent)
|
||||
for i, spec in enumerate(specs):
|
||||
key, name, unit, vmin, vmax, accent = spec[:6]
|
||||
zones = spec[6] if len(spec) > 6 else None
|
||||
g = ArcGauge(name, unit, vmin, vmax, accent, zones)
|
||||
self._gauges[key] = g
|
||||
self._grid.addWidget(g, i // self._cols, i % self._cols)
|
||||
|
||||
|
||||
@@ -80,6 +80,8 @@ def _pid_from_dict(d):
|
||||
vmin=d.get("vmin", 0.0), vmax=d.get("vmax", 100.0),
|
||||
confidence=d.get("confidence", "verified"), round=d.get("round"),
|
||||
deps=tuple(d.get("deps", ())), notes=d.get("notes", ""),
|
||||
warn_hi=d.get("warn_hi"), redline_hi=d.get("redline_hi"),
|
||||
warn_lo=d.get("warn_lo"), redline_lo=d.get("redline_lo"),
|
||||
)
|
||||
|
||||
|
||||
@@ -110,6 +112,9 @@ def _pid_to_dict(p):
|
||||
"confidence": p.confidence})
|
||||
if p.round is not None:
|
||||
d["round"] = p.round
|
||||
for z in ("warn_hi", "redline_hi", "warn_lo", "redline_lo"):
|
||||
if getattr(p, z) is not None:
|
||||
d[z] = getattr(p, z)
|
||||
if p.notes:
|
||||
d["notes"] = p.notes
|
||||
return d
|
||||
|
||||
@@ -26,6 +26,26 @@ class Pid:
|
||||
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
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
# OBDash Vehicle Profile Specification (v1)
|
||||
|
||||
This is the **canonical, self-contained spec** for an OBDash vehicle profile. A
|
||||
profile is a single JSON file that teaches OBDash how to read one vehicle: its
|
||||
PIDs (with scaling), trouble-code meanings, dashboards (presets), and gauge
|
||||
warning zones. Profiles are **pure data** — they cannot run code.
|
||||
|
||||
> **Using an AI agent to build a profile?** Paste this whole file into your
|
||||
> agent and say: *"Research <year make model engine> and produce an OBDash
|
||||
> vehicle profile JSON that conforms exactly to this spec."* Then drop the
|
||||
> result in `profiles/` and open a PR. See "Rules for authors / agents" below.
|
||||
|
||||
Spec version: **1** (matches the top-level `"schema": 1`). This document is kept
|
||||
in sync with the loader (`obdcore/profile.py`) and the PID model
|
||||
(`obdcore/registry.py`) — if they disagree, the loader wins; file an issue.
|
||||
|
||||
---
|
||||
|
||||
## 1. Top-level shape
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"schema": 1, // required, must be 1
|
||||
"meta": { ... }, // vehicle identity (object)
|
||||
"presets": { "name": ["KEY", ...], ... }, // named dashboards (object)
|
||||
"pids": [ { ...pid... }, ... ], // signals (array)
|
||||
"dtcs": [ { ...dtc... }, ... ] // trouble-code dictionary (array)
|
||||
}
|
||||
```
|
||||
|
||||
## 2. `meta`
|
||||
|
||||
| Field | Req | Meaning |
|
||||
|---|---|---|
|
||||
| `name` | ✓ | Display name, shown in the Profile menu (e.g. `"Ford 6.0L Power Stroke"`) |
|
||||
| `make`, `model`, `years`, `engine` | ✓ | Vehicle identity strings |
|
||||
| `protocol` | ✓ | One EXACT value (see below), or `"auto"` |
|
||||
| `author` | — | Your name / handle |
|
||||
| `version` | — | Profile semver, e.g. `"1.0.0"` |
|
||||
| `notes` | — | Provenance, caveats, confidence policy |
|
||||
|
||||
`protocol` must be one of:
|
||||
`"SAE J1850 PWM"`, `"SAE J1850 VPW"`, `"ISO 9141-2"`, `"ISO 14230 KWP2000"`,
|
||||
`"ISO 15765 CAN"`, or `"auto"`. The ELM327 auto-negotiates regardless, so this
|
||||
is a hint/record — but get it right when you can.
|
||||
|
||||
## 3. `pids` — the signal definitions
|
||||
|
||||
Each PID object:
|
||||
|
||||
| Field | Req | Type | Meaning |
|
||||
|---|---|---|---|
|
||||
| `key` | ✓ | string | Unique id, `UPPER_SNAKE` (e.g. `ICP`, `STFT1`). Used in presets + derived `deps`. |
|
||||
| `name` | ✓ | string | Display name |
|
||||
| `mode` | ✓ | string | `"01"` generic SAE · `"22"` manufacturer-enhanced · `"atrv"` adapter pin voltage · `"derived"` computed from other PIDs |
|
||||
| `pid` | mode 01/22 | string | Request id hex — `"0C"` (mode 01) or `"1446"` (mode 22). Omit for `atrv`/`derived`. |
|
||||
| `nbytes` | mode 01/22 | int | Number of data bytes in the response the formula uses |
|
||||
| `formula` | mode 01/22/derived | string | Scaling expression (see §4). Omit for `atrv`. |
|
||||
| `unit` | ✓ | string | `"rpm"`, `"C"`, `"kPa"`, `"psi"`, `"%"`, `"V"`, `"km/h"`, … |
|
||||
| `group` | ✓ | string | One of: `fuel` `air` `engine` `driveline` `power` `ficm` `misc` |
|
||||
| `vmin`, `vmax` | ✓ | number | Display range (used by gauges + the Normalize overlay) |
|
||||
| `confidence` | ✓ | string | `verified` · `doc` · `tentative` (see §6) |
|
||||
| `round` | — | int | Display rounding: omit = raw float, `0` = integer, `2` = 2 dp |
|
||||
| `deps` | derived | string[] | PID keys the `derived` formula references |
|
||||
| `notes` | — | string | Gotchas / provenance; shown as a tooltip |
|
||||
| `warn_hi` `redline_hi` `warn_lo` `redline_lo` | — | number | Gauge warning zones (see §5) |
|
||||
|
||||
Always include this adapter-voltage pseudo-PID:
|
||||
```json
|
||||
{"key":"BATT","name":"Battery (OBD port)","mode":"atrv","unit":"V","group":"power","vmin":0,"vmax":16,"confidence":"verified"}
|
||||
```
|
||||
|
||||
## 4. Formula language
|
||||
|
||||
Arithmetic over **data-byte variables** `A, B, C, …` (= response byte 0, 1, 2 …) —
|
||||
the same convention as Torque / FORScan / ScanGauge. For `derived` PIDs the
|
||||
variables are **other PID keys** instead.
|
||||
|
||||
A safe AST evaluator (`obdcore/formula.py`) runs formulas. **Allowed:**
|
||||
- Numbers and the declared variables (`A`/`B`/… or dep keys)
|
||||
- Operators: `+ - * / // % **` and bitwise `& | ^ << >>` and unary `- ~`, parentheses
|
||||
- Functions: `min`, `max`, `abs`, `round`, `int`, `float`
|
||||
|
||||
**Rejected at load** (so a hostile profile can't run code): any other name,
|
||||
attribute access (`x.y`), subscripts (`x[0]`), or any other function call.
|
||||
|
||||
Canonical standard SAE J1979 Mode-01 formulas:
|
||||
```
|
||||
RPM 010C: (A*256+B)/4 Speed 010D: A ECT 0105: A-40
|
||||
IAT 010F: A-40 MAP 010B: A (kPa) MAF 0110: (A*256+B)/100 (g/s)
|
||||
TPS 0111: A*100/255 Load 0104: A*100/255 Timing 010E: A/2-64
|
||||
STFT/LTFT 0106-0109: A*100/128-100 Fuel pressure 010A: A*3
|
||||
O2 voltage 0114-011B: A/200 Runtime 011F: A*256+B Module V 0142: (A*256+B)/1000
|
||||
Ambient 0146: A-40 Fuel level 012F: A*100/255 Baro 0133: A
|
||||
```
|
||||
Examples (enhanced): `(A*256+B)*0.57` (ICP psi), `(A>>1)&1` (a status bit),
|
||||
`A//2` (gear), `"MAP-BARO"` with `"deps":["MAP","BARO"]` (boost).
|
||||
|
||||
## 5. Gauge warning zones (optional)
|
||||
|
||||
Make a gauge color-code like a real tach. All optional; omit for a neutral gauge.
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `redline_hi` | value `>=` this → RED (high redline) |
|
||||
| `warn_hi` | value `>=` this → AMBER |
|
||||
| `redline_lo` | value `<=` this → RED (low redline) |
|
||||
| `warn_lo` | value `<=` this → AMBER |
|
||||
|
||||
Use **high** zones where *high is bad* (ECT/EOT/RPM/boost) and **low** zones
|
||||
where *low is bad* (ICP/FICM/oil pressure; both for battery). The gauge draws a
|
||||
colored band on the dial and turns the needle + readout amber/red in-zone.
|
||||
|
||||
Examples (Ford 6.0): `"ICP": redline_lo 500, warn_lo 600` (must make ~500 psi to
|
||||
fire); `"ECT": warn_hi 105, redline_hi 110`; `"RPM": warn_hi 3500, redline_hi 3800`.
|
||||
|
||||
## 6. `confidence` tiers
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `verified` | SAE-standard PID, OR multi-source AND confirmed on a real vehicle |
|
||||
| `doc` | Documented in sources, **not** yet read on this vehicle |
|
||||
| `tentative` | Single-source, or disputed scaling — sanity-check before trusting |
|
||||
|
||||
Standard Mode-01 PIDs are `verified` (they're SAE-mandated). Manufacturer-enhanced
|
||||
PIDs you found in one community list are `doc` or `tentative`.
|
||||
|
||||
## 7. `presets` and `dtcs`
|
||||
|
||||
`presets`: named dashboards → a list of PID keys, e.g.
|
||||
`"basic": ["RPM","SPEED","ECT","MAP","TPS","BATT"]`, `"fuel": ["STFT1","LTFT1","O2B1S1"]`.
|
||||
Reference only keys you define. Provide at least `basic` (and `fuel` if the
|
||||
vehicle reports trims/O2).
|
||||
|
||||
`dtcs`: array of `{"code","desc","system","no_start","causes"}`. `code` like
|
||||
`"P0301"`; `system` is freeform (`engine`/`fuel`/`emissions`/…); `no_start: true`
|
||||
flags drive-disabling faults (shown bold red). Include generic `P0xxx` plus
|
||||
manufacturer-specific `P1xxx` you can source.
|
||||
|
||||
## 8. Rules for authors / agents
|
||||
|
||||
- **Standard Mode-01 PIDs are the reliable backbone** — include the ones this
|
||||
engine actually supports (MAF *vs* MAP by induction type; the O2/trim banks
|
||||
it really has). Mark them `verified`.
|
||||
- **Never invent a PID number or formula.** Enhanced PIDs need a documented id
|
||||
AND scaling; mark `doc`/`tentative` and cite in `notes`. If you can't verify,
|
||||
leave it out.
|
||||
- **Don't fabricate signals the stock stream lacks.** Many vehicles have no EGT
|
||||
or engine-oil-*pressure* PID (e.g. the 6.0 reports ICP + EOT only). Don't add them.
|
||||
- **Every formula must obey §4** (only `A`/`B`/… or dep keys + allowed ops/funcs).
|
||||
- **Validate before PR:** the loader compiles every formula and rejects bad ones.
|
||||
Quick check: `python -c "from obdcore import load_profile; load_profile('profiles/<file>.json')"`
|
||||
(no exception = it's valid). The app's **Profile → Edit JSON** dialog also
|
||||
validates on save.
|
||||
|
||||
## 9. Minimal valid example
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": 1,
|
||||
"meta": {"name":"Example 2.0L","make":"Example","model":"Demo","years":"1999",
|
||||
"engine":"2.0L I4","protocol":"auto","author":"you","version":"0.1.0"},
|
||||
"presets": {"basic": ["RPM","ECT","BATT"]},
|
||||
"pids": [
|
||||
{"key":"RPM","name":"Engine RPM","mode":"01","pid":"0C","nbytes":2,
|
||||
"formula":"(A*256+B)/4","round":0,"unit":"rpm","group":"engine",
|
||||
"vmin":0,"vmax":8000,"confidence":"verified","warn_hi":6000,"redline_hi":6800},
|
||||
{"key":"ECT","name":"Engine Coolant Temp","mode":"01","pid":"05","nbytes":1,
|
||||
"formula":"A-40","round":0,"unit":"C","group":"engine","vmin":-40,"vmax":215,
|
||||
"confidence":"verified","warn_hi":110,"redline_hi":118},
|
||||
{"key":"BATT","name":"Battery (OBD port)","mode":"atrv","unit":"V",
|
||||
"group":"power","vmin":0,"vmax":16,"confidence":"verified",
|
||||
"warn_lo":12.0,"redline_lo":11.0}
|
||||
],
|
||||
"dtcs": [
|
||||
{"code":"P0301","desc":"Cylinder 1 misfire","system":"engine","no_start":false}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -7,6 +7,11 @@ scaling formulas), DTC meanings, and named presets. Load one in the app via
|
||||
|
||||
**Contributions welcome** — add a profile for your vehicle and open a PR.
|
||||
|
||||
> 📋 **The canonical, AI-agent-ready format is [`PROFILE_SPEC.md`](PROFILE_SPEC.md).**
|
||||
> Researching a new vehicle? Paste that spec into your AI agent and say *"produce
|
||||
> an OBDash vehicle profile JSON for <year make model engine> per this spec."*
|
||||
> The summary below is a quick reference; the spec is authoritative.
|
||||
|
||||
## Current profiles
|
||||
|
||||
| File | Vehicle | Notes |
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
"vitals": ["ICP", "FICM_M", "FICM_L", "IPR", "BATT", "RPM", "ECT", "EOT", "IAT", "VPCM"]
|
||||
},
|
||||
"pids": [
|
||||
{"key": "ICP", "name": "Injection Control Pressure", "mode": "22", "pid": "1446", "nbytes": 2, "formula": "(A*256+B)*0.57", "round": 1, "unit": "psi", "group": "fuel", "vmin": 0, "vmax": 3500, "confidence": "verified", "notes": "need ~500+ psi to fire"},
|
||||
{"key": "ICP", "name": "Injection Control Pressure", "mode": "22", "pid": "1446", "nbytes": 2, "formula": "(A*256+B)*0.57", "round": 1, "unit": "psi", "group": "fuel", "vmin": 0, "vmax": 3500, "confidence": "verified", "redline_lo": 500, "warn_lo": 600, "notes": "need ~500+ psi to fire (red below 500)"},
|
||||
{"key": "ICP_V", "name": "ICP Sensor Voltage", "mode": "22", "pid": "16AD", "nbytes": 2, "formula": "(A*256+B)*0.000072", "round": 4, "unit": "V", "group": "fuel", "vmin": 0, "vmax": 5, "confidence": "tentative", "notes": "single-source"},
|
||||
{"key": "IPR", "name": "Injection Pressure Regulator", "mode": "22", "pid": "1434", "nbytes": 1, "formula": "A*13.53/35", "round": 1, "unit": "%", "group": "fuel", "vmin": 0, "vmax": 100, "confidence": "tentative", "notes": "KOEO ~14-15%, cranking ~30-40%"},
|
||||
{"key": "INJ_TIMING", "name": "Injection Timing", "mode": "22", "pid": "09CC", "nbytes": 2, "formula": "(A*256+B)*10/64", "round": 1, "unit": "degBTDC", "group": "fuel", "vmin": -10, "vmax": 30, "confidence": "tentative", "notes": "scaling disputed; using *10/64 (ScanGauge), not /10"},
|
||||
{"key": "FUEL_PUMP", "name": "Fuel Pump Duty (HFCM)", "mode": "22", "pid": "1672", "nbytes": 1, "formula": "A*100/128", "round": 1, "unit": "%", "group": "fuel", "vmin": 0, "vmax": 100, "confidence": "tentative", "notes": "sits ~100%, drops on high EOT"},
|
||||
{"key": "MFDES", "name": "Mass Fuel Desired", "mode": "22", "pid": "1411", "nbytes": 2, "formula": "A*256+B", "round": 0, "unit": "raw", "group": "fuel", "vmin": 0, "vmax": 65535, "confidence": "tentative", "notes": "~mg/stroke internal count; no verified GPH formula"},
|
||||
{"key": "FICM_M", "name": "FICM Main Power", "mode": "22", "pid": "09D0", "nbytes": 2, "formula": "(A*256+B)/256", "round": 1, "unit": "V", "group": "ficm", "vmin": 0, "vmax": 55, "confidence": "verified", "notes": "~48V; <45 suspect; reads intermittently while cranking"},
|
||||
{"key": "FICM_M", "name": "FICM Main Power", "mode": "22", "pid": "09D0", "nbytes": 2, "formula": "(A*256+B)/256", "round": 1, "unit": "V", "group": "ficm", "vmin": 38, "vmax": 52, "confidence": "verified", "redline_lo": 40, "warn_lo": 48, "notes": "~48V; <45 suspect; reads intermittently while cranking"},
|
||||
{"key": "FICM_L", "name": "FICM Logic Power", "mode": "22", "pid": "09CF", "nbytes": 2, "formula": "(A*256+B)/256", "round": 1, "unit": "V", "group": "ficm", "vmin": 0, "vmax": 16, "confidence": "doc"},
|
||||
{"key": "FICM_V", "name": "FICM Vehicle Power", "mode": "22", "pid": "09CE", "nbytes": 2, "formula": "(A*256+B)/256", "round": 1, "unit": "V", "group": "ficm", "vmin": 0, "vmax": 16, "confidence": "doc"},
|
||||
{"key": "FICM_SYNC", "name": "FICM Sync", "mode": "22", "pid": "09CD", "nbytes": 1, "formula": "(A>>1)&1", "round": 0, "unit": "", "group": "ficm", "vmin": 0, "vmax": 1, "confidence": "doc", "notes": "1=in sync, 0=no sync"},
|
||||
@@ -31,19 +31,19 @@
|
||||
{"key": "BARO", "name": "Barometric Pressure", "mode": "22", "pid": "1442", "nbytes": 2, "formula": "(A*256+B)*0.03625", "round": 2, "unit": "psia", "group": "air", "vmin": 0, "vmax": 20, "confidence": "verified"},
|
||||
{"key": "EBP", "name": "Exhaust Back Pressure", "mode": "22", "pid": "1445", "nbytes": 2, "formula": "(A*256+B)*0.03625", "round": 2, "unit": "psia", "group": "air", "vmin": 0, "vmax": 60, "confidence": "verified", "notes": "minus BARO = gauge"},
|
||||
{"key": "VGT", "name": "VGT Duty Cycle", "mode": "22", "pid": "096D", "nbytes": 2, "formula": "(A*256+B)*100/32767", "round": 1, "unit": "%", "group": "air", "vmin": 0, "vmax": 100, "confidence": "doc", "notes": "turbo vane duty"},
|
||||
{"key": "EOT", "name": "Engine Oil Temperature", "mode": "22", "pid": "1310", "nbytes": 2, "formula": "(A*256+B)/100-40", "round": 1, "unit": "C", "group": "engine", "vmin": -40, "vmax": 160, "confidence": "verified"},
|
||||
{"key": "EOT", "name": "Engine Oil Temperature", "mode": "22", "pid": "1310", "nbytes": 2, "formula": "(A*256+B)/100-40", "round": 1, "unit": "C", "group": "engine", "vmin": -40, "vmax": 160, "confidence": "verified", "warn_hi": 121, "redline_hi": 127},
|
||||
{"key": "FAN", "name": "Fan Speed", "mode": "22", "pid": "099F", "nbytes": 2, "formula": "(A*256+B)/4", "round": 0, "unit": "rpm", "group": "engine", "vmin": 0, "vmax": 4000, "confidence": "doc", "notes": "real ceiling ~3500"},
|
||||
{"key": "GEAR", "name": "Current Gear", "mode": "22", "pid": "11B3", "nbytes": 1, "formula": "A//2", "round": 0, "unit": "", "group": "driveline", "vmin": 0, "vmax": 6, "confidence": "verified"},
|
||||
{"key": "TSS", "name": "Trans Input Shaft Speed", "mode": "22", "pid": "11B4", "nbytes": 2, "formula": "(A*256+B)/4", "round": 0, "unit": "rpm", "group": "driveline", "vmin": 0, "vmax": 4000, "confidence": "verified"},
|
||||
{"key": "RPM", "name": "Engine RPM", "mode": "01", "pid": "0C", "nbytes": 2, "formula": "(A*256+B)/4", "round": 0, "unit": "rpm", "group": "engine", "vmin": 0, "vmax": 4000, "confidence": "verified"},
|
||||
{"key": "ECT", "name": "Engine Coolant Temp", "mode": "01", "pid": "05", "nbytes": 1, "formula": "A-40", "round": 0, "unit": "C", "group": "engine", "vmin": -40, "vmax": 160, "confidence": "verified"},
|
||||
{"key": "RPM", "name": "Engine RPM", "mode": "01", "pid": "0C", "nbytes": 2, "formula": "(A*256+B)/4", "round": 0, "unit": "rpm", "group": "engine", "vmin": 0, "vmax": 4000, "confidence": "verified", "warn_hi": 3500, "redline_hi": 3800},
|
||||
{"key": "ECT", "name": "Engine Coolant Temp", "mode": "01", "pid": "05", "nbytes": 1, "formula": "A-40", "round": 0, "unit": "C", "group": "engine", "vmin": -40, "vmax": 160, "confidence": "verified", "warn_hi": 105, "redline_hi": 110},
|
||||
{"key": "IAT", "name": "Intake Air Temp", "mode": "01", "pid": "0F", "nbytes": 1, "formula": "A-40", "round": 0, "unit": "C", "group": "air", "vmin": -40, "vmax": 160, "confidence": "verified"},
|
||||
{"key": "LOAD", "name": "Engine Load", "mode": "01", "pid": "04", "nbytes": 1, "formula": "A*100/255", "round": 0, "unit": "%", "group": "engine", "vmin": 0, "vmax": 100, "confidence": "verified"},
|
||||
{"key": "VPCM", "name": "Module Voltage", "mode": "01", "pid": "42", "nbytes": 2, "formula": "(A*256+B)/1000", "round": 2, "unit": "V", "group": "power", "vmin": 0, "vmax": 16, "confidence": "verified"},
|
||||
{"key": "VPCM", "name": "Module Voltage", "mode": "01", "pid": "42", "nbytes": 2, "formula": "(A*256+B)/1000", "round": 2, "unit": "V", "group": "power", "vmin": 9, "vmax": 15, "confidence": "verified", "warn_lo": 12.0, "redline_lo": 11.0},
|
||||
{"key": "VBAT", "name": "Battery (PCM)", "mode": "22", "pid": "1172", "nbytes": 1, "formula": "A/16", "round": 1, "unit": "V", "group": "power", "vmin": 0, "vmax": 16, "confidence": "tentative", "notes": "PCM-reported B+; distinct from ATRV port voltage"},
|
||||
{"key": "FUEL_LVL", "name": "Fuel Level", "mode": "22", "pid": "16C1", "nbytes": 2, "formula": "(A*256+B)*100/328", "round": 1, "unit": "%", "group": "misc", "vmin": 0, "vmax": 100, "confidence": "tentative", "notes": "UNCALIBRATED -- needs per-truck full/empty cal"},
|
||||
{"key": "BATT", "name": "Battery (OBD port)", "mode": "atrv", "unit": "V", "group": "power", "vmin": 0, "vmax": 16, "confidence": "verified", "notes": "ELM327 ATRV pin voltage"},
|
||||
{"key": "BOOST", "name": "Boost (MGP)", "mode": "derived", "formula": "MAP-BARO", "deps": ["MAP", "BARO"], "round": 2, "unit": "psi", "group": "air", "vmin": -5, "vmax": 40, "confidence": "verified", "notes": "MAP - BARO"}
|
||||
{"key": "BATT", "name": "Battery (OBD port)", "mode": "atrv", "unit": "V", "group": "power", "vmin": 9, "vmax": 15, "confidence": "verified", "warn_lo": 12.0, "redline_lo": 11.0, "notes": "ELM327 ATRV pin voltage"},
|
||||
{"key": "BOOST", "name": "Boost (MGP)", "mode": "derived", "formula": "MAP-BARO", "deps": ["MAP", "BARO"], "round": 2, "unit": "psi", "group": "air", "vmin": -5, "vmax": 40, "confidence": "verified", "warn_hi": 30, "redline_hi": 35, "notes": "MAP - BARO"}
|
||||
],
|
||||
"dtcs": [
|
||||
{"code": "P0087", "desc": "Fuel rail/system pressure too LOW", "system": "fuel", "no_start": true},
|
||||
|
||||
Reference in New Issue
Block a user