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
4 changes: 4 additions & 0 deletions apps/arstack_studio/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ set(ARSTACK_STUDIO_CORE_SOURCES
src/M4StreamScenarioModel.hpp
src/M4MultiSvUploadTransaction.cpp
src/M4MultiSvUploadTransaction.hpp
src/M4ThreeMuScenarioApply.cpp
src/M4ThreeMuScenarioApply.hpp
src/DeviceIoWorker.cpp
src/DeviceIoWorker.hpp
src/DeviceController.cpp
Expand Down Expand Up @@ -246,6 +248,8 @@ qt_add_executable(arstack_m4_upload_harness
src/M4MultiSvUploadHarness.cpp
src/M4MultiSvUploadTransaction.cpp
src/M4MultiSvUploadTransaction.hpp
src/M4ThreeMuScenarioApply.cpp
src/M4ThreeMuScenarioApply.hpp
src/SclProfileModel.cpp
src/SclProfileModel.hpp
)
Expand Down
29 changes: 26 additions & 3 deletions apps/arstack_studio/qml/Main.qml
Original file line number Diff line number Diff line change
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 || device.m4Deploying); onActivated: workflowBar.requestStop() }
Shortcut { sequence: "F6"; enabled: workflowBar.session && (workflowBar.injectionRunning || workflowBar.startQueued || device.m4Deploying || device.m4ScenarioApplying); onActivated: workflowBar.requestStop() }

