From 9e25398110d31cbf4b16918e2221ed1915fb6c4f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 10:36:26 +0700 Subject: [PATCH 1/5] feat(workbench): align All-IED live grid to ARSAS with canonical q/t and honest read state (M2K) Expose Quality and IED Timestamp columns and bounded cross-IED search, reuse per-IED canonical watchlists and event-driven incremental model updates. Differentiate readable vs actually sampled Data Attributes (including legitimate empty value samples) inside the existing MMS tree. Add SSOT/large-model/source contract gates. --- .../qml/BrowserDataModelPane.qml | 3 + .../qml/BrowserGlobalDataPane.qml | 7 +- .../ied_simulator/qml/FleetGlobalDataPane.qml | 170 ++++++++++++++---- apps/ied_simulator/src/MmsLiveTreeModel.cpp | 5 +- apps/ied_simulator/src/MmsLiveTreeModel.hpp | 1 + .../src/ied_browser_large_model_qa.cpp | 13 ++ .../ied_simulator/test_browser_convergence.py | 28 +++ 7 files changed, 191 insertions(+), 36 deletions(-) diff --git a/apps/ied_simulator/qml/BrowserDataModelPane.qml b/apps/ied_simulator/qml/BrowserDataModelPane.qml index 496cdd527..c6f616245 100644 --- a/apps/ied_simulator/qml/BrowserDataModelPane.qml +++ b/apps/ied_simulator/qml/BrowserDataModelPane.qml @@ -264,6 +264,9 @@ Rectangle { model: [ {label: "Reference", value: root.currentNode.reference}, {label: "Value", value: root.currentNode.value}, + {label: "Read state", value: root.currentNode.kind === "DA" + ? (root.currentNode.hasReceivedValue === true ? "Received" : "Not read") + : ""}, {label: "Quality", value: root.currentNode.quality}, {label: "Timestamp", value: root.currentNode.timestamp}, {label: "Functional constraint", value: root.currentNode.functionalConstraint}, diff --git a/apps/ied_simulator/qml/BrowserGlobalDataPane.qml b/apps/ied_simulator/qml/BrowserGlobalDataPane.qml index 9192f2763..6e20d91f7 100644 --- a/apps/ied_simulator/qml/BrowserGlobalDataPane.qml +++ b/apps/ied_simulator/qml/BrowserGlobalDataPane.qml @@ -189,7 +189,12 @@ Rectangle { watchModel.setProperty(i, "value", node.value || "") watchModel.setProperty(i, "quality", node.quality || "") watchModel.setProperty(i, "timestamp", node.timestamp || "") - watchModel.setProperty(i, "status", context.online ? (node.readable ? "LIVE" : "UNRESOLVED") : "OFFLINE") + // Structural reachability is not a successful MMS sample. + // Legitimate empty-string values still count as received via the + // canonical model's explicit sample-ingress bit. + watchModel.setProperty(i, "status", !context.online ? "OFFLINE" + : !node.readable ? "UNRESOLVED" + : node.hasReceivedValue === true ? "LIVE" : "NOT READ") } else if (row.kind === "DATASET") { var members = row.membersText.length > 0 ? row.membersText.split("\n").length : 0 watchModel.setProperty(i, "status", context.online ? "READY" : "OFFLINE") diff --git a/apps/ied_simulator/qml/FleetGlobalDataPane.qml b/apps/ied_simulator/qml/FleetGlobalDataPane.qml index 94b792d50..41086da5c 100644 --- a/apps/ied_simulator/qml/FleetGlobalDataPane.qml +++ b/apps/ied_simulator/qml/FleetGlobalDataPane.qml @@ -23,35 +23,99 @@ Rectangle { ListModel { id: rows } + // Presentation only. Each IED retains its canonical watchlist, MMS + // association, and report authority; this pane never owns measurements. + property string filterText: "" + property int sourceCount: 0 + readonly property int maximumPreviewLength: 256 + + function preview(value) { + const text = value === undefined || value === null ? "" : String(value) + return text.length <= maximumPreviewLength ? text + : text.slice(0, maximumPreviewLength - 1) + "…" + } + + function requestRefresh() { + // Event-driven coalescing: a multi-value MMS update is one UI refresh. + Qt.callLater(root.refreshRows) + } + function refreshRows() { - rows.clear() - for (var index = 0; index < fleet.workspaceCount; ++index) { - var view = browserViews.itemAt(index) - var context = fleet.contextAt(index) - if (!view || !context) - continue - var entries = view.watchedRows() - for (var i = 0; i < entries.length; ++i) { - if (rows.count >= maximumRows) { - statusMessage = "Combined watchlist bound reached (2048); no more rows retained." - return - } - var row = entries[i] - rows.append({ + const next = [] + const needle = filterText.trim().toLowerCase() + let sourceTotal = 0 + let truncated = false + for (let index = 0; index < fleet.workspaceCount; ++index) { + const view = browserViews.itemAt(index) + const context = fleet.contextAt(index) + if (!view || !context) continue + const entries = view.watchedRows() + const ied = context.iedName || ("IED " + String(index + 1)) + for (let i = 0; i < entries.length; ++i) { + const row = entries[i] + ++sourceTotal + const item = { "slotIndex": index, - "iedName": context.iedName || ("IED " + String(index + 1)), + "iedName": ied, "kind": row.kind || "", "reference": row.reference || "", - "detail": row.detail || "", - "value": row.value || "", - "quality": row.quality || "", - "timestamp": row.timestamp || "", + "detail": preview(row.detail), + "value": preview(row.value), + "quality": preview(row.quality), + "timestamp": preview(row.timestamp), "status": row.status || "OFFLINE" - }) + } + // Search a bounded presentation preview, not a potentially + // megabyte-long MMS scalar or a second indexing/cache system. + if (needle.length > 0 && + [item.iedName, item.kind, preview(item.reference), + item.detail, item.value, item.quality, item.timestamp, + item.status].join(" ").toLowerCase().indexOf(needle) < 0) + continue + if (next.length >= maximumRows) { + truncated = true + continue + } + next.push(item) + } + } + + sourceCount = sourceTotal + // Keep virtualized delegates and scroll position stable for frequent + // real MMS value updates. Structural change is the only reset path. + let stable = rows.count === next.length + if (stable) { + for (let i = 0; i < next.length; ++i) { + const old = rows.get(i) + if (old.slotIndex !== next[i].slotIndex || + old.kind !== next[i].kind || + old.reference !== next[i].reference) { + stable = false + break + } + } + } + if (stable) { + const dynamicRoles = ["iedName", "detail", "value", "quality", "timestamp", "status"] + for (let i = 0; i < next.length; ++i) { + const before = rows.get(i) + for (let k = 0; k < dynamicRoles.length; ++k) { + const role = dynamicRoles[k] + if (before[role] !== next[i][role]) + rows.setProperty(i, role, next[i][role]) + } } + } else { + rows.clear() + for (let i = 0; i < next.length; ++i) + rows.append(next[i]) } - statusMessage = String(rows.count) + " entries from " - + String(fleet.workspaceCount) + " separate IED workspace(s)." + statusMessage = truncated + ? "Visible result limit reached (2048). Refine search." + : needle.length > 0 + ? String(rows.count) + " matches across " + String(sourceTotal) + " watched entries." + : String(sourceTotal) + " watched entries from " + + String(fleet.workspaceCount) + " independent IED(s)." } function refreshSelected() { @@ -68,7 +132,7 @@ Rectangle { Connections { target: fleet - function onWorkspacesChanged() { root.refreshRows() } + function onWorkspacesChanged() { root.requestRefresh() } } ColumnLayout { @@ -106,9 +170,29 @@ Rectangle { } } + TextField { + id: signalSearch + objectName: "fleetGlobalDataSearch" + Layout.preferredWidth: 215 + Layout.maximumWidth: 270 + maximumLength: 128 + placeholderText: "Search IED, signal, value, quality…" + selectByMouse: true + onTextChanged: { + root.filterText = text + root.requestRefresh() + } + ToolTip.visible: hovered + ToolTip.text: "Filter this view only. Never changes watched signals or starts MMS traffic." + } + Button { + text: "Clear" + visible: signalSearch.length > 0 + onClicked: signalSearch.clear() + } ComboBox { id: selectedIed - Layout.preferredWidth: 185 + Layout.preferredWidth: 160 model: fleet textRole: "label" } @@ -146,11 +230,13 @@ Rectangle { anchors.leftMargin: 12 anchors.rightMargin: 12 spacing: 9 - Label { Layout.preferredWidth: 110; text: "IED"; color: theme.muted; font.pixelSize: 8 } - Label { Layout.preferredWidth: 70; text: "TYPE"; color: theme.muted; font.pixelSize: 8 } - Label { Layout.fillWidth: true; text: "CANONICAL REFERENCE"; color: theme.muted; font.pixelSize: 8 } - Label { Layout.preferredWidth: 150; text: "VALUE"; color: theme.muted; font.pixelSize: 8 } - Label { Layout.preferredWidth: 90; text: "STATUS"; color: theme.muted; font.pixelSize: 8 } + Label { Layout.preferredWidth: 96; text: "IED"; color: theme.muted; font.pixelSize: 8 } + Label { Layout.preferredWidth: 58; text: "TYPE"; color: theme.muted; font.pixelSize: 8 } + Label { Layout.fillWidth: true; text: "IEC REFERENCE"; color: theme.muted; font.pixelSize: 8 } + Label { Layout.preferredWidth: 120; text: "VALUE"; color: theme.muted; font.pixelSize: 8 } + Label { Layout.preferredWidth: 110; text: "QUALITY"; color: theme.muted; font.pixelSize: 8 } + Label { Layout.preferredWidth: 145; text: "IED TIMESTAMP"; color: theme.muted; font.pixelSize: 8 } + Label { Layout.preferredWidth: 90; text: "STATE"; color: theme.muted; font.pixelSize: 8 } } } @@ -186,14 +272,14 @@ Rectangle { anchors.rightMargin: 12 spacing: 9 Label { - Layout.preferredWidth: 110 + Layout.preferredWidth: 96 text: iedName color: theme.text font.pixelSize: 8 elide: Text.ElideRight } Label { - Layout.preferredWidth: 70 + Layout.preferredWidth: 58 text: kind color: theme.muted font.pixelSize: 8 @@ -206,12 +292,26 @@ Rectangle { elide: Text.ElideMiddle } Label { - Layout.preferredWidth: 150 - text: value + Layout.preferredWidth: 120 + text: value.length ? value : "—" color: theme.text font.pixelSize: 8 elide: Text.ElideRight } + Label { + Layout.preferredWidth: 110 + text: quality.length ? quality : "—" + color: quality.length ? theme.textSoft : theme.muted + font.pixelSize: 8 + elide: Text.ElideRight + } + Label { + Layout.preferredWidth: 145 + text: timestamp.length ? timestamp : "—" + color: timestamp.length ? theme.textSoft : theme.muted + font.pixelSize: 8 + elide: Text.ElideRight + } Label { Layout.preferredWidth: 90 text: status @@ -235,7 +335,9 @@ Rectangle { anchors.centerIn: parent visible: rows.count === 0 width: Math.max(250, parent.width - 80) - text: "No watched signals yet. Open an IED tab and add DataAttributes, DataSets, Reports, or GOOSE to its Global Data." + text: root.filterText.length > 0 + ? "No watched signals match this search. Clear the filter to see all IEDs." + : "No watched signals yet. Open an IED tab and add DataAttributes, DataSets, Reports, or GOOSE to its Global Data." color: theme.muted font.pixelSize: 9 horizontalAlignment: Text.AlignHCenter diff --git a/apps/ied_simulator/src/MmsLiveTreeModel.cpp b/apps/ied_simulator/src/MmsLiveTreeModel.cpp index 7b6e92ac7..644fdaea9 100644 --- a/apps/ied_simulator/src/MmsLiveTreeModel.cpp +++ b/apps/ied_simulator/src/MmsLiveTreeModel.cpp @@ -344,6 +344,7 @@ QVariantMap MmsLiveTreeModel::nodeMap(const int nodeIndex) const { } } result.insert(QStringLiteral("value"), displayValue); + result.insert(QStringLiteral("hasReceivedValue"), node.hasReceivedValue); result.insert(QStringLiteral("engineeringEvidence"), node.engineeringEvidence); result.insert(QStringLiteral("hasChildren"), !node.children.isEmpty()); result.insert(QStringLiteral("childCount"), node.children.size()); @@ -667,7 +668,9 @@ void MmsLiveTreeModel::applyReadValue( if (found == mmsIndex_.cend()) return; const auto nodeIndex = found.value(); auto& node = nodes_[nodeIndex]; - if (node.value == displayValue && node.engineeringEvidence == engineeringEvidence) return; + if (node.hasReceivedValue && node.value == displayValue && + node.engineeringEvidence == engineeringEvidence) return; + node.hasReceivedValue = true; node.value = displayValue; // Evidence is immutable per received sample; a later untyped update must // clear previous bit/clock details rather than reuse stale measurements. diff --git a/apps/ied_simulator/src/MmsLiveTreeModel.hpp b/apps/ied_simulator/src/MmsLiveTreeModel.hpp index 7f433fe20..52f61314d 100644 --- a/apps/ied_simulator/src/MmsLiveTreeModel.hpp +++ b/apps/ied_simulator/src/MmsLiveTreeModel.hpp @@ -187,6 +187,7 @@ class MmsLiveTreeModel : public QAbstractListModel { QString typeStatus; QString description; QString value; + bool hasReceivedValue{}; // updated only at actual MMS sample ingress QVariantMap engineeringEvidence; bool writable{}; }; diff --git a/apps/ied_simulator/src/ied_browser_large_model_qa.cpp b/apps/ied_simulator/src/ied_browser_large_model_qa.cpp index 3ff728cf4..ee4af0396 100644 --- a/apps/ied_simulator/src/ied_browser_large_model_qa.cpp +++ b/apps/ied_simulator/src/ied_browser_large_model_qa.cpp @@ -201,6 +201,19 @@ int main(int argc, char** argv) { const QString liveKey = QStringLiteral("LD4") + QChar(0x1f) + QStringLiteral("LLN19$MX$DO99$DA4"); + // ARSAS parity: discovered/readable is not the same as measured. An + // empty-string MMS sample is still a real received value, not "Not read". + if (model.nodeForReference(QStringLiteral("LD4/LLN19.DO99.DA4")) + .value(QStringLiteral("hasReceivedValue")).toBool()) { + std::cerr << "IED_BROWSER_LARGE_MODEL_FAIL unread_sample_false_live\n"; + return 30; + } + model.applyReadValue(liveKey, QString{}); + if (!model.nodeForReference(QStringLiteral("LD4/LLN19.DO99.DA4")) + .value(QStringLiteral("hasReceivedValue")).toBool()) { + std::cerr << "IED_BROWSER_LARGE_MODEL_FAIL empty_sample_received\n"; + return 31; + } model.applyReadValue(liveKey, QStringLiteral("23")); if (model.nodeForReference(QStringLiteral("LD4/LLN19.DO99.DA4 [MX]")) .value(QStringLiteral("value")).toString() != QStringLiteral("23")) { diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index 3844b1f3f..48551cb40 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -12,6 +12,10 @@ navigation = Path(sys.argv[2]).read_text(encoding="utf-8") status = Path(sys.argv[3]).read_text(encoding="utf-8") data_model = Path(sys.argv[4]).read_text(encoding="utf-8") +fleet_global = (workspace_path.parent / "FleetGlobalDataPane.qml").read_text(encoding="utf-8") +per_ied_global = (workspace_path.parent / "BrowserGlobalDataPane.qml").read_text(encoding="utf-8") +canonical_header = (workspace_path.parent.parent / "src" / "MmsLiveTreeModel.hpp").read_text(encoding="utf-8") +canonical_tree = (workspace_path.parent.parent / "src" / "MmsLiveTreeModel.cpp").read_text(encoding="utf-8") workspace_required = [ 'component RibbonAction: Button', @@ -105,6 +109,30 @@ if 'readEngineeringSelected()' in data_set or 'readEngineeringSelected()' in data_model: raise SystemExit("BROWSER_TYPED_EVIDENCE_FAIL passive inspector initiated MMS Read") +# M2K — migrate ARSAS live-grid presentation while retaining one authoritative +# per-IED canonical MMS tree and rejecting "readable means LIVE" shortcuts. +for token in [ + 'objectName: "fleetGlobalDataSearch"', + 'text: "QUALITY"', + 'text: "IED TIMESTAMP"', + 'function requestRefresh()', + 'Qt.callLater(root.refreshRows)', + 'rows.setProperty(i, role, next[i][role])', + 'readonly property int maximumPreviewLength: 256', + 'onDoubleClicked: root.iedRequested(slotIndex)', +]: + if token not in fleet_global: + raise SystemExit("BROWSER_ARSAS_LIVE_GRID_FAIL missing=" + token) +if ("node.hasReceivedValue === true ? \"LIVE\" : \"NOT READ\"" not in per_ied_global or + "bool hasReceivedValue{};" not in canonical_header or + 'QStringLiteral("hasReceivedValue")' not in canonical_tree or + "node.hasReceivedValue = true;" not in canonical_tree or + "unread_sample_false_live" not in large_model_qa or + "empty_sample_received" not in large_model_qa): + raise SystemExit("BROWSER_ARSAS_SSOT_FAIL unread values fabricated as LIVE") +if "Timer {" in fleet_global or "readEngineeringSelected()" in fleet_global: + raise SystemExit("BROWSER_ARSAS_SSOT_FAIL implicit MMS polling or read") + # Typed MMS evidence from the initial trusted-SCL snapshot must survive into # both canonical trees. The large-model QA exercises valid/invalid wire cases. snapshot_projection = (workspace_path.parent.parent / "src" / "MmsSclClientProjection.hpp").read_text(encoding="utf-8") From 25ec94159798b607db422b73fa2c85477973aa2a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 10:39:53 +0700 Subject: [PATCH 2/5] perf(workbench): coalesce per-IED live watch bursts before All-IED projection Wire canonical watchlist value notifications through the same event-loop coalescing entrypoint used by search and fleet changes. Guard source contract against synchronous per-value full projection scans. --- apps/ied_simulator/qml/Main.qml | 2 +- apps/ied_simulator/test_browser_convergence.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/ied_simulator/qml/Main.qml b/apps/ied_simulator/qml/Main.qml index c4bef363c..409dd6ba3 100644 --- a/apps/ied_simulator/qml/Main.qml +++ b/apps/ied_simulator/qml/Main.qml @@ -482,7 +482,7 @@ ApplicationWindow { utilities: root.browserFleet.utilitiesAt(index) controls: root.browserFleet.controlsAt(index) engineering: root.browserFleet.engineeringAt(index) - onWatchedDataChanged: fleetGlobalData.refreshRows() + onWatchedDataChanged: fleetGlobalData.requestRefresh() } } } diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index 48551cb40..903c8f6e2 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -130,6 +130,10 @@ "unread_sample_false_live" not in large_model_qa or "empty_sample_received" not in large_model_qa): raise SystemExit("BROWSER_ARSAS_SSOT_FAIL unread values fabricated as LIVE") +main_qml = (workspace_path.parent / "Main.qml").read_text(encoding="utf-8") +if 'onWatchedDataChanged: fleetGlobalData.requestRefresh()' not in main_qml: + raise SystemExit("BROWSER_ARSAS_LIVE_GRID_FAIL uncoalesced cross-IED burst") + if "Timer {" in fleet_global or "readEngineeringSelected()" in fleet_global: raise SystemExit("BROWSER_ARSAS_SSOT_FAIL implicit MMS polling or read") From 19a5b9e361fe713384cf034767ceebbca74c50ec Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 10:40:32 +0700 Subject: [PATCH 3/5] test(workbench): initialize large-model QA source before ARSAS parity contract --- apps/ied_simulator/test_browser_convergence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index 903c8f6e2..075b01625 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -16,6 +16,7 @@ per_ied_global = (workspace_path.parent / "BrowserGlobalDataPane.qml").read_text(encoding="utf-8") canonical_header = (workspace_path.parent.parent / "src" / "MmsLiveTreeModel.hpp").read_text(encoding="utf-8") canonical_tree = (workspace_path.parent.parent / "src" / "MmsLiveTreeModel.cpp").read_text(encoding="utf-8") +large_model_qa = (workspace_path.parent.parent / "src" / "ied_browser_large_model_qa.cpp").read_text(encoding="utf-8") workspace_required = [ 'component RibbonAction: Button', @@ -141,7 +142,6 @@ # both canonical trees. The large-model QA exercises valid/invalid wire cases. snapshot_projection = (workspace_path.parent.parent / "src" / "MmsSclClientProjection.hpp").read_text(encoding="utf-8") mms_client = (workspace_path.parent.parent / "src" / "MmsClientController.cpp").read_text(encoding="utf-8") -large_model_qa = (workspace_path.parent.parent / "src" / "ied_browser_large_model_qa.cpp").read_text(encoding="utf-8") if ("isSclTypedEvidenceLeaf(item)" not in snapshot_projection or "item[item.size() - 2] == '$'" not in snapshot_projection or "kMaximumSclInitialTypedEvidence = 96U" not in snapshot_projection or From aae441a265dce89ed8bd1ff874782f1569350c00 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 10:49:52 +0700 Subject: [PATCH 4/5] test(workbench): align P6E multi-IED routing contract with coalesced SSOT watch updates --- apps/ied_simulator/test_multi_ied_workspace.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/ied_simulator/test_multi_ied_workspace.py b/apps/ied_simulator/test_multi_ied_workspace.py index 117ba5cc2..68c959a34 100644 --- a/apps/ied_simulator/test_multi_ied_workspace.py +++ b/apps/ied_simulator/test_multi_ied_workspace.py @@ -71,7 +71,7 @@ def require(haystack, words, label): "onOpenSourceRequested: function(fileUrl)", "fleet.prepareForNewSource()", "FleetGlobalDataPane {", - "onWatchedDataChanged: fleetGlobalData.refreshRows()", + "onWatchedDataChanged: fleetGlobalData.requestRefresh()", ), "shell routing") require(browser, ( "required property var fleet", @@ -91,7 +91,14 @@ def require(haystack, words, label): "view.watchedRows()", "view.refreshWatchedGlobalData()", "maximumRows: 2048", + "function requestRefresh()", + "Qt.callLater(root.refreshRows)", + "rows.setProperty(i, role, next[i][role])", + 'text: "QUALITY"', + 'text: "IED TIMESTAMP"', ), "all-IED Global Data") +if 'onWatchedDataChanged: fleetGlobalData.refreshRows()' in shell: + raise SystemExit("P6E_FAIL uncoalesced cross-IED watch refresh") require(global_data, ( "signal watchSnapshotChanged()", "function snapshotRows()", From 5c5e425c20acb1b849399f39bf62da0f68c0942b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 10:50:09 +0700 Subject: [PATCH 5/5] test(workbench): use declared Main.qml source in P6E negative refresh gate --- apps/ied_simulator/test_multi_ied_workspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ied_simulator/test_multi_ied_workspace.py b/apps/ied_simulator/test_multi_ied_workspace.py index 68c959a34..04812f92b 100644 --- a/apps/ied_simulator/test_multi_ied_workspace.py +++ b/apps/ied_simulator/test_multi_ied_workspace.py @@ -97,7 +97,7 @@ def require(haystack, words, label): 'text: "QUALITY"', 'text: "IED TIMESTAMP"', ), "all-IED Global Data") -if 'onWatchedDataChanged: fleetGlobalData.refreshRows()' in shell: +if 'onWatchedDataChanged: fleetGlobalData.refreshRows()' in main: raise SystemExit("P6E_FAIL uncoalesced cross-IED watch refresh") require(global_data, ( "signal watchSnapshotChanged()",