diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e564948b..7beeadc13 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/include/ariec61850/sampled_values/m4_three_sv_signals.hpp b/include/ariec61850/sampled_values/m4_three_sv_signals.hpp new file mode 100644 index 000000000..a8dbd016a --- /dev/null +++ b/include/ariec61850/sampled_values/m4_three_sv_signals.hpp @@ -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 +#include +#include + +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 banks_{}; +}; + +static_assert(M4ThreeSvSignalBanks::capacity == 3U); + +} // namespace ar::iec61850::sampled_values diff --git a/tests/test_m4_three_sv_signals.cpp b/tests/test_m4_three_sv_signals.cpp new file mode 100644 index 000000000..1621a5368 --- /dev/null +++ b/tests/test_m4_three_sv_signals.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "ariec61850/sampled_values/m4_three_sv_signals.hpp" + +#include +#include +#include +#include +#include +#include + +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 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 waveform_engines{}; + const std::array 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; + } +}