From 5105ab1ab63172955f3d574f99464bc0f28c205c Mon Sep 17 00:00:00 2001 From: jack Date: Mon, 29 Jun 2026 14:58:10 -0700 Subject: [PATCH] feat(tabs): update all tab views, dialogs, and session controller --- src/pygui/ISC/Main.qml | 2 +- src/pygui/ISC/Tabs/DataTab.qml | 52 ++-- src/pygui/ISC/Tabs/GraphTab.qml | 77 ++--- src/pygui/ISC/Tabs/SelectFileDialog.qml | 42 +-- src/pygui/ISC/Tabs/SettingsTab.qml | 56 +++- src/pygui/ISC/Tabs/StatusTab.qml | 282 +++++++++++------- src/pygui/ISC/Tabs/WaferMapTab.qml | 150 +++++----- .../Tabs/components/ReplaceSensorDialog.qml | 84 ++++-- .../ISC/Tabs/components/WaferMapView.qml | 34 +-- .../backend/controllers/session_controller.py | 35 +++ 10 files changed, 507 insertions(+), 307 deletions(-) diff --git a/src/pygui/ISC/Main.qml b/src/pygui/ISC/Main.qml index 73b1b29..e1182e5 100644 --- a/src/pygui/ISC/Main.qml +++ b/src/pygui/ISC/Main.qml @@ -5,7 +5,7 @@ import ISC Window { // ===== Window Dimensions ===== width: 1920 - height: 1080 +height: 1080 minimumWidth: 1100 minimumHeight: 700 visible: true diff --git a/src/pygui/ISC/Tabs/DataTab.qml b/src/pygui/ISC/Tabs/DataTab.qml index 302d685..ecc784d 100644 --- a/src/pygui/ISC/Tabs/DataTab.qml +++ b/src/pygui/ISC/Tabs/DataTab.qml @@ -1,7 +1,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import QtQuick.TableView import ISC // ===== Data Tab ===== @@ -18,7 +17,7 @@ Item { target: deviceController function onParsedDataReady(result) { if (result && result.success) { - dataTable.forceLayout() + dataTable.forceLayout(); } } } @@ -31,7 +30,7 @@ Item { Label { text: "No Data" - font.pixelSize: 24 + font.pixelSize: Theme.font2xl font.bold: true color: Theme.headingColor anchors.horizontalCenter: parent.horizontalCenter @@ -39,7 +38,7 @@ Item { Label { text: "Read and parse wafer data to see temperature readings here." - font.pixelSize: 14 + font.pixelSize: Theme.fontMd color: Theme.bodyColor anchors.horizontalCenter: parent.horizontalCenter wrapMode: Text.WordWrap @@ -60,17 +59,18 @@ Item { Label { text: "Temperature Data" - font.pixelSize: 18 + font.pixelSize: Theme.fontXl font.bold: true color: Theme.headingColor } - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } Label { - text: deviceController.dataRowCount + " rows × " + - deviceController.dataColCount + " sensors" - font.pixelSize: 13 + text: deviceController.dataRowCount + " rows × " + deviceController.dataColCount + " sensors" + font.pixelSize: Theme.fontSm color: Theme.bodyColor } } @@ -82,7 +82,8 @@ Item { Button { text: "Open in Excel" - icon.source: "icon/excel.svg" + icon.source: "icons/excel.svg" + icon.color: Theme.headingColor onClicked: deviceController.openCsvFile() } @@ -96,7 +97,9 @@ Item { onClicked: splitDialog.open() } - Item { Layout.fillWidth: true} + Item { + Layout.fillWidth: true + } } // --- Table container --- Rectangle { @@ -131,7 +134,7 @@ Item { // 'display' is the Qt.DisplayRole value from headerData() text: display !== undefined ? display : "" color: Theme.headingColor - font.pixelSize: 11 + font.pixelSize: Theme.fontXs font.bold: true elide: Text.ElideRight } @@ -147,14 +150,17 @@ Item { clip: true reuseItems: true - columnWidthProvider: function(col) { - if (col === 0) return 52 // row index column - const sensorCols = Math.max(1, dataTable.columns - 1) - const available = dataTable.width - 52 - return Math.max(52, Math.min(90, available / sensorCols)) + columnWidthProvider: function (col) { + if (col === 0) + return 52; // row index column + const sensorCols = Math.max(1, dataTable.columns - 1); + const available = dataTable.width - 52; + return Math.max(52, Math.min(90, available / sensorCols)); } - rowHeightProvider: function() { return 24 } + rowHeightProvider: function () { + return 24; + } delegate: Rectangle { color: row % 2 === 0 ? Theme.panelBackground : Theme.cardBackground @@ -163,13 +169,17 @@ Item { anchors.centerIn: parent text: display !== undefined ? display : "" color: Theme.bodyColor - font.pixelSize: 11 + font.pixelSize: Theme.fontXs elide: Text.ElideRight } } - ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AsNeeded } - ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AsNeeded + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } } } } diff --git a/src/pygui/ISC/Tabs/GraphTab.qml b/src/pygui/ISC/Tabs/GraphTab.qml index c88c69b..cf126e9 100644 --- a/src/pygui/ISC/Tabs/GraphTab.qml +++ b/src/pygui/ISC/Tabs/GraphTab.qml @@ -27,18 +27,20 @@ Item { Label { text: "Line Chart" color: Theme.headingColor - font.pixelSize: 13 + font.pixelSize: Theme.fontSm font.bold: true Layout.alignment: Qt.AlignVCenter } - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } // Refresh button — pulls data from deviceController Button { id: refreshBtn text: "REFRESH" - font.pixelSize: 10 + font.pixelSize: Theme.fontXs font.weight: Font.Medium implicitHeight: 26 implicitWidth: 72 @@ -68,7 +70,7 @@ Item { Label { text: "Source:" color: Theme.bodyColor - font.pixelSize: 10 + font.pixelSize: Theme.fontXs Layout.alignment: Qt.AlignVCenter } @@ -77,7 +79,7 @@ Item { model: ["Parsed Data", "Live Trend"] currentIndex: 0 implicitHeight: 26 - font.pixelSize: 11 + font.pixelSize: Theme.fontXs background: Rectangle { color: Theme.fieldBackground border.color: Theme.fieldBorder @@ -95,13 +97,15 @@ Item { onCurrentIndexChanged: reloadData() } - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } Label { id: statusLabel text: "Ready" color: Theme.bodyColor - font.pixelSize: 10 + font.pixelSize: Theme.fontXs font.italic: true Layout.alignment: Qt.AlignVCenter } @@ -125,8 +129,7 @@ Item { gridColor: Theme.softBorder axisColor: Theme.bodyColor textColor: Theme.headingColor - seriesColors: [Theme.sensorLow, Theme.sensorHigh, Theme.sensorInRange, - "#FFA726", "#AB47BC", "#00BCD4"] + seriesColors: [Theme.sensorLow, Theme.sensorHigh, Theme.sensorInRange, "#FFA726", "#AB47BC", "#00BCD4"] showLegend: true } } @@ -137,44 +140,44 @@ Item { function reloadData() { if (sourceSelector.currentIndex === 0) { // Parsed data from DeviceController - loadParsedData() + loadParsedData(); } else { // Live trend from SessionController - loadLiveTrend() + loadLiveTrend(); } } function loadParsedData() { - statusLabel.text = "Loading..." - var result = deviceController.getChartData() + statusLabel.text = "Loading..."; + var result = deviceController.getChartData(); if (!result || !result.success) { - graph.seriesData = [] - graph.sensorNames = [] - graph.title = "Sensor Temperature Over Time" - statusLabel.text = "No data — read a wafer first" - return + graph.seriesData = []; + graph.sensorNames = []; + graph.title = "Sensor Temperature Over Time"; + statusLabel.text = "No data — read a wafer first"; + return; } - graph.seriesData = result.series || [] - graph.sensorNames = result.sensor_names || [] - graph.title = "Sensor Temperature Over Time" - statusLabel.text = (result.series ? result.series.length : 0) + " sensors loaded" + graph.seriesData = result.series || []; + graph.sensorNames = result.sensor_names || []; + graph.title = "Sensor Temperature Over Time"; + statusLabel.text = (result.series ? result.series.length : 0) + " sensors loaded"; } function loadLiveTrend() { // Live trend: connects to streamController.trendData // The trend data comes as a JSON array of per-frame averages - statusLabel.text = "Connecting to live trend..." + statusLabel.text = "Connecting to live trend..."; // We need at least one data point if (streamController.stats && streamController.stats.avg !== undefined) { // Build a single-series graph from the trend buffer // For now show a placeholder — the trendData signal handles updates - graph.title = "Live Average Temperature" - graph.yLabel = "Avg Temperature (°C)" - graph.xLabel = "Time (s)" - statusLabel.text = "Live trend — waiting for data..." + graph.title = "Live Average Temperature"; + graph.yLabel = "Avg Temperature (°C)"; + graph.xLabel = "Time (s)"; + statusLabel.text = "Live trend — waiting for data..."; } else { - statusLabel.text = "No live data — start a stream on the Wafer Map tab" + statusLabel.text = "No live data — start a stream on the Wafer Map tab"; } } @@ -184,7 +187,7 @@ Item { target: deviceController function onParsedDataReady(result) { if (sourceSelector.currentIndex === 0) { - root.reloadData() + root.reloadData(); } } } @@ -194,14 +197,14 @@ Item { function onTrendData(avgsJson) { if (sourceSelector.currentIndex === 1) { try { - var avgs = JSON.parse(avgsJson) + var avgs = JSON.parse(avgsJson); if (avgs && avgs.length > 0) { - graph.seriesData = [avgs] - graph.sensorNames = ["Average"] - graph.title = "Live Average Temperature" - graph.yLabel = "Avg Temperature (°C)" - graph.xLabel = "Frame" - statusLabel.text = "Live: " + avgs.length + " data points" + graph.seriesData = [avgs]; + graph.sensorNames = ["Average"]; + graph.title = "Live Average Temperature"; + graph.yLabel = "Avg Temperature (°C)"; + graph.xLabel = "Frame"; + statusLabel.text = "Live: " + avgs.length + " data points"; } } catch (e) { // ignore parse errors @@ -213,7 +216,7 @@ Item { // On startup, try to load parsed data if available Component.onCompleted: { if (deviceController.dataRowCount > 0) { - reloadData() + reloadData(); } } } diff --git a/src/pygui/ISC/Tabs/SelectFileDialog.qml b/src/pygui/ISC/Tabs/SelectFileDialog.qml index 94a0cf9..7a2c2b8 100644 --- a/src/pygui/ISC/Tabs/SelectFileDialog.qml +++ b/src/pygui/ISC/Tabs/SelectFileDialog.qml @@ -24,9 +24,11 @@ Dialog { property int selectedRow: -1 property int metadataEditRow: -1 readonly property string masterTypeFieldText: { - if (metadataEditRow < 0) return ""; + if (metadataEditRow < 0) + return ""; const rec = tableModel[metadataEditRow]; - if (!rec) return ""; + if (!rec) + return ""; return rec.masterType ?? ""; } @@ -42,7 +44,7 @@ Dialog { chamberField.text = rec.chamber ?? ""; if (!chamberField.text && settingsModel.chamberId) chamberField.text = settingsModel.chamberId; - notesField.text = rec.notes ?? ""; + notesField.text = rec.notes ?? ""; filePathLabel.text = rec.fileName ?? ""; metadataEditDialog.open(); } @@ -76,7 +78,7 @@ Dialog { readonly property var normalizedRows: { // Touch settingsModel.masters so QML tracks this dependency. const _ = settingsModel.masters; - return (tableModel || []).map(function(row) { + return (tableModel || []).map(function (row) { row = row || {}; // settingsModel is the authoritative source; fall back to sidecar masterType. const masterType = root.masterFamilyForPath(row.fileName ?? "") || row.masterType || ""; @@ -113,7 +115,7 @@ Dialog { verticalAlignment: Text.AlignVCenter elide: Text.ElideRight font.bold: true - font.pixelSize: 13 + font.pixelSize: Theme.fontSm } } @@ -129,6 +131,12 @@ Dialog { color: Theme.fieldBackground border.width: dialogTextInput.activeFocus ? Theme.borderStrong : Theme.borderThin border.color: dialogTextInput.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder + Behavior on color { + ColorAnimation { duration: Theme.durationFast } + } + Behavior on border.color { + ColorAnimation { duration: Theme.durationFast } + } } } @@ -163,7 +171,7 @@ Dialog { anchors.left: parent.left anchors.leftMargin: 16 text: root.title - font.pixelSize: 15 + font.pixelSize: Theme.fontLg font.bold: true color: Theme.headingColor } @@ -221,7 +229,7 @@ Dialog { text: file_browser.currentDirectory color: Theme.fieldText elide: Text.ElideMiddle - font.pixelSize: 13 + font.pixelSize: Theme.fontSm } } } @@ -242,7 +250,7 @@ Dialog { anchors.centerIn: parent text: root.headerTitles[index] ?? "" font.bold: true - font.pixelSize: 12 + font.pixelSize: Theme.fontSm color: Theme.headingColor } } @@ -329,7 +337,7 @@ Dialog { const mt = root.normalizedRows[row]?.masterType ?? ""; return mt || "—"; } - font.pixelSize: 14 + font.pixelSize: Theme.fontMd font.bold: true color: { const mt = root.normalizedRows[row]?.masterType ?? ""; @@ -346,7 +354,7 @@ Dialog { sourceComponent: Button { hoverEnabled: true text: "Edit…" - font.pixelSize: 12 + font.pixelSize: Theme.fontSm onClicked: root.openMetadataEditor(row) background: Rectangle { @@ -361,7 +369,7 @@ Dialog { color: Theme.buttonNeutralText horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - font.pixelSize: 12 + font.pixelSize: Theme.fontSm } } } @@ -388,7 +396,7 @@ Dialog { return ""; } elide: Text.ElideRight - font.pixelSize: 13 + font.pixelSize: Theme.fontSm color: Theme.bodyColor } } @@ -409,12 +417,12 @@ Dialog { return record.notes ?? ""; } elide: Text.ElideRight - font.pixelSize: 13 + font.pixelSize: Theme.fontSm color: Theme.bodyColor } } - MouseArea { + MouseArea { anchors.fill: parent enabled: column !== 6 onClicked: { @@ -456,7 +464,7 @@ Dialog { verticalAlignment: Text.AlignVCenter text: root.selectedPath elide: Text.ElideLeft - font.pixelSize: 13 + font.pixelSize: Theme.fontSm color: root.selectedPath ? Theme.fieldText : Theme.fieldPlaceholder } } @@ -504,7 +512,7 @@ Dialog { anchors.left: parent.left anchors.leftMargin: 16 text: metadataEditDialog.title - font.pixelSize: 15 + font.pixelSize: Theme.fontLg font.bold: true color: Theme.headingColor } @@ -530,7 +538,7 @@ Dialog { maximumLineCount: 3 elide: Text.ElideRight color: Theme.bodyColor - font.pixelSize: 12 + font.pixelSize: Theme.fontSm } Label { diff --git a/src/pygui/ISC/Tabs/SettingsTab.qml b/src/pygui/ISC/Tabs/SettingsTab.qml index cef4045..b16d3fe 100644 --- a/src/pygui/ISC/Tabs/SettingsTab.qml +++ b/src/pygui/ISC/Tabs/SettingsTab.qml @@ -27,8 +27,7 @@ Item { } // ===== Settings Data Helpers ===== - readonly property var masterFamilies: ["A", "B", "C", "D", "E", "F", "P", "X", "Z"] - + // ListModel for master families defined inline in the ColumnLayout below function masterPath(family) { const table = settingsModel.masters; return table && table[family] ? table[family] : ""; @@ -58,7 +57,7 @@ Item { label: Label { text: settingsGroup.title color: Theme.panelTitleText - font.pixelSize: 16 + font.pixelSize: Theme.fontLg font.bold: true } } @@ -75,6 +74,12 @@ Item { color: Theme.fieldBackground border.width: textInput.activeFocus ? Theme.borderStrong : Theme.borderThin border.color: textInput.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder + Behavior on color { + ColorAnimation { duration: Theme.durationFast } + } + Behavior on border.color { + ColorAnimation { duration: Theme.durationFast } + } } } @@ -111,13 +116,23 @@ Item { border.width: Theme.borderThin border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder + Behavior on color { + ColorAnimation { duration: Theme.durationFast } + } + Behavior on border.color { + ColorAnimation { duration: Theme.durationFast } + } + Text { anchors.centerIn: parent text: "✓" - font.pixelSize: 14 + font.pixelSize: Theme.fontMd font.bold: true color: Theme.panelBackground - visible: toggle.checked + opacity: toggle.checked ? 1.0 : 0.0 + Behavior on opacity { + NumberAnimation { duration: Theme.durationFast } + } // Small nudge to center the checkmark perfectly verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter @@ -129,7 +144,7 @@ Item { color: Theme.headingColor verticalAlignment: Text.AlignVCenter leftPadding: toggle.indicator.width + toggle.spacing - font.pixelSize: 13 + font.pixelSize: Theme.fontSm } } @@ -159,6 +174,7 @@ Item { anchors.top: settingsScroll.top anchors.right: settingsScroll.right anchors.bottom: settingsScroll.bottom + anchors.leftMargin: 6 policy: ScrollBar.AsNeeded contentItem: Rectangle { @@ -174,16 +190,19 @@ Item { } } + // Gap reserved between the centered content and the right-edge scrollbar. + readonly property int scrollGap: 100 + ColumnLayout { id: container - width: Math.min(settingsScroll.availableWidth, Theme.settingsPanelMaxWidth) + width: Math.min(settingsScroll.availableWidth - settingsScroll.scrollGap, Theme.settingsPanelMaxWidth) x: (settingsScroll.availableWidth - width) / 2 spacing: Theme.settingsSectionSpacing // ===== Page Title ===== Label { text: "Settings" - font.pixelSize: 30 + font.pixelSize: Theme.font3xl font.bold: true color: Theme.headingColor } @@ -241,11 +260,24 @@ Item { anchors.fill: parent spacing: Theme.settingsGridSpacing + ListModel { + id: masterModel + ListElement { letter: "A" } + ListElement { letter: "B" } + ListElement { letter: "C" } + ListElement { letter: "D" } + ListElement { letter: "E" } + ListElement { letter: "F" } + ListElement { letter: "P" } + ListElement { letter: "X" } + ListElement { letter: "Z" } + } + Repeater { - model: root.masterFamilies + model: masterModel RowLayout { - property string family: modelData + property string family: model.letter Layout.fillWidth: true spacing: Theme.settingsRowSpacing @@ -361,7 +393,7 @@ Item { Label { text: Theme.isDarkMode ? "Dark" : "Light" color: Theme.bodyColor - font.pixelSize: 13 + font.pixelSize: Theme.fontSm Layout.alignment: Qt.AlignVCenter Behavior on color { @@ -423,7 +455,7 @@ Item { Label { anchors.centerIn: parent text: "↓ Scroll for more" - font.pixelSize: 13 + font.pixelSize: Theme.fontSm font.bold: true color: Theme.bodyColor opacity: (settingsScroll.ScrollBar.vertical.position <= 0 && settingsScroll.contentHeight > settingsScroll.height) ? (scrollHintTimer.running ? 1 : 0) : 0 diff --git a/src/pygui/ISC/Tabs/StatusTab.qml b/src/pygui/ISC/Tabs/StatusTab.qml index 88f7d7c..7e5dfb8 100644 --- a/src/pygui/ISC/Tabs/StatusTab.qml +++ b/src/pygui/ISC/Tabs/StatusTab.qml @@ -27,24 +27,24 @@ ColumnLayout { property bool dataParsed: false function cleanFolderUrl(url) { - return decodeURIComponent(String(url).replace(/^file:\/\//, "")) + return decodeURIComponent(String(url).replace(/^file:\/\//, "")); } function currentFamilyCode() { - var info = deviceController.lastWaferInfo - return info && info.length > 0 ? (info[0] || "") : "" + var info = deviceController.lastWaferInfo; + return info && info.length > 0 ? (info[0] || "") : ""; } function parseAndSavePendingRead() { - deviceController.parseAndSaveData(root.currentFamilyCode(), deviceController.selectedPort || "") + deviceController.parseAndSaveData(root.currentFamilyCode(), deviceController.selectedPort || ""); } FolderDialog { id: saveDirDialog title: "Choose a folder to save." onAccepted: { - deviceController.setSaveDataDir(root.cleanFolderUrl(selectedFolder)) - root.parseAndSavePendingRead() + deviceController.setSaveDataDir(root.cleanFolderUrl(selectedFolder)); + root.parseAndSavePendingRead(); } } @@ -59,9 +59,11 @@ ColumnLayout { Layout.preferredHeight: 92 color: Theme.cardBackground border.color: { - if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor - if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor - return Theme.cardBorder + if (deviceController.connectionStatus === "Connected") + return Theme.statusSuccessColor; + if (deviceController.connectionStatus === "Disconnected") + return Theme.statusErrorColor; + return Theme.cardBorder; } radius: Theme.radiusSm @@ -73,19 +75,21 @@ ColumnLayout { Text { text: deviceController.connectionStatus color: { - if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor - if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor - return Theme.bodyColor + if (deviceController.connectionStatus === "Connected") + return Theme.statusSuccessColor; + if (deviceController.connectionStatus === "Disconnected") + return Theme.statusErrorColor; + return Theme.bodyColor; } font.bold: true - font.pixelSize: 16 + font.pixelSize: Theme.fontLg Layout.fillWidth: true } Text { text: "Port: " + (deviceController.selectedPort || "None selected") color: Theme.bodyColor - font.pixelSize: 14 + font.pixelSize: Theme.fontMd Layout.fillWidth: true } @@ -97,7 +101,7 @@ ColumnLayout { Text { text: "Save dir: " + (deviceController.saveDataDir || "None selected") color: Theme.bodyColor - font.pixelSize: 12 + font.pixelSize: Theme.fontSm opacity: 0.7 elide: Text.ElideMiddle Layout.fillWidth: true @@ -107,7 +111,7 @@ ColumnLayout { Button { id: browseBtn text: "BROWSE..." - font.pixelSize: 9 + font.pixelSize: Theme.font2xs font.weight: Font.Medium implicitHeight: 20 implicitWidth: 65 @@ -149,22 +153,52 @@ ColumnLayout { ColumnLayout { Layout.fillWidth: true spacing: 4 - Text { text: "Family Code"; color: Theme.subheadingColor; font.pixelSize: 12 } - Text { id: waferInfoFamily; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } + Text { + text: "Family Code" + color: Theme.subheadingColor + font.pixelSize: Theme.fontSm + } + Text { + id: waferInfoFamily + text: "—" + color: Theme.bodyColor + font.pixelSize: Theme.fontLg + font.bold: true + } } ColumnLayout { Layout.fillWidth: true spacing: 4 - Text { text: "Serial Number"; color: Theme.subheadingColor; font.pixelSize: 12 } - Text { id: waferSerial; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } + Text { + text: "Serial Number" + color: Theme.subheadingColor + font.pixelSize: Theme.fontSm + } + Text { + id: waferSerial + text: "—" + color: Theme.bodyColor + font.pixelSize: Theme.fontLg + font.bold: true + } } ColumnLayout { Layout.fillWidth: true spacing: 4 - Text { text: "Sensor Count"; color: Theme.subheadingColor; font.pixelSize: 12 } - Text { id: waferSensors; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } + Text { + text: "Sensor Count" + color: Theme.subheadingColor + font.pixelSize: Theme.fontSm + } + Text { + id: waferSensors + text: "—" + color: Theme.bodyColor + font.pixelSize: Theme.fontLg + font.bold: true + } } } @@ -175,15 +209,35 @@ ColumnLayout { ColumnLayout { Layout.fillWidth: true spacing: 4 - Text { text: "Runtime"; color: Theme.subheadingColor; font.pixelSize: 12 } - Text { id: waferRuntime; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } + Text { + text: "Runtime" + color: Theme.subheadingColor + font.pixelSize: Theme.fontSm + } + Text { + id: waferRuntime + text: "—" + color: Theme.bodyColor + font.pixelSize: Theme.fontLg + font.bold: true + } } ColumnLayout { Layout.fillWidth: true spacing: 4 - Text { text: "Cycles"; color: Theme.subheadingColor; font.pixelSize: 12 } - Text { id: waferCycles; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } + Text { + text: "Cycles" + color: Theme.subheadingColor + font.pixelSize: Theme.fontSm + } + Text { + id: waferCycles + text: "—" + color: Theme.bodyColor + font.pixelSize: Theme.fontLg + font.bold: true + } } } } @@ -203,22 +257,49 @@ ColumnLayout { ColumnLayout { Layout.fillWidth: true spacing: 4 - Text { text: "Rows"; color: Theme.subheadingColor; font.pixelSize: 12 } - Text { text: String(root.dataRows); color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } + Text { + text: "Rows" + color: Theme.subheadingColor + font.pixelSize: Theme.fontSm + } + Text { + text: String(root.dataRows) + color: Theme.bodyColor + font.pixelSize: Theme.fontLg + font.bold: true + } } ColumnLayout { Layout.fillWidth: true spacing: 4 - Text { text: "Sensors"; color: Theme.subheadingColor; font.pixelSize: 12 } - Text { text: String(root.dataCols); color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } + Text { + text: "Sensors" + color: Theme.subheadingColor + font.pixelSize: Theme.fontSm + } + Text { + text: String(root.dataCols) + color: Theme.bodyColor + font.pixelSize: Theme.fontLg + font.bold: true + } } ColumnLayout { Layout.fillWidth: true spacing: 4 - Text { text: "CSV"; color: Theme.subheadingColor; font.pixelSize: 12 } - Text { text: root.csvPath; color: Theme.bodyColor; font.pixelSize: 12; elide: Text.ElideMiddle } + Text { + text: "CSV" + color: Theme.subheadingColor + font.pixelSize: Theme.fontSm + } + Text { + text: root.csvPath + color: Theme.bodyColor + font.pixelSize: Theme.fontSm + elide: Text.ElideMiddle + } } } } @@ -241,25 +322,25 @@ ColumnLayout { 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.pixelSize: Theme.fontXs font.letterSpacing: 1.8 font.weight: Font.Medium Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter } - + Button { text: "REFRESH SESSION" - font.pixelSize: 10 + font.pixelSize: Theme.fontXs implicitHeight: 24 hoverEnabled: true background: Rectangle { @@ -275,10 +356,10 @@ ColumnLayout { } onClicked: deviceController.clearSession() } - + Button { text: "CLEAR LOG" - font.pixelSize: 10 + font.pixelSize: Theme.fontXs implicitHeight: 24 hoverEnabled: true background: Rectangle { @@ -295,7 +376,7 @@ ColumnLayout { onClicked: deviceController.clearActivityLog() } } - + Rectangle { anchors.bottom: parent.bottom anchors.left: parent.left @@ -311,88 +392,87 @@ ColumnLayout { Layout.margins: Theme.panelPadding clip: true - TextArea { - id: activityLog - width: parent.width - text: qsTr("Welcome — use the side rail to detect a wafer, or import data to review.") - readOnly: true - font.family: "monospace" - font.pixelSize: 12 - wrapMode: TextArea.WordWrap - color: Theme.bodyColor - background: null - } + TextArea { + id: activityLog + width: parent.width + text: qsTr("Welcome — use the side rail to detect a wafer, or import data to review.") + readOnly: true + font.family: "monospace" + font.pixelSize: Theme.fontSm + wrapMode: TextArea.WordWrap + color: Theme.bodyColor + background: null + } } } Connections { target: deviceController function onActivityLogUpdated(newLog) { - activityLog.text = newLog + activityLog.text = newLog; } } } } // --- Signal Handlers --- - Connections { + Connections { target: deviceController function onLogMessage(message) { - if (message == "CHOOSE_SAVE_DIR"){ - saveDirDialog.open() + if (message == "CHOOSE_SAVE_DIR") { + saveDirDialog.open(); } } - - // Any operation start (detect/read/erase/debug) latches the panel on. - function onPortsUpdated() { - // Status tab always visible now - } + // Any operation start (detect/read/erase/debug) latches the panel on. + function onPortsUpdated() { + // Status tab always visible now + } - function onDetectResult(result) { - if (result && result.familyCode) { - root.waferDetected = true - waferInfoFamily.text = result.familyCode - waferSerial.text = result.serialNumber - waferSensors.text = String(result.sensorCount) - waferRuntime.text = result.runtime + "s" - waferCycles.text = String(result.cycleCount) - } - // On failure, keep the last displayed wafer info and do not change waferDetected. - } + function onDetectResult(result) { + if (result && result.familyCode) { + root.waferDetected = true; + waferInfoFamily.text = result.familyCode; + waferSerial.text = result.serialNumber; + waferSensors.text = String(result.sensorCount); + waferRuntime.text = result.runtime + "s"; + waferCycles.text = String(result.cycleCount); + } + // On failure, keep the last displayed wafer info and do not change waferDetected. + } - function onStatusRestored() { - // Show restored wafer info if available - var info = deviceController.lastWaferInfo - if (info && info.length > 0) { - root.waferDetected = true - waferInfoFamily.text = info[0] || "—" - waferSerial.text = info[1] || "—" - waferSensors.text = String(info[2] || 0) - waferRuntime.text = (info[3] || 0) + "s" - waferCycles.text = String(info[4] || 0) - } - // Show data summary if data was previously parsed - if (deviceController.dataRowCount > 0) { - root.dataParsed = true - root.dataRows = deviceController.dataRowCount - root.dataCols = deviceController.dataColCount - root.csvPath = "" // CSV path not persisted as full path, just show it was parsed - } - } - } + function onStatusRestored() { + // Show restored wafer info if available + var info = deviceController.lastWaferInfo; + if (info && info.length > 0) { + root.waferDetected = true; + waferInfoFamily.text = info[0] || "—"; + waferSerial.text = info[1] || "—"; + waferSensors.text = String(info[2] || 0); + waferRuntime.text = (info[3] || 0) + "s"; + waferCycles.text = String(info[4] || 0); + } + // Show data summary if data was previously parsed + if (deviceController.dataRowCount > 0) { + root.dataParsed = true; + root.dataRows = deviceController.dataRowCount; + root.dataCols = deviceController.dataColCount; + root.csvPath = ""; // CSV path not persisted as full path, just show it was parsed + } + } + } Connections { target: deviceController function onReadResult(result) { - root.dataParsed = false - root.dataRows = 0 - root.dataCols = 0 - root.csvPath = "" + root.dataParsed = false; + root.dataRows = 0; + root.dataCols = 0; + root.csvPath = ""; if (result && result.success === true) - saveDirDialog.open() + saveDirDialog.open(); } } @@ -400,12 +480,12 @@ ColumnLayout { target: deviceController function onParsedDataReady(result) { if (result && result.success) { - root.dataParsed = true - root.dataRows = result.rows - root.dataCols = result.cols - root.csvPath = result.csv_path || "" + root.dataParsed = true; + root.dataRows = result.rows; + root.dataCols = result.cols; + root.csvPath = result.csv_path || ""; } else { - root.dataParsed = false + root.dataParsed = false; } } } diff --git a/src/pygui/ISC/Tabs/WaferMapTab.qml b/src/pygui/ISC/Tabs/WaferMapTab.qml index ab5452c..8e049b3 100644 --- a/src/pygui/ISC/Tabs/WaferMapTab.qml +++ b/src/pygui/ISC/Tabs/WaferMapTab.qml @@ -18,12 +18,13 @@ Item { repeat: true running: streamController.mode === "live" && streamController.state !== "idle" onTriggered: root._liveSecs++ - onRunningChanged: if (!running) root._liveSecs = 0 + onRunningChanged: if (!running) + root._liveSecs = 0 } function fmtTime(s) { - var m = Math.floor(s / 60) - var ss = s % 60 - return (m < 10 ? "0" : "") + m + ":" + (ss < 10 ? "0" : "") + ss + var m = Math.floor(s / 60); + var ss = s % 60; + return (m < 10 ? "0" : "") + m + ":" + (ss < 10 ? "0" : "") + ss; } // Wire streamController.trendData (Signal(str) carrying JSON list of floats) // into the trend chart's data property. The slot parseJsonToData() is defined @@ -32,7 +33,7 @@ Item { Connections { target: streamController function onTrendData(avgsJson) { - trendChart.setDataFromJson(avgsJson) + trendChart.setDataFromJson(avgsJson); } } @@ -60,7 +61,8 @@ Item { } TabButton { text: "Review" - implicitWidth: 64; implicitHeight: 28 + implicitWidth: 64 + implicitHeight: 28 background: Rectangle { color: parent.checked ? Theme.tabActiveBackground : "transparent" radius: Theme.radiusSm - 1 @@ -68,7 +70,7 @@ Item { contentItem: Text { text: parent.text color: parent.checked ? Theme.headingColor : Theme.bodyColor - font.pixelSize: 11 + font.pixelSize: Theme.fontSm font.weight: parent.checked ? Font.Medium : Font.Normal horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter @@ -78,7 +80,8 @@ Item { text: "Live" enabled: deviceController.connectionStatus === "Connected" opacity: enabled ? 1.0 : 0.4 - implicitWidth: 58; implicitHeight: 28 + implicitWidth: 58 + implicitHeight: 28 background: Rectangle { color: parent.checked ? Theme.tabActiveBackground : "transparent" radius: Theme.radiusSm - 1 @@ -86,30 +89,29 @@ Item { contentItem: Text { text: parent.text color: parent.checked ? Theme.headingColor : Theme.bodyColor - font.pixelSize: 11 + font.pixelSize: Theme.fontSm font.weight: parent.checked ? Font.Medium : Font.Normal horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } - onCurrentIndexChanged: - if (currentIndex === 1){ - // Guard: revert to Review if not connected - if (deviceController.connectionStatus !== "Connected") { - currentIndex = 0 - return - } - // Entering Live mode - streamController.setMode("live") - 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") + onCurrentIndexChanged: if (currentIndex === 1) { + // Guard: revert to Review if not connected + if (deviceController.connectionStatus !== "Connected") { + currentIndex = 0; + return; } + // Entering Live mode + streamController.setMode("live"); + 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 @@ -120,33 +122,44 @@ Item { Label { id: liveIndicator text: "◉" - color: (deviceController.connectionStatus === "Connected" && streamController.state !== "idle") - ? Theme.liveColor : Theme.bodyColor - font.pixelSize: 13 - + color: (deviceController.connectionStatus === "Connected" && streamController.state !== "idle") ? Theme.liveColor : Theme.bodyColor + font.pixelSize: Theme.fontSm + SequentialAnimation on opacity { running: deviceController.connectionStatus === "Connected" && streamController.state !== "idle" loops: Animation.Infinite - NumberAnimation { to: 0.2; duration: 600; easing.type: Easing.InOutQuad } - NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutQuad } + NumberAnimation { + to: 0.2 + duration: 600 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1.0 + duration: 600 + easing.type: Easing.InOutQuad + } onRunningChanged: { - if (!running) liveIndicator.opacity = 1.0 + if (!running) + liveIndicator.opacity = 1.0; } } } Label { text: "Live stream" color: Theme.bodyColor - font.pixelSize: 11 + font.pixelSize: Theme.fontXs } } - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } // SET badge + avg temp Rectangle { visible: streamController.state === "set" - height: 22; radius: Theme.radiusSm + height: 22 + radius: Theme.radiusSm color: "transparent" border.color: Theme.liveColor border.width: 1 @@ -154,10 +167,9 @@ Item { Label { id: setLabel anchors.centerIn: parent - text: "SET " + (streamController.stats.avg !== undefined - ? streamController.stats.avg + " avg" : "") + text: "SET " + (streamController.stats.avg !== undefined ? streamController.stats.avg + " avg" : "") color: Theme.liveColor - font.pixelSize: 10 + font.pixelSize: Theme.fontXs font.weight: Font.Medium font.letterSpacing: 0.8 } @@ -168,19 +180,27 @@ Item { spacing: 5 visible: streamController.recording Rectangle { - width: 7; height: 7; radius: 4 + width: 7 + height: 7 + radius: 4 color: Theme.recordColor SequentialAnimation on opacity { running: streamController.recording loops: Animation.Infinite - NumberAnimation { to: 0.2; duration: 600 } - NumberAnimation { to: 1.0; duration: 600 } + NumberAnimation { + to: 0.2 + duration: 600 + } + NumberAnimation { + to: 1.0 + duration: 600 + } } } Label { text: "REC" color: Theme.recordColor - font.pixelSize: 10 + font.pixelSize: Theme.fontXs font.weight: Font.Medium font.letterSpacing: 1.2 } @@ -197,7 +217,7 @@ Item { // Label { // text: "LIVE " + root.fmtTime(root._liveSecs) // color: Theme.liveColor - // font.pixelSize: 10 + // font.pixelSize: Theme.fontXs // font.weight: Font.Medium // font.letterSpacing: 0.8 // } @@ -208,11 +228,11 @@ Item { // visible: streamController.mode === "review" || streamController.state === "idle" // text: streamController.state.toUpperCas`e`() // color: Theme.bodyColor - // font.pixelSize: 11 + // font.pixelSize: Theme.fontXs // font.weight: Font.Medium // font.letterSpacing: 1.2 // } - Button{ + Button { text: "Export PNG" onClicked: exportDialog.open() } @@ -248,21 +268,7 @@ Item { Layout.fillHeight: true spacing: 16 - // Source panel — bordered card - Rectangle { - Layout.preferredWidth: 220 - Layout.fillHeight: true - color: Theme.cardBackground - border.color: Theme.cardBorder - border.width: 1 - radius: Theme.radiusMd - clip: true - - SourcePanel { - anchors.fill: parent - anchors.margins: 10 - } - } + // Source panel — moved to side rail (P2.1) ColumnLayout { Layout.fillWidth: true @@ -280,7 +286,7 @@ Item { id: trendPane Layout.fillWidth: true implicitHeight: 160 - visible: trendChart.hasData + visible: trendChart.hasData && streamController.mode === "live" color: Theme.cardBackground border.color: Theme.cardBorder border.width: 1 @@ -293,28 +299,28 @@ Item { } } - Item { Layout.fillWidth: true; implicitHeight: 16 } - TransportBar { + Item { + Layout.fillWidth: true + implicitHeight: 16 + } + TransportBar { Layout.fillWidth: true - visible: streamController.mode !== "live" || trendChart.hasData - height: visible ? implicitHeight : 0 } } - // Readout panel — bordered card + // Readout panel — floating wrapper box Rectangle { Layout.preferredWidth: 220 Layout.fillHeight: true - color: Theme.cardBackground - border.color: Theme.cardBorder - border.width: 1 - radius: Theme.radiusMd + color: "transparent" + border.color: "transparent" + border.width: 0 clip: true ReadoutPanel { id: readoutPanel anchors.fill: parent - anchors.margins: 10 + anchors.margins: 0 } } } diff --git a/src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml b/src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml index 8dd37e8..cf43116 100644 --- a/src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml +++ b/src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml @@ -17,14 +17,15 @@ Dialog { property bool hasOverride: streamController.overriddenSensors.indexOf(sensorIndex) >= 0 function openFor(index) { - sensorIndex = index - var dot = streamController.sensorDots[index] - if (!dot) return - sensorLabel = dot.label !== undefined ? String(dot.label) : String(index + 1) - currentValue = dot.value - valueField.text = "" - offsetField.text = "" - open() + sensorIndex = index; + var dot = streamController.sensorDots[index]; + if (!dot) + return; + sensorLabel = dot.label !== undefined ? String(dot.label) : String(index + 1); + currentValue = dot.value; + valueField.text = ""; + offsetField.text = ""; + open(); } background: Rectangle { @@ -42,11 +43,10 @@ Dialog { RowLayout { Layout.fillWidth: true Label { - text: "Sensor #" + (root.sensorIndex + 1) + - (root.sensorLabel ? " (" + root.sensorLabel + ")" : "") + text: "Sensor #" + (root.sensorIndex + 1) + (root.sensorLabel ? " (" + root.sensorLabel + ")" : "") color: Theme.headingColor font.bold: true - font.pixelSize: 13 + font.pixelSize: Theme.fontSm Layout.fillWidth: true } Rectangle { @@ -61,7 +61,7 @@ Dialog { anchors.centerIn: parent text: "OVERRIDE" color: "white" - font.pixelSize: 10 + font.pixelSize: Theme.fontXs font.bold: true font.letterSpacing: 1 } @@ -71,13 +71,21 @@ Dialog { Label { text: "Live: " + root.currentValue.toFixed(2) color: Theme.bodyColor - font.pixelSize: 12 + font.pixelSize: Theme.fontSm } - Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder } + Rectangle { + Layout.fillWidth: true + height: 1 + color: Theme.cardBorder + } // ── replace field ───────────────────────────────────────── - Label { text: "Replace with value"; color: Theme.bodyColor; font.pixelSize: 11 } + Label { + text: "Replace with value" + color: Theme.bodyColor + font.pixelSize: Theme.fontXs + } TextField { id: valueField Layout.fillWidth: true @@ -92,11 +100,21 @@ Dialog { color: Theme.fieldBackground border.width: valueField.activeFocus ? Theme.borderStrong : Theme.borderThin border.color: valueField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder + Behavior on color { + ColorAnimation { duration: Theme.durationFast } + } + Behavior on border.color { + ColorAnimation { duration: Theme.durationFast } + } } } // ── offset field ────────────────────────────────────────── - Label { text: "Or add offset (± delta)"; color: Theme.bodyColor; font.pixelSize: 11 } + Label { + text: "Or add offset (± delta)" + color: Theme.bodyColor + font.pixelSize: Theme.fontXs + } TextField { id: offsetField Layout.fillWidth: true @@ -111,10 +129,20 @@ Dialog { color: Theme.fieldBackground border.width: offsetField.activeFocus ? Theme.borderStrong : Theme.borderThin border.color: offsetField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder + Behavior on color { + ColorAnimation { duration: Theme.durationFast } + } + Behavior on border.color { + ColorAnimation { duration: Theme.durationFast } + } } } - Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder } + Rectangle { + Layout.fillWidth: true + height: 1 + color: Theme.cardBorder + } // ── buttons ─────────────────────────────────────────────── RowLayout { @@ -140,20 +168,20 @@ Dialog { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.bold: true - font.pixelSize: 13 + font.pixelSize: Theme.fontSm } onClicked: { if (valueField.text !== "") { - var v = parseFloat(valueField.text) + var v = parseFloat(valueField.text); if (!isNaN(v)) - streamController.replaceSensor(root.sensorIndex, v) + streamController.replaceSensor(root.sensorIndex, v); } if (offsetField.text !== "") { - var d = parseFloat(offsetField.text) + var d = parseFloat(offsetField.text); if (!isNaN(d)) - streamController.offsetSensor(root.sensorIndex, d) + streamController.offsetSensor(root.sensorIndex, d); } - root.close() + root.close(); } } Button { @@ -175,11 +203,11 @@ Dialog { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.bold: true - font.pixelSize: 13 + font.pixelSize: Theme.fontSm } onClicked: { - streamController.clearSensorEdit(root.sensorIndex) - root.close() + streamController.clearSensorEdit(root.sensorIndex); + root.close(); } } Button { @@ -198,10 +226,10 @@ Dialog { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.bold: true - font.pixelSize: 13 + font.pixelSize: Theme.fontSm } onClicked: root.close() } } } -} \ No newline at end of file +} diff --git a/src/pygui/ISC/Tabs/components/WaferMapView.qml b/src/pygui/ISC/Tabs/components/WaferMapView.qml index 3194104..d3e3a8f 100644 --- a/src/pygui/ISC/Tabs/components/WaferMapView.qml +++ b/src/pygui/ISC/Tabs/components/WaferMapView.qml @@ -1,8 +1,7 @@ import QtQuick import QtQuick.Controls import ISC -import ISC.Wafer - +import ISC.Wafer Item { id: root @@ -22,28 +21,27 @@ Item { blend: root.blend showLabels: root.showLabels // Bind to Theme so colors update on dark/light mode switch - ringColor: Theme.waferRingColor - axisColor: Theme.waferAxisColor - lowColor: Theme.sensorLow + ringColor: Theme.waferRingColor + axisColor: Theme.waferAxisColor + lowColor: Theme.sensorLow inRangeColor: Theme.sensorInRange - highColor: Theme.sensorHigh - textColor: Theme.headingColor - + highColor: Theme.sensorHigh + textColor: Theme.headingColor TapHandler { - onTapped: (ev) => { - var idx = map.which_marker(ev.position.x, ev.position.y) - if (idx >= 0) replaceDialog.openFor(idx) + onTapped: ev => { + var idx = map.which_marker(ev.position.x, ev.position.y); + if (idx >= 0) + replaceDialog.openFor(idx); } } } - ReplaceSensorDialog { id: replaceDialog} - - function exportImage(filePath) { - return map.export_image(filePath) + ReplaceSensorDialog { + id: replaceDialog } - - -} \ No newline at end of file + function exportImage(filePath) { + return map.export_image(filePath); + } +} diff --git a/src/pygui/backend/controllers/session_controller.py b/src/pygui/backend/controllers/session_controller.py index 8d54907..c715c05 100644 --- a/src/pygui/backend/controllers/session_controller.py +++ b/src/pygui/backend/controllers/session_controller.py @@ -37,10 +37,12 @@ class SessionController(QObject): loadedFileChanged = Signal() clusterAveragingEnabledChanged = Signal() settingsChanged = Signal() # emitted when session state changes that should be persisted + liveStatsChanged = Signal() # emitted when receivedCount or errorCount change # 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 + _liveError = Signal() # worker-thread error ping def __init__(self, parent: QObject | None = None, settings: dict | None = None) -> None: @@ -72,6 +74,7 @@ class SessionController(QObject): self._dirty = False self._liveFrame.connect(self._on_live_frame, Qt.ConnectionType.QueuedConnection) + self._liveError.connect(self._on_live_error, Qt.ConnectionType.QueuedConnection) self._sensor_editor = SensorEditor() self._last_raw_frame: Frame | None = None @@ -79,6 +82,8 @@ class SessionController(QObject): self._cluster_averaging_enabled = False self._active_clusters: list[list[int]] = [] self._stats_tracker = ReplayStatsTracker() + self._received_count: int = 0 + self._error_count: int = 0 # Restore persisted state if available self._load_settings(settings or {}) @@ -105,6 +110,9 @@ class SessionController(QObject): @Property(int, notify=frameUpdated) def frameTotal(self) -> int: return self._player.total + @Property(bool, notify=stateChanged) + def playing(self) -> bool: return self._play_timer.isActive() + @Property(str, notify=stateChanged) def state(self) -> str: if self._last: @@ -210,6 +218,14 @@ class SessionController(QObject): self.clusterAveragingEnabledChanged.emit() self._reprocess_current() + @Property(int, notify=liveStatsChanged) + def receivedCount(self) -> int: + return self._received_count + + @Property(int, notify=liveStatsChanged) + def errorCount(self) -> int: + return self._error_count + # ---- mode + thresholds ---- @Slot(str) @slot_error_boundary @@ -292,11 +308,13 @@ class SessionController(QObject): @slot_error_boundary def play(self) -> None: self._play_timer.start(self._next_interval_ms()) + self.stateChanged.emit() @Slot() @slot_error_boundary def pause(self) -> None: self._play_timer.stop() + self.stateChanged.emit() @Slot() @slot_error_boundary @@ -304,6 +322,7 @@ class SessionController(QObject): self._play_timer.stop() self._player.seek(0) self._emit_current() + self.stateChanged.emit() @Slot(int) @slot_error_boundary @@ -311,6 +330,7 @@ class SessionController(QObject): self._play_timer.stop() self._player.step(delta) self._emit_current() + self.stateChanged.emit() @Slot(int) @slot_error_boundary @@ -335,6 +355,7 @@ class SessionController(QObject): def _advance(self) -> None: if self._player.at_end: self._play_timer.stop() + self.stateChanged.emit() return self._player.step(1) self._emit_current() @@ -419,6 +440,9 @@ class SessionController(QObject): self._stats_tracker.reset() self._last = None self._last_raw_frame = None + self._received_count = 0 + self._error_count = 0 + self.liveStatsChanged.emit() transport = SerialPort.open_port(port, timeout=1) # Send 'D2' command padded to 512 bytes to start the stream @@ -427,6 +451,7 @@ class SessionController(QObject): def on_error(exc: Exception): log.error("Live stream error: %s", exc) + self._liveError.emit() self._reader = StreamReader( transport, parse_binary_frame, @@ -444,6 +469,9 @@ class SessionController(QObject): def stopStream(self) -> None: self._repaint_timer.stop() self._trend_timer.stop() + self._received_count = 0 + self._error_count = 0 + self.liveStatsChanged.emit() if self._reader: transport = self._reader._transport self._reader.stop() @@ -465,6 +493,8 @@ class SessionController(QObject): @slot_error_boundary def _on_live_frame(self, frame: Frame) -> None: import json + self._received_count += 1 + self.liveStatsChanged.emit() self._stats_tracker.track(frame.seq, frame.values) # Main thread (queued). Record raw, process edited; mark dirty for repaint. self._last_raw_frame = frame @@ -498,6 +528,11 @@ class SessionController(QObject): self.frameUpdated.emit() self.stateChanged.emit() + def _on_live_error(self) -> None: + """Main-thread callback for worker-thread stream errors.""" + self._error_count += 1 + self.liveStatsChanged.emit() + # ---- recording ---- @Slot(str, str) @slot_error_boundary