Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4ed8f69
fix(studio): acknowledge PTP role/config/start in native guided Timin…
masarray Oct 15, 2026
725b51e
fix(studio): keep PTP START acknowledgements distinct from SV publish…
masarray Oct 15, 2026
a328da1
fix(workbench): preserve bounded initial SCL-assisted MMS q/t evidenc…
masarray Oct 15, 2026
8be7b8a
fix(studio): default native PTP Sync cadence to supported 1000 ms
masarray Oct 15, 2026
fb0457f
ci(studio): package side-by-side native Windows GUI preview with pinn…
masarray Oct 15, 2026
57d028c
build(studio): replace preview NSIS with Inno Setup smart upgrade and…
masarray Oct 15, 2026
d1c4124
feat(installer): gated ownership-safe migration from public NSIS v0.1…
masarray Oct 15, 2026
c375615
test(studio): localize Windows reference-template failure before inst…
masarray Oct 15, 2026
ff64f50
fix(ci): wait on GUI installers and application exit codes in Windows…
masarray Oct 15, 2026
7ce9025
fix(ci): copy pinned firmware/tool contents into canonical native Stu…
masarray Oct 15, 2026
eb7fba2
test(studio): capture baseline vs new native firmware contract failur…
masarray Oct 15, 2026
bc5415b
fix(studio): bundle matched PR-head ESP32-P4 firmware in isolated Inn…
masarray Oct 15, 2026
4b8de64
ci(esp32): isolate reusable preview firmware build from standalone wo…
masarray Oct 15, 2026
82d2f18
fix(esp32): fail closed on invalid firmware source commit provenance
masarray Oct 15, 2026
ebc62a1
fix(studio): stop independent PTP runtime on GUI shutdown before Inno…
masarray Oct 15, 2026
06db65e
fix(studio): bound full serial STOP drain instead of assuming first b…
masarray Oct 15, 2026
305ac0c
fix(installer): unambiguous Inno stable/preview channel and safe PTP-…
masarray Oct 15, 2026
8fa52fa
test(installer): correct PowerShell expression grouping in final NSIS…
masarray Oct 15, 2026
a0adcbd
fix(studio): require PTP role ACK and live PTP2 readback before GUI S…
masarray Oct 15, 2026
61be86f
docs(workbench): canonical ARSAS-inspired GUI/SSOT strategy and cross…
masarray Oct 15, 2026
03fceb0
merge(studio): reconcile native Inno preview with latest canonical Wo…
masarray Oct 15, 2026
58ed859
fix(studio): make PTP STOP fail-closed under queued START and transpo…
masarray Oct 15, 2026
c9c0155
test(studio): validate role-mismatch rollback through STOPPED before …
masarray Oct 15, 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
68 changes: 19 additions & 49 deletions apps/arstack_studio/qml/PtpExpertPanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ SurfacePanel {
property string monoFont: "Inter"
property var roleValues: ["SOURCE", "RECEIVER", "MONITOR"]
property bool advancedVisible: false
property bool startPending: false
readonly property bool startPending: panel.device && panel.device.ptpStarting

function roleIndex(role) {
if (role === "RECEIVER") return 1
Expand All @@ -27,9 +27,8 @@ SurfacePanel {
return 0
}

function applyProfile() {
if (!panel.session) return false
return panel.session.requestConfigurePtp({
function ptpProfile() {
return {
domain: Number(domainField.text),
transportSpecific: Number(transportField.text),
vlanEnabled: vlanCheck.checked,
Expand All @@ -38,24 +37,18 @@ SurfacePanel {
announceIntervalMs: Number(announceField.text),
syncIntervalMs: Number(syncField.text),
respondToPeerDelay: pdelayCheck.checked
})
}
}

function applyProfile() {
return panel.session && panel.session.requestConfigurePtp(panel.ptpProfile())
}

function startSelectedMode() {
if (!panel.session || !panel.device.ptpAvailable) return
var role = panel.roleValues[modeCombo.currentIndex]
if (!panel.session.requestPtpRole(role)) {
if (panel.controller) panel.controller.showMessage("PTP mode could not be selected.", true)
return
}
if (!panel.applyProfile()) {
if (panel.controller) panel.controller.showMessage("PTP settings were rejected before start.", true)
return
}
panel.startPending = true
if (!panel.session.requestStartPtp()) {
panel.startPending = false
if (panel.controller) panel.controller.showMessage("PTP start command could not be sent.", true)
if (!panel.session.requestStartPtpConfigured(role, panel.ptpProfile())) {
if (panel.controller) panel.controller.showMessage("PTP start blocked. See the Timing status for details.", true)
}
}

Expand All @@ -72,36 +65,11 @@ SurfacePanel {
: panel.device.ptpStatus === "PTP start failed" ? "START FAILED" : "OFF"

Timer {
interval: 700
// Status refresh only. Native C++ verifies ACKs and owns timeouts.
interval: 1000
repeat: true
running: panel.startPending || panel.device.ptpRunning
onTriggered: {
if (panel.session) panel.session.requestPtpRefresh()
if (panel.device.ptpRunning) panel.startPending = false
}
}

Timer {
interval: 3500
repeat: false
running: panel.startPending
onTriggered: {
panel.startPending = false
if (panel.session) panel.session.requestPtpRefresh()
if (!panel.device.ptpRunning && panel.controller)
panel.controller.showMessage("PTP did not reach verified transmit state. Check Ethernet link and retry.", true)
}
}

Connections {
target: panel.device
function onPtpStateChanged() {
if (panel.device.ptpRunning) {
panel.startPending = false
if (panel.controller && panel.sourceRole && panel.sourceEvidence)
panel.controller.showMessage("PTP Source active · Announce + Sync + Follow_Up verified.", false)
}
}
onTriggered: if (panel.session) panel.session.requestPtpRefresh()
}

ScrollView {
Expand Down Expand Up @@ -228,7 +196,6 @@ SurfacePanel {
text: "Stop PTP"
enabled: panel.session && (panel.device.ptpRunning || panel.startPending) && !panel.session.updatingFirmware
onClicked: {
panel.startPending = false
panel.session.requestStopPtp()
}
}
Expand Down Expand Up @@ -279,7 +246,7 @@ SurfacePanel {
Label {
Layout.columnSpan: 5
text: panel.sourceRole && panel.device.ptpRunning && panel.sourceEvidence
? "Verified transmit path: Announce + Sync + Follow_Up counters are advancing with zero TX failures."
? "Board reports Announce + Sync + Follow_Up TX with zero failures. Independent timing is not qualified."
: panel.sourceRole && panel.startPending
? "Waiting for the first verified Announce + Sync + Follow_Up transmission…"
: panel.device.ptpStatus
Expand Down Expand Up @@ -344,8 +311,11 @@ SurfacePanel {
theme: panel.theme
uiFont: panel.uiFont
text: "Apply advanced settings"
enabled: panel.session && panel.device.ptpAvailable && !panel.device.ptpRunning
onClicked: panel.applyProfile()
enabled: panel.session && panel.device.ptpAvailable && !panel.device.ptpRunning && !panel.startPending
onClicked: {
if (!panel.applyProfile() && panel.controller)
panel.controller.showMessage("PTP settings rejected.", true)
}
}
}

Expand Down
82 changes: 82 additions & 0 deletions apps/arstack_studio/src/DeterministicSessionHarness.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class DeterministicSessionHarness final {
{"manual recovery intent survives IDENTIFY -> firmware required", manualRecoveryIntentArmsFirmwareOffer()},
{"stale generation release -> ignored", staleReleaseGenerationIsIgnored()},
{"firmware probe before serial release -> blocked", firmwareProbeRequiresReleasedPort()},
{"PTP staged START requires role/config/runtime ACKs", ptpGuidedStartNeedsAcks()},
{"PTP rejected config never starts", ptpGuidedRejectedConfigStops()},
{"PTP STOP cancels pending start", ptpGuidedStopCancelsStart()},
{"PTP role mismatch and new session revoke pending start", ptpGuidedRoleAndGenerationGuards()},
{"profile timeout -> bounded terminal error", profileTimeoutIsTerminal()},
{"profile rejection -> bounded terminal error", profileRejectionIsTerminal()},
{"binary stale ACK identity -> fail closed", binaryProfileStaleAckFailsClosed()},
Expand Down Expand Up @@ -453,6 +457,84 @@ class DeterministicSessionHarness final {
!fixture.session.startReady();
}

static QVariantMap ptpValidProfile() {
return {{QStringLiteral("domain"), 0},
{QStringLiteral("transportSpecific"), 0},
{QStringLiteral("vlanEnabled"), false},
{QStringLiteral("vlanId"), 100},
{QStringLiteral("vlanPriority"), 4},
{QStringLiteral("announceIntervalMs"), 1000},
{QStringLiteral("syncIntervalMs"), 1000},
{QStringLiteral("respondToPeerDelay"), true}};
}

static bool setupGuidedPtp(Fixture& f, const QString& role) {
if (!f.profileReady) return false;
seedVerified(f, identity(), QStringLiteral("COM7"), false);
f.device.ptpAvailable_ = true;
f.device.ioWorkerReady_ = true; // No actual COM opened by this harness.
return f.session.requestStartPtpConfigured(role, ptpValidProfile());
}

static bool ptpGuidedStartNeedsAcks() {
Fixture f;
if (!setupGuidedPtp(f, QStringLiteral("RECEIVER"))) return false;
auto& d = f.device;
if (d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_role ||
d.ptpRunning_ || !d.ptpStarting()) return false;
d.processLine(QStringLiteral("PTPROLE role=RECEIVER value=1 running=0"));
if (d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_config ||
!d.ptpStarting()) return false;
d.processLine(QStringLiteral("PTP configuration accepted"));
if (d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_start ||
d.ptpRunning_) return false;
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"));
return !d.ptpStarting() && d.ptpRunning_ &&
d.ptpRole_ == QStringLiteral("RECEIVER") &&
d.ptpStatus_.contains(QStringLiteral("external timing still unqualified"));
}

static bool ptpGuidedRejectedConfigStops() {
Fixture f;
if (!setupGuidedPtp(f, QStringLiteral("SOURCE"))) return false;
auto& d = f.device;
d.processLine(QStringLiteral("PTPROLE role=SOURCE value=0 running=0"));
if (d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_config) return false;
d.processLine(QStringLiteral("PTP configuration rejected: invalid profile"));
return !d.ptpStarting() && !d.ptpRunning_ &&
d.ptpStatus_ == QStringLiteral("PTP start failed");
}

static bool ptpGuidedStopCancelsStart() {
Fixture f;
if (!setupGuidedPtp(f, QStringLiteral("SOURCE"))) return false;
auto& d = f.device;
if (!f.session.requestStopPtp() || d.ptpStarting()) return false;
// ACK from now-abandoned transaction cannot trigger CONFIG or START.
d.processLine(QStringLiteral("PTPROLE role=SOURCE value=0 running=0"));
return !d.ptpStarting() && d.ptpStartStage_ ==
DeviceController::PtpStartStage::idle;
}

static bool ptpGuidedRoleAndGenerationGuards() {
Fixture f;
if (!setupGuidedPtp(f, QStringLiteral("RECEIVER"))) return false;
auto& d = f.device;
d.processLine(QStringLiteral("PTPROLE role=MONITOR value=2 running=0"));
if (d.ptpStarting() || d.ptpStatus_ != QStringLiteral("PTP start failed"))
return false;
if (!f.session.requestStartPtpConfigured(QStringLiteral("RECEIVER"), ptpValidProfile()))
return false;
const quint64 previous = d.sessionGeneration_;
d.setSessionGeneration(previous + 1);
d.processLine(QStringLiteral("PTPROLE role=RECEIVER value=1 running=0"));
return !d.ptpStarting() && d.ptpStartStage_ ==
DeviceController::PtpStartStage::idle;
}

static bool staleReleaseGenerationIsIgnored() {
Fixture fixture;
if (!fixture.profileReady) return false;
Expand Down
Loading
Loading