From 427edc7f93ba69aad869a5a69574db43b2681e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BChlegger?= Date: Fri, 6 Mar 2026 12:29:16 +0100 Subject: [PATCH 1/4] add local test branch for easy testing --- DotNetSiemensPLCToolBoxLibrary.sln | 23 + SdaTest/Program.cs | 15 + SdaTest/ProjectConversionCommandHandler.cs | 547 +++++++++++++++++++++ SdaTest/SdaTest.csproj | 17 + 4 files changed, 602 insertions(+) create mode 100644 SdaTest/Program.cs create mode 100644 SdaTest/ProjectConversionCommandHandler.cs create mode 100644 SdaTest/SdaTest.csproj diff --git a/DotNetSiemensPLCToolBoxLibrary.sln b/DotNetSiemensPLCToolBoxLibrary.sln index d15b0163..ab38edb2 100644 --- a/DotNetSiemensPLCToolBoxLibrary.sln +++ b/DotNetSiemensPLCToolBoxLibrary.sln @@ -107,6 +107,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetSiemensPLCToolBoxLibr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetSiemensPLCToolBoxLibrary.TIAV21", "DotNetSiemensPLCToolBoxLibrary.TIAV21\DotNetSiemensPLCToolBoxLibrary.TIAV21.csproj", "{42C91E85-7052-F066-3257-5F05F7258BAC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SdaTest", "SdaTest\SdaTest.csproj", "{A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -827,6 +829,26 @@ Global {42C91E85-7052-F066-3257-5F05F7258BAC}.Release|Mixed Platforms.Build.0 = Release|Any CPU {42C91E85-7052-F066-3257-5F05F7258BAC}.Release|x86.ActiveCfg = Release|Any CPU {42C91E85-7052-F066-3257-5F05F7258BAC}.Release|x86.Build.0 = Release|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|iPhone.Build.0 = Debug|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|x86.ActiveCfg = Debug|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|x86.Build.0 = Debug|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|Any CPU.Build.0 = Release|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|iPhone.ActiveCfg = Release|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|iPhone.Build.0 = Release|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|x86.ActiveCfg = Release|Any CPU + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -860,6 +882,7 @@ Global {B2D9C814-26B4-46DF-AEAF-E82606E4E8B2} = {A4F11331-531B-4A7C-84B2-E319658AE2B8} {5623A0DA-4CC8-4954-AD76-90C25E10B2D2} = {A4F11331-531B-4A7C-84B2-E319658AE2B8} {5FC399A5-1C21-480A-987F-16388079F245} = {66A4ADD6-8EE2-49BD-B939-C5E69CAA3A9E} + {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01} = {14BB6088-3FC3-4643-B170-0BF3F6F91028} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {629DCEA8-C0FA-4650-97C6-ADD1E09A377D} diff --git a/SdaTest/Program.cs b/SdaTest/Program.cs new file mode 100644 index 00000000..db395ac1 --- /dev/null +++ b/SdaTest/Program.cs @@ -0,0 +1,15 @@ +using System; + +namespace SdaTest +{ + internal class Program + { + public static void Main(string[] args) + { + Console.WriteLine("Testing..."); + var projectPath = "/Users/tom/Downloads/broken_s7.zip"; + var exportPath = "/Users/tom/Downloads/export"; + ProjectConversionCommandHandler.Export(projectPath, exportPath); + } + } +} \ No newline at end of file diff --git a/SdaTest/ProjectConversionCommandHandler.cs b/SdaTest/ProjectConversionCommandHandler.cs new file mode 100644 index 00000000..9cba527b --- /dev/null +++ b/SdaTest/ProjectConversionCommandHandler.cs @@ -0,0 +1,547 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Xml.Linq; +using DotNetSiemensPLCToolBoxLibrary.DataTypes; +using DotNetSiemensPLCToolBoxLibrary.DataTypes.Blocks; +using DotNetSiemensPLCToolBoxLibrary.DataTypes.Blocks.Step7V5; +using DotNetSiemensPLCToolBoxLibrary.DataTypes.Hardware.Step7V5; +using DotNetSiemensPLCToolBoxLibrary.DataTypes.Network; +using DotNetSiemensPLCToolBoxLibrary.DataTypes.Projectfolders; +using DotNetSiemensPLCToolBoxLibrary.DataTypes.Projectfolders.Step7V5; +using DotNetSiemensPLCToolBoxLibrary.Projectfiles; +using Newtonsoft.Json; + +namespace SdaTest +{ + public class DeviceInfoEthernetCommunication + { + [JsonProperty("ip_address")] public string IpAddress { get; set; } + [JsonProperty("subnet_mask")] public string SubnetMask { get; set; } + } + + public class DeviceInfo + { + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("description")] public string Description { get; set; } + [JsonProperty("order_number")] public string OrderNumber { get; set; } + [JsonProperty("device_type")] public string DeviceType { get; set; } + + [JsonProperty("origin_device_type")] + public string OriginDeviceType { get; set; } + + [JsonProperty("ethernet_communication")] + public List EthernetCommunication { get; set; } + + [JsonProperty("password_protected")] + public bool PasswordProtected { get; set; } + } + + public class ProjectConversionCommandHandler + { + private static string _outputDir = ""; + private const string CpuDir = "cpu"; + private const string CpDir = "cp"; + private const string SymbolsDir = "symbols"; + private const string SourcesDir = "sources"; + private const string BlocksDir = "blocks"; + + private static void CreateWorkingDirectory(string directory, bool cleanup = false) + { + if (cleanup && Directory.Exists(directory)) + { + Directory.Delete(directory, true); + } + + Directory.CreateDirectory(directory); + } + + public static void Export(string projectPath, string outputPath, string? username = null, + string? password = null) + { + _outputDir = outputPath; + CreateWorkingDirectory(_outputDir); + CreateWorkingDirectory(Path.Combine(_outputDir, CpuDir), true); + CreateWorkingDirectory(Path.Combine(_outputDir, CpDir), true); + CreateWorkingDirectory(Path.Combine(_outputDir, SymbolsDir), true); + CreateWorkingDirectory(Path.Combine(_outputDir, SourcesDir), true); + CreateWorkingDirectory(Path.Combine(_outputDir, BlocksDir), true); + + Credentials? credentials = null; + + if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password)) + { + credentials = new Credentials() { Username = username, Password = new System.Security.SecureString() }; + foreach (var c in password) + { + credentials.Password.AppendChar(c); + } + } + + var project = Projects.LoadProject(projectPath, false, credentials); + // Setting the project language here (always use English) + project.ProjectLanguage = MnemonicLanguage.English; + + var projectItem = new XElement("ProjectItem", + new XAttribute("Name", "Project"), + new XAttribute("Type", "Folder")); + + // Parse the project and create the XML structure and write the XML files to the outputPath + AddXmlNodes(projectItem, project.ProjectStructure.SubItems); + + var xmlDocument = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), projectItem); + xmlDocument.Save(Path.Combine(outputPath, "project_tree.xml")); + + var stationConfigurations = project.ProjectStructure.SubItems + .FindAll(sc => sc is StationConfigurationFolder) + .ConvertAll(sc => (StationConfigurationFolder)sc); + GenerateDeviceConnectionInfo(stationConfigurations); + } + + private static string HashContent(string content) + { + var sha256 = SHA256.Create(); + var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(content)); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + + /** + * Create a new file with the given parameters and return the File XML item + */ + private static XElement CreateFile(XElement item, string subDirectory, string uniqueFileName, + string contentType) + { + var hashedFileName = $"{HashContent(uniqueFileName)}.xml"; + var relativeFilePath = Path.Combine(subDirectory, hashedFileName); + var absoluteFilePath = Path.Combine(_outputDir, relativeFilePath); + var xmlRelativeFilePath = $"{subDirectory}/{hashedFileName}"; + var newXmlDoc = new XDocument(item); + var newXmlContent = newXmlDoc.ToString(); + File.WriteAllText(absoluteFilePath, newXmlContent); + var fileHash = HashContent(newXmlContent); + return new XElement("File", new XAttribute("DiffHash", fileHash), + new XAttribute("ContentType", contentType), + xmlRelativeFilePath); + } + + private static XElement CreateItemElement(string name, string type) + { + return new XElement("Item", new XAttribute("Name", name), new XAttribute("Type", type)); + } + + private static XElement CreateSdaAttributeListElement(List> sdaAttributes, List columns, + string? title = null, string? comment = null) + { + var listRoot = new XElement("SdaTreeTable"); + + if (title is not null) + { + listRoot.Add(new XElement("Title", title)); + } + + if (comment is not null) + { + listRoot.Add(new XElement("Comment", comment)); + } + + var columnConfig = new XElement("ColumnsConfig", new XAttribute("KeyColumn", columns.First())); + foreach (var column in columns) + { + columnConfig.Add(new XElement("Column", new XAttribute("Col", column), + new XAttribute("Label", column))); + } + + listRoot.Add(columnConfig); + + var rows = new XElement("Rows"); + foreach (var sdaAttribute in sdaAttributes) + { + var row = new XElement("Row"); + + for (var i = 0; i < sdaAttribute.Count; i++) + { + row.Add(new XElement("Cell", new XAttribute("Col", columns[i]), sdaAttribute[i])); + } + + rows.Add(row); + } + + listRoot.Add(rows); + return listRoot; + } + + private static XElement GetNetworkInterfaceList(string title, List networkInterfaces) + { + var interfaceColumns = new List { "Name", "Type", "Address" }; + var interfaceAttributes = new List>(); + + foreach (var networkInterface in networkInterfaces) + { + var address = networkInterface.ToString(); + + switch (networkInterface) + { + case EthernetNetworkInterface ethernetNetworkInterface: + var mac = string.Join(":", + Array.ConvertAll(ethernetNetworkInterface.Mac.GetAddressBytes(), b => b.ToString("X2"))); + address = ethernetNetworkInterface.IpAddress + " (" + mac + ")"; + break; + case MpiProfiBusNetworkInterface mpiProfiBusNetworkInterface: + address = "Address: " + mpiProfiBusNetworkInterface.Address; + break; + } + + interfaceAttributes.Add(new List + { + networkInterface.Name, + networkInterface.NetworkInterfaceType.ToString(), + address + }); + } + + return CreateSdaAttributeListElement(interfaceAttributes, interfaceColumns, title); + } + + private static void AddStationConfigurationDetails(XElement rootXml, + StationConfigurationFolder stationConfiguration) + { + const string treeTableType = "SdaTreeTables"; + var tableRoot = new XElement(treeTableType); + // Set the type of the parent item + rootXml.SetAttributeValue("Type", treeTableType); + + foreach (var masterSystem in stationConfiguration.MasterSystems) + { + var interfaceColumns = new List { "Type", "Module" }; + var title = masterSystem.Name + " Interfaces"; + var interfaceAttributes = (from networkInterface in masterSystem.Children + let type = networkInterface.GetType().Name + " (" + networkInterface.NodeId + ")" + select new List { type, networkInterface.Name }).ToList(); + tableRoot.Add(CreateSdaAttributeListElement(interfaceAttributes, interfaceColumns, title)); + } + + var uniqueStationName = stationConfiguration.StructuredFolderName; + rootXml.Add(CreateFile(tableRoot, CpuDir, uniqueStationName, treeTableType)); + } + + private static void AddCpuDetails(XElement rootXml, CPUFolder cpuFolder) + { + if (cpuFolder.NetworkInterfaces == null) return; + const string treeTableType = "SdaTreeTables"; + var tableRoot = new XElement(treeTableType); + // Set the type of the parent item + rootXml.SetAttributeValue("Type", treeTableType); + tableRoot.Add(GetNetworkInterfaceList("Network Interfaces", cpuFolder.NetworkInterfaces)); + var uniqueCpuName = cpuFolder.StructuredFolderName; + rootXml.Add(CreateFile(tableRoot, CpuDir, uniqueCpuName, treeTableType)); + } + + private static void AddCpDetails(XElement rootXml, CPFolder cpFolder) + { + if (cpFolder.NetworkInterfaces == null) return; + const string treeTableType = "SdaTreeTables"; + var tableRoot = new XElement(treeTableType); + // Set the type of the parent item + rootXml.SetAttributeValue("Type", treeTableType); + + tableRoot.Add(GetNetworkInterfaceList("Network Interfaces", cpFolder.NetworkInterfaces)); + var uniqueCpName = cpFolder.StructuredFolderName; + rootXml.Add(CreateFile(tableRoot, CpDir, uniqueCpName, treeTableType)); + } + + private static void AddInterfaceParameters(XElement rootXml, List parameters) + { + foreach (var parameter in parameters) + { + try + { + if (parameter.LibNoDaveValue == null) + { + // This can be the case if empty (placeholders) are used + rootXml.Add(new XElement("variable", new XAttribute("Name", ""))); + continue; + } + + var varItem = new XElement("variable", new XAttribute("Name", parameter.LibNoDaveValue.ToString())); + + if (!string.IsNullOrEmpty(parameter.Comment)) + { + varItem.Add(new XElement("comment", parameter.Comment)); + } + + varItem.Add(new XElement("dataType", parameter.LibNoDaveValue.DataTypeStringFormat.ToString())); + varItem.Add(new XElement("address", parameter.S7FormatAddress)); + + rootXml.Add(varItem); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + } + + private static void AddInterfaceParameters(XElement rootXml, IDataRow parameter) + { + var interfaceItem = new XElement("interface"); + + AddInterfaceParameters(interfaceItem, parameter.Children); + + rootXml.Add(interfaceItem); + } + + private static void AddInterfaceParameters(XElement rootXml, List parameters) + { + foreach (var parameter in parameters) + { + var varItem = new XElement("variable", new XAttribute("Name", parameter.Name)); + + if (parameter is DataBlockRow dataBlockRow) + { + varItem.Add(new XElement("dataType", dataBlockRow.DataTypeAsString)); + } + else + { + varItem.Add(new XElement("dataType", parameter.DataType.ToString())); + } + + if (!string.IsNullOrEmpty(parameter.Comment)) + { + varItem.Add(new XElement("comment", parameter.Comment)); + } + + varItem.Add(new XElement("address", parameter.BlockAddress.ToString())); + + if (parameter is S7DataRow { StartValueAsString: not null } s7DataRowParameter) + { + varItem.Add(new XElement("initialValue", s7DataRowParameter.StartValueAsString)); + } + + AddInterfaceParameters(varItem, parameter.Children); + + rootXml.Add(varItem); + } + } + + // Method to determine if a character is valid for XML + private static bool IsValidXmlChar(char c) + { + return (c == 0x9 || c == 0xA || c == 0xD || (c >= 0x20 && c <= 0xD7FF) || (c >= 0xE000 && c <= 0xFFFD) || (c >= 0x10000 && c <= 0x10FFFF)); + } + + private static void AddBlocks(XElement rootXml, BlocksOfflineFolder blocksOfflineFolder) + { + var blocks = blocksOfflineFolder.BlockInfos.Select(blockIt => blockIt.GetBlock()); + + foreach (var block in blocks) + { + try + { + var blockType = block.GetType().Name; + var blockItem = CreateItemElement(block.BlockName, blockType); + var blockContent = new XElement(blockType, new XAttribute("Name", block.BlockName), + new XAttribute("Type", blockType)); + + + switch (block) + { + case S7FunctionBlock s7FunctionBlock: + // TODO: Check the mnemonic language + s7FunctionBlock.MnemonicLanguage = MnemonicLanguage.English; + + if (!string.IsNullOrEmpty(s7FunctionBlock.Title)) + { + blockContent.Add(new XAttribute("Title", s7FunctionBlock.Title)); + } + + if (!string.IsNullOrEmpty(s7FunctionBlock.Description)) + { + blockContent.Add(new XAttribute("Description", s7FunctionBlock.Description)); + } + + AddInterfaceParameters(blockContent, s7FunctionBlock.Parameter); + + var networksItem = new XElement("Networks"); + + foreach (var fbNetwork in s7FunctionBlock.Networks) + { + var networkItem = new XElement("Network", new XAttribute("Title", fbNetwork.Name)); + networkItem.Add(new XElement("comment", fbNetwork.Comment)); + var rawAwl = fbNetwork.AWLCodeToString(); + var sanitizedAwl = new string(rawAwl.Select(c => IsValidXmlChar(c) ? c : '?').ToArray()); + networkItem.Add(new XElement("AWL", sanitizedAwl)); + // Add the network item to the networks + networksItem.Add(networkItem); + } + + blockContent.Add(networksItem); + break; + case S7DataBlock s7DataBlock: + if (!string.IsNullOrEmpty(s7DataBlock.Title)) + { + blockContent.Add(new XAttribute("Title", s7DataBlock.Title)); + } + + AddInterfaceParameters(blockContent, s7DataBlock.Structure.Children); + break; + case S7VATBlock s7VatBlock: + AddInterfaceParameters(blockContent, s7VatBlock.VATRows); + break; + } + + var uniqueBlockName = blocksOfflineFolder.StructuredFolderName + "/" + block.BlockName; + blockItem.Add( + CreateFile(blockContent, BlocksDir, uniqueBlockName, blockType)); + + rootXml.Add(blockItem); + } + catch (Exception ex) + { + Console.WriteLine( + $"Error when extracting block {block.BlockName} of type {block.BlockType.ToString()} {ex.Message}"); + Console.Error.WriteLine(ex.StackTrace); + } + } + } + + private static void AddSymbols(XElement rootXml, SymbolTable symbolTable) + { + var symbolTableType = $"S7{symbolTable.GetType().Name}"; + var symbolTableContent = new XElement(symbolTableType, new XAttribute("Name", symbolTable.Name), + new XAttribute("Type", symbolTableType)); + + // Set the type of the parent item + rootXml.SetAttributeValue("Type", symbolTableType); + + var symbolEntries = symbolTable.SymbolTableEntrys; + symbolEntries.Sort((x, y) => string.Compare(x.Symbol, y.Symbol, StringComparison.Ordinal)); + + foreach (var symbolTableEntry in symbolEntries) + { + var symbolItem = new XElement("symbol"); + symbolItem.Add(new XAttribute("Symbol", symbolTableEntry.Symbol)); + symbolItem.Add(new XAttribute("Comment", symbolTableEntry.Comment)); + symbolItem.Add(new XAttribute("Type", symbolTableEntry.DataType)); + symbolItem.Add(new XAttribute("Address", symbolTableEntry.OperandIEC)); + + symbolTableContent.Add(symbolItem); + } + + var uniqueSymbolName = symbolTable.StructuredFolderName; + rootXml.Add( + CreateFile(symbolTableContent, SymbolsDir, uniqueSymbolName, symbolTableType)); + } + + private static void AddSources(XElement rootXml, SourceFolder sources) + { + foreach (var blockInfo in sources.BlockInfos) + { + var sourceBlockType = blockInfo.GetType().Name; + var sourceBlockItem = CreateItemElement(blockInfo.Name, sourceBlockType); + var sourceBlockContent = new XElement(sourceBlockType, new XAttribute("Name", blockInfo.Name), + new XAttribute("Type", sourceBlockType)); + + if (blockInfo is S7ProjectSourceInfo s7ProjectSourceInfo) + { + var rawSource = sources.GetSource(s7ProjectSourceInfo); + var source = new string(rawSource.Select(c => IsValidXmlChar(c) ? c : '?').ToArray()); + sourceBlockContent.Add(source); + sourceBlockContent.Add(source); + } + + var uniqueSourceName = sources.StructuredFolderName + "/" + blockInfo.Name; + sourceBlockItem.Add( + CreateFile(sourceBlockContent, SourcesDir, uniqueSourceName, sourceBlockType)); + rootXml.Add(sourceBlockItem); + } + } + + private static void AddXmlNodes(XElement rootXml, List items) + { + foreach (var subitem in items) + { + if (subitem is MasterSystem) + { + continue; + } + + var subItemElement = CreateItemElement(subitem.Name, "item"); + + switch (subitem) + { + case StationConfigurationFolder stationConfiguration: + AddStationConfigurationDetails(subItemElement, stationConfiguration); + break; + case CPUFolder cpuFolder: + AddCpuDetails(subItemElement, cpuFolder); + break; + case CPFolder cpFolder: + AddCpDetails(subItemElement, cpFolder); + break; + case SymbolTable symbolTable: + AddSymbols(subItemElement, symbolTable); + break; + case BlocksOfflineFolder blocksOfflineFolder: + AddBlocks(subItemElement, blocksOfflineFolder); + break; + case SourceFolder sourceFolder: + AddSources(subItemElement, sourceFolder); + break; + } + + if (subitem.SubItems != null) AddXmlNodes(subItemElement, subitem.SubItems); + rootXml.Add(subItemElement); + } + } + + private static void GenerateDeviceConnectionInfo(List stationConfigurations) + { + try + { + // Create device information list and append a new device + var deviceInformation = new List(); + + foreach (var stationConfiguration in stationConfigurations) + { + var cpu = stationConfiguration.SubItems.FirstOrDefault(x => x is CPUFolder) as CPUFolder; + var cp = stationConfiguration.SubItems.FirstOrDefault(x => x is CPFolder) as CPFolder; + var ethernetInterfaces = cp?.NetworkInterfaces + .FindAll(ni => ni is EthernetNetworkInterface) + .ConvertAll(ni => (EthernetNetworkInterface)ni); + + var ethernetCommunication = ethernetInterfaces? + .Select(ni => new DeviceInfoEthernetCommunication + { + IpAddress = ni.IpAddress.ToString(), + SubnetMask = ni.SubnetMask.ToString() + }) + .ToList() ?? new List(); + + deviceInformation.Add(new DeviceInfo + { + Name = stationConfiguration.Name, + Description = cpu?.Name ?? "Unknown", + OrderNumber = cpu?.MLFB_OrderNumber, + DeviceType = "plc", + OriginDeviceType = stationConfiguration.StationType.ToString(), + EthernetCommunication = ethernetCommunication, + PasswordProtected = cpu?.PasswdHard != null, + }); + } + + + var deviceJsonFilePath = Path.Combine(_outputDir, "devices_connection_information.json"); + File.WriteAllText(deviceJsonFilePath, JsonConvert.SerializeObject(deviceInformation, Formatting.Indented)); + + Console.WriteLine("Device information has been written to JSON file."); + } + catch (Exception e) + { + Console.Error.WriteLine($"Error when generating the devices connection info {e.Message}"); + } + } + } +} \ No newline at end of file diff --git a/SdaTest/SdaTest.csproj b/SdaTest/SdaTest.csproj new file mode 100644 index 00000000..0b69bc12 --- /dev/null +++ b/SdaTest/SdaTest.csproj @@ -0,0 +1,17 @@ + + + + Exe + 9 + net48 + + + + + + + + + + + From 97b6737a803437640dada0baafeabacd5d0e4c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BChlegger?= Date: Fri, 6 Mar 2026 13:44:32 +0100 Subject: [PATCH 2/4] fix: project handling if project file is under root level --- LibNoDaveConnectionLibrary/Projectfiles/Step7ProjectV5.cs | 8 +++++++- SdaTest/Program.cs | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/LibNoDaveConnectionLibrary/Projectfiles/Step7ProjectV5.cs b/LibNoDaveConnectionLibrary/Projectfiles/Step7ProjectV5.cs index 7ee6e46c..0e0605e2 100644 --- a/LibNoDaveConnectionLibrary/Projectfiles/Step7ProjectV5.cs +++ b/LibNoDaveConnectionLibrary/Projectfiles/Step7ProjectV5.cs @@ -74,7 +74,13 @@ public Step7ProjectV5(string projectfile, bool showDeleted, Encoding prEn) } ProjectFile = projectfile; - ProjectFolder = _projectfilename.Substring(0, _projectfilename.LastIndexOf(_DirSeperator)) + _DirSeperator; + ProjectFolder = ""; + + // Check if the s7p file is at the root level (so no / in the path) or in a subfolder and set the ProjectFolder accordingly + if (_projectfilename.IndexOf(_DirSeperator) != -1) + { + ProjectFolder = _projectfilename.Substring(0, _projectfilename.LastIndexOf(_DirSeperator)) + _DirSeperator; + } ProjectEncoding = (prEn ?? Encoding.GetEncoding("ISO-8859-1")); var lngFile = _ziphelper.GetReadStream(ProjectFolder + "Global" + _DirSeperator + "Language"); diff --git a/SdaTest/Program.cs b/SdaTest/Program.cs index db395ac1..40c71a3a 100644 --- a/SdaTest/Program.cs +++ b/SdaTest/Program.cs @@ -7,8 +7,8 @@ internal class Program public static void Main(string[] args) { Console.WriteLine("Testing..."); - var projectPath = "/Users/tom/Downloads/broken_s7.zip"; - var exportPath = "/Users/tom/Downloads/export"; + var projectPath = "/Users/tom/Downloads/S7/test.zip"; + var exportPath = "/Users/tom/Downloads/S7/export"; ProjectConversionCommandHandler.Export(projectPath, exportPath); } } From a327ca6634ed4379c330ab3e24974f452c365fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BChlegger?= Date: Fri, 6 Mar 2026 14:44:45 +0100 Subject: [PATCH 3/4] fix: handling of protected blocks --- SdaTest/ProjectConversionCommandHandler.cs | 277 ++++++++++++++++----- SdaTest/SdaTest.csproj | 1 + 2 files changed, 217 insertions(+), 61 deletions(-) diff --git a/SdaTest/ProjectConversionCommandHandler.cs b/SdaTest/ProjectConversionCommandHandler.cs index 9cba527b..c97b5503 100644 --- a/SdaTest/ProjectConversionCommandHandler.cs +++ b/SdaTest/ProjectConversionCommandHandler.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Security.Cryptography; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Xml.Linq; using DotNetSiemensPLCToolBoxLibrary.DataTypes; using DotNetSiemensPLCToolBoxLibrary.DataTypes.Blocks; @@ -13,30 +15,29 @@ using DotNetSiemensPLCToolBoxLibrary.DataTypes.Projectfolders; using DotNetSiemensPLCToolBoxLibrary.DataTypes.Projectfolders.Step7V5; using DotNetSiemensPLCToolBoxLibrary.Projectfiles; -using Newtonsoft.Json; namespace SdaTest { public class DeviceInfoEthernetCommunication { - [JsonProperty("ip_address")] public string IpAddress { get; set; } - [JsonProperty("subnet_mask")] public string SubnetMask { get; set; } + [JsonPropertyName("ip_address")] public string IpAddress { get; set; } + [JsonPropertyName("subnet_mask")] public string SubnetMask { get; set; } } public class DeviceInfo { - [JsonProperty("name")] public string Name { get; set; } - [JsonProperty("description")] public string Description { get; set; } - [JsonProperty("order_number")] public string OrderNumber { get; set; } - [JsonProperty("device_type")] public string DeviceType { get; set; } + [JsonPropertyName("name")] public string Name { get; set; } + [JsonPropertyName("description")] public string Description { get; set; } + [JsonPropertyName("order_number")] public string OrderNumber { get; set; } + [JsonPropertyName("device_type")] public string DeviceType { get; set; } - [JsonProperty("origin_device_type")] + [JsonPropertyName("origin_device_type")] public string OriginDeviceType { get; set; } - [JsonProperty("ethernet_communication")] + [JsonPropertyName("ethernet_communication")] public List EthernetCommunication { get; set; } - [JsonProperty("password_protected")] + [JsonPropertyName("password_protected")] public bool PasswordProtected { get; set; } } @@ -46,9 +47,14 @@ public class ProjectConversionCommandHandler private const string CpuDir = "cpu"; private const string CpDir = "cp"; private const string SymbolsDir = "symbols"; + private const string InterfacesDir = "interfaces"; private const string SourcesDir = "sources"; private const string BlocksDir = "blocks"; + // These dictionaries are storing the file paths for the symbols and interfaces for given CPUs + private static Dictionary _symbolsFilePaths = new Dictionary(); + private static Dictionary _interfacesXmls = new Dictionary(); + private static void CreateWorkingDirectory(string directory, bool cleanup = false) { if (cleanup && Directory.Exists(directory)) @@ -59,46 +65,98 @@ private static void CreateWorkingDirectory(string directory, bool cleanup = fals Directory.CreateDirectory(directory); } + private static void FailIfAnyErrors() + { + if (_failures.Count != 0) throw new Exception("Conversion Failed"); + + Console.WriteLine("Export finished successfully."); + } + + private class ExtractionFailure + { + public string Scope { get; set; } = ""; + public string Path { get; set; } = ""; + public string Name { get; set; } = ""; + public string Details { get; set; } = ""; + + } + + private static readonly List _failures = new(); + + private static void AddFailure(string scope, string path, string name, string errorMessage) + { + + Console.WriteLine($"[FAIL] [{scope}] {path}/{name}: {errorMessage}"); + _failures.Add(new ExtractionFailure + { + Scope = scope ?? "", + Path = path ?? "", + Name = name ?? "", + Details = errorMessage + }); + } + public static void Export(string projectPath, string outputPath, string? username = null, string? password = null) { - _outputDir = outputPath; - CreateWorkingDirectory(_outputDir); - CreateWorkingDirectory(Path.Combine(_outputDir, CpuDir), true); - CreateWorkingDirectory(Path.Combine(_outputDir, CpDir), true); - CreateWorkingDirectory(Path.Combine(_outputDir, SymbolsDir), true); - CreateWorkingDirectory(Path.Combine(_outputDir, SourcesDir), true); - CreateWorkingDirectory(Path.Combine(_outputDir, BlocksDir), true); + try + { + _outputDir = outputPath; + CreateWorkingDirectory(_outputDir); + CreateWorkingDirectory(Path.Combine(_outputDir, CpuDir), true); + CreateWorkingDirectory(Path.Combine(_outputDir, CpDir), true); + CreateWorkingDirectory(Path.Combine(_outputDir, SymbolsDir), true); + CreateWorkingDirectory(Path.Combine(_outputDir, InterfacesDir), true); + CreateWorkingDirectory(Path.Combine(_outputDir, SourcesDir), true); + CreateWorkingDirectory(Path.Combine(_outputDir, BlocksDir), true); + + Credentials? credentials = null; + + if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password)) + { + credentials = new Credentials() { Username = username, Password = new System.Security.SecureString() }; + foreach (var c in password) + { + credentials.Password.AppendChar(c); + } + } - Credentials? credentials = null; + var project = Projects.LoadProject(projectPath, false, credentials); + // Setting the project language here (always use English) - if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password)) - { - credentials = new Credentials() { Username = username, Password = new System.Security.SecureString() }; - foreach (var c in password) + if (project == null) { - credentials.Password.AppendChar(c); + throw new Exception("ERROR: Project file invalid or corrupted. No s7p file found in the project-archive."); } - } - var project = Projects.LoadProject(projectPath, false, credentials); - // Setting the project language here (always use English) - project.ProjectLanguage = MnemonicLanguage.English; + project.ProjectLanguage = MnemonicLanguage.English; + + var projectItem = new XElement("ProjectItem", + new XAttribute("Name", "Project"), + new XAttribute("Type", "Folder")); - var projectItem = new XElement("ProjectItem", - new XAttribute("Name", "Project"), - new XAttribute("Type", "Folder")); + // Parse the project and create the XML structure and write the XML files to the outputPath + AddXmlNodes(projectItem, project.ProjectStructure.SubItems); - // Parse the project and create the XML structure and write the XML files to the outputPath - AddXmlNodes(projectItem, project.ProjectStructure.SubItems); + // Add all interface XMLs + foreach (var interfaceXml in _interfacesXmls) + { + CreateFile(interfaceXml.Value, InterfacesDir, interfaceXml.Key, "S7Interface"); + } - var xmlDocument = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), projectItem); - xmlDocument.Save(Path.Combine(outputPath, "project_tree.xml")); + var xmlDocument = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), projectItem); + xmlDocument.Save(Path.Combine(outputPath, "project_tree.xml")); - var stationConfigurations = project.ProjectStructure.SubItems - .FindAll(sc => sc is StationConfigurationFolder) - .ConvertAll(sc => (StationConfigurationFolder)sc); - GenerateDeviceConnectionInfo(stationConfigurations); + var stationConfigurations = project.ProjectStructure.SubItems + .FindAll(sc => sc is StationConfigurationFolder) + .ConvertAll(sc => (StationConfigurationFolder)sc); + GenerateDeviceConnectionInfo(stationConfigurations); + FailIfAnyErrors(); + } + catch + { + throw; + } } private static string HashContent(string content) @@ -108,6 +166,71 @@ private static string HashContent(string content) return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); } + // New XML sanitization helpers + // Method to determine if a character is valid for XML + private static bool IsValidXmlChar(char c) + { + return (c == 0x9 || + c == 0xA || + c == 0xD || + (c >= 0x20 && c <= 0xD7FF) || + (c >= 0xE000 && c <= 0xFFFD) || + (c >= 0x10000 && c <= 0x10FFFF)); + } + + private static string SanitizeXmlString(string input) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + var chars = input.ToCharArray(); + for (int i = 0; i < chars.Length; i++) + { + if (!IsValidXmlChar(chars[i])) + { + // Replace invalid char; you can also choose to skip it instead + chars[i] = '?'; + } + } + + return new string(chars); + } + + private static void SanitizeElementInPlace(XElement element) + { + // Sanitize attributes + foreach (var attr in element.Attributes()) + { + if (!string.IsNullOrEmpty(attr.Value)) + { + attr.Value = SanitizeXmlString(attr.Value); + } + } + + // Sanitize text nodes and recurse into child elements + foreach (var node in element.Nodes().ToList()) + { + if (node is XText textNode) + { + textNode.Value = SanitizeXmlString(textNode.Value); + } + else if (node is XElement childElement) + { + SanitizeElementInPlace(childElement); + } + } + } + + private static XElement SanitizeXElement(XElement element) + { + // Work on a clone so we don't mutate the original + var clone = new XElement(element); + SanitizeElementInPlace(clone); + return clone; + } + /** * Create a new file with the given parameters and return the File XML item */ @@ -118,10 +241,16 @@ private static XElement CreateFile(XElement item, string subDirectory, string un var relativeFilePath = Path.Combine(subDirectory, hashedFileName); var absoluteFilePath = Path.Combine(_outputDir, relativeFilePath); var xmlRelativeFilePath = $"{subDirectory}/{hashedFileName}"; - var newXmlDoc = new XDocument(item); + + // *** IMPORTANT: sanitize the entire XElement tree before writing *** + var sanitizedItem = SanitizeXElement(item); + + var newXmlDoc = new XDocument(sanitizedItem); var newXmlContent = newXmlDoc.ToString(); + File.WriteAllText(absoluteFilePath, newXmlContent); var fileHash = HashContent(newXmlContent); + return new XElement("File", new XAttribute("DiffHash", fileHash), new XAttribute("ContentType", contentType), xmlRelativeFilePath); @@ -218,8 +347,8 @@ private static void AddStationConfigurationDetails(XElement rootXml, var interfaceColumns = new List { "Type", "Module" }; var title = masterSystem.Name + " Interfaces"; var interfaceAttributes = (from networkInterface in masterSystem.Children - let type = networkInterface.GetType().Name + " (" + networkInterface.NodeId + ")" - select new List { type, networkInterface.Name }).ToList(); + let type = networkInterface.GetType().Name + " (" + networkInterface.NodeId + ")" + select new List { type, networkInterface.Name }).ToList(); tableRoot.Add(CreateSdaAttributeListElement(interfaceAttributes, interfaceColumns, title)); } @@ -246,7 +375,7 @@ private static void AddCpDetails(XElement rootXml, CPFolder cpFolder) var tableRoot = new XElement(treeTableType); // Set the type of the parent item rootXml.SetAttributeValue("Type", treeTableType); - + tableRoot.Add(GetNetworkInterfaceList("Network Interfaces", cpFolder.NetworkInterfaces)); var uniqueCpName = cpFolder.StructuredFolderName; rootXml.Add(CreateFile(tableRoot, CpDir, uniqueCpName, treeTableType)); @@ -284,12 +413,18 @@ private static void AddInterfaceParameters(XElement rootXml, List para } } - private static void AddInterfaceParameters(XElement rootXml, IDataRow parameter) + private static void AddInterfaceParameters(XElement rootXml, IDataRow parameter, XElement interfaceXml = null, string blockName = null) { var interfaceItem = new XElement("interface"); AddInterfaceParameters(interfaceItem, parameter.Children); + if (interfaceXml != null) + { + interfaceItem.Add(new XAttribute("Name", blockName)); + interfaceXml.Add(interfaceItem); + } + rootXml.Add(interfaceItem); } @@ -326,16 +461,10 @@ private static void AddInterfaceParameters(XElement rootXml, List para } } - // Method to determine if a character is valid for XML - private static bool IsValidXmlChar(char c) - { - return (c == 0x9 || c == 0xA || c == 0xD || (c >= 0x20 && c <= 0xD7FF) || (c >= 0xE000 && c <= 0xFFFD) || (c >= 0x10000 && c <= 0x10FFFF)); - } - private static void AddBlocks(XElement rootXml, BlocksOfflineFolder blocksOfflineFolder) { var blocks = blocksOfflineFolder.BlockInfos.Select(blockIt => blockIt.GetBlock()); - + foreach (var block in blocks) { try @@ -345,13 +474,21 @@ private static void AddBlocks(XElement rootXml, BlocksOfflineFolder blocksOfflin var blockContent = new XElement(blockType, new XAttribute("Name", block.BlockName), new XAttribute("Type", blockType)); - switch (block) { case S7FunctionBlock s7FunctionBlock: - // TODO: Check the mnemonic language - s7FunctionBlock.MnemonicLanguage = MnemonicLanguage.English; + if (s7FunctionBlock.KnowHowProtection) + { + Console.Error.WriteLine($"Block '{block.BlockName}' is password protected, cannot extract details"); + var errorFileItem = new XElement("File", new XAttribute("Protected", "True")); + blockItem.Add(errorFileItem); + rootXml.Add(blockItem); + continue; + } + // Set the mnemonic language to English + s7FunctionBlock.MnemonicLanguage = MnemonicLanguage.English; + if (!string.IsNullOrEmpty(s7FunctionBlock.Title)) { blockContent.Add(new XAttribute("Title", s7FunctionBlock.Title)); @@ -362,10 +499,27 @@ private static void AddBlocks(XElement rootXml, BlocksOfflineFolder blocksOfflin blockContent.Add(new XAttribute("Description", s7FunctionBlock.Description)); } - AddInterfaceParameters(blockContent, s7FunctionBlock.Parameter); + var blockName = s7FunctionBlock.BlockName; + + var parentPath = block.ParentFolder.StructuredFolderName; + var existing = _interfacesXmls.TryGetValue(parentPath, out var interfaceXml); + if (!existing || interfaceXml == null) + { + interfaceXml = new XElement("S7Interface", + new XAttribute("Name", parentPath), + new XAttribute("Type", "S7Interface")); + _interfacesXmls[parentPath] = interfaceXml; + } + + var hashedInterfaceFileName = $"{InterfacesDir}/{HashContent(parentPath)}.xml"; + var attributeList = new XElement("AttributeList"); + attributeList.Add(new XElement("Attribute", new XAttribute("Name", "SdaS7DeviceBlockInterfaces"), new XAttribute("Value", hashedInterfaceFileName))); + blockItem.Add(attributeList); + + AddInterfaceParameters(blockContent, s7FunctionBlock.Parameter, interfaceXml, blockName); var networksItem = new XElement("Networks"); - + foreach (var fbNetwork in s7FunctionBlock.Networks) { var networkItem = new XElement("Network", new XAttribute("Title", fbNetwork.Name)); @@ -400,19 +554,20 @@ private static void AddBlocks(XElement rootXml, BlocksOfflineFolder blocksOfflin } catch (Exception ex) { - Console.WriteLine( - $"Error when extracting block {block.BlockName} of type {block.BlockType.ToString()} {ex.Message}"); - Console.Error.WriteLine(ex.StackTrace); + var errorMessage = $"Error when extracting block {block.BlockName}, {block.ParentFolder.Project.ProjectStructure} of type {block.BlockType.ToString()}. (Block is password protected)"; + + AddFailure("blocks", block.BlockName, block.BlockType.ToString(), errorMessage); } } } + private static void AddSymbols(XElement rootXml, SymbolTable symbolTable) { var symbolTableType = $"S7{symbolTable.GetType().Name}"; var symbolTableContent = new XElement(symbolTableType, new XAttribute("Name", symbolTable.Name), new XAttribute("Type", symbolTableType)); - + // Set the type of the parent item rootXml.SetAttributeValue("Type", symbolTableType); @@ -532,9 +687,9 @@ private static void GenerateDeviceConnectionInfo(List + From aaac3267d682bc2e199ce3af7d2b78eea13fddcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BChlegger?= Date: Fri, 6 Mar 2026 14:46:46 +0100 Subject: [PATCH 4/4] remove test project --- DotNetSiemensPLCToolBoxLibrary.sln | 23 - SdaTest/Program.cs | 15 - SdaTest/ProjectConversionCommandHandler.cs | 702 --------------------- SdaTest/SdaTest.csproj | 18 - 4 files changed, 758 deletions(-) delete mode 100644 SdaTest/Program.cs delete mode 100644 SdaTest/ProjectConversionCommandHandler.cs delete mode 100644 SdaTest/SdaTest.csproj diff --git a/DotNetSiemensPLCToolBoxLibrary.sln b/DotNetSiemensPLCToolBoxLibrary.sln index ab38edb2..d15b0163 100644 --- a/DotNetSiemensPLCToolBoxLibrary.sln +++ b/DotNetSiemensPLCToolBoxLibrary.sln @@ -107,8 +107,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetSiemensPLCToolBoxLibr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetSiemensPLCToolBoxLibrary.TIAV21", "DotNetSiemensPLCToolBoxLibrary.TIAV21\DotNetSiemensPLCToolBoxLibrary.TIAV21.csproj", "{42C91E85-7052-F066-3257-5F05F7258BAC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SdaTest", "SdaTest\SdaTest.csproj", "{A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -829,26 +827,6 @@ Global {42C91E85-7052-F066-3257-5F05F7258BAC}.Release|Mixed Platforms.Build.0 = Release|Any CPU {42C91E85-7052-F066-3257-5F05F7258BAC}.Release|x86.ActiveCfg = Release|Any CPU {42C91E85-7052-F066-3257-5F05F7258BAC}.Release|x86.Build.0 = Release|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|iPhone.Build.0 = Debug|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|x86.ActiveCfg = Debug|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Debug|x86.Build.0 = Debug|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|Any CPU.Build.0 = Release|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|iPhone.ActiveCfg = Release|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|iPhone.Build.0 = Release|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|x86.ActiveCfg = Release|Any CPU - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -882,7 +860,6 @@ Global {B2D9C814-26B4-46DF-AEAF-E82606E4E8B2} = {A4F11331-531B-4A7C-84B2-E319658AE2B8} {5623A0DA-4CC8-4954-AD76-90C25E10B2D2} = {A4F11331-531B-4A7C-84B2-E319658AE2B8} {5FC399A5-1C21-480A-987F-16388079F245} = {66A4ADD6-8EE2-49BD-B939-C5E69CAA3A9E} - {A51D3F3B-D4DD-40F9-ADC4-7CDF78957A01} = {14BB6088-3FC3-4643-B170-0BF3F6F91028} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {629DCEA8-C0FA-4650-97C6-ADD1E09A377D} diff --git a/SdaTest/Program.cs b/SdaTest/Program.cs deleted file mode 100644 index 40c71a3a..00000000 --- a/SdaTest/Program.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace SdaTest -{ - internal class Program - { - public static void Main(string[] args) - { - Console.WriteLine("Testing..."); - var projectPath = "/Users/tom/Downloads/S7/test.zip"; - var exportPath = "/Users/tom/Downloads/S7/export"; - ProjectConversionCommandHandler.Export(projectPath, exportPath); - } - } -} \ No newline at end of file diff --git a/SdaTest/ProjectConversionCommandHandler.cs b/SdaTest/ProjectConversionCommandHandler.cs deleted file mode 100644 index c97b5503..00000000 --- a/SdaTest/ProjectConversionCommandHandler.cs +++ /dev/null @@ -1,702 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Xml.Linq; -using DotNetSiemensPLCToolBoxLibrary.DataTypes; -using DotNetSiemensPLCToolBoxLibrary.DataTypes.Blocks; -using DotNetSiemensPLCToolBoxLibrary.DataTypes.Blocks.Step7V5; -using DotNetSiemensPLCToolBoxLibrary.DataTypes.Hardware.Step7V5; -using DotNetSiemensPLCToolBoxLibrary.DataTypes.Network; -using DotNetSiemensPLCToolBoxLibrary.DataTypes.Projectfolders; -using DotNetSiemensPLCToolBoxLibrary.DataTypes.Projectfolders.Step7V5; -using DotNetSiemensPLCToolBoxLibrary.Projectfiles; - -namespace SdaTest -{ - public class DeviceInfoEthernetCommunication - { - [JsonPropertyName("ip_address")] public string IpAddress { get; set; } - [JsonPropertyName("subnet_mask")] public string SubnetMask { get; set; } - } - - public class DeviceInfo - { - [JsonPropertyName("name")] public string Name { get; set; } - [JsonPropertyName("description")] public string Description { get; set; } - [JsonPropertyName("order_number")] public string OrderNumber { get; set; } - [JsonPropertyName("device_type")] public string DeviceType { get; set; } - - [JsonPropertyName("origin_device_type")] - public string OriginDeviceType { get; set; } - - [JsonPropertyName("ethernet_communication")] - public List EthernetCommunication { get; set; } - - [JsonPropertyName("password_protected")] - public bool PasswordProtected { get; set; } - } - - public class ProjectConversionCommandHandler - { - private static string _outputDir = ""; - private const string CpuDir = "cpu"; - private const string CpDir = "cp"; - private const string SymbolsDir = "symbols"; - private const string InterfacesDir = "interfaces"; - private const string SourcesDir = "sources"; - private const string BlocksDir = "blocks"; - - // These dictionaries are storing the file paths for the symbols and interfaces for given CPUs - private static Dictionary _symbolsFilePaths = new Dictionary(); - private static Dictionary _interfacesXmls = new Dictionary(); - - private static void CreateWorkingDirectory(string directory, bool cleanup = false) - { - if (cleanup && Directory.Exists(directory)) - { - Directory.Delete(directory, true); - } - - Directory.CreateDirectory(directory); - } - - private static void FailIfAnyErrors() - { - if (_failures.Count != 0) throw new Exception("Conversion Failed"); - - Console.WriteLine("Export finished successfully."); - } - - private class ExtractionFailure - { - public string Scope { get; set; } = ""; - public string Path { get; set; } = ""; - public string Name { get; set; } = ""; - public string Details { get; set; } = ""; - - } - - private static readonly List _failures = new(); - - private static void AddFailure(string scope, string path, string name, string errorMessage) - { - - Console.WriteLine($"[FAIL] [{scope}] {path}/{name}: {errorMessage}"); - _failures.Add(new ExtractionFailure - { - Scope = scope ?? "", - Path = path ?? "", - Name = name ?? "", - Details = errorMessage - }); - } - - public static void Export(string projectPath, string outputPath, string? username = null, - string? password = null) - { - try - { - _outputDir = outputPath; - CreateWorkingDirectory(_outputDir); - CreateWorkingDirectory(Path.Combine(_outputDir, CpuDir), true); - CreateWorkingDirectory(Path.Combine(_outputDir, CpDir), true); - CreateWorkingDirectory(Path.Combine(_outputDir, SymbolsDir), true); - CreateWorkingDirectory(Path.Combine(_outputDir, InterfacesDir), true); - CreateWorkingDirectory(Path.Combine(_outputDir, SourcesDir), true); - CreateWorkingDirectory(Path.Combine(_outputDir, BlocksDir), true); - - Credentials? credentials = null; - - if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password)) - { - credentials = new Credentials() { Username = username, Password = new System.Security.SecureString() }; - foreach (var c in password) - { - credentials.Password.AppendChar(c); - } - } - - var project = Projects.LoadProject(projectPath, false, credentials); - // Setting the project language here (always use English) - - if (project == null) - { - throw new Exception("ERROR: Project file invalid or corrupted. No s7p file found in the project-archive."); - } - - project.ProjectLanguage = MnemonicLanguage.English; - - var projectItem = new XElement("ProjectItem", - new XAttribute("Name", "Project"), - new XAttribute("Type", "Folder")); - - // Parse the project and create the XML structure and write the XML files to the outputPath - AddXmlNodes(projectItem, project.ProjectStructure.SubItems); - - // Add all interface XMLs - foreach (var interfaceXml in _interfacesXmls) - { - CreateFile(interfaceXml.Value, InterfacesDir, interfaceXml.Key, "S7Interface"); - } - - var xmlDocument = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), projectItem); - xmlDocument.Save(Path.Combine(outputPath, "project_tree.xml")); - - var stationConfigurations = project.ProjectStructure.SubItems - .FindAll(sc => sc is StationConfigurationFolder) - .ConvertAll(sc => (StationConfigurationFolder)sc); - GenerateDeviceConnectionInfo(stationConfigurations); - FailIfAnyErrors(); - } - catch - { - throw; - } - } - - private static string HashContent(string content) - { - var sha256 = SHA256.Create(); - var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(content)); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - - // New XML sanitization helpers - // Method to determine if a character is valid for XML - private static bool IsValidXmlChar(char c) - { - return (c == 0x9 || - c == 0xA || - c == 0xD || - (c >= 0x20 && c <= 0xD7FF) || - (c >= 0xE000 && c <= 0xFFFD) || - (c >= 0x10000 && c <= 0x10FFFF)); - } - - private static string SanitizeXmlString(string input) - { - if (string.IsNullOrEmpty(input)) - { - return input; - } - - var chars = input.ToCharArray(); - for (int i = 0; i < chars.Length; i++) - { - if (!IsValidXmlChar(chars[i])) - { - // Replace invalid char; you can also choose to skip it instead - chars[i] = '?'; - } - } - - return new string(chars); - } - - private static void SanitizeElementInPlace(XElement element) - { - // Sanitize attributes - foreach (var attr in element.Attributes()) - { - if (!string.IsNullOrEmpty(attr.Value)) - { - attr.Value = SanitizeXmlString(attr.Value); - } - } - - // Sanitize text nodes and recurse into child elements - foreach (var node in element.Nodes().ToList()) - { - if (node is XText textNode) - { - textNode.Value = SanitizeXmlString(textNode.Value); - } - else if (node is XElement childElement) - { - SanitizeElementInPlace(childElement); - } - } - } - - private static XElement SanitizeXElement(XElement element) - { - // Work on a clone so we don't mutate the original - var clone = new XElement(element); - SanitizeElementInPlace(clone); - return clone; - } - - /** - * Create a new file with the given parameters and return the File XML item - */ - private static XElement CreateFile(XElement item, string subDirectory, string uniqueFileName, - string contentType) - { - var hashedFileName = $"{HashContent(uniqueFileName)}.xml"; - var relativeFilePath = Path.Combine(subDirectory, hashedFileName); - var absoluteFilePath = Path.Combine(_outputDir, relativeFilePath); - var xmlRelativeFilePath = $"{subDirectory}/{hashedFileName}"; - - // *** IMPORTANT: sanitize the entire XElement tree before writing *** - var sanitizedItem = SanitizeXElement(item); - - var newXmlDoc = new XDocument(sanitizedItem); - var newXmlContent = newXmlDoc.ToString(); - - File.WriteAllText(absoluteFilePath, newXmlContent); - var fileHash = HashContent(newXmlContent); - - return new XElement("File", new XAttribute("DiffHash", fileHash), - new XAttribute("ContentType", contentType), - xmlRelativeFilePath); - } - - private static XElement CreateItemElement(string name, string type) - { - return new XElement("Item", new XAttribute("Name", name), new XAttribute("Type", type)); - } - - private static XElement CreateSdaAttributeListElement(List> sdaAttributes, List columns, - string? title = null, string? comment = null) - { - var listRoot = new XElement("SdaTreeTable"); - - if (title is not null) - { - listRoot.Add(new XElement("Title", title)); - } - - if (comment is not null) - { - listRoot.Add(new XElement("Comment", comment)); - } - - var columnConfig = new XElement("ColumnsConfig", new XAttribute("KeyColumn", columns.First())); - foreach (var column in columns) - { - columnConfig.Add(new XElement("Column", new XAttribute("Col", column), - new XAttribute("Label", column))); - } - - listRoot.Add(columnConfig); - - var rows = new XElement("Rows"); - foreach (var sdaAttribute in sdaAttributes) - { - var row = new XElement("Row"); - - for (var i = 0; i < sdaAttribute.Count; i++) - { - row.Add(new XElement("Cell", new XAttribute("Col", columns[i]), sdaAttribute[i])); - } - - rows.Add(row); - } - - listRoot.Add(rows); - return listRoot; - } - - private static XElement GetNetworkInterfaceList(string title, List networkInterfaces) - { - var interfaceColumns = new List { "Name", "Type", "Address" }; - var interfaceAttributes = new List>(); - - foreach (var networkInterface in networkInterfaces) - { - var address = networkInterface.ToString(); - - switch (networkInterface) - { - case EthernetNetworkInterface ethernetNetworkInterface: - var mac = string.Join(":", - Array.ConvertAll(ethernetNetworkInterface.Mac.GetAddressBytes(), b => b.ToString("X2"))); - address = ethernetNetworkInterface.IpAddress + " (" + mac + ")"; - break; - case MpiProfiBusNetworkInterface mpiProfiBusNetworkInterface: - address = "Address: " + mpiProfiBusNetworkInterface.Address; - break; - } - - interfaceAttributes.Add(new List - { - networkInterface.Name, - networkInterface.NetworkInterfaceType.ToString(), - address - }); - } - - return CreateSdaAttributeListElement(interfaceAttributes, interfaceColumns, title); - } - - private static void AddStationConfigurationDetails(XElement rootXml, - StationConfigurationFolder stationConfiguration) - { - const string treeTableType = "SdaTreeTables"; - var tableRoot = new XElement(treeTableType); - // Set the type of the parent item - rootXml.SetAttributeValue("Type", treeTableType); - - foreach (var masterSystem in stationConfiguration.MasterSystems) - { - var interfaceColumns = new List { "Type", "Module" }; - var title = masterSystem.Name + " Interfaces"; - var interfaceAttributes = (from networkInterface in masterSystem.Children - let type = networkInterface.GetType().Name + " (" + networkInterface.NodeId + ")" - select new List { type, networkInterface.Name }).ToList(); - tableRoot.Add(CreateSdaAttributeListElement(interfaceAttributes, interfaceColumns, title)); - } - - var uniqueStationName = stationConfiguration.StructuredFolderName; - rootXml.Add(CreateFile(tableRoot, CpuDir, uniqueStationName, treeTableType)); - } - - private static void AddCpuDetails(XElement rootXml, CPUFolder cpuFolder) - { - if (cpuFolder.NetworkInterfaces == null) return; - const string treeTableType = "SdaTreeTables"; - var tableRoot = new XElement(treeTableType); - // Set the type of the parent item - rootXml.SetAttributeValue("Type", treeTableType); - tableRoot.Add(GetNetworkInterfaceList("Network Interfaces", cpuFolder.NetworkInterfaces)); - var uniqueCpuName = cpuFolder.StructuredFolderName; - rootXml.Add(CreateFile(tableRoot, CpuDir, uniqueCpuName, treeTableType)); - } - - private static void AddCpDetails(XElement rootXml, CPFolder cpFolder) - { - if (cpFolder.NetworkInterfaces == null) return; - const string treeTableType = "SdaTreeTables"; - var tableRoot = new XElement(treeTableType); - // Set the type of the parent item - rootXml.SetAttributeValue("Type", treeTableType); - - tableRoot.Add(GetNetworkInterfaceList("Network Interfaces", cpFolder.NetworkInterfaces)); - var uniqueCpName = cpFolder.StructuredFolderName; - rootXml.Add(CreateFile(tableRoot, CpDir, uniqueCpName, treeTableType)); - } - - private static void AddInterfaceParameters(XElement rootXml, List parameters) - { - foreach (var parameter in parameters) - { - try - { - if (parameter.LibNoDaveValue == null) - { - // This can be the case if empty (placeholders) are used - rootXml.Add(new XElement("variable", new XAttribute("Name", ""))); - continue; - } - - var varItem = new XElement("variable", new XAttribute("Name", parameter.LibNoDaveValue.ToString())); - - if (!string.IsNullOrEmpty(parameter.Comment)) - { - varItem.Add(new XElement("comment", parameter.Comment)); - } - - varItem.Add(new XElement("dataType", parameter.LibNoDaveValue.DataTypeStringFormat.ToString())); - varItem.Add(new XElement("address", parameter.S7FormatAddress)); - - rootXml.Add(varItem); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - } - - private static void AddInterfaceParameters(XElement rootXml, IDataRow parameter, XElement interfaceXml = null, string blockName = null) - { - var interfaceItem = new XElement("interface"); - - AddInterfaceParameters(interfaceItem, parameter.Children); - - if (interfaceXml != null) - { - interfaceItem.Add(new XAttribute("Name", blockName)); - interfaceXml.Add(interfaceItem); - } - - rootXml.Add(interfaceItem); - } - - private static void AddInterfaceParameters(XElement rootXml, List parameters) - { - foreach (var parameter in parameters) - { - var varItem = new XElement("variable", new XAttribute("Name", parameter.Name)); - - if (parameter is DataBlockRow dataBlockRow) - { - varItem.Add(new XElement("dataType", dataBlockRow.DataTypeAsString)); - } - else - { - varItem.Add(new XElement("dataType", parameter.DataType.ToString())); - } - - if (!string.IsNullOrEmpty(parameter.Comment)) - { - varItem.Add(new XElement("comment", parameter.Comment)); - } - - varItem.Add(new XElement("address", parameter.BlockAddress.ToString())); - - if (parameter is S7DataRow { StartValueAsString: not null } s7DataRowParameter) - { - varItem.Add(new XElement("initialValue", s7DataRowParameter.StartValueAsString)); - } - - AddInterfaceParameters(varItem, parameter.Children); - - rootXml.Add(varItem); - } - } - - private static void AddBlocks(XElement rootXml, BlocksOfflineFolder blocksOfflineFolder) - { - var blocks = blocksOfflineFolder.BlockInfos.Select(blockIt => blockIt.GetBlock()); - - foreach (var block in blocks) - { - try - { - var blockType = block.GetType().Name; - var blockItem = CreateItemElement(block.BlockName, blockType); - var blockContent = new XElement(blockType, new XAttribute("Name", block.BlockName), - new XAttribute("Type", blockType)); - - switch (block) - { - case S7FunctionBlock s7FunctionBlock: - if (s7FunctionBlock.KnowHowProtection) - { - Console.Error.WriteLine($"Block '{block.BlockName}' is password protected, cannot extract details"); - var errorFileItem = new XElement("File", new XAttribute("Protected", "True")); - blockItem.Add(errorFileItem); - rootXml.Add(blockItem); - continue; - } - - // Set the mnemonic language to English - s7FunctionBlock.MnemonicLanguage = MnemonicLanguage.English; - - if (!string.IsNullOrEmpty(s7FunctionBlock.Title)) - { - blockContent.Add(new XAttribute("Title", s7FunctionBlock.Title)); - } - - if (!string.IsNullOrEmpty(s7FunctionBlock.Description)) - { - blockContent.Add(new XAttribute("Description", s7FunctionBlock.Description)); - } - - var blockName = s7FunctionBlock.BlockName; - - var parentPath = block.ParentFolder.StructuredFolderName; - var existing = _interfacesXmls.TryGetValue(parentPath, out var interfaceXml); - if (!existing || interfaceXml == null) - { - interfaceXml = new XElement("S7Interface", - new XAttribute("Name", parentPath), - new XAttribute("Type", "S7Interface")); - _interfacesXmls[parentPath] = interfaceXml; - } - - var hashedInterfaceFileName = $"{InterfacesDir}/{HashContent(parentPath)}.xml"; - var attributeList = new XElement("AttributeList"); - attributeList.Add(new XElement("Attribute", new XAttribute("Name", "SdaS7DeviceBlockInterfaces"), new XAttribute("Value", hashedInterfaceFileName))); - blockItem.Add(attributeList); - - AddInterfaceParameters(blockContent, s7FunctionBlock.Parameter, interfaceXml, blockName); - - var networksItem = new XElement("Networks"); - - foreach (var fbNetwork in s7FunctionBlock.Networks) - { - var networkItem = new XElement("Network", new XAttribute("Title", fbNetwork.Name)); - networkItem.Add(new XElement("comment", fbNetwork.Comment)); - var rawAwl = fbNetwork.AWLCodeToString(); - var sanitizedAwl = new string(rawAwl.Select(c => IsValidXmlChar(c) ? c : '?').ToArray()); - networkItem.Add(new XElement("AWL", sanitizedAwl)); - // Add the network item to the networks - networksItem.Add(networkItem); - } - - blockContent.Add(networksItem); - break; - case S7DataBlock s7DataBlock: - if (!string.IsNullOrEmpty(s7DataBlock.Title)) - { - blockContent.Add(new XAttribute("Title", s7DataBlock.Title)); - } - - AddInterfaceParameters(blockContent, s7DataBlock.Structure.Children); - break; - case S7VATBlock s7VatBlock: - AddInterfaceParameters(blockContent, s7VatBlock.VATRows); - break; - } - - var uniqueBlockName = blocksOfflineFolder.StructuredFolderName + "/" + block.BlockName; - blockItem.Add( - CreateFile(blockContent, BlocksDir, uniqueBlockName, blockType)); - - rootXml.Add(blockItem); - } - catch (Exception ex) - { - var errorMessage = $"Error when extracting block {block.BlockName}, {block.ParentFolder.Project.ProjectStructure} of type {block.BlockType.ToString()}. (Block is password protected)"; - - AddFailure("blocks", block.BlockName, block.BlockType.ToString(), errorMessage); - } - } - } - - - private static void AddSymbols(XElement rootXml, SymbolTable symbolTable) - { - var symbolTableType = $"S7{symbolTable.GetType().Name}"; - var symbolTableContent = new XElement(symbolTableType, new XAttribute("Name", symbolTable.Name), - new XAttribute("Type", symbolTableType)); - - // Set the type of the parent item - rootXml.SetAttributeValue("Type", symbolTableType); - - var symbolEntries = symbolTable.SymbolTableEntrys; - symbolEntries.Sort((x, y) => string.Compare(x.Symbol, y.Symbol, StringComparison.Ordinal)); - - foreach (var symbolTableEntry in symbolEntries) - { - var symbolItem = new XElement("symbol"); - symbolItem.Add(new XAttribute("Symbol", symbolTableEntry.Symbol)); - symbolItem.Add(new XAttribute("Comment", symbolTableEntry.Comment)); - symbolItem.Add(new XAttribute("Type", symbolTableEntry.DataType)); - symbolItem.Add(new XAttribute("Address", symbolTableEntry.OperandIEC)); - - symbolTableContent.Add(symbolItem); - } - - var uniqueSymbolName = symbolTable.StructuredFolderName; - rootXml.Add( - CreateFile(symbolTableContent, SymbolsDir, uniqueSymbolName, symbolTableType)); - } - - private static void AddSources(XElement rootXml, SourceFolder sources) - { - foreach (var blockInfo in sources.BlockInfos) - { - var sourceBlockType = blockInfo.GetType().Name; - var sourceBlockItem = CreateItemElement(blockInfo.Name, sourceBlockType); - var sourceBlockContent = new XElement(sourceBlockType, new XAttribute("Name", blockInfo.Name), - new XAttribute("Type", sourceBlockType)); - - if (blockInfo is S7ProjectSourceInfo s7ProjectSourceInfo) - { - var rawSource = sources.GetSource(s7ProjectSourceInfo); - var source = new string(rawSource.Select(c => IsValidXmlChar(c) ? c : '?').ToArray()); - sourceBlockContent.Add(source); - sourceBlockContent.Add(source); - } - - var uniqueSourceName = sources.StructuredFolderName + "/" + blockInfo.Name; - sourceBlockItem.Add( - CreateFile(sourceBlockContent, SourcesDir, uniqueSourceName, sourceBlockType)); - rootXml.Add(sourceBlockItem); - } - } - - private static void AddXmlNodes(XElement rootXml, List items) - { - foreach (var subitem in items) - { - if (subitem is MasterSystem) - { - continue; - } - - var subItemElement = CreateItemElement(subitem.Name, "item"); - - switch (subitem) - { - case StationConfigurationFolder stationConfiguration: - AddStationConfigurationDetails(subItemElement, stationConfiguration); - break; - case CPUFolder cpuFolder: - AddCpuDetails(subItemElement, cpuFolder); - break; - case CPFolder cpFolder: - AddCpDetails(subItemElement, cpFolder); - break; - case SymbolTable symbolTable: - AddSymbols(subItemElement, symbolTable); - break; - case BlocksOfflineFolder blocksOfflineFolder: - AddBlocks(subItemElement, blocksOfflineFolder); - break; - case SourceFolder sourceFolder: - AddSources(subItemElement, sourceFolder); - break; - } - - if (subitem.SubItems != null) AddXmlNodes(subItemElement, subitem.SubItems); - rootXml.Add(subItemElement); - } - } - - private static void GenerateDeviceConnectionInfo(List stationConfigurations) - { - try - { - // Create device information list and append a new device - var deviceInformation = new List(); - - foreach (var stationConfiguration in stationConfigurations) - { - var cpu = stationConfiguration.SubItems.FirstOrDefault(x => x is CPUFolder) as CPUFolder; - var cp = stationConfiguration.SubItems.FirstOrDefault(x => x is CPFolder) as CPFolder; - var ethernetInterfaces = cp?.NetworkInterfaces - .FindAll(ni => ni is EthernetNetworkInterface) - .ConvertAll(ni => (EthernetNetworkInterface)ni); - - var ethernetCommunication = ethernetInterfaces? - .Select(ni => new DeviceInfoEthernetCommunication - { - IpAddress = ni.IpAddress.ToString(), - SubnetMask = ni.SubnetMask.ToString() - }) - .ToList() ?? new List(); - - deviceInformation.Add(new DeviceInfo - { - Name = stationConfiguration.Name, - Description = cpu?.Name ?? "Unknown", - OrderNumber = cpu?.MLFB_OrderNumber, - DeviceType = "plc", - OriginDeviceType = stationConfiguration.StationType.ToString(), - EthernetCommunication = ethernetCommunication, - PasswordProtected = cpu?.PasswdHard != null, - }); - } - - var deviceJsonFilePath = Path.Combine(_outputDir, "devices_connection_information.json"); - File.WriteAllText(deviceJsonFilePath, - JsonSerializer.Serialize(deviceInformation, new JsonSerializerOptions { WriteIndented = true })); - - Console.WriteLine("Device information has been written to JSON file."); - } - catch (Exception e) - { - Console.Error.WriteLine($"Error when generating the devices connection info {e.Message}"); - } - } - } -} \ No newline at end of file diff --git a/SdaTest/SdaTest.csproj b/SdaTest/SdaTest.csproj deleted file mode 100644 index c8e04b9f..00000000 --- a/SdaTest/SdaTest.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - Exe - 9 - net48 - - - - - - - - - - - -