feat: implement slot_error_boundary decorator and add unit tests for session and device controllers

This commit is contained in:
jack
2026-06-18 21:01:37 -07:00
parent 5514653b94
commit 7206334a27
6 changed files with 353 additions and 1 deletions
@@ -14,6 +14,7 @@ from pygui.backend.models.frame_player import FramePlayer, frames_from_wafer_dat
from pygui.backend.models.sensor_editor import SensorEditor
from pygui.backend.models.session_model import SessionModel
from pygui.backend.models.threshold_classifier import ThresholdConfig
from pygui.backend.utils import slot_error_boundary
from pygui.backend.wafer.zwafer_models import Sensor
from pygui.backend.wafer.zwafer_parser import ZWaferParser
from pygui.serialcomm.stream_reader import StreamReader
@@ -182,6 +183,7 @@ class SessionController(QObject):
# ---- mode + thresholds ----
@Slot(str)
@slot_error_boundary
def setMode(self, mode: str) -> None:
if mode == self._mode:
return
@@ -191,6 +193,7 @@ class SessionController(QObject):
self.modeChanged.emit()
@Slot(float, float, bool)
@slot_error_boundary
def setThresholds(self, set_point: float, margin: float, auto: bool) -> None:
self._model.set_thresholds(ThresholdConfig(set_point, margin, auto))
if self._last: # re-band current frame in place
@@ -198,6 +201,7 @@ class SessionController(QObject):
# ---- review: file load + playback ----
@Slot(str)
@slot_error_boundary
def loadFile(self, file_path: str) -> None:
from pathlib import Path
@@ -250,30 +254,37 @@ class SessionController(QObject):
self._emit_current()
@Slot()
@slot_error_boundary
def play(self) -> None:
self._play_timer.start(self._next_interval_ms())
@Slot()
def pause(self) -> None: self._play_timer.stop()
@slot_error_boundary
def pause(self) -> None:
self._play_timer.stop()
@Slot()
@slot_error_boundary
def stop(self) -> None:
self._play_timer.stop()
self._player.seek(0)
self._emit_current()
@Slot(int)
@slot_error_boundary
def step(self, delta: int) -> None:
self._play_timer.stop()
self._player.step(delta)
self._emit_current()
@Slot(int)
@slot_error_boundary
def seek(self, i: int) -> None:
self._player.seek(i)
self._emit_current()
@Slot(float)
@slot_error_boundary
def setSpeed(self, x: float) -> None:
self._speed = max(0.1, x)
if self._play_timer.isActive():
@@ -325,6 +336,7 @@ class SessionController(QObject):
# ---- live: stream start/stop ----
@Slot(str, str)
@slot_error_boundary
def startStream(self, port: str, family_code: str = "") -> None:
from pygui.backend.wafer.wafer_layouts import load_layout_for_wafer_id
@@ -389,6 +401,7 @@ class SessionController(QObject):
self.stateChanged.emit()
@Slot()
@slot_error_boundary
def stopStream(self) -> None:
self._repaint_timer.stop()
if self._reader:
@@ -409,6 +422,7 @@ class SessionController(QObject):
self.stopRecording()
@Slot(object)
@slot_error_boundary
def _on_live_frame(self, frame: Frame) -> None:
# Main thread (queued). Record raw, process edited; mark dirty for repaint.
self._last_raw_frame = frame
@@ -428,35 +442,41 @@ class SessionController(QObject):
# ---- recording ----
@Slot(str, str)
@slot_error_boundary
def startRecording(self, path: str, serial: str = "") -> None:
self._recorder.start(path, self._sensors, serial)
self.recordingChanged.emit()
@Slot()
@slot_error_boundary
def stopRecording(self) -> None:
if self._recorder.is_recording:
self._recorder.stop()
self.recordingChanged.emit()
@Slot(int, float)
@slot_error_boundary
def replaceSensor(self, index: int, value: float) -> None:
"""Override sensor `index` to display `value` every frame."""
self._sensor_editor.set_replacement(index, value)
self._reprocess_current()
@Slot(int, float)
@slot_error_boundary
def offsetSensor(self, index: int, delta: float) -> None:
"""Shift sensor `index` by `delta` every frame."""
self._sensor_editor.set_offset(index, delta)
self._reprocess_current()
@Slot(int)
@slot_error_boundary
def clearSensorEdit(self, index: int) -> None:
"""Remove all overrides for sensor `index`."""
self._sensor_editor.clear(index)
self._reprocess_current()
@Slot()
@slot_error_boundary
def clearSensorEdits(self) -> None:
"""Remove all sensor overrides."""
self._sensor_editor.clear()