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
68 changes: 68 additions & 0 deletions src/AR.Iec61850/Discovery/CdcInferenceEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,62 @@ public static class CdcInferenceEngine
public static bool IsKnownCdc(string cdc)
=> !string.IsNullOrWhiteSpace(cdc) && KnownCdcValues.Contains(cdc.Trim());

/// <summary>
/// Refines only low/medium-confidence generic status inference when the MMS
/// TypeSpecification provides an exact primitive stVal type. Exact wire type is
/// stronger evidence than the generic ST/stVal/q/t shape; object names are never
/// used as a substitute for missing type evidence.
/// </summary>
public static CdcInferenceResult RefineWithExactPrimaryType(
CdcInferenceResult inferred,
IReadOnlyCollection<LiveIedDataAttributeModel> attributes)
{
ArgumentNullException.ThrowIfNull(inferred);
ArgumentNullException.ThrowIfNull(attributes);

if (!string.Equals(inferred.Cdc, "SPS", StringComparison.OrdinalIgnoreCase) ||
inferred.Confidence >= 0.85)
{
return inferred;
}

var exactStatusValues = attributes
.Where(attribute =>
attribute.TypeConfidence == LiveIedDiscoveryConfidenceLevel.Exact &&
string.Equals(
(attribute.AttributePath ?? string.Empty).Trim().Split('.').LastOrDefault(),
"stVal",
StringComparison.OrdinalIgnoreCase))
.ToArray();

if (exactStatusValues.Length != 1)
return inferred;

var primary = exactStatusValues[0];
var bType = (primary.SclBType ?? string.Empty).Trim().ToUpperInvariant();
var mmsType = (primary.MmsType ?? string.Empty).Trim().ToUpperInvariant();

if (IsExactSignedIntegerType(bType, mmsType))
{
return Result(
"INS",
0.96,
inferred.Evidence.Append(
$"exact MMS TypeSpecification stVal={FirstNonEmpty(primary.SclBType, primary.MmsType)} overrides generic SPS shape"));
}

if (IsExactBooleanType(bType, mmsType))
{
return Result(
"SPS",
Math.Max(inferred.Confidence, 0.96),
inferred.Evidence.Append(
$"exact MMS TypeSpecification stVal={FirstNonEmpty(primary.SclBType, primary.MmsType)} confirms SPS"));
}

return inferred;
}

