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
4 changes: 4 additions & 0 deletions apps/ied_simulator/src/MmsClientController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,10 @@ bool MmsClientController::connectToIed() {

mms::MmsLiveDiscoveryOptions options;
options.maximum_variable_type_probes = 8'192U;
// One bounded extra-LN pass, driven only by verified static
// MMS DataSet members; do not retry/sweep an entire IED.
options.use_verified_data_set_type_hints = true;
options.maximum_data_set_type_hint_roots = 48U;
options.maximum_data_set_directories = 4'096U;
options.maximum_report_control_probes = 1'024U;
auto discovery = std::make_shared<mms::MmsLiveDiscoveryResult>(
Expand Down
6 changes: 6 additions & 0 deletions include/ariec61850/mms/live_discovery.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ struct MmsLiveDiscoveryOptions final {
bool probe_variable_types{true};
std::size_t maximum_variable_type_probes{4'096U};

// Opt-in, fail-closed DataSet-backed LN type closure for truncated NameList.
// Runs only after successful live directory reads, not from an SCL guess.
// Extra exact ST/MX LN roots share the total GVAA probe limit.
bool use_verified_data_set_type_hints{false};
std::size_t maximum_data_set_type_hint_roots{48U};

bool read_data_set_directories{true};
std::size_t maximum_data_set_directories{4'096U};

Expand Down
8 changes: 4 additions & 4 deletions include/ariec61850/mms/live_model_chunk01.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ namespace live_model_detail {
}
[[nodiscard]] inline std::string key(std::string value) {
std::replace(value.begin(), value.end(), '$', '.');
return lower(std::move(value));
return value;
Comment thread
masarray marked this conversation as resolved.
}
[[nodiscard]] inline std::string bit_names(const MmsReportBitField& field) {
std::ostringstream out;
Expand All @@ -74,16 +74,16 @@ namespace live_model_detail {
const MmsTypeSpecification* best{};
std::size_t best_depth{};
for (const auto& item : evidence) {
if (!item.success() || !same(item.variable.domain, point.domain)) continue;
if (!item.success() || item.variable.domain != point.domain) continue;
const auto root = split(item.variable.item, '$');
if (root.empty() || root.size() > target.size()) continue;
bool prefix = true;
for (std::size_t i = 0; i < root.size(); ++i) prefix &= same(root[i], target[i]);
for (std::size_t i = 0; i < root.size(); ++i) prefix &= (root[i] == target[i]);
if (!prefix) continue;
const MmsTypeSpecification* current = &item.attributes->type;
for (std::size_t i = root.size(); current && i < target.size(); ++i) {
const auto found = std::find_if(current->children.begin(), current->children.end(),
[&](const auto& child) { return same(child.name, target[i]); });
[&](const auto& child) { return child.name == target[i]; });
current = found == current->children.end() ? nullptr : &*found;
}
if (current && root.size() >= best_depth) {
Expand Down
8 changes: 6 additions & 2 deletions include/ariec61850/mms/live_model_fingerprint.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ inline std::string MmsLiveModelDocument::structural_manifest() const {
append_control_blocks(log_controls);

std::sort(lines.begin() + 2, lines.end(), [](const auto& left, const auto& right) {
return lower(left) < lower(right);
const auto folded_left = lower(left);
const auto folded_right = lower(right);
return folded_left == folded_right ? left < right : folded_left < folded_right;
});

std::ostringstream out;
Expand Down Expand Up @@ -136,7 +138,9 @@ inline std::string MmsLiveModelDocument::runtime_snapshot_manifest() const {
append_runtime_controls(log_controls);

std::sort(lines.begin() + 2, lines.end(), [](const auto& left, const auto& right) {
return lower(left) < lower(right);
const auto folded_left = lower(left);
const auto folded_right = lower(right);
return folded_left == folded_right ? left < right : folded_left < folded_right;
});

std::ostringstream out;
Expand Down
175 changes: 131 additions & 44 deletions src/mms/live_discovery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ void validate_options(const MmsLiveDiscoveryOptions& options) {
throw std::invalid_argument(
"Live MMS discovery report-control probe limit must be positive.");
}
if (options.use_verified_data_set_type_hints &&
(!options.probe_variable_types || !options.read_data_set_directories ||
options.maximum_data_set_type_hint_roots == 0U)) {
throw std::invalid_argument(
"Verified DataSet type closure requires type and directory reads "
"and a positive extra-LN budget.");
}
}

[[nodiscard]] std::string object_class_name(
Expand Down Expand Up @@ -108,14 +115,6 @@ void validate_options(const MmsLiveDiscoveryOptions& options) {
": " + exception.what();
}

[[nodiscard]] std::string lower_ascii(std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](const char character) {
return static_cast<char>(
std::tolower(static_cast<unsigned char>(character)));
});
return value;
}

// Mirrors ARIEC61850 LiveIedVariableTypeProbePlanner: probe one MMS type tree at
// each Logical Node root instead of probing every LN$FC$DO$DA leaf. Standards-
// shaped servers usually advertise the Logical Node root directly (for example
Expand All @@ -138,7 +137,8 @@ void validate_options(const MmsLiveDiscoveryOptions& options) {
? item
: item.substr(0U, separator);
if (logical_node.empty()) continue;
const auto identity = lower_ascii(domain) + "\n" + lower_ascii(logical_node);
// IEC 61850/MMS references are case-sensitive identifiers.
const auto identity = domain + "\n" + logical_node;
if (!seen.insert(identity).second) {
continue;
}
Expand All @@ -152,6 +152,67 @@ void validate_options(const MmsLiveDiscoveryOptions& options) {
return candidates;
}


struct VerifiedTypeHintPlan final {
std::vector<MmsObjectName> roots;
bool budget_exceeded{};
};

// Trust only observed, non-deletable MMS DataSet directories, never an SCL
// declaration or a guessed LN name. An exact FC root proves a candidate for
// a targeted type probe; its type remains unknown until GVA actually succeeds.
// Reject the whole optional extension on budget overflow, preserving the
// original NameList-only discovery unchanged.
[[nodiscard]] VerifiedTypeHintPlan plan_verified_data_set_type_roots(
const MmsDiscoverySnapshot& snapshot,
const std::vector<MmsDataSetDirectoryEvidence>& directories,
const std::vector<MmsObjectName>& existing_roots,
const std::size_t maximum_total,
const std::size_t maximum_extra) {
VerifiedTypeHintPlan plan;
std::set<std::string, std::less<>> seen;
for (const auto& root : existing_roots) seen.insert(root.reference());

for (const auto& evidence : directories) {
if (!evidence.success() || evidence.directory->deletable ||
snapshot.domain_variables.find(evidence.candidate.domain) ==
snapshot.domain_variables.end()) {
continue;
}
for (const auto& member : evidence.directory->members) {
const auto& name = member.object_name;
if (name.kind != MmsObjectNameKind::domain_specific ||
name.domain != evidence.candidate.domain) continue;
const auto first = name.item.find('$');
if (first == std::string::npos || first == 0U) continue;
const auto second = name.item.find('$', first + 1U);
if (second == std::string::npos || second == first + 1U ||
second + 1U >= name.item.size()) continue;
const auto fc = std::string_view{name.item}.substr(
first + 1U, second - first - 1U);
if (fc != "ST" && fc != "MX") continue;
if (member.functional_constraint != fc ||
(!member.logical_node.empty() &&
member.logical_node != name.item.substr(0U, first)) ||
(!member.mms_reference.empty() &&
member.mms_reference != name.reference())) {
continue;
}
const auto root = MmsObjectName::domain_specific(
name.domain, name.item.substr(0U, first));
if (!seen.insert(root.reference()).second) continue;
if (plan.roots.size() >= maximum_extra ||
existing_roots.size() + plan.roots.size() >= maximum_total) {
plan.roots.clear();
plan.budget_exceeded = true;
return plan;
}
plan.roots.push_back(root);
}
}
return plan;
}

} // namespace

std::size_t MmsLiveDiscoveryResult::domain_count() const noexcept {
Expand Down Expand Up @@ -326,9 +387,58 @@ MmsLiveDiscoveryResult MmsLiveDiscoveryClient::discover(

result.report_inventory = MmsReportInventoryBuilder::build(result.names);

if (options.probe_variable_types) {
const auto candidates = build_logical_node_type_probe_candidates(
const auto readDataSetDirectories = [&] {
if (!options.read_data_set_directories) return;
const std::size_t count = std::min(
options.maximum_data_set_directories,
result.report_inventory.data_sets.size());
result.data_set_directories.reserve(count);
for (std::size_t index = 0U; index < count; ++index) {
MmsDataSetDirectoryEvidence evidence;
evidence.candidate = result.report_inventory.data_sets[index];
try {
MmsDataSetDirectoryRequest request;
request.invoke_id = association_.next_invoke_id();
request.data_set_name = MmsDataSetDirectoryCodec::parse_data_set_reference(
evidence.candidate.reference);
const auto encoded = MmsDataSetDirectoryCodec::encode_request_p_data(
request, association_.negotiated().presentation_context_id);
const auto exchange = association_.exchange_confirmed(
encoded, request.invoke_id, stop_token);
const auto payload = response_payload(
exchange, "GetNamedVariableListAttributes");
evidence.directory = MmsDataSetDirectoryCodec::decode_response(
payload, request.invoke_id);
} catch (const std::exception& exception) {
evidence.error = exception.what();
result.diagnostics.push_back(optional_probe_error(
"DataSet directory", evidence.candidate.reference, exception));
if (!options.continue_on_optional_probe_error) {
throw;
}
}
result.data_set_directories.push_back(std::move(evidence));
}
};

const auto probeVariableTypes = [&] {
if (!options.probe_variable_types) return;
auto candidates = build_logical_node_type_probe_candidates(
result.names, options.maximum_variable_type_probes);
if (options.use_verified_data_set_type_hints) {
const auto extra = plan_verified_data_set_type_roots(
result.names, result.data_set_directories, candidates,
options.maximum_variable_type_probes,
options.maximum_data_set_type_hint_roots);
if (extra.budget_exceeded) {
result.diagnostics.push_back(
"Verified DataSet LN type-root budget exceeded; "
"optional extra probes skipped without partial selection.");
} else {
candidates.insert(
candidates.end(), extra.roots.begin(), extra.roots.end());
}
}
result.variable_types.reserve(candidates.size());

for (const auto& candidate : candidates) {
Expand Down Expand Up @@ -361,39 +471,16 @@ MmsLiveDiscoveryResult MmsLiveDiscoveryClient::discover(
}
result.variable_types.push_back(std::move(evidence));
}
}

if (options.read_data_set_directories) {
const std::size_t count = std::min(
options.maximum_data_set_directories,
result.report_inventory.data_sets.size());
result.data_set_directories.reserve(count);
for (std::size_t index = 0U; index < count; ++index) {
MmsDataSetDirectoryEvidence evidence;
evidence.candidate = result.report_inventory.data_sets[index];
try {
MmsDataSetDirectoryRequest request;
request.invoke_id = association_.next_invoke_id();
request.data_set_name = MmsDataSetDirectoryCodec::parse_data_set_reference(
evidence.candidate.reference);
const auto encoded = MmsDataSetDirectoryCodec::encode_request_p_data(
request, association_.negotiated().presentation_context_id);
const auto exchange = association_.exchange_confirmed(
encoded, request.invoke_id, stop_token);
const auto payload = response_payload(
exchange, "GetNamedVariableListAttributes");
evidence.directory = MmsDataSetDirectoryCodec::decode_response(
payload, request.invoke_id);
} catch (const std::exception& exception) {
evidence.error = exception.what();
result.diagnostics.push_back(optional_probe_error(
"DataSet directory", evidence.candidate.reference, exception));
if (!options.continue_on_optional_probe_error) {
throw;
}
}
result.data_set_directories.push_back(std::move(evidence));
}
};

// Only the opt-in closure path changes phase order. Existing callers
// retain their wire sequence and unchanged default request count.
if (options.use_verified_data_set_type_hints) {
readDataSetDirectories();
probeVariableTypes();
} else {
probeVariableTypes();
readDataSetDirectories();
}

if (options.probe_report_controls) {
Expand Down
Loading
Loading