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

on:
push:
branches: ["agent/mms-server-direct-normal-control"]
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
cc: gcc
cxx: g++
- os: ubuntu-latest
cc: clang
cxx: clang++
- os: windows-latest
cc: ""
cxx: ""
runs-on: ${{ matrix.os }}
env:
CC: ${{ matrix.cc }}
CXX: ${{ matrix.cxx }}
steps:
- uses: actions/checkout@v4
- name: Configure
run: cmake -S tools/mms_direct_control_server -B build-mms-direct-control -DCMAKE_BUILD_TYPE=Release
- name: Build
run: cmake --build build-mms-direct-control --config Release --parallel
- name: Unit and help contract
run: ctest --test-dir build-mms-direct-control -C Release --output-on-failure
113 changes: 113 additions & 0 deletions docs/MMS_SERVER_DIRECT_CONTROL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# MMS Server Direct-Normal Control Trial

This slice advances the clean MMS server prototype from read-only browse/read to a bounded, explicitly writable IEC 61850 control target.

## Implemented server model

Domain: `ESP32S3IOLD0`

Logical Nodes:

- `LLN0`
- `LPHD1`
- `GGIO1`

Read-only digital inputs:

- `GGIO1.Ind1` .. `GGIO1.Ind8`

Direct-with-normal-security Boolean controls:

- `ESP32S3IOLD0/GGIO1.SPCSO1`
- `ESP32S3IOLD0/GGIO1.SPCSO2`
- ...
- `ESP32S3IOLD0/GGIO1.SPCSO8`

Each control exposes:

- `ST$SPCSO<n>$stVal`
- `CF$SPCSO<n>$ctlModel` = `1` (Direct-with-normal-security)
- `CO$SPCSO<n>$Oper`

The live `Oper` TypeSpecification is the bounded SPC shape:

1. `ctlVal` Boolean
2. `origin` structure (`orCat` Unsigned, `orIdent` OctetString)
3. `ctlNum` Unsigned
4. `T` UTC time
5. `Test` Boolean
6. `Check` 2-bit BitString

A static non-deletable DataSet is also exposed:

`ESP32S3IOLD0/LLN0.Outputs`

It contains the eight `SPCSO<n>.stVal` status leaves and is intended to become the first static reporting DataSet in the next server-side RCB tranche.

## Safety semantics

The `Oper` decoder is allocation-free and rejects malformed or unexpected structures.

- `ctlNum=0` is rejected.
- origin category outside the bounded IEC range is rejected.
- synchro/interlock Check requests are fail-closed by default because this prototype does not yet implement those plant checks.
- `Test=true` is accepted but does not change the live output state.
- an application/hardware apply callback can reject the command before the published status changes.
- there is no command retry.

This tranche implements only Direct-with-normal-security. SBO and enhanced-security CommandTermination belong to later server slices.

## Windows build

```powershell
powershell.exe -ExecutionPolicy Bypass -File .\scripts\build_mms_direct_control_server.ps1
```

The script configures, builds, and runs the direct-control unit/help contract.

## Run

Use a non-privileged lab port first:

```powershell
.\build-mms-direct-control\Release\ariec61850_mms_direct_control_server.exe `
--bind 0.0.0.0 `
--port 8102 `
--di-mask 0x55 `
--do-mask 0x00
```

Expected startup marker:

```text
MMS_DIRECT_CONTROL_SERVER_READY ... controls=8 ctlModel=1 dataset=ESP32S3IOLD0/LLN0.Outputs
```

For a standards-port laboratory run, use TCP 102 only when the host privileges/firewall configuration allow it.

## IEDScout trial

Connect IEDScout as an MMS/IEC 61850 client to the host running this executable.

Browse `ESP32S3IOLD0/GGIO1` and verify that `SPCSO1..8` expose `ctlModel=1`, `Oper`, and `stVal`. Operate one Boolean output and then read its `stVal`.

After the client disconnects, the server prints a summary such as:

```text
MMS_DIRECT_CONTROL_STATE doMask=1 acceptedOps=1 rejectedOps=0
```

Retain terminal output and a PCAP/PCAPNG for interoperability evidence. This is simulator/server interoperability evidence, not IEC 61850 conformance certification.

## Current boundary / next server tranche

Still pending after this slice:

1. server-side URCB/BRCB objects and InformationReport transmission;
2. static DataSet report binding and GI/event triggers;
3. SBO normal and enhanced control plus CommandTermination/LastApplError;
4. concurrent-client ownership/contention;
5. server-side FileDirectory/FileOpen/FileRead/FileClose;
6. GPIO/lwIP adapter on the target ESP board;
7. runtime SCL/CID-driven model generation;
8. TLS/security profiles and broader multi-vendor evidence.
107 changes: 107 additions & 0 deletions docs/MMS_SERVER_URCB_REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# MMS Server Static URCB / InformationReport Trial

This tranche adds a runnable server-side unbuffered report path on top of the bounded MMS server core.

## Model

Domain: `ESP32S3IOLD0`

Static DataSet:

`ESP32S3IOLD0/LLN0.Outputs`

Members, in fixed order:

