Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
70da762
fix(ptp): hold one bounded physical runtime recovery intent through v…
masarray Oct 20, 2026
9876f7a
fix(ptp): recover stale board RUNNING only after STOP ACK/readback, e…
masarray Oct 20, 2026
89e8600
fix(studio): clipboard adapter verifies actual OS readback
masarray Oct 20, 2026
a948eb4
fix(studio): do not report success when Windows clipboard write fails
masarray Oct 20, 2026
d0e3057
ux(studio): in-app selectable diagnostic fallback and truthful copy s…
masarray Oct 20, 2026
404bcd7
fix(studio): do not silently claim firmware diagnostics copied
masarray Oct 20, 2026
f200fb5
fix(studio): explicitly import registered StudioClipboard singleton i…
masarray Oct 20, 2026
701e09b
ux(studio): show truthful firmware clipboard result adjacent to Copy …
masarray Oct 20, 2026
9e56bb4
test(ptp): cover physical stale role recovery, bounded retries, STOP …
masarray Oct 20, 2026
a3caf34
test(studio): verify native Qt clipboard roundtrip and refuse false s…
masarray Oct 20, 2026
9ea3f12
test(studio): add native clipboard roundtrip harness using production…
masarray Oct 20, 2026
4887471
ci(studio): exercise actual GUI clipboard roundtrip on Windows and Linux
masarray Oct 20, 2026
0ca8106
ci(studio): require native clipboard roundtrip before packaging match…
masarray Oct 20, 2026
8676f03
fix(ptp): second stale-runtime rejection is terminal STOP-UNVERIFIED,…
masarray Oct 20, 2026
97a7876
test(ptp): second stale RUNNING rejection locks into manual STOP reco…
masarray Oct 20, 2026
799b664
test(studio): enforce shared PTP recovery and real clipboard success/…
masarray Oct 20, 2026
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
4 changes: 3 additions & 1 deletion .github/workflows/arstack-studio-native-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,12 @@ jobs:
run: |
cmake -S apps/arstack_studio -B build-studio-preview -G "Visual Studio 17 2022" -A x64 -DARIEC61850_WARNINGS_AS_ERRORS=ON -DARSTACK_STUDIO_ENGINEERING_PREVIEW=ON
if ($LASTEXITCODE -ne 0) { throw 'Preview configuration failed' }
cmake --build build-studio-preview --config Release --target arstack_studio arstack_session_harness --parallel 2
cmake --build build-studio-preview --config Release --target arstack_studio arstack_session_harness arstack_clipboard_harness --parallel 2
if ($LASTEXITCODE -ne 0) { throw 'Native Studio preview build failed' }
& "$PWD\build-studio-preview\Release\arstack_session_harness.exe"
if ($LASTEXITCODE -ne 0) { throw 'Native session regression failed' }
& "$PWD\build-studio-preview\Release\arstack_clipboard_harness.exe"
if ($LASTEXITCODE -ne 0) { throw 'Native clipboard readback regression failed' }
# Differentiate product code regressions from missing deployed DLLs.
$nativeGui = Join-Path $PWD 'build-studio-preview\Release\arstack_studio.exe'
$probe = Start-Process -FilePath $nativeGui -ArgumentList '--check-reference-template' -Wait -PassThru
Expand Down
22 changes: 20 additions & 2 deletions .github/workflows/arstack-studio-qt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ jobs:
shell: powershell
run: |
cmake --build build-arstack-studio --config Release `
--target arstack_session_harness arstack_windows_ownership_harness --parallel
--target arstack_session_harness arstack_clipboard_harness arstack_windows_ownership_harness --parallel

- name: S8A deterministic session harness on Windows
timeout-minutes: 1
Expand All @@ -98,6 +98,16 @@ jobs:
throw "S8A Windows deterministic harness failed with exit code $exitCode."
}

- name: Native Windows clipboard roundtrip regression
timeout-minutes: 1
shell: powershell
run: |
$exe = Join-Path $PWD 'build-arstack-studio\Release\arstack_clipboard_harness.exe'
& $exe
if ($LASTEXITCODE -ne 0) {
throw "Native Windows OS clipboard readback failed: exit $LASTEXITCODE"
}

- name: S8B cross-process ownership and serial-loss harness
timeout-minutes: 1
shell: powershell
Expand Down Expand Up @@ -216,7 +226,7 @@ jobs:
run: |
set -o pipefail
cmake --build build-arstack-studio \
--target arstack_studio arstack_session_harness arstack_capture_harness --parallel 2 \
--target arstack_studio arstack_session_harness arstack_capture_harness arstack_clipboard_harness --parallel 2 \
2>&1 | tee /tmp/arstack-studio-build.log

- name: 4I+4V reference template regression
Expand All @@ -240,6 +250,14 @@ jobs:
timeout 20s ./build-arstack-studio/arstack_session_harness \
2>&1 | tee /tmp/arstack-studio-session-harness.log

- name: Native GUI clipboard content/readback regression
shell: bash
run: |
set -o pipefail
timeout 15s env QT_QPA_PLATFORM=offscreen \
./build-arstack-studio/arstack_clipboard_harness \
2>&1 | tee /tmp/arstack-studio-clipboard-harness.log

- name: Native SV PCAP capture and CSV decoder regression
shell: bash
run: |
Expand Down
10 changes: 10 additions & 0 deletions apps/arstack_studio/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ qt_add_executable(arstack_session_harness
)

# Offline production wire decoder regression (no Npcap or hardware required).
# Isolated platform GUI clipboard verification (same source linked in Studio).
# No QML stubs, no fake service: tests actual QGuiApplication OS clipboard.
qt_add_executable(arstack_clipboard_harness
src/ClipboardHarness.cpp
src/StudioClipboard.cpp
src/StudioClipboard.hpp
)
target_link_libraries(arstack_clipboard_harness PRIVATE Qt6::Core Qt6::Gui)
target_compile_features(arstack_clipboard_harness PRIVATE cxx_std_20)

qt_add_executable(arstack_capture_harness
src/SvCaptureHarness.cpp
src/SvCaptureController.cpp
Expand Down
17 changes: 16 additions & 1 deletion apps/arstack_studio/qml/FirmwarePanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ SurfacePanel {
property string uiFont: "Inter"
property string monoFont: "Inter"
property bool technicalDetailsVisible: false
property string copyFeedback: ""
readonly property string firmwareDisplayStatus:
panel.session && panel.session.firmwareUpdateResult.length > 0 &&
!panel.session.updatingFirmware
Expand Down Expand Up @@ -298,7 +299,21 @@ SurfacePanel {
text: "Copy diagnostics"
toolTipText: "Logs may include device identifiers; review before sharing."
enabled: panel.session !== null
onClicked: StudioClipboard.copyText(panel.session.diagnosticsText())
onClicked: {
const evidence = panel.session.diagnosticsText()
if (StudioClipboard.copyText(evidence)) {
panel.copyFeedback = "Firmware diagnostics copied."
} else {
panel.copyFeedback = "Clipboard unavailable. Open technical details to review diagnostics."

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 Expose the requested diagnostics when clipboard copy fails

If clipboard access fails, this message directs the operator to technical details, but that view displays only firmware.logText; the value that failed to copy is session.diagnosticsText(), which also contains session state, device identity and telemetry, PTP/SV evidence, and the device serial log. In the failure scenario the requested evidence therefore remains unavailable from this panel despite the recovery instruction. Show the captured evidence in a selectable fallback, as the Time Sync panel does, or direct the user to a view containing the same text.

Useful? React with 👍 / 👎.

}
}
}
Label {
visible: panel.copyFeedback.length > 0
text: panel.copyFeedback
color: panel.theme.textSoft
font.family: panel.uiFont
font.pixelSize: 9
}
Item { Layout.fillWidth: true }
}
Expand Down
42 changes: 40 additions & 2 deletions apps/arstack_studio/qml/PtpExpertPanel.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 @@ -12,6 +13,7 @@ SurfacePanel {
property string monoFont: "Inter"
property var roleValues: ["SOURCE", "RECEIVER", "MONITOR"]
property bool advancedVisible: false
property string diagnosticFallbackText: ""
readonly property bool startPending: panel.device && panel.device.ptpStarting

function roleIndex(role) {
Expand Down Expand Up @@ -98,6 +100,30 @@ SurfacePanel {
onTriggered: if (panel.session) panel.session.requestPtpRefresh()
}

Dialog {
id: diagnosticFallback
parent: Overlay.overlay
anchors.centerIn: Overlay.overlay
modal: true
title: "PTP diagnostics · select and copy"
standardButtons: Dialog.Close
width: Math.min(760, Overlay.overlay.width - 32)
height: Math.min(480, Overlay.overlay.height - 32)
contentItem: ScrollView {
clip: true
TextArea {
id: diagnosticFallbackEditor
readOnly: true
selectByMouse: true
wrapMode: TextEdit.NoWrap
font.family: panel.monoFont
font.pixelSize: 11
text: panel.diagnosticFallbackText
}
}
onOpened: diagnosticFallbackEditor.selectAll()
}

ScrollView {
anchors.fill: parent
contentWidth: availableWidth
Expand Down Expand Up @@ -333,8 +359,20 @@ SurfacePanel {
text: "Copy diagnostics"
enabled: panel.session && panel.device.deviceVerified
onClicked: {
StudioClipboard.copyText(panel.session.diagnosticsText())
if (panel.controller) panel.controller.showMessage("PTP and firmware diagnostics copied.", false)
const evidence = panel.session.diagnosticsText()
if (StudioClipboard.copyText(evidence)) {
if (panel.controller)
panel.controller.showMessage("PTP diagnostics copied to clipboard.", false)
} else {
// Windows may temporarily deny clipboard ownership.
// Keep the complete text selectable in Studio instead
// of claiming success or asking for external scripts.
panel.diagnosticFallbackText = evidence
diagnosticFallback.open()
if (panel.controller)
panel.controller.showMessage(
"Clipboard unavailable. Diagnostic text is open: select and copy it.", true)
}
}
}
Item { Layout.fillWidth: true }
Expand Down
34 changes: 34 additions & 0 deletions apps/arstack_studio/src/ClipboardHarness.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Native GUI clipboard contract: exercise the SAME production adapter as Studio.
#include "StudioClipboard.hpp"

#include <QClipboard>
#include <QGuiApplication>
#include <QString>

int main(int argc, char** argv) {
QGuiApplication app(argc, argv);
StudioClipboard clipboard;
auto* osClipboard = QGuiApplication::clipboard();
if (osClipboard == nullptr) return 2;

if (clipboard.copyText(QString{})) return 3; // no fabricated success
constexpr auto kHeader = "ARStack Studio field diagnostics";
const QString evidence = QString::fromUtf8(
"ARStack Studio field diagnostics\n"
"Installed firmware: df73fea0b68e7f47\n"
"PTP START error: PTPROLE rejected: stop PTP before changing operating role\n"
"SVSYNC event: ETHERNET_LINK_LOSS\n");
if (!evidence.startsWith(QString::fromLatin1(kHeader))) return 4;
if (!clipboard.copyText(evidence)) return 5;
if (osClipboard->text(QClipboard::Clipboard) != evidence) return 6;

const QString second = QString::fromUtf8(
"PTP diagnostic: SOURCE running\n"
"MAC phase ±2 us\n"
"SVSYNC ACQUIRED\n");
if (!clipboard.copyText(second)) return 7;
if (osClipboard->text(QClipboard::Clipboard) != second) return 8;
if (osClipboard->text(QClipboard::Clipboard) == evidence) return 9;
return 0;
}
92 changes: 92 additions & 0 deletions apps/arstack_studio/src/DeterministicSessionHarness.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ class DeterministicSessionHarness final {
{"PTP source rejection survives verified safety STOP", ptpSourceReasonSurvivesSafetyStop()},
{"PTP rejected config never starts", ptpGuidedRejectedConfigStops()},
{"PTP STOP cancels pending start after verified readback", ptpGuidedStopCancelsStart()},
{"PTP stale board role rejection triggers one verified recovery", ptpStaleRuntimeOneRecovery()},
{"PTP stale runtime recovery cannot loop on second rejection", ptpStaleRuntimeNoRetryLoop()},
{"PTP runtime recovery requires STOPPED evidence", ptpStaleRuntimeStopUnverified()},
{"PTP user STOP cancels automatic recovery intent", ptpStaleRuntimeStopCancelsRecovery()},
{"legacy bare SV START refuses stale eight-channel state", legacyBareStartFailsClosed()},
{"SV commit ACK/readback START chain is generation bound", svTransactionAckChain()},
{"SV stale ACK and mismatched firmware readback fail closed", svTransactionNegativeAcks()},
Expand Down Expand Up @@ -820,6 +824,94 @@ class DeterministicSessionHarness final {
"PTP status=STOPPED Announce=0 Sync=0 FollowUp=0 PdelayFrames=0 TXfail=0");
}

static bool ptpStaleRuntimeOneRecovery() {
Fixture f;
if (!setupGuidedPtp(f, QStringLiteral("SOURCE"))) return false;
auto& d = f.device;
// Board was still transmitting, but desktop had cached OFF.
d.processLine(QStringLiteral(
"E (246315) ar_smv_profile: PTPROLE rejected: stop PTP before changing operating role"));
if (!d.ptpStarting() || !d.ptpRoleRecoveryAttempted_ ||
!d.ptpRestartAfterStop_ ||
d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_stop_ack)
return false;
// Unrelated role ACK and pre-STOP telemetry must never advance.
d.processLine(QStringLiteral("PTPROLE accepted"));
d.processLine(QStringLiteral(
"PTP status=RUNNING Announce=61 Sync=61 FollowUp=61 PdelayFrames=0 TXfail=1"));
if (d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_stop_ack)
return false;
d.processLine(QStringLiteral("PTP stop requested"));
if (d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_stop_readback)
return false;
d.processLine(ptpStoppedStatus());
if (d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_role_ack ||
d.ptpRestartAfterStop_ || !d.ptpRoleRecoveryAttempted_)
return false;
d.processLine(QStringLiteral("PTPROLE accepted"));
d.processLine(QStringLiteral("PTPROLE role=SOURCE value=0 running=0"));
d.processLine(QStringLiteral("PTP configuration accepted"));
d.processLine(QStringLiteral("PTP start accepted"));
d.processLine(ptpRoleEvidence(QStringLiteral("SOURCE")));
d.processLine(QStringLiteral(
"PTP status=RUNNING Announce=1 Sync=1 FollowUp=1 PdelayFrames=0 TXfail=0"));
return !d.ptpStarting() && d.ptpRunning_ &&
d.ptpRole_ == QStringLiteral("SOURCE") &&
!d.ptpRoleRecoveryAttempted_;
}

static bool ptpStaleRuntimeNoRetryLoop() {
Fixture f;
if (!setupGuidedPtp(f, QStringLiteral("SOURCE"))) return false;
auto& d = f.device;
const QString stale = QStringLiteral(
"PTPROLE rejected: stop PTP before changing operating role");
d.processLine(stale);
d.processLine(QStringLiteral("PTP stop requested"));
d.processLine(ptpStoppedStatus());
if (d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_role_ack)
return false;
d.processLine(stale);
return d.ptpStarting() && !d.ptpRestartAfterStop_ &&
d.ptpStartStage_ == DeviceController::PtpStartStage::stop_unconfirmed &&
d.ptpLastStartFailure_.contains(QStringLiteral("manual STOP")) &&
!f.session.requestStartPtpConfigured(
QStringLiteral("SOURCE"), ptpValidProfile());
}

static bool ptpStaleRuntimeStopUnverified() {
Fixture f;
if (!setupGuidedPtp(f, QStringLiteral("SOURCE"))) return false;
auto& d = f.device;
d.processLine(QStringLiteral(
"PTPROLE rejected: stop PTP before changing operating role"));
d.processLine(QStringLiteral("PTP stop requested"));
// Timeout is deterministic; no fake STOPPED readback accepted.
if (!QMetaObject::invokeMethod(
&d.ptpStartTimer_, "timeout", Qt::DirectConnection)) return false;
if (d.ptpStartStage_ != DeviceController::PtpStartStage::stop_unconfirmed ||
!d.ptpStarting() || !d.ptpRestartAfterStop_ ||
f.session.requestStartPtpConfigured(QStringLiteral("SOURCE"), ptpValidProfile()))
return false;
return d.ptpStatus_.contains(QStringLiteral("unverified"));
}

static bool ptpStaleRuntimeStopCancelsRecovery() {
Fixture f;
if (!setupGuidedPtp(f, QStringLiteral("SOURCE"))) return false;
auto& d = f.device;
d.processLine(QStringLiteral(
"PTPROLE rejected: stop PTP before changing operating role"));
if (!d.ptpRestartAfterStop_) return false;
if (!f.session.requestStopPtp() || d.ptpRestartAfterStop_) return false;
d.processLine(QStringLiteral("PTP stop requested"));
d.processLine(ptpStoppedStatus());
d.processLine(QStringLiteral("PTPROLE accepted"));
return !d.ptpStarting() &&
d.ptpStartStage_ == DeviceController::PtpStartStage::idle &&
d.ptpStatus_.contains(QStringLiteral("confirmed by board"));
}

static bool ptpGuidedStopCancelsStart() {
Fixture f;
if (!setupGuidedPtp(f, QStringLiteral("SOURCE"))) return false;
Expand Down
Loading
Loading