From 44c13445fd094ee5a6c4e36ce2bdb932d9304181 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 10 Aug 2026 04:48:37 +0700 Subject: [PATCH 1/8] Expose latest BRCB EntryID for MMS control reads --- .../ariec61850/mms/static_brcb_runtime.hpp | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/include/ariec61850/mms/static_brcb_runtime.hpp b/include/ariec61850/mms/static_brcb_runtime.hpp index 93e17317d..bb43ce04e 100644 --- a/include/ariec61850/mms/static_brcb_runtime.hpp +++ b/include/ariec61850/mms/static_brcb_runtime.hpp @@ -54,8 +54,6 @@ struct MmsStaticBrcbDefinition final { std::uint8_t trigger_options{}; }; -// Payload storage is caller-owned so applications choose the queue RAM budget. -// `storage` must remain valid for the lifetime of the runtime. struct MmsStaticBrcbSlot final { std::span storage{}; std::array entry_id{}; @@ -101,11 +99,6 @@ struct MmsStaticBrcbEntryView final { bool buffer_overflow{}; }; -// Bounded BRCB profile: one runtime instance represents one BRCB. Captured -// snapshots remain retained in caller-owned RAM after successful delivery so a -// client can reposition the delivery cursor by EntryID. The active delivery -// queue and the retained-history window are therefore intentionally distinct. -// Persistence is storage-agnostic and mediated by MmsStaticBrcbStateCodec. class MmsStaticBrcbRuntime final { public: MmsStaticBrcbRuntime( @@ -138,37 +131,40 @@ class MmsStaticBrcbRuntime final { std::span encode_buffer, std::span workspace) noexcept; - // Returns the next entry selected by the current delivery cursor. [[nodiscard]] bool front(MmsStaticBrcbEntryView& entry) const noexcept; - // Marks the currently selected entry as delivered without deleting the - // retained snapshot. This is what makes later replay/resume possible. [[nodiscard]] MmsStaticBrcbStatus commit_delivery( std::span expected_entry_id) noexcept; - // Reposition delivery to the exact retained EntryID. [[nodiscard]] MmsStaticBrcbStatus replay_from( std::span entry_id) noexcept; - // Reposition delivery to the retained entry immediately after EntryID. - // If EntryID is the newest retained entry, the active queue becomes empty. [[nodiscard]] MmsStaticBrcbStatus resume_after( std::span entry_id) noexcept; - // Replays the complete retained window from its oldest available entry. [[nodiscard]] MmsStaticBrcbStatus rewind_to_oldest() noexcept; - // Implements the runtime PurgeBuf primitive. It clears retained reports and - // delivery/recovery state while preserving the report definition, EntryID - // monotonic progression, and any independent pending trigger accumulation. [[nodiscard]] MmsStaticBrcbStatus purge_buffer() noexcept; - // Number of reports still selected for delivery. + // EntryID attribute readback: all zero when no report is buffered, + // otherwise the newest retained report EntryID. + [[nodiscard]] std::array latest_entry_id() const noexcept { + std::array empty{}; + if (!initialized_ || slots_.empty() || count_ == 0U || + count_ > slots_.size() || head_ >= slots_.size()) { + return empty; + } + const auto physical = (head_ + count_ - 1U) % slots_.size(); + const auto& slot = slots_[physical]; + return slot.occupied ? slot.entry_id : empty; + } + [[nodiscard]] constexpr std::size_t queue_size() const noexcept { return delivery_offset_ <= count_ ? count_ - delivery_offset_ : 0U; } - // Number of snapshots retained for possible replay, delivered or not. [[nodiscard]] constexpr std::size_t retained_size() const noexcept { return count_; } [[nodiscard]] constexpr std::size_t queue_capacity() const noexcept { @@ -178,9 +174,6 @@ class MmsStaticBrcbRuntime final { return dropped_reports_; } - // True after an undelivered retained entry was evicted because the bounded - // buffer filled. PurgeBuf clears this condition; replay cannot reconstruct - // data that is no longer retained. [[nodiscard]] constexpr bool replay_gap() const noexcept { return replay_gap_; } private: @@ -208,4 +201,4 @@ class MmsStaticBrcbRuntime final { bool initialized_{}; }; -} // namespace ar::iec61850::mms \ No newline at end of file +} // namespace ar::iec61850::mms From 9b7d905767f3e35e0406e2b688cbc88f80960601 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 10 Aug 2026 04:49:09 +0700 Subject: [PATCH 2/8] Define BRCB MMS operational object bank --- .../ariec61850/mms/static_brcb_objects.hpp | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 include/ariec61850/mms/static_brcb_objects.hpp diff --git a/include/ariec61850/mms/static_brcb_objects.hpp b/include/ariec61850/mms/static_brcb_objects.hpp new file mode 100644 index 000000000..97c3594b1 --- /dev/null +++ b/include/ariec61850/mms/static_brcb_objects.hpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/static_brcb_control.hpp" +#include "ariec61850/mms/static_object_table.hpp" + +#include +#include +#include + +namespace ar::iec61850::mms { + +using MmsStaticBrcbNowMsCallback = std::uint64_t (*)(const void* context) noexcept; + +enum class MmsStaticBrcbAttribute : std::uint8_t { + report_id, + report_enabled, + data_set, + conf_revision, + purge_buffer, + entry_id, + reservation_time, + owner, +}; + +struct MmsStaticBrcbObjectContext final { + const MmsStaticBrcbDefinition* definition{}; + MmsStaticBrcbRuntime* reports{}; + MmsStaticBrcbControl* control{}; + MmsStaticBrcbAttribute attribute{MmsStaticBrcbAttribute::report_id}; + MmsStaticBrcbNowMsCallback now_ms{}; + const void* now_context{}; +}; + +// Allocation-free MMS object facade for one bounded BRCB runtime. The caller +// owns object/context/name storage. Association-sensitive writes are routed via +// MmsStaticContextualWriteCallback and therefore receive the active request's +// opaque association/Owner identity. +class MmsStaticBrcbObjectBank final { +public: + static constexpr std::size_t attributes_per_control_block = 8U; + + MmsStaticBrcbObjectBank( + const MmsStaticBrcbDefinition& definition, + MmsStaticBrcbRuntime& reports, + MmsStaticBrcbControl& control, + std::span base_objects, + std::span object_storage, + std::span context_storage, + std::span name_storage, + MmsStaticBrcbNowMsCallback now_ms = nullptr, + const void* now_context = nullptr) noexcept + : definition_{&definition}, + reports_{&reports}, + control_{&control}, + base_objects_{base_objects}, + object_storage_{object_storage}, + context_storage_{context_storage}, + name_storage_{name_storage}, + now_ms_{now_ms}, + now_context_{now_context} {} + + [[nodiscard]] bool initialize() noexcept; + + [[nodiscard]] constexpr bool valid() const noexcept { return initialized_; } + [[nodiscard]] constexpr std::size_t object_count() const noexcept { + return initialized_ ? object_count_ : 0U; + } + [[nodiscard]] constexpr std::size_t name_bytes_used() const noexcept { + return initialized_ ? name_bytes_used_ : 0U; + } + + [[nodiscard]] std::size_t required_object_capacity() const noexcept; + [[nodiscard]] constexpr std::size_t required_context_capacity() const noexcept { + return attributes_per_control_block; + } + [[nodiscard]] std::size_t required_name_bytes() const noexcept; + + [[nodiscard]] constexpr const MmsStaticObjectTable& table() const noexcept { + return table_; + } + +private: + const MmsStaticBrcbDefinition* definition_{}; + MmsStaticBrcbRuntime* reports_{}; + MmsStaticBrcbControl* control_{}; + std::span base_objects_{}; + std::span object_storage_{}; + std::span context_storage_{}; + std::span name_storage_{}; + MmsStaticBrcbNowMsCallback now_ms_{}; + const void* now_context_{}; + MmsStaticObjectTable table_{std::span{}}; + std::size_t object_count_{}; + std::size_t name_bytes_used_{}; + bool initialized_{}; +}; + +} // namespace ar::iec61850::mms From 21fea638cc7abd9bd4972b42ccfaad7cdd09c4fb Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 10 Aug 2026 04:50:08 +0700 Subject: [PATCH 3/8] Implement BRCB MMS operational reads and writes --- src/mms/static_brcb_objects.cpp | 575 ++++++++++++++++++++++++++++++++ 1 file changed, 575 insertions(+) create mode 100644 src/mms/static_brcb_objects.cpp diff --git a/src/mms/static_brcb_objects.cpp b/src/mms/static_brcb_objects.cpp new file mode 100644 index 000000000..840f453a1 --- /dev/null +++ b/src/mms/static_brcb_objects.cpp @@ -0,0 +1,575 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_brcb_objects.hpp" + +#include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/asn1/ber_span_writer.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +constexpr std::array kBooleanType{0x83U, 0x00U}; +constexpr std::array kUnsigned32Type{0x86U, 0x01U, 0x20U}; +constexpr std::array kInteger16Type{0x85U, 0x01U, 0x10U}; +constexpr std::array kOctetString8Type{0x89U, 0x01U, 0x08U}; +constexpr std::array kOctetString16Type{0x89U, 0x01U, 0x10U}; +constexpr std::array kVisible255Type{0x8AU, 0x02U, 0x00U, 0xFFU}; + +constexpr std::array + kAttributes{ + MmsStaticBrcbAttribute::report_id, + MmsStaticBrcbAttribute::report_enabled, + MmsStaticBrcbAttribute::data_set, + MmsStaticBrcbAttribute::conf_revision, + MmsStaticBrcbAttribute::purge_buffer, + MmsStaticBrcbAttribute::entry_id, + MmsStaticBrcbAttribute::reservation_time, + MmsStaticBrcbAttribute::owner}; + +constexpr std::array + kSuffixes{ + "RptID", + "RptEna", + "DatSet", + "ConfRev", + "PurgeBuf", + "EntryID", + "ResvTms", + "Owner"}; + +constexpr std::uint32_t kHardwareFault = 1U; +constexpr std::uint32_t kTemporarilyUnavailable = 2U; +constexpr std::uint32_t kObjectAccessDenied = 3U; +constexpr std::uint32_t kTypeInconsistent = 7U; +constexpr std::uint32_t kObjectValueInvalid = 11U; + +[[nodiscard]] std::span as_bytes( + const std::string_view text) noexcept { + return { + reinterpret_cast(text.data()), + text.size()}; +} + +[[nodiscard]] std::uint64_t now_ms(const MmsStaticBrcbObjectContext& context) noexcept { + return context.now_ms == nullptr + ? std::uint64_t{0U} + : context.now_ms(context.now_context); +} + +[[nodiscard]] std::span type_for( + const MmsStaticBrcbAttribute attribute) noexcept { + switch (attribute) { + case MmsStaticBrcbAttribute::report_id: + case MmsStaticBrcbAttribute::data_set: + return kVisible255Type; + case MmsStaticBrcbAttribute::report_enabled: + case MmsStaticBrcbAttribute::purge_buffer: + return kBooleanType; + case MmsStaticBrcbAttribute::conf_revision: + return kUnsigned32Type; + case MmsStaticBrcbAttribute::entry_id: + return kOctetString8Type; + case MmsStaticBrcbAttribute::reservation_time: + return kInteger16Type; + case MmsStaticBrcbAttribute::owner: + return kOctetString16Type; + } + return {}; +} + +[[nodiscard]] bool writable_attribute(const MmsStaticBrcbAttribute attribute) noexcept { + return attribute == MmsStaticBrcbAttribute::report_enabled || + attribute == MmsStaticBrcbAttribute::purge_buffer || + attribute == MmsStaticBrcbAttribute::entry_id || + attribute == MmsStaticBrcbAttribute::reservation_time; +} + +[[nodiscard]] wire::EncodeResult capacity_result( + const std::size_t required, + const std::span destination) noexcept { + return destination.size() < required + ? wire::EncodeResult{wire::EncodeStatus::buffer_too_small, 0U, required} + : wire::EncodeResult{wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] wire::EncodeResult encode_boolean( + const bool value, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + const auto capacity = capacity_result(required, destination); + if (!capacity.success()) { + return capacity; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = value ? 0xFFU : 0x00U; + return capacity; +} + +[[nodiscard]] std::size_t unsigned_content_size(std::uint32_t value) noexcept { + std::size_t size = 1U; + while (value > 0xFFU) { + ++size; + value >>= 8U; + } + return size; +} + +[[nodiscard]] wire::EncodeResult encode_unsigned( + const std::uint32_t value, + const std::span destination) noexcept { + const auto content_size = unsigned_content_size(value); + const auto total = asn1::BerSpanWriter::tlv_size(6, content_size); + if (!total) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto capacity = capacity_result(*total, destination); + if (!capacity.success()) { + return capacity; + } + asn1::BerSpanWriter writer{destination.first(*total)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 6, content_size)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + for (std::size_t index = content_size; index-- > 0U;) { + const auto shift = static_cast(index * 8U); + if (!writer.write_byte(static_cast((value >> shift) & 0xFFU))) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + } + return writer.size() == *total + ? capacity + : wire::EncodeResult{wire::EncodeStatus::value_out_of_range, 0U, *total}; +} + +[[nodiscard]] wire::EncodeResult encode_nonnegative_integer16( + const std::uint32_t value, + const std::span destination) noexcept { + if (value > 0x7FFFU) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const std::size_t content_size = value <= 0x7FU ? 1U : 2U; + const auto total = asn1::BerSpanWriter::tlv_size(5, content_size); + if (!total) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto capacity = capacity_result(*total, destination); + if (!capacity.success()) { + return capacity; + } + asn1::BerSpanWriter writer{destination.first(*total)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 5, content_size)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + if (content_size == 2U && + !writer.write_byte(static_cast((value >> 8U) & 0x7FU))) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + if (!writer.write_byte(static_cast(value & 0xFFU)) || + writer.size() != *total) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + return capacity; +} + +[[nodiscard]] wire::EncodeResult encode_visible( + const std::string_view text, + const std::span destination) noexcept { + const auto total = asn1::BerSpanWriter::tlv_size(10, text.size()); + if (!total) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto capacity = capacity_result(*total, destination); + if (!capacity.success()) { + return capacity; + } + asn1::BerSpanWriter writer{destination.first(*total)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 10, text.size()) || + !writer.write_bytes(as_bytes(text)) || writer.size() != *total) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + return capacity; +} + +[[nodiscard]] wire::EncodeResult encode_data_set_reference( + const std::string_view domain, + const std::string_view item, + const std::span destination) noexcept { + if (domain.size() > std::numeric_limits::max() - 1U - item.size()) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto content_size = domain.size() + 1U + item.size(); + const auto total = asn1::BerSpanWriter::tlv_size(10, content_size); + if (!total) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto capacity = capacity_result(*total, destination); + if (!capacity.success()) { + return capacity; + } + asn1::BerSpanWriter writer{destination.first(*total)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 10, content_size) || + !writer.write_bytes(as_bytes(domain)) || + !writer.write_byte(static_cast('/')) || + !writer.write_bytes(as_bytes(item)) || writer.size() != *total) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + return capacity; +} + +[[nodiscard]] wire::EncodeResult encode_octets( + const std::span bytes, + const std::span destination) noexcept { + const auto total = asn1::BerSpanWriter::tlv_size(9, bytes.size()); + if (!total) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto capacity = capacity_result(*total, destination); + if (!capacity.success()) { + return capacity; + } + asn1::BerSpanWriter writer{destination.first(*total)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 9, bytes.size()) || + !writer.write_bytes(bytes) || writer.size() != *total) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + return capacity; +} + +[[nodiscard]] wire::EncodeResult read_brcb_attribute( + const void* raw_context, + const std::span destination) noexcept { + const auto* context = static_cast(raw_context); + if (context == nullptr || context->definition == nullptr || + context->reports == nullptr || context->control == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + switch (context->attribute) { + case MmsStaticBrcbAttribute::report_id: + return encode_visible(context->definition->report_id, destination); + case MmsStaticBrcbAttribute::report_enabled: + return encode_boolean(context->reports->enabled(), destination); + case MmsStaticBrcbAttribute::data_set: + return encode_data_set_reference( + context->definition->data_set_domain, + context->definition->data_set_item, + destination); + case MmsStaticBrcbAttribute::conf_revision: + return encode_unsigned(context->definition->conf_revision, destination); + case MmsStaticBrcbAttribute::purge_buffer: + return encode_boolean(false, destination); + case MmsStaticBrcbAttribute::entry_id: { + const auto entry_id = context->reports->latest_entry_id(); + return encode_octets(entry_id, destination); + } + case MmsStaticBrcbAttribute::reservation_time: { + const auto state = context->control->state(now_ms(*context)); + return encode_nonnegative_integer16(state.resv_tms_seconds, destination); + } + case MmsStaticBrcbAttribute::owner: { + const auto state = context->control->state(now_ms(*context)); + return encode_octets(state.owner, destination); + } + } + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; +} + +[[nodiscard]] bool decode_boolean( + const std::span encoded, + bool& value) noexcept { + asn1::BerTlvView tlv; + if (!asn1::BerSpanReader::try_read_exact(encoded, tlv) || + tlv.tag_class != asn1::BerClass::context_specific || + tlv.tag_number != 3 || tlv.constructed || tlv.value.size() != 1U) { + return false; + } + value = tlv.value[0] != 0U; + return true; +} + +[[nodiscard]] bool decode_nonnegative_integer16( + const std::span encoded, + std::uint32_t& value) noexcept { + value = 0U; + asn1::BerTlvView tlv; + if (!asn1::BerSpanReader::try_read_exact(encoded, tlv) || + tlv.tag_class != asn1::BerClass::context_specific || + tlv.tag_number != 5 || tlv.constructed) { + return false; + } + const auto decoded = asn1::BerSpanReader::read_signed_integer(tlv); + if (!decoded || *decoded < 0 || *decoded > 0x7FFF) { + return false; + } + value = static_cast(*decoded); + return true; +} + +[[nodiscard]] bool decode_entry_id( + const std::span encoded, + std::span& entry_id) noexcept { + entry_id = {}; + asn1::BerTlvView tlv; + if (!asn1::BerSpanReader::try_read_exact(encoded, tlv) || + tlv.tag_class != asn1::BerClass::context_specific || + tlv.tag_number != 9 || tlv.constructed || + tlv.value.size() != MmsInformationReportSpanCodec::entry_id_bytes) { + return false; + } + entry_id = tlv.value; + return true; +} + +[[nodiscard]] bool all_zero(const std::span bytes) noexcept { + return std::all_of(bytes.begin(), bytes.end(), [](const std::uint8_t byte) { + return byte == 0U; + }); +} + +[[nodiscard]] bool make_client( + const MmsStaticRequestAccessContext& access, + MmsStaticBrcbClientIdentity& client) noexcept { + client = {}; + if (!access.valid() || + access.owner.size() > MmsStaticBrcbClientIdentity::maximum_owner_bytes) { + return false; + } + client.association_id = access.association_id; + client.owner_size = access.owner.size(); + std::copy(access.owner.begin(), access.owner.end(), client.owner.begin()); + return client.valid(); +} + +[[nodiscard]] MmsStaticWriteResult map_control_status( + const MmsStaticBrcbControlStatus status) noexcept { + switch (status) { + case MmsStaticBrcbControlStatus::ok: + return {true, 0U}; + case MmsStaticBrcbControlStatus::object_access_denied: + case MmsStaticBrcbControlStatus::invalid_identity: + return {false, kObjectAccessDenied}; + case MmsStaticBrcbControlStatus::temporarily_unavailable: + return {false, kTemporarilyUnavailable}; + case MmsStaticBrcbControlStatus::entry_not_found: + return {false, kObjectValueInvalid}; + case MmsStaticBrcbControlStatus::invalid_runtime: + case MmsStaticBrcbControlStatus::backend_failure: + return {false, kHardwareFault}; + } + return {false, kHardwareFault}; +} + +[[nodiscard]] MmsStaticBrcbControlStatus ensure_claimed( + MmsStaticBrcbObjectContext& context, + const MmsStaticBrcbClientIdentity& client, + const std::uint64_t now) noexcept { + const auto state = context.control->state(now); + return state.reserved + ? MmsStaticBrcbControlStatus::ok + : context.control->reserve(client, 0U, now); +} + +[[nodiscard]] MmsStaticWriteResult write_brcb_attribute( + void* raw_context, + const std::span encoded_data, + const MmsStaticRequestAccessContext& access) noexcept { + auto* context = static_cast(raw_context); + if (context == nullptr || context->definition == nullptr || + context->reports == nullptr || context->control == nullptr) { + return {false, kHardwareFault}; + } + + MmsStaticBrcbClientIdentity client; + if (!make_client(access, client)) { + return {false, kObjectAccessDenied}; + } + const auto now = now_ms(*context); + + switch (context->attribute) { + case MmsStaticBrcbAttribute::report_enabled: { + bool value = false; + if (!decode_boolean(encoded_data, value)) { + return {false, kTypeInconsistent}; + } + const auto state = context->control->state(now); + if (!value && !state.reserved) { + return {true, 0U}; + } + const auto claim = ensure_claimed(*context, client, now); + if (claim != MmsStaticBrcbControlStatus::ok) { + return map_control_status(claim); + } + return map_control_status( + context->control->set_report_enabled(client, value, now)); + } + case MmsStaticBrcbAttribute::purge_buffer: { + bool value = false; + if (!decode_boolean(encoded_data, value)) { + return {false, kTypeInconsistent}; + } + if (!value) { + return {true, 0U}; + } + const auto claim = ensure_claimed(*context, client, now); + if (claim != MmsStaticBrcbControlStatus::ok) { + return map_control_status(claim); + } + return map_control_status(context->control->purge_buffer(client, now)); + } + case MmsStaticBrcbAttribute::entry_id: { + std::span entry_id; + if (!decode_entry_id(encoded_data, entry_id)) { + return {false, kTypeInconsistent}; + } + const auto claim = ensure_claimed(*context, client, now); + if (claim != MmsStaticBrcbControlStatus::ok) { + return map_control_status(claim); + } + return all_zero(entry_id) + ? map_control_status(context->control->rewind_to_oldest(client, now)) + : map_control_status(context->control->resume_after(client, entry_id, now)); + } + case MmsStaticBrcbAttribute::reservation_time: { + std::uint32_t seconds = 0U; + if (!decode_nonnegative_integer16(encoded_data, seconds)) { + return {false, kTypeInconsistent}; + } + const auto state = context->control->state(now); + if (seconds == 0U) { + return !state.reserved + ? MmsStaticWriteResult{true, 0U} + : map_control_status(context->control->release(client, now)); + } + return map_control_status(context->control->reserve(client, seconds, now)); + } + case MmsStaticBrcbAttribute::report_id: + case MmsStaticBrcbAttribute::data_set: + case MmsStaticBrcbAttribute::conf_revision: + case MmsStaticBrcbAttribute::owner: + return {false, kObjectAccessDenied}; + } + return {false, kHardwareFault}; +} + +} // namespace + +std::size_t MmsStaticBrcbObjectBank::required_object_capacity() const noexcept { + if (base_objects_.size() > + std::numeric_limits::max() - attributes_per_control_block) { + return std::numeric_limits::max(); + } + return base_objects_.size() + attributes_per_control_block; +} + +std::size_t MmsStaticBrcbObjectBank::required_name_bytes() const noexcept { + if (definition_ == nullptr) { + return std::numeric_limits::max(); + } + std::size_t total = 0U; + for (const auto suffix : kSuffixes) { + if (definition_->item.size() > + MmsServiceSpanCodec::maximum_identifier_bytes - 1U - suffix.size()) { + return std::numeric_limits::max(); + } + const auto bytes = definition_->item.size() + 1U + suffix.size(); + if (bytes > std::numeric_limits::max() - total) { + return std::numeric_limits::max(); + } + total += bytes; + } + return total; +} + +bool MmsStaticBrcbObjectBank::initialize() noexcept { + initialized_ = false; + object_count_ = 0U; + name_bytes_used_ = 0U; + table_ = MmsStaticObjectTable{std::span{}}; + + if (definition_ == nullptr || reports_ == nullptr || control_ == nullptr || + !reports_->valid() || !control_->valid()) { + return false; + } + const auto required_objects = required_object_capacity(); + const auto required_names = required_name_bytes(); + if (required_objects == std::numeric_limits::max() || + required_names == std::numeric_limits::max() || + required_objects > object_storage_.size() || + required_objects > MmsStaticObjectTable::maximum_objects || + attributes_per_control_block > context_storage_.size() || + required_names > name_storage_.size()) { + return false; + } + + std::copy(base_objects_.begin(), base_objects_.end(), object_storage_.begin()); + std::size_t object_offset = base_objects_.size(); + std::size_t name_offset = 0U; + + for (std::size_t attribute_index = 0U; + attribute_index < attributes_per_control_block; + ++attribute_index) { + const auto suffix = kSuffixes[attribute_index]; + const auto name_size = definition_->item.size() + 1U + suffix.size(); + auto* name = name_storage_.data() + name_offset; + std::copy(definition_->item.begin(), definition_->item.end(), name); + name[definition_->item.size()] = '$'; + std::copy( + suffix.begin(), suffix.end(), + name + definition_->item.size() + 1U); + + auto& context = context_storage_[attribute_index]; + context = MmsStaticBrcbObjectContext{ + definition_, + reports_, + control_, + kAttributes[attribute_index], + now_ms_, + now_context_}; + + const auto attribute = kAttributes[attribute_index]; + const auto writable = writable_attribute(attribute); + object_storage_[object_offset] = MmsStaticObjectEntry{ + definition_->domain, + std::string_view{name, name_size}, + type_for(attribute), + read_brcb_attribute, + &context, + false, + nullptr, + writable ? &context : nullptr, + writable ? write_brcb_attribute : nullptr}; + + name_offset += name_size; + ++object_offset; + } + + table_ = MmsStaticObjectTable{ + std::span{object_storage_.data(), object_offset}}; + if (!table_.valid()) { + table_ = MmsStaticObjectTable{std::span{}}; + return false; + } + + object_count_ = object_offset; + name_bytes_used_ = name_offset; + initialized_ = true; + return true; +} + +} // namespace ar::iec61850::mms From bd521a82d61a3b8595def4c1b42140fe30f0f856 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 10 Aug 2026 04:51:14 +0700 Subject: [PATCH 4/8] Add encoded MMS BRCB operational control smoke --- .../mms_brcb_objects_hard_profile_smoke.cpp | 349 ++++++++++++++++++ 1 file changed, 349 insertions(+) create mode 100644 embedded/mms_brcb_objects_hard_profile_smoke.cpp diff --git a/embedded/mms_brcb_objects_hard_profile_smoke.cpp b/embedded/mms_brcb_objects_hard_profile_smoke.cpp new file mode 100644 index 000000000..3c5ea6a6e --- /dev/null +++ b/embedded/mms_brcb_objects_hard_profile_smoke.cpp @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/services_span.hpp" +#include "ariec61850/mms/static_brcb_objects.hpp" +#include "ariec61850/mms/static_dispatcher.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { +using namespace ar::iec61850; + +constexpr std::array kBooleanType{0x83U, 0x00U}; +constexpr std::array kTrue{0x83U, 0x01U, 0xFFU}; +constexpr std::array kFalse{0x83U, 0x01U, 0x00U}; +constexpr std::array kFiveSeconds{0x85U, 0x01U, 0x05U}; +constexpr std::array kZeroSeconds{0x85U, 0x01U, 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]] std::uint64_t read_now(const void* context) noexcept { + return context == nullptr ? 0U : *static_cast(context); +} + +[[nodiscard]] mms::MmsStaticRequestAccessContext access( + const std::uint64_t association, + const std::span owner) noexcept { + return {association, owner}; +} + +[[nodiscard]] std::size_t build_write_request( + const std::uint8_t invoke, + const std::string_view domain, + const std::string_view item, + const std::span data, + const std::span destination) noexcept { + if (domain.size() > 0x7FU || item.size() > 0x7FU || data.size() > 0x7FU) { + return 0U; + } + const auto total = 24U + item.size() + data.size() + domain.size() - 3U; + if (total > destination.size() || total > 0x7FU) { + return 0U; + } + const auto confirmed_content = total - 2U; + const auto write_content = 14U + domain.size() + item.size() + data.size(); + const auto variable_list_content = 10U + domain.size() + item.size(); + const auto sequence_content = 8U + domain.size() + item.size(); + const auto variable_content = 6U + domain.size() + item.size(); + const auto domain_specific_content = 4U + domain.size() + item.size(); + + std::size_t offset = 0U; + const auto put = [&](const std::uint8_t byte) mutable noexcept -> bool { + if (offset >= destination.size()) { + return false; + } + destination[offset++] = byte; + return true; + }; + if (!put(0xA0U) || !put(static_cast(confirmed_content)) || + !put(0x02U) || !put(0x01U) || !put(invoke) || + !put(0xA5U) || !put(static_cast(write_content)) || + !put(0xA0U) || !put(static_cast(variable_list_content)) || + !put(0x30U) || !put(static_cast(sequence_content)) || + !put(0xA0U) || !put(static_cast(variable_content)) || + !put(0xA1U) || !put(static_cast(domain_specific_content)) || + !put(0x1AU) || !put(static_cast(domain.size()))) { + return 0U; + } + for (const auto ch : domain) { + if (!put(static_cast(static_cast(ch)))) { + return 0U; + } + } + if (!put(0x1AU) || !put(static_cast(item.size()))) { + return 0U; + } + for (const auto ch : item) { + if (!put(static_cast(static_cast(ch)))) { + return 0U; + } + } + if (!put(0xA0U) || !put(static_cast(data.size()))) { + return 0U; + } + for (const auto byte : data) { + if (!put(byte)) { + return 0U; + } + } + return offset == total ? offset : 0U; +} + +[[nodiscard]] bool dispatch_write( + const mms::MmsStaticApplicationDispatcher& dispatcher, + const std::string_view item, + const std::span data, + const mms::MmsStaticRequestAccessContext& client, + const bool expected_success, + const std::uint32_t expected_failure, + std::uint8_t& invoke) noexcept { + std::array request{}; + std::array response{}; + std::array workspace{}; + const auto bytes = build_write_request( + invoke++, "LD0", item, data, request); + if (bytes == 0U) { + return false; + } + const auto dispatched = dispatcher.dispatch( + std::span{request}.first(bytes), + response, + workspace, + client); + if (!dispatched.success()) { + return false; + } + mms::MmsWriteResponseView decoded; + mms::MmsWriteAccessResultView result; + if (!mms::MmsServiceSpanCodec::try_decode_write_response( + std::span{response}.first(dispatched.bytes_written), + decoded) || + decoded.result_count != 1U || !decoded.try_result(0U, result)) { + return false; + } + return expected_success + ? result.success + : !result.success && result.failure_code == expected_failure; +} + +[[nodiscard]] std::array octet_data( + const std::span value) noexcept { + std::array encoded{}; + if (value.size() != 8U) { + return encoded; + } + encoded[0] = 0x89U; + encoded[1] = 0x08U; + std::copy(value.begin(), value.end(), encoded.begin() + 2); + return encoded; +} + +} // namespace + +int main() { + bool value = true; + const std::array base_objects{ + mms::MmsStaticObjectEntry{ + "LD0", "X1", kBooleanType, read_boolean, &value, false}}; + const mms::MmsStaticObjectTable base_table{base_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", + "B1", + "LD0/LLN0$BR$B1", + "LD0", + "LLN0$Events", + 1U, + {0x7FU, 0x80U}, + 0U, + 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, base_table, data_set_table}; + if (!reports.initialize()) { + return 1; + } + mms::MmsStaticBrcbControl control{reports}; + + std::array object_storage{}; + std::array context_storage{}; + std::array name_storage{}; + std::uint64_t now = 100U; + mms::MmsStaticBrcbObjectBank bank{ + definition, + reports, + control, + base_objects, + object_storage, + context_storage, + name_storage, + read_now, + &now}; + if (!bank.initialize() || bank.object_count() != 9U || !bank.table().valid()) { + return 2; + } + + const mms::MmsStaticApplicationDispatcher dispatcher{bank.table(), data_set_table}; + const std::array owner_a{0xAAU, 0x01U}; + const std::array owner_b{0xBBU, 0x01U}; + const auto a = access(101U, owner_a); + const auto b = access(201U, owner_b); + std::uint8_t invoke = 1U; + + // Enabling an unreserved BRCB implicitly claims it for this association. + if (!dispatch_write(dispatcher, "B1$RptEna", kTrue, a, true, 0U, invoke) || + !reports.enabled()) { + return 3; + } + auto state = control.state(now); + if (!state.reserved || !state.owner_connected || + state.association_id != 101U || state.owner.size() != owner_a.size() || + !std::equal(state.owner.begin(), state.owner.end(), owner_a.begin())) { + return 4; + } + + // A second client cannot mutate the operational BRCB while A owns it. + if (!dispatch_write(dispatcher, "B1$RptEna", kTrue, b, false, 3U, invoke) || + !dispatch_write(dispatcher, "B1$PurgeBuf", kTrue, b, false, 3U, invoke)) { + return 5; + } + + // Capture two reports while A owns and enables the BRCB. + std::array encode_buffer{}; + std::array capture_workspace{}; + const std::array report_time{0U,0U,0U,1U,0U,0U,0U,0U}; + if (reports.notify(0U, mms::MmsStaticBrcbEventReason::data_change, now) != + mms::MmsStaticBrcbStatus::ok) { + return 6; + } + mms::MmsStaticBrcbCapturePlan plan; + if (!reports.next_due(now, plan)) { + return 7; + } + const auto first = reports.capture(plan, report_time, encode_buffer, capture_workspace); + if (!first.success()) { + return 8; + } + + value = false; + now = 200U; + if (reports.notify(0U, mms::MmsStaticBrcbEventReason::data_change, now) != + mms::MmsStaticBrcbStatus::ok || + !reports.next_due(now, plan)) { + return 9; + } + const auto second = reports.capture(plan, report_time, encode_buffer, capture_workspace); + if (!second.success() || reports.retained_size() != 2U || + reports.latest_entry_id() != second.entry_id) { + return 10; + } + + if (!dispatch_write(dispatcher, "B1$RptEna", kFalse, a, true, 0U, invoke) || + reports.enabled()) { + return 11; + } + + // Disabling reporting does not release Owner. ResvTms can be set afterward. + if (!dispatch_write(dispatcher, "B1$ResvTms", kFiveSeconds, a, true, 0U, invoke)) { + return 12; + } + state = control.state(now); + if (!state.reserved || state.resv_tms_seconds != 5U || + state.owner.size() != owner_a.size()) { + return 13; + } + + // EntryID write resumes AFTER the requested retained entry. + const auto first_entry_data = octet_data(first.entry_id); + if (!dispatch_write( + dispatcher, "B1$EntryID", first_entry_data, a, true, 0U, invoke) || + reports.queue_size() != 1U) { + return 14; + } + mms::MmsStaticBrcbEntryView front; + if (!reports.front(front) || front.entry_id.size() != second.entry_id.size() || + !std::equal(front.entry_id.begin(), front.entry_id.end(), second.entry_id.begin())) { + return 15; + } + + // All-zero EntryID rewinds to oldest retained; unavailable EntryID is + // object-value-invalid (MMS data-access failure 11). + const std::array zero_entry{}; + const auto zero_entry_data = octet_data(zero_entry); + if (!dispatch_write( + dispatcher, "B1$EntryID", zero_entry_data, a, true, 0U, invoke) || + reports.queue_size() != 2U) { + return 16; + } + const std::array missing_entry{0U,0U,0U,0U,0U,0U,0U,0x7FU}; + const auto missing_entry_data = octet_data(missing_entry); + if (!dispatch_write( + dispatcher, "B1$EntryID", missing_entry_data, a, false, 11U, invoke)) { + return 17; + } + + if (!dispatch_write(dispatcher, "B1$PurgeBuf", kTrue, a, true, 0U, invoke) || + reports.retained_size() != 0U || reports.queue_size() != 0U || + reports.latest_entry_id() != zero_entry) { + return 18; + } + + // Releasing the reservation is only allowed while reporting is disabled. + if (!dispatch_write(dispatcher, "B1$ResvTms", kZeroSeconds, a, true, 0U, invoke)) { + return 19; + } + state = control.state(now); + if (state.reserved || !state.owner.empty()) { + return 20; + } + + // Read callbacks expose command/state values with the expected MMS Data tags. + std::array read_buffer{}; + const auto& entry_object = object_storage[1U + 5U]; + const auto entry_read = entry_object.read(entry_object.context, read_buffer); + if (!entry_read.success() || entry_read.bytes_written != 10U || + read_buffer[0] != 0x89U || read_buffer[1] != 0x08U || + !std::all_of(read_buffer.begin() + 2, read_buffer.begin() + 10, + [](const std::uint8_t byte) { return byte == 0U; })) { + return 21; + } + const auto& purge_object = object_storage[1U + 4U]; + const auto purge_read = purge_object.read(purge_object.context, read_buffer); + if (!purge_read.success() || purge_read.bytes_written != 3U || + read_buffer[0] != 0x83U || read_buffer[2] != 0x00U) { + return 22; + } + + return 0; +} From c4829155a8059a73ebe43b12d67a4bb34e807b72 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 10 Aug 2026 04:51:40 +0700 Subject: [PATCH 5/8] Gate encoded BRCB MMS controls in hard profile --- embedded/brcb_hard_profile/CMakeLists.txt | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/embedded/brcb_hard_profile/CMakeLists.txt b/embedded/brcb_hard_profile/CMakeLists.txt index 81d5d1b8e..d7c286808 100644 --- a/embedded/brcb_hard_profile/CMakeLists.txt +++ b/embedded/brcb_hard_profile/CMakeLists.txt @@ -54,6 +54,23 @@ add_executable(ariec61850_hard_mms_brcb_control_smoke ${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_objects_smoke + ${CMAKE_CURRENT_LIST_DIR}/../mms_brcb_objects_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/write_span.cpp + ${ARIEC61850_ROOT}/src/mms/data_set_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/static_dispatcher.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_ROOT}/src/mms/static_brcb_objects.cpp) +ariec61850_configure_brcb_hard_target(ariec61850_hard_mms_brcb_objects_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 @@ -96,6 +113,9 @@ add_test(NAME ariec61850_hard_mms_brcb_smoke COMMAND ariec61850_hard_mms_brcb_sm add_test( NAME ariec61850_hard_mms_brcb_control_smoke COMMAND ariec61850_hard_mms_brcb_control_smoke) +add_test( + NAME ariec61850_hard_mms_brcb_objects_smoke + COMMAND ariec61850_hard_mms_brcb_objects_smoke) add_test( NAME ariec61850_hard_mms_brcb_connection_smoke COMMAND ariec61850_hard_mms_brcb_connection_smoke) @@ -105,6 +125,7 @@ add_test( 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: MMS RptEna/EntryID/PurgeBuf/ResvTms/Owner=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") From 4e400296ffab412f0c29d42f8b5364d798e8d3c7 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 10 Aug 2026 04:51:58 +0700 Subject: [PATCH 6/8] Include BRCB MMS object smoke in hard-profile evidence --- .github/workflows/brcb-hard-profile-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/brcb-hard-profile-ci.yml b/.github/workflows/brcb-hard-profile-ci.yml index 90f2f1103..9abeb1fd2 100644 --- a/.github/workflows/brcb-hard-profile-ci.yml +++ b/.github/workflows/brcb-hard-profile-ci.yml @@ -51,6 +51,7 @@ jobs: 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_objects_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 @@ -69,6 +70,7 @@ jobs: 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_objects_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 From 9ce0905355dcebdad004530ff0b32c4a3cdd9d87 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 10 Aug 2026 04:54:45 +0700 Subject: [PATCH 7/8] Fix BRCB MMS smoke write builder lambda --- embedded/mms_brcb_objects_hard_profile_smoke.cpp | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/embedded/mms_brcb_objects_hard_profile_smoke.cpp b/embedded/mms_brcb_objects_hard_profile_smoke.cpp index 3c5ea6a6e..a996fd1ed 100644 --- a/embedded/mms_brcb_objects_hard_profile_smoke.cpp +++ b/embedded/mms_brcb_objects_hard_profile_smoke.cpp @@ -67,7 +67,7 @@ constexpr std::array kZeroSeconds{0x85U, 0x01U, 0x00U}; const auto domain_specific_content = 4U + domain.size() + item.size(); std::size_t offset = 0U; - const auto put = [&](const std::uint8_t byte) mutable noexcept -> bool { + const auto put = [&](const std::uint8_t byte) noexcept -> bool { if (offset >= destination.size()) { return false; } @@ -221,7 +221,6 @@ int main() { const auto b = access(201U, owner_b); std::uint8_t invoke = 1U; - // Enabling an unreserved BRCB implicitly claims it for this association. if (!dispatch_write(dispatcher, "B1$RptEna", kTrue, a, true, 0U, invoke) || !reports.enabled()) { return 3; @@ -233,13 +232,11 @@ int main() { return 4; } - // A second client cannot mutate the operational BRCB while A owns it. if (!dispatch_write(dispatcher, "B1$RptEna", kTrue, b, false, 3U, invoke) || !dispatch_write(dispatcher, "B1$PurgeBuf", kTrue, b, false, 3U, invoke)) { return 5; } - // Capture two reports while A owns and enables the BRCB. std::array encode_buffer{}; std::array capture_workspace{}; const std::array report_time{0U,0U,0U,1U,0U,0U,0U,0U}; @@ -274,7 +271,6 @@ int main() { return 11; } - // Disabling reporting does not release Owner. ResvTms can be set afterward. if (!dispatch_write(dispatcher, "B1$ResvTms", kFiveSeconds, a, true, 0U, invoke)) { return 12; } @@ -284,7 +280,6 @@ int main() { return 13; } - // EntryID write resumes AFTER the requested retained entry. const auto first_entry_data = octet_data(first.entry_id); if (!dispatch_write( dispatcher, "B1$EntryID", first_entry_data, a, true, 0U, invoke) || @@ -297,8 +292,6 @@ int main() { return 15; } - // All-zero EntryID rewinds to oldest retained; unavailable EntryID is - // object-value-invalid (MMS data-access failure 11). const std::array zero_entry{}; const auto zero_entry_data = octet_data(zero_entry); if (!dispatch_write( @@ -319,7 +312,6 @@ int main() { return 18; } - // Releasing the reservation is only allowed while reporting is disabled. if (!dispatch_write(dispatcher, "B1$ResvTms", kZeroSeconds, a, true, 0U, invoke)) { return 19; } @@ -328,7 +320,6 @@ int main() { return 20; } - // Read callbacks expose command/state values with the expected MMS Data tags. std::array read_buffer{}; const auto& entry_object = object_storage[1U + 5U]; const auto entry_read = entry_object.read(entry_object.context, read_buffer); From 9e11ff380d79c5c246d364812083a24a7e4c01ff Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 10 Aug 2026 04:56:13 +0700 Subject: [PATCH 8/8] Use valid six-byte BRCB report time in MMS smoke --- embedded/mms_brcb_objects_hard_profile_smoke.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embedded/mms_brcb_objects_hard_profile_smoke.cpp b/embedded/mms_brcb_objects_hard_profile_smoke.cpp index a996fd1ed..e1349f9be 100644 --- a/embedded/mms_brcb_objects_hard_profile_smoke.cpp +++ b/embedded/mms_brcb_objects_hard_profile_smoke.cpp @@ -239,7 +239,7 @@ int main() { std::array encode_buffer{}; std::array capture_workspace{}; - const std::array report_time{0U,0U,0U,1U,0U,0U,0U,0U}; + const std::array report_time{0U,0U,0x12U,0x34U,0U,0x01U}; if (reports.notify(0U, mms::MmsStaticBrcbEventReason::data_change, now) != mms::MmsStaticBrcbStatus::ok) { return 6;