public static CdcInferenceResult Infer(
string logicalNodeClass,
string dataObjectName,
Expand Down Expand Up @@ -133,6 +189,18 @@ public static CdcInferenceResult Infer(
return Result(string.Empty, 0.0, evidence.Append("CDC cannot be inferred from current online discovery only"));
}

private static bool IsExactSignedIntegerType(string bType, string mmsType)
=> bType is "INT8" or "INT16" or "INT24" or "INT32" or "INT64" or "INTEGER" ||
mmsType.Contains("INTEGER", StringComparison.Ordinal) ||
mmsType.Contains("SIGNED", StringComparison.Ordinal);

private static bool IsExactBooleanType(string bType, string mmsType)
=> bType == "BOOLEAN" ||
mmsType.Contains("BOOLEAN", StringComparison.Ordinal);

private static string FirstNonEmpty(params string?[] values)
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? string.Empty;

private static bool IsProtectionOperation(string lnClass, string dataObjectName)
=> string.Equals(dataObjectName, "Op", StringComparison.OrdinalIgnoreCase) &&
((lnClass.Length > 0 && lnClass[0] == 'P') || string.Equals(lnClass, "RREC", StringComparison.OrdinalIgnoreCase));
Expand Down
4 changes: 2 additions & 2 deletions src/AR.Iec61850/Discovery/Iec61850StandardModelRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ public static class Iec61850StandardModelRegistry
[Key("MMXU", "PF")] = Def("MMXU", "PF", "WYE", 0.84, "power factor"),

// Harmonic measurements use phase-group CDCs. Physical AA1E1F06R4
// plus IEDScout SCL prove that MHAI ThdA / ThdPhV are WYE objects whose
// plus field-reference SCL evidence prove that MHAI ThdA / ThdPhV are WYE objects whose
// phase children are CMV SDOs, not ACD indication structures.
[Key("MHAI", "ThdA")] = Def("MHAI", "ThdA", "WYE", 0.98, "three-phase current THD"),
[Key("MHAI", "ThdPhV")] = Def("MHAI", "ThdPhV", "WYE", 0.98, "three-phase voltage THD"),

// Instrument-transformer semantics are standardized and were also observed in
// the AA1E1F06R4 IEDScout engineering model. Exact registry entries prevent
// the field-reference engineering model. Exact registry entries prevent
// live discovery from dropping these configuration DOs when attribute-name
// heuristics alone cannot infer their CDC safely.
[Key("TCTR", "Mod")] = Def("TCTR", "Mod", "ENC", 0.98, "mode control"),
Expand Down
3 changes: 2 additions & 1 deletion src/AR.Iec61850/Discovery/LiveIedModelDiscoveryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ private static IEnumerable<LiveIedDataObjectModel> BuildDataObjects(
.ToArray();
var attrPaths = attributes.Select(x => x.AttributePath).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
var fcs = group.Select(x => x.FunctionalConstraint).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
var cdc = CdcInferenceEngine.Infer(parsedLn.SclLnClass, group.Key, attrPaths, fcs);
var inferredCdc = CdcInferenceEngine.Infer(parsedLn.SclLnClass, group.Key, attrPaths, fcs);
var cdc = CdcInferenceEngine.RefineWithExactPrimaryType(inferredCdc, attributes);
var reference = $"{ln.Domain}/{ln.Name}.{group.Key}";
yield return new LiveIedDataObjectModel
{
Expand Down
33 changes: 29 additions & 4 deletions src/AR.Iec61850/Scl/Export/LiveIedSclExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,7 @@ private static XElement BuildDaElement(TypeTreeNode node, string ownerTypeId, Li
new XAttribute("type", enumTypeId));
}

var bType = NormalizeBType(node.BType, node.Name, node.Path, cdc);
var bType = NormalizeBType(node.BType, node.Name, node.Path, cdc, node.TypeConfidence);
return new XElement(
Scl + (isRootDa ? "DA" : "BDA"),
new XAttribute("name", SafeXmlName(node.Name)),
Expand Down Expand Up @@ -1198,7 +1198,12 @@ private static bool IsControlBlockDataObject(LiveIedDataObjectModel dataObject)
string.Equals(attribute.FunctionalConstraint, "US", StringComparison.OrdinalIgnoreCase) ||
string.Equals(attribute.FunctionalConstraint, "LG", StringComparison.OrdinalIgnoreCase));

private static string NormalizeBType(string bType, string name, string path, string cdc)
private static string NormalizeBType(
string bType,
string name,
string path,
string cdc,
LiveIedDiscoveryConfidenceLevel typeConfidence)
{
if (string.Equals(name, "q", StringComparison.OrdinalIgnoreCase))
return "Quality";
Expand All @@ -1207,15 +1212,32 @@ private static string NormalizeBType(string bType, string name, string path, str
if (string.Equals(name, "T", StringComparison.Ordinal))
return "Timestamp";

var normalizedBType = (bType ?? string.Empty).Trim();
var exactPrimitive =
typeConfidence == LiveIedDiscoveryConfidenceLevel.Exact &&
!string.IsNullOrWhiteSpace(normalizedBType) &&
!normalizedBType.Equals("Unknown", StringComparison.OrdinalIgnoreCase) &&
!normalizedBType.Equals("Struct", StringComparison.OrdinalIgnoreCase);

// Exact MMS TypeSpecification is protocol evidence. Never overwrite it merely
// because a generic CDC shape inferred a different primitive type.
if (exactPrimitive)
return normalizedBType;

var normalizedCdc = cdc.Trim().ToUpperInvariant();
var normalizedPath = path.Trim();
var inferred = InferBTypeFromCdc(normalizedCdc, name, normalizedPath);
if (!string.IsNullOrWhiteSpace(inferred))
return inferred;

if (string.IsNullOrWhiteSpace(bType) || string.Equals(bType, "Unknown", StringComparison.OrdinalIgnoreCase) || string.Equals(bType, "Struct", StringComparison.OrdinalIgnoreCase))
if (string.IsNullOrWhiteSpace(normalizedBType) ||
normalizedBType.Equals("Unknown", StringComparison.OrdinalIgnoreCase) ||
normalizedBType.Equals("Struct", StringComparison.OrdinalIgnoreCase))
{
return "VisString255";
return bType.Trim();
}

return normalizedBType;
}

private static string InferBTypeFromCdc(string cdc, string name, string path)
Expand Down Expand Up @@ -1428,6 +1450,8 @@ private TypeTreeNode(string name, string path)
public string Path { get; }
public string Fc { get; private set; } = string.Empty;
public string BType { get; private set; } = string.Empty;
public LiveIedDiscoveryConfidenceLevel TypeConfidence { get; private set; } =
LiveIedDiscoveryConfidenceLevel.Unknown;
public IReadOnlyList<TypeTreeNode> Children => _orderedChildren;

public string EffectiveFunctionalConstraint
Expand Down Expand Up @@ -1491,6 +1515,7 @@ private void Add(LiveIedDataAttributeModel attribute)

current.Fc = attribute.FunctionalConstraint;
current.BType = attribute.SclBType;
current.TypeConfidence = attribute.TypeConfidence;
}
}
}
103 changes: 103 additions & 0 deletions tests/AR.Iec61850.Tests/Scl/LiveIedSclExporterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,109 @@ public void Exporter_Preserves_Mms_TypeSpecification_Declaration_Order_For_Statu
.ToArray());
}