Dialog {
id: diagnosticsDialog
Expand Down Expand Up @@ -1371,7 +1371,9 @@ ApplicationWindow {
Item { Layout.fillWidth: true }
Label {
visible: m4Pages.active
text: device.m4Deploying ? "UPLOADING · VERIFIED ACK REQUIRED"
text: device.m4ScenarioApplying ? "APPLYING · MU BANK READBACK REQUIRED"
: device.m4ScenariosVerified && device.m4DeployedHash === sclProfiles.m4ConfigurationHash ? "3/3 MU BANKS VERIFIED · START GATED"
: 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
Expand All @@ -1382,7 +1384,7 @@ ApplicationWindow {
Button {
visible: m4Pages.active
enabled: m4Pages.active && workflowBar.session && workflowBar.session.canDeployMultiSv &&
!device.m4Deploying && !workflowBar.startQueued
!device.m4Deploying && !device.m4ScenarioApplying && !workflowBar.startQueued
text: device.m4Deploying ? "Uploading…" : "Deploy 3 MU"
font.family: root.uiFont
font.pixelSize: 10
Expand All @@ -1399,6 +1401,27 @@ ApplicationWindow {
}
}
Button {
visible: m4Pages.active
enabled: m4Pages.active && workflowBar.session &&
workflowBar.session.canApplyMultiSvScenarios &&
!workflowBar.startQueued
text: device.m4ScenarioApplying ? "Applying…" : "Apply 3 MU"
font.family: root.uiFont
font.pixelSize: 10
onClicked: {
if (!root.commitFrequencyEditor() ||
!currentMatrix.flushPendingEdits() ||
!voltageMatrix.flushPendingEdits()) return
if (!m4Pages.saveCurrent(root.startScenarioSnapshot(false))) return
const drafts = [m4Pages.draftFor(0), m4Pages.draftFor(1), m4Pages.draftFor(2)]
const accepted = workflowBar.session.requestApplyMultiSvScenarios(drafts)
root.showMessage(accepted
? "Applying 3 isolated MU signal banks with firmware COMMIT and READBACK."
: "Apply rejected: verify current SCD roster and all three scenario drafts.", !accepted)
}
}
Button {
enabled: !device.m4Deploying && !device.m4ScenarioApplying
text: m4Pages.active ? "Close MU pages" : "Open MU pages"
font.family: root.uiFont
font.pixelSize: 10
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) ? "danger" : "neutral"
tone: (bar.workflow.injectionRunning || 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)
enabled: (bar.workflow.injectionRunning || bar.workflow.startQueued || bar.controller.offlinePreviewRunning || bar.device.m4Deploying || bar.device.m4ScenarioApplying)
&& !bar.session.updatingFirmware
toolTipText: ""
onClicked: {
Expand Down
2 changes: 1 addition & 1 deletion apps/arstack_studio/qml/WorkflowBar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ 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 (device.m4Deploying || device.m4ScenarioApplying) {
if (!smartSession.requestStop())
controller.showMessage("Unable to stop the M4 device transaction.", true)
return
Expand Down
65 changes: 65 additions & 0 deletions apps/arstack_studio/src/DeterministicSessionHarness.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ class DeterministicSessionHarness final {
{"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()},
{"M4 per-MU CFG STOP aborts serial queue without V1 START", m4PerMuScenarioStopGuard()},
{"M4 timeout retires unsent exclusive CFG without unsolicited STOP", m4RetireExclusiveQueueWithoutStop()},
{"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 @@ -2074,6 +2076,69 @@ class DeterministicSessionHarness final {
session.state() == QStringLiteral("M4 DEPLOY NOT VERIFIED");
}

static bool m4PerMuScenarioStopGuard() {
Fixture fixture;
if (!fixture.profileReady) return false;
seedVerified(fixture, identity(), QStringLiteral("COM7"), false);
auto& d = fixture.device;
auto& session = fixture.session;
// The deterministic seed models a verified board but not always the
// asynchronous DeviceIoWorker::ready event. Model a fully ready
// serial owner explicitly so this test exercises retirement ACK,
// rather than a not-yet-initialized worker startup race.
d.ioWorkerReady_ = true;
session.m4Mode_ = true;
d.m4Ready_ = true;
d.m4ScenarioApplying_ = true;
d.m4ScenarioSession_ = d.sessionGeneration_;
d.m4ScenarioTimer_.start(5000);
session.reconcile();
if (session.startReady() || session.liveControlReady() ||
session.state() != QStringLiteral("M4 APPLYING MU BANKS")) return false;
static_cast<void>(session.requestStop());
return !d.m4ScenarioApplying_ && !d.m4ScenariosVerified_ &&
d.m4ScenarioRetiring_ && !d.m4ScenarioTimer_.isActive() &&
d.m4ScenarioStatus().contains(QStringLiteral("STOP")) &&
!session.requestStartWithScenario({}) &&
!session.canApplyMultiSvScenarios() && !session.startReady();
}

static bool m4RetireExclusiveQueueWithoutStop() {
DeviceIoWorker worker;
worker.activeGeneration_ = 77;
using Command = DeviceIoWorker::PendingCommand;
worker.commandQueue_.enqueue(Command{
QStringLiteral("SVSTREAM 1 CFG COMMIT 1 FFFF"),
QByteArrayLiteral("SVSTREAM 1 CFG COMMIT 1 FFFF\n"), false, true});
worker.commandQueue_.enqueue(Command{
QStringLiteral("HEARTBEAT"), QByteArrayLiteral("HEARTBEAT\n"), true, false});
worker.exclusiveCommandsRemaining_ = 1;
bool retired = false;
QObject::connect(&worker, &DeviceIoWorker::exclusiveCommandsRetired,
&worker, [&retired](quint64 generation) {
if (generation == 77) retired = true;
});
worker.retireExclusiveCommands(77);
const bool cleared = retired && worker.commandQueue_.size() == 1 &&
worker.commandQueue_.head().text == QStringLiteral("HEARTBEAT") &&
worker.exclusiveCommandsRemaining_ == 0 &&
!worker.svStopPending_ && !worker.ptpStopPending_;
// If one UART command was already being written, acknowledgement of
// retirement must wait until that write is drained.
retired = false;
worker.writeActive_ = true;
worker.activeCommand_ = Command{
QStringLiteral("SVSTREAM 2 CFG SHAPE 0 0 0 2 1000"),
QByteArrayLiteral("SVSTREAM 2 CFG SHAPE 0 0 0 2 1000\n"), false, true};
worker.exclusiveCommandsRemaining_ = 1;
worker.retireExclusiveCommands(77);
const bool waited = !retired && worker.retireExclusivePending_ &&
worker.exclusiveCommandsRemaining_ == 1;
worker.completeActiveWrite();
return cleared && waited && retired && !worker.retireExclusivePending_ &&
!worker.svStopPending_ && !worker.ptpStopPending_;
}

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