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
25 changes: 6 additions & 19 deletions MainWindow.IedBrowser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -311,28 +311,15 @@ private void RefreshIedBrowserVisibleValues()
row.Value = "Not read";
continue;
}
if (row.EvidenceField.Length == 0)
{
// The parent q and its inline children share one IEC Quality
// presentation path. Unknown flags stay unavailable, never false.
if (row.EvidenceKind == "Quality")
row.Value = Iec61850QualityPresentation.FormatObserved(
live.Value, row.EvidenceField);
else if (row.EvidenceField.Length == 0)
row.Value = live.Value;
continue;
}
if (row.EvidenceField == "Validity")
{
// A normalized q of Good/Invalid/Questionable is proof of
// validity, not proof of the remaining independent q bits.
var quality = live.Value.Trim();
row.Value = quality.Equals("good", StringComparison.OrdinalIgnoreCase) ||
quality.Equals("invalid", StringComparison.OrdinalIgnoreCase) ||
quality.Equals("questionable", StringComparison.OrdinalIgnoreCase)
? quality.ToLowerInvariant() : "Not exposed";
}
else
{
// The current report value formatter exposes normalized q/t
// without raw bits. Do not paint default false/true for bits
// that were not included in the model's actual value evidence.
row.Value = "Not exposed";
}
}
}

Expand Down
29 changes: 29 additions & 0 deletions Models/Iec61850ValueStatePresentation.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using ArIED61850Tester.Protocol.Mms;

namespace ArIED61850Tester.Models;

/// <summary>
Expand Down Expand Up @@ -255,6 +257,33 @@ private static bool LooksLikeAnalogAggregate(string text)
/// </summary>
public static class Iec61850QualityPresentation
{
public static string FormatObserved(string? observed, string evidenceField = "")
{
if (MmsQualityDecoder.TryDecodeRendered(observed, out var typed))
return typed.FormatField(evidenceField);

var value = (observed ?? "").Trim();
// Legacy report projection may retain only the validity word. It is
// not evidence for any other Quality bit (including a zero bit).
var separator = value.IndexOf(" / ", StringComparison.Ordinal);
var validity = separator < 0 ? value : value[..separator];
if (validity.Equals("Good", StringComparison.OrdinalIgnoreCase) ||
validity.Equals("Invalid", StringComparison.OrdinalIgnoreCase) ||
validity.Equals("Reserved", StringComparison.OrdinalIgnoreCase) ||
validity.Equals("Questionable", StringComparison.OrdinalIgnoreCase))
{
if (evidenceField == "Validity")
return validity.ToLowerInvariant();
return evidenceField.Length == 0 ? value : "Not exposed";
}

// The malformed renderer text is still preserved for diagnostics,
// never presented as a trusted live IEC state.
return evidenceField.Length == 0 &&
value.Length > 0 && !value.StartsWith("bits(", StringComparison.OrdinalIgnoreCase)
? value : "Not exposed";
}

public const string Good = "Good";
public const string Attention = "Attention";
public const string Bad = "Bad";
Expand Down
2 changes: 2 additions & 0 deletions Models/IedBrowserPresentation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ public string Value
// A typed child of a received q/t field; never treat the entire parent
// value text as this individual Boolean/time-quality bit.
public string EvidenceField { get; init; } = "";
// Schema-qualified presentation type; never infer a q/t bit from a DO name.
public string EvidenceKind { get; init; } = "";
public string FunctionalConstraint { get; init; } = "";
private IReadOnlyList<IedBrowserDetailRow>? _children;
public IedBrowserTreeRow? NavigateTo { get; init; }
Expand Down
11 changes: 8 additions & 3 deletions Models/IedBrowserStructuredDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@
children = () => BuildAt(descendants, pathIndex + 1, depth + 1);
else if (name is "q" or "t" && hasDirect)
children = () => name == "q"
? QualityChildren(depth + 1, attr.MmsReference, attr.FunctionalConstraint)

Check warning on line 84 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 84 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 84 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 84 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 84 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 84 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.
: TimeChildren(depth + 1, attr.MmsReference, attr.FunctionalConstraint);

Check warning on line 85 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 85 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 85 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 85 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 85 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 85 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

result.Add(new IedBrowserDetailRow("Data",
$"{name} [{attr.FunctionalConstraint}]", "Not read", depth,

Check warning on line 88 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 88 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 88 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 88 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 88 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 88 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.
$"IEC declaration · {attr.MmsReference} · {attr.TypeSource}")
{
// A structured intermediate row may have its own observed
Expand All @@ -95,6 +95,8 @@
? attr.MmsReference
: attr.ObjectReference + "." +
string.Join(".", groupArray[0].parts.Take(pathIndex + 1)),
EvidenceKind = hasDirect && name == "q" ? "Quality" :
hasDirect && name == "t" ? "Timestamp" : "",
FunctionalConstraint = groupArray.All(x =>
string.Equals(x.attr.FunctionalConstraint, attr.FunctionalConstraint,
StringComparison.OrdinalIgnoreCase))
Expand All @@ -119,7 +121,8 @@
{
ValueReference = qualityReference,
FunctionalConstraint = fc,
EvidenceField = evidence
EvidenceField = evidence,
EvidenceKind = "Quality"
};
var details = new[] { "Overflow", "OutOfRange", "BadReference",
"Oscillatory", "Failure", "OldData", "Inconsistent", "Inaccurate" };
Expand All @@ -133,7 +136,8 @@
{
ValueReference = qualityReference,
FunctionalConstraint = fc,
EvidenceField = name
EvidenceField = name,
EvidenceKind = "Quality"
}).ToArray()
},
QualityField("Source", "Source"),
Expand All @@ -150,6 +154,7 @@
{
ValueReference = timestampReference,
FunctionalConstraint = fc,
EvidenceField = name
EvidenceField = name,
EvidenceKind = "Timestamp"
}).ToArray();
}
153 changes: 113 additions & 40 deletions Protocol/Mms/MmsQualityDecoder.cs
Original file line number Diff line number Diff line change
@@ -1,57 +1,130 @@
using System;
using System.Collections.Generic;
using System.Globalization;

