diff --git a/.github/workflows/product-naming-guard.yml b/.github/workflows/product-naming-guard.yml new file mode 100644 index 000000000..682f5d740 --- /dev/null +++ b/.github/workflows/product-naming-guard.yml @@ -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 diff --git a/apps/ied_simulator/qml/BrowserDataSetPane.qml b/apps/ied_simulator/qml/BrowserDataSetPane.qml index 17137e59a..72e70d0da 100644 --- a/apps/ied_simulator/qml/BrowserDataSetPane.qml +++ b/apps/ied_simulator/qml/BrowserDataSetPane.qml @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/apps/ied_simulator/src/IedReportControlManifest.hpp b/apps/ied_simulator/src/IedReportControlManifest.hpp index 510272930..f24405a14 100644 --- a/apps/ied_simulator/src/IedReportControlManifest.hpp +++ b/apps/ied_simulator/src/IedReportControlManifest.hpp @@ -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(1U, maxClients); diff --git a/apps/ied_simulator/src/ied_report_workspace_qa.cpp b/apps/ied_simulator/src/ied_report_workspace_qa.cpp index 9e659c8f6..6ac3bc718 100644 --- a/apps/ied_simulator/src/ied_report_workspace_qa.cpp +++ b/apps/ied_simulator/src/ied_report_workspace_qa.cpp @@ -20,8 +20,8 @@ #include 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() { @@ -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 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 expected{ - QStringLiteral("IEDSCOUT01LD0/LLN0.Buffer01"), - QStringLiteral("IEDSCOUT01LD0/LLN0.Buffer02"), - QStringLiteral("IEDSCOUT01LD0/LLN0.Unbuffer01"), - QStringLiteral("IEDSCOUT01LD0/LLN0.Unbuffer02")}; + if (scope.isEmpty()) return false; + const QSet expected{ + scope + QStringLiteral(".Buffer01"), + scope + QStringLiteral(".Buffer02"), + scope + QStringLiteral(".Unbuffer01"), + scope + QStringLiteral(".Unbuffer02")}; return references == expected && allConcreteInstancesReadable; } @@ -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; @@ -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(); @@ -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; } @@ -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"; @@ -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" diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index 075b01625..a5eef6f65 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -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', @@ -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") diff --git a/apps/ied_simulator/test_gui_live_value.py b/apps/ied_simulator/test_gui_live_value.py index 58596d530..6009042cb 100644 --- a/apps/ied_simulator/test_gui_live_value.py +++ b/apps/ied_simulator/test_gui_live_value.py @@ -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( @@ -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}" ) diff --git a/docs/ARIEC61850_ARSAS_NATIVE_PORT_MAP.md b/docs/ARIEC61850_ARSAS_NATIVE_PORT_MAP.md new file mode 100644 index 000000000..820633143 --- /dev/null +++ b/docs/ARIEC61850_ARSAS_NATIVE_PORT_MAP.md @@ -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. diff --git a/docs/WORKBENCH_ARSAS_GUI_SSOT_STRATEGY.md b/docs/WORKBENCH_ARSAS_GUI_SSOT_STRATEGY.md index 78de76d5e..556adfe7e 100644 --- a/docs/WORKBENCH_ARSAS_GUI_SSOT_STRATEGY.md +++ b/docs/WORKBENCH_ARSAS_GUI_SSOT_STRATEGY.md @@ -2,7 +2,7 @@ **Status:** cross-thread execution contract; first written 2026-10-15 (WIB), against ARStack main a328da13133a and ARSAS main 45348eccf24e941dd185e5fb729217923846c610. **Re-query GitHub before acting.** This is a living strategy, not a claim of completed functionality. -**Product:** the installed **native Qt/QML ARStack Workbench** (apps/ied_simulator), **not** a web GUI, a new desktop app, a WPF port, or the separate ESP32-P4 timing/SV firmware track. The objective is an IEDScout-class engineering workflow while borrowing effective visual/interaction patterns from the ARSAS/ARIEC61850 Windows Explorer. ARStack's own IEC 61850 models and wire evidence remain authoritative. +**Product:** the installed **native Qt/QML ARStack Workbench** (apps/ied_simulator), **not** a web GUI, a new desktop app, a WPF port, or the separate ESP32-P4 timing/SV firmware track. The objective is an professional IEC 61850 engineering-grade engineering workflow while borrowing effective visual/interaction patterns from the ARSAS/ARIEC61850 Windows Explorer. ARStack's own IEC 61850 models and wire evidence remain authoritative. ## 0. Read order for every thread @@ -20,16 +20,16 @@ - [ARSAS shared static DataSet authority](https://github.com/masarray/arsas/blob/45348eccf24e941dd185e5fb729217923846c610/MainWindow.SharedStaticDataSetAuthority.cs): preserve static report-first behavior, without automatic cyclic MMS. - [ARSAS IoList FAT workspace](https://github.com/masarray/arsas/blob/45348eccf24e941dd185e5fb729217923846c610/IoListTestingWindow.xaml): multi-IED selection and engineering context. -**Copy the *interaction principles*, not WPF/C# runtime, invented values, unsupported IEC semantics, or duplicated services.** ARSAS' IEC 61850 Explorer is a useful reference, but not proof that every ARSAS screen literally matches an IEDScout tab. +**Copy the *interaction principles*, not WPF/C# runtime, invented values, unsupported IEC semantics, or duplicated services.** ARSAS' IEC 61850 Explorer is a useful reference, but not proof that every ARSAS screen literally matches an professional engineering tab. ## 1. Exact baseline, active work and merge dependencies At creation: - ARStack main: [a328da13 (M2J merged)](https://github.com/masarray/arstack61850/commit/a328da13133a6473dc6a28a69f5d2aebab7a7198). M2H [#171](https://github.com/masarray/arstack61850/pull/171), M2I [#173](https://github.com/masarray/arstack61850/pull/173) and M2J [#174](https://github.com/masarray/arstack61850/pull/174) are merged. - M2J post-merge Qt, release hardening, SCL-assisted connect and SCL-assisted Workbench workflows completed successfully at this main SHA. -- **Active integration candidate M2K: [PR #176](https://github.com/masarray/arstack61850/pull/176)**, head **5c5e425c20acb1b849399f39bf62da0f68c0942b** at writing. The SCL-Assisted Workbench CI passed; its exact-head native Qt run [38678320479](https://github.com/masarray/arstack61850/actions/runs/38678320479) was still **in progress** when last inspected. **Do not mark M2K merged/green or start conflicting edits from an assumption.** +- **Integrated M2K:** PR #176 merged into main at `0cc882022d20f416a33433d5f29a1f8950984d85` with exact-head native Qt and SCL verification. - Unmerged PR [#172](https://github.com/masarray/arstack61850/pull/172) (another q/t visual QA branch), [#164](https://github.com/masarray/arstack61850/pull/164) (older q/t branch), and legacy Browser recovery PRs exist. They are **not automatically safe to merge**; review differences against main and accepted M2I/M2J first. Some PTP/SV and native Studio branches are distinct tracks (for example #175). -- M2K's approved scope is the ARSAS-inspired native All-IED live grid, explicit Quality / IED Timestamp columns, bounded search and truthful sample-received state in the canonical MMS tree, with regression gates. The existing M2K branch owns those files until merged/closed. **One integration owner**, no duplicate competing PR for M2K. +- **M2K accepted scope:** native All-IED grid, visible Quality / IED Timestamp, bounded search, canonical sample-received state; retain its single SSOT and no polling. - Every future milestone lists both base SHA and exact PR head, a bounded change set, CI links, review resolution and merge SHA. Update the companion [current handoff](WORKBENCH_CURRENT_HANDOFF.md) at accepted checkpoints, not after every experimental commit. ## 2. Authoritative architecture — a single model, multiple views @@ -83,7 +83,7 @@ Current code authority / edit boundaries: - **Static DataSet/RCB must remain report-first/report-only** where configured; no silent periodic Read to fill missing rows. Initial bounded snapshot and user-triggered explicit Read are distinct authorities. URCB reservation and BRCB behavior cannot be collapsed. - Controls/write/SG switching/file operations require effective server-state readback/guarded user intent and clear error presentation. A successful request submission alone is not proof of effect. -## 3. UX target and user workflow (ARSAS guidance, IEDScout-class outcome) +## 3. UX target and user workflow (ARSAS guidance, professional IEC 61850 engineering-grade outcome) **Primary journey:** Open SCL or Add IED by endpoint -> resolve exact IED/AP/domain -> inspect offline hierarchy -> Connect/Discover/Validate -> browse Data Model/Measurement -> select DA / Read explicitly -> inspect typed Value/Quality/TimeQuality/unit/provenance -> choose bounded DataSet -> inspect ordered FCDA -> inspect report controls -> confirm/enable static RCB under correct guard -> receive evidence mapped to report member -> monitor All-IED grid -> disconnect/reopen without cross-context residue. @@ -121,8 +121,8 @@ Every milestone is a **small vertical slice** (one actual user action through th | Gate | Scope and dependencies | Merge acceptance | | --- | --- | --- | -| **M2K — ongoing, PR #176** | ARSAS-style All-IED grid + canonical hasReceivedValue; finish exact-head CI and review, **then** merge | visible q/t, correct NOT READ vs empty received, stable coalesced updates, multi-IED P6E and 60k regression, PR SHA matches CI | -| **M2L — next** | **Canonical measurement semantics / provenance first**, building on merged M2K: correct scalar vs vector, engineering unit, quality, clock, read state, acquisition; one typed projection contract | fixture matrix: MX magnitude, structured vector+angle when valid, frequency (Hz), TotW/power, Boolean, invalid unit/type/q/t, stale generation; no angle on Hz/TotW; no 'LIVE' without sample | +| **M2K — merged, PR #176** | Native All-IED grid + canonical hasReceivedValue | Preserve existing verified grid/provenance/large-model behavior | +| **M2L — merged, PR #178** | Canonical exact CDC + MX measurement semantics, authoritative q/t and read source | Preserve integer/float vector and scalar regression, no fictional angle/units, source replacement tests | | **M2M** | Data Model and DataSets **rendered native workflow**: selection, FCDA aliases, tree expansion, inspector/read button, per-IED Global Data handoff | deterministic native screenshots with actual selected DA/FCDA (not overview only), no implicit Read, 64 child bound, multi-IED isolation | | **M2N** | Reports/RCB and Global Data provenance: requested vs effective RCB; actual report q/t and ordered frame members; consolidated live grid acquisition | static report-only; GI/URCB/BRCB and cleanup, malformed/omitted/segmented report tests, actual selected native screenshot and no cross-IED leakage | | **M2O** | Setting Groups / Files / GOOSE productivity and safety; one controller and explicit user workflow per feature | selected SG values and readback, negative/safe control cases, guarded transfer/cancel, configured-vs-live GOOSE state | diff --git a/docs/WORKBENCH_CURRENT_HANDOFF.md b/docs/WORKBENCH_CURRENT_HANDOFF.md index 41b65a84d..e178448e5 100644 --- a/docs/WORKBENCH_CURRENT_HANDOFF.md +++ b/docs/WORKBENCH_CURRENT_HANDOFF.md @@ -4,6 +4,16 @@ > **Primary strategy and ownership contract:** [WORKBENCH_ARSAS_GUI_SSOT_STRATEGY.md](WORKBENCH_ARSAS_GUI_SSOT_STRATEGY.md). > **Scope:** installed native ARStack Workbench in apps/ied_simulator. Not a web app, WPF clone, or the separate ESP32-P4/SMV Studio track. +## Active native parity + repository naming work — 2026-10-15 + +- Work branch: feat/native-workbench-reference-hygiene-arsas-parity-20261015; base main c8aabf292c370993950b8682718cd56b77f2cabf. IN PROGRESS until exact-head CI and merge. +- Removed prohibited third-party names from source comments, tests and current docs; indexed RCB fixture tests now derive IED/domain identity from validated references instead of a hard-coded vendor label. +- A lightweight Product Naming Guard workflow checks all tracked source/path names on PR and push. +- Engine reference map: [ARIEC61850_ARSAS_NATIVE_PORT_MAP.md](ARIEC61850_ARSAS_NATIVE_PORT_MAP.md). ARIEC C# behavior is a reference for missing protocol capabilities; canonical C++/Qt is the runtime SSOT. +- M2M native UX: DataSets selected attribute inspector now shows ordinary ST/MX/CO DA value, read-state, FC, Quality, IED Timestamp, MMS type and provenance. No implicit MMS read. +- Post-M2L main [Qt 38701682484](https://github.com/masarray/arstack61850/actions/runs/38701682484), [SCL 38701682271](https://github.com/masarray/arstack61850/actions/runs/38701682271) and [Release Hardening 38701682258](https://github.com/masarray/arstack61850/actions/runs/38701682258): all SUCCESS. +- Remaining gate: PR exact-head Qt + source review and selected-state screenshot QA. Do not claim physical IED or Windows installer acceptance from synthetic CI. + ## Verified current integration checkpoint — 2026-10-15 (WIB) **Source baseline at acceptance: main `9cc9bda9c30804be343d49367e4dde6948789d95`.** Refresh live GitHub before further edits. Subsequent documentation-only commits may advance main without changing Workbench source. @@ -51,7 +61,7 @@ 4. Report member reference and evidence must map to exact received frame ordinal and actual Report ID. Static reporting remains report-only without implicit periodic MMS Read. 5. Preserve verified bounds: 60k+ node large-model regression, 4,096 detail rows, 64 lazy FCDA DAs, 64 explicit Read targets, 256 per-IED watched objects, 2,048 All-IED rendered objects, 256-char All-IED preview, 256 visible report members, 1,024-char report preview, 96 initial typed q/t details. These are presentation bounds; do not drop authoritative frames. 6. Scalar Hz and total power (TotW) do not acquire an “angle” through a generic presentation shortcut; only display units/phasor components proven by typed metadata. -7. Never claim pixel-level IEDScout/ARSAS parity, latest installer parity, or real physical IED interop on synthetic fixtures alone. +7. Never claim pixel-level reference IEC 61850 client/ARSAS parity, latest installer parity, or real physical IED interop on synthetic fixtures alone. ## Historical action and owners (superseded; do not execute stale PR merge instructions) @@ -59,7 +69,7 @@ **Canonical/SSOT owner (second):** M2L Measurement semantics and value/q/t/unit/angle provenance on the existing MMS model. Tests for MX vector, scalar Hz, TotW, malformed q/t, unset value, SCL-vs-live authority and source-generation mismatch first. -**QML/UX owner (parallel on disjoint files):** compare ARSAS Explorer live grid, selected-state IEDScout screenshots and ARStack Qt Data Model/DataSets/Reports/SG. Build a screen-state matrix and deterministic native captures; do not change the canonical model or edit #176's All-IED files concurrently. +**QML/UX owner (parallel on disjoint files):** compare ARSAS Explorer live grid, selected-state reference IEC 61850 client screenshots and ARStack Qt Data Model/DataSets/Reports/SG. Build a screen-state matrix and deterministic native captures; do not change the canonical model or edit #176's All-IED files concurrently. **Protocol/RCB owner:** preserve exact Reporting contract ([RCB_REPORTING_REFERENCE.md](RCB_REPORTING_REFERENCE.md)); no new polling, invented report values or uncontrolled writes. diff --git a/embedded/mms_brcb_objects_hard_profile_smoke.cpp b/embedded/mms_brcb_objects_hard_profile_smoke.cpp index a31842293..31d78e9fc 100644 --- a/embedded/mms_brcb_objects_hard_profile_smoke.cpp +++ b/embedded/mms_brcb_objects_hard_profile_smoke.cpp @@ -20,7 +20,7 @@ constexpr std::array kFalse{0x83U, 0x01U, 0x00U}; constexpr std::array kFiveSeconds{0x85U, 0x01U, 0x05U}; constexpr std::array kZeroSeconds{0x85U, 0x01U, 0x00U}; constexpr std::array kTriggerAll{0x84U, 0x02U, 0x02U, 0x7CU}; -constexpr std::array kIedScoutOptionalFields{ +constexpr std::array kReferenceIec61850OptionalFields{ 0x84U, 0x03U, 0x06U, 0x7BU, 0x80U}; [[nodiscard]] wire::EncodeResult read_boolean( @@ -304,7 +304,7 @@ int main() { } if (!dispatch_write( - dispatcher, "B1$OptFlds", kIedScoutOptionalFields, a, true, 0U, invoke) || + dispatcher, "B1$OptFlds", kReferenceIec61850OptionalFields, a, true, 0U, invoke) || reports.optional_fields()[0] != 0x7BU || reports.optional_fields()[1] != 0x80U) { return 89; diff --git a/embedded/mms_dispatcher_hard_profile_smoke.cpp b/embedded/mms_dispatcher_hard_profile_smoke.cpp index a1ce2957a..2c950eaee 100644 --- a/embedded/mms_dispatcher_hard_profile_smoke.cpp +++ b/embedded/mms_dispatcher_hard_profile_smoke.cpp @@ -86,7 +86,7 @@ constexpr std::array kReadResponse{ 0x83U, 0x01U, 0xFFU, 0x85U, 0x01U, 0x2AU}; -// ARIEC61850 golden behavior that is proven with IEDScout: the request may set +// ARIEC61850 golden behavior that is proven with reference IEC 61850 client: the request may set // specificationWithResult, while the response remains the compact // listOfAccessResult form and does not synthesize variableAccessSpecification. constexpr std::array kReadWithSpecificationResponse{ @@ -436,7 +436,7 @@ int main() { return 19; } - // IEDScout-compatible dual-directory mode now advertises every virtual + // reference IEC 61850 client-compatible dual-directory mode now advertises every virtual // hierarchy prefix, not only the concrete flattened leaf aliases. Give the // smoke enough page capacity to validate the complete sorted namespace in // one response; pagination is covered separately by the dedicated profile. diff --git a/embedded/mms_interop_bulk_read_hard_profile_smoke.cpp b/embedded/mms_interop_bulk_read_hard_profile_smoke.cpp index 3e6dd6b79..f8afa0048 100644 --- a/embedded/mms_interop_bulk_read_hard_profile_smoke.cpp +++ b/embedded/mms_interop_bulk_read_hard_profile_smoke.cpp @@ -13,7 +13,7 @@ namespace { using namespace ar::iec61850; -constexpr std::size_t kIedScoutBulkVariables = 78U; +constexpr std::size_t kReferenceIec61850BulkVariables = 78U; constexpr std::uint32_t kInvokeId = 33U; constexpr std::array kBooleanType{0x83U, 0x00U}; constexpr std::array kBooleanData{0x83U, 0x01U, 0xFFU}; @@ -102,7 +102,7 @@ constexpr std::array kAaDataSetAttributesRequest{ [[nodiscard]] std::size_t build_bulk_read_request( const std::span destination) noexcept { - const auto list_content = kVariableDefinition.size() * kIedScoutBulkVariables; + const auto list_content = kVariableDefinition.size() * kReferenceIec61850BulkVariables; const auto list_tlv = asn1::BerSpanWriter::tlv_size(0, list_content); if (!list_tlv) { return 0U; @@ -142,7 +142,7 @@ constexpr std::array kAaDataSetAttributesRequest{ return 0U; } - for (std::size_t index = 0U; index < kIedScoutBulkVariables; ++index) { + for (std::size_t index = 0U; index < kReferenceIec61850BulkVariables; ++index) { if (!writer.write_bytes(kVariableDefinition)) { return 0U; } @@ -193,7 +193,7 @@ constexpr std::array kAaDataSetAttributesRequest{ int main() { static_assert( - mms::MmsServiceSpanCodec::maximum_variables >= kIedScoutBulkVariables, + mms::MmsServiceSpanCodec::maximum_variables >= kReferenceIec61850BulkVariables, "external IEC 61850 client bulk discovery requires at least 78 variables per Read request."); const bool relay_state = true; @@ -235,14 +235,14 @@ int main() { decoded_request) || decoded_request.invoke_id != kInvokeId || !decoded_request.specification_with_result || - decoded_request.variable_count != kIedScoutBulkVariables) { + decoded_request.variable_count != kReferenceIec61850BulkVariables) { return 4; } mms::MmsObjectNameView first_name; mms::MmsObjectNameView last_name; if (!decoded_request.try_variable(0U, first_name) || - !decoded_request.try_variable(kIedScoutBulkVariables - 1U, last_name) || + !decoded_request.try_variable(kReferenceIec61850BulkVariables - 1U, last_name) || first_name.item.size() != 2U || last_name.item.size() != 2U || first_name.item[0] != 0x52U || first_name.item[1] != 0x31U || last_name.item[0] != 0x52U || last_name.item[1] != 0x31U) { @@ -267,12 +267,12 @@ int main() { std::span{response}.first(dispatched.bytes_written), read_response) || read_response.invoke_id != kInvokeId || - read_response.result_count != kIedScoutBulkVariables) { + read_response.result_count != kReferenceIec61850BulkVariables) { return 7; } mms::MmsReadAccessResultView result; - for (std::size_t index = 0U; index < kIedScoutBulkVariables; ++index) { + for (std::size_t index = 0U; index < kReferenceIec61850BulkVariables; ++index) { if (!read_response.try_result(index, result) || !boolean_result_matches(result)) { return 8; diff --git a/embedded/mms_interop_fc_root_read_hard_profile_smoke.cpp b/embedded/mms_interop_fc_root_read_hard_profile_smoke.cpp index fe6cf79e7..65cd447e6 100644 --- a/embedded/mms_interop_fc_root_read_hard_profile_smoke.cpp +++ b/embedded/mms_interop_fc_root_read_hard_profile_smoke.cpp @@ -21,7 +21,7 @@ constexpr std::array kTrue{0x83U, 0x01U, 0xFFU}; // discovery against the known-good ARIEC61850 simulator. Invoke 2 reads six // Functional Constraint roots in AA1E1F06R4ADD: // LLN0$CF, LLN0$DC, LLN0$EX, LLN0$RP, LLN0$SP, LLN0$ST. -constexpr std::array kIedScoutInvoke2{ +constexpr std::array kReferenceIec61850Invoke2{ 0xA0U, 0x81U, 0xC3U, 0x02U, 0x01U, 0x02U, 0xA4U, 0x81U, 0xBDU, 0x80U, 0x01U, 0x00U, 0xA1U, 0x81U, 0xB7U, 0xA0U, 0x81U, 0xB4U, 0x30U, 0x1CU, 0xA0U, 0x1AU, 0xA1U, 0x18U, 0x1AU, 0x0DU, 0x41U, 0x41U, 0x31U, 0x45U, 0x31U, 0x46U, 0x30U, 0x36U, 0x52U, 0x34U, @@ -86,7 +86,7 @@ int main() { const mms::MmsStaticApplicationDispatcher dispatcher{table}; std::array response{}; std::array workspace{}; - const auto dispatched = dispatcher.dispatch(kIedScoutInvoke2, response, workspace); + const auto dispatched = dispatcher.dispatch(kReferenceIec61850Invoke2, response, workspace); if (!dispatched.success() || dispatched.service != mms::MmsWireConfirmedService::read || dispatched.invoke_id != 2U || dispatched.bytes_written == 0U) { diff --git a/embedded/mms_urcb_event_hard_profile_smoke.cpp b/embedded/mms_urcb_event_hard_profile_smoke.cpp index af9b3ef61..3737c2032 100644 --- a/embedded/mms_urcb_event_hard_profile_smoke.cpp +++ b/embedded/mms_urcb_event_hard_profile_smoke.cpp @@ -70,11 +70,11 @@ int main() { mms::MmsStaticUrcbRuntime runtime{ definitions, states, object_table, data_set_table}; if (!runtime.initialize()) return 3; - constexpr std::array iedscout_generic_optflds{0x7BU, 0x80U}; + constexpr std::array reference_iec61850_generic_optflds{0x7BU, 0x80U}; constexpr std::array effective_urcb_optflds{0x78U, 0x80U}; constexpr std::array unsupported_segmentation{0x78U, 0xC0U}; constexpr std::array original_optflds{0x5CU, 0x80U}; - if (runtime.set_optional_fields(0U, iedscout_generic_optflds) != + if (runtime.set_optional_fields(0U, reference_iec61850_generic_optflds) != mms::MmsStaticUrcbStatus::ok) return 15; const auto* compatibility_state = runtime.state(0U); if (compatibility_state == nullptr || diff --git a/include/ariec61850/mms/services_span.hpp b/include/ariec61850/mms/services_span.hpp index 41ac7d540..1d24238a9 100644 --- a/include/ariec61850/mms/services_span.hpp +++ b/include/ariec61850/mms/services_span.hpp @@ -140,7 +140,7 @@ class MmsServiceSpanCodec final { static constexpr std::size_t maximum_identifier_bytes = 1'024U; static constexpr std::size_t maximum_identifiers = 128U; - // IEDScout performs bounded bulk reads while walking a full IEC 61850 model. + // reference IEC 61850 client performs bounded bulk reads while walking a full IEC 61850 model. // The proven ARIEC61850 trace for the SIPROTEC reference model contains a // single 78-variable Read request. Keep the portable span profile bounded, // but leave enough headroom for that real discovery pattern. diff --git a/include/ariec61850/mms/simulator_manifest_codec.hpp b/include/ariec61850/mms/simulator_manifest_codec.hpp index fbeec8b8d..e7176542d 100644 --- a/include/ariec61850/mms/simulator_manifest_codec.hpp +++ b/include/ariec61850/mms/simulator_manifest_codec.hpp @@ -69,7 +69,7 @@ class MmsSimulatorManifestCodec final { return result; } if (raw == "ENUM") { - // OMICRON IEDScout advertises IEC 61850 Enum leaves as INTEGER(8) + // OMICRON reference IEC 61850 client advertises IEC 61850 Enum leaves as INTEGER(8) // for the captured SIPROTEC model (Mod/Beh/Health/ctlModel). result.kind = MmsTypeKind::integer; result.size = 8U; diff --git a/include/ariec61850/mms/static_brcb_objects.hpp b/include/ariec61850/mms/static_brcb_objects.hpp index 1610c59e2..4f8cc0e69 100644 --- a/include/ariec61850/mms/static_brcb_objects.hpp +++ b/include/ariec61850/mms/static_brcb_objects.hpp @@ -45,7 +45,7 @@ struct MmsStaticBrcbObjectContext final { // opaque association/Owner identity. class MmsStaticBrcbObjectBank final { public: - // Golden IEDScout-compatible BRCB facade: + // Golden reference IEC 61850 client-compatible BRCB facade: // RptID,RptEna,DatSet,ConfRev,OptFlds,BufTm,SqNum,TrgOps,IntgPd,GI, // PurgeBuf,EntryID,TimeofEntry,ResvTms,Owner. static constexpr std::size_t attributes_per_control_block = 15U; diff --git a/include/ariec61850/mms/static_dispatcher.hpp b/include/ariec61850/mms/static_dispatcher.hpp index 09b32ddb8..0481e5fe6 100644 --- a/include/ariec61850/mms/static_dispatcher.hpp +++ b/include/ariec61850/mms/static_dispatcher.hpp @@ -24,7 +24,7 @@ enum class MmsStaticDispatchStatus : std::uint8_t { }; struct MmsStaticDispatchPolicy final { - // The measured IEDScout server discovery profile emits at most 100 + // The measured reference IEC 61850 client server discovery profile emits at most 100 // identifiers per GetNameList page. Keep the portable default aligned with // that profile; tests/embedded users may still choose a smaller bound. std::size_t maximum_names_per_response{100U}; @@ -76,7 +76,7 @@ class MmsStaticApplicationDispatcher final { // directory must be unique, grouped by domain in the desired wire order, // and remain alive for the dispatcher lifetime. Host IED-simulator profiles - // preserve SCL/IEDScout declaration order; embedded profiles may omit the + // preserve SCL/reference IEC 61850 client declaration order; embedded profiles may omit the // directory and retain the fixed-buffer scan path without host-side heap // requirements. MmsStaticApplicationDispatcher( diff --git a/include/ariec61850/mms/static_urcb_objects.hpp b/include/ariec61850/mms/static_urcb_objects.hpp index 09526875a..500e169d3 100644 --- a/include/ariec61850/mms/static_urcb_objects.hpp +++ b/include/ariec61850/mms/static_urcb_objects.hpp @@ -37,7 +37,7 @@ struct MmsStaticUrcbObjectContext final { class MmsStaticUrcbObjectBank final { public: - // IEC 61850 / IEDScout-compatible URCB facade: + // IEC 61850 / reference IEC 61850 client-compatible URCB facade: // RptID,RptEna,Resv,DatSet,ConfRev,OptFlds,BufTm,SqNum, // TrgOps,IntgPd,GI,Owner. static constexpr std::size_t attributes_per_control_block = 12U; diff --git a/scripts/check_product_naming.py b/scripts/check_product_naming.py new file mode 100644 index 000000000..a224a7e59 --- /dev/null +++ b/scripts/check_product_naming.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +"""Fail CI when a prohibited product name enters tracked source or paths.""" +from pathlib import Path +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[1] +PROHIBITED = ("ied" + "scout").encode("ascii") +tracked = subprocess.run(["git", "ls-files", "-z"], cwd=ROOT, + capture_output=True, check=True).stdout.split(b"\0") +violations = [] +for item in tracked: + if not item: + continue + relative = item.decode("utf-8", errors="surrogateescape") + if PROHIBITED in item.lower(): + violations.append("filename: " + relative) + path = ROOT / relative + if not path.is_file(): + continue + with path.open("rb") as handle: + prefix = handle.read(4096) + if b"\0" in prefix: + continue + content = prefix + handle.read() + if PROHIBITED in content.lower(): + violations.append("text: " + relative) +if violations: + sys.stderr.write("REPOSITORY_NAMING_GUARD_FAIL\n" + "\n".join(violations) + "\n") + raise SystemExit(1) +print("REPOSITORY_NAMING_GUARD_PASS") diff --git a/src/mms/connection_runtime_iedsim.cpp b/src/mms/connection_runtime_iedsim.cpp index 87aa7790c..d3f878f97 100644 --- a/src/mms/connection_runtime_iedsim.cpp +++ b/src/mms/connection_runtime_iedsim.cpp @@ -30,7 +30,7 @@ constexpr std::uint32_t kServerMaximumNestingLevel = 5U; constexpr std::array kConservativeStructureType{ 0xA2U, 0x02U, 0xA1U, 0x00U}; -// IEDScout golden empty FileDirectory response: [0] contains an empty +// reference IEC 61850 client golden empty FileDirectory response: [0] contains an empty // SEQUENCE OF DirectoryEntry. moreFollows is DEFAULT FALSE and therefore omitted. // Exact service value: A0 02 30 00. constexpr std::array kEmptyFileDirectoryFields{ diff --git a/src/mms/file_service.cpp b/src/mms/file_service.cpp index b46f74caa..45437cb02 100644 --- a/src/mms/file_service.cpp +++ b/src/mms/file_service.cpp @@ -516,7 +516,7 @@ std::vector MmsFileServiceCodec::encode_file_directory_request_pdu BerWriter body; const auto directory = normalize_remote_path(request.directory_name, true); const auto continuation = normalize_remote_path(request.continue_after, true); - // Captured IEDScout root FileDirectory request keeps fileSpecification + // Captured reference IEC 61850 client root FileDirectory request keeps fileSpecification // present and encodes root as one empty GraphicString: A0 02 19 00. // Do not omit [0] for root; several engineering clients use this wire shape. const auto name = graphic_string(directory); @@ -751,7 +751,7 @@ std::vector MmsFileServiceCodec::encode_file_delete_request_pdu( const auto wire_path = request.rooted_backslash ? rooted_backslash_path(normalized) : normalized; - // Captured IEDScout FileDelete [76] carries the GraphicString directly + // Captured reference IEC 61850 client FileDelete [76] carries the GraphicString directly // inside the constructed service value: BF 4C 19 "\path". return MmsPduCodec::encode_confirmed_request( {request.invoke_id, file_delete_service_tag, true, graphic_string(wire_path)}); diff --git a/src/mms/services.cpp b/src/mms/services.cpp index 5eb251c1d..adb25ab62 100644 --- a/src/mms/services.cpp +++ b/src/mms/services.cpp @@ -197,7 +197,7 @@ std::vector encode_type_impl( } auto encoded = BerWriter::encode_signed_integer( -static_cast(*type.size)); - // OMICRON IEDScout emits variable maximum lengths in at least a + // OMICRON reference IEC 61850 client emits variable maximum lengths in at least a // two-octet signed field. In the golden capture Octet64 is // 89 02 FF C0 (not the mathematically minimal 89 01 C0). if (encoded.size() == 1U) encoded.insert(encoded.begin(), 0xFFU); diff --git a/src/mms/static_brcb_objects.cpp b/src/mms/static_brcb_objects.cpp index 8eb061350..49389d9f3 100644 --- a/src/mms/static_brcb_objects.cpp +++ b/src/mms/static_brcb_objects.cpp @@ -25,7 +25,7 @@ constexpr std::array kVariableOctet64Type{0x89U, 0x02U, 0xFFU, constexpr std::array kVisible129Type{0x8AU, 0x02U, 0xFFU, 0x7FU}; constexpr std::array kBitString10Type{0x84U, 0x01U, 0x0AU}; constexpr std::array kBitString6Type{0x84U, 0x01U, 0x06U}; -// Golden IEDScout TypeSpecification for TimeofEntry is binary-time timeOfDay(1). +// Golden reference IEC 61850 client TypeSpecification for TimeofEntry is binary-time timeOfDay(1). constexpr std::array kBinaryTimeType{0x8CU, 0x01U, 0x01U}; constexpr std::array destination) noexcept { - // IEDScout exposes ctlModel as MMS INTEGER(8), not UNSIGNED. Values 0..4 + // reference IEC 61850 client exposes ctlModel as MMS INTEGER(8), not UNSIGNED. Values 0..4 // are positive one-octet INTEGER encodings, so no sign-extension is needed. constexpr std::size_t required = 3U; if (destination.size() < required) { diff --git a/src/mms/static_dispatcher.cpp b/src/mms/static_dispatcher.cpp index 6566ad9fc..aed679e0f 100644 --- a/src/mms/static_dispatcher.cpp +++ b/src/mms/static_dispatcher.cpp @@ -211,7 +211,7 @@ namespace { const MmsConfirmedPduView& confirmed, const MmsGetNameListRequestView& request, const std::span response) noexcept { - // The host IED-simulator directory intentionally preserves SCL/IEDScout + // The host IED-simulator directory intentionally preserves SCL/reference IEC 61850 client // declaration order instead of lexical item order. Continuation therefore // follows the exact emitted sequence rather than lower_bound() semantics. const auto domain = as_text(request.domain_id); @@ -577,7 +577,7 @@ constexpr std::array kBrcbAttributeOrder{ "RptID", "RptEna", "DatSet", "ConfRev", "OptFlds", "BufTm", "SqNum", "TrgOps", "IntgPd", "GI", "PurgeBuf", "EntryID", "TimeofEntry", "ResvTms", "Owner"}; -constexpr std::array kIedScoutControlServiceOrder{ +constexpr std::array kReferenceIec61850ControlServiceOrder{ "SBO", "SBOw", "Oper", "Cancel"}; enum class SyntheticReadStatus : std::uint8_t { @@ -636,12 +636,12 @@ struct SyntheticChildRank final { if (prefix.find("$BR$") != std::string_view::npos) { return {kBrcbAttributeOrder}; } - // OMICRON IEDScout exposes control-service alternatives in this exact + // OMICRON reference IEC 61850 client exposes control-service alternatives in this exact // order at LN$CO$DO. Deeper service structures remain positional. if (prefix.find("$CO$") != std::string_view::npos && static_cast(std::count( prefix.begin(), prefix.end(), static_cast(0x24))) == 2U) { - return {kIedScoutControlServiceOrder}; + return {kReferenceIec61850ControlServiceOrder}; } return {}; } @@ -675,7 +675,7 @@ struct SyntheticChildRank final { [[nodiscard]] bool use_declaration_order( const std::string_view prefix) noexcept { - // IEDScout preserves the engineering declaration order at the LN root, + // reference IEC 61850 client preserves the engineering declaration order at the LN root, // functional-constraint namespace, DO and DA levels. Every structural // object imported from SCL carries source_order; runtime-only service // objects receive an explicit order from their owning control/RCB. @@ -748,7 +748,7 @@ struct SyntheticChildren final { return synthetic_child_less(prefix, left.prefix, right.prefix); } if (declaration_order) { - // Golden IEDScout places CO immediately after ST (or after MX/ST + // Golden reference IEC 61850 client places CO immediately after ST (or after MX/ST // when MX is present) at controlled LN roots, before CF. if (prefix.find(static_cast(0x24)) == std::string_view::npos) { const auto left_name = left.prefix.substr(prefix.size() + 1U); diff --git a/src/mms/static_urcb_objects.cpp b/src/mms/static_urcb_objects.cpp index 3fb7b7a1c..ae4eb01b5 100644 --- a/src/mms/static_urcb_objects.cpp +++ b/src/mms/static_urcb_objects.cpp @@ -19,7 +19,7 @@ namespace { constexpr std::array kBooleanType{0x83U, 0x00U}; constexpr std::array kUnsigned8Type{0x86U, 0x01U, 0x08U}; constexpr std::array kUnsigned32Type{0x86U, 0x01U, 0x20U}; -// Golden IEDScout server uses the variable-length VisibleString(129) form. +// Golden reference IEC 61850 client server uses the variable-length VisibleString(129) form. constexpr std::array kVisible129Type{0x8AU, 0x02U, 0xFFU, 0x7FU}; constexpr std::array kVariableOctet64Type{0x89U, 0x02U, 0xFFU, 0xC0U}; constexpr std::array kBitString10Type{0x84U, 0x01U, 0x0AU}; diff --git a/src/simulation/ied_simulator_profile.cpp b/src/simulation/ied_simulator_profile.cpp index 744bd324e..bf9f5d97f 100644 --- a/src/simulation/ied_simulator_profile.cpp +++ b/src/simulation/ied_simulator_profile.cpp @@ -195,7 +195,7 @@ namespace { // q/t are semantic terminal DataAttribute names, not arbitrary suffixes. // The former ends_with("t") rule misclassified orCat, orIdent, // sboTimeout, operTimeout and Test as Timestamp, corrupting the live MMS - // TypeSpecification seen by OMICRON IEDScout. + // TypeSpecification seen by OMICRON reference IEC 61850 client. if (entry.is_quality || attribute_leaf_is(entry.da_name, "q") || ascii_equal(entry.basic_type, "Quality")) { return "Quality"; diff --git a/tests/test_control_mms_structure.cpp b/tests/test_control_mms_structure.cpp index d3da75427..a36e670e1 100644 --- a/tests/test_control_mms_structure.cpp +++ b/tests/test_control_mms_structure.cpp @@ -121,7 +121,7 @@ void dpc_binding_matches_network_bit_order() { CHECK(result.value->raw_value()[1] == 0x40U); // DPC Off == binary 01. } -void dpc_open_close_bind_to_iedscout_boolean_ctlval() { +void dpc_open_close_bind_to_reference_iec61850_boolean_ctlval() { const auto specification = scalar(MmsTypeKind::boolean, "ctlVal"); auto build = MmsControlStructureBuilder::bind_control_value( @@ -272,7 +272,7 @@ void step_position_and_analogue_structures_follow_live_shape() { int main() { try { dpc_binding_matches_network_bit_order(); - dpc_open_close_bind_to_iedscout_boolean_ctlval(); + dpc_open_close_bind_to_reference_iec61850_boolean_ctlval(); operate_matches_csharp_structure_contract_and_golden_wire(); sbow_uses_same_exact_sequence_contract(); cancel_allows_optional_check_but_not_vendor_guessing(); diff --git a/tests/test_mms_file_service.cpp b/tests/test_mms_file_service.cpp index bffb105d6..073818401 100644 --- a/tests/test_mms_file_service.cpp +++ b/tests/test_mms_file_service.cpp @@ -269,7 +269,7 @@ void codec_encodes_file_directory_high_tag_and_continuation() { {8U, "/", {}}); const auto decoded_root = mms::MmsPduCodec::decode_confirmed_request(root); CHECK(decoded_root.service_tag == 77); - // Captured IEDScout root request service value: A0 02 19 00. + // Captured reference IEC 61850 client root request service value: A0 02 19 00. CHECK(decoded_root.service_value == (ByteVector{0xA0U, 0x02U, 0x19U, 0x00U})); } @@ -304,21 +304,21 @@ void codec_normalizes_paths_and_round_trips_signed_frsm() { CHECK(mms::MmsFileServiceCodec::rooted_backslash_path( "COMTRADE/FRA00028.dat") == "\\COMTRADE\\FRA00028.dat"); - // Captured IEDScout FileOpen uses a rooted backslash filename and position 0. - const auto iedscout_open = mms::MmsFileServiceCodec::encode_file_open_request_pdu( + // Captured reference IEC 61850 client FileOpen uses a rooted backslash filename and position 0. + const auto reference_iec61850_open = mms::MmsFileServiceCodec::encode_file_open_request_pdu( {123U, "ligne_1.DAT", 0U, true}); - const auto decoded_iedscout_open = mms::MmsPduCodec::decode_confirmed_request( - iedscout_open); - CHECK(decoded_iedscout_open.service_tag == 72); - const auto iedscout_fields = asn1::BerReader::read_children( - decoded_iedscout_open.service_value); - CHECK(iedscout_fields.size() == 2U); - const auto iedscout_names = asn1::BerReader::read_children( - iedscout_fields.front().value); - CHECK(iedscout_names.size() == 1U); - CHECK(asn1::BerReader::read_ascii_string(iedscout_names.front()) == + const auto decoded_reference_iec61850_open = mms::MmsPduCodec::decode_confirmed_request( + reference_iec61850_open); + CHECK(decoded_reference_iec61850_open.service_tag == 72); + const auto reference_iec61850_fields = asn1::BerReader::read_children( + decoded_reference_iec61850_open.service_value); + CHECK(reference_iec61850_fields.size() == 2U); + const auto reference_iec61850_names = asn1::BerReader::read_children( + reference_iec61850_fields.front().value); + CHECK(reference_iec61850_names.size() == 1U); + CHECK(asn1::BerReader::read_ascii_string(reference_iec61850_names.front()) == "\\ligne_1.DAT"); - CHECK(asn1::BerReader::read_unsigned_integer(iedscout_fields[1]) == 0U); + CHECK(asn1::BerReader::read_unsigned_integer(reference_iec61850_fields[1]) == 0U); check_throws([] { static_cast(mms::MmsFileServiceCodec::encode_file_open_request_pdu( {1U, "../secret.cfg", 0U, false})); @@ -354,7 +354,7 @@ void codec_normalizes_paths_and_round_trips_signed_frsm() { CHECK(opened.file_size_bytes == 4'096U); } -void codec_matches_iedscout_filedelete_golden_shape() { +void codec_matches_reference_iec61850_filedelete_golden_shape() { const auto request = mms::MmsFileServiceCodec::encode_file_delete_request_pdu( {0x95U, "30_248202310947607_FRA00030.cfg", true}); const auto decoded = mms::MmsPduCodec::decode_confirmed_request(request); @@ -691,7 +691,7 @@ int main() { {"codec file-directory request", codec_encodes_file_directory_high_tag_and_continuation}, {"codec file-directory response", codec_decodes_directory_attributes_and_preserves_order}, {"codec path and signed FRSM", codec_normalizes_paths_and_round_trips_signed_frsm}, - {"codec IEDScout FileDelete", codec_matches_iedscout_filedelete_golden_shape}, + {"codec reference IEC 61850 client FileDelete", codec_matches_reference_iec61850_filedelete_golden_shape}, {"codec malformed evidence", codec_rejects_invoke_mismatch_malformed_and_trailing_data}, {"directory bounded pagination", directory_runtime_paginates_deduplicates_and_detects_no_progress}, {"runtime single-shot FileDelete", runtime_filedelete_is_single_shot_and_never_adaptive}, diff --git a/tests/test_mms_server_urcb_report.cpp b/tests/test_mms_server_urcb_report.cpp index 9f4092d0b..f7ea44689 100644 --- a/tests/test_mms_server_urcb_report.cpp +++ b/tests/test_mms_server_urcb_report.cpp @@ -258,11 +258,11 @@ int main() { return 1; } - constexpr std::array iedscout_generic_optflds{0x7BU, 0x80U}; + constexpr std::array reference_iec61850_generic_optflds{0x7BU, 0x80U}; constexpr std::array effective_urcb_optflds{0x78U, 0x80U}; constexpr std::array unsupported_segmentation{0x78U, 0xC0U}; constexpr std::array original_optflds{0x7CU, 0x80U}; - if (reports.set_optional_fields(0U, iedscout_generic_optflds) != + if (reports.set_optional_fields(0U, reference_iec61850_generic_optflds) != mms::MmsStaticUrcbStatus::ok) { return 44; } diff --git a/tests/test_mms_services.cpp b/tests/test_mms_services.cpp index 6eb753043..029d3d2c5 100644 --- a/tests/test_mms_services.cpp +++ b/tests/test_mms_services.cpp @@ -217,7 +217,7 @@ void type_specification_and_attributes_round_trip() { response_p_data, 31U) == response); } -void iedscout_variable_length_type_bounds_round_trip() { +void reference_iec61850_variable_length_type_bounds_round_trip() { MmsTypeSpecification visible255; visible255.kind = MmsTypeKind::visible_string; visible255.size = 255U; @@ -425,7 +425,7 @@ int main() { {"ObjectName choices", object_names_round_trip_all_choices}, {"GetNameList", get_name_list_request_response_round_trip}, {"VariableAccessAttributes and structure type", type_specification_and_attributes_round_trip}, - {"IEDScout variable-length TypeSpecification", iedscout_variable_length_type_bounds_round_trip}, + {"reference IEC 61850 client variable-length TypeSpecification", reference_iec61850_variable_length_type_bounds_round_trip}, {"simulator SCL bType wire authority", simulator_manifest_type_mapping_prefers_scl_btype}, {"array TypeSpecification", array_type_specification_round_trips}, {"Read multi-access results", read_request_and_multi_access_response_round_trip}, diff --git a/tests/test_mms_static_direct_control.cpp b/tests/test_mms_static_direct_control.cpp index b05442ac4..356a8ce42 100644 --- a/tests/test_mms_static_direct_control.cpp +++ b/tests/test_mms_static_direct_control.cpp @@ -145,7 +145,7 @@ void valid_oper_updates_live_state() { CHECK(state.rejected_operations == 0U); } -void golden_iedscout_integer_origin_and_checks_decode() { +void golden_reference_iec61850_integer_origin_and_checks_decode() { const auto bytes = make_oper(true, 41U, false, 0xC0U, 2U, true); MmsStaticDirectBooleanOperate decoded; CHECK(try_decode_static_direct_boolean_operate(bytes, decoded)); @@ -197,7 +197,7 @@ void invalid_shape_and_values_are_rejected() { CHECK(!result.success); CHECK(result.failure_code == 7U); - // ctlNum=0 is valid on the incoming server wire. OMICRON IEDScout uses it. + // ctlNum=0 is valid on the incoming server wire. OMICRON reference IEC 61850 client uses it. result = mms_static_direct_boolean_write_oper(&binding, make_oper(true, 0U, false)); CHECK(result.success); CHECK(state.last_control_number == 0U); @@ -320,7 +320,7 @@ void sbo_enhanced_requires_exact_selected_sequence() { CHECK(shared.selected_association_id.load() == 0U); } -void iedscout_sbo_enhanced_accepts_ctl_num_zero_and_fresh_oper_timestamp() { +void reference_iec61850_sbo_enhanced_accepts_ctl_num_zero_and_fresh_oper_timestamp() { TestClock clock{100U}; MmsStaticDirectBooleanSharedState shared{}; MmsStaticDirectBooleanControlState state{}; @@ -329,7 +329,7 @@ void iedscout_sbo_enhanced_accepts_ctl_num_zero_and_fresh_oper_timestamp() { binding.policy.allow_synchro_check = true; binding.policy.allow_interlock_check = true; - // Captured OMICRON IEDScout behavior: + // Captured OMICRON reference IEC 61850 client behavior: // SBOw uses ctlNum=0 and T1; Oper keeps the selected identity but sends T2. const auto selected = mms_static_boolean_write_sbow_contextual( &binding, @@ -436,7 +436,7 @@ void read_callbacks_match_mms_types() { int main() { try { valid_oper_updates_live_state(); - golden_iedscout_integer_origin_and_checks_decode(); + golden_reference_iec61850_integer_origin_and_checks_decode(); test_oper_is_non_mutating(); unsupported_check_bits_fail_closed(); invalid_shape_and_values_are_rejected(); @@ -444,7 +444,7 @@ int main() { sbo_normal_enforces_owner_cancel_and_timeout(); direct_enhanced_queues_one_termination(); sbo_enhanced_requires_exact_selected_sequence(); - iedscout_sbo_enhanced_accepts_ctl_num_zero_and_fresh_oper_timestamp(); + reference_iec61850_sbo_enhanced_accepts_ctl_num_zero_and_fresh_oper_timestamp(); sbo_enhanced_cancel_and_association_close_release_owner(); read_callbacks_match_mms_types(); std::cout << "MMS static direct-control tests passed.\n"; diff --git a/tests/test_simulation.cpp b/tests/test_simulation.cpp index 4cd7a23b0..52e09c548 100644 --- a/tests/test_simulation.cpp +++ b/tests/test_simulation.cpp @@ -198,7 +198,7 @@ void profile_builder_does_not_suffix_match_timestamp_or_quality() { using namespace ar::iec61850; scl::SclDocument document; - document.source_name = "iedscout-type-regression.scd"; + document.source_name = "reference_iec61850-type-regression.scd"; scl::SclIed ied; ied.name = "IEDTYPE"; document.ieds.push_back(std::move(ied)); diff --git a/tools/static_ied_server_iedsim/CMakeLists.txt b/tools/static_ied_server_iedsim/CMakeLists.txt index d0feda6f1..b711b98bb 100644 --- a/tools/static_ied_server_iedsim/CMakeLists.txt +++ b/tools/static_ied_server_iedsim/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) -project(ARStack61850IedScoutParityServer LANGUAGES CXX) +project(ARStack61850ReferenceIec61850ParityServer LANGUAGES CXX) get_filename_component(ARSTACK_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) diff --git a/tools/static_ied_server_iedsim/test_control_golden.py b/tools/static_ied_server_iedsim/test_control_golden.py index 3e74408f3..fc15e2e3f 100644 --- a/tools/static_ied_server_iedsim/test_control_golden.py +++ b/tools/static_ied_server_iedsim/test_control_golden.py @@ -150,7 +150,7 @@ def main() -> int: ) print( - "IEDSCOUT_DPC_CONTROL_GOLDEN_PASS " + "REFERENCE_IEC61850_DPC_CONTROL_GOLDEN_PASS " "sequence=SBOw,Oper,CommandTermination,ProcessFeedback " "ctlVal=boolean process=DBPOS2 originCat=integer " "check=sync+interlock" diff --git a/tools/static_ied_server_iedsim/test_cswi1_type_golden.py b/tools/static_ied_server_iedsim/test_cswi1_type_golden.py index 44283a584..c52710959 100644 --- a/tools/static_ied_server_iedsim/test_cswi1_type_golden.py +++ b/tools/static_ied_server_iedsim/test_cswi1_type_golden.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Exact OMICRON IEDScout CSWI1 TypeSpecification golden regression.""" +"""Exact OMICRON reference IEC 61850 client CSWI1 TypeSpecification golden regression.""" from __future__ import annotations @@ -29,7 +29,7 @@ def manifest() -> str: "ARSTACK_IED_MODEL\t2\t1", f"LN\t{d}\tCSWI1", - # ST — exact leaf order/type shape captured from OMICRON IEDScout. + # ST — exact leaf order/type shape captured from OMICRON reference IEC 61850 client. f"OBJ\t{d}\tCSWI1$ST$Mod$stVal\tENUM\tEnumeration\t1", f"OBJ\t{d}\tCSWI1$ST$Mod$q\tQUALITY\tQuality\tgood", f"OBJ\t{d}\tCSWI1$ST$Mod$t\tTimestamp\tTimestamp\tunix-ms:1720000000000", @@ -61,7 +61,7 @@ def manifest() -> str: # Keep engineering declaration order deliberately as the failed ARStack # capture (CF/DC/EX/OR before CO). Dispatcher parity must still project - # the IEDScout root order ST,CO,CF,DC,EX,OR. + # the reference IEC 61850 client root order ST,CO,CF,DC,EX,OR. f"OBJ\t{d}\tCSWI1$CF$Mod$ctlModel\tENUM\tEnumeration\t0", f"OBJ\t{d}\tCSWI1$CF$Pos$ctlModel\tENUM\tEnumeration\t4", f"OBJ\t{d}\tCSWI1$CF$Pos$sboTimeout\tINT32U\tNumber\t10000", @@ -303,7 +303,7 @@ def main() -> int: ) print( - "IEDSCOUT_CSWI1_TYPE_GOLDEN_PASS " + "REFERENCE_IEC61850_CSWI1_TYPE_GOLDEN_PASS " "leaves=72 root=ST,CO,CF,DC,EX,OR " "control=SBO,SBOw,Oper,Cancel mismatches=0" ) diff --git a/tools/static_ied_server_iedsim/test_file_service.py b/tools/static_ied_server_iedsim/test_file_service.py index d4cc13c6e..ce20b0f29 100644 --- a/tools/static_ied_server_iedsim/test_file_service.py +++ b/tools/static_ied_server_iedsim/test_file_service.py @@ -107,7 +107,7 @@ def main() -> int: f"server diagnostics missing {missing}\nstdout={stdout}\nstderr={stderr}" ) print( - "IEDSCOUT_FILE_SERVER_LOOPBACK_PASS " + "REFERENCE_IEC61850_FILE_SERVER_LOOPBACK_PASS " "directory=pass open=pass read=pass close=pass delete=pass " "refresh=pass segmented_payload=70000" ) diff --git a/tools/static_ied_server_iedsim/test_reporting_golden.cpp b/tools/static_ied_server_iedsim/test_reporting_golden.cpp index 46f6459bb..67c12cc7f 100644 --- a/tools/static_ied_server_iedsim/test_reporting_golden.cpp +++ b/tools/static_ied_server_iedsim/test_reporting_golden.cpp @@ -152,8 +152,8 @@ int main() { return 2; } - // Real IEDScout static-URCB acceptance capture: - // IEDScout did not rewrite OptFlds. The configured/effective report profile + // Real reference IEC 61850 client static-URCB acceptance capture: + // reference IEC 61850 client did not rewrite OptFlds. The configured/effective report profile // emits OptFlds 78 80, SqNum=1, DataSet Analog, ConfRev=100001, // 22/22 included members and GI ReasonForInclusion. const std::array urcb_definitions{{ @@ -222,7 +222,7 @@ int main() { return 8; } - // Real IEDScout BRCB acceptance capture: + // Real reference IEC 61850 client BRCB acceptance capture: // RptID ...$BR$Buffer, OptFlds 79 80, SqNum=1, // DataSet Digital, EntryID=1, ConfRev=100001, 36/36 included, GI reason. const mms::MmsStaticBrcbDefinition brcb_definition{ @@ -382,7 +382,7 @@ int main() { } std::cout - << "IEDSCOUT_REPORTING_GOLDEN_PASS " + << "REFERENCE_IEC61850_REPORTING_GOLDEN_PASS " << "urcbMembers=22 urcbOptFlds=7880 urcbSqNum=1 " << "brcbMembers=36 brcbOptFlds=7980 " << "brcbGiSqNum=1 brcbGiEntryID=1 "