From 2e4763510dab54bc9b6054eeeff7d44889f02aea Mon Sep 17 00:00:00 2001 From: jack Date: Mon, 15 Jun 2026 11:28:28 -0700 Subject: [PATCH] feat(ui): improve device session management, update data directory paths, and add session clearing functionality. --- src/pygui/ISC/HomePage.qml | 30 ++++- src/pygui/ISC/Tabs/StatusTab.qml | 116 ++++++++++++++---- src/pygui/ISC/Tabs/WaferMapTab.qml | 15 ++- .../ISC/Tabs/components/WaferMapView.qml | 2 + src/pygui/__main__.py | 5 + .../backend/controllers/device_controller.py | 45 ++++--- .../backend/data/local_settings_model.py | 2 +- 7 files changed, 173 insertions(+), 42 deletions(-) diff --git a/src/pygui/ISC/HomePage.qml b/src/pygui/ISC/HomePage.qml index f2d3f77..3b6e963 100644 --- a/src/pygui/ISC/HomePage.qml +++ b/src/pygui/ISC/HomePage.qml @@ -24,6 +24,19 @@ Rectangle { property int selectedTabIndex: 0 property int selectedSideActionIndex: -1 // nothing active on startup + Connections { + target: deviceController + function onParsedDataReady(result) { + if (result.success && result.csv_path) { + // Load the freshly read CSV into the player + streamController.loadFile(result.csv_path) + + // Automatically switch to the "Wafer Map Tab" (index 3) + root.selectedTabIndex = 3 + } + } + } + // ===== Main Two-Column Layout ===== RowLayout { anchors.fill: parent @@ -60,9 +73,19 @@ Rectangle { onClicked: { root.selectedSideActionIndex = index root.selectedTabIndex = 0 // always jump to Status tab - if (index === 0) deviceController.detectWafer() - else if (index === 1) deviceController.readMemoryAsync() - else if (index === 2) deviceController.openCsvFile() + if (index === 0) { + streamController.setMode("review") + streamController.stopStream() + deviceController.detectWafer() + } + else if (index === 1) { + streamController.setMode("review") + streamController.stopStream() + deviceController.readMemoryAsync() + } + else if (index === 2) { + deviceController.openCsvFile() + } } background: Rectangle { @@ -182,6 +205,7 @@ Rectangle { color: Theme.tabBarBackground border.color: Theme.workspaceBorder border.width: Theme.borderThin + radius: Theme.radiusMd RowLayout { anchors.fill: parent diff --git a/src/pygui/ISC/Tabs/StatusTab.qml b/src/pygui/ISC/Tabs/StatusTab.qml index 6fd5272..1762db3 100644 --- a/src/pygui/ISC/Tabs/StatusTab.qml +++ b/src/pygui/ISC/Tabs/StatusTab.qml @@ -47,11 +47,7 @@ ColumnLayout { Rectangle { Layout.fillWidth: true Layout.preferredHeight: 86 - color: { - if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor + "22" - if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor + "22" - return Theme.cardBackground - } + color: Theme.cardBackground border.color: { if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor @@ -187,27 +183,105 @@ ColumnLayout { } // --- Activity Log --- - GroupBox { - title: "Activity Log" + Rectangle { Layout.fillWidth: true Layout.fillHeight: true + color: Theme.cardBackground + border.color: Theme.cardBorder + radius: Theme.radiusMd + clip: true - ScrollView { + ColumnLayout { anchors.fill: parent - clip: true + spacing: 0 - TextArea { - id: activityLog - width: parent.width - text: "" - readOnly: true - font.family: "monospace" - font.pixelSize: 12 - wrapMode: TextArea.WordWrap - leftPadding: 4 - rightPadding: 4 - color: Theme.bodyColor - } + // Header + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 40 + color: Theme.subtleSectionBackground + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Theme.panelPadding + anchors.rightMargin: Theme.panelPadding + + Label { + text: "ACTIVITY LOG" + color: Theme.bodyColor + font.pixelSize: 10 + font.letterSpacing: 1.8 + font.weight: Font.Medium + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + + Button { + text: "REFRESH SESSION" + font.pixelSize: 10 + implicitHeight: 24 + hoverEnabled: true + background: Rectangle { + color: parent.hovered ? Theme.buttonNeutralHover : "transparent" + radius: Theme.radiusSm + } + contentItem: Text { + text: parent.text + color: Theme.bodyColor + font: parent.font + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + onClicked: deviceController.clearSession() + } + + Button { + text: "CLEAR LOG" + font.pixelSize: 10 + implicitHeight: 24 + hoverEnabled: true + background: Rectangle { + color: parent.hovered ? Theme.buttonNeutralHover : "transparent" + radius: Theme.radiusSm + } + contentItem: Text { + text: parent.text + color: Theme.bodyColor + font: parent.font + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + onClicked: deviceController.clearActivityLog() + } + } + + Rectangle { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: Theme.cardBorder + } + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: Theme.panelPadding + clip: true + + TextArea { + id: activityLog + width: parent.width + text: "" + readOnly: true + font.family: "monospace" + font.pixelSize: 12 + wrapMode: TextArea.WordWrap + color: Theme.bodyColor + background: null + } + } } Connections { diff --git a/src/pygui/ISC/Tabs/WaferMapTab.qml b/src/pygui/ISC/Tabs/WaferMapTab.qml index 0dce1b7..fdac4b0 100644 --- a/src/pygui/ISC/Tabs/WaferMapTab.qml +++ b/src/pygui/ISC/Tabs/WaferMapTab.qml @@ -81,7 +81,20 @@ Item { } } onCurrentIndexChanged: - streamController.setMode(currentIndex === 0 ? "live" : "review") + if (currentIndex === 0){ + // Entering Live mode, only work when wafer detected/connected + streamController.setMode("live") + if (deviceController.connectionStatus === "Connected") { + var fc = "" + if (deviceController.lastWaferInfo && deviceController.lastWaferInfo.length > 0) { + fc = deviceController.lastWaferInfo[0] + } + streamController.startStream(deviceController.selectedPort, fc) + } + } else { + // Entering Review Mode (automatically calls stopStream in backend) + streamController.setMode("review") + } } // Live source info: WiFi icon + port ยท serial diff --git a/src/pygui/ISC/Tabs/components/WaferMapView.qml b/src/pygui/ISC/Tabs/components/WaferMapView.qml index 4a7bc4b..bc037fb 100644 --- a/src/pygui/ISC/Tabs/components/WaferMapView.qml +++ b/src/pygui/ISC/Tabs/components/WaferMapView.qml @@ -15,6 +15,8 @@ Item { sensors: streamController.sensorLayout // [{label,x,y}] values: streamController.sensorValues // [float] bands: streamController.sensorBands // ["in_range"|"high"|"low"] + shape: streamController.waferShape + size: streamController.waferSize target: streamController.target margin: streamController.margin blend: root.blend diff --git a/src/pygui/__main__.py b/src/pygui/__main__.py index 5b82651..a56d514 100644 --- a/src/pygui/__main__.py +++ b/src/pygui/__main__.py @@ -1,5 +1,6 @@ import sys from pathlib import Path +import logging from PySide6.QtQml import QQmlApplicationEngine from PySide6.QtQuickControls2 import QQuickStyle @@ -15,6 +16,10 @@ from pygui.backend.visualization.wafer_map_item import WaferMapItem # ===== Application Entry Point ===== def main() -> int: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" + ) # ===== UI Style Setup ===== # Use a non-native controls style so our custom QML button backgrounds are supported. QQuickStyle.setStyle("Basic") diff --git a/src/pygui/backend/controllers/device_controller.py b/src/pygui/backend/controllers/device_controller.py index fd0840d..6d23f88 100644 --- a/src/pygui/backend/controllers/device_controller.py +++ b/src/pygui/backend/controllers/device_controller.py @@ -1,10 +1,4 @@ -"""QML-exposed controller for wafer device communication. - -Bridges DeviceService (serialcomm) to QML via @Slot methods and signals. -All hardware operations run synchronously on the main thread (matching -C# Form1.cs per-operation open/close pattern). For long operations -(erase ~15s, read up to 120s) the caller should show a loading state. -""" +"""QML-exposed controller for wafer device communication.""" from __future__ import annotations @@ -67,9 +61,8 @@ class DeviceController(QObject): self._operation_in_progress = False self._activity_log: list[str] = getattr(settings, 'activity_log', []) self._raw_bytes: Optional[bytes] = None - # Default save dir lives next to the settings file (~/Documents/isc_data/csv) from pathlib import Path - self._save_data_dir: str = getattr(settings, 'save_data_dir', str(Path.home() / "Documents" / "isc_data" / "csv")) + self._save_data_dir: str = getattr(settings, 'save_data_dir', "") or str(Path(self._data_dir) / "csv") self._last_wafer_info: dict[str, Any] = getattr(settings, 'last_wafer_info', {}) self._last_csv_path: str = getattr(settings, 'last_csv_path', "") self._selected_port: str = getattr(settings, 'selected_port', "") @@ -209,6 +202,28 @@ class DeviceController(QObject): self._activity_log = self._activity_log[-200:] self.activityLogUpdated.emit("\n".join(self._activity_log)) + @Slot() + def clearActivityLog(self) -> None: + """Clear the activity log.""" + self._activity_log.clear() + self.activityLogUpdated.emit("") + self._save_status() + + @Slot() + def clearSession(self) -> None: + """Clear the current session state, allowing a clean detect.""" + self._connection_status = "Disconnected" + self._selected_port = "" + self._last_wafer_info = {} + self._data_row_count = 0 + self._data_col_count = 0 + self._raw_bytes = None + self._operation_in_progress = False + self.detectResult.emit(None) + self.portsUpdated.emit(self.availablePorts) + 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. @@ -424,12 +439,10 @@ class DeviceController(QObject): return if not self._save_data_dir: - self._append_log("No save data directory set") - self.parsedDataReady.emit({ - "success": False, - "error": "No save data directory set", - }) - return + from pathlib import Path + self._save_data_dir = str(Path(self._data_dir) / "csv") + self._append_log(f"Auto-set save directory to: {self._save_data_dir}") + self._save_status() fc = family_code or ( self._last_wafer_info.get("familyCode", "") @@ -509,7 +522,7 @@ class DeviceController(QObject): self._settings.last_csv_path = self._last_csv_path # Save to disk - LocalSettings.save_settings(str(LocalSettings._settings_path(self._data_dir)), self._settings) + LocalSettings.save_settings(self._data_dir, self._settings) # ---- Helpers ---- diff --git a/src/pygui/backend/data/local_settings_model.py b/src/pygui/backend/data/local_settings_model.py index b4ff97d..689bc17 100644 --- a/src/pygui/backend/data/local_settings_model.py +++ b/src/pygui/backend/data/local_settings_model.py @@ -52,7 +52,7 @@ class LocalSettingsModel(QObject): QStandardPaths.DocumentsLocation ) base_dir = Path(documents_dir) if documents_dir else (Path.home() / "Documents") - return base_dir / "isc_data" + return base_dir / "ISC_DATA" def _new_defaults(self) -> dict[str, Any]: return {