namespace ArIED61850Tester.Protocol.Mms;

public static class MmsQualityDecoder
/// <summary>Thirteen verified IEC 61850 Quality bits, not inferred defaults.</summary>
public sealed record MmsQualityEvidence(
string Validity, bool Overflow, bool OutOfRange, bool BadReference,
bool Oscillatory, bool Failure, bool OldData, bool Inconsistent,
bool Inaccurate, bool Substituted, bool Test, bool OperatorBlocked)
{
public static string Decode(ReadOnlySpan<byte> bitStringPayload)
{
if (bitStringPayload.Length < 2)
return "Quality: empty";
public string Source => Substituted ? "substituted" : "process";

var bits = ExpandBits(bitStringPayload[1..]);
var validity = bits.Count >= 2
? ((bits[0] ? 2 : 0) | (bits[1] ? 1 : 0))
: 0;
public string FormatField(string field) => field switch
{
"" => Validity,
"Validity" => Validity.ToLowerInvariant(),
"Source" => Source,
"Overflow" => Flag(Overflow),
"OutOfRange" => Flag(OutOfRange),
"BadReference" => Flag(BadReference),
"Oscillatory" => Flag(Oscillatory),
"Failure" => Flag(Failure),
"OldData" => Flag(OldData),
"Inconsistent" => Flag(Inconsistent),
"Inaccurate" => Flag(Inaccurate),
"Test" => Flag(Test),
"OperatorBlocked" => Flag(OperatorBlocked),
_ => "Not exposed"
};

var validityText = validity switch
public string Summary
{
get
{
0 => "Good",
1 => "Invalid",
2 => "Reserved",
3 => "Questionable",
_ => "Unknown"
};

var details = new List<string> { validityText };
Add(details, bits, 2, "Overflow");
Add(details, bits, 3, "OutOfRange");
Add(details, bits, 4, "BadReference");
Add(details, bits, 5, "Oscillatory");
Add(details, bits, 6, "Failure");
Add(details, bits, 7, "OldData");
Add(details, bits, 8, "Inconsistent");
Add(details, bits, 9, "Inaccurate");
Add(details, bits, 10, "Substituted");
Add(details, bits, 11, "Test");
Add(details, bits, 12, "OperatorBlocked");
return string.Join(" / ", details);
var parts = new List<string> { Validity };
Add(parts, Overflow, "Overflow");
Add(parts, OutOfRange, "OutOfRange");
Add(parts, BadReference, "BadReference");
Add(parts, Oscillatory, "Oscillatory");
Add(parts, Failure, "Failure");
Add(parts, OldData, "OldData");
Add(parts, Inconsistent, "Inconsistent");
Add(parts, Inaccurate, "Inaccurate");
Add(parts, Substituted, "Substituted");
Add(parts, Test, "Test");
Add(parts, OperatorBlocked, "OperatorBlocked");
return string.Join(" / ", parts);
}
}

private static List<bool> ExpandBits(ReadOnlySpan<byte> bytes)
private static string Flag(bool value) => value ? "true" : "false";
private static void Add(List<string> parts, bool enabled, string name)
{
var result = new List<bool>(bytes.Length * 8);
foreach (var b in bytes)
{
for (var bit = 7; bit >= 0; bit--)
result.Add(((b >> bit) & 0x01) != 0);
}
return result;
if (enabled) parts.Add(name);
}
}

