Skip to content
Merged
178 changes: 177 additions & 1 deletion apps/ied_simulator/qml/BrowserGoosePane.qml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,42 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ARStack.IedSimulator 1.0

Rectangle {
id: root
required property var theme
required property var context
required property GooseMonitorController monitor

// One shared monitor is owned by Main.qml/Sniffer. This pane never
// starts an independent raw socket, MMS Read, or background poller.
property int evidenceGeneration: 0
Connections {
target: root.monitor
function onMonitorChanged() { root.evidenceGeneration++ }
}
property var observation: {
const generation = root.evidenceGeneration
if (root.selectedIndex < 0 || !root.selectedStream.reference)
return ({matched: false, reason: "Select a configured GOOSE stream"})
return root.monitor.observedForConfiguredStream(root.selectedStream)
}

// A matching frame is not evidence that the configured FCDA order is
// unchanged. Only align member names when ConfRev and counts agree.
property bool modelAligned: {
if (!root.observation.matched || !root.selectedStream.members ||
root.selectedStream.members.length === 0 ||
root.selectedStream.members.length !== root.observation.valueCount)
return false
const configured = root.selectedStream.confRev
if (configured === undefined || configured === null || String(configured).length === 0)
return false
const expected = Number(configured)
return isFinite(expected) && Math.floor(expected) === expected && expected >= 0
&& expected === Number(root.observation.confRev)
}

property var visibleStreams: context.gooseStreams
property int selectedIndex: -1
Expand Down Expand Up @@ -148,6 +179,120 @@ Rectangle {

Rectangle { Layout.fillWidth: true; Layout.preferredHeight: 1; color: theme.lineSoft }

Rectangle {
Layout.fillWidth: true
implicitHeight: subscriptionPanel.implicitHeight + 16
color: theme.surface
border.width: 1
border.color: root.observation.matched
? (root.observation.timedOut || !root.monitor.capturing ? theme.amber : theme.green)
: theme.line

ColumnLayout {
id: subscriptionPanel
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 8
spacing: 6

RowLayout {
Layout.fillWidth: true
Label {
text: "GOOSE Subscribe · read-only observation"
color: theme.text
font.pixelSize: 10
font.weight: Font.DemiBold
}
Item { Layout.fillWidth: true }
Label {
text: root.monitor.capturing ? "CAPTURING" : "STOPPED"
color: root.monitor.capturing ? theme.green : theme.muted
font.pixelSize: 8
}
}

RowLayout {
Layout.fillWidth: true
Label {
text: "NIC"
color: theme.muted
font.pixelSize: 9
}
ComboBox {
id: gooseNicSelector
Layout.fillWidth: true
model: root.monitor.interfaces
enabled: !root.monitor.capturing
currentIndex: root.monitor.interfaces.indexOf(root.monitor.interfaceName)
Comment thread
masarray marked this conversation as resolved.
displayText: currentIndex < 0 ? "Select capture interface" : currentText
onActivated: root.monitor.interfaceName = currentText
}
Button {
text: "Refresh NICs"
enabled: !root.monitor.capturing
onClicked: root.monitor.refreshInterfaces()
}
Button {
text: root.monitor.capturing ? "Stop" : "GOOSE Subscribe"
enabled: root.monitor.capturing || root.monitor.interfaceName.length > 0
onClicked: root.monitor.capturing
? root.monitor.stopCapture()
: root.monitor.startCapture()
ToolTip.visible: hovered
ToolTip.text: "Starts/stops the shared passive Ethernet capture; does not send GOOSE or MMS commands."
}
}

Label {
Layout.fillWidth: true
text: root.observation.matched
? (root.observation.reason + " · " + root.observation.appIdText
+ " · st/sq " + root.observation.stNum + "/" + root.observation.sqNum
+ " · frames " + root.observation.packets)
: (root.monitor.capturing
? "Waiting for matching Ethernet GOOSE · " + root.observation.reason
: "No live capture · select NIC and press GOOSE Subscribe")
color: root.observation.matched && !root.observation.timedOut
&& root.monitor.capturing ? theme.green : theme.textSoft
font.pixelSize: 9
wrapMode: Text.WordWrap
}
Label {
Layout.fillWidth: true
visible: root.observation.matched &&
root.selectedStream.members &&
root.selectedStream.members.length !== root.observation.valueCount
text: "DataSet count mismatch · configured "
+ root.selectedStream.members.length
+ " · observed " + root.observation.valueCount
+ " (do not assume aligned signals)"
color: theme.amber
font.pixelSize: 9
wrapMode: Text.WordWrap
}
Label {
Layout.fillWidth: true
visible: root.observation.matched && !root.modelAligned
text: "Raw values are not assigned to configured signals until "
+ "ConfRev and DataSet member count agree. "
+ "Configured ConfRev=" + root.text(root.selectedStream.confRev)
+ " · observed=" + root.observation.confRev
color: theme.amber
font.pixelSize: 9
wrapMode: Text.WordWrap
}
Label {
Layout.fillWidth: true
text: root.monitor.statusText
color: theme.muted
font.pixelSize: 8
wrapMode: Text.WordWrap
}
}
}

Rectangle { Layout.fillWidth: true; Layout.preferredHeight: 1; color: theme.lineSoft }

Rectangle {
Layout.fillWidth: true
implicitHeight: gooseAttributes.implicitHeight
Expand Down Expand Up @@ -272,13 +417,44 @@ Rectangle {
font.pixelSize: 9
elide: Text.ElideMiddle
}
Label {
Layout.preferredWidth: 175
visible: root.observation.matched
text: root.modelAligned && index < root.observation.values.length
? root.observation.values[index]
: "Unresolved · count mismatch"
color: root.observation.timedOut || !root.monitor.capturing
? theme.amber : theme.green
font.pixelSize: 9
elide: Text.ElideRight
}
}
}
}

