diff --git a/docs/gui-gauge-zones.png b/docs/gui-gauge-zones.png new file mode 100644 index 0000000..9251cad Binary files /dev/null and b/docs/gui-gauge-zones.png differ diff --git a/gui/main.py b/gui/main.py index 8b3b021..b46540e 100644 --- a/gui/main.py +++ b/gui/main.py @@ -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() diff --git a/gui/widgets.py b/gui/widgets.py index 3f6bde1..9fb4bfd 100644 --- a/gui/widgets.py +++ b/gui/widgets.py @@ -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) diff --git a/obdcore/profile.py b/obdcore/profile.py index f6f3d85..cacc276 100644 --- a/obdcore/profile.py +++ b/obdcore/profile.py @@ -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 diff --git a/obdcore/registry.py b/obdcore/registry.py index 4f4ec26..da6b6dc 100644 --- a/obdcore/registry.py +++ b/obdcore/registry.py @@ -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 diff --git a/profiles/PROFILE_SPEC.md b/profiles/PROFILE_SPEC.md new file mode 100644 index 0000000..285de0f --- /dev/null +++ b/profiles/PROFILE_SPEC.md @@ -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 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/.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} + ] +} +``` diff --git a/profiles/README.md b/profiles/README.md index 92e7fd6..218b2b4 100644 --- a/profiles/README.md +++ b/profiles/README.md @@ -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 | diff --git a/profiles/ford-6.0-powerstroke.json b/profiles/ford-6.0-powerstroke.json index 7f13681..15cea73 100644 --- a/profiles/ford-6.0-powerstroke.json +++ b/profiles/ford-6.0-powerstroke.json @@ -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},