Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1546639
fix(studio): improve readable ribbon, consistent hover controls and v…
masarray Oct 18, 2026
28aa514
fix(studio): improve readable ribbon, consistent hover controls and v…
masarray Oct 18, 2026
73994a8
fix(studio): improve readable ribbon, consistent hover controls and v…
masarray Oct 18, 2026
10b9e28
fix(studio): improve readable ribbon, consistent hover controls and v…
masarray Oct 18, 2026
fdab1cd
fix(studio): improve readable ribbon, consistent hover controls and v…
masarray Oct 18, 2026
2895c6d
fix(studio): avoid conflicting anchors in ribbon Controls content layout
masarray Oct 18, 2026
474e1d4
test(studio): lock snapshot START, full 4I+4V signed phases and reada…
masarray Oct 18, 2026
513c327
fix(studio): dark caption on white hover for Advanced icon/text tool …
masarray Oct 18, 2026
fa6c60a
test(studio): keep readable Advanced tool hovers and single-size chec…
masarray Oct 18, 2026
7a2826c
test(studio): pin verified SV/PTP phase-evidence profile across futur…
masarray Oct 18, 2026
e7e197e
fix(studio): Advanced dropdown hover uses white surface and dark read…
masarray Oct 18, 2026
03d49a9
test(studio): prevent white-on-white Advanced dropdown hover regressions
masarray Oct 18, 2026
9a34990
docs: freeze 120s SV/PTP local-lock wire checkpoint and table-to-SV f…
masarray Oct 18, 2026
8ce086f
fix(studio): pair white Advanced button hover with dark icon artwork too
masarray Oct 18, 2026
62baf84
test(studio): preserve dark icon/caption on light Advanced hover
masarray Oct 18, 2026
1ea4912
fix(studio): distribute vertical matrix height among four editable ch…
masarray Oct 18, 2026
4ea4b0a
test(studio): prevent blank-bottom Voltage/Current editor layout regr…
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
8 changes: 5 additions & 3 deletions apps/arstack_studio/qml/CalmButton.qml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Button {
property url iconSource
property real iconSize: 16
property string toolTipText: ""
readonly property string hoverPath: iconSource.toString().replace("/assets/material/", "/assets/material_dark/")

implicitHeight: theme ? theme.controlHeight : 36
implicitWidth: Math.max(64, contentItem.implicitWidth + 20)
Expand All @@ -28,6 +29,7 @@ Button {
tone === "accent" ? (hovered ? "#1d3c5e" : theme.accentSoft) :
(down ? "#1a2530" : hovered ? theme.raisedHover : "#141c25")
readonly property color activeEdge: !enabled ? theme.lineSoft :
hovered ? "#aac9e9" :
tone === "success" ? "#347a59" :
tone === "danger" ? "#86434b" :
tone === "accent" ? "#315f8d" :
Expand All @@ -39,15 +41,15 @@ Button {
tone === "accent" ? "#dcebff" : theme.textSoft

contentItem: Row {
// Both caption and icon belong to the same centered Control item.
spacing: control.iconSource.toString().length > 0 ? 7 : 0
// Center the complete icon+caption pair in both axes in Qt Controls.
anchors.centerIn: parent
spacing: control.iconSource.toString().length > 0 ? 7 : 0
Image {
visible: control.iconSource.toString().length > 0
width: control.iconSize
height: control.iconSize
anchors.verticalCenter: parent.verticalCenter
source: control.iconSource
source: control.hovered && control.enabled ? control.hoverPath : control.iconSource
sourceSize.width: control.iconSize * 2
sourceSize.height: control.iconSize * 2
opacity: control.enabled ? 0.94 : 0.30
Expand Down
4 changes: 2 additions & 2 deletions apps/arstack_studio/qml/DarkToolButton.qml
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ ToolButton {
visible: control.iconSource.toString().length === 0
anchors.fill: parent
text: control.text
color: control.enabled ? (control.hovered ? "#ffffff" : theme.textSoft) : theme.muted2
color: control.enabled ? (control.hovered ? "#142334" : theme.textSoft) : theme.muted2
font: control.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
background: Rectangle {
radius: 6
color: control.down ? theme.accentSoft : (control.hovered ? theme.raisedHover : "transparent")
color: control.down ? "#d6e6f9" : (control.hovered ? "#edf5ff" : "transparent")
border.width: control.activeFocus ? 1 : 0
border.color: theme.accent
}
Expand Down
9 changes: 5 additions & 4 deletions apps/arstack_studio/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -1080,10 +1080,10 @@ ApplicationWindow {

ColumnLayout {
Layout.fillWidth: true
// Use all available vertical workspace rather than
// a fixed 384px island with empty space below.
// Stack Voltage and Current vertically; allocate
// the available height between the two matrices.
Layout.fillHeight: true
Layout.preferredHeight: root.compactLayout ? 350 : 380
Layout.preferredHeight: root.compactLayout ? 350 : 450
// Allow 960x620 minimum viewport to shrink without
// clipping timing and quality rows; on larger
// viewports this column expands into spare space.
Expand Down Expand Up @@ -1171,7 +1171,8 @@ ApplicationWindow {
compact: root.compactLayout
}
}
Item { Layout.fillHeight: true }
// No trailing filler: vertically stacked matrices
// expand to use the otherwise blank injection space.
}
}

Expand Down
16 changes: 8 additions & 8 deletions apps/arstack_studio/qml/ModernRibbon.qml
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,23 @@ Rectangle {
spacing: 6

RowLayout {
Layout.preferredWidth: bar.compact ? 190 : 224
Layout.preferredWidth: bar.compact ? 220 : 270
Layout.alignment: Qt.AlignVCenter
spacing: 8

Rectangle {
Layout.preferredWidth: 48
Layout.preferredHeight: 48
width: 48
height: 48
Layout.preferredWidth: 56
Layout.preferredHeight: 56
width: 56
height: 56
radius: 10
color: "#0d151d"
border.width: 1
border.color: bar.theme.lineSoft
Image {
anchors.centerIn: parent
width: 34
height: 34
width: 44
height: 44
source: Qt.resolvedUrl("../assets/material/smv_injector.svg")
sourceSize.width: 68
sourceSize.height: 68
Expand All @@ -75,7 +75,7 @@ Rectangle {
text: "SMV Injector"
color: bar.theme.text
font.family: bar.uiFont
font.pixelSize: bar.compact ? 17 : 19
font.pixelSize: bar.compact ? 20 : 22
font.weight: Font.Bold
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
Expand Down
4 changes: 2 additions & 2 deletions apps/arstack_studio/qml/RibbonAction.qml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ Button {
: tone === "accent" || checked ? "#e1f1ff" : theme.textSoft
contentItem: ColumnLayout {
spacing: 4
// Controls own the content rectangle; centering is declarative
// on the layout itself, rather than anchoring Qt Controls content.
// Qt Quick Controls sizes the content rectangle; equal stretchers
// center the icon and caption vertically without anchor conflicts.
Item { Layout.fillHeight: true }
Image {
visible: control.iconSource.toString().length > 0
Expand Down
6 changes: 5 additions & 1 deletion apps/arstack_studio/qml/SignalMatrix.qml
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ Rectangle {
}

Layout.fillWidth: true
// Distribute spare workspace across editable channel rows;
// never leave a large dead zone under four fixed-height rows.
Layout.fillHeight: true
Layout.minimumHeight: 34
Layout.preferredHeight: matrix.compact ? 40 : 42
color: signalRow.selected ? "#101c27"
: (magnitudeField.activeFocus || phaseField.activeFocus) ? "#0f1922"
Expand Down Expand Up @@ -438,6 +442,6 @@ Rectangle {
}
}

Item { Layout.fillHeight: true }
// Channel delegates stretch into available height.
}
}
9 changes: 6 additions & 3 deletions apps/arstack_studio/qml/StudioDarkCheckBox.qml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ CheckBox {
id: control
property var theme
property string uiFont: "Inter"
implicitHeight: 30
implicitHeight: 32
// Keep one fixed indicator geometry at rest, during hover and focus.
// Native style decorations must not replace this explicit indicator.
background: Item { implicitWidth: 1; implicitHeight: 1 }
implicitWidth: Math.max(28, contentItem.implicitWidth + 30)
leftPadding: 1
rightPadding: 2
Expand All @@ -25,8 +28,8 @@ CheckBox {
radius: 5
color: control.checked ? control.theme.accent : control.theme.surface2
border.width: 1
border.color: control.checked || control.hovered ? control.theme.accent
: control.activeFocus ? control.theme.textSoft : control.theme.line
border.color: control.checked || control.hovered || control.activeFocus
? control.theme.accent : control.theme.line
Text {
anchors.centerIn: parent
visible: control.checked
Expand Down
5 changes: 3 additions & 2 deletions apps/arstack_studio/qml/StudioDarkComboBox.qml
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ ComboBox {
width: combo.width - 8
implicitHeight: 36
highlighted: combo.highlightedIndex === index
hoverEnabled: true
contentItem: Text {
text: combo.textAt(choice.index)
color: combo.theme.text
color: choice.highlighted || choice.hovered ? "#142334" : combo.theme.text
font: combo.font
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
Expand All @@ -63,7 +64,7 @@ ComboBox {
}
background: Rectangle {
radius: 5
color: parent.highlighted ? combo.theme.accentSoft : combo.theme.raised
color: choice.highlighted || choice.hovered ? "#edf5ff" : combo.theme.raised
}
}

Expand Down
58 changes: 53 additions & 5 deletions apps/arstack_studio/tests/check_operator_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ def ordered(data: str, first: str, second: str, reason: str) -> None:


def main() -> None:
# Lock the successful 120-second PTP/SV baseline in the operator CI.
# A GUI-only change must not silently restore the old free-running bundle.
project_root = STUDIO.parent.parent
firmware_profile = (project_root / ".github/workflows/esp32p4-smv-injector.yml").read_text(encoding="utf-8")
preview_profile = (project_root / ".github/workflows/arstack-studio-native-preview.yml").read_text(encoding="utf-8")
require("CONFIG_AR_SMV_PTP_PHASE_SERVO_EXPERIMENTAL=y" in firmware_profile
and "CONFIG_AR_SMV_HW_TX_EVIDENCE_EXPERIMENTAL=y" in firmware_profile
and "# CONFIG_AR_SMV_BROADCAST_MIRROR is not set" in firmware_profile
and "phase_sync_preview: true" in preview_profile,
"Studio native preview must ship measured PTP-SV AUTO firmware with no diagnostic mirror")
sv_gate = (project_root / "include/ariec61850/time_sync/sv_tx_egress_evidence.hpp").read_text(encoding="utf-8")
sv_tests = (project_root / "tests/test_sv_tx_egress_evidence.cpp").read_text(encoding="utf-8")
require("kMaxSoftwarePhaseBoundUs = 4U" in sv_gate
and "kMaxCanonicalTxOffsetNs = 250000LL" in sv_gate
and "check_auto_local_sync_gate_requires_both_clocks_and_fresh_mac" in sv_tests,
"retain independently bounded PTP sample-phase and nonzero MAC publication latency")
main_qml = source("Main.qml")
preview = source("SignalPreview.qml")
tray = source("StudioTimingTray.qml")
Expand Down Expand Up @@ -61,6 +77,10 @@ def main() -> None:
and "root.ptpDesiredForSession = false" in main_qml,
"PTP user intent restoration must be bounded, explicit and cancellable")
matrix = source("SignalMatrix.qml")
require("Layout.fillHeight: true" in matrix
and "Layout.minimumHeight: 34" in matrix
and "Item { Layout.fillHeight: true }" not in matrix,
"editable channel rows must consume vertical space without a blank filler")
require("function flushPendingEdits()" in matrix
and "magnitudeApplyTimer.stop()" in matrix
and "phaseApplyTimer.stop()" in matrix
Expand All @@ -73,15 +93,43 @@ def main() -> None:
and main_qml.index("id: voltageMatrix") < main_qml.index("id: currentMatrix"),
"Voltage and Current need a vertical, flexible-height editor workspace")
ribbon_qml = source("ModernRibbon.qml")
require("Layout.preferredWidth: bar.compact ? 190 : 224" in ribbon_qml
and "font.pixelSize: bar.compact ? 17 : 19" in ribbon_qml,
"SMV Injector logo/caption must be instrument-sized, not micro branding")
require("Layout.preferredWidth: bar.compact ? 220 : 270" in ribbon_qml
and "font.pixelSize: bar.compact ? 20 : 22" in ribbon_qml
and "width: 44" in ribbon_qml,
"SMV Injector branding must be readable at compact and normal widths")
device_controller = (STUDIO / "src/DeviceController.cpp").read_text(encoding="utf-8")
workflow = source("WorkflowBar.qml")
require("const scenario = controller.startScenarioSnapshot()" in workflow
and "smartSession.requestStartWithScenario(scenario)" in workflow
and "smartSession.requestStart()" not in workflow,
"every START must transmit all 8 visible setpoints, never reuse stale board angles")
require('QStringLiteral("IA"), QStringLiteral("IB")' in device_controller
and 'QStringLiteral("IC"), QStringLiteral("IN")' in device_controller
and 'QStringLiteral("UA"), QStringLiteral("UB")' in device_controller
and 'QStringLiteral("UC"), QStringLiteral("UN")' in device_controller
and 'commands << QStringLiteral("START");' in device_controller
and 'const auto phaseMdeg = std::llround(angleMdeg);' in device_controller,
"START transaction must retain every channel and signed millidegree")
require("anchors.centerIn: parent" in source("CalmButton.qml")
and "Text.AlignHCenter" in source("RibbonAction.qml"),
"control captions must center on both axes")
checkbox_qml = source("StudioDarkCheckBox.qml")
require("width: 20" in checkbox_qml and "height: 20" in checkbox_qml,
"checkbox geometry must remain unchanged by hover")
require("hovered ? \"#edf5ff\"" in source("CalmButton.qml")
and "hovered ? \"#142334\"" in source("CalmButton.qml"),
"light hover background must always pair with dark caption")
and "hovered ? \"#142334\"" in source("CalmButton.qml")
and "/assets/material_dark/" in source("CalmButton.qml"),
"light hover background must pair with dark caption and icon")
combo = source("StudioDarkComboBox.qml")
require('choice.highlighted || choice.hovered ? "#142334"' in combo
and 'choice.highlighted || choice.hovered ? "#edf5ff"' in combo,
"Advanced dropdown options must be readable on white hover")
dark_tool = source("DarkToolButton.qml")
require('control.hovered ? "#142334"' in dark_tool
and 'control.hovered ? "#edf5ff"' in dark_tool,
"Advanced text/icon tool buttons must use dark captions on white hover")
require("background: Item { implicitWidth: 1; implicitHeight: 1 }" in checkbox_qml,
"checkbox hover must not render a second platform-native indicator")

menu = source("StudioActionMenu.qml")
cmake = (STUDIO / "CMakeLists.txt").read_text(encoding="utf-8")
Expand Down
20 changes: 20 additions & 0 deletions docs/ARSTACK_SV_PTP_LOCK_BASELINE_20261018.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# ARStack Studio — SV/PTP non-regression baseline (2026-10-18)

## Frozen engineering evidence
Operator's latest 120s ASIX PCAP contained 480,294 canonical 4I+4V SV frames on APPID 0x4000, svID ARSTACK_SV01, 4,000 fps, VLAN 0 PCP 4; **zero** smpCnt discontinuities (120 modulo-4000 wraps), every frame smpSynch=1 LOCAL, and no broadcast diagnostic mirror. There were 120 each of PTP Announce, Sync and matched Follow_Up on domain 0, all from ARStack's own local SOURCE. Correlation between host-received smpCnt=0 frames and PTP timestamp epochs yielded ~+48.9 us median / ~+67.2 us p95 across 120 matched observations; these are **Windows/ASIX host RX offsets**, not actual hardware sampling phase error.

This is strong wire-level evidence for stable local-epoch alignment, **not** a certificate of external GM synchronization, cross-vendor acceptance, or microsecond hardware phase accuracy.

## CI release guardrails
- Native Windows Preview must embed firmware from its own exact commit with phase servo ON, canonical MAC TX timestamp evidence ON, and diagnostic mirror OFF. Never quietly fall back to the old free-running firmware.
- Preserve fail-closed AUTO=0 when clock/sample-phase evidence is absent; qualified SOURCE-local AUTO=1 is allowed; never imply AUTO=2 global time traceability without evidence.
- Independently measure GPTimer sample-epoch phase (4us implementation threshold) and MAC SV transmission publication latency (nonzero is expected, admission range currently [-4us,+250us]); do not conflate the two.
- Never modify canonical EtherType, APPID 0x4000, svID, VLAN, confRev, ASDU 4I+4V payload, sample-counter continuity or PTP profile as collateral of UI work.
- Every START must use a fresh snapshot of all eight displayed waveform setpoints: IA IB IC IN UA UB UC UN, preserving signed angle millidegrees, magnitude scaling, enable flags and quality. A bare START that can reuse old board setpoints is forbidden.
- GUI angles are user setpoints, not independent firmware readback. **Do not claim actual phase injection matches until a same-run SET transaction, board SHOW phase_mdeg, and decoded SV waveform agree**.
- Default PTP Source/AUTO, Phasor ON and Wave ON; the operator should not encounter additional dialog gates for normal PTP ON and SMV Start.

## Follow-up acceptance
With GUI Ia=-30°, Ib=-150°, Ic=+90°, capture a new immediately adjacent SV waveform *after* starting with that exact table. Verify the board's per-channel phase_mdeg (e.g. IA=-30000), then compare LS-fitted raw SV instantaneous waveform phase to the requested values. Repeat after STOP/START and reconnect. Verify PTP AUTO local acquisition and smpCnt continuity remain intact.

This baseline intentionally does not claim SIPROTEC 5 lock or IED compatibility without actual subscriber evidence.
Loading