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:
+44
-71
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user