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
7 changes: 6 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -562,10 +562,15 @@ if(ARIEC61850_BUILD_TESTS)
add_test(NAME ariec61850_ptp_sv_wire_audit_selftest
COMMAND ariec61850_ptp_sv_wire_audit --self-test)
add_test(
NAME ariec61850_pcap_interop_smoke
NAME ariec61850_pcap_interop_detects_noncanonical_legacy
COMMAND ariec61850_pcap_interop_check
${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/process_bus_csharp_vectors.pcap
--json)
# Historical fixture has min-width integers; under canonical IEC
# widths the audit must reject an exact byte-for-byte re-encode.
# The C++ evidence test also asserts the precise mismatch reason/count.
set_tests_properties(ariec61850_pcap_interop_detects_noncanonical_legacy
PROPERTIES WILL_FAIL TRUE)
add_test(
NAME ariec61850_comtrade_inspect_smoke
COMMAND ariec61850_comtrade_inspect
Expand Down
2 changes: 2 additions & 0 deletions apps/arstack_studio/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,14 @@ qt_add_resources(arstack_studio "reference_templates"
PREFIX "/arstack"
FILES
templates/arstack_4i4v_9-2le_reference.scd
templates/arstack_4i4v_9-2le_metadata_lab.scd
)

qt_add_resources(arstack_session_harness "session_harness_reference_templates"
PREFIX "/arstack"
FILES
templates/arstack_4i4v_9-2le_reference.scd
templates/arstack_4i4v_9-2le_metadata_lab.scd
)

target_link_libraries(arstack_studio PRIVATE
Expand Down
19 changes: 19 additions & 0 deletions apps/arstack_studio/src/DeterministicSessionHarness.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,25 @@ class DeterministicSessionHarness final {
firmware.firmwareBuildId_ = QStringLiteral("0123456789abcdef");
firmware.bundleReady_ = true;
profileReady = profiles.loadReferenceTemplate();
if (profileReady) {
// Production Quick Start must compile the VLAN0/PCP4 metadata
// SCL, NOT silently reuse the older no-datSet/no-rate artifact.
const auto metadata = profiles.selectedProfile();
profileReady =
metadata.value(QStringLiteral("svId")).toString() == QStringLiteral("ARSTACK_SV01") &&
metadata.value(QStringLiteral("appId")).toInt() == 0x4000 &&
metadata.value(QStringLiteral("vlanPresent")).toBool() &&
metadata.value(QStringLiteral("vlanId")).toInt() == 0 &&
metadata.value(QStringLiteral("vlanPriority")).toInt() == 4 &&
metadata.value(QStringLiteral("includeDataSet")).toBool() &&
metadata.value(QStringLiteral("includeSampleRate")).toBool() &&
metadata.value(QStringLiteral("wireSmpRate")).toULongLong() == 4000ULL &&
metadata.value(QStringLiteral("wireSmpMod")).toInt() == 1 &&
metadata.value(QStringLiteral("payloadBytes")).toULongLong() == 64ULL &&
metadata.value(QStringLiteral("channelLeafCount")).toULongLong() == 16ULL;
if (!profileReady)
qCritical() << "Quick Start semantic SCL metadata regression";
}
session.setProfiles(&profiles);
session.setFirmware(&firmware);
session.setDevice(&device);
Expand Down
6 changes: 3 additions & 3 deletions apps/arstack_studio/src/SclProfileModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ using ar::iec61850::sampled_values::sv_profile_family_resolution_name;
using ar::iec61850::sampled_values::sv_transport_mode_name;

constexpr std::uint16_t kReferenceCounterModulus = 4000;
constexpr auto kReferenceResource = ":/arstack/templates/arstack_4i4v_9-2le_reference.scd";
constexpr auto kReferenceSourceName = "ARStack 4I+4V 9-2LE Reference";
constexpr auto kReferenceResource = ":/arstack/templates/arstack_4i4v_9-2le_metadata_lab.scd";
constexpr auto kReferenceSourceName = "ARStack 4I+4V VLAN-0 Metadata Laboratory Reference";

QString qstring(const std::string& value) {
return QString::fromStdString(value);
Expand Down Expand Up @@ -185,7 +185,7 @@ QString SclProfileModel::sourceName() const {
QString SclProfileModel::documentStatus() const {
if (!document_.has_value()) return QStringLiteral("No engineering file loaded");
if (referenceTemplateActive_) {
return QStringLiteral("4I+4V 9-2LE reference · %1 resolved SV stream%2")
return QStringLiteral("4I+4V VLAN-0 metadata lab · %1 resolved SV stream%2")
.arg(static_cast<qulonglong>(rows_.size()))
.arg(rows_.size() == 1U ? QString{} : QStringLiteral("s"));
}
Expand Down
15 changes: 13 additions & 2 deletions apps/arstack_studio/templates/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# ARStack Studio reference templates

The first public-release target intentionally ships one built-in Sampled Values reference profile:
The default 4I+4V Quick Start now uses a metadata-enabled, priority-tagged
IEC 61850 SV laboratory profile. The previously published minimal 9-2LE
reference remains unchanged and available for strict legacy interoperability.

The default laboratory reference profile has:

- **4 current + 4 voltage** (`Ia, Ib, Ic, In, Ua, Ub, Uc, Un`)
- `INT32 value + Quality` for each channel
Expand All @@ -13,7 +17,14 @@ The first public-release target intentionally ships one built-in Sampled Values
- VLAN VID 0 / PCP 4
- configuration revision 1

`arstack_4i4v_9-2le_reference.scd` is a convenience engineering source, not a second runtime profile implementation. ARStack Studio loads it through the same `SclParser -> SvPublisherProfileCompiler -> ESP32-P4 support classifier` path used for external SCL/CID/SCD/IID files.
`arstack_4i4v_9-2le_metadata_lab.scd` is the packaged **Quick Start** source
(802.1Q priority-tagged VID 0 / PCP 4, dataSet=true, sampleRate=true,
smpRate=4000, smpMod=SmpPerSec, 16 ordered leaves, one ASDU).
`arstack_4i4v_9-2le_reference.scd` remains the separate minimal 9-2LE
compatibility fixture with these optional fields absent. Use the matching SCL
and wire profile required by the receiving relay/analyzer.

Both are engineering sources, not secondary runtime profile implementations. ARStack Studio loads the selected profile through the same `SclParser -> SvPublisherProfileCompiler -> ESP32-P4 support classifier` path used for external SCL/CID/SCD/IID files.

## Counter policy

Expand Down
72 changes: 72 additions & 0 deletions apps/arstack_studio/templates/arstack_4i4v_9-2le_metadata_lab.scd
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<SCL xmlns="http://www.iec.ch/61850/2003/SCL" xmlns:sxy="http://www.iec.ch/61850/2003/SCLcoordinates" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.iec.ch/61850/2003/SCL SCL.xsd http://www.iec.ch/61850/2003/SCLcoordinates">
<Header id="ARStack-4I4V-9-2LE-Metadata-Lab"/>
<Communication>
<SubNetwork name="ProcessBus" desc="ARStack SMV reference process bus" type="8-MMS">
<ConnectedAP iedName="ARSTACK_MU01" apName="LD0">
<SMV ldInst="MU01" cbName="MSVCB01">
<Address>
<P type="VLAN-ID" xsi:type="tP_VLAN-ID">0</P>
<P type="VLAN-PRIORITY" xsi:type="tP_VLAN-PRIORITY">4</P>
<P type="MAC-Address" xsi:type="tP_MAC-Address">01-0C-CD-04-00-01</P>
<P type="APPID" xsi:type="tP_APPID">4000</P>
</Address>
</SMV>
</ConnectedAP>
</SubNetwork>
</Communication>
<IED name="ARSTACK_MU01" desc="ARStack 4I+4V reference merging unit" type="ARStack-SMV-Reference" manufacturer="ARStack61850" configVersion="1.0">
<AccessPoint name="LD0">
<Server>
<LDevice desc="ARStack 4I+4V reference" inst="MU01">
<LN0 desc="ARStack 4I+4V reference" lnClass="LLN0" inst="" lnType="MULLN0_ED1_A1_0">
<DataSet name="PhsMeas1">
<FCDA ldInst="MU01" prefix="" lnClass="TCTR" lnInst="1" doName="AmpSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" prefix="" lnClass="TCTR" lnInst="1" doName="AmpSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" prefix="" lnClass="TCTR" lnInst="2" doName="AmpSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" prefix="" lnClass="TCTR" lnInst="2" doName="AmpSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" prefix="" lnClass="TCTR" lnInst="3" doName="AmpSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" prefix="" lnClass="TCTR" lnInst="3" doName="AmpSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" prefix="" lnClass="TCTR" lnInst="4" doName="AmpSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" prefix="" lnClass="TCTR" lnInst="4" doName="AmpSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" prefix="" lnClass="TVTR" lnInst="1" doName="VolSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" prefix="" lnClass="TVTR" lnInst="1" doName="VolSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" prefix="" lnClass="TVTR" lnInst="2" doName="VolSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" prefix="" lnClass="TVTR" lnInst="2" doName="VolSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" prefix="" lnClass="TVTR" lnInst="3" doName="VolSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" prefix="" lnClass="TVTR" lnInst="3" doName="VolSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" prefix="" lnClass="TVTR" lnInst="4" doName="VolSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" prefix="" lnClass="TVTR" lnInst="4" doName="VolSv" daName="q" fc="MX"/>
</DataSet>
<SampledValueControl multicast="true" name="MSVCB01" datSet="PhsMeas1" confRev="1" smvID="ARSTACK_SV01" smpRate="4000" smpMod="SmpPerSec" nofASDU="1">
<SmvOpts refreshTime="false" sampleSynchronized="true" sampleRate="true" dataSet="true" security="false" synchSourceId="false"/>
</SampledValueControl>
</LN0>
<LN lnType="CTR-P" lnClass="TCTR" inst="1"/>
<LN lnType="CTR-P" lnClass="TCTR" inst="2"/>
<LN lnType="CTR-P" lnClass="TCTR" inst="3"/>
<LN lnType="CTR-N" lnClass="TCTR" inst="4"/>
<LN lnType="VTR-P" lnClass="TVTR" inst="1"/>
<LN lnType="VTR-P" lnClass="TVTR" inst="2"/>
<LN lnType="VTR-P" lnClass="TVTR" inst="3"/>
<LN lnType="VTR-N" lnClass="TVTR" inst="4"/>
</LDevice>
</Server>
</AccessPoint>
</IED>
<DataTypeTemplates>
<LNodeType id="MULLN0_ED1_A1_0" lnClass="LLN0"/>
<LNodeType id="CTR-P" lnClass="TCTR"><DO name="AmpSv" type="AmpSAV"/></LNodeType>
<LNodeType id="CTR-N" lnClass="TCTR"><DO name="AmpSv" type="AmpSAV"/></LNodeType>
<LNodeType id="VTR-P" lnClass="TVTR"><DO name="VolSv" type="VolSAV"/></LNodeType>
<LNodeType id="VTR-N" lnClass="TVTR"><DO name="VolSv" type="VolSAV"/></LNodeType>
<DOType id="AmpSAV" cdc="SAV">
<DA name="instMag" bType="Struct" type="AnalogueValue" fc="MX"/>
<DA name="q" bType="Quality" fc="MX" qchg="true"/>
</DOType>
<DOType id="VolSAV" cdc="SAV">
<DA name="instMag" bType="Struct" type="AnalogueValue" fc="MX"/>
<DA name="q" bType="Quality" fc="MX" qchg="true"/>
</DOType>
<DAType id="AnalogueValue"><BDA name="i" bType="INT32"/></DAType>
</DataTypeTemplates>
</SCL>
4 changes: 2 additions & 2 deletions apps/arstack_studio/templates/arstack_4i4v_untagged_lab.scd
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<SCL xmlns="http://www.iec.ch/61850/2003/SCL" xmlns:sxy="http://www.iec.ch/61850/2003/SCLcoordinates" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.iec.ch/61850/2003/SCL SCL.xsd http://www.iec.ch/61850/2003/SCLcoordinates">
<Header id="ARStack-4I4V-Untagged-Lab"/>
<Header id="ARStack-4I4V-Untagged-Metadata-Interop"/>
<Communication>
<SubNetwork name="ProcessBus" desc="ARStack untagged Ethernet SV laboratory profile" type="8-MMS">
<ConnectedAP iedName="ARSTACK_MU01" apName="LD0">
Expand Down Expand Up @@ -36,7 +36,7 @@
<FCDA ldInst="MU01" prefix="" lnClass="TVTR" lnInst="4" doName="VolSv" daName="q" fc="MX"/>
</DataSet>
<SampledValueControl multicast="true" name="MSVCB01" datSet="PhsMeas1" confRev="1" smvID="ARSTACK_SV01" smpRate="4000" smpMod="SmpPerSec" nofASDU="1">
<SmvOpts refreshTime="false" sampleSynchronized="true" sampleRate="false" dataSet="false" security="false" synchSourceId="false"/>
<SmvOpts refreshTime="false" sampleSynchronized="true" sampleRate="true" dataSet="true" security="false" synchSourceId="false"/>
</SampledValueControl>
</LN0>
<LN lnType="CTR-P" lnClass="TCTR" inst="1"/>
Expand Down
21 changes: 21 additions & 0 deletions docs/SV_WIRE_METADATA_INTEROP_2026_11_04.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SV wire metadata / analyzer interoperability — 2026-11-04

## Independent wire evidence
Operator-supplied 35.03 s classic Ethernet PCAP: 140,116 SV frames, approximately 3,999.91 frame/s, no sample-counter gaps, duplicates or malformed frames in the independent report. One ASDU per frame; each seqData is 64 bytes of eight signed INT32 + UINT32 quality pairs. The current capture sends svID=ARSTACK_SV01, confRev=1, smpCnt with 4,000-count rollover, and smpSynch 0/2. It contains **no ASDU datSet [1], smpRate [6], or smpMod [8]**; it does contain optional gmIdentity [9] despite the bundled SCL setting synchSourceId=false. This capture **does not** establish global time synchronization.

The older analyzer screenshot says `Mode: Live` and `Scope view · Synthetic locked timebase`; this is **not** the current SVGen WPF raw analyzer (`RawAnalyzerDataSource`). Update the app to the exact tested PR #3 preview before interpreting the new raw waveform path.

## Standards and profile truth
IEC 61850-9-2 Ed2.1 ASDU requires svID, smpCnt (two octets), confRev (four octets), smpSynch (one octet), seqData. datSet, smpRate, smpMod and gmIdentity are OPTIONAL on the wire; their field presence must match a supported SCL SmvOpts profile and the receiving device's expected dataset.

- The **default 4I+4V Quick Start** now loads `arstack_4i4v_9-2le_metadata_lab.scd`, matching the operator's actual **802.1Q VID 0 / PCP 4** capture; it explicitly selects `SmvOpts dataSet=true sampleRate=true` and emits configured datSet, smpRate=4000, smpMod=1 (SmpPerSec) through the existing immutable binary profile pipeline.
- `arstack_4i4v_untagged_lab.scd` is separately metadata-enabled for untagged capture networks only; do NOT switch a priority-tagged relay to untagged silently.
- The original `arstack_4i4v_9-2le_reference.scd` stays minimal for strict legacy receivers and historical PCAP regression. This remains an explicitly selected legacy option, not the Quick Start default.
- Canonical BER ASDU now encodes smpCnt=2 bytes, confRev=4, smpSynch=1, smpRate=2 and smpMod=2; firmware still prebuilds packet bytes before 4 kHz TX.
- Do not advertise globally synchronized (2) from an internally generated lab grandmaster. Local measured phase/MAC evidence may support 1, otherwise advertise 0. The unsupported synchSourceId option does not authorize gmIdentity [9]. No real UTC traceability, PTP lock, synchronized MAC egress, accuracy or calibration is claimed.
- The original capture will not gain fields retroactively. To apply new SCL options, operator must **STOP SV**, load the updated lab SCL profile, inspect compiled values, **Deploy with authoritative readback**, then operator **START**. To apply codec/synchronization changes, a revision-qualified firmware update with full stopped-board proof and physical re-test is separately required; never auto-flash or replace a running board.

## Independent acceptance
Run `python tools/smv_wire_capture_audit.py <capture.pcap> --minimum-seconds 10 --require-metadata` for metadata-enabled lab profile. This requires both field presence and rate/mode consistency and fails when fields are absent; legacy captures can still pass ordinary parser-only acceptance with no `--require-metadata`. Use `--require-unsynchronized` only in a lab where no qualified source reference exists. Failures do not mean every standards-compliant legacy ASDU must contain optional fields.

For physical acceptance: corroborate exact source MAC/APPID/VLAN/VID/PCP, svID/confRev/ASDU metadata, SCL DataSet leaf sequence, 4k fps and smpCnt 3999→0, per-channel RMS/phase and quality, capture-drop counters, PTP state and independent UTC traceability. Do not convert synthetic/unit/CI evidence into an interoperability certification.
35 changes: 4 additions & 31 deletions embedded/esp32p4_smv_injector/main/app_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -370,37 +370,10 @@ SampledValuesFrame make_runtime_frame(
}
asdu.sample_payload = make_4i4v_payload();

#if CONFIG_AR_PTP_LAB_TX
// IEC 61850-9-2 Ed2.1 optional gmIdentity / SynchSrcID. The eight
// bytes MUST equal the GrandmasterIdentity in Announce. On the
// simulated GM bench this is the board's own clock identity; in
// RECEIVER mode it is the measured selected external source.
// Keep legacy 9-2LE unchanged unless a bench GM is selected.
if (!diagnostic_mirror) {
ar_ptp_lab_config_t ptp_config{};
ar_ptp_timing_authority_t authority{};
if (ar_ptp_lab_get_config(&ptp_config) &&
ar_ptp_lab_get_timing_authority(&authority) &&
authority.clock_usable) {
if (ptp_config.role == AR_PTP_ROLE_LAB_SOURCE &&
ptp_config.bench_global_gm) {
if (ptp_config.clock_identity_override) {
std::array<std::uint8_t, 8> identity{};
std::copy_n(ptp_config.clock_identity, 8U, identity.begin());
asdu.gm_identity = identity;
} else {
asdu.gm_identity =
ar::iec61850::time_sync::ptp_clock_identity_from_mac(source_mac);
}
} else if (ptp_config.role == AR_PTP_ROLE_TIME_RECEIVER &&
authority.source_selected) {
std::array<std::uint8_t, 8> identity{};
std::copy_n(authority.selected_source_clock_identity, 8U, identity.begin());
asdu.gm_identity = identity;
}
}
}
#endif
// The deployed device capability rejects SmvOpts synchSourceId=true.
// Never append optional [9] gmIdentity when SCL declares it absent.
// Future support must be explicitly compiled/validated from SCL and
// backed by an independently identified reference clock source.

SampledValuesFrame frame;
frame.destination = MacAddress{std::span<const std::uint8_t>{profile.destination_mac}};
Expand Down
9 changes: 3 additions & 6 deletions embedded/esp32p4_smv_injector/main/smp_synch_lab.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,9 @@ void record_transition(const SvSyncLossReason reason,
// originates exclusively from the sample phase + canonical TX gate.
// Global sync is reserved for measured external time traceability.
if (g_source_local_qualified.load(std::memory_order_acquire)) {
#if defined(ESP_PLATFORM) && CONFIG_AR_PTP_LAB_TX
// The same measured phase/MAC gate qualifies both local and bench
// global simulation. A UI flag alone can never promote AUTO to 2.
if (ar_ptp_lab_bench_global_ready())
return SmpSynchValue::global_synchronized;
#endif
// An internally generated bench GM is not independently traceable
// to global time. A measured local sample/MAC phase can justify
// smpSynch=1, never smpSynch=2. Keep AUTO fail-closed globally.
return SmpSynchValue::local_synchronized;
}
return ar::iec61850::time_sync::qualified_sv_sync_value(
Expand Down
Loading
Loading