- `GGIO1$ST$SPCSO1$stVal`
- `GGIO1$ST$SPCSO2$stVal`
- `GGIO1$ST$SPCSO3$stVal`
- `GGIO1$ST$SPCSO4$stVal`
- `GGIO1$ST$SPCSO5$stVal`
- `GGIO1$ST$SPCSO6$stVal`
- `GGIO1$ST$SPCSO7$stVal`
- `GGIO1$ST$SPCSO8$stVal`

URCB:

`ESP32S3IOLD0/LLN0$RP$Outputs01`

Default `RptID`: `ARSTACK-Outputs01`.

The object bank exposes `RptID`, `RptEna`, `Resv`, `DatSet`, `ConfRev`, `OptFlds`, `BufTm`, `TrgOps`, `IntgPd`, `GI`, and `SqNum` as MMS variables.

## First acceptance profile

The first live gate is deliberately deterministic and GI-driven:

1. associate;
2. inspect the static DataSet and URCB;
3. reserve the URCB if the client uses `Resv`;
4. set `RptEna=true`;
5. set `GI=true`;
6. receive one MMS `InformationReport` carrying all eight ordered DataSet values;
7. verify `SqNum` advances only after a complete report frame is encoded;
8. disable/release and disconnect.

The initial `OptFlds` enable sequence number, reason, DataSet name, data references, and configuration revision. Report time is intentionally omitted in this first profile so timestamp semantics do not obscure the basic interoperability gate.

## Safety and ownership boundary

This executable currently serves one client at a time.

`RptEna` and `Resv` are explicitly reset to false at connection boundaries so one disconnected client cannot leave a live reservation/enable state for the next client.

No dynamic DataSet mutation is available on this server. `LLN0.Outputs` is static and non-deletable.

No automatic report-control takeover or retry is performed.

## Build

```powershell
powershell.exe -ExecutionPolicy Bypass -File .\scripts\build_mms_direct_control_server.ps1
```

The helper builds/tests both server trial executables.

## Run

```powershell
.\build-mms-direct-control\Release\ariec61850_mms_urcb_server.exe `
--bind 0.0.0.0 `
--port 8102 `
--mask 0x55
```

Expected startup marker:

```text
MMS_URCB_SERVER_READY ... dataset=ESP32S3IOLD0/LLN0.Outputs urcb=ESP32S3IOLD0/LLN0$RP$Outputs01 members=8 ...
```

On each emitted report the server prints:

```text
MMS_URCB_REPORT_SENT index=0 sqNum=<n> reason=8 bytes=<n>
```

Reason `8` is General Interrogation.

For TCP 102, use a laboratory host configuration that permits binding the standard MMS port.

## IEDScout trial

Connect IEDScout to the server host and browse `ESP32S3IOLD0`.

Verify the `LLN0.Outputs` DataSet and `LLN0$RP$Outputs01` URCB are visible. Enable the report and issue GI. The expected evidence is one report with eight Boolean values matching the configured `--mask` bit order.

Retain console output and PCAP/PCAPNG. Until this live run is performed, this tranche is classified as software/offline-tested rather than live-proven server interoperability.

## Still pending

- event-on-change/data-change triggering;
- multiple URCBs and concurrent-client ownership;
- BRCB retention/replay/EntryID/PurgeBuf on the runnable server adapter;
- Direct/SBO enhanced server control and CommandTermination;
- server-side MMS file services;
- GPIO/lwIP hardware adapter;
- runtime SCL/CID model generation;
- TLS/security profiles and broader multi-vendor evidence.
64 changes: 64 additions & 0 deletions include/ariec61850/mms/information_report_span.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

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

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

namespace ar::iec61850::mms {

struct MmsInformationReportReferenceInput final {
std::string_view domain;
std::string_view item;
};

struct MmsInformationReportSnapshotInput final {
std::string_view report_id;
std::span<const std::uint8_t> optional_fields;
std::uint32_t sequence_number{};
std::span<const std::uint8_t> report_time;
MmsInformationReportReferenceInput data_set_reference{};
bool buffered{};
bool buffer_overflow{};
std::span<const std::uint8_t> entry_id;
std::uint32_t conf_revision{};
std::span<const MmsInformationReportReferenceInput> member_references;
std::span<const MmsReadAccessResultInput> member_results;
std::uint8_t reason_for_inclusion{};
};

struct MmsInformationReportView final {
MmsObjectNameView variable_list_name{};
std::span<const std::uint8_t> access_result_list{};
std::size_t item_count{};

[[nodiscard]] bool try_item(
std::size_t index,
MmsReadAccessResultView& result) const noexcept;
};

class MmsInformationReportSpanCodec final {
public:
static constexpr std::size_t maximum_members = MmsServiceSpanCodec::maximum_variables;
static constexpr std::size_t maximum_reference_bytes = 129U;
static constexpr std::size_t maximum_report_id_bytes = 129U;
static constexpr std::size_t optional_field_bytes = 2U;
static constexpr std::size_t binary_time_bytes = 6U;
static constexpr std::size_t entry_id_bytes = 8U;
static constexpr std::size_t maximum_access_results =
(3U * maximum_members) + 16U;

[[nodiscard]] static bool try_decode_information_report(
std::span<const std::uint8_t> mms_pdu,
MmsInformationReportView& report) noexcept;

[[nodiscard]] static wire::EncodeResult encode_snapshot_into(
const MmsInformationReportSnapshotInput& report,
std::span<std::uint8_t> destination) noexcept;
};

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