P1: PySide6 + pyqtgraph GUI shell (PID browser + live overlay plot)
First graphical frontend on obdcore. Cross-platform (Win/mac/Linux). - gui/controller.py: owns link/registry/store/scheduler; subscribe == poll == plot; per-PID rates (ICP/FICM/RPM fast); optional CSV recording. - gui/main.py: connection bar (port dropdown via find_ports, baud, Mock, connect), left PID browser grouped by system with live values + confidence badges + checkboxes, central pyqtgraph overlay plot with legend, Normalize (% of range) toggle for mixed-scale PIDs, Crank/Driving/Vitals presets, 10Hz refresh reading the store off the acquisition thread. - run_gui.py launcher; requirements-gui.txt. - store.py: lock Channel push/series (GUI reads while scheduler writes). - docs/gui-p1-preview.png: validated render (mock crank, ICP ramp to 540). Validated headless (offscreen Qt): connect(mock) -> crank preset -> ICP streams past 500 -> normalize -> uncheck removes curve -> clean disconnect. obdcore tests still pass after the locking change. 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:
+18
-9
@@ -10,7 +10,11 @@ from collections import deque
|
||||
|
||||
|
||||
class Channel:
|
||||
"""One PID's rolling history plus session min/max."""
|
||||
"""One PID's rolling history plus session min/max.
|
||||
|
||||
Lock-guarded so the acquisition thread can push while the GUI thread
|
||||
reads series()/snapshots without a 'deque mutated during iteration' race.
|
||||
"""
|
||||
|
||||
def __init__(self, key, maxlen=3600):
|
||||
self.key = key
|
||||
@@ -19,22 +23,27 @@ class Channel:
|
||||
self.hi = None
|
||||
self.last_t = None
|
||||
self.last_v = None
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def push(self, t, v):
|
||||
self.buf.append((t, v))
|
||||
self.last_t, self.last_v = t, v
|
||||
if v is not None:
|
||||
self.lo = v if self.lo is None else min(self.lo, v)
|
||||
self.hi = v if self.hi is None else max(self.hi, v)
|
||||
with self._lock:
|
||||
self.buf.append((t, v))
|
||||
self.last_t, self.last_v = t, v
|
||||
if v is not None:
|
||||
self.lo = v if self.lo is None else min(self.lo, v)
|
||||
self.hi = v if self.hi is None else max(self.hi, v)
|
||||
|
||||
def reset_minmax(self):
|
||||
self.lo = self.hi = self.last_v
|
||||
with self._lock:
|
||||
self.lo = self.hi = self.last_v
|
||||
|
||||
def series(self, since=None):
|
||||
"""Return [(t, v), ...]; if since given, only samples with t >= since."""
|
||||
with self._lock:
|
||||
items = list(self.buf) # snapshot under lock
|
||||
if since is None:
|
||||
return list(self.buf)
|
||||
return [(t, v) for (t, v) in self.buf if t >= since]
|
||||
return items
|
||||
return [(t, v) for (t, v) in items if t >= since]
|
||||
|
||||
|
||||
class TimeSeriesStore:
|
||||
|
||||
Reference in New Issue
Block a user