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
64 changes: 64 additions & 0 deletions .github/workflows/control-mms-profile-ci.yml
Original file line number Diff line number Diff line change
@@ -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
146 changes: 146 additions & 0 deletions include/ariec61850/control/mms_control_structure.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

#include "ariec61850/control/guarded_control.hpp"
#include "ariec61850/mms/services.hpp"

#include <cstdint>
#include <optional>
#include <span>
#include <string>
#include <utility>
#include <variant>
#include <vector>

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<mms::MmsDataValue> 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<std::uint8_t> origin_identifier;
std::uint8_t control_number{};
std::optional<mms::MmsDataValue> timestamp;
std::optional<mms::MmsDataValue> 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
Loading
Loading