Repository navigation
feat(p0): center-screen actionable Studio toast for board faults, PTP Ethernet and operator attention #272
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: fix/p0-ptp-ethernet-link-preflight-20261021
Are you sure you want to change the base?
Changes from all commits
b52d981
ed6ce6e
92c554f
3788f1a
7d6c7e7
8dc4562
a03da5e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -156,8 +156,14 @@ ApplicationWindow { | |
| property real activePhase: 0.0 | ||
| property real activeQuality: 0 | ||
| property string transientMessage: "" | ||
| property string transientDetail: "" | ||
| property string statusMessage: "" | ||
| property bool transientError: false | ||
| property string lastOperatorNoticeKey: "" | ||
| property double lastOperatorNoticeMs: 0 | ||
| property bool lastPtpStartPending: false | ||
| property string lastPtpPhyLink: "UNKNOWN" | ||
| property string lastSessionPresentationState: "" | ||
| property bool phasorDockVisible: true | ||
| property bool phasorDetached: false | ||
| property bool waveformDockVisible: true | ||
|
|
@@ -196,6 +202,7 @@ ApplicationWindow { | |
| readonly property bool canDeploy: workflowBar.session ? workflowBar.session.canDeployProfile : false | ||
| readonly property bool canStart: workflowBar.session ? workflowBar.session.startReady : false | ||
| readonly property string toastMessage: transientMessage | ||
| readonly property string toastDetail: transientDetail | ||
| readonly property bool toastError: transientError | ||
|
|
||
| SclProfileModel { id: sclProfiles } | ||
|
|
@@ -208,7 +215,28 @@ ApplicationWindow { | |
| root.profileDirty = false | ||
| } | ||
| function onDeviceMessage(message) { | ||
| root.showMessage(message, false) | ||
| // DeviceController::setError() emits deviceMessage after updating | ||
| // lastError. Previously all real firmware faults arrived as INFO | ||
| // and were silently relegated to the 9px footer. | ||
| root.showMessage(message, message === device.lastError) | ||
| } | ||
| function onPtpStateChanged() { | ||
| const pending = device.ptpStarting | ||
| // New explicit START = new operator action; do not suppress a | ||
| // repeated physical fault merely because its code is the same. | ||
| if (pending && !root.lastPtpStartPending) | ||
| root.lastOperatorNoticeKey = "" | ||
| if (!pending && root.lastPtpStartPending && !device.ptpRunning && | ||
| device.ptpLastStartFailure.length > 0) | ||
| root.showMessage(device.ptpLastStartFailure, true) | ||
| root.lastPtpStartPending = pending | ||
|
|
||
| // Report a real PHY recovery once, never an inferred USB link. | ||
| const phy = device.ptpEthernetLink | ||
| if (root.lastPtpPhyLink === "DOWN" && phy === "UP" && | ||
| device.ptpEthernetDriver === "STARTED") | ||
| root.showMessage("Ethernet ESP32-P4 connected and verified.", false) | ||
| root.lastPtpPhyLink = phy | ||
| } | ||
| function onSvStopReasonChanged() { | ||
| if (!device.running && device.svStopReason !== "" && | ||
|
|
@@ -226,7 +254,24 @@ ApplicationWindow { | |
|
|
||
| Connections { | ||
| target: workflowBar.session | ||
| function onStateChanged() { | ||
| const currentState = workflowBar.session.state | ||
| // A stable terminal fault is one operator event, not a toast | ||
| // every time the session emits changed for unrelated telemetry. | ||
| if (currentState !== root.lastSessionPresentationState && | ||
| /ERROR|FAILED|BLOCKED/i.test(currentState)) | ||
| root.showMessage(workflowBar.session.statusText, true) | ||
| if (currentState !== root.lastSessionPresentationState && | ||
| !/ERROR|FAILED|BLOCKED/i.test(currentState)) | ||
| root.lastOperatorNoticeKey = "" | ||
| root.lastSessionPresentationState = currentState | ||
| } | ||
| function onFirmwareUpdateFinished(success) { | ||
| if (success) | ||
| root.showMessage("Firmware updated and verified on the board.", false) | ||
| else | ||
| root.showMessage("Firmware update failed: " + | ||
| workflowBar.session.firmwareUpdateResult, true) | ||
| // Reset during firmware install stops the board's PTP SOURCE. | ||
| // If the operator explicitly enabled it in this live Studio | ||
| // session, restore once after the verified board is ready. | ||
|
|
@@ -250,6 +295,13 @@ ApplicationWindow { | |
| if (!session || !device.deviceVerified || !session.startReady || | ||
| session.updatingFirmware || device.ptpStarting) return | ||
| root.ptpDefaultStartAttempted = true | ||
| if (device.ptpEthernetLink === "DOWN" || | ||
| device.ptpEthernetDriver === "STOPPED") { | ||
| // This default is a convenience, not an excuse to send a | ||
| // doomed START without a board-proven physical Ethernet link. | ||
| root.showMessage("ETHERNET_LINK_DOWN: PTP auto-start held", true) | ||
| return | ||
| } | ||
| if (!device.ptpRunning && !root.startPtpRole("SOURCE")) | ||
| root.showMessage("PTP could not start automatically; use PTP On to retry.", true) | ||
| } | ||
|
|
@@ -284,6 +336,7 @@ ApplicationWindow { | |
| interval: 2600 | ||
| onTriggered: { | ||
| root.transientMessage = "" | ||
| root.transientDetail = "" | ||
| root.transientError = false | ||
| root.statusMessage = "" | ||
| } | ||
|
|
@@ -310,18 +363,80 @@ ApplicationWindow { | |
| ListElement { timeText: "--:--:--"; messageText: "ARStack Studio ready."; isError: false } | ||
| } | ||
|
|
||
| // Single operator notification policy for Main + Advanced. The original | ||
| // error remains in the bounded event history and Copy diagnostics; only | ||
| // the short, actionable explanation is shown over the controls. | ||
| function operatorNotice(raw, failure) { | ||
| const m = String(raw || "").trim() | ||
| const linkDown = /ETHERNET_LINK_DOWN|Ethernet PHY link is DOWN/i.test(m) || | ||
| (/PTP/i.test(m) && device.ptpEthernetLink === "DOWN") | ||
| if (linkDown) return { | ||
| key: "ptp-link-down", | ||
| title: "PTP belum bisa dimulai — Ethernet terputus", | ||
| detail: "Hubungkan port Ethernet ESP32-P4 ke laptop atau switch yang aktif. Tunggu LINK menyala, lalu tekan Start PTP lagi.", | ||
| error: true | ||
| } | ||
| if (/ETHERNET_DRIVER_NOT_STARTED|ETHERNET_DRIVER_UNAVAILABLE|ETHERNET_DRIVER_NOT_READY/i.test(m)) | ||
| return { key: "ptp-driver", title: "Driver Ethernet board belum siap", | ||
| detail: "Periksa status Driver di Time Sync. Jika tetap STOPPED, restart board dan coba lagi.", error: true } | ||
| if (/ETHERNET_TX_INVALID_STATE|ESP_ERR_INVALID_STATE/i.test(m)) | ||
| return { key: "ptp-mac-tx", title: "PTP gagal mengirim lewat Ethernet", | ||
| detail: "EMAC menolak TX. Periksa Driver dan PHY di Time Sync, lalu Copy diagnostics.", error: true } | ||
| if (/LEASE_EXPIRED|control.session.lease.expired/i.test(m)) | ||
| return { key: "sv-heartbeat", title: "Injeksi SV berhenti — komunikasi terputus", | ||
| detail: "Periksa USB/COM dan koneksi Studio–board sebelum menekan Start lagi.", error: true } | ||
| if (/RUNTIME_FAULT|FIRMWARE_STOP_CAUSE/i.test(m)) | ||
| return { key: "sv-fault", title: "Injeksi SV dihentikan oleh board", | ||
| detail: "Buka Copy diagnostics untuk melihat fault. Pastikan board siap sebelum Start ulang.", error: true } | ||
| if (/PTP .*failed|PTP .*rejected|PTP .*timed out|PTP could not start|PTP did not restart|PTP source did not|PTP start did not/i.test(m)) | ||
| return { key: "ptp-failed", title: "PTP belum berhasil aktif", | ||
| detail: "Periksa Driver/PHY pada Time Sync. Detail penyebab tersedia di Copy diagnostics.", error: true } | ||
| if (/firmware.*(fail|reject|error|timeout)|firmware update failed/i.test(m)) | ||
| return { key: "firmware-failed", title: "Pembaruan firmware belum berhasil", | ||
| detail: "Periksa koneksi USB dan tahap update di Advanced → Firmware. Lihat diagnostics sebelum mencoba lagi.", error: true } | ||
| if (/profile.*(fail|reject|error|blocked)|configuration.*(fail|reject)/i.test(m)) | ||
| return { key: "profile-failed", title: "Konfigurasi belum diterapkan", | ||
| detail: "Periksa profil dan koneksi board. Gunakan Copy diagnostics untuk rincian.", error: true } | ||
| if (/capture.*(fail|reject|error|unavailable)/i.test(m)) | ||
| return { key: "capture-failed", title: "Capture belum berhasil", | ||
| detail: "Periksa adapter Ethernet yang dipilih dan akses capture. Lihat detail diagnostik.", error: true } | ||
| if (failure) | ||
| return { key: "error:" + m.slice(0, 100), title: "Perintah belum berhasil", | ||
| detail: m.length > 190 ? m.slice(0, 187) + "…" : m, error: true } | ||
| if (/copied|saved|verified|firmware updated|successfully|completed|Ethernet ESP32-P4 connected/i.test(m)) | ||
| return { key: "info:" + m.slice(0, 90), title: "Selesai", | ||
| detail: m.length > 180 ? m.slice(0, 177) + "…" : m, error: false } | ||
| // High-frequency telemetry and routine informational chatter never | ||
| // steal focus or overwrite a currently displayed fault. | ||
| return null | ||
| } | ||
|
|
||
| function showMessage(message, error) { | ||
| statusMessage = message | ||
| transientMessage = error === true ? message : "" | ||
| transientError = error === true | ||
| const raw = String(message || "").trim() | ||
| if (raw.length === 0) return | ||
| statusMessage = raw | ||
| statusHistoryModel.append({ | ||
| timeText: new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss"), | ||
| messageText: message, | ||
| messageText: raw, | ||
| isError: error === true | ||
| }) | ||
| while (statusHistoryModel.count > 100) | ||
| statusHistoryModel.remove(0) | ||
| messageTimer.restart() | ||
|
|
||
| const notice = operatorNotice(raw, error === true) | ||
| if (!notice) return | ||
|
Comment on lines
+426
to
+427
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For routine messages that Useful? React with 👍 / 👎. |
||
| const now = Date.now() | ||
| if (notice.key === lastOperatorNoticeKey && now - lastOperatorNoticeMs < 8000) | ||
| return | ||
| if (messageTimer.running && transientError && !notice.error) | ||
| return // a success/info notice must not bury an active failure | ||
| lastOperatorNoticeKey = notice.key | ||
| lastOperatorNoticeMs = now | ||
| transientMessage = notice.title | ||
| transientDetail = notice.detail | ||
| transientError = notice.error | ||
| messageTimer.interval = notice.error ? 4600 : 2800 | ||
| messageTimer.restart() // toast clears; board fault remains unchanged | ||
| } | ||
|
|
||
| function detachPhasor() { | ||
|
|
@@ -870,6 +985,17 @@ ApplicationWindow { | |
| monoFont: root.monoFont | ||
| } | ||
| } | ||
|
|
||
| // The active Advanced window is its own native Window; an overlay | ||
| // attached only to Main.qml is invisible behind this window. | ||
| StatusToast { | ||
| anchors.centerIn: parent | ||
| theme: studioTheme | ||
| uiFont: root.uiFont | ||
| message: root.toastMessage | ||
| detail: root.toastDetail | ||
| error: root.toastError | ||
| } | ||
| } | ||
|
|
||
| Window { | ||
|
|
@@ -1370,12 +1496,12 @@ ApplicationWindow { | |
| } | ||
|
|
||
| StatusToast { | ||
| anchors.horizontalCenter: parent.horizontalCenter | ||
| anchors.bottom: parent.bottom | ||
| anchors.bottomMargin: 38 | ||
| anchors.centerIn: parent | ||
| visible: root.toastMessage.length > 0 && !configurationWindow.visible | ||
| theme: studioTheme | ||
| uiFont: root.uiFont | ||
| message: root.toastMessage | ||
| detail: root.toastDetail | ||
| error: root.toastError | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,38 +1,80 @@ | ||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||
| // Center-screen operator notice. Presentation ONLY: dismissal never changes | ||
| // board state, acknowledges a fault, or retries PTP/SV behind the operator. | ||
| import QtQuick | ||
| import QtQuick.Controls | ||
| import QtQuick.Layouts | ||
|
|
||
| Rectangle { | ||
| id: toast | ||
| objectName: "centralOperatorToast" | ||
| property var theme | ||
| property string uiFont: "Inter" | ||
| property string message: "" | ||
| property string detail: "" | ||
| property bool error: false | ||
|
|
||
| implicitWidth: Math.min(620, messageLabel.implicitWidth + 30) | ||
| implicitHeight: 36 | ||
| radius: 6 | ||
| color: "#18212b" | ||
| width: Math.min(490, parent ? Math.max(260, parent.width - 36) : 490) | ||
| implicitHeight: message.length > 0 ? (detail.length > 0 ? 124 : 72) : 0 | ||
| radius: 12 | ||
| z: 900 | ||
| color: "#1b2632" | ||
| border.width: 1 | ||
| border.color: error ? "#70404a" : "#36536f" | ||
| border.color: error ? "#b65b62" : "#557287" | ||
| opacity: message.length > 0 ? 1 : 0 | ||
| scale: message.length > 0 ? 1 : 0.985 | ||
| scale: message.length > 0 ? 1 : 0.97 | ||
| visible: opacity > 0 | ||
|
|
||
| Behavior on opacity { NumberAnimation { duration: 140; easing.type: Easing.OutCubic } } | ||
| Behavior on scale { NumberAnimation { duration: 140; easing.type: Easing.OutCubic } } | ||
| Behavior on opacity { NumberAnimation { duration: 170; easing.type: Easing.OutCubic } } | ||
| Behavior on scale { NumberAnimation { duration: 170; easing.type: Easing.OutCubic } } | ||
|
|
||
| Label { | ||
| id: messageLabel | ||
| RowLayout { | ||
| anchors.fill: parent | ||
| anchors.leftMargin: 14 | ||
| anchors.rightMargin: 14 | ||
| text: toast.message | ||
| color: toast.error ? "#ffb3bb" : toast.theme.textSoft | ||
| font.family: toast.uiFont | ||
| font.pixelSize: 9 | ||
| horizontalAlignment: Text.AlignHCenter | ||
| verticalAlignment: Text.AlignVCenter | ||
| elide: Text.ElideRight | ||
| anchors.margins: 16 | ||
| spacing: 12 | ||
|
|
||
| Rectangle { | ||
| Layout.alignment: Qt.AlignTop | ||
| Layout.preferredWidth: 30 | ||
| Layout.preferredHeight: 30 | ||
| radius: 15 | ||
| color: toast.error ? "#503038" : "#274255" | ||
| Label { | ||
| anchors.centerIn: parent | ||
| text: toast.error ? "!" : "i" | ||
| color: toast.error ? "#ffb8bf" : "#b9daec" | ||
| font.family: toast.uiFont | ||
| font.pixelSize: 16 | ||
| font.weight: Font.Bold | ||
| } | ||
| } | ||
|
|
||
| ColumnLayout { | ||
| Layout.fillWidth: true | ||
| Layout.alignment: Qt.AlignVCenter | ||
| spacing: 5 | ||
| Label { | ||
| Layout.fillWidth: true | ||
| text: toast.message | ||
| color: "#f2f7fb" | ||
| font.family: toast.uiFont | ||
| font.pixelSize: 13 | ||
| font.weight: Font.DemiBold | ||
| wrapMode: Text.WordWrap | ||
| maximumLineCount: 2 | ||
| elide: Text.ElideRight | ||
| } | ||
| Label { | ||
| Layout.fillWidth: true | ||
| visible: toast.detail.length > 0 | ||
| text: toast.detail | ||
| color: "#b7c5d4" | ||
| font.family: toast.uiFont | ||
| font.pixelSize: 11 | ||
| wrapMode: Text.WordWrap | ||
| maximumLineCount: 3 | ||
| elide: Text.ElideRight | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the board reports PHY
DOWN, this condition classifies every message containingPTPas a start failure regardless of thefailureargument or message semantics. For example, the existing successful Copy diagnostics path sends"PTP and firmware diagnostics copied."fromPtpExpertPanel.qml, but in the common link-down troubleshooting state it now displays the Ethernet-disconnected error instead of confirming the copy; restrict this fallback to start/failure messages rather than all PTP text.Useful? React with 👍 / 👎.