feat: implement DTW comparison, add data segmentation and persistence, and update QML UI for file imports and graphing.

This commit is contained in:
jack
2026-06-23 21:38:45 -07:00
parent 20a34e22ee
commit 62ce5a9c37
16 changed files with 1049 additions and 8 deletions
@@ -1,6 +1,7 @@
"""QML-facing controller for the live/review wafer dashboard."""
from __future__ import annotations
import json
import logging
import time
from typing import Optional
@@ -35,12 +36,14 @@ class SessionController(QObject):
sensorsChanged = Signal()
loadedFileChanged = Signal()
clusterAveragingEnabledChanged = Signal()
settingsChanged = Signal() # emitted when session state changes that should be persisted
# 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
def __init__(self, parent: QObject | None = None) -> None:
def __init__(self, parent: QObject | None = None,
settings: dict | None = None) -> None:
super().__init__(parent)
self._mode = MODE_REVIEW
self._model = SessionModel()
@@ -50,6 +53,13 @@ class SessionController(QObject):
self._sensors: list[Sensor] = []
self._last = None # last SessionUpdate
self._elapsed = 0.0
self._trend_buffer: list[float] = [] # rolling window
self._trend_timestamps: list[float] = [] # monotonic timestamps
# Trend refresh timer - update every 1s
self._trend_timer = QTimer(self)
self._trend_timer.setInterval(1000)
self._trend_timer.timeout.connect(self._flush_trend)
self._play_timer = QTimer(self)
self._play_timer.timeout.connect(self._advance)
@@ -70,6 +80,26 @@ class SessionController(QObject):
self._active_clusters: list[list[int]] = []
self._stats_tracker = ReplayStatsTracker()
# Restore persisted state if available
self._load_settings(settings or {})
# ---- persisted settings ----
def _load_settings(self, settings: dict) -> None:
"""Restore session state from persisted settings dict."""
last_file = settings.get("session_last_file", "")
if last_file:
try:
self.loadFile(last_file)
except Exception:
log.warning("Could not restore last session file: %s", last_file)
def collect_settings(self) -> dict:
"""Return a dict of session state to persist."""
return {
"session_last_file": self._loaded_file,
}
# ---- properties QML binds to ----
@Property(str, notify=modeChanged)
def mode(self) -> str: return self._mode
@@ -257,6 +287,11 @@ class SessionController(QObject):
self.loadedFileChanged.emit()
self.sensorsChanged.emit()
self._emit_current()
# Populate stats tracker from all loaded frames for trend chart (P3.1)
for frame in self._player._frames:
self._stats_tracker.track(frame.seq, frame.values)
self.trendData.emit(json.dumps(self._stats_tracker.get_trend()))
self.settingsChanged.emit()
@Slot()
@slot_error_boundary
@@ -383,6 +418,9 @@ class SessionController(QObject):
# Clear out any old data from the prev sessions
self._model.reset()
self._trend_buffer.clear()
self._trend_timestamps.clear()
self._trend_timer.start()
self._stats_tracker.reset()
self._last = None
self._last_raw_frame = None
@@ -410,6 +448,7 @@ class SessionController(QObject):
@slot_error_boundary
def stopStream(self) -> None:
self._repaint_timer.stop()
self._trend_timer.stop()
if self._reader:
transport = self._reader._transport
self._reader.stop()
@@ -442,6 +481,21 @@ class SessionController(QObject):
self._dirty = True
self.trendData.emit(json.dumps(self._stats_tracker.get_trend()))
if self._last and self._last.stats:
avg = self._last.stats.avg
now = time.monotonic()
self._trend_buffer.append(avg)
self._trend_timestamps.append(now)
# drop entries older than 60 secs
cutoff = now -60
while self._trend_timestamps and self._trend_timestamps[0] < cutoff:
self._trend_timestamps.pop(0)
self._trend_buffer.pop(0)
def _flush_repaint(self) -> None:
if not self._dirty:
return
@@ -491,4 +545,8 @@ class SessionController(QObject):
self._sensor_editor.clear()
self._reprocess_current()
def _flush_trend(self) -> None:
import json
self.trendData.emit(json.dumps(self._trend_buffer))