From db4f174d10a063c5850edba71bfeb30f2a2b99e1 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 06:08:09 +0700 Subject: [PATCH 01/11] feat: add secure SCL endpoint parser --- Services/SclImportService.cs | 207 +++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 Services/SclImportService.cs diff --git a/Services/SclImportService.cs b/Services/SclImportService.cs new file mode 100644 index 000000000..4f2fe0219 --- /dev/null +++ b/Services/SclImportService.cs @@ -0,0 +1,207 @@ +using System.Globalization; +using System.Net; +using System.Xml; +using System.Xml.Linq; + +namespace ArIED61850Tester.Services; + +public sealed record SclIedEndpoint( + string IedName, + string AccessPointName, + string IpAddress, + int Port, + string SubNetworkName) +{ + public string EndpointText => $"{IpAddress}:{Port}"; +} + +public sealed class SclImportResult +{ + public string SourceFilePath { get; init; } = string.Empty; + public int IedDefinitionCount { get; init; } + public int ConnectedAccessPointCount { get; init; } + public IReadOnlyList Endpoints { get; init; } = Array.Empty(); + public IReadOnlyList Warnings { get; init; } = Array.Empty(); +} + +/// +/// Reads IEC 61850 SCL communication data without trusting external entities. +/// The importer is namespace-version agnostic and extracts IED/AP/IP endpoint +/// identity only; the live IED model is still verified through MMS discovery. +/// +public static class SclImportService +{ + private static readonly string[] IpParameterNames = + { + "IP", "IPv4", "IPv6", "IP-Address", "IPAddress" + }; + + private static readonly string[] PortParameterNames = + { + "MMS-Port", "MMS_PORT", "IP-Port", "TCP-Port", "Port" + }; + + public static Task LoadAsync(string filePath, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(filePath); + return Task.Run(() => Load(filePath, cancellationToken), cancellationToken); + } + + private static SclImportResult Load(string filePath, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (!File.Exists(filePath)) + throw new FileNotFoundException("The selected SCL file no longer exists.", filePath); + + var settings = new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null, + IgnoreComments = true, + IgnoreProcessingInstructions = true, + IgnoreWhitespace = true, + CloseInput = true + }; + + XDocument document; + using (var stream = File.OpenRead(filePath)) + using (var reader = XmlReader.Create(stream, settings)) + document = XDocument.Load(reader, LoadOptions.None); + + cancellationToken.ThrowIfCancellationRequested(); + var root = document.Root ?? throw new InvalidDataException("The selected XML document is empty."); + if (!NameIs(root, "SCL")) + throw new InvalidDataException("The selected XML document is not an IEC 61850 SCL file (missing SCL root element)."); + + var warnings = new List(); + var iedNames = root + .Descendants() + .Where(element => NameIs(element, "IED")) + .Select(element => AttributeValue(element, "name")) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var connectedAccessPoints = root + .Descendants() + .Where(element => NameIs(element, "ConnectedAP")) + .ToArray(); + + var endpoints = new List(); + var endpointKeys = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var connectedAp in connectedAccessPoints) + { + cancellationToken.ThrowIfCancellationRequested(); + + var iedName = AttributeValue(connectedAp, "iedName"); + var accessPointName = AttributeValue(connectedAp, "apName"); + var subNetworkName = connectedAp + .Ancestors() + .FirstOrDefault(element => NameIs(element, "SubNetwork")) is { } subNetwork + ? AttributeValue(subNetwork, "name") + : string.Empty; + + if (string.IsNullOrWhiteSpace(iedName)) + { + warnings.Add("A ConnectedAP entry was ignored because its iedName attribute is empty."); + continue; + } + + if (iedNames.Count > 0 && !iedNames.Contains(iedName)) + warnings.Add($"ConnectedAP '{iedName}/{accessPointName}' does not match an IED definition in the file; its endpoint was still imported."); + + var parameters = ReadAddressParameters(connectedAp).ToArray(); + var ipText = FindParameter(parameters, IpParameterNames); + if (string.IsNullOrWhiteSpace(ipText)) + { + warnings.Add($"{iedName}/{DisplayAccessPoint(accessPointName)} has no IP address in its ConnectedAP Address block."); + continue; + } + + if (!IPAddress.TryParse(ipText, out var parsedIp)) + { + warnings.Add($"{iedName}/{DisplayAccessPoint(accessPointName)} has an invalid IP address '{ipText}'."); + continue; + } + + var port = 102; + var portText = FindParameter(parameters, PortParameterNames); + if (!string.IsNullOrWhiteSpace(portText) && + (!int.TryParse(portText, NumberStyles.Integer, CultureInfo.InvariantCulture, out port) || port is <= 0 or > 65535)) + { + warnings.Add($"{iedName}/{DisplayAccessPoint(accessPointName)} has invalid MMS port '{portText}'; TCP 102 was used."); + port = 102; + } + + var canonicalIp = parsedIp.ToString(); + var key = $"{canonicalIp}|{port}"; + if (!endpointKeys.Add(key)) + { + warnings.Add($"Duplicate endpoint {canonicalIp}:{port} for {iedName}/{DisplayAccessPoint(accessPointName)} was ignored."); + continue; + } + + endpoints.Add(new SclIedEndpoint( + iedName, + accessPointName, + canonicalIp, + port, + subNetworkName)); + } + + return new SclImportResult + { + SourceFilePath = Path.GetFullPath(filePath), + IedDefinitionCount = iedNames.Count, + ConnectedAccessPointCount = connectedAccessPoints.Length, + Endpoints = endpoints + .OrderBy(endpoint => endpoint.IedName, StringComparer.OrdinalIgnoreCase) + .ThenBy(endpoint => endpoint.AccessPointName, StringComparer.OrdinalIgnoreCase) + .ThenBy(endpoint => endpoint.IpAddress, StringComparer.OrdinalIgnoreCase) + .ToArray(), + Warnings = warnings.ToArray() + }; + } + + private static IEnumerable> ReadAddressParameters(XElement connectedAp) + { + var address = connectedAp.Elements().FirstOrDefault(element => NameIs(element, "Address")) ?? + connectedAp.Descendants().FirstOrDefault(element => NameIs(element, "Address")); + if (address == null) + yield break; + + foreach (var parameter in address.Elements().Where(element => NameIs(element, "P"))) + { + var type = AttributeValue(parameter, "type"); + var value = parameter.Value.Trim(); + if (!string.IsNullOrWhiteSpace(type) && !string.IsNullOrWhiteSpace(value)) + yield return new KeyValuePair(type, value); + } + } + + private static string FindParameter( + IReadOnlyCollection> parameters, + IEnumerable candidateNames) + { + foreach (var candidate in candidateNames) + { + var value = parameters.FirstOrDefault(parameter => + parameter.Key.Equals(candidate, StringComparison.OrdinalIgnoreCase)).Value; + if (!string.IsNullOrWhiteSpace(value)) + return value.Trim(); + } + + return string.Empty; + } + + private static bool NameIs(XElement element, string localName) + => element.Name.LocalName.Equals(localName, StringComparison.OrdinalIgnoreCase); + + private static string AttributeValue(XElement element, string localName) + => element.Attributes() + .FirstOrDefault(attribute => attribute.Name.LocalName.Equals(localName, StringComparison.OrdinalIgnoreCase)) + ?.Value.Trim() ?? string.Empty; + + private static string DisplayAccessPoint(string accessPointName) + => string.IsNullOrWhiteSpace(accessPointName) ? "unnamed AP" : accessPointName; +} From e73c0395b31771264d46c37be50634fc4dd8ae9b Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 06:09:12 +0700 Subject: [PATCH 02/11] ci: add SCL import source patch --- tools/apply_open_scl_import.py | 325 +++++++++++++++++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 tools/apply_open_scl_import.py diff --git a/tools/apply_open_scl_import.py b/tools/apply_open_scl_import.py new file mode 100644 index 000000000..173a7ca75 --- /dev/null +++ b/tools/apply_open_scl_import.py @@ -0,0 +1,325 @@ +from __future__ import annotations + +from pathlib import Path +import xml.etree.ElementTree as ET + + +ROOT = Path(__file__).resolve().parents[1] + + +def read_source(relative_path: str) -> tuple[Path, str, str]: + path = ROOT / relative_path + raw = path.read_bytes() + newline = "\r\n" if b"\r\n" in raw else "\n" + text = raw.decode("utf-8").replace("\r\n", "\n") + return path, text, newline + + +def write_source(path: Path, text: str, newline: str) -> None: + normalized = text.replace("\r\n", "\n") + path.write_bytes(normalized.replace("\n", newline).encode("utf-8")) + + +def replace_once(text: str, old: str, new: str, label: str) -> str: + count = text.count(old) + if count != 1: + raise RuntimeError(f"{label}: expected exactly one match, found {count}") + return text.replace(old, new, 1) + + +def patch_main_window_xaml() -> None: + path, text, newline = read_source("MainWindow.xaml") + + text = replace_once( + text, + ' Text="Fast workflow: add IED → discover live model → choose signals → monitor each IED independently"', + ' Text="Fast workflow: add IED or open SCL → verify live model → choose signals → monitor independently"', + "header workflow text", + ) + + old_actions = """ + + + + + + + + +""" + + new_actions = """ + + + + + + + + + + + + + + + + + +""" + + text = replace_once(text, old_actions, new_actions, "IED Explorer fast actions") + write_source(path, text, newline) + + +def patch_main_window_code() -> None: + path, text, newline = read_source("MainWindow.xaml.cs") + + anchor = """ private async void AddRelay_Click(object sender, RoutedEventArgs e) + { + var initialIp = SelectedDevice?.IpAddress ?? NewDeviceIp; + var initialPort = SelectedDevice?.Port ?? 102; + var wizard = new IpConnectWizardWindow(initialIp, initialPort) { Owner = this }; + if (wizard.ShowDialog() != true) + return; + + NewDeviceIp = wizard.RelayIpAddress; + NewDevicePort = wizard.MmsPort.ToString(CultureInfo.InvariantCulture); + await AddOrDiscoverEndpointAsync(wizard.RelayIpAddress, wizard.MmsPort); + } + + +""" + + replacement = anchor + """ private async void OpenScl_Click(object sender, RoutedEventArgs e) + { + var dialog = new OpenFileDialog + { + Title = "Open IEC 61850 SCL", + Filter = "IEC 61850 SCL (*.scd;*.cid;*.icd;*.iid;*.ssd)|*.scd;*.cid;*.icd;*.iid;*.ssd|XML SCL (*.xml)|*.xml|All files (*.*)|*.*", + CheckFileExists = true, + Multiselect = false + }; + if (dialog.ShowDialog(this) != true) + return; + + var sourceName = Path.GetFileName(dialog.FileName); + SetStatus($"Reading IED endpoints from {sourceName}…"); + try + { + var result = await SclImportService.LoadAsync(dialog.FileName, _applicationCancellation.Token); + foreach (var warning in result.Warnings.Take(25)) + AddLog("WARN", "SCL", warning); + if (result.Warnings.Count > 25) + AddLog("WARN", "SCL", $"{result.Warnings.Count - 25} additional SCL warning(s) were omitted from the live log."); + + if (result.Endpoints.Count == 0) + { + var reason = result.ConnectedAccessPointCount == 0 + ? "No ConnectedAP communication entries were found." + : "ConnectedAP entries were found, but none contained a valid IP address."; + SetStatus($"{sourceName}: no usable IEC 61850 MMS endpoints. {reason}"); + AddLog("WARN", "SCL", $"{sourceName}: {reason}"); + MessageBox.Show( + this, + $"No usable IEC 61850 MMS endpoint was found in {sourceName}.\n\n{reason}\n\nThe file may contain only an IED template without a Communication section.", + "Open SCL", + MessageBoxButton.OK, + MessageBoxImage.Information); + return; + } + + var added = 0; + var refreshed = 0; + var retained = 0; + Iec61850MonitorDevice? firstImported = null; + + foreach (var endpoint in result.Endpoints) + { + var device = Devices.FirstOrDefault(item => + item.IpAddress.Equals(endpoint.IpAddress, StringComparison.OrdinalIgnoreCase) && + item.Port == endpoint.Port); + + if (device == null) + { + device = new Iec61850MonitorDevice + { + Name = endpoint.IedName, + IdentitySource = $"SCL • {sourceName}", + LogicalDeviceSummary = BuildSclEndpointSummary(endpoint), + IpAddress = endpoint.IpAddress, + Port = endpoint.Port, + AllowDynamicDataSetWrites = true, + Status = "SCL endpoint ready", + Detail = $"Imported from {sourceName}. Press Play to connect and verify the live IEC 61850 model.", + AcquisitionMode = "SCL • live discovery pending" + }; + Devices.Add(device); + added++; + } + else if (!device.IsConnected && !device.IsBusy && !device.HasDiscoveryCache) + { + if (string.IsNullOrWhiteSpace(device.Name) || + device.Name.Equals(device.IpAddress, StringComparison.OrdinalIgnoreCase)) + { + device.Name = endpoint.IedName; + } + device.IdentitySource = $"SCL • {sourceName}"; + device.LogicalDeviceSummary = BuildSclEndpointSummary(endpoint); + device.Status = "SCL endpoint ready"; + device.Detail = $"Endpoint refreshed from {sourceName}. Press Play to connect and verify the live IEC 61850 model."; + device.AcquisitionMode = "SCL • live discovery pending"; + device.RefreshComputed(); + refreshed++; + } + else + { + // Preserve active sessions and successful discovery caches. SCL is an + // endpoint-import path, never authority over a verified live model. + retained++; + } + + firstImported ??= device; + } + + if (firstImported != null) + SelectedDevice = firstImported; + MainTabs.SelectedIndex = 0; + UpdateNavigationVisuals(0, animate: true); + RaiseWorkspaceCounts(); + + var warningText = result.Warnings.Count == 0 ? string.Empty : $", {result.Warnings.Count} warning(s)"; + var status = $"{sourceName}: {result.Endpoints.Count} SCL endpoint(s) read — {added} added, {refreshed} refreshed, {retained} existing retained{warningText}. Use Play or Connect All for live verification."; + SetStatus(status); + AddLog("INFO", "SCL", status); + } + catch (OperationCanceledException) + { + SetStatus($"{sourceName}: SCL import cancelled."); + } + catch (Exception ex) + { + AddLog("ERROR", "SCL", $"Could not open {sourceName}: {ex.Message}"); + SetStatus($"{sourceName}: SCL import failed. Diagnostics is marked with !."); + MarkDiagnosticAlert(); + MessageBox.Show( + this, + $"ArIED could not read this SCL file.\n\n{ex.Message}", + "Open SCL", + MessageBoxButton.OK, + MessageBoxImage.Error); + } + } + + private static string BuildSclEndpointSummary(SclIedEndpoint endpoint) + { + var parts = new List(); + if (!string.IsNullOrWhiteSpace(endpoint.AccessPointName)) + parts.Add($"AP {endpoint.AccessPointName}"); + if (!string.IsNullOrWhiteSpace(endpoint.SubNetworkName)) + parts.Add(endpoint.SubNetworkName); + return parts.Count == 0 ? "SCL endpoint" : string.Join(" • ", parts); + } + + +""" + + text = replace_once(text, anchor, replacement, "Open SCL command handler") + write_source(path, text, newline) + + +def patch_readme() -> None: + path, text, newline = read_source("README.md") + text = replace_once( + text, + """Add or open IED +→ discover or restore cached model +""", + """Add IED by IP, open a project, or import SCL +→ connect and verify the live model, or restore a cached model +""", + "README core workflow", + ) + text = replace_once( + text, + """- Real IEDName and Logical Device boundary resolution. +- Multi-IED independent connections and monitoring. +""", + """- Real IEDName and Logical Device boundary resolution. +- SCL endpoint import from SCD, CID, ICD, IID, SSD, or XML files, including multi-IED ConnectedAP/IP extraction and duplicate protection. +- Multi-IED independent connections and monitoring. +""", + "README SCL capability", + ) + write_source(path, text, newline) + + +def validate() -> None: + ET.parse(ROOT / "MainWindow.xaml") + main_window = (ROOT / "MainWindow.xaml.cs").read_text(encoding="utf-8") + service = (ROOT / "Services" / "SclImportService.cs").read_text(encoding="utf-8") + required = [ + "OpenScl_Click", + "SclImportService.LoadAsync", + "BuildSclEndpointSummary", + "SCL endpoint ready", + "DtdProcessing.Prohibit", + "ConnectedAP", + ] + combined = main_window + service + missing = [token for token in required if token not in combined] + if missing: + raise RuntimeError(f"SCL import validation missing: {', '.join(missing)}") + + +if __name__ == "__main__": + patch_main_window_xaml() + patch_main_window_code() + patch_readme() + validate() + print("SCL import source patch and validation completed") From 983c77aa134cf27af2726e3c7fce00c0106be27e Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 06:09:22 +0700 Subject: [PATCH 03/11] ci: apply Open SCL import feature --- .github/workflows/apply-open-scl-import.yml | 42 +++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/apply-open-scl-import.yml diff --git a/.github/workflows/apply-open-scl-import.yml b/.github/workflows/apply-open-scl-import.yml new file mode 100644 index 000000000..b0eb73aff --- /dev/null +++ b/.github/workflows/apply-open-scl-import.yml @@ -0,0 +1,42 @@ +name: Apply Open SCL import feature + +on: + push: + branches: + - feat/open-scl-import + +permissions: + contents: write + +jobs: + apply: + runs-on: windows-latest + steps: + - name: Checkout feature branch + uses: actions/checkout@v4 + with: + ref: feat/open-scl-import + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Apply and validate Open SCL source changes + shell: pwsh + run: | + python tools/apply_open_scl_import.py + python -c "import xml.etree.ElementTree as E; E.parse('App.xaml'); E.parse('MainWindow.xaml'); print('XAML XML validation passed')" + git diff --check + + - name: Commit generated source changes + shell: pwsh + run: | + git rm tools/apply_open_scl_import.py + git rm .github/workflows/apply-open-scl-import.yml + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add Services/SclImportService.cs MainWindow.xaml MainWindow.xaml.cs README.md + git commit -m 'feat: import IEC 61850 IED endpoints from SCL' + git push origin HEAD:feat/open-scl-import From b627d2f5f6ba56a272d0bf26bdd46890f4005343 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 23:09:44 +0000 Subject: [PATCH 04/11] feat: import IEC 61850 IED endpoints from SCL --- .github/workflows/apply-open-scl-import.yml | 42 --- MainWindow.xaml | 34 +- MainWindow.xaml.cs | 137 +++++++++ README.md | 5 +- tools/apply_open_scl_import.py | 325 -------------------- 5 files changed, 168 insertions(+), 375 deletions(-) delete mode 100644 .github/workflows/apply-open-scl-import.yml delete mode 100644 tools/apply_open_scl_import.py diff --git a/.github/workflows/apply-open-scl-import.yml b/.github/workflows/apply-open-scl-import.yml deleted file mode 100644 index b0eb73aff..000000000 --- a/.github/workflows/apply-open-scl-import.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Apply Open SCL import feature - -on: - push: - branches: - - feat/open-scl-import - -permissions: - contents: write - -jobs: - apply: - runs-on: windows-latest - steps: - - name: Checkout feature branch - uses: actions/checkout@v4 - with: - ref: feat/open-scl-import - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Apply and validate Open SCL source changes - shell: pwsh - run: | - python tools/apply_open_scl_import.py - python -c "import xml.etree.ElementTree as E; E.parse('App.xaml'); E.parse('MainWindow.xaml'); print('XAML XML validation passed')" - git diff --check - - - name: Commit generated source changes - shell: pwsh - run: | - git rm tools/apply_open_scl_import.py - git rm .github/workflows/apply-open-scl-import.yml - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add Services/SclImportService.cs MainWindow.xaml MainWindow.xaml.cs README.md - git commit -m 'feat: import IEC 61850 IED endpoints from SCL' - git push origin HEAD:feat/open-scl-import diff --git a/MainWindow.xaml b/MainWindow.xaml index a04a4dc77..2724e4014 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -30,7 +30,7 @@ @@ -131,13 +131,20 @@ Background="#F7FAFF" BorderBrush="#DCE6F5" BorderThickness="1" CornerRadius="15" Effect="{StaticResource SoftShadow}" Panel.ZIndex="30"> + + + + + - - + + diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index dc7c58ff7..5b7299866 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -147,6 +147,143 @@ private async void AddRelay_Click(object sender, RoutedEventArgs e) } + private async void OpenScl_Click(object sender, RoutedEventArgs e) + { + var dialog = new OpenFileDialog + { + Title = "Open IEC 61850 SCL", + Filter = "IEC 61850 SCL (*.scd;*.cid;*.icd;*.iid;*.ssd)|*.scd;*.cid;*.icd;*.iid;*.ssd|XML SCL (*.xml)|*.xml|All files (*.*)|*.*", + CheckFileExists = true, + Multiselect = false + }; + if (dialog.ShowDialog(this) != true) + return; + + var sourceName = Path.GetFileName(dialog.FileName); + SetStatus($"Reading IED endpoints from {sourceName}…"); + try + { + var result = await SclImportService.LoadAsync(dialog.FileName, _applicationCancellation.Token); + foreach (var warning in result.Warnings.Take(25)) + AddLog("WARN", "SCL", warning); + if (result.Warnings.Count > 25) + AddLog("WARN", "SCL", $"{result.Warnings.Count - 25} additional SCL warning(s) were omitted from the live log."); + + if (result.Endpoints.Count == 0) + { + var reason = result.ConnectedAccessPointCount == 0 + ? "No ConnectedAP communication entries were found." + : "ConnectedAP entries were found, but none contained a valid IP address."; + SetStatus($"{sourceName}: no usable IEC 61850 MMS endpoints. {reason}"); + AddLog("WARN", "SCL", $"{sourceName}: {reason}"); + MessageBox.Show( + this, + $"No usable IEC 61850 MMS endpoint was found in {sourceName}. + +{reason} + +The file may contain only an IED template without a Communication section.", + "Open SCL", + MessageBoxButton.OK, + MessageBoxImage.Information); + return; + } + + var added = 0; + var refreshed = 0; + var retained = 0; + Iec61850MonitorDevice? firstImported = null; + + foreach (var endpoint in result.Endpoints) + { + var device = Devices.FirstOrDefault(item => + item.IpAddress.Equals(endpoint.IpAddress, StringComparison.OrdinalIgnoreCase) && + item.Port == endpoint.Port); + + if (device == null) + { + device = new Iec61850MonitorDevice + { + Name = endpoint.IedName, + IdentitySource = $"SCL • {sourceName}", + LogicalDeviceSummary = BuildSclEndpointSummary(endpoint), + IpAddress = endpoint.IpAddress, + Port = endpoint.Port, + AllowDynamicDataSetWrites = true, + Status = "SCL endpoint ready", + Detail = $"Imported from {sourceName}. Press Play to connect and verify the live IEC 61850 model.", + AcquisitionMode = "SCL • live discovery pending" + }; + Devices.Add(device); + added++; + } + else if (!device.IsConnected && !device.IsBusy && !device.HasDiscoveryCache) + { + if (string.IsNullOrWhiteSpace(device.Name) || + device.Name.Equals(device.IpAddress, StringComparison.OrdinalIgnoreCase)) + { + device.Name = endpoint.IedName; + } + device.IdentitySource = $"SCL • {sourceName}"; + device.LogicalDeviceSummary = BuildSclEndpointSummary(endpoint); + device.Status = "SCL endpoint ready"; + device.Detail = $"Endpoint refreshed from {sourceName}. Press Play to connect and verify the live IEC 61850 model."; + device.AcquisitionMode = "SCL • live discovery pending"; + device.RefreshComputed(); + refreshed++; + } + else + { + // Preserve active sessions and successful discovery caches. SCL is an + // endpoint-import path, never authority over a verified live model. + retained++; + } + + firstImported ??= device; + } + + if (firstImported != null) + SelectedDevice = firstImported; + MainTabs.SelectedIndex = 0; + UpdateNavigationVisuals(0, animate: true); + RaiseWorkspaceCounts(); + + var warningText = result.Warnings.Count == 0 ? string.Empty : $", {result.Warnings.Count} warning(s)"; + var status = $"{sourceName}: {result.Endpoints.Count} SCL endpoint(s) read — {added} added, {refreshed} refreshed, {retained} existing retained{warningText}. Use Play or Connect All for live verification."; + SetStatus(status); + AddLog("INFO", "SCL", status); + } + catch (OperationCanceledException) + { + SetStatus($"{sourceName}: SCL import cancelled."); + } + catch (Exception ex) + { + AddLog("ERROR", "SCL", $"Could not open {sourceName}: {ex.Message}"); + SetStatus($"{sourceName}: SCL import failed. Diagnostics is marked with !."); + MarkDiagnosticAlert(); + MessageBox.Show( + this, + $"ArIED could not read this SCL file. + +{ex.Message}", + "Open SCL", + MessageBoxButton.OK, + MessageBoxImage.Error); + } + } + + private static string BuildSclEndpointSummary(SclIedEndpoint endpoint) + { + var parts = new List(); + if (!string.IsNullOrWhiteSpace(endpoint.AccessPointName)) + parts.Add($"AP {endpoint.AccessPointName}"); + if (!string.IsNullOrWhiteSpace(endpoint.SubNetworkName)) + parts.Add(endpoint.SubNetworkName); + return parts.Count == 0 ? "SCL endpoint" : string.Join(" • ", parts); + } + + private async void ConnectAllIeds_Click(object sender, RoutedEventArgs e) { if (_connectAllInProgress) diff --git a/README.md b/README.md index 7e7585ff6..e7f2b20b0 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ ArIED 61850 is a Windows desktop engineering tool for IEC 61850 MMS discovery, m ## Core workflow ```text -Add or open IED -→ discover or restore cached model +Add IED by IP, open a project, or import SCL +→ connect and verify the live model, or restore a cached model → select live and control objects → start independent per-IED monitoring → inspect reports, SOE, diagnostics, and commands @@ -20,6 +20,7 @@ Each IED keeps its own connection, discovery progress, selected signals, report - Live MMS discovery of Logical Devices, Logical Nodes, data objects, values, quality, and IED timestamps. - Real IEDName and Logical Device boundary resolution. +- SCL endpoint import from SCD, CID, ICD, IID, SSD, or XML files, including multi-IED ConnectedAP/IP extraction and duplicate protection. - Multi-IED independent connections and monitoring. - Static RCB/DataSet first, dynamic reporting when needed, and bounded MMS fallback for uncovered or unverified points. - Saved project model cache for fast reconnect without repeating full discovery. diff --git a/tools/apply_open_scl_import.py b/tools/apply_open_scl_import.py deleted file mode 100644 index 173a7ca75..000000000 --- a/tools/apply_open_scl_import.py +++ /dev/null @@ -1,325 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -import xml.etree.ElementTree as ET - - -ROOT = Path(__file__).resolve().parents[1] - - -def read_source(relative_path: str) -> tuple[Path, str, str]: - path = ROOT / relative_path - raw = path.read_bytes() - newline = "\r\n" if b"\r\n" in raw else "\n" - text = raw.decode("utf-8").replace("\r\n", "\n") - return path, text, newline - - -def write_source(path: Path, text: str, newline: str) -> None: - normalized = text.replace("\r\n", "\n") - path.write_bytes(normalized.replace("\n", newline).encode("utf-8")) - - -def replace_once(text: str, old: str, new: str, label: str) -> str: - count = text.count(old) - if count != 1: - raise RuntimeError(f"{label}: expected exactly one match, found {count}") - return text.replace(old, new, 1) - - -def patch_main_window_xaml() -> None: - path, text, newline = read_source("MainWindow.xaml") - - text = replace_once( - text, - ' Text="Fast workflow: add IED → discover live model → choose signals → monitor each IED independently"', - ' Text="Fast workflow: add IED or open SCL → verify live model → choose signals → monitor independently"', - "header workflow text", - ) - - old_actions = """ - - - - - - - - -""" - - new_actions = """ - - - - - - - - - - - - - - - - - -""" - - text = replace_once(text, old_actions, new_actions, "IED Explorer fast actions") - write_source(path, text, newline) - - -def patch_main_window_code() -> None: - path, text, newline = read_source("MainWindow.xaml.cs") - - anchor = """ private async void AddRelay_Click(object sender, RoutedEventArgs e) - { - var initialIp = SelectedDevice?.IpAddress ?? NewDeviceIp; - var initialPort = SelectedDevice?.Port ?? 102; - var wizard = new IpConnectWizardWindow(initialIp, initialPort) { Owner = this }; - if (wizard.ShowDialog() != true) - return; - - NewDeviceIp = wizard.RelayIpAddress; - NewDevicePort = wizard.MmsPort.ToString(CultureInfo.InvariantCulture); - await AddOrDiscoverEndpointAsync(wizard.RelayIpAddress, wizard.MmsPort); - } - - -""" - - replacement = anchor + """ private async void OpenScl_Click(object sender, RoutedEventArgs e) - { - var dialog = new OpenFileDialog - { - Title = "Open IEC 61850 SCL", - Filter = "IEC 61850 SCL (*.scd;*.cid;*.icd;*.iid;*.ssd)|*.scd;*.cid;*.icd;*.iid;*.ssd|XML SCL (*.xml)|*.xml|All files (*.*)|*.*", - CheckFileExists = true, - Multiselect = false - }; - if (dialog.ShowDialog(this) != true) - return; - - var sourceName = Path.GetFileName(dialog.FileName); - SetStatus($"Reading IED endpoints from {sourceName}…"); - try - { - var result = await SclImportService.LoadAsync(dialog.FileName, _applicationCancellation.Token); - foreach (var warning in result.Warnings.Take(25)) - AddLog("WARN", "SCL", warning); - if (result.Warnings.Count > 25) - AddLog("WARN", "SCL", $"{result.Warnings.Count - 25} additional SCL warning(s) were omitted from the live log."); - - if (result.Endpoints.Count == 0) - { - var reason = result.ConnectedAccessPointCount == 0 - ? "No ConnectedAP communication entries were found." - : "ConnectedAP entries were found, but none contained a valid IP address."; - SetStatus($"{sourceName}: no usable IEC 61850 MMS endpoints. {reason}"); - AddLog("WARN", "SCL", $"{sourceName}: {reason}"); - MessageBox.Show( - this, - $"No usable IEC 61850 MMS endpoint was found in {sourceName}.\n\n{reason}\n\nThe file may contain only an IED template without a Communication section.", - "Open SCL", - MessageBoxButton.OK, - MessageBoxImage.Information); - return; - } - - var added = 0; - var refreshed = 0; - var retained = 0; - Iec61850MonitorDevice? firstImported = null; - - foreach (var endpoint in result.Endpoints) - { - var device = Devices.FirstOrDefault(item => - item.IpAddress.Equals(endpoint.IpAddress, StringComparison.OrdinalIgnoreCase) && - item.Port == endpoint.Port); - - if (device == null) - { - device = new Iec61850MonitorDevice - { - Name = endpoint.IedName, - IdentitySource = $"SCL • {sourceName}", - LogicalDeviceSummary = BuildSclEndpointSummary(endpoint), - IpAddress = endpoint.IpAddress, - Port = endpoint.Port, - AllowDynamicDataSetWrites = true, - Status = "SCL endpoint ready", - Detail = $"Imported from {sourceName}. Press Play to connect and verify the live IEC 61850 model.", - AcquisitionMode = "SCL • live discovery pending" - }; - Devices.Add(device); - added++; - } - else if (!device.IsConnected && !device.IsBusy && !device.HasDiscoveryCache) - { - if (string.IsNullOrWhiteSpace(device.Name) || - device.Name.Equals(device.IpAddress, StringComparison.OrdinalIgnoreCase)) - { - device.Name = endpoint.IedName; - } - device.IdentitySource = $"SCL • {sourceName}"; - device.LogicalDeviceSummary = BuildSclEndpointSummary(endpoint); - device.Status = "SCL endpoint ready"; - device.Detail = $"Endpoint refreshed from {sourceName}. Press Play to connect and verify the live IEC 61850 model."; - device.AcquisitionMode = "SCL • live discovery pending"; - device.RefreshComputed(); - refreshed++; - } - else - { - // Preserve active sessions and successful discovery caches. SCL is an - // endpoint-import path, never authority over a verified live model. - retained++; - } - - firstImported ??= device; - } - - if (firstImported != null) - SelectedDevice = firstImported; - MainTabs.SelectedIndex = 0; - UpdateNavigationVisuals(0, animate: true); - RaiseWorkspaceCounts(); - - var warningText = result.Warnings.Count == 0 ? string.Empty : $", {result.Warnings.Count} warning(s)"; - var status = $"{sourceName}: {result.Endpoints.Count} SCL endpoint(s) read — {added} added, {refreshed} refreshed, {retained} existing retained{warningText}. Use Play or Connect All for live verification."; - SetStatus(status); - AddLog("INFO", "SCL", status); - } - catch (OperationCanceledException) - { - SetStatus($"{sourceName}: SCL import cancelled."); - } - catch (Exception ex) - { - AddLog("ERROR", "SCL", $"Could not open {sourceName}: {ex.Message}"); - SetStatus($"{sourceName}: SCL import failed. Diagnostics is marked with !."); - MarkDiagnosticAlert(); - MessageBox.Show( - this, - $"ArIED could not read this SCL file.\n\n{ex.Message}", - "Open SCL", - MessageBoxButton.OK, - MessageBoxImage.Error); - } - } - - private static string BuildSclEndpointSummary(SclIedEndpoint endpoint) - { - var parts = new List(); - if (!string.IsNullOrWhiteSpace(endpoint.AccessPointName)) - parts.Add($"AP {endpoint.AccessPointName}"); - if (!string.IsNullOrWhiteSpace(endpoint.SubNetworkName)) - parts.Add(endpoint.SubNetworkName); - return parts.Count == 0 ? "SCL endpoint" : string.Join(" • ", parts); - } - - -""" - - text = replace_once(text, anchor, replacement, "Open SCL command handler") - write_source(path, text, newline) - - -def patch_readme() -> None: - path, text, newline = read_source("README.md") - text = replace_once( - text, - """Add or open IED -→ discover or restore cached model -""", - """Add IED by IP, open a project, or import SCL -→ connect and verify the live model, or restore a cached model -""", - "README core workflow", - ) - text = replace_once( - text, - """- Real IEDName and Logical Device boundary resolution. -- Multi-IED independent connections and monitoring. -""", - """- Real IEDName and Logical Device boundary resolution. -- SCL endpoint import from SCD, CID, ICD, IID, SSD, or XML files, including multi-IED ConnectedAP/IP extraction and duplicate protection. -- Multi-IED independent connections and monitoring. -""", - "README SCL capability", - ) - write_source(path, text, newline) - - -def validate() -> None: - ET.parse(ROOT / "MainWindow.xaml") - main_window = (ROOT / "MainWindow.xaml.cs").read_text(encoding="utf-8") - service = (ROOT / "Services" / "SclImportService.cs").read_text(encoding="utf-8") - required = [ - "OpenScl_Click", - "SclImportService.LoadAsync", - "BuildSclEndpointSummary", - "SCL endpoint ready", - "DtdProcessing.Prohibit", - "ConnectedAP", - ] - combined = main_window + service - missing = [token for token in required if token not in combined] - if missing: - raise RuntimeError(f"SCL import validation missing: {', '.join(missing)}") - - -if __name__ == "__main__": - patch_main_window_xaml() - patch_main_window_code() - patch_readme() - validate() - print("SCL import source patch and validation completed") From 83c1400a487c3129cefb677a33e809e39da60ef2 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 06:10:11 +0700 Subject: [PATCH 05/11] fix: repair SCL dialog string literals --- tools/fix_open_scl_string_literals.py | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tools/fix_open_scl_string_literals.py diff --git a/tools/fix_open_scl_string_literals.py b/tools/fix_open_scl_string_literals.py new file mode 100644 index 000000000..ab7779684 --- /dev/null +++ b/tools/fix_open_scl_string_literals.py @@ -0,0 +1,30 @@ +from pathlib import Path + +path = Path(__file__).resolve().parents[1] / "MainWindow.xaml.cs" +raw = path.read_bytes() +newline = "\r\n" if b"\r\n" in raw else "\n" +text = raw.decode("utf-8").replace("\r\n", "\n") + +old_empty = ''' $"No usable IEC 61850 MMS endpoint was found in {sourceName}. + +{reason} + +The file may contain only an IED template without a Communication section.",''' +new_empty = ''' $"No usable IEC 61850 MMS endpoint was found in {sourceName}.\\n\\n{reason}\\n\\nThe file may contain only an IED template without a Communication section.",''' + +old_error = ''' $"ArIED could not read this SCL file. + +{ex.Message}",''' +new_error = ''' $"ArIED could not read this SCL file.\\n\\n{ex.Message}",''' + +for old, new, label in ( + (old_empty, new_empty, "empty endpoint dialog"), + (old_error, new_error, "error dialog"), +): + count = text.count(old) + if count != 1: + raise RuntimeError(f"{label}: expected exactly one match, found {count}") + text = text.replace(old, new, 1) + +path.write_bytes(text.replace("\n", newline).encode("utf-8")) +print("SCL dialog string literals repaired") From a407a8601e312736f0413e2ef561cf853622076f Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 06:10:21 +0700 Subject: [PATCH 06/11] ci: apply SCL string literal repair --- .../fix-open-scl-string-literals.yml | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/fix-open-scl-string-literals.yml diff --git a/.github/workflows/fix-open-scl-string-literals.yml b/.github/workflows/fix-open-scl-string-literals.yml new file mode 100644 index 000000000..0b5aeb14d --- /dev/null +++ b/.github/workflows/fix-open-scl-string-literals.yml @@ -0,0 +1,42 @@ +name: Fix Open SCL string literals + +on: + push: + branches: + - feat/open-scl-import + +permissions: + contents: write + +jobs: + apply: + runs-on: windows-latest + steps: + - name: Checkout feature branch + uses: actions/checkout@v4 + with: + ref: feat/open-scl-import + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Repair and validate source + shell: pwsh + run: | + python tools/fix_open_scl_string_literals.py + python -c "import xml.etree.ElementTree as E; E.parse('MainWindow.xaml'); print('XAML XML validation passed')" + git diff --check + + - name: Commit repaired source + shell: pwsh + run: | + git rm tools/fix_open_scl_string_literals.py + git rm .github/workflows/fix-open-scl-string-literals.yml + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add MainWindow.xaml.cs + git commit -m 'fix: use escaped newlines in SCL dialogs' + git push origin HEAD:feat/open-scl-import From 15f7391969fdacb79bcf6e10fc5976b52d674393 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 23:10:42 +0000 Subject: [PATCH 07/11] fix: use escaped newlines in SCL dialogs --- .../fix-open-scl-string-literals.yml | 42 ------------------- MainWindow.xaml.cs | 10 +---- tools/fix_open_scl_string_literals.py | 30 ------------- 3 files changed, 2 insertions(+), 80 deletions(-) delete mode 100644 .github/workflows/fix-open-scl-string-literals.yml delete mode 100644 tools/fix_open_scl_string_literals.py diff --git a/.github/workflows/fix-open-scl-string-literals.yml b/.github/workflows/fix-open-scl-string-literals.yml deleted file mode 100644 index 0b5aeb14d..000000000 --- a/.github/workflows/fix-open-scl-string-literals.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Fix Open SCL string literals - -on: - push: - branches: - - feat/open-scl-import - -permissions: - contents: write - -jobs: - apply: - runs-on: windows-latest - steps: - - name: Checkout feature branch - uses: actions/checkout@v4 - with: - ref: feat/open-scl-import - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Repair and validate source - shell: pwsh - run: | - python tools/fix_open_scl_string_literals.py - python -c "import xml.etree.ElementTree as E; E.parse('MainWindow.xaml'); print('XAML XML validation passed')" - git diff --check - - - name: Commit repaired source - shell: pwsh - run: | - git rm tools/fix_open_scl_string_literals.py - git rm .github/workflows/fix-open-scl-string-literals.yml - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add MainWindow.xaml.cs - git commit -m 'fix: use escaped newlines in SCL dialogs' - git push origin HEAD:feat/open-scl-import diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 5b7299866..86660102f 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -178,11 +178,7 @@ private async void OpenScl_Click(object sender, RoutedEventArgs e) AddLog("WARN", "SCL", $"{sourceName}: {reason}"); MessageBox.Show( this, - $"No usable IEC 61850 MMS endpoint was found in {sourceName}. - -{reason} - -The file may contain only an IED template without a Communication section.", + $"No usable IEC 61850 MMS endpoint was found in {sourceName}.\n\n{reason}\n\nThe file may contain only an IED template without a Communication section.", "Open SCL", MessageBoxButton.OK, MessageBoxImage.Information); @@ -264,9 +260,7 @@ private async void OpenScl_Click(object sender, RoutedEventArgs e) MarkDiagnosticAlert(); MessageBox.Show( this, - $"ArIED could not read this SCL file. - -{ex.Message}", + $"ArIED could not read this SCL file.\n\n{ex.Message}", "Open SCL", MessageBoxButton.OK, MessageBoxImage.Error); diff --git a/tools/fix_open_scl_string_literals.py b/tools/fix_open_scl_string_literals.py deleted file mode 100644 index ab7779684..000000000 --- a/tools/fix_open_scl_string_literals.py +++ /dev/null @@ -1,30 +0,0 @@ -from pathlib import Path - -path = Path(__file__).resolve().parents[1] / "MainWindow.xaml.cs" -raw = path.read_bytes() -newline = "\r\n" if b"\r\n" in raw else "\n" -text = raw.decode("utf-8").replace("\r\n", "\n") - -old_empty = ''' $"No usable IEC 61850 MMS endpoint was found in {sourceName}. - -{reason} - -The file may contain only an IED template without a Communication section.",''' -new_empty = ''' $"No usable IEC 61850 MMS endpoint was found in {sourceName}.\\n\\n{reason}\\n\\nThe file may contain only an IED template without a Communication section.",''' - -old_error = ''' $"ArIED could not read this SCL file. - -{ex.Message}",''' -new_error = ''' $"ArIED could not read this SCL file.\\n\\n{ex.Message}",''' - -for old, new, label in ( - (old_empty, new_empty, "empty endpoint dialog"), - (old_error, new_error, "error dialog"), -): - count = text.count(old) - if count != 1: - raise RuntimeError(f"{label}: expected exactly one match, found {count}") - text = text.replace(old, new, 1) - -path.write_bytes(text.replace("\n", newline).encode("utf-8")) -print("SCL dialog string literals repaired") From e6e96469cbe8b63387fc70d2dc2300ccc18346a6 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 06:13:53 +0700 Subject: [PATCH 08/11] ci: capture Open SCL build diagnostics --- .github/workflows/capture-open-scl-build.yml | 63 ++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/workflows/capture-open-scl-build.yml diff --git a/.github/workflows/capture-open-scl-build.yml b/.github/workflows/capture-open-scl-build.yml new file mode 100644 index 000000000..804fb9f40 --- /dev/null +++ b/.github/workflows/capture-open-scl-build.yml @@ -0,0 +1,63 @@ +name: Capture Open SCL build diagnostics + +on: + push: + branches: + - feat/open-scl-import + +permissions: + contents: write + +jobs: + capture: + runs-on: windows-latest + steps: + - name: Checkout ArIED application + uses: actions/checkout@v4 + with: + ref: feat/open-scl-import + path: ArIED61850Tester + fetch-depth: 0 + + - name: Checkout ARIEC61850 engine + uses: actions/checkout@v4 + with: + repository: masarray/ARIEC61850 + path: ARIEC61850 + + - name: Setup .NET 8 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Capture restore and build output + shell: pwsh + run: | + $log = Join-Path $env:GITHUB_WORKSPACE 'ArIED61850Tester\open-scl-build-diagnostic.txt' + "Open SCL build diagnostic" | Set-Content $log + "Commit: $env:GITHUB_SHA" | Add-Content $log + "" | Add-Content $log + dotnet restore .\ArIED61850Tester\ArIED61850Tester.csproj *>&1 | Tee-Object -FilePath $log -Append + $restoreExit = $LASTEXITCODE + "" | Add-Content $log + "RESTORE_EXIT=$restoreExit" | Add-Content $log + if ($restoreExit -eq 0) { + dotnet build .\ArIED61850Tester\ArIED61850Tester.csproj -c Release --no-restore *>&1 | Tee-Object -FilePath $log -Append + $buildExit = $LASTEXITCODE + } else { + $buildExit = -1 + } + "" | Add-Content $log + "BUILD_EXIT=$buildExit" | Add-Content $log + exit 0 + + - name: Commit diagnostic file and remove helper workflow + shell: pwsh + working-directory: ArIED61850Tester + run: | + git rm .github/workflows/capture-open-scl-build.yml + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add open-scl-build-diagnostic.txt + git commit -m 'ci: capture Open SCL build diagnostic' + git push origin HEAD:feat/open-scl-import From c70a7a8032bd82e6c26b89377ca2c858727b57d5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 23:14:44 +0000 Subject: [PATCH 09/11] ci: capture Open SCL build diagnostic --- .github/workflows/capture-open-scl-build.yml | 63 -------------------- open-scl-build-diagnostic.txt | 41 +++++++++++++ 2 files changed, 41 insertions(+), 63 deletions(-) delete mode 100644 .github/workflows/capture-open-scl-build.yml create mode 100644 open-scl-build-diagnostic.txt diff --git a/.github/workflows/capture-open-scl-build.yml b/.github/workflows/capture-open-scl-build.yml deleted file mode 100644 index 804fb9f40..000000000 --- a/.github/workflows/capture-open-scl-build.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Capture Open SCL build diagnostics - -on: - push: - branches: - - feat/open-scl-import - -permissions: - contents: write - -jobs: - capture: - runs-on: windows-latest - steps: - - name: Checkout ArIED application - uses: actions/checkout@v4 - with: - ref: feat/open-scl-import - path: ArIED61850Tester - fetch-depth: 0 - - - name: Checkout ARIEC61850 engine - uses: actions/checkout@v4 - with: - repository: masarray/ARIEC61850 - path: ARIEC61850 - - - name: Setup .NET 8 - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - - name: Capture restore and build output - shell: pwsh - run: | - $log = Join-Path $env:GITHUB_WORKSPACE 'ArIED61850Tester\open-scl-build-diagnostic.txt' - "Open SCL build diagnostic" | Set-Content $log - "Commit: $env:GITHUB_SHA" | Add-Content $log - "" | Add-Content $log - dotnet restore .\ArIED61850Tester\ArIED61850Tester.csproj *>&1 | Tee-Object -FilePath $log -Append - $restoreExit = $LASTEXITCODE - "" | Add-Content $log - "RESTORE_EXIT=$restoreExit" | Add-Content $log - if ($restoreExit -eq 0) { - dotnet build .\ArIED61850Tester\ArIED61850Tester.csproj -c Release --no-restore *>&1 | Tee-Object -FilePath $log -Append - $buildExit = $LASTEXITCODE - } else { - $buildExit = -1 - } - "" | Add-Content $log - "BUILD_EXIT=$buildExit" | Add-Content $log - exit 0 - - - name: Commit diagnostic file and remove helper workflow - shell: pwsh - working-directory: ArIED61850Tester - run: | - git rm .github/workflows/capture-open-scl-build.yml - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add open-scl-build-diagnostic.txt - git commit -m 'ci: capture Open SCL build diagnostic' - git push origin HEAD:feat/open-scl-import diff --git a/open-scl-build-diagnostic.txt b/open-scl-build-diagnostic.txt new file mode 100644 index 000000000..687d04d5d --- /dev/null +++ b/open-scl-build-diagnostic.txt @@ -0,0 +1,41 @@ +Open SCL build diagnostic +Commit: e6e96469cbe8b63387fc70d2dc2300ccc18346a6 + + Determining projects to restore... + Restored D:\a\ArIED61850Tester\ArIED61850Tester\ARIEC61850\src\AR.Iec61850\AR.Iec61850.csproj (in 136 ms). + Restored D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj (in 153 ms). + +RESTORE_EXIT=0 + ArIED 61850 is compiling against ARIEC61850 project: ..\ARIEC61850\src\AR.Iec61850\AR.Iec61850.csproj + AR.Iec61850 -> D:\a\ArIED61850Tester\ArIED61850Tester\ARIEC61850\.artifacts\bin\AR.Iec61850\Release\net8.0\AR.Iec61850.dll +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(34,50): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(47,50): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(56,50): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(65,50): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(72,46): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(53,14): error CS0103: The name 'File' does not exist in the current context [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(54,23): error CS0246: The type or namespace name 'FileNotFoundException' could not be found (are you missing a using directive or an assembly reference?) [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(67,29): error CS0103: The name 'File' does not exist in the current context [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(72,47): error CS0246: The type or namespace name 'InvalidDataException' could not be found (are you missing a using directive or an assembly reference?) [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(74,23): error CS0246: The type or namespace name 'InvalidDataException' could not be found (are you missing a using directive or an assembly reference?) [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(154,30): error CS0103: The name 'Path' does not exist in the current context [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] + +Build FAILED. + +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(34,50): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(47,50): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(56,50): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(65,50): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(72,46): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(53,14): error CS0103: The name 'File' does not exist in the current context [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(54,23): error CS0246: The type or namespace name 'FileNotFoundException' could not be found (are you missing a using directive or an assembly reference?) [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(67,29): error CS0103: The name 'File' does not exist in the current context [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(72,47): error CS0246: The type or namespace name 'InvalidDataException' could not be found (are you missing a using directive or an assembly reference?) [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(74,23): error CS0246: The type or namespace name 'InvalidDataException' could not be found (are you missing a using directive or an assembly reference?) [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] +D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(154,30): error CS0103: The name 'Path' does not exist in the current context [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] + 5 Warning(s) + 6 Error(s) + +Time Elapsed 00:00:16.94 + +BUILD_EXIT=1 From aebf05a094b080a90f035cc92de43d6c9111a117 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 06:15:16 +0700 Subject: [PATCH 10/11] fix: import System.IO for SCL file access --- Services/SclImportService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Services/SclImportService.cs b/Services/SclImportService.cs index 4f2fe0219..77bcc13c4 100644 --- a/Services/SclImportService.cs +++ b/Services/SclImportService.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.IO; using System.Net; using System.Xml; using System.Xml.Linq; From 96f0007c90c30954b6228e4c5a31c1c6a8f109ed Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 06:15:20 +0700 Subject: [PATCH 11/11] chore: remove temporary SCL build diagnostic --- open-scl-build-diagnostic.txt | 41 ----------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 open-scl-build-diagnostic.txt diff --git a/open-scl-build-diagnostic.txt b/open-scl-build-diagnostic.txt deleted file mode 100644 index 687d04d5d..000000000 --- a/open-scl-build-diagnostic.txt +++ /dev/null @@ -1,41 +0,0 @@ -Open SCL build diagnostic -Commit: e6e96469cbe8b63387fc70d2dc2300ccc18346a6 - - Determining projects to restore... - Restored D:\a\ArIED61850Tester\ArIED61850Tester\ARIEC61850\src\AR.Iec61850\AR.Iec61850.csproj (in 136 ms). - Restored D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj (in 153 ms). - -RESTORE_EXIT=0 - ArIED 61850 is compiling against ARIEC61850 project: ..\ARIEC61850\src\AR.Iec61850\AR.Iec61850.csproj - AR.Iec61850 -> D:\a\ArIED61850Tester\ArIED61850Tester\ARIEC61850\.artifacts\bin\AR.Iec61850\Release\net8.0\AR.Iec61850.dll -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(34,50): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(47,50): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(56,50): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(65,50): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(72,46): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(53,14): error CS0103: The name 'File' does not exist in the current context [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(54,23): error CS0246: The type or namespace name 'FileNotFoundException' could not be found (are you missing a using directive or an assembly reference?) [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(67,29): error CS0103: The name 'File' does not exist in the current context [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(72,47): error CS0246: The type or namespace name 'InvalidDataException' could not be found (are you missing a using directive or an assembly reference?) [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(74,23): error CS0246: The type or namespace name 'InvalidDataException' could not be found (are you missing a using directive or an assembly reference?) [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(154,30): error CS0103: The name 'Path' does not exist in the current context [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] - -Build FAILED. - -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(34,50): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(47,50): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(56,50): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(65,50): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\Iec61850ConnectionFailureClassifier.cs(72,46): warning CS8604: Possible null reference argument for parameter 'associationSummary' in 'string Iec61850ConnectionFailureClassifier.BuildTechnicalSummary(Exception? exception, string associationSummary, SocketError? socketError)'. [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(53,14): error CS0103: The name 'File' does not exist in the current context [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(54,23): error CS0246: The type or namespace name 'FileNotFoundException' could not be found (are you missing a using directive or an assembly reference?) [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(67,29): error CS0103: The name 'File' does not exist in the current context [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(72,47): error CS0246: The type or namespace name 'InvalidDataException' could not be found (are you missing a using directive or an assembly reference?) [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(74,23): error CS0246: The type or namespace name 'InvalidDataException' could not be found (are you missing a using directive or an assembly reference?) [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] -D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\Services\SclImportService.cs(154,30): error CS0103: The name 'Path' does not exist in the current context [D:\a\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester\ArIED61850Tester.csproj] - 5 Warning(s) - 6 Error(s) - -Time Elapsed 00:00:16.94 - -BUILD_EXIT=1