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
1 change: 1 addition & 0 deletions apps/arstack_studio/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ qt_add_qml_module(arstack_studio
qml/StudioActionMenu.qml
qml/StudioMenuDivider.qml
qml/StudioPtpSwitch.qml
qml/StudioLinkSwitch.qml
qml/StudioTimingTray.qml
qml/DeviceExpertPanel.qml
qml/SvCapturePanel.qml
Expand Down
57 changes: 35 additions & 22 deletions apps/arstack_studio/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,20 @@ ApplicationWindow {
validate(data.current, 0)
validate(data.voltage, 1)
restoringScenario = true
// Legacy presets can contain disabled output rows. This simple
// matrix has no channel enable controls by design. At application
// startup ONLY (never during live RUNNING), migrate disabled
// outputs to enabled channels with zero magnitude, preserving
// the previous zero-output effect without hidden OFF flags.
var migratedDisabled = 0
function assign(model, rows) {
for (var i = 0; i < 4; ++i) {
model.setProperty(i, "magnitude", rows[i][0])
const wasDisabled = rows[i][2] === false
model.setProperty(i, "magnitude", wasDisabled ? 0 : rows[i][0])
model.setProperty(i, "phase", rows[i][1])
model.setProperty(i, "enabled", rows[i][2])
model.setProperty(i, "enabled", true)
model.setProperty(i, "quality", rows[i][3])
if (wasDisabled) migratedDisabled += 1
}
}
assign(currentModel, data.current)
Expand All @@ -101,6 +109,13 @@ ApplicationWindow {
refreshPreview()
restoringScenario = false
pendingApply = true
if (migratedDisabled > 0) {
// Persist the migration to avoid redoing it on next launch.
// Explicitly notify the operator; no serial output is sent.
scenarioSettings.lastScenario = JSON.stringify(scenarioSnapshot())
showMessage("Legacy preset: " + migratedDisabled +
" disabled channel(s) restored as enabled at 0 magnitude. Review before START.", true)
}
} catch (e) {
restoringScenario = false
scenarioSettings.lastScenario = ""
Expand Down Expand Up @@ -516,6 +531,18 @@ ApplicationWindow {
}
// START always serializes the same ListModels that draw the table,
// including all eight current phase angles and enabled flags.
// Never silently START with an invisible disabled channel. The
// legacy restore path migrates at boot; an unexpected future state
// must be corrected explicitly instead of serialized unnoticed.
for (var group = 0; group < 2; ++group) {
var rows = groupModel(group)
for (var row = 0; row < 4; ++row) {
if (rows.get(row).enabled !== true) {
showMessage("Hidden disabled channel detected. Restore the scenario before START.", true)
return null
}
}
}
var draft = scenarioSnapshot()
draft.currentScale = currentScale
draft.voltageScale = voltageScale
Expand Down Expand Up @@ -1062,32 +1089,18 @@ ApplicationWindow {
CalmButton { theme: studioTheme; uiFont: root.uiFont; text: "50"; implicitWidth: 44; onClicked: root.setFrequencyValue(50) }
CalmButton { theme: studioTheme; uiFont: root.uiFont; text: "60"; implicitWidth: 44; onClicked: root.setFrequencyValue(60) }
Rectangle { width: 1; height: 24; color: studioTheme.lineSoft }
StudioDarkCheckBox {
StudioLinkSwitch {
theme: studioTheme
uiFont: root.uiFont
enabled: root.signalFrequency > 0
enabled: root.signalFrequency > 0 && !workflowBar.startQueued
checked: root.phaseLink
text: "3-phase link"
onToggled: {
// Link is an editing preference, not a wire
// setpoint. No 8-channel live replay or
// pendingApply on a presentation-only switch.
root.phaseLink = checked
// Linking phases changes future edit behavior only;
// never silently rewrite existing setpoints.
root.markScenarioDirty()
if (workflowBar.injectionRunning && workflowBar.session
&& workflowBar.session.liveControlReady) {
// Persist *all eight current table values* on
// live link changes; the displayed draft
// must never silently diverge from TX.
var ok = true
for (var group = 0; group < 2; ++group)
for (var row = 0; row < 4; ++row)
ok = root.sendSignal(group, row) && ok
if (ok) root.pendingApply = false
else root.showMessage("Not all live phases were sent. Stop/Start to apply the table.", true)
}
root.saveScenarioTimer.restart()
Comment on lines 1097 to +1102

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 Update the workspace gate after removing live replay

This replacement removes the last root.sendSignal(group, row) occurrence, but check_operator_workspace.py:141 still requires that exact string. The mandatory Studio workflow runs this checker for changes under apps/arstack_studio/**, and executing python3 apps/arstack_studio/tests/check_operator_workspace.py now fails with tables and live sent phases must use explicit bounded layout, so every affected non-draft PR is blocked before the Qt build. Update the stale assertion to validate the new no-replay contract.

AGENTS.md reference: AGENTS.md:L230-L236

Useful? React with 👍 / 👎.

}
font.family: root.uiFont
font.pixelSize: 9
}
Item { Layout.fillWidth: true }
}
Expand Down
19 changes: 0 additions & 19 deletions apps/arstack_studio/qml/SignalMatrix.qml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ Rectangle {
Layout.leftMargin: 9
Layout.rightMargin: 9
spacing: 6
Item { Layout.preferredWidth: 25 }
Label {
text: "CH"
Layout.preferredWidth: 46
Expand Down Expand Up @@ -126,7 +125,6 @@ Rectangle {
property string sid: model.signalId
property real mag: model.magnitude
property real angle: model.phase
property bool channelEnabled: model.enabled
property real signalQuality: model.quality
property color phaseColor: matrix.phaseColorFor(rowIndex)
property bool selected: matrix.controller.activeSignal === signalRow.sid
Expand Down Expand Up @@ -271,23 +269,6 @@ Rectangle {
anchors.rightMargin: 9
spacing: 6

StudioDarkCheckBox {
theme: matrix.theme
uiFont: matrix.uiFont
Layout.preferredWidth: 25
Layout.alignment: Qt.AlignVCenter
checked: signalRow.channelEnabled
ToolTip.visible: false
onToggled: {
matrix.sourceModel.setProperty(signalRow.rowIndex, "enabled", checked)
matrix.controller.selectSignal(matrix.groupIndex, signalRow.rowIndex)
matrix.controller.refreshPreview()
matrix.controller.markScenarioDirty()
if (matrix.session && matrix.session.liveControlReady && !matrix.controller.pendingApply)
matrix.session.requestSetEnabled(signalRow.sid, checked)
}
}

RowLayout {
Layout.preferredWidth: 46
Layout.alignment: Qt.AlignVCenter
Expand Down
56 changes: 56 additions & 0 deletions apps/arstack_studio/qml/StudioLinkSwitch.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Only changes how subsequent A/B/C setpoint edits are linked.
// Actual 4I+4V TX remains owned by Main.qml's verified scenario transaction.
import QtQuick
import QtQuick.Controls

Switch {
id: control
property var theme
property string uiFont: "Inter"
text: "3-phase link"
font.family: uiFont
font.pixelSize: 10
font.weight: Font.Medium
hoverEnabled: true
focusPolicy: Qt.StrongFocus
spacing: 8
Accessible.name: "3-phase link"
Accessible.description: "Link subsequent changes to phases A, B and C"
ToolTip.visible: hovered
ToolTip.delay: 900
ToolTip.text: checked ? "Editing A, B or C also updates the linked phases"
: "Edit phases A, B and C independently"
indicator: Rectangle {
implicitWidth: 44
implicitHeight: 24
x: control.leftPadding
y: (control.height - height) / 2
radius: 12
color: !control.enabled ? control.theme.surface2
: control.checked ? control.theme.greenSoft : control.theme.surface2
border.width: control.visualFocus ? 2 : 1
border.color: control.visualFocus ? control.theme.green
: control.checked ? control.theme.green : control.theme.line
opacity: control.enabled ? 1.0 : 0.48
Behavior on color { ColorAnimation { duration: 150 } }
Rectangle {
width: 16
height: 16
radius: 8
y: 4
x: control.checked ? parent.width - width - 4 : 4
color: control.checked ? control.theme.green : control.theme.textSoft
Behavior on x { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
Behavior on color { ColorAnimation { duration: 140 } }
}
}
contentItem: Text {
text: control.text
font: control.font
color: control.enabled ? control.theme.text : control.theme.muted
opacity: control.enabled ? 1.0 : 0.65
leftPadding: control.indicator.width + control.spacing
verticalAlignment: Text.AlignVCenter
}
}
25 changes: 23 additions & 2 deletions apps/arstack_studio/tests/check_operator_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,10 @@ def main() -> None:
"checkbox must keep one stable custom indicator on hover")
require("anchors.centerIn: parent" in calm,
"button caption needs horizontal centering")
require("root.sendSignal(group, row)" in main_qml
require("function sendSignal(group, row)" in main_qml
and "Layout.fillHeight: false" in main_qml
and "Layout.preferredHeight: 196" in main_qml,
"tables and live sent phases must use explicit bounded layout")
"tables and live sent phases must use explicit bounded layout without link-toggle replay")
require('property bool ptpDefaultStartAttempted: false' in main_qml
and 'root.startPtpRole("SOURCE")' in main_qml
and "device.deviceVerified" in main_qml,
Expand All @@ -156,6 +156,27 @@ def main() -> None:
and 'scenarioStage_ = ScenarioStage::awaiting_start_ack;' in device_cpp,
"START requires signed millidegree setpoints, firmware commit ACK and readback")
matrix = source("SignalMatrix.qml")
link_toggle = source("StudioLinkSwitch.qml")
require("StudioDarkCheckBox {" not in matrix
and "requestSetEnabled(" not in matrix
and "property bool channelEnabled:" not in matrix
and "Item { Layout.preferredWidth: 25 }" not in matrix,
"all eight injection table channel checkboxes and their header spacer must be gone")
require("StudioLinkSwitch {" in main_qml
and "Switch {" in link_toggle
and "Behavior on x" in link_toggle
and "Accessible.name" in link_toggle
and "root.saveScenarioTimer.restart()" in main_qml
and "Not all live phases were sent." not in main_qml,
"3-phase link must be an accessible animated switch that sends no setpoints")
require("wasDisabled ? 0 : rows[i][0]" in main_qml
and 'model.setProperty(i, "enabled", true)' in main_qml
and "migratedDisabled > 0" in main_qml
and "Review before START." in main_qml
and "Hidden disabled channel detected." in main_qml,
"legacy disabled-channel presets must migrate visibly and START cannot hide OFF channels")
require("qml/StudioLinkSwitch.qml" in (STUDIO / "CMakeLists.txt").read_text(encoding="utf-8"),
"link control must be registered with the native QML module")
require("Layout.fillHeight: true" in matrix
and "Layout.minimumHeight: 34" in matrix
and "Item { Layout.fillHeight: true }" not in matrix,
Expand Down
Loading