Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .github/workflows/brcb-hard-profile-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
19 changes: 18 additions & 1 deletion embedded/brcb_hard_profile/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand Down
168 changes: 168 additions & 0 deletions embedded/mms_brcb_control_hard_profile_smoke.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// SPDX-License-Identifier: GPL-3.0-or-later

#include "ariec61850/mms/static_brcb_control.hpp"

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

namespace {
using namespace ar::iec61850;

constexpr std::array<std::uint8_t, 2U> kBooleanType{0x83U, 0x00U};

[[nodiscard]] wire::EncodeResult read_boolean(
const void* context,
const std::span<std::uint8_t> 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<const bool*>(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<mms::MmsStaticObjectEntry, 1U> objects{
mms::MmsStaticObjectEntry{"LD0", "X1", kBooleanType, read_boolean, &value, false}};
const mms::MmsStaticObjectTable object_table{objects};

const std::array<mms::MmsStaticDataSetMember, 1U> members{
mms::MmsStaticDataSetMember{"LD0", "X1"}};
const std::array<mms::MmsStaticDataSetEntry, 1U> 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<std::uint8_t, 512U> slot0{};
std::array<std::uint8_t, 512U> slot1{};
std::array<mms::MmsStaticBrcbSlot, 2U> 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<std::uint8_t, 8U> 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;
}
132 changes: 132 additions & 0 deletions include/ariec61850/mms/static_brcb_control.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

#include "ariec61850/mms/static_brcb_runtime.hpp"

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

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<std::uint8_t, maximum_owner_bytes> 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<const std::uint8_t> 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<const std::uint8_t> 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<const std::uint8_t> entry_id,
std::uint64_t now_ms) noexcept;

[[nodiscard]] MmsStaticBrcbControlStatus resume_after(
const MmsStaticBrcbClientIdentity& client,
std::span<const std::uint8_t> 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<std::uint8_t, MmsStaticBrcbClientIdentity::maximum_owner_bytes> 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
Loading
Loading