diff --git a/embedded/mms_brcb_hard_profile_smoke.cpp b/embedded/mms_brcb_hard_profile_smoke.cpp index 0dd71f886..60d4d066a 100644 --- a/embedded/mms_brcb_hard_profile_smoke.cpp +++ b/embedded/mms_brcb_hard_profile_smoke.cpp @@ -129,7 +129,8 @@ int main() { mms::MmsStaticBrcbRuntime runtime{ definition, pending, slots, object_table, data_set_table}; if (!runtime.initialize() || !runtime.valid() || runtime.enabled() || - runtime.queue_size() != 0U || runtime.queue_capacity() != 2U) { + runtime.queue_size() != 0U || runtime.retained_size() != 0U || + runtime.queue_capacity() != 2U || runtime.replay_gap()) { return 1; } if (runtime.notify(0U, mms::MmsStaticBrcbEventReason::data_change, 0U) != @@ -161,7 +162,7 @@ int main() { } capture = runtime.capture(plan, kReportTime, staging, workspace); - if (!capture.success() || runtime.queue_size() != 1U || + if (!capture.success() || runtime.queue_size() != 1U || runtime.retained_size() != 1U || capture.included_member_count != 2U || !expected_entry(capture.entry_id, 1U)) { return 6; } @@ -191,7 +192,7 @@ int main() { return 9; } capture = runtime.capture(plan, kReportTime, staging, workspace); - if (!capture.success() || runtime.queue_size() != 2U || + if (!capture.success() || runtime.queue_size() != 2U || runtime.retained_size() != 2U || !expected_entry(capture.entry_id, 2U)) { return 10; } @@ -206,14 +207,16 @@ int main() { mms::MmsInformationReportSpanCodec::entry_id_bytes>{ 0U, 0U, 0U, 0U, 0U, 0U, 0U, 1U}; if (runtime.commit_delivery(first_entry_id) != mms::MmsStaticBrcbStatus::ok || + runtime.queue_size() != 1U || runtime.retained_size() != 2U || runtime.capture(stale_plan, kReportTime, staging, workspace).status != mms::MmsStaticBrcbStatus::stale_plan || !runtime.next_due(320U, plan) || plan.buffer_overflow) { return 12; } capture = runtime.capture(plan, kReportTime, staging, workspace); - if (!capture.success() || runtime.queue_size() != 2U || - !expected_entry(capture.entry_id, 3U) || runtime.dropped_reports() != 0U) { + if (!capture.success() || runtime.queue_size() != 2U || runtime.retained_size() != 2U || + !expected_entry(capture.entry_id, 3U) || runtime.dropped_reports() != 0U || + runtime.replay_gap()) { return 13; } @@ -223,12 +226,13 @@ int main() { return 14; } capture = runtime.capture(plan, kReportTime, staging, workspace); - if (!capture.success() || runtime.queue_size() != 2U || - !expected_entry(capture.entry_id, 4U) || runtime.dropped_reports() != 1U) { + if (!capture.success() || runtime.queue_size() != 2U || runtime.retained_size() != 2U || + !expected_entry(capture.entry_id, 4U) || runtime.dropped_reports() != 1U || + !runtime.replay_gap()) { return 15; } - // Entry 2 was the oldest and must have been dropped. Entry 3 is now front. + // Entry 2 was the oldest undelivered report and was lost. Entry 3 is now front. if (!runtime.front(entry) || entry.entry_id.size() != 8U || entry.entry_id[7] != 3U || runtime.commit_delivery(entry.entry_id) != mms::MmsStaticBrcbStatus::ok || !runtime.front(entry) || entry.entry_id[7] != 4U || !entry.buffer_overflow || @@ -238,34 +242,54 @@ int main() { !report.try_item(6U, item)) { return 16; } + const std::array third_id{0U,0U,0U,0U,0U,0U,0U,3U}; const std::array fourth_id{0U,0U,0U,0U,0U,0U,0U,4U}; if (!decode_octet_string(item, fourth_id) || runtime.commit_delivery(fourth_id) != mms::MmsStaticBrcbStatus::ok || - runtime.queue_size() != 0U) { + runtime.queue_size() != 0U || runtime.retained_size() != 2U) { return 17; } - // Repeated capture/delivery proves bounded steady-state behavior and EntryID progression. + // Delivered entries remain replayable until buffer eviction or PurgeBuf. + if (runtime.replay_from(fourth_id) != mms::MmsStaticBrcbStatus::ok || + runtime.queue_size() != 1U || !runtime.front(entry) || entry.entry_id[7U] != 4U || + runtime.commit_delivery(fourth_id) != mms::MmsStaticBrcbStatus::ok || + runtime.queue_size() != 0U || + runtime.rewind_to_oldest() != mms::MmsStaticBrcbStatus::ok || + runtime.queue_size() != 2U || !runtime.front(entry) || entry.entry_id[7U] != 3U || + runtime.resume_after(third_id) != mms::MmsStaticBrcbStatus::ok || + runtime.queue_size() != 1U || !runtime.front(entry) || entry.entry_id[7U] != 4U) { + return 18; + } + const std::array missing_id{0U,0U,0U,0U,0U,0U,0U,2U}; + if (runtime.resume_after(missing_id) != mms::MmsStaticBrcbStatus::entry_not_found || + runtime.purge_buffer() != mms::MmsStaticBrcbStatus::ok || + runtime.queue_size() != 0U || runtime.retained_size() != 0U || runtime.replay_gap() || + runtime.front(entry)) { + return 19; + } + + // Repeated capture/delivery proves bounded retained-history behavior and EntryID progression. for (std::uint32_t iteration = 0U; iteration < 10'000U; ++iteration) { const auto now = static_cast(1'000U + (iteration * 25U)); if (runtime.notify(0U, mms::MmsStaticBrcbEventReason::data_update, now) != mms::MmsStaticBrcbStatus::ok || !runtime.next_due(now + 20U, plan)) { - return 18; + return 20; } capture = runtime.capture(plan, kReportTime, staging, workspace); if (!capture.success() || !runtime.front(entry) || runtime.commit_delivery(entry.entry_id) != mms::MmsStaticBrcbStatus::ok || - runtime.queue_size() != 0U) { - return 19; + runtime.queue_size() != 0U || runtime.retained_size() > runtime.queue_capacity()) { + return 21; } } if (runtime.set_enabled(false) != mms::MmsStaticBrcbStatus::ok || runtime.enabled() || runtime.notify(0U, mms::MmsStaticBrcbEventReason::data_change, 999'999U) != mms::MmsStaticBrcbStatus::temporarily_unavailable) { - return 20; + return 22; } return 0; -} +} \ No newline at end of file diff --git a/include/ariec61850/mms/static_brcb_runtime.hpp b/include/ariec61850/mms/static_brcb_runtime.hpp index 72e605d4b..93e17317d 100644 --- a/include/ariec61850/mms/static_brcb_runtime.hpp +++ b/include/ariec61850/mms/static_brcb_runtime.hpp @@ -101,9 +101,11 @@ struct MmsStaticBrcbEntryView final { bool buffer_overflow{}; }; -// First bounded BRCB profile: one runtime instance represents one BRCB. It -// buffers event snapshots in caller-owned RAM across connection resets. Queue -// persistence is storage-agnostic and mediated by MmsStaticBrcbStateCodec. +// 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( @@ -136,11 +138,39 @@ 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; - [[nodiscard]] constexpr std::size_t queue_size() const noexcept { return count_; } + // 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. + [[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 { return slots_.size(); } @@ -148,9 +178,19 @@ 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: friend class MmsStaticBrcbStateCodec; + [[nodiscard]] bool find_entry( + std::span entry_id, + std::size_t& logical_index) const noexcept; + void clear_retained_slots() noexcept; + const MmsStaticBrcbDefinition* definition_{}; MmsStaticBrcbPendingState* pending_{}; std::span slots_{}; @@ -158,12 +198,14 @@ class MmsStaticBrcbRuntime final { const MmsStaticDataSetTable* data_sets_{}; std::size_t head_{}; std::size_t count_{}; + std::size_t delivery_offset_{}; std::uint64_t next_entry_number_{1U}; std::uint64_t dropped_reports_{}; std::uint32_t queue_revision_{1U}; std::uint8_t sequence_number_{}; + bool replay_gap_{}; bool enabled_{}; bool initialized_{}; }; -} // namespace ar::iec61850::mms +} // namespace ar::iec61850::mms \ No newline at end of file diff --git a/src/mms/static_brcb_runtime.cpp b/src/mms/static_brcb_runtime.cpp index dff27bc3f..509b5beab 100644 --- a/src/mms/static_brcb_runtime.cpp +++ b/src/mms/static_brcb_runtime.cpp @@ -188,17 +188,15 @@ bool MmsStaticBrcbRuntime::initialize() noexcept { *pending_ = {}; pending_->revision = 1U; - for (auto& slot : slots_) { - const auto storage = slot.storage; - slot = {}; - slot.storage = storage; - } + clear_retained_slots(); head_ = 0U; count_ = 0U; + delivery_offset_ = 0U; next_entry_number_ = 1U; dropped_reports_ = 0U; queue_revision_ = 1U; sequence_number_ = 0U; + replay_gap_ = false; enabled_ = false; initialized_ = true; return true; @@ -273,7 +271,7 @@ bool MmsStaticBrcbRuntime::next_due( plan.queue_revision = queue_revision_; plan.entry_number = next_entry_number_; plan.sequence_number = next_sequence_number(sequence_number_); - plan.buffer_overflow = count_ == slots_.size(); + plan.buffer_overflow = count_ == slots_.size() && delivery_offset_ == 0U; return true; } @@ -284,17 +282,19 @@ MmsStaticBrcbCaptureResult MmsStaticBrcbRuntime::capture( const std::span workspace) noexcept { MmsStaticBrcbCaptureResult result; if (!initialized_ || definition_ == nullptr || pending_ == nullptr || - objects_ == nullptr || data_sets_ == nullptr || slots_.empty()) { + objects_ == nullptr || data_sets_ == nullptr || slots_.empty() || + delivery_offset_ > count_) { result.status = MmsStaticBrcbStatus::invalid_runtime; return result; } const bool full = count_ == slots_.size(); + const bool overflow = full && delivery_offset_ == 0U; if (!enabled_ || !pending_->pending || pending_->pending_member_count == 0U || plan.pending_revision != pending_->revision || plan.queue_revision != queue_revision_ || plan.entry_number != next_entry_number_ || plan.sequence_number != next_sequence_number(sequence_number_) || - plan.buffer_overflow != full) { + plan.buffer_overflow != overflow) { result.status = MmsStaticBrcbStatus::stale_plan; return result; } @@ -416,8 +416,17 @@ MmsStaticBrcbCaptureResult MmsStaticBrcbRuntime::capture( slot.occupied = true; if (full) { + // The physical oldest slot is being recycled. If the delivery cursor is + // already past it, retain the same logical next-to-deliver entry by + // shifting the cursor with the head. Otherwise an undelivered report is + // irrecoverably lost and the recovery gap must remain visible. + if (delivery_offset_ == 0U) { + ++dropped_reports_; + replay_gap_ = true; + } else { + --delivery_offset_; + } head_ = (head_ + 1U) % slots_.size(); - ++dropped_reports_; } else { ++count_; } @@ -437,10 +446,12 @@ MmsStaticBrcbCaptureResult MmsStaticBrcbRuntime::capture( bool MmsStaticBrcbRuntime::front(MmsStaticBrcbEntryView& entry) const noexcept { entry = {}; - if (!initialized_ || count_ == 0U || head_ >= slots_.size()) { + if (!initialized_ || count_ == 0U || delivery_offset_ >= count_ || + head_ >= slots_.size()) { return false; } - const auto& slot = slots_[head_]; + const auto physical = (head_ + delivery_offset_) % slots_.size(); + const auto& slot = slots_[physical]; if (!slot.occupied || slot.bytes == 0U || slot.bytes > slot.storage.size()) { return false; } @@ -453,24 +464,98 @@ bool MmsStaticBrcbRuntime::front(MmsStaticBrcbEntryView& entry) const noexcept { MmsStaticBrcbStatus MmsStaticBrcbRuntime::commit_delivery( const std::span expected_entry_id) noexcept { - if (!initialized_ || slots_.empty()) { + if (!initialized_ || slots_.empty() || delivery_offset_ > count_) { return MmsStaticBrcbStatus::invalid_runtime; } - if (count_ == 0U || head_ >= slots_.size()) { + if (delivery_offset_ >= count_ || head_ >= slots_.size()) { return MmsStaticBrcbStatus::entry_not_found; } - auto& slot = slots_[head_]; + const auto physical = (head_ + delivery_offset_) % slots_.size(); + const auto& slot = slots_[physical]; if (!slot.occupied || !entry_id_equal(expected_entry_id, slot.entry_id)) { return MmsStaticBrcbStatus::entry_not_found; } - const auto storage = slot.storage; - slot = {}; - slot.storage = storage; - head_ = (head_ + 1U) % slots_.size(); - --count_; + ++delivery_offset_; + bump_revision(queue_revision_); + return MmsStaticBrcbStatus::ok; +} + +bool MmsStaticBrcbRuntime::find_entry( + const std::span entry_id, + std::size_t& logical_index) const noexcept { + logical_index = 0U; + if (!initialized_ || entry_id.size() != MmsInformationReportSpanCodec::entry_id_bytes || + count_ > slots_.size() || head_ >= slots_.size()) { + return false; + } + for (std::size_t logical = 0U; logical < count_; ++logical) { + const auto physical = (head_ + logical) % slots_.size(); + const auto& slot = slots_[physical]; + if (slot.occupied && entry_id_equal(entry_id, slot.entry_id)) { + logical_index = logical; + return true; + } + } + return false; +} + +MmsStaticBrcbStatus MmsStaticBrcbRuntime::replay_from( + const std::span entry_id) noexcept { + if (!initialized_) { + return MmsStaticBrcbStatus::invalid_runtime; + } + std::size_t logical{}; + if (!find_entry(entry_id, logical)) { + return MmsStaticBrcbStatus::entry_not_found; + } + delivery_offset_ = logical; + bump_revision(queue_revision_); + return MmsStaticBrcbStatus::ok; +} + +MmsStaticBrcbStatus MmsStaticBrcbRuntime::resume_after( + const std::span entry_id) noexcept { + if (!initialized_) { + return MmsStaticBrcbStatus::invalid_runtime; + } + std::size_t logical{}; + if (!find_entry(entry_id, logical)) { + return MmsStaticBrcbStatus::entry_not_found; + } + delivery_offset_ = logical + 1U; + bump_revision(queue_revision_); + return MmsStaticBrcbStatus::ok; +} + +MmsStaticBrcbStatus MmsStaticBrcbRuntime::rewind_to_oldest() noexcept { + if (!initialized_ || count_ > slots_.size()) { + return MmsStaticBrcbStatus::invalid_runtime; + } + delivery_offset_ = 0U; + bump_revision(queue_revision_); + return MmsStaticBrcbStatus::ok; +} + +void MmsStaticBrcbRuntime::clear_retained_slots() noexcept { + for (auto& slot : slots_) { + const auto storage = slot.storage; + slot = {}; + slot.storage = storage; + } +} + +MmsStaticBrcbStatus MmsStaticBrcbRuntime::purge_buffer() noexcept { + if (!initialized_ || slots_.empty()) { + return MmsStaticBrcbStatus::invalid_runtime; + } + clear_retained_slots(); + head_ = 0U; + count_ = 0U; + delivery_offset_ = 0U; + replay_gap_ = false; bump_revision(queue_revision_); return MmsStaticBrcbStatus::ok; } -} // namespace ar::iec61850::mms +} // namespace ar::iec61850::mms \ No newline at end of file diff --git a/src/mms/static_brcb_state_codec.cpp b/src/mms/static_brcb_state_codec.cpp index 47ec8666d..b30c39935 100644 --- a/src/mms/static_brcb_state_codec.cpp +++ b/src/mms/static_brcb_state_codec.cpp @@ -154,14 +154,25 @@ MmsStaticBrcbStateResult MmsStaticBrcbStateCodec::encode( if (!runtime.initialized_ || runtime.definition_ == nullptr || runtime.pending_ == nullptr || runtime.slots_.empty() || runtime.count_ > runtime.slots_.size() || - runtime.count_ > std::numeric_limits::max()) { + runtime.delivery_offset_ > runtime.count_) { + result.status = MmsStaticBrcbStateStatus::invalid_runtime; + return result; + } + + // Checkpoint format v1 intentionally persists only reports that are still + // selected for delivery. Delivered retained history is a volatile R1 replay + // window; persisting its cursor/history requires an explicit future format + // revision so reboot behavior cannot silently change. + const auto persisted_count = runtime.queue_size(); + if (persisted_count > std::numeric_limits::max()) { result.status = MmsStaticBrcbStateStatus::invalid_runtime; return result; } std::size_t required = kHeaderBytes; - for (std::size_t logical = 0U; logical < runtime.count_; ++logical) { - const auto physical = (runtime.head_ + logical) % runtime.slots_.size(); + for (std::size_t logical = 0U; logical < persisted_count; ++logical) { + const auto physical = + (runtime.head_ + runtime.delivery_offset_ + logical) % runtime.slots_.size(); const auto& slot = runtime.slots_[physical]; if (!slot.occupied || slot.bytes == 0U || slot.bytes > slot.storage.size() || slot.bytes > std::numeric_limits::max() || @@ -183,14 +194,15 @@ MmsStaticBrcbStateResult MmsStaticBrcbStateCodec::encode( write_u16(output, 8U, format_version); write_u16(output, 10U, static_cast(kHeaderBytes)); write_u64(output, 12U, definition_fingerprint(*runtime.definition_)); - write_u32(output, 20U, static_cast(runtime.count_)); + write_u32(output, 20U, static_cast(persisted_count)); write_u64(output, 24U, runtime.next_entry_number_); write_u64(output, 32U, runtime.dropped_reports_); output[40U] = runtime.sequence_number_; std::size_t offset = kHeaderBytes; - for (std::size_t logical = 0U; logical < runtime.count_; ++logical) { - const auto physical = (runtime.head_ + logical) % runtime.slots_.size(); + for (std::size_t logical = 0U; logical < persisted_count; ++logical) { + const auto physical = + (runtime.head_ + runtime.delivery_offset_ + logical) % runtime.slots_.size(); const auto& slot = runtime.slots_[physical]; write_u32(output, offset, static_cast(slot.bytes)); std::copy( @@ -319,9 +331,11 @@ MmsStaticBrcbStateResult MmsStaticBrcbStateCodec::restore( runtime.head_ = 0U; runtime.count_ = count; + runtime.delivery_offset_ = 0U; runtime.next_entry_number_ = restored_next_entry; runtime.dropped_reports_ = restored_dropped; runtime.sequence_number_ = restored_sequence; + runtime.replay_gap_ = false; runtime.queue_revision_ = runtime.queue_revision_ == std::numeric_limits::max() ? 1U @@ -335,4 +349,4 @@ MmsStaticBrcbStateResult MmsStaticBrcbStateCodec::restore( return result; } -} // namespace ar::iec61850::mms +} // namespace ar::iec61850::mms \ No newline at end of file