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:
2026-06-30 15:52:49 -04:00
parent deef305c63
commit d893ff383a
8 changed files with 326 additions and 32 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

+60 -10
View File
@@ -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
View File
@@ -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)
+5
View File
@@ -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
+20
View File
@@ -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
+179
View File
@@ -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}
]
}
```
+5
View File
@@ -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 &lt;year make model engine&gt; per this spec."*
> The summary below is a quick reference; the spec is authoritative.
## Current profiles
| File | Vehicle | Notes |
+8 -8
View File
@@ -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},