Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ add_library(ariec61850_core
src/mms/tcp_transport.cpp
src/mms/live_discovery.cpp
src/mms/rcb_selection.cpp
src/mms/rcb_failover.cpp
src/sampled_values/pdu_codec.cpp
src/sampled_values/frame_codec.cpp
src/sampled_values/quality.cpp
Expand Down Expand Up @@ -216,6 +217,13 @@ if(ARIEC61850_BUILD_TESTS)
ariec61850_apply_sanitizers(ariec61850_dynamic_data_set_tests)
add_test(NAME ariec61850_dynamic_data_set_tests COMMAND ariec61850_dynamic_data_set_tests)

add_executable(ariec61850_rcb_failover_tests tests/test_rcb_failover.cpp)
target_link_libraries(ariec61850_rcb_failover_tests PRIVATE ARIEC61850::core)
target_compile_features(ariec61850_rcb_failover_tests PRIVATE cxx_std_20)
ariec61850_apply_warnings(ariec61850_rcb_failover_tests)
ariec61850_apply_sanitizers(ariec61850_rcb_failover_tests)
add_test(NAME ariec61850_rcb_failover_tests COMMAND ariec61850_rcb_failover_tests)

add_executable(ariec61850_reporting_tests tests/test_reporting.cpp)
target_link_libraries(ariec61850_reporting_tests PRIVATE ARIEC61850::core)
target_compile_features(ariec61850_reporting_tests PRIVATE cxx_std_20)
Expand Down
10 changes: 9 additions & 1 deletion REPORTING_PROFILE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ optionally reserves an URCB, configures only explicitly requested attributes, en
optionally issues GI, receives InformationReports, and cleans up only state touched by the
runtime. Lost-association cleanup is recorded rather than assumed.

Before an armed Dynamic RCB lifecycle, the smart pre-claim failover layer runs
bounded repeated Read probes. A candidate that becomes enabled, reserved, or
flaps between probes is excluded from the current command and the pool selector
may rank another candidate. Once any mutation is attempted, automatic switching
is disabled; an uncertain write must be cleaned up and diagnosed on the original
RCB instead of risking a second remote mutation.

## Phase 4C.1 live-model integration

Read-only discovery includes DataSet directory and RCB state evidence in `live-ied-model-v1`.
Expand All @@ -27,5 +34,6 @@ mode. Runtime report subscription acceptance remains separate from read-only mod

- BRCB EntryID resume and replay;
- purge policy and buffer-overflow recovery;
- dynamic DataSet lifecycle;
- live proof where a contended preferred RCB is skipped for a second candidate;
- reconnect and automatic resubscribe policy;
- long-duration multi-vendor report interoperability.
44 changes: 31 additions & 13 deletions docs/DYNAMIC_RCB_LIVE_TRIAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,27 @@ This harness is for an isolated IEC 61850 laboratory IED/simulator. It intention

## Safety model

Default execution is read-only. It performs live MMS association/discovery, probes RCB state, ranks only empty dynamic slots, and selects scalar ST/MX members. No mutation service is sent unless the exact arm token is present.
Default execution is read-only. It performs live MMS association/discovery,
ranks only empty dynamic slots, and repeats bounded pre-claim Read probes on the
selected candidate. If that candidate is busy or changes state between probes,
it is excluded from the current command and the next safe candidate is ranked.
No mutation service is sent unless the exact arm token is present.

An armed run performs one bounded lifecycle with no automatic mutation retry:

1. discover and rank empty dynamic RCB candidates;
2. choose one RCB with empty `DatSet`, `RptEna=false`, and no active reservation;
3. create one uniquely named domain-specific Dynamic DataSet;
4. verify exact member count and order with `GetNamedVariableListAttributes`;
5. bind the selected RCB `DatSet`;
6. reserve URCB when required, enable `RptEna`, and request GI when exposed;
7. use bounded confirmed Read cycles to give asynchronous reports an opportunity to enter the association queue;
8. decode/drain queued reports through the normal reporting runtime;
9. disable the RCB and release only reservations acquired by this runtime;
10. set `DatSet` back to empty;
11. delete only the Dynamic DataSet created by this run.
2. repeatedly probe the selected RCB immediately before mutation;
3. skip busy/flapping candidates, with a hard candidate-attempt bound;
4. choose one stable RCB with empty `DatSet`, `RptEna=false`, and no active reservation;
5. create one uniquely named domain-specific Dynamic DataSet;
6. verify exact member count and order with `GetNamedVariableListAttributes`;
7. bind the selected RCB `DatSet`;
8. reserve URCB when required, enable `RptEna`, and request GI when exposed;
9. use bounded confirmed Read cycles to give asynchronous reports an opportunity to enter the association queue;
10. decode/drain queued reports through the normal reporting runtime;
11. disable the RCB and release only reservations acquired by this runtime;
12. set `DatSet` back to empty;
13. delete only the Dynamic DataSet created by this run.

Automatic members are projected from the bounded Logical-Node
`GetVariableAccessAttributes` type trees. The planner walks nested FC/DO/DA
Expand All @@ -32,7 +38,9 @@ Incoming reports accept both `listOfVariable [0]` and the
`variableListName [1]` form used by IEDScout, while still decoding only the
trailing `listOfAccessResult` as report data.

Cleanup traffic is allowed after a failed mutation, but the user action itself is never retried on another RCB automatically.
Cleanup traffic is allowed after a failed mutation, but the user action itself
is never retried on another RCB automatically. Smart failover applies only to
the read-only pre-claim window, where no remote state has been changed.

## Windows build

Expand All @@ -54,7 +62,17 @@ powershell.exe -ExecutionPolicy Bypass -File .\scripts\build_dynamic_rcb_trial.p

Depending on the generator, the executable can also be directly under `build-dynamic-rcb-trial`.

Expected output includes `RCB selection`, one `Selected` candidate, `SMART_DYNAMIC_RCB_PLAN`, and `READ_ONLY_PLAN_OK`.
Expected output includes `RCB selection`, `PRECLAIM_ATTEMPT`, one stable
candidate, `SMART_DYNAMIC_RCB_PLAN`, and `READ_ONLY_PLAN_OK`. When a candidate
is contended, `RCB_FAILOVER_SKIP` records the reason before another candidate is
ranked.

The defaults probe up to four candidates, with three Read samples per candidate
and 250 ms between samples. They can be bounded explicitly:

```powershell
... --max-claim-candidates 4 --preclaim-probes 3 --preclaim-delay-ms 250 --contention-cooldown 60
```

## Armed trial

Expand Down
6 changes: 4 additions & 2 deletions docs/FEATURE_MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,16 @@ Status terms are intentionally conservative:
| Cross-language | Same-IED runtime comparator | Live-proven / mutable | Runtime comparison is informational; C++-only `DataSetBindingStatus` is treated as enrichment rather than a false cross-schema mismatch. |
| DataSet | Name inventory | Live-proven | Dynamic inventory may legitimately change between sessions. |
| DataSet | Member directory | Live-proven/optional | Explicitly skipped with `--no-datasets`; empty member list then produces evidence warning, not structural failure. |
| DataSet | Dynamic create/verify/delete lifecycle | Live-proven / guarded lab | One bounded lifecycle created a domain-specific DataSet, verified exact member order, used it for reporting, and deleted only the runtime-owned DataSet. |
| Reporting | RCB inventory | Live-proven | Current endpoint exposes 286 RCB identities: 8 BRCB and 278 URCB in the C# oracle capture. |
| Reporting | Full RCB read-only probe | Live-proven | Current C++ full-probe snapshot classified all 286 RCBs with 0 NotRead and 0 ReadFailed. |
| Reporting | Bound/Unbound runtime semantics | Live-proven | One current snapshot observed 6 Bound and 280 Unbound. This is mutable runtime state, not a permanent device capability claim. |
| Reporting | Dynamic RCB candidate planner | Live-proven | Read-only ranking selected an empty dynamic BRCB candidate on current endpoint. |
| Reporting | Conservative operational availability | Live-proven | Separates dynamic-slot eligibility from populated/static report availability. |
| Reporting | Read-only contention pre-claim probe | Live-proven | Three repeated probes on `OCR7SR12CTRL/BI6GGIO1.urcbA01` remained free/stable and returned `StableProceed`; no claim/write was performed. |
| Reporting | RCB claim/reservation/enable | Planned / out of Phase 4C | No Write, reservation, `RptEna` or GI is authorized by live discovery. |
| Reporting | InformationReport decode / subscription runtime | Offline-tested / partial live integration | Core implementation exists; full live reporting acceptance is not claimed. |
| Reporting | Bounded smart pre-claim failover | Offline-tested / stable live probe | Immediately before mutation, repeated read-only probes reject busy/flapping candidates, add them to the command-local exclusion set, and rerank the next candidate. A stable live candidate passed three probes; a live contended switch remains pending. Mutation failures never trigger automatic switching. |
| Reporting | RCB claim/reservation/enable | Live-proven / guarded lab | Explicitly armed Dynamic RCB trial bound and enabled one BRCB, requested GI, then disabled and unbound it with cleanup verified. Live discovery itself remains read-only. |
| Reporting | InformationReport decode / subscription runtime | Live-proven / single profile | The guarded trial received and decoded an InformationReport through the persistent subscription runtime; long-duration and multi-vendor acceptance remain pending. |
| GOOSE | PDU / Ethernet encode-decode | Offline-tested | Golden vectors, deterministic tests and fuzz corpus. |
| GOOSE | Publisher/subscriber runtime foundations | Offline-tested | Physical multi-vendor process-bus acceptance pending. |
| Sampled Values | PDU / Ethernet encode-decode | Offline-tested | Golden vectors, deterministic tests and fuzz corpus. |
Expand Down
68 changes: 68 additions & 0 deletions include/ariec61850/mms/rcb_failover.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

#include <cstddef>
#include <string>
#include <string_view>
#include <vector>

namespace ar::iec61850::mms {

struct MmsRcbContentionProbeResult;

enum class MmsRcbPreclaimOutcome {
stable_proceed,
skipped_contended,
attempts_exhausted,
};

[[nodiscard]] std::string_view mms_rcb_preclaim_outcome_name(
MmsRcbPreclaimOutcome value) noexcept;

struct MmsRcbPreclaimAttemptEvidence final {
std::size_t attempt_number{};
std::string rcb_reference;
MmsRcbPreclaimOutcome outcome{MmsRcbPreclaimOutcome::skipped_contended};
bool busy{};
bool flapping{};
int cooldown_seconds{};
std::string reason;
};

struct MmsRcbPreclaimFailoverSnapshot final {
std::size_t maximum_candidate_attempts{};
std::vector<MmsRcbPreclaimAttemptEvidence> attempts;
std::vector<std::string> excluded_rcb_references;
std::string selected_rcb_reference;
bool exhausted{};

[[nodiscard]] bool selected() const noexcept {
return !selected_rcb_reference.empty();
}
};

// Bounded policy state for the read-only pre-claim window. A contended RCB is
// excluded from the current command so the pool selector can rank another
// candidate. This tracker intentionally stops being usable after one candidate
// is accepted; mutation failures are not failover signals.
class MmsRcbPreclaimFailoverTracker final {
public:
explicit MmsRcbPreclaimFailoverTracker(
std::size_t maximum_candidate_attempts = 4U);

[[nodiscard]] bool may_attempt() const noexcept;
[[nodiscard]] const std::vector<std::string>& excluded_rcb_references() const noexcept {
return snapshot_.excluded_rcb_references;
}

[[nodiscard]] MmsRcbPreclaimOutcome observe(
const MmsRcbContentionProbeResult& result);
[[nodiscard]] const MmsRcbPreclaimFailoverSnapshot& snapshot() const noexcept {
return snapshot_;
}

private:
MmsRcbPreclaimFailoverSnapshot snapshot_;
};

} // namespace ar::iec61850::mms
82 changes: 82 additions & 0 deletions src/mms/rcb_failover.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: GPL-3.0-or-later

#include "ariec61850/mms/rcb_failover.hpp"

#include "ariec61850/mms/rcb_contention.hpp"

#include <algorithm>
#include <stdexcept>
#include <utility>

namespace ar::iec61850::mms {

std::string_view mms_rcb_preclaim_outcome_name(
const MmsRcbPreclaimOutcome value) noexcept {
switch (value) {
case MmsRcbPreclaimOutcome::stable_proceed: return "StableProceed";
case MmsRcbPreclaimOutcome::skipped_contended: return "SkippedContended";
case MmsRcbPreclaimOutcome::attempts_exhausted: return "AttemptsExhausted";
}
return "Unknown";
}

MmsRcbPreclaimFailoverTracker::MmsRcbPreclaimFailoverTracker(
const std::size_t maximum_candidate_attempts) {
if (maximum_candidate_attempts == 0U) {
throw std::invalid_argument(
"RCB pre-claim failover requires at least one candidate attempt.");
}
snapshot_.maximum_candidate_attempts = maximum_candidate_attempts;
}

bool MmsRcbPreclaimFailoverTracker::may_attempt() const noexcept {
return !snapshot_.selected() && !snapshot_.exhausted &&
snapshot_.attempts.size() < snapshot_.maximum_candidate_attempts;
}

MmsRcbPreclaimOutcome MmsRcbPreclaimFailoverTracker::observe(
const MmsRcbContentionProbeResult& result) {
if (!may_attempt()) {
throw std::logic_error(
"RCB pre-claim failover is already selected or exhausted.");
}
if (result.rcb_reference.empty()) {
throw std::invalid_argument(
"RCB pre-claim probe result requires a candidate reference.");
}
const auto duplicate = std::find(
snapshot_.excluded_rcb_references.begin(),
snapshot_.excluded_rcb_references.end(),
result.rcb_reference);
if (duplicate != snapshot_.excluded_rcb_references.end()) {
throw std::logic_error(
"RCB pre-claim failover cannot probe an excluded candidate twice.");
}

MmsRcbPreclaimAttemptEvidence attempt;
attempt.attempt_number = snapshot_.attempts.size() + 1U;
attempt.rcb_reference = result.rcb_reference;
attempt.busy = result.is_busy_at_probe;
attempt.flapping = result.is_flapping;
attempt.cooldown_seconds = result.cooldown_seconds;
attempt.reason = result.reason;

if (!result.is_contended) {
attempt.outcome = MmsRcbPreclaimOutcome::stable_proceed;
snapshot_.selected_rcb_reference = result.rcb_reference;
snapshot_.attempts.push_back(std::move(attempt));
return MmsRcbPreclaimOutcome::stable_proceed;
}

snapshot_.excluded_rcb_references.push_back(result.rcb_reference);
const bool exhausted =
snapshot_.attempts.size() + 1U >= snapshot_.maximum_candidate_attempts;
attempt.outcome = exhausted
? MmsRcbPreclaimOutcome::attempts_exhausted
: MmsRcbPreclaimOutcome::skipped_contended;
snapshot_.exhausted = exhausted;
snapshot_.attempts.push_back(std::move(attempt));
return snapshot_.attempts.back().outcome;
}

} // namespace ar::iec61850::mms
Loading
Loading