diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b3a4f5c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/Examples/bin/Debug/net5.0/Examples.dll", + "args": [], + "cwd": "${workspaceFolder}/Examples", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..2e99677 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Examples/Examples.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/Examples/Examples.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/Examples/Examples.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Builds/MewtocolNet.xml b/Builds/MewtocolNet.xml new file mode 100644 index 0000000..a0dad5e --- /dev/null +++ b/Builds/MewtocolNet.xml @@ -0,0 +1,193 @@ + + + + MewtocolNet + + + + + Trys to connect to the PLC by the IP given in the constructor + + Gets called when a connection with a PLC was established + Gets called when an error or timeout during connection occurs + + + + + Attaches a continous reader that reads back the Registers and Contacts + + + + + Generic information about the connected PLC + + + + + Builds a new Interfacer for a PLC + + + + + + + Sends a command to the PLC and awaits results + + MEWTOCOL Formatted request string ex: %01#RT + Auto close of frame [true]%01#RT01\r [false]%01#RT + Returns the result + + + + Gets generic information about the PLC + + A PLCInfo class + + + + Reads bool values from the plc by the given Contact List + + A list of contacts + The PLCs station number + List of IBoolContact with unique copys of the given contacts + + + + Writes a boolen value to the given contact + + The contact to write + The boolean state to write + Station Number (optional) + A result struct + + + + Reads the given numeric register from PLC + + Type of number (short, ushort, int, uint, float) + The register to read + Station number to access + A result with the given NumberRegister containing the readback value and a result struct + + + + Reads the given numeric register from PLC + + Type of number (short, ushort, int, uint, float) + The register to write + Station number to access + A result with the given NumberRegister and a result struct + + + + Gets fired whenever a contact of the observed list changes its value + + + + + A class describing a register + + + + + Gets called whenever the value was changed + + + + + Defines a register containing a number + + The type of the numeric value + + + + The value of the register + + + + + Defines a register containing a number + + Memory start adress max 99999 + The format in which the variable is stored + + + + Result for a read/write operation + + The type of the numeric value + + + + Defines a register containing a string + + + + + Defines a register containing a string + + + + + The formatted result of a ascii command + + + + + Contains generic information about the plc + + + + + Gets operation mode from 2 digit hex number + + + + + Contact as bool contact + + + + + A class describing a PLC contact + + + + + Creates a new base Contact + + A prefix identifier eg. X,Y,R,L + The number of the PLC contact + + + + Creates a new base Contact + + A prefix identifier eg. X,Y,R,L + The number of the PLC contact + + + + Build contact from complete contact name + + Complete contact name e.g. Y1C, Y3D or X1 + + + + Builds the mewtocol ascii contact identifier + + The identifier e.g. Y0001 or Y000A or X001C + + + + Converts the class to a generic json compatible object + + + + + + Creates a copy of the contact + + + + diff --git a/Builds/net5.0/MewtocolNet.deps.json b/Builds/net5.0/MewtocolNet.deps.json new file mode 100644 index 0000000..e225eb1 --- /dev/null +++ b/Builds/net5.0/MewtocolNet.deps.json @@ -0,0 +1,23 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v5.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v5.0": { + "MewtocolNet/0.1.5": { + "runtime": { + "MewtocolNet.dll": {} + } + } + } + }, + "libraries": { + "MewtocolNet/0.1.5": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/Builds/net5.0/MewtocolNet.dll b/Builds/net5.0/MewtocolNet.dll new file mode 100644 index 0000000..455e3b7 Binary files /dev/null and b/Builds/net5.0/MewtocolNet.dll differ diff --git a/Builds/net5.0/MewtocolNet.xml b/Builds/net5.0/MewtocolNet.xml new file mode 100644 index 0000000..a0dad5e --- /dev/null +++ b/Builds/net5.0/MewtocolNet.xml @@ -0,0 +1,193 @@ + + + + MewtocolNet + + + + + Trys to connect to the PLC by the IP given in the constructor + + Gets called when a connection with a PLC was established + Gets called when an error or timeout during connection occurs + + + + + Attaches a continous reader that reads back the Registers and Contacts + + + + + Generic information about the connected PLC + + + + + Builds a new Interfacer for a PLC + + + + + + + Sends a command to the PLC and awaits results + + MEWTOCOL Formatted request string ex: %01#RT + Auto close of frame [true]%01#RT01\r [false]%01#RT + Returns the result + + + + Gets generic information about the PLC + + A PLCInfo class + + + + Reads bool values from the plc by the given Contact List + + A list of contacts + The PLCs station number + List of IBoolContact with unique copys of the given contacts + + + + Writes a boolen value to the given contact + + The contact to write + The boolean state to write + Station Number (optional) + A result struct + + + + Reads the given numeric register from PLC + + Type of number (short, ushort, int, uint, float) + The register to read + Station number to access + A result with the given NumberRegister containing the readback value and a result struct + + + + Reads the given numeric register from PLC + + Type of number (short, ushort, int, uint, float) + The register to write + Station number to access + A result with the given NumberRegister and a result struct + + + + Gets fired whenever a contact of the observed list changes its value + + + + + A class describing a register + + + + + Gets called whenever the value was changed + + + + + Defines a register containing a number + + The type of the numeric value + + + + The value of the register + + + + + Defines a register containing a number + + Memory start adress max 99999 + The format in which the variable is stored + + + + Result for a read/write operation + + The type of the numeric value + + + + Defines a register containing a string + + + + + Defines a register containing a string + + + + + The formatted result of a ascii command + + + + + Contains generic information about the plc + + + + + Gets operation mode from 2 digit hex number + + + + + Contact as bool contact + + + + + A class describing a PLC contact + + + + + Creates a new base Contact + + A prefix identifier eg. X,Y,R,L + The number of the PLC contact + + + + Creates a new base Contact + + A prefix identifier eg. X,Y,R,L + The number of the PLC contact + + + + Build contact from complete contact name + + Complete contact name e.g. Y1C, Y3D or X1 + + + + Builds the mewtocol ascii contact identifier + + The identifier e.g. Y0001 or Y000A or X001C + + + + Converts the class to a generic json compatible object + + + + + + Creates a copy of the contact + + + + diff --git a/Examples/Examples.csproj b/Examples/Examples.csproj new file mode 100644 index 0000000..8b8151e --- /dev/null +++ b/Examples/Examples.csproj @@ -0,0 +1,12 @@ + + + + + + + + Exe + net5.0 + + + diff --git a/Examples/Program.cs b/Examples/Program.cs new file mode 100644 index 0000000..ba6ddef --- /dev/null +++ b/Examples/Program.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using System.Text.Json; +using MewtocolNet; + +namespace Examples { + class Program { + static void Main(string[] args) { + + Console.WriteLine("Starting test"); + + Task.Factory.StartNew(async () => { + + MewtocolInterface interf = new MewtocolInterface("10.237.191.3"); + + interf.AddRegister("Cooler Status",1204); + interf.AddRegister(1101, 4); + + interf.RegisterChanged += (o) => { + Console.WriteLine($"DT{o.MemoryAdress} {(o.Name != null ? $"({o.Name}) " : "")}changed to {o.GetValueString()}"); + }; + + await interf.ConnectAsync( + (plcinf) => { + + Console.WriteLine("Connected to PLC:\n" + plcinf.ToString()); + }, + () => { + Console.WriteLine("Failed connection"); + } + ).AttachContinousReader(50); + + + }); + + Console.ReadLine(); + } + } +} diff --git a/MewtocolNet.sln b/MewtocolNet.sln new file mode 100644 index 0000000..11f2096 --- /dev/null +++ b/MewtocolNet.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MewtocolNet", "MewtocolNet\MewtocolNet.csproj", "{8B7863E7-5E82-4990-9138-2C0C24629982}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples", "Examples\Examples.csproj", "{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8B7863E7-5E82-4990-9138-2C0C24629982}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B7863E7-5E82-4990-9138-2C0C24629982}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B7863E7-5E82-4990-9138-2C0C24629982}.Debug|x64.ActiveCfg = Debug|Any CPU + {8B7863E7-5E82-4990-9138-2C0C24629982}.Debug|x64.Build.0 = Debug|Any CPU + {8B7863E7-5E82-4990-9138-2C0C24629982}.Debug|x86.ActiveCfg = Debug|Any CPU + {8B7863E7-5E82-4990-9138-2C0C24629982}.Debug|x86.Build.0 = Debug|Any CPU + {8B7863E7-5E82-4990-9138-2C0C24629982}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B7863E7-5E82-4990-9138-2C0C24629982}.Release|Any CPU.Build.0 = Release|Any CPU + {8B7863E7-5E82-4990-9138-2C0C24629982}.Release|x64.ActiveCfg = Release|Any CPU + {8B7863E7-5E82-4990-9138-2C0C24629982}.Release|x64.Build.0 = Release|Any CPU + {8B7863E7-5E82-4990-9138-2C0C24629982}.Release|x86.ActiveCfg = Release|Any CPU + {8B7863E7-5E82-4990-9138-2C0C24629982}.Release|x86.Build.0 = Release|Any CPU + {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|x64.ActiveCfg = Debug|Any CPU + {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|x64.Build.0 = Debug|Any CPU + {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|x86.ActiveCfg = Debug|Any CPU + {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|x86.Build.0 = Debug|Any CPU + {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|Any CPU.Build.0 = Release|Any CPU + {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|x64.ActiveCfg = Release|Any CPU + {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|x64.Build.0 = Release|Any CPU + {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|x86.ActiveCfg = Release|Any CPU + {D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/MewtocolNet/Mewtocol/DynamicInterface.cs b/MewtocolNet/Mewtocol/DynamicInterface.cs new file mode 100644 index 0000000..088a1a6 --- /dev/null +++ b/MewtocolNet/Mewtocol/DynamicInterface.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MewtocolNet.Responses; + +namespace MewtocolNet { + public partial class MewtocolInterface { + + public event Action RegisterChanged; + + internal CancellationTokenSource cTokenAutoUpdater; + protected internal bool isWriting = false; + + + public bool ContinousReaderRunning { get; set; } + public List Registers { get; set; } = new List(); + public List Contacts { get; set; } = new List(); + + /// + /// Trys to connect to the PLC by the IP given in the constructor + /// + /// Gets called when a connection with a PLC was established + /// Gets called when an error or timeout during connection occurs + /// + public async Task ConnectAsync (Action OnConnected = null, Action OnFailed = null) { + + var plcinf = await GetPLCInfoAsync(); + + if(plcinf is not null) { + + if(OnConnected != null) OnConnected(plcinf); + + } else { + + if(OnFailed != null) OnFailed(); + + } + + return this; + + } + + #region Register Adding + + /// + /// Adds a PLC memory register to the watchlist + /// The registers can be read back by attaching + /// + /// to the end of a method + /// + /// + /// The type of the register translated from C# to IEC 61131-3 types + /// C# ------ IEC + /// short => INT/WORD + /// ushort => UINT + /// int => DOUBLE + /// uint => UDOUBLE + /// float => REAL + /// string => STRING + /// + /// The address of the register in the PLCs memory + /// The length of the string (Can be ignored for other types) + public void AddRegister (int _address, int _length = 1) { + + Type regType = typeof(T); + + if (regType == typeof(short)) { + Registers.Add(new NRegister(_address)); + } else if (regType == typeof(ushort)) { + Registers.Add(new NRegister(_address)); + } else if (regType == typeof(int)) { + Registers.Add(new NRegister(_address)); + } else if (regType == typeof(uint)) { + Registers.Add(new NRegister(_address)); + } else if (regType == typeof(float)) { + Registers.Add(new NRegister(_address)); + } else if (regType == typeof(string)) { + Registers.Add(new SRegister(_address, _length)); + } else { + throw new NotSupportedException($"The type {regType} is not allowed for Registers \n" + + $"Allowed are: short, ushort, int, uint, float and string"); + } + + } + + /// + /// Adds a PLC memory register to the watchlist + /// The registers can be read back by attaching + /// + /// to the end of a method + /// + /// + /// The type of the register translated from C# to IEC 61131-3 types + /// C# ------ IEC + /// short => INT/WORD + /// ushort => UINT + /// int => DOUBLE + /// uint => UDOUBLE + /// float => REAL + /// string => STRING + /// + /// A naming definition for QOL, doesn't effect PLC and is optional + /// The address of the register in the PLCs memory + /// The length of the string (Can be ignored for other types) + public void AddRegister(string _name, int _address, int _length = 1) { + + Type regType = typeof(T); + + if (regType == typeof(short)) { + Registers.Add(new NRegister(_address, _name)); + } else if (regType == typeof(ushort)) { + Registers.Add(new NRegister(_address, _name)); + } else if (regType == typeof(int)) { + Registers.Add(new NRegister(_address, _name)); + } else if (regType == typeof(uint)) { + Registers.Add(new NRegister(_address, _name)); + } else if (regType == typeof(float)) { + Registers.Add(new NRegister(_address, _name)); + } else if (regType == typeof(string)) { + Registers.Add(new SRegister(_address, _length, _name)); + } else { + throw new NotSupportedException($"The type {regType} is not allowed for Registers \n" + + $"Allowed are: short, ushort, int, uint, float and string"); + } + + } + + #endregion + + internal void InvokeRegisterChanged (Register reg) { + + RegisterChanged?.Invoke (reg); + + } + + } +} diff --git a/MewtocolNet/Mewtocol/LinkedLists.cs b/MewtocolNet/Mewtocol/LinkedLists.cs new file mode 100644 index 0000000..9924761 --- /dev/null +++ b/MewtocolNet/Mewtocol/LinkedLists.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; + +namespace MewtocolNet.Links { + + public class LinkedData { + + public static Dictionary ErrorCodes = new System.Collections.Generic.Dictionary { + + {21, "NACK error"}, + {22, "WACK error"}, + {23, "Station number overlap"}, + {24, "Transmission error"}, + {25, "Hardware error"}, + {26, "Station number setting error"}, + {27, "Frame over error"}, + {28, "No response error"}, + {29, "Buffer close error"}, + {30, "Timeout error"}, + {32, "Transmission impossible"}, + {33, "Communication stop"}, + {36, "No local station"}, + {38, "Other com error"}, + {40, "BCC error"}, + {41, "Format error"}, + {42, "Not supported error"}, + {43, "Procedure error"}, + {50, "Link setting error"}, + {51, "Simultanious operation error"}, + {52, "Sending disable error"}, + {53, "Busy error"}, + {60, "Paramter error"}, + {61, "Data error"}, + {62, "Registration error"}, + {63, "Mode error"}, + {66, "Adress error"}, + {67, "No data error"}, + {72, "Timeout"}, + {73, "Timeout"}, + + }; + + + } + +} \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/MewtocolEvents.cs b/MewtocolNet/Mewtocol/MewtocolEvents.cs new file mode 100644 index 0000000..5289a4e --- /dev/null +++ b/MewtocolNet/Mewtocol/MewtocolEvents.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Linq; +using MewtocolNet.Responses; + +namespace MewtocolNet.Events { + + public class MewtocolContactListener : IDisposable { + + /// + /// Gets fired whenever a contact of the observed list changes its value + /// + public event Action> ContactsChangedValue; + + //privates + private List lastContacts = new List(); + private CancellationTokenSource cToken = new CancellationTokenSource(); + + public static MewtocolContactListener ListenContactChanges (MewtocolInterface _interFace, List _observeContacts, int _refreshMS = 100, int _stationNumber = 1) { + + MewtocolContactListener listener = new MewtocolContactListener(); + _ = Task.Factory.StartNew( async () => { + //get contacts first time + listener.lastContacts = (List) await _interFace.ReadBoolContacts(_observeContacts, _stationNumber); + while(!listener.cToken.Token.IsCancellationRequested) { + //compare and update + var newContactData = (List) await _interFace.ReadBoolContacts(_observeContacts, _stationNumber); + var difference = newContactData.Where(p => listener.lastContacts.Any(l => p.Value != l.Value && p.Identifier == l.Identifier)); + + if(difference.Count() > 0) { + listener.ContactsChangedValue?.Invoke(difference.ToList()); + listener.lastContacts = newContactData; + } else { + } + await Task.Delay(_refreshMS); + } + }); + return listener; + } + + public void Dispose() { + cToken.Cancel(); + } + } + +} \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/MewtocolHelpers.cs b/MewtocolNet/Mewtocol/MewtocolHelpers.cs new file mode 100644 index 0000000..38fa548 --- /dev/null +++ b/MewtocolNet/Mewtocol/MewtocolHelpers.cs @@ -0,0 +1,121 @@ +using System; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using MewtocolNet.Responses; + +namespace MewtocolNet { + public static class MewtocolHelpers { + public static Byte[] ToHexASCIIBytes (this string _str) { + ASCIIEncoding ascii = new ASCIIEncoding(); + Byte[] bytes = ascii.GetBytes(_str.ToUpper()); + return bytes; + } + + public static string BuildBCCFrame (this string asciiArr) { + Encoding ae = Encoding.ASCII; + byte[] b = ae.GetBytes(asciiArr); + byte xorTotalByte = 0; + for(int i = 0; i < b.Length; i++) + xorTotalByte ^= b[i]; + return asciiArr.Insert(asciiArr.Length, xorTotalByte.ToString("X2")); + } + + public static byte[] ParseDTBytes (this string _onString ,int _blockSize = 4) { + var res = new Regex(@"\%([0-9]{2})\$RD(.{"+_blockSize+"})").Match(_onString); + if(res.Success) { + string val = res.Groups[2].Value; + return val.HexStringToByteArray(); + } + return null; + } + + public static string ParseDTByteString (this string _onString, int _blockSize = 4) { + var res = new Regex(@"\%([0-9]{2})\$RD(.{" + _blockSize + "})").Match(_onString); + if (res.Success) { + string val = res.Groups[2].Value; + return val; + } + return null; + } + + public static string ParseDTString (this string _onString) { + var res = new Regex(@"\%([0-9]{2})\$RD.{8}(.*)...").Match(_onString); + if(res.Success) { + string val = res.Groups[2].Value; + return val.GetStringFromAsciiHex(); + } + return null; + } + + public static string BuildDTString (this string _inString, short _stringReservedSize) { + StringBuilder sb = new StringBuilder(); + //06000600 + short stringSize = (short)_inString.Length; + var sizeBytes = BitConverter.GetBytes(stringSize).ToHexString(); + var reservedSizeBytes = BitConverter.GetBytes(_stringReservedSize).ToHexString(); + //reserved string count bytes + sb.Append(reservedSizeBytes); + //string count actual bytes + sb.Append(sizeBytes); + //actual string content + sb.Append(_inString.GetAsciiHexFromString().PadRight(_stringReservedSize * 2, '0')); + + return sb.ToString(); + } + + + public static string GetStringFromAsciiHex (this string input) { + if (input.Length % 2 != 0) + throw new ArgumentException("input not a hex string"); + byte[] bytes = new byte[input.Length / 2]; + for (int i = 0; i < input.Length; i += 2) { + String hex = input.Substring(i, 2); + bytes[i/2] = Convert.ToByte(hex, 16); + } + return Encoding.ASCII.GetString(bytes); + } + + public static string GetAsciiHexFromString (this string input) { + var bytes = new ASCIIEncoding().GetBytes(input); + return bytes.ToHexString(); + } + + public static byte[] HexStringToByteArray(this string hex) { + return Enumerable.Range(0, hex.Length) + .Where(x => x % 2 == 0) + .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) + .ToArray(); + } + + public static string ToHexString (this byte[] arr) { + StringBuilder sb = new StringBuilder(); + foreach (var b in arr) { + sb.Append(b.ToString("X2")); + } + return sb.ToString(); + } + + public static string ToJsonString (this IEnumerable _contacts, bool formatPretty = false) { + return JsonSerializer.Serialize(_contacts, new JsonSerializerOptions { + WriteIndented = formatPretty, + }); + } + + public static bool IsSubclassOfRawGeneric(Type generic, Type toCheck) { + while (toCheck != null && toCheck != typeof(object)) { + var cur = toCheck.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck; + if (generic == cur) { + return true; + } + toCheck = toCheck.BaseType; + } + return false; + } + + } + +} \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/MewtocolInterface.cs b/MewtocolNet/Mewtocol/MewtocolInterface.cs new file mode 100644 index 0000000..bba1a42 --- /dev/null +++ b/MewtocolNet/Mewtocol/MewtocolInterface.cs @@ -0,0 +1,135 @@ +using System; +using System.IO; +using System.Net.Sockets; +using System.Text; +using System.Text.RegularExpressions; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MewtocolNet.Responses; + +namespace MewtocolNet { + + public partial class MewtocolInterface { + + /// + /// Generic information about the connected PLC + /// + public PLCInfo PlcInfo {get;private set;} + private CancellationTokenSource tokenSource; + + private string ip {get;set;} + private int port {get;set;} + public int ConnectionTimeout {get;set;} = 2000; + + #region Initialization + /// + /// Builds a new Interfacer for a PLC + /// + /// + /// + public MewtocolInterface (string _ip, int _port = 9094) { + ip = _ip; + port = _port; + } + + + #endregion + + #region Low level command handling + + /// + /// Sends a command to the PLC and awaits results + /// + /// MEWTOCOL Formatted request string ex: %01#RT + /// Auto close of frame [true]%01#RT01\r [false]%01#RT + /// Returns the result + public async Task SendCommandAsync (string _msg) { + + _msg = _msg.BuildBCCFrame(); + _msg += "\r"; + + //send request + var response = await SendSingleBlock(_msg); + + if(response == null) { + return new CommandResult { + Success = false, + Error = "0000", + ErrorDescription = "null result" + }; + } + + //error catching + Regex errorcheck = new Regex(@"\%[0-9]{2}\!([0-9]{2})", RegexOptions.IgnoreCase); + Match m = errorcheck.Match(response.ToString()); + if (m.Success) { + string eCode = m.Groups[1].Value; + string eDes = Links.LinkedData.ErrorCodes[Convert.ToInt32(eCode)]; + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Response is: {response}"); + Console.WriteLine($"Error on command {_msg.Replace("\r", "")} the PLC returned error code: {eCode}, {eDes}"); + Console.ResetColor(); + return new CommandResult { + Success = false, + Error = eCode, + ErrorDescription = eDes + }; + } + + return new CommandResult { + Success = true, + Error = "0000", + Response = response.ToString() + }; + + } + + private async Task SendSingleBlock (string _blockString) { + + if(isWriting) { + return null; + } + + tokenSource = new CancellationTokenSource(); + + using (TcpClient client = new TcpClient() { ReceiveBufferSize = 64, NoDelay = true, ExclusiveAddressUse = true }) { + + try { + await client.ConnectAsync(ip, port, tokenSource.Token); + } catch(SocketException) { + return null; + } + + using (NetworkStream stream = client.GetStream()) { + var message = _blockString.ToHexASCIIBytes(); + var messageAscii = BitConverter.ToString(message).Replace("-", " "); + //send request + isWriting = true; + using (var sendStream = new MemoryStream(message)) { + await sendStream.CopyToAsync(stream); + //log message sent + ASCIIEncoding enc = new ASCIIEncoding(); + string characters = enc.GetString(message); + } + //await result + StringBuilder response = new StringBuilder(); + byte[] responseBuffer = new byte[256]; + do { + int bytes = stream.Read(responseBuffer, 0, responseBuffer.Length); + response.Append(Encoding.UTF8.GetString(responseBuffer, 0, bytes)); + } + while (stream.DataAvailable); + isWriting = false; + return response.ToString(); + } + + } + + } + + + #endregion + } + +} \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/MewtocolInterfaceExtensions.cs b/MewtocolNet/Mewtocol/MewtocolInterfaceExtensions.cs new file mode 100644 index 0000000..37c397a --- /dev/null +++ b/MewtocolNet/Mewtocol/MewtocolInterfaceExtensions.cs @@ -0,0 +1,120 @@ +using MewtocolNet.Responses; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MewtocolNet { + + public static class MewtocolInterfaceExtensions { + + /// + /// Attaches a continous reader that reads back the Registers and Contacts + /// + public static Task AttachContinousReader (this Task interfaceTask, int _refreshTimeMS = 200) { + + interfaceTask.Wait(-1); + + var interf = interfaceTask.Result; + + if (interf.ContinousReaderRunning) + return Task.CompletedTask; + + interf.cTokenAutoUpdater = new CancellationTokenSource(); + + Console.WriteLine("Attaching cont reader"); + + Task.Factory.StartNew(async () => { + + var plcinf = await interf.GetPLCInfoAsync(); + if (plcinf == null) { + Console.WriteLine("PLC is not reachable"); + throw new Exception("PLC is not reachable"); + } + if (!plcinf.OperationMode.RunMode) { + Console.WriteLine("PLC is not running"); + throw new Exception("PLC is not running"); + } + + interf.ContinousReaderRunning = true; + + while (true) { + + //dont update when currently writing a var + if (interf.isWriting) { + continue; + } + + await Task.Delay(_refreshTimeMS); + foreach (var reg in interf.Registers) { + + if (reg is NRegister shortReg) { + var lastVal = shortReg.Value; + var readout = (await interf.ReadNumRegister(shortReg)).Register.Value; + if (lastVal != readout) { + shortReg.LastValue = readout; + interf.InvokeRegisterChanged(shortReg); + shortReg.TriggerNotifyChange(); + } + } + if (reg is NRegister ushortReg) { + var lastVal = ushortReg.Value; + var readout = (await interf.ReadNumRegister(ushortReg)).Register.Value; + if (lastVal != readout) { + ushortReg.LastValue = readout; + interf.InvokeRegisterChanged(ushortReg); + ushortReg.TriggerNotifyChange(); + } + } + if (reg is NRegister intReg) { + var lastVal = intReg.Value; + var readout = (await interf.ReadNumRegister(intReg)).Register.Value; + if (lastVal != readout) { + intReg.LastValue = readout; + interf.InvokeRegisterChanged(intReg); + intReg.TriggerNotifyChange(); + } + } + if (reg is NRegister uintReg) { + var lastVal = uintReg.Value; + var readout = (await interf.ReadNumRegister(uintReg)).Register.Value; + if (lastVal != readout) { + uintReg.LastValue = readout; + interf.InvokeRegisterChanged(uintReg); + uintReg.TriggerNotifyChange(); + } + } + if (reg is NRegister floatReg) { + var lastVal = floatReg.Value; + var readout = (await interf.ReadNumRegister(floatReg)).Register.Value; + if (lastVal != readout) { + floatReg.LastValue = readout; + interf.InvokeRegisterChanged(floatReg); + floatReg.TriggerNotifyChange(); + } + } else if (reg is SRegister stringReg) { + var lastVal = stringReg.Value; + var readout = (await interf.ReadStringRegister(stringReg)).Register.Value; + if (lastVal != readout) { + interf.InvokeRegisterChanged(stringReg); + stringReg.TriggerNotifyChange(); + } + + } + + } + + + } + + }, interf.cTokenAutoUpdater.Token); + + return Task.CompletedTask; + + } + + } + +} diff --git a/MewtocolNet/Mewtocol/MewtocolInterfaceRequests.cs b/MewtocolNet/Mewtocol/MewtocolInterfaceRequests.cs new file mode 100644 index 0000000..2d6aa1e --- /dev/null +++ b/MewtocolNet/Mewtocol/MewtocolInterfaceRequests.cs @@ -0,0 +1,255 @@ +using System; +using System.IO; +using System.Net.Sockets; +using System.Text; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using MewtocolNet.Responses; +using System.Linq; +using System.Globalization; + +namespace MewtocolNet { + + public partial class MewtocolInterface { + + #region High level command handling + + /// + /// Gets generic information about the PLC + /// + /// A PLCInfo class + public async Task GetPLCInfoAsync () { + var resu = await SendCommandAsync("%01#RT"); + if(!resu.Success) return null; + + var reg = new Regex(@"\%([0-9]{2})\$RT([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{4})..", RegexOptions.IgnoreCase); + Match m = reg.Match(resu.Response); + + if(m.Success) { + + string station = m.Groups[1].Value; + string cpu = m.Groups[2].Value; + string version = m.Groups[3].Value; + string capacity = m.Groups[4].Value; + string operation = m.Groups[5].Value; + + string errorflag = m.Groups[7].Value; + string error = m.Groups[8].Value; + + PLCInfo retInfo = new PLCInfo { + CpuInformation = PLCInfo.CpuInfo.BuildFromHexString(cpu, version, capacity), + OperationMode = PLCInfo.PLCMode.BuildFromHex(operation), + ErrorCode = error, + StationNumber = int.Parse(station ?? "0"), + }; + return retInfo; + + } + return null; + } + + /// + /// Reads bool values from the plc by the given Contact List + /// + /// A list of contacts + /// The PLCs station number + /// List of IBoolContact with unique copys of the given contacts + public async Task> ReadBoolContacts (List _contactsToRead, int _stationNumber = 1) { + + //re order by contact pfx for faster querying + _contactsToRead = _contactsToRead.OrderBy(x=>x.Prefix).ToList(); + + //return list + List returnContacts = new List(); + + //grouped by 8 each + List> nestedContacts = new List>(); + + //group into max 8 contacts list + List tempGroup = new List(); + for (int i = 0; i < _contactsToRead.Count; i++) { + tempGroup.Add(_contactsToRead[i]); + //each 8 contacts make a new list + if(i % 7 == 0 && i != 0 && i != _contactsToRead.Count) { + nestedContacts.Add(tempGroup); + tempGroup = new List(); + } + //if end of list and contacts cannot be broke down to 8 each group + if(i == _contactsToRead.Count - 1 && _contactsToRead.Count % 8 != 0) { + nestedContacts.Add(tempGroup); + tempGroup = new List(); + } + } + + //make task for each group + foreach (var group in nestedContacts) { + //regex for getting values + StringBuilder regexString = new StringBuilder(@"\%..\$RC"); + //append start %01#RCP2 + StringBuilder messageString = new StringBuilder(); + messageString.Append($"%{_stationNumber.ToString().PadLeft(2, '0')}#RCP"); + messageString.Append($"{group.Count}"); + //append each contact of group Y0000 Y0001 etc + foreach (var cont in group) { + messageString.Append(cont.BuildMewtocolIdent()); + regexString.Append(@"([0-9])"); + } + regexString.Append(@"(..)"); + //parse the result + var result = await SendCommandAsync(messageString.ToString()); + Regex regCheck = new Regex(regexString.ToString(), RegexOptions.IgnoreCase); + if(result.Success && regCheck.IsMatch(result.Response)) { + //parse result string + Match regMatch = regCheck.Match(result.Response); + // add to return list + for (int i = 0; i < group.Count; i++) { + Contact cont = group[i].ShallowCopy(); + Contact toadd = cont; + if( regMatch.Groups[i + 1].Value == "1" ) { + toadd.Value = true; + } else if( regMatch.Groups[i + 1].Value == "0" ) { + toadd.Value = false; + } + returnContacts.Add(toadd); + } + } + } + return returnContacts; + } + + /// + /// Writes a boolen value to the given contact + /// + /// The contact to write + /// The boolean state to write + /// Station Number (optional) + /// A result struct + public async Task WriteContact (Contact _contact, bool _value, int _stationNumber = 1) { + string stationNum = _stationNumber.ToString().PadLeft(2, '0'); + string dataArea = _contact.BuildMewtocolIdent(); + string dataString = _value ? "1" : "0"; + string requeststring = $"%{stationNum}#WCS{dataArea}{dataString}"; + var res = await SendCommandAsync(requeststring); + return res; + } + + /// + /// Reads the given numeric register from PLC + /// + /// Type of number (short, ushort, int, uint, float) + /// The register to read + /// Station number to access + /// A result with the given NumberRegister containing the readback value and a result struct + public async Task> ReadNumRegister (NRegister _toRead, int _stationNumber = 1) { + + Type numType = typeof(T); + + string requeststring = $"%{_stationNumber.ToString().PadLeft(2, '0')}#RD{_toRead.BuildMewtocolIdent()}"; + var result = await SendCommandAsync(requeststring); + + if (numType == typeof(short)) { + + var resultBytes = result.Response.ParseDTByteString(4); + var val = short.Parse(resultBytes, NumberStyles.HexNumber); + (_toRead as NRegister).LastValue = val; + + } else if (numType == typeof(ushort)) { + var resultBytes = result.Response.ParseDTBytes(4); + var val = BitConverter.ToInt16(resultBytes); + _toRead.Value = (T)Convert.ChangeType(val, typeof(T)); + } else if (numType == typeof(int)) { + var resultBytes = result.Response.ParseDTBytes(8); + var val = BitConverter.ToInt16(resultBytes); + _toRead.Value = (T)Convert.ChangeType(val, typeof(T)); + } else if (numType == typeof(uint)) { + var resultBytes = result.Response.ParseDTBytes(8); + var val = BitConverter.ToInt16(resultBytes); + _toRead.Value = (T)Convert.ChangeType(val, typeof(T)); + } else if (numType == typeof(float)) { + var resultBytes = result.Response.ParseDTBytes(8); + var val = BitConverter.ToSingle(resultBytes); + _toRead.Value = (T)Convert.ChangeType(val, typeof(T)); + } + + var finalRes = new NRegisterResult { + Result = result, + Register = _toRead + }; + + return finalRes; + } + + /// + /// Reads the given numeric register from PLC + /// + /// Type of number (short, ushort, int, uint, float) + /// The register to write + /// Station number to access + /// A result with the given NumberRegister and a result struct + public async Task> WriteNumRegister(NRegister _toWrite, T _value, int _stationNumber = 1) { + + byte[] toWriteVal; + Type numType = typeof(T); + + if (numType == typeof(short)) { + toWriteVal = BitConverter.GetBytes(Convert.ToInt16(_value)); + } else if (numType == typeof(ushort)) { + toWriteVal = BitConverter.GetBytes(Convert.ToUInt16(_value)); + } else if (numType == typeof(int)) { + toWriteVal = BitConverter.GetBytes(Convert.ToInt32(_value)); + } else if (numType == typeof(uint)) { + toWriteVal = BitConverter.GetBytes(Convert.ToUInt32(_value)); + } else if (numType == typeof(float)) { + toWriteVal = BitConverter.GetBytes(Convert.ToUInt32(_value)); + } else { + toWriteVal = null; + } + + string requeststring = $"%{_stationNumber.ToString().PadLeft(2, '0')}#WD{_toWrite.BuildMewtocolIdent()}{toWriteVal.ToHexString()}"; + var result = await SendCommandAsync(requeststring); + + return new NRegisterResult { + Result = result, + Register = _toWrite + }; + } + + + public async Task ReadStringRegister (SRegister _toRead, int _stationNumber = 1) { + string requeststring = $"%{_stationNumber.ToString().PadLeft(2, '0')}#RD{_toRead.BuildMewtocolIdent()}"; + var result = await SendCommandAsync(requeststring); + if (result.Success) + _toRead.SetValueFromPLC(result.Response.ParseDTString()); + return new SRegisterResult { + Result = result, + Register = _toRead + }; + } + + public async Task WriteStringRegister(SRegister _toWrite, string _value, int _stationNumber = 1) { + + if (_value == null) _value = ""; + if(_value.Length > _toWrite.ReservedSize) { + throw new ArgumentException("Write string size cannot be longer than reserved string size"); + } + + string stationNum = _stationNumber.ToString().PadLeft(2, '0'); + string dataArea = _toWrite.BuildMewtocolIdent(); + string dataString = _value.BuildDTString(_toWrite.ReservedSize); + string requeststring = $"%{stationNum}#WD{dataArea}{dataString}"; + + Console.WriteLine($"reserved: {_toWrite.MemoryLength}, size: {_value.Length}"); + + var result = await SendCommandAsync(requeststring); + return new SRegisterResult { + Result = result, + Register = _toWrite + }; + } + + #endregion + + } + +} diff --git a/MewtocolNet/Mewtocol/Register.cs b/MewtocolNet/Mewtocol/Register.cs new file mode 100644 index 0000000..2b11010 --- /dev/null +++ b/MewtocolNet/Mewtocol/Register.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolNet.Responses { + + /// + /// A class describing a register + /// + public abstract class Register : INotifyPropertyChanged { + + /// + /// Gets called whenever the value was changed + /// + public event Action ValueChanged; + public event PropertyChangedEventHandler PropertyChanged; + + public string Name { get; set; } + public int MemoryAdress { get; set; } + public int MemoryLength { get; set; } + public virtual string BuildMewtocolIdent() { + StringBuilder asciistring = new StringBuilder("D"); + asciistring.Append(MemoryAdress.ToString().PadLeft(5, '0')); + asciistring.Append((MemoryAdress + MemoryLength).ToString().PadLeft(5, '0')); + return asciistring.ToString(); + } + protected void TriggerChangedEvnt(object changed) { + ValueChanged?.Invoke(changed); + } + + public void TriggerNotifyChange () { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value")); + } + + public string GetValueString () { + + if (this is NRegister shortReg) { + return shortReg.Value.ToString(); + } + if (this is NRegister ushortReg) { + return ushortReg.Value.ToString(); + } + if (this is NRegister intReg) { + return intReg.Value.ToString(); + } + if (this is NRegister uintReg) { + return uintReg.Value.ToString(); + } + if (this is NRegister floatReg) { + return floatReg.Value.ToString(); + } + else if (this is SRegister stringReg) { + return stringReg.Value.ToString(); + + } + + return "Type of the register is not supported."; + + } + + } + /// + /// Defines a register containing a number + /// + /// The type of the numeric value + public class NRegister : Register { + + public T NeedValue; + public T LastValue; + + /// + /// The value of the register + /// + public T Value { + get => LastValue; + set { + NeedValue = value; + TriggerChangedEvnt(this); + } + } + + /// + /// Defines a register containing a number + /// + /// Memory start adress max 99999 + /// The format in which the variable is stored + public NRegister(int _adress, string _name = null) { + if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); + MemoryAdress = _adress; + Name = _name; + Type numType = typeof(T); + if (numType == typeof(short)) { + MemoryLength = 0; + } else if (numType == typeof(ushort)) { + MemoryLength = 0; + } else if (numType == typeof(int)) { + MemoryLength = 1; + } else if (numType == typeof(uint)) { + MemoryLength = 1; + } else if (numType == typeof(float)) { + MemoryLength = 1; + } else { + throw new NotSupportedException($"The type {numType} is not allowed for Number Registers"); + } + } + + public override string ToString() { + return $"Adress: {MemoryAdress} Val: {Value}"; + } + } + + /// + /// Result for a read/write operation + /// + /// The type of the numeric value + public class NRegisterResult { + public CommandResult Result { get; set; } + public NRegister Register { get; set; } + + public override string ToString() { + string errmsg = Result.Success ? "" : $", Error [{Result.ErrorDescription}]"; + return $"Result [{Result.Success}], Register [{Register.ToString()}]{errmsg}"; + } + } + + /// + /// Defines a register containing a string + /// + public class SRegister : Register { + + private string lastVal = ""; + public string Value { + + get => lastVal; + + } + + public short ReservedSize { get; set; } + + /// + /// Defines a register containing a string + /// + public SRegister(int _adress, int _reservedStringSize, string _name = null) { + if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); + Name = _name; + MemoryAdress = _adress; + ReservedSize = (short)_reservedStringSize; + MemoryLength = 1 + (_reservedStringSize) / 2; + } + + public override string ToString() { + return $"Adress: {MemoryAdress} Val: {Value}"; + } + + public override string BuildMewtocolIdent() { + StringBuilder asciistring = new StringBuilder("D"); + asciistring.Append(MemoryAdress.ToString().PadLeft(5, '0')); + asciistring.Append((MemoryAdress + MemoryLength).ToString().PadLeft(5, '0')); + return asciistring.ToString(); + } + + public void SetValueFromPLC (string val) { + lastVal = val; + TriggerChangedEvnt(this); + TriggerNotifyChange(); + } + + + } + + public class SRegisterResult { + public CommandResult Result { get; set; } + public SRegister Register { get; set; } + + public override string ToString() { + string errmsg = Result.Success ? "" : $", Error [{Result.ErrorDescription}]"; + return $"Result [{Result.Success}], Register [{Register.ToString()}]{errmsg}"; + } + } +} diff --git a/MewtocolNet/Mewtocol/Responses.cs b/MewtocolNet/Mewtocol/Responses.cs new file mode 100644 index 0000000..0550346 --- /dev/null +++ b/MewtocolNet/Mewtocol/Responses.cs @@ -0,0 +1,343 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Linq; +using System.Text; +using System.ComponentModel; + +namespace MewtocolNet.Responses { + + /// + /// The formatted result of a ascii command + /// + public struct CommandResult { + public bool Success {get;set;} + public string Response {get;set;} + public string Error {get;set;} + public string ErrorDescription {get;set;} + + public override string ToString() { + string errmsg = Success ? "" : ErrorDescription; + return $"Success: {Success}, Response: {Response} {errmsg}"; + } + } + + + + /// + /// Contains generic information about the plc + /// + public class PLCInfo { + + public class PLCMode { + public bool RunMode {get;set;} + public bool TestRunMode {get;set;} + public bool BreakExcecuting {get;set;} + public bool BreakValid {get;set;} + public bool OutputEnabled {get;set;} + public bool StepRunMode {get;set;} + public bool MessageExecuting {get;set;} + public bool RemoteMode {get;set;} + + /// + /// Gets operation mode from 2 digit hex number + /// + public static PLCMode BuildFromHex (string _hexString) { + + string lower = Convert.ToString(Convert.ToInt32(_hexString.Substring(0, 1)), 2).PadLeft(4, '0'); + string higher = Convert.ToString(Convert.ToInt32(_hexString.Substring(1, 1)), 2).PadLeft(4, '0'); + string combined = lower + higher; + + var retMode = new PLCMode(); + + for (int i = 0; i < 8; i++) { + char digit = combined[i]; + bool state = false; + if(digit.ToString() == "1") state = true; + switch (i) { + case 0 : + retMode.RunMode = state; + break; + case 1 : + retMode.TestRunMode = state; + break; + case 2 : + retMode.BreakExcecuting = state; + break; + case 3 : + retMode.BreakValid = state; + break; + case 4 : + retMode.OutputEnabled = state; + break; + case 5 : + retMode.StepRunMode = state; + break; + case 6 : + retMode.MessageExecuting = state; + break; + case 7 : + retMode.RemoteMode = state; + break; + } + } + + return retMode; + + } + } + + public class CpuInfo { + public enum CpuType { + FP0_FP1_2_7K, + FP0_FP1_5K_10K, + FP1_M_0_9K, + FP2_16K_32K, + FP3_C_10K, + FP3_C_16K, + FP5_16K, + FP5_24K, + FP_Sigma_X_H_30K_60K_120K + + } + + public CpuType Cputype {get;set;} + public int ProgramCapacity {get;set;} + public string CpuVersion {get;set;} + + + public static CpuInfo BuildFromHexString (string _cpuType, string _cpuVersion, string _progCapacity) { + + CpuInfo retInf = new CpuInfo(); + + switch (_cpuType) { + case "02": + retInf.Cputype = CpuType.FP5_16K; + break; + case "03": + retInf.Cputype = CpuType.FP3_C_10K; + break; + case "04": + retInf.Cputype = CpuType.FP1_M_0_9K; + break; + case "05": + retInf.Cputype = CpuType.FP0_FP1_2_7K; + break; + case "06": + retInf.Cputype = CpuType.FP0_FP1_5K_10K; + break; + case "12": + retInf.Cputype = CpuType.FP5_24K; + break; + case "13": + retInf.Cputype = CpuType.FP3_C_16K; + break; + case "20": + retInf.Cputype = CpuType.FP_Sigma_X_H_30K_60K_120K; + break; + case "50": + retInf.Cputype = CpuType.FP2_16K_32K; + break; + } + + retInf.ProgramCapacity = Convert.ToInt32(_progCapacity); + retInf.CpuVersion = _cpuVersion.Insert(1, "."); + return retInf; + + } + } + + public CpuInfo CpuInformation {get;set;} + public PLCMode OperationMode {get;set;} + public string ErrorCode {get;set;} + public int StationNumber { get;set;} + + public override string ToString () { + + return $"Type: {CpuInformation.Cputype},\n" + + $"Capacity: {CpuInformation.ProgramCapacity}k\n" + + $"CPU v: {CpuInformation.CpuVersion}\n" + + $"Station Num: {StationNumber}\n" + + $"--------------------------------\n" + + $"OP Mode: {(OperationMode.RunMode ? "Run" : "Prog")}\n" + + $"Error Code: {ErrorCode}"; + + } + + } + + + + /// + /// Contact as bool contact + /// + public interface IBoolContact { + public string Name {get;set;} + public string Identifier {get;} + public bool? Value {get;set;} + + } + + /// + /// A class describing a PLC contact + /// + public class Contact : IBoolContact { + public string Name {get;set;} + public PFX Prefix {get;set;} + public int Number {get;set;} + public ContactType Type {get;set;} + public string Endprefix {get;set;} + public string Asciistring {get => BuildMewtocolIdent();} + public string Identifier {get => Asciistring;} + public bool? Value {get;set;} = null; + public enum ContactType { + Unknown, + Input, + Output, + } + + public enum PFX { + X, + Y, + R + } + + /// + /// Creates a new base Contact + /// + /// A prefix identifier eg. X,Y,R,L + /// The number of the PLC contact + public Contact (PFX _prefix, int _number, string _name = "unknown") { + switch (_prefix) { + case PFX.X: + Type = ContactType.Input; + break; + case PFX.Y: + case PFX.R: + Type = ContactType.Output; + break; + } + + Prefix = _prefix; + Number = _number; + Name = _name; + } + + /// + /// Creates a new base Contact + /// + /// A prefix identifier eg. X,Y,R,L + /// The number of the PLC contact + public Contact (string _prefix, int _number, string _name = "unknown") { + PFX parsedPFX; + if(Enum.TryParse(_prefix, true, out parsedPFX)) { + switch (parsedPFX) { + case PFX.X: + Type = ContactType.Input; + break; + case PFX.Y: + case PFX.R: + Type = ContactType.Output; + break; + } + Prefix = parsedPFX; + Number = _number; + Name = _name; + } else { + throw new ArgumentException($"The prefix {_prefix} is no valid contact prefix"); + } + } + + /// + /// Build contact from complete contact name + /// + /// Complete contact name e.g. Y1C, Y3D or X1 + public Contact (string _contactName, string _name = "unknown") { + + string prefix = ""; + int number = 0; + string endpfx = null; + + Match regcheck = new Regex(@"(Y|X|R|L)([0-9]{1,3})?(.)?", RegexOptions.IgnoreCase).Match(_contactName); + if(regcheck.Success) { + + /* for (int i = 0; i < regcheck.Groups.Count; i++) { + var item = regcheck.Groups[i].Value; + Console.WriteLine(item); + } */ + + prefix = regcheck.Groups[1].Value; + number = regcheck.Groups[2]?.Value != string.Empty ? Convert.ToInt32(regcheck.Groups[2].Value) : -1; + endpfx = regcheck.Groups[3]?.Value; + } else { + throw new ArgumentException($"The contact {_contactName} is no valid contact"); + } + + PFX parsedPFX; + if(Enum.TryParse(prefix, true, out parsedPFX)) { + switch (parsedPFX) { + case PFX.X: + Type = ContactType.Input; + break; + case PFX.Y: + case PFX.R: + Type = ContactType.Output; + break; + } + Prefix = parsedPFX; + Number = number; + Endprefix = endpfx; + Name = _name; + + } else { + throw new ArgumentException($"The prefix {prefix} is no valid contact prefix"); + } + + Console.WriteLine(BuildMewtocolIdent()); + } + + /// + /// Builds the mewtocol ascii contact identifier + /// + /// The identifier e.g. Y0001 or Y000A or X001C + public string BuildMewtocolIdent () { + string contactstring = ""; + if(Endprefix == null) { + contactstring += Prefix; + contactstring += Number.ToString().PadLeft(4, '0'); + } else { + contactstring += Prefix; + if(Number == -1) { + contactstring += "000" + Endprefix; + } else { + contactstring += (Number.ToString() + Endprefix).PadLeft(4, '0'); + } + } + if(string.IsNullOrEmpty(contactstring)) { + return null; + } + return contactstring; + } + + /// + /// Converts the class to a generic json compatible object + /// + /// + public object ToGenericObject () { + return new { + Name = this.Name, + Identifier = this.Asciistring, + Value = this.Value + }; + } + /// + /// Creates a copy of the contact + /// + public Contact ShallowCopy() { + return (Contact) this.MemberwiseClone(); + } + + } + + +} \ No newline at end of file diff --git a/MewtocolNet/MewtocolNet.csproj b/MewtocolNet/MewtocolNet.csproj new file mode 100644 index 0000000..fa4db7c --- /dev/null +++ b/MewtocolNet/MewtocolNet.csproj @@ -0,0 +1,14 @@ + + + net5.0 + AppLogger + 0.1.5 + Felix Weiss + Womed + true + + + P:\QUELLEN\PRODUKTAKTE\DESIGN_V_V\Software SPS\Projekt FP-XH\MewtocolNet\Builds\MewtocolNet.xml + P:\QUELLEN\PRODUKTAKTE\DESIGN_V_V\Software SPS\Projekt FP-XH\MewtocolNet\Builds\ + +