Skip to content

Commit e56560b

Browse files
authored
feat(m4-p0a): compile three SCD SV profiles as bounded shared-grid roster
* feat(m4): introduce bounded three-SV SCL roster and explicit shared-grid contract * feat(m4): compile three SCD profiles independently and preserve repeated APPID * fix(m4): match exact sample/frame rate optional uint32 types * test(m4): 3-MU 9-2LE SCD regression fixture with explicit SmvOpts * test(m4): three SCD MU profiles, V1 binary roundtrip, repeated APPID, full-roster rollback * build(m4): compile three-SV roster into core and register CTest regression
1 parent c07d20c commit e56560b

5 files changed

Lines changed: 412 additions & 0 deletions

File tree

‎CMakeLists.txt‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ add_library(ariec61850_core
5050
src/sampled_values/pdu_codec.cpp
5151
src/sampled_values/frame_codec.cpp
5252
src/sampled_values/compiled_device_profile.cpp
53+
src/sampled_values/m4_three_sv_roster.cpp
5354
src/sampled_values/quality.cpp
5455
src/sampled_values/payload_inspector.cpp
5556
src/sampled_values/sample_counter.cpp
@@ -313,6 +314,16 @@ if(ARIEC61850_BUILD_TESTS)
313314
ariec61850_apply_sanitizers(ariec61850_scl_tests)
314315
add_test(NAME ariec61850_scl_tests COMMAND ariec61850_scl_tests)
315316

317+
add_executable(ariec61850_m4_three_sv_roster_tests tests/test_m4_three_sv_roster.cpp)
318+
target_link_libraries(ariec61850_m4_three_sv_roster_tests PRIVATE ARIEC61850::core)
319+
target_compile_definitions(ariec61850_m4_three_sv_roster_tests PRIVATE
320+
ARIEC61850_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
321+
target_compile_features(ariec61850_m4_three_sv_roster_tests PRIVATE cxx_std_20)
322+
ariec61850_apply_warnings(ariec61850_m4_three_sv_roster_tests)
323+
ariec61850_apply_sanitizers(ariec61850_m4_three_sv_roster_tests)
324+
add_test(NAME ariec61850_m4_three_sv_roster_tests
325+
COMMAND ariec61850_m4_three_sv_roster_tests)
326+
316327
add_executable(ariec61850_simulation_tests tests/test_simulation.cpp)
317328
target_link_libraries(ariec61850_simulation_tests PRIVATE ARIEC61850::core)
318329
target_compile_definitions(ariec61850_simulation_tests PRIVATE
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
#pragma once
3+
4+
#include "ariec61850/sampled_values/compiled_device_profile.hpp"
5+
#include "ariec61850/scl/model.hpp"
6+
7+
#include <array>
8+
#include <cstddef>
9+
#include <cstdint>
10+
#include <optional>
11+
#include <string>
12+
#include <vector>
13+
14+
namespace ar::iec61850::sampled_values {
15+
16+
// M4.0 intentionally fixes the number of source-backed virtual MUs at three.
17+
// A firmware capability change, rather than an unbounded vector, raises this limit.
18+
inline constexpr std::size_t m4_three_sv_capacity = 3U;
19+
inline constexpr std::uint32_t m4_three_sv_rate_hz = 4000U;
20+
21+
struct M4CompiledSvStream final {
22+
std::string ied_name;
23+
std::string control_block_reference;
24+
CompiledSvDeviceProfile device_profile;
25+
};
26+
27+
struct M4ThreeSvRoster final {
28+
std::array<M4CompiledSvStream, m4_three_sv_capacity> streams{};
29+
std::uint32_t common_sample_rate_hz{m4_three_sv_rate_hz};
30+
std::uint32_t total_ethernet_frames_per_second{
31+
static_cast<std::uint32_t>(m4_three_sv_capacity) * m4_three_sv_rate_hz};
32+
};
33+
34+
struct M4ThreeSvCompileResult final {
35+
std::optional<M4ThreeSvRoster> roster;
36+
std::vector<std::string> errors;
37+
[[nodiscard]] bool ok() const noexcept { return roster.has_value() && errors.empty(); }
38+
};
39+
40+
// Pure host-side compiler. No hardware/clock/USB dependency. Never parses XML
41+
// twice: the existing SclParser creates SclDocument and the existing one-stream
42+
// compiler/codec remains the only authority for each stream's wire fields.
43+
// The caller EXPLICITLY selects the 9-2LE family and counter policy.
44+
class M4ThreeSvRosterCompiler final {
45+
public:
46+
[[nodiscard]] static M4ThreeSvCompileResult compile(
47+
const scl::SclDocument& document,
48+
const SvPublisherProfileCompileContext& context);
49+
};
50+
51+
} // namespace ar::iec61850::sampled_values
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
#include "ariec61850/sampled_values/m4_three_sv_roster.hpp"
3+
#include "ariec61850/sampled_values/esp32p4_profile_support.hpp"
4+
5+
#include <cstddef>
6+
#include <cstdint>
7+
#include <string>
8+
#include <string_view>
9+
#include <utility>
10+
11+
namespace ar::iec61850::sampled_values {
12+
namespace {
13+
14+
[[nodiscard]] bool same_wire_identity(
15+
const CompiledSvDeviceProfile& lhs,
16+
const CompiledSvDeviceProfile& rhs) noexcept {
17+
const auto id = [](const CompiledSvDeviceProfile& p) {
18+
return std::string_view{p.sv_id.data(), p.sv_id_length};
19+
};
20+
return lhs.app_id == rhs.app_id &&
21+
lhs.destination_mac == rhs.destination_mac &&
22+
lhs.vlan_present == rhs.vlan_present &&
23+
(!lhs.vlan_present ||
24+
(lhs.vlan_id == rhs.vlan_id &&
25+
lhs.vlan_priority == rhs.vlan_priority)) &&
26+
lhs.transport_mode == rhs.transport_mode &&
27+
id(lhs) == id(rhs);
28+
}
29+
30+
} // namespace
31+
32+
M4ThreeSvCompileResult M4ThreeSvRosterCompiler::compile(
33+
const scl::SclDocument& document,
34+
const SvPublisherProfileCompileContext& context) {
35+
M4ThreeSvCompileResult result;
36+
if (document.sampled_values_streams.size() != m4_three_sv_capacity) {
37+
result.errors.push_back(
38+
"M4.0 requires exactly three SCL SampledValueControl streams; found " +
39+
std::to_string(document.sampled_values_streams.size()));
40+
return result;
41+
}
42+
43+
// The family is deliberate engineering intent, never inferred from a
44+
// filename, APPID, DataSet, or vendor metadata.
45+
if (context.profile_family != SvProfileFamily::legacy_9_2le &&
46+
context.profile_family != SvProfileFamily::iec61850_9_2) {
47+
result.errors.push_back(
48+
"Select an explicit supported IEC 61850-9-2/9-2LE profile family.");
49+
return result;
50+
}
51+
if (context.sample_counter_modulus !=
52+
std::optional<std::uint16_t>{static_cast<std::uint16_t>(4000U)}) {
53+
result.errors.push_back(
54+
"M4.0 needs an explicitly validated 4000-sample counter modulus "
55+
"for one shared PTP-aligned second.");
56+
return result;
57+
}
58+
59+
M4ThreeSvRoster candidate;
60+
for (std::size_t i = 0U; i < m4_three_sv_capacity; ++i) {
61+
const auto& stream = document.sampled_values_streams[i];
62+
const std::string name = stream.ied_name + " / " + stream.control_name;
63+
const auto compiled = SvPublisherProfileCompiler::compile(stream, context);
64+
for (const auto& error : compiled.errors) {
65+
result.errors.push_back(name + ": " + error);
66+
}
67+
if (!compiled.ok()) continue;
68+
69+
const SvPublisherProfile& p = *compiled.profile;
70+
if (p.timing.exact_sample_rate_hz() !=
71+
std::optional<std::uint32_t>{m4_three_sv_rate_hz} ||
72+
p.timing.exact_frame_rate_hz() !=
73+
std::optional<std::uint32_t>{m4_three_sv_rate_hz} ||
74+
p.timing.asdus_per_frame != 1U ||
75+
p.sample_counter_modulus != context.sample_counter_modulus ||
76+
!p.asdu_options.sample_synchronized ||
77+
p.payload_size_bytes != 64U || p.channels.size() != 16U) {
78+
result.errors.push_back(
79+
name + ": M4.0 needs synchronized 4000 Hz 4I+4V, one "
80+
"ASDU/frame and 64-byte canonical payload.");
81+
continue;
82+
}
83+
if (classify_esp32p4_sv_profile(p) != Esp32P4SvProfileSupport::ready) {
84+
result.errors.push_back(
85+
name + ": host SCL profile is not supported by the "
86+
"ESP32-P4 4I+4V device boundary.");
87+
continue;
88+
}
89+
90+
const auto device = compile_esp32p4_device_profile(p);
91+
if (!device.ok()) {
92+
result.errors.push_back(
93+
name + ": failed to compile validated binary device profile.");
94+
for (const auto& error : device.errors) {
95+
result.errors.push_back(name + ": " + error);
96+
}
97+
continue;
98+
}
99+
100+
candidate.streams[i] = M4CompiledSvStream{
101+
stream.ied_name, stream.control_block_reference, *device.profile};
102+
103+
// The real SCD repeats APPID 0x4000 on three DIFFERENT MAC addresses;
104+
// APPID-only uniqueness would wrongly reject this valid lab scenario.
105+
for (std::size_t previous = 0U; previous < i; ++previous) {
106+
if (stream.ied_name == candidate.streams[previous].ied_name &&
107+
stream.control_block_reference ==
108+
candidate.streams[previous].control_block_reference) {
109+
result.errors.push_back(
110+
name + ": duplicate IED-qualified control block.");
111+
}
112+
if (same_wire_identity(candidate.streams[i].device_profile,
113+
candidate.streams[previous].device_profile)) {
114+
result.errors.push_back(
115+
name + ": duplicate complete SV wire identity.");
116+
}
117+
}
118+
}
119+
if (result.errors.empty()) result.roster = std::move(candidate);
120+
return result;
121+
}
122+
123+
} // namespace ar::iec61850::sampled_values
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<SCL xmlns="http://www.iec.ch/61850/2003/SCL">
3+
<Header id="AR_M4_3_MU_REFERENCE" version="1" revision="A"/>
4+
<Communication><SubNetwork name="ProcessBus" type="8-MMS">
5+
<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>
6+
<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>
7+
<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>
8+
</SubNetwork></Communication>
9+
<IED name="MU01_4I_4V_1" manufacturer="AR" type="MergingUnit" configVersion="G"><AccessPoint name="LD0"><Server><LDevice inst="MU01"><LN0 lnClass="LLN0" lnType="MULLN0">
10+
<DataSet name="PhsMeas1">
11+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="1" doName="AmpSv" daName="instMag.i" fc="MX"/>
12+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="1" doName="AmpSv" daName="q" fc="MX"/>
13+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="2" doName="AmpSv" daName="instMag.i" fc="MX"/>
14+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="2" doName="AmpSv" daName="q" fc="MX"/>
15+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="3" doName="AmpSv" daName="instMag.i" fc="MX"/>
16+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="3" doName="AmpSv" daName="q" fc="MX"/>
17+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="4" doName="AmpSv" daName="instMag.i" fc="MX"/>
18+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="4" doName="AmpSv" daName="q" fc="MX"/>
19+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="1" doName="VolSv" daName="instMag.i" fc="MX"/>
20+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="1" doName="VolSv" daName="q" fc="MX"/>
21+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="2" doName="VolSv" daName="instMag.i" fc="MX"/>
22+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="2" doName="VolSv" daName="q" fc="MX"/>
23+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="3" doName="VolSv" daName="instMag.i" fc="MX"/>
24+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="3" doName="VolSv" daName="q" fc="MX"/>
25+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="4" doName="VolSv" daName="instMag.i" fc="MX"/>
26+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="4" doName="VolSv" daName="q" fc="MX"/>
27+
</DataSet>
28+
<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>
29+
</LN0>
30+
<LN lnType="CTR" lnClass="TCTR" inst="1"/>
31+
<LN lnType="CTR" lnClass="TCTR" inst="2"/>
32+
<LN lnType="CTR" lnClass="TCTR" inst="3"/>
33+
<LN lnType="CTR" lnClass="TCTR" inst="4"/>
34+
<LN lnType="VTR" lnClass="TVTR" inst="1"/>
35+
<LN lnType="VTR" lnClass="TVTR" inst="2"/>
36+
<LN lnType="VTR" lnClass="TVTR" inst="3"/>
37+
<LN lnType="VTR" lnClass="TVTR" inst="4"/>
38+
</LDevice></Server></AccessPoint></IED>
39+
<IED name="MU01_4I_4V_2" manufacturer="AR" type="MergingUnit" configVersion="G"><AccessPoint name="LD0"><Server><LDevice inst="MU01"><LN0 lnClass="LLN0" lnType="MULLN0">
40+
<DataSet name="PhsMeas1">
41+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="1" doName="AmpSv" daName="instMag.i" fc="MX"/>
42+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="1" doName="AmpSv" daName="q" fc="MX"/>
43+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="2" doName="AmpSv" daName="instMag.i" fc="MX"/>
44+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="2" doName="AmpSv" daName="q" fc="MX"/>
45+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="3" doName="AmpSv" daName="instMag.i" fc="MX"/>
46+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="3" doName="AmpSv" daName="q" fc="MX"/>
47+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="4" doName="AmpSv" daName="instMag.i" fc="MX"/>
48+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="4" doName="AmpSv" daName="q" fc="MX"/>
49+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="1" doName="VolSv" daName="instMag.i" fc="MX"/>
50+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="1" doName="VolSv" daName="q" fc="MX"/>
51+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="2" doName="VolSv" daName="instMag.i" fc="MX"/>
52+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="2" doName="VolSv" daName="q" fc="MX"/>
53+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="3" doName="VolSv" daName="instMag.i" fc="MX"/>
54+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="3" doName="VolSv" daName="q" fc="MX"/>
55+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="4" doName="VolSv" daName="instMag.i" fc="MX"/>
56+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="4" doName="VolSv" daName="q" fc="MX"/>
57+
</DataSet>
58+
<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>
59+
</LN0>
60+
<LN lnType="CTR" lnClass="TCTR" inst="1"/>
61+
<LN lnType="CTR" lnClass="TCTR" inst="2"/>
62+
<LN lnType="CTR" lnClass="TCTR" inst="3"/>
63+
<LN lnType="CTR" lnClass="TCTR" inst="4"/>
64+
<LN lnType="VTR" lnClass="TVTR" inst="1"/>
65+
<LN lnType="VTR" lnClass="TVTR" inst="2"/>
66+
<LN lnType="VTR" lnClass="TVTR" inst="3"/>
67+
<LN lnType="VTR" lnClass="TVTR" inst="4"/>
68+
</LDevice></Server></AccessPoint></IED>
69+
<IED name="MU01_4I_4V_3" manufacturer="AR" type="MergingUnit" configVersion="G"><AccessPoint name="LD0"><Server><LDevice inst="MU01"><LN0 lnClass="LLN0" lnType="MULLN0">
70+
<DataSet name="PhsMeas1">
71+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="1" doName="AmpSv" daName="instMag.i" fc="MX"/>
72+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="1" doName="AmpSv" daName="q" fc="MX"/>
73+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="2" doName="AmpSv" daName="instMag.i" fc="MX"/>
74+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="2" doName="AmpSv" daName="q" fc="MX"/>
75+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="3" doName="AmpSv" daName="instMag.i" fc="MX"/>
76+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="3" doName="AmpSv" daName="q" fc="MX"/>
77+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="4" doName="AmpSv" daName="instMag.i" fc="MX"/>
78+
<FCDA ldInst="MU01" lnClass="TCTR" lnInst="4" doName="AmpSv" daName="q" fc="MX"/>
79+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="1" doName="VolSv" daName="instMag.i" fc="MX"/>
80+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="1" doName="VolSv" daName="q" fc="MX"/>
81+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="2" doName="VolSv" daName="instMag.i" fc="MX"/>
82+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="2" doName="VolSv" daName="q" fc="MX"/>
83+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="3" doName="VolSv" daName="instMag.i" fc="MX"/>
84+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="3" doName="VolSv" daName="q" fc="MX"/>
85+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="4" doName="VolSv" daName="instMag.i" fc="MX"/>
86+
<FCDA ldInst="MU01" lnClass="TVTR" lnInst="4" doName="VolSv" daName="q" fc="MX"/>
87+
</DataSet>
88+
<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>
89+
</LN0>
90+
<LN lnType="CTR" lnClass="TCTR" inst="1"/>
91+
<LN lnType="CTR" lnClass="TCTR" inst="2"/>
92+
<LN lnType="CTR" lnClass="TCTR" inst="3"/>
93+
<LN lnType="CTR" lnClass="TCTR" inst="4"/>
94+
<LN lnType="VTR" lnClass="TVTR" inst="1"/>
95+
<LN lnType="VTR" lnClass="TVTR" inst="2"/>
96+
<LN lnType="VTR" lnClass="TVTR" inst="3"/>
97+
<LN lnType="VTR" lnClass="TVTR" inst="4"/>
98+
</LDevice></Server></AccessPoint></IED>
99+
<DataTypeTemplates>
100+
<LNodeType id="MULLN0" lnClass="LLN0"/>
101+
<LNodeType id="CTR" lnClass="TCTR"><DO name="AmpSv" type="AmpSAV"/></LNodeType>
102+
<LNodeType id="VTR" lnClass="TVTR"><DO name="VolSv" type="VolSAV"/></LNodeType>
103+
<DOType id="AmpSAV" cdc="SAV"><DA name="instMag" bType="Struct" type="AnalogueValue" fc="MX"/><DA name="q" bType="Quality" fc="MX"/></DOType>
104+
<DOType id="VolSAV" cdc="SAV"><DA name="instMag" bType="Struct" type="AnalogueValue" fc="MX"/><DA name="q" bType="Quality" fc="MX"/></DOType>
105+
<DAType id="AnalogueValue"><BDA name="i" bType="INT32"/></DAType>
106+
</DataTypeTemplates></SCL>

0 commit comments

Comments
 (0)