Skip to content
Draft
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
67 changes: 67 additions & 0 deletions apps/ied_simulator/qml/BrowserDataModelPane.qml
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,73 @@ Rectangle {
}
}

// Semantic fields are present only for verified typed MMS
// quality/UTC values. No false flags for unread/offline data.
Repeater {
model: [
{heading: "Quality Details", data: root.currentNode.qualityDetails},
{heading: "Time Quality", data: root.currentNode.timestampDetails}
]
delegate: ColumnLayout {
id: semanticSection
required property int index
required property var modelData
visible: modelData.data !== undefined &&
modelData.data.fields !== undefined
Layout.fillWidth: true
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: visible ? 24 : 0
color: theme.surfaceRaised
Label {
anchors.fill: parent
leftPadding: 8
verticalAlignment: Text.AlignVCenter
text: modelData.heading + " · MMS Read"
font.pixelSize: 10
font.weight: Font.DemiBold
color: theme.text
}
}
Repeater {
model: semanticSection.modelData.data &&
semanticSection.modelData.data.fields
? semanticSection.modelData.data.fields : []
delegate: Rectangle {
required property int index
required property var modelData
Layout.fillWidth: true
Layout.preferredHeight: 25
color: index % 2 ? theme.surfaceSoft : theme.surface
RowLayout {
anchors.fill: parent
anchors.leftMargin: 15
anchors.rightMargin: 8
Label {
text: modelData.label
Layout.preferredWidth: Math.max(150, root.width * 0.34)
color: theme.textSoft
font.pixelSize: 10
}
Label {
text: root.text(modelData.value)
Layout.fillWidth: true
color: theme.text
font.pixelSize: 10
}
}
Rectangle {
anchors.bottom: parent.bottom
width: parent.width
height: 1
color: theme.lineSoft
}
}
}
}
}

