From e49e25a9a5d5361c899664fec61e01759dd88006 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 10 Aug 2026 19:34:09 +0700 Subject: [PATCH 1/6] Add live-type MMS control structure builder API --- .../control/mms_control_structure.hpp | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 include/ariec61850/control/mms_control_structure.hpp diff --git a/include/ariec61850/control/mms_control_structure.hpp b/include/ariec61850/control/mms_control_structure.hpp new file mode 100644 index 000000000..b667a7bbf --- /dev/null +++ b/include/ariec61850/control/mms_control_structure.hpp @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/control/guarded_control.hpp" +#include "ariec61850/mms/services.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::control { + +enum class DoublePointValue : std::uint8_t { + intermediate = 0U, + off = 1U, + on = 2U, + bad = 3U, +}; + +struct StepPosition final { + std::int64_t position{}; + bool transient{}; +}; + +enum class ControlValueKind : std::uint8_t { + boolean, + double_point, + integer, + unsigned_integer, + floating_point, + step_position, + raw_mms, +}; + +class ControlValue final { +public: + using Value = std::variant< + bool, + DoublePointValue, + std::int64_t, + std::uint64_t, + double, + StepPosition, + mms::MmsDataValue>; + + [[nodiscard]] static ControlValue boolean(bool value); + [[nodiscard]] static ControlValue double_point(DoublePointValue value); + [[nodiscard]] static ControlValue integer(std::int64_t value); + [[nodiscard]] static ControlValue unsigned_integer(std::uint64_t value); + [[nodiscard]] static ControlValue floating_point(double value); + [[nodiscard]] static ControlValue step_position(StepPosition value); + [[nodiscard]] static ControlValue raw_mms(mms::MmsDataValue value); + + [[nodiscard]] ControlValueKind kind() const noexcept { return kind_; } + [[nodiscard]] const Value& value() const noexcept { return value_; } + +private: + ControlValue(ControlValueKind kind, Value value) + : kind_{kind}, value_{std::move(value)} {} + + ControlValueKind kind_; + Value value_; +}; + +enum class MmsControlService : std::uint8_t { + operate, + select_with_value, + cancel, +}; + +enum class MmsControlBuildStatus : std::uint8_t { + ok, + invalid_service_specification, + unsupported_component, + missing_required_component, + type_mismatch, + shape_mismatch, + value_out_of_range, + timestamp_missing, + unsupported_control_value, +}; + +struct MmsControlBuildResult final { + MmsControlBuildStatus status{MmsControlBuildStatus::invalid_service_specification}; + std::optional value; + std::string path; + + [[nodiscard]] bool success() const noexcept { + return status == MmsControlBuildStatus::ok && value.has_value(); + } +}; + +struct MmsControlSequenceContext final { + ControlValue control_value{ControlValue::boolean(false)}; + OriginCategory origin_category{OriginCategory::station_control}; + std::vector origin_identifier; + std::uint8_t control_number{}; + std::optional timestamp; + std::optional operate_at; + bool test{}; + bool interlock_check{}; + bool synchro_check{}; +}; + +// Hosted/live-control builder. It mirrors the C# oracle's conservative policy: +// consume the live MMS TypeSpecification, preserve its component order, and +// reject unknown/vendor-specific command fields rather than guessing them. +class MmsControlStructureBuilder final { +public: + [[nodiscard]] static MmsControlBuildResult bind_control_value( + const ControlValue& value, + const mms::MmsTypeSpecification& specification); + + [[nodiscard]] static MmsControlBuildResult validate_value( + const mms::MmsDataValue& value, + const mms::MmsTypeSpecification& specification, + std::string path = "ctlVal"); + + [[nodiscard]] static MmsControlBuildResult build_operate( + const MmsControlSequenceContext& context, + const mms::MmsTypeSpecification& specification, + bool require_exact_named_fields = true); + + [[nodiscard]] static MmsControlBuildResult build_select_with_value( + const MmsControlSequenceContext& context, + const mms::MmsTypeSpecification& specification, + bool require_exact_named_fields = true); + + [[nodiscard]] static MmsControlBuildResult build_cancel( + const MmsControlSequenceContext& context, + const mms::MmsTypeSpecification& specification, + bool require_exact_named_fields = true); + +private: + [[nodiscard]] static MmsControlBuildResult build_service( + MmsControlService service, + const MmsControlSequenceContext& context, + const mms::MmsTypeSpecification& specification, + bool require_exact_named_fields); +}; + +} // namespace ar::iec61850::control From bada5f93d241f3ee5c3074103b3196ac9c2e94b2 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 10 Aug 2026 19:35:37 +0700 Subject: [PATCH 2/6] Implement live-type Oper SBOw and Cancel builder --- src/control/mms_control_structure.cpp | 742 ++++++++++++++++++++++++++ 1 file changed, 742 insertions(+) create mode 100644 src/control/mms_control_structure.cpp diff --git a/src/control/mms_control_structure.cpp b/src/control/mms_control_structure.cpp new file mode 100644 index 000000000..128dd7d4c --- /dev/null +++ b/src/control/mms_control_structure.cpp @@ -0,0 +1,742 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/control/mms_control_structure.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::control { +namespace { + +using mms::MmsDataKind; +using mms::MmsDataValue; +using mms::MmsTypeKind; +using mms::MmsTypeSpecification; + +[[nodiscard]] MmsControlBuildResult failure( + const MmsControlBuildStatus status, + std::string path) { + return {status, std::nullopt, std::move(path)}; +} + +[[nodiscard]] MmsControlBuildResult success(MmsDataValue value) { + return {MmsControlBuildStatus::ok, std::move(value), {}}; +} + +[[nodiscard]] std::string normalize_name(const std::string& value) { + std::string result; + result.reserve(value.size()); + for (const auto character : value) { + const auto byte = static_cast(character); + if (std::isalnum(byte) != 0) { + result.push_back(static_cast(std::tolower(byte))); + } + } + return result; +} + +[[nodiscard]] bool numeric_kind(const MmsTypeKind kind) noexcept { + return kind == MmsTypeKind::integer || + kind == MmsTypeKind::unsigned_integer || + kind == MmsTypeKind::floating_point || + kind == MmsTypeKind::bcd; +} + +[[nodiscard]] bool compatible_kind( + const MmsDataKind actual, + const MmsTypeKind expected) noexcept { + switch (expected) { + case MmsTypeKind::array: return actual == MmsDataKind::array; + case MmsTypeKind::structure: return actual == MmsDataKind::structure; + case MmsTypeKind::boolean: return actual == MmsDataKind::boolean; + case MmsTypeKind::bit_string: return actual == MmsDataKind::bit_string; + case MmsTypeKind::integer: return actual == MmsDataKind::integer; + case MmsTypeKind::unsigned_integer: return actual == MmsDataKind::unsigned_integer; + case MmsTypeKind::floating_point: return actual == MmsDataKind::floating_point; + case MmsTypeKind::octet_string: return actual == MmsDataKind::octet_string; + case MmsTypeKind::visible_string: return actual == MmsDataKind::visible_string; + case MmsTypeKind::binary_time: return actual == MmsDataKind::binary_time; + case MmsTypeKind::bcd: return actual == MmsDataKind::integer; + case MmsTypeKind::boolean_array: return actual == MmsDataKind::boolean_array; + case MmsTypeKind::object_id: return actual == MmsDataKind::object_id; + case MmsTypeKind::mms_string: return actual == MmsDataKind::mms_string; + case MmsTypeKind::utc_time: return actual == MmsDataKind::utc_time; + case MmsTypeKind::unknown: return false; + } + return false; +} + +[[nodiscard]] MmsControlBuildResult validate_recursive( + const MmsDataValue& value, + const MmsTypeSpecification& specification, + const std::string& path) { + if (!compatible_kind(value.kind(), specification.kind)) { + return failure(MmsControlBuildStatus::type_mismatch, path); + } + + if (specification.kind == MmsTypeKind::bit_string) { + const auto& raw = value.raw_value(); + if (raw.size() <= 1U || raw.front() > 7U) { + return failure(MmsControlBuildStatus::shape_mismatch, path); + } + const auto total_bits = (raw.size() - 1U) * 8U; + const auto unused_bits = static_cast(raw.front()); + if (unused_bits > total_bits) { + return failure(MmsControlBuildStatus::shape_mismatch, path); + } + const auto actual_bits = total_bits - unused_bits; + if (specification.size.has_value() && specification.size.value() != 0U && + actual_bits != static_cast(specification.size.value())) { + return failure(MmsControlBuildStatus::shape_mismatch, path); + } + } + + if (specification.kind == MmsTypeKind::octet_string && + specification.size.has_value() && specification.size.value() != 0U && + value.raw_value().size() > static_cast(specification.size.value())) { + return failure(MmsControlBuildStatus::shape_mismatch, path); + } + + if ((specification.kind == MmsTypeKind::visible_string || + specification.kind == MmsTypeKind::mms_string) && + specification.size.has_value() && specification.size.value() != 0U) { + const auto* text = std::get_if(&value.value()); + if (text == nullptr || + text->size() > static_cast(specification.size.value())) { + return failure(MmsControlBuildStatus::shape_mismatch, path); + } + } + + if (specification.kind == MmsTypeKind::structure || + specification.kind == MmsTypeKind::array) { + const auto& children = value.children(); + if (!specification.children.empty() && children.size() != specification.children.size()) { + return failure(MmsControlBuildStatus::shape_mismatch, path); + } + const auto count = std::min(children.size(), specification.children.size()); + for (std::size_t index = 0U; index < count; ++index) { + const auto child_name = specification.children[index].name.empty() + ? "[" + std::to_string(index) + "]" + : specification.children[index].name; + auto child = validate_recursive( + children[index], specification.children[index], path + "." + child_name); + if (!child.success()) { + return child; + } + } + } + + return success(value); +} + +[[nodiscard]] MmsControlBuildResult default_value( + const MmsTypeSpecification& specification, + const std::string& path); + +[[nodiscard]] MmsControlBuildResult bind_value( + const ControlValue& value, + const MmsTypeSpecification& specification, + const std::string& path); + +[[nodiscard]] MmsControlBuildResult zero_bit_string( + const MmsTypeSpecification& specification, + const std::string& path) { + auto bit_count = specification.size.value_or(1U); + if (bit_count == 0U || bit_count > 1024U) { + bit_count = 1U; + } + const auto byte_count = static_cast((bit_count + 7U) / 8U); + std::vector bytes(byte_count, 0U); + const auto unused = static_cast(byte_count * 8U - bit_count); + auto result = MmsDataValue::bit_string(unused, bytes); + return validate_recursive(result, specification, path); +} + +[[nodiscard]] MmsControlBuildResult default_value( + const MmsTypeSpecification& specification, + const std::string& path) { + switch (specification.kind) { + case MmsTypeKind::boolean: + return success(MmsDataValue::boolean(false)); + case MmsTypeKind::bit_string: + return zero_bit_string(specification, path); + case MmsTypeKind::integer: + case MmsTypeKind::bcd: + return success(MmsDataValue::integer(0)); + case MmsTypeKind::unsigned_integer: + return success(MmsDataValue::unsigned_integer(0U)); + case MmsTypeKind::floating_point: + return success(MmsDataValue::floating_point(0.0)); + case MmsTypeKind::octet_string: + return success(MmsDataValue::octet_string({})); + case MmsTypeKind::visible_string: + return success(MmsDataValue::visible_string({})); + case MmsTypeKind::mms_string: + return success(MmsDataValue::mms_string({})); + case MmsTypeKind::utc_time: + return success(MmsDataValue::utc_time(mms::Iec61850UtcTime{})); + case MmsTypeKind::binary_time: { + const auto size = specification.size.value_or(6U) == 4U ? 4U : 6U; + std::array zero{}; + return success(MmsDataValue::binary_time( + std::span{zero}.first(size))); + } + case MmsTypeKind::structure: { + std::vector children; + children.reserve(specification.children.size()); + for (const auto& child_specification : specification.children) { + auto child = default_value( + child_specification, + path + "." + child_specification.name); + if (!child.success()) { + return child; + } + children.push_back(std::move(child.value.value())); + } + return success(MmsDataValue::structure(std::move(children))); + } + default: + return failure(MmsControlBuildStatus::unsupported_component, path); + } +} + +[[nodiscard]] MmsControlBuildResult bind_boolean( + const ControlValue& value, + const MmsTypeSpecification& specification, + const std::string& path) { + bool result{}; + switch (value.kind()) { + case ControlValueKind::boolean: + result = std::get(value.value()); + break; + case ControlValueKind::integer: + result = std::get(value.value()) != 0; + break; + case ControlValueKind::unsigned_integer: + result = std::get(value.value()) != 0U; + break; + default: + return failure(MmsControlBuildStatus::type_mismatch, path); + } + auto encoded = MmsDataValue::boolean(result); + return validate_recursive(encoded, specification, path); +} + +[[nodiscard]] bool try_signed( + const ControlValue& value, + std::int64_t& result) noexcept { + result = 0; + switch (value.kind()) { + case ControlValueKind::integer: + result = std::get(value.value()); + return true; + case ControlValueKind::unsigned_integer: { + const auto input = std::get(value.value()); + if (input > static_cast(std::numeric_limits::max())) { + return false; + } + result = static_cast(input); + return true; + } + case ControlValueKind::double_point: + result = static_cast(std::get(value.value())); + return true; + case ControlValueKind::floating_point: { + const auto input = std::get(value.value()); + if (!std::isfinite(input) || + input < static_cast(std::numeric_limits::min()) || + input > static_cast(std::numeric_limits::max())) { + return false; + } + result = static_cast(input); + return true; + } + default: + return false; + } +} + +[[nodiscard]] bool try_unsigned( + const ControlValue& value, + std::uint64_t& result) noexcept { + result = 0U; + switch (value.kind()) { + case ControlValueKind::unsigned_integer: + result = std::get(value.value()); + return true; + case ControlValueKind::integer: { + const auto input = std::get(value.value()); + if (input < 0) { + return false; + } + result = static_cast(input); + return true; + } + case ControlValueKind::double_point: + result = static_cast(std::get(value.value())); + return true; + default: + return false; + } +} + +[[nodiscard]] MmsControlBuildResult bind_bit_string( + const ControlValue& value, + const MmsTypeSpecification& specification, + const std::string& path) { + std::uint64_t numeric{}; + if (!try_unsigned(value, numeric)) { + return failure(MmsControlBuildStatus::type_mismatch, path); + } + + auto bit_count = specification.size.value_or(2U); + if (bit_count == 0U || bit_count > 32U) { + bit_count = 2U; + } + const auto limit = std::uint64_t{1U} << bit_count; + if (numeric >= limit) { + return failure(MmsControlBuildStatus::value_out_of_range, path); + } + + const auto byte_count = static_cast((bit_count + 7U) / 8U); + std::vector bytes(byte_count, 0U); + for (std::uint32_t encoded_bit = 0U; encoded_bit < bit_count; ++encoded_bit) { + const auto numeric_bit = bit_count - 1U - encoded_bit; + if ((numeric & (std::uint64_t{1U} << numeric_bit)) != 0U) { + const auto byte_index = static_cast(encoded_bit / 8U); + const auto bit_in_byte = encoded_bit % 8U; + bytes[byte_index] = static_cast( + bytes[byte_index] | static_cast(0x80U >> bit_in_byte)); + } + } + const auto unused = static_cast(byte_count * 8U - bit_count); + auto encoded = MmsDataValue::bit_string(unused, bytes); + return validate_recursive(encoded, specification, path); +} + +[[nodiscard]] MmsControlBuildResult bind_structure( + const ControlValue& value, + const MmsTypeSpecification& specification, + const std::string& path) { + if (specification.children.empty()) { + return failure(MmsControlBuildStatus::shape_mismatch, path); + } + + if (value.kind() == ControlValueKind::step_position) { + const auto step = std::get(value.value()); + std::vector children; + children.reserve(specification.children.size()); + for (const auto& child_specification : specification.children) { + const auto name = normalize_name(child_specification.name); + MmsControlBuildResult child; + if (name == "posval" || name == "position" || name == "i") { + child = bind_value( + ControlValue::integer(step.position), child_specification, + path + "." + child_specification.name); + } else if (name == "transind" || name == "transient") { + child = bind_value( + ControlValue::boolean(step.transient), child_specification, + path + "." + child_specification.name); + } else { + return failure( + MmsControlBuildStatus::unsupported_component, + path + "." + child_specification.name); + } + if (!child.success()) { + return child; + } + children.push_back(std::move(child.value.value())); + } + auto encoded = MmsDataValue::structure(std::move(children)); + return validate_recursive(encoded, specification, path); + } + + const bool scalar_numeric = value.kind() == ControlValueKind::floating_point || + value.kind() == ControlValueKind::integer || + value.kind() == ControlValueKind::unsigned_integer; + if (scalar_numeric) { + std::vector numeric_indices; + for (std::size_t index = 0U; index < specification.children.size(); ++index) { + if (numeric_kind(specification.children[index].kind)) { + numeric_indices.push_back(index); + } + } + + std::optional selected_index; + if (numeric_indices.size() == 1U) { + selected_index = numeric_indices.front(); + } else { + const auto desired_name = value.kind() == ControlValueKind::floating_point ? "f" : "i"; + for (std::size_t index = 0U; index < specification.children.size(); ++index) { + if (normalize_name(specification.children[index].name) == desired_name) { + selected_index = index; + break; + } + } + } + + if (selected_index.has_value()) { + std::vector children; + children.reserve(specification.children.size()); + for (std::size_t index = 0U; index < specification.children.size(); ++index) { + MmsControlBuildResult child = index == selected_index.value() + ? bind_value(value, specification.children[index], + path + "." + specification.children[index].name) + : default_value(specification.children[index], + path + "." + specification.children[index].name); + if (!child.success()) { + return child; + } + children.push_back(std::move(child.value.value())); + } + auto encoded = MmsDataValue::structure(std::move(children)); + return validate_recursive(encoded, specification, path); + } + } + + if (specification.children.size() == 1U) { + auto child = bind_value( + value, specification.children.front(), + path + "." + specification.children.front().name); + if (!child.success()) { + return child; + } + std::vector children; + children.push_back(std::move(child.value.value())); + auto encoded = MmsDataValue::structure(std::move(children)); + return validate_recursive(encoded, specification, path); + } + + return failure(MmsControlBuildStatus::unsupported_control_value, path); +} + +[[nodiscard]] MmsControlBuildResult bind_value( + const ControlValue& value, + const MmsTypeSpecification& specification, + const std::string& path) { + if (value.kind() == ControlValueKind::raw_mms) { + return validate_recursive( + std::get(value.value()), specification, path); + } + + switch (specification.kind) { + case MmsTypeKind::boolean: + return bind_boolean(value, specification, path); + case MmsTypeKind::bit_string: + return bind_bit_string(value, specification, path); + case MmsTypeKind::integer: + case MmsTypeKind::bcd: { + std::int64_t numeric{}; + if (!try_signed(value, numeric)) { + return failure(MmsControlBuildStatus::value_out_of_range, path); + } + auto encoded = MmsDataValue::integer(numeric); + return validate_recursive(encoded, specification, path); + } + case MmsTypeKind::unsigned_integer: { + std::uint64_t numeric{}; + if (!try_unsigned(value, numeric)) { + return failure(MmsControlBuildStatus::value_out_of_range, path); + } + auto encoded = MmsDataValue::unsigned_integer(numeric); + return validate_recursive(encoded, specification, path); + } + case MmsTypeKind::floating_point: { + double numeric{}; + switch (value.kind()) { + case ControlValueKind::floating_point: + numeric = std::get(value.value()); + break; + case ControlValueKind::integer: + numeric = static_cast(std::get(value.value())); + break; + case ControlValueKind::unsigned_integer: + numeric = static_cast(std::get(value.value())); + break; + default: + return failure(MmsControlBuildStatus::type_mismatch, path); + } + auto encoded = MmsDataValue::floating_point(numeric); + return validate_recursive(encoded, specification, path); + } + case MmsTypeKind::structure: + return bind_structure(value, specification, path); + default: + return failure(MmsControlBuildStatus::unsupported_control_value, path); + } +} + +[[nodiscard]] MmsControlBuildResult build_integer_like( + const std::int64_t value, + const MmsTypeSpecification& specification, + const std::string& path) { + if (specification.kind == MmsTypeKind::integer || specification.kind == MmsTypeKind::bcd) { + auto encoded = MmsDataValue::integer(value); + return validate_recursive(encoded, specification, path); + } + if (specification.kind == MmsTypeKind::unsigned_integer) { + if (value < 0) { + return failure(MmsControlBuildStatus::value_out_of_range, path); + } + auto encoded = MmsDataValue::unsigned_integer(static_cast(value)); + return validate_recursive(encoded, specification, path); + } + return failure(MmsControlBuildStatus::type_mismatch, path); +} + +[[nodiscard]] MmsControlBuildResult build_origin( + const MmsControlSequenceContext& context, + const MmsTypeSpecification& specification) { + if (specification.kind != MmsTypeKind::structure || specification.children.empty()) { + return failure(MmsControlBuildStatus::type_mismatch, "origin"); + } + + std::vector children; + children.reserve(specification.children.size()); + for (const auto& child_specification : specification.children) { + const auto name = normalize_name(child_specification.name); + MmsControlBuildResult child; + if (name == "orcat") { + child = build_integer_like( + static_cast(context.origin_category), + child_specification, + "origin.orCat"); + } else if (name == "orident") { + if (child_specification.kind == MmsTypeKind::octet_string) { + auto encoded = MmsDataValue::octet_string(context.origin_identifier); + child = validate_recursive(encoded, child_specification, "origin.orIdent"); + } else if (child_specification.kind == MmsTypeKind::visible_string) { + std::string text; + text.reserve(context.origin_identifier.size()); + for (const auto byte : context.origin_identifier) { + text.push_back(byte <= 0x7FU ? static_cast(byte) : '?'); + } + auto encoded = MmsDataValue::visible_string(std::move(text)); + child = validate_recursive(encoded, child_specification, "origin.orIdent"); + } else { + return failure(MmsControlBuildStatus::type_mismatch, "origin.orIdent"); + } + } else { + return failure( + MmsControlBuildStatus::unsupported_component, + "origin." + child_specification.name); + } + if (!child.success()) { + return child; + } + children.push_back(std::move(child.value.value())); + } + + auto encoded = MmsDataValue::structure(std::move(children)); + return validate_recursive(encoded, specification, "origin"); +} + +[[nodiscard]] MmsControlBuildResult build_boolean_field( + const bool value, + const MmsTypeSpecification& specification, + const std::string& path) { + if (specification.kind != MmsTypeKind::boolean) { + return failure(MmsControlBuildStatus::type_mismatch, path); + } + auto encoded = MmsDataValue::boolean(value); + return validate_recursive(encoded, specification, path); +} + +[[nodiscard]] MmsControlBuildResult build_check( + const MmsControlSequenceContext& context, + const MmsTypeSpecification& specification) { + if (specification.kind != MmsTypeKind::bit_string) { + return failure(MmsControlBuildStatus::type_mismatch, "Check"); + } + std::uint8_t bits = 0U; + if (context.synchro_check) { + bits = static_cast(bits | 0x80U); + } + if (context.interlock_check) { + bits = static_cast(bits | 0x40U); + } + const std::array data{bits}; + auto encoded = MmsDataValue::bit_string(6U, data); + return validate_recursive(encoded, specification, "Check"); +} + +[[nodiscard]] MmsControlBuildResult build_default_timestamp( + const MmsTypeSpecification& specification, + const std::string& path) { + if (specification.kind == MmsTypeKind::utc_time) { + auto encoded = MmsDataValue::utc_time(mms::Iec61850UtcTime{}); + return validate_recursive(encoded, specification, path); + } + if (specification.kind == MmsTypeKind::binary_time) { + const auto size = specification.size.value_or(6U) == 4U ? 4U : 6U; + std::array zero{}; + auto encoded = MmsDataValue::binary_time( + std::span{zero}.first(size)); + return validate_recursive(encoded, specification, path); + } + return failure(MmsControlBuildStatus::type_mismatch, path); +} + +[[nodiscard]] bool required_field( + const MmsControlService service, + const std::string& name) noexcept { + if (name == "ctlval" || name == "origin" || name == "ctlnum" || + name == "t" || name == "test") { + return true; + } + return service != MmsControlService::cancel && name == "check"; +} + +} // namespace + +ControlValue ControlValue::boolean(const bool value) { + return {ControlValueKind::boolean, value}; +} + +ControlValue ControlValue::double_point(const DoublePointValue value) { + return {ControlValueKind::double_point, value}; +} + +ControlValue ControlValue::integer(const std::int64_t value) { + return {ControlValueKind::integer, value}; +} + +ControlValue ControlValue::unsigned_integer(const std::uint64_t value) { + return {ControlValueKind::unsigned_integer, value}; +} + +ControlValue ControlValue::floating_point(const double value) { + return {ControlValueKind::floating_point, value}; +} + +ControlValue ControlValue::step_position(const StepPosition value) { + return {ControlValueKind::step_position, value}; +} + +ControlValue ControlValue::raw_mms(MmsDataValue value) { + return {ControlValueKind::raw_mms, std::move(value)}; +} + +MmsControlBuildResult MmsControlStructureBuilder::bind_control_value( + const ControlValue& value, + const MmsTypeSpecification& specification) { + return bind_value(value, specification, "ctlVal"); +} + +MmsControlBuildResult MmsControlStructureBuilder::validate_value( + const MmsDataValue& value, + const MmsTypeSpecification& specification, + std::string path) { + return validate_recursive(value, specification, path); +} + +MmsControlBuildResult MmsControlStructureBuilder::build_operate( + const MmsControlSequenceContext& context, + const MmsTypeSpecification& specification, + const bool require_exact_named_fields) { + return build_service( + MmsControlService::operate, context, specification, require_exact_named_fields); +} + +MmsControlBuildResult MmsControlStructureBuilder::build_select_with_value( + const MmsControlSequenceContext& context, + const MmsTypeSpecification& specification, + const bool require_exact_named_fields) { + return build_service( + MmsControlService::select_with_value, context, specification, + require_exact_named_fields); +} + +MmsControlBuildResult MmsControlStructureBuilder::build_cancel( + const MmsControlSequenceContext& context, + const MmsTypeSpecification& specification, + const bool require_exact_named_fields) { + return build_service( + MmsControlService::cancel, context, specification, require_exact_named_fields); +} + +MmsControlBuildResult MmsControlStructureBuilder::build_service( + const MmsControlService service, + const MmsControlSequenceContext& context, + const MmsTypeSpecification& specification, + const bool require_exact_named_fields) { + if (specification.kind != MmsTypeKind::structure || specification.children.empty()) { + return failure(MmsControlBuildStatus::invalid_service_specification, "service"); + } + + std::array found{}; + const auto mark = [&](const std::string& name) { + if (name == "ctlval") found[0] = true; + else if (name == "origin") found[1] = true; + else if (name == "ctlnum") found[2] = true; + else if (name == "t") found[3] = true; + else if (name == "test") found[4] = true; + else if (name == "check") found[5] = true; + }; + + std::vector children; + children.reserve(specification.children.size()); + for (const auto& child_specification : specification.children) { + const auto name = normalize_name(child_specification.name); + MmsControlBuildResult child; + if (name == "ctlval") { + child = bind_value(context.control_value, child_specification, "ctlVal"); + } else if (name == "opertm" && service != MmsControlService::cancel) { + child = context.operate_at.has_value() + ? validate_recursive(context.operate_at.value(), child_specification, "operTm") + : build_default_timestamp(child_specification, "operTm"); + } else if (name == "origin") { + child = build_origin(context, child_specification); + } else if (name == "ctlnum") { + child = build_integer_like( + static_cast(context.control_number), + child_specification, + "ctlNum"); + } else if (name == "t") { + if (!context.timestamp.has_value()) { + return failure(MmsControlBuildStatus::timestamp_missing, "T"); + } + child = validate_recursive(context.timestamp.value(), child_specification, "T"); + } else if (name == "test") { + child = build_boolean_field(context.test, child_specification, "Test"); + } else if (name == "check") { + child = build_check(context, child_specification); + } else { + return failure( + MmsControlBuildStatus::unsupported_component, + child_specification.name); + } + + if (!child.success()) { + return child; + } + mark(name); + children.push_back(std::move(child.value.value())); + } + + if (require_exact_named_fields) { + constexpr std::array names{ + "ctlval", "origin", "ctlnum", "t", "test", "check"}; + for (std::size_t index = 0U; index < names.size(); ++index) { + if (required_field(service, std::string{names[index]}) && !found[index]) { + return failure( + MmsControlBuildStatus::missing_required_component, + std::string{names[index]}); + } + } + } + + auto encoded = MmsDataValue::structure(std::move(children)); + const auto service_name = service == MmsControlService::operate + ? "Oper" + : (service == MmsControlService::select_with_value ? "SBOw" : "Cancel"); + return validate_recursive(encoded, specification, service_name); +} + +} // namespace ar::iec61850::control From 443faf78d731cc53776f9f169bab2db8470129ad Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 10 Aug 2026 19:36:17 +0700 Subject: [PATCH 3/6] Fix control structure header utility dependency --- include/ariec61850/control/mms_control_structure.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/ariec61850/control/mms_control_structure.hpp b/include/ariec61850/control/mms_control_structure.hpp index b667a7bbf..dc3a892e7 100644 --- a/include/ariec61850/control/mms_control_structure.hpp +++ b/include/ariec61850/control/mms_control_structure.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include From 23b40a943efd2ba06b051d9d8bfd5d048adac249 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 10 Aug 2026 19:37:59 +0700 Subject: [PATCH 4/6] Add MMS control structure golden-wire tests --- tests/test_control_mms_structure.cpp | 267 +++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 tests/test_control_mms_structure.cpp diff --git a/tests/test_control_mms_structure.cpp b/tests/test_control_mms_structure.cpp new file mode 100644 index 000000000..17e157950 --- /dev/null +++ b/tests/test_control_mms_structure.cpp @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/control/mms_control_structure.hpp" +#include "ariec61850/mms/data_codec.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; +using namespace ar::iec61850::control; +using namespace ar::iec61850::mms; + +#define CHECK(condition) do { \ + if (!(condition)) { \ + throw std::runtime_error(std::string{"CHECK failed: "} + #condition + \ + " at " + __FILE__ + ":" + std::to_string(__LINE__)); \ + } \ +} while (false) + +std::string to_hex(const std::vector& bytes) { + static constexpr char digits[] = "0123456789ABCDEF"; + std::string result; + result.reserve(bytes.size() * 2U); + for (const auto byte : bytes) { + result.push_back(digits[(byte >> 4U) & 0x0FU]); + result.push_back(digits[byte & 0x0FU]); + } + return result; +} + +MmsTypeSpecification scalar( + const MmsTypeKind kind, + std::string name, + const std::optional size = std::nullopt) { + MmsTypeSpecification result; + result.kind = kind; + result.name = std::move(name); + result.size = size; + return result; +} + +MmsTypeSpecification structure( + std::string name, + std::vector children) { + MmsTypeSpecification result; + result.kind = MmsTypeKind::structure; + result.name = std::move(name); + result.children = std::move(children); + return result; +} + +MmsTypeSpecification origin_spec() { + return structure("origin", { + scalar(MmsTypeKind::unsigned_integer, "orCat"), + scalar(MmsTypeKind::octet_string, "orIdent", 64U), + }); +} + +MmsTypeSpecification oper_spec() { + // Deliberately put operTm before origin to prove that live component order, + // not a locally invented fixed order, drives the resulting MMS structure. + return structure("Oper", { + scalar(MmsTypeKind::bit_string, "ctlVal", 2U), + scalar(MmsTypeKind::utc_time, "operTm"), + origin_spec(), + scalar(MmsTypeKind::unsigned_integer, "ctlNum"), + scalar(MmsTypeKind::utc_time, "T"), + scalar(MmsTypeKind::boolean, "Test"), + scalar(MmsTypeKind::bit_string, "Check", 2U), + }); +} + +MmsTypeSpecification cancel_spec(const bool include_check) { + std::vector children{ + scalar(MmsTypeKind::bit_string, "ctlVal", 2U), + origin_spec(), + scalar(MmsTypeKind::unsigned_integer, "ctlNum"), + scalar(MmsTypeKind::utc_time, "T"), + scalar(MmsTypeKind::boolean, "Test"), + }; + if (include_check) { + children.push_back(scalar(MmsTypeKind::bit_string, "Check", 2U)); + } + return structure("Cancel", std::move(children)); +} + +MmsControlSequenceContext context() { + MmsControlSequenceContext result; + result.control_value = ControlValue::double_point(DoublePointValue::on); + result.origin_category = OriginCategory::station_control; + result.origin_identifier = {'H', 'M', 'I'}; + result.control_number = 7U; + result.timestamp = MmsDataValue::utc_time(Iec61850UtcTime{}); + result.test = false; + result.synchro_check = true; + result.interlock_check = true; + return result; +} + +void dpc_binding_matches_network_bit_order() { + const auto specification = scalar(MmsTypeKind::bit_string, "ctlVal", 2U); + auto result = MmsControlStructureBuilder::bind_control_value( + ControlValue::double_point(DoublePointValue::on), specification); + CHECK(result.success()); + CHECK(result.value->kind() == MmsDataKind::bit_string); + CHECK(result.value->raw_value().size() == 2U); + CHECK(result.value->raw_value()[0] == 6U); + CHECK(result.value->raw_value()[1] == 0x80U); // DPC On == binary 10. + + result = MmsControlStructureBuilder::bind_control_value( + ControlValue::double_point(DoublePointValue::off), specification); + CHECK(result.success()); + CHECK(result.value->raw_value()[1] == 0x40U); // DPC Off == binary 01. +} + +void operate_matches_csharp_structure_contract_and_golden_wire() { + const auto specification = oper_spec(); + auto build = MmsControlStructureBuilder::build_operate(context(), specification); + CHECK(build.success()); + CHECK(build.value->kind() == MmsDataKind::structure); + CHECK(build.value->children().size() == 7U); + + const auto& children = build.value->children(); + CHECK(children[0].kind() == MmsDataKind::bit_string); + CHECK(children[0].raw_value() == std::vector({0x06U, 0x80U})); + CHECK(children[1].kind() == MmsDataKind::utc_time); // default operTm epoch. + CHECK(children[2].kind() == MmsDataKind::structure); + CHECK(std::get(children[2].children()[0].value()) == 2U); + CHECK(children[2].children()[1].raw_value() == + std::vector({'H', 'M', 'I'})); + CHECK(std::get(children[3].value()) == 7U); + CHECK(children[4].kind() == MmsDataKind::utc_time); + CHECK(!std::get(children[5].value())); + CHECK(children[6].raw_value() == std::vector({0x06U, 0xC0U})); + + const auto encoded = MmsDataCodec::encode(build.value.value()); + CHECK(to_hex(encoded) == + "A22C8402068091080000000000000000A2088601028903484D4986010791080000000000000000830100840206C0"); + + const auto decoded = MmsDataCodec::decode_all(encoded); + CHECK(decoded.size() == 1U); + CHECK(decoded.front().kind() == MmsDataKind::structure); + CHECK(decoded.front().children().size() == 7U); + CHECK(decoded.front().children()[6].raw_value() == + std::vector({0x06U, 0xC0U})); +} + +void sbow_uses_same_exact_sequence_contract() { + auto specification = oper_spec(); + specification.name = "SBOw"; + auto input = context(); + input.operate_at = MmsDataValue::utc_time(Iec61850UtcTime{ + std::chrono::system_clock::time_point{std::chrono::seconds{10}}, 0U}); + const auto build = MmsControlStructureBuilder::build_select_with_value( + input, specification); + CHECK(build.success()); + CHECK(build.value->children().size() == 7U); + CHECK(build.value->children()[1].kind() == MmsDataKind::utc_time); +} + +void cancel_allows_optional_check_but_not_vendor_guessing() { + auto build = MmsControlStructureBuilder::build_cancel(context(), cancel_spec(false)); + CHECK(build.success()); + CHECK(build.value->children().size() == 5U); + + build = MmsControlStructureBuilder::build_cancel(context(), cancel_spec(true)); + CHECK(build.success()); + CHECK(build.value->children().size() == 6U); + CHECK(build.value->children().back().raw_value() == + std::vector({0x06U, 0xC0U})); + + auto vendor = cancel_spec(false); + vendor.children.push_back(scalar(MmsTypeKind::boolean, "vendorMagic")); + build = MmsControlStructureBuilder::build_cancel(context(), vendor); + CHECK(build.status == MmsControlBuildStatus::unsupported_component); + CHECK(build.path == "vendorMagic"); +} + +void exact_required_fields_and_check_shape_fail_closed() { + auto missing_check = oper_spec(); + missing_check.children.pop_back(); + auto build = MmsControlStructureBuilder::build_operate(context(), missing_check); + CHECK(build.status == MmsControlBuildStatus::missing_required_component); + CHECK(build.path == "check"); + + auto wrong_check = oper_spec(); + wrong_check.children.back().size = 3U; + build = MmsControlStructureBuilder::build_operate(context(), wrong_check); + CHECK(build.status == MmsControlBuildStatus::shape_mismatch); + CHECK(build.path == "Check"); + + auto missing_timestamp = context(); + missing_timestamp.timestamp.reset(); + build = MmsControlStructureBuilder::build_operate(missing_timestamp, oper_spec()); + CHECK(build.status == MmsControlBuildStatus::timestamp_missing); +} + +void origin_ctl_num_and_raw_values_are_live_type_checked() { + auto bad_origin = oper_spec(); + bad_origin.children[2].children[0].kind = MmsTypeKind::boolean; + auto build = MmsControlStructureBuilder::build_operate(context(), bad_origin); + CHECK(build.status == MmsControlBuildStatus::type_mismatch); + CHECK(build.path == "origin.orCat"); + + auto bad_ctl_num = oper_spec(); + bad_ctl_num.children[3].kind = MmsTypeKind::visible_string; + build = MmsControlStructureBuilder::build_operate(context(), bad_ctl_num); + CHECK(build.status == MmsControlBuildStatus::type_mismatch); + CHECK(build.path == "ctlNum"); + + const auto raw_good = ControlValue::raw_mms(MmsDataValue::boolean(true)); + auto raw_result = MmsControlStructureBuilder::bind_control_value( + raw_good, scalar(MmsTypeKind::boolean, "ctlVal")); + CHECK(raw_result.success()); + raw_result = MmsControlStructureBuilder::bind_control_value( + raw_good, scalar(MmsTypeKind::integer, "ctlVal")); + CHECK(raw_result.status == MmsControlBuildStatus::type_mismatch); +} + +void step_position_and_analogue_structures_follow_live_shape() { + const auto step_specification = structure("ctlVal", { + scalar(MmsTypeKind::integer, "posVal"), + scalar(MmsTypeKind::boolean, "transInd"), + }); + auto result = MmsControlStructureBuilder::bind_control_value( + ControlValue::step_position({-3, true}), step_specification); + CHECK(result.success()); + CHECK(std::get(result.value->children()[0].value()) == -3); + CHECK(std::get(result.value->children()[1].value())); + + const auto analogue_specification = structure("ctlVal", { + scalar(MmsTypeKind::floating_point, "f"), + scalar(MmsTypeKind::boolean, "qualityHint"), + }); + result = MmsControlStructureBuilder::bind_control_value( + ControlValue::floating_point(12.5), analogue_specification); + CHECK(result.success()); + CHECK(std::get(result.value->children()[0].value()) == 12.5); + CHECK(!std::get(result.value->children()[1].value())); +} + +} // namespace + +int main() { + try { + dpc_binding_matches_network_bit_order(); + operate_matches_csharp_structure_contract_and_golden_wire(); + sbow_uses_same_exact_sequence_contract(); + cancel_allows_optional_check_but_not_vendor_guessing(); + exact_required_fields_and_check_shape_fail_closed(); + origin_ctl_num_and_raw_values_are_live_type_checked(); + step_position_and_analogue_structures_follow_live_shape(); + std::cout << "Control MMS structure tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From e20060490aa3d4490e24a90d02883cb277e612c8 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 10 Aug 2026 19:38:21 +0700 Subject: [PATCH 5/6] Add strict control MMS structure validation profile --- tests/control_mms_profile/CMakeLists.txt | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/control_mms_profile/CMakeLists.txt diff --git a/tests/control_mms_profile/CMakeLists.txt b/tests/control_mms_profile/CMakeLists.txt new file mode 100644 index 000000000..827736de3 --- /dev/null +++ b/tests/control_mms_profile/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.20) + +project(ARIEC61850ControlMmsProfile + VERSION 0.1.0 + DESCRIPTION "Live-type IEC 61850 Oper/SBOw/Cancel MMS structure validation" + LANGUAGES CXX) + +set(ARIEC61850_ROOT "${CMAKE_CURRENT_LIST_DIR}/../..") + +add_executable(ariec61850_control_mms_structure_tests + ${ARIEC61850_ROOT}/tests/test_control_mms_structure.cpp + ${ARIEC61850_ROOT}/src/asn1/ber.cpp + ${ARIEC61850_ROOT}/src/mms/utc_time.cpp + ${ARIEC61850_ROOT}/src/mms/data_value.cpp + ${ARIEC61850_ROOT}/src/mms/data_codec.cpp + ${ARIEC61850_ROOT}/src/control/guarded_control.cpp + ${ARIEC61850_ROOT}/src/control/mms_control_structure.cpp) + +target_compile_features(ariec61850_control_mms_structure_tests PRIVATE cxx_std_20) +target_include_directories(ariec61850_control_mms_structure_tests PRIVATE + ${ARIEC61850_ROOT}/include) + +if(MSVC) + target_compile_options(ariec61850_control_mms_structure_tests PRIVATE + /W4 /WX /permissive-) +else() + target_compile_options(ariec61850_control_mms_structure_tests PRIVATE + -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Werror) +endif() + +enable_testing() +add_test( + NAME ariec61850_control_mms_structure_tests + COMMAND ariec61850_control_mms_structure_tests) + +message(STATUS "arstack61850 Control MMS profile: live TypeSpecification binding=ON") +message(STATUS "arstack61850 Control MMS profile: Oper/SBOw/Cancel exact fields=ON") +message(STATUS "arstack61850 Control MMS profile: DPC network-bit-order oracle=ON") +message(STATUS "arstack61850 Control MMS profile: Check 2-bit oracle=ON") +message(STATUS "arstack61850 Control MMS profile: vendor-specific field guessing=OFF") From 1de987f081e1a79ecf258275a7cd76c40db2537d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 10 Aug 2026 19:38:35 +0700 Subject: [PATCH 6/6] Gate control MMS structures on GCC and Clang --- .github/workflows/control-mms-profile-ci.yml | 64 ++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/control-mms-profile-ci.yml diff --git a/.github/workflows/control-mms-profile-ci.yml b/.github/workflows/control-mms-profile-ci.yml new file mode 100644 index 000000000..86ef42d47 --- /dev/null +++ b/.github/workflows/control-mms-profile-ci.yml @@ -0,0 +1,64 @@ +name: Control MMS Profile CI + +on: + push: + branches: + - main + pull_request: + paths: + - 'include/ariec61850/control/**' + - 'src/control/**' + - 'tests/test_control_mms_structure.cpp' + - 'tests/control_mms_profile/**' + - 'include/ariec61850/mms/data_value.hpp' + - 'include/ariec61850/mms/services.hpp' + - 'src/mms/data_value.cpp' + - 'src/mms/data_codec.cpp' + - 'src/mms/utc_time.cpp' + - '.github/workflows/control-mms-profile-ci.yml' + +permissions: + contents: read + +jobs: + control-mms-profile: + name: Control MMS / ${{ matrix.compiler }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + compiler: [gcc, clang] + + steps: + - uses: actions/checkout@v4 + + - name: Select compiler + shell: bash + run: | + if [ "${{ matrix.compiler }}" = "clang" ]; then + echo "CC=clang" >> "$GITHUB_ENV" + echo "CXX=clang++" >> "$GITHUB_ENV" + else + echo "CC=gcc" >> "$GITHUB_ENV" + echo "CXX=g++" >> "$GITHUB_ENV" + fi + + - name: Configure control MMS profile + run: cmake -S tests/control_mms_profile -B build-control-mms + + - name: Build control MMS profile + run: cmake --build build-control-mms --parallel + + - name: Run control MMS golden-wire tests + run: >- + ctest --test-dir build-control-mms + --output-on-failure + --output-junit control-mms-ctest.xml + + - name: Upload control MMS evidence + uses: actions/upload-artifact@v4 + with: + name: control-mms-${{ matrix.compiler }}-${{ github.event.pull_request.head.sha || github.sha }} + if-no-files-found: error + retention-days: 14 + path: build-control-mms/control-mms-ctest.xml