[Fact]
public void Exact_Integer_StVal_Refines_Generic_Status_To_Ins_And_Preserves_Int32()
{
var discovery = new MmsDiscoveryResult
{
IedDirectory = new MmsIedModelDirectory(
[
new MmsFcResolvedPoint
{
Domain = "IEDADD",
LogicalNode = "GGIO1",
FunctionalConstraint = "ST",
DataObjectPath = "UserStatus.stVal",
MmsItemName = "GGIO1$ST$UserStatus$stVal"
},
new MmsFcResolvedPoint
{
Domain = "IEDADD",
LogicalNode = "GGIO1",
FunctionalConstraint = "ST",
DataObjectPath = "UserStatus.q",
MmsItemName = "GGIO1$ST$UserStatus$q"
},
new MmsFcResolvedPoint
{
Domain = "IEDADD",
LogicalNode = "GGIO1",
FunctionalConstraint = "ST",
DataObjectPath = "UserStatus.t",
MmsItemName = "GGIO1$ST$UserStatus$t"
}
])
};
var typeResult = new MmsVariableAccessAttributesResult
{
IsSuccess = true,
Reference = new MmsObjectReference("IEDADD", "GGIO1", string.Empty),
TypeSpecification = new MmsTypeSpecificationNode
{
MmsType = "structure",
SclBType = "Struct",
Children =
[
new MmsTypeSpecificationNode
{
Name = "ST",
MmsType = "structure",
SclBType = "Struct",
Children =
[
new MmsTypeSpecificationNode
{
Name = "UserStatus",
MmsType = "structure",
SclBType = "Struct",
Children =
[
new MmsTypeSpecificationNode { Name = "stVal", MmsType = "integer", SclBType = "INT32" },
new MmsTypeSpecificationNode { Name = "q", MmsType = "bit-string", SclBType = "Quality" },
new MmsTypeSpecificationNode { Name = "t", MmsType = "utc-time", SclBType = "Timestamp" }
]
}
]
}
]
}
};

var model = LiveIedModelDiscoveryBuilder.Build(
discovery,
new LiveIedModelDiscoveryBuildOptions { IedName = "IED" },
variableTypeAttributes: [typeResult]);

var dataObject = Assert.Single(
model.LogicalDevices
.SelectMany(device => device.LogicalNodes)
.SelectMany(node => node.DataObjects),
item => item.Name == "UserStatus");
Assert.Equal("INS", dataObject.InferredCdc);
Assert.Equal(LiveIedDiscoveryConfidenceLevel.Exact,
Assert.Single(dataObject.Attributes, attribute => attribute.AttributePath == "stVal").TypeConfidence);

var document = LiveIedSclExporter.BuildDocument(
model,
new LiveIedSclExportOptions { Profile = "full-model" });
var ns = document.Root!.Name.Namespace;
var lNodeType = Assert.Single(
document.Descendants(ns + "LNodeType"),
element => (string?)element.Attribute("lnClass") == "GGIO");
var doRef = Assert.Single(
lNodeType.Elements(ns + "DO"),
element => (string?)element.Attribute("name") == "UserStatus");
var doType = Assert.Single(
document.Descendants(ns + "DOType"),
element => (string?)element.Attribute("id") == (string?)doRef.Attribute("type"));

Assert.Equal("INS", (string?)doType.Attribute("cdc"));
var stVal = Assert.Single(
doType.Elements(ns + "DA"),
element => (string?)element.Attribute("name") == "stVal");
Assert.Equal("INT32", (string?)stVal.Attribute("bType"));
}

[Fact]
public void Exporter_Preserves_CaseDistinct_Ltrk_Cts_Members_From_Root_TypeSpecification()
{
Expand Down