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
16 changes: 13 additions & 3 deletions apps/arstack_studio/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,14 @@ ApplicationWindow {
autoLiveQueuedRevision = -1
showMessage("Three MU pages ready. Use Deploy 3 MU for verified firmware roster upload; START remains gated.", false)
}
// Flush the active MU editor and compare all three frozen drafts to the
// hashes actually COMMITTED+READBACK, never to the visible selected page.
function m4StartDrafts() {
if (!m4Pages.active || !root.commitFrequencyEditor() ||
!currentMatrix.flushPendingEdits() || !voltageMatrix.flushPendingEdits() ||
!m4Pages.saveCurrent(root.startScenarioSnapshot(false))) return null
return [m4Pages.draftFor(0), m4Pages.draftFor(1), m4Pages.draftFor(2)]
}
function m4SelectPage(slot) {
if (!m4Pages.active || slot === m4Pages.selectedSlot) return
// A tab click need not blur the focused NumericField; commit first.
Expand Down Expand Up @@ -1376,7 +1384,9 @@ ApplicationWindow {
visible: device.m4ReconnectMode === "SELECTED"
text: "BOARD ROSTER / SCD · " +
(workflowBar.session ? workflowBar.session.m4SclRosterMatch : "NOT VERIFIED") +
" · START GATED"
(device.running ? " · RUNNING" :
(workflowBar.session && workflowBar.session.canStartMultiSv ?
" · READY" : " · START REQUIRES PROOFS"))
color: workflowBar.session && workflowBar.session.m4SclRosterMatch === "MATCHED"
? studioTheme.green : studioTheme.amber
font.family: root.uiFont
Expand All @@ -1385,10 +1395,10 @@ ApplicationWindow {
Label {
visible: m4Pages.active
text: device.m4ScenarioApplying ? "APPLYING · MU BANK READBACK REQUIRED"
: device.m4ScenariosVerified && device.m4DeployedHash === sclProfiles.m4ConfigurationHash ? "3/3 MU BANKS VERIFIED · START GATED"
: device.m4ScenariosVerified && device.m4DeployedHash === sclProfiles.m4ConfigurationHash ? "3/3 MU BANKS VERIFIED · CHECK GROUP START"
: device.m4Deploying ? "UPLOADING · VERIFIED ACK REQUIRED"
: device.m4Ready && device.m4DeployedHash === sclProfiles.m4ConfigurationHash
? "3 MU ROSTER VERIFIED · START GATED" : "DRAFT · NOT DEPLOYED"
? "3 MU ROSTER VERIFIED · APPLY BANKS" : "DRAFT · NOT DEPLOYED"
color: device.m4Ready && device.m4DeployedHash === sclProfiles.m4ConfigurationHash
? studioTheme.green : studioTheme.amber
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 || bar.device.m4Deploying || bar.device.m4ScenarioApplying) ? "danger" : "neutral"
tone: (bar.workflow.injectionRunning || bar.device.m4GroupNeedsStop || bar.device.m4Deploying || bar.device.m4ScenarioApplying) ? "danger" : "neutral"
iconSource: Qt.resolvedUrl("../assets/material/stop.svg")
enabled: (bar.workflow.injectionRunning || bar.workflow.startQueued || bar.controller.offlinePreviewRunning || bar.device.m4Deploying || bar.device.m4ScenarioApplying)
enabled: (bar.workflow.injectionRunning || bar.workflow.startQueued || bar.controller.offlinePreviewRunning || bar.device.m4Deploying || bar.device.m4ScenarioApplying || bar.device.m4GroupNeedsStop)
&& !bar.session.updatingFirmware
toolTipText: ""
onClicked: {
Expand Down
15 changes: 12 additions & 3 deletions apps/arstack_studio/qml/WorkflowBar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ SurfacePanel {
// interactive while firmware verifies it; new edits coalesce and drain.
readonly property bool startQueued: Boolean(
startIntentPending || (ribbon.device && ribbon.device.scenarioApplying &&
!ribbon.device.running))
!ribbon.device.running)) ||
Boolean(ribbon.device && ribbon.device.m4GroupStarting)
Timer {
id: startIntentExpiry
interval: 6500
Expand Down Expand Up @@ -341,7 +342,15 @@ SurfacePanel {

function requestStart() {
if (ribbon.m4DraftOnly) {
controller.showMessage("M4 draft pages are not deployed. START blocked until MULTISV deploy/readback is implemented.", true)
if (!smartSession.canStartMultiSv) {
controller.showMessage("3-MU START blocked: " + device.m4GroupStatus +
". Deploy the matching SCD and Apply all three MU banks first.", true)
return
}
const drafts = controller.m4StartDrafts()
if (!drafts || drafts.length !== 3 ||
!smartSession.requestStartMultiSv(drafts))
controller.showMessage("3-MU START refused: editor changed or board proof no longer current.", true)
return
}
if (ribbon.injectionRunning || ribbon.startQueued) return
Expand Down Expand Up @@ -406,7 +415,7 @@ SurfacePanel {
}
// A pending CFG transaction can progress to START: Stop must remain
// actionable before RUNNING telemetry, not only after it.
if (!ribbon.injectionRunning && !ribbon.startQueued) return
if (!ribbon.injectionRunning && !ribbon.startQueued && !device.m4GroupNeedsStop) return
if (!smartSession.requestStop()) {
controller.showMessage("Studio could not cancel the current device session.", true)
return
Expand Down
84 changes: 84 additions & 0 deletions apps/arstack_studio/src/DeterministicSessionHarness.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class DeterministicSessionHarness final {
{"retry after unverified STOP cannot probe ROM without renewed consent", unverifiedFirmwareRetryNeverProbesRom()},
{"slow firmware STOP suspends ordinary health teardown, resumes afterward", maintenanceStopDoesNotTriggerHealthDisconnect()},
{"firmware STOP proof readbacks survive M4 exclusive retirement", maintenanceProofReadbackSurvivesM4Retirement()},
{"M4 group STOP ACK preserves existing firmware-quiescence SHOW/SVLIFE proof", m4MaintenanceStopUsesFirmwareProof()},
{"real timed-out firmware subprocess retires before COM release", firmwareWorkerTimeoutAwaitsProcessExit()},
{"current identity -> READY without recovery", currentIdentityNeverRecovers()},
{"legacy identity -> firmware update", legacyIdentityOffersUpdate()},
Expand Down Expand Up @@ -83,6 +84,7 @@ class DeterministicSessionHarness final {
{"M4 per-MU CFG STOP aborts serial queue without V1 START", m4PerMuScenarioStopGuard()},
{"M4 timeout retires unsent exclusive CFG without unsolicited STOP", m4RetireExclusiveQueueWithoutStop()},
{"M4 board inspection defers refresh but PTP STOP preempts it", m4BoardInspectionPtpSafetyPreempts()},
{"M4 group START/STOP requires exact boot/roster/token/mask RUNNING proof", m4GroupReadbackFailClosed()},
{"M4 reconnect selected roster blocks legacy V1 sync and awaits read-only banks", m4ReconnectSelectedGate()},
{"M4 reconnect uncertain roster never guesses legacy", m4ReconnectUnknownGate()},
{"legacy bare SV START refuses stale eight-channel state", legacyBareStartFailsClosed()},
Expand Down Expand Up @@ -219,6 +221,31 @@ class DeterministicSessionHarness final {
}


static bool m4MaintenanceStopUsesFirmwareProof() {
Fixture f;
if (!f.profileReady) return false;
seedVerified(f, identity(), QStringLiteral("COM7"), false);
auto& d = f.device;
d.ioWorkerReady_ = true;
d.m4GroupSession_ = d.sessionGeneration_;
d.m4GroupBoot_ = d.identity_.bootId;
d.m4GroupStage_ = DeviceController::M4GroupStage::awaiting_stop_ack;
d.m4GroupInFlight_.clear();
d.operatorStopPending_ = true;
d.firmwareQuiescenceActive_ = true;
d.firmwareQuiescenceSession_ = d.sessionGeneration_;
d.firmwareQuiescenceBoot_ = d.identity_.bootId;
d.processLine(QStringLiteral("STOP accepted: SV transmission suppressed"));
const bool routed = d.firmwareSvStopAck_ && !d.firmwareSvStoppedReadback_ &&
d.m4GroupStage_ == DeviceController::M4GroupStage::uncertain &&
!d.operatorStopPending_ && !d.m4GroupTimer_.isActive();
d.processLine(QStringLiteral("SVLIFE state=STOPPED stop_cause=OPERATOR_STOP stop_seq=1"));
const bool verified = d.firmwareSvStoppedReadback_ &&
!d.firmwareQuiescenceVerified(); // PTP still needs separate STOP proof
d.endFirmwareQuiescence();
return routed && verified;
}

static bool maintenanceProofReadbackSurvivesM4Retirement() {
Fixture f;
if (!f.profileReady) return false;
Expand Down Expand Up @@ -2450,6 +2477,63 @@ class DeterministicSessionHarness final {
d.deferredPtpShow_ && d.m4BoardSnapshots_.isEmpty();
}

static bool m4GroupReadbackFailClosed() {
Fixture fixture;
if (!fixture.profileReady) return false;
seedVerified(fixture, identity(), QStringLiteral("COM7"), false);
auto& d = fixture.device;
d.m4GroupSession_ = d.sessionGeneration_;
d.m4GroupBoot_ = d.identity_.bootId;
d.m4GroupRoster_ = QStringLiteral("0123456789ABCDEF");
d.m4GroupToken_ = 42U;
d.m4ReconnectRosterGeneration_ = 5U;
const auto state = [&d](const int running, const unsigned mask = 7,
const QString& boot = QString{}) {
return QStringLiteral("I (123) sv: M4STATE token=42 boot=%1 "
"roster=0123456789ABCDEF generation=5 selected=1 mask=%2 running=%3")
.arg(boot.isEmpty() ? d.m4GroupBoot_ : boot).arg(mask).arg(running);
};
d.m4GroupStage_ = DeviceController::M4GroupStage::awaiting_start_readback;
if (!d.processM4GroupLine(state(1)) ||
d.m4GroupStage_ != DeviceController::M4GroupStage::running_confirmed ||
!d.running() || d.m4GroupStarting()) return false;
d.m4GroupStage_ = DeviceController::M4GroupStage::awaiting_stop_readback;
d.operatorStopPending_ = true;
if (!d.processM4GroupLine(state(0)) ||
d.m4GroupStage_ != DeviceController::M4GroupStage::confirmed_stopped ||
d.running() || !d.m4GroupStartAllowed() ||
d.operatorStopPending_) return false;
// MU2 may have been disabled independently; group STOP still
// verifies the single publisher is STOPPED with that 3-bit mask.
d.m4GroupStage_ = DeviceController::M4GroupStage::awaiting_stop_readback;
d.operatorStopPending_ = true;
if (!d.processM4GroupLine(state(0, 5)) ||
d.m4GroupStage_ != DeviceController::M4GroupStage::confirmed_stopped ||
d.operatorStopPending_) return false;
d.m4GroupStage_ = DeviceController::M4GroupStage::awaiting_start_readback;
d.operatorStopPending_ = true;
if (!d.processM4GroupLine(state(1)) || !d.running() || d.operatorStopPending_)
return false;
d.operatorStopPending_ = false;
d.m4GroupStage_ = DeviceController::M4GroupStage::awaiting_start_readback;
if (!d.processM4GroupLine(state(1, 3)) ||
d.m4GroupStage_ != DeviceController::M4GroupStage::uncertain ||
d.m4GroupStartAllowed() || !d.m4GroupNeedsStop()) return false;
d.m4GroupStage_ = DeviceController::M4GroupStage::awaiting_start_readback;
if (!d.processM4GroupLine(state(1, 7, QStringLiteral("DEADBEEFDEADBEEF"))) ||
d.m4GroupStage_ != DeviceController::M4GroupStage::uncertain) return false;
d.m4GroupStage_ = DeviceController::M4GroupStage::awaiting_start_ack;
d.m4GroupInFlight_.clear();
if (!d.processM4GroupLine(QStringLiteral(
"M4START accepted token=41 boot=%1 roster=0123456789ABCDEF mask=7")
.arg(d.m4GroupBoot_)) ||
d.m4GroupStage_ != DeviceController::M4GroupStage::uncertain) return false;
d.m4GroupStage_ = DeviceController::M4GroupStage::awaiting_stop_readback;
if (!d.processM4GroupLine(state(1)) ||
d.m4GroupStage_ != DeviceController::M4GroupStage::uncertain) return false;
return true;
}

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