Skip to content
Draft
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
5057316
feat(browser): add canonical read-only Signal Catalog for SCL and dis…
masarray Sep 27, 2026
833e1a4
build(qml): include canonical Signal Catalog in workbench module
masarray Sep 27, 2026
df22ef6
feat(browser): unify canonical DataSet catalog selection across Open …
masarray Sep 27, 2026
a546e14
feat(browser): expose Signal Catalog action and route DataSets throug…
masarray Sep 27, 2026
eb0329b
feat(browser): navigate canonical DataSets offline and route live sel…
masarray Sep 27, 2026
fbe879a
feat(browser): inspect ordered canonical DataSet signals offline or o…
masarray Sep 27, 2026
a2ebb68
fix(qml): normalize IEC reference FC suffix with proper regex escaping
masarray Sep 27, 2026
d7f5dca
test(browser): enrich rendered offline fixture with ordered static Da…
masarray Sep 27, 2026
55286a9
test(browser): expose rendered ordered DataSet member list for QA
masarray Sep 27, 2026
26979bb
test(browser): assert canonical offline DataSet projection and explic…
masarray Sep 27, 2026
918e3d9
test(browser): add R1A canonical Signal Catalog and safety contract
masarray Sep 27, 2026
0429bbc
test(browser): expose first rendered DataSet member text
masarray Sep 27, 2026
57c1658
test(browser): require rendered offline canonical DataSet member in f…
masarray Sep 27, 2026
2e66813
ci(browser): gate R1A catalog source contract and rendered offline Da…
masarray Sep 27, 2026
6456880
test(browser): verify normalized canonical reference lookup in DataSe…
masarray Sep 27, 2026
a389e94
fix(browser): normalize DataSet FC labels for explicit Global Data MM…
masarray Sep 27, 2026
2359103
fix(qml): bind nested DataSet member text explicitly from required de…
masarray Sep 27, 2026
7d7cc5e
test(browser): allow one render frame for virtualized ordered DataSet…
masarray Sep 27, 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
9 changes: 9 additions & 0 deletions .github/workflows/ied-simulator-qt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,15 @@ jobs:
apps/ied_simulator/qml/MmsClientWorkspace.qml
apps/ied_simulator/qml/BrowserDataSetPane.qml

- name: R1A canonical Signal Catalog UX and safety contract
run: >-
python3 apps/ied_simulator/test_browser_signal_catalog.py
apps/ied_simulator/qml/IedBrowserWorkspace.qml
apps/ied_simulator/qml/IedBrowserNavigation.qml
apps/ied_simulator/qml/BrowserDataSetPane.qml
apps/ied_simulator/qml/BrowserSignalCatalog.qml
apps/ied_simulator/src/main.cpp

- name: Canonical Browser service reuse contract
run: >-
python3 apps/ied_simulator/test_browser_service_reuse.py
Expand Down
1 change: 1 addition & 0 deletions apps/ied_simulator/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ qt_add_qml_module(arstack_ied_simulator
qml/IedBrowserNavigation.qml
qml/BrowserStatusConsole.qml
qml/BrowserDataSetPane.qml
qml/BrowserSignalCatalog.qml
qml/BrowserGoosePane.qml
qml/BrowserGlobalDataPane.qml
qml/FleetGlobalDataPane.qml
Expand Down
48 changes: 37 additions & 11 deletions apps/ied_simulator/qml/BrowserDataSetPane.qml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Rectangle {
required property var reports
required property var context
required property var client
required property var dataSetCatalog
required property string selectedDataSetReference

property int valueRevision: 0

Expand All @@ -20,15 +22,37 @@ Rectangle {
return value === undefined || value === null || String(value).length === 0 ? "—" : String(value)
}

function memberReference(member) {
// The canonical catalog shows FC as evidence, not as part of its
// DataAttribute key. Preserve the displayed member order and label.
return String(member || "").replace(/\s+\[[^\]]+\]$/, "")
}

function memberFc(member) {
var matched = String(member || "").match(/\[([^\]]+)\]$/)
return matched ? matched[1] : ""
}

function resolvedNode(reference) {
root.valueRevision
return context.treeModel.nodeForReference(reference)
return context.treeModel.nodeForReference(root.memberReference(reference))
}

