Diagnostics as dialogs (no dock) + real car-style gauges

Diagnostics: removed the always-on right-side Diagnostics dock (it ate screen
space on every view). Read Codes now opens a modal dialog listing codes
grouped stored/pending/permanent with descriptions (no-start codes bold red);
Clear Codes keeps its confirmation, and on re-read pops a dialog only if codes
returned. Dropped the View > Show Diagnostics Panel toggle.

Gauges: replaced the plain progress-arc with a proper automotive dial -- round
face + bezel, major/minor tick marks, a numeric scale (rounded labels), a red
needle + center hub, a colored rim arc for the value, and an amber peak tick.
Digital readout + name/unit below.

Validated headless: no Diagnostics dock; Read Codes opens the Trouble Codes
dialog; gauge view renders. obdcore + diagnostics tests still 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:12:49 -04:00
parent f2308cd4eb
commit e7e04b740e
3 changed files with 122 additions and 101 deletions
+44 -71
View File
@@ -46,7 +46,6 @@ class MainWindow(QtWidgets.QMainWindow):
self._build_menubar()
self._build_connection_bar()
self._build_pid_browser()
self._build_diag_dock()
self._build_center()
self._build_statusbar()
self._refresh_title()
@@ -93,9 +92,6 @@ class MainWindow(QtWidgets.QMainWindow):
self.show_pids = self._act(viewm, "Show PID Panel", self._toggle_pid_dock,
checkable=True)
self.show_pids.setChecked(True)
self.show_diag = self._act(viewm, "Show Diagnostics Panel", self._toggle_diag_dock,
checkable=True)
self.show_diag.setChecked(True)
self.norm_act = self._act(viewm, "Normalize Graph (% of range)",
self._sync_norm_from_menu, checkable=True)
viewm.addSeparator()
@@ -213,53 +209,24 @@ class MainWindow(QtWidgets.QMainWindow):
self.tree.resizeColumnToContents(0)
self.tree.blockSignals(False)
# ---------- diagnostics dock (DTCs) ----------
def _build_diag_dock(self):
self.diag_dock = QtWidgets.QDockWidget("Diagnostics", self)
wrap = QtWidgets.QWidget()
lay = QtWidgets.QVBoxLayout(wrap)
lay.setContentsMargins(4, 4, 4, 4)
bar = QtWidgets.QHBoxLayout()
self.diag_read_btn = QtWidgets.QPushButton("Read Codes")
self.diag_read_btn.clicked.connect(self._read_codes)
self.diag_clear_btn = QtWidgets.QPushButton("Clear Codes…")
self.diag_clear_btn.clicked.connect(self._clear_codes)
bar.addWidget(self.diag_read_btn)
bar.addWidget(self.diag_clear_btn)
bar.addStretch(1)
lay.addLayout(bar)
self.diag_tree = QtWidgets.QTreeWidget()
self.diag_tree.setColumnCount(3)
self.diag_tree.setHeaderLabels(["Code", "Description", "System"])
self.diag_tree.setRootIsDecorated(True)
self.diag_tree.header().setStretchLastSection(False)
self.diag_tree.header().setSectionResizeMode(
1, QtWidgets.QHeaderView.Stretch)
lay.addWidget(self.diag_tree)
self.diag_hint = QtWidgets.QLabel(
"Connect, then Read Codes. Bold red = no-start / drive-disabling.")
self.diag_hint.setWordWrap(True)
lay.addWidget(self.diag_hint)
self.diag_dock.setWidget(wrap)
self.diag_dock.visibilityChanged.connect(
lambda vis: self.show_diag.setChecked(vis))
self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.diag_dock)
# ---------- diagnostics (DTCs) -- menu-driven dialogs, no docked panel ----------
_DIAG_GROUPS = [("stored", "Stored (mode 03)"),
("pending", "Pending (mode 07)"),
("permanent", "Permanent (mode 0A)")]
def _populate_diag(self, codes):
"""codes: {'stored':[...], 'pending':[...], 'permanent':[...]}"""
self.diag_tree.clear()
def _codes_tree(self, codes):
"""Build a populated QTreeWidget of codes for a dialog (not docked)."""
tree = QtWidgets.QTreeWidget()
tree.setColumnCount(3)
tree.setHeaderLabels(["Code", "Description", "System"])
tree.header().setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
total = 0
for bucket, label in self._DIAG_GROUPS:
lst = codes.get(bucket, [])
total += len(lst)
top = QtWidgets.QTreeWidgetItem([f"{label} ({len(lst)})", "", ""])
f = top.font(0); f.setBold(True); top.setFont(0, f)
top.setFirstColumnSpanned(False)
self.diag_tree.addTopLevelItem(top)
tree.addTopLevelItem(top)
if not lst:
none = QtWidgets.QTreeWidgetItem(["", "(no codes)", ""])
none.setForeground(1, QtGui.QBrush(QtGui.QColor("#888")))
@@ -267,18 +234,33 @@ class MainWindow(QtWidgets.QMainWindow):
for code in lst:
d = self.ctl.dtcdb.get(code)
it = QtWidgets.QTreeWidgetItem([code, d.desc, d.system])
it.setData(0, QtCore.Qt.UserRole, code)
if getattr(d, "no_start", False):
red = QtGui.QBrush(QtGui.QColor("#e6194B"))
bf = it.font(0); bf.setBold(True)
for c in range(3):
it.setFont(c, bf)
it.setForeground(c, red)
it.setFont(c, bf); it.setForeground(c, red)
it.setToolTip(0, "No-start / drive-disabling fault")
top.addChild(it)
top.setExpanded(True)
self.diag_tree.resizeColumnToContents(0)
self.diag_tree.resizeColumnToContents(2)
tree.resizeColumnToContents(0)
tree.resizeColumnToContents(2)
return tree, total
def _show_codes_dialog(self, codes, title, header=""):
dlg = QtWidgets.QDialog(self)
dlg.setWindowTitle(title)
dlg.resize(560, 360)
lay = QtWidgets.QVBoxLayout(dlg)
tree, total = self._codes_tree(codes)
if header:
lbl = QtWidgets.QLabel(header); lbl.setWordWrap(True); lay.addWidget(lbl)
lay.addWidget(tree)
foot = QtWidgets.QLabel("Bold red = no-start / drive-disabling fault.")
foot.setStyleSheet("color:#888;"); lay.addWidget(foot)
bb = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Close)
bb.rejected.connect(dlg.reject); bb.accepted.connect(dlg.accept)
lay.addWidget(bb)
dlg.exec()
return total
def _read_codes(self):
@@ -289,17 +271,10 @@ class MainWindow(QtWidgets.QMainWindow):
try:
codes = self.ctl.read_dtcs()
except Exception as e:
QtWidgets.QMessageBox.critical(self, "Read failed", str(e))
return
total = self._populate_diag(codes)
self.diag_dock.setVisible(True)
self.show_diag.setChecked(True)
self.status.showMessage(
f"Read codes: {total} found "
f"(stored {len(codes.get('stored', []))}, "
f"pending {len(codes.get('pending', []))}, "
f"permanent {len(codes.get('permanent', []))})."
if total else "Read codes: none stored.")
QtWidgets.QMessageBox.critical(self, "Read failed", str(e)); return
total = self._show_codes_dialog(codes, "Trouble Codes")
self.status.showMessage(f"Read codes: {total} found." if total
else "Read codes: none stored.")
def _clear_codes(self):
if not self.ctl.connected:
@@ -310,8 +285,8 @@ class MainWindow(QtWidgets.QMainWindow):
self, "Clear codes?",
"This erases stored + pending codes AND freeze-frame data, and "
"resets emissions monitors.\n\n"
"Write the codes down first — and read them on a no-start before "
"clearing. If the fault is still present the code comes right back.\n"
"Read the codes first — on a no-start especially. If the fault is "
"still present the code comes right back.\n"
"Permanent codes (mode 0A) will NOT clear until the fault is fixed "
"and the vehicle self-clears them over several drive cycles.\n\n"
"Clear codes now?",
@@ -323,32 +298,30 @@ class MainWindow(QtWidgets.QMainWindow):
try:
ok = self.ctl.clear_dtcs()
except Exception as e:
QtWidgets.QMessageBox.critical(self, "Clear failed", str(e))
return
QtWidgets.QMessageBox.critical(self, "Clear failed", str(e)); return
if not ok:
QtWidgets.QMessageBox.warning(
self, "No acknowledgement",
"The ECU did not acknowledge the clear.\n"
"Make sure the key is in RUN and the vehicle is connected, then "
"try again.")
"Make sure the key is in RUN and connected, then try again.")
self.status.showMessage("Clear not acknowledged by ECU.")
return
# Re-read immediately so anything that came straight back is shown.
try:
codes = self.ctl.read_dtcs()
except Exception:
codes = {}
returned = self._populate_diag(codes)
returned = sum(len(codes.get(b, [])) for b, _ in self._DIAG_GROUPS)
if returned:
self.status.showMessage(
f"Cleared — but {returned} code(s) returned immediately "
"(active fault present).")
self._show_codes_dialog(
codes, "Cleared — codes returned",
"These codes came back immediately after clearing — an active "
"fault is still present:")
self.status.showMessage(f"Cleared — {returned} code(s) returned (active fault).")
else:
QtWidgets.QMessageBox.information(self, "Codes cleared",
"Cleared. No codes on re-read.")
self.status.showMessage("Cleared. No codes on re-read.")
def _toggle_diag_dock(self):
self.diag_dock.setVisible(self.show_diag.isChecked())
# ---------- center (graph + table stack) ----------
def _build_center(self):
self.stack = QtWidgets.QStackedWidget()