feat(tabs): update all tab views, dialogs, and session controller

This commit is contained in:
jack
2026-06-29 14:58:10 -07:00
parent db21f201f6
commit 5105ab1ab6
10 changed files with 507 additions and 307 deletions
@@ -37,10 +37,12 @@ class SessionController(QObject):
loadedFileChanged = Signal()
clusterAveragingEnabledChanged = Signal()
settingsChanged = Signal() # emitted when session state changes that should be persisted
liveStatsChanged = Signal() # emitted when receivedCount or errorCount change
# trend: per-frame avg for live graph
trendData = Signal(str) # JSON array of floats
# private: marshal a worker-thread frame onto the main thread
_liveFrame = Signal(object) # Frame
_liveError = Signal() # worker-thread error ping
def __init__(self, parent: QObject | None = None,
settings: dict | None = None) -> None:
@@ -72,6 +74,7 @@ class SessionController(QObject):
self._dirty = False
self._liveFrame.connect(self._on_live_frame, Qt.ConnectionType.QueuedConnection)
self._liveError.connect(self._on_live_error, Qt.ConnectionType.QueuedConnection)
self._sensor_editor = SensorEditor()
self._last_raw_frame: Frame | None = None
@@ -79,6 +82,8 @@ class SessionController(QObject):
self._cluster_averaging_enabled = False
self._active_clusters: list[list[int]] = []
self._stats_tracker = ReplayStatsTracker()
self._received_count: int = 0
self._error_count: int = 0
# Restore persisted state if available
self._load_settings(settings or {})
@@ -105,6 +110,9 @@ class SessionController(QObject):
@Property(int, notify=frameUpdated)
def frameTotal(self) -> int: return self._player.total
@Property(bool, notify=stateChanged)
def playing(self) -> bool: return self._play_timer.isActive()
@Property(str, notify=stateChanged)
def state(self) -> str:
if self._last:
@@ -210,6 +218,14 @@ class SessionController(QObject):
self.clusterAveragingEnabledChanged.emit()
self._reprocess_current()
@Property(int, notify=liveStatsChanged)
def receivedCount(self) -> int:
return self._received_count
@Property(int, notify=liveStatsChanged)
def errorCount(self) -> int:
return self._error_count
# ---- mode + thresholds ----
@Slot(str)
@slot_error_boundary
@@ -292,11 +308,13 @@ class SessionController(QObject):
@slot_error_boundary
def play(self) -> None:
self._play_timer.start(self._next_interval_ms())
self.stateChanged.emit()
@Slot()
@slot_error_boundary
def pause(self) -> None:
self._play_timer.stop()
self.stateChanged.emit()
@Slot()
@slot_error_boundary
@@ -304,6 +322,7 @@ class SessionController(QObject):
self._play_timer.stop()
self._player.seek(0)
self._emit_current()
self.stateChanged.emit()
@Slot(int)
@slot_error_boundary
@@ -311,6 +330,7 @@ class SessionController(QObject):
self._play_timer.stop()
self._player.step(delta)
self._emit_current()
self.stateChanged.emit()
@Slot(int)
@slot_error_boundary
@@ -335,6 +355,7 @@ class SessionController(QObject):
def _advance(self) -> None:
if self._player.at_end:
self._play_timer.stop()
self.stateChanged.emit()
return
self._player.step(1)
self._emit_current()
@@ -419,6 +440,9 @@ class SessionController(QObject):
self._stats_tracker.reset()
self._last = None
self._last_raw_frame = None
self._received_count = 0
self._error_count = 0
self.liveStatsChanged.emit()
transport = SerialPort.open_port(port, timeout=1)
# Send 'D2' command padded to 512 bytes to start the stream
@@ -427,6 +451,7 @@ class SessionController(QObject):
def on_error(exc: Exception):
log.error("Live stream error: %s", exc)
self._liveError.emit()
self._reader = StreamReader(
transport, parse_binary_frame,
@@ -444,6 +469,9 @@ class SessionController(QObject):
def stopStream(self) -> None:
self._repaint_timer.stop()
self._trend_timer.stop()
self._received_count = 0
self._error_count = 0
self.liveStatsChanged.emit()
if self._reader:
transport = self._reader._transport
self._reader.stop()
@@ -465,6 +493,8 @@ class SessionController(QObject):
@slot_error_boundary
def _on_live_frame(self, frame: Frame) -> None:
import json
self._received_count += 1
self.liveStatsChanged.emit()
self._stats_tracker.track(frame.seq, frame.values)
# Main thread (queued). Record raw, process edited; mark dirty for repaint.
self._last_raw_frame = frame
@@ -498,6 +528,11 @@ class SessionController(QObject):
self.frameUpdated.emit()
self.stateChanged.emit()
def _on_live_error(self) -> None:
"""Main-thread callback for worker-thread stream errors."""
self._error_count += 1
self.liveStatsChanged.emit()
# ---- recording ----
@Slot(str, str)
@slot_error_boundary