Skip to content
Merged
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
2 changes: 2 additions & 0 deletions apps/arstack_studio/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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}
)

Expand Down
29 changes: 22 additions & 7 deletions apps/arstack_studio/qml/FirmwarePanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ARStack.Studio 1.0

SurfacePanel {
id: panel
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions apps/arstack_studio/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions apps/arstack_studio/qml/WorkflowBar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions apps/arstack_studio/src/DeterministicSessionHarness.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()},
Expand Down Expand Up @@ -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;
{
Expand Down
77 changes: 77 additions & 0 deletions apps/arstack_studio/src/SmartSessionController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "SclProfileModel.hpp"

#include <QVariantMap>
#include <QStringList>

#include <algorithm>
#include <limits>
Expand Down Expand Up @@ -478,6 +479,7 @@ bool SmartSessionController::beginFirmwareOperation(const QString& portName) {
resetProfileSync(true);
setupError_ = false;
setupErrorStatus_.clear();
lastFirmwareUpdateResult_.clear();

if (device_->running()) {
updateStage_ = UpdateStage::stopping;
Expand All @@ -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;
Expand Down Expand Up @@ -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_;
Expand All @@ -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.")
Expand Down Expand Up @@ -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);
}
Expand Down
4 changes: 4 additions & 0 deletions apps/arstack_studio/src/SmartSessionController.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -380,6 +383,7 @@ class SmartSessionController : public QObject {
QString updatePort_;
QString blankBoardPort_;
QString setupErrorStatus_;
QString lastFirmwareUpdateResult_;
QString profileSyncBaselineGeneration_;
QString profileSyncBootId_;
QString profileSyncError_;
Expand Down
10 changes: 10 additions & 0 deletions apps/arstack_studio/src/StudioClipboard.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "StudioClipboard.hpp"

#include <QClipboard>
#include <QGuiApplication>

void StudioClipboard::copyText(const QString& text) const {
if (auto* clipboard = QGuiApplication::clipboard(); clipboard != nullptr)
clipboard->setText(text);
}
13 changes: 13 additions & 0 deletions apps/arstack_studio/src/StudioClipboard.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

#include <QObject>
#include <QString>

// 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;
};
3 changes: 3 additions & 0 deletions apps/arstack_studio/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "SclProfileModel.hpp"
#include "SingleInstanceGuard.hpp"
#include "SmartSessionController.hpp"
#include "StudioClipboard.hpp"
#include "StudioDeviceController.hpp"

#include <QCloseEvent>
Expand Down Expand Up @@ -462,6 +463,8 @@ int main(int argc, char* argv[]) {
qmlRegisterType<StudioDeviceController>("ARStack.Studio", 1, 0, "DeviceController");
qmlRegisterType<SmartSessionController>("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(
Expand Down
Loading