Label {
visible: !root.currentNode.kind
Layout.fillWidth: true
Expand Down
8 changes: 7 additions & 1 deletion apps/ied_simulator/qml/BrowserDataSetPane.qml
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,13 @@ Rectangle {
}
Label {
Layout.preferredWidth: Math.max(180, root.width * 0.42)
text: root.displayValue(attributeRow.modelData.value)
text: attributeRow.modelData.qualityDetails &&
attributeRow.modelData.qualityDetails.summary
? attributeRow.modelData.qualityDetails.summary
: attributeRow.modelData.timestampDetails &&
attributeRow.modelData.timestampDetails.summary
? attributeRow.modelData.timestampDetails.summary
: root.displayValue(attributeRow.modelData.value)
color: theme.textSoft
font.pixelSize: 10
elide: Text.ElideRight
Expand Down
13 changes: 8 additions & 5 deletions apps/ied_simulator/src/MmsClientController.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "MmsClientController.hpp"
#include "MmsSclClientProjection.hpp"
#include "MmsSemanticValue.hpp"

#include "ariec61850/mms/data_codec.hpp"
#include "ariec61850/mms/live_discovery.hpp"
Expand Down Expand Up @@ -34,6 +35,7 @@ std::span<const std::uint8_t> responsePayload(const mms::MmsConfirmedExchangeRes
struct ReadValue final {
QString key;
QString display;
QVariantMap typedEvidence;
};

QVector<ReadValue> readTargets(
Expand Down Expand Up @@ -67,7 +69,8 @@ QVector<ReadValue> readTargets(
if (!result.success()) continue;
values.push_back({
targets.at(index).key,
QString::fromStdString(mms::MmsDataCodec::to_display_string(*result.value))});
QString::fromStdString(mms::MmsDataCodec::to_display_string(*result.value)),
arstack::iedsim::decodeMmsSemanticAttribute(*result.value, targets.at(index).item)});
}
if (values.isEmpty()) throw std::runtime_error("MMS Read returned no successful AccessResult.");
return values;
Expand Down Expand Up @@ -517,10 +520,10 @@ void MmsClientController::startRead(
QMetaObject::invokeMethod(self, [self, generation, values, operationName] {
if (!self || self->generation_ != generation) return;
for (const auto& value : values) {
self->treeModel_.applyReadValue(value.key, value.display);
self->treeModel_.applyReadValue(value.key, value.display, value.typedEvidence);
if (self->engineeringContext_) {
self->engineeringContext_->treeModel()->applyReadValue(
value.key, value.display);
value.key, value.display, value.typedEvidence);
}
}
self->setOperationBusy(false);
Expand Down Expand Up @@ -670,10 +673,10 @@ bool MmsClientController::writeSelected(const QString& textValue) {
QMetaObject::invokeMethod(self, [self, generation, reference, values] {
if (!self || self->generation_ != generation) return;
for (const auto& read : values) {
self->treeModel_.applyReadValue(read.key, read.display);
self->treeModel_.applyReadValue(read.key, read.display, read.typedEvidence);
if (self->engineeringContext_) {
self->engineeringContext_->treeModel()->applyReadValue(
read.key, read.display);
read.key, read.display, read.typedEvidence);
}
}
self->setOperationBusy(false);
Expand Down
30 changes: 28 additions & 2 deletions apps/ied_simulator/src/MmsLiveTreeModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ void MmsLiveTreeModel::applyDocument(const ar::iec61850::mms::MmsLiveModelDocume
visible_.clear();
mmsIndex_.clear();
referenceIndex_.clear();
typedEvidence_.clear();
selectedNode_ = -1;

Node root;
Expand Down Expand Up @@ -228,6 +229,7 @@ void MmsLiveTreeModel::clear() {
visible_.clear();
mmsIndex_.clear();
referenceIndex_.clear();
typedEvidence_.clear();
selectedNode_ = -1;
endResetModel();
emit countsChanged();
Expand Down Expand Up @@ -348,6 +350,26 @@ QVariantMap MmsLiveTreeModel::nodeMap(const int nodeIndex) const {
result.insert(QStringLiteral("childCount"), node.children.size());
result.insert(QStringLiteral("quality"), siblingValue(nodeIndex, QStringLiteral("q")));
result.insert(QStringLiteral("timestamp"), siblingValue(nodeIndex, QStringLiteral("t")));
// Typed evidence is held only for MMS Read-verified q/t leaves. The
// inspector borrows it from a sibling under the same canonical DO.
if (node.kind == NodeKind::dataAttribute && node.parent >= 0) {
const auto borrow = [this, nodeIndex](const QString& wanted) -> QVariantMap {
const auto& selected = nodes_.at(nodeIndex);
const auto matching = [this, &wanted](int index) {
const auto& candidate = nodes_.at(index);
return candidate.label.compare(wanted, Qt::CaseInsensitive) == 0
|| leafName(candidate.attributePath).compare(wanted, Qt::CaseInsensitive) == 0;
};
if (matching(nodeIndex)) return typedEvidence_.value(nodeIndex);
for (const int sibling : nodes_.at(selected.parent).children)
if (matching(sibling)) return typedEvidence_.value(sibling);
return {};
};
const auto q = borrow(QStringLiteral("q"));
const auto t = borrow(QStringLiteral("t"));
if (!q.isEmpty()) result.insert(QStringLiteral("qualityDetails"), q);
if (!t.isEmpty()) result.insert(QStringLiteral("timestampDetails"), t);
}
result.insert(QStringLiteral("writable"), node.writable);
result.insert(QStringLiteral("readable"), node.kind == NodeKind::dataAttribute && !node.item.isEmpty());
QString objectReference;
Expand Down Expand Up @@ -659,13 +681,17 @@ QVector<MmsLiveTreeModel::ReadTarget> MmsLiveTreeModel::readTargetsForReferences
return result;
}

void MmsLiveTreeModel::applyReadValue(const QString& key, const QString& displayValue) {
void MmsLiveTreeModel::applyReadValue(
const QString& key, const QString& displayValue, const QVariantMap& typedEvidence) {
const auto found = mmsIndex_.constFind(key);
if (found == mmsIndex_.cend()) return;
const auto nodeIndex = found.value();
auto& node = nodes_[nodeIndex];
if (node.value == displayValue) return;
const bool evidenceChanged = typedEvidence_.value(nodeIndex) != typedEvidence;
if (node.value == displayValue && !evidenceChanged) return;
node.value = displayValue;
if (!typedEvidence.isEmpty()) typedEvidence_.insert(nodeIndex, typedEvidence);
else typedEvidence_.remove(nodeIndex);
detailModel_->nodeValueChanged(nodeIndex);
emit valuesChanged();
const auto row = visibleRowForNode(nodeIndex);
Expand Down
5 changes: 4 additions & 1 deletion apps/ied_simulator/src/MmsLiveTreeModel.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ class MmsLiveTreeModel : public QAbstractListModel {
[[nodiscard]] QVector<ReadTarget> readTargetsForReferences(
const QStringList& references,
int maximumTargets = 64) const;
void applyReadValue(const QString& key, const QString& displayValue);
void applyReadValue(const QString& key, const QString& displayValue, const QVariantMap& typedEvidence = {});

signals:
void filterTextChanged();
Expand Down Expand Up @@ -211,6 +211,9 @@ class MmsLiveTreeModel : public QAbstractListModel {
QVector<int> visible_;
QHash<QString, int> mmsIndex_;
QHash<QString, QVector<int>> referenceIndex_;
// Sparse: only successfully decoded live q/t leaves own a map.
// Do not allocate expensive QVariantMaps for every discovered node.
QHash<int, QVariantMap> typedEvidence_;
QString filterText_;
int selectedNode_{-1};
static constexpr int maximumProjectedChildren_{4096};
Expand Down
81 changes: 81 additions & 0 deletions apps/ied_simulator/src/MmsSemanticValue.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

#include "ariec61850/mms/data_value.hpp"

#include <QDateTime>
#include <QString>
#include <QVariantList>
#include <QVariantMap>

#include <chrono>
#include <cstdint>
#include <variant>

// Typed IEC 61850 evidence, shared by Browser Data Model/DataSets.
// Never parse hex display text or infer quality flags from missing reads.
namespace arstack::iedsim {

inline QVariantMap decodeMmsSemanticAttribute(
const ar::iec61850::mms::MmsDataValue& data, const QString& mmsItem) {
namespace mms = ar::iec61850::mms;
const QString leaf = mmsItem.mid(mmsItem.lastIndexOf(QLatin1Char('$')) + 1);
const auto append = [](QVariantList& fields, const QString& name, const QVariant& value) {
fields.append(QVariantMap{{QStringLiteral("label"), name},
{QStringLiteral("value"), value}});
};
if (leaf == QStringLiteral("q") && data.kind() == mms::MmsDataKind::bit_string) {
const auto& raw = data.raw_value(); // BER content: unused-bit count + payload
if (raw.size() != 3U || raw[0] != 3U || (raw[2] & 0x07U) != 0U)
return {}; // quality is exactly 13 meaningful bits; invalid evidence stays unknown
const std::uint16_t bits = (std::uint16_t(raw[1]) << 8U) | raw[2];
const auto active = [bits](unsigned shift) -> bool {
return (bits & (std::uint16_t(0x8000U) >> shift)) != 0U;
};
const unsigned validity = (active(0) ? 2U : 0U) | (active(1) ? 1U : 0U);
const QString validityName = validity == 0U ? QStringLiteral("good")
: validity == 1U ? QStringLiteral("invalid")
: validity == 2U ? QStringLiteral("reserved")
: QStringLiteral("questionable");
QVariantList fields;
append(fields, QStringLiteral("Validity"), validityName);
constexpr const char* labels[] = {
"Overflow", "OutOfRange", "BadReference", "Oscillatory",
"Failure", "OldData", "Inconsistent", "Inaccurate"
};
for (unsigned offset = 0U; offset < 8U; ++offset)
append(fields, QString::fromLatin1(labels[offset]), active(offset + 2U));
append(fields, QStringLiteral("Source"),
active(10U) ? QStringLiteral("substituted") : QStringLiteral("process"));
append(fields, QStringLiteral("Test"), active(11U));
append(fields, QStringLiteral("OperatorBlocked"), active(12U));
return {{QStringLiteral("kind"), QStringLiteral("quality")},
{QStringLiteral("summary"), validityName},
{QStringLiteral("evidence"), QStringLiteral("MMS Read")},
{QStringLiteral("fields"), fields}};
}
if (leaf == QStringLiteral("t") && data.kind() == mms::MmsDataKind::utc_time) {
const auto* utc = std::get_if<mms::Iec61850UtcTime>(&data.value());
if (!utc) return {};
const auto epochMs = std::chrono::duration_cast<std::chrono::milliseconds>(
utc->value.time_since_epoch()).count();
const QString timeIso = QDateTime::fromMSecsSinceEpoch(epochMs, Qt::UTC)
.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz 'UTC'"));
const unsigned accuracy = utc->quality & 0x1FU;
QVariantList fields;
append(fields, QStringLiteral("UTC time"), timeIso);
append(fields, QStringLiteral("LeapSecondsKnown"), (utc->quality & 0x80U) != 0U);
append(fields, QStringLiteral("ClockFailure"), (utc->quality & 0x40U) != 0U);
append(fields, QStringLiteral("ClockNotSynchronized"), (utc->quality & 0x20U) != 0U);
append(fields, QStringLiteral("TimeAccuracy"),
accuracy == 31U ? QStringLiteral("unspecified")
: QString::number(accuracy));
return {{QStringLiteral("kind"), QStringLiteral("timestamp")},
{QStringLiteral("summary"), timeIso},
{QStringLiteral("evidence"), QStringLiteral("MMS Read")},
{QStringLiteral("fields"), fields}};
}
return {};
}

} // namespace arstack::iedsim
Loading
Loading