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
30 changes: 26 additions & 4 deletions apps/arstack_studio/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ ApplicationWindow {
scenarioSettings.lastScenario = JSON.stringify(root.scenarioSnapshot())
autoLiveApplyTimer.stop()
autoLiveQueuedRevision = -1
showMessage("Three MU draft pages ready. Device deploy is not enabled yet.", false)
showMessage("Three MU pages ready. Use Deploy 3 MU for verified firmware roster upload; START remains gated.", false)
}
function m4SelectPage(slot) {
if (!m4Pages.active || slot === m4Pages.selectedSlot) return
Expand Down Expand Up @@ -1024,7 +1024,7 @@ ApplicationWindow {
Shortcut { sequence: "Ctrl+B"; enabled: !workflowBar.startQueued; onActivated: root.balanced() }
Shortcut { sequence: "Ctrl+0"; enabled: !workflowBar.startQueued; onActivated: root.zeroAll() }
Shortcut { sequence: "F5"; enabled: workflowBar.session && !workflowBar.injectionRunning && !workflowBar.startQueued; onActivated: workflowBar.requestStart() }
Shortcut { sequence: "F6"; enabled: workflowBar.session && (workflowBar.injectionRunning || workflowBar.startQueued); onActivated: workflowBar.requestStop() }
Shortcut { sequence: "F6"; enabled: workflowBar.session && (workflowBar.injectionRunning || workflowBar.startQueued || device.m4Deploying); onActivated: workflowBar.requestStop() }

Dialog {
id: diagnosticsDialog
Expand Down Expand Up @@ -1371,11 +1371,33 @@ ApplicationWindow {
Item { Layout.fillWidth: true }
Label {
visible: m4Pages.active
text: "DRAFT ONLY · NOT DEPLOYED"
color: studioTheme.amber
text: device.m4Deploying ? "UPLOADING · VERIFIED ACK REQUIRED"
: device.m4Ready && device.m4DeployedHash === sclProfiles.m4ConfigurationHash
? "3 MU ROSTER VERIFIED · START GATED" : "DRAFT · NOT DEPLOYED"
color: device.m4Ready && device.m4DeployedHash === sclProfiles.m4ConfigurationHash
? studioTheme.green : studioTheme.amber
font.family: root.uiFont
font.pixelSize: 9
}
Button {
visible: m4Pages.active
enabled: m4Pages.active && workflowBar.session && workflowBar.session.canDeployMultiSv &&
!device.m4Deploying && !workflowBar.startQueued
text: device.m4Deploying ? "Uploading…" : "Deploy 3 MU"
font.family: root.uiFont
font.pixelSize: 10
onClicked: {
if (!root.commitFrequencyEditor() ||
!currentMatrix.flushPendingEdits() ||
!voltageMatrix.flushPendingEdits()) return
if (m4Pages.active)
m4Pages.saveCurrent(root.startScenarioSnapshot(false))
const accepted = workflowBar.session && workflowBar.session.requestDeployMultiSv()
root.showMessage(accepted
? "Uploading ARM3. Board hash, all 3 MAC/svID/VLAN identities and SELECT ACK will be checked."
: "M4 deploy blocked. Check stopped board, firmware capability, or diagnostics.", !accepted)
}
}
Button {
text: m4Pages.active ? "Close MU pages" : "Open MU pages"
font.family: root.uiFont
Expand Down
4 changes: 2 additions & 2 deletions apps/arstack_studio/qml/ModernRibbon.qml
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ Rectangle {
theme: bar.theme
uiFont: bar.uiFont
text: "Stop"
tone: bar.workflow.injectionRunning ? "danger" : "neutral"
tone: (bar.workflow.injectionRunning || bar.device.m4Deploying) ? "danger" : "neutral"
iconSource: Qt.resolvedUrl("../assets/material/stop.svg")
enabled: (bar.workflow.injectionRunning || bar.workflow.startQueued || bar.controller.offlinePreviewRunning)
enabled: (bar.workflow.injectionRunning || bar.workflow.startQueued || bar.controller.offlinePreviewRunning || bar.device.m4Deploying)
&& !bar.session.updatingFirmware
toolTipText: ""
onClicked: {
Expand Down
7 changes: 7 additions & 0 deletions apps/arstack_studio/qml/WorkflowBar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,13 @@ SurfacePanel {
}

function requestStop() {
// STOP is the operator's emergency cancellation even while an M4
// roster upload is staged with the publisher still STOPPED.
if (device.m4Deploying) {
if (!smartSession.requestStop())
controller.showMessage("Unable to stop the M4 device transaction.", true)
return
}
if (ribbon.startIntentPending) {
ribbon.cancelStartIntent()
controller.showMessage("Pending START cancelled.", false)
Expand Down
53 changes: 53 additions & 0 deletions apps/arstack_studio/src/DeterministicSessionHarness.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ class DeterministicSessionHarness final {
{"PTP LINK_DOWN is classified early; MAC INVALID_STATE keeps STOP verification", ptpPhysicalEthernetPreflightAndMacFailure()},
{"PTP rejected config never starts", ptpGuidedRejectedConfigStops()},
{"PTP STOP cancels pending start after verified readback", ptpGuidedStopCancelsStart()},
{"M4 roster never authorizes V1 START or automatic profile sync", m4RosterIsolationAndV1StartGuard()},
{"M4 upload STOP revokes transaction proof before serial STOP", m4StopCancelsUpload()},
{"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 @@ -2021,6 +2023,57 @@ class DeterministicSessionHarness final {
report.contains(QStringLiteral("Firmware protocol compatible: UNKNOWN"));
}

static bool m4RosterIsolationAndV1StartGuard() {
Fixture fixture;
if (!fixture.profileReady) return false;
seedVerified(fixture, identity(), QStringLiteral("COM7"), false);
auto& device = fixture.device;
auto& session = fixture.session;

// Explicit V1/M4 mode isolation: no implicit PROFILE transaction,
// START or live edit can issue legacy commands after M4 selection.
session.m4Mode_ = true;
device.m4Ready_ = true;
device.m4DeployedHash_ = QStringLiteral("0011223344556677");
session.needsProfileSync_ = true;
session.reconcile();
const bool isolated = !session.startReady() &&
!session.canDeployProfile() &&
!session.liveControlReady() &&
session.state() == QStringLiteral("M4 ROSTER VERIFIED") &&
!session.requestStartWithScenario({}) &&
!session.requestApplyLiveScenario({}) &&
!device.startWithScenario({}) &&
!device.deployProfile({}) &&
session.needsProfileSync_ && !device.profileDeploying();
return isolated;
}

static bool m4StopCancelsUpload() {
Fixture fixture;
if (!fixture.profileReady) return false;
seedVerified(fixture, identity(), QStringLiteral("COM7"), false);
auto& device = fixture.device;
auto& session = fixture.session;
session.m4Mode_ = true;
device.m4Ready_ = false;
device.m4Deploying_ = true;
device.m4SessionGeneration_ = device.sessionGeneration_;
device.m4InFlightCommand_ = QStringLiteral("MULTISV CHUNK 42 0 0102");
device.m4AckTimer_.start(5000);
// An explicit STOP is a reserved priority command. The device
// transaction is invalidated synchronously before any worker write.
static_cast<void>(device.stop());
const bool cancelled = !device.m4Deploying_ && !device.m4Ready_ &&
!device.m4AckTimer_.isActive() &&
device.m4InFlightCommand_.isEmpty() &&
device.m4DeploymentStatus().contains(QStringLiteral("STOP"));
// The profile synchronizer must NOT revive the old V1 profile.
session.reconcile();
return cancelled && !session.startReady() &&
session.state() == QStringLiteral("M4 DEPLOY NOT VERIFIED");
}

static bool workerTeardownCompletes() {
bool profileReady = false;
{
Expand Down
Loading
Loading