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
8 changes: 8 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,14 @@ if(ARIEC61850_BUILD_TESTS)
add_test(NAME ariec61850_m4_three_sv_grid_tests
COMMAND ariec61850_m4_three_sv_grid_tests)

add_executable(ariec61850_m4_three_sv_signals_tests tests/test_m4_three_sv_signals.cpp)
target_link_libraries(ariec61850_m4_three_sv_signals_tests PRIVATE ARIEC61850::core)
target_compile_features(ariec61850_m4_three_sv_signals_tests PRIVATE cxx_std_20)
ariec61850_apply_warnings(ariec61850_m4_three_sv_signals_tests)
ariec61850_apply_sanitizers(ariec61850_m4_three_sv_signals_tests)
add_test(NAME ariec61850_m4_three_sv_signals_tests
COMMAND ariec61850_m4_three_sv_signals_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_signals.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/live_signal_state.hpp"
#include "ariec61850/sampled_values/m4_three_sv_roster.hpp"

#include <array>
#include <cstddef>
#include <cstdint>

namespace ar::iec61850::sampled_values {

// Three separate 4I+4V input banks. Each bank uses the project's existing
// four-buffer reader-pinning protocol, so the 4kHz publisher sees ONE
// immutable generation while the single host/control-plane writer commits.
// No heap allocation, shared mutable waveform values or per-MU RTOS worker.
//
// Slot 0/1/2 maps to IED/MU1/MU2/MU3 in the compiled M4 SCD roster.
// This class owns SIGNALS only; clock/PTP generation is shared separately.
class M4ThreeSvSignalBanks final {
public:
static constexpr std::size_t capacity = m4_three_sv_capacity;

[[nodiscard]] bool publish(
const std::size_t slot,
const SvLiveSignalState& complete_eight_channel_state) noexcept {
return slot < banks_.size() &&
banks_[slot].publish(complete_eight_channel_state);
}

[[nodiscard]] bool snapshot(
const std::size_t slot,
SvLiveSignalState& output) const noexcept {
if (slot >= banks_.size()) return false;
output = banks_[slot].snapshot();
return true;
}

[[nodiscard]] std::uint64_t generation(
const std::size_t slot) const noexcept {
if (slot >= banks_.size()) return 0U;
return banks_[slot].snapshot().generation;
}

private:
std::array<SvLiveSignalBank, capacity> banks_{};
};

static_assert(M4ThreeSvSignalBanks::capacity == 3U);

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

#include <array>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <string>

namespace {
#define CHECK(condition) do { if (!(condition)) { \
throw std::runtime_error(std::string{"M4 MU signals failed: "} + #condition + \
" at line " + std::to_string(__LINE__)); \
} } while (false)

using namespace ar::iec61850::sampled_values;

void verify_three_atomic_isolated_setpoint_banks() {
M4ThreeSvSignalBanks banks;
std::array<SvLiveSignalState, 3> initial{};
for (std::size_t slot = 0U; slot < 3U; ++slot) {
CHECK(banks.snapshot(slot, initial[slot]));
CHECK(initial[slot].generation == 0U);
CHECK(initial[slot].channels[0].rms_counts == 1000);
}

// The selected GUI page index is NOT the state owner: all three MU
// states remain independently retained through arbitrary page switches.
auto mu1 = initial[0];
mu1.frequency_millihz = 0U; // DC makes wire value obvious
mu1.channels[0].rms_counts = 1234;
CHECK(banks.publish(0U, mu1));

auto mu2 = initial[1];
mu2.frequency_millihz = 0U;
mu2.channels[0].rms_counts = 5678;
mu2.channels[4].rms_counts = 10000;
mu2.channels[4].quality = 0x11000000U;
CHECK(banks.publish(1U, mu2));

auto mu3 = initial[2];
mu3.frequency_millihz = 0U;
mu3.channels[0].rms_counts = -321;
mu3.channels[4].rms_counts = 15555;
CHECK(banks.publish(2U, mu3));

std::array<SvFixedPointSineEngine, 3> waveform_engines{};
const std::array<std::int32_t, 3> currents{1234, 5678, -321};
for (std::size_t page : {2U, 0U, 1U, 0U, 2U, 1U}) {
SvLiveSignalState state{};
CHECK(banks.snapshot(page, state));
CHECK(state.generation == 1U);
const auto output = waveform_engines[page].next(state, 4000U);
CHECK(output[0] == currents[page]);
CHECK(output[4] == state.channels[4].rms_counts);
}

SvLiveSignalState original_mu1{}, original_mu3{};
CHECK(banks.snapshot(0U, original_mu1));
CHECK(banks.snapshot(2U, original_mu3));

SvLiveSignalState edited_mu2{};
CHECK(banks.snapshot(1U, edited_mu2));
edited_mu2.channels[1].phase_millidegrees = 45000;
edited_mu2.channels[1].quality = 0x01000000U;
CHECK(banks.publish(1U, edited_mu2));
CHECK(banks.generation(1U) == 2U);
CHECK(banks.generation(0U) == 1U);
CHECK(banks.generation(2U) == 1U);

SvLiveSignalState after_mu1{}, after_mu3{};
CHECK(banks.snapshot(0U, after_mu1));
CHECK(banks.snapshot(2U, after_mu3));
CHECK(after_mu1 == original_mu1);
CHECK(after_mu3 == original_mu3);

// Rejected partial/invalid bank must never alter any published slot.
auto invalid = edited_mu2;
invalid.channels[0].phase_millidegrees = 360000001;
CHECK(!banks.publish(1U, invalid));
CHECK(banks.generation(1U) == 2U);
CHECK(!banks.publish(3U, edited_mu2));
SvLiveSignalState ignored{};
CHECK(!banks.snapshot(3U, ignored));
CHECK(banks.generation(3U) == 0U);

std::cout << "PASS M4 P0D1: three independent 4I4V setpoints, "
"quality, phase, generation and GUI page switching\n";
}
} // namespace

int main() {
try {
verify_three_atomic_isolated_setpoint_banks();
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
Loading