diff --git a/.github/workflows/brcb-hard-profile-ci.yml b/.github/workflows/brcb-hard-profile-ci.yml index 5d3ea5cfa..90f2f1103 100644 --- a/.github/workflows/brcb-hard-profile-ci.yml +++ b/.github/workflows/brcb-hard-profile-ci.yml @@ -50,6 +50,7 @@ jobs: set -euo pipefail for binary in \ build-brcb-hard/ariec61850_hard_mms_brcb_smoke \ + build-brcb-hard/ariec61850_hard_mms_brcb_control_smoke \ build-brcb-hard/ariec61850_hard_mms_brcb_connection_smoke \ build-brcb-hard/ariec61850_hard_mms_brcb_persistence_smoke; do if nm -C "$binary" | grep -E '__cxa_throw|__cxa_begin_catch|__cxa_end_catch|__gxx_personality_v0'; then @@ -67,6 +68,7 @@ jobs: echo "compiler=${{ matrix.compiler }}" size \ build-brcb-hard/ariec61850_hard_mms_brcb_smoke \ + build-brcb-hard/ariec61850_hard_mms_brcb_control_smoke \ build-brcb-hard/ariec61850_hard_mms_brcb_connection_smoke \ build-brcb-hard/ariec61850_hard_mms_brcb_persistence_smoke } | tee build-brcb-hard/brcb-footprint.txt diff --git a/embedded/brcb_hard_profile/CMakeLists.txt b/embedded/brcb_hard_profile/CMakeLists.txt index 79d1052b5..81d5d1b8e 100644 --- a/embedded/brcb_hard_profile/CMakeLists.txt +++ b/embedded/brcb_hard_profile/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.20) project(ARIEC61850BrcbHardProfile - VERSION 0.2.0 + VERSION 0.3.0 DESCRIPTION "No-exception bounded IEC 61850 buffered-report profile" LANGUAGES CXX) @@ -41,6 +41,19 @@ add_executable(ariec61850_hard_mms_brcb_smoke ${ARIEC61850_ROOT}/src/mms/static_brcb_runtime.cpp) ariec61850_configure_brcb_hard_target(ariec61850_hard_mms_brcb_smoke) +add_executable(ariec61850_hard_mms_brcb_control_smoke + ${CMAKE_CURRENT_LIST_DIR}/../mms_brcb_control_hard_profile_smoke.cpp + ${ARIEC61850_ROOT}/src/asn1/ber_span_reader.cpp + ${ARIEC61850_ROOT}/src/mms/pdu_span.cpp + ${ARIEC61850_ROOT}/src/mms/services_span.cpp + ${ARIEC61850_ROOT}/src/mms/information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_object_table.cpp + ${ARIEC61850_ROOT}/src/mms/static_data_set_table.cpp + ${ARIEC61850_ROOT}/src/mms/buffered_selective_information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_runtime.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_control.cpp) +ariec61850_configure_brcb_hard_target(ariec61850_hard_mms_brcb_control_smoke) + add_executable(ariec61850_hard_mms_brcb_connection_smoke ${CMAKE_CURRENT_LIST_DIR}/../mms_brcb_connection_hard_profile_smoke.cpp ${ARIEC61850_ROOT}/src/asn1/ber_span_reader.cpp @@ -80,6 +93,9 @@ ariec61850_configure_brcb_hard_target(ariec61850_hard_mms_brcb_persistence_smoke enable_testing() add_test(NAME ariec61850_hard_mms_brcb_smoke COMMAND ariec61850_hard_mms_brcb_smoke) +add_test( + NAME ariec61850_hard_mms_brcb_control_smoke + COMMAND ariec61850_hard_mms_brcb_control_smoke) add_test( NAME ariec61850_hard_mms_brcb_connection_smoke COMMAND ariec61850_hard_mms_brcb_connection_smoke) @@ -88,6 +104,7 @@ add_test( COMMAND ariec61850_hard_mms_brcb_persistence_smoke) message(STATUS "arstack61850 BRCB hard profile: bounded RAM queue + EntryID + BufOvfl=ON") +message(STATUS "arstack61850 BRCB hard profile: association ownership + ResvTms gate=ON") message(STATUS "arstack61850 BRCB hard profile: retry-safe reconnect delivery=ON") message(STATUS "arstack61850 BRCB hard profile: dual-bank checkpoint + reboot recovery oracle=ON") message(STATUS "arstack61850 BRCB hard profile: platform non-volatile adapter=NOT_CLAIMED") diff --git a/embedded/mms_brcb_control_hard_profile_smoke.cpp b/embedded/mms_brcb_control_hard_profile_smoke.cpp new file mode 100644 index 000000000..63c480c21 --- /dev/null +++ b/embedded/mms_brcb_control_hard_profile_smoke.cpp @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_brcb_control.hpp" + +#include +#include +#include +#include + +namespace { +using namespace ar::iec61850; + +constexpr std::array kBooleanType{0x83U, 0x00U}; + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = *static_cast(context) ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] mms::MmsStaticBrcbClientIdentity client( + const std::uint64_t association, + const std::uint8_t first, + const std::uint8_t second) noexcept { + mms::MmsStaticBrcbClientIdentity identity; + identity.association_id = association; + identity.owner[0] = first; + identity.owner[1] = second; + identity.owner_size = 2U; + return identity; +} + +} // namespace + +int main() { + bool value = true; + const std::array objects{ + mms::MmsStaticObjectEntry{"LD0", "X1", kBooleanType, read_boolean, &value, false}}; + const mms::MmsStaticObjectTable object_table{objects}; + + const std::array members{ + mms::MmsStaticDataSetMember{"LD0", "X1"}}; + const std::array data_sets{ + mms::MmsStaticDataSetEntry{"LD0", "LLN0$Events", members, false}}; + const mms::MmsStaticDataSetTable data_set_table{data_sets}; + + const mms::MmsStaticBrcbDefinition definition{ + "LD0", + "LLN0$BR$Events", + "LD0/LLN0$BR$Events", + "LD0", + "LLN0$Events", + 1U, + {0x7FU, 0x80U}, + 20U, + 0x70U}; + + std::array slot0{}; + std::array slot1{}; + std::array slots{ + mms::MmsStaticBrcbSlot{slot0}, + mms::MmsStaticBrcbSlot{slot1}}; + mms::MmsStaticBrcbPendingState pending{}; + mms::MmsStaticBrcbRuntime reports{ + definition, pending, slots, object_table, data_set_table}; + if (!reports.initialize()) { + return 1; + } + + mms::MmsStaticBrcbControl control{reports}; + const auto a1 = client(101U, 0xAAU, 0x01U); + const auto a2 = client(102U, 0xAAU, 0x01U); + const auto a3 = client(103U, 0xAAU, 0x01U); + const auto b1 = client(201U, 0xBBU, 0x01U); + const std::array missing_entry{0U,0U,0U,0U,0U,0U,0U,1U}; + + if (control.reserve(a1, 5U, 100U) != mms::MmsStaticBrcbControlStatus::ok) { + return 2; + } + auto state = control.state(100U); + if (!state.reserved || !state.owner_connected || state.report_enabled || + state.association_id != 101U || state.resv_tms_seconds != 5U || + state.owner.size() != 2U || state.owner[0] != 0xAAU) { + return 3; + } + + // A second live association, even with the same stable Owner identity, + // cannot steal a currently connected reservation. A different Owner also + // cannot reserve or enable the BRCB. + if (control.reserve(a2, 5U, 200U) != + mms::MmsStaticBrcbControlStatus::object_access_denied || + control.reserve(b1, 5U, 200U) != + mms::MmsStaticBrcbControlStatus::object_access_denied || + control.set_report_enabled(b1, true, 200U) != + mms::MmsStaticBrcbControlStatus::object_access_denied) { + return 4; + } + + if (control.set_report_enabled(a1, true, 300U) != + mms::MmsStaticBrcbControlStatus::ok || + !reports.enabled() || + control.replay_from(a1, missing_entry, 300U) != + mms::MmsStaticBrcbControlStatus::temporarily_unavailable) { + return 5; + } + + // Association loss always disables reporting. Non-zero ResvTms retains the + // stable Owner identity so the same Owner can reclaim before timeout. + control.on_association_closed(a1.association_id, 1'000U); + state = control.state(1'000U); + if (reports.enabled() || !state.reserved || state.owner_connected || + state.association_id != 0U || state.reservation_expires_at_ms != 6'000U || + control.reserve(b1, 5U, 2'000U) != + mms::MmsStaticBrcbControlStatus::object_access_denied || + control.reserve(a2, 7U, 2'500U) != mms::MmsStaticBrcbControlStatus::ok) { + return 6; + } + + // The old association token is dead; only the reconnecting live association + // may control replay/purge/release. + if (control.set_report_enabled(a1, true, 2'600U) != + mms::MmsStaticBrcbControlStatus::object_access_denied || + control.replay_from(a2, missing_entry, 2'600U) != + mms::MmsStaticBrcbControlStatus::entry_not_found || + control.purge_buffer(a2, 2'600U) != mms::MmsStaticBrcbControlStatus::ok || + control.release(a2, 2'600U) != mms::MmsStaticBrcbControlStatus::ok || + control.state(2'600U).reserved) { + return 7; + } + + // ResvTms==0 releases immediately on association loss. + if (control.reserve(a3, 0U, 3'000U) != mms::MmsStaticBrcbControlStatus::ok || + control.set_report_enabled(a3, true, 3'000U) != mms::MmsStaticBrcbControlStatus::ok) { + return 8; + } + control.on_association_closed(a3.association_id, 3'100U); + if (reports.enabled() || control.state(3'100U).reserved) { + return 9; + } + + // A retained reservation blocks another Owner until the exact expiry, then + // automatically becomes available without dynamic allocation or threads. + if (control.reserve(a1, 2U, 10'000U) != mms::MmsStaticBrcbControlStatus::ok) { + return 10; + } + control.on_association_closed(a1.association_id, 10'000U); + if (control.reserve(b1, 1U, 11'999U) != + mms::MmsStaticBrcbControlStatus::object_access_denied) { + return 11; + } + control.tick(12'000U); + if (control.state(12'000U).reserved || + control.reserve(b1, 1U, 12'000U) != mms::MmsStaticBrcbControlStatus::ok) { + return 12; + } + + return 0; +} diff --git a/include/ariec61850/mms/static_brcb_control.hpp b/include/ariec61850/mms/static_brcb_control.hpp new file mode 100644 index 000000000..946e8b884 --- /dev/null +++ b/include/ariec61850/mms/static_brcb_control.hpp @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/static_brcb_runtime.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::mms { + +enum class MmsStaticBrcbControlStatus : std::uint8_t { + ok, + invalid_runtime, + invalid_identity, + object_access_denied, + temporarily_unavailable, + entry_not_found, + backend_failure, +}; + +// Association identity is deliberately separate from the stable Owner identity. +// `association_id` is unique for one live MMS association. `owner` is a stable, +// caller-supplied binary identity that may be reused by a reconnecting client. +// No IP/socket dependency is embedded in the portable core. +struct MmsStaticBrcbClientIdentity final { + static constexpr std::size_t maximum_owner_bytes = 16U; + + std::uint64_t association_id{}; + std::array owner{}; + std::size_t owner_size{}; + + [[nodiscard]] constexpr bool valid() const noexcept { + return association_id != 0U && owner_size > 0U && owner_size <= owner.size(); + } + + [[nodiscard]] constexpr std::span owner_view() const noexcept { + return {owner.data(), owner_size <= owner.size() ? owner_size : 0U}; + } +}; + +struct MmsStaticBrcbControlStateView final { + bool reserved{}; + bool owner_connected{}; + bool report_enabled{}; + std::uint32_t resv_tms_seconds{}; + std::uint64_t association_id{}; + std::uint64_t reservation_expires_at_ms{}; + std::span owner{}; +}; + +// Portable owner/reservation gate for one BRCB runtime. The runtime itself owns +// buffered report data; this class owns association-sensitive control state. +// It is allocation-free, exception-free and does not depend on TCP/IP types. +class MmsStaticBrcbControl final { +public: + explicit constexpr MmsStaticBrcbControl(MmsStaticBrcbRuntime& reports) noexcept + : reports_{&reports} {} + + [[nodiscard]] constexpr bool valid() const noexcept { + return reports_ != nullptr && reports_->valid(); + } + + [[nodiscard]] MmsStaticBrcbControlStatus reserve( + const MmsStaticBrcbClientIdentity& client, + std::uint32_t resv_tms_seconds, + std::uint64_t now_ms) noexcept; + + [[nodiscard]] MmsStaticBrcbControlStatus release( + const MmsStaticBrcbClientIdentity& client, + std::uint64_t now_ms) noexcept; + + [[nodiscard]] MmsStaticBrcbControlStatus set_report_enabled( + const MmsStaticBrcbClientIdentity& client, + bool enabled, + std::uint64_t now_ms) noexcept; + + [[nodiscard]] MmsStaticBrcbControlStatus replay_from( + const MmsStaticBrcbClientIdentity& client, + std::span entry_id, + std::uint64_t now_ms) noexcept; + + [[nodiscard]] MmsStaticBrcbControlStatus resume_after( + const MmsStaticBrcbClientIdentity& client, + std::span entry_id, + std::uint64_t now_ms) noexcept; + + [[nodiscard]] MmsStaticBrcbControlStatus rewind_to_oldest( + const MmsStaticBrcbClientIdentity& client, + std::uint64_t now_ms) noexcept; + + [[nodiscard]] MmsStaticBrcbControlStatus purge_buffer( + const MmsStaticBrcbClientIdentity& client, + std::uint64_t now_ms) noexcept; + + // Called when a live MMS association is lost/closed. RptEna is always + // disabled. ResvTms==0 releases immediately; non-zero retains the stable + // Owner identity until expiry and permits that Owner to reconnect early. + void on_association_closed( + std::uint64_t association_id, + std::uint64_t now_ms) noexcept; + + // Expires a disconnected retained reservation once ResvTms elapses. + void tick(std::uint64_t now_ms) noexcept; + + [[nodiscard]] MmsStaticBrcbControlStateView state( + std::uint64_t now_ms) noexcept; + +private: + [[nodiscard]] bool same_owner( + const MmsStaticBrcbClientIdentity& client) const noexcept; + [[nodiscard]] bool owns_live_association( + const MmsStaticBrcbClientIdentity& client) const noexcept; + [[nodiscard]] MmsStaticBrcbControlStatus require_control_access( + const MmsStaticBrcbClientIdentity& client, + std::uint64_t now_ms) noexcept; + [[nodiscard]] MmsStaticBrcbControlStatus map_runtime_status( + MmsStaticBrcbStatus status) const noexcept; + void clear_reservation() noexcept; + + MmsStaticBrcbRuntime* reports_{}; + std::array owner_{}; + std::size_t owner_size_{}; + std::uint64_t association_id_{}; + std::uint64_t reservation_expires_at_ms_{}; + std::uint32_t resv_tms_seconds_{}; + bool reserved_{}; + bool owner_connected_{}; +}; + +} // namespace ar::iec61850::mms diff --git a/src/mms/static_brcb_control.cpp b/src/mms/static_brcb_control.cpp new file mode 100644 index 000000000..3aeb051ce --- /dev/null +++ b/src/mms/static_brcb_control.cpp @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_brcb_control.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +[[nodiscard]] constexpr std::uint64_t reservation_deadline( + const std::uint64_t now_ms, + const std::uint32_t seconds) noexcept { + constexpr auto kMax = std::numeric_limits::max(); + const auto seconds64 = static_cast(seconds); + if (seconds64 > kMax / 1'000U) { + return kMax; + } + const auto duration = seconds64 * 1'000U; + return duration > kMax - now_ms ? kMax : now_ms + duration; +} + +} // namespace + +bool MmsStaticBrcbControl::same_owner( + const MmsStaticBrcbClientIdentity& client) const noexcept { + return client.valid() && reserved_ && client.owner_size == owner_size_ && + std::equal( + client.owner.begin(), + client.owner.begin() + static_cast(client.owner_size), + owner_.begin()); +} + +bool MmsStaticBrcbControl::owns_live_association( + const MmsStaticBrcbClientIdentity& client) const noexcept { + return same_owner(client) && owner_connected_ && + client.association_id == association_id_; +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::map_runtime_status( + const MmsStaticBrcbStatus status) const noexcept { + switch (status) { + case MmsStaticBrcbStatus::ok: + return MmsStaticBrcbControlStatus::ok; + case MmsStaticBrcbStatus::entry_not_found: + return MmsStaticBrcbControlStatus::entry_not_found; + case MmsStaticBrcbStatus::temporarily_unavailable: + case MmsStaticBrcbStatus::no_report_due: + case MmsStaticBrcbStatus::stale_plan: + return MmsStaticBrcbControlStatus::temporarily_unavailable; + case MmsStaticBrcbStatus::invalid_runtime: + case MmsStaticBrcbStatus::invalid_definition: + case MmsStaticBrcbStatus::member_out_of_range: + case MmsStaticBrcbStatus::trigger_not_selected: + case MmsStaticBrcbStatus::data_set_not_found: + case MmsStaticBrcbStatus::object_not_found: + case MmsStaticBrcbStatus::workspace_too_small: + case MmsStaticBrcbStatus::response_buffer_too_small: + case MmsStaticBrcbStatus::slot_too_small: + case MmsStaticBrcbStatus::backend_failure: + case MmsStaticBrcbStatus::report_encode_failed: + return MmsStaticBrcbControlStatus::backend_failure; + } + return MmsStaticBrcbControlStatus::backend_failure; +} + +void MmsStaticBrcbControl::clear_reservation() noexcept { + owner_.fill(0U); + owner_size_ = 0U; + association_id_ = 0U; + reservation_expires_at_ms_ = 0U; + resv_tms_seconds_ = 0U; + reserved_ = false; + owner_connected_ = false; +} + +void MmsStaticBrcbControl::tick(const std::uint64_t now_ms) noexcept { + if (!reserved_ || owner_connected_ || resv_tms_seconds_ == 0U) { + return; + } + if (now_ms < reservation_expires_at_ms_) { + return; + } + if (reports_ != nullptr && reports_->valid() && reports_->enabled()) { + static_cast(reports_->set_enabled(false)); + } + clear_reservation(); +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::reserve( + const MmsStaticBrcbClientIdentity& client, + const std::uint32_t resv_tms_seconds, + const std::uint64_t now_ms) noexcept { + if (!valid()) { + return MmsStaticBrcbControlStatus::invalid_runtime; + } + if (!client.valid()) { + return MmsStaticBrcbControlStatus::invalid_identity; + } + + tick(now_ms); + if (!reserved_) { + std::copy_n(client.owner.begin(), client.owner_size, owner_.begin()); + owner_size_ = client.owner_size; + association_id_ = client.association_id; + resv_tms_seconds_ = resv_tms_seconds; + reservation_expires_at_ms_ = 0U; + reserved_ = true; + owner_connected_ = true; + return MmsStaticBrcbControlStatus::ok; + } + + if (!same_owner(client)) { + return MmsStaticBrcbControlStatus::object_access_denied; + } + if (owner_connected_ && association_id_ != client.association_id) { + return MmsStaticBrcbControlStatus::object_access_denied; + } + + // A disconnected stable Owner may reclaim its reservation before timeout. + association_id_ = client.association_id; + owner_connected_ = true; + resv_tms_seconds_ = resv_tms_seconds; + reservation_expires_at_ms_ = 0U; + return MmsStaticBrcbControlStatus::ok; +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::require_control_access( + const MmsStaticBrcbClientIdentity& client, + const std::uint64_t now_ms) noexcept { + if (!valid()) { + return MmsStaticBrcbControlStatus::invalid_runtime; + } + if (!client.valid()) { + return MmsStaticBrcbControlStatus::invalid_identity; + } + tick(now_ms); + return owns_live_association(client) + ? MmsStaticBrcbControlStatus::ok + : MmsStaticBrcbControlStatus::object_access_denied; +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::release( + const MmsStaticBrcbClientIdentity& client, + const std::uint64_t now_ms) noexcept { + const auto access = require_control_access(client, now_ms); + if (access != MmsStaticBrcbControlStatus::ok) { + return access; + } + if (reports_->enabled()) { + return MmsStaticBrcbControlStatus::temporarily_unavailable; + } + clear_reservation(); + return MmsStaticBrcbControlStatus::ok; +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::set_report_enabled( + const MmsStaticBrcbClientIdentity& client, + const bool enabled, + const std::uint64_t now_ms) noexcept { + const auto access = require_control_access(client, now_ms); + if (access != MmsStaticBrcbControlStatus::ok) { + return access; + } + return map_runtime_status(reports_->set_enabled(enabled)); +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::replay_from( + const MmsStaticBrcbClientIdentity& client, + const std::span entry_id, + const std::uint64_t now_ms) noexcept { + const auto access = require_control_access(client, now_ms); + if (access != MmsStaticBrcbControlStatus::ok) { + return access; + } + if (reports_->enabled()) { + return MmsStaticBrcbControlStatus::temporarily_unavailable; + } + return map_runtime_status(reports_->replay_from(entry_id)); +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::resume_after( + const MmsStaticBrcbClientIdentity& client, + const std::span entry_id, + const std::uint64_t now_ms) noexcept { + const auto access = require_control_access(client, now_ms); + if (access != MmsStaticBrcbControlStatus::ok) { + return access; + } + if (reports_->enabled()) { + return MmsStaticBrcbControlStatus::temporarily_unavailable; + } + return map_runtime_status(reports_->resume_after(entry_id)); +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::rewind_to_oldest( + const MmsStaticBrcbClientIdentity& client, + const std::uint64_t now_ms) noexcept { + const auto access = require_control_access(client, now_ms); + if (access != MmsStaticBrcbControlStatus::ok) { + return access; + } + if (reports_->enabled()) { + return MmsStaticBrcbControlStatus::temporarily_unavailable; + } + return map_runtime_status(reports_->rewind_to_oldest()); +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::purge_buffer( + const MmsStaticBrcbClientIdentity& client, + const std::uint64_t now_ms) noexcept { + const auto access = require_control_access(client, now_ms); + if (access != MmsStaticBrcbControlStatus::ok) { + return access; + } + if (reports_->enabled()) { + return MmsStaticBrcbControlStatus::temporarily_unavailable; + } + return map_runtime_status(reports_->purge_buffer()); +} + +void MmsStaticBrcbControl::on_association_closed( + const std::uint64_t association_id, + const std::uint64_t now_ms) noexcept { + tick(now_ms); + if (!reserved_ || !owner_connected_ || association_id == 0U || + association_id != association_id_) { + return; + } + + if (reports_ != nullptr && reports_->valid() && reports_->enabled()) { + static_cast(reports_->set_enabled(false)); + } + association_id_ = 0U; + owner_connected_ = false; + if (resv_tms_seconds_ == 0U) { + clear_reservation(); + return; + } + reservation_expires_at_ms_ = reservation_deadline(now_ms, resv_tms_seconds_); +} + +MmsStaticBrcbControlStateView MmsStaticBrcbControl::state( + const std::uint64_t now_ms) noexcept { + tick(now_ms); + return { + reserved_, + owner_connected_, + reports_ != nullptr && reports_->valid() && reports_->enabled(), + resv_tms_seconds_, + association_id_, + reservation_expires_at_ms_, + {owner_.data(), owner_size_}}; +} + +} // namespace ar::iec61850::mms