diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 41ec6ec..fc8039c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,4 +1,4 @@ -# ford-obd — Architecture & Roadmap +# OBDash — Architecture & Roadmap Plan for growing the terminal `obd_reader.py` into a graphical Windows scan tool for the 6.0L Power Stroke, on a shared headless core. diff --git a/README.md b/README.md index c647964..7f54f78 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ford-obd +# OBDash Minimal **ELM327 OBD-II code reader** with a **Ford 6.0L Power Stroke no-start triage**, built for a cheap CH340 ELM327 USB adapter. Works on any OBD-II vehicle for generic diff --git a/diagnostics/2026-06-29-no-start/README.md b/diagnostics/2026-06-29-no-start/README.md index 62e55db..ba5ec5d 100644 --- a/diagnostics/2026-06-29-no-start/README.md +++ b/diagnostics/2026-06-29-no-start/README.md @@ -1,6 +1,6 @@ # 2026-06-29 — 6.0 Power Stroke no-start session -In-cab diagnostic session using the `ford-obd` tool on the truck. +In-cab diagnostic session using the `OBDash` tool on the truck. Charger on "12V Hi" was attached for most of the session. ## Symptoms diff --git a/docs/gui-multiaxis-a.png b/docs/gui-multiaxis-a.png new file mode 100644 index 0000000..ea43764 Binary files /dev/null and b/docs/gui-multiaxis-a.png differ diff --git a/docs/gui-multiaxis-b.png b/docs/gui-multiaxis-b.png new file mode 100644 index 0000000..611ccc2 Binary files /dev/null and b/docs/gui-multiaxis-b.png differ diff --git a/docs/gui-p2-multiaxis.png b/docs/gui-p2-multiaxis.png index a8eff3b..1ea0d68 100644 Binary files a/docs/gui-p2-multiaxis.png and b/docs/gui-p2-multiaxis.png differ diff --git a/gui/__init__.py b/gui/__init__.py index 9e76f19..6d20a9f 100644 --- a/gui/__init__.py +++ b/gui/__init__.py @@ -1,4 +1,4 @@ -"""PySide6 + pyqtgraph GUI for ford-obd (P1 shell). +"""PySide6 + pyqtgraph GUI for OBDash (P1 shell). Run: python run_gui.py (or) python -m gui """ diff --git a/gui/main.py b/gui/main.py index 9eb675e..8b3b021 100644 --- a/gui/main.py +++ b/gui/main.py @@ -1,4 +1,4 @@ -"""ford-obd GUI -- vehicle-agnostic scanner shell. +"""OBDash GUI -- vehicle-agnostic scanner shell. Menu bar: File (captures) | Profile (vehicle profiles) | View | Help. Toolbar: port / baud / mock / connect + per-profile preset buttons. @@ -98,7 +98,7 @@ class MainWindow(QtWidgets.QMainWindow): self.theme_act = self._act(viewm, "Light Theme", self._toggle_theme, checkable=True) helpm = mb.addMenu("&Help") - self._act(helpm, "About ford-obd", self._about) + self._act(helpm, "About OBDash", self._about) self._act(helpm, "PID Confidence Legend", self._legend) self._act(helpm, "Active Profile Info", self._profile_info) @@ -375,7 +375,7 @@ class MainWindow(QtWidgets.QMainWindow): self._refresh_title() def _refresh_title(self): - self.setWindowTitle(f"ford-obd — {self.ctl.profile.name}") + self.setWindowTitle(f"OBDash — {self.ctl.profile.name}") def _load_profile(self, path): if self.ctl.connected: @@ -643,10 +643,10 @@ class MainWindow(QtWidgets.QMainWindow): # ---------- help ---------- def _about(self): - QtWidgets.QMessageBox.about(self, "About ford-obd", - "ford-obd — vehicle-agnostic OBD-II scanner\n\n" + QtWidgets.QMessageBox.about(self, "About OBDash", + "OBDash — vehicle-agnostic OBD-II scanner\n\n" "Open source. Vehicle data lives in JSON profiles you can add/share.\n" - "git.jpaul.io/justin/ford-obd") + "git.jpaul.io/justin/obdash") def _legend(self): QtWidgets.QMessageBox.information(self, "PID confidence", diff --git a/gui/widgets.py b/gui/widgets.py index cc82c1e..3f6bde1 100644 --- a/gui/widgets.py +++ b/gui/widgets.py @@ -1,5 +1,5 @@ -"""Custom widgets for the ford-obd GUI: a unit-grouped multi-axis plot, a -simple single-axis plot, and an arc gauge / gauge grid. +"""Custom widgets for the OBDash GUI: a multi-axis plot (one colored axis per +metric), a single-axis plot, and a car-style gauge / gauge grid. All three plot/gauge containers share a small interface so the main window can swap between them: add_curve(key,name,unit,color) / set_data(key,xs,ys) / @@ -10,8 +10,6 @@ import math from PySide6 import QtCore, QtGui, QtWidgets import pyqtgraph as pg -MAX_EXTRA_AXES = 4 # base left axis + up to 4 right axes (5 units shown) - class SinglePlot(QtWidgets.QWidget): """One shared Y axis. Used for the Normalize (% of range) mode.""" @@ -58,9 +56,14 @@ class SinglePlot(QtWidgets.QWidget): class MultiAxisPlot(QtWidgets.QWidget): - """Overlay with one Y axis PER UNIT (psi / V / rpm / C / %), so mixed-scale - signals are all readable at their true values. Curves are grouped by unit; - each extra unit gets its own right-hand axis + linked ViewBox.""" + """Overlay with one Y axis PER METRIC, each axis colored to match its line, + so mixed-scale signals are all readable at their true values. + + The 'primary' metric owns the LEFT axis; the rest stack on the right. + Click a line to promote it to the left axis. Beyond MAX_RIGHT extra metrics, + overflow curves share the primary axis (rare; plot fewer to compare).""" + + MAX_RIGHT = 5 def __init__(self): super().__init__() @@ -73,40 +76,29 @@ class MultiAxisPlot(QtWidgets.QWidget): self.p.setLabel("bottom", "time", units="s") self.legend = self.p.addLegend(offset=(10, 10)) self.base_vb = self.p.vb - self._units = {} # unit -> {vb, ax, base} - self._curves = {} # key -> {curve, unit} - self._next_col = 3 + self._curves = {} # key -> {color,name,unit,curve,vb,ax} + self._order = [] # insertion order of keys + self._primary = None self.base_vb.sigResized.connect(self._sync) - def _ensure_unit(self, unit): - e = self._units.get(unit) - if e is not None: - return e - if not self._units: # first unit -> base left axis - self.p.setLabel("left", unit) - e = {"vb": self.base_vb, "ax": self.p.getAxis("left"), "base": True} - elif len(self._units) - 1 < MAX_EXTRA_AXES: # add a right axis - vb = pg.ViewBox() - ax = pg.AxisItem("right") - self.p.layout.addItem(ax, 2, self._next_col) - self._next_col += 1 - self.p.scene().addItem(vb) - ax.linkToView(vb) - vb.setXLink(self.p) - ax.setLabel(unit) - e = {"vb": vb, "ax": ax, "base": False} - self._sync() - else: # out of axes -> reuse base - e = self._units[next(iter(self._units))] - self._units[unit] = e - return e - def add_curve(self, key, name, unit, color): - e = self._ensure_unit(unit) + if key in self._curves: + return curve = pg.PlotCurveItem(pen=pg.mkPen(color, width=2), name=name) - e["vb"].addItem(curve) - self.legend.addItem(curve, name) - self._curves[key] = {"curve": curve, "unit": unit} + curve.setClickable(True, width=8) + curve.sigClicked.connect(lambda *_a, k=key: self.set_primary(k)) + self._curves[key] = {"color": color, "name": name, "unit": unit, + "curve": curve, "vb": None, "ax": None} + self._order.append(key) + if self._primary is None: + self._primary = key + self._rebuild() + + def set_primary(self, key): + """Move this metric's axis to the LEFT (make it the primary axis).""" + if key in self._curves and key != self._primary: + self._primary = key + self._rebuild() def set_data(self, key, xs, ys): c = self._curves.get(key) @@ -117,30 +109,94 @@ class MultiAxisPlot(QtWidgets.QWidget): c = self._curves.pop(key, None) if not c: return - e = self._units.get(c["unit"]) - if e: - e["vb"].removeItem(c["curve"]) - try: - self.legend.removeItem(c["curve"]) - except Exception: - pass + self._order.remove(key) + self._detach(c) + if self._primary == key: + self._primary = self._order[0] if self._order else None + self._rebuild() def clear(self): - for key in list(self._curves): - self.remove_curve(key) + for c in self._curves.values(): + self._detach(c) + self._curves = {} + self._order = [] + self._primary = None + self.legend.clear() def set_y_label(self, _text): - pass # multi-axis labels itself per unit + pass # labels itself per metric def set_background(self, bg): self.glw.setBackground(bg) + # -- internals -- + def _detach(self, c): + """Remove a curve from its current ViewBox and tear down its extra axis + (the base left axis is kept). Idempotent -- safe to call repeatedly.""" + cur = c["curve"] + vb = c.get("vb") + if vb is not None: + try: + vb.removeItem(cur) + except Exception: + pass + ax = c.get("ax") + if ax is not None and ax is not self.p.getAxis("left"): + try: + self.p.layout.removeItem(ax) + except Exception: + pass + sc = ax.scene() + if sc is not None: + sc.removeItem(ax) + if vb is not None and vb is not self.base_vb: + sc = vb.scene() + if sc is not None: + sc.removeItem(vb) + c["vb"] = None + c["ax"] = None + + def _rebuild(self): + for c in self._curves.values(): + self._detach(c) + self.legend.clear() + if self._primary is None: + return + order = [self._primary] + [k for k in self._order if k != self._primary] + col = 3 + left = self.p.getAxis("left") + for idx, key in enumerate(order): + c = self._curves[key] + cur = c["curve"] + pen = pg.mkPen(c["color"]) + if idx == 0: # primary -> left axis + self.base_vb.addItem(cur) + left.setLabel(c["name"]) # name already carries the unit + left.setPen(pen); left.setTextPen(pen) + c["vb"] = self.base_vb; c["ax"] = left + elif idx <= self.MAX_RIGHT: # right axis, colored to line + vb = pg.ViewBox() + ax = pg.AxisItem("right") + self.p.layout.addItem(ax, 2, col); col += 1 + self.p.scene().addItem(vb) + ax.linkToView(vb); vb.setXLink(self.p) + ax.setLabel(c["name"]) + ax.setPen(pen); ax.setTextPen(pen) + vb.addItem(cur) + c["vb"] = vb; c["ax"] = ax + else: # overflow -> primary axis + self.base_vb.addItem(cur) + c["vb"] = self.base_vb; c["ax"] = None + self.legend.addItem(cur, c["name"]) + self._sync() + def _sync(self): rect = self.base_vb.sceneBoundingRect() - for e in self._units.values(): - if not e["base"]: - e["vb"].setGeometry(rect) - e["vb"].linkedViewChanged(self.base_vb, e["vb"].XAxis) + for c in self._curves.values(): + vb = c.get("vb") + if vb is not None and vb is not self.base_vb: + vb.setGeometry(rect) + vb.linkedViewChanged(self.base_vb, vb.XAxis) class ArcGauge(QtWidgets.QWidget): diff --git a/handoff.md b/handoff.md index a764fe0..7d4cec0 100644 --- a/handoff.md +++ b/handoff.md @@ -1,6 +1,6 @@ -# Handoff — ford-obd / 6.0 Power Stroke no-start +# Handoff — OBDash / 6.0 Power Stroke no-start -Pick-up notes for diagnosing the truck in the cab. Repo: `git.jpaul.io/justin/ford-obd` (private). +Pick-up notes for diagnosing the truck in the cab. Repo: `git.jpaul.io/justin/obdash` (private). ## TL;DR — what to do at the truck 1. CH340 ELM327 adapter → OBD port (under dash, driver side). @@ -78,10 +78,10 @@ The 6.0 needs, to fire: **good batteries → FICM ~48V → ICP ~500 psi → fuel [crank-test-2026-06-30.txt](diagnostics/2026-06-29-no-start/crank-test-2026-06-30.txt). Next: physical inspection (IPR valve first, then STC fitting, then oil rail O-rings). -- Pushed to `git.jpaul.io/justin/ford-obd`, branch `main`. +- Pushed to `git.jpaul.io/justin/obdash`, branch `main`. ## To resume with Claude from the cab -Mention: "6.0 Power Stroke no-start, using the ford-obd tool (git.jpaul.io/justin/ford-obd)". +Mention: "6.0 Power Stroke no-start, using the OBDash tool (git.jpaul.io/justin/obdash)". Then **paste the tool's full output** (codes + live values). Useful to also say: cranks vs. no-crank, hot vs. cold, what changed before it died, and FICM/ICP readings if you metered them. diff --git a/profiles/README.md b/profiles/README.md index 973df56..92e7fd6 100644 --- a/profiles/README.md +++ b/profiles/README.md @@ -1,7 +1,7 @@ # Vehicle Profiles Each `*.json` file here is a **vehicle profile** — pure data that makes the -ford-obd app vehicle-agnostic. A profile defines a vehicle's PIDs (with safe +OBDash app vehicle-agnostic. A profile defines a vehicle's PIDs (with safe scaling formulas), DTC meanings, and named presets. Load one in the app via **Profile → Load**, or drop a new file in this folder and it appears in the list. diff --git a/profiles/ford-6.0-powerstroke.json b/profiles/ford-6.0-powerstroke.json index 49690b8..7f13681 100644 --- a/profiles/ford-6.0-powerstroke.json +++ b/profiles/ford-6.0-powerstroke.json @@ -6,7 +6,7 @@ "model": "Super Duty / Excursion", "years": "2003-2007", "engine": "6.0L Power Stroke diesel", - "author": "ford-obd project", + "author": "OBDash project", "version": "1.1.0", "protocol": "auto", "notes": "PID addresses + scaling corrected/verified by the ford-60-pid-hunt workflow (2026-06-29) and on-truck reads (2026-06-30). confidence: verified = multi-source or read on a real 6.0; doc = corroborated in sources, not yet read on-vehicle; tentative = single-source / disputed scaling. ICP_DES (desired ICP) has no public Mode-22 DID -> FORScan-only, not included." diff --git a/profiles/ford-mustang-cobra-4.6-1996.json b/profiles/ford-mustang-cobra-4.6-1996.json index f942df0..02420dd 100644 --- a/profiles/ford-mustang-cobra-4.6-1996.json +++ b/profiles/ford-mustang-cobra-4.6-1996.json @@ -6,7 +6,7 @@ "model": "Mustang SVT Cobra", "years": "1996", "engine": "4.6L DOHC 32-valve modular V8 (naturally aspirated, EEC-V, MAF-metered)", - "author": "ford-obd project", + "author": "OBDash project", "version": "0.1.0", "protocol": "SAE J1850 PWM", "notes": "First OBD-II model year on early EEC-V; Ford SCP J1850 PWM at 41.6 kbps. MAF-based airflow (no MAP/speed-density). V8 with two banks: dual-bank fuel trims and pre/post-cat narrowband HO2S included. CAN-era PIDs (0142 module voltage, 0146 ambient, 0133 baro, 012F fuel level) not supported and omitted. No verifiable manufacturer-enhanced (mode 22) PIDs for this EEC-V SCP vehicle; generic ELM327 adapters widely reported to fail enhanced sessions on Ford SCP PWM. All reliable data is in the standard SAE J1979 mode-01 set. Validated: all formulas safe-evaluator-clean; fuel-trim vmax normalized to +-100." diff --git a/profiles/ford-mustang-gt-4.6-1996.json b/profiles/ford-mustang-gt-4.6-1996.json index 4051b49..8c4cd02 100644 --- a/profiles/ford-mustang-gt-4.6-1996.json +++ b/profiles/ford-mustang-gt-4.6-1996.json @@ -6,7 +6,7 @@ "model": "Mustang GT", "years": "1996", "engine": "4.6L SOHC 2-valve modular V8 (EEC-V, MAF-metered)", - "author": "ford-obd project", + "author": "OBDash project", "version": "0.1.0", "protocol": "SAE J1850 PWM", "notes": "EEC-V PCM on SAE J1850 PWM (41.6 kbaud, OBD-II pins 2 & 10). MAF-metered V8 with dual exhaust: four heated O2 sensors (B1S1/B1S2/B2S1/B2S2) and two fuel-trim banks. Bank 1 = passenger side (cyl #1). Standard SAE J1979 Mode-01 backbone; no verifiable mode-22 enhanced PIDs published for this early EEC-V generation." diff --git a/profiles/generic-obd2.json b/profiles/generic-obd2.json index 53cf6bb..c1a1259 100644 --- a/profiles/generic-obd2.json +++ b/profiles/generic-obd2.json @@ -6,7 +6,7 @@ "model": "Any OBD-II vehicle (1996+)", "years": "1996+", "engine": "any", - "author": "ford-obd project", + "author": "OBDash project", "version": "1.0.0", "protocol": "auto", "notes": "Standard SAE J1979 Mode-01 PIDs only -- supported by essentially every OBD-II vehicle. Use as a base/starting point for a new vehicle profile, then add manufacturer-enhanced Mode-22 PIDs. Decodes are the SAE-standard formulas." diff --git a/profiles/jeep-wrangler-4.0-1997.json b/profiles/jeep-wrangler-4.0-1997.json index aea4806..ec3d220 100644 --- a/profiles/jeep-wrangler-4.0-1997.json +++ b/profiles/jeep-wrangler-4.0-1997.json @@ -6,7 +6,7 @@ "model": "Wrangler TJ", "years": "1997", "engine": "4.0L I6 (242 cu in)", - "author": "ford-obd project", + "author": "OBDash project", "version": "0.1.0", "protocol": "ISO 9141-2", "notes": "Chrysler/Jeep OBD-II over ISO 9141-2 K-line (used by Chrysler/Jeep for generic OBD-II through the early 2000s, later replaced by ISO 15765 CAN; Chrysler never used J1850). Speed-density: MAP sensor, no MAF. Single bank (inline-6): fuel trim bank 1 only, two narrowband O2 sensors (B1S1 upstream, B1S2 post-cat). Generic Mode-01 only; no generic service-22 enhanced channel on this JTEC PCM (Chrysler enhanced data lives on the proprietary SCI bus)." diff --git a/profiles/mercury-mountaineer-4.6-2006.json b/profiles/mercury-mountaineer-4.6-2006.json index e9f0c17..f2f27cf 100644 --- a/profiles/mercury-mountaineer-4.6-2006.json +++ b/profiles/mercury-mountaineer-4.6-2006.json @@ -6,7 +6,7 @@ "model": "Mountaineer", "years": "2006", "engine": "4.6L SOHC 3-valve Triton V8 (MAF-metered, AWD)", - "author": "ford-obd project", + "author": "OBDash project", "version": "0.1.0", "protocol": "SAE J1850 PWM", "notes": "UN150 platform, badge-twin of the 4th-gen (2006-2010) Ford Explorer. Uses Ford SCP = SAE J1850 PWM (DLC pins 2 & 10, 41.6 kbps) for SAE J1979 diagnostics \u2014 ELM327 protocol 1. The MS-CAN on this platform is an internal body network (FORScan), not the generic emissions bus; ISO 15765 CAN only arrived for MY2008. MAF-metered, no MAP. Two banks, two narrowband HEGO O2 sensors per bank. No verifiable mode-22 enhanced PIDs for this pre-CAN J1850 PCM, so none included. Mode-01 0142 module voltage omitted (CAN-era PID); use BATT (ATRV)." diff --git a/run_gui.py b/run_gui.py index 4647e9b..e5587b5 100644 --- a/run_gui.py +++ b/run_gui.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Launcher for the ford-obd GUI. +"""Launcher for the OBDash GUI. pip install PySide6 pyqtgraph numpy pyserial python run_gui.py