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
54 changes: 39 additions & 15 deletions embedded/mms_brcb_hard_profile_smoke.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) !=
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}

Expand All @@ -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 ||
Expand All @@ -238,34 +242,54 @@ int main() {
!report.try_item(6U, item)) {
return 16;
}
const std::array<std::uint8_t, 8U> third_id{0U,0U,0U,0U,0U,0U,0U,3U};
const std::array<std::uint8_t, 8U> 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<std::uint8_t, 8U> 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<std::uint64_t>(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;
}
}
52 changes: 47 additions & 5 deletions include/ariec61850/mms/static_brcb_runtime.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -136,34 +138,74 @@ class MmsStaticBrcbRuntime final {
std::span<std::uint8_t> encode_buffer,
std::span<std::uint8_t> 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<const std::uint8_t> 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<const std::uint8_t> 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<const std::uint8_t> 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();
}
[[nodiscard]] constexpr std::uint64_t dropped_reports() const noexcept {
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<const std::uint8_t> entry_id,
std::size_t& logical_index) const noexcept;
void clear_retained_slots() noexcept;

const MmsStaticBrcbDefinition* definition_{};
MmsStaticBrcbPendingState* pending_{};
std::span<MmsStaticBrcbSlot> slots_{};
const MmsStaticObjectTable* objects_{};
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
Loading
Loading