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
61 changes: 61 additions & 0 deletions .github/workflows/mms-r12-server-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: MMS R1-R2 Server CI

on:
pull_request:
paths:
- 'include/ariec61850/acse/**'
- 'include/ariec61850/mms/**'
- 'src/acse/**'
- 'src/mms/**'
- 'tools/mms_direct_control_server.cpp'
- 'tools/mms_urcb_server.cpp'
- 'tools/mms_direct_control_server/**'
- 'tests/test_mms_static_direct_control.cpp'
- 'tests/test_mms_server_urcb_report.cpp'
- 'scripts/build_mms_direct_control_server.ps1'
- '.github/workflows/mms-r12-server-ci.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
linux:
name: Linux ${{ 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
run: cmake -S tools/mms_direct_control_server -B build-mms-r12 -DCMAKE_BUILD_TYPE=Release
- name: Build
run: cmake --build build-mms-r12 --parallel
- name: Test
run: ctest --test-dir build-mms-r12 --output-on-failure
- name: Help contracts
shell: bash
run: |
./build-mms-r12/ariec61850_mms_direct_control_server --help
./build-mms-r12/ariec61850_mms_urcb_server --help

windows:
name: Windows MSVC
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Configure, build, test and locate executables
shell: powershell
run: .\scripts\build_mms_direct_control_server.ps1 -BuildDir build-mms-r12
115 changes: 114 additions & 1 deletion embedded/mms_dispatcher_hard_profile_smoke.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <cstddef>
#include <cstdint>
#include <span>
#include <string_view>

namespace {

Expand All @@ -28,6 +29,12 @@ constexpr std::array<std::uint8_t, 21U> kDomainContinueRequest{
0xA1U, 0x02U, 0x80U, 0x00U,
0x82U, 0x03U, 0x4CU, 0x44U, 0x30U};

constexpr std::array<std::uint8_t, 19U> kNamedVariableDirectoryRequest{
0xA0U, 0x11U, 0x02U, 0x01U, 0x16U,
0xA1U, 0x0CU,
0xA0U, 0x03U, 0x80U, 0x01U, 0x00U,
0xA1U, 0x05U, 0x81U, 0x03U, 0x4CU, 0x44U, 0x48U};

constexpr std::array<std::uint8_t, 17U> kDomainFirstResponse{
0xA1U, 0x0FU, 0x02U, 0x01U, 0x07U,
0xA1U, 0x0AU, 0xA0U, 0x05U,
Expand Down Expand Up @@ -92,6 +99,21 @@ template <std::size_t N>
std::equal(actual.begin(), actual.end(), expected.begin());
}

[[nodiscard]] bool identifier_equals(
const std::span<const std::uint8_t> identifier,
const std::string_view expected) noexcept {
if (identifier.size() != expected.size()) {
return false;
}
for (std::size_t index = 0U; index < identifier.size(); ++index) {
if (identifier[index] != static_cast<std::uint8_t>(
static_cast<unsigned char>(expected[index]))) {
return false;
}
}
return true;
}

[[nodiscard]] wire::EncodeResult read_boolean(
const void* context,
const std::span<std::uint8_t> destination) noexcept {
Expand Down Expand Up @@ -307,12 +329,103 @@ int main() {
return 16;
}

// ARIEC61850 IED simulator parity: when a root Logical Node object exists,
// GetNameList(NamedVariable) advertises the root and leaves its flattened
// FC/DO/DA aliases for Read/Write lookup and TypeSpecification traversal.
// A flat-only generic MMS item remains discoverable for compatibility.
bool hierarchy_value = true;
const std::array<mms::MmsStaticObjectEntry, 5U> hierarchy_objects{
mms::MmsStaticObjectEntry{
"LDH", "LLN0", kBooleanType,
read_boolean, &hierarchy_value, false},
mms::MmsStaticObjectEntry{
"LDH", "LLN0$ST$Mod$stVal", kBooleanType,
read_boolean, &hierarchy_value, false},
mms::MmsStaticObjectEntry{
"LDH", "GGIO1", kBooleanType,
read_boolean, &hierarchy_value, false},
mms::MmsStaticObjectEntry{
"LDH", "GGIO1$ST$Ind1$stVal", kBooleanType,
read_boolean, &hierarchy_value, false},
mms::MmsStaticObjectEntry{
"LDH", "Orphan$ST$stVal", kBooleanType,
read_boolean, &hierarchy_value, false}};
const mms::MmsStaticObjectTable hierarchy_table{hierarchy_objects};
const mms::MmsStaticDispatchPolicy hierarchy_policy{
8U,
1U,
10U,
3U,
10U};
const mms::MmsStaticApplicationDispatcher hierarchy_dispatcher{
hierarchy_table,
hierarchy_policy};
if (!hierarchy_table.valid()) {
return 17;
}

dispatched = hierarchy_dispatcher.dispatch(
kNamedVariableDirectoryRequest,
response,
workspace);
mms::MmsGetNameListResponseView hierarchy_directory;
if (!dispatched.success() ||
!mms::MmsServiceSpanCodec::try_decode_get_name_list_response(
std::span<const std::uint8_t>{response}.first(dispatched.bytes_written),
hierarchy_directory) ||
hierarchy_directory.invoke_id != 22U ||
hierarchy_directory.identifier_count != 3U ||
hierarchy_directory.more_follows) {
return 18;
}

std::span<const std::uint8_t> identifier;
if (!hierarchy_directory.try_identifier(0U, identifier) ||
!identifier_equals(identifier, "LLN0") ||
!hierarchy_directory.try_identifier(1U, identifier) ||
!identifier_equals(identifier, "GGIO1") ||
!hierarchy_directory.try_identifier(2U, identifier) ||
!identifier_equals(identifier, "Orphan$ST$stVal")) {
return 19;
}

auto dual_directory_policy = hierarchy_policy;
dual_directory_policy.advertise_flattened_child_aliases = true;
const mms::MmsStaticApplicationDispatcher dual_directory_dispatcher{
hierarchy_table,
dual_directory_policy};
dispatched = dual_directory_dispatcher.dispatch(
kNamedVariableDirectoryRequest,
response,
workspace);
mms::MmsGetNameListResponseView dual_directory;
if (!dispatched.success() ||
!mms::MmsServiceSpanCodec::try_decode_get_name_list_response(
std::span<const std::uint8_t>{response}.first(dispatched.bytes_written),
dual_directory) ||
dual_directory.identifier_count != 5U ||
dual_directory.more_follows) {
return 20;
}
constexpr std::array<std::string_view, 5U> dual_names{
"LLN0",
"LLN0$ST$Mod$stVal",
"GGIO1",
"GGIO1$ST$Ind1$stVal",
"Orphan$ST$stVal"};
for (std::size_t index = 0U; index < dual_names.size(); ++index) {
if (!dual_directory.try_identifier(index, identifier) ||
!identifier_equals(identifier, dual_names[index])) {
return 21;
}
}

for (std::uint32_t iteration = 0U; iteration < 20'000U; ++iteration) {
relay_state = true;
const auto read = dispatcher.dispatch(kReadRequest, response, workspace);
const auto write = dispatcher.dispatch(kWriteRequest, response, workspace);
if (!read.success() || !write.success() || relay_state) {
return 17;
return 22;
}
}

Expand Down
11 changes: 11 additions & 0 deletions include/ariec61850/acse/association_span.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,21 @@ class AcseSpanCodec final {
std::span<const std::uint8_t> bytes,
AcseAarqView& aarq) noexcept;

// Strict protocol decoder used by conformance-oriented/offline paths.
[[nodiscard]] static bool try_decode_association_request_view(
std::span<const std::uint8_t> bytes,
AssociationRequestView& request) noexcept;

// Bounded engineering-client compatibility decoder. It prefers the strict
// path, then falls back to the tolerant association-inspection behavior
// used by the proven ARIEC61850 simulator/server: Session Connect,
// Presentation contexts, AARQ UserInformation and MMS InitiateRequest are
// required, while harmless optional Presentation/ACSE decorations are
// ignored rather than turning into a disconnect.
[[nodiscard]] static bool try_decode_association_request_compat_view(
std::span<const std::uint8_t> bytes,
AssociationRequestView& request) noexcept;

[[nodiscard]] static constexpr std::size_t default_accept_aare_size() noexcept {
return 76U;
}
Expand Down
14 changes: 13 additions & 1 deletion include/ariec61850/mms/connection_runtime.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ struct MmsStaticConnectionPolicy final {
MmsStaticAssociationClosedCallback association_closed{};
void* association_closed_context{};

// Appended for aggregate source compatibility. When non-empty, this is the
// adapter-owned COTP Data reassembly buffer. Otherwise the runtime may
// borrow half of the stable per-session workspace while a fragmented COTP
// message is active. No heap/global storage is introduced.
std::span<std::uint8_t> cotp_reassembly{};

[[nodiscard]] constexpr MmsStaticRequestAccessContext access_context() const noexcept {
return {
association_id,
Expand Down Expand Up @@ -141,15 +147,21 @@ class MmsStaticConnectionRuntime final {

private:
void notify_association_closed() noexcept;
void clear_cotp_reassembly() noexcept;

const MmsStaticApplicationDispatcher& dispatcher_;
MmsStaticConnectionPolicy policy_{};
MmsStaticConnectionState state_{MmsStaticConnectionState::awaiting_cotp_connect};
std::uint32_t mms_presentation_context_id_{};
std::size_t negotiated_tpdu_size_bytes_{};
std::uint32_t negotiated_mms_pdu_size_{};
std::size_t cotp_reassembly_size_{};
bool cotp_reassembly_complete_{};
bool cotp_reassembly_uses_workspace_{};
std::uint8_t* cotp_reassembly_workspace_data_{};
std::size_t cotp_reassembly_workspace_size_{};
bool association_active_{};
bool association_close_notified_{};
};

} // namespace ar::iec61850::mms
} // namespace ar::iec61850::mms
22 changes: 21 additions & 1 deletion include/ariec61850/mms/pdu_span.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ enum class MmsWireConfirmedService : std::int32_t {
write = 5,
get_variable_access_attributes = 6,
get_named_variable_list_attributes = 12,
file_directory = 77,
};

enum class MmsConfirmedRequestRejectReason : std::uint8_t {
Expand Down Expand Up @@ -105,6 +106,25 @@ class MmsPduSpanCodec final {
std::uint32_t invoke_id,
MmsConfirmedRequestRejectReason reason,
std::span<std::uint8_t> destination) noexcept;

// Compatibility shims used by the isolated IED-simulator parity profile.
// They do not change the strict current capability advertisement. The
// ConfirmedError-named shim intentionally maps to the current RejectPDU
// primitive so unsupported engineering-client probes keep the association
// alive without resurrecting unsupported service claims.
[[nodiscard]] static wire::EncodeResult encode_identify_response_into(
std::uint32_t invoke_id,
std::span<std::uint8_t> destination) noexcept;

[[nodiscard]] static wire::EncodeResult encode_confirmed_error_into(
std::uint32_t invoke_id,
std::span<std::uint8_t> destination) noexcept;

[[nodiscard]] static bool is_conclude_request(
std::span<const std::uint8_t> bytes) noexcept;

[[nodiscard]] static wire::EncodeResult encode_conclude_response_into(
std::span<std::uint8_t> destination) noexcept;
};

} // namespace ar::iec61850::mms
} // namespace ar::iec61850::mms
78 changes: 78 additions & 0 deletions include/ariec61850/mms/static_direct_control.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

#include "ariec61850/mms/static_object_table.hpp"
#include "ariec61850/wire/encode_result.hpp"

#include <cstddef>
#include <cstdint>
#include <span>

namespace ar::iec61850::mms {

struct MmsStaticDirectBooleanOperate final {
bool control_value{};
std::uint8_t origin_category{};
std::uint8_t control_number{};
bool test{};
bool synchro_check{};
bool interlock_check{};
};

struct MmsStaticDirectBooleanControlState final {
std::uint8_t value{};
std::uint8_t last_control_number{};
bool last_test{};
std::size_t accepted_operations{};
std::size_t rejected_operations{};
};

using MmsStaticBooleanApplyCallback = bool (*)(
void* context,
bool value) noexcept;

struct MmsStaticDirectBooleanControlPolicy final {
bool allow_test{true};
bool allow_synchro_check{};
bool allow_interlock_check{};
std::uint32_t malformed_failure_code{7U}; // type-inconsistent
std::uint32_t invalid_value_failure_code{11U}; // object-value-invalid
std::uint32_t backend_failure_code{10U}; // object-non-existent/backend
};

struct MmsStaticDirectBooleanControlBinding final {
MmsStaticDirectBooleanControlState* state{};
MmsStaticBooleanApplyCallback apply{};
void* apply_context{};
MmsStaticDirectBooleanControlPolicy policy{};
};

// Decode one IEC 61850 Direct-with-normal-security Oper value with the exact
// ordered structure used by the live TypeSpecification exposed by this server:
// ctlVal, origin(orCat,orIdent), ctlNum, T, Test, Check.
// The parser is allocation-free and accepts only the bounded scalar shape.
[[nodiscard]] bool try_decode_static_direct_boolean_operate(
std::span<const std::uint8_t> encoded_data,
MmsStaticDirectBooleanOperate& decoded) noexcept;

// Read callbacks suitable for MmsStaticObjectEntry.
[[nodiscard]] wire::EncodeResult mms_static_direct_boolean_read_state(
const void* context,
std::span<std::uint8_t> destination) noexcept;

[[nodiscard]] wire::EncodeResult mms_static_direct_normal_read_ctl_model(
const void* context,
std::span<std::uint8_t> destination) noexcept;

[[nodiscard]] wire::EncodeResult mms_static_control_read_unavailable(
const void* context,
std::span<std::uint8_t> destination) noexcept;

// Write callback suitable for the CO$...$Oper MmsStaticObjectEntry.
// Test=true is acknowledged without changing the live output state. Check bits
// are denied by default until the application explicitly enables them.
[[nodiscard]] MmsStaticWriteResult mms_static_direct_boolean_write_oper(
void* context,
std::span<const std::uint8_t> encoded_data) noexcept;

} // namespace ar::iec61850::mms
5 changes: 5 additions & 0 deletions include/ariec61850/mms/static_dispatcher.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ struct MmsStaticDispatchPolicy final {
std::uint32_t missing_object_failure_code{10U};
std::uint32_t access_denied_failure_code{3U};
std::uint32_t backend_failure_code{10U};
// Some live discovery clients still derive the Logical Node root from
// flattened LN$FC$DO$DA names before requesting the root TypeSpecification.
// Keep strict IED-simulator root-only discovery as the default, while
// allowing host interoperability adapters to advertise both forms.
bool advertise_flattened_child_aliases{};
};

struct MmsStaticDispatchResult final {
Expand Down
Loading
Loading