Compare commits

...

3 Commits

Author SHA1 Message Date
jack 0d5cf9e4ff test: add device controller test 2026-06-30 13:24:19 -07:00
jack 27292c3bdb chore: cleanup
WaferMapTab stale comments + minor component fixes
2026-06-30 13:24:00 -07:00
jack d7962f2850 feat: wire family
code display and status restore in StatusTab + HomePage
2026-06-30 13:22:44 -07:00
8 changed files with 611 additions and 421 deletions
+2
View File
@@ -323,6 +323,7 @@ Rectangle {
label: "READ MEMORY" label: "READ MEMORY"
iconSource: "../icons/read.svg" iconSource: "../icons/read.svg"
Layout.fillWidth: true Layout.fillWidth: true
enabled: root.waferDetected
onClicked: deviceController.readMemoryAsync() onClicked: deviceController.readMemoryAsync()
} }
@@ -331,6 +332,7 @@ Rectangle {
iconSource: "../icons/erase.svg" iconSource: "../icons/erase.svg"
Layout.fillWidth: true Layout.fillWidth: true
destructive: true destructive: true
enabled: root.waferDetected
onClicked: deviceController.eraseMemory( onClicked: deviceController.eraseMemory(
deviceController.selectedPort) deviceController.selectedPort)
} }
+394 -252
View File
@@ -4,37 +4,106 @@ import QtQuick.Controls
import QtQuick.Dialogs import QtQuick.Dialogs
import ISC import ISC
// ===== Status Tab ===== // ===== Status Tab — Bento Grid Dashboard =====
// Shows connection status, wafer info, and activity log. // ROW 1: Connection Status (65%) | Cycles Completed (35%)
// Initially visible — displays connection status and activity log from start. // ROW 2: Wafer & Sensor + Directory (65%) | Total Runtime (35%)
// ROW 3: Activity Log (100%, fills remaining)
ColumnLayout { ColumnLayout {
id: root id: root
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.panelPadding anchors.margins: Theme.panelPadding
spacing: Theme.rightPaneGap spacing: 8
property alias waferFamilyCode: waferInfoFamily.text // ── Reusable Inline Components ──
property alias waferSerialNumber: waferSerial.text component StatCard : Rectangle {
property alias waferSensorCount: waferSensors.text property alias value: valueText.text
property alias waferRuntime: waferRuntime.text property alias label: labelText.text
property alias waferCycles: waferCycles.text property bool active: root.waferDetected
property bool waferDetected: false property int valSize: active ? Theme.font3xl : Theme.font2xl
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusSm
ColumnLayout {
anchors.centerIn: parent
spacing: 4
Text {
id: valueText
color: active ? Theme.headingColor : Theme.sideMutedText
font.pixelSize: valSize
font.weight: Font.Bold
font.family: Theme.uiFontFamily
Layout.alignment: Qt.AlignHCenter
}
Text {
id: labelText
color: Theme.sideMutedText
font.pixelSize: Theme.fontXs
font.weight: Font.Medium
font.letterSpacing: 1.2
font.family: Theme.uiFontFamily
Layout.alignment: Qt.AlignHCenter
}
}
}
component GridCell : ColumnLayout {
property alias label: labelText.text
property alias value: valueText.text
property bool isRight: false
property bool active: root.waferDetected
spacing: 1
Layout.alignment: isRight ? Qt.AlignRight : Qt.AlignLeft
Text {
id: labelText
color: Theme.sideMutedText
font.pixelSize: Theme.fontXs
font.family: Theme.uiFontFamily
Layout.alignment: isRight ? Qt.AlignRight : Qt.AlignLeft
}
Text {
id: valueText
color: active ? Theme.headingColor : Theme.sideMutedText
font.pixelSize: Theme.fontMd
font.weight: Font.Bold
font.family: Theme.uiFontFamily
Layout.alignment: isRight ? Qt.AlignRight : Qt.AlignLeft
}
}
// ── Aliases for external wiring ──
property alias waferFamilyCode: waferInfoFamilyCell.value
property alias waferSerialNumber: waferSerialCell.value
property alias waferSensorCount: waferSensorsCell.value
property alias waferRuntime: runtimeCard.value
property alias waferCycles: waferCyclesCell.value
property bool waferDetected: {
var info = deviceController.lastWaferInfo;
return info && info.length > 0 && info[0] !== "";
}
// Data summary after parse
property int dataRows: 0 property int dataRows: 0
property int dataCols: 0 property int dataCols: 0
property string csvPath: "" property string csvPath: ""
property bool dataParsed: false property bool dataParsed: false
readonly property bool isConnected: deviceController.connectionStatus === "Connected"
readonly property bool isBusy: deviceController.connectionStatus.endsWith("...")
readonly property string portName: deviceController.selectedPort || "—"
function cleanFolderUrl(url) { function cleanFolderUrl(url) {
return decodeURIComponent(String(url).replace(/^file:\/\//, "")); return decodeURIComponent(String(url).replace(/^file:\/\//, ""));
} }
function currentFamilyCode() { function currentFamilyCode() {
var info = deviceController.lastWaferInfo; var info = deviceController.lastWaferInfo;
return info && info.length > 0 ? (info[0] || "") : ""; return info && info.length > 0 ? (info[0] || "") : "";
} }
function parseAndSavePendingRead() { function parseAndSavePendingRead() {
deviceController.parseAndSaveData(root.currentFamilyCode(), deviceController.selectedPort || ""); deviceController.parseAndSaveData(root.currentFamilyCode(), deviceController.selectedPort || "");
} }
@@ -48,268 +117,349 @@ ColumnLayout {
} }
} }
ColumnLayout { // ═══════════════════════════════════════════════════════════════════════
// ROW 1: Connection Status (100%)
// ═══════════════════════════════════════════════════════════════════════
RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.preferredHeight: 90
spacing: Theme.rightPaneGap Layout.maximumHeight: 90
spacing: 8
// --- Connection Status ---
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 92 Layout.fillHeight: true
color: Theme.cardBackground color: Theme.cardBackground
border.color: { border.color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.statusErrorColor)
if (deviceController.connectionStatus === "Connected") border.width: 1
return Theme.statusSuccessColor; radius: Theme.radiusSm
if (deviceController.connectionStatus === "Disconnected")
return Theme.statusErrorColor; ColumnLayout {
return Theme.cardBorder; id: connStatusCol
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.panelPadding
anchors.rightMargin: Theme.panelPadding
spacing: 8
Text {
text: "CONNECTION STATUS"
color: Theme.sideMutedText
font.pixelSize: Theme.fontXs
font.weight: Font.Medium
font.letterSpacing: 1.2
font.family: Theme.uiFontFamily
} }
RowLayout {
Layout.fillWidth: true
spacing: 0
ColumnLayout {
spacing: 1
Text {
text: "HOST PC"
color: Theme.sideMutedText
font.pixelSize: Theme.fontXs
font.family: Theme.uiFontFamily
}
Text {
text: "localhost"
color: Theme.headingColor
font.pixelSize: Theme.fontMd
font.weight: Font.Bold
font.family: Theme.uiFontFamily
}
}
Item {
Layout.fillWidth: true
implicitHeight: 22
Row {
anchors.centerIn: parent
spacing: 5
Repeater {
model: 4
Rectangle {
width: 4; height: 4; radius: 2
anchors.verticalCenter: parent.verticalCenter
color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.sideMutedText)
opacity: (root.isConnected || root.isBusy) ? 1.0 : 0.3
SequentialAnimation on opacity {
running: root.isConnected || root.isBusy
loops: Animation.Infinite
NumberAnimation { to: 0.3; duration: 1400 }
NumberAnimation { to: 1.0; duration: 1400 }
}
}
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: statusPillText.implicitWidth + 16
height: 20
radius: 10
color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.statusErrorColor)
Text {
id: statusPillText
anchors.centerIn: parent
text: root.isConnected ? "ACTIVE" : (root.isBusy ? deviceController.connectionStatus.replace("...", "").toUpperCase() : "INACTIVE")
color: "#111111"
font.pixelSize: Theme.fontXs
font.weight: Font.Bold
font.family: Theme.uiFontFamily
font.letterSpacing: 0.6
}
}
Repeater {
model: 4
Rectangle {
width: 4; height: 4; radius: 2
anchors.verticalCenter: parent.verticalCenter
color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.sideMutedText)
opacity: (root.isConnected || root.isBusy) ? 1.0 : 0.3
SequentialAnimation on opacity {
running: root.isConnected || root.isBusy
loops: Animation.Infinite
NumberAnimation { to: 0.3; duration: 1400 }
NumberAnimation { to: 1.0; duration: 1400 }
}
}
}
}
}
ColumnLayout {
spacing: 1
Text {
text: "COM PORT"
color: Theme.sideMutedText
font.pixelSize: Theme.fontXs
font.family: Theme.uiFontFamily
Layout.alignment: Qt.AlignRight
}
Text {
text: root.portName
color: root.isConnected ? Theme.statusSuccessColor : Theme.headingColor
font.pixelSize: Theme.fontMd
font.weight: Font.Bold
font.family: Theme.uiFontFamily
Layout.alignment: Qt.AlignRight
}
}
}
}
}
}
// ═══════════════════════════════════════════════════════════════════════
// ROW 2: Wafer & Sensor + Directory (65%) | Total Runtime (35%)
// ═══════════════════════════════════════════════════════════════════════
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 90
Layout.maximumHeight: 90
spacing: 8
RowLayout {
Layout.preferredWidth: 65
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 8
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusSm radius: Theme.radiusSm
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.panelPadding anchors.margins: Theme.panelPadding
spacing: 4 spacing: 6
Text { ColumnLayout {
text: deviceController.connectionStatus
color: {
if (deviceController.connectionStatus === "Connected")
return Theme.statusSuccessColor;
if (deviceController.connectionStatus === "Disconnected")
return Theme.statusErrorColor;
return Theme.bodyColor;
}
font.bold: true
font.pixelSize: Theme.fontLg
Layout.fillWidth: true Layout.fillWidth: true
} spacing: 2
Text { Text {
text: "Port: " + (deviceController.selectedPort || "None selected") text: "DIRECTORY"
color: Theme.bodyColor color: Theme.sideMutedText
font.pixelSize: Theme.fontMd font.pixelSize: Theme.fontMd
font.weight: Font.Medium
font.letterSpacing: 1.2
font.family: Theme.uiFontFamily
}
Text {
text: deviceController.saveDataDir || "Not configured"
color: Theme.bodyColor
font.pixelSize: Theme.fontSm
font.family: Theme.uiFontFamily
font.italic: true
opacity: 0.7
elide: Text.ElideMiddle
Layout.fillWidth: true Layout.fillWidth: true
} }
}
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 8 spacing: 8
Layout.alignment: Qt.AlignVCenter
Text {
text: "Save dir: " + (deviceController.saveDataDir || "None selected")
color: Theme.bodyColor
font.pixelSize: Theme.fontSm
opacity: 0.7
elide: Text.ElideMiddle
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
Button { Button {
id: browseBtn id: browseBtn
text: "BROWSE..." text: "Browse"
font.pixelSize: Theme.font2xs font.pixelSize: Theme.fontSm
font.weight: Font.Medium font.weight: Font.Medium
implicitHeight: 20 font.family: Theme.uiFontFamily
implicitWidth: 65 implicitHeight: 24
implicitWidth: 60
hoverEnabled: true hoverEnabled: true
background: Rectangle { background: Rectangle {
color: parent.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground color: browseBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
border.color: Theme.cardBorder border.color: Theme.cardBorder
border.width: 1 border.width: 1
radius: Theme.radiusSm radius: Theme.radiusSm
} }
contentItem: Text { contentItem: Text {
text: parent.text text: browseBtn.text
color: Theme.bodyColor color: Theme.bodyColor
font: parent.font font: browseBtn.font
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
onClicked: saveDirDialog.open() onClicked: saveDirDialog.open()
} }
Item { Layout.fillWidth: true }
Row {
spacing: 4
visible: root.isConnected
Rectangle {
width: 6; height: 6; radius: 3
color: Theme.statusSuccessColor
anchors.verticalCenter: parent.verticalCenter
}
}
Rectangle {
visible: root.csvPath !== ""
width: sessionSavedLabel.implicitWidth + 12
height: 18
radius: 4
color: Theme.fieldBackground
border.color: Theme.sideBorder
border.width: 1
Text {
id: sessionSavedLabel
anchors.centerIn: parent
text: "SESSION SAVED"
color: Theme.sideMutedText
font.pixelSize: Theme.fontXs
font.weight: Font.Bold
font.family: Theme.uiFontFamily
font.letterSpacing: 0.6
}
}
} }
} }
} }
// --- Wafer Info --- Rectangle {
GroupBox {
title: "Wafer Information"
visible: root.waferDetected
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusSm
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.panelPadding anchors.margins: Theme.panelPadding
spacing: 8 spacing: 8
RowLayout { Item {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Theme.rightPaneGap Layout.fillHeight: true
ColumnLayout { GridLayout {
Layout.fillWidth: true
spacing: 4
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: 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: Theme.fontSm
}
Text {
id: waferSensors
text: "—"
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.rightPaneGap
ColumnLayout {
Layout.fillWidth: true
spacing: 4
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: Theme.fontSm
}
Text {
id: waferCycles
text: "—"
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
}
}
}
}
// --- Data Summary (shown after parse) ---
GroupBox {
title: "Data Summary"
visible: root.dataParsed
Layout.fillWidth: true
RowLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.panelPadding columns: 2
spacing: Theme.rightPaneGap rowSpacing: 6
columnSpacing: 16
ColumnLayout { GridCell {
Layout.fillWidth: true id: waferInfoFamilyCell
spacing: 4 label: "Family Code"
Text { value: {
text: "Rows" var info = deviceController.lastWaferInfo;
color: Theme.subheadingColor return (info && info.length > 0 && info[0]) ? info[0] : "—";
font.pixelSize: Theme.fontSm
}
Text {
text: String(root.dataRows)
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
} }
} }
GridCell {
ColumnLayout { id: waferCyclesCell
Layout.fillWidth: true label: "Cycles Completed"
spacing: 4 value: {
Text { var info = deviceController.lastWaferInfo;
text: "Sensors" return (info && info.length > 5 && info[5] !== undefined) ? String(info[5]) : "—";
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
} }
Text { isRight: true
text: String(root.dataCols) active: root.waferDetected
color: Theme.bodyColor }
font.pixelSize: Theme.fontLg GridCell {
font.bold: true id: waferSerialCell
label: "Serial"
value: {
var info = deviceController.lastWaferInfo;
return (info && info.length > 1 && info[1]) ? info[1] : "—";
} }
} }
GridCell {
ColumnLayout { id: waferSensorsCell
Layout.fillWidth: true label: "Sensors"
spacing: 4 value: {
Text { var info = deviceController.lastWaferInfo;
text: "CSV" return (info && info.length > 2 && info[2]) ? String(info[2]) : "—";
color: Theme.subheadingColor }
font.pixelSize: Theme.fontSm isRight: true
}
} }
Text {
text: root.csvPath
color: Theme.bodyColor
font.pixelSize: Theme.fontSm
elide: Text.ElideMiddle
} }
} }
} }
} }
// --- Activity Log --- StatCard {
id: runtimeCard
Layout.preferredWidth: 35
value: {
var info = deviceController.lastWaferInfo;
return (info && info.length > 4 && info[4] !== undefined) ? info[4] + "s" : "—";
}
label: "TOTAL RUNTIME"
}
}
// ═══════════════════════════════════════════════════════════════════════
// ROW 3: Activity Log — ALWAYS visible (fills remaining height)
// ═══════════════════════════════════════════════════════════════════════
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
color: Theme.cardBackground color: Theme.cardBackground
border.color: Theme.cardBorder border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusMd radius: Theme.radiusMd
clip: true clip: true
@@ -317,40 +467,51 @@ ColumnLayout {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
// Header
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 40 Layout.preferredHeight: 36
color: Theme.subtleSectionBackground color: Theme.subtleSectionBackground
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Theme.panelPadding anchors.leftMargin: Theme.panelPadding
anchors.rightMargin: Theme.panelPadding anchors.rightMargin: Theme.panelPadding
spacing: 8
Text {
text: "📄"
font.pixelSize: Theme.fontSm
visible: root.csvPath !== ""
Layout.alignment: Qt.AlignVCenter
}
Label { Label {
text: "ACTIVITY LOG" text: root.csvPath !== "" ? root.csvPath.split("/").pop() : "ACTIVITY LOG"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: Theme.fontXs font.pixelSize: root.csvPath !== "" ? Theme.fontMd : Theme.fontSm
font.letterSpacing: 1.8 font.letterSpacing: root.csvPath !== "" ? 0 : 1.8
font.weight: Font.Medium font.weight: Font.Medium
font.family: Theme.uiFontFamily
elide: Text.ElideMiddle
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
Button { Button {
text: "REFRESH SESSION" id: refreshSessionBtn
font.pixelSize: Theme.fontXs text: "Refresh"
font.pixelSize: Theme.fontSm
font.family: Theme.uiFontFamily
implicitHeight: 24 implicitHeight: 24
hoverEnabled: true hoverEnabled: true
background: Rectangle { background: Rectangle {
color: parent.hovered ? Theme.buttonNeutralHover : "transparent" color: refreshSessionBtn.hovered ? Theme.buttonNeutralHover : "transparent"
radius: Theme.radiusSm radius: Theme.radiusSm
} }
contentItem: Text { contentItem: Text {
text: parent.text text: refreshSessionBtn.text
color: Theme.bodyColor color: Theme.bodyColor
font: parent.font font: refreshSessionBtn.font
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
@@ -358,18 +519,20 @@ ColumnLayout {
} }
Button { Button {
text: "CLEAR LOG" id: clearLogBtn
font.pixelSize: Theme.fontXs text: "Clear"
font.pixelSize: Theme.fontSm
font.family: Theme.uiFontFamily
implicitHeight: 24 implicitHeight: 24
hoverEnabled: true hoverEnabled: true
background: Rectangle { background: Rectangle {
color: parent.hovered ? Theme.buttonNeutralHover : "transparent" color: clearLogBtn.hovered ? Theme.buttonNeutralHover : "transparent"
radius: Theme.radiusSm radius: Theme.radiusSm
} }
contentItem: Text { contentItem: Text {
text: parent.text text: clearLogBtn.text
color: Theme.bodyColor color: Theme.bodyColor
font: parent.font font: clearLogBtn.font
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
@@ -409,56 +572,36 @@ ColumnLayout {
Connections { Connections {
target: deviceController target: deviceController
function onActivityLogUpdated(newLog) { function onActivityLogUpdated(newLog) {
activityLog.text = newLog; activityLog.text = newLog ? newLog : qsTr("Welcome — use the side rail to detect a wafer, or import data to review.");
}
} }
} }
} }
// --- Signal Handlers --- // ═══════════════════════════════════════════════════════════════════════
// SIGNAL HANDLERS
// ═══════════════════════════════════════════════════════════════════════
Connections { Connections {
target: deviceController target: deviceController
function onLogMessage(message) { function onLogMessage(message) {
if (message == "CHOOSE_SAVE_DIR") { if (message == "CHOOSE_SAVE_DIR") {
saveDirDialog.open(); saveDirDialog.open();
} }
} }
function onPortsUpdated() {}
// Any operation start (detect/read/erase/debug) latches the panel on.
function onPortsUpdated() {
// Status tab always visible now
}
function onDetectResult(result) { function onDetectResult(result) {
if (result && result.familyCode) { if (!result) {
root.waferDetected = true; root.dataParsed = false;
waferInfoFamily.text = result.familyCode; root.dataRows = 0;
waferSerial.text = result.serialNumber; root.dataCols = 0;
waferSensors.text = String(result.sensorCount); root.csvPath = "";
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() { 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) { if (deviceController.dataRowCount > 0) {
root.dataParsed = true; root.dataParsed = true;
root.dataRows = deviceController.dataRowCount; root.dataRows = deviceController.dataRowCount;
root.dataCols = deviceController.dataColCount; root.dataCols = deviceController.dataColCount;
root.csvPath = ""; // CSV path not persisted as full path, just show it was parsed root.csvPath = "";
} }
} }
} }
@@ -470,7 +613,6 @@ ColumnLayout {
root.dataRows = 0; root.dataRows = 0;
root.dataCols = 0; root.dataCols = 0;
root.csvPath = ""; root.csvPath = "";
if (result && result.success === true) if (result && result.success === true)
saveDirDialog.open(); saveDirDialog.open();
} }
+2 -18
View File
@@ -246,29 +246,12 @@ Item {
} }
} }
// ── Body: 3 zones (TODO P2.1: becomes 2 zones) ────────────────────── // ── Body: 2 columns (wafer + trend + transport | readout) ──────────
// -------------------------------------------------------------------
// TODO P2.1: Delete the SourcePanel Rectangle (lines ~219-233) —
// the file list moved to the rail (P1.3 Map context panel).
//
// THINKING:
// SourcePanel shows a file list driven by file_browser — now that
// the Map tab's context panel in the rail (P1.3) provides the same
// file list with search/filter, SourcePanel in the wafer body is
// redundant. The wafer map gets more horizontal space.
//
// After deletion: RowLayout has 2 children instead of 3.
// Add Layout.fillWidth: true to the center ColumnLayout so it
// fills the vacated width.
//
// Cross-ref: P1.3 (Map context panel), P2.1 (this task)
// -------------------------------------------------------------------
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
spacing: 16 spacing: 16
// Source panel — moved to side rail (P2.1)
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
@@ -281,6 +264,7 @@ Item {
Layout.fillHeight: true Layout.fillHeight: true
blend: readoutPanel.heatmapBlend blend: readoutPanel.heatmapBlend
showLabels: readoutPanel.showLabels showLabels: readoutPanel.showLabels
showThickness: readoutPanel.showThickness
} }
Rectangle { Rectangle {
id: trendPane id: trendPane
@@ -11,6 +11,7 @@ ColumnLayout {
property var s: streamController.stats property var s: streamController.stats
property alias showLabels: labelsToggle.checked property alias showLabels: labelsToggle.checked
property alias heatmapBlend: heatmapSlider.value property alias heatmapBlend: heatmapSlider.value
property alias showThickness: thicknessToggle.checked
component PanelCheckBox: CheckBox { component PanelCheckBox: CheckBox {
id: toggle id: toggle
@@ -333,7 +334,6 @@ ColumnLayout {
text: "Show Thickness" text: "Show Thickness"
checked: false checked: false
font.pixelSize: Theme.fontXs font.pixelSize: Theme.fontXs
onCheckedChanged: waferMapItem.showThickness = checked
} }
PanelCheckBox { PanelCheckBox {
@@ -47,15 +47,16 @@ ColumnLayout {
} }
ToolTip { ToolTip {
id: editTooltip
visible: editBtn.hovered visible: editBtn.hovered
text: "Edit" text: "Edit"
delay: 400 delay: 400
font.pixelSize: Theme.fontXs font.pixelSize: Theme.fontXs
font.family: Theme.uiFontFamily font.family: Theme.uiFontFamily
contentItem: Text { contentItem: Text {
text: parent.text text: editTooltip.text
color: Theme.headingColor color: Theme.headingColor
font: parent.font font: editTooltip.font
} }
background: Rectangle { background: Rectangle {
color: Theme.cardBackground color: Theme.cardBackground
@@ -84,15 +85,16 @@ ColumnLayout {
onClicked: file_browser.refreshFiles() onClicked: file_browser.refreshFiles()
ToolTip { ToolTip {
id: refreshTooltip
visible: refreshBtn.hovered visible: refreshBtn.hovered
text: "Refresh" text: "Refresh"
delay: 400 delay: 400
font.pixelSize: Theme.fontXs font.pixelSize: Theme.fontXs
font.family: Theme.uiFontFamily font.family: Theme.uiFontFamily
contentItem: Text { contentItem: Text {
text: parent.text text: refreshTooltip.text
color: Theme.headingColor color: Theme.headingColor
font: parent.font font: refreshTooltip.font
} }
background: Rectangle { background: Rectangle {
color: Theme.cardBackground color: Theme.cardBackground
@@ -7,6 +7,7 @@ Item {
id: root id: root
property real blend: 0.0 property real blend: 0.0
property bool showLabels: true property bool showLabels: true
property alias showThickness: map.showThickness
WaferMapItem { WaferMapItem {
id: map id: map
@@ -40,10 +40,10 @@ class DeviceController(QObject):
# ---- public signals ---- # ---- public signals ----
portsUpdated = Signal(list) portsUpdated = Signal(list)
detectResult = Signal(object) # WaferInfo dict or None detectResult = Signal(object) # WaferInfo dict or None
readResult = Signal(object) # {"success": bool, "bytes": int} or {"error": str} readResult = Signal(dict) # {"success": bool, "bytes": int} or {"error": str}
eraseResult = Signal(object) # {"success": bool} eraseResult = Signal(dict) # {"success": bool}
debugResult = Signal(object) # {"success": bool, "sensor_bytes": int, "debug_bytes": int} debugResult = Signal(dict) # {"success": bool, "sensor_bytes": int, "debug_bytes": int}
parsedDataReady = Signal(object) # {"success": bool, "rows": int, "cols": int, "data": ..., "csv_path": str} parsedDataReady = Signal(dict) # {"success": bool, "rows": int, "cols": int, "data": ..., "csv_path": str}
statusRestored = Signal() # Emitted when status is restored from previous session statusRestored = Signal() # Emitted when status is restored from previous session
logMessage = Signal(str) logMessage = Signal(str)
activityLogUpdated = Signal(str) activityLogUpdated = Signal(str)
@@ -58,35 +58,21 @@ class DeviceController(QObject):
self._data_dir = data_dir self._data_dir = data_dir
self._service = DeviceService(settings) self._service = DeviceService(settings)
# Always start fresh — never restore connection status from a prior session # Always start fresh — never restore connection status or logs from a prior session
self._connection_status = "Disconnected" self._connection_status = "Disconnected"
self._operation_in_progress = False self._operation_in_progress = False
self._activity_log: list[str] = getattr(settings, 'activity_log', []) self._activity_log: list[str] = []
self._raw_bytes: Optional[bytes] = None self._raw_bytes: Optional[bytes] = None
from pathlib import Path from pathlib import Path
self._save_data_dir: str = getattr(settings, 'save_data_dir', "") or str(Path(self._data_dir) / "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] = {}
self._last_csv_path: str = getattr(settings, 'last_csv_path', "") self._last_csv_path: str = ""
self._selected_port: str = getattr(settings, 'selected_port', "") self._selected_port: str = ""
self._data_row_count: int = getattr(settings, 'data_row_count', 0) self._data_row_count: int = 0
self._data_col_count: int = getattr(settings, 'data_col_count', 0) self._data_col_count: int = 0
self._data_model = TemperatureTableModel(self) self._data_model = TemperatureTableModel(self)
self._graph_view = GraphView(self) self._graph_view = GraphView(self)
# If we have persisted activity log, emit it to QML
if self._activity_log:
self.activityLogUpdated.emit("\n".join(self._activity_log))
# Log status restoration and emit signal for UI updates
if self._connection_status != "Disconnected" or self._selected_port or self._last_wafer_info:
self._append_log("Restored previous session status")
if self._selected_port:
self._append_log(f"Last selected port: {self._selected_port}")
if self._last_wafer_info:
info = self._last_wafer_info
self._append_log(f"Last wafer: {info.get('serialNumber', 'Unknown')} ({info.get('familyCode', 'Unknown')})")
self.statusRestored.emit()
# Reset connection status on fresh start # Reset connection status on fresh start
self._connection_status = "Disconnected" self._connection_status = "Disconnected"
@@ -162,10 +148,17 @@ class DeviceController(QObject):
Returns [familyCode, serialNumber, sensorCount, mfgDateHex, runtime, cycleCount]. Returns [familyCode, serialNumber, sensorCount, mfgDateHex, runtime, cycleCount].
""" """
info = self._last_wafer_info info = self._last_wafer_info
family_code = info.get("familyCode", "")
sensor_count = info.get("sensorCount", 0)
from pygui.serialcomm.data_parser import csv_column_count
mapped_count = csv_column_count(family_code)
if mapped_count > 0:
sensor_count = mapped_count
return [ return [
info.get("familyCode", ""), family_code,
info.get("serialNumber", ""), info.get("serialNumber", ""),
info.get("sensorCount", 0), sensor_count,
info.get("mfgDateHex", ""), info.get("mfgDateHex", ""),
info.get("runtime", 0), info.get("runtime", 0),
info.get("cycleCount", 0), info.get("cycleCount", 0),
@@ -239,9 +232,13 @@ class DeviceController(QObject):
self._data_col_count = 0 self._data_col_count = 0
self._raw_bytes = None self._raw_bytes = None
self._operation_in_progress = False self._operation_in_progress = False
self._data_model.reset()
self._graph_view.resetChart()
self._activity_log.clear()
self.detectResult.emit(None) self.detectResult.emit(None)
self.parsedDataReady.emit({"success": False})
self.portsUpdated.emit(self.availablePorts) self.portsUpdated.emit(self.availablePorts)
self._append_log("Session refreshed/cleared") self.activityLogUpdated.emit("")
self._save_status() self._save_status()
@@ -280,6 +277,18 @@ class DeviceController(QObject):
self._append_log("Already busy — ignoring detect request") self._append_log("Already busy — ignoring detect request")
return return
# Clear previous session data & logs for a fresh session
self._selected_port = ""
self._last_wafer_info = {}
self._data_row_count = 0
self._data_col_count = 0
self._raw_bytes = None
self._data_model.reset()
self._graph_view.resetChart()
self._activity_log.clear()
self.detectResult.emit(None)
self.parsedDataReady.emit({"success": False})
self._set_operation_progress(True) self._set_operation_progress(True)
self._set_connection_status("Detecting...") self._set_connection_status("Detecting...")
self.portsUpdated.emit(self._service.enumerate_ports()) self.portsUpdated.emit(self._service.enumerate_ports())
@@ -306,10 +315,6 @@ class DeviceController(QObject):
if info is not None: if info is not None:
self._selected_port = port or "" self._selected_port = port or ""
self._set_connection_status("Connected") self._set_connection_status("Connected")
self._append_log(
f"Detected: {info.serial_number} (family={info.family_code}, "
f"port={port}, runtime={info.runtime}s, cycles={info.cycle_count})"
)
wafer_dict = self._wafer_info_to_dict(info) wafer_dict = self._wafer_info_to_dict(info)
self._last_wafer_info = wafer_dict self._last_wafer_info = wafer_dict
self.detectResult.emit(wafer_dict) self.detectResult.emit(wafer_dict)
@@ -558,10 +563,16 @@ class DeviceController(QObject):
@staticmethod @staticmethod
def _wafer_info_to_dict(info: WaferInfo) -> dict[str, Any]: def _wafer_info_to_dict(info: WaferInfo) -> dict[str, Any]:
"""Convert WaferInfo dataclass to JSON-serialisable dict for QML.""" """Convert WaferInfo dataclass to JSON-serialisable dict for QML."""
from pygui.serialcomm.data_parser import csv_column_count
sensor_count = info.sensor_count
mapped_count = csv_column_count(info.family_code)
if mapped_count > 0:
sensor_count = mapped_count
return { return {
"familyCode": info.family_code, "familyCode": info.family_code,
"serialNumber": info.serial_number, "serialNumber": info.serial_number,
"sensorCount": info.sensor_count, "sensorCount": sensor_count,
"mfgDateHex": info.mfg_date_hex, "mfgDateHex": info.mfg_date_hex,
"runtime": info.runtime, "runtime": info.runtime,
"cycleCount": info.cycle_count, "cycleCount": info.cycle_count,
@@ -572,8 +583,9 @@ class QmlActivityLogHandler(logging.Handler):
super().__init__() super().__init__()
self._controller = controller self._controller = controller
def emit(self, record: logging.LogRecord) -> None: def emit(self, record: logging.LogRecord) -> None:
timestamp = datetime.now().strftime("%H:%M:%S")
level = record.levelname
message = record.getMessage() message = record.getMessage()
msg = f"[{timestamp}] {level}: {message}" if "found wafer on" in message:
return
level = record.levelname
msg = f"{level}: {message}"
self._controller._append_log(msg) self._controller._append_log(msg)
+47
View File
@@ -140,3 +140,50 @@ def test_logging_handler(controller):
logger = logging.getLogger("test_logger") logger = logging.getLogger("test_logger")
logger.info("Hello QML Activity Log") logger.info("Hello QML Activity Log")
assert any("Hello QML Activity Log" in line for line in controller._activity_log) assert any("Hello QML Activity Log" in line for line in controller._activity_log)
def test_fresh_session_on_startup(mock_settings):
import tempfile
import shutil
# Configure mock settings with some data to simulate previous session
mock_settings.activity_log = ["[12:00:00] Old message"]
mock_settings.selected_port = "COM9"
mock_settings.last_wafer_info = {"familyCode": "P", "serialNumber": "P00002"}
mock_settings.data_row_count = 50
mock_settings.data_col_count = 244
temp_dir = tempfile.mkdtemp()
try:
new_controller = DeviceController(mock_settings, temp_dir)
# Should start completely fresh (empty/default) ignoring the persisted status
assert len(new_controller._activity_log) == 0
assert new_controller.selectedPort == ""
assert new_controller.lastWaferInfo == ["", "", 0, "", 0, 0]
assert new_controller.dataRowCount == 0
assert new_controller.dataColCount == 0
finally:
shutil.rmtree(temp_dir)
def test_detect_wafer_clears_old_session(controller):
controller._selected_port = "COM1"
controller._last_wafer_info = {"familyCode": "P", "serialNumber": "P00001"}
controller._data_row_count = 10
controller._data_col_count = 244
controller._activity_log.append("[12:00:00] Previous message")
# We mock detect_all_ports to not run long or fail
controller._service.detect_all_ports = MagicMock(return_value=(None, None))
with patch("threading.Thread") as mock_thread:
controller.detectWafer()
# Verify it cleared all session variables and logs immediately at start of detect
assert controller.selectedPort == ""
assert controller.lastWaferInfo == ["", "", 0, "", 0, 0]
assert controller.dataRowCount == 0
assert controller.dataColCount == 0
# Log should only contain "Scanning all ports for wafer ..."
assert len(controller._activity_log) == 1
assert "Scanning all ports" in controller._activity_log[0]