Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cbd69e7
fix(studio): show exact bundled/installed firmware build IDs and dist…
masarray Oct 20, 2026
89762ee
feat(studio): expose exact PTP failure reason and independent runtime…
masarray Oct 20, 2026
df4dc79
fix(ptp-studio): retain firmware failure cause, accept RUNNING and PT…
masarray Oct 20, 2026
0677b53
fix(studio): show specific PTP startup failure only when needed, with…
masarray Oct 20, 2026
d49c536
fix(ptp): preserve exact failed START transaction stage across rollba…
masarray Oct 20, 2026
708635e
fix(studio): name exact bundled reinstall clearly and avoid stale boo…
masarray Oct 20, 2026
c9725e7
test(studio): enforce visible firmware build match and stage-specific…
masarray Oct 20, 2026
065422e
feat(ptp-fw): distinguish failed START from physical PHY link state w…
masarray Oct 20, 2026
095bc26
fix(studio): prioritize measured Ethernet PHY failure evidence over g…
masarray Oct 20, 2026
716aa39
feat(studio): include precise PTP failure stage in single-click Copy …
masarray Oct 20, 2026
598e2ef
test(studio): require actual PHY failure diagnosis and PTP reason in …
masarray Oct 20, 2026
fc2fdb2
feat(eth): expose canonical Ethernet event link status for PTP failur…
masarray Oct 20, 2026
8787af1
fix(ptp-fw): diagnose START from shared event link state, not unavail…
masarray Oct 20, 2026
9026564
feat(ptp-fw): export existing FreeRTOS ETH_EVENT link bit read-only w…
masarray Oct 20, 2026
cbc6945
test(ptp): enforce single-source-of-truth ETH_EVENT link check; no no…
masarray Oct 20, 2026
7f3751b
feat(ptp-source): report live Announce/Sync/FollowUp and TXfail count…
masarray Oct 20, 2026
08ec53b
fix(studio): retain PHY-DOWN or actual PTP source TX counters as fail…
masarray Oct 20, 2026
52dad63
test(ptp): assert finite failure-only SOURCE TX counters captured in …
masarray Oct 20, 2026
a6ce09b
fix(ptp): retain measured SOURCE TX counters and PHY-DOWN evidence ac…
masarray Oct 20, 2026
c10f245
docs(ptp): document event-owner link read without referencing unavail…
masarray Oct 20, 2026
07efb16
test(ptp): prove out-of-order PTP2/runtime acceptance and timeout fai…
masarray Oct 20, 2026
11bf49c
fix(studio): label verified USB-board readiness separately from PTP t…
masarray Oct 20, 2026
8d50911
fix(studio): label verified USB-board readiness separately from PTP t…
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
2 changes: 1 addition & 1 deletion apps/arstack_studio/qml/ConfigurationHub.qml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Item {
color: hub.device.deviceVerified ? hub.theme.green : hub.theme.amber
}
Label {
text: hub.device.deviceVerified ? "Device ready" : "Setup mode"
text: hub.device.deviceVerified ? "Board verified" : "Setup mode"
color: hub.device.deviceVerified ? hub.theme.green : hub.theme.amber
font.family: hub.uiFont
font.pixelSize: 10
Expand Down
40 changes: 33 additions & 7 deletions apps/arstack_studio/qml/FirmwarePanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ SurfacePanel {
!panel.session.firmwareUpdateResult.startsWith("Exact bundled firmware build verified")

readonly property bool hasPort: firmwarePort.currentText.length > 0
readonly property string bundledBuild: panel.session
? String(panel.session.expectedFirmwareBuildId || "") : ""
readonly property string installedBuild: panel.device.deviceVerified
? String(panel.device.firmwareBuildId || "") : ""
readonly property bool exactBuildMatch: panel.device.deviceVerified
&& panel.bundledBuild.length === 16 && panel.installedBuild.length === 16
&& panel.bundledBuild.toLowerCase() === panel.installedBuild.toLowerCase()

component StepBadge: Rectangle {
property string stepText: "1"
Expand Down Expand Up @@ -77,7 +84,7 @@ SurfacePanel {

Rectangle {
Layout.fillWidth: true
implicitHeight: 46
implicitHeight: 60
radius: 8
color: panel.theme.surface2
border.width: 1
Expand All @@ -99,21 +106,31 @@ SurfacePanel {
Label {
Layout.fillWidth: true
text: panel.firmware.bundleReady
? "Bundled firmware v" + panel.firmware.firmwareVersion + " · ready"
? "Bundled firmware v" + panel.firmware.firmwareVersion
: "Firmware package unavailable"
elide: Text.ElideRight
color: panel.theme.text
font.family: panel.uiFont
font.pixelSize: 11
font.weight: Font.DemiBold
}
Label {
Layout.fillWidth: true
text: panel.bundledBuild.length
? "Build " + panel.bundledBuild + " · included in Studio"
: "Build identity unavailable — installation not verifiable"
color: panel.bundledBuild.length ? panel.theme.textSoft : panel.theme.amber
font.family: panel.monoFont
font.pixelSize: 9
elide: Text.ElideRight
}
}
}
}

Rectangle {
Layout.fillWidth: true
implicitHeight: 112
implicitHeight: 132
radius: 9
color: panel.theme.surface2
border.width: 1
Expand Down Expand Up @@ -151,6 +168,16 @@ SurfacePanel {
font.pixelSize: 10
wrapMode: Text.WordWrap
}
Label {
Layout.fillWidth: true
text: !panel.device.deviceVerified ? "Installed build not verified"
: "Installed build " + (panel.installedBuild || "UNKNOWN")
+ (panel.exactBuildMatch ? " · MATCH" : " · MISMATCH / UNKNOWN")
color: panel.exactBuildMatch ? panel.theme.green : panel.theme.amber
font.family: panel.monoFont
font.pixelSize: 9
elide: Text.ElideRight
}

RowLayout {
Layout.fillWidth: true
Expand Down Expand Up @@ -192,8 +219,7 @@ SurfacePanel {

StepBadge {
stepText: "2"
complete: panel.device.deviceVerified &&
panel.device.protocolVersion === panel.firmware.expectedProtocol
complete: panel.exactBuildMatch
Layout.alignment: Qt.AlignTop
}

Expand Down Expand Up @@ -228,7 +254,7 @@ SurfacePanel {
implicitWidth: 172
text: panel.session && (panel.session.firmwareUpdateRequired || panel.session.firmwareUpdateAvailable)
? (panel.session.firmwareUpdateRequired ? "Update firmware" : "Install bundled build")
: "Reinstall current"
: "Reinstall bundled"
visible: panel.session &&
(panel.session.firmwareUpdateRequired || panel.session.firmwareUpdateAvailable || panel.session.firmwareReinstallAvailable)
enabled: visible && !panel.firmware.busy
Expand Down Expand Up @@ -302,7 +328,7 @@ SurfacePanel {
Label { text: "Protocol"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 8 }
Label { text: panel.firmware.expectedProtocol === "-" ? "-" : "v" + panel.firmware.expectedProtocol; color: panel.theme.text; font.family: panel.uiFont; font.pixelSize: 9 }
Label { text: "Target"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 8 }
Label { text: panel.firmware.targetChip; color: panel.theme.textSoft; font.family: panel.uiFont; font.pixelSize: 9 }
Label { text: panel.device.deviceVerified ? panel.device.deviceTarget : (panel.firmware.bundleReady ? "ESP32-P4 bundle" : "Unverified"); color: panel.theme.textSoft; font.family: panel.uiFont; font.pixelSize: 9 }
Label { text: "SHA-256"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 8 }
Label { Layout.columnSpan: 3; text: panel.firmware.firmwareSha256.length ? panel.firmware.firmwareSha256 : "-"; color: panel.theme.textSoft; font.family: panel.monoFont; font.pixelSize: 8; elide: Text.ElideMiddle }
}
Expand Down
2 changes: 1 addition & 1 deletion apps/arstack_studio/qml/ModernRibbon.qml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Rectangle {
function conciseState() {
if (!device.deviceVerified && controller.offlinePreviewRunning) return "Offline preview"
if (session.state === "RUNNING") return "Running"
if (session.state === "READY") return "Ready"
if (session.state === "READY") return "Board ready"
if (session.state === "WAITING FOR DEVICE") return "Offline"
if (session.state === "DEVICE FOUND" || session.state === "CONNECTING" || session.state === "CHECKING DEVICE") return "Connecting"
if (session.state === "FIRMWARE RECOVERY") return "Firmware status unknown"
Expand Down
20 changes: 19 additions & 1 deletion apps/arstack_studio/qml/PtpExpertPanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ SurfacePanel {
return false
const accepted = panel.session.requestStartPtpConfigured(role, panel.ptpProfile())
if (!accepted && panel.controller)
panel.controller.showMessage("PTP start blocked. Check timing diagnostics.", true)
panel.controller.showMessage(
panel.device.lastError && panel.device.lastError.length
? panel.device.lastError
: "PTP START not queued; check board identity and firmware state.", true)
return Boolean(accepted)
}

Expand Down Expand Up @@ -149,6 +152,21 @@ SurfacePanel {
}
}

Label {
Layout.fillWidth: true
visible: Boolean(panel.device.ptpFailureReason) &&
panel.device.ptpFailureReason.length > 0 &&
!panel.device.ptpRunning
text: "PTP start unsuccessful · " + panel.device.ptpFailureReason
color: panel.theme.red
font.family: panel.uiFont
font.pixelSize: 10
wrapMode: Text.WordWrap
maximumLineCount: 3
elide: Text.ElideRight
ToolTip.visible: false
}

Rectangle {
Layout.fillWidth: true
implicitHeight: 82
Expand Down
33 changes: 28 additions & 5 deletions apps/arstack_studio/src/DeterministicSessionHarness.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,10 @@ class DeterministicSessionHarness final {
}

static bool ptpRuntimeRoleEvidenceRequired() {
// The PTP2 role and PTP STATUS RUNNING are asynchronous observations.
// A positive STATUS arriving before matching PTP2 must wait, not fail
// immediately. Conversely missing/wrong role MUST fail at the original
// finite deadline and follow STOP verification.
Fixture f;
if (!setupGuidedPtp(f, QStringLiteral("SOURCE"))) return false;
auto& d = f.device;
Expand All @@ -664,14 +668,26 @@ class DeterministicSessionHarness final {
d.processLine(QStringLiteral("PTP configuration accepted"));
d.processLine(QStringLiteral("PTP start accepted"));
if (d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_runtime) return false;

d.processLine(QStringLiteral(
"PTP status=RUNNING Announce=1 Sync=1 FollowUp=1 PdelayFrames=0 TXfail=0"));
// Missing PTP2 evidence after START must trigger a verified rollback,
// not silently clear state while the board might still transmit.
if (d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_stop_ack ||
!d.ptpStarting() ||
// No success and no false rollback from early STATUS; competing
// requests cannot enter the active START transaction.
if (d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_runtime ||
!d.ptpStarting() || !d.ptpRuntimeRunningSeen_ ||
d.ptpRuntimeRoleSeen_ ||
f.session.requestStartPtpConfigured(QStringLiteral("SOURCE"), ptpValidProfile()) ||
d.startPtp() || d.setPtpRole(QStringLiteral("MONITOR"))) return false;

d.processLine(ptpRoleEvidence(QStringLiteral("SOURCE")));
if (d.ptpStarting() || !d.ptpRunning_ ||
!d.ptpStatus_.contains(QStringLiteral("external timing still unqualified")) ||
!d.ptpFailureReason_.isEmpty()) return false;

// A new START with an old/incorrect role must remain pending, and a
// STOPPED response cannot certify RUNNING. On timeout we must roll
// back and independently verify STOP, never mark a false PTP PASS.
if (!f.session.requestStopPtp()) return false;
d.processLine(QStringLiteral("PTP stop requested"));
d.processLine(ptpStoppedStatus());
if (d.ptpStarting() || d.ptpRunning_) return false;
Expand All @@ -682,8 +698,15 @@ class DeterministicSessionHarness final {
d.processLine(QStringLiteral("PTP configuration accepted"));
d.processLine(QStringLiteral("PTP start accepted"));
d.processLine(ptpRoleEvidence(QStringLiteral("RECEIVER")));
if (d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_stop_ack ||
d.processLine(ptpStoppedStatus());
if (d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_runtime ||
!d.ptpStarting()) return false;

if (!QMetaObject::invokeMethod(&d.ptpStartTimer_, "timeout",
Qt::DirectConnection)) return false;
if (d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_stop_ack ||
!d.ptpStarting() ||
!d.ptpFailureReason_.contains(QStringLiteral("PTP runtime readback"))) return false;
d.processLine(QStringLiteral("PTP stop requested"));
d.processLine(ptpStoppedStatus());
return !d.ptpStarting() && !d.ptpRunning_ &&
Expand Down
72 changes: 53 additions & 19 deletions apps/arstack_studio/src/DeviceController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,7 @@ void DeviceController::resetPtpStartup() noexcept {
pendingPtpRole_.clear();
pendingPtpConfig_.clear();
ptpRuntimeRoleSeen_ = false;
ptpRuntimeRunningSeen_ = false;
}

bool DeviceController::beginPtpStopVerification() {
Expand Down Expand Up @@ -1342,7 +1343,28 @@ void DeviceController::markPtpStopUnconfirmed(const QString& reason) {
}

void DeviceController::failPtpStartup(const QString& reason, const bool rollback) {
setError(reason);
// Attach the stage BEFORE reset/STOP rollback destroys this information.
QString stage;
switch (ptpStartStage_) {
case PtpStartStage::awaiting_role_ack: stage = QStringLiteral("PTP role ACK"); break;
case PtpStartStage::awaiting_role_readback: stage = QStringLiteral("PTP role readback"); break;
case PtpStartStage::awaiting_config: stage = QStringLiteral("PTP settings"); break;
case PtpStartStage::awaiting_start: stage = QStringLiteral("PTP START ACK"); break;
case PtpStartStage::awaiting_runtime: stage = QStringLiteral("PTP runtime readback"); break;
case PtpStartStage::awaiting_stop_ack: stage = QStringLiteral("PTP STOP ACK"); break;
case PtpStartStage::awaiting_stop_readback: stage = QStringLiteral("PTP STOP readback"); break;
case PtpStartStage::stop_unconfirmed: stage = QStringLiteral("PTP STOP unverified"); break;
case PtpStartStage::idle: stage = QStringLiteral("PTP startup"); break;
}
// If the board already reported measured SOURCE counters or a PHY-DOWN
// condition, retain that more useful observation across its subsequent
// generic failure/rollback log.
const bool hasDetailedEvidence =
ptpFailureReason_.contains(QStringLiteral("Announce=")) ||
ptpFailureReason_.contains(QStringLiteral("Ethernet PHY link DOWN"));
const QString observedCause = hasDetailedEvidence ? ptpFailureReason_ : reason;
ptpFailureReason_ = QStringLiteral("%1: %2").arg(stage, observedCause).left(300);
setError(ptpFailureReason_);
if (rollback && connected_ && sessionGeneration_ != 0) {
static_cast<void>(beginPtpStopVerification());
return;
Expand All @@ -1369,6 +1391,7 @@ bool DeviceController::startPtpConfigured(const QString& requestedRole,
setError(QStringLiteral("Invalid PTP settings. Check domain, VLAN and timing intervals."));
return false;
}
ptpFailureReason_.clear();
pendingPtpRole_ = role;
pendingPtpConfig_ = command;
ptpStartStage_ = PtpStartStage::awaiting_role_ack;
Expand Down Expand Up @@ -1423,7 +1446,8 @@ void DeviceController::advancePtpStartup(const QString& line) {
if (rejected) {
const bool rollback = ptpStartStage_ == PtpStartStage::awaiting_start ||
ptpStartStage_ == PtpStartStage::awaiting_runtime;
failPtpStartup(QStringLiteral("Board rejected PTP mode, settings or start."), rollback);
failPtpStartup(QStringLiteral("Board PTP rejection: %1").arg(
line.mid(line.indexOf(QStringLiteral("PTP"))).left(200)), rollback);
return;
}
if (ptpStartStage_ == PtpStartStage::awaiting_role_ack) {
Expand Down Expand Up @@ -1469,6 +1493,7 @@ void DeviceController::advancePtpStartup(const QString& line) {
line.contains(QStringLiteral("PTP start accepted"), Qt::CaseInsensitive)) {
ptpStartStage_ = PtpStartStage::awaiting_runtime;
ptpRuntimeRoleSeen_ = false;
ptpRuntimeRunningSeen_ = false;
ptpStatus_ = QStringLiteral("Verifying live PTP role and runtime...");
emit ptpStateChanged();
if (!sendPtpShow()) {
Expand All @@ -1477,22 +1502,22 @@ void DeviceController::advancePtpStartup(const QString& line) {
}
ptpStartTimer_.start(5000);
} else if (ptpStartStage_ == PtpStartStage::awaiting_runtime) {
// PTP2 and PTP STATUS arrive independently, with unsolicited older
// telemetry potentially interleaving. A STOPPED or old role line is
// NOT evidence of failure during an async START. Wait for BOTH fresh
// positive facts; the existing absolute timeout remains fail-closed.
const auto roleEvidence = kPtpMeasuredStateExpression.match(line);
if (roleEvidence.hasMatch()) {
if (roleEvidence.captured(1).toUpper() != pendingPtpRole_) {
failPtpStartup(QStringLiteral("PTP runtime role differs from the selected mode."), true);
return;
}
if (roleEvidence.hasMatch() &&
roleEvidence.captured(1).toUpper() == pendingPtpRole_)
ptpRuntimeRoleSeen_ = true;
return;
}
const auto readback = kPtpStatusExpression.match(line);
if (!readback.hasMatch()) return;
if (!ptpRuntimeRoleSeen_ || !ptpRunning_ || ptpRole_ != pendingPtpRole_) {
failPtpStartup(QStringLiteral("PTP requires matching PTP2 role and RUNNING readback."), true);
return;
}
const auto statusEvidence = kPtpStatusExpression.match(line);
if (statusEvidence.hasMatch() &&
statusEvidence.captured(1).toUpper() == QStringLiteral("RUNNING"))
ptpRuntimeRunningSeen_ = true;
if (!ptpRuntimeRoleSeen_ || !ptpRuntimeRunningSeen_ ||
!ptpRunning_ || ptpRole_ != pendingPtpRole_) return;
resetPtpStartup();
ptpFailureReason_.clear();
ptpStatus_ = QStringLiteral("PTP runtime active; external timing still unqualified");
emit ptpStateChanged();
}
Expand Down Expand Up @@ -1941,13 +1966,21 @@ void DeviceController::processLine(const QString& rawLine) {
ptpStatus_ = QStringLiteral("PTP start acknowledged; requesting live status");
static_cast<void>(sendPtpShow());
}
if (line.contains(QStringLiteral("PTP start rejected"), Qt::CaseInsensitive) || line.contains(QStringLiteral("PTP source readiness timeout"), Qt::CaseInsensitive)) {
if (line.contains(QStringLiteral("PTP start rejected"), Qt::CaseInsensitive) ||
line.contains(QStringLiteral("PTP source readiness timeout"), Qt::CaseInsensitive)) {
const QString boardReason =
line.mid(line.indexOf(QStringLiteral("PTP"))).left(300);
const bool linkDown = boardReason.contains(QStringLiteral("Ethernet PHY link DOWN"));
const bool sourceCounters = boardReason.contains(QStringLiteral("readiness timeout")) &&
boardReason.contains(QStringLiteral("Announce="));
// Preserve actual PHY-DOWN evidence or the source's observed TX
// counters rather than letting a later generic error hide the cause.
if (linkDown || sourceCounters || ptpFailureReason_.isEmpty())
ptpFailureReason_ = boardReason;
ptpRunning_ = false;
ptpStatus_ = QStringLiteral("PTP start failed");
emit ptpStateChanged();
setError(ptpRole_ == QStringLiteral("SOURCE")
? QStringLiteral("PTP source did not emit Announce/Sync/Follow_Up. Check the Ethernet link and retry Start PTP Source.")
: QStringLiteral("PTP %1 could not start. Check the Ethernet link and retry.").arg(ptpRole_.toLower()));
setError(boardReason);
}

advancePtpStartup(line);
Expand All @@ -1969,6 +2002,7 @@ void DeviceController::resetTelemetry() {

void DeviceController::resetPtpState() {
resetPtpStartup();
ptpFailureReason_.clear();
syncDiagnostics_ = QStringLiteral(
"No current-board SV lock evidence. Select Inspect SV lock after verification.");
syncFlightLatest_.clear();
Expand Down
Loading
Loading