Files
pyGUI/src/pygui/ISC/Tabs/DataTab.qml
T
jack 9e3bec9031 feat(debug): add debug read with CSV export
- Parse F1 debug data via family-independent temp formula
  (bits 4-14 scaled by 2**(i-8), sign at bit 0)
- Save sensor+debug temps side-by-side to debug_info.csv
- Wire READ DEBUG button in StatusActionsPanel
- Rename ReplayChart → RunChart; move into GraphTab
- Extract PanelBox/SectionTitle/PanelSlider in ReadoutPanel
  to cut ~300 lines of duplicated styling
- Add rightRailWidth token to Theme for consistent rail sizing
- Wrap ReadoutPanel in ScrollView so Thresholds card stays
  reachable when E-C delta rows appear
2026-07-11 18:39:29 -07:00

376 lines
15 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Controls.impl
import QtQuick.Layouts
import ISC
import ISC.Tabs.components
import ISC.Wafer
// ===== Data Tab (Compare Content Only) =====
// Per docs/design/navigation-mock.html: the Data tab shows only the Compare
// Runs content — the raw CSV table ("Inspect Table" mode in earlier drafts)
// is intentionally not displayed in the application.
// Layout: left column = aligned-curves chart with alignment scrubber + wafer
// overlap map; right side panel = run selection dropdowns, overlap settings,
// and DTW readout cards.
Item {
id: root
anchors.fill: parent
// Properties for DTW Comparison
property int activeBox: 1 // 1 for Reference (Run A), 2 for Session (Run B), 0 for inactive
property string compareFileA: ""
property string compareFileB: ""
property bool useMasterFile: false // Run A comes from settingsModel.masters[runBWaferType] instead of manual selection
function waferTypeForFile(path) {
if (!path) return ""
// A file registered as a master carries that slot's family — this is
// the only way a live recording gets a family (its filename/metadata
// don't encode one reliably).
for (var fam in settingsModel.masters)
if (settingsModel.masters[fam] === path) return fam.toUpperCase()
var rows = file_browser.files
for (var i = 0; i < rows.length; i++) {
if (rows[i].fileName === path) {
if (rows[i].isRecording) return (rows[i].masterType || "").toUpperCase()
return (rows[i].waferType || "").toUpperCase()
}
}
var base = path.split("/").pop()
if (base.toLowerCase().indexOf("live_") === 0) return ""
return base ? base.charAt(0).toUpperCase() : ""
}
// Master file must match Run B's wafer family — looking it up by that
// family is itself the comparability gate (a mismatched-family master
// simply won't exist under this key).
readonly property string runBWaferType: root.waferTypeForFile(root.compareFileB)
readonly property string masterFileForRunB: root.runBWaferType !== "" ? (settingsModel.masters[root.runBWaferType] || "") : ""
readonly property string runAWaferType: root.useMasterFile ? root.runBWaferType : root.waferTypeForFile(root.compareFileA)
// Family gate: both runs must resolve to the same wafer family. An empty
// family means an unassigned live recording — comparable only after it is
// registered as a master file (Settings → Master Files).
readonly property string familyGateError: {
var fileA = root.useMasterFile ? root.masterFileForRunB : root.compareFileA
if (fileA === "" || root.compareFileB === "") return ""
if (root.runAWaferType === "" || root.runBWaferType === "")
return "Recording has no wafer family — register it as a master file (Settings → Master Files)"
if (root.runAWaferType !== root.runBWaferType)
return "Wafer Family Mismatch: " + root.runAWaferType + " vs " + root.runBWaferType
return ""
}
property real warpingDistance: -1
property int frameOffset: 0
property real frameOffsetSeconds: 0
property real maxSensorDeviation: -1
property bool comparing: false
property var trendDataA: []
property var trendDataB: []
property var timeDataA: []
property var timeDataB: []
property int frameCountA: 0
property int frameCountB: 0
property string errorMessage: ""
property string mismatchMessage: ""
// Wafer overlap properties
property var compareSensorLayout: []
property var compareSensorDiff: []
property string compareWaferShape: "round"
property real compareWaferSize: 300.0
readonly property bool hasSeries: trendDataA.length > 1 && trendDataB.length > 1
readonly property int seriesLen: Math.max(trendDataA.length, trendDataB.length)
function diffBands(diffs) {
return diffs.map(d => d > 1.0 ? "high" : d < -1.0 ? "low" : "in_range")
}
function shortName(path) {
return path ? path.split("/").pop() : ""
}
function scrubTimeLabel(idx) {
var t = idx < root.timeDataA.length ? root.timeDataA[idx]
: (idx < root.timeDataB.length ? root.timeDataB[idx] : null)
return t !== null ? " (" + t.toFixed(1) + "s)" : ""
}
function formatMinutes(secondsValue) {
var mins = Math.floor(secondsValue / 60)
var secs = secondsValue % 60
return mins + ":" + (secs < 10 ? "0" : "") + secs.toFixed(1)
}
function frameForTime(t) {
var times = root.timeDataA.length > 0 ? root.timeDataA : root.timeDataB
if (times.length === 0) return 0
var closestIdx = 0
var minDiff = Math.abs(times[0] - t)
for (var i = 1; i < times.length; i++) {
var diff = Math.abs(times[i] - t)
if (diff < minDiff) {
minDiff = diff
closestIdx = i
}
}
return closestIdx
}
function clearResults() {
root.warpingDistance = -1
root.frameOffset = 0
root.frameOffsetSeconds = 0
root.maxSensorDeviation = -1
root.trendDataA = []
root.trendDataB = []
root.timeDataA = []
root.timeDataB = []
root.frameCountA = 0
root.frameCountB = 0
root.compareSensorLayout = []
root.compareSensorDiff = []
root.errorMessage = ""
root.mismatchMessage = ""
mismatchHideTimer.stop()
}
function resetComparison() {
root.compareFileA = ""
root.compareFileB = ""
root.useMasterFile = false
root.comparing = false
root.activeBox = 1
clearResults()
}
Component.onCompleted: file_browser.refreshFiles()
Timer {
id: mismatchHideTimer
interval: 6000
onTriggered: root.mismatchMessage = ""
}
// Clicking a file in the left rail's browser populates the armed run slot
Connections {
target: streamController
function onLoadedFileChanged() {
if (streamController.loadedFile && root.activeBox > 0) {
if (root.activeBox === 1) {
root.compareFileA = streamController.loadedFile
root.activeBox = 2 // Auto-advance to box 2
} else if (root.activeBox === 2) {
root.compareFileB = streamController.loadedFile
root.activeBox = 0 // Finished selecting
}
}
}
function onComparisonResult(result) {
root.comparing = false
if (result && result.success) {
root.warpingDistance = result.distance
root.frameOffset = result.frame_offset !== undefined ? result.frame_offset : 0
root.frameOffsetSeconds = result.frame_offset_seconds !== undefined ? result.frame_offset_seconds : 0
root.maxSensorDeviation = result.max_sensor_deviation !== undefined ? result.max_sensor_deviation : -1
root.trendDataA = result.series_a || []
root.trendDataB = result.series_b || []
root.timeDataA = result.time_a || []
root.timeDataB = result.time_b || []
root.frameCountA = result.frame_count_a || 0
root.frameCountB = result.frame_count_b || 0
root.compareSensorLayout = result.sensor_layout || []
root.compareSensorDiff = result.sensor_diff || []
root.compareWaferShape = result.wafer_shape || "round"
root.compareWaferSize = result.wafer_size || 300.0
root.errorMessage = ""
chartCard.frameIndex = Math.floor(root.seriesLen / 2)
chartCard.focusScrubber()
if (root.frameCountA !== root.frameCountB) {
var timeA = root.timeDataA.length > 0 ? root.timeDataA[root.timeDataA.length - 1] : 0
var timeB = root.timeDataB.length > 0 ? root.timeDataB[root.timeDataB.length - 1] : 0
root.mismatchMessage = "Run A: " + root.frameCountA + " frames (" + timeA.toFixed(1) + "s) · "
+ "Run B: " + root.frameCountB + " frames (" + timeB.toFixed(1) + "s). "
+ "Deviation beyond the shorter run shows as \"none\"."
mismatchHideTimer.restart()
} else {
root.mismatchMessage = ""
mismatchHideTimer.stop()
}
} else {
root.clearResults()
root.errorMessage = (result && result.error) ? result.error : "Comparison failed"
}
}
}
// ── Main Layout ───────────────────────────────────────────────
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.panelPadding
spacing: Theme.rightPaneGap
// ── Error Banner ───────────────────────────────────────────
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: errorRow.implicitHeight + 16
visible: root.errorMessage !== ""
color: Theme.errorSurface
border.color: Theme.statusErrorColor
border.width: 1
radius: Theme.radiusSm
RowLayout {
id: errorRow
anchors.fill: parent
anchors.margins: 8
spacing: 8
IconImage {
source: "icons/triangle-alert.svg"
width: 16; height: 16
sourceSize.width: 16
sourceSize.height: 16
color: Theme.errorTextSoft
Layout.alignment: Qt.AlignTop
}
Label {
id: errorLabel
text: root.errorMessage
color: Theme.errorTextSoft
font.pixelSize: Theme.fontSm
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
}
// ── Body: main column | side panel ─────────────────────────
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 16
// --- Left Column: chart + scrubber, wafer overlap map ---
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumWidth: 350
spacing: 12
// Wafer Map Overlap View card
OverlapMapCard {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 220
sensorLayout: root.compareSensorLayout
diffValues: streamController.getSensorDiffAt(Math.round(chartCard.frameIndex))
diffBands: root.diffBands
waferShape: root.compareWaferShape
waferSize: root.compareWaferSize
overlapEnabled: sidePanel.overlapEnabled
blendAmount: sidePanel.blendAmount
}
// Chart card with alignment scrubber
ComparisonChartCard {
id: chartCard
Layout.fillWidth: true
trendDataA: root.trendDataA
trendDataB: root.trendDataB
seriesLen: root.seriesLen
hasSeries: root.hasSeries
comparing: root.comparing
}
}
// --- Right Column: run selection, overlap settings, readout ---
ComparisonSidePanel {
id: sidePanel
Layout.fillWidth: false
Layout.preferredWidth: Theme.rightRailWidth
Layout.minimumWidth: Theme.rightRailWidth
Layout.maximumWidth: Theme.rightRailWidth
Layout.fillHeight: true
warpingDistance: root.warpingDistance
frameOffset: root.frameOffset
frameOffsetSeconds: root.frameOffsetSeconds
maxSensorDeviation: root.maxSensorDeviation
seriesLen: root.seriesLen
hasSeries: root.hasSeries
trendDataA: root.trendDataA
trendDataB: root.trendDataB
timeDataA: root.timeDataA
timeDataB: root.timeDataB
frameForTime: root.frameForTime
frameIndex: chartCard.frameIndex
onFrameIndexRequested: (idx) => chartCard.frameIndex = idx
onScrubberFocusRequested: chartCard.focusScrubber()
}
}
}
// ── Frame Mismatch Banner (amber, floats over content, auto-hides) ──
Rectangle {
id: mismatchBanner
visible: root.mismatchMessage !== ""
z: 100
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 12
width: Math.min(parent.width - 48, 640)
height: mismatchRow.implicitHeight + 16
color: Theme.cardBackground
border.color: Theme.statusWarningColor
border.width: 1
radius: Theme.radiusSm
RowLayout {
id: mismatchRow
anchors.fill: parent
anchors.margins: 8
spacing: 8
IconImage {
source: "icons/triangle-alert.svg"
width: 16; height: 16
sourceSize.width: 16
sourceSize.height: 16
color: Theme.statusWarningColor
Layout.alignment: Qt.AlignTop
}
Label {
text: root.mismatchMessage
color: Theme.statusWarningColor
font.pixelSize: Theme.fontSm
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Button {
flat: true
implicitWidth: 24
implicitHeight: 24
Layout.alignment: Qt.AlignTop
onClicked: {
root.mismatchMessage = ""
mismatchHideTimer.stop()
}
background: Rectangle { color: "transparent" }
contentItem: Label {
text: "✕"
color: Theme.statusWarningColor
font.pixelSize: Theme.fontXs
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
}