Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c3f91fb
fix(injector): ALWAYS send 4I+4V table snapshot atomically before eac…
masarray Oct 17, 2026
8843d9c
fix(studio): default verified PTP SOURCE, live-setpoint failure handl…
masarray Oct 17, 2026
a2d938c
fix(studio UX): improve full-height tables, ribbon brand/button cente…
masarray Oct 17, 2026
284e114
fix(studio UX): improve full-height tables, ribbon brand/button cente…
masarray Oct 17, 2026
bc0be48
fix(studio UX): improve full-height tables, ribbon brand/button cente…
masarray Oct 17, 2026
e2fe937
fix(studio UX): improve full-height tables, ribbon brand/button cente…
masarray Oct 17, 2026
f50c376
fix(studio): readable light-hover/dark-caption and centered advanced …
masarray Oct 17, 2026
72b5592
fix(studio): readable light-hover/dark-caption and centered advanced …
masarray Oct 17, 2026
7b96dad
fix(studio): readable light-hover/dark-caption and centered advanced …
masarray Oct 17, 2026
25c7a1a
fix(studio): unify Advanced and Main PTP user intent, with explicit O…
masarray Oct 17, 2026
c72cf68
test(studio): enforce exact −30/−150/+90 degree injected SET wire angles
masarray Oct 17, 2026
cd85669
test(studio): lock snapshot-on-every-START, default PTP, 4I/4V stacke…
masarray Oct 17, 2026
5d7e3da
docs: lock good SV/PTP AUTO-LOCAL 120s evidence and operator UI regre…
masarray Oct 17, 2026
7f4bd26
fix(studio): never hide unsent phasor/frequency/quality changes while…
masarray Oct 17, 2026
620ff15
test(studio): catch silent live-setpoint failures and show pending st…
masarray Oct 17, 2026
5c2b72e
test(studio): enforce vertical Voltage over Current and no overlap at…
masarray Oct 17, 2026
651076a
build(studio): explicitly include cmath for DPI-aligned geometric tol…
masarray Oct 17, 2026
b4337c6
fix(studio): scroll vertically at minimum window size without clippin…
masarray Oct 17, 2026
5dbc499
test(studio): validate scrollable minimum viewport and full desktop v…
masarray Oct 17, 2026
db7558d
test(studio): enforce vertical matrix scroll at supported 960x620 window
masarray Oct 17, 2026
d02bb03
test(studio): allow quality editor below fold only when minimum-heigh…
masarray Oct 17, 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
54 changes: 31 additions & 23 deletions apps/arstack_studio/qml/CalmButton.qml
Original file line number Diff line number Diff line change
Expand Up @@ -22,39 +22,47 @@ Button {
Behavior on scale { NumberAnimation { duration: 70; easing.type: Easing.OutCubic } }

readonly property color activeFill: !enabled ? "#10161d" :
tone === "success" ? (hovered ? "#1e5c43" : "#194d38") :
tone === "danger" ? (hovered ? "#523039" : theme.redSoft) :
tone === "accent" ? (hovered ? "#1d3c5e" : theme.accentSoft) :
(down ? "#1a2530" : hovered ? theme.raisedHover : "#141c25")
hovered ? "#e9f2fd" :
tone === "success" ? "#194d38" :
tone === "danger" ? theme.redSoft :
tone === "accent" ? theme.accentSoft :
down ? "#1a2530" : "#141c25"
readonly property color activeEdge: !enabled ? theme.lineSoft :
hovered ? "#a6c9f2" :
tone === "success" ? "#347a59" :
tone === "danger" ? "#86434b" :
tone === "accent" ? "#315f8d" :
(activeFocus ? theme.accent : hovered ? "#3a4a5b" : theme.line)
readonly property color activeText: !enabled ? theme.muted2 :
hovered ? "#142334" :
tone === "success" ? "#d6f4e6" :
tone === "danger" ? "#ffdce0" :
tone === "accent" ? "#dcebff" : theme.textSoft

contentItem: Row {
spacing: control.iconSource.toString().length > 0 ? 7 : 0
anchors.centerIn: parent
Image {
visible: control.iconSource.toString().length > 0
width: control.iconSize
height: control.iconSize
anchors.verticalCenter: parent.verticalCenter
source: control.iconSource
sourceSize.width: control.iconSize * 2
sourceSize.height: control.iconSize * 2
opacity: control.enabled ? 0.94 : 0.30
}
Text {
text: control.text
color: control.activeText
font: control.font
anchors.verticalCenter: parent.verticalCenter
Behavior on color { ColorAnimation { duration: 90 } }
contentItem: Item {
Row {
Comment on lines +42 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve CalmButton's content-based implicit width

The replacement Item has an implicit width of zero and does not inherit the inner Row's implicit size, while the button still computes its width from contentItem.implicitWidth. Consequently every CalmButton without an explicit width now resolves to the 64 px minimum, causing captions such as “Start PTP Receiver” and “Copy diagnostics” to overflow or overlap adjacent controls. Give the content item the Row's implicit dimensions or retain a content item that propagates child sizing.

Useful? React with 👍 / 👎.

spacing: control.iconSource.toString().length > 0 ? 7 : 0
anchors.centerIn: parent
Image {
visible: control.iconSource.toString().length > 0
width: control.iconSize
height: control.iconSize
anchors.verticalCenter: parent.verticalCenter
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
}
Text {
text: control.text
color: control.activeText
font: control.font
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}

Expand Down
12 changes: 7 additions & 5 deletions apps/arstack_studio/qml/ConfigurationHub.qml
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,18 @@ Item {
font.weight: checked ? Font.DemiBold : Font.Medium
contentItem: Label {
text: parent.text
color: parent.checked ? hub.theme.text : hub.theme.muted
color: parent.hovered && !parent.checked ? "#142334"
: 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.checked ? "#e9f2fd"
: parent.checked ? hub.theme.raised : "transparent"
radius: 6
border.width: parent.checked ? 1 : 0
border.color: hub.theme.line
border.width: parent.checked || parent.hovered ? 1 : 0
border.color: parent.hovered && !parent.checked ? "#a6c9f2" : hub.theme.line
Rectangle {
visible: parent.parent.checked
anchors.left: parent.left
Expand All @@ -59,7 +61,7 @@ Item {

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

RowLayout {
Layout.fillWidth: true
Expand Down
105 changes: 84 additions & 21 deletions apps/arstack_studio/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ ApplicationWindow {
saveScenarioTimer.restart()
// While STOPPED, a new edit belongs to the next supervised Start batch.
// Only a genuinely RUNNING board receives incremental live SETs.
if (!workflowBar.injectionRunning)
if (!workflowBar.injectionRunning ||
!workflowBar.session || !workflowBar.session.liveControlReady)
pendingApply = true
}
function restoreLocalScenario() {
Expand Down Expand Up @@ -119,6 +120,8 @@ 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 ptpDefaultStartPending: true
property bool ptpExplicitOff: false
property string ptpDesiredRole: "SOURCE"
property bool ptpRestoreAfterFirmware: false
property int ptpRestoreChecks: 0
Expand Down Expand Up @@ -198,6 +201,8 @@ ApplicationWindow {
function onDeviceVerifiedChanged() {
root.pendingApply = true
root.offlinePreviewRunning = false
if (device.deviceVerified && !root.ptpExplicitOff)
root.ptpDefaultStartPending = true
}
}

Expand All @@ -215,6 +220,28 @@ ApplicationWindow {
}
}

// One automatic SOURCE start on verified connection. An explicit OFF
// cancels this for the remainder of the Studio session; never send PTP
// commands without a verified board or while firmware is updating.
Timer {
interval: 400
repeat: true
running: root.ptpDefaultStartPending && !root.ptpExplicitOff && device.deviceVerified
onTriggered: {
if (device.ptpRunning) {
root.ptpDefaultStartPending = false
return
}
const session = workflowBar.session
if (!session || !session.startReady || session.updatingFirmware ||
device.ptpStarting || !device.ptpAvailable)
return
root.ptpDefaultStartPending = false
if (!root.startPtpRole("SOURCE"))
root.showMessage("PTP auto-start was not accepted. Use PTP On to retry.", true)
Comment on lines +239 to +241

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 an explicitly selected PTP role across reconnects

After an operator starts RECEIVER or MONITOR, a serial disconnect/reconnect re-arms ptpDefaultStartPending because ptpExplicitOff is false, but this handler always starts SOURCE instead of using the existing ptpDesiredForSession/ptpDesiredRole intent. The reconnect consequently turns a receiving or monitoring station into a PTP publisher without another operator action; only use SOURCE when no explicit role has been selected, otherwise restore ptpDesiredRole.

Useful? React with 👍 / 👎.

}
}

Timer {
interval: 300
repeat: true
Expand Down Expand Up @@ -317,13 +344,17 @@ ApplicationWindow {
const accepted = profilePanel.startPtpRole(role)
if (accepted) {
root.ptpDesiredForSession = true
root.ptpExplicitOff = false
root.ptpDefaultStartPending = false
root.ptpDesiredRole = role
}
return accepted
}

function requestStopPtp() {
root.ptpDesiredForSession = false
root.ptpDefaultStartPending = false
root.ptpExplicitOff = true
root.ptpRestoreAfterFirmware = false
return workflowBar.session && workflowBar.session.requestStopPtp()
}
Expand Down Expand Up @@ -468,8 +499,11 @@ ApplicationWindow {
selectSignal(group, row)
markScenarioDirty()
if (workflowBar.session && workflowBar.session.liveControlReady && !pendingApply) {
if (phaseLink && row < 3) sendLinkedGroup(group)
else sendSignal(group, row)
const sent = phaseLink && row < 3 ? sendLinkedGroup(group) : sendSignal(group, row)
if (!sent) {
pendingApply = true
showMessage("Setpoint not queued. STOP and START to apply all displayed 4I+4V values.", true)
Comment on lines +502 to +505

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 Propagate asynchronous queue rejection to the draft

When the worker queue is full during a live edit, requestSetSignal() still returns true because DeviceController::sendCommandBatch() only confirms that the queued invokeMethod was scheduled; the actual rejection occurs later in DeviceIoWorker::enqueueCommandsInternal(). Since WorkflowBar.onLastErrorChanged() ignores errors unless startQueued is true, this new sent check never marks the draft pending for the queue-exhaustion case it is intended to handle, leaving the GUI and board setpoints inconsistent. Handle the asynchronous commandRejected result for live edits rather than treating dispatcher acceptance as queue admission.

AGENTS.md reference: AGENTS.md:L116-L116

Useful? React with 👍 / 👎.

}
}
return true
}
Expand Down Expand Up @@ -503,8 +537,9 @@ ApplicationWindow {
frequencyField.invalidInput = false
refreshPreview()
markScenarioDirty()
if (workflowBar.session && workflowBar.session.liveControlReady && !pendingApply)
workflowBar.session.requestSetFrequency(value)
if (workflowBar.session && workflowBar.session.liveControlReady && !pendingApply &&
!workflowBar.session.requestSetFrequency(value))
pendingApply = true
return true
}

Expand Down Expand Up @@ -610,17 +645,19 @@ ApplicationWindow {
selectSignal(activeGroup, activeRow)
refreshPreview()
markScenarioDirty()
if (workflowBar.session && workflowBar.session.liveControlReady && !pendingApply)
workflowBar.session.requestZero()
if (workflowBar.session && workflowBar.session.liveControlReady && !pendingApply &&
!workflowBar.session.requestZero())
pendingApply = true
}

function setActiveQuality(value) {
var unsignedValue = Number(value) >>> 0
groupModel(activeGroup).setProperty(activeRow, "quality", unsignedValue)
activeQuality = unsignedValue
markScenarioDirty()
if (workflowBar.session && workflowBar.session.liveControlReady && !pendingApply)
workflowBar.session.requestSetQuality(activeSignal, unsignedValue)
if (workflowBar.session && workflowBar.session.liveControlReady && !pendingApply &&
!workflowBar.session.requestSetQuality(activeSignal, unsignedValue))
pendingApply = true
}

Shortcut { sequence: "Ctrl+B"; enabled: !workflowBar.startQueued; onActivated: root.balanced() }
Expand Down Expand Up @@ -698,11 +735,12 @@ ApplicationWindow {
}
Rectangle { width: 1; height: 12; color: studioTheme.lineSoft }
Label {
text: root.pendingApply && !workflowBar.injectionRunning
? "4I + 4V · Changes sent on Start"
text: root.pendingApply
? (workflowBar.injectionRunning
? "4I + 4V · Changes pending · restart to apply"
: "4I + 4V · Changes sent on Start")
: "4I + 4V · 4000 samples/s"
color: root.pendingApply && !workflowBar.injectionRunning
? studioTheme.amber : studioTheme.muted
color: root.pendingApply ? studioTheme.amber : studioTheme.muted
font.family: root.uiFont
font.pixelSize: 9
}
Expand Down Expand Up @@ -930,10 +968,31 @@ ApplicationWindow {
SplitView.preferredWidth: root.compactLayout ? 700 : 860
theme: studioTheme

ColumnLayout {
// A narrow Windows viewport cannot display two full
// engineering tables plus timing/quality at once. Scroll
// the injection editor as ONE surface at small heights;
// normal desktop sizes fully expand the two matrices.
ScrollView {
id: injectionScroll
objectName: "studioInjectionScroll"
anchors.fill: parent
anchors.margins: root.compactLayout ? 11 : 13
spacing: root.compactLayout ? 6 : 8
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
contentWidth: availableWidth
contentHeight: injectionEditor.y + injectionEditor.height
+ (root.compactLayout ? 8 : 9)

ColumnLayout {
id: injectionEditor
x: root.compactLayout ? 8 : 9
y: root.compactLayout ? 8 : 9
width: Math.max(1, injectionScroll.availableWidth -
2 * (root.compactLayout ? 8 : 9))
height: Math.max(implicitHeight,
injectionScroll.availableHeight -
2 * (root.compactLayout ? 8 : 9))
spacing: root.compactLayout ? 4 : 5

RowLayout {
Layout.fillWidth: true
Expand Down Expand Up @@ -1049,11 +1108,15 @@ ApplicationWindow {
}
}

RowLayout {
// Stack voltage above current. Both tables share the
// available height instead of leaving an empty lower
// half; the inner rows stretch with the desktop.
ColumnLayout {
Layout.fillWidth: true
Layout.preferredHeight: root.compactLayout ? 214 : 224
Layout.maximumHeight: root.compactLayout ? 214 : 224
spacing: 9
Layout.fillHeight: true
Layout.minimumHeight: 408

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the stacked matrices within the supported minimum height

At the declared 620 px window minimum, the footer, outer margins, workflow bar, and panel margins leave about 474 px inside the injection surface. This 408 px matrix minimum plus the 57 px timing tray and spacing already consumes essentially all of that space before the title, 48 px frequency row, separator, and quality row are laid out, so the editor must overflow or clip at a supported window size. Either raise the window minimum, make this section scrollable, or allow the matrices to shrink further.

Useful? React with 👍 / 👎.

Layout.preferredHeight: 422
spacing: 7
SignalMatrix {
id: voltageMatrix
enabled: !workflowBar.startQueued
Expand Down Expand Up @@ -1136,7 +1199,7 @@ ApplicationWindow {
compact: root.compactLayout
}
}
Item { Layout.fillHeight: true }
}
}
}

Expand Down
18 changes: 9 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 ? 150 : 176
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: 28
height: 28
source: Qt.resolvedUrl("../assets/material/smv_injector.svg")
sourceSize.width: 30
sourceSize.height: 30
sourceSize.width: 56
sourceSize.height: 56
}
}

Expand All @@ -73,7 +73,7 @@ Rectangle {
text: "SMV Injector"
color: bar.theme.text
font.family: bar.uiFont
font.pixelSize: 11
font.pixelSize: bar.compact ? 14 : 15
font.weight: Font.DemiBold
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
Expand Down
11 changes: 9 additions & 2 deletions apps/arstack_studio/qml/PtpExpertPanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ SurfacePanel {
|| panel.device.ptpRunning || panel.startPending || panel.session.updatingFirmware)
return false
const accepted = panel.session.requestStartPtpConfigured(role, panel.ptpProfile())
if (accepted && panel.controller) {
// A start from Advanced has exactly the same owner as Main.
panel.controller.ptpDesiredForSession = true
panel.controller.ptpExplicitOff = false
panel.controller.ptpDefaultStartPending = false
panel.controller.ptpDesiredRole = role
}
if (!accepted && panel.controller)
panel.controller.showMessage("PTP start blocked. Check timing diagnostics.", true)
return Boolean(accepted)
Expand Down Expand Up @@ -134,7 +141,7 @@ SurfacePanel {
const wantsRunning = checked
checked = Qt.binding(function() { return Boolean(panel.device.ptpRunning) })
if (wantsRunning) panel.startSelectedMode()
else if (panel.session && !panel.session.requestStopPtp() && panel.controller)
else if (panel.controller && !panel.controller.requestStopPtp())
panel.controller.showMessage("PTP stop was not accepted. Copy diagnostics.", true)
}
}
Expand Down Expand Up @@ -287,7 +294,7 @@ SurfacePanel {
text: panel.device.ptpStatus.indexOf("PTP STOP unverified") === 0 ? "Retry Stop PTP" : "Stop PTP"
enabled: panel.session && (panel.device.ptpRunning || panel.startPending) && !panel.session.updatingFirmware
onClicked: {
panel.session.requestStopPtp()
if (panel.controller) panel.controller.requestStopPtp()
}
}
CalmButton {
Expand Down
Loading
Loading