From a43756f00cc627bf261b99fc1699cbd336a57f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Fri, 17 Jun 2022 17:46:47 +0200 Subject: [PATCH] Added ability to read registers in a sperate collection class - multiple bugfixes --- Examples/Program.cs | 97 +++-- MewtocolNet/Mewtocol/DynamicInterface.cs | 175 ++++++--- MewtocolNet/Mewtocol/Logging/Logger.cs | 43 +++ MewtocolNet/Mewtocol/Logging/LoggerEnums.cs | 34 ++ MewtocolNet/Mewtocol/MewtocolEvents.cs | 48 --- MewtocolNet/Mewtocol/MewtocolHelpers.cs | 92 +++-- MewtocolNet/Mewtocol/MewtocolInterface.cs | 335 +++++++++++++++++- .../Mewtocol/MewtocolInterfaceExtensions.cs | 16 - .../Mewtocol/MewtocolInterfaceRequests.cs | 187 +++++----- MewtocolNet/Mewtocol/Register.cs | 183 ---------- .../RegisterAttributes/RegisterAttribute.cs | 81 +++++ .../RegisterCollectionBase.cs | 26 ++ MewtocolNet/Mewtocol/RegisterEnums.cs | 57 +++ MewtocolNet/Mewtocol/Responses.cs | 173 --------- .../Mewtocol/Subregisters/BRegister.cs | 82 +++++ .../Mewtocol/Subregisters/BRegisterResult.cs | 13 + .../Mewtocol/Subregisters/NRegister.cs | 61 ++++ .../Mewtocol/Subregisters/NRegisterResult.cs | 19 + MewtocolNet/Mewtocol/Subregisters/Register.cs | 153 ++++++++ .../Mewtocol/Subregisters/SRegister.cs | 58 +++ .../Mewtocol/Subregisters/SRegisterResult.cs | 15 + MewtocolNet/MewtocolNet.csproj | 4 +- 22 files changed, 1313 insertions(+), 639 deletions(-) create mode 100644 MewtocolNet/Mewtocol/Logging/Logger.cs create mode 100644 MewtocolNet/Mewtocol/Logging/LoggerEnums.cs delete mode 100644 MewtocolNet/Mewtocol/MewtocolEvents.cs delete mode 100644 MewtocolNet/Mewtocol/MewtocolInterfaceExtensions.cs delete mode 100644 MewtocolNet/Mewtocol/Register.cs create mode 100644 MewtocolNet/Mewtocol/RegisterAttributes/RegisterAttribute.cs create mode 100644 MewtocolNet/Mewtocol/RegisterAttributes/RegisterCollectionBase.cs create mode 100644 MewtocolNet/Mewtocol/RegisterEnums.cs create mode 100644 MewtocolNet/Mewtocol/Subregisters/BRegister.cs create mode 100644 MewtocolNet/Mewtocol/Subregisters/BRegisterResult.cs create mode 100644 MewtocolNet/Mewtocol/Subregisters/NRegister.cs create mode 100644 MewtocolNet/Mewtocol/Subregisters/NRegisterResult.cs create mode 100644 MewtocolNet/Mewtocol/Subregisters/Register.cs create mode 100644 MewtocolNet/Mewtocol/Subregisters/SRegister.cs create mode 100644 MewtocolNet/Mewtocol/Subregisters/SRegisterResult.cs diff --git a/Examples/Program.cs b/Examples/Program.cs index b78de7f..232a912 100644 --- a/Examples/Program.cs +++ b/Examples/Program.cs @@ -1,44 +1,101 @@ using System; using System.Threading.Tasks; +using System.Linq; using System.Text.Json; using MewtocolNet; -using MewtocolNet.Responses; +using MewtocolNet.RegisterAttributes; +using System.Collections; +using MewtocolNet.Logging; namespace Examples { - class Program { - static void Main(string[] args) { - Console.WriteLine("Starting test"); + + public class TestRegisters : RegisterCollectionBase { + + //corresponds to a R100 boolean register in the PLC + [Register(100, RegisterType.R)] + public bool TestBool1 { get; private set; } + + //corresponds to a R100 boolean register in the PLC + [Register(RegisterType.X, SpecialAddress.D)] + public bool TestBoolInputXD { get; private set; } + + //corresponds to a DT1101 - DT1104 string register in the PLC with (STRING[4]) + [Register(1101, 4)] + public string TestString1 { get; private set; } + + //corresponds to a DT7000 16 bit int register in the PLC + [Register(7000)] + public short TestInt16 { get; private set; } + + //corresponds to a DTD7001 - DTD7002 32 bit int register in the PLC + [Register(7001)] + public int TestInt32 { get; private set; } + + //corresponds to a DTD7001 - DTD7002 32 bit float register in the PLC (REAL) + [Register(7003)] + public float TestFloat32 { get; private set; } + + //corresponds to a DT7005 - DT7009 string register in the PLC with (STRING[5]) + [Register(7005, 5)] + public string TestString2 { get; private set; } + + //corresponds to a DT7010 as a 16bit word/int and parses the word as single bits + [Register(7010)] + public BitArray TestBitRegister { get; private set; } + + //corresponds to a DT1204 as a 16bit word/int takes the bit at index 9 and writes it back as a boolean + [Register(1204, 9, BitCount.B16)] + public bool BitValue { get; private set; } + + + } + + class Program { + + static void Main(string[] args) { Task.Factory.StartNew(async () => { + //attaching the logger + Logger.LogLevel = LogLevel.Critical; + Logger.OnNewLogMessage((date, msg) => { + Console.WriteLine($"{date.ToString("HH:mm:ss")} {msg}"); + }); + + //setting up a new PLC interface and register collection MewtocolInterface interf = new MewtocolInterface("10.237.191.3"); + TestRegisters registers = new TestRegisters(); - interf.AddRegister("Cooler Status",1204); - interf.AddRegister(1101, 4); - - interf.WithPoller(); - - interf.RegisterChanged += (o) => { - Console.WriteLine($"DT{o.MemoryAdress} {(o.Name != null ? $"({o.Name}) " : "")}changed to {o.GetValueString()}"); - }; + //attaching the register collection and an automatic poller + interf.WithRegisterCollection(registers).WithPoller(); await interf.ConnectAsync( (plcinf) => { - Console.WriteLine("Connected to PLC:\n" + plcinf.ToString()); + //reading a value from the register collection + Console.WriteLine($"BitValue is: {registers.BitValue}"); - //read back a register value - var statusNum = (NRegister)interf.Registers[1204]; - Console.WriteLine($"Status num is: {statusNum.Value}"); + //writing a value to the registers + Task.Factory.StartNew(async () => { + + await Task.Delay(2000); + //inverts the boolean register + interf.SetRegister(nameof(registers.TestBool1), !registers.TestBool1); + //adds 10 each time the plc connects to the PLCs INT regíster + interf.SetRegister(nameof(registers.TestInt16), (short)(registers.TestInt16 + 10)); + //adds 1 each time the plc connects to the PLCs DINT regíster + interf.SetRegister(nameof(registers.TestInt32), (registers.TestInt32 + 1)); + //adds 11.11 each time the plc connects to the PLCs REAL regíster + interf.SetRegister(nameof(registers.TestFloat32), (float)(registers.TestFloat32 + 11.11)); + + interf.SetRegister(nameof(registers.TestString2), new Random().Next(0, 99999).ToString()); + + }); - }, - () => { - Console.WriteLine("Failed connection"); } ); - }); Console.ReadLine(); diff --git a/MewtocolNet/Mewtocol/DynamicInterface.cs b/MewtocolNet/Mewtocol/DynamicInterface.cs index 2eb0b0f..a596069 100644 --- a/MewtocolNet/Mewtocol/DynamicInterface.cs +++ b/MewtocolNet/Mewtocol/DynamicInterface.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using MewtocolNet.Logging; using MewtocolNet.Responses; namespace MewtocolNet { @@ -15,7 +16,6 @@ namespace MewtocolNet { internal event Action PolledCycle; internal CancellationTokenSource cTokenAutoUpdater; - internal bool isWriting; internal bool ContinousReaderRunning; internal bool usePoller = false; @@ -30,30 +30,49 @@ namespace MewtocolNet { cTokenAutoUpdater = new CancellationTokenSource(); - Console.WriteLine("Attaching cont reader"); + Logger.Log("Poller is attaching", LogLevel.Info, this); Task.Factory.StartNew(async () => { var plcinf = await GetPLCInfoAsync(); if (plcinf == null) { - Console.WriteLine("PLC is not reachable"); - throw new Exception("PLC is not reachable"); + Logger.Log("PLC not reachable, stopping logger", LogLevel.Info, this); + return; } - if (!plcinf.OperationMode.RunMode) { - Console.WriteLine("PLC is not running"); - throw new Exception("PLC is not running"); + + PolledCycle += MewtocolInterface_PolledCycle; + void MewtocolInterface_PolledCycle () { + + StringBuilder stringBuilder = new StringBuilder(); + foreach (var reg in GetAllRegisters()) { + string address = $"{reg.GetRegisterString()}{reg.GetStartingMemoryArea()}".PadRight(8, (char)32); + stringBuilder.AppendLine($"{address}{(reg.Name != null ? $" ({reg.Name})" : "")}: {reg.GetValueString()}"); + } + + Logger.Log($"Registers loaded are: \n" + + $"--------------------\n" + + $"{stringBuilder.ToString()}" + + $"--------------------", + LogLevel.Verbose, this); + + Logger.Log("Logger did its first cycle successfully", LogLevel.Info, this); + + PolledCycle -= MewtocolInterface_PolledCycle; } ContinousReaderRunning = true; while (true) { - //dont update when currently writing a var - if (isWriting) { - continue; + //do priority tasks first + if(PriorityTasks.Count > 0) { + + await PriorityTasks.FirstOrDefault(x => !x.IsCompleted); + } - await Task.Delay(pollingDelayMs); + //await Task.Delay(pollingDelayMs); + foreach (var registerPair in Registers) { var reg = registerPair.Value; @@ -102,7 +121,17 @@ namespace MewtocolNet { InvokeRegisterChanged(floatReg); floatReg.TriggerNotifyChange(); } - } else if (reg is SRegister stringReg) { + } + if (reg is BRegister boolReg) { + var lastVal = boolReg.Value; + var readout = (await ReadBoolRegister(boolReg, stationNumber)).Register.Value; + if (lastVal != readout) { + boolReg.LastValue = readout; + InvokeRegisterChanged(boolReg); + boolReg.TriggerNotifyChange(); + } + } + if (reg is SRegister stringReg) { var lastVal = stringReg.Value; var readout = (await ReadStringRegister(stringReg, stationNumber)).Register.Value; if (lastVal != readout) { @@ -115,7 +144,7 @@ namespace MewtocolNet { } //invoke cycle polled event - InvokePolledCycleDone(); + InvokePolledCycleDone(); } @@ -127,42 +156,68 @@ namespace MewtocolNet { #region Register Adding + /// /// Adds a PLC memory register to the watchlist /// The registers can be read back by attaching /// - /// - /// 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) { + /// + /// The memory area type + /// X = Physical input area (bool) + /// Y = Physical input area (bool) + /// R = Internal relay area (bool) + /// DT = Internal data area (short/ushort) + /// DDT = Internal relay area (int/uint) + /// + /// A naming definition for QOL, doesn't effect PLC and is optional + public void AddRegister (int _address, RegisterType _type, string _name = null) { - Type regType = typeof(T); - - if (regType == typeof(short)) { - Registers.Add(_address, new NRegister(_address)); - } else if (regType == typeof(ushort)) { - Registers.Add(_address, new NRegister(_address)); - } else if (regType == typeof(int)) { - Registers.Add(_address, new NRegister(_address)); - } else if (regType == typeof(uint)) { - Registers.Add(_address, new NRegister(_address)); - } else if (regType == typeof(float)) { - Registers.Add(_address, new NRegister(_address)); - } else if (regType == typeof(string)) { - Registers.Add(_address, 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"); + //as number registers + if (_type == RegisterType.DT_short) { + Registers.Add(_address, new NRegister(_address, _name)); + return; } + if (_type == RegisterType.DT_ushort) { + Registers.Add(_address, new NRegister(_address, _name)); + return; + } + if (_type == RegisterType.DDT_int) { + Registers.Add(_address, new NRegister(_address, _name)); + return; + } + if (_type == RegisterType.DDT_uint) { + Registers.Add(_address, new NRegister(_address, _name)); + return; + } + if (_type == RegisterType.DDT_float) { + Registers.Add(_address, new NRegister(_address, _name)); + return; + } + + //as bool registers + Registers.Add(_address, new BRegister(_address, _type, _name)); + + } + + /// + /// Adds a PLC memory register to the watchlist + /// The registers can be read back by attaching + /// + /// The special address of the register in the PLCs memory + /// + /// The memory area type + /// X = Physical input area (bool) + /// Y = Physical input area (bool) + /// R = Internal relay area (bool) + /// DT = Internal data area (short/ushort) + /// DDT = Internal relay area (int/uint) + /// + /// A naming definition for QOL, doesn't effect PLC and is optional + public void AddRegister (SpecialAddress _spAddress, RegisterType _type, string _name = null) { + + //as bool registers + Registers.Add((int)_spAddress, new BRegister(_spAddress, _type, _name)); } @@ -183,22 +238,35 @@ namespace MewtocolNet { /// 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) { + public void AddRegister(int _address, int _length = 1, string _name = null, bool _isBitwise = false) { Type regType = typeof(T); + if (regType != typeof(string) && _length != 1) { + throw new NotSupportedException($"_lenght parameter only allowed for register of type string"); + } + + if (Registers.Any(x => x.Key == _address)) { + + throw new NotSupportedException($"Cannot add a register multiple times, " + + $"make sure that all register attributes or AddRegister assignments have different adresses."); + + } + if (regType == typeof(short)) { - Registers.Add(_address, new NRegister(_address, _name)); + Registers.Add(_address, new NRegister(_address, _name, _isBitwise)); } else if (regType == typeof(ushort)) { Registers.Add(_address, new NRegister(_address, _name)); } else if (regType == typeof(int)) { - Registers.Add(_address, new NRegister(_address, _name)); + Registers.Add(_address, new NRegister(_address, _name, _isBitwise)); } else if (regType == typeof(uint)) { Registers.Add(_address, new NRegister(_address, _name)); } else if (regType == typeof(float)) { Registers.Add(_address, new NRegister(_address, _name)); } else if (regType == typeof(string)) { Registers.Add(_address, new SRegister(_address, _length, _name)); + } else if (regType == typeof(bool)) { + Registers.Add(_address, new BRegister(_address, RegisterType.R, _name)); } else { throw new NotSupportedException($"The type {regType} is not allowed for Registers \n" + $"Allowed are: short, ushort, int, uint, float and string"); @@ -208,6 +276,21 @@ namespace MewtocolNet { #endregion + #region Register Reading + + /// + /// Gets a list of all added registers + /// + public List GetAllRegisters () { + + return Registers.Values.ToList(); + + } + + #endregion + + #region Event Invoking + internal void InvokeRegisterChanged (Register reg) { RegisterChanged?.Invoke(reg); @@ -220,5 +303,7 @@ namespace MewtocolNet { } + #endregion + } } diff --git a/MewtocolNet/Mewtocol/Logging/Logger.cs b/MewtocolNet/Mewtocol/Logging/Logger.cs new file mode 100644 index 0000000..ee4d311 --- /dev/null +++ b/MewtocolNet/Mewtocol/Logging/Logger.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.Logging { + + /// + /// Logging module for all PLCs + /// + public static class Logger { + + /// + /// Sets the loglevel for the logger module + /// + public static LogLevel LogLevel { get; set; } + + internal static Action LogInvoked; + + /// + /// Gets invoked whenever a new log message is ready + /// + public static void OnNewLogMessage (Action onMsg) { + + LogInvoked += (t, m) => { + onMsg(t, m); + }; + + } + + internal static void Log (string message, LogLevel loglevel, MewtocolInterface sender = null) { + + if ((int)loglevel <= (int)LogLevel) { + if (sender == null) { + LogInvoked?.Invoke(DateTime.Now, message); + } else { + LogInvoked?.Invoke(DateTime.Now, $"[{sender.GetConnectionPortInfo()}] {message}"); + } + } + + } + + } +} diff --git a/MewtocolNet/Mewtocol/Logging/LoggerEnums.cs b/MewtocolNet/Mewtocol/Logging/LoggerEnums.cs new file mode 100644 index 0000000..385bdae --- /dev/null +++ b/MewtocolNet/Mewtocol/Logging/LoggerEnums.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.Logging { + + /// + /// The loglevel of the logging module + /// + public enum LogLevel { + + /// + /// Logs only errors + /// + Error = 0, + /// + /// Logs info like connection establish and loss + /// + Info = 1, + /// + /// Logs only state changes + /// + Change = 2, + /// + /// Logs all errors, state changes, and messages + /// + Verbose = 3, + /// + /// Logs all types including network traffic + /// + Critical = 4, + } + +} diff --git a/MewtocolNet/Mewtocol/MewtocolEvents.cs b/MewtocolNet/Mewtocol/MewtocolEvents.cs deleted file mode 100644 index 5289a4e..0000000 --- a/MewtocolNet/Mewtocol/MewtocolEvents.cs +++ /dev/null @@ -1,48 +0,0 @@ -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 index 38fa548..4e9432e 100644 --- a/MewtocolNet/Mewtocol/MewtocolHelpers.cs +++ b/MewtocolNet/Mewtocol/MewtocolHelpers.cs @@ -3,19 +3,30 @@ 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; +using System.Collections; namespace MewtocolNet { public static class MewtocolHelpers { - public static Byte[] ToHexASCIIBytes (this string _str) { + + /// + /// Turns a bit array into a 0 and 1 string + /// + public static string ToBitString (this BitArray arr) { + + var bits = new bool[arr.Length]; + arr.CopyTo(bits, 0); + return string.Join("", bits.Select(x => x ? "1" : "0")); + + } + + public static byte[] ToHexASCIIBytes (this string _str) { ASCIIEncoding ascii = new ASCIIEncoding(); - Byte[] bytes = ascii.GetBytes(_str.ToUpper()); + byte[] bytes = ascii.GetBytes(_str.ToUpper()); return bytes; } - public static string BuildBCCFrame (this string asciiArr) { + internal static string BuildBCCFrame (this string asciiArr) { Encoding ae = Encoding.ASCII; byte[] b = ae.GetBytes(asciiArr); byte xorTotalByte = 0; @@ -24,7 +35,7 @@ namespace MewtocolNet { return asciiArr.Insert(asciiArr.Length, xorTotalByte.ToString("X2")); } - public static byte[] ParseDTBytes (this string _onString ,int _blockSize = 4) { + internal 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; @@ -33,7 +44,7 @@ namespace MewtocolNet { return null; } - public static string ParseDTByteString (this string _onString, int _blockSize = 4) { + internal 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; @@ -42,16 +53,46 @@ namespace MewtocolNet { return null; } - public static string ParseDTString (this string _onString) { - var res = new Regex(@"\%([0-9]{2})\$RD.{8}(.*)...").Match(_onString); - if(res.Success) { + internal static bool? ParseRCSingleBit (this string _onString, int _blockSize = 4) { + var res = new Regex(@"\%([0-9]{2})\$RC(.)").Match(_onString); + if (res.Success) { string val = res.Groups[2].Value; - return val.GetStringFromAsciiHex(); + return val == "1"; } return null; } - public static string BuildDTString (this string _inString, short _stringReservedSize) { + internal 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().Trim(); + } + return null; + } + + internal static string ReverseByteOrder (this string _onString) { + + //split into 2 chars + var stringBytes = _onString.SplitInParts(2).ToList(); + + stringBytes.Reverse(); + + return string.Join("", stringBytes); + + } + + internal static IEnumerable SplitInParts (this string s, int partLength) { + if (s == null) + throw new ArgumentNullException(nameof(s)); + if (partLength <= 0) + throw new ArgumentException("Part length has to be positive.", nameof(partLength)); + + for (var i = 0; i < s.Length; i += partLength) + yield return s.Substring(i, Math.Min(partLength, s.Length - i)); + } + + internal static string BuildDTString (this string _inString, short _stringReservedSize) { StringBuilder sb = new StringBuilder(); //06000600 short stringSize = (short)_inString.Length; @@ -62,13 +103,13 @@ namespace MewtocolNet { //string count actual bytes sb.Append(sizeBytes); //actual string content - sb.Append(_inString.GetAsciiHexFromString().PadRight(_stringReservedSize * 2, '0')); + sb.Append(_inString.GetAsciiHexFromString()); return sb.ToString(); } - public static string GetStringFromAsciiHex (this string input) { + internal 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]; @@ -79,19 +120,19 @@ namespace MewtocolNet { return Encoding.ASCII.GetString(bytes); } - public static string GetAsciiHexFromString (this string input) { + internal static string GetAsciiHexFromString (this string input) { var bytes = new ASCIIEncoding().GetBytes(input); return bytes.ToHexString(); } - public static byte[] HexStringToByteArray(this string hex) { + internal 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) { + internal static string ToHexString (this byte[] arr) { StringBuilder sb = new StringBuilder(); foreach (var b in arr) { sb.Append(b.ToString("X2")); @@ -99,23 +140,6 @@ namespace MewtocolNet { 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 index e67349f..fcafb13 100644 --- a/MewtocolNet/Mewtocol/MewtocolInterface.cs +++ b/MewtocolNet/Mewtocol/MewtocolInterface.cs @@ -6,7 +6,14 @@ using System.Text.RegularExpressions; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using System.Linq; using MewtocolNet.Responses; +using MewtocolNet.RegisterAttributes; +using MewtocolNet.Logging; +using System.Collections; +using System.Reflection; +using MewtocolNet.Logging; +using System.Diagnostics; namespace MewtocolNet { @@ -33,14 +40,11 @@ namespace MewtocolNet { /// /// The registered data registers of the PLC /// - public Dictionary Registers { get; set; } = new(); - - private CancellationTokenSource tokenSource; + public Dictionary Registers { get; set; } = new Dictionary(); private string ip {get;set;} private int port {get;set;} private int stationNumber {get;set;} - private int pollingDelayMs {get;set;} /// /// The current IP of the PLC connection @@ -55,6 +59,7 @@ namespace MewtocolNet { /// public int StationNumber => stationNumber; + internal List PriorityTasks { get; set; } = new List(); #region Initialization @@ -79,6 +84,15 @@ namespace MewtocolNet { } + RegisterChanged += (o) => { + + string address = $"{o.GetRegisterString()}{o.MemoryAdress}".PadRight(5, (char)32); ; + + Logger.Log($"{address} " + + $"{(o.Name != null ? $"({o.Name}) " : "")}" + + $"changed to \"{o.GetValueString()}\"", LogLevel.Change, this); + }; + } #endregion @@ -99,7 +113,10 @@ namespace MewtocolNet { var plcinf = await GetPLCInfoAsync(); - if (plcinf is not null) { + if (plcinf != null) { + + Logger.Log("Connected", LogLevel.Info, this); + Logger.Log($"\n\n{plcinf.ToString()}\n\n", LogLevel.Verbose, this); Connected?.Invoke(plcinf); @@ -119,8 +136,10 @@ namespace MewtocolNet { } else { - if (OnFailed != null) + if (OnFailed != null) { OnFailed(); + Logger.Log("Initial connection failed", LogLevel.Info, this); + } } @@ -132,9 +151,8 @@ namespace MewtocolNet { /// Attaches a poller to the interface that continously /// polls the registered data registers and writes the values to them /// - public MewtocolInterface WithPoller (int pollerDelayMs = 50) { + public MewtocolInterface WithPoller () { - pollingDelayMs = pollerDelayMs; usePoller = true; return this; @@ -143,6 +161,258 @@ namespace MewtocolNet { #endregion + #region Register Collection + + /// + /// Attaches a register collection object to + /// the interface that can be updated automatically. + /// + /// Just create a class inheriting from + /// and assert some propertys with the custom . + /// + /// A collection inherting the class + public MewtocolInterface WithRegisterCollection (RegisterCollectionBase collection) { + + collection.PLCInterface = this; + + var props = collection.GetType().GetProperties(); + + foreach (var prop in props) { + + var attributes = prop.GetCustomAttributes(true); + + string propName = prop.Name; + foreach (var attr in attributes) { + + if(attr is RegisterAttribute cAttribute) { + + if (prop.PropertyType == typeof(bool) && cAttribute.AssignedBitIndex == -1) { + if (cAttribute.SpecialAddress == SpecialAddress.None) { + AddRegister(cAttribute.MemoryArea, cAttribute.RegisterType, _name: propName); + } else { + AddRegister(cAttribute.SpecialAddress, cAttribute.RegisterType, _name: propName); + } + } + + if (prop.PropertyType == typeof(short)) { + AddRegister(cAttribute.MemoryArea, _name: propName); + } + + if (prop.PropertyType == typeof(ushort)) { + AddRegister(cAttribute.MemoryArea, _name: propName); + } + + if (prop.PropertyType == typeof(int)) { + AddRegister(cAttribute.MemoryArea, _name: propName); + } + + if (prop.PropertyType == typeof(uint)) { + AddRegister(cAttribute.MemoryArea, _name: propName); + } + + if (prop.PropertyType == typeof(float)) { + AddRegister(cAttribute.MemoryArea, _name: propName); + } + + if (prop.PropertyType == typeof(string)) { + AddRegister(cAttribute.MemoryArea, cAttribute.StringLength, _name: propName); + } + + //read number as bit array + if (prop.PropertyType == typeof(BitArray)) { + + if(cAttribute.BitCount == BitCount.B16) { + AddRegister(cAttribute.MemoryArea, _name: propName, _isBitwise: true); + } else { + AddRegister(cAttribute.MemoryArea, _name: propName, _isBitwise: true); + } + + } + + //read number as bit array by invdividual properties + if (prop.PropertyType == typeof(bool) && cAttribute.AssignedBitIndex != -1) { + + if (cAttribute.BitCount == BitCount.B16) { + AddRegister(cAttribute.MemoryArea, _name: propName, _isBitwise: true); + } else { + AddRegister(cAttribute.MemoryArea, _name: propName, _isBitwise: true); + } + + } + + } + + } + + } + + RegisterChanged += (reg) => { + + var foundToUpdate = props.FirstOrDefault(x => x.Name == reg.Name); + + if (foundToUpdate != null) { + + var foundAttributes = foundToUpdate.GetCustomAttributes(true); + var foundAttr = foundAttributes.FirstOrDefault(x => x.GetType() == typeof(RegisterAttribute)); + + if (foundAttr == null) + return; + + var registerAttr = (RegisterAttribute)foundAttr; + + //check if bit parse mode + if (registerAttr.AssignedBitIndex == -1) { + + //setting back booleans + if (foundToUpdate.PropertyType == typeof(bool)) { + foundToUpdate.SetValue(collection, ((BRegister)reg).Value); + } + + //setting back numbers + + if (foundToUpdate.PropertyType == typeof(short)) { + foundToUpdate.SetValue(collection, ((NRegister)reg).Value); + } + + if (foundToUpdate.PropertyType == typeof(ushort)) { + foundToUpdate.SetValue(collection, ((NRegister)reg).Value); + } + + if (foundToUpdate.PropertyType == typeof(int)) { + foundToUpdate.SetValue(collection, ((NRegister)reg).Value); + } + + if (foundToUpdate.PropertyType == typeof(uint)) { + foundToUpdate.SetValue(collection, ((NRegister)reg).Value); + } + + if (foundToUpdate.PropertyType == typeof(float)) { + foundToUpdate.SetValue(collection, ((NRegister)reg).Value); + } + + //setting back strings + + if (foundToUpdate.PropertyType == typeof(string)) { + foundToUpdate.SetValue(collection, ((SRegister)reg).Value); + } + + } + + + if (foundToUpdate.PropertyType == typeof(bool) && registerAttr.AssignedBitIndex >= 0) { + + //setting back bit registers to individual properties + if (reg is NRegister shortReg) { + + var bytes = BitConverter.GetBytes(shortReg.Value); + BitArray bitAr = new BitArray(bytes); + foundToUpdate.SetValue(collection, bitAr[registerAttr.AssignedBitIndex]); + + } + + if (reg is NRegister intReg) { + + var bytes = BitConverter.GetBytes(intReg.Value); + BitArray bitAr = new BitArray(bytes); + foundToUpdate.SetValue(collection, bitAr[registerAttr.AssignedBitIndex]); + + } + + + } else if(foundToUpdate.PropertyType == typeof(BitArray)) { + + //setting back bit registers + if (reg is NRegister shortReg) { + + var bytes = BitConverter.GetBytes(shortReg.Value); + BitArray bitAr = new BitArray(bytes); + foundToUpdate.SetValue(collection, bitAr); + + } + + if (reg is NRegister intReg) { + + var bytes = BitConverter.GetBytes(intReg.Value); + BitArray bitAr = new BitArray(bytes); + foundToUpdate.SetValue(collection, bitAr); + + } + + } + + collection.TriggerPropertyChanged(foundToUpdate.Name); + + } + + }; + + return this; + + } + + #endregion + + #region Register Writing + + /// + /// Sets a register in the PLCs memory + /// + /// The name the register was given to or a property name from the RegisterCollection class + /// The value to write to the register + public void SetRegister (string registerName, object value) { + + + var foundRegister = GetAllRegisters().FirstOrDefault(x => x.Name == registerName); + + if (foundRegister == null) { + throw new Exception($"Register with the name {registerName} was not found"); + } + + if (foundRegister.GetType() == typeof(BRegister)) { + + _ = WriteBoolRegister((BRegister)foundRegister, (bool)value, StationNumber); + + } + + if (foundRegister.GetType() == typeof(NRegister)) { + + _ = WriteNumRegister((NRegister)foundRegister, (short)value, StationNumber); + + } + + if (foundRegister.GetType() == typeof(NRegister)) { + + _ = WriteNumRegister((NRegister)foundRegister, (ushort)value, StationNumber); + + } + + if (foundRegister.GetType() == typeof(NRegister)) { + + _ = WriteNumRegister((NRegister)foundRegister, (int)value, StationNumber); + + } + + if (foundRegister.GetType() == typeof(NRegister)) { + + _ = WriteNumRegister((NRegister)foundRegister, (uint)value, StationNumber); + + } + + if (foundRegister.GetType() == typeof(NRegister)) { + + _ = WriteNumRegister((NRegister)foundRegister, (float)value, StationNumber); + + } + + if (foundRegister.GetType() == typeof(SRegister)) { + + _ = WriteStringRegister((SRegister)foundRegister, (string)value, StationNumber); + + } + + } + + #endregion #region Low level command handling @@ -158,7 +428,27 @@ namespace MewtocolNet { _msg += "\r"; //send request - var response = await SendSingleBlock(_msg); + + string response = null; + + if(ContinousReaderRunning) { + + //if the poller is active then add all messages to a qeueue + + var awaittask = SendSingleBlock(_msg); + PriorityTasks.Add(awaittask); + awaittask.Wait(); + + PriorityTasks.Remove(awaittask); + response = awaittask.Result; + + } else { + + //poller not active let the user manage message timing + + response = await SendSingleBlock(_msg); + + } if(response == null) { return new CommandResult { @@ -195,16 +485,12 @@ namespace MewtocolNet { private async Task SendSingleBlock (string _blockString) { - if(isWriting) { - return null; - } - - tokenSource = new CancellationTokenSource(); + Stopwatch sw = Stopwatch.StartNew(); using (TcpClient client = new TcpClient() { ReceiveBufferSize = 64, NoDelay = true, ExclusiveAddressUse = true }) { try { - await client.ConnectAsync(ip, port, tokenSource.Token); + await client.ConnectAsync(ip, port); } catch(SocketException) { return null; } @@ -213,9 +499,9 @@ namespace MewtocolNet { var message = _blockString.ToHexASCIIBytes(); var messageAscii = BitConverter.ToString(message).Replace("-", " "); //send request - isWriting = true; using (var sendStream = new MemoryStream(message)) { await sendStream.CopyToAsync(stream); + Logger.Log($"OUT MSG: {_blockString}", LogLevel.Critical, this); //log message sent ASCIIEncoding enc = new ASCIIEncoding(); string characters = enc.GetString(message); @@ -228,7 +514,8 @@ namespace MewtocolNet { response.Append(Encoding.UTF8.GetString(responseBuffer, 0, bytes)); } while (stream.DataAvailable); - isWriting = false; + sw.Stop(); + Logger.Log($"IN MSG ({(int)sw.Elapsed.TotalMilliseconds}ms): {_blockString}", LogLevel.Critical, this); return response.ToString(); } @@ -238,6 +525,20 @@ namespace MewtocolNet { #endregion + + #region Accessing Info + + /// + /// Gets the connection info string + /// + public string GetConnectionPortInfo () { + + return $"{IpAddress}:{Port}"; + + } + + #endregion + } } \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/MewtocolInterfaceExtensions.cs b/MewtocolNet/Mewtocol/MewtocolInterfaceExtensions.cs deleted file mode 100644 index f829cd4..0000000 --- a/MewtocolNet/Mewtocol/MewtocolInterfaceExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -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 { - - - } - -} diff --git a/MewtocolNet/Mewtocol/MewtocolInterfaceRequests.cs b/MewtocolNet/Mewtocol/MewtocolInterfaceRequests.cs index 2d6aa1e..282639c 100644 --- a/MewtocolNet/Mewtocol/MewtocolInterfaceRequests.cs +++ b/MewtocolNet/Mewtocol/MewtocolInterfaceRequests.cs @@ -13,7 +13,7 @@ namespace MewtocolNet { public partial class MewtocolInterface { - #region High level command handling + #region PLC info getters /// /// Gets generic information about the PLC @@ -49,91 +49,50 @@ namespace MewtocolNet { 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(); + #endregion - //return list - List returnContacts = new List(); + #region Bool register reading / writing - //grouped by 8 each - List> nestedContacts = new List>(); + public async Task ReadBoolRegister (BRegister _toRead, int _stationNumber = 1) { - //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(); - } - } + string requeststring = $"%{_stationNumber.ToString().PadLeft(2, '0')}#RCS{_toRead.BuildMewtocolIdent()}"; + var result = await SendCommandAsync(requeststring); - //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); - } - } + if(!result.Success) { + return new BRegisterResult { + Result = result, + Register = _toRead + }; } - return returnContacts; + + var resultBool = result.Response.ParseRCSingleBit(); + if(resultBool != null) { + _toRead.LastValue = resultBool.Value; + } + + var finalRes = new BRegisterResult { + Result = result, + Register = _toRead + }; + + return finalRes; + } - /// - /// 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; + public async Task WriteBoolRegister (BRegister _toWrite, bool value, int _stationNumber = 1) { + + string requeststring = $"%{_stationNumber.ToString().PadLeft(2, '0')}#WCS{_toWrite.BuildMewtocolIdent()}{(value ? "1" : "0")}"; + + var result = await SendCommandAsync(requeststring); + + return result.Success && result.Response.StartsWith($"%{ _stationNumber.ToString().PadLeft(2, '0')}#WC"); + } + #endregion + + #region Number register reading / writing + /// /// Reads the given numeric register from PLC /// @@ -150,26 +109,39 @@ namespace MewtocolNet { if (numType == typeof(short)) { - var resultBytes = result.Response.ParseDTByteString(4); + var resultBytes = result.Response.ParseDTByteString(4).ReverseByteOrder(); 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)); + + var resultBytes = result.Response.ParseDTByteString(4).ReverseByteOrder(); + var val = ushort.Parse(resultBytes, NumberStyles.HexNumber); + (_toRead as NRegister).LastValue = val; + } else if (numType == typeof(int)) { - var resultBytes = result.Response.ParseDTBytes(8); - var val = BitConverter.ToInt16(resultBytes); - _toRead.Value = (T)Convert.ChangeType(val, typeof(T)); + + var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder(); + var val = int.Parse(resultBytes, NumberStyles.HexNumber); + (_toRead as NRegister).LastValue = val; + } else if (numType == typeof(uint)) { - var resultBytes = result.Response.ParseDTBytes(8); - var val = BitConverter.ToInt16(resultBytes); - _toRead.Value = (T)Convert.ChangeType(val, typeof(T)); + + var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder(); + var val = uint.Parse(resultBytes, NumberStyles.HexNumber); + (_toRead as NRegister).LastValue = val; + } 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 resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder(); + //convert to unsigned int first + var val = uint.Parse(resultBytes, NumberStyles.HexNumber); + + byte[] floatVals = BitConverter.GetBytes(val); + float finalFloat = BitConverter.ToSingle(floatVals, 0); + + (_toRead as NRegister).LastValue = finalFloat; + } var finalRes = new NRegisterResult { @@ -187,7 +159,7 @@ namespace MewtocolNet { /// 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) { + public async Task WriteNumRegister (NRegister _toWrite, T _value, int _stationNumber = 1) { byte[] toWriteVal; Type numType = typeof(T); @@ -201,22 +173,38 @@ namespace MewtocolNet { } else if (numType == typeof(uint)) { toWriteVal = BitConverter.GetBytes(Convert.ToUInt32(_value)); } else if (numType == typeof(float)) { - toWriteVal = BitConverter.GetBytes(Convert.ToUInt32(_value)); + + var fl = _value as float?; + if (fl == null) + throw new NullReferenceException("Float cannot be null"); + + toWriteVal = BitConverter.GetBytes(fl.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 - }; + return result.Success && result.Response.StartsWith($"%{ _stationNumber.ToString().PadLeft(2, '0')}#WD"); + } + #endregion + + #region String register reading / writing public async Task ReadStringRegister (SRegister _toRead, int _stationNumber = 1) { + + //string is build up like this + //04 00 04 00 53 50 33 35 13 + //0, 1 = reserved size + //1, 2 = current size + //3,4,5,6 = ASCII encoded chars (SP35) + //7,8 = checksum + string requeststring = $"%{_stationNumber.ToString().PadLeft(2, '0')}#RD{_toRead.BuildMewtocolIdent()}"; var result = await SendCommandAsync(requeststring); if (result.Success) @@ -227,7 +215,7 @@ namespace MewtocolNet { }; } - public async Task WriteStringRegister(SRegister _toWrite, string _value, int _stationNumber = 1) { + public async Task WriteStringRegister(SRegister _toWrite, string _value, int _stationNumber = 1) { if (_value == null) _value = ""; if(_value.Length > _toWrite.ReservedSize) { @@ -242,10 +230,7 @@ namespace MewtocolNet { Console.WriteLine($"reserved: {_toWrite.MemoryLength}, size: {_value.Length}"); var result = await SendCommandAsync(requeststring); - return new SRegisterResult { - Result = result, - Register = _toWrite - }; + return result.Success && result.Response.StartsWith($"%{ _stationNumber.ToString().PadLeft(2, '0')}#WD"); } #endregion diff --git a/MewtocolNet/Mewtocol/Register.cs b/MewtocolNet/Mewtocol/Register.cs deleted file mode 100644 index 2b11010..0000000 --- a/MewtocolNet/Mewtocol/Register.cs +++ /dev/null @@ -1,183 +0,0 @@ -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/RegisterAttributes/RegisterAttribute.cs b/MewtocolNet/Mewtocol/RegisterAttributes/RegisterAttribute.cs new file mode 100644 index 0000000..f5c45c3 --- /dev/null +++ b/MewtocolNet/Mewtocol/RegisterAttributes/RegisterAttribute.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolNet.RegisterAttributes { + + public enum BitCount { + B16, + B32 + } + + [AttributeUsage(AttributeTargets.Property)] + public class RegisterAttribute : Attribute { + + public int MemoryArea; + public int StringLength; + public RegisterType RegisterType; + public SpecialAddress SpecialAddress = SpecialAddress.None; + public BitCount BitCount; + public int AssignedBitIndex = -1; + + + public RegisterAttribute (int memoryArea, int stringLength = 1) { + + MemoryArea = memoryArea; + StringLength = stringLength; + + } + + public RegisterAttribute (int memoryArea, RegisterType type) { + + if (type.ToString().StartsWith("DT")) + throw new NotSupportedException("DT types are not supported for attribute register setups because the number type is casted automatically"); + + MemoryArea = memoryArea; + RegisterType = type; + SpecialAddress = SpecialAddress.None; + + } + + public RegisterAttribute (RegisterType type, SpecialAddress spAdress) { + + if (type.ToString().StartsWith("DT")) + throw new NotSupportedException("DT types are not supported for attribute register setups because the number type is casted automatically"); + + RegisterType = type; + SpecialAddress = spAdress; + + } + + + public RegisterAttribute (int memoryArea, BitCount bitcount) { + + MemoryArea = memoryArea; + StringLength = 0; + BitCount = bitcount; + + } + + public RegisterAttribute (int memoryArea, uint assignBit, BitCount bitcount) { + + if(assignBit > 15 && bitcount == BitCount.B16) { + throw new NotSupportedException("The assignBit parameter cannot be greater than 15 in a 16 bit var"); + } + + if (assignBit > 31 && bitcount == BitCount.B32) { + throw new NotSupportedException("The assignBit parameter cannot be greater than 31 in a 32 bit var"); + } + + MemoryArea = memoryArea; + StringLength = 0; + BitCount = bitcount; + AssignedBitIndex = (int)assignBit; + + } + + } + +} diff --git a/MewtocolNet/Mewtocol/RegisterAttributes/RegisterCollectionBase.cs b/MewtocolNet/Mewtocol/RegisterAttributes/RegisterCollectionBase.cs new file mode 100644 index 0000000..f86c61b --- /dev/null +++ b/MewtocolNet/Mewtocol/RegisterAttributes/RegisterCollectionBase.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolNet.RegisterAttributes { + + public class RegisterCollectionBase : INotifyPropertyChanged { + + public MewtocolInterface PLCInterface { get; set; } + + public event PropertyChangedEventHandler PropertyChanged; + + internal void TriggerPropertyChanged (string propertyName = null) { + var handler = PropertyChanged; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + } + +} diff --git a/MewtocolNet/Mewtocol/RegisterEnums.cs b/MewtocolNet/Mewtocol/RegisterEnums.cs new file mode 100644 index 0000000..d640f5a --- /dev/null +++ b/MewtocolNet/Mewtocol/RegisterEnums.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolNet { + public enum RegisterType { + + /// + /// Physical input as a bool (Relay) + /// + X, + /// + /// Physical output as a bool (Relay) + /// + Y, + /// + /// Internal as a bool (Relay) + /// + R, + /// + /// Data area as a short (Register) + /// + DT_short, + /// + /// Data area as an unsigned short (Register) + /// + DT_ushort, + /// + /// Double data area as an integer (Register) + /// + DDT_int, + /// + /// Double data area as an unsigned integer (Register) + /// + DDT_uint, + /// + /// Double data area as an floating point number (Register) + /// + DDT_float, + + } + + public enum SpecialAddress { + + None, + A = -10, + B = -11, + C = -12, + D = -13, + E = -14, + F = -15, + + } + +} diff --git a/MewtocolNet/Mewtocol/Responses.cs b/MewtocolNet/Mewtocol/Responses.cs index 0550346..2a79361 100644 --- a/MewtocolNet/Mewtocol/Responses.cs +++ b/MewtocolNet/Mewtocol/Responses.cs @@ -167,177 +167,4 @@ namespace MewtocolNet.Responses { } - - /// - /// 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/Mewtocol/Subregisters/BRegister.cs b/MewtocolNet/Mewtocol/Subregisters/BRegister.cs new file mode 100644 index 0000000..ee83e94 --- /dev/null +++ b/MewtocolNet/Mewtocol/Subregisters/BRegister.cs @@ -0,0 +1,82 @@ +using System; +using System.Text; +using MewtocolNet; + +namespace MewtocolNet.Responses { + + /// + /// Defines a register containing a boolean + /// + public class BRegister : Register { + + internal RegisterType RegType { get; set; } + internal SpecialAddress SpecialAddress { get; set; } + + public bool NeedValue; + public bool LastValue; + + /// + /// The value of the register + /// + public bool Value { + get => LastValue; + set { + NeedValue = value; + TriggerChangedEvnt(this); + } + } + + /// + /// Defines a register containing a number + /// + /// Memory start adress max 99999 + /// Type of boolean register + /// Name of the register + public BRegister (int _address, RegisterType _type = RegisterType.R, string _name = null) { + + if (_address > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); + MemoryAdress = _address; + Name = _name; + + RegType = _type; + + } + + /// + /// Defines a register containing a number + /// + /// Memory start adress max 99999 + /// Type of boolean register + /// Name of the register + public BRegister (SpecialAddress _address, RegisterType _type = RegisterType.R, string _name = null) { + + if (_address == SpecialAddress.None) + throw new NotSupportedException("Special adress cant be none"); + + SpecialAddress = _address; + Name = _name; + + RegType = _type; + + } + + public override string BuildMewtocolIdent () { + + //build area code from register type + StringBuilder asciistring = new StringBuilder(RegType.ToString()); + if(SpecialAddress == SpecialAddress.None) { + asciistring.Append(MemoryAdress.ToString().PadLeft(4, '0')); + } else { + asciistring.Append(SpecialAddress.ToString().PadLeft(4, '0')); + } + + return asciistring.ToString(); + + } + + public override string ToString() { + return $"Adress: {MemoryAdress} Val: {Value}"; + } + } + +} diff --git a/MewtocolNet/Mewtocol/Subregisters/BRegisterResult.cs b/MewtocolNet/Mewtocol/Subregisters/BRegisterResult.cs new file mode 100644 index 0000000..17e8f36 --- /dev/null +++ b/MewtocolNet/Mewtocol/Subregisters/BRegisterResult.cs @@ -0,0 +1,13 @@ +namespace MewtocolNet.Responses { + public class BRegisterResult { + + public CommandResult Result { get; set; } + public BRegister 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/Subregisters/NRegister.cs b/MewtocolNet/Mewtocol/Subregisters/NRegister.cs new file mode 100644 index 0000000..49bbf5a --- /dev/null +++ b/MewtocolNet/Mewtocol/Subregisters/NRegister.cs @@ -0,0 +1,61 @@ +using System; + +namespace MewtocolNet.Responses { + /// + /// 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, bool isBitwise = false) { + + 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"); + } + + isUsedBitwise = isBitwise; + + } + + public override string ToString() { + return $"Adress: {MemoryAdress} Val: {Value}"; + } + } + + + + +} diff --git a/MewtocolNet/Mewtocol/Subregisters/NRegisterResult.cs b/MewtocolNet/Mewtocol/Subregisters/NRegisterResult.cs new file mode 100644 index 0000000..e76f1d1 --- /dev/null +++ b/MewtocolNet/Mewtocol/Subregisters/NRegisterResult.cs @@ -0,0 +1,19 @@ +namespace MewtocolNet.Responses { + /// + /// 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}"; + } + } + + + + +} diff --git a/MewtocolNet/Mewtocol/Subregisters/Register.cs b/MewtocolNet/Mewtocol/Subregisters/Register.cs new file mode 100644 index 0000000..84f996b --- /dev/null +++ b/MewtocolNet/Mewtocol/Subregisters/Register.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections; +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; } + internal bool isUsedBitwise { 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(); + + } + internal void TriggerChangedEvnt(object changed) { + ValueChanged?.Invoke(changed); + } + + internal void TriggerNotifyChange () { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value")); + } + + /// + /// Gets the starting memory are either numeric or A,B,C,D etc for special areas like inputs + /// + /// + public string GetStartingMemoryArea () { + + if (this is BRegister bReg && bReg.SpecialAddress != SpecialAddress.None) { + return bReg.SpecialAddress.ToString(); + } + + return this.MemoryAdress.ToString(); + + } + + /// + /// Gets the current value in the adress as a string + /// + /// + public string GetValueString () { + + if (this is NRegister shortReg) { + return $"{shortReg.Value}{(isUsedBitwise ? $" [{shortReg.GetBitwise().ToBitString()}]" : "")}"; + } + if (this is NRegister ushortReg) { + return ushortReg.Value.ToString(); + } + if (this is NRegister intReg) { + return $"{intReg.Value}{(isUsedBitwise ? $" [{intReg.GetBitwise().ToBitString()}]" : "")}"; + } + if (this is NRegister uintReg) { + return uintReg.Value.ToString(); + } + if (this is NRegister floatReg) { + return floatReg.Value.ToString(); + } + if (this is BRegister boolReg) { + return boolReg.Value.ToString(); + } + if (this is SRegister stringReg) { + return stringReg.Value.ToString(); + + } + + return "Type of the register is not supported."; + + } + + /// + /// Gets the register bitwise if its a 16 or 32 bit int + /// + /// A bitarray + public BitArray GetBitwise () { + + if (this is NRegister shortReg) { + + var bytes = BitConverter.GetBytes(shortReg.Value); + BitArray bitAr = new BitArray(bytes); + return bitAr; + + } + + if (this is NRegister intReg) { + + var bytes = BitConverter.GetBytes(intReg.Value); + BitArray bitAr = new BitArray(bytes); + return bitAr; + + } + + return null; + + } + + public string GetRegisterString () { + + if (this is NRegister shortReg) { + return "DT"; + } + if (this is NRegister ushortReg) { + return "DT"; + } + if (this is NRegister intReg) { + return "DDT"; + } + if (this is NRegister uintReg) { + return "DDT"; + } + if (this is NRegister floatReg) { + return "DDT"; + } + if (this is BRegister boolReg) { + return boolReg.RegType.ToString(); + } + if (this is SRegister stringReg) { + return "DT"; + + } + + return "Type of the register is not supported."; + + } + + public string GetRegisterPLCName () { + + return $"{GetRegisterString()}{MemoryAdress}"; + + } + + } + +} diff --git a/MewtocolNet/Mewtocol/Subregisters/SRegister.cs b/MewtocolNet/Mewtocol/Subregisters/SRegister.cs new file mode 100644 index 0000000..98ca12e --- /dev/null +++ b/MewtocolNet/Mewtocol/Subregisters/SRegister.cs @@ -0,0 +1,58 @@ +using System; +using System.Text; + +namespace MewtocolNet.Responses { + /// + /// 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; + + //calc mem length + var wordsize = (double)_reservedStringSize / 2; + if (wordsize % 2 != 0) { + wordsize++; + } + + MemoryLength = (int)Math.Round(wordsize + 1); + } + + 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(); + } + + + } + +} diff --git a/MewtocolNet/Mewtocol/Subregisters/SRegisterResult.cs b/MewtocolNet/Mewtocol/Subregisters/SRegisterResult.cs new file mode 100644 index 0000000..c7ad14c --- /dev/null +++ b/MewtocolNet/Mewtocol/Subregisters/SRegisterResult.cs @@ -0,0 +1,15 @@ +namespace MewtocolNet.Responses { + 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/MewtocolNet.csproj b/MewtocolNet/MewtocolNet.csproj index fa4db7c..bc8adf4 100644 --- a/MewtocolNet/MewtocolNet.csproj +++ b/MewtocolNet/MewtocolNet.csproj @@ -1,6 +1,6 @@ - + - net5.0 + netstandard2.0 AppLogger 0.1.5 Felix Weiss