import QtQuick import QtQuick.Controls import QtQuick.Dialogs import QtQuick.Layouts import ISC import ISC.Tabs.components import ISC.Wafer Item { id: root anchors.fill: parent // Live elapsed-time counter property int _liveSecs: 0 Timer { id: liveTimer interval: 1000 repeat: true running: streamController.mode === "live" && streamController.state !== "idle" onTriggered: root._liveSecs++ 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; } // Wire streamController.trendData (Signal(str) carrying JSON list of floats) // into the trend chart's data property. The slot parseJsonToData() is defined // on the Python TrendChartItem; we use it so malformed payloads are logged // there instead of crashing the QML binding. Connections { target: streamController function onTrendData(avgsJson) { trendChart.setDataFromJson(avgsJson); } } ColumnLayout { anchors.fill: parent anchors.margins: 16 spacing: 16 // ── Toolbar ─────────────────────────────────────────────────────── RowLayout { Layout.fillWidth: true spacing: 10 // Mode toggle TabBar { id: modeBar currentIndex: 0 // Default to "Review" spacing: 2 padding: 2 background: Rectangle { color: Theme.subtleSectionBackground radius: Theme.radiusSm border.color: Theme.cardBorder border.width: Theme.borderThin } TabButton { text: "Review" implicitWidth: 64 implicitHeight: 28 background: Rectangle { color: parent.checked ? Theme.tabActiveBackground : "transparent" radius: Theme.radiusSm - 1 } contentItem: Text { text: parent.text color: parent.checked ? Theme.headingColor : Theme.bodyColor font.pixelSize: Theme.fontSm font.weight: parent.checked ? Font.Medium : Font.Normal horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } TabButton { text: "Live" enabled: deviceController.connectionStatus === "Connected" opacity: enabled ? 1.0 : 0.4 implicitWidth: 58 implicitHeight: 28 background: Rectangle { color: parent.checked ? Theme.tabActiveBackground : "transparent" radius: Theme.radiusSm - 1 } contentItem: Text { text: parent.text color: parent.checked ? Theme.headingColor : Theme.bodyColor 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"); } } // Live source info: WiFi icon + port · serial RowLayout { visible: streamController.mode === "live" spacing: 5 // WiFi icon (Unicode approximation; swap for SVG if available) Label { id: liveIndicator text: "◉" 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 } onRunningChanged: { if (!running) liveIndicator.opacity = 1.0; } } } Label { text: "Live stream" color: Theme.bodyColor font.pixelSize: Theme.fontXs } } Item { Layout.fillWidth: true } // SET badge + avg temp Rectangle { visible: streamController.state === "set" height: 22 radius: Theme.radiusSm color: "transparent" border.color: Theme.liveColor border.width: 1 implicitWidth: setLabel.implicitWidth + 16 Label { id: setLabel anchors.centerIn: parent text: "SET " + (streamController.stats.avg !== undefined ? streamController.stats.avg + " avg" : "") color: Theme.liveColor font.pixelSize: Theme.fontXs font.weight: Font.Medium font.letterSpacing: 0.8 } } // Record indicator RowLayout { spacing: 5 visible: streamController.recording Rectangle { 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 } } } Label { text: "REC" color: Theme.recordColor font.pixelSize: Theme.fontXs font.weight: Font.Medium font.letterSpacing: 1.2 } } // Record start/stop toggle (Live mode) Button { visible: streamController.mode === "live" enabled: streamController.state !== "idle" || streamController.recording text: streamController.recording ? "Stop REC" : "Record" onClicked: { if (streamController.recording) { streamController.stopRecording(); } else { var info = deviceController.lastWaferInfo; var serial = (info && info.length > 1) ? info[1] : ""; var ts = Qt.formatDateTime(new Date(), "yyyyMMdd_HHmmss"); var name = "live_" + (serial ? serial + "_" : "") + ts + ".csv"; streamController.startRecording(deviceController.saveDataDir + "/" + name, serial); } } } // Stream statistics popup (Live mode) Button { visible: streamController.mode === "live" text: "Stats" onClicked: streamStatsDialog.open() } StreamStatsDialog { id: streamStatsDialog elapsedSecs: root._liveSecs } Button { text: "Export PNG" onClicked: exportDialog.open() } FileDialog { id: exportDialog title: "Export Wafer Map" fileMode: FileDialog.SaveFile nameFilters: ["PNG files (*.png)"] onAccepted: waferView.exportImage(String(selectedFile).replace(/^file:\/\//, "")) } } // ── Body: 2 columns (wafer + trend + transport | readout) ────────── RowLayout { Layout.fillWidth: true Layout.fillHeight: true spacing: 16 ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true spacing: 8 WaferMapView { id: waferView Layout.fillWidth: true Layout.fillHeight: true blend: readoutPanel.heatmapBlend showLabels: readoutPanel.showLabels showThickness: readoutPanel.showThickness } Rectangle { id: trendPane Layout.fillWidth: true implicitHeight: 160 visible: trendChart.hasData && streamController.mode === "live" color: Theme.cardBackground border.color: Theme.cardBorder border.width: 1 radius: Theme.radiusMd TrendChartItem { id: trendChart anchors.fill: parent anchors.margins: 8 } } Item { Layout.fillWidth: true implicitHeight: 16 } TransportBar { Layout.fillWidth: true } } // Readout panel — floating wrapper box Rectangle { Layout.preferredWidth: 220 Layout.fillHeight: true color: "transparent" border.color: "transparent" border.width: 0 clip: true ReadoutPanel { id: readoutPanel anchors.fill: parent anchors.margins: 0 } } } } }