From ebee8a062358a5976161382f050a7c34790686a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Fri, 23 Jun 2023 13:51:11 +0200 Subject: [PATCH 01/18] Changed boolean register usage and attributes --- Examples/TestRegisters.cs | 4 +- Examples/TestRegistersEnumBitwise.cs | 2 +- .../{LinkedLists.cs => CodeDescriptions.cs} | 4 +- MewtocolNet/Mewtocol/DynamicInterface.cs | 164 +----------------- MewtocolNet/Mewtocol/MewtocolHelpers.cs | 18 ++ MewtocolNet/Mewtocol/MewtocolInterface.cs | 13 +- .../RegisterAttributes/RegisterAttribute.cs | 30 ++-- MewtocolNet/Mewtocol/RegisterBuilder.cs | 26 +++ MewtocolNet/Mewtocol/RegisterEnums.cs | 44 +++-- .../Mewtocol/Subregisters/BRegister.cs | 89 +++++----- MewtocolNet/MewtocolNet.csproj | 2 +- MewtocolTests/AutomatedPropertyRegisters.cs | 7 +- MewtocolTests/TestLinkedLists.cs | 2 +- MewtocolTests/TestRegisterInterface.cs | 57 +++--- MewtocolTests/TestRegisterParsing.cs | 84 +++++++++ 15 files changed, 251 insertions(+), 295 deletions(-) rename MewtocolNet/Mewtocol/{LinkedLists.cs => CodeDescriptions.cs} (90%) create mode 100644 MewtocolNet/Mewtocol/RegisterBuilder.cs create mode 100644 MewtocolTests/TestRegisterParsing.cs diff --git a/Examples/TestRegisters.cs b/Examples/TestRegisters.cs index dfa77cc..afe76f7 100644 --- a/Examples/TestRegisters.cs +++ b/Examples/TestRegisters.cs @@ -7,7 +7,7 @@ namespace Examples { public class TestRegisters : RegisterCollectionBase { //corresponds to a R100 boolean register in the PLC - [Register(1000, RegisterType.R)] + [Register(IOType.R, 1000)] public bool TestBool1 { get; private set; } private int testDuplicate; @@ -19,7 +19,7 @@ public int TestDuplicate { } //corresponds to a XD input of the PLC - [Register(RegisterType.X, SpecialAddress.D)] + [Register(IOType.X, (byte)0xD)] public bool TestBoolInputXD { get; private set; } //corresponds to a DT1101 - DT1104 string register in the PLC with (STRING[4]) diff --git a/Examples/TestRegistersEnumBitwise.cs b/Examples/TestRegistersEnumBitwise.cs index 2a24825..ea4954c 100644 --- a/Examples/TestRegistersEnumBitwise.cs +++ b/Examples/TestRegistersEnumBitwise.cs @@ -9,7 +9,7 @@ public class TestRegistersEnumBitwise : RegisterCollectionBase { private bool startCyclePLC; - [Register(50, RegisterType.R)] + [Register(IOType.R, 50)] public bool StartCyclePLC { get => startCyclePLC; set => AutoSetter(value, ref startCyclePLC); diff --git a/MewtocolNet/Mewtocol/LinkedLists.cs b/MewtocolNet/Mewtocol/CodeDescriptions.cs similarity index 90% rename from MewtocolNet/Mewtocol/LinkedLists.cs rename to MewtocolNet/Mewtocol/CodeDescriptions.cs index 468a28a..cbe90ef 100644 --- a/MewtocolNet/Mewtocol/LinkedLists.cs +++ b/MewtocolNet/Mewtocol/CodeDescriptions.cs @@ -3,9 +3,9 @@ namespace MewtocolNet.Links { - internal class LinkedData { + internal class CodeDescriptions { - internal static Dictionary ErrorCodes = new Dictionary { + internal static Dictionary Error = new Dictionary { {21, "NACK error"}, {22, "WACK error"}, diff --git a/MewtocolNet/Mewtocol/DynamicInterface.cs b/MewtocolNet/Mewtocol/DynamicInterface.cs index 1fe5054..8bee8e6 100644 --- a/MewtocolNet/Mewtocol/DynamicInterface.cs +++ b/MewtocolNet/Mewtocol/DynamicInterface.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using System.Reflection; using System.Text; @@ -205,167 +206,6 @@ internal void PropertyRegisterWasSet (string propName, object value) { #region Register Adding - - /// - /// Adds a PLC memory register to the watchlist - /// The registers can be read back by attaching - /// - /// The 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 (int _address, RegisterType _type, string _name = null) { - - IRegister toAdd = null; - - //as number registers - if (_type == RegisterType.DT_short) { - toAdd = new NRegister(_address, _name); - } - if (_type == RegisterType.DT_ushort) { - toAdd = new NRegister(_address, _name); - } - if (_type == RegisterType.DDT_int) { - toAdd = new NRegister(_address, _name); - } - if (_type == RegisterType.DDT_uint) { - toAdd = new NRegister(_address, _name); - } - if (_type == RegisterType.DDT_float) { - toAdd = new NRegister(_address, _name); - } - - if(toAdd == null) { - toAdd = new BRegister(_address, _type, _name); - } - - Registers.Add(toAdd); - - } - - internal void AddRegister (Type _colType, int _address, RegisterType _type, string _name = null) { - - IRegister toAdd = null; - - //as number registers - if (_type == RegisterType.DT_short) { - toAdd = new NRegister(_address, _name).WithCollectionType(_colType); - } - if (_type == RegisterType.DT_ushort) { - toAdd = new NRegister(_address, _name).WithCollectionType(_colType); - } - if (_type == RegisterType.DDT_int) { - toAdd = new NRegister(_address, _name).WithCollectionType(_colType); - } - if (_type == RegisterType.DDT_uint) { - toAdd = new NRegister(_address, _name).WithCollectionType(_colType); - } - if (_type == RegisterType.DDT_float) { - toAdd = new NRegister(_address, _name).WithCollectionType(_colType); - } - - if (toAdd == null) { - toAdd = new BRegister(_address, _type, _name).WithCollectionType(_colType); - } - - Registers.Add(toAdd); - - } - - /// - /// 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(new BRegister(_spAddress, _type, _name)); - - } - - internal void AddRegister (Type _colType, SpecialAddress _spAddress, RegisterType _type, string _name = null) { - - var reg = new BRegister(_spAddress, _type, _name); - - reg.collectionType = _colType; - - //as bool registers - Registers.Add(reg); - - } - - /// - /// 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 - /// - /// 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(int _address, int _length = 1, string _name = null) { - - Type regType = typeof(T); - - if (regType != typeof(string) && _length != 1) { - throw new NotSupportedException($"_lenght parameter only allowed for register of type string"); - } - - IRegister toAdd; - - if (regType == typeof(short)) { - toAdd = new NRegister(_address, _name); - } else if (regType == typeof(ushort)) { - toAdd = new NRegister(_address, _name); - } else if (regType == typeof(int)) { - toAdd = new NRegister(_address, _name); - } else if (regType == typeof(uint)) { - toAdd = new NRegister(_address, _name); - } else if (regType == typeof(float)) { - toAdd = new NRegister(_address, _name); - } else if (regType == typeof(string)) { - toAdd = new SRegister(_address, _length, _name); - } else if (regType == typeof(TimeSpan)) { - toAdd = new NRegister(_address, _name); - } else if (regType == typeof(bool)) { - toAdd = 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"); - } - - - if (Registers.Any(x => x.GetRegisterPLCName() == toAdd.GetRegisterPLCName())) { - throw new NotSupportedException($"Cannot add a register multiple times, " + - $"make sure that all register attributes or AddRegister assignments have different adresses."); - } - - } - //Internal register adding for auto register collection building internal void AddRegister (Type _colType, int _address, PropertyInfo boundProp, int _length = 1, bool _isBitwise = false, Type _enumType = null) { @@ -405,7 +245,7 @@ internal void AddRegister (Type _colType, int _address, PropertyInfo boundPro } else if (regType == typeof(TimeSpan)) { reg = new NRegister(_address, propName).WithCollectionType(_colType); } else if (regType == typeof(bool)) { - reg = new BRegister(_address, RegisterType.R, propName).WithCollectionType(_colType); + reg = new BRegister(IOType.R, 0x0, _address,propName).WithCollectionType(_colType); } if (reg == null) { diff --git a/MewtocolNet/Mewtocol/MewtocolHelpers.cs b/MewtocolNet/Mewtocol/MewtocolHelpers.cs index 1b82ccc..715a869 100644 --- a/MewtocolNet/Mewtocol/MewtocolHelpers.cs +++ b/MewtocolNet/Mewtocol/MewtocolHelpers.cs @@ -239,6 +239,24 @@ internal static bool IsNumericSupportedType (this Type type) { } + /// + /// Checks if the register type is non numeric + /// + internal static bool IsBoolean (this RegisterType type) { + + return type == RegisterType.X || type == RegisterType.Y || type == RegisterType.R; + + } + + /// + /// Checks if the register type is an physical in or output of the plc + /// + internal static bool IsPhysicalInOutType (this RegisterType type) { + + return type == RegisterType.X || type == RegisterType.Y; + + } + } } \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/MewtocolInterface.cs b/MewtocolNet/Mewtocol/MewtocolInterface.cs index 9062077..009f7fe 100644 --- a/MewtocolNet/Mewtocol/MewtocolInterface.cs +++ b/MewtocolNet/Mewtocol/MewtocolInterface.cs @@ -17,6 +17,8 @@ using MewtocolNet.Queue; using System.Reflection; using System.Timers; +using System.Data; +using System.Xml.Linq; namespace MewtocolNet { @@ -437,11 +439,10 @@ public MewtocolInterface WithRegisterCollection (RegisterCollectionBase collecti if (attr is RegisterAttribute cAttribute) { if (prop.PropertyType == typeof(bool) && cAttribute.AssignedBitIndex == -1) { - if (cAttribute.SpecialAddress == SpecialAddress.None) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, cAttribute.RegisterType, _name: propName); - } else { - AddRegister(collection.GetType(), cAttribute.SpecialAddress, cAttribute.RegisterType, _name: propName); - } + + //add bool register non bit assgined + Registers.Add(new BRegister((IOType)(int)cAttribute.RegisterType, cAttribute.SpecialAddress, cAttribute.MemoryArea, _name: propName).WithCollectionType(collection.GetType())); + } if (prop.PropertyType == typeof(short)) { @@ -738,7 +739,7 @@ public async Task SendCommandAsync (string _msg) { Match m = errorcheck.Match(response.ToString()); if (m.Success) { string eCode = m.Groups[1].Value; - string eDes = Links.LinkedData.ErrorCodes[Convert.ToInt32(eCode)]; + string eDes = Links.CodeDescriptions.Error[Convert.ToInt32(eCode)]; Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"Response is: {response}"); Logger.Log($"Error on command {_msg.Replace("\r", "")} the PLC returned error code: {eCode}, {eDes}", LogLevel.Error); diff --git a/MewtocolNet/Mewtocol/RegisterAttributes/RegisterAttribute.cs b/MewtocolNet/Mewtocol/RegisterAttributes/RegisterAttribute.cs index 382c574..8e81f21 100644 --- a/MewtocolNet/Mewtocol/RegisterAttributes/RegisterAttribute.cs +++ b/MewtocolNet/Mewtocol/RegisterAttributes/RegisterAttribute.cs @@ -23,17 +23,16 @@ public enum BitCount { /// /// Defines the behavior of a register property /// - [AttributeUsage(AttributeTargets.Property)] + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class RegisterAttribute : Attribute { internal int MemoryArea; internal int StringLength; internal RegisterType RegisterType; - internal SpecialAddress SpecialAddress = SpecialAddress.None; + internal byte SpecialAddress = 0x0; internal BitCount BitCount; internal int AssignedBitIndex = -1; - /// /// Attribute for string type or numeric registers /// @@ -49,31 +48,22 @@ public RegisterAttribute (int memoryArea, int stringLength = 1) { /// /// Attribute for boolean registers /// - /// The area in the plcs memory - /// The type of boolean register - public RegisterAttribute (int memoryArea, RegisterType type) { + public RegisterAttribute(IOType type, byte spAdress = 0x0) { - 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; + MemoryArea = 0; + RegisterType = (RegisterType)(int)type; + SpecialAddress = spAdress; } /// /// Attribute for boolean registers /// - /// The special area in the plcs memory - /// The type of boolean register - 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"); + public RegisterAttribute (IOType type, int memoryArea, byte spAdress = 0x0) { - RegisterType = type; - SpecialAddress = spAdress; + MemoryArea = memoryArea; + RegisterType = (RegisterType)(int)type; + SpecialAddress = spAdress; } diff --git a/MewtocolNet/Mewtocol/RegisterBuilder.cs b/MewtocolNet/Mewtocol/RegisterBuilder.cs new file mode 100644 index 0000000..7278ac4 --- /dev/null +++ b/MewtocolNet/Mewtocol/RegisterBuilder.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.Mewtocol { + + /// + /// Contains useful tools for register creation + /// + public static class RegisterBuilder { + + /// + /// Parses a register from its PLC name + /// + /// The name, fe. DT100 + /// An or null if + /// True if successfully parsed + //public static bool TryBuildFromName (string name, out IRegister reg) { + + + + //} + + } + +} diff --git a/MewtocolNet/Mewtocol/RegisterEnums.cs b/MewtocolNet/Mewtocol/RegisterEnums.cs index 66f7582..d6559c4 100644 --- a/MewtocolNet/Mewtocol/RegisterEnums.cs +++ b/MewtocolNet/Mewtocol/RegisterEnums.cs @@ -7,64 +7,62 @@ namespace MewtocolNet { /// - /// The special register type + /// The register prefixed type /// public enum RegisterType { /// /// Physical input as a bool (Relay) /// - X, + X = 0, /// /// Physical output as a bool (Relay) /// - Y, + Y = 1, /// /// Internal as a bool (Relay) /// - R, + R = 2, /// /// Data area as a short (Register) /// - DT_short, + DT_short = 3, /// /// Data area as an unsigned short (Register) /// - DT_ushort, + DT_ushort = 4, /// /// Double data area as an integer (Register) /// - DDT_int, + DDT_int = 5, /// /// Double data area as an unsigned integer (Register) /// - DDT_uint, + DDT_uint = 6, /// /// Double data area as an floating point number (Register) /// - DDT_float, + DDT_float = 7, } /// - /// The special input / output channel address + /// The type of an input/output register /// - public enum SpecialAddress { - - #pragma warning disable CS1591 + public enum IOType { /// - /// No defined + /// Physical input as a bool (Relay) /// - None, - A = -10, - B = -11, - C = -12, - D = -13, - E = -14, - F = -15, - - #pragma warning restore + X = 0, + /// + /// Physical output as a bool (Relay) + /// + Y = 1, + /// + /// Internal relay + /// + R = 2, } diff --git a/MewtocolNet/Mewtocol/Subregisters/BRegister.cs b/MewtocolNet/Mewtocol/Subregisters/BRegister.cs index 92cb068..c49d13e 100644 --- a/MewtocolNet/Mewtocol/Subregisters/BRegister.cs +++ b/MewtocolNet/Mewtocol/Subregisters/BRegister.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Data; using System.Text; using MewtocolNet; @@ -22,8 +23,6 @@ public class BRegister : IRegister, INotifyPropertyChanged { internal RegisterType RegType { get; private set; } - internal SpecialAddress SpecialAddress { get; private set; } - internal Type collectionType; /// @@ -44,50 +43,46 @@ public class BRegister : IRegister, INotifyPropertyChanged { /// public string Name => name; - internal int memoryAdress; + internal int memoryAddress; /// /// The registers memory adress if not a special register /// - public int MemoryAddress => memoryAdress; + public int MemoryAddress => memoryAddress; + internal byte specialAddress; /// - /// Defines a register containing a number + /// The registers memory adress if not a special register /// - /// 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 addresses cant be greater than 99999"); - - if (_type != RegisterType.X && _type != RegisterType.Y && _type != RegisterType.R) - throw new NotSupportedException("The register type cant be numeric, use X, Y or R"); - - memoryAdress = _address; - name = _name; - - RegType = _type; - - } + public byte SpecialAddress => specialAddress; /// - /// Defines a register containing a number + /// Creates a new boolean register /// - /// Memory start adress max 99999 - /// Type of boolean register - /// Name of the register - public BRegister (SpecialAddress _address, RegisterType _type = RegisterType.R, string _name = null) { + /// The io type prefix + /// The special address + /// The area special address + /// The custom name + /// + /// + public BRegister (IOType _io, byte _spAddress = 0x0, int _areaAdress = 0, string _name = null) { - if (_address == SpecialAddress.None) - throw new NotSupportedException("Special address cant be none"); + if (_areaAdress < 0) + throw new NotSupportedException("The area address cant be negative"); - if (_type != RegisterType.X && _type != RegisterType.Y && _type != RegisterType.R) - throw new NotSupportedException("The register type cant be numeric, use X, Y or R"); + if(_io == IOType.R && _areaAdress >= 512) + throw new NotSupportedException("R area addresses cant be greater than 511"); - SpecialAddress = _address; + if ((_io == IOType.X || _io == IOType.Y) && _areaAdress >= 110) + throw new NotSupportedException("XY area addresses cant be greater than 110"); + + if (_spAddress > 0xF) + throw new NotSupportedException("Special address cant be greater 15 or 0xF"); + + memoryAddress = (int)_areaAdress; + specialAddress = _spAddress; name = _name; - RegType = _type; + RegType = (RegisterType)(int)_io; } @@ -105,12 +100,13 @@ public string BuildMewtocolQuery () { //build area code from register type StringBuilder asciistring = new StringBuilder(RegType.ToString()); - if(SpecialAddress == SpecialAddress.None) { - asciistring.Append(MemoryAddress.ToString().PadLeft(4, '0')); - } else { - asciistring.Append(SpecialAddress.ToString().PadLeft(4, '0')); - } - + + string memPadded = MemoryAddress.ToString().PadLeft(4, '0'); + string sp = SpecialAddress.ToString("X1"); + + asciistring.Append(memPadded); + asciistring.Append(sp); + return asciistring.ToString(); } @@ -125,9 +121,6 @@ internal void SetValueFromPLC (bool val) { public string GetStartingMemoryArea() { - if (SpecialAddress != SpecialAddress.None) - return SpecialAddress.ToString(); - return MemoryAddress.ToString(); } @@ -148,8 +141,18 @@ public string GetStartingMemoryArea() { public string GetRegisterPLCName() { - if (SpecialAddress != SpecialAddress.None) { - return $"{GetRegisterString()}{SpecialAddress}"; + var spAdressEnd = SpecialAddress.ToString("X1"); + + if (MemoryAddress == 0) { + + return $"{GetRegisterString()}{spAdressEnd}"; + + } + + if(MemoryAddress > 0 && SpecialAddress != 0) { + + return $"{GetRegisterString()}{MemoryAddress}{spAdressEnd}"; + } return $"{GetRegisterString()}{MemoryAddress}"; diff --git a/MewtocolNet/MewtocolNet.csproj b/MewtocolNet/MewtocolNet.csproj index 5f420a3..c296329 100644 --- a/MewtocolNet/MewtocolNet.csproj +++ b/MewtocolNet/MewtocolNet.csproj @@ -2,7 +2,7 @@ netstandard2.0 Mewtocol.NET - 0.7.0 + 0.7.1 Felix Weiss Womed true diff --git a/MewtocolTests/AutomatedPropertyRegisters.cs b/MewtocolTests/AutomatedPropertyRegisters.cs index 30e902d..834a39c 100644 --- a/MewtocolTests/AutomatedPropertyRegisters.cs +++ b/MewtocolTests/AutomatedPropertyRegisters.cs @@ -21,11 +21,12 @@ public AutomatedPropertyRegisters(ITestOutputHelper output) { public class TestRegisterCollection : RegisterCollectionBase { //corresponds to a R100 boolean register in the PLC - [Register(1000, RegisterType.R)] + //can also be written as R1000 because the last one is a special address + [Register(IOType.R, 100, spAdress: 0)] public bool TestBool1 { get; private set; } //corresponds to a XD input of the PLC - [Register(RegisterType.X, SpecialAddress.D)] + [Register(IOType.X, (byte)0xD)] public bool TestBoolInputXD { get; private set; } //corresponds to a DT1101 - DT1104 string register in the PLC with (STRING[4]) @@ -117,7 +118,7 @@ public void BooleanGen () { var register = interf.GetRegister(nameof(TestRegisterCollection.TestBool1)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestBool1), false, 1000, "R1000"); + TestBasicGeneration(register, nameof(TestRegisterCollection.TestBool1), false, 100, "R100"); } diff --git a/MewtocolTests/TestLinkedLists.cs b/MewtocolTests/TestLinkedLists.cs index cef48f8..4f47f03 100644 --- a/MewtocolTests/TestLinkedLists.cs +++ b/MewtocolTests/TestLinkedLists.cs @@ -53,7 +53,7 @@ public void NumericRegisterMewtocolIdentifiers () { }; - Assert.Equal(expectedData, LinkedData.ErrorCodes); + Assert.Equal(expectedData, CodeDescriptions.Error); } diff --git a/MewtocolTests/TestRegisterInterface.cs b/MewtocolTests/TestRegisterInterface.cs index f3acba8..d66daba 100644 --- a/MewtocolTests/TestRegisterInterface.cs +++ b/MewtocolTests/TestRegisterInterface.cs @@ -26,7 +26,7 @@ public void NumericRegisterMewtocolIdentifiers () { new NRegister(50, _name : null), }; - List expcectedIdents = new List { + List expectedIdents = new List { "D0005000050", //single word register "D0005000050", //single word register "D0005000051", //double word register @@ -39,7 +39,7 @@ public void NumericRegisterMewtocolIdentifiers () { for (int i = 0; i < registers.Count; i++) { IRegister? reg = registers[i]; - string expect = expcectedIdents[i]; + string expect = expectedIdents[i]; Assert.Equal(expect, reg.BuildMewtocolQuery()); @@ -58,25 +58,38 @@ public void PLCRegisterIdentifiers () { new NRegister(80, _name : null), new NRegister(90, _name : null), new NRegister(100, _name : null), + //boolean - new BRegister(100), - new BRegister(5, RegisterType.X), - new BRegister(SpecialAddress.A, RegisterType.X), + new BRegister(IOType.R, 0, 100), + new BRegister(IOType.X, 5), + new BRegister(IOType.X, 0xA), + new BRegister(IOType.X, 0xF, 109), + new BRegister(IOType.Y, 0xC, 75), + //string new SRegister(999, 5), }; List expcectedIdents = new List { + + //numeric ones "DT50", "DT60", "DDT70", "DDT80", "DDT90", "DDT100", + + //boolean "R100", "X5", "XA", + "X109F", + "Y75C", + + //string "DT999" + }; //test mewtocol idents @@ -104,7 +117,7 @@ public void OverFlowRegisterAddress () { var ex1 = Assert.Throws(() => { - new BRegister(100000); + new BRegister(IOType.R, _areaAdress: 512); }); @@ -112,46 +125,28 @@ public void OverFlowRegisterAddress () { var ex2 = Assert.Throws(() => { - new SRegister(100000, 5); + new BRegister(IOType.X, _areaAdress: 110); }); output.WriteLine(ex2.Message.ToString()); - } - - [Fact(DisplayName = "Non allowed (Wrong data type)")] - public void WrongDataTypeRegister () { - - var ex = Assert.Throws(() => { + var ex3 = Assert.Throws(() => { - new NRegister(100, _name: null); - - }); - - output.WriteLine(ex.Message.ToString()); - - } - - [Fact(DisplayName = "Non allowed (Wrong bool type address)")] - public void WrongDataTypeRegisterBool1 () { - - var ex = Assert.Throws(() => { - - new BRegister(100, RegisterType.DDT_int); + new SRegister(100000, 5); }); - output.WriteLine(ex.Message.ToString()); + output.WriteLine(ex3.Message.ToString()); } - [Fact(DisplayName = "Non allowed (Wrong bool special address)")] - public void WrongDataTypeRegisterBool2 () { + [Fact(DisplayName = "Non allowed (Wrong data type)")] + public void WrongDataTypeRegister () { var ex = Assert.Throws(() => { - new BRegister(SpecialAddress.None, RegisterType.X); + new NRegister(100, _name: null); }); diff --git a/MewtocolTests/TestRegisterParsing.cs b/MewtocolTests/TestRegisterParsing.cs new file mode 100644 index 0000000..94953fd --- /dev/null +++ b/MewtocolTests/TestRegisterParsing.cs @@ -0,0 +1,84 @@ +using MewtocolNet; +using MewtocolNet.Mewtocol; +using MewtocolNet.Registers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace MewtocolTests; + +public class TestRegisterParsing { + + private readonly ITestOutputHelper output; + + public TestRegisterParsing (ITestOutputHelper output) { + this.output = output; + } + + [Fact(DisplayName = "Parsing as BRegister (Phyiscal Outputs)")] + public void TestParsingBRegisterY () { + + var tests = new Dictionary() { + + {"Y0", new BRegister(IOType.Y)}, + {"Y1", new BRegister(IOType.Y, 0x1)}, + {"Y2", new BRegister(IOType.Y, 0x2)}, + {"Y3", new BRegister(IOType.Y, 0x3)}, + {"Y4", new BRegister(IOType.Y, 0x4)}, + {"Y5", new BRegister(IOType.Y, 0x5)}, + {"Y6", new BRegister(IOType.Y, 0x6)}, + {"Y7", new BRegister(IOType.Y, 0x7)}, + {"Y8", new BRegister(IOType.Y, 0x8)}, + {"Y9", new BRegister(IOType.Y, 0x9)}, + + {"YA", new BRegister(IOType.Y, 0xA)}, + {"YB", new BRegister(IOType.Y, 0xB)}, + {"YC", new BRegister(IOType.Y, 0xC)}, + {"YD", new BRegister(IOType.Y, 0xD)}, + {"YE", new BRegister(IOType.Y, 0xE)}, + {"YF", new BRegister(IOType.Y, 0xF)}, + + {"Y1A", new BRegister(IOType.Y, 0xA, 1)}, + {"Y10B", new BRegister(IOType.Y, 0xB, 10)}, + {"Y109C", new BRegister(IOType.Y, 0xC, 109)}, + + }; + + } + + [Fact(DisplayName = "Parsing as BRegister (Phyiscal Inputs)")] + public void TestParsingBRegisterX () { + + var tests = new Dictionary() { + + {"X0", new BRegister(IOType.X)}, + {"X1", new BRegister(IOType.X, 0x1)}, + {"X2", new BRegister(IOType.X, 0x2)}, + {"X3", new BRegister(IOType.X, 0x3)}, + {"X4", new BRegister(IOType.X, 0x4)}, + {"X5", new BRegister(IOType.X, 0x5)}, + {"X6", new BRegister(IOType.X, 0x6)}, + {"X7", new BRegister(IOType.X, 0x7)}, + {"X8", new BRegister(IOType.X, 0x8)}, + {"X9", new BRegister(IOType.X, 0x9)}, + + {"XA", new BRegister(IOType.X, 0xA)}, + {"XB", new BRegister(IOType.X, 0xB)}, + {"XC", new BRegister(IOType.X, 0xC)}, + {"XD", new BRegister(IOType.X, 0xD)}, + {"XE", new BRegister(IOType.X, 0xE)}, + {"XF", new BRegister(IOType.X, 0xF)}, + + {"X1A", new BRegister(IOType.X, 0xA, 1)}, + {"X10B", new BRegister(IOType.X, 0xB, 10)}, + {"X109C", new BRegister(IOType.X, 0xC, 109)}, + + }; + + } + +} From 88ad17514552f1cf33cda63d18d45cacdb0c677b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Fri, 23 Jun 2023 15:40:34 +0200 Subject: [PATCH 02/18] Restructured files - householding --- .../{Mewtocol => }/CodeDescriptions.cs | 4 +- MewtocolNet/{Mewtocol => }/CpuInfo.cs | 9 +- .../{Mewtocol => }/DynamicInterface.cs | 53 ++-- MewtocolNet/{Mewtocol => }/IRegister.cs | 3 - MewtocolNet/{Mewtocol => }/Logging/Logger.cs | 8 +- .../{Mewtocol => }/Logging/LoggerEnums.cs | 6 +- MewtocolNet/{Mewtocol => }/MewtocolHelpers.cs | 59 +++-- .../{Mewtocol => }/MewtocolInterface.cs | 123 +++++----- .../MewtocolInterfaceRequests.cs | 70 +++--- .../{Mewtocol => }/PLCEnums/CpuType.cs | 2 +- MewtocolNet/{Mewtocol => }/PLCEnums/OPMode.cs | 6 +- MewtocolNet/{Mewtocol => }/PLCInfo.cs | 12 +- MewtocolNet/{Mewtocol => }/PLCMode.cs | 5 +- MewtocolNet/Queue/SerialQueue.cs | 2 +- MewtocolNet/RegisterAttributes/BitCount.cs | 16 ++ .../RegisterAttributes/RegisterAttribute.cs | 32 +-- .../RegisterCollectionBase.cs | 24 +- .../RegisterBuilder.cs | 14 +- MewtocolNet/{Mewtocol => }/RegisterEnums.cs | 8 +- MewtocolNet/{Mewtocol => }/Responses.cs | 16 +- .../{Mewtocol => }/Subregisters/BRegister.cs | 20 +- .../Subregisters/BRegisterResult.cs | 2 +- .../{Mewtocol => }/Subregisters/NRegister.cs | 27 +-- .../Subregisters/NRegisterResult.cs | 12 +- .../{Mewtocol => }/Subregisters/SRegister.cs | 12 +- .../Subregisters/SRegisterResult.cs | 2 +- MewtocolTests/AutomatedPropertyRegisters.cs | 5 +- MewtocolTests/TestLinkedLists.cs | 4 +- MewtocolTests/TestLivePLC.cs | 4 +- MewtocolTests/TestRegisterInterface.cs | 2 + formatting_settings | 229 ++++++++++++++++++ 31 files changed, 485 insertions(+), 306 deletions(-) rename MewtocolNet/{Mewtocol => }/CodeDescriptions.cs (96%) rename MewtocolNet/{Mewtocol => }/CpuInfo.cs (90%) rename MewtocolNet/{Mewtocol => }/DynamicInterface.cs (88%) rename MewtocolNet/{Mewtocol => }/IRegister.cs (97%) rename MewtocolNet/{Mewtocol => }/Logging/Logger.cs (74%) rename MewtocolNet/{Mewtocol => }/Logging/LoggerEnums.cs (86%) rename MewtocolNet/{Mewtocol => }/MewtocolHelpers.cs (78%) rename MewtocolNet/{Mewtocol => }/MewtocolInterface.cs (91%) rename MewtocolNet/{Mewtocol => }/MewtocolInterfaceRequests.cs (90%) rename MewtocolNet/{Mewtocol => }/PLCEnums/CpuType.cs (96%) rename MewtocolNet/{Mewtocol => }/PLCEnums/OPMode.cs (74%) rename MewtocolNet/{Mewtocol => }/PLCInfo.cs (80%) rename MewtocolNet/{Mewtocol => }/PLCMode.cs (96%) create mode 100644 MewtocolNet/RegisterAttributes/BitCount.cs rename MewtocolNet/{Mewtocol => }/RegisterAttributes/RegisterAttribute.cs (75%) rename MewtocolNet/{Mewtocol => }/RegisterAttributes/RegisterCollectionBase.cs (69%) rename MewtocolNet/{Mewtocol => RegisterBuilding}/RegisterBuilder.cs (67%) rename MewtocolNet/{Mewtocol => }/RegisterEnums.cs (90%) rename MewtocolNet/{Mewtocol => }/Responses.cs (57%) rename MewtocolNet/{Mewtocol => }/Subregisters/BRegister.cs (90%) rename MewtocolNet/{Mewtocol => }/Subregisters/BRegisterResult.cs (90%) rename MewtocolNet/{Mewtocol => }/Subregisters/NRegister.cs (93%) rename MewtocolNet/{Mewtocol => }/Subregisters/NRegisterResult.cs (79%) rename MewtocolNet/{Mewtocol => }/Subregisters/SRegister.cs (92%) rename MewtocolNet/{Mewtocol => }/Subregisters/SRegisterResult.cs (90%) create mode 100644 formatting_settings diff --git a/MewtocolNet/Mewtocol/CodeDescriptions.cs b/MewtocolNet/CodeDescriptions.cs similarity index 96% rename from MewtocolNet/Mewtocol/CodeDescriptions.cs rename to MewtocolNet/CodeDescriptions.cs index cbe90ef..9407856 100644 --- a/MewtocolNet/Mewtocol/CodeDescriptions.cs +++ b/MewtocolNet/CodeDescriptions.cs @@ -1,7 +1,6 @@ -using System; using System.Collections.Generic; -namespace MewtocolNet.Links { +namespace MewtocolNet { internal class CodeDescriptions { @@ -40,7 +39,6 @@ internal class CodeDescriptions { }; - } } \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/CpuInfo.cs b/MewtocolNet/CpuInfo.cs similarity index 90% rename from MewtocolNet/Mewtocol/CpuInfo.cs rename to MewtocolNet/CpuInfo.cs index 3fa008b..4f21723 100644 --- a/MewtocolNet/Mewtocol/CpuInfo.cs +++ b/MewtocolNet/CpuInfo.cs @@ -1,6 +1,7 @@ -using System; +using MewtocolNet.PLCEnums; +using System; -namespace MewtocolNet.Registers { +namespace MewtocolNet { /// /// Contains information about the plc and its cpu @@ -22,7 +23,7 @@ public partial class CpuInfo { /// public string CpuVersion { get; set; } - internal static CpuInfo BuildFromHexString (string _cpuType, string _cpuVersion, string _progCapacity) { + internal static CpuInfo BuildFromHexString(string _cpuType, string _cpuVersion, string _progCapacity) { CpuInfo retInf = new CpuInfo(); @@ -61,7 +62,7 @@ internal static CpuInfo BuildFromHexString (string _cpuType, string _cpuVersion, return retInf; } - + } } \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/DynamicInterface.cs b/MewtocolNet/DynamicInterface.cs similarity index 88% rename from MewtocolNet/Mewtocol/DynamicInterface.cs rename to MewtocolNet/DynamicInterface.cs index 8bee8e6..f8726b0 100644 --- a/MewtocolNet/Mewtocol/DynamicInterface.cs +++ b/MewtocolNet/DynamicInterface.cs @@ -1,14 +1,10 @@ -using System; +using MewtocolNet.Logging; +using MewtocolNet.Subregisters; +using System; using System.Collections.Generic; -using System.Data; using System.Linq; using System.Reflection; -using System.Text; -using System.Threading; using System.Threading.Tasks; -using MewtocolNet.Logging; -using MewtocolNet.RegisterAttributes; -using MewtocolNet.Registers; namespace MewtocolNet { @@ -41,7 +37,7 @@ public partial class MewtocolInterface { /// /// Kills the poller completely /// - internal void KillPoller () { + internal void KillPoller() { pollerTaskRunning = false; pollerTaskStopped = true; @@ -54,7 +50,7 @@ internal void KillPoller () { /// Pauses the polling and waits for the last message to be sent /// /// - public async Task PausePollingAsync () { + public async Task PausePollingAsync() { if (!pollerTaskRunning) return; @@ -65,9 +61,9 @@ public async Task PausePollingAsync () { if (pollerIsPaused) break; - + await Task.Delay(10); - + } pollerTaskRunning = false; @@ -77,7 +73,7 @@ public async Task PausePollingAsync () { /// /// Resumes the polling /// - public void ResumePolling () { + public void ResumePolling() { pollerTaskRunning = true; @@ -86,7 +82,7 @@ public void ResumePolling () { /// /// Attaches a continous reader that reads back the Registers and Contacts /// - internal void AttachPoller () { + internal void AttachPoller() { if (pollerTaskRunning) return; @@ -196,9 +192,9 @@ internal void AttachPoller () { } - internal void PropertyRegisterWasSet (string propName, object value) { + internal void PropertyRegisterWasSet(string propName, object value) { - SetRegister(propName, value); + SetRegister(propName, value); } @@ -207,7 +203,7 @@ internal void PropertyRegisterWasSet (string propName, object value) { #region Register Adding //Internal register adding for auto register collection building - internal void AddRegister (Type _colType, int _address, PropertyInfo boundProp, int _length = 1, bool _isBitwise = false, Type _enumType = null) { + internal void AddRegister(Type _colType, int _address, PropertyInfo boundProp, int _length = 1, bool _isBitwise = false, Type _enumType = null) { Type regType = typeof(T); @@ -224,7 +220,7 @@ internal void AddRegister (Type _colType, int _address, PropertyInfo boundPro string propName = boundProp.Name; //rename the property name to prevent duplicate names in case of a bitwise prop - if(_isBitwise && regType == typeof(short)) + if (_isBitwise && regType == typeof(short)) propName = $"Auto_Bitwise_DT{_address}"; if (_isBitwise && regType == typeof(int)) @@ -245,7 +241,7 @@ internal void AddRegister (Type _colType, int _address, PropertyInfo boundPro } else if (regType == typeof(TimeSpan)) { reg = new NRegister(_address, propName).WithCollectionType(_colType); } else if (regType == typeof(bool)) { - reg = new BRegister(IOType.R, 0x0, _address,propName).WithCollectionType(_colType); + reg = new BRegister(IOType.R, 0x0, _address, propName).WithCollectionType(_colType); } if (reg == null) { @@ -270,7 +266,7 @@ internal void AddRegister (Type _colType, int _address, PropertyInfo boundPro /// Gets a register that was added by its name /// /// - public IRegister GetRegister (string name) { + public IRegister GetRegister(string name) { return Registers.FirstOrDefault(x => x.Name == name); @@ -281,16 +277,16 @@ public IRegister GetRegister (string name) { /// /// The type of register /// A casted register or the default value - public T GetRegister (string name) where T : IRegister { + public T GetRegister(string name) where T : IRegister { try { - + var reg = Registers.FirstOrDefault(x => x.Name == name); return (T)reg; } catch (InvalidCastException) { - + return default(T); - + } } @@ -302,7 +298,7 @@ public T GetRegister (string name) where T : IRegister { /// /// Gets a list of all added registers /// - public List GetAllRegisters () { + public List GetAllRegisters() { return Registers; @@ -312,19 +308,20 @@ public List GetAllRegisters () { #region Event Invoking - internal void InvokeRegisterChanged (IRegister reg) { + internal void InvokeRegisterChanged(IRegister reg) { - RegisterChanged?.Invoke(reg); + RegisterChanged?.Invoke(reg); } - internal void InvokePolledCycleDone () { + internal void InvokePolledCycleDone() { - PolledCycle?.Invoke(); + PolledCycle?.Invoke(); } #endregion } + } diff --git a/MewtocolNet/Mewtocol/IRegister.cs b/MewtocolNet/IRegister.cs similarity index 97% rename from MewtocolNet/Mewtocol/IRegister.cs rename to MewtocolNet/IRegister.cs index 524bf15..c771084 100644 --- a/MewtocolNet/Mewtocol/IRegister.cs +++ b/MewtocolNet/IRegister.cs @@ -1,7 +1,4 @@ using System; -using System.Collections; -using System.Collections.Generic; -using System.Text; namespace MewtocolNet { diff --git a/MewtocolNet/Mewtocol/Logging/Logger.cs b/MewtocolNet/Logging/Logger.cs similarity index 74% rename from MewtocolNet/Mewtocol/Logging/Logger.cs rename to MewtocolNet/Logging/Logger.cs index ee4d311..d9d68ae 100644 --- a/MewtocolNet/Mewtocol/Logging/Logger.cs +++ b/MewtocolNet/Logging/Logger.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace MewtocolNet.Logging { @@ -12,14 +10,14 @@ public static class Logger { /// /// Sets the loglevel for the logger module /// - public static LogLevel LogLevel { get; set; } + 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) { + public static void OnNewLogMessage(Action onMsg) { LogInvoked += (t, m) => { onMsg(t, m); @@ -27,7 +25,7 @@ public static void OnNewLogMessage (Action onMsg) { } - internal static void Log (string message, LogLevel loglevel, MewtocolInterface sender = null) { + internal static void Log(string message, LogLevel loglevel, MewtocolInterface sender = null) { if ((int)loglevel <= (int)LogLevel) { if (sender == null) { diff --git a/MewtocolNet/Mewtocol/Logging/LoggerEnums.cs b/MewtocolNet/Logging/LoggerEnums.cs similarity index 86% rename from MewtocolNet/Mewtocol/Logging/LoggerEnums.cs rename to MewtocolNet/Logging/LoggerEnums.cs index 385bdae..a845218 100644 --- a/MewtocolNet/Mewtocol/Logging/LoggerEnums.cs +++ b/MewtocolNet/Logging/LoggerEnums.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace MewtocolNet.Logging { +namespace MewtocolNet.Logging { /// /// The loglevel of the logging module diff --git a/MewtocolNet/Mewtocol/MewtocolHelpers.cs b/MewtocolNet/MewtocolHelpers.cs similarity index 78% rename from MewtocolNet/Mewtocol/MewtocolHelpers.cs rename to MewtocolNet/MewtocolHelpers.cs index 715a869..e3f0d2f 100644 --- a/MewtocolNet/Mewtocol/MewtocolHelpers.cs +++ b/MewtocolNet/MewtocolHelpers.cs @@ -1,10 +1,9 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; -using System.Collections.Generic; -using MewtocolNet.Registers; -using System.Collections; namespace MewtocolNet { @@ -16,7 +15,7 @@ public static class MewtocolHelpers { /// /// Turns a bit array into a 0 and 1 string /// - public static string ToBitString (this BitArray arr) { + public static string ToBitString(this BitArray arr) { var bits = new bool[arr.Length]; arr.CopyTo(bits, 0); @@ -27,29 +26,29 @@ public static string ToBitString (this BitArray arr) { /// /// Converts a string (after converting to upper case) to ascii bytes /// - internal static byte[] ToHexASCIIBytes (this string _str) { + internal static byte[] ToHexASCIIBytes(this string _str) { ASCIIEncoding ascii = new ASCIIEncoding(); - byte[] bytes = ascii.GetBytes(_str.ToUpper()); + byte[] bytes = ascii.GetBytes(_str.ToUpper()); return bytes; } - internal 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; - for(int i = 0; i < b.Length; i++) - xorTotalByte ^= b[i]; + for (int i = 0; i < b.Length; i++) + xorTotalByte ^= b[i]; return asciiArr.Insert(asciiArr.Length, xorTotalByte.ToString("X2")); - + } /// /// Parses the byte string from a incoming RD message /// - internal static string ParseDTByteString (this string _onString, int _blockSize = 4) { + internal static string ParseDTByteString(this string _onString, int _blockSize = 4) { if (_onString == null) return null; @@ -63,7 +62,7 @@ internal static string ParseDTByteString (this string _onString, int _blockSize } - internal static bool? ParseRCSingleBit (this string _onString) { + internal static bool? ParseRCSingleBit(this string _onString) { var res = new Regex(@"\%([0-9]{2})\$RC(.)").Match(_onString); if (res.Success) { @@ -74,10 +73,10 @@ internal static string ParseDTByteString (this string _onString, int _blockSize } - internal static string ParseDTString (this string _onString) { + internal static string ParseDTString(this string _onString) { var res = new Regex(@"\%([0-9]{2})\$RD.{8}(.*)...").Match(_onString); - if(res.Success) { + if (res.Success) { string val = res.Groups[2].Value; return val.GetStringFromAsciiHex()?.Trim(); } @@ -85,9 +84,9 @@ internal static string ParseDTString (this string _onString) { } - internal static string ReverseByteOrder (this string _onString) { + internal static string ReverseByteOrder(this string _onString) { - if(_onString == null) return null; + if (_onString == null) return null; //split into 2 chars var stringBytes = _onString.SplitInParts(2).ToList(); @@ -98,7 +97,7 @@ internal static string ReverseByteOrder (this string _onString) { } - internal static IEnumerable SplitInParts (this string s, int partLength) { + internal static IEnumerable SplitInParts(this string s, int partLength) { if (s == null) throw new ArgumentNullException(nameof(s)); @@ -110,7 +109,7 @@ internal static IEnumerable SplitInParts (this string s, int partLength) } - internal static string BuildDTString (this string _inString, short _stringReservedSize) { + internal static string BuildDTString(this string _inString, short _stringReservedSize) { StringBuilder sb = new StringBuilder(); @@ -140,7 +139,7 @@ internal static string BuildDTString (this string _inString, short _stringReserv sb.Append(reservedSizeBytes); //string count actual bytes sb.Append(sizeBytes); - + sb.Append(hexstring); @@ -148,18 +147,18 @@ internal static string BuildDTString (this string _inString, short _stringReserv } - internal static string GetStringFromAsciiHex (this string input) { + internal static string GetStringFromAsciiHex(this string input) { if (input.Length % 2 != 0) return null; 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); + bytes[i / 2] = Convert.ToByte(hex, 16); } return Encoding.ASCII.GetString(bytes); } - internal static string GetAsciiHexFromString (this string input) { + internal static string GetAsciiHexFromString(this string input) { var bytes = new ASCIIEncoding().GetBytes(input); return bytes.ToHexString(); } @@ -173,7 +172,7 @@ internal static byte[] HexStringToByteArray(this string hex) { .ToArray(); } - internal static string ToHexString (this byte[] arr) { + internal static string ToHexString(this byte[] arr) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < arr.Length; i++) { @@ -184,17 +183,17 @@ internal static string ToHexString (this byte[] arr) { } - internal static byte[] BigToMixedEndian (this byte[] arr) { + internal static byte[] BigToMixedEndian(this byte[] arr) { List oldBL = new List(arr); List tempL = new List(); //make the input list even - if(arr.Length % 2 != 0) + if (arr.Length % 2 != 0) oldBL.Add((byte)0); - for (int i = 0; i < oldBL.Count; i+=2) { + for (int i = 0; i < oldBL.Count; i += 2) { byte firstByte = oldBL[i]; byte lastByte = oldBL[i + 1]; tempL.Add(lastByte); @@ -206,7 +205,7 @@ internal static byte[] BigToMixedEndian (this byte[] arr) { } - internal static bool IsDoubleNumericRegisterType (this Type type) { + internal static bool IsDoubleNumericRegisterType(this Type type) { //Type[] singles = new Type[] { // typeof(short), @@ -224,7 +223,7 @@ internal static bool IsDoubleNumericRegisterType (this Type type) { } - internal static bool IsNumericSupportedType (this Type type) { + internal static bool IsNumericSupportedType(this Type type) { Type[] supported = new Type[] { typeof(short), @@ -242,7 +241,7 @@ internal static bool IsNumericSupportedType (this Type type) { /// /// Checks if the register type is non numeric /// - internal static bool IsBoolean (this RegisterType type) { + internal static bool IsBoolean(this RegisterType type) { return type == RegisterType.X || type == RegisterType.Y || type == RegisterType.R; @@ -251,7 +250,7 @@ internal static bool IsBoolean (this RegisterType type) { /// /// Checks if the register type is an physical in or output of the plc /// - internal static bool IsPhysicalInOutType (this RegisterType type) { + internal static bool IsPhysicalInOutType(this RegisterType type) { return type == RegisterType.X || type == RegisterType.Y; diff --git a/MewtocolNet/Mewtocol/MewtocolInterface.cs b/MewtocolNet/MewtocolInterface.cs similarity index 91% rename from MewtocolNet/Mewtocol/MewtocolInterface.cs rename to MewtocolNet/MewtocolInterface.cs index 009f7fe..b5f28a6 100644 --- a/MewtocolNet/Mewtocol/MewtocolInterface.cs +++ b/MewtocolNet/MewtocolInterface.cs @@ -1,27 +1,21 @@ +using MewtocolNet.Logging; +using MewtocolNet.Queue; +using MewtocolNet.RegisterAttributes; +using MewtocolNet.Subregisters; using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; using System.IO; +using System.Linq; +using System.Net; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; -using System.Collections.Generic; using System.Threading.Tasks; -using System.Linq; -using MewtocolNet.Registers; -using MewtocolNet.RegisterAttributes; -using MewtocolNet.Logging; -using System.Collections; -using System.Diagnostics; -using System.ComponentModel; -using System.Net; -using System.Threading; -using MewtocolNet.Queue; -using System.Reflection; -using System.Timers; -using System.Data; -using System.Xml.Linq; -namespace MewtocolNet -{ +namespace MewtocolNet { /// /// The PLC com interface class @@ -63,7 +57,7 @@ public int ConnectTimeout { /// public int PollerDelayMs { get => pollerDelayMs; - set { + set { pollerDelayMs = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PollerDelayMs))); } @@ -108,8 +102,8 @@ public bool Disposed { /// /// Generic information about the connected PLC /// - public PLCInfo PlcInfo { - get => plcInfo; + public PLCInfo PlcInfo { + get => plcInfo; private set { plcInfo = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PlcInfo))); @@ -158,7 +152,7 @@ private set { /// The current transmission speed in bytes per second /// public int BytesPerSecondUpstream { - get { return bytesPerSecondUpstream; } + get { return bytesPerSecondUpstream; } private set { bytesPerSecondUpstream = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BytesPerSecondUpstream))); @@ -195,7 +189,7 @@ private set { /// IP adress of the PLC /// Port of the PLC /// Station Number of the PLC - public MewtocolInterface (string _ip, int _port = 9094, int _station = 1) { + public MewtocolInterface(string _ip, int _port = 9094, int _station = 1) { ip = _ip; port = _port; @@ -203,7 +197,7 @@ public MewtocolInterface (string _ip, int _port = 9094, int _station = 1) { Connected += MewtocolInterface_Connected; - void MewtocolInterface_Connected (PLCInfo obj) { + void MewtocolInterface_Connected(PLCInfo obj) { if (usePoller) AttachPoller(); @@ -237,7 +231,7 @@ void MewtocolInterface_Connected (PLCInfo obj) { /// /// Gets called when an error or timeout during connection occurs /// - public async Task ConnectAsync (Action OnConnected = null, Action OnFailed = null) { + public async Task ConnectAsync(Action OnConnected = null, Action OnFailed = null) { Logger.Log("Connecting to PLC...", LogLevel.Info, this); @@ -258,7 +252,7 @@ public async Task ConnectAsync (Action OnConnected = } PolledCycle += OnPollCycleDone; - void OnPollCycleDone () { + void OnPollCycleDone() { OnConnected(plcinf); PolledCycle -= OnPollCycleDone; } @@ -284,7 +278,7 @@ void OnPollCycleDone () { /// Ip adress /// Port number /// Station number - public void ChangeConnectionSettings (string _ip, int _port, int _station = 1) { + public void ChangeConnectionSettings(string _ip, int _port, int _station = 1) { if (IsConnected) throw new Exception("Cannot change the connection settings while the PLC is connected"); @@ -298,7 +292,7 @@ public void ChangeConnectionSettings (string _ip, int _port, int _station = 1) { /// /// Closes the connection all cyclic polling /// - public void Disconnect () { + public void Disconnect() { if (!IsConnected) return; @@ -311,7 +305,7 @@ public void Disconnect () { /// Attaches a poller to the interface that continously /// polls the registered data registers and writes the values to them /// - public MewtocolInterface WithPoller () { + public MewtocolInterface WithPoller() { usePoller = true; @@ -323,7 +317,7 @@ public MewtocolInterface WithPoller () { #region TCP connection state handling - private async Task ConnectTCP () { + private async Task ConnectTCP() { if (!IPAddress.TryParse(ip, out var targetIP)) { throw new ArgumentException("The IP adress of the PLC was no valid format"); @@ -331,8 +325,8 @@ private async Task ConnectTCP () { try { - if(HostEndpoint != null) { - + if (HostEndpoint != null) { + client = new TcpClient(HostEndpoint) { ReceiveBufferSize = RecBufferSize, NoDelay = false, @@ -353,12 +347,12 @@ private async Task ConnectTCP () { var result = client.BeginConnect(targetIP, port, null, null); var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(ConnectTimeout)); - if(!success || !client.Connected) { + if (!success || !client.Connected) { OnMajorSocketExceptionWhileConnecting(); return; } - if(HostEndpoint == null) { + if (HostEndpoint == null) { var ep = (IPEndPoint)client.Client.LocalEndPoint; Logger.Log($"Connecting [AUTO] endpoint: {ep.Address.MapToIPv4()}:{ep.Port}", LogLevel.Verbose, this); } @@ -376,7 +370,7 @@ private async Task ConnectTCP () { } - private void OnMajorSocketExceptionWhileConnecting () { + private void OnMajorSocketExceptionWhileConnecting() { Logger.Log("The PLC connection timed out", LogLevel.Error, this); CycleTimeMs = 0; @@ -385,7 +379,7 @@ private void OnMajorSocketExceptionWhileConnecting () { } - private void OnMajorSocketExceptionWhileConnected () { + private void OnMajorSocketExceptionWhileConnected() { if (IsConnected) { @@ -400,7 +394,7 @@ private void OnMajorSocketExceptionWhileConnected () { } - private void ClearRegisterVals () { + private void ClearRegisterVals() { for (int i = 0; i < Registers.Count; i++) { @@ -423,7 +417,7 @@ private void ClearRegisterVals () { /// and assert some propertys with the custom . /// /// A collection inherting the class - public MewtocolInterface WithRegisterCollection (RegisterCollectionBase collection) { + public MewtocolInterface WithRegisterCollection(RegisterCollectionBase collection) { collection.PLCInterface = this; @@ -516,7 +510,7 @@ public MewtocolInterface WithRegisterCollection (RegisterCollectionBase collecti RegisterChanged += (reg) => { //register is used bitwise - if(reg.IsUsedBitwise()) { + if (reg.IsUsedBitwise()) { for (int i = 0; i < props.Length; i++) { @@ -524,7 +518,7 @@ public MewtocolInterface WithRegisterCollection (RegisterCollectionBase collecti var bitWiseFound = prop.GetCustomAttributes(true) .FirstOrDefault(y => y.GetType() == typeof(RegisterAttribute) && ((RegisterAttribute)y).MemoryArea == reg.MemoryAddress); - if(bitWiseFound != null) { + if (bitWiseFound != null) { var casted = (RegisterAttribute)bitWiseFound; var bitIndex = casted.AssignedBitIndex; @@ -534,13 +528,13 @@ public MewtocolInterface WithRegisterCollection (RegisterCollectionBase collecti if (reg is NRegister reg16) { var bytes = BitConverter.GetBytes((short)reg16.Value); bitAr = new BitArray(bytes); - } else if(reg is NRegister reg32) { + } else if (reg is NRegister reg32) { var bytes = BitConverter.GetBytes((int)reg32.Value); bitAr = new BitArray(bytes); } if (bitAr != null && bitIndex < bitAr.Length && bitIndex >= 0) { - + //set the specific bit index if needed prop.SetValue(collection, bitAr[bitIndex]); collection.TriggerPropertyChanged(prop.Name); @@ -550,7 +544,7 @@ public MewtocolInterface WithRegisterCollection (RegisterCollectionBase collecti //set the specific bit array if needed prop.SetValue(collection, bitAr); collection.TriggerPropertyChanged(prop.Name); - + } } @@ -558,7 +552,7 @@ public MewtocolInterface WithRegisterCollection (RegisterCollectionBase collecti } } - + //updating normal properties var foundToUpdate = props.FirstOrDefault(x => x.Name == reg.Name); @@ -576,13 +570,13 @@ public MewtocolInterface WithRegisterCollection (RegisterCollectionBase collecti if (registerAttr.AssignedBitIndex == -1) { HashSet NumericTypes = new HashSet { - typeof(bool), - typeof(short), + typeof(bool), + typeof(short), typeof(ushort), - typeof(int), - typeof(uint), - typeof(float), - typeof(TimeSpan), + typeof(int), + typeof(uint), + typeof(float), + typeof(TimeSpan), typeof(string) }; @@ -625,7 +619,7 @@ public MewtocolInterface WithRegisterCollection (RegisterCollectionBase collecti /// /// 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) { + public void SetRegister(string registerName, object value) { var foundRegister = GetAllRegisters().FirstOrDefault(x => x.Name == registerName); @@ -642,7 +636,7 @@ public void SetRegister (string registerName, object value) { /// /// The name the register was given to or a property name from the RegisterCollection class /// The value to write to the register - public async Task SetRegisterAsync (string registerName, object value) { + public async Task SetRegisterAsync(string registerName, object value) { var foundRegister = GetAllRegisters().FirstOrDefault(x => x.Name == registerName); @@ -711,7 +705,7 @@ public async Task SetRegisterAsync (string registerName, object value) { /// /// MEWTOCOL Formatted request string ex: %01#RT /// Returns the result - public async Task SendCommandAsync (string _msg) { + public async Task SendCommandAsync(string _msg) { _msg = _msg.BuildBCCFrame(); _msg += "\r"; @@ -720,10 +714,10 @@ public async Task SendCommandAsync (string _msg) { try { queuedMessages++; - + var response = await queue.Enqueue(() => SendSingleBlock(_msg)); - if (queuedMessages > 0) + if (queuedMessages > 0) queuedMessages--; if (response == null) { @@ -739,7 +733,7 @@ public async Task SendCommandAsync (string _msg) { Match m = errorcheck.Match(response.ToString()); if (m.Success) { string eCode = m.Groups[1].Value; - string eDes = Links.CodeDescriptions.Error[Convert.ToInt32(eCode)]; + string eDes = CodeDescriptions.Error[Convert.ToInt32(eCode)]; Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"Response is: {response}"); Logger.Log($"Error on command {_msg.Replace("\r", "")} the PLC returned error code: {eCode}, {eDes}", LogLevel.Error); @@ -767,9 +761,9 @@ public async Task SendCommandAsync (string _msg) { } - private async Task SendSingleBlock (string _blockString) { + private async Task SendSingleBlock(string _blockString) { - if (client == null || !client.Connected ) { + if (client == null || !client.Connected) { await ConnectTCP(); } @@ -779,11 +773,11 @@ private async Task SendSingleBlock (string _blockString) { var message = _blockString.ToHexASCIIBytes(); //time measuring - if(speedStopwatchUpstr == null) { + if (speedStopwatchUpstr == null) { speedStopwatchUpstr = Stopwatch.StartNew(); } - if(speedStopwatchUpstr.Elapsed.TotalSeconds >= 1) { + if (speedStopwatchUpstr.Elapsed.TotalSeconds >= 1) { speedStopwatchUpstr.Restart(); bytesTotalCountedUpstream = 0; } @@ -846,15 +840,15 @@ private async Task SendSingleBlock (string _blockString) { return null; } - if(!string.IsNullOrEmpty(response.ToString())) { - + if (!string.IsNullOrEmpty(response.ToString())) { + Logger.Log($"<-- IN MSG: {response}", LogLevel.Critical, this); bytesTotalCountedDownstream += Encoding.ASCII.GetByteCount(response.ToString()); var perSecDownstream = (double)((bytesTotalCountedDownstream / speedStopwatchDownstr.Elapsed.TotalMilliseconds) * 1000); - if(perSecUpstream <= 10000) + if (perSecUpstream <= 10000) BytesPerSecondDownstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero); return response.ToString(); @@ -872,7 +866,7 @@ private async Task SendSingleBlock (string _blockString) { /// /// Disposes the current interface and clears all its members /// - public void Dispose () { + public void Dispose() { if (Disposed) return; @@ -891,7 +885,7 @@ public void Dispose () { /// /// Gets the connection info string /// - public string GetConnectionPortInfo () { + public string GetConnectionPortInfo() { return $"{IpAddress}:{Port}"; @@ -901,5 +895,4 @@ public string GetConnectionPortInfo () { } - } \ No newline at end of file diff --git a/MewtocolNet/Mewtocol/MewtocolInterfaceRequests.cs b/MewtocolNet/MewtocolInterfaceRequests.cs similarity index 90% rename from MewtocolNet/Mewtocol/MewtocolInterfaceRequests.cs rename to MewtocolNet/MewtocolInterfaceRequests.cs index 375fb73..94917db 100644 --- a/MewtocolNet/Mewtocol/MewtocolInterfaceRequests.cs +++ b/MewtocolNet/MewtocolInterfaceRequests.cs @@ -1,17 +1,15 @@ +using MewtocolNet.Logging; +using MewtocolNet.PLCEnums; +using MewtocolNet.Subregisters; using System; -using System.IO; -using System.Net.Sockets; -using System.Text; using System.Collections.Generic; +using System.Globalization; +using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using MewtocolNet.Registers; -using System.Linq; -using System.Globalization; -using MewtocolNet.Logging; namespace MewtocolNet { - + public partial class MewtocolInterface { #region PLC info getters @@ -20,14 +18,14 @@ public partial class MewtocolInterface { /// Gets generic information about the PLC /// /// A PLCInfo class - public async Task GetPLCInfoAsync () { + public async Task GetPLCInfoAsync() { var resu = await SendCommandAsync("%01#RT"); - if(!resu.Success) return null; + 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) { + + if (m.Success) { string station = m.Groups[1].Value; string cpu = m.Groups[2].Value; @@ -48,7 +46,7 @@ public async Task GetPLCInfoAsync () { PlcInfo = retInfo; return retInfo; - } + } return null; } @@ -61,7 +59,7 @@ public async Task GetPLCInfoAsync () { /// /// The mode to change to /// The success state of the write operation - public async Task SetOperationMode (OPMode mode) { + public async Task SetOperationMode(OPMode mode) { string modeChar = mode == OPMode.Prog ? "P" : "R"; @@ -90,7 +88,7 @@ public async Task SetOperationMode (OPMode mode) { /// /// start address of the array /// /// - public async Task WriteByteRange (int start, byte[] byteArr) { + public async Task WriteByteRange(int start, byte[] byteArr) { string byteString = byteArr.BigToMixedEndian().ToHexString(); var wordLength = byteArr.Length / 2; @@ -114,17 +112,17 @@ public async Task WriteByteRange (int start, byte[] byteArr) { /// Number of bytes to get /// Gets invoked when the progress changes, contains the progress as a double /// A byte array or null of there was an error - public async Task ReadByteRange (int start, int count, Action onProgress = null) { + public async Task ReadByteRange(int start, int count, Action onProgress = null) { var byteList = new List(); - + var wordLength = count / 2; - if (count % 2 != 0) + if (count % 2 != 0) wordLength++; //read blocks of max 4 words per msg - for (int i = 0; i < wordLength; i+=8) { + for (int i = 0; i < wordLength; i += 8) { int curWordStart = start + i; int curWordEnd = curWordStart + 7; @@ -147,12 +145,12 @@ public async Task ReadByteRange (int start, int count, Action on } - if(onProgress != null) + if (onProgress != null) onProgress((double)i / wordLength); } - return byteList.ToArray(); + return byteList.ToArray(); } @@ -164,12 +162,12 @@ public async Task ReadByteRange (int start, int count, Action on /// Reads the given boolean register from the PLC /// /// The register to read - public async Task ReadBoolRegister (BRegister _toRead) { + public async Task ReadBoolRegister(BRegister _toRead) { string requeststring = $"%{GetStationNumber()}#RCS{_toRead.BuildMewtocolQuery()}"; var result = await SendCommandAsync(requeststring); - if(!result.Success) { + if (!result.Success) { return new BRegisterResult { Result = result, Register = _toRead @@ -177,9 +175,9 @@ public async Task ReadBoolRegister (BRegister _toRead) { } var resultBool = result.Response.ParseRCSingleBit(); - if(resultBool != null) { + if (resultBool != null) { _toRead.SetValueFromPLC(resultBool.Value); - } + } var finalRes = new BRegisterResult { Result = result, @@ -196,13 +194,13 @@ public async Task ReadBoolRegister (BRegister _toRead) { /// The register to write to /// The value to write /// The success state of the write operation - public async Task WriteBoolRegister (BRegister _toWrite, bool value) { + public async Task WriteBoolRegister(BRegister _toWrite, bool value) { string requeststring = $"%{GetStationNumber()}#WCS{_toWrite.BuildMewtocolQuery()}{(value ? "1" : "0")}"; var result = await SendCommandAsync(requeststring); - return result.Success && result.Response.StartsWith($"%{ GetStationNumber()}$WC"); + return result.Success && result.Response.StartsWith($"%{GetStationNumber()}$WC"); } @@ -216,7 +214,7 @@ public async Task WriteBoolRegister (BRegister _toWrite, bool value) { /// Type of number (short, ushort, int, uint, float) /// The register to read /// A result with the given NumberRegister containing the readback value and a result struct - public async Task> ReadNumRegister (NRegister _toRead) { + public async Task> ReadNumRegister(NRegister _toRead) { Type numType = typeof(T); @@ -231,7 +229,7 @@ public async Task> ReadNumRegister (NRegister _toRead) if (!result.Success || string.IsNullOrEmpty(result.Response)) { return failedResult; } - + if (numType == typeof(short)) { var resultBytes = result.Response.ParseDTByteString(4).ReverseByteOrder(); @@ -301,7 +299,7 @@ public async Task> ReadNumRegister (NRegister _toRead) /// The register to write /// The value to write /// The success state of the write operation - public async Task WriteNumRegister (NRegister _toWrite, T _value) { + public async Task WriteNumRegister(NRegister _toWrite, T _value) { byte[] toWriteVal; Type numType = typeof(T); @@ -339,7 +337,7 @@ public async Task WriteNumRegister (NRegister _toWrite, T _value) { var result = await SendCommandAsync(requeststring); - return result.Success && result.Response.StartsWith($"%{ GetStationNumber()}$WD"); + return result.Success && result.Response.StartsWith($"%{GetStationNumber()}$WD"); } @@ -360,7 +358,7 @@ public async Task WriteNumRegister (NRegister _toWrite, T _value) { /// The register to read /// The station number of the PLC /// - public async Task ReadStringRegister (SRegister _toRead, int _stationNumber = 1) { + public async Task ReadStringRegister(SRegister _toRead, int _stationNumber = 1) { string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolQuery()}"; var result = await SendCommandAsync(requeststring); @@ -382,27 +380,27 @@ public async Task ReadStringRegister (SRegister _toRead, int _s public async Task WriteStringRegister(SRegister _toWrite, string _value, int _stationNumber = 1) { if (_value == null) _value = ""; - if(_value.Length > _toWrite.ReservedSize) { + if (_value.Length > _toWrite.ReservedSize) { throw new ArgumentException("Write string size cannot be longer than reserved string size"); } string stationNum = GetStationNumber(); string dataString = _value.BuildDTString(_toWrite.ReservedSize); string dataArea = _toWrite.BuildCustomIdent(dataString.Length / 4); - + string requeststring = $"%{stationNum}#WD{dataArea}{dataString}"; var result = await SendCommandAsync(requeststring); - return result.Success && result.Response.StartsWith($"%{ GetStationNumber()}$WD"); + return result.Success && result.Response.StartsWith($"%{GetStationNumber()}$WD"); } #endregion #region Helpers - internal string GetStationNumber () { + internal string GetStationNumber() { return StationNumber.ToString().PadLeft(2, '0'); diff --git a/MewtocolNet/Mewtocol/PLCEnums/CpuType.cs b/MewtocolNet/PLCEnums/CpuType.cs similarity index 96% rename from MewtocolNet/Mewtocol/PLCEnums/CpuType.cs rename to MewtocolNet/PLCEnums/CpuType.cs index 9312177..cc5bcef 100644 --- a/MewtocolNet/Mewtocol/PLCEnums/CpuType.cs +++ b/MewtocolNet/PLCEnums/CpuType.cs @@ -1,4 +1,4 @@ -namespace MewtocolNet { +namespace MewtocolNet.PLCEnums { /// /// CPU type of the PLC diff --git a/MewtocolNet/Mewtocol/PLCEnums/OPMode.cs b/MewtocolNet/PLCEnums/OPMode.cs similarity index 74% rename from MewtocolNet/Mewtocol/PLCEnums/OPMode.cs rename to MewtocolNet/PLCEnums/OPMode.cs index 150338c..9ae73f1 100644 --- a/MewtocolNet/Mewtocol/PLCEnums/OPMode.cs +++ b/MewtocolNet/PLCEnums/OPMode.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace MewtocolNet { +namespace MewtocolNet.PLCEnums { /// /// CPU type of the PLC diff --git a/MewtocolNet/Mewtocol/PLCInfo.cs b/MewtocolNet/PLCInfo.cs similarity index 80% rename from MewtocolNet/Mewtocol/PLCInfo.cs rename to MewtocolNet/PLCInfo.cs index c89748e..4b1b46d 100644 --- a/MewtocolNet/Mewtocol/PLCInfo.cs +++ b/MewtocolNet/PLCInfo.cs @@ -1,4 +1,4 @@ -namespace MewtocolNet.Registers { +namespace MewtocolNet { /// /// Contains generic information about the plc /// @@ -7,26 +7,26 @@ public class PLCInfo { /// /// Contains information about the PLCs cpu /// - public CpuInfo CpuInformation {get;set;} + public CpuInfo CpuInformation { get; set; } /// /// Contains information about the PLCs operation modes /// - public PLCMode OperationMode {get;set;} + public PLCMode OperationMode { get; set; } /// /// Current error code of the PLC /// - public string ErrorCode {get;set;} + public string ErrorCode { get; set; } /// /// Current station number of the PLC /// - public int StationNumber { get;set;} + public int StationNumber { get; set; } /// /// Generates a string containing some of the most important informations /// /// - public override string ToString () { + public override string ToString() { return $"Type: {CpuInformation.Cputype},\n" + $"Capacity: {CpuInformation.ProgramCapacity}k\n" + diff --git a/MewtocolNet/Mewtocol/PLCMode.cs b/MewtocolNet/PLCMode.cs similarity index 96% rename from MewtocolNet/Mewtocol/PLCMode.cs rename to MewtocolNet/PLCMode.cs index e9a10eb..9079527 100644 --- a/MewtocolNet/Mewtocol/PLCMode.cs +++ b/MewtocolNet/PLCMode.cs @@ -1,6 +1,6 @@ using System; -namespace MewtocolNet.Registers { +namespace MewtocolNet { /// /// All modes @@ -43,7 +43,7 @@ public class PLCMode { /// /// Gets operation mode from 2 digit hex number /// - internal static PLCMode BuildFromHex (string _hexString) { + internal 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'); @@ -87,6 +87,7 @@ internal static PLCMode BuildFromHex (string _hexString) { return retMode; } + } } \ No newline at end of file diff --git a/MewtocolNet/Queue/SerialQueue.cs b/MewtocolNet/Queue/SerialQueue.cs index 06001bf..efa71c6 100644 --- a/MewtocolNet/Queue/SerialQueue.cs +++ b/MewtocolNet/Queue/SerialQueue.cs @@ -8,7 +8,7 @@ internal class SerialQueue { readonly object _locker = new object(); readonly WeakReference _lastTask = new WeakReference(null); - internal Task Enqueue (Func> asyncFunction) { + internal Task Enqueue(Func> asyncFunction) { lock (_locker) { Task lastTask; Task resultTask; diff --git a/MewtocolNet/RegisterAttributes/BitCount.cs b/MewtocolNet/RegisterAttributes/BitCount.cs new file mode 100644 index 0000000..08228b1 --- /dev/null +++ b/MewtocolNet/RegisterAttributes/BitCount.cs @@ -0,0 +1,16 @@ +namespace MewtocolNet.RegisterAttributes { + /// + /// The size of the bitwise register + /// + public enum BitCount { + /// + /// 16 bit + /// + B16, + /// + /// 32 bit + /// + B32 + } + +} diff --git a/MewtocolNet/Mewtocol/RegisterAttributes/RegisterAttribute.cs b/MewtocolNet/RegisterAttributes/RegisterAttribute.cs similarity index 75% rename from MewtocolNet/Mewtocol/RegisterAttributes/RegisterAttribute.cs rename to MewtocolNet/RegisterAttributes/RegisterAttribute.cs index 8e81f21..dd90c3c 100644 --- a/MewtocolNet/Mewtocol/RegisterAttributes/RegisterAttribute.cs +++ b/MewtocolNet/RegisterAttributes/RegisterAttribute.cs @@ -1,25 +1,7 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace MewtocolNet.RegisterAttributes { - /// - /// The size of the bitwise register - /// - public enum BitCount { - /// - /// 16 bit - /// - B16, - /// - /// 32 bit - /// - B32 - } - /// /// Defines the behavior of a register property /// @@ -38,10 +20,10 @@ public class RegisterAttribute : Attribute { /// /// The area in the plcs memory /// The max string length in the plc - public RegisterAttribute (int memoryArea, int stringLength = 1) { + public RegisterAttribute(int memoryArea, int stringLength = 1) { MemoryArea = memoryArea; - StringLength = stringLength; + StringLength = stringLength; } @@ -59,11 +41,11 @@ public RegisterAttribute(IOType type, byte spAdress = 0x0) { /// /// Attribute for boolean registers /// - public RegisterAttribute (IOType type, int memoryArea, byte spAdress = 0x0) { + public RegisterAttribute(IOType type, int memoryArea, byte spAdress = 0x0) { MemoryArea = memoryArea; RegisterType = (RegisterType)(int)type; - SpecialAddress = spAdress; + SpecialAddress = spAdress; } @@ -72,7 +54,7 @@ public RegisterAttribute (IOType type, int memoryArea, byte spAdress = 0x0) { /// /// The area in the plcs memory /// The number of bits to parse - public RegisterAttribute (int memoryArea, BitCount bitcount) { + public RegisterAttribute(int memoryArea, BitCount bitcount) { MemoryArea = memoryArea; StringLength = 0; @@ -86,9 +68,9 @@ public RegisterAttribute (int memoryArea, BitCount bitcount) { /// The area in the plcs memory /// The number of bits to parse /// The index of the bit that gets linked to the bool - public RegisterAttribute (int memoryArea, uint assignBit, BitCount bitcount) { + public RegisterAttribute(int memoryArea, uint assignBit, BitCount bitcount) { - if(assignBit > 15 && bitcount == BitCount.B16) { + if (assignBit > 15 && bitcount == BitCount.B16) { throw new NotSupportedException("The assignBit parameter cannot be greater than 15 in a 16 bit var"); } diff --git a/MewtocolNet/Mewtocol/RegisterAttributes/RegisterCollectionBase.cs b/MewtocolNet/RegisterAttributes/RegisterCollectionBase.cs similarity index 69% rename from MewtocolNet/Mewtocol/RegisterAttributes/RegisterCollectionBase.cs rename to MewtocolNet/RegisterAttributes/RegisterCollectionBase.cs index 4b5f2a5..66d38d8 100644 --- a/MewtocolNet/Mewtocol/RegisterAttributes/RegisterCollectionBase.cs +++ b/MewtocolNet/RegisterAttributes/RegisterCollectionBase.cs @@ -1,15 +1,7 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq; +using System.ComponentModel; using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; -namespace MewtocolNet.RegisterAttributes -{ +namespace MewtocolNet.RegisterAttributes { /// /// A register collection base with full auto read and notification support built in @@ -19,7 +11,7 @@ public class RegisterCollectionBase : INotifyPropertyChanged { /// /// Reference to its bound interface /// - public MewtocolInterface PLCInterface { get; set; } + public MewtocolInterface PLCInterface { get; set; } /// /// Whenever one of its props changes @@ -30,7 +22,7 @@ public class RegisterCollectionBase : INotifyPropertyChanged { /// Triggers a property changed event /// /// Name of the property to trigger for - public void TriggerPropertyChanged (string propertyName = null) { + public void TriggerPropertyChanged(string propertyName = null) { var handler = PropertyChanged; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } @@ -38,11 +30,11 @@ public void TriggerPropertyChanged (string propertyName = null) { /// /// Use this on the setter method of a property to enable automatic property register writing /// - public void AutoSetter (object value, ref T privateField, [CallerMemberName] string propName = null) { + public void AutoSetter(object value, ref T privateField, [CallerMemberName] string propName = null) { PLCInterface.PropertyRegisterWasSet(propName, value); - if(value is IRegister reg) { + if (value is IRegister reg) { privateField = (T)reg.Value; return; @@ -57,14 +49,14 @@ public void AutoSetter (object value, ref T privateField, [CallerMemberName] /// Gets called when the register collection base was linked to its parent mewtocol interface /// /// The parent interface - public virtual void OnInterfaceLinked (MewtocolInterface plc) { } + public virtual void OnInterfaceLinked(MewtocolInterface plc) { } /// /// Gets called when the register collection base was linked to its parent mewtocol interface /// and the plc connection is established /// /// The parent interface - public virtual void OnInterfaceLinkedAndOnline (MewtocolInterface plc) { } + public virtual void OnInterfaceLinkedAndOnline(MewtocolInterface plc) { } } diff --git a/MewtocolNet/Mewtocol/RegisterBuilder.cs b/MewtocolNet/RegisterBuilding/RegisterBuilder.cs similarity index 67% rename from MewtocolNet/Mewtocol/RegisterBuilder.cs rename to MewtocolNet/RegisterBuilding/RegisterBuilder.cs index 7278ac4..96cbf67 100644 --- a/MewtocolNet/Mewtocol/RegisterBuilder.cs +++ b/MewtocolNet/RegisterBuilding/RegisterBuilder.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace MewtocolNet.RegisterBuilding { -namespace MewtocolNet.Mewtocol { - /// /// Contains useful tools for register creation /// @@ -15,12 +11,14 @@ public static class RegisterBuilder { /// The name, fe. DT100 /// An or null if /// True if successfully parsed - //public static bool TryBuildFromName (string name, out IRegister reg) { + public static bool TryBuildFromName(string name, out IRegister reg) { + reg = null; + return false; + + } - //} - } } diff --git a/MewtocolNet/Mewtocol/RegisterEnums.cs b/MewtocolNet/RegisterEnums.cs similarity index 90% rename from MewtocolNet/Mewtocol/RegisterEnums.cs rename to MewtocolNet/RegisterEnums.cs index d6559c4..55c3528 100644 --- a/MewtocolNet/Mewtocol/RegisterEnums.cs +++ b/MewtocolNet/RegisterEnums.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MewtocolNet { +namespace MewtocolNet { /// /// The register prefixed type diff --git a/MewtocolNet/Mewtocol/Responses.cs b/MewtocolNet/Responses.cs similarity index 57% rename from MewtocolNet/Mewtocol/Responses.cs rename to MewtocolNet/Responses.cs index b9b2289..efc003b 100644 --- a/MewtocolNet/Mewtocol/Responses.cs +++ b/MewtocolNet/Responses.cs @@ -1,10 +1,4 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; -using System.Linq; -using System.Text; -using System.ComponentModel; - -namespace MewtocolNet.Registers { +namespace MewtocolNet { /// /// The formatted result of a ascii command @@ -14,19 +8,19 @@ public struct CommandResult { /// /// Success state of the message /// - public bool Success {get;set;} + public bool Success { get; set; } /// /// Response text of the message /// - public string Response {get;set;} + public string Response { get; set; } /// /// Error code of the message /// - public string Error {get;set;} + public string Error { get; set; } /// /// Error text of the message /// - public string ErrorDescription {get;set;} + public string ErrorDescription { get; set; } } diff --git a/MewtocolNet/Mewtocol/Subregisters/BRegister.cs b/MewtocolNet/Subregisters/BRegister.cs similarity index 90% rename from MewtocolNet/Mewtocol/Subregisters/BRegister.cs rename to MewtocolNet/Subregisters/BRegister.cs index c49d13e..88b8590 100644 --- a/MewtocolNet/Mewtocol/Subregisters/BRegister.cs +++ b/MewtocolNet/Subregisters/BRegister.cs @@ -1,10 +1,8 @@ using System; using System.ComponentModel; -using System.Data; using System.Text; -using MewtocolNet; -namespace MewtocolNet.Registers { +namespace MewtocolNet.Subregisters { /// /// Defines a register containing a boolean @@ -21,7 +19,7 @@ public class BRegister : IRegister, INotifyPropertyChanged { /// public event PropertyChangedEventHandler PropertyChanged; - internal RegisterType RegType { get; private set; } + internal RegisterType RegType { get; private set; } internal Type collectionType; @@ -64,12 +62,12 @@ public class BRegister : IRegister, INotifyPropertyChanged { /// The custom name /// /// - public BRegister (IOType _io, byte _spAddress = 0x0, int _areaAdress = 0, string _name = null) { + public BRegister(IOType _io, byte _spAddress = 0x0, int _areaAdress = 0, string _name = null) { if (_areaAdress < 0) throw new NotSupportedException("The area address cant be negative"); - if(_io == IOType.R && _areaAdress >= 512) + if (_io == IOType.R && _areaAdress >= 512) throw new NotSupportedException("R area addresses cant be greater than 511"); if ((_io == IOType.X || _io == IOType.Y) && _areaAdress >= 110) @@ -78,7 +76,7 @@ public BRegister (IOType _io, byte _spAddress = 0x0, int _areaAdress = 0, string if (_spAddress > 0xF) throw new NotSupportedException("Special address cant be greater 15 or 0xF"); - memoryAddress = (int)_areaAdress; + memoryAddress = _areaAdress; specialAddress = _spAddress; name = _name; @@ -96,7 +94,7 @@ internal BRegister WithCollectionType(Type colType) { /// /// Builds the register area name /// - public string BuildMewtocolQuery () { + public string BuildMewtocolQuery() { //build area code from register type StringBuilder asciistring = new StringBuilder(RegType.ToString()); @@ -111,12 +109,12 @@ public string BuildMewtocolQuery () { } - internal void SetValueFromPLC (bool val) { + internal void SetValueFromPLC(bool val) { lastValue = val; TriggerChangedEvnt(this); TriggerNotifyChange(); - + } public string GetStartingMemoryArea() { @@ -149,7 +147,7 @@ public string GetRegisterPLCName() { } - if(MemoryAddress > 0 && SpecialAddress != 0) { + if (MemoryAddress > 0 && SpecialAddress != 0) { return $"{GetRegisterString()}{MemoryAddress}{spAdressEnd}"; diff --git a/MewtocolNet/Mewtocol/Subregisters/BRegisterResult.cs b/MewtocolNet/Subregisters/BRegisterResult.cs similarity index 90% rename from MewtocolNet/Mewtocol/Subregisters/BRegisterResult.cs rename to MewtocolNet/Subregisters/BRegisterResult.cs index 8a39277..ddc4477 100644 --- a/MewtocolNet/Mewtocol/Subregisters/BRegisterResult.cs +++ b/MewtocolNet/Subregisters/BRegisterResult.cs @@ -1,4 +1,4 @@ -namespace MewtocolNet.Registers { +namespace MewtocolNet.Subregisters { /// /// Result for a boolean register diff --git a/MewtocolNet/Mewtocol/Subregisters/NRegister.cs b/MewtocolNet/Subregisters/NRegister.cs similarity index 93% rename from MewtocolNet/Mewtocol/Subregisters/NRegister.cs rename to MewtocolNet/Subregisters/NRegister.cs index 79cf631..1030cd6 100644 --- a/MewtocolNet/Mewtocol/Subregisters/NRegister.cs +++ b/MewtocolNet/Subregisters/NRegister.cs @@ -2,16 +2,15 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; -using System.Reflection; using System.Text; -namespace MewtocolNet.Registers { +namespace MewtocolNet.Subregisters { /// /// Defines a register containing a number /// /// The type of the numeric value - public class NRegister : IRegister { + public class NRegister : IRegister { /// /// Gets called whenever the value was changed @@ -64,7 +63,7 @@ public class NRegister : IRegister { /// /// Memory start adress max 99999 /// Name of the register - public NRegister (int _adress, string _name = null) { + public NRegister(int _adress, string _name = null) { if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); @@ -113,18 +112,18 @@ internal NRegister(int _adress, string _name = null, bool isBitwise = false, Typ } isUsedBitwise = isBitwise; - enumType = _enumType; + enumType = _enumType; } - internal NRegister WithCollectionType (Type colType) { + internal NRegister WithCollectionType(Type colType) { collectionType = colType; return this; } - internal void SetValueFromPLC (object val) { + internal void SetValueFromPLC(object val) { lastValue = (T)val; TriggerChangedEvnt(this); @@ -132,7 +131,7 @@ internal void SetValueFromPLC (object val) { } - public string GetStartingMemoryArea () => this.MemoryAddress.ToString(); + public string GetStartingMemoryArea() => MemoryAddress.ToString(); public Type GetCollectionType() => CollectionType; @@ -141,10 +140,10 @@ internal void SetValueFromPLC (object val) { public string GetValueString() { //is number or bitwise - if(enumType == null) { + if (enumType == null) { return $"{Value}{(isUsedBitwise ? $" [{GetBitwise().ToBitString()}]" : "")}"; - + } //is enum @@ -164,11 +163,11 @@ public string GetValueString() { if (dict.ContainsKey(shortVal)) { return $"{Value} ({dict[shortVal]})"; - + } else { - + return $"{Value} (Missing Enum)"; - + } } @@ -228,7 +227,7 @@ public string BuildMewtocolQuery() { public string GetRegisterString() { - if(Value is short) { + if (Value is short) { return "DT"; } diff --git a/MewtocolNet/Mewtocol/Subregisters/NRegisterResult.cs b/MewtocolNet/Subregisters/NRegisterResult.cs similarity index 79% rename from MewtocolNet/Mewtocol/Subregisters/NRegisterResult.cs rename to MewtocolNet/Subregisters/NRegisterResult.cs index 74f7214..fa5e29d 100644 --- a/MewtocolNet/Mewtocol/Subregisters/NRegisterResult.cs +++ b/MewtocolNet/Subregisters/NRegisterResult.cs @@ -1,6 +1,4 @@ -using System; - -namespace MewtocolNet.Registers { +namespace MewtocolNet.Subregisters { /// /// Result for a read/write operation /// @@ -20,15 +18,15 @@ public class NRegisterResult { /// /// Trys to get the value of there is one /// - public bool TryGetValue (out T value) { + public bool TryGetValue(out T value) { - if(Result.Success) { + if (Result.Success) { value = (T)Register.Value; return true; } - value = default(T); + value = default; return false; - + } } diff --git a/MewtocolNet/Mewtocol/Subregisters/SRegister.cs b/MewtocolNet/Subregisters/SRegister.cs similarity index 92% rename from MewtocolNet/Mewtocol/Subregisters/SRegister.cs rename to MewtocolNet/Subregisters/SRegister.cs index 49c0d6e..49fa579 100644 --- a/MewtocolNet/Mewtocol/Subregisters/SRegister.cs +++ b/MewtocolNet/Subregisters/SRegister.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Text; -namespace MewtocolNet.Registers { +namespace MewtocolNet.Subregisters { /// /// Defines a register containing a string /// @@ -91,7 +91,7 @@ public string BuildMewtocolQuery() { return asciistring.ToString(); } - internal string BuildCustomIdent (int overwriteWordLength) { + internal string BuildCustomIdent(int overwriteWordLength) { if (overwriteWordLength <= 0) throw new Exception("overwriteWordLength cant be 0 or less"); @@ -108,16 +108,16 @@ internal string BuildCustomIdent (int overwriteWordLength) { public bool IsUsedBitwise() => false; - internal void SetValueFromPLC (string val) { + internal void SetValueFromPLC(string val) { lastValue = val; TriggerChangedEvnt(this); TriggerNotifyChange(); - + } - public string GetStartingMemoryArea() => this.MemoryAddress.ToString(); + public string GetStartingMemoryArea() => MemoryAddress.ToString(); public string GetValueString() => Value?.ToString() ?? ""; @@ -135,7 +135,7 @@ internal void SetValueFromPLC (string val) { public void TriggerNotifyChange() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value")); - public override string ToString () => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; + public override string ToString() => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; } diff --git a/MewtocolNet/Mewtocol/Subregisters/SRegisterResult.cs b/MewtocolNet/Subregisters/SRegisterResult.cs similarity index 90% rename from MewtocolNet/Mewtocol/Subregisters/SRegisterResult.cs rename to MewtocolNet/Subregisters/SRegisterResult.cs index 45129a6..9ffed98 100644 --- a/MewtocolNet/Mewtocol/Subregisters/SRegisterResult.cs +++ b/MewtocolNet/Subregisters/SRegisterResult.cs @@ -1,4 +1,4 @@ -namespace MewtocolNet.Registers { +namespace MewtocolNet.Subregisters { /// /// The results of a string register operation diff --git a/MewtocolTests/AutomatedPropertyRegisters.cs b/MewtocolTests/AutomatedPropertyRegisters.cs index 834a39c..5ae5d3e 100644 --- a/MewtocolTests/AutomatedPropertyRegisters.cs +++ b/MewtocolTests/AutomatedPropertyRegisters.cs @@ -1,14 +1,15 @@ using Xunit; using MewtocolNet; -using MewtocolNet.Registers; using System.Diagnostics; using Xunit.Abstractions; using System.Collections; using MewtocolNet.RegisterAttributes; using Microsoft.Win32; +using MewtocolNet.Subregisters; -namespace MewtocolTests { +namespace MewtocolTests +{ public class AutomatedPropertyRegisters { diff --git a/MewtocolTests/TestLinkedLists.cs b/MewtocolTests/TestLinkedLists.cs index 4f47f03..324cdf5 100644 --- a/MewtocolTests/TestLinkedLists.cs +++ b/MewtocolTests/TestLinkedLists.cs @@ -3,9 +3,9 @@ using MewtocolNet; using MewtocolNet.Registers; using Xunit.Abstractions; -using MewtocolNet.Links; -namespace MewtocolTests { +namespace MewtocolTests +{ public class TestLinkedLists { diff --git a/MewtocolTests/TestLivePLC.cs b/MewtocolTests/TestLivePLC.cs index eea5f52..79f9bcf 100644 --- a/MewtocolTests/TestLivePLC.cs +++ b/MewtocolTests/TestLivePLC.cs @@ -1,5 +1,6 @@ using MewtocolNet; using MewtocolNet.Logging; +using MewtocolNet.PLCEnums; using System; using System.Collections.Generic; using System.Linq; @@ -8,7 +9,8 @@ using Xunit; using Xunit.Abstractions; -namespace MewtocolTests { +namespace MewtocolTests +{ public class TestLivePLC { diff --git a/MewtocolTests/TestRegisterInterface.cs b/MewtocolTests/TestRegisterInterface.cs index d66daba..ec0f04f 100644 --- a/MewtocolTests/TestRegisterInterface.cs +++ b/MewtocolTests/TestRegisterInterface.cs @@ -61,6 +61,7 @@ public void PLCRegisterIdentifiers () { //boolean new BRegister(IOType.R, 0, 100), + new BRegister(IOType.R, 0, 0), new BRegister(IOType.X, 5), new BRegister(IOType.X, 0xA), new BRegister(IOType.X, 0xF, 109), @@ -82,6 +83,7 @@ public void PLCRegisterIdentifiers () { //boolean "R100", + "R0", "X5", "XA", "X109F", diff --git a/formatting_settings b/formatting_settings new file mode 100644 index 0000000..e140ae8 --- /dev/null +++ b/formatting_settings @@ -0,0 +1,229 @@ +# Entfernen Sie die folgende Zeile, wenn Sie EDITORCONFIG-Einstellungen von höheren Verzeichnissen vererben möchten. +root = true + +# C#-Dateien +[*.cs] + +#### Wichtige EditorConfig-Optionen #### + +# Einzüge und Abstände +indent_size = 4 +indent_style = space +tab_width = 4 + +# Einstellungen für neue Zeilen +end_of_line = crlf +insert_final_newline = false + +#### .NET-Codierungskonventionen #### + +# Using-Direktiven organisieren +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this.- und Me.-Einstellungen +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Einstellungen für Sprachschlüsselwörter und BCL-Typen +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Einstellungen für Klammern +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Einstellungen für Modifizierer +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Einstellungen für Ausdrucksebene +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Einstellungen für Felder +dotnet_style_readonly_field = true + +# Einstellungen für Parameter +dotnet_code_quality_unused_parameters = all + +# Unterdrückungseinstellungen +dotnet_remove_unnecessary_suppression_exclusions = none + +# Einstellungen für neue Zeilen +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C#-Codierungskonventionen #### + +# Var-Einstellungen +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Ausdruckskörpermember +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Einstellungen für den Musterabgleich +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Einstellungen für NULL-Überprüfung +csharp_style_conditional_delegate_call = true + +# Einstellungen für Modifizierer +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Einstellungen für Codeblöcke +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_style_namespace_declarations = file_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_top_level_statements = true + +# Einstellungen für Ausdrucksebene +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# Einstellungen für using-Anweisungen +csharp_using_directive_placement = outside_namespace + +# Einstellungen für neue Zeilen +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C#-Formatierungsregeln #### + +# Einstellungen für neue Zeilen +csharp_new_line_before_catch = false +csharp_new_line_before_else = false +csharp_new_line_before_finally = false +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = none +csharp_new_line_between_query_expression_clauses = true + +# Einstellungen für Einrückung +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = false +csharp_indent_case_contents_when_block = false +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Einstellungen für Abstände +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Umbrucheinstellungen +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Benennungsstile #### + +# Benennungsregeln + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbolspezifikationen + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Benennungsstile + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case From f5f1e3bea929df43f58ae40fb78dc2796096eac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Fri, 23 Jun 2023 15:45:14 +0200 Subject: [PATCH 03/18] Namespaces fixed --- MewtocolNet/DynamicInterface.cs | 2 +- MewtocolNet/MewtocolInterface.cs | 2 +- MewtocolNet/MewtocolInterfaceRequests.cs | 2 +- .../{Subregisters => Registers}/BRegister.cs | 2 +- .../BRegisterResult.cs | 2 +- .../{Subregisters => Registers}/NRegister.cs | 2 +- .../NRegisterResult.cs | 6 +-- .../{Subregisters => Registers}/SRegister.cs | 3 +- .../SRegisterResult.cs | 2 +- MewtocolTests/AutomatedPropertyRegisters.cs | 44 +++++++++---------- MewtocolTests/TestHelperExtensions.cs | 21 ++++----- MewtocolTests/TestLinkedLists.cs | 11 ++--- MewtocolTests/TestLivePLC.cs | 22 ++++------ MewtocolTests/TestRegisterInterface.cs | 17 ++++--- MewtocolTests/TestRegisterParsing.cs | 12 ++--- 15 files changed, 63 insertions(+), 87 deletions(-) rename MewtocolNet/{Subregisters => Registers}/BRegister.cs (99%) rename MewtocolNet/{Subregisters => Registers}/BRegisterResult.cs (90%) rename MewtocolNet/{Subregisters => Registers}/NRegister.cs (99%) rename MewtocolNet/{Subregisters => Registers}/NRegisterResult.cs (94%) rename MewtocolNet/{Subregisters => Registers}/SRegister.cs (99%) rename MewtocolNet/{Subregisters => Registers}/SRegisterResult.cs (90%) diff --git a/MewtocolNet/DynamicInterface.cs b/MewtocolNet/DynamicInterface.cs index f8726b0..f13ceff 100644 --- a/MewtocolNet/DynamicInterface.cs +++ b/MewtocolNet/DynamicInterface.cs @@ -1,5 +1,5 @@ using MewtocolNet.Logging; -using MewtocolNet.Subregisters; +using MewtocolNet.Registers; using System; using System.Collections.Generic; using System.Linq; diff --git a/MewtocolNet/MewtocolInterface.cs b/MewtocolNet/MewtocolInterface.cs index b5f28a6..81cd8ff 100644 --- a/MewtocolNet/MewtocolInterface.cs +++ b/MewtocolNet/MewtocolInterface.cs @@ -1,7 +1,7 @@ using MewtocolNet.Logging; using MewtocolNet.Queue; using MewtocolNet.RegisterAttributes; -using MewtocolNet.Subregisters; +using MewtocolNet.Registers; using System; using System.Collections; using System.Collections.Generic; diff --git a/MewtocolNet/MewtocolInterfaceRequests.cs b/MewtocolNet/MewtocolInterfaceRequests.cs index 94917db..90aab30 100644 --- a/MewtocolNet/MewtocolInterfaceRequests.cs +++ b/MewtocolNet/MewtocolInterfaceRequests.cs @@ -1,6 +1,6 @@ using MewtocolNet.Logging; using MewtocolNet.PLCEnums; -using MewtocolNet.Subregisters; +using MewtocolNet.Registers; using System; using System.Collections.Generic; using System.Globalization; diff --git a/MewtocolNet/Subregisters/BRegister.cs b/MewtocolNet/Registers/BRegister.cs similarity index 99% rename from MewtocolNet/Subregisters/BRegister.cs rename to MewtocolNet/Registers/BRegister.cs index 88b8590..d42cfd4 100644 --- a/MewtocolNet/Subregisters/BRegister.cs +++ b/MewtocolNet/Registers/BRegister.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Text; -namespace MewtocolNet.Subregisters { +namespace MewtocolNet.Registers { /// /// Defines a register containing a boolean diff --git a/MewtocolNet/Subregisters/BRegisterResult.cs b/MewtocolNet/Registers/BRegisterResult.cs similarity index 90% rename from MewtocolNet/Subregisters/BRegisterResult.cs rename to MewtocolNet/Registers/BRegisterResult.cs index ddc4477..8a39277 100644 --- a/MewtocolNet/Subregisters/BRegisterResult.cs +++ b/MewtocolNet/Registers/BRegisterResult.cs @@ -1,4 +1,4 @@ -namespace MewtocolNet.Subregisters { +namespace MewtocolNet.Registers { /// /// Result for a boolean register diff --git a/MewtocolNet/Subregisters/NRegister.cs b/MewtocolNet/Registers/NRegister.cs similarity index 99% rename from MewtocolNet/Subregisters/NRegister.cs rename to MewtocolNet/Registers/NRegister.cs index 1030cd6..5550e5a 100644 --- a/MewtocolNet/Subregisters/NRegister.cs +++ b/MewtocolNet/Registers/NRegister.cs @@ -4,7 +4,7 @@ using System.ComponentModel; using System.Text; -namespace MewtocolNet.Subregisters { +namespace MewtocolNet.Registers { /// /// Defines a register containing a number diff --git a/MewtocolNet/Subregisters/NRegisterResult.cs b/MewtocolNet/Registers/NRegisterResult.cs similarity index 94% rename from MewtocolNet/Subregisters/NRegisterResult.cs rename to MewtocolNet/Registers/NRegisterResult.cs index fa5e29d..367233a 100644 --- a/MewtocolNet/Subregisters/NRegisterResult.cs +++ b/MewtocolNet/Registers/NRegisterResult.cs @@ -1,4 +1,5 @@ -namespace MewtocolNet.Subregisters { +namespace MewtocolNet.Registers { + /// /// Result for a read/write operation /// @@ -31,7 +32,4 @@ public bool TryGetValue(out T value) { } - - - } diff --git a/MewtocolNet/Subregisters/SRegister.cs b/MewtocolNet/Registers/SRegister.cs similarity index 99% rename from MewtocolNet/Subregisters/SRegister.cs rename to MewtocolNet/Registers/SRegister.cs index 49fa579..00e7b2a 100644 --- a/MewtocolNet/Subregisters/SRegister.cs +++ b/MewtocolNet/Registers/SRegister.cs @@ -2,7 +2,8 @@ using System.ComponentModel; using System.Text; -namespace MewtocolNet.Subregisters { +namespace MewtocolNet.Registers { + /// /// Defines a register containing a string /// diff --git a/MewtocolNet/Subregisters/SRegisterResult.cs b/MewtocolNet/Registers/SRegisterResult.cs similarity index 90% rename from MewtocolNet/Subregisters/SRegisterResult.cs rename to MewtocolNet/Registers/SRegisterResult.cs index 9ffed98..45129a6 100644 --- a/MewtocolNet/Subregisters/SRegisterResult.cs +++ b/MewtocolNet/Registers/SRegisterResult.cs @@ -1,4 +1,4 @@ -namespace MewtocolNet.Subregisters { +namespace MewtocolNet.Registers { /// /// The results of a string register operation diff --git a/MewtocolTests/AutomatedPropertyRegisters.cs b/MewtocolTests/AutomatedPropertyRegisters.cs index 5ae5d3e..cd46f3f 100644 --- a/MewtocolTests/AutomatedPropertyRegisters.cs +++ b/MewtocolTests/AutomatedPropertyRegisters.cs @@ -1,15 +1,11 @@ -using Xunit; - using MewtocolNet; -using System.Diagnostics; -using Xunit.Abstractions; -using System.Collections; using MewtocolNet.RegisterAttributes; -using Microsoft.Win32; -using MewtocolNet.Subregisters; +using MewtocolNet.Registers; +using System.Collections; +using Xunit; +using Xunit.Abstractions; -namespace MewtocolTests -{ +namespace MewtocolTests { public class AutomatedPropertyRegisters { @@ -94,7 +90,7 @@ public enum CurrentState { public CurrentState TestEnum32 { get; private set; } } - + private void TestBasicGeneration(IRegister reg, string propName, object expectValue, int expectAddr, string expectPlcName) { Assert.NotNull(reg); @@ -111,7 +107,7 @@ private void TestBasicGeneration(IRegister reg, string propName, object expectVa //actual tests [Fact(DisplayName = "Boolean R generation")] - public void BooleanGen () { + public void BooleanGen() { var interf = new MewtocolInterface("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); @@ -124,7 +120,7 @@ public void BooleanGen () { } [Fact(DisplayName = "Boolean input XD generation")] - public void BooleanInputGen () { + public void BooleanInputGen() { var interf = new MewtocolInterface("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); @@ -137,7 +133,7 @@ public void BooleanInputGen () { } [Fact(DisplayName = "Int16 generation")] - public void Int16Gen () { + public void Int16Gen() { var interf = new MewtocolInterface("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); @@ -150,7 +146,7 @@ public void Int16Gen () { } [Fact(DisplayName = "UInt16 generation")] - public void UInt16Gen () { + public void UInt16Gen() { var interf = new MewtocolInterface("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); @@ -163,7 +159,7 @@ public void UInt16Gen () { } [Fact(DisplayName = "Int32 generation")] - public void Int32Gen () { + public void Int32Gen() { var interf = new MewtocolInterface("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); @@ -176,7 +172,7 @@ public void Int32Gen () { } [Fact(DisplayName = "UInt32 generation")] - public void UInt32Gen () { + public void UInt32Gen() { var interf = new MewtocolInterface("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); @@ -189,7 +185,7 @@ public void UInt32Gen () { } [Fact(DisplayName = "Float32 generation")] - public void Float32Gen () { + public void Float32Gen() { var interf = new MewtocolInterface("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); @@ -202,7 +198,7 @@ public void Float32Gen () { } [Fact(DisplayName = "String generation")] - public void StringGen () { + public void StringGen() { var interf = new MewtocolInterface("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); @@ -218,7 +214,7 @@ public void StringGen () { } [Fact(DisplayName = "BitArray 16bit generation")] - public void BitArray16Gen () { + public void BitArray16Gen() { var interf = new MewtocolInterface("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); @@ -234,7 +230,7 @@ public void BitArray16Gen () { } [Fact(DisplayName = "BitArray 32bit generation")] - public void BitArray32Gen () { + public void BitArray32Gen() { var interf = new MewtocolInterface("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); @@ -250,7 +246,7 @@ public void BitArray32Gen () { } [Fact(DisplayName = "BitArray single bool generation")] - public void BitArraySingleBool16Gen () { + public void BitArraySingleBool16Gen() { var interf = new MewtocolInterface("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); @@ -265,7 +261,7 @@ public void BitArraySingleBool16Gen () { } [Fact(DisplayName = "TimeSpan generation")] - public void TimespanGen () { + public void TimespanGen() { var interf = new MewtocolInterface("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); @@ -278,7 +274,7 @@ public void TimespanGen () { } [Fact(DisplayName = "Enum16 generation")] - public void Enum16Gen () { + public void Enum16Gen() { var interf = new MewtocolInterface("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); @@ -291,7 +287,7 @@ public void Enum16Gen () { } [Fact(DisplayName = "Enum32 generation")] - public void Enum32Gen () { + public void Enum32Gen() { var interf = new MewtocolInterface("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); diff --git a/MewtocolTests/TestHelperExtensions.cs b/MewtocolTests/TestHelperExtensions.cs index b39e65e..26ab943 100644 --- a/MewtocolTests/TestHelperExtensions.cs +++ b/MewtocolTests/TestHelperExtensions.cs @@ -1,10 +1,7 @@ -using Xunit; - using MewtocolNet; -using MewtocolNet.Registers; -using Xunit.Abstractions; -using MewtocolNet.Links; using System.Collections; +using Xunit; +using Xunit.Abstractions; namespace MewtocolTests { @@ -12,12 +9,12 @@ public class TestHelperExtensions { private readonly ITestOutputHelper output; - public TestHelperExtensions (ITestOutputHelper output) { + public TestHelperExtensions(ITestOutputHelper output) { this.output = output; } [Fact(DisplayName = nameof(MewtocolHelpers.ToBitString))] - public void ToBitStringGeneration () { + public void ToBitStringGeneration() { var bitarr = new BitArray(16); bitarr[2] = true; @@ -31,7 +28,7 @@ public void ToBitStringGeneration () { } [Fact(DisplayName = nameof(MewtocolHelpers.ToHexString))] - public void ToHexStringGeneration () { + public void ToHexStringGeneration() { var bytes = new byte[6] { 0x10, @@ -47,7 +44,7 @@ public void ToHexStringGeneration () { } [Fact(DisplayName = nameof(MewtocolHelpers.ToHexASCIIBytes))] - public void ToHexASCIIBytesGeneration () { + public void ToHexASCIIBytesGeneration() { string test = "Hello, world!"; @@ -70,7 +67,7 @@ public void ToHexASCIIBytesGeneration () { } [Fact(DisplayName = nameof(MewtocolHelpers.BuildBCCFrame))] - public void BuildBCCFrameGeneration () { + public void BuildBCCFrameGeneration() { string test = "%01#RCSX0000"; string expect = "%01#RCSX00001D"; @@ -80,7 +77,7 @@ public void BuildBCCFrameGeneration () { } [Fact(DisplayName = nameof(MewtocolHelpers.ParseDTByteString))] - public void ParseDTByteStringGeneration () { + public void ParseDTByteStringGeneration() { var testList = new List() { "1112", @@ -97,7 +94,7 @@ public void ParseDTByteStringGeneration () { } [Fact(DisplayName = nameof(MewtocolHelpers.ParseRCSingleBit))] - public void ParseRCSingleBitGeneration () { + public void ParseRCSingleBitGeneration() { Assert.True($"%01$RC1".BuildBCCFrame().ParseRCSingleBit()); Assert.False($"%01$RC0".BuildBCCFrame().ParseRCSingleBit()); diff --git a/MewtocolTests/TestLinkedLists.cs b/MewtocolTests/TestLinkedLists.cs index 324cdf5..0f24484 100644 --- a/MewtocolTests/TestLinkedLists.cs +++ b/MewtocolTests/TestLinkedLists.cs @@ -1,22 +1,19 @@ -using Xunit; - using MewtocolNet; -using MewtocolNet.Registers; +using Xunit; using Xunit.Abstractions; -namespace MewtocolTests -{ +namespace MewtocolTests { public class TestLinkedLists { private readonly ITestOutputHelper output; - public TestLinkedLists (ITestOutputHelper output) { + public TestLinkedLists(ITestOutputHelper output) { this.output = output; } [Fact(DisplayName = "Linked error list")] - public void NumericRegisterMewtocolIdentifiers () { + public void NumericRegisterMewtocolIdentifiers() { var expectedData = new Dictionary { diff --git a/MewtocolTests/TestLivePLC.cs b/MewtocolTests/TestLivePLC.cs index 79f9bcf..b4d40f1 100644 --- a/MewtocolTests/TestLivePLC.cs +++ b/MewtocolTests/TestLivePLC.cs @@ -1,16 +1,10 @@ using MewtocolNet; using MewtocolNet.Logging; using MewtocolNet.PLCEnums; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; -namespace MewtocolTests -{ +namespace MewtocolTests { public class TestLivePLC { @@ -39,21 +33,21 @@ public class TestLivePLC { }; - public TestLivePLC (ITestOutputHelper output) { + public TestLivePLC(ITestOutputHelper output) { this.output = output; Logger.LogLevel = LogLevel.Verbose; - Logger.OnNewLogMessage((d,m) => { + Logger.OnNewLogMessage((d, m) => { output.WriteLine($"Mewtocol Logger: {d} {m}"); }); - + } [Fact(DisplayName = "Connection cycle client to PLC")] - public async void TestClientConnection () { + public async void TestClientConnection() { foreach (var plc in testData) { @@ -74,7 +68,7 @@ public async void TestClientConnection () { } [Fact(DisplayName = "Reading basic information from PLC")] - public async void TestClientReadPLCStatus () { + public async void TestClientReadPLCStatus() { foreach (var plc in testData) { @@ -107,9 +101,9 @@ public class ExpectedTestData { public int PLCPort { get; set; } - public CpuType Type { get; set; } + public CpuType Type { get; set; } - public int ProgCapacity { get; set; } + public int ProgCapacity { get; set; } } diff --git a/MewtocolTests/TestRegisterInterface.cs b/MewtocolTests/TestRegisterInterface.cs index ec0f04f..bf11d1b 100644 --- a/MewtocolTests/TestRegisterInterface.cs +++ b/MewtocolTests/TestRegisterInterface.cs @@ -1,7 +1,6 @@ -using Xunit; - using MewtocolNet; using MewtocolNet.Registers; +using Xunit; using Xunit.Abstractions; namespace MewtocolTests { @@ -10,12 +9,12 @@ public class TestRegisterInterface { private readonly ITestOutputHelper output; - public TestRegisterInterface (ITestOutputHelper output) { + public TestRegisterInterface(ITestOutputHelper output) { this.output = output; } [Fact(DisplayName = "Numeric mewtocol query building")] - public void NumericRegisterMewtocolIdentifiers () { + public void NumericRegisterMewtocolIdentifiers() { List registers = new List { new NRegister(50, _name: null), @@ -48,7 +47,7 @@ public void NumericRegisterMewtocolIdentifiers () { } [Fact(DisplayName = "PLC register naming convention test")] - public void PLCRegisterIdentifiers () { + public void PLCRegisterIdentifiers() { List registers = new List { //numeric ones @@ -77,8 +76,8 @@ public void PLCRegisterIdentifiers () { "DT50", "DT60", "DDT70", - "DDT80", - "DDT90", + "DDT80", + "DDT90", "DDT100", //boolean @@ -107,7 +106,7 @@ public void PLCRegisterIdentifiers () { } [Fact(DisplayName = "Non allowed (Overflow address)")] - public void OverFlowRegisterAddress () { + public void OverFlowRegisterAddress() { var ex = Assert.Throws(() => { @@ -144,7 +143,7 @@ public void OverFlowRegisterAddress () { } [Fact(DisplayName = "Non allowed (Wrong data type)")] - public void WrongDataTypeRegister () { + public void WrongDataTypeRegister() { var ex = Assert.Throws(() => { diff --git a/MewtocolTests/TestRegisterParsing.cs b/MewtocolTests/TestRegisterParsing.cs index 94953fd..41687f1 100644 --- a/MewtocolTests/TestRegisterParsing.cs +++ b/MewtocolTests/TestRegisterParsing.cs @@ -1,11 +1,5 @@ using MewtocolNet; -using MewtocolNet.Mewtocol; using MewtocolNet.Registers; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -15,12 +9,12 @@ public class TestRegisterParsing { private readonly ITestOutputHelper output; - public TestRegisterParsing (ITestOutputHelper output) { + public TestRegisterParsing(ITestOutputHelper output) { this.output = output; } [Fact(DisplayName = "Parsing as BRegister (Phyiscal Outputs)")] - public void TestParsingBRegisterY () { + public void TestParsingBRegisterY() { var tests = new Dictionary() { @@ -51,7 +45,7 @@ public void TestParsingBRegisterY () { } [Fact(DisplayName = "Parsing as BRegister (Phyiscal Inputs)")] - public void TestParsingBRegisterX () { + public void TestParsingBRegisterX() { var tests = new Dictionary() { From b48f86d23d9f6cb20aca66a4e5836c10c6903ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Sun, 25 Jun 2023 22:45:07 +0200 Subject: [PATCH 04/18] Added register builder for booleans and numerics --- Examples/ExampleScenarios.cs | 2 + Examples/Program.cs | 7 +- MewtocolNet/CpuInfo.cs | 3 +- MewtocolNet/IRegister.cs | 16 ++ MewtocolNet/MewtocolHelpers.cs | 13 +- MewtocolNet/MewtocolInterfaceRequests.cs | 1 - MewtocolNet/PLCEnums/CpuType.cs | 2 +- MewtocolNet/PLCEnums/OPMode.cs | 2 +- MewtocolNet/PLCEnums/PlcVarType.cs | 18 ++ MewtocolNet/PLCEnums/PlcVarTypeConversions.cs | 91 ++++++++ .../RegisterBuilding/FinalizerExtensions.cs | 97 +++++++++ MewtocolNet/RegisterBuilding/ParseResult.cs | 12 ++ .../RegisterBuilding/ParseResultState.cs | 19 ++ MewtocolNet/RegisterBuilding/RegBuilder.cs | 184 ++++++++++++++++ .../RegisterBuilding/RegisterBuilder.cs | 24 --- .../RegisterBuilding/RegisterBuilderStep.cs | 92 ++++++++ MewtocolNet/RegisterEnums.cs | 23 +- MewtocolNet/Registers/BRegister.cs | 28 ++- MewtocolNet/Registers/NRegister.cs | 26 ++- MewtocolNet/Registers/SRegister.cs | 10 +- MewtocolTests/TestLivePLC.cs | 1 - MewtocolTests/TestRegisterBuilder.cs | 204 ++++++++++++++++++ MewtocolTests/TestRegisterParsing.cs | 78 ------- 23 files changed, 821 insertions(+), 132 deletions(-) create mode 100644 MewtocolNet/PLCEnums/PlcVarType.cs create mode 100644 MewtocolNet/PLCEnums/PlcVarTypeConversions.cs create mode 100644 MewtocolNet/RegisterBuilding/FinalizerExtensions.cs create mode 100644 MewtocolNet/RegisterBuilding/ParseResult.cs create mode 100644 MewtocolNet/RegisterBuilding/ParseResultState.cs create mode 100644 MewtocolNet/RegisterBuilding/RegBuilder.cs delete mode 100644 MewtocolNet/RegisterBuilding/RegisterBuilder.cs create mode 100644 MewtocolNet/RegisterBuilding/RegisterBuilderStep.cs create mode 100644 MewtocolTests/TestRegisterBuilder.cs delete mode 100644 MewtocolTests/TestRegisterParsing.cs diff --git a/Examples/ExampleScenarios.cs b/Examples/ExampleScenarios.cs index a0ccdf9..bd2244a 100644 --- a/Examples/ExampleScenarios.cs +++ b/Examples/ExampleScenarios.cs @@ -4,6 +4,8 @@ using System.Reflection; using System.Threading.Tasks; using System.Collections; +using MewtocolNet.RegisterBuilding; +using System.Collections.Generic; namespace Examples; diff --git a/Examples/Program.cs b/Examples/Program.cs index c3bd799..a868993 100644 --- a/Examples/Program.cs +++ b/Examples/Program.cs @@ -1,4 +1,6 @@ -using System; +using MewtocolNet.RegisterBuilding; +using MewtocolNet; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -12,6 +14,9 @@ class Program { static void Main(string[] args) { + RegBuilder.FromPlcRegName("DT303").AsPlcType(PlcVarType.INT).Build(); + var res = RegBuilder.FromPlcRegName("DT100").AsPlcType(PlcVarType.INT).Build(); + AppDomain.CurrentDomain.UnhandledException += (s,e) => { Console.WriteLine(e.ExceptionObject.ToString()); }; diff --git a/MewtocolNet/CpuInfo.cs b/MewtocolNet/CpuInfo.cs index 4f21723..001d491 100644 --- a/MewtocolNet/CpuInfo.cs +++ b/MewtocolNet/CpuInfo.cs @@ -1,5 +1,4 @@ -using MewtocolNet.PLCEnums; -using System; +using System; namespace MewtocolNet { diff --git a/MewtocolNet/IRegister.cs b/MewtocolNet/IRegister.cs index c771084..f68d60d 100644 --- a/MewtocolNet/IRegister.cs +++ b/MewtocolNet/IRegister.cs @@ -12,6 +12,11 @@ public interface IRegister { /// event Action ValueChanged; + /// + /// Type of the underlying register + /// + RegisterType RegisterType { get; } + /// /// The name of the register /// @@ -27,6 +32,12 @@ public interface IRegister { /// int MemoryAddress { get; } + /// + /// Gets the special address of the register or -1 if it has none + /// + /// + byte? GetSpecialAddress(); + /// /// Indicates if the register is processed bitwise /// @@ -93,6 +104,11 @@ public interface IRegister { /// string ToString(); + /// + /// Builds a readable string with all important register informations and additional infos + /// + string ToString(bool detailed); + } } diff --git a/MewtocolNet/MewtocolHelpers.cs b/MewtocolNet/MewtocolHelpers.cs index e3f0d2f..faee445 100644 --- a/MewtocolNet/MewtocolHelpers.cs +++ b/MewtocolNet/MewtocolHelpers.cs @@ -239,14 +239,23 @@ internal static bool IsNumericSupportedType(this Type type) { } /// - /// Checks if the register type is non numeric + /// Checks if the register type is boolean /// - internal static bool IsBoolean(this RegisterType type) { + internal static bool IsBoolean (this RegisterType type) { return type == RegisterType.X || type == RegisterType.Y || type == RegisterType.R; } + /// + /// Checks if the register type numeric + /// + internal static bool IsNumericDTDDT (this RegisterType type) { + + return type == RegisterType.DT || type == RegisterType.DDT; + + } + /// /// Checks if the register type is an physical in or output of the plc /// diff --git a/MewtocolNet/MewtocolInterfaceRequests.cs b/MewtocolNet/MewtocolInterfaceRequests.cs index 90aab30..caa9e5c 100644 --- a/MewtocolNet/MewtocolInterfaceRequests.cs +++ b/MewtocolNet/MewtocolInterfaceRequests.cs @@ -1,5 +1,4 @@ using MewtocolNet.Logging; -using MewtocolNet.PLCEnums; using MewtocolNet.Registers; using System; using System.Collections.Generic; diff --git a/MewtocolNet/PLCEnums/CpuType.cs b/MewtocolNet/PLCEnums/CpuType.cs index cc5bcef..9312177 100644 --- a/MewtocolNet/PLCEnums/CpuType.cs +++ b/MewtocolNet/PLCEnums/CpuType.cs @@ -1,4 +1,4 @@ -namespace MewtocolNet.PLCEnums { +namespace MewtocolNet { /// /// CPU type of the PLC diff --git a/MewtocolNet/PLCEnums/OPMode.cs b/MewtocolNet/PLCEnums/OPMode.cs index 9ae73f1..e760230 100644 --- a/MewtocolNet/PLCEnums/OPMode.cs +++ b/MewtocolNet/PLCEnums/OPMode.cs @@ -1,4 +1,4 @@ -namespace MewtocolNet.PLCEnums { +namespace MewtocolNet { /// /// CPU type of the PLC diff --git a/MewtocolNet/PLCEnums/PlcVarType.cs b/MewtocolNet/PLCEnums/PlcVarType.cs new file mode 100644 index 0000000..d3e13a2 --- /dev/null +++ b/MewtocolNet/PLCEnums/PlcVarType.cs @@ -0,0 +1,18 @@ +using System.Text; + +namespace MewtocolNet { + + public enum PlcVarType { + + BOOL, + INT, + UINT, + DINT, + UDINT, + REAL, + TIME, + STRING + + } + +} diff --git a/MewtocolNet/PLCEnums/PlcVarTypeConversions.cs b/MewtocolNet/PLCEnums/PlcVarTypeConversions.cs new file mode 100644 index 0000000..4c6653b --- /dev/null +++ b/MewtocolNet/PLCEnums/PlcVarTypeConversions.cs @@ -0,0 +1,91 @@ +using MewtocolNet.Registers; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MewtocolNet { + internal static class PlcVarTypeConversions { + + static Dictionary dictTypeConv = new Dictionary { + + { PlcVarType.BOOL, typeof(bool) }, + { PlcVarType.INT, typeof(short) }, + { PlcVarType.UINT, typeof(ushort) }, + { PlcVarType.DINT, typeof(int) }, + { PlcVarType.UDINT, typeof(uint) }, + { PlcVarType.REAL, typeof(float) }, + { PlcVarType.TIME, typeof(TimeSpan) }, + { PlcVarType.STRING, typeof(string) }, + + }; + + static Dictionary dictRegisterConv = new Dictionary { + + { PlcVarType.BOOL, typeof(BRegister) }, + { PlcVarType.INT, typeof(NRegister) }, + { PlcVarType.UINT, typeof(NRegister) }, + { PlcVarType.DINT, typeof(NRegister) }, + { PlcVarType.UDINT, typeof(NRegister) }, + { PlcVarType.REAL, typeof(NRegister) }, + { PlcVarType.TIME, typeof(NRegister) }, + { PlcVarType.STRING, typeof(SRegister) }, + + }; + + internal static bool IsAllowedPlcCastingType () { + + var inversed = dictTypeConv.ToDictionary((i) => i.Value, (i) => i.Key); + + return inversed.ContainsKey(typeof(T)); + + } + + internal static bool IsAllowedPlcCastingType (this Type type) { + + var inversed = dictTypeConv.ToDictionary((i) => i.Value, (i) => i.Key); + + return inversed.ContainsKey(type); + + } + + internal static Type ToDotnetType (this PlcVarType type) { + + if(dictTypeConv.ContainsKey(type)) { + + return dictTypeConv[type]; + + } + + throw new NotSupportedException($"The PlcVarType: '{type}' is not supported"); + + } + + internal static PlcVarType ToPlcVarType (this Type type) { + + var inversed = dictTypeConv.ToDictionary((i) => i.Value, (i) => i.Key); + + if (inversed.ContainsKey(type)) { + + return inversed[type]; + + } + + throw new NotSupportedException($"The Dotnet Type: '{type}' is not supported"); + + } + + internal static Type ToRegisterType (this PlcVarType type) { + + if (dictRegisterConv.ContainsKey(type)) { + + return dictRegisterConv[type]; + + } + + throw new NotSupportedException($"The PlcVarType: '{type}' is not supported"); + + } + + } + +} diff --git a/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs b/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs new file mode 100644 index 0000000..b42a30e --- /dev/null +++ b/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs @@ -0,0 +1,97 @@ +using MewtocolNet.Registers; +using System; +using System.Reflection; + +namespace MewtocolNet.RegisterBuilding { + + public static class FinalizerExtensions { + + public static IRegister Build (this RegisterBuilderStep step) { + + //if no casting method in builder was called => autocast the type from the RegisterType + if (!step.wasCasted) + step.AutoType(); + + bool isBoolean = step.RegType.IsBoolean(); + bool isTypeNotDefined = step.plcVarType == null && step.dotnetVarType == null; + + //fallbacks if no casting builder was given + if (isTypeNotDefined && step.RegType == RegisterType.DT) { + + step.dotnetVarType = typeof(short); + + } + if (isTypeNotDefined && step.RegType == RegisterType.DDT) { + + step.dotnetVarType = typeof(int); + + } else if (isTypeNotDefined && isBoolean) { + + step.dotnetVarType = typeof(bool); + + } else if (isTypeNotDefined && step.RegType == RegisterType.DT_START) { + + step.dotnetVarType = typeof(string); + + } + + if(step.plcVarType != null) { + + step.dotnetVarType = step.plcVarType.Value.ToDotnetType(); + + } + + //as numeric register + if (step.RegType.IsNumericDTDDT()) { + + if(step.plcVarType == null && step.dotnetVarType != null) { + + step.plcVarType = step.dotnetVarType.ToPlcVarType(); + + } + + var type = step.plcVarType.Value.ToRegisterType(); + + var areaAddr = step.MemAddress; + var name = step.Name; + + //create a new bregister instance + var flags = BindingFlags.Public | BindingFlags.Instance; + + //int _adress, string _name = null, bool isBitwise = false, Type _enumType = null + var parameters = new object[] { areaAddr, name, false, null }; + var instance = (IRegister)Activator.CreateInstance(type, flags, null, parameters, null); + + return instance; + + } + + if (step.RegType.IsBoolean()) { + + var io = (IOType)(int)step.RegType; + var spAddr = step.SpecialAddress; + var areaAddr = step.MemAddress; + var name = step.Name; + + //create a new bregister instance + var flags = BindingFlags.Public | BindingFlags.Instance; + var parameters = new object[] { io, spAddr.Value, areaAddr, name }; + var instance = (BRegister)Activator.CreateInstance(typeof(BRegister), flags, null, parameters, null); + + return instance; + + } + + if (step.dotnetVarType != null) { + + + + } + + throw new Exception("Failed to build register"); + + } + + } + +} diff --git a/MewtocolNet/RegisterBuilding/ParseResult.cs b/MewtocolNet/RegisterBuilding/ParseResult.cs new file mode 100644 index 0000000..d548d2c --- /dev/null +++ b/MewtocolNet/RegisterBuilding/ParseResult.cs @@ -0,0 +1,12 @@ +namespace MewtocolNet.RegisterBuilding { + internal struct ParseResult { + + public ParseResultState state; + + public string hardFailReason; + + public RegisterBuilderStep stepData; + + } + +} diff --git a/MewtocolNet/RegisterBuilding/ParseResultState.cs b/MewtocolNet/RegisterBuilding/ParseResultState.cs new file mode 100644 index 0000000..142ac3c --- /dev/null +++ b/MewtocolNet/RegisterBuilding/ParseResultState.cs @@ -0,0 +1,19 @@ +namespace MewtocolNet.RegisterBuilding { + internal enum ParseResultState { + + /// + /// The parse try failed at the intial regex match + /// + FailedSoft, + /// + /// The parse try failed at the afer- regex match + /// + FailedHard, + /// + /// The parse try did work + /// + Success, + + } + +} diff --git a/MewtocolNet/RegisterBuilding/RegBuilder.cs b/MewtocolNet/RegisterBuilding/RegBuilder.cs new file mode 100644 index 0000000..ce91a17 --- /dev/null +++ b/MewtocolNet/RegisterBuilding/RegBuilder.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace MewtocolNet.RegisterBuilding { + + /// + /// Contains useful tools for register creation + /// + public static class RegBuilder { + + //methods to test the input string on + private static List> parseMethods = new List>() { + + (x) => TryBuildBoolean(x), + (x) => TryBuildNumericBased(x), + + }; + + public static RegisterBuilderStep FromPlcRegName (string plcAddrName, string name = null) { + + foreach (var method in parseMethods) { + + var res = method.Invoke(plcAddrName); + + if(res.state == ParseResultState.Success) { + + if (!string.IsNullOrEmpty(name)) + res.stepData.Name = name; + + res.stepData.OriginalInput = plcAddrName; + + return res.stepData; + + } else if(res.state == ParseResultState.FailedHard) { + + throw new Exception(res.hardFailReason); + + } + + } + + throw new Exception("Wrong input format"); + + } + + //bool registers + private static ParseResult TryBuildBoolean (string plcAddrName) { + + //regex to find special register values + var patternBool = new Regex(@"(?X|Y|R)(?[0-9]{0,3})(?(?:[0-9]|[A-F]){1})?"); + + var match = patternBool.Match(plcAddrName); + + if (!match.Success) + return new ParseResult { + state = ParseResultState.FailedSoft + }; + + string prefix = match.Groups["prefix"].Value; + string area = match.Groups["area"].Value; + string special = match.Groups["special"].Value; + + IOType regType; + int areaAdd = 0; + byte specialAdd = 0x0; + + //try cast the prefix + if(!Enum.TryParse(prefix, out regType)) { + + return new ParseResult { + state = ParseResultState.FailedHard, + hardFailReason = $"Cannot parse '{plcAddrName}', the prefix is not allowed for boolean registers" + }; + + } + + if(!string.IsNullOrEmpty(area) && !int.TryParse(area, out areaAdd) ) { + + return new ParseResult { + state = ParseResultState.FailedHard, + hardFailReason = $"Cannot parse '{plcAddrName}', the area address: '{area}' is wrong" + }; + + } + + //special address not given + if(string.IsNullOrEmpty(special) && !string.IsNullOrEmpty(area)) { + + var isAreaInt = int.TryParse(area, NumberStyles.Number, CultureInfo.InvariantCulture, out var areaInt); + + if (isAreaInt && areaInt >= 0 && areaInt <= 9) { + + //area address is actually meant as special address but 0-9 + specialAdd = (byte)areaInt; + areaAdd = 0; + + + } else if (isAreaInt && areaInt > 9) { + + //area adress is meant to be the actual area address + areaAdd = areaInt; + specialAdd = 0; + + } else { + + return new ParseResult { + state = ParseResultState.FailedHard, + hardFailReason = $"Cannot parse '{plcAddrName}', the special address: '{special}' is wrong 1", + }; + + } + + } else { + + //special address parsed as hex num + if (!byte.TryParse(special, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out specialAdd)) { + + return new ParseResult { + state = ParseResultState.FailedHard, + hardFailReason = $"Cannot parse '{plcAddrName}', the special address: '{special}' is wrong 2", + }; + + } + + } + + return new ParseResult { + state = ParseResultState.Success, + stepData = new RegisterBuilderStep ((RegisterType)(int)regType, areaAdd, specialAdd), + }; + + } + + // one to two word registers + private static ParseResult TryBuildNumericBased (string plcAddrName) { + + var patternByte = new Regex(@"(?DT|DDT)(?[0-9]{1,5})"); + + var match = patternByte.Match(plcAddrName); + + if (!match.Success) + return new ParseResult { + state = ParseResultState.FailedSoft + }; + + + string prefix = match.Groups["prefix"].Value; + string area = match.Groups["area"].Value; + + RegisterType regType; + int areaAdd = 0; + + //try cast the prefix + if (!Enum.TryParse(prefix, out regType)) { + + return new ParseResult { + state = ParseResultState.FailedHard, + hardFailReason = $"Cannot parse '{plcAddrName}', the prefix is not allowed for numeric registers" + }; + + } + + if (!string.IsNullOrEmpty(area) && !int.TryParse(area, out areaAdd)) { + + return new ParseResult { + state = ParseResultState.FailedHard, + hardFailReason = $"Cannot parse '{plcAddrName}', the area address: '{area}' is wrong" + }; + + } + + return new ParseResult { + state = ParseResultState.Success, + stepData = new RegisterBuilderStep(regType, areaAdd), + }; + + } + + } + +} diff --git a/MewtocolNet/RegisterBuilding/RegisterBuilder.cs b/MewtocolNet/RegisterBuilding/RegisterBuilder.cs deleted file mode 100644 index 96cbf67..0000000 --- a/MewtocolNet/RegisterBuilding/RegisterBuilder.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MewtocolNet.RegisterBuilding { - - /// - /// Contains useful tools for register creation - /// - public static class RegisterBuilder { - - /// - /// Parses a register from its PLC name - /// - /// The name, fe. DT100 - /// An or null if - /// True if successfully parsed - public static bool TryBuildFromName(string name, out IRegister reg) { - - reg = null; - - return false; - - } - - } - -} diff --git a/MewtocolNet/RegisterBuilding/RegisterBuilderStep.cs b/MewtocolNet/RegisterBuilding/RegisterBuilderStep.cs new file mode 100644 index 0000000..e1c2103 --- /dev/null +++ b/MewtocolNet/RegisterBuilding/RegisterBuilderStep.cs @@ -0,0 +1,92 @@ +using System; + +namespace MewtocolNet.RegisterBuilding { + public class RegisterBuilderStep { + + internal bool wasCasted = false; + + internal string OriginalInput; + + internal string Name; + internal RegisterType RegType; + internal int MemAddress; + internal byte? SpecialAddress; + + internal PlcVarType? plcVarType; + internal Type dotnetVarType; + + public RegisterBuilderStep () => throw new NotSupportedException("Cant make a new instance of RegisterBuilderStep, use the builder pattern"); + + internal RegisterBuilderStep (RegisterType regType, int memAddr) { + + RegType = regType; + MemAddress = memAddr; + + } + + internal RegisterBuilderStep(RegisterType regType, int memAddr, byte specialAddr) { + + RegType = regType; + MemAddress = memAddr; + SpecialAddress = specialAddr; + + } + + public RegisterBuilderStep AsPlcType (PlcVarType varType) { + + dotnetVarType = null; + plcVarType = varType; + + wasCasted = true; + + return this; + + } + + public RegisterBuilderStep AsType () { + + if(!typeof(T).IsAllowedPlcCastingType()) { + + throw new NotSupportedException($"The dotnet type {typeof(T)}, is not supported for PLC type casting"); + + } + + dotnetVarType = typeof(T); + plcVarType = null; + + wasCasted = true; + + return this; + + } + + internal RegisterBuilderStep AutoType () { + + switch (RegType) { + case RegisterType.X: + case RegisterType.Y: + case RegisterType.R: + dotnetVarType = typeof(bool); + break; + case RegisterType.DT: + dotnetVarType = typeof(short); + break; + case RegisterType.DDT: + dotnetVarType = typeof(int); + break; + case RegisterType.DT_START: + dotnetVarType = typeof(string); + break; + } + + plcVarType = null; + + wasCasted = true; + + return this; + + } + + } + +} diff --git a/MewtocolNet/RegisterEnums.cs b/MewtocolNet/RegisterEnums.cs index 55c3528..1195f35 100644 --- a/MewtocolNet/RegisterEnums.cs +++ b/MewtocolNet/RegisterEnums.cs @@ -18,28 +18,23 @@ public enum RegisterType { /// R = 2, /// - /// Data area as a short (Register) + /// Single word area (Register) /// - DT_short = 3, + DT = 3, /// - /// Data area as an unsigned short (Register) + /// Double word area (Register) /// - DT_ushort = 4, + DDT = 4, /// - /// Double data area as an integer (Register) + /// Start area of a byte sequence longer than 2 words /// - DDT_int = 5, - /// - /// Double data area as an unsigned integer (Register) - /// - DDT_uint = 6, - /// - /// Double data area as an floating point number (Register) - /// - DDT_float = 7, + DT_START = 5, } + // this is just used as syntactic sugar, + // when creating registers that are R/X/Y typed you dont need the DT types + /// /// The type of an input/output register /// diff --git a/MewtocolNet/Registers/BRegister.cs b/MewtocolNet/Registers/BRegister.cs index d42cfd4..3e83adb 100644 --- a/MewtocolNet/Registers/BRegister.cs +++ b/MewtocolNet/Registers/BRegister.cs @@ -19,7 +19,7 @@ public class BRegister : IRegister, INotifyPropertyChanged { /// public event PropertyChangedEventHandler PropertyChanged; - internal RegisterType RegType { get; private set; } + public RegisterType RegisterType { get; private set; } internal Type collectionType; @@ -80,7 +80,7 @@ public BRegister(IOType _io, byte _spAddress = 0x0, int _areaAdress = 0, string specialAddress = _spAddress; name = _name; - RegType = (RegisterType)(int)_io; + RegisterType = (RegisterType)(int)_io; } @@ -91,13 +91,15 @@ internal BRegister WithCollectionType(Type colType) { } + public byte? GetSpecialAddress() => SpecialAddress; + /// /// Builds the register area name /// public string BuildMewtocolQuery() { //build area code from register type - StringBuilder asciistring = new StringBuilder(RegType.ToString()); + StringBuilder asciistring = new StringBuilder(RegisterType.ToString()); string memPadded = MemoryAddress.ToString().PadLeft(4, '0'); string sp = SpecialAddress.ToString("X1"); @@ -127,11 +129,13 @@ public string GetStartingMemoryArea() { public Type GetCollectionType() => CollectionType; + public RegisterType GetRegisterType() => RegisterType; + public string GetValueString() => Value.ToString(); public void ClearValue() => SetValueFromPLC(false); - public string GetRegisterString() => RegType.ToString(); + public string GetRegisterString() => RegisterType.ToString(); public string GetCombinedName() => $"{(CollectionType != null ? $"{CollectionType.Name}." : "")}{Name ?? "Unnamed"}"; @@ -163,6 +167,22 @@ public string GetRegisterPLCName() { public override string ToString() => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; + public string ToString(bool additional) { + + if (!additional) return this.ToString(); + + StringBuilder sb = new StringBuilder(); + sb.AppendLine($"PLC Naming: {GetRegisterPLCName()}"); + sb.AppendLine($"Name: {Name ?? "Not named"}"); + sb.AppendLine($"Value: {GetValueString()}"); + sb.AppendLine($"Register Type: {RegisterType}"); + sb.AppendLine($"Memory Address: {MemoryAddress}"); + sb.AppendLine($"Special Address: {SpecialAddress:X1}"); + + return sb.ToString(); + + } + } } diff --git a/MewtocolNet/Registers/NRegister.cs b/MewtocolNet/Registers/NRegister.cs index 5550e5a..c36c0d4 100644 --- a/MewtocolNet/Registers/NRegister.cs +++ b/MewtocolNet/Registers/NRegister.cs @@ -22,6 +22,8 @@ public class NRegister : IRegister { /// public event PropertyChangedEventHandler PropertyChanged; + public RegisterType RegisterType { get; private set; } + internal Type collectionType; /// @@ -63,7 +65,7 @@ public class NRegister : IRegister { /// /// Memory start adress max 99999 /// Name of the register - public NRegister(int _adress, string _name = null) { + public NRegister (int _adress, string _name = null) { if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); @@ -87,9 +89,16 @@ public NRegister(int _adress, string _name = null) { throw new NotSupportedException($"The type {numType} is not allowed for Number Registers"); } + //set register type + if(memoryLength == 1) { + RegisterType = RegisterType.DDT; + } else { + RegisterType = RegisterType.DT; + } + } - internal NRegister(int _adress, string _name = null, bool isBitwise = false, Type _enumType = null) { + public NRegister (int _adress, string _name = null, bool isBitwise = false, Type _enumType = null) { if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); memoryAdress = _adress; @@ -111,6 +120,13 @@ internal NRegister(int _adress, string _name = null, bool isBitwise = false, Typ throw new NotSupportedException($"The type {numType} is not allowed for Number Registers"); } + //set register type + if (memoryLength == 1) { + RegisterType = RegisterType.DDT; + } else { + RegisterType = RegisterType.DT; + } + isUsedBitwise = isBitwise; enumType = _enumType; @@ -131,6 +147,8 @@ internal void SetValueFromPLC(object val) { } + public byte? GetSpecialAddress() => null; + public string GetStartingMemoryArea() => MemoryAddress.ToString(); public Type GetCollectionType() => CollectionType; @@ -267,8 +285,12 @@ public string GetRegisterString() { public void TriggerNotifyChange() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value")); + public RegisterType GetRegisterType() => RegisterType; + public override string ToString() => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; + public string ToString(bool additional) => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; + } } diff --git a/MewtocolNet/Registers/SRegister.cs b/MewtocolNet/Registers/SRegister.cs index 00e7b2a..91b6ca4 100644 --- a/MewtocolNet/Registers/SRegister.cs +++ b/MewtocolNet/Registers/SRegister.cs @@ -19,6 +19,8 @@ public class SRegister : IRegister { /// public event PropertyChangedEventHandler PropertyChanged; + public RegisterType RegisterType { get; private set; } + internal Type collectionType; /// @@ -69,10 +71,12 @@ public SRegister(int _adress, int _reservedStringSize, string _name = null) { wordsize++; } + RegisterType = RegisterType.DT_START; + memoryLength = (int)Math.Round(wordsize + 1); } - internal SRegister WithCollectionType(Type colType) { + internal SRegister WithCollectionType (Type colType) { collectionType = colType; return this; @@ -105,6 +109,8 @@ internal string BuildCustomIdent(int overwriteWordLength) { return asciistring.ToString(); } + public byte? GetSpecialAddress() => null; + public Type GetCollectionType() => CollectionType; public bool IsUsedBitwise() => false; @@ -138,6 +144,8 @@ internal void SetValueFromPLC(string val) { public override string ToString() => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; + public string ToString(bool additional) => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; + } } diff --git a/MewtocolTests/TestLivePLC.cs b/MewtocolTests/TestLivePLC.cs index b4d40f1..420e5d4 100644 --- a/MewtocolTests/TestLivePLC.cs +++ b/MewtocolTests/TestLivePLC.cs @@ -1,6 +1,5 @@ using MewtocolNet; using MewtocolNet.Logging; -using MewtocolNet.PLCEnums; using Xunit; using Xunit.Abstractions; diff --git a/MewtocolTests/TestRegisterBuilder.cs b/MewtocolTests/TestRegisterBuilder.cs new file mode 100644 index 0000000..d72671c --- /dev/null +++ b/MewtocolTests/TestRegisterBuilder.cs @@ -0,0 +1,204 @@ +using MewtocolNet; +using MewtocolNet.RegisterBuilding; +using MewtocolNet.Registers; +using System.Collections; +using Xunit; +using Xunit.Abstractions; +using static System.Net.Mime.MediaTypeNames; + +namespace MewtocolTests; + +public class TestRegisterBuilder { + + private readonly ITestOutputHelper output; + + public TestRegisterBuilder(ITestOutputHelper output) { + this.output = output; + } + + [Fact(DisplayName = "Parsing as BRegister List (Phyiscal Outputs)")] + public void TestParsingBRegisterY() { + + var tests = new Dictionary() { + + {"Y0", new BRegister(IOType.Y)}, + {"Y1", new BRegister(IOType.Y, 0x1)}, + {"Y2", new BRegister(IOType.Y, 0x2)}, + {"Y3", new BRegister(IOType.Y, 0x3)}, + {"Y4", new BRegister(IOType.Y, 0x4)}, + {"Y5", new BRegister(IOType.Y, 0x5)}, + {"Y6", new BRegister(IOType.Y, 0x6)}, + {"Y7", new BRegister(IOType.Y, 0x7)}, + {"Y8", new BRegister(IOType.Y, 0x8)}, + {"Y9", new BRegister(IOType.Y, 0x9)}, + + {"YA", new BRegister(IOType.Y, 0xA)}, + {"YB", new BRegister(IOType.Y, 0xB)}, + {"YC", new BRegister(IOType.Y, 0xC)}, + {"YD", new BRegister(IOType.Y, 0xD)}, + {"YE", new BRegister(IOType.Y, 0xE)}, + {"YF", new BRegister(IOType.Y, 0xF)}, + + {"Y1A", new BRegister(IOType.Y, 0xA, 1)}, + {"Y10B", new BRegister(IOType.Y, 0xB, 10)}, + {"Y109C", new BRegister(IOType.Y, 0xC, 109)}, + + }; + + TestBoolDict(tests); + + } + + [Fact(DisplayName = "Parsing as BRegister List (Phyiscal Inputs)")] + public void TestParsingBRegisterX() { + + var tests = new Dictionary() { + + {"X0", new BRegister(IOType.X)}, + {"X1", new BRegister(IOType.X, 0x1)}, + {"X2", new BRegister(IOType.X, 0x2)}, + {"X3", new BRegister(IOType.X, 0x3)}, + {"X4", new BRegister(IOType.X, 0x4)}, + {"X5", new BRegister(IOType.X, 0x5)}, + {"X6", new BRegister(IOType.X, 0x6)}, + {"X7", new BRegister(IOType.X, 0x7)}, + {"X8", new BRegister(IOType.X, 0x8)}, + {"X9", new BRegister(IOType.X, 0x9)}, + + {"XA", new BRegister(IOType.X, 0xA)}, + {"XB", new BRegister(IOType.X, 0xB)}, + {"XC", new BRegister(IOType.X, 0xC)}, + {"XD", new BRegister(IOType.X, 0xD)}, + {"XE", new BRegister(IOType.X, 0xE)}, + {"XF", new BRegister(IOType.X, 0xF)}, + + {"X1A", new BRegister(IOType.X, 0xA, 1)}, + {"X10B", new BRegister(IOType.X, 0xB, 10)}, + {"X109C", new BRegister(IOType.X, 0xC, 109)}, + + }; + + TestBoolDict(tests); + + } + + [Fact(DisplayName = "Parsing as BRegister List (Internal Relay)")] + public void TestParsingBRegisterR() { + + var tests = new Dictionary() { + + {"R0", new BRegister(IOType.R)}, + {"R1", new BRegister(IOType.R, 0x1)}, + {"R2", new BRegister(IOType.R, 0x2)}, + {"R3", new BRegister(IOType.R, 0x3)}, + {"R4", new BRegister(IOType.R, 0x4)}, + {"R5", new BRegister(IOType.R, 0x5)}, + {"R6", new BRegister(IOType.R, 0x6)}, + {"R7", new BRegister(IOType.R, 0x7)}, + {"R8", new BRegister(IOType.R, 0x8)}, + {"R9", new BRegister(IOType.R, 0x9)}, + + {"RA", new BRegister(IOType.R, 0xA)}, + {"RB", new BRegister(IOType.R, 0xB)}, + {"RC", new BRegister(IOType.R, 0xC)}, + {"RD", new BRegister(IOType.R, 0xD)}, + {"RE", new BRegister(IOType.R, 0xE)}, + {"RF", new BRegister(IOType.R, 0xF)}, + + {"R1A", new BRegister(IOType.R, 0xA, 1)}, + {"R10B", new BRegister(IOType.R, 0xB, 10)}, + {"R109C", new BRegister(IOType.R, 0xC, 109)}, + {"R1000", new BRegister(IOType.R, 0x0, 100)}, + {"R511", new BRegister(IOType.R, 0x0, 511)}, + {"R511A", new BRegister(IOType.R, 0xA, 511)}, + + }; + + TestBoolDict(tests); + + } + + private void TestBoolDict (Dictionary dict) { + + foreach (var item in dict) { + + try { + + output.WriteLine($"Expected: {item.Key}"); + + var built = RegBuilder.FromPlcRegName(item.Key).AsPlcType(PlcVarType.BOOL).Build(); + + output.WriteLine($"{(built?.ToString(true) ?? "null")}\n"); + Assert.Equivalent(item.Value, built); + + } catch (Exception ex) { + + output.WriteLine(ex.Message.ToString()); + + } + + } + + } + + [Fact(DisplayName = "Parsing as BRegister (Casted)")] + public void TestRegisterBuildingBoolCasted () { + + var expect = new BRegister(IOType.R, 0x1, 0); + var expect2 = new BRegister(IOType.Y, 0xA, 103); + + Assert.Equivalent(expect, RegBuilder.FromPlcRegName("R1").AsPlcType(PlcVarType.BOOL).Build()); + Assert.Equivalent(expect, RegBuilder.FromPlcRegName("R1").AsType().Build()); + + Assert.Equivalent(expect2, RegBuilder.FromPlcRegName("Y103A").AsPlcType(PlcVarType.BOOL).Build()); + Assert.Equivalent(expect2, RegBuilder.FromPlcRegName("Y103A").AsType().Build()); + + } + + [Fact(DisplayName = "Parsing as BRegister (Auto)")] + public void TestRegisterBuildingBoolAuto () { + + var expect = new BRegister(IOType.R, 0x1, 0); + var expect2 = new BRegister(IOType.Y, 0xA, 103); + + Assert.Equivalent(expect, RegBuilder.FromPlcRegName("R1").Build()); + Assert.Equivalent(expect, RegBuilder.FromPlcRegName("R1").Build()); + + Assert.Equivalent(expect2, RegBuilder.FromPlcRegName("Y103A").Build()); + Assert.Equivalent(expect2, RegBuilder.FromPlcRegName("Y103A").Build()); + + } + + [Fact(DisplayName = "Parsing as NRegister (Casted)")] + public void TestRegisterBuildingNumericCasted() { + + var expect = new NRegister(303, null); + var expect2 = new NRegister(10002, null); + var expect3 = new NRegister(400, null); + //var expect4 = new NRegister(103, null, true); + + Assert.Equivalent(expect, RegBuilder.FromPlcRegName("DT303").AsPlcType(PlcVarType.INT).Build()); + Assert.Equivalent(expect, RegBuilder.FromPlcRegName("DT303").AsType().Build()); + + Assert.Equivalent(expect2, RegBuilder.FromPlcRegName("DDT10002").AsPlcType(PlcVarType.DINT).Build()); + Assert.Equivalent(expect2, RegBuilder.FromPlcRegName("DDT10002").AsType().Build()); + + Assert.Equivalent(expect3, RegBuilder.FromPlcRegName("DDT400").AsPlcType(PlcVarType.TIME).Build()); + Assert.Equivalent(expect3, RegBuilder.FromPlcRegName("DDT400").AsType().Build()); + + //Assert.Equivalent(expect4, RegBuilder.FromPlcRegName("DT103").AsType().Build()); + + } + + [Fact(DisplayName = "Parsing as NRegister (Auto)")] + public void TestRegisterBuildingNumericAuto() { + + var expect = new NRegister(303, null); + var expect2 = new NRegister(10002, null); + + Assert.Equivalent(expect, RegBuilder.FromPlcRegName("DT303").Build()); + Assert.Equivalent(expect2, RegBuilder.FromPlcRegName("DDT10002").Build()); + + } + +} diff --git a/MewtocolTests/TestRegisterParsing.cs b/MewtocolTests/TestRegisterParsing.cs deleted file mode 100644 index 41687f1..0000000 --- a/MewtocolTests/TestRegisterParsing.cs +++ /dev/null @@ -1,78 +0,0 @@ -using MewtocolNet; -using MewtocolNet.Registers; -using Xunit; -using Xunit.Abstractions; - -namespace MewtocolTests; - -public class TestRegisterParsing { - - private readonly ITestOutputHelper output; - - public TestRegisterParsing(ITestOutputHelper output) { - this.output = output; - } - - [Fact(DisplayName = "Parsing as BRegister (Phyiscal Outputs)")] - public void TestParsingBRegisterY() { - - var tests = new Dictionary() { - - {"Y0", new BRegister(IOType.Y)}, - {"Y1", new BRegister(IOType.Y, 0x1)}, - {"Y2", new BRegister(IOType.Y, 0x2)}, - {"Y3", new BRegister(IOType.Y, 0x3)}, - {"Y4", new BRegister(IOType.Y, 0x4)}, - {"Y5", new BRegister(IOType.Y, 0x5)}, - {"Y6", new BRegister(IOType.Y, 0x6)}, - {"Y7", new BRegister(IOType.Y, 0x7)}, - {"Y8", new BRegister(IOType.Y, 0x8)}, - {"Y9", new BRegister(IOType.Y, 0x9)}, - - {"YA", new BRegister(IOType.Y, 0xA)}, - {"YB", new BRegister(IOType.Y, 0xB)}, - {"YC", new BRegister(IOType.Y, 0xC)}, - {"YD", new BRegister(IOType.Y, 0xD)}, - {"YE", new BRegister(IOType.Y, 0xE)}, - {"YF", new BRegister(IOType.Y, 0xF)}, - - {"Y1A", new BRegister(IOType.Y, 0xA, 1)}, - {"Y10B", new BRegister(IOType.Y, 0xB, 10)}, - {"Y109C", new BRegister(IOType.Y, 0xC, 109)}, - - }; - - } - - [Fact(DisplayName = "Parsing as BRegister (Phyiscal Inputs)")] - public void TestParsingBRegisterX() { - - var tests = new Dictionary() { - - {"X0", new BRegister(IOType.X)}, - {"X1", new BRegister(IOType.X, 0x1)}, - {"X2", new BRegister(IOType.X, 0x2)}, - {"X3", new BRegister(IOType.X, 0x3)}, - {"X4", new BRegister(IOType.X, 0x4)}, - {"X5", new BRegister(IOType.X, 0x5)}, - {"X6", new BRegister(IOType.X, 0x6)}, - {"X7", new BRegister(IOType.X, 0x7)}, - {"X8", new BRegister(IOType.X, 0x8)}, - {"X9", new BRegister(IOType.X, 0x9)}, - - {"XA", new BRegister(IOType.X, 0xA)}, - {"XB", new BRegister(IOType.X, 0xB)}, - {"XC", new BRegister(IOType.X, 0xC)}, - {"XD", new BRegister(IOType.X, 0xD)}, - {"XE", new BRegister(IOType.X, 0xE)}, - {"XF", new BRegister(IOType.X, 0xF)}, - - {"X1A", new BRegister(IOType.X, 0xA, 1)}, - {"X10B", new BRegister(IOType.X, 0xB, 10)}, - {"X109C", new BRegister(IOType.X, 0xC, 109)}, - - }; - - } - -} From 7be52efb7efd546cab3e463344f08b18feed3c9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Mon, 26 Jun 2023 19:13:04 +0200 Subject: [PATCH 05/18] Further implementation - added new parsing method --- Examples/ExampleScenarios.cs | 50 ++- Examples/Program.cs | 3 - Examples/TestRegisters.cs | 4 +- Examples/TestRegistersEnumBitwise.cs | 32 +- MewtocolNet/DynamicInterface.cs | 315 ++++++++++++------ MewtocolNet/Exceptions/MewtocolException.cs | 34 ++ MewtocolNet/IRegister.cs | 1 + MewtocolNet/IRegisterInternal.cs | 18 + MewtocolNet/MewtocolHelpers.cs | 28 +- MewtocolNet/MewtocolInterface.cs | 294 +--------------- MewtocolNet/MewtocolInterfaceRequests.cs | 248 ++++---------- MewtocolNet/PLCEnums/PlcVarType.cs | 4 +- MewtocolNet/PLCEnums/PlcVarTypeConversions.cs | 91 ----- .../RegisterAttributes/RegisterAttribute.cs | 73 ++-- MewtocolNet/RegisterBuildInfo.cs | 118 +++++++ .../RegisterBuilding/FinalizerExtensions.cs | 61 +--- MewtocolNet/RegisterBuilding/RegBuilder.cs | 21 +- .../RegisterBuilding/RegisterBuilderStep.cs | 27 +- MewtocolNet/RegisterEnums.cs | 8 +- .../{BRegister.cs => BoolRegister.cs} | 41 ++- ...egisterResult.cs => BoolRegisterResult.cs} | 4 +- .../{SRegister.cs => BytesRegister.cs} | 36 +- ...gisterResult.cs => BytesRegisterResult.cs} | 4 +- .../{NRegister.cs => NumberRegister.cs} | 33 +- ...isterResult.cs => NumberRegisterResult.cs} | 4 +- MewtocolNet/TypeConversion/Conversions.cs | 124 +++++++ .../TypeConversion/IPlcTypeConverter.cs | 21 ++ .../TypeConversion/PlcTypeConversion.cs | 42 +++ MewtocolNet/TypeConversion/PlcValueParser.cs | 55 +++ .../TypeConversion/PlcVarTypeConversions.cs | 65 ++++ MewtocolTests/AutomatedPropertyRegisters.cs | 22 +- MewtocolTests/TestRegisterBuilder.cs | 210 ++++++------ MewtocolTests/TestRegisterInterface.cs | 48 +-- PLC_Test/test_c30_fpx_h.ini | Bin 832 -> 832 bytes PLC_Test/test_c30_fpx_h.pro | Bin 262144 -> 262144 bytes PLC_Test/test_c30_fpx_h.xml | 14 +- 36 files changed, 1165 insertions(+), 988 deletions(-) create mode 100644 MewtocolNet/Exceptions/MewtocolException.cs create mode 100644 MewtocolNet/IRegisterInternal.cs delete mode 100644 MewtocolNet/PLCEnums/PlcVarTypeConversions.cs create mode 100644 MewtocolNet/RegisterBuildInfo.cs rename MewtocolNet/Registers/{BRegister.cs => BoolRegister.cs} (81%) rename MewtocolNet/Registers/{BRegisterResult.cs => BoolRegisterResult.cs} (78%) rename MewtocolNet/Registers/{SRegister.cs => BytesRegister.cs} (82%) rename MewtocolNet/Registers/{SRegisterResult.cs => BytesRegisterResult.cs} (78%) rename MewtocolNet/Registers/{NRegister.cs => NumberRegister.cs} (90%) rename MewtocolNet/Registers/{NRegisterResult.cs => NumberRegisterResult.cs} (88%) create mode 100644 MewtocolNet/TypeConversion/Conversions.cs create mode 100644 MewtocolNet/TypeConversion/IPlcTypeConverter.cs create mode 100644 MewtocolNet/TypeConversion/PlcTypeConversion.cs create mode 100644 MewtocolNet/TypeConversion/PlcValueParser.cs create mode 100644 MewtocolNet/TypeConversion/PlcVarTypeConversions.cs diff --git a/Examples/ExampleScenarios.cs b/Examples/ExampleScenarios.cs index bd2244a..df60f16 100644 --- a/Examples/ExampleScenarios.cs +++ b/Examples/ExampleScenarios.cs @@ -6,6 +6,7 @@ using System.Collections; using MewtocolNet.RegisterBuilding; using System.Collections.Generic; +using MewtocolNet.Registers; namespace Examples; @@ -46,7 +47,7 @@ public async Task RunCyclicPollerAsync () { while (interf.IsConnected) { //flip the bool register each tick and wait for it to be registered - await interf.SetRegisterAsync(nameof(registers.TestBool1), !registers.TestBool1); + //await interf.SetRegisterAsync(nameof(registers.TestBool1), !registers.TestBool1); Console.Title = $"Polling Paused: {interf.PollingPaused}, " + $"Poller active: {interf.PollerActive}, " + @@ -167,7 +168,7 @@ public async Task RunEnumsBitwiseAsync () { await interf.ConnectAsync(); //use the async method to make sure the cycling is stopped - await interf.SetRegisterAsync(nameof(registers.StartCyclePLC), false); + //await interf.SetRegisterAsync(nameof(registers.StartCyclePLC), false); await Task.Delay(5000); @@ -182,4 +183,49 @@ public async Task RunEnumsBitwiseAsync () { } + [Scenario("Read register test")] + public async Task RunReadTest () { + + Console.WriteLine("Starting auto enums and bitwise"); + + //setting up a new PLC interface and register collection + MewtocolInterface interf = new MewtocolInterface("192.168.115.210").WithPoller(); + + //auto add all built registers to the interface + var builder = RegBuilder.ForInterface(interf); + var r0reg = builder.FromPlcRegName("R0").Build(); + builder.FromPlcRegName("R1").Build(); + builder.FromPlcRegName("R1F").Build(); + builder.FromPlcRegName("R101A").Build(); + + var shortReg = builder.FromPlcRegName("DT35").AsPlcType(PlcVarType.INT).Build(); + builder.FromPlcRegName("DDT36").AsPlcType(PlcVarType.DINT).Build(); + + //builder.FromPlcRegName("DDT38").AsPlcType(PlcVarType.TIME).Build(); + //builder.FromPlcRegName("DT40").AsPlcType(PlcVarType.STRING).Build(); + + //connect + await interf.ConnectAsync(); + + //var res = await interf.SendCommandAsync("%01#RCSR000F"); + + while(true) { + + await interf.SetRegisterAsync(r0reg, !(bool)r0reg.Value); + await interf.SetRegisterAsync(shortReg, (short)new Random().Next(0, 100)); + + foreach (var reg in interf.Registers) { + + Console.WriteLine($"Register {reg.GetRegisterPLCName()} val: {reg.Value}"); + + } + + Console.WriteLine(); + + await Task.Delay(1000); + + } + + } + } diff --git a/Examples/Program.cs b/Examples/Program.cs index a868993..57c913f 100644 --- a/Examples/Program.cs +++ b/Examples/Program.cs @@ -14,9 +14,6 @@ class Program { static void Main(string[] args) { - RegBuilder.FromPlcRegName("DT303").AsPlcType(PlcVarType.INT).Build(); - var res = RegBuilder.FromPlcRegName("DT100").AsPlcType(PlcVarType.INT).Build(); - AppDomain.CurrentDomain.UnhandledException += (s,e) => { Console.WriteLine(e.ExceptionObject.ToString()); }; diff --git a/Examples/TestRegisters.cs b/Examples/TestRegisters.cs index afe76f7..4b13ca1 100644 --- a/Examples/TestRegisters.cs +++ b/Examples/TestRegisters.cs @@ -47,10 +47,10 @@ public int TestDuplicate { 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)] + [Register(1204, BitCount.B16, 9)] public bool BitValue { get; private set; } - [Register(1204, 5, BitCount.B16)] + [Register(1204, BitCount.B16, 5)] public bool FillTest { get; private set; } //corresponds to a DT7012 - DT7013 as a 32bit time value that gets parsed as a timespan (TIME) diff --git a/Examples/TestRegistersEnumBitwise.cs b/Examples/TestRegistersEnumBitwise.cs index ea4954c..72f5ede 100644 --- a/Examples/TestRegistersEnumBitwise.cs +++ b/Examples/TestRegistersEnumBitwise.cs @@ -53,52 +53,52 @@ public enum CurrentState { //you can also extract single bits from DT503 - [Register(503, 0, BitCount.B16)] + [Register(503, BitCount.B16, 0)] public bool BitValue0 { get; private set; } - [Register(503, 1, BitCount.B16)] + [Register(503, BitCount.B16, 1)] public bool BitValue1 { get; private set; } - [Register(503, 2, BitCount.B16)] + [Register(503, BitCount.B16, 2)] public bool BitValue2 { get; private set; } - [Register(503, 3, BitCount.B16)] + [Register(503, BitCount.B16, 3)] public bool BitValue3 { get; private set; } - [Register(503, 4, BitCount.B16)] + [Register(503, BitCount.B16, 4)] public bool BitValue4 { get; private set; } - [Register(503, 5, BitCount.B16)] + [Register(503, BitCount.B16, 5)] public bool BitValue5 { get; private set; } - [Register(503, 6, BitCount.B16)] + [Register(503, BitCount.B16, 6)] public bool BitValue6 { get; private set; } - [Register(503, 7, BitCount.B16)] + [Register(503, BitCount.B16, 7)] public bool BitValue7 { get; private set; } - [Register(503, 8, BitCount.B16)] + [Register(503, BitCount.B16, 8)] public bool BitValue8 { get; private set; } - [Register(503, 9, BitCount.B16)] + [Register(503, BitCount.B16, 9)] public bool BitValue9 { get; private set; } - [Register(503, 10, BitCount.B16)] + [Register(503, BitCount.B16, 10)] public bool BitValue10 { get; private set; } - [Register(503, 11, BitCount.B16)] + [Register(503, BitCount.B16, 11)] public bool BitValue11 { get; private set; } - [Register(503, 12, BitCount.B16)] + [Register(503, BitCount.B16, 12)] public bool BitValue12 { get; private set; } - [Register(503, 13, BitCount.B16)] + [Register(503, BitCount.B16, 13)] public bool BitValue13 { get; private set; } - [Register(503, 14, BitCount.B16)] + [Register(503, BitCount.B16, 14)] public bool BitValue14 { get; private set; } - [Register(503, 15, BitCount.B16)] + [Register(503, BitCount.B16, 15)] public bool BitValue15 { get; private set; } } diff --git a/MewtocolNet/DynamicInterface.cs b/MewtocolNet/DynamicInterface.cs index f13ceff..1bd86c6 100644 --- a/MewtocolNet/DynamicInterface.cs +++ b/MewtocolNet/DynamicInterface.cs @@ -1,12 +1,16 @@ -using MewtocolNet.Logging; +using MewtocolNet.Exceptions; +using MewtocolNet.Logging; +using MewtocolNet.RegisterAttributes; using MewtocolNet.Registers; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; -namespace MewtocolNet { +namespace MewtocolNet +{ /// /// The PLC com interface class @@ -118,61 +122,21 @@ internal void AttachPoller() { var reg = Registers[iteration]; - if (reg is NRegister shortReg) { - var lastVal = shortReg.Value; - var readout = (await ReadNumRegister(shortReg)).Register.Value; - if (lastVal != readout) { - InvokeRegisterChanged(shortReg); - } - } - if (reg is NRegister ushortReg) { - var lastVal = ushortReg.Value; - var readout = (await ReadNumRegister(ushortReg)).Register.Value; - if (lastVal != readout) { - InvokeRegisterChanged(ushortReg); - } - } - if (reg is NRegister intReg) { - var lastVal = intReg.Value; - var readout = (await ReadNumRegister(intReg)).Register.Value; - if (lastVal != readout) { - InvokeRegisterChanged(intReg); - } - } - if (reg is NRegister uintReg) { - var lastVal = uintReg.Value; - var readout = (await ReadNumRegister(uintReg)).Register.Value; - if (lastVal != readout) { - InvokeRegisterChanged(uintReg); - } - } - if (reg is NRegister floatReg) { - var lastVal = floatReg.Value; - var readout = (await ReadNumRegister(floatReg)).Register.Value; - if (lastVal != readout) { - InvokeRegisterChanged(floatReg); - } - } - if (reg is NRegister tsReg) { - var lastVal = tsReg.Value; - var readout = (await ReadNumRegister(tsReg)).Register.Value; - if (lastVal != readout) { - InvokeRegisterChanged(tsReg); - } - } - if (reg is BRegister boolReg) { - var lastVal = boolReg.Value; - var readout = (await ReadBoolRegister(boolReg)).Register.Value; - if (lastVal != readout) { - InvokeRegisterChanged(boolReg); - } - } - if (reg is SRegister stringReg) { - var lastVal = stringReg.Value; - var readout = (await ReadStringRegister(stringReg)).Register.Value; + if(reg.IsAllowedRegisterGenericType()) { + + var lastVal = reg.Value; + + var rwReg = (IRegisterInternal)reg; + + var readout = await rwReg.ReadAsync(this); + if (lastVal != readout) { - InvokeRegisterChanged(stringReg); + + rwReg.SetValueFromPLC(readout); + InvokeRegisterChanged(reg); + } + } iteration++; @@ -194,67 +158,218 @@ internal void AttachPoller() { internal void PropertyRegisterWasSet(string propName, object value) { - SetRegister(propName, value); + _ = SetRegisterAsync(GetRegister(propName), value); } #endregion - #region Register Adding + #region Register Colleciton adding - //Internal register adding for auto register collection building - internal void AddRegister(Type _colType, int _address, PropertyInfo boundProp, int _length = 1, bool _isBitwise = false, Type _enumType = null) { + #region Register Collection - Type regType = typeof(T); + /// + /// 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 && prop.PropertyType.IsAllowedPlcCastingType()) { + + var dotnetType = prop.PropertyType; + + AddRegister(new RegisterBuildInfo { + memoryAddress = cAttribute.MemoryArea, + specialAddress = cAttribute.SpecialAddress, + memorySizeBytes = cAttribute.ByteLength, + registerType = cAttribute.RegisterType, + dotnetCastType = dotnetType, + collectionType = collection.GetType(), + name = prop.Name, + }); + + } + + } - if (regType != typeof(string) && _length != 1) { - throw new NotSupportedException($"_lenght parameter only allowed for register of type string"); } - if (Registers.Any(x => x.MemoryAddress == _address) && _isBitwise) { + RegisterChanged += (reg) => { + + //register is used bitwise + if (reg.IsUsedBitwise()) { + + for (int i = 0; i < props.Length; i++) { + + var prop = props[i]; + var bitWiseFound = prop.GetCustomAttributes(true) + .FirstOrDefault(y => y.GetType() == typeof(RegisterAttribute) && ((RegisterAttribute)y).MemoryArea == reg.MemoryAddress); + + if (bitWiseFound != null) { + + var casted = (RegisterAttribute)bitWiseFound; + var bitIndex = casted.AssignedBitIndex; + + BitArray bitAr = null; + + if (reg is NumberRegister reg16) { + var bytes = BitConverter.GetBytes((short)reg16.Value); + bitAr = new BitArray(bytes); + } else if (reg is NumberRegister reg32) { + var bytes = BitConverter.GetBytes((int)reg32.Value); + bitAr = new BitArray(bytes); + } + + if (bitAr != null && bitIndex < bitAr.Length && bitIndex >= 0) { + + //set the specific bit index if needed + prop.SetValue(collection, bitAr[bitIndex]); + collection.TriggerPropertyChanged(prop.Name); + + } else if (bitAr != null) { + + //set the specific bit array if needed + prop.SetValue(collection, bitAr); + collection.TriggerPropertyChanged(prop.Name); + + } + + } + + } + + } + + //updating normal properties + 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) { + + HashSet NumericTypes = new HashSet { + typeof(bool), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(float), + typeof(TimeSpan), + typeof(string) + }; + + var regValue = ((IRegister)reg).Value; + + if (NumericTypes.Any(x => foundToUpdate.PropertyType == x)) { + foundToUpdate.SetValue(collection, regValue); + } + + if (foundToUpdate.PropertyType.IsEnum) { + foundToUpdate.SetValue(collection, regValue); + } + + } + + collection.TriggerPropertyChanged(foundToUpdate.Name); + + } + + }; + + if (collection != null) + collection.OnInterfaceLinked(this); + + Connected += (i) => { + if (collection != null) + collection.OnInterfaceLinkedAndOnline(this); + }; + + return this; + + } + + #endregion + + #endregion + + #region Register Adding + + internal void AddRegister (RegisterBuildInfo buildInfo) { + + var builtRegister = buildInfo.Build(); + + //is bitwise and the register list already contains that area register + if(builtRegister.IsUsedBitwise() && CheckDuplicateRegister(builtRegister, out var existing)) { + return; - } - IRegister reg = null; - - string propName = boundProp.Name; - - //rename the property name to prevent duplicate names in case of a bitwise prop - if (_isBitwise && regType == typeof(short)) - propName = $"Auto_Bitwise_DT{_address}"; - - if (_isBitwise && regType == typeof(int)) - propName = $"Auto_Bitwise_DDT{_address}"; - - if (regType == typeof(short)) { - reg = new NRegister(_address, propName, _isBitwise, _enumType).WithCollectionType(_colType); - } else if (regType == typeof(ushort)) { - reg = new NRegister(_address, propName).WithCollectionType(_colType); - } else if (regType == typeof(int)) { - reg = new NRegister(_address, propName, _isBitwise, _enumType).WithCollectionType(_colType); - } else if (regType == typeof(uint)) { - reg = new NRegister(_address, propName).WithCollectionType(_colType); - } else if (regType == typeof(float)) { - reg = new NRegister(_address, propName).WithCollectionType(_colType); - } else if (regType == typeof(string)) { - reg = new SRegister(_address, _length, propName).WithCollectionType(_colType); - } else if (regType == typeof(TimeSpan)) { - reg = new NRegister(_address, propName).WithCollectionType(_colType); - } else if (regType == typeof(bool)) { - reg = new BRegister(IOType.R, 0x0, _address, propName).WithCollectionType(_colType); } - if (reg == null) { - throw new NotSupportedException($"The type {regType} is not allowed for Registers \n" + - $"Allowed are: short, ushort, int, uint, float and string"); - } + if (CheckDuplicateRegister(builtRegister)) + throw MewtocolException.DupeRegister(builtRegister); - if (Registers.Any(x => x.GetRegisterPLCName() == reg.GetRegisterPLCName()) && !_isBitwise) { - throw new NotSupportedException($"Cannot add a register multiple times, " + - $"make sure that all register attributes or AddRegister assignments have different adresses."); - } + if(CheckDuplicateNameRegister(builtRegister)) + throw MewtocolException.DupeNameRegister(builtRegister); + + Registers.Add(builtRegister); + + } + + public void AddRegister(IRegister register) { + + if (CheckDuplicateRegister(register)) + throw MewtocolException.DupeRegister(register); + + if (CheckDuplicateNameRegister(register)) + throw MewtocolException.DupeNameRegister(register); + + Registers.Add(register); + + } + + private bool CheckDuplicateRegister (IRegister instance, out IRegister foundDupe) { + + foundDupe = Registers.FirstOrDefault(x => x.CompareIsDuplicate(instance)); + + return Registers.Contains(instance) || foundDupe != null; + + } + + private bool CheckDuplicateRegister(IRegister instance) { + + var foundDupe = Registers.FirstOrDefault(x => x.CompareIsDuplicate(instance)); + + return Registers.Contains(instance) || foundDupe != null; + + } + + private bool CheckDuplicateNameRegister(IRegister instance) { - Registers.Add(reg); + return Registers.Any(x => x.CompareIsNameDuplicate(instance)); } diff --git a/MewtocolNet/Exceptions/MewtocolException.cs b/MewtocolNet/Exceptions/MewtocolException.cs new file mode 100644 index 0000000..0601df4 --- /dev/null +++ b/MewtocolNet/Exceptions/MewtocolException.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.Exceptions { + + [Serializable] + public class MewtocolException : Exception { + + public MewtocolException() { } + + public MewtocolException(string message) : base(message) { } + + public MewtocolException(string message, Exception inner) : base(message, inner) { } + + protected MewtocolException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public static MewtocolException DupeRegister (IRegister register) { + + return new MewtocolException($"The mewtocol interface already contains this register: {register.GetRegisterPLCName()}"); + + } + + public static MewtocolException DupeNameRegister (IRegister register) { + + return new MewtocolException($"The mewtocol interface registers already contains a register with the name: {register.Name}"); + + } + + } + +} diff --git a/MewtocolNet/IRegister.cs b/MewtocolNet/IRegister.cs index f68d60d..c442584 100644 --- a/MewtocolNet/IRegister.cs +++ b/MewtocolNet/IRegister.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace MewtocolNet { diff --git a/MewtocolNet/IRegisterInternal.cs b/MewtocolNet/IRegisterInternal.cs new file mode 100644 index 0000000..9b9c5a4 --- /dev/null +++ b/MewtocolNet/IRegisterInternal.cs @@ -0,0 +1,18 @@ +using MewtocolNet.Registers; +using System; +using System.Threading.Tasks; + +namespace MewtocolNet { + internal interface IRegisterInternal { + + void WithCollectionType(Type colType); + + void SetValueFromPLC(object value); + + Task ReadAsync(MewtocolInterface interf); + + Task WriteAsync(MewtocolInterface interf, object data); + + } + +} diff --git a/MewtocolNet/MewtocolHelpers.cs b/MewtocolNet/MewtocolHelpers.cs index faee445..7da65b8 100644 --- a/MewtocolNet/MewtocolHelpers.cs +++ b/MewtocolNet/MewtocolHelpers.cs @@ -109,17 +109,17 @@ internal static IEnumerable SplitInParts(this string s, int partLength) } - internal static string BuildDTString(this string _inString, short _stringReservedSize) { + internal static string BuildDTString (this byte[] inBytes, short reservedSize) { StringBuilder sb = new StringBuilder(); //clamp string lenght - if (_inString.Length > _stringReservedSize) { - _inString = _inString.Substring(0, _stringReservedSize); + if (inBytes.Length > reservedSize) { + inBytes = inBytes.Take(reservedSize).ToArray(); } //actual string content - var hexstring = _inString.GetAsciiHexFromString(); + var hexstring = inBytes.ToHexString(); var sizeBytes = BitConverter.GetBytes((short)(hexstring.Length / 2)).ToHexString(); @@ -133,7 +133,7 @@ internal static string BuildDTString(this string _inString, short _stringReserve } - var reservedSizeBytes = BitConverter.GetBytes(_stringReservedSize).ToHexString(); + var reservedSizeBytes = BitConverter.GetBytes(reservedSize).ToHexString(); //reserved string count bytes sb.Append(reservedSizeBytes); @@ -159,8 +159,10 @@ internal static string GetStringFromAsciiHex(this string input) { } internal static string GetAsciiHexFromString(this string input) { + var bytes = new ASCIIEncoding().GetBytes(input); return bytes.ToHexString(); + } internal static byte[] HexStringToByteArray(this string hex) { @@ -265,6 +267,22 @@ internal static bool IsPhysicalInOutType(this RegisterType type) { } + internal static bool CompareIsDuplicate (this IRegister reg1, IRegister compare) { + + bool valCompare = reg1.RegisterType == compare.RegisterType && + reg1.MemoryAddress == compare.MemoryAddress && + reg1.GetSpecialAddress() == compare.GetSpecialAddress(); + + return valCompare; + + } + + internal static bool CompareIsNameDuplicate(this IRegister reg1, IRegister compare) { + + return ( reg1.Name != null || compare.Name != null) && reg1.Name == compare.Name; + + } + } } \ No newline at end of file diff --git a/MewtocolNet/MewtocolInterface.cs b/MewtocolNet/MewtocolInterface.cs index 81cd8ff..273dc1e 100644 --- a/MewtocolNet/MewtocolInterface.cs +++ b/MewtocolNet/MewtocolInterface.cs @@ -308,7 +308,6 @@ public void Disconnect() { public MewtocolInterface WithPoller() { usePoller = true; - return this; } @@ -407,301 +406,10 @@ private void ClearRegisterVals() { #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) { - - //add bool register non bit assgined - Registers.Add(new BRegister((IOType)(int)cAttribute.RegisterType, cAttribute.SpecialAddress, cAttribute.MemoryArea, _name: propName).WithCollectionType(collection.GetType())); - - } - - if (prop.PropertyType == typeof(short)) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop); - } - - if (prop.PropertyType == typeof(ushort)) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop); - } - - if (prop.PropertyType == typeof(int)) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop); - } - - if (prop.PropertyType == typeof(uint)) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop); - } - - if (prop.PropertyType == typeof(float)) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop); - } - - if (prop.PropertyType == typeof(string)) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop, cAttribute.StringLength); - } - - if (prop.PropertyType.IsEnum) { - - if (cAttribute.BitCount == BitCount.B16) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop, _enumType: prop.PropertyType); - } else { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop, _enumType: prop.PropertyType); - } - - } - - //read number as bit array - if (prop.PropertyType == typeof(BitArray)) { - - if (cAttribute.BitCount == BitCount.B16) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop, _isBitwise: true); - } else { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop, _isBitwise: true); - } - - } - - //read number as bit array by invdividual properties - if (prop.PropertyType == typeof(bool) && cAttribute.AssignedBitIndex != -1) { - - //var bitwiseCount = Registers.Count(x => x.Value.isUsedBitwise); - - if (cAttribute.BitCount == BitCount.B16) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop, _isBitwise: true); - } else { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop, _isBitwise: true); - } - - } - - if (prop.PropertyType == typeof(TimeSpan)) { - AddRegister(collection.GetType(), cAttribute.MemoryArea, prop); - } - - } - - } - - } - - RegisterChanged += (reg) => { - - //register is used bitwise - if (reg.IsUsedBitwise()) { - - for (int i = 0; i < props.Length; i++) { - - var prop = props[i]; - var bitWiseFound = prop.GetCustomAttributes(true) - .FirstOrDefault(y => y.GetType() == typeof(RegisterAttribute) && ((RegisterAttribute)y).MemoryArea == reg.MemoryAddress); - - if (bitWiseFound != null) { - - var casted = (RegisterAttribute)bitWiseFound; - var bitIndex = casted.AssignedBitIndex; - - BitArray bitAr = null; - - if (reg is NRegister reg16) { - var bytes = BitConverter.GetBytes((short)reg16.Value); - bitAr = new BitArray(bytes); - } else if (reg is NRegister reg32) { - var bytes = BitConverter.GetBytes((int)reg32.Value); - bitAr = new BitArray(bytes); - } - - if (bitAr != null && bitIndex < bitAr.Length && bitIndex >= 0) { - - //set the specific bit index if needed - prop.SetValue(collection, bitAr[bitIndex]); - collection.TriggerPropertyChanged(prop.Name); - - } else if (bitAr != null) { - - //set the specific bit array if needed - prop.SetValue(collection, bitAr); - collection.TriggerPropertyChanged(prop.Name); - - } - - } - - } - - } - - //updating normal properties - 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) { - - HashSet NumericTypes = new HashSet { - typeof(bool), - typeof(short), - typeof(ushort), - typeof(int), - typeof(uint), - typeof(float), - typeof(TimeSpan), - typeof(string) - }; - - var regValue = ((IRegister)reg).Value; - - if (NumericTypes.Any(x => foundToUpdate.PropertyType == x)) { - foundToUpdate.SetValue(collection, regValue); - } - - if (foundToUpdate.PropertyType.IsEnum) { - foundToUpdate.SetValue(collection, regValue); - } - - } - - collection.TriggerPropertyChanged(foundToUpdate.Name); - - } - - }; - - if (collection != null) - collection.OnInterfaceLinked(this); - - Connected += (i) => { - if (collection != null) - collection.OnInterfaceLinkedAndOnline(this); - }; - - 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"); - } - - _ = SetRegisterAsync(registerName, value); - - } - - /// - /// Sets a register in the PLCs memory asynchronously, returns the result status from the PLC - /// - /// The name the register was given to or a property name from the RegisterCollection class - /// The value to write to the register - public async Task SetRegisterAsync(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)) { - - return await WriteBoolRegister((BRegister)foundRegister, (bool)value); - - } - - if (foundRegister.GetType() == typeof(NRegister)) { - - return await WriteNumRegister((NRegister)foundRegister, (short)value); - - } - - if (foundRegister.GetType() == typeof(NRegister)) { - - return await WriteNumRegister((NRegister)foundRegister, (ushort)value); - - } - - if (foundRegister.GetType() == typeof(NRegister)) { - - return await WriteNumRegister((NRegister)foundRegister, (int)value); - - } - - if (foundRegister.GetType() == typeof(NRegister)) { - - return await WriteNumRegister((NRegister)foundRegister, (uint)value); - - } - - if (foundRegister.GetType() == typeof(NRegister)) { - - return await WriteNumRegister((NRegister)foundRegister, (float)value); - - } - - if (foundRegister.GetType() == typeof(NRegister)) { - - return await WriteNumRegister((NRegister)foundRegister, (TimeSpan)value); - - } - - if (foundRegister.GetType() == typeof(SRegister)) { - - return await WriteStringRegister((SRegister)foundRegister, (string)value); - - } - - return false; - - } - - #endregion - #region Low level command handling /// - /// Calculates checksum and sends a command to the PLC then awaits results + /// Calculates the checksum automatically and sends a command to the PLC then awaits results /// /// MEWTOCOL Formatted request string ex: %01#RT /// Returns the result diff --git a/MewtocolNet/MewtocolInterfaceRequests.cs b/MewtocolNet/MewtocolInterfaceRequests.cs index caa9e5c..6a3b9cc 100644 --- a/MewtocolNet/MewtocolInterfaceRequests.cs +++ b/MewtocolNet/MewtocolInterfaceRequests.cs @@ -4,7 +4,9 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; namespace MewtocolNet { @@ -155,244 +157,110 @@ public async Task ReadByteRange(int start, int count, Action onP #endregion - #region Bool register reading / writing + #region Raw register reading / writing - /// - /// Reads the given boolean register from the PLC - /// - /// The register to read - public async Task ReadBoolRegister(BRegister _toRead) { - - string requeststring = $"%{GetStationNumber()}#RCS{_toRead.BuildMewtocolQuery()}"; - var result = await SendCommandAsync(requeststring); - - if (!result.Success) { - return new BRegisterResult { - Result = result, - Register = _toRead - }; - } - - var resultBool = result.Response.ParseRCSingleBit(); - if (resultBool != null) { - _toRead.SetValueFromPLC(resultBool.Value); - } - - var finalRes = new BRegisterResult { - Result = result, - Register = _toRead - }; - - return finalRes; - - } + internal async Task ReadRawRegisterAsync (IRegister _toRead) { - /// - /// Writes to the given bool register on the PLC - /// - /// The register to write to - /// The value to write - /// The success state of the write operation - public async Task WriteBoolRegister(BRegister _toWrite, bool value) { + //returns a byte array 1 long and with the byte beeing 0 or 1 + if (_toRead.GetType() == typeof(BoolRegister)) { - string requeststring = $"%{GetStationNumber()}#WCS{_toWrite.BuildMewtocolQuery()}{(value ? "1" : "0")}"; - - var result = await SendCommandAsync(requeststring); - - return result.Success && result.Response.StartsWith($"%{GetStationNumber()}$WC"); - - } - - #endregion - - #region Number register reading / writing + string requeststring = $"%{GetStationNumber()}#RCS{_toRead.BuildMewtocolQuery()}"; + var result = await SendCommandAsync(requeststring); - /// - /// Reads the given numeric register from the PLC - /// - /// Type of number (short, ushort, int, uint, float) - /// The register to read - /// A result with the given NumberRegister containing the readback value and a result struct - public async Task> ReadNumRegister(NRegister _toRead) { + var resultBool = result.Response.ParseRCSingleBit(); + if (resultBool != null) { - Type numType = typeof(T); + return resultBool.Value ? new byte[] { 1 } : new byte[] { 0 }; - string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolQuery()}"; - var result = await SendCommandAsync(requeststring); - - var failedResult = new NRegisterResult { - Result = result, - Register = _toRead - }; + } - if (!result.Success || string.IsNullOrEmpty(result.Response)) { - return failedResult; } - if (numType == typeof(short)) { - - var resultBytes = result.Response.ParseDTByteString(4).ReverseByteOrder(); - if (resultBytes == null) return failedResult; - var val = short.Parse(resultBytes, NumberStyles.HexNumber); - _toRead.SetValueFromPLC(val); + //returns a byte array 2 bytes or 4 bytes long depending on the data size + if (_toRead.GetType().GetGenericTypeDefinition() == typeof(NumberRegister<>)) { - } else if (numType == typeof(ushort)) { - - var resultBytes = result.Response.ParseDTByteString(4).ReverseByteOrder(); - if (resultBytes == null) return failedResult; - var val = ushort.Parse(resultBytes, NumberStyles.HexNumber); - _toRead.SetValueFromPLC(val); + string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolQuery()}"; + var result = await SendCommandAsync(requeststring); - } else if (numType == typeof(int)) { + if (!result.Success) + throw new Exception($"Failed to load the byte data for: {_toRead}"); - var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder(); - if (resultBytes == null) return failedResult; - var val = int.Parse(resultBytes, NumberStyles.HexNumber); - _toRead.SetValueFromPLC(val); + if(_toRead.RegisterType == RegisterType.DT) { - } else if (numType == typeof(uint)) { + return result.Response.ParseDTByteString(4).HexStringToByteArray(); - var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder(); - if (resultBytes == null) return failedResult; - var val = uint.Parse(resultBytes, NumberStyles.HexNumber); - _toRead.SetValueFromPLC(val); + } else { - } else if (numType == typeof(float)) { + return result.Response.ParseDTByteString(8).HexStringToByteArray(); - var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder(); - if (resultBytes == null) return failedResult; - //convert to unsigned int first - var val = uint.Parse(resultBytes, NumberStyles.HexNumber); + } - byte[] floatVals = BitConverter.GetBytes(val); - float finalFloat = BitConverter.ToSingle(floatVals, 0); + } - _toRead.SetValueFromPLC(finalFloat); + //returns a byte array with variable size + if (_toRead.GetType() == typeof(BytesRegister<>)) { - } else if (numType == typeof(TimeSpan)) { + string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolQuery()}"; + var result = await SendCommandAsync(requeststring); - var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder(); - if (resultBytes == null) return failedResult; - //convert to unsigned int first - var vallong = long.Parse(resultBytes, NumberStyles.HexNumber); - var valMillis = vallong * 10; - var ts = TimeSpan.FromMilliseconds(valMillis); + if (!result.Success) + throw new Exception($"Failed to load the byte data for: {_toRead}"); - //minmax writable / readable value is 10ms - _toRead.SetValueFromPLC(ts); + return result.Response.ParseDTString().ReverseByteOrder().HexStringToByteArray(); } - var finalRes = new NRegisterResult { - Result = result, - Register = _toRead - }; + throw new Exception($"Failed to load the byte data for: {_toRead}"); - return finalRes; } - /// - /// Reads the given numeric register from the PLC - /// - /// Type of number (short, ushort, int, uint, float) - /// The register to write - /// The value to write - /// The success state of the write operation - public async Task WriteNumRegister(NRegister _toWrite, T _value) { - - 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)) { + internal async Task WriteRawRegisterAsync (IRegister _toWrite, byte[] data) { - var fl = _value as float?; - if (fl == null) - throw new NullReferenceException("Float cannot be null"); + //returns a byte array 1 long and with the byte beeing 0 or 1 + if (_toWrite.GetType() == typeof(BoolRegister)) { - toWriteVal = BitConverter.GetBytes(fl.Value); + string requeststring = $"%{GetStationNumber()}#WCS{_toWrite.BuildMewtocolQuery()}{(data[0] == 1 ? "1" : "0")}"; + var result = await SendCommandAsync(requeststring); + return result.Success; - } else if (numType == typeof(TimeSpan)) { + } - var fl = _value as TimeSpan?; - if (fl == null) - throw new NullReferenceException("Timespan cannot be null"); + //returns a byte array 2 bytes or 4 bytes long depending on the data size + if (_toWrite.GetType().GetGenericTypeDefinition() == typeof(NumberRegister<>)) { - var tLong = (uint)(fl.Value.TotalMilliseconds / 10); - toWriteVal = BitConverter.GetBytes(tLong); + string requeststring = $"%{GetStationNumber()}#WD{_toWrite.BuildMewtocolQuery()}{data.ToHexString()}"; + var result = await SendCommandAsync(requeststring); + return result.Success; - } else { - toWriteVal = null; } - string requeststring = $"%{GetStationNumber()}#WD{_toWrite.BuildMewtocolQuery()}{toWriteVal.ToHexString()}"; + //returns a byte array with variable size + if (_toWrite.GetType() == typeof(BytesRegister<>)) { - var result = await SendCommandAsync(requeststring); + //string stationNum = GetStationNumber(); + //string dataString = gotBytes.BuildDTString(_toWrite.ReservedSize); + //string dataArea = _toWrite.BuildCustomIdent(dataString.Length / 4); - return result.Success && result.Response.StartsWith($"%{GetStationNumber()}$WD"); + //string requeststring = $"%{stationNum}#WD{dataArea}{dataString}"; - } + //var result = await SendCommandAsync(requeststring); - #endregion - - #region String register reading / writing - - //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 + } - /// - /// Reads back the value of a string register - /// - /// The register to read - /// The station number of the PLC - /// - public async Task ReadStringRegister(SRegister _toRead, int _stationNumber = 1) { + return false; - string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolQuery()}"; - var result = await SendCommandAsync(requeststring); - if (result.Success) - _toRead.SetValueFromPLC(result.Response.ParseDTString()); - return new SRegisterResult { - Result = result, - Register = _toRead - }; } - /// - /// Writes a string to a string register - /// - /// The register to write - /// The value to write, if the strings length is longer than the cap size it gets trimmed to the max char length - /// The station number of the PLC - /// The success state of the write operation - 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"); - } + #endregion - string stationNum = GetStationNumber(); - string dataString = _value.BuildDTString(_toWrite.ReservedSize); - string dataArea = _toWrite.BuildCustomIdent(dataString.Length / 4); + #region Register reading / writing - string requeststring = $"%{stationNum}#WD{dataArea}{dataString}"; + public async Task SetRegisterAsync (IRegister register, object value) { - var result = await SendCommandAsync(requeststring); + var internalReg = (IRegisterInternal)register; + return await internalReg.WriteAsync(this, value); - return result.Success && result.Response.StartsWith($"%{GetStationNumber()}$WD"); } #endregion diff --git a/MewtocolNet/PLCEnums/PlcVarType.cs b/MewtocolNet/PLCEnums/PlcVarType.cs index d3e13a2..d7ac5de 100644 --- a/MewtocolNet/PLCEnums/PlcVarType.cs +++ b/MewtocolNet/PLCEnums/PlcVarType.cs @@ -11,7 +11,9 @@ public enum PlcVarType { UDINT, REAL, TIME, - STRING + STRING, + WORD, + DWORD } diff --git a/MewtocolNet/PLCEnums/PlcVarTypeConversions.cs b/MewtocolNet/PLCEnums/PlcVarTypeConversions.cs deleted file mode 100644 index 4c6653b..0000000 --- a/MewtocolNet/PLCEnums/PlcVarTypeConversions.cs +++ /dev/null @@ -1,91 +0,0 @@ -using MewtocolNet.Registers; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace MewtocolNet { - internal static class PlcVarTypeConversions { - - static Dictionary dictTypeConv = new Dictionary { - - { PlcVarType.BOOL, typeof(bool) }, - { PlcVarType.INT, typeof(short) }, - { PlcVarType.UINT, typeof(ushort) }, - { PlcVarType.DINT, typeof(int) }, - { PlcVarType.UDINT, typeof(uint) }, - { PlcVarType.REAL, typeof(float) }, - { PlcVarType.TIME, typeof(TimeSpan) }, - { PlcVarType.STRING, typeof(string) }, - - }; - - static Dictionary dictRegisterConv = new Dictionary { - - { PlcVarType.BOOL, typeof(BRegister) }, - { PlcVarType.INT, typeof(NRegister) }, - { PlcVarType.UINT, typeof(NRegister) }, - { PlcVarType.DINT, typeof(NRegister) }, - { PlcVarType.UDINT, typeof(NRegister) }, - { PlcVarType.REAL, typeof(NRegister) }, - { PlcVarType.TIME, typeof(NRegister) }, - { PlcVarType.STRING, typeof(SRegister) }, - - }; - - internal static bool IsAllowedPlcCastingType () { - - var inversed = dictTypeConv.ToDictionary((i) => i.Value, (i) => i.Key); - - return inversed.ContainsKey(typeof(T)); - - } - - internal static bool IsAllowedPlcCastingType (this Type type) { - - var inversed = dictTypeConv.ToDictionary((i) => i.Value, (i) => i.Key); - - return inversed.ContainsKey(type); - - } - - internal static Type ToDotnetType (this PlcVarType type) { - - if(dictTypeConv.ContainsKey(type)) { - - return dictTypeConv[type]; - - } - - throw new NotSupportedException($"The PlcVarType: '{type}' is not supported"); - - } - - internal static PlcVarType ToPlcVarType (this Type type) { - - var inversed = dictTypeConv.ToDictionary((i) => i.Value, (i) => i.Key); - - if (inversed.ContainsKey(type)) { - - return inversed[type]; - - } - - throw new NotSupportedException($"The Dotnet Type: '{type}' is not supported"); - - } - - internal static Type ToRegisterType (this PlcVarType type) { - - if (dictRegisterConv.ContainsKey(type)) { - - return dictRegisterConv[type]; - - } - - throw new NotSupportedException($"The PlcVarType: '{type}' is not supported"); - - } - - } - -} diff --git a/MewtocolNet/RegisterAttributes/RegisterAttribute.cs b/MewtocolNet/RegisterAttributes/RegisterAttribute.cs index dd90c3c..cd79645 100644 --- a/MewtocolNet/RegisterAttributes/RegisterAttribute.cs +++ b/MewtocolNet/RegisterAttributes/RegisterAttribute.cs @@ -8,10 +8,12 @@ namespace MewtocolNet.RegisterAttributes { [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class RegisterAttribute : Attribute { - internal int MemoryArea; - internal int StringLength; - internal RegisterType RegisterType; + internal RegisterType? RegisterType; + + internal int MemoryArea = 0; + internal int ByteLength = 2; internal byte SpecialAddress = 0x0; + internal BitCount BitCount; internal int AssignedBitIndex = -1; @@ -19,69 +21,58 @@ public class RegisterAttribute : Attribute { /// Attribute for string type or numeric registers /// /// The area in the plcs memory - /// The max string length in the plc - public RegisterAttribute(int memoryArea, int stringLength = 1) { + public RegisterAttribute(int memoryArea) { MemoryArea = memoryArea; - StringLength = stringLength; } - /// - /// Attribute for boolean registers - /// - public RegisterAttribute(IOType type, byte spAdress = 0x0) { + public RegisterAttribute(int memoryArea, int byteLength) { - MemoryArea = 0; - RegisterType = (RegisterType)(int)type; - SpecialAddress = spAdress; + MemoryArea = memoryArea; + ByteLength = byteLength; } - /// - /// Attribute for boolean registers - /// - public RegisterAttribute(IOType type, int memoryArea, byte spAdress = 0x0) { + public RegisterAttribute(int memoryArea, BitCount bitCount) { MemoryArea = memoryArea; - RegisterType = (RegisterType)(int)type; - SpecialAddress = spAdress; + BitCount = bitCount; + AssignedBitIndex = 0; + + RegisterType = BitCount == BitCount.B16 ? MewtocolNet.RegisterType.DT : MewtocolNet.RegisterType.DDT; } - /// - /// Attribute to read numeric registers as bitwise - /// - /// The area in the plcs memory - /// The number of bits to parse - public RegisterAttribute(int memoryArea, BitCount bitcount) { + public RegisterAttribute(int memoryArea, BitCount bitCount, int bitIndex) { MemoryArea = memoryArea; - StringLength = 0; - BitCount = bitcount; + BitCount = bitCount; + AssignedBitIndex = bitIndex; + + RegisterType = BitCount == BitCount.B16 ? MewtocolNet.RegisterType.DT : MewtocolNet.RegisterType.DDT; } /// - /// Attribute to read numeric registers as bitwise + /// Attribute for boolean registers /// - /// The area in the plcs memory - /// The number of bits to parse - /// The index of the bit that gets linked to the bool - public RegisterAttribute(int memoryArea, uint assignBit, BitCount bitcount) { + public RegisterAttribute(IOType type, byte spAdress = 0x0) { - if (assignBit > 15 && bitcount == BitCount.B16) { - throw new NotSupportedException("The assignBit parameter cannot be greater than 15 in a 16 bit var"); - } + MemoryArea = 0; + RegisterType = (RegisterType)(int)type; + SpecialAddress = spAdress; - if (assignBit > 31 && bitcount == BitCount.B32) { - throw new NotSupportedException("The assignBit parameter cannot be greater than 31 in a 32 bit var"); - } + } + + /// + /// Attribute for boolean registers + /// + public RegisterAttribute(IOType type, int memoryArea, byte spAdress = 0x0) { MemoryArea = memoryArea; - StringLength = 0; - BitCount = bitcount; - AssignedBitIndex = (int)assignBit; + RegisterType = (RegisterType)(int)type; + SpecialAddress = spAdress; } diff --git a/MewtocolNet/RegisterBuildInfo.cs b/MewtocolNet/RegisterBuildInfo.cs new file mode 100644 index 0000000..c53074a --- /dev/null +++ b/MewtocolNet/RegisterBuildInfo.cs @@ -0,0 +1,118 @@ +using MewtocolNet.Registers; +using System; +using System.Collections; +using System.Reflection; + +namespace MewtocolNet +{ + + internal struct RegisterBuildInfo { + + internal string name; + internal int memoryAddress; + internal int memorySizeBytes; + internal byte? specialAddress; + + internal RegisterType? registerType; + internal Type dotnetCastType; + internal Type collectionType; + + internal IRegister Build () { + + RegisterType regType = registerType ?? dotnetCastType.ToRegisterTypeDefault(); + + PlcVarType plcType = dotnetCastType.ToPlcVarType(); + Type registerClassType = plcType.GetDefaultPlcVarType(); + + if (regType.IsNumericDTDDT() && (dotnetCastType == typeof(bool) || dotnetCastType == typeof(BitArray))) { + + //------------------------------------------- + //as numeric register with boolean bit target + + var type = typeof(NumberRegister); + + var areaAddr = memoryAddress; + + //create a new bregister instance + var flags = BindingFlags.Public | BindingFlags.Instance; + + //int _adress, string _name = null, bool isBitwise = false, Type _enumType = null + var parameters = new object[] { areaAddr, name, true, null }; + var instance = (IRegister)Activator.CreateInstance(type, flags, null, parameters, null); + + if (collectionType != null) + ((IRegisterInternal)instance).WithCollectionType(collectionType); + + return instance; + + } else if (regType.IsNumericDTDDT()) { + + //------------------------------------------- + //as numeric register + + var type = plcType.GetDefaultPlcVarType(); + + var areaAddr = memoryAddress; + + //create a new bregister instance + var flags = BindingFlags.Public | BindingFlags.Instance; + + //int _adress, string _name = null, bool isBitwise = false, Type _enumType = null + var parameters = new object[] { areaAddr, name, false, null }; + var instance = (IRegister)Activator.CreateInstance(type, flags, null, parameters, null); + + if(collectionType != null) + ((IRegisterInternal)instance).WithCollectionType(collectionType); + + return instance; + + } + + if (regType.IsBoolean()) { + + //------------------------------------------- + //as boolean register + + var io = (IOType)(int)regType; + var spAddr = specialAddress; + var areaAddr = memoryAddress; + + //create a new bregister instance + var flags = BindingFlags.Public | BindingFlags.Instance; + var parameters = new object[] { io, spAddr.Value, areaAddr, name }; + var instance = (BoolRegister)Activator.CreateInstance(typeof(BoolRegister), flags, null, parameters, null); + + if (collectionType != null) + ((IRegisterInternal)instance).WithCollectionType(collectionType); + + return instance; + + } + + if(regType == RegisterType.DT_RANGE) { + + //------------------------------------------- + //as byte range register + + var type = plcType.GetDefaultPlcVarType(); + + //create a new bregister instance + var flags = BindingFlags.Public | BindingFlags.Instance; + //int _adress, int _reservedSize, string _name = null + var parameters = new object[] { memoryAddress, memorySizeBytes, name }; + var instance = (IRegister)Activator.CreateInstance(type, flags, null, parameters, null); + + if (collectionType != null) + ((IRegisterInternal)instance).WithCollectionType(collectionType); + + return instance; + + } + + throw new Exception("Failed to build register"); + + } + + } + +} diff --git a/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs b/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs index b42a30e..a06e77b 100644 --- a/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs +++ b/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs @@ -1,8 +1,10 @@ using MewtocolNet.Registers; using System; +using System.Linq; using System.Reflection; -namespace MewtocolNet.RegisterBuilding { +namespace MewtocolNet.RegisterBuilding +{ public static class FinalizerExtensions { @@ -29,7 +31,7 @@ public static IRegister Build (this RegisterBuilderStep step) { step.dotnetVarType = typeof(bool); - } else if (isTypeNotDefined && step.RegType == RegisterType.DT_START) { + } else if (isTypeNotDefined && step.RegType == RegisterType.DT_RANGE) { step.dotnetVarType = typeof(string); @@ -37,58 +39,31 @@ public static IRegister Build (this RegisterBuilderStep step) { if(step.plcVarType != null) { - step.dotnetVarType = step.plcVarType.Value.ToDotnetType(); + step.dotnetVarType = step.plcVarType.Value.GetDefaultDotnetType(); } - //as numeric register - if (step.RegType.IsNumericDTDDT()) { + var builtReg = new RegisterBuildInfo { - if(step.plcVarType == null && step.dotnetVarType != null) { + name = step.Name, + specialAddress = step.SpecialAddress, + memoryAddress = step.MemAddress, + registerType = step.RegType, + dotnetCastType = step.dotnetVarType, - step.plcVarType = step.dotnetVarType.ToPlcVarType(); + }.Build(); - } + step.AddToRegisterList(builtReg); - var type = step.plcVarType.Value.ToRegisterType(); - - var areaAddr = step.MemAddress; - var name = step.Name; - - //create a new bregister instance - var flags = BindingFlags.Public | BindingFlags.Instance; - - //int _adress, string _name = null, bool isBitwise = false, Type _enumType = null - var parameters = new object[] { areaAddr, name, false, null }; - var instance = (IRegister)Activator.CreateInstance(type, flags, null, parameters, null); - - return instance; - - } - - if (step.RegType.IsBoolean()) { - - var io = (IOType)(int)step.RegType; - var spAddr = step.SpecialAddress; - var areaAddr = step.MemAddress; - var name = step.Name; - - //create a new bregister instance - var flags = BindingFlags.Public | BindingFlags.Instance; - var parameters = new object[] { io, spAddr.Value, areaAddr, name }; - var instance = (BRegister)Activator.CreateInstance(typeof(BRegister), flags, null, parameters, null); - - return instance; - - } - - if (step.dotnetVarType != null) { + return builtReg; + } + private static void AddToRegisterList (this RegisterBuilderStep step, IRegister instance) { - } + if (step.forInterface == null) return; - throw new Exception("Failed to build register"); + step.forInterface.AddRegister(instance); } diff --git a/MewtocolNet/RegisterBuilding/RegBuilder.cs b/MewtocolNet/RegisterBuilding/RegBuilder.cs index ce91a17..4ca8416 100644 --- a/MewtocolNet/RegisterBuilding/RegBuilder.cs +++ b/MewtocolNet/RegisterBuilding/RegBuilder.cs @@ -9,7 +9,9 @@ namespace MewtocolNet.RegisterBuilding { /// /// Contains useful tools for register creation /// - public static class RegBuilder { + public class RegBuilder { + + internal MewtocolInterface forInterface = null; //methods to test the input string on private static List> parseMethods = new List>() { @@ -19,7 +21,18 @@ public static class RegBuilder { }; - public static RegisterBuilderStep FromPlcRegName (string plcAddrName, string name = null) { + public static RegBuilder ForInterface (MewtocolInterface interf) { + + var rb = new RegBuilder(); + rb.forInterface = interf; + return rb; + + } + + public static RegBuilder Factory { get; private set; } = new RegBuilder(); + + + public RegisterBuilderStep FromPlcRegName (string plcAddrName, string name = null) { foreach (var method in parseMethods) { @@ -30,8 +43,8 @@ public static RegisterBuilderStep FromPlcRegName (string plcAddrName, string nam if (!string.IsNullOrEmpty(name)) res.stepData.Name = name; - res.stepData.OriginalInput = plcAddrName; - + res.stepData.OriginalInput = plcAddrName; + res.stepData.forInterface = forInterface; return res.stepData; } else if(res.state == ParseResultState.FailedHard) { diff --git a/MewtocolNet/RegisterBuilding/RegisterBuilderStep.cs b/MewtocolNet/RegisterBuilding/RegisterBuilderStep.cs index e1c2103..04883be 100644 --- a/MewtocolNet/RegisterBuilding/RegisterBuilderStep.cs +++ b/MewtocolNet/RegisterBuilding/RegisterBuilderStep.cs @@ -1,8 +1,11 @@ using System; namespace MewtocolNet.RegisterBuilding { + public class RegisterBuilderStep { + internal MewtocolInterface forInterface; + internal bool wasCasted = false; internal string OriginalInput; @@ -15,12 +18,12 @@ public class RegisterBuilderStep { internal PlcVarType? plcVarType; internal Type dotnetVarType; - public RegisterBuilderStep () => throw new NotSupportedException("Cant make a new instance of RegisterBuilderStep, use the builder pattern"); - - internal RegisterBuilderStep (RegisterType regType, int memAddr) { - - RegType = regType; - MemAddress = memAddr; + public RegisterBuilderStep() => throw new NotSupportedException("Cant make a new instance of RegisterBuilderStep, use the builder pattern"); + + internal RegisterBuilderStep(RegisterType regType, int memAddr) { + + RegType = regType; + MemAddress = memAddr; } @@ -28,11 +31,11 @@ internal RegisterBuilderStep(RegisterType regType, int memAddr, byte specialAddr RegType = regType; MemAddress = memAddr; - SpecialAddress = specialAddr; + SpecialAddress = specialAddr; } - public RegisterBuilderStep AsPlcType (PlcVarType varType) { + public RegisterBuilderStep AsPlcType(PlcVarType varType) { dotnetVarType = null; plcVarType = varType; @@ -43,9 +46,9 @@ public RegisterBuilderStep AsPlcType (PlcVarType varType) { } - public RegisterBuilderStep AsType () { + public RegisterBuilderStep AsType() { - if(!typeof(T).IsAllowedPlcCastingType()) { + if (!typeof(T).IsAllowedPlcCastingType()) { throw new NotSupportedException($"The dotnet type {typeof(T)}, is not supported for PLC type casting"); @@ -60,7 +63,7 @@ public RegisterBuilderStep AsType () { } - internal RegisterBuilderStep AutoType () { + internal RegisterBuilderStep AutoType() { switch (RegType) { case RegisterType.X: @@ -74,7 +77,7 @@ internal RegisterBuilderStep AutoType () { case RegisterType.DDT: dotnetVarType = typeof(int); break; - case RegisterType.DT_START: + case RegisterType.DT_RANGE: dotnetVarType = typeof(string); break; } diff --git a/MewtocolNet/RegisterEnums.cs b/MewtocolNet/RegisterEnums.cs index 1195f35..e23e89f 100644 --- a/MewtocolNet/RegisterEnums.cs +++ b/MewtocolNet/RegisterEnums.cs @@ -1,4 +1,6 @@ -namespace MewtocolNet { +using System; + +namespace MewtocolNet { /// /// The register prefixed type @@ -26,9 +28,9 @@ public enum RegisterType { /// DDT = 4, /// - /// Start area of a byte sequence longer than 2 words + /// Area of a byte sequence longer than 2 words /// - DT_START = 5, + DT_RANGE = 5, } diff --git a/MewtocolNet/Registers/BRegister.cs b/MewtocolNet/Registers/BoolRegister.cs similarity index 81% rename from MewtocolNet/Registers/BRegister.cs rename to MewtocolNet/Registers/BoolRegister.cs index 3e83adb..2ec09f4 100644 --- a/MewtocolNet/Registers/BRegister.cs +++ b/MewtocolNet/Registers/BoolRegister.cs @@ -1,13 +1,14 @@ using System; using System.ComponentModel; using System.Text; +using System.Threading.Tasks; namespace MewtocolNet.Registers { /// /// Defines a register containing a boolean /// - public class BRegister : IRegister, INotifyPropertyChanged { + public class BoolRegister : IRegister, IRegisterInternal, INotifyPropertyChanged { /// /// Gets called whenever the value was changed @@ -62,7 +63,7 @@ public class BRegister : IRegister, INotifyPropertyChanged { /// The custom name /// /// - public BRegister(IOType _io, byte _spAddress = 0x0, int _areaAdress = 0, string _name = null) { + public BoolRegister(IOType _io, byte _spAddress = 0x0, int _areaAdress = 0, string _name = null) { if (_areaAdress < 0) throw new NotSupportedException("The area address cant be negative"); @@ -84,36 +85,33 @@ public BRegister(IOType _io, byte _spAddress = 0x0, int _areaAdress = 0, string } - internal BRegister WithCollectionType(Type colType) { - - collectionType = colType; - return this; - - } + public void WithCollectionType (Type colType) => collectionType = colType; public byte? GetSpecialAddress() => SpecialAddress; /// - /// Builds the register area name + /// Builds the register area name for the mewtocol protocol /// public string BuildMewtocolQuery() { - //build area code from register type - StringBuilder asciistring = new StringBuilder(RegisterType.ToString()); + //(R|X|Y)(area add [3] + special add [1]) + StringBuilder asciistring = new StringBuilder(); - string memPadded = MemoryAddress.ToString().PadLeft(4, '0'); + string prefix = RegisterType.ToString(); + string mem = MemoryAddress.ToString(); string sp = SpecialAddress.ToString("X1"); - asciistring.Append(memPadded); + asciistring.Append(prefix); + asciistring.Append(mem.PadLeft(3, '0')); asciistring.Append(sp); return asciistring.ToString(); } - internal void SetValueFromPLC(bool val) { + public void SetValueFromPLC(object val) { - lastValue = val; + lastValue = (bool)val; TriggerChangedEvnt(this); TriggerNotifyChange(); @@ -183,6 +181,19 @@ public string ToString(bool additional) { } + public async Task ReadAsync (MewtocolInterface interf) { + + var read = await interf.ReadRawRegisterAsync(this); + return PlcValueParser.Parse(read); + + } + + public async Task WriteAsync (MewtocolInterface interf, object data) { + + return await interf.WriteRawRegisterAsync(this, PlcValueParser.Encode((bool)data)); + + } + } } diff --git a/MewtocolNet/Registers/BRegisterResult.cs b/MewtocolNet/Registers/BoolRegisterResult.cs similarity index 78% rename from MewtocolNet/Registers/BRegisterResult.cs rename to MewtocolNet/Registers/BoolRegisterResult.cs index 8a39277..4016a48 100644 --- a/MewtocolNet/Registers/BRegisterResult.cs +++ b/MewtocolNet/Registers/BoolRegisterResult.cs @@ -3,7 +3,7 @@ /// /// Result for a boolean register /// - public class BRegisterResult { + public class BoolRegisterResult { /// /// The command result @@ -13,7 +13,7 @@ public class BRegisterResult { /// /// The used register /// - public BRegister Register { get; set; } + public BoolRegister Register { get; set; } } diff --git a/MewtocolNet/Registers/SRegister.cs b/MewtocolNet/Registers/BytesRegister.cs similarity index 82% rename from MewtocolNet/Registers/SRegister.cs rename to MewtocolNet/Registers/BytesRegister.cs index 91b6ca4..8ac6240 100644 --- a/MewtocolNet/Registers/SRegister.cs +++ b/MewtocolNet/Registers/BytesRegister.cs @@ -1,13 +1,15 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Text; +using System.Threading.Tasks; namespace MewtocolNet.Registers { /// /// Defines a register containing a string /// - public class SRegister : IRegister { + public class BytesRegister : IRegister, IRegisterInternal { /// /// Gets called whenever the value was changed @@ -58,30 +60,25 @@ public class SRegister : IRegister { /// /// Defines a register containing a string /// - public SRegister(int _adress, int _reservedStringSize, string _name = null) { + public BytesRegister(int _adress, int _reservedSize, string _name = null) { if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); name = _name; memoryAdress = _adress; - ReservedSize = (short)_reservedStringSize; + ReservedSize = (short)_reservedSize; //calc mem length - var wordsize = (double)_reservedStringSize / 2; + var wordsize = (double)_reservedSize / 2; if (wordsize % 2 != 0) { wordsize++; } - RegisterType = RegisterType.DT_START; + RegisterType = RegisterType.DT_RANGE; memoryLength = (int)Math.Round(wordsize + 1); } - internal SRegister WithCollectionType (Type colType) { - - collectionType = colType; - return this; - - } + public void WithCollectionType(Type colType) => collectionType = colType; /// /// Builds the register identifier for the mewotocol protocol @@ -115,9 +112,9 @@ internal string BuildCustomIdent(int overwriteWordLength) { public bool IsUsedBitwise() => false; - internal void SetValueFromPLC(string val) { + public void SetValueFromPLC(object val) { - lastValue = val; + lastValue = (string)val; TriggerChangedEvnt(this); TriggerNotifyChange(); @@ -146,6 +143,19 @@ internal void SetValueFromPLC(string val) { public string ToString(bool additional) => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; + public async Task ReadAsync(MewtocolInterface interf) { + + var read = await interf.ReadRawRegisterAsync(this); + return PlcValueParser.Parse(read); + + } + + public async Task WriteAsync(MewtocolInterface interf, object data) { + + return await interf.WriteRawRegisterAsync(this, (byte[])data); + + } + } } diff --git a/MewtocolNet/Registers/SRegisterResult.cs b/MewtocolNet/Registers/BytesRegisterResult.cs similarity index 78% rename from MewtocolNet/Registers/SRegisterResult.cs rename to MewtocolNet/Registers/BytesRegisterResult.cs index 45129a6..314decf 100644 --- a/MewtocolNet/Registers/SRegisterResult.cs +++ b/MewtocolNet/Registers/BytesRegisterResult.cs @@ -3,7 +3,7 @@ /// /// The results of a string register operation /// - public class SRegisterResult { + public class BytesRegisterResult { /// /// The command result @@ -12,7 +12,7 @@ public class SRegisterResult { /// /// The register definition used /// - public SRegister Register { get; set; } + public BytesRegister Register { get; set; } } diff --git a/MewtocolNet/Registers/NRegister.cs b/MewtocolNet/Registers/NumberRegister.cs similarity index 90% rename from MewtocolNet/Registers/NRegister.cs rename to MewtocolNet/Registers/NumberRegister.cs index c36c0d4..b4f09c6 100644 --- a/MewtocolNet/Registers/NRegister.cs +++ b/MewtocolNet/Registers/NumberRegister.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Text; +using System.Threading.Tasks; namespace MewtocolNet.Registers { @@ -10,7 +11,7 @@ namespace MewtocolNet.Registers { /// Defines a register containing a number /// /// The type of the numeric value - public class NRegister : IRegister { + public class NumberRegister : IRegister, IRegisterInternal { /// /// Gets called whenever the value was changed @@ -65,7 +66,7 @@ public class NRegister : IRegister { /// /// Memory start adress max 99999 /// Name of the register - public NRegister (int _adress, string _name = null) { + public NumberRegister (int _adress, string _name = null) { if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); @@ -98,7 +99,7 @@ public NRegister (int _adress, string _name = null) { } - public NRegister (int _adress, string _name = null, bool isBitwise = false, Type _enumType = null) { + public NumberRegister (int _adress, string _name = null, bool isBitwise = false, Type _enumType = null) { if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); memoryAdress = _adress; @@ -132,14 +133,9 @@ public NRegister (int _adress, string _name = null, bool isBitwise = false, Type } - internal NRegister WithCollectionType(Type colType) { + public void WithCollectionType(Type colType) => collectionType = colType; - collectionType = colType; - return this; - - } - - internal void SetValueFromPLC(object val) { + public void SetValueFromPLC(object val) { lastValue = (T)val; TriggerChangedEvnt(this); @@ -214,7 +210,7 @@ public string GetValueString() { /// A bitarray public BitArray GetBitwise() { - if (this is NRegister shortReg) { + if (this is NumberRegister shortReg) { var bytes = BitConverter.GetBytes((short)Value); BitArray bitAr = new BitArray(bytes); @@ -222,7 +218,7 @@ public BitArray GetBitwise() { } - if (this is NRegister intReg) { + if (this is NumberRegister intReg) { var bytes = BitConverter.GetBytes((int)Value); BitArray bitAr = new BitArray(bytes); @@ -291,6 +287,19 @@ public string GetRegisterString() { public string ToString(bool additional) => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; + public async Task ReadAsync(MewtocolInterface interf) { + + var read = await interf.ReadRawRegisterAsync(this); + return PlcValueParser.Parse(read); + + } + + public async Task WriteAsync(MewtocolInterface interf, object data) { + + return await interf.WriteRawRegisterAsync(this, PlcValueParser.Encode((T)data)); + + } + } } diff --git a/MewtocolNet/Registers/NRegisterResult.cs b/MewtocolNet/Registers/NumberRegisterResult.cs similarity index 88% rename from MewtocolNet/Registers/NRegisterResult.cs rename to MewtocolNet/Registers/NumberRegisterResult.cs index 367233a..04bdec3 100644 --- a/MewtocolNet/Registers/NRegisterResult.cs +++ b/MewtocolNet/Registers/NumberRegisterResult.cs @@ -4,7 +4,7 @@ /// Result for a read/write operation /// /// The type of the numeric value - public class NRegisterResult { + public class NumberRegisterResult { /// /// Command result @@ -14,7 +14,7 @@ public class NRegisterResult { /// /// The used register /// - public NRegister Register { get; set; } + public NumberRegister Register { get; set; } /// /// Trys to get the value of there is one diff --git a/MewtocolNet/TypeConversion/Conversions.cs b/MewtocolNet/TypeConversion/Conversions.cs new file mode 100644 index 0000000..db72369 --- /dev/null +++ b/MewtocolNet/TypeConversion/Conversions.cs @@ -0,0 +1,124 @@ +using MewtocolNet.Registers; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace MewtocolNet.TypeConversion { + + internal static class Conversions { + + internal static Dictionary dictPlcTypeToRegisterType = new Dictionary { + + { PlcVarType.BOOL, RegisterType.R }, + { PlcVarType.INT, RegisterType.DT }, + { PlcVarType.UINT, RegisterType.DT }, + { PlcVarType.DINT, RegisterType.DDT }, + { PlcVarType.UDINT, RegisterType.DDT }, + { PlcVarType.REAL, RegisterType.DDT }, + { PlcVarType.TIME, RegisterType.DDT }, + { PlcVarType.WORD, RegisterType.DT }, + { PlcVarType.DWORD, RegisterType.DDT }, + { PlcVarType.STRING, RegisterType.DT_RANGE }, + + }; + + internal static List items = new List { + + new PlcTypeConversion(RegisterType.R) { + HoldingRegisterType = typeof(BoolRegister), + PlcVarType = PlcVarType.BOOL, + FromRaw = bytes => { + + return (bool)(bytes[0] == 1); + }, + ToRaw = value => { + + return new byte[] { (byte)(value ? 1 : 0) }; + + }, + }, + new PlcTypeConversion(RegisterType.X) { + HoldingRegisterType = typeof(BoolRegister), + PlcVarType = PlcVarType.BOOL, + FromRaw = bytes => { + + return bytes[0] == 1; + }, + ToRaw = value => { + + return new byte[] { (byte)(value ? 1 : 0) }; + + }, + }, + new PlcTypeConversion(RegisterType.Y) { + HoldingRegisterType = typeof(BoolRegister), + PlcVarType = PlcVarType.BOOL, + FromRaw = bytes => { + + return bytes[0] == 1; + }, + ToRaw = value => { + + return new byte[] { (byte)(value ? 1 : 0) }; + + }, + }, + new PlcTypeConversion(RegisterType.DT) { + HoldingRegisterType = typeof(NumberRegister), + PlcVarType = PlcVarType.INT, + FromRaw = bytes => { + + return BitConverter.ToInt16(bytes, 0); + }, + ToRaw = value => { + + return BitConverter.GetBytes(value); + + }, + }, + new PlcTypeConversion(RegisterType.DT) { + HoldingRegisterType = typeof(NumberRegister), + PlcVarType = PlcVarType.UINT, + FromRaw = bytes => { + + return BitConverter.ToUInt16(bytes, 0); + }, + ToRaw = value => { + + return BitConverter.GetBytes(value); + + }, + }, + new PlcTypeConversion(RegisterType.DDT) { + HoldingRegisterType = typeof(NumberRegister), + PlcVarType = PlcVarType.DINT, + FromRaw = bytes => { + + return BitConverter.ToInt32(bytes, 0); + }, + ToRaw = value => { + + return BitConverter.GetBytes(value); + + }, + }, + new PlcTypeConversion(RegisterType.DDT) { + HoldingRegisterType = typeof(NumberRegister), + PlcVarType = PlcVarType.UDINT, + FromRaw = bytes => { + + return BitConverter.ToUInt32(bytes, 0); + }, + ToRaw = value => { + + return BitConverter.GetBytes(value); + + }, + }, + + }; + + } + +} diff --git a/MewtocolNet/TypeConversion/IPlcTypeConverter.cs b/MewtocolNet/TypeConversion/IPlcTypeConverter.cs new file mode 100644 index 0000000..0b381f8 --- /dev/null +++ b/MewtocolNet/TypeConversion/IPlcTypeConverter.cs @@ -0,0 +1,21 @@ +using System; + +namespace MewtocolNet { + + internal interface IPlcTypeConverter { + + object FromRawData(byte[] data); + + byte[] ToRawData(object value); + + Type GetDotnetType(); + + Type GetHoldingRegisterType(); + + RegisterType GetPlcRegisterType(); + + PlcVarType GetPlcVarType(); + + } + +} diff --git a/MewtocolNet/TypeConversion/PlcTypeConversion.cs b/MewtocolNet/TypeConversion/PlcTypeConversion.cs new file mode 100644 index 0000000..f50d0b5 --- /dev/null +++ b/MewtocolNet/TypeConversion/PlcTypeConversion.cs @@ -0,0 +1,42 @@ +using System; +using System.ComponentModel; + +namespace MewtocolNet { + + internal class PlcTypeConversion : IPlcTypeConverter { + + public Type MainType { get; private set; } + + public RegisterType PlcType { get; private set; } + + public PlcVarType PlcVarType { get; set; } + + public Type HoldingRegisterType { get; set; } + + public Func FromRaw { get; set; } + + public Func ToRaw { get; set; } + + public PlcTypeConversion(RegisterType plcType) { + + MainType = typeof(T); + PlcType = plcType; + + } + + public Type GetDotnetType() => MainType; + + public Type GetHoldingRegisterType() => HoldingRegisterType; + + public RegisterType GetPlcRegisterType() => PlcType; + + public PlcVarType GetPlcVarType() => PlcVarType; + + public object FromRawData(byte[] data) => FromRaw.Invoke(data); + + public byte[] ToRawData(object value) => ToRaw.Invoke((T)value); + + + } + +} diff --git a/MewtocolNet/TypeConversion/PlcValueParser.cs b/MewtocolNet/TypeConversion/PlcValueParser.cs new file mode 100644 index 0000000..05a34a1 --- /dev/null +++ b/MewtocolNet/TypeConversion/PlcValueParser.cs @@ -0,0 +1,55 @@ +using MewtocolNet.Exceptions; +using MewtocolNet.Registers; +using MewtocolNet.TypeConversion; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; + +namespace MewtocolNet { + + internal static class PlcValueParser { + + private static List conversions => Conversions.items; + + public static T Parse(byte[] bytes) { + + var converter = conversions.FirstOrDefault(x => x.GetDotnetType() == typeof(T)); + + if (converter == null) + throw new MewtocolException($"A converter for the dotnet type {typeof(T)} doesn't exist"); + + return (T)converter.FromRawData(bytes); + + } + + public static byte[] Encode (T value) { + + var converter = conversions.FirstOrDefault(x => x.GetDotnetType() == typeof(T)); + + if (converter == null) + throw new MewtocolException($"A converter for the dotnet type {typeof(T)} doesn't exist"); + + return converter.ToRawData(value); + + } + + public static List GetAllowDotnetTypes () => conversions.Select(x => x.GetDotnetType()).ToList(); + + public static List GetAllowRegisterTypes () => conversions.Select(x => x.GetHoldingRegisterType()).ToList(); + + public static RegisterType? GetDefaultRegisterType (Type type) => + conversions.FirstOrDefault(x => x.GetDotnetType() == type)?.GetPlcRegisterType(); + + public static Type GetDefaultPlcVarType (this PlcVarType type) => + conversions.FirstOrDefault(x => x.GetPlcVarType() == type)?.GetHoldingRegisterType(); + + public static Type GetDefaultDotnetType (this PlcVarType type) => + conversions.FirstOrDefault(x => x.GetPlcVarType() == type)?.GetDotnetType(); + + public static PlcVarType? GetDefaultPlcVarType (this Type type) => + conversions.FirstOrDefault(x => x.GetDotnetType() == type)?.GetPlcVarType(); + + } + +} diff --git a/MewtocolNet/TypeConversion/PlcVarTypeConversions.cs b/MewtocolNet/TypeConversion/PlcVarTypeConversions.cs new file mode 100644 index 0000000..2ff6817 --- /dev/null +++ b/MewtocolNet/TypeConversion/PlcVarTypeConversions.cs @@ -0,0 +1,65 @@ +using MewtocolNet.Exceptions; +using MewtocolNet.Registers; +using MewtocolNet.TypeConversion; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace MewtocolNet { + + internal static class PlcVarTypeConversions { + + static List allowedCastingTypes = PlcValueParser.GetAllowDotnetTypes(); + + static List allowedGenericRegisters = PlcValueParser.GetAllowRegisterTypes(); + + internal static bool IsAllowedRegisterGenericType(this IRegister register) { + + return allowedGenericRegisters.Contains(register.GetType()); + + } + + internal static bool IsAllowedPlcCastingType() { + + return allowedCastingTypes.Contains(typeof(T)); + + } + + internal static bool IsAllowedPlcCastingType(this Type type) { + + return allowedCastingTypes.Contains(type); + + } + + internal static RegisterType ToRegisterTypeDefault(this Type type) { + + var found = PlcValueParser.GetDefaultRegisterType(type); + + if (found != null) { + + return found.Value; + + } + + throw new MewtocolException("No default register type found"); + + } + + internal static PlcVarType ToPlcVarType (this Type type) { + + var found = type.GetDefaultPlcVarType().Value; + + if (found != null) { + + return found; + + } + + throw new MewtocolException("No default plcvar type found"); + + } + + } + +} diff --git a/MewtocolTests/AutomatedPropertyRegisters.cs b/MewtocolTests/AutomatedPropertyRegisters.cs index cd46f3f..d0e4b1c 100644 --- a/MewtocolTests/AutomatedPropertyRegisters.cs +++ b/MewtocolTests/AutomatedPropertyRegisters.cs @@ -19,7 +19,7 @@ public class TestRegisterCollection : RegisterCollectionBase { //corresponds to a R100 boolean register in the PLC //can also be written as R1000 because the last one is a special address - [Register(IOType.R, 100, spAdress: 0)] + [Register(IOType.R, memoryArea: 85, spAdress: 0)] public bool TestBool1 { get; private set; } //corresponds to a XD input of the PLC @@ -60,10 +60,10 @@ public class TestRegisterCollection : RegisterCollectionBase { public BitArray TestBitRegister32 { 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)] + [Register(1204, BitCount.B16, 9)] public bool BitValue { get; private set; } - [Register(1204, 5, BitCount.B16)] + [Register(1204, BitCount.B32, 5)] public bool FillTest { get; private set; } //corresponds to a DT7012 - DT7013 as a 32bit time value that gets parsed as a timespan (TIME) @@ -115,7 +115,7 @@ public void BooleanGen() { var register = interf.GetRegister(nameof(TestRegisterCollection.TestBool1)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestBool1), false, 100, "R100"); + TestBasicGeneration(register, nameof(TestRegisterCollection.TestBool1), false, 85, "R85"); } @@ -208,8 +208,8 @@ public void StringGen() { //test generic properties TestBasicGeneration(register, nameof(TestRegisterCollection.TestString2), null!, 7005, "DT7005"); - Assert.Equal(5, ((SRegister)register).ReservedSize); - Assert.Equal(4, ((SRegister)register).MemoryLength); + Assert.Equal(5, ((BytesRegister)register).ReservedSize); + Assert.Equal(4, ((BytesRegister)register).MemoryLength); } @@ -224,8 +224,8 @@ public void BitArray16Gen() { //test generic properties TestBasicGeneration(register, "Auto_Bitwise_DT7010", (short)0, 7010, "DT7010"); - Assert.True(((NRegister)register).isUsedBitwise); - Assert.Equal(0, ((NRegister)register).MemoryLength); + Assert.True(((NumberRegister)register).isUsedBitwise); + Assert.Equal(0, ((NumberRegister)register).MemoryLength); } @@ -240,8 +240,8 @@ public void BitArray32Gen() { //test generic properties TestBasicGeneration(register, "Auto_Bitwise_DDT8010", (int)0, 8010, "DDT8010"); - Assert.True(((NRegister)register).isUsedBitwise); - Assert.Equal(1, ((NRegister)register).MemoryLength); + Assert.True(((NumberRegister)register).isUsedBitwise); + Assert.Equal(1, ((NumberRegister)register).MemoryLength); } @@ -256,7 +256,7 @@ public void BitArraySingleBool16Gen() { //test generic properties TestBasicGeneration(register, "Auto_Bitwise_DT1204", (short)0, 1204, "DT1204"); - Assert.True(((NRegister)register).isUsedBitwise); + Assert.True(((NumberRegister)register).isUsedBitwise); } diff --git a/MewtocolTests/TestRegisterBuilder.cs b/MewtocolTests/TestRegisterBuilder.cs index d72671c..24a63f3 100644 --- a/MewtocolTests/TestRegisterBuilder.cs +++ b/MewtocolTests/TestRegisterBuilder.cs @@ -16,32 +16,32 @@ public TestRegisterBuilder(ITestOutputHelper output) { this.output = output; } - [Fact(DisplayName = "Parsing as BRegister List (Phyiscal Outputs)")] + [Fact(DisplayName = "Parsing as Bool Register List (Phyiscal Outputs)")] public void TestParsingBRegisterY() { var tests = new Dictionary() { - {"Y0", new BRegister(IOType.Y)}, - {"Y1", new BRegister(IOType.Y, 0x1)}, - {"Y2", new BRegister(IOType.Y, 0x2)}, - {"Y3", new BRegister(IOType.Y, 0x3)}, - {"Y4", new BRegister(IOType.Y, 0x4)}, - {"Y5", new BRegister(IOType.Y, 0x5)}, - {"Y6", new BRegister(IOType.Y, 0x6)}, - {"Y7", new BRegister(IOType.Y, 0x7)}, - {"Y8", new BRegister(IOType.Y, 0x8)}, - {"Y9", new BRegister(IOType.Y, 0x9)}, - - {"YA", new BRegister(IOType.Y, 0xA)}, - {"YB", new BRegister(IOType.Y, 0xB)}, - {"YC", new BRegister(IOType.Y, 0xC)}, - {"YD", new BRegister(IOType.Y, 0xD)}, - {"YE", new BRegister(IOType.Y, 0xE)}, - {"YF", new BRegister(IOType.Y, 0xF)}, - - {"Y1A", new BRegister(IOType.Y, 0xA, 1)}, - {"Y10B", new BRegister(IOType.Y, 0xB, 10)}, - {"Y109C", new BRegister(IOType.Y, 0xC, 109)}, + {"Y0", new BoolRegister(IOType.Y)}, + {"Y1", new BoolRegister(IOType.Y, 0x1)}, + {"Y2", new BoolRegister(IOType.Y, 0x2)}, + {"Y3", new BoolRegister(IOType.Y, 0x3)}, + {"Y4", new BoolRegister(IOType.Y, 0x4)}, + {"Y5", new BoolRegister(IOType.Y, 0x5)}, + {"Y6", new BoolRegister(IOType.Y, 0x6)}, + {"Y7", new BoolRegister(IOType.Y, 0x7)}, + {"Y8", new BoolRegister(IOType.Y, 0x8)}, + {"Y9", new BoolRegister(IOType.Y, 0x9)}, + + {"YA", new BoolRegister(IOType.Y, 0xA)}, + {"YB", new BoolRegister(IOType.Y, 0xB)}, + {"YC", new BoolRegister(IOType.Y, 0xC)}, + {"YD", new BoolRegister(IOType.Y, 0xD)}, + {"YE", new BoolRegister(IOType.Y, 0xE)}, + {"YF", new BoolRegister(IOType.Y, 0xF)}, + + {"Y1A", new BoolRegister(IOType.Y, 0xA, 1)}, + {"Y10B", new BoolRegister(IOType.Y, 0xB, 10)}, + {"Y109C", new BoolRegister(IOType.Y, 0xC, 109)}, }; @@ -49,32 +49,32 @@ public void TestParsingBRegisterY() { } - [Fact(DisplayName = "Parsing as BRegister List (Phyiscal Inputs)")] + [Fact(DisplayName = "Parsing as Bool Register List (Phyiscal Inputs)")] public void TestParsingBRegisterX() { var tests = new Dictionary() { - {"X0", new BRegister(IOType.X)}, - {"X1", new BRegister(IOType.X, 0x1)}, - {"X2", new BRegister(IOType.X, 0x2)}, - {"X3", new BRegister(IOType.X, 0x3)}, - {"X4", new BRegister(IOType.X, 0x4)}, - {"X5", new BRegister(IOType.X, 0x5)}, - {"X6", new BRegister(IOType.X, 0x6)}, - {"X7", new BRegister(IOType.X, 0x7)}, - {"X8", new BRegister(IOType.X, 0x8)}, - {"X9", new BRegister(IOType.X, 0x9)}, - - {"XA", new BRegister(IOType.X, 0xA)}, - {"XB", new BRegister(IOType.X, 0xB)}, - {"XC", new BRegister(IOType.X, 0xC)}, - {"XD", new BRegister(IOType.X, 0xD)}, - {"XE", new BRegister(IOType.X, 0xE)}, - {"XF", new BRegister(IOType.X, 0xF)}, - - {"X1A", new BRegister(IOType.X, 0xA, 1)}, - {"X10B", new BRegister(IOType.X, 0xB, 10)}, - {"X109C", new BRegister(IOType.X, 0xC, 109)}, + {"X0", new BoolRegister(IOType.X)}, + {"X1", new BoolRegister(IOType.X, 0x1)}, + {"X2", new BoolRegister(IOType.X, 0x2)}, + {"X3", new BoolRegister(IOType.X, 0x3)}, + {"X4", new BoolRegister(IOType.X, 0x4)}, + {"X5", new BoolRegister(IOType.X, 0x5)}, + {"X6", new BoolRegister(IOType.X, 0x6)}, + {"X7", new BoolRegister(IOType.X, 0x7)}, + {"X8", new BoolRegister(IOType.X, 0x8)}, + {"X9", new BoolRegister(IOType.X, 0x9)}, + + {"XA", new BoolRegister(IOType.X, 0xA)}, + {"XB", new BoolRegister(IOType.X, 0xB)}, + {"XC", new BoolRegister(IOType.X, 0xC)}, + {"XD", new BoolRegister(IOType.X, 0xD)}, + {"XE", new BoolRegister(IOType.X, 0xE)}, + {"XF", new BoolRegister(IOType.X, 0xF)}, + + {"X1A", new BoolRegister(IOType.X, 0xA, 1)}, + {"X10B", new BoolRegister(IOType.X, 0xB, 10)}, + {"X109C", new BoolRegister(IOType.X, 0xC, 109)}, }; @@ -82,35 +82,35 @@ public void TestParsingBRegisterX() { } - [Fact(DisplayName = "Parsing as BRegister List (Internal Relay)")] + [Fact(DisplayName = "Parsing as Bool Register List (Internal Relay)")] public void TestParsingBRegisterR() { var tests = new Dictionary() { - {"R0", new BRegister(IOType.R)}, - {"R1", new BRegister(IOType.R, 0x1)}, - {"R2", new BRegister(IOType.R, 0x2)}, - {"R3", new BRegister(IOType.R, 0x3)}, - {"R4", new BRegister(IOType.R, 0x4)}, - {"R5", new BRegister(IOType.R, 0x5)}, - {"R6", new BRegister(IOType.R, 0x6)}, - {"R7", new BRegister(IOType.R, 0x7)}, - {"R8", new BRegister(IOType.R, 0x8)}, - {"R9", new BRegister(IOType.R, 0x9)}, - - {"RA", new BRegister(IOType.R, 0xA)}, - {"RB", new BRegister(IOType.R, 0xB)}, - {"RC", new BRegister(IOType.R, 0xC)}, - {"RD", new BRegister(IOType.R, 0xD)}, - {"RE", new BRegister(IOType.R, 0xE)}, - {"RF", new BRegister(IOType.R, 0xF)}, - - {"R1A", new BRegister(IOType.R, 0xA, 1)}, - {"R10B", new BRegister(IOType.R, 0xB, 10)}, - {"R109C", new BRegister(IOType.R, 0xC, 109)}, - {"R1000", new BRegister(IOType.R, 0x0, 100)}, - {"R511", new BRegister(IOType.R, 0x0, 511)}, - {"R511A", new BRegister(IOType.R, 0xA, 511)}, + {"R0", new BoolRegister(IOType.R)}, + {"R1", new BoolRegister(IOType.R, 0x1)}, + {"R2", new BoolRegister(IOType.R, 0x2)}, + {"R3", new BoolRegister(IOType.R, 0x3)}, + {"R4", new BoolRegister(IOType.R, 0x4)}, + {"R5", new BoolRegister(IOType.R, 0x5)}, + {"R6", new BoolRegister(IOType.R, 0x6)}, + {"R7", new BoolRegister(IOType.R, 0x7)}, + {"R8", new BoolRegister(IOType.R, 0x8)}, + {"R9", new BoolRegister(IOType.R, 0x9)}, + + {"RA", new BoolRegister(IOType.R, 0xA)}, + {"RB", new BoolRegister(IOType.R, 0xB)}, + {"RC", new BoolRegister(IOType.R, 0xC)}, + {"RD", new BoolRegister(IOType.R, 0xD)}, + {"RE", new BoolRegister(IOType.R, 0xE)}, + {"RF", new BoolRegister(IOType.R, 0xF)}, + + {"R1A", new BoolRegister(IOType.R, 0xA, 1)}, + {"R10B", new BoolRegister(IOType.R, 0xB, 10)}, + {"R109C", new BoolRegister(IOType.R, 0xC, 109)}, + {"R1000", new BoolRegister(IOType.R, 0x0, 100)}, + {"R511", new BoolRegister(IOType.R, 0x0, 511)}, + {"R511A", new BoolRegister(IOType.R, 0xA, 511)}, }; @@ -126,7 +126,7 @@ private void TestBoolDict (Dictionary dict) { output.WriteLine($"Expected: {item.Key}"); - var built = RegBuilder.FromPlcRegName(item.Key).AsPlcType(PlcVarType.BOOL).Build(); + var built = RegBuilder.Factory.FromPlcRegName(item.Key).AsPlcType(PlcVarType.BOOL).Build(); output.WriteLine($"{(built?.ToString(true) ?? "null")}\n"); Assert.Equivalent(item.Value, built); @@ -141,63 +141,75 @@ private void TestBoolDict (Dictionary dict) { } - [Fact(DisplayName = "Parsing as BRegister (Casted)")] + [Fact(DisplayName = "Parsing as Bool Register (Casted)")] public void TestRegisterBuildingBoolCasted () { - var expect = new BRegister(IOType.R, 0x1, 0); - var expect2 = new BRegister(IOType.Y, 0xA, 103); + var expect = new BoolRegister(IOType.R, 0x1, 0); + var expect2 = new BoolRegister(IOType.Y, 0xA, 103); - Assert.Equivalent(expect, RegBuilder.FromPlcRegName("R1").AsPlcType(PlcVarType.BOOL).Build()); - Assert.Equivalent(expect, RegBuilder.FromPlcRegName("R1").AsType().Build()); + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("R1").AsPlcType(PlcVarType.BOOL).Build()); + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("R1").AsType().Build()); - Assert.Equivalent(expect2, RegBuilder.FromPlcRegName("Y103A").AsPlcType(PlcVarType.BOOL).Build()); - Assert.Equivalent(expect2, RegBuilder.FromPlcRegName("Y103A").AsType().Build()); + Assert.Equivalent(expect2, RegBuilder.Factory.FromPlcRegName("Y103A").AsPlcType(PlcVarType.BOOL).Build()); + Assert.Equivalent(expect2, RegBuilder.Factory.FromPlcRegName("Y103A").AsType().Build()); } - [Fact(DisplayName = "Parsing as BRegister (Auto)")] + [Fact(DisplayName = "Parsing as Bool Register (Auto)")] public void TestRegisterBuildingBoolAuto () { - var expect = new BRegister(IOType.R, 0x1, 0); - var expect2 = new BRegister(IOType.Y, 0xA, 103); + var expect = new BoolRegister(IOType.R, 0x1, 0); + var expect2 = new BoolRegister(IOType.Y, 0xA, 103); - Assert.Equivalent(expect, RegBuilder.FromPlcRegName("R1").Build()); - Assert.Equivalent(expect, RegBuilder.FromPlcRegName("R1").Build()); + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("R1").Build()); + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("R1").Build()); - Assert.Equivalent(expect2, RegBuilder.FromPlcRegName("Y103A").Build()); - Assert.Equivalent(expect2, RegBuilder.FromPlcRegName("Y103A").Build()); + Assert.Equivalent(expect2, RegBuilder.Factory.FromPlcRegName("Y103A").Build()); + Assert.Equivalent(expect2, RegBuilder.Factory.FromPlcRegName("Y103A").Build()); } - [Fact(DisplayName = "Parsing as NRegister (Casted)")] + [Fact(DisplayName = "Parsing as Number Register (Casted)")] public void TestRegisterBuildingNumericCasted() { - var expect = new NRegister(303, null); - var expect2 = new NRegister(10002, null); - var expect3 = new NRegister(400, null); + var expect = new NumberRegister(303, null); + var expect2 = new NumberRegister(10002, null); + var expect3 = new NumberRegister(400, null); //var expect4 = new NRegister(103, null, true); - Assert.Equivalent(expect, RegBuilder.FromPlcRegName("DT303").AsPlcType(PlcVarType.INT).Build()); - Assert.Equivalent(expect, RegBuilder.FromPlcRegName("DT303").AsType().Build()); + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("DT303").AsPlcType(PlcVarType.INT).Build()); + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("DT303").AsType().Build()); - Assert.Equivalent(expect2, RegBuilder.FromPlcRegName("DDT10002").AsPlcType(PlcVarType.DINT).Build()); - Assert.Equivalent(expect2, RegBuilder.FromPlcRegName("DDT10002").AsType().Build()); + Assert.Equivalent(expect2, RegBuilder.Factory.FromPlcRegName("DDT10002").AsPlcType(PlcVarType.DINT).Build()); + Assert.Equivalent(expect2, RegBuilder.Factory.FromPlcRegName("DDT10002").AsType().Build()); - Assert.Equivalent(expect3, RegBuilder.FromPlcRegName("DDT400").AsPlcType(PlcVarType.TIME).Build()); - Assert.Equivalent(expect3, RegBuilder.FromPlcRegName("DDT400").AsType().Build()); + Assert.Equivalent(expect3, RegBuilder.Factory.FromPlcRegName("DDT400").AsPlcType(PlcVarType.TIME).Build()); + Assert.Equivalent(expect3, RegBuilder.Factory.FromPlcRegName("DDT400").AsType().Build()); //Assert.Equivalent(expect4, RegBuilder.FromPlcRegName("DT103").AsType().Build()); } - [Fact(DisplayName = "Parsing as NRegister (Auto)")] + [Fact(DisplayName = "Parsing as Number Register (Auto)")] public void TestRegisterBuildingNumericAuto() { - var expect = new NRegister(303, null); - var expect2 = new NRegister(10002, null); + var expect = new NumberRegister(303, null); + var expect2 = new NumberRegister(10002, null); + + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("DT303").Build()); + Assert.Equivalent(expect2, RegBuilder.Factory.FromPlcRegName("DDT10002").Build()); + + } + + [Fact(DisplayName = "Parsing as Bytes Register (Casted)")] + public void TestRegisterBuildingByteRangeCasted() { + + var expect = new BytesRegister(303, 5); + + + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("DT303").AsPlcType(PlcVarType.INT).Build()); + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("DT303").AsType().Build()); - Assert.Equivalent(expect, RegBuilder.FromPlcRegName("DT303").Build()); - Assert.Equivalent(expect2, RegBuilder.FromPlcRegName("DDT10002").Build()); } diff --git a/MewtocolTests/TestRegisterInterface.cs b/MewtocolTests/TestRegisterInterface.cs index bf11d1b..8a11726 100644 --- a/MewtocolTests/TestRegisterInterface.cs +++ b/MewtocolTests/TestRegisterInterface.cs @@ -17,12 +17,12 @@ public TestRegisterInterface(ITestOutputHelper output) { public void NumericRegisterMewtocolIdentifiers() { List registers = new List { - new NRegister(50, _name: null), - new NRegister(50, _name: null), - new NRegister(50, _name : null), - new NRegister(50, _name : null), - new NRegister(50, _name : null), - new NRegister(50, _name : null), + new NumberRegister(50, _name: null), + new NumberRegister(50, _name: null), + new NumberRegister(50, _name : null), + new NumberRegister(50, _name : null), + new NumberRegister(50, _name : null), + new NumberRegister(50, _name : null), }; List expectedIdents = new List { @@ -51,23 +51,23 @@ public void PLCRegisterIdentifiers() { List registers = new List { //numeric ones - new NRegister(50, _name: null), - new NRegister(60, _name : null), - new NRegister(70, _name : null), - new NRegister(80, _name : null), - new NRegister(90, _name : null), - new NRegister(100, _name : null), + new NumberRegister(50, _name: null), + new NumberRegister(60, _name : null), + new NumberRegister(70, _name : null), + new NumberRegister(80, _name : null), + new NumberRegister(90, _name : null), + new NumberRegister(100, _name : null), //boolean - new BRegister(IOType.R, 0, 100), - new BRegister(IOType.R, 0, 0), - new BRegister(IOType.X, 5), - new BRegister(IOType.X, 0xA), - new BRegister(IOType.X, 0xF, 109), - new BRegister(IOType.Y, 0xC, 75), + new BoolRegister(IOType.R, 0, 100), + new BoolRegister(IOType.R, 0, 0), + new BoolRegister(IOType.X, 5), + new BoolRegister(IOType.X, 0xA), + new BoolRegister(IOType.X, 0xF, 109), + new BoolRegister(IOType.Y, 0xC, 75), //string - new SRegister(999, 5), + new BytesRegister(999, 5), }; List expcectedIdents = new List { @@ -110,7 +110,7 @@ public void OverFlowRegisterAddress() { var ex = Assert.Throws(() => { - new NRegister(100000, _name: null); + new NumberRegister(100000, _name: null); }); @@ -118,7 +118,7 @@ public void OverFlowRegisterAddress() { var ex1 = Assert.Throws(() => { - new BRegister(IOType.R, _areaAdress: 512); + new BoolRegister(IOType.R, _areaAdress: 512); }); @@ -126,7 +126,7 @@ public void OverFlowRegisterAddress() { var ex2 = Assert.Throws(() => { - new BRegister(IOType.X, _areaAdress: 110); + new BoolRegister(IOType.X, _areaAdress: 110); }); @@ -134,7 +134,7 @@ public void OverFlowRegisterAddress() { var ex3 = Assert.Throws(() => { - new SRegister(100000, 5); + new BytesRegister(100000, 5); }); @@ -147,7 +147,7 @@ public void WrongDataTypeRegister() { var ex = Assert.Throws(() => { - new NRegister(100, _name: null); + new NumberRegister(100, _name: null); }); diff --git a/PLC_Test/test_c30_fpx_h.ini b/PLC_Test/test_c30_fpx_h.ini index e810e5053b49cd35fcbb9a00a3778ff8e0125482..90aaa531c22eca64cae0491a956aac4a25380712 100644 GIT binary patch delta 16 YcmX@Wc7Sa|4ddh`j6$3H7&kEj05-D)`2YX_ delta 14 VcmX@Wc7Sa|4I`t$=32(Xi~uKw1mFMw diff --git a/PLC_Test/test_c30_fpx_h.pro b/PLC_Test/test_c30_fpx_h.pro index fb087320a9349313ae522a4eebbcfad0389e9824..5b9447ff957c588719ffb9874c21de6929339927 100644 GIT binary patch delta 9161 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 diff --git a/PLC_Test/test_c30_fpx_h.xml b/PLC_Test/test_c30_fpx_h.xml index 4109899..9a30f61 100644 --- a/PLC_Test/test_c30_fpx_h.xml +++ b/PLC_Test/test_c30_fpx_h.xml @@ -2,10 +2,20 @@ - + - + + + + + + + + + + + From a9bd2962b494967c25b40d7fdca9bda06ccd6ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Tue, 27 Jun 2023 20:44:11 +0200 Subject: [PATCH 06/18] Added performance improvements for cyclic polling by using single frame building of multiple registers - cleaned and refactored codebase --- Examples/ExampleScenarios.cs | 101 +++++++- Examples/Program.cs | 19 +- MewtocolNet/DynamicInterface.cs | 200 +++++++++----- MewtocolNet/Exceptions/MewtocolException.cs | 6 +- MewtocolNet/IRegister.cs | 80 ++---- MewtocolNet/IRegisterInternal.cs | 51 +++- MewtocolNet/Logging/Logger.cs | 12 +- MewtocolNet/MewtocolHelpers.cs | 178 +++++-------- MewtocolNet/MewtocolInterface.cs | 75 ++++-- MewtocolNet/MewtocolInterfaceRequests.cs | 20 +- MewtocolNet/RegisterBuildInfo.cs | 62 ++--- .../RegisterBuilding/FinalizerExtensions.cs | 43 +-- .../RegisterBuilding/RegisterBuilderStep.cs | 21 +- MewtocolNet/RegisterEnums.cs | 2 +- MewtocolNet/Registers/BaseRegister.cs | 127 +++++++++ MewtocolNet/Registers/BoolRegister.cs | 131 +++------- MewtocolNet/Registers/BoolRegisterResult.cs | 20 -- MewtocolNet/Registers/BytesRegister.cs | 138 +++------- MewtocolNet/Registers/BytesRegisterResult.cs | 19 -- MewtocolNet/Registers/NumberRegister.cs | 245 +++++------------- MewtocolNet/Registers/NumberRegisterResult.cs | 35 --- MewtocolNet/Registers/StringRegister.cs | 87 +++++++ MewtocolNet/TCPMessageResult.cs | 12 + MewtocolNet/TypeConversion/Conversions.cs | 64 ++++- MewtocolNet/TypeConversion/PlcValueParser.cs | 5 +- MewtocolTests/AutomatedPropertyRegisters.cs | 111 ++------ .../ExpectedPlcInformationData.cs | 17 ++ .../RegisterReadWriteTest.cs | 20 ++ MewtocolTests/TestHelperExtensions.cs | 4 +- MewtocolTests/TestLivePLC.cs | 68 ++++- MewtocolTests/TestRegisterBuilder.cs | 22 +- MewtocolTests/TestRegisterInterface.cs | 28 +- PLC_Test/test_c30_fpx_h.pro | Bin 262144 -> 262144 bytes PLC_Test/test_c30_fpx_h.xml | 6 +- 34 files changed, 1085 insertions(+), 944 deletions(-) create mode 100644 MewtocolNet/Registers/BaseRegister.cs delete mode 100644 MewtocolNet/Registers/BoolRegisterResult.cs delete mode 100644 MewtocolNet/Registers/BytesRegisterResult.cs delete mode 100644 MewtocolNet/Registers/NumberRegisterResult.cs create mode 100644 MewtocolNet/Registers/StringRegister.cs create mode 100644 MewtocolNet/TCPMessageResult.cs create mode 100644 MewtocolTests/EncapsulatedTests/ExpectedPlcInformationData.cs create mode 100644 MewtocolTests/EncapsulatedTests/RegisterReadWriteTest.cs diff --git a/Examples/ExampleScenarios.cs b/Examples/ExampleScenarios.cs index df60f16..d4c82d6 100644 --- a/Examples/ExampleScenarios.cs +++ b/Examples/ExampleScenarios.cs @@ -7,20 +7,25 @@ using MewtocolNet.RegisterBuilding; using System.Collections.Generic; using MewtocolNet.Registers; +using System.Diagnostics; +using System.Text; namespace Examples; public class ExampleScenarios { - public static bool MewtocolLoggerEnabled = false; - public void SetupLogger () { //attaching the logger - Logger.LogLevel = LogLevel.Verbose; - Logger.OnNewLogMessage((date, msg) => { - if(MewtocolLoggerEnabled) - Console.WriteLine($"{date.ToString("HH:mm:ss")} {msg}"); + Logger.LogLevel = LogLevel.Info; + Logger.OnNewLogMessage((date, level, msg) => { + + if (level == LogLevel.Error) Console.ForegroundColor = ConsoleColor.Red; + + Console.WriteLine($"{date.ToString("HH:mm:ss")} {msg}"); + + Console.ResetColor(); + }); } @@ -61,7 +66,7 @@ public async Task RunCyclicPollerAsync () { foreach (var register in interf.Registers) { - Console.WriteLine($"{register.GetCombinedName()} / {register.GetRegisterPLCName()} - Value: {register.GetValueString()}"); + Console.WriteLine($"{register.ToString(true)}"); } @@ -186,10 +191,8 @@ public async Task RunEnumsBitwiseAsync () { [Scenario("Read register test")] public async Task RunReadTest () { - Console.WriteLine("Starting auto enums and bitwise"); - //setting up a new PLC interface and register collection - MewtocolInterface interf = new MewtocolInterface("192.168.115.210").WithPoller(); + MewtocolInterface interf = new MewtocolInterface("192.168.115.210"); //auto add all built registers to the interface var builder = RegBuilder.ForInterface(interf); @@ -200,8 +203,9 @@ public async Task RunReadTest () { var shortReg = builder.FromPlcRegName("DT35").AsPlcType(PlcVarType.INT).Build(); builder.FromPlcRegName("DDT36").AsPlcType(PlcVarType.DINT).Build(); + builder.FromPlcRegName("DT200").AsBytes(30).Build(); - //builder.FromPlcRegName("DDT38").AsPlcType(PlcVarType.TIME).Build(); + builder.FromPlcRegName("DDT38").AsPlcType(PlcVarType.TIME).Build(); //builder.FromPlcRegName("DT40").AsPlcType(PlcVarType.STRING).Build(); //connect @@ -214,12 +218,20 @@ public async Task RunReadTest () { await interf.SetRegisterAsync(r0reg, !(bool)r0reg.Value); await interf.SetRegisterAsync(shortReg, (short)new Random().Next(0, 100)); + var sw = Stopwatch.StartNew(); + foreach (var reg in interf.Registers) { - Console.WriteLine($"Register {reg.GetRegisterPLCName()} val: {reg.Value}"); + await reg.ReadAsync(); + + Console.WriteLine($"Register {reg.ToString()}"); } + sw.Stop(); + + Console.WriteLine($"Total read time for registers: {sw.Elapsed.TotalMilliseconds:N0}ms"); + Console.WriteLine(); await Task.Delay(1000); @@ -228,4 +240,69 @@ public async Task RunReadTest () { } + [Scenario("Test multi frame")] + public async Task MultiFrameTest() { + + var preLogLevel = Logger.LogLevel; + Logger.LogLevel = LogLevel.Error; + + //setting up a new PLC interface and register collection + MewtocolInterface interf = new MewtocolInterface("192.168.115.210"); + + //auto add all built registers to the interface + var builder = RegBuilder.ForInterface(interf); + var r0reg = builder.FromPlcRegName("R0").Build(); + builder.FromPlcRegName("R1").Build(); + builder.FromPlcRegName("R1F").Build(); + builder.FromPlcRegName("R60A").Build(); + builder.FromPlcRegName("R61A").Build(); + builder.FromPlcRegName("R62A").Build(); + builder.FromPlcRegName("R63A").Build(); + builder.FromPlcRegName("R64A").Build(); + builder.FromPlcRegName("R65A").Build(); + builder.FromPlcRegName("R66A").Build(); + builder.FromPlcRegName("R67A").Build(); + builder.FromPlcRegName("R68A").Build(); + builder.FromPlcRegName("R69A").Build(); + builder.FromPlcRegName("R70A").Build(); + builder.FromPlcRegName("R71A").Build(); + + //connect + await interf.ConnectAsync(); + + Console.WriteLine("Poller cycle started"); + var sw = Stopwatch.StartNew(); + + await interf.RunPollerCylceManual(false); + + sw.Stop(); + + Console.WriteLine("Poller cycle finished"); + + Console.WriteLine($"Single frame excec time: {sw.ElapsedMilliseconds:N0}ms"); + + interf.Disconnect(); + + await Task.Delay(1000); + + await interf.ConnectAsync(); + + sw = Stopwatch.StartNew(); + + Console.WriteLine("Poller cycle started"); + + await interf.RunPollerCylceManual(true); + + sw.Stop(); + + Console.WriteLine("Poller cycle finished"); + + Console.WriteLine($"Multi frame excec time: {sw.ElapsedMilliseconds:N0}ms"); + + Logger.LogLevel = preLogLevel; + + await Task.Delay(10000); + + } + } diff --git a/Examples/Program.cs b/Examples/Program.cs index 57c913f..3b2a56e 100644 --- a/Examples/Program.cs +++ b/Examples/Program.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; +using MewtocolNet.Logging; +using System.Text.RegularExpressions; namespace Examples; @@ -55,20 +57,23 @@ private static void LoopInput () { Console.WriteLine("\nEnter a number to excecute a example"); Console.ResetColor(); - Console.WriteLine("\nOther possible commands: \n\n" + - "'toggle logger' - toggle the built in mewtocol logger on/off\n" + - "'exit' - to close this program \n" + - "'clear' - to clear the output \n"); + Console.WriteLine("\nOther possible commands: \n"); + Console.WriteLine($"'logger ' - set loglevel to one of: {string.Join(", ", Enum.GetNames(typeof(LogLevel)).ToList())}"); + Console.WriteLine("'exit' - to close this program"); + Console.WriteLine("'clear' - to clear the output"); + Console.Write("> "); var line = Console.ReadLine(); - if (line == "toggle logger") { + var loggerMatch = Regex.Match(line, @"logger (?[a-zA-Z]{0,})"); + + if (loggerMatch.Success && Enum.TryParse(loggerMatch.Groups["level"].Value, out var loglevel)) { - ExampleScenarios.MewtocolLoggerEnabled = !ExampleScenarios.MewtocolLoggerEnabled; + Logger.LogLevel = loglevel; - Console.WriteLine(ExampleScenarios.MewtocolLoggerEnabled ? "Logger enabled" : "Logger disabled"); + Console.WriteLine($"Loglevel changed to: {loglevel}"); } else if (line == "exit") { diff --git a/MewtocolNet/DynamicInterface.cs b/MewtocolNet/DynamicInterface.cs index 1bd86c6..357bd52 100644 --- a/MewtocolNet/DynamicInterface.cs +++ b/MewtocolNet/DynamicInterface.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text; +using System.Threading; using System.Threading.Tasks; namespace MewtocolNet @@ -35,6 +37,7 @@ public partial class MewtocolInterface { internal volatile bool pollerFirstCycle = false; internal bool usePoller = false; + internal bool pollerUseMultiFrame = false; #region Register Polling @@ -93,66 +96,140 @@ internal void AttachPoller() { pollerFirstCycle = true; - Task.Factory.StartNew(async () => { + Task.Run(Poll); - Logger.Log("Poller is attaching", LogLevel.Info, this); + } - int iteration = 0; + /// + /// Runs a single poller cycle manually, + /// useful if you want to use a custom update frequency + /// + /// + public async Task RunPollerCylceManual (bool useMultiFrame = false) { - pollerTaskStopped = false; - pollerTaskRunning = true; - pollerIsPaused = false; + if (useMultiFrame) { + await OnMultiFrameCycle(); + } else { + await OnSingleFrameCycle(); + } - while (!pollerTaskStopped) { + } - while (pollerTaskRunning) { + //polls all registers one by one (slow) + internal async Task Poll () { - if (iteration >= Registers.Count + 1) { - iteration = 0; - //invoke cycle polled event - InvokePolledCycleDone(); - continue; - } + Logger.Log("Poller is attaching", LogLevel.Info, this); - if (iteration >= Registers.Count) { - await GetPLCInfoAsync(); - iteration++; - continue; - } + int iteration = 0; - var reg = Registers[iteration]; + pollerTaskStopped = false; + pollerTaskRunning = true; + pollerIsPaused = false; - if(reg.IsAllowedRegisterGenericType()) { + while (!pollerTaskStopped) { - var lastVal = reg.Value; + if (iteration >= Registers.Count + 1) { + iteration = 0; + //invoke cycle polled event + InvokePolledCycleDone(); + continue; + } - var rwReg = (IRegisterInternal)reg; + if(pollerUseMultiFrame) { + await OnMultiFrameCycle(); + } else { + await OnSingleFrameCycle(); + } - var readout = await rwReg.ReadAsync(this); + pollerFirstCycle = false; - if (lastVal != readout) { + iteration++; - rwReg.SetValueFromPLC(readout); - InvokeRegisterChanged(reg); - - } + pollerIsPaused = !pollerTaskRunning; - } + } + + pollerIsPaused = false; + + } + + private async Task OnSingleFrameCycle () { + + foreach (var reg in Registers) { + + if (reg.IsAllowedRegisterGenericType()) { - iteration++; - pollerFirstCycle = false; + var lastVal = reg.Value; - await Task.Delay(pollerDelayMs); + var rwReg = (IRegisterInternal)reg; + + var readout = await rwReg.ReadAsync(); + + if (lastVal != readout) { + + rwReg.SetValueFromPLC(readout); + InvokeRegisterChanged(reg); } - pollerIsPaused = !pollerTaskRunning; + } + + } + + await GetPLCInfoAsync(); + + } + + private async Task OnMultiFrameCycle () { + + await UpdateRCPRegisters(); + + await GetPLCInfoAsync(); + + } + + private async Task UpdateRCPRegisters () { + + //build booleans + var rcpList = Registers.Where(x => x.GetType() == typeof(BoolRegister)) + .Select(x => (BoolRegister)x) + .ToArray(); + + //one frame can only read 8 registers at a time + int rcpFrameCount = (int)Math.Ceiling((double)rcpList.Length / 8); + int rcpLastFrameRemainder = rcpList.Length <= 8 ? rcpList.Length : rcpList.Length % 8; + + for (int i = 0; i < rcpFrameCount; i++) { + + int toReadRegistersCount = 8; + + if(i == rcpFrameCount - 1) toReadRegistersCount = rcpLastFrameRemainder; + + var rcpString = new StringBuilder($"%{GetStationNumber()}#RCP{toReadRegistersCount}"); + + for (int j = 0; j < toReadRegistersCount; j++) { + + BoolRegister register = rcpList[i + j]; + rcpString.Append(register.BuildMewtocolQuery()); } - pollerIsPaused = false; + string rcpRequest = rcpString.ToString(); + var result = await SendCommandAsync(rcpRequest); + var resultBitArray = result.Response.ParseRCMultiBit(); + + for (int k = 0; k < resultBitArray.Length; k++) { - }); + var register = rcpList[i + k]; + + if((bool)register.Value != resultBitArray[k]) { + register.SetValueFromPLC(resultBitArray[k]); + InvokeRegisterChanged(register); + } + + } + + } } @@ -166,8 +243,6 @@ internal void PropertyRegisterWasSet(string propName, object value) { #region Register Colleciton adding - #region Register Collection - /// /// Attaches a register collection object to /// the interface that can be updated automatically. @@ -212,7 +287,7 @@ public MewtocolInterface WithRegisterCollection(RegisterCollectionBase collectio RegisterChanged += (reg) => { //register is used bitwise - if (reg.IsUsedBitwise()) { + if (reg.GetType() == typeof(BytesRegister)) { for (int i = 0; i < props.Length; i++) { @@ -314,8 +389,6 @@ public MewtocolInterface WithRegisterCollection(RegisterCollectionBase collectio #endregion - #endregion - #region Register Adding internal void AddRegister (RegisterBuildInfo buildInfo) { @@ -323,7 +396,7 @@ internal void AddRegister (RegisterBuildInfo buildInfo) { var builtRegister = buildInfo.Build(); //is bitwise and the register list already contains that area register - if(builtRegister.IsUsedBitwise() && CheckDuplicateRegister(builtRegister, out var existing)) { + if(builtRegister.GetType() == typeof(BytesRegister) && CheckDuplicateRegister(builtRegister, out var existing)) { return; @@ -335,11 +408,12 @@ internal void AddRegister (RegisterBuildInfo buildInfo) { if(CheckDuplicateNameRegister(builtRegister)) throw MewtocolException.DupeNameRegister(builtRegister); + builtRegister.attachedInterface = this; Registers.Add(builtRegister); } - public void AddRegister(IRegister register) { + public void AddRegister (BaseRegister register) { if (CheckDuplicateRegister(register)) throw MewtocolException.DupeRegister(register); @@ -347,29 +421,30 @@ public void AddRegister(IRegister register) { if (CheckDuplicateNameRegister(register)) throw MewtocolException.DupeNameRegister(register); + register.attachedInterface = this; Registers.Add(register); } - private bool CheckDuplicateRegister (IRegister instance, out IRegister foundDupe) { + private bool CheckDuplicateRegister (IRegisterInternal instance, out IRegisterInternal foundDupe) { - foundDupe = Registers.FirstOrDefault(x => x.CompareIsDuplicate(instance)); + foundDupe = RegistersInternal.FirstOrDefault(x => x.CompareIsDuplicate(instance)); - return Registers.Contains(instance) || foundDupe != null; + return RegistersInternal.Contains(instance) || foundDupe != null; } - private bool CheckDuplicateRegister(IRegister instance) { + private bool CheckDuplicateRegister(IRegisterInternal instance) { - var foundDupe = Registers.FirstOrDefault(x => x.CompareIsDuplicate(instance)); + var foundDupe = RegistersInternal.FirstOrDefault(x => x.CompareIsDuplicate(instance)); - return Registers.Contains(instance) || foundDupe != null; + return RegistersInternal.Contains(instance) || foundDupe != null; } - private bool CheckDuplicateNameRegister(IRegister instance) { + private bool CheckDuplicateNameRegister(IRegisterInternal instance) { - return Registers.Any(x => x.CompareIsNameDuplicate(instance)); + return RegistersInternal.Any(x => x.CompareIsNameDuplicate(instance)); } @@ -387,25 +462,6 @@ public IRegister GetRegister(string name) { } - /// - /// Gets a register that was added by its name - /// - /// The type of register - /// A casted register or the default value - public T GetRegister(string name) where T : IRegister { - try { - - var reg = Registers.FirstOrDefault(x => x.Name == name); - return (T)reg; - - } catch (InvalidCastException) { - - return default(T); - - } - - } - #endregion #region Register Reading @@ -413,9 +469,9 @@ public T GetRegister(string name) where T : IRegister { /// /// Gets a list of all added registers /// - public List GetAllRegisters() { + public IEnumerable GetAllRegisters() { - return Registers; + return Registers.Cast(); } diff --git a/MewtocolNet/Exceptions/MewtocolException.cs b/MewtocolNet/Exceptions/MewtocolException.cs index 0601df4..46c948e 100644 --- a/MewtocolNet/Exceptions/MewtocolException.cs +++ b/MewtocolNet/Exceptions/MewtocolException.cs @@ -17,15 +17,15 @@ protected MewtocolException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - public static MewtocolException DupeRegister (IRegister register) { + internal static MewtocolException DupeRegister (IRegisterInternal register) { return new MewtocolException($"The mewtocol interface already contains this register: {register.GetRegisterPLCName()}"); } - public static MewtocolException DupeNameRegister (IRegister register) { + internal static MewtocolException DupeNameRegister (IRegisterInternal register) { - return new MewtocolException($"The mewtocol interface registers already contains a register with the name: {register.Name}"); + return new MewtocolException($"The mewtocol interface registers already contains a register with the name: {register.GetRegisterPLCName()}"); } diff --git a/MewtocolNet/IRegister.cs b/MewtocolNet/IRegister.cs index c442584..6bd6bca 100644 --- a/MewtocolNet/IRegister.cs +++ b/MewtocolNet/IRegister.cs @@ -23,6 +23,11 @@ public interface IRegister { /// string Name { get; } + /// + /// Gets the register address name as in the plc + /// + string PLCAddressName { get; } + /// /// The current value of the register /// @@ -34,81 +39,26 @@ public interface IRegister { int MemoryAddress { get; } /// - /// Gets the special address of the register or -1 if it has none - /// - /// - byte? GetSpecialAddress(); - - /// - /// Indicates if the register is processed bitwise - /// - /// True if bitwise - bool IsUsedBitwise(); - - /// - /// Generates a string describing the starting memory address of the register - /// - string GetStartingMemoryArea(); - - /// - /// Gets the current value formatted as a readable string - /// - string GetValueString(); - - /// - /// Builds the identifier for the mewtocol query string - /// - /// - string BuildMewtocolQuery(); - - /// - /// Builds a register string that prepends the memory address fe. DDT or DT, X, Y etc - /// - string GetRegisterString(); - - /// - /// Builds a combined name for the attached property to uniquely identify the property register binding - /// - /// - string GetCombinedName(); - - /// - /// Gets the name of the class that contains the attached property - /// - /// - string GetContainerName(); - - /// - /// Builds a register name after the PLC convention
- /// Example DDT100, XA, X6, Y1, DT3300 - ///
- string GetRegisterPLCName(); - - /// - /// Clears the current value of the register and resets it to default - /// - void ClearValue(); - - /// - /// Triggers a notifychanged update event + /// Builds a readable string with all important register informations /// - void TriggerNotifyChange(); + string ToString(); /// - /// Gets the type of the class collection its attached property is in or null + /// Builds a readable string with all important register informations and additional infos /// - /// The class name or null if manually added - Type GetCollectionType(); + string ToString(bool detailed); /// - /// Builds a readable string with all important register informations + /// Sets the register value in the plc async /// - string ToString(); + /// True if successful + Task SetValueAsync(); /// - /// Builds a readable string with all important register informations and additional infos + /// Gets the register value from the plc async /// - string ToString(bool detailed); + /// The value or null if failed + Task GetValueAsync(); } diff --git a/MewtocolNet/IRegisterInternal.cs b/MewtocolNet/IRegisterInternal.cs index 9b9c5a4..e5b7a3a 100644 --- a/MewtocolNet/IRegisterInternal.cs +++ b/MewtocolNet/IRegisterInternal.cs @@ -5,13 +5,60 @@ namespace MewtocolNet { internal interface IRegisterInternal { + event Action ValueChanged; + + //props + + MewtocolInterface AttachedInterface { get; } + + RegisterType RegisterType { get; } + + string Name { get; } + + object Value { get; } + + int MemoryAddress { get; } + + // setters + void WithCollectionType(Type colType); void SetValueFromPLC(object value); - Task ReadAsync(MewtocolInterface interf); + void ClearValue(); + + // Accessors + + Type GetCollectionType(); + + string GetRegisterString(); + + string GetCombinedName(); + + string GetContainerName(); + + string GetRegisterPLCName(); + + byte? GetSpecialAddress(); + + string GetStartingMemoryArea(); + + string GetValueString(); + + string BuildMewtocolQuery(); + + + //others + + void TriggerNotifyChange(); + + Task ReadAsync(); + + Task WriteAsync(object data); + + string ToString(); - Task WriteAsync(MewtocolInterface interf, object data); + string ToString(bool detailed); } diff --git a/MewtocolNet/Logging/Logger.cs b/MewtocolNet/Logging/Logger.cs index d9d68ae..53e9c01 100644 --- a/MewtocolNet/Logging/Logger.cs +++ b/MewtocolNet/Logging/Logger.cs @@ -12,15 +12,15 @@ public static class Logger { /// public static LogLevel LogLevel { get; set; } - internal static Action LogInvoked; + internal static Action LogInvoked; /// /// Gets invoked whenever a new log message is ready /// - public static void OnNewLogMessage(Action onMsg) { + public static void OnNewLogMessage(Action onMsg) { - LogInvoked += (t, m) => { - onMsg(t, m); + LogInvoked += (t, l, m) => { + onMsg(t, l, m); }; } @@ -29,9 +29,9 @@ internal static void Log(string message, LogLevel loglevel, MewtocolInterface se if ((int)loglevel <= (int)LogLevel) { if (sender == null) { - LogInvoked?.Invoke(DateTime.Now, message); + LogInvoked?.Invoke(DateTime.Now, loglevel, message); } else { - LogInvoked?.Invoke(DateTime.Now, $"[{sender.GetConnectionPortInfo()}] {message}"); + LogInvoked?.Invoke(DateTime.Now, loglevel, $"[{sender.GetConnectionPortInfo()}] {message}"); } } diff --git a/MewtocolNet/MewtocolHelpers.cs b/MewtocolNet/MewtocolHelpers.cs index 7da65b8..38c0285 100644 --- a/MewtocolNet/MewtocolHelpers.cs +++ b/MewtocolNet/MewtocolHelpers.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -26,7 +27,7 @@ public static string ToBitString(this BitArray arr) { /// /// Converts a string (after converting to upper case) to ascii bytes /// - internal static byte[] ToHexASCIIBytes(this string _str) { + internal static byte[] BytesFromHexASCIIString(this string _str) { ASCIIEncoding ascii = new ASCIIEncoding(); byte[] bytes = ascii.GetBytes(_str.ToUpper()); @@ -73,31 +74,59 @@ internal static string ParseDTByteString(this string _onString, int _blockSize = } - internal static string ParseDTString(this string _onString) { + internal static BitArray ParseRCMultiBit(this string _onString) { - var res = new Regex(@"\%([0-9]{2})\$RD.{8}(.*)...").Match(_onString); + var res = new Regex(@"\%([0-9]{2})\$RC(?(?:0|1){0,8})(..)").Match(_onString); if (res.Success) { - string val = res.Groups[2].Value; - return val.GetStringFromAsciiHex()?.Trim(); + + string val = res.Groups["bits"].Value; + + return new BitArray(val.Select(c => c == '1').ToArray()); + } return null; } - internal static string ReverseByteOrder(this string _onString) { + /// + /// Parses a return string from the PLC as a raw byte array
+ /// Example: + /// + /// ↓Start ↓end + /// %01$RD0100020010\r + /// + /// This will return the byte array: + /// + /// [0x01, 0x00, 0x02, 0x00] + /// + ///
+ /// + /// A or null of failed + internal static byte[] ParseDTRawStringAsBytes (this string _onString) { + + var res = new Regex(@"\%([0-9]{2})\$RD(?.*)(?..)..").Match(_onString); + if (res.Success) { + + string val = res.Groups["data"].Value; + var parts = val.SplitInParts(2).ToArray(); + var bytes = new byte[parts.Length]; - if (_onString == null) return null; + for (int i = 0; i < bytes.Length; i++) { - //split into 2 chars - var stringBytes = _onString.SplitInParts(2).ToList(); + bytes[i] = byte.Parse(parts[i], NumberStyles.HexNumber); - stringBytes.Reverse(); + } - return string.Join("", stringBytes); + return bytes; + } + return null; } - internal static IEnumerable SplitInParts(this string s, int partLength) { + /// + /// Splits a string in even parts + /// + internal static IEnumerable SplitInParts(this string s, int partLength) { if (s == null) throw new ArgumentNullException(nameof(s)); @@ -109,63 +138,7 @@ internal static IEnumerable SplitInParts(this string s, int partLength) } - internal static string BuildDTString (this byte[] inBytes, short reservedSize) { - - StringBuilder sb = new StringBuilder(); - - //clamp string lenght - if (inBytes.Length > reservedSize) { - inBytes = inBytes.Take(reservedSize).ToArray(); - } - - //actual string content - var hexstring = inBytes.ToHexString(); - - var sizeBytes = BitConverter.GetBytes((short)(hexstring.Length / 2)).ToHexString(); - - if (hexstring.Length >= 2) { - - var remainderBytes = (hexstring.Length / 2) % 2; - - if (remainderBytes != 0) { - hexstring += "20"; - } - - } - - var reservedSizeBytes = BitConverter.GetBytes(reservedSize).ToHexString(); - - //reserved string count bytes - sb.Append(reservedSizeBytes); - //string count actual bytes - sb.Append(sizeBytes); - - - sb.Append(hexstring); - - return sb.ToString(); - } - - - internal static string GetStringFromAsciiHex(this string input) { - if (input.Length % 2 != 0) - return null; - 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); - } - - internal static string GetAsciiHexFromString(this string input) { - - var bytes = new ASCIIEncoding().GetBytes(input); - return bytes.ToHexString(); - - } - - internal static byte[] HexStringToByteArray(this string hex) { + internal static byte[] HexStringToByteArray (this string hex) { if (hex == null) return null; return Enumerable.Range(0, hex.Length) @@ -174,13 +147,39 @@ internal static byte[] HexStringToByteArray(this string hex) { .ToArray(); } - internal static string ToHexString(this byte[] arr) { + /// + /// Converts a byte array to a hexadecimal string + /// + /// Seperator between the hex numbers + /// The byte array + internal static string ToHexString (this byte[] arr, string seperator = "") { StringBuilder sb = new StringBuilder(); + for (int i = 0; i < arr.Length; i++) { byte b = arr[i]; sb.Append(b.ToString("X2")); + if(i < arr.Length - 1) sb.Append(seperator); } + + return sb.ToString(); + + } + + internal static string AsPLC (this TimeSpan timespan) { + + StringBuilder sb = new StringBuilder("T#"); + + int millis = timespan.Milliseconds; + int seconds = timespan.Seconds; + int minutes = timespan.Minutes; + int hours = timespan.Hours; + + if (hours > 0) sb.Append($"{hours}h"); + if (minutes > 0) sb.Append($"{minutes}m"); + if (seconds > 0) sb.Append($"{seconds}s"); + if (millis > 0) sb.Append($"{millis}ms"); + return sb.ToString(); } @@ -207,39 +206,6 @@ internal static byte[] BigToMixedEndian(this byte[] arr) { } - internal static bool IsDoubleNumericRegisterType(this Type type) { - - //Type[] singles = new Type[] { - // typeof(short), - // typeof(ushort), - //}; - - Type[] doubles = new Type[] { - typeof(int), - typeof(uint), - typeof(float), - typeof(TimeSpan), - }; - - return doubles.Contains(type); - - } - - internal static bool IsNumericSupportedType(this Type type) { - - Type[] supported = new Type[] { - typeof(short), - typeof(ushort), - typeof(int), - typeof(uint), - typeof(float), - typeof(TimeSpan), - }; - - return supported.Contains(type); - - } - /// /// Checks if the register type is boolean /// @@ -267,7 +233,7 @@ internal static bool IsPhysicalInOutType(this RegisterType type) { } - internal static bool CompareIsDuplicate (this IRegister reg1, IRegister compare) { + internal static bool CompareIsDuplicate (this IRegisterInternal reg1, IRegisterInternal compare) { bool valCompare = reg1.RegisterType == compare.RegisterType && reg1.MemoryAddress == compare.MemoryAddress && @@ -277,7 +243,7 @@ internal static bool CompareIsDuplicate (this IRegister reg1, IRegister compare) } - internal static bool CompareIsNameDuplicate(this IRegister reg1, IRegister compare) { + internal static bool CompareIsNameDuplicate(this IRegisterInternal reg1, IRegisterInternal compare) { return ( reg1.Name != null || compare.Name != null) && reg1.Name == compare.Name; diff --git a/MewtocolNet/MewtocolInterface.cs b/MewtocolNet/MewtocolInterface.cs index 273dc1e..98a3aa4 100644 --- a/MewtocolNet/MewtocolInterface.cs +++ b/MewtocolNet/MewtocolInterface.cs @@ -1,3 +1,4 @@ +using MewtocolNet.Exceptions; using MewtocolNet.Logging; using MewtocolNet.Queue; using MewtocolNet.RegisterAttributes; @@ -113,7 +114,9 @@ private set { /// /// The registered data registers of the PLC /// - public List Registers { get; set; } = new List(); + public List Registers { get; private set; } = new List(); + + internal IEnumerable RegistersInternal => Registers.Cast(); private string ip; private int port; @@ -208,11 +211,13 @@ void MewtocolInterface_Connected(PLCInfo obj) { RegisterChanged += (o) => { - string address = $"{o.GetRegisterString()}{o.GetStartingMemoryArea()}".PadRight(5, (char)32); + var asInternal = (IRegisterInternal)o; + + string address = $"{asInternal.GetRegisterString()}{asInternal.GetStartingMemoryArea()}".PadRight(5, (char)32); Logger.Log($"{address} " + $"{(o.Name != null ? $"({o.Name}) " : "")}" + - $"changed to \"{o.GetValueString()}\"", LogLevel.Change, this); + $"changed to \"{asInternal.GetValueString()}\"", LogLevel.Change, this); }; } @@ -305,9 +310,10 @@ public void Disconnect() { /// Attaches a poller to the interface that continously /// polls the registered data registers and writes the values to them /// - public MewtocolInterface WithPoller() { + public MewtocolInterface WithPoller(bool useMultiFrame = false) { usePoller = true; + pollerUseMultiFrame = useMultiFrame; return this; } @@ -397,7 +403,7 @@ private void ClearRegisterVals() { for (int i = 0; i < Registers.Count; i++) { - var reg = Registers[i]; + var reg = (IRegisterInternal)Registers[i]; reg.ClearValue(); } @@ -423,22 +429,38 @@ public async Task SendCommandAsync(string _msg) { queuedMessages++; - var response = await queue.Enqueue(() => SendSingleBlock(_msg)); + TCPMessageResult tcpResult = TCPMessageResult.Waiting; + string response = ""; - if (queuedMessages > 0) - queuedMessages--; + int lineFeedFails = 0; + + //recursively try to get a response on failed line feeds + while (tcpResult == TCPMessageResult.Waiting || tcpResult == TCPMessageResult.FailedLineFeed) { + + if (lineFeedFails >= 5) + throw new MewtocolException($"The message ${_msg} had {lineFeedFails} linefeed fails"); + + var tempResponse = await queue.Enqueue(() => SendSingleBlock(_msg)); + tcpResult = tempResponse.Item1; + response = tempResponse.Item2; + + if(tcpResult == TCPMessageResult.FailedLineFeed) { + lineFeedFails++; + Logger.Log($"Linefeed fail, retrying...", LogLevel.Error); + } + + if (queuedMessages > 0) + queuedMessages--; + + if (tcpResult == TCPMessageResult.FailedWithException) + throw new MewtocolException("The connection to the device was terminated"); - 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()); + Match m = errorcheck.Match(response); + if (m.Success) { string eCode = m.Groups[1].Value; string eDes = CodeDescriptions.Error[Convert.ToInt32(eCode)]; @@ -456,7 +478,7 @@ public async Task SendCommandAsync(string _msg) { return new CommandResult { Success = true, Error = "0000", - Response = response.ToString() + Response = response, }; } catch { @@ -469,16 +491,16 @@ public async Task SendCommandAsync(string _msg) { } - private async Task SendSingleBlock(string _blockString) { + private async Task<(TCPMessageResult, string)> SendSingleBlock(string _blockString) { if (client == null || !client.Connected) { await ConnectTCP(); } if (client == null || !client.Connected) - return null; + return (TCPMessageResult.NotConnected, null); - var message = _blockString.ToHexASCIIBytes(); + var message = _blockString.BytesFromHexASCIIString(); //time measuring if (speedStopwatchUpstr == null) { @@ -542,13 +564,16 @@ private async Task SendSingleBlock(string _blockString) { } catch (IOException) { OnMajorSocketExceptionWhileConnected(); - return null; + return (TCPMessageResult.FailedWithException, null); } catch (SocketException) { OnMajorSocketExceptionWhileConnected(); - return null; + return (TCPMessageResult.FailedWithException, null); } - if (!string.IsNullOrEmpty(response.ToString())) { + + string resString = response.ToString(); + + if (!string.IsNullOrEmpty(resString) && resString != "\r" ) { Logger.Log($"<-- IN MSG: {response}", LogLevel.Critical, this); @@ -559,10 +584,12 @@ private async Task SendSingleBlock(string _blockString) { if (perSecUpstream <= 10000) BytesPerSecondDownstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero); - return response.ToString(); + return (TCPMessageResult.Success, resString); } else { - return null; + + return (TCPMessageResult.FailedLineFeed, null); + } } diff --git a/MewtocolNet/MewtocolInterfaceRequests.cs b/MewtocolNet/MewtocolInterfaceRequests.cs index 6a3b9cc..3b0f4f9 100644 --- a/MewtocolNet/MewtocolInterfaceRequests.cs +++ b/MewtocolNet/MewtocolInterfaceRequests.cs @@ -159,10 +159,12 @@ public async Task ReadByteRange(int start, int count, Action onP #region Raw register reading / writing - internal async Task ReadRawRegisterAsync (IRegister _toRead) { + internal async Task ReadRawRegisterAsync (IRegisterInternal _toRead) { + + var toreadType = _toRead.GetType(); //returns a byte array 1 long and with the byte beeing 0 or 1 - if (_toRead.GetType() == typeof(BoolRegister)) { + if (toreadType == typeof(BoolRegister)) { string requeststring = $"%{GetStationNumber()}#RCS{_toRead.BuildMewtocolQuery()}"; var result = await SendCommandAsync(requeststring); @@ -177,7 +179,7 @@ internal async Task ReadRawRegisterAsync (IRegister _toRead) { } //returns a byte array 2 bytes or 4 bytes long depending on the data size - if (_toRead.GetType().GetGenericTypeDefinition() == typeof(NumberRegister<>)) { + if (toreadType.IsGenericType && _toRead.GetType().GetGenericTypeDefinition() == typeof(NumberRegister<>)) { string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolQuery()}"; var result = await SendCommandAsync(requeststring); @@ -198,7 +200,7 @@ internal async Task ReadRawRegisterAsync (IRegister _toRead) { } //returns a byte array with variable size - if (_toRead.GetType() == typeof(BytesRegister<>)) { + if (toreadType == typeof(BytesRegister)) { string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolQuery()}"; var result = await SendCommandAsync(requeststring); @@ -206,7 +208,9 @@ internal async Task ReadRawRegisterAsync (IRegister _toRead) { if (!result.Success) throw new Exception($"Failed to load the byte data for: {_toRead}"); - return result.Response.ParseDTString().ReverseByteOrder().HexStringToByteArray(); + var resBytes = result.Response.ParseDTRawStringAsBytes(); + + return resBytes; } @@ -214,7 +218,7 @@ internal async Task ReadRawRegisterAsync (IRegister _toRead) { } - internal async Task WriteRawRegisterAsync (IRegister _toWrite, byte[] data) { + internal async Task WriteRawRegisterAsync (IRegisterInternal _toWrite, byte[] data) { //returns a byte array 1 long and with the byte beeing 0 or 1 if (_toWrite.GetType() == typeof(BoolRegister)) { @@ -235,7 +239,7 @@ internal async Task WriteRawRegisterAsync (IRegister _toWrite, byte[] data } //returns a byte array with variable size - if (_toWrite.GetType() == typeof(BytesRegister<>)) { + if (_toWrite.GetType() == typeof(BytesRegister)) { //string stationNum = GetStationNumber(); //string dataString = gotBytes.BuildDTString(_toWrite.ReservedSize); @@ -259,7 +263,7 @@ public async Task SetRegisterAsync (IRegister register, object value) { var internalReg = (IRegisterInternal)register; - return await internalReg.WriteAsync(this, value); + return await internalReg.WriteAsync(value); } diff --git a/MewtocolNet/RegisterBuildInfo.cs b/MewtocolNet/RegisterBuildInfo.cs index c53074a..4ad24f6 100644 --- a/MewtocolNet/RegisterBuildInfo.cs +++ b/MewtocolNet/RegisterBuildInfo.cs @@ -17,90 +17,82 @@ internal struct RegisterBuildInfo { internal Type dotnetCastType; internal Type collectionType; - internal IRegister Build () { + internal BaseRegister Build () { RegisterType regType = registerType ?? dotnetCastType.ToRegisterTypeDefault(); - PlcVarType plcType = dotnetCastType.ToPlcVarType(); - Type registerClassType = plcType.GetDefaultPlcVarType(); + Type registerClassType = dotnetCastType.GetDefaultRegisterHoldingType(); - if (regType.IsNumericDTDDT() && (dotnetCastType == typeof(bool) || dotnetCastType == typeof(BitArray))) { + bool isBytesRegister = !registerClassType.IsGenericType && registerClassType == typeof(BytesRegister); + + if (regType.IsNumericDTDDT() && (dotnetCastType == typeof(bool))) { //------------------------------------------- //as numeric register with boolean bit target - - var type = typeof(NumberRegister); - - var areaAddr = memoryAddress; - //create a new bregister instance var flags = BindingFlags.Public | BindingFlags.Instance; - //int _adress, string _name = null, bool isBitwise = false, Type _enumType = null - var parameters = new object[] { areaAddr, name, true, null }; - var instance = (IRegister)Activator.CreateInstance(type, flags, null, parameters, null); + //int _adress, int _reservedByteSize, string _name = null + var parameters = new object[] { memoryAddress, memorySizeBytes, name }; + var instance = (BaseRegister)Activator.CreateInstance(typeof(BytesRegister), flags, null, parameters, null); if (collectionType != null) - ((IRegisterInternal)instance).WithCollectionType(collectionType); + instance.WithCollectionType(collectionType); return instance; - } else if (regType.IsNumericDTDDT()) { + } else if (regType.IsNumericDTDDT() && !isBytesRegister) { //------------------------------------------- //as numeric register - var type = plcType.GetDefaultPlcVarType(); - var areaAddr = memoryAddress; //create a new bregister instance var flags = BindingFlags.Public | BindingFlags.Instance; - //int _adress, string _name = null, bool isBitwise = false, Type _enumType = null - var parameters = new object[] { areaAddr, name, false, null }; - var instance = (IRegister)Activator.CreateInstance(type, flags, null, parameters, null); + //int _adress, Type _enumType = null, string _name = null + var parameters = new object[] { areaAddr, null, name }; + var instance = (BaseRegister)Activator.CreateInstance(registerClassType, flags, null, parameters, null); if(collectionType != null) - ((IRegisterInternal)instance).WithCollectionType(collectionType); + instance.WithCollectionType(collectionType); return instance; } - if (regType.IsBoolean()) { + if(isBytesRegister) { //------------------------------------------- - //as boolean register - - var io = (IOType)(int)regType; - var spAddr = specialAddress; - var areaAddr = memoryAddress; + //as byte range register //create a new bregister instance var flags = BindingFlags.Public | BindingFlags.Instance; - var parameters = new object[] { io, spAddr.Value, areaAddr, name }; - var instance = (BoolRegister)Activator.CreateInstance(typeof(BoolRegister), flags, null, parameters, null); + //int _adress, int _reservedSize, string _name = null + var parameters = new object[] { memoryAddress, memorySizeBytes, name }; + var instance = (BaseRegister)Activator.CreateInstance(typeof(BytesRegister), flags, null, parameters, null); if (collectionType != null) - ((IRegisterInternal)instance).WithCollectionType(collectionType); + instance.WithCollectionType(collectionType); return instance; } - if(regType == RegisterType.DT_RANGE) { + if (regType.IsBoolean()) { //------------------------------------------- - //as byte range register + //as boolean register - var type = plcType.GetDefaultPlcVarType(); + var io = (IOType)(int)regType; + var spAddr = specialAddress; + var areaAddr = memoryAddress; //create a new bregister instance var flags = BindingFlags.Public | BindingFlags.Instance; - //int _adress, int _reservedSize, string _name = null - var parameters = new object[] { memoryAddress, memorySizeBytes, name }; - var instance = (IRegister)Activator.CreateInstance(type, flags, null, parameters, null); + var parameters = new object[] { io, spAddr.Value, areaAddr, name }; + var instance = (BoolRegister)Activator.CreateInstance(typeof(BoolRegister), flags, null, parameters, null); if (collectionType != null) ((IRegisterInternal)instance).WithCollectionType(collectionType); diff --git a/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs b/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs index a06e77b..4772469 100644 --- a/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs +++ b/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs @@ -14,10 +14,31 @@ public static IRegister Build (this RegisterBuilderStep step) { if (!step.wasCasted) step.AutoType(); + //fallbacks if no casting builder was given + step.GetFallbackDotnetType(); + + var builtReg = new RegisterBuildInfo { + + name = step.Name, + specialAddress = step.SpecialAddress, + memoryAddress = step.MemAddress, + memorySizeBytes = step.MemByteSize, + registerType = step.RegType, + dotnetCastType = step.dotnetVarType, + + }.Build(); + + step.AddToRegisterList(builtReg); + + return builtReg; + + } + + private static void GetFallbackDotnetType (this RegisterBuilderStep step) { + bool isBoolean = step.RegType.IsBoolean(); bool isTypeNotDefined = step.plcVarType == null && step.dotnetVarType == null; - //fallbacks if no casting builder was given if (isTypeNotDefined && step.RegType == RegisterType.DT) { step.dotnetVarType = typeof(short); @@ -31,35 +52,21 @@ public static IRegister Build (this RegisterBuilderStep step) { step.dotnetVarType = typeof(bool); - } else if (isTypeNotDefined && step.RegType == RegisterType.DT_RANGE) { + } else if (isTypeNotDefined && step.RegType == RegisterType.DT_BYTE_RANGE) { step.dotnetVarType = typeof(string); } - if(step.plcVarType != null) { + if (step.plcVarType != null) { step.dotnetVarType = step.plcVarType.Value.GetDefaultDotnetType(); } - var builtReg = new RegisterBuildInfo { - - name = step.Name, - specialAddress = step.SpecialAddress, - memoryAddress = step.MemAddress, - registerType = step.RegType, - dotnetCastType = step.dotnetVarType, - - }.Build(); - - step.AddToRegisterList(builtReg); - - return builtReg; - } - private static void AddToRegisterList (this RegisterBuilderStep step, IRegister instance) { + private static void AddToRegisterList (this RegisterBuilderStep step, BaseRegister instance) { if (step.forInterface == null) return; diff --git a/MewtocolNet/RegisterBuilding/RegisterBuilderStep.cs b/MewtocolNet/RegisterBuilding/RegisterBuilderStep.cs index 04883be..c334a72 100644 --- a/MewtocolNet/RegisterBuilding/RegisterBuilderStep.cs +++ b/MewtocolNet/RegisterBuilding/RegisterBuilderStep.cs @@ -13,6 +13,7 @@ public class RegisterBuilderStep { internal string Name; internal RegisterType RegType; internal int MemAddress; + internal int MemByteSize; internal byte? SpecialAddress; internal PlcVarType? plcVarType; @@ -63,6 +64,24 @@ public RegisterBuilderStep AsType() { } + public RegisterBuilderStep AsBytes (int byteLength) { + + if (RegType != RegisterType.DT) { + + throw new NotSupportedException($"Cant use the AsByte converter on a non DT register"); + + } + + MemByteSize = byteLength; + dotnetVarType = typeof(byte[]); + plcVarType = null; + + wasCasted = true; + + return this; + + } + internal RegisterBuilderStep AutoType() { switch (RegType) { @@ -77,7 +96,7 @@ internal RegisterBuilderStep AutoType() { case RegisterType.DDT: dotnetVarType = typeof(int); break; - case RegisterType.DT_RANGE: + case RegisterType.DT_BYTE_RANGE: dotnetVarType = typeof(string); break; } diff --git a/MewtocolNet/RegisterEnums.cs b/MewtocolNet/RegisterEnums.cs index e23e89f..a4071a6 100644 --- a/MewtocolNet/RegisterEnums.cs +++ b/MewtocolNet/RegisterEnums.cs @@ -30,7 +30,7 @@ public enum RegisterType { /// /// Area of a byte sequence longer than 2 words /// - DT_RANGE = 5, + DT_BYTE_RANGE = 5, } diff --git a/MewtocolNet/Registers/BaseRegister.cs b/MewtocolNet/Registers/BaseRegister.cs new file mode 100644 index 0000000..13b0289 --- /dev/null +++ b/MewtocolNet/Registers/BaseRegister.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolNet.Registers { + + public abstract class BaseRegister : IRegister, IRegisterInternal, INotifyPropertyChanged { + + /// + /// Gets called whenever the value was changed + /// + public event Action ValueChanged; + + internal MewtocolInterface attachedInterface; + internal object lastValue; + internal Type collectionType; + internal string name; + internal int memoryAddress; + + /// + public MewtocolInterface AttachedInterface => attachedInterface; + + /// + public object Value => lastValue; + + /// + public RegisterType RegisterType { get; protected set; } + + /// + public Type CollectionType => collectionType; + + /// + public string Name => name; + + /// + public string PLCAddressName => GetRegisterPLCName(); + + /// + public int MemoryAddress => memoryAddress; + + #region Trigger update notify + + public event PropertyChangedEventHandler PropertyChanged; + + internal void TriggerChangedEvnt(object changed) => ValueChanged?.Invoke(changed); + + public void TriggerNotifyChange() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value")); + + #endregion + + public virtual void ClearValue() => SetValueFromPLC(null); + + public virtual void SetValueFromPLC(object val) { + + lastValue = val; + TriggerChangedEvnt(this); + TriggerNotifyChange(); + + } + + public void WithCollectionType(Type colType) => collectionType = colType; + + #region Default accessors + + public Type GetCollectionType() => CollectionType; + + public RegisterType GetRegisterType() => RegisterType; + + public virtual string BuildMewtocolQuery() { + + StringBuilder asciistring = new StringBuilder("D"); + asciistring.Append(MemoryAddress.ToString().PadLeft(5, '0')); + asciistring.Append(MemoryAddress.ToString().PadLeft(5, '0')); + return asciistring.ToString(); + + } + + public virtual string GetStartingMemoryArea() => MemoryAddress.ToString(); + + public virtual byte? GetSpecialAddress() => null; + + public virtual string GetValueString() => Value?.ToString() ?? "null"; + + public virtual string GetRegisterString() => RegisterType.ToString(); + + public virtual string GetCombinedName() => $"{(CollectionType != null ? $"{CollectionType.Name}." : "")}{Name ?? "Unnamed"}"; + + public virtual string GetContainerName() => $"{(CollectionType != null ? $"{CollectionType.Name}" : "")}"; + + public virtual string GetRegisterPLCName() => $"{GetRegisterString()}{MemoryAddress}"; + + #endregion + + #region Read / Write + + public virtual async Task ReadAsync() => throw new NotImplementedException(); + + public virtual async Task WriteAsync(object data) => throw new NotImplementedException(); + + public virtual Task SetValueAsync() => throw new NotImplementedException(); + + public virtual Task GetValueAsync() => throw new NotImplementedException(); + + #endregion + + public override string ToString() => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; + + public virtual string ToString(bool additional) { + + if (!additional) return this.ToString(); + + StringBuilder sb = new StringBuilder(); + sb.AppendLine($"PLC Naming: {GetRegisterPLCName()}"); + sb.AppendLine($"Name: {Name ?? "Not named"}"); + sb.AppendLine($"Value: {GetValueString()}"); + sb.AppendLine($"Register Type: {RegisterType}"); + sb.AppendLine($"Memory Address: {MemoryAddress}"); + + return sb.ToString(); + + } + + } + +} diff --git a/MewtocolNet/Registers/BoolRegister.cs b/MewtocolNet/Registers/BoolRegister.cs index 2ec09f4..3930814 100644 --- a/MewtocolNet/Registers/BoolRegister.cs +++ b/MewtocolNet/Registers/BoolRegister.cs @@ -8,45 +8,7 @@ namespace MewtocolNet.Registers { /// /// Defines a register containing a boolean /// - public class BoolRegister : IRegister, IRegisterInternal, INotifyPropertyChanged { - - /// - /// Gets called whenever the value was changed - /// - public event Action ValueChanged; - - /// - /// Triggers when a property on the register changes - /// - public event PropertyChangedEventHandler PropertyChanged; - - public RegisterType RegisterType { get; private set; } - - internal Type collectionType; - - /// - /// The type of collection the register is in or null of added manually - /// - public Type CollectionType => collectionType; - - internal bool lastValue; - - /// - /// The value of the register - /// - public object Value => lastValue; - - internal string name; - /// - /// The register name or null of not defined - /// - public string Name => name; - - internal int memoryAddress; - /// - /// The registers memory adress if not a special register - /// - public int MemoryAddress => memoryAddress; + public class BoolRegister : BaseRegister { internal byte specialAddress; /// @@ -77,6 +39,8 @@ public BoolRegister(IOType _io, byte _spAddress = 0x0, int _areaAdress = 0, stri if (_spAddress > 0xF) throw new NotSupportedException("Special address cant be greater 15 or 0xF"); + lastValue = false; + memoryAddress = _areaAdress; specialAddress = _spAddress; name = _name; @@ -85,61 +49,62 @@ public BoolRegister(IOType _io, byte _spAddress = 0x0, int _areaAdress = 0, stri } - public void WithCollectionType (Type colType) => collectionType = colType; + #region Read / Write - public byte? GetSpecialAddress() => SpecialAddress; + public override void SetValueFromPLC(object val) { - /// - /// Builds the register area name for the mewtocol protocol - /// - public string BuildMewtocolQuery() { + lastValue = (bool)val; + TriggerChangedEvnt(this); + TriggerNotifyChange(); - //(R|X|Y)(area add [3] + special add [1]) - StringBuilder asciistring = new StringBuilder(); + } - string prefix = RegisterType.ToString(); - string mem = MemoryAddress.ToString(); - string sp = SpecialAddress.ToString("X1"); + /// + public override async Task ReadAsync() { - asciistring.Append(prefix); - asciistring.Append(mem.PadLeft(3, '0')); - asciistring.Append(sp); + var read = await attachedInterface.ReadRawRegisterAsync(this); + var parsed = PlcValueParser.Parse(read); - return asciistring.ToString(); + SetValueFromPLC(parsed); + return parsed; } - public void SetValueFromPLC(object val) { + /// + public override async Task WriteAsync(object data) { - lastValue = (bool)val; - TriggerChangedEvnt(this); - TriggerNotifyChange(); + return await attachedInterface.WriteRawRegisterAsync(this, PlcValueParser.Encode((bool)data)); } - public string GetStartingMemoryArea() { + #endregion - return MemoryAddress.ToString(); + /// + public override byte? GetSpecialAddress() => SpecialAddress; - } - - public bool IsUsedBitwise() => false; + /// + public override string BuildMewtocolQuery() { - public Type GetCollectionType() => CollectionType; - - public RegisterType GetRegisterType() => RegisterType; + //(R|X|Y)(area add [3] + special add [1]) + StringBuilder asciistring = new StringBuilder(); - public string GetValueString() => Value.ToString(); + string prefix = RegisterType.ToString(); + string mem = MemoryAddress.ToString(); + string sp = SpecialAddress.ToString("X1"); - public void ClearValue() => SetValueFromPLC(false); + asciistring.Append(prefix); + asciistring.Append(mem.PadLeft(3, '0')); + asciistring.Append(sp); - public string GetRegisterString() => RegisterType.ToString(); + return asciistring.ToString(); - public string GetCombinedName() => $"{(CollectionType != null ? $"{CollectionType.Name}." : "")}{Name ?? "Unnamed"}"; + } - public string GetContainerName() => $"{(CollectionType != null ? $"{CollectionType.Name}" : "")}"; + /// + public override void ClearValue() => SetValueFromPLC(false); - public string GetRegisterPLCName() { + /// + public override string GetRegisterPLCName() { var spAdressEnd = SpecialAddress.ToString("X1"); @@ -159,13 +124,8 @@ public string GetRegisterPLCName() { } - internal void TriggerChangedEvnt(object changed) => ValueChanged?.Invoke(changed); - - public void TriggerNotifyChange() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value")); - - public override string ToString() => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; - - public string ToString(bool additional) { + /// + public override string ToString(bool additional) { if (!additional) return this.ToString(); @@ -181,19 +141,6 @@ public string ToString(bool additional) { } - public async Task ReadAsync (MewtocolInterface interf) { - - var read = await interf.ReadRawRegisterAsync(this); - return PlcValueParser.Parse(read); - - } - - public async Task WriteAsync (MewtocolInterface interf, object data) { - - return await interf.WriteRawRegisterAsync(this, PlcValueParser.Encode((bool)data)); - - } - } } diff --git a/MewtocolNet/Registers/BoolRegisterResult.cs b/MewtocolNet/Registers/BoolRegisterResult.cs deleted file mode 100644 index 4016a48..0000000 --- a/MewtocolNet/Registers/BoolRegisterResult.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace MewtocolNet.Registers { - - /// - /// Result for a boolean register - /// - public class BoolRegisterResult { - - /// - /// The command result - /// - public CommandResult Result { get; set; } - - /// - /// The used register - /// - public BoolRegister Register { get; set; } - - } - -} diff --git a/MewtocolNet/Registers/BytesRegister.cs b/MewtocolNet/Registers/BytesRegister.cs index 8ac6240..d247800 100644 --- a/MewtocolNet/Registers/BytesRegister.cs +++ b/MewtocolNet/Registers/BytesRegister.cs @@ -9,150 +9,80 @@ namespace MewtocolNet.Registers { /// /// Defines a register containing a string /// - public class BytesRegister : IRegister, IRegisterInternal { + public class BytesRegister : BaseRegister { + internal int addressLength; /// - /// Gets called whenever the value was changed + /// The rgisters memory length /// - public event Action ValueChanged; - - /// - /// Triggers when a property on the register changes - /// - public event PropertyChangedEventHandler PropertyChanged; - - public RegisterType RegisterType { get; private set; } - - internal Type collectionType; - - /// - /// The type of collection the register is in or null of added manually - /// - public Type CollectionType => collectionType; - - internal string lastValue; - - /// - /// The value of the register - /// - public object Value => lastValue; - - internal string name; - /// - /// The register name or null of not defined - /// - public string Name => name; - - internal int memoryAdress; - /// - /// The registers memory adress if not a special register - /// - public int MemoryAddress => memoryAdress; - - internal int memoryLength; - /// - /// The registers memory length - /// - public int MemoryLength => memoryLength; + public int AddressLength => addressLength; internal short ReservedSize { get; set; } /// /// Defines a register containing a string /// - public BytesRegister(int _adress, int _reservedSize, string _name = null) { + public BytesRegister(int _address, int _reservedByteSize, string _name = null) { - if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); + if (_address > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); name = _name; - memoryAdress = _adress; - ReservedSize = (short)_reservedSize; + memoryAddress = _address; + ReservedSize = (short)_reservedByteSize; - //calc mem length - var wordsize = (double)_reservedSize / 2; - if (wordsize % 2 != 0) { - wordsize++; - } + //calc mem length + //because one register is always 1 word (2 bytes) long, if the bytecount is uneven we get the trailing word too + var byteSize = _reservedByteSize; + if (_reservedByteSize % 2 != 0) byteSize++; - RegisterType = RegisterType.DT_RANGE; + RegisterType = RegisterType.DT_BYTE_RANGE; + addressLength = (byteSize / 2) - 1; - memoryLength = (int)Math.Round(wordsize + 1); } - public void WithCollectionType(Type colType) => collectionType = colType; + public override string GetValueString() => Value == null ? "null" : ((byte[])Value).ToHexString("-"); - /// - /// Builds the register identifier for the mewotocol protocol - /// - public string BuildMewtocolQuery() { + /// + public override string BuildMewtocolQuery() { StringBuilder asciistring = new StringBuilder("D"); asciistring.Append(MemoryAddress.ToString().PadLeft(5, '0')); - asciistring.Append((MemoryAddress + MemoryLength).ToString().PadLeft(5, '0')); + asciistring.Append((MemoryAddress + AddressLength).ToString().PadLeft(5, '0')); return asciistring.ToString(); } - internal string BuildCustomIdent(int overwriteWordLength) { - - if (overwriteWordLength <= 0) - throw new Exception("overwriteWordLength cant be 0 or less"); - - StringBuilder asciistring = new StringBuilder("D"); - - asciistring.Append(MemoryAddress.ToString().PadLeft(5, '0')); - asciistring.Append((MemoryAddress + overwriteWordLength - 1).ToString().PadLeft(5, '0')); - - return asciistring.ToString(); - } - - public byte? GetSpecialAddress() => null; - - public Type GetCollectionType() => CollectionType; + /// + public override void SetValueFromPLC (object val) { - public bool IsUsedBitwise() => false; - - public void SetValueFromPLC(object val) { - - lastValue = (string)val; + lastValue = (byte[])val; TriggerChangedEvnt(this); TriggerNotifyChange(); } - public string GetStartingMemoryArea() => MemoryAddress.ToString(); - - public string GetValueString() => Value?.ToString() ?? ""; - - public string GetRegisterString() => "DT"; - - public string GetCombinedName() => $"{(CollectionType != null ? $"{CollectionType.Name}." : "")}{Name ?? "Unnamed"}"; - - public string GetContainerName() => $"{(CollectionType != null ? $"{CollectionType.Name}" : "")}"; - - public string GetRegisterPLCName() => $"{GetRegisterString()}{MemoryAddress}"; - - public void ClearValue() => SetValueFromPLC(null); - - internal void TriggerChangedEvnt(object changed) => ValueChanged?.Invoke(changed); - - public void TriggerNotifyChange() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value")); + /// + public override string GetRegisterString() => "DT"; - public override string ToString() => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; + /// + public override void ClearValue() => SetValueFromPLC(null); - public string ToString(bool additional) => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; + /// + public override async Task ReadAsync() { - public async Task ReadAsync(MewtocolInterface interf) { + var read = await attachedInterface.ReadRawRegisterAsync(this); + var parsed = PlcValueParser.Parse(read); - var read = await interf.ReadRawRegisterAsync(this); - return PlcValueParser.Parse(read); + SetValueFromPLC(parsed); + return parsed; } - public async Task WriteAsync(MewtocolInterface interf, object data) { + /// + public override async Task WriteAsync(object data) { - return await interf.WriteRawRegisterAsync(this, (byte[])data); + return await attachedInterface.WriteRawRegisterAsync(this, (byte[])data); } diff --git a/MewtocolNet/Registers/BytesRegisterResult.cs b/MewtocolNet/Registers/BytesRegisterResult.cs deleted file mode 100644 index 314decf..0000000 --- a/MewtocolNet/Registers/BytesRegisterResult.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace MewtocolNet.Registers { - - /// - /// The results of a string register operation - /// - public class BytesRegisterResult { - - /// - /// The command result - /// - public CommandResult Result { get; set; } - /// - /// The register definition used - /// - public BytesRegister Register { get; set; } - - } - -} diff --git a/MewtocolNet/Registers/NumberRegister.cs b/MewtocolNet/Registers/NumberRegister.cs index b4f09c6..0b16135 100644 --- a/MewtocolNet/Registers/NumberRegister.cs +++ b/MewtocolNet/Registers/NumberRegister.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; @@ -11,131 +12,64 @@ namespace MewtocolNet.Registers { /// Defines a register containing a number /// /// The type of the numeric value - public class NumberRegister : IRegister, IRegisterInternal { + public class NumberRegister : BaseRegister { - /// - /// Gets called whenever the value was changed - /// - public event Action ValueChanged; - - /// - /// Triggers when a property on the register changes - /// - public event PropertyChangedEventHandler PropertyChanged; - - public RegisterType RegisterType { get; private set; } - - internal Type collectionType; + internal Type enumType { get; set; } /// - /// The type of collection the register is in or null of added manually + /// Defines a register containing a number /// - public Type CollectionType => collectionType; + /// Memory start adress max 99999 + /// Name of the register + public NumberRegister (int _address, string _name = null) { - internal T lastValue; + if (_address > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); - /// - /// The value of the register - /// - public object Value => lastValue; + memoryAddress = _address; + name = _name; - internal string name; - /// - /// The register name or null of not defined - /// - public string Name => name; + Type numType = typeof(T); - internal int memoryAdress; - /// - /// The registers memory adress if not a special register - /// - public int MemoryAddress => memoryAdress; + var allowedTypes = PlcValueParser.GetAllowDotnetTypes(); + if (!allowedTypes.Contains(numType)) + throw new NotSupportedException($"The type {numType} is not allowed for Number Registers"); - internal int memoryLength; - /// - /// The rgisters memory length - /// - public int MemoryLength => memoryLength; + var areaLen = (Marshal.SizeOf(numType) / 2) - 1; + RegisterType = areaLen >= 1 ? RegisterType.DDT : RegisterType.DT; - internal bool isUsedBitwise { get; set; } + lastValue = default(T); - internal Type enumType { get; set; } + } /// /// Defines a register containing a number /// - /// Memory start adress max 99999 + /// Memory start adress max 99999 + /// Enum type to parse as /// Name of the register - public NumberRegister (int _adress, string _name = null) { + public NumberRegister(int _address, Type _enumType, string _name = null) { - if (_adress > 99999) - throw new NotSupportedException("Memory adresses cant be greater than 99999"); + if (_address > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); - memoryAdress = _adress; + memoryAddress = _address; 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 if (numType == typeof(TimeSpan)) { - memoryLength = 1; - } else { - throw new NotSupportedException($"The type {numType} is not allowed for Number Registers"); - } - - //set register type - if(memoryLength == 1) { - RegisterType = RegisterType.DDT; - } else { - RegisterType = RegisterType.DT; - } - - } - public NumberRegister (int _adress, string _name = null, bool isBitwise = false, Type _enumType = 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 if (numType == typeof(TimeSpan)) { - memoryLength = 1; - } else { + + var allowedTypes = PlcValueParser.GetAllowDotnetTypes(); + if (!allowedTypes.Contains(numType)) throw new NotSupportedException($"The type {numType} is not allowed for Number Registers"); - } - //set register type - if (memoryLength == 1) { - RegisterType = RegisterType.DDT; - } else { - RegisterType = RegisterType.DT; - } + var areaLen = (Marshal.SizeOf(numType) / 2) - 1; + RegisterType = areaLen >= 1 ? RegisterType.DDT : RegisterType.DT; - isUsedBitwise = isBitwise; enumType = _enumType; + lastValue = default(T); } - public void WithCollectionType(Type colType) => collectionType = colType; - - public void SetValueFromPLC(object val) { + /// + public override void SetValueFromPLC(object val) { lastValue = (T)val; TriggerChangedEvnt(this); @@ -143,20 +77,34 @@ public void SetValueFromPLC(object val) { } - public byte? GetSpecialAddress() => null; + /// + public override string BuildMewtocolQuery() { - public string GetStartingMemoryArea() => MemoryAddress.ToString(); + StringBuilder asciistring = new StringBuilder("D"); + asciistring.Append(MemoryAddress.ToString().PadLeft(5, '0')); + + int offsetAddress = 0; + if(RegisterType == RegisterType.DDT) + offsetAddress = 1; - public Type GetCollectionType() => CollectionType; + asciistring.Append((MemoryAddress + offsetAddress).ToString().PadLeft(5, '0')); + return asciistring.ToString(); + + } - public bool IsUsedBitwise() => isUsedBitwise; + /// + public override string GetValueString() { - public string GetValueString() { + if(typeof(T) == typeof(TimeSpan)) { + + return $"{Value} [{((TimeSpan)Value).AsPLC()}]"; + + } //is number or bitwise if (enumType == null) { - return $"{Value}{(isUsedBitwise ? $" [{GetBitwise().ToBitString()}]" : "")}"; + return $"{Value}"; } @@ -204,6 +152,27 @@ public string GetValueString() { } + /// + public override void ClearValue() => SetValueFromPLC(default(T)); + + /// + public override async Task ReadAsync() { + + var read = await attachedInterface.ReadRawRegisterAsync(this); + var parsed = PlcValueParser.Parse(read); + + SetValueFromPLC(parsed); + return parsed; + + } + + /// + public override async Task WriteAsync(object data) { + + return await attachedInterface.WriteRawRegisterAsync(this, PlcValueParser.Encode((T)data)); + + } + /// /// Gets the register bitwise if its a 16 or 32 bit int /// @@ -230,76 +199,6 @@ public BitArray GetBitwise() { } - public string BuildMewtocolQuery() { - - StringBuilder asciistring = new StringBuilder("D"); - asciistring.Append(MemoryAddress.ToString().PadLeft(5, '0')); - asciistring.Append((MemoryAddress + MemoryLength).ToString().PadLeft(5, '0')); - return asciistring.ToString(); - - } - - public string GetRegisterString() { - - if (Value is short) { - return "DT"; - } - - if (Value is ushort) { - return "DT"; - } - - if (Value is int) { - return "DDT"; - } - - if (Value is uint) { - return "DDT"; - } - - if (Value is float) { - return "DDT"; - } - - if (Value is TimeSpan) { - return "DDT"; - } - - throw new NotSupportedException("Numeric type is not supported"); - - } - - public string GetCombinedName() => $"{(CollectionType != null ? $"{CollectionType.Name}." : "")}{Name ?? "Unnamed"}"; - - public string GetContainerName() => $"{(CollectionType != null ? $"{CollectionType.Name}" : "")}"; - - public string GetRegisterPLCName() => $"{GetRegisterString()}{MemoryAddress}"; - - public void ClearValue() => SetValueFromPLC(default(T)); - - internal void TriggerChangedEvnt(object changed) => ValueChanged?.Invoke(changed); - - public void TriggerNotifyChange() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value")); - - public RegisterType GetRegisterType() => RegisterType; - - public override string ToString() => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; - - public string ToString(bool additional) => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; - - public async Task ReadAsync(MewtocolInterface interf) { - - var read = await interf.ReadRawRegisterAsync(this); - return PlcValueParser.Parse(read); - - } - - public async Task WriteAsync(MewtocolInterface interf, object data) { - - return await interf.WriteRawRegisterAsync(this, PlcValueParser.Encode((T)data)); - - } - } } diff --git a/MewtocolNet/Registers/NumberRegisterResult.cs b/MewtocolNet/Registers/NumberRegisterResult.cs deleted file mode 100644 index 04bdec3..0000000 --- a/MewtocolNet/Registers/NumberRegisterResult.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace MewtocolNet.Registers { - - /// - /// Result for a read/write operation - /// - /// The type of the numeric value - public class NumberRegisterResult { - - /// - /// Command result - /// - public CommandResult Result { get; set; } - - /// - /// The used register - /// - public NumberRegister Register { get; set; } - - /// - /// Trys to get the value of there is one - /// - public bool TryGetValue(out T value) { - - if (Result.Success) { - value = (T)Register.Value; - return true; - } - value = default; - return false; - - } - - } - -} diff --git a/MewtocolNet/Registers/StringRegister.cs b/MewtocolNet/Registers/StringRegister.cs new file mode 100644 index 0000000..14248a3 --- /dev/null +++ b/MewtocolNet/Registers/StringRegister.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolNet.Registers { + + /// + /// Defines a register containing a string + /// + public class StringRegister : BaseRegister { + + internal int addressLength; + /// + /// The rgisters memory length + /// + public int AddressLength => addressLength; + + internal short ReservedSize { get; set; } + + /// + /// Defines a register containing a string + /// + public StringRegister (int _adress, int _reservedByteSize, string _name = null) { + + if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); + name = _name; + memoryAddress = _adress; + ReservedSize = (short)_reservedByteSize; + + //calc mem length + var wordsize = (double)_reservedByteSize / 2; + if (wordsize % 2 != 0) { + wordsize++; + } + + RegisterType = RegisterType.DT_BYTE_RANGE; + addressLength = (int)Math.Round(wordsize + 1); + + } + + /// + public override string BuildMewtocolQuery() { + + StringBuilder asciistring = new StringBuilder("D"); + + asciistring.Append(MemoryAddress.ToString().PadLeft(5, '0')); + asciistring.Append((MemoryAddress + AddressLength).ToString().PadLeft(5, '0')); + + return asciistring.ToString(); + } + + /// + public override void SetValueFromPLC (object val) { + + lastValue = (byte[])val; + + TriggerChangedEvnt(this); + TriggerNotifyChange(); + + } + + /// + public override string GetRegisterString() => "DT"; + + /// + public override void ClearValue() => SetValueFromPLC(null); + + /// + public override async Task ReadAsync() { + + var read = await attachedInterface.ReadRawRegisterAsync(this); + return PlcValueParser.Parse(read); + + } + + /// + public override async Task WriteAsync(object data) { + + return await attachedInterface.WriteRawRegisterAsync(this, (byte[])data); + + } + + } + +} diff --git a/MewtocolNet/TCPMessageResult.cs b/MewtocolNet/TCPMessageResult.cs new file mode 100644 index 0000000..d3e23eb --- /dev/null +++ b/MewtocolNet/TCPMessageResult.cs @@ -0,0 +1,12 @@ +namespace MewtocolNet { + internal enum TCPMessageResult { + + Waiting, + Success, + NotConnected, + FailedWithException, + FailedLineFeed, + + } + +} \ No newline at end of file diff --git a/MewtocolNet/TypeConversion/Conversions.cs b/MewtocolNet/TypeConversion/Conversions.cs index db72369..41543e8 100644 --- a/MewtocolNet/TypeConversion/Conversions.cs +++ b/MewtocolNet/TypeConversion/Conversions.cs @@ -1,5 +1,6 @@ using MewtocolNet.Registers; using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Text; @@ -19,10 +20,13 @@ internal static class Conversions { { PlcVarType.TIME, RegisterType.DDT }, { PlcVarType.WORD, RegisterType.DT }, { PlcVarType.DWORD, RegisterType.DDT }, - { PlcVarType.STRING, RegisterType.DT_RANGE }, + { PlcVarType.STRING, RegisterType.DT_BYTE_RANGE }, }; + /// + /// All conversions for reading dataf from and to the plc + /// internal static List items = new List { new PlcTypeConversion(RegisterType.R) { @@ -116,6 +120,64 @@ internal static class Conversions { }, }, + new PlcTypeConversion(RegisterType.DDT) { + HoldingRegisterType = typeof(NumberRegister), + PlcVarType = PlcVarType.REAL, + FromRaw = bytes => { + + var val = BitConverter.ToUInt32(bytes, 0); + byte[] floatVals = BitConverter.GetBytes(val); + float finalFloat = BitConverter.ToSingle(floatVals, 0); + + return finalFloat; + + }, + ToRaw = value => { + + return BitConverter.GetBytes(value); + + }, + }, + new PlcTypeConversion(RegisterType.DDT) { + HoldingRegisterType = typeof(NumberRegister), + PlcVarType = PlcVarType.TIME, + FromRaw = bytes => { + + var vallong = BitConverter.ToUInt32(bytes, 0); + var valMillis = vallong * 10; + var ts = TimeSpan.FromMilliseconds(valMillis); + return ts; + + }, + ToRaw = value => { + + var tLong = (uint)(value.TotalMilliseconds / 10); + return BitConverter.GetBytes(tLong); + + }, + }, + new PlcTypeConversion(RegisterType.DT) { + HoldingRegisterType = typeof(BytesRegister), + FromRaw = bytes => bytes, + ToRaw = value => value, + }, + new PlcTypeConversion(RegisterType.DT) { + HoldingRegisterType = typeof(BytesRegister), + PlcVarType = PlcVarType.WORD, + FromRaw = bytes => { + + BitArray bitAr = new BitArray(bytes); + return bitAr; + + }, + ToRaw = value => { + + byte[] ret = new byte[(value.Length - 1) / 8 + 1]; + value.CopyTo(ret, 0); + return ret; + + }, + }, }; diff --git a/MewtocolNet/TypeConversion/PlcValueParser.cs b/MewtocolNet/TypeConversion/PlcValueParser.cs index 05a34a1..c6c5497 100644 --- a/MewtocolNet/TypeConversion/PlcValueParser.cs +++ b/MewtocolNet/TypeConversion/PlcValueParser.cs @@ -41,9 +41,12 @@ public static T Parse(byte[] bytes) { public static RegisterType? GetDefaultRegisterType (Type type) => conversions.FirstOrDefault(x => x.GetDotnetType() == type)?.GetPlcRegisterType(); - public static Type GetDefaultPlcVarType (this PlcVarType type) => + public static Type GetDefaultRegisterHoldingType (this PlcVarType type) => conversions.FirstOrDefault(x => x.GetPlcVarType() == type)?.GetHoldingRegisterType(); + public static Type GetDefaultRegisterHoldingType (this Type type) => + conversions.FirstOrDefault(x => x.GetDotnetType() == type)?.GetHoldingRegisterType(); + public static Type GetDefaultDotnetType (this PlcVarType type) => conversions.FirstOrDefault(x => x.GetPlcVarType() == type)?.GetDotnetType(); diff --git a/MewtocolTests/AutomatedPropertyRegisters.cs b/MewtocolTests/AutomatedPropertyRegisters.cs index d0e4b1c..df6830a 100644 --- a/MewtocolTests/AutomatedPropertyRegisters.cs +++ b/MewtocolTests/AutomatedPropertyRegisters.cs @@ -91,7 +91,7 @@ public enum CurrentState { } - private void TestBasicGeneration(IRegister reg, string propName, object expectValue, int expectAddr, string expectPlcName) { + private void TestBasicGeneration(IRegisterInternal reg, string propName, object expectValue, int expectAddr, string expectPlcName) { Assert.NotNull(reg); Assert.Equal(propName, reg.Name); @@ -115,7 +115,7 @@ public void BooleanGen() { var register = interf.GetRegister(nameof(TestRegisterCollection.TestBool1)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestBool1), false, 85, "R85"); + TestBasicGeneration((IRegisterInternal)register, nameof(TestRegisterCollection.TestBool1), false, 85, "R85"); } @@ -128,7 +128,7 @@ public void BooleanInputGen() { var register = interf.GetRegister(nameof(TestRegisterCollection.TestBoolInputXD)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestBoolInputXD), false, 0, "XD"); + TestBasicGeneration((IRegisterInternal)register, nameof(TestRegisterCollection.TestBoolInputXD), false, 0, "XD"); } @@ -141,7 +141,7 @@ public void Int16Gen() { var register = interf.GetRegister(nameof(TestRegisterCollection.TestInt16)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestInt16), (short)0, 899, "DT899"); + TestBasicGeneration((IRegisterInternal)register, nameof(TestRegisterCollection.TestInt16), (short)0, 899, "DT899"); } @@ -154,7 +154,7 @@ public void UInt16Gen() { var register = interf.GetRegister(nameof(TestRegisterCollection.TestUInt16)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestUInt16), (ushort)0, 342, "DT342"); + TestBasicGeneration((IRegisterInternal)register, nameof(TestRegisterCollection.TestUInt16), (ushort)0, 342, "DT342"); } @@ -167,7 +167,7 @@ public void Int32Gen() { var register = interf.GetRegister(nameof(TestRegisterCollection.TestInt32)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestInt32), (int)0, 7001, "DDT7001"); + TestBasicGeneration((IRegisterInternal)register, nameof(TestRegisterCollection.TestInt32), (int)0, 7001, "DDT7001"); } @@ -180,7 +180,7 @@ public void UInt32Gen() { var register = interf.GetRegister(nameof(TestRegisterCollection.TestUInt32)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestUInt32), (uint)0, 765, "DDT765"); + TestBasicGeneration((IRegisterInternal)register, nameof(TestRegisterCollection.TestUInt32), (uint)0, 765, "DDT765"); } @@ -193,70 +193,7 @@ public void Float32Gen() { var register = interf.GetRegister(nameof(TestRegisterCollection.TestFloat32)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestFloat32), (float)0, 7003, "DDT7003"); - - } - - [Fact(DisplayName = "String generation")] - public void StringGen() { - - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); - - var register = interf.GetRegister(nameof(TestRegisterCollection.TestString2)); - - //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestString2), null!, 7005, "DT7005"); - - Assert.Equal(5, ((BytesRegister)register).ReservedSize); - Assert.Equal(4, ((BytesRegister)register).MemoryLength); - - } - - [Fact(DisplayName = "BitArray 16bit generation")] - public void BitArray16Gen() { - - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); - - var register = interf.GetRegister($"Auto_Bitwise_DT7010"); - - //test generic properties - TestBasicGeneration(register, "Auto_Bitwise_DT7010", (short)0, 7010, "DT7010"); - - Assert.True(((NumberRegister)register).isUsedBitwise); - Assert.Equal(0, ((NumberRegister)register).MemoryLength); - - } - - [Fact(DisplayName = "BitArray 32bit generation")] - public void BitArray32Gen() { - - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); - - var register = interf.GetRegister($"Auto_Bitwise_DDT8010"); - - //test generic properties - TestBasicGeneration(register, "Auto_Bitwise_DDT8010", (int)0, 8010, "DDT8010"); - - Assert.True(((NumberRegister)register).isUsedBitwise); - Assert.Equal(1, ((NumberRegister)register).MemoryLength); - - } - - [Fact(DisplayName = "BitArray single bool generation")] - public void BitArraySingleBool16Gen() { - - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); - - var register = interf.GetRegister($"Auto_Bitwise_DT1204"); - - //test generic properties - TestBasicGeneration(register, "Auto_Bitwise_DT1204", (short)0, 1204, "DT1204"); - - Assert.True(((NumberRegister)register).isUsedBitwise); + TestBasicGeneration((IRegisterInternal)register, nameof(TestRegisterCollection.TestFloat32), (float)0, 7003, "DDT7003"); } @@ -269,35 +206,25 @@ public void TimespanGen() { var register = interf.GetRegister(nameof(TestRegisterCollection.TestTime)); //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestTime), TimeSpan.Zero, 7012, "DDT7012"); + TestBasicGeneration((IRegisterInternal)register, nameof(TestRegisterCollection.TestTime), TimeSpan.Zero, 7012, "DDT7012"); } - [Fact(DisplayName = "Enum16 generation")] - public void Enum16Gen() { + //[Fact(DisplayName = "String generation")] + //public void StringGen() { - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + // var interf = new MewtocolInterface("192.168.0.1"); + // interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); - var register = interf.GetRegister(nameof(TestRegisterCollection.TestEnum16)); + // var register = interf.GetRegister(nameof(TestRegisterCollection.TestString2)); - //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestEnum16), (short)TestRegisterCollection.CurrentState.Undefined, 50, "DT50"); + // //test generic properties + // TestBasicGeneration(register, nameof(TestRegisterCollection.TestString2), null!, 7005, "DT7005"); - } - - [Fact(DisplayName = "Enum32 generation")] - public void Enum32Gen() { + // Assert.Equal(5, ((BytesRegister)register).ReservedSize); + // Assert.Equal(4, ((BytesRegister)register).MemoryLength); - var interf = new MewtocolInterface("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); - - var register = interf.GetRegister(nameof(TestRegisterCollection.TestEnum32)); - - //test generic properties - TestBasicGeneration(register, nameof(TestRegisterCollection.TestEnum32), (int)TestRegisterCollection.CurrentState.Undefined, 51, "DDT51"); - - } + //} } diff --git a/MewtocolTests/EncapsulatedTests/ExpectedPlcInformationData.cs b/MewtocolTests/EncapsulatedTests/ExpectedPlcInformationData.cs new file mode 100644 index 0000000..2a97059 --- /dev/null +++ b/MewtocolTests/EncapsulatedTests/ExpectedPlcInformationData.cs @@ -0,0 +1,17 @@ +using MewtocolNet; + +namespace MewtocolTests.EncapsulatedTests; + +public class ExpectedPlcInformationData { + + public string PLCName { get; set; } + + public string PLCIP { get; set; } + + public int PLCPort { get; set; } + + public CpuType Type { get; set; } + + public int ProgCapacity { get; set; } + +} diff --git a/MewtocolTests/EncapsulatedTests/RegisterReadWriteTest.cs b/MewtocolTests/EncapsulatedTests/RegisterReadWriteTest.cs new file mode 100644 index 0000000..529cc84 --- /dev/null +++ b/MewtocolTests/EncapsulatedTests/RegisterReadWriteTest.cs @@ -0,0 +1,20 @@ +using MewtocolNet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolTests.EncapsulatedTests; + +internal class RegisterReadWriteTest { + + public IRegister TargetRegister { get; set; } + + public object IntialValue { get; set; } + + public object AfterWriteValue { get; set; } + + public string RegisterPlcAddressName { get; set; } + +} diff --git a/MewtocolTests/TestHelperExtensions.cs b/MewtocolTests/TestHelperExtensions.cs index 26ab943..7c58817 100644 --- a/MewtocolTests/TestHelperExtensions.cs +++ b/MewtocolTests/TestHelperExtensions.cs @@ -43,7 +43,7 @@ public void ToHexStringGeneration() { } - [Fact(DisplayName = nameof(MewtocolHelpers.ToHexASCIIBytes))] + [Fact(DisplayName = nameof(MewtocolHelpers.BytesFromHexASCIIString))] public void ToHexASCIIBytesGeneration() { string test = "Hello, world!"; @@ -62,7 +62,7 @@ public void ToHexASCIIBytesGeneration() { 0x4C, 0x44, 0x21 - }, test.ToHexASCIIBytes()); + }, test.BytesFromHexASCIIString()); } diff --git a/MewtocolTests/TestLivePLC.cs b/MewtocolTests/TestLivePLC.cs index 420e5d4..04cb782 100644 --- a/MewtocolTests/TestLivePLC.cs +++ b/MewtocolTests/TestLivePLC.cs @@ -1,17 +1,21 @@ using MewtocolNet; using MewtocolNet.Logging; +using MewtocolNet.RegisterBuilding; +using MewtocolNet.Registers; +using MewtocolTests.EncapsulatedTests; using Xunit; using Xunit.Abstractions; -namespace MewtocolTests { +namespace MewtocolTests +{ public class TestLivePLC { private readonly ITestOutputHelper output; - private List testData = new() { + private List testPlcInformationData = new() { - new ExpectedTestData { + new ExpectedPlcInformationData { PLCName = "FPX-H C30T", PLCIP = "192.168.115.210", @@ -20,7 +24,7 @@ public class TestLivePLC { ProgCapacity = 32, }, - new ExpectedTestData { + new ExpectedPlcInformationData { PLCName = "FPX-H C14R", PLCIP = "192.168.115.212", @@ -32,12 +36,29 @@ public class TestLivePLC { }; + private List testRegisterRW = new() { + + new RegisterReadWriteTest { + TargetRegister = new BoolRegister(IOType.R, 0xA, 10), + RegisterPlcAddressName = "R10A", + IntialValue = false, + AfterWriteValue = true, + }, + new RegisterReadWriteTest { + TargetRegister = new NumberRegister(3000), + RegisterPlcAddressName = "DT3000", + IntialValue = (int)0, + AfterWriteValue = (int)-513, + }, + + }; + public TestLivePLC(ITestOutputHelper output) { this.output = output; Logger.LogLevel = LogLevel.Verbose; - Logger.OnNewLogMessage((d, m) => { + Logger.OnNewLogMessage((d, l, m) => { output.WriteLine($"Mewtocol Logger: {d} {m}"); @@ -48,7 +69,7 @@ public TestLivePLC(ITestOutputHelper output) { [Fact(DisplayName = "Connection cycle client to PLC")] public async void TestClientConnection() { - foreach (var plc in testData) { + foreach (var plc in testPlcInformationData) { output.WriteLine($"Testing: {plc.PLCName}"); @@ -69,7 +90,7 @@ public async void TestClientConnection() { [Fact(DisplayName = "Reading basic information from PLC")] public async void TestClientReadPLCStatus() { - foreach (var plc in testData) { + foreach (var plc in testPlcInformationData) { output.WriteLine($"Testing: {plc.PLCName}\n"); @@ -90,19 +111,38 @@ public async void TestClientReadPLCStatus() { } - } + //[Fact(DisplayName = "Reading basic information from PLC")] + //public async void TestRegisterReadWriteAsync () { + + // foreach (var plc in testPlcInformationData) { + + // output.WriteLine($"Testing: {plc.PLCName}\n"); + + // var client = new MewtocolInterface(plc.PLCIP, plc.PLCPort); + + // foreach (var testRW in testRegisterRW) { + + // client.AddRegister(testRW.TargetRegister); + + // } + + // await client.ConnectAsync(); + // Assert.True(client.IsConnected); + + // foreach (var testRW in testRegisterRW) { - public class ExpectedTestData { + // client.AddRegister(testRW.TargetRegister); - public string PLCName { get; set; } + // } - public string PLCIP { get; set; } + // Assert.Equal(client.PlcInfo.CpuInformation.Cputype, plc.Type); + // Assert.Equal(client.PlcInfo.CpuInformation.ProgramCapacity, plc.ProgCapacity); - public int PLCPort { get; set; } + // client.Disconnect(); - public CpuType Type { get; set; } + // } - public int ProgCapacity { get; set; } + //} } diff --git a/MewtocolTests/TestRegisterBuilder.cs b/MewtocolTests/TestRegisterBuilder.cs index 24a63f3..4be5918 100644 --- a/MewtocolTests/TestRegisterBuilder.cs +++ b/MewtocolTests/TestRegisterBuilder.cs @@ -122,20 +122,12 @@ private void TestBoolDict (Dictionary dict) { foreach (var item in dict) { - try { + output.WriteLine($"Expected: {item.Key}"); - output.WriteLine($"Expected: {item.Key}"); + var built = RegBuilder.Factory.FromPlcRegName(item.Key).AsPlcType(PlcVarType.BOOL).Build(); - var built = RegBuilder.Factory.FromPlcRegName(item.Key).AsPlcType(PlcVarType.BOOL).Build(); - - output.WriteLine($"{(built?.ToString(true) ?? "null")}\n"); - Assert.Equivalent(item.Value, built); - - } catch (Exception ex) { - - output.WriteLine(ex.Message.ToString()); - - } + output.WriteLine($"{(built?.ToString(true) ?? "null")}\n"); + Assert.Equivalent(item.Value, built); } @@ -204,11 +196,9 @@ public void TestRegisterBuildingNumericAuto() { [Fact(DisplayName = "Parsing as Bytes Register (Casted)")] public void TestRegisterBuildingByteRangeCasted() { - var expect = new BytesRegister(303, 5); + var expect = new BytesRegister(303, 5); - - Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("DT303").AsPlcType(PlcVarType.INT).Build()); - Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("DT303").AsType().Build()); + Assert.Equivalent(expect, RegBuilder.Factory.FromPlcRegName("DT303").AsBytes(5).Build()); } diff --git a/MewtocolTests/TestRegisterInterface.cs b/MewtocolTests/TestRegisterInterface.cs index 8a11726..1b0e3ab 100644 --- a/MewtocolTests/TestRegisterInterface.cs +++ b/MewtocolTests/TestRegisterInterface.cs @@ -16,13 +16,15 @@ public TestRegisterInterface(ITestOutputHelper output) { [Fact(DisplayName = "Numeric mewtocol query building")] public void NumericRegisterMewtocolIdentifiers() { - List registers = new List { - new NumberRegister(50, _name: null), - new NumberRegister(50, _name: null), - new NumberRegister(50, _name : null), - new NumberRegister(50, _name : null), - new NumberRegister(50, _name : null), - new NumberRegister(50, _name : null), + List registers = new List { + new NumberRegister(50), + new NumberRegister(50), + new NumberRegister(50), + new NumberRegister(50), + new NumberRegister(50), + new NumberRegister(50), + new BytesRegister(50, 30), + new BytesRegister(50, 31), }; List expectedIdents = new List { @@ -32,12 +34,14 @@ public void NumericRegisterMewtocolIdentifiers() { "D0005000051", //double word register "D0005000051", //double word register "D0005000051", //double word register + "D0005000065", //variable len register even bytes + "D0005000066", //variable len register odd bytes }; //test mewtocol idents for (int i = 0; i < registers.Count; i++) { - IRegister? reg = registers[i]; + IRegisterInternal? reg = registers[i]; string expect = expectedIdents[i]; Assert.Equal(expect, reg.BuildMewtocolQuery()); @@ -49,7 +53,7 @@ public void NumericRegisterMewtocolIdentifiers() { [Fact(DisplayName = "PLC register naming convention test")] public void PLCRegisterIdentifiers() { - List registers = new List { + List registers = new List { //numeric ones new NumberRegister(50, _name: null), new NumberRegister(60, _name : null), @@ -67,7 +71,7 @@ public void PLCRegisterIdentifiers() { new BoolRegister(IOType.Y, 0xC, 75), //string - new BytesRegister(999, 5), + new BytesRegister(999, 5), }; List expcectedIdents = new List { @@ -96,7 +100,7 @@ public void PLCRegisterIdentifiers() { //test mewtocol idents for (int i = 0; i < registers.Count; i++) { - IRegister? reg = registers[i]; + IRegisterInternal? reg = registers[i]; string expect = expcectedIdents[i]; Assert.Equal(expect, reg.GetRegisterPLCName()); @@ -134,7 +138,7 @@ public void OverFlowRegisterAddress() { var ex3 = Assert.Throws(() => { - new BytesRegister(100000, 5); + new BytesRegister(100000, 5); }); diff --git a/PLC_Test/test_c30_fpx_h.pro b/PLC_Test/test_c30_fpx_h.pro index 5b9447ff957c588719ffb9874c21de6929339927..564b2a649c500559ce1cd06107dd065b159329d3 100644 GIT binary patch delta 6799 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 NNlXhmcmy0R{6G9y-Xs73 delta 6422 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 diff --git a/PLC_Test/test_c30_fpx_h.xml b/PLC_Test/test_c30_fpx_h.xml index 9a30f61..52b5a75 100644 --- a/PLC_Test/test_c30_fpx_h.xml +++ b/PLC_Test/test_c30_fpx_h.xml @@ -2,17 +2,17 @@ - + - + - + From bc765b870a3580b475d5635458041670832c714e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Wed, 28 Jun 2023 18:59:28 +0200 Subject: [PATCH 07/18] Added new conversions - added compile target for net6 --- Examples/ExampleScenarios.cs | 112 +++-- MewtocolNet/DynamicInterface.cs | 189 ++++---- MewtocolNet/IRegister.cs | 20 +- MewtocolNet/MewtocolFrameResponse.cs | 46 ++ MewtocolNet/MewtocolHelpers.cs | 87 +++- MewtocolNet/MewtocolInterface.cs | 404 ++++++++++-------- MewtocolNet/MewtocolInterfaceRequests.cs | 61 ++- MewtocolNet/MewtocolNet.csproj | 2 +- MewtocolNet/RegisterBuildInfo.cs | 35 +- .../RegisterBuilding/FinalizerExtensions.cs | 9 +- MewtocolNet/RegisterBuilding/ParseResult.cs | 1 + MewtocolNet/Registers/BaseRegister.cs | 9 +- MewtocolNet/Registers/BoolRegister.cs | 9 +- MewtocolNet/Registers/BytesRegister.cs | 9 +- MewtocolNet/Registers/NumberRegister.cs | 15 +- MewtocolNet/Registers/StringRegister.cs | 68 ++- MewtocolNet/TCPMessageResult.cs | 9 + MewtocolNet/TypeConversion/Conversions.cs | 78 +++- .../TypeConversion/IPlcTypeConverter.cs | 4 +- .../TypeConversion/PlcTypeConversion.cs | 8 +- MewtocolNet/TypeConversion/PlcValueParser.cs | 8 +- MewtocolTests/TestRegisterInterface.cs | 4 +- PLC_Test/test_c30_fpx_h.pro | Bin 262144 -> 262144 bytes PLC_Test/test_c30_fpx_h.xml | 6 +- 24 files changed, 707 insertions(+), 486 deletions(-) create mode 100644 MewtocolNet/MewtocolFrameResponse.cs diff --git a/Examples/ExampleScenarios.cs b/Examples/ExampleScenarios.cs index d4c82d6..24dcd95 100644 --- a/Examples/ExampleScenarios.cs +++ b/Examples/ExampleScenarios.cs @@ -9,6 +9,7 @@ using MewtocolNet.Registers; using System.Diagnostics; using System.Text; +using Microsoft.Win32; namespace Examples; @@ -17,7 +18,7 @@ public class ExampleScenarios { public void SetupLogger () { //attaching the logger - Logger.LogLevel = LogLevel.Info; + Logger.LogLevel = LogLevel.Error; Logger.OnNewLogMessage((date, level, msg) => { if (level == LogLevel.Error) Console.ForegroundColor = ConsoleColor.Red; @@ -46,6 +47,7 @@ public async Task RunCyclicPollerAsync () { interf.WithRegisterCollection(registers).WithPoller(); await interf.ConnectAsync(); + await interf.AwaitFirstDataAsync(); _ = Task.Factory.StartNew(async () => { @@ -54,22 +56,18 @@ public async Task RunCyclicPollerAsync () { //flip the bool register each tick and wait for it to be registered //await interf.SetRegisterAsync(nameof(registers.TestBool1), !registers.TestBool1); - Console.Title = $"Polling Paused: {interf.PollingPaused}, " + - $"Poller active: {interf.PollerActive}, " + + Console.Title = $"Speed UP: {interf.BytesPerSecondUpstream} B/s, " + $"Speed DOWN: {interf.BytesPerSecondDownstream} B/s, " + - $"Poll delay: {interf.PollerDelayMs} ms, " + + $"Poll cycle: {interf.PollerCycleDurationMs} ms, " + $"Queued MSGs: {interf.QueuedMessages}"; Console.Clear(); Console.WriteLine("Underlying registers on tick: \n"); - foreach (var register in interf.Registers) { - + foreach (var register in interf.Registers) Console.WriteLine($"{register.ToString(true)}"); - } - Console.WriteLine($"{registers.TestBool1}"); Console.WriteLine($"{registers.TestDuplicate}"); @@ -192,12 +190,12 @@ public async Task RunEnumsBitwiseAsync () { public async Task RunReadTest () { //setting up a new PLC interface and register collection - MewtocolInterface interf = new MewtocolInterface("192.168.115.210"); + MewtocolInterface interf = new MewtocolInterface("192.168.115.210").WithPoller(); //auto add all built registers to the interface var builder = RegBuilder.ForInterface(interf); var r0reg = builder.FromPlcRegName("R0").Build(); - builder.FromPlcRegName("R1").Build(); + builder.FromPlcRegName("R1", "Testname").Build(); builder.FromPlcRegName("R1F").Build(); builder.FromPlcRegName("R101A").Build(); @@ -205,35 +203,57 @@ public async Task RunReadTest () { builder.FromPlcRegName("DDT36").AsPlcType(PlcVarType.DINT).Build(); builder.FromPlcRegName("DT200").AsBytes(30).Build(); - builder.FromPlcRegName("DDT38").AsPlcType(PlcVarType.TIME).Build(); - //builder.FromPlcRegName("DT40").AsPlcType(PlcVarType.STRING).Build(); + var timeReg = builder.FromPlcRegName("DDT38").AsPlcType(PlcVarType.TIME).Build(); + var stringReg = builder.FromPlcRegName("DT40").AsPlcType(PlcVarType.STRING).Build(); //connect await interf.ConnectAsync(); - //var res = await interf.SendCommandAsync("%01#RCSR000F"); + //await first register data + await interf.AwaitFirstDataAsync(); - while(true) { + _ = Task.Factory.StartNew(async () => { - await interf.SetRegisterAsync(r0reg, !(bool)r0reg.Value); - await interf.SetRegisterAsync(shortReg, (short)new Random().Next(0, 100)); + void setTitle () { - var sw = Stopwatch.StartNew(); + Console.Title = + $"Speed UP: {interf.BytesPerSecondUpstream} B/s, " + + $"Speed DOWN: {interf.BytesPerSecondDownstream} B/s, " + + $"Poll cycle: {interf.PollerCycleDurationMs} ms, " + + $"Queued MSGs: {interf.QueuedMessages}"; - foreach (var reg in interf.Registers) { + } + + while (interf.IsConnected) { + setTitle(); + await Task.Delay(1000); + } - await reg.ReadAsync(); + setTitle(); - Console.WriteLine($"Register {reg.ToString()}"); + }); - } + while (interf.IsConnected) { + + var sw = Stopwatch.StartNew(); + + //set bool + await r0reg.WriteAsync(!(bool)r0reg.Value); + + //set random num + await shortReg.WriteAsync((short)new Random().Next(0, 100)); + await stringReg.WriteAsync($"_{DateTime.Now.Second}s_"); sw.Stop(); - Console.WriteLine($"Total read time for registers: {sw.Elapsed.TotalMilliseconds:N0}ms"); + foreach (var reg in interf.Registers) + Console.WriteLine(reg.ToString()); + + Console.WriteLine($"Total write time for registers: {sw.Elapsed.TotalMilliseconds:N0}ms"); Console.WriteLine(); + //await Task.Delay(new Random().Next(0, 10000)); await Task.Delay(1000); } @@ -244,28 +264,24 @@ public async Task RunReadTest () { public async Task MultiFrameTest() { var preLogLevel = Logger.LogLevel; - Logger.LogLevel = LogLevel.Error; + Logger.LogLevel = LogLevel.Critical; //setting up a new PLC interface and register collection - MewtocolInterface interf = new MewtocolInterface("192.168.115.210"); + MewtocolInterface interf = new MewtocolInterface("192.168.115.210") { + ConnectTimeout = 3000, + }; //auto add all built registers to the interface var builder = RegBuilder.ForInterface(interf); var r0reg = builder.FromPlcRegName("R0").Build(); builder.FromPlcRegName("R1").Build(); - builder.FromPlcRegName("R1F").Build(); - builder.FromPlcRegName("R60A").Build(); - builder.FromPlcRegName("R61A").Build(); - builder.FromPlcRegName("R62A").Build(); - builder.FromPlcRegName("R63A").Build(); - builder.FromPlcRegName("R64A").Build(); - builder.FromPlcRegName("R65A").Build(); - builder.FromPlcRegName("R66A").Build(); - builder.FromPlcRegName("R67A").Build(); - builder.FromPlcRegName("R68A").Build(); - builder.FromPlcRegName("R69A").Build(); - builder.FromPlcRegName("R70A").Build(); - builder.FromPlcRegName("R71A").Build(); + builder.FromPlcRegName("DT0").AsBytes(100).Build(); + + for (int i = 1; i < 100; i++) { + + builder.FromPlcRegName($"R{i}A").Build(); + + } //connect await interf.ConnectAsync(); @@ -273,36 +289,18 @@ public async Task MultiFrameTest() { Console.WriteLine("Poller cycle started"); var sw = Stopwatch.StartNew(); - await interf.RunPollerCylceManual(false); + int cmdCount = await interf.RunPollerCylceManual(); sw.Stop(); Console.WriteLine("Poller cycle finished"); - Console.WriteLine($"Single frame excec time: {sw.ElapsedMilliseconds:N0}ms"); + Console.WriteLine($"Single frame excec time: {sw.ElapsedMilliseconds:N0}ms for {cmdCount} commands"); interf.Disconnect(); await Task.Delay(1000); - await interf.ConnectAsync(); - - sw = Stopwatch.StartNew(); - - Console.WriteLine("Poller cycle started"); - - await interf.RunPollerCylceManual(true); - - sw.Stop(); - - Console.WriteLine("Poller cycle finished"); - - Console.WriteLine($"Multi frame excec time: {sw.ElapsedMilliseconds:N0}ms"); - - Logger.LogLevel = preLogLevel; - - await Task.Delay(10000); - } } diff --git a/MewtocolNet/DynamicInterface.cs b/MewtocolNet/DynamicInterface.cs index 357bd52..2344bba 100644 --- a/MewtocolNet/DynamicInterface.cs +++ b/MewtocolNet/DynamicInterface.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; @@ -19,70 +20,42 @@ namespace MewtocolNet /// public partial class MewtocolInterface { - /// - /// True if the auto poller is currently paused - /// - public bool PollingPaused => pollerIsPaused; - - /// - /// True if the poller is actvice (can be paused) - /// - public bool PollerActive => !pollerTaskStopped; - internal event Action PolledCycle; - internal volatile bool pollerTaskRunning; - internal volatile bool pollerTaskStopped; - internal volatile bool pollerIsPaused; - internal volatile bool pollerFirstCycle = false; + internal volatile bool pollerTaskStopped = true; + internal volatile bool pollerFirstCycle; internal bool usePoller = false; - internal bool pollerUseMultiFrame = false; - #region Register Polling + private int tcpMessagesSentThisCycle = 0; + + private int pollerCycleDurationMs; /// - /// Kills the poller completely + /// True if the poller is actvice (can be paused) /// - internal void KillPoller() { - - pollerTaskRunning = false; - pollerTaskStopped = true; - - ClearRegisterVals(); - - } + public bool PollerActive => !pollerTaskStopped; /// - /// Pauses the polling and waits for the last message to be sent + /// Current poller cycle duration /// - /// - public async Task PausePollingAsync() { - - if (!pollerTaskRunning) - return; - - pollerTaskRunning = false; - - while (!pollerIsPaused) { - - if (pollerIsPaused) - break; - - await Task.Delay(10); - + public int PollerCycleDurationMs { + get => pollerCycleDurationMs; + private set { + pollerCycleDurationMs = value; + OnPropChange(); } - - pollerTaskRunning = false; - } + #region Register Polling + /// - /// Resumes the polling + /// Kills the poller completely /// - public void ResumePolling() { + internal void KillPoller() { - pollerTaskRunning = true; + pollerTaskStopped = true; + ClearRegisterVals(); } @@ -91,9 +64,10 @@ public void ResumePolling() { /// internal void AttachPoller() { - if (pollerTaskRunning) + if (!pollerTaskStopped) return; + PollerCycleDurationMs = 0; pollerFirstCycle = true; Task.Run(Poll); @@ -104,14 +78,18 @@ internal void AttachPoller() { /// Runs a single poller cycle manually, /// useful if you want to use a custom update frequency /// - /// - public async Task RunPollerCylceManual (bool useMultiFrame = false) { + /// The number of inidvidual mewtocol commands sent + public async Task RunPollerCylceManual () { - if (useMultiFrame) { - await OnMultiFrameCycle(); - } else { - await OnSingleFrameCycle(); - } + if (!pollerTaskStopped) + throw new NotSupportedException($"The poller is already running, " + + $"please make sure there is no polling active before calling {nameof(RunPollerCylceManual)}"); + + tcpMessagesSentThisCycle = 0; + + await OnMultiFrameCycle(); + + return tcpMessagesSentThisCycle; } @@ -120,78 +98,49 @@ internal async Task Poll () { Logger.Log("Poller is attaching", LogLevel.Info, this); - int iteration = 0; - pollerTaskStopped = false; - pollerTaskRunning = true; - pollerIsPaused = false; while (!pollerTaskStopped) { - if (iteration >= Registers.Count + 1) { - iteration = 0; - //invoke cycle polled event - InvokePolledCycleDone(); - continue; - } + tcpMessagesSentThisCycle = 0; - if(pollerUseMultiFrame) { - await OnMultiFrameCycle(); - } else { - await OnSingleFrameCycle(); + await OnMultiFrameCycle(); + + if(!IsConnected) { + pollerTaskStopped = true; + return; } pollerFirstCycle = false; - - iteration++; - - pollerIsPaused = !pollerTaskRunning; + InvokePolledCycleDone(); } - pollerIsPaused = false; - } - private async Task OnSingleFrameCycle () { - - foreach (var reg in Registers) { - - if (reg.IsAllowedRegisterGenericType()) { - - var lastVal = reg.Value; - - var rwReg = (IRegisterInternal)reg; - - var readout = await rwReg.ReadAsync(); - - if (lastVal != readout) { + private async Task OnMultiFrameCycle () { - rwReg.SetValueFromPLC(readout); - InvokeRegisterChanged(reg); + var sw = Stopwatch.StartNew(); - } - - } + await UpdateRCPRegisters(); - } + await UpdateDTRegisters(); await GetPLCInfoAsync(); - } - - private async Task OnMultiFrameCycle () { + sw.Stop(); + PollerCycleDurationMs = (int)sw.ElapsedMilliseconds; - await UpdateRCPRegisters(); + } - await GetPLCInfoAsync(); + #endregion - } + #region Smart register polling methods private async Task UpdateRCPRegisters () { //build booleans - var rcpList = Registers.Where(x => x.GetType() == typeof(BoolRegister)) + var rcpList = RegistersUnderlying.Where(x => x.GetType() == typeof(BoolRegister)) .Select(x => (BoolRegister)x) .ToArray(); @@ -216,6 +165,8 @@ private async Task UpdateRCPRegisters () { string rcpRequest = rcpString.ToString(); var result = await SendCommandAsync(rcpRequest); + if (!result.Success) return; + var resultBitArray = result.Response.ParseRCMultiBit(); for (int k = 0; k < resultBitArray.Length; k++) { @@ -233,9 +184,27 @@ private async Task UpdateRCPRegisters () { } - internal void PropertyRegisterWasSet(string propName, object value) { + private async Task UpdateDTRegisters () { - _ = SetRegisterAsync(GetRegister(propName), value); + foreach (var reg in RegistersUnderlying) { + + var type = reg.GetType(); + + if(reg.RegisterType.IsNumericDTDDT() || reg.RegisterType == RegisterType.DT_BYTE_RANGE) { + + var lastVal = reg.Value; + var rwReg = (IRegisterInternal)reg; + var readout = await rwReg.ReadAsync(); + if (readout == null) return; + + if (lastVal != readout) { + rwReg.SetValueFromPLC(readout); + InvokeRegisterChanged(reg); + } + + } + + } } @@ -409,7 +378,7 @@ internal void AddRegister (RegisterBuildInfo buildInfo) { throw MewtocolException.DupeNameRegister(builtRegister); builtRegister.attachedInterface = this; - Registers.Add(builtRegister); + RegistersUnderlying.Add(builtRegister); } @@ -422,7 +391,7 @@ public void AddRegister (BaseRegister register) { throw MewtocolException.DupeNameRegister(register); register.attachedInterface = this; - Registers.Add(register); + RegistersUnderlying.Add(register); } @@ -458,7 +427,7 @@ private bool CheckDuplicateNameRegister(IRegisterInternal instance) { /// public IRegister GetRegister(string name) { - return Registers.FirstOrDefault(x => x.Name == name); + return RegistersUnderlying.FirstOrDefault(x => x.Name == name); } @@ -471,7 +440,7 @@ public IRegister GetRegister(string name) { /// public IEnumerable GetAllRegisters() { - return Registers.Cast(); + return RegistersUnderlying.Cast(); } @@ -479,6 +448,12 @@ public IEnumerable GetAllRegisters() { #region Event Invoking + internal void PropertyRegisterWasSet(string propName, object value) { + + _ = SetRegisterAsync(GetRegister(propName), value); + + } + internal void InvokeRegisterChanged(IRegister reg) { RegisterChanged?.Invoke(reg); diff --git a/MewtocolNet/IRegister.cs b/MewtocolNet/IRegister.cs index 6bd6bca..18aa8d8 100644 --- a/MewtocolNet/IRegister.cs +++ b/MewtocolNet/IRegister.cs @@ -1,4 +1,5 @@ using System; +using System.Text; using System.Threading.Tasks; namespace MewtocolNet { @@ -38,6 +39,12 @@ public interface IRegister { /// int MemoryAddress { get; } + /// + /// Gets the value of the register as the plc representation string + /// + /// + string GetAsPLC(); + /// /// Builds a readable string with all important register informations /// @@ -49,16 +56,17 @@ public interface IRegister { string ToString(bool detailed); /// - /// Sets the register value in the plc async + /// Reads the register value async from the plc /// - /// True if successful - Task SetValueAsync(); + /// The register value + Task ReadAsync(); /// - /// Gets the register value from the plc async + /// Writes the register content async to the plc /// - /// The value or null if failed - Task GetValueAsync(); + /// True if successfully set + Task WriteAsync(object data); + } diff --git a/MewtocolNet/MewtocolFrameResponse.cs b/MewtocolNet/MewtocolFrameResponse.cs new file mode 100644 index 0000000..a381336 --- /dev/null +++ b/MewtocolNet/MewtocolFrameResponse.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet { + + public struct MewtocolFrameResponse { + + public bool Success { get; private set; } + + public string Response { get; private set; } + + public int ErrorCode { get; private set; } + + public string Error { get; private set; } + + public MewtocolFrameResponse (string response) { + + Success = true; + ErrorCode = 0; + Response = response; + Error = null; + + } + + public MewtocolFrameResponse(int errorCode) { + + Success = false; + Response = null; + ErrorCode = errorCode; + Error = CodeDescriptions.Error[errorCode]; + + } + + public MewtocolFrameResponse(int errorCode, string exceptionMsg) { + + Success = false; + Response = null; + ErrorCode = errorCode; + Error = exceptionMsg; + + } + + } + +} diff --git a/MewtocolNet/MewtocolHelpers.cs b/MewtocolNet/MewtocolHelpers.cs index 38c0285..99be553 100644 --- a/MewtocolNet/MewtocolHelpers.cs +++ b/MewtocolNet/MewtocolHelpers.cs @@ -13,6 +13,37 @@ namespace MewtocolNet { /// public static class MewtocolHelpers { + #region Value PLC Humanizers + + /// + /// Gets the TimeSpan as a PLC representation string fe. + /// + /// T#1h10m30s20ms + /// + /// + /// + /// + public static string AsPLCTime (this TimeSpan timespan) { + + if (timespan == null || timespan == TimeSpan.Zero) + return $"T#0s"; + + StringBuilder sb = new StringBuilder("T#"); + + int millis = timespan.Milliseconds; + int seconds = timespan.Seconds; + int minutes = timespan.Minutes; + int hours = timespan.Hours; + + if (hours > 0) sb.Append($"{hours}h"); + if (minutes > 0) sb.Append($"{minutes}m"); + if (seconds > 0) sb.Append($"{seconds}s"); + if (millis > 0) sb.Append($"{millis}ms"); + + return sb.ToString(); + + } + /// /// Turns a bit array into a 0 and 1 string /// @@ -24,6 +55,10 @@ public static string ToBitString(this BitArray arr) { } + #endregion + + #region Byte and string operation helpers + /// /// Converts a string (after converting to upper case) to ascii bytes /// @@ -35,7 +70,12 @@ internal static byte[] BytesFromHexASCIIString(this string _str) { } - internal static string BuildBCCFrame(this string asciiArr) { + /// + /// Builds the BCC / Checksum for the mewtocol command + /// + /// The mewtocol command (%01#RCS0001) + /// The mewtocol command with the appended checksum + public static string BuildBCCFrame(this string asciiArr) { Encoding ae = Encoding.ASCII; byte[] b = ae.GetBytes(asciiArr); @@ -63,8 +103,13 @@ internal static string ParseDTByteString(this string _onString, int _blockSize = } + /// + /// Parses a return message as RCS single bit + /// internal static bool? ParseRCSingleBit(this string _onString) { + _onString = _onString.Replace("\r", ""); + var res = new Regex(@"\%([0-9]{2})\$RC(.)").Match(_onString); if (res.Success) { string val = res.Groups[2].Value; @@ -74,8 +119,13 @@ internal static string ParseDTByteString(this string _onString, int _blockSize = } + /// + /// Parses a return message as RCS multiple bits + /// internal static BitArray ParseRCMultiBit(this string _onString) { + _onString = _onString.Replace("\r", ""); + var res = new Regex(@"\%([0-9]{2})\$RC(?(?:0|1){0,8})(..)").Match(_onString); if (res.Success) { @@ -124,7 +174,7 @@ internal static byte[] ParseDTRawStringAsBytes (this string _onString) { } /// - /// Splits a string in even parts + /// Splits a string into even parts /// internal static IEnumerable SplitInParts(this string s, int partLength) { @@ -138,9 +188,11 @@ internal static IEnumerable SplitInParts(this string s, int partLength) } + /// + /// Converts a hex string (AB01C1) to a byte array + /// internal static byte[] HexStringToByteArray (this string hex) { - if (hex == null) - return null; + if (hex == null) return null; return Enumerable.Range(0, hex.Length) .Where(x => x % 2 == 0) .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) @@ -166,24 +218,9 @@ internal static string ToHexString (this byte[] arr, string seperator = "") { } - internal static string AsPLC (this TimeSpan timespan) { - - StringBuilder sb = new StringBuilder("T#"); - - int millis = timespan.Milliseconds; - int seconds = timespan.Seconds; - int minutes = timespan.Minutes; - int hours = timespan.Hours; - - if (hours > 0) sb.Append($"{hours}h"); - if (minutes > 0) sb.Append($"{minutes}m"); - if (seconds > 0) sb.Append($"{seconds}s"); - if (millis > 0) sb.Append($"{millis}ms"); - - return sb.ToString(); - - } - + /// + /// Switches byte order from mixed to big endian + /// internal static byte[] BigToMixedEndian(this byte[] arr) { List oldBL = new List(arr); @@ -206,6 +243,10 @@ internal static byte[] BigToMixedEndian(this byte[] arr) { } + #endregion + + #region Comparerers + /// /// Checks if the register type is boolean /// @@ -249,6 +290,8 @@ internal static bool CompareIsNameDuplicate(this IRegisterInternal reg1, IRegist } + #endregion + } } \ No newline at end of file diff --git a/MewtocolNet/MewtocolInterface.cs b/MewtocolNet/MewtocolInterface.cs index 98a3aa4..cb48916 100644 --- a/MewtocolNet/MewtocolInterface.cs +++ b/MewtocolNet/MewtocolInterface.cs @@ -7,11 +7,13 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using System.ComponentModel.Design; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; +using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -52,18 +54,6 @@ public int ConnectTimeout { set { connectTimeout = value; } } - private volatile int pollerDelayMs = 0; - /// - /// Delay for each poller cycle in milliseconds, default = 0 - /// - public int PollerDelayMs { - get => pollerDelayMs; - set { - pollerDelayMs = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PollerDelayMs))); - } - } - private volatile int queuedMessages; /// /// Currently queued Messages @@ -114,9 +104,11 @@ private set { /// /// The registered data registers of the PLC /// - public List Registers { get; private set; } = new List(); + internal List RegistersUnderlying { get; private set; } = new List(); - internal IEnumerable RegistersInternal => Registers.Cast(); + public IEnumerable Registers => RegistersUnderlying.Cast(); + + internal IEnumerable RegistersInternal => RegistersUnderlying.Cast(); private string ip; private int port; @@ -184,6 +176,8 @@ private set { private Stopwatch speedStopwatchUpstr; private Stopwatch speedStopwatchDownstr; + private Task firstPollTask = new Task(() => { }); + #region Initialization /// @@ -238,6 +232,8 @@ void MewtocolInterface_Connected(PLCInfo obj) { /// public async Task ConnectAsync(Action OnConnected = null, Action OnFailed = null) { + firstPollTask = new Task(() => { }); + Logger.Log("Connecting to PLC...", LogLevel.Info, this); var plcinf = await GetPLCInfoAsync(); @@ -249,18 +245,19 @@ public async Task ConnectAsync(Action OnConnected = Connected?.Invoke(plcinf); - if (OnConnected != null) { + if (!usePoller) { + if (OnConnected != null) OnConnected(plcinf); + firstPollTask.RunSynchronously(); + return this; + } + + PolledCycle += OnPollCycleDone; + void OnPollCycleDone() { - if (!usePoller) { - OnConnected(plcinf); - return this; - } + if (OnConnected != null) OnConnected(plcinf); + firstPollTask.RunSynchronously(); + PolledCycle -= OnPollCycleDone; - PolledCycle += OnPollCycleDone; - void OnPollCycleDone() { - OnConnected(plcinf); - PolledCycle -= OnPollCycleDone; - } } } else { @@ -268,6 +265,7 @@ void OnPollCycleDone() { if (OnFailed != null) { OnFailed(); Disconnected?.Invoke(); + firstPollTask.RunSynchronously(); Logger.Log("Initial connection failed", LogLevel.Info, this); } @@ -277,6 +275,12 @@ void OnPollCycleDone() { } + /// + /// Use this to await the first poll iteration after connecting, + /// This also completes if the initial connection fails + /// + public async Task AwaitFirstDataAsync () => await firstPollTask; + /// /// Changes the connections parameters of the PLC, only applyable when the connection is offline /// @@ -310,10 +314,9 @@ public void Disconnect() { /// Attaches a poller to the interface that continously /// polls the registered data registers and writes the values to them /// - public MewtocolInterface WithPoller(bool useMultiFrame = false) { + public MewtocolInterface WithPoller() { usePoller = true; - pollerUseMultiFrame = useMultiFrame; return this; } @@ -375,229 +378,229 @@ private async Task ConnectTCP() { } - private void OnMajorSocketExceptionWhileConnecting() { - - Logger.Log("The PLC connection timed out", LogLevel.Error, this); - CycleTimeMs = 0; - IsConnected = false; - KillPoller(); + #endregion - } + #region Low level command handling - private void OnMajorSocketExceptionWhileConnected() { + /// + /// Calculates the checksum automatically and sends a command to the PLC then awaits results + /// + /// MEWTOCOL Formatted request string ex: %01#RT + /// Append the checksum and bcc automatically + /// Returns the result + public async Task SendCommandAsync(string _msg, bool withTerminator = true) { - if (IsConnected) { + //send request + queuedMessages++; + var tempResponse = await queue.Enqueue(() => SendFrameAsync(_msg, withTerminator, withTerminator)); - Logger.Log("The PLC connection was closed", LogLevel.Error, this); - CycleTimeMs = 0; - IsConnected = false; - Disconnected?.Invoke(); - KillPoller(); - client.Close(); + tcpMessagesSentThisCycle++; + queuedMessages--; - } + return tempResponse; } - private void ClearRegisterVals() { + private async Task SendFrameAsync (string frame, bool useBcc = true, bool useCr = true) { - for (int i = 0; i < Registers.Count; i++) { + try { - var reg = (IRegisterInternal)Registers[i]; - reg.ClearValue(); + //stop time + if (speedStopwatchUpstr == null) { + speedStopwatchUpstr = Stopwatch.StartNew(); + } - } + if (speedStopwatchUpstr.Elapsed.TotalSeconds >= 1) { + speedStopwatchUpstr.Restart(); + bytesTotalCountedUpstream = 0; + } - } + const char CR = '\r'; + const char DELIMITER = '&'; - #endregion + if (client == null || !client.Connected) await ConnectTCP(); - #region Low level command handling + if (useBcc) + frame = $"{frame.BuildBCCFrame()}"; - /// - /// Calculates the checksum automatically and sends a command to the PLC then awaits results - /// - /// MEWTOCOL Formatted request string ex: %01#RT - /// Returns the result - public async Task SendCommandAsync(string _msg) { + if (useCr) + frame = $"{frame}\r"; - _msg = _msg.BuildBCCFrame(); - _msg += "\r"; + //write inital command + byte[] writeBuffer = Encoding.UTF8.GetBytes(frame); + await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length); - //send request - try { + //calc upstream speed + bytesTotalCountedUpstream += writeBuffer.Length; - queuedMessages++; + var perSecUpstream = (double)((bytesTotalCountedUpstream / speedStopwatchUpstr.Elapsed.TotalMilliseconds) * 1000); + if (perSecUpstream <= 10000) + BytesPerSecondUpstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero); - TCPMessageResult tcpResult = TCPMessageResult.Waiting; - string response = ""; - int lineFeedFails = 0; + Logger.Log($"[---------CMD START--------]", LogLevel.Critical, this); + Logger.Log($"--> OUT MSG: {frame.Replace("\r", "(CR)")}", LogLevel.Critical, this); - //recursively try to get a response on failed line feeds - while (tcpResult == TCPMessageResult.Waiting || tcpResult == TCPMessageResult.FailedLineFeed) { + //read + List totalResponse = new List(); + byte[] responseBuffer = new byte[512]; - if (lineFeedFails >= 5) - throw new MewtocolException($"The message ${_msg} had {lineFeedFails} linefeed fails"); + bool wasMultiFramedResponse = false; + CommandState cmdState = CommandState.Intial; - var tempResponse = await queue.Enqueue(() => SendSingleBlock(_msg)); - tcpResult = tempResponse.Item1; - response = tempResponse.Item2; + //read until command complete + while (cmdState != CommandState.Complete) { - if(tcpResult == TCPMessageResult.FailedLineFeed) { - lineFeedFails++; - Logger.Log($"Linefeed fail, retrying...", LogLevel.Error); + //time measuring + if (speedStopwatchDownstr == null) { + speedStopwatchDownstr = Stopwatch.StartNew(); } - if (queuedMessages > 0) - queuedMessages--; + if (speedStopwatchDownstr.Elapsed.TotalSeconds >= 1) { + speedStopwatchDownstr.Restart(); + bytesTotalCountedDownstream = 0; + } - if (tcpResult == TCPMessageResult.FailedWithException) - throw new MewtocolException("The connection to the device was terminated"); + responseBuffer = new byte[128]; - } + await stream.ReadAsync(responseBuffer, 0, responseBuffer.Length); - //error catching - Regex errorcheck = new Regex(@"\%[0-9]{2}\!([0-9]{2})", RegexOptions.IgnoreCase); - Match m = errorcheck.Match(response); - - if (m.Success) { - string eCode = m.Groups[1].Value; - string eDes = CodeDescriptions.Error[Convert.ToInt32(eCode)]; - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Response is: {response}"); - Logger.Log($"Error on command {_msg.Replace("\r", "")} the PLC returned error code: {eCode}, {eDes}", LogLevel.Error); - Console.ResetColor(); - return new CommandResult { - Success = false, - Error = eCode, - ErrorDescription = eDes - }; - } + bool terminatorReceived = responseBuffer.Any(x => x == (byte)CR); + var delimiterTerminatorIdx = SearchBytePattern(responseBuffer, new byte[] { (byte)DELIMITER, (byte)CR }); - return new CommandResult { - Success = true, - Error = "0000", - Response = response, - }; - - } catch { - return new CommandResult { - Success = false, - Error = "0000", - ErrorDescription = "null result" - }; - } + if (terminatorReceived && delimiterTerminatorIdx == -1) { + cmdState = CommandState.Complete; + } else if (delimiterTerminatorIdx != -1) { + cmdState = CommandState.RequestedNextFrame; + } else { + cmdState = CommandState.LineFeed; + } - } + //log message parts + var tempMsg = Encoding.UTF8.GetString(responseBuffer).Replace("\r", "(CR)"); + Logger.Log($">> IN PART: {tempMsg}, Command state: {cmdState}", LogLevel.Critical, this); - private async Task<(TCPMessageResult, string)> SendSingleBlock(string _blockString) { + //error response + int errorCode = CheckForErrorMsg(tempMsg); + if (errorCode != 0) return new MewtocolFrameResponse(errorCode); - if (client == null || !client.Connected) { - await ConnectTCP(); - } + //add complete response to collector without empty bytes + totalResponse.AddRange(responseBuffer.Where(x => x != (byte)0x0)); - if (client == null || !client.Connected) - return (TCPMessageResult.NotConnected, null); + //request next part of the command if the delimiter was received + if (cmdState == CommandState.RequestedNextFrame) { - var message = _blockString.BytesFromHexASCIIString(); + Logger.Log($"Requesting next frame...", LogLevel.Critical, this); - //time measuring - if (speedStopwatchUpstr == null) { - speedStopwatchUpstr = Stopwatch.StartNew(); - } + wasMultiFramedResponse = true; + writeBuffer = Encoding.UTF8.GetBytes("%01**&\r"); + await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length); - if (speedStopwatchUpstr.Elapsed.TotalSeconds >= 1) { - speedStopwatchUpstr.Restart(); - bytesTotalCountedUpstream = 0; - } + } - //send request - using (var sendStream = new MemoryStream(message)) { - await sendStream.CopyToAsync(stream); - Logger.Log($"[--------------------------------]", LogLevel.Critical, this); - Logger.Log($"--> OUT MSG: {_blockString}", LogLevel.Critical, this); - } + } - //calc upstream speed - bytesTotalCountedUpstream += message.Length; + //build final result + string resString = Encoding.UTF8.GetString(totalResponse.ToArray()); - var perSecUpstream = (double)((bytesTotalCountedUpstream / speedStopwatchUpstr.Elapsed.TotalMilliseconds) * 1000); - if (perSecUpstream <= 10000) - BytesPerSecondUpstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero); + if (wasMultiFramedResponse) { - //await result - StringBuilder response = new StringBuilder(); - try { + var split = resString.Split('&'); - byte[] responseBuffer = new byte[128 * 16]; + for (int j = 0; j < split.Length; j++) { - bool endLineCode = false; - bool startMsgCode = false; + split[j] = split[j].Replace("\r", ""); + split[j] = split[j].Substring(0, split[j].Length - 2); + if (j > 0) split[j] = split[j].Replace($"%{GetStationNumber()}", ""); - while (!endLineCode && !startMsgCode) { + } - do { + resString = string.Join("", split); - //time measuring - if (speedStopwatchDownstr == null) { - speedStopwatchDownstr = Stopwatch.StartNew(); - } + } - if (speedStopwatchDownstr.Elapsed.TotalSeconds >= 1) { - speedStopwatchDownstr.Restart(); - bytesTotalCountedDownstream = 0; - } + bytesTotalCountedDownstream += Encoding.ASCII.GetByteCount(resString); - int bytes = await stream.ReadAsync(responseBuffer, 0, responseBuffer.Length); + var perSecDownstream = (double)((bytesTotalCountedDownstream / speedStopwatchDownstr.Elapsed.TotalMilliseconds) * 1000); - endLineCode = responseBuffer.Any(x => x == 0x0D); - startMsgCode = responseBuffer.Count(x => x == 0x25) > 1; + if (perSecUpstream <= 10000) + BytesPerSecondDownstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero); - if (!endLineCode && !startMsgCode) break; + Logger.Log($"<-- IN MSG: {resString.Replace("\r", "(CR)")}", LogLevel.Critical, this); + Logger.Log($"Total bytes parsed: {resString.Length}", LogLevel.Critical, this); + Logger.Log($"[---------CMD END----------]", LogLevel.Critical, this); - response.Append(Encoding.UTF8.GetString(responseBuffer, 0, bytes)); - } - while (stream.DataAvailable); + return new MewtocolFrameResponse(resString); - } + } catch (Exception ex) { + + return new MewtocolFrameResponse(400, ex.Message.ToString(System.Globalization.CultureInfo.InvariantCulture)); - } catch (IOException) { - OnMajorSocketExceptionWhileConnected(); - return (TCPMessageResult.FailedWithException, null); - } catch (SocketException) { - OnMajorSocketExceptionWhileConnected(); - return (TCPMessageResult.FailedWithException, null); } + } - string resString = response.ToString(); + private int CheckForErrorMsg (string msg) { - if (!string.IsNullOrEmpty(resString) && resString != "\r" ) { + //error catching + Regex errorcheck = new Regex(@"\%[0-9]{2}\!([0-9]{2})", RegexOptions.IgnoreCase); + Match m = errorcheck.Match(msg); - Logger.Log($"<-- IN MSG: {response}", LogLevel.Critical, this); + if (m.Success) { - bytesTotalCountedDownstream += Encoding.ASCII.GetByteCount(response.ToString()); + string eCode = m.Groups[1].Value; + return Convert.ToInt32(eCode); - var perSecDownstream = (double)((bytesTotalCountedDownstream / speedStopwatchDownstr.Elapsed.TotalMilliseconds) * 1000); + } - if (perSecUpstream <= 10000) - BytesPerSecondDownstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero); + return 0; - return (TCPMessageResult.Success, resString); + } - } else { + private int SearchBytePattern (byte[] src, byte[] pattern) { - return (TCPMessageResult.FailedLineFeed, null); + int maxFirstCharSlot = src.Length - pattern.Length + 1; + for (int i = 0; i < maxFirstCharSlot; i++) { + if (src[i] != pattern[0]) // compare only first byte + continue; + // found a match on first byte, now try to match rest of the pattern + for (int j = pattern.Length - 1; j >= 1; j--) { + if (src[i + j] != pattern[j]) break; + if (j == 1) return i; + } } - + return -1; + } #endregion #region Disposing + private void OnMajorSocketExceptionWhileConnecting() { + + if (IsConnected) { + + Logger.Log("The PLC connection timed out", LogLevel.Error, this); + OnDisconnect(); + + } + + } + + private void OnMajorSocketExceptionWhileConnected() { + + if (IsConnected) { + + Logger.Log("The PLC connection was closed", LogLevel.Error, this); + OnDisconnect(); + + } + + } + + /// /// Disposes the current interface and clears all its members /// @@ -607,12 +610,43 @@ public void Dispose() { Disconnect(); - GC.SuppressFinalize(this); + //GC.SuppressFinalize(this); Disposed = true; } + private void OnDisconnect () { + + if (IsConnected) { + + BytesPerSecondDownstream = 0; + BytesPerSecondUpstream = 0; + CycleTimeMs = 0; + + IsConnected = false; + ClearRegisterVals(); + + Disconnected?.Invoke(); + KillPoller(); + client.Close(); + + } + + } + + + private void ClearRegisterVals() { + + for (int i = 0; i < RegistersUnderlying.Count; i++) { + + var reg = (IRegisterInternal)RegistersUnderlying[i]; + reg.ClearValue(); + + } + + } + #endregion #region Accessing Info @@ -628,6 +662,20 @@ public string GetConnectionPortInfo() { #endregion + #region Property change evnts + + /// + /// Triggers a property changed event + /// + /// Name of the property to trigger for + private void OnPropChange ([CallerMemberName]string propertyName = null) { + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + } + + #endregion + } } \ No newline at end of file diff --git a/MewtocolNet/MewtocolInterfaceRequests.cs b/MewtocolNet/MewtocolInterfaceRequests.cs index 3b0f4f9..54137bd 100644 --- a/MewtocolNet/MewtocolInterfaceRequests.cs +++ b/MewtocolNet/MewtocolInterfaceRequests.cs @@ -48,7 +48,9 @@ public async Task GetPLCInfoAsync() { return retInfo; } + return null; + } #endregion @@ -89,7 +91,7 @@ public async Task SetOperationMode(OPMode mode) { /// /// start address of the array /// /// - public async Task WriteByteRange(int start, byte[] byteArr) { + public async Task WriteByteRange (int start, byte[] byteArr) { string byteString = byteArr.BigToMixedEndian().ToHexString(); var wordLength = byteArr.Length / 2; @@ -111,9 +113,10 @@ public async Task WriteByteRange(int start, byte[] byteArr) { /// /// Start adress /// Number of bytes to get + /// Flips bytes from big to mixed endian /// Gets invoked when the progress changes, contains the progress as a double /// A byte array or null of there was an error - public async Task ReadByteRange(int start, int count, Action onProgress = null) { + public async Task ReadByteRange(int start, int count, bool flipBytes = true, Action onProgress = null) { var byteList = new List(); @@ -138,11 +141,13 @@ public async Task ReadByteRange(int start, int count, Action onP var bytes = result.Response.ParseDTByteString(8 * 4).HexStringToByteArray(); - if (bytes == null) { - return null; - } + if (bytes == null) return null; - byteList.AddRange(bytes.BigToMixedEndian().Take(count).ToArray()); + if (flipBytes) { + byteList.AddRange(bytes.BigToMixedEndian().Take(count).ToArray()); + } else { + byteList.AddRange(bytes.Take(count).ToArray()); + } } @@ -168,6 +173,7 @@ internal async Task ReadRawRegisterAsync (IRegisterInternal _toRead) { string requeststring = $"%{GetStationNumber()}#RCS{_toRead.BuildMewtocolQuery()}"; var result = await SendCommandAsync(requeststring); + if (!result.Success) return null; var resultBool = result.Response.ParseRCSingleBit(); if (resultBool != null) { @@ -183,9 +189,7 @@ internal async Task ReadRawRegisterAsync (IRegisterInternal _toRead) { string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolQuery()}"; var result = await SendCommandAsync(requeststring); - - if (!result.Success) - throw new Exception($"Failed to load the byte data for: {_toRead}"); + if (!result.Success) return null; if(_toRead.RegisterType == RegisterType.DT) { @@ -204,9 +208,19 @@ internal async Task ReadRawRegisterAsync (IRegisterInternal _toRead) { string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolQuery()}"; var result = await SendCommandAsync(requeststring); + if (!result.Success) return null; + + var resBytes = result.Response.ParseDTRawStringAsBytes(); + + return resBytes; + + } - if (!result.Success) - throw new Exception($"Failed to load the byte data for: {_toRead}"); + if (toreadType == typeof(StringRegister)) { + + string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolQuery()}"; + var result = await SendCommandAsync(requeststring); + if (!result.Success) return null; var resBytes = result.Response.ParseDTRawStringAsBytes(); @@ -220,8 +234,10 @@ internal async Task ReadRawRegisterAsync (IRegisterInternal _toRead) { internal async Task WriteRawRegisterAsync (IRegisterInternal _toWrite, byte[] data) { + var toWriteType = _toWrite.GetType(); + //returns a byte array 1 long and with the byte beeing 0 or 1 - if (_toWrite.GetType() == typeof(BoolRegister)) { + if (toWriteType == typeof(BoolRegister)) { string requeststring = $"%{GetStationNumber()}#WCS{_toWrite.BuildMewtocolQuery()}{(data[0] == 1 ? "1" : "0")}"; var result = await SendCommandAsync(requeststring); @@ -229,8 +245,8 @@ internal async Task WriteRawRegisterAsync (IRegisterInternal _toWrite, byt } - //returns a byte array 2 bytes or 4 bytes long depending on the data size - if (_toWrite.GetType().GetGenericTypeDefinition() == typeof(NumberRegister<>)) { + //writes a byte array 2 bytes or 4 bytes long depending on the data size + if (toWriteType.IsGenericType && toWriteType.GetGenericTypeDefinition() == typeof(NumberRegister<>)) { string requeststring = $"%{GetStationNumber()}#WD{_toWrite.BuildMewtocolQuery()}{data.ToHexString()}"; var result = await SendCommandAsync(requeststring); @@ -239,15 +255,18 @@ internal async Task WriteRawRegisterAsync (IRegisterInternal _toWrite, byt } //returns a byte array with variable size - if (_toWrite.GetType() == typeof(BytesRegister)) { + if (toWriteType == typeof(BytesRegister)) { - //string stationNum = GetStationNumber(); - //string dataString = gotBytes.BuildDTString(_toWrite.ReservedSize); - //string dataArea = _toWrite.BuildCustomIdent(dataString.Length / 4); + throw new NotImplementedException("Not imp"); - //string requeststring = $"%{stationNum}#WD{dataArea}{dataString}"; + } + + //writes to the string area + if (toWriteType == typeof(StringRegister)) { - //var result = await SendCommandAsync(requeststring); + string requeststring = $"%{GetStationNumber()}#WD{_toWrite.BuildMewtocolQuery()}{data.ToHexString()}"; + var result = await SendCommandAsync(requeststring); + return result.Success; } @@ -259,7 +278,7 @@ internal async Task WriteRawRegisterAsync (IRegisterInternal _toWrite, byt #region Register reading / writing - public async Task SetRegisterAsync (IRegister register, object value) { + internal async Task SetRegisterAsync (IRegister register, object value) { var internalReg = (IRegisterInternal)register; diff --git a/MewtocolNet/MewtocolNet.csproj b/MewtocolNet/MewtocolNet.csproj index c296329..8c6901f 100644 --- a/MewtocolNet/MewtocolNet.csproj +++ b/MewtocolNet/MewtocolNet.csproj @@ -1,6 +1,6 @@  - netstandard2.0 + netstandard2.0;net6.0; Mewtocol.NET 0.7.1 Felix Weiss diff --git a/MewtocolNet/RegisterBuildInfo.cs b/MewtocolNet/RegisterBuildInfo.cs index 4ad24f6..4eda284 100644 --- a/MewtocolNet/RegisterBuildInfo.cs +++ b/MewtocolNet/RegisterBuildInfo.cs @@ -3,8 +3,7 @@ using System.Collections; using System.Reflection; -namespace MewtocolNet -{ +namespace MewtocolNet { internal struct RegisterBuildInfo { @@ -24,24 +23,21 @@ internal BaseRegister Build () { Type registerClassType = dotnetCastType.GetDefaultRegisterHoldingType(); bool isBytesRegister = !registerClassType.IsGenericType && registerClassType == typeof(BytesRegister); + bool isStringRegister = !registerClassType.IsGenericType && registerClassType == typeof(StringRegister); if (regType.IsNumericDTDDT() && (dotnetCastType == typeof(bool))) { //------------------------------------------- //as numeric register with boolean bit target //create a new bregister instance - var flags = BindingFlags.Public | BindingFlags.Instance; - - //int _adress, int _reservedByteSize, string _name = null - var parameters = new object[] { memoryAddress, memorySizeBytes, name }; - var instance = (BaseRegister)Activator.CreateInstance(typeof(BytesRegister), flags, null, parameters, null); + var instance = new BytesRegister(memoryAddress, memorySizeBytes, name); if (collectionType != null) instance.WithCollectionType(collectionType); return instance; - } else if (regType.IsNumericDTDDT() && !isBytesRegister) { + } else if (regType.IsNumericDTDDT() && !isBytesRegister && !isStringRegister) { //------------------------------------------- //as numeric register @@ -66,12 +62,20 @@ internal BaseRegister Build () { //------------------------------------------- //as byte range register + var instance = new BytesRegister(memoryAddress, memorySizeBytes, name); - //create a new bregister instance - var flags = BindingFlags.Public | BindingFlags.Instance; - //int _adress, int _reservedSize, string _name = null - var parameters = new object[] { memoryAddress, memorySizeBytes, name }; - var instance = (BaseRegister)Activator.CreateInstance(typeof(BytesRegister), flags, null, parameters, null); + if (collectionType != null) + instance.WithCollectionType(collectionType); + + return instance; + + } + + if (isStringRegister) { + + //------------------------------------------- + //as byte range register + var instance = (BaseRegister)new StringRegister(memoryAddress, name); if (collectionType != null) instance.WithCollectionType(collectionType); @@ -89,10 +93,7 @@ internal BaseRegister Build () { var spAddr = specialAddress; var areaAddr = memoryAddress; - //create a new bregister instance - var flags = BindingFlags.Public | BindingFlags.Instance; - var parameters = new object[] { io, spAddr.Value, areaAddr, name }; - var instance = (BoolRegister)Activator.CreateInstance(typeof(BoolRegister), flags, null, parameters, null); + var instance = new BoolRegister(io, spAddr.Value, areaAddr, name); if (collectionType != null) ((IRegisterInternal)instance).WithCollectionType(collectionType); diff --git a/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs b/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs index 4772469..683232c 100644 --- a/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs +++ b/MewtocolNet/RegisterBuilding/FinalizerExtensions.cs @@ -3,12 +3,11 @@ using System.Linq; using System.Reflection; -namespace MewtocolNet.RegisterBuilding -{ +namespace MewtocolNet.RegisterBuilding { public static class FinalizerExtensions { - public static IRegister Build (this RegisterBuilderStep step) { + public static IRegister Build(this RegisterBuilderStep step) { //if no casting method in builder was called => autocast the type from the RegisterType if (!step.wasCasted) @@ -34,7 +33,7 @@ public static IRegister Build (this RegisterBuilderStep step) { } - private static void GetFallbackDotnetType (this RegisterBuilderStep step) { + private static void GetFallbackDotnetType(this RegisterBuilderStep step) { bool isBoolean = step.RegType.IsBoolean(); bool isTypeNotDefined = step.plcVarType == null && step.dotnetVarType == null; @@ -66,7 +65,7 @@ private static void GetFallbackDotnetType (this RegisterBuilderStep step) { } - private static void AddToRegisterList (this RegisterBuilderStep step, BaseRegister instance) { + private static void AddToRegisterList(this RegisterBuilderStep step, BaseRegister instance) { if (step.forInterface == null) return; diff --git a/MewtocolNet/RegisterBuilding/ParseResult.cs b/MewtocolNet/RegisterBuilding/ParseResult.cs index d548d2c..b851848 100644 --- a/MewtocolNet/RegisterBuilding/ParseResult.cs +++ b/MewtocolNet/RegisterBuilding/ParseResult.cs @@ -1,4 +1,5 @@ namespace MewtocolNet.RegisterBuilding { + internal struct ParseResult { public ParseResultState state; diff --git a/MewtocolNet/Registers/BaseRegister.cs b/MewtocolNet/Registers/BaseRegister.cs index 13b0289..5a74bad 100644 --- a/MewtocolNet/Registers/BaseRegister.cs +++ b/MewtocolNet/Registers/BaseRegister.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Threading.Tasks; @@ -83,6 +82,8 @@ public virtual string BuildMewtocolQuery() { public virtual string GetValueString() => Value?.ToString() ?? "null"; + public virtual string GetAsPLC () => Value?.ToString() ?? "null"; + public virtual string GetRegisterString() => RegisterType.ToString(); public virtual string GetCombinedName() => $"{(CollectionType != null ? $"{CollectionType.Name}." : "")}{Name ?? "Unnamed"}"; @@ -99,13 +100,9 @@ public virtual string BuildMewtocolQuery() { public virtual async Task WriteAsync(object data) => throw new NotImplementedException(); - public virtual Task SetValueAsync() => throw new NotImplementedException(); - - public virtual Task GetValueAsync() => throw new NotImplementedException(); - #endregion - public override string ToString() => $"{GetRegisterPLCName()} - Value: {GetValueString()}"; + public override string ToString() => $"{GetRegisterPLCName()}{(Name != null ? $" ({Name})" : "")} - Value: {GetValueString()}"; public virtual string ToString(bool additional) { diff --git a/MewtocolNet/Registers/BoolRegister.cs b/MewtocolNet/Registers/BoolRegister.cs index 3930814..a8f14aa 100644 --- a/MewtocolNet/Registers/BoolRegister.cs +++ b/MewtocolNet/Registers/BoolRegister.cs @@ -62,8 +62,11 @@ public override void SetValueFromPLC(object val) { /// public override async Task ReadAsync() { + if (!attachedInterface.IsConnected) return null; + var read = await attachedInterface.ReadRawRegisterAsync(this); - var parsed = PlcValueParser.Parse(read); + if(read == null) return null; + var parsed = PlcValueParser.Parse(this, read); SetValueFromPLC(parsed); return parsed; @@ -73,7 +76,9 @@ public override async Task ReadAsync() { /// public override async Task WriteAsync(object data) { - return await attachedInterface.WriteRawRegisterAsync(this, PlcValueParser.Encode((bool)data)); + if (!attachedInterface.IsConnected) return false; + + return await attachedInterface.WriteRawRegisterAsync(this, PlcValueParser.Encode(this, (bool)data)); } diff --git a/MewtocolNet/Registers/BytesRegister.cs b/MewtocolNet/Registers/BytesRegister.cs index d247800..c6c102e 100644 --- a/MewtocolNet/Registers/BytesRegister.cs +++ b/MewtocolNet/Registers/BytesRegister.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace MewtocolNet.Registers { @@ -71,8 +72,12 @@ public override void SetValueFromPLC (object val) { /// public override async Task ReadAsync() { + if (!attachedInterface.IsConnected) return null; + var read = await attachedInterface.ReadRawRegisterAsync(this); - var parsed = PlcValueParser.Parse(read); + if (read == null) return null; + + var parsed = PlcValueParser.Parse(this, read); SetValueFromPLC(parsed); return parsed; @@ -82,6 +87,8 @@ public override async Task ReadAsync() { /// public override async Task WriteAsync(object data) { + if (!attachedInterface.IsConnected) return false; + return await attachedInterface.WriteRawRegisterAsync(this, (byte[])data); } diff --git a/MewtocolNet/Registers/NumberRegister.cs b/MewtocolNet/Registers/NumberRegister.cs index 0b16135..207d7de 100644 --- a/MewtocolNet/Registers/NumberRegister.cs +++ b/MewtocolNet/Registers/NumberRegister.cs @@ -92,12 +92,15 @@ public override string BuildMewtocolQuery() { } + /// + public override string GetAsPLC() => ((TimeSpan)Value).AsPLCTime(); + /// public override string GetValueString() { if(typeof(T) == typeof(TimeSpan)) { - return $"{Value} [{((TimeSpan)Value).AsPLC()}]"; + return $"{Value} [{((TimeSpan)Value).AsPLCTime()}]"; } @@ -158,8 +161,12 @@ public override string GetValueString() { /// public override async Task ReadAsync() { + if (!attachedInterface.IsConnected) return null; + var read = await attachedInterface.ReadRawRegisterAsync(this); - var parsed = PlcValueParser.Parse(read); + if (read == null) return null; + + var parsed = PlcValueParser.Parse(this, read); SetValueFromPLC(parsed); return parsed; @@ -169,7 +176,9 @@ public override async Task ReadAsync() { /// public override async Task WriteAsync(object data) { - return await attachedInterface.WriteRawRegisterAsync(this, PlcValueParser.Encode((T)data)); + if (!attachedInterface.IsConnected) return false; + + return await attachedInterface.WriteRawRegisterAsync(this, PlcValueParser.Encode(this, (T)data)); } diff --git a/MewtocolNet/Registers/StringRegister.cs b/MewtocolNet/Registers/StringRegister.cs index 14248a3..44fb5ed 100644 --- a/MewtocolNet/Registers/StringRegister.cs +++ b/MewtocolNet/Registers/StringRegister.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Net; using System.Text; using System.Threading.Tasks; @@ -11,32 +12,23 @@ namespace MewtocolNet.Registers { /// public class StringRegister : BaseRegister { - internal int addressLength; - /// - /// The rgisters memory length - /// - public int AddressLength => addressLength; - internal short ReservedSize { get; set; } + internal short UsedSize { get; set; } + + internal int WordsSize { get; set; } + + private bool isCalibrated = false; + /// /// Defines a register containing a string /// - public StringRegister (int _adress, int _reservedByteSize, string _name = null) { + public StringRegister (int _address, string _name = null) { - if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); + if (_address > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999"); name = _name; - memoryAddress = _adress; - ReservedSize = (short)_reservedByteSize; - - //calc mem length - var wordsize = (double)_reservedByteSize / 2; - if (wordsize % 2 != 0) { - wordsize++; - } - + memoryAddress = _address; RegisterType = RegisterType.DT_BYTE_RANGE; - addressLength = (int)Math.Round(wordsize + 1); } @@ -46,15 +38,18 @@ public override string BuildMewtocolQuery() { StringBuilder asciistring = new StringBuilder("D"); asciistring.Append(MemoryAddress.ToString().PadLeft(5, '0')); - asciistring.Append((MemoryAddress + AddressLength).ToString().PadLeft(5, '0')); + asciistring.Append((MemoryAddress + WordsSize - 1).ToString().PadLeft(5, '0')); return asciistring.ToString(); } + /// + public override string GetValueString() => $"'{Value}'"; + /// public override void SetValueFromPLC (object val) { - lastValue = (byte[])val; + lastValue = (string)val; TriggerChangedEvnt(this); TriggerNotifyChange(); @@ -65,20 +60,47 @@ public override void SetValueFromPLC (object val) { public override string GetRegisterString() => "DT"; /// - public override void ClearValue() => SetValueFromPLC(null); + public override void ClearValue() => SetValueFromPLC(""); /// public override async Task ReadAsync() { + if (!attachedInterface.IsConnected) return null; + + //get the string params first + + if(!isCalibrated) await Calibrate(); + var read = await attachedInterface.ReadRawRegisterAsync(this); - return PlcValueParser.Parse(read); + if (read == null) return null; + + return PlcValueParser.Parse(this, read); + + } + + private async Task Calibrate () { + + //get the string describer bytes + var bytes = await attachedInterface.ReadByteRange(MemoryAddress, 4, false); + + ReservedSize = BitConverter.ToInt16(bytes, 0); + UsedSize = BitConverter.ToInt16(bytes, 2); + WordsSize = 2 + (ReservedSize + 1) / 2; + + isCalibrated = true; } /// public override async Task WriteAsync(object data) { - return await attachedInterface.WriteRawRegisterAsync(this, (byte[])data); + if (!attachedInterface.IsConnected) return false; + + var res = await attachedInterface.WriteRawRegisterAsync(this, PlcValueParser.Encode(this, (string)data)); + + if (res) UsedSize = (short)((string)Value).Length; + + return res; } diff --git a/MewtocolNet/TCPMessageResult.cs b/MewtocolNet/TCPMessageResult.cs index d3e23eb..0d51d14 100644 --- a/MewtocolNet/TCPMessageResult.cs +++ b/MewtocolNet/TCPMessageResult.cs @@ -9,4 +9,13 @@ internal enum TCPMessageResult { } + internal enum CommandState { + + Intial, + LineFeed, + RequestedNextFrame, + Complete + + } + } \ No newline at end of file diff --git a/MewtocolNet/TypeConversion/Conversions.cs b/MewtocolNet/TypeConversion/Conversions.cs index 41543e8..53d62e4 100644 --- a/MewtocolNet/TypeConversion/Conversions.cs +++ b/MewtocolNet/TypeConversion/Conversions.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Text; namespace MewtocolNet.TypeConversion { @@ -32,11 +33,11 @@ internal static class Conversions { new PlcTypeConversion(RegisterType.R) { HoldingRegisterType = typeof(BoolRegister), PlcVarType = PlcVarType.BOOL, - FromRaw = bytes => { + FromRaw = (reg, bytes) => { return (bool)(bytes[0] == 1); }, - ToRaw = value => { + ToRaw = (reg, value) => { return new byte[] { (byte)(value ? 1 : 0) }; @@ -45,11 +46,11 @@ internal static class Conversions { new PlcTypeConversion(RegisterType.X) { HoldingRegisterType = typeof(BoolRegister), PlcVarType = PlcVarType.BOOL, - FromRaw = bytes => { + FromRaw = (reg, bytes) => { return bytes[0] == 1; }, - ToRaw = value => { + ToRaw = (reg, value) => { return new byte[] { (byte)(value ? 1 : 0) }; @@ -58,11 +59,11 @@ internal static class Conversions { new PlcTypeConversion(RegisterType.Y) { HoldingRegisterType = typeof(BoolRegister), PlcVarType = PlcVarType.BOOL, - FromRaw = bytes => { + FromRaw = (reg, bytes) => { return bytes[0] == 1; }, - ToRaw = value => { + ToRaw = (reg, value) => { return new byte[] { (byte)(value ? 1 : 0) }; @@ -71,11 +72,11 @@ internal static class Conversions { new PlcTypeConversion(RegisterType.DT) { HoldingRegisterType = typeof(NumberRegister), PlcVarType = PlcVarType.INT, - FromRaw = bytes => { + FromRaw = (reg, bytes) => { return BitConverter.ToInt16(bytes, 0); }, - ToRaw = value => { + ToRaw = (reg, value) => { return BitConverter.GetBytes(value); @@ -84,11 +85,11 @@ internal static class Conversions { new PlcTypeConversion(RegisterType.DT) { HoldingRegisterType = typeof(NumberRegister), PlcVarType = PlcVarType.UINT, - FromRaw = bytes => { + FromRaw = (reg, bytes) => { return BitConverter.ToUInt16(bytes, 0); }, - ToRaw = value => { + ToRaw = (reg, value) => { return BitConverter.GetBytes(value); @@ -97,11 +98,11 @@ internal static class Conversions { new PlcTypeConversion(RegisterType.DDT) { HoldingRegisterType = typeof(NumberRegister), PlcVarType = PlcVarType.DINT, - FromRaw = bytes => { + FromRaw = (reg, bytes) => { return BitConverter.ToInt32(bytes, 0); }, - ToRaw = value => { + ToRaw = (reg, value) => { return BitConverter.GetBytes(value); @@ -110,11 +111,11 @@ internal static class Conversions { new PlcTypeConversion(RegisterType.DDT) { HoldingRegisterType = typeof(NumberRegister), PlcVarType = PlcVarType.UDINT, - FromRaw = bytes => { + FromRaw = (reg, bytes) => { return BitConverter.ToUInt32(bytes, 0); }, - ToRaw = value => { + ToRaw = (reg, value) => { return BitConverter.GetBytes(value); @@ -123,7 +124,7 @@ internal static class Conversions { new PlcTypeConversion(RegisterType.DDT) { HoldingRegisterType = typeof(NumberRegister), PlcVarType = PlcVarType.REAL, - FromRaw = bytes => { + FromRaw = (reg, bytes) => { var val = BitConverter.ToUInt32(bytes, 0); byte[] floatVals = BitConverter.GetBytes(val); @@ -132,7 +133,7 @@ internal static class Conversions { return finalFloat; }, - ToRaw = value => { + ToRaw = (reg, value) => { return BitConverter.GetBytes(value); @@ -141,7 +142,7 @@ internal static class Conversions { new PlcTypeConversion(RegisterType.DDT) { HoldingRegisterType = typeof(NumberRegister), PlcVarType = PlcVarType.TIME, - FromRaw = bytes => { + FromRaw = (reg, bytes) => { var vallong = BitConverter.ToUInt32(bytes, 0); var valMillis = vallong * 10; @@ -149,7 +150,7 @@ internal static class Conversions { return ts; }, - ToRaw = value => { + ToRaw = (reg, value) => { var tLong = (uint)(value.TotalMilliseconds / 10); return BitConverter.GetBytes(tLong); @@ -158,19 +159,52 @@ internal static class Conversions { }, new PlcTypeConversion(RegisterType.DT) { HoldingRegisterType = typeof(BytesRegister), - FromRaw = bytes => bytes, - ToRaw = value => value, + FromRaw = (reg, bytes) => bytes, + ToRaw = (reg, value) => value, + }, + new PlcTypeConversion(RegisterType.DT_BYTE_RANGE) { + HoldingRegisterType = typeof(StringRegister), + PlcVarType = PlcVarType.STRING, + FromRaw = (reg, bytes) => { + + //get actual showed size + short actualLen = BitConverter.ToInt16(bytes, 2); + + //skip 4 bytes because they only describe the length + return Encoding.UTF8.GetString(bytes.Skip(4).Take(actualLen).ToArray()); + + }, + ToRaw = (reg, value) => { + + var sReg = (StringRegister)reg; + + if(value.Length > sReg.ReservedSize) + value = value.Substring(0, sReg.ReservedSize); + + int padLen = sReg.ReservedSize; + if(sReg.ReservedSize % 2 != 0) padLen++; + + var strBytes = Encoding.UTF8.GetBytes(value.PadRight(padLen, '\0')); + + List finalBytes = new List(); + finalBytes.AddRange(BitConverter.GetBytes(sReg.ReservedSize)); + finalBytes.AddRange(BitConverter.GetBytes((short)value.Length)); + finalBytes.AddRange(strBytes); + + return finalBytes.ToArray(); + + }, }, new PlcTypeConversion(RegisterType.DT) { HoldingRegisterType = typeof(BytesRegister), PlcVarType = PlcVarType.WORD, - FromRaw = bytes => { + FromRaw = (reg, bytes) => { BitArray bitAr = new BitArray(bytes); return bitAr; }, - ToRaw = value => { + ToRaw = (reg, value) => { byte[] ret = new byte[(value.Length - 1) / 8 + 1]; value.CopyTo(ret, 0); diff --git a/MewtocolNet/TypeConversion/IPlcTypeConverter.cs b/MewtocolNet/TypeConversion/IPlcTypeConverter.cs index 0b381f8..cdbffb5 100644 --- a/MewtocolNet/TypeConversion/IPlcTypeConverter.cs +++ b/MewtocolNet/TypeConversion/IPlcTypeConverter.cs @@ -4,9 +4,9 @@ namespace MewtocolNet { internal interface IPlcTypeConverter { - object FromRawData(byte[] data); + object FromRawData(IRegister register, byte[] data); - byte[] ToRawData(object value); + byte[] ToRawData(IRegister register, object value); Type GetDotnetType(); diff --git a/MewtocolNet/TypeConversion/PlcTypeConversion.cs b/MewtocolNet/TypeConversion/PlcTypeConversion.cs index f50d0b5..b65f961 100644 --- a/MewtocolNet/TypeConversion/PlcTypeConversion.cs +++ b/MewtocolNet/TypeConversion/PlcTypeConversion.cs @@ -13,9 +13,9 @@ internal class PlcTypeConversion : IPlcTypeConverter { public Type HoldingRegisterType { get; set; } - public Func FromRaw { get; set; } + public Func FromRaw { get; set; } - public Func ToRaw { get; set; } + public Func ToRaw { get; set; } public PlcTypeConversion(RegisterType plcType) { @@ -32,9 +32,9 @@ public PlcTypeConversion(RegisterType plcType) { public PlcVarType GetPlcVarType() => PlcVarType; - public object FromRawData(byte[] data) => FromRaw.Invoke(data); + public object FromRawData(IRegister register, byte[] data) => FromRaw.Invoke(register, data); - public byte[] ToRawData(object value) => ToRaw.Invoke((T)value); + public byte[] ToRawData(IRegister register, object value) => ToRaw.Invoke(register, (T)value); } diff --git a/MewtocolNet/TypeConversion/PlcValueParser.cs b/MewtocolNet/TypeConversion/PlcValueParser.cs index c6c5497..ab873d8 100644 --- a/MewtocolNet/TypeConversion/PlcValueParser.cs +++ b/MewtocolNet/TypeConversion/PlcValueParser.cs @@ -12,25 +12,25 @@ internal static class PlcValueParser { private static List conversions => Conversions.items; - public static T Parse(byte[] bytes) { + public static T Parse(IRegister register, byte[] bytes) { var converter = conversions.FirstOrDefault(x => x.GetDotnetType() == typeof(T)); if (converter == null) throw new MewtocolException($"A converter for the dotnet type {typeof(T)} doesn't exist"); - return (T)converter.FromRawData(bytes); + return (T)converter.FromRawData(register, bytes); } - public static byte[] Encode (T value) { + public static byte[] Encode (IRegister register, T value) { var converter = conversions.FirstOrDefault(x => x.GetDotnetType() == typeof(T)); if (converter == null) throw new MewtocolException($"A converter for the dotnet type {typeof(T)} doesn't exist"); - return converter.ToRawData(value); + return converter.ToRawData(register, value); } diff --git a/MewtocolTests/TestRegisterInterface.cs b/MewtocolTests/TestRegisterInterface.cs index 1b0e3ab..b2b8b50 100644 --- a/MewtocolTests/TestRegisterInterface.cs +++ b/MewtocolTests/TestRegisterInterface.cs @@ -34,8 +34,8 @@ public void NumericRegisterMewtocolIdentifiers() { "D0005000051", //double word register "D0005000051", //double word register "D0005000051", //double word register - "D0005000065", //variable len register even bytes - "D0005000066", //variable len register odd bytes + "D0005000064", //variable len register even bytes + "D0005000065", //variable len register odd bytes }; //test mewtocol idents diff --git a/PLC_Test/test_c30_fpx_h.pro b/PLC_Test/test_c30_fpx_h.pro index 564b2a649c500559ce1cd06107dd065b159329d3..098bb1c000c969a9c522080b2f2289d348875031 100644 GIT binary patch delta 424 zcmZo@5NK!+n2^J9iGhLPN6MU(&5jd#*f()ZWM;n%5!pP0`>86s2aw|(H)nH;;X-l7 z2b0x2!{PERo+}yQ924(XF3do4HY-l}6ejR`D}_;@2& zK=g^PC3CLVoaw2uj8j1(?SEw%xBr!8dKp#!7-%gU5S!#NF)#yZ5YYVp|9?Ic14G4} zxd8|H4h9I!$qH1DDJUua$l3JmQNsj*?_Mq{u9r4*{5!qfcJo=YS{0{VXG_dCzdw5} zI=SZ~!<0_0KvlLQ6C?za|Mc-a%+mcXdv(&A=)G;B>9w89lg}=cpPU-MQ);72lX3mG zKkgQvZr3Jmevw@ka+>eq?N0%_b_!nKx|(mvZSALia;p@yXB8h~T)utRg)LV8v71tF zzTf-bQg`dEUpwApUiRnrF*wYm$n;Ef))TdkovU=0Caqi9&AYl~lb~fHmz`Uf_@o(1 s{SKec2o(iI>tCE$8XQr4xV){h>^!sFpFd0r4DC-UnYKTvWR__I0FXeqfB*mh delta 424 zcmZo@5NK!+n2^J9f`NhIo9^tC&5jd#*f()ZWM)4J5!pP0`>86s8<2B^bN1#I!-e9E z=O(LphQsAsJXbQpIVRq(T$q98Y*w7`$DjEm=j_P}58S|X=7TpNdUM1>HYTt@;^U2A z0nsPEmdrr2r>Dv?PGvs9IlKL@EaUdSvP>_d>R$k@WdmZ9JSGNaAPoX4|NsBbXJTNe zm@_xvAm2d;f!b)-xdp;9cVcRq7VQ19z)7iay^Bkbifwnx|4-I-hf??EE_*qN!|?2D zpZjn2Rm!g2TE;k~lPgfQ?MR1&uy9SkSm2~vcfuE?Zc6_u6*l*~@0}^8?R6#dY#(OT zA7Rq$yYWdj`F#54# - + - + - + From 15cc2e245d8d9a15de385a7ed13ee39a6a6d23f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Wed, 28 Jun 2023 23:31:07 +0200 Subject: [PATCH 08/18] Added blazor MewExplorer project --- MewExplorer/App.xaml | 26 + MewExplorer/App.xaml.cs | 9 + MewExplorer/Main.razor | 11 + MewExplorer/MainPage.xaml | 14 + MewExplorer/MainPage.xaml.cs | 7 + MewExplorer/MauiProgram.cs | 22 + MewExplorer/MewExplorer.csproj | 56 ++ MewExplorer/Pages/Index.razor | 31 + MewExplorer/Pages/Index.razor.css | 48 ++ .../Platforms/Android/AndroidManifest.xml | 6 + MewExplorer/Platforms/Android/MainActivity.cs | 8 + .../Platforms/Android/MainApplication.cs | 12 + .../Android/Resources/values/colors.xml | 6 + MewExplorer/Platforms/Windows/App.xaml | 17 + MewExplorer/Platforms/Windows/App.xaml.cs | 21 + .../Platforms/Windows/Package.appxmanifest | 46 ++ MewExplorer/Platforms/Windows/app.manifest | 15 + MewExplorer/Properties/launchSettings.json | 8 + MewExplorer/Resources/AppIcon/appicon.svg | 4 + MewExplorer/Resources/AppIcon/appiconfg.svg | 8 + .../Resources/Fonts/OpenSans-Regular.ttf | Bin 0 -> 107124 bytes MewExplorer/Resources/Images/dotnet_bot.svg | 93 +++ MewExplorer/Resources/Raw/AboutAssets.txt | 15 + MewExplorer/Resources/Splash/splash.svg | 8 + MewExplorer/Shared/MainLayout.razor | 21 + MewExplorer/Shared/MainLayout.razor.css | 40 ++ MewExplorer/Shared/NavMenu.razor | 24 + MewExplorer/Shared/NavMenu.razor.css | 44 ++ MewExplorer/Shared/PlcStatus.razor | 6 + MewExplorer/Shared/PlcStatus.razor.css | 1 + MewExplorer/Shared/PlcStatusBar.razor | 7 + MewExplorer/Shared/PlcStatusBar.razor.css | 10 + MewExplorer/Shared/SurveyPrompt.razor | 16 + MewExplorer/_Imports.razor | 8 + MewExplorer/wwwroot/css/app.css | 94 +++ .../wwwroot/css/open-iconic/FONT-LICENSE | 86 +++ .../wwwroot/css/open-iconic/ICON-LICENSE | 21 + MewExplorer/wwwroot/css/open-iconic/README.md | 114 ++++ .../font/css/open-iconic-bootstrap.min.css | 1 + .../open-iconic/font/fonts/open-iconic.eot | Bin 0 -> 28196 bytes .../open-iconic/font/fonts/open-iconic.otf | Bin 0 -> 20996 bytes .../open-iconic/font/fonts/open-iconic.svg | 543 ++++++++++++++++++ .../open-iconic/font/fonts/open-iconic.ttf | Bin 0 -> 38220 bytes .../open-iconic/font/fonts/open-iconic.woff | Bin 0 -> 14984 bytes MewExplorer/wwwroot/favicon.ico | Bin 0 -> 5430 bytes MewExplorer/wwwroot/index.html | 29 + MewtocolNet.sln | 25 +- 47 files changed, 1580 insertions(+), 1 deletion(-) create mode 100644 MewExplorer/App.xaml create mode 100644 MewExplorer/App.xaml.cs create mode 100644 MewExplorer/Main.razor create mode 100644 MewExplorer/MainPage.xaml create mode 100644 MewExplorer/MainPage.xaml.cs create mode 100644 MewExplorer/MauiProgram.cs create mode 100644 MewExplorer/MewExplorer.csproj create mode 100644 MewExplorer/Pages/Index.razor create mode 100644 MewExplorer/Pages/Index.razor.css create mode 100644 MewExplorer/Platforms/Android/AndroidManifest.xml create mode 100644 MewExplorer/Platforms/Android/MainActivity.cs create mode 100644 MewExplorer/Platforms/Android/MainApplication.cs create mode 100644 MewExplorer/Platforms/Android/Resources/values/colors.xml create mode 100644 MewExplorer/Platforms/Windows/App.xaml create mode 100644 MewExplorer/Platforms/Windows/App.xaml.cs create mode 100644 MewExplorer/Platforms/Windows/Package.appxmanifest create mode 100644 MewExplorer/Platforms/Windows/app.manifest create mode 100644 MewExplorer/Properties/launchSettings.json create mode 100644 MewExplorer/Resources/AppIcon/appicon.svg create mode 100644 MewExplorer/Resources/AppIcon/appiconfg.svg create mode 100644 MewExplorer/Resources/Fonts/OpenSans-Regular.ttf create mode 100644 MewExplorer/Resources/Images/dotnet_bot.svg create mode 100644 MewExplorer/Resources/Raw/AboutAssets.txt create mode 100644 MewExplorer/Resources/Splash/splash.svg create mode 100644 MewExplorer/Shared/MainLayout.razor create mode 100644 MewExplorer/Shared/MainLayout.razor.css create mode 100644 MewExplorer/Shared/NavMenu.razor create mode 100644 MewExplorer/Shared/NavMenu.razor.css create mode 100644 MewExplorer/Shared/PlcStatus.razor create mode 100644 MewExplorer/Shared/PlcStatus.razor.css create mode 100644 MewExplorer/Shared/PlcStatusBar.razor create mode 100644 MewExplorer/Shared/PlcStatusBar.razor.css create mode 100644 MewExplorer/Shared/SurveyPrompt.razor create mode 100644 MewExplorer/_Imports.razor create mode 100644 MewExplorer/wwwroot/css/app.css create mode 100644 MewExplorer/wwwroot/css/open-iconic/FONT-LICENSE create mode 100644 MewExplorer/wwwroot/css/open-iconic/ICON-LICENSE create mode 100644 MewExplorer/wwwroot/css/open-iconic/README.md create mode 100644 MewExplorer/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css create mode 100644 MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.eot create mode 100644 MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.otf create mode 100644 MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.svg create mode 100644 MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf create mode 100644 MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.woff create mode 100644 MewExplorer/wwwroot/favicon.ico create mode 100644 MewExplorer/wwwroot/index.html diff --git a/MewExplorer/App.xaml b/MewExplorer/App.xaml new file mode 100644 index 0000000..eba1b07 --- /dev/null +++ b/MewExplorer/App.xaml @@ -0,0 +1,26 @@ + + + + + + #512bdf + White + + + + + + + + \ No newline at end of file diff --git a/MewExplorer/App.xaml.cs b/MewExplorer/App.xaml.cs new file mode 100644 index 0000000..42f7b60 --- /dev/null +++ b/MewExplorer/App.xaml.cs @@ -0,0 +1,9 @@ +namespace MewExplorer; + +public partial class App : Application { + public App() { + InitializeComponent(); + + MainPage = new MainPage(); + } +} diff --git a/MewExplorer/Main.razor b/MewExplorer/Main.razor new file mode 100644 index 0000000..8d4cb6f --- /dev/null +++ b/MewExplorer/Main.razor @@ -0,0 +1,11 @@ + + + + + + + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/MewExplorer/MainPage.xaml b/MewExplorer/MainPage.xaml new file mode 100644 index 0000000..fc365d0 --- /dev/null +++ b/MewExplorer/MainPage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/MewExplorer/MainPage.xaml.cs b/MewExplorer/MainPage.xaml.cs new file mode 100644 index 0000000..0aa8fd8 --- /dev/null +++ b/MewExplorer/MainPage.xaml.cs @@ -0,0 +1,7 @@ +namespace MewExplorer; + +public partial class MainPage : ContentPage { + public MainPage() { + InitializeComponent(); + } +} diff --git a/MewExplorer/MauiProgram.cs b/MewExplorer/MauiProgram.cs new file mode 100644 index 0000000..2d13104 --- /dev/null +++ b/MewExplorer/MauiProgram.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Logging; + +namespace MewExplorer; +public static class MauiProgram { + public static MauiApp CreateMauiApp() { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + }); + + builder.Services.AddMauiBlazorWebView(); + + #if DEBUG + builder.Services.AddBlazorWebViewDeveloperTools(); + builder.Logging.AddDebug(); + #endif + + return builder.Build(); + } +} diff --git a/MewExplorer/MewExplorer.csproj b/MewExplorer/MewExplorer.csproj new file mode 100644 index 0000000..9b64231 --- /dev/null +++ b/MewExplorer/MewExplorer.csproj @@ -0,0 +1,56 @@ + + + + net7.0-android;net7.0-ios;net7.0-maccatalyst + $(TargetFrameworks);net7.0-windows10.0.19041.0 + + + Exe + MewExplorer + true + true + enable + false + + + MewExplorer + + + com.companyname.mewexplorer + 5DD46388-985C-4BE2-94EF-3BFE6B5A280B + + + 1.0 + 1 + + 14.2 + 14.0 + 24.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MewExplorer/Pages/Index.razor b/MewExplorer/Pages/Index.razor new file mode 100644 index 0000000..a351049 --- /dev/null +++ b/MewExplorer/Pages/Index.razor @@ -0,0 +1,31 @@ +@page "/" + +
+ +
+ +
+ + Plc Overview + FPX-H C30T + +
+ 192.168.0.1 +
+ +
+ +
+ + Plc Overview + Text + +
+ +
+ +
+ +
+ +
\ No newline at end of file diff --git a/MewExplorer/Pages/Index.razor.css b/MewExplorer/Pages/Index.razor.css new file mode 100644 index 0000000..3a4f079 --- /dev/null +++ b/MewExplorer/Pages/Index.razor.css @@ -0,0 +1,48 @@ +.index-main { + + display: flex; + flex-direction: column; + flex: 1; + align-items: stretch; + justify-content: stretch; + height: 100%; + +} + +.plc-header { + + background: var(--dark-2); + display: flex; + flex-direction: row; + flex-basis: 30%; + + color: white; + +} + +.plc-header-left { + display: flex; + flex-direction: column; + flex-basis: 50%; + padding: 1em; +} + +.plc-header-left > span:nth-child(1) { + + font-size: 1em; + +} + +.plc-header-left > span:nth-child(2) { + + font-size: 2em; + +} + +.plc-header-right { + + display: flex; + flex-direction: column; + padding: 1em; + +} diff --git a/MewExplorer/Platforms/Android/AndroidManifest.xml b/MewExplorer/Platforms/Android/AndroidManifest.xml new file mode 100644 index 0000000..dbf9e7e --- /dev/null +++ b/MewExplorer/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/MewExplorer/Platforms/Android/MainActivity.cs b/MewExplorer/Platforms/Android/MainActivity.cs new file mode 100644 index 0000000..7e7bca8 --- /dev/null +++ b/MewExplorer/Platforms/Android/MainActivity.cs @@ -0,0 +1,8 @@ +using Android.App; +using Android.Content.PM; +using Android.OS; + +namespace MewExplorer; +[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] +public class MainActivity : MauiAppCompatActivity { +} diff --git a/MewExplorer/Platforms/Android/MainApplication.cs b/MewExplorer/Platforms/Android/MainApplication.cs new file mode 100644 index 0000000..18a3a2c --- /dev/null +++ b/MewExplorer/Platforms/Android/MainApplication.cs @@ -0,0 +1,12 @@ +using Android.App; +using Android.Runtime; + +namespace MewExplorer; +[Application] +public class MainApplication : MauiApplication { + public MainApplication(IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) { + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/MewExplorer/Platforms/Android/Resources/values/colors.xml b/MewExplorer/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 0000000..c04d749 --- /dev/null +++ b/MewExplorer/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #512BD4 + #2B0B98 + #2B0B98 + \ No newline at end of file diff --git a/MewExplorer/Platforms/Windows/App.xaml b/MewExplorer/Platforms/Windows/App.xaml new file mode 100644 index 0000000..6d32a5b --- /dev/null +++ b/MewExplorer/Platforms/Windows/App.xaml @@ -0,0 +1,17 @@ + + + + + #201C21 + #201C21 + #FFF + #FFF + + + + diff --git a/MewExplorer/Platforms/Windows/App.xaml.cs b/MewExplorer/Platforms/Windows/App.xaml.cs new file mode 100644 index 0000000..5f4e8dc --- /dev/null +++ b/MewExplorer/Platforms/Windows/App.xaml.cs @@ -0,0 +1,21 @@ +using Microsoft.UI.Xaml; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace MewExplorer.WinUI; +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +public partial class App : MauiWinUIApplication { + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() { + this.InitializeComponent(); + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} + diff --git a/MewExplorer/Platforms/Windows/Package.appxmanifest b/MewExplorer/Platforms/Windows/Package.appxmanifest new file mode 100644 index 0000000..7125b1b --- /dev/null +++ b/MewExplorer/Platforms/Windows/Package.appxmanifest @@ -0,0 +1,46 @@ + + + + + + + + + $placeholder$ + User Name + $placeholder$.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MewExplorer/Platforms/Windows/app.manifest b/MewExplorer/Platforms/Windows/app.manifest new file mode 100644 index 0000000..6abc802 --- /dev/null +++ b/MewExplorer/Platforms/Windows/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/MewExplorer/Properties/launchSettings.json b/MewExplorer/Properties/launchSettings.json new file mode 100644 index 0000000..edf8aad --- /dev/null +++ b/MewExplorer/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Windows Machine": { + "commandName": "MsixPackage", + "nativeDebugging": false + } + } +} \ No newline at end of file diff --git a/MewExplorer/Resources/AppIcon/appicon.svg b/MewExplorer/Resources/AppIcon/appicon.svg new file mode 100644 index 0000000..9d63b65 --- /dev/null +++ b/MewExplorer/Resources/AppIcon/appicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/MewExplorer/Resources/AppIcon/appiconfg.svg b/MewExplorer/Resources/AppIcon/appiconfg.svg new file mode 100644 index 0000000..21dfb25 --- /dev/null +++ b/MewExplorer/Resources/AppIcon/appiconfg.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/MewExplorer/Resources/Fonts/OpenSans-Regular.ttf b/MewExplorer/Resources/Fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..293a1cd10666f3b0d965fec6f0e78bb30ccbb963 GIT binary patch literal 107124 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 literal 0 HcmV?d00001 diff --git a/MewExplorer/Resources/Images/dotnet_bot.svg b/MewExplorer/Resources/Images/dotnet_bot.svg new file mode 100644 index 0000000..abfaff2 --- /dev/null +++ b/MewExplorer/Resources/Images/dotnet_bot.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MewExplorer/Resources/Raw/AboutAssets.txt b/MewExplorer/Resources/Raw/AboutAssets.txt new file mode 100644 index 0000000..531df33 --- /dev/null +++ b/MewExplorer/Resources/Raw/AboutAssets.txt @@ -0,0 +1,15 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories). Deployment of the asset to your application +is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. + + + +These files will be deployed with you package and will be accessible using Essentials: + + async Task LoadMauiAsset() + { + using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); + using var reader = new StreamReader(stream); + + var contents = reader.ReadToEnd(); + } diff --git a/MewExplorer/Resources/Splash/splash.svg b/MewExplorer/Resources/Splash/splash.svg new file mode 100644 index 0000000..21dfb25 --- /dev/null +++ b/MewExplorer/Resources/Splash/splash.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/MewExplorer/Shared/MainLayout.razor b/MewExplorer/Shared/MainLayout.razor new file mode 100644 index 0000000..8593ecd --- /dev/null +++ b/MewExplorer/Shared/MainLayout.razor @@ -0,0 +1,21 @@ +@inherits LayoutComponentBase + +
+ +
+ + + +
+
+ @Body +
+
+ +
+ + + +
diff --git a/MewExplorer/Shared/MainLayout.razor.css b/MewExplorer/Shared/MainLayout.razor.css new file mode 100644 index 0000000..61e35f4 --- /dev/null +++ b/MewExplorer/Shared/MainLayout.razor.css @@ -0,0 +1,40 @@ +.page { + + background: var(--dark-3); + position: relative; + height: 100vh; + display: flex; + flex: 1; + flex-direction: column; + align-items: stretch; + justify-content: stretch; + +} + +.hor-layout { + + display: flex; + flex-direction: row; + flex: 1; + +} + +main { + + flex: 1; + display: flex; + +} + +main article { + + flex: 1; + +} + +.sidebar { + + background: var(--dark-1); + flex-basis: 25%; + +} \ No newline at end of file diff --git a/MewExplorer/Shared/NavMenu.razor b/MewExplorer/Shared/NavMenu.razor new file mode 100644 index 0000000..700dffe --- /dev/null +++ b/MewExplorer/Shared/NavMenu.razor @@ -0,0 +1,24 @@ +
+ MewExplorer +
+ +
+ +
+ +@code { + private bool collapseNavMenu = true; + + private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; + + private void ToggleNavMenu() + { + collapseNavMenu = !collapseNavMenu; + } +} diff --git a/MewExplorer/Shared/NavMenu.razor.css b/MewExplorer/Shared/NavMenu.razor.css new file mode 100644 index 0000000..a5b8bd3 --- /dev/null +++ b/MewExplorer/Shared/NavMenu.razor.css @@ -0,0 +1,44 @@ +.app-name-header { + + margin: 2em; + color: white; + +} + +.navbar-brand { + font-size: 1.1rem; +} + +.oi { + margin: 1em; + width: 2rem; + font-size: 1.1rem; + vertical-align: text-top; + top: -2px; +} + +.nav-item { + + font-size: 0.9rem; + padding: 2em; + +} + +.nav-item ::deep a { + color: #d7d7d7; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; +} + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.25); + color: white; +} + +.nav-item ::deep a:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} \ No newline at end of file diff --git a/MewExplorer/Shared/PlcStatus.razor b/MewExplorer/Shared/PlcStatus.razor new file mode 100644 index 0000000..813e7bc --- /dev/null +++ b/MewExplorer/Shared/PlcStatus.razor @@ -0,0 +1,6 @@ +

PlcStatus

+ +@code { + + +} \ No newline at end of file diff --git a/MewExplorer/Shared/PlcStatus.razor.css b/MewExplorer/Shared/PlcStatus.razor.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/MewExplorer/Shared/PlcStatus.razor.css @@ -0,0 +1 @@ + diff --git a/MewExplorer/Shared/PlcStatusBar.razor b/MewExplorer/Shared/PlcStatusBar.razor new file mode 100644 index 0000000..1177caf --- /dev/null +++ b/MewExplorer/Shared/PlcStatusBar.razor @@ -0,0 +1,7 @@ +
+ Stat bar +
+ +@code { + +} diff --git a/MewExplorer/Shared/PlcStatusBar.razor.css b/MewExplorer/Shared/PlcStatusBar.razor.css new file mode 100644 index 0000000..ca66444 --- /dev/null +++ b/MewExplorer/Shared/PlcStatusBar.razor.css @@ -0,0 +1,10 @@ +.plc-stat-bar { + color: white; + background: var(--vibrant-dark); + display: flex; + flex-direction: row; + flex-basis: 1.5em; + align-items: center; + padding-left: .5em; + font-size: 1rem; +} diff --git a/MewExplorer/Shared/SurveyPrompt.razor b/MewExplorer/Shared/SurveyPrompt.razor new file mode 100644 index 0000000..fc67435 --- /dev/null +++ b/MewExplorer/Shared/SurveyPrompt.razor @@ -0,0 +1,16 @@ +
+ + @Title + + + Please take our + brief survey + + and tell us what you think. +
+ +@code { + // Demonstrates how a parent component can supply parameters + [Parameter] + public string Title { get; set; } +} diff --git a/MewExplorer/_Imports.razor b/MewExplorer/_Imports.razor new file mode 100644 index 0000000..a7075a3 --- /dev/null +++ b/MewExplorer/_Imports.razor @@ -0,0 +1,8 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using MewExplorer +@using MewExplorer.Shared diff --git a/MewExplorer/wwwroot/css/app.css b/MewExplorer/wwwroot/css/app.css new file mode 100644 index 0000000..c2feb37 --- /dev/null +++ b/MewExplorer/wwwroot/css/app.css @@ -0,0 +1,94 @@ +@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); + +:root { + --vibrant-dark: #00CC14; + --vibrant-light: #5CFF6C; + --dark-1: #201C21; + --dark-2: #2B262C; + --dark-3: #352F37; + --light-1: #EFF2EF; +} + +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + margin: 0; +} + +h1:focus { + outline: none; +} + +a, .btn-link { + color: #0071c1; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.content { + padding-top: 1.1rem; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + +.blazor-error-boundary::after { + content: "An error has occurred." +} + +.status-bar-safe-area { + display: none; +} + +@supports (-webkit-touch-callout: none) { + .status-bar-safe-area { + display: flex; + position: sticky; + top: 0; + height: env(safe-area-inset-top); + background-color: #f7f7f7; + width: 100%; + z-index: 1; + } + + .flex-column, .navbar-brand { + padding-left: env(safe-area-inset-left); + } +} diff --git a/MewExplorer/wwwroot/css/open-iconic/FONT-LICENSE b/MewExplorer/wwwroot/css/open-iconic/FONT-LICENSE new file mode 100644 index 0000000..a1dc03f --- /dev/null +++ b/MewExplorer/wwwroot/css/open-iconic/FONT-LICENSE @@ -0,0 +1,86 @@ +SIL OPEN FONT LICENSE Version 1.1 + +Copyright (c) 2014 Waybury + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/MewExplorer/wwwroot/css/open-iconic/ICON-LICENSE b/MewExplorer/wwwroot/css/open-iconic/ICON-LICENSE new file mode 100644 index 0000000..2199f4a --- /dev/null +++ b/MewExplorer/wwwroot/css/open-iconic/ICON-LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Waybury + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/MewExplorer/wwwroot/css/open-iconic/README.md b/MewExplorer/wwwroot/css/open-iconic/README.md new file mode 100644 index 0000000..5ac0c17 --- /dev/null +++ b/MewExplorer/wwwroot/css/open-iconic/README.md @@ -0,0 +1,114 @@ +[Open Iconic v1.1.1](https://github.com/iconic/open-iconic) +=========== + +### Open Iconic is the open source sibling of [Iconic](https://github.com/iconic/open-iconic). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](https://github.com/iconic/open-iconic) + + + +## What's in Open Iconic? + +* 223 icons designed to be legible down to 8 pixels +* Super-light SVG files - 61.8 for the entire set +* SVG sprite—the modern replacement for icon fonts +* Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats +* Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats +* PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. + + +## Getting Started + +#### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](https://github.com/iconic/open-iconic) and [Reference](https://github.com/iconic/open-iconic) sections. + +### General Usage + +#### Using Open Iconic's SVGs + +We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). + +``` +icon name +``` + +#### Using Open Iconic's SVG Sprite + +Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. + +Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* + +``` + + + +``` + +Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. + +``` +.icon { + width: 16px; + height: 16px; +} +``` + +Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. + +``` +.icon-account-login { + fill: #f00; +} +``` + +To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). + +#### Using Open Iconic's Icon Font... + + +##### …with Bootstrap + +You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` + + +``` + +``` + + +``` + +``` + +##### …with Foundation + +You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` + +``` + +``` + + +``` + +``` + +##### …on its own + +You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` + +``` + +``` + +``` + +``` + + +## License + +### Icons + +All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). + +### Fonts + +All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). diff --git a/MewExplorer/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css b/MewExplorer/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css new file mode 100644 index 0000000..4664f2e --- /dev/null +++ b/MewExplorer/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css @@ -0,0 +1 @@ +@font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} \ No newline at end of file diff --git a/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.eot b/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.eot new file mode 100644 index 0000000000000000000000000000000000000000..f98177dbf711863eff7c90f84d5d419d02d99ba8 GIT binary patch literal 28196 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 f%X4~$E_2;^J#ifP;CI~=<%5iE_!YyhznS + + + + +Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 + By P.J. Onori +Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf b/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a894b68ca3993534243218e9a10df03d502979dc GIT binary patch literal 38220 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 literal 0 HcmV?d00001 diff --git a/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.woff b/MewExplorer/wwwroot/css/open-iconic/font/fonts/open-iconic.woff new file mode 100644 index 0000000000000000000000000000000000000000..f9309988aeab3868040d3b322658902098eba27f GIT binary patch literal 14984 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 literal 0 HcmV?d00001 diff --git a/MewExplorer/wwwroot/favicon.ico b/MewExplorer/wwwroot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..63e859b476eff5055e0e557aaa151ca8223fbeef GIT binary patch literal 5430 zcmc&&Yj2xp8Fqnv;>&(QB_ve7>^E#o2mu=cO~A%R>DU-_hfbSRv1t;m7zJ_AMrntN zy0+^f&8be>q&YYzH%(88lQ?#KwiCzaCO*ZEo%j&v;<}&Lj_stKTKK>#U3nin@AF>w zb3ONSAFR{u(S1d?cdw53y}Gt1b-Hirbh;;bm(Rcbnoc*%@jiaXM|4jU^1WO~`TYZ~ zC-~jh9~b-f?fX`DmwvcguQzn*uV}c^Vd&~?H|RUs4Epv~gTAfR(B0lT&?RWQOtduM z^1vUD9{HQsW!{a9|0crA34m7Z6lpG^}f6f?={zD+ zXAzk^i^aKN_}s2$eX81wjSMONE#WVdzf|MT)Ap*}Vsn!XbvsI#6o&ij{87^d%$|A{ z=F{KB%)g%@z76yBzbb7seW**Ju8r4e*Z3PWNX3_tTDgzZatz7)Q6ytwB%@&@A|XT; zecM`Snxx5po$C)%yCP!KEtos~eOS)@2=kX-RIm)4glMCoagTEFxrBeSX%Euz734Fk z%7)x(k~T!@Hbg_37NSQL!vlTBXoURSzt~I**Zw`&F24fH*&kx=%nvZv|49SC*daD( zIw<~%#=lk8{2-l(BcIjy^Q$Q&m#KlWL9?UG{b8@qhlD z;umc+6p%|NsAT~0@DgV4-NKgQuWPWrmPIK&&XhV&n%`{l zOl^bbWYjQNuVXTXESO)@|iUKVmErPUDfz2Wh`4dF@OFiaCW|d`3paV^@|r^8T_ZxM)Z+$p5qx# z#K=z@%;aBPO=C4JNNGqVv6@UGolIz;KZsAro``Rz8X%vq_gpi^qEV&evgHb_=Y9-l z`)imdx0UC>GWZYj)3+3aKh?zVb}=@%oNzg7a8%kfVl)SV-Amp1Okw&+hEZ3|v(k8vRjXW9?ih`&FFM zV$~{j3IzhtcXk?Mu_!12;=+I7XK-IR2>Yd%VB^?oI9c^E&Chb&&je$NV0P-R;ujkP z;cbLCCPEF6|22NDj=S`F^2e~XwT1ZnRX8ra0#DaFa9-X|8(xNW_+JhD75WnSd7cxo z2>I_J5{c|WPfrgl7E2R)^c}F7ry()Z>$Jhk9CzZxiPKL#_0%`&{MX>P_%b~Dx0D^S z7xP1(DQ!d_Icpk!RN3I1w@~|O1ru#CO==h#9M~S4Chx*@?=EKUPGBv$tmU+7Zs_al z`!jR?6T&Z7(%uVq>#yLu`abWk!FBlnY{RFNHlj~6zh*;@u}+}viRKsD`IIxN#R-X3 z@vxu#EA_m}I503U(8Qmx^}u;)KfGP`O9E1H1Q|xeeksX8jC%@!{YT1)!lWgO=+Y3*jr=iSxvOW1}^HSy=y){tOMQJ@an>sOl4FYniE z;GOxd7AqxZNbYFNqobpv&HVO$c-w!Y*6r;$2oJ~h(a#(Bp<-)dg*mNigX~9rPqcHv z^;c*|Md?tD)$y?6FO$DWl$jUGV`F1G_^E&E>sY*YnA~ruv3=z9F8&&~Xpm<<75?N3 z>x~`I&M9q)O1=zWZHN9hZWx>RQ}zLP+iL57Q)%&_^$Sme^^G7;e-P~CR?kqU#Io#( z(nH1Wn*Ig)|M>WLGrxoU?FZrS`4GO&w;+39A3f8w{{Q7eg|$+dIlNFPAe+tN=FOYU z{A&Fg|H73+w1IK(W=j*L>JQgz$g0 z7JpKXLHIh}#$wm|N`s}o-@|L_`>*(gTQ~)wr3Eap7g%PVNisKw82im;Gdv#85x#s+ zoqqtnwu4ycd>cOQgRh-=aEJbnvVK`}ja%+FZx}&ehtX)n(9nVfe4{mn0bgijUbNr7Tf5X^$*{qh2%`?--%+sbSrjE^;1e3>% zqa%jdY16{Y)a1hSy*mr0JGU05Z%=qlx5vGvTjSpTt6k%nR06q}1DU`SQh_ZAeJ}A@`hL~xvv05U?0%=spP`R>dk?cOWM9^KNb7B?xjex>OZo%JMQQ1Q zB|q@}8RiP@DWn-(fB;phPaIOP2Yp)XN3-Fsn)S3w($4&+p8f5W_f%gac}QvmkHfCj$2=!t`boCvQ zCW;&Dto=f8v##}dy^wg3VNaBy&kCe3N;1|@n@pUaMPT?(aJ9b*(gJ28$}(2qFt$H~u5z94xcIQkcOI++)*exzbrk?WOOOf*|%k5#KV zL=&ky3)Eirv$wbRJ2F2s_ILQY--D~~7>^f}W|Aw^e7inXr#WLI{@h`0|jHud2Y~cI~Yn{r_kU^Vo{1gja + + + + + MewExplorer + + + + + + + + +

+ +
Loading...
+ +
+ + + + + + \ No newline at end of file diff --git a/MewtocolNet.sln b/MewtocolNet.sln index 4f0c7eb..2550c60 100644 --- a/MewtocolNet.sln +++ b/MewtocolNet.sln @@ -7,7 +7,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewtocolNet", "MewtocolNet\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples", "Examples\Examples.csproj", "{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MewtocolTests", "MewtocolTests\MewtocolTests.csproj", "{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewtocolTests", "MewtocolTests\MewtocolTests.csproj", "{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MewExplorer", "MewExplorer\MewExplorer.csproj", "{F243F38A-76D3-4C38-BAE6-C61729479661}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -55,8 +57,29 @@ Global {C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Release|x64.Build.0 = Release|Any CPU {C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Release|x86.ActiveCfg = Release|Any CPU {C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Release|x86.Build.0 = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|x64.ActiveCfg = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|x64.Build.0 = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|x64.Deploy.0 = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|x86.ActiveCfg = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|x86.Build.0 = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Debug|x86.Deploy.0 = Debug|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|Any CPU.Build.0 = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|Any CPU.Deploy.0 = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|x64.ActiveCfg = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|x64.Build.0 = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|x64.Deploy.0 = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|x86.ActiveCfg = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|x86.Build.0 = Release|Any CPU + {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|x86.Deploy.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4ABB8137-CD8F-4691-9802-9ED371012F47} + EndGlobalSection EndGlobal From c332cd9f86d2ad57afab1dc2d4945dcdf9983e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Thu, 29 Jun 2023 18:13:28 +0200 Subject: [PATCH 09/18] Added COM cassette features --- Examples/ExampleScenarios.cs | 167 ++++------------ Examples/Program.cs | 18 +- MewtocolNet/ComCassette/CassetteFinder.cs | 161 ++++++++++++++++ .../ComCassette/CassetteInformation.cs | 182 ++++++++++++++++++ MewtocolNet/ComCassette/CassetteStatus.cs | 23 +++ MewtocolNet/Extensions/AsyncExtensions.cs | 27 +++ MewtocolNet/MewtocolHelpers.cs | 25 ++- MewtocolNet/MewtocolInterface.cs | 19 +- 8 files changed, 474 insertions(+), 148 deletions(-) create mode 100644 MewtocolNet/ComCassette/CassetteFinder.cs create mode 100644 MewtocolNet/ComCassette/CassetteInformation.cs create mode 100644 MewtocolNet/ComCassette/CassetteStatus.cs create mode 100644 MewtocolNet/Extensions/AsyncExtensions.cs diff --git a/Examples/ExampleScenarios.cs b/Examples/ExampleScenarios.cs index 24dcd95..7d62dff 100644 --- a/Examples/ExampleScenarios.cs +++ b/Examples/ExampleScenarios.cs @@ -10,6 +10,9 @@ using System.Diagnostics; using System.Text; using Microsoft.Win32; +using MewtocolNet.ComCassette; +using System.Linq; +using System.Net; namespace Examples; @@ -31,61 +34,6 @@ public void SetupLogger () { } - [Scenario("Permament connection with poller")] - public async Task RunCyclicPollerAsync () { - - Console.WriteLine("Starting poller scenario"); - - int runTime = 10000; - int remainingTime = runTime; - - //setting up a new PLC interface and register collection - MewtocolInterface interf = new MewtocolInterface("192.168.115.210"); - TestRegisters registers = new TestRegisters(); - - //attaching the register collection and an automatic poller - interf.WithRegisterCollection(registers).WithPoller(); - - await interf.ConnectAsync(); - await interf.AwaitFirstDataAsync(); - - _ = Task.Factory.StartNew(async () => { - - while (interf.IsConnected) { - - //flip the bool register each tick and wait for it to be registered - //await interf.SetRegisterAsync(nameof(registers.TestBool1), !registers.TestBool1); - - Console.Title = - $"Speed UP: {interf.BytesPerSecondUpstream} B/s, " + - $"Speed DOWN: {interf.BytesPerSecondDownstream} B/s, " + - $"Poll cycle: {interf.PollerCycleDurationMs} ms, " + - $"Queued MSGs: {interf.QueuedMessages}"; - - Console.Clear(); - Console.WriteLine("Underlying registers on tick: \n"); - - foreach (var register in interf.Registers) - Console.WriteLine($"{register.ToString(true)}"); - - Console.WriteLine($"{registers.TestBool1}"); - Console.WriteLine($"{registers.TestDuplicate}"); - - remainingTime -= 1000; - - Console.WriteLine($"\nStopping in: {remainingTime}ms"); - - await Task.Delay(1000); - - } - - }); - - await Task.Delay(runTime); - interf.Disconnect(); - - } - [Scenario("Dispose and disconnect connection")] public async Task RunDisposalAndDisconnectAsync () { @@ -125,68 +73,7 @@ public async Task RunDisposalAndDisconnectAsync () { } - [Scenario("Test auto enums and bitwise, needs the example program from MewtocolNet/PLC_Test")] - public async Task RunEnumsBitwiseAsync () { - - Console.WriteLine("Starting auto enums and bitwise"); - - //setting up a new PLC interface and register collection - MewtocolInterface interf = new MewtocolInterface("192.168.115.210"); - TestRegistersEnumBitwise registers = new TestRegistersEnumBitwise(); - - //attaching the register collection and an automatic poller - interf.WithRegisterCollection(registers).WithPoller(); - - registers.PropertyChanged += (s, e) => { - - Console.Clear(); - - var props = registers.GetType().GetProperties(); - - foreach (var prop in props) { - - var val = prop.GetValue(registers); - string printVal = val?.ToString() ?? "null"; - - if (val is BitArray bitarr) { - printVal = bitarr.ToBitString(); - } - - Console.Write($"{prop.Name} - "); - - if(printVal == "True") { - Console.ForegroundColor = ConsoleColor.Green; - } - - Console.Write($"{printVal}"); - - Console.ResetColor(); - - Console.WriteLine(); - - } - - }; - - await interf.ConnectAsync(); - - //use the async method to make sure the cycling is stopped - //await interf.SetRegisterAsync(nameof(registers.StartCyclePLC), false); - - await Task.Delay(5000); - - //set the register without waiting for it async - registers.StartCyclePLC = true; - - await Task.Delay(5000); - - registers.StartCyclePLC = false; - - await Task.Delay(2000); - - } - - [Scenario("Read register test")] + [Scenario("Read all kinds of example registers")] public async Task RunReadTest () { //setting up a new PLC interface and register collection @@ -260,8 +147,8 @@ void setTitle () { } - [Scenario("Test multi frame")] - public async Task MultiFrameTest() { + [Scenario("Test read speed 100 R registers")] + public async Task ReadRSpeedTest() { var preLogLevel = Logger.LogLevel; Logger.LogLevel = LogLevel.Critical; @@ -273,11 +160,7 @@ public async Task MultiFrameTest() { //auto add all built registers to the interface var builder = RegBuilder.ForInterface(interf); - var r0reg = builder.FromPlcRegName("R0").Build(); - builder.FromPlcRegName("R1").Build(); - builder.FromPlcRegName("DT0").AsBytes(100).Build(); - - for (int i = 1; i < 100; i++) { + for (int i = 0; i < 100; i++) { builder.FromPlcRegName($"R{i}A").Build(); @@ -295,7 +178,7 @@ public async Task MultiFrameTest() { Console.WriteLine("Poller cycle finished"); - Console.WriteLine($"Single frame excec time: {sw.ElapsedMilliseconds:N0}ms for {cmdCount} commands"); + Console.WriteLine($"Single frame excec time: {sw.ElapsedMilliseconds:N0}ms for {cmdCount} commands and {interf.Registers.Count()} R registers"); interf.Disconnect(); @@ -303,4 +186,38 @@ public async Task MultiFrameTest() { } + [Scenario("Find all COM5 cassettes in the network")] + public async Task FindCassettes () { + + Console.Clear(); + + var casettes = await CassetteFinder.FindClientsAsync(); + + foreach (var cassette in casettes) { + + Console.WriteLine($"{cassette.Name}"); + Console.WriteLine($"IP: {cassette.IPAddress}"); + Console.WriteLine($"Port: {cassette.Port}"); + Console.WriteLine($"DHCP: {cassette.UsesDHCP}"); + Console.WriteLine($"Subnet Mask: {cassette.SubnetMask}"); + Console.WriteLine($"Gateway: {cassette.GatewayAddress}"); + Console.WriteLine($"Mac: {cassette.MacAddress.ToHexString(":")}"); + Console.WriteLine($"Firmware: {cassette.FirmwareVersion}"); + Console.WriteLine($"Status: {cassette.Status}"); + Console.WriteLine($"Endpoint: {cassette.EndpointName} - {cassette.Endpoint.Address}"); + Console.WriteLine(); + + } + + await Task.Delay(5000); + + var found = casettes.FirstOrDefault(x => x.Endpoint.Address.ToString() == "10.237.191.75"); + + found.IPAddress = IPAddress.Parse($"192.168.1.{new Random().Next(20, 120)}"); + found.Name = $"Rand{new Random().Next(5, 15)}"; + + await found.SendNewConfigAsync(); + + } + } diff --git a/Examples/Program.cs b/Examples/Program.cs index 3b2a56e..dc1b27c 100644 --- a/Examples/Program.cs +++ b/Examples/Program.cs @@ -7,6 +7,8 @@ using System.Threading.Tasks; using MewtocolNet.Logging; using System.Text.RegularExpressions; +using System.Globalization; +using System.Threading; namespace Examples; @@ -16,13 +18,21 @@ class Program { static void Main(string[] args) { + Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us"); + + Console.Clear(); + AppDomain.CurrentDomain.UnhandledException += (s,e) => { - Console.WriteLine(e.ExceptionObject.ToString()); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Uncatched exception: {e.ExceptionObject.ToString()}"); + Console.ResetColor(); }; - TaskScheduler.UnobservedTaskException += (s,e) => { - Console.WriteLine(e.Exception.ToString()); - }; + //TaskScheduler.UnobservedTaskException += (s,e) => { + // Console.ForegroundColor = ConsoleColor.Magenta; + // Console.WriteLine($"Unobserved Task Uncatched exception: {e.Exception.ToString()}"); + // Console.ResetColor(); + //}; ExampleSzenarios.SetupLogger(); diff --git a/MewtocolNet/ComCassette/CassetteFinder.cs b/MewtocolNet/ComCassette/CassetteFinder.cs new file mode 100644 index 0000000..346b212 --- /dev/null +++ b/MewtocolNet/ComCassette/CassetteFinder.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MewtocolNet.ComCassette { + + /// + /// Provides a interface to modify and find PLC network cassettes also known as COM5 + /// + public class CassetteFinder { + + public static async Task> FindClientsAsync (string ipSource = null, int timeoutMs = 100) { + + var from = new IPEndPoint(IPAddress.Any, 0); + + List cassettesFound = new List(); + List>> interfacesTasks = new List>>(); + + var usableInterfaces = GetUseableNetInterfaces(); + + if (ipSource == null) { + + var interfaces = NetworkInterface.GetAllNetworkInterfaces(); + + foreach (NetworkInterface netInterface in usableInterfaces) { + + IPInterfaceProperties ipProps = netInterface.GetIPProperties(); + var unicastInfo = ipProps.UnicastAddresses + .FirstOrDefault(x => x.Address.AddressFamily == AddressFamily.InterNetwork); + + var ep = new IPEndPoint(unicastInfo.Address, 0); + interfacesTasks.Add(FindClientsForEndpoint(ep, timeoutMs, netInterface.Name)); + + } + + } else { + + from = new IPEndPoint(IPAddress.Parse(ipSource), 0); + + var netInterface = usableInterfaces.FirstOrDefault(x => x.GetIPProperties().UnicastAddresses.Any(y => y.Address.ToString() == ipSource)); + + if (netInterface == null) + throw new NotSupportedException($"The host endpoint {ipSource}, is not available"); + + interfacesTasks.Add(FindClientsForEndpoint(from, timeoutMs, netInterface.Name)); + + } + + //run the interface querys + var grouped = await Task.WhenAll(interfacesTasks); + + foreach (var item in grouped) + cassettesFound.AddRange(item); + + return cassettesFound; + + } + + private static IEnumerable GetUseableNetInterfaces () { + + foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces()) { + + bool isEthernet = + netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet || + netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet3Megabit || + netInterface.NetworkInterfaceType == NetworkInterfaceType.FastEthernetFx || + netInterface.NetworkInterfaceType == NetworkInterfaceType.FastEthernetT || + netInterface.NetworkInterfaceType == NetworkInterfaceType.GigabitEthernet; + + bool isWlan = netInterface.NetworkInterfaceType == NetworkInterfaceType.Wireless80211; + + bool isUsable = netInterface.OperationalStatus == OperationalStatus.Up; + + if (!isUsable) continue; + if (!(isWlan || isEthernet)) continue; + + IPInterfaceProperties ipProps = netInterface.GetIPProperties(); + var hasUnicastInfo = ipProps.UnicastAddresses + .Any(x => x.Address.AddressFamily == AddressFamily.InterNetwork); + + if (!hasUnicastInfo) continue; + + yield return netInterface; + + } + + } + + private static async Task> FindClientsForEndpoint (IPEndPoint from, int timeoutMs, string ipEndpointName) { + + var cassettesFound = new List(); + + int plcPort = 9090; + + // Byte msg to request the status transmission of all plcs + byte[] requestCode = new byte[] { 0x88, 0x40, 0x00 }; + + // The start code of the status transmission response + byte[] startCode = new byte[] { 0x88, 0xC0, 0x00 }; + + using(var udpClient = new UdpClient()) { + + udpClient.EnableBroadcast = true; + + udpClient.Client.Bind(from); + + //broadcast packet to all devices (plc specific package) + udpClient.Send(requestCode, requestCode.Length, "255.255.255.255", plcPort); + + //canceling after no new data was read + CancellationTokenSource tSource = new CancellationTokenSource(); + var tm = new System.Timers.Timer(timeoutMs); + tm.Elapsed += (s, e) => { + tSource.Cancel(); + tm.Stop(); + }; + tm.Start(); + + //wait for devices to send response + try { + + byte[] recvBuffer = null; + + while (!tSource.Token.IsCancellationRequested) { + + var res = await udpClient.ReceiveAsync().WithCancellation(tSource.Token); + + if (res.Buffer == null) break; + + recvBuffer = res.Buffer; + + if (recvBuffer.SearchBytePattern(startCode) == 0) { + + tm.Stop(); + tm.Start(); + + var parsed = CassetteInformation.FromBytes(recvBuffer, from, ipEndpointName); + if (parsed != null) cassettesFound.Add(parsed); + + } + + } + + } catch (OperationCanceledException) { } catch (SocketException) { } + + } + + return cassettesFound; + + } + + } + +} diff --git a/MewtocolNet/ComCassette/CassetteInformation.cs b/MewtocolNet/ComCassette/CassetteInformation.cs new file mode 100644 index 0000000..fd5845f --- /dev/null +++ b/MewtocolNet/ComCassette/CassetteInformation.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +//WARNING! The whole UDP protocol was reverse engineered and is not fully implemented.. + +namespace MewtocolNet.ComCassette { + + /// + /// Information about the COM cassette + /// + public class CassetteInformation { + + /// + /// Indicates if the cassette is currently configurating + /// + public bool IsConfigurating { get; private set; } + + /// + /// Name of the COM cassette + /// + public string Name { get; set; } + + /// + /// Indicates the usage of DHCP + /// + public bool UsesDHCP { get; set; } + + /// + /// IP Address of the COM cassette + /// + public IPAddress IPAddress { get; set; } + + /// + /// Subnet mask of the cassette + /// + public IPAddress SubnetMask { get; set; } + + /// + /// Default gateway of the cassette + /// + public IPAddress GatewayAddress { get; set; } + + /// + /// Mac address of the cassette + /// + public byte[] MacAddress { get; private set; } + + /// + /// The source endpoint the cassette is reachable from + /// + public IPEndPoint Endpoint { get; private set; } + + /// + /// The name of the endpoint the device is reachable from, or null if not specifically defined + /// + public string EndpointName { get; private set; } + + /// + /// Firmware version as string + /// + public string FirmwareVersion { get; private set; } + + /// + /// The tcp port of the cassette + /// + public int Port { get; private set; } + + /// + /// Status of the cassette + /// + public CassetteStatus Status { get; private set; } + + internal static CassetteInformation FromBytes(byte[] bytes, IPEndPoint endpoint, string endpointName) { + + // Receive data package explained: + // 0 3 4 8 12 17 22 24 27 29 31 32 + // 88 C0 00 | 00 | C0 A8 73 D4 | FF FF FF 00 | C0 A8 73 3C | 00 | C0 8F 60 53 1C | 01 10 | 23 86 | 00 | 25 | 00 | 00 | 00 | 0D | (byte) * (n) NAME LEN + // Header |DHCP| IPv4 addr. | Subnet Mask | IPv4 Gatwy | | Mac Addr. | Ver. | Port | | | |STAT| | Name LEN | Name + // 1 or 0 Procuct Type? StatusCode Length of Name + + //get ips / mac + var dhcpOn = bytes.Skip(3).First() != 0x00; + var ipAdd = new IPAddress(bytes.Skip(4).Take(4).ToArray()); + var subnetMask = new IPAddress(bytes.Skip(8).Take(4).ToArray()); + var gateWaysAdd = new IPAddress(bytes.Skip(12).Take(4).ToArray()); + var macAdd = bytes.Skip(17).Take(5).ToArray(); + var firmwareV = string.Join(".", bytes.Skip(22).Take(2).Select(x => x.ToString("X1")).ToArray()); + var port = BitConverter.ToUInt16(bytes.Skip(24).Take(2).Reverse().ToArray(), 0); + var status = (CassetteStatus)bytes.Skip(29).First(); + + //missing blocks, later + + //get name + var name = Encoding.ASCII.GetString(bytes.Skip(32).ToArray()); + + return new CassetteInformation { + + Name = name, + UsesDHCP = dhcpOn, + IPAddress = ipAdd, + SubnetMask = subnetMask, + GatewayAddress = gateWaysAdd, + MacAddress = macAdd, + Endpoint = endpoint, + EndpointName = endpointName, + FirmwareVersion = firmwareV, + Port = port, + Status = status, + + }; + + } + + public async Task SendNewConfigAsync () { + + if (IsConfigurating) return; + + // this command gets sent to a specific plc ip address to overwrite the cassette config + // If dhcp is set to 1 the ip is ignored but still must be valid + + // 88 41 00 | 00 | C0 8F 61 07 1B | 05 | 54 65 73 74 31 | 05 | 46 50 58 45 54 | 00 | C0 A8 01 07 | FF FF FF 00 | C0 A8 73 3C + // Header | | | 5 | T e s t 1 | 05 | F P X E T |0||1| 192.168.1.7 | 255.255... | 192.168.115.60 + // Header | | Mac Address |LEN>| ASCII Name |LEN>| Static |DHCP| Target IP | Subnet Mask | Gateway + + IsConfigurating = true; + + List sendBytes = new List(); + + //add cmd header + sendBytes.AddRange(new byte[] { 0x88, 0x41, 0x00, 0x00 }); + + //add mac + sendBytes.AddRange(MacAddress); + + //add name length + sendBytes.Add((byte)Name.Length); + + //add name + sendBytes.AddRange(Encoding.ASCII.GetBytes(Name)); + + //FPXET + var subname = Encoding.ASCII.GetBytes("TESTFP"); + + //add sub name length + sendBytes.Add((byte)subname.Length); + + //add subname + sendBytes.AddRange(subname); + + //add dhcp 0 | 1 + sendBytes.Add((byte)(UsesDHCP ? 0x01 : 0x00)); + + //add ip address + sendBytes.AddRange(IPAddress.GetAddressBytes()); + + //add subnet mask ip address + sendBytes.AddRange(SubnetMask.GetAddressBytes()); + + //add gateway ip + sendBytes.AddRange(GatewayAddress.GetAddressBytes()); + + var sendBytesArr = sendBytes.ToArray(); + + using(var udpClient = new UdpClient()) { + + udpClient.Client.Bind(Endpoint); + + //broadcast packet to all devices (plc specific package) + await udpClient.SendAsync(sendBytesArr, sendBytesArr.Length, "255.255.255.255", 9090); + + } + + } + + } + +} diff --git a/MewtocolNet/ComCassette/CassetteStatus.cs b/MewtocolNet/ComCassette/CassetteStatus.cs new file mode 100644 index 0000000..4f84666 --- /dev/null +++ b/MewtocolNet/ComCassette/CassetteStatus.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.ComCassette { + + /// + /// Needs a list of all status codes.. hard to reverse engineer + /// + public enum CassetteStatus { + + /// + /// Cassette is running as intended + /// + Normal = 0, + /// + /// Cassette DHCP resolution error + /// + DHCPError = 2, + + } + +} diff --git a/MewtocolNet/Extensions/AsyncExtensions.cs b/MewtocolNet/Extensions/AsyncExtensions.cs new file mode 100644 index 0000000..463ad8a --- /dev/null +++ b/MewtocolNet/Extensions/AsyncExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.Threading; +using System.Net.Sockets; + +namespace MewtocolNet { + + internal static class AsyncExtensions { + + public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) { + + var tcs = new TaskCompletionSource(); + using (cancellationToken.Register(s => ((TaskCompletionSource)s).TrySetResult(true), tcs)) { + if (task != await Task.WhenAny(task, tcs.Task)) { + throw new OperationCanceledException(cancellationToken); + } + } + + return task.Result; + + } + + } + +} diff --git a/MewtocolNet/MewtocolHelpers.cs b/MewtocolNet/MewtocolHelpers.cs index 99be553..1b4f071 100644 --- a/MewtocolNet/MewtocolHelpers.cs +++ b/MewtocolNet/MewtocolHelpers.cs @@ -59,6 +59,29 @@ public static string ToBitString(this BitArray arr) { #region Byte and string operation helpers + /// + /// Searches a byte array for a pattern + /// + /// + /// + /// The start index of the found pattern or -1 + public static int SearchBytePattern(this byte[] src, byte[] pattern) { + + int maxFirstCharSlot = src.Length - pattern.Length + 1; + for (int i = 0; i < maxFirstCharSlot; i++) { + if (src[i] != pattern[0]) // compare only first byte + continue; + + // found a match on first byte, now try to match rest of the pattern + for (int j = pattern.Length - 1; j >= 1; j--) { + if (src[i + j] != pattern[j]) break; + if (j == 1) return i; + } + } + return -1; + + } + /// /// Converts a string (after converting to upper case) to ascii bytes /// @@ -204,7 +227,7 @@ internal static byte[] HexStringToByteArray (this string hex) { /// /// Seperator between the hex numbers /// The byte array - internal static string ToHexString (this byte[] arr, string seperator = "") { + public static string ToHexString (this byte[] arr, string seperator = "") { StringBuilder sb = new StringBuilder(); diff --git a/MewtocolNet/MewtocolInterface.cs b/MewtocolNet/MewtocolInterface.cs index cb48916..793720c 100644 --- a/MewtocolNet/MewtocolInterface.cs +++ b/MewtocolNet/MewtocolInterface.cs @@ -466,7 +466,7 @@ private async Task SendFrameAsync (string frame, bool use await stream.ReadAsync(responseBuffer, 0, responseBuffer.Length); bool terminatorReceived = responseBuffer.Any(x => x == (byte)CR); - var delimiterTerminatorIdx = SearchBytePattern(responseBuffer, new byte[] { (byte)DELIMITER, (byte)CR }); + var delimiterTerminatorIdx = responseBuffer.SearchBytePattern(new byte[] { (byte)DELIMITER, (byte)CR }); if (terminatorReceived && delimiterTerminatorIdx == -1) { cmdState = CommandState.Complete; @@ -557,23 +557,6 @@ private int CheckForErrorMsg (string msg) { } - private int SearchBytePattern (byte[] src, byte[] pattern) { - - int maxFirstCharSlot = src.Length - pattern.Length + 1; - for (int i = 0; i < maxFirstCharSlot; i++) { - if (src[i] != pattern[0]) // compare only first byte - continue; - - // found a match on first byte, now try to match rest of the pattern - for (int j = pattern.Length - 1; j >= 1; j--) { - if (src[i + j] != pattern[j]) break; - if (j == 1) return i; - } - } - return -1; - - } - #endregion #region Disposing From 8c2ba1f68f09f9101f0d3a68d2592ec429ad16a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Fri, 30 Jun 2023 18:39:19 +0200 Subject: [PATCH 10/18] Added serial port support - complete restructure of codebase --- Examples/ExampleScenarios.cs | 95 ++- Examples/Program.cs | 24 +- MewtocolNet/CpuInfo.cs | 2 +- MewtocolNet/Extensions/AsyncExtensions.cs | 2 + .../Extensions/SerialPortExtensions.cs | 36 + .../SerialQueue.cs => Helpers/AsyncQueue.cs} | 6 +- MewtocolNet/{ => Helpers}/CodeDescriptions.cs | 0 MewtocolNet/{ => Helpers}/MewtocolHelpers.cs | 0 MewtocolNet/IPlc.cs | 96 +++ MewtocolNet/IPlcEthernet.cs | 33 + MewtocolNet/IPlcSerial.cs | 57 ++ MewtocolNet/InternalEnums/CommandState.cs | 12 + MewtocolNet/Logging/Logger.cs | 2 +- MewtocolNet/Mewtocol.cs | 62 ++ MewtocolNet/MewtocolInterface.cs | 637 ++++++------------ ...s => MewtocolInterfaceRegisterHandling.cs} | 23 +- MewtocolNet/MewtocolInterfaceRequests.cs | 5 +- MewtocolNet/MewtocolInterfaceSerial.cs | 260 +++++++ MewtocolNet/MewtocolInterfaceTcp.cs | 164 +++++ MewtocolNet/MewtocolNet.csproj | 3 + MewtocolNet/PLCMode.cs | 2 +- MewtocolNet/PublicEnums/BaudRate.cs | 21 + .../BitCount.cs | 3 +- .../{PLCEnums => PublicEnums}/CpuType.cs | 0 MewtocolNet/PublicEnums/DataBits.cs | 9 + MewtocolNet/PublicEnums/IOType.cs | 26 + .../{PLCEnums => PublicEnums}/OPMode.cs | 0 .../{PLCEnums => PublicEnums}/PlcVarType.cs | 0 .../RegisterType.cs} | 23 - MewtocolNet/RegisterBuilding/RegBuilder.cs | 12 +- .../RegisterBuildInfo.cs | 2 +- .../{ => Registers/Interfaces}/IRegister.cs | 0 .../Interfaces}/IRegisterInternal.cs | 0 MewtocolNet/Responses.cs | 27 - MewtocolNet/TCPMessageResult.cs | 21 - MewtocolTests/AutomatedPropertyRegisters.cs | 16 +- MewtocolTests/TestLivePLC.cs | 8 +- 37 files changed, 1130 insertions(+), 559 deletions(-) create mode 100644 MewtocolNet/Extensions/SerialPortExtensions.cs rename MewtocolNet/{Queue/SerialQueue.cs => Helpers/AsyncQueue.cs} (90%) rename MewtocolNet/{ => Helpers}/CodeDescriptions.cs (100%) rename MewtocolNet/{ => Helpers}/MewtocolHelpers.cs (100%) create mode 100644 MewtocolNet/IPlc.cs create mode 100644 MewtocolNet/IPlcEthernet.cs create mode 100644 MewtocolNet/IPlcSerial.cs create mode 100644 MewtocolNet/InternalEnums/CommandState.cs create mode 100644 MewtocolNet/Mewtocol.cs rename MewtocolNet/{DynamicInterface.cs => MewtocolInterfaceRegisterHandling.cs} (97%) create mode 100644 MewtocolNet/MewtocolInterfaceSerial.cs create mode 100644 MewtocolNet/MewtocolInterfaceTcp.cs create mode 100644 MewtocolNet/PublicEnums/BaudRate.cs rename MewtocolNet/{RegisterAttributes => PublicEnums}/BitCount.cs (85%) rename MewtocolNet/{PLCEnums => PublicEnums}/CpuType.cs (100%) create mode 100644 MewtocolNet/PublicEnums/DataBits.cs create mode 100644 MewtocolNet/PublicEnums/IOType.cs rename MewtocolNet/{PLCEnums => PublicEnums}/OPMode.cs (100%) rename MewtocolNet/{PLCEnums => PublicEnums}/PlcVarType.cs (100%) rename MewtocolNet/{RegisterEnums.cs => PublicEnums/RegisterType.cs} (59%) rename MewtocolNet/{ => RegisterBuilding}/RegisterBuildInfo.cs (98%) rename MewtocolNet/{ => Registers/Interfaces}/IRegister.cs (100%) rename MewtocolNet/{ => Registers/Interfaces}/IRegisterInternal.cs (100%) delete mode 100644 MewtocolNet/Responses.cs delete mode 100644 MewtocolNet/TCPMessageResult.cs diff --git a/Examples/ExampleScenarios.cs b/Examples/ExampleScenarios.cs index 7d62dff..00c1a95 100644 --- a/Examples/ExampleScenarios.cs +++ b/Examples/ExampleScenarios.cs @@ -13,6 +13,7 @@ using MewtocolNet.ComCassette; using System.Linq; using System.Net; +using System.IO.Ports; namespace Examples; @@ -21,7 +22,7 @@ public class ExampleScenarios { public void SetupLogger () { //attaching the logger - Logger.LogLevel = LogLevel.Error; + Logger.LogLevel = LogLevel.Verbose; Logger.OnNewLogMessage((date, level, msg) => { if (level == LogLevel.Error) Console.ForegroundColor = ConsoleColor.Red; @@ -38,7 +39,7 @@ public void SetupLogger () { public async Task RunDisposalAndDisconnectAsync () { //automatic disposal - using (var interf = new MewtocolInterface("192.168.115.210")) { + using (var interf = Mewtocol.Ethernet("192.168.115.210")) { await interf.ConnectAsync(); @@ -55,7 +56,7 @@ public async Task RunDisposalAndDisconnectAsync () { Console.WriteLine("Disposed, closed connection"); //manual close - var interf2 = new MewtocolInterface("192.168.115.210"); + var interf2 = Mewtocol.Ethernet("192.168.115.210"); await interf2.ConnectAsync(); @@ -77,7 +78,7 @@ public async Task RunDisposalAndDisconnectAsync () { public async Task RunReadTest () { //setting up a new PLC interface and register collection - MewtocolInterface interf = new MewtocolInterface("192.168.115.210").WithPoller(); + var interf = Mewtocol.Ethernet("192.168.115.210").WithPoller(); //auto add all built registers to the interface var builder = RegBuilder.ForInterface(interf); @@ -147,20 +148,18 @@ void setTitle () { } - [Scenario("Test read speed 100 R registers")] - public async Task ReadRSpeedTest() { + [Scenario("Test read speed TCP (n) R registers")] + public async Task ReadRSpeedTest (string registerCount) { var preLogLevel = Logger.LogLevel; Logger.LogLevel = LogLevel.Critical; //setting up a new PLC interface and register collection - MewtocolInterface interf = new MewtocolInterface("192.168.115.210") { - ConnectTimeout = 3000, - }; + using var interf = Mewtocol.Ethernet("192.168.115.210"); //auto add all built registers to the interface var builder = RegBuilder.ForInterface(interf); - for (int i = 0; i < 100; i++) { + for (int i = 0; i < int.Parse(registerCount); i++) { builder.FromPlcRegName($"R{i}A").Build(); @@ -169,6 +168,11 @@ public async Task ReadRSpeedTest() { //connect await interf.ConnectAsync(); + if(!interf.IsConnected) { + Console.WriteLine("Aborted, connection failed"); + return; + } + Console.WriteLine("Poller cycle started"); var sw = Stopwatch.StartNew(); @@ -180,12 +184,75 @@ public async Task ReadRSpeedTest() { Console.WriteLine($"Single frame excec time: {sw.ElapsedMilliseconds:N0}ms for {cmdCount} commands and {interf.Registers.Count()} R registers"); - interf.Disconnect(); - await Task.Delay(1000); } + [Scenario("Test read speed Serial (n) R registers")] + public async Task ReadRSpeedTestSerial (string registerCount) { + + var preLogLevel = Logger.LogLevel; + Logger.LogLevel = LogLevel.Critical; + + //setting up a new PLC interface and register collection + //MewtocolInterfaceShared interf = Mewtocol.SerialAuto("COM4"); + using var interf = Mewtocol.Serial("COM4", BaudRate._115200, DataBits.Eight, Parity.Odd, StopBits.One); + + //auto add all built registers to the interface + var builder = RegBuilder.ForInterface(interf); + for (int i = 0; i < int.Parse(registerCount); i++) { + + builder.FromPlcRegName($"R{i}A").Build(); + + } + + //connect + await interf.ConnectAsync(); + + if (!interf.IsConnected) { + Console.WriteLine("Aborted, connection failed"); + return; + } + + Console.WriteLine("Poller cycle started"); + var sw = Stopwatch.StartNew(); + + int cmdCount = await interf.RunPollerCylceManual(); + + sw.Stop(); + + Console.WriteLine("Poller cycle finished"); + + Console.WriteLine($"Single frame excec time: {sw.ElapsedMilliseconds:N0}ms for {cmdCount} commands and {interf.Registers.Count()} R registers"); + + } + + [Scenario("Test automatic serial port setup")] + public async Task TestAutoSerialSetup () { + + var preLogLevel = Logger.LogLevel; + Logger.LogLevel = LogLevel.Critical; + + //setting up a new PLC interface and register collection + var interf = Mewtocol.SerialAuto("COM4"); + + //connect + await interf.ConnectAsync(); + + if (!interf.IsConnected) { + + Console.WriteLine("Aborted, connection failed"); + return; + + } else { + + Console.WriteLine("Serial port settings found"); + + } + + + } + [Scenario("Find all COM5 cassettes in the network")] public async Task FindCassettes () { @@ -209,10 +276,10 @@ public async Task FindCassettes () { } - await Task.Delay(5000); - var found = casettes.FirstOrDefault(x => x.Endpoint.Address.ToString() == "10.237.191.75"); + if (found == null) return; + found.IPAddress = IPAddress.Parse($"192.168.1.{new Random().Next(20, 120)}"); found.Name = $"Rand{new Random().Next(5, 15)}"; diff --git a/Examples/Program.cs b/Examples/Program.cs index dc1b27c..d925120 100644 --- a/Examples/Program.cs +++ b/Examples/Program.cs @@ -54,7 +54,8 @@ private static void LoopInput () { if(foundAtt != null && foundAtt is ScenarioAttribute att) { - Console.WriteLine($"[{j + 1}] {method.Name}() - {att.Description}"); + string paramsStr = string.Join(" ", method.GetParameters().Select(x => x.Name)); + Console.WriteLine($"[{j + 1}] {method.Name}({paramsStr}) - {att.Description}"); invokeableMethods.Add(method); j++; @@ -78,6 +79,8 @@ private static void LoopInput () { var line = Console.ReadLine(); var loggerMatch = Regex.Match(line, @"logger (?[a-zA-Z]{0,})"); + var splitInput = Regex.Split(line, " "); + if (loggerMatch.Success && Enum.TryParse(loggerMatch.Groups["level"].Value, out var loglevel)) { @@ -93,13 +96,26 @@ private static void LoopInput () { Console.Clear(); - } else if (int.TryParse(line, out var lineNum)) { + } else if (int.TryParse(splitInput[0], out var lineNum)) { var index = Math.Clamp(lineNum - 1, 0, invokeableMethods.Count - 1); - var task = (Task)invokeableMethods.ElementAt(index).Invoke(ExampleSzenarios, null); + object[] invParams = null; + + if(splitInput.Length > 1) { + invParams = splitInput.Skip(1).Cast().ToArray(); + } + + try { - task.Wait(); + var task = (Task)invokeableMethods.ElementAt(index).Invoke(ExampleSzenarios, invParams); + task.Wait(); + + } catch (TargetParameterCountException) { + + Console.WriteLine("Missing parameters"); + + } Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("The program ran to completition"); diff --git a/MewtocolNet/CpuInfo.cs b/MewtocolNet/CpuInfo.cs index 001d491..969226f 100644 --- a/MewtocolNet/CpuInfo.cs +++ b/MewtocolNet/CpuInfo.cs @@ -5,7 +5,7 @@ namespace MewtocolNet { /// /// Contains information about the plc and its cpu /// - public partial class CpuInfo { + public struct CpuInfo { /// /// The cpu type of the plc diff --git a/MewtocolNet/Extensions/AsyncExtensions.cs b/MewtocolNet/Extensions/AsyncExtensions.cs index 463ad8a..120dea0 100644 --- a/MewtocolNet/Extensions/AsyncExtensions.cs +++ b/MewtocolNet/Extensions/AsyncExtensions.cs @@ -18,6 +18,8 @@ public static async Task WithCancellation(this Task task, CancellationT } } + if(task.IsCanceled) return default(T); + return task.Result; } diff --git a/MewtocolNet/Extensions/SerialPortExtensions.cs b/MewtocolNet/Extensions/SerialPortExtensions.cs new file mode 100644 index 0000000..573f608 --- /dev/null +++ b/MewtocolNet/Extensions/SerialPortExtensions.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.IO.Ports; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolNet { + + internal static class SerialPortExtensions { + + public async static Task WriteAsync (this SerialPort serialPort, byte[] buffer, int offset, int count) { + + await serialPort.BaseStream.WriteAsync(buffer, 0, buffer.Length); + + } + + public async static Task ReadAsync (this SerialPort serialPort, byte[] buffer, int offset, int count) { + var bytesToRead = count; + var temp = new byte[count]; + + while (bytesToRead > 0) { + var readBytes = await serialPort.BaseStream.ReadAsync(temp, 0, bytesToRead); + Array.Copy(temp, 0, buffer, offset + count - bytesToRead, readBytes); + bytesToRead -= readBytes; + } + } + + public async static Task ReadAsync (this SerialPort serialPort, int count) { + var buffer = new byte[count]; + await serialPort.ReadAsync(buffer, 0, count); + return buffer; + } + + } + +} diff --git a/MewtocolNet/Queue/SerialQueue.cs b/MewtocolNet/Helpers/AsyncQueue.cs similarity index 90% rename from MewtocolNet/Queue/SerialQueue.cs rename to MewtocolNet/Helpers/AsyncQueue.cs index efa71c6..79ad62d 100644 --- a/MewtocolNet/Queue/SerialQueue.cs +++ b/MewtocolNet/Helpers/AsyncQueue.cs @@ -1,15 +1,18 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; +using System.Linq; namespace MewtocolNet.Queue { - internal class SerialQueue { + internal class AsyncQueue { readonly object _locker = new object(); readonly WeakReference _lastTask = new WeakReference(null); internal Task Enqueue(Func> asyncFunction) { lock (_locker) { + Task lastTask; Task resultTask; @@ -22,6 +25,7 @@ internal Task Enqueue(Func> asyncFunction) { _lastTask.SetTarget(resultTask); return resultTask; + } } diff --git a/MewtocolNet/CodeDescriptions.cs b/MewtocolNet/Helpers/CodeDescriptions.cs similarity index 100% rename from MewtocolNet/CodeDescriptions.cs rename to MewtocolNet/Helpers/CodeDescriptions.cs diff --git a/MewtocolNet/MewtocolHelpers.cs b/MewtocolNet/Helpers/MewtocolHelpers.cs similarity index 100% rename from MewtocolNet/MewtocolHelpers.cs rename to MewtocolNet/Helpers/MewtocolHelpers.cs diff --git a/MewtocolNet/IPlc.cs b/MewtocolNet/IPlc.cs new file mode 100644 index 0000000..62c1c33 --- /dev/null +++ b/MewtocolNet/IPlc.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MewtocolNet { + + /// + /// Provides a interface for Panasonic PLCs + /// + public interface IPlc : IDisposable { + + /// + /// The current connection state of the interface + /// + bool IsConnected { get; } + + /// + /// The current transmission speed in bytes per second + /// + int BytesPerSecondUpstream { get; } + + /// + /// The current transmission speed in bytes per second + /// + int BytesPerSecondDownstream { get; } + + /// + /// Current poller cycle duration + /// + int PollerCycleDurationMs { get; } + + /// + /// Currently queued message count + /// + int QueuedMessages { get; } + + /// + /// The registered data registers of the PLC + /// + IEnumerable Registers { get; } + + /// + /// Generic information about the connected PLC + /// + PLCInfo PlcInfo { get; } + + /// + /// The station number of the PLC + /// + int StationNumber { get; } + + /// + /// The initial connection timeout in milliseconds + /// + int ConnectTimeout { get; set; } + + /// + /// Tries to establish a connection with the device asynchronously + /// + Task ConnectAsync(); + + /// + /// Disconnects the devive from its current connection + /// + void Disconnect(); + + /// + /// Calculates the checksum automatically and sends a command to the PLC then awaits results + /// + /// MEWTOCOL Formatted request string ex: %01#RT + /// Append the checksum and bcc automatically + /// Timout to wait for a response + /// Returns the result + Task SendCommandAsync(string _msg, bool withTerminator = true, int timeoutMs = -1); + + /// + /// Use this to await the first poll iteration after connecting, + /// This also completes if the initial connection fails + /// + Task AwaitFirstDataAsync(); + + /// + /// Runs a single poller cycle manually, + /// useful if you want to use a custom update frequency + /// + /// The number of inidvidual mewtocol commands sent + Task RunPollerCylceManual(); + + /// + /// Gets the connection info string + /// + string GetConnectionInfo(); + + } + +} diff --git a/MewtocolNet/IPlcEthernet.cs b/MewtocolNet/IPlcEthernet.cs new file mode 100644 index 0000000..f9032c1 --- /dev/null +++ b/MewtocolNet/IPlcEthernet.cs @@ -0,0 +1,33 @@ +namespace MewtocolNet { + + /// + /// Provides a interface for Panasonic PLCs over a ethernet connection + /// + public interface IPlcEthernet : IPlc { + + /// + /// The current IP of the PLC connection + /// + string IpAddress { get; } + + /// + /// The current port of the PLC connection + /// + int Port { get; } + + /// + /// Attaches a poller to the interface + /// + public IPlcEthernet WithPoller(); + + /// + /// Configures the serial interface + /// + /// IP adress of the PLC + /// Port of the PLC + /// Station Number of the PLC + void ConfigureConnection(string _ip, int _port = 9094, int _station = 1); + + } + +} diff --git a/MewtocolNet/IPlcSerial.cs b/MewtocolNet/IPlcSerial.cs new file mode 100644 index 0000000..09d2587 --- /dev/null +++ b/MewtocolNet/IPlcSerial.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.IO.Ports; +using System.Text; +using System.Threading.Tasks; + +namespace MewtocolNet { + + /// + /// Provides a interface for Panasonic PLCs over a serial port connection + /// + public interface IPlcSerial : IPlc { + + /// + /// Port name of the serial port that this device is configured for + /// + string PortName { get; } + + /// + /// The serial connection baud rate that this device is configured for + /// + int SerialBaudRate { get; } + + /// + /// The serial connection data bits + /// + int SerialDataBits { get; } + + /// + /// The serial connection parity + /// + Parity SerialParity { get; } + + /// + /// The serial connection stop bits + /// + StopBits SerialStopBits { get; } + + /// + /// Attaches a poller to the interface + /// + public IPlcSerial WithPoller(); + + /// + /// Sets up the connection settings for the device + /// + /// Port name of COM port + /// The serial connection baud rate + /// The serial connection data bits + /// The serial connection parity + /// The serial connection stop bits + /// The station number of the PLC + void ConfigureConnection(string _portName, int _baudRate = 19200, int _dataBits = 8, Parity _parity = Parity.Odd, StopBits _stopBits = StopBits.One, int _station = 1) + + } + +} diff --git a/MewtocolNet/InternalEnums/CommandState.cs b/MewtocolNet/InternalEnums/CommandState.cs new file mode 100644 index 0000000..c755904 --- /dev/null +++ b/MewtocolNet/InternalEnums/CommandState.cs @@ -0,0 +1,12 @@ +namespace MewtocolNet { + + internal enum CommandState { + + Initial, + LineFeed, + RequestedNextFrame, + Complete + + } + +} \ No newline at end of file diff --git a/MewtocolNet/Logging/Logger.cs b/MewtocolNet/Logging/Logger.cs index 53e9c01..7e5ae3b 100644 --- a/MewtocolNet/Logging/Logger.cs +++ b/MewtocolNet/Logging/Logger.cs @@ -31,7 +31,7 @@ internal static void Log(string message, LogLevel loglevel, MewtocolInterface se if (sender == null) { LogInvoked?.Invoke(DateTime.Now, loglevel, message); } else { - LogInvoked?.Invoke(DateTime.Now, loglevel, $"[{sender.GetConnectionPortInfo()}] {message}"); + LogInvoked?.Invoke(DateTime.Now, loglevel, $"[{sender.GetConnectionInfo()}] {message}"); } } diff --git a/MewtocolNet/Mewtocol.cs b/MewtocolNet/Mewtocol.cs new file mode 100644 index 0000000..7515cbf --- /dev/null +++ b/MewtocolNet/Mewtocol.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.IO.Ports; +using System.Text; + +namespace MewtocolNet { + + /// + /// Builder helper for mewtocol interfaces + /// + public static class Mewtocol { + + /// + /// Builds a ethernet based Mewtocol Interface + /// + /// + /// + /// + /// + public static IPlcEthernet Ethernet (string _ip, int _port = 9094, int _station = 1) { + + var instance = new MewtocolInterfaceTcp(); + instance.ConfigureConnection(_ip, _port, _station); + return instance; + + } + + /// + /// Builds a serial port based Mewtocol Interface + /// + /// + /// + /// + /// + /// + /// + public static IPlcSerial Serial (string _portName, BaudRate _baudRate = BaudRate._19200, DataBits _dataBits = DataBits.Eight, Parity _parity = Parity.Odd, StopBits _stopBits = StopBits.One, int _station = 1) { + + var instance = new MewtocolInterfaceSerial(); + instance.ConfigureConnection(_portName, (int)_baudRate, (int)_dataBits, _parity, _stopBits, _station); + return instance; + + } + + /// + /// Builds a serial mewtocol interface that finds the correct settings for the given port name automatically + /// + /// + /// + /// + public static IPlcSerial SerialAuto (string _portName, int _station = 1) { + + var instance = new MewtocolInterfaceSerial(); + instance.ConfigureConnection(_portName, _station); + instance.ConfigureConnectionAuto(); + return instance; + + } + + } + +} diff --git a/MewtocolNet/MewtocolInterface.cs b/MewtocolNet/MewtocolInterface.cs index 793720c..340924f 100644 --- a/MewtocolNet/MewtocolInterface.cs +++ b/MewtocolNet/MewtocolInterface.cs @@ -1,18 +1,12 @@ -using MewtocolNet.Exceptions; -using MewtocolNet.Logging; +using MewtocolNet.Logging; using MewtocolNet.Queue; -using MewtocolNet.RegisterAttributes; using MewtocolNet.Registers; using System; -using System.Collections; using System.Collections.Generic; using System.ComponentModel; -using System.ComponentModel.Design; using System.Diagnostics; using System.IO; using System.Linq; -using System.Net; -using System.Net.Sockets; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; @@ -20,177 +14,119 @@ namespace MewtocolNet { - /// - /// The PLC com interface class - /// - public partial class MewtocolInterface : INotifyPropertyChanged, IDisposable { + public partial class MewtocolInterface : IPlc, INotifyPropertyChanged, IDisposable { - /// - /// Gets triggered when the PLC connection was established - /// + #region Private fields + + private protected Stream stream; + + private int tcpMessagesSentThisCycle = 0; + private int pollerCycleDurationMs; + private volatile int queuedMessages; + private bool isConnected; + private PLCInfo plcInfo; + private protected int stationNumber; + + private protected int bytesTotalCountedUpstream = 0; + private protected int bytesTotalCountedDownstream = 0; + private protected int cycleTimeMs = 25; + private protected int bytesPerSecondUpstream = 0; + private protected int bytesPerSecondDownstream = 0; + + private protected AsyncQueue queue = new AsyncQueue(); + private protected int RecBufferSize = 128; + private protected Stopwatch speedStopwatchUpstr; + private protected Stopwatch speedStopwatchDownstr; + private protected Task firstPollTask = new Task(() => { }); + + #endregion + + #region Internal fields + + internal event Action PolledCycle; + internal volatile bool pollerTaskStopped = true; + internal volatile bool pollerFirstCycle; + internal bool usePoller = false; + + internal List RegistersUnderlying { get; private set; } = new List(); + internal IEnumerable RegistersInternal => RegistersUnderlying.Cast(); + + #endregion + + #region Public Read Only Properties / Fields + + /// public event Action Connected; - /// - /// Gets triggered when the PLC connection was closed or lost - /// + /// public event Action Disconnected; - /// - /// Gets triggered when a registered data register changes its value - /// + /// public event Action RegisterChanged; - /// - /// Gets triggered when a property of the interface changes - /// + /// public event PropertyChangedEventHandler PropertyChanged; - private int connectTimeout = 3000; - /// - /// The initial connection timeout in milliseconds - /// - public int ConnectTimeout { - get { return connectTimeout; } - set { connectTimeout = value; } - } - - private volatile int queuedMessages; - /// - /// Currently queued Messages - /// - public int QueuedMessages { - get => queuedMessages; - } + /// + public bool Disposed { get; private set; } - /// - /// The host ip endpoint, leave it null to use an automatic interface - /// - public IPEndPoint HostEndpoint { get; set; } + /// + public int QueuedMessages => queuedMessages; - private bool isConnected; - /// - /// The current connection state of the interface - /// + /// public bool IsConnected { get => isConnected; - private set { + private protected set { isConnected = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsConnected))); + OnPropChange(); } } - private bool disposed; - /// - /// True if the current interface was disposed - /// - public bool Disposed { - get { return disposed; } - private set { disposed = value; } - } - - - private PLCInfo plcInfo; - /// - /// Generic information about the connected PLC - /// + /// public PLCInfo PlcInfo { get => plcInfo; private set { plcInfo = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PlcInfo))); + OnPropChange(); } } - /// - /// The registered data registers of the PLC - /// - internal List RegistersUnderlying { get; private set; } = new List(); - + /// public IEnumerable Registers => RegistersUnderlying.Cast(); - internal IEnumerable RegistersInternal => RegistersUnderlying.Cast(); - - private string ip; - private int port; - private int stationNumber; - private int cycleTimeMs = 25; - - private int bytesTotalCountedUpstream = 0; - private int bytesTotalCountedDownstream = 0; - - /// - /// The current IP of the PLC connection - /// - public string IpAddress => ip; - /// - /// The current port of the PLC connection - /// - public int Port => port; - /// - /// The station number of the PLC - /// + /// public int StationNumber => stationNumber; - /// - /// The duration of the last message cycle - /// - public int CycleTimeMs { - get { return cycleTimeMs; } - private set { - cycleTimeMs = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CycleTimeMs))); - } - } - - private int bytesPerSecondUpstream = 0; - /// - /// The current transmission speed in bytes per second - /// + /// public int BytesPerSecondUpstream { get { return bytesPerSecondUpstream; } - private set { + private protected set { bytesPerSecondUpstream = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BytesPerSecondUpstream))); + OnPropChange(); } } - private int bytesPerSecondDownstream = 0; - /// - /// The current transmission speed in bytes per second - /// + /// public int BytesPerSecondDownstream { get { return bytesPerSecondDownstream; } - private set { + private protected set { bytesPerSecondDownstream = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BytesPerSecondDownstream))); + OnPropChange(); } } - internal NetworkStream stream; - internal TcpClient client; - internal readonly SerialQueue queue = new SerialQueue(); - private int RecBufferSize = 128; - internal int SendExceptionsInRow = 0; - internal bool ImportantTaskRunning = false; + #endregion - private Stopwatch speedStopwatchUpstr; - private Stopwatch speedStopwatchDownstr; + #region Public read/write Properties / Fields - private Task firstPollTask = new Task(() => { }); + /// + public int ConnectTimeout { get; set; } = 3000; - #region Initialization + #endregion - /// - /// Builds a new Interfacer for a PLC - /// - /// IP adress of the PLC - /// Port of the PLC - /// Station Number of the PLC - public MewtocolInterface(string _ip, int _port = 9094, int _station = 1) { + #region Methods - ip = _ip; - port = _port; - stationNumber = _station; + private protected MewtocolInterface () { Connected += MewtocolInterface_Connected; @@ -215,211 +151,61 @@ void MewtocolInterface_Connected(PLCInfo obj) { }; } + + /// + public virtual Task ConnectAsync () => throw new NotImplementedException(); - #endregion - - #region Setup - - /// - /// Trys to connect to the PLC by the IP given in the constructor - /// - /// - /// Gets called when a connection with a PLC was established - /// - /// If is used it waits for the first data receive cycle to complete - /// - /// Gets called when an error or timeout during connection occurs - /// - public async Task ConnectAsync(Action OnConnected = null, Action OnFailed = null) { - - firstPollTask = new Task(() => { }); - - Logger.Log("Connecting to PLC...", LogLevel.Info, this); - - var plcinf = await GetPLCInfoAsync(); - - if (plcinf != null) { - - Logger.Log("Connected", LogLevel.Info, this); - Logger.Log($"\n\n{plcinf.ToString()}\n\n", LogLevel.Verbose, this); - - Connected?.Invoke(plcinf); - - if (!usePoller) { - if (OnConnected != null) OnConnected(plcinf); - firstPollTask.RunSynchronously(); - return this; - } - - PolledCycle += OnPollCycleDone; - void OnPollCycleDone() { - - if (OnConnected != null) OnConnected(plcinf); - firstPollTask.RunSynchronously(); - PolledCycle -= OnPollCycleDone; - - } - - } else { - - if (OnFailed != null) { - OnFailed(); - Disconnected?.Invoke(); - firstPollTask.RunSynchronously(); - Logger.Log("Initial connection failed", LogLevel.Info, this); - } - - } - - return this; - - } - - /// - /// Use this to await the first poll iteration after connecting, - /// This also completes if the initial connection fails - /// - public async Task AwaitFirstDataAsync () => await firstPollTask; - - /// - /// Changes the connections parameters of the PLC, only applyable when the connection is offline - /// - /// Ip adress - /// Port number - /// Station number - public void ChangeConnectionSettings(string _ip, int _port, int _station = 1) { - - if (IsConnected) - throw new Exception("Cannot change the connection settings while the PLC is connected"); - - ip = _ip; - port = _port; - stationNumber = _station; + /// + public async Task AwaitFirstDataAsync() => await firstPollTask; - } - - /// - /// Closes the connection all cyclic polling - /// + /// public void Disconnect() { - if (!IsConnected) - return; - - OnMajorSocketExceptionWhileConnected(); - - } + if (!IsConnected) return; - /// - /// Attaches a poller to the interface that continously - /// polls the registered data registers and writes the values to them - /// - public MewtocolInterface WithPoller() { + pollCycleTask.Wait(); - usePoller = true; - return this; + OnMajorSocketExceptionWhileConnected(); } - #endregion - - #region TCP connection state handling - - private async Task ConnectTCP() { - - if (!IPAddress.TryParse(ip, out var targetIP)) { - throw new ArgumentException("The IP adress of the PLC was no valid format"); - } - - try { - - if (HostEndpoint != null) { - - client = new TcpClient(HostEndpoint) { - ReceiveBufferSize = RecBufferSize, - NoDelay = false, - }; - var ep = (IPEndPoint)client.Client.LocalEndPoint; - Logger.Log($"Connecting [MAN] endpoint: {ep.Address}:{ep.Port}", LogLevel.Verbose, this); - - } else { - - client = new TcpClient() { - ReceiveBufferSize = RecBufferSize, - NoDelay = false, - ExclusiveAddressUse = true, - }; - - } - - var result = client.BeginConnect(targetIP, port, null, null); - var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(ConnectTimeout)); - - if (!success || !client.Connected) { - OnMajorSocketExceptionWhileConnecting(); - return; - } - - if (HostEndpoint == null) { - var ep = (IPEndPoint)client.Client.LocalEndPoint; - Logger.Log($"Connecting [AUTO] endpoint: {ep.Address.MapToIPv4()}:{ep.Port}", LogLevel.Verbose, this); - } - - stream = client.GetStream(); - stream.ReadTimeout = 1000; - - await Task.CompletedTask; - - } catch (SocketException) { - - OnMajorSocketExceptionWhileConnecting(); + /// + public void Dispose() { - } + if (Disposed) return; + Disconnect(); + //GC.SuppressFinalize(this); + Disposed = true; } - #endregion - - #region Low level command handling + /// + public virtual string GetConnectionInfo() => throw new NotImplementedException(); - /// - /// Calculates the checksum automatically and sends a command to the PLC then awaits results - /// - /// MEWTOCOL Formatted request string ex: %01#RT - /// Append the checksum and bcc automatically - /// Returns the result - public async Task SendCommandAsync(string _msg, bool withTerminator = true) { + /// + public async Task SendCommandAsync(string _msg, bool withTerminator = true, int timeoutMs = -1) { //send request queuedMessages++; - var tempResponse = await queue.Enqueue(() => SendFrameAsync(_msg, withTerminator, withTerminator)); + + var tempResponse = queue.Enqueue(async () => await SendFrameAsync(_msg, withTerminator, withTerminator)); + + if (await Task.WhenAny(tempResponse, Task.Delay(timeoutMs)) != tempResponse) { + // timeout logic + return new MewtocolFrameResponse(403, "Timed out"); + } tcpMessagesSentThisCycle++; queuedMessages--; - return tempResponse; + return tempResponse.Result; } - private async Task SendFrameAsync (string frame, bool useBcc = true, bool useCr = true) { + private protected async Task SendFrameAsync (string frame, bool useBcc = true, bool useCr = true) { try { - //stop time - if (speedStopwatchUpstr == null) { - speedStopwatchUpstr = Stopwatch.StartNew(); - } - - if (speedStopwatchUpstr.Elapsed.TotalSeconds >= 1) { - speedStopwatchUpstr.Restart(); - bytesTotalCountedUpstream = 0; - } - - const char CR = '\r'; - const char DELIMITER = '&'; - - if (client == null || !client.Connected) await ConnectTCP(); - if (useBcc) frame = $"{frame.BuildBCCFrame()}"; @@ -428,119 +214,128 @@ private async Task SendFrameAsync (string frame, bool use //write inital command byte[] writeBuffer = Encoding.UTF8.GetBytes(frame); - await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length); + stream.Write(writeBuffer, 0, writeBuffer.Length); - //calc upstream speed - bytesTotalCountedUpstream += writeBuffer.Length; + Logger.Log($"[---------CMD START--------]", LogLevel.Critical, this); + Logger.Log($"--> OUT MSG: {frame.Replace("\r", "(CR)")}", LogLevel.Critical, this); - var perSecUpstream = (double)((bytesTotalCountedUpstream / speedStopwatchUpstr.Elapsed.TotalMilliseconds) * 1000); - if (perSecUpstream <= 10000) - BytesPerSecondUpstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero); + var readResult = await ReadCommandAsync(); + //did not receive bytes but no errors, the com port was not configured right + if (readResult.Item1.Length == 0) { - Logger.Log($"[---------CMD START--------]", LogLevel.Critical, this); - Logger.Log($"--> OUT MSG: {frame.Replace("\r", "(CR)")}", LogLevel.Critical, this); + return new MewtocolFrameResponse(402, "Receive buffer was empty"); - //read - List totalResponse = new List(); - byte[] responseBuffer = new byte[512]; + } + + //build final result + string resString = Encoding.UTF8.GetString(readResult.Item1); - bool wasMultiFramedResponse = false; - CommandState cmdState = CommandState.Intial; + //check if the message had errors + //error response + var gotErrorcode = CheckForErrorMsg(resString); + if (gotErrorcode != 0) { + return new MewtocolFrameResponse(gotErrorcode); + } - //read until command complete - while (cmdState != CommandState.Complete) { + //was multiframed response + if (readResult.Item2) { - //time measuring - if (speedStopwatchDownstr == null) { - speedStopwatchDownstr = Stopwatch.StartNew(); - } + var split = resString.Split('&'); + + for (int j = 0; j < split.Length; j++) { + + split[j] = split[j].Replace("\r", ""); + split[j] = split[j].Substring(0, split[j].Length - 2); + if (j > 0) split[j] = split[j].Replace($"%{GetStationNumber()}", ""); - if (speedStopwatchDownstr.Elapsed.TotalSeconds >= 1) { - speedStopwatchDownstr.Restart(); - bytesTotalCountedDownstream = 0; } - responseBuffer = new byte[128]; + resString = string.Join("", split); - await stream.ReadAsync(responseBuffer, 0, responseBuffer.Length); + } - bool terminatorReceived = responseBuffer.Any(x => x == (byte)CR); - var delimiterTerminatorIdx = responseBuffer.SearchBytePattern(new byte[] { (byte)DELIMITER, (byte)CR }); + Logger.Log($"<-- IN MSG: {resString.Replace("\r", "(CR)")}", LogLevel.Critical, this); + Logger.Log($"Total bytes parsed: {resString.Length}", LogLevel.Critical, this); + Logger.Log($"[---------CMD END----------]", LogLevel.Critical, this); - if (terminatorReceived && delimiterTerminatorIdx == -1) { - cmdState = CommandState.Complete; - } else if (delimiterTerminatorIdx != -1) { - cmdState = CommandState.RequestedNextFrame; - } else { - cmdState = CommandState.LineFeed; - } + return new MewtocolFrameResponse(resString); - //log message parts - var tempMsg = Encoding.UTF8.GetString(responseBuffer).Replace("\r", "(CR)"); - Logger.Log($">> IN PART: {tempMsg}, Command state: {cmdState}", LogLevel.Critical, this); + } catch (Exception ex) { - //error response - int errorCode = CheckForErrorMsg(tempMsg); - if (errorCode != 0) return new MewtocolFrameResponse(errorCode); + return new MewtocolFrameResponse(400, ex.Message.ToString(System.Globalization.CultureInfo.InvariantCulture)); - //add complete response to collector without empty bytes - totalResponse.AddRange(responseBuffer.Where(x => x != (byte)0x0)); + } - //request next part of the command if the delimiter was received - if (cmdState == CommandState.RequestedNextFrame) { + } - Logger.Log($"Requesting next frame...", LogLevel.Critical, this); + private protected async Task<(byte[], bool)> ReadCommandAsync () { - wasMultiFramedResponse = true; - writeBuffer = Encoding.UTF8.GetBytes("%01**&\r"); - await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length); + //read total + List totalResponse = new List(); + bool wasMultiFramedResponse = false; - } + try { - } + bool needsRead = false; - //build final result - string resString = Encoding.UTF8.GetString(totalResponse.ToArray()); + do { - if (wasMultiFramedResponse) { + byte[] buffer = new byte[128]; + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); - var split = resString.Split('&'); + byte[] received = new byte[bytesRead]; + Buffer.BlockCopy(buffer, 0, received, 0, bytesRead); - for (int j = 0; j < split.Length; j++) { + var commandRes = ParseBufferFrame(received); + needsRead = commandRes == CommandState.LineFeed || commandRes == CommandState.RequestedNextFrame; - split[j] = split[j].Replace("\r", ""); - split[j] = split[j].Substring(0, split[j].Length - 2); - if (j > 0) split[j] = split[j].Replace($"%{GetStationNumber()}", ""); + var tempMsg = Encoding.UTF8.GetString(received).Replace("\r", "(CR)"); + Logger.Log($">> IN PART: {tempMsg}, Command state: {commandRes}", LogLevel.Critical, this); - } + //add complete response to collector without empty bytes + totalResponse.AddRange(received.Where(x => x != (byte)0x0)); - resString = string.Join("", split); + if (commandRes == CommandState.RequestedNextFrame) { - } + //request next frame + var writeBuffer = Encoding.UTF8.GetBytes("%01**&\r"); + await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length); + wasMultiFramedResponse = true; + + } - bytesTotalCountedDownstream += Encoding.ASCII.GetByteCount(resString); + } while (needsRead); - var perSecDownstream = (double)((bytesTotalCountedDownstream / speedStopwatchDownstr.Elapsed.TotalMilliseconds) * 1000); + } catch (OperationCanceledException) { } - if (perSecUpstream <= 10000) - BytesPerSecondDownstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero); + return (totalResponse.ToArray(), wasMultiFramedResponse); - Logger.Log($"<-- IN MSG: {resString.Replace("\r", "(CR)")}", LogLevel.Critical, this); - Logger.Log($"Total bytes parsed: {resString.Length}", LogLevel.Critical, this); - Logger.Log($"[---------CMD END----------]", LogLevel.Critical, this); + } - return new MewtocolFrameResponse(resString); + private protected CommandState ParseBufferFrame(byte[] received) { - } catch (Exception ex) { + const char CR = '\r'; + const char DELIMITER = '&'; - return new MewtocolFrameResponse(400, ex.Message.ToString(System.Globalization.CultureInfo.InvariantCulture)); + CommandState cmdState; + + bool terminatorReceived = received.Any(x => x == (byte)CR); + var delimiterTerminatorIdx = received.ToArray().SearchBytePattern(new byte[] { (byte)DELIMITER, (byte)CR }); + if (terminatorReceived && delimiterTerminatorIdx == -1) { + cmdState = CommandState.Complete; + } else if (delimiterTerminatorIdx != -1) { + cmdState = CommandState.RequestedNextFrame; + } else { + cmdState = CommandState.LineFeed; } + return cmdState; + } - private int CheckForErrorMsg (string msg) { + private protected int CheckForErrorMsg (string msg) { //error catching Regex errorcheck = new Regex(@"\%[0-9]{2}\!([0-9]{2})", RegexOptions.IgnoreCase); @@ -557,11 +352,7 @@ private int CheckForErrorMsg (string msg) { } - #endregion - - #region Disposing - - private void OnMajorSocketExceptionWhileConnecting() { + private protected void OnMajorSocketExceptionWhileConnecting() { if (IsConnected) { @@ -572,7 +363,7 @@ private void OnMajorSocketExceptionWhileConnecting() { } - private void OnMajorSocketExceptionWhileConnected() { + private protected void OnMajorSocketExceptionWhileConnected() { if (IsConnected) { @@ -583,43 +374,44 @@ private void OnMajorSocketExceptionWhileConnected() { } + private protected virtual void OnConnected (PLCInfo plcinf) { - /// - /// Disposes the current interface and clears all its members - /// - public void Dispose() { + Logger.Log("Connected", LogLevel.Info, this); + Logger.Log($"\n\n{plcinf.ToString()}\n\n", LogLevel.Verbose, this); - if (Disposed) return; + IsConnected = true; - Disconnect(); + Connected?.Invoke(plcinf); - //GC.SuppressFinalize(this); + if (!usePoller) { + firstPollTask.RunSynchronously(); + } - Disposed = true; + PolledCycle += OnPollCycleDone; + void OnPollCycleDone() { - } + firstPollTask.RunSynchronously(); + PolledCycle -= OnPollCycleDone; - private void OnDisconnect () { + } - if (IsConnected) { + } - BytesPerSecondDownstream = 0; - BytesPerSecondUpstream = 0; - CycleTimeMs = 0; + private protected virtual void OnDisconnect () { - IsConnected = false; - ClearRegisterVals(); + BytesPerSecondDownstream = 0; + BytesPerSecondUpstream = 0; + CycleTimeMs = 0; - Disconnected?.Invoke(); - KillPoller(); - client.Close(); + IsConnected = false; + ClearRegisterVals(); - } + Disconnected?.Invoke(); + KillPoller(); } - - private void ClearRegisterVals() { + private protected void ClearRegisterVals() { for (int i = 0; i < RegistersUnderlying.Count; i++) { @@ -630,28 +422,7 @@ private void ClearRegisterVals() { } - #endregion - - #region Accessing Info - - /// - /// Gets the connection info string - /// - public string GetConnectionPortInfo() { - - return $"{IpAddress}:{Port}"; - - } - - #endregion - - #region Property change evnts - - /// - /// Triggers a property changed event - /// - /// Name of the property to trigger for - private void OnPropChange ([CallerMemberName]string propertyName = null) { + private protected void OnPropChange([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); @@ -661,4 +432,4 @@ private void OnPropChange ([CallerMemberName]string propertyName = null) { } -} \ No newline at end of file +} diff --git a/MewtocolNet/DynamicInterface.cs b/MewtocolNet/MewtocolInterfaceRegisterHandling.cs similarity index 97% rename from MewtocolNet/DynamicInterface.cs rename to MewtocolNet/MewtocolInterfaceRegisterHandling.cs index 2344bba..f86a029 100644 --- a/MewtocolNet/DynamicInterface.cs +++ b/MewtocolNet/MewtocolInterfaceRegisterHandling.cs @@ -1,6 +1,7 @@ using MewtocolNet.Exceptions; using MewtocolNet.Logging; using MewtocolNet.RegisterAttributes; +using MewtocolNet.RegisterBuilding; using MewtocolNet.Registers; using System; using System.Collections; @@ -12,24 +13,14 @@ using System.Threading; using System.Threading.Tasks; -namespace MewtocolNet -{ +namespace MewtocolNet { /// /// The PLC com interface class /// public partial class MewtocolInterface { - internal event Action PolledCycle; - - internal volatile bool pollerTaskStopped = true; - internal volatile bool pollerFirstCycle; - - internal bool usePoller = false; - - private int tcpMessagesSentThisCycle = 0; - - private int pollerCycleDurationMs; + internal Task pollCycleTask; /// /// True if the poller is actvice (can be paused) @@ -87,7 +78,8 @@ public async Task RunPollerCylceManual () { tcpMessagesSentThisCycle = 0; - await OnMultiFrameCycle(); + pollCycleTask = OnMultiFrameCycle(); + await pollCycleTask; return tcpMessagesSentThisCycle; @@ -104,9 +96,10 @@ internal async Task Poll () { tcpMessagesSentThisCycle = 0; - await OnMultiFrameCycle(); + pollCycleTask = OnMultiFrameCycle(); + await pollCycleTask; - if(!IsConnected) { + if (!IsConnected) { pollerTaskStopped = true; return; } diff --git a/MewtocolNet/MewtocolInterfaceRequests.cs b/MewtocolNet/MewtocolInterfaceRequests.cs index 54137bd..7b35869 100644 --- a/MewtocolNet/MewtocolInterfaceRequests.cs +++ b/MewtocolNet/MewtocolInterfaceRequests.cs @@ -19,8 +19,9 @@ public partial class MewtocolInterface { /// Gets generic information about the PLC /// /// A PLCInfo class - public async Task GetPLCInfoAsync() { - var resu = await SendCommandAsync("%01#RT"); + public async Task GetPLCInfoAsync(int timeout = -1) { + + var resu = await SendCommandAsync("%01#RT", true, timeout); 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); diff --git a/MewtocolNet/MewtocolInterfaceSerial.cs b/MewtocolNet/MewtocolInterfaceSerial.cs new file mode 100644 index 0000000..afe148a --- /dev/null +++ b/MewtocolNet/MewtocolInterfaceSerial.cs @@ -0,0 +1,260 @@ +using MewtocolNet.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Ports; +using System.Linq; +using System.Net.Sockets; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using System.Threading; + +namespace MewtocolNet { + + public class MewtocolInterfaceSerial : MewtocolInterface, IPlcSerial { + + private bool autoSerial; + + //serial config + public string PortName { get; private set; } + public int SerialBaudRate { get; private set; } + public int SerialDataBits { get; private set; } + public Parity SerialParity { get; private set; } + public StopBits SerialStopBits { get; private set; } + + //Serial + internal SerialPort serialClient; + + internal MewtocolInterfaceSerial () : base() { } + + + /// + public IPlcSerial WithPoller () { + + usePoller = true; + return this; + + } + + /// + public override string GetConnectionInfo() { + + StringBuilder sb = new StringBuilder(); + + sb.Append($"{PortName}, "); + sb.Append($"{SerialBaudRate}, "); + sb.Append($"{SerialDataBits} "); + + sb.Append($"{SerialParity.ToString().Substring(0, 1)} "); + + switch (SerialStopBits) { + case StopBits.None: + sb.Append("0"); + break; + case StopBits.One: + sb.Append("1"); + break; + case StopBits.Two: + sb.Append("2"); + break; + case StopBits.OnePointFive: + sb.Append("1.5"); + break; + } + + return sb.ToString(); + + } + + /// + public void ConfigureConnection (string _portName, int _baudRate = 19200, int _dataBits = 8, Parity _parity = Parity.Odd, StopBits _stopBits = StopBits.One, int _station = 1) { + + PortName = _portName; + SerialBaudRate = _baudRate; + SerialDataBits = _dataBits; + SerialParity = _parity; + SerialStopBits = _stopBits; + stationNumber = _station; + + OnSerialPropsChanged(); + Disconnect(); + + } + + internal void ConfigureConnectionAuto () { + + autoSerial = true; + + } + + /// + public override async Task ConnectAsync () { + + try { + + PLCInfo gotInfo = null; + + if(autoSerial) { + + gotInfo = await TryConnectAsyncMulti(); + + } else { + + gotInfo = await TryConnectAsyncSingle(PortName, SerialBaudRate, SerialDataBits, SerialParity, SerialStopBits); + + } + + if(gotInfo != null) { + + OnConnected(gotInfo); + + } else { + + Logger.Log("Initial connection failed", LogLevel.Info, this); + OnMajorSocketExceptionWhileConnecting(); + + } + + await Task.CompletedTask; + + } catch (SocketException) { + + OnMajorSocketExceptionWhileConnecting(); + + } + + } + + private async Task TryConnectAsyncMulti () { + + var baudRates = Enum.GetValues(typeof(BaudRate)).Cast(); + + //ordered by most commonly used + baudRates = new List { + //most common 3 + BaudRate._19200, + BaudRate._115200, + BaudRate._9600, + //others + BaudRate._1200, + BaudRate._2400, + BaudRate._4800, + BaudRate._38400, + BaudRate._57600, + BaudRate._230400, + }; + + var dataBits = Enum.GetValues(typeof(DataBits)).Cast(); + var parities = new List() { Parity.None, Parity.Odd, Parity.Even, Parity.Mark }; + var stopBits = new List { StopBits.One, StopBits.Two }; + + foreach (var baud in baudRates) { + + foreach (var databit in dataBits) { + + foreach (var parity in parities) { + + foreach (var stopBit in stopBits) { + + var res = await TryConnectAsyncSingle(PortName, (int)baud, (int)databit, parity, stopBit); + if(res != null) return res; + + } + + } + + } + + } + + return null; + + } + + private async Task TryConnectAsyncSingle (string port, int baud, int dbits, Parity par, StopBits sbits) { + + try { + + serialClient = new SerialPort() { + PortName = port, + BaudRate = baud, + DataBits = dbits, + Parity = par, + StopBits = sbits, + ReadTimeout = 100, + Handshake = Handshake.None + }; + + PortName = port; + SerialBaudRate = baud; + SerialDataBits = dbits; + SerialParity = par; + SerialStopBits = sbits; + OnSerialPropsChanged(); + + serialClient.Open(); + + if (!serialClient.IsOpen) { + + Logger.Log($"Failed to open [SERIAL]: {GetConnectionInfo()}", LogLevel.Verbose, this); + return null; + + } + + stream = serialClient.BaseStream; + + Logger.Log($"Opened [SERIAL]: {GetConnectionInfo()}", LogLevel.Verbose, this); + + var plcinf = await GetPLCInfoAsync(100); + + if (plcinf == null) CloseClient(); + + return plcinf; + + } catch (UnauthorizedAccessException) { + + Logger.Log($"The port {serialClient.PortName} is currently in use. Close all accessing applications first", LogLevel.Error, this); + return null; + + } + + } + + private void CloseClient () { + + if(serialClient.IsOpen) { + + serialClient.Close(); + Logger.Log($"Closed [SERIAL]", LogLevel.Verbose, this); + + } + + } + + private protected override void OnDisconnect() { + + if (IsConnected) { + + base.OnDisconnect(); + + CloseClient(); + + } + + } + + private void OnSerialPropsChanged () { + + OnPropChange(nameof(PortName)); + OnPropChange(nameof(SerialBaudRate)); + OnPropChange(nameof(SerialDataBits)); + OnPropChange(nameof(SerialParity)); + OnPropChange(nameof(SerialStopBits)); + + } + + } + +} diff --git a/MewtocolNet/MewtocolInterfaceTcp.cs b/MewtocolNet/MewtocolInterfaceTcp.cs new file mode 100644 index 0000000..9a4f60e --- /dev/null +++ b/MewtocolNet/MewtocolInterfaceTcp.cs @@ -0,0 +1,164 @@ +using MewtocolNet.Exceptions; +using MewtocolNet.Logging; +using MewtocolNet.Queue; +using MewtocolNet.RegisterAttributes; +using MewtocolNet.Registers; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.Design; +using System.Diagnostics; +using System.IO; +using System.IO.Ports; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace MewtocolNet { + + /// + /// The PLC com interface class + /// + public class MewtocolInterfaceTcp : MewtocolInterface, IPlcEthernet { + + /// + /// The host ip endpoint, leave it null to use an automatic interface + /// + public IPEndPoint HostEndpoint { get; set; } + + //TCP + internal TcpClient client; + + //tcp/ip config + private string ip; + private int port; + + /// + public string IpAddress => ip; + + /// + public int Port => port; + + internal MewtocolInterfaceTcp () : base() { } + + /// + public IPlcEthernet WithPoller () { + + usePoller = true; + return this; + + } + + #region TCP connection state handling + + /// + public void ConfigureConnection (string _ip, int _port = 9094, int _station = 1) { + + ip = _ip; + port = _port; + stationNumber = _station; + + Disconnect(); + + } + + /// + public override async Task ConnectAsync () { + + if (!IPAddress.TryParse(ip, out var targetIP)) { + throw new ArgumentException("The IP adress of the PLC was no valid format"); + } + + try { + + if (HostEndpoint != null) { + + client = new TcpClient(HostEndpoint) { + ReceiveBufferSize = RecBufferSize, + NoDelay = false, + }; + var ep = (IPEndPoint)client.Client.LocalEndPoint; + Logger.Log($"Connecting [MAN] endpoint: {ep.Address}:{ep.Port}", LogLevel.Verbose, this); + + } else { + + client = new TcpClient() { + ReceiveBufferSize = RecBufferSize, + NoDelay = false, + ExclusiveAddressUse = true, + }; + + } + + var result = client.BeginConnect(targetIP, port, null, null); + var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(ConnectTimeout)); + + if (!success || !client.Connected) { + OnMajorSocketExceptionWhileConnecting(); + return; + } + + if (HostEndpoint == null) { + var ep = (IPEndPoint)client.Client.LocalEndPoint; + Logger.Log($"Connecting [AUTO] endpoint: {ep.Address.MapToIPv4()}:{ep.Port}", LogLevel.Verbose, this); + } + + //get the stream + stream = client.GetStream(); + stream.ReadTimeout = 1000; + + //get plc info + var plcinf = await GetPLCInfoAsync(); + + if (plcinf != null) { + + OnConnected(plcinf); + + } else { + + Logger.Log("Initial connection failed", LogLevel.Info, this); + OnDisconnect(); + + } + + await Task.CompletedTask; + + } catch (SocketException) { + + OnMajorSocketExceptionWhileConnecting(); + + } + + } + + /// + /// Gets the connection info string + /// + public override string GetConnectionInfo() { + + return $"{IpAddress}:{Port}"; + + } + + private protected override void OnDisconnect() { + + if (IsConnected) { + + base.OnDisconnect(); + + client.Close(); + + } + + } + + #endregion + + } + +} \ No newline at end of file diff --git a/MewtocolNet/MewtocolNet.csproj b/MewtocolNet/MewtocolNet.csproj index 8c6901f..a172606 100644 --- a/MewtocolNet/MewtocolNet.csproj +++ b/MewtocolNet/MewtocolNet.csproj @@ -23,4 +23,7 @@ <_Parameter1>MewtocolTests + + + diff --git a/MewtocolNet/PLCMode.cs b/MewtocolNet/PLCMode.cs index 9079527..3662b2a 100644 --- a/MewtocolNet/PLCMode.cs +++ b/MewtocolNet/PLCMode.cs @@ -5,7 +5,7 @@ namespace MewtocolNet { /// /// All modes /// - public class PLCMode { + public struct PLCMode { /// /// PLC is running diff --git a/MewtocolNet/PublicEnums/BaudRate.cs b/MewtocolNet/PublicEnums/BaudRate.cs new file mode 100644 index 0000000..772eefd --- /dev/null +++ b/MewtocolNet/PublicEnums/BaudRate.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet { + + public enum BaudRate { + + _1200 = 1200, + _2400 = 2400, + _4800 = 4800, + _9600 = 9600, + _19200 = 19200, + _38400 = 38400, + _57600 = 57600, + _115200 = 115200, + _230400 = 230400, + + } + +} diff --git a/MewtocolNet/RegisterAttributes/BitCount.cs b/MewtocolNet/PublicEnums/BitCount.cs similarity index 85% rename from MewtocolNet/RegisterAttributes/BitCount.cs rename to MewtocolNet/PublicEnums/BitCount.cs index 08228b1..fb0478f 100644 --- a/MewtocolNet/RegisterAttributes/BitCount.cs +++ b/MewtocolNet/PublicEnums/BitCount.cs @@ -1,4 +1,5 @@ -namespace MewtocolNet.RegisterAttributes { +namespace MewtocolNet { + /// /// The size of the bitwise register /// diff --git a/MewtocolNet/PLCEnums/CpuType.cs b/MewtocolNet/PublicEnums/CpuType.cs similarity index 100% rename from MewtocolNet/PLCEnums/CpuType.cs rename to MewtocolNet/PublicEnums/CpuType.cs diff --git a/MewtocolNet/PublicEnums/DataBits.cs b/MewtocolNet/PublicEnums/DataBits.cs new file mode 100644 index 0000000..f5f41fd --- /dev/null +++ b/MewtocolNet/PublicEnums/DataBits.cs @@ -0,0 +1,9 @@ +namespace MewtocolNet { + public enum DataBits { + + Seven = 7, + Eight = 8, + + } + +} diff --git a/MewtocolNet/PublicEnums/IOType.cs b/MewtocolNet/PublicEnums/IOType.cs new file mode 100644 index 0000000..dde6f35 --- /dev/null +++ b/MewtocolNet/PublicEnums/IOType.cs @@ -0,0 +1,26 @@ +namespace MewtocolNet { + + // this is just used as syntactic sugar, + // when creating registers that are R/X/Y typed you dont need the DT types + + /// + /// The type of an input/output register + /// + public enum IOType { + + /// + /// Physical input as a bool (Relay) + /// + X = 0, + /// + /// Physical output as a bool (Relay) + /// + Y = 1, + /// + /// Internal relay + /// + R = 2, + + } + +} diff --git a/MewtocolNet/PLCEnums/OPMode.cs b/MewtocolNet/PublicEnums/OPMode.cs similarity index 100% rename from MewtocolNet/PLCEnums/OPMode.cs rename to MewtocolNet/PublicEnums/OPMode.cs diff --git a/MewtocolNet/PLCEnums/PlcVarType.cs b/MewtocolNet/PublicEnums/PlcVarType.cs similarity index 100% rename from MewtocolNet/PLCEnums/PlcVarType.cs rename to MewtocolNet/PublicEnums/PlcVarType.cs diff --git a/MewtocolNet/RegisterEnums.cs b/MewtocolNet/PublicEnums/RegisterType.cs similarity index 59% rename from MewtocolNet/RegisterEnums.cs rename to MewtocolNet/PublicEnums/RegisterType.cs index a4071a6..4dcf5ce 100644 --- a/MewtocolNet/RegisterEnums.cs +++ b/MewtocolNet/PublicEnums/RegisterType.cs @@ -34,27 +34,4 @@ public enum RegisterType { } - // this is just used as syntactic sugar, - // when creating registers that are R/X/Y typed you dont need the DT types - - /// - /// The type of an input/output register - /// - public enum IOType { - - /// - /// Physical input as a bool (Relay) - /// - X = 0, - /// - /// Physical output as a bool (Relay) - /// - Y = 1, - /// - /// Internal relay - /// - R = 2, - - } - } diff --git a/MewtocolNet/RegisterBuilding/RegBuilder.cs b/MewtocolNet/RegisterBuilding/RegBuilder.cs index 4ca8416..5879645 100644 --- a/MewtocolNet/RegisterBuilding/RegBuilder.cs +++ b/MewtocolNet/RegisterBuilding/RegBuilder.cs @@ -21,10 +21,18 @@ public class RegBuilder { }; - public static RegBuilder ForInterface (MewtocolInterface interf) { + public static RegBuilder ForInterface (IPlcEthernet interf) { var rb = new RegBuilder(); - rb.forInterface = interf; + rb.forInterface = interf as MewtocolInterface; + return rb; + + } + + public static RegBuilder ForInterface(IPlcSerial interf) { + + var rb = new RegBuilder(); + rb.forInterface = interf as MewtocolInterface; return rb; } diff --git a/MewtocolNet/RegisterBuildInfo.cs b/MewtocolNet/RegisterBuilding/RegisterBuildInfo.cs similarity index 98% rename from MewtocolNet/RegisterBuildInfo.cs rename to MewtocolNet/RegisterBuilding/RegisterBuildInfo.cs index 4eda284..7e88f4a 100644 --- a/MewtocolNet/RegisterBuildInfo.cs +++ b/MewtocolNet/RegisterBuilding/RegisterBuildInfo.cs @@ -3,7 +3,7 @@ using System.Collections; using System.Reflection; -namespace MewtocolNet { +namespace MewtocolNet.RegisterBuilding { internal struct RegisterBuildInfo { diff --git a/MewtocolNet/IRegister.cs b/MewtocolNet/Registers/Interfaces/IRegister.cs similarity index 100% rename from MewtocolNet/IRegister.cs rename to MewtocolNet/Registers/Interfaces/IRegister.cs diff --git a/MewtocolNet/IRegisterInternal.cs b/MewtocolNet/Registers/Interfaces/IRegisterInternal.cs similarity index 100% rename from MewtocolNet/IRegisterInternal.cs rename to MewtocolNet/Registers/Interfaces/IRegisterInternal.cs diff --git a/MewtocolNet/Responses.cs b/MewtocolNet/Responses.cs deleted file mode 100644 index efc003b..0000000 --- a/MewtocolNet/Responses.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace MewtocolNet { - - /// - /// The formatted result of a ascii command - /// - public struct CommandResult { - - /// - /// Success state of the message - /// - public bool Success { get; set; } - /// - /// Response text of the message - /// - public string Response { get; set; } - /// - /// Error code of the message - /// - public string Error { get; set; } - /// - /// Error text of the message - /// - public string ErrorDescription { get; set; } - - } - -} \ No newline at end of file diff --git a/MewtocolNet/TCPMessageResult.cs b/MewtocolNet/TCPMessageResult.cs deleted file mode 100644 index 0d51d14..0000000 --- a/MewtocolNet/TCPMessageResult.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace MewtocolNet { - internal enum TCPMessageResult { - - Waiting, - Success, - NotConnected, - FailedWithException, - FailedLineFeed, - - } - - internal enum CommandState { - - Intial, - LineFeed, - RequestedNextFrame, - Complete - - } - -} \ No newline at end of file diff --git a/MewtocolTests/AutomatedPropertyRegisters.cs b/MewtocolTests/AutomatedPropertyRegisters.cs index df6830a..6a002b7 100644 --- a/MewtocolTests/AutomatedPropertyRegisters.cs +++ b/MewtocolTests/AutomatedPropertyRegisters.cs @@ -109,7 +109,7 @@ private void TestBasicGeneration(IRegisterInternal reg, string propName, object [Fact(DisplayName = "Boolean R generation")] public void BooleanGen() { - var interf = new MewtocolInterface("192.168.0.1"); + var interf = new MewtocolInterfaceShared("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestBool1)); @@ -122,7 +122,7 @@ public void BooleanGen() { [Fact(DisplayName = "Boolean input XD generation")] public void BooleanInputGen() { - var interf = new MewtocolInterface("192.168.0.1"); + var interf = new MewtocolInterfaceShared("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestBoolInputXD)); @@ -135,7 +135,7 @@ public void BooleanInputGen() { [Fact(DisplayName = "Int16 generation")] public void Int16Gen() { - var interf = new MewtocolInterface("192.168.0.1"); + var interf = new MewtocolInterfaceShared("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestInt16)); @@ -148,7 +148,7 @@ public void Int16Gen() { [Fact(DisplayName = "UInt16 generation")] public void UInt16Gen() { - var interf = new MewtocolInterface("192.168.0.1"); + var interf = new MewtocolInterfaceShared("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestUInt16)); @@ -161,7 +161,7 @@ public void UInt16Gen() { [Fact(DisplayName = "Int32 generation")] public void Int32Gen() { - var interf = new MewtocolInterface("192.168.0.1"); + var interf = new MewtocolInterfaceShared("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestInt32)); @@ -174,7 +174,7 @@ public void Int32Gen() { [Fact(DisplayName = "UInt32 generation")] public void UInt32Gen() { - var interf = new MewtocolInterface("192.168.0.1"); + var interf = new MewtocolInterfaceShared("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestUInt32)); @@ -187,7 +187,7 @@ public void UInt32Gen() { [Fact(DisplayName = "Float32 generation")] public void Float32Gen() { - var interf = new MewtocolInterface("192.168.0.1"); + var interf = new MewtocolInterfaceShared("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestFloat32)); @@ -200,7 +200,7 @@ public void Float32Gen() { [Fact(DisplayName = "TimeSpan generation")] public void TimespanGen() { - var interf = new MewtocolInterface("192.168.0.1"); + var interf = new MewtocolInterfaceShared("192.168.0.1"); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestTime)); diff --git a/MewtocolTests/TestLivePLC.cs b/MewtocolTests/TestLivePLC.cs index 04cb782..540a451 100644 --- a/MewtocolTests/TestLivePLC.cs +++ b/MewtocolTests/TestLivePLC.cs @@ -73,9 +73,9 @@ public async void TestClientConnection() { output.WriteLine($"Testing: {plc.PLCName}"); - var cycleClient = new MewtocolInterface(plc.PLCIP, plc.PLCPort); + var cycleClient = new MewtocolInterfaceShared(plc.PLCIP, plc.PLCPort); - await cycleClient.ConnectAsync(); + await cycleClient.ConnectAsyncOld(); Assert.True(cycleClient.IsConnected); @@ -94,9 +94,9 @@ public async void TestClientReadPLCStatus() { output.WriteLine($"Testing: {plc.PLCName}\n"); - var client = new MewtocolInterface(plc.PLCIP, plc.PLCPort); + var client = new MewtocolInterfaceShared(plc.PLCIP, plc.PLCPort); - await client.ConnectAsync(); + await client.ConnectAsyncOld(); output.WriteLine($"{client.PlcInfo}\n"); From f338acfe8a2c07392178c7730a8a98e3ca203175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Mon, 3 Jul 2023 01:25:29 +0200 Subject: [PATCH 11/18] Fixes --- Examples/ExampleScenarios.cs | 73 +++++++++++++++-- Examples/TestRegisters.cs | 2 +- Examples/TestRegistersEnumBitwise.cs | 2 +- MewExplorer/MewExplorer.csproj | 6 +- MewtocolNet/IPlc.cs | 28 ++++++- MewtocolNet/IPlcEthernet.cs | 28 ++++++- MewtocolNet/IPlcSerial.cs | 32 ++++++-- MewtocolNet/Mewtocol.cs | 66 +++++++++++---- MewtocolNet/MewtocolInterface.cs | 75 +++++++++++++++-- .../MewtocolInterfaceRegisterHandling.cs | 55 +++++-------- MewtocolNet/MewtocolInterfaceRequests.cs | 80 +++++++++++++++++++ MewtocolNet/MewtocolInterfaceSerial.cs | 51 +++++++++--- MewtocolNet/MewtocolInterfaceTcp.cs | 61 ++++++++------ ...ollectionBase.cs => RegisterCollection.cs} | 2 +- MewtocolNet/RegisterBuilding/RegBuilder.cs | 2 +- MewtocolTests/AutomatedPropertyRegisters.cs | 50 ++++-------- MewtocolTests/TestLivePLC.cs | 55 +++++++------ 17 files changed, 494 insertions(+), 174 deletions(-) rename MewtocolNet/RegisterAttributes/{RegisterCollectionBase.cs => RegisterCollection.cs} (96%) diff --git a/Examples/ExampleScenarios.cs b/Examples/ExampleScenarios.cs index 00c1a95..442844f 100644 --- a/Examples/ExampleScenarios.cs +++ b/Examples/ExampleScenarios.cs @@ -22,7 +22,7 @@ public class ExampleScenarios { public void SetupLogger () { //attaching the logger - Logger.LogLevel = LogLevel.Verbose; + Logger.LogLevel = LogLevel.Info; Logger.OnNewLogMessage((date, level, msg) => { if (level == LogLevel.Error) Console.ForegroundColor = ConsoleColor.Red; @@ -74,12 +74,28 @@ public async Task RunDisposalAndDisconnectAsync () { } - [Scenario("Read all kinds of example registers")] - public async Task RunReadTest () { + [Scenario("Read all kinds of example registers over ethernet")] + public async Task RunReadTestEth () { //setting up a new PLC interface and register collection var interf = Mewtocol.Ethernet("192.168.115.210").WithPoller(); + await RunCyclicReadTest(interf); + + } + + [Scenario("Read all kinds of example registers over serial")] + public async Task RunReadTestSer () { + + //setting up a new PLC interface and register collection + var interf = Mewtocol.SerialAuto("COM4").WithPoller(); + + await RunCyclicReadTest(interf); + + } + + private async Task RunCyclicReadTest (IPlc interf) { + //auto add all built registers to the interface var builder = RegBuilder.ForInterface(interf); var r0reg = builder.FromPlcRegName("R0").Build(); @@ -95,14 +111,26 @@ public async Task RunReadTest () { var stringReg = builder.FromPlcRegName("DT40").AsPlcType(PlcVarType.STRING).Build(); //connect - await interf.ConnectAsync(); + if(interf is IPlcSerial serialPlc) { + + await serialPlc.ConnectAsync(() => { + + Console.WriteLine($"Trying config: {serialPlc.ConnectionInfo}"); + + }); + + } else { + + await interf.ConnectAsync(); + + } //await first register data await interf.AwaitFirstDataAsync(); _ = Task.Factory.StartNew(async () => { - void setTitle () { + void setTitle() { Console.Title = $"Speed UP: {interf.BytesPerSecondUpstream} B/s, " + @@ -112,7 +140,7 @@ void setTitle () { } - while (interf.IsConnected) { + while (interf.IsConnected) { setTitle(); await Task.Delay(1000); } @@ -127,7 +155,7 @@ void setTitle () { //set bool await r0reg.WriteAsync(!(bool)r0reg.Value); - + //set random num await shortReg.WriteAsync((short)new Random().Next(0, 100)); await stringReg.WriteAsync($"_{DateTime.Now.Second}s_"); @@ -135,7 +163,7 @@ void setTitle () { sw.Stop(); foreach (var reg in interf.Registers) - Console.WriteLine(reg.ToString()); + Console.WriteLine(reg.ToString()); Console.WriteLine($"Total write time for registers: {sw.Elapsed.TotalMilliseconds:N0}ms"); @@ -287,4 +315,33 @@ public async Task FindCassettes () { } + [Scenario("Test")] + public async Task Test () { + + Logger.LogLevel = LogLevel.Critical; + + //fpx c14 r + var plxFpx = Mewtocol.Ethernet("192.168.178.55"); + await plxFpx.ConnectAsync(); + await ((MewtocolInterface)plxFpx).GetSystemRegister(); + + //fpx-h c30 t + var plcFpxH = Mewtocol.Ethernet("192.168.115.210"); + await plcFpxH.ConnectAsync(); + await ((MewtocolInterface)plcFpxH).GetSystemRegister(); + + //fpx-h c14 r + var plcFpxHc14 = Mewtocol.Ethernet("192.168.115.212"); + await plcFpxHc14.ConnectAsync(); + await ((MewtocolInterface)plcFpxHc14).GetSystemRegister(); + + //fpx c30 t + var plcFpxc30T = Mewtocol.Ethernet("192.168.115.213"); + await plcFpxc30T.ConnectAsync(); + await ((MewtocolInterface)plcFpxc30T).GetSystemRegister(); + + await Task.Delay(-1); + + } + } diff --git a/Examples/TestRegisters.cs b/Examples/TestRegisters.cs index 4b13ca1..c2bbe9a 100644 --- a/Examples/TestRegisters.cs +++ b/Examples/TestRegisters.cs @@ -4,7 +4,7 @@ using System.Collections; namespace Examples { - public class TestRegisters : RegisterCollectionBase { + public class TestRegisters : RegisterCollection { //corresponds to a R100 boolean register in the PLC [Register(IOType.R, 1000)] diff --git a/Examples/TestRegistersEnumBitwise.cs b/Examples/TestRegistersEnumBitwise.cs index 72f5ede..a3990b8 100644 --- a/Examples/TestRegistersEnumBitwise.cs +++ b/Examples/TestRegistersEnumBitwise.cs @@ -5,7 +5,7 @@ namespace Examples { - public class TestRegistersEnumBitwise : RegisterCollectionBase { + public class TestRegistersEnumBitwise : RegisterCollection { private bool startCyclePLC; diff --git a/MewExplorer/MewExplorer.csproj b/MewExplorer/MewExplorer.csproj index 9b64231..9a8e14e 100644 --- a/MewExplorer/MewExplorer.csproj +++ b/MewExplorer/MewExplorer.csproj @@ -1,7 +1,7 @@  - net7.0-android;net7.0-ios;net7.0-maccatalyst + net7.0-android; $(TargetFrameworks);net7.0-windows10.0.19041.0 @@ -23,12 +23,10 @@ 1.0 1 - 14.2 - 14.0 24.0 10.0.17763.0 10.0.17763.0 - 6.5 + diff --git a/MewtocolNet/IPlc.cs b/MewtocolNet/IPlc.cs index 62c1c33..6bc1c1b 100644 --- a/MewtocolNet/IPlc.cs +++ b/MewtocolNet/IPlc.cs @@ -1,4 +1,5 @@ -using System; +using MewtocolNet.Registers; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -49,6 +50,11 @@ public interface IPlc : IDisposable { /// int StationNumber { get; } + /// + /// A connection info string + /// + string ConnectionInfo { get; } + /// /// The initial connection timeout in milliseconds /// @@ -91,6 +97,26 @@ public interface IPlc : IDisposable { /// string GetConnectionInfo(); + /// + /// Adds a register to the plc + /// + void AddRegister(BaseRegister register); + + /// + /// Adds a register to the plc + /// + void AddRegister(IRegister register); + + /// + /// Gets a register from the plc by name + /// + IRegister GetRegister(string name); + + /// + /// Gets all registers from the plc + /// + IEnumerable GetAllRegisters(); + } } diff --git a/MewtocolNet/IPlcEthernet.cs b/MewtocolNet/IPlcEthernet.cs index f9032c1..dd95ddf 100644 --- a/MewtocolNet/IPlcEthernet.cs +++ b/MewtocolNet/IPlcEthernet.cs @@ -1,4 +1,9 @@ -namespace MewtocolNet { +using MewtocolNet.RegisterAttributes; +using System; +using System.Net; +using System.Threading.Tasks; + +namespace MewtocolNet { /// /// Provides a interface for Panasonic PLCs over a ethernet connection @@ -16,9 +21,14 @@ public interface IPlcEthernet : IPlc { int Port { get; } /// - /// Attaches a poller to the interface + /// The host ip endpoint, leave it null to use an automatic interface + /// + IPEndPoint HostEndpoint { get; set; } + + /// + /// Tries to establish a connection with the device asynchronously /// - public IPlcEthernet WithPoller(); + Task ConnectAsync(); /// /// Configures the serial interface @@ -28,6 +38,18 @@ public interface IPlcEthernet : IPlc { /// Station Number of the PLC void ConfigureConnection(string _ip, int _port = 9094, int _station = 1); + /// + /// Attaches a poller to the interface + /// + IPlcEthernet WithPoller(); + + /// + /// Attaches a register collection object to + /// the interface that can be updated automatically. + /// + /// The type of the collection base class + IPlcEthernet AddRegisterCollection(RegisterCollection collection); + } } diff --git a/MewtocolNet/IPlcSerial.cs b/MewtocolNet/IPlcSerial.cs index 09d2587..609bf40 100644 --- a/MewtocolNet/IPlcSerial.cs +++ b/MewtocolNet/IPlcSerial.cs @@ -1,4 +1,5 @@ -using System; +using MewtocolNet.RegisterAttributes; +using System; using System.Collections.Generic; using System.IO.Ports; using System.Text; @@ -36,11 +37,6 @@ public interface IPlcSerial : IPlc { /// StopBits SerialStopBits { get; } - /// - /// Attaches a poller to the interface - /// - public IPlcSerial WithPoller(); - /// /// Sets up the connection settings for the device /// @@ -50,7 +46,29 @@ public interface IPlcSerial : IPlc { /// The serial connection parity /// The serial connection stop bits /// The station number of the PLC - void ConfigureConnection(string _portName, int _baudRate = 19200, int _dataBits = 8, Parity _parity = Parity.Odd, StopBits _stopBits = StopBits.One, int _station = 1) + void ConfigureConnection(string _portName, int _baudRate = 19200, int _dataBits = 8, Parity _parity = Parity.Odd, StopBits _stopBits = StopBits.One, int _station = 1); + + /// + /// Tries to establish a connection with the device asynchronously + /// + Task ConnectAsync(); + + /// + /// Tries to establish a connection with the device asynchronously + /// + Task ConnectAsync(Action onTryingConfig); + + /// + /// Attaches a poller to the interface + /// + IPlcSerial WithPoller(); + + /// + /// Attaches a register collection object to + /// the interface that can be updated automatically. + /// + /// The type of the collection base class + IPlcSerial AddRegisterCollection(RegisterCollection collection); } diff --git a/MewtocolNet/Mewtocol.cs b/MewtocolNet/Mewtocol.cs index 7515cbf..ca88908 100644 --- a/MewtocolNet/Mewtocol.cs +++ b/MewtocolNet/Mewtocol.cs @@ -1,6 +1,9 @@ -using System; +using MewtocolNet.Exceptions; +using System; using System.Collections.Generic; using System.IO.Ports; +using System.Linq; +using System.Net; using System.Text; namespace MewtocolNet { @@ -13,14 +16,29 @@ public static class Mewtocol { /// /// Builds a ethernet based Mewtocol Interface /// - /// - /// - /// + /// + /// + /// Plc station number /// - public static IPlcEthernet Ethernet (string _ip, int _port = 9094, int _station = 1) { + public static IPlcEthernet Ethernet (string ip, int port = 9094, int station = 1) { var instance = new MewtocolInterfaceTcp(); - instance.ConfigureConnection(_ip, _port, _station); + instance.ConfigureConnection(ip, port, station); + return instance; + + } + + /// + /// Builds a ethernet based Mewtocol Interface + /// + /// + /// + /// Plc station number + /// + public static IPlcEthernet Ethernet(IPAddress ip, int port = 9094, int station = 1) { + + var instance = new MewtocolInterfaceTcp(); + instance.ConfigureConnection(ip, port, station); return instance; } @@ -28,16 +46,19 @@ public static IPlcEthernet Ethernet (string _ip, int _port = 9094, int _station /// /// Builds a serial port based Mewtocol Interface /// - /// - /// - /// - /// - /// + /// + /// + /// + /// + /// + /// /// - public static IPlcSerial Serial (string _portName, BaudRate _baudRate = BaudRate._19200, DataBits _dataBits = DataBits.Eight, Parity _parity = Parity.Odd, StopBits _stopBits = StopBits.One, int _station = 1) { + public static IPlcSerial Serial (string portName, BaudRate baudRate = BaudRate._19200, DataBits dataBits = DataBits.Eight, Parity parity = Parity.Odd, StopBits stopBits = StopBits.One, int station = 1) { + + TestPortName(portName); var instance = new MewtocolInterfaceSerial(); - instance.ConfigureConnection(_portName, (int)_baudRate, (int)_dataBits, _parity, _stopBits, _station); + instance.ConfigureConnection(portName, (int)baudRate, (int)dataBits, parity, stopBits, station); return instance; } @@ -45,18 +66,29 @@ public static IPlcSerial Serial (string _portName, BaudRate _baudRate = BaudRate /// /// Builds a serial mewtocol interface that finds the correct settings for the given port name automatically /// - /// - /// + /// + /// /// - public static IPlcSerial SerialAuto (string _portName, int _station = 1) { + public static IPlcSerial SerialAuto (string portName, int station = 1) { + + TestPortName(portName); var instance = new MewtocolInterfaceSerial(); - instance.ConfigureConnection(_portName, _station); + instance.ConfigureConnection(portName, station); instance.ConfigureConnectionAuto(); return instance; } + private static void TestPortName (string portName) { + + var portnames = SerialPort.GetPortNames(); + + if (!portnames.Any(x => x == portName)) + throw new MewtocolException($"The port {portName} is no valid port"); + + } + } } diff --git a/MewtocolNet/MewtocolInterface.cs b/MewtocolNet/MewtocolInterface.cs index 340924f..da8e69b 100644 --- a/MewtocolNet/MewtocolInterface.cs +++ b/MewtocolNet/MewtocolInterface.cs @@ -115,6 +115,9 @@ private protected set { } } + /// + public string ConnectionInfo => GetConnectionInfo(); + #endregion #region Public read/write Properties / Fields @@ -151,9 +154,9 @@ void MewtocolInterface_Connected(PLCInfo obj) { }; } - + /// - public virtual Task ConnectAsync () => throw new NotImplementedException(); + public virtual async Task ConnectAsync() => throw new NotImplementedException(); /// public async Task AwaitFirstDataAsync() => await firstPollTask; @@ -163,7 +166,8 @@ public void Disconnect() { if (!IsConnected) return; - pollCycleTask.Wait(); + if(pollCycleTask != null && !pollCycleTask.IsCompleted) + pollCycleTask.Wait(); OnMajorSocketExceptionWhileConnected(); @@ -212,10 +216,16 @@ private protected async Task SendFrameAsync (string frame if (useCr) frame = $"{frame}\r"; + + SetUpstreamStopWatchStart(); + //write inital command byte[] writeBuffer = Encoding.UTF8.GetBytes(frame); stream.Write(writeBuffer, 0, writeBuffer.Length); + //calc upstream speed + CalcUpstreamSpeed(writeBuffer.Length); + Logger.Log($"[---------CMD START--------]", LogLevel.Critical, this); Logger.Log($"--> OUT MSG: {frame.Replace("\r", "(CR)")}", LogLevel.Critical, this); @@ -235,7 +245,9 @@ private protected async Task SendFrameAsync (string frame //error response var gotErrorcode = CheckForErrorMsg(resString); if (gotErrorcode != 0) { - return new MewtocolFrameResponse(gotErrorcode); + var errResponse = new MewtocolFrameResponse(gotErrorcode); + Logger.Log($"Command error: {errResponse.Error}", LogLevel.Error, this); + return errResponse; } //was multiframed response @@ -281,9 +293,13 @@ private protected async Task SendFrameAsync (string frame do { + SetDownstreamStopWatchStart(); + byte[] buffer = new byte[128]; int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + CalcDownstreamSpeed(bytesRead); + byte[] received = new byte[bytesRead]; Buffer.BlockCopy(buffer, 0, received, 0, bytesRead); @@ -338,7 +354,7 @@ private protected CommandState ParseBufferFrame(byte[] received) { private protected int CheckForErrorMsg (string msg) { //error catching - Regex errorcheck = new Regex(@"\%[0-9]{2}\!([0-9]{2})", RegexOptions.IgnoreCase); + Regex errorcheck = new Regex(@"\%..\!([0-9]{2})", RegexOptions.IgnoreCase); Match m = errorcheck.Match(msg); if (m.Success) { @@ -401,7 +417,7 @@ private protected virtual void OnDisconnect () { BytesPerSecondDownstream = 0; BytesPerSecondUpstream = 0; - CycleTimeMs = 0; + PollerCycleDurationMs = 0; IsConnected = false; ClearRegisterVals(); @@ -422,6 +438,53 @@ private protected void ClearRegisterVals() { } + private void SetUpstreamStopWatchStart () { + + if (speedStopwatchUpstr == null) { + speedStopwatchUpstr = Stopwatch.StartNew(); + } + + if (speedStopwatchUpstr.Elapsed.TotalSeconds >= 1) { + speedStopwatchUpstr.Restart(); + bytesTotalCountedUpstream = 0; + } + + } + + private void SetDownstreamStopWatchStart () { + + if (speedStopwatchDownstr == null) { + speedStopwatchDownstr = Stopwatch.StartNew(); + } + + if (speedStopwatchDownstr.Elapsed.TotalSeconds >= 1) { + speedStopwatchDownstr.Restart(); + bytesTotalCountedDownstream = 0; + } + + } + + private void CalcUpstreamSpeed (int byteCount) { + + bytesTotalCountedUpstream += byteCount; + + var perSecUpstream = (double)((bytesTotalCountedUpstream / speedStopwatchUpstr.Elapsed.TotalMilliseconds) * 1000); + if (perSecUpstream <= 10000) + BytesPerSecondUpstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero); + + } + + private void CalcDownstreamSpeed (int byteCount) { + + bytesTotalCountedDownstream += byteCount; + + var perSecDownstream = (double)((bytesTotalCountedDownstream / speedStopwatchDownstr.Elapsed.TotalMilliseconds) * 1000); + + if (perSecDownstream <= 10000) + BytesPerSecondDownstream = (int)Math.Round(perSecDownstream, MidpointRounding.AwayFromZero); + + } + private protected void OnPropChange([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); diff --git a/MewtocolNet/MewtocolInterfaceRegisterHandling.cs b/MewtocolNet/MewtocolInterfaceRegisterHandling.cs index f86a029..51bb07f 100644 --- a/MewtocolNet/MewtocolInterfaceRegisterHandling.cs +++ b/MewtocolNet/MewtocolInterfaceRegisterHandling.cs @@ -205,15 +205,7 @@ private async Task UpdateDTRegisters () { #region Register Colleciton adding - /// - /// 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) { + internal MewtocolInterface WithRegisterCollection (RegisterCollection collection) { collection.PLCInterface = this; @@ -353,6 +345,23 @@ public MewtocolInterface WithRegisterCollection(RegisterCollectionBase collectio #region Register Adding + /// + public void AddRegister(BaseRegister register) { + + if (CheckDuplicateRegister(register)) + throw MewtocolException.DupeRegister(register); + + if (CheckDuplicateNameRegister(register)) + throw MewtocolException.DupeNameRegister(register); + + register.attachedInterface = this; + RegistersUnderlying.Add(register); + + } + + /// + public void AddRegister(IRegister register) => AddRegister(register as BaseRegister); + internal void AddRegister (RegisterBuildInfo buildInfo) { var builtRegister = buildInfo.Build(); @@ -375,19 +384,6 @@ internal void AddRegister (RegisterBuildInfo buildInfo) { } - public void AddRegister (BaseRegister register) { - - if (CheckDuplicateRegister(register)) - throw MewtocolException.DupeRegister(register); - - if (CheckDuplicateNameRegister(register)) - throw MewtocolException.DupeNameRegister(register); - - register.attachedInterface = this; - RegistersUnderlying.Add(register); - - } - private bool CheckDuplicateRegister (IRegisterInternal instance, out IRegisterInternal foundDupe) { foundDupe = RegistersInternal.FirstOrDefault(x => x.CompareIsDuplicate(instance)); @@ -414,24 +410,15 @@ private bool CheckDuplicateNameRegister(IRegisterInternal instance) { #region Register accessing - /// - /// Gets a register that was added by its name - /// - /// + /// > public IRegister GetRegister(string name) { return RegistersUnderlying.FirstOrDefault(x => x.Name == name); } - #endregion - - #region Register Reading - - /// - /// Gets a list of all added registers - /// - public IEnumerable GetAllRegisters() { + /// + public IEnumerable GetAllRegisters () { return RegistersUnderlying.Cast(); diff --git a/MewtocolNet/MewtocolInterfaceRequests.cs b/MewtocolNet/MewtocolInterfaceRequests.cs index 7b35869..75e8906 100644 --- a/MewtocolNet/MewtocolInterfaceRequests.cs +++ b/MewtocolNet/MewtocolInterfaceRequests.cs @@ -289,6 +289,86 @@ internal async Task SetRegisterAsync (IRegister register, object value) { #endregion + #region Reading / Writing Plc program + + public async Task ReadPLCProgramAsync () { + + var cmd = SendCommandAsync($""); + + + } + + public async Task GetSystemRegister () { + + //the "." means CR or \r + + await SendCommandAsync("%EE#RT"); + + //then get plc status extended? gets polled all time + // %EE#EX00RT00 + await SendCommandAsync("%EE#EX00RT00"); + + //fpx C14 r + + //%EE$EX00 RT + //00 Extended mode + //32 Data item count + //70 Machine type + //00 Version (Fixed to 00) + //16 Prog capacity in K + //81 Operation mode / status + //00 Link unit + //60 Error flag + //0000 Self diag error + //50 Version + //02 Hardware information + //0 Number of programs + //4100 Program size BCD + //1600 Header size (no. of words) bcd + //1604 System register size + //96230000001480004 ?? + // + + // PLC TYPE | Machine Code | HW Information + // FPX C14 R | 70 | 02 + // FPX C30 T | 77 | 02 + + // FPX-H C14 R | A0 | 01 + // FPX-H C30 T | A5 | 01 + + + //then a sequence of these is sent + + // Specifiy register for monitoring + // %EE#MDFFFFFF + //await SendCommandAsync("%EE#MDFFFFFF"); + + // reset monitor registers + // %EE#MCFFFFF -> gets ackn + //await SendCommandAsync("%EE#MCFFFFF"); + + // maybe some special registers? + // %EE#MCR9029R0000R0000R0000R0000R0000R0000R0000 -> gets ackn + //await SendCommandAsync("%EE#MCR9029R0000R0000R0000R0000R0000R0000R0000"); + + // gets requested when opening plc status + // %EE#MG + // has a response like: + + //await SendCommandAsync("%EE#MG"); + + + //var res = cmd.Response.Replace("%01$RR", ""); + + //var parts = res.SplitInParts(4); + + //foreach (var part in parts) + // Console.WriteLine(part); + + } + + #endregion + #region Helpers internal string GetStationNumber() { diff --git a/MewtocolNet/MewtocolInterfaceSerial.cs b/MewtocolNet/MewtocolInterfaceSerial.cs index afe148a..8d692a0 100644 --- a/MewtocolNet/MewtocolInterfaceSerial.cs +++ b/MewtocolNet/MewtocolInterfaceSerial.cs @@ -10,6 +10,7 @@ using System.Text; using System.Threading.Tasks; using System.Threading; +using MewtocolNet.RegisterAttributes; namespace MewtocolNet { @@ -17,11 +18,23 @@ public class MewtocolInterfaceSerial : MewtocolInterface, IPlcSerial { private bool autoSerial; + private event Action tryingSerialConfig; + //serial config - public string PortName { get; private set; } - public int SerialBaudRate { get; private set; } - public int SerialDataBits { get; private set; } - public Parity SerialParity { get; private set; } + + /// + public string PortName { get; private set; } + + /// + public int SerialBaudRate { get; private set; } + + /// + public int SerialDataBits { get; private set; } + + /// + public Parity SerialParity { get; private set; } + + /// public StopBits SerialStopBits { get; private set; } //Serial @@ -29,7 +42,6 @@ public class MewtocolInterfaceSerial : MewtocolInterface, IPlcSerial { internal MewtocolInterfaceSerial () : base() { } - /// public IPlcSerial WithPoller () { @@ -38,6 +50,13 @@ public IPlcSerial WithPoller () { } + public IPlcSerial AddRegisterCollection (RegisterCollection collection) { + + WithRegisterCollection(collection); + return this; + + } + /// public override string GetConnectionInfo() { @@ -89,8 +108,17 @@ internal void ConfigureConnectionAuto () { } + public override async Task ConnectAsync() => await ConnectAsync(null); + /// - public override async Task ConnectAsync () { + public async Task ConnectAsync (Action onTryingConfig = null) { + + void OnTryConfig() { + onTryingConfig(); + } + + if (onTryingConfig != null) + tryingSerialConfig += OnTryConfig; try { @@ -98,10 +126,12 @@ public override async Task ConnectAsync () { if(autoSerial) { + Logger.Log($"Connecting [AUTO CONFIGURE]: {PortName}", LogLevel.Info, this); gotInfo = await TryConnectAsyncMulti(); } else { + Logger.Log($"Connecting [MAN]: {PortName}", LogLevel.Info, this); gotInfo = await TryConnectAsyncSingle(PortName, SerialBaudRate, SerialDataBits, SerialParity, SerialStopBits); } @@ -112,7 +142,7 @@ public override async Task ConnectAsync () { } else { - Logger.Log("Initial connection failed", LogLevel.Info, this); + Logger.Log("Initial connection failed", LogLevel.Error, this); OnMajorSocketExceptionWhileConnecting(); } @@ -125,6 +155,8 @@ public override async Task ConnectAsync () { } + tryingSerialConfig -= OnTryConfig; + } private async Task TryConnectAsyncMulti () { @@ -193,19 +225,20 @@ private async Task TryConnectAsyncSingle (string port, int baud, int db SerialParity = par; SerialStopBits = sbits; OnSerialPropsChanged(); + tryingSerialConfig?.Invoke(); serialClient.Open(); if (!serialClient.IsOpen) { - Logger.Log($"Failed to open [SERIAL]: {GetConnectionInfo()}", LogLevel.Verbose, this); + Logger.Log($"Failed to open [SERIAL]: {GetConnectionInfo()}", LogLevel.Critical, this); return null; } stream = serialClient.BaseStream; - Logger.Log($"Opened [SERIAL]: {GetConnectionInfo()}", LogLevel.Verbose, this); + Logger.Log($"Opened [SERIAL]: {GetConnectionInfo()}", LogLevel.Critical, this); var plcinf = await GetPLCInfoAsync(100); diff --git a/MewtocolNet/MewtocolInterfaceTcp.cs b/MewtocolNet/MewtocolInterfaceTcp.cs index 9a4f60e..3d25c16 100644 --- a/MewtocolNet/MewtocolInterfaceTcp.cs +++ b/MewtocolNet/MewtocolInterfaceTcp.cs @@ -26,23 +26,19 @@ namespace MewtocolNet { /// public class MewtocolInterfaceTcp : MewtocolInterface, IPlcEthernet { - /// - /// The host ip endpoint, leave it null to use an automatic interface - /// - public IPEndPoint HostEndpoint { get; set; } - //TCP internal TcpClient client; - //tcp/ip config - private string ip; - private int port; + private IPAddress ipAddr; + + /// + public string IpAddress => ipAddr.ToString(); /// - public string IpAddress => ip; + public int Port { get; private set; } /// - public int Port => port; + public IPEndPoint HostEndpoint { get; set; } internal MewtocolInterfaceTcp () : base() { } @@ -54,25 +50,42 @@ public IPlcEthernet WithPoller () { } + /// + public IPlcEthernet AddRegisterCollection (RegisterCollection collection) { + + WithRegisterCollection(collection); + return this; + + } + #region TCP connection state handling /// - public void ConfigureConnection (string _ip, int _port = 9094, int _station = 1) { + public void ConfigureConnection (string ip, int port = 9094, int station = 1) { - ip = _ip; - port = _port; - stationNumber = _station; + if (!IPAddress.TryParse(ip, out ipAddr)) + throw new MewtocolException($"The ip: {ip} is no valid ip address"); + + Port = port; + stationNumber = station; Disconnect(); } /// - public override async Task ConnectAsync () { + public void ConfigureConnection(IPAddress ip, int port = 9094, int station = 1) { - if (!IPAddress.TryParse(ip, out var targetIP)) { - throw new ArgumentException("The IP adress of the PLC was no valid format"); - } + ipAddr = ip; + Port = port; + stationNumber = station; + + Disconnect(); + + } + + /// + public override async Task ConnectAsync () { try { @@ -83,29 +96,31 @@ public override async Task ConnectAsync () { NoDelay = false, }; var ep = (IPEndPoint)client.Client.LocalEndPoint; - Logger.Log($"Connecting [MAN] endpoint: {ep.Address}:{ep.Port}", LogLevel.Verbose, this); + Logger.Log($"Connecting [MAN] endpoint: {ep.Address}:{ep.Port}", LogLevel.Info, this); } else { client = new TcpClient() { ReceiveBufferSize = RecBufferSize, NoDelay = false, - ExclusiveAddressUse = true, + //ExclusiveAddressUse = true, }; } - var result = client.BeginConnect(targetIP, port, null, null); + var result = client.BeginConnect(ipAddr, Port, null, null); var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(ConnectTimeout)); if (!success || !client.Connected) { + + Logger.Log("The PLC connection timed out", LogLevel.Error, this); OnMajorSocketExceptionWhileConnecting(); return; } if (HostEndpoint == null) { var ep = (IPEndPoint)client.Client.LocalEndPoint; - Logger.Log($"Connecting [AUTO] endpoint: {ep.Address.MapToIPv4()}:{ep.Port}", LogLevel.Verbose, this); + Logger.Log($"Connecting [AUTO] endpoint: {ep.Address.MapToIPv4()}:{ep.Port}", LogLevel.Info, this); } //get the stream @@ -121,7 +136,7 @@ public override async Task ConnectAsync () { } else { - Logger.Log("Initial connection failed", LogLevel.Info, this); + Logger.Log("Initial connection failed", LogLevel.Error, this); OnDisconnect(); } diff --git a/MewtocolNet/RegisterAttributes/RegisterCollectionBase.cs b/MewtocolNet/RegisterAttributes/RegisterCollection.cs similarity index 96% rename from MewtocolNet/RegisterAttributes/RegisterCollectionBase.cs rename to MewtocolNet/RegisterAttributes/RegisterCollection.cs index 66d38d8..497abf1 100644 --- a/MewtocolNet/RegisterAttributes/RegisterCollectionBase.cs +++ b/MewtocolNet/RegisterAttributes/RegisterCollection.cs @@ -6,7 +6,7 @@ namespace MewtocolNet.RegisterAttributes { /// /// A register collection base with full auto read and notification support built in /// - public class RegisterCollectionBase : INotifyPropertyChanged { + public class RegisterCollection : INotifyPropertyChanged { /// /// Reference to its bound interface diff --git a/MewtocolNet/RegisterBuilding/RegBuilder.cs b/MewtocolNet/RegisterBuilding/RegBuilder.cs index 5879645..6994662 100644 --- a/MewtocolNet/RegisterBuilding/RegBuilder.cs +++ b/MewtocolNet/RegisterBuilding/RegBuilder.cs @@ -21,7 +21,7 @@ public class RegBuilder { }; - public static RegBuilder ForInterface (IPlcEthernet interf) { + public static RegBuilder ForInterface (IPlc interf) { var rb = new RegBuilder(); rb.forInterface = interf as MewtocolInterface; diff --git a/MewtocolTests/AutomatedPropertyRegisters.cs b/MewtocolTests/AutomatedPropertyRegisters.cs index 6a002b7..9a30f15 100644 --- a/MewtocolTests/AutomatedPropertyRegisters.cs +++ b/MewtocolTests/AutomatedPropertyRegisters.cs @@ -15,7 +15,7 @@ public AutomatedPropertyRegisters(ITestOutputHelper output) { this.output = output; } - public class TestRegisterCollection : RegisterCollectionBase { + public class TestRegisterCollection : RegisterCollection { //corresponds to a R100 boolean register in the PLC //can also be written as R1000 because the last one is a special address @@ -109,8 +109,8 @@ private void TestBasicGeneration(IRegisterInternal reg, string propName, object [Fact(DisplayName = "Boolean R generation")] public void BooleanGen() { - var interf = new MewtocolInterfaceShared("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + var interf = Mewtocol.Ethernet("192.168.0.1"); + interf.AddRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestBool1)); @@ -122,8 +122,8 @@ public void BooleanGen() { [Fact(DisplayName = "Boolean input XD generation")] public void BooleanInputGen() { - var interf = new MewtocolInterfaceShared("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + var interf = Mewtocol.Ethernet("192.168.0.1"); + interf.AddRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestBoolInputXD)); @@ -135,8 +135,8 @@ public void BooleanInputGen() { [Fact(DisplayName = "Int16 generation")] public void Int16Gen() { - var interf = new MewtocolInterfaceShared("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + var interf = Mewtocol.Ethernet("192.168.0.1"); + interf.AddRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestInt16)); @@ -148,8 +148,8 @@ public void Int16Gen() { [Fact(DisplayName = "UInt16 generation")] public void UInt16Gen() { - var interf = new MewtocolInterfaceShared("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + var interf = Mewtocol.Ethernet("192.168.0.1"); + interf.AddRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestUInt16)); @@ -161,8 +161,8 @@ public void UInt16Gen() { [Fact(DisplayName = "Int32 generation")] public void Int32Gen() { - var interf = new MewtocolInterfaceShared("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + var interf = Mewtocol.Ethernet("192.168.0.1"); + interf.AddRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestInt32)); @@ -174,8 +174,8 @@ public void Int32Gen() { [Fact(DisplayName = "UInt32 generation")] public void UInt32Gen() { - var interf = new MewtocolInterfaceShared("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + var interf = Mewtocol.Ethernet("192.168.0.1"); + interf.AddRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestUInt32)); @@ -187,8 +187,8 @@ public void UInt32Gen() { [Fact(DisplayName = "Float32 generation")] public void Float32Gen() { - var interf = new MewtocolInterfaceShared("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + var interf = Mewtocol.Ethernet("192.168.0.1"); + interf.AddRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestFloat32)); @@ -200,8 +200,8 @@ public void Float32Gen() { [Fact(DisplayName = "TimeSpan generation")] public void TimespanGen() { - var interf = new MewtocolInterfaceShared("192.168.0.1"); - interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); + var interf = Mewtocol.Ethernet("192.168.0.1"); + interf.AddRegisterCollection(new TestRegisterCollection()).WithPoller(); var register = interf.GetRegister(nameof(TestRegisterCollection.TestTime)); @@ -210,22 +210,6 @@ public void TimespanGen() { } - //[Fact(DisplayName = "String generation")] - //public void StringGen() { - - // var interf = new MewtocolInterface("192.168.0.1"); - // interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); - - // var register = interf.GetRegister(nameof(TestRegisterCollection.TestString2)); - - // //test generic properties - // TestBasicGeneration(register, nameof(TestRegisterCollection.TestString2), null!, 7005, "DT7005"); - - // Assert.Equal(5, ((BytesRegister)register).ReservedSize); - // Assert.Equal(4, ((BytesRegister)register).MemoryLength); - - //} - } } \ No newline at end of file diff --git a/MewtocolTests/TestLivePLC.cs b/MewtocolTests/TestLivePLC.cs index 540a451..a8bdaef 100644 --- a/MewtocolTests/TestLivePLC.cs +++ b/MewtocolTests/TestLivePLC.cs @@ -45,10 +45,10 @@ public class TestLivePLC { AfterWriteValue = true, }, new RegisterReadWriteTest { - TargetRegister = new NumberRegister(3000), + TargetRegister = new NumberRegister(3000), RegisterPlcAddressName = "DT3000", - IntialValue = (int)0, - AfterWriteValue = (int)-513, + IntialValue = (short)0, + AfterWriteValue = (short)-513, }, }; @@ -73,9 +73,9 @@ public async void TestClientConnection() { output.WriteLine($"Testing: {plc.PLCName}"); - var cycleClient = new MewtocolInterfaceShared(plc.PLCIP, plc.PLCPort); + var cycleClient = Mewtocol.Ethernet(plc.PLCIP, plc.PLCPort); - await cycleClient.ConnectAsyncOld(); + await cycleClient.ConnectAsync(); Assert.True(cycleClient.IsConnected); @@ -94,9 +94,9 @@ public async void TestClientReadPLCStatus() { output.WriteLine($"Testing: {plc.PLCName}\n"); - var client = new MewtocolInterfaceShared(plc.PLCIP, plc.PLCPort); + var client = Mewtocol.Ethernet(plc.PLCIP, plc.PLCPort); - await client.ConnectAsyncOld(); + await client.ConnectAsync(); output.WriteLine($"{client.PlcInfo}\n"); @@ -111,38 +111,43 @@ public async void TestClientReadPLCStatus() { } - //[Fact(DisplayName = "Reading basic information from PLC")] - //public async void TestRegisterReadWriteAsync () { + [Fact(DisplayName = "Reading basic information from PLC")] + public async void TestRegisterReadWriteAsync() { - // foreach (var plc in testPlcInformationData) { + foreach (var plc in testPlcInformationData) { - // output.WriteLine($"Testing: {plc.PLCName}\n"); + output.WriteLine($"Testing: {plc.PLCName}\n"); - // var client = new MewtocolInterface(plc.PLCIP, plc.PLCPort); + var client = Mewtocol.Ethernet(plc.PLCIP, plc.PLCPort); - // foreach (var testRW in testRegisterRW) { + foreach (var testRW in testRegisterRW) { - // client.AddRegister(testRW.TargetRegister); + client.AddRegister(testRW.TargetRegister); - // } + } - // await client.ConnectAsync(); - // Assert.True(client.IsConnected); + await client.ConnectAsync(); + Assert.True(client.IsConnected); - // foreach (var testRW in testRegisterRW) { + foreach (var testRW in testRegisterRW) { - // client.AddRegister(testRW.TargetRegister); + var testRegister = client.Registers.First(x => x.PLCAddressName == testRW.RegisterPlcAddressName); - // } + //test inital val + Assert.Equal(testRW.IntialValue, testRegister.Value); - // Assert.Equal(client.PlcInfo.CpuInformation.Cputype, plc.Type); - // Assert.Equal(client.PlcInfo.CpuInformation.ProgramCapacity, plc.ProgCapacity); + await testRegister.WriteAsync(testRW.AfterWriteValue); - // client.Disconnect(); + //test after write val + Assert.Equal(testRW.AfterWriteValue, testRegister.Value); - // } + } - //} + client.Disconnect(); + + } + + } } From ab7f5ca302b865baab986f45e664099da36b71e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Thu, 6 Jul 2023 00:14:40 +0200 Subject: [PATCH 12/18] Added new PLC types and auto documentation --- DocBuilder/DocBuilder.csproj | 14 + DocBuilder/Docs/plctypes.md | 509 ++++++++++++++++++ DocBuilder/Docs/reverse_engineering_data.md | 97 ++++ DocBuilder/Program.cs | 135 +++++ MewtocolNet.sln | 16 +- MewtocolNet/CpuInfo.cs | 30 -- .../DocAttributes/PlcCodeTestedAttribute.cs | 14 + MewtocolNet/DocAttributes/PlcEXRTAttribute.cs | 14 + .../DocAttributes/PlcLegacyAttribute.cs | 14 + MewtocolNet/Helpers/MewtocolHelpers.cs | 74 ++- MewtocolNet/Helpers/ParsedPlcName.cs | 95 ++++ MewtocolNet/MewtocolInterfaceRequests.cs | 106 +--- MewtocolNet/MewtocolInterfaceSerial.cs | 8 +- MewtocolNet/MewtocolInterfaceTcp.cs | 2 +- MewtocolNet/MewtocolNet.csproj | 21 +- MewtocolNet/PLCInfo.cs | 27 +- MewtocolNet/PublicEnums/CpuType.cs | 46 -- MewtocolNet/PublicEnums/PlcType.cs | 323 +++++++++++ MewtocolNet/ReverseEngineering.md | 97 ++++ 19 files changed, 1466 insertions(+), 176 deletions(-) create mode 100644 DocBuilder/DocBuilder.csproj create mode 100644 DocBuilder/Docs/plctypes.md create mode 100644 DocBuilder/Docs/reverse_engineering_data.md create mode 100644 DocBuilder/Program.cs create mode 100644 MewtocolNet/DocAttributes/PlcCodeTestedAttribute.cs create mode 100644 MewtocolNet/DocAttributes/PlcEXRTAttribute.cs create mode 100644 MewtocolNet/DocAttributes/PlcLegacyAttribute.cs create mode 100644 MewtocolNet/Helpers/ParsedPlcName.cs delete mode 100644 MewtocolNet/PublicEnums/CpuType.cs create mode 100644 MewtocolNet/PublicEnums/PlcType.cs create mode 100644 MewtocolNet/ReverseEngineering.md diff --git a/DocBuilder/DocBuilder.csproj b/DocBuilder/DocBuilder.csproj new file mode 100644 index 0000000..70b2327 --- /dev/null +++ b/DocBuilder/DocBuilder.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + diff --git a/DocBuilder/Docs/plctypes.md b/DocBuilder/Docs/plctypes.md new file mode 100644 index 0000000..72e0eb9 --- /dev/null +++ b/DocBuilder/Docs/plctypes.md @@ -0,0 +1,509 @@ +# PLC Type Table +All supported PLC types for auto recognition are listed in this table. Other ones might also be supported but are shown as unknown in the library +> Discontinued PLCs
+> These are PLCs that are no longer sold by Panasonic. Marked with ⚠️ + +> EXRT PLCs
+> These are PLCs that utilize the basic `%EE#RT` and `%EE#EX00RT` command. All newer models do this. Old models only use the `%EE#RT` command. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeCapacityCodeEnumDCNTEXRTTested
📟 FP0
C10, C14, C16 2.7k 0x40FP0_2c7k__C10_C14_C16⚠️
C32, SL1 5k 0x41FP0_5k__C32_SL1⚠️
T32 10k 0x42FP0_10c0k__T32⚠️
📟 FP0H
C32T/P 32k 0xB0FP0H_32k__C32TsP
C32ET/EP 32k 0xB1FP0H_32k__C32ETsEP
📟 FP0R
C10, C14, C16 16k 0x46FP0R_16k__C10_C14_C16
C32 32k 0x47FP0R_32k__C32
T32 32k 0x48FP0R_32k__T32
F32 32k 0x49FP0R_32k__F32
📟 FP1
C14, C16 0.9k 0x04FP1_0c9k__C14_C16_OR_FPdM_0c9k__C16T⚠️
C16T 0.9k 0x04FP1_0c9k__C14_C16_OR_FPdM_0c9k__C16T⚠️
C24, C40 2.7k 0x05FP1_2c7k__C24_C40_OR_FPdM_2c7k__C20R_C20T_C32T⚠️
C20R, C20T, C32T 2.7k 0x05FP1_2c7k__C24_C40_OR_FPdM_2c7k__C20R_C20T_C32T⚠️
C56, C72 5k 0x06FP1_5k__C56_C72⚠️
📟 FP10
- 30k 0x20FP10_30k_OR_FP10S_30k⚠️
- 30k 0x20FP10_30k_OR_FP10S_30k⚠️
📟 FP10SH
- 30k 0x30FP10SH_30k⚠️
📟 FP2
- 16k 0x50FP2_16k_OR_FP2_32k⚠️
- 32k 0x50FP2_16k_OR_FP2_32k⚠️
📟 FP2SH
- 60k 0x60FP2SH_60k⚠️
- 32k 0x62FP2SH_32k⚠️
- 120k 0xE0FP2SH_120k⚠️
📟 FP3
- 10k 0x03FP3_10k⚠️
- 16k 0x13FP3_16k_OR_FPdC_16k⚠️
- 16k 0x13FP3_16k_OR_FPdC_16k⚠️
📟 FP5
- 16k 0x02FP5_16k⚠️
- 24k 0x12FP5_24k⚠️
📟 FP-e
- 2.7k 0x45FPde_2c7k⚠️
📟 FP-SIGMA
- 12k 0x43FPdSIGMA_12k⚠️
- 32k 0x44FPdSIGMA_32k⚠️
- 16k 0xE1FPdSIGMA_16k⚠️
📟 FP-X
C14R 16k 0x70FPdX_16k__C14R⚠️
C30R, C60R 32k 0x71FPdX_32k__C30R_C60R⚠️
L14 16k 0x73FPdX_16k__L14⚠️
L30, L60 32k 0x74FPdX_32k__L30_L60⚠️
C14T/P 16k 0x76FPdX_16k__C14TsP⚠️
C30T/P, C60T/P, C38AT, C40T 32k 0x77FPdX_32k__C30TsP_C60TsP_C38AT_C40T⚠️
C40RT0A 2.5k 0x7AFPdX_2c5k__C40RT0A⚠️
📟 FP-X0
L14, L30 2.5k 0x72FPdX0_2c5k__L14_L30⚠️
L40, L60 8k 0x75FPdX0_8k__L40_L60⚠️
L40, L60 16k 0x7FFPdX0_16k__L40_L60⚠️
📟 FP-XH
C14R 16k 0xA0FPdXH_16k__C14R
C30R, C40R, C60R 32k 0xA1FPdXH_32k__C30R_C40R_C60R
C14T/P 16k 0xA4FPdXH_16k__C14TsP
C30T/P, C40T, C60T/P 32k 0xA5FPdXH_32k__C30TsP_C40T_C60TsP
C38AT 32k 0xA7FPdXH_32k__C38AT
M4T/L 32k 0xA8FPdXH_32k__M4TsL
M8N16T/P 32k 0xACFPdXH_32k__M8N16TsP
M8N30T 32k 0xADFPdXH_32k__M8N30T
C40ET, C60ET 32k 0xAEFPdXH_32k__C40ET_C60ET
C60ETF 32k 0xAFFPdXH_32k__C60ETF
+ + diff --git a/DocBuilder/Docs/reverse_engineering_data.md b/DocBuilder/Docs/reverse_engineering_data.md new file mode 100644 index 0000000..0bb6a8c --- /dev/null +++ b/DocBuilder/Docs/reverse_engineering_data.md @@ -0,0 +1,97 @@ +# Open Questions + +- What is the HW information byte for? +- What are the last bytes of the EXRT message for? + +# Actual Readouts + +| PLC TYPE | CPU Code | Machine Code | HW Information| +|--------------|--------------|--------------|---------------| +| FPX C14 R | 20 | 70 | 02 | +| FPX C30 T | 20 | 77 | 02 | +| FPX-H C14 R | 20 | A0 | 01 | +| FPX-H C30 T | 20 | A5 | 01 | + +## FPX 16k C14R Actual Response Examples + +### %EE$RT + +|Reponse Byte|Description| +|------------|-----------| +| 20 | Model code | +| 25 | Version | +| 16 | Prog capacity | +| 80 | Op mode | +| 00 | Link unit | +| E1 | Error flag | +| 2D00 | Self diag error | + +### %EE$EX00RT Normal Operation + +|Reponse Byte|Description| +|------------|-----------| +| 00 | Extended mode +| 32 | Data item count +| 70 | Machine type +| 00 | Version (Fixed to 00) +| 16 | Prog capacity in K +| 80 | Operation mode / status +| 00 | Link unit +| E1 | Error flag +| 2D00 | Self diag error +| 50 | Version +| 02 | Hardware information +| 0 | Number of programs +| 4100 | Program size BCD +| 1600 | Header size (no. of words) bcd +| 1604 | System register size +| 96230000001480004 | ? + +### %EE$EX00RT with error + +|Reponse Byte|Description| +|------------|-----------| +| 00 | Extended mode +| 32 | Data item count +| 70 | Machine type +| 00 | Version (Fixed to 00) +| 16 | Prog capacity in K +| 81 | Operation mode / status +| 00 | Link unit +| 60 | Error flag +| 0000 | Self diag error +| 50 | Version +| 02 | Hardware information +| 0 | Number of programs +| 4100 | Program size BCD +| 1600 | Header size (no. of words) bcd +| 1604 | System register size +| 96230000001480004 | ? + +What are the last bytes? + +FP-X 16k C14R +96 23 00 00 00 14 80 00 4 + +FP-X 32k C30T/P +96 23 00 00 00 14 80 00 4 + +FP-XH 32k C30T/P +96 23 00 00 00 40 00 00 4 + +FP-XH 16k C14R +96 23 00 00 00 40 00 00 4 + +## FP-XH 16k C14R Actual Response Examples + +### %EE$RT + +|Reponse Byte|Description| +|------------|-----------| +| 20 | Model code | +| 16 | Version | +| 16 | Prog capacity | +| 81 | Op mode | +| 00 | Link unit | +| 60 | Error flag | +| 0000 | Self diag error | \ No newline at end of file diff --git a/DocBuilder/Program.cs b/DocBuilder/Program.cs new file mode 100644 index 0000000..c5333ea --- /dev/null +++ b/DocBuilder/Program.cs @@ -0,0 +1,135 @@ +//This program builds Markdown and docs for all kinds of data + +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; +using System.Reflection.PortableExecutable; +using System.Text; +using MewtocolNet; +using MewtocolNet.DocAttributes; + +Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; +Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + +Console.WriteLine("Building docs for PLC types..."); + +var entryLoc = Assembly.GetEntryAssembly(); +ArgumentNullException.ThrowIfNull(entryLoc); + +string filePath = Path.Combine(entryLoc.Location, @"..\..\..\..\Docs\plctypes.md"); +Console.WriteLine($"{filePath}"); + +StringBuilder markdownBuilder = new StringBuilder(); + +var plcs = Enum.GetValues(typeof(PlcType)).Cast().OrderBy(x => x.ToString()); + +void WritePlcTypeTable(IEnumerable vals) { + + var groups = vals.GroupBy(x => x.ToNameDecompose()[0].Group) + .SelectMany(grouping => grouping.OrderBy(b => (int)b)) + .GroupBy(x => x.ToNameDecompose()[0].Group); + + markdownBuilder.AppendLine(""); + + bool isFirstIt = true; + + foreach (var group in groups) { + + group.OrderBy(x => (int)x); + + bool isFirstGroup = true; + + foreach (var enu in group) { + + ParsedPlcName[] decomposed = null!; + string cpuOrMachCode = null!; + + decomposed = enu.ToNameDecompose(); + + cpuOrMachCode = ((int)enu).ToString("X2"); + ArgumentNullException.ThrowIfNull(decomposed); + + //first iteration + if (isFirstIt) { + + markdownBuilder.AppendLine(""); + + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + + markdownBuilder.AppendLine(""); + + isFirstIt = false; + + } + + if(isFirstGroup) { + + markdownBuilder.AppendLine(""); + + markdownBuilder.AppendLine($""); + + markdownBuilder.AppendLine(""); + + isFirstGroup = false; + + } + + foreach (var decomp in decomposed) { + + markdownBuilder.AppendLine(""); + + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + + if(enu.IsDiscontinued()) { + + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + + } else { + + markdownBuilder.AppendLine($""); + + } + + markdownBuilder.AppendLine($""); + markdownBuilder.AppendLine($""); + + markdownBuilder.AppendLine(""); + + } + + + } + + isFirstIt = false; + + } + + markdownBuilder.AppendLine("
TypeCapacityCodeEnumDCNTEXRTTested
📟 {group.Key}
{(decomp.SubTypes.Length == 0 ? "-" : string.Join(", ", decomp.SubTypes))} {decomp.Size}k 0x{cpuOrMachCode}{enu.ToString()}⚠️{enu.ToString()} {(enu.IsEXRTPLC() ? "✅" : "❌")} {(enu.WasTestedLive() ? "✅" : "❌")}
"); + markdownBuilder.AppendLine("\n"); + +} + +markdownBuilder.AppendLine($"# PLC Type Table"); +markdownBuilder.AppendLine($"All supported PLC types for auto recognition are listed in this table. " + + $"Other ones might also be supported but are shown as unknown in the library"); + +markdownBuilder.AppendLine($"> Discontinued PLCs
"); +markdownBuilder.AppendLine($"> These are PLCs that are no longer sold by Panasonic. Marked with ⚠️\n"); + +markdownBuilder.AppendLine($"> EXRT PLCs
"); +markdownBuilder.AppendLine($"> These are PLCs that utilize the basic `%EE#RT` and `%EE#EX00RT` command. All newer models do this. Old models only use the `%EE#RT` command.\n"); + +WritePlcTypeTable(plcs); + + +File.WriteAllText(filePath, markdownBuilder.ToString()); \ No newline at end of file diff --git a/MewtocolNet.sln b/MewtocolNet.sln index 2550c60..0fe6862 100644 --- a/MewtocolNet.sln +++ b/MewtocolNet.sln @@ -9,7 +9,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples", "Examples\Exampl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewtocolTests", "MewtocolTests\MewtocolTests.csproj", "{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MewExplorer", "MewExplorer\MewExplorer.csproj", "{F243F38A-76D3-4C38-BAE6-C61729479661}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewExplorer", "MewExplorer\MewExplorer.csproj", "{F243F38A-76D3-4C38-BAE6-C61729479661}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocBuilder", "DocBuilder\DocBuilder.csproj", "{50F2D23F-C875-4006-A0B6-7F5A181BC944}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -75,6 +77,18 @@ Global {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|x86.ActiveCfg = Release|Any CPU {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|x86.Build.0 = Release|Any CPU {F243F38A-76D3-4C38-BAE6-C61729479661}.Release|x86.Deploy.0 = Release|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Debug|x64.ActiveCfg = Debug|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Debug|x64.Build.0 = Debug|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Debug|x86.ActiveCfg = Debug|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Debug|x86.Build.0 = Debug|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Release|Any CPU.Build.0 = Release|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Release|x64.ActiveCfg = Release|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Release|x64.Build.0 = Release|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Release|x86.ActiveCfg = Release|Any CPU + {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MewtocolNet/CpuInfo.cs b/MewtocolNet/CpuInfo.cs index 969226f..273e636 100644 --- a/MewtocolNet/CpuInfo.cs +++ b/MewtocolNet/CpuInfo.cs @@ -26,36 +26,6 @@ internal static CpuInfo BuildFromHexString(string _cpuType, string _cpuVersion, 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; diff --git a/MewtocolNet/DocAttributes/PlcCodeTestedAttribute.cs b/MewtocolNet/DocAttributes/PlcCodeTestedAttribute.cs new file mode 100644 index 0000000..3883f15 --- /dev/null +++ b/MewtocolNet/DocAttributes/PlcCodeTestedAttribute.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.DocAttributes { + + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] + internal class PlcCodeTestedAttribute : Attribute { + + public PlcCodeTestedAttribute() { } + + } + +} \ No newline at end of file diff --git a/MewtocolNet/DocAttributes/PlcEXRTAttribute.cs b/MewtocolNet/DocAttributes/PlcEXRTAttribute.cs new file mode 100644 index 0000000..1975a52 --- /dev/null +++ b/MewtocolNet/DocAttributes/PlcEXRTAttribute.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.DocAttributes { + + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] + internal class PlcEXRTAttribute : Attribute { + + public PlcEXRTAttribute() {} + + } + +} diff --git a/MewtocolNet/DocAttributes/PlcLegacyAttribute.cs b/MewtocolNet/DocAttributes/PlcLegacyAttribute.cs new file mode 100644 index 0000000..01367dc --- /dev/null +++ b/MewtocolNet/DocAttributes/PlcLegacyAttribute.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.DocAttributes { + + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] + internal class PlcLegacyAttribute : Attribute { + + public PlcLegacyAttribute() {} + + } + +} diff --git a/MewtocolNet/Helpers/MewtocolHelpers.cs b/MewtocolNet/Helpers/MewtocolHelpers.cs index 1b4f071..33e692e 100644 --- a/MewtocolNet/Helpers/MewtocolHelpers.cs +++ b/MewtocolNet/Helpers/MewtocolHelpers.cs @@ -1,4 +1,5 @@ -using System; +using MewtocolNet.DocAttributes; +using System; using System.Collections; using System.Collections.Generic; using System.Globalization; @@ -315,6 +316,77 @@ internal static bool CompareIsNameDuplicate(this IRegisterInternal reg1, IRegist #endregion + #region PLC Type Enum Parsing + + /// + /// Converts the enum to a plc name string + /// + public static string ToName(this PlcType plcT) { + + return string.Join(" or ", ParsedPlcName.LegacyPlcDeconstruct(plcT).Select(x => x.WholeName)); + + } + + /// + /// Converts the enum to a decomposed struct + /// + public static ParsedPlcName[] ToNameDecompose (this PlcType legacyT) { + + return ParsedPlcName.LegacyPlcDeconstruct(legacyT); + + } + + /// + /// Checks if the PLC type is discontinued + /// + public static bool IsDiscontinued (this PlcType plcT) { + + var memberInfos = plcT.GetType().GetMember(plcT.ToString()); + var enumValueMemberInfo = memberInfos.FirstOrDefault(m => m.DeclaringType == plcT.GetType()); + var valueAttributes = enumValueMemberInfo?.GetCustomAttributes(typeof(PlcLegacyAttribute), false); + if (valueAttributes != null) { + var found = valueAttributes.FirstOrDefault(x => x.GetType() == typeof(PlcLegacyAttribute)); + if (found != null) return true; + } + + return false; + + } + + #if DEBUG + + internal static bool WasTestedLive (this PlcType plcT) { + + var memberInfos = plcT.GetType().GetMember(plcT.ToString()); + var enumValueMemberInfo = memberInfos.FirstOrDefault(m => m.DeclaringType == plcT.GetType()); + var valueAttributes = enumValueMemberInfo?.GetCustomAttributes(typeof(PlcCodeTestedAttribute), false); + if (valueAttributes != null) { + var found = valueAttributes.FirstOrDefault(x => x.GetType() == typeof(PlcCodeTestedAttribute)); + if (found != null) return true; + } + + return false; + + } + + internal static bool IsEXRTPLC (this PlcType plcT) { + + var memberInfos = plcT.GetType().GetMember(plcT.ToString()); + var enumValueMemberInfo = memberInfos.FirstOrDefault(m => m.DeclaringType == plcT.GetType()); + var valueAttributes = enumValueMemberInfo?.GetCustomAttributes(typeof(PlcEXRTAttribute), false); + if (valueAttributes != null) { + var found = valueAttributes.FirstOrDefault(x => x.GetType() == typeof(PlcEXRTAttribute)); + if (found != null) return true; + } + + return false; + + } + + #endif + + #endregion + } } \ No newline at end of file diff --git a/MewtocolNet/Helpers/ParsedPlcName.cs b/MewtocolNet/Helpers/ParsedPlcName.cs new file mode 100644 index 0000000..d4d266c --- /dev/null +++ b/MewtocolNet/Helpers/ParsedPlcName.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace MewtocolNet { + + /// + /// A structure containing the PLC name parsed + /// + public struct ParsedPlcName { + + /// + /// Whole name of the PLC + /// + public string WholeName { get; internal set; } + + /// + /// The family group of the PLC + /// + public string Group { get; internal set; } + + /// + /// The Memory size of the PLC + /// + public float Size { get; internal set; } + + /// + /// The subtype strings of the plc + /// + public string[] SubTypes { get; internal set; } + + /// + public override string ToString() => WholeName; + + internal static ParsedPlcName[] LegacyPlcDeconstruct (T legacyT) { + + string wholeStr = legacyT.ToString(); + + var split = wholeStr.Replace("_OR_", "|").Split('|'); + var reg = new Regex(@"(?[A-Za-z0-9]*)_(?[A-Za-z0-9]*)(?:__)?(?.*)"); + + var retList = new List(); + + foreach (var item in split) { + + var match = reg.Match(item); + + if (match.Success) { + + string groupStr = SanitizePlcEncodedString(match.Groups["group"].Value); + string sizeStr = SanitizePlcEncodedString(match.Groups["size"].Value); + float sizeFl = float.Parse(sizeStr.Replace("k", ""), NumberStyles.Float, CultureInfo.InvariantCulture); + string additionalStr = match.Groups["additional"].Value; + string[] subTypes = additionalStr.Split('_').Select(x => SanitizePlcEncodedString(x)).ToArray(); + + string wholeName = $"{groupStr} {sizeFl:0.##}k{(subTypes.Length > 1 ? " " : "")}{string.Join(",", subTypes)}"; + + if (string.IsNullOrEmpty(subTypes[0])) + subTypes = Array.Empty(); + + retList.Add(new ParsedPlcName { + Group = groupStr, + Size = sizeFl, + SubTypes = subTypes, + WholeName = wholeName, + }); + + } else { + + throw new FormatException($"The plc enum was not formatted correctly: {item}"); + + } + + } + + return retList.ToArray(); + + } + + private static string SanitizePlcEncodedString(string input) { + + input = input.Replace("d", "-"); + input = input.Replace("c", "."); + input = input.Replace("s", "/"); + + return input; + + } + + } + +} diff --git a/MewtocolNet/MewtocolInterfaceRequests.cs b/MewtocolNet/MewtocolInterfaceRequests.cs index 75e8906..e9506fa 100644 --- a/MewtocolNet/MewtocolInterfaceRequests.cs +++ b/MewtocolNet/MewtocolInterfaceRequests.cs @@ -19,36 +19,36 @@ public partial class MewtocolInterface { /// Gets generic information about the PLC ///
/// A PLCInfo class - public async Task GetPLCInfoAsync(int timeout = -1) { + public async Task GetPLCInfoAsync(int timeout = -1) { - var resu = await SendCommandAsync("%01#RT", true, timeout); - if (!resu.Success) return null; + //var resu = await SendCommandAsync("%01#RT", true, timeout); + //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); + //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) { + //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 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; + // string errorflag = m.Groups[7].Value; + // string error = m.Groups[8].Value; - PLCInfo retInfo = new PLCInfo { - CpuInformation = CpuInfo.BuildFromHexString(cpu, version, capacity), - OperationMode = PLCMode.BuildFromHex(operation), - ErrorCode = error, - StationNumber = int.Parse(station ?? "0"), - }; + // PLCInfo retInfo = new PLCInfo { + // CpuInformation = CpuInfo.BuildFromHexString(cpu, version, capacity), + // OperationMode = PLCMode.BuildFromHex(operation), + // ErrorCode = error, + // StationNumber = int.Parse(station ?? "0"), + // }; - PlcInfo = retInfo; - return retInfo; + // PlcInfo = retInfo; + // return retInfo; - } + //} return null; @@ -291,13 +291,6 @@ internal async Task SetRegisterAsync (IRegister register, object value) { #region Reading / Writing Plc program - public async Task ReadPLCProgramAsync () { - - var cmd = SendCommandAsync($""); - - - } - public async Task GetSystemRegister () { //the "." means CR or \r @@ -308,62 +301,7 @@ public async Task GetSystemRegister () { // %EE#EX00RT00 await SendCommandAsync("%EE#EX00RT00"); - //fpx C14 r - - //%EE$EX00 RT - //00 Extended mode - //32 Data item count - //70 Machine type - //00 Version (Fixed to 00) - //16 Prog capacity in K - //81 Operation mode / status - //00 Link unit - //60 Error flag - //0000 Self diag error - //50 Version - //02 Hardware information - //0 Number of programs - //4100 Program size BCD - //1600 Header size (no. of words) bcd - //1604 System register size - //96230000001480004 ?? - // - - // PLC TYPE | Machine Code | HW Information - // FPX C14 R | 70 | 02 - // FPX C30 T | 77 | 02 - - // FPX-H C14 R | A0 | 01 - // FPX-H C30 T | A5 | 01 - - - //then a sequence of these is sent - - // Specifiy register for monitoring - // %EE#MDFFFFFF - //await SendCommandAsync("%EE#MDFFFFFF"); - - // reset monitor registers - // %EE#MCFFFFF -> gets ackn - //await SendCommandAsync("%EE#MCFFFFF"); - - // maybe some special registers? - // %EE#MCR9029R0000R0000R0000R0000R0000R0000R0000 -> gets ackn - //await SendCommandAsync("%EE#MCR9029R0000R0000R0000R0000R0000R0000R0000"); - - // gets requested when opening plc status - // %EE#MG - // has a response like: - - //await SendCommandAsync("%EE#MG"); - - - //var res = cmd.Response.Replace("%01$RR", ""); - - //var parts = res.SplitInParts(4); - //foreach (var part in parts) - // Console.WriteLine(part); } diff --git a/MewtocolNet/MewtocolInterfaceSerial.cs b/MewtocolNet/MewtocolInterfaceSerial.cs index 8d692a0..62abbc8 100644 --- a/MewtocolNet/MewtocolInterfaceSerial.cs +++ b/MewtocolNet/MewtocolInterfaceSerial.cs @@ -122,7 +122,7 @@ void OnTryConfig() { try { - PLCInfo gotInfo = null; + PLCInfo? gotInfo = null; if(autoSerial) { @@ -138,7 +138,7 @@ void OnTryConfig() { if(gotInfo != null) { - OnConnected(gotInfo); + OnConnected(gotInfo.Value); } else { @@ -159,7 +159,7 @@ void OnTryConfig() { } - private async Task TryConnectAsyncMulti () { + private async Task TryConnectAsyncMulti () { var baudRates = Enum.GetValues(typeof(BaudRate)).Cast(); @@ -205,7 +205,7 @@ private async Task TryConnectAsyncMulti () { } - private async Task TryConnectAsyncSingle (string port, int baud, int dbits, Parity par, StopBits sbits) { + private async Task TryConnectAsyncSingle (string port, int baud, int dbits, Parity par, StopBits sbits) { try { diff --git a/MewtocolNet/MewtocolInterfaceTcp.cs b/MewtocolNet/MewtocolInterfaceTcp.cs index 3d25c16..af5ffb2 100644 --- a/MewtocolNet/MewtocolInterfaceTcp.cs +++ b/MewtocolNet/MewtocolInterfaceTcp.cs @@ -132,7 +132,7 @@ public override async Task ConnectAsync () { if (plcinf != null) { - OnConnected(plcinf); + OnConnected(plcinf.Value); } else { diff --git a/MewtocolNet/MewtocolNet.csproj b/MewtocolNet/MewtocolNet.csproj index a172606..2bb9118 100644 --- a/MewtocolNet/MewtocolNet.csproj +++ b/MewtocolNet/MewtocolNet.csproj @@ -18,12 +18,17 @@ ..\Builds\MewtocolNet.xml ..\Builds - - - <_Parameter1>MewtocolTests - - - - - + + + <_Parameter1>MewtocolTests + + + + + <_Parameter1>DocBuilder + + + + + diff --git a/MewtocolNet/PLCInfo.cs b/MewtocolNet/PLCInfo.cs index 4b1b46d..939bd59 100644 --- a/MewtocolNet/PLCInfo.cs +++ b/MewtocolNet/PLCInfo.cs @@ -1,8 +1,32 @@ namespace MewtocolNet { + + + public enum CpuType { + + + + } + + public struct PLCInfo { + + /// + /// Current error code of the PLC + /// + public string ErrorCode { get; internal set; } + + /// + /// Current station number of the PLC + /// + public int StationNumber { get; internal set; } + + + } + /// /// Contains generic information about the plc /// - public class PLCInfo { + public class PLCInfoOld + { /// /// Contains information about the PLCs cpu @@ -12,6 +36,7 @@ public class PLCInfo { /// Contains information about the PLCs operation modes /// public PLCMode OperationMode { get; set; } + /// /// Current error code of the PLC /// diff --git a/MewtocolNet/PublicEnums/CpuType.cs b/MewtocolNet/PublicEnums/CpuType.cs deleted file mode 100644 index 9312177..0000000 --- a/MewtocolNet/PublicEnums/CpuType.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace MewtocolNet { - - /// - /// CPU type of the PLC - /// - public enum CpuType { - /// - /// FP 0 / FP 2.7K - /// - FP0_FP1_2_7K, - /// - /// FP0 / FP1, 5K / 10K - /// - FP0_FP1_5K_10K, - /// - /// FP1 M 0.9K - /// - FP1_M_0_9K, - /// - /// FP2 16k / 32k - /// - FP2_16K_32K, - /// - /// FP3 C 10K - /// - FP3_C_10K, - /// - /// FP3 C 16K - /// - FP3_C_16K, - /// - /// FP5 16K - /// - FP5_16K, - /// - /// FP 5 24K - /// - FP5_24K, - /// - /// Includes panasonic FPX, FPX-H, Sigma - /// - FP_Sigma_X_H_30K_60K_120K - - } - -} \ No newline at end of file diff --git a/MewtocolNet/PublicEnums/PlcType.cs b/MewtocolNet/PublicEnums/PlcType.cs new file mode 100644 index 0000000..66e01ca --- /dev/null +++ b/MewtocolNet/PublicEnums/PlcType.cs @@ -0,0 +1,323 @@ +using MewtocolNet.DocAttributes; + +namespace MewtocolNet { + + //this overwrites the CPU code and only comes with EXRT + //special chars: (d = -) (c = .) (s = /) + + /// + /// The type of the PLC + /// + public enum PlcType { + + #region FP5 Family (Legacy) + + /// + /// FP5 16k + /// + [PlcLegacy] + FP5_16k = 0x02, + + /// + /// FP5 24k + /// + [PlcLegacy] + FP5_24k = 0x12, + + #endregion + + #region FP2 Family (Legacy) + + /// + /// FP2 16k OR FP2 32k + /// + [PlcLegacy] + FP2_16k_OR_FP2_32k = 0x50, + + //misses entry FP2 32k + + #endregion + + #region FP3/FP-C Family (Legacy) + + /// + /// FP3 10k + /// + [PlcLegacy] + FP3_10k = 0x03, + /// + /// FP3 or FP-C 16k + /// + [PlcLegacy] + FP3_16k_OR_FPdC_16k = 0x13, + + #endregion + + #region FP1 / FPM Family (Legacy) + + /// + /// FP1 0.9k C14,C16 or FP-M 0.9k C16T + /// + [PlcLegacy] + FP1_0c9k__C14_C16_OR_FPdM_0c9k__C16T = 0x04, + /// + /// FP1 2.7k C24,C40 or FP-M 2.7k C20R,C20T,C32T + /// + [PlcLegacy] + FP1_2c7k__C24_C40_OR_FPdM_2c7k__C20R_C20T_C32T = 0x05, + /// + /// FP1 5.0k C56,C72 + /// + [PlcLegacy] + FP1_5k__C56_C72 = 0x06, + + #endregion + + #region FP10 Family (Legacy) + + /// + /// FP10 OR FP10S 30k + /// + [PlcLegacy] + FP10_30k_OR_FP10S_30k = 0x20, + + //misses entry FP10 60k + + #endregion + + #region FP10SH Family (Legacy) + + /// + /// FP10SH 30k + /// + [PlcLegacy] + FP10SH_30k = 0x30, + + //misses entry FP10SH 60k + //misses entry FP10SH 120k + + #endregion + + #region FP0 Family (Legacy) + + /// + /// FP0 2.7k C10,C14,C16 + /// + [PlcLegacy] + FP0_2c7k__C10_C14_C16 = 0x40, + /// + /// FP0 5k + /// + [PlcLegacy] + FP0_5k__C32_SL1 = 0x41, + /// + /// FP0 10k + /// + [PlcLegacy] + FP0_10c0k__T32 = 0x42, + + #endregion + + #region FP-Sigma Family (Legacy) + + /// + /// FP-Sigma 12k + /// + [PlcLegacy, PlcEXRT] + FPdSIGMA_12k = 0x43, + /// + /// FP-Sigma 32k + /// + [PlcLegacy, PlcEXRT] + FPdSIGMA_32k = 0x44, + /// + /// FP-SIGMA 16k + /// + [PlcLegacy, PlcEXRT] + FPdSIGMA_16k = 0xE1, + + #endregion + + #region FP-e Family (Legacy) + + /// + /// FP-e 2.7k + /// + [PlcLegacy, PlcEXRT] + FPde_2c7k = 0x45, + + #endregion + + #region FP0R Family + + /// + /// FP0R 16k C10,C14,C16 + /// + [PlcEXRT] + FP0R_16k__C10_C14_C16 = 0x46, + /// + /// FP0R 32k C32 + /// + [PlcEXRT] + FP0R_32k__C32 = 0x47, + /// + /// FP0R 32k T32 + /// + [PlcEXRT] + FP0R_32k__T32 = 0x48, + /// + /// FP0R 32k F32 + /// + [PlcEXRT] + FP0R_32k__F32 = 0x49, + + #endregion + + #region FP2SH Family (Legacy) + + /// + /// FP2SH 60k + /// + [PlcLegacy, PlcEXRT] + FP2SH_60k = 0x60, + /// + /// FP2SH 32k + /// + [PlcLegacy, PlcEXRT] + FP2SH_32k = 0x62, + /// + /// FP2SH 120k + /// + [PlcLegacy, PlcEXRT] + FP2SH_120k = 0xE0, + + #endregion + + #region FP-X Family (Legacy) + + /// + /// FP-X 16k C14R + /// + [PlcLegacy, PlcEXRT, PlcCodeTested] + FPdX_16k__C14R = 0x70, + /// + /// FP-X 32k C30R,C60R + /// + [PlcLegacy, PlcEXRT] + FPdX_32k__C30R_C60R = 0x71, + /// + /// FP-X0 2.5k L14,L30 + /// + [PlcLegacy, PlcEXRT] + FPdX0_2c5k__L14_L30 = 0x72, + /// + /// FP-X 16k L14 + /// + [PlcLegacy, PlcEXRT] + FPdX_16k__L14 = 0x73, + /// + /// FP-X 32k L30,L60 + /// + [PlcLegacy, PlcEXRT] + FPdX_32k__L30_L60 = 0x74, + /// + /// FP-X0 8k L40,L60 + /// + [PlcLegacy, PlcEXRT] + FPdX0_8k__L40_L60 = 0x75, + /// + /// FP-X 16k C14T/P + /// + [PlcLegacy, PlcEXRT] + FPdX_16k__C14TsP = 0x76, + /// + /// FP-X 32k C30T/P,C60T/P,C38AT,C40T + /// + [PlcLegacy, PlcEXRT, PlcCodeTested] + FPdX_32k__C30TsP_C60TsP_C38AT_C40T = 0x77, + /// + /// FP-X 2.5k C40RT0A + /// + [PlcLegacy, PlcEXRT] + FPdX_2c5k__C40RT0A = 0x7A, + /// + /// FP-X0 16k L40,L60 + /// + [PlcLegacy, PlcEXRT] + FPdX0_16k__L40_L60 = 0x7F, + + #endregion + + #region FP-XH Family + + /// + /// FP-XH 16k C14R + /// + [PlcEXRT, PlcCodeTested] + FPdXH_16k__C14R = 0xA0, + /// + /// FP-XH 32k C30R,C40R,C60R + /// + [PlcEXRT] + FPdXH_32k__C30R_C40R_C60R = 0xA1, + /// + /// FP-XH 16k C14T/P + /// + [PlcEXRT] + FPdXH_16k__C14TsP = 0xA4, + /// + /// FP-XH 32k C30T/P,C40T,C60T/P + /// + [PlcEXRT, PlcCodeTested] + FPdXH_32k__C30TsP_C40T_C60TsP = 0xA5, + /// + /// FP-XH 32k C38AT + /// + [PlcEXRT] + FPdXH_32k__C38AT = 0xA7, + /// + /// FP-XH 32k M4T/L + /// + [PlcEXRT] + FPdXH_32k__M4TsL = 0xA8, + /// + /// FP-XH 32k M8N16T/P (RTEX) + /// + [PlcEXRT] + FPdXH_32k__M8N16TsP = 0xAC, + /// + /// FP-XH 32k M8N30T (RTEX) + /// + [PlcEXRT] + FPdXH_32k__M8N30T = 0xAD, + /// + /// FP-XH 32k C40ET,C60ET + /// + [PlcEXRT] + FPdXH_32k__C40ET_C60ET = 0xAE, + /// + /// FP-XH 32k C60ETF + /// + [PlcEXRT] + FPdXH_32k__C60ETF = 0xAF, + + #endregion + + #region FP0H Family + + /// + /// FP0H 32k C32T/P + /// + [PlcEXRT] + FP0H_32k__C32TsP = 0xB0, + /// + /// FP0H 32k C32ET/EP + /// + [PlcEXRT] + FP0H_32k__C32ETsEP = 0xB1, + + #endregion + + } + +} \ No newline at end of file diff --git a/MewtocolNet/ReverseEngineering.md b/MewtocolNet/ReverseEngineering.md new file mode 100644 index 0000000..0bb6a8c --- /dev/null +++ b/MewtocolNet/ReverseEngineering.md @@ -0,0 +1,97 @@ +# Open Questions + +- What is the HW information byte for? +- What are the last bytes of the EXRT message for? + +# Actual Readouts + +| PLC TYPE | CPU Code | Machine Code | HW Information| +|--------------|--------------|--------------|---------------| +| FPX C14 R | 20 | 70 | 02 | +| FPX C30 T | 20 | 77 | 02 | +| FPX-H C14 R | 20 | A0 | 01 | +| FPX-H C30 T | 20 | A5 | 01 | + +## FPX 16k C14R Actual Response Examples + +### %EE$RT + +|Reponse Byte|Description| +|------------|-----------| +| 20 | Model code | +| 25 | Version | +| 16 | Prog capacity | +| 80 | Op mode | +| 00 | Link unit | +| E1 | Error flag | +| 2D00 | Self diag error | + +### %EE$EX00RT Normal Operation + +|Reponse Byte|Description| +|------------|-----------| +| 00 | Extended mode +| 32 | Data item count +| 70 | Machine type +| 00 | Version (Fixed to 00) +| 16 | Prog capacity in K +| 80 | Operation mode / status +| 00 | Link unit +| E1 | Error flag +| 2D00 | Self diag error +| 50 | Version +| 02 | Hardware information +| 0 | Number of programs +| 4100 | Program size BCD +| 1600 | Header size (no. of words) bcd +| 1604 | System register size +| 96230000001480004 | ? + +### %EE$EX00RT with error + +|Reponse Byte|Description| +|------------|-----------| +| 00 | Extended mode +| 32 | Data item count +| 70 | Machine type +| 00 | Version (Fixed to 00) +| 16 | Prog capacity in K +| 81 | Operation mode / status +| 00 | Link unit +| 60 | Error flag +| 0000 | Self diag error +| 50 | Version +| 02 | Hardware information +| 0 | Number of programs +| 4100 | Program size BCD +| 1600 | Header size (no. of words) bcd +| 1604 | System register size +| 96230000001480004 | ? + +What are the last bytes? + +FP-X 16k C14R +96 23 00 00 00 14 80 00 4 + +FP-X 32k C30T/P +96 23 00 00 00 14 80 00 4 + +FP-XH 32k C30T/P +96 23 00 00 00 40 00 00 4 + +FP-XH 16k C14R +96 23 00 00 00 40 00 00 4 + +## FP-XH 16k C14R Actual Response Examples + +### %EE$RT + +|Reponse Byte|Description| +|------------|-----------| +| 20 | Model code | +| 16 | Version | +| 16 | Prog capacity | +| 81 | Op mode | +| 00 | Link unit | +| 60 | Error flag | +| 0000 | Self diag error | \ No newline at end of file From a0da9e77fecd3d8caacc1e2bfb424ba9f593788e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Thu, 6 Jul 2023 11:39:34 +0200 Subject: [PATCH 13/18] Added missing PLC types for FP2, FP10SH, FM-M, FP10 --- DocBuilder/Docs/plctypes.md | 62 +++++++++++++++++++++--- DocBuilder/Program.cs | 7 ++- MewtocolNet/MewtocolInterface.cs | 2 + MewtocolNet/MewtocolInterfaceRequests.cs | 31 +++--------- MewtocolNet/PublicEnums/PlcType.cs | 25 +++++----- 5 files changed, 81 insertions(+), 46 deletions(-) diff --git a/DocBuilder/Docs/plctypes.md b/DocBuilder/Docs/plctypes.md index 72e0eb9..6bd3ce1 100644 --- a/DocBuilder/Docs/plctypes.md +++ b/DocBuilder/Docs/plctypes.md @@ -101,7 +101,7 @@ All supported PLC types for auto recognition are listed in this table. Other one ❌ -📟 FP1 +📟 FP1, FP-M C14, C16 @@ -143,19 +143,37 @@ All supported PLC types for auto recognition are listed in this table. Other one C56, C72 5k 0x06 -FP1_5k__C56_C72 +FP1_5k__C56_C72_OR_FPdM_5k__C20RC_C20TC_C32TC ⚠️ ❌ ❌ -📟 FP10 + C20RC, C20TC, C32TC + 5k +0x06 +FP1_5k__C56_C72_OR_FPdM_5k__C20RC_C20TC_C32TC +⚠️ + ❌ + ❌ + + +📟 FP10, FP10S - 30k 0x20 -FP10_30k_OR_FP10S_30k +FP10_30k_OR_FP10_60k_OR_FP10S_30k +⚠️ + ❌ + ❌ + + + - + 60k +0x20 +FP10_30k_OR_FP10_60k_OR_FP10S_30k ⚠️ ❌ ❌ @@ -164,7 +182,7 @@ All supported PLC types for auto recognition are listed in this table. Other one - 30k 0x20 -FP10_30k_OR_FP10S_30k +FP10_30k_OR_FP10_60k_OR_FP10S_30k ⚠️ ❌ ❌ @@ -176,7 +194,25 @@ All supported PLC types for auto recognition are listed in this table. Other one - 30k 0x30 -FP10SH_30k +FP10SH_30k_OR_FP10SH_60k_OR_FP10SH_120k +⚠️ + ❌ + ❌ + + + - + 60k +0x30 +FP10SH_30k_OR_FP10SH_60k_OR_FP10SH_120k +⚠️ + ❌ + ❌ + + + - + 120k +0x30 +FP10SH_30k_OR_FP10SH_60k_OR_FP10SH_120k ⚠️ ❌ ❌ @@ -245,6 +281,9 @@ All supported PLC types for auto recognition are listed in this table. Other one ❌ +📟 FP3, FP-C + + - 16k 0x13 @@ -320,7 +359,16 @@ All supported PLC types for auto recognition are listed in this table. Other one - 16k 0xE1 -FPdSIGMA_16k +FPdSIGMA_16k_OR_FPdSIGMA_40k +⚠️ + ✅ + ❌ + + + - + 40k +0xE1 +FPdSIGMA_16k_OR_FPdSIGMA_40k ⚠️ ✅ ❌ diff --git a/DocBuilder/Program.cs b/DocBuilder/Program.cs index c5333ea..d2dddfb 100644 --- a/DocBuilder/Program.cs +++ b/DocBuilder/Program.cs @@ -28,7 +28,12 @@ void WritePlcTypeTable(IEnumerable vals) { var groups = vals.GroupBy(x => x.ToNameDecompose()[0].Group) .SelectMany(grouping => grouping.OrderBy(b => (int)b)) - .GroupBy(x => x.ToNameDecompose()[0].Group); + .GroupBy( + x => string.Join(", ", + x.ToNameDecompose() + .DistinctBy(y => y.Group) + .Select(y => y.Group)) + ); markdownBuilder.AppendLine(""); diff --git a/MewtocolNet/MewtocolInterface.cs b/MewtocolNet/MewtocolInterface.cs index da8e69b..eec4492 100644 --- a/MewtocolNet/MewtocolInterface.cs +++ b/MewtocolNet/MewtocolInterface.cs @@ -210,6 +210,8 @@ private protected async Task SendFrameAsync (string frame try { + if (stream == null) return new MewtocolFrameResponse(405, "PLC not initialized"); + if (useBcc) frame = $"{frame.BuildBCCFrame()}"; diff --git a/MewtocolNet/MewtocolInterfaceRequests.cs b/MewtocolNet/MewtocolInterfaceRequests.cs index e9506fa..9775b1d 100644 --- a/MewtocolNet/MewtocolInterfaceRequests.cs +++ b/MewtocolNet/MewtocolInterfaceRequests.cs @@ -21,34 +21,15 @@ public partial class MewtocolInterface { /// A PLCInfo class public async Task GetPLCInfoAsync(int timeout = -1) { - //var resu = await SendCommandAsync("%01#RT", true, timeout); - //if (!resu.Success) return null; + var regexRT = new Regex(@"\%EE\$RT(?..)(?..)(?..)(?..)..(?..)(?....).*", RegexOptions.IgnoreCase); + + var regexEXRT = new Regex(@"\%EE\$EX00RT00(?..)(?..)..(?..)(?..)..(?..)(?....)(?..)(?..)(?.)(?....)(?....)(?....).*", RegexOptions.IgnoreCase); - //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); + var resRT = await SendCommandAsync("%EE#RT", timeoutMs: timeout); + if (!resRT.Success) return null; - //if (m.Success) { + var resEXRT = await SendCommandAsync("%EE#EX00RT00", timeoutMs: timeout); - // 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 = CpuInfo.BuildFromHexString(cpu, version, capacity), - // OperationMode = PLCMode.BuildFromHex(operation), - // ErrorCode = error, - // StationNumber = int.Parse(station ?? "0"), - // }; - - // PlcInfo = retInfo; - // return retInfo; - - //} return null; diff --git a/MewtocolNet/PublicEnums/PlcType.cs b/MewtocolNet/PublicEnums/PlcType.cs index 66e01ca..b1e2cbf 100644 --- a/MewtocolNet/PublicEnums/PlcType.cs +++ b/MewtocolNet/PublicEnums/PlcType.cs @@ -5,6 +5,8 @@ namespace MewtocolNet { //this overwrites the CPU code and only comes with EXRT //special chars: (d = -) (c = .) (s = /) + //MISSING! FP7 and EcoLogix + /// /// The type of the PLC /// @@ -66,20 +68,20 @@ public enum PlcType { [PlcLegacy] FP1_2c7k__C24_C40_OR_FPdM_2c7k__C20R_C20T_C32T = 0x05, /// - /// FP1 5.0k C56,C72 + /// FP1 5.0k C56,C72 or FPM 5k C20RC,C20TC,C32TC /// [PlcLegacy] - FP1_5k__C56_C72 = 0x06, + FP1_5k__C56_C72_OR_FPdM_5k__C20RC_C20TC_C32TC = 0x06, #endregion #region FP10 Family (Legacy) /// - /// FP10 OR FP10S 30k + /// FP10 30k,60k OR FP10S 30k /// [PlcLegacy] - FP10_30k_OR_FP10S_30k = 0x20, + FP10_30k_OR_FP10_60k_OR_FP10S_30k = 0x20, //misses entry FP10 60k @@ -88,13 +90,10 @@ public enum PlcType { #region FP10SH Family (Legacy) /// - /// FP10SH 30k + /// FP10SH 30k, 60k, 120k /// [PlcLegacy] - FP10SH_30k = 0x30, - - //misses entry FP10SH 60k - //misses entry FP10SH 120k + FP10SH_30k_OR_FP10SH_60k_OR_FP10SH_120k = 0x30, #endregion @@ -121,20 +120,20 @@ public enum PlcType { #region FP-Sigma Family (Legacy) /// - /// FP-Sigma 12k + /// FP-SIGMA 12k /// [PlcLegacy, PlcEXRT] FPdSIGMA_12k = 0x43, /// - /// FP-Sigma 32k + /// FP-SIGMA 32k /// [PlcLegacy, PlcEXRT] FPdSIGMA_32k = 0x44, /// - /// FP-SIGMA 16k + /// FP-SIGMA 16k or FP-SIGMA 40k /// [PlcLegacy, PlcEXRT] - FPdSIGMA_16k = 0xE1, + FPdSIGMA_16k_OR_FPdSIGMA_40k = 0xE1, #endregion From 6d3b5adf7d6a1b17b9edfe947d719e770d496617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Thu, 6 Jul 2023 11:46:51 +0200 Subject: [PATCH 14/18] Remove wrong md --- MewtocolNet/ReverseEngineering.md | 97 ------------------------------- 1 file changed, 97 deletions(-) delete mode 100644 MewtocolNet/ReverseEngineering.md diff --git a/MewtocolNet/ReverseEngineering.md b/MewtocolNet/ReverseEngineering.md deleted file mode 100644 index 0bb6a8c..0000000 --- a/MewtocolNet/ReverseEngineering.md +++ /dev/null @@ -1,97 +0,0 @@ -# Open Questions - -- What is the HW information byte for? -- What are the last bytes of the EXRT message for? - -# Actual Readouts - -| PLC TYPE | CPU Code | Machine Code | HW Information| -|--------------|--------------|--------------|---------------| -| FPX C14 R | 20 | 70 | 02 | -| FPX C30 T | 20 | 77 | 02 | -| FPX-H C14 R | 20 | A0 | 01 | -| FPX-H C30 T | 20 | A5 | 01 | - -## FPX 16k C14R Actual Response Examples - -### %EE$RT - -|Reponse Byte|Description| -|------------|-----------| -| 20 | Model code | -| 25 | Version | -| 16 | Prog capacity | -| 80 | Op mode | -| 00 | Link unit | -| E1 | Error flag | -| 2D00 | Self diag error | - -### %EE$EX00RT Normal Operation - -|Reponse Byte|Description| -|------------|-----------| -| 00 | Extended mode -| 32 | Data item count -| 70 | Machine type -| 00 | Version (Fixed to 00) -| 16 | Prog capacity in K -| 80 | Operation mode / status -| 00 | Link unit -| E1 | Error flag -| 2D00 | Self diag error -| 50 | Version -| 02 | Hardware information -| 0 | Number of programs -| 4100 | Program size BCD -| 1600 | Header size (no. of words) bcd -| 1604 | System register size -| 96230000001480004 | ? - -### %EE$EX00RT with error - -|Reponse Byte|Description| -|------------|-----------| -| 00 | Extended mode -| 32 | Data item count -| 70 | Machine type -| 00 | Version (Fixed to 00) -| 16 | Prog capacity in K -| 81 | Operation mode / status -| 00 | Link unit -| 60 | Error flag -| 0000 | Self diag error -| 50 | Version -| 02 | Hardware information -| 0 | Number of programs -| 4100 | Program size BCD -| 1600 | Header size (no. of words) bcd -| 1604 | System register size -| 96230000001480004 | ? - -What are the last bytes? - -FP-X 16k C14R -96 23 00 00 00 14 80 00 4 - -FP-X 32k C30T/P -96 23 00 00 00 14 80 00 4 - -FP-XH 32k C30T/P -96 23 00 00 00 40 00 00 4 - -FP-XH 16k C14R -96 23 00 00 00 40 00 00 4 - -## FP-XH 16k C14R Actual Response Examples - -### %EE$RT - -|Reponse Byte|Description| -|------------|-----------| -| 20 | Model code | -| 16 | Version | -| 16 | Prog capacity | -| 81 | Op mode | -| 00 | Link unit | -| 60 | Error flag | -| 0000 | Self diag error | \ No newline at end of file From 616d102dea5219b0f02718d3554dfcbb9759332b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Thu, 6 Jul 2023 18:51:54 +0200 Subject: [PATCH 15/18] Added new console for all examples later on --- Examples/ExampleScenarios.cs | 2 +- MewTerminal/Commands/ClearCommand.cs | 19 +++ .../Commands/CommandLineExcecuteable.cs | 41 ++++++ MewTerminal/Commands/ScanCommand.cs | 102 +++++++++++++++ MewTerminal/Helpers/Helpers.cs | 54 ++++++++ MewTerminal/MewTerminal.csproj | 19 +++ MewTerminal/Program.cs | 86 ++++++++++++ MewtocolNet.sln | 16 ++- MewtocolNet/ComCassette/CassetteFinder.cs | 23 +++- MewtocolNet/CpuInfo.cs | 37 ------ MewtocolNet/Helpers/LinqHelpers.cs | 31 +++++ MewtocolNet/Helpers/MewtocolHelpers.cs | 8 +- MewtocolNet/Helpers/ParsedPlcName.cs | 4 +- MewtocolNet/IPlc.cs | 2 +- MewtocolNet/Logging/LoggerEnums.cs | 4 + MewtocolNet/MewtocolFrameResponse.cs | 14 ++ MewtocolNet/MewtocolInterface.cs | 6 +- MewtocolNet/MewtocolInterfaceRequests.cs | 37 +++++- MewtocolNet/MewtocolNet.csproj | 2 +- MewtocolNet/PLCInfo.cs | 123 ++++++++++++------ MewtocolNet/PLCMode.cs | 93 ------------- MewtocolNet/PublicEnums/HWInformation.cs | 77 +++++++++++ 22 files changed, 608 insertions(+), 192 deletions(-) create mode 100644 MewTerminal/Commands/ClearCommand.cs create mode 100644 MewTerminal/Commands/CommandLineExcecuteable.cs create mode 100644 MewTerminal/Commands/ScanCommand.cs create mode 100644 MewTerminal/Helpers/Helpers.cs create mode 100644 MewTerminal/MewTerminal.csproj create mode 100644 MewTerminal/Program.cs delete mode 100644 MewtocolNet/CpuInfo.cs create mode 100644 MewtocolNet/Helpers/LinqHelpers.cs delete mode 100644 MewtocolNet/PLCMode.cs create mode 100644 MewtocolNet/PublicEnums/HWInformation.cs diff --git a/Examples/ExampleScenarios.cs b/Examples/ExampleScenarios.cs index 442844f..59d4bb5 100644 --- a/Examples/ExampleScenarios.cs +++ b/Examples/ExampleScenarios.cs @@ -126,7 +126,7 @@ await serialPlc.ConnectAsync(() => { } //await first register data - await interf.AwaitFirstDataAsync(); + await interf.AwaitFirstDataCycleAsync(); _ = Task.Factory.StartNew(async () => { diff --git a/MewTerminal/Commands/ClearCommand.cs b/MewTerminal/Commands/ClearCommand.cs new file mode 100644 index 0000000..098f89c --- /dev/null +++ b/MewTerminal/Commands/ClearCommand.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using CommandLine; +using MewtocolNet; +using MewtocolNet.ComCassette; +using Spectre.Console; + +namespace MewTerminal.Commands; + +[Verb("clear", HelpText = "Clears console", Hidden = true)] +internal class ClearCommand : CommandLineExcecuteable { + + public override void Run() { + + Console.Clear(); + + } + +} \ No newline at end of file diff --git a/MewTerminal/Commands/CommandLineExcecuteable.cs b/MewTerminal/Commands/CommandLineExcecuteable.cs new file mode 100644 index 0000000..977b438 --- /dev/null +++ b/MewTerminal/Commands/CommandLineExcecuteable.cs @@ -0,0 +1,41 @@ +using CommandLine.Text; +using CommandLine; +using MewtocolNet.Logging; + +namespace MewTerminal.Commands; + +public abstract class CommandLineExcecuteable { + + static UnParserSettings UnparserSet = new UnParserSettings { + PreferShortName = true, + }; + + [Option('v', "verbosity", HelpText = "Sets the Loglevel verbosity", Default = LogLevel.None)] + public LogLevel LogLevel { get; set; } = LogLevel.None; + + [Usage] + public static IEnumerable Examples { + get { + return new List() { + new Example( + helpText: "Sanning from adapter with ip 127.0.0.1 and logging all critical messages", + formatStyle: UnparserSet, + sample: new ScanCommand { + IPSource = "127.0.0.1", + LogLevel = LogLevel.Critical, + }), + new Example( + helpText: "Scanning from all adapters and logging only errors", + formatStyle: UnparserSet, + sample: new ScanCommand { + LogLevel = LogLevel.Error, + }), + }; + } + } + + public virtual void Run() { } + + public virtual Task RunAsync () => Task.CompletedTask; + +} diff --git a/MewTerminal/Commands/ScanCommand.cs b/MewTerminal/Commands/ScanCommand.cs new file mode 100644 index 0000000..697766b --- /dev/null +++ b/MewTerminal/Commands/ScanCommand.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using CommandLine; +using MewtocolNet; +using MewtocolNet.ComCassette; +using MewtocolNet.Logging; +using Spectre.Console; + +namespace MewTerminal.Commands; + +[Verb("scan", HelpText = "Scans all network PLCs")] +internal class ScanCommand : CommandLineExcecuteable { + + [Option("ip", HelpText = "IP of the source adapter" )] + public string? IPSource { get; set; } + + [Option("timeout", Default = 100)] + public int? TimeoutMS { get; set; } + + [Option("plc", Required = false, HelpText = "Gets the PLC types")] + public bool GetPLCTypes { get; set; } + + private class PLCCassetteTypeInfo { + + public CassetteInformation Cassette { get; set; } + + public PLCInfo PLCInf { get; set; } + + } + + public override async Task RunAsync () { + + await AnsiConsole.Status() + .Spinner(Spinner.Known.Dots) + .StartAsync("Scanning...", async ctx => { + + var query = await CassetteFinder.FindClientsAsync(IPSource, TimeoutMS ?? 100); + + var found = query.Select(x => new PLCCassetteTypeInfo { Cassette = x }).ToList(); + + if (found.Count > 0 && GetPLCTypes) { + + foreach (var item in found) { + + ctx.Status($"Getting cassette PLC {item.Cassette.IPAddress}:{item.Cassette.Port}") + .Spinner(Spinner.Known.Dots); + + var dev = Mewtocol.Ethernet(item.Cassette.IPAddress, item.Cassette.Port); + dev.ConnectTimeout = 1000; + await dev.ConnectAsync(); + item.PLCInf = dev.PlcInfo; + + dev.Disconnect(); + + } + + } + + if (found.Count() > 0) { + + AnsiConsole.MarkupLineInterpolated($"✅ Found {found.Count()} devices..."); + + } else { + + AnsiConsole.MarkupLineInterpolated($"❌ Found no devices"); + return; + + } + + if (found.Any(x => x.PLCInf != PLCInfo.None)) { + + AnsiConsole.Write(found.Select(x => new { + x.Cassette.Name, + PLC = x.PLCInf.TypeCode.ToName(), + IsRun = x.PLCInf.OperationMode.HasFlag(OPMode.Run), + IP = x.Cassette.IPAddress, + x.Cassette.Port, + DHCP = x.Cassette.UsesDHCP, + MAC = x.Cassette.MacAddress, + Ver = x.Cassette.FirmwareVersion, + x.Cassette.Status, + }).ToTable()); + + } else { + + AnsiConsole.Write(found.Select(x => new { + x.Cassette.Name, + IP = x.Cassette.IPAddress, + x.Cassette.Port, + DHCP = x.Cassette.UsesDHCP, + MAC = x.Cassette.MacAddress, + Ver = x.Cassette.FirmwareVersion, + x.Cassette.Status, + }).ToTable()); + + } + + }); + + } + +} \ No newline at end of file diff --git a/MewTerminal/Helpers/Helpers.cs b/MewTerminal/Helpers/Helpers.cs new file mode 100644 index 0000000..bc03a36 --- /dev/null +++ b/MewTerminal/Helpers/Helpers.cs @@ -0,0 +1,54 @@ +using Spectre.Console; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MewTerminal; + +internal static class Helpers { + + internal static Table ToTable (this IEnumerable data, params string[] markups) { + + // Create a table + var table = new Table(); + var type = typeof(T); + + var props = type.GetProperties(); + bool isFirst = true; + + foreach (var item in data) { + + var rowVals = new List(); + + foreach (var prop in props) { + + if(isFirst) table.AddColumn(prop.Name); + + var propVal = prop.GetValue(item); + + string strVal = propVal?.ToString() ?? "null"; + + if (propVal is byte[] bArr) { + strVal = string.Join(" ", bArr.Select(x => x.ToString("X2"))); + } + + strVal = strVal.Replace("[", ""); + strVal = strVal.Replace("]", ""); + + rowVals.Add(strVal); + + } + + isFirst = false; + + table.AddRow(rowVals.ToArray()); + + } + + return table; + + } + +} diff --git a/MewTerminal/MewTerminal.csproj b/MewTerminal/MewTerminal.csproj new file mode 100644 index 0000000..e75817e --- /dev/null +++ b/MewTerminal/MewTerminal.csproj @@ -0,0 +1,19 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/MewTerminal/Program.cs b/MewTerminal/Program.cs new file mode 100644 index 0000000..2de046c --- /dev/null +++ b/MewTerminal/Program.cs @@ -0,0 +1,86 @@ +using CommandLine; +using CommandLine.Text; +using MewTerminal.Commands; +using MewtocolNet.Logging; +using Spectre.Console; +using System.Reflection; + +namespace MewTerminal; + +internal class Program { + + static void Main(string[] args) { + + Logger.OnNewLogMessage((dt, lv, msg) => { + + AnsiConsole.WriteLine($"{msg}"); + + }); + + #if DEBUG + + Console.Clear(); + + var firstArg = new string[] { "help" }; + + start: + + if(firstArg == null) { + Console.WriteLine("Enter arguments [DEBUG MODE]"); + args = Console.ReadLine().SplitArgs(); + } + + //print help first time + InitParser(firstArg ?? args); + firstArg = null; + goto start; + + #else + + InitParser(args); + + #endif + + } + + private static Type[] LoadVerbs() { + + var lst = Assembly.GetExecutingAssembly() + .GetTypes() + .Where(t => t.GetCustomAttribute() != null) + .ToArray(); + return lst; + + } + + static void InitParser (string[] args) { + + var types = LoadVerbs(); + + var parseRes = Parser.Default.ParseArguments(args, types); + + var helpText = HelpText.AutoBuild(parseRes, h => { + + h.AddEnumValuesToHelpText = true; + + return h; + + }, e => e); + + parseRes.WithNotParsed(err => { + + }); + + if(parseRes?.Value != null && parseRes.Value is CommandLineExcecuteable exc) { + + Logger.LogLevel = exc.LogLevel; + + exc.Run(); + var task = Task.Run(exc.RunAsync); + task.Wait(); + + } + + } + +} diff --git a/MewtocolNet.sln b/MewtocolNet.sln index 0fe6862..5c0ee6e 100644 --- a/MewtocolNet.sln +++ b/MewtocolNet.sln @@ -11,7 +11,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewtocolTests", "MewtocolTe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewExplorer", "MewExplorer\MewExplorer.csproj", "{F243F38A-76D3-4C38-BAE6-C61729479661}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocBuilder", "DocBuilder\DocBuilder.csproj", "{50F2D23F-C875-4006-A0B6-7F5A181BC944}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocBuilder", "DocBuilder\DocBuilder.csproj", "{50F2D23F-C875-4006-A0B6-7F5A181BC944}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MewTerminal", "MewTerminal\MewTerminal.csproj", "{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -89,6 +91,18 @@ Global {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Release|x64.Build.0 = Release|Any CPU {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Release|x86.ActiveCfg = Release|Any CPU {50F2D23F-C875-4006-A0B6-7F5A181BC944}.Release|x86.Build.0 = Release|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|x64.ActiveCfg = Debug|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|x64.Build.0 = Debug|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|x86.ActiveCfg = Debug|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|x86.Build.0 = Debug|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|Any CPU.Build.0 = Release|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|x64.ActiveCfg = Release|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|x64.Build.0 = Release|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|x86.ActiveCfg = Release|Any CPU + {D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MewtocolNet/ComCassette/CassetteFinder.cs b/MewtocolNet/ComCassette/CassetteFinder.cs index 346b212..748d689 100644 --- a/MewtocolNet/ComCassette/CassetteFinder.cs +++ b/MewtocolNet/ComCassette/CassetteFinder.cs @@ -1,4 +1,5 @@ -using System; +using MewtocolNet.Helpers; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -20,8 +21,7 @@ public static async Task> FindClientsAsync (str var from = new IPEndPoint(IPAddress.Any, 0); - List cassettesFound = new List(); - List>> interfacesTasks = new List>>(); + var interfacesTasks = new List>>(); var usableInterfaces = GetUseableNetInterfaces(); @@ -56,10 +56,21 @@ public static async Task> FindClientsAsync (str //run the interface querys var grouped = await Task.WhenAll(interfacesTasks); - foreach (var item in grouped) - cassettesFound.AddRange(item); + var decomposed = new List(); - return cassettesFound; + foreach (var grp in grouped) { + + foreach (var cassette in grp) { + + if (decomposed.Any(x => x.MacAddress.SequenceEqual(cassette.MacAddress))) continue; + + decomposed.Add(cassette); + + } + + } + + return decomposed; } diff --git a/MewtocolNet/CpuInfo.cs b/MewtocolNet/CpuInfo.cs deleted file mode 100644 index 273e636..0000000 --- a/MewtocolNet/CpuInfo.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; - -namespace MewtocolNet { - - /// - /// Contains information about the plc and its cpu - /// - public struct CpuInfo { - - /// - /// The cpu type of the plc - /// - public CpuType Cputype { get; set; } - - /// - /// Program capacity in 1K steps - /// - public int ProgramCapacity { get; set; } - - /// - /// Version of the cpu - /// - public string CpuVersion { get; set; } - - internal static CpuInfo BuildFromHexString(string _cpuType, string _cpuVersion, string _progCapacity) { - - CpuInfo retInf = new CpuInfo(); - - retInf.ProgramCapacity = Convert.ToInt32(_progCapacity); - retInf.CpuVersion = _cpuVersion.Insert(1, "."); - return retInf; - - } - - } - -} \ No newline at end of file diff --git a/MewtocolNet/Helpers/LinqHelpers.cs b/MewtocolNet/Helpers/LinqHelpers.cs new file mode 100644 index 0000000..03d9595 --- /dev/null +++ b/MewtocolNet/Helpers/LinqHelpers.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.Helpers { + + internal static class LinqHelpers { + + internal static IEnumerable DistinctBy(this IEnumerable source, Func keySelector) { + return DistinctBy(source, keySelector, null); + } + + internal static IEnumerable DistinctBy + (this IEnumerable source, Func keySelector, IEqualityComparer comparer) { + + if (source == null) throw new ArgumentNullException(nameof(source)); + if (keySelector == null) throw new ArgumentNullException(nameof(keySelector)); + + return _(); IEnumerable _() { + var knownKeys = new HashSet(comparer); + foreach (var element in source) { + if (knownKeys.Add(keySelector(element))) + yield return element; + } + } + + } + + } + +} diff --git a/MewtocolNet/Helpers/MewtocolHelpers.cs b/MewtocolNet/Helpers/MewtocolHelpers.cs index 33e692e..0633e98 100644 --- a/MewtocolNet/Helpers/MewtocolHelpers.cs +++ b/MewtocolNet/Helpers/MewtocolHelpers.cs @@ -323,7 +323,9 @@ internal static bool CompareIsNameDuplicate(this IRegisterInternal reg1, IRegist /// public static string ToName(this PlcType plcT) { - return string.Join(" or ", ParsedPlcName.LegacyPlcDeconstruct(plcT).Select(x => x.WholeName)); + if (plcT == 0) return "Unknown"; + + return string.Join(" or ", ParsedPlcName.PlcDeconstruct(plcT).Select(x => x.WholeName)); } @@ -332,7 +334,9 @@ public static string ToName(this PlcType plcT) { /// public static ParsedPlcName[] ToNameDecompose (this PlcType legacyT) { - return ParsedPlcName.LegacyPlcDeconstruct(legacyT); + if ((int)legacyT == 0) return Array.Empty(); + + return ParsedPlcName.PlcDeconstruct(legacyT); } diff --git a/MewtocolNet/Helpers/ParsedPlcName.cs b/MewtocolNet/Helpers/ParsedPlcName.cs index d4d266c..c1e9ea2 100644 --- a/MewtocolNet/Helpers/ParsedPlcName.cs +++ b/MewtocolNet/Helpers/ParsedPlcName.cs @@ -35,9 +35,9 @@ public struct ParsedPlcName { /// public override string ToString() => WholeName; - internal static ParsedPlcName[] LegacyPlcDeconstruct (T legacyT) { + internal static ParsedPlcName[] PlcDeconstruct (PlcType plcT) { - string wholeStr = legacyT.ToString(); + string wholeStr = plcT.ToString(); var split = wholeStr.Replace("_OR_", "|").Split('|'); var reg = new Regex(@"(?[A-Za-z0-9]*)_(?[A-Za-z0-9]*)(?:__)?(?.*)"); diff --git a/MewtocolNet/IPlc.cs b/MewtocolNet/IPlc.cs index 6bc1c1b..1386b93 100644 --- a/MewtocolNet/IPlc.cs +++ b/MewtocolNet/IPlc.cs @@ -83,7 +83,7 @@ public interface IPlc : IDisposable { /// Use this to await the first poll iteration after connecting, /// This also completes if the initial connection fails /// - Task AwaitFirstDataAsync(); + Task AwaitFirstDataCycleAsync(); /// /// Runs a single poller cycle manually, diff --git a/MewtocolNet/Logging/LoggerEnums.cs b/MewtocolNet/Logging/LoggerEnums.cs index a845218..6bd2d19 100644 --- a/MewtocolNet/Logging/LoggerEnums.cs +++ b/MewtocolNet/Logging/LoggerEnums.cs @@ -5,6 +5,10 @@ /// public enum LogLevel { + /// + /// Logs nothing + /// + None = -1, /// /// Logs only errors /// diff --git a/MewtocolNet/MewtocolFrameResponse.cs b/MewtocolNet/MewtocolFrameResponse.cs index a381336..0ff4547 100644 --- a/MewtocolNet/MewtocolFrameResponse.cs +++ b/MewtocolNet/MewtocolFrameResponse.cs @@ -14,6 +14,10 @@ public struct MewtocolFrameResponse { public string Error { get; private set; } + public static MewtocolFrameResponse Timeout => new MewtocolFrameResponse(403, "Request timed out"); + + public static MewtocolFrameResponse NotIntialized => new MewtocolFrameResponse(405, "PLC was not initialized"); + public MewtocolFrameResponse (string response) { Success = true; @@ -41,6 +45,16 @@ public MewtocolFrameResponse(int errorCode, string exceptionMsg) { } + /// + public static bool operator == (MewtocolFrameResponse c1, MewtocolFrameResponse c2) { + return c1.Equals(c2); + } + + /// + public static bool operator != (MewtocolFrameResponse c1, MewtocolFrameResponse c2) { + return !c1.Equals(c2); + } + } } diff --git a/MewtocolNet/MewtocolInterface.cs b/MewtocolNet/MewtocolInterface.cs index eec4492..3522607 100644 --- a/MewtocolNet/MewtocolInterface.cs +++ b/MewtocolNet/MewtocolInterface.cs @@ -159,7 +159,7 @@ void MewtocolInterface_Connected(PLCInfo obj) { public virtual async Task ConnectAsync() => throw new NotImplementedException(); /// - public async Task AwaitFirstDataAsync() => await firstPollTask; + public async Task AwaitFirstDataCycleAsync() => await firstPollTask; /// public void Disconnect() { @@ -196,7 +196,7 @@ public async Task SendCommandAsync(string _msg, bool with if (await Task.WhenAny(tempResponse, Task.Delay(timeoutMs)) != tempResponse) { // timeout logic - return new MewtocolFrameResponse(403, "Timed out"); + return MewtocolFrameResponse.Timeout; } tcpMessagesSentThisCycle++; @@ -210,7 +210,7 @@ private protected async Task SendFrameAsync (string frame try { - if (stream == null) return new MewtocolFrameResponse(405, "PLC not initialized"); + if (stream == null) return MewtocolFrameResponse.NotIntialized; if (useBcc) frame = $"{frame.BuildBCCFrame()}"; diff --git a/MewtocolNet/MewtocolInterfaceRequests.cs b/MewtocolNet/MewtocolInterfaceRequests.cs index 9775b1d..409071a 100644 --- a/MewtocolNet/MewtocolInterfaceRequests.cs +++ b/MewtocolNet/MewtocolInterfaceRequests.cs @@ -1,3 +1,4 @@ +using MewtocolNet.Exceptions; using MewtocolNet.Logging; using MewtocolNet.Registers; using System; @@ -21,18 +22,42 @@ public partial class MewtocolInterface { /// A PLCInfo class public async Task GetPLCInfoAsync(int timeout = -1) { - var regexRT = new Regex(@"\%EE\$RT(?..)(?..)(?..)(?..)..(?..)(?....).*", RegexOptions.IgnoreCase); + var resRT = await SendCommandAsync("%EE#RT", timeoutMs: timeout); - var regexEXRT = new Regex(@"\%EE\$EX00RT00(?..)(?..)..(?..)(?..)..(?..)(?....)(?..)(?..)(?.)(?....)(?....)(?....).*", RegexOptions.IgnoreCase); + if (!resRT.Success) { - var resRT = await SendCommandAsync("%EE#RT", timeoutMs: timeout); - if (!resRT.Success) return null; + //timeouts are ok and dont throw + if (resRT == MewtocolFrameResponse.Timeout) return null; + + throw new MewtocolException(resRT.Error); + + } var resEXRT = await SendCommandAsync("%EE#EX00RT00", timeoutMs: timeout); + //timeouts are ok and dont throw + if (!resRT.Success && resRT == MewtocolFrameResponse.Timeout) return null; + + PLCInfo plcInf; + + //dont overwrite, use first + if (!PLCInfo.TryFromRT(resRT.Response, out plcInf)) { + + throw new MewtocolException("The RT message could not be parsed"); + + } + + //overwrite first with EXRT + if (resEXRT.Success && !plcInf.TryExtendFromEXRT(resEXRT.Response)) { + + throw new MewtocolException("The EXRT message could not be parsed"); + + } + + PlcInfo = plcInf; + + return plcInf; - return null; - } #endregion diff --git a/MewtocolNet/MewtocolNet.csproj b/MewtocolNet/MewtocolNet.csproj index 2bb9118..c9ade86 100644 --- a/MewtocolNet/MewtocolNet.csproj +++ b/MewtocolNet/MewtocolNet.csproj @@ -1,6 +1,6 @@  - netstandard2.0;net6.0; + netstandard2.0; Mewtocol.NET 0.7.1 Felix Weiss diff --git a/MewtocolNet/PLCInfo.cs b/MewtocolNet/PLCInfo.cs index 939bd59..5334306 100644 --- a/MewtocolNet/PLCInfo.cs +++ b/MewtocolNet/PLCInfo.cs @@ -1,66 +1,111 @@ -namespace MewtocolNet { +using MewtocolNet.PublicEnums; +using System.Globalization; +using System.Text.RegularExpressions; +namespace MewtocolNet { - public enum CpuType { - - - - } - + /// + /// Holds various informations about the PLC + /// public struct PLCInfo { /// - /// Current error code of the PLC + /// The type of the PLC named by Panasonic /// - public string ErrorCode { get; internal set; } + public PlcType TypeCode { get; private set; } /// - /// Current station number of the PLC + /// Contains information about the PLCs operation modes as flags /// - public int StationNumber { get; internal set; } - - - } - - /// - /// Contains generic information about the plc - /// - public class PLCInfoOld - { + public OPMode OperationMode { get; private set; } /// - /// Contains information about the PLCs cpu + /// Hardware information flags about the PLC /// - public CpuInfo CpuInformation { get; set; } + public HWInformation HardwareInformation { get; private set; } + /// - /// Contains information about the PLCs operation modes + /// Program capacity in 1K steps /// - public PLCMode OperationMode { get; set; } + public int ProgramCapacity { get; private set; } /// - /// Current error code of the PLC + /// Version of the cpu /// - public string ErrorCode { get; set; } + public string CpuVersion { get; private set; } /// - /// Current station number of the PLC + /// Current error code of the PLC /// - public int StationNumber { get; set; } + public string SelfDiagnosticError { get; internal set; } + + internal bool TryExtendFromEXRT (string msg) { + + var regexEXRT = new Regex(@"\%EE\$EX00RT00(?..)(?..)..(?..)(?..)..(?..)(?....)(?..)(?..)(?.)(?....)(?....)(?....).*", RegexOptions.IgnoreCase); + var match = regexEXRT.Match(msg); + if(match.Success) { + + byte typeCodeByte = byte.Parse(match.Groups["mc"].Value, NumberStyles.HexNumber); + + this.TypeCode = (PlcType)typeCodeByte; + this.CpuVersion = match.Groups["ver"].Value; + this.HardwareInformation = (HWInformation)byte.Parse(match.Groups["hwif"].Value, NumberStyles.HexNumber); + + return true; + + } + + return false; + + } + + internal static bool TryFromRT (string msg, out PLCInfo inf) { + + var regexRT = new Regex(@"\%EE\$RT(?..)(?..)(?..)(?..)..(?..)(?....).*", RegexOptions.IgnoreCase); + var match = regexRT.Match(msg); + if (match.Success) { + + byte typeCodeByte = byte.Parse(match.Groups["cputype"].Value, NumberStyles.HexNumber); + + inf = new PLCInfo { + TypeCode = (PlcType)typeCodeByte, + CpuVersion = match.Groups["cpuver"].Value, + ProgramCapacity = int.Parse(match.Groups["cap"].Value), + SelfDiagnosticError = match.Groups["sdiag"].Value, + OperationMode = (OPMode)byte.Parse(match.Groups["op"].Value, NumberStyles.HexNumber), + }; + + return true; + + } + + inf = default(PLCInfo); + return false; + + } /// - /// Generates a string containing some of the most important informations + /// Plc info when its not connected /// - /// - 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}"; + public static PLCInfo None => new PLCInfo() { + + SelfDiagnosticError = "", + CpuVersion = "", + HardwareInformation = 0, + OperationMode = 0, + ProgramCapacity = 0, + TypeCode = 0, + + }; + + /// + public static bool operator == (PLCInfo c1, PLCInfo c2) { + return c1.Equals(c2); + } + /// + public static bool operator != (PLCInfo c1, PLCInfo c2) { + return !c1.Equals(c2); } } diff --git a/MewtocolNet/PLCMode.cs b/MewtocolNet/PLCMode.cs deleted file mode 100644 index 3662b2a..0000000 --- a/MewtocolNet/PLCMode.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; - -namespace MewtocolNet { - - /// - /// All modes - /// - public struct PLCMode { - - /// - /// PLC is running - /// - public bool RunMode { get; set; } - /// - /// PLC is in test - /// - public bool TestRunMode { get; set; } - /// - /// BreakExcecuting - /// - public bool BreakExcecuting { get; set; } - /// - /// BreakValid - /// - public bool BreakValid { get; set; } - /// - /// PLC output is enabled - /// - public bool OutputEnabled { get; set; } - /// - /// PLC runs step per step - /// - public bool StepRunMode { get; set; } - /// - /// Message executing - /// - public bool MessageExecuting { get; set; } - /// - /// PLC is in remote mode - /// - public bool RemoteMode { get; set; } - - /// - /// Gets operation mode from 2 digit hex number - /// - internal 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; - - } - - } - -} \ No newline at end of file diff --git a/MewtocolNet/PublicEnums/HWInformation.cs b/MewtocolNet/PublicEnums/HWInformation.cs new file mode 100644 index 0000000..6089eab --- /dev/null +++ b/MewtocolNet/PublicEnums/HWInformation.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.PublicEnums { + + /// + /// Contains hardware information about the device as flags + /// + [Flags] + public enum HWInformation : byte { + + /// + /// Has user ROM + /// + UserROM = 1, + /// + /// Has IC card + /// + ICCard = 2, + /// + /// Has general purpose memory + /// + GeneralPurposeMemory = 4, + /// + /// Is CPU ultra high speed type + /// + UltraHighSpeed = 8, + + } + + /// + /// Descibes the operation mode of the device as flags + /// + [Flags] + public enum OPMode : byte { + + /// + /// No operation mode flag active + /// + None = 0, + /// + /// Is in RUN mode, otherwise its PROG Mode + /// + RunMode = 1, + /// + /// Is in test mode, otherwise ok + /// + TestMode = 2, + /// + /// Is BRK/1 step executed + /// + BreakPointPerOneStep = 4, + /// + /// Is BRK command enabled + /// + BreakEnabled = 16, + /// + /// Is outputting to external device + /// + ExternalOutput = 32, + /// + /// Is 1 step exec enabled + /// + OneStepExecEnabled = 64, + /// + /// Is a message displayed? + /// + MessageInstructionDisplayed = 128, + /// + /// Is in remote mode + /// + RemoteMode = 255, + + } + +} From d95652553850fa63966c684b444074e6020f8b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Thu, 6 Jul 2023 22:20:01 +0200 Subject: [PATCH 16/18] Removed old examples - added instant register update if write was successful --- DocBuilder/DocBuilder.csproj | 4 + Examples/ExampleScenarios.cs | 347 ------------------ Examples/Examples.csproj | 12 - Examples/Program.cs | 134 ------- Examples/ScenarioAttribute.cs | 15 - Examples/TestRegisters.cs | 80 ---- Examples/TestRegistersEnumBitwise.cs | 106 ------ MewExplorer/MewExplorer.csproj | 7 +- MewTerminal/Commands/ScanCommand.cs | 2 +- MewTerminal/MewTerminal.csproj | 17 +- MewtocolNet.sln | 16 +- MewtocolNet/MewtocolInterfaceRequests.cs | 6 +- MewtocolNet/MewtocolNet.csproj | 26 +- MewtocolNet/PLCInfo.cs | 14 +- MewtocolNet/PublicEnums/HWInformation.cs | 47 +-- MewtocolNet/PublicEnums/OPMode.cs | 49 ++- MewtocolNet/Registers/BoolRegister.cs | 4 +- MewtocolNet/Registers/BytesRegister.cs | 4 +- MewtocolNet/Registers/NumberRegister.cs | 4 +- MewtocolNet/Registers/StringRegister.cs | 8 +- .../ExpectedPlcInformationData.cs | 2 +- MewtocolTests/MewtocolTests.csproj | 11 +- MewtocolTests/TestLivePLC.cs | 10 +- build_order.md | 7 + 24 files changed, 135 insertions(+), 797 deletions(-) delete mode 100644 Examples/ExampleScenarios.cs delete mode 100644 Examples/Examples.csproj delete mode 100644 Examples/Program.cs delete mode 100644 Examples/ScenarioAttribute.cs delete mode 100644 Examples/TestRegisters.cs delete mode 100644 Examples/TestRegistersEnumBitwise.cs create mode 100644 build_order.md diff --git a/DocBuilder/DocBuilder.csproj b/DocBuilder/DocBuilder.csproj index 70b2327..c2b9011 100644 --- a/DocBuilder/DocBuilder.csproj +++ b/DocBuilder/DocBuilder.csproj @@ -1,10 +1,14 @@ + + false + false Exe net6.0 enable enable + diff --git a/Examples/ExampleScenarios.cs b/Examples/ExampleScenarios.cs deleted file mode 100644 index 59d4bb5..0000000 --- a/Examples/ExampleScenarios.cs +++ /dev/null @@ -1,347 +0,0 @@ -using MewtocolNet.Logging; -using MewtocolNet; -using System; -using System.Reflection; -using System.Threading.Tasks; -using System.Collections; -using MewtocolNet.RegisterBuilding; -using System.Collections.Generic; -using MewtocolNet.Registers; -using System.Diagnostics; -using System.Text; -using Microsoft.Win32; -using MewtocolNet.ComCassette; -using System.Linq; -using System.Net; -using System.IO.Ports; - -namespace Examples; - -public class ExampleScenarios { - - public void SetupLogger () { - - //attaching the logger - Logger.LogLevel = LogLevel.Info; - Logger.OnNewLogMessage((date, level, msg) => { - - if (level == LogLevel.Error) Console.ForegroundColor = ConsoleColor.Red; - - Console.WriteLine($"{date.ToString("HH:mm:ss")} {msg}"); - - Console.ResetColor(); - - }); - - } - - [Scenario("Dispose and disconnect connection")] - public async Task RunDisposalAndDisconnectAsync () { - - //automatic disposal - using (var interf = Mewtocol.Ethernet("192.168.115.210")) { - - await interf.ConnectAsync(); - - if (interf.IsConnected) { - - Console.WriteLine("Opened connection"); - - await Task.Delay(5000); - - } - - } - - Console.WriteLine("Disposed, closed connection"); - - //manual close - var interf2 = Mewtocol.Ethernet("192.168.115.210"); - - await interf2.ConnectAsync(); - - if (interf2.IsConnected) { - - Console.WriteLine("Opened connection"); - - await Task.Delay(5000); - - } - - interf2.Disconnect(); - - Console.WriteLine("Disconnected, closed connection"); - - } - - [Scenario("Read all kinds of example registers over ethernet")] - public async Task RunReadTestEth () { - - //setting up a new PLC interface and register collection - var interf = Mewtocol.Ethernet("192.168.115.210").WithPoller(); - - await RunCyclicReadTest(interf); - - } - - [Scenario("Read all kinds of example registers over serial")] - public async Task RunReadTestSer () { - - //setting up a new PLC interface and register collection - var interf = Mewtocol.SerialAuto("COM4").WithPoller(); - - await RunCyclicReadTest(interf); - - } - - private async Task RunCyclicReadTest (IPlc interf) { - - //auto add all built registers to the interface - var builder = RegBuilder.ForInterface(interf); - var r0reg = builder.FromPlcRegName("R0").Build(); - builder.FromPlcRegName("R1", "Testname").Build(); - builder.FromPlcRegName("R1F").Build(); - builder.FromPlcRegName("R101A").Build(); - - var shortReg = builder.FromPlcRegName("DT35").AsPlcType(PlcVarType.INT).Build(); - builder.FromPlcRegName("DDT36").AsPlcType(PlcVarType.DINT).Build(); - builder.FromPlcRegName("DT200").AsBytes(30).Build(); - - var timeReg = builder.FromPlcRegName("DDT38").AsPlcType(PlcVarType.TIME).Build(); - var stringReg = builder.FromPlcRegName("DT40").AsPlcType(PlcVarType.STRING).Build(); - - //connect - if(interf is IPlcSerial serialPlc) { - - await serialPlc.ConnectAsync(() => { - - Console.WriteLine($"Trying config: {serialPlc.ConnectionInfo}"); - - }); - - } else { - - await interf.ConnectAsync(); - - } - - //await first register data - await interf.AwaitFirstDataCycleAsync(); - - _ = Task.Factory.StartNew(async () => { - - void setTitle() { - - Console.Title = - $"Speed UP: {interf.BytesPerSecondUpstream} B/s, " + - $"Speed DOWN: {interf.BytesPerSecondDownstream} B/s, " + - $"Poll cycle: {interf.PollerCycleDurationMs} ms, " + - $"Queued MSGs: {interf.QueuedMessages}"; - - } - - while (interf.IsConnected) { - setTitle(); - await Task.Delay(1000); - } - - setTitle(); - - }); - - while (interf.IsConnected) { - - var sw = Stopwatch.StartNew(); - - //set bool - await r0reg.WriteAsync(!(bool)r0reg.Value); - - //set random num - await shortReg.WriteAsync((short)new Random().Next(0, 100)); - await stringReg.WriteAsync($"_{DateTime.Now.Second}s_"); - - sw.Stop(); - - foreach (var reg in interf.Registers) - Console.WriteLine(reg.ToString()); - - Console.WriteLine($"Total write time for registers: {sw.Elapsed.TotalMilliseconds:N0}ms"); - - Console.WriteLine(); - - //await Task.Delay(new Random().Next(0, 10000)); - await Task.Delay(1000); - - } - - } - - [Scenario("Test read speed TCP (n) R registers")] - public async Task ReadRSpeedTest (string registerCount) { - - var preLogLevel = Logger.LogLevel; - Logger.LogLevel = LogLevel.Critical; - - //setting up a new PLC interface and register collection - using var interf = Mewtocol.Ethernet("192.168.115.210"); - - //auto add all built registers to the interface - var builder = RegBuilder.ForInterface(interf); - for (int i = 0; i < int.Parse(registerCount); i++) { - - builder.FromPlcRegName($"R{i}A").Build(); - - } - - //connect - await interf.ConnectAsync(); - - if(!interf.IsConnected) { - Console.WriteLine("Aborted, connection failed"); - return; - } - - Console.WriteLine("Poller cycle started"); - var sw = Stopwatch.StartNew(); - - int cmdCount = await interf.RunPollerCylceManual(); - - sw.Stop(); - - Console.WriteLine("Poller cycle finished"); - - Console.WriteLine($"Single frame excec time: {sw.ElapsedMilliseconds:N0}ms for {cmdCount} commands and {interf.Registers.Count()} R registers"); - - await Task.Delay(1000); - - } - - [Scenario("Test read speed Serial (n) R registers")] - public async Task ReadRSpeedTestSerial (string registerCount) { - - var preLogLevel = Logger.LogLevel; - Logger.LogLevel = LogLevel.Critical; - - //setting up a new PLC interface and register collection - //MewtocolInterfaceShared interf = Mewtocol.SerialAuto("COM4"); - using var interf = Mewtocol.Serial("COM4", BaudRate._115200, DataBits.Eight, Parity.Odd, StopBits.One); - - //auto add all built registers to the interface - var builder = RegBuilder.ForInterface(interf); - for (int i = 0; i < int.Parse(registerCount); i++) { - - builder.FromPlcRegName($"R{i}A").Build(); - - } - - //connect - await interf.ConnectAsync(); - - if (!interf.IsConnected) { - Console.WriteLine("Aborted, connection failed"); - return; - } - - Console.WriteLine("Poller cycle started"); - var sw = Stopwatch.StartNew(); - - int cmdCount = await interf.RunPollerCylceManual(); - - sw.Stop(); - - Console.WriteLine("Poller cycle finished"); - - Console.WriteLine($"Single frame excec time: {sw.ElapsedMilliseconds:N0}ms for {cmdCount} commands and {interf.Registers.Count()} R registers"); - - } - - [Scenario("Test automatic serial port setup")] - public async Task TestAutoSerialSetup () { - - var preLogLevel = Logger.LogLevel; - Logger.LogLevel = LogLevel.Critical; - - //setting up a new PLC interface and register collection - var interf = Mewtocol.SerialAuto("COM4"); - - //connect - await interf.ConnectAsync(); - - if (!interf.IsConnected) { - - Console.WriteLine("Aborted, connection failed"); - return; - - } else { - - Console.WriteLine("Serial port settings found"); - - } - - - } - - [Scenario("Find all COM5 cassettes in the network")] - public async Task FindCassettes () { - - Console.Clear(); - - var casettes = await CassetteFinder.FindClientsAsync(); - - foreach (var cassette in casettes) { - - Console.WriteLine($"{cassette.Name}"); - Console.WriteLine($"IP: {cassette.IPAddress}"); - Console.WriteLine($"Port: {cassette.Port}"); - Console.WriteLine($"DHCP: {cassette.UsesDHCP}"); - Console.WriteLine($"Subnet Mask: {cassette.SubnetMask}"); - Console.WriteLine($"Gateway: {cassette.GatewayAddress}"); - Console.WriteLine($"Mac: {cassette.MacAddress.ToHexString(":")}"); - Console.WriteLine($"Firmware: {cassette.FirmwareVersion}"); - Console.WriteLine($"Status: {cassette.Status}"); - Console.WriteLine($"Endpoint: {cassette.EndpointName} - {cassette.Endpoint.Address}"); - Console.WriteLine(); - - } - - var found = casettes.FirstOrDefault(x => x.Endpoint.Address.ToString() == "10.237.191.75"); - - if (found == null) return; - - found.IPAddress = IPAddress.Parse($"192.168.1.{new Random().Next(20, 120)}"); - found.Name = $"Rand{new Random().Next(5, 15)}"; - - await found.SendNewConfigAsync(); - - } - - [Scenario("Test")] - public async Task Test () { - - Logger.LogLevel = LogLevel.Critical; - - //fpx c14 r - var plxFpx = Mewtocol.Ethernet("192.168.178.55"); - await plxFpx.ConnectAsync(); - await ((MewtocolInterface)plxFpx).GetSystemRegister(); - - //fpx-h c30 t - var plcFpxH = Mewtocol.Ethernet("192.168.115.210"); - await plcFpxH.ConnectAsync(); - await ((MewtocolInterface)plcFpxH).GetSystemRegister(); - - //fpx-h c14 r - var plcFpxHc14 = Mewtocol.Ethernet("192.168.115.212"); - await plcFpxHc14.ConnectAsync(); - await ((MewtocolInterface)plcFpxHc14).GetSystemRegister(); - - //fpx c30 t - var plcFpxc30T = Mewtocol.Ethernet("192.168.115.213"); - await plcFpxc30T.ConnectAsync(); - await ((MewtocolInterface)plcFpxc30T).GetSystemRegister(); - - await Task.Delay(-1); - - } - -} diff --git a/Examples/Examples.csproj b/Examples/Examples.csproj deleted file mode 100644 index b9f9fac..0000000 --- a/Examples/Examples.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - Exe - net6.0 - - - diff --git a/Examples/Program.cs b/Examples/Program.cs deleted file mode 100644 index d925120..0000000 --- a/Examples/Program.cs +++ /dev/null @@ -1,134 +0,0 @@ -using MewtocolNet.RegisterBuilding; -using MewtocolNet; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using MewtocolNet.Logging; -using System.Text.RegularExpressions; -using System.Globalization; -using System.Threading; - -namespace Examples; - -class Program { - - static ExampleScenarios ExampleSzenarios = new ExampleScenarios(); - - static void Main(string[] args) { - - Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us"); - - Console.Clear(); - - AppDomain.CurrentDomain.UnhandledException += (s,e) => { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Uncatched exception: {e.ExceptionObject.ToString()}"); - Console.ResetColor(); - }; - - //TaskScheduler.UnobservedTaskException += (s,e) => { - // Console.ForegroundColor = ConsoleColor.Magenta; - // Console.WriteLine($"Unobserved Task Uncatched exception: {e.Exception.ToString()}"); - // Console.ResetColor(); - //}; - - ExampleSzenarios.SetupLogger(); - - LoopInput(); - - } - - private static void LoopInput () { - - Console.WriteLine("All available scenarios\n"); - - var methods = ExampleSzenarios.GetType().GetMethods(); - var invokeableMethods = new List(); - - for (int i = 0, j = 0; i < methods.Length; i++) { - - MethodInfo method = methods[i]; - var foundAtt = method.GetCustomAttribute(typeof(ScenarioAttribute)); - - if(foundAtt != null && foundAtt is ScenarioAttribute att) { - - string paramsStr = string.Join(" ", method.GetParameters().Select(x => x.Name)); - Console.WriteLine($"[{j + 1}] {method.Name}({paramsStr}) - {att.Description}"); - invokeableMethods.Add(method); - - j++; - - } - - } - - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("\nEnter a number to excecute a example"); - Console.ResetColor(); - - Console.WriteLine("\nOther possible commands: \n"); - Console.WriteLine($"'logger ' - set loglevel to one of: {string.Join(", ", Enum.GetNames(typeof(LogLevel)).ToList())}"); - Console.WriteLine("'exit' - to close this program"); - Console.WriteLine("'clear' - to clear the output"); - - - Console.Write("> "); - - var line = Console.ReadLine(); - - var loggerMatch = Regex.Match(line, @"logger (?[a-zA-Z]{0,})"); - var splitInput = Regex.Split(line, " "); - - - if (loggerMatch.Success && Enum.TryParse(loggerMatch.Groups["level"].Value, out var loglevel)) { - - Logger.LogLevel = loglevel; - - Console.WriteLine($"Loglevel changed to: {loglevel}"); - - } else if (line == "exit") { - - Environment.Exit(0); - - } else if (line == "clear") { - - Console.Clear(); - - } else if (int.TryParse(splitInput[0], out var lineNum)) { - - var index = Math.Clamp(lineNum - 1, 0, invokeableMethods.Count - 1); - - object[] invParams = null; - - if(splitInput.Length > 1) { - invParams = splitInput.Skip(1).Cast().ToArray(); - } - - try { - - var task = (Task)invokeableMethods.ElementAt(index).Invoke(ExampleSzenarios, invParams); - task.Wait(); - - } catch (TargetParameterCountException) { - - Console.WriteLine("Missing parameters"); - - } - - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("The program ran to completition"); - Console.ResetColor(); - - } else { - - Console.WriteLine("Wrong input"); - - } - - LoopInput(); - - } - -} diff --git a/Examples/ScenarioAttribute.cs b/Examples/ScenarioAttribute.cs deleted file mode 100644 index 5a869c2..0000000 --- a/Examples/ScenarioAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Examples; - -public class ScenarioAttribute : Attribute { - - public string Description { get; private set; } - - public ScenarioAttribute(string description) { - - Description = description; - - } - -} \ No newline at end of file diff --git a/Examples/TestRegisters.cs b/Examples/TestRegisters.cs deleted file mode 100644 index c2bbe9a..0000000 --- a/Examples/TestRegisters.cs +++ /dev/null @@ -1,80 +0,0 @@ -using MewtocolNet; -using MewtocolNet.RegisterAttributes; -using System; -using System.Collections; - -namespace Examples { - public class TestRegisters : RegisterCollection { - - //corresponds to a R100 boolean register in the PLC - [Register(IOType.R, 1000)] - public bool TestBool1 { get; private set; } - - private int testDuplicate; - - [Register(1000)] - public int TestDuplicate { - get => testDuplicate; - set => AutoSetter(value, ref testDuplicate); - } - - //corresponds to a XD input of the PLC - [Register(IOType.X, (byte)0xD)] - 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(899)] - 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, BitCount.B16, 9)] - public bool BitValue { get; private set; } - - [Register(1204, BitCount.B16, 5)] - public bool FillTest { get; private set; } - - //corresponds to a DT7012 - DT7013 as a 32bit time value that gets parsed as a timespan (TIME) - //the smallest value to communicate to the PLC is 10ms - [Register(7012)] - public TimeSpan TestTime { get; private set; } - - public enum CurrentState { - Undefined = 0, - State1 = 1, - State2 = 2, - //State3 = 3, - State4 = 4, - State5 = 5, - StateBetween = 100, - State6 = 6, - State7 = 7, - } - - [Register(50)] - public CurrentState TestEnum { get; private set; } - - [Register(100)] - public TimeSpan TsTest2 { get; private set; } - - } -} diff --git a/Examples/TestRegistersEnumBitwise.cs b/Examples/TestRegistersEnumBitwise.cs deleted file mode 100644 index a3990b8..0000000 --- a/Examples/TestRegistersEnumBitwise.cs +++ /dev/null @@ -1,106 +0,0 @@ -using MewtocolNet; -using MewtocolNet.RegisterAttributes; -using System; -using System.Collections; - -namespace Examples { - - public class TestRegistersEnumBitwise : RegisterCollection { - - private bool startCyclePLC; - - [Register(IOType.R, 50)] - public bool StartCyclePLC { - get => startCyclePLC; - set => AutoSetter(value, ref startCyclePLC); - } - - //the enum you want to read out - public enum CurrentState { - - Undefined = 0, - State1 = 1, - State2 = 2, - //If you leave an enum empty it still works - //State3 = 3, - State4 = 4, - State5 = 5, - State6 = 6, - State7 = 7, - State8 = 8, - State9 = 9, - State10 = 10, - State11 = 11, - State12 = 12, - State13 = 13, - State14 = 14, - State15 = 15, - - } - - //automatically convert the short (PLC int) register to an enum - [Register(500)] - public CurrentState TestEnum16 { get; private set; } - - //also works for 32bit registers - [Register(501, BitCount.B32)] - public CurrentState TestEnum32 { get; private set; } - - //get the whole bit array from DT503 - - [Register(503)] - public BitArray TestBitRegister16 { get; private set; } - - //you can also extract single bits from DT503 - - [Register(503, BitCount.B16, 0)] - public bool BitValue0 { get; private set; } - - [Register(503, BitCount.B16, 1)] - public bool BitValue1 { get; private set; } - - [Register(503, BitCount.B16, 2)] - public bool BitValue2 { get; private set; } - - [Register(503, BitCount.B16, 3)] - public bool BitValue3 { get; private set; } - - [Register(503, BitCount.B16, 4)] - public bool BitValue4 { get; private set; } - - [Register(503, BitCount.B16, 5)] - public bool BitValue5 { get; private set; } - - [Register(503, BitCount.B16, 6)] - public bool BitValue6 { get; private set; } - - [Register(503, BitCount.B16, 7)] - public bool BitValue7 { get; private set; } - - [Register(503, BitCount.B16, 8)] - public bool BitValue8 { get; private set; } - - [Register(503, BitCount.B16, 9)] - public bool BitValue9 { get; private set; } - - [Register(503, BitCount.B16, 10)] - public bool BitValue10 { get; private set; } - - [Register(503, BitCount.B16, 11)] - public bool BitValue11 { get; private set; } - - [Register(503, BitCount.B16, 12)] - public bool BitValue12 { get; private set; } - - [Register(503, BitCount.B16, 13)] - public bool BitValue13 { get; private set; } - - [Register(503, BitCount.B16, 14)] - public bool BitValue14 { get; private set; } - - [Register(503, BitCount.B16, 15)] - public bool BitValue15 { get; private set; } - - } - -} diff --git a/MewExplorer/MewExplorer.csproj b/MewExplorer/MewExplorer.csproj index 9a8e14e..a21d09b 100644 --- a/MewExplorer/MewExplorer.csproj +++ b/MewExplorer/MewExplorer.csproj @@ -1,10 +1,11 @@  - net7.0-android; + + false + + net7.0-android; $(TargetFrameworks);net7.0-windows10.0.19041.0 - - Exe MewExplorer true diff --git a/MewTerminal/Commands/ScanCommand.cs b/MewTerminal/Commands/ScanCommand.cs index 697766b..d197c68 100644 --- a/MewTerminal/Commands/ScanCommand.cs +++ b/MewTerminal/Commands/ScanCommand.cs @@ -72,7 +72,7 @@ await AnsiConsole.Status() AnsiConsole.Write(found.Select(x => new { x.Cassette.Name, PLC = x.PLCInf.TypeCode.ToName(), - IsRun = x.PLCInf.OperationMode.HasFlag(OPMode.Run), + IsRun = x.PLCInf.OperationMode.HasFlag(OPMode.RunMode), IP = x.Cassette.IPAddress, x.Cassette.Port, DHCP = x.Cassette.UsesDHCP, diff --git a/MewTerminal/MewTerminal.csproj b/MewTerminal/MewTerminal.csproj index e75817e..417a0cf 100644 --- a/MewTerminal/MewTerminal.csproj +++ b/MewTerminal/MewTerminal.csproj @@ -1,10 +1,11 @@ - + Exe net7.0 enable enable + true @@ -16,4 +17,18 @@ + + + x64 + win-x64 + True + None + False + false + none + en + ..\Builds\MewTerminal + + + diff --git a/MewtocolNet.sln b/MewtocolNet.sln index 5c0ee6e..8a20ae2 100644 --- a/MewtocolNet.sln +++ b/MewtocolNet.sln @@ -5,15 +5,13 @@ VisualStudioVersion = 17.5.33103.201 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewtocolNet", "MewtocolNet\MewtocolNet.csproj", "{8B7863E7-5E82-4990-9138-2C0C24629982}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples", "Examples\Examples.csproj", "{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewtocolTests", "MewtocolTests\MewtocolTests.csproj", "{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewExplorer", "MewExplorer\MewExplorer.csproj", "{F243F38A-76D3-4C38-BAE6-C61729479661}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocBuilder", "DocBuilder\DocBuilder.csproj", "{50F2D23F-C875-4006-A0B6-7F5A181BC944}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MewTerminal", "MewTerminal\MewTerminal.csproj", "{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewTerminal", "MewTerminal\MewTerminal.csproj", "{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -37,18 +35,6 @@ Global {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 {C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Debug|Any CPU.Build.0 = Debug|Any CPU {C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Debug|x64.ActiveCfg = Debug|Any CPU diff --git a/MewtocolNet/MewtocolInterfaceRequests.cs b/MewtocolNet/MewtocolInterfaceRequests.cs index 409071a..94015f3 100644 --- a/MewtocolNet/MewtocolInterfaceRequests.cs +++ b/MewtocolNet/MewtocolInterfaceRequests.cs @@ -69,15 +69,15 @@ public partial class MewtocolInterface { /// /// The mode to change to /// The success state of the write operation - public async Task SetOperationMode(OPMode mode) { + public async Task SetOperationMode (bool setRun) { - string modeChar = mode == OPMode.Prog ? "P" : "R"; + string modeChar = setRun ? "R" : "P"; string requeststring = $"%{GetStationNumber()}#RM{modeChar}"; var result = await SendCommandAsync(requeststring); if (result.Success) { - Logger.Log($"operation mode was changed to {mode}", LogLevel.Info, this); + Logger.Log($"operation mode was changed to {(setRun ? "Run" : "Prog")}", LogLevel.Info, this); } else { Logger.Log("Operation mode change failed", LogLevel.Error, this); } diff --git a/MewtocolNet/MewtocolNet.csproj b/MewtocolNet/MewtocolNet.csproj index c9ade86..89d2219 100644 --- a/MewtocolNet/MewtocolNet.csproj +++ b/MewtocolNet/MewtocolNet.csproj @@ -1,8 +1,12 @@  + - netstandard2.0; + + false + + netstandard2.0 Mewtocol.NET - 0.7.1 + 0.0.0 Felix Weiss Womed true @@ -13,22 +17,28 @@ plc;panasonic;mewtocol;automation; MIT 2ccdcc9b-94a3-4e76-8827-453ab889ea33 + - - ..\Builds\MewtocolNet.xml - ..\Builds + + + ..\Builds\MewtocolNet\MewtocolNet.xml + ..\Builds\MewtocolNet - + + <_Parameter1>MewtocolTests - + + <_Parameter1>DocBuilder - + + + diff --git a/MewtocolNet/PLCInfo.cs b/MewtocolNet/PLCInfo.cs index 5334306..ce3ae79 100644 --- a/MewtocolNet/PLCInfo.cs +++ b/MewtocolNet/PLCInfo.cs @@ -1,5 +1,4 @@ -using MewtocolNet.PublicEnums; -using System.Globalization; +using System.Globalization; using System.Text.RegularExpressions; namespace MewtocolNet { @@ -39,6 +38,11 @@ public struct PLCInfo { /// public string SelfDiagnosticError { get; internal set; } + /// + /// Quickcheck for the runmode flag + /// + public bool IsRunMode => OperationMode.HasFlag(OPMode.RunMode); + internal bool TryExtendFromEXRT (string msg) { var regexEXRT = new Regex(@"\%EE\$EX00RT00(?..)(?..)..(?..)(?..)..(?..)(?....)(?..)(?..)(?.)(?....)(?....)(?....).*", RegexOptions.IgnoreCase); @@ -108,6 +112,12 @@ internal static bool TryFromRT (string msg, out PLCInfo inf) { return !c1.Equals(c2); } + public override string ToString() { + + return $"{TypeCode.ToName()}, OP: {OperationMode}"; + + } + } } \ No newline at end of file diff --git a/MewtocolNet/PublicEnums/HWInformation.cs b/MewtocolNet/PublicEnums/HWInformation.cs index 6089eab..2f9b06d 100644 --- a/MewtocolNet/PublicEnums/HWInformation.cs +++ b/MewtocolNet/PublicEnums/HWInformation.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace MewtocolNet.PublicEnums { +namespace MewtocolNet { /// /// Contains hardware information about the device as flags @@ -29,49 +29,4 @@ public enum HWInformation : byte { } - /// - /// Descibes the operation mode of the device as flags - /// - [Flags] - public enum OPMode : byte { - - /// - /// No operation mode flag active - /// - None = 0, - /// - /// Is in RUN mode, otherwise its PROG Mode - /// - RunMode = 1, - /// - /// Is in test mode, otherwise ok - /// - TestMode = 2, - /// - /// Is BRK/1 step executed - /// - BreakPointPerOneStep = 4, - /// - /// Is BRK command enabled - /// - BreakEnabled = 16, - /// - /// Is outputting to external device - /// - ExternalOutput = 32, - /// - /// Is 1 step exec enabled - /// - OneStepExecEnabled = 64, - /// - /// Is a message displayed? - /// - MessageInstructionDisplayed = 128, - /// - /// Is in remote mode - /// - RemoteMode = 255, - - } - } diff --git a/MewtocolNet/PublicEnums/OPMode.cs b/MewtocolNet/PublicEnums/OPMode.cs index e760230..73a8467 100644 --- a/MewtocolNet/PublicEnums/OPMode.cs +++ b/MewtocolNet/PublicEnums/OPMode.cs @@ -1,17 +1,50 @@ -namespace MewtocolNet { +using System; + +namespace MewtocolNet { /// - /// CPU type of the PLC + /// Descibes the operation mode of the device as flags /// - public enum OPMode { + [Flags] + public enum OPMode : byte { + /// - /// PLC run mode + /// No operation mode flag active /// - Run, + None = 0, /// - /// PLC programming mode + /// Is in RUN mode, otherwise its PROG Mode /// - Prog, + RunMode = 1, + /// + /// Is in test mode, otherwise ok + /// + TestMode = 2, + /// + /// Is BRK/1 step executed + /// + BreakPointPerOneStep = 4, + /// + /// Is BRK command enabled + /// + BreakEnabled = 16, + /// + /// Is outputting to external device + /// + ExternalOutput = 32, + /// + /// Is 1 step exec enabled + /// + OneStepExecEnabled = 64, + /// + /// Is a message displayed? + /// + MessageInstructionDisplayed = 128, + /// + /// Is in remote mode + /// + RemoteMode = 255, + } -} \ No newline at end of file +} diff --git a/MewtocolNet/Registers/BoolRegister.cs b/MewtocolNet/Registers/BoolRegister.cs index a8f14aa..cf1af8c 100644 --- a/MewtocolNet/Registers/BoolRegister.cs +++ b/MewtocolNet/Registers/BoolRegister.cs @@ -78,7 +78,9 @@ public override async Task WriteAsync(object data) { if (!attachedInterface.IsConnected) return false; - return await attachedInterface.WriteRawRegisterAsync(this, PlcValueParser.Encode(this, (bool)data)); + var res = await attachedInterface.WriteRawRegisterAsync(this, PlcValueParser.Encode(this, (bool)data)); + if (res) SetValueFromPLC(data); + return res; } diff --git a/MewtocolNet/Registers/BytesRegister.cs b/MewtocolNet/Registers/BytesRegister.cs index c6c102e..bd2483f 100644 --- a/MewtocolNet/Registers/BytesRegister.cs +++ b/MewtocolNet/Registers/BytesRegister.cs @@ -89,7 +89,9 @@ public override async Task WriteAsync(object data) { if (!attachedInterface.IsConnected) return false; - return await attachedInterface.WriteRawRegisterAsync(this, (byte[])data); + var res = await attachedInterface.WriteRawRegisterAsync(this, (byte[])data); + if (res) SetValueFromPLC(data); + return res; } diff --git a/MewtocolNet/Registers/NumberRegister.cs b/MewtocolNet/Registers/NumberRegister.cs index 207d7de..2f8a4ab 100644 --- a/MewtocolNet/Registers/NumberRegister.cs +++ b/MewtocolNet/Registers/NumberRegister.cs @@ -178,7 +178,9 @@ public override async Task WriteAsync(object data) { if (!attachedInterface.IsConnected) return false; - return await attachedInterface.WriteRawRegisterAsync(this, PlcValueParser.Encode(this, (T)data)); + var res = await attachedInterface.WriteRawRegisterAsync(this, PlcValueParser.Encode(this, (T)data)); + if (res) SetValueFromPLC(data); + return res; } diff --git a/MewtocolNet/Registers/StringRegister.cs b/MewtocolNet/Registers/StringRegister.cs index 44fb5ed..9147049 100644 --- a/MewtocolNet/Registers/StringRegister.cs +++ b/MewtocolNet/Registers/StringRegister.cs @@ -97,9 +97,11 @@ public override async Task WriteAsync(object data) { if (!attachedInterface.IsConnected) return false; var res = await attachedInterface.WriteRawRegisterAsync(this, PlcValueParser.Encode(this, (string)data)); - - if (res) UsedSize = (short)((string)Value).Length; - + if (res) { + UsedSize = (short)((string)Value).Length; + SetValueFromPLC(data); + } + return res; } diff --git a/MewtocolTests/EncapsulatedTests/ExpectedPlcInformationData.cs b/MewtocolTests/EncapsulatedTests/ExpectedPlcInformationData.cs index 2a97059..0f35705 100644 --- a/MewtocolTests/EncapsulatedTests/ExpectedPlcInformationData.cs +++ b/MewtocolTests/EncapsulatedTests/ExpectedPlcInformationData.cs @@ -10,7 +10,7 @@ public class ExpectedPlcInformationData { public int PLCPort { get; set; } - public CpuType Type { get; set; } + public PlcType Type { get; set; } public int ProgCapacity { get; set; } diff --git a/MewtocolTests/MewtocolTests.csproj b/MewtocolTests/MewtocolTests.csproj index 9d3002f..7c619ff 100644 --- a/MewtocolTests/MewtocolTests.csproj +++ b/MewtocolTests/MewtocolTests.csproj @@ -1,13 +1,16 @@ - net6.0 + + false + false + + net6.0 enable enable - - false + - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/MewtocolTests/TestLivePLC.cs b/MewtocolTests/TestLivePLC.cs index a8bdaef..1a94956 100644 --- a/MewtocolTests/TestLivePLC.cs +++ b/MewtocolTests/TestLivePLC.cs @@ -20,7 +20,7 @@ public class TestLivePLC { PLCName = "FPX-H C30T", PLCIP = "192.168.115.210", PLCPort = 9094, - Type = CpuType.FP_Sigma_X_H_30K_60K_120K, + Type = PlcType.FPdXH_32k__C30TsP_C40T_C60TsP, ProgCapacity = 32, }, @@ -29,7 +29,7 @@ public class TestLivePLC { PLCName = "FPX-H C14R", PLCIP = "192.168.115.212", PLCPort = 9094, - Type = CpuType.FP_Sigma_X_H_30K_60K_120K, + Type = PlcType.FPdXH_16k__C14R, ProgCapacity = 16, }, @@ -57,7 +57,7 @@ public TestLivePLC(ITestOutputHelper output) { this.output = output; - Logger.LogLevel = LogLevel.Verbose; + Logger.LogLevel = LogLevel.Critical; Logger.OnNewLogMessage((d, l, m) => { output.WriteLine($"Mewtocol Logger: {d} {m}"); @@ -102,8 +102,8 @@ public async void TestClientReadPLCStatus() { Assert.True(client.IsConnected); - Assert.Equal(client.PlcInfo.CpuInformation.Cputype, plc.Type); - Assert.Equal(client.PlcInfo.CpuInformation.ProgramCapacity, plc.ProgCapacity); + Assert.Equal(client.PlcInfo.TypeCode, plc.Type); + Assert.Equal(client.PlcInfo.ProgramCapacity, plc.ProgCapacity); client.Disconnect(); diff --git a/build_order.md b/build_order.md new file mode 100644 index 0000000..656465c --- /dev/null +++ b/build_order.md @@ -0,0 +1,7 @@ +## 1. Run the tests + +`dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=./TestResults/coverage.opencover.xml` + +## 2. Publish + +`dotnet publish -c:Release --property WarningLevel=0` \ No newline at end of file From 953e22503ab59aab226248ff8cb3c3e795679bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Thu, 6 Jul 2023 22:46:17 +0200 Subject: [PATCH 17/18] Added new logic for test pipeline --- .github/workflows/test-pipeline.yml | 61 ++++++++--------------------- build_order.md | 12 +++++- 2 files changed, 26 insertions(+), 47 deletions(-) diff --git a/.github/workflows/test-pipeline.yml b/.github/workflows/test-pipeline.yml index 59ea4a4..0f8db5b 100644 --- a/.github/workflows/test-pipeline.yml +++ b/.github/workflows/test-pipeline.yml @@ -9,30 +9,15 @@ on: required: false type: string push: - branches: - - main - - master paths-ignore: - '**.md' permissions: write-all jobs: - - #Check the online status of the test PLCs first - check-plcs-online: - name: 'Test PLC online status' - runs-on: [self-hosted, linux, x64, womed-local-linux] - steps: - - name: 'Ping FPX-H-C30T' - run: ping 192.168.115.210 -w 5 - - name: 'Ping FPX-H-C14R' - run: ping 192.168.115.212 -w 5 - - name: 'Ping FPX-C30T' - run: ping 192.168.115.213 -w 5 #Run unit tests on the test PLCs run-unit-tests: - name: 'Run unit tests' + name: 'Run tests and documentation' needs: check-plcs-online runs-on: [self-hosted, linux, x64, womed-local-linux] steps: @@ -44,58 +29,44 @@ jobs: dotnet-version: '6.0.x' - name: 'Run tests' - run: | - cd './MewtocolTests' - dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=./TestResults/coverage.opencover.xml - cd ../ + run: dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=../Builds/TestResults/coverage.opencover.xml - name: Report Generator uses: danielpalme/ReportGenerator-GitHub-Action@5.1.22 with: - reports: './MewtocolTests/TestResults/coverage.opencover.xml' - targetdir: './MewtocolTests/TestResults' + reports: './Builds/TestResults/coverage.opencover.xml' + targetdir: './Builds/TestResults' reporttypes: HtmlSummary;MarkdownSummaryGithub;Badges - historydir: './MewtocolTests/Hist' + historydir: './Builds/Hist' title: Report - name: Markdown report and copy for badges branch run: | - cat './MewtocolTests/TestResults/SummaryGithub.md' >> $GITHUB_STEP_SUMMARY - cp ./MewtocolTests/TestResults/badge_combined.svg ~/badge_combined.svg - cp ./MewtocolTests/TestResults/summary.html ~/summary.html + cat './Builds/TestResults/SummaryGithub.md' >> $GITHUB_STEP_SUMMARY + cp ./Builds/TestResults/badge_combined.svg ~/badge_combined.svg + cp ./Builds/TestResults/summary.html ~/summary.html ls -l ~ - - #Upload to codecov - # - name: Upload coverage reports to Codecov - # uses: codecov/codecov-action@v3 - # env: - # CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - #- uses: actions/upload-artifact@v3 - # with: - # name: test-results - # path: ./MewtocolTests/TestResults/ - + - name: Cache test results if: ${{ github.event_name == 'workflow_call' }} uses: actions/cache/save@v3 with: - key: ${{ inputs.cache-id }} + key: ${{ inputs.cache-id }}-${{ GITHUB_REF##*/ }} path: | - ${{ github.workspace }}/MewtocolTests/TestResults + ${{ github.workspace }}/Builds/TestResults - name: Commit badge continue-on-error: true run: | git fetch git checkout badges - cp ~/summary.html ./MewtocolTests/TestResults/summary.html - cp ~/badge_combined.svg ./MewtocolTests/TestResults/badge_combined.svg + cp ~/summary.html ./Builds/TestResults/summary_${{ GITHUB_REF##*/ }}.html + cp ~/badge_combined.svg ./Builds/TestResults/badge_combined_${{ GITHUB_REF##*/ }}.svg git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - git add "./MewtocolTests/TestResults/badge_combined.svg" -f - git add "./MewtocolTests/TestResults/summary.html" -f - git commit -m "Add/Update badge" + git add "./Builds/TestResults/badge_combined_${{ GITHUB_REF##*/ }}.svg" -f + git add "./Builds/TestResults/summary_${{ GITHUB_REF##*/ }}.html" -f + git commit -m "Add/Update badge for branch ${{ GITHUB_REF##*/ }}" - name: Push badge commit uses: ad-m/github-push-action@master diff --git a/build_order.md b/build_order.md index 656465c..9cbbabe 100644 --- a/build_order.md +++ b/build_order.md @@ -1,7 +1,15 @@ +# On commit pipeline + ## 1. Run the tests -`dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=./TestResults/coverage.opencover.xml` +`dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=../Builds/TestResults/coverage.opencover.xml` + +## 2. Run the docs Autobuilder + +`dotnet run --project "./DocBuilder/DocBuilder.csproj"` + +# On publish pipeline -## 2. Publish +## 3. Publish `dotnet publish -c:Release --property WarningLevel=0` \ No newline at end of file From fe3c849934db002cf0f892346312fc27c60d819a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Thu, 6 Jul 2023 22:49:04 +0200 Subject: [PATCH 18/18] Fixed needs in pipeline --- .github/workflows/test-pipeline.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test-pipeline.yml b/.github/workflows/test-pipeline.yml index 0f8db5b..8f2bf9b 100644 --- a/.github/workflows/test-pipeline.yml +++ b/.github/workflows/test-pipeline.yml @@ -18,7 +18,6 @@ jobs: #Run unit tests on the test PLCs run-unit-tests: name: 'Run tests and documentation' - needs: check-plcs-online runs-on: [self-hosted, linux, x64, womed-local-linux] steps: - uses: actions/checkout@v3