Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 51 additions & 6 deletions apps/arstack_studio/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ ApplicationWindow {
function onDeviceMessage(message) {
root.showMessage(message, false)
}
function onLastErrorChanged() {
if (device.lastError && device.lastError.length > 0)
root.showMessage(device.lastError, true)
Comment on lines +213 to +215

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Consume each device error only once

DeviceController::setError() emits lastErrorChanged and then deviceMessage for the same failure, so this new handler first records/displays it as critical and the existing onDeviceMessage immediately processes it again as informational. Every native error therefore creates duplicate history entries with conflicting severity, and errors containing words matched by meaningful—for example the Ethernet-driver message containing “not STARTED”—replace the critical 4.2-second toast with a non-error 2.3-second toast. Suppress the paired informational handling or expose a single severity-aware event.

Useful? React with 👍 / 👎.

}
function onSvStopReasonChanged() {
if (!device.running && device.svStopReason !== "" &&
device.svStopReason !== "AWAITING_BOARD_CAUSE") {
Expand Down Expand Up @@ -310,18 +314,51 @@ ApplicationWindow {
ListElement { timeText: "--:--:--"; messageText: "ARStack Studio ready."; isError: false }
}

property string lastToastKey: ""
property double lastToastAt: 0
function explainFailure(message) {
const m = String(message || "")
if (m.indexOf("ETHERNET_LINK_DOWN") >= 0 ||
m.indexOf("Ethernet PHY link is DOWN") >= 0 ||
m.indexOf("source did not emit") >= 0 ||
m.indexOf("no verified Announce") >= 0)
return "PTP belum bisa aktif. Sambungkan kabel LAN ESP32-P4 ke switch/port Ethernet aktif, tunggu PHY UP, lalu START PTP."
if (m.indexOf("ETHERNET_DRIVER_NOT_STARTED") >= 0 ||
m.indexOf("ETHERNET_DRIVER_UNAVAILABLE") >= 0)
return "Driver Ethernet ESP32-P4 belum siap. Periksa koneksi board, lalu restart perangkat bila driver tetap STOPPED."
if (m.indexOf("ETHERNET_TX_INVALID_STATE") >= 0 ||
m.indexOf("ESP_ERR_INVALID_STATE") >= 0)
return "PTP TX ditolak EMAC. Periksa Driver/PHY di Advanced → Time Sync; salin diagnostics jika PHY sudah UP."
if (m.indexOf("LEASE_EXPIRED") >= 0)
return "Injeksi SV berhenti: heartbeat kontrol USB terputus. Periksa kabel USB/COM lalu START kembali."
if (m.indexOf("RUNTIME_FAULT") >= 0)
return "Board menghentikan injeksi SV karena gangguan runtime. Buka Copy diagnostics sebelum START ulang."
return m
}
function showMessage(message, error) {
if (!message || String(message).trim().length === 0) return
statusMessage = message
transientMessage = error === true ? message : ""
transientError = error === true
const critical = error === true
const userMessage = critical ? explainFailure(message) : String(message)
const key = (critical ? "E:" : "I:") + userMessage
const now = Date.now()
const duplicate = key === lastToastKey && now - lastToastAt < 4000
Comment on lines +343 to +345

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow identical failures after the dedupe window

The four-second deduplication cannot provide another toast for a repeated native error because DeviceController::setError() returns whenever lastError_ == message, so neither signal reaches this code again. For example, after a PHY-down PTP attempt fails and its toast expires, retrying against the same disconnected cable produces the identical actionable error and no second toast even minutes later; the asynchronous start was initially accepted, so PtpExpertPanel does not supply a fallback notification. Clear or re-emit the error per operation before applying this time-bounded UI deduplication.

Useful? React with 👍 / 👎.

const meaningful = critical || /accepted|ready|connected|installed|updated|stopped|started|saved|completed|success/i.test(userMessage)
if (meaningful && !duplicate) {
lastToastKey = key
lastToastAt = now
transientMessage = userMessage
transientError = critical
messageTimer.interval = critical ? 4200 : 2300
messageTimer.restart()
}
statusHistoryModel.append({
timeText: new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss"),
messageText: message,
isError: error === true
})
while (statusHistoryModel.count > 100)
statusHistoryModel.remove(0)
messageTimer.restart()
}

function detachPhasor() {
Expand Down Expand Up @@ -847,6 +884,15 @@ ApplicationWindow {
configurationWindow.hide()
}

StatusToast {
anchors.centerIn: parent
z: 1500
theme: studioTheme
uiFont: root.uiFont
message: root.toastMessage
error: root.toastError
}

DockFrame {
anchors.fill: parent
anchors.margins: 10
Expand Down Expand Up @@ -1370,9 +1416,8 @@ ApplicationWindow {
}

StatusToast {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 38
anchors.centerIn: parent
z: 1500
theme: studioTheme
uiFont: root.uiFont
message: root.toastMessage
Expand Down
19 changes: 12 additions & 7 deletions apps/arstack_studio/qml/StatusToast.qml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ Rectangle {
property string message: ""
property bool error: false

implicitWidth: Math.min(620, messageLabel.implicitWidth + 30)
implicitHeight: 36
radius: 6
implicitWidth: Math.min(520, Math.max(290, messageLabel.implicitWidth + 52))
implicitHeight: Math.max(60, messageLabel.implicitHeight + 30)
radius: 12
color: "#18212b"
border.width: 1
border.color: error ? "#70404a" : "#36536f"
border.color: error ? "#98505a" : "#36536f"
opacity: message.length > 0 ? 1 : 0
scale: message.length > 0 ? 1 : 0.985
visible: opacity > 0
Expand All @@ -25,14 +25,19 @@ Rectangle {
Label {
id: messageLabel
anchors.fill: parent
anchors.leftMargin: 14
anchors.rightMargin: 14
anchors.leftMargin: 24
anchors.rightMargin: 24
anchors.topMargin: 12
anchors.bottomMargin: 12
text: toast.message
color: toast.error ? "#ffb3bb" : toast.theme.textSoft
font.family: toast.uiFont
font.pixelSize: 9
font.pixelSize: 13
font.weight: Font.DemiBold
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
maximumLineCount: 4
elide: Text.ElideRight
}
}
7 changes: 7 additions & 0 deletions apps/arstack_studio/src/DeviceController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2130,9 +2130,16 @@ void DeviceController::processLine(const QString& rawLine) {
if (match.hasMatch()) {
// Only the board's ETH_EVENT stream may establish this state;
// NEVER infer it from COM, profile selection or PTP SOURCE intent.
const QString previousLink = ptpEthernetLink_;
ptpEthernetDriver_ = match.captured(1).toUpper();
ptpEthernetLink_ = match.captured(2).toUpper();
emit ptpStateChanged();
if (previousLink == QStringLiteral("DOWN") &&
ptpEthernetLink_ == QStringLiteral("UP") &&
ptpEthernetDriver_ == QStringLiteral("STARTED")) {
emit deviceMessage(QStringLiteral(
"Ethernet connected — PHY UP. PTP dapat di-START kembali."));
}
}

match = kPtpStatusExpression.match(line);
Expand Down
15 changes: 15 additions & 0 deletions apps/arstack_studio/tests/check_operator_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,21 @@ def main() -> None:
"SV output must fail-closed on measured heartbeat loss, preserve board STOP cause, "
"and never show misleading green READY after an unrequested stop")

toast_layout = source("Main.qml")
toast_view = source("StatusToast.qml")
require("function explainFailure(message)" in toast_layout
and "ETHERNET_LINK_DOWN" in toast_layout
and "ETHERNET_TX_INVALID_STATE" in toast_layout
and "function onLastErrorChanged()" in toast_layout
and toast_layout.count("anchors.centerIn: parent") >= 2
and "lastToastAt" in toast_layout
and "messageTimer.interval = critical ? 4200 : 2300" in toast_layout
and "wrapMode: Text.WordWrap" in toast_view
and "Ethernet connected" in
(STUDIO / "src/DeviceController.cpp").read_text(encoding="utf-8"),
"PTP physical-link and board failures must generate bounded actionable center-screen toasts "
"in both the main window and Advanced")

clipboard_header = (STUDIO / "src/StudioClipboard.hpp").read_text(encoding="utf-8")
clipboard_cpp = (STUDIO / "src/StudioClipboard.cpp").read_text(encoding="utf-8")
main_cpp = (STUDIO / "src/main.cpp").read_text(encoding="utf-8")
Expand Down
Loading