Skip to content
Open
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
2 changes: 1 addition & 1 deletion apps/arstack_studio/qml/ModernRibbon.qml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Rectangle {
text: "Stop"
tone: bar.workflow.injectionRunning ? "danger" : "neutral"
iconSource: Qt.resolvedUrl("../assets/material/stop.svg")
enabled: (bar.workflow.injectionRunning || bar.controller.offlinePreviewRunning)
enabled: (bar.workflow.injectionRunning || bar.workflow.startQueued || bar.controller.offlinePreviewRunning)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enable the F6 stop shortcut while START is queued

During the pending-START interval, this enables cancellation only through the ribbon button, while the F6 shortcut in Main.qml:658 remains enabled only when the session state is already RUNNING. An operator pressing F6 while startQueued is true therefore gets no action, allowing the queued START to proceed despite the stop intent; gate the shortcut on workflowBar.startQueued as well.

Useful? React with 👍 / 👎.

&& !bar.session.updatingFirmware
toolTipText: ""
onClicked: {
Expand Down
10 changes: 8 additions & 2 deletions apps/arstack_studio/qml/WorkflowBar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,14 @@ SurfacePanel {
}

function requestStop() {
if (ribbon.injectionRunning && !smartSession.requestStop())
controller.showMessage("Studio could not stop the current device session.", true)
if (!ribbon.injectionRunning && !ribbon.startQueued) return
if (!smartSession.requestStop()) {
controller.showMessage("Studio could not queue safety STOP; device state is unverified.", true)
return
}
ribbon.startQueued = false
startWatchdog.stop()
controller.pendingApply = true
}

function openEngineeringDialog() {
Expand Down
36 changes: 36 additions & 0 deletions apps/arstack_studio/src/DeterministicSessionHarness.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class DeterministicSessionHarness final {
{"PTP rejected config never starts", ptpGuidedRejectedConfigStops()},
{"PTP STOP cancels pending start after verified readback", ptpGuidedStopCancelsStart()},
{"ABC live SET encoder remains consistent and bounded", abcSetpointBatchContract()},
{"bare SV START cannot bypass full 4I+4V scenario", bareSvStartIsRejected()},
{"SV STOP cancels queued START despite full queue", svSafetyStopBypassesFullQueue()},
{"PTP STOP full queue uses bounded priority slot", ptpSafetyStopBypassesFullQueue()},
{"PTP STOP requires firmware ACK and stopped readback", ptpStopConfirmationCannotBeGuessed()},
{"PTP STOP timeout blocks restart pending manual recovery", ptpUnverifiedStopBlocksRestart()},
Expand Down Expand Up @@ -683,6 +685,40 @@ class DeterministicSessionHarness final {
dcNegative == QStringLiteral("SET IA -1000 0 0");
}

static bool bareSvStartIsRejected() {
Fixture f;
if (!f.profileReady) return false;
seedVerified(f, identity(), QStringLiteral("COM15"), false);
return !f.device.DeviceController::start() &&
f.device.lastError().contains(QStringLiteral("Bare START refused")) &&
!f.session.requestStart() &&
!f.device.running();
}

static bool svSafetyStopBypassesFullQueue() {
DeviceIoWorker worker;
for (int i = 0; i < DeviceIoWorker::commandQueueCapacity(); ++i) {
const QString command = i == 5 ? QStringLiteral("START") :
(i == 10 ? QStringLiteral("SCENARIO START 7") :
QStringLiteral("PROFILE SHOW"));
worker.commandQueue_.enqueue(DeviceIoWorker::PendingCommand{
command, command.toUtf8() + '\n', false, i == 5 || i == 10});
}
worker.exclusiveCommandsRemaining_ = 2;
worker.queuePrioritySvStop(); // no COM: check bounded queue mutation
if (!worker.svStopPending_ ||
worker.commandQueue_.size() != DeviceIoWorker::commandQueueCapacity() - 2 ||
worker.exclusiveCommandsRemaining_ != 0) return false;
for (const auto& command : worker.commandQueue_) {
if (command.text == QStringLiteral("START") ||
command.text.startsWith(QStringLiteral("SCENARIO START "))) return false;
}
worker.queuePrioritySvStop();
worker.queuePriorityPtpStop();
return worker.svStopPending_ && worker.ptpStopPending_ &&
worker.commandQueue_.size() == DeviceIoWorker::commandQueueCapacity() - 2;
}

static bool ptpSafetyStopBypassesFullQueue() {
DeviceIoWorker worker;
for (int i = 0; i < DeviceIoWorker::commandQueueCapacity(); ++i) {
Expand Down
7 changes: 6 additions & 1 deletion apps/arstack_studio/src/DeviceController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,12 @@ void DeviceController::handlePortClosed(const QString& portName) {
}

bool DeviceController::sendShow() { return sendCommand(QStringLiteral("SHOW")); }
bool DeviceController::start() { return sendCommand(QStringLiteral("START")); }
bool DeviceController::start() {
// A bare START has no source-of-truth snapshot. The only operator
// entrance is startWithScenario(), which serializes all eight channels.
setError(QStringLiteral("Bare START refused: provide the current 4I+4V scenario."));
return false;
}

bool DeviceController::startWithScenario(const QVariantMap& scenario) {
if (!deviceVerified_ || running_) return false;
Expand Down
49 changes: 42 additions & 7 deletions apps/arstack_studio/src/DeviceIoWorker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ void DeviceIoWorker::shutdown(const bool requestSvStop, const bool requestPtpSto
// during grandmaster shutdown. This runs once during teardown only.
commandQueue_.clear();
ptpStopPending_ = false;
svStopPending_ = false;
if ((requestSvStop || requestPtpStop) && serial_ != nullptr && serial_->isOpen()) {
const auto sendBoundedStop = [this](const QByteArray& command) {
const qint64 queued = serial_->write(command);
Expand Down Expand Up @@ -245,6 +246,7 @@ bool DeviceIoWorker::openPortInternal(const QString& portName, const bool automa
pendingRx_.clear();
commandQueue_.clear();
ptpStopPending_ = false;
svStopPending_ = false;
exclusiveCommandsRemaining_ = 0;
writeActive_ = false;
activeCommand_ = {};
Expand Down Expand Up @@ -293,6 +295,7 @@ void DeviceIoWorker::closePortInternal(const bool emitRelease) {
pendingRx_.clear();
commandQueue_.clear();
ptpStopPending_ = false;
svStopPending_ = false;
exclusiveCommandsRemaining_ = 0;
writeActive_ = false;
activeCommand_ = {};
Expand Down Expand Up @@ -377,11 +380,17 @@ bool DeviceIoWorker::enqueueCommandsInternal(
}
if (commands.isEmpty()) return true;

if (!exclusive && commands.size() == 1 &&
commands.constFirst().trimmed() == QStringLiteral("PTP STOP")) {
// One reserved safety slot cannot be rejected by normal queue saturation.
queuePriorityPtpStop();
return true;
if (!exclusive && commands.size() == 1) {
const QString command = commands.constFirst().trimmed();
if (command == QStringLiteral("PTP STOP")) {
queuePriorityPtpStop();
return true;
}
if (command == QStringLiteral("STOP")) {
// A safety STOP must never be rejected by a full normal queue.
queuePrioritySvStop();
return true;
}
}

if (exclusiveCommandsRemaining_ > 0) {
Expand Down Expand Up @@ -445,11 +454,37 @@ void DeviceIoWorker::queuePriorityPtpStop() {
pumpWriteQueue();
}

void DeviceIoWorker::queuePrioritySvStop() {
// Drop every not-yet-written SV START, including a future transactional
// START. Preserve other commands and the independent PTP STOP lease.
QQueue<PendingCommand> retained;
while (!commandQueue_.isEmpty()) {
PendingCommand command = commandQueue_.dequeue();
if (command.text == QStringLiteral("START") ||
command.text.startsWith(QStringLiteral("SCENARIO START "))) {
if (command.exclusive && exclusiveCommandsRemaining_ > 0) {
--exclusiveCommandsRemaining_;
}
continue;
}
retained.enqueue(std::move(command));
}
commandQueue_ = std::move(retained);
if (!(writeActive_ && activeCommand_.text == QStringLiteral("STOP"))) {
svStopPending_ = true; // coalesce repeated operator cancellations
}
pumpWriteQueue();
}

void DeviceIoWorker::pumpWriteQueue() {
if (serial_ == nullptr || !serial_->isOpen() || writeActive_ ||
(!ptpStopPending_ && commandQueue_.isEmpty())) return;
(!svStopPending_ && !ptpStopPending_ && commandQueue_.isEmpty())) return;

if (ptpStopPending_) {
if (svStopPending_) {
svStopPending_ = false;
activeCommand_ = PendingCommand{
QStringLiteral("STOP"), QByteArrayLiteral("STOP\n"), false, false};
} else if (ptpStopPending_) {
ptpStopPending_ = false;
activeCommand_ = PendingCommand{
QStringLiteral("PTP STOP"), QByteArrayLiteral("PTP STOP\n"), false, false};
Expand Down
4 changes: 3 additions & 1 deletion apps/arstack_studio/src/DeviceIoWorker.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ public slots:
bool exclusive,
bool reportRejection);
void queuePriorityPtpStop();
void queuePrioritySvStop();
void pumpWriteQueue();
void completeActiveWrite();
void processReadyRead();
Expand Down Expand Up @@ -193,6 +194,7 @@ public slots:
bool genericProbeActive_{false};
bool identityConfirmed_{false};
bool writeActive_{false};
bool ptpStopPending_{false}; // reserved bounded emergency STOP slot
bool ptpStopPending_{false}; // reserved bounded emergency PTP STOP slot
bool svStopPending_{false}; // independent reserved emergency SV STOP slot
bool heartbeatEnabled_{false};
};
13 changes: 8 additions & 5 deletions apps/arstack_studio/src/SmartSessionController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,9 @@ void SmartSessionController::start() {
}

bool SmartSessionController::requestStart() {
if (!startReady_ || !deviceControlAvailable() || device_ == nullptr || device_->running()) return false;
return device_->start();
// Retain the QML method for compatibility, but never let an indirect
// caller bypass startScenarioSnapshot()/requestStartWithScenario().
return false;
}

bool SmartSessionController::requestStartWithScenario(const QVariantMap& scenario) {
Expand All @@ -316,9 +317,11 @@ bool SmartSessionController::requestStartWithScenario(const QVariantMap& scenari
}

bool SmartSessionController::requestStop() {
if (device_ == nullptr) return false;
if (!device_->running()) return true;
if (portOwner_ != PortOwner::deviceSession || updateRequested_) return false;
if (device_ == nullptr || portOwner_ != PortOwner::deviceSession || updateRequested_)
return false;
// STOP is also a cancellation request while START is queued but the
// asynchronous firmware RUNNING acknowledgement has not arrived.
if (!device_->connected() || !device_->deviceVerified()) return false;
return device_->stop();
}

Expand Down
Loading