feat(ui): improve device session management, update data directory paths, and add session clearing functionality.
This commit is contained in:
@@ -24,6 +24,19 @@ Rectangle {
|
|||||||
property int selectedTabIndex: 0
|
property int selectedTabIndex: 0
|
||||||
property int selectedSideActionIndex: -1 // nothing active on startup
|
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 =====
|
// ===== Main Two-Column Layout =====
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -60,9 +73,19 @@ Rectangle {
|
|||||||
onClicked: {
|
onClicked: {
|
||||||
root.selectedSideActionIndex = index
|
root.selectedSideActionIndex = index
|
||||||
root.selectedTabIndex = 0 // always jump to Status tab
|
root.selectedTabIndex = 0 // always jump to Status tab
|
||||||
if (index === 0) deviceController.detectWafer()
|
if (index === 0) {
|
||||||
else if (index === 1) deviceController.readMemoryAsync()
|
streamController.setMode("review")
|
||||||
else if (index === 2) deviceController.openCsvFile()
|
streamController.stopStream()
|
||||||
|
deviceController.detectWafer()
|
||||||
|
}
|
||||||
|
else if (index === 1) {
|
||||||
|
streamController.setMode("review")
|
||||||
|
streamController.stopStream()
|
||||||
|
deviceController.readMemoryAsync()
|
||||||
|
}
|
||||||
|
else if (index === 2) {
|
||||||
|
deviceController.openCsvFile()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
@@ -182,6 +205,7 @@ Rectangle {
|
|||||||
color: Theme.tabBarBackground
|
color: Theme.tabBarBackground
|
||||||
border.color: Theme.workspaceBorder
|
border.color: Theme.workspaceBorder
|
||||||
border.width: Theme.borderThin
|
border.width: Theme.borderThin
|
||||||
|
radius: Theme.radiusMd
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -47,11 +47,7 @@ ColumnLayout {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 86
|
Layout.preferredHeight: 86
|
||||||
color: {
|
color: Theme.cardBackground
|
||||||
if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor + "22"
|
|
||||||
if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor + "22"
|
|
||||||
return Theme.cardBackground
|
|
||||||
}
|
|
||||||
border.color: {
|
border.color: {
|
||||||
if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor
|
if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor
|
||||||
if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor
|
if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor
|
||||||
@@ -187,13 +183,91 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Activity Log ---
|
// --- Activity Log ---
|
||||||
GroupBox {
|
Rectangle {
|
||||||
title: "Activity Log"
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
color: Theme.cardBackground
|
||||||
|
border.color: Theme.cardBorder
|
||||||
|
radius: Theme.radiusMd
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
// 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 {
|
ScrollView {
|
||||||
anchors.fill: parent
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.margins: Theme.panelPadding
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
TextArea {
|
TextArea {
|
||||||
@@ -204,9 +278,9 @@ ColumnLayout {
|
|||||||
font.family: "monospace"
|
font.family: "monospace"
|
||||||
font.pixelSize: 12
|
font.pixelSize: 12
|
||||||
wrapMode: TextArea.WordWrap
|
wrapMode: TextArea.WordWrap
|
||||||
leftPadding: 4
|
|
||||||
rightPadding: 4
|
|
||||||
color: Theme.bodyColor
|
color: Theme.bodyColor
|
||||||
|
background: null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,20 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onCurrentIndexChanged:
|
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
|
// Live source info: WiFi icon + port · serial
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ Item {
|
|||||||
sensors: streamController.sensorLayout // [{label,x,y}]
|
sensors: streamController.sensorLayout // [{label,x,y}]
|
||||||
values: streamController.sensorValues // [float]
|
values: streamController.sensorValues // [float]
|
||||||
bands: streamController.sensorBands // ["in_range"|"high"|"low"]
|
bands: streamController.sensorBands // ["in_range"|"high"|"low"]
|
||||||
|
shape: streamController.waferShape
|
||||||
|
size: streamController.waferSize
|
||||||
target: streamController.target
|
target: streamController.target
|
||||||
margin: streamController.margin
|
margin: streamController.margin
|
||||||
blend: root.blend
|
blend: root.blend
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import logging
|
||||||
|
|
||||||
from PySide6.QtQml import QQmlApplicationEngine
|
from PySide6.QtQml import QQmlApplicationEngine
|
||||||
from PySide6.QtQuickControls2 import QQuickStyle
|
from PySide6.QtQuickControls2 import QQuickStyle
|
||||||
@@ -15,6 +16,10 @@ from pygui.backend.visualization.wafer_map_item import WaferMapItem
|
|||||||
|
|
||||||
# ===== Application Entry Point =====
|
# ===== Application Entry Point =====
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
||||||
|
)
|
||||||
# ===== UI Style Setup =====
|
# ===== UI Style Setup =====
|
||||||
# Use a non-native controls style so our custom QML button backgrounds are supported.
|
# Use a non-native controls style so our custom QML button backgrounds are supported.
|
||||||
QQuickStyle.setStyle("Basic")
|
QQuickStyle.setStyle("Basic")
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
"""QML-exposed controller for wafer device communication.
|
"""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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -67,9 +61,8 @@ class DeviceController(QObject):
|
|||||||
self._operation_in_progress = False
|
self._operation_in_progress = False
|
||||||
self._activity_log: list[str] = getattr(settings, 'activity_log', [])
|
self._activity_log: list[str] = getattr(settings, 'activity_log', [])
|
||||||
self._raw_bytes: Optional[bytes] = None
|
self._raw_bytes: Optional[bytes] = None
|
||||||
# Default save dir lives next to the settings file (~/Documents/isc_data/csv)
|
|
||||||
from pathlib import Path
|
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_wafer_info: dict[str, Any] = getattr(settings, 'last_wafer_info', {})
|
||||||
self._last_csv_path: str = getattr(settings, 'last_csv_path', "")
|
self._last_csv_path: str = getattr(settings, 'last_csv_path', "")
|
||||||
self._selected_port: str = getattr(settings, 'selected_port', "")
|
self._selected_port: str = getattr(settings, 'selected_port', "")
|
||||||
@@ -209,6 +202,28 @@ class DeviceController(QObject):
|
|||||||
self._activity_log = self._activity_log[-200:]
|
self._activity_log = self._activity_log[-200:]
|
||||||
self.activityLogUpdated.emit("\n".join(self._activity_log))
|
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:
|
def _set_operation_progress(self, in_progress: bool) -> None:
|
||||||
"""Set operation progress state and notify QML.
|
"""Set operation progress state and notify QML.
|
||||||
|
|
||||||
@@ -424,12 +439,10 @@ class DeviceController(QObject):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if not self._save_data_dir:
|
if not self._save_data_dir:
|
||||||
self._append_log("No save data directory set")
|
from pathlib import Path
|
||||||
self.parsedDataReady.emit({
|
self._save_data_dir = str(Path(self._data_dir) / "csv")
|
||||||
"success": False,
|
self._append_log(f"Auto-set save directory to: {self._save_data_dir}")
|
||||||
"error": "No save data directory set",
|
self._save_status()
|
||||||
})
|
|
||||||
return
|
|
||||||
|
|
||||||
fc = family_code or (
|
fc = family_code or (
|
||||||
self._last_wafer_info.get("familyCode", "")
|
self._last_wafer_info.get("familyCode", "")
|
||||||
@@ -509,7 +522,7 @@ class DeviceController(QObject):
|
|||||||
self._settings.last_csv_path = self._last_csv_path
|
self._settings.last_csv_path = self._last_csv_path
|
||||||
|
|
||||||
# Save to disk
|
# Save to disk
|
||||||
LocalSettings.save_settings(str(LocalSettings._settings_path(self._data_dir)), self._settings)
|
LocalSettings.save_settings(self._data_dir, self._settings)
|
||||||
|
|
||||||
# ---- Helpers ----
|
# ---- Helpers ----
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class LocalSettingsModel(QObject):
|
|||||||
QStandardPaths.DocumentsLocation
|
QStandardPaths.DocumentsLocation
|
||||||
)
|
)
|
||||||
base_dir = Path(documents_dir) if documents_dir else (Path.home() / "Documents")
|
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]:
|
def _new_defaults(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user