diff --git a/apps/arstack_studio/CMakeLists.txt b/apps/arstack_studio/CMakeLists.txt index d6dfe913c..ddfed8eb6 100644 --- a/apps/arstack_studio/CMakeLists.txt +++ b/apps/arstack_studio/CMakeLists.txt @@ -36,6 +36,8 @@ set(ARSTACK_STUDIO_CORE_SOURCES qt_add_executable(arstack_studio src/main.cpp + src/StudioClipboard.cpp + src/StudioClipboard.hpp ${ARSTACK_STUDIO_CORE_SOURCES} ) diff --git a/apps/arstack_studio/qml/FirmwarePanel.qml b/apps/arstack_studio/qml/FirmwarePanel.qml index a151ce9db..c0b36c5ee 100644 --- a/apps/arstack_studio/qml/FirmwarePanel.qml +++ b/apps/arstack_studio/qml/FirmwarePanel.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import ARStack.Studio 1.0 SurfacePanel { id: panel @@ -11,6 +12,13 @@ SurfacePanel { property string uiFont: "Inter" 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 + readonly property bool verificationFailed: + panel.session && panel.session.firmwareUpdateResult.length > 0 && + !panel.session.firmwareUpdateResult.startsWith("Exact bundled firmware build verified") readonly property bool hasPort: firmwarePort.currentText.length > 0 @@ -317,17 +325,17 @@ SurfacePanel { Rectangle { Layout.fillWidth: true - implicitHeight: panel.firmware.status.length > 0 ? 52 : 0 - visible: panel.firmware.status.length > 0 + implicitHeight: panel.firmwareDisplayStatus.length > 0 ? 52 : 0 + visible: panel.firmwareDisplayStatus.length > 0 radius: 7 - color: panel.firmware.targetVerified ? "#10251d" : (panel.firmware.bootloaderHelpNeeded ? "#261f13" : "#171d25") + color: panel.verificationFailed ? "#2a171c" : (panel.firmware.targetVerified ? "#10251d" : (panel.firmware.bootloaderHelpNeeded ? "#261f13" : "#171d25")) border.width: 1 - border.color: panel.firmware.targetVerified ? "#2c674e" : (panel.firmware.bootloaderHelpNeeded ? "#705827" : panel.theme.lineSoft) + border.color: panel.verificationFailed ? panel.theme.red : (panel.firmware.targetVerified ? "#2c674e" : (panel.firmware.bootloaderHelpNeeded ? "#705827" : panel.theme.lineSoft)) Label { anchors.fill: parent anchors.margins: 11 - text: panel.firmware.status - color: panel.firmware.targetVerified ? panel.theme.green : (panel.firmware.bootloaderHelpNeeded ? panel.theme.amber : panel.theme.textSoft) + text: panel.firmwareDisplayStatus + color: panel.verificationFailed ? panel.theme.red : (panel.firmware.targetVerified ? panel.theme.green : (panel.firmware.bootloaderHelpNeeded ? panel.theme.amber : panel.theme.textSoft)) font.family: panel.uiFont font.pixelSize: 10 wrapMode: Text.WordWrap @@ -344,9 +352,16 @@ SurfacePanel { text: panel.technicalDetailsVisible ? "Hide technical details" : "Technical details" onClicked: panel.technicalDetailsVisible = !panel.technicalDetailsVisible } + CalmButton { + theme: panel.theme + uiFont: panel.uiFont + text: "Copy diagnostics" + enabled: panel.session !== null + onClicked: StudioClipboard.copyText(panel.session.diagnosticsText()) + } Label { Layout.fillWidth: true - text: "Protocol, target revision, SHA-256 and firmware-service log" + text: "Review/redact device identifiers and logs before sharing" color: panel.theme.muted font.family: panel.uiFont font.pixelSize: 9 diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index 775ac6adf..4da38730e 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -410,6 +410,7 @@ ApplicationWindow { font.pixelSize: 9 } Item { Layout.fillWidth: true } + CalmButton { theme: studioTheme; uiFont: root.uiFont; text: "Copy diagnostics"; onClicked: StudioClipboard.copyText(workflowBar.session.diagnosticsText()) } CalmButton { theme: studioTheme; uiFont: root.uiFont; text: "Clear"; onClicked: device.clearLog() } } ScrollView { diff --git a/apps/arstack_studio/qml/WorkflowBar.qml b/apps/arstack_studio/qml/WorkflowBar.qml index 7ad54c4df..c4e75cf88 100644 --- a/apps/arstack_studio/qml/WorkflowBar.qml +++ b/apps/arstack_studio/qml/WorkflowBar.qml @@ -146,9 +146,9 @@ SurfacePanel { controller.showMessage("Firmware installed and verified successfully.", false) } else { ribbon.firmwareResultTitle = "Firmware installation not completed" - ribbon.firmwareResultMessage = FirmwareService.status.length > 0 - ? FirmwareService.status - : "Studio could not verify the firmware installation. Keep the board connected and try again." + ribbon.firmwareResultMessage = smartSession.firmwareUpdateResult.length > 0 + ? smartSession.firmwareUpdateResult + : "Studio could not verify the firmware installation. Open Advanced > Firmware > Copy diagnostics." controller.showMessage("Firmware installation was not verified.", true) } Qt.callLater(resultDialog.open) diff --git a/apps/arstack_studio/src/DeterministicSessionHarness.cpp b/apps/arstack_studio/src/DeterministicSessionHarness.cpp index 5829964cc..cc2111514 100644 --- a/apps/arstack_studio/src/DeterministicSessionHarness.cpp +++ b/apps/arstack_studio/src/DeterministicSessionHarness.cpp @@ -33,6 +33,7 @@ class DeterministicSessionHarness final { {"same version stale build -> READY with update suggestion", sameVersionStaleBuildOffersUpdate()}, {"optional bootloader wait -> cancel restores supervisor", optionalBootloaderWaitCanCancel()}, {"firmware stage projection -> supervisor-owned", firmwareStageProjectionIsExplicit()}, + {"firmware verification failure remains copyable after READY", firmwareFailureRemainsDiagnosable()}, {"fragmented espflash progress -> monotonic", fragmentedEspflashProgressIsMonotonic()}, {"trusted ESP32-P4 identity timeout -> firmware required", automaticIdentityTimeoutOffersFirmwareRecovery()}, {"single visible COM without metadata -> firmware required", singleVisibleTimeoutWithoutRecommendationOffersFirmwareRecovery()}, @@ -941,6 +942,26 @@ class DeterministicSessionHarness final { !device.healthProbeOutstanding_; } + static bool firmwareFailureRemainsDiagnosable() { + Fixture fixture; + if (!fixture.profileReady) return false; + auto& session = fixture.session; + session.updateWasOptional_ = true; + session.updateRequested_ = true; + session.updateStage_ = SmartSessionController::UpdateStage::reconnecting; + session.latchFirmwareFailure(QStringLiteral( + "Firmware was written, but reconnect reported a different build.")); + const QString report = session.diagnosticsText(); + return !session.updateRequested_ && !session.setupError_ && + session.firmwareUpdateResult().contains(QStringLiteral("different build")) && + report.contains(QStringLiteral("Last firmware update result: Firmware was written")) && + report.contains(QStringLiteral("Bundled build ID: 0123456789abcdef")) && + report.contains(QStringLiteral("Configured SV ID: ARSTACK_SV01")) && + report.contains(QStringLiteral("Configured publisher fps: 4000")) && + report.contains(QStringLiteral("Exact bundled build match: NOT VERIFIED")) && + report.contains(QStringLiteral("Firmware protocol compatible: UNKNOWN")); + } + static bool workerTeardownCompletes() { bool profileReady = false; { diff --git a/apps/arstack_studio/src/SmartSessionController.cpp b/apps/arstack_studio/src/SmartSessionController.cpp index ef337feaf..d1646d2d0 100644 --- a/apps/arstack_studio/src/SmartSessionController.cpp +++ b/apps/arstack_studio/src/SmartSessionController.cpp @@ -7,6 +7,7 @@ #include "SclProfileModel.hpp" #include +#include #include #include @@ -478,6 +479,7 @@ bool SmartSessionController::beginFirmwareOperation(const QString& portName) { resetProfileSync(true); setupError_ = false; setupErrorStatus_.clear(); + lastFirmwareUpdateResult_.clear(); if (device_->running()) { updateStage_ = UpdateStage::stopping; @@ -503,6 +505,7 @@ bool SmartSessionController::retryFirmwareUpdate() { if (device_ == nullptr || firmware_ == nullptr || updatePort_.isEmpty() || firmware_->busy()) return false; setupError_ = false; setupErrorStatus_.clear(); + lastFirmwareUpdateResult_.clear(); blankBoardDetected_ = false; updateRequested_ = true; updateReconnectAttempts_ = 0; @@ -861,6 +864,76 @@ void SmartSessionController::clearBlankBoardContext() { setupErrorStatus_.clear(); } +QString SmartSessionController::diagnosticsText() const { + // Operator-triggered read-only snapshot; does not touch serial ownership. + QStringList rows; + auto add = [&rows](const QString& key, const QString& value) { + rows << key + QStringLiteral(": ") + (value.isEmpty() ? QStringLiteral("-") : value); + }; + rows << QStringLiteral("ARStack Studio field diagnostics") + << QStringLiteral("Device telemetry is reported, not independently verified on wire."); + add(QStringLiteral("Studio version"), expectedFirmwareVersion()); + add(QStringLiteral("Session state"), state_); + add(QStringLiteral("Session status"), statusText_); + add(QStringLiteral("Session generation"), QString::number(sessionGeneration_)); + add(QStringLiteral("Serial owner"), portOwner_ == PortOwner::deviceSession + ? QStringLiteral("device session") : portOwner_ == PortOwner::firmwareTool + ? QStringLiteral("firmware tool") : QStringLiteral("none")); + add(QStringLiteral("Firmware update stage"), firmwareUpdateStage()); + add(QStringLiteral("Last firmware update result"), lastFirmwareUpdateResult_); + add(QStringLiteral("Setup error"), setupErrorStatus_); + if (firmware_ != nullptr) { + add(QStringLiteral("Bundled firmware version"), firmware_->firmwareVersion()); + add(QStringLiteral("Bundled build ID"), firmware_->firmwareBuildId()); + add(QStringLiteral("Bundled firmware SHA256"), firmware_->firmwareSha256()); + add(QStringLiteral("Firmware service status"), firmware_->status()); + add(QStringLiteral("Firmware target"), firmware_->targetChip()); + add(QStringLiteral("Firmware operation busy"), firmware_->busy() ? QStringLiteral("yes") : QStringLiteral("no")); + } + if (profiles_ != nullptr && profiles_->hasProfiles()) { + const QVariantMap profile = profiles_->selectedProfile(); + add(QStringLiteral("Configured SV ID"), profile.value(QStringLiteral("svId")).toString()); + add(QStringLiteral("Configured APPID"), profile.value(QStringLiteral("appId")).toString()); + add(QStringLiteral("Configured publisher fps"), profile.value(QStringLiteral("publisherRate")).toString()); + add(QStringLiteral("Configured counter wrap"), profile.value(QStringLiteral("counterModulus")).toString()); + add(QStringLiteral("Configured payload bytes"), profile.value(QStringLiteral("payloadBytes")).toString()); + } + if (device_ != nullptr) { + add(QStringLiteral("Device verified"), device_->deviceVerified() ? QStringLiteral("yes") : QStringLiteral("no")); + add(QStringLiteral("COM port"), device_->portName()); + add(QStringLiteral("Device product"), device_->deviceProduct()); + add(QStringLiteral("Device target"), device_->deviceTarget()); + add(QStringLiteral("Device ID"), device_->deviceId()); + add(QStringLiteral("Device firmware version"), device_->firmwareVersion()); + add(QStringLiteral("Device firmware build ID"), device_->firmwareBuildId()); + add(QStringLiteral("Device protocol"), device_->protocolVersion()); + add(QStringLiteral("Device capabilities"), device_->capabilities().join(QStringLiteral(", "))); + add(QStringLiteral("Exact bundled build match"), firmwareIsCurrent() ? QStringLiteral("yes") : QStringLiteral("NOT VERIFIED")); + add(QStringLiteral("Firmware protocol compatible"), !device_->deviceVerified() + ? QStringLiteral("UNKNOWN (identity not verified)") + : firmwareIsCompatible() ? QStringLiteral("yes") : QStringLiteral("no")); + add(QStringLiteral("Profile generation"), device_->profileGeneration()); + add(QStringLiteral("Profile armed"), device_->profileArmed() ? QStringLiteral("yes") : QStringLiteral("no")); + add(QStringLiteral("SMV running"), device_->running() ? QStringLiteral("yes") : QStringLiteral("no")); + add(QStringLiteral("SMV FPS reported"), device_->fps()); + add(QStringLiteral("SMV missed reported"), device_->missed()); + add(QStringLiteral("SMV TX failures reported"), device_->txFailures()); + add(QStringLiteral("PTP status reported"), device_->ptpStatus()); + add(QStringLiteral("PTP role reported"), device_->ptpRole()); + add(QStringLiteral("PTP discipline reported"), device_->ptpDiscipline()); + add(QStringLiteral("SMPSYNCH value reported"), device_->smpSynchValue()); + add(QStringLiteral("Device last error"), device_->lastError()); + rows << QStringLiteral("--- Device serial log, last 12000 chars ---") + << device_->logText().right(12000); + } + if (firmware_ != nullptr) { + rows << QStringLiteral("--- Firmware service log, last 12000 chars ---") + << firmware_->logText().right(12000); + } + rows << QStringLiteral("Review and redact site identifiers or addresses before sharing."); + return rows.join(QLatin1Char('\n')); +} + void SmartSessionController::latchFirmwareFailure(QString message) { reconnectTimer_.stop(); const bool optionalOperation = updateWasOptional_; @@ -872,6 +945,9 @@ void SmartSessionController::latchFirmwareFailure(QString message) { blankBoardDetected_ = false; if (portOwner_ == PortOwner::firmwareTool) setPortOwner(PortOwner::none); message = message.trimmed(); + lastFirmwareUpdateResult_ = message.isEmpty() + ? QStringLiteral("Firmware verification failed; inspect the firmware-service log.") + : message; setupError_ = !optionalOperation; setupErrorStatus_ = optionalOperation ? QString{} : (message.isEmpty() ? QStringLiteral("Firmware setup did not complete. Retry explicitly when the board is ready.") @@ -1143,6 +1219,7 @@ void SmartSessionController::reconcile() { updateRequested_ = false; updateWasOptional_ = false; updateStage_ = UpdateStage::idle; + lastFirmwareUpdateResult_ = QStringLiteral("Exact bundled firmware build verified after reconnect."); clearBlankBoardContext(); emit firmwareUpdateFinished(true); } diff --git a/apps/arstack_studio/src/SmartSessionController.hpp b/apps/arstack_studio/src/SmartSessionController.hpp index 8b00a16ec..b31d2a673 100644 --- a/apps/arstack_studio/src/SmartSessionController.hpp +++ b/apps/arstack_studio/src/SmartSessionController.hpp @@ -42,6 +42,7 @@ class SmartSessionController : public QObject { Q_PROPERTY(QString expectedFirmwareBuildId READ expectedFirmwareBuildId NOTIFY stateChanged) Q_PROPERTY(QString deviceFirmwareVersion READ deviceFirmwareVersion NOTIFY stateChanged) Q_PROPERTY(QString deviceFirmwareBuildId READ deviceFirmwareBuildId NOTIFY stateChanged) + Q_PROPERTY(QString firmwareUpdateResult READ firmwareUpdateResult NOTIFY stateChanged) Q_PROPERTY(qulonglong sessionGeneration READ sessionGeneration NOTIFY stateChanged) Q_PROPERTY(PortOwner portOwner READ portOwner NOTIFY stateChanged) @@ -205,6 +206,8 @@ class SmartSessionController : public QObject { Q_INVOKABLE bool retryFirmwareSetup(); Q_INVOKABLE bool retryIdentification(); Q_INVOKABLE bool retryProfileSync(); + Q_INVOKABLE QString diagnosticsText() const; + [[nodiscard]] QString firmwareUpdateResult() const { return lastFirmwareUpdateResult_; } signals: void dependenciesChanged(); @@ -380,6 +383,7 @@ class SmartSessionController : public QObject { QString updatePort_; QString blankBoardPort_; QString setupErrorStatus_; + QString lastFirmwareUpdateResult_; QString profileSyncBaselineGeneration_; QString profileSyncBootId_; QString profileSyncError_; diff --git a/apps/arstack_studio/src/StudioClipboard.cpp b/apps/arstack_studio/src/StudioClipboard.cpp new file mode 100644 index 000000000..ee2e3c2fd --- /dev/null +++ b/apps/arstack_studio/src/StudioClipboard.cpp @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "StudioClipboard.hpp" + +#include +#include + +void StudioClipboard::copyText(const QString& text) const { + if (auto* clipboard = QGuiApplication::clipboard(); clipboard != nullptr) + clipboard->setText(text); +} diff --git a/apps/arstack_studio/src/StudioClipboard.hpp b/apps/arstack_studio/src/StudioClipboard.hpp new file mode 100644 index 000000000..35c105ccb --- /dev/null +++ b/apps/arstack_studio/src/StudioClipboard.hpp @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include + +// GUI-only clipboard adapter. The shared session supervisor stays QtCore-only. +class StudioClipboard final : public QObject { + Q_OBJECT +public: + using QObject::QObject; + Q_INVOKABLE void copyText(const QString& text) const; +}; diff --git a/apps/arstack_studio/src/main.cpp b/apps/arstack_studio/src/main.cpp index 14e3389c5..f208cd705 100644 --- a/apps/arstack_studio/src/main.cpp +++ b/apps/arstack_studio/src/main.cpp @@ -5,6 +5,7 @@ #include "SclProfileModel.hpp" #include "SingleInstanceGuard.hpp" #include "SmartSessionController.hpp" +#include "StudioClipboard.hpp" #include "StudioDeviceController.hpp" #include @@ -462,6 +463,8 @@ int main(int argc, char* argv[]) { qmlRegisterType("ARStack.Studio", 1, 0, "DeviceController"); qmlRegisterType("ARStack.Studio", 1, 0, "SmartSessionController"); qmlRegisterSingletonInstance("ARStack.Studio", 1, 0, "FirmwareService", &firmwareService); + StudioClipboard clipboard; + qmlRegisterSingletonInstance("ARStack.Studio", 1, 0, "StudioClipboard", &clipboard); QQmlApplicationEngine engine; QObject::connect(