317 lines
12 KiB
QML
317 lines
12 KiB
QML
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|