Label {
Layout.fillWidth: true
text: "Engineering-model evidence only. Live Ethernet capture remains in Sniffer; publication remains in IED Simulator."
visible: root.observation.matched && !root.modelAligned
text: "Unbound wire allData (ordered, no guessed signal names)"
color: theme.textSoft
font.pixelSize: 9
}
Repeater {
model: root.observation.matched && !root.modelAligned
? root.observation.values : []
delegate: Label {
required property string modelData
Layout.fillWidth: true
text: modelData
color: theme.text
font.pixelSize: 9
elide: Text.ElideRight
}
}

Label {
Layout.fillWidth: true
text: "Observed values come from the shared passive Sniffer, bound only by exact GOOSE identity. No GOOSE publish, MMS writes or implicit reads."
color: theme.muted
font.pixelSize: 8
wrapMode: Text.WordWrap
Expand Down
14 changes: 9 additions & 5 deletions apps/ied_simulator/qml/GooseWorkspace.qml
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,16 @@ Item {
Layout.preferredWidth: 240
model: root.monitor.interfaces
enabled: !root.monitor.capturing
currentIndex: Math.max(0, root.monitor.interfaces.indexOf(root.monitor.interfaceName))
currentIndex: root.monitor.interfaces.indexOf(root.monitor.interfaceName)
displayText: currentIndex < 0 ? "Select capture interface" : currentText
onActivated: root.monitor.interfaceName = currentText
Component.onCompleted: {
if (root.monitor.interfaceName.length === 0 && count > 0)
root.monitor.interfaceName = currentText
}
}

ActionButton {
theme: root.theme
text: "Refresh NICs"
enabled: !root.monitor.capturing
onClicked: root.monitor.refreshInterfaces()
}

ActionButton {
Expand Down
4 changes: 3 additions & 1 deletion apps/ied_simulator/qml/IedBrowserWorkspace.qml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Item {
required property var session
required property var client
required property var context
required property var gooseBackend
required property var reports
required property var utilities
required property var controls
Expand Down Expand Up @@ -732,7 +733,7 @@ Item {

Label {
visible: root.activeSection === 5
text: "Configured model view"
text: root.gooseBackend.capturing ? "Read-only GOOSE capture active" : "GOOSE capture idle"
color: root.theme.muted
font.pixelSize: 8
}
Expand Down Expand Up @@ -797,6 +798,7 @@ Item {
id: goosePane
theme: root.theme
context: root.context
monitor: root.gooseBackend
selectedIndex: root.selectedGooseIndex
}

Expand Down
1 change: 1 addition & 0 deletions apps/ied_simulator/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ ApplicationWindow {
session: root.browserFleet.sessionAt(index)
client: root.browserFleet.clientAt(index)
context: root.browserFleet.contextAt(index)
gooseBackend: gooseMonitor
reports: root.browserFleet.reportsAt(index)
utilities: root.browserFleet.utilitiesAt(index)
controls: root.browserFleet.controlsAt(index)
Expand Down
Loading
Loading