feat(backend): introduce session controller and status models

Implement DeviceController and SessionController to bridge QML requests with backend state, supporting playback replay tracking, cluster averaging, and slot error boundaries.
This commit is contained in:
jack
2026-06-25 13:47:21 -07:00
parent 470f8acba3
commit 2cbc143c20
4 changed files with 40 additions and 30 deletions
@@ -58,8 +58,8 @@ class DeviceController(QObject):
self._data_dir = data_dir
self._service = DeviceService(settings)
# Initialize status from persisted settings (with defaults for new installations)
self._connection_status = getattr(settings, 'connection_status', "Disconnected")
# Always start fresh — never restore connection status from a prior session
self._connection_status = "Disconnected"
self._operation_in_progress = False
self._activity_log: list[str] = getattr(settings, 'activity_log', [])
self._raw_bytes: Optional[bytes] = None
@@ -87,6 +87,9 @@ class DeviceController(QObject):
self._append_log(f"Last wafer: {info.get('serialNumber', 'Unknown')} ({info.get('familyCode', 'Unknown')})")
self.statusRestored.emit()
# Reset connection status on fresh start
self._connection_status = "Disconnected"
# Marshal worker-thread results onto the Qt main thread.
self._detectFinished.connect(self._handle_detect_finished, Qt.ConnectionType.QueuedConnection)
self._readFinished.connect(self._handle_read_finished, Qt.ConnectionType.QueuedConnection)
@@ -101,6 +104,12 @@ class DeviceController(QObject):
"""Currently available serial port names."""
return self._service.enumerate_ports()
def _set_connection_status(self, status: str) -> None:
"""Update connection status and notify QML."""
if self._connection_status != status:
self._connection_status = status
self.portsUpdated.emit(self.availablePorts)
@Property(str, notify=portsUpdated)
def connectionStatus(self) -> str:
"""Current connection status string for display."""
@@ -223,7 +232,7 @@ class DeviceController(QObject):
@slot_error_boundary
def clearSession(self) -> None:
"""Clear the current session state, allowing a clean detect."""
self._connection_status = "Disconnected"
self._set_connection_status("Disconnected")
self._selected_port = ""
self._last_wafer_info = {}
self._data_row_count = 0
@@ -235,6 +244,7 @@ class DeviceController(QObject):
self._append_log("Session refreshed/cleared")
self._save_status()
def _set_operation_progress(self, in_progress: bool) -> None:
"""Set operation progress state and notify QML.
@@ -252,7 +262,7 @@ class DeviceController(QObject):
def refreshPorts(self) -> None:
"""Scan for available serial ports and emit updated list."""
ports = self._service.enumerate_ports()
self._connection_status = "Disconnected"
self._set_connection_status("Disconnected")
self.portsUpdated.emit(ports)
self._append_log(f"Scanned {len(ports)} serial port(s)")
self._save_status()
@@ -271,7 +281,7 @@ class DeviceController(QObject):
return
self._set_operation_progress(True)
self._connection_status = "Detecting..."
self._set_connection_status("Detecting...")
self.portsUpdated.emit(self._service.enumerate_ports())
self._append_log("Scanning all ports for wafer ...")
@@ -295,7 +305,7 @@ class DeviceController(QObject):
"""Main-thread completion handler for detectWafer."""
if info is not None:
self._selected_port = port or ""
self._connection_status = "Connected"
self._set_connection_status("Connected")
self._append_log(
f"Detected: {info.serial_number} (family={info.family_code}, "
f"port={port}, runtime={info.runtime}s, cycles={info.cycle_count})"
@@ -306,7 +316,7 @@ class DeviceController(QObject):
self._save_status()
else:
self._selected_port = ""
self._connection_status = "Disconnected"
self._set_connection_status("Disconnected")
self._append_log("No wafer detected on any port")
self._last_wafer_info = {}
self.detectResult.emit(None)
@@ -334,7 +344,7 @@ class DeviceController(QObject):
family_code = self._last_wafer_info.get("familyCode", "")
self._set_operation_progress(True)
self._connection_status = "Reading..."
self._set_connection_status("Reading...")
self.portsUpdated.emit(self._service.enumerate_ports())
self._append_log(
f"Reading memory on {port} (family={family_code or 'auto'}) ..."
@@ -362,7 +372,7 @@ class DeviceController(QObject):
) -> None:
"""Main-thread completion handler for readMemoryAsync."""
if data is not None:
self._connection_status = "Connected"
self._set_connection_status("Connected")
self._append_log(f"Read {len(data)} bytes")
self._raw_bytes = data
self.readResult.emit({"success": True, "bytes": len(data)})
@@ -370,7 +380,7 @@ class DeviceController(QObject):
self._append_log("Memory read complete - choose save directory to save CSV")
self._save_status()
return
self._connection_status = "Disconnected"
self._set_connection_status("Disconnected")
self._append_log("Read returned no data")
self._raw_bytes = None
self.readResult.emit({"error": "Read returned no data"})
@@ -382,16 +392,16 @@ class DeviceController(QObject):
def eraseMemory(self, port: str) -> None:
"""Send p1 erase command."""
self._set_operation_progress(True)
self._connection_status = "Erasing..."
self._set_connection_status("Erasing...")
self.portsUpdated.emit(self._service.enumerate_ports())
self._append_log(f"Erase command sent to {port} (wafer takes ~15s) ...")
ok = self._service.erase_wafer(port)
if ok:
self._connection_status = "Connected"
self._set_connection_status("Connected")
self._append_log("Erase command accepted — wait ~15 seconds")
else:
self._connection_status = "Disconnected"
self._set_connection_status("Disconnected")
self._append_log("Erase command failed")
self.eraseResult.emit({"success": ok})
self._set_operation_progress(False)
@@ -404,14 +414,14 @@ class DeviceController(QObject):
Emits debugResult with counts on success.
"""
self._set_operation_progress(True)
self._connection_status = "Reading debug..."
self._set_connection_status("Reading debug...")
self.portsUpdated.emit(self._service.enumerate_ports())
self._append_log(f"Reading debug data on {port} ...")
# Step 1: D1 sensor data
sensor_data = self._service.read_wafer_data(port, family_code="")
if sensor_data is None:
self._connection_status = "Disconnected"
self._set_connection_status("Disconnected")
self._append_log("D1 read failed — aborting debug read")
self.debugResult.emit({"error": "D1 read failed"})
self._set_operation_progress(False)
@@ -422,13 +432,13 @@ class DeviceController(QObject):
# Step 2: F1 debug data
debug_data = self._service.read_wafer_data(port, family_code="", cmd="F1")
if debug_data is None:
self._connection_status = "Disconnected"
self._set_connection_status("Disconnected")
self._append_log("F1 read failed")
self.debugResult.emit({"error": "F1 read failed"})
self._set_operation_progress(False)
return
self._connection_status = "Connected"
self._set_connection_status("Connected")
self._append_log(
f"Debug read complete: {len(sensor_data)} sensor bytes, "
f"{len(debug_data)} debug bytes"