Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions .github/workflows/product-naming-guard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Product Naming Guard
on:
pull_request:
push:
branches: [main]

permissions:
contents: read

concurrency:
group: product-naming-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
tracked-tree-names:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
- name: Check tracked source and paths
run: python3 scripts/check_product_naming.py
54 changes: 50 additions & 4 deletions apps/ied_simulator/qml/BrowserDataSetPane.qml
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,9 @@ Rectangle {
}
Label {
Layout.preferredWidth: Math.max(180, root.width * 0.42)
text: root.displayValue(memberRow.resolved.value)
text: memberRow.resolved.kind === "DA" &&
memberRow.resolved.hasReceivedValue !== true
? "NOT READ" : root.displayValue(memberRow.resolved.value)
color: theme.textSoft
font.pixelSize: 10
elide: Text.ElideRight
Expand Down Expand Up @@ -278,7 +280,9 @@ Rectangle {
}
Label {
Layout.preferredWidth: Math.max(180, root.width * 0.42)
text: root.displayValue(attributeRow.modelData.value)
text: attributeRow.modelData.hasReceivedValue === true
? root.displayValue(attributeRow.modelData.value)
: "NOT READ"
color: theme.textSoft
font.pixelSize: 10
elide: Text.ElideRight
Expand Down Expand Up @@ -319,8 +323,7 @@ Rectangle {
ScrollView {
id: dataSetEvidenceView
objectName: "browserDataSetEvidenceInspector"
visible: root.selectedAttributeEvidence.kind === "quality"
|| root.selectedAttributeEvidence.kind === "timestamp"
visible: root.selectedAttributeNode.kind === "DA"
Layout.fillWidth: true
Layout.preferredHeight: visible
? Math.min(235, Math.max(130, root.height * 0.36)) : 0
Expand All @@ -345,6 +348,49 @@ Rectangle {
elide: Text.ElideMiddle
}
}
Repeater {
// One selected canonical node; no parallel DataSet value store.
// A normal MX/ST/CO leaf is inspectable, not only q/t leaves.
model: [
{label: "Functional constraint", value: root.selectedAttributeNode.functionalConstraint},
{label: "Read state", value: root.selectedAttributeNode.hasReceivedValue === true
? "RECEIVED" : "NOT READ"},
{label: "Value", value: root.selectedAttributeNode.hasReceivedValue === true
? root.displayValue(root.selectedAttributeNode.value) : "—"},
{label: "Quality", value: root.displayValue(root.selectedAttributeNode.quality)},
{label: "IED Timestamp", value: root.displayValue(root.selectedAttributeNode.timestamp)},
{label: "MMS type", value: root.displayValue(root.selectedAttributeNode.mmsType)},
{label: "Value source", value: root.selectedAttributeNode.valueSourceReference || "—"}
]
delegate: Rectangle {
required property int index
required property var modelData
Layout.fillWidth: true
implicitHeight: 22
color: index % 2 ? theme.surfaceSoft : theme.surface
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 6
Label {
Layout.preferredWidth: Math.max(120, root.width * 0.34)
text: modelData.label
color: theme.muted
font.pixelSize: 9
}
Label {
Layout.fillWidth: true
text: modelData.value || "—"
color: theme.text
font.pixelSize: 9
elide: Text.ElideMiddle
ToolTip.visible: hovered
ToolTip.text: text
}
}
}
}
BrowserEngineeringEvidence {
theme: root.theme
engineeringEvidence: root.selectedAttributeEvidence
Expand Down
2 changes: 1 addition & 1 deletion apps/ied_simulator/src/IedReportControlManifest.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ inline QStringList concreteReportControlNames(

// IEC 61850 SCL keeps one ReportControl definition while RptEnabled@max
// describes the number of concrete MMS RCB instances. Engineering clients
// such as IEDScout address those instances with a two-digit suffix
// such as reference IEC 61850 client address those instances with a two-digit suffix
// (Buffer01, Buffer02, Unbuffer01, ...). Keep the SCL model definition-level
// and expand only at the runtime-manifest boundary.
const auto count = std::max<std::uint32_t>(1U, maxClients);
Expand Down
73 changes: 47 additions & 26 deletions apps/ied_simulator/src/ied_report_workspace_qa.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
#include <functional>

namespace {
bool isIedScoutIndexedFixture(const QString& sclPath) {
return QFileInfo(sclPath).fileName() == QStringLiteral("iedscout-indexed-reports.scd");
bool isIndexedReportFixture(const QString& sclPath) {
return QFileInfo(sclPath).fileName().endsWith(QStringLiteral("indexed-reports.scd"), Qt::CaseInsensitive);
}

bool verifyIndexedRcbManifestExpansion() {
Expand Down Expand Up @@ -82,34 +82,43 @@ bool verifyIndexedRcbManifestExpansion() {
return brcbExpanded && urcbExpanded && nonIndexedStable;
}

bool verifyIedScoutIndexedInventory(
bool verifyIndexedReportInventory(
const QString& sclPath,
const QVariantList& reportControls) {
if (!isIedScoutIndexedFixture(sclPath)) return true;
if (!isIndexedReportFixture(sclPath)) return true;

QSet<QString> references;
QString scope;
bool allConcreteInstancesReadable = true;
for (const auto& value : reportControls) {
const auto item = value.toMap();
const auto reference = item.value(QStringLiteral("reference")).toString();
if (reference.isEmpty()) continue;
const int separator = reference.lastIndexOf(QLatin1Char('.'));
if (separator <= 0) return false;
const QString prefix = reference.left(separator);
const QString instance = reference.mid(separator + 1);
if (scope.isEmpty()) scope = prefix;
if (scope != prefix) return false; // No cross-IED/LD/LLN aliasing.
references.insert(reference);

const bool buffered = item.value(QStringLiteral("buffered")).toBool();
const auto expectedReportId = buffered
? QStringLiteral("IEDSCOUT01LD0/LLN0$BR$Buffer")
: QStringLiteral("IEDSCOUT01LD0/LLN0$RP$Unbuffer");
const QString expectedReportId = scope + (buffered
? QStringLiteral("$BR$Buffer") : QStringLiteral("$RP$Unbuffer"));
const bool validInstance = buffered
? (instance == QStringLiteral("Buffer01") || instance == QStringLiteral("Buffer02"))
: (instance == QStringLiteral("Unbuffer01") || instance == QStringLiteral("Unbuffer02"));
allConcreteInstancesReadable = allConcreteInstancesReadable &&
validInstance &&
item.value(QStringLiteral("probeOk")).toBool() &&
!item.value(QStringLiteral("dataSet")).toString().isEmpty() &&
item.value(QStringLiteral("reportId")).toString() == expectedReportId;
}

static const QSet<QString> expected{
QStringLiteral("IEDSCOUT01LD0/LLN0.Buffer01"),
QStringLiteral("IEDSCOUT01LD0/LLN0.Buffer02"),
QStringLiteral("IEDSCOUT01LD0/LLN0.Unbuffer01"),
QStringLiteral("IEDSCOUT01LD0/LLN0.Unbuffer02")};
if (scope.isEmpty()) return false;
const QSet<QString> expected{
scope + QStringLiteral(".Buffer01"),
scope + QStringLiteral(".Buffer02"),
scope + QStringLiteral(".Unbuffer01"),
scope + QStringLiteral(".Unbuffer02")};
return references == expected && allConcreteInstancesReadable;
}

Expand Down Expand Up @@ -198,13 +207,7 @@ int main(int argc, char* argv[]) {

IedFleetController simulator;
const QString sclPath = QString::fromLocal8Bit(argv[1]);
const bool iedScoutFixture = isIedScoutIndexedFixture(sclPath);
const QString exactUrcb = iedScoutFixture
? QStringLiteral("IEDSCOUT01LD0/LLN0.Unbuffer01")
: QString{};
const QString exactBrcb = iedScoutFixture
? QStringLiteral("IEDSCOUT01LD0/LLN0.Buffer01")
: QString{};
const bool indexedFixture = isIndexedReportFixture(sclPath);
if (!loadAsync(simulator, sclPath)) {
qCritical().noquote() << "REPORTS_WORKBENCH_FAIL import" << simulator.fatalError();
return 4;
Expand Down Expand Up @@ -255,7 +258,25 @@ int main(int argc, char* argv[]) {
qCritical() << "REPORTS_WORKBENCH_FAIL unexpected_dynamic_ownership";
return 28;
}
if (!verifyIedScoutIndexedInventory(sclPath, reportControls)) {
// The fixture's actual IED/domain identity is authoritative. Resolve
// concrete RCB instances from the just-received inventory, never from
// a vendor-shaped string that could silently miss the selected IED.
QString exactUrcb;
QString exactBrcb;
if (indexedFixture) {
for (const auto& value : reportControls) {
const QString reference = value.toMap().value(QStringLiteral("reference")).toString();
if (reference.endsWith(QStringLiteral(".Unbuffer01")))
exactUrcb = reference;
else if (reference.endsWith(QStringLiteral(".Buffer01")))
exactBrcb = reference;
}
if (exactUrcb.isEmpty() || exactBrcb.isEmpty()) {
qCritical() << "REPORTS_WORKBENCH_FAIL indexed_rcb_identity";
return 21;
}
}
if (!verifyIndexedReportInventory(sclPath, reportControls)) {
QStringList actual;
for (const auto& value : reportControls) {
const auto item = value.toMap();
Expand All @@ -265,7 +286,7 @@ int main(int argc, char* argv[]) {
(item.value(QStringLiteral("probeOk")).toBool() ? QStringLiteral("ok") : QStringLiteral("fail")) +
QStringLiteral("|rptID=") + item.value(QStringLiteral("reportId")).toString());
}
qCritical().noquote() << "REPORTS_WORKBENCH_FAIL iedscout_indexed_inventory"
qCritical().noquote() << "REPORTS_WORKBENCH_FAIL reference_iec61850_indexed_inventory"
<< actual.join(QLatin1Char(','));
return 21;
}
Expand Down Expand Up @@ -418,7 +439,7 @@ int main(int argc, char* argv[]) {
}

qulonglong brcbGiReports = 0;
if (iedScoutFixture) {
if (indexedFixture) {
if (selectEligibleRcb(reports, true, exactBrcb) < 0 ||
reports.selectedDataSetMembers().isEmpty()) {
qCritical() << "REPORTS_WORKBENCH_FAIL eligible_brcb";
Expand Down Expand Up @@ -464,9 +485,9 @@ int main(int argc, char* argv[]) {
<< "urcb_gi_reports=" + QString::number(reportCountAfterGi)
<< "brcb_gi_reports=" + QString::number(brcbGiReports)
<< "rcb_manifest_instances=pass"
<< QStringLiteral("iedscout_indexed_inventory=%1").arg(iedScoutFixture ? QStringLiteral("pass") : QStringLiteral("n/a"))
<< QStringLiteral("reference_iec61850_indexed_inventory=%1").arg(indexedFixture ? QStringLiteral("pass") : QStringLiteral("n/a"))
<< "urcb=pass"
<< QStringLiteral("brcb_gi=%1").arg(iedScoutFixture ? QStringLiteral("pass") : QStringLiteral("n/a"))
<< QStringLiteral("brcb_gi=%1").arg(indexedFixture ? QStringLiteral("pass") : QStringLiteral("n/a"))
<< "brcb_inventory=pass"
<< "entryid_indicator=pass"
<< "cleanup=pass"
Expand Down
8 changes: 8 additions & 0 deletions apps/ied_simulator/test_browser_convergence.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@
'root.showSelectedEvidence = selected',
'BrowserEngineeringEvidence {',
'objectName: "browserDataSetEvidenceInspector"',
'visible: root.selectedAttributeNode.kind === "DA"',
'root.selectedAttributeNode.hasReceivedValue === true',
'root.selectedAttributeNode.valueSourceReference',
'{label: "IED Timestamp"',
'memberRow.resolved.hasReceivedValue !== true',
'attributeRow.modelData.hasReceivedValue === true',
'root.selectedAttributeNode.engineeringEvidence || ({})',
'selected.reference === root.selectedAttributeReference',
'root.selectedAttributeReference = selected',
Expand All @@ -107,6 +113,8 @@
raise SystemExit("BROWSER_DATASET_EVIDENCE_FAIL inspector is not in member pane")
if data_set.count('BrowserEngineeringEvidence {') != 1:
raise SystemExit("BROWSER_DATASET_EVIDENCE_FAIL duplicate/hidden evidence inspector")
if 'Timer {' in data_set:
raise SystemExit("BROWSER_DATASET_PROVENANCE_FAIL implicit per-DA timer")
if 'readEngineeringSelected()' in data_set or 'readEngineeringSelected()' in data_model:
raise SystemExit("BROWSER_TYPED_EVIDENCE_FAIL passive inspector initiated MMS Read")

Expand Down
4 changes: 2 additions & 2 deletions apps/ied_simulator/test_gui_live_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ def run_direct_normal_control_regression(
f"exit={discovery.returncode} stdout={discovery.stdout!r} stderr={discovery.stderr!r}"
)

# IEDScout accepts ARSAS Check=0xC0 (synchro + interlock requested).
# reference IEC 61850 client accepts ARSAS Check=0xC0 (synchro + interlock requested).
# Keep this compatibility explicit instead of treating valid Check bits as
# an object-value-invalid condition.
checked = subprocess.run(
Expand Down Expand Up @@ -296,7 +296,7 @@ def run_direct_normal_control_regression(
or "NO_RETRY_EVIDENCE controlWrites=1" not in checked.stdout
):
raise RuntimeError(
"Direct-Normal IEDScout Check=0xC0 compatibility regression failed: "
"Direct-Normal reference IEC 61850 client Check=0xC0 compatibility regression failed: "
f"exit={checked.returncode} stdout={checked.stdout!r} stderr={checked.stderr!r}"
)

Expand Down
35 changes: 35 additions & 0 deletions docs/ARIEC61850_ARSAS_NATIVE_PORT_MAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# ARIEC61850 engine and ARSAS Explorer to ARStack native capability map

Integration baseline: 2026-10-15. Target: installed C++/Qt/QML ARStack IEC 61850 Workbench in apps/ied_simulator. The apps/arstack_studio product and ESP32-P4 installer are separate. Do not assume changes here ship in that installer.

## Verified references

- ARIEC61850 masarray/ARIEC61850 main e5deed1d8aa11d97991695c6e390baafea7ab797: README.md, apps/AR.Iec61850.IedDiscovery, apps/AR.Iec61850.EngineeringWorkbench, src/AR.Iec61850.
- ARSAS masarray/arsas main 45348eccf24e941dd185e5fb729217923846c610: MainWindow.xaml, MainWindow.ExplorerSignalGrid.cs, MainWindow.ExplorerHeroActions.cs, MainWindow.SclLiveModelAuthority.cs and MainWindow.SharedStaticDataSetAuthority.cs.
- ARStack baseline c8aabf292c370993950b8682718cd56b77f2cabf.

## Integration boundaries

| Reference capability | Existing authoritative C++/Qt owner | Required work |
| --- | --- | --- |
| Live discovery and typed SCL | IedEngineeringContextController, C++ MMS/SCL model | Port only missing validated semantics; never introduce a second .NET engine or XML semantic tree |
| Confirmed MMS Read/Write | MmsClientController, IedBrowserFleetController | Retain exact-source associations, generation checks and explicit Read |
| LD/LN/DO/DA, CDC, FC, q/t | MmsLiveTreeModel, MmsEngineeringEvidence.hpp | One indexed canonical model; M2K/M2L accepted; no guessed unit/multiplier |
| DataSet ordered FCD/FCDA | MmsReportController, MmsLiveTreeModel | Preserve member order, FC, indexed aliasing and 64-child lazy expansion |
| URCB/BRCB and received report | MmsReportController, BrowserReportMemberProjection.hpp | Preserve actual frame identity, member ordinal, replay and report-first static path |
| Guarded control, SG and files | Existing C++ controllers | Transfer demonstrated missing capability only with negative/lifecycle tests |
| ARSAS live signal grid | FleetGlobalDataPane.qml, BrowserGlobalDataPane.qml | Already present after M2K; future work is genuine acquisition evidence and rendered visual QA |
| ARSAS first-run actions | IedBrowserWorkspace.qml | Reuse existing Open SCL, Connect and Discover; improve empty state based on native screenshot QA |
| Selected DA/FCDA inspector | BrowserDataModelPane.qml, BrowserDataSetPane.qml | Show every selected DA's value, read state, FC, q/t and source without implicit Read |

## Engineering acceptance

1. Open real SCL/CID, resolve IED, navigate LD/LN/DO/DA offline.
2. Connect using the selected engineering context; show actual sample, NOT READ, failed or unresolved evidence correctly.
3. Select a DataSet FCD/FCDA and inspect an ordinary DA, not only q/t leaves; no network action on selection.
4. Explicit bounded Read updates only the proper IED association, not a parallel UI cache.
5. Select RCB, inspect effective state, enable under guards and map an actual InformationReport in original member order.
6. Disconnect/change source without stale context or callbacks.
7. Validate native rendered selected-state screenshots, 60k+ model, 8-IED isolation, malformed inputs, reconnect, and actual Windows Workbench installer separately.

This is an implementation map, not a claim of physical interop or completed feature/visual parity.
Loading
Loading