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
11 changes: 11 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ add_library(ariec61850_core
src/sampled_values/pdu_codec.cpp
src/sampled_values/frame_codec.cpp
src/sampled_values/compiled_device_profile.cpp
src/sampled_values/m4_three_sv_roster.cpp
src/sampled_values/quality.cpp
src/sampled_values/payload_inspector.cpp
src/sampled_values/sample_counter.cpp
Expand Down Expand Up @@ -313,6 +314,16 @@ if(ARIEC61850_BUILD_TESTS)
ariec61850_apply_sanitizers(ariec61850_scl_tests)
add_test(NAME ariec61850_scl_tests COMMAND ariec61850_scl_tests)

add_executable(ariec61850_m4_three_sv_roster_tests tests/test_m4_three_sv_roster.cpp)
target_link_libraries(ariec61850_m4_three_sv_roster_tests PRIVATE ARIEC61850::core)
target_compile_definitions(ariec61850_m4_three_sv_roster_tests PRIVATE
ARIEC61850_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
target_compile_features(ariec61850_m4_three_sv_roster_tests PRIVATE cxx_std_20)
ariec61850_apply_warnings(ariec61850_m4_three_sv_roster_tests)
ariec61850_apply_sanitizers(ariec61850_m4_three_sv_roster_tests)
add_test(NAME ariec61850_m4_three_sv_roster_tests
COMMAND ariec61850_m4_three_sv_roster_tests)

add_executable(ariec61850_simulation_tests tests/test_simulation.cpp)
target_link_libraries(ariec61850_simulation_tests PRIVATE ARIEC61850::core)
target_compile_definitions(ariec61850_simulation_tests PRIVATE
Expand Down
51 changes: 51 additions & 0 deletions include/ariec61850/sampled_values/m4_three_sv_roster.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

#include "ariec61850/sampled_values/compiled_device_profile.hpp"
#include "ariec61850/scl/model.hpp"

#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>

namespace ar::iec61850::sampled_values {

// M4.0 intentionally fixes the number of source-backed virtual MUs at three.
// A firmware capability change, rather than an unbounded vector, raises this limit.
inline constexpr std::size_t m4_three_sv_capacity = 3U;
inline constexpr std::uint32_t m4_three_sv_rate_hz = 4000U;

struct M4CompiledSvStream final {
std::string ied_name;
std::string control_block_reference;
CompiledSvDeviceProfile device_profile;
};

struct M4ThreeSvRoster final {
std::array<M4CompiledSvStream, m4_three_sv_capacity> streams{};
std::uint32_t common_sample_rate_hz{m4_three_sv_rate_hz};
std::uint32_t total_ethernet_frames_per_second{
static_cast<std::uint32_t>(m4_three_sv_capacity) * m4_three_sv_rate_hz};
};

struct M4ThreeSvCompileResult final {
std::optional<M4ThreeSvRoster> roster;
std::vector<std::string> errors;
[[nodiscard]] bool ok() const noexcept { return roster.has_value() && errors.empty(); }
};

// Pure host-side compiler. No hardware/clock/USB dependency. Never parses XML
// twice: the existing SclParser creates SclDocument and the existing one-stream
// compiler/codec remains the only authority for each stream's wire fields.
// The caller EXPLICITLY selects the 9-2LE family and counter policy.
class M4ThreeSvRosterCompiler final {
public:
[[nodiscard]] static M4ThreeSvCompileResult compile(
const scl::SclDocument& document,
const SvPublisherProfileCompileContext& context);
};

} // namespace ar::iec61850::sampled_values
123 changes: 123 additions & 0 deletions src/sampled_values/m4_three_sv_roster.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ariec61850/sampled_values/m4_three_sv_roster.hpp"
#include "ariec61850/sampled_values/esp32p4_profile_support.hpp"

#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
#include <utility>

namespace ar::iec61850::sampled_values {
namespace {

[[nodiscard]] bool same_wire_identity(
const CompiledSvDeviceProfile& lhs,
const CompiledSvDeviceProfile& rhs) noexcept {
const auto id = [](const CompiledSvDeviceProfile& p) {
return std::string_view{p.sv_id.data(), p.sv_id_length};
};
return lhs.app_id == rhs.app_id &&
lhs.destination_mac == rhs.destination_mac &&
lhs.vlan_present == rhs.vlan_present &&
(!lhs.vlan_present ||
(lhs.vlan_id == rhs.vlan_id &&
lhs.vlan_priority == rhs.vlan_priority)) &&
lhs.transport_mode == rhs.transport_mode &&
id(lhs) == id(rhs);
}

} // namespace

M4ThreeSvCompileResult M4ThreeSvRosterCompiler::compile(
const scl::SclDocument& document,
const SvPublisherProfileCompileContext& context) {
M4ThreeSvCompileResult result;
if (document.sampled_values_streams.size() != m4_three_sv_capacity) {
result.errors.push_back(
"M4.0 requires exactly three SCL SampledValueControl streams; found " +
std::to_string(document.sampled_values_streams.size()));
return result;
}

// The family is deliberate engineering intent, never inferred from a
// filename, APPID, DataSet, or vendor metadata.
if (context.profile_family != SvProfileFamily::legacy_9_2le &&
context.profile_family != SvProfileFamily::iec61850_9_2) {
result.errors.push_back(
"Select an explicit supported IEC 61850-9-2/9-2LE profile family.");
return result;
}
if (context.sample_counter_modulus !=
std::optional<std::uint16_t>{static_cast<std::uint16_t>(4000U)}) {
result.errors.push_back(
"M4.0 needs an explicitly validated 4000-sample counter modulus "
"for one shared PTP-aligned second.");
return result;
}

M4ThreeSvRoster candidate;
for (std::size_t i = 0U; i < m4_three_sv_capacity; ++i) {
const auto& stream = document.sampled_values_streams[i];
const std::string name = stream.ied_name + " / " + stream.control_name;
const auto compiled = SvPublisherProfileCompiler::compile(stream, context);
for (const auto& error : compiled.errors) {
result.errors.push_back(name + ": " + error);
}
if (!compiled.ok()) continue;

const SvPublisherProfile& p = *compiled.profile;
if (p.timing.exact_sample_rate_hz() !=
std::optional<std::uint32_t>{m4_three_sv_rate_hz} ||
p.timing.exact_frame_rate_hz() !=
std::optional<std::uint32_t>{m4_three_sv_rate_hz} ||
p.timing.asdus_per_frame != 1U ||
p.sample_counter_modulus != context.sample_counter_modulus ||
!p.asdu_options.sample_synchronized ||
p.payload_size_bytes != 64U || p.channels.size() != 16U) {
result.errors.push_back(
name + ": M4.0 needs synchronized 4000 Hz 4I+4V, one "
"ASDU/frame and 64-byte canonical payload.");
continue;
}
if (classify_esp32p4_sv_profile(p) != Esp32P4SvProfileSupport::ready) {
result.errors.push_back(
name + ": host SCL profile is not supported by the "
"ESP32-P4 4I+4V device boundary.");
continue;
}

const auto device = compile_esp32p4_device_profile(p);
if (!device.ok()) {
result.errors.push_back(
name + ": failed to compile validated binary device profile.");
for (const auto& error : device.errors) {
result.errors.push_back(name + ": " + error);
}
continue;
}

candidate.streams[i] = M4CompiledSvStream{
stream.ied_name, stream.control_block_reference, *device.profile};

// The real SCD repeats APPID 0x4000 on three DIFFERENT MAC addresses;
// APPID-only uniqueness would wrongly reject this valid lab scenario.
for (std::size_t previous = 0U; previous < i; ++previous) {
if (stream.ied_name == candidate.streams[previous].ied_name &&
stream.control_block_reference ==
candidate.streams[previous].control_block_reference) {
result.errors.push_back(
name + ": duplicate IED-qualified control block.");
}
if (same_wire_identity(candidate.streams[i].device_profile,
candidate.streams[previous].device_profile)) {
result.errors.push_back(
name + ": duplicate complete SV wire identity.");
}
}
}
if (result.errors.empty()) result.roster = std::move(candidate);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject duplicate IED conflicts before accepting the roster

When the SCD contains duplicate IED@name declarations with different control-block names, this compiler returns a successful roster despite the parser reporting an IED conflict. Reproduced by renaming the fixture's second IED to MU01_4I_4V_1 and its control/binding to MSVCB02: document.conflicts reports the duplicate IED, but compile().ok() is true. The pairwise control-reference check does not catch this, allowing profiles compiled from ambiguous IED/DataSet namespaces through the validation boundary. Preserve the deliberate repeated-APPID exception, but reject duplicate-IED conflicts before publishing the roster.

AGENTS.md reference: AGENTS.md:L152-L152

Useful? React with 👍 / 👎.

return result;
}

} // namespace ar::iec61850::sampled_values
106 changes: 106 additions & 0 deletions tests/fixtures/scl/m4-three-9-2le-reference.scd
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<SCL xmlns="http://www.iec.ch/61850/2003/SCL">
<Header id="AR_M4_3_MU_REFERENCE" version="1" revision="A"/>
<Communication><SubNetwork name="ProcessBus" type="8-MMS">
<ConnectedAP iedName="MU01_4I_4V_1" apName="LD0"><SMV ldInst="MU01" cbName="MSVCB01"><Address><P type="VLAN-ID">0</P><P type="VLAN-PRIORITY">4</P><P type="MAC-Address">01-0C-CD-04-00-01</P><P type="APPID">4000</P></Address></SMV></ConnectedAP>
<ConnectedAP iedName="MU01_4I_4V_2" apName="LD0"><SMV ldInst="MU01" cbName="MSVCB01"><Address><P type="VLAN-ID">0</P><P type="VLAN-PRIORITY">4</P><P type="MAC-Address">01-0C-CD-04-00-02</P><P type="APPID">4000</P></Address></SMV></ConnectedAP>
<ConnectedAP iedName="MU01_4I_4V_3" apName="LD0"><SMV ldInst="MU01" cbName="MSVCB01"><Address><P type="VLAN-ID">0</P><P type="VLAN-PRIORITY">4</P><P type="MAC-Address">01-0C-CD-04-00-03</P><P type="APPID">4000</P></Address></SMV></ConnectedAP>
</SubNetwork></Communication>
<IED name="MU01_4I_4V_1" manufacturer="AR" type="MergingUnit" configVersion="G"><AccessPoint name="LD0"><Server><LDevice inst="MU01"><LN0 lnClass="LLN0" lnType="MULLN0">
<DataSet name="PhsMeas1">
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="1" doName="AmpSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="1" doName="AmpSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="2" doName="AmpSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="2" doName="AmpSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="3" doName="AmpSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="3" doName="AmpSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="4" doName="AmpSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="4" doName="AmpSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="1" doName="VolSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="1" doName="VolSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="2" doName="VolSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="2" doName="VolSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="3" doName="VolSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="3" doName="VolSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="4" doName="VolSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="4" doName="VolSv" daName="q" fc="MX"/>
</DataSet>
<SampledValueControl multicast="true" name="MSVCB01" datSet="PhsMeas1" confRev="1" smvID="MU01_SV1" smpRate="4000" smpMod="SmpPerSec" nofASDU="1"><SmvOpts refreshTime="false" sampleSynchronized="true" sampleRate="false" dataSet="false" security="false" synchSourceId="false"/></SampledValueControl>
</LN0>
<LN lnType="CTR" lnClass="TCTR" inst="1"/>
<LN lnType="CTR" lnClass="TCTR" inst="2"/>
<LN lnType="CTR" lnClass="TCTR" inst="3"/>
<LN lnType="CTR" lnClass="TCTR" inst="4"/>
<LN lnType="VTR" lnClass="TVTR" inst="1"/>
<LN lnType="VTR" lnClass="TVTR" inst="2"/>
<LN lnType="VTR" lnClass="TVTR" inst="3"/>
<LN lnType="VTR" lnClass="TVTR" inst="4"/>
</LDevice></Server></AccessPoint></IED>
<IED name="MU01_4I_4V_2" manufacturer="AR" type="MergingUnit" configVersion="G"><AccessPoint name="LD0"><Server><LDevice inst="MU01"><LN0 lnClass="LLN0" lnType="MULLN0">
<DataSet name="PhsMeas1">
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="1" doName="AmpSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="1" doName="AmpSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="2" doName="AmpSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="2" doName="AmpSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="3" doName="AmpSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="3" doName="AmpSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="4" doName="AmpSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="4" doName="AmpSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="1" doName="VolSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="1" doName="VolSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="2" doName="VolSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="2" doName="VolSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="3" doName="VolSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="3" doName="VolSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="4" doName="VolSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="4" doName="VolSv" daName="q" fc="MX"/>
</DataSet>
<SampledValueControl multicast="true" name="MSVCB01" datSet="PhsMeas1" confRev="1" smvID="MU01_SV2" smpRate="4000" smpMod="SmpPerSec" nofASDU="1"><SmvOpts refreshTime="false" sampleSynchronized="true" sampleRate="false" dataSet="false" security="false" synchSourceId="false"/></SampledValueControl>
</LN0>
<LN lnType="CTR" lnClass="TCTR" inst="1"/>
<LN lnType="CTR" lnClass="TCTR" inst="2"/>
<LN lnType="CTR" lnClass="TCTR" inst="3"/>
<LN lnType="CTR" lnClass="TCTR" inst="4"/>
<LN lnType="VTR" lnClass="TVTR" inst="1"/>
<LN lnType="VTR" lnClass="TVTR" inst="2"/>
<LN lnType="VTR" lnClass="TVTR" inst="3"/>
<LN lnType="VTR" lnClass="TVTR" inst="4"/>
</LDevice></Server></AccessPoint></IED>
<IED name="MU01_4I_4V_3" manufacturer="AR" type="MergingUnit" configVersion="G"><AccessPoint name="LD0"><Server><LDevice inst="MU01"><LN0 lnClass="LLN0" lnType="MULLN0">
<DataSet name="PhsMeas1">
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="1" doName="AmpSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="1" doName="AmpSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="2" doName="AmpSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="2" doName="AmpSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="3" doName="AmpSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="3" doName="AmpSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="4" doName="AmpSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="4" doName="AmpSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="1" doName="VolSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="1" doName="VolSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="2" doName="VolSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="2" doName="VolSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="3" doName="VolSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="3" doName="VolSv" daName="q" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="4" doName="VolSv" daName="instMag.i" fc="MX"/>
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="4" doName="VolSv" daName="q" fc="MX"/>
</DataSet>
<SampledValueControl multicast="true" name="MSVCB01" datSet="PhsMeas1" confRev="1" smvID="MU01_SV3" smpRate="4000" smpMod="SmpPerSec" nofASDU="1"><SmvOpts refreshTime="false" sampleSynchronized="true" sampleRate="false" dataSet="false" security="false" synchSourceId="false"/></SampledValueControl>
</LN0>
<LN lnType="CTR" lnClass="TCTR" inst="1"/>
<LN lnType="CTR" lnClass="TCTR" inst="2"/>
<LN lnType="CTR" lnClass="TCTR" inst="3"/>
<LN lnType="CTR" lnClass="TCTR" inst="4"/>
<LN lnType="VTR" lnClass="TVTR" inst="1"/>
<LN lnType="VTR" lnClass="TVTR" inst="2"/>
<LN lnType="VTR" lnClass="TVTR" inst="3"/>
<LN lnType="VTR" lnClass="TVTR" inst="4"/>
</LDevice></Server></AccessPoint></IED>
<DataTypeTemplates>
<LNodeType id="MULLN0" lnClass="LLN0"/>
<LNodeType id="CTR" lnClass="TCTR"><DO name="AmpSv" type="AmpSAV"/></LNodeType>
<LNodeType id="VTR" 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"/></DOType>
<DOType id="VolSAV" cdc="SAV"><DA name="instMag" bType="Struct" type="AnalogueValue" fc="MX"/><DA name="q" bType="Quality" fc="MX"/></DOType>
<DAType id="AnalogueValue"><BDA name="i" bType="INT32"/></DAType>
</DataTypeTemplates></SCL>
Loading
Loading