diff --git a/docs/gui-p2-gauges.png b/docs/gui-p2-gauges.png index 61f4984..27871de 100644 Binary files a/docs/gui-p2-gauges.png and b/docs/gui-p2-gauges.png differ diff --git a/gui/main.py b/gui/main.py index c8b3dad..9eb675e 100644 --- a/gui/main.py +++ b/gui/main.py @@ -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() diff --git a/gui/widgets.py b/gui/widgets.py index 56f68a3..cc82c1e 100644 --- a/gui/widgets.py +++ b/gui/widgets.py @@ -156,7 +156,7 @@ class ArcGauge(QtWidgets.QWidget): self.accent = QtGui.QColor(accent) self.value = None self.peak = None - self.setMinimumSize(150, 130) + self.setMinimumSize(172, 176) def set_value(self, v, peak=None): self.value = v @@ -166,45 +166,93 @@ class ArcGauge(QtWidgets.QWidget): def _frac(self, v): return max(0.0, min(1.0, (v - self.vmin) / (self.vmax - self.vmin))) + # 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 + + def _ang(self, frac): + return self._START - self._SWEEP * max(0.0, min(1.0, frac)) + def paintEvent(self, _ev): p = QtGui.QPainter(self) p.setRenderHint(QtGui.QPainter.Antialiasing) w, h = self.width(), self.height() - p.fillRect(self.rect(), QtGui.QColor("#141414")) # own dark bg (theme-proof) - m = 14 - side = min(w, h - 18) - rect = QtCore.QRectF((w - side) / 2 + m / 2, m / 2, side - m, side - m) - start, span = 225 * 16, -270 * 16 # 270deg sweep, top-open down + p.fillRect(self.rect(), QtGui.QColor("#141414")) - def arc_pen(color, width): - pen = QtGui.QPen(QtGui.QColor(color), width) - pen.setCapStyle(QtCore.Qt.RoundCap) - return pen + side = min(w, h - 14) + R = side / 2 - 6 + cx, cy = w / 2.0, R + 8 - p.setPen(arc_pen("#333", 9)) - p.drawArc(rect, start, span) + def pt(ang_deg, r): + a = math.radians(ang_deg) + return QtCore.QPointF(cx + r * math.cos(a), cy - r * math.sin(a)) + # dial face + bezel + p.setPen(QtGui.QPen(QtGui.QColor("#2a2a2a"), 3)) + 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: - frac = self._frac(self.value) - p.setPen(arc_pen(self.accent, 9)) - p.drawArc(rect, start, int(span * frac)) - if self.peak is not None and self.peak != self.value: - pf = self._frac(self.peak) - ang = (225 - 270 * pf) - p.setPen(QtGui.QPen(QtGui.QColor("#e6c84b"), 2)) - cx, cy = rect.center().x(), rect.center().y() - r1, r2 = side / 2 - m - 9, side / 2 - m + 2 - a = math.radians(ang) - p.drawLine(QtCore.QPointF(cx + r1 * math.cos(a), cy - r1 * math.sin(a)), - QtCore.QPointF(cx + r2 * math.cos(a), cy - r2 * math.sin(a))) + pen = QtGui.QPen(QtGui.QColor(self.accent), 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.setPen(QtGui.QColor("#eee")) - f = p.font(); f.setPointSize(15); f.setBold(True); p.setFont(f) + # tick marks + numeric scale + majors = 6 + fnt = p.font(); fnt.setPointSize(7); p.setFont(fnt) + for i in range(majors * 5 + 1): + frac = i / (majors * 5) + a = self._ang(frac) + major = (i % 5 == 0) + p.setPen(QtGui.QPen(QtGui.QColor("#ddd" if major else "#555"), + 2 if major else 1)) + p.drawLine(pt(a, R - 3), pt(a, R - (13 if major else 7))) + if major: + val = self.vmin + frac * (self.vmax - self.vmin) + span = abs(self.vmax - self.vmin) + lbl = f"{val:.0f}" if span >= 50 else f"{val:.1f}" if span >= 5 else f"{val:g}" + lp = pt(a, R - 25) + p.setPen(QtGui.QColor("#9a9a9a")) + p.drawText(QtCore.QRectF(lp.x() - 18, lp.y() - 7, 36, 14), + QtCore.Qt.AlignCenter, lbl) + + # peak marker (thin amber tick) + if self.peak is not None and self.value is not None and self.peak != self.value: + a = self._ang(self._frac(self.peak)) + p.setPen(QtGui.QPen(QtGui.QColor("#e6c84b"), 2)) + p.drawLine(pt(a, R - 3), pt(a, R - 14)) + + # needle + if self.value is not None: + a = self._ang(self._frac(self.value)) + tip = pt(a, R - 16) + b1 = pt(a + 90, 4.5) + b2 = pt(a - 90, 4.5) + tail = pt(a + 180, 10) + path = QtGui.QPainterPath() + 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.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")) + fnt.setPointSize(13); fnt.setBold(True); p.setFont(fnt) val = "--" if self.value is None else f"{self.value:g}" - p.drawText(rect, QtCore.Qt.AlignCenter, val) - f.setPointSize(8); f.setBold(False); p.setFont(f) - p.setPen(QtGui.QColor("#999")) - p.drawText(QtCore.QRectF(0, h - 16, w, 14), QtCore.Qt.AlignCenter, + p.drawText(QtCore.QRectF(cx - 55, cy + R * 0.30, 110, 22), + QtCore.Qt.AlignCenter, val) + # name + unit + fnt.setPointSize(8); fnt.setBold(False); p.setFont(fnt) + p.setPen(QtGui.QColor("#9a9a9a")) + p.drawText(QtCore.QRectF(0, h - 15, w, 14), QtCore.Qt.AlignCenter, f"{self.name} ({self.unit})" if self.unit else self.name) p.end()