/// <summary>
/// Existing MMS Quality decoder, now fail-closed on width and unused-bit
/// metadata. The rendered-value adapter is used only for schema-verified q.
/// </summary>
public static class MmsQualityDecoder
{
public static string Decode(ReadOnlySpan<byte> bitStringPayload)
=> TryDecode(bitStringPayload, out var quality)
? quality.Summary : "Quality: unavailable";

public static bool TryDecode(ReadOnlySpan<byte> payload, out MmsQualityEvidence quality)
{
quality = null!;
// Quality has exactly 13 significant bits. Reject nonzero padding.
if (payload.Length != 3 || payload[0] != 3 || (payload[2] & 0x07) != 0)
return false;

var validity = (Bit(payload, 0) ? 2 : 0) | (Bit(payload, 1) ? 1 : 0);
quality = new MmsQualityEvidence(
validity switch
{
0 => "Good",
1 => "Invalid",
2 => "Reserved",
_ => "Questionable"
},
Bit(payload, 2), Bit(payload, 3), Bit(payload, 4),
Bit(payload, 5), Bit(payload, 6), Bit(payload, 7),
Bit(payload, 8), Bit(payload, 9), Bit(payload, 10),
Bit(payload, 11), Bit(payload, 12));
return true;
}

private static void Add(List<string> details, List<bool> bits, int index, string label)
private static bool Bit(ReadOnlySpan<byte> payload, int index)
=> (payload[1 + index / 8] & (0x80 >> (index % 8))) != 0;

/// <summary>
/// Adapter for the already existing MMS renderer's bits(0000,unused=3)
/// representation. It does not decide which references have Quality type.
/// </summary>
public static bool TryDecodeRendered(string? rendered, out MmsQualityEvidence quality)
{
if (bits.Count > index && bits[index]) details.Add(label);
quality = null!;
if (string.IsNullOrWhiteSpace(rendered))
return false;

var text = rendered.AsSpan().Trim();
if (!text.StartsWith("bits(".AsSpan(), StringComparison.OrdinalIgnoreCase) ||
text[^1] != ')')
return false;
var body = text[5..^1];
var comma = body.IndexOf(',');
if (comma < 0) return false;
var hex = body[..comma].Trim();
if (hex.StartsWith("0x".AsSpan(), StringComparison.OrdinalIgnoreCase))
hex = hex[2..];
var suffix = body[(comma + 1)..].Trim();
if (!suffix.StartsWith("unused=".AsSpan(), StringComparison.OrdinalIgnoreCase) ||
!byte.TryParse(suffix[7..].Trim(), NumberStyles.Integer,
CultureInfo.InvariantCulture, out var padding) ||
hex.Length != 4 ||
!byte.TryParse(hex[..2], NumberStyles.HexNumber,
CultureInfo.InvariantCulture, out var first) ||
!byte.TryParse(hex[2..], NumberStyles.HexNumber,
CultureInfo.InvariantCulture, out var second))
return false;

Span<byte> payload = stackalloc byte[] { padding, first, second };
return TryDecode(payload, out quality);
}
}
40 changes: 22 additions & 18 deletions Protocol/Mms/MmsReadResponseDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ public static MmsReadDecodeResult DecodeSingleVariable(byte[] presentationPayloa
if (TryDecodeByHint(values, hint, out var hinted))
return Ok(hinted, hex);

// A declared Quality/Dbpos BIT STRING of the wrong width is not
// another valid process type. Never reinterpret it via generic fallback.
if ((hint.Contains("quality") || hint == "q" || hint.Contains("dbpos")) &&
values.Any(v => v.Tag == 0x84))
return Fail("MMS BIT STRING does not match the declared IEC value width.", hex);

if (TryDecodeAny(values, out var value))
return Ok(value, hex);

Expand Down Expand Up @@ -288,24 +294,21 @@ private static bool TryDecodeInteger(List<BerTlv> values, out object? value)

private static bool TryDecodeDbpos(List<BerTlv> values, out object? value)
{
var tlv = values.LastOrDefault(v => v.Tag == 0x84 && v.Length >= 2);
if (tlv.Tag == 0x84)
var tlv = values.LastOrDefault(v => v.Tag == 0x84);
// IEC 61850 Dbpos is a two-bit MMS BIT STRING. Quality and other
// BIT STRINGs must never be interpreted as a process position.
if (tlv.Tag == 0x84 && tlv.Length == 2 &&
tlv.Value.Span[0] == 6 && (tlv.Value.Span[1] & 0x3F) == 0)
{
var span = tlv.Value.Span;
var bytes = span[1..];
if (bytes.Length > 0)
var code = tlv.Value.Span[1] >> 6;
value = code switch
{
var code = (bytes[0] >> 6) & 0x03;
value = code switch
{
0 => "Intermediate [00]",
1 => "Open [01]",
2 => "Close [10]",
3 => "Invalid [11]",
_ => code.ToString(CultureInfo.InvariantCulture)
};
return true;
}
0 => "Intermediate [00]",
1 => "Open [01]",
2 => "Close [10]",
_ => "Bad state [11]"
};
return true;
}
value = null;
return false;
Expand All @@ -314,9 +317,10 @@ private static bool TryDecodeDbpos(List<BerTlv> values, out object? value)
private static bool TryDecodeQuality(List<BerTlv> values, out object? value)
{
var tlv = values.LastOrDefault(v => v.Tag == 0x84 && v.Length >= 2);
if (tlv.Tag == 0x84)
if (tlv.Tag == 0x84 &&
MmsQualityDecoder.TryDecode(tlv.Value.Span, out var quality))
{
value = MmsQualityDecoder.Decode(tlv.Value.Span);
value = quality.Summary;
return true;
}
value = null;
Expand Down
Loading
Loading