readonly property var selectedDataSet: reports && reports.dataSets
&& reports.selectedDataSetIndex >= 0
&& reports.selectedDataSetIndex < reports.dataSets.length
? (reports.dataSets[reports.selectedDataSetIndex] || null) : null
readonly property var selectedDataSet: {
for (var i = 0; i < dataSetCatalog.length; ++i) {
if (dataSetCatalog[i].reference === selectedDataSetReference)
return dataSetCatalog[i]
}
return null
}
readonly property var selectedDataSetMembers: selectedDataSet
? (selectedDataSet.members || []) : []
readonly property var readableMemberReferences: {
var result = []
for (var i = 0; i < selectedDataSetMembers.length; ++i)
result.push(root.memberReference(selectedDataSetMembers[i]))
return result
}

function hasValue(value) {
return value !== undefined && value !== null && String(value).length > 0
Expand Down Expand Up @@ -109,13 +133,13 @@ Rectangle {
theme: root.theme
text: root.client.operationBusy ? "Reading…" : "Read members"
enabled: root.client.connected && !root.client.operationBusy
&& reports.selectedDataSetMembers.length > 0
onClicked: root.client.refreshEngineeringReferences(reports.selectedDataSetMembers)
&& root.readableMemberReferences.length > 0
onClicked: root.client.refreshEngineeringReferences(root.readableMemberReferences)
ToolTip.visible: hovered
ToolTip.text: "Read the ordered DataSet members through the canonical model. Requests remain bounded by the client controller."
}
Label {
text: String(reports.selectedDataSetMembers.length)
text: String(root.selectedDataSetMembers.length)
color: theme.muted
font.pixelSize: theme.captionSize
}
Expand Down Expand Up @@ -143,11 +167,12 @@ Rectangle {

ListView {
id: memberList
objectName: "iedBrowserDataSetMembers"
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
reuseItems: true
model: reports.selectedDataSetMembers
model: root.selectedDataSetMembers
spacing: 1
ScrollBar.vertical: ScrollBar {}

Expand All @@ -173,15 +198,16 @@ Rectangle {
font.pixelSize: theme.captionSize
}
Label {
objectName: "iedBrowserDataSetMember_" + memberRow.index
Layout.fillWidth: true
text: modelData
text: memberRow.modelData
color: theme.text
font.pixelSize: theme.labelSize
elide: Text.ElideMiddle
}
Label {
Layout.preferredWidth: 48
text: memberRow.resolved.functionalConstraint || ""
text: memberRow.resolved.functionalConstraint || root.memberFc(modelData)
color: theme.muted
font.pixelSize: theme.captionSize
}
Expand Down
4 changes: 3 additions & 1 deletion apps/ied_simulator/qml/BrowserGlobalDataPane.qml
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@ Rectangle {
candidates = row.membersText.split("\n")
}
for (var j = 0; j < candidates.length; ++j) {
var reference = candidates[j]
// FC is display evidence in a DataSet member, not part of
// the canonical read key. Keep the watchlist text unchanged.
var reference = String(candidates[j]).replace(/\s+\[[^\]]+\]$/, "")
if (reference.length === 0 || seen[reference])
continue
seen[reference] = true
Expand Down
163 changes: 163 additions & 0 deletions apps/ied_simulator/qml/BrowserSignalCatalog.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

// Read-only entry point for both Open SCL and live MMS discovery. The catalog
// derives solely from the canonical engineering context; it never creates a
// DataSet, connects another association or enables reporting implicitly.
Dialog {
id: root
objectName: "iedSignalCatalog"
required property var theme
required property var context
signal dataSetRequested(string reference)
signal browseRequested()

title: "Signal Catalog"
modal: true
width: Math.min(650, Overlay.overlay ? Overlay.overlay.width - 32 : 650)
height: Math.min(520, Overlay.overlay ? Overlay.overlay.height - 32 : 520)
anchors.centerIn: Overlay.overlay
standardButtons: Dialog.Close

readonly property var canonicalDataSets: context.loaded ? context.dataSets : []

contentItem: ColumnLayout {
spacing: 10

Label {
Layout.fillWidth: true
text: root.context.iedName + " · " + root.context.authority
color: root.theme.text
font.pixelSize: root.theme.labelSize
font.weight: Font.DemiBold
elide: Text.ElideMiddle
}
Label {
Layout.fillWidth: true
text: "Choose exact DataSet members or browse the complete canonical model."
color: root.theme.textSoft
font.pixelSize: root.theme.captionSize
wrapMode: Text.WordWrap
}
RowLayout {
Layout.fillWidth: true
spacing: 8
Button {
objectName: "iedCatalogDatasetSignals"
text: "Dataset Signals"
enabled: root.canonicalDataSets.length > 0
onClicked: {
root.dataSetRequested(root.canonicalDataSets[0].reference)
root.close()
}
}
Button {
objectName: "iedCatalogBrowseSignals"
text: "Browse All Signals"
onClicked: {
root.browseRequested()
root.close()
}
}
Item { Layout.fillWidth: true }
Label {
text: root.canonicalDataSets.length + " DataSet(s)"
color: root.theme.muted
font.pixelSize: root.theme.captionSize
}
}

Rectangle { Layout.fillWidth: true; Layout.preferredHeight: 1; color: root.theme.lineSoft }
Label {
text: "DATASET SIGNALS · ORDERED IED MEMBERSHIP"
color: root.theme.muted
font.pixelSize: root.theme.captionSize
font.weight: Font.DemiBold
}

ListView {
id: dataSetsView
objectName: "iedSignalCatalogDataSets"
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
reuseItems: true
model: root.canonicalDataSets
spacing: 2
ScrollBar.vertical: ScrollBar {}

delegate: Rectangle {
required property int index
required property var modelData
width: ListView.view.width
height: 48
radius: 5
color: dsMouse.containsMouse ? root.theme.surfaceRaised : root.theme.surface
border.width: 1
border.color: root.theme.lineSoft
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 8
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Label {
Layout.fillWidth: true
text: modelData.reference || "DataSet"
color: root.theme.text
font.pixelSize: root.theme.labelSize
elide: Text.ElideMiddle
}
Label {
Layout.fillWidth: true
text: modelData.usedByReports && modelData.usedByReports.length
? "RCB: " + modelData.usedByReports.join(", ")
: "RCB binding not identified in model"
color: root.theme.muted
font.pixelSize: root.theme.captionSize
elide: Text.ElideMiddle
}
}
Label {
text: (modelData.memberCount || 0) + " members"
color: root.theme.textSoft
font.pixelSize: root.theme.captionSize
}
Label {
text: "›"
color: root.theme.accent
font.pixelSize: root.theme.labelSize
}
}
MouseArea {
id: dsMouse
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.dataSetRequested(modelData.reference)
root.close()
}
}
}

Label {
anchors.centerIn: parent
visible: dataSetsView.count === 0
text: "No DataSets in this canonical model. Browse All Signals instead."
color: root.theme.muted
font.pixelSize: root.theme.labelSize
}
}
Label {
Layout.fillWidth: true
text: "Viewing never changes IED membership or writes an RCB. Report enable and GI remain explicit actions."
color: root.theme.muted
font.pixelSize: root.theme.captionSize
wrapMode: Text.WordWrap
}
}
}
13 changes: 8 additions & 5 deletions apps/ied_simulator/qml/IedBrowserNavigation.qml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Rectangle {
required property var context
required property var reports
required property var utilities
required property var dataSetCatalog
required property string selectedDataSetReference

signal dataSetRequested(string reference)

property int section: 0
property int globalDataCount: 0
Expand Down Expand Up @@ -301,19 +305,19 @@ Rectangle {

Loader {
Layout.fillWidth: true
active: root.section === 1 && reports.dataSets.length > 0
active: root.section === 1 && root.dataSetCatalog.length > 0
visible: active
sourceComponent: Component {
ColumnLayout {
spacing: 0
Repeater {
model: reports.dataSets
model: root.dataSetCatalog
delegate: Rectangle {
required property int index
required property var modelData
Layout.fillWidth: true
implicitHeight: 30
color: reports.selectedDataSetIndex === index
color: root.selectedDataSetReference === modelData.reference
? root.theme.surfaceRaised : dsMouse.containsMouse ? root.theme.surface : "transparent"
RowLayout {
anchors.fill: parent
Expand All @@ -338,8 +342,7 @@ Rectangle {
anchors.fill: parent
hoverEnabled: true
onClicked: {
reports.selectDataSet(index)
root.choose(1, "DataSets")
root.dataSetRequested(modelData.reference)
}
}
}
Expand Down
Loading
Loading