Gauge redline zones + C/F units toggle + cleaner dials

Gauges:
- Optional per-metric warning zones (warn_hi/redline_hi/warn_lo/redline_lo) in
  the profile schema; gauges draw colored redline/warn bands and color the
  needle + readout by zone. Default neutral when unset (no false redline).
- Removed the value progress-arc fill (it dominated the dial / looked wrong) ->
  clean tach face: bezel, ticks, numeric scale, needle, redline band, readout.
- Auto-derivation rejected: bad direction/threshold vary per metric, so zones
  are config (with a sensible neutral default).

Units:
- New Units menu: Temperature C / F. Converts gauges, graph, table, and PID
  browser (values, scale, zones, unit labels) at display time; data stays C.

Ford 6.0 profile: zones for ICP (red<500), FICM (red<40/amber40-48/green48+),
ECT/EOT (high redline), RPM (redline 3800), boost, battery; tightened FICM
(38-52) and battery (9-15) ranges so redline bands land sensibly.

Docs: profiles/PROFILE_SPEC.md -- canonical, AI-agent-ready profile spec
(schema, formula language, zones, confidence, rules); README points to it.

Validated headless: zones parse/classify, F conversion (112C->233.6F, zones
converted), gauges render; obdcore + diagnostics tests 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:52:49 -04:00
parent deef305c63
commit d893ff383a
8 changed files with 326 additions and 32 deletions
+60 -10
View File
@@ -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()