Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4834a19
fix(studio): every START sends current 4I+4V table instead of reusing…
masarray Oct 18, 2026
29e53db
style(studio): stack voltage over current for full-width control and …
masarray Oct 18, 2026
7045de2
style(studio): restore normal 40px injector brand mark and legible ri…
masarray Oct 18, 2026
c727058
style(studio): readable near-white hover with dark text and matching …
masarray Oct 18, 2026
d49127e
style(studio): single consistent custom checkbox geometry across hove…
masarray Oct 18, 2026
517cbe1
style(studio): center ribbon icons and captions both horizontally and…
masarray Oct 18, 2026
56b2dd3
feat(studio): default verified device to PTP SOURCE ON with AUTO sync…
masarray Oct 18, 2026
63658b8
style(studio): Advanced tabs readable white hover/dark captions and t…
masarray Oct 18, 2026
dd78fed
test(studio): freeze SSOT START, local-sync baseline and normalized o…
masarray Oct 18, 2026
e8fc839
test(studio): accept multiline SSOT START snapshot command formatting
masarray Oct 18, 2026
047e5a7
style(studio): Advanced selectors use high-contrast white hover and d…
masarray Oct 18, 2026
2c3a223
test(studio): recognize multiline full table START without requiring …
masarray Oct 18, 2026
e0b98ee
fix(studio): reserve full 4-row intrinsic height for each stacked tab…
masarray Oct 18, 2026
3ce4bea
test(studio): assert non-clipping vertical matrix height
masarray Oct 18, 2026
c5d886c
fix(studio): responsive vertical Voltage/Current with compact short-w…
masarray Oct 18, 2026
2356e37
test(studio): assert stacked full-size or compact fallback geometry w…
masarray Oct 18, 2026
988faf6
test(studio): protect vertical normal-height and compact short-height…
masarray Oct 18, 2026
679da6c
build(studio): include cmath explicitly for viewport alignment tolerance
masarray Oct 18, 2026
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
6 changes: 5 additions & 1 deletion apps/arstack_studio/qml/CalmButton.qml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Button {
Behavior on scale { NumberAnimation { duration: 70; easing.type: Easing.OutCubic } }

readonly property color activeFill: !enabled ? "#10161d" :
hovered ? "#f0f6ff" :
tone === "success" ? (hovered ? "#1e5c43" : "#194d38") :
tone === "danger" ? (hovered ? "#523039" : theme.redSoft) :
tone === "accent" ? (hovered ? "#1d3c5e" : theme.accentSoft) :
Expand All @@ -32,6 +33,7 @@ Button {
tone === "accent" ? "#315f8d" :
(activeFocus ? theme.accent : hovered ? "#3a4a5b" : theme.line)
readonly property color activeText: !enabled ? theme.muted2 :
hovered ? "#132233" :
tone === "success" ? "#d6f4e6" :
tone === "danger" ? "#ffdce0" :
tone === "accent" ? "#dcebff" : theme.textSoft
Expand All @@ -44,7 +46,9 @@ Button {
width: control.iconSize
height: control.iconSize
anchors.verticalCenter: parent.verticalCenter
source: control.iconSource
source: control.hovered && control.enabled
? control.iconSource.toString().replace("/assets/material/", "/assets/material_dark/")
: control.iconSource
sourceSize.width: control.iconSize * 2
sourceSize.height: control.iconSize * 2
opacity: control.enabled ? 0.94 : 0.30
Expand Down
9 changes: 6 additions & 3 deletions apps/arstack_studio/qml/ConfigurationHub.qml
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,18 @@ Item {
font.family: hub.uiFont
font.pixelSize: 11
font.weight: checked ? Font.DemiBold : Font.Medium
hoverEnabled: true
contentItem: Label {
text: parent.text
color: parent.checked ? hub.theme.text : hub.theme.muted
color: parent.hovered && parent.enabled ? "#132233"
: parent.checked ? hub.theme.text : hub.theme.muted
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.checked ? hub.theme.raised : "transparent"
color: parent.hovered && parent.enabled ? "#f0f6ff"
: parent.checked ? hub.theme.raised : "transparent"
radius: 6
border.width: parent.checked ? 1 : 0
border.color: hub.theme.line
Expand All @@ -59,7 +62,7 @@ Item {

ColumnLayout {
anchors.fill: parent
spacing: 16
spacing: 11

RowLayout {
Layout.fillWidth: true
Expand Down
39 changes: 35 additions & 4 deletions apps/arstack_studio/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ ApplicationWindow {
// an explicit PTP ON can be restored after a firmware reflash/reconnect
// without introducing an uncontrolled power-on publisher.
property bool ptpDesiredForSession: false
property bool defaultPtpAttempted: false
property string ptpDesiredRole: "SOURCE"
property bool ptpRestoreAfterFirmware: false
property int ptpRestoreChecks: 0
Expand Down Expand Up @@ -215,6 +216,25 @@ ApplicationWindow {
}
}

// Operator default: start PTP SOURCE automatically once the connected
// board has been verified, without starting SV or claiming clock lock.
// Never retry rejected commands indefinitely. Explicit PTP OFF wins.
Timer {
interval: 400
repeat: true
running: !root.defaultPtpAttempted
onTriggered: {
const session = workflowBar.session
if (!session || !session.startReady || session.updatingFirmware ||
!device.deviceVerified || !device.ptpAvailable ||
device.ptpStarting)
return
root.defaultPtpAttempted = true
if (!device.ptpRunning)
root.startPtpRole("SOURCE")
Comment on lines +233 to +234

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 Preserve explicit PTP OFF across firmware updates

When this timer auto-starts a stopped board, root.startPtpRole() records ptpDesiredForSession = true; however, the Advanced PTP switch stops directly through panel.session.requestStopPtp() in PtpExpertPanel.qml:137 and never clears that intent. After an explicit OFF through that switch, any successful firmware update therefore enters the restoration path at Main.qml:212-214 and turns SOURCE back on, unexpectedly resuming network transmission. Keep automatic/default state separate from explicit restore intent, or route every OFF control through the intent owner.

AGENTS.md reference: AGENTS.md:L74-L76

Useful? React with 👍 / 👎.

}
}

Timer {
interval: 300
repeat: true
Expand Down Expand Up @@ -314,6 +334,7 @@ ApplicationWindow {
}

function startPtpRole(role) {
root.defaultPtpAttempted = true
const accepted = profilePanel.startPtpRole(role)
if (accepted) {
root.ptpDesiredForSession = true
Expand All @@ -323,6 +344,7 @@ ApplicationWindow {
}

function requestStopPtp() {
root.defaultPtpAttempted = true
root.ptpDesiredForSession = false
root.ptpRestoreAfterFirmware = false
return workflowBar.session && workflowBar.session.requestStopPtp()
Expand Down Expand Up @@ -1049,11 +1071,20 @@ ApplicationWindow {
}
}

RowLayout {
// Use stacked full-width Voltage/Current in the
// normal operator window; keep a two-column fallback
// only in short viewports so all controls remain
// accessible at the supported 960x620 minimum.
GridLayout {
readonly property bool stacked: root.height >= 720
columns: stacked ? 1 : 2
Layout.fillWidth: true
Layout.preferredHeight: root.compactLayout ? 214 : 224
Layout.maximumHeight: root.compactLayout ? 214 : 224
spacing: 9
Layout.preferredHeight: stacked
? (root.compactLayout ? 438 : 460)
: (root.compactLayout ? 214 : 224)
Layout.maximumHeight: Layout.preferredHeight
rowSpacing: 8
columnSpacing: 9
SignalMatrix {
id: voltageMatrix
enabled: !workflowBar.startQueued
Expand Down
19 changes: 10 additions & 9 deletions apps/arstack_studio/qml/ModernRibbon.qml
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,24 @@ Rectangle {
spacing: 6

RowLayout {
Layout.preferredWidth: bar.compact ? 105 : 124
Layout.preferredWidth: bar.compact ? 148 : 172
Layout.alignment: Qt.AlignVCenter
spacing: 8

Rectangle {
width: 28
height: 28
radius: 7
width: 40
height: 40
radius: 9
color: "#0d151d"
border.width: 1
border.color: bar.theme.lineSoft
Image {
anchors.centerIn: parent
width: 15
height: 15
width: 27
height: 27
source: Qt.resolvedUrl("../assets/material/smv_injector.svg")
sourceSize.width: 30
sourceSize.height: 30
sourceSize.width: 54
sourceSize.height: 54
}
}

Expand All @@ -73,10 +73,11 @@ Rectangle {
text: "SMV Injector"
color: bar.theme.text
font.family: bar.uiFont
font.pixelSize: 11
font.pixelSize: 14
font.weight: Font.DemiBold
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
}
}

Expand Down
13 changes: 7 additions & 6 deletions apps/arstack_studio/qml/RibbonAction.qml
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,15 @@ Button {
: tone === "warning" ? "#f5daaa"
: tone === "danger" ? "#ffe3e6"
: tone === "accent" || checked ? "#e1f1ff" : theme.textSoft
contentItem: ColumnLayout {
spacing: 3
contentItem: Column {
spacing: 4
anchors.centerIn: parent
Image {
visible: control.iconSource.toString().length > 0
source: control.hovered && control.enabled ? control.hoverPath : control.iconSource
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 24
Layout.preferredHeight: 24
anchors.horizontalCenter: parent.horizontalCenter
width: 24
height: 24
sourceSize.width: 48
sourceSize.height: 48
opacity: control.enabled ? 1 : 0.3
Expand All @@ -56,7 +57,7 @@ Button {
text: control.text
color: control.textColor
font: control.font
Layout.alignment: Qt.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
Expand Down
10 changes: 8 additions & 2 deletions apps/arstack_studio/qml/StudioDarkCheckBox.qml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ CheckBox {
id: control
property var theme
property string uiFont: "Inter"
implicitHeight: 30
implicitHeight: 32
implicitWidth: contentItem.implicitWidth + indicator.implicitWidth + spacing + leftPadding + rightPadding
spacing: 9
hoverEnabled: true
// Use one fixed-size custom indicator for all states. No native style
// hover/focus sub-checkbox may appear alongside the engineering control.
background: Item {}
font.family: uiFont
font.pixelSize: 11
indicator: Rectangle {
Expand All @@ -20,7 +25,8 @@ CheckBox {
color: control.checked ? control.theme.accent : control.theme.surface2
border.width: 1
border.color: control.checked ? control.theme.accent
: control.activeFocus ? control.theme.textSoft : control.theme.line
: (control.hovered || control.activeFocus) ? control.theme.textSoft
: control.theme.line
Text {
anchors.centerIn: parent
visible: control.checked
Expand Down
12 changes: 7 additions & 5 deletions apps/arstack_studio/qml/StudioDarkComboBox.qml
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@ ComboBox {

background: Rectangle {
radius: 7
color: combo.enabled ? (combo.hovered ? combo.theme.raisedHover : combo.theme.surface2)
color: combo.enabled ? (combo.hovered ? "#f0f6ff" : combo.theme.surface2)
: combo.theme.surface2
border.width: 1
border.color: combo.activeFocus ? combo.theme.accent : combo.theme.line
}

contentItem: Text {
text: combo.displayText
color: combo.enabled ? combo.theme.text : combo.theme.muted
color: !combo.enabled ? combo.theme.muted
: combo.hovered ? "#132233" : combo.theme.text
font: combo.font
verticalAlignment: Text.AlignVCenter
leftPadding: 12
Expand All @@ -39,7 +40,8 @@ ComboBox {

indicator: Text {
text: "\u25be"
color: combo.enabled ? combo.theme.textSoft : combo.theme.muted
color: !combo.enabled ? combo.theme.muted
: combo.hovered ? "#132233" : combo.theme.textSoft
font.pixelSize: 14
anchors.right: parent.right
anchors.rightMargin: 11
Expand All @@ -54,7 +56,7 @@ ComboBox {
highlighted: combo.highlightedIndex === index
contentItem: Text {
text: combo.textAt(choice.index)
color: combo.theme.text
color: choice.highlighted ? "#132233" : combo.theme.text
font: combo.font
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
Expand All @@ -63,7 +65,7 @@ ComboBox {
}
background: Rectangle {
radius: 5
color: parent.highlighted ? combo.theme.accentSoft : combo.theme.raised
color: parent.highlighted ? "#f0f6ff" : combo.theme.raised
}
}

Expand Down
8 changes: 5 additions & 3 deletions apps/arstack_studio/qml/WorkflowBar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,11 @@ SurfacePanel {
controller.showMessage(ribbon.startReason(), false)
return
}
const accepted = controller.pendingApply
? smartSession.requestStartWithScenario(controller.startScenarioSnapshot())
: smartSession.requestStart()
// Always apply the *visible* 4I+4V table as one ordered START
// transaction. Device settings may retain the previous session;
// pendingApply is a UI hint, never an authority to omit setpoints.
const accepted = smartSession.requestStartWithScenario(
controller.startScenarioSnapshot())
if (!accepted) {
controller.pendingApply = true
controller.showMessage("Start rejected. Check device status.", true)
Expand Down
14 changes: 12 additions & 2 deletions apps/arstack_studio/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

#include <algorithm>
#include <array>
#include <cmath>
#include <memory>
#include <string_view>

Expand Down Expand Up @@ -82,8 +83,17 @@ struct UiBounds {
return false;
}
}
if (!(voltage.area.right() <= current.area.left() + tolerance
&& tray.area.top() >= voltage.area.bottom() - tolerance
// The full-size operator view deliberately stacks Voltage ABOVE Current.
// At the supported 960x620 minimum a two-column fallback retains all
// controls inside the viewport instead of hiding the PTP/quality rows.
const bool stacked = size.height() >= 720;
const bool matricesOrdered = stacked
? (voltage.area.bottom() <= current.area.top() + tolerance &&
std::abs(voltage.area.left() - current.area.left()) <= tolerance)
: (voltage.area.right() <= current.area.left() + tolerance);
const qreal matrixBottom = std::max(voltage.area.bottom(), current.area.bottom());
if (!(matricesOrdered
&& tray.area.top() >= matrixBottom - tolerance
&& quality.area.top() >= tray.area.bottom() - tolerance
&& quality.area.bottom() <= surface.area.bottom() + tolerance)) {
qCritical().noquote() << "Studio UI panel order/overlap violation" << size
Expand Down
34 changes: 32 additions & 2 deletions apps/arstack_studio/tests/check_operator_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,34 @@ def main() -> None:
and "root.ptpRestoreChecks > 120" in main_qml
and "root.ptpDesiredForSession = false" in main_qml,
"PTP user intent restoration must be bounded, explicit and cancellable")
workflow_start = source("WorkflowBar.qml")
before_stop = workflow_start.split("function requestStart() {", 1)[1].split(
"function requestStop()", 1)[0]
require("requestStartWithScenario(" in before_stop
and "requestStart()" not in before_stop,
"each physical START must use the current GUI 4I+4V table, not stale persisted firmware setpoints")
require("readonly property bool stacked: root.height >= 720" in main_qml
and "columns: stacked ? 1 : 2" in main_qml
and "Layout.preferredHeight: stacked" in main_qml
and main_qml.index("id: voltageMatrix") < main_qml.index("id: currentMatrix"),
"Voltage must stack above Current at normal height, and keep a non-clipping short-window fallback")
require('root.startPtpRole("SOURCE")' in main_qml
and 'root.defaultPtpAttempted = true' in main_qml
and 'property bool phasorDockVisible: true' in main_qml
and 'property bool waveformDockVisible: true' in main_qml,
"default verified PTP SOURCE/AUTO and visible Phasor/Wave must preserve operator OFF authority")
ribbon_component = source("ModernRibbon.qml")
ribbon_action = source("RibbonAction.qml")
calm_button = source("CalmButton.qml")
checkbox = source("StudioDarkCheckBox.qml")
require('font.pixelSize: 14' in ribbon_component
and 'width: 40' in ribbon_component
and 'anchors.centerIn: parent' in ribbon_action,
"primary brand size and action captions/icons must stay centered")
require('hovered ? "#132233"' in calm_button
and 'hovered ? "#f0f6ff"' in calm_button
and 'background: Item {}' in checkbox,
"shared buttons and checkbox must have legible, consistent hover states")
matrix = source("SignalMatrix.qml")
menu = source("StudioActionMenu.qml")
cmake = (STUDIO / "CMakeLists.txt").read_text(encoding="utf-8")
Expand Down Expand Up @@ -116,7 +144,8 @@ def main() -> None:
and 'enabled: !workflowBar.startQueued; onActivated: root.zeroAll()' in main_qml,
"scenario editors and shortcuts must be frozen during a pending START snapshot")
require("controller.pendingApply = true" in workflow
and "requestStartWithScenario(controller.startScenarioSnapshot())" in workflow
and "requestStartWithScenario(" in workflow
and "controller.startScenarioSnapshot()" in workflow
and "controller.applyAllSignals()" not in workflow
and "startWatchdog" in workflow and "onRunningChanged()" in workflow,
"reconnect must not auto-transmit, but one-click Start must stage a bounded scenario")
Expand Down Expand Up @@ -145,7 +174,8 @@ def main() -> None:
and 'toolTipText: "Advanced"' not in ribbon
and 'ToolTip.text: "smpSynch mode"' not in timing_ui,
"captioned operator controls must not duplicate labels as tooltips")
require('requestStartWithScenario(controller.startScenarioSnapshot())' in workflow
require('requestStartWithScenario(' in workflow
and 'controller.startScenarioSnapshot()' in workflow
and "Apply to board" not in main_qml,
"Edit-to-Start must not regress to a manual Apply-to-board action")
require("assets/material/smv_injector.svg" in cmake_icons
Expand Down
Loading