From d3e191c10505299561c251e9e6a0b2e02d8854c5 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 4 Nov 2026 02:19:57 +0700 Subject: [PATCH 1/3] fix(P1.2): unblock supervised firmware update through verified SV/PTP quiescence Remove Advanced firmware action's circular active-PTP disabled gate. Add operator confirmation and single-supervisor STOP/readback preflight that retires pending SV/profile transactions, preempts both services, requires board SVLIFE and PTP STOP proof before ROM probe, blocks new mutations during handoff, and fails closed on uncertain state. Add deterministic regression for CFG-pending plus PTP STOP_UNVERIFIED. --- apps/arstack_studio/qml/FirmwarePanel.qml | 56 +++++++++++-- .../src/DeterministicSessionHarness.cpp | 50 ++++++++++++ apps/arstack_studio/src/DeviceController.cpp | 81 +++++++++++++++++++ apps/arstack_studio/src/DeviceController.hpp | 12 +++ .../src/SmartSessionController.cpp | 57 +++++++------ 5 files changed, 223 insertions(+), 33 deletions(-) diff --git a/apps/arstack_studio/qml/FirmwarePanel.qml b/apps/arstack_studio/qml/FirmwarePanel.qml index bd25ac991..6b3db2114 100644 --- a/apps/arstack_studio/qml/FirmwarePanel.qml +++ b/apps/arstack_studio/qml/FirmwarePanel.qml @@ -250,15 +250,10 @@ SurfacePanel { visible: panel.session && (panel.session.firmwareUpdateRequired || panel.session.firmwareUpdateAvailable || panel.session.firmwareReinstallAvailable) enabled: visible && !panel.firmware.busy && - !panel.device.running && !panel.device.ptpRunning && !panel.device.ptpStarting - toolTipText: enabled ? "Install the matched firmware" : "Stop active SV/PTP in the main firmware update dialog first" + !panel.session.updatingFirmware + toolTipText: "Studio will stop SV/PTP and verify board readback before firmware installation" tone: panel.session && panel.session.firmwareUpdateRequired ? "accent" : "normal" - onClicked: { - if (panel.session.firmwareUpdateRequired || panel.session.firmwareUpdateAvailable) - panel.session.beginFirmwareUpdate() - else - panel.session.beginFirmwareReinstall() - } + onClicked: firmwareConfirmDialog.open() } @@ -362,4 +357,49 @@ SurfacePanel { Layout.fillHeight: true } } + + Dialog { + id: firmwareConfirmDialog + modal: true + anchors.centerIn: Overlay.overlay + width: Math.min(400, panel.width - 32) + padding: 16 + closePolicy: Popup.CloseOnEscape + title: "Update board firmware" + background: Rectangle { + color: panel.theme.surface + radius: 9 + border.width: 1 + border.color: panel.theme.line + } + contentItem: ColumnLayout { + spacing: 12 + Label { + Layout.fillWidth: true + text: "Studio will stop SV and PTP, verify both are stopped on the board, then install the matching firmware. If STOP cannot be verified, nothing will be flashed. Output will not restart automatically." + wrapMode: Text.WordWrap + color: panel.theme.textSoft + font.family: panel.uiFont + font.pixelSize: 11 + } + RowLayout { + Layout.fillWidth: true + Item { Layout.fillWidth: true } + CalmButton { + theme: panel.theme; uiFont: panel.uiFont; text: "Cancel" + onClicked: firmwareConfirmDialog.close() + } + CalmButton { + theme: panel.theme; uiFont: panel.uiFont + text: "Stop services & update"; tone: "accent"; implicitWidth: 172 + onClicked: { + const ok = (panel.session.firmwareUpdateRequired || panel.session.firmwareUpdateAvailable) + ? panel.session.beginFirmwareUpdate() + : panel.session.beginFirmwareReinstall() + if (ok) firmwareConfirmDialog.close() + } + } + } + } + } } diff --git a/apps/arstack_studio/src/DeterministicSessionHarness.cpp b/apps/arstack_studio/src/DeterministicSessionHarness.cpp index f039e96b1..b3c659652 100644 --- a/apps/arstack_studio/src/DeterministicSessionHarness.cpp +++ b/apps/arstack_studio/src/DeterministicSessionHarness.cpp @@ -43,6 +43,7 @@ class DeterministicSessionHarness final { const CaseResult cases[] = { {"dual-revision catalog selects only qualified silicon", revisionCatalogFailClosed()}, {"firmware timeout retains port until flasher exits", firmwarePortReleaseWaitsForWorker()}, + {"firmware update waits for fresh SV and PTP STOP readback", firmwareStopProofPreventsEarlyFlash()}, {"real timed-out firmware subprocess retires before COM release", firmwareWorkerTimeoutAwaitsProcessExit()}, {"current identity -> READY without recovery", currentIdentityNeverRecovers()}, {"legacy identity -> firmware update", legacyIdentityOffersUpdate()}, @@ -214,6 +215,55 @@ class DeterministicSessionHarness final { return restartedCleanly && !unexpectedTerminal && terminalCount == 2; } + + static bool firmwareStopProofPreventsEarlyFlash() { + Fixture f; + if (!f.profileReady) return false; + auto stale = identity(); + stale.buildId = QStringLiteral("fedcba9876543210"); + seedVerified(f, stale, QStringLiteral("COM7"), false); + auto& d = f.device; + auto& s = f.session; + // Reproduce field condition: not RUNNING, but a stale SV START + // transaction and an unverified PTP STOP block the GUI path. + d.ioWorkerReady_ = true; + d.ptpAvailable_ = true; + d.ptpRunning_ = true; + d.ptpStartStage_ = DeviceController::PtpStartStage::stop_unconfirmed; + d.scenarioStage_ = DeviceController::ScenarioStage::awaiting_commit; + const bool started = s.beginFirmwareUpdate(); + const bool safeStage = started && s.updateRequested_ && + s.updateStage_ == SmartSessionController::UpdateStage::stopping && + s.portOwner_ == SmartSessionController::PortOwner::deviceSession && + d.scenarioStage_ == DeviceController::ScenarioStage::idle && + !d.firmwareQuiescenceVerified() && !f.firmware.busy_; + if (!safeStage) { + s.updateRequested_ = false; + d.endFirmwareQuiescence(); + return false; + } + + d.processLine(QStringLiteral("STOP accepted: SV transmission suppressed")); + // SV STOP accepted is insufficient without a fresh board SHOW. + const bool ackOnlyBlocked = d.firmwareSvStopAck_ && + !d.firmwareQuiescenceVerified(); + d.processLine(QStringLiteral( + "SVLIFE state=STOPPED stop_cause=OPERATOR_STOP stop_seq=1")); + const bool stillWaitingForPtp = d.firmwareSvStoppedReadback_ && + !d.firmwareQuiescenceVerified(); + d.processLine(QStringLiteral("PTP stop requested")); + d.processLine(QStringLiteral( + "PTP status=STOPPED Announce=0 Sync=0 FollowUp=0 PdelayFrames=0 TXfail=0")); + const bool proof = d.firmwareQuiescenceVerified(); + d.ptpRunning_ = true; + const bool rejectsReactivation = !d.firmwareQuiescenceVerified(); + s.updateRequested_ = false; + s.firmwareTotalTimer_.stop(); + d.endFirmwareQuiescence(); + return ackOnlyBlocked && stillWaitingForPtp && proof && + rejectsReactivation && !d.firmwareQuiescenceVerified(); + } + static bool revisionCatalogFailClosed() { FirmwareManager firmware; firmware.revisionCatalogAvailable_ = true; diff --git a/apps/arstack_studio/src/DeviceController.cpp b/apps/arstack_studio/src/DeviceController.cpp index a1c050660..b19ef1a09 100644 --- a/apps/arstack_studio/src/DeviceController.cpp +++ b/apps/arstack_studio/src/DeviceController.cpp @@ -1794,6 +1794,54 @@ bool DeviceController::startPtp() { } return sendCommand(QStringLiteral("PTP START")); } +bool DeviceController::beginFirmwareQuiescence() { + if (!connected_ || !deviceVerified_ || !ioWorkerReady_ || sessionGeneration_ == 0 || + identity_.bootId.isEmpty()) { + setError(QStringLiteral("Firmware update requires a connected, verified board.")); + return false; + } + + // An unfinished PROFILE/CFG transaction can keep the PTP STOP readback + // deferred until its deadline. Retire that local work before STOP, even + // when RUNNING is false (START may still be pending). + endFirmwareQuiescence(); + if (profileDeploying_) abandonProfileDeployment(); + firmwareQuiescenceActive_ = true; + firmwareQuiescenceSession_ = sessionGeneration_; + firmwareQuiescenceBoot_ = identity_.bootId; + + // Explicit operator-authorized STOP for BOTH services, independent of + // cached telemetry. STOP preempts pending SV START in the serial worker. + const bool svAccepted = stop(); + const bool ptpAccepted = svAccepted && stopPtp(); + if (!svAccepted || !ptpAccepted) { + endFirmwareQuiescence(); + setError(QStringLiteral( + "Could not request verified SV/PTP STOP. Firmware write remains blocked.")); + return false; + } + return true; +} + +void DeviceController::endFirmwareQuiescence() noexcept { + firmwareQuiescenceActive_ = false; + firmwareSvStopAck_ = false; + firmwareSvStoppedReadback_ = false; + firmwarePtpStoppedReadback_ = false; + firmwareQuiescenceSession_ = 0; + firmwareQuiescenceBoot_.clear(); +} + +bool DeviceController::firmwareQuiescenceVerified() const noexcept { + return firmwareQuiescenceActive_ && connected_ && deviceVerified_ && + firmwareQuiescenceSession_ == sessionGeneration_ && + firmwareQuiescenceBoot_ == identity_.bootId && + firmwareSvStopAck_ && firmwareSvStoppedReadback_ && + firmwarePtpStoppedReadback_ && !running_ && !ptpRunning_ && !ptpStarting() && + scenarioStage_ == ScenarioStage::idle && !profileDeploying_ && + !m4Deploying_ && !m4ScenarioApplying_ && !m4ScenarioRetiring_; +} + bool DeviceController::stopPtp() { if (!connected_ || !ioWorkerReady_ || !deviceVerified_) { setError(QStringLiteral("Connect the verified ESP32-P4 before stopping PTP.")); @@ -2010,6 +2058,12 @@ void DeviceController::advancePtpStartup(const QString& line) { resetPtpStartup(); ptpRunning_ = false; ptpStatus_ = QStringLiteral("PTP stopped — confirmed by board"); + if (firmwareQuiescenceActive_ && + firmwareQuiescenceSession_ == sessionGeneration_ && + firmwareQuiescenceBoot_ == identity_.bootId) { + firmwarePtpStoppedReadback_ = true; + emit firmwareQuiescenceChanged(); + } emit ptpStateChanged(); } else if (!ptpStopPollTimer_.isActive()) { // Firmware stop is asynchronous. Poll on Qt timers without @@ -2196,6 +2250,18 @@ bool DeviceController::sendQuietCommand(const QString& command) { bool DeviceController::sendCommandBatch(const QStringList& commands, const bool quiet, const bool exclusive) { const bool single = !exclusive && commands.size() == 1; const QString onlyCommand = single ? commands.constFirst() : QString{}; + // Firmware STOP proof owns the transition to ROM mode. Never allow + // a queued UI START/CONFIG transaction to rearm the board in this window. + if (firmwareQuiescenceActive_ && + (!single || (onlyCommand != QStringLiteral("STOP") && + onlyCommand != QStringLiteral("PTP STOP") && + onlyCommand != QStringLiteral("SHOW") && + onlyCommand != QStringLiteral("PTP SHOW") && + onlyCommand != QStringLiteral("HEARTBEAT") && + !onlyCommand.startsWith(QStringLiteral("PROFILE BINABORT "))))) { + setError(QStringLiteral("Firmware update is safely stopping board services.")); + return false; + } // PTP STOP has higher safety priority than non-mutating MU inspection; // never let the reader postpone STOP readback beyond its own deadline. if (m4BoardInspecting_ && @@ -2865,6 +2931,14 @@ void DeviceController::processLine(const QString& rawLine) { // local RUNNING -> STOPPED incident may raise a live alarm. setRunning(false); // preserve operatorStopPending_ until edge classification operatorStopPending_ = false; + if (firmwareQuiescenceActive_ && !forced && + firmwareQuiescenceSession_ == sessionGeneration_ && + firmwareQuiescenceBoot_ == identity_.bootId) { + firmwareSvStopAck_ = true; + // This SHOW is ordered AFTER STOP on the same serial worker; + // a plain STOP log or cached running=false is not stop proof. + static_cast(sendQuietCommand(QStringLiteral("SHOW"))); + } if (forced && svUnexpectedStopObserved_ && svStopReason_ != QStringLiteral("RUNTIME_FAULT")) { svStopReason_ = QStringLiteral("RUNTIME_FAULT"); @@ -2901,6 +2975,13 @@ void DeviceController::processLine(const QString& rawLine) { QStringLiteral("SVLIFE state=(RUNNING|STOPPED) stop_cause=([A-Z_]+) stop_seq=(\\d+)")); const auto svLife = kSvLife.match(line); if (svLife.hasMatch()) { + if (firmwareQuiescenceActive_ && firmwareSvStopAck_ && + firmwareQuiescenceSession_ == sessionGeneration_ && + firmwareQuiescenceBoot_ == identity_.bootId) { + firmwareSvStoppedReadback_ = + svLife.captured(1) == QStringLiteral("STOPPED"); + emit firmwareQuiescenceChanged(); + } bool seqOk = false; const auto seq = svLife.captured(3).toULongLong(&seqOk); if (seqOk && (seq >= lastSvStopSeq_ || !deviceVerified_)) { diff --git a/apps/arstack_studio/src/DeviceController.hpp b/apps/arstack_studio/src/DeviceController.hpp index 630a23957..0ad3b8008 100644 --- a/apps/arstack_studio/src/DeviceController.hpp +++ b/apps/arstack_studio/src/DeviceController.hpp @@ -313,6 +313,11 @@ class DeviceController : public QObject { bool startPtp(); bool startPtpConfigured(const QString& role, const QVariantMap& profile); bool stopPtp(); + // One supervised firmware-update safety gate. This is not a second + // serial owner: the existing STOP/PTP STOP/readback paths remain authoritative. + bool beginFirmwareQuiescence(); + void endFirmwareQuiescence() noexcept; + [[nodiscard]] bool firmwareQuiescenceVerified() const noexcept; bool configurePtp(const QVariantMap& profile); bool setPtpRole(const QString& requestedRole) { @@ -373,6 +378,7 @@ class DeviceController : public QObject { void m4BoardChanged(); void m4ReconnectModeChanged(); void ptpStateChanged(); + void firmwareQuiescenceChanged(); void deviceMessage(const QString& message); void scenarioStateChanged(); void scenarioApplicationFinished(); @@ -513,6 +519,12 @@ class DeviceController : public QObject { int highConfidenceCount_{0}; bool running_{false}; bool operatorStopPending_{false}; + bool firmwareQuiescenceActive_{false}; + bool firmwareSvStopAck_{false}; + bool firmwareSvStoppedReadback_{false}; + bool firmwarePtpStoppedReadback_{false}; + quint64 firmwareQuiescenceSession_{0}; + QString firmwareQuiescenceBoot_; QString svStopReason_; QString svLastStopCause_; // Only an observed unexpected RUNNING -> STOPPED edge in this Studio diff --git a/apps/arstack_studio/src/SmartSessionController.cpp b/apps/arstack_studio/src/SmartSessionController.cpp index 950fda184..8d9c67d2f 100644 --- a/apps/arstack_studio/src/SmartSessionController.cpp +++ b/apps/arstack_studio/src/SmartSessionController.cpp @@ -638,21 +638,27 @@ bool SmartSessionController::beginFirmwareOperation(const QString& portName) { setupErrorStatus_.clear(); lastFirmwareUpdateResult_.clear(); - if (device_->running() || device_->ptpRunning() || device_->ptpStarting()) { + if (device_->deviceVerified()) { + if (!device_->connected() || portOwner_ != PortOwner::deviceSession) { + latchFirmwareFailure(QStringLiteral( + "Verified board serial ownership was lost. Firmware write is blocked.")); + emit firmwareUpdateFinished(false); + reconcile(); + return false; + } + // Always issue a fresh STOP + readback for BOTH board services. + // PREPARING/CFG and STOP_UNVERIFIED are not proof that output is idle. updateStage_ = UpdateStage::stopping; - const bool stopSv = device_->running(); - stoppingPtpForFirmware_ = device_->ptpRunning() || device_->ptpStarting(); + stoppingPtpForFirmware_ = true; setPresentation( QStringLiteral("UPDATING FIRMWARE"), - QStringLiteral("Stopping active SV/PTP test before firmware installation…"), + QStringLiteral("Stopping and verifying SV/PTP on the board…"), false, false); - // Both STOP paths are ACK/readback verified by the existing device owner. - // An asynchronous STOP acceptance is not evidence of a stopped board. - const bool svAccepted = !stopSv || device_->stop(); - const bool ptpAccepted = !stoppingPtpForFirmware_ || (svAccepted && device_->stopPtp()); - if (!svAccepted || !ptpAccepted) { - latchFirmwareFailure(QStringLiteral("Could not safely stop active SV/PTP output. Firmware access was blocked.")); + if (!device_->beginFirmwareQuiescence()) { + latchFirmwareFailure(QStringLiteral( + "The board did not accept the safe SV/PTP stop preflight. " + "Nothing was flashed; inspect diagnostics and retry.")); emit firmwareUpdateFinished(false); reconcile(); return false; @@ -660,6 +666,8 @@ bool SmartSessionController::beginFirmwareOperation(const QString& portName) { return true; } + // Only an explicitly authorized blank-board recovery may probe without + // application STOP proof; it still needs ROM identity and isolation gates. continueFirmwareUpdate(); return true; } @@ -786,10 +794,12 @@ bool SmartSessionController::startFirmwareProbe() { void SmartSessionController::continueFirmwareUpdate() { if (!updateRequested_ || device_ == nullptr || firmware_ == nullptr) return; - if (device_->running() || device_->ptpRunning() || device_->ptpStarting()) { - updateStage_ = UpdateStage::stopping; - return; + if (updateStage_ == UpdateStage::stopping) { + if (!device_->firmwareQuiescenceVerified()) return; } + if (device_->connected() && device_->deviceVerified() && + (device_->running() || device_->ptpRunning() || device_->ptpStarting() || + !device_->firmwareQuiescenceVerified())) return; if (portOwner_ == PortOwner::firmwareTool) { static_cast(startFirmwareProbe()); @@ -907,19 +917,15 @@ void SmartSessionController::reconnectDeviceSignals() { reconcile(); }); }); - connect(device_, &DeviceController::runningChanged, this, [this] { - if (updateRequested_ && updateStage_ == UpdateStage::stopping && - device_ != nullptr && !device_->running() && - !device_->ptpRunning() && !device_->ptpStarting()) { - QTimer::singleShot(0, this, [this] { continueFirmwareUpdate(); }); - } - reconcile(); - }); - connect(device_, &DeviceController::ptpStateChanged, this, [this] { + connect(device_, &DeviceController::runningChanged, this, &SmartSessionController::reconcile); + connect(device_, &DeviceController::ptpStateChanged, this, &SmartSessionController::reconcile); + connect(device_, &DeviceController::firmwareQuiescenceChanged, this, [this] { if (updateRequested_ && updateStage_ == UpdateStage::stopping && - device_ != nullptr && !device_->running() && - !device_->ptpRunning() && !device_->ptpStarting()) { - QTimer::singleShot(0, this, [this] { continueFirmwareUpdate(); }); + device_ != nullptr && device_->firmwareQuiescenceVerified()) { + QTimer::singleShot(0, this, [this] { + if (updateRequested_ && updateStage_ == UpdateStage::stopping) + continueFirmwareUpdate(); + }); } reconcile(); }); @@ -1238,6 +1244,7 @@ QString SmartSessionController::diagnosticsText() const { void SmartSessionController::latchFirmwareFailure(QString message) { reconnectTimer_.stop(); firmwareTotalTimer_.stop(); + if (device_ != nullptr) device_->endFirmwareQuiescence(); stoppingPtpForFirmware_ = false; const bool optionalOperation = updateWasOptional_; updateRequested_ = false; From 2f587829a888c90083d86867317f8e39a1e0b002 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 4 Nov 2026 02:23:48 +0700 Subject: [PATCH 2/3] fix(firmware): release verified stop lease after serial handoff and new boot Prevent maintenance quiescence from persisting into a successfully updated board and blocking future START; wake firmware preflight when prior M4 exclusive commands finish retirement. --- apps/arstack_studio/src/DeviceController.cpp | 3 +++ apps/arstack_studio/src/SmartSessionController.cpp | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/apps/arstack_studio/src/DeviceController.cpp b/apps/arstack_studio/src/DeviceController.cpp index b19ef1a09..0bf13a391 100644 --- a/apps/arstack_studio/src/DeviceController.cpp +++ b/apps/arstack_studio/src/DeviceController.cpp @@ -468,6 +468,9 @@ void DeviceController::connectWorkerSignals() { !m4ScenarioRetiring_) return; m4ScenarioRetiring_ = false; emit m4DeploymentChanged(); + // If an update was waiting for the last old CFG write to retire, + // fresh STOP evidence may now complete its verified handoff. + if (firmwareQuiescenceActive_) emit firmwareQuiescenceChanged(); }); connect(ioWorker_, &DeviceIoWorker::commandRejected, this, diff --git a/apps/arstack_studio/src/SmartSessionController.cpp b/apps/arstack_studio/src/SmartSessionController.cpp index 8d9c67d2f..ab5d70c48 100644 --- a/apps/arstack_studio/src/SmartSessionController.cpp +++ b/apps/arstack_studio/src/SmartSessionController.cpp @@ -937,6 +937,11 @@ void SmartSessionController::reconnectDeviceSignals() { return; } pendingReleaseGeneration_ = 0; + // The verified STOP lease ends only AFTER QSerialPort has closed. + // Release DeviceController's local mutation guard before the next + // firmware boot; retaining it would block START after a successful + // update, even though the new board identity is verified. + if (device_ != nullptr) device_->endFirmwareQuiescence(); setPortOwner(PortOwner::none); QTimer::singleShot(0, this, [this] { continueFirmwareUpdate(); }); }); @@ -1567,6 +1572,9 @@ void SmartSessionController::reconcile() { } firmwareTotalTimer_.stop(); stoppingPtpForFirmware_ = false; + // A rebooted, verified board must never inherit a pre-flash + // maintenance lock from the old device session. + device_->endFirmwareQuiescence(); updateRequested_ = false; updateWasOptional_ = false; updateStage_ = UpdateStage::idle; From 92b3d3ad71154f6db7e7ed8808a1fb978a1c0638 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 4 Nov 2026 02:33:44 +0700 Subject: [PATCH 3/3] fix(studio): show firmware STOP/readback stage in Advanced without hidden dialogs Display supervisor updateStatus from authorization through reconnect in the same Advanced panel; block port refresh during exclusive handoff, show truthful Updating status in the compact header, and close confirmation even when C++ rejects preflight so the error is visible. --- apps/arstack_studio/qml/ConfigurationHub.qml | 17 ++++++++++----- apps/arstack_studio/qml/FirmwarePanel.qml | 23 ++++++++++++-------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/apps/arstack_studio/qml/ConfigurationHub.qml b/apps/arstack_studio/qml/ConfigurationHub.qml index cc41ad14d..33887362f 100644 --- a/apps/arstack_studio/qml/ConfigurationHub.qml +++ b/apps/arstack_studio/qml/ConfigurationHub.qml @@ -93,12 +93,14 @@ Item { } Rectangle { - implicitWidth: 112 + implicitWidth: hub.session && hub.session.updatingFirmware ? 144 : 112 implicitHeight: 30 radius: 15 - color: hub.device.deviceVerified ? "#10251d" : "#261f13" + color: hub.session && hub.session.updatingFirmware + ? "#261f13" : hub.device.deviceVerified ? "#10251d" : "#261f13" border.width: 1 - border.color: hub.device.deviceVerified ? "#347a59" : "#705827" + border.color: hub.session && hub.session.updatingFirmware + ? "#705827" : hub.device.deviceVerified ? "#347a59" : "#705827" RowLayout { anchors.centerIn: parent @@ -107,11 +109,14 @@ Item { width: 7 height: 7 radius: 4 - color: hub.device.deviceVerified ? hub.theme.green : hub.theme.amber + color: hub.session && hub.session.updatingFirmware + ? hub.theme.amber : hub.device.deviceVerified ? hub.theme.green : hub.theme.amber } Label { - text: hub.device.deviceVerified ? "Device ready" : "Setup mode" - color: hub.device.deviceVerified ? hub.theme.green : hub.theme.amber + text: hub.session && hub.session.updatingFirmware + ? "Updating firmware" : hub.device.deviceVerified ? "Device ready" : "Setup mode" + color: hub.session && hub.session.updatingFirmware + ? hub.theme.amber : hub.device.deviceVerified ? hub.theme.green : hub.theme.amber font.family: hub.uiFont font.pixelSize: 10 font.weight: Font.DemiBold diff --git a/apps/arstack_studio/qml/FirmwarePanel.qml b/apps/arstack_studio/qml/FirmwarePanel.qml index 6b3db2114..538b1c67c 100644 --- a/apps/arstack_studio/qml/FirmwarePanel.qml +++ b/apps/arstack_studio/qml/FirmwarePanel.qml @@ -13,9 +13,10 @@ SurfacePanel { property string monoFont: "Inter" property bool technicalDetailsVisible: false readonly property string firmwareDisplayStatus: - panel.session && panel.session.firmwareUpdateResult.length > 0 && - !panel.session.updatingFirmware - ? panel.session.firmwareUpdateResult : panel.firmware.status + panel.session && panel.session.updatingFirmware + ? panel.session.updateStatus + : panel.session && panel.session.firmwareUpdateResult.length > 0 + ? panel.session.firmwareUpdateResult : panel.firmware.status readonly property bool verificationFailed: panel.session && panel.session.firmwareUpdateResult.length > 0 && !panel.session.firmwareUpdateResult.startsWith("Exact bundled firmware build verified") @@ -179,14 +180,16 @@ SurfacePanel { id: firmwarePort Layout.preferredWidth: 205 model: panel.device.ports - enabled: panel.session && !panel.firmware.busy + enabled: panel.session && !panel.firmware.busy && + !panel.session.updatingFirmware onPressedChanged: if (pressed && panel.session) panel.session.requestRefreshPorts() } CalmButton { theme: panel.theme uiFont: panel.uiFont text: "Refresh" - enabled: panel.session && !panel.firmware.busy + enabled: panel.session && !panel.firmware.busy && + !panel.session.updatingFirmware onClicked: panel.session.requestRefreshPorts() } Item { Layout.fillWidth: true } @@ -219,7 +222,8 @@ SurfacePanel { Layout.fillWidth: true spacing: 5 Label { - text: panel.firmware.busy ? "Installing firmware" : "Firmware action" + text: panel.session && panel.session.updatingFirmware + ? "Preparing / installing firmware" : "Firmware action" color: panel.theme.text font.family: panel.uiFont font.pixelSize: 12 @@ -227,8 +231,8 @@ SurfacePanel { } Label { Layout.fillWidth: true - text: panel.firmware.busy - ? "Keep USB connected until verification completes." + text: panel.session && panel.session.updatingFirmware + ? "Keep USB connected. Studio is verifying the board and firmware." : (panel.session && panel.session.firmwareUpdateAvailable ? "Current firmware works; update is optional." : "Installation and recovery are supervised automatically.") @@ -396,7 +400,8 @@ SurfacePanel { const ok = (panel.session.firmwareUpdateRequired || panel.session.firmwareUpdateAvailable) ? panel.session.beginFirmwareUpdate() : panel.session.beginFirmwareReinstall() - if (ok) firmwareConfirmDialog.close() + // Never leave an inoperable modal covering the error. + firmwareConfirmDialog.close() } } }